196 lines · 9.0 KB
import { env } from "cloudflare:workers";
import { all, first } from "./db";
import { accountDb, repoDb } from "./scoped-db";
import { badRequest, notFoundError } from "./errors";
import { queueEmail, emailTemplates } from "./email";
import { publicUrl } from "./events/emit";
import { notifyUsers } from "./notify/deliver";
import { stripe, stripeEnabled } from "./billing/config";
import { ACTIVE_STATUSES, customerFor } from "./billing/checkout";
import { domainAddon } from "./billing/domain-addon";
import { resumeHosting, suspendHostingFor } from "./sites/suspend";
import { dropMcpAccess } from "./access-revocation";
import { stopAllAccountContainers } from "./editor/container-admin";
import { cancelRunRow } from "./ci/run-control";
import type { WorkflowRunRow } from "./ci/jobs";
import { accountDisableState, clearAccountDisabled, disabledNotice, markAccountDisabled } from "./account-disable";
import type { DisableReason } from "~/lib/account-disable";

const MAX_RUNS_CANCELLED = 200;
const NOTIFIED_TTL_SECONDS = 90 * 24 * 60 * 60;

type Disable = { accountId: string; reason: DisableReason; note: string | null; by: string };

/** Stripe answers a deleted subscription with resource_missing; anything else must make the trigger retry. */
function alreadyGone(error: unknown): boolean {
  return typeof error === "object" && error !== null && Reflect.get(error, "code") === "resource_missing";
}

/** Ends the plan and add-on subscriptions now, with no proration credit, so nothing is charged again. */
export async function cancelAccountSubscriptions(accountId: string): Promise<void> {
  if (!stripeEnabled()) return;
  const [customer, addon] = await Promise.all([customerFor(accountId), domainAddon(accountId)]);
  const ids = [
    ACTIVE_STATUSES.has(customer?.subscription_status ?? "") ? customer?.subscription_id : null,
    ACTIVE_STATUSES.has(addon?.status ?? "") ? addon?.subscription_id : null,
  ].filter((id): id is string => typeof id === "string" && id !== "");
  for (const id of ids) {
    try {
      const live = await stripe().subscriptions.retrieve(id);
      if (live.status === "canceled" || live.status === "incomplete_expired") continue;