92 lines
3.2 KiB
JavaScript
92 lines
3.2 KiB
JavaScript
// backend/utils/smsService.js
|
||
// Centralised Twilio helper + DB helpers for the Reminders feature.
|
||
// Now *also* writes back status → reminders.status and sent_at so the
|
||
// cron‑job doesn’t need its own UPDATE logic.
|
||
|
||
import twilio from 'twilio';
|
||
import { decrypt } from '../shared/crypto/encryption.js';
|
||
import { v4 as uuid } from 'uuid';
|
||
import db from '../config/mysqlPool.js';
|
||
|
||
const {
|
||
TWILIO_ACCOUNT_SID,
|
||
TWILIO_AUTH_TOKEN,
|
||
TWILIO_MESSAGING_SERVICE_SID
|
||
} = process.env;
|
||
|
||
if (!TWILIO_ACCOUNT_SID || !TWILIO_AUTH_TOKEN || !TWILIO_MESSAGING_SERVICE_SID) {
|
||
throw new Error('Twilio env vars missing; check env or PM2 config');
|
||
}
|
||
|
||
const client = twilio(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN);
|
||
|
||
/* ────────────────────────────────────────────────────────────── *
|
||
Immediate send + status update
|
||
------------------------------------------------------------------ */
|
||
export async function sendSMS ({ reminderId = null, to, body }) {
|
||
try {
|
||
// decrypt-at-send (DB stores encrypted)
|
||
const toPlain = typeof to === 'string' && to.startsWith('gcm:') ? decrypt(to) : to;
|
||
const bodyPlain = typeof body === 'string' && body.startsWith('gcm:') ? decrypt(body) : body;
|
||
// normalize to E.164
|
||
const toE164 = (() => {
|
||
const s = String(toPlain || '').trim();
|
||
if (!s) return s;
|
||
if (s.startsWith('+')) return s.replace(/[^\d+]/g, '');
|
||
return '+' + s.replace(/\D/g, '');
|
||
})();
|
||
const msg = await client.messages.create({
|
||
to: toE164,
|
||
body: bodyPlain,
|
||
messagingServiceSid: TWILIO_MESSAGING_SERVICE_SID
|
||
});
|
||
|
||
// Mark success if we were called from reminderCron
|
||
if (reminderId) {
|
||
await db.execute(
|
||
`UPDATE reminders
|
||
SET status = 'sent',
|
||
sent_at = UTC_TIMESTAMP(),
|
||
twilio_sid = ?
|
||
WHERE id = ?`,
|
||
[msg.sid, reminderId]
|
||
);
|
||
}
|
||
|
||
return msg;
|
||
} catch (err) {
|
||
// Persist failure so we don’t keep retrying blindly
|
||
if (reminderId) {
|
||
await db.execute(
|
||
`UPDATE reminders
|
||
SET status = 'failed',
|
||
sent_at = UTC_TIMESTAMP(),
|
||
error_code = ?,
|
||
error_message = ?
|
||
WHERE id = ?`,
|
||
[err.code || null, err.message || null, reminderId]
|
||
);
|
||
}
|
||
throw err; // propagate so cron can log
|
||
}
|
||
}
|
||
|
||
/* ────────────────────────────────────────────────────────────── *
|
||
Persist a *future* reminder row
|
||
------------------------------------------------------------------ */
|
||
export async function createReminder ({ userId, phone, body, sendAtUtc }) {
|
||
const id = uuid();
|
||
const mysqlDateTime = new Date(sendAtUtc)
|
||
.toISOString()
|
||
.slice(0, 19) // 2025-06-17T22:00:00
|
||
.replace('T', ' '); // 2025-06-17 22:00:00
|
||
|
||
await db.execute(
|
||
`INSERT INTO reminders (id, user_id, phone_e164, message_body, send_at_utc)
|
||
VALUES (?,?,?,?,?)`,
|
||
[id, userId, phone, body.slice(0, 320), mysqlDateTime]
|
||
);
|
||
|
||
return id;
|
||
}
|