const EmailClient = require("../core/EmailClient"); const TemplateManager = require("../core/TemplateManager"); const { formatEmailDate } = require("../core/emailUtils"); const logger = require("../../../utils/logger"); /** * 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} */ async initialize() { if (this.initialized) return; await Promise.all([ this.emailClient.initialize(), this.templateManager.initialize(), ]); this.initialized = true; logger.info("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) { logger.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) { logger.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) { logger.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) { logger.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) { logger.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.CUSTOMER_SUPPORT_EMAIL; return await this.emailClient.sendEmail( adminEmail, `URGENT: Payment Dispute - Rental #${disputeData.rentalId}`, htmlContent, ); } catch (error) { logger.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.CUSTOMER_SUPPORT_EMAIL; return await this.emailClient.sendEmail( adminEmail, `ACTION REQUIRED: Dispute Lost - Owner Already Paid - Rental #${disputeData.rentalId}`, htmlContent, ); } catch (error) { logger.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;