@fonderie/billing 5.3.1 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -0
- package/brain/outcomes.md +71 -0
- package/brain/signatures.md +191 -17
- package/dist/{index-Byy5mBE4.d.ts → index-BdNYDuhk.d.ts} +107 -9
- package/dist/{index-DjAGcrSi.d.cts → index-Ca4pXx07.d.cts} +107 -9
- package/dist/index.cjs +1097 -146
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +137 -15
- package/dist/index.d.ts +137 -15
- package/dist/index.js +1076 -145
- package/dist/index.js.map +1 -1
- package/dist/middlewares/index.cjs +180 -0
- package/dist/middlewares/index.cjs.map +1 -1
- package/dist/middlewares/index.d.cts +1 -1
- package/dist/middlewares/index.d.ts +1 -1
- package/dist/middlewares/index.js +180 -0
- package/dist/middlewares/index.js.map +1 -1
- package/dist/migrations/sql/006_wallet.sql +85 -0
- package/dist/types.cjs +17 -3
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +34 -7
- package/dist/types.d.ts +34 -7
- package/dist/types.js +13 -2
- package/dist/types.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/routes.ts","../src/schemas.ts","../src/services/price-cache.ts","../src/controllers/plan.controller.ts","../src/services/plans.ts","../src/models/plan.model.ts","../src/dtos/billing.ts","../src/controllers/subscription.controller.ts","../src/services/subscriptions.ts","../src/models/subscription.model.ts","../src/utils.ts","../src/controllers/checkout.controller.ts","../src/controllers/usage.controller.ts","../src/services/usage.ts","../src/models/usage.model.ts","../src/controllers/webhook.controller.ts","../src/middlewares/billing.ts","../src/config.ts","../src/services/policy.ts","../src/backends/memory.ts","../src/backends/db.ts","../src/backends/index.ts","../src/module.ts","../src/types.ts","../src/providers/stripe.ts","../src/middlewares/require-plan.ts","../src/helpers.ts"],"sourcesContent":["// ── Public API ───────────────────────────────────────────────────\nexport { BillingModule } from './module';\nexport { StripeProvider } from './providers/stripe';\n\n// Middleware\nexport { requirePlan } from './middlewares/require-plan';\nexport { withBilling } from './middlewares/billing';\n\n// Helpers — sync, read from cached ctx.meta['billing']\nexport { hasFeature, getPlanLimit, getLimitStatus, requireFeature } from './helpers';\n\n// Config + constants\nexport { MESSAGE_KEYS } from './config';\nexport type {\n\tIBillingConfig,\n\tIBillingPlan,\n\tIBillingPlanDefaults,\n\tIBillingPlanPrice,\n\tIBillingPricingConfig,\n\tRateLimitBackendConfig,\n\tIBillingNotificationsConfig,\n\tBillingMessageKey,\n} from './config';\n\n// Backends\nexport { MemoryCounterBackend, DBCounterBackend } from './backends';\nexport type { ICounterBackend } from './backends';\n\nexport { BILLING_INTERVAL } from './types';\nexport type { BillingInterval } from './types';\n// Types\nexport type { IBillingProvider, IBillingEvent, IResolvedPrice } from './providers/types';\nexport type {\n\tIPlan,\n\tISubscription,\n\tIUsageRecord,\n\tSubscriptionStatus,\n\tPolicyEntry,\n\tLimitStatus,\n\tIPolicyStatus,\n\tIBillingContext,\n} from './types';\nexport type { IPlanDTO, ISubscriptionDTO, IUsageRecordDTO } from './dtos/billing';\n\n// DTOs\nexport { toPlanDTO, toSubscriptionDTO, toUsageRecordDTO } from './dtos/billing';\n\n// Services (for advanced usage)\nexport { recordUsage, getUsage } from './services/usage';\nexport {\n\tgetPlans,\n\tgetPlanByName,\n\tgetDBPlans,\n\tgetPlanById,\n\tcreatePlan,\n\tupdatePlan,\n\tdeletePlan,\n} from './services/plans';\nexport { getSubscription } from './services/subscriptions';\n\n// Request validation — enforced contract for body-taking routes (webhook\n// excluded: provider-shaped, signature-verified). Exported for docs/clients.\nexport * as schemas from './schemas';\n","import type { IStoreAdapter } from '@fonderie/store';\nimport type { Middleware } from '@fonderie/core';\nimport { requireAuth, validate } from '@fonderie/core/middlewares';\n\nimport { checkoutSchema, createPlanSchema, recordUsageSchema, updatePlanSchema } from './schemas';\n\nimport type { IBillingConfig } from './config';\nimport { PriceCache } from './services/price-cache';\nimport { planController } from './controllers/plan.controller';\nimport { subscriptionController } from './controllers/subscription.controller';\nimport { checkoutController } from './controllers/checkout.controller';\nimport { usageController } from './controllers/usage.controller';\nimport { webhookController } from './controllers/webhook.controller';\n\ntype RouteDefinition = [string, string, ...Middleware[]];\n\nexport function buildBillingRoutes(\n\tstore: IStoreAdapter,\n\tconfig: IBillingConfig,\n): RouteDefinition[] {\n\tconst priceCache = new PriceCache({\n\t\tttlMs: config.pricing?.cacheTtlMs,\n\t\tgraceMs: config.pricing?.transferGraceMs,\n\t\tmaxStaleMs: config.pricing?.maxStaleMs,\n\t});\n\tconst plan = planController(store, config, priceCache);\n\tconst subscription = subscriptionController(store);\n\tconst checkout = checkoutController(store, config);\n\tconst usage = usageController(store);\n\tconst webhook = webhookController(store, config, priceCache);\n\n\treturn [\n\t\t// Plans — public read-only\n\t\t['GET', '/plans', plan.list],\n\t\t['GET', '/plans/:planId', plan.get],\n\n\t\t// Plans — admin write (caller is responsible for authorization)\n\t\t['POST', '/plans', validate(createPlanSchema), plan.create],\n\t\t['PUT', '/plans/:planId', validate(updatePlanSchema), plan.update],\n\t\t['DELETE', '/plans/:planId', plan.delete],\n\n\t\t// Billing — subscriber resolved from X-Workspace-ID header (workspace) or session (user)\n\t\t// Workspace membership is verified automatically by the withBilling global middleware\n\t\t['GET', '/billing/subscription', requireAuth, subscription.get],\n\t\t['POST', '/billing/checkout', requireAuth, validate(checkoutSchema), checkout.createSession],\n\t\t['POST', '/billing/portal', requireAuth, checkout.createPortal],\n\t\t['POST', '/billing/usage', requireAuth, validate(recordUsageSchema), usage.record],\n\t\t['GET', '/billing/usage/:metric', requireAuth, usage.get],\n\n\t\t// Webhook — signature verified inside the handler\n\t\t['POST', '/billing/webhook', webhook.handle],\n\t];\n}\n","import { z } from 'zod';\n\n// Request schemas — the validation contract for billing's body-taking routes\n// (webhook excluded: provider-shaped, signature-verified in the handler).\n// Wired via @fonderie/core's validate(); same pattern as @fonderie/auth.\n\nconst planFields = {\n\tdescription: z.string().max(2000).nullable().optional(),\n\ttier: z.number().int().min(0).optional(),\n\tseats: z.number().int().min(0).nullable().optional(),\n\ttrialDays: z.number().int().min(0).optional(),\n\tmonthlyAmount: z.number().min(0).nullable().optional(),\n\tmonthlyPriceId: z.string().max(200).nullable().optional(),\n\tyearlyAmount: z.number().min(0).nullable().optional(),\n\tyearlyPriceId: z.string().max(200).nullable().optional(),\n\tfeatures: z.unknown().optional(),\n\tmetadata: z.unknown().optional(),\n};\n\nexport const createPlanSchema = z.object({\n\tname: z.string().trim().min(1, 'name is required').max(200),\n\t...planFields,\n});\n\nexport const updatePlanSchema = z\n\t.object({ name: z.string().trim().min(1).max(200).optional(), ...planFields })\n\t.refine((o) => Object.values(o).some((v) => v !== undefined), 'Provide at least one field');\n\nexport const checkoutSchema = z.object({\n\tplan: z.string().min(1, 'plan is required'),\n\tinterval: z.enum(['month', 'year']).optional(),\n});\n\nexport const recordUsageSchema = z.object({\n\tmetric: z.string().min(1, 'metric is required').max(100),\n\tquantity: z.number().min(0).optional(),\n});\n","import type { IBillingProvider, IResolvedPrice } from '../providers/types';\n\nexport interface IPriceCacheOptions {\n\tttlMs?: number | undefined; // fresh window — default 5m\n\tgraceMs?: number | undefined; // serve last-cached on transient miss (transfer race) — default 1h\n\tmaxStaleMs?: number | undefined; // serve last-cached during provider outage — default 24h\n}\n\nexport interface IPriceLookup {\n\t/** Resolved price, or null if it could not be resolved at all. */\n\tprice: IResolvedPrice | null;\n\t/** True when the returned price is served past its fresh TTL (transfer grace / outage). */\n\tstale: boolean;\n}\n\n/**\n * Read-through cache over the billing provider's price resolution.\n * - fresh within `ttlMs`\n * - single-flight: concurrent misses for the same id share one provider call (§16.2)\n * - transient miss (provider returns null — e.g. lookup_key transfer window) →\n * serve last-cached within `graceMs`, marked stale (§16.1)\n * - provider throws (outage) → serve last-cached within `maxStaleMs`, marked stale (§16.8)\n */\nexport class PriceCache {\n\tprivate readonly ttl: number;\n\tprivate readonly grace: number;\n\tprivate readonly maxStale: number;\n\tprivate readonly byId = new Map<string, { price: IResolvedPrice; at: number }>();\n\tprivate readonly inflight = new Map<string, Promise<IResolvedPrice | null>>();\n\n\tconstructor(opts: IPriceCacheOptions = {}) {\n\t\tthis.ttl = opts.ttlMs ?? 300_000;\n\t\tthis.grace = opts.graceMs ?? 3_600_000;\n\t\tthis.maxStale = opts.maxStaleMs ?? 86_400_000;\n\t}\n\n\tasync byPriceId(priceId: string, provider: IBillingProvider): Promise<IPriceLookup> {\n\t\tconst now = Date.now();\n\t\tconst hit = this.byId.get(priceId);\n\t\tif (hit && now - hit.at < this.ttl) return { price: hit.price, stale: false };\n\n\t\tlet fresh: IResolvedPrice | null;\n\t\ttry {\n\t\t\tfresh = await this.single(priceId, () => provider.resolvePriceById(priceId));\n\t\t} catch {\n\t\t\t// Provider outage — serve last-cached within maxStale.\n\t\t\tif (hit && now - hit.at < this.maxStale) return { price: hit.price, stale: true };\n\t\t\treturn { price: null, stale: true };\n\t\t}\n\t\tif (fresh) {\n\t\t\tthis.byId.set(priceId, { price: fresh, at: now });\n\t\t\treturn { price: fresh, stale: false };\n\t\t}\n\t\t// Transient miss (e.g. lookup_key transfer window) — serve last-cached within grace.\n\t\tif (hit && now - hit.at < this.grace) return { price: hit.price, stale: true };\n\t\treturn { price: null, stale: true };\n\t}\n\n\tinvalidate(priceId?: string): void {\n\t\tif (priceId) this.byId.delete(priceId);\n\t\telse this.byId.clear();\n\t}\n\n\t/** Warm the cache with prices already resolved elsewhere (e.g. boot guard). */\n\tprime(prices: Iterable<IResolvedPrice>): void {\n\t\tconst now = Date.now();\n\t\tfor (const p of prices) this.byId.set(p.priceId, { price: p, at: now });\n\t}\n\n\tprivate single(key: string, run: () => Promise<IResolvedPrice | null>): Promise<IResolvedPrice | null> {\n\t\tconst existing = this.inflight.get(key);\n\t\tif (existing) return existing;\n\t\tconst p = run().finally(() => this.inflight.delete(key));\n\t\tthis.inflight.set(key, p);\n\t\treturn p;\n\t}\n}\n","import { setApiResponse, HTTP, stringOrEmpty, numberOrZero } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IBillingConfig } from '../config';\nimport type { IPlan } from '../types';\nimport type { IPlanDTO } from '../dtos/billing';\nimport { PlanModel } from '../models/plan.model';\nimport { toPlanDTO } from '../dtos/billing';\nimport { PriceCache } from '../services/price-cache';\n\n// Read-through hydration: override the DTO's amount/currency with live Stripe\n// prices (source of truth). Best-effort per plan — on error (incl. currency\n// mismatch, §16.4) keep the fallback amount/currency and flag pricingStale.\nasync function hydratePricing(\n\tdto: IPlanDTO,\n\tplan: IPlan,\n\tconfig: IBillingConfig,\n\tcache: PriceCache,\n): Promise<void> {\n\ttry {\n\t\tlet stale = false;\n\t\tconst resolve = async (priceId: string | null) => {\n\t\t\tif (!priceId) return null;\n\t\t\tconst r = await cache.byPriceId(priceId, config.provider);\n\t\t\tif (r.stale) stale = true;\n\t\t\treturn r.price;\n\t\t};\n\t\tconst [m, y] = await Promise.all([resolve(plan.monthlyPriceId), resolve(plan.yearlyPriceId)]);\n\t\tif (m && y && m.currency !== y.currency) {\n\t\t\tthrow new Error(\n\t\t\t\t`[billing] plan \"${plan.name}\": monthly/yearly currency mismatch (${m.currency} vs ${y.currency})`,\n\t\t\t);\n\t\t}\n\t\tif (m) dto.pricing.monthly = m.unitAmount;\n\t\tif (y) dto.pricing.yearly = y.unitAmount;\n\t\tconst currency = m?.currency ?? y?.currency;\n\t\tif (currency) dto.pricing.currency = currency.toUpperCase();\n\t\tif (stale) dto.pricingStale = true;\n\t} catch (err) {\n\t\t// eslint-disable-next-line no-console\n\t\tconsole.error(`[billing] pricing hydration failed for \"${plan.name}\":`, (err as Error).message);\n\t\tdto.pricingStale = true;\n\t}\n}\n\nexport function planController(store: IStoreAdapter, config: IBillingConfig, cache: PriceCache) {\n\tconst plans = new PlanModel(store);\n\tconst hydrate = config.pricing?.hydration === true;\n\n\treturn {\n\t\tasync list(_ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst list = await plans.list();\n\t\t\tconst dtos = list.map(toPlanDTO);\n\t\t\tif (hydrate) {\n\t\t\t\tawait Promise.all(dtos.map((dto, i) => hydratePricing(dto, list[i]!, config, cache)));\n\t\t\t}\n\t\t\treturn setApiResponse(HTTP.OK, 'PLAN_LIST', `Retrieved ${list.length} workspace plans`, {\n\t\t\t\tplans: dtos,\n\t\t\t});\n\t\t},\n\n\t\tasync get(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst params = ctx.meta['params'] as Record<string, string> | undefined;\n\t\t\tconst id = params?.['planId'];\n\t\t\tif (!id) return setApiResponse(HTTP.BAD_REQUEST, 'INVALID_PARAMETER', 'Plan ID required');\n\n\t\t\tconst plan = await plans.findById(id);\n\t\t\tif (!plan) return setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Plan not found');\n\n\t\t\tconst dto = toPlanDTO(plan);\n\t\t\tif (hydrate) await hydratePricing(dto, plan, config, cache);\n\n\t\t\treturn setApiResponse(HTTP.OK, 'PLAN_FETCHED', 'Plan retrieved successfully.', {\n\t\t\t\tplan: dto,\n\t\t\t});\n\t\t},\n\n\t\tasync create(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\t\t\tconst name = stringOrEmpty(body?.['name']);\n\t\t\tif (!name) {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'VALIDATION_ERROR', 'name is required');\n\t\t\t}\n\n\t\t\tconst plan = await plans.create({\n\t\t\t\tname,\n\t\t\t\tdescription: body?.['description'] != null ? String(body['description']) : null,\n\t\t\t\ttier: body?.['tier'] != null ? numberOrZero(body['tier']) : 0,\n\t\t\t\tseats: body?.['seats'] != null ? numberOrZero(body['seats']) : null,\n\t\t\t\ttrialDays: body?.['trialDays'] != null ? numberOrZero(body['trialDays']) : 0,\n\t\t\t\tmonthlyAmount: body?.['monthlyAmount'] != null ? numberOrZero(body['monthlyAmount']) : null,\n\t\t\t\tmonthlyPriceId: body?.['monthlyPriceId'] != null ? String(body['monthlyPriceId']) : null,\n\t\t\t\tyearlyAmount: body?.['yearlyAmount'] != null ? numberOrZero(body['yearlyAmount']) : null,\n\t\t\t\tyearlyPriceId: body?.['yearlyPriceId'] != null ? String(body['yearlyPriceId']) : null,\n\t\t\t\tfeatures: body?.['features'],\n\t\t\t\tmetadata: body?.['metadata'],\n\t\t\t});\n\n\t\t\treturn setApiResponse(HTTP.CREATED, 'PLAN_CREATED', 'Plan created successfully.', {\n\t\t\t\tplan: toPlanDTO(plan),\n\t\t\t});\n\t\t},\n\n\t\tasync update(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst params = ctx.meta['params'] as Record<string, string> | undefined;\n\t\t\tconst id = params?.['planId'];\n\t\t\tif (!id) {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_PARAMETER', 'Plan ID required');\n\t\t\t}\n\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\t\t\tif (!body || Object.keys(body).length === 0) {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'VALIDATION_ERROR', 'Request body is empty');\n\t\t\t}\n\n\t\t\tconst patch: Record<string, unknown> = {};\n\t\t\tconst allowed = ['name', 'description', 'tier', 'seats', 'trialDays',\n\t\t\t\t'monthlyAmount', 'monthlyPriceId', 'yearlyAmount', 'yearlyPriceId',\n\t\t\t\t'features', 'metadata'];\n\n\t\t\tfor (const key of allowed) {\n\t\t\t\tif (key in body) patch[key] = body[key];\n\t\t\t}\n\n\t\t\tconst plan = await plans.update(id, patch);\n\t\t\tif (!plan) {\n\t\t\t\treturn setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Plan not found');\n\t\t\t}\n\n\t\t\treturn setApiResponse(HTTP.OK, 'PLAN_UPDATED', 'Plan updated successfully.', {\n\t\t\t\tplan: toPlanDTO(plan),\n\t\t\t});\n\t\t},\n\n\t\tasync delete(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst params = ctx.meta['params'] as Record<string, string> | undefined;\n\t\t\tconst id = params?.['planId'];\n\t\t\tif (!id) {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_PARAMETER', 'Plan ID required');\n\t\t\t}\n\n\t\t\tconst deleted = await plans.delete(id);\n\t\t\tif (!deleted) {\n\t\t\t\treturn setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Plan not found');\n\t\t\t}\n\n\t\t\treturn setApiResponse(HTTP.OK, 'PLAN_DELETED', 'Plan deleted successfully.');\n\t\t},\n\t};\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IPlan } from '../types';\nimport type { IBillingConfig, IBillingPlan } from '../config';\n\nexport function getPlans(config: IBillingConfig): IBillingPlan[] {\n\treturn config.plans;\n}\n\nexport function getPlanByName(name: string, config: IBillingConfig): IBillingPlan | null {\n\treturn config.plans.find((p) => p.name.toLowerCase() === name.toLowerCase()) ?? null;\n}\n\n/**\n * Attribute a subscription to a Fonderie plan by its price, precedence\n * `lookup_key → priceId` (§16.3). Pure — pass the plans list. Returns null so the\n * caller can fall back to the legacy nickname-derived plan.\n */\nexport function resolvePlanNameByPrice(\n\tprice: { lookupKey?: string | null; priceId?: string | null },\n\tplans: IBillingPlan[],\n): string | null {\n\tconst find = (pred: (p?: { lookupKey?: string; priceId?: string }) => boolean) =>\n\t\tplans.find((pl) => pred(pl.monthly) || pred(pl.yearly))?.name ?? null;\n\tif (price.lookupKey) {\n\t\tconst m = find((p) => p?.lookupKey === price.lookupKey);\n\t\tif (m) return m;\n\t}\n\tif (price.priceId) {\n\t\tconst m = find((p) => p?.priceId === price.priceId);\n\t\tif (m) return m;\n\t}\n\treturn null;\n}\n\nexport async function syncPlansToDB(config: IBillingConfig, store: IStoreAdapter): Promise<void> {\n\tconst plans = config.plans;\n\tif (plans.length === 0) return;\n\n\tconst values = plans.map((_, i) => {\n\t\tconst b = i * 9;\n\t\treturn `($${b + 1}, $${b + 2}, $${b + 3}, $${b + 4}, $${b + 5}, $${b + 6}, $${b + 7}, $${b + 8}, $${b + 9}::jsonb)`;\n\t});\n\n\tconst params = plans.flatMap((plan) => [\n\t\tplan.name,\n\t\tplan.trialDays ?? 0,\n\t\tplan.monthly?.amount ?? null,\n\t\tplan.monthly?.priceId ?? null,\n\t\tplan.yearly?.amount ?? null,\n\t\tplan.yearly?.priceId ?? null,\n\t\tplan.description ?? null,\n\t\tplan.tier ?? 0,\n\t\tJSON.stringify(plan.metadata ?? {}),\n\t]);\n\n\tawait store.query(\n\t\t`INSERT INTO fonderie_plans\n\t\t\t(name, trial_days,\n\t\t\t monthly_amount, monthly_price_id,\n\t\t\t yearly_amount, yearly_price_id,\n\t\t\t description, tier, metadata)\n\t\tVALUES ${values.join(', ')}\n\t\tON CONFLICT (name) DO UPDATE SET\n\t\t\ttrial_days = EXCLUDED.trial_days,\n\t\t\tmonthly_amount = EXCLUDED.monthly_amount,\n\t\t\tmonthly_price_id = EXCLUDED.monthly_price_id,\n\t\t\tyearly_amount = EXCLUDED.yearly_amount,\n\t\t\tyearly_price_id = EXCLUDED.yearly_price_id,\n\t\t\tdescription = EXCLUDED.description,\n\t\t\ttier = EXCLUDED.tier,\n\t\t\tmetadata = EXCLUDED.metadata`,\n\t\tparams,\n\t);\n}\n\nconst SELECT_PLAN = `\n\tSELECT\n\t\tid,\n\t\tname,\n\t\tseats,\n\t\ttrial_days AS \"trialDays\",\n\t\tmonthly_amount AS \"monthlyAmount\",\n\t\tmonthly_price_id AS \"monthlyPriceId\",\n\t\tyearly_amount AS \"yearlyAmount\",\n\t\tyearly_price_id AS \"yearlyPriceId\",\n\t\tdescription,\n\t\ttier,\n\t\tfeatures,\n\t\tmetadata\n\tFROM fonderie_plans`;\n\nexport async function getDBPlans(store: IStoreAdapter): Promise<IPlan[]> {\n\treturn store.query<IPlan>(\n\t\t`${SELECT_PLAN} WHERE active = true ORDER BY tier ASC, monthly_amount ASC NULLS LAST`,\n\t);\n}\n\nexport async function getPlanById(id: string, store: IStoreAdapter): Promise<IPlan | null> {\n\tconst [row] = await store.query<IPlan>(`${SELECT_PLAN} WHERE id = $1`, [id]);\n\treturn row ?? null;\n}\n\nexport async function createPlan(\n\tdata: {\n\t\tname: string;\n\t\tdescription?: string | null;\n\t\ttier?: number;\n\t\tseats?: number | null;\n\t\ttrialDays?: number;\n\t\tfeatures?: unknown;\n\t\tmetadata?: unknown;\n\t\tmonthlyAmount?: number | null;\n\t\tmonthlyPriceId?: string | null;\n\t\tyearlyAmount?: number | null;\n\t\tyearlyPriceId?: string | null;\n\t},\n\tstore: IStoreAdapter,\n): Promise<IPlan> {\n\tconst [row] = await store.query<IPlan>(\n\t\t`INSERT INTO fonderie_plans\n\t\t\t(name, seats, trial_days, monthly_amount, monthly_price_id,\n\t\t\t yearly_amount, yearly_price_id, description, tier, features, metadata)\n\t\tVALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10::jsonb, $11::jsonb)\n\t\tRETURNING\n\t\t\tid, name, seats,\n\t\t\ttrial_days AS \"trialDays\",\n\t\t\tmonthly_amount AS \"monthlyAmount\",\n\t\t\tmonthly_price_id AS \"monthlyPriceId\",\n\t\t\tyearly_amount AS \"yearlyAmount\",\n\t\t\tyearly_price_id AS \"yearlyPriceId\",\n\t\t\tdescription, tier, features, metadata`,\n\t\t[\n\t\t\tdata.name,\n\t\t\tdata.seats ?? null,\n\t\t\tdata.trialDays ?? 0,\n\t\t\tdata.monthlyAmount ?? null,\n\t\t\tdata.monthlyPriceId ?? null,\n\t\t\tdata.yearlyAmount ?? null,\n\t\t\tdata.yearlyPriceId ?? null,\n\t\t\tdata.description ?? null,\n\t\t\tdata.tier ?? 0,\n\t\t\tJSON.stringify(data.features ?? []),\n\t\t\tJSON.stringify(data.metadata ?? {}),\n\t\t],\n\t);\n\tif (!row) throw new Error('Failed to create plan');\n\treturn row;\n}\n\nexport async function updatePlan(\n\tid: string,\n\tdata: Partial<Omit<IPlan, 'id'>>,\n\tstore: IStoreAdapter,\n): Promise<IPlan | null> {\n\tconst fieldMap: Record<string, string> = {\n\t\tname: 'name',\n\t\tseats: 'seats',\n\t\ttrialDays: 'trial_days',\n\t\tmonthlyAmount: 'monthly_amount',\n\t\tmonthlyPriceId: 'monthly_price_id',\n\t\tyearlyAmount: 'yearly_amount',\n\t\tyearlyPriceId: 'yearly_price_id',\n\t\tdescription: 'description',\n\t\ttier: 'tier',\n\t};\n\n\tconst jsonbFields = new Set(['features', 'metadata']);\n\tconst setClauses: string[] = [];\n\tconst params: unknown[] = [id];\n\n\tfor (const [key, col] of Object.entries(fieldMap)) {\n\t\tif (key in data) {\n\t\t\tparams.push((data as Record<string, unknown>)[key]);\n\t\t\tsetClauses.push(`${col} = $${params.length}`);\n\t\t}\n\t}\n\n\tfor (const key of jsonbFields) {\n\t\tif (key in data) {\n\t\t\tparams.push(JSON.stringify((data as Record<string, unknown>)[key]));\n\t\t\tsetClauses.push(`${key} = $${params.length}::jsonb`);\n\t\t}\n\t}\n\n\tif (setClauses.length === 0) return getPlanById(id, store);\n\n\tconst [row] = await store.query<IPlan>(\n\t\t`UPDATE fonderie_plans SET ${setClauses.join(', ')}\n\t\tWHERE id = $1\n\t\tRETURNING\n\t\t\tid, name, seats,\n\t\t\ttrial_days AS \"trialDays\",\n\t\t\tmonthly_amount AS \"monthlyAmount\",\n\t\t\tmonthly_price_id AS \"monthlyPriceId\",\n\t\t\tyearly_amount AS \"yearlyAmount\",\n\t\t\tyearly_price_id AS \"yearlyPriceId\",\n\t\t\tdescription, tier, features, metadata`,\n\t\tparams,\n\t);\n\treturn row ?? null;\n}\n\nexport async function deletePlan(id: string, store: IStoreAdapter): Promise<boolean> {\n\tconst rows = await store.query<{ id: string }>(\n\t\t`DELETE FROM fonderie_plans WHERE id = $1 RETURNING id`,\n\t\t[id],\n\t);\n\treturn rows.length > 0;\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IPlan } from '../types';\nimport type { IBillingConfig } from '../config';\nimport {\n\tgetDBPlans,\n\tgetPlanById,\n\tcreatePlan,\n\tupdatePlan,\n\tdeletePlan,\n\tgetPlans,\n\tgetPlanByName,\n} from '../services/plans';\n\nexport class PlanModel {\n\tconstructor(private readonly store: IStoreAdapter) {}\n\n\tlistFromConfig(config: IBillingConfig) {\n\t\treturn getPlans(config);\n\t}\n\n\tfindByNameInConfig(name: string, config: IBillingConfig) {\n\t\treturn getPlanByName(name, config);\n\t}\n\n\tlist(): Promise<IPlan[]> {\n\t\treturn getDBPlans(this.store);\n\t}\n\n\tfindById(id: string): Promise<IPlan | null> {\n\t\treturn getPlanById(id, this.store);\n\t}\n\n\tcreate(data: Parameters<typeof createPlan>[0]): Promise<IPlan> {\n\t\treturn createPlan(data, this.store);\n\t}\n\n\tupdate(id: string, data: Parameters<typeof updatePlan>[1]): Promise<IPlan | null> {\n\t\treturn updatePlan(id, data, this.store);\n\t}\n\n\tdelete(id: string): Promise<boolean> {\n\t\treturn deletePlan(id, this.store);\n\t}\n}\n","import type { IPlan, IPlanFeature, ISubscription, IUsageRecord, SubscriberType } from '../types';\n\nexport interface IPlanDTO {\n\tid: string;\n\tplanId: string;\n\tname: string;\n\tdescription: string;\n\ttier: number;\n\tseats: number | null;\n\ttrialDays: number;\n\tpricing: {\n\t\tmonthly: number; // in cents, e.g. 1999 = $19.99\n\t\tyearly: number; // in cents\n\t\tcurrency: string; // ISO 4217, e.g. 'USD'\n\t};\n\t/** True when pricing was served from stale cache (transfer window / provider outage). */\n\tpricingStale?: boolean;\n\tfeatures: IPlanFeature[];\n\tmetadata: Record<string, unknown>;\n}\n\nexport interface ISubscriptionDTO {\n\tid: string;\n\tsubscriberType: SubscriberType;\n\tsubscriberId: string;\n\tplan: string;\n\tinterval: string;\n\tstatus: string;\n\tcancelAtPeriodEnd: boolean;\n\tcurrentPeriodStart: string | null;\n\tcurrentPeriodEnd: string | null;\n\ttrialEndsAt: string | null;\n\tcreatedAt: string;\n}\n\nexport interface IUsageRecordDTO {\n\tid: string;\n\tsubscriberType: SubscriberType;\n\tsubscriberId: string;\n\tmetric: string;\n\tquantity: number;\n\trecordedAt: string;\n}\n\nexport function toPlanDTO(plan: IPlan): IPlanDTO {\n\treturn {\n\t\tid: plan.id,\n\t\tplanId: plan.name.toUpperCase(),\n\t\tname: plan.name,\n\t\tdescription: plan.description ?? '',\n\t\ttier: plan.tier,\n\t\tseats: plan.seats,\n\t\ttrialDays: plan.trialDays,\n\t\tpricing: {\n\t\t\tmonthly: plan.monthlyAmount ?? 0,\n\t\t\tyearly: plan.yearlyAmount ?? 0,\n\t\t\tcurrency: 'USD',\n\t\t},\n\t\tfeatures: Array.isArray(plan.features) ? plan.features : [],\n\t\tmetadata:\n\t\t\tplan.metadata && typeof plan.metadata === 'object'\n\t\t\t\t? (plan.metadata as Record<string, unknown>)\n\t\t\t\t: {},\n\t};\n}\n\nexport function toSubscriptionDTO(sub: ISubscription): ISubscriptionDTO {\n\treturn {\n\t\tid: sub.id,\n\t\tsubscriberType: sub.subscriberType,\n\t\tsubscriberId: sub.subscriberId,\n\t\tplan: sub.plan,\n\t\tinterval: sub.interval,\n\t\tstatus: sub.status,\n\t\tcancelAtPeriodEnd: sub.cancelAtPeriodEnd,\n\t\tcurrentPeriodStart: sub.currentPeriodStart,\n\t\tcurrentPeriodEnd: sub.currentPeriodEnd,\n\t\ttrialEndsAt: sub.trialEndsAt,\n\t\tcreatedAt: sub.createdAt,\n\t};\n}\n\nexport function toUsageRecordDTO(record: IUsageRecord): IUsageRecordDTO {\n\treturn {\n\t\tid: record.id,\n\t\tsubscriberType: record.subscriberType,\n\t\tsubscriberId: record.subscriberId,\n\t\tmetric: record.metric,\n\t\tquantity: record.quantity,\n\t\trecordedAt: record.recordedAt,\n\t};\n}\n","import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { SubscriptionModel } from '../models/subscription.model';\nimport { toSubscriptionDTO } from '../dtos/billing';\nimport { resolveSubscriber } from '../utils';\n\nexport function subscriptionController(store: IStoreAdapter) {\n\tconst subscriptions = new SubscriptionModel(store);\n\n\treturn {\n\t\tasync get(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst subscriber = resolveSubscriber(ctx);\n\t\t\tif (!subscriber) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.BAD_REQUEST,\n\t\t\t\t\t'SUBSCRIBER_REQUIRED',\n\t\t\t\t\t'Subscriber context required',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst subscription = await subscriptions.get(subscriber.type, subscriber.id);\n\t\t\tif (!subscription)\n\t\t\t\treturn setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No active subscription');\n\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.OK,\n\t\t\t\t'SUBSCRIPTION_FETCHED',\n\t\t\t\t'Subscription retrieved successfully.',\n\t\t\t\t{\n\t\t\t\t\tsubscription: toSubscriptionDTO(subscription),\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\t};\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { ISubscription, SubscriberType } from '../types';\n\nconst SELECT_SUBSCRIPTION = `\n\tSELECT\n\t\tid,\n\t\tsubscriber_type AS \"subscriberType\",\n\t\tsubscriber_id AS \"subscriberId\",\n\t\tplan,\n\t\tinterval,\n\t\tstatus,\n\t\tprovider_customer_id AS \"providerCustomerId\",\n\t\tprovider_subscription_id AS \"providerSubscriptionId\",\n\t\tcurrent_period_start AS \"currentPeriodStart\",\n\t\tcurrent_period_end AS \"currentPeriodEnd\",\n\t\tcancel_at_period_end AS \"cancelAtPeriodEnd\",\n\t\ttrial_ends_at AS \"trialEndsAt\",\n\t\tcreated_at AS \"createdAt\"\n\tFROM fonderie_subscriptions`;\n\nexport async function getSubscription(\n\tsubscriberType: SubscriberType,\n\tsubscriberId: string,\n\tstore: IStoreAdapter,\n): Promise<ISubscription | null> {\n\tconst [row] = await store.query<ISubscription>(\n\t\t`${SELECT_SUBSCRIPTION} WHERE subscriber_type = $1 AND subscriber_id = $2`,\n\t\t[subscriberType, subscriberId],\n\t);\n\treturn row ?? null;\n}\n\nexport async function upsertSubscription(\n\tdata: {\n\t\tsubscriberType: SubscriberType;\n\t\tsubscriberId: string;\n\t\tplan: string;\n\t\tinterval?: 'month' | 'year';\n\t\tstatus: string;\n\t\tproviderCustomerId?: string;\n\t\tproviderSubscriptionId?: string;\n\t\tcurrentPeriodStart?: Date;\n\t\tcurrentPeriodEnd?: Date;\n\t\tcancelAtPeriodEnd?: boolean;\n\t\ttrialEndsAt?: Date | null;\n\t},\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tawait store.query(\n\t\t`INSERT INTO fonderie_subscriptions\n\t\t\t(subscriber_type, subscriber_id, plan, interval, status,\n\t\t\t provider_customer_id, provider_subscription_id,\n\t\t\t current_period_start, current_period_end,\n\t\t\t cancel_at_period_end, trial_ends_at)\n\t\t VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)\n\t\t ON CONFLICT (subscriber_type, subscriber_id) DO UPDATE SET\n\t\t\t plan = $3,\n\t\t\t interval = $4,\n\t\t\t status = $5,\n\t\t\t provider_customer_id = COALESCE($6, fonderie_subscriptions.provider_customer_id),\n\t\t\t provider_subscription_id = COALESCE($7, fonderie_subscriptions.provider_subscription_id),\n\t\t\t current_period_start = $8,\n\t\t\t current_period_end = $9,\n\t\t\t cancel_at_period_end = $10,\n\t\t\t trial_ends_at = $11`,\n\t\t[\n\t\t\tdata.subscriberType,\n\t\t\tdata.subscriberId,\n\t\t\tdata.plan,\n\t\t\tdata.interval ?? 'month',\n\t\t\tdata.status,\n\t\t\tdata.providerCustomerId ?? null,\n\t\t\tdata.providerSubscriptionId ?? null,\n\t\t\tdata.currentPeriodStart ?? null,\n\t\t\tdata.currentPeriodEnd ?? null,\n\t\t\tdata.cancelAtPeriodEnd ?? false,\n\t\t\tdata.trialEndsAt ?? null,\n\t\t],\n\t);\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { ISubscription, SubscriberType } from '../types';\nimport { getSubscription, upsertSubscription } from '../services/subscriptions';\n\nexport class SubscriptionModel {\n\tconstructor(private readonly store: IStoreAdapter) {}\n\n\tget(subscriberType: SubscriberType, subscriberId: string): Promise<ISubscription | null> {\n\t\treturn getSubscription(subscriberType, subscriberId, this.store);\n\t}\n\n\tupsert(data: Parameters<typeof upsertSubscription>[0]): Promise<void> {\n\t\treturn upsertSubscription(data, this.store);\n\t}\n}\n","import type { IFonderieContext } from '@fonderie/core';\n\nimport type { SubscriberType } from './types';\n\nexport interface ISubscriber {\n\ttype: SubscriberType;\n\tid: string;\n}\n\n// Converts window strings like '1d', '30d', '1h' to milliseconds.\nexport function parseWindowMs(window: string): number {\n\tconst n = parseInt(window, 10);\n\tconst unit = window.slice(String(n).length);\n\tswitch (unit) {\n\t\tcase 'h':\n\t\t\treturn n * 3_600_000;\n\t\tcase 'd':\n\t\t\treturn n * 86_400_000;\n\t\tcase 'm':\n\t\t\treturn n * 60_000;\n\t\tdefault:\n\t\t\tthrow new Error(`Unknown window unit: '${unit}' in '${window}'`);\n\t}\n}\n\n// Resolves billing subscriber from request context.\n// Precedence: X-Workspace-ID header → ctx.workspace (set by withWorkspace) → ctx.user\nexport function resolveSubscriber(ctx: IFonderieContext): ISubscriber | null {\n\tconst wsFromHeader = ctx.request.headers.get('x-workspace-id');\n\n\tif (wsFromHeader) {\n\t\treturn {\n\t\t\ttype: 'workspace',\n\t\t\tid: wsFromHeader,\n\t\t};\n\t}\n\n\tif (ctx.workspace?.id) {\n\t\treturn {\n\t\t\ttype: 'workspace',\n\t\t\tid: ctx.workspace.id,\n\t\t};\n\t}\n\n\tif (ctx.user?.id) {\n\t\treturn {\n\t\t\ttype: 'user',\n\t\t\tid: ctx.user.id,\n\t\t};\n\t}\n\n\treturn null;\n}\n","import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IBillingConfig } from '../config';\nimport { PlanModel } from '../models/plan.model';\nimport { SubscriptionModel } from '../models/subscription.model';\nimport { resolveSubscriber } from '../utils';\n\nexport function checkoutController(store: IStoreAdapter, config: IBillingConfig) {\n\tconst plans = new PlanModel(store);\n\tconst subscriptions = new SubscriptionModel(store);\n\n\treturn {\n\t\tasync createSession(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\t\t\tconst planName = body?.['plan'];\n\t\t\tconst interval = (body?.['interval'] ?? 'month') as 'month' | 'year';\n\t\t\tconst subscriber = resolveSubscriber(ctx);\n\n\t\t\tif (typeof planName !== 'string') {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'plan is required');\n\t\t\t}\n\t\t\tif (interval !== 'month' && interval !== 'year') {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t\t'interval must be month or year',\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (!subscriber) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.BAD_REQUEST,\n\t\t\t\t\t'SUBSCRIBER_REQUIRED',\n\t\t\t\t\t'Subscriber context required',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst plan = plans.findByNameInConfig(planName, config);\n\t\t\tif (!plan) {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', `Unknown plan: ${planName}`);\n\t\t\t}\n\n\t\t\tconst pricing = interval === 'year' ? plan.yearly : plan.monthly;\n\t\t\tif (!pricing?.priceId) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t\t`Plan ${planName} does not support ${interval} billing`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// If there's already an active subscription: allow upgrades only, and\n\t\t\t// change the subscription in place (proration) rather than opening a\n\t\t\t// second checkout / creating a duplicate subscription.\n\t\t\tconst current = await subscriptions.get(subscriber.type, subscriber.id);\n\t\t\tconst ACTIVE = ['active', 'trialing', 'past_due'];\n\t\t\tif (current && ACTIVE.includes(current.status)) {\n\t\t\t\tconst currentTier = plans.findByNameInConfig(current.plan, config)?.tier ?? -1;\n\t\t\t\tconst targetTier = plan.tier ?? -1;\n\t\t\t\tif (targetTier <= currentTier) {\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t\t'DOWNGRADE_NOT_ALLOWED',\n\t\t\t\t\t\t`Cannot switch from ${current.plan} to a same-or-lower tier (${planName}) mid-cycle. Upgrades only.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (current.providerSubscriptionId) {\n\t\t\t\t\tconst res = await config.provider.updateSubscription({\n\t\t\t\t\t\tsubscriptionId: current.providerSubscriptionId,\n\t\t\t\t\t\tpriceId: pricing.priceId,\n\t\t\t\t\t});\n\t\t\t\t\tconst upsert: Parameters<typeof subscriptions.upsert>[0] = {\n\t\t\t\t\t\tsubscriberType: subscriber.type,\n\t\t\t\t\t\tsubscriberId: subscriber.id,\n\t\t\t\t\t\tplan: planName,\n\t\t\t\t\t\tinterval,\n\t\t\t\t\t\tstatus: res.status,\n\t\t\t\t\t\tproviderSubscriptionId: current.providerSubscriptionId,\n\t\t\t\t\t};\n\t\t\t\t\tif (current.providerCustomerId) upsert.providerCustomerId = current.providerCustomerId;\n\t\t\t\t\tif (res.currentPeriodStart) upsert.currentPeriodStart = res.currentPeriodStart;\n\t\t\t\t\tif (res.currentPeriodEnd) upsert.currentPeriodEnd = res.currentPeriodEnd;\n\t\t\t\t\tawait subscriptions.upsert(upsert);\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t\t'SUBSCRIPTION_UPGRADED',\n\t\t\t\t\t\t'Subscription upgraded; the prorated difference was charged.',\n\t\t\t\t\t\t{ upgraded: true, plan: planName },\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst { customerId } = await config.provider.createCustomer({\n\t\t\t\temail: ctx.user!.email ?? '',\n\t\t\t\tsubscriberType: subscriber.type,\n\t\t\t\tsubscriberId: subscriber.id,\n\t\t\t\tuserId: ctx.user!.id,\n\t\t\t});\n\n\t\t\tconst sessionOpts: Parameters<typeof config.provider.createCheckoutSession>[0] = {\n\t\t\t\tcustomerId,\n\t\t\t\tpriceId: pricing.priceId,\n\t\t\t\tsubscriberType: subscriber.type,\n\t\t\t\tsubscriberId: subscriber.id,\n\t\t\t\tsuccessUrl: config.successUrl,\n\t\t\t\tcancelUrl: config.cancelUrl,\n\t\t\t};\n\t\t\tif (plan.trialDays !== undefined) sessionOpts.trialDays = plan.trialDays;\n\n\t\t\tconst { url } = await config.provider.createCheckoutSession(sessionOpts);\n\n\t\t\tawait subscriptions.upsert({\n\t\t\t\tsubscriberType: subscriber.type,\n\t\t\t\tsubscriberId: subscriber.id,\n\t\t\t\tplan: planName,\n\t\t\t\tinterval,\n\t\t\t\tstatus: 'incomplete',\n\t\t\t\tproviderCustomerId: customerId,\n\t\t\t});\n\n\t\t\treturn setApiResponse(HTTP.OK, 'CHECKOUT_URL', 'Checkout session created.', { url });\n\t\t},\n\n\t\tasync createPortal(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst subscriber = resolveSubscriber(ctx);\n\t\t\tif (!subscriber) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.BAD_REQUEST,\n\t\t\t\t\t'SUBSCRIBER_REQUIRED',\n\t\t\t\t\t'Subscriber context required',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst subscription = await subscriptions.get(subscriber.type, subscriber.id);\n\t\t\tif (!subscription?.providerCustomerId) {\n\t\t\t\treturn setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No active subscription');\n\t\t\t}\n\n\t\t\tconst { url } = await config.provider.createPortalSession({\n\t\t\t\tcustomerId: subscription.providerCustomerId,\n\t\t\t\treturnUrl: config.successUrl,\n\t\t\t});\n\n\t\t\treturn setApiResponse(HTTP.OK, 'PORTAL_URL', 'Portal session created.', { url });\n\t\t},\n\t};\n}\n","import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { UsageModel } from '../models/usage.model';\nimport { resolveSubscriber } from '../utils';\n\nexport function usageController(store: IStoreAdapter) {\n\tconst usage = new UsageModel(store);\n\n\treturn {\n\t\tasync record(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\t\t\tconst metric = body?.['metric'];\n\t\t\tconst quantity = body?.['quantity'];\n\t\t\tconst subscriber = resolveSubscriber(ctx);\n\n\t\t\tif (!subscriber) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.BAD_REQUEST,\n\t\t\t\t\t'SUBSCRIBER_REQUIRED',\n\t\t\t\t\t'Subscriber context required',\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (typeof metric !== 'string') {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'metric is required');\n\t\t\t}\n\n\t\t\tawait usage.record({\n\t\t\t\tsubscriberType: subscriber.type,\n\t\t\t\tsubscriberId: subscriber.id,\n\t\t\t\tmetric,\n\t\t\t\tquantity: typeof quantity === 'number' ? quantity : 1,\n\t\t\t});\n\t\t\treturn setApiResponse(HTTP.OK, 'USAGE_RECORDED', 'Usage recorded successfully.');\n\t\t},\n\n\t\tasync get(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst params = ctx.meta['params'] as Record<string, string> | undefined;\n\t\t\tconst metric = params?.['metric'];\n\t\t\tconst subscriber = resolveSubscriber(ctx);\n\n\t\t\tif (!subscriber || !metric) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t\t'subscriber and metric are required',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst since = new Date();\n\t\t\tsince.setDate(1);\n\t\t\tsince.setHours(0, 0, 0, 0);\n\n\t\t\tconst total = await usage.get(subscriber.type, subscriber.id, metric, since);\n\t\t\treturn setApiResponse(HTTP.OK, 'USAGE_FETCHED', 'Usage retrieved successfully.', {\n\t\t\t\tmetric,\n\t\t\t\ttotal,\n\t\t\t\tsince,\n\t\t\t});\n\t\t},\n\t};\n}\n","import type { IStoreAdapter } from '@fonderie/store';\nimport type { SubscriberType } from '../types';\n\nexport async function recordUsage(\n\topts: { subscriberType: SubscriberType; subscriberId: string; metric: string; quantity: number },\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tawait store.query(\n\t\t`INSERT INTO fonderie_usage_records (subscriber_type, subscriber_id, metric, quantity)\n\t\tVALUES ($1, $2, $3, $4)`,\n\t\t[opts.subscriberType, opts.subscriberId, opts.metric, opts.quantity],\n\t);\n}\n\nexport async function getUsage(\n\tsubscriberType: SubscriberType,\n\tsubscriberId: string,\n\tmetric: string,\n\tsince: Date,\n\tstore: IStoreAdapter,\n): Promise<number> {\n\tconst rows = await store.query<{ total: string }>(\n\t\t`SELECT COALESCE(SUM(quantity), 0) AS total\n\t\tFROM fonderie_usage_records\n\t\tWHERE subscriber_type = $1\n\t\t\tAND subscriber_id = $2\n\t\t\tAND metric = $3\n\t\t\tAND recorded_at >= $4`,\n\t\t[subscriberType, subscriberId, metric, since],\n\t);\n\treturn parseInt(rows[0]?.total ?? '0', 10);\n}\n","import type { IStoreAdapter } from '@fonderie/store';\nimport type { SubscriberType } from '../types';\n\nimport { recordUsage, getUsage } from '../services/usage';\n\nexport class UsageModel {\n\tconstructor(private readonly store: IStoreAdapter) {}\n\n\trecord(opts: Parameters<typeof recordUsage>[0]): Promise<void> {\n\t\treturn recordUsage(opts, this.store);\n\t}\n\n\tget(\n\t\tsubscriberType: SubscriberType,\n\t\tsubscriberId: string,\n\t\tmetric: string,\n\t\tsince: Date,\n\t): Promise<number> {\n\t\treturn getUsage(subscriberType, subscriberId, metric, since, this.store);\n\t}\n}\n","import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IBillingConfig } from '../config';\nimport type { PriceCache } from '../services/price-cache';\nimport { SubscriptionModel } from '../models/subscription.model';\nimport { resolvePlanNameByPrice } from '../services/plans';\n\nexport function webhookController(store: IStoreAdapter, config: IBillingConfig, priceCache?: PriceCache) {\n\tconst subscriptions = new SubscriptionModel(store);\n\n\treturn {\n\t\tasync handle(ctx: IFonderieContext): Promise<Response> {\n\t\t\tif (!config.webhookSecret) {\n\t\t\t\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Webhook secret not configured');\n\t\t\t}\n\n\t\t\tconst signature =\n\t\t\t\tctx.request.headers.get('stripe-signature') ??\n\t\t\t\tctx.request.headers.get('paypal-auth-algo') ??\n\t\t\t\t'';\n\n\t\t\tif (!signature) {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_REQUEST', 'Missing webhook signature');\n\t\t\t}\n\n\t\t\tconst payload = await ctx.request.text();\n\n\t\t\tlet event: Awaited<ReturnType<typeof config.provider.constructEvent>>;\n\t\t\ttry {\n\t\t\t\tevent = await config.provider.constructEvent({\n\t\t\t\t\tpayload,\n\t\t\t\t\tsignature,\n\t\t\t\t\tsecret: config.webhookSecret,\n\t\t\t\t});\n\t\t\t} catch {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_REQUEST', 'Invalid webhook signature');\n\t\t\t}\n\n\t\t\t// §8: keep the price cache honest. Invalidate on any price/product change\n\t\t\t// regardless of arrival order (invalidate-and-refetch is order-safe).\n\t\t\tif (priceCache && (event.type.startsWith('price.') || event.type.startsWith('product.'))) {\n\t\t\t\tpriceCache.invalidate();\n\t\t\t}\n\n\t\t\tif (event.subscription) {\n\t\t\t\t// A deletion resolves to the free/canceled state set by the provider;\n\t\t\t\t// otherwise map the plan from the price (dual-mapping), falling back to\n\t\t\t\t// the nickname-derived value.\n\t\t\t\tconst plan =\n\t\t\t\t\tevent.type === 'customer.subscription.deleted'\n\t\t\t\t\t\t? event.subscription.plan\n\t\t\t\t\t\t: resolvePlanNameByPrice(event.subscription, config.plans) ?? event.subscription.plan;\n\n\t\t\t\tawait subscriptions.upsert({\n\t\t\t\t\tsubscriberType: event.subscription.subscriberType,\n\t\t\t\t\tsubscriberId: event.subscription.subscriberId,\n\t\t\t\t\tplan,\n\t\t\t\t\tinterval: event.subscription.interval,\n\t\t\t\t\tstatus: event.subscription.status,\n\t\t\t\t\tproviderCustomerId: event.subscription.providerCustomerId,\n\t\t\t\t\tproviderSubscriptionId: event.subscription.providerSubscriptionId,\n\t\t\t\t\tcurrentPeriodStart: event.subscription.currentPeriodStart,\n\t\t\t\t\tcurrentPeriodEnd: event.subscription.currentPeriodEnd,\n\t\t\t\t\tcancelAtPeriodEnd: event.subscription.cancelAtPeriodEnd,\n\t\t\t\t\ttrialEndsAt: event.subscription.trialEndsAt,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn Response.json({ received: true });\n\t\t},\n\t};\n}\n","import type { Middleware, ICourierMessage } from '@fonderie/core';\nimport { setApiResponse, HTTP } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IBillingConfig } from '../config';\nimport type { ICounterBackend } from '../backends/types';\nimport { MESSAGE_KEYS } from '../config';\nimport { getSubscription } from '../services/subscriptions';\nimport { buildBillingContext } from '../services/policy';\nimport { resolveSubscriber, parseWindowMs } from '../utils';\n\n// In-process de-dup: tracks which threshold notifications have fired this session.\n// Acceptable to lose on restart (may send one duplicate after a redeploy).\nconst notified = new Set<string>();\n\nexport function withBilling(\n\tstore: IStoreAdapter,\n\tconfig: IBillingConfig,\n\tbackend: ICounterBackend,\n): Middleware {\n\treturn async (ctx, next) => {\n\t\tconst subscriber = resolveSubscriber(ctx);\n\n\t\t// No subscriber (unauthenticated / public route) — skip entirely\n\t\tif (!subscriber) return next();\n\n\t\t// Resolve subscription → plan name (fall back to first plan = free)\n\t\tconst subscription = await getSubscription(subscriber.type, subscriber.id, store);\n\t\tconst planName = subscription?.plan ?? config.plans[0]?.name ?? 'free';\n\t\tconst active =\n\t\t\t!subscription || subscription.status === 'active' || subscription.status === 'trialing';\n\n\t\tconst plan = config.plans.find((p) => p.name === planName) ?? config.plans[0];\n\t\tif (!plan) return next();\n\n\t\t// Increment windowed (rate-limit) counters and read their current totals\n\t\tconst counters: Record<string, number> = {};\n\n\t\tfor (const [key, entry] of Object.entries(plan.policy ?? {})) {\n\t\t\tif ('enabled' in entry || !entry.window) continue;\n\n\t\t\tconst windowMs = parseWindowMs(entry.window);\n\t\t\tconst counterKey = `${subscriber.type}:${subscriber.id}:${key}`;\n\t\t\tcounters[key] = await backend.increment(counterKey, windowMs);\n\t\t}\n\n\t\t// Build and cache billing context on ctx\n\t\tconst billingCtx = buildBillingContext({ subscriber, plan, active, counters });\n\t\tctx.meta['billing'] = billingCtx;\n\n\t\t// Block requests that have hit a hard limit\n\t\tfor (const [key, status] of Object.entries(billingCtx.statuses)) {\n\t\t\tif (status.type === 'counter' && status.status === 'blocked') {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.TOO_MANY_REQUESTS,\n\t\t\t\t\t'RATE_LIMIT_EXCEEDED',\n\t\t\t\t\t`Limit exceeded for: ${key}`,\n\t\t\t\t\t{ key, limit: status.limit, used: status.used, resetsAt: status.resetsAt },\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\t// Fire threshold notifications (once per subscriber per key per session)\n\t\tif (config.notifications) {\n\t\t\tconst toNotify: ICourierMessage[] = [];\n\t\t\tconst recipient = {\n\t\t\t\temail: ctx.user?.email ?? null,\n\t\t\t\tphone: null,\n\t\t\t\tdeviceToken: null,\n\t\t\t};\n\n\t\t\tfor (const [key, status] of Object.entries(billingCtx.statuses)) {\n\t\t\t\tif (status.type !== 'counter' || status.limit === null) continue;\n\n\t\t\t\tconst base = `${subscriber.type}:${subscriber.id}:${key}`;\n\n\t\t\t\tif (config.notifications.softHit && status.status === 'over_limit') {\n\t\t\t\t\tconst nk = `${base}:reached`;\n\t\t\t\t\tif (!notified.has(nk)) {\n\t\t\t\t\t\tnotified.add(nk);\n\t\t\t\t\t\ttoNotify.push({\n\t\t\t\t\t\t\ttype: MESSAGE_KEYS.limitReached,\n\t\t\t\t\t\t\trecipient,\n\t\t\t\t\t\t\tdata: {\n\t\t\t\t\t\t\t\tkey,\n\t\t\t\t\t\t\t\tplan: plan.name,\n\t\t\t\t\t\t\t\tlimit: status.limit,\n\t\t\t\t\t\t\t\tused: status.used,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t} else if (config.notifications.warnAt && status.status === 'warning') {\n\t\t\t\t\tconst nk = `${base}:warning`;\n\t\t\t\t\tif (!notified.has(nk)) {\n\t\t\t\t\t\tnotified.add(nk);\n\t\t\t\t\t\ttoNotify.push({\n\t\t\t\t\t\t\ttype: MESSAGE_KEYS.limitWarning,\n\t\t\t\t\t\t\trecipient,\n\t\t\t\t\t\t\tdata: {\n\t\t\t\t\t\t\t\tkey,\n\t\t\t\t\t\t\t\tplan: plan.name,\n\t\t\t\t\t\t\t\tlimit: status.limit,\n\t\t\t\t\t\t\t\tused: status.used,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (toNotify.length > 0) {\n\t\t\t\tconst existing = ctx.meta['messages'] as ICourierMessage[] | undefined;\n\t\t\t\tctx.meta['messages'] = [...(existing ?? []), ...toNotify];\n\t\t\t}\n\t\t}\n\n\t\treturn next();\n\t};\n}\n","import type { IBillingProvider } from './providers/types';\nimport type { PolicyEntry } from './types';\nimport type { ICounterBackend } from './backends/types';\n\nexport interface IBillingPlanPrice {\n\t/** Stable Stripe Price lookup_key, e.g. \"pro_monthly\". Preferred reference. */\n\tlookupKey?: string;\n\t/** Stripe price id. Used for hydration and as a lookup_key fallback. */\n\tpriceId?: string;\n\t/**\n\t * Display amount in cents — the seed value written to fonderie_plans and the\n\t * fallback shown (flagged pricingStale) when hydration is off or Stripe is\n\t * unreachable. When hydration resolves a live price, the live amount wins.\n\t */\n\tamount?: number;\n}\n\n/**\n * Read-through pricing: amount/currency come from Stripe (source of truth) rather\n * than the duplicated `amount` above. Gated by `hydration` (kill-switch, §16.9).\n * See packages/billing/docs/pricing-hydration.md.\n */\nexport interface IBillingPricingConfig {\n\t/** Kill-switch. When false (default), serve the configured amount/USD directly. */\n\thydration?: boolean;\n\t/** Fresh-cache TTL. Default 300_000 (5m). */\n\tcacheTtlMs?: number;\n\t/** Serve last-cached price on a transient resolution miss (lookup_key transfer). Default 3_600_000 (1h). */\n\ttransferGraceMs?: number;\n\t/** Max age to serve stale price during a Stripe outage before giving up. Default 86_400_000 (24h). */\n\tmaxStaleMs?: number;\n}\n\nexport interface IBillingPlanDefaults {\n\twarnAt?: number; // default warnAt fraction (0–1) for counter policies\n\tbuffer?: number; // default buffer for counter policies\n}\n\nexport interface IBillingPlan {\n\tname: string;\n\tdescription?: string;\n\ttier?: number;\n\ttrialDays?: number;\n\tmonthly?: IBillingPlanPrice;\n\tyearly?: IBillingPlanPrice;\n\tdefaults?: IBillingPlanDefaults;\n\tpolicy?: Record<string, PolicyEntry>;\n\tmetadata?: Record<string, unknown>;\n}\n\nexport type RateLimitBackendConfig = 'memory' | 'db' | ICounterBackend;\n\nexport interface IBillingNotificationsConfig {\n\twarnAt?: boolean; // fire courier message when warnAt threshold crossed\n\tsoftHit?: boolean; // fire when soft limit crossed\n}\n\nexport interface IBillingConfig {\n\tprovider: IBillingProvider;\n\tplans: IBillingPlan[];\n\tsuccessUrl: string;\n\tcancelUrl: string;\n\twebhookSecret?: string;\n\trateLimit?: { backend?: RateLimitBackendConfig };\n\tnotifications?: IBillingNotificationsConfig;\n\tpricing?: IBillingPricingConfig;\n}\n\nexport const MESSAGE_KEYS = {\n\tlimitWarning: 'billing.limit-warning',\n\tlimitReached: 'billing.limit-reached',\n\tlimitBlocked: 'billing.limit-blocked',\n} as const;\n\nexport type BillingMessageKey = (typeof MESSAGE_KEYS)[keyof typeof MESSAGE_KEYS];\n","import type { IBillingPlan } from '../config';\nimport type { LimitStatus, IPolicyStatus, IBillingContext, SubscriberType } from '../types';\nimport { parseWindowMs } from '../utils';\n\nexport function buildBillingContext(opts: {\n\tsubscriber: { type: SubscriberType; id: string };\n\tplan: IBillingPlan;\n\tactive: boolean;\n\t// Pre-fetched windowed counter values keyed by policy key.\n\t// Non-windowed counter keys are absent (their used count is 0 — app manages those).\n\tcounters: Record<string, number>;\n}): IBillingContext {\n\tconst { subscriber, plan, active, counters } = opts;\n\tconst defaults = plan.defaults ?? {};\n\tconst statuses: Record<string, IPolicyStatus> = {};\n\n\tfor (const [key, entry] of Object.entries(plan.policy ?? {})) {\n\t\tif ('enabled' in entry) {\n\t\t\tstatuses[key] = { type: 'feature', enabled: entry.enabled };\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst { limit, buffer = defaults.buffer ?? 0, warnAt = defaults.warnAt ?? 0.8, window } = entry;\n\n\t\tconst used = counters[key] ?? 0;\n\t\tconst hardLimit = limit !== null ? limit + buffer : null;\n\n\t\tlet status: LimitStatus = 'ok';\n\t\tif (hardLimit !== null && used >= hardLimit) status = 'blocked';\n\t\telse if (limit !== null && used >= limit) status = 'over_limit';\n\t\telse if (limit !== null && used >= limit * warnAt) status = 'warning';\n\n\t\tlet resetsAt: string | null = null;\n\t\tif (window) {\n\t\t\tconst windowMs = parseWindowMs(window);\n\t\t\tconst windowStart = Math.floor(Date.now() / windowMs) * windowMs;\n\t\t\tresetsAt = new Date(windowStart + windowMs).toISOString();\n\t\t}\n\n\t\tstatuses[key] = { type: 'counter', limit, used, status, resetsAt };\n\t}\n\n\treturn { subscriber, plan: plan.name, active, statuses };\n}\n","import type { ICounterBackend } from './types';\n\ninterface Entry {\n\tcount: number;\n\twindowStart: number; // epoch ms — used for windowed expiry\n}\n\nexport class MemoryCounterBackend implements ICounterBackend {\n\tprivate readonly counters = new Map<string, Entry>();\n\n\tasync increment(key: string, windowMs: number | null, quantity = 1): Promise<number> {\n\t\tconst now = Date.now();\n\t\tconst existing = this.counters.get(key);\n\n\t\tif (!existing || (windowMs !== null && now - existing.windowStart >= windowMs)) {\n\t\t\tthis.counters.set(key, { count: quantity, windowStart: now });\n\t\t\treturn quantity;\n\t\t}\n\n\t\texisting.count += quantity;\n\t\treturn existing.count;\n\t}\n\n\tasync get(key: string, windowMs: number | null): Promise<number> {\n\t\tconst now = Date.now();\n\t\tconst existing = this.counters.get(key);\n\t\tif (!existing) return 0;\n\t\tif (windowMs !== null && now - existing.windowStart >= windowMs) return 0;\n\t\treturn existing.count;\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\nimport type { ICounterBackend } from './types';\n\nexport class DBCounterBackend implements ICounterBackend {\n\tconstructor(private readonly store: IStoreAdapter) {}\n\n\tasync increment(key: string, windowMs: number | null, quantity = 1): Promise<number> {\n\t\tconst [subscriberType, subscriberId, ...rest] = key.split(':');\n\t\tconst metric = rest.join(':');\n\n\t\tawait this.store.query(\n\t\t\t`INSERT INTO fonderie_usage_records (subscriber_type, subscriber_id, metric, quantity)\n\t\t\t VALUES ($1, $2, $3, $4)`,\n\t\t\t[subscriberType, subscriberId, metric, quantity],\n\t\t);\n\n\t\treturn this.get(key, windowMs);\n\t}\n\n\tasync get(key: string, windowMs: number | null): Promise<number> {\n\t\tconst [subscriberType, subscriberId, ...rest] = key.split(':');\n\t\tconst metric = rest.join(':');\n\t\tconst since = windowMs !== null ? new Date(Date.now() - windowMs) : new Date(0);\n\n\t\tconst rows = await this.store.query<{ total: string }>(\n\t\t\t`SELECT COALESCE(SUM(quantity), 0) AS total\n\t\t\t FROM fonderie_usage_records\n\t\t\t WHERE subscriber_type = $1\n\t\t\t AND subscriber_id = $2\n\t\t\t AND metric = $3\n\t\t\t AND recorded_at >= $4`,\n\t\t\t[subscriberType, subscriberId, metric, since],\n\t\t);\n\n\t\treturn parseInt(rows[0]?.total ?? '0', 10);\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\nimport type { RateLimitBackendConfig } from '../config';\nimport { MemoryCounterBackend } from './memory';\nimport { DBCounterBackend } from './db';\n\nexport function createBackend(config: RateLimitBackendConfig | undefined, store: IStoreAdapter) {\n\tif (!config || config === 'memory') return new MemoryCounterBackend();\n\tif (config === 'db') return new DBCounterBackend(store);\n\treturn config;\n}\n\nexport type { ICounterBackend } from './types';\nexport { MemoryCounterBackend } from './memory';\nexport { DBCounterBackend } from './db';\n","import type { IFonderieModule, IFonderieApp } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IBillingConfig } from './config';\nimport { buildBillingRoutes } from './routes';\nimport { syncPlansToDB } from './services/plans';\nimport { withBilling } from './middlewares/billing';\nimport { createBackend } from './backends';\n\nexport class BillingModule implements IFonderieModule {\n\treadonly name = '@fonderie/billing';\n\treadonly deps = ['@fonderie/auth'];\n\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tprivate config: IBillingConfig,\n\t) {}\n\n\tasync install(app: IFonderieApp): Promise<void> {\n\t\tawait syncPlansToDB(this.config, this.store);\n\n\t\tconst backend = createBackend(this.config.rateLimit?.backend, this.store);\n\n\t\t// Global middleware — resolves subscriber + plan, enforces rate limits,\n\t\t// caches IBillingContext on ctx.meta['billing'] for every request.\n\t\t// Runs after auth (ctx.user available), before route handlers.\n\t\tapp.use(withBilling(this.store, this.config, backend));\n\n\t\tconst routes = buildBillingRoutes(this.store, this.config);\n\t\tfor (const [method, path, ...handlers] of routes) {\n\t\t\tapp.addRoute(method, path, ...handlers);\n\t\t}\n\t}\n}\n","export type SubscriberType = 'user' | 'workspace';\n\n// Billing interval — one source for the 'month' | 'year' literals.\nexport const BILLING_INTERVAL = { MONTH: 'month', YEAR: 'year' } as const;\nexport type BillingInterval = (typeof BILLING_INTERVAL)[keyof typeof BILLING_INTERVAL];\n\n// ── Policy ────────────────────────────────────────────────────────\n\nexport type PolicyEntry =\n\t| { enabled: boolean }\n\t| {\n\t\t\tlimit: number | null; // advertised ceiling; null = unlimited\n\t\t\tbuffer?: number; // unadvertised grace on top of limit\n\t\t\twarnAt?: number; // fraction of limit to trigger warning (0–1)\n\t\t\twindow?: string; // '1d' | '30d' | '1h' — if set, auto rate-limited\n\t\t\tunit?: string; // display only, e.g. 'mb', 'requests'\n\t };\n\nexport type LimitStatus = 'ok' | 'warning' | 'over_limit' | 'blocked';\n\nexport type IPolicyStatus =\n\t| { type: 'feature'; enabled: boolean }\n\t| {\n\t\t\ttype: 'counter';\n\t\t\tlimit: number | null; // advertised — safe to send to client\n\t\t\tused: number;\n\t\t\tstatus: LimitStatus;\n\t\t\tresetsAt: string | null; // ISO string for windowed counters, null otherwise\n\t };\n\nexport interface IBillingContext {\n\tsubscriber: { type: SubscriberType; id: string };\n\tplan: string;\n\tactive: boolean; // subscription is active or trialing\n\tstatuses: Record<string, IPolicyStatus>;\n}\n\n// ── Subscription ──────────────────────────────────────────────────\n\nexport interface ISubscription {\n\tid: string;\n\tsubscriberType: SubscriberType;\n\tsubscriberId: string;\n\tplan: string;\n\tinterval: 'month' | 'year';\n\tstatus: SubscriptionStatus;\n\tproviderCustomerId: string | null;\n\tproviderSubscriptionId: string | null;\n\tcurrentPeriodStart: string | null;\n\tcurrentPeriodEnd: string | null;\n\tcancelAtPeriodEnd: boolean;\n\ttrialEndsAt: string | null;\n\tcreatedAt: string;\n}\n\nexport type SubscriptionStatus =\n\t| 'trialing'\n\t| 'active'\n\t| 'past_due'\n\t| 'canceled'\n\t| 'incomplete'\n\t| 'paused';\n\n// ── DB plan (read from fonderie_plans table) ──────────────────────\n\nexport interface IPlan {\n\tid: string;\n\tname: string;\n\tseats: number | null;\n\ttrialDays: number;\n\tmonthlyAmount: number | null;\n\tmonthlyPriceId: string | null;\n\tyearlyAmount: number | null;\n\tyearlyPriceId: string | null;\n\tdescription: string | null;\n\ttier: number;\n\tfeatures: IPlanFeature[];\n\tmetadata: Record<string, unknown>;\n}\n\nexport interface IPlanFeature {\n\tname: string;\n\tdescription: string;\n\tenabled: boolean;\n\tlimit?: number;\n}\n\n// ── Usage ─────────────────────────────────────────────────────────\n\nexport interface IUsageRecord {\n\tid: string;\n\tsubscriberType: SubscriberType;\n\tsubscriberId: string;\n\tmetric: string;\n\tquantity: number;\n\trecordedAt: string;\n}\n","import type { IBillingProvider, IBillingEvent, INormalizedSubscription, IResolvedPrice } from './types';\nimport { BILLING_INTERVAL } from '../types';\nimport type { SubscriberType } from '../types';\n\ninterface IStripeSubscriptionRaw {\n\tid: string;\n\tstatus: string;\n\tcustomer: string;\n\tmetadata?: Record<string, string>;\n\titems: {\n\t\tdata: Array<{\n\t\t\tprice: { id: string; nickname: string | null; lookup_key?: string | null; recurring?: { interval: string } };\n\t\t\t// Since Stripe API 2025+, the period lives on the item, not the subscription.\n\t\t\tcurrent_period_start?: number;\n\t\t\tcurrent_period_end?: number;\n\t\t}>;\n\t};\n\t// Older API versions (pre-2025) expose the period on the subscription itself.\n\tcurrent_period_start?: number;\n\tcurrent_period_end?: number;\n\tcancel_at_period_end: boolean;\n\ttrial_end: number | null;\n}\n\ninterface IStripeEventRaw {\n\ttype: string;\n\tdata: { object: unknown };\n}\n\n// Lazy singleton — Stripe SDK is optional\nlet _client: unknown = null;\n\nasync function getClient(secretKey: string): Promise<unknown> {\n\tif (_client) return _client;\n\n\tconst pkg = 'stripe';\n\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tconst mod: any = await import(pkg).catch(() => {\n\t\tthrow new Error('[billing:stripe] stripe is required: npm install stripe');\n\t});\n\n\tconst Stripe = mod.default ?? mod;\n\t_client = new Stripe(secretKey, { apiVersion: '2024-11-20.acacia' });\n\treturn _client;\n}\n\nfunction normalizeSubscription(sub: IStripeSubscriptionRaw): INormalizedSubscription {\n\tconst item = sub.items.data[0];\n\t// Period moved from the subscription to the item in Stripe API 2025+; read the\n\t// item first, fall back to the subscription-level fields for older versions.\n\tconst periodStart = item?.current_period_start ?? sub.current_period_start;\n\tconst periodEnd = item?.current_period_end ?? sub.current_period_end;\n\treturn {\n\t\tsubscriberType: (sub.metadata?.['subscriberType'] ?? 'workspace') as SubscriberType,\n\t\tsubscriberId: sub.metadata?.['subscriberId'] ?? '',\n\t\tplan: item?.price.nickname ?? 'unknown',\n\t\tpriceLookupKey: item?.price.lookup_key ?? null,\n\t\tpriceId: item?.price.id ?? null,\n\t\tstatus: sub.status,\n\t\tproviderCustomerId: sub.customer,\n\t\tproviderSubscriptionId: sub.id,\n\t\tcurrentPeriodStart: periodStart ? new Date(periodStart * 1000) : new Date(),\n\t\tcurrentPeriodEnd: periodEnd ? new Date(periodEnd * 1000) : new Date(),\n\t\tcancelAtPeriodEnd: sub.cancel_at_period_end,\n\t\ttrialEndsAt: sub.trial_end ? new Date(sub.trial_end * 1000) : null,\n\t\tinterval: item?.price.recurring?.interval === BILLING_INTERVAL.YEAR ? BILLING_INTERVAL.YEAR : BILLING_INTERVAL.MONTH,\n\t};\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction toResolvedPrice(p: any): IResolvedPrice {\n\treturn {\n\t\tpriceId: p.id,\n\t\tlookupKey: p.lookup_key ?? null,\n\t\tunitAmount: p.unit_amount ?? 0,\n\t\tcurrency: p.currency,\n\t\tinterval: p.recurring?.interval === BILLING_INTERVAL.YEAR ? BILLING_INTERVAL.YEAR : BILLING_INTERVAL.MONTH,\n\t\tnickname: p.nickname ?? null,\n\t\tproductId: typeof p.product === 'string' ? p.product : (p.product?.id ?? ''),\n\t\tactive: p.active ?? true,\n\t};\n}\n\nexport class StripeProvider implements IBillingProvider {\n\treadonly name = 'stripe';\n\n\tconstructor(\n\t\tprivate secretKey: string,\n\t\tprivate webhookSecret?: string,\n\t) {}\n\n\tprivate async client(): Promise<any> {\n\t\treturn getClient(this.secretKey);\n\t}\n\n\tasync createCustomer(opts: {\n\t\temail: string;\n\t\tsubscriberType: SubscriberType;\n\t\tsubscriberId: string;\n\t\tuserId: string;\n\t}): Promise<{ customerId: string }> {\n\t\tconst stripe = await this.client();\n\t\tconst customer = await stripe.customers.create({\n\t\t\temail: opts.email,\n\t\t\tmetadata: {\n\t\t\t\tsubscriberType: opts.subscriberType,\n\t\t\t\tsubscriberId: opts.subscriberId,\n\t\t\t\tuserId: opts.userId,\n\t\t\t},\n\t\t});\n\t\treturn { customerId: customer.id };\n\t}\n\n\tasync createCheckoutSession(opts: {\n\t\tcustomerId: string;\n\t\tpriceId: string;\n\t\tsubscriberType: SubscriberType;\n\t\tsubscriberId: string;\n\t\ttrialDays?: number;\n\t\tsuccessUrl: string;\n\t\tcancelUrl: string;\n\t}): Promise<{ url: string }> {\n\t\tconst stripe = await this.client();\n\t\tconst session = await stripe.checkout.sessions.create({\n\t\t\tcustomer: opts.customerId,\n\t\t\tmode: 'subscription',\n\t\t\tline_items: [{ price: opts.priceId, quantity: 1 }],\n\t\t\tsuccess_url: opts.successUrl,\n\t\t\tcancel_url: opts.cancelUrl,\n\t\t\tsubscription_data: {\n\t\t\t\tmetadata: {\n\t\t\t\t\tsubscriberType: opts.subscriberType,\n\t\t\t\t\tsubscriberId: opts.subscriberId,\n\t\t\t\t},\n\t\t\t\t...(opts.trialDays && opts.trialDays > 0 ? { trial_period_days: opts.trialDays } : {}),\n\t\t\t},\n\t\t});\n\t\treturn { url: session.url ?? '' };\n\t}\n\n\tasync resolvePriceById(priceId: string): Promise<IResolvedPrice | null> {\n\t\tconst stripe = await this.client();\n\t\ttry {\n\t\t\tconst p = await stripe.prices.retrieve(priceId, { expand: ['product'] });\n\t\t\treturn toResolvedPrice(p);\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tasync resolvePricesByLookupKey(lookupKeys: string[]): Promise<Map<string, IResolvedPrice>> {\n\t\tconst out = new Map<string, IResolvedPrice>();\n\t\tif (lookupKeys.length === 0) return out;\n\t\tconst stripe = await this.client();\n\t\tconst res = await stripe.prices.list({\n\t\t\tlookup_keys: lookupKeys,\n\t\t\tactive: true,\n\t\t\texpand: ['data.product'],\n\t\t\tlimit: 100,\n\t\t});\n\t\tfor (const p of res.data) {\n\t\t\tif (p.lookup_key) out.set(p.lookup_key, toResolvedPrice(p));\n\t\t}\n\t\treturn out;\n\t}\n\n\tasync updateSubscription(opts: {\n\t\tsubscriptionId: string;\n\t\tpriceId: string;\n\t}): Promise<{ status: string; currentPeriodStart: Date | null; currentPeriodEnd: Date | null }> {\n\t\tconst stripe = await this.client();\n\t\tconst sub = await stripe.subscriptions.retrieve(opts.subscriptionId);\n\t\tconst itemId = sub.items.data[0]?.id;\n\t\t// Swap the price on the existing item and invoice the prorated difference\n\t\t// immediately (upgrade → pay the difference now).\n\t\tconst updated = await stripe.subscriptions.update(opts.subscriptionId, {\n\t\t\titems: [{ id: itemId, price: opts.priceId }],\n\t\t\tproration_behavior: 'always_invoice',\n\t\t\tpayment_behavior: 'error_if_incomplete',\n\t\t});\n\t\tconst item = updated.items?.data?.[0];\n\t\tconst cps = item?.current_period_start ?? updated.current_period_start;\n\t\tconst cpe = item?.current_period_end ?? updated.current_period_end;\n\t\treturn {\n\t\t\tstatus: updated.status,\n\t\t\tcurrentPeriodStart: cps ? new Date(cps * 1000) : null,\n\t\t\tcurrentPeriodEnd: cpe ? new Date(cpe * 1000) : null,\n\t\t};\n\t}\n\n\tasync createPortalSession(opts: {\n\t\tcustomerId: string;\n\t\treturnUrl: string;\n\t}): Promise<{ url: string }> {\n\t\tconst stripe = await this.client();\n\t\tconst session = await stripe.billingPortal.sessions.create({\n\t\t\tcustomer: opts.customerId,\n\t\t\treturn_url: opts.returnUrl,\n\t\t});\n\t\treturn { url: session.url };\n\t}\n\n\tasync constructEvent(opts: {\n\t\tpayload: string;\n\t\tsignature: string;\n\t\tsecret: string;\n\t}): Promise<IBillingEvent> {\n\t\tconst stripe = await this.client();\n\n\t\tlet raw: IStripeEventRaw;\n\t\ttry {\n\t\t\traw = stripe.webhooks.constructEvent(opts.payload, opts.signature, opts.secret);\n\t\t} catch {\n\t\t\tthrow new Error('[billing:stripe] Invalid webhook signature');\n\t\t}\n\n\t\tconst isSubscriptionEvent = [\n\t\t\t'customer.subscription.created',\n\t\t\t'customer.subscription.updated',\n\t\t\t'customer.subscription.deleted',\n\t\t].includes(raw.type);\n\n\t\tif (!isSubscriptionEvent) {\n\t\t\treturn { type: raw.type, subscription: null };\n\t\t}\n\n\t\tconst sub = raw.data.object as IStripeSubscriptionRaw;\n\n\t\tif (raw.type === 'customer.subscription.deleted') {\n\t\t\treturn {\n\t\t\t\ttype: raw.type,\n\t\t\t\tsubscription: { ...normalizeSubscription(sub), plan: 'free', status: 'canceled' },\n\t\t\t};\n\t\t}\n\n\t\treturn { type: raw.type, subscription: normalizeSubscription(sub) };\n\t}\n}\n","import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { Middleware } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { getSubscription } from '../services/subscriptions';\nimport { resolveSubscriber } from '../utils';\n\n// Gates a route behind a minimum plan.\n// Works for both user-level and workspace-level subscriptions.\n// Usage: requirePlan(['pro', 'enterprise'], store)\n\nfunction makeHandler(plans: string | string[], store: IStoreAdapter): Middleware {\n\tconst allowed = Array.isArray(plans) ? plans : [plans];\n\n\treturn async (ctx, next) => {\n\t\tif (!ctx.user) {\n\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t\t}\n\n\t\tconst subscriber = resolveSubscriber(ctx);\n\t\tif (!subscriber) {\n\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'SUBSCRIBER_REQUIRED', 'Subscriber context required');\n\t\t}\n\n\t\tconst subscription = await getSubscription(subscriber.type, subscriber.id, store);\n\n\t\tif (!subscription || !allowed.includes(subscription.plan)) {\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.PAYMENT_REQUIRED,\n\t\t\t\t'PLAN_UPGRADE_REQUIRED',\n\t\t\t\t'Plan upgrade required',\n\t\t\t\t{ required: allowed, current: subscription?.plan ?? 'none' },\n\t\t\t);\n\t\t}\n\n\t\tif (subscription.status !== 'active' && subscription.status !== 'trialing') {\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.PAYMENT_REQUIRED,\n\t\t\t\t'SUBSCRIPTION_INACTIVE',\n\t\t\t\t'Subscription is not active',\n\t\t\t\t{ status: subscription.status },\n\t\t\t);\n\t\t}\n\n\t\treturn next();\n\t};\n}\n\nexport function requirePlan(plans: string | string[], store: IStoreAdapter): Middleware;\nexport function requirePlan(\n\tplans: string | string[],\n\tstore: IStoreAdapter,\n\tctx: IFonderieContext,\n\tnext: () => Promise<Response>,\n): Promise<Response>;\nexport function requirePlan(\n\tplans: string | string[],\n\tstore: IStoreAdapter,\n\tctx?: IFonderieContext,\n\tnext?: () => Promise<Response>,\n): Middleware | Promise<Response> {\n\tconst handler = makeHandler(plans, store);\n\tif (ctx !== undefined && next !== undefined) return handler(ctx, next);\n\treturn handler;\n}\n","import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { Middleware } from '@fonderie/core';\nimport type { IBillingContext, IPolicyStatus } from './types';\n\nfunction getBillingContext(ctx: IFonderieContext): IBillingContext | null {\n\treturn (ctx.meta['billing'] as IBillingContext | undefined) ?? null;\n}\n\n// Returns true if the feature flag is enabled on the subscriber's plan.\n// Returns true when no billing context is present (fail-open when billing not configured).\nexport function hasFeature(ctx: IFonderieContext, key: string): boolean {\n\tconst billing = getBillingContext(ctx);\n\tif (!billing) return true;\n\n\tconst status = billing.statuses[key];\n\tif (!status) return true; // key not declared in policy → allow\n\tif (status.type === 'feature') return status.enabled;\n\treturn true; // counter entry = feature present\n}\n\n// Returns the advertised limit for a counter policy key, or null if unlimited / not configured.\nexport function getPlanLimit(ctx: IFonderieContext, key: string): number | null {\n\tconst billing = getBillingContext(ctx);\n\tif (!billing) return null;\n\n\tconst status = billing.statuses[key];\n\tif (!status || status.type === 'feature') return null;\n\treturn status.limit;\n}\n\n// Returns the full policy status for a key, or null if not configured.\nexport function getLimitStatus(ctx: IFonderieContext, key: string): IPolicyStatus | null {\n\tconst billing = getBillingContext(ctx);\n\tif (!billing) return null;\n\treturn billing.statuses[key] ?? null;\n}\n\n// Middleware — gates a route behind a feature flag.\n// Reads from cached ctx.meta['billing']; no store arg, no async DB call.\n// Fails open if billing context is absent (billing module not registered).\nexport function requireFeature(key: string): Middleware {\n\treturn (ctx, next) => {\n\t\tif (!hasFeature(ctx, key)) {\n\t\t\treturn Promise.resolve(\n\t\t\t\tsetApiResponse(\n\t\t\t\t\tHTTP.PAYMENT_REQUIRED,\n\t\t\t\t\t'FEATURE_UNAVAILABLE',\n\t\t\t\t\t`Feature '${key}' is not available on your current plan`,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t\treturn next();\n\t};\n}\n"],"mappings":"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","names":["import_core","import_core","import_core","import_core","import_core","import_core","import_core"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/routes.ts","../src/schemas.ts","../src/types.ts","../src/services/price-cache.ts","../src/controllers/plan.controller.ts","../src/utils.ts","../src/services/plans.ts","../src/models/plan.model.ts","../src/dtos/billing.ts","../src/controllers/subscription.controller.ts","../src/services/subscriptions.ts","../src/models/subscription.model.ts","../src/controllers/checkout.controller.ts","../src/controllers/usage.controller.ts","../src/services/usage.ts","../src/models/usage.model.ts","../src/controllers/wallet.controller.ts","../src/errors.ts","../src/services/wallet.ts","../src/models/wallet.model.ts","../src/services/credit-packs.ts","../src/helpers.ts","../src/controllers/webhook.controller.ts","../src/controllers/webhook-shared.ts","../src/controllers/payment-webhook.controller.ts","../src/middlewares/admin-token.ts","../src/middlewares/billing.ts","../src/config.ts","../src/services/membership.ts","../src/services/policy.ts","../src/backends/memory.ts","../src/backends/db.ts","../src/backends/index.ts","../src/module.ts","../src/providers/stripe.ts","../src/middlewares/require-plan.ts"],"sourcesContent":["// ── Public API ───────────────────────────────────────────────────\nexport { BillingModule } from './module';\nexport { StripeProvider } from './providers/stripe';\n\n// Middleware\nexport { requirePlan } from './middlewares/require-plan';\nexport { withBilling } from './middlewares/billing';\n\n// Helpers — sync, read from cached ctx.meta['billing']\nexport {\n\thasFeature,\n\tgetPlanLimit,\n\tgetLimitStatus,\n\trequireFeature,\n\tgetWalletStatus,\n\tgetWalletRate,\n\trequireWalletBalance,\n\tdebitWalletForMetric,\n\tinsufficientCreditsResponse,\n} from './helpers';\n\n// Config + constants\nexport { MESSAGE_KEYS } from './config';\nexport type {\n\tIBillingConfig,\n\tIBillingCreditPack,\n\tIBillingPlan,\n\tIBillingPlanDefaults,\n\tIBillingPlanPrice,\n\tIBillingPlanWallet,\n\tIBillingPricingConfig,\n\tIBillingWalletConfig,\n\tRateLimitBackendConfig,\n\tIBillingNotificationsConfig,\n\tBillingMessageKey,\n} from './config';\n\n// Backends\nexport { MemoryCounterBackend, DBCounterBackend } from './backends';\nexport type { ICounterBackend } from './backends';\n\nexport { BILLING_INTERVAL, BILLING_INTERVALS, isBillingInterval, WALLET_LEDGER_TYPES } from './types';\nexport type { BillingInterval, WalletLedgerType } from './types';\n// Types\nexport type {\n\tIBillingProvider,\n\tIBillingEvent,\n\tINormalizedPayment,\n\tIResolvedPrice,\n} from './providers/types';\nexport type {\n\tIPlan,\n\tISubscription,\n\tIWalletBalance,\n\tIWalletContext,\n\tIWalletLedgerEntry,\n\tIWalletRate,\n\tSubscriptionStatus,\n\tPolicyEntry,\n\tLimitStatus,\n\tIPolicyStatus,\n\tIBillingContext,\n} from './types';\nexport type {\n\tIPlanDTO,\n\tISubscriptionDTO,\n\tIWalletDTO,\n\tIWalletTransactionDTO,\n} from './dtos/billing';\n\n// DTOs\nexport {\n\ttoPlanDTO,\n\ttoSubscriptionDTO,\n\ttoWalletDTO,\n\ttoWalletTransactionDTO,\n} from './dtos/billing';\n\n// Wallet — ledger-backed stored value. Product code debits through\n// debitWallet with an idempotency key derived from its own unit of work.\nexport {\n\tcreditWallet,\n\tdebitWallet,\n\tgetWalletBalance,\n\tgetWalletLedger,\n\tensurePeriodicGrant,\n\tcurrentGrantPeriod,\n\tresolvePlanWallet,\n\tencodeLedgerCursor,\n\tdecodeLedgerCursor,\n} from './services/wallet';\nexport type {\n\tIWalletSubscriber,\n\tIWalletMutationResult,\n\tIWalletLedgerPage,\n\tIGrantResult,\n\tIResolvedPlanWallet,\n} from './services/wallet';\nexport { InsufficientFundsError, DuplicateTransactionError } from './errors';\n\n// Services (for advanced usage)\nexport { recordUsage, getUsage } from './services/usage';\nexport {\n\tgetPlans,\n\tgetPlanByName,\n\tgetDBPlans,\n\tgetPlanById,\n\tcreatePlan,\n\tupdatePlan,\n\tdeletePlan,\n} from './services/plans';\nexport { getSubscription } from './services/subscriptions';\n\n// Request validation — enforced contract for body-taking routes (webhook\n// excluded: provider-shaped, signature-verified). Exported for docs/clients.\nexport * as schemas from './schemas';\n","import type { IStoreAdapter } from '@fonderie/store';\nimport type { Middleware } from '@fonderie/core';\nimport { requireAuth, validate } from '@fonderie/core/middlewares';\n\nimport {\n\tcheckoutSchema,\n\tcreatePlanSchema,\n\tgrantWalletSchema,\n\trecordUsageSchema,\n\tupdatePlanSchema,\n\twalletCheckoutSchema,\n} from './schemas';\n\nimport type { IBillingConfig } from './config';\nimport { PriceCache } from './services/price-cache';\nimport { planController } from './controllers/plan.controller';\nimport { subscriptionController } from './controllers/subscription.controller';\nimport { checkoutController } from './controllers/checkout.controller';\nimport { usageController } from './controllers/usage.controller';\nimport { walletController } from './controllers/wallet.controller';\nimport { webhookController } from './controllers/webhook.controller';\nimport { paymentWebhookController } from './controllers/payment-webhook.controller';\nimport { requireAdminToken } from './middlewares/admin-token';\n\ntype RouteDefinition = [string, string, ...Middleware[]];\n\nexport function buildBillingRoutes(\n\tstore: IStoreAdapter,\n\tconfig: IBillingConfig,\n): RouteDefinition[] {\n\tconst priceCache = new PriceCache({\n\t\tttlMs: config.pricing?.cacheTtlMs,\n\t\tgraceMs: config.pricing?.transferGraceMs,\n\t\tmaxStaleMs: config.pricing?.maxStaleMs,\n\t});\n\tconst plan = planController(store, config, priceCache);\n\tconst subscription = subscriptionController(store);\n\tconst checkout = checkoutController(store, config);\n\tconst usage = usageController(store);\n\tconst webhook = webhookController(store, config, priceCache);\n\n\tconst routes: RouteDefinition[] = [\n\t\t// Plans — public read-only\n\t\t['GET', '/plans', plan.list],\n\t\t['GET', '/plans/:planId', plan.get],\n\n\t\t// Plans — admin write (caller is responsible for authorization)\n\t\t['POST', '/plans', validate(createPlanSchema), plan.create],\n\t\t['PUT', '/plans/:planId', validate(updatePlanSchema), plan.update],\n\t\t['DELETE', '/plans/:planId', plan.delete],\n\n\t\t// Billing — subscriber resolved from X-Workspace-ID header (workspace) or session (user).\n\t\t// The withBilling global middleware verifies workspace membership against\n\t\t// fonderie_role_user_workspaces (403 for non-members, fail-closed) before\n\t\t// any billing surface acts on a header-derived workspace id.\n\t\t['GET', '/billing/subscription', requireAuth, subscription.get],\n\t\t['POST', '/billing/checkout', requireAuth, validate(checkoutSchema), checkout.createSession],\n\t\t['POST', '/billing/portal', requireAuth, checkout.createPortal],\n\t\t['POST', '/billing/usage', requireAuth, validate(recordUsageSchema), usage.record],\n\t\t['GET', '/billing/usage/:metric', requireAuth, usage.get],\n\n\t\t// Webhook — signature verified inside the handler\n\t\t['POST', '/billing/webhook', webhook.handle],\n\t];\n\n\t// Stored-value wallet — opt-in via config.wallet; absent config registers\n\t// nothing and changes nothing for subscription-only consumers.\n\tif (config.wallet) {\n\t\tconst wallet = walletController(store, config);\n\t\tconst paymentWebhook = paymentWebhookController(store, config);\n\t\troutes.push(\n\t\t\t['GET', '/billing/wallet', requireAuth, wallet.get],\n\t\t\t['GET', '/billing/wallet/transactions', requireAuth, wallet.transactions],\n\t\t\t['POST', '/billing/wallet/checkout', requireAuth, validate(walletCheckoutSchema), wallet.checkout],\n\t\t\t// Payment webhook — separate endpoint and secret from the\n\t\t\t// subscription webhook; signature verified inside the handler.\n\t\t\t['POST', '/billing/webhook/payment', paymentWebhook.handle],\n\t\t);\n\t\t// Manual grants are an ops surface: bootstrap admin token, not sessions.\n\t\tif (config.wallet.adminToken) {\n\t\t\troutes.push([\n\t\t\t\t'POST',\n\t\t\t\t'/billing/wallet/grant',\n\t\t\t\trequireAdminToken(config.wallet.adminToken),\n\t\t\t\tvalidate(grantWalletSchema),\n\t\t\t\twallet.grant,\n\t\t\t]);\n\t\t}\n\t}\n\n\treturn routes;\n}\n","import { z } from 'zod';\n\nimport { BILLING_INTERVALS } from './types';\n\n// Request schemas — the validation contract for billing's body-taking routes\n// (webhook excluded: provider-shaped, signature-verified in the handler).\n// Wired via @fonderie/core's validate(); same pattern as @fonderie/auth.\n\nconst planFields = {\n\tdescription: z.string().max(2000).nullable().optional(),\n\ttier: z.number().int().min(0).optional(),\n\tseats: z.number().int().min(0).nullable().optional(),\n\ttrialDays: z.number().int().min(0).optional(),\n\tmonthlyAmount: z.number().min(0).nullable().optional(),\n\tmonthlyPriceId: z.string().max(200).nullable().optional(),\n\tyearlyAmount: z.number().min(0).nullable().optional(),\n\tyearlyPriceId: z.string().max(200).nullable().optional(),\n\tfeatures: z.unknown().optional(),\n\tmetadata: z.unknown().optional(),\n};\n\nexport const createPlanSchema = z.object({\n\tname: z.string().trim().min(1, 'name is required').max(200),\n\t...planFields,\n});\n\nexport const updatePlanSchema = z\n\t.object({ name: z.string().trim().min(1).max(200).optional(), ...planFields })\n\t.refine((o) => Object.values(o).some((v) => v !== undefined), 'Provide at least one field');\n\nexport const checkoutSchema = z.object({\n\tplan: z.string().min(1, 'plan is required'),\n\tinterval: z.enum(BILLING_INTERVALS).optional(),\n});\n\nexport const recordUsageSchema = z.object({\n\tmetric: z.string().min(1, 'metric is required').max(100),\n\tquantity: z.number().min(0).optional(),\n});\n\n// Wallet amounts are bigint on the server; the wire carries them as digit\n// strings (JSON numbers accepted too, for small hand-written requests).\nconst walletAmount = z\n\t.union([\n\t\tz.string().regex(/^\\d{1,30}$/, 'amount must be a positive integer string'),\n\t\t// JSON numbers past 2^53 arrive already rounded — force the digit-string\n\t\t// form for anything larger instead of silently granting a wrong amount.\n\t\tz.number().int().min(1).max(Number.MAX_SAFE_INTEGER),\n\t])\n\t.transform((v) => BigInt(v))\n\t.refine((v) => v > 0n, 'amount must be positive');\n\nexport const walletCheckoutSchema = z.object({\n\tpackId: z.string().trim().min(1, 'packId is required').max(100),\n});\n\nexport const grantWalletSchema = z.object({\n\tsubscriberType: z.enum(['user', 'workspace']),\n\tsubscriberId: z.string().uuid('subscriberId must be a UUID'),\n\tamount: walletAmount,\n\tcurrency: z\n\t\t.string()\n\t\t.trim()\n\t\t.regex(/^[A-Za-z]{3,20}$/, 'currency must be a 3-20 letter code')\n\t\t.transform((s) => s.toUpperCase())\n\t\t.optional(),\n\tdescription: z.string().max(500).optional(),\n\tidempotencyKey: z.string().min(1, 'idempotencyKey is required').max(255),\n});\n","export type SubscriberType = 'user' | 'workspace';\n\n// Billing interval — ONE source for the 'month' | 'year' literals. The tuple\n// drives the type, the zod schema, and runtime membership checks; the object\n// is the ergonomic dot-access companion (`satisfies` pins its values to the\n// union). Adding an interval starts here and the compiler walks you through\n// every exhaustive switch that must learn about it.\nexport const BILLING_INTERVALS = ['month', 'year'] as const;\nexport type BillingInterval = (typeof BILLING_INTERVALS)[number];\nexport const BILLING_INTERVAL = {\n\tMONTH: 'month',\n\tYEAR: 'year',\n} as const satisfies Record<string, BillingInterval>;\n\nexport function isBillingInterval(value: unknown): value is BillingInterval {\n\treturn (BILLING_INTERVALS as readonly unknown[]).includes(value);\n}\n\n// ── Policy ────────────────────────────────────────────────────────\n\nexport type PolicyEntry =\n\t| { enabled: boolean }\n\t| {\n\t\t\tlimit: number | null; // advertised ceiling; null = unlimited\n\t\t\tbuffer?: number; // unadvertised grace on top of limit\n\t\t\twarnAt?: number; // fraction of limit to trigger warning (0–1)\n\t\t\twindow?: string; // '1d' | '30d' | '1h' — if set, auto rate-limited\n\t\t\tunit?: string; // display only, e.g. 'mb', 'requests'\n\t };\n\nexport type LimitStatus = 'ok' | 'warning' | 'over_limit' | 'blocked';\n\nexport type IPolicyStatus =\n\t| { type: 'feature'; enabled: boolean }\n\t| {\n\t\t\ttype: 'counter';\n\t\t\tlimit: number | null; // advertised — safe to send to client\n\t\t\tused: number;\n\t\t\tstatus: LimitStatus;\n\t\t\tresetsAt: string | null; // ISO string for windowed counters, null otherwise\n\t };\n\n// Per-metric wallet pricing (plan-defined unit economics).\nexport interface IWalletRate {\n\tcost: bigint; // per unit, in the smallest wallet-currency unit\n\tunit?: string; // display only, e.g. 'msg', 'min'\n}\n\n// Wallet snapshot cached on ctx.meta['billing'] by withBilling when the\n// subscriber's plan defines wallet economics. Server-side only — bigint\n// values here never hit JSON.stringify; the HTTP surface uses IWalletDTO.\nexport interface IWalletContext {\n\tbalance: bigint;\n\tcurrency: string;\n\tprecision: number;\n\toverdraftLimit: bigint;\n\trates: Record<string, IWalletRate>;\n}\n\nexport interface IBillingContext {\n\tsubscriber: { type: SubscriberType; id: string };\n\tplan: string;\n\tactive: boolean; // subscription is active or trialing\n\tstatuses: Record<string, IPolicyStatus>;\n\twallet?: IWalletContext;\n}\n\n// ── Subscription ──────────────────────────────────────────────────\n\nexport interface ISubscription {\n\tid: string;\n\tsubscriberType: SubscriberType;\n\tsubscriberId: string;\n\tplan: string;\n\tinterval: BillingInterval;\n\tstatus: SubscriptionStatus;\n\tproviderCustomerId: string | null;\n\tproviderSubscriptionId: string | null;\n\tcurrentPeriodStart: string | null;\n\tcurrentPeriodEnd: string | null;\n\tcancelAtPeriodEnd: boolean;\n\ttrialEndsAt: string | null;\n\tcreatedAt: string;\n}\n\nexport type SubscriptionStatus =\n\t| 'trialing'\n\t| 'active'\n\t| 'past_due'\n\t| 'canceled'\n\t| 'incomplete'\n\t| 'paused';\n\n// ── DB plan (read from fonderie_plans table) ──────────────────────\n\nexport interface IPlan {\n\tid: string;\n\tname: string;\n\tseats: number | null;\n\ttrialDays: number;\n\tmonthlyAmount: number | null;\n\tmonthlyPriceId: string | null;\n\tyearlyAmount: number | null;\n\tyearlyPriceId: string | null;\n\tdescription: string | null;\n\ttier: number;\n\tfeatures: IPlanFeature[];\n\tmetadata: Record<string, unknown>;\n}\n\nexport interface IPlanFeature {\n\tname: string;\n\tdescription: string;\n\tenabled: boolean;\n\tlimit?: number;\n}\n\n// ── Wallet ────────────────────────────────────────────────────────\n\nexport const WALLET_LEDGER_TYPES = ['purchase', 'grant', 'usage', 'refund', 'adjustment'] as const;\nexport type WalletLedgerType = (typeof WALLET_LEDGER_TYPES)[number];\n\nexport interface IWalletBalance {\n\tbalance: bigint;\n\tversion: number;\n\tupdatedAt: string | null; // ISO string; null when no balance row exists yet\n}\n\nexport interface IWalletLedgerEntry {\n\tid: string;\n\tsubscriberType: SubscriberType;\n\tsubscriberId: string;\n\tcurrency: string;\n\ttype: WalletLedgerType;\n\tamount: bigint; // signed: positive = credit, negative = debit\n\tbalanceAfter: bigint;\n\tdescription: string | null;\n\tidempotencyKey: string;\n\tmetadata: Record<string, unknown>;\n\tproviderTxId: string | null;\n\tcreatedAt: string;\n}\n\n","import type { IBillingProvider, IResolvedPrice } from '../providers/types';\n\nexport interface IPriceCacheOptions {\n\tttlMs?: number | undefined; // fresh window — default 5m\n\tgraceMs?: number | undefined; // serve last-cached on transient miss (transfer race) — default 1h\n\tmaxStaleMs?: number | undefined; // serve last-cached during provider outage — default 24h\n}\n\nexport interface IPriceLookup {\n\t/** Resolved price, or null if it could not be resolved at all. */\n\tprice: IResolvedPrice | null;\n\t/** True when the returned price is served past its fresh TTL (transfer grace / outage). */\n\tstale: boolean;\n}\n\n/**\n * Read-through cache over the billing provider's price resolution.\n * - fresh within `ttlMs`\n * - single-flight: concurrent misses for the same id share one provider call (§16.2)\n * - transient miss (provider returns null — e.g. lookup_key transfer window) →\n * serve last-cached within `graceMs`, marked stale (§16.1)\n * - provider throws (outage) → serve last-cached within `maxStaleMs`, marked stale (§16.8)\n */\nexport class PriceCache {\n\tprivate readonly ttl: number;\n\tprivate readonly grace: number;\n\tprivate readonly maxStale: number;\n\tprivate readonly byId = new Map<string, { price: IResolvedPrice; at: number }>();\n\tprivate readonly inflight = new Map<string, Promise<IResolvedPrice | null>>();\n\n\tconstructor(opts: IPriceCacheOptions = {}) {\n\t\tthis.ttl = opts.ttlMs ?? 300_000;\n\t\tthis.grace = opts.graceMs ?? 3_600_000;\n\t\tthis.maxStale = opts.maxStaleMs ?? 86_400_000;\n\t}\n\n\tasync byPriceId(priceId: string, provider: IBillingProvider): Promise<IPriceLookup> {\n\t\tconst now = Date.now();\n\t\tconst hit = this.byId.get(priceId);\n\t\tif (hit && now - hit.at < this.ttl) return { price: hit.price, stale: false };\n\n\t\tlet fresh: IResolvedPrice | null;\n\t\ttry {\n\t\t\tfresh = await this.single(priceId, () => provider.resolvePriceById(priceId));\n\t\t} catch {\n\t\t\t// Provider outage — serve last-cached within maxStale.\n\t\t\tif (hit && now - hit.at < this.maxStale) return { price: hit.price, stale: true };\n\t\t\treturn { price: null, stale: true };\n\t\t}\n\t\tif (fresh) {\n\t\t\tthis.byId.set(priceId, { price: fresh, at: now });\n\t\t\treturn { price: fresh, stale: false };\n\t\t}\n\t\t// Transient miss (e.g. lookup_key transfer window) — serve last-cached within grace.\n\t\tif (hit && now - hit.at < this.grace) return { price: hit.price, stale: true };\n\t\treturn { price: null, stale: true };\n\t}\n\n\tinvalidate(priceId?: string): void {\n\t\tif (priceId) this.byId.delete(priceId);\n\t\telse this.byId.clear();\n\t}\n\n\t/** Warm the cache with prices already resolved elsewhere (e.g. boot guard). */\n\tprime(prices: Iterable<IResolvedPrice>): void {\n\t\tconst now = Date.now();\n\t\tfor (const p of prices) this.byId.set(p.priceId, { price: p, at: now });\n\t}\n\n\tprivate single(key: string, run: () => Promise<IResolvedPrice | null>): Promise<IResolvedPrice | null> {\n\t\tconst existing = this.inflight.get(key);\n\t\tif (existing) return existing;\n\t\tconst p = run().finally(() => this.inflight.delete(key));\n\t\tthis.inflight.set(key, p);\n\t\treturn p;\n\t}\n}\n","import { setApiResponse, HTTP, stringOrEmpty, numberOrZero } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IBillingConfig } from '../config';\nimport type { IPlan } from '../types';\nimport type { IPlanDTO } from '../dtos/billing';\nimport { PlanModel } from '../models/plan.model';\nimport { toPlanDTO } from '../dtos/billing';\nimport { PriceCache } from '../services/price-cache';\nimport { toSafeNumber } from '../utils';\n\n// Read-through hydration: override the DTO's amount/currency with live Stripe\n// prices (source of truth). Best-effort per plan — on error (incl. currency\n// mismatch, §16.4) keep the fallback amount/currency and flag pricingStale.\nasync function hydratePricing(\n\tdto: IPlanDTO,\n\tplan: IPlan,\n\tconfig: IBillingConfig,\n\tcache: PriceCache,\n): Promise<void> {\n\ttry {\n\t\tlet stale = false;\n\t\tconst resolve = async (priceId: string | null) => {\n\t\t\tif (!priceId) return null;\n\t\t\tconst r = await cache.byPriceId(priceId, config.provider);\n\t\t\tif (r.stale) stale = true;\n\t\t\treturn r.price;\n\t\t};\n\t\tconst [m, y] = await Promise.all([resolve(plan.monthlyPriceId), resolve(plan.yearlyPriceId)]);\n\t\tif (m && y && m.currency !== y.currency) {\n\t\t\tthrow new Error(\n\t\t\t\t`[billing] plan \"${plan.name}\": monthly/yearly currency mismatch (${m.currency} vs ${y.currency})`,\n\t\t\t);\n\t\t}\n\t\tif (m) dto.pricing.monthly = toSafeNumber(m.unitAmount);\n\t\tif (y) dto.pricing.yearly = toSafeNumber(y.unitAmount);\n\t\tconst currency = m?.currency ?? y?.currency;\n\t\tif (currency) dto.pricing.currency = currency.toUpperCase();\n\t\tif (stale) dto.pricingStale = true;\n\t} catch (err) {\n\t\t// eslint-disable-next-line no-console\n\t\tconsole.error(`[billing] pricing hydration failed for \"${plan.name}\":`, (err as Error).message);\n\t\tdto.pricingStale = true;\n\t}\n}\n\nexport function planController(store: IStoreAdapter, config: IBillingConfig, cache: PriceCache) {\n\tconst plans = new PlanModel(store);\n\tconst hydrate = config.pricing?.hydration === true;\n\n\treturn {\n\t\tasync list(_ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst list = await plans.list();\n\t\t\tconst dtos = list.map(toPlanDTO);\n\t\t\tif (hydrate) {\n\t\t\t\tawait Promise.all(dtos.map((dto, i) => hydratePricing(dto, list[i]!, config, cache)));\n\t\t\t}\n\t\t\treturn setApiResponse(HTTP.OK, 'PLAN_LIST', `Retrieved ${list.length} workspace plans`, {\n\t\t\t\tplans: dtos,\n\t\t\t});\n\t\t},\n\n\t\tasync get(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst params = ctx.meta['params'] as Record<string, string> | undefined;\n\t\t\tconst id = params?.['planId'];\n\t\t\tif (!id) return setApiResponse(HTTP.BAD_REQUEST, 'INVALID_PARAMETER', 'Plan ID required');\n\n\t\t\tconst plan = await plans.findById(id);\n\t\t\tif (!plan) return setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Plan not found');\n\n\t\t\tconst dto = toPlanDTO(plan);\n\t\t\tif (hydrate) await hydratePricing(dto, plan, config, cache);\n\n\t\t\treturn setApiResponse(HTTP.OK, 'PLAN_FETCHED', 'Plan retrieved successfully.', {\n\t\t\t\tplan: dto,\n\t\t\t});\n\t\t},\n\n\t\tasync create(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\t\t\tconst name = stringOrEmpty(body?.['name']);\n\t\t\tif (!name) {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'VALIDATION_ERROR', 'name is required');\n\t\t\t}\n\n\t\t\tconst plan = await plans.create({\n\t\t\t\tname,\n\t\t\t\tdescription: body?.['description'] != null ? String(body['description']) : null,\n\t\t\t\ttier: body?.['tier'] != null ? numberOrZero(body['tier']) : 0,\n\t\t\t\tseats: body?.['seats'] != null ? numberOrZero(body['seats']) : null,\n\t\t\t\ttrialDays: body?.['trialDays'] != null ? numberOrZero(body['trialDays']) : 0,\n\t\t\t\tmonthlyAmount: body?.['monthlyAmount'] != null ? numberOrZero(body['monthlyAmount']) : null,\n\t\t\t\tmonthlyPriceId: body?.['monthlyPriceId'] != null ? String(body['monthlyPriceId']) : null,\n\t\t\t\tyearlyAmount: body?.['yearlyAmount'] != null ? numberOrZero(body['yearlyAmount']) : null,\n\t\t\t\tyearlyPriceId: body?.['yearlyPriceId'] != null ? String(body['yearlyPriceId']) : null,\n\t\t\t\tfeatures: body?.['features'],\n\t\t\t\tmetadata: body?.['metadata'],\n\t\t\t});\n\n\t\t\treturn setApiResponse(HTTP.CREATED, 'PLAN_CREATED', 'Plan created successfully.', {\n\t\t\t\tplan: toPlanDTO(plan),\n\t\t\t});\n\t\t},\n\n\t\tasync update(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst params = ctx.meta['params'] as Record<string, string> | undefined;\n\t\t\tconst id = params?.['planId'];\n\t\t\tif (!id) {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_PARAMETER', 'Plan ID required');\n\t\t\t}\n\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\t\t\tif (!body || Object.keys(body).length === 0) {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'VALIDATION_ERROR', 'Request body is empty');\n\t\t\t}\n\n\t\t\tconst patch: Record<string, unknown> = {};\n\t\t\tconst allowed = ['name', 'description', 'tier', 'seats', 'trialDays',\n\t\t\t\t'monthlyAmount', 'monthlyPriceId', 'yearlyAmount', 'yearlyPriceId',\n\t\t\t\t'features', 'metadata'];\n\n\t\t\tfor (const key of allowed) {\n\t\t\t\tif (key in body) patch[key] = body[key];\n\t\t\t}\n\n\t\t\tconst plan = await plans.update(id, patch);\n\t\t\tif (!plan) {\n\t\t\t\treturn setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Plan not found');\n\t\t\t}\n\n\t\t\treturn setApiResponse(HTTP.OK, 'PLAN_UPDATED', 'Plan updated successfully.', {\n\t\t\t\tplan: toPlanDTO(plan),\n\t\t\t});\n\t\t},\n\n\t\tasync delete(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst params = ctx.meta['params'] as Record<string, string> | undefined;\n\t\t\tconst id = params?.['planId'];\n\t\t\tif (!id) {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_PARAMETER', 'Plan ID required');\n\t\t\t}\n\n\t\t\tconst deleted = await plans.delete(id);\n\t\t\tif (!deleted) {\n\t\t\t\treturn setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Plan not found');\n\t\t\t}\n\n\t\t\treturn setApiResponse(HTTP.OK, 'PLAN_DELETED', 'Plan deleted successfully.');\n\t\t},\n\t};\n}\n","import type { IFonderieContext } from '@fonderie/core';\n\nimport type { SubscriberType } from './types';\n\nexport interface ISubscriber {\n\ttype: SubscriberType;\n\tid: string;\n}\n\n// Narrow a bigint into a JS number, refusing values past 2^53 — loud failure\n// beats silent rounding. Used where a BOUNDED amount meets a number-typed\n// boundary (the wire-stable plan pricing DTO, the Stripe SDK). Deliberately\n// not named after money: wallet balances are unbounded and must stay bigint —\n// this is a narrowing tool, not a blessed money-to-number escape hatch.\nexport function toSafeNumber(amount: bigint): number {\n\tif (amount > BigInt(Number.MAX_SAFE_INTEGER) || amount < -BigInt(Number.MAX_SAFE_INTEGER)) {\n\t\tthrow new Error(`[billing] amount ${amount} exceeds Number.MAX_SAFE_INTEGER`);\n\t}\n\treturn Number(amount);\n}\n\n// One canonical form for wallet currency codes. Balances are keyed by the\n// literal string — a lowercase 'usd' or a padded 'USD ' would open a second,\n// unreachable bucket next to 'USD', so every boundary (config, schema, query\n// param, webhook metadata) normalizes through here. Trim + case only:\n// interior garbage ('U SD') is NOT repaired — write boundaries reject it\n// instead, because silently guessing at a money-bucket key hides caller bugs.\nexport function normalizeCurrency(currency: string): string {\n\treturn currency.trim().toUpperCase();\n}\n\n// Converts window strings like '1d', '30d', '1h' to milliseconds.\nexport function parseWindowMs(window: string): number {\n\tconst n = parseInt(window, 10);\n\tconst unit = window.slice(String(n).length);\n\tswitch (unit) {\n\t\tcase 'h':\n\t\t\treturn n * 3_600_000;\n\t\tcase 'd':\n\t\t\treturn n * 86_400_000;\n\t\tcase 'm':\n\t\t\treturn n * 60_000;\n\t\tdefault:\n\t\t\tthrow new Error(`Unknown window unit: '${unit}' in '${window}'`);\n\t}\n}\n\n// Resolves billing subscriber from request context.\n// Precedence: X-Workspace-ID header → ctx.workspace (set by withWorkspace) → ctx.user\nexport function resolveSubscriber(ctx: IFonderieContext): ISubscriber | null {\n\tconst wsFromHeader = ctx.request.headers.get('x-workspace-id');\n\n\tif (wsFromHeader) {\n\t\treturn {\n\t\t\ttype: 'workspace',\n\t\t\tid: wsFromHeader,\n\t\t};\n\t}\n\n\tif (ctx.workspace?.id) {\n\t\treturn {\n\t\t\ttype: 'workspace',\n\t\t\tid: ctx.workspace.id,\n\t\t};\n\t}\n\n\tif (ctx.user?.id) {\n\t\treturn {\n\t\t\ttype: 'user',\n\t\t\tid: ctx.user.id,\n\t\t};\n\t}\n\n\treturn null;\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IPlan } from '../types';\nimport type { IBillingConfig, IBillingPlan } from '../config';\nimport { toSafeNumber } from '../utils';\n\n// fonderie_plans money columns are BIGINT (int8) since 006_wallet.sql — pg\n// returns those as strings. The read model keeps JS numbers (bounded display\n// cents; the wire format stays numeric), guarded loudly at 2^53.\ntype IPlanRow = Omit<IPlan, 'monthlyAmount' | 'yearlyAmount'> & {\n\tmonthlyAmount: string | number | null;\n\tyearlyAmount: string | number | null;\n};\n\nconst planAmount = (v: string | number | null): number | null =>\n\tv == null ? null : toSafeNumber(BigInt(v));\n\nfunction mapPlanRow(row: IPlanRow): IPlan {\n\treturn {\n\t\t...row,\n\t\tmonthlyAmount: planAmount(row.monthlyAmount),\n\t\tyearlyAmount: planAmount(row.yearlyAmount),\n\t};\n}\n\nexport function getPlans(config: IBillingConfig): IBillingPlan[] {\n\treturn config.plans;\n}\n\nexport function getPlanByName(name: string, config: IBillingConfig): IBillingPlan | null {\n\treturn config.plans.find((p) => p.name.toLowerCase() === name.toLowerCase()) ?? null;\n}\n\n/**\n * Attribute a subscription to a Fonderie plan by its price, precedence\n * `lookup_key → priceId` (§16.3). Pure — pass the plans list. Returns null so the\n * caller can fall back to the legacy nickname-derived plan.\n */\nexport function resolvePlanNameByPrice(\n\tprice: { lookupKey?: string | null; priceId?: string | null },\n\tplans: IBillingPlan[],\n): string | null {\n\tconst find = (pred: (p?: { lookupKey?: string; priceId?: string }) => boolean) =>\n\t\tplans.find((pl) => pred(pl.monthly) || pred(pl.yearly))?.name ?? null;\n\tif (price.lookupKey) {\n\t\tconst m = find((p) => p?.lookupKey === price.lookupKey);\n\t\tif (m) return m;\n\t}\n\tif (price.priceId) {\n\t\tconst m = find((p) => p?.priceId === price.priceId);\n\t\tif (m) return m;\n\t}\n\treturn null;\n}\n\n// Plan wallet config carries bigints; the JSONB ops copy stores them as\n// digit strings.\nconst walletToJson = (wallet: IBillingPlan['wallet']): string | null =>\n\twallet == null\n\t\t? null\n\t\t: JSON.stringify(wallet, (_key, value) => (typeof value === 'bigint' ? value.toString() : value));\n\nexport async function syncPlansToDB(config: IBillingConfig, store: IStoreAdapter): Promise<void> {\n\tconst plans = config.plans;\n\tif (plans.length === 0) return;\n\n\tconst values = plans.map((_, i) => {\n\t\tconst b = i * 10;\n\t\treturn `($${b + 1}, $${b + 2}, $${b + 3}, $${b + 4}, $${b + 5}, $${b + 6}, $${b + 7}, $${b + 8}, $${b + 9}::jsonb, $${b + 10}::jsonb)`;\n\t});\n\n\tconst params = plans.flatMap((plan) => [\n\t\tplan.name,\n\t\tplan.trialDays ?? 0,\n\t\t// bigint params go over the wire as strings; pg casts into the column type.\n\t\tplan.monthly?.amount?.toString() ?? null,\n\t\tplan.monthly?.priceId ?? null,\n\t\tplan.yearly?.amount?.toString() ?? null,\n\t\tplan.yearly?.priceId ?? null,\n\t\tplan.description ?? null,\n\t\tplan.tier ?? 0,\n\t\tJSON.stringify(plan.metadata ?? {}),\n\t\twalletToJson(plan.wallet),\n\t]);\n\n\tawait store.query(\n\t\t`INSERT INTO fonderie_plans\n\t\t\t(name, trial_days,\n\t\t\t monthly_amount, monthly_price_id,\n\t\t\t yearly_amount, yearly_price_id,\n\t\t\t description, tier, metadata, wallet)\n\t\tVALUES ${values.join(', ')}\n\t\tON CONFLICT (name) DO UPDATE SET\n\t\t\ttrial_days = EXCLUDED.trial_days,\n\t\t\tmonthly_amount = EXCLUDED.monthly_amount,\n\t\t\tmonthly_price_id = EXCLUDED.monthly_price_id,\n\t\t\tyearly_amount = EXCLUDED.yearly_amount,\n\t\t\tyearly_price_id = EXCLUDED.yearly_price_id,\n\t\t\tdescription = EXCLUDED.description,\n\t\t\ttier = EXCLUDED.tier,\n\t\t\tmetadata = EXCLUDED.metadata,\n\t\t\twallet = EXCLUDED.wallet`,\n\t\tparams,\n\t);\n}\n\nconst SELECT_PLAN = `\n\tSELECT\n\t\tid,\n\t\tname,\n\t\tseats,\n\t\ttrial_days AS \"trialDays\",\n\t\tmonthly_amount AS \"monthlyAmount\",\n\t\tmonthly_price_id AS \"monthlyPriceId\",\n\t\tyearly_amount AS \"yearlyAmount\",\n\t\tyearly_price_id AS \"yearlyPriceId\",\n\t\tdescription,\n\t\ttier,\n\t\tfeatures,\n\t\tmetadata\n\tFROM fonderie_plans`;\n\nexport async function getDBPlans(store: IStoreAdapter): Promise<IPlan[]> {\n\tconst rows = await store.query<IPlanRow>(\n\t\t`${SELECT_PLAN} WHERE active = true ORDER BY tier ASC, monthly_amount ASC NULLS LAST`,\n\t);\n\treturn rows.map(mapPlanRow);\n}\n\nexport async function getPlanById(id: string, store: IStoreAdapter): Promise<IPlan | null> {\n\tconst [row] = await store.query<IPlanRow>(`${SELECT_PLAN} WHERE id = $1`, [id]);\n\treturn row ? mapPlanRow(row) : null;\n}\n\nexport async function createPlan(\n\tdata: {\n\t\tname: string;\n\t\tdescription?: string | null;\n\t\ttier?: number;\n\t\tseats?: number | null;\n\t\ttrialDays?: number;\n\t\tfeatures?: unknown;\n\t\tmetadata?: unknown;\n\t\tmonthlyAmount?: number | null;\n\t\tmonthlyPriceId?: string | null;\n\t\tyearlyAmount?: number | null;\n\t\tyearlyPriceId?: string | null;\n\t},\n\tstore: IStoreAdapter,\n): Promise<IPlan> {\n\tconst [row] = await store.query<IPlanRow>(\n\t\t`INSERT INTO fonderie_plans\n\t\t\t(name, seats, trial_days, monthly_amount, monthly_price_id,\n\t\t\t yearly_amount, yearly_price_id, description, tier, features, metadata)\n\t\tVALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10::jsonb, $11::jsonb)\n\t\tRETURNING\n\t\t\tid, name, seats,\n\t\t\ttrial_days AS \"trialDays\",\n\t\t\tmonthly_amount AS \"monthlyAmount\",\n\t\t\tmonthly_price_id AS \"monthlyPriceId\",\n\t\t\tyearly_amount AS \"yearlyAmount\",\n\t\t\tyearly_price_id AS \"yearlyPriceId\",\n\t\t\tdescription, tier, features, metadata`,\n\t\t[\n\t\t\tdata.name,\n\t\t\tdata.seats ?? null,\n\t\t\tdata.trialDays ?? 0,\n\t\t\tdata.monthlyAmount ?? null,\n\t\t\tdata.monthlyPriceId ?? null,\n\t\t\tdata.yearlyAmount ?? null,\n\t\t\tdata.yearlyPriceId ?? null,\n\t\t\tdata.description ?? null,\n\t\t\tdata.tier ?? 0,\n\t\t\tJSON.stringify(data.features ?? []),\n\t\t\tJSON.stringify(data.metadata ?? {}),\n\t\t],\n\t);\n\tif (!row) throw new Error('Failed to create plan');\n\treturn mapPlanRow(row);\n}\n\nexport async function updatePlan(\n\tid: string,\n\tdata: Partial<Omit<IPlan, 'id'>>,\n\tstore: IStoreAdapter,\n): Promise<IPlan | null> {\n\tconst fieldMap: Record<string, string> = {\n\t\tname: 'name',\n\t\tseats: 'seats',\n\t\ttrialDays: 'trial_days',\n\t\tmonthlyAmount: 'monthly_amount',\n\t\tmonthlyPriceId: 'monthly_price_id',\n\t\tyearlyAmount: 'yearly_amount',\n\t\tyearlyPriceId: 'yearly_price_id',\n\t\tdescription: 'description',\n\t\ttier: 'tier',\n\t};\n\n\tconst jsonbFields = new Set(['features', 'metadata']);\n\tconst setClauses: string[] = [];\n\tconst params: unknown[] = [id];\n\n\tfor (const [key, col] of Object.entries(fieldMap)) {\n\t\tif (key in data) {\n\t\t\tparams.push((data as Record<string, unknown>)[key]);\n\t\t\tsetClauses.push(`${col} = $${params.length}`);\n\t\t}\n\t}\n\n\tfor (const key of jsonbFields) {\n\t\tif (key in data) {\n\t\t\tparams.push(JSON.stringify((data as Record<string, unknown>)[key]));\n\t\t\tsetClauses.push(`${key} = $${params.length}::jsonb`);\n\t\t}\n\t}\n\n\tif (setClauses.length === 0) return getPlanById(id, store);\n\n\tconst [row] = await store.query<IPlanRow>(\n\t\t`UPDATE fonderie_plans SET ${setClauses.join(', ')}\n\t\tWHERE id = $1\n\t\tRETURNING\n\t\t\tid, name, seats,\n\t\t\ttrial_days AS \"trialDays\",\n\t\t\tmonthly_amount AS \"monthlyAmount\",\n\t\t\tmonthly_price_id AS \"monthlyPriceId\",\n\t\t\tyearly_amount AS \"yearlyAmount\",\n\t\t\tyearly_price_id AS \"yearlyPriceId\",\n\t\t\tdescription, tier, features, metadata`,\n\t\tparams,\n\t);\n\treturn row ? mapPlanRow(row) : null;\n}\n\nexport async function deletePlan(id: string, store: IStoreAdapter): Promise<boolean> {\n\tconst rows = await store.query<{ id: string }>(\n\t\t`DELETE FROM fonderie_plans WHERE id = $1 RETURNING id`,\n\t\t[id],\n\t);\n\treturn rows.length > 0;\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IPlan } from '../types';\nimport type { IBillingConfig } from '../config';\nimport {\n\tgetDBPlans,\n\tgetPlanById,\n\tcreatePlan,\n\tupdatePlan,\n\tdeletePlan,\n\tgetPlans,\n\tgetPlanByName,\n} from '../services/plans';\n\nexport class PlanModel {\n\tconstructor(private readonly store: IStoreAdapter) {}\n\n\tlistFromConfig(config: IBillingConfig) {\n\t\treturn getPlans(config);\n\t}\n\n\tfindByNameInConfig(name: string, config: IBillingConfig) {\n\t\treturn getPlanByName(name, config);\n\t}\n\n\tlist(): Promise<IPlan[]> {\n\t\treturn getDBPlans(this.store);\n\t}\n\n\tfindById(id: string): Promise<IPlan | null> {\n\t\treturn getPlanById(id, this.store);\n\t}\n\n\tcreate(data: Parameters<typeof createPlan>[0]): Promise<IPlan> {\n\t\treturn createPlan(data, this.store);\n\t}\n\n\tupdate(id: string, data: Parameters<typeof updatePlan>[1]): Promise<IPlan | null> {\n\t\treturn updatePlan(id, data, this.store);\n\t}\n\n\tdelete(id: string): Promise<boolean> {\n\t\treturn deletePlan(id, this.store);\n\t}\n}\n","import type {\n\tIPlan,\n\tIPlanFeature,\n\tISubscription,\n\tIWalletLedgerEntry,\n\tSubscriberType,\n\tWalletLedgerType,\n} from '../types';\n\nexport interface IPlanDTO {\n\tid: string;\n\tplanId: string;\n\tname: string;\n\tdescription: string;\n\ttier: number;\n\tseats: number | null;\n\ttrialDays: number;\n\tpricing: {\n\t\tmonthly: number; // in cents, e.g. 1999 = $19.99\n\t\tyearly: number; // in cents\n\t\tcurrency: string; // ISO 4217, e.g. 'USD'\n\t};\n\t/** True when pricing was served from stale cache (transfer window / provider outage). */\n\tpricingStale?: boolean;\n\tfeatures: IPlanFeature[];\n\tmetadata: Record<string, unknown>;\n}\n\nexport interface ISubscriptionDTO {\n\tid: string;\n\tsubscriberType: SubscriberType;\n\tsubscriberId: string;\n\tplan: string;\n\tinterval: string;\n\tstatus: string;\n\tcancelAtPeriodEnd: boolean;\n\tcurrentPeriodStart: string | null;\n\tcurrentPeriodEnd: string | null;\n\ttrialEndsAt: string | null;\n\tcreatedAt: string;\n}\n\nexport function toPlanDTO(plan: IPlan): IPlanDTO {\n\treturn {\n\t\tid: plan.id,\n\t\tplanId: plan.name.toUpperCase(),\n\t\tname: plan.name,\n\t\tdescription: plan.description ?? '',\n\t\ttier: plan.tier,\n\t\tseats: plan.seats,\n\t\ttrialDays: plan.trialDays,\n\t\tpricing: {\n\t\t\tmonthly: plan.monthlyAmount ?? 0,\n\t\t\tyearly: plan.yearlyAmount ?? 0,\n\t\t\tcurrency: 'USD',\n\t\t},\n\t\tfeatures: Array.isArray(plan.features) ? plan.features : [],\n\t\tmetadata:\n\t\t\tplan.metadata && typeof plan.metadata === 'object'\n\t\t\t\t? (plan.metadata as Record<string, unknown>)\n\t\t\t\t: {},\n\t};\n}\n\n// The pg driver returns TIMESTAMPTZ columns as Date objects (no type-parser\n// override exists); the DTO's string fields were only correct by accident of\n// Date.toJSON. Normalize explicitly, like the wallet path does.\nconst isoOrNull = (value: string | Date | null): string | null =>\n\tvalue == null ? null : new Date(value).toISOString();\n\nexport function toSubscriptionDTO(sub: ISubscription): ISubscriptionDTO {\n\treturn {\n\t\tid: sub.id,\n\t\tsubscriberType: sub.subscriberType,\n\t\tsubscriberId: sub.subscriberId,\n\t\tplan: sub.plan,\n\t\tinterval: sub.interval,\n\t\tstatus: sub.status,\n\t\tcancelAtPeriodEnd: sub.cancelAtPeriodEnd,\n\t\tcurrentPeriodStart: isoOrNull(sub.currentPeriodStart),\n\t\tcurrentPeriodEnd: isoOrNull(sub.currentPeriodEnd),\n\t\ttrialEndsAt: isoOrNull(sub.trialEndsAt),\n\t\tcreatedAt: isoOrNull(sub.createdAt) ?? '',\n\t};\n}\n\n// Wallet amounts are bigint on the server and would throw in JSON.stringify —\n// the DTO layer serializes every money field as a digit string.\nexport interface IWalletDTO {\n\tbalance: string; // smallest currency unit, e.g. '1999' = $19.99 at precision 2\n\tcurrency: string;\n\tprecision: number;\n}\n\nexport interface IWalletTransactionDTO {\n\tid: string;\n\ttype: WalletLedgerType;\n\tamount: string; // signed: positive = credit, negative = debit\n\tbalanceAfter: string;\n\tcurrency: string;\n\tdescription: string | null;\n\tproviderTxId: string | null;\n\tmetadata: Record<string, unknown>;\n\tcreatedAt: string;\n}\n\nexport function toWalletDTO(balance: bigint, currency: string, precision: number): IWalletDTO {\n\treturn { balance: balance.toString(), currency, precision };\n}\n\nexport function toWalletTransactionDTO(entry: IWalletLedgerEntry): IWalletTransactionDTO {\n\treturn {\n\t\tid: entry.id,\n\t\ttype: entry.type,\n\t\tamount: entry.amount.toString(),\n\t\tbalanceAfter: entry.balanceAfter.toString(),\n\t\tcurrency: entry.currency,\n\t\tdescription: entry.description,\n\t\tproviderTxId: entry.providerTxId,\n\t\tmetadata: entry.metadata,\n\t\tcreatedAt: entry.createdAt,\n\t};\n}\n","import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { SubscriptionModel } from '../models/subscription.model';\nimport { toSubscriptionDTO } from '../dtos/billing';\nimport { resolveSubscriber } from '../utils';\n\nexport function subscriptionController(store: IStoreAdapter) {\n\tconst subscriptions = new SubscriptionModel(store);\n\n\treturn {\n\t\tasync get(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst subscriber = resolveSubscriber(ctx);\n\t\t\tif (!subscriber) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.BAD_REQUEST,\n\t\t\t\t\t'SUBSCRIBER_REQUIRED',\n\t\t\t\t\t'Subscriber context required',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst subscription = await subscriptions.get(subscriber.type, subscriber.id);\n\t\t\tif (!subscription)\n\t\t\t\treturn setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No active subscription');\n\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.OK,\n\t\t\t\t'SUBSCRIPTION_FETCHED',\n\t\t\t\t'Subscription retrieved successfully.',\n\t\t\t\t{\n\t\t\t\t\tsubscription: toSubscriptionDTO(subscription),\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\t};\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { BillingInterval, ISubscription, SubscriberType } from '../types';\n\nconst SELECT_SUBSCRIPTION = `\n\tSELECT\n\t\tid,\n\t\tsubscriber_type AS \"subscriberType\",\n\t\tsubscriber_id AS \"subscriberId\",\n\t\tplan,\n\t\tinterval,\n\t\tstatus,\n\t\tprovider_customer_id AS \"providerCustomerId\",\n\t\tprovider_subscription_id AS \"providerSubscriptionId\",\n\t\tcurrent_period_start AS \"currentPeriodStart\",\n\t\tcurrent_period_end AS \"currentPeriodEnd\",\n\t\tcancel_at_period_end AS \"cancelAtPeriodEnd\",\n\t\ttrial_ends_at AS \"trialEndsAt\",\n\t\tcreated_at AS \"createdAt\"\n\tFROM fonderie_subscriptions`;\n\nexport async function getSubscription(\n\tsubscriberType: SubscriberType,\n\tsubscriberId: string,\n\tstore: IStoreAdapter,\n): Promise<ISubscription | null> {\n\tconst [row] = await store.query<ISubscription>(\n\t\t`${SELECT_SUBSCRIPTION} WHERE subscriber_type = $1 AND subscriber_id = $2`,\n\t\t[subscriberType, subscriberId],\n\t);\n\treturn row ?? null;\n}\n\nexport async function upsertSubscription(\n\tdata: {\n\t\tsubscriberType: SubscriberType;\n\t\tsubscriberId: string;\n\t\tplan: string;\n\t\tinterval?: BillingInterval;\n\t\tstatus: string;\n\t\tproviderCustomerId?: string;\n\t\tproviderSubscriptionId?: string;\n\t\tcurrentPeriodStart?: Date;\n\t\tcurrentPeriodEnd?: Date;\n\t\tcancelAtPeriodEnd?: boolean;\n\t\ttrialEndsAt?: Date | null;\n\t},\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tawait store.query(\n\t\t`INSERT INTO fonderie_subscriptions\n\t\t\t(subscriber_type, subscriber_id, plan, interval, status,\n\t\t\t provider_customer_id, provider_subscription_id,\n\t\t\t current_period_start, current_period_end,\n\t\t\t cancel_at_period_end, trial_ends_at)\n\t\t VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)\n\t\t ON CONFLICT (subscriber_type, subscriber_id) DO UPDATE SET\n\t\t\t plan = $3,\n\t\t\t interval = $4,\n\t\t\t status = $5,\n\t\t\t provider_customer_id = COALESCE($6, fonderie_subscriptions.provider_customer_id),\n\t\t\t provider_subscription_id = COALESCE($7, fonderie_subscriptions.provider_subscription_id),\n\t\t\t current_period_start = $8,\n\t\t\t current_period_end = $9,\n\t\t\t cancel_at_period_end = $10,\n\t\t\t trial_ends_at = $11`,\n\t\t[\n\t\t\tdata.subscriberType,\n\t\t\tdata.subscriberId,\n\t\t\tdata.plan,\n\t\t\tdata.interval ?? 'month',\n\t\t\tdata.status,\n\t\t\tdata.providerCustomerId ?? null,\n\t\t\tdata.providerSubscriptionId ?? null,\n\t\t\tdata.currentPeriodStart ?? null,\n\t\t\tdata.currentPeriodEnd ?? null,\n\t\t\tdata.cancelAtPeriodEnd ?? false,\n\t\t\tdata.trialEndsAt ?? null,\n\t\t],\n\t);\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { ISubscription, SubscriberType } from '../types';\nimport { getSubscription, upsertSubscription } from '../services/subscriptions';\n\nexport class SubscriptionModel {\n\tconstructor(private readonly store: IStoreAdapter) {}\n\n\tget(subscriberType: SubscriberType, subscriberId: string): Promise<ISubscription | null> {\n\t\treturn getSubscription(subscriberType, subscriberId, this.store);\n\t}\n\n\tupsert(data: Parameters<typeof upsertSubscription>[0]): Promise<void> {\n\t\treturn upsertSubscription(data, this.store);\n\t}\n}\n","import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IBillingConfig, IBillingPlan, IBillingPlanPrice } from '../config';\nimport type { BillingInterval } from '../types';\nimport { BILLING_INTERVAL, BILLING_INTERVALS, isBillingInterval } from '../types';\nimport { PlanModel } from '../models/plan.model';\nimport { SubscriptionModel } from '../models/subscription.model';\nimport { resolveSubscriber } from '../utils';\n\n// Exhaustive by construction: a new BillingInterval fails compilation here\n// instead of silently falling through to a default price.\nfunction planPriceFor(plan: IBillingPlan, interval: BillingInterval): IBillingPlanPrice | undefined {\n\tswitch (interval) {\n\t\tcase BILLING_INTERVAL.MONTH:\n\t\t\treturn plan.monthly;\n\t\tcase BILLING_INTERVAL.YEAR:\n\t\t\treturn plan.yearly;\n\t\tdefault: {\n\t\t\tconst unhandled: never = interval;\n\t\t\tthrow new Error(`[billing] unhandled billing interval: ${unhandled}`);\n\t\t}\n\t}\n}\n\nexport function checkoutController(store: IStoreAdapter, config: IBillingConfig) {\n\tconst plans = new PlanModel(store);\n\tconst subscriptions = new SubscriptionModel(store);\n\n\treturn {\n\t\tasync createSession(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\t\t\tconst planName = body?.['plan'];\n\t\t\tconst interval = body?.['interval'] ?? BILLING_INTERVAL.MONTH;\n\t\t\tconst subscriber = resolveSubscriber(ctx);\n\n\t\t\tif (typeof planName !== 'string') {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'plan is required');\n\t\t\t}\n\t\t\tif (!isBillingInterval(interval)) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t\t`interval must be one of: ${BILLING_INTERVALS.join(', ')}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (!subscriber) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.BAD_REQUEST,\n\t\t\t\t\t'SUBSCRIBER_REQUIRED',\n\t\t\t\t\t'Subscriber context required',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst plan = plans.findByNameInConfig(planName, config);\n\t\t\tif (!plan) {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', `Unknown plan: ${planName}`);\n\t\t\t}\n\n\t\t\tconst pricing = planPriceFor(plan, interval);\n\t\t\tif (!pricing?.priceId) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t\t`Plan ${planName} does not support ${interval} billing`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// If there's already an active subscription: allow upgrades only, and\n\t\t\t// change the subscription in place (proration) rather than opening a\n\t\t\t// second checkout / creating a duplicate subscription.\n\t\t\tconst current = await subscriptions.get(subscriber.type, subscriber.id);\n\t\t\tconst ACTIVE = ['active', 'trialing', 'past_due'];\n\t\t\tif (current && ACTIVE.includes(current.status)) {\n\t\t\t\tconst currentTier = plans.findByNameInConfig(current.plan, config)?.tier ?? -1;\n\t\t\t\tconst targetTier = plan.tier ?? -1;\n\t\t\t\tif (targetTier <= currentTier) {\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t\t'DOWNGRADE_NOT_ALLOWED',\n\t\t\t\t\t\t`Cannot switch from ${current.plan} to a same-or-lower tier (${planName}) mid-cycle. Upgrades only.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (current.providerSubscriptionId) {\n\t\t\t\t\tconst res = await config.provider.updateSubscription({\n\t\t\t\t\t\tsubscriptionId: current.providerSubscriptionId,\n\t\t\t\t\t\tpriceId: pricing.priceId,\n\t\t\t\t\t});\n\t\t\t\t\tconst upsert: Parameters<typeof subscriptions.upsert>[0] = {\n\t\t\t\t\t\tsubscriberType: subscriber.type,\n\t\t\t\t\t\tsubscriberId: subscriber.id,\n\t\t\t\t\t\tplan: planName,\n\t\t\t\t\t\tinterval,\n\t\t\t\t\t\tstatus: res.status,\n\t\t\t\t\t\tproviderSubscriptionId: current.providerSubscriptionId,\n\t\t\t\t\t};\n\t\t\t\t\tif (current.providerCustomerId) upsert.providerCustomerId = current.providerCustomerId;\n\t\t\t\t\tif (res.currentPeriodStart) upsert.currentPeriodStart = res.currentPeriodStart;\n\t\t\t\t\tif (res.currentPeriodEnd) upsert.currentPeriodEnd = res.currentPeriodEnd;\n\t\t\t\t\tawait subscriptions.upsert(upsert);\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t\t'SUBSCRIPTION_UPGRADED',\n\t\t\t\t\t\t'Subscription upgraded; the prorated difference was charged.',\n\t\t\t\t\t\t{ upgraded: true, plan: planName },\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst { customerId } = await config.provider.createCustomer({\n\t\t\t\temail: ctx.user!.email ?? '',\n\t\t\t\tsubscriberType: subscriber.type,\n\t\t\t\tsubscriberId: subscriber.id,\n\t\t\t\tuserId: ctx.user!.id,\n\t\t\t});\n\n\t\t\tconst sessionOpts: Parameters<typeof config.provider.createCheckoutSession>[0] = {\n\t\t\t\tcustomerId,\n\t\t\t\tpriceId: pricing.priceId,\n\t\t\t\tsubscriberType: subscriber.type,\n\t\t\t\tsubscriberId: subscriber.id,\n\t\t\t\tsuccessUrl: config.successUrl,\n\t\t\t\tcancelUrl: config.cancelUrl,\n\t\t\t};\n\t\t\tif (plan.trialDays !== undefined) sessionOpts.trialDays = plan.trialDays;\n\n\t\t\tconst { url } = await config.provider.createCheckoutSession(sessionOpts);\n\n\t\t\tawait subscriptions.upsert({\n\t\t\t\tsubscriberType: subscriber.type,\n\t\t\t\tsubscriberId: subscriber.id,\n\t\t\t\tplan: planName,\n\t\t\t\tinterval,\n\t\t\t\tstatus: 'incomplete',\n\t\t\t\tproviderCustomerId: customerId,\n\t\t\t});\n\n\t\t\treturn setApiResponse(HTTP.OK, 'CHECKOUT_URL', 'Checkout session created.', { url });\n\t\t},\n\n\t\tasync createPortal(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst subscriber = resolveSubscriber(ctx);\n\t\t\tif (!subscriber) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.BAD_REQUEST,\n\t\t\t\t\t'SUBSCRIBER_REQUIRED',\n\t\t\t\t\t'Subscriber context required',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst subscription = await subscriptions.get(subscriber.type, subscriber.id);\n\t\t\tif (!subscription?.providerCustomerId) {\n\t\t\t\treturn setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No active subscription');\n\t\t\t}\n\n\t\t\tconst { url } = await config.provider.createPortalSession({\n\t\t\t\tcustomerId: subscription.providerCustomerId,\n\t\t\t\treturnUrl: config.successUrl,\n\t\t\t});\n\n\t\t\treturn setApiResponse(HTTP.OK, 'PORTAL_URL', 'Portal session created.', { url });\n\t\t},\n\t};\n}\n","import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { UsageModel } from '../models/usage.model';\nimport { resolveSubscriber } from '../utils';\n\nexport function usageController(store: IStoreAdapter) {\n\tconst usage = new UsageModel(store);\n\n\treturn {\n\t\tasync record(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\t\t\tconst metric = body?.['metric'];\n\t\t\tconst quantity = body?.['quantity'];\n\t\t\tconst subscriber = resolveSubscriber(ctx);\n\n\t\t\tif (!subscriber) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.BAD_REQUEST,\n\t\t\t\t\t'SUBSCRIBER_REQUIRED',\n\t\t\t\t\t'Subscriber context required',\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (typeof metric !== 'string') {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'metric is required');\n\t\t\t}\n\n\t\t\tawait usage.record({\n\t\t\t\tsubscriberType: subscriber.type,\n\t\t\t\tsubscriberId: subscriber.id,\n\t\t\t\tmetric,\n\t\t\t\tquantity: typeof quantity === 'number' ? quantity : 1,\n\t\t\t});\n\t\t\treturn setApiResponse(HTTP.OK, 'USAGE_RECORDED', 'Usage recorded successfully.');\n\t\t},\n\n\t\tasync get(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst params = ctx.meta['params'] as Record<string, string> | undefined;\n\t\t\tconst metric = params?.['metric'];\n\t\t\tconst subscriber = resolveSubscriber(ctx);\n\n\t\t\tif (!subscriber || !metric) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t\t'subscriber and metric are required',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst since = new Date();\n\t\t\tsince.setDate(1);\n\t\t\tsince.setHours(0, 0, 0, 0);\n\n\t\t\tconst total = await usage.get(subscriber.type, subscriber.id, metric, since);\n\t\t\treturn setApiResponse(HTTP.OK, 'USAGE_FETCHED', 'Usage retrieved successfully.', {\n\t\t\t\tmetric,\n\t\t\t\ttotal,\n\t\t\t\t// Explicit ISO — the client's IUsageResult.since promises a string.\n\t\t\t\tsince: since.toISOString(),\n\t\t\t});\n\t\t},\n\t};\n}\n","import type { IStoreAdapter } from '@fonderie/store';\nimport type { SubscriberType } from '../types';\n\nexport async function recordUsage(\n\topts: { subscriberType: SubscriberType; subscriberId: string; metric: string; quantity: number },\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tawait store.query(\n\t\t`INSERT INTO fonderie_usage_records (subscriber_type, subscriber_id, metric, quantity)\n\t\tVALUES ($1, $2, $3, $4)`,\n\t\t[opts.subscriberType, opts.subscriberId, opts.metric, opts.quantity],\n\t);\n}\n\nexport async function getUsage(\n\tsubscriberType: SubscriberType,\n\tsubscriberId: string,\n\tmetric: string,\n\tsince: Date,\n\tstore: IStoreAdapter,\n): Promise<number> {\n\tconst rows = await store.query<{ total: string }>(\n\t\t`SELECT COALESCE(SUM(quantity), 0) AS total\n\t\tFROM fonderie_usage_records\n\t\tWHERE subscriber_type = $1\n\t\t\tAND subscriber_id = $2\n\t\t\tAND metric = $3\n\t\t\tAND recorded_at >= $4`,\n\t\t[subscriberType, subscriberId, metric, since],\n\t);\n\treturn parseInt(rows[0]?.total ?? '0', 10);\n}\n","import type { IStoreAdapter } from '@fonderie/store';\nimport type { SubscriberType } from '../types';\n\nimport { recordUsage, getUsage } from '../services/usage';\n\nexport class UsageModel {\n\tconstructor(private readonly store: IStoreAdapter) {}\n\n\trecord(opts: Parameters<typeof recordUsage>[0]): Promise<void> {\n\t\treturn recordUsage(opts, this.store);\n\t}\n\n\tget(\n\t\tsubscriberType: SubscriberType,\n\t\tsubscriberId: string,\n\t\tmetric: string,\n\t\tsince: Date,\n\t): Promise<number> {\n\t\treturn getUsage(subscriberType, subscriberId, metric, since, this.store);\n\t}\n}\n","import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IBillingConfig } from '../config';\nimport type { SubscriberType } from '../types';\nimport { SubscriptionModel } from '../models/subscription.model';\nimport { WalletModel } from '../models/wallet.model';\nimport { decodeLedgerCursor } from '../services/wallet';\nimport { findCreditPack } from '../services/credit-packs';\nimport { DuplicateTransactionError } from '../errors';\nimport { toWalletDTO, toWalletTransactionDTO } from '../dtos/billing';\nimport { getWalletStatus } from '../helpers';\nimport { normalizeCurrency, resolveSubscriber } from '../utils';\n\n// The wallet routes are only registered when config.wallet is present, so\n// config.wallet is always defined on these paths — defaults are still applied\n// defensively.\n\nexport function walletController(store: IStoreAdapter, config: IBillingConfig) {\n\tconst wallet = new WalletModel(store);\n\tconst subscriptions = new SubscriptionModel(store);\n\n\tconst defaultCurrency = () => normalizeCurrency(config.wallet?.currency ?? 'USD');\n\n\t// ?currency= lets a multi-currency subscriber address a specific balance;\n\t// otherwise reads follow the same bucket every write path uses — the\n\t// subscriber's plan-wallet currency (cached by withBilling), then the\n\t// configured default.\n\tconst currencyOf = (ctx: IFonderieContext) => {\n\t\tconst q = new URL(ctx.request.url).searchParams.get('currency');\n\t\tif (q) return normalizeCurrency(q);\n\t\treturn getWalletStatus(ctx)?.currency ?? defaultCurrency();\n\t};\n\n\tconst precisionOf = (ctx: IFonderieContext) =>\n\t\tgetWalletStatus(ctx)?.precision ?? config.wallet?.precision ?? 2;\n\n\treturn {\n\t\tasync get(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst subscriber = resolveSubscriber(ctx);\n\t\t\tif (!subscriber) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.BAD_REQUEST,\n\t\t\t\t\t'SUBSCRIBER_REQUIRED',\n\t\t\t\t\t'Subscriber context required',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst currency = currencyOf(ctx);\n\t\t\tconst { balance } = await wallet.balance({\n\t\t\t\tsubscriberType: subscriber.type,\n\t\t\t\tsubscriberId: subscriber.id,\n\t\t\t\tcurrency,\n\t\t\t});\n\n\t\t\treturn setApiResponse(HTTP.OK, 'WALLET_FETCHED', 'Wallet retrieved successfully.', {\n\t\t\t\twallet: toWalletDTO(balance, currency, precisionOf(ctx)),\n\t\t\t});\n\t\t},\n\n\t\tasync transactions(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst subscriber = resolveSubscriber(ctx);\n\t\t\tif (!subscriber) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.BAD_REQUEST,\n\t\t\t\t\t'SUBSCRIBER_REQUIRED',\n\t\t\t\t\t'Subscriber context required',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst params = new URL(ctx.request.url).searchParams;\n\t\t\tconst rawLimit = params.get('limit');\n\t\t\tconst limit = rawLimit !== null ? Number.parseInt(rawLimit, 10) : 50;\n\t\t\tif (Number.isNaN(limit) || limit < 1 || limit > 100) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t\t'limit must be an integer between 1 and 100',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst rawCursor = params.get('cursor');\n\t\t\tconst cursor = rawCursor !== null ? decodeLedgerCursor(rawCursor) : null;\n\t\t\tif (rawCursor !== null && cursor === null) {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'Malformed cursor');\n\t\t\t}\n\n\t\t\tconst page = await wallet.ledger({\n\t\t\t\tsubscriberType: subscriber.type,\n\t\t\t\tsubscriberId: subscriber.id,\n\t\t\t\tcurrency: currencyOf(ctx),\n\t\t\t\tlimit,\n\t\t\t\t...(cursor ? { cursor } : {}),\n\t\t\t});\n\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.OK,\n\t\t\t\t'WALLET_TRANSACTIONS',\n\t\t\t\t`Retrieved ${page.entries.length} wallet transactions`,\n\t\t\t\t{\n\t\t\t\t\ttransactions: page.entries.map(toWalletTransactionDTO),\n\t\t\t\t\tnextCursor: page.nextCursor,\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\n\t\t// One-time checkout for a credit pack. The pack's credits and the\n\t\t// buyer's WALLET currency are snapshotted into the session metadata at\n\t\t// creation time, so the webhook credits exactly what was bought (even\n\t\t// if config changes later) into the bucket the buyer's spend paths\n\t\t// actually read. pack.currency only prices the provider charge.\n\t\tasync checkout(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst body = ctx.meta['body'] as { packId: string };\n\t\t\tconst subscriber = resolveSubscriber(ctx);\n\t\t\tif (!subscriber) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.BAD_REQUEST,\n\t\t\t\t\t'SUBSCRIBER_REQUIRED',\n\t\t\t\t\t'Subscriber context required',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst pack = findCreditPack(body.packId, config);\n\t\t\tif (!pack) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t\t`Unknown credit pack: ${body.packId}`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (!config.provider.createPaymentCheckoutSession) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.NOT_IMPLEMENTED,\n\t\t\t\t\t'PAYMENT_NOT_SUPPORTED',\n\t\t\t\t\t`Provider '${config.provider.name}' does not support one-time payments`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// The wallet bucket to credit: the buyer's plan-wallet currency\n\t\t\t// (cached by withBilling), falling back to the global default. A\n\t\t\t// pack priced in EUR must still credit the USD wallet a USD-plan\n\t\t\t// subscriber spends from — otherwise the purchase would land in a\n\t\t\t// bucket no spend path ever reads.\n\t\t\tconst creditCurrency = getWalletStatus(ctx)?.currency ?? defaultCurrency();\n\t\t\tconst chargeCurrency = normalizeCurrency(pack.currency ?? creditCurrency);\n\n\t\t\t// Reuse the subscription's provider customer when one exists (same\n\t\t\t// convention as the subscription checkout) — pack purchases then\n\t\t\t// share payment history and saved methods with the subscription.\n\t\t\tconst current = await subscriptions.get(subscriber.type, subscriber.id);\n\t\t\tconst customerId =\n\t\t\t\tcurrent?.providerCustomerId ??\n\t\t\t\t(\n\t\t\t\t\tawait config.provider.createCustomer({\n\t\t\t\t\t\temail: ctx.user!.email ?? '',\n\t\t\t\t\t\tsubscriberType: subscriber.type,\n\t\t\t\t\t\tsubscriberId: subscriber.id,\n\t\t\t\t\t\tuserId: ctx.user!.id,\n\t\t\t\t\t})\n\t\t\t\t).customerId;\n\n\t\t\tconst session = await config.provider.createPaymentCheckoutSession({\n\t\t\t\tcustomerId,\n\t\t\t\tamount: pack.priceAmount,\n\t\t\t\tcurrency: chargeCurrency,\n\t\t\t\tname: pack.name,\n\t\t\t\t...(pack.priceId ? { priceId: pack.priceId } : {}),\n\t\t\t\tmetadata: {\n\t\t\t\t\tsubscriberType: subscriber.type,\n\t\t\t\t\tsubscriberId: subscriber.id,\n\t\t\t\t\tpackId: pack.id,\n\t\t\t\t\tcredits: pack.credits.toString(),\n\t\t\t\t\tcurrency: creditCurrency,\n\t\t\t\t},\n\t\t\t\tsuccessUrl: config.successUrl,\n\t\t\t\tcancelUrl: config.cancelUrl,\n\t\t\t});\n\n\t\t\treturn setApiResponse(HTTP.OK, 'CHECKOUT_URL', 'Checkout session created.', {\n\t\t\t\turl: session.url,\n\t\t\t\tsessionId: session.sessionId,\n\t\t\t});\n\t\t},\n\n\t\t// Admin-token-guarded manual grant (support/ops). Body is validated and\n\t\t// transformed by grantWalletSchema — amount arrives as a bigint.\n\t\tasync grant(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst body = ctx.meta['body'] as {\n\t\t\t\tsubscriberType: SubscriberType;\n\t\t\t\tsubscriberId: string;\n\t\t\t\tamount: bigint;\n\t\t\t\tcurrency?: string;\n\t\t\t\tdescription?: string;\n\t\t\t\tidempotencyKey: string;\n\t\t\t};\n\n\t\t\tconst currency = body.currency ? normalizeCurrency(body.currency) : defaultCurrency();\n\t\t\ttry {\n\t\t\t\tconst result = await wallet.credit({\n\t\t\t\t\tsubscriberType: body.subscriberType,\n\t\t\t\t\tsubscriberId: body.subscriberId,\n\t\t\t\t\tcurrency,\n\t\t\t\t\tamount: body.amount,\n\t\t\t\t\ttype: 'grant',\n\t\t\t\t\tdescription: body.description ?? 'Manual grant',\n\t\t\t\t\tidempotencyKey: body.idempotencyKey,\n\t\t\t\t});\n\t\t\t\treturn setApiResponse(HTTP.OK, 'WALLET_GRANTED', 'Credits granted.', {\n\t\t\t\t\tbalance: result.balance.toString(),\n\t\t\t\t\tcurrency,\n\t\t\t\t\tduplicate: result.duplicate,\n\t\t\t\t});\n\t\t\t} catch (err) {\n\t\t\t\tif (err instanceof DuplicateTransactionError) {\n\t\t\t\t\treturn setApiResponse(HTTP.CONFLICT, 'DUPLICATE_TRANSACTION', err.message);\n\t\t\t\t}\n\t\t\t\tthrow err;\n\t\t\t}\n\t\t},\n\t};\n}\n","// Typed wallet domain errors, mapped to HTTP explicitly where they surface\n// (same catch-specific-else-rethrow shape as store's VersionConflictError):\n// DuplicateTransactionError → 409 in the grant and payment-webhook handlers;\n// InsufficientFundsError → 402 in product routes that debit. Note that\n// requireWalletBalance answers its 402 from a pre-check without throwing, so\n// a route that debits inside its unit of work should catch this error and\n// reply with insufficientCreditsResponse() — the pre-check alone cannot\n// reserve funds against a concurrent drain.\n\nexport class InsufficientFundsError extends Error {\n\tconstructor(\n\t\treadonly available: bigint,\n\t\treadonly required: bigint,\n\t\treadonly currency: string,\n\t) {\n\t\tsuper(\n\t\t\t`[billing:wallet] insufficient funds: available ${available}, required ${required} ${currency}`,\n\t\t);\n\t\tthis.name = 'InsufficientFundsError';\n\t}\n}\n\n// An idempotency key was replayed against a DIFFERENT subscriber or currency\n// than the ledger row it originally wrote — a caller bug (key reuse across\n// scopes), never a safe retry. Same-scope replays are not errors: mutations\n// return `{ duplicate: true }` for those.\nexport class DuplicateTransactionError extends Error {\n\tconstructor(readonly idempotencyKey: string) {\n\t\tsuper(\n\t\t\t`[billing:wallet] idempotency key '${idempotencyKey}' was already used for a different subscriber or currency`,\n\t\t);\n\t\tthis.name = 'DuplicateTransactionError';\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type {\n\tIWalletBalance,\n\tIWalletLedgerEntry,\n\tIWalletRate,\n\tSubscriberType,\n\tWalletLedgerType,\n} from '../types';\nimport type { IBillingConfig, IBillingPlan } from '../config';\nimport { DuplicateTransactionError, InsufficientFundsError } from '../errors';\nimport { normalizeCurrency } from '../utils';\n\n// The ledger is the source of truth; fonderie_wallet_balances is a cache that\n// is NEVER written without a ledger row in the same transaction. Every\n// mutation carries an idempotency key backed by the ledger's UNIQUE\n// constraint, so a replayed request re-reads instead of re-applying: the\n// pre-check catches replays cheaply, and a lost race between two identical\n// replays still resolves safely — the second ledger INSERT violates the\n// constraint and rolls its balance write back with it.\n//\n// CONTRACT: pass the module-level store, never a tx-scoped adapter from an\n// enclosing store.transaction. The wallet manages its own transaction; the\n// pg adapter flattens nested transactions WITHOUT savepoints, so inside a\n// caller's transaction the rollback-on-conflict guarantee above would not\n// hold (and a conflict would poison the caller's whole transaction).\n\nexport interface IWalletSubscriber {\n\tsubscriberType: SubscriberType;\n\tsubscriberId: string;\n\tcurrency: string;\n}\n\nexport interface IWalletMutationResult {\n\tbalance: bigint;\n\t// True when the idempotency key had already been applied — the wallet was\n\t// left untouched and `balance` is the current value.\n\tduplicate: boolean;\n}\n\ninterface ILedgerKeyRow {\n\tsubscriberType: SubscriberType;\n\tsubscriberId: string;\n\tcurrency: string;\n}\n\ninterface IBalanceRow {\n\tamount: string;\n\tversion: string;\n\tupdatedAt: string | Date | null;\n}\n\nconst UNIQUE_VIOLATION = '23505';\n\n// Only the ledger's idempotency-key UNIQUE violation counts as a safe\n// replay. Anything else (NOT NULL violations, other constraints) must\n// surface as the error it is — a loose message match here once turned a\n// rolled-back failure into a fake duplicate-success.\nfunction isIdempotencyConflict(err: unknown): boolean {\n\tconst e = err as { code?: string; constraint?: string; message?: string };\n\tif (e?.code !== UNIQUE_VIOLATION) return false;\n\tif (typeof e.constraint === 'string') return e.constraint.includes('idempotency_key');\n\treturn typeof e.message === 'string' && e.message.includes('idempotency_key');\n}\n\n// Returns the existing ledger row for the key, or null. Throws when the key\n// exists but belongs to a different subscriber/currency — key reuse across\n// scopes is a caller bug, not a safe replay.\nasync function findByIdempotencyKey(\n\tsub: IWalletSubscriber,\n\tidempotencyKey: string,\n\tstore: IStoreAdapter,\n): Promise<ILedgerKeyRow | null> {\n\tconst [row] = await store.query<ILedgerKeyRow>(\n\t\t`SELECT\n\t\t\tsubscriber_type AS \"subscriberType\",\n\t\t\tsubscriber_id AS \"subscriberId\",\n\t\t\tcurrency\n\t\tFROM fonderie_wallet_ledger\n\t\tWHERE idempotency_key = $1`,\n\t\t[idempotencyKey],\n\t);\n\tif (!row) return null;\n\tif (\n\t\trow.subscriberType !== sub.subscriberType ||\n\t\trow.subscriberId !== sub.subscriberId ||\n\t\trow.currency !== sub.currency\n\t) {\n\t\tthrow new DuplicateTransactionError(idempotencyKey);\n\t}\n\treturn row;\n}\n\nasync function readBalance(sub: IWalletSubscriber, store: IStoreAdapter): Promise<bigint> {\n\tconst [row] = await store.query<{ amount: string }>(\n\t\t`SELECT amount FROM fonderie_wallet_balances\n\t\tWHERE subscriber_type = $1 AND subscriber_id = $2 AND currency = $3`,\n\t\t[sub.subscriberType, sub.subscriberId, sub.currency],\n\t);\n\treturn BigInt(row?.amount ?? '0');\n}\n\n// Atomic upsert-add on the balance cache. tx-scoped: callers pair it with a\n// ledger row in the same transaction, never alone.\nasync function applyBalanceCredit(\n\ttx: IStoreAdapter,\n\tsub: IWalletSubscriber,\n\tamount: bigint,\n): Promise<bigint> {\n\tconst [row] = await tx.query<{ amount: string }>(\n\t\t`INSERT INTO fonderie_wallet_balances (subscriber_type, subscriber_id, currency, amount)\n\t\tVALUES ($1, $2, $3, $4)\n\t\tON CONFLICT (subscriber_type, subscriber_id, currency) DO UPDATE SET\n\t\t\tamount = fonderie_wallet_balances.amount + EXCLUDED.amount,\n\t\t\tversion = fonderie_wallet_balances.version + 1,\n\t\t\tupdated_at = now()\n\t\tRETURNING amount`,\n\t\t[sub.subscriberType, sub.subscriberId, sub.currency, amount.toString()],\n\t);\n\treturn BigInt(row?.amount ?? '0');\n}\n\nasync function insertLedgerRow(\n\ttx: IStoreAdapter,\n\tsub: IWalletSubscriber,\n\topts: {\n\t\ttype: WalletLedgerType;\n\t\tamount: bigint; // signed\n\t\tbalanceAfter: bigint;\n\t\tidempotencyKey: string;\n\t\tdescription: string | null;\n\t\tmetadata: Record<string, unknown>;\n\t\tproviderTxId: string | null;\n\t},\n): Promise<void> {\n\tawait tx.query(\n\t\t`INSERT INTO fonderie_wallet_ledger\n\t\t\t(subscriber_type, subscriber_id, currency, type, amount, balance_after,\n\t\t\t description, idempotency_key, metadata, provider_tx_id)\n\t\tVALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10)`,\n\t\t[\n\t\t\tsub.subscriberType,\n\t\t\tsub.subscriberId,\n\t\t\tsub.currency,\n\t\t\topts.type,\n\t\t\topts.amount.toString(),\n\t\t\topts.balanceAfter.toString(),\n\t\t\topts.description,\n\t\t\topts.idempotencyKey,\n\t\t\tJSON.stringify(opts.metadata),\n\t\t\topts.providerTxId,\n\t\t],\n\t);\n}\n\n// Add credits. Idempotent: a replayed key returns the current balance with\n// duplicate: true. The balance upsert-add is a single atomic statement, so\n// credits need no row lock; the same-transaction ledger row (with its UNIQUE\n// key) is what makes a concurrent identical replay roll back cleanly.\n// Pass the module-level store — never a tx-scoped adapter (see file header).\nexport async function creditWallet(\n\topts: IWalletSubscriber & {\n\t\tamount: bigint; // positive\n\t\tidempotencyKey: string;\n\t\ttype?: WalletLedgerType;\n\t\tdescription?: string;\n\t\tmetadata?: Record<string, unknown>;\n\t\tproviderTxId?: string;\n\t},\n\tstore: IStoreAdapter,\n): Promise<IWalletMutationResult> {\n\tif (opts.amount < 0n) throw new Error('[billing:wallet] credit amount must be positive');\n\tif (!opts.idempotencyKey) throw new Error('[billing:wallet] idempotencyKey is required');\n\tif (opts.amount === 0n) {\n\t\treturn { balance: await readBalance(opts, store), duplicate: false };\n\t}\n\n\ttry {\n\t\treturn await store.transaction(async (tx) => {\n\t\t\tconst existing = await findByIdempotencyKey(opts, opts.idempotencyKey, tx);\n\t\t\tif (existing) return { balance: await readBalance(opts, tx), duplicate: true };\n\n\t\t\tconst balance = await applyBalanceCredit(tx, opts, opts.amount);\n\n\t\t\tawait insertLedgerRow(tx, opts, {\n\t\t\t\ttype: opts.type ?? 'adjustment',\n\t\t\t\tamount: opts.amount,\n\t\t\t\tbalanceAfter: balance,\n\t\t\t\tidempotencyKey: opts.idempotencyKey,\n\t\t\t\tdescription: opts.description ?? null,\n\t\t\t\tmetadata: opts.metadata ?? {},\n\t\t\t\tproviderTxId: opts.providerTxId ?? null,\n\t\t\t});\n\n\t\t\treturn { balance, duplicate: false };\n\t\t});\n\t} catch (err) {\n\t\t// Lost a race against an identical replay: its ledger row landed first,\n\t\t// ours violated the UNIQUE key and the whole transaction rolled back.\n\t\tif (isIdempotencyConflict(err)) {\n\t\t\treturn { balance: await readBalance(opts, store), duplicate: true };\n\t\t}\n\t\tthrow err;\n\t}\n}\n\n// Atomically deduct credits. Two independent guarantees prevent double-spend:\n// the SELECT ... FOR UPDATE serializes concurrent debits for one subscriber,\n// and the conditional UPDATE (amount - cost >= floor) re-checks the floor in\n// the same statement — so even a backend without row locks cannot go below\n// the overdraft floor. Throws InsufficientFundsError past the floor.\n// Pass the module-level store — never a tx-scoped adapter (see file header).\nexport async function debitWallet(\n\topts: IWalletSubscriber & {\n\t\tamount: bigint; // positive; recorded as negative in the ledger\n\t\tidempotencyKey: string;\n\t\ttype?: WalletLedgerType;\n\t\toverdraftLimit?: bigint; // >= 0; how far below zero the balance may go\n\t\tdescription?: string;\n\t\tmetadata?: Record<string, unknown>;\n\t},\n\tstore: IStoreAdapter,\n): Promise<IWalletMutationResult> {\n\tif (opts.amount < 0n) throw new Error('[billing:wallet] debit amount must be positive');\n\tif (!opts.idempotencyKey) throw new Error('[billing:wallet] idempotencyKey is required');\n\tif (opts.amount === 0n) {\n\t\t// Zero-cost debit (e.g. unlimited plan rate) — no ledger row, no-op.\n\t\treturn { balance: await readBalance(opts, store), duplicate: false };\n\t}\n\tconst floor = -(opts.overdraftLimit ?? 0n);\n\n\ttry {\n\t\treturn await store.transaction(async (tx) => {\n\t\t\tconst existing = await findByIdempotencyKey(opts, opts.idempotencyKey, tx);\n\t\t\tif (existing) return { balance: await readBalance(opts, tx), duplicate: true };\n\n\t\t\t// Make sure the row exists so FOR UPDATE has something to lock, then\n\t\t\t// lock it — concurrent debits for this subscriber serialize here.\n\t\t\tawait tx.query(\n\t\t\t\t`INSERT INTO fonderie_wallet_balances (subscriber_type, subscriber_id, currency, amount)\n\t\t\t\tVALUES ($1, $2, $3, 0)\n\t\t\t\tON CONFLICT (subscriber_type, subscriber_id, currency) DO NOTHING`,\n\t\t\t\t[opts.subscriberType, opts.subscriberId, opts.currency],\n\t\t\t);\n\t\t\tconst [locked] = await tx.query<{ amount: string }>(\n\t\t\t\t`SELECT amount FROM fonderie_wallet_balances\n\t\t\t\tWHERE subscriber_type = $1 AND subscriber_id = $2 AND currency = $3\n\t\t\t\tFOR UPDATE`,\n\t\t\t\t[opts.subscriberType, opts.subscriberId, opts.currency],\n\t\t\t);\n\t\t\tconst current = BigInt(locked?.amount ?? '0');\n\n\t\t\tif (current - opts.amount < floor) {\n\t\t\t\tthrow new InsufficientFundsError(current, opts.amount, opts.currency);\n\t\t\t}\n\n\t\t\tconst [updated] = await tx.query<{ amount: string }>(\n\t\t\t\t`UPDATE fonderie_wallet_balances\n\t\t\t\tSET amount = amount - $4, version = version + 1, updated_at = now()\n\t\t\t\tWHERE subscriber_type = $1 AND subscriber_id = $2 AND currency = $3\n\t\t\t\t\tAND amount - $4 >= $5\n\t\t\t\tRETURNING amount`,\n\t\t\t\t[\n\t\t\t\t\topts.subscriberType,\n\t\t\t\t\topts.subscriberId,\n\t\t\t\t\topts.currency,\n\t\t\t\t\topts.amount.toString(),\n\t\t\t\t\tfloor.toString(),\n\t\t\t\t],\n\t\t\t);\n\t\t\t// Belt and braces: with row locking this cannot miss after the check\n\t\t\t// above; without it, this is the statement that holds the floor.\n\t\t\tif (!updated) {\n\t\t\t\tthrow new InsufficientFundsError(current, opts.amount, opts.currency);\n\t\t\t}\n\t\t\tconst balance = BigInt(updated.amount);\n\n\t\t\tawait insertLedgerRow(tx, opts, {\n\t\t\t\ttype: opts.type ?? 'usage',\n\t\t\t\tamount: -opts.amount,\n\t\t\t\tbalanceAfter: balance,\n\t\t\t\tidempotencyKey: opts.idempotencyKey,\n\t\t\t\tdescription: opts.description ?? null,\n\t\t\t\tmetadata: opts.metadata ?? {},\n\t\t\t\tproviderTxId: null,\n\t\t\t});\n\n\t\t\treturn { balance, duplicate: false };\n\t\t});\n\t} catch (err) {\n\t\tif (isIdempotencyConflict(err)) {\n\t\t\treturn { balance: await readBalance(opts, store), duplicate: true };\n\t\t}\n\t\tthrow err;\n\t}\n}\n\nexport async function getWalletBalance(\n\tsub: IWalletSubscriber,\n\tstore: IStoreAdapter,\n): Promise<IWalletBalance> {\n\tconst [row] = await store.query<IBalanceRow>(\n\t\t`SELECT amount, version, updated_at AS \"updatedAt\"\n\t\tFROM fonderie_wallet_balances\n\t\tWHERE subscriber_type = $1 AND subscriber_id = $2 AND currency = $3`,\n\t\t[sub.subscriberType, sub.subscriberId, sub.currency],\n\t);\n\tif (!row) return { balance: 0n, version: 0, updatedAt: null };\n\treturn {\n\t\tbalance: BigInt(row.amount),\n\t\tversion: Number(row.version),\n\t\tupdatedAt: row.updatedAt ? new Date(row.updatedAt).toISOString() : null,\n\t};\n}\n\nexport interface IWalletLedgerPage {\n\tentries: IWalletLedgerEntry[];\n\t// Opaque cursor for the next (older) page, or null when exhausted.\n\tnextCursor: string | null;\n}\n\nexport function encodeLedgerCursor(createdAt: string, id: string): string {\n\treturn Buffer.from(JSON.stringify([createdAt, id])).toString('base64url');\n}\n\n// Accepts ISO timestamps and Postgres' own text format (microsecond\n// precision, e.g. '2026-09-04 18:50:50.888123+00') — the cursor carries the\n// latter to avoid the JS Date millisecond truncation that would skip\n// same-millisecond ledger rows between pages.\nconst CURSOR_TS_RE = /^\\d{4}-\\d{2}-\\d{2}[T ]\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,6})?(Z|[+-]\\d{2}(:?\\d{2})?)?$/;\nconst CURSOR_ID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;\n\nexport function decodeLedgerCursor(cursor: string): { createdAt: string; id: string } | null {\n\tif (cursor.length > 256) return null;\n\ttry {\n\t\tconst parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));\n\t\tif (!Array.isArray(parsed) || typeof parsed[0] !== 'string' || typeof parsed[1] !== 'string') {\n\t\t\treturn null;\n\t\t}\n\t\t// Both halves feed ::timestamptz / ::uuid casts — validate here so a\n\t\t// crafted cursor yields a 422, not a Postgres cast error.\n\t\tif (!CURSOR_TS_RE.test(parsed[0]) || !CURSOR_ID_RE.test(parsed[1])) return null;\n\t\treturn { createdAt: parsed[0], id: parsed[1] };\n\t} catch {\n\t\treturn null;\n\t}\n}\n\ninterface ILedgerRow {\n\tid: string;\n\tsubscriberType: SubscriberType;\n\tsubscriberId: string;\n\tcurrency: string;\n\ttype: WalletLedgerType;\n\tamount: string;\n\tbalanceAfter: string;\n\tdescription: string | null;\n\tidempotencyKey: string;\n\tmetadata: Record<string, unknown> | null;\n\tproviderTxId: string | null;\n\tcreatedAt: string | Date;\n\t// created_at::text — full microsecond precision for the keyset cursor\n\t// (node-pg parses timestamptz into a millisecond Date, which would make\n\t// the cursor skip rows sharing a truncated millisecond).\n\tcreatedAtRaw: string;\n}\n\nexport async function getWalletLedger(\n\topts: IWalletSubscriber & {\n\t\tlimit?: number; // 1..100, default 50\n\t\tcursor?: { createdAt: string; id: string };\n\t},\n\tstore: IStoreAdapter,\n): Promise<IWalletLedgerPage> {\n\tconst limit = Math.min(Math.max(opts.limit ?? 50, 1), 100);\n\n\tconst params: unknown[] = [opts.subscriberType, opts.subscriberId, opts.currency];\n\tlet cursorClause = '';\n\tif (opts.cursor) {\n\t\tparams.push(opts.cursor.createdAt, opts.cursor.id);\n\t\tcursorClause = `AND (created_at, id) < ($4::timestamptz, $5::uuid)`;\n\t}\n\tparams.push(limit + 1);\n\n\tconst rows = await store.query<ILedgerRow>(\n\t\t`SELECT\n\t\t\tid,\n\t\t\tsubscriber_type AS \"subscriberType\",\n\t\t\tsubscriber_id AS \"subscriberId\",\n\t\t\tcurrency,\n\t\t\ttype,\n\t\t\tamount,\n\t\t\tbalance_after AS \"balanceAfter\",\n\t\t\tdescription,\n\t\t\tidempotency_key AS \"idempotencyKey\",\n\t\t\tmetadata,\n\t\t\tprovider_tx_id AS \"providerTxId\",\n\t\t\tcreated_at AS \"createdAt\",\n\t\t\tcreated_at::text AS \"createdAtRaw\"\n\t\tFROM fonderie_wallet_ledger\n\t\tWHERE subscriber_type = $1 AND subscriber_id = $2 AND currency = $3\n\t\t\t${cursorClause}\n\t\tORDER BY created_at DESC, id DESC\n\t\tLIMIT $${params.length}`,\n\t\tparams,\n\t);\n\n\tconst page = rows.slice(0, limit);\n\tconst entries: IWalletLedgerEntry[] = page.map((r) => ({\n\t\tid: r.id,\n\t\tsubscriberType: r.subscriberType,\n\t\tsubscriberId: r.subscriberId,\n\t\tcurrency: r.currency,\n\t\ttype: r.type,\n\t\tamount: BigInt(r.amount),\n\t\tbalanceAfter: BigInt(r.balanceAfter),\n\t\tdescription: r.description,\n\t\tidempotencyKey: r.idempotencyKey,\n\t\tmetadata: r.metadata ?? {},\n\t\tproviderTxId: r.providerTxId,\n\t\tcreatedAt: new Date(r.createdAt).toISOString(),\n\t}));\n\n\tconst lastRow = page[page.length - 1];\n\tconst nextCursor =\n\t\trows.length > limit && lastRow ? encodeLedgerCursor(lastRow.createdAtRaw, lastRow.id) : null;\n\treturn { entries, nextCursor };\n}\n\n// A plan's wallet economics with every default applied. Null when the wallet\n// subsystem is off (no config.wallet) or the plan defines no wallet.\nexport interface IResolvedPlanWallet {\n\tcurrency: string;\n\tprecision: number;\n\toverdraftLimit: bigint;\n\tgrantAmount: bigint | null;\n\tgrantPeriod: 'month' | 'week' | 'day';\n\trates: Record<string, IWalletRate>;\n}\n\nexport function resolvePlanWallet(\n\tplan: IBillingPlan,\n\tconfig: IBillingConfig,\n): IResolvedPlanWallet | null {\n\tif (!config.wallet || !plan.wallet) return null;\n\treturn {\n\t\tcurrency: normalizeCurrency(plan.wallet.currency ?? config.wallet.currency ?? 'USD'),\n\t\tprecision: plan.wallet.precision ?? config.wallet.precision ?? 2,\n\t\toverdraftLimit: plan.wallet.overdraftLimit ?? 0n,\n\t\tgrantAmount: plan.wallet.grantAmount ?? null,\n\t\tgrantPeriod: plan.wallet.grantPeriod ?? 'month',\n\t\trates: plan.wallet.rates ?? {},\n\t};\n}\n\n// UTC period key for periodic grants: '2026-09' (month), '2026-09-04' (day),\n// '2026-W36' (ISO week — note the ISO week-numbering year at boundaries).\nexport function currentGrantPeriod(period: 'month' | 'week' | 'day', now = new Date()): string {\n\tconst y = now.getUTCFullYear();\n\tconst m = String(now.getUTCMonth() + 1).padStart(2, '0');\n\tconst d = String(now.getUTCDate()).padStart(2, '0');\n\tif (period === 'month') return `${y}-${m}`;\n\tif (period === 'day') return `${y}-${m}-${d}`;\n\t// ISO week: shift to the Thursday of the current week, whose year is the\n\t// ISO week-numbering year; week 1 contains January 4th.\n\tconst thursday = new Date(Date.UTC(y, now.getUTCMonth(), now.getUTCDate()));\n\tthursday.setUTCDate(thursday.getUTCDate() + 4 - (thursday.getUTCDay() || 7));\n\tconst isoYear = thursday.getUTCFullYear();\n\tconst jan4 = new Date(Date.UTC(isoYear, 0, 4));\n\tjan4.setUTCDate(jan4.getUTCDate() + 4 - (jan4.getUTCDay() || 7));\n\tconst week = 1 + Math.round((thursday.getTime() - jan4.getTime()) / (7 * 86_400_000));\n\treturn `${isoYear}-W${String(week).padStart(2, '0')}`;\n}\n\nexport interface IGrantResult {\n\tgranted: boolean; // false when this period's grant was already applied\n\tbalance: bigint | null; // new balance when granted, null otherwise\n}\n\n// Apply a periodic grant exactly once per (subscriber, currency, period).\n// The grant marker and the credit commit in ONE transaction, so a crash\n// between them cannot mark a period as granted without crediting it.\nexport async function ensurePeriodicGrant(\n\topts: IWalletSubscriber & {\n\t\tamount: bigint; // positive\n\t\tperiod: string; // from currentGrantPeriod()\n\t\tdescription?: string;\n\t},\n\tstore: IStoreAdapter,\n): Promise<IGrantResult> {\n\tif (opts.amount <= 0n) return { granted: false, balance: null };\n\n\t// Fast path: one indexed read per request once the period is granted.\n\tconst [seen] = await store.query<{ period: string }>(\n\t\t`SELECT period FROM fonderie_wallet_grants\n\t\tWHERE subscriber_type = $1 AND subscriber_id = $2 AND currency = $3 AND period = $4`,\n\t\t[opts.subscriberType, opts.subscriberId, opts.currency, opts.period],\n\t);\n\tif (seen) return { granted: false, balance: null };\n\n\ttry {\n\t\treturn await store.transaction(async (tx) => {\n\t\t\tconst [marked] = await tx.query<{ period: string }>(\n\t\t\t\t`INSERT INTO fonderie_wallet_grants (subscriber_type, subscriber_id, currency, period, amount)\n\t\t\t\tVALUES ($1, $2, $3, $4, $5)\n\t\t\t\tON CONFLICT (subscriber_type, subscriber_id, currency, period) DO NOTHING\n\t\t\t\tRETURNING period`,\n\t\t\t\t[\n\t\t\t\t\topts.subscriberType,\n\t\t\t\t\topts.subscriberId,\n\t\t\t\t\topts.currency,\n\t\t\t\t\topts.period,\n\t\t\t\t\topts.amount.toString(),\n\t\t\t\t],\n\t\t\t);\n\t\t\t// Lost the race — another request granted this period first.\n\t\t\tif (!marked) return { granted: false, balance: null };\n\n\t\t\tconst balance = await applyBalanceCredit(tx, opts, opts.amount);\n\n\t\t\tawait insertLedgerRow(tx, opts, {\n\t\t\t\ttype: 'grant',\n\t\t\t\tamount: opts.amount,\n\t\t\t\tbalanceAfter: balance,\n\t\t\t\tidempotencyKey: `grant:${opts.subscriberType}:${opts.subscriberId}:${opts.currency}:${opts.period}`,\n\t\t\t\tdescription: opts.description ?? `Periodic grant ${opts.period}`,\n\t\t\t\tmetadata: { period: opts.period },\n\t\t\t\tproviderTxId: null,\n\t\t\t});\n\n\t\t\treturn { granted: true, balance };\n\t\t});\n\t} catch (err) {\n\t\tif (isIdempotencyConflict(err)) return { granted: false, balance: null };\n\t\tthrow err;\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IWalletBalance } from '../types';\nimport {\n\tcreditWallet,\n\tdebitWallet,\n\tensurePeriodicGrant,\n\tgetWalletBalance,\n\tgetWalletLedger,\n} from '../services/wallet';\nimport type { IGrantResult, IWalletLedgerPage, IWalletMutationResult } from '../services/wallet';\n\nexport class WalletModel {\n\tconstructor(private readonly store: IStoreAdapter) {}\n\n\tcredit(opts: Parameters<typeof creditWallet>[0]): Promise<IWalletMutationResult> {\n\t\treturn creditWallet(opts, this.store);\n\t}\n\n\tdebit(opts: Parameters<typeof debitWallet>[0]): Promise<IWalletMutationResult> {\n\t\treturn debitWallet(opts, this.store);\n\t}\n\n\tbalance(sub: Parameters<typeof getWalletBalance>[0]): Promise<IWalletBalance> {\n\t\treturn getWalletBalance(sub, this.store);\n\t}\n\n\tledger(opts: Parameters<typeof getWalletLedger>[0]): Promise<IWalletLedgerPage> {\n\t\treturn getWalletLedger(opts, this.store);\n\t}\n\n\tensureGrant(opts: Parameters<typeof ensurePeriodicGrant>[0]): Promise<IGrantResult> {\n\t\treturn ensurePeriodicGrant(opts, this.store);\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IBillingConfig, IBillingCreditPack } from '../config';\nimport { normalizeCurrency } from '../utils';\n\n// Credit packs mirror the plans pattern: config is the runtime source of\n// truth (checkout reads it directly); the DB copy exists for ops visibility\n// and reporting, upserted at boot.\n\nexport function findCreditPack(packId: string, config: IBillingConfig): IBillingCreditPack | null {\n\tconst pack = config.wallet?.creditPacks?.find((p) => p.id === packId);\n\tif (!pack || pack.active === false) return null;\n\treturn pack;\n}\n\nexport async function syncCreditPacksToDB(\n\tconfig: IBillingConfig,\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tconst packs = config.wallet?.creditPacks ?? [];\n\tif (packs.length === 0) return;\n\n\tconst defaultCurrency = normalizeCurrency(config.wallet?.currency ?? 'USD');\n\tconst values = packs.map((_, i) => {\n\t\tconst b = i * 8;\n\t\treturn `($${b + 1}, $${b + 2}, $${b + 3}, $${b + 4}, $${b + 5}, $${b + 6}, $${b + 7}, $${b + 8}::jsonb)`;\n\t});\n\n\tconst params = packs.flatMap((pack) => [\n\t\tpack.id,\n\t\tpack.name,\n\t\tnormalizeCurrency(pack.currency ?? defaultCurrency),\n\t\tpack.credits.toString(),\n\t\tpack.priceAmount.toString(),\n\t\tpack.priceId ?? null,\n\t\tpack.active !== false,\n\t\tJSON.stringify(pack.metadata ?? {}),\n\t]);\n\n\tawait store.query(\n\t\t`INSERT INTO fonderie_credit_packs\n\t\t\t(id, name, currency, credits, price_amount, price_id, active, metadata)\n\t\tVALUES ${values.join(', ')}\n\t\tON CONFLICT (id) DO UPDATE SET\n\t\t\tname = EXCLUDED.name,\n\t\t\tcurrency = EXCLUDED.currency,\n\t\t\tcredits = EXCLUDED.credits,\n\t\t\tprice_amount = EXCLUDED.price_amount,\n\t\t\tprice_id = EXCLUDED.price_id,\n\t\t\tactive = EXCLUDED.active,\n\t\t\tmetadata = EXCLUDED.metadata`,\n\t\tparams,\n\t);\n}\n","import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { Middleware } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\nimport type { IBillingContext, IPolicyStatus, IWalletContext } from './types';\nimport type { InsufficientFundsError } from './errors';\nimport { debitWallet } from './services/wallet';\nimport type { IWalletMutationResult } from './services/wallet';\n\nfunction getBillingContext(ctx: IFonderieContext): IBillingContext | null {\n\treturn (ctx.meta['billing'] as IBillingContext | undefined) ?? null;\n}\n\n// Returns true if the feature flag is enabled on the subscriber's plan.\n// Returns true when no billing context is present (fail-open when billing not configured).\nexport function hasFeature(ctx: IFonderieContext, key: string): boolean {\n\tconst billing = getBillingContext(ctx);\n\tif (!billing) return true;\n\n\tconst status = billing.statuses[key];\n\tif (!status) return true; // key not declared in policy → allow\n\tif (status.type === 'feature') return status.enabled;\n\treturn true; // counter entry = feature present\n}\n\n// Returns the advertised limit for a counter policy key, or null if unlimited / not configured.\nexport function getPlanLimit(ctx: IFonderieContext, key: string): number | null {\n\tconst billing = getBillingContext(ctx);\n\tif (!billing) return null;\n\n\tconst status = billing.statuses[key];\n\tif (!status || status.type === 'feature') return null;\n\treturn status.limit;\n}\n\n// Returns the full policy status for a key, or null if not configured.\nexport function getLimitStatus(ctx: IFonderieContext, key: string): IPolicyStatus | null {\n\tconst billing = getBillingContext(ctx);\n\tif (!billing) return null;\n\treturn billing.statuses[key] ?? null;\n}\n\n// Returns the wallet snapshot cached by withBilling, or null when the wallet\n// subsystem is off or the subscriber's plan defines no wallet.\nexport function getWalletStatus(ctx: IFonderieContext): IWalletContext | null {\n\treturn getBillingContext(ctx)?.wallet ?? null;\n}\n\n// The plan's unit cost for a metric, or null when the metric is not priced\n// (no wallet, or no rate for the key) — null means \"no charge\".\nexport function getWalletRate(ctx: IFonderieContext, metric: string): bigint | null {\n\treturn getWalletStatus(ctx)?.rates[metric]?.cost ?? null;\n}\n\n// Middleware — checks (does NOT debit) that the subscriber can afford one\n// unit of `metric` at the plan's rate. The actual deduction must happen\n// atomically in the unit of work via debitWallet/debitWalletForMetric — a\n// middleware-time debit would charge for requests that later fail, and a\n// middleware-time check alone can never reserve funds. Fails open when the\n// wallet or the rate is not configured, like requireFeature.\nexport function requireWalletBalance(metric: string): Middleware {\n\treturn (ctx, next) => {\n\t\tconst wallet = getWalletStatus(ctx);\n\t\tconst cost = wallet?.rates[metric]?.cost;\n\t\tif (!wallet || cost === undefined || cost === 0n) return next();\n\n\t\tif (wallet.balance - cost < -wallet.overdraftLimit) {\n\t\t\treturn Promise.resolve(\n\t\t\t\tsetApiResponse(HTTP.PAYMENT_REQUIRED, 'INSUFFICIENT_CREDITS', 'Insufficient credits', {\n\t\t\t\t\tmetric,\n\t\t\t\t\tcost: cost.toString(),\n\t\t\t\t\tbalance: wallet.balance.toString(),\n\t\t\t\t\tcurrency: wallet.currency,\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\treturn next();\n\t};\n}\n\n// Debit the caller's wallet at the plan rate for `metric` — the deduction\n// product code calls inside its unit of work, with an idempotency key derived\n// from that unit (e.g. a task id) so retries never double-charge. Returns\n// null (charging nothing) when the wallet or the rate is not configured or\n// the rate is zero (e.g. unlimited plans). Throws InsufficientFundsError past\n// the plan's overdraft floor.\nexport async function debitWalletForMetric(\n\tctx: IFonderieContext,\n\tmetric: string,\n\topts: {\n\t\tidempotencyKey: string;\n\t\tquantity?: number;\n\t\tdescription?: string;\n\t\tmetadata?: Record<string, unknown>;\n\t},\n\tstore: IStoreAdapter,\n): Promise<IWalletMutationResult | null> {\n\tconst quantity = opts.quantity ?? 1;\n\tif (!Number.isInteger(quantity) || quantity <= 0) {\n\t\tthrow new Error('[billing:wallet] quantity must be a positive integer');\n\t}\n\n\tconst billing = getBillingContext(ctx);\n\tconst wallet = billing?.wallet;\n\tconst cost = wallet?.rates[metric]?.cost;\n\tif (!billing || !wallet || cost === undefined || cost === 0n) return null;\n\n\treturn debitWallet(\n\t\t{\n\t\t\tsubscriberType: billing.subscriber.type,\n\t\t\tsubscriberId: billing.subscriber.id,\n\t\t\tcurrency: wallet.currency,\n\t\t\tamount: cost * BigInt(quantity),\n\t\t\toverdraftLimit: wallet.overdraftLimit,\n\t\t\tidempotencyKey: opts.idempotencyKey,\n\t\t\tdescription: opts.description ?? metric,\n\t\t\tmetadata: { metric, quantity, ...(opts.metadata ?? {}) },\n\t\t},\n\t\tstore,\n\t);\n}\n\n// The 402 a product route should return when a debit loses the race between\n// requireWalletBalance's snapshot and the actual deduction — catch\n// InsufficientFundsError around debitWallet/debitWalletForMetric and reply\n// with this (same shape as requireWalletBalance's own rejection).\nexport function insufficientCreditsResponse(err: InsufficientFundsError, metric?: string): Response {\n\treturn setApiResponse(HTTP.PAYMENT_REQUIRED, 'INSUFFICIENT_CREDITS', 'Insufficient credits', {\n\t\t...(metric ? { metric } : {}),\n\t\tcost: err.required.toString(),\n\t\tbalance: err.available.toString(),\n\t\tcurrency: err.currency,\n\t});\n}\n\n// Middleware — gates a route behind a feature flag.\n// Reads from cached ctx.meta['billing']; no store arg, no async DB call.\n// Fails open if billing context is absent (billing module not registered).\nexport function requireFeature(key: string): Middleware {\n\treturn (ctx, next) => {\n\t\tif (!hasFeature(ctx, key)) {\n\t\t\treturn Promise.resolve(\n\t\t\t\tsetApiResponse(\n\t\t\t\t\tHTTP.PAYMENT_REQUIRED,\n\t\t\t\t\t'FEATURE_UNAVAILABLE',\n\t\t\t\t\t`Feature '${key}' is not available on your current plan`,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t\treturn next();\n\t};\n}\n","import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IBillingConfig } from '../config';\nimport type { PriceCache } from '../services/price-cache';\nimport { SubscriptionModel } from '../models/subscription.model';\nimport { resolvePlanNameByPrice } from '../services/plans';\nimport { readWebhookEvent } from './webhook-shared';\n\nexport function webhookController(store: IStoreAdapter, config: IBillingConfig, priceCache?: PriceCache) {\n\tconst subscriptions = new SubscriptionModel(store);\n\n\treturn {\n\t\tasync handle(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst event = await readWebhookEvent(\n\t\t\t\tctx,\n\t\t\t\tconfig.webhookSecret,\n\t\t\t\tconfig.provider,\n\t\t\t\t'Webhook secret not configured',\n\t\t\t);\n\t\t\tif (event instanceof Response) return event;\n\n\t\t\t// §8: keep the price cache honest. Invalidate on any price/product change\n\t\t\t// regardless of arrival order (invalidate-and-refetch is order-safe).\n\t\t\tif (priceCache && (event.type.startsWith('price.') || event.type.startsWith('product.'))) {\n\t\t\t\tpriceCache.invalidate();\n\t\t\t}\n\n\t\t\tif (event.subscription) {\n\t\t\t\t// A deletion resolves to the free/canceled state set by the provider;\n\t\t\t\t// otherwise map the plan from the price (dual-mapping), falling back to\n\t\t\t\t// the nickname-derived value.\n\t\t\t\tconst plan =\n\t\t\t\t\tevent.type === 'customer.subscription.deleted'\n\t\t\t\t\t\t? event.subscription.plan\n\t\t\t\t\t\t: resolvePlanNameByPrice(event.subscription, config.plans) ?? event.subscription.plan;\n\n\t\t\t\tawait subscriptions.upsert({\n\t\t\t\t\tsubscriberType: event.subscription.subscriberType,\n\t\t\t\t\tsubscriberId: event.subscription.subscriberId,\n\t\t\t\t\tplan,\n\t\t\t\t\tinterval: event.subscription.interval,\n\t\t\t\t\tstatus: event.subscription.status,\n\t\t\t\t\tproviderCustomerId: event.subscription.providerCustomerId,\n\t\t\t\t\tproviderSubscriptionId: event.subscription.providerSubscriptionId,\n\t\t\t\t\tcurrentPeriodStart: event.subscription.currentPeriodStart,\n\t\t\t\t\tcurrentPeriodEnd: event.subscription.currentPeriodEnd,\n\t\t\t\t\tcancelAtPeriodEnd: event.subscription.cancelAtPeriodEnd,\n\t\t\t\t\ttrialEndsAt: event.subscription.trialEndsAt,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn Response.json({ received: true });\n\t\t},\n\t};\n}\n","import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\n\nimport type { IBillingEvent, IBillingProvider } from '../providers/types';\n\n// Shared verification front half of both webhook endpoints: secret presence,\n// signature-header extraction, payload read, and provider signature check.\n// Returns the normalized event, or the error Response to send as-is.\nexport async function readWebhookEvent(\n\tctx: IFonderieContext,\n\tsecret: string | undefined,\n\tprovider: IBillingProvider,\n\tmissingSecretMessage: string,\n): Promise<IBillingEvent | Response> {\n\tif (!secret) {\n\t\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', missingSecretMessage);\n\t}\n\n\tconst signature =\n\t\tctx.request.headers.get('stripe-signature') ??\n\t\tctx.request.headers.get('paypal-auth-algo') ??\n\t\t'';\n\tif (!signature) {\n\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_REQUEST', 'Missing webhook signature');\n\t}\n\n\tconst payload = await ctx.request.text();\n\ttry {\n\t\treturn await provider.constructEvent({ payload, signature, secret });\n\t} catch {\n\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_REQUEST', 'Invalid webhook signature');\n\t}\n}\n","import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IBillingConfig } from '../config';\nimport type { SubscriberType } from '../types';\nimport { WalletModel } from '../models/wallet.model';\nimport { DuplicateTransactionError } from '../errors';\nimport { normalizeCurrency } from '../utils';\nimport { readWebhookEvent } from './webhook-shared';\n\n// One-time payment webhook — a SEPARATE endpoint (and secret) from the\n// subscription webhook, so each provider endpoint carries one event family.\n// Idempotency: the ledger key `<provider>:checkout:<sessionId>` makes event\n// replays no-ops.\n\nexport function paymentWebhookController(store: IStoreAdapter, config: IBillingConfig) {\n\tconst wallet = new WalletModel(store);\n\n\treturn {\n\t\tasync handle(ctx: IFonderieContext): Promise<Response> {\n\t\t\t// Deliberately NOT falling back to the subscription webhook's secret:\n\t\t\t// per-endpoint secrets exist so a delivery captured for one endpoint\n\t\t\t// can never replay validly against the other.\n\t\t\tconst event = await readWebhookEvent(\n\t\t\t\tctx,\n\t\t\t\tconfig.wallet?.webhookSecret,\n\t\t\t\tconfig.provider,\n\t\t\t\t'Payment webhook secret not configured — set wallet.webhookSecret',\n\t\t\t);\n\t\t\tif (event instanceof Response) return event;\n\n\t\t\tconst payment = event.payment;\n\t\t\t// Not a completed one-time payment (subscription events and other\n\t\t\t// noise arrive here when the operator points one endpoint at both).\n\t\t\tif (!payment) return Response.json({ received: true });\n\n\t\t\tconst meta = payment.metadata;\n\t\t\t// Only sessions created by the wallet checkout carry a packId; other\n\t\t\t// one-time payments through the same account are not ours to credit.\n\t\t\tconst packId = meta['packId'];\n\t\t\tif (!packId) return Response.json({ received: true });\n\n\t\t\tconst subscriberType = meta['subscriberType'];\n\t\t\tconst subscriberId = meta['subscriberId'];\n\t\t\tconst credits = meta['credits'] ?? '';\n\t\t\tif (\n\t\t\t\t(subscriberType !== 'user' && subscriberType !== 'workspace') ||\n\t\t\t\t!subscriberId ||\n\t\t\t\t!/^\\d{1,30}$/.test(credits)\n\t\t\t) {\n\t\t\t\t// Ours (packId present) but broken — surface as a webhook failure\n\t\t\t\t// so the provider dashboard flags it instead of silently dropping.\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t\t'Malformed wallet checkout metadata',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// Only credit once funds are confirmed. Delayed-notification methods\n\t\t\t// (ACH/SEPA debit, vouchers…) complete checkout with an unpaid\n\t\t\t// status; the provider sends a paid follow-up event later (e.g.\n\t\t\t// checkout.session.async_payment_succeeded) which lands here again.\n\t\t\tconst status = payment.paymentStatus ?? null;\n\t\t\tif (status !== null && status !== 'paid' && status !== 'no_payment_required') {\n\t\t\t\treturn Response.json({ received: true, pending: true });\n\t\t\t}\n\n\t\t\tconst currency = normalizeCurrency(meta['currency'] ?? config.wallet?.currency ?? 'USD');\n\t\t\ttry {\n\t\t\t\tconst result = await wallet.credit({\n\t\t\t\t\tsubscriberType: subscriberType as SubscriberType,\n\t\t\t\t\tsubscriberId,\n\t\t\t\t\tcurrency,\n\t\t\t\t\tamount: BigInt(credits),\n\t\t\t\t\ttype: 'purchase',\n\t\t\t\t\tidempotencyKey: `${config.provider.name}:checkout:${payment.sessionId}`,\n\t\t\t\t\tdescription: `Credit pack ${packId}`,\n\t\t\t\t\tmetadata: {\n\t\t\t\t\t\tpackId,\n\t\t\t\t\t\tamountPaid: payment.amountTotal?.toString() ?? null,\n\t\t\t\t\t\tpaymentCurrency: payment.currency,\n\t\t\t\t\t},\n\t\t\t\t\t...(payment.providerTxId ? { providerTxId: payment.providerTxId } : {}),\n\t\t\t\t});\n\t\t\t\treturn Response.json({ received: true, duplicate: result.duplicate });\n\t\t\t} catch (err) {\n\t\t\t\tif (err instanceof DuplicateTransactionError) {\n\t\t\t\t\treturn setApiResponse(HTTP.CONFLICT, 'DUPLICATE_TRANSACTION', err.message);\n\t\t\t\t}\n\t\t\t\tthrow err;\n\t\t\t}\n\t\t},\n\t};\n}\n","import { timingSafeEqual } from 'node:crypto';\n\nimport { setApiResponse, HTTP } from '@fonderie/core';\nimport type { Middleware } from '@fonderie/core';\n\n// Bootstrap-token guard for the wallet's manual-grant surface — the same\n// mechanism as @fonderie/config's admin surface: a Bearer token compared in\n// constant time, with the route only registered when a token is configured.\n\n// Constant-time comparison so a wrong token can't be recovered byte-by-byte\n// from response timing. Length-guard first: timingSafeEqual throws on unequal\n// lengths, and that early return is acceptable — the secret's length is not\n// the sensitive part.\nfunction safeTokenEqual(a: string, b: string): boolean {\n\tconst bufA = Buffer.from(a);\n\tconst bufB = Buffer.from(b);\n\tif (bufA.length !== bufB.length) return false;\n\treturn timingSafeEqual(bufA, bufB);\n}\n\nexport function requireAdminToken(adminToken: string): Middleware {\n\treturn (ctx, next) => {\n\t\tconst header = ctx.request.headers.get('authorization') ?? '';\n\t\tconst token = header.startsWith('Bearer ') ? header.slice(7) : '';\n\t\tif (!token || !safeTokenEqual(token, adminToken)) {\n\t\t\treturn Promise.resolve(\n\t\t\t\tsetApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Missing or invalid admin token'),\n\t\t\t);\n\t\t}\n\t\treturn next();\n\t};\n}\n","import type { Middleware, ICourierMessage } from '@fonderie/core';\nimport { setApiResponse, HTTP } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IBillingConfig } from '../config';\nimport type { ICounterBackend } from '../backends/types';\nimport { MESSAGE_KEYS } from '../config';\nimport { getSubscription } from '../services/subscriptions';\nimport { isWorkspaceMember } from '../services/membership';\nimport { buildBillingContext } from '../services/policy';\nimport {\n\tcurrentGrantPeriod,\n\tensurePeriodicGrant,\n\tgetWalletBalance,\n\tresolvePlanWallet,\n} from '../services/wallet';\nimport { resolveSubscriber, parseWindowMs } from '../utils';\n\n// In-process de-dup: tracks which threshold notifications have fired this session.\n// Acceptable to lose on restart (may send one duplicate after a redeploy).\nconst notified = new Set<string>();\n\nexport function withBilling(\n\tstore: IStoreAdapter,\n\tconfig: IBillingConfig,\n\tbackend: ICounterBackend,\n): Middleware {\n\treturn async (ctx, next) => {\n\t\tconst subscriber = resolveSubscriber(ctx);\n\n\t\t// No subscriber (unauthenticated / public route) — skip entirely\n\t\tif (!subscriber) return next();\n\n\t\t// SECURITY — workspace subscribers can come from the raw X-Workspace-ID\n\t\t// header. Trust the id only when it matches ctx.workspace (already\n\t\t// membership-verified by @fonderie/workspaces' withWorkspace) or when\n\t\t// the session user proves active membership here. Anything else would\n\t\t// let any caller read, drain, or rate-limit another tenant's billing.\n\t\tif (subscriber.type === 'workspace' && ctx.workspace?.id !== subscriber.id) {\n\t\t\t// Anonymous request naming a workspace: no billing context at all —\n\t\t\t// public routes keep working, and an unverified workspace's counters\n\t\t\t// and wallet stay untouched.\n\t\t\tif (!ctx.user) return next();\n\t\t\tif (!(await isWorkspaceMember(ctx.user.id, subscriber.id, store))) {\n\t\t\t\treturn setApiResponse(HTTP.FORBIDDEN, 'FORBIDDEN', 'Not a member of this workspace');\n\t\t\t}\n\t\t}\n\n\t\t// Resolve subscription → plan name (fall back to first plan = free)\n\t\tconst subscription = await getSubscription(subscriber.type, subscriber.id, store);\n\t\tconst planName = subscription?.plan ?? config.plans[0]?.name ?? 'free';\n\t\tconst active =\n\t\t\t!subscription || subscription.status === 'active' || subscription.status === 'trialing';\n\n\t\tconst plan = config.plans.find((p) => p.name === planName) ?? config.plans[0];\n\t\tif (!plan) return next();\n\n\t\t// Increment windowed (rate-limit) counters and read their current totals\n\t\tconst counters: Record<string, number> = {};\n\n\t\tfor (const [key, entry] of Object.entries(plan.policy ?? {})) {\n\t\t\tif ('enabled' in entry || !entry.window) continue;\n\n\t\t\tconst windowMs = parseWindowMs(entry.window);\n\t\t\tconst counterKey = `${subscriber.type}:${subscriber.id}:${key}`;\n\t\t\tcounters[key] = await backend.increment(counterKey, windowMs);\n\t\t}\n\n\t\t// Build and cache billing context on ctx\n\t\tconst billingCtx = buildBillingContext({ subscriber, plan, active, counters });\n\t\tctx.meta['billing'] = billingCtx;\n\n\t\t// Wallet economics — lazy periodic grant, then a balance snapshot for\n\t\t// requireWalletBalance and product code. Non-fatal by design: a wallet\n\t\t// hiccup must not take down unrelated requests.\n\t\tconst planWallet = resolvePlanWallet(plan, config);\n\t\tif (planWallet) {\n\t\t\ttry {\n\t\t\t\tconst sub = {\n\t\t\t\t\tsubscriberType: subscriber.type,\n\t\t\t\t\tsubscriberId: subscriber.id,\n\t\t\t\t\tcurrency: planWallet.currency,\n\t\t\t\t};\n\t\t\t\t// Grants require an active (or trialing) subscription — a past_due\n\t\t\t\t// or paused subscriber keeps spending existing credits but is not\n\t\t\t\t// extended new ones while payment is failing.\n\t\t\t\tif (active && planWallet.grantAmount !== null && planWallet.grantAmount > 0n) {\n\t\t\t\t\tawait ensurePeriodicGrant(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t...sub,\n\t\t\t\t\t\t\tamount: planWallet.grantAmount,\n\t\t\t\t\t\t\tperiod: currentGrantPeriod(planWallet.grantPeriod),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tstore,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tconst { balance } = await getWalletBalance(sub, store);\n\t\t\t\tbillingCtx.wallet = {\n\t\t\t\t\tbalance,\n\t\t\t\t\tcurrency: planWallet.currency,\n\t\t\t\t\tprecision: planWallet.precision,\n\t\t\t\t\toverdraftLimit: planWallet.overdraftLimit,\n\t\t\t\t\trates: planWallet.rates,\n\t\t\t\t};\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[billing] wallet context failed:', (err as Error).message);\n\t\t\t}\n\t\t}\n\n\t\t// Block requests that have hit a hard limit\n\t\tfor (const [key, status] of Object.entries(billingCtx.statuses)) {\n\t\t\tif (status.type === 'counter' && status.status === 'blocked') {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.TOO_MANY_REQUESTS,\n\t\t\t\t\t'RATE_LIMIT_EXCEEDED',\n\t\t\t\t\t`Limit exceeded for: ${key}`,\n\t\t\t\t\t{ key, limit: status.limit, used: status.used, resetsAt: status.resetsAt },\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\t// Fire threshold notifications (once per subscriber per key per session)\n\t\tif (config.notifications) {\n\t\t\tconst toNotify: ICourierMessage[] = [];\n\t\t\tconst recipient = {\n\t\t\t\temail: ctx.user?.email ?? null,\n\t\t\t\tphone: null,\n\t\t\t\tdeviceToken: null,\n\t\t\t};\n\n\t\t\tfor (const [key, status] of Object.entries(billingCtx.statuses)) {\n\t\t\t\tif (status.type !== 'counter' || status.limit === null) continue;\n\n\t\t\t\tconst base = `${subscriber.type}:${subscriber.id}:${key}`;\n\n\t\t\t\tif (config.notifications.softHit && status.status === 'over_limit') {\n\t\t\t\t\tconst nk = `${base}:reached`;\n\t\t\t\t\tif (!notified.has(nk)) {\n\t\t\t\t\t\tnotified.add(nk);\n\t\t\t\t\t\ttoNotify.push({\n\t\t\t\t\t\t\ttype: MESSAGE_KEYS.limitReached,\n\t\t\t\t\t\t\trecipient,\n\t\t\t\t\t\t\tdata: {\n\t\t\t\t\t\t\t\tkey,\n\t\t\t\t\t\t\t\tplan: plan.name,\n\t\t\t\t\t\t\t\tlimit: status.limit,\n\t\t\t\t\t\t\t\tused: status.used,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t} else if (config.notifications.warnAt && status.status === 'warning') {\n\t\t\t\t\tconst nk = `${base}:warning`;\n\t\t\t\t\tif (!notified.has(nk)) {\n\t\t\t\t\t\tnotified.add(nk);\n\t\t\t\t\t\ttoNotify.push({\n\t\t\t\t\t\t\ttype: MESSAGE_KEYS.limitWarning,\n\t\t\t\t\t\t\trecipient,\n\t\t\t\t\t\t\tdata: {\n\t\t\t\t\t\t\t\tkey,\n\t\t\t\t\t\t\t\tplan: plan.name,\n\t\t\t\t\t\t\t\tlimit: status.limit,\n\t\t\t\t\t\t\t\tused: status.used,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (toNotify.length > 0) {\n\t\t\t\tconst existing = ctx.meta['messages'] as ICourierMessage[] | undefined;\n\t\t\t\tctx.meta['messages'] = [...(existing ?? []), ...toNotify];\n\t\t\t}\n\t\t}\n\n\t\treturn next();\n\t};\n}\n","import type { IBillingProvider } from './providers/types';\nimport type { IWalletRate, PolicyEntry } from './types';\nimport type { ICounterBackend } from './backends/types';\n\nexport interface IBillingPlanPrice {\n\t/** Stable Stripe Price lookup_key, e.g. \"pro_monthly\". Preferred reference. */\n\tlookupKey?: string;\n\t/** Stripe price id. Used for hydration and as a lookup_key fallback. */\n\tpriceId?: string;\n\t/**\n\t * Display amount in the smallest currency unit (bigint, e.g. 1999n = $19.99) —\n\t * the seed value written to fonderie_plans and the fallback shown (flagged\n\t * pricingStale) when hydration is off or Stripe is unreachable. When\n\t * hydration resolves a live price, the live amount wins.\n\t */\n\tamount?: bigint;\n}\n\n/**\n * Read-through pricing: amount/currency come from Stripe (source of truth) rather\n * than the duplicated `amount` above. Gated by `hydration` (kill-switch, §16.9).\n * See packages/billing/docs/pricing-hydration.md.\n */\nexport interface IBillingPricingConfig {\n\t/** Kill-switch. When false (default), serve the configured amount/USD directly. */\n\thydration?: boolean;\n\t/** Fresh-cache TTL. Default 300_000 (5m). */\n\tcacheTtlMs?: number;\n\t/** Serve last-cached price on a transient resolution miss (lookup_key transfer). Default 3_600_000 (1h). */\n\ttransferGraceMs?: number;\n\t/** Max age to serve stale price during a Stripe outage before giving up. Default 86_400_000 (24h). */\n\tmaxStaleMs?: number;\n}\n\nexport interface IBillingPlanDefaults {\n\twarnAt?: number; // default warnAt fraction (0–1) for counter policies\n\tbuffer?: number; // default buffer for counter policies\n}\n\n/**\n * Per-plan wallet economics. Requires config.wallet to be set — a plan-level\n * wallet without the global opt-in is ignored (with a boot warning).\n */\nexport interface IBillingPlanWallet {\n\t/** Overrides the global wallet currency for this plan's grants and rates. */\n\tcurrency?: string;\n\t/** Display precision override. */\n\tprecision?: number;\n\t/**\n\t * Credits auto-granted once per grantPeriod, applied lazily by withBilling\n\t * on the subscriber's first request of the period. Only granted while the\n\t * subscription is active or trialing (no new credit while payment fails).\n\t */\n\tgrantAmount?: bigint;\n\t/** Grant cadence for grantAmount. Default 'month'. */\n\tgrantPeriod?: 'month' | 'week' | 'day';\n\t/** How far below zero rate debits may take the balance. Default 0n (block at zero). */\n\toverdraftLimit?: bigint;\n\t/** Per-metric unit costs, e.g. { 'sms:send': { cost: 75n, unit: 'msg' } }. */\n\trates?: Record<string, IWalletRate>;\n}\n\nexport interface IBillingPlan {\n\tname: string;\n\tdescription?: string;\n\ttier?: number;\n\ttrialDays?: number;\n\tmonthly?: IBillingPlanPrice;\n\tyearly?: IBillingPlanPrice;\n\tdefaults?: IBillingPlanDefaults;\n\tpolicy?: Record<string, PolicyEntry>;\n\twallet?: IBillingPlanWallet;\n\tmetadata?: Record<string, unknown>;\n}\n\nexport type RateLimitBackendConfig = 'memory' | 'db' | ICounterBackend;\n\nexport interface IBillingNotificationsConfig {\n\twarnAt?: boolean; // fire courier message when warnAt threshold crossed\n\tsoftHit?: boolean; // fire when soft limit crossed\n}\n\n/**\n * A purchasable credit top-up, synced to fonderie_credit_packs at boot (same\n * pattern as plans). Purchases go through the provider's one-time checkout;\n * the payment webhook credits `credits` to the buyer's wallet.\n */\nexport interface IBillingCreditPack {\n\t/** Stable identifier used by POST /billing/wallet/checkout, e.g. 'small'. */\n\tid: string;\n\tname: string;\n\t/** Wallet credits granted on purchase, in the smallest wallet unit. */\n\tcredits: bigint;\n\t/** Purchase price in the provider's smallest currency unit. */\n\tpriceAmount: bigint;\n\t/**\n\t * ISO 4217 PAYMENT currency for the provider charge; defaults to the\n\t * buyer's wallet currency. Credits always land in the buyer's wallet\n\t * currency regardless of what the charge was priced in.\n\t */\n\tcurrency?: string;\n\t/** Existing provider Price id — used instead of the ad-hoc priceAmount. */\n\tpriceId?: string;\n\t/** Inactive packs stay in the DB but can no longer be checked out. */\n\tactive?: boolean;\n\tmetadata?: Record<string, unknown>;\n}\n\n/**\n * Opt-in stored-value wallet. Presence of this object activates the wallet\n * subsystem (routes, credit packs, per-plan grants and rates); leaving it out\n * changes nothing for existing subscription-only consumers.\n */\nexport interface IBillingWalletConfig {\n\t/** Default wallet currency when a plan doesn't override it. Default 'USD'. */\n\tcurrency?: string;\n\t/** Display precision — decimal places of the smallest unit. Default 2. */\n\tprecision?: number;\n\t/**\n\t * Bearer token guarding POST /billing/wallet/grant (manual support/ops\n\t * grants). The route is only registered when a token is configured.\n\t */\n\tadminToken?: string;\n\t/**\n\t * Signing secret for POST /billing/webhook/payment. REQUIRED for pack\n\t * purchases: the route answers 500 until it is set, and it deliberately\n\t * does NOT fall back to the subscription webhook's secret — per-endpoint\n\t * secrets keep a delivery captured for one endpoint from replaying\n\t * against the other.\n\t */\n\twebhookSecret?: string;\n\tcreditPacks?: IBillingCreditPack[];\n}\n\nexport interface IBillingConfig {\n\tprovider: IBillingProvider;\n\tplans: IBillingPlan[];\n\tsuccessUrl: string;\n\tcancelUrl: string;\n\twebhookSecret?: string;\n\trateLimit?: { backend?: RateLimitBackendConfig };\n\tnotifications?: IBillingNotificationsConfig;\n\tpricing?: IBillingPricingConfig;\n\twallet?: IBillingWalletConfig;\n}\n\nexport const MESSAGE_KEYS = {\n\tlimitWarning: 'billing.limit-warning',\n\tlimitReached: 'billing.limit-reached',\n\tlimitBlocked: 'billing.limit-blocked',\n} as const;\n\nexport type BillingMessageKey = (typeof MESSAGE_KEYS)[keyof typeof MESSAGE_KEYS];\n","import type { IStoreAdapter } from '@fonderie/store';\n\n// Workspace-membership verification for header-derived subscribers.\n//\n// This is a deliberate cross-module DATA dependency (billing reads the\n// workspaces module's membership table) rather than a code import — modules\n// never import each other. The predicate mirrors @fonderie/workspaces'\n// getMember exactly: an active member is a role_user_workspaces row that is\n// neither removed nor suspended.\n//\n// Fail closed: when the query errors (e.g. the workspaces module — and thus\n// its table — is not installed), the caller is NOT a member. Apps without\n// workspaces never send X-Workspace-ID legitimately, so nothing breaks; an\n// attacker probing with the header gets a 403 instead of a wallet.\nexport async function isWorkspaceMember(\n\tuserId: string,\n\tworkspaceId: string,\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\ttry {\n\t\tconst rows = await store.query<{ ok: number }>(\n\t\t\t`SELECT 1 AS ok\n\t\t\t FROM fonderie_role_user_workspaces\n\t\t\t WHERE user_id = $1\n\t\t\t AND workspace_id = $2\n\t\t\t AND removed = false\n\t\t\t AND suspended = false\n\t\t\t LIMIT 1`,\n\t\t\t[userId, workspaceId],\n\t\t);\n\t\treturn rows.length > 0;\n\t} catch {\n\t\treturn false;\n\t}\n}\n","import type { IBillingPlan } from '../config';\nimport type { LimitStatus, IPolicyStatus, IBillingContext, SubscriberType } from '../types';\nimport { parseWindowMs } from '../utils';\n\nexport function buildBillingContext(opts: {\n\tsubscriber: { type: SubscriberType; id: string };\n\tplan: IBillingPlan;\n\tactive: boolean;\n\t// Pre-fetched windowed counter values keyed by policy key.\n\t// Non-windowed counter keys are absent (their used count is 0 — app manages those).\n\tcounters: Record<string, number>;\n}): IBillingContext {\n\tconst { subscriber, plan, active, counters } = opts;\n\tconst defaults = plan.defaults ?? {};\n\tconst statuses: Record<string, IPolicyStatus> = {};\n\n\tfor (const [key, entry] of Object.entries(plan.policy ?? {})) {\n\t\tif ('enabled' in entry) {\n\t\t\tstatuses[key] = { type: 'feature', enabled: entry.enabled };\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst { limit, buffer = defaults.buffer ?? 0, warnAt = defaults.warnAt ?? 0.8, window } = entry;\n\n\t\tconst used = counters[key] ?? 0;\n\t\tconst hardLimit = limit !== null ? limit + buffer : null;\n\n\t\tlet status: LimitStatus = 'ok';\n\t\tif (hardLimit !== null && used >= hardLimit) status = 'blocked';\n\t\telse if (limit !== null && used >= limit) status = 'over_limit';\n\t\telse if (limit !== null && used >= limit * warnAt) status = 'warning';\n\n\t\tlet resetsAt: string | null = null;\n\t\tif (window) {\n\t\t\tconst windowMs = parseWindowMs(window);\n\t\t\tconst windowStart = Math.floor(Date.now() / windowMs) * windowMs;\n\t\t\tresetsAt = new Date(windowStart + windowMs).toISOString();\n\t\t}\n\n\t\tstatuses[key] = { type: 'counter', limit, used, status, resetsAt };\n\t}\n\n\treturn { subscriber, plan: plan.name, active, statuses };\n}\n","import type { ICounterBackend } from './types';\n\ninterface Entry {\n\tcount: number;\n\twindowStart: number; // epoch ms — used for windowed expiry\n}\n\nexport class MemoryCounterBackend implements ICounterBackend {\n\tprivate readonly counters = new Map<string, Entry>();\n\n\tasync increment(key: string, windowMs: number | null, quantity = 1): Promise<number> {\n\t\tconst now = Date.now();\n\t\tconst existing = this.counters.get(key);\n\n\t\tif (!existing || (windowMs !== null && now - existing.windowStart >= windowMs)) {\n\t\t\tthis.counters.set(key, { count: quantity, windowStart: now });\n\t\t\treturn quantity;\n\t\t}\n\n\t\texisting.count += quantity;\n\t\treturn existing.count;\n\t}\n\n\tasync get(key: string, windowMs: number | null): Promise<number> {\n\t\tconst now = Date.now();\n\t\tconst existing = this.counters.get(key);\n\t\tif (!existing) return 0;\n\t\tif (windowMs !== null && now - existing.windowStart >= windowMs) return 0;\n\t\treturn existing.count;\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\nimport type { ICounterBackend } from './types';\n\nexport class DBCounterBackend implements ICounterBackend {\n\tconstructor(private readonly store: IStoreAdapter) {}\n\n\tasync increment(key: string, windowMs: number | null, quantity = 1): Promise<number> {\n\t\tconst [subscriberType, subscriberId, ...rest] = key.split(':');\n\t\tconst metric = rest.join(':');\n\n\t\tawait this.store.query(\n\t\t\t`INSERT INTO fonderie_usage_records (subscriber_type, subscriber_id, metric, quantity)\n\t\t\t VALUES ($1, $2, $3, $4)`,\n\t\t\t[subscriberType, subscriberId, metric, quantity],\n\t\t);\n\n\t\treturn this.get(key, windowMs);\n\t}\n\n\tasync get(key: string, windowMs: number | null): Promise<number> {\n\t\tconst [subscriberType, subscriberId, ...rest] = key.split(':');\n\t\tconst metric = rest.join(':');\n\t\tconst since = windowMs !== null ? new Date(Date.now() - windowMs) : new Date(0);\n\n\t\tconst rows = await this.store.query<{ total: string }>(\n\t\t\t`SELECT COALESCE(SUM(quantity), 0) AS total\n\t\t\t FROM fonderie_usage_records\n\t\t\t WHERE subscriber_type = $1\n\t\t\t AND subscriber_id = $2\n\t\t\t AND metric = $3\n\t\t\t AND recorded_at >= $4`,\n\t\t\t[subscriberType, subscriberId, metric, since],\n\t\t);\n\n\t\treturn parseInt(rows[0]?.total ?? '0', 10);\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\nimport type { RateLimitBackendConfig } from '../config';\nimport { MemoryCounterBackend } from './memory';\nimport { DBCounterBackend } from './db';\n\nexport function createBackend(config: RateLimitBackendConfig | undefined, store: IStoreAdapter) {\n\tif (!config || config === 'memory') return new MemoryCounterBackend();\n\tif (config === 'db') return new DBCounterBackend(store);\n\treturn config;\n}\n\nexport type { ICounterBackend } from './types';\nexport { MemoryCounterBackend } from './memory';\nexport { DBCounterBackend } from './db';\n","import type { IFonderieModule, IFonderieApp } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IBillingConfig } from './config';\nimport { buildBillingRoutes } from './routes';\nimport { syncPlansToDB } from './services/plans';\nimport { syncCreditPacksToDB } from './services/credit-packs';\nimport { withBilling } from './middlewares/billing';\nimport { createBackend } from './backends';\n\nexport class BillingModule implements IFonderieModule {\n\treadonly name = '@fonderie/billing';\n\treadonly deps = ['@fonderie/auth'];\n\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tprivate config: IBillingConfig,\n\t) {}\n\n\tasync install(app: IFonderieApp): Promise<void> {\n\t\tif (!this.config.wallet && this.config.plans.some((p) => p.wallet)) {\n\t\t\t// eslint-disable-next-line no-console\n\t\t\tconsole.warn(\n\t\t\t\t'[billing] plans define wallet economics but config.wallet is not set — wallet features are disabled',\n\t\t\t);\n\t\t}\n\n\t\tawait syncPlansToDB(this.config, this.store);\n\t\tif (this.config.wallet) await syncCreditPacksToDB(this.config, this.store);\n\n\t\tconst backend = createBackend(this.config.rateLimit?.backend, this.store);\n\n\t\t// Global middleware — resolves subscriber + plan, enforces rate limits,\n\t\t// caches IBillingContext on ctx.meta['billing'] for every request.\n\t\t// Runs after auth (ctx.user available), before route handlers.\n\t\tapp.use(withBilling(this.store, this.config, backend));\n\n\t\tconst routes = buildBillingRoutes(this.store, this.config);\n\t\tfor (const [method, path, ...handlers] of routes) {\n\t\t\tapp.addRoute(method, path, ...handlers);\n\t\t}\n\t}\n}\n","import type {\n\tIBillingProvider,\n\tIBillingEvent,\n\tINormalizedPayment,\n\tINormalizedSubscription,\n\tIResolvedPrice,\n} from './types';\nimport { BILLING_INTERVAL, isBillingInterval } from '../types';\nimport type { BillingInterval, SubscriberType } from '../types';\nimport { toSafeNumber } from '../utils';\n\ninterface IStripeSubscriptionRaw {\n\tid: string;\n\tstatus: string;\n\tcustomer: string;\n\tmetadata?: Record<string, string>;\n\titems: {\n\t\tdata: Array<{\n\t\t\tprice: { id: string; nickname: string | null; lookup_key?: string | null; recurring?: { interval: string } };\n\t\t\t// Since Stripe API 2025+, the period lives on the item, not the subscription.\n\t\t\tcurrent_period_start?: number;\n\t\t\tcurrent_period_end?: number;\n\t\t}>;\n\t};\n\t// Older API versions (pre-2025) expose the period on the subscription itself.\n\tcurrent_period_start?: number;\n\tcurrent_period_end?: number;\n\tcancel_at_period_end: boolean;\n\ttrial_end: number | null;\n}\n\ninterface IStripeEventRaw {\n\ttype: string;\n\tdata: { object: unknown };\n}\n\nexport interface IStripeCheckoutSessionRaw {\n\tid: string;\n\tmode?: string;\n\tpayment_intent?: string | { id: string } | null;\n\tamount_total?: number | null;\n\tcurrency?: string | null;\n\tpayment_status?: string | null;\n\tmetadata?: Record<string, string> | null;\n}\n\n// Pure normalization of a completed payment-mode checkout session — exported\n// for tests (constructEvent itself needs the Stripe SDK for signatures).\nexport function normalizePaymentSession(session: IStripeCheckoutSessionRaw): INormalizedPayment {\n\tconst pi = session.payment_intent;\n\treturn {\n\t\tsessionId: session.id,\n\t\tproviderTxId: typeof pi === 'string' ? pi : (pi?.id ?? null),\n\t\tamountTotal: session.amount_total != null ? BigInt(session.amount_total) : null,\n\t\tcurrency: session.currency ?? null,\n\t\tpaymentStatus: session.payment_status ?? null,\n\t\tmetadata: session.metadata ?? {},\n\t};\n}\n\n// Lazy singleton — Stripe SDK is optional\nlet _client: unknown = null;\n\nasync function getClient(secretKey: string): Promise<unknown> {\n\tif (_client) return _client;\n\n\tconst pkg = 'stripe';\n\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tconst mod: any = await import(pkg).catch(() => {\n\t\tthrow new Error('[billing:stripe] stripe is required: npm install stripe');\n\t});\n\n\tconst Stripe = mod.default ?? mod;\n\t_client = new Stripe(secretKey, { apiVersion: '2024-11-20.acacia' });\n\treturn _client;\n}\n\n// Stripe also supports 'day' and 'week' recurring prices; the framework's\n// billing model is month/year. Anything else keeps the historical MONTH\n// fallback — loudly, so a weekly price can't silently masquerade as monthly.\nexport function toBillingInterval(raw: string | undefined): BillingInterval {\n\tif (isBillingInterval(raw)) return raw;\n\tif (raw !== undefined) {\n\t\t// eslint-disable-next-line no-console\n\t\tconsole.warn(\n\t\t\t`[billing:stripe] unsupported price interval '${raw}' — recording as '${BILLING_INTERVAL.MONTH}'`,\n\t\t);\n\t}\n\treturn BILLING_INTERVAL.MONTH;\n}\n\nfunction normalizeSubscription(sub: IStripeSubscriptionRaw): INormalizedSubscription {\n\tconst item = sub.items.data[0];\n\t// Period moved from the subscription to the item in Stripe API 2025+; read the\n\t// item first, fall back to the subscription-level fields for older versions.\n\tconst periodStart = item?.current_period_start ?? sub.current_period_start;\n\tconst periodEnd = item?.current_period_end ?? sub.current_period_end;\n\treturn {\n\t\tsubscriberType: (sub.metadata?.['subscriberType'] ?? 'workspace') as SubscriberType,\n\t\tsubscriberId: sub.metadata?.['subscriberId'] ?? '',\n\t\tplan: item?.price.nickname ?? 'unknown',\n\t\tpriceLookupKey: item?.price.lookup_key ?? null,\n\t\tpriceId: item?.price.id ?? null,\n\t\tstatus: sub.status,\n\t\tproviderCustomerId: sub.customer,\n\t\tproviderSubscriptionId: sub.id,\n\t\tcurrentPeriodStart: periodStart ? new Date(periodStart * 1000) : new Date(),\n\t\tcurrentPeriodEnd: periodEnd ? new Date(periodEnd * 1000) : new Date(),\n\t\tcancelAtPeriodEnd: sub.cancel_at_period_end,\n\t\ttrialEndsAt: sub.trial_end ? new Date(sub.trial_end * 1000) : null,\n\t\tinterval: toBillingInterval(item?.price.recurring?.interval),\n\t};\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction toResolvedPrice(p: any): IResolvedPrice {\n\treturn {\n\t\tpriceId: p.id,\n\t\tlookupKey: p.lookup_key ?? null,\n\t\tunitAmount: BigInt(p.unit_amount ?? 0),\n\t\tcurrency: p.currency,\n\t\tinterval: toBillingInterval(p.recurring?.interval),\n\t\tnickname: p.nickname ?? null,\n\t\tproductId: typeof p.product === 'string' ? p.product : (p.product?.id ?? ''),\n\t\tactive: p.active ?? true,\n\t};\n}\n\nexport class StripeProvider implements IBillingProvider {\n\treadonly name = 'stripe';\n\n\tconstructor(\n\t\tprivate secretKey: string,\n\t\tprivate webhookSecret?: string,\n\t) {}\n\n\tprivate async client(): Promise<any> {\n\t\treturn getClient(this.secretKey);\n\t}\n\n\tasync createCustomer(opts: {\n\t\temail: string;\n\t\tsubscriberType: SubscriberType;\n\t\tsubscriberId: string;\n\t\tuserId: string;\n\t}): Promise<{ customerId: string }> {\n\t\tconst stripe = await this.client();\n\t\tconst customer = await stripe.customers.create({\n\t\t\temail: opts.email,\n\t\t\tmetadata: {\n\t\t\t\tsubscriberType: opts.subscriberType,\n\t\t\t\tsubscriberId: opts.subscriberId,\n\t\t\t\tuserId: opts.userId,\n\t\t\t},\n\t\t});\n\t\treturn { customerId: customer.id };\n\t}\n\n\tasync createCheckoutSession(opts: {\n\t\tcustomerId: string;\n\t\tpriceId: string;\n\t\tsubscriberType: SubscriberType;\n\t\tsubscriberId: string;\n\t\ttrialDays?: number;\n\t\tsuccessUrl: string;\n\t\tcancelUrl: string;\n\t}): Promise<{ url: string }> {\n\t\tconst stripe = await this.client();\n\t\tconst session = await stripe.checkout.sessions.create({\n\t\t\tcustomer: opts.customerId,\n\t\t\tmode: 'subscription',\n\t\t\tline_items: [{ price: opts.priceId, quantity: 1 }],\n\t\t\tsuccess_url: opts.successUrl,\n\t\t\tcancel_url: opts.cancelUrl,\n\t\t\tsubscription_data: {\n\t\t\t\tmetadata: {\n\t\t\t\t\tsubscriberType: opts.subscriberType,\n\t\t\t\t\tsubscriberId: opts.subscriberId,\n\t\t\t\t},\n\t\t\t\t...(opts.trialDays && opts.trialDays > 0 ? { trial_period_days: opts.trialDays } : {}),\n\t\t\t},\n\t\t});\n\t\treturn { url: session.url ?? '' };\n\t}\n\n\tasync createPaymentCheckoutSession(opts: {\n\t\tcustomerId: string;\n\t\tamount: bigint;\n\t\tcurrency: string;\n\t\tname: string;\n\t\tquantity?: number;\n\t\tpriceId?: string;\n\t\tmetadata: Record<string, string>;\n\t\tsuccessUrl: string;\n\t\tcancelUrl: string;\n\t}): Promise<{ url: string; sessionId: string }> {\n\t\tconst stripe = await this.client();\n\t\tconst lineItem = opts.priceId\n\t\t\t? { price: opts.priceId, quantity: opts.quantity ?? 1 }\n\t\t\t: {\n\t\t\t\t\tprice_data: {\n\t\t\t\t\t\tcurrency: opts.currency.toLowerCase(),\n\t\t\t\t\t\t// Stripe's SDK takes a JS number; toSafeNumber throws past 2^53\n\t\t\t\t\t\t// instead of silently rounding.\n\t\t\t\t\t\tunit_amount: toSafeNumber(opts.amount),\n\t\t\t\t\t\tproduct_data: { name: opts.name },\n\t\t\t\t\t},\n\t\t\t\t\tquantity: opts.quantity ?? 1,\n\t\t\t\t};\n\t\tconst session = await stripe.checkout.sessions.create({\n\t\t\tcustomer: opts.customerId,\n\t\t\tmode: 'payment',\n\t\t\tline_items: [lineItem],\n\t\t\tsuccess_url: opts.successUrl,\n\t\t\tcancel_url: opts.cancelUrl,\n\t\t\tmetadata: opts.metadata,\n\t\t});\n\t\treturn { url: session.url ?? '', sessionId: session.id };\n\t}\n\n\tasync resolvePriceById(priceId: string): Promise<IResolvedPrice | null> {\n\t\tconst stripe = await this.client();\n\t\ttry {\n\t\t\tconst p = await stripe.prices.retrieve(priceId, { expand: ['product'] });\n\t\t\treturn toResolvedPrice(p);\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tasync resolvePricesByLookupKey(lookupKeys: string[]): Promise<Map<string, IResolvedPrice>> {\n\t\tconst out = new Map<string, IResolvedPrice>();\n\t\tif (lookupKeys.length === 0) return out;\n\t\tconst stripe = await this.client();\n\t\tconst res = await stripe.prices.list({\n\t\t\tlookup_keys: lookupKeys,\n\t\t\tactive: true,\n\t\t\texpand: ['data.product'],\n\t\t\tlimit: 100,\n\t\t});\n\t\tfor (const p of res.data) {\n\t\t\tif (p.lookup_key) out.set(p.lookup_key, toResolvedPrice(p));\n\t\t}\n\t\treturn out;\n\t}\n\n\tasync updateSubscription(opts: {\n\t\tsubscriptionId: string;\n\t\tpriceId: string;\n\t}): Promise<{ status: string; currentPeriodStart: Date | null; currentPeriodEnd: Date | null }> {\n\t\tconst stripe = await this.client();\n\t\tconst sub = await stripe.subscriptions.retrieve(opts.subscriptionId);\n\t\tconst itemId = sub.items.data[0]?.id;\n\t\t// Swap the price on the existing item and invoice the prorated difference\n\t\t// immediately (upgrade → pay the difference now).\n\t\tconst updated = await stripe.subscriptions.update(opts.subscriptionId, {\n\t\t\titems: [{ id: itemId, price: opts.priceId }],\n\t\t\tproration_behavior: 'always_invoice',\n\t\t\tpayment_behavior: 'error_if_incomplete',\n\t\t});\n\t\tconst item = updated.items?.data?.[0];\n\t\tconst cps = item?.current_period_start ?? updated.current_period_start;\n\t\tconst cpe = item?.current_period_end ?? updated.current_period_end;\n\t\treturn {\n\t\t\tstatus: updated.status,\n\t\t\tcurrentPeriodStart: cps ? new Date(cps * 1000) : null,\n\t\t\tcurrentPeriodEnd: cpe ? new Date(cpe * 1000) : null,\n\t\t};\n\t}\n\n\tasync createPortalSession(opts: {\n\t\tcustomerId: string;\n\t\treturnUrl: string;\n\t}): Promise<{ url: string }> {\n\t\tconst stripe = await this.client();\n\t\tconst session = await stripe.billingPortal.sessions.create({\n\t\t\tcustomer: opts.customerId,\n\t\t\treturn_url: opts.returnUrl,\n\t\t});\n\t\treturn { url: session.url };\n\t}\n\n\tasync constructEvent(opts: {\n\t\tpayload: string;\n\t\tsignature: string;\n\t\tsecret: string;\n\t}): Promise<IBillingEvent> {\n\t\tconst stripe = await this.client();\n\n\t\tlet raw: IStripeEventRaw;\n\t\ttry {\n\t\t\traw = stripe.webhooks.constructEvent(opts.payload, opts.signature, opts.secret);\n\t\t} catch {\n\t\t\tthrow new Error('[billing:stripe] Invalid webhook signature');\n\t\t}\n\n\t\t// One-time payment events — normalized for the payment webhook.\n\t\t// checkout.session.completed can arrive with payment_status 'unpaid'\n\t\t// for delayed-notification methods; the paid follow-up is\n\t\t// checkout.session.async_payment_succeeded. Subscription-mode checkout\n\t\t// completions pass through untouched (the subscription lifecycle\n\t\t// arrives via customer.subscription.* events).\n\t\tif (\n\t\t\traw.type === 'checkout.session.completed' ||\n\t\t\traw.type === 'checkout.session.async_payment_succeeded'\n\t\t) {\n\t\t\tconst session = raw.data.object as IStripeCheckoutSessionRaw;\n\t\t\tif (session.mode === 'payment') {\n\t\t\t\treturn { type: raw.type, subscription: null, payment: normalizePaymentSession(session) };\n\t\t\t}\n\t\t\treturn { type: raw.type, subscription: null };\n\t\t}\n\n\t\tconst isSubscriptionEvent = [\n\t\t\t'customer.subscription.created',\n\t\t\t'customer.subscription.updated',\n\t\t\t'customer.subscription.deleted',\n\t\t].includes(raw.type);\n\n\t\tif (!isSubscriptionEvent) {\n\t\t\treturn { type: raw.type, subscription: null };\n\t\t}\n\n\t\tconst sub = raw.data.object as IStripeSubscriptionRaw;\n\n\t\tif (raw.type === 'customer.subscription.deleted') {\n\t\t\treturn {\n\t\t\t\ttype: raw.type,\n\t\t\t\tsubscription: { ...normalizeSubscription(sub), plan: 'free', status: 'canceled' },\n\t\t\t};\n\t\t}\n\n\t\treturn { type: raw.type, subscription: normalizeSubscription(sub) };\n\t}\n}\n","import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { Middleware } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { getSubscription } from '../services/subscriptions';\nimport { resolveSubscriber } from '../utils';\n\n// Gates a route behind a minimum plan.\n// Works for both user-level and workspace-level subscriptions.\n// Usage: requirePlan(['pro', 'enterprise'], store)\n\nfunction makeHandler(plans: string | string[], store: IStoreAdapter): Middleware {\n\tconst allowed = Array.isArray(plans) ? plans : [plans];\n\n\treturn async (ctx, next) => {\n\t\tif (!ctx.user) {\n\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t\t}\n\n\t\tconst subscriber = resolveSubscriber(ctx);\n\t\tif (!subscriber) {\n\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'SUBSCRIBER_REQUIRED', 'Subscriber context required');\n\t\t}\n\n\t\tconst subscription = await getSubscription(subscriber.type, subscriber.id, store);\n\n\t\tif (!subscription || !allowed.includes(subscription.plan)) {\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.PAYMENT_REQUIRED,\n\t\t\t\t'PLAN_UPGRADE_REQUIRED',\n\t\t\t\t'Plan upgrade required',\n\t\t\t\t{ required: allowed, current: subscription?.plan ?? 'none' },\n\t\t\t);\n\t\t}\n\n\t\tif (subscription.status !== 'active' && subscription.status !== 'trialing') {\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.PAYMENT_REQUIRED,\n\t\t\t\t'SUBSCRIPTION_INACTIVE',\n\t\t\t\t'Subscription is not active',\n\t\t\t\t{ status: subscription.status },\n\t\t\t);\n\t\t}\n\n\t\treturn next();\n\t};\n}\n\nexport function requirePlan(plans: string | string[], store: IStoreAdapter): Middleware;\nexport function requirePlan(\n\tplans: string | string[],\n\tstore: IStoreAdapter,\n\tctx: IFonderieContext,\n\tnext: () => Promise<Response>,\n): Promise<Response>;\nexport function requirePlan(\n\tplans: string | string[],\n\tstore: IStoreAdapter,\n\tctx?: IFonderieContext,\n\tnext?: () => Promise<Response>,\n): Middleware | Promise<Response> {\n\tconst handler = makeHandler(plans, store);\n\tif (ctx !== undefined && next !== undefined) return handler(ctx, next);\n\treturn handler;\n}\n"],"mappings":"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","names":["import_core","import_core","import_core","import_core","import_core","import_core","import_core","import_core","import_core","import_core","import_core"]}
|