feat(policy-ocr): store the IVA A.N.A. already prints
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:
@@ -689,8 +689,23 @@ describe("parsePolicy / ANA automobile", () => {
|
|||||||
// reading would shift every value one column left.
|
// reading would shift every value one column left.
|
||||||
expect(p.netPremium).toBe(298.61);
|
expect(p.netPremium).toBe(298.61);
|
||||||
expect(p.policyFee).toBe(30);
|
expect(p.policyFee).toBe(30);
|
||||||
|
expect(p.tax).toBe(26.29);
|
||||||
expect(p.total).toBe(354.9);
|
expect(p.total).toBe(354.9);
|
||||||
expect(p.notes.join(" | ")).toMatch(/impuesto: 26\.29/);
|
});
|
||||||
|
|
||||||
|
it("reads a TAX that reconciles against the rest of the row", () => {
|
||||||
|
// 298.61 + 30.00 = 328.61, taxed at 8% -> 26.29, totalling 354.90. The
|
||||||
|
// whole row agreeing is what proves the positional mapping landed on the
|
||||||
|
// right cells rather than merely on six numbers.
|
||||||
|
const base = p.netPremium! + p.policyFee!;
|
||||||
|
expect(Math.round(base * 0.08 * 100) / 100).toBe(p.tax);
|
||||||
|
expect(Math.round((base + p.tax!) * 100) / 100).toBe(p.total);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not fold LOCAL TAX into the IVA", () => {
|
||||||
|
// It prints 0.00 here, so nothing to fold — but the guard is that a
|
||||||
|
// non-zero one would surface as a note instead of inflating `tax`.
|
||||||
|
expect(p.notes.join(" | ")).not.toMatch(/impuesto local/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reads the vehicle by token role, not by column", () => {
|
it("reads the vehicle by token role, not by column", () => {
|
||||||
|
|||||||
@@ -36,6 +36,17 @@ export interface ParsedPolicy {
|
|||||||
netPremium: number | null;
|
netPremium: number | null;
|
||||||
policyFee: number | null;
|
policyFee: number | null;
|
||||||
brokerFee: 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;
|
total: number | null;
|
||||||
/** "CONTADO" / "MENSUAL" / … — premium-payment cadence text. */
|
/** "CONTADO" / "MENSUAL" / … — premium-payment cadence text. */
|
||||||
premiumPayment: string | null;
|
premiumPayment: string | null;
|
||||||
@@ -319,6 +330,7 @@ function emptyParsedPolicy(provider: string): ParsedPolicy {
|
|||||||
netPremium: null,
|
netPremium: null,
|
||||||
policyFee: null,
|
policyFee: null,
|
||||||
brokerFee: null,
|
brokerFee: null,
|
||||||
|
tax: null,
|
||||||
total: null,
|
total: null,
|
||||||
premiumPayment: null,
|
premiumPayment: null,
|
||||||
coverages: [],
|
coverages: [],
|
||||||
@@ -1178,6 +1190,7 @@ interface AnaHeader {
|
|||||||
coveragePeriodDays: number | null;
|
coveragePeriodDays: number | null;
|
||||||
netPremium: number | null;
|
netPremium: number | null;
|
||||||
policyFee: number | null;
|
policyFee: number | null;
|
||||||
|
tax: number | null;
|
||||||
total: number | null;
|
total: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1253,8 +1266,13 @@ function parseAnaHeader(lines: string[], notes: string[]): AnaHeader {
|
|||||||
// ----- money row ---------------------------------------------------------
|
// ----- money row ---------------------------------------------------------
|
||||||
const row = anaMoneyRow(lines);
|
const row = anaMoneyRow(lines);
|
||||||
if (!row) notes.push("no se pudo leer el renglón de primas");
|
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)}`);
|
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 {
|
return {
|
||||||
policyNumber,
|
policyNumber,
|
||||||
@@ -1266,6 +1284,7 @@ function parseAnaHeader(lines: string[], notes: string[]): AnaHeader {
|
|||||||
coveragePeriodDays,
|
coveragePeriodDays,
|
||||||
netPremium: row?.netPremium ?? null,
|
netPremium: row?.netPremium ?? null,
|
||||||
policyFee: row?.policyFee ?? null,
|
policyFee: row?.policyFee ?? null,
|
||||||
|
tax: row?.tax ?? null,
|
||||||
total: row?.total ?? null,
|
total: row?.total ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1455,6 +1474,7 @@ function parseAnaAutomobile(lines: string[]): ParsedPolicy {
|
|||||||
currency: anaCurrency(text, notes),
|
currency: anaCurrency(text, notes),
|
||||||
netPremium: header.netPremium,
|
netPremium: header.netPremium,
|
||||||
policyFee: header.policyFee,
|
policyFee: header.policyFee,
|
||||||
|
tax: header.tax,
|
||||||
total: header.total,
|
total: header.total,
|
||||||
premiumPayment: paymentDeadline,
|
premiumPayment: paymentDeadline,
|
||||||
coverages,
|
coverages,
|
||||||
@@ -1854,6 +1874,7 @@ function parseAnaDriverPolicy(lines: string[]): ParsedPolicy {
|
|||||||
currency: anaCurrency(text, notes),
|
currency: anaCurrency(text, notes),
|
||||||
netPremium: header.netPremium,
|
netPremium: header.netPremium,
|
||||||
policyFee: header.policyFee,
|
policyFee: header.policyFee,
|
||||||
|
tax: header.tax,
|
||||||
total: header.total,
|
total: header.total,
|
||||||
coverages,
|
coverages,
|
||||||
coveragePeriodDays: header.coveragePeriodDays,
|
coveragePeriodDays: header.coveragePeriodDays,
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export class ConfirmPolicyDocumentDto {
|
|||||||
@IsOptional() @IsNumber() netPremium?: number;
|
@IsOptional() @IsNumber() netPremium?: number;
|
||||||
@IsOptional() @IsNumber() policyFee?: number;
|
@IsOptional() @IsNumber() policyFee?: number;
|
||||||
@IsOptional() @IsNumber() brokerFee?: number;
|
@IsOptional() @IsNumber() brokerFee?: number;
|
||||||
|
@IsOptional() @IsNumber() tax?: number;
|
||||||
@IsOptional() @IsNumber() total?: number;
|
@IsOptional() @IsNumber() total?: number;
|
||||||
@IsOptional() @IsString() premiumPayment?: string;
|
@IsOptional() @IsString() premiumPayment?: string;
|
||||||
/** Printed term in days. Omitted leaves the parsed value (or the schema's
|
/** Printed term in days. Omitted leaves the parsed value (or the schema's
|
||||||
@@ -79,6 +80,7 @@ export class ReviewPolicyDocumentDto {
|
|||||||
@IsOptional() @IsNumber() netPremium?: number;
|
@IsOptional() @IsNumber() netPremium?: number;
|
||||||
@IsOptional() @IsNumber() policyFee?: number;
|
@IsOptional() @IsNumber() policyFee?: number;
|
||||||
@IsOptional() @IsNumber() brokerFee?: number;
|
@IsOptional() @IsNumber() brokerFee?: number;
|
||||||
|
@IsOptional() @IsNumber() tax?: number;
|
||||||
@IsOptional() @IsNumber() total?: number;
|
@IsOptional() @IsNumber() total?: number;
|
||||||
@IsOptional() @IsString() premiumPayment?: string;
|
@IsOptional() @IsString() premiumPayment?: string;
|
||||||
@IsOptional() @IsInt() @Min(1) @Max(3660) coveragePeriodDays?: number;
|
@IsOptional() @IsInt() @Min(1) @Max(3660) coveragePeriodDays?: number;
|
||||||
|
|||||||
@@ -192,6 +192,8 @@ export class PolicyOcrService {
|
|||||||
parsed.policyFee != null ? new Prisma.Decimal(parsed.policyFee) : null,
|
parsed.policyFee != null ? new Prisma.Decimal(parsed.policyFee) : null,
|
||||||
extractedBrokerFee:
|
extractedBrokerFee:
|
||||||
parsed.brokerFee != null ? new Prisma.Decimal(parsed.brokerFee) : null,
|
parsed.brokerFee != null ? new Prisma.Decimal(parsed.brokerFee) : null,
|
||||||
|
extractedTax:
|
||||||
|
parsed.tax != null ? new Prisma.Decimal(parsed.tax) : null,
|
||||||
extractedTotal:
|
extractedTotal:
|
||||||
parsed.total != null ? new Prisma.Decimal(parsed.total) : null,
|
parsed.total != null ? new Prisma.Decimal(parsed.total) : null,
|
||||||
extractedCoveragesJson: parsed.coverages.length
|
extractedCoveragesJson: parsed.coverages.length
|
||||||
@@ -365,6 +367,7 @@ export class PolicyOcrService {
|
|||||||
dto.policyFee != null ? new Prisma.Decimal(dto.policyFee) : undefined,
|
dto.policyFee != null ? new Prisma.Decimal(dto.policyFee) : undefined,
|
||||||
extractedBrokerFee:
|
extractedBrokerFee:
|
||||||
dto.brokerFee != null ? new Prisma.Decimal(dto.brokerFee) : undefined,
|
dto.brokerFee != null ? new Prisma.Decimal(dto.brokerFee) : undefined,
|
||||||
|
extractedTax: dto.tax != null ? new Prisma.Decimal(dto.tax) : undefined,
|
||||||
extractedTotal:
|
extractedTotal:
|
||||||
dto.total != null ? new Prisma.Decimal(dto.total) : undefined,
|
dto.total != null ? new Prisma.Decimal(dto.total) : undefined,
|
||||||
extractedCoveragesJson: dto.coveragesJson
|
extractedCoveragesJson: dto.coveragesJson
|
||||||
@@ -799,6 +802,7 @@ function buildPolicyUpdateFromDoc(
|
|||||||
extractedNetPremium: Prisma.Decimal | null;
|
extractedNetPremium: Prisma.Decimal | null;
|
||||||
extractedPolicyFee: Prisma.Decimal | null;
|
extractedPolicyFee: Prisma.Decimal | null;
|
||||||
extractedBrokerFee: Prisma.Decimal | null;
|
extractedBrokerFee: Prisma.Decimal | null;
|
||||||
|
extractedTax: Prisma.Decimal | null;
|
||||||
extractedTotal: Prisma.Decimal | null;
|
extractedTotal: Prisma.Decimal | null;
|
||||||
extractedCoveragesJson: Prisma.JsonValue | null;
|
extractedCoveragesJson: Prisma.JsonValue | null;
|
||||||
extractedPremiumPayment: string | null;
|
extractedPremiumPayment: string | null;
|
||||||
@@ -845,6 +849,10 @@ function buildPolicyUpdateFromDoc(
|
|||||||
netPremium: numOrUndef(item.netPremium, doc.extractedNetPremium),
|
netPremium: numOrUndef(item.netPremium, doc.extractedNetPremium),
|
||||||
policyFee: numOrUndef(item.policyFee, doc.extractedPolicyFee),
|
policyFee: numOrUndef(item.policyFee, doc.extractedPolicyFee),
|
||||||
brokerFee: numOrUndef(item.brokerFee, doc.extractedBrokerFee),
|
brokerFee: numOrUndef(item.brokerFee, doc.extractedBrokerFee),
|
||||||
|
// `taxRate` is deliberately left alone. A.N.A. prints the IVA amount, not
|
||||||
|
// the rate, and back-dividing it would mint a rate the document never
|
||||||
|
// stated — the policy form resolves one from the line of business instead.
|
||||||
|
tax: numOrUndef(item.tax, doc.extractedTax),
|
||||||
total: numOrUndef(item.total, doc.extractedTotal),
|
total: numOrUndef(item.total, doc.extractedTotal),
|
||||||
// coveragesJson / observations: freeform, keep the GMX data when present.
|
// coveragesJson / observations: freeform, keep the GMX data when present.
|
||||||
coveragesJson:
|
coveragesJson:
|
||||||
@@ -886,6 +894,7 @@ function buildPolicyCreateFromDoc(
|
|||||||
extractedNetPremium: Prisma.Decimal | null;
|
extractedNetPremium: Prisma.Decimal | null;
|
||||||
extractedPolicyFee: Prisma.Decimal | null;
|
extractedPolicyFee: Prisma.Decimal | null;
|
||||||
extractedBrokerFee: Prisma.Decimal | null;
|
extractedBrokerFee: Prisma.Decimal | null;
|
||||||
|
extractedTax: Prisma.Decimal | null;
|
||||||
extractedTotal: Prisma.Decimal | null;
|
extractedTotal: Prisma.Decimal | null;
|
||||||
extractedCoveragesJson: Prisma.JsonValue | null;
|
extractedCoveragesJson: Prisma.JsonValue | null;
|
||||||
extractedPremiumPayment: string | null;
|
extractedPremiumPayment: string | null;
|
||||||
@@ -936,6 +945,10 @@ function buildPolicyCreateFromDoc(
|
|||||||
netPremium: numOrUndef(item.netPremium, doc.extractedNetPremium),
|
netPremium: numOrUndef(item.netPremium, doc.extractedNetPremium),
|
||||||
policyFee: numOrUndef(item.policyFee, doc.extractedPolicyFee),
|
policyFee: numOrUndef(item.policyFee, doc.extractedPolicyFee),
|
||||||
brokerFee: numOrUndef(item.brokerFee, doc.extractedBrokerFee),
|
brokerFee: numOrUndef(item.brokerFee, doc.extractedBrokerFee),
|
||||||
|
// `taxRate` is deliberately left alone. A.N.A. prints the IVA amount, not
|
||||||
|
// the rate, and back-dividing it would mint a rate the document never
|
||||||
|
// stated — the policy form resolves one from the line of business instead.
|
||||||
|
tax: numOrUndef(item.tax, doc.extractedTax),
|
||||||
total: numOrUndef(item.total, doc.extractedTotal),
|
total: numOrUndef(item.total, doc.extractedTotal),
|
||||||
coveragesJson:
|
coveragesJson:
|
||||||
item.coveragesJson !== undefined
|
item.coveragesJson !== undefined
|
||||||
|
|||||||
@@ -276,6 +276,8 @@ function DocumentRow({ doc, customerIndex, canReview, onSave, onReject }: Docume
|
|||||||
policyDate: doc.extractedPolicyDate?.slice(0, 10) ?? "",
|
policyDate: doc.extractedPolicyDate?.slice(0, 10) ?? "",
|
||||||
currency: doc.extractedCurrency ?? "USD",
|
currency: doc.extractedCurrency ?? "USD",
|
||||||
netPremium: doc.extractedNetPremium ?? "",
|
netPremium: doc.extractedNetPremium ?? "",
|
||||||
|
policyFee: doc.extractedPolicyFee ?? "",
|
||||||
|
tax: doc.extractedTax ?? "",
|
||||||
total: doc.extractedTotal ?? "",
|
total: doc.extractedTotal ?? "",
|
||||||
premiumPayment: doc.extractedPremiumPayment ?? "",
|
premiumPayment: doc.extractedPremiumPayment ?? "",
|
||||||
coveragePeriodDays: doc.extractedCoveragePeriodDays?.toString() ?? "",
|
coveragePeriodDays: doc.extractedCoveragePeriodDays?.toString() ?? "",
|
||||||
@@ -314,6 +316,8 @@ function DocumentRow({ doc, customerIndex, canReview, onSave, onReject }: Docume
|
|||||||
policyDate: v.policyDate || undefined,
|
policyDate: v.policyDate || undefined,
|
||||||
currency,
|
currency,
|
||||||
netPremium: numOrUndef(v.netPremium),
|
netPremium: numOrUndef(v.netPremium),
|
||||||
|
policyFee: numOrUndef(v.policyFee),
|
||||||
|
tax: numOrUndef(v.tax),
|
||||||
total: numOrUndef(v.total),
|
total: numOrUndef(v.total),
|
||||||
premiumPayment: trimOrUndef(v.premiumPayment),
|
premiumPayment: trimOrUndef(v.premiumPayment),
|
||||||
coveragePeriodDays: numOrUndef(v.coveragePeriodDays),
|
coveragePeriodDays: numOrUndef(v.coveragePeriodDays),
|
||||||
@@ -336,6 +340,8 @@ function DocumentRow({ doc, customerIndex, canReview, onSave, onReject }: Docume
|
|||||||
policyDate: reviewInput.policyDate,
|
policyDate: reviewInput.policyDate,
|
||||||
currency: (currency as "MXN" | "USD" | "EUR" | undefined) ?? undefined,
|
currency: (currency as "MXN" | "USD" | "EUR" | undefined) ?? undefined,
|
||||||
netPremium: reviewInput.netPremium,
|
netPremium: reviewInput.netPremium,
|
||||||
|
policyFee: reviewInput.policyFee,
|
||||||
|
tax: reviewInput.tax,
|
||||||
total: reviewInput.total,
|
total: reviewInput.total,
|
||||||
premiumPayment: reviewInput.premiumPayment,
|
premiumPayment: reviewInput.premiumPayment,
|
||||||
coveragePeriodDays: reviewInput.coveragePeriodDays,
|
coveragePeriodDays: reviewInput.coveragePeriodDays,
|
||||||
@@ -501,7 +507,25 @@ function DocumentRow({ doc, customerIndex, canReview, onSave, onReject }: Docume
|
|||||||
onChange={(e) => set("netPremium", e.target.value)}
|
onChange={(e) => set("netPremium", e.target.value)}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Total">
|
<Field label="Derecho de póliza">
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
value={v.policyFee}
|
||||||
|
onChange={(e) => set("policyFee", e.target.value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label="IVA">
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
value={v.tax}
|
||||||
|
onChange={(e) => set("tax", e.target.value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label="Prima total">
|
||||||
<input
|
<input
|
||||||
className="input"
|
className="input"
|
||||||
type="number"
|
type="number"
|
||||||
|
|||||||
@@ -1555,6 +1555,10 @@ export interface PolicyOcrDocument {
|
|||||||
extractedNetPremium: string | null;
|
extractedNetPremium: string | null;
|
||||||
extractedPolicyFee: string | null;
|
extractedPolicyFee: string | null;
|
||||||
extractedBrokerFee: string | null;
|
extractedBrokerFee: string | null;
|
||||||
|
/** IVA off A.N.A.'s `TAX` cell. Null on GMX — its certificate carries no
|
||||||
|
* premium, so no tax either. `LOCAL TAX` is not folded in; a non-zero one
|
||||||
|
* shows up in `matchNote`. */
|
||||||
|
extractedTax: string | null;
|
||||||
extractedTotal: string | null;
|
extractedTotal: string | null;
|
||||||
extractedCoveragesJson: PolicyOcrCoverage[] | null;
|
extractedCoveragesJson: PolicyOcrCoverage[] | null;
|
||||||
extractedPremiumPayment: string | null;
|
extractedPremiumPayment: string | null;
|
||||||
@@ -1589,6 +1593,7 @@ export interface PolicyOcrReviewInput {
|
|||||||
netPremium?: number;
|
netPremium?: number;
|
||||||
policyFee?: number;
|
policyFee?: number;
|
||||||
brokerFee?: number;
|
brokerFee?: number;
|
||||||
|
tax?: number;
|
||||||
total?: number;
|
total?: number;
|
||||||
premiumPayment?: string;
|
premiumPayment?: string;
|
||||||
coveragePeriodDays?: number;
|
coveragePeriodDays?: number;
|
||||||
@@ -1615,6 +1620,7 @@ export interface PolicyOcrConfirmDocument {
|
|||||||
netPremium?: number;
|
netPremium?: number;
|
||||||
policyFee?: number;
|
policyFee?: number;
|
||||||
brokerFee?: number;
|
brokerFee?: number;
|
||||||
|
tax?: number;
|
||||||
total?: number;
|
total?: number;
|
||||||
premiumPayment?: string;
|
premiumPayment?: string;
|
||||||
coveragePeriodDays?: number;
|
coveragePeriodDays?: number;
|
||||||
|
|||||||
+9
-8
@@ -184,14 +184,15 @@ Each of these is a known, deliberate stopping point rather than a bug.
|
|||||||
controles calculados sin campo, así que las 2,378 pólizas migradas leen
|
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
|
`tax` y `total` en null hasta que alguien las edite. No es recuperable: no
|
||||||
hay de dónde.
|
hay de dónde.
|
||||||
- **El OCR de A.N.A. lee `TAX` y `LOCAL TAX` y los tira.** El parser ya extrae
|
- **`LOCAL TAX` de A.N.A. no se captura.** El IVA (`TAX`) sí — se guarda desde
|
||||||
la fila `DISCOUNT | PREMIUM | POLICY FEE | TAX | LOCAL TAX | TOTAL`
|
2026-08-18 — pero `LOCAL TAX` es un gravamen distinto sin columna destino y
|
||||||
(`policy-parser.ts`), pero `ParsedPolicy` no tiene campo para el impuesto,
|
**no** se suma al IVA: sumarlo daría una cifra que ya no divide de vuelta a
|
||||||
así que la ruta OCR sigue guardando `tax` en null aunque el papel lo
|
una tasa. Imprime 0.00 en todas las pólizas vistas hasta hoy; una distinta
|
||||||
imprima. Cerrarlo son: campo en `ParsedPolicy`, columna
|
de cero levanta la nota *"impuesto local N no capturado"* y significa que
|
||||||
`extractedTax` en `policy_ocr_documents`, campo en la pantalla de revisión,
|
`total` no cuadra contra `netPremium + policyFee + tax`.
|
||||||
y escritura en confirm. `LOCAL TAX` no tiene columna destino y habría que
|
- **`Policy.taxRate` queda en null por la ruta OCR.** A.N.A. imprime el monto
|
||||||
decidir si suma al IVA o va aparte.
|
del IVA, no la tasa, y despejarla a la inversa inventaría una tasa que el
|
||||||
|
documento nunca declaró. El formulario resuelve una desde el ramo.
|
||||||
- **El recargo no se valida contra la forma de pago en datos migrados.** El
|
- **El recargo no se valida contra la forma de pago en datos migrados.** El
|
||||||
formulario lo deshabilita en ANUAL/CONTADO, pero
|
formulario lo deshabilita en ANUAL/CONTADO, pero
|
||||||
`backfill_policy_premium_breakdown.py` solo advierte cuando encuentra una
|
`backfill_policy_premium_breakdown.py` solo advierte cuando encuentra una
|
||||||
|
|||||||
@@ -365,6 +365,27 @@ A.N.A.'s faces do print one — the `DISCOUNT / PREMIUM / POLICY FEE / TAX /
|
|||||||
LOCAL TAX / TOTAL` row is on the same page — so an ANA document reaches the
|
LOCAL TAX / TOTAL` row is on the same page — so an ANA document reaches the
|
||||||
review queue with `netPremium` populated and `postPremium` already ticked.
|
review queue with `netPremium` populated and `postPremium` already ticked.
|
||||||
|
|
||||||
|
Four of those six cells are stored: `PREMIUM` → `netPremium`, `POLICY FEE` →
|
||||||
|
`policyFee`, `TAX` → `tax` (`extractedTax` on the document, `Policy.tax` on
|
||||||
|
confirm), `TOTAL` → `total`. `DISCOUNT` and `LOCAL TAX` are reported as notes
|
||||||
|
instead:
|
||||||
|
|
||||||
|
- **`DISCOUNT`** has no column, and it prints as a bare `-` when unused, which
|
||||||
|
is what makes the row positional rather than "find six amounts".
|
||||||
|
- **`LOCAL TAX`** is a separate levy and is deliberately **not** summed into
|
||||||
|
`tax`. Folding it in would produce an IVA figure that no longer divides back
|
||||||
|
to a rate, which is the reason to store it at all. It reads 0.00 on every
|
||||||
|
A.N.A. policy seen so far; a non-zero one raises
|
||||||
|
*"impuesto local N no capturado"* and means `total` will not reconcile
|
||||||
|
against `netPremium + policyFee + tax`.
|
||||||
|
|
||||||
|
`Policy.taxRate` is left null by confirm. A.N.A. prints the IVA **amount**, not
|
||||||
|
the rate, and back-dividing one would mint a rate the document never stated —
|
||||||
|
the capture form resolves it from the line of business instead
|
||||||
|
(`PolicyType.taxRate`, see `apps/api/src/policies/premium.ts`). The figures do
|
||||||
|
agree: 298.61 + 30.00 taxed at 8% is 26.29, totalling 354.90, asserted in
|
||||||
|
`policy-parser.spec.ts`.
|
||||||
|
|
||||||
Deductible and loss participation are stored as **strings** (`"5%"`, `"20%"`,
|
Deductible and loss participation are stored as **strings** (`"5%"`, `"20%"`,
|
||||||
`"USD 1,000"`) — they are printed as a mix of percentages, currency amounts
|
`"USD 1,000"`) — they are printed as a mix of percentages, currency amounts
|
||||||
and free text, and normalising them would lose the distinction.
|
and free text, and normalising them would lose the distinction.
|
||||||
|
|||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
-- A.N.A. prints IVA on the policy face and the parser already read it, but
|
||||||
|
-- `ParsedPolicy` had no field for it, so the figure only ever reached a review
|
||||||
|
-- note and the confirmed Policy was written with `tax` null. This gives it a
|
||||||
|
-- column, matching the premium fields beside it.
|
||||||
|
--
|
||||||
|
-- GMX stays null: its certificate carries no premium at all, so there is no
|
||||||
|
-- tax on it to read either.
|
||||||
|
ALTER TABLE `policy_ocr_documents`
|
||||||
|
ADD COLUMN `extractedTax` DECIMAL(12, 2) NULL;
|
||||||
@@ -503,6 +503,11 @@ model PolicyOcrDocument {
|
|||||||
extractedNetPremium Decimal? @db.Decimal(12, 2)
|
extractedNetPremium Decimal? @db.Decimal(12, 2)
|
||||||
extractedPolicyFee Decimal? @db.Decimal(12, 2)
|
extractedPolicyFee Decimal? @db.Decimal(12, 2)
|
||||||
extractedBrokerFee Decimal? @db.Decimal(12, 2)
|
extractedBrokerFee Decimal? @db.Decimal(12, 2)
|
||||||
|
/// IVA off A.N.A.'s `TAX` cell. Null on GMX, whose certificate carries no
|
||||||
|
/// premium at all. The adjacent `LOCAL TAX` is a separate levy with no
|
||||||
|
/// column of its own and is NOT summed in — it would make the figure stop
|
||||||
|
/// dividing back to a rate; the parser reports a non-zero one as a note.
|
||||||
|
extractedTax Decimal? @db.Decimal(12, 2)
|
||||||
extractedTotal Decimal? @db.Decimal(12, 2)
|
extractedTotal Decimal? @db.Decimal(12, 2)
|
||||||
/// Per-coverage rows from the GMX "Material damages" / "Additional risk"
|
/// Per-coverage rows from the GMX "Material damages" / "Additional risk"
|
||||||
/// tables and ANA's numbered risk sections — preserved verbatim so a
|
/// tables and ANA's numbered risk sections — preserved verbatim so a
|
||||||
|
|||||||
Reference in New Issue
Block a user