The office books customers surname-first ("WAGONER, PAMELA") and carriers
print them given-name-first ("PAMELA DENISE WAGONER"), so the review screen
made staff retype a name the parser had already read. Comparing normalized
token sets makes the two orderings the same thing.
Only on the zero-hit path, where the policy number found nothing and a human
has to pick a customer anyway. The suggestions are written to a new
`customerSuggestions` column rather than `matchCandidates`, which the review
screen reads as policy-number hits, and they never set `matchedCustomerId` or
`confident` — matching on `Policy.policyNumber` is unchanged.
Two tiers, drawn where the real book has cliffs: EXACT (identical token sets)
and PARTIAL (containment, >=2 shared tokens, surname present). Of 1536
customers, 1487 have a distinct token set, so EXACT cross-person collisions
are ~0; loosen to surname + first given name and 131 (8.5%) collide, and 185
surnames are shared by 524 customers, which is why one token is never enough
and the surname must be printed explicitly. Replaying every book row as a
carrier would print it: 97.9% top-ranked correct, 1.2% a different row, all
but two of those the same human on a duplicate or variant row.
Normalization folds accents (OCR's MUNOZ reaches the book's MUÑOZ), drops
initials, Spanish particles, JR/S.A. DE C.V., and any token with a digit —
ANA prints the phone hard against the name as `Ph.3102001538`. Names over 8
tokens or 80 characters are refused outright, because GMX's especificación
has no field labels and the parser has handed its whole first page over as
`insuredName`.
Not used for utility statements: there the registrant genuinely is not the
customer, so the same trick would be wrong rather than noisy.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
101 lines
3.2 KiB
TypeScript
101 lines
3.2 KiB
TypeScript
import { PolicyMatcherService } from "./policy-matcher.service";
|
|
import type { PrismaService } from "../prisma/prisma.service";
|
|
import type { ParsedPolicy } from "./parsers/policy-parser";
|
|
|
|
function parsed(over: Partial<ParsedPolicy> = {}): ParsedPolicy {
|
|
return {
|
|
provider: "GMX",
|
|
policyNumber: null,
|
|
insuredName: null,
|
|
notes: [],
|
|
coverages: [],
|
|
vehicles: [],
|
|
drivers: [],
|
|
...over,
|
|
} as unknown as ParsedPolicy;
|
|
}
|
|
|
|
function prismaStub(policies: unknown[], customers: { id: string; name: string }[]) {
|
|
const findManyPolicy = jest.fn().mockResolvedValue(policies);
|
|
const findManyCustomer = jest.fn().mockResolvedValue(customers);
|
|
return {
|
|
prisma: {
|
|
policy: { findMany: findManyPolicy },
|
|
customer: { findMany: findManyCustomer },
|
|
} as unknown as PrismaService,
|
|
findManyPolicy,
|
|
findManyCustomer,
|
|
};
|
|
}
|
|
|
|
const BOOK = [
|
|
{ id: "cust-1", name: "WAGONER, PAMELA" },
|
|
{ id: "cust-2", name: "SMITH, JOHN" },
|
|
];
|
|
|
|
describe("PolicyMatcherService name suggestions", () => {
|
|
it("suggests a customer when the policy number is new", async () => {
|
|
const { prisma } = prismaStub([], BOOK);
|
|
const svc = new PolicyMatcherService(prisma);
|
|
|
|
const r = await svc.match(
|
|
parsed({ policyNumber: "P-999", insuredName: "PAMELA DENISE WAGONER" } as never),
|
|
);
|
|
|
|
expect(r.customerSuggestions).toEqual([
|
|
expect.objectContaining({ customerId: "cust-1", tier: "PARTIAL" }),
|
|
]);
|
|
// The suggestion is surfaced, never applied.
|
|
expect(r.customerId).toBeNull();
|
|
expect(r.confident).toBe(false);
|
|
expect(r.note).toContain("posibles clientes por nombre: WAGONER, PAMELA");
|
|
});
|
|
|
|
it("suggests when the policy number could not be read at all", async () => {
|
|
const { prisma } = prismaStub([], BOOK);
|
|
const svc = new PolicyMatcherService(prisma);
|
|
|
|
const r = await svc.match(parsed({ insuredName: "PAMELA WAGONER" } as never));
|
|
|
|
expect(r.customerSuggestions[0]).toMatchObject({ customerId: "cust-1", tier: "EXACT" });
|
|
expect(r.customerId).toBeNull();
|
|
expect(r.note).toBe(
|
|
"no se pudo leer el número de póliza; posible cliente por nombre: WAGONER, PAMELA",
|
|
);
|
|
});
|
|
|
|
it("does not touch the book when the policy number hits", async () => {
|
|
const { prisma, findManyCustomer } = prismaStub(
|
|
[
|
|
{
|
|
id: "pol-1",
|
|
policyNumber: "P-1",
|
|
customerId: "cust-2",
|
|
customer: { name: "SMITH, JOHN" },
|
|
},
|
|
],
|
|
BOOK,
|
|
);
|
|
const svc = new PolicyMatcherService(prisma);
|
|
|
|
const r = await svc.match(
|
|
parsed({ policyNumber: "P-1", insuredName: "PAMELA WAGONER" } as never),
|
|
);
|
|
|
|
expect(r.confident).toBe(true);
|
|
expect(r.customerId).toBe("cust-2");
|
|
expect(r.customerSuggestions).toEqual([]);
|
|
expect(findManyCustomer).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("reads the customer book once across a batch", async () => {
|
|
const { prisma, findManyCustomer } = prismaStub([], BOOK);
|
|
const svc = new PolicyMatcherService(prisma);
|
|
|
|
await svc.match(parsed({ policyNumber: "A", insuredName: "PAMELA WAGONER" } as never));
|
|
await svc.match(parsed({ policyNumber: "B", insuredName: "JOHN SMITH" } as never));
|
|
|
|
expect(findManyCustomer).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|