feat(notificaciones): ejecutar todos for servicios jobs
Add POST /notifications/run-all: runs the four notification jobs (outstanding, payment confirmation, account status, trust confirmation) sequentially with one shared set of flags from "Flags del envío". Sequential rather than parallel — the jobs share the SES transport and account status can self-throttle via useEmailLimit. A job that throws is captured and the sweep continues, so one bad query cannot swallow the other three envíos; the aggregate response carries per-job results plus summed sent/skipped/failed and an errors count. Audited as a single notification.run-all.run entry so one staff click is one audit row. UI adds the button to the flags card, with a confirm when debug is off, and a per-job summary in "Última respuesta".
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
getNotificationStats,
|
||||
listNotificationLog,
|
||||
runAccountStatus,
|
||||
runAllNotifications,
|
||||
runOutstandingPayments,
|
||||
runPaymentConfirmation,
|
||||
runTrustConfirmation,
|
||||
@@ -21,6 +22,7 @@ import type {
|
||||
NotificationFlags,
|
||||
NotificationJobResponse,
|
||||
NotificationLogPage,
|
||||
NotificationRunAllResponse,
|
||||
NotificationStats,
|
||||
NotificationStatus,
|
||||
} from "@/lib/api";
|
||||
@@ -79,6 +81,12 @@ const JOBS: JobDef[] = [
|
||||
},
|
||||
];
|
||||
|
||||
/** Job title by kind — used by the run-all summary, which only carries kinds. */
|
||||
const JOB_TITLES: Record<JobKind, string> = JOBS.reduce(
|
||||
(acc, j) => ({ ...acc, [j.kind]: j.title }),
|
||||
{} as Record<JobKind, string>,
|
||||
);
|
||||
|
||||
const LOG_VIEWS = [
|
||||
{ key: "all", label: "Todos" },
|
||||
{ key: "sent", label: "Enviados" },
|
||||
@@ -97,8 +105,10 @@ export function NotificacionesServicios() {
|
||||
view: "all" | "sent" | "failed" | "skipped";
|
||||
}>({ view: "all" });
|
||||
const [logPage, setLogPage] = useState(1);
|
||||
const [busy, setBusy] = useState<JobKind | null>(null);
|
||||
const [lastResult, setLastResult] = useState<NotificationJobResponse | null>(null);
|
||||
const [busy, setBusy] = useState<JobKind | "all" | null>(null);
|
||||
const [lastResult, setLastResult] = useState<
|
||||
NotificationJobResponse | NotificationRunAllResponse | null
|
||||
>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
@@ -146,6 +156,30 @@ export function NotificacionesServicios() {
|
||||
[allowed, flags, refresh],
|
||||
);
|
||||
|
||||
// "Ejecutar todos" — one POST, the API runs the four jobs sequentially with
|
||||
// the same flags. Confirmation only matters when debug is off, since that
|
||||
// is the case where real customers receive mail.
|
||||
const runAll = useCallback(async () => {
|
||||
if (!allowed) return;
|
||||
if (!flags.debug) {
|
||||
const ok = window.confirm(
|
||||
"debug está desactivado: los cuatro envíos irán a los correos reales de los clientes. ¿Ejecutar todos?",
|
||||
);
|
||||
if (!ok) return;
|
||||
}
|
||||
setBusy("all");
|
||||
setError(null);
|
||||
try {
|
||||
const res = await runAllNotifications(flags);
|
||||
setLastResult(res);
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e));
|
||||
} finally {
|
||||
setBusy(null);
|
||||
}
|
||||
}, [allowed, flags, refresh]);
|
||||
|
||||
return (
|
||||
<div style={{ display: "grid", gap: 20 }}>
|
||||
<p className="muted" style={{ maxWidth: 760, margin: 0 }}>
|
||||
@@ -221,6 +255,32 @@ export function NotificacionesServicios() {
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
flexWrap: "wrap",
|
||||
marginTop: 16,
|
||||
paddingTop: 16,
|
||||
borderTop: "1px solid var(--line)",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary btn-sm"
|
||||
disabled={!allowed || busy !== null}
|
||||
onClick={() => void runAll()}
|
||||
>
|
||||
{busy === "all" ? "Ejecutando todos…" : "Ejecutar todos"}
|
||||
</button>
|
||||
<span className="muted small">
|
||||
Dispara los cuatro envíos en orden (pagos pendientes, confirmación
|
||||
de pago, estado de cuenta, fideicomiso) con estos mismos flags. Si
|
||||
uno falla, los demás continúan.
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
@@ -316,6 +376,33 @@ export function NotificacionesServicios() {
|
||||
{lastResult && (
|
||||
<section className="card" style={{ padding: 20 }}>
|
||||
<h2 className="section-title">Última respuesta</h2>
|
||||
{lastResult.type === "RUN_ALL" && (
|
||||
<ul
|
||||
className="small"
|
||||
style={{ marginTop: 10, marginBottom: 0, paddingLeft: 18 }}
|
||||
>
|
||||
<li>
|
||||
Totales: enviados {lastResult.sent} · omitidos{" "}
|
||||
{lastResult.skipped} · fallidos {lastResult.failed}
|
||||
{lastResult.errors > 0 && ` · jobs con error ${lastResult.errors}`}
|
||||
</li>
|
||||
{lastResult.jobs.map((j) => (
|
||||
<li key={j.kind}>
|
||||
{JOB_TITLES[j.kind]}:{" "}
|
||||
{j.ok && j.result ? (
|
||||
<>
|
||||
enviados {j.result.sent} · omitidos {j.result.skipped} ·
|
||||
fallidos {j.result.failed}
|
||||
</>
|
||||
) : (
|
||||
<span style={{ color: "var(--negative)" }}>
|
||||
error — {j.error}
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<pre
|
||||
className="mono"
|
||||
style={{
|
||||
|
||||
@@ -1125,6 +1125,38 @@ export function runTrustConfirmation(
|
||||
});
|
||||
}
|
||||
|
||||
export type NotificationJobKind = "outstanding" | "payment" | "account" | "trust";
|
||||
|
||||
export interface NotificationRunAllJobResult {
|
||||
kind: NotificationJobKind;
|
||||
ok: boolean;
|
||||
result?: NotificationJobResponse;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/** Aggregate response of the "Ejecutar todos" sweep. */
|
||||
export interface NotificationRunAllResponse {
|
||||
request: "success";
|
||||
notificationType: "runAllNotifications";
|
||||
statusCode: 200;
|
||||
debug: boolean;
|
||||
sent: number;
|
||||
skipped: number;
|
||||
failed: number;
|
||||
errors: number;
|
||||
jobs: NotificationRunAllJobResult[];
|
||||
type: "RUN_ALL";
|
||||
}
|
||||
|
||||
export function runAllNotifications(
|
||||
flags: NotificationFlags = {},
|
||||
): Promise<NotificationRunAllResponse> {
|
||||
return apiFetch<NotificationRunAllResponse>("/notifications/run-all", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(flags),
|
||||
});
|
||||
}
|
||||
|
||||
export interface NotificationLogQuery {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
|
||||
Reference in New Issue
Block a user