"use server"; import { createAdminClient } from "@/lib/supabase/server"; import { auth } from "@clerk/nextjs/server"; import { METERS, type MeterId } from "@/lib/stripe-config"; const stripeKey = process.env.STRIPE_SECRET_KEY; async function getOrganisationId(): Promise { const { userId } = await auth(); if (!userId) throw new Error("Unauthenticated"); const supabase = await createAdminClient(); const { data: member, error } = await supabase .from("organisation_members") .select("organisation_id") .eq("user_id", userId) .single(); if (error || !member) throw new Error("No organisation found"); return member.organisation_id; } /** * Get the current subscription info for the organisation. */ export async function getSubscription(): Promise<{ status: string; stripeCustomerId: string | null; stripeSubscriptionId: string | null; currentPeriodEnd: string | null; cancelAtPeriodEnd: boolean; } | null> { const orgId = await getOrganisationId(); const supabase = await createAdminClient(); const { data } = await supabase .from("subscriptions") .select("*") .eq("organisation_id", orgId) .single(); if (!data) return null; return { status: data.status, stripeCustomerId: data.stripe_customer_id, stripeSubscriptionId: data.stripe_subscription_id, currentPeriodEnd: data.current_period_end, cancelAtPeriodEnd: data.cancel_at_period_end, }; } /** * Create a Stripe Checkout session for PAYG metered billing. * All metered prices are added as line items — user pays per usage. */ export async function createCheckoutSession(): Promise<{ url: string; error?: string }> { if (!stripeKey) return { url: "", error: "Stripe not configured" }; const { userId } = await auth(); if (!userId) return { url: "", error: "Unauthenticated" }; const orgId = await getOrganisationId(); const supabase = await createAdminClient(); // Check if already subscribed const { data: existingSub } = await supabase .from("subscriptions") .select("stripe_customer_id, stripe_subscription_id, status") .eq("organisation_id", orgId) .single(); if (existingSub?.stripe_subscription_id && existingSub.status === "active") { // Already subscribed — redirect to portal instead return { url: "", error: "already_subscribed" }; } const Stripe = (await import("stripe")).default; const stripe = new Stripe(stripeKey, { apiVersion: "2026-03-25.dahlia" as any }); let customerId = existingSub?.stripe_customer_id; // Get user email const { data: profile } = await supabase .from("profiles") .select("email") .eq("id", userId) .single(); if (!customerId) { const customer = await stripe.customers.create({ email: profile?.email || undefined, metadata: { organisation_id: orgId, clerk_user_id: userId }, }); customerId = customer.id; } // Get organisation name const { data: org } = await supabase .from("organisations") .select("name") .eq("id", orgId) .single(); // Build line items for ALL meters (no upfront charge — metered = $0 until usage reported) const lineItems = Object.values(METERS) .filter((m) => m.stripePriceId) .map((m) => ({ price: m.stripePriceId, })); if (lineItems.length === 0) { return { url: "", error: "No metered prices configured. Contact support." }; } const successUrl = `${process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"}/dashboard?checkout=success`; const cancelUrl = `${process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"}/pricing?checkout=cancelled`; const session = await stripe.checkout.sessions.create({ customer: customerId, mode: "subscription", payment_method_types: ["card"], line_items: lineItems, success_url: successUrl, cancel_url: cancelUrl, client_reference_id: orgId, metadata: { organisation_id: orgId }, customer_update: { name: "auto" }, subscription_data: { metadata: { organisation_id: orgId }, }, }); if (!session.url) return { url: "", error: "Failed to create checkout session" }; return { url: session.url }; } /** * Report usage for a specific meter. * Call this after each billable action. * * Example: await reportUsage("invoices", 1); */ export async function reportUsage( meterId: MeterId, quantity: number = 1 ): Promise<{ success: boolean; error?: string }> { if (!stripeKey) return { success: false, error: "Stripe not configured" }; const meter = METERS[meterId]; if (!meter) return { success: false, error: `Unknown meter: ${meterId}` }; const orgId = await getOrganisationId(); const supabase = await createAdminClient(); // Get Stripe customer ID const { data: sub } = await supabase .from("subscriptions") .select("stripe_customer_id, status") .eq("organisation_id", orgId) .single(); if (!sub?.stripe_customer_id) return { success: false, error: "No active subscription" }; if (sub.status !== "active" && sub.status !== "trialing") { return { success: false, error: `Subscription is ${sub.status}` }; } const Stripe = (await import("stripe")).default; const stripe = new Stripe(stripeKey, { apiVersion: "2026-03-25.dahlia" as any }); try { await stripe.billing.meterEvents.create({ event_name: meter.stripeMeterName, payload: { stripe_customer_id: sub.stripe_customer_id, value: String(quantity), timestamp: String(Math.floor(Date.now() / 1000)), }, }); return { success: true }; } catch (err: any) { console.error(`Failed to report usage for ${meterId}:`, err.message); return { success: false, error: err.message }; } } /** * Create a Stripe Customer Portal session for managing billing. */ export async function createCustomerPortalSession(): Promise<{ url: string; error?: string }> { if (!stripeKey) return { url: "", error: "Stripe not configured" }; const orgId = await getOrganisationId(); const supabase = await createAdminClient(); const { data: sub } = await supabase .from("subscriptions") .select("stripe_customer_id") .eq("organisation_id", orgId) .single(); if (!sub?.stripe_customer_id) return { url: "", error: "No subscription found" }; const Stripe = (await import("stripe")).default; const stripe = new Stripe(stripeKey, { apiVersion: "2026-03-25.dahlia" as any }); const portalSession = await stripe.billingPortal.sessions.create({ customer: sub.stripe_customer_id, return_url: `${process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"}/dashboard`, }); return { url: portalSession.url }; } /** * Cancel subscription. */ export async function cancelSubscription(): Promise<{ success: boolean; error?: string }> { if (!stripeKey) return { success: false, error: "Stripe not configured" }; const orgId = await getOrganisationId(); const supabase = await createAdminClient(); const { data: sub } = await supabase .from("subscriptions") .select("stripe_subscription_id") .eq("organisation_id", orgId) .single(); if (!sub?.stripe_subscription_id) return { success: false, error: "No active subscription" }; const Stripe = (await import("stripe")).default; const stripe = new Stripe(stripeKey, { apiVersion: "2026-03-25.dahlia" as any }); try { await stripe.subscriptions.cancel(sub.stripe_subscription_id); return { success: true }; } catch (err: any) { return { success: false, error: err.message }; } }