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; } }