feat(notificaciones): mass email notifications over SES
Replaces the four legacy PHP scripts under email.notifications/send*.php with a single NestJS module. Four jobs (outstanding payments, payment confirmations, account-status alerts with day-of-week gates, trust payment confirmations) share one MailService modelled on StorageService: env-driven SES client, null fallback in dev with console logging, refuses to send in production when unconfigured. Schema adds email_notification_log (every attempt, sent/failed/skipped) and account_status_history (one row per threshold hit, Job 3). Enums encode the legacy wire shape so external log scrapers keep parsing notificationType keys verbatim. Web adds /notificaciones with four trigger cards, a flags panel, and a paginated log browser. New notification:send ability gates all four endpoints at MANAGER, matching the renewal:send trust tier.
This commit is contained in:
@@ -972,6 +972,185 @@ export function runReport(
|
||||
return apiFetch<ReportRunResult>(`/reports/${slug}${tail ? `?${tail}` : ""}`);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------- Mass email notifications */
|
||||
|
||||
export type NotificationType =
|
||||
| "OUTSTANDING_PAYMENT"
|
||||
| "PAYMENT_CONFIRMATION"
|
||||
| "ACCOUNT_STATUS"
|
||||
| "TRUST_PAYMENT_CONFIRMATION";
|
||||
|
||||
export type NotificationServicio = "CUSTOMERS" | "TRUST";
|
||||
|
||||
export type NotificationStatus =
|
||||
| "SENT"
|
||||
| "FAILED"
|
||||
| "SKIPPED_NO_EMAIL"
|
||||
| "SKIPPED_GATE";
|
||||
|
||||
export interface NotificationLogRow {
|
||||
id: string;
|
||||
sendDate: string;
|
||||
notificationType: NotificationType;
|
||||
level: number | null;
|
||||
servicio: NotificationServicio;
|
||||
customerId: string | null;
|
||||
customerName: string;
|
||||
customerEmail: string;
|
||||
subject: string;
|
||||
debug: boolean;
|
||||
status: NotificationStatus;
|
||||
providerMessageId: string | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export interface NotificationLogPage {
|
||||
items: NotificationLogRow[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
pageCount: number;
|
||||
}
|
||||
|
||||
export interface NotificationStats {
|
||||
byType: { notificationType: NotificationType; status: NotificationStatus; _count: { _all: number } }[];
|
||||
byStatus: { status: NotificationStatus; _count: { _all: number } }[];
|
||||
byServicio: { servicio: NotificationServicio; status: NotificationStatus; _count: { _all: number } }[];
|
||||
lastRun: { sendDate: string; notificationType: NotificationType } | null;
|
||||
transport: { available: boolean; devFallback: boolean };
|
||||
}
|
||||
|
||||
export type NotificationFlags = {
|
||||
debug?: boolean;
|
||||
ignoreDayRestriction?: boolean;
|
||||
useEmailLimit?: boolean;
|
||||
};
|
||||
|
||||
/** Job 1 (Outstanding) response — legacy `result` field. */
|
||||
export interface OutstandingResponse {
|
||||
result: "success";
|
||||
notificationType: "sendPaymentConfirmation";
|
||||
reason: string;
|
||||
statusCode: 200;
|
||||
sent: number;
|
||||
skipped: number;
|
||||
failed: number;
|
||||
debug: boolean;
|
||||
type: "OUTSTANDING_PAYMENT";
|
||||
}
|
||||
|
||||
/** Job 2 (Payment Confirmation) response. */
|
||||
export interface PaymentConfirmResponse {
|
||||
request: "success";
|
||||
notificationType: "sendPaymentConfirmation";
|
||||
confirmationSent: string;
|
||||
statusCode: 200;
|
||||
sent: number;
|
||||
skipped: number;
|
||||
failed: number;
|
||||
debug: boolean;
|
||||
type: "PAYMENT_CONFIRMATION";
|
||||
}
|
||||
|
||||
/** Job 3 (Account Status) response. */
|
||||
export interface AccountStatusResponse {
|
||||
request: "success";
|
||||
notificationType: "sendAccountStatus";
|
||||
statusSent: string;
|
||||
statusReport: string;
|
||||
statusCode: 200;
|
||||
red: number;
|
||||
yellow: number;
|
||||
total: number;
|
||||
sent: number;
|
||||
skipped: number;
|
||||
failed: number;
|
||||
debug: boolean;
|
||||
type: "ACCOUNT_STATUS";
|
||||
}
|
||||
|
||||
/** Job 4 (Trust Confirmation) response. */
|
||||
export interface TrustConfirmResponse {
|
||||
request: "success";
|
||||
notificationType: "sendTrustPaymentConfirmation";
|
||||
confirmationSent: string;
|
||||
statusCode: 200;
|
||||
sent: number;
|
||||
skipped: number;
|
||||
failed: number;
|
||||
debug: boolean;
|
||||
type: "TRUST_PAYMENT_CONFIRMATION";
|
||||
}
|
||||
|
||||
export type NotificationJobResponse =
|
||||
| OutstandingResponse
|
||||
| PaymentConfirmResponse
|
||||
| AccountStatusResponse
|
||||
| TrustConfirmResponse;
|
||||
|
||||
export function runOutstandingPayments(
|
||||
flags: NotificationFlags = {},
|
||||
): Promise<OutstandingResponse> {
|
||||
return apiFetch<OutstandingResponse>("/notifications/outstanding-payments", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(flags),
|
||||
});
|
||||
}
|
||||
|
||||
export function runPaymentConfirmation(
|
||||
flags: NotificationFlags = {},
|
||||
): Promise<PaymentConfirmResponse> {
|
||||
return apiFetch<PaymentConfirmResponse>("/notifications/payment-confirmation", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(flags),
|
||||
});
|
||||
}
|
||||
|
||||
export function runAccountStatus(
|
||||
flags: NotificationFlags = {},
|
||||
): Promise<AccountStatusResponse> {
|
||||
return apiFetch<AccountStatusResponse>("/notifications/account-status", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(flags),
|
||||
});
|
||||
}
|
||||
|
||||
export function runTrustConfirmation(
|
||||
flags: NotificationFlags = {},
|
||||
): Promise<TrustConfirmResponse> {
|
||||
return apiFetch<TrustConfirmResponse>("/notifications/trust-payment-confirmation", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(flags),
|
||||
});
|
||||
}
|
||||
|
||||
export interface NotificationLogQuery {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
type?: NotificationType;
|
||||
servicio?: NotificationServicio;
|
||||
status?: NotificationStatus;
|
||||
view?: "sent" | "failed" | "skipped" | "all";
|
||||
}
|
||||
|
||||
export function listNotificationLog(
|
||||
q: NotificationLogQuery = {},
|
||||
): Promise<NotificationLogPage> {
|
||||
const qs = new URLSearchParams();
|
||||
if (q.page) qs.set("page", String(q.page));
|
||||
if (q.pageSize) qs.set("pageSize", String(q.pageSize));
|
||||
if (q.type) qs.set("type", q.type);
|
||||
if (q.servicio) qs.set("servicio", q.servicio);
|
||||
if (q.status) qs.set("status", q.status);
|
||||
if (q.view) qs.set("view", q.view);
|
||||
const tail = qs.toString();
|
||||
return apiFetch<NotificationLogPage>(`/notifications/log${tail ? `?${tail}` : ""}`);
|
||||
}
|
||||
|
||||
export function getNotificationStats(): Promise<NotificationStats> {
|
||||
return apiFetch<NotificationStats>("/notifications/stats");
|
||||
}
|
||||
|
||||
/** Build a download URL for a report's file output. The session cookie
|
||||
* travels with the browser's same-origin navigation, so a plain `href`
|
||||
* is enough — no fetch-with-credentials dance. */
|
||||
|
||||
Reference in New Issue
Block a user