3D Secure handling
This commit is contained in:
@@ -1209,6 +1209,50 @@ class RentalFlowEmailService {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send authentication required email to renter when 3DS verification is needed
|
||||
* This is sent when the owner approves a rental but the renter's bank requires
|
||||
* additional verification (3D Secure) to complete the payment.
|
||||
*
|
||||
* @param {string} email - Renter's email address
|
||||
* @param {Object} data - Email data
|
||||
* @param {string} data.renterName - Renter's first name
|
||||
* @param {string} data.itemName - Name of the item being rented
|
||||
* @param {string} data.ownerName - Owner's first name
|
||||
* @param {number} data.amount - Total rental amount
|
||||
* @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
|
||||
*/
|
||||
async sendAuthenticationRequiredEmail(email, data) {
|
||||
if (!this.initialized) {
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
try {
|
||||
const { renterName, itemName, ownerName, amount } = data;
|
||||
|
||||
const variables = {
|
||||
renterName: renterName || "there",
|
||||
itemName: itemName || "the item",
|
||||
ownerName: ownerName || "The owner",
|
||||
amount: typeof amount === "number" ? amount.toFixed(2) : "0.00",
|
||||
};
|
||||
|
||||
const htmlContent = await this.templateManager.renderTemplate(
|
||||
"authenticationRequiredToRenter",
|
||||
variables
|
||||
);
|
||||
|
||||
return await this.emailClient.sendEmail(
|
||||
email,
|
||||
`Action Required: Complete payment for ${itemName}`,
|
||||
htmlContent
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to send authentication required email:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RentalFlowEmailService;
|
||||
|
||||
@@ -3,14 +3,16 @@ const logger = require("../utils/logger");
|
||||
const { parseStripeError, PaymentError } = require("../utils/stripeErrors");
|
||||
|
||||
class StripeService {
|
||||
|
||||
static async getCheckoutSession(sessionId) {
|
||||
try {
|
||||
return await stripe.checkout.sessions.retrieve(sessionId, {
|
||||
expand: ['setup_intent', 'setup_intent.payment_method']
|
||||
expand: ["setup_intent", "setup_intent.payment_method"],
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error retrieving checkout session", { error: error.message, stack: error.stack });
|
||||
logger.error("Error retrieving checkout session", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -28,7 +30,10 @@ class StripeService {
|
||||
|
||||
return account;
|
||||
} catch (error) {
|
||||
logger.error("Error creating connected account", { error: error.message, stack: error.stack });
|
||||
logger.error("Error creating connected account", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -44,7 +49,10 @@ class StripeService {
|
||||
|
||||
return accountLink;
|
||||
} catch (error) {
|
||||
logger.error("Error creating account link", { error: error.message, stack: error.stack });
|
||||
logger.error("Error creating account link", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -60,7 +68,10 @@ class StripeService {
|
||||
requirements: account.requirements,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error("Error retrieving account status", { error: error.message, stack: error.stack });
|
||||
logger.error("Error retrieving account status", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -76,7 +87,10 @@ class StripeService {
|
||||
|
||||
return accountSession;
|
||||
} catch (error) {
|
||||
logger.error("Error creating account session", { error: error.message, stack: error.stack });
|
||||
logger.error("Error creating account session", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -97,7 +111,10 @@ class StripeService {
|
||||
|
||||
return transfer;
|
||||
} catch (error) {
|
||||
logger.error("Error creating transfer", { error: error.message, stack: error.stack });
|
||||
logger.error("Error creating transfer", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -118,7 +135,10 @@ class StripeService {
|
||||
|
||||
return refund;
|
||||
} catch (error) {
|
||||
logger.error("Error creating refund", { error: error.message, stack: error.stack });
|
||||
logger.error("Error creating refund", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -127,12 +147,20 @@ class StripeService {
|
||||
try {
|
||||
return await stripe.refunds.retrieve(refundId);
|
||||
} catch (error) {
|
||||
logger.error("Error retrieving refund", { error: error.message, stack: error.stack });
|
||||
logger.error("Error retrieving refund", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async chargePaymentMethod(paymentMethodId, amount, customerId, metadata = {}) {
|
||||
static async chargePaymentMethod(
|
||||
paymentMethodId,
|
||||
amount,
|
||||
customerId,
|
||||
metadata = {}
|
||||
) {
|
||||
try {
|
||||
// Create a payment intent with the stored payment method
|
||||
const paymentIntent = await stripe.paymentIntents.create({
|
||||
@@ -142,49 +170,71 @@ class StripeService {
|
||||
customer: customerId, // Include customer ID
|
||||
confirm: true, // Automatically confirm the payment
|
||||
off_session: true, // Indicate this is an off-session payment
|
||||
return_url: `${process.env.FRONTEND_URL || 'http://localhost:3000'}/payment-complete`,
|
||||
return_url: `${
|
||||
process.env.FRONTEND_URL || "http://localhost:3000"
|
||||
}/complete-payment`,
|
||||
metadata,
|
||||
expand: ['charges.data.payment_method_details'], // Expand to get payment method details
|
||||
expand: ["latest_charge.payment_method_details"], // Expand to get payment method details
|
||||
});
|
||||
|
||||
// Extract payment method details from charges
|
||||
const charge = paymentIntent.charges?.data?.[0];
|
||||
// Check if additional authentication is required
|
||||
if (paymentIntent.status === "requires_action") {
|
||||
return {
|
||||
status: "requires_action",
|
||||
requiresAction: true,
|
||||
paymentIntentId: paymentIntent.id,
|
||||
clientSecret: paymentIntent.client_secret,
|
||||
};
|
||||
}
|
||||
|
||||
// Extract payment method details from latest_charge
|
||||
const charge = paymentIntent.latest_charge;
|
||||
const paymentMethodDetails = charge?.payment_method_details;
|
||||
|
||||
// Build payment method info object
|
||||
let paymentMethod = null;
|
||||
if (paymentMethodDetails) {
|
||||
const type = paymentMethodDetails.type;
|
||||
if (type === 'card') {
|
||||
if (type === "card") {
|
||||
paymentMethod = {
|
||||
type: 'card',
|
||||
brand: paymentMethodDetails.card?.brand || 'card',
|
||||
last4: paymentMethodDetails.card?.last4 || '****',
|
||||
type: "card",
|
||||
brand: paymentMethodDetails.card?.brand || "card",
|
||||
last4: paymentMethodDetails.card?.last4 || "****",
|
||||
};
|
||||
} else if (type === 'us_bank_account') {
|
||||
} else if (type === "us_bank_account") {
|
||||
paymentMethod = {
|
||||
type: 'bank',
|
||||
brand: 'bank_account',
|
||||
last4: paymentMethodDetails.us_bank_account?.last4 || '****',
|
||||
type: "bank",
|
||||
brand: "bank_account",
|
||||
last4: paymentMethodDetails.us_bank_account?.last4 || "****",
|
||||
};
|
||||
} else {
|
||||
paymentMethod = {
|
||||
type: type || 'unknown',
|
||||
brand: type || 'payment',
|
||||
type: type || "unknown",
|
||||
brand: type || "payment",
|
||||
last4: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: "succeeded",
|
||||
paymentIntentId: paymentIntent.id,
|
||||
status: paymentIntent.status,
|
||||
clientSecret: paymentIntent.client_secret,
|
||||
paymentMethod: paymentMethod,
|
||||
chargedAt: new Date(paymentIntent.created * 1000), // Convert Unix timestamp to Date
|
||||
amountCharged: amount, // Original amount in dollars
|
||||
};
|
||||
} catch (error) {
|
||||
// Handle authentication_required error (thrown for off-session 3DS)
|
||||
if (error.code === "authentication_required") {
|
||||
return {
|
||||
status: "requires_action",
|
||||
requiresAction: true,
|
||||
paymentIntentId: error.payment_intent?.id,
|
||||
clientSecret: error.payment_intent?.client_secret,
|
||||
};
|
||||
}
|
||||
|
||||
// Parse Stripe error into structured format
|
||||
const parsedError = parseStripeError(error);
|
||||
|
||||
@@ -213,17 +263,22 @@ class StripeService {
|
||||
|
||||
return customer;
|
||||
} catch (error) {
|
||||
logger.error("Error creating customer", { error: error.message, stack: error.stack });
|
||||
logger.error("Error creating customer", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static async getPaymentMethod(paymentMethodId) {
|
||||
try {
|
||||
return await stripe.paymentMethods.retrieve(paymentMethodId);
|
||||
} catch (error) {
|
||||
logger.error("Error retrieving payment method", { error: error.message, paymentMethodId });
|
||||
logger.error("Error retrieving payment method", {
|
||||
error: error.message,
|
||||
paymentMethodId,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -232,19 +287,28 @@ class StripeService {
|
||||
try {
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
customer: customerId,
|
||||
payment_method_types: ['card', 'us_bank_account', 'link'],
|
||||
mode: 'setup',
|
||||
ui_mode: 'embedded',
|
||||
redirect_on_completion: 'never',
|
||||
payment_method_types: ["card", "us_bank_account", "link"],
|
||||
mode: "setup",
|
||||
ui_mode: "embedded",
|
||||
redirect_on_completion: "never",
|
||||
// Configure for off-session usage - triggers 3DS during setup
|
||||
payment_method_options: {
|
||||
card: {
|
||||
request_three_d_secure: "any",
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
type: 'payment_method_setup',
|
||||
...metadata
|
||||
}
|
||||
type: "payment_method_setup",
|
||||
...metadata,
|
||||
},
|
||||
});
|
||||
|
||||
return session;
|
||||
} catch (error) {
|
||||
logger.error("Error creating setup checkout session", { error: error.message, stack: error.stack });
|
||||
logger.error("Error creating setup checkout session", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user