handling stripe disputes/chargeback where renter disputes the charge through their credit card company or bank

This commit is contained in:
jackiettran
2026-01-08 17:23:55 -05:00
parent 5248c3dc39
commit 3042a9007f
9 changed files with 1119 additions and 1 deletions

View File

@@ -1,5 +1,6 @@
const EmailClient = require("../core/EmailClient");
const TemplateManager = require("../core/TemplateManager");
const { formatEmailDate } = require("../core/emailUtils");
/**
* PaymentEmailService handles payment-related emails
@@ -171,6 +172,120 @@ class PaymentEmailService {
return { success: false, error: error.message };
}
}
/**
* Send dispute alert to platform admin
* Called when a new dispute is opened
* @param {Object} disputeData - Dispute information
* @param {string} disputeData.rentalId - Rental ID
* @param {number} disputeData.amount - Disputed amount in dollars
* @param {string} disputeData.reason - Stripe dispute reason code
* @param {Date} disputeData.evidenceDueBy - Evidence submission deadline
* @param {string} disputeData.renterEmail - Renter's email
* @param {string} disputeData.renterName - Renter's name
* @param {string} disputeData.ownerEmail - Owner's email
* @param {string} disputeData.ownerName - Owner's name
* @param {string} disputeData.itemName - Item name
* @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
*/
async sendDisputeAlertEmail(disputeData) {
if (!this.initialized) {
await this.initialize();
}
try {
const variables = {
rentalId: disputeData.rentalId,
itemName: disputeData.itemName || "Unknown Item",
amount: disputeData.amount.toFixed(2),
reason: this.formatDisputeReason(disputeData.reason),
evidenceDueBy: formatEmailDate(disputeData.evidenceDueBy),
renterName: disputeData.renterName || "Unknown",
renterEmail: disputeData.renterEmail || "Unknown",
ownerName: disputeData.ownerName || "Unknown",
ownerEmail: disputeData.ownerEmail || "Unknown",
};
const htmlContent = await this.templateManager.renderTemplate(
"disputeAlertToAdmin",
variables
);
// Send to admin email (configure in env)
const adminEmail = process.env.ADMIN_EMAIL || process.env.SES_FROM_EMAIL;
return await this.emailClient.sendEmail(
adminEmail,
`URGENT: Payment Dispute - Rental #${disputeData.rentalId}`,
htmlContent
);
} catch (error) {
console.error("Failed to send dispute alert email:", error);
return { success: false, error: error.message };
}
}
/**
* Send alert when dispute is lost and owner was already paid
* Flags for manual review to decide on potential clawback
* @param {Object} disputeData - Dispute information
* @param {string} disputeData.rentalId - Rental ID
* @param {number} disputeData.amount - Lost dispute amount in dollars
* @param {number} disputeData.ownerPayoutAmount - Amount already paid to owner
* @param {string} disputeData.ownerEmail - Owner's email
* @param {string} disputeData.ownerName - Owner's name
* @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
*/
async sendDisputeLostAlertEmail(disputeData) {
if (!this.initialized) {
await this.initialize();
}
try {
const variables = {
rentalId: disputeData.rentalId,
amount: disputeData.amount.toFixed(2),
ownerPayoutAmount: parseFloat(disputeData.ownerPayoutAmount || 0).toFixed(2),
ownerName: disputeData.ownerName || "Unknown",
ownerEmail: disputeData.ownerEmail || "Unknown",
};
const htmlContent = await this.templateManager.renderTemplate(
"disputeLostAlertToAdmin",
variables
);
const adminEmail = process.env.ADMIN_EMAIL || process.env.SES_FROM_EMAIL;
return await this.emailClient.sendEmail(
adminEmail,
`ACTION REQUIRED: Dispute Lost - Owner Already Paid - Rental #${disputeData.rentalId}`,
htmlContent
);
} catch (error) {
console.error("Failed to send dispute lost alert email:", error);
return { success: false, error: error.message };
}
}
/**
* Format Stripe dispute reason codes to human-readable text
* @param {string} reason - Stripe dispute reason code
* @returns {string} Human-readable reason
*/
formatDisputeReason(reason) {
const reasonMap = {
duplicate: "Duplicate charge",
fraudulent: "Fraudulent transaction",
subscription_canceled: "Subscription canceled",
product_unacceptable: "Product unacceptable",
product_not_received: "Product not received",
unrecognized: "Unrecognized charge",
credit_not_processed: "Credit not processed",
general: "General dispute",
};
return reasonMap[reason] || reason || "Unknown reason";
}
}
module.exports = PaymentEmailService;