feat(notificaciones): one send log across servicios and pólizas
Build and Push Images / Build jorgecuadros-web (push) Successful in 2m30s
Build and Push Images / Build jorgecuadros-api (push) Failing after 3h13m42s

Renewal avisos left behind only a `RenewalNotice` row, whose sole job is
gating: a row with `sentAt` drops the policy off the pending list. It
cannot represent a failed send or a customer with no address, so the
Pólizas tab had no "Registro de envíos" to show and a sent notice simply
vanished from the list.

Renewals now write `email_notification_log` — the same table the four
bulk jobs write — as `RENEWAL_NOTICE` / `POLICIES`, with rows for
failures and no-email skips too. `RenewalNotice` keeps its gating role
unchanged; the two are complementary, not redundant.

- extend `EmailNotificationType` (+RENEWAL_NOTICE) and
  `EmailNotificationServicio` (+POLICIES); `level` now carries the aviso
  generation on renewal rows, so every reader must branch on the type
  first (`notificationLevelLabel()` is the one place that lives)
- backfill emailed notices (`channel = 'EMAIL'`) into the log; MAIL-channel
  rows are legacy printed letters and are deliberately left out
- extract `NotificationLogService`/`NotificationLogModule` as the single
  writer, so a feature that sends mail records it without pulling the
  bulk-job pipelines into its module
- `GET /notifications/log` and `/stats` take a comma-separated `servicio`
  list; each tab reads its own slice. This also fixes the "Omitidos"
  view, which mapped to no filter at all and showed every row
- share one `NotificationLogPanel` between both tabs
- pass SES_* / NOTIFICATION_ADMIN_EMAILS through the galactus compose,
  which was missing them entirely — mail is runtime config, not a CI
  secret, and the prod image sets NODE_ENV=production so a blank config
  fails loudly instead of falling back to stdout

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-02 03:01:03 -07:00
co-authored by Claude Opus 5
parent c0cc0d2ac2
commit 33833c3af9
20 changed files with 813 additions and 194 deletions
+22
View File
@@ -393,13 +393,35 @@ export const NOTIFICATION_TYPE_LABELS: Record<NotificationType, string> = {
PAYMENT_CONFIRMATION: "Confirmación de pago",
ACCOUNT_STATUS: "Estado de cuenta",
TRUST_PAYMENT_CONFIRMATION: "Confirmación fideicomiso",
RENEWAL_NOTICE: "Aviso de renovación",
};
export const NOTIFICATION_SERVICIO_LABELS: Record<NotificationServicio, string> = {
CUSTOMERS: "Clientes",
TRUST: "Fideicomiso",
POLICIES: "Pólizas",
};
/**
* The `level` column means something different per notification type, so it
* can only be read alongside one. ACCOUNT_STATUS uses it for the alert colour;
* RENEWAL_NOTICE for the aviso generation. Everything else leaves it null.
*/
export function notificationLevelLabel(
type: NotificationType,
level: number | null,
): string {
if (level === null) return "";
if (type === "ACCOUNT_STATUS") return level === 0 ? " (amarilla)" : " (roja)";
if (type === "RENEWAL_NOTICE") {
if (level === 1) return " (1.º, 30 días antes)";
if (level === 2) return " (2.º, 15 días antes)";
if (level === 3) return " (3.º, 7 días después)";
return ` (aviso ${level})`;
}
return "";
}
export const NOTIFICATION_STATUS_LABELS: Record<NotificationStatus, string> = {
SENT: "Enviado",
FAILED: "Falló",