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