feat(polizas): OCR capture for insurance policy PDFs
Mirrors the utility statement intake on the insurance side: a policy_ocr batch/document pair of tables, a GMX parser, a matcher keyed on Policy.policyNumber, and a "Captura" screen under /polizas that proposes policy -> customer for staff to confirm. Lifts the OCR seam out of StatementsModule into its own OcrModule so PolicyOcrModule can inject OCR_PROVIDER without taking on the rest of the statement pipeline; StatementsModule now imports it and binds nothing itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -49,6 +49,12 @@ import type {
|
||||
PolicySort,
|
||||
PolicyStats,
|
||||
PolicyStatus,
|
||||
PolicyOcrBatch,
|
||||
PolicyOcrBatchDetail,
|
||||
PolicyOcrDocument,
|
||||
PolicyOcrReviewInput,
|
||||
PolicyOcrConfirmInput,
|
||||
PolicyOcrConfirmResult,
|
||||
LookupsResponse,
|
||||
OpsJob,
|
||||
OpsJobKind,
|
||||
@@ -1077,3 +1083,96 @@ export function confirmStatementBatch(
|
||||
export function statementPageUrl(documentId: string): string {
|
||||
return `${API_ORIGIN}/statements/documents/${documentId}/page`;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------- Policy OCR (GMX) */
|
||||
|
||||
export function getPolicyOcrStatus(): Promise<{
|
||||
ocrAvailable: boolean;
|
||||
storageAvailable: boolean;
|
||||
}> {
|
||||
return apiFetch("/policy-ocr/status");
|
||||
}
|
||||
|
||||
export function listPolicyOcrBatches(
|
||||
page = 1,
|
||||
pageSize = 25,
|
||||
): Promise<{
|
||||
items: PolicyOcrBatch[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
pageCount: number;
|
||||
}> {
|
||||
return apiFetch(`/policy-ocr/batches?page=${page}&pageSize=${pageSize}`);
|
||||
}
|
||||
|
||||
export function getPolicyOcrBatch(id: string): Promise<PolicyOcrBatchDetail> {
|
||||
return apiFetch(`/policy-ocr/batches/${id}`);
|
||||
}
|
||||
|
||||
export function listPolicyOcrDocuments(batchId: string): Promise<PolicyOcrDocument[]> {
|
||||
return apiFetch(`/policy-ocr/batches/${batchId}/documents`);
|
||||
}
|
||||
|
||||
export async function uploadPolicyOcrBatch(
|
||||
files: File[],
|
||||
label?: string,
|
||||
): Promise<PolicyOcrBatch> {
|
||||
const body = new FormData();
|
||||
for (const f of files) body.append("files", f, f.name);
|
||||
const qs = new URLSearchParams();
|
||||
if (label) qs.set("label", label);
|
||||
|
||||
const res = await fetch(
|
||||
`${API_ORIGIN}/policy-ocr/batches${qs.toString() ? `?${qs}` : ""}`,
|
||||
{
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body,
|
||||
},
|
||||
);
|
||||
if (!res.ok) {
|
||||
let message = `Error ${res.status}`;
|
||||
try {
|
||||
const b = await res.json();
|
||||
if (b?.message) message = b.message;
|
||||
} catch {
|
||||
/* non-JSON error body */
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export function reviewPolicyOcrDocument(
|
||||
id: string,
|
||||
input: PolicyOcrReviewInput,
|
||||
): Promise<PolicyOcrDocument> {
|
||||
return apiFetch(`/policy-ocr/documents/${id}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
export function rejectPolicyOcrDocument(id: string): Promise<PolicyOcrDocument> {
|
||||
return apiFetch(`/policy-ocr/documents/${id}/reject`, { method: "POST" });
|
||||
}
|
||||
|
||||
export function confirmPolicyOcrBatch(
|
||||
batchId: string,
|
||||
input: PolicyOcrConfirmInput,
|
||||
): Promise<PolicyOcrConfirmResult> {
|
||||
return apiFetch(`/policy-ocr/batches/${batchId}/confirm`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* URL for the source PDF of a parsed policy document. The endpoint returns
|
||||
* the original upload (one PDF = one parsed policy), not a rendered page
|
||||
* image, so the review screen embeds it in an iframe.
|
||||
*/
|
||||
export function policyOcrDocumentUrl(documentId: string): string {
|
||||
return `${API_ORIGIN}/policy-ocr/documents/${documentId}/page`;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ export type Ability =
|
||||
| "policy:create"
|
||||
| "policy:update"
|
||||
| "policy:delete"
|
||||
| "policy:ingest"
|
||||
| "policy:ocr-review"
|
||||
| "property:create"
|
||||
| "property:update"
|
||||
| "property:delete"
|
||||
@@ -1293,3 +1295,140 @@ export interface ConfirmBatchResult {
|
||||
total: string;
|
||||
checkNumber: string;
|
||||
}
|
||||
|
||||
/* ------------------------------------------ Policy OCR intake (GMX) */
|
||||
|
||||
export type PolicyOcrBatchStatus =
|
||||
| "UPLOADED"
|
||||
| "PROCESSING"
|
||||
| "READY_FOR_REVIEW"
|
||||
| "COMPLETED"
|
||||
| "FAILED";
|
||||
|
||||
export type PolicyOcrDocumentStatus =
|
||||
| "PENDING_OCR"
|
||||
| "OCR_FAILED"
|
||||
| "NEEDS_REVIEW"
|
||||
| "MATCHED"
|
||||
| "CONFIRMED"
|
||||
| "POSTED"
|
||||
| "REJECTED";
|
||||
|
||||
export interface PolicyOcrBatch {
|
||||
id: string;
|
||||
provider: string;
|
||||
status: PolicyOcrBatchStatus;
|
||||
label: string | null;
|
||||
fileCount: number;
|
||||
error: string | null;
|
||||
createdAt: string;
|
||||
completedAt: string | null;
|
||||
uploadedBy?: { name: string };
|
||||
_count?: { documents: number };
|
||||
}
|
||||
|
||||
export interface PolicyOcrBatchDetail extends PolicyOcrBatch {
|
||||
byStatus: Partial<Record<PolicyOcrDocumentStatus, number>>;
|
||||
}
|
||||
|
||||
export interface PolicyOcrCoverage {
|
||||
risk: string;
|
||||
insuredAmount: number | null;
|
||||
deductible: string | null;
|
||||
lossParticipation: string | null;
|
||||
}
|
||||
|
||||
export interface PolicyOcrMatchCandidate {
|
||||
policyId: string;
|
||||
customerId: string;
|
||||
customerName: string;
|
||||
policyNumber: string;
|
||||
}
|
||||
|
||||
export interface PolicyOcrDocument {
|
||||
id: string;
|
||||
pageNumber: number;
|
||||
status: PolicyOcrDocumentStatus;
|
||||
provider: string | null;
|
||||
ocrConfidence: string | null;
|
||||
extractedPolicyNumber: string | null;
|
||||
extractedInsuredName: string | null;
|
||||
extractedAdditionalInsured: string | null;
|
||||
extractedAgentName: string | null;
|
||||
extractedLegalAddress: string | null;
|
||||
extractedZip: string | null;
|
||||
extractedPolicyFrom: string | null;
|
||||
extractedPolicyTo: string | null;
|
||||
extractedPolicyDate: string | null;
|
||||
extractedCurrency: string | null;
|
||||
extractedNetPremium: string | null;
|
||||
extractedPolicyFee: string | null;
|
||||
extractedBrokerFee: string | null;
|
||||
extractedTotal: string | null;
|
||||
extractedCoveragesJson: PolicyOcrCoverage[] | null;
|
||||
extractedPremiumPayment: string | null;
|
||||
matchedPolicy: {
|
||||
id: string;
|
||||
policyNumber: string | null;
|
||||
customerId: string;
|
||||
customer: { name: string };
|
||||
} | null;
|
||||
matchedCustomer: { id: string; name: string } | null;
|
||||
matchCandidates: PolicyOcrMatchCandidate[] | null;
|
||||
matchNote: string | null;
|
||||
}
|
||||
|
||||
export interface PolicyOcrReviewInput {
|
||||
policyNumber?: string;
|
||||
insuredName?: string;
|
||||
additionalInsured?: string;
|
||||
agentName?: string;
|
||||
legalAddress?: string;
|
||||
zip?: string;
|
||||
policyFrom?: string;
|
||||
policyTo?: string;
|
||||
policyDate?: string;
|
||||
currency?: string;
|
||||
netPremium?: number;
|
||||
policyFee?: number;
|
||||
brokerFee?: number;
|
||||
total?: number;
|
||||
premiumPayment?: string;
|
||||
coveragesJson?: PolicyOcrCoverage[];
|
||||
matchedPolicyId?: string;
|
||||
matchedCustomerId?: string;
|
||||
forceConfirm?: boolean;
|
||||
}
|
||||
|
||||
export interface PolicyOcrConfirmDocument {
|
||||
documentId: string;
|
||||
policyId?: string;
|
||||
customerId?: string;
|
||||
policyNumber?: string;
|
||||
insuredName?: string;
|
||||
additionalInsured?: string;
|
||||
agentName?: string;
|
||||
legalAddress?: string;
|
||||
zip?: string;
|
||||
policyFrom?: string;
|
||||
policyTo?: string;
|
||||
policyDate?: string;
|
||||
currency?: string;
|
||||
netPremium?: number;
|
||||
policyFee?: number;
|
||||
brokerFee?: number;
|
||||
total?: number;
|
||||
premiumPayment?: string;
|
||||
coveragesJson?: PolicyOcrCoverage[];
|
||||
postPremium?: boolean;
|
||||
}
|
||||
|
||||
export interface PolicyOcrConfirmInput {
|
||||
documents: PolicyOcrConfirmDocument[];
|
||||
}
|
||||
|
||||
export interface PolicyOcrConfirmResult {
|
||||
applied: number;
|
||||
policies: string[];
|
||||
postedTransactions: number;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user