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:
@@ -31,7 +31,17 @@ class ListLogDto {
|
||||
@IsOptional() @Type(() => Number) @IsInt() @Min(1) page?: number;
|
||||
@IsOptional() @Type(() => Number) @IsInt() @Min(1) @Max(200) pageSize?: number;
|
||||
@IsOptional() @IsEnum(EmailNotificationType) type?: EmailNotificationType;
|
||||
@IsOptional() @IsEnum(EmailNotificationServicio) servicio?: EmailNotificationServicio;
|
||||
/** One or more servicios, comma-separated. The /notificaciones tabs each
|
||||
* read their own slice of the one log: Servicios passes
|
||||
* `CUSTOMERS,TRUST`, Pólizas passes `POLICIES`. Omitted = every servicio. */
|
||||
@IsOptional()
|
||||
@Transform(({ value }) =>
|
||||
typeof value === "string"
|
||||
? value.split(",").map((s) => s.trim()).filter(Boolean)
|
||||
: value,
|
||||
)
|
||||
@IsEnum(EmailNotificationServicio, { each: true })
|
||||
servicio?: EmailNotificationServicio[];
|
||||
@IsOptional() @IsEnum(EmailNotificationStatus) status?: EmailNotificationStatus;
|
||||
@IsOptional() @IsEnum(["sent", "failed", "skipped", "all"]) view?: "sent" | "failed" | "skipped" | "all";
|
||||
}
|
||||
@@ -185,19 +195,27 @@ export class NotificationsController {
|
||||
}
|
||||
|
||||
@Get("stats")
|
||||
stats() {
|
||||
return this.svc.stats();
|
||||
stats(@Query() q: ListLogDto) {
|
||||
return this.svc.stats(q.servicio);
|
||||
}
|
||||
|
||||
/** Resolve the UI's coarse view tabs to concrete statuses. An explicit
|
||||
* `status` wins. "Omitidos" covers both SKIPPED_* variants, which is why
|
||||
* this returns a list rather than a single value. */
|
||||
private mapViewStatus(
|
||||
view: ListLogDto["view"],
|
||||
status: ListLogDto["status"],
|
||||
): EmailNotificationStatus | undefined {
|
||||
if (status) return status;
|
||||
): EmailNotificationStatus[] | undefined {
|
||||
if (status) return [status];
|
||||
if (!view || view === "all") return undefined;
|
||||
if (view === "sent") return EmailNotificationStatus.SENT;
|
||||
if (view === "failed") return EmailNotificationStatus.FAILED;
|
||||
if (view === "skipped") return undefined; // both SKIPPED_* variants
|
||||
if (view === "sent") return [EmailNotificationStatus.SENT];
|
||||
if (view === "failed") return [EmailNotificationStatus.FAILED];
|
||||
if (view === "skipped") {
|
||||
return [
|
||||
EmailNotificationStatus.SKIPPED_NO_EMAIL,
|
||||
EmailNotificationStatus.SKIPPED_GATE,
|
||||
];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user