feat(notificaciones): one send log across servicios and pólizas
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:
1 parent
c0cc0d2ac2
commit
33833c3af9
20 files changed
+813
-194
No files matched your search
@@ -0,0 +1,74 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import {
|
||||
EmailNotificationServicio,
|
||||
EmailNotificationStatus,
|
||||
EmailNotificationType,
|
||||
} from "@jorgecuadros/database";
|
||||
import { PrismaService } from "../prisma/prisma.service";
|
||||
import { AttemptStatus } from "./notification.types";
|
||||
|
||||
/**
|
||||
* The single writer for `email_notification_log`.
|
||||
*
|
||||
* Extracted out of `NotificationsService` so the renewal sweep can write the
|
||||
* same rows as the four bulk jobs without pulling that service (and its four
|
||||
* job pipelines) into `RenewalsModule`. Every outbound email the platform
|
||||
* sends goes through here, which is what makes /notificaciones' "Registro de
|
||||
* envíos" complete rather than per-feature.
|
||||
*/
|
||||
export interface NotificationLogEntry {
|
||||
notificationType: EmailNotificationType;
|
||||
servicio: EmailNotificationServicio;
|
||||
/** Defaults to now(). Pass it when the row must line up exactly with
|
||||
* another record of the same send (the renewal sweep pins it to
|
||||
* `RenewalNotice.sentAt`). */
|
||||
sendDate?: Date;
|
||||
/** Type-dependent discriminator — see the `level` doc on the Prisma model.
|
||||
* 0/1 for ACCOUNT_STATUS, the generation for RENEWAL_NOTICE. */
|
||||
level?: number | null;
|
||||
customerId: string | null;
|
||||
customerName: string;
|
||||
customerEmail: string;
|
||||
subject: string;
|
||||
bodySnapshot: string;
|
||||
bodyRequestUrl?: string;
|
||||
status: AttemptStatus;
|
||||
debug: boolean;
|
||||
providerMessageId?: string;
|
||||
providerResponse?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/** `providerResponse` is a VARCHAR(191); anything longer is a provider dump
|
||||
* we only need the head of. Errors go to the TEXT `error` column and get
|
||||
* the 4k cap the schema documents. */
|
||||
const PROVIDER_RESPONSE_MAX = 180;
|
||||
const ERROR_MAX = 4096;
|
||||
|
||||
@Injectable()
|
||||
export class NotificationLogService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async record(entry: NotificationLogEntry): Promise<void> {
|
||||
await this.prisma.emailNotificationLog.create({
|
||||
data: {
|
||||
notificationType: entry.notificationType,
|
||||
servicio: entry.servicio,
|
||||
...(entry.sendDate && { sendDate: entry.sendDate }),
|
||||
level: entry.level ?? null,
|
||||
customerId: entry.customerId,
|
||||
customerName: entry.customerName,
|
||||
customerEmail: entry.customerEmail,
|
||||
subject: entry.subject,
|
||||
bodySnapshot: entry.bodySnapshot,
|
||||
bodyRequestUrl: entry.bodyRequestUrl ?? null,
|
||||
debug: entry.debug,
|
||||
providerMessageId: entry.providerMessageId ?? null,
|
||||
providerResponse:
|
||||
entry.providerResponse?.slice(0, PROVIDER_RESPONSE_MAX) ?? null,
|
||||
status: entry.status as EmailNotificationStatus,
|
||||
error: entry.error?.slice(0, ERROR_MAX) ?? null,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user