@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.
- package/README.md +184 -0
- package/dist/billing/index.d.ts +121 -0
- package/dist/billing/index.js +1082 -0
- package/dist/billing/index.js.map +1 -0
- package/dist/billing-service-LsAFesou.d.ts +578 -0
- package/dist/dunning/index.d.ts +310 -0
- package/dist/dunning/index.js +2677 -0
- package/dist/dunning/index.js.map +1 -0
- package/dist/index.d.ts +185 -0
- package/dist/index.js +7698 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/index.d.ts +5 -0
- package/dist/providers/index.js +1396 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/iyzico.d.ts +250 -0
- package/dist/providers/iyzico.js +469 -0
- package/dist/providers/iyzico.js.map +1 -0
- package/dist/providers/paddle.d.ts +66 -0
- package/dist/providers/paddle.js +437 -0
- package/dist/providers/paddle.js.map +1 -0
- package/dist/providers/stripe.d.ts +122 -0
- package/dist/providers/stripe.js +586 -0
- package/dist/providers/stripe.js.map +1 -0
- package/dist/schema-C5Zcju_j.d.ts +4191 -0
- package/dist/types.d.ts +388 -0
- package/dist/types.js +74 -0
- package/dist/types.js.map +1 -0
- package/dist/usage/index.d.ts +2674 -0
- package/dist/usage/index.js +2916 -0
- package/dist/usage/index.js.map +1 -0
- package/dist/webhooks/index.d.ts +89 -0
- package/dist/webhooks/index.js +188 -0
- package/dist/webhooks/index.js.map +1 -0
- package/package.json +91 -0
|
@@ -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
|