From 2802869d580396271fe7a32275d40be5f774a7b2 Mon Sep 17 00:00:00 2001 From: AdeepaK2 Date: Tue, 22 Jul 2025 23:14:32 +0530 Subject: [PATCH] feat: implement meeting cancellation email notifications with nodemailer integration --- .../api/meeting-cron/cancellation/route.ts | 533 ++++++++++++++++++ 1 file changed, 533 insertions(+) create mode 100644 src/app/api/meeting-cron/cancellation/route.ts diff --git a/src/app/api/meeting-cron/cancellation/route.ts b/src/app/api/meeting-cron/cancellation/route.ts new file mode 100644 index 00000000..1656c67f --- /dev/null +++ b/src/app/api/meeting-cron/cancellation/route.ts @@ -0,0 +1,533 @@ +import { NextRequest, NextResponse } from 'next/server'; +import nodemailer from 'nodemailer'; +import Meeting from '@/lib/models/meetingSchema'; +import User from '@/lib/models/userSchema'; +import MeetingEmailNotification from '@/lib/models/meetingEmailNotificationSchema'; +import connect from '@/lib/db'; + +// Create nodemailer transporter using Gmail SMTP +const createTransporter = () => { + return nodemailer.createTransport({ + service: 'gmail', + host: 'smtp.gmail.com', + port: 465, + secure: true, + auth: { + user: process.env.MEETING_NOTI_MAIL, + pass: process.env.MEETING_NOTI_PW, + }, + connectionTimeout: 60000, // 60 seconds + greetingTimeout: 30000, // 30 seconds + socketTimeout: 60000, // 60 seconds + }); +}; + +// Email template for meeting cancellation +const createMeetingCancellationEmail = ( + userFirstName: string, + userLastName: string, + otherUserFirstName: string, + otherUserLastName: string, + meetingTime: Date, + description: string, + meetingId: string, + isCancelledBy: boolean // true if this user cancelled, false if other user cancelled +) => { + const formattedTime = meetingTime.toLocaleString('en-US', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + timeZoneName: 'short' + }); + + const cancellationMessage = isCancelledBy + ? `You have successfully cancelled your meeting with ${otherUserFirstName} ${otherUserLastName}.` + : `${otherUserFirstName} ${otherUserLastName} has cancelled your scheduled meeting.`; + + const subject = isCancelledBy + ? `✅ Meeting Cancelled: Confirmation of your cancellation` + : `❌ Meeting Cancelled: Your meeting has been cancelled`; + + return { + subject, + html: ` + + + + + + Meeting Cancelled + + +
+

❌ Meeting Cancelled

+

Your skill swap session has been cancelled

+
+ +
+

+ Hi ${userFirstName} ${userLastName}, +

+ +

+ ${cancellationMessage} +

+ +
+

📅 Cancelled Meeting Details

+

Originally Scheduled: ${formattedTime}

+

With: ${otherUserFirstName} ${otherUserLastName}

+

Description: ${description}

+

Meeting ID: ${meetingId}

+
+ + ${!isCancelledBy ? ` +
+

💡 What's Next?

+
    +
  • You can browse and connect with other users on SkillSwap Hub
  • +
  • Consider reaching out to reschedule if the timing didn't work
  • +
  • Look for other skill swap opportunities that match your interests
  • +
  • Update your availability if your schedule has changed
  • +
+
+ ` : ` +
+

💡 Cancellation Confirmed

+

+ Your meeting has been successfully cancelled. Both you and ${otherUserFirstName} ${otherUserLastName} have been notified. + You can always schedule new meetings with other users on SkillSwap Hub. +

+
+ `} + +
+

+ This is an automated notification from SkillSwap Hub.
+ We're sorry this meeting didn't work out, but there are always more opportunities! 🌟 +

+
+
+ +
+

+ © 2024 SkillSwap Hub. All rights reserved.
+ If you have any questions, please contact our support team. +

