The parsers are written against `pdftotext -layout`, and every policy-ocr
fixture is a verbatim excerpt of it. The runtime does not use it: it reads
`-bbox-layout` and rebuilds the page from word boxes, and that rebuild
collapsed all white space — no blank lines between blocks, one space
between columns. White space is the only thing marking a cell boundary on
these borderless forms, so the fixtures could not see any of it.
What it cost, on the GMX PVL especificación and the ANA driver's policy:
- `espectBlock` walks a wrapped cell until a blank line. With no blank
line it ran to the end of the page, so the insured's name came back as
the entire first page of the specification.
- `INSURED\s{2,}` and its siblings matched nothing, and the phone that
shares the name cell rode along with it ("PAMELA DENISE WAGONER
Ph.3102001538"), which matches no customer.
- `parseAnaDriverCoverages` splits SUM INSURED from PREMIUM by the
header's own column offsets. Without offsets, every premium was filed
as a sum insured.
So `toVisualRows` now emits a blank line where the reader sees one (a
vertical gap over 1.6 line heights — the two populations measure 0.3-1.1
and 2.1+, so the threshold sits in empty space) and pads each word to its
own column, using one space wherever words merely follow each other so
rounding drift cannot sprinkle false cell boundaries through prose.
Two independent guards, so neither failure can come back silently: the
ANA phone splits on a single space, and the especificación's cell walk is
capped at the one wrap the longest cell on that document actually uses.
Verified against the real PDFs: the especificación reads "EMMER .
KATHLEEN" with all 18 coverages named (they were "(sin nombre)"), and the
ten born-digital gas invoices parse byte-identically to before. The
scanned statements are untouched — they come through tesseract, not this
path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
920 lines
52 KiB
TypeScript
920 lines
52 KiB
TypeScript
import type { OcrPage } from "../../statements/ocr/ocr.provider";
|
||
import {
|
||
detectPolicyProvider,
|
||
parsePolicy,
|
||
type ParsedCoverage,
|
||
} from "./policy-parser";
|
||
|
||
/**
|
||
* Verbatim excerpts of what the GMX portal's translation PDF actually
|
||
* rendered through pdftotext — same convention as the statement parser
|
||
* tests, where invented-clean input would test nothing because clean input
|
||
* is not the failure mode.
|
||
*/
|
||
function page(text: string): OcrPage {
|
||
return { text, words: [], confidence: 0.95 };
|
||
}
|
||
|
||
/** Coverages keyed by their risk label, so an assertion names the coverage
|
||
* it is about instead of an array index that shifts when one is added. */
|
||
const byRisk = (p: ReturnType<typeof parsePolicy>): Record<string, ParsedCoverage> =>
|
||
Object.fromEntries(p.coverages.map((c) => [c.risk, c]));
|
||
|
||
describe("detectPolicyProvider", () => {
|
||
it("claims GMX from the brand wordmark on the letterhead", () => {
|
||
expect(
|
||
detectPolicyProvider(
|
||
"Grupo Mexicano de Seguros, S.A. de C.V.\nTecoyotitla 412, Edificio GMX",
|
||
),
|
||
).toBe("GMX");
|
||
});
|
||
|
||
it("claims GMX from the 'gmx.com.mx' footer URL", () => {
|
||
expect(detectPolicyProvider("JUNTOS EL RIESGO ES MENOR\nwww.gmx.com.mx")).toBe("GMX");
|
||
});
|
||
});
|
||
|
||
describe("parsePolicy / GMX", () => {
|
||
// Verbatim text extracted from ~/Downloads/HC_Folio_000767_Traduccion.pdf via
|
||
// `pdftotext -layout`. Two pages joined by "\n\n".
|
||
const GMX_FULL = page(
|
||
"Multiple Policy\nHome\n" +
|
||
"Policy 007-037-07005947-0000-02 in accordance with the enclosed clauses, to insurance:\n" +
|
||
"Insured JON ASHLEY STRABALA\n" +
|
||
"Additional insured VIVIAN\n" +
|
||
"Legal address BONAMPACK No. EXT26 No.INT 0 COL. Punta Bandera, Tijuana, Baja California, C.P. 22550\n" +
|
||
"ZIP 22550 Income Tax No. XEXX-010101-000\n" +
|
||
"Broker (1176) Jorge Humberto Cuadros\n" +
|
||
"Term 12 months\n" +
|
||
"From 19/07/2026\n" +
|
||
"To 19/07/2027 at twelve hours (noon) Mexico City time.\n" +
|
||
"Currency DOLARES Premium payment CONTADO\n" +
|
||
"Free translation from the Spanish Insurance contract. The English text is just copy given by courtesy. In case of a dispute, the Spanish will prevail over the English version.\n" +
|
||
"Agreed clauses:\n" +
|
||
"•The insured and GMX Hereby declared...\n" +
|
||
"From the above, the present contract shall not be considered under the condition mentioned within article 36-B from the Insurance Companies General Law. Therefore it shall not be required its registration before the Comision National de Seguros y Fianzas.\n" +
|
||
"July 23, 2026\n" +
|
||
"Authority sign.\n" +
|
||
"Grupo Mexicano de Seguros, S.A. de C.V.\n" +
|
||
"Tecoyotitla 412, Edificio GMX\n" +
|
||
"JUNTOS EL RIESGO ES MENOR\n" +
|
||
"www.gmx.com.mx\n\n" +
|
||
"Risk Insured Amount Deductible Loss Participation\n" +
|
||
"Building $350,000.00 Not applies Not applies\n" +
|
||
"Contents $60,000.00 Not applies Not applies\n" +
|
||
"ADDITIONAL RISK\n" +
|
||
"Risk Insured Amount Deductible Loss Participation\n" +
|
||
"Debris removal Building $35,000.00 Not applies Not applies\n" +
|
||
"Debris removal Contents $6,000.00 Not applies Not applies\n" +
|
||
"Outdoors Constructions $10,000.00 5% 10%\n" +
|
||
"Coverage Extention Covered Not applies Not applies\n" +
|
||
"All Risk Covered Not applies Not applies\n" +
|
||
"Earthquake and/or volcanic eruption Covered 2% of the sum insured for each damage structure 20%\n" +
|
||
"Extra Expenses $41,000.00 Not applies Not applies\n" +
|
||
"Robbery with violence $10,000.00 Not applies Not applies\n" +
|
||
"Jewerly $3,900.00 Not applies Not applies\n" +
|
||
"Electronic Equipment $10,000.00 Not applies Not applies\n" +
|
||
"Glasses $10,000.00 Not applies Not applies\n" +
|
||
"Tenant $200,000.00 Not applies Not applies\n" +
|
||
"Family $200,000.00 Not applies Not applies\n" +
|
||
"Family $200,000.00 Not applies Not applies\n" +
|
||
"Domestic workers $7,010.00 Not applies Not applies\n" +
|
||
"VALUES ADDED, HOME GMX",
|
||
);
|
||
|
||
it("extracts the policy number, insured name, broker, dates, and currency", () => {
|
||
const p = parsePolicy(GMX_FULL);
|
||
expect(p.provider).toBe("GMX");
|
||
expect(p.policyNumber).toBe("007-037-07005947-0000-02");
|
||
expect(p.insuredName).toBe("JON ASHLEY STRABALA");
|
||
expect(p.additionalInsured).toBe("VIVIAN");
|
||
expect(p.agentName).toBe("Jorge Humberto Cuadros");
|
||
expect(p.policyFrom?.toISOString().slice(0, 10)).toBe("2026-07-19");
|
||
expect(p.policyTo?.toISOString().slice(0, 10)).toBe("2027-07-19");
|
||
expect(p.policyDate?.toISOString().slice(0, 10)).toBe("2026-07-23");
|
||
expect(p.currency).toBe("USD");
|
||
expect(p.zip).toBe("22550");
|
||
expect(p.legalAddress).toContain("BONAMPACK");
|
||
expect(p.premiumPayment).toBe("CONTADO");
|
||
});
|
||
|
||
it("extracts every coverage row off the second page table", () => {
|
||
const p = parsePolicy(GMX_FULL);
|
||
const byName = Object.fromEntries(p.coverages.map((c) => [c.risk, c]));
|
||
expect(byName.Building?.insuredAmount).toBe(350000);
|
||
expect(byName.Contents?.insuredAmount).toBe(60000);
|
||
expect(byName["Debris removal Building"]?.insuredAmount).toBe(35000);
|
||
expect(byName["Outdoors Constructions"]?.insuredAmount).toBe(10000);
|
||
expect(byName["Outdoors Constructions"]?.deductible).toBe("5%");
|
||
expect(byName["Outdoors Constructions"]?.lossParticipation).toBe("10%");
|
||
// Free-text coverage cells kept verbatim (the policy form surfaces them
|
||
// as observations, not as numbers).
|
||
expect(byName["Earthquake and/or volcanic eruption"]?.insuredAmount).toBeNull();
|
||
expect(byName["Earthquake and/or volcanic eruption"]?.deductible).toContain("2%");
|
||
expect(byName["Earthquake and/or volcanic eruption"]?.lossParticipation).toBe("20%");
|
||
expect(byName["All Risk"]?.insuredAmount).toBeNull();
|
||
expect(p.coverages.length).toBeGreaterThan(10);
|
||
});
|
||
|
||
it("names the product MULT for confirm to resolve", () => {
|
||
// The caratula's own header reads "Multiple Policy / Home". MULT is the
|
||
// legacy discriminator for that multi-line home policy; INCENDIO is
|
||
// fire-only and no policy in the book has ever used it.
|
||
expect(parsePolicy(GMX_FULL).policyTypeName).toBe("MULT");
|
||
});
|
||
|
||
it("leaves premium fields null on the certificate page and notes it", () => {
|
||
const p = parsePolicy(GMX_FULL);
|
||
expect(p.netPremium).toBeNull();
|
||
expect(p.total).toBeNull();
|
||
expect(p.policyFee).toBeNull();
|
||
expect(p.notes.join(" ")).toMatch(/prima/i);
|
||
});
|
||
|
||
it("still parses when the broker parens are missing", () => {
|
||
const p = parsePolicy(
|
||
page(
|
||
"Insured JON ASHLEY STRABALA\nBroker Jorge Humberto Cuadros\n" +
|
||
"From 19/07/2026\nTo 19/07/2027\nCurrency DOLARES\n" +
|
||
"Grupo Mexicano de Seguros",
|
||
),
|
||
);
|
||
expect(p.agentName).toBe("Jorge Humberto Cuadros");
|
||
});
|
||
|
||
it("rejects a page that carries no GMX signal at all", () => {
|
||
const p = parsePolicy(page("Random unrelated document with no policy data."));
|
||
expect(p.provider).toBe("");
|
||
expect(p.notes.join(" ")).toContain("no se reconoció el proveedor");
|
||
});
|
||
|
||
it("captures the deductible / loss-participation columns verbatim as strings", () => {
|
||
const p = parsePolicy(GMX_FULL);
|
||
const eq = p.coverages.find((c) => c.risk === "Earthquake and/or volcanic eruption");
|
||
expect(eq).toBeDefined();
|
||
const eqTyped = eq as ParsedCoverage;
|
||
expect(eqTyped.deductible).toContain("sum insured");
|
||
expect(eqTyped.lossParticipation).toBe("20%");
|
||
});
|
||
});
|
||
|
||
/**
|
||
* The second GMX document family: the Spanish PVL "especificación" the office
|
||
* receives as `…-CondicionesParticulares.pdf`. Verbatim excerpts from
|
||
* `007_LGS-HGMX_07006957_01_0-CondicionesParticulares.pdf` through
|
||
* `pdftotext -layout`, indentation included — the column positions and the
|
||
* blank lines between blocks are what the parser reads, so a cleaned-up
|
||
* fixture would test nothing.
|
||
*/
|
||
describe("parsePolicy / GMX especificación (PVL Hogar)", () => {
|
||
const HEADER =
|
||
" ESPECIFICACIÓN QUE SE ADHIERE Y FORMA PARTE INTEGRANTE DE LA PÓLIZA\n" +
|
||
" 07-037-07006957-00000-01\n" +
|
||
"\n";
|
||
|
||
const GMX_ESPEC = page(
|
||
HEADER +
|
||
"\n" +
|
||
" Nombre del asegurado EMMER . KATHLEEN\n" +
|
||
"\n" +
|
||
" Tipo Persona Asegurada Propietario\n" +
|
||
"\n" +
|
||
" Ubicación del riesgo LOS PELICANOS ESTE NO. 98 Col. LAS GAVIOTAS PLAYAS\n" +
|
||
" DE ROSARITO BAJA CALIFORNIA 22713\n" +
|
||
"\n" +
|
||
" Características del Inmueble Casa Tipo constructivo Combinado: Macizo y Madera.\n" +
|
||
" Consta de 2 pisos incluyendo sótanos y planta baja.\n" +
|
||
"\n" +
|
||
" -500 mts.cuerpo agua SI\n" +
|
||
"\n" +
|
||
" Asegurado Adicional\n" +
|
||
"\n" +
|
||
"PVL Hogar - GMX Seguros Página: 1 de 10\n" +
|
||
HEADER +
|
||
"\n" +
|
||
" SECCIÓN INCENDIO EDIFICIO Y CONTENIDOS\n" +
|
||
"\n" +
|
||
" EDIFICIO\n" +
|
||
"\n" +
|
||
" Límite Máximo de Responsabilidad:\n" +
|
||
" $200,000.00 USD\n" +
|
||
"\n" +
|
||
" Quedan amparados los muros de contención y bardas, así como puertas y portones, hasta un sublimite de $ 50,000.00 M.N. o su\n" +
|
||
" equivalente en dólares americanos, o hasta el 10% de la suma asegurada de la sección de Edificio, lo que resulte menor.\n" +
|
||
"\n" +
|
||
"\n" +
|
||
" CONTENIDOS\n" +
|
||
"\n" +
|
||
" Límite Máximo de Responsabilidad:\n" +
|
||
" $20,000.00 USD\n" +
|
||
"\n" +
|
||
" 2. Terremoto o erupción volcánica: Sección Edificio EXCLUIDO, Sección Contenidos EXCLUIDO\n" +
|
||
"\n" +
|
||
" 3. Fenómenos hidrometeorológicos: Sección Edificio $200,000.00 USD, Sección Contenidos $20,000.00 USD\n" +
|
||
"\n" +
|
||
" Riesgos adicionales.\n" +
|
||
"\n" +
|
||
" Remoción de escombros\n" +
|
||
"\n" +
|
||
" Límite Máximo de Responsabilidad:\n" +
|
||
" Edificio\n" +
|
||
" $20,000.00 USD\n" +
|
||
" Contenidos\n" +
|
||
" $2,000.00 USD\n" +
|
||
"\n" +
|
||
" Gastos extraordinarios para casa habitación\n" +
|
||
"\n" +
|
||
" En caso de siniestro por los riesgos cubiertos en esta póliza, GMX Seguros pagará la renta de casa o departamento, casa de\n" +
|
||
" huéspedes u hotel cuando se asegure el inmueble, así como los gastos de mudanza, seguro de transporte del menaje de casa y\n" +
|
||
" efectuados.\n" +
|
||
"\n" +
|
||
" Límite Máximo de Responsabilidad:\n" +
|
||
" $22,000.00 USD\n" +
|
||
" Periodo de indemnización: 4 meses.\n" +
|
||
"\n" +
|
||
" Bienes a la Intemperie:\n" +
|
||
"\n" +
|
||
"\n" +
|
||
" 5 POR CIENTO SOBRE SUMA ASEGURADA, 20 PORCIENTO DE PARTICIPACIÓN A CARGO DEL ASEGURADO DE TODA\n" +
|
||
" Y CADA PÉRDIDA.\n" +
|
||
"\n" +
|
||
"\n" +
|
||
" Límite Máximo de Responsabilidad: $10,000.00 USD\n" +
|
||
"\n" +
|
||
" DEDUCIBLES:\n" +
|
||
"\n" +
|
||
" El procedimiento que se seguirá para la aplicación de deducibles en caso de que la póliza cuente con cláusula inflacionaria en todas\n" +
|
||
" y/o en algunas de sus coberturas será como sigue:\n" +
|
||
"\n" +
|
||
" Fenómenos hidrometeorológicos\n" +
|
||
" Zona: A2\n" +
|
||
" Deducible\n" +
|
||
" Edificio: 1 POR CIENTO SOBRE SUMA ASEGURADA\n" +
|
||
" Coaseguro:\n" +
|
||
" Zona 1: (INTERIOR) Participación a cargo del asegurado del 10% de toda y cada pérdida.\n" +
|
||
" Zona 2: Participación a cargo del asegurado del 10% de toda y cada pérdida.\n" +
|
||
"\n" +
|
||
" Deducible\n" +
|
||
" Contenidos: 1 POR CIENTO SOBRE SUMA ASEGURADA\n" +
|
||
"\n" +
|
||
" II.- SECCIÓN DIVERSOS MISCELÁNEOS\n" +
|
||
"\n" +
|
||
" ROBO DE CONTENIDOS\n" +
|
||
"\n" +
|
||
" Límite de Responsabilidad:\n" +
|
||
" $4,000.00 USD\n" +
|
||
"\n" +
|
||
"\n" +
|
||
" Deducible:\n" +
|
||
" Sin deducible\n" +
|
||
"\n" +
|
||
"\n" +
|
||
" Sublímites:\n" +
|
||
" Joyas, artículos de oro y plata, armas, relojes, pieles, piedras preciosas montadas, colecciones, obras de arte y demás que por su\n" +
|
||
"\n" +
|
||
"PVL Hogar - GMX Seguros Página: 7 de 10\n" +
|
||
HEADER +
|
||
"\n" +
|
||
" naturaleza se consideran como objetos de difícil o imposible reposición\n" +
|
||
"\n" +
|
||
"\n" +
|
||
" Límite de Responsabilidad:\n" +
|
||
"\n" +
|
||
" $2,000.00 USD\n" +
|
||
"\n" +
|
||
"\n" +
|
||
" Deducible:\n" +
|
||
" Sin deducible\n" +
|
||
"\n" +
|
||
" Las condiciones generales que forman parte de la presente póliza son las identificadas bajo el nombre:\n" +
|
||
" W_HogarGMX_12.11.2025.pdf\n" +
|
||
"\n" +
|
||
"PVL Hogar - GMX Seguros Página: 10 de 10\n",
|
||
);
|
||
|
||
it("reads a policy number whose groups are not the caratula's widths", () => {
|
||
// 2-3-8-5-2 here vs 3-3-8-4-2 on the English caratula. Pinning the widths
|
||
// reads one family and returns null on the other.
|
||
expect(parsePolicy(GMX_ESPEC).policyNumber).toBe("07-037-07006957-00000-01");
|
||
});
|
||
|
||
it("reads the insured, the risk location across its wrapped line, and the ZIP", () => {
|
||
const p = parsePolicy(GMX_ESPEC);
|
||
expect(p.provider).toBe("GMX");
|
||
expect(p.insuredName).toBe("EMMER . KATHLEEN");
|
||
expect(p.legalAddress).toBe(
|
||
"LOS PELICANOS ESTE NO. 98 Col. LAS GAVIOTAS PLAYAS DE ROSARITO BAJA CALIFORNIA 22713",
|
||
);
|
||
expect(p.zip).toBe("22713");
|
||
// The cell is printed but empty on this policy — an empty label must not
|
||
// capture the next line of the form.
|
||
expect(p.additionalInsured).toBeNull();
|
||
});
|
||
|
||
it("leaves the fields this document does not carry null, and says so", () => {
|
||
const p = parsePolicy(GMX_ESPEC);
|
||
expect(p.policyFrom).toBeNull();
|
||
expect(p.policyTo).toBeNull();
|
||
expect(p.policyDate).toBeNull();
|
||
expect(p.agentName).toBeNull();
|
||
expect(p.netPremium).toBeNull();
|
||
expect(p.total).toBeNull();
|
||
// The note must tell the reviewer to key them in — those three are
|
||
// captured by hand on this layout — and must say what silently breaks if
|
||
// the vigencia is left empty.
|
||
const notes = p.notes.join(" ");
|
||
expect(notes).toMatch(/no trae vigencia, agente ni prima/i);
|
||
expect(notes).toMatch(/captúrelos a mano/i);
|
||
expect(notes).toMatch(/avisos de renovación/i);
|
||
});
|
||
|
||
it("takes the currency from the printed limits, not from the M.N. sublimits", () => {
|
||
// The body prose quotes sublimits in pesos ("$ 50,000.00 M.N."); every
|
||
// limit is in USD, and only the limits vote.
|
||
expect(parsePolicy(GMX_ESPEC).currency).toBe("USD");
|
||
});
|
||
|
||
it("reads each coverage under its own heading", () => {
|
||
const c = byRisk(parsePolicy(GMX_ESPEC));
|
||
expect(c.EDIFICIO?.insuredAmount).toBe(200000);
|
||
expect(c.CONTENIDOS?.insuredAmount).toBe(20000);
|
||
expect(c["ROBO DE CONTENIDOS"]?.insuredAmount).toBe(4000);
|
||
expect(c["ROBO DE CONTENIDOS"]?.deductible).toBe("Sin deducible");
|
||
});
|
||
|
||
it("splits a limit printed under Edificio / Contenidos sub-labels", () => {
|
||
const c = byRisk(parsePolicy(GMX_ESPEC));
|
||
expect(c["Remoción de escombros — Edificio"]?.insuredAmount).toBe(20000);
|
||
expect(c["Remoción de escombros — Contenidos"]?.insuredAmount).toBe(2000);
|
||
});
|
||
|
||
it("names a coverage after its heading, not after the wrapped tail of the prose above it", () => {
|
||
// Walking back from the limit hits "efectuados." — short, and the only
|
||
// thing separating it from a heading is that it is not preceded by a
|
||
// blank line.
|
||
const c = byRisk(parsePolicy(GMX_ESPEC));
|
||
expect(c["Gastos extraordinarios para casa habitación"]?.insuredAmount).toBe(22000);
|
||
expect(c["efectuados."]).toBeUndefined();
|
||
});
|
||
|
||
it("reads a limit printed on the label's own line", () => {
|
||
const c = byRisk(parsePolicy(GMX_ESPEC));
|
||
expect(c["Bienes a la Intemperie"]?.insuredAmount).toBe(10000);
|
||
});
|
||
|
||
it("reads a deductible stated as a sentence above the limit", () => {
|
||
const c = byRisk(parsePolicy(GMX_ESPEC));
|
||
expect(c["Bienes a la Intemperie"]?.deductible).toBe(
|
||
"5 POR CIENTO SOBRE SUMA ASEGURADA, 20 PORCIENTO DE PARTICIPACIÓN A CARGO DEL ASEGURADO DE TODA Y CADA PÉRDIDA.",
|
||
);
|
||
});
|
||
|
||
it("never borrows a neighbouring coverage's prose as a deductible", () => {
|
||
// "…o hasta el 10% de la suma asegurada de la sección de Edificio" is a
|
||
// sublimit rule for EDIFICIO, printed two paragraphs above CONTENIDOS.
|
||
const c = byRisk(parsePolicy(GMX_ESPEC));
|
||
expect(c.CONTENIDOS?.deductible).toBeNull();
|
||
expect(c.EDIFICIO?.deductible).toBeNull();
|
||
});
|
||
|
||
it("does not read the page-level DEDUCIBLES paragraph as a deductible", () => {
|
||
const p = parsePolicy(GMX_ESPEC);
|
||
expect(
|
||
p.coverages.some((c) => (c.deductible ?? "").includes("cláusula inflacionaria")),
|
||
).toBe(false);
|
||
});
|
||
|
||
it("reads a sublimit block as a sublimit OF the coverage above it", () => {
|
||
// The amount sits after a blank line AND a page break, and the block's
|
||
// own heading ("Sublímites:") names no risk.
|
||
const c = byRisk(parsePolicy(GMX_ESPEC));
|
||
expect(c["ROBO DE CONTENIDOS — sublímite"]?.insuredAmount).toBe(2000);
|
||
});
|
||
|
||
it("records an excluded catastrophic risk as excluded, never as zero", () => {
|
||
const p = parsePolicy(GMX_ESPEC);
|
||
const quake = p.coverages.filter((c) => /Terremoto/i.test(c.risk));
|
||
expect(quake).toHaveLength(2);
|
||
for (const c of quake) {
|
||
expect(c.risk).toMatch(/EXCLUIDO/);
|
||
// A coverage insured for $0 and an excluded coverage are the same
|
||
// number and very different facts.
|
||
expect(c.insuredAmount).toBeNull();
|
||
}
|
||
});
|
||
|
||
it("attaches the hydrometeorological deductible and coinsurance from its own block", () => {
|
||
const c = byRisk(parsePolicy(GMX_ESPEC));
|
||
const building = c["Fenómenos hidrometeorológicos — Sección Edificio"];
|
||
expect(building?.insuredAmount).toBe(200000);
|
||
expect(building?.deductible).toBe("1 POR CIENTO SOBRE SUMA ASEGURADA");
|
||
expect(building?.lossParticipation).toBe("10%");
|
||
expect(c["Fenómenos hidrometeorológicos — Sección Contenidos"]?.insuredAmount).toBe(20000);
|
||
});
|
||
|
||
it("names the same product as the caratula — one policy, two artifacts", () => {
|
||
expect(parsePolicy(GMX_ESPEC).policyTypeName).toBe("MULT");
|
||
});
|
||
|
||
it("carries the underwriting context the fields have no home for", () => {
|
||
const notes = parsePolicy(GMX_ESPEC).notes.join(" | ");
|
||
expect(notes).toMatch(/tipo de persona asegurada: Propietario/);
|
||
expect(notes).toMatch(/características del inmueble: Casa/);
|
||
expect(notes).toMatch(/cuerpo de agua/);
|
||
expect(notes).toMatch(/zona catastrófica declarada: A2/);
|
||
expect(notes).toMatch(/W_HogarGMX_12\.11\.2025\.pdf/);
|
||
});
|
||
});
|
||
|
||
/* ------------------------------------------------------------------ ANA */
|
||
|
||
/**
|
||
* Verbatim `pdftotext -layout` output of the PDFs A.N.A.'s portal produced
|
||
* for three real policies, cut at the end of the risk table (the legal
|
||
* boilerplate and the repeated AGENT COPY below it are not parsed, and the
|
||
* repeats are covered by their own test).
|
||
*
|
||
* The column padding is load-bearing on the driver's policy, which
|
||
* distinguishes SUM INSURED from PREMIUM by horizontal position alone — do
|
||
* not reflow these strings.
|
||
*/
|
||
|
||
const ANA_AUTO_AMPLIA = page(`A.N.A. COMPAÑIA DE SEGUROS SA DE CV
|
||
LUIS CABRERA #2033 INT. 201, Col. ZONA URBANA RIO TIJUANA
|
||
C.P. 22010 MUNICIPIO DE TIJUANA, BAJA CALIFORNIA
|
||
www.anaseguros.com.mx
|
||
AUTOMOBILE
|
||
ALL CLAIMS MUST BE REPORTED BEFORE LEAVING MEXICO
|
||
U.S. CELL PHONES TRY + 011-52-55-5322-82-66 MEXICAN CELL PHONES 800-911-911-9 SPECIAL POLICY FOR TOURISTS
|
||
TOLL-FREE FROM THE U.S.A. 888-335-7072 BELIZE CELL PHONES 00-52-55-5322-8266
|
||
WHATSAPP + 52-55-80-50-3633
|
||
No. 700489651
|
||
ISSUED BY: DATE ISSUED TERM OF INSURANCE
|
||
DAYS
|
||
JORGE HUMBERTO CUADROS DAY MONTH YEAR DAY MONTH YEAR TIME
|
||
BENITO JUAREZ 25 No.50 INT 38 CENTRO
|
||
04 08 2026 FROM 07 08 2026 12:01
|
||
365
|
||
ROSARITO, BAJA CALIFORNIA 22710
|
||
. 70175 TO 07 08 2027 12:01
|
||
DISCOUNT PREMIUM POLICY FEE TAX LOCAL TAX TOTAL
|
||
- 298.61 30.00 26.29 0.00 354.90
|
||
|
||
INSURED RAY DEAN II AND SUSAN ROCKHOLD
|
||
LICENSE P0066762
|
||
ADDRESS 10308 DONNA AVE EMAIL PROLABSALE@AOL.COM
|
||
CITY & STATE NORTHRIDGE, CA 91326 TELEPHONE 8184453524
|
||
PAYMENT DEADLINE
|
||
INSURANCE COMPANY LIEN HOLDER
|
||
IMMEDIATE
|
||
|
||
ITEM YEAR MAKE BODY SERIAL No. PLATES
|
||
VEHICLE 2017 CHRYSLER PACIFICA 2C4RC1DG7HR654698 8BPX206
|
||
TRAILER . .
|
||
TOWING . .
|
||
*** VALUE STATED MUST NOT EXCEED MARKET VALUE ***
|
||
***VEHICLES THAT HAVE BEEN ACQUIRED AS SALVAGE, REBUILT, OR HAVE BEEN USED PREVIOUSLY AS A TAXI WILL BE CONSIDERED WITH A REDUCED VALUE OF 35% (thirty-five percent), TAKING
|
||
AS A BASE THE VALUE OF A SIMILAR NORMAL VEHICLE, THAT IS, ONE THAT HAS NOT BEEN ACQUIRED AS SALVAGE AND ITS PREVIOUS USE HAS NOT BEEN AS A TAXI OR REBUILT. IT WILL BE THE
|
||
SOLE OBLIGATION AND RESPONSIBILITY OF THE INSURED TO DECLARATE THIS WHEN ACQUIRING THE POLICY.
|
||
SECTION SPECIFICATION OF RISKS LIMIT OF LIABILITY
|
||
MATERIAL DAMAGE WITH MANDATORY DEDUCTIBLE COVERED/EXCLUDED VEHICLE 8,000.00 DLLS.
|
||
1 DEDUCTIBLE: WITH MINIMUM OF $500.00 ON AUTOS
|
||
TRAILER
|
||
(SEDANS, COUPES, CONVERTIBLES AND STATION WAGONS) COVERED
|
||
AND $500.00 ON ALL OTHERS (PICK UPS, VANS, SUV´s AND MOTOR HOMES). 0.00 DLLS.
|
||
|
||
TOTAL THEFT WITH MANDATORY DEDUCTIBLE COVERED/EXCLUDED TOWING
|
||
2 DEDUCTIBLE: WITH MINIMUM OF $1,000.00 ON AUTOS 0.00 DLLS.
|
||
(SEDANS, COUPES, CONVERTIBLES AND STATION WAGONS) COVERED
|
||
AND $1,000.00 ON ALL OTHERS (PICK UPS, VANS, SUV´s AND MOTOR HOMES).
|
||
LIABILITY FOR PROPERTY DAMAGE TO THIRD PARTIES
|
||
3 100,000.00 DLLS.
|
||
|
||
|
||
BODILY INJURY LIABILITY PER PER
|
||
4 PERSON 100,000.00 ACCIDENT 200,000.00 DLLS.
|
||
|
||
MEDICAL EXPENSES PER PER
|
||
5 PERSON 5,000.00 ACCIDENT 25,000.00 DLLS.
|
||
|
||
COVERED/EXCLUDED PREMIUM
|
||
6 A.N.A.'s LEGAL AID
|
||
COVERED 40.00
|
||
COVERED/EXCLUDED PREMIUM
|
||
7 A.N.A.'s ROADSIDE ASSISTANCE
|
||
COVERED 40.00
|
||
CATASTROPHIC LIABILITY FOR DEATH OF THIRD PREMIUM
|
||
8 EXCLUDED
|
||
PARTIES DLLS. 0.00
|
||
ELITE OR ELITE PLUS WITH MANDATORY DEDUCTIBLE COVERED/EXCLUDED
|
||
9 PARTIAL THEFT (LIMIT 0.00 DLLS.WITH DEDUCTIBLE: 0.00 DLLS. PER EVENT) 0.00
|
||
VANDALISM (LIMIT 0.00 DLLS.WITH DEDUCTIBLE: 0.00 DLLS. PER EVENT) EXCLUDED
|
||
|
||
|
||
ISSUED ONLINE`);
|
||
|
||
const ANA_AUTO_RC_DIAS = page(`A.N.A. COMPAÑIA DE SEGUROS SA DE CV
|
||
LUIS CABRERA #2033 INT. 201, Col. ZONA URBANA RIO TIJUANA
|
||
C.P. 22010 MUNICIPIO DE TIJUANA, BAJA CALIFORNIA
|
||
www.anaseguros.com.mx
|
||
AUTOMOBILE
|
||
ALL CLAIMS MUST BE REPORTED BEFORE LEAVING MEXICO
|
||
U.S. CELL PHONES TRY + 011-52-55-5322-82-66 MEXICAN CELL PHONES 800-911-911-9 SPECIAL POLICY FOR TOURISTS
|
||
TOLL-FREE FROM THE U.S.A. 888-335-7072 BELIZE CELL PHONES 00-52-55-5322-8266
|
||
WHATSAPP + 52-55-80-50-3633
|
||
No. 700487807
|
||
ISSUED BY: DATE ISSUED TERM OF INSURANCE
|
||
DAYS
|
||
JORGE HUMBERTO CUADROS DIARIA DAY MONTH YEAR DAY MONTH YEAR TIME
|
||
BENITO JUAREZ 25 NO50 INT 38 COL CENTRO
|
||
22 07 2026 FROM 23 07 2026 12:01
|
||
3
|
||
ROSARITO BAJA CALIFORNIA 22710
|
||
(661) 612 12 55 70175 TO 26 07 2026 12:01
|
||
DISCOUNT PREMIUM POLICY FEE TAX LOCAL TAX TOTAL
|
||
- 10.77 25.00 2.86 0.00 38.63
|
||
|
||
INSURED STEPHEN RUPAN SHATAFIAN
|
||
LICENSE C1394198
|
||
ADDRESS 13181 CROSSROADS PARKWAY NORTH STE 300 EMAIL sshatafian@lee-associates.com
|
||
|
||
|
||
CITY & STATE CITY OF INDUSTRY, CA 91746 TELEPHONE 7143221072
|
||
PAYMENT DEADLINE
|
||
INSURANCE COMPANY LIEN HOLDER
|
||
IMMEDIATE
|
||
|
||
ITEM YEAR MAKE BODY SERIAL No. PLATES
|
||
VEHICLE 2022 FORD TRANSIT 1FBAX2CG3NKA69091 EC46T99
|
||
TRAILER . .
|
||
TOWING . .
|
||
*** VALUE STATED MUST NOT EXCEED MARKET VALUE ***
|
||
***VEHICLES THAT HAVE BEEN ACQUIRED AS SALVAGE, REBUILT, OR HAVE BEEN USED PREVIOUSLY AS A TAXI WILL BE CONSIDERED WITH A REDUCED VALUE OF 35% (thirty-five percent), TAKING
|
||
AS A BASE THE VALUE OF A SIMILAR NORMAL VEHICLE, THAT IS, ONE THAT HAS NOT BEEN ACQUIRED AS SALVAGE AND ITS PREVIOUS USE HAS NOT BEEN AS A TAXI OR REBUILT. IT WILL BE THE
|
||
SOLE OBLIGATION AND RESPONSIBILITY OF THE INSURED TO DECLARATE THIS WHEN ACQUIRING THE POLICY.
|
||
SECTION SPECIFICATION OF RISKS LIMIT OF LIABILITY
|
||
MATERIAL DAMAGE WITH MANDATORY DEDUCTIBLE COVERED/EXCLUDED VEHICLE 0.00 DLLS.
|
||
1 DEDUCTIBLE: ON AUTOS (SEDANS, COUPES, CONVERTIBLES AND
|
||
TRAILER
|
||
STATION WAGONS) AND OTHERS (PICK UPS, VANS, EXCLUDED
|
||
SUV´s AND MOTOR HOMES). 0.00 DLLS.
|
||
|
||
TOTAL THEFT WITH MANDATORY DEDUCTIBLE COVERED/EXCLUDED TOWING
|
||
2 DEDUCTIBLE: ON AUTOS (SEDANS, COUPES, CONVERTIBLES AND 0.00 DLLS.
|
||
STATION WAGONS) AND OTHERS (PICK UPS, VANS, EXCLUDED
|
||
SUV´s AND MOTOR HOMES).
|
||
LIABILITY FOR PROPERTY DAMAGE TO THIRD PARTIES
|
||
3 100,000.00 DLLS.
|
||
|
||
|
||
BODILY INJURY LIABILITY PER PER
|
||
4 PERSON 100,000.00 ACCIDENT 200,000.00 DLLS.
|
||
|
||
MEDICAL EXPENSES PER PER
|
||
5 PERSON 5,000.00 ACCIDENT 25,000.00 DLLS.
|
||
|
||
COVERED/EXCLUDED PREMIUM
|
||
6 A.N.A.'s LEGAL AID
|
||
COVERED 2.25
|
||
COVERED/EXCLUDED PREMIUM
|
||
7 A.N.A.'s ROADSIDE ASSISTANCE
|
||
COVERED 2.25
|
||
CATASTROPHIC LIABILITY FOR DEATH OF THIRD PREMIUM
|
||
8 EXCLUDED
|
||
PARTIES DLLS. 0.00
|
||
ELITE OR ELITE PLUS WITH MANDATORY DEDUCTIBLE COVERED/EXCLUDED
|
||
9 PARTIAL THEFT (LIMIT 0.00 DLLS.WITH DEDUCTIBLE: 0.00 DLLS. PER EVENT) 0.00
|
||
VANDALISM (LIMIT 0.00 DLLS.WITH DEDUCTIBLE: 0.00 DLLS. PER EVENT) EXCLUDED
|
||
|
||
|
||
ISSUED ONLINE`);
|
||
|
||
const ANA_LICENCIA = page(`A.N.A. COMPAÑIA DE SEGUROS SA DE CV
|
||
LUIS CABRERA #2033 INT. 201, Col.4 ZONA URBANA RIO TIJUANA
|
||
C.P. 22010 MUNICIPIO DE TIJUANA, BAJA CALIFORNIA
|
||
www.anaseguros.com.mx
|
||
DRIVER´S POLICY FOR AUTOMOBILE
|
||
ALL CLAIMS MUST BE REPORTED BEFORE LEAVING MEXICO
|
||
U.S. CELL PHONES TRY + 011-52-55-5322-82-66 MEXICAN CELL PHONES 800-911-911-9
|
||
SPECIAL POLICY FOR TOURISTS
|
||
TOLL-FREE FROM THE U.S.A. 888-335-7072 BELIZE CELL PHONES 00-52-55-5322-8266
|
||
WHATSAPP + 52-55-80-50-3633 No. 700489616
|
||
ISSUED BY: DATE ISSUED & TIME TERM OF INSURANCE
|
||
JORGE HUMBERTO CUADROS
|
||
DAYS
|
||
DAY MONTH YEAR DAY MONTH YEAR TIME
|
||
BENITO JUAREZ 25 No.50 INT 38 CENTRO 04 08 2026 FROM 06 08 2026 12:01
|
||
365
|
||
ROSARITO, BAJA CALIFORNIA 22710 TO 06 08 2027 12:01
|
||
. 70175
|
||
DISCOUNT PREMIUM POLICY FEE TAX LOCAL TAX TOTAL
|
||
- 142.78 30.00 13.82 0.00 186.60
|
||
|
||
LICENSE N0017668 EMAIL PWAGONER49@AOL.COM TELEPHONE 3102001538
|
||
POLICY HOLDER
|
||
1. NAME : PAMELA DENISE WAGONER Ph.3102001538
|
||
ADDRESS : 49305 HIGHWAY 74 SPC 10, PALM DESERT, CA, 92260,
|
||
DRIVER LICENSE : N0017668
|
||
2. NAME :
|
||
ADDRESS :
|
||
DRIVER LICENSE :
|
||
NONE
|
||
3. NAME :
|
||
ADDRESS :
|
||
DRIVER LICENSE :
|
||
NONE
|
||
4. NAME :
|
||
ADDRESS :
|
||
DRIVER LICENSE : NONE
|
||
5. NAME :
|
||
ADDRESS :
|
||
DRIVER LICENSE : NONE
|
||
SPECIFICATION OF RISKS SUM INSURED PREMIUM
|
||
LIABILITY FOR PROPERTY DAMAGE TO THIRD PARTIES 100,000.00 usd. 18.70 usd.
|
||
BODILY INJURY LIABILITY ( EXCLUDING OCCUPANTS OF THE VEHICLE ) 100,000.00 usd. Per Person
|
||
54.27 usd.
|
||
200,000.00 usd. Per Accident
|
||
CATASTROPHIC LIABILITY FOR DEATH OF THIRD PARTIES 0.00 usd. 0.00 usd.
|
||
|
||
MEDICAL EXPENSES 4,000.00 usd. Per Person
|
||
9.81 usd.
|
||
20,000.00 usd. Per Accident
|
||
COVERED/EXCLUDED PREMIUM
|
||
LEGAL AID
|
||
COVERED 30.00 usd.
|
||
COVERED/EXCLUDED PREMIUM
|
||
AUTOMOBILE ASSISTANCE
|
||
COVERED 30.00 usd.
|
||
|
||
The following risks are excluded Collision, overtuning and glass breakage, fire, total theft and natural disasters, partial theft and vandalism.`);
|
||
|
||
|
||
describe("detectPolicyProvider / ANA", () => {
|
||
it("claims ANA from the letterhead", () => {
|
||
expect(
|
||
detectPolicyProvider("A.N.A. COMPAÑIA DE SEGUROS SA DE CV\nwww.anaseguros.com.mx"),
|
||
).toBe("ANA");
|
||
});
|
||
|
||
it("does not let GMX's layout rules claim an ANA page", () => {
|
||
// Both books print "MATERIAL DAMAGE"-ish headings; the brand pass runs
|
||
// before any layout rule precisely so this can't go the other way.
|
||
expect(detectPolicyProvider(ANA_AUTO_AMPLIA.text)).toBe("ANA");
|
||
expect(detectPolicyProvider(ANA_LICENCIA.text)).toBe("ANA");
|
||
});
|
||
});
|
||
|
||
describe("parsePolicy / ANA automobile", () => {
|
||
const p = parsePolicy(ANA_AUTO_AMPLIA);
|
||
|
||
it("reads the header band", () => {
|
||
expect(p.provider).toBe("ANA");
|
||
expect(p.policyNumber).toBe("700489651");
|
||
expect(p.insuredName).toBe("RAY DEAN II AND SUSAN ROCKHOLD");
|
||
expect(p.agentName).toBe("JORGE HUMBERTO CUADROS");
|
||
expect(p.legalAddress).toBe("10308 DONNA AVE, NORTHRIDGE, CA 91326");
|
||
expect(p.zip).toBe("91326");
|
||
expect(p.currency).toBe("USD");
|
||
expect(p.premiumPayment).toBe("IMMEDIATE");
|
||
});
|
||
|
||
it("reads DD MM YYYY out of the three date column cells", () => {
|
||
expect(p.policyDate?.toISOString().slice(0, 10)).toBe("2026-08-04");
|
||
expect(p.policyFrom?.toISOString().slice(0, 10)).toBe("2026-08-07");
|
||
expect(p.policyTo?.toISOString().slice(0, 10)).toBe("2027-08-07");
|
||
});
|
||
|
||
it("maps the six money cells positionally, not by finding six amounts", () => {
|
||
// DISCOUNT prints as a bare "-" here. A "take the amounts in order"
|
||
// reading would shift every value one column left.
|
||
expect(p.netPremium).toBe(298.61);
|
||
expect(p.policyFee).toBe(30);
|
||
expect(p.total).toBe(354.9);
|
||
expect(p.notes.join(" | ")).toMatch(/impuesto: 26\.29/);
|
||
});
|
||
|
||
it("reads the vehicle by token role, not by column", () => {
|
||
expect(p.vehicles).toHaveLength(1);
|
||
expect(p.vehicles[0]).toEqual({
|
||
item: "VEHICLE",
|
||
modelYear: "2017",
|
||
make: "CHRYSLER",
|
||
bodyType: "PACIFICA",
|
||
vinNumber: "2C4RC1DG7HR654698",
|
||
licensePlate: "8BPX206",
|
||
});
|
||
});
|
||
|
||
it("reads a two-word BODY cell without losing the VIN", () => {
|
||
// "GENESIS SEDAN" is two tokens where "PACIFICA" is one — the VIN shape
|
||
// is the anchor, not the token count.
|
||
const v = parsePolicy(ANA_AUTO_RC_DIAS).vehicles[0];
|
||
expect(v.make).toBe("FORD");
|
||
expect(v.vinNumber).toBe("1FBAX2CG3NKA69091");
|
||
expect(v.licensePlate).toBe("EC46T99");
|
||
});
|
||
|
||
it("skips the empty TRAILER and TOWING slots", () => {
|
||
// Both print a "." per cell rather than being absent.
|
||
expect(p.vehicles.map((v) => v.item)).toEqual(["VEHICLE"]);
|
||
});
|
||
|
||
it("records the insured as a named driver with their licence", () => {
|
||
expect(p.drivers).toHaveLength(1);
|
||
expect(p.drivers[0].fullName).toBe("RAY DEAN II AND SUSAN ROCKHOLD");
|
||
expect(p.drivers[0].licenseNumber).toBe("P0066762");
|
||
expect(p.drivers[0].email).toBe("PROLABSALE@AOL.COM");
|
||
});
|
||
|
||
it("does not read the agent's own street number as the policy number", () => {
|
||
// "BENITO JUAREZ 25 No.50 INT 38" sits three lines above the No. cell.
|
||
expect(p.policyNumber).not.toBe("50");
|
||
expect(p.notes.join(" | ")).not.toMatch(/formas/);
|
||
});
|
||
|
||
it("reads the agent clave without picking up their postal code", () => {
|
||
// "ROSARITO, BAJA CALIFORNIA 22710" is five digits in the same band.
|
||
expect(p.notes.join(" | ")).toMatch(/clave de agente: 70175/);
|
||
expect(p.notes.join(" | ")).not.toMatch(/22710/);
|
||
});
|
||
|
||
it("labels the declared values by their printed item slot", () => {
|
||
const c = byRisk(p);
|
||
expect(c["MATERIAL DAMAGE — VEHICLE"]?.insuredAmount).toBe(8000);
|
||
expect(c["MATERIAL DAMAGE — TRAILER"]?.insuredAmount).toBe(0);
|
||
expect(c["TOTAL THEFT — TOWING"]?.insuredAmount).toBe(0);
|
||
});
|
||
|
||
it("keeps the deductible sentence out of the value columns", () => {
|
||
const c = byRisk(p);
|
||
expect(c["MATERIAL DAMAGE — VEHICLE"]?.deductible).toBe(
|
||
"WITH MINIMUM OF $500.00 ON AUTOS (SEDANS, COUPES, CONVERTIBLES AND " +
|
||
"STATION WAGONS) AND $500.00 ON ALL OTHERS (PICK UPS, VANS, SUV´s AND " +
|
||
"MOTOR HOMES).",
|
||
);
|
||
});
|
||
|
||
it("does not mistake the $500.00 inside the deductible for a sum insured", () => {
|
||
// It is the one amount in the block not suffixed "DLLS.".
|
||
const amounts = p.coverages.map((c) => c.insuredAmount);
|
||
expect(amounts).not.toContain(500);
|
||
});
|
||
|
||
it("splits the per-person and per-accident limits", () => {
|
||
const c = byRisk(p);
|
||
expect(c["BODILY INJURY LIABILITY — POR PERSONA"]?.insuredAmount).toBe(100000);
|
||
expect(c["BODILY INJURY LIABILITY — POR EVENTO"]?.insuredAmount).toBe(200000);
|
||
expect(c["MEDICAL EXPENSES — POR PERSONA"]?.insuredAmount).toBe(5000);
|
||
expect(c["MEDICAL EXPENSES — POR EVENTO"]?.insuredAmount).toBe(25000);
|
||
});
|
||
|
||
it("records an add-on's figure as a premium, never as a sum insured", () => {
|
||
// $40 is what legal aid COST. As `insuredAmount` it would read on the
|
||
// review screen as a $40 liability limit.
|
||
const c = byRisk(p);
|
||
expect(c["LEGAL AID"]?.premium).toBe(40);
|
||
expect(c["LEGAL AID"]?.insuredAmount).toBeNull();
|
||
expect(c["ROADSIDE ASSISTANCE"]?.premium).toBe(40);
|
||
});
|
||
|
||
it("unpacks section 9's parenthesised limit and deductible", () => {
|
||
const c = byRisk(p);
|
||
const theft = c["ELITE / ELITE PLUS — PARTIAL THEFT: EXCLUDED"];
|
||
expect(theft?.insuredAmount).toBe(0);
|
||
expect(theft?.deductible).toBe("0.00 DLLS. POR EVENTO");
|
||
expect(c["ELITE / ELITE PLUS — VANDALISM: EXCLUDED"]).toBeDefined();
|
||
});
|
||
|
||
it("emits each coverage once even though the PDF prints the face twice", () => {
|
||
// The real upload is ORIGINAL + AGENT COPY + receipt + three travel
|
||
// cards, all concatenated into one string before parsing.
|
||
const doubled = page(ANA_AUTO_AMPLIA.text + "\n\n" + ANA_AUTO_AMPLIA.text);
|
||
expect(parsePolicy(doubled).coverages).toHaveLength(p.coverages.length);
|
||
expect(parsePolicy(doubled).vehicles).toHaveLength(1);
|
||
});
|
||
});
|
||
|
||
describe("policy type, as a name for confirm to resolve", () => {
|
||
it("names ANA's two faces after the legacy tables they belong to", () => {
|
||
expect(parsePolicy(ANA_AUTO_AMPLIA).policyTypeName).toBe("AUTO");
|
||
expect(parsePolicy(ANA_AUTO_RC_DIAS).policyTypeName).toBe("AUTO");
|
||
expect(parsePolicy(ANA_LICENCIA).policyTypeName).toBe("LICENCIAS");
|
||
});
|
||
|
||
it("emits a NAME, never an id — the parser must not need a database", () => {
|
||
// Anything id-shaped here would mean the parser had reached for the DB.
|
||
for (const p of [ANA_AUTO_AMPLIA, ANA_AUTO_RC_DIAS, ANA_LICENCIA]) {
|
||
expect(parsePolicy(p).policyTypeName).toMatch(/^[A-Z_]+$/);
|
||
}
|
||
});
|
||
|
||
it("leaves the type unnamed when no parser claimed the page", () => {
|
||
expect(parsePolicy(page("a laundry receipt")).policyTypeName).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe("parsePolicy / ANA responsabilidad civil por días", () => {
|
||
const p = parsePolicy(ANA_AUTO_RC_DIAS);
|
||
|
||
it("reads a by-the-day term rather than defaulting to a year", () => {
|
||
// Left at the schema's 365 default this weekend policy would sit in the
|
||
// renewals window a year out.
|
||
expect(p.policyFrom?.toISOString().slice(0, 10)).toBe("2026-07-23");
|
||
expect(p.policyTo?.toISOString().slice(0, 10)).toBe("2026-07-26");
|
||
expect(p.coveragePeriodDays).toBe(3);
|
||
});
|
||
|
||
it("reads the clave when the agent's phone occupies the left cell", () => {
|
||
// The by-the-day products print "(661) 612 12 55" ahead of the clave, so
|
||
// it is no longer the first thing on its line.
|
||
expect(p.notes.join(" | ")).toMatch(/clave de agente: 70175/);
|
||
});
|
||
|
||
it("marks the excluded sections as excluded, not as insured for zero", () => {
|
||
const risks = p.coverages.map((c) => c.risk);
|
||
expect(risks).toContain("MATERIAL DAMAGE — VEHICLE: EXCLUDED");
|
||
expect(risks).toContain("TOTAL THEFT — TOWING: EXCLUDED");
|
||
// The liability sections are what this product actually sells, and they
|
||
// are NOT excluded.
|
||
expect(risks).toContain("LIABILITY FOR PROPERTY DAMAGE TO THIRD PARTIES");
|
||
});
|
||
});
|
||
|
||
describe("parsePolicy / ANA driver's policy (licencia)", () => {
|
||
const p = parsePolicy(ANA_LICENCIA);
|
||
|
||
it("reads the holder off the numbered POLICY HOLDER list", () => {
|
||
expect(p.policyNumber).toBe("700489616");
|
||
expect(p.insuredName).toBe("PAMELA DENISE WAGONER");
|
||
expect(p.legalAddress).toBe("49305 HIGHWAY 74 SPC 10, PALM DESERT, CA, 92260");
|
||
expect(p.zip).toBe("92260");
|
||
});
|
||
|
||
it("lists one driver, not one per printed copy of the page", () => {
|
||
// The face renders three times in the real PDF; an unbounded walk
|
||
// returns the same person three times, which reads as a three-driver
|
||
// policy rather than as a parse bug.
|
||
const tripled = page([ANA_LICENCIA.text, ANA_LICENCIA.text, ANA_LICENCIA.text].join("\n\n"));
|
||
expect(p.drivers).toHaveLength(1);
|
||
expect(parsePolicy(tripled).drivers).toHaveLength(1);
|
||
});
|
||
|
||
it("splits the phone off the name even without the printed column gap", () => {
|
||
// The phone shares the name cell, and the only thing marking it off is
|
||
// white space — which the OCR seam is free to collapse. Depending on the
|
||
// gap surviving is what put "PAMELA DENISE WAGONER Ph.3102001538" in the
|
||
// insured field, where it matched no customer.
|
||
const collapsed = page(ANA_LICENCIA.text.replace(/ {2,}/g, " "));
|
||
expect(parsePolicy(collapsed).insuredName).toBe("PAMELA DENISE WAGONER");
|
||
});
|
||
|
||
it("drops the four empty driver slots", () => {
|
||
// Slots 2-5 print an empty NAME and a bare "NONE" licence.
|
||
expect(p.drivers.map((d) => d.fullName)).toEqual(["PAMELA DENISE WAGONER"]);
|
||
expect(p.drivers[0].licenseNumber).toBe("N0017668");
|
||
expect(p.drivers[0].phone).toBe("3102001538");
|
||
});
|
||
|
||
it("insures no vehicle", () => {
|
||
expect(p.vehicles).toEqual([]);
|
||
expect(p.notes.join(" | ")).toMatch(/no ampara un veh[íi]culo determinado/);
|
||
});
|
||
|
||
it("separates the SUM INSURED and PREMIUM columns by position", () => {
|
||
// Both columns print the same shape ("100,000.00 usd." / "18.70 usd.")
|
||
// and neither is labelled per row — only the offset tells them apart.
|
||
const c = byRisk(p);
|
||
const pd = c["LIABILITY FOR PROPERTY DAMAGE TO THIRD PARTIES"];
|
||
expect(pd?.insuredAmount).toBe(100000);
|
||
expect(pd?.premium).toBe(18.7);
|
||
});
|
||
|
||
it("reads the trailing Per Person / Per Accident labels on this layout", () => {
|
||
// They FOLLOW their amount here and PRECEDE it on the automobile face.
|
||
const c = byRisk(p);
|
||
expect(c["BODILY INJURY LIABILITY — POR PERSONA"]?.insuredAmount).toBe(100000);
|
||
expect(c["BODILY INJURY LIABILITY — POR EVENTO"]?.insuredAmount).toBe(200000);
|
||
expect(c["MEDICAL EXPENSES — POR PERSONA"]?.insuredAmount).toBe(4000);
|
||
expect(c["MEDICAL EXPENSES — POR EVENTO"]?.insuredAmount).toBe(20000);
|
||
});
|
||
|
||
it("charges a section's premium once, not once per limit", () => {
|
||
const c = byRisk(p);
|
||
expect(c["BODILY INJURY LIABILITY — POR PERSONA"]?.premium).toBe(54.27);
|
||
expect(c["BODILY INJURY LIABILITY — POR EVENTO"]?.premium).toBeNull();
|
||
});
|
||
|
||
it("handles the section order this layout uses", () => {
|
||
// CATASTROPHIC LIABILITY prints ABOVE MEDICAL EXPENSES here and below it
|
||
// on the automobile face; blocks are keyed by where the labels land.
|
||
const c = byRisk(p);
|
||
expect(c["CATASTROPHIC LIABILITY FOR DEATH OF THIRD PARTIES"]?.insuredAmount).toBe(0);
|
||
expect(c["LEGAL AID"]?.premium).toBe(30);
|
||
expect(c["ROADSIDE ASSISTANCE"]?.premium).toBe(30);
|
||
});
|
||
|
||
it("carries the excluded-risk sentence that defines the product", () => {
|
||
expect(p.notes.join(" | ")).toMatch(/riesgos excluidos: Collision, overtuning/);
|
||
});
|
||
});
|