A bad scan, the wrong PDFs or a duplicate upload used to leave a batch sitting in READY_FOR_REVIEW forever, because the only exits were confirm (posts to the books) or rejecting every page one at a time. Add a DISCARDED terminal status to both OCR domains and a single endpoint per domain that rejects every page still pending in one shot. Discarding is refused once anything has landed: statements once a page is POSTED, policies once a page is APPLIED. Those batches did real work and have to be settled page by page. - POST /statements/batches/:id/discard - POST /policy-ocr/batches/:id/discard - shared DiscardBatchCard on both review screens, gated the same way
173 lines
5.1 KiB
TypeScript
173 lines
5.1 KiB
TypeScript
import {
|
|
Body,
|
|
Controller,
|
|
Get,
|
|
Param,
|
|
Patch,
|
|
Post,
|
|
Query,
|
|
Req,
|
|
Res,
|
|
StreamableFile,
|
|
UploadedFiles,
|
|
UseGuards,
|
|
UseInterceptors,
|
|
} from "@nestjs/common";
|
|
import { FilesInterceptor } from "@nestjs/platform-express";
|
|
import type { Request, Response } from "express";
|
|
import { AuthenticatedGuard } from "../auth/authenticated.guard";
|
|
import { AbilityGuard } from "../auth/ability.guard";
|
|
import { RequireAbility } from "../auth/require-ability.decorator";
|
|
import { AuditService } from "../common/audit.service";
|
|
import type { UploadedFileLike } from "../storage/upload-file";
|
|
import { PolicyOcrService } from "./policy-ocr.service";
|
|
import {
|
|
ConfirmPolicyBatchDto,
|
|
CreatePolicyOcrBatchDto,
|
|
ReviewPolicyDocumentDto,
|
|
} from "./policy-ocr.dto";
|
|
|
|
/**
|
|
* Insurance OCR intake (policy_ocr_intake).
|
|
*
|
|
* Mirrors StatementsController shape: one batch = one upload session of
|
|
* policy PDFs from a provider portal (GMX today), one document per page.
|
|
* Confirming a batch delegates nothing to a separate billing path —
|
|
* everything goes through `Policy` (and optionally a Transaction for the
|
|
* premium), the same tables the manual `PolicyForm` writes.
|
|
*/
|
|
@Controller("policy-ocr")
|
|
@UseGuards(AuthenticatedGuard, AbilityGuard)
|
|
export class PolicyOcrController {
|
|
constructor(
|
|
private readonly policyOcr: PolicyOcrService,
|
|
private readonly audit: AuditService,
|
|
) {}
|
|
|
|
private actingId(req: Request): string {
|
|
return (req.user as { id: string } | undefined)?.id ?? "";
|
|
}
|
|
|
|
@Get("status")
|
|
async status() {
|
|
return {
|
|
ocrAvailable: await this.policyOcr.ocrAvailable(),
|
|
storageAvailable: this.policyOcr.storageAvailable(),
|
|
};
|
|
}
|
|
|
|
@Get("batches")
|
|
listBatches(@Query("page") page?: string, @Query("pageSize") pageSize?: string) {
|
|
return this.policyOcr.listBatches(
|
|
Math.max(1, Number(page) || 1),
|
|
Math.min(100, Math.max(1, Number(pageSize) || 25)),
|
|
);
|
|
}
|
|
|
|
@Get("batches/:id")
|
|
getBatch(@Param("id") id: string) {
|
|
return this.policyOcr.getBatch(id);
|
|
}
|
|
|
|
@Get("batches/:id/documents")
|
|
listDocuments(@Param("id") id: string) {
|
|
return this.policyOcr.listDocuments(id);
|
|
}
|
|
|
|
/**
|
|
* The source PDF for a parsed policy document. One PDF = one parsed policy,
|
|
* so this returns the entire upload (typically multi-page for insurance
|
|
* certificates). The review screen embeds it in an iframe.
|
|
*/
|
|
@Get("documents/:id/page")
|
|
async pageImage(
|
|
@Param("id") id: string,
|
|
@Res({ passthrough: true }) res: Response,
|
|
) {
|
|
const { stream, contentType, contentLength } = await this.policyOcr.pageImage(id);
|
|
res.set({
|
|
// The doc row stores the source PDF, not a rendered page image.
|
|
"Content-Type": contentType ?? "application/pdf",
|
|
...(contentLength ? { "Content-Length": String(contentLength) } : {}),
|
|
});
|
|
return new StreamableFile(stream);
|
|
}
|
|
|
|
// --- writes ---------------------------------------------------------------
|
|
|
|
@Post("batches")
|
|
@RequireAbility("policy:ingest")
|
|
@UseInterceptors(
|
|
FilesInterceptor("files", 25, { limits: { fileSize: 50 * 1024 * 1024 } }),
|
|
)
|
|
async createBatch(
|
|
@UploadedFiles() files: UploadedFileLike[] | undefined,
|
|
@Body() _dto: CreatePolicyOcrBatchDto,
|
|
@Query("label") label: string | undefined,
|
|
@Req() req: Request,
|
|
) {
|
|
const batch = await this.policyOcr.createBatch(
|
|
files ?? [],
|
|
this.actingId(req),
|
|
label ?? _dto.label,
|
|
);
|
|
void this.audit.log(this.actingId(req), "policyOcr.batch.create", {
|
|
batchId: batch.id,
|
|
fileCount: batch.fileCount,
|
|
});
|
|
return batch;
|
|
}
|
|
|
|
@Patch("documents/:id")
|
|
@RequireAbility("policy:ocr-review")
|
|
async review(
|
|
@Param("id") id: string,
|
|
@Body() dto: ReviewPolicyDocumentDto,
|
|
@Req() req: Request,
|
|
) {
|
|
const doc = await this.policyOcr.review(id, dto, this.actingId(req));
|
|
void this.audit.log(this.actingId(req), "policyOcr.document.review", {
|
|
documentId: id,
|
|
status: doc.status,
|
|
});
|
|
return doc;
|
|
}
|
|
|
|
@Post("documents/:id/reject")
|
|
@RequireAbility("policy:ocr-review")
|
|
async reject(@Param("id") id: string, @Req() req: Request) {
|
|
const doc = await this.policyOcr.reject(id, this.actingId(req));
|
|
void this.audit.log(this.actingId(req), "policyOcr.document.reject", {
|
|
documentId: id,
|
|
});
|
|
return doc;
|
|
}
|
|
|
|
/** Abandon a batch pending review — rejects every unapplied page. */
|
|
@Post("batches/:id/discard")
|
|
@RequireAbility("policy:ocr-review")
|
|
async discard(@Param("id") id: string, @Req() req: Request) {
|
|
const result = await this.policyOcr.discardBatch(id, this.actingId(req));
|
|
void this.audit.log(this.actingId(req), "policyOcr.batch.discard", {
|
|
batchId: id,
|
|
rejected: result.rejected,
|
|
});
|
|
return result;
|
|
}
|
|
|
|
@Post("batches/:id/confirm")
|
|
@RequireAbility("policy:ocr-review")
|
|
async confirm(
|
|
@Param("id") id: string,
|
|
@Body() dto: ConfirmPolicyBatchDto,
|
|
@Req() req: Request,
|
|
) {
|
|
const result = await this.policyOcr.confirmBatch(id, dto, this.actingId(req));
|
|
void this.audit.log(this.actingId(req), "policyOcr.batch.confirm", {
|
|
batchId: id,
|
|
applied: result.applied,
|
|
postedTransactions: result.postedTransactions,
|
|
});
|
|
return result;
|
|
}
|
|
} |