@fonderie/billing 1.1.0 → 1.1.2

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.
@@ -0,0 +1,91 @@
1
+ <!-- GENERATED — do not edit. Regenerate with: npm run docs:signatures -->
2
+
3
+ # @fonderie/billing — outcomes
4
+
5
+ What this package does to a running app: tables its migrations create,
6
+ rows it seeds, routes it registers. Generated from the migration SQL and
7
+ route tables in source — trust this file instead of reading `dist/` or
8
+ downloading tarballs.
9
+
10
+ ## Database tables (after all migrations)
11
+
12
+ ### `fonderie_billing_notifications`
13
+
14
+ ```sql
15
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid()
16
+ subscriber_type TEXT NOT NULL
17
+ subscriber_id UUID NOT NULL
18
+ policy_key TEXT NOT NULL
19
+ notification TEXT NOT NULL
20
+ window_key TEXT NOT NULL
21
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
22
+ -- CONSTRAINT fonderie_billing_notifications_unique UNIQUE (subscriber_type, subscriber_id, policy_key, notification, window_key)
23
+ ```
24
+
25
+ ### `fonderie_plans`
26
+
27
+ ```sql
28
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid()
29
+ name TEXT NOT NULL UNIQUE
30
+ seats INT
31
+ trial_days INT NOT NULL DEFAULT 0
32
+ monthly_amount INT
33
+ monthly_price_id TEXT
34
+ yearly_amount INT
35
+ yearly_price_id TEXT
36
+ active BOOLEAN NOT NULL DEFAULT true
37
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
38
+ description TEXT
39
+ tier INT NOT NULL DEFAULT 0
40
+ features JSONB NOT NULL DEFAULT '[]'
41
+ metadata JSONB NOT NULL DEFAULT '{}'
42
+ ```
43
+
44
+ ### `fonderie_subscriptions`
45
+
46
+ ```sql
47
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid()
48
+ plan TEXT NOT NULL
49
+ interval TEXT NOT NULL DEFAULT 'month'
50
+ status TEXT NOT NULL DEFAULT 'incomplete'
51
+ provider_customer_id TEXT
52
+ provider_subscription_id TEXT
53
+ current_period_start TIMESTAMPTZ
54
+ current_period_end TIMESTAMPTZ
55
+ cancel_at_period_end BOOLEAN NOT NULL DEFAULT false
56
+ trial_ends_at TIMESTAMPTZ
57
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
58
+ subscriber_type TEXT NOT NULL
59
+ subscriber_id UUID NOT NULL
60
+ CONSTRAINT fonderie_subscriptions_subscriber_unique UNIQUE (subscriber_type, subscriber_id)
61
+ ```
62
+
63
+ ### `fonderie_usage_records`
64
+
65
+ ```sql
66
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid()
67
+ metric TEXT NOT NULL
68
+ quantity INT NOT NULL DEFAULT 1
69
+ recorded_at TIMESTAMPTZ NOT NULL DEFAULT now()
70
+ subscriber_type TEXT NOT NULL
71
+ subscriber_id UUID NOT NULL
72
+ CONSTRAINT fonderie_usage_records_subscriber_type_check CHECK (subscriber_type IN ('user', 'workspace'))
73
+ ```
74
+
75
+ Raw SQL ships in `node_modules/@fonderie/billing/dist/migrations/sql/` — read it there if you must; never download tarballs.
76
+
77
+ ## HTTP routes registered
78
+
79
+ | Method | Path | Middleware chain (auth / validation / handler) |
80
+ |---|---|---|
81
+ | POST | `/billing/checkout` | `requireAuth → validate(checkoutSchema) → checkout.createSession` |
82
+ | POST | `/billing/portal` | `requireAuth → checkout.createPortal` |
83
+ | GET | `/billing/subscription` | `requireAuth → subscription.get` |
84
+ | POST | `/billing/usage` | `requireAuth → validate(recordUsageSchema) → usage.record` |
85
+ | GET | `/billing/usage/:metric` | `requireAuth → usage.get` |
86
+ | POST | `/billing/webhook` | `webhook.handle` |
87
+ | GET | `/plans` | `plan.list` |
88
+ | POST | `/plans` | `validate(createPlanSchema) → plan.create` |
89
+ | DELETE | `/plans/:planId` | `plan.delete` |
90
+ | GET | `/plans/:planId` | `plan.get` |
91
+ | PUT | `/plans/:planId` | `validate(updatePlanSchema) → plan.update` |
@@ -0,0 +1,268 @@
1
+ <!-- GENERATED — do not edit. Regenerate with: npm run docs:signatures -->
2
+
3
+ # @fonderie/billing — signatures
4
+
5
+ ## @fonderie/billing
6
+
7
+ Subpath exports: `@fonderie/billing/types`, `@fonderie/billing/middleware`, `@fonderie/billing/migrations`
8
+
9
+ ```ts
10
+ new BillingModule(store: IStoreAdapter, config: IBillingConfig): BillingModule
11
+ .name: "@fonderie/billing"
12
+ .deps: string[]
13
+ .install(app: IFonderieApp): Promise<void>
14
+
15
+ new StripeProvider(secretKey: string, webhookSecret?: string | undefined): StripeProvider
16
+ .name: "stripe"
17
+ .createCustomer(opts: { email: string; subscriberType: SubscriberType; subscriberId: string; userId: string; }): Promise<{ customerId: string; }>
18
+ .createCheckoutSession(opts: { customerId: string; priceId: string; subscriberType: SubscriberType; subscriberId: string; trialDays?: number; successUrl: string; cancelUrl: string; }): Promise<{ url: string; }>
19
+ .createPortalSession(opts: { customerId: string; returnUrl: string; }): Promise<{ url: string; }>
20
+ .constructEvent(opts: { payload: string; signature: string; secret: string; }): Promise<IBillingEvent>
21
+
22
+ function requirePlan(plans: string | string[], store: IStoreAdapter): Middleware
23
+
24
+ function withBilling(store: IStoreAdapter, config: IBillingConfig, backend: ICounterBackend): Middleware
25
+
26
+ function hasFeature(ctx: IFonderieContext, key: string): boolean
27
+
28
+ function getPlanLimit(ctx: IFonderieContext, key: string): number | null
29
+
30
+ function getLimitStatus(ctx: IFonderieContext, key: string): IPolicyStatus | null
31
+
32
+ function requireFeature(key: string): Middleware
33
+
34
+ const MESSAGE_KEYS: { readonly limitWarning: "billing.limit-warning"; readonly limitReached: "billing.limit-reached"; readonly limitBlocked: "billing.limit-blocked"; }
35
+
36
+ interface IBillingConfig {
37
+ provider: IBillingProvider;
38
+ plans: IBillingPlan[];
39
+ successUrl: string;
40
+ cancelUrl: string;
41
+ webhookSecret?: string;
42
+ rateLimit?: {
43
+ backend?: RateLimitBackendConfig;
44
+ };
45
+ notifications?: IBillingNotificationsConfig;
46
+ }
47
+
48
+ interface IBillingPlan {
49
+ name: string;
50
+ description?: string;
51
+ tier?: number;
52
+ trialDays?: number;
53
+ monthly?: IBillingPlanPrice;
54
+ yearly?: IBillingPlanPrice;
55
+ defaults?: IBillingPlanDefaults;
56
+ policy?: Record<string, PolicyEntry>;
57
+ metadata?: Record<string, unknown>;
58
+ }
59
+
60
+ interface IBillingPlanDefaults {
61
+ warnAt?: number;
62
+ buffer?: number;
63
+ }
64
+
65
+ type RateLimitBackendConfig = 'memory' | 'db' | ICounterBackend;
66
+
67
+ interface IBillingNotificationsConfig {
68
+ warnAt?: boolean;
69
+ softHit?: boolean;
70
+ }
71
+
72
+ type BillingMessageKey = (typeof MESSAGE_KEYS)[keyof typeof MESSAGE_KEYS];
73
+
74
+ new MemoryCounterBackend(): MemoryCounterBackend
75
+ .increment(key: string, windowMs: number | null, quantity?: number): Promise<number>
76
+ .get(key: string, windowMs: number | null): Promise<number>
77
+
78
+ new DBCounterBackend(store: IStoreAdapter): DBCounterBackend
79
+ .increment(key: string, windowMs: number | null, quantity?: number): Promise<number>
80
+ .get(key: string, windowMs: number | null): Promise<number>
81
+
82
+ interface ICounterBackend {
83
+ increment(key: string, windowMs: number | null, quantity?: number): Promise<number>;
84
+ get(key: string, windowMs: number | null): Promise<number>;
85
+ }
86
+
87
+ interface IBillingProvider {
88
+ name: string;
89
+ createCustomer(opts: {
90
+ email: string;
91
+ subscriberType: SubscriberType;
92
+ subscriberId: string;
93
+ userId: string;
94
+ }): Promise<{
95
+ customerId: string;
96
+ }>;
97
+ createCheckoutSession(opts: {
98
+ customerId: string;
99
+ priceId: string;
100
+ subscriberType: SubscriberType;
101
+ subscriberId: string;
102
+ trialDays?: number;
103
+ successUrl: string;
104
+ cancelUrl: string;
105
+ }): Promise<{
106
+ url: string;
107
+ }>;
108
+ createPortalSession(opts: {
109
+ customerId: string;
110
+ returnUrl: string;
111
+ }): Promise<{
112
+ url: string;
113
+ }>;
114
+ constructEvent(opts: {
115
+ payload: string;
116
+ signature: string;
117
+ secret: string;
118
+ }): Promise<IBillingEvent>;
119
+ }
120
+
121
+ interface IBillingEvent {
122
+ type: string;
123
+ subscription: INormalizedSubscription | null;
124
+ }
125
+
126
+ interface IPlan {
127
+ id: string;
128
+ name: string;
129
+ seats: number | null;
130
+ trialDays: number;
131
+ monthlyAmount: number | null;
132
+ monthlyPriceId: string | null;
133
+ yearlyAmount: number | null;
134
+ yearlyPriceId: string | null;
135
+ description: string | null;
136
+ tier: number;
137
+ features: IPlanFeature[];
138
+ metadata: Record<string, unknown>;
139
+ }
140
+
141
+ interface ISubscription {
142
+ id: string;
143
+ subscriberType: SubscriberType;
144
+ subscriberId: string;
145
+ plan: string;
146
+ interval: 'month' | 'year';
147
+ status: SubscriptionStatus;
148
+ providerCustomerId: string | null;
149
+ providerSubscriptionId: string | null;
150
+ currentPeriodStart: string | null;
151
+ currentPeriodEnd: string | null;
152
+ cancelAtPeriodEnd: boolean;
153
+ trialEndsAt: string | null;
154
+ createdAt: string;
155
+ }
156
+
157
+ interface IUsageRecord {
158
+ id: string;
159
+ subscriberType: SubscriberType;
160
+ subscriberId: string;
161
+ metric: string;
162
+ quantity: number;
163
+ recordedAt: string;
164
+ }
165
+
166
+ type SubscriptionStatus = 'trialing' | 'active' | 'past_due' | 'canceled' | 'incomplete' | 'paused';
167
+
168
+ type PolicyEntry = {
169
+ enabled: boolean;
170
+ } | {
171
+ limit: number | null;
172
+ buffer?: number;
173
+ warnAt?: number;
174
+ window?: string;
175
+ unit?: string;
176
+ };
177
+
178
+ type LimitStatus = 'ok' | 'warning' | 'over_limit' | 'blocked';
179
+
180
+ type IPolicyStatus = {
181
+ type: 'feature';
182
+ enabled: boolean;
183
+ } | {
184
+ type: 'counter';
185
+ limit: number | null;
186
+ used: number;
187
+ status: LimitStatus;
188
+ resetsAt: string | null;
189
+ };
190
+
191
+ interface IBillingContext {
192
+ subscriber: {
193
+ type: SubscriberType;
194
+ id: string;
195
+ };
196
+ plan: string;
197
+ active: boolean;
198
+ statuses: Record<string, IPolicyStatus>;
199
+ }
200
+
201
+ interface IPlanDTO {
202
+ id: string;
203
+ planId: string;
204
+ name: string;
205
+ description: string;
206
+ tier: number;
207
+ seats: number | null;
208
+ trialDays: number;
209
+ pricing: {
210
+ monthly: number;
211
+ yearly: number;
212
+ currency: string;
213
+ };
214
+ features: IPlanFeature[];
215
+ metadata: Record<string, unknown>;
216
+ }
217
+
218
+ interface ISubscriptionDTO {
219
+ id: string;
220
+ subscriberType: SubscriberType;
221
+ subscriberId: string;
222
+ plan: string;
223
+ interval: string;
224
+ status: string;
225
+ cancelAtPeriodEnd: boolean;
226
+ currentPeriodStart: string | null;
227
+ currentPeriodEnd: string | null;
228
+ trialEndsAt: string | null;
229
+ createdAt: string;
230
+ }
231
+
232
+ interface IUsageRecordDTO {
233
+ id: string;
234
+ subscriberType: SubscriberType;
235
+ subscriberId: string;
236
+ metric: string;
237
+ quantity: number;
238
+ recordedAt: string;
239
+ }
240
+
241
+ function toPlanDTO(plan: IPlan): IPlanDTO
242
+
243
+ function toSubscriptionDTO(sub: ISubscription): ISubscriptionDTO
244
+
245
+ function toUsageRecordDTO(record: IUsageRecord): IUsageRecordDTO
246
+
247
+ function recordUsage(opts: { subscriberType: SubscriberType; subscriberId: string; metric: string; quantity: number; }, store: IStoreAdapter): Promise<void>
248
+
249
+ function getUsage(subscriberType: SubscriberType, subscriberId: string, metric: string, since: Date, store: IStoreAdapter): Promise<number>
250
+
251
+ function getPlans(config: IBillingConfig): IBillingPlan[]
252
+
253
+ function getPlanByName(name: string, config: IBillingConfig): IBillingPlan | null
254
+
255
+ function getDBPlans(store: IStoreAdapter): Promise<IPlan[]>
256
+
257
+ function getPlanById(id: string, store: IStoreAdapter): Promise<IPlan | null>
258
+
259
+ function createPlan(data: { name: string; description?: string | null; tier?: number; seats?: number | null; trialDays?: number; features?: unknown; metadata?: unknown; monthlyAmount?: number | null; monthlyPriceId?: string | null; yearlyAmount?: number | null; yearlyPriceId?: string | null; }, store: IStoreAdapter): Promise<...>
260
+
261
+ function updatePlan(id: string, data: Partial<Omit<IPlan, "id">>, store: IStoreAdapter): Promise<IPlan | null>
262
+
263
+ function deletePlan(id: string, store: IStoreAdapter): Promise<boolean>
264
+
265
+ function getSubscription(subscriberType: SubscriberType, subscriberId: string, store: IStoreAdapter): Promise<ISubscription | null>
266
+
267
+ namespace schemas — exports: checkoutSchema, createPlanSchema, recordUsageSchema, updatePlanSchema
268
+ ```
@@ -0,0 +1,38 @@
1
+ CREATE TABLE IF NOT EXISTS fonderie_plans (
2
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
3
+ name TEXT NOT NULL UNIQUE,
4
+ seats INT,
5
+ trial_days INT NOT NULL DEFAULT 0,
6
+ monthly_amount INT,
7
+ monthly_price_id TEXT,
8
+ yearly_amount INT,
9
+ yearly_price_id TEXT,
10
+ active BOOLEAN NOT NULL DEFAULT true,
11
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
12
+ );
13
+
14
+ CREATE TABLE IF NOT EXISTS fonderie_subscriptions (
15
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
16
+ workspace_id UUID NOT NULL UNIQUE,
17
+ plan TEXT NOT NULL,
18
+ interval TEXT NOT NULL DEFAULT 'month',
19
+ status TEXT NOT NULL DEFAULT 'incomplete',
20
+ provider_customer_id TEXT,
21
+ provider_subscription_id TEXT,
22
+ current_period_start TIMESTAMPTZ,
23
+ current_period_end TIMESTAMPTZ,
24
+ cancel_at_period_end BOOLEAN NOT NULL DEFAULT false,
25
+ trial_ends_at TIMESTAMPTZ,
26
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
27
+ );
28
+
29
+ CREATE TABLE IF NOT EXISTS fonderie_usage_records (
30
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
31
+ workspace_id UUID NOT NULL,
32
+ metric TEXT NOT NULL,
33
+ quantity INT NOT NULL DEFAULT 1,
34
+ recorded_at TIMESTAMPTZ NOT NULL DEFAULT now()
35
+ );
36
+
37
+ CREATE INDEX IF NOT EXISTS fonderie_usage_records_workspace_metric_idx
38
+ ON fonderie_usage_records (workspace_id, metric, recorded_at);
@@ -0,0 +1,5 @@
1
+ ALTER TABLE fonderie_plans
2
+ ADD COLUMN IF NOT EXISTS description TEXT,
3
+ ADD COLUMN IF NOT EXISTS tier INT NOT NULL DEFAULT 0,
4
+ ADD COLUMN IF NOT EXISTS features JSONB NOT NULL DEFAULT '[]',
5
+ ADD COLUMN IF NOT EXISTS metadata JSONB NOT NULL DEFAULT '{}';
@@ -0,0 +1 @@
1
+ ALTER TABLE fonderie_plans DROP COLUMN IF EXISTS limits;
@@ -0,0 +1,49 @@
1
+ -- fonderie_subscriptions: replace workspace_id with polymorphic subscriber
2
+ ALTER TABLE fonderie_subscriptions
3
+ ADD COLUMN subscriber_type TEXT,
4
+ ADD COLUMN subscriber_id UUID;
5
+
6
+ UPDATE fonderie_subscriptions
7
+ SET subscriber_type = 'workspace',
8
+ subscriber_id = workspace_id;
9
+
10
+ ALTER TABLE fonderie_subscriptions
11
+ ALTER COLUMN subscriber_type SET NOT NULL,
12
+ ALTER COLUMN subscriber_id SET NOT NULL;
13
+
14
+ ALTER TABLE fonderie_subscriptions
15
+ DROP CONSTRAINT fonderie_subscriptions_workspace_id_key;
16
+
17
+ ALTER TABLE fonderie_subscriptions
18
+ DROP COLUMN workspace_id;
19
+
20
+ ALTER TABLE fonderie_subscriptions
21
+ ADD CONSTRAINT fonderie_subscriptions_subscriber_type_check
22
+ CHECK (subscriber_type IN ('user', 'workspace')),
23
+ ADD CONSTRAINT fonderie_subscriptions_subscriber_unique
24
+ UNIQUE (subscriber_type, subscriber_id);
25
+
26
+ -- fonderie_usage_records: replace workspace_id with polymorphic subscriber
27
+ ALTER TABLE fonderie_usage_records
28
+ ADD COLUMN subscriber_type TEXT,
29
+ ADD COLUMN subscriber_id UUID;
30
+
31
+ UPDATE fonderie_usage_records
32
+ SET subscriber_type = 'workspace',
33
+ subscriber_id = workspace_id;
34
+
35
+ ALTER TABLE fonderie_usage_records
36
+ ALTER COLUMN subscriber_type SET NOT NULL,
37
+ ALTER COLUMN subscriber_id SET NOT NULL;
38
+
39
+ ALTER TABLE fonderie_usage_records
40
+ DROP COLUMN workspace_id;
41
+
42
+ ALTER TABLE fonderie_usage_records
43
+ ADD CONSTRAINT fonderie_usage_records_subscriber_type_check
44
+ CHECK (subscriber_type IN ('user', 'workspace'));
45
+
46
+ DROP INDEX IF EXISTS fonderie_usage_records_workspace_metric_idx;
47
+
48
+ CREATE INDEX fonderie_usage_records_subscriber_metric_idx
49
+ ON fonderie_usage_records (subscriber_type, subscriber_id, metric, recorded_at);
@@ -0,0 +1,13 @@
1
+ -- Tracks which threshold notifications have been sent per subscriber/key/window.
2
+ -- Prevents duplicate emails when a subscriber hovers around a threshold.
3
+ CREATE TABLE IF NOT EXISTS fonderie_billing_notifications (
4
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
5
+ subscriber_type TEXT NOT NULL,
6
+ subscriber_id UUID NOT NULL,
7
+ policy_key TEXT NOT NULL,
8
+ notification TEXT NOT NULL, -- 'warning' | 'reached' | 'blocked'
9
+ window_key TEXT NOT NULL, -- e.g. '2026-05-13' for a 1-day window
10
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
11
+ CONSTRAINT fonderie_billing_notifications_unique
12
+ UNIQUE (subscriber_type, subscriber_id, policy_key, notification, window_key)
13
+ );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fonderie/billing",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
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
  "fonderie-js",
@@ -71,17 +71,18 @@
71
71
  },
72
72
  "files": [
73
73
  "dist",
74
+ "brain",
74
75
  "LICENSE",
75
76
  "README.md"
76
77
  ],
77
78
  "repository": {
78
79
  "type": "git",
79
- "url": "git+https://github.com/fonderie-js/sdk.git",
80
+ "url": "git+https://github.com/fonderiejs/sdk.git",
80
81
  "directory": "packages/billing"
81
82
  },
82
- "homepage": "https://github.com/fonderie-js/sdk/tree/main/packages/billing#readme",
83
+ "homepage": "https://github.com/fonderiejs/sdk/tree/main/packages/billing#readme",
83
84
  "bugs": {
84
- "url": "https://github.com/fonderie-js/sdk/issues"
85
+ "url": "https://github.com/fonderiejs/sdk/issues"
85
86
  },
86
87
  "dependencies": {
87
88
  "zod": "^4.4.3"