← back to Commercialrealestate

scripts/crcp-billing.js

71 lines

// crcp-billing.js — P1.5 Stripe subscription tiers (docs/TOOL-SPEC.md), TEST-mode scaffold.
// HARD RAIL: TEST keys only. Live keys, real charges, and turning billing on are Steve-gated — this
// module never uses a live key and degrades to {configured:false} until STRIPE_TEST_SECRET is set.
// Mount from serve.js AFTER crcp-accounts:  require('./crcp-billing')(app, ROOT, acct.userOf);
'use strict';
const fs = require('fs');
const path = require('path');

const TIERS = {
  free: { id: 'free', label: 'Free', price: 0, blurb: 'Browse the board, basic filters, last 30 days' },
  pro:  { id: 'pro',  label: 'Pro',  price: 79, price_id_env: 'STRIPE_PRICE_PRO',
          blurb: 'Full corpus + history, saved searches, daily alerts, watchlist, assumable-loan leads' },
  team: { id: 'team', label: 'Team / API', price: 299, price_id_env: 'STRIPE_PRICE_TEAM',
          blurb: 'Everything + federal-data export/API (SEC+FHA, resell-clean), multi-seat' },
};

module.exports = function mountBilling(app, ROOT, userOf) {
  const ACC = path.join(ROOT, 'data', 'crcp-accounts.json');
  const loadAcc = () => { try { return JSON.parse(fs.readFileSync(ACC, 'utf8')); } catch { return null; } };
  const saveAcc = (db) => { const t = ACC + '.tmp'; fs.writeFileSync(t, JSON.stringify(db, null, 2)); fs.renameSync(t, ACC); };
  const setTier = (email, tier) => { const db = loadAcc(); if (!db || !db.users || !db.users[email]) return false; db.users[email].tier = tier; saveAcc(db); return true; };

  // Lazy, graceful: only "configured" when the TEST secret is present AND the stripe lib is installed.
  function stripe() {
    const key = process.env.STRIPE_TEST_SECRET || '';
    if (!key || !/^sk_test_/.test(key)) return null;          // TEST keys ONLY — refuse a live key
    try { return require('stripe')(key); } catch { return null; }
  }
  const configured = () => !!stripe();

  // Catalog — always available (drives the pricing UI).
  app.get('/api/billing/tiers', (req, res) => res.json({ tiers: TIERS, configured: configured(), mode: configured() ? 'test' : 'unconfigured' }));

  // Start a TEST checkout for the signed-in user. 503 until Stripe TEST is wired.
  app.post('/api/billing/checkout', async (req, res) => {
    const u = userOf && userOf(req); if (!u || !u.email) return res.status(401).json({ error: 'sign in' });
    const tierId = String((req.body || {}).tier || '');
    const tier = TIERS[tierId]; if (!tier || tier.id === 'free') return res.status(400).json({ error: 'pick a paid tier' });
    const s = stripe();
    if (!s) return res.status(503).json({ configured: false, hint: 'set STRIPE_TEST_SECRET (sk_test_…) + STRIPE_PRICE_* and `npm i stripe` to enable — TEST mode, Steve-gated for live' });
    const priceId = process.env[tier.price_id_env];
    if (!priceId) return res.status(503).json({ configured: false, hint: `set ${tier.price_id_env} to the Stripe TEST price id` });
    try {
      const base = process.env.CRCP_BASE || `${req.protocol}://${req.get('host')}`;
      const session = await s.checkout.sessions.create({
        mode: 'subscription', customer_email: u.email,
        line_items: [{ price: priceId, quantity: 1 }],
        success_url: `${base}/deals-flow.html?upgraded=1`, cancel_url: `${base}/deals-flow.html`,
        metadata: { crcp_email: u.email, crcp_tier: tier.id },
      });
      res.json({ url: session.url, mode: 'test' });
    } catch (e) { res.status(502).json({ error: 'stripe_error', detail: String(e.message).slice(0, 200) }); }
  });

  // Webhook — sets the user's tier on a completed TEST checkout. NOTE (go-live gap): real signature
  // verification needs express.raw on THIS route (global express.json consumes the body first). For the
  // TEST scaffold we accept a shared-secret header; wiring Stripe-Signature verify is a go-live task.
  app.post('/api/billing/webhook', (req, res) => {
    if ((req.headers['x-crcp-webhook'] || '') !== (process.env.CRCP_WEBHOOK_TOKEN || '')) return res.status(401).end();
    const ev = req.body || {};
    if (ev.type === 'checkout.session.completed') {
      const m = (ev.data && ev.data.object && ev.data.object.metadata) || {};
      if (m.crcp_email && m.crcp_tier) setTier(m.crcp_email, m.crcp_tier);
    }
    res.json({ received: true });
  });

  console.log(`[crcp-billing] tiers mounted (${configured() ? 'Stripe TEST configured' : 'unconfigured — set STRIPE_TEST_SECRET to enable'})`);
};
module.exports.TIERS = TIERS;