Merge branch 'massive-email-notification' into master
Build and Push Images / Build jorgecuadros-web (push) Successful in 1m50s
Build and Push Images / Build jorgecuadros-api (push) Successful in 2m7s

# Conflicts:
#	.env.example
#	apps/api/src/app.module.ts
This commit is contained in:
2026-08-02 02:05:36 -07:00
23 changed files with 2883 additions and 139 deletions
+179
View File
@@ -973,6 +973,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. */
+34
View File
@@ -379,3 +379,37 @@ export function sourceSystemLabel(source: string): string {
};
return map[source] ?? source;
}
// ----- Mass email notifications -----
import type {
NotificationStatus,
NotificationType,
NotificationServicio,
} from "./api";
export const NOTIFICATION_TYPE_LABELS: Record<NotificationType, string> = {
OUTSTANDING_PAYMENT: "Pagos pendientes",
PAYMENT_CONFIRMATION: "Confirmación de pago",
ACCOUNT_STATUS: "Estado de cuenta",
TRUST_PAYMENT_CONFIRMATION: "Confirmación fideicomiso",
};
export const NOTIFICATION_SERVICIO_LABELS: Record<NotificationServicio, string> = {
CUSTOMERS: "Clientes",
TRUST: "Fideicomiso",
};
export const NOTIFICATION_STATUS_LABELS: Record<NotificationStatus, string> = {
SENT: "Enviado",
FAILED: "Falló",
SKIPPED_NO_EMAIL: "Sin email",
SKIPPED_GATE: "Fuera de día",
};
export const NOTIFICATION_STATUS_COLORS: Record<NotificationStatus, string> = {
SENT: "#1f7a3a",
FAILED: "#b3261e",
SKIPPED_NO_EMAIL: "#666",
SKIPPED_GATE: "#888",
};
+2 -1
View File
@@ -27,7 +27,8 @@ export type Ability =
| "statement:review"
| "lookup:manage"
| "user:manage"
| "db:manage";
| "db:manage"
| "notification:send";
export interface AuthUser {
id: string;