Files
rentall-app/backend/routes/stripeWebhooks.js
2026-01-08 17:49:02 -05:00

112 lines
3.3 KiB
JavaScript

const express = require("express");
const StripeWebhookService = require("../services/stripeWebhookService");
const DisputeService = require("../services/disputeService");
const logger = require("../utils/logger");
const router = express.Router();
const WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET;
/**
* POST /stripe/webhooks
* Stripe webhook endpoint - receives events from Stripe.
* Must use raw body for signature verification.
*/
router.post("/", async (req, res) => {
const signature = req.headers["stripe-signature"];
if (!signature) {
logger.warn("Webhook request missing stripe-signature header");
return res.status(400).json({ error: "Missing signature" });
}
if (!WEBHOOK_SECRET) {
logger.error("STRIPE_WEBHOOK_SECRET not configured");
return res.status(500).json({ error: "Webhook not configured" });
}
let event;
try {
// Use rawBody stored by bodyParser in server.js
event = StripeWebhookService.constructEvent(
req.rawBody,
signature,
WEBHOOK_SECRET
);
} catch (err) {
logger.error("Webhook signature verification failed", {
error: err.message,
});
return res.status(400).json({ error: "Invalid signature" });
}
// Log event receipt for debugging
// For Connect account events, event.account contains the connected account ID
logger.info("Stripe webhook received", {
eventId: event.id,
eventType: event.type,
connectedAccount: event.account || null,
});
try {
switch (event.type) {
case "account.updated":
await StripeWebhookService.handleAccountUpdated(event.data.object);
break;
case "payout.paid":
// Payout to connected account's bank succeeded
await StripeWebhookService.handlePayoutPaid(
event.data.object,
event.account
);
break;
case "payout.failed":
// Payout to connected account's bank failed
await StripeWebhookService.handlePayoutFailed(
event.data.object,
event.account
);
break;
case "account.application.deauthorized":
// Owner disconnected their Stripe account from our platform
await StripeWebhookService.handleAccountDeauthorized(event.account);
break;
case "charge.dispute.created":
// Renter disputed a charge with their bank
await DisputeService.handleDisputeCreated(event.data.object);
break;
case "charge.dispute.closed":
case "charge.dispute.funds_reinstated":
case "charge.dispute.funds_withdrawn":
// Dispute was resolved (won, lost, or warning closed)
await DisputeService.handleDisputeClosed(event.data.object);
break;
default:
logger.info("Unhandled webhook event type", { type: event.type });
}
// Always return 200 to acknowledge receipt
res.json({ received: true, eventId: event.id });
} catch (error) {
logger.error("Error processing webhook", {
eventId: event.id,
eventType: event.type,
error: error.message,
stack: error.stack,
});
// Still return 200 to prevent Stripe retries for processing errors
// Failed payouts will be handled by retry job
res.json({ received: true, eventId: event.id, error: error.message });
}
});
module.exports = router;