UNPKG

@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
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: