failed payment method handling
This commit is contained in:
@@ -51,7 +51,14 @@ class TemplateManager {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async loadEmailTemplates() {
|
||||
const templatesDir = path.join(__dirname, "..", "..", "..", "templates", "emails");
|
||||
const templatesDir = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"templates",
|
||||
"emails"
|
||||
);
|
||||
|
||||
// Critical templates that must load for the app to function
|
||||
const criticalTemplates = [
|
||||
@@ -95,6 +102,8 @@ class TemplateManager {
|
||||
"forumItemRequestNotification.html",
|
||||
"forumPostDeletionToAuthor.html",
|
||||
"forumCommentDeletionToAuthor.html",
|
||||
"paymentDeclinedToRenter.html",
|
||||
"paymentMethodUpdatedToOwner.html",
|
||||
];
|
||||
|
||||
const failedTemplates = [];
|
||||
@@ -129,7 +138,9 @@ class TemplateManager {
|
||||
|
||||
if (missingCriticalTemplates.length > 0) {
|
||||
const error = new Error(
|
||||
`Critical email templates failed to load: ${missingCriticalTemplates.join(", ")}`
|
||||
`Critical email templates failed to load: ${missingCriticalTemplates.join(
|
||||
", "
|
||||
)}`
|
||||
);
|
||||
error.missingTemplates = missingCriticalTemplates;
|
||||
throw error;
|
||||
@@ -138,7 +149,9 @@ class TemplateManager {
|
||||
// Warn if non-critical templates failed
|
||||
if (failedTemplates.length > 0) {
|
||||
console.warn(
|
||||
`⚠️ Non-critical templates failed to load: ${failedTemplates.join(", ")}`
|
||||
`⚠️ Non-critical templates failed to load: ${failedTemplates.join(
|
||||
", "
|
||||
)}`
|
||||
);
|
||||
console.warn("These templates will use fallback versions");
|
||||
}
|
||||
@@ -483,6 +496,36 @@ class TemplateManager {
|
||||
<p>Please review this feedback and take appropriate action if needed.</p>
|
||||
`
|
||||
),
|
||||
|
||||
paymentDeclinedToRenter: baseTemplate.replace(
|
||||
"{{content}}",
|
||||
`
|
||||
<p>Hi {{renterFirstName}},</p>
|
||||
<h2>Payment Issue with Your Rental Request</h2>
|
||||
<p>The owner tried to approve your rental for <strong>{{itemName}}</strong>, but there was an issue processing your payment.</p>
|
||||
<h3>What Happened</h3>
|
||||
<p>{{declineReason}}</p>
|
||||
<div class="info-box">
|
||||
<p><strong>What You Can Do</strong></p>
|
||||
<p>Please update your payment method so the owner can complete the approval of your rental request.</p>
|
||||
</div>
|
||||
<p>Once you update your payment method, the owner will be notified and can try to approve your rental again.</p>
|
||||
`
|
||||
),
|
||||
|
||||
paymentMethodUpdatedToOwner: baseTemplate.replace(
|
||||
"{{content}}",
|
||||
`
|
||||
<p>Hi {{ownerFirstName}},</p>
|
||||
<h2>Payment Method Updated</h2>
|
||||
<p>The renter has updated their payment method for the rental of <strong>{{itemName}}</strong>.</p>
|
||||
<div class="info-box">
|
||||
<p><strong>Ready to Approve</strong></p>
|
||||
<p>You can now try approving the rental request again. The renter's new payment method will be charged when you approve.</p>
|
||||
</div>
|
||||
<p style="text-align: center;"><a href="{{approvalUrl}}" class="button">Review & Approve Rental</a></p>
|
||||
`
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
121
backend/services/email/domain/PaymentEmailService.js
Normal file
121
backend/services/email/domain/PaymentEmailService.js
Normal file
@@ -0,0 +1,121 @@
|
||||
const EmailClient = require("../core/EmailClient");
|
||||
const TemplateManager = require("../core/TemplateManager");
|
||||
|
||||
/**
|
||||
* 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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PaymentEmailService;
|
||||
@@ -7,6 +7,7 @@ const RentalFlowEmailService = require("./domain/RentalFlowEmailService");
|
||||
const RentalReminderEmailService = require("./domain/RentalReminderEmailService");
|
||||
const UserEngagementEmailService = require("./domain/UserEngagementEmailService");
|
||||
const AlphaInvitationEmailService = require("./domain/AlphaInvitationEmailService");
|
||||
const PaymentEmailService = require("./domain/PaymentEmailService");
|
||||
|
||||
/**
|
||||
* EmailServices aggregates all domain-specific email services
|
||||
@@ -24,6 +25,7 @@ class EmailServices {
|
||||
this.rentalReminder = new RentalReminderEmailService();
|
||||
this.userEngagement = new UserEngagementEmailService();
|
||||
this.alphaInvitation = new AlphaInvitationEmailService();
|
||||
this.payment = new PaymentEmailService();
|
||||
|
||||
this.initialized = false;
|
||||
}
|
||||
@@ -45,6 +47,7 @@ class EmailServices {
|
||||
this.rentalReminder.initialize(),
|
||||
this.userEngagement.initialize(),
|
||||
this.alphaInvitation.initialize(),
|
||||
this.payment.initialize(),
|
||||
]);
|
||||
|
||||
this.initialized = true;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY);
|
||||
const logger = require("../utils/logger");
|
||||
const { parseStripeError, PaymentError } = require("../utils/stripeErrors");
|
||||
|
||||
class StripeService {
|
||||
|
||||
@@ -184,8 +185,21 @@ class StripeService {
|
||||
amountCharged: amount, // Original amount in dollars
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error("Error charging payment method", { error: error.message, stack: error.stack });
|
||||
throw error;
|
||||
// Parse Stripe error into structured format
|
||||
const parsedError = parseStripeError(error);
|
||||
|
||||
logger.error("Payment failed", {
|
||||
code: parsedError.code,
|
||||
ownerMessage: parsedError.ownerMessage,
|
||||
originalError: parsedError._originalMessage,
|
||||
stripeCode: parsedError._stripeCode,
|
||||
paymentMethodId,
|
||||
customerId,
|
||||
amount,
|
||||
stack: error.stack,
|
||||
});
|
||||
|
||||
throw new PaymentError(parsedError);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,6 +219,15 @@ class StripeService {
|
||||
}
|
||||
|
||||
|
||||
static async getPaymentMethod(paymentMethodId) {
|
||||
try {
|
||||
return await stripe.paymentMethods.retrieve(paymentMethodId);
|
||||
} catch (error) {
|
||||
logger.error("Error retrieving payment method", { error: error.message, paymentMethodId });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async createSetupCheckoutSession({ customerId, metadata = {} }) {
|
||||
try {
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
|
||||
Reference in New Issue
Block a user