/** * Email ricevuta recesso - layout FISSO (stile Shopify), con solo alcuni TESTI * editabili dal merchant: oggetto, introduzione, nota opzionale. Le parti legali * (dichiarazione, timestamp, avviso supporto durevole art. 54-bis), la struttura * e il footer sono fissi -> sempre conforme e non rompibile. * * Modulo PURO (niente node/server): usato dal mailer e dall'anteprima admin. * Segnaposto ammessi nei testi: {{customerName}} {{orderName}} {{shopName}}. */ export interface EmailVars { shopName: string; shopUrl: string; orderName: string; orderUrl: string; customerName: string; transmittedAt: string; statementText: string; } // Segnaposto usabili nei campi di testo editabili. export const TEXT_PLACEHOLDERS = ["customerName", "orderName", "shopName"] as const; export const DEFAULT_SUBJECT = "{{shopName}} - Ricevuta della tua richiesta di recesso - Ordine {{orderName}}"; export const DEFAULT_INTRO = "Gentile {{customerName}},\n\nconfermiamo di aver ricevuto la tua dichiarazione di recesso relativa all'ordine {{orderName}}."; export const DEFAULT_NOTE = ""; function escHtml(s: string): string { return String(s) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function nl2br(s: string): string { return s.replace(/\n/g, ""); } /** Sostituzione segnaposto in testo semplice (valori grezzi). Per l'oggetto. */ function substPlain(tpl: string, vars: EmailVars): string { return tpl.replace(/\{\{\s*(\w+)\s*\}\}/g, (_m, k: string) => String((vars as unknown as Record)[k] ?? ""), ); } /** Testo del merchant -> HTML sicuro: escape del letterale + segnaposto (valori escaped) + a-capo. */ function richText(tpl: string, vars: EmailVars): string { const escaped = escHtml(tpl); const withVars = escaped.replace(/\{\{\s*(\w+)\s*\}\}/g, (_m, k: string) => escHtml(String((vars as unknown as Record)[k] ?? "")), ); return nl2br(withVars); } export function renderSubject(subjectTpl: string | null | undefined, vars: EmailVars): string { const tpl = (subjectTpl && subjectTpl.trim()) || DEFAULT_SUBJECT; return substPlain(tpl, vars).trim() || substPlain(DEFAULT_SUBJECT, vars); } // Testi del riquadro operativo (A6-bis) - editabili dal merchant, con segnaposto. export const OP_PLACEHOLDERS = [ "returnAddress", "returnCost", "orderState", "customerName", "orderName", "shopName", ] as const; export const DEFAULT_OP_UNFULFILLED = "Il tuo ordine non risultava ancora spedito: procederemo all'annullamento e al rimborso. Non devi restituire nulla."; export const DEFAULT_OP_SHIPPED = "Il prodotto risulta {{orderState}}. Per ottenere il rimborso, restituisci la merce integra a: {{returnAddress}}. Le spese di restituzione sono {{returnCost}}. Il rimborso sarĂ disposto dopo il rientro della merce."; export interface OperationalConfig { state: "unfulfilled" | "shipped" | "delivered"; textUnfulfilled?: string | null; textShipped?: string | null; returnAddress?: string | null; atCustomerExpense: boolean; } /** Blocco operativo per stato ordine (A6-bis): testo editabile + segnaposto. */ function renderOperationalBlock(op: OperationalConfig, vars: EmailVars): string { const tpl = op.state === "unfulfilled" ? (op.textUnfulfilled && op.textUnfulfilled.trim()) || DEFAULT_OP_UNFULFILLED : (op.textShipped && op.textShipped.trim()) || DEFAULT_OP_SHIPPED; const map: Record = { returnAddress: op.returnAddress && op.returnAddress.trim() ? op.returnAddress.trim() : "l'indirizzo che ti comunicheremo", returnCost: op.atCustomerExpense ? "a tuo carico" : "a nostro carico", orderState: op.state === "delivered" ? "consegnato" : "spedito", customerName: vars.customerName, orderName: vars.orderName, shopName: vars.shopName, }; const withVars = escHtml(tpl).replace( /\{\{\s*(\w+)\s*\}\}/g, (_m, k: string) => escHtml(map[k] ?? ""), ); const inner = nl2br(withVars); return ` ${inner} `; } /** Anteprima del riquadro operativo per un dato stato (per l'admin). */ export function renderOperationalPreview( op: OperationalConfig, vars: EmailVars, ): string { return renderOperationalBlock(op, vars); } /** Corpo HTML fisso con intro/nota editabili inseriti. */ export function renderReceiptHtml( vars: EmailVars, introTpl: string | null | undefined, noteTpl: string | null | undefined, operational?: OperationalConfig | null, ): string { const intro = richText((introTpl && introTpl.trim()) || DEFAULT_INTRO, vars); const noteVal = noteTpl && noteTpl.trim() ? richText(noteTpl, vars) : ""; const note = noteVal ? ` ${noteVal} ` : ""; return ` ${escHtml(vars.shopName)} Ricevuta della richiesta di recesso ${intro} Data e ora di trasmissione: ${escHtml(vars.transmittedAt)} Ordine: ${escHtml(vars.orderName)} Nome: ${escHtml(vars.customerName)} Dichiarazione trasmessa ${escHtml(vars.statementText)} Vedi il tuo ordine Questa comunicazione costituisce la ricevuta su supporto durevole ai sensi dell'art. 54-bis del Codice del Consumo. La data e l'ora indicate attestano il momento della trasmissione. ${operational ? renderOperationalBlock(operational, vars) : ""} ${note} Ti invieremo separatamente le istruzioni per l'eventuale reso e i tempi di rimborso. Questa funzione e' aggiuntiva: restano validi il modulo tipo (Allegato I, parte B) e la dichiarazione via email. ${escHtml(vars.shopName)} `; } /** Dati fittizi per l'anteprima admin. */ export const SAMPLE_VARS: EmailVars = { shopName: "Il tuo negozio", shopUrl: "https://esempio.myshopify.com", orderName: "#1001", orderUrl: "https://esempio.myshopify.com/orders/xyz", customerName: "Mario Rossi", transmittedAt: "07/07/2026, 11:32:07 CEST", statementText: "Con la presente comunico il recesso dal contratto di vendita relativo all'ordine #1001.", };
${intro}
Questa comunicazione costituisce la ricevuta su supporto durevole ai sensi dell'art. 54-bis del Codice del Consumo. La data e l'ora indicate attestano il momento della trasmissione.
Ti invieremo separatamente le istruzioni per l'eventuale reso e i tempi di rimborso.
Questa funzione e' aggiuntiva: restano validi il modulo tipo (Allegato I, parte B) e la dichiarazione via email.
${escHtml(vars.shopName)}