queue-med/server/services/sms.ts

73 lines
2.1 KiB
TypeScript

/**
* SMS notification service using Twilio.
*
* Reads TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_PHONE_NUMBER from env.
* If any of those are missing, sendSms is a no-op that returns
* { success: false } and logs a warning. This keeps the app running in
* environments where Twilio has not been provisioned yet.
*/
import twilio, { type Twilio } from "twilio";
import { childLogger } from "../_core/logger.js";
const log = childLogger("sms");
let cachedClient: Twilio | null = null;
export function isSmsConfigured(): boolean {
return Boolean(
process.env.TWILIO_ACCOUNT_SID &&
process.env.TWILIO_AUTH_TOKEN &&
process.env.TWILIO_PHONE_NUMBER
);
}
function getClient(): Twilio | null {
if (cachedClient) return cachedClient;
if (!isSmsConfigured()) return null;
try {
cachedClient = twilio(
process.env.TWILIO_ACCOUNT_SID!,
process.env.TWILIO_AUTH_TOKEN!
);
return cachedClient;
} catch (err) {
log.error({ err }, "failed to initialise Twilio client");
return null;
}
}
function normalizePhone(input: string): string {
const trimmed = input.trim();
if (trimmed.startsWith("+")) return trimmed;
const digits = trimmed.replace(/[^0-9]/g, "");
return `+${digits}`;
}
export async function sendSms(
to: string,
body: string
): Promise<{ success: boolean; messageId?: string; error?: string }> {
const client = getClient();
if (!client) {
log.warn({ to: to.slice(0, 4) + "***" }, "Twilio not configured — SMS skipped");
return { success: false, error: "Twilio non configuré" };
}
const from = process.env.TWILIO_PHONE_NUMBER!;
const normalized = normalizePhone(to);
try {
const msg = await client.messages.create({
to: normalized,
from,
body,
});
log.info({ messageId: msg.sid, to: normalized.slice(0, 4) + "***" }, "SMS sent");
return { success: true, messageId: msg.sid };
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
log.error({ err, to: normalized.slice(0, 4) + "***" }, "SMS send failed");
return { success: false, error: errorMessage };
}
}