331 lines · 13.0 KB
import { env } from "cloudflare:workers";
import type Stripe from "stripe";
import { all, first, run } from "../db";
import { applyCreditMovement, normalizePool, purchasesForPayment } from "../metering/credits";
import type { CreditPurchase } from "../metering/credits";
import type { CreditPoolId } from "~/lib/pricing";
import { setPlan } from "../metering/quota";
import { stripe, stripeEnabled, webCrypto } from "./config";
import { ACTIVE_STATUSES, accountForCustomer, customerFor, linkCustomer } from "./checkout";
import { captureServerEvent } from "../analytics";

export const STRIPE_WEBHOOK_PATH = "/stripe/webhook";
const MAX_BODY_BYTES = 512 * 1024;

function idOf(value: string | { id: string } | null | undefined): string | null {
  if (!value) return null;
  return typeof value === "string" ? value : value.id;
}

/** The account a Stripe object belongs to: the mapped customer first, else our metadata (then it is linked). */
async function accountFor(
  customer: string | null,
  metadata: Stripe.Metadata | null | undefined,
): Promise<string | null> {
  if (customer) {
    const mapped = await accountForCustomer(customer);
    if (mapped) return mapped;
  }
  const claimed = metadata?.account_id;
  if (!claimed || !(await first("SELECT 1 AS one FROM accounts WHERE id = ?", claimed))) return null;
  if (customer) await linkCustomer(claimed, customer);
  // A customer already linked to another account never moves: trust the mapping, not the metadata.
  if (customer && (await accountForCustomer(customer)) !== claimed) return accountForCustomer(customer);
  return claimed;
}

async function saveSubscription(
  accountId: string,
  fields: { id: string; status: string; priceId: string | null; periodEnd: number | null; cancelAtPeriodEnd: boolean },
  eventAt: number,