@ideal-photography/shared
Version:
Shared MongoDB and utility logic for Ideal Photography PWAs: users, products, services, bookings, orders/cart, galleries, reviews, notifications, campaigns, settings, audit logs, minimart items/orders, and push notification subscriptions.
1,211 lines (1,105 loc) • 76.8 kB
JavaScript
import webpush from 'web-push';
import { models } from '../mongoDB/index.js';
import NotificationService from '../services/NotificationService.js';
// Configure web-push if VAPID keys are available
const vapidPublicKey = process.env.VAPID_PUBLIC_KEY;
const vapidPrivateKey = process.env.VAPID_PRIVATE_KEY;
const vapidSubject = process.env.VAPID_SUBJECT || 'mailto:support@ideasmediacompany.com';
if (vapidPublicKey && vapidPrivateKey) {
webpush.setVapidDetails(vapidSubject, vapidPublicKey, vapidPrivateKey);
}
/**
* Send push notification to specific user
*/
export async function sendNotificationToUser(userId, notification) {
try {
// NOTE: Notification should already be created by the route handler
// This function only sends push notifications, not create DB records
if (!vapidPublicKey || !vapidPrivateKey) {
console.warn('VAPID keys not configured, skipping push notification');
return { success: false, error: 'VAPID not configured' };
}
// Get user's active push subscriptions
const subscriptions = await models.UserPushSubscription.find({
user: userId,
isActive: true
});
if (subscriptions.length === 0) {
return { success: false, error: 'No active subscriptions found' };
}
const payload = JSON.stringify({
title: notification.title,
body: notification.body,
icon: notification.icon || '/icons/idealphotography-logo-192x192.png',
badge: notification.badge || '/icons/idealphotography-logo-96x96.png',
url: notification.url || '/',
data: notification.data || {},
actions: notification.actions || [],
tag: notification.tag || 'default',
timestamp: Date.now()
});
const results = [];
for (const subscription of subscriptions) {
try {
const result = await webpush.sendNotification(subscription.subscription, payload);
results.push({
endpoint: subscription.endpoint,
status: result.statusCode,
success: true
});
// Update last sent timestamp
await models.UserPushSubscription.findByIdAndUpdate(subscription._id, {
lastSentAt: new Date()
});
} catch (error) {
console.error('Failed to send push notification:', error);
results.push({
endpoint: subscription.endpoint,
error: error.message,
success: false
});
// If subscription is invalid (410, 404), mark as inactive
if (error.statusCode === 410 || error.statusCode === 404) {
await models.UserPushSubscription.findByIdAndUpdate(subscription._id, {
isActive: false
});
}
}
}
const successCount = results.filter(r => r.success).length;
return {
success: successCount > 0,
results,
successCount,
totalCount: subscriptions.length
};
} catch (error) {
console.error('Error sending notification to user:', error);
return { success: false, error: error.message };
}
}
/**
* Send push notification to multiple users
*/
export async function sendNotificationToUsers(userIds, notification) {
const results = [];
for (const userId of userIds) {
const result = await sendNotificationToUser(userId, notification);
results.push({ userId, ...result });
}
return results;
}
/**
* Send push notification to specific admin
*/
export async function sendNotificationToAdmin(adminId, notification) {
try {
// NOTE: Notification should already be created by the route handler
// This function only sends push notifications, not create DB records
if (!vapidPublicKey || !vapidPrivateKey) {
console.warn('VAPID keys not configured, skipping push notification');
return { success: false, error: 'VAPID not configured' };
}
// Get admin's active push subscriptions
const subscriptions = await models.AdminPushSubscription.find({
admin: adminId,
isActive: true
});
if (subscriptions.length === 0) {
return { success: false, error: 'No active subscriptions found' };
}
const payload = JSON.stringify({
title: notification.title,
body: notification.body,
icon: notification.icon || '/icons/idealphotography-logo-192x192.png',
badge: notification.badge || '/icons/idealphotography-logo-96x96.png',
url: notification.url || '/',
data: notification.data || {},
actions: notification.actions || [],
tag: notification.tag || 'default',
timestamp: Date.now()
});
const results = [];
for (const subscription of subscriptions) {
try {
const result = await webpush.sendNotification(subscription.subscription, payload);
results.push({
endpoint: subscription.endpoint,
status: result.statusCode,
success: true
});
// Update last sent timestamp
await models.AdminPushSubscription.findByIdAndUpdate(subscription._id, {
lastSentAt: new Date()
});
} catch (error) {
console.error('Failed to send push notification:', error);
results.push({
endpoint: subscription.endpoint,
error: error.message,
success: false
});
// If subscription is invalid (410, 404), mark as inactive
if (error.statusCode === 410 || error.statusCode === 404) {
await models.AdminPushSubscription.findByIdAndUpdate(subscription._id, {
isActive: false
});
}
}
}
const successCount = results.filter(r => r.success).length;
return {
success: successCount > 0,
results,
successCount,
totalCount: subscriptions.length
};
} catch (error) {
console.error('Error sending notification to admin:', error);
return { success: false, error: error.message };
}
}
/**
* Send push notification to multiple admins
*/
export async function sendNotificationToAdmins(adminIds, notification) {
const results = [];
for (const adminId of adminIds) {
const result = await sendNotificationToAdmin(adminId, notification);
results.push({ adminId, ...result });
}
return results;
}
/**
* Send notification for welcome message
*/
export async function sendWelcomeNotification(userId, userName) {
return await sendNotificationToUser(userId, {
title: '🎉 Welcome to IDEAS MEDIA COMPANY!',
body: `Hi ${userName}! Thanks for joining us. Start exploring our services!`,
url: '/dashboard',
tag: 'welcome',
data: { type: 'welcome', userId }
});
}
/**
* Send notification for booking confirmation
*/
export async function sendBookingNotification(userId, booking, product) {
const bookingDate = new Date(booking.date).toLocaleDateString('en-US', {
weekday: 'long',
month: 'short',
day: 'numeric'
});
return await sendNotificationToUser(userId, {
title: '📅 Booking Confirmed',
body: `Your ${product?.name || 'session'} on ${bookingDate} at ${booking.time} is confirmed!`,
url: `/bookings/${booking._id}`,
tag: 'booking',
data: {
type: 'booking_confirmed',
bookingId: booking._id,
date: booking.date,
time: booking.time
}
});
}
/**
* Send notification for payment confirmation
*/
export async function sendPaymentNotification(userId, amount, bookingId = null) {
return await sendNotificationToUser(userId, {
title: '💳 Payment Confirmed',
body: `Your payment of ₦${amount?.toLocaleString()} has been successfully processed!`,
url: bookingId ? `/bookings/${bookingId}` : '/payments',
tag: 'payment',
data: {
type: 'payment_confirmed',
amount,
bookingId,
timestamp: Date.now()
}
});
}
/**
* Send notification for ID verification status
*/
export async function sendVerificationNotification(userId, type, status, reason = null) {
const documentType = type === 'nin' ? 'NIN' : 'Driver\'s License';
let title, body, url;
if (status === 'verified') {
title = '✅ Verification Approved';
body = `Your ${documentType} has been approved! You now have full access.`;
url = '/profile';
} else if (status === 'rejected') {
title = '❌ Verification Update';
body = `Your ${documentType} verification needs attention. Check details.`;
url = '/verification';
}
return await sendNotificationToUser(userId, {
title,
body,
url,
tag: 'verification',
data: {
type: 'id_verification',
documentType: type,
status,
reason
}
});
}
/**
* Send admin notification for new user registration
*/
export async function sendAdminNewUserNotification(adminIds, newUser) {
const notification = {
title: '👤 New User Registration',
body: `${newUser.name} just registered and needs verification!`,
url: `/users/${newUser._id}`,
tag: 'admin_new_user',
data: {
type: 'new_user_registration',
userId: newUser._id,
userName: newUser.name,
userEmail: newUser.email
}
};
return await sendNotificationToAdmins(adminIds, notification);
}
/**
* Send admin notification for new booking
*/
export async function sendAdminNewBookingNotification(adminIds, booking, client, product) {
const bookingDate = new Date(booking.date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric'
});
const notification = {
title: '📅 New Booking',
body: `${client.name} booked ${product?.name || 'a session'} for ${bookingDate}!`,
url: `/bookings/${booking._id}`,
tag: 'admin_new_booking',
data: {
type: 'new_booking',
bookingId: booking._id,
clientId: client._id,
productId: product?._id,
date: booking.date
}
};
return await sendNotificationToAdmins(adminIds, notification);
}
/**
* Send admin notification for verification submission
*/
export async function sendAdminVerificationNotification(adminIds, user, type) {
const documentType = type === 'nin' ? 'NIN' : 'Driver\'s License';
const notification = {
title: '🔍 Verification Needed',
body: `${user.name} submitted ${documentType} for verification`,
url: `/users/${user._id}/verification`,
tag: 'admin_verification',
data: {
type: 'verification_submission',
userId: user._id,
documentType: type,
userName: user.name
}
};
return await sendNotificationToAdmins(adminIds, notification);
}
/**
* Check user notification preferences
*/
export async function checkNotificationPreferences(userId) {
try {
const user = await models.User.findById(userId).select('preferences');
// Handle case where preferences don't exist or are null
if (!user || !user.preferences) {
return { email: true, sms: false, push: true }; // Default preferences
}
return {
email: user.preferences.notifications?.email ?? true,
sms: user.preferences.notifications?.sms ?? false,
push: user.preferences.notifications?.push ?? true
};
} catch (error) {
console.error('Error checking notification preferences:', error);
return { email: true, sms: false, push: true }; // Default preferences
}
}
/**
* Get active admin user IDs for notifications
*/
export async function getActiveAdminIds() {
try {
const admins = await models.Admin.find({
isActive: true,
isVerified: true
}).select('_id');
return admins.map(admin => admin._id);
} catch (error) {
console.error('Error getting active admin IDs:', error);
return [];
}
}
/**
* Send push notification to all users (broadcast)
* @param {Object} notification - Notification object
* @returns {Promise<Object>} Result object
*/
export async function sendNotificationToAllUsers(notification) {
try {
if (!vapidPublicKey || !vapidPrivateKey) {
console.warn('VAPID keys not configured, skipping push notification');
return { success: false, error: 'VAPID not configured' };
}
// Fetch all active user push subscriptions
const subscriptions = await models.UserPushSubscription.find({
isActive: true
}).populate('user', 'name email');
if (subscriptions.length === 0) {
return { success: false, message: 'No active subscriptions' };
}
const payload = JSON.stringify({
title: notification.title,
body: notification.message ?? notification.body,
icon: notification.icon || '/icons/idealphotography-logo-192x192.png',
badge: notification.badge || '/icons/idealphotography-logo-96x96.png',
url: notification.url || '/notifications',
data: notification.data || { type: notification.type, priority: notification.priority },
actions: notification.actions || [],
tag: notification.tag || 'broadcast',
timestamp: Date.now()
});
const results = [];
for (const sub of subscriptions) {
try {
await webpush.sendNotification(sub.subscription, payload);
results.push({ success: true, userId: sub.user?._id || null });
await models.UserPushSubscription.findByIdAndUpdate(sub._id, {
lastSentAt: new Date()
});
} catch (error) {
console.error('Failed to send push notification:', error);
results.push({ success: false, userId: sub.user?._id || null, error: error.message });
if (error.statusCode === 410 || error.statusCode === 404) {
await models.UserPushSubscription.findByIdAndUpdate(sub._id, { isActive: false });
}
}
}
const successCount = results.filter(r => r.success).length;
return {
success: successCount > 0,
results,
successCount,
totalCount: subscriptions.length
};
} catch (error) {
console.error('Error sending broadcast notification:', error);
return { success: false, error: error.message };
}
}
/**
* Check if push notifications should be sent based on notification settings
* @param {Object} notification - Notification object
* @returns {boolean} Whether to send push notification
*/
export function shouldSendPushNotification(notification) {
// Only send if push channel is enabled
if (!notification.channels?.push) {
return false;
}
// Send for high priority notifications
if (notification.priority === 'high' || notification.priority === 'urgent') {
return true;
}
// Send for important notification types
const importantTypes = ['announcement', 'system', 'verification'];
if (importantTypes.includes(notification.type)) {
return true;
}
// Send for booking and payment notifications
if (notification.type === 'booking' || notification.type === 'payment') {
return true;
}
return false;
}
/**
* Send notifications for user signup
* Creates in-app notifications, sends push notifications, and emails to admins and user
* @param {Object} user - The newly registered user
* @returns {Promise<Object>} Result object with success status
*/
export async function sendUserSignupNotifications(user) {
try {
// Get active admin IDs
const adminIds = await getActiveAdminIds();
// 1. Create admin notification (in-app)
if (adminIds.length > 0) {
try {
const adminNotification = await models.Notification.create({
title: 'New User Registration',
message: `${user.name} just signed up and needs verification`,
type: 'system',
priority: 'normal',
status: 'sent',
channels: {
inApp: true,
push: true,
email: true
},
recipients: {
roles: ['admin', 'manager', 'super_admin'],
users: [],
broadcast: false
},
url: `/users/${user._id}`,
metadata: {
userId: user._id.toString(),
userName: user.name,
userEmail: user.email
}
});
// Send push notifications to admins
if (shouldSendPushNotification(adminNotification)) {
const pushPayload = {
title: adminNotification.title,
body: adminNotification.message,
url: adminNotification.url || '/notifications',
tag: 'admin_new_user',
data: {
type: adminNotification.type,
priority: adminNotification.priority,
notificationId: adminNotification._id.toString(),
userId: user._id.toString()
}
};
await sendNotificationToAdmins(adminIds, pushPayload);
}
} catch (adminNotifError) {
console.error('Error creating admin signup notification:', adminNotifError);
}
}
// 2. Create user notification (in-app) asking them to verify email
try {
const userNotification = await models.Notification.create({
title: 'Verify Your Email',
message: 'Welcome! Please check your email and verify your account to get started',
type: 'verification',
priority: 'high',
status: 'sent',
channels: {
inApp: true,
push: true,
email: false // Email already sent in registration flow
},
recipients: {
roles: [],
users: [user._id],
broadcast: false
},
url: '/verify-email',
metadata: {
userId: user._id.toString(),
requiresEmailVerification: true
}
});
// Send push notification to user
if (shouldSendPushNotification(userNotification)) {
const pushPayload = {
title: userNotification.title,
body: userNotification.message,
url: userNotification.url || '/notifications',
tag: 'email_verification',
data: {
type: userNotification.type,
priority: userNotification.priority,
notificationId: userNotification._id.toString()
}
};
await sendNotificationToUser(user._id, pushPayload);
}
} catch (userNotifError) {
console.error('Error creating user signup notification:', userNotifError);
}
return { success: true };
} catch (error) {
console.error('Error sending user signup notifications:', error);
return { success: false, error: error.message };
}
}
/**
* Send notifications for payment confirmation
* Creates in-app notifications, sends push notifications, and emails to admins
* @param {Object} order - The order object
* @param {Object} user - The user who made the payment
* @param {Object} paymentData - Payment details (amount, reference, etc.)
* @returns {Promise<Object>} Result object with success status
*/
export async function sendPaymentConfirmationNotifications(order, user, paymentData) {
try {
// Get active admin IDs
const adminIds = await getActiveAdminIds();
// Format amount
const amount = paymentData?.amount || order?.pricing?.total || 0;
const formattedAmount = new Intl.NumberFormat('en-NG', {
style: 'currency',
currency: 'NGN'
}).format(amount);
// 1. Create admin notification (in-app)
if (adminIds.length > 0) {
try {
const orderId = order?._id?.toString() || order?.id || 'Unknown';
const adminNotification = await models.Notification.create({
title: 'Payment Received',
message: `${user?.name || 'A user'} completed payment for Order #${orderId} - ${formattedAmount}`,
type: 'payment',
priority: 'normal',
status: 'sent',
channels: {
inApp: true,
push: true,
email: true
},
recipients: {
roles: ['admin', 'manager', 'super_admin'],
users: [],
broadcast: false
},
url: `/orders/${orderId}`,
metadata: {
orderId: orderId,
userId: user?._id?.toString() || null,
userName: user?.name || null,
amount: amount,
paymentReference: paymentData?.reference || null
}
});
// Send push notifications to admins
if (shouldSendPushNotification(adminNotification)) {
const pushPayload = {
title: adminNotification.title,
body: adminNotification.message,
url: adminNotification.url || '/notifications',
tag: 'admin_payment',
data: {
type: adminNotification.type,
priority: adminNotification.priority,
notificationId: adminNotification._id.toString(),
orderId: orderId
}
};
await sendNotificationToAdmins(adminIds, pushPayload);
}
// Send email to admins
try {
const { sendMail } = await import('./email.js');
const adminEmails = await models.Admin.find({
_id: { $in: adminIds },
isActive: true
}).select('email');
for (const admin of adminEmails) {
if (admin.email) {
await sendMail({
to: admin.email,
subject: `Payment Received - Order #${orderId} - Ideas Media Company`,
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2>Payment Received</h2>
<p>A customer has completed payment for an order:</p>
<div style="background-color: #f3f4f6; padding: 15px; border-radius: 5px; margin: 20px 0;">
<p><strong>Order ID:</strong> #${orderId}</p>
<p><strong>Customer:</strong> ${user?.name || 'Unknown'}</p>
<p><strong>Email:</strong> ${user?.email || 'Not provided'}</p>
<p><strong>Amount:</strong> ${formattedAmount}</p>
<p><strong>Payment Reference:</strong> ${paymentData?.reference || 'N/A'}</p>
<p><strong>Paid At:</strong> ${new Date().toLocaleString()}</p>
</div>
<p><a href="${process.env.ADMIN_URL || process.env.CLIENT_URL}/orders/${orderId}" style="background: #007bff; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">View Order</a></p>
<hr>
<p><small>Ideas Media Company Admin</small></p>
</div>
`
});
}
}
} catch (emailError) {
console.error('Error sending admin payment email:', emailError);
}
} catch (adminNotifError) {
console.error('Error creating admin payment notification:', adminNotifError);
}
}
return { success: true };
} catch (error) {
console.error('Error sending payment confirmation notifications:', error);
return { success: false, error: error.message };
}
}
/**
* Create payment confirmation notification for the purchasing user
* @param {Object} order
* @param {Object} user
* @param {Object} paymentData
*/
export async function createUserPaymentNotification(order, user, paymentData = {}) {
try {
if (!user?._id) {
throw new Error('User information is required to create payment notification');
}
const orderId = order?._id?.toString() || order?.id || '';
const amount = paymentData.amount ?? order?.pricing?.total ?? 0;
const currency = paymentData.currency || order?.pricing?.currency || 'NGN';
const formatter = new Intl.NumberFormat('en-NG', {
style: 'currency',
currency
});
const formattedAmount = formatter.format(amount);
const notification = await models.Notification.create({
title: 'Payment Confirmed',
message: `Your payment of ${formattedAmount} has been successfully processed!`,
type: 'payment',
priority: 'normal',
status: 'sent',
channels: {
inApp: true,
push: true,
email: false // Email already handled in payment flow
},
recipients: {
roles: [],
users: [user._id],
broadcast: false
},
url: `/orders/${orderId}`,
metadata: {
orderId,
amount,
currency,
paymentReference: paymentData?.reference || null
}
});
if (shouldSendPushNotification(notification)) {
const pushPayload = {
title: notification.title,
body: notification.message,
url: notification.url || '/notifications',
tag: 'payment_confirmed',
data: {
type: notification.type,
priority: notification.priority,
notificationId: notification._id.toString(),
orderId
}
};
await sendNotificationToUser(user._id, pushPayload);
}
return { success: true, notificationId: notification._id };
} catch (error) {
console.error('Error creating user payment notification:', error);
return { success: false, error: error.message };
}
}
/**
* Create payment failure notification for the user
* @param {Object} order - The order object
* @param {Object} user - The user who attempted payment
* @param {Object} paymentData - Payment details (reason, reference, etc.)
* @returns {Promise<Object>} Result object with success status
*/
export async function createUserPaymentFailureNotification(order, user, paymentData = {}) {
try {
if (!user?._id) {
throw new Error('User information is required to create payment failure notification');
}
const orderId = order?._id?.toString() || order?.id || '';
const orderNumber = order?.orderNumber || orderId;
const reason = paymentData?.reason || paymentData?.gatewayResponse || 'Payment could not be processed';
const notification = await models.Notification.create({
title: 'Payment Failed',
message: `Your payment for Order #${orderNumber} was unsuccessful. ${reason}. Please try again.`,
type: 'payment',
priority: 'high',
status: 'sent',
channels: {
inApp: true,
push: true,
email: false // Email handled separately if needed
},
recipients: {
roles: [],
users: [user._id],
broadcast: false
},
url: `/checkout?order=${orderId}`,
metadata: {
orderId,
orderNumber,
paymentStatus: 'failed',
reason,
paymentReference: paymentData?.reference || null
}
});
if (shouldSendPushNotification(notification)) {
const pushPayload = {
title: notification.title,
body: notification.message,
url: notification.url || '/notifications',
tag: 'payment_failed',
data: {
type: notification.type,
priority: notification.priority,
notificationId: notification._id.toString(),
orderId,
status: 'failed'
}
};
await sendNotificationToUser(user._id, pushPayload);
}
return { success: true, notificationId: notification._id };
} catch (error) {
console.error('Error creating user payment failure notification:', error);
return { success: false, error: error.message };
}
}
/**
* Send payment failure notifications to admins
* Creates in-app notifications, sends push notifications, and emails to admins
* @param {Object} order - The order object
* @param {Object} user - The user who attempted payment
* @param {Object} paymentData - Payment details (reason, reference, etc.)
* @returns {Promise<Object>} Result object with success status
*/
export async function sendPaymentFailureNotifications(order, user, paymentData = {}) {
try {
// Get active admin IDs
const adminIds = await getActiveAdminIds();
const orderId = order?._id?.toString() || order?.id || 'Unknown';
const orderNumber = order?.orderNumber || orderId;
const reason = paymentData?.reason || paymentData?.gatewayResponse || 'Unknown reason';
const amount = paymentData?.amount || order?.pricing?.total || 0;
const formattedAmount = new Intl.NumberFormat('en-NG', {
style: 'currency',
currency: 'NGN'
}).format(amount);
// 1. Create admin notification (in-app)
if (adminIds.length > 0) {
try {
const adminNotification = await models.Notification.create({
title: 'Payment Failed',
message: `${user?.name || 'A user'}'s payment for Order #${orderNumber} failed - ${reason}`,
type: 'payment',
priority: 'normal',
status: 'sent',
channels: {
inApp: true,
push: true,
email: true
},
recipients: {
roles: ['admin', 'manager', 'super_admin'],
users: [],
broadcast: false
},
url: `/orders/${orderId}`,
metadata: {
orderId,
orderNumber,
userId: user?._id?.toString() || null,
userName: user?.name || null,
userEmail: user?.email || null,
amount,
paymentStatus: 'failed',
reason,
paymentReference: paymentData?.reference || null
}
});
// Send push notifications to admins
if (shouldSendPushNotification(adminNotification)) {
const pushPayload = {
title: adminNotification.title,
body: adminNotification.message,
url: adminNotification.url || '/notifications',
tag: 'admin_payment_failed',
data: {
type: adminNotification.type,
priority: adminNotification.priority,
notificationId: adminNotification._id.toString(),
orderId,
status: 'failed'
}
};
await sendNotificationToAdmins(adminIds, pushPayload);
}
// Send email to admins
try {
const { sendMail } = await import('./email.js');
const adminEmails = await models.Admin.find({
_id: { $in: adminIds },
isActive: true
}).select('email');
for (const admin of adminEmails) {
if (admin.email) {
await sendMail({
to: admin.email,
subject: `Payment Failed - Order #${orderNumber} - Ideas Media Company`,
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<div style="background-color: #dc3545; color: white; padding: 20px; text-align: center;">
<h2 style="margin: 0;">Payment Failed</h2>
</div>
<div style="padding: 20px;">
<p>A customer's payment attempt has failed:</p>
<div style="background-color: #f8d7da; padding: 15px; border-radius: 5px; margin: 20px 0; border-left: 4px solid #dc3545;">
<p style="margin: 5px 0;"><strong>Order ID:</strong> #${orderNumber}</p>
<p style="margin: 5px 0;"><strong>Customer:</strong> ${user?.name || 'Unknown'}</p>
<p style="margin: 5px 0;"><strong>Email:</strong> ${user?.email || 'Not provided'}</p>
<p style="margin: 5px 0;"><strong>Amount:</strong> ${formattedAmount}</p>
<p style="margin: 5px 0;"><strong>Reason:</strong> ${reason}</p>
<p style="margin: 5px 0;"><strong>Reference:</strong> ${paymentData?.reference || 'N/A'}</p>
<p style="margin: 5px 0;"><strong>Time:</strong> ${new Date().toLocaleString()}</p>
</div>
<p>The customer may retry payment or require assistance.</p>
<p><a href="${process.env.ADMIN_URL || process.env.CLIENT_URL}/orders/${orderId}" style="background: #007bff; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">View Order</a></p>
</div>
<div style="background-color: #f8f9fa; padding: 15px; text-align: center; color: #666; font-size: 12px;">
<p style="margin: 0;">Ideas Media Company Admin</p>
</div>
</div>
`
});
}
}
} catch (emailError) {
console.error('Error sending admin payment failure email:', emailError);
}
} catch (adminNotifError) {
console.error('Error creating admin payment failure notification:', adminNotifError);
}
}
return { success: true };
} catch (error) {
console.error('Error sending payment failure notifications:', error);
return { success: false, error: error.message };
}
}
// ============================================================================
// BOOKING WORKFLOW NOTIFICATIONS
// ============================================================================
/**
* Get booking item name from productInfo
*/
function getBookingItemName(bookingItem) {
if (!bookingItem) return 'Booking';
const productInfo = typeof bookingItem.productInfo === 'string'
? JSON.parse(bookingItem.productInfo)
: bookingItem.productInfo;
return productInfo?.name || 'Booking';
}
/**
* Get booking type display name
*/
function getBookingTypeDisplay(productType) {
const types = {
'studio_session': 'Studio Session',
'makeover_session': 'Makeover Session',
'equipment_rental': 'Equipment Rental'
};
return types[productType] || 'Booking';
}
/**
* Format booking date for display
*/
function formatBookingDate(bookingItem) {
const date = bookingItem?.serviceDetails?.date || bookingItem?.rentalPeriod?.startDate;
if (!date) return 'TBD';
return new Date(date).toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
/**
* Send booking accepted notification to user
* HIGH PRIORITY: Email + In-app + Push
*/
export async function sendBookingAcceptedNotification(order, user, bookingItem) {
try {
if (!user?._id) {
console.warn('No user provided for booking accepted notification');
return { success: false, error: 'No user provided' };
}
const bookingName = getBookingItemName(bookingItem);
const bookingType = getBookingTypeDisplay(bookingItem?.productType);
const bookingDate = formatBookingDate(bookingItem);
const orderId = order?._id?.toString() || '';
const orderNumber = order?.orderNumber || orderId;
// Create in-app notification
const notification = await models.Notification.create({
title: 'Booking Confirmed! ✓',
message: `Great news! Your ${bookingType} "${bookingName}" has been confirmed for ${bookingDate}.`,
type: 'booking',
priority: 'high',
status: 'sent',
channels: {
inApp: true,
push: true,
email: true
},
recipients: {
roles: [],
users: [user._id],
broadcast: false
},
url: `/bookings/${orderId}`,
metadata: {
orderId,
orderNumber,
bookingType: bookingItem?.productType,
bookingName,
action: 'accepted'
}
});
// Send push notification
if (shouldSendPushNotification(notification)) {
const pushPayload = {
title: notification.title,
body: notification.message,
url: notification.url || '/bookings',
tag: 'booking_accepted',
data: {
type: notification.type,
priority: notification.priority,
notificationId: notification._id.toString(),
orderId
}
};
await sendNotificationToUser(user._id, pushPayload);
}
// Send email
try {
const { sendMail } = await import('./email.js');
if (user.email) {
await sendMail({
to: user.email,
subject: `Booking Confirmed - ${bookingName} - Ideas Media Company`,
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<div style="background-color: #22c55e; color: white; padding: 20px; text-align: center;">
<h2 style="margin: 0;">✓ Booking Confirmed!</h2>
</div>
<div style="padding: 20px;">
<p>Hi ${user.name || 'Valued Customer'},</p>
<p>Great news! Your booking has been confirmed:</p>
<div style="background-color: #dcfce7; padding: 15px; border-radius: 5px; margin: 20px 0; border-left: 4px solid #22c55e;">
<p style="margin: 5px 0;"><strong>Service:</strong> ${bookingName}</p>
<p style="margin: 5px 0;"><strong>Type:</strong> ${bookingType}</p>
<p style="margin: 5px 0;"><strong>Date:</strong> ${bookingDate}</p>
<p style="margin: 5px 0;"><strong>Order:</strong> #${orderNumber}</p>
</div>
<p>We look forward to seeing you!</p>
<p><a href="${process.env.CLIENT_URL || 'https://idealmediacompany.com'}/bookings/${orderId}" style="background: #22c55e; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">View Booking Details</a></p>
</div>
<div style="background-color: #f8f9fa; padding: 15px; text-align: center; color: #666; font-size: 12px;">
<p style="margin: 0;">Ideas Media Company</p>
</div>
</div>
`
});
}
} catch (emailError) {
console.error('Error sending booking accepted email:', emailError);
}
return { success: true, notificationId: notification._id };
} catch (error) {
console.error('Error sending booking accepted notification:', error);
return { success: false, error: error.message };
}
}
/**
* Send booking rejected notification to user
* HIGH PRIORITY: Email + In-app + Push
*/
export async function sendBookingRejectedNotification(order, user, bookingItem, reason) {
try {
if (!user?._id) {
console.warn('No user provided for booking rejected notification');
return { success: false, error: 'No user provided' };
}
const bookingName = getBookingItemName(bookingItem);
const bookingType = getBookingTypeDisplay(bookingItem?.productType);
const orderId = order?._id?.toString() || '';
const orderNumber = order?.orderNumber || orderId;
const rejectionReason = reason || 'The requested time slot is not available';
// Create in-app notification
const notification = await models.Notification.create({
title: 'Booking Not Available',
message: `Unfortunately, your ${bookingType} "${bookingName}" could not be confirmed. ${rejectionReason}`,
type: 'booking',
priority: 'high',
status: 'sent',
channels: {
inApp: true,
push: true,
email: true
},
recipients: {
roles: [],
users: [user._id],
broadcast: false
},
url: `/bookings/${orderId}`,
metadata: {
orderId,
orderNumber,
bookingType: bookingItem?.productType,
bookingName,
action: 'rejected',
reason: rejectionReason
}
});
// Send push notification
if (shouldSendPushNotification(notification)) {
const pushPayload = {
title: notification.title,
body: notification.message,
url: notification.url || '/bookings',
tag: 'booking_rejected',
data: {
type: notification.type,
priority: notification.priority,
notificationId: notification._id.toString(),
orderId
}
};
await sendNotificationToUser(user._id, pushPayload);
}
// Send email
try {
const { sendMail } = await import('./email.js');
if (user.email) {
await sendMail({
to: user.email,
subject: `Booking Update - ${bookingName} - Ideas Media Company`,
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<div style="background-color: #ef4444; color: white; padding: 20px; text-align: center;">
<h2 style="margin: 0;">Booking Update</h2>
</div>
<div style="padding: 20px;">
<p>Hi ${user.name || 'Valued Customer'},</p>
<p>We regret to inform you that we could not confirm your booking:</p>
<div style="background-color: #fef2f2; padding: 15px; border-radius: 5px; margin: 20px 0; border-left: 4px solid #ef4444;">
<p style="margin: 5px 0;"><strong>Service:</strong> ${bookingName}</p>
<p style="margin: 5px 0;"><strong>Type:</strong> ${bookingType}</p>
<p style="margin: 5px 0;"><strong>Order:</strong> #${orderNumber}</p>
<p style="margin: 5px 0;"><strong>Reason:</strong> ${rejectionReason}</p>
</div>
<p>If you've already paid, a refund will be processed within 3-5 business days.</p>
<p>Please feel free to book a different time slot or contact us for assistance.</p>
<p><a href="${process.env.CLIENT_URL || 'https://idealmediacompany.com'}/contact" style="background: #3b82f6; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">Contact Support</a></p>
</div>
<div style="background-color: #f8f9fa; padding: