Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions src/api/documentOcr.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
createDocumentOcrRun,
fetchDocumentOcrRun,
fetchLatestDocumentOcrRun,
reviewDocumentOcrRun,
} from './documentOcr'

function jsonResponse() {
return new Response(
JSON.stringify({
ocr_run_id: 'run-1',
document_id: 'document-1',
file_id: 'file-1',
document_type: 'ARC',
status: 'QUEUED',
result: null,
corrected_fields: {},
error_code: null,
reviewed_by: null,
review_reason: null,
created_at: '2026-08-09T00:00:00Z',
started_at: null,
completed_at: null,
reviewed_at: null,
updated_at: '2026-08-09T00:00:00Z',
version: 0,
already_requested: false,
}),
{ status: 200, headers: { 'Content-Type': 'application/json' } },
)
}

beforeEach(() => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(jsonResponse()))
})

afterEach(() => {
vi.unstubAllGlobals()
})

describe('document OCR API', () => {
it('starts an OCR run with an idempotency key', async () => {
await createDocumentOcrRun('document/1', 'ocr-request-1')

const [url, init] = vi.mocked(fetch).mock.calls[0]
expect(String(url)).toContain('/documents/document%2F1/ocr-runs')
expect(init?.method).toBe('POST')
expect(new Headers(init?.headers).get('Idempotency-Key')).toBe('ocr-request-1')
})

it('gets one OCR run', async () => {
await fetchDocumentOcrRun('document-1', 'run/1')

const [url, init] = vi.mocked(fetch).mock.calls[0]
expect(String(url)).toContain('/documents/document-1/ocr-runs/run%2F1')
expect(init?.method).toBeUndefined()
})

it('gets the latest OCR run for the document', async () => {
await fetchLatestDocumentOcrRun('document-1')

const [url] = vi.mocked(fetch).mock.calls[0]
expect(String(url)).toContain('/documents/document-1/ocr-runs/latest')
})

it('submits HR corrections and the latest version for review', async () => {
await reviewDocumentOcrRun('document-1', 'run-1', {
expected_version: 2,
decision: 'APPROVE',
corrected_fields: { stay_expiration_date: '2026-12-31' },
})

const [url, init] = vi.mocked(fetch).mock.calls[0]
expect(String(url)).toContain('/documents/document-1/ocr-runs/run-1/review')
expect(init?.method).toBe('POST')
expect(JSON.parse(init?.body as string)).toEqual({
expected_version: 2,
decision: 'APPROVE',
corrected_fields: { stay_expiration_date: '2026-12-31' },
})
})
})
80 changes: 80 additions & 0 deletions src/api/documentOcr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import type { DocumentType } from './documents'
import { apiFetch } from './client'

export type DocumentOcrStatus =
'QUEUED' | 'RUNNING' | 'READY_FOR_REVIEW' | 'REVIEW_REQUIRED' | 'APPROVED' | 'REJECTED' | 'FAILED'

export type DocumentOcrReviewDecision = 'APPROVE' | 'REJECT'

export interface DocumentOcrResult {
matched_template_id: number | null
document_side: 'FRONT' | 'BACK'
fields: Record<string, string>
field_confidences: Record<string, number>
review_reasons: string[]
}

export interface DocumentOcrRunResponse {
ocr_run_id: string
document_id: string
file_id: string
document_type: DocumentType
status: DocumentOcrStatus
result: DocumentOcrResult | null
corrected_fields: Record<string, string>
error_code: string | null
reviewed_by: string | null
review_reason: string | null
created_at: string
started_at: string | null
completed_at: string | null
reviewed_at: string | null
updated_at: string
version: number
already_requested: boolean
}

export interface DocumentOcrReviewBody {
expected_version: number
decision: DocumentOcrReviewDecision
reason?: string
corrected_fields: Record<string, string>
}

function ocrRunsPath(documentId: string, suffix = '') {
return `/documents/${encodeURIComponent(documentId)}/ocr-runs${suffix}`
}

export function createDocumentOcrRun(
documentId: string,
idempotencyKey: string,
): Promise<DocumentOcrRunResponse> {
return apiFetch<DocumentOcrRunResponse>(ocrRunsPath(documentId), {
method: 'POST',
headers: { 'Idempotency-Key': idempotencyKey },
})
}

export function fetchDocumentOcrRun(
documentId: string,
ocrRunId: string,
): Promise<DocumentOcrRunResponse> {
return apiFetch<DocumentOcrRunResponse>(
ocrRunsPath(documentId, `/${encodeURIComponent(ocrRunId)}`),
)
}

export function fetchLatestDocumentOcrRun(documentId: string): Promise<DocumentOcrRunResponse> {
return apiFetch<DocumentOcrRunResponse>(ocrRunsPath(documentId, '/latest'))
}

export function reviewDocumentOcrRun(
documentId: string,
ocrRunId: string,
body: DocumentOcrReviewBody,
): Promise<DocumentOcrRunResponse> {
return apiFetch<DocumentOcrRunResponse>(
ocrRunsPath(documentId, `/${encodeURIComponent(ocrRunId)}/review`),
{ method: 'POST', body: JSON.stringify(body) },
)
}
12 changes: 0 additions & 12 deletions src/pages/DocumentDetailPage/DocumentDetailPage.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -108,15 +108,3 @@
color: var(--brand-primary);
cursor: pointer;
}

.actionDock {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 16px;
padding: var(--fowoco-spacing-12) var(--fowoco-spacing-20);
background: var(--surface-default);
border: 1px solid var(--border-default);
border-radius: var(--fowoco-radius-10);
box-shadow: var(--shadow-sm);
}
Loading