All files / services/config-service/src/routes stripe.js

23.91% Statements 33/138
25% Branches 2/8
0% Functions 0/7
23.91% Lines 33/138

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 1391x 1x 1x 1x 1x 1x 1x 1x 1x       1x         1x                                       1x                                         1x                                     1x       1x               1x 1x 1x 1x 1x 1x         1x 1x 1x 1x                       1x     1x     1x     1x                     1x 1x 1x 1x  
const express = require('express');
const path = require('path');
const OrganizationPlan = require('../models/OrganizationPlan');
const StripeEvent = require('../models/StripeEvent');
const ProcessedEvent = require('../models/ProcessedEvent');
const UsageTracking = require('../models/UsageTracking');
 
const router = express.Router();
 
function getPlans() {
  return require(path.join(__dirname, '..', 'plans-config.json')).plans;
}
 
function getStripe() {
  if (!process.env.STRIPE_SECRET_KEY) return null;
  return require('stripe')(process.env.STRIPE_SECRET_KEY);
}
 
async function handleCheckoutCompleted(session) {
  const { organizationId, planCode } = session.metadata;
  const stripe = getStripe();
  const stripeSub = await stripe.subscriptions.retrieve(session.subscription);
  const update = {
    planCode,
    stripeCustomerId: session.customer,
    stripeSubscriptionId: session.subscription,
    status: stripeSub.status,
    currentPeriodStart: new Date(stripeSub.current_period_start * 1000),
    currentPeriodEnd: new Date(stripeSub.current_period_end * 1000),
  };

  await OrganizationPlan.findOneAndUpdate(
    { organizationId },
    update,
    { upsert: true, new: true }
  );
}
 
async function handleSubscriptionUpdated(subscription) {
  const orgPlan = await OrganizationPlan.findOne({ stripeSubscriptionId: subscription.id });
  if (!orgPlan) return;
  const previousStatus = orgPlan.status;
  orgPlan.status = subscription.status;
  orgPlan.currentPeriodStart = new Date(subscription.current_period_start * 1000);
  orgPlan.currentPeriodEnd = new Date(subscription.current_period_end * 1000);
  if (previousStatus === 'past_due' && subscription.status === 'active') orgPlan.pastDueSince = null;

  const stripePriceId = subscription.items.data[0]?.price.id;
  const plans = getPlans();
  const plan = plans.find(p => p.code === orgPlan.planCode);
  if (plan?.stripePriceId && stripePriceId !== plan.stripePriceId) {
    const newPlan = plans.find(p => p.stripePriceId === stripePriceId);
    if (newPlan) {
      orgPlan.planCode = newPlan.code;
    }
  }
  await orgPlan.save();
}
 
async function downgradeToFree(stripeSubscriptionIdOrCustomer) {
  const orgPlan = await OrganizationPlan.findOne({
    $or: [{ stripeSubscriptionId: stripeSubscriptionIdOrCustomer }, { stripeCustomerId: stripeSubscriptionIdOrCustomer }],
  });
  if (!orgPlan) return;
  orgPlan.planCode = 'free';
  orgPlan.status = 'active';
  orgPlan.pastDueSince = null;
  orgPlan.stripeSubscriptionId = null;
  orgPlan.currentPeriodStart = new Date();
  orgPlan.currentPeriodEnd = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
  await orgPlan.save();
  await UsageTracking.findOneAndUpdate(
    { organizationId: orgPlan.organizationId, periodStart: orgPlan.currentPeriodStart },
    { $set: { llmCostUsed: 0, ocrCostUsed: 0 } },
    { upsert: true }
  );
}
 
async function handleSubscriptionDeleted(subscription) {
  await downgradeToFree(subscription.id);
}
 
async function handlePaymentFailed(invoice) {
  const orgPlan = await OrganizationPlan.findOne({ stripeCustomerId: invoice.customer });
  if (!orgPlan) return;
  orgPlan.status = 'past_due';
  orgPlan.pastDueSince = new Date();
  await orgPlan.save();
}
 
router.post('/webhooks/stripe', async (req, res) => {
  let event;
  try {
    const sig = req.headers['stripe-signature'];
    if (!sig) throw new Error('Missing stripe-signature header');
    if (!process.env.STRIPE_WEBHOOK_SECRET) throw new Error('STRIPE_WEBHOOK_SECRET not configured');
    const stripe = getStripe();
    if (!stripe) throw new Error('Stripe not configured');
    event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET);
  } catch (err) {
    console.error('[Stripe] Webhook signature verification failed:', err.message);
    return res.status(400).json({ error: 'Invalid signature' });
  }

  const exists = await ProcessedEvent.findOne({ eventId: event.id });
  if (exists) return res.status(200).json({ status: 'duplicate', eventId: event.id });

  await StripeEvent.create({ stripeEventId: event.id, type: event.type, data: event.data.object, status: 'received' });

  try {
    switch (event.type) {
      case 'checkout.session.completed':
        await handleCheckoutCompleted(event.data.object);
        break;
      case 'customer.subscription.updated':
        await handleSubscriptionUpdated(event.data.object);
        break;
      case 'customer.subscription.deleted':
        await handleSubscriptionDeleted(event.data.object);
        break;
      case 'invoice.payment_failed':
        await handlePaymentFailed(event.data.object);
        break;
    }

    await StripeEvent.updateOne({ stripeEventId: event.id }, { status: 'processed', processedAt: new Date() });
    await ProcessedEvent.create({ eventId: event.id, receivedAt: new Date() });

    res.status(200).json({ status: 'processed', eventId: event.id });
  } catch (error) {
    console.error(`[Stripe] Failed to process ${event.type}:`, error.message);
    await StripeEvent.updateOne({ stripeEventId: event.id }, { status: 'failed', error: error.message });
    res.status(200).json({ status: 'queued', eventId: event.id });
  }
});
 
module.exports = router;
module.exports.handlers = { handleCheckoutCompleted, handleSubscriptionUpdated, handleSubscriptionDeleted, handlePaymentFailed, downgradeToFree };