const path = require("path"); const { SchedulerClient, DeleteScheduleCommand, } = require("@aws-sdk/client-scheduler"); const { queries, email, logger } = require("../shared"); const { conditionCheckExists } = require("./queries"); let schedulerClient = null; /** * Get or create an EventBridge Scheduler client. */ function getSchedulerClient() { if (!schedulerClient) { schedulerClient = new SchedulerClient({ region: process.env.AWS_REGION, }); } return schedulerClient; } /** * Delete a one-time EventBridge schedule after it has fired. * @param {string} scheduleName - Name of the schedule to delete */ async function deleteSchedule(scheduleName) { try { const client = getSchedulerClient(); const groupName = process.env.SCHEDULE_GROUP_NAME || "condition-check-reminders"; await client.send( new DeleteScheduleCommand({ Name: scheduleName, GroupName: groupName, }), ); logger.info("Deleted schedule after execution", { scheduleName, groupName, }); } catch (error) { // Log but don't fail - schedule may have already been deleted logger.warn("Failed to delete schedule", { scheduleName, error: error.message, }); } } /** * Get email content based on check type. * @param {string} checkType - Type of condition check * @param {Object} rental - Rental with item, owner, renter details * @returns {Object} Email content (subject, title, message, recipient) */ function getEmailContent(checkType, rental) { const itemName = rental.item.name; const frontendUrl = process.env.FRONTEND_URL; const content = { pre_rental_owner: { recipient: rental.owner, subject: `Condition Check Reminder: ${itemName}`, title: "Pre-Rental Condition Check", message: `Please take photos of "${itemName}" before the rental begins tomorrow. This helps protect both you and the renter.`, deadline: email.formatEmailDate(rental.startDateTime), }, rental_start_renter: { recipient: rental.renter, subject: `Document Item Condition: ${itemName}`, title: "Rental Start Condition Check", message: `Please take photos when you receive "${itemName}" to document its condition. This protects you in case of any disputes.`, deadline: email.formatEmailDate( new Date( new Date(rental.startDateTime).getTime() + 24 * 60 * 60 * 1000, ), ), }, rental_end_renter: { recipient: rental.renter, subject: `Return Condition Check: ${itemName}`, title: "Rental End Condition Check", message: `Please take photos when returning "${itemName}" to document its condition before handoff.`, deadline: email.formatEmailDate(rental.endDateTime), }, post_rental_owner: { recipient: rental.owner, subject: `Review Return: ${itemName}`, title: "Post-Rental Condition Check", message: `Please take photos and mark the return status for "${itemName}". This completes the rental process.`, deadline: email.formatEmailDate( new Date(new Date(rental.endDateTime).getTime() + 48 * 60 * 60 * 1000), ), }, }; return content[checkType]; } /** * Process a condition check reminder. * @param {string} rentalId - UUID of the rental * @param {string} checkType - Type of condition check * @param {string} scheduleName - Name of the EventBridge schedule (for cleanup) * @returns {Promise} Result of the operation */ async function processReminder(rentalId, checkType, scheduleName) { logger.info("Processing condition check reminder", { rentalId, checkType, scheduleName, }); try { // 1. Check if condition check already exists (skip if yes) const exists = await conditionCheckExists(rentalId, checkType); if (exists) { logger.info("Condition check already exists, skipping reminder", { rentalId, checkType, }); // Still delete the schedule if (scheduleName) { await deleteSchedule(scheduleName); } return { success: true, skipped: true, reason: "condition_check_exists" }; } // 2. Fetch rental details const rental = await queries.getRentalWithDetails(rentalId); if (!rental) { logger.error("Rental not found", { rentalId }); return { success: false, error: "rental_not_found" }; } // 3. Check rental status - only send for active rentals const validStatuses = ["confirmed", "active", "completed"]; if (!validStatuses.includes(rental.status)) { logger.info("Rental status not valid for reminder", { rentalId, status: rental.status, }); if (scheduleName) { await deleteSchedule(scheduleName); } return { success: true, skipped: true, reason: "invalid_rental_status" }; } // 4. Get email content and send const emailContent = getEmailContent(checkType, rental); if (!emailContent) { logger.error("Unknown check type", { checkType }); return { success: false, error: "unknown_check_type" }; } // 5. Load and render the email template const templatePath = path.join( __dirname, "templates", "conditionCheckReminderToUser.html", ); const template = await email.loadTemplate(templatePath); const htmlBody = email.renderTemplate(template, { title: emailContent.title, message: emailContent.message, itemName: rental.item.name, deadline: emailContent.deadline, recipientName: emailContent.recipient.firstName, }); // 6. Send the email const result = await email.sendEmail( emailContent.recipient.email, emailContent.subject, htmlBody, ); if (!result.success) { logger.error("Failed to send reminder email", { rentalId, checkType, error: result.error, }); return { success: false, error: result.error }; } logger.info("Sent condition check reminder", { rentalId, checkType, to: emailContent.recipient.email, messageId: result.messageId, }); // 7. Delete the one-time schedule if (scheduleName) { await deleteSchedule(scheduleName); } return { success: true, messageId: result.messageId }; } catch (error) { logger.error("Error processing condition check reminder", { rentalId, checkType, error: error.message, stack: error.stack, }); return { success: false, error: error.message }; } } module.exports = { processReminder, getEmailContent, deleteSchedule, };