← back to Ventura Claw Leads

routes/webhooks.js

89 lines

const express = require('express');
const db = require('../lib/db');
const stripe = require('../lib/stripe');
const router = express.Router();

const IS_PROD = process.env.NODE_ENV === 'production';

router.get('/', (req, res) => res.status(200).type('text/plain').send('ok'));
router.head('/', (req, res) => res.status(200).end());

router.post('/', async (req, res) => {
  const sig = req.headers['stripe-signature'];

  let event;
  try {
    event = stripe.constructWebhookEvent(req.body, sig);
  } catch (err) {
    console.warn('[webhook] sig fail', err.message);
    return res.status(400).send(`bad signature: ${err.message}`);
  }

  if (!event) {
    console.warn('[webhook] not_configured', { isProd: IS_PROD });
    return res.status(400).json({ error: 'webhook_not_configured' });
  }

  // Same Stripe account fires events to every subscribed endpoint (NPH + lawyer +
  // home-history sites + VCL). Drop events that aren't VCL's.
  if (!stripe.eventBelongsToVCL(event)) {
    return res.json({ received: true, ignored: true, reason: 'not_vcl_event' });
  }

  const client = await db.pool.connect();
  try {
    await client.query('BEGIN');
    const audit = await client.query(
      `INSERT INTO subscription_events (stripe_event_id, event_type, payload)
       VALUES ($1, $2, $3) ON CONFLICT (stripe_event_id) DO NOTHING
       RETURNING id`,
      [event.id, event.type, event]
    );
    if (audit.rowCount === 0) {
      await client.query('COMMIT');
      client.release();
      return res.json({ received: true, idempotent: true });
    }

    const obj = event.data.object;
    if (event.type.startsWith('customer.subscription.')) {
      const businessId = obj.metadata && obj.metadata.business_id;
      const tier       = obj.metadata && obj.metadata.vcl_tier;
      if (businessId) {
        if (event.type === 'customer.subscription.deleted') {
          await client.query(
            `UPDATE businesses SET tier='free', subscription_status='canceled', stripe_subscription_id=NULL WHERE id=$1`,
            [businessId]
          );
        } else {
          let resolvedTier = tier;
          if (!resolvedTier && obj.items && obj.items.data && obj.items.data[0]) {
            resolvedTier = stripe.tierFromPriceId(obj.items.data[0].price.id);
          }
          await client.query(
            `UPDATE businesses SET tier=COALESCE($2,tier), subscription_status=$3, stripe_subscription_id=$4, stripe_customer_id=COALESCE($5,stripe_customer_id) WHERE id=$1`,
            [businessId, resolvedTier || null, obj.status || 'active', obj.id, obj.customer]
          );
        }
      }
    }
    if (event.type === 'checkout.session.completed' && obj.metadata && obj.metadata.business_id) {
      await client.query(
        `UPDATE businesses SET stripe_customer_id=COALESCE(stripe_customer_id,$2) WHERE id=$1`,
        [obj.metadata.business_id, obj.customer]
      );
    }

    await client.query('COMMIT');
    res.json({ received: true, processed: event.type });
  } catch (err) {
    await client.query('ROLLBACK').catch(() => {});
    console.error('[webhook] error', event.type, err.message);
    res.status(500).json({ error: 'internal' });
  } finally {
    client.release();
  }
});

module.exports = router;