@fonderie/billing 1.0.0 → 1.1.1

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
+ ```
package/dist/index.cjs CHANGED
@@ -39,6 +39,7 @@ __export(index_exports, {
39
39
  recordUsage: () => recordUsage,
40
40
  requireFeature: () => requireFeature,
41
41
  requirePlan: () => requirePlan,
42
+ schemas: () => schemas_exports,
42
43
  toPlanDTO: () => toPlanDTO,
43
44
  toSubscriptionDTO: () => toSubscriptionDTO,
44
45
  toUsageRecordDTO: () => toUsageRecordDTO,
@@ -50,6 +51,41 @@ module.exports = __toCommonJS(index_exports);
50
51
  // src/routes.ts
51
52
  var import_middlewares = require("@fonderie/core/middlewares");
52
53
 
54
+ // src/schemas.ts
55
+ var schemas_exports = {};
56
+ __export(schemas_exports, {
57
+ checkoutSchema: () => checkoutSchema,
58
+ createPlanSchema: () => createPlanSchema,
59
+ recordUsageSchema: () => recordUsageSchema,
60
+ updatePlanSchema: () => updatePlanSchema
61
+ });
62
+ var import_zod = require("zod");
63
+ var planFields = {
64
+ description: import_zod.z.string().max(2e3).nullable().optional(),
65
+ tier: import_zod.z.number().int().min(0).optional(),
66
+ seats: import_zod.z.number().int().min(0).nullable().optional(),
67
+ trialDays: import_zod.z.number().int().min(0).optional(),
68
+ monthlyAmount: import_zod.z.number().min(0).nullable().optional(),
69
+ monthlyPriceId: import_zod.z.string().max(200).nullable().optional(),
70
+ yearlyAmount: import_zod.z.number().min(0).nullable().optional(),
71
+ yearlyPriceId: import_zod.z.string().max(200).nullable().optional(),
72
+ features: import_zod.z.unknown().optional(),
73
+ metadata: import_zod.z.unknown().optional()
74
+ };
75
+ var createPlanSchema = import_zod.z.object({
76
+ name: import_zod.z.string().trim().min(1, "name is required").max(200),
77
+ ...planFields
78
+ });
79
+ var updatePlanSchema = import_zod.z.object({ name: import_zod.z.string().trim().min(1).max(200).optional(), ...planFields }).refine((o) => Object.values(o).some((v) => v !== void 0), "Provide at least one field");
80
+ var checkoutSchema = import_zod.z.object({
81
+ plan: import_zod.z.string().min(1, "plan is required"),
82
+ interval: import_zod.z.enum(["month", "year"]).optional()
83
+ });
84
+ var recordUsageSchema = import_zod.z.object({
85
+ metric: import_zod.z.string().min(1, "metric is required").max(100),
86
+ quantity: import_zod.z.number().min(0).optional()
87
+ });
88
+
53
89
  // src/controllers/plan.controller.ts
54
90
  var import_core = require("@fonderie/core");
55
91
 
@@ -744,15 +780,15 @@ function buildBillingRoutes(store, config) {
744
780
  ["GET", "/plans", plan.list],
745
781
  ["GET", "/plans/:planId", plan.get],
746
782
  // Plans — admin write (caller is responsible for authorization)
747
- ["POST", "/plans", plan.create],
748
- ["PUT", "/plans/:planId", plan.update],
783
+ ["POST", "/plans", (0, import_middlewares.validate)(createPlanSchema), plan.create],
784
+ ["PUT", "/plans/:planId", (0, import_middlewares.validate)(updatePlanSchema), plan.update],
749
785
  ["DELETE", "/plans/:planId", plan.delete],
750
786
  // Billing — subscriber resolved from X-Workspace-ID header (workspace) or session (user)
751
787
  // Workspace membership is verified automatically by the withBilling global middleware
752
788
  ["GET", "/billing/subscription", import_middlewares.requireAuth, subscription.get],
753
- ["POST", "/billing/checkout", import_middlewares.requireAuth, checkout.createSession],
789
+ ["POST", "/billing/checkout", import_middlewares.requireAuth, (0, import_middlewares.validate)(checkoutSchema), checkout.createSession],
754
790
  ["POST", "/billing/portal", import_middlewares.requireAuth, checkout.createPortal],
755
- ["POST", "/billing/usage", import_middlewares.requireAuth, usage.record],
791
+ ["POST", "/billing/usage", import_middlewares.requireAuth, (0, import_middlewares.validate)(recordUsageSchema), usage.record],
756
792
  ["GET", "/billing/usage/:metric", import_middlewares.requireAuth, usage.get],
757
793
  // Webhook — signature verified inside the handler
758
794
  ["POST", "/billing/webhook", webhook.handle]
@@ -1161,6 +1197,7 @@ function requireFeature(key) {
1161
1197
  recordUsage,
1162
1198
  requireFeature,
1163
1199
  requirePlan,
1200
+ schemas,
1164
1201
  toPlanDTO,
1165
1202
  toSubscriptionDTO,
1166
1203
  toUsageRecordDTO,