← back to Govarbitrage

src/lib/billing.ts

87 lines

// Stripe billing for the paid-alerts SaaS — TEST/MOCK ONLY.
// HARD RAIL: a live secret key (sk_live_) is REFUSED — going live is Steve
// flipping the switch himself, never us. With no key we run in MOCK mode: no
// real Stripe call, but the upgrade flow still completes so it's demoable.
import Stripe from "stripe";
import type { Tier } from "@prisma/client";
import { TIERS } from "@/lib/tiers";

const KEY = process.env.STRIPE_TEST_SECRET_KEY || "";
export const LIVE_KEY_REFUSED = KEY.startsWith("sk_live_");
export const STRIPE_MODE: "test" | "mock" =
  KEY.startsWith("sk_test_") && !LIVE_KEY_REFUSED ? "test" : "mock";

let _stripe: Stripe | null = null;
function stripe(): Stripe | null {
  if (STRIPE_MODE !== "test") return null;
  if (!_stripe) _stripe = new Stripe(KEY);
  return _stripe;
}

export interface CheckoutResult {
  ok: boolean;
  mode: "test" | "mock";
  url: string;
  error?: string;
}

/**
 * Create a (TEST) subscription Checkout session for a tier. In mock mode returns
 * a local success URL that simulates the upgrade so the flow is demoable.
 */
export async function createCheckout(opts: {
  userId: string;
  email: string;
  tier: Tier;
  baseUrl: string;
}): Promise<CheckoutResult> {
  const def = TIERS[opts.tier];
  if (!def || def.priceUsd <= 0) {
    return { ok: false, mode: STRIPE_MODE, url: "", error: "not a paid tier" };
  }
  if (LIVE_KEY_REFUSED) {
    return { ok: false, mode: "mock", url: "", error: "live key refused — TEST only" };
  }

  // MOCK: no Stripe call; simulate the successful upgrade locally.
  if (STRIPE_MODE === "mock") {
    const url = `${opts.baseUrl}/billing/success?tier=${opts.tier}&mock=1&session=mock_${opts.userId.slice(0, 8)}`;
    return { ok: true, mode: "mock", url };
  }

  // TEST: real Stripe test-mode subscription checkout with inline recurring price.
  const s = stripe()!;
  const session = await s.checkout.sessions.create({
    mode: "subscription",
    customer_email: opts.email,
    client_reference_id: opts.userId,
    metadata: { userId: opts.userId, tier: opts.tier },
    line_items: [
      {
        quantity: 1,
        price_data: {
          currency: "usd",
          recurring: { interval: "month" },
          unit_amount: Math.round(def.priceUsd * 100),
          product_data: { name: `GovArbitrage ${def.label} — hot-deal alerts` },
        },
      },
    ],
    success_url: `${opts.baseUrl}/billing/success?tier=${opts.tier}&session={CHECKOUT_SESSION_ID}`,
    cancel_url: `${opts.baseUrl}/pricing?canceled=1`,
  });
  return { ok: true, mode: "test", url: session.url || "" };
}

/** Verify a Stripe webhook (TEST). Returns the event or null if unverifiable. */
export function verifyWebhook(rawBody: string, sig: string | null): Stripe.Event | null {
  const s = stripe();
  const secret = process.env.STRIPE_WEBHOOK_SECRET || "";
  if (!s || !sig || !secret) return null;
  try {
    return s.webhooks.constructEvent(rawBody, sig, secret);
  } catch {
    return null;
  }
}