Files
pcrt-legal-return/app/app/lib/mailer.server.ts

187 lines
6.8 KiB
TypeScript

/**
* Invio ricevuta di recesso su supporto durevole (A4 - Art. 54-bis).
*
* Il corpo/oggetto sono TEMPLATE editabili dal merchant (HTML + segnaposto), con
* default in ./emailTemplate. Qui sostituiamo i segnaposto coi valori reali
* (dinamici da Shopify) e inviamo. La validazione compliance (segnaposto
* obbligatori) avviene al salvataggio lato admin.
*
* Transport via env: SMTP_HOST, SMTP_PORT(=587), SMTP_USER?, SMTP_PASS?,
* SMTP_SECURE("true"/"false"), MAIL_FROM. DEV: Mailpit localhost:1025.
*/
import nodemailer from "nodemailer";
import {
renderReceiptHtml,
renderSubject,
type EmailVars,
type OperationalConfig,
} from "./emailTemplate";
function buildTransport() {
const host = process.env.SMTP_HOST;
if (!host) return null;
const port = Number(process.env.SMTP_PORT ?? 587);
const user = process.env.SMTP_USER;
return nodemailer.createTransport({
host,
port,
secure: process.env.SMTP_SECURE === "true",
auth: user ? { user, pass: process.env.SMTP_PASS ?? "" } : undefined,
connectionTimeout: 10_000,
greetingTimeout: 10_000,
socketTimeout: 10_000,
});
}
/** Versione testo grezza dell'HTML (fallback per client senza HTML). */
function htmlToText(html: string): string {
return html
.replace(/<style[\s\S]*?<\/style>/gi, "")
.replace(/<(?:\/p|\/div|\/tr|\/h1|\/h2|\/li|br\s*\/?)>/gi, "\n")
.replace(/<[^>]+>/g, "")
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, '"')
.replace(/&middot;/g, "·")
.replace(/&nbsp;/g, " ")
.replace(/[ \t]+\n/g, "\n")
.replace(/\n{3,}/g, "\n\n")
.trim();
}
export type ReceiptResult =
| { ok: true; messageId: string }
| { ok: false; error: string };
/**
* Invia la ricevuta. Non lancia mai: cattura e ritorna l'esito.
* `subjectTemplate`/`bodyTemplate` dai Settings (null = default). `vars` sono i
* valori reali (shop/ordine/cliente), gia' pronti dal chiamante.
*/
export async function sendWithdrawalReceipt(params: {
to: string;
vars: EmailVars;
subject?: string | null;
intro?: string | null;
note?: string | null;
operational?: OperationalConfig | null;
}): Promise<ReceiptResult> {
const transport = buildTransport();
if (!transport) {
return { ok: false, error: "SMTP non configurato (SMTP_HOST mancante)" };
}
const subject = renderSubject(params.subject, params.vars);
const html = renderReceiptHtml(
params.vars,
params.intro,
params.note,
params.operational,
);
const text = htmlToText(html);
try {
const info = await transport.sendMail({
from: process.env.MAIL_FROM ?? "no-reply@localhost",
to: params.to,
subject,
text,
html,
});
return { ok: true, messageId: info.messageId };
} catch (e) {
return {
ok: false,
error: e instanceof Error ? e.message : "invio ricevuta fallito",
};
}
}
function escM(s: string): string {
return String(s)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
/**
* Notifica al merchant di una nuova richiesta di recesso. Non lancia mai.
* `returnStatus` riflette l'esito dell'integrazione Resi (per suggerire l'azione).
*/
export async function sendMerchantNotification(params: {
to: string;
orderName: string;
customerName: string;
customerEmail: string;
orderUrl: string;
transmittedAt: string;
returnStatus: "created" | "no_returnable" | "error";
}): Promise<ReceiptResult> {
const transport = buildTransport();
if (!transport) {
return { ok: false, error: "SMTP non configurato (SMTP_HOST mancante)" };
}
const actionLine =
params.returnStatus === "created"
? "E' stato creato un reso nell'ordine: gestiscilo dalla pagina dell'ordine."
: params.returnStatus === "no_returnable"
? "L'ordine non risulta evaso: valuta annullamento o rimborso."
: "Reso non creato automaticamente: verifica manualmente l'ordine.";
const orderBtn = /^https?:\/\//i.test(params.orderUrl)
? `<p style="margin:16px 0 0;"><a href="${escM(params.orderUrl)}" style="display:inline-block;padding:10px 18px;background:#1a1a1a;color:#fff;text-decoration:none;border-radius:8px;font-size:14px;font-weight:600;">Apri l'ordine</a></p>`
: "";
const subject = `Nuovo recesso - Ordine ${params.orderName}`;
const html = `<!doctype html><html lang="it"><head><meta charset="utf-8"></head>
<body style="margin:0;padding:24px;background:#f4f4f5;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;color:#1a1a1a;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td align="center">
<table role="presentation" width="520" cellpadding="0" cellspacing="0" style="width:100%;max-width:520px;background:#fff;border:1px solid #e5e5e5;border-radius:12px;">
<tr><td style="padding:24px 28px;">
<h1 style="margin:0 0 12px;font-size:19px;">Nuova richiesta di recesso</h1>
<p style="margin:0 0 14px;font-size:14px;line-height:1.6;color:#3a3a3a;">Un cliente ha esercitato il diritto di recesso.</p>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f6f6f7;border-radius:8px;"><tr><td style="padding:14px 16px;font-size:14px;line-height:1.9;color:#444;">
<div><span style="color:#777;">Ordine:</span> <strong>${escM(params.orderName)}</strong></div>
<div><span style="color:#777;">Cliente:</span> <strong>${escM(params.customerName)}</strong></div>
<div><span style="color:#777;">Email:</span> ${escM(params.customerEmail)}</div>
<div><span style="color:#777;">Trasmesso:</span> ${escM(params.transmittedAt)}</div>
</td></tr></table>
<p style="margin:16px 0 0;font-size:14px;line-height:1.6;color:#3a3a3a;">${actionLine}</p>
<p style="margin:10px 0 0;font-size:12.5px;line-height:1.6;color:#8a8a8a;">Promemoria: disponi il rimborso entro 14 giorni dalla richiesta (art. 56 Cod. Consumo). Puoi trattenerlo fino alla riconsegna della merce o alla prova di spedizione da parte del cliente.</p>
${orderBtn}
</td></tr></table>
</td></tr></table>
</body></html>`;
const text = [
"Nuova richiesta di recesso",
`Ordine: ${params.orderName}`,
`Cliente: ${params.customerName} (${params.customerEmail})`,
`Trasmesso: ${params.transmittedAt}`,
actionLine,
"Promemoria: rimborso entro 14 giorni dalla richiesta (art. 56); puoi trattenere fino alla riconsegna della merce o alla prova di spedizione.",
/^https?:\/\//i.test(params.orderUrl) ? params.orderUrl : "",
]
.filter(Boolean)
.join("\n");
try {
const info = await transport.sendMail({
from: process.env.MAIL_FROM ?? "no-reply@localhost",
to: params.to,
subject,
text,
html,
});
return { ok: true, messageId: info.messageId };
} catch (e) {
return {
ok: false,
error: e instanceof Error ? e.message : "invio notifica fallito",
};
}
}