Files
rmancinasandClaude Opus 5 6331481f82 docs: record notificaciones as built, flags global, schedules editable
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>
2026-08-02 12:43:19 -07:00

7.4 KiB
Raw Permalink Blame History

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 batch
  • RC_RENEW_X_MES_NEW_ATLAS_13.txt — Auto/RC (liability only)
  • RCR_RENEW_X_MES_NEWATLAS_2013.txt — Auto/RC, renewal-of-renewal variant
  • LIC_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 23 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:

  1. Per-policy data, pulled live from the query: customer id, policy number, vehicle (make/model/body/engine), expiry date.
  2. 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.generation 1/2/3, mailed by apps/api/src/renewals/ on an operator-editable cadence (default 06:00 daily) and driven from the Pólizas tab of /notificaciones. The CONTROL … X MES companion reports have no equivalent and need none: every attempt — sent, failed, or skipped for a missing address — lands in email_notification_log. See MASS_EMAIL_NOTIFICATIONS.md and INSURANCE_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 COMP filter and hardcoded figures.
  • coveragesJson key names above are derived from migration/extract.py's sanitize_column_name (lowercase, non-alphanumeric → _) applied to the source column names in docs/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.