@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,586 @@
1
+ // src/types.ts
2
+ import {
3
+ type,
4
+ currencyCode,
5
+ money,
6
+ paymentCustomer,
7
+ createCustomerRequest,
8
+ cardDetails,
9
+ paymentMethod,
10
+ paymentIntentStatus,
11
+ paymentIntent,
12
+ createPaymentIntentRequest,
13
+ subscriptionStatus,
14
+ priceInterval,
15
+ price,
16
+ subscription,
17
+ createSubscriptionRequest,
18
+ refundStatus,
19
+ refund,
20
+ createRefundRequest,
21
+ webhookEventType,
22
+ webhookEvent,
23
+ stripeConfig,
24
+ paddleConfig,
25
+ iyzicoConfig,
26
+ paymentsConfig
27
+ } from "@parsrun/types";
28
+ var PaymentError = class extends Error {
29
+ constructor(message, code, cause) {
30
+ super(message);
31
+ this.code = code;
32
+ this.cause = cause;
33
+ this.name = "PaymentError";
34
+ }
35
+ };
36
+ var PaymentErrorCodes = {
37
+ INVALID_CONFIG: "INVALID_CONFIG",
38
+ CUSTOMER_NOT_FOUND: "CUSTOMER_NOT_FOUND",
39
+ SUBSCRIPTION_NOT_FOUND: "SUBSCRIPTION_NOT_FOUND",
40
+ CHECKOUT_FAILED: "CHECKOUT_FAILED",
41
+ PAYMENT_FAILED: "PAYMENT_FAILED",
42
+ WEBHOOK_VERIFICATION_FAILED: "WEBHOOK_VERIFICATION_FAILED",
43
+ API_ERROR: "API_ERROR",
44
+ RATE_LIMITED: "RATE_LIMITED"
45
+ };
46
+
47
+ // src/providers/stripe.ts
48
+ var StripeProvider = class {
49
+ type = "stripe";
50
+ secretKey;
51
+ webhookSecret;
52
+ baseUrl = "https://api.stripe.com/v1";
53
+ apiVersion;
54
+ constructor(config) {
55
+ this.secretKey = config.secretKey;
56
+ this.webhookSecret = config.webhookSecret;
57
+ this.apiVersion = config.apiVersion ?? "2024-12-18.acacia";
58
+ }
59
+ async request(endpoint, options = {}) {
60
+ const { method = "GET", body } = options;
61
+ const headers = {
62
+ Authorization: `Bearer ${this.secretKey}`,
63
+ "Stripe-Version": this.apiVersion
64
+ };
65
+ const fetchOptions = {
66
+ method,
67
+ headers
68
+ };
69
+ if (body) {
70
+ headers["Content-Type"] = "application/x-www-form-urlencoded";
71
+ fetchOptions.body = this.encodeFormData(body);
72
+ }
73
+ const response = await fetch(`${this.baseUrl}${endpoint}`, fetchOptions);
74
+ const data = await response.json();
75
+ if (!response.ok || data.error) {
76
+ const errorMessage = data.error?.message ?? `HTTP ${response.status}`;
77
+ throw new PaymentError(
78
+ `Stripe API error: ${errorMessage}`,
79
+ data.error?.code ?? PaymentErrorCodes.API_ERROR,
80
+ data.error
81
+ );
82
+ }
83
+ return data;
84
+ }
85
+ encodeFormData(obj, prefix = "") {
86
+ const parts = [];
87
+ for (const [key, value] of Object.entries(obj)) {
88
+ if (value === void 0 || value === null) continue;
89
+ const fullKey = prefix ? `${prefix}[${key}]` : key;
90
+ if (typeof value === "object" && !Array.isArray(value)) {
91
+ parts.push(this.encodeFormData(value, fullKey));
92
+ } else if (Array.isArray(value)) {
93
+ value.forEach((item, index) => {
94
+ if (typeof item === "object") {
95
+ parts.push(this.encodeFormData(item, `${fullKey}[${index}]`));
96
+ } else {
97
+ parts.push(`${encodeURIComponent(`${fullKey}[${index}]`)}=${encodeURIComponent(String(item))}`);
98
+ }
99
+ });
100
+ } else {
101
+ parts.push(`${encodeURIComponent(fullKey)}=${encodeURIComponent(String(value))}`);
102
+ }
103
+ }
104
+ return parts.filter(Boolean).join("&");
105
+ }
106
+ // ============================================================================
107
+ // Customer
108
+ // ============================================================================
109
+ async createCustomer(options) {
110
+ const body = {
111
+ email: options.email
112
+ };
113
+ if (options.name) body["name"] = options.name;
114
+ if (options.phone) body["phone"] = options.phone;
115
+ if (options.metadata) body["metadata"] = options.metadata;
116
+ if (options.address) {
117
+ body["address"] = {
118
+ line1: options.address.line1,
119
+ line2: options.address.line2,
120
+ city: options.address.city,
121
+ state: options.address.state,
122
+ postal_code: options.address.postalCode,
123
+ country: options.address.country
124
+ };
125
+ }
126
+ const result = await this.request("/customers", {
127
+ method: "POST",
128
+ body
129
+ });
130
+ return this.mapCustomer(result);
131
+ }
132
+ async getCustomer(customerId) {
133
+ try {
134
+ const result = await this.request(`/customers/${customerId}`);
135
+ return this.mapCustomer(result);
136
+ } catch (err) {
137
+ if (err instanceof PaymentError && err.code === "resource_missing") {
138
+ return null;
139
+ }
140
+ throw err;
141
+ }
142
+ }
143
+ async updateCustomer(customerId, options) {
144
+ const body = {};
145
+ if (options.email) body["email"] = options.email;
146
+ if (options.name) body["name"] = options.name;
147
+ if (options.phone) body["phone"] = options.phone;
148
+ if (options.metadata) body["metadata"] = options.metadata;
149
+ if (options.address) {
150
+ body["address"] = {
151
+ line1: options.address.line1,
152
+ line2: options.address.line2,
153
+ city: options.address.city,
154
+ state: options.address.state,
155
+ postal_code: options.address.postalCode,
156
+ country: options.address.country
157
+ };
158
+ }
159
+ const result = await this.request(`/customers/${customerId}`, {
160
+ method: "POST",
161
+ body
162
+ });
163
+ return this.mapCustomer(result);
164
+ }
165
+ async deleteCustomer(customerId) {
166
+ await this.request(`/customers/${customerId}`, { method: "DELETE" });
167
+ }
168
+ mapCustomer(stripe) {
169
+ return {
170
+ id: stripe.id,
171
+ email: stripe.email ?? "",
172
+ name: stripe.name ?? void 0,
173
+ phone: stripe.phone ?? void 0,
174
+ address: stripe.address ? {
175
+ line1: stripe.address.line1 ?? void 0,
176
+ line2: stripe.address.line2 ?? void 0,
177
+ city: stripe.address.city ?? void 0,
178
+ state: stripe.address.state ?? void 0,
179
+ postalCode: stripe.address.postal_code ?? void 0,
180
+ country: stripe.address.country ?? void 0
181
+ } : void 0,
182
+ metadata: stripe.metadata ?? void 0,
183
+ providerData: stripe
184
+ };
185
+ }
186
+ // ============================================================================
187
+ // Checkout
188
+ // ============================================================================
189
+ async createCheckout(options) {
190
+ const body = {
191
+ mode: options.mode,
192
+ success_url: options.successUrl,
193
+ cancel_url: options.cancelUrl,
194
+ line_items: options.lineItems.map((item) => ({
195
+ price: item.priceId,
196
+ quantity: item.quantity
197
+ }))
198
+ };
199
+ if (options.customerId) body["customer"] = options.customerId;
200
+ if (options.customerEmail) body["customer_email"] = options.customerEmail;
201
+ if (options.allowPromotionCodes) body["allow_promotion_codes"] = true;
202
+ if (options.metadata) body["metadata"] = options.metadata;
203
+ if (options.mode === "subscription" && options.trialDays) {
204
+ body["subscription_data"] = {
205
+ trial_period_days: options.trialDays
206
+ };
207
+ }
208
+ const result = await this.request("/checkout/sessions", {
209
+ method: "POST",
210
+ body
211
+ });
212
+ return this.mapCheckoutSession(result);
213
+ }
214
+ async getCheckout(sessionId) {
215
+ try {
216
+ const result = await this.request(
217
+ `/checkout/sessions/${sessionId}`
218
+ );
219
+ return this.mapCheckoutSession(result);
220
+ } catch (err) {
221
+ if (err instanceof PaymentError && err.code === "resource_missing") {
222
+ return null;
223
+ }
224
+ throw err;
225
+ }
226
+ }
227
+ mapCheckoutSession(stripe) {
228
+ return {
229
+ id: stripe.id,
230
+ url: stripe.url ?? "",
231
+ customerId: stripe.customer ?? void 0,
232
+ status: stripe.status,
233
+ mode: stripe.mode,
234
+ amountTotal: stripe.amount_total ?? void 0,
235
+ currency: stripe.currency ?? void 0,
236
+ providerData: stripe
237
+ };
238
+ }
239
+ // ============================================================================
240
+ // Subscriptions
241
+ // ============================================================================
242
+ async createSubscription(options) {
243
+ const body = {
244
+ customer: options.customerId,
245
+ items: [{ price: options.priceId }]
246
+ };
247
+ if (options.trialDays) body["trial_period_days"] = options.trialDays;
248
+ if (options.metadata) body["metadata"] = options.metadata;
249
+ if (options.paymentBehavior) body["payment_behavior"] = options.paymentBehavior;
250
+ const result = await this.request("/subscriptions", {
251
+ method: "POST",
252
+ body
253
+ });
254
+ return this.mapSubscription(result);
255
+ }
256
+ async getSubscription(subscriptionId) {
257
+ try {
258
+ const result = await this.request(
259
+ `/subscriptions/${subscriptionId}`
260
+ );
261
+ return this.mapSubscription(result);
262
+ } catch (err) {
263
+ if (err instanceof PaymentError && err.code === "resource_missing") {
264
+ return null;
265
+ }
266
+ throw err;
267
+ }
268
+ }
269
+ async updateSubscription(subscriptionId, options) {
270
+ const body = {};
271
+ if (options.cancelAtPeriodEnd !== void 0) {
272
+ body["cancel_at_period_end"] = options.cancelAtPeriodEnd;
273
+ }
274
+ if (options.metadata) body["metadata"] = options.metadata;
275
+ if (options.prorationBehavior) body["proration_behavior"] = options.prorationBehavior;
276
+ if (options.priceId) {
277
+ const current = await this.request(
278
+ `/subscriptions/${subscriptionId}`
279
+ );
280
+ const itemId = current.items.data[0]?.id;
281
+ if (itemId) {
282
+ body["items"] = [{ id: itemId, price: options.priceId }];
283
+ }
284
+ }
285
+ const result = await this.request(
286
+ `/subscriptions/${subscriptionId}`,
287
+ { method: "POST", body }
288
+ );
289
+ return this.mapSubscription(result);
290
+ }
291
+ async cancelSubscription(subscriptionId, cancelAtPeriodEnd = true) {
292
+ if (cancelAtPeriodEnd) {
293
+ return this.updateSubscription(subscriptionId, { cancelAtPeriodEnd: true });
294
+ }
295
+ const result = await this.request(
296
+ `/subscriptions/${subscriptionId}`,
297
+ { method: "DELETE" }
298
+ );
299
+ return this.mapSubscription(result);
300
+ }
301
+ async listSubscriptions(customerId) {
302
+ const result = await this.request(
303
+ `/subscriptions?customer=${customerId}`
304
+ );
305
+ return result.data.map((sub) => this.mapSubscription(sub));
306
+ }
307
+ mapSubscription(stripe) {
308
+ const item = stripe.items.data[0];
309
+ return {
310
+ id: stripe.id,
311
+ customerId: typeof stripe.customer === "string" ? stripe.customer : stripe.customer.id,
312
+ status: stripe.status,
313
+ priceId: item?.price.id ?? "",
314
+ productId: typeof item?.price.product === "string" ? item.price.product : item?.price.product?.id,
315
+ currentPeriodStart: new Date(stripe.current_period_start * 1e3),
316
+ currentPeriodEnd: new Date(stripe.current_period_end * 1e3),
317
+ cancelAtPeriodEnd: stripe.cancel_at_period_end,
318
+ canceledAt: stripe.canceled_at ? new Date(stripe.canceled_at * 1e3) : void 0,
319
+ trialStart: stripe.trial_start ? new Date(stripe.trial_start * 1e3) : void 0,
320
+ trialEnd: stripe.trial_end ? new Date(stripe.trial_end * 1e3) : void 0,
321
+ metadata: stripe.metadata ?? void 0,
322
+ providerData: stripe
323
+ };
324
+ }
325
+ // ============================================================================
326
+ // Portal
327
+ // ============================================================================
328
+ async createPortalSession(options) {
329
+ const result = await this.request("/billing_portal/sessions", {
330
+ method: "POST",
331
+ body: {
332
+ customer: options.customerId,
333
+ return_url: options.returnUrl
334
+ }
335
+ });
336
+ return {
337
+ url: result.url,
338
+ returnUrl: options.returnUrl
339
+ };
340
+ }
341
+ // ============================================================================
342
+ // Products & Prices
343
+ // ============================================================================
344
+ async getProduct(productId) {
345
+ try {
346
+ const result = await this.request(`/products/${productId}`);
347
+ return this.mapProduct(result);
348
+ } catch (err) {
349
+ if (err instanceof PaymentError && err.code === "resource_missing") {
350
+ return null;
351
+ }
352
+ throw err;
353
+ }
354
+ }
355
+ async getPrice(priceId) {
356
+ try {
357
+ const result = await this.request(`/prices/${priceId}`);
358
+ return this.mapPrice(result);
359
+ } catch (err) {
360
+ if (err instanceof PaymentError && err.code === "resource_missing") {
361
+ return null;
362
+ }
363
+ throw err;
364
+ }
365
+ }
366
+ async listPrices(productId) {
367
+ let endpoint = "/prices?active=true&limit=100";
368
+ if (productId) {
369
+ endpoint += `&product=${productId}`;
370
+ }
371
+ const result = await this.request(endpoint);
372
+ return result.data.map((price2) => this.mapPrice(price2));
373
+ }
374
+ mapProduct(stripe) {
375
+ return {
376
+ id: stripe.id,
377
+ name: stripe.name,
378
+ description: stripe.description ?? void 0,
379
+ active: stripe.active,
380
+ metadata: stripe.metadata ?? void 0,
381
+ providerData: stripe
382
+ };
383
+ }
384
+ mapPrice(stripe) {
385
+ return {
386
+ id: stripe.id,
387
+ productId: typeof stripe.product === "string" ? stripe.product : stripe.product.id,
388
+ unitAmount: stripe.unit_amount ?? 0,
389
+ currency: stripe.currency.toUpperCase(),
390
+ recurring: stripe.recurring ? {
391
+ interval: stripe.recurring.interval,
392
+ intervalCount: stripe.recurring.interval_count
393
+ } : void 0,
394
+ active: stripe.active,
395
+ metadata: stripe.metadata ?? void 0,
396
+ providerData: stripe
397
+ };
398
+ }
399
+ // ============================================================================
400
+ // Webhooks
401
+ // ============================================================================
402
+ async verifyWebhook(payload, signature) {
403
+ if (!this.webhookSecret) {
404
+ throw new PaymentError(
405
+ "Webhook secret not configured",
406
+ PaymentErrorCodes.INVALID_CONFIG
407
+ );
408
+ }
409
+ const payloadString = typeof payload === "string" ? payload : new TextDecoder().decode(payload);
410
+ const signatureParts = signature.split(",").reduce((acc, part) => {
411
+ const [key, value] = part.split("=");
412
+ if (key && value) {
413
+ acc[key] = value;
414
+ }
415
+ return acc;
416
+ }, {});
417
+ const timestamp = signatureParts["t"];
418
+ const expectedSignature = signatureParts["v1"];
419
+ if (!timestamp || !expectedSignature) {
420
+ return null;
421
+ }
422
+ const timestampSeconds = parseInt(timestamp, 10);
423
+ const now = Math.floor(Date.now() / 1e3);
424
+ if (Math.abs(now - timestampSeconds) > 300) {
425
+ return null;
426
+ }
427
+ const signedPayload = `${timestamp}.${payloadString}`;
428
+ const computedSignature = await this.computeHmacSignature(
429
+ signedPayload,
430
+ this.webhookSecret
431
+ );
432
+ if (!this.secureCompare(computedSignature, expectedSignature)) {
433
+ return null;
434
+ }
435
+ const event = JSON.parse(payloadString);
436
+ return {
437
+ id: event.id,
438
+ type: this.mapEventType(event.type),
439
+ data: event.data.object,
440
+ created: new Date(event.created * 1e3),
441
+ provider: "stripe",
442
+ raw: event
443
+ };
444
+ }
445
+ async computeHmacSignature(payload, secret) {
446
+ const encoder = new TextEncoder();
447
+ const keyData = encoder.encode(secret);
448
+ const messageData = encoder.encode(payload);
449
+ const cryptoKey = await crypto.subtle.importKey(
450
+ "raw",
451
+ keyData,
452
+ { name: "HMAC", hash: "SHA-256" },
453
+ false,
454
+ ["sign"]
455
+ );
456
+ const signature = await crypto.subtle.sign("HMAC", cryptoKey, messageData);
457
+ const signatureArray = new Uint8Array(signature);
458
+ return Array.from(signatureArray).map((b) => b.toString(16).padStart(2, "0")).join("");
459
+ }
460
+ secureCompare(a, b) {
461
+ if (a.length !== b.length) return false;
462
+ let result = 0;
463
+ for (let i = 0; i < a.length; i++) {
464
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
465
+ }
466
+ return result === 0;
467
+ }
468
+ mapEventType(stripeType) {
469
+ const mapping = {
470
+ "checkout.session.completed": "checkout.session.completed",
471
+ "checkout.session.expired": "checkout.session.expired",
472
+ "customer.created": "customer.created",
473
+ "customer.updated": "customer.updated",
474
+ "customer.deleted": "customer.deleted",
475
+ "customer.subscription.created": "subscription.created",
476
+ "customer.subscription.updated": "subscription.updated",
477
+ "customer.subscription.deleted": "subscription.deleted",
478
+ "customer.subscription.trial_will_end": "subscription.trial_will_end",
479
+ "payment_intent.succeeded": "payment.succeeded",
480
+ "payment_intent.payment_failed": "payment.failed",
481
+ "invoice.created": "invoice.created",
482
+ "invoice.paid": "invoice.paid",
483
+ "invoice.payment_failed": "invoice.payment_failed",
484
+ "invoice.upcoming": "invoice.upcoming",
485
+ "charge.refunded": "refund.created",
486
+ "refund.created": "refund.created",
487
+ "refund.updated": "refund.updated"
488
+ };
489
+ return mapping[stripeType] ?? "unknown";
490
+ }
491
+ // ============================================================================
492
+ // Usage Reporting (Metered Billing)
493
+ // ============================================================================
494
+ /**
495
+ * Report usage for metered billing
496
+ *
497
+ * @example
498
+ * ```typescript
499
+ * // Report 100 API calls for a subscription item
500
+ * await stripe.reportUsage({
501
+ * subscriptionItemId: "si_xxx",
502
+ * quantity: 100,
503
+ * action: "increment", // or "set" to replace
504
+ * });
505
+ * ```
506
+ */
507
+ async reportUsage(record) {
508
+ const body = {
509
+ quantity: record.quantity,
510
+ action: record.action ?? "increment"
511
+ };
512
+ if (record.timestamp) {
513
+ body["timestamp"] = Math.floor(record.timestamp.getTime() / 1e3);
514
+ }
515
+ const headers = {};
516
+ if (record.idempotencyKey) {
517
+ headers["Idempotency-Key"] = record.idempotencyKey;
518
+ }
519
+ await this.request(
520
+ `/subscription_items/${record.subscriptionItemId}/usage_records`,
521
+ {
522
+ method: "POST",
523
+ body
524
+ }
525
+ );
526
+ }
527
+ /**
528
+ * Report multiple usage records (batch)
529
+ * Note: Stripe doesn't have a batch API, so this is sequential
530
+ */
531
+ async reportUsageBatch(records) {
532
+ for (const record of records) {
533
+ await this.reportUsage(record);
534
+ }
535
+ }
536
+ /**
537
+ * Get subscription item ID for a subscription and price
538
+ */
539
+ async getSubscriptionItemId(subscriptionId, priceId) {
540
+ const subscription2 = await this.request(
541
+ `/subscriptions/${subscriptionId}`
542
+ );
543
+ const item = subscription2.items.data.find((i) => i.price.id === priceId);
544
+ return item?.id ?? null;
545
+ }
546
+ /**
547
+ * Get usage records for a subscription item
548
+ */
549
+ async getUsageRecords(subscriptionItemId, options) {
550
+ let endpoint = `/subscription_items/${subscriptionItemId}/usage_record_summaries?`;
551
+ if (options?.limit) {
552
+ endpoint += `limit=${options.limit}&`;
553
+ }
554
+ if (options?.startingAfter) {
555
+ endpoint += `starting_after=${options.startingAfter}&`;
556
+ }
557
+ if (options?.endingBefore) {
558
+ endpoint += `ending_before=${options.endingBefore}&`;
559
+ }
560
+ const result = await this.request(endpoint);
561
+ return {
562
+ data: result.data.map((r) => ({
563
+ id: r.id,
564
+ quantity: r.total_usage,
565
+ timestamp: new Date(r.period.start * 1e3),
566
+ subscriptionItem: r.subscription_item
567
+ })),
568
+ hasMore: result.has_more
569
+ };
570
+ }
571
+ /**
572
+ * Get current period usage total for a subscription item
573
+ */
574
+ async getCurrentUsage(subscriptionItemId) {
575
+ const result = await this.getUsageRecords(subscriptionItemId, { limit: 1 });
576
+ return result.data[0]?.quantity ?? 0;
577
+ }
578
+ };
579
+ function createStripeProvider(config) {
580
+ return new StripeProvider(config);
581
+ }
582
+ export {
583
+ StripeProvider,
584
+ createStripeProvider
585
+ };
586
+ //# sourceMappingURL=stripe.js.map