docs: SES secrets created, ship blocker cleared

Five documents asserted the SES_* secrets were unset in Gitea. They now
exist, so all five are corrected rather than leaving the claim to rot in
whichever one a reader opens first.

Replaces the blocker with the two things creating the secrets does NOT
establish, since both fail in ways that look identical to a missing
config: SES_FROM must be a verified identity in SES_REGION, and the
account must be out of the SES sandbox — in sandbox SES only delivers to
verified recipients, so a sweep across 815 policyholders would fail
almost every send while the configuration reads as correct.

Recommends running the first sweep with debug on, which diverts every
recipient and, on the pólizas side, leaves the avisos pending so a failed
test consumes nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-02 13:25:44 -07:00
co-authored by Claude Opus 5
parent 3e12597204
commit e77e5546d8
5 changed files with 49 additions and 25 deletions
+1 -1
View File
@@ -155,7 +155,7 @@ Given the amount of near-duplicate/overlapping data across snapshot tables (mult
- **Renewal notification emails — DONE** (2026-08-01, extended 08-02). A sweep that mails the customer 30 days before expiry, 15 days before, and 7 days after, mapping onto `RenewalNotice.generation` 1/2/3 with **no schema change**. Sending is **Amazon SES** (`@aws-sdk/client-sesv2`, mirroring `StorageService`'s optional-client/degrade-don't-crash pattern). The letter body is the *existing* `aviso-renovacion` report; `@@unique([policyId, generation])` is already-in-place idempotency, so a re-run cannot double-send. Volume ≈260 mails/month, and **815 of the 893 policyholders (91%) have an email**. - **Renewal notification emails — DONE** (2026-08-01, extended 08-02). A sweep that mails the customer 30 days before expiry, 15 days before, and 7 days after, mapping onto `RenewalNotice.generation` 1/2/3 with **no schema change**. Sending is **Amazon SES** (`@aws-sdk/client-sesv2`, mirroring `StorageService`'s optional-client/degrade-don't-crash pattern). The letter body is the *existing* `aviso-renovacion` report; `@@unique([policyId, generation])` is already-in-place idempotency, so a re-run cannot double-send. Volume ≈260 mails/month, and **815 of the 893 policyholders (91%) have an email**.
**Three things came out differently from the spec.** (a) The manual mark-as-sent mutation was **dropped on purpose** — a button that marks a notice sent without sending anything lets the list claim a customer was told when they were not. `POST /renewals/send` replaced it: sending from the list *is* the marking, and the report's `enviadas` total becomes real the same way. (b) The send history is **not renewal-specific** — every attempt, including the failures and no-email skips a `RenewalNotice` row cannot represent, also writes `email_notification_log` as `RENEWAL_NOTICE`/`POLICIES`, shared with the four bulk jobs from [`docs/MASS_EMAIL_NOTIFICATIONS.md`](docs/MASS_EMAIL_NOTIFICATIONS.md). `RenewalNotice` stays *gating* state; the log is *history*. (c) The `@Cron("0 6 * * *")` literal the spec called for lasted one day: both this sweep and the servicios jobs now take their cadence from `NotificationScheduleService`, stored in `app_settings` and reinstalled on save — no redeploy. Defaults preserve the old behaviour (pólizas 06:00 daily, servicios off). **Three things came out differently from the spec.** (a) The manual mark-as-sent mutation was **dropped on purpose** — a button that marks a notice sent without sending anything lets the list claim a customer was told when they were not. `POST /renewals/send` replaced it: sending from the list *is* the marking, and the report's `enviadas` total becomes real the same way. (b) The send history is **not renewal-specific** — every attempt, including the failures and no-email skips a `RenewalNotice` row cannot represent, also writes `email_notification_log` as `RENEWAL_NOTICE`/`POLICIES`, shared with the four bulk jobs from [`docs/MASS_EMAIL_NOTIFICATIONS.md`](docs/MASS_EMAIL_NOTIFICATIONS.md). `RenewalNotice` stays *gating* state; the log is *history*. (c) The `@Cron("0 6 * * *")` literal the spec called for lasted one day: both this sweep and the servicios jobs now take their cadence from `NotificationScheduleService`, stored in `app_settings` and reinstalled on save — no redeploy. Defaults preserve the old behaviour (pólizas 06:00 daily, servicios off).
**Both halves live on one screen.** `/notificaciones` has Servicios and Pólizas tabs over the one log; `/renovaciones` is an alias onto the Pólizas tab. The send flags (`debug` in particular) sit in the shell above the tabs and govern both — before that there was no way to test a renewal aviso without mailing a real customer. A debug send diverts the mail, skips the `RenewalNotice` upsert **and** does not advance the sweep's `lastSuccessfulAt`; all three are needed together, or a test run silently narrows tomorrow's window and drops the letters it only pretended to send. **Both halves live on one screen.** `/notificaciones` has Servicios and Pólizas tabs over the one log; `/renovaciones` is an alias onto the Pólizas tab. The send flags (`debug` in particular) sit in the shell above the tabs and govern both — before that there was no way to test a renewal aviso without mailing a real customer. A debug send diverts the mail, skips the `RenewalNotice` upsert **and** does not advance the sweep's `lastSuccessfulAt`; all three are needed together, or a test run silently narrows tomorrow's window and drops the letters it only pretended to send.
**Still blocked in production:** the `SES_*` variables are wired through the deploy workflow but unset in Gitea, so a production send fails loudly rather than going out. **Production status:** the `SES_*` Gitea secrets were created 2026-08-02, clearing the last blocker — but the feature has not shipped yet (master is well past the newest tag) and nothing has confirmed that `SES_FROM` is a verified SES identity or that the account is out of the sandbox. Run the first sweep with `debug` on. See [`docs/BACKLOG.md`](docs/BACKLOG.md) §0.
- **Liquidación batch workflow** — ~70% already built (`liquidated`/`liquidationNumber`/`liquidationDate` are wired through DTOs, list filter, stats, form and detail page); only the *batch* print-and-mark step is missing, against a live pending set of 226 policies. Adds a ramo-parameterized pending report plus `POST /policies/liquidate-batch` under a new MANAGER `policy:liquidate` ability. Parameterized by ramo, not MULT-only — legacy `TABLA LIQUIDA MF` served `MULT`, `INCENDIO` and `M EMPR` alike. - **Liquidación batch workflow** — ~70% already built (`liquidated`/`liquidationNumber`/`liquidationDate` are wired through DTOs, list filter, stats, form and detail page); only the *batch* print-and-mark step is missing, against a live pending set of 226 policies. Adds a ramo-parameterized pending report plus `POST /policies/liquidate-batch` under a new MANAGER `policy:liquidate` ability. Parameterized by ramo, not MULT-only — legacy `TABLA LIQUIDA MF` served `MULT`, `INCENDIO` and `M EMPR` alike.
- **Certificate / "Solicitud Atlas"** — renders from the same `format: "letter"` machinery `aviso-renovacion` uses, then reaches customers as an extension of the step-8/9 replication (PDF generated here, pushed to MinIO, pointer replicated), **not** as a new public surface in this repo. Half-blocked: "Solicitud" has zero referent in the legacy system and normally means an *application form*, a different artifact from a certificate. - **Certificate / "Solicitud Atlas"** — renders from the same `format: "letter"` machinery `aviso-renovacion` uses, then reaches customers as an extension of the step-8/9 replication (PDF generated here, pushed to MinIO, pointer replicated), **not** as a new public surface in this repo. Half-blocked: "Solicitud" has zero referent in the legacy system and normally means an *application form*, a different artifact from a certificate.
- **Carrier API integration (ANA Seguros + GMX)** — shape only (`CarrierConnector` + an import-review queue rather than direct `Policy` writes, matching how step 11's OCR results are routed). Carrier research done 2026-07-27: **the two carriers are one company** — both belong to **Grupo Valore** (ANA writes autos, GMX writes daños, which is exactly this database's `AUTO`/`LICENCIAS` vs `MULT`/`INCENDIO`/`M_EMPR` split), so it is one commercial relationship, not two. **ANA has a real live SOAP service** (`server.anaseguros.com.mx/ananetws/service.asmx`, ASP.NET `.asmx`) with a published operation list — catalogs, `CalculaValor`/`CalculaMSI`, `ValidaSerie`, `RecuperaCotizacion`, `Transaccion`. **GMX publishes no machine interface at all**, only human agent portals. ⚠️ **Critical mismatch:** every ANA operation serves *new-business quoting/issuance*, not "list the policies where I am agent of record" — so if the ask is inbound portfolio sync, no evidence exists that either carrier sells it. Blocked on one phone call to Grupo Valore ((55) 5480-4000) for credentials + a direction answer, not on further research. ("GDMX" in the meeting notes was a typo for `GMX` — confirmed 2026-07-27.) - **Carrier API integration (ANA Seguros + GMX)** — shape only (`CarrierConnector` + an import-review queue rather than direct `Policy` writes, matching how step 11's OCR results are routed). Carrier research done 2026-07-27: **the two carriers are one company** — both belong to **Grupo Valore** (ANA writes autos, GMX writes daños, which is exactly this database's `AUTO`/`LICENCIAS` vs `MULT`/`INCENDIO`/`M_EMPR` split), so it is one commercial relationship, not two. **ANA has a real live SOAP service** (`server.anaseguros.com.mx/ananetws/service.asmx`, ASP.NET `.asmx`) with a published operation list — catalogs, `CalculaValor`/`CalculaMSI`, `ValidaSerie`, `RecuperaCotizacion`, `Transaccion`. **GMX publishes no machine interface at all**, only human agent portals. ⚠️ **Critical mismatch:** every ANA operation serves *new-business quoting/issuance*, not "list the policies where I am agent of record" — so if the ask is inbound portfolio sync, no evidence exists that either carrier sells it. Blocked on one phone call to Grupo Valore ((55) 5480-4000) for credentials + a direction answer, not on further research. ("GDMX" in the meeting notes was a typo for `GMX` — confirmed 2026-07-27.)
+10 -6
View File
@@ -685,9 +685,13 @@ Implementation notes worth keeping:
- Wire shapes of the four jobs are byte-for-byte the legacy PHP responses, - Wire shapes of the four jobs are byte-for-byte the legacy PHP responses,
quirks included (Job 1 reports `result`, not `request`). quirks included (Job 1 reports `result`, not `request`).
**Open:** `SES_*` is wired through the deploy workflow but **unset in Gitea**, **Open:** the `SES_*` Gitea secrets were created 2026-08-02, so the feature is
so production sends fail loudly rather than going out. The 78 policyholders no longer blocked — but it has not shipped (master is well past the newest tag)
with no email are logged as `SKIPPED_NO_EMAIL` but there is still no printable and two things nobody has checked decide whether mail leaves the building:
worklist for them, and the notice body is English-only (`Customer` carries no `SES_FROM` must be a verified identity in `SES_REGION`, and the AWS account
language preference) — the same three questions §1 of the insurance spec must be out of the SES sandbox, which otherwise restricts delivery to verified
opened. recipients and would fail a real sweep while looking correctly configured. Run
the first sweep with `debug` on. Still open beyond that: the 78 policyholders
with no email are logged as `SKIPPED_NO_EMAIL` but have no printable worklist,
and the notice body is English-only (`Customer` carries no language
preference).
+24 -12
View File
@@ -38,21 +38,33 @@ Plus three backup-pipeline fixes that have never reached prod (`860d483`,
`567b033`, `898cf48` — the last prod run went green through the whole chain and `567b033`, `898cf48` — the last prod run went green through the whole chain and
died on the final step wanting `deploy/.env.prod`). died on the final step wanting `deploy/.env.prod`).
> ### ⚠️ `SES_*` is unset in Gitea, and the renewal sweep is on by default > ### `SES_*` secrets created in Gitea 2026-08-02 — unblocked, unverified
> >
> The variables are wired through both deploy workflows and the app stack, but > The variables were wired through both deploy workflows and the app stack but
> the secrets were never set. The production image runs `NODE_ENV=production`, > had never been set. **They now exist.** What that clears: the production
> which disables the stdout dev fallback, so every send fails and logs > image runs `NODE_ENV=production`, which disables the stdout dev fallback, so
> `FAILED`. > a blank config made every send fail and log `FAILED` — and the pólizas sweep
> defaults to **enabled, 06:00 America/Tijuana**, so the failure would have
> repeated nightly.
> >
> The pólizas sweep defaults to **enabled, 06:00 America/Tijuana**. Deploying > **Not yet confirmed, and the first deploy is what confirms it:**
> current master with SES still blank means a sweep that fails every single
> night, silently except for the log.
> >
> **Before cutting `v1.0.7`:** either set the Gitea secrets, or disable the > 1. **Names match.** The preflight checks `SES_REGION`, `SES_FROM`,
> pólizas schedule at `/notificaciones` → "Programación de envíos" first. > `SES_ACCESS_KEY`, `SES_SECRET_KEY` and warns by name if any is blank —
> See [`MASS_EMAIL_NOTIFICATIONS.md`](MASS_EMAIL_NOTIFICATIONS.md) and > read that warning on the next run. No `_GALACTUS` suffix on any of them;
> [`DEPLOY_AND_MIGRATIONS.md`](DEPLOY_AND_MIGRATIONS.md). > one SES identity serves every deployment.
> 2. **`SES_FROM` is a verified identity in `SES_REGION`.** An unverified
> sender is rejected per-send, which looks identical to a missing config in
> the log.
> 3. **The AWS account is out of the SES sandbox.** This is the one that would
> hurt: in sandbox, SES only delivers to *verified* recipients, so a renewal
> sweep across 815 policyholders would fail almost every send while the
> config looks entirely correct. Check before letting a real sweep run.
>
> Until 2 and 3 are confirmed, run the first sweep with `debug` on — it diverts
> every recipient to the override inbox, and on the pólizas side it also leaves
> the avisos pending, so nothing is consumed by a failed test. See
> [`MASS_EMAIL_NOTIFICATIONS.md`](MASS_EMAIL_NOTIFICATIONS.md) "Send flags".
Also outstanding on the deploy path: every pre-existing database still needs Also outstanding on the deploy path: every pre-existing database still needs
its one-time `prisma migrate resolve --applied 0000_init`. its one-time `prisma migrate resolve --applied 0000_init`.
+7 -2
View File
@@ -335,5 +335,10 @@ backup does them (see `deploy/scripts/pre-migrate-backup.mjs`):
which has no DB lock (the pólizas one does). Keep it single-replica. which has no DB lock (the pólizas one does). Keep it single-replica.
- `SES_*` is optional to deploy — the preflight only warns — but the production - `SES_*` is optional to deploy — the preflight only warns — but the production
image sets `NODE_ENV=production`, which disables the stdout dev fallback. A image sets `NODE_ENV=production`, which disables the stdout dev fallback. A
blank SES config therefore makes every send fail and log `FAILED`. As of blank SES config therefore makes every send fail and log `FAILED`. The
2026-08-02 these secrets are still unset in Gitea. secrets were created 2026-08-02; the preflight warning on the next run is
what confirms the names are right. Two things it cannot check: that
`SES_FROM` is a **verified identity** in `SES_REGION`, and that the account
is **out of the SES sandbox** (in sandbox, delivery is restricted to verified
recipients, which would fail a real sweep while looking correctly
configured).
+7 -4
View File
@@ -797,10 +797,13 @@ No collision with the abilities proposed in `RECEIPT_CAPTURE_SPEC.md`
## Open questions to take back to Jorge (collected) ## Open questions to take back to Jorge (collected)
**§1 — renewal emails** (feature built; these three are still open) **§1 — renewal emails** (feature built; these three are still open)
- Which SES region + verified identity/configuration set, and whether to reuse - ~~Which SES region + verified identity/configuration set, and whether to
existing IAM credentials or create a scoped `ses:SendEmail` user. **Still reuse existing IAM credentials or create a scoped `ses:SendEmail` user.~~
unanswered in production**: the `SES_*` variables are wired through the **Answered in practice 2026-08-02** — the Gitea secrets were created. Two
deploy workflow but unset in Gitea, so production sends fail loudly. things the deploy preflight cannot verify and that decide whether mail
actually goes out: `SES_FROM` must be a verified identity in `SES_REGION`,
and the account must be out of the SES sandbox (which restricts delivery to
verified recipients). See [`BACKLOG.md`](BACKLOG.md) §0.
- The 78 policyholders with no email: skip silently, or produce a print - The 78 policyholders with no email: skip silently, or produce a print
worklist? Currently they are **logged as `SKIPPED_NO_EMAIL`** in worklist? Currently they are **logged as `SKIPPED_NO_EMAIL`** in
`email_notification_log` — visible in "Registro de envíos", but not yet a `email_notification_log` — visible in "Registro de envíos", but not yet a