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>
This commit is contained in:
@@ -526,3 +526,87 @@ Implementation notes worth keeping:
|
||||
**Open:** whether the CFE charge should be the rounded barcode/headline figure
|
||||
(`$268`, what is paid at the window — what the parser uses today) or the exact
|
||||
breakdown total (`$268.88`). One question for Jorge.
|
||||
|
||||
## Notificaciones (`/notificaciones`) — DONE 2026-08-01 → 08-02
|
||||
|
||||
Two features that were spec'd separately turned out to be one screen. The four
|
||||
legacy mass-email jobs (`docs/MASS_EMAIL_NOTIFICATIONS.md`, ported from
|
||||
`email.notifications/send*.php`) and the insurance renewal avisos
|
||||
(`docs/INSURANCE_FEATURES_SPEC.md` §1) both mean *tell a customer something by
|
||||
email*, so they are **tabs of one screen over one log**, not two menu entries.
|
||||
`/renovaciones` is an alias that lands on the Pólizas tab, the same pattern
|
||||
Captura uses.
|
||||
|
||||
- **Servicios tab** — the four jobs (pagos pendientes, confirmación de pago,
|
||||
estado de cuenta, fideicomiso), individually or "Ejecutar todos". Ability
|
||||
`notification:send` (MANAGER); STAFF sees the log read-only.
|
||||
- **Pólizas tab** — pending avisos at 30/15 days before expiry and 7 days
|
||||
after, sent one at a time or as a sweep. Ability `renewal:send` (MANAGER).
|
||||
|
||||
**One send log for the whole platform.** `email_notification_log` is not
|
||||
job-specific: renewals write it too (`RENEWAL_NOTICE` / `POLICIES`) through the
|
||||
same `NotificationLogService`. That is what makes "Registro de envíos" complete
|
||||
— the failures and no-email skips exist *only* there. `RenewalNotice` was not
|
||||
made redundant by it: that row is **gating** state (one per policy+generation,
|
||||
drives the pending list), the log is **history** (every attempt). `level` is
|
||||
therefore per-type and unreadable without its `notificationType` — 0/1
|
||||
yellow/red on `ACCOUNT_STATUS`, the aviso generation 1/2/3 on
|
||||
`RENEWAL_NOTICE`.
|
||||
|
||||
**Manual mark-as-sent was dropped on purpose.** The spec called for it; a
|
||||
button that marks a notice sent without sending anything is a button that lets
|
||||
the list claim a customer was told when they were not. `POST /renewals/send`
|
||||
replaced it — sending from the list *is* the marking.
|
||||
|
||||
**`app_settings` is the operator-config seam this work introduced.**
|
||||
`SettingsService` resolves every key **db → env → default** and reports which
|
||||
rung a value came from, so an existing deployment keeps behaving exactly as it
|
||||
did until somebody saves in the UI. Three keys today: the summary recipients
|
||||
(was `NOTIFICATION_ADMIN_EMAILS`, now a fallback) and the two sweep cadences.
|
||||
Credentials deliberately stay in the environment — SES keys, `DATABASE_URL`
|
||||
and S3 config are deployment identity, must exist before the app can reach its
|
||||
own database, and a table only widens who can read them.
|
||||
|
||||
**The send flags are global, and that was a real bug fix (08-02).** The
|
||||
`debug` / `ignoreDayRestriction` / `useEmailLimit` panel lived inside the
|
||||
Servicios tab, so there was **no way to test a renewal aviso without mailing a
|
||||
real customer**. It now lives in the shell above the tabs and both halves read
|
||||
it. On the pólizas path `debug` does three things, and all three are required
|
||||
together: it diverts the mail, it skips the `RenewalNotice` upsert, and it does
|
||||
not advance the sweep's `lastSuccessfulAt`. Miss the third and `renewalWindow()`
|
||||
narrows back to a single day on the next real run — a test send would silently
|
||||
destroy the letters it only pretended to send. Flags are per-visit UI state and
|
||||
are **never persisted**; a stored `debug` would survive a reload and swallow
|
||||
real customer mail until somebody noticed.
|
||||
|
||||
**Both cadences are operator-editable (08-02).** The renewal sweep's
|
||||
`@Cron("0 6 * * *")` literal lasted one day. `NotificationScheduleService` now
|
||||
owns both: the owning services register a handler in `onModuleInit`, the
|
||||
service compiles the stored `{hour, minute, weekdays}` to a cron expression and
|
||||
installs it in `SchedulerRegistry`, and saving from the UI reinstalls the job —
|
||||
no restart, which was the point. It lives in its own module for the same reason
|
||||
as `NotificationLogModule`: `NotificationsModule` and `RenewalsModule` both need
|
||||
it and neither may import the other. Defaults preserve prior behaviour exactly
|
||||
(pólizas 06:00 daily, servicios **off** — a default that starts mailing 260
|
||||
customers after a deploy is not a default, it's an incident). A scheduled run
|
||||
never inherits the UI flags: no `debug`, and no `ignoreDayRestriction`, since an
|
||||
automatic run on the operator's own cadence is precisely the case the
|
||||
Mon/Wed/Fri gate was written for.
|
||||
|
||||
Implementation notes worth keeping:
|
||||
|
||||
- `cron` had to become a **direct dependency of `apps/api`**. It is a
|
||||
transitive dep of `@nestjs/schedule`, but pnpm's strict layout does not hoist
|
||||
it, so `import { CronJob } from "cron"` does not resolve without it.
|
||||
- The pólizas sweep already had a DB lock (`scheduled_job_states`); the
|
||||
servicios run-all does not, and relies on the deployment being
|
||||
single-replica, which it is on galactus today.
|
||||
- Wire shapes of the four jobs are byte-for-byte the legacy PHP responses,
|
||||
quirks included (Job 1 reports `result`, not `request`).
|
||||
|
||||
**Open:** `SES_*` is wired through the deploy workflow but **unset in Gitea**,
|
||||
so production sends fail loudly rather than going out. The 78 policyholders
|
||||
with no email are logged as `SKIPPED_NO_EMAIL` but there is still no printable
|
||||
worklist for them, and the notice body is English-only (`Customer` carries no
|
||||
language preference) — the same three questions §1 of the insurance spec
|
||||
opened.
|
||||
|
||||
Reference in New Issue
Block a user