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:
2026-08-02 02:04:14 -07:00
parent 5e9cb12fba
commit a52e59cbc5
21 changed files with 3128 additions and 4 deletions
+125
View File
@@ -0,0 +1,125 @@
import {
renderAccountStatus,
renderOutstanding,
renderPaymentConfirm,
renderTrustConfirm,
} from "./render";
/**
* Render-level tests. The legacy PHP scripts fetched these bodies by URL;
* we render server-side and inline. The tests assert the *shape* of each
* body — account id, name, subject, balance/tipo, color band — because
* the customer base has been seeing these letters for years and a visual
* regression costs trust faster than any backend change does.
*/
describe("renderOutstanding", () => {
it("includes the customer id, name, total, and per-row table", () => {
const html = renderOutstanding({
customerId: "C-001",
customerName: "Acme & Co.",
total: "1234.50",
rows: [
{
date: "2026-07-01",
reference: "INV-1",
period: "Jul-26",
type: "CHECK",
amount: "-500.00",
balance: "-500.00",
},
{
date: "2026-07-15",
reference: "INV-2",
period: "Jul-26",
type: "CASH",
amount: "-734.50",
balance: "-1234.50",
},
],
year: 2026,
});
expect(html).toContain("Acme & Co.");
expect(html).toContain("ACCOUNT #C-001");
expect(html).toContain("$ 1,234.50");
expect(html).toContain("INV-1");
expect(html).toContain("CHECK");
expect(html).toContain("IF YOU ALREADY SENT THE CHECK");
});
it("escapes HTML in the customer name", () => {
const html = renderOutstanding({
customerId: "x",
customerName: "<script>alert(1)</script>",
total: "0.00",
rows: [],
year: 2026,
});
expect(html).not.toContain("<script>alert(1)</script>");
expect(html).toContain("&lt;script&gt;alert(1)&lt;/script&gt;");
});
});
describe("renderPaymentConfirm", () => {
it("uses the transaction type in the heading and the amount in the body", () => {
const html = renderPaymentConfirm({
customerId: "C-002",
customerName: "Bob",
typeOfTrx: "CHECK DEPOSIT",
reference: "DEP-99",
amount: "500.00",
year: 2026,
});
expect(html).toContain("CHECK DEPOSIT CONFIRMATION");
expect(html).toContain("HI, Bob");
expect(html).toContain("REFER# DEP-99");
expect(html).toContain("$ 500.00");
});
});
describe("renderAccountStatus", () => {
it("uses the yellow band and the under-minimum phrasing for level=0", () => {
const html = renderAccountStatus({
customerId: "C-003",
customerName: "Carol",
level: 0,
balance: "10.00",
tipo: "40.00",
year: 2026,
});
expect(html).toContain("#88D5EE");
expect(html).toContain("under our minimum");
expect(html).toContain("Carol");
expect(html).toContain("$ 10.00");
expect(html).toContain("$ 40.00");
});
it("uses the red band and the rush phrasing for level=1", () => {
const html = renderAccountStatus({
customerId: "C-003",
customerName: "Carol",
level: 1,
balance: "-25.50",
tipo: "25.50",
year: 2026,
});
expect(html).toContain("#FF8D71");
expect(html).toContain("overdrawn");
expect(html).toContain("reactivate your payments");
expect(html).toContain("$ 25.50");
});
});
describe("renderTrustConfirm", () => {
it("labels the trust annual fee and quotes the amount", () => {
const html = renderTrustConfirm({
customerId: "C-004",
customerName: "Dan",
amount: "350.00",
year: 2026,
});
expect(html).toContain("Annual Bank Fee Payment Confirmation");
expect(html).toContain("$ 350.00");
expect(html).toContain("Most banks always request");
});
});