billsdk 0.0.1 → 0.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.
package/dist/index.js CHANGED
@@ -1,36 +1,1300 @@
1
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
1
+ export * from '@billsdk/core';
2
+ export { drizzleAdapter } from '@billsdk/drizzle-adapter';
3
+ import { memoryAdapter } from '@billsdk/memory-adapter';
4
+ export { memoryAdapter } from '@billsdk/memory-adapter';
5
+ import { paymentAdapter } from '@billsdk/payment-adapter';
6
+ export { paymentAdapter } from '@billsdk/payment-adapter';
7
+ import { z } from 'zod';
8
+
9
+ // src/index.ts
10
+ var createCustomerSchema = z.object({
11
+ externalId: z.string().min(1),
12
+ email: z.string().email(),
13
+ name: z.string().optional(),
14
+ metadata: z.record(z.string(), z.unknown()).optional()
15
+ });
16
+ var getCustomerQuerySchema = z.object({
17
+ externalId: z.string().min(1)
18
+ });
19
+ var customerEndpoints = {
20
+ createCustomer: {
21
+ path: "/customer",
22
+ options: {
23
+ method: "POST",
24
+ body: createCustomerSchema
25
+ },
26
+ handler: async (context) => {
27
+ const { ctx, body } = context;
28
+ const existing = await ctx.internalAdapter.findCustomerByExternalId(
29
+ body.externalId
30
+ );
31
+ if (existing) {
32
+ return { customer: existing };
33
+ }
34
+ const customer = await ctx.internalAdapter.createCustomer(body);
35
+ return { customer };
36
+ }
37
+ },
38
+ getCustomer: {
39
+ path: "/customer",
40
+ options: {
41
+ method: "GET",
42
+ query: getCustomerQuerySchema
43
+ },
44
+ handler: async (context) => {
45
+ const { ctx, query } = context;
46
+ const customer = await ctx.internalAdapter.findCustomerByExternalId(
47
+ query.externalId
48
+ );
49
+ if (!customer) {
50
+ return { customer: null };
51
+ }
52
+ return { customer };
53
+ }
54
+ }
9
55
  };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
56
+ var checkFeatureQuerySchema = z.object({
57
+ customerId: z.string().min(1),
58
+ feature: z.string().min(1)
59
+ });
60
+ var listFeaturesQuerySchema = z.object({
61
+ customerId: z.string().min(1)
62
+ });
63
+ var featureEndpoints = {
64
+ checkFeature: {
65
+ path: "/features/check",
66
+ options: {
67
+ method: "GET",
68
+ query: checkFeatureQuerySchema
69
+ },
70
+ handler: async (context) => {
71
+ const { ctx, query } = context;
72
+ const result = await ctx.internalAdapter.checkFeatureAccess(
73
+ query.customerId,
74
+ query.feature
75
+ );
76
+ return result;
77
+ }
78
+ },
79
+ listFeatures: {
80
+ path: "/features",
81
+ options: {
82
+ method: "GET",
83
+ query: listFeaturesQuerySchema
84
+ },
85
+ handler: async (context) => {
86
+ const { ctx, query } = context;
87
+ const customer = await ctx.internalAdapter.findCustomerByExternalId(
88
+ query.customerId
89
+ );
90
+ if (!customer) {
91
+ return { features: [] };
92
+ }
93
+ const subscription = await ctx.internalAdapter.findSubscriptionByCustomerId(customer.id);
94
+ if (!subscription) {
95
+ return { features: [] };
96
+ }
97
+ const featureCodes = ctx.internalAdapter.getPlanFeatures(
98
+ subscription.planCode
99
+ );
100
+ const features = featureCodes.map((code) => {
101
+ const feature = ctx.internalAdapter.findFeatureByCode(code);
102
+ return {
103
+ code,
104
+ name: feature?.name ?? code,
105
+ type: feature?.type ?? "boolean",
106
+ enabled: true
107
+ };
108
+ });
109
+ return { features };
110
+ }
15
111
  }
16
- return to;
17
112
  };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
113
 
