499 lines · 21.3 KB
import { env } from "cloudflare:workers";
import type Stripe from "stripe";
import { first, run } from "../db";
import { accountDb } from "../scoped-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 { liveMode, stripe, stripeEnabled, webCrypto } from "./config";
import { ACTIVE_STATUSES, accountForCustomer, customerFor, linkCustomer } from "./checkout";
import { captureServerEvent } from "../analytics";
import { DOMAIN_ADDON, isDomainAddonPrice, saveDomainAddon } from "./domain-addon";
import { disableAccount, notifyAdminsAboutAccount } from "../account-disable-actions";

export const STRIPE_WEBHOOK_PATH = "/stripe/webhook";
const MAX_BODY_BYTES = 512 * 1024;
const DISPUTE_HANDLED_TTL_SECONDS = 365 * 24 * 60 * 60;

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;
}