const { Rental } = require("../models"); const StripeService = require("./stripeService"); const EventBridgeSchedulerService = require("./eventBridgeSchedulerService"); const { isActive } = require("../utils/rentalStatus"); const logger = require("../utils/logger"); class RefundService { /** * Calculate refund amount based on policy and who cancelled * @param {Object} rental - Rental instance * @param {string} cancelledBy - 'renter' or 'owner' * @returns {Object} - { refundAmount, refundPercentage, reason } */ static calculateRefundAmount(rental, cancelledBy) { const totalAmount = rental.totalAmount; let refundPercentage = 0; let reason = ""; if (cancelledBy === "owner") { // Owner cancellation = full refund refundPercentage = 1.0; reason = "Full refund - cancelled by owner"; } else if (cancelledBy === "renter") { // Calculate based on time until rental start const now = new Date(); const startDateTime = new Date(rental.startDateTime); const hoursUntilStart = (startDateTime - now) / (1000 * 60 * 60); if (hoursUntilStart < 24) { refundPercentage = 0.0; reason = "No refund - cancelled within 24 hours of start time"; } else if (hoursUntilStart < 48) { refundPercentage = 0.5; reason = "50% refund - cancelled between 24-48 hours of start time"; } else { refundPercentage = 1.0; reason = "Full refund - cancelled more than 48 hours before start time"; } } const refundAmount = parseFloat((totalAmount * refundPercentage).toFixed(2)); return { refundAmount, refundPercentage, reason, }; } /** * Validate if a rental can be cancelled * @param {Object} rental - Rental instance * @param {string} userId - User ID attempting to cancel * @returns {Object} - { canCancel, reason, cancelledBy } */ static validateCancellationEligibility(rental, userId) { // Check if rental is already cancelled if (rental.status === "cancelled") { return { canCancel: false, reason: "Rental is already cancelled", cancelledBy: null, }; } // Check if rental is completed if (rental.status === "completed") { return { canCancel: false, reason: "Cannot cancel completed rental", cancelledBy: null, }; } // Check if rental is active (computed from confirmed + start time passed) if (isActive(rental)) { return { canCancel: false, reason: "Cannot cancel active rental", cancelledBy: null, }; } // Check if user has permission to cancel let cancelledBy = null; if (rental.renterId === userId) { cancelledBy = "renter"; } else if (rental.ownerId === userId) { cancelledBy = "owner"; } else { return { canCancel: false, reason: "You are not authorized to cancel this rental", cancelledBy: null, }; } // Allow cancellation for pending rentals (before owner approval) or paid/free rentals const isPendingRequest = rental.status === "pending"; const isPaymentSettled = rental.paymentStatus === "paid" || rental.paymentStatus === "not_required"; if (!isPendingRequest && !isPaymentSettled) { return { canCancel: false, reason: "Cannot cancel rental that hasn't been paid", cancelledBy: null, }; } return { canCancel: true, reason: "Cancellation allowed", cancelledBy, }; } /** * Process the full cancellation and refund * @param {string} rentalId - Rental ID * @param {string} userId - User ID cancelling * @param {string} cancellationReason - Optional reason provided by user * @returns {Object} - Updated rental with refund information */ static async processCancellation(rentalId, userId, cancellationReason = null) { const rental = await Rental.findByPk(rentalId); if (!rental) { throw new Error("Rental not found"); } // Validate cancellation eligibility const eligibility = this.validateCancellationEligibility(rental, userId); if (!eligibility.canCancel) { throw new Error(eligibility.reason); } // Calculate refund amount const refundCalculation = this.calculateRefundAmount( rental, eligibility.cancelledBy ); let stripeRefundId = null; let refundProcessedAt = null; // Process refund with Stripe if amount > 0 and we have payment intent ID if ( refundCalculation.refundAmount > 0 && rental.stripePaymentIntentId ) { try { const refund = await StripeService.createRefund({ paymentIntentId: rental.stripePaymentIntentId, amount: refundCalculation.refundAmount, metadata: { rentalId: rental.id, cancelledBy: eligibility.cancelledBy, refundReason: refundCalculation.reason, }, }); stripeRefundId = refund.id; refundProcessedAt = new Date(); } catch (error) { logger.error("Error processing Stripe refund", { error }); throw new Error(`Failed to process refund: ${error.message}`); } } else if (refundCalculation.refundAmount > 0) { // Log warning if we should refund but don't have payment intent logger.warn( "Refund amount calculated but no payment intent ID for rental", { rentalId } ); } // Update rental with cancellation and refund info const updatedRental = await rental.update({ status: "cancelled", cancelledBy: eligibility.cancelledBy, cancelledAt: new Date(), refundAmount: refundCalculation.refundAmount, refundProcessedAt, refundReason: cancellationReason || refundCalculation.reason, stripeRefundId, // Reset payout status since rental is cancelled 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: { amount: refundCalculation.refundAmount, percentage: refundCalculation.refundPercentage, reason: refundCalculation.reason, processed: !!refundProcessedAt, stripeRefundId, }, }; } /** * Get refund preview without processing * @param {string} rentalId - Rental ID * @param {string} userId - User ID requesting preview * @returns {Object} - Preview of refund calculation */ static async getRefundPreview(rentalId, userId) { const rental = await Rental.findByPk(rentalId); if (!rental) { throw new Error("Rental not found"); } const eligibility = this.validateCancellationEligibility(rental, userId); if (!eligibility.canCancel) { throw new Error(eligibility.reason); } const refundCalculation = this.calculateRefundAmount( rental, eligibility.cancelledBy ); return { canCancel: true, cancelledBy: eligibility.cancelledBy, refundAmount: refundCalculation.refundAmount, refundPercentage: refundCalculation.refundPercentage, reason: refundCalculation.reason, totalAmount: rental.totalAmount, }; } } module.exports = RefundService;