2 * @file routes/stripe-webhook.js
3 * @module routes/stripe-webhook
4 * @description Stripe webhook endpoint handler for processing platform and Connected Account
5 * events. Verifies webhook signatures, processes payment and subscription events, and logs
6 * all webhook activity to database.
9 * - Stripe Connect platform model (one platform account, multiple connected accounts)
10 * - Handles both snapshot webhooks (full event data) and thin webhooks (minimal data)
11 * - Signature verification prevents unauthorized webhook calls
12 * - Idempotent event processing prevents duplicate handling
13 * - Comprehensive event logging for audit trails
15 * **Webhook Configuration:**
16 * - Endpoint URL: https://rmm-psa-backend-t9f7k.ondigitalocean.app/api/stripe/webhook
17 * - Snapshot webhook secret: STRIPE_WEBHOOK_SECRET (primary, full event data)
18 * - Thin webhook secret: STRIPE_WEBHOOK_SECRET_THIN (fallback, minimal data)
19 * - Both secrets configured in .env file
21 * **Event Types Handled:**
22 * - payment_intent.succeeded - Payment completed successfully
23 * - payment_intent.payment_failed - Payment failed (log + notify)
24 * - customer.subscription.created - New subscription started
25 * - customer.subscription.updated - Subscription modified (plan change, quantity, etc)
26 * - customer.subscription.deleted - Subscription canceled
27 * - invoice.payment_succeeded - Recurring invoice paid
28 * - invoice.payment_failed - Recurring invoice payment failed
29 * - account.updated - Connected account status changed
30 * - charge.succeeded - Direct charge completed
31 * - charge.failed - Direct charge failed
34 * - No authenticateToken required (webhooks from Stripe, not clients)
35 * - Signature verification using stripe.webhooks.constructEvent()
36 * - Raw body buffer required for signature validation
37 * - 401 Unauthorized for invalid signatures
38 * - Rate limiting via Stripe's built-in webhook throttling
40 * **Event Processing:**
41 * 1. Receive webhook POST with raw body
42 * 2. Extract Stripe-Signature header
43 * 3. Verify signature with webhook secret
44 * 4. Check for duplicate event (idempotency)
45 * 5. Process event based on type
46 * 6. Log event to stripe_webhook_events table
47 * 7. Return 200 OK (or 400/500 on errors)
49 * **Database Tables:**
50 * - stripe_webhook_events - Full event log with payload, status, processing errors
51 * - payments - Updated on payment_intent.succeeded/failed
52 * - stripe_subscriptions - Updated on subscription lifecycle events
53 * - stripe_config - Connected account status updates
56 * - Event ID stored in stripe_webhook_events table
57 * - Duplicate events return 200 OK without reprocessing
58 * - Prevents duplicate charges, double notifications, etc.
61 * - Signature verification failure: 401 Unauthorized
62 * - Processing errors: Log to database, return 500 (Stripe will retry)
63 * - Unhandled event types: Log and return 200 (not an error)
65 * **Related Modules:**
66 * - services/stripeService.js - Core Stripe API operations
67 * - routes/invoices.js - Invoice management integration
68 * - routes/contracts.js - Recurring billing subscription integration
72 * @see {@link https://stripe.com/docs/webhooks|Stripe Webhooks Documentation}
73 * @see {@link https://stripe.com/docs/connect/webhooks|Stripe Connect Webhooks}
76const express = require('express');
77const router = express.Router();
78const pool = require('../services/db');
80// Initialize Stripe with correct key based on mode
81const stripeMode = process.env.STRIPE_MODE || 'test';
82const stripeSecretKey = stripeMode === 'live'
83 ? process.env.STRIPE_LIVE_SECRET_KEY
84 : process.env.STRIPE_TEST_SECRET_KEY;
85const stripe = require('stripe')(stripeSecretKey);
87console.log(`[Stripe] Webhook handler initialized in ${stripeMode.toUpperCase()} mode`);
89// Webhook signing secrets from .env (mode-specific)
90const WEBHOOK_SECRET_SNAPSHOT = stripeMode === 'live'
91 ? process.env.STRIPE_LIVE_WEBHOOK_SECRET
92 : process.env.STRIPE_WEBHOOK_SECRET;
93const WEBHOOK_SECRET_THIN = stripeMode === 'live'
94 ? process.env.STRIPE_LIVE_WEBHOOK_SECRET_THIN
95 : process.env.STRIPE_WEBHOOK_SECRET_THIN;
98 * POST /api/stripe/webhook
100 * Stripe webhook endpoint for receiving platform and Connected Account events.
102 * **Request Requirements:**
103 * - Content-Type: application/json
104 * - Stripe-Signature header (computed by Stripe)
105 * - Raw body buffer (required for signature verification)
108 * 1. Extract raw body and Stripe-Signature header
109 * 2. Try snapshot webhook secret, fallback to thin webhook secret
110 * 3. Construct and verify event signature
111 * 4. Check for duplicate event (idempotency)
112 * 5. Process event based on type
113 * 6. Log event to database
116 * **Response Codes:**
117 * - 200 OK - Event processed successfully (or duplicate)
118 * - 401 Unauthorized - Signature verification failed
119 * - 400 Bad Request - Missing signature header or body
120 * - 500 Internal Server Error - Processing error (Stripe will retry)
122 * @route POST /api/stripe/webhook
123 * @param {express.Request} req - Express request object (requires raw body buffer)
124 * @param {express.Response} res - Express response object
125 * @returns {Promise<void>} Returns 200 OK on success, error codes on failure
128 * // Stripe sends POST request with signature
129 * POST /api/stripe/webhook
131 * Stripe-Signature: t=1234567890,v1=abc123...,v0=def456...
132 * Content-Type: application/json
135 * "id": "evt_1234567890",
137 * "type": "payment_intent.succeeded",
143 * { "received": true, "event_id": "evt_1234567890" }
145router.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
146 const sig = req.headers['stripe-signature'];
149 console.error('❌ Missing Stripe-Signature header');
150 return res.status(400).json({ error: 'Missing Stripe signature' });
156 // Try snapshot webhook secret first (primary)
158 event = stripe.webhooks.constructEvent(req.body, sig, WEBHOOK_SECRET_SNAPSHOT);
159 console.log('✅ Webhook verified with snapshot secret');
161 // Fallback to thin webhook secret
162 event = stripe.webhooks.constructEvent(req.body, sig, WEBHOOK_SECRET_THIN);
163 console.log('✅ Webhook verified with thin secret');
166 console.error('❌ Webhook signature verification failed:', err.message);
167 return res.status(401).json({ error: `Webhook Error: ${err.message}` });
170 console.log(`📥 Received webhook: ${event.type} (${event.id})`);
172 // Check for duplicate event (idempotency)
173 const duplicateCheck = await pool.query(
174 'SELECT event_id FROM stripe_webhook_events WHERE event_id = $1',
178 if (duplicateCheck.rows.length > 0) {
179 console.log(`⚠️ Duplicate event ${event.id} - already processed`);
180 return res.status(200).json({ received: true, duplicate: true });
183 // Process event based on type
185 await handleWebhookEvent(event);
187 // Log successful processing
188 await logWebhookEvent(event, 'processed', null);
190 console.log(`✅ Successfully processed ${event.type}`);
191 res.status(200).json({ received: true, event_id: event.id });
193 console.error(`❌ Error processing webhook ${event.id}:`, error);
195 // Log failed processing (Stripe will retry)
196 await logWebhookEvent(event, 'failed', error.message);
198 res.status(500).json({
199 error: 'Webhook processing failed',
201 message: error.message
207 * Handles webhook event processing based on event type.
209 * Routes events to appropriate handlers for payment intents, subscriptions,
210 * invoices, and Connected Account events. Logs unhandled event types for monitoring.
212 * **Supported Event Types:**
213 * - Payment Intent: succeeded, payment_failed
214 * - Subscription: created, updated, deleted
215 * - Invoice: payment_succeeded, payment_failed
216 * - Account: updated (Connected Account status)
217 * - Charge: succeeded, failed
220 * @function handleWebhookEvent
221 * @param {Object} event - Stripe event object
222 * @param {string} event.id - Unique event identifier (evt_...)
223 * @param {string} event.type - Event type (e.g., 'payment_intent.succeeded')
224 * @param {Object} event.data - Event data payload
225 * @param {string} [event.account] - Connected Account ID (for Connect events)
226 * @returns {Promise<void>} Resolves when event handled
227 * @throws {Error} If event processing fails
230 * await handleWebhookEvent({
232 * type: 'payment_intent.succeeded',
238 * status: 'succeeded'
243async function handleWebhookEvent(event) {
244 const { type, data } = event;
245 const object = data.object;
248 // Payment Intent Events
249 case 'payment_intent.succeeded':
250 await handlePaymentSuccess(object, event.account);
253 case 'payment_intent.payment_failed':
254 await handlePaymentFailed(object, event.account);
257 // Subscription Events
258 case 'customer.subscription.created':
259 await handleSubscriptionCreated(object, event.account);
262 case 'customer.subscription.updated':
263 await handleSubscriptionUpdated(object, event.account);
266 case 'customer.subscription.deleted':
267 await handleSubscriptionDeleted(object, event.account);
270 // Invoice Events (recurring billing)
271 case 'invoice.payment_succeeded':
272 await handleInvoicePaymentSuccess(object, event.account);
275 case 'invoice.payment_failed':
276 await handleInvoicePaymentFailed(object, event.account);
279 // Connected Account Events
280 case 'account.updated':
281 await handleAccountUpdated(object);
285 case 'charge.succeeded':
286 await handleChargeSuccess(object, event.account);
289 case 'charge.failed':
290 await handleChargeFailed(object, event.account);
294 console.log(`⚠️ Unhandled event type: ${type}`);
295 // Not an error - just log and continue
300 * Handles successful payment intent completion.
302 * Updates payment record in database with success status, Stripe payment ID,
303 * and payment timestamp. Links to invoice if metadata contains invoice_id.
304 * Updates invoice payment_status to 'paid' if linked.
307 * @function handlePaymentSuccess
308 * @param {Object} paymentIntent - Stripe PaymentIntent object
309 * @param {string} paymentIntent.id - Payment Intent ID (pi_...)
310 * @param {number} paymentIntent.amount - Amount in cents
311 * @param {string} paymentIntent.currency - Currency code (usd, eur, etc)
312 * @param {string} paymentIntent.status - Payment status ('succeeded')
313 * @param {Object} [paymentIntent.metadata] - Custom metadata (invoice_id, tenant_id, etc)
314 * @param {string} [connectedAccountId] - Connected Account ID (if Connect payment)
315 * @returns {Promise<void>} Resolves when payment record updated
316 * @throws {Error} If database update fails
319 * await handlePaymentSuccess({
323 * status: 'succeeded',
330async function handlePaymentSuccess(paymentIntent, connectedAccountId) {
331 console.log(`💰 Payment succeeded: ${paymentIntent.id} ($${paymentIntent.amount / 100})`);
333 // TODO: Update payments table
334 // - Set status = 'succeeded'
335 // - Set stripe_payment_intent_id = paymentIntent.id
336 // - Set paid_at = NOW()
337 // - Update invoice payment_status = 'paid' if metadata.invoice_id exists
339 // TODO: Send customer receipt email
340 // TODO: Trigger post-payment webhooks/notifications
344 * Handles failed payment intent.
346 * Updates payment record with failed status and error message. Logs failure
347 * reason for debugging. Triggers customer notification email. Updates invoice
348 * payment_status to 'failed' if linked.
351 * @function handlePaymentFailed
352 * @param {Object} paymentIntent - Stripe PaymentIntent object
353 * @param {string} paymentIntent.id - Payment Intent ID (pi_...)
354 * @param {number} paymentIntent.amount - Amount in cents
355 * @param {string} paymentIntent.currency - Currency code
356 * @param {string} paymentIntent.status - Payment status ('failed')
357 * @param {Object} [paymentIntent.last_payment_error] - Error details
358 * @param {string} [paymentIntent.last_payment_error.message] - Human-readable error
359 * @param {Object} [paymentIntent.metadata] - Custom metadata
360 * @param {string} [connectedAccountId] - Connected Account ID
361 * @returns {Promise<void>} Resolves when failure recorded
362 * @throws {Error} If database update fails
365 * await handlePaymentFailed({
369 * last_payment_error: {
370 * message: 'Your card was declined.'
372 * metadata: { invoice_id: '456' }
375async function handlePaymentFailed(paymentIntent, connectedAccountId) {
376 console.log(`❌ Payment failed: ${paymentIntent.id} - ${paymentIntent.last_payment_error?.message}`);
378 // TODO: Update payments table
379 // - Set status = 'failed'
380 // - Set error_message = last_payment_error.message
381 // - Update invoice payment_status = 'failed'
383 // TODO: Send customer payment failure notification
384 // TODO: Trigger retry logic if applicable
388 * Handles new subscription creation.
390 * Creates record in stripe_subscriptions table with subscription details,
391 * pricing, billing cycle, and status. Links to tenant via Connected Account ID
392 * or metadata. Creates initial invoice record if applicable.
395 * @function handleSubscriptionCreated
396 * @param {Object} subscription - Stripe Subscription object
397 * @param {string} subscription.id - Subscription ID (sub_...)
398 * @param {string} subscription.customer - Customer ID (cus_...)
399 * @param {string} subscription.status - Subscription status (active, trialing, etc)
400 * @param {Array} subscription.items - Subscription items with pricing
401 * @param {number} subscription.current_period_start - Unix timestamp
402 * @param {number} subscription.current_period_end - Unix timestamp
403 * @param {Object} [subscription.metadata] - Custom metadata
404 * @param {string} [connectedAccountId] - Connected Account ID
405 * @returns {Promise<void>} Resolves when subscription record created
406 * @throws {Error} If database insert fails
409 * await handleSubscriptionCreated({
411 * customer: 'cus_456',
414 * price: { id: 'price_789', unit_amount: 2000 }
416 * current_period_start: 1234567890,
417 * current_period_end: 1237159890,
418 * metadata: { contract_id: '101' }
421async function handleSubscriptionCreated(subscription, connectedAccountId) {
422 console.log(`📅 Subscription created: ${subscription.id} (${subscription.status})`);
424 // TODO: Insert into stripe_subscriptions table
425 // - stripe_subscription_id = subscription.id
426 // - customer_id = subscription.customer
427 // - status = subscription.status
428 // - current_period_start/end
429 // - linked_account_id = connectedAccountId
431 // TODO: Link to contracts table if metadata.contract_id exists
435 * Handles subscription updates.
437 * Updates subscription record with new status, pricing, quantity, or billing
438 * cycle. Handles plan changes, quantity adjustments, cancellations, and reactivations.
439 * Logs changes for audit trail.
442 * @function handleSubscriptionUpdated
443 * @param {Object} subscription - Stripe Subscription object
444 * @param {string} subscription.id - Subscription ID
445 * @param {string} subscription.status - New status (active, canceled, past_due, etc)
446 * @param {Array} subscription.items - Updated subscription items
447 * @param {number} subscription.current_period_start - Unix timestamp
448 * @param {number} subscription.current_period_end - Unix timestamp
449 * @param {boolean} [subscription.cancel_at_period_end] - Cancellation flag
450 * @param {string} [connectedAccountId] - Connected Account ID
451 * @returns {Promise<void>} Resolves when subscription updated
452 * @throws {Error} If database update fails
455 * await handleSubscriptionUpdated({
458 * cancel_at_period_end: true,
459 * items: [{ price: { unit_amount: 3000 } }]
462async function handleSubscriptionUpdated(subscription, connectedAccountId) {
463 console.log(`📅 Subscription updated: ${subscription.id} (${subscription.status})`);
465 // TODO: Update stripe_subscriptions table
466 // - Update status, current_period_start/end, cancel_at_period_end
467 // - Log plan/quantity changes
469 // TODO: Update linked contracts if prices changed
473 * Handles subscription deletion/cancellation.
475 * Updates subscription record with canceled status and cancellation timestamp.
476 * Marks as ended. Triggers post-cancellation workflows (notifications, access
477 * revocation, etc). Logs cancellation reason if available.
480 * @function handleSubscriptionDeleted
481 * @param {Object} subscription - Stripe Subscription object
482 * @param {string} subscription.id - Subscription ID
483 * @param {string} subscription.status - Final status (typically 'canceled')
484 * @param {number} [subscription.canceled_at] - Cancellation timestamp
485 * @param {string} [subscription.cancellation_reason] - Reason for cancellation
486 * @param {string} [connectedAccountId] - Connected Account ID
487 * @returns {Promise<void>} Resolves when cancellation processed
488 * @throws {Error} If database update fails
491 * await handleSubscriptionDeleted({
493 * status: 'canceled',
494 * canceled_at: 1234567890,
495 * cancellation_reason: 'Customer requested'
498async function handleSubscriptionDeleted(subscription, connectedAccountId) {
499 console.log(`🚫 Subscription deleted: ${subscription.id}`);
501 // TODO: Update stripe_subscriptions table
502 // - Set status = 'canceled'
503 // - Set canceled_at = NOW()
504 // - End linked contracts
506 // TODO: Revoke customer access if applicable
507 // TODO: Send cancellation confirmation email
511 * Handles successful recurring invoice payment.
513 * Records invoice payment in database, updates invoice status to paid, and
514 * creates payment record. Links to subscription if applicable. Triggers receipt
515 * email and post-payment workflows.
518 * @function handleInvoicePaymentSuccess
519 * @param {Object} invoice - Stripe Invoice object
520 * @param {string} invoice.id - Invoice ID (in_...)
521 * @param {string} invoice.subscription - Subscription ID (if recurring)
522 * @param {number} invoice.amount_paid - Amount paid in cents
523 * @param {string} invoice.currency - Currency code
524 * @param {string} invoice.status - Invoice status ('paid')
525 * @param {string} invoice.customer - Customer ID
526 * @param {string} [connectedAccountId] - Connected Account ID
527 * @returns {Promise<void>} Resolves when invoice payment recorded
528 * @throws {Error} If database update fails
531 * await handleInvoicePaymentSuccess({
533 * subscription: 'sub_456',
537 * customer: 'cus_789'
540async function handleInvoicePaymentSuccess(invoice, connectedAccountId) {
541 console.log(`💰 Invoice paid: ${invoice.id} ($${invoice.amount_paid / 100})`);
543 // TODO: Create payment record in payments table
544 // - Link to subscription if invoice.subscription exists
545 // - Update subscription next_billing_date
547 // TODO: Send receipt email
551 * Handles failed recurring invoice payment.
553 * Records payment failure, updates invoice status, logs error message. Triggers
554 * customer notification about failed payment. Initiates retry logic according
555 * to subscription settings. May trigger subscription suspension/cancellation.
558 * @function handleInvoicePaymentFailed
559 * @param {Object} invoice - Stripe Invoice object
560 * @param {string} invoice.id - Invoice ID
561 * @param {string} invoice.subscription - Subscription ID
562 * @param {number} invoice.amount_due - Amount due in cents
563 * @param {string} invoice.status - Invoice status ('open' or 'uncollectible')
564 * @param {number} [invoice.attempt_count] - Number of payment attempts
565 * @param {string} [connectedAccountId] - Connected Account ID
566 * @returns {Promise<void>} Resolves when failure recorded
567 * @throws {Error} If database update fails
570 * await handleInvoicePaymentFailed({
572 * subscription: 'sub_456',
578async function handleInvoicePaymentFailed(invoice, connectedAccountId) {
579 console.log(`❌ Invoice payment failed: ${invoice.id}`);
581 // TODO: Log payment failure
582 // - Update subscription status if multiple failures
583 // - Check attempt_count and trigger suspension if needed
585 // TODO: Send payment failure notification
586 // TODO: Trigger automatic retry if configured
590 * Handles Connected Account updates.
592 * Updates tenant's Stripe account status in stripe_config table. Monitors
593 * account verification status (verified, pending, disabled). Triggers notifications
594 * for status changes (account verified, verification required, account disabled).
597 * @function handleAccountUpdated
598 * @param {Object} account - Stripe Account object
599 * @param {string} account.id - Account ID (acct_...)
600 * @param {boolean} account.details_submitted - Onboarding complete
601 * @param {boolean} account.charges_enabled - Can accept charges
602 * @param {boolean} account.payouts_enabled - Can receive payouts
603 * @param {Object} [account.requirements] - Verification requirements
604 * @param {Array} [account.requirements.currently_due] - Info needed now
605 * @param {Array} [account.requirements.errors] - Verification errors
606 * @returns {Promise<void>} Resolves when account status updated
607 * @throws {Error} If database update fails
610 * await handleAccountUpdated({
612 * details_submitted: true,
613 * charges_enabled: true,
614 * payouts_enabled: true,
621async function handleAccountUpdated(account) {
622 console.log(`🏢 Account updated: ${account.id} (charges: ${account.charges_enabled}, payouts: ${account.payouts_enabled})`);
624 // TODO: Update stripe_config table
625 // - Find tenant by stripe_account_id = account.id
626 // - Update charges_enabled, payouts_enabled flags
627 // - Update verification_status based on requirements
629 // TODO: Send notification if verification status changed
633 * Handles successful charge completion.
635 * Records charge in payments table with success status. Logs charge details
636 * including amount, fee, net amount. Links to invoice if charge.metadata contains
637 * invoice_id. Updates invoice payment status if applicable.
640 * @function handleChargeSuccess
641 * @param {Object} charge - Stripe Charge object
642 * @param {string} charge.id - Charge ID (ch_...)
643 * @param {number} charge.amount - Amount charged in cents
644 * @param {string} charge.currency - Currency code
645 * @param {boolean} charge.paid - Payment success flag
646 * @param {string} charge.payment_intent - PaymentIntent ID (if applicable)
647 * @param {Object} [charge.metadata] - Custom metadata
648 * @param {string} [connectedAccountId] - Connected Account ID
649 * @returns {Promise<void>} Resolves when charge recorded
650 * @throws {Error} If database insert fails
653 * await handleChargeSuccess({
658 * payment_intent: 'pi_456',
659 * metadata: { invoice_id: '789' }
662async function handleChargeSuccess(charge, connectedAccountId) {
663 console.log(`💳 Charge succeeded: ${charge.id} ($${charge.amount / 100})`);
665 // TODO: Record charge in payments table
666 // - Link to payment_intent if exists
667 // - Store charge fees (application_fee_amount)
671 * Handles failed charge.
673 * Records charge failure with error details. Logs failure reason and outcome.
674 * Triggers customer notification. Updates invoice payment status to failed if
675 * linked. May trigger retry logic depending on failure type.
678 * @function handleChargeFailed
679 * @param {Object} charge - Stripe Charge object
680 * @param {string} charge.id - Charge ID
681 * @param {number} charge.amount - Attempted amount in cents
682 * @param {string} charge.currency - Currency code
683 * @param {boolean} charge.paid - Always false for failed charges
684 * @param {string} charge.failure_code - Machine-readable failure code
685 * @param {string} charge.failure_message - Human-readable failure message
686 * @param {Object} [charge.metadata] - Custom metadata
687 * @param {string} [connectedAccountId] - Connected Account ID
688 * @returns {Promise<void>} Resolves when failure recorded
689 * @throws {Error} If database update fails
692 * await handleChargeFailed({
696 * failure_code: 'card_declined',
697 * failure_message: 'Your card was declined.',
698 * metadata: { invoice_id: '789' }
701async function handleChargeFailed(charge, connectedAccountId) {
702 console.log(`❌ Charge failed: ${charge.id} - ${charge.failure_message}`);
704 // TODO: Record charge failure
705 // - Store failure_code and failure_message
706 // - Update invoice payment_status = 'failed'
710 * Logs webhook event to database for audit trail and idempotency.
712 * Inserts event record with full payload, processing status, and error message
713 * (if failed). Used for duplicate detection (idempotency check), debugging,
714 * and compliance audit trails. Stores complete event data for replay if needed.
717 * @function logWebhookEvent
718 * @param {Object} event - Stripe event object
719 * @param {string} event.id - Unique event ID (evt_...)
720 * @param {string} event.type - Event type (e.g., 'payment_intent.succeeded')
721 * @param {Object} event.data - Event data payload
722 * @param {number} event.created - Event creation timestamp (Unix)
723 * @param {string} [event.account] - Connected Account ID (if applicable)
724 * @param {string} status - Processing status ('processed' or 'failed')
725 * @param {string|null} errorMessage - Error message if status is 'failed'
726 * @returns {Promise<void>} Resolves when event logged
727 * @throws {Error} If database insert fails
730 * await logWebhookEvent({
732 * type: 'payment_intent.succeeded',
733 * data: { object: {...} },
734 * created: 1234567890
735 * }, 'processed', null);
738 * // Failed event logging
739 * await logWebhookEvent(event, 'failed', 'Database connection timeout');
741async function logWebhookEvent(event, status, errorMessage) {
744 `INSERT INTO stripe_webhook_events
745 (event_id, event_type, payload, status, error_message, processed_at)
746 VALUES ($1, $2, $3, $4, $5, NOW())`,
747 [event.id, event.type, JSON.stringify(event), status, errorMessage]
750 console.error('❌ Failed to log webhook event to database:', err);
751 // Don't throw - logging failure shouldn't break webhook processing
755module.exports = router;