@parsrun/payments 0.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.
@@ -0,0 +1,1082 @@
1
+ // src/webhooks/index.ts
2
+ var WebhookHandlerRegistry = class {
3
+ handlers = /* @__PURE__ */ new Map();
4
+ /**
5
+ * Register a handler for a specific event type
6
+ */
7
+ on(type2, handler) {
8
+ const handlers = this.handlers.get(type2) ?? [];
9
+ handlers.push(handler);
10
+ this.handlers.set(type2, handlers);
11
+ return this;
12
+ }
13
+ /**
14
+ * Register handlers for checkout events
15
+ */
16
+ onCheckout(handler) {
17
+ this.on("checkout.session.completed", handler);
18
+ this.on("checkout.session.expired", handler);
19
+ return this;
20
+ }
21
+ /**
22
+ * Register handlers for subscription events
23
+ */
24
+ onSubscription(handler) {
25
+ this.on("subscription.created", handler);
26
+ this.on("subscription.updated", handler);
27
+ this.on("subscription.deleted", handler);
28
+ this.on("subscription.trial_will_end", handler);
29
+ return this;
30
+ }
31
+ /**
32
+ * Register handlers for payment events
33
+ */
34
+ onPayment(handler) {
35
+ this.on("payment.succeeded", handler);
36
+ this.on("payment.failed", handler);
37
+ return this;
38
+ }
39
+ /**
40
+ * Register handlers for invoice events
41
+ */
42
+ onInvoice(handler) {
43
+ this.on("invoice.created", handler);
44
+ this.on("invoice.paid", handler);
45
+ this.on("invoice.payment_failed", handler);
46
+ this.on("invoice.upcoming", handler);
47
+ return this;
48
+ }
49
+ /**
50
+ * Register handlers for customer events
51
+ */
52
+ onCustomer(handler) {
53
+ this.on("customer.created", handler);
54
+ this.on("customer.updated", handler);
55
+ this.on("customer.deleted", handler);
56
+ return this;
57
+ }
58
+ /**
59
+ * Get handlers for an event type
60
+ */
61
+ getHandlers(type2) {
62
+ const specificHandlers = this.handlers.get(type2) ?? [];
63
+ const globalHandlers = this.handlers.get("*") ?? [];
64
+ return [...specificHandlers, ...globalHandlers];
65
+ }
66
+ /**
67
+ * Execute all handlers for an event
68
+ */
69
+ async handle(event) {
70
+ const handlers = this.getHandlers(event.type);
71
+ for (const handler of handlers) {
72
+ await handler(event);
73
+ }
74
+ }
75
+ };
76
+ var WebhookProcessor = class {
77
+ provider;
78
+ registry;
79
+ constructor(provider, registry) {
80
+ this.provider = provider;
81
+ this.registry = registry ?? new WebhookHandlerRegistry();
82
+ }
83
+ /**
84
+ * Get the handler registry
85
+ */
86
+ get handlers() {
87
+ return this.registry;
88
+ }
89
+ /**
90
+ * Process a webhook request
91
+ */
92
+ async process(request) {
93
+ try {
94
+ const payload = await request.text();
95
+ const signature = this.getSignature(request);
96
+ if (!signature) {
97
+ return {
98
+ success: false,
99
+ error: "Missing webhook signature"
100
+ };
101
+ }
102
+ const event = await this.provider.verifyWebhook(payload, signature);
103
+ if (!event) {
104
+ return {
105
+ success: false,
106
+ error: "Invalid webhook signature"
107
+ };
108
+ }
109
+ await this.registry.handle(event);
110
+ return {
111
+ success: true,
112
+ event
113
+ };
114
+ } catch (err) {
115
+ return {
116
+ success: false,
117
+ error: err instanceof Error ? err.message : "Unknown error"
118
+ };
119
+ }
120
+ }
121
+ /**
122
+ * Process raw webhook payload
123
+ */
124
+ async processRaw(payload, signature) {
125
+ try {
126
+ const event = await this.provider.verifyWebhook(payload, signature);
127
+ if (!event) {
128
+ return {
129
+ success: false,
130
+ error: "Invalid webhook signature"
131
+ };
132
+ }
133
+ await this.registry.handle(event);
134
+ return {
135
+ success: true,
136
+ event
137
+ };
138
+ } catch (err) {
139
+ return {
140
+ success: false,
141
+ error: err instanceof Error ? err.message : "Unknown error"
142
+ };
143
+ }
144
+ }
145
+ getSignature(request) {
146
+ const stripeSignature = request.headers.get("stripe-signature");
147
+ if (stripeSignature) return stripeSignature;
148
+ const paddleSignature = request.headers.get("paddle-signature");
149
+ if (paddleSignature) return paddleSignature;
150
+ return null;
151
+ }
152
+ };
153
+
154
+ // src/billing/types.ts
155
+ var BillingErrorCodes = {
156
+ NO_PROVIDER_CONFIGURED: "NO_PROVIDER_CONFIGURED",
157
+ PROVIDER_UNAVAILABLE: "PROVIDER_UNAVAILABLE",
158
+ ALL_PROVIDERS_FAILED: "ALL_PROVIDERS_FAILED",
159
+ REGION_NOT_SUPPORTED: "REGION_NOT_SUPPORTED",
160
+ SUBSCRIPTION_NOT_FOUND: "SUBSCRIPTION_NOT_FOUND",
161
+ CUSTOMER_NOT_FOUND: "CUSTOMER_NOT_FOUND",
162
+ FALLBACK_DISABLED: "FALLBACK_DISABLED",
163
+ OPERATION_NOT_ALLOWED: "OPERATION_NOT_ALLOWED"
164
+ };
165
+ var BillingError = class extends Error {
166
+ constructor(message, code, provider, cause) {
167
+ super(message);
168
+ this.code = code;
169
+ this.provider = provider;
170
+ this.cause = cause;
171
+ this.name = "BillingError";
172
+ }
173
+ };
174
+
175
+ // src/billing/provider-strategy.ts
176
+ var defaultRegionDetector = (context) => {
177
+ if (!context.countryCode) {
178
+ return "GLOBAL";
179
+ }
180
+ const countryToRegion = {
181
+ // Turkey
182
+ TR: "TR",
183
+ // EU countries
184
+ DE: "EU",
185
+ FR: "EU",
186
+ IT: "EU",
187
+ ES: "EU",
188
+ NL: "EU",
189
+ BE: "EU",
190
+ AT: "EU",
191
+ PT: "EU",
192
+ GR: "EU",
193
+ PL: "EU",
194
+ CZ: "EU",
195
+ RO: "EU",
196
+ HU: "EU",
197
+ SE: "EU",
198
+ DK: "EU",
199
+ FI: "EU",
200
+ IE: "EU",
201
+ SK: "EU",
202
+ BG: "EU",
203
+ HR: "EU",
204
+ LT: "EU",
205
+ LV: "EU",
206
+ SI: "EU",
207
+ EE: "EU",
208
+ CY: "EU",
209
+ LU: "EU",
210
+ MT: "EU",
211
+ // UK
212
+ GB: "UK",
213
+ UK: "UK",
214
+ // US
215
+ US: "US",
216
+ // APAC
217
+ JP: "APAC",
218
+ CN: "APAC",
219
+ KR: "APAC",
220
+ AU: "APAC",
221
+ NZ: "APAC",
222
+ SG: "APAC",
223
+ HK: "APAC",
224
+ TW: "APAC",
225
+ IN: "APAC",
226
+ ID: "APAC",
227
+ MY: "APAC",
228
+ TH: "APAC",
229
+ PH: "APAC",
230
+ VN: "APAC",
231
+ // LATAM
232
+ BR: "LATAM",
233
+ MX: "LATAM",
234
+ AR: "LATAM",
235
+ CL: "LATAM",
236
+ CO: "LATAM",
237
+ PE: "LATAM",
238
+ VE: "LATAM",
239
+ EC: "LATAM",
240
+ UY: "LATAM",
241
+ PY: "LATAM"
242
+ };
243
+ return countryToRegion[context.countryCode.toUpperCase()] ?? "GLOBAL";
244
+ };
245
+ var ProviderStrategy = class {
246
+ defaultProvider;
247
+ regionProviders;
248
+ rules;
249
+ regionDetector;
250
+ logger;
251
+ constructor(config, logger) {
252
+ this.defaultProvider = config.default;
253
+ this.regionProviders = /* @__PURE__ */ new Map();
254
+ this.rules = config.rules ?? [];
255
+ this.regionDetector = config.regionDetector ?? defaultRegionDetector;
256
+ this.logger = logger ?? void 0;
257
+ if (config.regions) {
258
+ for (const [region, provider] of Object.entries(config.regions)) {
259
+ this.regionProviders.set(region, provider);
260
+ }
261
+ }
262
+ this.rules.sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100));
263
+ }
264
+ /**
265
+ * Select provider for given context
266
+ */
267
+ async selectProvider(context, forceProvider) {
268
+ if (forceProvider) {
269
+ const provider = this.findProviderByType(forceProvider);
270
+ if (provider) {
271
+ this.logger?.debug("Provider forced", { provider: forceProvider });
272
+ return {
273
+ provider,
274
+ type: forceProvider,
275
+ region: await this.detectRegion(context),
276
+ reason: "forced"
277
+ };
278
+ }
279
+ throw new BillingError(
280
+ `Forced provider "${forceProvider}" not configured`,
281
+ "NO_PROVIDER_CONFIGURED",
282
+ forceProvider
283
+ );
284
+ }
285
+ const region = await this.detectRegion(context);
286
+ this.logger?.debug("Region detected", { region, context });
287
+ for (const rule of this.rules) {
288
+ if (rule.regions.includes(region)) {
289
+ if (rule.condition && !rule.condition(context)) {
290
+ continue;
291
+ }
292
+ this.logger?.debug("Rule matched", {
293
+ regions: rule.regions,
294
+ provider: rule.provider.type
295
+ });
296
+ return {
297
+ provider: rule.provider,
298
+ type: rule.provider.type,
299
+ region,
300
+ reason: "rule"
301
+ };
302
+ }
303
+ }
304
+ const regionProvider = this.regionProviders.get(region);
305
+ if (regionProvider) {
306
+ this.logger?.debug("Region provider selected", {
307
+ region,
308
+ provider: regionProvider.type
309
+ });
310
+ return {
311
+ provider: regionProvider,
312
+ type: regionProvider.type,
313
+ region,
314
+ reason: "region"
315
+ };
316
+ }
317
+ this.logger?.debug("Using default provider", {
318
+ region,
319
+ provider: this.defaultProvider.type
320
+ });
321
+ return {
322
+ provider: this.defaultProvider,
323
+ type: this.defaultProvider.type,
324
+ region,
325
+ reason: "default"
326
+ };
327
+ }
328
+ /**
329
+ * Detect region from context
330
+ */
331
+ async detectRegion(context) {
332
+ try {
333
+ return await this.regionDetector(context);
334
+ } catch (error) {
335
+ this.logger?.warn("Region detection failed, using GLOBAL", {
336
+ error: error instanceof Error ? error.message : String(error)
337
+ });
338
+ return "GLOBAL";
339
+ }
340
+ }
341
+ /**
342
+ * Get all configured providers
343
+ */
344
+ getAllProviders() {
345
+ const providers = /* @__PURE__ */ new Set();
346
+ providers.add(this.defaultProvider);
347
+ for (const provider of this.regionProviders.values()) {
348
+ providers.add(provider);
349
+ }
350
+ for (const rule of this.rules) {
351
+ providers.add(rule.provider);
352
+ }
353
+ return Array.from(providers);
354
+ }
355
+ /**
356
+ * Get provider by type
357
+ */
358
+ getProviderByType(type2) {
359
+ return this.findProviderByType(type2);
360
+ }
361
+ /**
362
+ * Get default provider
363
+ */
364
+ getDefaultProvider() {
365
+ return this.defaultProvider;
366
+ }
367
+ /**
368
+ * Get provider for region
369
+ */
370
+ getProviderForRegion(region) {
371
+ return this.regionProviders.get(region) ?? this.defaultProvider;
372
+ }
373
+ /**
374
+ * Check if region is supported
375
+ */
376
+ isRegionSupported(region) {
377
+ if (region === "GLOBAL") return true;
378
+ for (const rule of this.rules) {
379
+ if (rule.regions.includes(region)) return true;
380
+ }
381
+ return this.regionProviders.has(region);
382
+ }
383
+ /**
384
+ * Get supported regions
385
+ */
386
+ getSupportedRegions() {
387
+ const regions = /* @__PURE__ */ new Set(["GLOBAL"]);
388
+ for (const region of this.regionProviders.keys()) {
389
+ regions.add(region);
390
+ }
391
+ for (const rule of this.rules) {
392
+ for (const region of rule.regions) {
393
+ regions.add(region);
394
+ }
395
+ }
396
+ return Array.from(regions);
397
+ }
398
+ /**
399
+ * Find provider by type across all configured providers
400
+ */
401
+ findProviderByType(type2) {
402
+ if (this.defaultProvider.type === type2) {
403
+ return this.defaultProvider;
404
+ }
405
+ for (const provider of this.regionProviders.values()) {
406
+ if (provider.type === type2) {
407
+ return provider;
408
+ }
409
+ }
410
+ for (const rule of this.rules) {
411
+ if (rule.provider.type === type2) {
412
+ return rule.provider;
413
+ }
414
+ }
415
+ return void 0;
416
+ }
417
+ };
418
+ function createProviderStrategy(config, logger) {
419
+ return new ProviderStrategy(config, logger);
420
+ }
421
+
422
+ // src/types.ts
423
+ import {
424
+ type,
425
+ currencyCode,
426
+ money,
427
+ paymentCustomer,
428
+ createCustomerRequest,
429
+ cardDetails,
430
+ paymentMethod,
431
+ paymentIntentStatus,
432
+ paymentIntent,
433
+ createPaymentIntentRequest,
434
+ subscriptionStatus,
435
+ priceInterval,
436
+ price,
437
+ subscription,
438
+ createSubscriptionRequest,
439
+ refundStatus,
440
+ refund,
441
+ createRefundRequest,
442
+ webhookEventType,
443
+ webhookEvent,
444
+ stripeConfig,
445
+ paddleConfig,
446
+ iyzicoConfig,
447
+ paymentsConfig
448
+ } from "@parsrun/types";
449
+ var PaymentErrorCodes = {
450
+ INVALID_CONFIG: "INVALID_CONFIG",
451
+ CUSTOMER_NOT_FOUND: "CUSTOMER_NOT_FOUND",
452
+ SUBSCRIPTION_NOT_FOUND: "SUBSCRIPTION_NOT_FOUND",
453
+ CHECKOUT_FAILED: "CHECKOUT_FAILED",
454
+ PAYMENT_FAILED: "PAYMENT_FAILED",
455
+ WEBHOOK_VERIFICATION_FAILED: "WEBHOOK_VERIFICATION_FAILED",
456
+ API_ERROR: "API_ERROR",
457
+ RATE_LIMITED: "RATE_LIMITED"
458
+ };
459
+
460
+ // src/billing/fallback-executor.ts
461
+ var DEFAULT_RETRYABLE_ERRORS = [
462
+ PaymentErrorCodes.API_ERROR,
463
+ PaymentErrorCodes.RATE_LIMITED,
464
+ "PROVIDER_UNAVAILABLE",
465
+ "ECONNREFUSED",
466
+ "ETIMEDOUT",
467
+ "ENOTFOUND",
468
+ "NetworkError",
469
+ "fetch failed"
470
+ ];
471
+ var DEFAULT_ALLOWED_OPERATIONS = ["createCheckout"];
472
+ var FallbackExecutor = class {
473
+ config;
474
+ logger;
475
+ constructor(config, logger) {
476
+ this.config = {
477
+ enabled: config.enabled,
478
+ maxAttempts: config.maxAttempts ?? 1,
479
+ allowedOperations: config.allowedOperations ?? DEFAULT_ALLOWED_OPERATIONS,
480
+ retryableErrors: config.retryableErrors ?? DEFAULT_RETRYABLE_ERRORS,
481
+ providers: config.providers,
482
+ onFallback: config.onFallback,
483
+ onAllFailed: config.onAllFailed
484
+ };
485
+ this.logger = logger;
486
+ }
487
+ /**
488
+ * Check if fallback is enabled
489
+ */
490
+ get isEnabled() {
491
+ return this.config.enabled;
492
+ }
493
+ /**
494
+ * Execute operation with fallback support
495
+ *
496
+ * @param operation - Operation name for safety checks
497
+ * @param primaryProvider - Primary provider to try first
498
+ * @param fallbackProviders - Fallback providers in order of preference
499
+ * @param execute - Function that executes the operation on a provider
500
+ */
501
+ async execute(operation, primaryProvider, fallbackProviders, execute) {
502
+ const canFallback = this.canFallback(operation);
503
+ let lastError;
504
+ let attempt = 0;
505
+ const providers = [primaryProvider];
506
+ if (canFallback) {
507
+ for (const fb of fallbackProviders) {
508
+ if (fb !== primaryProvider && providers.length <= this.config.maxAttempts) {
509
+ providers.push(fb);
510
+ }
511
+ }
512
+ }
513
+ for (const provider of providers) {
514
+ attempt++;
515
+ const isUsingFallback = provider !== primaryProvider;
516
+ try {
517
+ this.logger?.debug(`Attempting ${operation}`, {
518
+ provider: provider.type,
519
+ attempt,
520
+ isUsingFallback
521
+ });
522
+ const result = await execute(provider);
523
+ if (isUsingFallback) {
524
+ this.logger?.info(`Fallback succeeded`, {
525
+ operation,
526
+ provider: provider.type,
527
+ originalProvider: primaryProvider.type
528
+ });
529
+ }
530
+ return {
531
+ result,
532
+ provider,
533
+ usedFallback: isUsingFallback
534
+ };
535
+ } catch (error) {
536
+ lastError = error instanceof Error ? error : new Error(String(error));
537
+ this.logger?.warn(`Provider ${provider.type} failed`, {
538
+ operation,
539
+ error: lastError.message,
540
+ attempt,
541
+ maxAttempts: providers.length
542
+ });
543
+ if (isUsingFallback || !canFallback || attempt >= providers.length || !this.isRetryableError(lastError)) {
544
+ if (attempt >= providers.length) {
545
+ if (this.config.onAllFailed) {
546
+ await this.notifyAllFailed(
547
+ operation,
548
+ primaryProvider.type,
549
+ lastError,
550
+ attempt
551
+ );
552
+ }
553
+ break;
554
+ }
555
+ continue;
556
+ }
557
+ if (this.config.onFallback && attempt < providers.length) {
558
+ const nextProvider = providers[attempt];
559
+ if (nextProvider) {
560
+ await this.notifyFallback(
561
+ operation,
562
+ primaryProvider.type,
563
+ nextProvider.type,
564
+ lastError,
565
+ attempt,
566
+ providers.length
567
+ );
568
+ }
569
+ }
570
+ }
571
+ }
572
+ throw new BillingError(
573
+ `All providers failed for ${operation}: ${lastError?.message}`,
574
+ "ALL_PROVIDERS_FAILED",
575
+ primaryProvider.type,
576
+ lastError
577
+ );
578
+ }
579
+ /**
580
+ * Check if fallback is allowed for operation
581
+ */
582
+ canFallback(operation) {
583
+ if (!this.config.enabled) {
584
+ return false;
585
+ }
586
+ return this.config.allowedOperations.includes(operation);
587
+ }
588
+ /**
589
+ * Check if error is retryable
590
+ */
591
+ isRetryableError(error) {
592
+ const errorCode = error.code;
593
+ const errorMessage = error.message;
594
+ for (const retryable of this.config.retryableErrors) {
595
+ if (errorCode === retryable) return true;
596
+ if (errorMessage.includes(retryable)) return true;
597
+ }
598
+ return false;
599
+ }
600
+ /**
601
+ * Notify fallback callback
602
+ */
603
+ async notifyFallback(operation, originalProvider, fallbackProvider, error, attempt, totalAttempts) {
604
+ if (!this.config.onFallback) return;
605
+ const context = {
606
+ operation,
607
+ originalProvider,
608
+ fallbackProvider,
609
+ error,
610
+ attempt,
611
+ totalAttempts,
612
+ allFailed: false
613
+ };
614
+ try {
615
+ await this.config.onFallback(context);
616
+ } catch (callbackError) {
617
+ this.logger?.error("Fallback callback failed", {
618
+ error: callbackError instanceof Error ? callbackError.message : String(callbackError)
619
+ });
620
+ }
621
+ }
622
+ /**
623
+ * Notify all failed callback
624
+ */
625
+ async notifyAllFailed(operation, originalProvider, error, totalAttempts) {
626
+ if (!this.config.onAllFailed) return;
627
+ const context = {
628
+ operation,
629
+ originalProvider,
630
+ // fallbackProvider is omitted when all failed (undefined not allowed in type)
631
+ error,
632
+ attempt: totalAttempts,
633
+ totalAttempts,
634
+ allFailed: true
635
+ };
636
+ try {
637
+ await this.config.onAllFailed(context);
638
+ } catch (callbackError) {
639
+ this.logger?.error("All-failed callback failed", {
640
+ error: callbackError instanceof Error ? callbackError.message : String(callbackError)
641
+ });
642
+ }
643
+ }
644
+ };
645
+ function createDisabledFallback() {
646
+ return new FallbackExecutor({ enabled: false });
647
+ }
648
+ function createFallbackExecutor(config, logger) {
649
+ return new FallbackExecutor(config, logger);
650
+ }
651
+
652
+ // src/billing/billing-service.ts
653
+ var consoleLogger = {
654
+ debug: (msg, ctx) => console.debug(`[Billing] ${msg}`, ctx ?? ""),
655
+ info: (msg, ctx) => console.info(`[Billing] ${msg}`, ctx ?? ""),
656
+ warn: (msg, ctx) => console.warn(`[Billing] ${msg}`, ctx ?? ""),
657
+ error: (msg, ctx) => console.error(`[Billing] ${msg}`, ctx ?? "")
658
+ };
659
+ var nullLogger = {
660
+ debug: () => {
661
+ },
662
+ info: () => {
663
+ },
664
+ warn: () => {
665
+ },
666
+ error: () => {
667
+ }
668
+ };
669
+ var BillingService = class {
670
+ strategy;
671
+ fallback;
672
+ webhookRegistry;
673
+ webhookProcessors;
674
+ logger;
675
+ tenantId;
676
+ debug;
677
+ constructor(config) {
678
+ this.debug = config.debug ?? false;
679
+ this.logger = config.logger ?? (this.debug ? consoleLogger : nullLogger);
680
+ this.tenantId = config.tenantId;
681
+ this.strategy = new ProviderStrategy(config.providers, this.logger);
682
+ this.fallback = config.fallback ? new FallbackExecutor(config.fallback, this.logger) : createDisabledFallback();
683
+ this.webhookRegistry = new WebhookHandlerRegistry();
684
+ this.webhookProcessors = /* @__PURE__ */ new Map();
685
+ for (const provider of this.strategy.getAllProviders()) {
686
+ const processor = new WebhookProcessor(provider, this.webhookRegistry);
687
+ this.webhookProcessors.set(provider.type, processor);
688
+ }
689
+ this.logger.info("BillingService initialized", {
690
+ providers: this.strategy.getAllProviders().map((p) => p.type),
691
+ regions: this.strategy.getSupportedRegions(),
692
+ fallbackEnabled: this.fallback.isEnabled
693
+ });
694
+ }
695
+ // ============================================================================
696
+ // High-Level API
697
+ // ============================================================================
698
+ /**
699
+ * Subscribe a customer to a plan
700
+ *
701
+ * This is the recommended way to handle subscriptions:
702
+ * 1. Creates or retrieves customer
703
+ * 2. Selects provider based on region
704
+ * 3. Creates checkout session
705
+ * 4. Returns checkout URL for redirect
706
+ *
707
+ * @example
708
+ * ```typescript
709
+ * const { checkoutUrl } = await billing.subscribe({
710
+ * email: "user@example.com",
711
+ * planId: "price_monthly",
712
+ * successUrl: "https://app.com/success",
713
+ * cancelUrl: "https://app.com/cancel",
714
+ * countryCode: "TR",
715
+ * });
716
+ * redirect(checkoutUrl);
717
+ * ```
718
+ */
719
+ async subscribe(options) {
720
+ const context = {
721
+ email: options.email,
722
+ countryCode: options.countryCode,
723
+ customerId: options.customerId
724
+ };
725
+ const selection = await this.strategy.selectProvider(
726
+ context,
727
+ options.forceProvider
728
+ );
729
+ this.logger.info("Starting subscription", {
730
+ email: options.email,
731
+ planId: options.planId,
732
+ provider: selection.type,
733
+ region: selection.region
734
+ });
735
+ let customerId = options.customerId;
736
+ if (!customerId) {
737
+ const customer = await this.executeWithFallback(
738
+ "createCustomer",
739
+ selection,
740
+ async (provider) => {
741
+ return provider.createCustomer({
742
+ email: options.email,
743
+ name: options.name,
744
+ metadata: {
745
+ ...options.metadata,
746
+ region: selection.region,
747
+ ...this.tenantId && { tenantId: this.tenantId }
748
+ }
749
+ });
750
+ }
751
+ );
752
+ customerId = customer.id;
753
+ }
754
+ const checkout = await this.executeWithFallback(
755
+ "createCheckout",
756
+ selection,
757
+ async (provider) => {
758
+ return provider.createCheckout({
759
+ customerId,
760
+ customerEmail: options.email,
761
+ lineItems: [{ priceId: options.planId, quantity: 1 }],
762
+ successUrl: options.successUrl,
763
+ cancelUrl: options.cancelUrl,
764
+ mode: "subscription",
765
+ trialDays: options.trialDays,
766
+ metadata: {
767
+ ...options.metadata,
768
+ region: selection.region,
769
+ ...this.tenantId && { tenantId: this.tenantId }
770
+ }
771
+ });
772
+ }
773
+ );
774
+ return {
775
+ checkoutUrl: checkout.url,
776
+ sessionId: checkout.id,
777
+ customerId,
778
+ provider: selection.type,
779
+ region: selection.region
780
+ };
781
+ }
782
+ /**
783
+ * Cancel a subscription
784
+ *
785
+ * @example
786
+ * ```typescript
787
+ * const result = await billing.cancel({
788
+ * subscriptionId: "sub_xxx",
789
+ * immediate: false, // Cancel at period end
790
+ * });
791
+ * ```
792
+ */
793
+ async cancel(options) {
794
+ const provider = options.provider ? this.strategy.getProviderByType(options.provider) : await this.findSubscriptionProvider(options.subscriptionId);
795
+ if (!provider) {
796
+ throw new BillingError(
797
+ `Cannot find provider for subscription: ${options.subscriptionId}`,
798
+ "SUBSCRIPTION_NOT_FOUND"
799
+ );
800
+ }
801
+ this.logger.info("Canceling subscription", {
802
+ subscriptionId: options.subscriptionId,
803
+ provider: provider.type,
804
+ immediate: options.immediate
805
+ });
806
+ const subscription2 = await provider.cancelSubscription(
807
+ options.subscriptionId,
808
+ !options.immediate
809
+ // cancelAtPeriodEnd = !immediate
810
+ );
811
+ return {
812
+ subscriptionId: subscription2.id,
813
+ status: subscription2.status,
814
+ endsAt: subscription2.cancelAtPeriodEnd ? subscription2.currentPeriodEnd : /* @__PURE__ */ new Date(),
815
+ provider: provider.type
816
+ };
817
+ }
818
+ /**
819
+ * Get subscription(s) for a customer
820
+ *
821
+ * @example
822
+ * ```typescript
823
+ * // Get by customer ID
824
+ * const subs = await billing.getSubscriptions({ customerId: "cus_xxx" });
825
+ *
826
+ * // Get specific subscription
827
+ * const sub = await billing.getSubscription({ subscriptionId: "sub_xxx" });
828
+ * ```
829
+ */
830
+ async getSubscriptions(options) {
831
+ const results = [];
832
+ if (options.subscriptionId) {
833
+ const sub = await this.getSubscription(options);
834
+ if (sub) results.push(sub);
835
+ return results;
836
+ }
837
+ const providers = options.provider ? [this.strategy.getProviderByType(options.provider)].filter(Boolean) : this.strategy.getAllProviders();
838
+ for (const provider of providers) {
839
+ if (!provider) continue;
840
+ try {
841
+ if (options.customerId) {
842
+ const subs = await provider.listSubscriptions(options.customerId);
843
+ for (const sub of subs) {
844
+ results.push({ ...sub, provider: provider.type });
845
+ }
846
+ }
847
+ } catch (error) {
848
+ this.logger.warn(`Failed to get subscriptions from ${provider.type}`, {
849
+ error: error instanceof Error ? error.message : String(error)
850
+ });
851
+ }
852
+ }
853
+ return results;
854
+ }
855
+ /**
856
+ * Get a single subscription
857
+ */
858
+ async getSubscription(options) {
859
+ if (!options.subscriptionId) {
860
+ const subs = await this.getSubscriptions(options);
861
+ return subs[0] ?? null;
862
+ }
863
+ const providers = options.provider ? [this.strategy.getProviderByType(options.provider)].filter(Boolean) : this.strategy.getAllProviders();
864
+ for (const provider of providers) {
865
+ if (!provider) continue;
866
+ try {
867
+ const sub = await provider.getSubscription(options.subscriptionId);
868
+ if (sub) {
869
+ return { ...sub, provider: provider.type };
870
+ }
871
+ } catch {
872
+ }
873
+ }
874
+ return null;
875
+ }
876
+ /**
877
+ * Get customer by ID
878
+ */
879
+ async getCustomer(customerId, provider) {
880
+ const providers = provider ? [this.strategy.getProviderByType(provider)].filter(Boolean) : this.strategy.getAllProviders();
881
+ for (const p of providers) {
882
+ if (!p) continue;
883
+ try {
884
+ const customer = await p.getCustomer(customerId);
885
+ if (customer) {
886
+ return { ...customer, provider: p.type };
887
+ }
888
+ } catch {
889
+ }
890
+ }
891
+ return null;
892
+ }
893
+ /**
894
+ * Create a customer portal session
895
+ */
896
+ async createPortalSession(customerId, returnUrl, provider) {
897
+ const p = provider ? this.strategy.getProviderByType(provider) : await this.findCustomerProvider(customerId);
898
+ if (!p) {
899
+ throw new BillingError(
900
+ `Cannot find provider for customer: ${customerId}`,
901
+ "CUSTOMER_NOT_FOUND"
902
+ );
903
+ }
904
+ const session = await p.createPortalSession({ customerId, returnUrl });
905
+ return { url: session.url, provider: p.type };
906
+ }
907
+ // ============================================================================
908
+ // Provider Selection
909
+ // ============================================================================
910
+ /**
911
+ * Select provider for a region/context
912
+ */
913
+ async selectProvider(context, forceProvider) {
914
+ return this.strategy.selectProvider(context, forceProvider);
915
+ }
916
+ /**
917
+ * Get all configured providers
918
+ */
919
+ getProviders() {
920
+ return this.strategy.getAllProviders();
921
+ }
922
+ /**
923
+ * Get provider by type
924
+ */
925
+ getProvider(type2) {
926
+ return this.strategy.getProviderByType(type2);
927
+ }
928
+ /**
929
+ * Get supported regions
930
+ */
931
+ getSupportedRegions() {
932
+ return this.strategy.getSupportedRegions();
933
+ }
934
+ // ============================================================================
935
+ // Webhooks
936
+ // ============================================================================
937
+ /**
938
+ * Register webhook handler for all providers
939
+ *
940
+ * @example
941
+ * ```typescript
942
+ * billing.onWebhook("subscription.created", async (event) => {
943
+ * console.log(`New subscription from ${event.provider}:`, event.data);
944
+ * });
945
+ *
946
+ * // Handle all events
947
+ * billing.onWebhook("*", async (event) => {
948
+ * await saveToAuditLog(event);
949
+ * });
950
+ * ```
951
+ */
952
+ onWebhook(type2, handler) {
953
+ this.webhookRegistry.on(type2, handler);
954
+ return this;
955
+ }
956
+ /**
957
+ * Handle webhook request
958
+ *
959
+ * @example
960
+ * ```typescript
961
+ * app.post("/webhooks/:provider", async (c) => {
962
+ * const provider = c.req.param("provider") as PaymentProviderType;
963
+ * const result = await billing.handleWebhook(c.req.raw, provider);
964
+ * return c.json({ received: result.success });
965
+ * });
966
+ * ```
967
+ */
968
+ async handleWebhook(request, provider) {
969
+ const processor = this.webhookProcessors.get(provider);
970
+ if (!processor) {
971
+ this.logger.error(`No webhook processor for provider: ${provider}`);
972
+ return {
973
+ success: false,
974
+ error: `Unknown provider: ${provider}`
975
+ };
976
+ }
977
+ return processor.process(request);
978
+ }
979
+ /**
980
+ * Handle raw webhook payload
981
+ */
982
+ async handleWebhookRaw(payload, signature, provider) {
983
+ const processor = this.webhookProcessors.get(provider);
984
+ if (!processor) {
985
+ return {
986
+ success: false,
987
+ error: `Unknown provider: ${provider}`
988
+ };
989
+ }
990
+ return processor.processRaw(payload, signature);
991
+ }
992
+ // ============================================================================
993
+ // Low-Level Provider Access
994
+ // ============================================================================
995
+ /**
996
+ * Execute operation on specific provider
997
+ *
998
+ * Use this for advanced operations not covered by high-level API
999
+ *
1000
+ * @example
1001
+ * ```typescript
1002
+ * const prices = await billing.withProvider("stripe", async (provider) => {
1003
+ * return provider.listPrices?.("prod_xxx") ?? [];
1004
+ * });
1005
+ * ```
1006
+ */
1007
+ async withProvider(type2, operation) {
1008
+ const provider = this.strategy.getProviderByType(type2);
1009
+ if (!provider) {
1010
+ throw new BillingError(
1011
+ `Provider not configured: ${type2}`,
1012
+ "NO_PROVIDER_CONFIGURED",
1013
+ type2
1014
+ );
1015
+ }
1016
+ return operation(provider);
1017
+ }
1018
+ // ============================================================================
1019
+ // Private Helpers
1020
+ // ============================================================================
1021
+ /**
1022
+ * Execute operation with fallback support
1023
+ */
1024
+ async executeWithFallback(operation, selection, execute) {
1025
+ const fallbackProviders = this.strategy.getAllProviders().filter((p) => p !== selection.provider);
1026
+ const result = await this.fallback.execute(
1027
+ operation,
1028
+ selection.provider,
1029
+ fallbackProviders,
1030
+ execute
1031
+ );
1032
+ if (result.usedFallback) {
1033
+ this.logger.warn(`Operation used fallback provider`, {
1034
+ operation,
1035
+ original: selection.type,
1036
+ fallback: result.provider.type
1037
+ });
1038
+ }
1039
+ return result.result;
1040
+ }
1041
+ /**
1042
+ * Find which provider owns a subscription
1043
+ */
1044
+ async findSubscriptionProvider(subscriptionId) {
1045
+ for (const provider of this.strategy.getAllProviders()) {
1046
+ try {
1047
+ const sub = await provider.getSubscription(subscriptionId);
1048
+ if (sub) return provider;
1049
+ } catch {
1050
+ }
1051
+ }
1052
+ return void 0;
1053
+ }
1054
+ /**
1055
+ * Find which provider owns a customer
1056
+ */
1057
+ async findCustomerProvider(customerId) {
1058
+ for (const provider of this.strategy.getAllProviders()) {
1059
+ try {
1060
+ const customer = await provider.getCustomer(customerId);
1061
+ if (customer) return provider;
1062
+ } catch {
1063
+ }
1064
+ }
1065
+ return void 0;
1066
+ }
1067
+ };
1068
+ function createBillingService(config) {
1069
+ return new BillingService(config);
1070
+ }
1071
+ export {
1072
+ BillingError,
1073
+ BillingErrorCodes,
1074
+ BillingService,
1075
+ FallbackExecutor,
1076
+ ProviderStrategy,
1077
+ createBillingService,
1078
+ createDisabledFallback,
1079
+ createFallbackExecutor,
1080
+ createProviderStrategy
1081
+ };
1082
+ //# sourceMappingURL=index.js.map