import { Injectable } from "@nestjs/common"; import { PrismaService } from "../prisma/prisma.service"; import type { ParsedPolicy } from "./parsers/policy-parser"; export interface MatchResult { policyId: string | null; customerId: string | null; /** Why it landed here — shown in the review queue verbatim. */ note: string; /** True only for an unambiguous hit on `Policy.policyNumber`. */ confident: boolean; /** * Every policy that carries the parsed number, with its customer. >1 means * the policy number is shared across customers and a human must pick. */ candidates: { policyId: string; customerId: string; customerName: string; policyNumber: string }[]; } /** * Resolves a parsed policy page to an existing Policy (and its customer) the * office already holds. * * **Match on `Policy.policyNumber` alone, never on the printed insured name.** * The certificate's "Insured" line is the account's registrant, which drifts * from the current owner — the same problem the statement matcher cites for * utility bills ("ARNAIZ ROSAS ELSA AURORA" on a CESPT receipt for a * customer this office holds as "CATT, RANDY"). Names are surfaced for the * reviewer to sanity-check and never feed matching. * * A policy number that matches zero rows means the policy is new: the * review screen then offers a customer picker and the confirm step creates * the row. Multiple hits are surfaced rather than auto-picked — duplicate * policy numbers across customers do occur (same group policy bound by two * related parties), and picking one arbitrarily would silently book the * wrong coverage. */ @Injectable() export class PolicyMatcherService { constructor(private readonly prisma: PrismaService) {} async match(parsed: ParsedPolicy): Promise { if (!parsed.policyNumber) { return this.unmatched("no se pudo leer el número de póliza"); } const rows = await this.prisma.policy.findMany({ where: { policyNumber: parsed.policyNumber }, select: { id: true, policyNumber: true, customerId: true, customer: { select: { name: true } }, }, }); const candidates = rows.map((r) => ({ policyId: r.id, customerId: r.customerId, customerName: r.customer.name, policyNumber: r.policyNumber, })); if (rows.length === 0) { return { policyId: null, customerId: null, note: `no se encontró ninguna póliza con el número ${parsed.policyNumber}`, confident: false, candidates: [], }; } if (rows.length > 1) { return { policyId: null, customerId: null, note: `${rows.length} pólizas comparten el número ${parsed.policyNumber}`, confident: false, candidates, }; } return { policyId: candidates[0].policyId, customerId: candidates[0].customerId, note: `coincidencia exacta por número de póliza ${parsed.policyNumber}`, confident: true, candidates, }; } private unmatched(note: string): MatchResult { return { policyId: null, customerId: null, note, confident: false, candidates: [], }; } }