2026-06-07 03:58:32 -04:00
|
|
|
import Stripe from "stripe";
|
|
|
|
|
import { prisma } from "@/lib/db";
|
|
|
|
|
import { stripePriceId, type BillingInterval } from "./catalog";
|
|
|
|
|
import type { PlanKey } from "./plans";
|
|
|
|
|
|
|
|
|
|
let client: Stripe | null = null;
|
|
|
|
|
|
|
|
|
|
export function stripe(): Stripe {
|
|
|
|
|
if (!client) {
|
|
|
|
|
const key = process.env.STRIPE_SECRET_KEY;
|
|
|
|
|
if (!key) throw new Error("STRIPE_SECRET_KEY is not set");
|
|
|
|
|
client = new Stripe(key);
|
|
|
|
|
}
|
|
|
|
|
return client;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function isStripeConfigured(): boolean {
|
|
|
|
|
return !!process.env.STRIPE_SECRET_KEY;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-08 04:46:58 -04:00
|
|
|
const appUrl = () => process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
|
2026-06-07 03:58:32 -04:00
|
|
|
|
|
|
|
|
/** Find-or-create the Stripe customer for a user and persist the id. */
|
|
|
|
|
export async function ensureStripeCustomer(user: {
|
|
|
|
|
id: string;
|
|
|
|
|
email: string;
|
|
|
|
|
name: string;
|
|
|
|
|
stripeCustomerId: string | null;
|
|
|
|
|
}): Promise<string> {
|
|
|
|
|
if (user.stripeCustomerId) return user.stripeCustomerId;
|
|
|
|
|
const customer = await stripe().customers.create({
|
|
|
|
|
email: user.email,
|
|
|
|
|
name: user.name,
|
|
|
|
|
metadata: { userId: user.id },
|
|
|
|
|
});
|
|
|
|
|
await prisma.user.update({ where: { id: user.id }, data: { stripeCustomerId: customer.id } });
|
|
|
|
|
return customer.id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Create a Stripe Checkout Session for a subscription; returns the redirect URL. */
|
|
|
|
|
export async function createStripeCheckout(args: {
|
|
|
|
|
user: { id: string; email: string; name: string; stripeCustomerId: string | null };
|
|
|
|
|
plan: PlanKey;
|
|
|
|
|
interval: BillingInterval;
|
|
|
|
|
subjectId: string;
|
|
|
|
|
subjectType: "user" | "organization";
|
|
|
|
|
}): Promise<string> {
|
|
|
|
|
const price = stripePriceId(args.plan, args.interval);
|
|
|
|
|
if (!price) throw new Error(`No Stripe price configured for ${args.plan}/${args.interval}`);
|
|
|
|
|
const customer = await ensureStripeCustomer(args.user);
|
|
|
|
|
|
|
|
|
|
const session = await stripe().checkout.sessions.create({
|
|
|
|
|
mode: "subscription",
|
|
|
|
|
customer,
|
|
|
|
|
line_items: [{ price, quantity: 1 }],
|
|
|
|
|
client_reference_id: args.subjectId,
|
|
|
|
|
metadata: { plan: args.plan, subjectId: args.subjectId, subjectType: args.subjectType },
|
|
|
|
|
subscription_data: {
|
|
|
|
|
metadata: { plan: args.plan, subjectId: args.subjectId, subjectType: args.subjectType },
|
|
|
|
|
},
|
|
|
|
|
allow_promotion_codes: true,
|
|
|
|
|
success_url: `${appUrl()}/billing?status=success`,
|
|
|
|
|
cancel_url: `${appUrl()}/billing?status=cancel`,
|
|
|
|
|
});
|
|
|
|
|
if (!session.url) throw new Error("Stripe did not return a checkout URL");
|
|
|
|
|
return session.url;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Create a Stripe Billing Portal session for managing/cancelling a subscription. */
|
|
|
|
|
export async function createStripePortal(customerId: string): Promise<string> {
|
|
|
|
|
const session = await stripe().billingPortal.sessions.create({
|
|
|
|
|
customer: customerId,
|
|
|
|
|
return_url: `${appUrl()}/billing`,
|
|
|
|
|
});
|
|
|
|
|
return session.url;
|
|
|
|
|
}
|