condition check lambda
This commit is contained in:
292
backend/services/eventBridgeSchedulerService.js
Normal file
292
backend/services/eventBridgeSchedulerService.js
Normal file
@@ -0,0 +1,292 @@
|
||||
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();
|
||||
@@ -1,5 +1,6 @@
|
||||
const { Rental } = require("../models");
|
||||
const StripeService = require("./stripeService");
|
||||
const EventBridgeSchedulerService = require("./eventBridgeSchedulerService");
|
||||
const { isActive } = require("../utils/rentalStatus");
|
||||
const logger = require("../utils/logger");
|
||||
|
||||
@@ -187,6 +188,17 @@ class RefundService {
|
||||
payoutStatus: "pending",
|
||||
});
|
||||
|
||||
// Delete condition check schedules since rental is cancelled
|
||||
try {
|
||||
await EventBridgeSchedulerService.deleteConditionCheckSchedules(updatedRental);
|
||||
} catch (schedulerError) {
|
||||
logger.error("Failed to delete condition check schedules", {
|
||||
error: schedulerError.message,
|
||||
rentalId: updatedRental.id,
|
||||
});
|
||||
// Don't fail the cancellation - schedule cleanup is non-critical
|
||||
}
|
||||
|
||||
return {
|
||||
rental: updatedRental,
|
||||
refund: {
|
||||
|
||||
Reference in New Issue
Block a user