← 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;