@fonderie/billing 5.0.2 → 5.1.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/dist/{index-CKzM1gyJ.d.ts → index-BvYwTSIF.d.ts} +8 -0
- package/dist/{index-Ce_BMy1V.d.cts → index-CjwZwEVR.d.cts} +8 -0
- package/dist/index.cjs +55 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -2
- package/dist/index.d.ts +10 -2
- package/dist/index.js +55 -0
- package/dist/index.js.map +1 -1
- package/dist/middlewares/index.d.cts +1 -1
- package/dist/middlewares/index.d.ts +1 -1
- package/package.json +1 -1
|
@@ -40,6 +40,14 @@ interface IBillingProvider {
|
|
|
40
40
|
}): Promise<{
|
|
41
41
|
url: string;
|
|
42
42
|
}>;
|
|
43
|
+
updateSubscription(opts: {
|
|
44
|
+
subscriptionId: string;
|
|
45
|
+
priceId: string;
|
|
46
|
+
}): Promise<{
|
|
47
|
+
status: string;
|
|
48
|
+
currentPeriodStart: Date | null;
|
|
49
|
+
currentPeriodEnd: Date | null;
|
|
50
|
+
}>;
|
|
43
51
|
createPortalSession(opts: {
|
|
44
52
|
customerId: string;
|
|
45
53
|
returnUrl: string;
|
|
@@ -40,6 +40,14 @@ interface IBillingProvider {
|
|
|
40
40
|
}): Promise<{
|
|
41
41
|
url: string;
|
|
42
42
|
}>;
|
|
43
|
+
updateSubscription(opts: {
|
|
44
|
+
subscriptionId: string;
|
|
45
|
+
priceId: string;
|
|
46
|
+
}): Promise<{
|
|
47
|
+
status: string;
|
|
48
|
+
currentPeriodStart: Date | null;
|
|
49
|
+
currentPeriodEnd: Date | null;
|
|
50
|
+
}>;
|
|
43
51
|
createPortalSession(opts: {
|
|
44
52
|
customerId: string;
|
|
45
53
|
returnUrl: string;
|
package/dist/index.cjs
CHANGED
|
@@ -588,6 +588,43 @@ function checkoutController(store, config) {
|
|
|
588
588
|
`Plan ${planName} does not support ${interval} billing`
|
|
589
589
|
);
|
|
590
590
|
}
|
|
591
|
+
const current = await subscriptions.get(subscriber.type, subscriber.id);
|
|
592
|
+
const ACTIVE = ["active", "trialing", "past_due"];
|
|
593
|
+
if (current && ACTIVE.includes(current.status)) {
|
|
594
|
+
const currentTier = plans.findByNameInConfig(current.plan, config)?.tier ?? -1;
|
|
595
|
+
const targetTier = plan.tier ?? -1;
|
|
596
|
+
if (targetTier <= currentTier) {
|
|
597
|
+
return (0, import_core3.setApiResponse)(
|
|
598
|
+
import_core3.HTTP.UNPROCESSABLE,
|
|
599
|
+
"DOWNGRADE_NOT_ALLOWED",
|
|
600
|
+
`Cannot switch from ${current.plan} to a same-or-lower tier (${planName}) mid-cycle. Upgrades only.`
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
if (current.providerSubscriptionId) {
|
|
604
|
+
const res = await config.provider.updateSubscription({
|
|
605
|
+
subscriptionId: current.providerSubscriptionId,
|
|
606
|
+
priceId: pricing.priceId
|
|
607
|
+
});
|
|
608
|
+
const upsert = {
|
|
609
|
+
subscriberType: subscriber.type,
|
|
610
|
+
subscriberId: subscriber.id,
|
|
611
|
+
plan: planName,
|
|
612
|
+
interval,
|
|
613
|
+
status: res.status,
|
|
614
|
+
providerSubscriptionId: current.providerSubscriptionId
|
|
615
|
+
};
|
|
616
|
+
if (current.providerCustomerId) upsert.providerCustomerId = current.providerCustomerId;
|
|
617
|
+
if (res.currentPeriodStart) upsert.currentPeriodStart = res.currentPeriodStart;
|
|
618
|
+
if (res.currentPeriodEnd) upsert.currentPeriodEnd = res.currentPeriodEnd;
|
|
619
|
+
await subscriptions.upsert(upsert);
|
|
620
|
+
return (0, import_core3.setApiResponse)(
|
|
621
|
+
import_core3.HTTP.OK,
|
|
622
|
+
"SUBSCRIPTION_UPGRADED",
|
|
623
|
+
"Subscription upgraded; the prorated difference was charged.",
|
|
624
|
+
{ upgraded: true, plan: planName }
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
591
628
|
const { customerId } = await config.provider.createCustomer({
|
|
592
629
|
email: ctx.user.email ?? "",
|
|
593
630
|
subscriberType: subscriber.type,
|
|
@@ -1069,6 +1106,24 @@ var StripeProvider = class {
|
|
|
1069
1106
|
});
|
|
1070
1107
|
return { url: session.url ?? "" };
|
|
1071
1108
|
}
|
|
1109
|
+
async updateSubscription(opts) {
|
|
1110
|
+
const stripe = await this.client();
|
|
1111
|
+
const sub = await stripe.subscriptions.retrieve(opts.subscriptionId);
|
|
1112
|
+
const itemId = sub.items.data[0]?.id;
|
|
1113
|
+
const updated = await stripe.subscriptions.update(opts.subscriptionId, {
|
|
1114
|
+
items: [{ id: itemId, price: opts.priceId }],
|
|
1115
|
+
proration_behavior: "always_invoice",
|
|
1116
|
+
payment_behavior: "error_if_incomplete"
|
|
1117
|
+
});
|
|
1118
|
+
const item = updated.items?.data?.[0];
|
|
1119
|
+
const cps = item?.current_period_start ?? updated.current_period_start;
|
|
1120
|
+
const cpe = item?.current_period_end ?? updated.current_period_end;
|
|
1121
|
+
return {
|
|
1122
|
+
status: updated.status,
|
|
1123
|
+
currentPeriodStart: cps ? new Date(cps * 1e3) : null,
|
|
1124
|
+
currentPeriodEnd: cpe ? new Date(cpe * 1e3) : null
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1072
1127
|
async createPortalSession(opts) {
|
|
1073
1128
|
const stripe = await this.client();
|
|
1074
1129
|
const session = await stripe.billingPortal.sessions.create({
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/routes.ts","../src/schemas.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/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\tRateLimitBackendConfig,\n\tIBillingNotificationsConfig,\n\tBillingMessageKey,\n} from './config';\n\n// Backends\nexport { MemoryCounterBackend, DBCounterBackend } from './backends';\nexport type { ICounterBackend } from './backends';\n\n// Types\nexport type { IBillingProvider, IBillingEvent } 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 { 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 plan = planController(store);\n\tconst subscription = subscriptionController(store);\n\tconst checkout = checkoutController(store, config);\n\tconst usage = usageController(store);\n\tconst webhook = webhookController(store, config);\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 { setApiResponse, HTTP, stringOrEmpty, numberOrZero } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { PlanModel } from '../models/plan.model';\nimport { toPlanDTO } from '../dtos/billing';\n\nexport function planController(store: IStoreAdapter) {\n\tconst plans = new PlanModel(store);\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\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\treturn setApiResponse(HTTP.OK, 'PLAN_FETCHED', 'Plan retrieved successfully.', {\n\t\t\t\tplan: toPlanDTO(plan),\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\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\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\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 { SubscriptionModel } from '../models/subscription.model';\n\nexport function webhookController(store: IStoreAdapter, config: IBillingConfig) {\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\tif (event.subscription) {\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: event.subscription.plan,\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\tamount: number;\n\tpriceId?: string;\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}\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","import type { IBillingProvider, IBillingEvent, INormalizedSubscription } 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; 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\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 === 'year' ? 'year' : 'month',\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 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/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/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\tRateLimitBackendConfig,\n\tIBillingNotificationsConfig,\n\tBillingMessageKey,\n} from './config';\n\n// Backends\nexport { MemoryCounterBackend, DBCounterBackend } from './backends';\nexport type { ICounterBackend } from './backends';\n\n// Types\nexport type { IBillingProvider, IBillingEvent } 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 { 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 plan = planController(store);\n\tconst subscription = subscriptionController(store);\n\tconst checkout = checkoutController(store, config);\n\tconst usage = usageController(store);\n\tconst webhook = webhookController(store, config);\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 { setApiResponse, HTTP, stringOrEmpty, numberOrZero } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { PlanModel } from '../models/plan.model';\nimport { toPlanDTO } from '../dtos/billing';\n\nexport function planController(store: IStoreAdapter) {\n\tconst plans = new PlanModel(store);\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\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\treturn setApiResponse(HTTP.OK, 'PLAN_FETCHED', 'Plan retrieved successfully.', {\n\t\t\t\tplan: toPlanDTO(plan),\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\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\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 { SubscriptionModel } from '../models/subscription.model';\n\nexport function webhookController(store: IStoreAdapter, config: IBillingConfig) {\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\tif (event.subscription) {\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: event.subscription.plan,\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\tamount: number;\n\tpriceId?: string;\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}\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","import type { IBillingProvider, IBillingEvent, INormalizedSubscription } 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; 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\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 === 'year' ? 'year' : 'month',\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 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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACEA,yBAAsC;;;ACFtC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iBAAkB;AAMlB,IAAM,aAAa;AAAA,EAClB,aAAa,aAAE,OAAO,EAAE,IAAI,GAAI,EAAE,SAAS,EAAE,SAAS;AAAA,EACtD,MAAM,aAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,EACvC,OAAO,aAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,SAAS,EAAE,SAAS;AAAA,EACnD,WAAW,aAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,EAC5C,eAAe,aAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS,EAAE,SAAS;AAAA,EACrD,gBAAgB,aAAE,OAAO,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,SAAS;AAAA,EACxD,cAAc,aAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS,EAAE,SAAS;AAAA,EACpD,eAAe,aAAE,OAAO,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,SAAS;AAAA,EACvD,UAAU,aAAE,QAAQ,EAAE,SAAS;AAAA,EAC/B,UAAU,aAAE,QAAQ,EAAE,SAAS;AAChC;AAEO,IAAM,mBAAmB,aAAE,OAAO;AAAA,EACxC,MAAM,aAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,kBAAkB,EAAE,IAAI,GAAG;AAAA,EAC1D,GAAG;AACJ,CAAC;AAEM,IAAM,mBAAmB,aAC9B,OAAO,EAAE,MAAM,aAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS,GAAG,GAAG,WAAW,CAAC,EAC5E,OAAO,CAAC,MAAM,OAAO,OAAO,CAAC,EAAE,KAAK,CAAC,MAAM,MAAM,MAAS,GAAG,4BAA4B;AAEpF,IAAM,iBAAiB,aAAE,OAAO;AAAA,EACtC,MAAM,aAAE,OAAO,EAAE,IAAI,GAAG,kBAAkB;AAAA,EAC1C,UAAU,aAAE,KAAK,CAAC,SAAS,MAAM,CAAC,EAAE,SAAS;AAC9C,CAAC;AAEM,IAAM,oBAAoB,aAAE,OAAO;AAAA,EACzC,QAAQ,aAAE,OAAO,EAAE,IAAI,GAAG,oBAAoB,EAAE,IAAI,GAAG;AAAA,EACvD,UAAU,aAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS;AACtC,CAAC;;;ACpCD,kBAAkE;;;ACK3D,SAAS,SAAS,QAAwC;AAChE,SAAO,OAAO;AACf;AAEO,SAAS,cAAc,MAAc,QAA6C;AACxF,SAAO,OAAO,MAAM,KAAK,CAAC,MAAM,EAAE,KAAK,YAAY,MAAM,KAAK,YAAY,CAAC,KAAK;AACjF;AAEA,eAAsB,cAAc,QAAwB,OAAqC;AAChG,QAAM,QAAQ,OAAO;AACrB,MAAI,MAAM,WAAW,EAAG;AAExB,QAAM,SAAS,MAAM,IAAI,CAAC,GAAG,MAAM;AAClC,UAAM,IAAI,IAAI;AACd,WAAO,KAAK,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,IAAI,CAAC;AAAA,EAC1G,CAAC;AAED,QAAM,SAAS,MAAM,QAAQ,CAAC,SAAS;AAAA,IACtC,KAAK;AAAA,IACL,KAAK,aAAa;AAAA,IAClB,KAAK,SAAS,UAAU;AAAA,IACxB,KAAK,SAAS,WAAW;AAAA,IACzB,KAAK,QAAQ,UAAU;AAAA,IACvB,KAAK,QAAQ,WAAW;AAAA,IACxB,KAAK,eAAe;AAAA,IACpB,KAAK,QAAQ;AAAA,IACb,KAAK,UAAU,KAAK,YAAY,CAAC,CAAC;AAAA,EACnC,CAAC;AAED,QAAM,MAAM;AAAA,IACX;AAAA;AAAA;AAAA;AAAA;AAAA,WAKS,OAAO,KAAK,IAAI,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAU1B;AAAA,EACD;AACD;AAEA,IAAM,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgBpB,eAAsB,WAAW,OAAwC;AACxE,SAAO,MAAM;AAAA,IACZ,GAAG,WAAW;AAAA,EACf;AACD;AAEA,eAAsB,YAAY,IAAY,OAA6C;AAC1F,QAAM,CAAC,GAAG,IAAI,MAAM,MAAM,MAAa,GAAG,WAAW,kBAAkB,CAAC,EAAE,CAAC;AAC3E,SAAO,OAAO;AACf;AAEA,eAAsB,WACrB,MAaA,OACiB;AACjB,QAAM,CAAC,GAAG,IAAI,MAAM,MAAM;AAAA,IACzB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAYA;AAAA,MACC,KAAK;AAAA,MACL,KAAK,SAAS;AAAA,MACd,KAAK,aAAa;AAAA,MAClB,KAAK,iBAAiB;AAAA,MACtB,KAAK,kBAAkB;AAAA,MACvB,KAAK,gBAAgB;AAAA,MACrB,KAAK,iBAAiB;AAAA,MACtB,KAAK,eAAe;AAAA,MACpB,KAAK,QAAQ;AAAA,MACb,KAAK,UAAU,KAAK,YAAY,CAAC,CAAC;AAAA,MAClC,KAAK,UAAU,KAAK,YAAY,CAAC,CAAC;AAAA,IACnC;AAAA,EACD;AACA,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,uBAAuB;AACjD,SAAO;AACR;AAEA,eAAsB,WACrB,IACA,MACA,OACwB;AACxB,QAAM,WAAmC;AAAA,IACxC,MAAM;AAAA,IACN,OAAO;AAAA,IACP,WAAW;AAAA,IACX,eAAe;AAAA,IACf,gBAAgB;AAAA,IAChB,cAAc;AAAA,IACd,eAAe;AAAA,IACf,aAAa;AAAA,IACb,MAAM;AAAA,EACP;AAEA,QAAM,cAAc,oBAAI,IAAI,CAAC,YAAY,UAAU,CAAC;AACpD,QAAM,aAAuB,CAAC;AAC9B,QAAM,SAAoB,CAAC,EAAE;AAE7B,aAAW,CAAC,KAAK,GAAG,KAAK,OAAO,QAAQ,QAAQ,GAAG;AAClD,QAAI,OAAO,MAAM;AAChB,aAAO,KAAM,KAAiC,GAAG,CAAC;AAClD,iBAAW,KAAK,GAAG,GAAG,OAAO,OAAO,MAAM,EAAE;AAAA,IAC7C;AAAA,EACD;AAEA,aAAW,OAAO,aAAa;AAC9B,QAAI,OAAO,MAAM;AAChB,aAAO,KAAK,KAAK,UAAW,KAAiC,GAAG,CAAC,CAAC;AAClE,iBAAW,KAAK,GAAG,GAAG,OAAO,OAAO,MAAM,SAAS;AAAA,IACpD;AAAA,EACD;AAEA,MAAI,WAAW,WAAW,EAAG,QAAO,YAAY,IAAI,KAAK;AAEzD,QAAM,CAAC,GAAG,IAAI,MAAM,MAAM;AAAA,IACzB,6BAA6B,WAAW,KAAK,IAAI,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAUlD;AAAA,EACD;AACA,SAAO,OAAO;AACf;AAEA,eAAsB,WAAW,IAAY,OAAwC;AACpF,QAAM,OAAO,MAAM,MAAM;AAAA,IACxB;AAAA,IACA,CAAC,EAAE;AAAA,EACJ;AACA,SAAO,KAAK,SAAS;AACtB;;;AC7KO,IAAM,YAAN,MAAgB;AAAA,EACtB,YAA6B,OAAsB;AAAtB;AAAA,EAAuB;AAAA,EAAvB;AAAA,EAE7B,eAAe,QAAwB;AACtC,WAAO,SAAS,MAAM;AAAA,EACvB;AAAA,EAEA,mBAAmB,MAAc,QAAwB;AACxD,WAAO,cAAc,MAAM,MAAM;AAAA,EAClC;AAAA,EAEA,OAAyB;AACxB,WAAO,WAAW,KAAK,KAAK;AAAA,EAC7B;AAAA,EAEA,SAAS,IAAmC;AAC3C,WAAO,YAAY,IAAI,KAAK,KAAK;AAAA,EAClC;AAAA,EAEA,OAAO,MAAwD;AAC9D,WAAO,WAAW,MAAM,KAAK,KAAK;AAAA,EACnC;AAAA,EAEA,OAAO,IAAY,MAA+D;AACjF,WAAO,WAAW,IAAI,MAAM,KAAK,KAAK;AAAA,EACvC;AAAA,EAEA,OAAO,IAA8B;AACpC,WAAO,WAAW,IAAI,KAAK,KAAK;AAAA,EACjC;AACD;;;ACFO,SAAS,UAAU,MAAuB;AAChD,SAAO;AAAA,IACN,IAAI,KAAK;AAAA,IACT,QAAQ,KAAK,KAAK,YAAY;AAAA,IAC9B,MAAM,KAAK;AAAA,IACX,aAAa,KAAK,eAAe;AAAA,IACjC,MAAM,KAAK;AAAA,IACX,OAAO,KAAK;AAAA,IACZ,WAAW,KAAK;AAAA,IAChB,SAAS;AAAA,MACR,SAAS,KAAK,iBAAiB;AAAA,MAC/B,QAAQ,KAAK,gBAAgB;AAAA,MAC7B,UAAU;AAAA,IACX;AAAA,IACA,UAAU,MAAM,QAAQ,KAAK,QAAQ,IAAI,KAAK,WAAW,CAAC;AAAA,IAC1D,UACC,KAAK,YAAY,OAAO,KAAK,aAAa,WACtC,KAAK,WACN,CAAC;AAAA,EACN;AACD;AAEO,SAAS,kBAAkB,KAAsC;AACvE,SAAO;AAAA,IACN,IAAI,IAAI;AAAA,IACR,gBAAgB,IAAI;AAAA,IACpB,cAAc,IAAI;AAAA,IAClB,MAAM,IAAI;AAAA,IACV,UAAU,IAAI;AAAA,IACd,QAAQ,IAAI;AAAA,IACZ,mBAAmB,IAAI;AAAA,IACvB,oBAAoB,IAAI;AAAA,IACxB,kBAAkB,IAAI;AAAA,IACtB,aAAa,IAAI;AAAA,IACjB,WAAW,IAAI;AAAA,EAChB;AACD;AAEO,SAAS,iBAAiB,QAAuC;AACvE,SAAO;AAAA,IACN,IAAI,OAAO;AAAA,IACX,gBAAgB,OAAO;AAAA,IACvB,cAAc,OAAO;AAAA,IACrB,QAAQ,OAAO;AAAA,IACf,UAAU,OAAO;AAAA,IACjB,YAAY,OAAO;AAAA,EACpB;AACD;;;AHlFO,SAAS,eAAe,OAAsB;AACpD,QAAM,QAAQ,IAAI,UAAU,KAAK;AAEjC,SAAO;AAAA,IACN,MAAM,KAAK,MAA2C;AACrD,YAAM,OAAO,MAAM,MAAM,KAAK;AAC9B,YAAM,OAAO,KAAK,IAAI,SAAS;AAC/B,iBAAO,4BAAe,iBAAK,IAAI,aAAa,aAAa,KAAK,MAAM,oBAAoB;AAAA,QACvF,OAAO;AAAA,MACR,CAAC;AAAA,IACF;AAAA,IAEA,MAAM,IAAI,KAA0C;AACnD,YAAM,SAAS,IAAI,KAAK,QAAQ;AAChC,YAAM,KAAK,SAAS,QAAQ;AAC5B,UAAI,CAAC,GAAI,YAAO,4BAAe,iBAAK,aAAa,qBAAqB,kBAAkB;AAExF,YAAM,OAAO,MAAM,MAAM,SAAS,EAAE;AACpC,UAAI,CAAC,KAAM,YAAO,4BAAe,iBAAK,WAAW,aAAa,gBAAgB;AAE9E,iBAAO,4BAAe,iBAAK,IAAI,gBAAgB,gCAAgC;AAAA,QAC9E,MAAM,UAAU,IAAI;AAAA,MACrB,CAAC;AAAA,IACF;AAAA,IAEA,MAAM,OAAO,KAA0C;AACtD,YAAM,OAAO,IAAI,KAAK,MAAM;AAC5B,YAAM,WAAO,2BAAc,OAAO,MAAM,CAAC;AACzC,UAAI,CAAC,MAAM;AACV,mBAAO,4BAAe,iBAAK,eAAe,oBAAoB,kBAAkB;AAAA,MACjF;AAEA,YAAM,OAAO,MAAM,MAAM,OAAO;AAAA,QAC/B;AAAA,QACA,aAAkB,OAAO,aAAa,KAAU,OAAO,OAAO,KAAK,aAAa,CAAC,IAAS;AAAA,QAC1F,MAAkB,OAAO,MAAM,KAAkB,WAAO,0BAAa,KAAK,MAAM,CAAC,IAAW;AAAA,QAC5F,OAAkB,OAAO,OAAO,KAAiB,WAAO,0BAAa,KAAK,OAAO,CAAC,IAAU;AAAA,QAC5F,WAAkB,OAAO,WAAW,KAAa,WAAO,0BAAa,KAAK,WAAW,CAAC,IAAM;AAAA,QAC5F,eAAkB,OAAO,eAAe,KAAS,WAAO,0BAAa,KAAK,eAAe,CAAC,IAAI;AAAA,QAC9F,gBAAkB,OAAO,gBAAgB,KAAO,OAAO,OAAO,KAAK,gBAAgB,CAAC,IAAM;AAAA,QAC1F,cAAkB,OAAO,cAAc,KAAU,WAAO,0BAAa,KAAK,cAAc,CAAC,IAAK;AAAA,QAC9F,eAAkB,OAAO,eAAe,KAAQ,OAAO,OAAO,KAAK,eAAe,CAAC,IAAO;AAAA,QAC1F,UAAkB,OAAO,UAAU;AAAA,QACnC,UAAkB,OAAO,UAAU;AAAA,MACpC,CAAC;AAED,iBAAO,4BAAe,iBAAK,SAAS,gBAAgB,8BAA8B;AAAA,QACjF,MAAM,UAAU,IAAI;AAAA,MACrB,CAAC;AAAA,IACF;AAAA,IAEA,MAAM,OAAO,KAA0C;AACtD,YAAM,SAAS,IAAI,KAAK,QAAQ;AAChC,YAAM,KAAK,SAAS,QAAQ;AAC5B,UAAI,CAAC,IAAI;AACR,mBAAO,4BAAe,iBAAK,aAAa,qBAAqB,kBAAkB;AAAA,MAChF;AAEA,YAAM,OAAO,IAAI,KAAK,MAAM;AAC5B,UAAI,CAAC,QAAQ,OAAO,KAAK,IAAI,EAAE,WAAW,GAAG;AAC5C,mBAAO,4BAAe,iBAAK,eAAe,oBAAoB,uBAAuB;AAAA,MACtF;AAEA,YAAM,QAAiC,CAAC;AACxC,YAAM,UAAU;AAAA,QAAC;AAAA,QAAQ;AAAA,QAAe;AAAA,QAAQ;AAAA,QAAS;AAAA,QACxD;AAAA,QAAiB;AAAA,QAAkB;AAAA,QAAgB;AAAA,QACnD;AAAA,QAAY;AAAA,MAAU;AAEvB,iBAAW,OAAO,SAAS;AAC1B,YAAI,OAAO,KAAM,OAAM,GAAG,IAAI,KAAK,GAAG;AAAA,MACvC;AAEA,YAAM,OAAO,MAAM,MAAM,OAAO,IAAI,KAAK;AACzC,UAAI,CAAC,MAAM;AACV,mBAAO,4BAAe,iBAAK,WAAW,aAAa,gBAAgB;AAAA,MACpE;AAEA,iBAAO,4BAAe,iBAAK,IAAI,gBAAgB,8BAA8B;AAAA,QAC5E,MAAM,UAAU,IAAI;AAAA,MACrB,CAAC;AAAA,IACF;AAAA,IAEA,MAAM,OAAO,KAA0C;AACtD,YAAM,SAAS,IAAI,KAAK,QAAQ;AAChC,YAAM,KAAK,SAAS,QAAQ;AAC5B,UAAI,CAAC,IAAI;AACR,mBAAO,4BAAe,iBAAK,aAAa,qBAAqB,kBAAkB;AAAA,MAChF;AAEA,YAAM,UAAU,MAAM,MAAM,OAAO,EAAE;AACrC,UAAI,CAAC,SAAS;AACb,mBAAO,4BAAe,iBAAK,WAAW,aAAa,gBAAgB;AAAA,MACpE;AAEA,iBAAO,4BAAe,iBAAK,IAAI,gBAAgB,4BAA4B;AAAA,IAC5E;AAAA,EACD;AACD;;;AIxGA,IAAAA,eAAqC;;;ACIrC,IAAM,sBAAsB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiB5B,eAAsB,gBACrB,gBACA,cACA,OACgC;AAChC,QAAM,CAAC,GAAG,IAAI,MAAM,MAAM;AAAA,IACzB,GAAG,mBAAmB;AAAA,IACtB,CAAC,gBAAgB,YAAY;AAAA,EAC9B;AACA,SAAO,OAAO;AACf;AAEA,eAAsB,mBACrB,MAaA,OACgB;AAChB,QAAM,MAAM;AAAA,IACX;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAgBA;AAAA,MACC,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK,YAAY;AAAA,MACjB,KAAK;AAAA,MACL,KAAK,sBAAsB;AAAA,MAC3B,KAAK,0BAA0B;AAAA,MAC/B,KAAK,sBAAsB;AAAA,MAC3B,KAAK,oBAAoB;AAAA,MACzB,KAAK,qBAAqB;AAAA,MAC1B,KAAK,eAAe;AAAA,IACrB;AAAA,EACD;AACD;;;AC3EO,IAAM,oBAAN,MAAwB;AAAA,EAC9B,YAA6B,OAAsB;AAAtB;AAAA,EAAuB;AAAA,EAAvB;AAAA,EAE7B,IAAI,gBAAgC,cAAqD;AACxF,WAAO,gBAAgB,gBAAgB,cAAc,KAAK,KAAK;AAAA,EAChE;AAAA,EAEA,OAAO,MAA+D;AACrE,WAAO,mBAAmB,MAAM,KAAK,KAAK;AAAA,EAC3C;AACD;;;ACLO,SAAS,cAAc,QAAwB;AACrD,QAAM,IAAI,SAAS,QAAQ,EAAE;AAC7B,QAAM,OAAO,OAAO,MAAM,OAAO,CAAC,EAAE,MAAM;AAC1C,UAAQ,MAAM;AAAA,IACb,KAAK;AACJ,aAAO,IAAI;AAAA,IACZ,KAAK;AACJ,aAAO,IAAI;AAAA,IACZ,KAAK;AACJ,aAAO,IAAI;AAAA,IACZ;AACC,YAAM,IAAI,MAAM,yBAAyB,IAAI,SAAS,MAAM,GAAG;AAAA,EACjE;AACD;AAIO,SAAS,kBAAkB,KAA2C;AAC5E,QAAM,eAAe,IAAI,QAAQ,QAAQ,IAAI,gBAAgB;AAE7D,MAAI,cAAc;AACjB,WAAO;AAAA,MACN,MAAM;AAAA,MACN,IAAI;AAAA,IACL;AAAA,EACD;AAEA,MAAI,IAAI,WAAW,IAAI;AACtB,WAAO;AAAA,MACN,MAAM;AAAA,MACN,IAAI,IAAI,UAAU;AAAA,IACnB;AAAA,EACD;AAEA,MAAI,IAAI,MAAM,IAAI;AACjB,WAAO;AAAA,MACN,MAAM;AAAA,MACN,IAAI,IAAI,KAAK;AAAA,IACd;AAAA,EACD;AAEA,SAAO;AACR;;;AH5CO,SAAS,uBAAuB,OAAsB;AAC5D,QAAM,gBAAgB,IAAI,kBAAkB,KAAK;AAEjD,SAAO;AAAA,IACN,MAAM,IAAI,KAA0C;AACnD,YAAM,aAAa,kBAAkB,GAAG;AACxC,UAAI,CAAC,YAAY;AAChB,mBAAO;AAAA,UACN,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,QACD;AAAA,MACD;AAEA,YAAM,eAAe,MAAM,cAAc,IAAI,WAAW,MAAM,WAAW,EAAE;AAC3E,UAAI,CAAC;AACJ,mBAAO,6BAAe,kBAAK,WAAW,aAAa,wBAAwB;AAE5E,iBAAO;AAAA,QACN,kBAAK;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA,UACC,cAAc,kBAAkB,YAAY;AAAA,QAC7C;AAAA,MACD;AAAA,IACD;AAAA,EACD;AACD;;;AIpCA,IAAAC,eAAqC;AAS9B,SAAS,mBAAmB,OAAsB,QAAwB;AAChF,QAAM,QAAQ,IAAI,UAAU,KAAK;AACjC,QAAM,gBAAgB,IAAI,kBAAkB,KAAK;AAEjD,SAAO;AAAA,IACN,MAAM,cAAc,KAA0C;AAC7D,YAAM,OAAO,IAAI,KAAK,MAAM;AAC5B,YAAM,WAAW,OAAO,MAAM;AAC9B,YAAM,WAAY,OAAO,UAAU,KAAK;AACxC,YAAM,aAAa,kBAAkB,GAAG;AAExC,UAAI,OAAO,aAAa,UAAU;AACjC,mBAAO,6BAAe,kBAAK,eAAe,qBAAqB,kBAAkB;AAAA,MAClF;AACA,UAAI,aAAa,WAAW,aAAa,QAAQ;AAChD,mBAAO;AAAA,UACN,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,QACD;AAAA,MACD;AACA,UAAI,CAAC,YAAY;AAChB,mBAAO;AAAA,UACN,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,QACD;AAAA,MACD;AAEA,YAAM,OAAO,MAAM,mBAAmB,UAAU,MAAM;AACtD,UAAI,CAAC,MAAM;AACV,mBAAO,6BAAe,kBAAK,eAAe,qBAAqB,iBAAiB,QAAQ,EAAE;AAAA,MAC3F;AAEA,YAAM,UAAU,aAAa,SAAS,KAAK,SAAS,KAAK;AACzD,UAAI,CAAC,SAAS,SAAS;AACtB,mBAAO;AAAA,UACN,kBAAK;AAAA,UACL;AAAA,UACA,QAAQ,QAAQ,qBAAqB,QAAQ;AAAA,QAC9C;AAAA,MACD;AAKA,YAAM,UAAU,MAAM,cAAc,IAAI,WAAW,MAAM,WAAW,EAAE;AACtE,YAAM,SAAS,CAAC,UAAU,YAAY,UAAU;AAChD,UAAI,WAAW,OAAO,SAAS,QAAQ,MAAM,GAAG;AAC/C,cAAM,cAAc,MAAM,mBAAmB,QAAQ,MAAM,MAAM,GAAG,QAAQ;AAC5E,cAAM,aAAa,KAAK,QAAQ;AAChC,YAAI,cAAc,aAAa;AAC9B,qBAAO;AAAA,YACN,kBAAK;AAAA,YACL;AAAA,YACA,sBAAsB,QAAQ,IAAI,6BAA6B,QAAQ;AAAA,UACxE;AAAA,QACD;AACA,YAAI,QAAQ,wBAAwB;AACnC,gBAAM,MAAM,MAAM,OAAO,SAAS,mBAAmB;AAAA,YACpD,gBAAgB,QAAQ;AAAA,YACxB,SAAS,QAAQ;AAAA,UAClB,CAAC;AACD,gBAAM,SAAqD;AAAA,YAC1D,gBAAgB,WAAW;AAAA,YAC3B,cAAc,WAAW;AAAA,YACzB,MAAM;AAAA,YACN;AAAA,YACA,QAAQ,IAAI;AAAA,YACZ,wBAAwB,QAAQ;AAAA,UACjC;AACA,cAAI,QAAQ,mBAAoB,QAAO,qBAAqB,QAAQ;AACpE,cAAI,IAAI,mBAAoB,QAAO,qBAAqB,IAAI;AAC5D,cAAI,IAAI,iBAAkB,QAAO,mBAAmB,IAAI;AACxD,gBAAM,cAAc,OAAO,MAAM;AACjC,qBAAO;AAAA,YACN,kBAAK;AAAA,YACL;AAAA,YACA;AAAA,YACA,EAAE,UAAU,MAAM,MAAM,SAAS;AAAA,UAClC;AAAA,QACD;AAAA,MACD;AAEA,YAAM,EAAE,WAAW,IAAI,MAAM,OAAO,SAAS,eAAe;AAAA,QAC3D,OAAO,IAAI,KAAM,SAAS;AAAA,QAC1B,gBAAgB,WAAW;AAAA,QAC3B,cAAc,WAAW;AAAA,QACzB,QAAQ,IAAI,KAAM;AAAA,MACnB,CAAC;AAED,YAAM,cAA2E;AAAA,QAChF;AAAA,QACA,SAAS,QAAQ;AAAA,QACjB,gBAAgB,WAAW;AAAA,QAC3B,cAAc,WAAW;AAAA,QACzB,YAAY,OAAO;AAAA,QACnB,WAAW,OAAO;AAAA,MACnB;AACA,UAAI,KAAK,cAAc,OAAW,aAAY,YAAY,KAAK;AAE/D,YAAM,EAAE,IAAI,IAAI,MAAM,OAAO,SAAS,sBAAsB,WAAW;AAEvE,YAAM,cAAc,OAAO;AAAA,QAC1B,gBAAgB,WAAW;AAAA,QAC3B,cAAc,WAAW;AAAA,QACzB,MAAM;AAAA,QACN;AAAA,QACA,QAAQ;AAAA,QACR,oBAAoB;AAAA,MACrB,CAAC;AAED,iBAAO,6BAAe,kBAAK,IAAI,gBAAgB,6BAA6B,EAAE,IAAI,CAAC;AAAA,IACpF;AAAA,IAEA,MAAM,aAAa,KAA0C;AAC5D,YAAM,aAAa,kBAAkB,GAAG;AACxC,UAAI,CAAC,YAAY;AAChB,mBAAO;AAAA,UACN,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,QACD;AAAA,MACD;AAEA,YAAM,eAAe,MAAM,cAAc,IAAI,WAAW,MAAM,WAAW,EAAE;AAC3E,UAAI,CAAC,cAAc,oBAAoB;AACtC,mBAAO,6BAAe,kBAAK,WAAW,aAAa,wBAAwB;AAAA,MAC5E;AAEA,YAAM,EAAE,IAAI,IAAI,MAAM,OAAO,SAAS,oBAAoB;AAAA,QACzD,YAAY,aAAa;AAAA,QACzB,WAAW,OAAO;AAAA,MACnB,CAAC;AAED,iBAAO,6BAAe,kBAAK,IAAI,cAAc,2BAA2B,EAAE,IAAI,CAAC;AAAA,IAChF;AAAA,EACD;AACD;;;ACnJA,IAAAC,eAAqC;;;ACGrC,eAAsB,YACrB,MACA,OACgB;AAChB,QAAM,MAAM;AAAA,IACX;AAAA;AAAA,IAEA,CAAC,KAAK,gBAAgB,KAAK,cAAc,KAAK,QAAQ,KAAK,QAAQ;AAAA,EACpE;AACD;AAEA,eAAsB,SACrB,gBACA,cACA,QACA,OACA,OACkB;AAClB,QAAM,OAAO,MAAM,MAAM;AAAA,IACxB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,CAAC,gBAAgB,cAAc,QAAQ,KAAK;AAAA,EAC7C;AACA,SAAO,SAAS,KAAK,CAAC,GAAG,SAAS,KAAK,EAAE;AAC1C;;;AC1BO,IAAM,aAAN,MAAiB;AAAA,EACvB,YAA6B,OAAsB;AAAtB;AAAA,EAAuB;AAAA,EAAvB;AAAA,EAE7B,OAAO,MAAwD;AAC9D,WAAO,YAAY,MAAM,KAAK,KAAK;AAAA,EACpC;AAAA,EAEA,IACC,gBACA,cACA,QACA,OACkB;AAClB,WAAO,SAAS,gBAAgB,cAAc,QAAQ,OAAO,KAAK,KAAK;AAAA,EACxE;AACD;;;AFbO,SAAS,gBAAgB,OAAsB;AACrD,QAAM,QAAQ,IAAI,WAAW,KAAK;AAElC,SAAO;AAAA,IACN,MAAM,OAAO,KAA0C;AACtD,YAAM,OAAO,IAAI,KAAK,MAAM;AAC5B,YAAM,SAAS,OAAO,QAAQ;AAC9B,YAAM,WAAW,OAAO,UAAU;AAClC,YAAM,aAAa,kBAAkB,GAAG;AAExC,UAAI,CAAC,YAAY;AAChB,mBAAO;AAAA,UACN,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,QACD;AAAA,MACD;AACA,UAAI,OAAO,WAAW,UAAU;AAC/B,mBAAO,6BAAe,kBAAK,eAAe,qBAAqB,oBAAoB;AAAA,MACpF;AAEA,YAAM,MAAM,OAAO;AAAA,QAClB,gBAAgB,WAAW;AAAA,QAC3B,cAAc,WAAW;AAAA,QACzB;AAAA,QACA,UAAU,OAAO,aAAa,WAAW,WAAW;AAAA,MACrD,CAAC;AACD,iBAAO,6BAAe,kBAAK,IAAI,kBAAkB,8BAA8B;AAAA,IAChF;AAAA,IAEA,MAAM,IAAI,KAA0C;AACnD,YAAM,SAAS,IAAI,KAAK,QAAQ;AAChC,YAAM,SAAS,SAAS,QAAQ;AAChC,YAAM,aAAa,kBAAkB,GAAG;AAExC,UAAI,CAAC,cAAc,CAAC,QAAQ;AAC3B,mBAAO;AAAA,UACN,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,QACD;AAAA,MACD;AAEA,YAAM,QAAQ,oBAAI,KAAK;AACvB,YAAM,QAAQ,CAAC;AACf,YAAM,SAAS,GAAG,GAAG,GAAG,CAAC;AAEzB,YAAM,QAAQ,MAAM,MAAM,IAAI,WAAW,MAAM,WAAW,IAAI,QAAQ,KAAK;AAC3E,iBAAO,6BAAe,kBAAK,IAAI,iBAAiB,iCAAiC;AAAA,QAChF;AAAA,QACA;AAAA,QACA;AAAA,MACD,CAAC;AAAA,IACF;AAAA,EACD;AACD;;;AG9DA,IAAAC,eAAqC;AAO9B,SAAS,kBAAkB,OAAsB,QAAwB;AAC/E,QAAM,gBAAgB,IAAI,kBAAkB,KAAK;AAEjD,SAAO;AAAA,IACN,MAAM,OAAO,KAA0C;AACtD,UAAI,CAAC,OAAO,eAAe;AAC1B,mBAAO,6BAAe,kBAAK,cAAc,gBAAgB,+BAA+B;AAAA,MACzF;AAEA,YAAM,YACL,IAAI,QAAQ,QAAQ,IAAI,kBAAkB,KAC1C,IAAI,QAAQ,QAAQ,IAAI,kBAAkB,KAC1C;AAED,UAAI,CAAC,WAAW;AACf,mBAAO,6BAAe,kBAAK,aAAa,mBAAmB,2BAA2B;AAAA,MACvF;AAEA,YAAM,UAAU,MAAM,IAAI,QAAQ,KAAK;AAEvC,UAAI;AACJ,UAAI;AACH,gBAAQ,MAAM,OAAO,SAAS,eAAe;AAAA,UAC5C;AAAA,UACA;AAAA,UACA,QAAQ,OAAO;AAAA,QAChB,CAAC;AAAA,MACF,QAAQ;AACP,mBAAO,6BAAe,kBAAK,aAAa,mBAAmB,2BAA2B;AAAA,MACvF;AAEA,UAAI,MAAM,cAAc;AACvB,cAAM,cAAc,OAAO;AAAA,UAC1B,gBAAgB,MAAM,aAAa;AAAA,UACnC,cAAc,MAAM,aAAa;AAAA,UACjC,MAAM,MAAM,aAAa;AAAA,UACzB,UAAU,MAAM,aAAa;AAAA,UAC7B,QAAQ,MAAM,aAAa;AAAA,UAC3B,oBAAoB,MAAM,aAAa;AAAA,UACvC,wBAAwB,MAAM,aAAa;AAAA,UAC3C,oBAAoB,MAAM,aAAa;AAAA,UACvC,kBAAkB,MAAM,aAAa;AAAA,UACrC,mBAAmB,MAAM,aAAa;AAAA,UACtC,aAAa,MAAM,aAAa;AAAA,QACjC,CAAC;AAAA,MACF;AAEA,aAAO,SAAS,KAAK,EAAE,UAAU,KAAK,CAAC;AAAA,IACxC;AAAA,EACD;AACD;;;Ad1CO,SAAS,mBACf,OACA,QACoB;AACpB,QAAM,OAAO,eAAe,KAAK;AACjC,QAAM,eAAe,uBAAuB,KAAK;AACjD,QAAM,WAAW,mBAAmB,OAAO,MAAM;AACjD,QAAM,QAAQ,gBAAgB,KAAK;AACnC,QAAM,UAAU,kBAAkB,OAAO,MAAM;AAE/C,SAAO;AAAA;AAAA,IAEN,CAAC,OAAO,UAAU,KAAK,IAAI;AAAA,IAC3B,CAAC,OAAO,kBAAkB,KAAK,GAAG;AAAA;AAAA,IAGlC,CAAC,QAAQ,cAAU,6BAAS,gBAAgB,GAAG,KAAK,MAAM;AAAA,IAC1D,CAAC,OAAO,sBAAkB,6BAAS,gBAAgB,GAAG,KAAK,MAAM;AAAA,IACjE,CAAC,UAAU,kBAAkB,KAAK,MAAM;AAAA;AAAA;AAAA,IAIxC,CAAC,OAAO,yBAAyB,gCAAa,aAAa,GAAG;AAAA,IAC9D,CAAC,QAAQ,qBAAqB,oCAAa,6BAAS,cAAc,GAAG,SAAS,aAAa;AAAA,IAC3F,CAAC,QAAQ,mBAAmB,gCAAa,SAAS,YAAY;AAAA,IAC9D,CAAC,QAAQ,kBAAkB,oCAAa,6BAAS,iBAAiB,GAAG,MAAM,MAAM;AAAA,IACjF,CAAC,OAAO,0BAA0B,gCAAa,MAAM,GAAG;AAAA;AAAA,IAGxD,CAAC,QAAQ,oBAAoB,QAAQ,MAAM;AAAA,EAC5C;AACD;;;Ae7CA,IAAAC,eAAqC;;;AC0C9B,IAAM,eAAe;AAAA,EAC3B,cAAc;AAAA,EACd,cAAc;AAAA,EACd,cAAc;AACf;;;AC3CO,SAAS,oBAAoB,MAOhB;AACnB,QAAM,EAAE,YAAY,MAAM,QAAQ,SAAS,IAAI;AAC/C,QAAM,WAAW,KAAK,YAAY,CAAC;AACnC,QAAM,WAA0C,CAAC;AAEjD,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,UAAU,CAAC,CAAC,GAAG;AAC7D,QAAI,aAAa,OAAO;AACvB,eAAS,GAAG,IAAI,EAAE,MAAM,WAAW,SAAS,MAAM,QAAQ;AAC1D;AAAA,IACD;AAEA,UAAM,EAAE,OAAO,SAAS,SAAS,UAAU,GAAG,SAAS,SAAS,UAAU,KAAK,OAAO,IAAI;AAE1F,UAAM,OAAO,SAAS,GAAG,KAAK;AAC9B,UAAM,YAAY,UAAU,OAAO,QAAQ,SAAS;AAEpD,QAAI,SAAsB;AAC1B,QAAI,cAAc,QAAQ,QAAQ,UAAW,UAAS;AAAA,aAC7C,UAAU,QAAQ,QAAQ,MAAO,UAAS;AAAA,aAC1C,UAAU,QAAQ,QAAQ,QAAQ,OAAQ,UAAS;AAE5D,QAAI,WAA0B;AAC9B,QAAI,QAAQ;AACX,YAAM,WAAW,cAAc,MAAM;AACrC,YAAM,cAAc,KAAK,MAAM,KAAK,IAAI,IAAI,QAAQ,IAAI;AACxD,iBAAW,IAAI,KAAK,cAAc,QAAQ,EAAE,YAAY;AAAA,IACzD;AAEA,aAAS,GAAG,IAAI,EAAE,MAAM,WAAW,OAAO,MAAM,QAAQ,SAAS;AAAA,EAClE;AAEA,SAAO,EAAE,YAAY,MAAM,KAAK,MAAM,QAAQ,SAAS;AACxD;;;AF9BA,IAAM,WAAW,oBAAI,IAAY;AAE1B,SAAS,YACf,OACA,QACA,SACa;AACb,SAAO,OAAO,KAAK,SAAS;AAC3B,UAAM,aAAa,kBAAkB,GAAG;AAGxC,QAAI,CAAC,WAAY,QAAO,KAAK;AAG7B,UAAM,eAAe,MAAM,gBAAgB,WAAW,MAAM,WAAW,IAAI,KAAK;AAChF,UAAM,WAAW,cAAc,QAAQ,OAAO,MAAM,CAAC,GAAG,QAAQ;AAChE,UAAM,SACL,CAAC,gBAAgB,aAAa,WAAW,YAAY,aAAa,WAAW;AAE9E,UAAM,OAAO,OAAO,MAAM,KAAK,CAAC,MAAM,EAAE,SAAS,QAAQ,KAAK,OAAO,MAAM,CAAC;AAC5E,QAAI,CAAC,KAAM,QAAO,KAAK;AAGvB,UAAM,WAAmC,CAAC;AAE1C,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,UAAU,CAAC,CAAC,GAAG;AAC7D,UAAI,aAAa,SAAS,CAAC,MAAM,OAAQ;AAEzC,YAAM,WAAW,cAAc,MAAM,MAAM;AAC3C,YAAM,aAAa,GAAG,WAAW,IAAI,IAAI,WAAW,EAAE,IAAI,GAAG;AAC7D,eAAS,GAAG,IAAI,MAAM,QAAQ,UAAU,YAAY,QAAQ;AAAA,IAC7D;AAGA,UAAM,aAAa,oBAAoB,EAAE,YAAY,MAAM,QAAQ,SAAS,CAAC;AAC7E,QAAI,KAAK,SAAS,IAAI;AAGtB,eAAW,CAAC,KAAK,MAAM,KAAK,OAAO,QAAQ,WAAW,QAAQ,GAAG;AAChE,UAAI,OAAO,SAAS,aAAa,OAAO,WAAW,WAAW;AAC7D,mBAAO;AAAA,UACN,kBAAK;AAAA,UACL;AAAA,UACA,uBAAuB,GAAG;AAAA,UAC1B,EAAE,KAAK,OAAO,OAAO,OAAO,MAAM,OAAO,MAAM,UAAU,OAAO,SAAS;AAAA,QAC1E;AAAA,MACD;AAAA,IACD;AAGA,QAAI,OAAO,eAAe;AACzB,YAAM,WAA8B,CAAC;AACrC,YAAM,YAAY;AAAA,QACjB,OAAO,IAAI,MAAM,SAAS;AAAA,QAC1B,OAAO;AAAA,QACP,aAAa;AAAA,MACd;AAEA,iBAAW,CAAC,KAAK,MAAM,KAAK,OAAO,QAAQ,WAAW,QAAQ,GAAG;AAChE,YAAI,OAAO,SAAS,aAAa,OAAO,UAAU,KAAM;AAExD,cAAM,OAAO,GAAG,WAAW,IAAI,IAAI,WAAW,EAAE,IAAI,GAAG;AAEvD,YAAI,OAAO,cAAc,WAAW,OAAO,WAAW,cAAc;AACnE,gBAAM,KAAK,GAAG,IAAI;AAClB,cAAI,CAAC,SAAS,IAAI,EAAE,GAAG;AACtB,qBAAS,IAAI,EAAE;AACf,qBAAS,KAAK;AAAA,cACb,MAAM,aAAa;AAAA,cACnB;AAAA,cACA,MAAM;AAAA,gBACL;AAAA,gBACA,MAAM,KAAK;AAAA,gBACX,OAAO,OAAO;AAAA,gBACd,MAAM,OAAO;AAAA,cACd;AAAA,YACD,CAAC;AAAA,UACF;AAAA,QACD,WAAW,OAAO,cAAc,UAAU,OAAO,WAAW,WAAW;AACtE,gBAAM,KAAK,GAAG,IAAI;AAClB,cAAI,CAAC,SAAS,IAAI,EAAE,GAAG;AACtB,qBAAS,IAAI,EAAE;AACf,qBAAS,KAAK;AAAA,cACb,MAAM,aAAa;AAAA,cACnB;AAAA,cACA,MAAM;AAAA,gBACL;AAAA,gBACA,MAAM,KAAK;AAAA,gBACX,OAAO,OAAO;AAAA,gBACd,MAAM,OAAO;AAAA,cACd;AAAA,YACD,CAAC;AAAA,UACF;AAAA,QACD;AAAA,MACD;AAEA,UAAI,SAAS,SAAS,GAAG;AACxB,cAAM,WAAW,IAAI,KAAK,UAAU;AACpC,YAAI,KAAK,UAAU,IAAI,CAAC,GAAI,YAAY,CAAC,GAAI,GAAG,QAAQ;AAAA,MACzD;AAAA,IACD;AAEA,WAAO,KAAK;AAAA,EACb;AACD;;;AG9GO,IAAM,uBAAN,MAAsD;AAAA,EAC3C,WAAW,oBAAI,IAAmB;AAAA,EAEnD,MAAM,UAAU,KAAa,UAAyB,WAAW,GAAoB;AACpF,UAAM,MAAM,KAAK,IAAI;AACrB,UAAM,WAAW,KAAK,SAAS,IAAI,GAAG;AAEtC,QAAI,CAAC,YAAa,aAAa,QAAQ,MAAM,SAAS,eAAe,UAAW;AAC/E,WAAK,SAAS,IAAI,KAAK,EAAE,OAAO,UAAU,aAAa,IAAI,CAAC;AAC5D,aAAO;AAAA,IACR;AAEA,aAAS,SAAS;AAClB,WAAO,SAAS;AAAA,EACjB;AAAA,EAEA,MAAM,IAAI,KAAa,UAA0C;AAChE,UAAM,MAAM,KAAK,IAAI;AACrB,UAAM,WAAW,KAAK,SAAS,IAAI,GAAG;AACtC,QAAI,CAAC,SAAU,QAAO;AACtB,QAAI,aAAa,QAAQ,MAAM,SAAS,eAAe,SAAU,QAAO;AACxE,WAAO,SAAS;AAAA,EACjB;AACD;;;AC3BO,IAAM,mBAAN,MAAkD;AAAA,EACxD,YAA6B,OAAsB;AAAtB;AAAA,EAAuB;AAAA,EAAvB;AAAA,EAE7B,MAAM,UAAU,KAAa,UAAyB,WAAW,GAAoB;AACpF,UAAM,CAAC,gBAAgB,cAAc,GAAG,IAAI,IAAI,IAAI,MAAM,GAAG;AAC7D,UAAM,SAAS,KAAK,KAAK,GAAG;AAE5B,UAAM,KAAK,MAAM;AAAA,MAChB;AAAA;AAAA,MAEA,CAAC,gBAAgB,cAAc,QAAQ,QAAQ;AAAA,IAChD;AAEA,WAAO,KAAK,IAAI,KAAK,QAAQ;AAAA,EAC9B;AAAA,EAEA,MAAM,IAAI,KAAa,UAA0C;AAChE,UAAM,CAAC,gBAAgB,cAAc,GAAG,IAAI,IAAI,IAAI,MAAM,GAAG;AAC7D,UAAM,SAAS,KAAK,KAAK,GAAG;AAC5B,UAAM,QAAQ,aAAa,OAAO,IAAI,KAAK,KAAK,IAAI,IAAI,QAAQ,IAAI,oBAAI,KAAK,CAAC;AAE9E,UAAM,OAAO,MAAM,KAAK,MAAM;AAAA,MAC7B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAMA,CAAC,gBAAgB,cAAc,QAAQ,KAAK;AAAA,IAC7C;AAEA,WAAO,SAAS,KAAK,CAAC,GAAG,SAAS,KAAK,EAAE;AAAA,EAC1C;AACD;;;AC/BO,SAAS,cAAc,QAA4C,OAAsB;AAC/F,MAAI,CAAC,UAAU,WAAW,SAAU,QAAO,IAAI,qBAAqB;AACpE,MAAI,WAAW,KAAM,QAAO,IAAI,iBAAiB,KAAK;AACtD,SAAO;AACR;;;ACAO,IAAM,gBAAN,MAA+C;AAAA,EAIrD,YACS,OACA,QACP;AAFO;AACA;AAAA,EACN;AAAA,EAFM;AAAA,EACA;AAAA,EALA,OAAO;AAAA,EACP,OAAO,CAAC,gBAAgB;AAAA,EAOjC,MAAM,QAAQ,KAAkC;AAC/C,UAAM,cAAc,KAAK,QAAQ,KAAK,KAAK;AAE3C,UAAM,UAAU,cAAc,KAAK,OAAO,WAAW,SAAS,KAAK,KAAK;AAKxE,QAAI,IAAI,YAAY,KAAK,OAAO,KAAK,QAAQ,OAAO,CAAC;AAErD,UAAM,SAAS,mBAAmB,KAAK,OAAO,KAAK,MAAM;AACzD,eAAW,CAAC,QAAQ,MAAM,GAAG,QAAQ,KAAK,QAAQ;AACjD,UAAI,SAAS,QAAQ,MAAM,GAAG,QAAQ;AAAA,IACvC;AAAA,EACD;AACD;;;ACJA,IAAI,UAAmB;AAEvB,eAAe,UAAU,WAAqC;AAC7D,MAAI,QAAS,QAAO;AAEpB,QAAM,MAAM;AAGZ,QAAM,MAAW,MAAM,OAAO,KAAK,MAAM,MAAM;AAC9C,UAAM,IAAI,MAAM,yDAAyD;AAAA,EAC1E,CAAC;AAED,QAAM,SAAS,IAAI,WAAW;AAC9B,YAAU,IAAI,OAAO,WAAW,EAAE,YAAY,oBAAoB,CAAC;AACnE,SAAO;AACR;AAEA,SAAS,sBAAsB,KAAsD;AACpF,QAAM,OAAO,IAAI,MAAM,KAAK,CAAC;AAG7B,QAAM,cAAc,MAAM,wBAAwB,IAAI;AACtD,QAAM,YAAY,MAAM,sBAAsB,IAAI;AAClD,SAAO;AAAA,IACN,gBAAiB,IAAI,WAAW,gBAAgB,KAAK;AAAA,IACrD,cAAc,IAAI,WAAW,cAAc,KAAK;AAAA,IAChD,MAAM,MAAM,MAAM,YAAY;AAAA,IAC9B,QAAQ,IAAI;AAAA,IACZ,oBAAoB,IAAI;AAAA,IACxB,wBAAwB,IAAI;AAAA,IAC5B,oBAAoB,cAAc,IAAI,KAAK,cAAc,GAAI,IAAI,oBAAI,KAAK;AAAA,IAC1E,kBAAkB,YAAY,IAAI,KAAK,YAAY,GAAI,IAAI,oBAAI,KAAK;AAAA,IACpE,mBAAmB,IAAI;AAAA,IACvB,aAAa,IAAI,YAAY,IAAI,KAAK,IAAI,YAAY,GAAI,IAAI;AAAA,IAC9D,UAAU,MAAM,MAAM,WAAW,aAAa,SAAS,SAAS;AAAA,EACjE;AACD;AAEO,IAAM,iBAAN,MAAiD;AAAA,EAGvD,YACS,WACA,eACP;AAFO;AACA;AAAA,EACN;AAAA,EAFM;AAAA,EACA;AAAA,EAJA,OAAO;AAAA,EAOhB,MAAc,SAAuB;AACpC,WAAO,UAAU,KAAK,SAAS;AAAA,EAChC;AAAA,EAEA,MAAM,eAAe,MAKe;AACnC,UAAM,SAAS,MAAM,KAAK,OAAO;AACjC,UAAM,WAAW,MAAM,OAAO,UAAU,OAAO;AAAA,MAC9C,OAAO,KAAK;AAAA,MACZ,UAAU;AAAA,QACT,gBAAgB,KAAK;AAAA,QACrB,cAAc,KAAK;AAAA,QACnB,QAAQ,KAAK;AAAA,MACd;AAAA,IACD,CAAC;AACD,WAAO,EAAE,YAAY,SAAS,GAAG;AAAA,EAClC;AAAA,EAEA,MAAM,sBAAsB,MAQC;AAC5B,UAAM,SAAS,MAAM,KAAK,OAAO;AACjC,UAAM,UAAU,MAAM,OAAO,SAAS,SAAS,OAAO;AAAA,MACrD,UAAU,KAAK;AAAA,MACf,MAAM;AAAA,MACN,YAAY,CAAC,EAAE,OAAO,KAAK,SAAS,UAAU,EAAE,CAAC;AAAA,MACjD,aAAa,KAAK;AAAA,MAClB,YAAY,KAAK;AAAA,MACjB,mBAAmB;AAAA,QAClB,UAAU;AAAA,UACT,gBAAgB,KAAK;AAAA,UACrB,cAAc,KAAK;AAAA,QACpB;AAAA,QACA,GAAI,KAAK,aAAa,KAAK,YAAY,IAAI,EAAE,mBAAmB,KAAK,UAAU,IAAI,CAAC;AAAA,MACrF;AAAA,IACD,CAAC;AACD,WAAO,EAAE,KAAK,QAAQ,OAAO,GAAG;AAAA,EACjC;AAAA,EAEA,MAAM,mBAAmB,MAGuE;AAC/F,UAAM,SAAS,MAAM,KAAK,OAAO;AACjC,UAAM,MAAM,MAAM,OAAO,cAAc,SAAS,KAAK,cAAc;AACnE,UAAM,SAAS,IAAI,MAAM,KAAK,CAAC,GAAG;AAGlC,UAAM,UAAU,MAAM,OAAO,cAAc,OAAO,KAAK,gBAAgB;AAAA,MACtE,OAAO,CAAC,EAAE,IAAI,QAAQ,OAAO,KAAK,QAAQ,CAAC;AAAA,MAC3C,oBAAoB;AAAA,MACpB,kBAAkB;AAAA,IACnB,CAAC;AACD,UAAM,OAAO,QAAQ,OAAO,OAAO,CAAC;AACpC,UAAM,MAAM,MAAM,wBAAwB,QAAQ;AAClD,UAAM,MAAM,MAAM,sBAAsB,QAAQ;AAChD,WAAO;AAAA,MACN,QAAQ,QAAQ;AAAA,MAChB,oBAAoB,MAAM,IAAI,KAAK,MAAM,GAAI,IAAI;AAAA,MACjD,kBAAkB,MAAM,IAAI,KAAK,MAAM,GAAI,IAAI;AAAA,IAChD;AAAA,EACD;AAAA,EAEA,MAAM,oBAAoB,MAGG;AAC5B,UAAM,SAAS,MAAM,KAAK,OAAO;AACjC,UAAM,UAAU,MAAM,OAAO,cAAc,SAAS,OAAO;AAAA,MAC1D,UAAU,KAAK;AAAA,MACf,YAAY,KAAK;AAAA,IAClB,CAAC;AACD,WAAO,EAAE,KAAK,QAAQ,IAAI;AAAA,EAC3B;AAAA,EAEA,MAAM,eAAe,MAIM;AAC1B,UAAM,SAAS,MAAM,KAAK,OAAO;AAEjC,QAAI;AACJ,QAAI;AACH,YAAM,OAAO,SAAS,eAAe,KAAK,SAAS,KAAK,WAAW,KAAK,MAAM;AAAA,IAC/E,QAAQ;AACP,YAAM,IAAI,MAAM,4CAA4C;AAAA,IAC7D;AAEA,UAAM,sBAAsB;AAAA,MAC3B;AAAA,MACA;AAAA,MACA;AAAA,IACD,EAAE,SAAS,IAAI,IAAI;AAEnB,QAAI,CAAC,qBAAqB;AACzB,aAAO,EAAE,MAAM,IAAI,MAAM,cAAc,KAAK;AAAA,IAC7C;AAEA,UAAM,MAAM,IAAI,KAAK;AAErB,QAAI,IAAI,SAAS,iCAAiC;AACjD,aAAO;AAAA,QACN,MAAM,IAAI;AAAA,QACV,cAAc,EAAE,GAAG,sBAAsB,GAAG,GAAG,MAAM,QAAQ,QAAQ,WAAW;AAAA,MACjF;AAAA,IACD;AAEA,WAAO,EAAE,MAAM,IAAI,MAAM,cAAc,sBAAsB,GAAG,EAAE;AAAA,EACnE;AACD;;;ACnMA,IAAAC,eAAqC;AAYrC,SAAS,YAAY,OAA0B,OAAkC;AAChF,QAAM,UAAU,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK;AAErD,SAAO,OAAO,KAAK,SAAS;AAC3B,QAAI,CAAC,IAAI,MAAM;AACd,iBAAO,6BAAe,kBAAK,cAAc,gBAAgB,cAAc;AAAA,IACxE;AAEA,UAAM,aAAa,kBAAkB,GAAG;AACxC,QAAI,CAAC,YAAY;AAChB,iBAAO,6BAAe,kBAAK,aAAa,uBAAuB,6BAA6B;AAAA,IAC7F;AAEA,UAAM,eAAe,MAAM,gBAAgB,WAAW,MAAM,WAAW,IAAI,KAAK;AAEhF,QAAI,CAAC,gBAAgB,CAAC,QAAQ,SAAS,aAAa,IAAI,GAAG;AAC1D,iBAAO;AAAA,QACN,kBAAK;AAAA,QACL;AAAA,QACA;AAAA,QACA,EAAE,UAAU,SAAS,SAAS,cAAc,QAAQ,OAAO;AAAA,MAC5D;AAAA,IACD;AAEA,QAAI,aAAa,WAAW,YAAY,aAAa,WAAW,YAAY;AAC3E,iBAAO;AAAA,QACN,kBAAK;AAAA,QACL;AAAA,QACA;AAAA,QACA,EAAE,QAAQ,aAAa,OAAO;AAAA,MAC/B;AAAA,IACD;AAEA,WAAO,KAAK;AAAA,EACb;AACD;AASO,SAAS,YACf,OACA,OACA,KACA,MACiC;AACjC,QAAM,UAAU,YAAY,OAAO,KAAK;AACxC,MAAI,QAAQ,UAAa,SAAS,OAAW,QAAO,QAAQ,KAAK,IAAI;AACrE,SAAO;AACR;;;ACjEA,IAAAC,eAAqC;AAKrC,SAAS,kBAAkB,KAA+C;AACzE,SAAQ,IAAI,KAAK,SAAS,KAAqC;AAChE;AAIO,SAAS,WAAW,KAAuB,KAAsB;AACvE,QAAM,UAAU,kBAAkB,GAAG;AACrC,MAAI,CAAC,QAAS,QAAO;AAErB,QAAM,SAAS,QAAQ,SAAS,GAAG;AACnC,MAAI,CAAC,OAAQ,QAAO;AACpB,MAAI,OAAO,SAAS,UAAW,QAAO,OAAO;AAC7C,SAAO;AACR;AAGO,SAAS,aAAa,KAAuB,KAA4B;AAC/E,QAAM,UAAU,kBAAkB,GAAG;AACrC,MAAI,CAAC,QAAS,QAAO;AAErB,QAAM,SAAS,QAAQ,SAAS,GAAG;AACnC,MAAI,CAAC,UAAU,OAAO,SAAS,UAAW,QAAO;AACjD,SAAO,OAAO;AACf;AAGO,SAAS,eAAe,KAAuB,KAAmC;AACxF,QAAM,UAAU,kBAAkB,GAAG;AACrC,MAAI,CAAC,QAAS,QAAO;AACrB,SAAO,QAAQ,SAAS,GAAG,KAAK;AACjC;AAKO,SAAS,eAAe,KAAyB;AACvD,SAAO,CAAC,KAAK,SAAS;AACrB,QAAI,CAAC,WAAW,KAAK,GAAG,GAAG;AAC1B,aAAO,QAAQ;AAAA,YACd;AAAA,UACC,kBAAK;AAAA,UACL;AAAA,UACA,YAAY,GAAG;AAAA,QAChB;AAAA,MACD;AAAA,IACD;AACA,WAAO,KAAK;AAAA,EACb;AACD;","names":["import_core","import_core","import_core","import_core","import_core","import_core","import_core"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { IFonderieModule, IFonderieApp, IFonderieContext, Middleware } from '@fonderie/core';
|
|
2
2
|
import { IStoreAdapter } from '@fonderie/store';
|
|
3
|
-
import { I as IBillingConfig, a as IBillingProvider, b as IBillingEvent, c as ICounterBackend, d as IBillingPlan } from './index-
|
|
4
|
-
export { B as BillingMessageKey, e as IBillingNotificationsConfig, f as IBillingPlanDefaults, M as MESSAGE_KEYS, R as RateLimitBackendConfig, r as requirePlan, w as withBilling } from './index-
|
|
3
|
+
import { I as IBillingConfig, a as IBillingProvider, b as IBillingEvent, c as ICounterBackend, d as IBillingPlan } from './index-CjwZwEVR.cjs';
|
|
4
|
+
export { B as BillingMessageKey, e as IBillingNotificationsConfig, f as IBillingPlanDefaults, M as MESSAGE_KEYS, R as RateLimitBackendConfig, r as requirePlan, w as withBilling } from './index-CjwZwEVR.cjs';
|
|
5
5
|
import { SubscriberType, IPolicyStatus, IPlanFeature, IPlan, ISubscription, IUsageRecord } from './types.cjs';
|
|
6
6
|
export { IBillingContext, LimitStatus, PolicyEntry, SubscriptionStatus } from './types.cjs';
|
|
7
7
|
import { z } from 'zod';
|
|
@@ -40,6 +40,14 @@ declare class StripeProvider implements IBillingProvider {
|
|
|
40
40
|
}): Promise<{
|
|
41
41
|
url: string;
|
|
42
42
|
}>;
|
|
43
|
+
updateSubscription(opts: {
|
|
44
|
+
subscriptionId: string;
|
|
45
|
+
priceId: string;
|
|
46
|
+
}): Promise<{
|
|
47
|
+
status: string;
|
|
48
|
+
currentPeriodStart: Date | null;
|
|
49
|
+
currentPeriodEnd: Date | null;
|
|
50
|
+
}>;
|
|
43
51
|
createPortalSession(opts: {
|
|
44
52
|
customerId: string;
|
|
45
53
|
returnUrl: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { IFonderieModule, IFonderieApp, IFonderieContext, Middleware } from '@fonderie/core';
|
|
2
2
|
import { IStoreAdapter } from '@fonderie/store';
|
|
3
|
-
import { I as IBillingConfig, a as IBillingProvider, b as IBillingEvent, c as ICounterBackend, d as IBillingPlan } from './index-
|
|
4
|
-
export { B as BillingMessageKey, e as IBillingNotificationsConfig, f as IBillingPlanDefaults, M as MESSAGE_KEYS, R as RateLimitBackendConfig, r as requirePlan, w as withBilling } from './index-
|
|
3
|
+
import { I as IBillingConfig, a as IBillingProvider, b as IBillingEvent, c as ICounterBackend, d as IBillingPlan } from './index-BvYwTSIF.js';
|
|
4
|
+
export { B as BillingMessageKey, e as IBillingNotificationsConfig, f as IBillingPlanDefaults, M as MESSAGE_KEYS, R as RateLimitBackendConfig, r as requirePlan, w as withBilling } from './index-BvYwTSIF.js';
|
|
5
5
|
import { SubscriberType, IPolicyStatus, IPlanFeature, IPlan, ISubscription, IUsageRecord } from './types.js';
|
|
6
6
|
export { IBillingContext, LimitStatus, PolicyEntry, SubscriptionStatus } from './types.js';
|
|
7
7
|
import { z } from 'zod';
|
|
@@ -40,6 +40,14 @@ declare class StripeProvider implements IBillingProvider {
|
|
|
40
40
|
}): Promise<{
|
|
41
41
|
url: string;
|
|
42
42
|
}>;
|
|
43
|
+
updateSubscription(opts: {
|
|
44
|
+
subscriptionId: string;
|
|
45
|
+
priceId: string;
|
|
46
|
+
}): Promise<{
|
|
47
|
+
status: string;
|
|
48
|
+
currentPeriodStart: Date | null;
|
|
49
|
+
currentPeriodEnd: Date | null;
|
|
50
|
+
}>;
|
|
43
51
|
createPortalSession(opts: {
|
|
44
52
|
customerId: string;
|
|
45
53
|
returnUrl: string;
|
package/dist/index.js
CHANGED
|
@@ -544,6 +544,43 @@ function checkoutController(store, config) {
|
|
|
544
544
|
`Plan ${planName} does not support ${interval} billing`
|
|
545
545
|
);
|
|
546
546
|
}
|
|
547
|
+
const current = await subscriptions.get(subscriber.type, subscriber.id);
|
|
548
|
+
const ACTIVE = ["active", "trialing", "past_due"];
|
|
549
|
+
if (current && ACTIVE.includes(current.status)) {
|
|
550
|
+
const currentTier = plans.findByNameInConfig(current.plan, config)?.tier ?? -1;
|
|
551
|
+
const targetTier = plan.tier ?? -1;
|
|
552
|
+
if (targetTier <= currentTier) {
|
|
553
|
+
return setApiResponse3(
|
|
554
|
+
HTTP3.UNPROCESSABLE,
|
|
555
|
+
"DOWNGRADE_NOT_ALLOWED",
|
|
556
|
+
`Cannot switch from ${current.plan} to a same-or-lower tier (${planName}) mid-cycle. Upgrades only.`
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
if (current.providerSubscriptionId) {
|
|
560
|
+
const res = await config.provider.updateSubscription({
|
|
561
|
+
subscriptionId: current.providerSubscriptionId,
|
|
562
|
+
priceId: pricing.priceId
|
|
563
|
+
});
|
|
564
|
+
const upsert = {
|
|
565
|
+
subscriberType: subscriber.type,
|
|
566
|
+
subscriberId: subscriber.id,
|
|
567
|
+
plan: planName,
|
|
568
|
+
interval,
|
|
569
|
+
status: res.status,
|
|
570
|
+
providerSubscriptionId: current.providerSubscriptionId
|
|
571
|
+
};
|
|
572
|
+
if (current.providerCustomerId) upsert.providerCustomerId = current.providerCustomerId;
|
|
573
|
+
if (res.currentPeriodStart) upsert.currentPeriodStart = res.currentPeriodStart;
|
|
574
|
+
if (res.currentPeriodEnd) upsert.currentPeriodEnd = res.currentPeriodEnd;
|
|
575
|
+
await subscriptions.upsert(upsert);
|
|
576
|
+
return setApiResponse3(
|
|
577
|
+
HTTP3.OK,
|
|
578
|
+
"SUBSCRIPTION_UPGRADED",
|
|
579
|
+
"Subscription upgraded; the prorated difference was charged.",
|
|
580
|
+
{ upgraded: true, plan: planName }
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
547
584
|
const { customerId } = await config.provider.createCustomer({
|
|
548
585
|
email: ctx.user.email ?? "",
|
|
549
586
|
subscriberType: subscriber.type,
|
|
@@ -1025,6 +1062,24 @@ var StripeProvider = class {
|
|
|
1025
1062
|
});
|
|
1026
1063
|
return { url: session.url ?? "" };
|
|
1027
1064
|
}
|
|
1065
|
+
async updateSubscription(opts) {
|
|
1066
|
+
const stripe = await this.client();
|
|
1067
|
+
const sub = await stripe.subscriptions.retrieve(opts.subscriptionId);
|
|
1068
|
+
const itemId = sub.items.data[0]?.id;
|
|
1069
|
+
const updated = await stripe.subscriptions.update(opts.subscriptionId, {
|
|
1070
|
+
items: [{ id: itemId, price: opts.priceId }],
|
|
1071
|
+
proration_behavior: "always_invoice",
|
|
1072
|
+
payment_behavior: "error_if_incomplete"
|
|
1073
|
+
});
|
|
1074
|
+
const item = updated.items?.data?.[0];
|
|
1075
|
+
const cps = item?.current_period_start ?? updated.current_period_start;
|
|
1076
|
+
const cpe = item?.current_period_end ?? updated.current_period_end;
|
|
1077
|
+
return {
|
|
1078
|
+
status: updated.status,
|
|
1079
|
+
currentPeriodStart: cps ? new Date(cps * 1e3) : null,
|
|
1080
|
+
currentPeriodEnd: cpe ? new Date(cpe * 1e3) : null
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1028
1083
|
async createPortalSession(opts) {
|
|
1029
1084
|
const stripe = await this.client();
|
|
1030
1085
|
const session = await stripe.billingPortal.sessions.create({
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/routes.ts","../src/schemas.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/providers/stripe.ts","../src/middlewares/require-plan.ts","../src/helpers.ts"],"sourcesContent":["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 { 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 plan = planController(store);\n\tconst subscription = subscriptionController(store);\n\tconst checkout = checkoutController(store, config);\n\tconst usage = usageController(store);\n\tconst webhook = webhookController(store, config);\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 { setApiResponse, HTTP, stringOrEmpty, numberOrZero } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { PlanModel } from '../models/plan.model';\nimport { toPlanDTO } from '../dtos/billing';\n\nexport function planController(store: IStoreAdapter) {\n\tconst plans = new PlanModel(store);\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\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\treturn setApiResponse(HTTP.OK, 'PLAN_FETCHED', 'Plan retrieved successfully.', {\n\t\t\t\tplan: toPlanDTO(plan),\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\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\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\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 { SubscriptionModel } from '../models/subscription.model';\n\nexport function webhookController(store: IStoreAdapter, config: IBillingConfig) {\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\tif (event.subscription) {\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: event.subscription.plan,\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\tamount: number;\n\tpriceId?: string;\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}\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","import type { IBillingProvider, IBillingEvent, INormalizedSubscription } 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; 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\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 === 'year' ? 'year' : 'month',\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 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":["setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP"]}
|
|
1
|
+
{"version":3,"sources":["../src/routes.ts","../src/schemas.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/providers/stripe.ts","../src/middlewares/require-plan.ts","../src/helpers.ts"],"sourcesContent":["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 { 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 plan = planController(store);\n\tconst subscription = subscriptionController(store);\n\tconst checkout = checkoutController(store, config);\n\tconst usage = usageController(store);\n\tconst webhook = webhookController(store, config);\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 { setApiResponse, HTTP, stringOrEmpty, numberOrZero } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { PlanModel } from '../models/plan.model';\nimport { toPlanDTO } from '../dtos/billing';\n\nexport function planController(store: IStoreAdapter) {\n\tconst plans = new PlanModel(store);\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\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\treturn setApiResponse(HTTP.OK, 'PLAN_FETCHED', 'Plan retrieved successfully.', {\n\t\t\t\tplan: toPlanDTO(plan),\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\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\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 { SubscriptionModel } from '../models/subscription.model';\n\nexport function webhookController(store: IStoreAdapter, config: IBillingConfig) {\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\tif (event.subscription) {\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: event.subscription.plan,\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\tamount: number;\n\tpriceId?: string;\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}\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","import type { IBillingProvider, IBillingEvent, INormalizedSubscription } 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; 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\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 === 'year' ? 'year' : 'month',\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 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":["setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fonderie/billing",
|
|
3
|
-
"version": "5.0
|
|
3
|
+
"version": "5.1.0",
|
|
4
4
|
"description": "SaaS billing in one module — config-driven plan catalogue, Stripe subscriptions, polymorphic user and workspace billing surfaces, usage metering, and webhook handling.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fonderiejs",
|