MailModule's provider used a `useFactory` with no `inject`, so the factory
received `undefined` and `new MailService(config)` threw on `config.get`,
taking the whole API down at boot. The module also wasn't actually
`@Global()` even though both NotificationsModule and RenewalsModule inject
MailService without importing it — that would have failed next. Replaced the
factory with a plain provider (ConfigModule is already `isGlobal`) and marked
the module global.
On the web side, mass email and renewal notices were two menu entries doing
the same job — telling a customer something by email. They are now two tabs
of `/notificaciones` (Servicios and Pólizas), following the Captura pattern:
`/renovaciones` still resolves, opening the same screen on its Pólizas tab so
existing bookmarks keep working.
The notifications page was also the last screen written in raw inline styles,
with blue buttons and filter pills that appear nowhere else in the app. It now
uses the shared design system: btn-primary/btn-outline, the seg segmented
control, card, tx-table, pager, and the servicios/fideicomiso badges.
Two supporting fixes found on the way: NOTIFICATION_STATUS_COLORS hardcoded
hex instead of the theme's positive/negative/muted vars, and `.small` was
referenced in 19 places across the app but never defined in globals.css.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
INSURANCE_FEATURES_SPEC §1. The office printed and mailed renewal letters
from the legacy CONTROL <ramo> RENEW[2/3] paper log; 91% of policyholders
have an email on file, so send the notice instead and keep the paper log
as the fallback.
A daily cron (06:00 America/Tijuana) sweeps three generations off
policyTo — 30 and 15 days before expiry, 7 days after — sends each
through SES, and upserts RenewalNotice by [policyId, generation] so a
policy is never notified twice for the same milestone. RenewalNotice now
records providerMessageId, so a later bounce or complaint webhook can be
traced back to the row that sent it.
- customers.emailOptOut excludes a customer from every sweep; editable
from the customer form
- scheduled_job_states holds the sweep's lock and last successful run;
the window is widened to cover days the job did not run, so a weekend
outage does not silently drop a generation
- SES unconfigured is not an error outside production — messages are
logged and skipped, so dev and CI never send
- /renovaciones (renewal:send, MANAGER+) lists what is pending per
generation, runs the sweep by hand, and marks a notice sent by mail
for the customers with no email
- POST /policies/:id/renewal-notices records that manual mark
- the aviso-renovacion report and the emails now share one projection
(reports/renewal-letter.ts) instead of two copies of the mapping