Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/openstatusHQ/openstatus. ๐ซ Status page with uptime monitoring & API monitoring as code ๐ซ openstatus.dev
bun drizzle-orm monitoring monitoring-as-code nextjs observability on-call open-source shadcn-ui status-page statuspage synthetic-monitoring tinybird turso uptime uptime-checker uptime-monitor
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245import { TRPCError } from "@trpc/server";import Stripe from "stripe";
import { env } from "../../env";import { buildLimitsFromSubscription } from "./utils";
// The constructor throws on an empty key, and this module is loaded at import// time. Self-hosted installs leave STRIPE_SECRET_KEY unset and never call Stripe.export const stripe = new Stripe(env.STRIPE_SECRET_KEY || "sk_unset", { apiVersion: "2026-08-26.dahlia", appInfo: { name: "OpenStatus", version: "0.1.0", },});
// A subscription we cannot classify is a permanent misconfiguration; surface// it as a 400 so Stripe stops retrying instead of hammering the endpoint.export function buildFromSubscriptionOrThrow( subscription: Stripe.Subscription,) { try { return buildLimitsFromSubscription(subscription); } catch (e) { console.error(e); throw new TRPCError({ code: "BAD_REQUEST", message: e instanceof Error ? e.message : "Invalid subscription", }); }}
/** The limits to write, or undefined to keep a custom deal's hand-set ones. */export function syncedLimits( subscription: Stripe.Subscription, built: NonNullable<ReturnType<typeof buildLimitsFromSubscription>>,) { if (built.customPriceIds.length === 0) return built.limits; console.warn( `Subscription ${subscription.id} has custom prices (${built.customPriceIds.join(", ")}); keeping workspace limits`, ); return undefined;}
// Statuses that mean the customer still has a subscription. `incomplete` and// `incomplete_expired` are abandoned checkouts, `unpaid` and `paused` no longer// entitle anything, and `canceled` is gone.const LIVE_STATUSES: Stripe.Subscription.Status[] = [ "active", "trialing", "past_due",];
/** * Every subscription that still entitles the customer to something. * * Auto-paged on purpose: every abandoned checkout leaves an `incomplete` (then * `incomplete_expired`) subscription behind and Stripe returns those in an * unfiltered list, so enough of them push a customer's real subscription past * the first page. Reading one page would report "nothing left" for a paying * customer โ which is what `customer.subscription.deleted` turns into a * destructive downgrade. */export async function listLiveSubscriptions(customerId: string) { const subscriptions = await stripe.subscriptions .list({ customer: customerId, limit: 100 }) .autoPagingToArray({ limit: 1000 });
return subscriptions.filter((sub) => LIVE_STATUSES.includes(sub.status));}
/** * Total order over a customer's subscriptions, newest first. `created` is only * second-granular, so two subscriptions can tie; falling back to the id keeps * every caller โ the "which one is current" pick, the staleness guard and the * cancellation โ resolving a tie the same way instead of each choosing its own * winner and contradicting the others. */export function isNewerSubscription( a: Stripe.Subscription, b: Stripe.Subscription,) { if (a.created !== b.created) return a.created > b.created; return a.id > b.id;}
/** * The customer's live subscriptions and the one the workspace should follow: * the newest of them. Stripe returns `list` newest-first today but does not * contract it, so the pick is explicit. * * Live rather than strictly `active`: a newer subscription that is `trialing` * or `past_due` still entitles the customer, and omitting it would let an * older active one look current and revert the workspace onto its plan. * * Always read through this rather than off a webhook payload: Stripe * guarantees neither delivery order nor exactly-once delivery, and it * serialises the payload with the API version pinned on the *endpoint* rather * than the one this client pins. */export async function getCurrentSubscription(customerId: string) { const live = await listLiveSubscriptions(customerId);
const current = live.reduce<Stripe.Subscription | undefined>( (newest, sub) => newest === undefined || isNewerSubscription(sub, newest) ? sub : newest, undefined, );
return { live, current };}
/** * When the subscription's current period ends. Since API version 2025-03-31 * the period lives on each item rather than on the subscription. Items share * one billing cycle here (add-ons are monthly-only and blocked on yearly * plans), so the latest end is the date everything is paid through. */export function getCurrentPeriodEnd(subscription: Stripe.Subscription) { const ends = subscription.items.data.map((item) => item.current_period_end); if (ends.length === 0) { throw new Error(`Subscription ${subscription.id} has no items`); } return new Date(Math.max(...ends) * 1000);}
/** * A customer carries exactly one subscription. `current` is the one to keep; * every subscription that predates it is a leftover that would otherwise keep * billing. Best-effort โ a Stripe failure here must not fail the caller, which * for a webhook would mean Stripe replaying the whole plan sync. */export async function cancelSupersededSubscriptions( subscriptions: Stripe.Subscription[], current: Stripe.Subscription,) { // Never retire anything in favour of a subscription that is not itself // active. A checkout still settling (`incomplete` โ async payment method, // deferred 3DS) would otherwise leave the customer with nothing active, and // the `customer.subscription.deleted` fired by our own cancellation reads // that as "they cancelled": `downgradeWorkspaceToFree` then hard-deletes // pages, deactivates monitors and strips members on the way to free. if (current.status !== "active") return;
for (const sub of subscriptions) { if (sub.id === current.id) continue; // Only ever cancel *downwards*. A replayed or out-of-order delivery names // a subscription that may already have been superseded, and retiring the // newer one on its behalf is the failure this whole path exists to avoid. if (isNewerSubscription(sub, current)) continue; try { await stripe.subscriptions.cancel(sub.id); } catch (e) { console.error(`Failed to cancel superseded subscription ${sub.id}:`, e); } }}
export async function cancelSubscription(customer?: string) { if (!customer) return;
// Throws on Stripe failure so the caller never deletes an account whose // subscription is still billing. const { current } = await getCurrentSubscription(customer); if (!current) return;
if (current.status === "trialing") { return await stripe.subscriptions.cancel(current.id); }
return await stripe.subscriptions.update(current.id, { cancel_at_period_end: true, cancellation_details: { comment: "Customer deleted their OpenStatus project.", }, });}
export function customerIdOf(subscription: Stripe.Subscription) { return typeof subscription.customer === "string" ? subscription.customer : subscription.customer.id;}
export async function hasPaymentMethod(subscription: Stripe.Subscription) { if (subscription.default_payment_method) return true; const customer = await stripe.customers.retrieve(customerIdOf(subscription)); return ( !customer.deleted && Boolean(customer.invoice_settings?.default_payment_method) );}
// Stripe rejects automatic tax on a subscription whose customer it cannot// place (no address yet, e.g. a card-less trial), so only opt in once it can.// `not_collecting` is a valid location where we are not registered: no tax.export async function canAutomateTax(customerId: string) { const customer = await stripe.customers.retrieve(customerId, { expand: ["tax"], }); if (customer.deleted) return false; const status = customer.tax?.automatic_tax; return status === "supported" || status === "not_collecting";}
export function trialEndsAtOf(subscription: Stripe.Subscription) { return subscription.status === "trialing" && subscription.trial_end ? new Date(subscription.trial_end * 1000) : null;}
type CustomerEmailClient = { customers: { retrieve(id: string): Promise<Stripe.Customer | Stripe.DeletedCustomer>; update( id: string, params: Stripe.CustomerUpdateParams, ): Promise<Stripe.Customer>; };};
/** * After an ownership transfer, move the Stripe customer email to the new * owner, but only when it still equals the former owner's email. A finance * address set in the portal stays. Returns whether the customer was updated. */export async function syncCustomerEmailToNewOwner(args: { customerId: string; formerOwnerEmail: string; newOwnerEmail: string; client?: CustomerEmailClient;}): Promise<boolean> { const client = args.client ?? stripe; const customer = await client.customers.retrieve(args.customerId); if (customer.deleted) return false; const current = customer.email?.trim().toLowerCase(); if (!current || current !== args.formerOwnerEmail.trim().toLowerCase()) { return false; } await client.customers.update(args.customerId, { email: args.newOwnerEmail, }); return true;}