256 lines
10 KiB
JavaScript
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
|