const { SchedulerClient, CreateScheduleCommand, DeleteScheduleCommand, } = require("@aws-sdk/client-scheduler"); const { getAWSConfig } = require("../config/aws"); const logger = require("../utils/logger"); /** * Service for managing EventBridge Scheduler schedules for condition check reminders. * * Creates one-time schedules when a rental is confirmed: * - pre_rental_owner: 24 hours before rental start * - rental_start_renter: At rental start * - rental_end_renter: At rental end * - post_rental_owner: 24 hours after rental end */ class EventBridgeSchedulerService { constructor() { if (EventBridgeSchedulerService.instance) { return EventBridgeSchedulerService.instance; } this.client = null; this.initialized = false; // Only enable when explicitly set - allows deploying code without activating the feature this.conditionCheckRemindersEnabled = process.env.CONDITION_CHECK_REMINDERS_ENABLED === "true"; EventBridgeSchedulerService.instance = this; } /** * Initialize the EventBridge Scheduler client. */ async initialize() { if (this.initialized) return; try { const awsConfig = getAWSConfig(); this.client = new SchedulerClient(awsConfig); this.initialized = true; logger.info("EventBridge Scheduler Service initialized"); } catch (error) { logger.error("Failed to initialize EventBridge Scheduler Service", { error, }); throw error; } } /** * Get configuration from environment. */ getConfig() { return { groupName: process.env.SCHEDULER_GROUP_NAME || "condition-check-reminders", lambdaArn: process.env.LAMBDA_CONDITION_CHECK_ARN, schedulerRoleArn: process.env.SCHEDULER_ROLE_ARN, }; } /** * Create schedule name for a rental and check type. */ getScheduleName(rentalId, checkType) { return `rental-${rentalId}-${checkType}`; } /** * Calculate schedule times for all 4 check types. */ getScheduleTimes(rental) { const startDate = new Date(rental.startDateTime); const endDate = new Date(rental.endDateTime); return { pre_rental_owner: new Date(startDate.getTime() - 24 * 60 * 60 * 1000), rental_start_renter: startDate, rental_end_renter: endDate, post_rental_owner: new Date(endDate.getTime() + 24 * 60 * 60 * 1000), }; } /** * Create a single EventBridge schedule. */ async createSchedule(name, scheduleTime, payload) { if (!this.initialized) { await this.initialize(); } const config = this.getConfig(); if (!config.lambdaArn) { logger.warn("Lambda ARN not configured, skipping schedule creation", { name, }); return null; } // Don't create schedules for times in the past if (scheduleTime <= new Date()) { logger.info("Schedule time is in the past, skipping", { name, scheduleTime, }); return null; } try { const command = new CreateScheduleCommand({ Name: name, GroupName: config.groupName, ScheduleExpression: `at(${scheduleTime.toISOString().replace(".000Z", "")})`, ScheduleExpressionTimezone: "UTC", FlexibleTimeWindow: { Mode: "OFF", }, Target: { Arn: config.lambdaArn, RoleArn: config.schedulerRoleArn, Input: JSON.stringify({ ...payload, scheduleName: name, }), }, ActionAfterCompletion: "DELETE", // Auto-delete after execution }); const result = await this.client.send(command); logger.info("Created EventBridge schedule", { name, scheduleTime, scheduleArn: result.ScheduleArn, }); return result.ScheduleArn; } catch (error) { // If schedule already exists, that's okay if (error.name === "ConflictException") { logger.info("Schedule already exists", { name }); return name; } logger.error("Failed to create EventBridge schedule", { name, error: error.message, }); throw error; } } /** * Delete a single EventBridge schedule. */ async deleteSchedule(name) { if (!this.initialized) { await this.initialize(); } const config = this.getConfig(); try { const command = new DeleteScheduleCommand({ Name: name, GroupName: config.groupName, }); await this.client.send(command); logger.info("Deleted EventBridge schedule", { name }); return true; } catch (error) { // If schedule doesn't exist, that's okay if (error.name === "ResourceNotFoundException") { logger.info("Schedule not found (already deleted)", { name }); return true; } logger.error("Failed to delete EventBridge schedule", { name, error: error.message, }); throw error; } } /** * Create all 4 condition check schedules for a rental. * Call this after a rental is confirmed. */ async createConditionCheckSchedules(rental) { if (!this.conditionCheckRemindersEnabled) { logger.debug( "EventBridge Scheduler disabled, skipping schedule creation" ); return null; } logger.info("Creating condition check schedules for rental", { rentalId: rental.id, }); const scheduleTimes = this.getScheduleTimes(rental); let schedulesCreated = 0; const checkTypes = [ "pre_rental_owner", "rental_start_renter", "rental_end_renter", "post_rental_owner", ]; for (const checkType of checkTypes) { const name = this.getScheduleName(rental.id, checkType); const scheduleTime = scheduleTimes[checkType]; try { const arn = await this.createSchedule(name, scheduleTime, { rentalId: rental.id, checkType, scheduledFor: scheduleTime.toISOString(), }); if (arn) { schedulesCreated++; } } catch (error) { logger.error("Failed to create schedule, continuing with others", { rentalId: rental.id, checkType, error: error.message, }); } } logger.info("Finished creating condition check schedules", { rentalId: rental.id, schedulesCreated, }); return schedulesCreated; } /** * Delete all condition check schedules for a rental. * Call this when a rental is cancelled. */ async deleteConditionCheckSchedules(rental) { if (!this.conditionCheckRemindersEnabled) { logger.debug( "EventBridge Scheduler disabled, skipping schedule deletion" ); return; } logger.info("Deleting condition check schedules for rental", { rentalId: rental.id, }); const checkTypes = [ "pre_rental_owner", "rental_start_renter", "rental_end_renter", "post_rental_owner", ]; for (const checkType of checkTypes) { const name = this.getScheduleName(rental.id, checkType); try { await this.deleteSchedule(name); } catch (error) { logger.error("Failed to delete schedule, continuing with others", { rentalId: rental.id, checkType, error: error.message, }); } } logger.info("Finished deleting condition check schedules", { rentalId: rental.id, }); } } // Export singleton instance module.exports = new EventBridgeSchedulerService();