datewise/app/services/notifications.server.ts
metatroncubeswdev e56182e0e3 chore: initial Datewise app (rebranded from SlotSure prototype)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:06:49 -04:00

82 lines
3.1 KiB
TypeScript

/**
* 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<void> {
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} <reminders@${domain}>`;
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) }] }],
},
}),
});
}