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:
@@ -10,6 +10,7 @@ import {
|
||||
} from "@jorgecuadros/database";
|
||||
import { MailService } from "../mail/mail.service";
|
||||
import { PrismaService } from "../prisma/prisma.service";
|
||||
import { NotificationLogService } from "./notification-log.service";
|
||||
import {
|
||||
SendAttempt,
|
||||
NotificationJobKind,
|
||||
@@ -81,6 +82,7 @@ export class NotificationsService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly mail: MailService,
|
||||
private readonly log: NotificationLogService,
|
||||
config: ConfigService,
|
||||
) {
|
||||
const csv = config.get<string>("NOTIFICATION_ADMIN_EMAILS");
|
||||
@@ -721,14 +723,16 @@ export class NotificationsService {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
type?: EmailNotificationType;
|
||||
servicio?: EmailNotificationServicio;
|
||||
status?: EmailNotificationStatus;
|
||||
/** Empty/omitted = every servicio. The /notificaciones tabs pass their
|
||||
* own slice (Servicios: CUSTOMERS+TRUST, Pólizas: POLICIES). */
|
||||
servicio?: EmailNotificationServicio[];
|
||||
status?: EmailNotificationStatus[];
|
||||
customerId?: string;
|
||||
}) {
|
||||
const where: Prisma.EmailNotificationLogWhereInput = {};
|
||||
if (params.type) where.notificationType = params.type;
|
||||
if (params.servicio) where.servicio = params.servicio;
|
||||
if (params.status) where.status = params.status;
|
||||
if (params.servicio?.length) where.servicio = { in: params.servicio };
|
||||
if (params.status?.length) where.status = { in: params.status };
|
||||
if (params.customerId) where.customerId = params.customerId;
|
||||
|
||||
const [total, rows] = await this.prisma.$transaction([
|
||||
@@ -765,22 +769,32 @@ export class NotificationsService {
|
||||
};
|
||||
}
|
||||
|
||||
/** Per-type + per-status counts for the dashboard header. */
|
||||
async stats() {
|
||||
/** Per-type + per-status counts for the dashboard header. Scoped by
|
||||
* servicio so each /notificaciones tab reports its own totals instead of
|
||||
* the whole platform's. */
|
||||
async stats(servicio?: EmailNotificationServicio[]) {
|
||||
const where: Prisma.EmailNotificationLogWhereInput = servicio?.length
|
||||
? { servicio: { in: servicio } }
|
||||
: {};
|
||||
|
||||
const [byType, byStatus, byServicio, lastRun] = await Promise.all([
|
||||
this.prisma.emailNotificationLog.groupBy({
|
||||
by: ["notificationType", "status"],
|
||||
where,
|
||||
_count: { _all: true },
|
||||
}),
|
||||
this.prisma.emailNotificationLog.groupBy({
|
||||
by: ["status"],
|
||||
where,
|
||||
_count: { _all: true },
|
||||
}),
|
||||
this.prisma.emailNotificationLog.groupBy({
|
||||
by: ["servicio", "status"],
|
||||
where,
|
||||
_count: { _all: true },
|
||||
}),
|
||||
this.prisma.emailNotificationLog.findFirst({
|
||||
where,
|
||||
orderBy: { sendDate: "desc" },
|
||||
select: { sendDate: true, notificationType: true },
|
||||
}),
|
||||
@@ -917,7 +931,9 @@ export class NotificationsService {
|
||||
}
|
||||
}
|
||||
|
||||
/** Persist one notification log row. */
|
||||
/** Persist one notification log row. Thin pass-through to the shared
|
||||
* writer — the renewal sweep writes the same rows through the same
|
||||
* service, which is what keeps /notificaciones' log complete. */
|
||||
private async recordAttempt(args: {
|
||||
notificationType: EmailNotificationType;
|
||||
servicio: EmailNotificationServicio;
|
||||
@@ -934,24 +950,7 @@ export class NotificationsService {
|
||||
providerResponse?: string;
|
||||
error?: string;
|
||||
}) {
|
||||
await this.prisma.emailNotificationLog.create({
|
||||
data: {
|
||||
notificationType: args.notificationType,
|
||||
servicio: args.servicio,
|
||||
level: args.level ?? null,
|
||||
customerId: args.customerId,
|
||||
customerName: args.customerName,
|
||||
customerEmail: args.customerEmail,
|
||||
subject: args.subject,
|
||||
bodySnapshot: args.bodySnapshot,
|
||||
bodyRequestUrl: args.bodyRequestUrl ?? null,
|
||||
debug: args.debug,
|
||||
providerMessageId: args.providerMessageId ?? null,
|
||||
providerResponse: args.providerResponse ?? null,
|
||||
status: args.status as EmailNotificationStatus,
|
||||
error: args.error ?? null,
|
||||
},
|
||||
});
|
||||
await this.log.record(args);
|
||||
}
|
||||
|
||||
/** Send the admin summary email after every job. The PHP sent one to
|
||||
|
||||
Reference in New Issue
Block a user