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
66 lines
2.6 KiB
TypeScript
66 lines
2.6 KiB
TypeScript
import type { RenewalLetterRow } from "../reports/renewal-letter";
|
|
|
|
const GENERATION_TEXT: Record<number, string> = {
|
|
1: "Le enviamos el primer aviso para renovar su póliza.",
|
|
2: "Le enviamos el segundo aviso para renovar su póliza.",
|
|
3: "Le informamos que su póliza está vencida.",
|
|
};
|
|
|
|
function escapeHtml(value: unknown): string {
|
|
return String(value ?? "")
|
|
.replaceAll("&", "&")
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">")
|
|
.replaceAll('"', """)
|
|
.replaceAll("'", "'");
|
|
}
|
|
|
|
function displayDate(value: string): string {
|
|
if (value === "—") return value;
|
|
const [year, month, day] = value.split("-");
|
|
return `${day}/${month}/${year}`;
|
|
}
|
|
|
|
function money(value: string | null, currency: string): string {
|
|
if (!value) return "No disponible";
|
|
return new Intl.NumberFormat("es-MX", {
|
|
style: "currency",
|
|
currency,
|
|
minimumFractionDigits: 2,
|
|
}).format(Number(value));
|
|
}
|
|
|
|
function row(label: string, value: string): string {
|
|
return `<tr><th style="padding:8px 12px;text-align:left;background:#f4f4f4;border:1px solid #ddd">${escapeHtml(label)}</th><td style="padding:8px 12px;border:1px solid #ddd">${escapeHtml(value)}</td></tr>`;
|
|
}
|
|
|
|
export function renderRenewalEmail(letter: RenewalLetterRow): {
|
|
subject: string;
|
|
html: string;
|
|
} {
|
|
const expired = letter.generation === 3;
|
|
const subject = expired
|
|
? `Póliza vencida: ${letter.policyNumber}`
|
|
: `Aviso de renovación: póliza ${letter.policyNumber}`;
|
|
const phone = letter.customerMobile ?? letter.customerPhone ?? "No disponible";
|
|
const address = letter.customerAddress.join(", ") || "No disponible";
|
|
const premium = letter.total ?? letter.netPremium;
|
|
|
|
const details = [
|
|
row("Número de póliza", letter.policyNumber),
|
|
row("Tipo de póliza", letter.policyType),
|
|
row("Aseguradora", letter.provider),
|
|
row("Fecha de vencimiento", displayDate(letter.policyTo)),
|
|
row("Prima", money(premium, letter.currency)),
|
|
row("Cliente", letter.customerName),
|
|
row("Correo", letter.customerEmail ?? "No disponible"),
|
|
row("Teléfono", phone),
|
|
row("Dirección", address),
|
|
].join("");
|
|
|
|
return {
|
|
subject,
|
|
html: `<div style="font-family:Arial,sans-serif;color:#222;line-height:1.5"><p>Estimado(a) ${escapeHtml(letter.customerName)}:</p><p>${escapeHtml(GENERATION_TEXT[letter.generation] ?? "Le enviamos un aviso sobre la renovación de su póliza.")}</p><table style="border-collapse:collapse;width:100%;max-width:680px">${details}</table><p>Por favor, comuníquese con Jorge Cuadros & Asociados para revisar su renovación.</p><p>Atentamente,<br>Jorge Cuadros & Asociados</p></div>`,
|
|
};
|
|
}
|