293 lines
7.4 KiB
JavaScript
293 lines
7.4 KiB
JavaScript
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();
|