Files
rentall-app/backend/routes/stripe.js

345 lines
10 KiB
JavaScript

const express = require("express");
const { authenticateToken, requireVerifiedEmail } = require("../middleware/auth");
const { User, Item } = require("../models");
const StripeService = require("../services/stripeService");
const StripeWebhookService = require("../services/stripeWebhookService");
const emailServices = require("../services/email");
const logger = require("../utils/logger");
const router = express.Router();
// Get checkout session status
router.get("/checkout-session/:sessionId", async (req, res, next) => {
try {
const { sessionId } = req.params;
const session = await StripeService.getCheckoutSession(sessionId);
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("Stripe checkout session retrieved", {
sessionId: sessionId,
status: session.status,
payment_status: session.payment_status,
metadata: session.metadata,
});
res.json({
status: session.status,
payment_status: session.payment_status,
customer_email: session.customer_details?.email,
setup_intent: session.setup_intent,
metadata: session.metadata,
});
} catch (error) {
const reqLogger = logger.withRequestId(req.id);
reqLogger.error("Stripe checkout session retrieval failed", {
error: error.message,
stack: error.stack,
sessionId: req.params.sessionId,
});
next(error);
}
});
// Create connected account
router.post("/accounts", authenticateToken, requireVerifiedEmail, async (req, res, next) => {
try {
const user = await User.findByPk(req.user.id);
if (!user) {
return res.status(404).json({ error: "User not found" });
}
// Check if user already has a connected account
if (user.stripeConnectedAccountId) {
return res
.status(400)
.json({ error: "User already has a connected account" });
}
// Create connected account
const account = await StripeService.createConnectedAccount({
email: user.email,
country: "US", // You may want to make this configurable
});
// Update user with account ID
await user.update({
stripeConnectedAccountId: account.id,
});
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("Stripe connected account created", {
userId: req.user.id,
stripeConnectedAccountId: account.id,
});
res.json({
stripeConnectedAccountId: account.id,
success: true,
});
} catch (error) {
const reqLogger = logger.withRequestId(req.id);
reqLogger.error("Stripe connected account creation failed", {
error: error.message,
stack: error.stack,
userId: req.user.id,
});
next(error);
}
});
// Generate onboarding link
router.post("/account-links", authenticateToken, requireVerifiedEmail, async (req, res, next) => {
let user = null;
try {
user = await User.findByPk(req.user.id);
if (!user || !user.stripeConnectedAccountId) {
return res.status(400).json({ error: "No connected account found" });
}
const { refreshUrl, returnUrl } = req.body;
if (!refreshUrl || !returnUrl) {
return res
.status(400)
.json({ error: "refreshUrl and returnUrl are required" });
}
const accountLink = await StripeService.createAccountLink(
user.stripeConnectedAccountId,
refreshUrl,
returnUrl
);
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("Stripe account link created", {
userId: req.user.id,
stripeConnectedAccountId: user.stripeConnectedAccountId,
expiresAt: accountLink.expires_at,
});
res.json({
url: accountLink.url,
expiresAt: accountLink.expires_at,
});
} catch (error) {
const reqLogger = logger.withRequestId(req.id);
reqLogger.error("Stripe account link creation failed", {
error: error.message,
stack: error.stack,
userId: req.user.id,
stripeConnectedAccountId: user?.stripeConnectedAccountId,
});
next(error);
}
});
// Create account session for embedded onboarding
router.post("/account-sessions", authenticateToken, requireVerifiedEmail, async (req, res, next) => {
let user = null;
try {
user = await User.findByPk(req.user.id);
if (!user || !user.stripeConnectedAccountId) {
return res.status(400).json({ error: "No connected account found" });
}
const accountSession = await StripeService.createAccountSession(
user.stripeConnectedAccountId
);
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("Stripe account session created", {
userId: req.user.id,
stripeConnectedAccountId: user.stripeConnectedAccountId,
});
res.json({
clientSecret: accountSession.client_secret,
});
} catch (error) {
const reqLogger = logger.withRequestId(req.id);
reqLogger.error("Stripe account session creation failed", {
error: error.message,
stack: error.stack,
userId: req.user.id,
stripeConnectedAccountId: user?.stripeConnectedAccountId,
});
next(error);
}
});
// Get account status with reconciliation
router.get("/account-status", authenticateToken, async (req, res, next) => {
let user = null;
try {
user = await User.findByPk(req.user.id);
if (!user || !user.stripeConnectedAccountId) {
return res.status(400).json({ error: "No connected account found" });
}
const accountStatus = await StripeService.getAccountStatus(
user.stripeConnectedAccountId
);
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("Stripe account status retrieved", {
userId: req.user.id,
stripeConnectedAccountId: user.stripeConnectedAccountId,
detailsSubmitted: accountStatus.details_submitted,
payoutsEnabled: accountStatus.payouts_enabled,
});
// Reconciliation: Compare fetched status with stored User fields
const previousPayoutsEnabled = user.stripePayoutsEnabled;
const currentPayoutsEnabled = accountStatus.payouts_enabled;
const requirements = accountStatus.requirements || {};
// Check if status has changed and needs updating
const statusChanged =
previousPayoutsEnabled !== currentPayoutsEnabled ||
JSON.stringify(user.stripeRequirementsCurrentlyDue || []) !==
JSON.stringify(requirements.currently_due || []);
if (statusChanged) {
reqLogger.info("Reconciling account status from API call", {
userId: req.user.id,
previousPayoutsEnabled,
currentPayoutsEnabled,
previousCurrentlyDue: user.stripeRequirementsCurrentlyDue?.length || 0,
newCurrentlyDue: requirements.currently_due?.length || 0,
});
// Update user with current status
await user.update({
stripePayoutsEnabled: currentPayoutsEnabled,
stripeRequirementsCurrentlyDue: requirements.currently_due || [],
stripeRequirementsPastDue: requirements.past_due || [],
stripeDisabledReason: requirements.disabled_reason || null,
stripeRequirementsLastUpdated: new Date(),
});
// If payouts just became disabled (true -> false), send notification
if (!currentPayoutsEnabled && previousPayoutsEnabled) {
reqLogger.warn("Payouts disabled detected during reconciliation", {
userId: req.user.id,
disabledReason: requirements.disabled_reason,
});
try {
const disabledReason = StripeWebhookService.formatDisabledReason(
requirements.disabled_reason
);
await emailServices.payment.sendPayoutsDisabledEmail(user.email, {
ownerName: user.firstName || user.name,
disabledReason,
});
reqLogger.info("Sent payouts disabled email during reconciliation", {
userId: req.user.id,
});
} catch (emailError) {
reqLogger.error("Failed to send payouts disabled email", {
userId: req.user.id,
error: emailError.message,
});
}
}
}
res.json({
accountId: accountStatus.id,
detailsSubmitted: accountStatus.details_submitted,
payoutsEnabled: accountStatus.payouts_enabled,
capabilities: accountStatus.capabilities,
requirements: accountStatus.requirements,
});
} catch (error) {
const reqLogger = logger.withRequestId(req.id);
reqLogger.error("Stripe account status retrieval failed", {
error: error.message,
stack: error.stack,
userId: req.user.id,
stripeConnectedAccountId: user?.stripeConnectedAccountId,
});
next(error);
}
});
// Create embedded setup checkout session for collecting payment method
router.post(
"/create-setup-checkout-session",
authenticateToken,
requireVerifiedEmail,
async (req, res, next) => {
let user = null;
try {
const { rentalData } = req.body;
user = await User.findByPk(req.user.id);
if (!user) {
return res.status(404).json({ error: "User not found" });
}
// Create or get Stripe customer
let stripeCustomerId = user.stripeCustomerId;
if (!stripeCustomerId) {
// Create new Stripe customer
const customer = await StripeService.createCustomer({
email: user.email,
name: `${user.firstName} ${user.lastName}`,
metadata: {
userId: user.id.toString(),
},
});
stripeCustomerId = customer.id;
// Save customer ID to user record
await user.update({ stripeCustomerId });
}
// Add rental data to metadata if provided
const metadata = rentalData
? {
rentalData: JSON.stringify(rentalData),
}
: {};
const session = await StripeService.createSetupCheckoutSession({
customerId: stripeCustomerId,
metadata,
});
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("Stripe setup checkout session created", {
userId: req.user.id,
stripeCustomerId: stripeCustomerId,
sessionId: session.id,
hasRentalData: !!rentalData,
});
res.json({
clientSecret: session.client_secret,
sessionId: session.id,
});
} catch (error) {
const reqLogger = logger.withRequestId(req.id);
reqLogger.error("Stripe setup checkout session creation failed", {
error: error.message,
stack: error.stack,
userId: req.user.id,
stripeCustomerId: user?.stripeCustomerId,
});
next(error);
}
}
);
module.exports = router;