/** * Creates (or finds) the Stripe test-mode objects gitfor uses: a Pro monthly price and the prepaid packs, at the * amounts in src/lib/pricing.ts, then writes STRIPE_PRICE_PRO, STRIPE_PRICE_CUSTOM_DOMAIN and STRIPE_TOPUP_PRICES into .dev.vars. Idempotent via price lookup keys. * * STRIPE_SETUP_KEY=sk_test_... bun run scripts/stripe-test-setup.ts [--write .dev.vars] * STRIPE_SETUP_KEY=rk_live_... bun run scripts/stripe-test-setup.ts --live * * Live keys need an explicit `--live` and can't be combined with `--write`, so live price ids never land in * .dev.vars. Never prints the key. */ import { existsSync, readFileSync, writeFileSync } from "node:fs"; import Stripe from "stripe"; import { CUSTOM_DOMAIN_PRICE_CENTS, PACKS, PRO_PRICE_CENTS } from "../src/lib/pricing"; const key = process.env.STRIPE_SETUP_KEY ?? ""; const live = process.argv.includes("--live"); const testKey = /^(sk|rk)_test_/.test(key); const liveKey = /^(sk|rk)_live_/.test(key); if (live ? !liveKey || process.argv.includes("--write") : !testKey) { console.error( live ? "--live needs a live key (sk_live_ or rk_live_) and can't be combined with --write; refusing to continue." : "STRIPE_SETUP_KEY must be a Stripe test-mode key (sk_test_ or rk_test_); pass --live for live mode.", ); process.exit(1); } const stripe = new Stripe(key); /** * The active price for `lookupKey` at exactly `cents`. Stripe prices are immutable, so when pricing.ts changes * the amount a new price takes over the lookup key and the old one is archived (existing subscriptions keep it). */ async function ensurePrice( lookupKey: string, product: string, cents: number, recurring: boolean, metadata: Record, ): Promise { const existing = (await stripe.prices.list({ lookup_keys: [lookupKey], limit: 1, expand: ["data.product"] })).data[0]; if (existing && existing.unit_amount === cents && existing.active) return existing.id; const productId = typeof existing?.product === "object" ? existing.product.id : (existing?.product ?? null); const price = await stripe.prices.create({ currency: "usd", unit_amount: cents, lookup_key: lookupKey, transfer_lookup_key: true, metadata, ...(recurring ? { recurring: { interval: "month" } } : {}), ...(productId ? { product: productId } : { product_data: { name: product, metadata } }), }); if (existing) { const product = typeof price.product === "string" ? price.product : price.product.id; await stripe.products.update(product, { default_price: price.id }); await stripe.prices.update(existing.id, { active: false }); } return price.id; } const pro = await ensurePrice("gitfor_pro_monthly", "gitfor Pro", PRO_PRICE_CENTS, true, { plan: "pro" }); const domains = await ensurePrice( "gitfor_custom_domain_monthly", "gitfor custom domain", CUSTOM_DOMAIN_PRICE_CENTS, true, { addon: "custom_domains" }, ); const topups: Record = {}; for (const pack of PACKS) { topups[pack.id] = await ensurePrice(`gitfor_${pack.id}`, `gitfor ${pack.label}`, pack.cents, false, { pack: pack.id, pool: pack.pool, quantity: String(pack.quantity), }); } const output = { STRIPE_PRICE_PRO: pro, STRIPE_PRICE_CUSTOM_DOMAIN: domains, STRIPE_TOPUP_PRICES: JSON.stringify(topups), }; console.log(JSON.stringify({ pro, domains, packs: topups }, null, 2)); const writeIndex = process.argv.indexOf("--write"); if (writeIndex !== -1) { const file = process.argv[writeIndex + 1] ?? ".dev.vars"; let text = existsSync(file) ? readFileSync(file, "utf8") : ""; for (const [name, value] of Object.entries(output)) { const line = `${name}='${value}'`; const pattern = new RegExp(`^${name}=.*$`, "m"); text = pattern.test(text) ? text.replace(pattern, line) : `${text.replace(/\n?$/, "\n")}${line}\n`; } writeFileSync(file, text); console.log(`Wrote ${Object.keys(output).join(", ")} to ${file}`); }