feat(policy-ocr): store the IVA A.N.A. already prints
Build and Push Images / Build jorgecuadros-api (push) Successful in 2m16s
Build and Push Images / Build jorgecuadros-web (push) Successful in 1m50s

The parser has been reading A.N.A.'s `TAX` cell since the ANA layout landed,
but `ParsedPolicy` had nowhere to put it, so the figure only ever reached a
review note and every OCR-confirmed policy was written with `tax` null — even
though the paper states it.

`TAX` now flows parser -> `extractedTax` -> `Policy.tax`, alongside the premium
fields beside it. GMX stays null: its certificate carries no premium at all, so
there is no tax on it either.

The other two cells stay out, for reasons worth keeping:

  - `LOCAL TAX` is a separate levy with no destination column, and summing it
    into `tax` would produce an IVA that no longer divides back to a rate —
    which is the whole reason to store the figure. It reads 0.00 on every
    policy seen so far; a non-zero one now raises a note saying the total will
    not reconcile, instead of quietly inflating the IVA.
  - `DISCOUNT` has no column and prints as a bare "-" when unused, which is
    what makes the row positional rather than "find six amounts".

`taxRate` is left null by confirm. A.N.A. prints the amount, not the rate, and
back-dividing it would mint a rate the document never stated; the capture form
resolves one from the line of business instead.

The review screen gains derecho de póliza next to the new IVA field. It was
already parsed and already written on confirm, but never shown — and an IVA
with no fee beside it leaves the reviewer unable to see why premium + fee + tax
equals the printed total.

The spec's assertion moved off the note and onto the field, plus a check that
the row reconciles: 298.61 + 30.00 at 8% is 26.29, totalling 354.90. That
agreement is what proves the positional mapping landed on the right cells
rather than merely on six numbers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rmancinasandClaude Opus 5 committed 2026-08-18 07:38:23 -07:00
1 parent 48e01ddd21
commit 75e9f582b4
10 files changed
+128 -11

No files matched your search

@@ -36,6 +36,17 @@ export interface ParsedPolicy {
netPremium: number | null;
policyFee: number | null;
brokerFee: number | null;
/**
* IVA, off A.N.A.'s `TAX` cell. Null on GMX — its certificate carries no
* premium at all, so there is no tax on it to read either.
*
* The adjacent `LOCAL TAX` cell is deliberately NOT folded in here. It is a
* separate levy with no column of its own, and summing the two would report
* an IVA figure that no longer divides back to a rate — the whole point of
* storing it. It prints 0.00 on every policy seen so far and is surfaced as
* a note when it is not.
*/
tax: number | null;
total: number | null;
/** "CONTADO" / "MENSUAL" / … — premium-payment cadence text. */
premiumPayment: string | null;
@@ -319,6 +330,7 @@ function emptyParsedPolicy(provider: string): ParsedPolicy {
netPremium: null,
policyFee: null,
brokerFee: null,
tax: null,
total: null,
premiumPayment: null,
coverages: [],
@@ -1178,6 +1190,7 @@ interface AnaHeader {
coveragePeriodDays: number | null;
netPremium: number | null;
policyFee: number | null;
tax: number | null;
total: number | null;
}
@@ -1253,8 +1266,13 @@ function parseAnaHeader(lines: string[], notes: string[]): AnaHeader {
// ----- money row ---------------------------------------------------------
const row = anaMoneyRow(lines);
if (!row) notes.push("no se pudo leer el renglón de primas");
if (row?.tax) notes.push(`impuesto: ${row.tax.toFixed(2)}`);
if (row?.discount) notes.push(`descuento: ${row.discount.toFixed(2)}`);
// LOCAL TAX has no destination column and prints 0.00 on every A.N.A. policy
// seen so far. A non-zero one means the total will not reconcile against the
// stored IVA, so say so rather than folding it in and hiding the difference.
if (row?.localTax) {
notes.push(`impuesto local ${row.localTax.toFixed(2)} no capturado`);
}
return {
policyNumber,
@@ -1266,6 +1284,7 @@ function parseAnaHeader(lines: string[], notes: string[]): AnaHeader {
coveragePeriodDays,
netPremium: row?.netPremium ?? null,
policyFee: row?.policyFee ?? null,
tax: row?.tax ?? null,
total: row?.total ?? null,
};
}
@@ -1455,6 +1474,7 @@ function parseAnaAutomobile(lines: string[]): ParsedPolicy {
currency: anaCurrency(text, notes),
netPremium: header.netPremium,
policyFee: header.policyFee,
tax: header.tax,
total: header.total,
premiumPayment: paymentDeadline,
coverages,
@@ -1854,6 +1874,7 @@ function parseAnaDriverPolicy(lines: string[]): ParsedPolicy {
currency: anaCurrency(text, notes),
netPremium: header.netPremium,
policyFee: header.policyFee,
tax: header.tax,
total: header.total,
coverages,
coveragePeriodDays: header.coveragePeriodDays,