229 lines
6.7 KiB
JavaScript
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; |