Files
alrahma_sunday_school_api/apps/api/dist/services/notificationService.js
T
2026-06-11 03:22:12 -04:00

256 lines
10 KiB
JavaScript

"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.sendNotification = sendNotification;
exports.sendTransactionalEmail = sendTransactionalEmail;
const resend_1 = require("resend");
const twilio_1 = __importDefault(require("twilio"));
const firebase_admin_1 = __importDefault(require("firebase-admin"));
const prisma_1 = require("../lib/prisma");
const redis_1 = require("../lib/redis");
const notificationLocalizationService_1 = require("./notificationLocalizationService");
const resendApiKey = process.env.RESEND_API_KEY &&
process.env.RESEND_API_KEY !== 're_...' &&
process.env.RESEND_API_KEY.startsWith('re_')
? process.env.RESEND_API_KEY
: null;
const resend = resendApiKey ? new resend_1.Resend(resendApiKey) : null;
const emailFromAddress = process.env.EMAIL_FROM && process.env.EMAIL_FROM !== 'noreply@example.com'
? process.env.EMAIL_FROM
: null;
const emailFromName = process.env.EMAIL_FROM_NAME && process.env.EMAIL_FROM_NAME !== 'Example App'
? process.env.EMAIL_FROM_NAME
: null;
const smtpHost = process.env.MAIL_HOST;
const smtpPort = Number(process.env.MAIL_PORT ?? 0);
const smtpUser = process.env.MAIL_USERNAME;
const smtpPass = process.env.MAIL_PASSWORD;
const smtpSecure = process.env.MAIL_SCHEME === 'smtps' ||
process.env.MAIL_ENCRYPTION === 'ssl' ||
smtpPort === 465;
const smtpFromAddress = process.env.MAIL_FROM_ADDRESS && !process.env.MAIL_FROM_ADDRESS.includes('${')
? process.env.MAIL_FROM_ADDRESS
: null;
const smtpFromName = process.env.MAIL_FROM_NAME && !process.env.MAIL_FROM_NAME.includes('${')
? process.env.MAIL_FROM_NAME
: null;
let smtpTransport = null;
if (smtpHost && smtpPort && smtpUser && smtpPass) {
try {
const nodemailer = require('nodemailer');
smtpTransport = nodemailer.createTransport({
host: smtpHost,
port: smtpPort,
secure: smtpSecure,
auth: {
user: smtpUser,
pass: smtpPass,
},
});
}
catch (err) {
console.warn('[Notifications] SMTP transport unavailable:', err?.message ?? String(err));
}
}
const twilioClient = process.env.TWILIO_ACCOUNT_SID &&
process.env.TWILIO_AUTH_TOKEN &&
process.env.TWILIO_ACCOUNT_SID !== 'AC...' &&
process.env.TWILIO_AUTH_TOKEN !== 'your-twilio-auth-token'
? (0, twilio_1.default)(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN)
: null;
const firebasePrivateKey = process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n');
const hasFirebaseConfig = !!process.env.FIREBASE_PROJECT_ID &&
!!process.env.FIREBASE_CLIENT_EMAIL &&
!!firebasePrivateKey &&
!firebasePrivateKey.includes('BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY');
if (hasFirebaseConfig && !firebase_admin_1.default.apps.length) {
try {
firebase_admin_1.default.initializeApp({
credential: firebase_admin_1.default.credential.cert({
projectId: process.env.FIREBASE_PROJECT_ID,
privateKey: firebasePrivateKey,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
}),
});
}
catch (err) {
console.warn('[Notifications] Firebase init skipped:', err.message);
}
}
function resolveSmtpReplyTo() {
if (!process.env.MAIL_REPLY_TO_ADDRESS || process.env.MAIL_REPLY_TO_ADDRESS.includes('${')) {
return undefined;
}
if (process.env.MAIL_REPLY_TO_NAME && !process.env.MAIL_REPLY_TO_NAME.includes('${')) {
return `${process.env.MAIL_REPLY_TO_NAME} <${process.env.MAIL_REPLY_TO_ADDRESS}>`;
}
return process.env.MAIL_REPLY_TO_ADDRESS;
}
async function sendEmailWithProviders(opts) {
const errors = [];
if (resend) {
try {
if (!emailFromAddress || !emailFromName) {
throw new Error('Email sender identity is not configured');
}
const { data, error } = await resend.emails.send({
from: `${emailFromName} <${emailFromAddress}>`,
to: opts.to,
subject: opts.subject,
html: opts.html,
text: opts.text,
});
if (error) {
throw new Error(error.message);
}
return {
provider: 'resend',
providerMessageId: data?.id ?? null,
};
}
catch (err) {
errors.push(`Resend: ${err?.message ?? String(err)}`);
console.warn('[Notifications] Resend delivery failed, falling back if possible:', err?.message ?? String(err));
}
}
if (smtpTransport) {
try {
if (!smtpFromAddress) {
throw new Error('SMTP sender identity is not configured');
}
const info = await smtpTransport.sendMail({
from: smtpFromName ? `${smtpFromName} <${smtpFromAddress}>` : smtpFromAddress,
to: opts.to,
subject: opts.subject,
html: opts.html,
text: opts.text,
replyTo: resolveSmtpReplyTo(),
});
return {
provider: 'smtp',
providerMessageId: info.messageId ?? null,
};
}
catch (err) {
errors.push(`SMTP: ${err?.message ?? String(err)}`);
}
}
if (errors.length) {
throw new Error(errors.join(' | '));
}
throw new Error('No email provider is configured');
}
async function sendNotification(opts) {
const results = [];
const resolvedLocale = await (0, notificationLocalizationService_1.resolveNotificationLocale)({
companyId: opts.companyId,
employeeId: opts.employeeId,
renterId: opts.renterId,
billingAccountId: opts.billingAccountId,
locale: opts.locale,
});
for (const channel of opts.channels) {
try {
const rendered = opts.templateKey
? await (0, notificationLocalizationService_1.resolveNotificationTemplate)({
templateKey: opts.templateKey,
channel,
locale: resolvedLocale,
variables: opts.templateVariables,
})
: null;
const title = rendered?.subject ?? opts.title;
const body = rendered?.body ?? opts.body;
if (!title || !body) {
throw new Error(`Notification content is incomplete for channel ${channel}`);
}
const notification = await prisma_1.prisma.notification.create({
data: {
type: opts.type,
title,
body,
data: (opts.data ?? {}),
channel,
templateKey: rendered?.templateKey ?? opts.templateKey ?? null,
locale: rendered?.locale ?? resolvedLocale,
status: 'PENDING',
companyId: opts.companyId ?? null,
employeeId: opts.employeeId ?? null,
renterId: opts.renterId ?? null,
},
});
let providerMessageId = null;
let success = false;
if (channel === 'EMAIL' && opts.email) {
const emailResult = await sendEmailWithProviders({
to: opts.email,
subject: title,
html: (0, notificationLocalizationService_1.renderLocalizedEmailHtml)(body, rendered?.locale ?? resolvedLocale),
text: body,
});
providerMessageId = emailResult.providerMessageId;
success = true;
}
if (channel === 'SMS' && opts.phone) {
if (!twilioClient)
throw new Error('Twilio is not configured');
const msg = await twilioClient.messages.create({
body,
from: process.env.TWILIO_PHONE_NUMBER,
to: opts.phone,
});
providerMessageId = msg.sid;
success = true;
}
if (channel === 'WHATSAPP' && opts.phone) {
if (!twilioClient)
throw new Error('Twilio is not configured');
const msg = await twilioClient.messages.create({
body,
from: `whatsapp:${process.env.TWILIO_WHATSAPP_NUMBER}`,
to: `whatsapp:${opts.phone}`,
});
providerMessageId = msg.sid;
success = true;
}
if (channel === 'PUSH' && opts.fcmToken) {
if (!firebase_admin_1.default.apps.length)
throw new Error('Firebase is not configured');
const response = await firebase_admin_1.default.messaging().send({
token: opts.fcmToken,
notification: { title, body },
data: Object.fromEntries(Object.entries(opts.data ?? {}).map(([k, v]) => [k, String(v)])),
});
providerMessageId = response;
success = true;
}
if (channel === 'IN_APP') {
// Emit via Socket.io through Redis pub/sub
const targetId = opts.employeeId ?? opts.renterId;
if (targetId) {
await redis_1.redis.publish(`notifications:${targetId}`, JSON.stringify({ ...notification, status: 'DELIVERED' }));
}
success = true;
}
await prisma_1.prisma.notification.update({
where: { id: notification.id },
data: {
status: success ? 'SENT' : 'FAILED',
sentAt: success ? new Date() : null,
providerMessageId,
},
});
results.push({ channel, success });
}
catch (err) {
results.push({ channel, success: false, error: err.message });
}
}
return results;
}
async function sendTransactionalEmail(opts) {
await sendEmailWithProviders(opts);
}
//# sourceMappingURL=notificationService.js.map