/** * Outbound messages. Email goes through the app's Resend account (reply-to = merchant); * SMS and WhatsApp use the merchant's own Twilio / Meta Cloud API credentials so * messaging costs stay on their account. * * WhatsApp business-initiated messages must use a pre-approved template. Datewise sends * the merchant's chosen template with the rendered reminder text as body parameter {{1}}. */ import { decrypt } from "./crypto.server"; import type { AppSettings } from "./settings"; export type Channel = "email" | "sms" | "whatsapp"; export interface OutboundMessage { channel: Channel; to: string; subject?: string; body: string; } export class NotConfigured extends Error {} async function post(url: string, init: RequestInit) { const ctrl = new AbortController(); const t = setTimeout(() => ctrl.abort(), 8000); try { const r = await fetch(url, { ...init, signal: ctrl.signal }); if (!r.ok) throw new Error(`${new URL(url).host} ${r.status}: ${(await r.text()).slice(0, 300)}`); return r; } finally { clearTimeout(t); } } export async function send(settings: AppSettings, shopName: string, msg: OutboundMessage): Promise { if (msg.channel === "email") { const key = process.env.RESEND_API_KEY; const domain = process.env.EMAIL_FROM_DOMAIN; if (!key || !domain) throw new NotConfigured("Email sending is not configured on the server"); const from = `${settings.messaging.emailFromName || shopName} `; await post("https://api.resend.com/emails", { method: "POST", headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" }, body: JSON.stringify({ from, to: [msg.to], reply_to: settings.messaging.replyTo || undefined, subject: msg.subject ?? `Your order with ${shopName}`, text: msg.body, }), }); return; } if (msg.channel === "sms") { const t = settings.messaging.twilio; if (!t) throw new NotConfigured("Connect Twilio in Settings to send SMS"); const auth = Buffer.from(`${t.accountSid}:${decrypt(t.authTokenEnc)}`).toString("base64"); await post(`https://api.twilio.com/2010-04-01/Accounts/${t.accountSid}/Messages.json`, { method: "POST", headers: { Authorization: `Basic ${auth}`, "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ To: msg.to, From: t.from, Body: msg.body }), }); return; } const w = settings.messaging.whatsapp; if (!w) throw new NotConfigured("Connect WhatsApp in Settings to send WhatsApp messages"); await post(`https://graph.facebook.com/v21.0/${w.phoneNumberId}/messages`, { method: "POST", headers: { Authorization: `Bearer ${decrypt(w.tokenEnc)}`, "Content-Type": "application/json" }, body: JSON.stringify({ messaging_product: "whatsapp", to: msg.to.replace(/[^\d]/g, ""), type: "template", template: { name: w.templateName, language: { code: w.templateLang }, components: [{ type: "body", parameters: [{ type: "text", text: msg.body.slice(0, 1000) }] }], }, }), }); }