372 lines
13 KiB
JavaScript
372 lines
13 KiB
JavaScript
const EmailClient = require("../core/EmailClient");
|
|
const TemplateManager = require("../core/TemplateManager");
|
|
const { formatEmailDate } = require("../core/emailUtils");
|
|
|
|
/**
|
|
* PaymentEmailService handles payment-related emails
|
|
* This service is responsible for:
|
|
* - Sending payment declined notifications to renters
|
|
* - Sending payment method updated notifications to owners
|
|
*/
|
|
class PaymentEmailService {
|
|
constructor() {
|
|
this.emailClient = new EmailClient();
|
|
this.templateManager = new TemplateManager();
|
|
this.initialized = false;
|
|
}
|
|
|
|
/**
|
|
* Initialize the payment email service
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async initialize() {
|
|
if (this.initialized) return;
|
|
|
|
await Promise.all([
|
|
this.emailClient.initialize(),
|
|
this.templateManager.initialize(),
|
|
]);
|
|
|
|
this.initialized = true;
|
|
console.log("Payment Email Service initialized successfully");
|
|
}
|
|
|
|
/**
|
|
* Send payment declined notification to renter
|
|
* @param {string} renterEmail - Renter's email address
|
|
* @param {Object} params - Email parameters
|
|
* @param {string} params.renterFirstName - Renter's first name
|
|
* @param {string} params.itemName - Item name
|
|
* @param {string} params.declineReason - User-friendly decline reason
|
|
* @param {string} params.rentalId - Rental ID
|
|
* @param {string} params.updatePaymentUrl - URL to update payment method
|
|
* @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
|
|
*/
|
|
async sendPaymentDeclinedNotification(renterEmail, params) {
|
|
if (!this.initialized) {
|
|
await this.initialize();
|
|
}
|
|
|
|
try {
|
|
const {
|
|
renterFirstName,
|
|
itemName,
|
|
declineReason,
|
|
updatePaymentUrl,
|
|
} = params;
|
|
|
|
const variables = {
|
|
renterFirstName: renterFirstName || "there",
|
|
itemName: itemName || "the item",
|
|
declineReason: declineReason || "Your payment could not be processed.",
|
|
updatePaymentUrl: updatePaymentUrl,
|
|
};
|
|
|
|
const htmlContent = await this.templateManager.renderTemplate(
|
|
"paymentDeclinedToRenter",
|
|
variables
|
|
);
|
|
|
|
return await this.emailClient.sendEmail(
|
|
renterEmail,
|
|
`Action Required: Payment Issue - ${itemName || "Your Rental"}`,
|
|
htmlContent
|
|
);
|
|
} catch (error) {
|
|
console.error("Failed to send payment declined notification:", error);
|
|
return { success: false, error: error.message };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send payment method updated notification to owner
|
|
* @param {string} ownerEmail - Owner's email address
|
|
* @param {Object} params - Email parameters
|
|
* @param {string} params.ownerFirstName - Owner's first name
|
|
* @param {string} params.itemName - Item name
|
|
* @param {string} params.rentalId - Rental ID
|
|
* @param {string} params.approvalUrl - URL to approve the rental
|
|
* @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
|
|
*/
|
|
async sendPaymentMethodUpdatedNotification(ownerEmail, params) {
|
|
if (!this.initialized) {
|
|
await this.initialize();
|
|
}
|
|
|
|
try {
|
|
const { ownerFirstName, itemName, approvalUrl } = params;
|
|
|
|
const variables = {
|
|
ownerFirstName: ownerFirstName || "there",
|
|
itemName: itemName || "the item",
|
|
approvalUrl: approvalUrl,
|
|
};
|
|
|
|
const htmlContent = await this.templateManager.renderTemplate(
|
|
"paymentMethodUpdatedToOwner",
|
|
variables
|
|
);
|
|
|
|
return await this.emailClient.sendEmail(
|
|
ownerEmail,
|
|
`Payment Method Updated - ${itemName || "Your Item"}`,
|
|
htmlContent
|
|
);
|
|
} catch (error) {
|
|
console.error("Failed to send payment method updated notification:", error);
|
|
return { success: false, error: error.message };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send payout failed notification to owner
|
|
* @param {string} ownerEmail - Owner's email address
|
|
* @param {Object} params - Email parameters
|
|
* @param {string} params.ownerName - Owner's name
|
|
* @param {number} params.payoutAmount - Payout amount in dollars
|
|
* @param {string} params.failureMessage - User-friendly failure message
|
|
* @param {string} params.actionRequired - Action the owner needs to take
|
|
* @param {string} params.failureCode - The Stripe failure code
|
|
* @param {boolean} params.requiresBankUpdate - Whether bank account update is needed
|
|
* @param {string} params.payoutSettingsUrl - URL to payout settings
|
|
* @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
|
|
*/
|
|
async sendPayoutFailedNotification(ownerEmail, params) {
|
|
if (!this.initialized) {
|
|
await this.initialize();
|
|
}
|
|
|
|
try {
|
|
const {
|
|
ownerName,
|
|
payoutAmount,
|
|
failureMessage,
|
|
actionRequired,
|
|
failureCode,
|
|
requiresBankUpdate,
|
|
payoutSettingsUrl,
|
|
} = params;
|
|
|
|
const variables = {
|
|
ownerName: ownerName || "there",
|
|
payoutAmount: payoutAmount?.toFixed(2) || "0.00",
|
|
failureMessage: failureMessage || "There was an issue with your payout.",
|
|
actionRequired: actionRequired || "Please check your bank account details.",
|
|
failureCode: failureCode || "unknown",
|
|
requiresBankUpdate: requiresBankUpdate || false,
|
|
payoutSettingsUrl: payoutSettingsUrl || process.env.FRONTEND_URL + "/settings/payouts",
|
|
};
|
|
|
|
const htmlContent = await this.templateManager.renderTemplate(
|
|
"payoutFailedToOwner",
|
|
variables
|
|
);
|
|
|
|
return await this.emailClient.sendEmail(
|
|
ownerEmail,
|
|
"Action Required: Payout Issue - Village Share",
|
|
htmlContent
|
|
);
|
|
} catch (error) {
|
|
console.error("Failed to send payout failed notification:", error);
|
|
return { success: false, error: error.message };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send notification when owner disconnects their Stripe account
|
|
* @param {string} ownerEmail - Owner's email address
|
|
* @param {Object} params - Email parameters
|
|
* @param {string} params.ownerName - Owner's name
|
|
* @param {boolean} params.hasPendingPayouts - Whether there are pending payouts
|
|
* @param {number} params.pendingPayoutCount - Number of pending payouts
|
|
* @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
|
|
*/
|
|
async sendAccountDisconnectedEmail(ownerEmail, params) {
|
|
if (!this.initialized) {
|
|
await this.initialize();
|
|
}
|
|
|
|
try {
|
|
const { ownerName, hasPendingPayouts, pendingPayoutCount } = params;
|
|
|
|
const variables = {
|
|
ownerName: ownerName || "there",
|
|
hasPendingPayouts: hasPendingPayouts || false,
|
|
pendingPayoutCount: pendingPayoutCount || 0,
|
|
reconnectUrl: `${process.env.FRONTEND_URL}/settings/payouts`,
|
|
};
|
|
|
|
const htmlContent = await this.templateManager.renderTemplate(
|
|
"accountDisconnectedToOwner",
|
|
variables
|
|
);
|
|
|
|
return await this.emailClient.sendEmail(
|
|
ownerEmail,
|
|
"Your payout account has been disconnected - Village Share",
|
|
htmlContent
|
|
);
|
|
} catch (error) {
|
|
console.error("Failed to send account disconnected email:", error);
|
|
return { success: false, error: error.message };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send notification when owner's payouts are disabled due to requirements
|
|
* @param {string} ownerEmail - Owner's email address
|
|
* @param {Object} params - Email parameters
|
|
* @param {string} params.ownerName - Owner's name
|
|
* @param {string} params.disabledReason - Human-readable reason for disabling
|
|
* @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
|
|
*/
|
|
async sendPayoutsDisabledEmail(ownerEmail, params) {
|
|
if (!this.initialized) {
|
|
await this.initialize();
|
|
}
|
|
|
|
try {
|
|
const { ownerName, disabledReason } = params;
|
|
|
|
const variables = {
|
|
ownerName: ownerName || "there",
|
|
disabledReason:
|
|
disabledReason ||
|
|
"Additional verification is required for your account.",
|
|
earningsUrl: `${process.env.FRONTEND_URL}/earnings`,
|
|
};
|
|
|
|
const htmlContent = await this.templateManager.renderTemplate(
|
|
"payoutsDisabledToOwner",
|
|
variables
|
|
);
|
|
|
|
return await this.emailClient.sendEmail(
|
|
ownerEmail,
|
|
"Action Required: Your payouts have been paused - Village Share",
|
|
htmlContent
|
|
);
|
|
} catch (error) {
|
|
console.error("Failed to send payouts disabled email:", error);
|
|
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;
|