Companion to docs/RECEIPT_CAPTURE_SPEC.md — the insurance half of the 2026-07-25/26 meeting. Documentation only; no application code. Verified against the code and a live query of the dev DB rather than designed from the meeting notes alone, which changed several conclusions: - Renewal emails and the liquidación batch are much smaller than they look. RenewalNotice + its @@unique([policyId, generation]) idempotency key and the aviso-renovacion letter body already exist; the per-policy liquidation fields are wired end to end. What's missing is a scheduler, a mail client, and the batch layer. - Carrier research: ANA and GMX are one company (Grupo Valore). ANA exposes a live SOAP service with a published operation list; GMX publishes no machine interface at all. Every ANA operation serves new-business quoting/issuance, not "list my book" — so the direction question decides whether the feature is buildable. - UTILSEG is unusable for Utilities↔Seguros reconciliation and the spec closes that long-standing open question: DATGRAL.[NUM UTIL] is authoritative (name match 298/563 vs 58/1024), and where the two sources overlap they contradict on 170 of 218 shared ids. Also records two live defects found while verifying: policy_types is missing its INCENDIO and M_EMPR rows (the FK is ON DELETE SET NULL, so 5 m_empr policies silently lost their ramo), and the legacy settlement slots don't match the target model (MULT/INCENDIO carry two, M EMPR carries four, Policy collapses to one). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
38 KiB
Insurance Features — Implementation Spec
Source: Jorge Cuadros meeting notes, 2026-07-25/26 (Seguros section), plus a
read-through of the current policies/, reports/, storage/ and auth/
code and a live query of the dev database. This is a forward spec for work
not yet built — contrast with RENEWAL_NOTICES.md,
which documents the legacy renewal-report chain that has already been
migrated into the aviso-renovacion report.
Companion doc: RECEIPT_CAPTURE_SPEC.md covers the
Utility Management half of the same meeting (PLAN.md step 11). This doc is the
insurance half (PLAN.md step 12).
Why these four features are one spec
The meeting produced four insurance asks. They are specified together because
they share a spine — the Policy record and its expiry/settlement lifecycle:
- Renewal notification emails — automates the outbound half of a policy's expiry (30 days before, 15 days before, 7 days after). The report that produces the letter text already exists; nothing sends it.
- Liquidación batch workflow — the settlement half of the same lifecycle. The per-policy fields are wired end to end; only the batch print-and-mark step is missing.
- Certificate / "Solicitud Atlas" — a customer-facing artifact rendered from the same policy record, delivered through the existing PHP portal.
- Carrier API integration — an inbound path that would populate the
same
Policyrows automatically instead of by hand.
1 and 2 are small additions on top of shipped code. 3 is half-buildable and half-blocked on infrastructure. 4 is fully blocked on vendor information.
Two of the four are much smaller than they sound, and the spec says so up front so nobody re-estimates them as greenfield work: §1 needs a scheduler, a mail client and one mutation — the notice table, its idempotency key, and the letter body all exist. §2 needs one report and one endpoint.
Ground truth (verified 2026-07-27, do not re-derive)
Everything below was checked against the code and the dev DB
(192.168.4.212:3307), not inferred from the meeting notes.
What exists
| Thing | Where | State |
|---|---|---|
Policy.liquidated / liquidationNumber / liquidationDate |
schema.prisma:165-167 |
wired end to end (DTOs, ?liquidated= filter, stats, form checkbox, detail label) |
RenewalNotice model + @@unique([policyId, generation]) |
schema.prisma:201-217 |
0 rows — never written by anything |
aviso-renovacion letter report |
reports.registry.ts:623-799 |
shipped; read-only. Its enviadas/pendientes totals are permanently 0 because nothing writes RenewalNotice |
| Letter render + PDF/CSV/XLSX/print outputs | reports.types.ts:32, outputs.ts, ReportRunner.tsx:502 (LetterLayout) |
shipped, reusable as-is |
| S3-style optional-client service pattern | storage.service.ts:28-57 |
the pattern the mail client should copy |
| Single-running-job guard | ops.service.ts:171-176 |
the pattern the cron sweep should copy |
| Ability matrix (17 abilities) | auth/abilities.ts |
single source of truth; web consumes the server-resolved map |
What does not exist
- No scheduler. No
@nestjs/schedule, bull/bullmq, node-cron orsetIntervalinapps/api.ops/spawns detached child processes on user request only. - No mail code or dependency. Nothing in any
package.json,.env.exampleordocker-compose.yml. EmailTemplate/EmailCampaign/EmailLog(schema.prisma:540-571) are dead migrated legacy tables — no FKs, no code touches them. Leave them alone;RenewalNoticeis the send log.express-sessionuses the in-memory default store (main.ts:28-39), so sessions die on API restart. Relevant to any customer-identity idea in §3.- Reports are gated by
AuthenticatedGuardalone (reports.controller.ts:28) — any logged-in user, including VIEWER, can run any report. Adding a mutation to the reports area (§2) means it cannot live on that controller.
Live data shape
| Measure | Value |
|---|---|
| Customers | 1,536 — 1,304 (85%) have a non-blank email |
| Customers holding ≥1 policy | 893 — 815 (91%) have an email |
| Policies | 2,396 (0 archived); 1,865 have policyTo |
| Policies expiring in the next 12 months | 1,045 (≈87/month) |
| Liquidated | 2,170; pending 226 |
liquidationNumber / liquidationDate populated |
2,245 / 2,239 |
| Installments | 4,724 — 1,849 with paidDate, 1,651 with checkNumber |
renewal_notices rows |
0 |
Email volume for §1 sizing: ≈87 policies/month × 3 notices ≈ 260 emails/month, and 91% of policyholders are reachable. This is an email channel, not a print-fallback channel — but see §1's open question on the remaining 9%.
Two meeting terms have no referent in the data
Do not guess at these. Negative greps re-run 2026-07-27 across docs/,
migration/ and apps/. (A third — "GDMX" — turned out to be a typo for
GMX, confirmed with the user; see §4.)
- "Solicitud" — 0 hits. Not a legacy report, form or table. ("Atlas" is a
carrier —
COMP = "ATLAS, S.A."— not a report; seeRENEWAL_NOTICES.md.) The closest legacy artifact to a certificate is the* MENS/*MENSAJEblob letter templates, one per line of business, deliberately excluded from migration (LEGACY_DATABASES.md→ excluded tables). - "Garantías" — 0 hits for
garant/warranty. No table, no column.
For reference, both carriers named in the meeting do appear in the data:
GMX in mult.comp, m_empr.comp and gen1.comp, and ANA SEGUROS
verbatim (with an inconsistent ANA variant in licencias.comp) — which
matches the ANA-autos / GMX-daños split described in §4.
The only hit for transferencia anywhere is a bank-register UI label
(apps/web/src/app/banco/page.tsx:948, a SCOTHIA movement type) — unrelated to
policy settlement. "Número de transferencia" is therefore a new requirement
mapping onto the existing liquidationNumber field, not a missed migration.
Two defects found while verifying this spec
Both are pre-existing, both affect the features below, and both should be fixed as part of §2 rather than filed separately.
(a) INCENDIO and M_EMPR have no policy_types row, and 5 policies lost
their ramo. policy_types currently holds only AUTO, LICENCIAS, MULT.
transform_policies.py:111-115 configures INCENDIO and M_EMPR too, so the
migration creates all five — but policies_policyTypeId_fkey is ON DELETE SET NULL, so deleting an (apparently unused) lookup row silently blanked the
ramo on every policy pointing at it. The 5 m_empr policies now have
policyTypeId = NULL:
3249481 / 3249872 vence 2014-03-26 pendiente
3673 / 1200003673 vence 2013-03-30 pendiente
7000017 sin vigencia liquidada
Consequence: every ramo-parameterized query filters on
policyType: { name: … } (reports.registry.ts:703-707), so these 5 are invisible
to aviso-renovacion and would be invisible to §2's pending-liquidación
report — including 4 that are genuinely pending. INCENDIO is a different
story: the legacy INCENDIO table has exactly 1 row, and it did not
migrate (customer unresolved), so the ramo is legitimately empty — but the
aviso-renovacion "Incendio" dropdown option still promises a report that can
only ever return zero rows.
Fix as part of §2: re-seed the two missing policy_types rows, re-point the 5
orphans, and change the FK to ON DELETE RESTRICT so a lookup delete fails
loudly instead of silently blanking data.
(b) The legacy settlement slots do not match the plan's assumption.
MULT and INCENDIO carry two slots (LIQUIDADA/LIQUIDADA 2,
NUM LIQUIDACION/NUM LIQUIDACION2, F LIQUIDA1/F LIQUIDA2) — but
M EMPR carries four (liquidada … liquidada_4,
num_liquidacion … num_liquidacion4, f_liquida1 … f_liquida4).
Actual usage in the staged data:
| Table | rows | slot 2 number | slot 2 date | slots 3-4 |
|---|---|---|---|---|
mult |
773 | 41 (5.3%) | 39 | n/a |
m_empr |
5 | 0 | 0 | 0 |
incendio |
1 | 0 | 0 | n/a |
So the second slot was used on ~5% of MULT policies and never anywhere else,
and slots 3–4 were never used at all. Policy collapses this to one set, which
means ≤41 rows lost a second settlement record in migration. Design
decision in §2.
Utilities ↔ Seguros reconciliation — resolved, not open
The plan carried this as "two competing sources." It is not competitive; one of them is unusable.
-
SEGUROS 16_be.mdb: DATGRAL.[NUM UTIL]— 563 complete(num_id, num_util)pairs. Validated by comparing the insurance customer's ownNOMBREagainst the utilities customer it points at: 298/563 (53%) match exactly, the remainder being ordinary name variants (spouses, married names, entity vs. person). This is a real link, and it is the keytransform_customers.pyalready uses. -
UTILSEG(1,582 rows) — 379 rows carry both asegurosand autilnumber. Under the obvious reading (seguros→ segurosDATGRAL.num_id,util→ utilitiesDATGRAL.num_id) the row's ownNOMBREmatches the target master's name 58/1,024 and 70/932 of the time respectively — i.e. essentially never. Spot-checking makes it plain:UTILSEG 'STEWART, KENNETH' seguros=220 → 'ZEPEDA, JAIME RAUL' util=441 → 'MENDOZA, SERGIO' UTILSEG 'HANCOCK, STEVENS' seguros=225 → 'RODRIGUEZ, MIKE' util=403 → 'JOW, LILY/EVANS, LARRY' UTILSEG 'HUDSON, RICHARD L.' seguros=227 → 'WELLES, ROBERT' util=218 → 'ARTER, KAREN'And where the two sources overlap they contradict each other: of 218
segurosids present in both, 170 (78%) point at a different utilities customer; only 48 pairs agree outright.
Rule: DATGRAL.[NUM UTIL] is authoritative. UTILSEG is a stale artifact of
an older numbering and must not be used to reconcile customers. This matters
directly to RECEIPT_CAPTURE_SPEC.md §4
(customer-number recycling), which touches the same identity space — a
recycling backfill that consulted UTILSEG would merge unrelated people.
1. Renewal notification emails
What Jorge asked for
Automatic notice to the customer at 30 days before expiry, 15 days before,
and 7 days after — replacing the manual monthly run of the legacy
RENEW/RENEW2/RENEW3 report batch.
What's already built (do not re-build)
- The letter itself:
aviso-renovacion(reports.registry.ts:623-799) already resolves customer, carrier,policyTo, premium, vehicle and the ramo-specificcoveragesJsonkeys (cov.cobertura,cov.csl_limite,cov.gastos_medico,cov.propiedades,cov.personas,cov.servicio_adicional) into a__kind: "letter"row. Do not fork this copy — one letter definition, two render targets. - The send log:
RenewalNotice, with@@unique([policyId, generation])(schema.prisma:216) — this is the idempotency mechanism and it is already in place. A sweep that upserts on that key cannot double-send, even on re-run, redeploy or double-fire. No new dedup design is needed. - The cadence maps onto the existing
generation Intwith no schema change: 30d-before = 1, 15d-before = 2, 7d-after = 3 — exactly the legacy 1st/2nd/3rd notice model.
1.1 The scheduler
Add @nestjs/schedule. One @Cron job, daily, early morning local time.
@Cron("0 6 * * *", { timeZone: "America/Tijuana" })
async sweepRenewals()
Guard multi-replica double-fire the same way ops.service.ts:171-176 guards
concurrent jobs — a DB row, not an in-process flag. Reuse OpsJob with a new
kind, or add a minimal ScheduledRun row; either way the guard must be a
database write, because the API is deployed as a Swarm service and may run more
than one replica.
The sweep must also be manually runnable (an admin endpoint that invokes the same service method), so a missed day can be caught up without waiting 24h and so the job is testable without clock manipulation.
1.2 The sweep query
For each of the three offsets, select non-archived policies whose policyTo
falls on the target date:
| Generation | Target date | Meaning |
|---|---|---|
| 1 | today + 30d |
primer aviso |
| 2 | today + 15d |
segundo aviso |
| 3 | today - 7d |
tercer aviso (vencida) |
archivedAt: null, policyTo non-null. Date comparison must be on the UTC
date, not the timestamp — policyTo is stored midnight-UTC (see the existing
report's Date.UTC(year, month - 1, 1) bounds at reports.registry.ts:700),
and a naive local-time comparison shifts the whole sweep by a day for
America/Tijuana.
For each hit: render the letter, send, then upsert RenewalNotice on
[policyId, generation] with sentAt, channel: EMAIL, and the provider
message id. Upsert after a successful send, not before — a failed send must
leave the row absent so the next day's sweep retries it. A row that already has
sentAt is skipped.
Catch-up behaviour: because the query is date-equality, a day the job doesn't
run is a day of notices silently skipped. Either make the sweep look at a
window (policyTo between the target date and the last successful run's target
date) or record the last successful sweep date and re-run the gap. Recommend
the window — it needs no extra state beyond a lastSweptAt and it degrades
correctly if the API is down for a week.
1.3 The mail client
MailProvider interface:
send(msg: { to: string; subject: string; html: string; attachments?: … })
=> Promise<{ providerId: string }>
Amazon SES is the first and intended implementation — the user already runs SES for mass notification, so this reuses an established sending reputation rather than warming a new channel. Provider choice and budget are settled, not open questions; ≈260 emails/month is negligible against existing usage.
Implement it with @aws-sdk/client-sesv2, mirroring StorageService
(storage.service.ts:28-57) exactly:
- env-driven config (
SES_REGION,SES_FROM,SES_ACCESS_KEY,SES_SECRET_KEY, optionalSES_CONFIGURATION_SET), added to.env.example; - null client when unconfigured,
ServiceUnavailableExceptionon use — an unconfigured mail setup must never crash API boot, same degradation as document storage today; - a no-op/log implementation for dev, selected when SES env vars are absent.
The interface stays swappable for testability, not for vendor escape.
Persist the SES message id — add providerMessageId String? to RenewalNotice
rather than overloading notes, so a bounce or complaint notification can be
traced back to the notice that caused it. (notes stays free-text for staff.)
1.4 Manual mark-as-sent
The aviso-renovacion doc comment (reports.registry.ts:617-621) already
anticipates this: staff who mail a paper notice need to record it.
RenewalNoticeChannel (MAIL | EMAIL) exists for exactly this distinction.
POST /policies/:id/renewal-notices — body { generation, channel, sentAt?, notes? }, upserting on the same unique key. This closes the loop that makes
the report's enviadas/pendientes totals meaningful for the first time.
1.5 Bounces and unsubscribes
Not in the meeting notes, but sending 260 mails/month to a 1,304-address list
built from decades-old Access data will produce bounces. Minimum viable:
record providerMessageId, and add a Customer.emailOptOut Boolean @default(false)
checked by the sweep. Full SNS bounce-webhook handling is out of scope for the
first build — but the opt-out flag is not, because there is no other way for a
customer to stop the mail.
API surface
| Method | Route | Ability |
|---|---|---|
POST |
/policies/:id/renewal-notices |
renewal:send |
POST |
/renewals/sweep (manual trigger of the cron body) |
renewal:send |
GET |
/renewals/pending?days= (what the next sweep would send) |
read (AuthenticatedGuard) |
Abilities (new)
| Ability | Min role | Notes |
|---|---|---|
renewal:send |
MANAGER | sends mail to customers on the office's behalf — a higher trust tier than ordinary data entry |
Add to both the Ability union and ABILITY_MIN in auth/abilities.ts — that
file is the single source of truth; apps/web/src/lib/abilities.ts only
consumes the server-resolved map.
Open questions
- Which SES region + verified identity/configuration set this sends under, and
whether it reuses existing IAM credentials or gets its own scoped
ses:SendEmailuser. - The 9% of policyholders with no email (78 of 893) — silently skipped, or
surfaced as a "print these" worklist? Recommend the worklist: the existing
aviso-renovacionreport already produces exactly those letters, so it costs one filter parameter. - Spanish or English body? The legacy letters were Spanish; the customer base
is substantially US-resident.
Customerhas no language preference field.
2. Liquidación batch workflow
What Jorge asked for
Print the pending set, then mark many policies settled at once with one transfer number — "liquidación de pólizas MULT", garantías excluded.
What's already built (do not re-build)
liquidated / liquidationNumber / liquidationDate are wired end to end:
schema.prisma:165-167, create+update DTOs (policy.dto.ts:35-37,59-61),
the ?liquidated= list filter (policies.service.ts:148), liquidada/pendiente
counts in stats() (:219,:237), headerData() pass-through (:316), the
"Liquidada" checkbox in PolicyForm.tsx:250, and the detail-page label
(polizas/[id]/page.tsx:323).
Only the batch layer is missing. 2,170 of 2,396 policies are already marked liquidated from migration; the live pending set is 226.
2.1 Pending-liquidación report
New entry in reports.registry.ts, format: "tabular" — gets print/PDF/CSV/XLSX
free via the existing /reportes/:slug machinery.
Parameterized by ramo, mirroring how vigente and aviso-renovacion already
take a policyType select param. The workflow is not MULT-only: the legacy
TABLA LIQUIDA MF scratch table served MULT, INCENDIO and M EMPR
(LEGACY_DATABASES_OBJECTS.md:4887-5017).
Params: ramo (with an "todos" option), aseguradora, date range on policyFrom.
Columns: póliza, cliente, ramo, aseguradora, vigencia, prima neta, forma de pago.
Totals: count + prima neta sum per currency (never collapse MXN and USD —
same constraint as the billing module).
⚠️ Fix defect (a) above before building this, or the report inherits the same
blind spot: 4 of the 226 pending policies carry policyTypeId = NULL and would
be missing from every ramo-filtered run and from the "todos" run if that is
implemented as a union over known types rather than as "no filter."
2.2 Batch settle endpoint
POST /policies/liquidate-batch — body:
{ policyIds: string[], liquidationNumber: string, liquidationDate: string }
One prisma.$transaction. Rejects ids that are already liquidated (return
them in the response rather than silently skipping, so the UI can say which).
Writes an ActivityLog row per policy — this is a financial settlement marker
being set across many records at once, and it is the one place in the app where
a single click changes dozens of rows.
Ability: new policy:liquidate at MANAGER, not the existing policy:update
(STAFF). Reason: a STAFF user editing one policy's checkbox is data entry; a
STAFF user settling 200 policies against one transfer number is a financial
control. Recommend the new ability; note it as a question for Jorge only if he
wants STAFF to keep doing it.
2.3 Un-settle path
The legacy had one (MULT FAM X POLIZA Consulta,
LEGACY_DATABASES_OBJECTS.md:5570-5573). POST /policies/liquidate-batch/undo
with the same shape, or { liquidationNumber } to reverse a whole batch.
Gated at MANAGER via the same policy:liquidate. Also logs.
2.4 The two-slot decision (defect (b))
Policy has one settlement slot; MULT/INCENDIO had two and M EMPR had
four, with real usage on ≤41 MULT rows and nowhere else.
Recommendation: move settlement onto PolicyPaymentInstallment, do not add a
second slot to Policy. Reasons:
PolicyPaymentInstallmentalready exists, already haspaidDateandcheckNumber, and already models "the n-th payment of this policy" — which is exactly what the second settlement slot meant. 4,724 rows, 1,849 with a paid date.- Adding
liquidated2/liquidationNumber2/liquidationDate2reproduces the legacy's hardcoded-repeated-columns mistake that this whole migration exists to undo — andM EMPRproves it doesn't stop at two. - The
Policy-level fields stay as the rollup ("this policy is fully settled"), which is what the existing UI and?liquidated=filter already mean. No breaking change.
Concretely: add liquidationNumber String? + liquidatedAt DateTime? to
PolicyPaymentInstallment; batch-settle writes the installment rows and sets
Policy.liquidated = true when all installments are settled. Backfill the ≤41
lost slot-2 values from mult.num_liquidacion2 / f_liquida2 in
transform_policies.py at the same time.
If Jorge wants the simpler thing instead, say so explicitly and accept that those 41 second settlements stay unmigrated.
2.5 "Garantías excluded"
Blocked — the term has no referent anywhere in the data (0 hits). Do not guess
at a filter. Spec'd as: the batch report takes an explicit exclusion list or a
flag once Jorge identifies what a "garantía" is in his data. Most likely
candidates to ask about: a forma_pago value, an aseguradora, or a
coveragesJson key.
API surface
| Method | Route | Ability |
|---|---|---|
GET |
/reports/liquidacion-pendiente?policyType=&provider= |
read |
POST |
/policies/liquidate-batch |
policy:liquidate |
POST |
/policies/liquidate-batch/undo |
policy:liquidate |
Note the mutation lives on PoliciesController, not ReportsController —
that controller is deliberately read-only and guarded by AuthenticatedGuard
alone (reports.controller.ts:28), so any logged-in VIEWER reaches it.
Web
Extend /polizas with a "Liquidación" tab: the pending list with checkboxes, a
select-all-filtered action, and one dialog collecting número de transferencia +
fecha. Print goes through the existing /reportes/liquidacion-pendiente runner
rather than a bespoke print view.
Abilities (new)
| Ability | Min role | Notes |
|---|---|---|
policy:liquidate |
MANAGER | batch settlement across many rows; distinct from policy:update (STAFF) |
Open questions
- What "garantías" refers to (blocks the exclusion filter).
- Two-slot settlement: installment-level (recommended) or a second
Policyslot. - Should
policy:liquidatebe a new MANAGER ability, or is reusingpolicy:update(STAFF) what the office actually wants?
3. Certificate / "Solicitud Atlas" + portal delivery
What Jorge asked for
A "Solicitud Atlas" / insurance certificate, visible to customers on the website.
The blocked half
"Solicitud" has no referent — 0 hits across 212 SEGUROS reports and 96 UTILITIES reports; "Atlas" is a carrier, not a report. A solicitud is normally an application form (pre-policy, filled in by the applicant), which is a materially different artifact from a certificate (post-policy, proof of coverage issued to the insured). These need different data, different timing and different delivery.
Do not build until Jorge confirms which one he means. The spec below covers the certificate reading, because that is what "visible to customers on the website" implies.
The buildable half — certificate rendering
Reuse the letter machinery, exactly as aviso-renovacion does:
format: "letter"report (reports.types.ts:32), rendered byLetterLayout(ReportRunner.tsx:502) on screen and byoutputs.tsrenderPdffor the file.- Data needed, all already on
Policyand its relations: customer name + address, policy number, carrier,policyFrom/policyTo, and the ramo-specific coverage keys already mapped inRENEWAL_NOTICES.md— plusvehicles[0]for auto and the property address for MULT/INCENDIO/M_EMPR. - Parameter is a single policy, not a month —
/reports/certificado?policyId=. Staff-facing route: a "Certificado" button on/polizas/[id].
The infrastructure half — portal delivery
PLAN.md:16,20-24 locks the customer portal
(my-jorgecuadros-web, PHP/mysqli, its own utility_dbo DB) as out of
scope and unchanged. This repo has no public route and no CUSTOMER role
(UserRole = ADMIN/MANAGER/STAFF/VIEWER, schema.prisma:43-48), and its
sessions are in-memory. Insurance therefore reaches customers as an extension
of the already-planned replication (PLAN.md steps 8/9), not as a new public
surface here.
What this spec adds to that design, to be finalized when step 8 runs:
- Which policy fields join the replicated set — recommend the certificate's
own field list and nothing more (policy number, carrier, ramo, vigencia,
customer link), explicitly excluding premiums, commissions, liquidation
status,
observationsandnotes. The replicated side is the internet-exposed one; it should never carry the office's margin data. - Certificate as generated PDF, not portal-side rendering. Render here,
upload to the existing S3/MinIO bucket via
StorageService, replicate the pointer. The portal is PHP and is not being modified; giving it a URL is cheaper than giving it a template. This also means the certificate the customer sees is byte-identical to the one staff printed. - Where in
utility_dbothe pointer lands — depends on the portal's existing policy-facing views (fm2/fm3/fmt,full_coverage,mx_liability,usa_liability), and needs a read of the portal's PHP before it can be stated.
Abilities
None new. Certificate generation is a read; delivery is a replication concern.
Open questions
- What "Solicitud Atlas" actually is — application form or certificate. Blocks the whole section.
- If it's an application form: who fills it in (staff on the customer's behalf,
or the customer on the portal), and does it need to exist as a record before
a
Policydoes? That would be a new model, not a report. - Does the certificate need a carrier logo/letterhead? The legacy
* MENStemplates were per-carrier blobs;outputs.tsrenderPdfhas no image support today.
4. Carrier API integration
What Jorge asked for
Integration with ANA Seguros and GMX. ("GDMX" in the meeting notes was
a typo — confirmed with the user 2026-07-27. The data's GMX is correct, and
this is no longer an open question.)
Carrier research (2026-07-27) — what actually exists
The two carriers are one company. ANA and GMX are both members of Grupo
Valore, alongside Seguros Argos (vida) and Prevem Seguros (gastos médicos).
ANA writes autos; GMX writes daños — which maps exactly onto the split
in this database: ANA covers the AUTO/LICENCIAS book, GMX covers
MULT/INCENDIO/M_EMPR. Practical consequence: this is one commercial
conversation, not two. The group also shares infrastructure — GMX's own
quoting micrositio is served from ANA's host
(server.anaseguros.com.mx/Micrositios/GRUPOVALOREGMXCOR/), so one technical
contact plausibly covers both.
ANA has a real, live web service. https://server.anaseguros.com.mx/ananetws/service.asmx
— a classic ASP.NET .asmx endpoint speaking SOAP 1.1 and 1.2, with its
operation list published on the standard help page:
| Purpose | Operations |
|---|---|
| Catálogos | Marca, SubMarca, Modelo, MarcaMoto, SubMarcaMoto, Color, Categoria, CatVeh, CodigoPostal, Colonia, ColxCP, DelMun, EDOS, Bancos, FormaPago, TipoPersona, TipoIndem, RegimenFiscal, Nacionalidad, Ocupacion, Identificacion, GiroEmpresa, PropositoMotos, Vigencia |
| Cotización | CalculaValor, CalculaMSI |
| Vehículo | Vehiculo, VehiculoMoto, ValidaSerie |
| Recuperación / validación | RecuperaCotizacion, ValidaAsegurado |
| Transacción | Transaccion |
GMX publishes no machine interface. Its agent area
(gmx.com.mx/soy-agente/herramientas/) lists only human portals — reporte de
agentes, cobranzas, envío/descarga de facturas, documentos emitidos, reporte de
siniestros, artículo 492. No API, no WSDL, no developer contact. The only
number published is (55) 5480-4000.
Neither carrier has a public developer portal or published documentation. Across this market, web service credentials are granted by the carrier, at its discretion, to appointed agents on written request — expect a lead time measured in weeks, not a signup form.
⚠️ The critical mismatch — read before estimating this
The ANA service is a new-business quoting/issuance API. What this platform
needs is an inbound feed of the office's existing book. Every operation
above serves "price and issue a policy that does not exist yet." Not one of
them is "list the policies where I am the agent of record," which is what
would populate Policy rows and keep them current.
So the honest reading of the research is:
- If Jorge's ask means "stop re-typing new policies into two systems" — the ANA service can do that for autos, and it is genuinely buildable once credentials arrive. GMX/daños would stay manual.
- If Jorge's ask means "keep our policy data in sync with the carrier automatically" — no evidence exists that either carrier offers it, and the question to ask is specifically whether a portfolio/cartera download service exists for an agent's own book. That question has not been asked yet.
Do not commit to this section until Jorge says which of the two he means. The first is a moderate feature; the second may not be purchasable at all.
Note also that nothing in this spec authorizes calling those endpoints. The
operation list above comes from a published help page; actually invoking
CalculaValor or Transaccion requires the agent credentials Jorge would
obtain, and should not be attempted before then.
Legacy precedent
Carrier config that exists in the legacy system: gen1/gen2
(LEGACY_DATABASES.md:1872-1892) — 9 rows keyed by carrier with RFC,
CLAVE, FPAGO, MONED, plus a 14-row agent list. It is the only
carrier-keyed table anywhere, and it carries no API metadata — no endpoint,
no credential, no identifier that looks like one. In the new schema the
equivalent is InsuranceProvider, which today holds only a name.
Shape
CarrierConnectorinterface —fetchPolicies(since: Date),fetchPolicy(number: string), returning a normalized DTO, one implementation per carrier. The ANA implementation cannot satisfyfetchPoliciesfrom the operations known today (see the mismatch above); if the ask turns out to be outbound issuance instead, the interface is the wrong shape and should becomequote(...)/issue(...)againstCalculaValor/Transaccion.- SOAP, not REST, for ANA —
.asmxwith a WSDL. Node has no first-class SOAP client in this stack; budget forstrong-soap/soapplus the schema work, and generate types from the WSDL rather than hand-writing envelopes. - Credentials and endpoint config per carrier: extend
InsuranceProviderwith the connector's identifier and store secrets in env, keyed by that identifier — never in the database row. - The catalog operations (
Marca/SubMarca/Modelo/CodigoPostal/Colonia) are useful independently of any policy sync — they would let the policy form validate vehicle and address data against the carrier's own catalogs instead of free text. That is the cheapest possible first use of these credentials and a sensible pilot: read-only, no issuance risk, immediately visible inPolicyForm. - An import-staging + review step, never a direct write to
Policy. Same principle asRECEIPT_CAPTURE_SPEC.md§2, which routes OCR results through a review queue instead of writing ledger rows: one write path, one audit trail, and a human confirms anything a machine proposed. A carrier feed that wrotePolicyrows directly would also fight the Access sync (run_all.py --sync), which owns every row carrying provenance columns — an imported policy needs its own provenance (legacySourceDb = 'carrier:<name>') or the next sync will delete it as a row that vanished from source.
Abilities (new)
| Ability | Min role | Notes |
|---|---|---|
carrier:import |
MANAGER | trigger a fetch and approve imported policies |
Open questions
Does "GDMX" meanResolved 2026-07-27 — yes, a typo in the meeting notes.GMX?- Direction — the one that decides whether this is buildable. Does Jorge want to stop re-typing new policies (outbound quote/issue, which the ANA service supports), or keep existing policies in sync (inbound portfolio download, which nothing found suggests either carrier offers)?
- What to ask Grupo Valore, in one call to (55) 5480-4000 or the ANA agent
channel:
- WSDL + test/production credentials for
server.anaseguros.com.mx/ananetws/service.asmx, and whether an agent appointment is a prerequisite. - Whether a cartera / portfolio download service exists for an agent's own book — the question that decides the direction above.
- Whether GMX daños has any machine interface at all, or whether its
agent portals are the only access. This is the more valuable half for this
office: GMX writes the
MULT/INCENDIO/M_EMPRbook. - Whether one set of Grupo Valore credentials spans both carriers, given the shared hosting.
- WSDL + test/production credentials for
- Does the office hold agent appointments with both ANA and GMX in good standing? Credential grants are discretionary and appointment-gated.
Build sequencing
- §1 renewal emails — highest value, schema already ready, no blocker beyond the SES sending account. ≈260 mails/month against a 91%-reachable policyholder base.
- §2 liquidación batch — small, builds on fields already wired. Do the two
defect fixes (missing
policy_typesrows + FKON DELETE RESTRICT) as part of it, since both distort its own report. - §3 certificate — the report half is buildable now; portal delivery waits on PLAN.md steps 8/9 infrastructure, and the whole section waits on what "Solicitud" means.
- §4 carrier APIs — blocked on a single phone call, not on research. ANA's SOAP service is real and its operation list is known; what is missing is credentials and an answer on direction (§4's open questions). GMX appears to have nothing machine-readable, which matters because GMX writes the larger half of this office's book. Build last, and consider the catalog-only pilot before anything else.
§1 and §2 are independent of each other and can be built in parallel; both are
independent of everything in RECEIPT_CAPTURE_SPEC.md.
New abilities across this spec
| Ability | Min role | Section |
|---|---|---|
renewal:send |
MANAGER | §1 |
policy:liquidate |
MANAGER | §2 |
carrier:import |
MANAGER | §4 |
No collision with the abilities proposed in RECEIPT_CAPTURE_SPEC.md
(statement:ingest, statement:review, bank:manage-accounts,
customer:recycle, customer:purge).
Open questions to take back to Jorge (collected)
§1 — renewal emails
- Which SES region + verified identity/configuration set, and whether to reuse
existing IAM credentials or create a scoped
ses:SendEmailuser. - The 78 policyholders with no email: skip silently, or produce a print worklist? (Recommend the worklist.)
- Spanish or English notice body?
§2 — liquidación
- What "garantías" refers to — blocks the exclusion filter.
- Settlement on
PolicyPaymentInstallment(recommended) vs. a second slot onPolicy; and whether to backfill the ≤41 lost MULT second settlements. - New
policy:liquidate(MANAGER) vs. reusingpolicy:update(STAFF).
§3 — certificate
- What "Solicitud Atlas" is: application form or certificate. Blocks the section.
- If application form: who fills it in, and does it precede the
Policyrecord? - Does the certificate need carrier letterhead/logo?
§4 — carrier APIs (all four go in one call to Grupo Valore, (55) 5480-4000)
- Direction: outbound quote/issue (supported by ANA today) or inbound portfolio sync (no evidence either carrier offers it)? This decides whether the feature is buildable at all.
- WSDL + credentials for
server.anaseguros.com.mx/ananetws/service.asmx. - Does a cartera/portfolio download exist for an agent's own book?
- Does GMX daños have any machine interface, or portals only? GMX writes the
MULT/INCENDIO/M_EMPRbook — the bigger half for this office. - Does one Grupo Valore credential span both carriers?
Resolved — no longer open
Which of→UTILSEG/DATGRAL.[NUM UTIL]is authoritativeNUM UTIL;UTILSEGis stale and must not be used (see Ground truth).OCR/mail provider and budget→ SES, settled before this spec was written.Does "GDMX" mean→ yes, a typo in the meeting notes (2026-07-27).GMXDo the carriers' APIs exist→ ANA: yes, a live SOAP service with a known operation list. GMX: no published machine interface. Both are Grupo Valore, so it is one relationship. See §4.
Sources (§4 carrier research, 2026-07-27)
- ANA Seguros web service (
ananetws/service.asmx) - ANA Seguros — quiénes somos / Grupo Valore
- GMX Seguros — herramientas para agentes
- GMX quoting micrositio hosted on ANA's server
- Agentemotor — how carriers grant web service credentials (Colombian market, cited only for the credential-request pattern)