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
First master-data CRUD module on the phase-1 RBAC foundation.
API:
- Customer gains archivedAt (soft-delete marker, distinct from the legacy
`status` business flag); pushed to dev (nullable, non-destructive).
- CustomersService: create/update/archive/restore. list() and the browser
default to archivedAt=null; ?includeArchived=true opts in. App-created
rows set nameMissing=false and leave legacy provenance null.
- CustomersController write routes guarded per the matrix: create/update
need STAFF+ (customer:create/update), archive/restore need ADMIN
(customer:delete). Every mutation audit-logged.
- create/update DTOs (class-validator); date strings coerced to Date.
Web:
- Shared CustomerForm (create + edit) with identity/address/account
sections; new routes /clientes/nuevo and /clientes/[id]/editar, each
self-gated on the ability.
- List page: ability-gated "Nuevo cliente" button. Detail page: gated
Editar / Archivar (Restaurar) action bar; archived badge.
- api.ts create/update/archive/restore; CustomerInput type; archived flag
on list items.
Verified against dev: create (dates coerced, archivedAt null), edit 200,
VIEWER create 403, STAFF create 201 but archive 403, ADMIN archive drops
the row from the default list and includeArchived surfaces it, restore
returns it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>