docs(insurance): spec renewal emails, liquidación batch, certificate, carrier APIs
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>
This commit is contained in:
@@ -0,0 +1,786 @@
|
|||||||
|
# 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`](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`](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:
|
||||||
|
|
||||||
|
1. **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.
|
||||||
|
2. **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.
|
||||||
|
3. **Certificate / "Solicitud Atlas"** — a customer-facing artifact rendered
|
||||||
|
from the same policy record, delivered through the existing PHP portal.
|
||||||
|
4. **Carrier API integration** — an *inbound* path that would populate the
|
||||||
|
same `Policy` rows 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 or
|
||||||
|
`setInterval` in `apps/api`. `ops/` spawns detached child processes on user
|
||||||
|
request only.
|
||||||
|
- **No mail code or dependency.** Nothing in any `package.json`, `.env.example`
|
||||||
|
or `docker-compose.yml`.
|
||||||
|
- **`EmailTemplate` / `EmailCampaign` / `EmailLog`** (`schema.prisma:540-571`)
|
||||||
|
are dead migrated legacy tables — no FKs, no code touches them. **Leave them
|
||||||
|
alone**; `RenewalNotice` is the send log.
|
||||||
|
- `express-session` uses 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 `AuthenticatedGuard` alone (`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; see
|
||||||
|
[`RENEWAL_NOTICES.md`](RENEWAL_NOTICES.md).) The closest legacy artifact to a
|
||||||
|
certificate is the `* MENS`/`*MENSAJE` blob 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
|
||||||
|
own `NOMBRE` against 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 key
|
||||||
|
`transform_customers.py` already uses.
|
||||||
|
- **`UTILSEG`** (1,582 rows) — 379 rows carry both a `seguros` and a `util`
|
||||||
|
number. Under the obvious reading (`seguros` → seguros `DATGRAL.num_id`,
|
||||||
|
`util` → utilities `DATGRAL.num_id`) the row's own `NOMBRE` matches 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
|
||||||
|
`seguros` ids 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`](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-specific
|
||||||
|
`coveragesJson` keys (`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 Int` with **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:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
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`, optional `SES_CONFIGURATION_SET`), added to `.env.example`;
|
||||||
|
- **null client when unconfigured, `ServiceUnavailableException` on 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:SendEmail` user.
|
||||||
|
- 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-renovacion` report 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. `Customer` has 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:
|
||||||
|
|
||||||
|
- `PolicyPaymentInstallment` already exists, already has `paidDate` and
|
||||||
|
`checkNumber`, 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`/`liquidationDate2` reproduces the
|
||||||
|
legacy's hardcoded-repeated-columns mistake that this whole migration exists
|
||||||
|
to undo — and `M EMPR` proves 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 `Policy` slot.
|
||||||
|
- Should `policy:liquidate` be a new MANAGER ability, or is reusing
|
||||||
|
`policy: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 by `LetterLayout`
|
||||||
|
(`ReportRunner.tsx:502`) on screen and by `outputs.ts` `renderPdf` for the
|
||||||
|
file.
|
||||||
|
- Data needed, all already on `Policy` and its relations: customer name +
|
||||||
|
address, policy number, carrier, `policyFrom`/`policyTo`, and the
|
||||||
|
ramo-specific coverage keys already mapped in
|
||||||
|
[`RENEWAL_NOTICES.md`](RENEWAL_NOTICES.md) — plus `vehicles[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`](../PLAN.md) 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, `observations` and `notes`. 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_dbo` the 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 `Policy` does? That would be a new model, not a report.
|
||||||
|
- Does the certificate need a carrier logo/letterhead? The legacy `* MENS`
|
||||||
|
templates were per-carrier blobs; `outputs.ts` `renderPdf` has 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
|
||||||
|
|
||||||
|
- `CarrierConnector` interface — `fetchPolicies(since: Date)`,
|
||||||
|
`fetchPolicy(number: string)`, returning a normalized DTO, one implementation
|
||||||
|
per carrier. **The ANA implementation cannot satisfy `fetchPolicies` from 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 become
|
||||||
|
`quote(...)` / `issue(...)` against `CalculaValor` / `Transaccion`.
|
||||||
|
- SOAP, not REST, for ANA — `.asmx` with a WSDL. Node has no first-class SOAP
|
||||||
|
client in this stack; budget for `strong-soap`/`soap` plus the schema work,
|
||||||
|
and generate types from the WSDL rather than hand-writing envelopes.
|
||||||
|
- Credentials and endpoint config per carrier: extend `InsuranceProvider` with
|
||||||
|
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 in `PolicyForm`.
|
||||||
|
- **An import-staging + review step, never a direct write to `Policy`.** Same
|
||||||
|
principle as [`RECEIPT_CAPTURE_SPEC.md`](RECEIPT_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 wrote `Policy` rows 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" mean `GMX`?~~ **Resolved 2026-07-27** — yes, a typo in the
|
||||||
|
meeting notes.
|
||||||
|
- **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:
|
||||||
|
1. WSDL + test/production credentials for `server.anaseguros.com.mx/ananetws/service.asmx`,
|
||||||
|
and whether an agent appointment is a prerequisite.
|
||||||
|
2. Whether a **cartera / portfolio download** service exists for an agent's
|
||||||
|
own book — the question that decides the direction above.
|
||||||
|
3. 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_EMPR` book.
|
||||||
|
4. Whether one set of Grupo Valore credentials spans both carriers, given the
|
||||||
|
shared hosting.
|
||||||
|
- Does the office hold agent appointments with both ANA and GMX in good
|
||||||
|
standing? Credential grants are discretionary and appointment-gated.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Build sequencing
|
||||||
|
|
||||||
|
1. **§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. **§2 liquidación batch** — small, builds on fields already wired. Do the two
|
||||||
|
defect fixes (missing `policy_types` rows + FK `ON DELETE RESTRICT`) as part
|
||||||
|
of it, since both distort its own report.
|
||||||
|
3. **§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. **§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:SendEmail` user.
|
||||||
|
- 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 on
|
||||||
|
`Policy`; and whether to backfill the ≤41 lost MULT second settlements.
|
||||||
|
- New `policy:liquidate` (MANAGER) vs. reusing `policy: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 `Policy` record?
|
||||||
|
- 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_EMPR` book — 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 authoritative~~ → `NUM UTIL`;
|
||||||
|
`UTILSEG` is stale and must not be used (see Ground truth).
|
||||||
|
- ~~OCR/mail provider and budget~~ → SES, settled before this spec was written.
|
||||||
|
- ~~Does "GDMX" mean `GMX`~~ → yes, a typo in the meeting notes (2026-07-27).
|
||||||
|
- ~~Do 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`)](https://server.anaseguros.com.mx/ananetws/service.asmx)
|
||||||
|
- [ANA Seguros — quiénes somos / Grupo Valore](https://anaseguros.com.mx/anaweb/ana_seguros.html)
|
||||||
|
- [GMX Seguros — herramientas para agentes](https://www.gmx.com.mx/soy-agente/herramientas/)
|
||||||
|
- [GMX quoting micrositio hosted on ANA's server](https://server.anaseguros.com.mx/Micrositios/GRUPOVALOREGMXCOR/cotizador.html)
|
||||||
|
- [Agentemotor — how carriers grant web service credentials](https://www.agentemotor.com/blog/noticias-agentemotor/como-integrarte-a-las-aseguradoras-via-web-service-utilizando-agentemotor/)
|
||||||
|
(Colombian market, cited only for the credential-request pattern)
|
||||||
Reference in New Issue
Block a user