The docs still described the state before the last five commits: the insurance spec called for a `@Cron` literal and a manual mark-as-sent mutation, PLAN.md had step 12 as "NOT STARTED", and README's module and route lists predated seven modules. - MASS_EMAIL_NOTIFICATIONS.md: new "Send flags", "API surface" and "Scheduled runs" sections; "Cron (future)" removed — it exists. The flags table says which flags apply where, and why a debug renewal send must skip both the RenewalNotice row and `lastSuccessfulAt`. - INSURANCE_FEATURES_SPEC.md: §1 BUILT note listing the three places the build diverged from the spec; §1.1 and §1.4 marked superseded in place rather than deleted, so the reasoning stays readable. - PLAN.md: step 12 renewal emails DONE with the divergences; status paragraph rewritten. - README.md: current module/route lists, plus a "Scheduled jobs" section — a reader cloning this repo had no way to know the API sends mail on a timer. - DEPLOY_AND_MIGRATIONS.md: the cadence lives in app_settings and survives an image rollback, and the servicios sweep has no multi-replica lock. - RESUME.md: session record for the whole notificaciones arc. - RENEWAL_NOTICES.md: pointer that this is the legacy record, not what shipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7.4 KiB
Insurance Renewal Notices ("Atlas" reports)
Staff refer to this report in the UI as "the Atlas report", but Atlas
isn't a report — it's a carrier: ATLAS, S.A. is one of the insurance
companies (COMP column) SEGUROS brokers policies for, alongside
QUALITAS, S.A. and others. The legacy frontend (SEGUROS 16.mdb) never
parameterized carrier or coverage tier in its renewal-notice report — it
cloned the entire report + query chain once per carrier per coverage
variant instead. This doc explains that clone pattern and the underlying
workflow so the new platform can replace ~40 cloned Access objects with
one parameterized feature.
Why this needed extra tooling
objects.json/LEGACY_DATABASES_OBJECTS.md (see migration/catalog_objects.py)
only capture report names — DAO's catalog interface doesn't expose a
report's RecordSource or control layout, only the full Access object
model does, and that model refused to load here
("The Visual Basic for Applications project in the database is corrupt",
a common failure mode for old .mdb files opened in a newer Access build).
The workaround: Application.SaveAsText(acReport, name, path) exports a
report's complete design as plain text without touching the VBA project.
The raw (binary-blob-stripped) exports for the ATLAS renewal reports are
committed in migration/legacy_report_defs/:
AMPL_R_RENEW_X_MES_NEW_ATLAS_13.txt— Auto/Amplia (full coverage)AMPL_RENEW_X_MES_NEW_ATLAS_2013.txt— Auto/Amplia, alternate batchRC_RENEW_X_MES_NEW_ATLAS_13.txt— Auto/RC (liability only)RCR_RENEW_X_MES_NEWATLAS_2013.txt— Auto/RC, renewal-of-renewal variantLIC_RENEW_X_VENCE_ATLAS_2013.txt— Driver's-license insurance
(PrtDevMode/PrtMip/OleData/GUID binary properties — printer
settings and object GUIDs, no business meaning — were stripped so the
files are readable text instead of multi-hundred-KB hex dumps.)
The report chain
Each report is bound to a query that layers 2–3 other queries, filtered to one carrier, with two typed parameters staff fill in every run:
Report: AMPL R RENEW X MES NEW ATLAS 13
RecordSource -> Query: AMPL R RENEW CALC ATLAS 13
FROM [AMPL R CALC VIG], [AMPL R MENS] (in-force calc view + installment schedule)
WHERE COMP = "ATLAS, S.A."
AND DatePart("m",[HASTA]) = [TECLEE MES DE VENCIMIENTO (1 A 12)] -- typed param
AND DatePart("yyyy",[HASTA]) = [TECLEE AÑO DE VENCIMIENTO (1999)] -- typed param
Report: LIC RENEW X VENCE ATLAS 2013
RecordSource -> Query of the SAME NAME (query and report share a name)
FROM [LIC MENS], LIC INNER JOIN DATGRAL ... INNER JOIN [VIGENT CASA] ...
WHERE DatePart("m",[hasta]) = [TECLEE MES DE VENCIMIENTO 1 A 12]
AND DatePart("yyyy",[hasta]) = [TECLE AÑO VENCIMIENTO (1999)]
AND LIC.COMP = "ATLAS, S.A."
Staff pick a line of business, type the expiry month + year, and the report prints one notice per matching policy for that carrier that month. On screen the report is captioned "AVISO DE RENOVACION" (auto lines) or "R E N E W A L N O T I C E" (license-insurance line). Every page prints the notice twice (identical top-half/bottom-half sections) — one copy to mail, one for the office file.
The multi-notice (reminder) workflow
Renewal reminders escalate through three generations, each its own
report clone, with a matching CONTROL ... companion report (a
send/checklist log):
| Generation | Report suffix | Control/log report |
|---|---|---|
| 1st notice | RENEW / (bare) |
CONTROL <LOB> RENEW X MES |
| 2nd notice | RENEW2 |
CONTROL <LOB> RENEW2 X MES (or X MES sibling) |
| 3rd notice | RENEW3 |
CONTROL <LOB> RENEW3 X MES |
This pattern repeats per line of business: AMPL/AMPL R (auto full
coverage), RC/RC R (auto liability), LIC (driver's license), RCR,
MF/MF2/MF3 (home/multi-risk), MCA2, ME, INCEN (fire) — none of
it is visible from the table schema alone, only from the report/query
names (see docs/LEGACY_DATABASES_OBJECTS.md, "What the Reports actually
reveal").
What's hardcoded vs. what's real policy data
The extracted designs show the letter body mixes two very different kinds of content:
- Per-policy data, pulled live from the query: customer id, policy number, vehicle (make/model/body/engine), expiry date.
- Static label text baked into the report design, re-typed by hand
every time a batch was cloned for a new rate or carrier — e.g. (from
AMPL_R_RENEW_X_MES_NEW_ATLAS_13.txt):"COLLISION DEDUCTIBLE $ 500.00 Dls. THEFT DEDUCTIBLE $ 1000.00 Dls. ...""New Renewal annual Premium $ 365.25 Dls.""Total Annual Premium $ 405.25 Dls"- the whole CSL/medical-coverage recommendation and rental-car upsell paragraphs
None of those dollar figures are formulas — they're literal text, which is why there are so many near-duplicate reports: a new coverage tier or rate meant cloning the whole report and hand-editing the labels, rather than changing a parameter.
The underlying data these figures should come from already exists on
the source tables and is preserved (unmapped-but-captured) in
Policy.coveragesJson after migration — confirmed against
docs/LEGACY_DATABASES.md's table appendix:
| Legacy column | Sanitized coveragesJson key |
Meaning |
|---|---|---|
COBERTURA |
cobertura |
Coverage days/territory tier (30/40/50/365) |
CSL LIMITE |
csl_limite |
Combined single limit (liability) |
GASTOS MEDICO |
gastos_medico |
Medical coverage amount |
SERVICIO ADICIONAL |
servicio_adicional (LICENCIAS: servicio_adiconal, a source typo) |
Add-on service flag |
PROPIEDADES |
propiedades |
Property-damage coverage amount |
PERSONAS |
personas |
Per-person liability amount |
(Policy.netPremium/total/currency are already first-class columns —
see packages/database/prisma/schema.prisma.)
Migration implication: a rebuilt renewal notice should render these
from data (one parameterized template), not from report design text. See
RenewalNotice in schema.prisma and the aviso-renovacion entry in
apps/api/src/reports/reports.registry.ts for the first cut at this.
Built 2026-08-01/02. The three generations above are now
RenewalNotice.generation1/2/3, mailed byapps/api/src/renewals/on an operator-editable cadence (default 06:00 daily) and driven from the Pólizas tab of/notificaciones. TheCONTROL … X MEScompanion reports have no equivalent and need none: every attempt — sent, failed, or skipped for a missing address — lands inemail_notification_log. SeeMASS_EMAIL_NOTIFICATIONS.mdandINSURANCE_FEATURES_SPEC.md§1. This document stays a record of the legacy report chain, not of what shipped.
Caveats
- Only the ATLAS variants were extracted verbatim; the QUALITAS and
"generic" (no-carrier-suffix) clones weren't pulled but are presumed
structurally identical modulo the
COMPfilter and hardcoded figures. coveragesJsonkey names above are derived frommigration/extract.py'ssanitize_column_name(lowercase, non-alphanumeric →_) applied to the source column names indocs/LEGACY_DATABASES.md, not verified against a live migrated database (no staged output was present in this environment). Confirm against real data before wiring a template to these keys.