127 lines · 5.2 KB
import { env } from "cloudflare:workers";
import { first } from "../db";
import { accountDb } from "../scoped-db";
import { meterDef } from "~/lib/meters";
import type { MeterId } from "~/lib/meters";
import { isPlanId, parsePlans } from "~/lib/plans";
import type { Plan } from "~/lib/plans";
import { emailConfigured } from "../email";

export function plans(): Record<string, Plan> {
  return parsePlans(env.METERING_PLANS);
}

export type BillingSettings = { plan: Plan; spendingLimitCents: number | null; disabled?: boolean };

const DEFAULT_PAST_DUE_GRACE_DAYS = 14;
const DAY_MS = 86_400_000;

/** Days a past_due subscription keeps Pro (PAST_DUE_GRACE_DAYS, default 14) before usage falls back to Free. */
export function pastDueGraceMs(raw = env.PAST_DUE_GRACE_DAYS): number {
  const days = raw === undefined || raw === "" ? DEFAULT_PAST_DUE_GRACE_DAYS : Number(raw);
  return (Number.isFinite(days) && days >= 0 ? days : DEFAULT_PAST_DUE_GRACE_DAYS) * DAY_MS;
}

export async function billingSettings(accountId: string): Promise<BillingSettings> {
  const all = plans();
  if (accountId.startsWith("scratch:")) return { plan: all.scratch, spendingLimitCents: 0 };
  const [billing, directory] = await Promise.all([
    accountDb(accountId).first<BillingFields>(BILLING_SQL, accountId),
    first<DirectoryFields>(directorySql(emailConfigured()), accountId),
  ]);
  const row: BillingRow | null = billing || directory ? { ...EMPTY_BILLING, ...billing, ...directory } : null;
  let planId = row?.plan && isPlanId(row.plan) ? row.plan : "free";
  // Stripe may leave a subscription past_due indefinitely; Pro compute stops after the grace period anyway.
  if (
    planId === "pro" &&
    !row?.plan_override &&
    row?.subscription_status === "past_due" &&
    // No recorded start (a row predating the column) never counts as lapsed: its grace starts now.
    Date.now() - (row.past_due_since ?? Date.now()) > pastDueGraceMs()