Files
rentall-app/backend/services/refundService.js
2025-09-22 22:02:08 -04:00

229 lines
6.7 KiB
JavaScript

const { Rental } = require("../models");
const StripeService = require("./stripeService");
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
if (rental.status === "active") {
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,
};
}
// Check payment status - allow cancellation for both paid and free rentals
if (rental.paymentStatus !== "paid" && rental.paymentStatus !== "not_required") {
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) {
console.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
console.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",
});
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;