@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,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
|