20
- // src/index.ts
21
- var index_exports = {};
22
- __export(index_exports, {
23
- billsdk: () => billsdk,
24
- version: () => version
114
+ // src/api/routes/health.ts
115
+ var healthEndpoint = {
116
+ health: {
117
+ path: "/health",
118
+ options: {
119
+ method: "GET"
120
+ },
121
+ handler: async () => {
122
+ return {
123
+ status: "ok",
124
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
125
+ version: "0.1.0"
126
+ };
127
+ }
128
+ }
129
+ };
130
+ var planEndpoints = {
131
+ listPlans: {
132
+ path: "/plans",
133
+ options: {
134
+ method: "GET"
135
+ },
136
+ handler: async (context) => {
137
+ const { ctx } = context;
138
+ const plans = ctx.internalAdapter.listPlans({ includePrivate: false });
139
+ return { plans };
140
+ }
141
+ },
142
+ getPlan: {
143
+ path: "/plan",
144
+ options: {
145
+ method: "GET",
146
+ query: z.object({
147
+ code: z.string()
148
+ })
149
+ },
150
+ handler: async (context) => {
151
+ const { ctx, query } = context;
152
+ const plan = ctx.internalAdapter.findPlanByCode(query.code);
153
+ if (!plan) {
154
+ return { plan: null };
155
+ }
156
+ return { plan };
157
+ }
158
+ }
159
+ };
160
+ var getSubscriptionQuerySchema = z.object({
161
+ customerId: z.string().min(1)
162
+ });
163
+ var createSubscriptionSchema = z.object({
164
+ customerId: z.string().min(1),
165
+ planCode: z.string().min(1),
166
+ interval: z.enum(["monthly", "yearly"]).optional().default("monthly"),
167
+ successUrl: z.string().url().optional(),
168
+ cancelUrl: z.string().url().optional()
25
169
  });
26
- module.exports = __toCommonJS(index_exports);
27
- var version = "0.0.1";
28
- function billsdk() {
29
- return "Billsdk package reserved";
30
- }
31
- // Annotate the CommonJS export names for ESM import in node:
32
- 0 && (module.exports = {
33
- billsdk,
34
- version
170
+ var cancelSubscriptionSchema = z.object({
171
+ customerId: z.string().min(1),
172
+ cancelAt: z.enum(["period_end", "immediately"]).optional().default("period_end")
35
173
  });
174
+ var subscriptionEndpoints = {
175
+ getSubscription: {
176
+ path: "/subscription",
177
+ options: {
178
+ method: "GET",
179
+ query: getSubscriptionQuerySchema
180
+ },
181
+ handler: async (context) => {
182
+ const { ctx, query } = context;
183
+ const customer = await ctx.internalAdapter.findCustomerByExternalId(
184
+ query.customerId
185
+ );
186
+ if (!customer) {
187
+ return { subscription: null };
188
+ }
189
+ const subscription = await ctx.internalAdapter.findSubscriptionByCustomerId(customer.id);
190
+ if (!subscription) {
191
+ return { subscription: null };
192
+ }
193
+ const plan = ctx.internalAdapter.findPlanByCode(subscription.planCode);
194
+ const price = plan ? ctx.internalAdapter.getPlanPrice(
195
+ subscription.planCode,
196
+ subscription.interval
197
+ ) : null;
198
+ return {
199
+ subscription,
200
+ plan,
201
+ price
202
+ };
203
+ }
204
+ },
205
+ createSubscription: {
206
+ path: "/subscription",
207
+ options: {
208
+ method: "POST",
209
+ body: createSubscriptionSchema
210
+ },
211
+ handler: async (context) => {
212
+ const { ctx, body } = context;
213
+ if (!ctx.paymentAdapter) {
214
+ throw new Error("Payment adapter not configured");
215
+ }
216
+ const customer = await ctx.internalAdapter.findCustomerByExternalId(
217
+ body.customerId
218
+ );
219
+ if (!customer) {
220
+ throw new Error("Customer not found");
221
+ }
222
+ const plan = ctx.internalAdapter.findPlanByCode(body.planCode);
223
+ if (!plan) {
224
+ throw new Error("Plan not found");
225
+ }
226
+ const price = ctx.internalAdapter.getPlanPrice(
227
+ body.planCode,
228
+ body.interval
229
+ );
230
+ if (!price) {
231
+ throw new Error(
232
+ `No price found for plan ${body.planCode} with interval ${body.interval}`
233
+ );
234
+ }
235
+ const subscription = await ctx.internalAdapter.createSubscription({
236
+ customerId: customer.id,
237
+ planCode: body.planCode,
238
+ interval: body.interval,
239
+ status: "pending_payment",
240
+ trialDays: price.trialDays
241
+ });
242
+ const result = await ctx.paymentAdapter.processPayment({
243
+ customer: {
244
+ id: customer.id,
245
+ email: customer.email,
246
+ providerCustomerId: customer.providerCustomerId
247
+ },
248
+ plan: {
249
+ code: plan.code,
250
+ name: plan.name
251
+ },
252
+ price: {
253
+ amount: price.amount,
254
+ currency: price.currency,
255
+ interval: price.interval
256
+ },
257
+ subscription: {
258
+ id: subscription.id
259
+ },
260
+ successUrl: body.successUrl,
261
+ cancelUrl: body.cancelUrl,
262
+ metadata: {
263
+ subscriptionId: subscription.id,
264
+ customerId: customer.id
265
+ }
266
+ });
267
+ if (result.status === "active") {
268
+ const existingSubscriptions = await ctx.internalAdapter.listSubscriptions(customer.id);
269
+ for (const existing of existingSubscriptions) {
270
+ if (existing.id !== subscription.id && (existing.status === "active" || existing.status === "trialing")) {
271
+ await ctx.internalAdapter.cancelSubscription(existing.id);
272
+ }
273
+ }
274
+ const activeSubscription = await ctx.internalAdapter.updateSubscription(
275
+ subscription.id,
276
+ { status: "active" }
277
+ );
278
+ if (result.providerCustomerId && !customer.providerCustomerId) {
279
+ await ctx.internalAdapter.updateCustomer(customer.id, {
280
+ providerCustomerId: result.providerCustomerId
281
+ });
282
+ }
283
+ return {
284
+ subscription: activeSubscription ?? {
285
+ ...subscription,
286
+ status: "active"
287
+ }
288
+ };
289
+ }
290
+ if (result.status === "pending") {
291
+ await ctx.internalAdapter.updateSubscription(subscription.id, {
292
+ providerCheckoutSessionId: result.sessionId
293
+ });
294
+ if (result.providerCustomerId && !customer.providerCustomerId) {
295
+ await ctx.internalAdapter.updateCustomer(customer.id, {
296
+ providerCustomerId: result.providerCustomerId
297
+ });
298
+ }
299
+ return {
300
+ subscription,
301
+ redirectUrl: result.redirectUrl
302
+ };
303
+ }
304
+ await ctx.internalAdapter.updateSubscription(subscription.id, {
305
+ status: "canceled"
306
+ });
307
+ throw new Error(result.error);
308
+ }
309
+ },
310
+ cancelSubscription: {
311
+ path: "/subscription/cancel",
312
+ options: {
313
+ method: "POST",
314
+ body: cancelSubscriptionSchema
315
+ },
316
+ handler: async (context) => {
317
+ const { ctx, body } = context;
318
+ const customer = await ctx.internalAdapter.findCustomerByExternalId(
319
+ body.customerId
320
+ );
321
+ if (!customer) {
322
+ throw new Error("Customer not found");
323
+ }
324
+ const subscription = await ctx.internalAdapter.findSubscriptionByCustomerId(customer.id);
325
+ if (!subscription) {
326
+ throw new Error("No active subscription found");
327
+ }
328
+ if (body.cancelAt === "immediately") {
329
+ const canceled2 = await ctx.internalAdapter.cancelSubscription(
330
+ subscription.id
331
+ );
332
+ return { subscription: canceled2, canceledImmediately: true };
333
+ }
334
+ const canceled = await ctx.internalAdapter.cancelSubscription(
335
+ subscription.id,
336
+ subscription.currentPeriodEnd
337
+ );
338
+ return {
339
+ subscription: canceled,
340
+ canceledImmediately: false,
341
+ accessUntil: subscription.currentPeriodEnd
342
+ };
343
+ }
344
+ }
345
+ };
346
+
347
+ // src/api/routes/webhook.ts
348
+ var webhookEndpoints = {
349
+ handleWebhook: {
350
+ path: "/webhook",
351
+ options: {
352
+ method: "POST"
353
+ },
354
+ handler: async (context) => {
355
+ const { ctx, request } = context;
356
+ if (!ctx.paymentAdapter) {
357
+ throw new Error("Payment adapter not configured");
358
+ }
359
+ if (!ctx.paymentAdapter.confirmPayment) {
360
+ ctx.logger.debug("Payment adapter does not support confirmPayment");
361
+ return { received: true };
362
+ }
363
+ const result = await ctx.paymentAdapter.confirmPayment(request);
364
+ if (!result) {
365
+ ctx.logger.debug("Webhook event acknowledged but not processed");
366
+ return { received: true };
367
+ }
368
+ ctx.logger.debug("Payment confirmation received", {
369
+ subscriptionId: result.subscriptionId,
370
+ status: result.status
371
+ });
372
+ if (result.status === "active") {
373
+ const subscription = await ctx.internalAdapter.findSubscriptionById(
374
+ result.subscriptionId
375
+ );
376
+ if (subscription) {
377
+ const existingSubscriptions = await ctx.internalAdapter.listSubscriptions(
378
+ subscription.customerId
379
+ );
380
+ for (const existing of existingSubscriptions) {
381
+ if (existing.id !== subscription.id && (existing.status === "active" || existing.status === "trialing")) {
382
+ await ctx.internalAdapter.cancelSubscription(existing.id);
383
+ ctx.logger.info("Canceled previous subscription", {
384
+ subscriptionId: existing.id,
385
+ planCode: existing.planCode
386
+ });
387
+ }
388
+ }
389
+ await ctx.internalAdapter.updateSubscription(subscription.id, {
390
+ status: "active",
391
+ providerSubscriptionId: result.providerSubscriptionId
392
+ });
393
+ if (result.providerCustomerId) {
394
+ const customer = await ctx.internalAdapter.findCustomerById(
395
+ subscription.customerId
396
+ );
397
+ if (customer && !customer.providerCustomerId) {
398
+ await ctx.internalAdapter.updateCustomer(customer.id, {
399
+ providerCustomerId: result.providerCustomerId
400
+ });
401
+ }
402
+ }
403
+ ctx.logger.info("Subscription activated via webhook", {
404
+ subscriptionId: subscription.id,
405
+ providerSubscriptionId: result.providerSubscriptionId
406
+ });
407
+ } else {
408
+ ctx.logger.warn("Subscription not found for confirmation", {
409
+ subscriptionId: result.subscriptionId
410
+ });
411
+ }
412
+ } else if (result.status === "failed") {
413
+ const subscription = await ctx.internalAdapter.findSubscriptionById(
414
+ result.subscriptionId
415
+ );
416
+ if (subscription) {
417
+ await ctx.internalAdapter.updateSubscription(subscription.id, {
418
+ status: "canceled"
419
+ });
420
+ ctx.logger.warn("Payment failed, subscription canceled", {
421
+ subscriptionId: subscription.id
422
+ });
423
+ }
424
+ }
425
+ return { received: true };
426
+ }
427
+ }
428
+ };
429
+
430
+ // src/api/router.ts
431
+ function getEndpoints(ctx) {
432
+ const baseEndpoints = {
433
+ ...healthEndpoint,
434
+ ...customerEndpoints,
435
+ ...planEndpoints,
436
+ ...subscriptionEndpoints,
437
+ ...featureEndpoints,
438
+ ...webhookEndpoints
439
+ };
440
+ let allEndpoints = { ...baseEndpoints };
441
+ for (const plugin of ctx.plugins) {
442
+ if (plugin.endpoints) {
443
+ allEndpoints = { ...allEndpoints, ...plugin.endpoints };
444
+ }
445
+ }
446
+ return allEndpoints;
447
+ }
448
+ function parseUrl(url, basePath) {
449
+ const urlObj = new URL(url);
450
+ let path = urlObj.pathname;
451
+ if (path.startsWith(basePath)) {
452
+ path = path.slice(basePath.length);
453
+ }
454
+ if (!path.startsWith("/")) {
455
+ path = `/${path}`;
456
+ }
457
+ return { path, query: urlObj.searchParams };
458
+ }
459
+ function queryToObject(query) {
460
+ const obj = {};
461
+ query.forEach((value, key) => {
462
+ obj[key] = value;
463
+ });
464
+ return obj;
465
+ }
466
+ function jsonResponse(data, status = 200) {
467
+ return new Response(JSON.stringify(data), {
468
+ status,
469
+ headers: {
470
+ "Content-Type": "application/json"
471
+ }
472
+ });
473
+ }
474
+ function errorResponse(code, message, status = 400) {
475
+ return jsonResponse({ error: { code, message } }, status);
476
+ }
477
+ function createRouter(ctx) {
478
+ const endpoints = getEndpoints(ctx);
479
+ const handler = async (request) => {
480
+ const method = request.method.toUpperCase();
481
+ const { path, query } = parseUrl(request.url, ctx.basePath);
482
+ ctx.logger.debug(`${method} ${path}`);
483
+ if (ctx.options.hooks.before) {
484
+ const result = await ctx.options.hooks.before({
485
+ request,
486
+ path,
487
+ method
488
+ });
489
+ if (result instanceof Response) {
490
+ return result;
491
+ }
492
+ }
493
+ for (const plugin of ctx.plugins) {
494
+ if (plugin.hooks?.before) {
495
+ for (const hook of plugin.hooks.before) {
496
+ if (hook.matcher({ path, method })) {
497
+ const result = await hook.handler({
498
+ request,
499
+ path,
500
+ method,
501
+ billingContext: ctx
502
+ });
503
+ if (result instanceof Response) {
504
+ return result;
505
+ }
506
+ }
507
+ }
508
+ }
509
+ }
510
+ const endpointKey = Object.keys(endpoints).find((key) => {
511
+ const endpoint2 = endpoints[key];
512
+ if (!endpoint2) return false;
513
+ if (endpoint2.options.method !== method) return false;
514
+ return endpoint2.path === path;
515
+ });
516
+ if (!endpointKey) {
517
+ return errorResponse(
518
+ "NOT_FOUND",
519
+ `No endpoint found for ${method} ${path}`,
520
+ 404
521
+ );
522
+ }
523
+ const endpoint = endpoints[endpointKey];
524
+ try {
525
+ const requestForHandler = request.clone();
526
+ let body;
527
+ if (["POST", "PUT", "PATCH"].includes(method) && endpoint.options.body) {
528
+ try {
529
+ const text = await request.text();
530
+ if (text) {
531
+ body = JSON.parse(text);
532
+ }
533
+ } catch {
534
+ }
535
+ }
536
+ if (endpoint.options.body && body) {
537
+ const result = endpoint.options.body.safeParse(body);
538
+ if (!result.success) {
539
+ const issues = "issues" in result.error ? result.error.issues : [];
540
+ return errorResponse(
541
+ "VALIDATION_ERROR",
542
+ issues.map((e) => e.message).join(", "),
543
+ 400
544
+ );
545
+ }
546
+ body = result.data;
547
+ }
548
+ let queryObj = queryToObject(query);
549
+ if (endpoint.options.query) {
550
+ const result = endpoint.options.query.safeParse(queryObj);
551
+ if (!result.success) {
552
+ const issues = "issues" in result.error ? result.error.issues : [];
553
+ return errorResponse(
554
+ "VALIDATION_ERROR",
555
+ issues.map((e) => e.message).join(", "),
556
+ 400
557
+ );
558
+ }
559
+ queryObj = result.data;
560
+ }
561
+ const endpointContext = {
562
+ request: requestForHandler,
563
+ body,
564
+ query: queryObj,
565
+ headers: requestForHandler.headers,
566
+ params: {},
567
+ ctx
568
+ // Add billing context
569
+ };
570
+ const response = await endpoint.handler(endpointContext);
571
+ let finalResponse = jsonResponse(response);
572
+ for (const plugin of ctx.plugins) {
573
+ if (plugin.hooks?.after) {
574
+ for (const hook of plugin.hooks.after) {
575
+ if (hook.matcher({ path, method })) {
576
+ const result = await hook.handler({
577
+ request,
578
+ path,
579
+ method,
580
+ billingContext: ctx
581
+ });
582
+ if (result instanceof Response) {
583
+ finalResponse = result;
584
+ }
585
+ }
586
+ }
587
+ }
588
+ }
589
+ if (ctx.options.hooks.after) {
590
+ const result = await ctx.options.hooks.after({
591
+ request,
592
+ path,
593
+ method
594
+ });
595
+ if (result instanceof Response) {
596
+ finalResponse = result;
597
+ }
598
+ }
599
+ return finalResponse;
600
+ } catch (error) {
601
+ ctx.logger.error("Endpoint error", error);
602
+ if (error instanceof Error) {
603
+ return errorResponse("INTERNAL_ERROR", error.message, 500);
604
+ }
605
+ return errorResponse(
606
+ "INTERNAL_ERROR",
607
+ "An unexpected error occurred",
608
+ 500
609
+ );
610
+ }
611
+ };
612
+ return { handler, endpoints };
613
+ }
614
+
615
+ // src/db/field.ts
616
+ function defineField(attribute) {
617
+ return {
618
+ required: true,
619
+ input: true,
620
+ returned: true,
621
+ ...attribute
622
+ };
623
+ }
624
+ function defineTable(fields) {
625
+ return { fields };
626
+ }
627
+
628
+ // src/db/schema.ts
629
+ var generateId = () => crypto.randomUUID();
630
+ var billingSchema = {
631
+ customer: defineTable({
632
+ id: defineField({
633
+ type: "string",
634
+ primaryKey: true,
635
+ defaultValue: generateId,
636
+ input: false
637
+ }),
638
+ externalId: defineField({
639
+ type: "string",
640
+ unique: true,
641
+ index: true
642
+ }),
643
+ email: defineField({
644
+ type: "string",
645
+ index: true
646
+ }),
647
+ name: defineField({
648
+ type: "string",
649
+ required: false
650
+ }),
651
+ providerCustomerId: defineField({
652
+ type: "string",
653
+ required: false,
654
+ index: true
655
+ }),
656
+ metadata: defineField({
657
+ type: "json",
658
+ required: false
659
+ }),
660
+ createdAt: defineField({
661
+ type: "date",
662
+ defaultValue: () => /* @__PURE__ */ new Date(),
663
+ input: false
664
+ }),
665
+ updatedAt: defineField({
666
+ type: "date",
667
+ defaultValue: () => /* @__PURE__ */ new Date(),
668
+ input: false
669
+ })
670
+ }),
671
+ subscription: defineTable({
672
+ id: defineField({
673
+ type: "string",
674
+ primaryKey: true,
675
+ defaultValue: generateId,
676
+ input: false
677
+ }),
678
+ customerId: defineField({
679
+ type: "string",
680
+ index: true,
681
+ references: {
682
+ model: "customer",
683
+ field: "id",
684
+ onDelete: "cascade"
685
+ }
686
+ }),
687
+ // Plan code from config (not a foreign key)
688
+ planCode: defineField({
689
+ type: "string",
690
+ index: true
691
+ }),
692
+ // Billing interval
693
+ interval: defineField({
694
+ type: "string",
695
+ // "monthly" | "yearly"
696
+ defaultValue: "monthly"
697
+ }),
698
+ status: defineField({
699
+ type: "string",
700
+ // SubscriptionStatus
701
+ defaultValue: "active"
702
+ }),
703
+ providerSubscriptionId: defineField({
704
+ type: "string",
705
+ required: false,
706
+ index: true
707
+ }),
708
+ providerCheckoutSessionId: defineField({
709
+ type: "string",
710
+ required: false,
711
+ index: true
712
+ }),
713
+ currentPeriodStart: defineField({
714
+ type: "date",
715
+ defaultValue: () => /* @__PURE__ */ new Date()
716
+ }),
717
+ currentPeriodEnd: defineField({
718
+ type: "date"
719
+ }),
720
+ canceledAt: defineField({
721
+ type: "date",
722
+ required: false
723
+ }),
724
+ cancelAt: defineField({
725
+ type: "date",
726
+ required: false
727
+ }),
728
+ trialStart: defineField({
729
+ type: "date",
730
+ required: false
731
+ }),
732
+ trialEnd: defineField({
733
+ type: "date",
734
+ required: false
735
+ }),
736
+ metadata: defineField({
737
+ type: "json",
738
+ required: false
739
+ }),
740
+ createdAt: defineField({
741
+ type: "date",
742
+ defaultValue: () => /* @__PURE__ */ new Date(),
743
+ input: false
744
+ }),
745
+ updatedAt: defineField({
746
+ type: "date",
747
+ defaultValue: () => /* @__PURE__ */ new Date(),
748
+ input: false
749
+ })
750
+ })
751
+ };
752
+ function getBillingSchema() {
753
+ return billingSchema;
754
+ }
755
+ var TABLES = {
756
+ CUSTOMER: "customer",
757
+ SUBSCRIPTION: "subscription"
758
+ };
759
+
760
+ // src/db/internal-adapter.ts
761
+ function planConfigToPlan(config) {
762
+ return {
763
+ code: config.code,
764
+ name: config.name,
765
+ description: config.description,
766
+ isPublic: config.isPublic ?? true,
767
+ prices: config.prices.map((p) => ({
768
+ amount: p.amount,
769
+ currency: p.currency ?? "usd",
770
+ interval: p.interval,
771
+ trialDays: p.trialDays
772
+ })),
773
+ features: config.features ?? []
774
+ };
775
+ }
776
+ function featureConfigToFeature(config) {
777
+ return {
778
+ code: config.code,
779
+ name: config.name,
780
+ type: config.type ?? "boolean"
781
+ };
782
+ }
783
+ function createInternalAdapter(adapter, plans = [], features = []) {
784
+ const plansByCode = /* @__PURE__ */ new Map();
785
+ for (const config of plans) {
786
+ plansByCode.set(config.code, planConfigToPlan(config));
787
+ }
788
+ const featuresByCode = /* @__PURE__ */ new Map();
789
+ for (const config of features) {
790
+ featuresByCode.set(config.code, featureConfigToFeature(config));
791
+ }
792
+ return {
793
+ // Customer operations (DB)
794
+ async createCustomer(data) {
795
+ const now = /* @__PURE__ */ new Date();
796
+ return adapter.create({
797
+ model: TABLES.CUSTOMER,
798
+ data: {
799
+ ...data,
800
+ createdAt: now,
801
+ updatedAt: now
802
+ }
803
+ });
804
+ },
805
+ async findCustomerById(id) {
806
+ return adapter.findOne({
807
+ model: TABLES.CUSTOMER,
808
+ where: [{ field: "id", operator: "eq", value: id }]
809
+ });
810
+ },
811
+ async findCustomerByExternalId(externalId) {
812
+ return adapter.findOne({
813
+ model: TABLES.CUSTOMER,
814
+ where: [{ field: "externalId", operator: "eq", value: externalId }]
815
+ });
816
+ },
817
+ async updateCustomer(id, data) {
818
+ return adapter.update({
819
+ model: TABLES.CUSTOMER,
820
+ where: [{ field: "id", operator: "eq", value: id }],
821
+ update: { ...data, updatedAt: /* @__PURE__ */ new Date() }
822
+ });
823
+ },
824
+ async deleteCustomer(id) {
825
+ await adapter.delete({
826
+ model: TABLES.CUSTOMER,
827
+ where: [{ field: "id", operator: "eq", value: id }]
828
+ });
829
+ },
830
+ async listCustomers(options) {
831
+ return adapter.findMany({
832
+ model: TABLES.CUSTOMER,
833
+ limit: options?.limit,
834
+ offset: options?.offset,
835
+ sortBy: { field: "createdAt", direction: "desc" }
836
+ });
837
+ },
838
+ // Plan operations (from config - synchronous)
839
+ findPlanByCode(code) {
840
+ return plansByCode.get(code) ?? null;
841
+ },
842
+ listPlans(options) {
843
+ const allPlans = Array.from(plansByCode.values());
844
+ if (options?.includePrivate) {
845
+ return allPlans;
846
+ }
847
+ return allPlans.filter((p) => p.isPublic);
848
+ },
849
+ getPlanPrice(planCode, interval) {
850
+ const plan = plansByCode.get(planCode);
851
+ if (!plan) return null;
852
+ return plan.prices.find((p) => p.interval === interval) ?? plan.prices[0] ?? null;
853
+ },
854
+ // Feature operations (from config - synchronous)
855
+ findFeatureByCode(code) {
856
+ return featuresByCode.get(code) ?? null;
857
+ },
858
+ listFeatures() {
859
+ return Array.from(featuresByCode.values());
860
+ },
861
+ getPlanFeatures(planCode) {
862
+ const plan = plansByCode.get(planCode);
863
+ return plan?.features ?? [];
864
+ },
865
+ // Subscription operations (DB)
866
+ async createSubscription(data) {
867
+ const now = /* @__PURE__ */ new Date();
868
+ const interval = data.interval ?? "monthly";
869
+ const currentPeriodEnd = new Date(now);
870
+ if (interval === "yearly") {
871
+ currentPeriodEnd.setFullYear(currentPeriodEnd.getFullYear() + 1);
872
+ } else if (interval === "quarterly") {
873
+ currentPeriodEnd.setMonth(currentPeriodEnd.getMonth() + 3);
874
+ } else {
875
+ currentPeriodEnd.setMonth(currentPeriodEnd.getMonth() + 1);
876
+ }
877
+ let trialStart;
878
+ let trialEnd;
879
+ let status = data.status ?? "pending_payment";
880
+ if (data.trialDays && data.trialDays > 0) {
881
+ trialStart = now;
882
+ trialEnd = new Date(now);
883
+ trialEnd.setDate(trialEnd.getDate() + data.trialDays);
884
+ status = "trialing";
885
+ }
886
+ return adapter.create({
887
+ model: TABLES.SUBSCRIPTION,
888
+ data: {
889
+ customerId: data.customerId,
890
+ planCode: data.planCode,
891
+ interval,
892
+ status,
893
+ providerSubscriptionId: data.providerSubscriptionId,
894
+ providerCheckoutSessionId: data.providerCheckoutSessionId,
895
+ currentPeriodStart: now,
896
+ currentPeriodEnd: trialEnd ?? currentPeriodEnd,
897
+ trialStart,
898
+ trialEnd,
899
+ metadata: data.metadata,
900
+ createdAt: now,
901
+ updatedAt: now
902
+ }
903
+ });
904
+ },
905
+ async findSubscriptionById(id) {
906
+ return adapter.findOne({
907
+ model: TABLES.SUBSCRIPTION,
908
+ where: [{ field: "id", operator: "eq", value: id }]
909
+ });
910
+ },
911
+ async findSubscriptionByCustomerId(customerId) {
912
+ return adapter.findOne({
913
+ model: TABLES.SUBSCRIPTION,
914
+ where: [
915
+ { field: "customerId", operator: "eq", value: customerId },
916
+ {
917
+ field: "status",
918
+ operator: "in",
919
+ value: ["active", "trialing", "past_due", "pending_payment"]
920
+ }
921
+ ]
922
+ });
923
+ },
924
+ async findSubscriptionByProviderSessionId(sessionId) {
925
+ return adapter.findOne({
926
+ model: TABLES.SUBSCRIPTION,
927
+ where: [
928
+ {
929
+ field: "providerCheckoutSessionId",
930
+ operator: "eq",
931
+ value: sessionId
932
+ }
933
+ ]
934
+ });
935
+ },
936
+ async updateSubscription(id, data) {
937
+ return adapter.update({
938
+ model: TABLES.SUBSCRIPTION,
939
+ where: [{ field: "id", operator: "eq", value: id }],
940
+ update: { ...data, updatedAt: /* @__PURE__ */ new Date() }
941
+ });
942
+ },
943
+ async cancelSubscription(id, cancelAt) {
944
+ const now = /* @__PURE__ */ new Date();
945
+ return adapter.update({
946
+ model: TABLES.SUBSCRIPTION,
947
+ where: [{ field: "id", operator: "eq", value: id }],
948
+ update: {
949
+ status: cancelAt ? "active" : "canceled",
950
+ canceledAt: now,
951
+ cancelAt: cancelAt ?? now,
952
+ updatedAt: now
953
+ }
954
+ });
955
+ },
956
+ async listSubscriptions(customerId) {
957
+ return adapter.findMany({
958
+ model: TABLES.SUBSCRIPTION,
959
+ where: [{ field: "customerId", operator: "eq", value: customerId }],
960
+ sortBy: { field: "createdAt", direction: "desc" }
961
+ });
962
+ },
963
+ // Feature access check
964
+ async checkFeatureAccess(customerId, featureCode) {
965
+ const customer = await adapter.findOne({
966
+ model: TABLES.CUSTOMER,
967
+ where: [{ field: "externalId", operator: "eq", value: customerId }]
968
+ });
969
+ if (!customer) {
970
+ return { allowed: false };
971
+ }
972
+ const subscription = await adapter.findOne({
973
+ model: TABLES.SUBSCRIPTION,
974
+ where: [
975
+ { field: "customerId", operator: "eq", value: customer.id },
976
+ { field: "status", operator: "in", value: ["active", "trialing"] }
977
+ ]
978
+ });
979
+ if (!subscription) {
980
+ return { allowed: false };
981
+ }
982
+ const planFeatures = this.getPlanFeatures(subscription.planCode);
983
+ return { allowed: planFeatures.includes(featureCode) };
984
+ }
985
+ };
986
+ }
987
+
988
+ // src/context/create-context.ts
989
+ function createLogger(options) {
990
+ const level = options?.level ?? "info";
991
+ const disabled = options?.disabled ?? false;
992
+ const levels = ["debug", "info", "warn", "error"];
993
+ const currentLevelIndex = levels.indexOf(level);
994
+ const shouldLog = (logLevel) => {
995
+ if (disabled) return false;
996
+ return levels.indexOf(logLevel) >= currentLevelIndex;
997
+ };
998
+ return {
999
+ debug: (message, ...args) => {
1000
+ if (shouldLog("debug")) console.debug(`[billsdk] ${message}`, ...args);
1001
+ },
1002
+ info: (message, ...args) => {
1003
+ if (shouldLog("info")) console.info(`[billsdk] ${message}`, ...args);
1004
+ },
1005
+ warn: (message, ...args) => {
1006
+ if (shouldLog("warn")) console.warn(`[billsdk] ${message}`, ...args);
1007
+ },
1008
+ error: (message, ...args) => {
1009
+ if (shouldLog("error")) console.error(`[billsdk] ${message}`, ...args);
1010
+ }
1011
+ };
1012
+ }
1013
+ function resolveOptions(options, adapter) {
1014
+ return {
1015
+ database: adapter,
1016
+ payment: options.payment,
1017
+ basePath: options.basePath ?? "/api/billing",
1018
+ secret: options.secret ?? generateDefaultSecret(),
1019
+ plans: options.plans,
1020
+ features: options.features,
1021
+ plugins: options.plugins ?? [],
1022
+ hooks: options.hooks ?? {},
1023
+ logger: {
1024
+ level: options.logger?.level ?? "info",
1025
+ disabled: options.logger?.disabled ?? false
1026
+ }
1027
+ };
1028
+ }
1029
+ function generateDefaultSecret() {
1030
+ return "billsdk-development-secret-change-in-production";
1031
+ }
1032
+ async function createBillingContext(adapter, options) {
1033
+ const resolvedOptions = resolveOptions(options, adapter);
1034
+ const logger = createLogger(options.logger);
1035
+ const plugins = resolvedOptions.plugins;
1036
+ let schema = getBillingSchema();
1037
+ for (const plugin of plugins) {
1038
+ if (plugin.schema) {
1039
+ schema = { ...schema, ...plugin.schema };
1040
+ }
1041
+ }
1042
+ const internalAdapter = createInternalAdapter(
1043
+ adapter,
1044
+ options.plans ?? [],
1045
+ options.features ?? []
1046
+ );
1047
+ const context = {
1048
+ options: resolvedOptions,
1049
+ basePath: resolvedOptions.basePath,
1050
+ adapter,
1051
+ paymentAdapter: options.payment,
1052
+ internalAdapter,
1053
+ schema,
1054
+ plugins,
1055
+ logger,
1056
+ secret: resolvedOptions.secret,
1057
+ hasPlugin(id) {
1058
+ return plugins.some((p) => p.id === id);
1059
+ },
1060
+ getPlugin(id) {
1061
+ const plugin = plugins.find((p) => p.id === id);
1062
+ return plugin ?? null;
1063
+ },
1064
+ generateId() {
1065
+ return crypto.randomUUID();
1066
+ }
1067
+ };
1068
+ for (const plugin of plugins) {
1069
+ if (plugin.init) {
1070
+ await plugin.init(context);
1071
+ }
1072
+ }
1073
+ logger.debug("BillingContext created", {
1074
+ basePath: context.basePath,
1075
+ plugins: plugins.map((p) => p.id),
1076
+ hasPaymentAdapter: !!context.paymentAdapter,
1077
+ plans: options.plans?.length ?? 0,
1078
+ features: options.features?.length ?? 0
1079
+ });
1080
+ return context;
1081
+ }
1082
+
1083
+ // src/billsdk/base.ts
1084
+ var BASE_ERROR_CODES = {
1085
+ CUSTOMER_NOT_FOUND: "CUSTOMER_NOT_FOUND",
1086
+ PLAN_NOT_FOUND: "PLAN_NOT_FOUND",
1087
+ SUBSCRIPTION_NOT_FOUND: "SUBSCRIPTION_NOT_FOUND",
1088
+ FEATURE_NOT_FOUND: "FEATURE_NOT_FOUND",
1089
+ PAYMENT_ADAPTER_NOT_CONFIGURED: "PAYMENT_ADAPTER_NOT_CONFIGURED",
1090
+ INVALID_REQUEST: "INVALID_REQUEST",
1091
+ INTERNAL_ERROR: "INTERNAL_ERROR"
1092
+ };
1093
+ function createAPI(contextPromise) {
1094
+ return {
1095
+ async getCustomer(params) {
1096
+ const ctx = await contextPromise;
1097
+ return ctx.internalAdapter.findCustomerByExternalId(params.externalId);
1098
+ },
1099
+ async createCustomer(data) {
1100
+ const ctx = await contextPromise;
1101
+ return ctx.internalAdapter.createCustomer(data);
1102
+ },
1103
+ async listPlans() {
1104
+ const ctx = await contextPromise;
1105
+ return ctx.internalAdapter.listPlans();
1106
+ },
1107
+ async getPlan(params) {
1108
+ const ctx = await contextPromise;
1109
+ return ctx.internalAdapter.findPlanByCode(params.code);
1110
+ },
1111
+ async getSubscription(params) {
1112
+ const ctx = await contextPromise;
1113
+ const customer = await ctx.internalAdapter.findCustomerByExternalId(
1114
+ params.customerId
1115
+ );
1116
+ if (!customer) return null;
1117
+ return ctx.internalAdapter.findSubscriptionByCustomerId(customer.id);
1118
+ },
1119
+ async createSubscription(params) {
1120
+ const ctx = await contextPromise;
1121
+ if (!ctx.paymentAdapter) {
1122
+ throw new Error("Payment adapter not configured");
1123
+ }
1124
+ const customer = await ctx.internalAdapter.findCustomerByExternalId(
1125
+ params.customerId
1126
+ );
1127
+ if (!customer) {
1128
+ throw new Error("Customer not found");
1129
+ }
1130
+ const plan = ctx.internalAdapter.findPlanByCode(params.planCode);
1131
+ if (!plan) {
1132
+ throw new Error("Plan not found");
1133
+ }
1134
+ const interval = params.interval ?? "monthly";
1135
+ const price = ctx.internalAdapter.getPlanPrice(params.planCode, interval);
1136
+ if (!price) {
1137
+ throw new Error(
1138
+ `No price found for plan ${params.planCode} with interval ${interval}`
1139
+ );
1140
+ }
1141
+ const subscription = await ctx.internalAdapter.createSubscription({
1142
+ customerId: customer.id,
1143
+ planCode: params.planCode,
1144
+ interval,
1145
+ status: "pending_payment",
1146
+ trialDays: price.trialDays
1147
+ });
1148
+ const result = await ctx.paymentAdapter.processPayment({
1149
+ customer: {
1150
+ id: customer.id,
1151
+ email: customer.email,
1152
+ providerCustomerId: customer.providerCustomerId
1153
+ },
1154
+ plan: { code: plan.code, name: plan.name },
1155
+ price: {
1156
+ amount: price.amount,
1157
+ currency: price.currency,
1158
+ interval: price.interval
1159
+ },
1160
+ subscription: { id: subscription.id },
1161
+ successUrl: params.successUrl,
1162
+ cancelUrl: params.cancelUrl,
1163
+ metadata: { subscriptionId: subscription.id }
1164
+ });
1165
+ if (result.status === "active") {
1166
+ const existingSubscriptions = await ctx.internalAdapter.listSubscriptions(customer.id);
1167
+ for (const existing of existingSubscriptions) {
1168
+ if (existing.id !== subscription.id && (existing.status === "active" || existing.status === "trialing")) {
1169
+ await ctx.internalAdapter.cancelSubscription(existing.id);
1170
+ }
1171
+ }
1172
+ const activeSubscription = await ctx.internalAdapter.updateSubscription(
1173
+ subscription.id,
1174
+ { status: "active" }
1175
+ );
1176
+ if (result.providerCustomerId && !customer.providerCustomerId) {
1177
+ await ctx.internalAdapter.updateCustomer(customer.id, {
1178
+ providerCustomerId: result.providerCustomerId
1179
+ });
1180
+ }
1181
+ return {
1182
+ subscription: activeSubscription ?? {
1183
+ ...subscription,
1184
+ status: "active"
1185
+ }
1186
+ };
1187
+ }
1188
+ if (result.status === "pending") {
1189
+ await ctx.internalAdapter.updateSubscription(subscription.id, {
1190
+ providerCheckoutSessionId: result.sessionId
1191
+ });
1192
+ if (result.providerCustomerId && !customer.providerCustomerId) {
1193
+ await ctx.internalAdapter.updateCustomer(customer.id, {
1194
+ providerCustomerId: result.providerCustomerId
1195
+ });
1196
+ }
1197
+ return {
1198
+ subscription,
1199
+ redirectUrl: result.redirectUrl
1200
+ };
1201
+ }
1202
+ await ctx.internalAdapter.updateSubscription(subscription.id, {
1203
+ status: "canceled"
1204
+ });
1205
+ throw new Error(result.error);
1206
+ },
1207
+ async cancelSubscription(params) {
1208
+ const ctx = await contextPromise;
1209
+ const customer = await ctx.internalAdapter.findCustomerByExternalId(
1210
+ params.customerId
1211
+ );
1212
+ if (!customer) {
1213
+ return null;
1214
+ }
1215
+ const subscription = await ctx.internalAdapter.findSubscriptionByCustomerId(customer.id);
1216
+ if (!subscription) {
1217
+ return null;
1218
+ }
1219
+ const cancelAt = params.cancelAt ?? "period_end";
1220
+ if (cancelAt === "immediately") {
1221
+ return ctx.internalAdapter.cancelSubscription(subscription.id);
1222
+ }
1223
+ return ctx.internalAdapter.cancelSubscription(
1224
+ subscription.id,
1225
+ subscription.currentPeriodEnd
1226
+ );
1227
+ },
1228
+ async checkFeature(params) {
1229
+ const ctx = await contextPromise;
1230
+ return ctx.internalAdapter.checkFeatureAccess(
1231
+ params.customerId,
1232
+ params.feature
1233
+ );
1234
+ },
1235
+ async listFeatures(params) {
1236
+ const ctx = await contextPromise;
1237
+ const customer = await ctx.internalAdapter.findCustomerByExternalId(
1238
+ params.customerId
1239
+ );
1240
+ if (!customer) return [];
1241
+ const subscription = await ctx.internalAdapter.findSubscriptionByCustomerId(customer.id);
1242
+ if (!subscription) return [];
1243
+ const featureCodes = ctx.internalAdapter.getPlanFeatures(
1244
+ subscription.planCode
1245
+ );
1246
+ return featureCodes.map((code) => {
1247
+ const feature = ctx.internalAdapter.findFeatureByCode(code);
1248
+ return feature ? {
1249
+ code: feature.code,
1250
+ name: feature.name,
1251
+ enabled: true
1252
+ } : { code, name: code, enabled: true };
1253
+ });
1254
+ },
1255
+ async health() {
1256
+ return {
1257
+ status: "ok",
1258
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1259
+ };
1260
+ }
1261
+ };
1262
+ }
1263
+ async function init(options) {
1264
+ const database = options.database ?? memoryAdapter();
1265
+ const payment = options.payment ?? paymentAdapter();
1266
+ return createBillingContext(database, { ...options, payment });
1267
+ }
1268
+ function createBillSDK(options) {
1269
+ const contextPromise = init(options);
1270
+ const errorCodes = {};
1271
+ for (const plugin of options.plugins ?? []) {
1272
+ if (plugin.$ERROR_CODES) {
1273
+ Object.assign(errorCodes, plugin.$ERROR_CODES);
1274
+ }
1275
+ }
1276
+ const handler = async (request) => {
1277
+ const ctx = await contextPromise;
1278
+ const { handler: routeHandler } = createRouter(ctx);
1279
+ return routeHandler(request);
1280
+ };
1281
+ const api = createAPI(contextPromise);
1282
+ return {
1283
+ handler,
1284
+ api,
1285
+ options,
1286
+ $context: contextPromise,
1287
+ $Infer: {},
1288
+ $ERROR_CODES: {
1289
+ ...BASE_ERROR_CODES,
1290
+ ...errorCodes
1291
+ }
1292
+ };
1293
+ }
1294
+ function billsdk(options) {
1295
+ return createBillSDK(options);
1296
+ }
1297
+
1298
+ export { billsdk };
1299
+ //# sourceMappingURL=index.js.map
36
1300
  //# sourceMappingURL=index.js.map