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.
255 lines
10 KiB
TypeScript
255 lines
10 KiB
TypeScript
/**
|
|
* HTML body renderers for the four notification jobs. These are the modern
|
|
* in-process equivalent of the legacy `getXxxForEmail.php` files the PHP
|
|
* scripts `fetch()`ed by URL. Rendering server-side and inlining the body
|
|
* in the response keeps a single SES MessageId tied to one frozen HTML
|
|
* snapshot (vs. the legacy flow, where the URL kept re-rendering with
|
|
* whatever the database looked like at click time).
|
|
*
|
|
* The visual style mirrors the legacy PHP templates where it makes sense
|
|
* (the office's customer base has been seeing these letters for years;
|
|
* gratuitous redesign costs trust). The body shell, table layout and the
|
|
* canonical contact block are preserved verbatim. English copy because the
|
|
* legacy letters were English; switching to Spanish is a future decision
|
|
* (see INSURANCE_FEATURES_SPEC §1.6 "Spanish or English body?").
|
|
*/
|
|
|
|
const HEAD = `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
|
<head>
|
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
|
<title>{title}</title>
|
|
</head>`;
|
|
|
|
const FOOT_CONTACT = `<p>If you have any questions regarding this notice please contact us at:
|
|
Tel. 011 52 (661) 612 - 1295 Fax. (661) 612 - 1285
|
|
For any type of a 24 Hrs. emergencies: please dial 52 (664) 304 - 7778 |
|
|
<a href="mailto:jorge@jorgecuadros.com">jorge@jorgecuadros.com</a> |
|
|
<a href="https://www.jorgecuadros.com/contactus.php">Contact Us Form</a></p>`;
|
|
|
|
const SIGNED = (year: number) => `<center><span class="small">This message has been generated by the Jorge Cuadros & Assoc. Information Server.<br />Copyright ${year} <a href="http://www.freakma.net/">Developed by FreaKmA.Net</a></span></center>`;
|
|
|
|
const esc = (s: string | null | undefined): string =>
|
|
String(s ?? "")
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """);
|
|
|
|
const usd = (n: number | string | null | undefined): string => {
|
|
if (n === null || n === undefined) return "$ 0.00";
|
|
const v = typeof n === "string" ? Number(n) : n;
|
|
if (!isFinite(v)) return "$ 0.00";
|
|
return `$ ${v.toLocaleString("en-US", {
|
|
minimumFractionDigits: 2,
|
|
maximumFractionDigits: 2,
|
|
})}`;
|
|
};
|
|
|
|
/** Shared shell: a 2-column table that matches the PHP output layout. */
|
|
function shell(opts: {
|
|
title: string;
|
|
bg: string;
|
|
heading: string;
|
|
accountId: string | number;
|
|
accountName: string;
|
|
body: string;
|
|
note?: string;
|
|
statementLink?: string;
|
|
year: number;
|
|
}): string {
|
|
const { title, bg, heading, accountId, accountName, body, note, statementLink, year } = opts;
|
|
const stmt = statementLink ?? "https://my.jorgecuadros.com/";
|
|
return `${HEAD.replace("{title}", esc(title))}
|
|
<body style="background-color:${bg};color:#333;font-family:'Courier New', Courier, monospace;">
|
|
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
|
<tr>
|
|
<td width="43%" style="font-size:20px;font-weight:bold;">${esc(heading)}</td>
|
|
<td width="57%" style="font-size:12px;">Please do not reply to this message. For any Jorge Cuadros & Assoc. customer service inquiries, visit: <a href="https://www.jorgecuadros.com/contactus.php">Customer Support</a></td>
|
|
</tr>
|
|
<tr>
|
|
<td><strong>${esc(accountName)}<br />ACCOUNT #${esc(String(accountId))}</strong></td>
|
|
<td><div align="center"><a href="${esc(stmt)}" target="_blank" style="color:#006699;font-weight:bold">Click Here to View Your Account Statement</a></div></td>
|
|
</tr>
|
|
<tr><td colspan="2"> </td></tr>
|
|
<tr><td colspan="2">${body}</td></tr>
|
|
<tr><td colspan="2"> </td></tr>
|
|
${
|
|
note
|
|
? `<tr><td colspan="2"><h4>${esc(note)}</h4>${FOOT_CONTACT}</td></tr>`
|
|
: `<tr><td colspan="2">${FOOT_CONTACT}</td></tr>`
|
|
}
|
|
<tr><td colspan="2"> </td></tr>
|
|
<tr><td colspan="2">${SIGNED(year)}</td></tr>
|
|
</table>
|
|
</body>
|
|
</html>`;
|
|
}
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Outstanding payments — Job 1 */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
export interface OutstandingRow {
|
|
date: Date | string;
|
|
reference: string | null;
|
|
period: string | null;
|
|
type: string | null;
|
|
/** Signed amount (negative for charges). */
|
|
amount: number | string;
|
|
/** Running balance in the customer's currency, after this row. */
|
|
balance: number | string;
|
|
}
|
|
|
|
export function renderOutstanding(args: {
|
|
customerId: string;
|
|
customerName: string;
|
|
total: number | string;
|
|
rows: OutstandingRow[];
|
|
year: number;
|
|
}): string {
|
|
const rows = args.rows
|
|
.map(
|
|
(r) => `<tr>
|
|
<td>${esc(String(r.date))}</td>
|
|
<td>${esc(r.reference ?? "")}</td>
|
|
<td>${esc(r.period ?? "")}</td>
|
|
<td>${esc(r.type ?? "")}</td>
|
|
<td align="right">${esc(usd(r.amount))}</td>
|
|
<td align="right">${esc(usd(r.balance))}</td>
|
|
</tr>`,
|
|
)
|
|
.join("\n");
|
|
|
|
const body = `<p>This needs your prompt attention in order to avoid any disruption(s):</p>
|
|
<p align="center"><strong><font color="#FF0000">TOTAL OF OUTSTANDING BILLS: ${esc(
|
|
usd(args.total),
|
|
)} PESOS.</font></strong></p>
|
|
<table width="100%" border="0" cellpadding="0" cellspacing="0">
|
|
<tr><th>DATE</th><th>REFER</th><th>PERIOD</th><th>TYPEOFTRX</th><th>CHARGECREDIT</th><th>BALANCE</th></tr>
|
|
${rows}
|
|
</table>`;
|
|
|
|
return shell({
|
|
title: "Outstanding Payments",
|
|
bg: "#9CC",
|
|
heading: "Outstanding Payments",
|
|
accountId: args.customerId,
|
|
accountName: args.customerName,
|
|
body,
|
|
note: "NOTE : IF YOU ALREADY SENT THE CHECK, PLEASE DISREGARD THIS EMAIL",
|
|
year: args.year,
|
|
});
|
|
}
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Payment confirmation — Job 2 */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
export function renderPaymentConfirm(args: {
|
|
customerId: string;
|
|
customerName: string;
|
|
typeOfTrx: string;
|
|
reference: string | null;
|
|
/** The deposited amount (positive number — credits are positive in the
|
|
* unified ledger). */
|
|
amount: number | string;
|
|
year: number;
|
|
}): string {
|
|
const body = `<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
|
<tr>
|
|
<td width="48%" style="font-size:20px;font-weight:bold;">${esc(
|
|
args.typeOfTrx,
|
|
)} CONFIRMATION</td>
|
|
<td width="52%" style="font-size:12px;">Please do not reply to this message. For any Jorge Cuadros & Assoc. customer service inquiries, visit: <a href="https://www.jorgecuadros.com/contactus.php" target="_blank">Customer Support</a></td>
|
|
</tr>
|
|
<tr>
|
|
<td>
|
|
<strong>HI, ${esc(args.customerName)}</strong><br/>
|
|
<strong>ACCOUNT #${esc(args.customerId)}</strong><br/>
|
|
<strong>REFER# ${esc(args.reference ?? "")}</strong>
|
|
</td>
|
|
<td>
|
|
<div align="center" style="padding:20px;">
|
|
<a href="https://my.jorgecuadros.com/" target="_blank" style="color:#006699;font-weight:bold"><em>Click Here to View Your Account Statement</em></a>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr><td colspan="2"> </td></tr>
|
|
<tr><td colspan="2">
|
|
<p>Your account is now current to keep paying your future obligations. If for any reason your next bill is more than what's available; our system will email you our automatic alert requesting more funds. Thank You,</p>
|
|
<p align="center" style="color:#006600;font-weight:bold;">Your deposit was for ${esc(
|
|
usd(args.amount),
|
|
)} PESOS.</p>
|
|
</td></tr>
|
|
<tr><td colspan="2"> </td></tr>
|
|
<tr><td colspan="2"><h4>NOTE : IF YOU ALREADY SENT THE CHECK, PLEASE DISREGARD THIS EMAIL</h4>${FOOT_CONTACT}</td></tr>
|
|
<tr><td colspan="2"> </td></tr>
|
|
<tr><td colspan="2">${SIGNED(args.year)}</td></tr>
|
|
</table>`;
|
|
return `${HEAD.replace("{title}", "Payment Confirmation")}<body>${body}</body></html>`;
|
|
}
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Account status — Job 3 (yellow + red) */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
export function renderAccountStatus(args: {
|
|
customerId: string;
|
|
customerName: string;
|
|
level: 0 | 1; // 0 = yellow (DEBAJO DEL TIPO), 1 = red (EN ROJO)
|
|
balance: number | string;
|
|
/** Amount the customer needs to deposit to clear the threshold. */
|
|
tipo: number | string;
|
|
year: number;
|
|
}): string {
|
|
const isYellow = args.level === 0;
|
|
const body = isYellow
|
|
? `<p>In order to avoid any disruptions please mail or bring ${esc(
|
|
usd(args.tipo),
|
|
)} USD ASAP. As your current Balance ${esc(
|
|
usd(args.balance),
|
|
)} is under our minimum required to run this account.</p>`
|
|
: `<p>Sorry Account is overdrawn and all utility bills are on hold please rush ${esc(
|
|
usd(args.tipo),
|
|
)} USD these funds must be on hand ASAP to reactivate your payments.</p>`;
|
|
return shell({
|
|
title: "Account Alert",
|
|
bg: isYellow ? "#88D5EE" : "#FF8D71",
|
|
heading: "Account Alert",
|
|
accountId: args.customerId,
|
|
accountName: args.customerName,
|
|
body,
|
|
note: "NOTE : PLEASE MAKE YOUR CHECK PAYABLE TO UMC AND ASSOCIATES. IF YOU ALREADY SENT THE CHECK, PLEASE DISREGARD THIS EMAIL.",
|
|
year: args.year,
|
|
});
|
|
}
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Trust payment confirmation — Job 4 */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
export function renderTrustConfirm(args: {
|
|
customerId: string;
|
|
customerName: string;
|
|
/** Annual fee amount posted (positive, in MXN per the PHP). */
|
|
amount: number | string;
|
|
year: number;
|
|
}): string {
|
|
const body = `<p>This automatic notice is to confirm, that your Annual Bank Fee has been paid by, and posted in your account. Thank You,</p>
|
|
<p align="center"><strong>The annual fee was posted for the amount of <font color="#FF0000">${esc(
|
|
usd(args.amount),
|
|
)} PESOS.</font></strong></p>`;
|
|
return shell({
|
|
title: "Trust Payment Confirmation",
|
|
bg: "#C0BEA0",
|
|
heading: "Annual Bank Fee Payment Confirmation",
|
|
accountId: args.customerId,
|
|
accountName: args.customerName,
|
|
body,
|
|
note: "NOTE : Most banks always request to make such payment in advance.",
|
|
statementLink: "https://my.jorgecuadros.com/",
|
|
year: args.year,
|
|
});
|
|
}
|