{"version":3,"sources":["../../src/stripe/index.ts","../../src/stripe/client.ts","../../src/stripe/prices.ts"],"sourcesContent":["export { getStripe, resetStripeClientCache } from './client';\nexport {\n  syncProductToStripe,\n  archiveProductInStripe,\n  type SyncResult,\n  type SyncOptions,\n} from './prices';\n","/**\n * F136 Phase 1 — Stripe SDK wrapper.\n *\n * Lazy-init: the Stripe client is constructed the first time it's needed,\n * not at import time. Two reasons:\n *   1. Tests can run without STRIPE_SECRET_KEY (calls are mocked elsewhere).\n *   2. Sites that don't actually use Stripe (free-tier marketing sites\n *      that only consume product collections) won't crash on missing env.\n *\n * Mode: per-site. Each site loads its own STRIPE_SECRET_KEY from env;\n * `getStripe(secretKey?)` accepts an explicit key for multi-tenant cms-admin\n * where the key lives in the site config rather than process.env.\n */\nimport Stripe from 'stripe';\n\nconst cache = new Map<string, Stripe>();\n\n/**\n * Get a memoized Stripe client.\n *\n * @param secretKey — explicit key (e.g. site.stripeSecretKey from cms-admin\n *                    site config). Falls back to process.env.STRIPE_SECRET_KEY.\n */\nexport function getStripe(secretKey?: string): Stripe {\n  const key = secretKey ?? process.env.STRIPE_SECRET_KEY;\n  if (!key) {\n    throw new Error(\n      '[cms-shop] STRIPE_SECRET_KEY is not set. ' +\n      'Set it as an env var on this site (or pass secretKey explicitly to getStripe).',\n    );\n  }\n  let client = cache.get(key);\n  if (!client) {\n    client = new Stripe(key, {\n      // Pin the API version so future Stripe upgrades don't silently\n      // change response shapes mid-deploy. We use the SDK's own\n      // pinned literal (`Stripe.API_VERSION`) so the value always\n      // matches the version the installed types were built against.\n      apiVersion: Stripe.API_VERSION,\n      typescript: true,\n      // Identify the integration in Stripe's logs — easier debugging\n      // when multiple webhouse sites share a Stripe account.\n      appInfo: {\n        name: '@webhouse/cms-shop',\n        version: '0.3.0',\n        url: 'https://docs.webhouse.app',\n      },\n    });\n    cache.set(key, client);\n  }\n  return client;\n}\n\n/** Test/cleanup hook — clears the memoized client cache. */\nexport function resetStripeClientCache(): void {\n  cache.clear();\n}\n","/**\n * F136 Phase 1 — Stripe product/price sync.\n *\n * Idempotent push from CMS document → Stripe Product + Prices.\n *   - First call creates the Stripe Product and one Price per currency.\n *   - Subsequent calls update the Product, archive stale Prices, and\n *     create new Prices for changed amounts (Stripe Prices are immutable\n *     once attached to a Subscription, so we never mutate; we archive +\n *     replace).\n *   - The CMS document's `stripeProductId` and `stripePriceIds` fields\n *     are populated and returned so the caller can persist them.\n *\n * Called from the cms-admin `content.afterCreate` and `afterUpdate`\n * hooks (wired up when @webhouse/cms-shop is registered as a module).\n */\nimport type Stripe from 'stripe';\nimport { getStripe } from './client';\nimport type { ShopProduct, MoneyAmountByCurrency } from '../types';\n\nexport interface SyncResult {\n  /** Stripe Product id (created or reused). */\n  stripeProductId: string;\n  /** Map of currency → Stripe Price id (active price for each currency). */\n  stripePriceIds: Record<string, string>;\n  /** Stripe Price ids that were archived in this sync (changed amounts). */\n  archivedPriceIds: string[];\n  /** Whether the Stripe Product was created (true) or updated (false). */\n  created: boolean;\n}\n\nexport interface SyncOptions {\n  /** Pass an explicit secret key (multi-tenant cms-admin). Falls back to env. */\n  secretKey?: string;\n  /** Override the active flag (e.g. archive on product deletion). */\n  active?: boolean;\n}\n\n/**\n * Push a CMS product document to Stripe.\n *\n * Behaviour:\n *   - status='active'  → Stripe Product is active, Prices are active\n *   - status='draft'   → Stripe Product is INactive (no checkout possible)\n *   - status='archived'→ Stripe Product is INactive, Prices archived too\n */\nexport async function syncProductToStripe(\n  product: ShopProduct,\n  opts: SyncOptions = {},\n): Promise<SyncResult> {\n  const stripe = getStripe(opts.secretKey);\n\n  const data = product.data;\n  const status = data.status ?? 'active';\n  const active = opts.active ?? status === 'active';\n\n  // ── 1. Upsert Product ────────────────────────────────────────────────\n  let stripeProductId = data.stripeProductId;\n  let created = false;\n  const productPayload: Stripe.ProductUpdateParams = {\n    name: data.title,\n    active,\n    metadata: {\n      cms_product_id: product.id,\n      cms_slug: product.slug,\n      cms_locale: product.locale ?? '',\n      cms_translation_group: product.translationGroup ?? '',\n      cms_product_type: data.productType,\n      cms_sku: data.sku ?? '',\n    },\n  };\n  if (data.shortDescription) productPayload.description = data.shortDescription;\n  if (data.images && data.images.length > 0) {\n    productPayload.images = data.images.slice(0, 8);  // Stripe caps at 8\n  }\n\n  if (stripeProductId) {\n    await stripe.products.update(stripeProductId, productPayload);\n  } else {\n    const newProduct = await stripe.products.create({\n      ...productPayload,\n      // `id` is optional — let Stripe generate one. We persist it on the doc.\n    } as Stripe.ProductCreateParams);\n    stripeProductId = newProduct.id;\n    created = true;\n  }\n\n  // ── 2. Sync Prices per currency ──────────────────────────────────────\n  // Strategy: list existing active prices for the product. For each\n  // currency in priceByCurrency: if amount matches an active price, keep.\n  // Otherwise archive the old price and create a new one. Currencies\n  // removed from the map get their prices archived.\n  const desired: MoneyAmountByCurrency = data.priceByCurrency ?? {};\n  const archivedPriceIds: string[] = [];\n  const stripePriceIds: Record<string, string> = {};\n\n  const existing = await stripe.prices.list({\n    product: stripeProductId,\n    active: true,\n    limit: 100,\n  });\n\n  // Index existing by currency for quick lookup\n  const existingByCurrency = new Map<string, Stripe.Price>();\n  for (const p of existing.data) {\n    existingByCurrency.set(p.currency.toUpperCase(), p);\n  }\n\n  for (const [currency, amount] of Object.entries(desired)) {\n    if (!Number.isFinite(amount) || amount <= 0) continue;\n    const cur = currency.toUpperCase();\n    const current = existingByCurrency.get(cur);\n\n    if (current && current.unit_amount === amount && current.active) {\n      // Amount unchanged — reuse.\n      stripePriceIds[cur] = current.id;\n      existingByCurrency.delete(cur);\n      continue;\n    }\n\n    if (current) {\n      // Archive the old one (Stripe forbids mutating prices already used\n      // in subscriptions; the safe pattern is archive + replace).\n      await stripe.prices.update(current.id, { active: false });\n      archivedPriceIds.push(current.id);\n      existingByCurrency.delete(cur);\n    }\n\n    if (active) {\n      const newPrice = await stripe.prices.create({\n        product: stripeProductId,\n        currency: cur.toLowerCase(),\n        unit_amount: amount,\n        // tax_behavior controls whether the amount above is inclusive or\n        // exclusive of tax. taxIncluded is the per-product override; if\n        // unset, Stripe uses the account default.\n        ...(typeof data.taxIncluded === 'boolean'\n          ? { tax_behavior: data.taxIncluded ? 'inclusive' as const : 'exclusive' as const }\n          : {}),\n      });\n      stripePriceIds[cur] = newPrice.id;\n    }\n  }\n\n  // Archive currencies removed from the desired map.\n  for (const stale of existingByCurrency.values()) {\n    await stripe.prices.update(stale.id, { active: false });\n    archivedPriceIds.push(stale.id);\n  }\n\n  return { stripeProductId, stripePriceIds, archivedPriceIds, created };\n}\n\n/**\n * Archive a product in Stripe (called when the CMS document is deleted\n * or its status changes to 'archived'). Stripe doesn't allow deleting\n * Products that have ever been associated with a Charge or Subscription,\n * so we always archive instead of delete.\n */\nexport async function archiveProductInStripe(\n  stripeProductId: string,\n  opts: SyncOptions = {},\n): Promise<void> {\n  const stripe = getStripe(opts.secretKey);\n  await stripe.products.update(stripeProductId, { active: false });\n\n  // Archive all active prices too — otherwise checkout sessions could\n  // still reference them and create orders for archived products.\n  const prices = await stripe.prices.list({\n    product: stripeProductId,\n    active: true,\n    limit: 100,\n  });\n  for (const p of prices.data) {\n    await stripe.prices.update(p.id, { active: false });\n  }\n}\n"],"mappings":"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","names":["Stripe"]}