+
+ + + `, + text: ` +Meeting Cancelled + +Hi ${userFirstName} ${userLastName}, + +${isCancelledBy + ? `You have successfully cancelled your meeting with ${otherUserFirstName} ${otherUserLastName}.` + : `${otherUserFirstName} ${otherUserLastName} has cancelled your scheduled meeting.` +} + +Cancelled Meeting Details: +- Originally Scheduled: ${formattedTime} +- With: ${otherUserFirstName} ${otherUserLastName} +- Description: ${description} +- Meeting ID: ${meetingId} + +${!isCancelledBy + ? `What's Next? +• You can browse and connect with other users on SkillSwap Hub +• Consider reaching out to reschedule if the timing didn't work +• Look for other skill swap opportunities that match your interests +• Update your availability if your schedule has changed` + : `Cancellation Confirmed +Your meeting has been successfully cancelled. Both you and ${otherUserFirstName} ${otherUserLastName} have been notified. +You can always schedule new meetings with other users on SkillSwap Hub.` +} + +This is an automated notification from SkillSwap Hub. +We're sorry this meeting didn't work out, but there are always more opportunities! + +© 2024 SkillSwap Hub. All rights reserved. + `.trim() + }; +}; + +// Main function to send cancellation emails for cancelled meetings +async function sendCancellationEmails() { + try { + console.log('🔍 Starting cancellation email check...'); + + const now = new Date(); + + console.log('⏰ Current time:', now.toISOString()); + + // Find cancelled meetings that are scheduled in the future and haven't been processed yet + const cancelledMeetings = await Meeting.find({ + state: 'cancelled', + meetingTime: { + $gt: now // Meeting time is in the future + } + }).populate('senderId receiverId'); + + console.log(`📅 Found ${cancelledMeetings.length} cancelled meetings scheduled for the future`); + + if (cancelledMeetings.length === 0) { + return { + success: true, + message: 'No cancelled meetings found that are scheduled for the future', + processed: 0 + }; + } + + // Create transporter + const transporter = createTransporter(); + + // Verify transporter configuration + await transporter.verify(); + console.log('✅ Email transporter verified'); + + let emailsSent = 0; + let errors = []; + let skippedMeetings = 0; + + // Process each cancelled meeting + for (const meeting of cancelledMeetings) { + try { + console.log(`📧 Processing cancelled meeting ${meeting._id}`); + + // Check if cancellation notifications have already been sent for this meeting + const existingNotification = await MeetingEmailNotification.getNotificationStatus(meeting._id); + + if (existingNotification && existingNotification.senderNotified && existingNotification.receiverNotified) { + console.log(`⏭️ Skipping meeting ${meeting._id} - cancellation emails already sent`); + skippedMeetings++; + continue; + } + + // Get populated user data + const sender = meeting.senderId as any; + const receiver = meeting.receiverId as any; + + if (!sender || !receiver) { + console.error(`❌ Missing user data for meeting ${meeting._id}`); + errors.push(`Missing user data for meeting ${meeting._id}`); + continue; + } + + console.log('👥 Meeting participants:', { + sender: `${sender.firstName} ${sender.lastName} (${sender.email})`, + receiver: `${receiver.firstName} ${receiver.lastName} (${receiver.email})` + }); + + // Send cancellation email to sender + if (!existingNotification?.senderNotified) { + const senderEmail = createMeetingCancellationEmail( + sender.firstName, + sender.lastName, + receiver.firstName, + receiver.lastName, + meeting.meetingTime, + meeting.description, + meeting._id.toString(), + true // Assuming sender cancelled (you might want to track who cancelled) + ); + + try { + await transporter.sendMail({ + from: `"SkillSwap Hub" <${process.env.MEETING_NOTI_MAIL}>`, + to: sender.email, + subject: senderEmail.subject, + html: senderEmail.html, + text: senderEmail.text + }); + console.log(`✅ Cancellation email sent to sender: ${sender.email}`); + emailsSent++; + + // Mark sender as notified + await MeetingEmailNotification.markUserNotified(meeting._id, 'sender'); + console.log(`✅ Marked sender as notified for cancelled meeting ${meeting._id}`); + + } catch (emailError: any) { + console.error(`❌ Failed to send cancellation email to sender ${sender.email}:`, emailError); + errors.push(`Failed to send email to sender ${sender.email}: ${emailError?.message || 'Unknown error'}`); + } + } + + // Send cancellation email to receiver + if (!existingNotification?.receiverNotified) { + const receiverEmail = createMeetingCancellationEmail( + receiver.firstName, + receiver.lastName, + sender.firstName, + sender.lastName, + meeting.meetingTime, + meeting.description, + meeting._id.toString(), + false // Receiver didn't cancel + ); + + try { + await transporter.sendMail({ + from: `"SkillSwap Hub" <${process.env.MEETING_NOTI_MAIL}>`, + to: receiver.email, + subject: receiverEmail.subject, + html: receiverEmail.html, + text: receiverEmail.text + }); + console.log(`✅ Cancellation email sent to receiver: ${receiver.email}`); + emailsSent++; + + // Mark receiver as notified + await MeetingEmailNotification.markUserNotified(meeting._id, 'receiver'); + console.log(`✅ Marked receiver as notified for cancelled meeting ${meeting._id}`); + + } catch (emailError: any) { + console.error(`❌ Failed to send cancellation email to receiver ${receiver.email}:`, emailError); + errors.push(`Failed to send email to receiver ${receiver.email}: ${emailError?.message || 'Unknown error'}`); + } + } + + // Small delay between emails to avoid rate limiting + await new Promise(resolve => setTimeout(resolve, 100)); + + } catch (meetingError: any) { + console.error(`❌ Error processing cancelled meeting ${meeting._id}:`, meetingError); + errors.push(`Error processing meeting ${meeting._id}: ${meetingError?.message || 'Unknown error'}`); + } + } + + console.log(`🎉 Cancellation email job completed. Emails sent: ${emailsSent}, Meetings skipped: ${skippedMeetings}`); + + return { + success: true, + message: `Cancellation emails processed successfully`, + processed: cancelledMeetings.length, + emailsSent, + skippedMeetings, + errors: errors.length > 0 ? errors : undefined + }; + + } catch (error) { + console.error('❌ Error in sendCancellationEmails:', error); + throw error; + } +} + +// GET endpoint for cron job +export async function GET(request: NextRequest) { + try { + console.log('🚀 Meeting cancellation cron job started at:', new Date().toISOString()); + + // Validate System API Key + const apiKey = request.headers.get('x-api-key'); + if (!apiKey || apiKey !== process.env.SYSTEM_API_KEY) { + console.error('❌ Unauthorized: Invalid or missing API key'); + return NextResponse.json({ + success: false, + message: 'Unauthorized: Invalid or missing API key', + timestamp: new Date().toISOString() + }, { status: 401 }); + } + + // Connect to database + await connect(); + + // Verify required environment variables + if (!process.env.MEETING_NOTI_MAIL || !process.env.MEETING_NOTI_PW) { + throw new Error('Missing required email configuration environment variables'); + } + + // Send cancellation emails + const result = await sendCancellationEmails(); + + console.log('✅ Meeting cancellation cron job completed successfully:', result); + + return NextResponse.json({ + success: true, + message: 'Meeting cancellation emails cron job executed successfully', + timestamp: new Date().toISOString(), + result + }, { status: 200 }); + + } catch (error: any) { + console.error('❌ Meeting cancellation cron job failed:', error); + + return NextResponse.json({ + success: false, + message: 'Meeting cancellation emails cron job failed', + error: error?.message || 'Unknown error', + timestamp: new Date().toISOString() + }, { status: 500 }); + } +} + +// Optional: Add a health check endpoint +export async function HEAD(request: NextRequest) { + return new NextResponse(null, { status: 200 }); +} + +// POST endpoint for immediate cancellation email sending (when a meeting is cancelled) +export async function POST(request: NextRequest) { + try { + console.log('🚀 Immediate meeting cancellation email triggered at:', new Date().toISOString()); + + // Validate System API Key + const apiKey = request.headers.get('x-api-key'); + if (!apiKey || apiKey !== process.env.SYSTEM_API_KEY) { + console.error('❌ Unauthorized: Invalid or missing API key'); + return NextResponse.json({ + success: false, + message: 'Unauthorized: Invalid or missing API key', + timestamp: new Date().toISOString() + }, { status: 401 }); + } + + // Get meeting ID from request body + const body = await request.json(); + const { meetingId, cancelledBy } = body; + + if (!meetingId) { + return NextResponse.json({ + success: false, + message: 'Meeting ID is required', + timestamp: new Date().toISOString() + }, { status: 400 }); + } + + // Connect to database + await connect(); + + // Verify required environment variables + if (!process.env.MEETING_NOTI_MAIL || !process.env.MEETING_NOTI_PW) { + throw new Error('Missing required email configuration environment variables'); + } + + // Find the specific cancelled meeting + const meeting = await Meeting.findById(meetingId).populate('senderId receiverId'); + + if (!meeting) { + return NextResponse.json({ + success: false, + message: 'Meeting not found', + timestamp: new Date().toISOString() + }, { status: 404 }); + } + + if (meeting.state !== 'cancelled') { + return NextResponse.json({ + success: false, + message: 'Meeting is not in cancelled state', + timestamp: new Date().toISOString() + }, { status: 400 }); + } + + const now = new Date(); + if (meeting.meetingTime <= now) { + return NextResponse.json({ + success: false, + message: 'Meeting is not scheduled for the future', + timestamp: new Date().toISOString() + }, { status: 400 }); + } + + // Check if emails have already been sent + const existingNotification = await MeetingEmailNotification.getNotificationStatus(meeting._id); + if (existingNotification && existingNotification.senderNotified && existingNotification.receiverNotified) { + return NextResponse.json({ + success: false, + message: 'Cancellation emails have already been sent for this meeting', + timestamp: new Date().toISOString() + }, { status: 400 }); + } + + // Create transporter + const transporter = createTransporter(); + await transporter.verify(); + + const sender = meeting.senderId as any; + const receiver = meeting.receiverId as any; + + let emailsSent = 0; + const errors = []; + + // Determine who cancelled the meeting + const senderCancelled = cancelledBy === 'sender' || cancelledBy === sender._id.toString(); + + // Send email to sender + const senderEmail = createMeetingCancellationEmail( + sender.firstName, + sender.lastName, + receiver.firstName, + receiver.lastName, + meeting.meetingTime, + meeting.description, + meeting._id.toString(), + senderCancelled + ); + + try { + await transporter.sendMail({ + from: `"SkillSwap Hub" <${process.env.MEETING_NOTI_MAIL}>`, + to: sender.email, + subject: senderEmail.subject, + html: senderEmail.html, + text: senderEmail.text + }); + emailsSent++; + await MeetingEmailNotification.markUserNotified(meeting._id, 'sender'); + } catch (emailError: any) { + errors.push(`Failed to send email to sender: ${emailError?.message}`); + } + + // Send email to receiver + const receiverEmail = createMeetingCancellationEmail( + receiver.firstName, + receiver.lastName, + sender.firstName, + sender.lastName, + meeting.meetingTime, + meeting.description, + meeting._id.toString(), + !senderCancelled + ); + + try { + await transporter.sendMail({ + from: `"SkillSwap Hub" <${process.env.MEETING_NOTI_MAIL}>`, + to: receiver.email, + subject: receiverEmail.subject, + html: receiverEmail.html, + text: receiverEmail.text + }); + emailsSent++; + await MeetingEmailNotification.markUserNotified(meeting._id, 'receiver'); + } catch (emailError: any) { + errors.push(`Failed to send email to receiver: ${emailError?.message}`); + } + + return NextResponse.json({ + success: true, + message: 'Meeting cancellation emails sent successfully', + timestamp: new Date().toISOString(), + result: { + meetingId, + emailsSent, + errors: errors.length > 0 ? errors : undefined + } + }, { status: 200 }); + + } catch (error: any) { + console.error('❌ Immediate meeting cancellation email failed:', error); + + return NextResponse.json({ + success: false, + message: 'Meeting cancellation email failed', + error: error?.message || 'Unknown error', + timestamp: new Date().toISOString() + }, { status: 500 }); + } +}