feat(renovaciones): renewal notification emails over SES
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
This commit is contained in:
+15
@@ -0,0 +1,15 @@
|
||||
ALTER TABLE `customers`
|
||||
ADD COLUMN `emailOptOut` BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
ALTER TABLE `renewal_notices`
|
||||
ADD COLUMN `providerMessageId` VARCHAR(191) NULL;
|
||||
|
||||
CREATE TABLE `scheduled_job_states` (
|
||||
`name` VARCHAR(191) NOT NULL,
|
||||
`lockedUntil` DATETIME(3) NULL,
|
||||
`lastSuccessfulAt` DATETIME(3) NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
PRIMARY KEY (`name`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
@@ -99,6 +99,7 @@ model Customer {
|
||||
mobile String?
|
||||
fax String?
|
||||
email String?
|
||||
emailOptOut Boolean @default(false)
|
||||
notes String? @db.Text
|
||||
identificationType String?
|
||||
identificationNumber String?
|
||||
@@ -230,16 +231,17 @@ enum RenewalNoticeChannel {
|
||||
/// 3rd notice and when" is a query instead of a paper trail. See
|
||||
/// docs/RENEWAL_NOTICES.md for the legacy report chain this replaces.
|
||||
model RenewalNotice {
|
||||
id String @id @default(uuid())
|
||||
policyId String
|
||||
policy Policy @relation(fields: [policyId], references: [id])
|
||||
id String @id @default(uuid())
|
||||
policyId String
|
||||
policy Policy @relation(fields: [policyId], references: [id])
|
||||
// 1 = first notice (bare RENEW), 2 = RENEW2, 3 = RENEW3 in the legacy naming.
|
||||
generation Int
|
||||
channel RenewalNoticeChannel @default(MAIL)
|
||||
sentAt DateTime?
|
||||
sentById String?
|
||||
notes String? @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
generation Int
|
||||
channel RenewalNoticeChannel @default(MAIL)
|
||||
sentAt DateTime?
|
||||
sentById String?
|
||||
providerMessageId String?
|
||||
notes String? @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
// One row per generation per policy — matches the legacy's 1st/2nd/3rd
|
||||
// notice cadence; re-running the same generation for a policy updates it
|
||||
@@ -953,3 +955,13 @@ model OpsJob {
|
||||
@@index([startedAt])
|
||||
@@map("ops_jobs")
|
||||
}
|
||||
|
||||
model ScheduledJobState {
|
||||
name String @id
|
||||
lockedUntil DateTime?
|
||||
lastSuccessfulAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("scheduled_job_states")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user