"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