feat(policies): capture the full premium breakdown
Build and Push Images / Build jorgecuadros-api (push) Successful in 2m19s
Build and Push Images / Build jorgecuadros-web (push) Successful in 2m1s

The capture form only ever had prima neta, derecho de póliza and comisión.
The Access form it replaces has seven figures, and the four that were missing
are the ones that make a policy paid in installments add up.

Adds recargo, IVA, prima total and forma de pago to the policy header, the
same breakdown per installment, and a per-line-of-business IVA rate.

IVA and prima total are the only derived figures:

    base  = prima neta + recargo + derecho de póliza
    IVA   = round(base * tasa)
    total = base + IVA

The recargo is inside the taxable base. That is not a guess — policy 7006785
prints IVA 52.03 on 610.86 + 8.55 + 31.00, and leaving the recargo out gives
51.35, which matches nothing on the page. Both of its money rows are asserted
in premium.spec.ts. The recargo itself is never derived: the carrier quotes it,
so staff key it in, and the field is disabled on ANNUAL/SINGLE. Both derived
figures are stored rather than recomputed on read, and stay editable, because
the printed policy is the record of truth and a later rate change must not
silently restate what was issued.

The rate lives on PolicyType (seeded to 0.08, editable in Catálogos), which is
the legacy one-row IMPUESTOS / IMPUESTOS_AUTOS tables made configurable. The
rate applied is stamped on the policy so an old one reads back at its original
rate.

Per-installment, not two fixed slots on the header: a policy split into several
exhibiciones prices each payment separately — that is why the Access form drew
the money row twice — and a trimestral policy needs four, which the Access
layout could not hold.

Also fixes two losses in the ETL, which is how these went missing:

  - `forma_pago` was marked consumed by the coverage sweep and then never
    written to any column, so FORMA PAGO existed nowhere in the platform.
  - `recargo` and the whole second money row fell into `coveragesJson` as
    loose strings, mislabeled as coverage amounts.

transform_policies.py now writes all of it directly;
backfill_policy_premium_breakdown.py recovers it on a database that must not be
re-imported, and strips the migrated keys back out of coveragesJson. Both are
COALESCE-only, so a figure a human has corrected in the app wins.

IVA and TOTAL are NOT backfilled: they were unbound calculated controls on the
Access form, never columns, so there is nothing to recover and every migrated
policy reads null until it is edited.

The backfill warns on 5 annual policies that carry a non-zero recargo — a
contradiction that predates this change and is left for a human, not silently
corrected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 00:24:22 -07:00
co-authored by Claude Opus 5
parent 8c144fe8c4
commit 48e01ddd21
20 changed files with 1049 additions and 60 deletions
+22
View File
@@ -179,6 +179,28 @@ Each of these is a known, deliberate stopping point rather than a bug.
be added.
- No `SKIPPED_NO_EMAIL` worklist (see 1.9).
**Captura de pólizas — desglose de primas** (built 2026-08-18)
- **IVA y prima total no existen en los datos legacy.** En Access eran
controles calculados sin campo, así que las 2,378 pólizas migradas leen
`tax` y `total` en null hasta que alguien las edite. No es recuperable: no
hay de dónde.
- **El OCR de A.N.A. lee `TAX` y `LOCAL TAX` y los tira.** El parser ya extrae
la fila `DISCOUNT | PREMIUM | POLICY FEE | TAX | LOCAL TAX | TOTAL`
(`policy-parser.ts`), pero `ParsedPolicy` no tiene campo para el impuesto,
así que la ruta OCR sigue guardando `tax` en null aunque el papel lo
imprima. Cerrarlo son: campo en `ParsedPolicy`, columna
`extractedTax` en `policy_ocr_documents`, campo en la pantalla de revisión,
y escritura en confirm. `LOCAL TAX` no tiene columna destino y habría que
decidir si suma al IVA o va aparte.
- **El recargo no se valida contra la forma de pago en datos migrados.** El
formulario lo deshabilita en ANUAL/CONTADO, pero
`backfill_policy_premium_breakdown.py` solo advierte cuando encuentra una
póliza anual con recargo; no la corrige.
- **Las parcialidades 3 y 4 no llevan desglose.** Access solo dibujó la fila
de dinero dos veces, así que una póliza trimestral capturada hoy sí puede
llenar las cuatro a mano, pero no hay nada legacy que migrar a las dos
últimas.
**Policy OCR** — [`POLICY_OCR.md`](POLICY_OCR.md)
- **GMX and A.N.A. only.** The dispatcher is a `[provider, pattern]` table plus
a parser map, so a third carrier is one function and two entries — but no
+6
View File
@@ -440,6 +440,12 @@ take a `policyType` select param. The workflow is *not* MULT-only: the legacy
Params: ramo (with an "todos" option), aseguradora, date range on `policyFrom`.
Columns: póliza, cliente, ramo, aseguradora, vigencia, prima neta, forma de pago.
️ `forma de pago` became a real column on 2026-08-18 (`Policy.paymentFrequency`).
It is **null on every policy migrated before that date** — the original ETL
marked Access's `FORMA PAGO` consumed and then never wrote it anywhere — so the
report must render null as "—" rather than assuming annual. Running
`backfill_policy_premium_breakdown.py` recovers it from the staged Parquet.
Totals: count + prima neta sum per currency (**never collapse MXN and USD** —
same constraint as the billing module).