@better-auth/stripe 1.5.0-beta.2 → 1.5.0-beta.3
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/.turbo/turbo-build.log +9 -9
- package/CHANGELOG.md +15 -13
- package/LICENSE.md +15 -12
- package/dist/client.d.mts +105 -1
- package/dist/client.mjs +1 -1
- package/dist/error-codes-Bkj5yJMT.mjs +29 -0
- package/dist/{index-SbT5j9k6.d.mts → index-BnHmwMru.d.mts} +269 -154
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +449 -194
- package/package.json +6 -6
- package/src/error-codes.ts +16 -0
- package/src/hooks.ts +98 -71
- package/src/index.ts +142 -45
- package/src/middleware.ts +89 -42
- package/src/routes.ts +502 -224
- package/src/schema.ts +18 -0
- package/src/types.ts +75 -19
- package/src/utils.ts +11 -0
- package/test/stripe-organization.test.ts +1993 -0
- package/{src → test}/stripe.test.ts +821 -18
- package/dist/error-codes-qqooUh6R.mjs +0 -16
package/src/routes.ts
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
2
2
|
import { APIError } from "@better-auth/core/error";
|
|
3
|
-
import type { GenericEndpointContext } from "better-auth";
|
|
3
|
+
import type { GenericEndpointContext, User } from "better-auth";
|
|
4
4
|
import { HIDE_METADATA } from "better-auth";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
sessionMiddleware,
|
|
9
|
-
} from "better-auth/api";
|
|
5
|
+
import { getSessionFromCtx, originCheck } from "better-auth/api";
|
|
6
|
+
import type { Organization } from "better-auth/plugins/organization";
|
|
7
|
+
import { defu } from "defu";
|
|
10
8
|
import type Stripe from "stripe";
|
|
11
9
|
import type { Stripe as StripeType } from "stripe";
|
|
12
10
|
import * as z from "zod/v4";
|
|
@@ -17,14 +15,17 @@ import {
|
|
|
17
15
|
onSubscriptionDeleted,
|
|
18
16
|
onSubscriptionUpdated,
|
|
19
17
|
} from "./hooks";
|
|
20
|
-
import { referenceMiddleware } from "./middleware";
|
|
18
|
+
import { referenceMiddleware, stripeSessionMiddleware } from "./middleware";
|
|
21
19
|
import type {
|
|
22
|
-
|
|
20
|
+
CustomerType,
|
|
21
|
+
StripeCtxSession,
|
|
23
22
|
StripeOptions,
|
|
24
23
|
Subscription,
|
|
25
24
|
SubscriptionOptions,
|
|
25
|
+
WithStripeCustomerId,
|
|
26
26
|
} from "./types";
|
|
27
27
|
import {
|
|
28
|
+
escapeStripeSearchValue,
|
|
28
29
|
getPlanByName,
|
|
29
30
|
getPlanByPriceInfo,
|
|
30
31
|
getPlans,
|
|
@@ -33,6 +34,70 @@ import {
|
|
|
33
34
|
isStripePendingCancel,
|
|
34
35
|
} from "./utils";
|
|
35
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Converts a relative URL to an absolute URL using baseURL.
|
|
39
|
+
* @internal
|
|
40
|
+
*/
|
|
41
|
+
function getUrl(ctx: GenericEndpointContext, url: string) {
|
|
42
|
+
if (/^[a-zA-Z][a-zA-Z0-9+\-.]*:/.test(url)) {
|
|
43
|
+
return url;
|
|
44
|
+
}
|
|
45
|
+
return `${ctx.context.options.baseURL}${
|
|
46
|
+
url.startsWith("/") ? url : `/${url}`
|
|
47
|
+
}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolves a Stripe price ID from a lookup key.
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
async function resolvePriceIdFromLookupKey(
|
|
55
|
+
stripeClient: Stripe,
|
|
56
|
+
lookupKey: string,
|
|
57
|
+
): Promise<string | undefined> {
|
|
58
|
+
if (!lookupKey) return undefined;
|
|
59
|
+
const prices = await stripeClient.prices.list({
|
|
60
|
+
lookup_keys: [lookupKey],
|
|
61
|
+
active: true,
|
|
62
|
+
limit: 1,
|
|
63
|
+
});
|
|
64
|
+
return prices.data[0]?.id;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Determines the reference ID based on customer type.
|
|
69
|
+
* - `user` (default): uses userId
|
|
70
|
+
* - `organization`: uses activeOrganizationId from session
|
|
71
|
+
* @internal
|
|
72
|
+
*/
|
|
73
|
+
function getReferenceId(
|
|
74
|
+
ctxSession: StripeCtxSession,
|
|
75
|
+
customerType: CustomerType | undefined,
|
|
76
|
+
options: StripeOptions,
|
|
77
|
+
): string {
|
|
78
|
+
const { user, session } = ctxSession;
|
|
79
|
+
const type = customerType || "user";
|
|
80
|
+
|
|
81
|
+
if (type === "organization") {
|
|
82
|
+
if (!options.organization?.enabled) {
|
|
83
|
+
throw APIError.from(
|
|
84
|
+
"BAD_REQUEST",
|
|
85
|
+
STRIPE_ERROR_CODES.ORGANIZATION_SUBSCRIPTION_NOT_ENABLED,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!session.activeOrganizationId) {
|
|
90
|
+
throw APIError.from(
|
|
91
|
+
"BAD_REQUEST",
|
|
92
|
+
STRIPE_ERROR_CODES.ORGANIZATION_NOT_FOUND,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return session.activeOrganizationId;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return user.id;
|
|
99
|
+
}
|
|
100
|
+
|
|
36
101
|
const upgradeSubscriptionBodySchema = z.object({
|
|
37
102
|
/**
|
|
38
103
|
* The name of the plan to subscribe
|
|
@@ -50,14 +115,14 @@ const upgradeSubscriptionBodySchema = z.object({
|
|
|
50
115
|
})
|
|
51
116
|
.optional(),
|
|
52
117
|
/**
|
|
53
|
-
* Reference
|
|
54
|
-
*
|
|
55
|
-
*
|
|
118
|
+
* Reference ID for the subscription based on customerType:
|
|
119
|
+
* - `user`: defaults to `user.id`
|
|
120
|
+
* - `organization`: defaults to `session.activeOrganizationId`
|
|
56
121
|
*/
|
|
57
122
|
referenceId: z
|
|
58
123
|
.string()
|
|
59
124
|
.meta({
|
|
60
|
-
description: 'Reference
|
|
125
|
+
description: 'Reference ID for the subscription. Eg: "org_123"',
|
|
61
126
|
})
|
|
62
127
|
.optional(),
|
|
63
128
|
/**
|
|
@@ -72,12 +137,23 @@ const upgradeSubscriptionBodySchema = z.object({
|
|
|
72
137
|
})
|
|
73
138
|
.optional(),
|
|
74
139
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
140
|
+
* Customer type for the subscription.
|
|
141
|
+
* - `user`: User owns the subscription (default)
|
|
142
|
+
* - `organization`: Organization owns the subscription (requires referenceId)
|
|
143
|
+
*/
|
|
144
|
+
customerType: z
|
|
145
|
+
.enum(["user", "organization"])
|
|
146
|
+
.meta({
|
|
147
|
+
description:
|
|
148
|
+
'Customer type for the subscription. Eg: "user" or "organization"',
|
|
149
|
+
})
|
|
150
|
+
.optional(),
|
|
151
|
+
/**
|
|
152
|
+
* Additional metadata to store with the subscription.
|
|
77
153
|
*/
|
|
78
154
|
metadata: z.record(z.string(), z.any()).optional(),
|
|
79
155
|
/**
|
|
80
|
-
*
|
|
156
|
+
* Number of seats for subscriptions.
|
|
81
157
|
*/
|
|
82
158
|
seats: z
|
|
83
159
|
.number()
|
|
@@ -156,22 +232,27 @@ export const upgradeSubscription = (options: StripeOptions) => {
|
|
|
156
232
|
},
|
|
157
233
|
},
|
|
158
234
|
use: [
|
|
159
|
-
|
|
235
|
+
stripeSessionMiddleware,
|
|
236
|
+
referenceMiddleware(subscriptionOptions, "upgrade-subscription"),
|
|
160
237
|
originCheck((c) => {
|
|
161
238
|
return [c.body.successUrl as string, c.body.cancelUrl as string];
|
|
162
239
|
}),
|
|
163
|
-
referenceMiddleware(subscriptionOptions, "upgrade-subscription"),
|
|
164
240
|
],
|
|
165
241
|
},
|
|
166
242
|
async (ctx) => {
|
|
167
243
|
const { user, session } = ctx.context.session;
|
|
244
|
+
const customerType = ctx.body.customerType || "user";
|
|
245
|
+
const referenceId =
|
|
246
|
+
ctx.body.referenceId ||
|
|
247
|
+
getReferenceId(ctx.context.session, customerType, options);
|
|
248
|
+
|
|
168
249
|
if (!user.emailVerified && subscriptionOptions.requireEmailVerification) {
|
|
169
250
|
throw APIError.from(
|
|
170
251
|
"BAD_REQUEST",
|
|
171
252
|
STRIPE_ERROR_CODES.EMAIL_VERIFICATION_REQUIRED,
|
|
172
253
|
);
|
|
173
254
|
}
|
|
174
|
-
|
|
255
|
+
|
|
175
256
|
const plan = await getPlanByName(options, ctx.body.plan);
|
|
176
257
|
if (!plan) {
|
|
177
258
|
throw APIError.from(
|
|
@@ -179,6 +260,8 @@ export const upgradeSubscription = (options: StripeOptions) => {
|
|
|
179
260
|
STRIPE_ERROR_CODES.SUBSCRIPTION_PLAN_NOT_FOUND,
|
|
180
261
|
);
|
|
181
262
|
}
|
|
263
|
+
|
|
264
|
+
// Find existing subscription by Stripe ID or reference ID
|
|
182
265
|
let subscriptionToUpdate = ctx.body.subscriptionId
|
|
183
266
|
? await ctx.context.adapter.findOne<Subscription>({
|
|
184
267
|
model: "subscription",
|
|
@@ -192,7 +275,12 @@ export const upgradeSubscription = (options: StripeOptions) => {
|
|
|
192
275
|
: referenceId
|
|
193
276
|
? await ctx.context.adapter.findOne<Subscription>({
|
|
194
277
|
model: "subscription",
|
|
195
|
-
where: [
|
|
278
|
+
where: [
|
|
279
|
+
{
|
|
280
|
+
field: "referenceId",
|
|
281
|
+
value: referenceId,
|
|
282
|
+
},
|
|
283
|
+
],
|
|
196
284
|
})
|
|
197
285
|
: null;
|
|
198
286
|
|
|
@@ -203,7 +291,6 @@ export const upgradeSubscription = (options: StripeOptions) => {
|
|
|
203
291
|
) {
|
|
204
292
|
subscriptionToUpdate = null;
|
|
205
293
|
}
|
|
206
|
-
|
|
207
294
|
if (ctx.body.subscriptionId && !subscriptionToUpdate) {
|
|
208
295
|
throw APIError.from(
|
|
209
296
|
"BAD_REQUEST",
|
|
@@ -211,51 +298,154 @@ export const upgradeSubscription = (options: StripeOptions) => {
|
|
|
211
298
|
);
|
|
212
299
|
}
|
|
213
300
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
let stripeCustomer = existingCustomers.data[0];
|
|
226
|
-
|
|
227
|
-
if (!stripeCustomer) {
|
|
228
|
-
stripeCustomer = await client.customers.create({
|
|
229
|
-
email: user.email,
|
|
230
|
-
name: user.name,
|
|
231
|
-
metadata: {
|
|
232
|
-
...ctx.body.metadata,
|
|
233
|
-
userId: user.id,
|
|
234
|
-
},
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Update local DB with Stripe customer ID
|
|
239
|
-
await ctx.context.adapter.update({
|
|
240
|
-
model: "user",
|
|
241
|
-
update: {
|
|
242
|
-
stripeCustomerId: stripeCustomer.id,
|
|
243
|
-
},
|
|
301
|
+
// Determine customer id
|
|
302
|
+
let customerId: string | undefined;
|
|
303
|
+
if (customerType === "organization") {
|
|
304
|
+
// Organization subscription - get customer ID from organization
|
|
305
|
+
customerId = subscriptionToUpdate?.stripeCustomerId;
|
|
306
|
+
if (!customerId) {
|
|
307
|
+
const org = await ctx.context.adapter.findOne<
|
|
308
|
+
Organization & WithStripeCustomerId
|
|
309
|
+
>({
|
|
310
|
+
model: "organization",
|
|
244
311
|
where: [
|
|
245
312
|
{
|
|
246
313
|
field: "id",
|
|
247
|
-
value:
|
|
314
|
+
value: referenceId,
|
|
248
315
|
},
|
|
249
316
|
],
|
|
250
317
|
});
|
|
318
|
+
if (!org) {
|
|
319
|
+
throw APIError.from(
|
|
320
|
+
"BAD_REQUEST",
|
|
321
|
+
STRIPE_ERROR_CODES.ORGANIZATION_NOT_FOUND,
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
customerId = org.stripeCustomerId;
|
|
325
|
+
|
|
326
|
+
// If org doesn't have a customer ID, create one
|
|
327
|
+
if (!customerId) {
|
|
328
|
+
try {
|
|
329
|
+
// First, search for existing organization customer by organizationId
|
|
330
|
+
const existingOrgCustomers = await client.customers.search({
|
|
331
|
+
query: `metadata["organizationId"]:"${org.id}"`,
|
|
332
|
+
limit: 1,
|
|
333
|
+
});
|
|
251
334
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
335
|
+
let stripeCustomer = existingOrgCustomers.data[0];
|
|
336
|
+
|
|
337
|
+
if (!stripeCustomer) {
|
|
338
|
+
// Get custom params if provided
|
|
339
|
+
let extraCreateParams: Partial<StripeType.CustomerCreateParams> =
|
|
340
|
+
{};
|
|
341
|
+
if (options.organization?.getCustomerCreateParams) {
|
|
342
|
+
extraCreateParams =
|
|
343
|
+
await options.organization.getCustomerCreateParams(
|
|
344
|
+
org,
|
|
345
|
+
ctx,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Create Stripe customer for organization
|
|
350
|
+
// Email can be set via getCustomerCreateParams or updated in billing portal
|
|
351
|
+
// Use defu to ensure internal metadata fields are preserved
|
|
352
|
+
const customerParams: StripeType.CustomerCreateParams = defu(
|
|
353
|
+
{
|
|
354
|
+
name: org.name,
|
|
355
|
+
metadata: {
|
|
356
|
+
...ctx.body.metadata,
|
|
357
|
+
organizationId: org.id,
|
|
358
|
+
customerType: "organization",
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
extraCreateParams,
|
|
362
|
+
);
|
|
363
|
+
stripeCustomer = await client.customers.create(customerParams);
|
|
364
|
+
|
|
365
|
+
// Call onCustomerCreate callback only for newly created customers
|
|
366
|
+
await options.organization?.onCustomerCreate?.(
|
|
367
|
+
{
|
|
368
|
+
stripeCustomer,
|
|
369
|
+
organization: {
|
|
370
|
+
...org,
|
|
371
|
+
stripeCustomerId: stripeCustomer.id,
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
ctx,
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
await ctx.context.adapter.update({
|
|
379
|
+
model: "organization",
|
|
380
|
+
update: {
|
|
381
|
+
stripeCustomerId: stripeCustomer.id,
|
|
382
|
+
},
|
|
383
|
+
where: [
|
|
384
|
+
{
|
|
385
|
+
field: "id",
|
|
386
|
+
value: org.id,
|
|
387
|
+
},
|
|
388
|
+
],
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
customerId = stripeCustomer.id;
|
|
392
|
+
} catch (e: any) {
|
|
393
|
+
ctx.context.logger.error(e);
|
|
394
|
+
throw APIError.from(
|
|
395
|
+
"BAD_REQUEST",
|
|
396
|
+
STRIPE_ERROR_CODES.UNABLE_TO_CREATE_CUSTOMER,
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
} else {
|
|
402
|
+
// User subscription - get customer ID from user
|
|
403
|
+
customerId =
|
|
404
|
+
subscriptionToUpdate?.stripeCustomerId || user.stripeCustomerId;
|
|
405
|
+
if (!customerId) {
|
|
406
|
+
try {
|
|
407
|
+
// Try to find existing user Stripe customer by email
|
|
408
|
+
const existingCustomers = await client.customers.search({
|
|
409
|
+
query: `email:"${escapeStripeSearchValue(user.email)}" AND -metadata["customerType"]:"organization"`,
|
|
410
|
+
limit: 1,
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
let stripeCustomer = existingCustomers.data[0];
|
|
414
|
+
|
|
415
|
+
if (!stripeCustomer) {
|
|
416
|
+
stripeCustomer = await client.customers.create({
|
|
417
|
+
email: user.email,
|
|
418
|
+
name: user.name,
|
|
419
|
+
metadata: {
|
|
420
|
+
...ctx.body.metadata,
|
|
421
|
+
userId: user.id,
|
|
422
|
+
customerType: "user",
|
|
423
|
+
},
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Update local DB with Stripe customer ID
|
|
428
|
+
await ctx.context.adapter.update({
|
|
429
|
+
model: "user",
|
|
430
|
+
update: {
|
|
431
|
+
stripeCustomerId: stripeCustomer.id,
|
|
432
|
+
},
|
|
433
|
+
where: [
|
|
434
|
+
{
|
|
435
|
+
field: "id",
|
|
436
|
+
value: user.id,
|
|
437
|
+
},
|
|
438
|
+
],
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
customerId = stripeCustomer.id;
|
|
442
|
+
} catch (e: any) {
|
|
443
|
+
ctx.context.logger.error(e);
|
|
444
|
+
throw APIError.from(
|
|
445
|
+
"BAD_REQUEST",
|
|
446
|
+
STRIPE_ERROR_CODES.UNABLE_TO_CREATE_CUSTOMER,
|
|
447
|
+
);
|
|
448
|
+
}
|
|
259
449
|
}
|
|
260
450
|
}
|
|
261
451
|
|
|
@@ -266,7 +456,7 @@ export const upgradeSubscription = (options: StripeOptions) => {
|
|
|
266
456
|
where: [
|
|
267
457
|
{
|
|
268
458
|
field: "referenceId",
|
|
269
|
-
value:
|
|
459
|
+
value: referenceId,
|
|
270
460
|
},
|
|
271
461
|
],
|
|
272
462
|
});
|
|
@@ -330,7 +520,7 @@ export const upgradeSubscription = (options: StripeOptions) => {
|
|
|
330
520
|
|
|
331
521
|
// If no database record exists for this Stripe subscription, update the existing one
|
|
332
522
|
if (!dbSubscription && activeOrTrialingSubscription) {
|
|
333
|
-
await ctx.context.adapter.update<
|
|
523
|
+
await ctx.context.adapter.update<Subscription>({
|
|
334
524
|
model: "subscription",
|
|
335
525
|
update: {
|
|
336
526
|
stripeSubscriptionId: activeSubscription.id,
|
|
@@ -412,7 +602,7 @@ export const upgradeSubscription = (options: StripeOptions) => {
|
|
|
412
602
|
activeOrTrialingSubscription || incompleteSubscription;
|
|
413
603
|
|
|
414
604
|
if (incompleteSubscription && !activeOrTrialingSubscription) {
|
|
415
|
-
const updated = await ctx.context.adapter.update<
|
|
605
|
+
const updated = await ctx.context.adapter.update<Subscription>({
|
|
416
606
|
model: "subscription",
|
|
417
607
|
update: {
|
|
418
608
|
plan: plan.name.toLowerCase(),
|
|
@@ -430,10 +620,7 @@ export const upgradeSubscription = (options: StripeOptions) => {
|
|
|
430
620
|
}
|
|
431
621
|
|
|
432
622
|
if (!subscription) {
|
|
433
|
-
subscription = await ctx.context.adapter.create<
|
|
434
|
-
InputSubscription,
|
|
435
|
-
Subscription
|
|
436
|
-
>({
|
|
623
|
+
subscription = await ctx.context.adapter.create<Subscription>({
|
|
437
624
|
model: "subscription",
|
|
438
625
|
data: {
|
|
439
626
|
plan: plan.name.toLowerCase(),
|
|
@@ -447,7 +634,10 @@ export const upgradeSubscription = (options: StripeOptions) => {
|
|
|
447
634
|
|
|
448
635
|
if (!subscription) {
|
|
449
636
|
ctx.context.logger.error("Subscription ID not found");
|
|
450
|
-
throw
|
|
637
|
+
throw APIError.from(
|
|
638
|
+
"NOT_FOUND",
|
|
639
|
+
STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
|
|
640
|
+
);
|
|
451
641
|
}
|
|
452
642
|
|
|
453
643
|
const params = await subscriptionOptions.getCheckoutSessionParams?.(
|
|
@@ -504,13 +694,13 @@ export const upgradeSubscription = (options: StripeOptions) => {
|
|
|
504
694
|
...(customerId
|
|
505
695
|
? {
|
|
506
696
|
customer: customerId,
|
|
507
|
-
customer_update:
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
697
|
+
customer_update:
|
|
698
|
+
customerType !== "user"
|
|
699
|
+
? ({ address: "auto" } as const)
|
|
700
|
+
: ({ name: "auto", address: "auto" } as const), // The customer name is automatically set only for users
|
|
511
701
|
}
|
|
512
702
|
: {
|
|
513
|
-
customer_email:
|
|
703
|
+
customer_email: user.email,
|
|
514
704
|
}),
|
|
515
705
|
success_url: getUrl(
|
|
516
706
|
ctx,
|
|
@@ -529,15 +719,23 @@ export const upgradeSubscription = (options: StripeOptions) => {
|
|
|
529
719
|
],
|
|
530
720
|
subscription_data: {
|
|
531
721
|
...freeTrial,
|
|
722
|
+
metadata: {
|
|
723
|
+
...ctx.body.metadata,
|
|
724
|
+
...params?.params?.subscription_data?.metadata,
|
|
725
|
+
userId: user.id,
|
|
726
|
+
subscriptionId: subscription.id,
|
|
727
|
+
referenceId,
|
|
728
|
+
},
|
|
532
729
|
},
|
|
533
730
|
mode: "subscription",
|
|
534
731
|
client_reference_id: referenceId,
|
|
535
732
|
...params?.params,
|
|
536
733
|
metadata: {
|
|
734
|
+
...ctx.body.metadata,
|
|
735
|
+
...params?.params?.metadata,
|
|
537
736
|
userId: user.id,
|
|
538
737
|
subscriptionId: subscription.id,
|
|
539
738
|
referenceId,
|
|
540
|
-
...params?.params?.metadata,
|
|
541
739
|
},
|
|
542
740
|
},
|
|
543
741
|
params?.options,
|
|
@@ -579,9 +777,7 @@ export const cancelSubscriptionCallback = (options: StripeOptions) => {
|
|
|
579
777
|
if (!ctx.query || !ctx.query.callbackURL || !ctx.query.subscriptionId) {
|
|
580
778
|
throw ctx.redirect(getUrl(ctx, ctx.query?.callbackURL || "/"));
|
|
581
779
|
}
|
|
582
|
-
const session = await getSessionFromCtx<
|
|
583
|
-
ctx,
|
|
584
|
-
);
|
|
780
|
+
const session = await getSessionFromCtx<User & WithStripeCustomerId>(ctx);
|
|
585
781
|
if (!session) {
|
|
586
782
|
throw ctx.redirect(getUrl(ctx, ctx.query?.callbackURL || "/"));
|
|
587
783
|
}
|
|
@@ -673,6 +869,18 @@ const cancelSubscriptionBodySchema = z.object({
|
|
|
673
869
|
"The Stripe subscription ID to cancel. Eg: 'sub_1ABC2DEF3GHI4JKL'",
|
|
674
870
|
})
|
|
675
871
|
.optional(),
|
|
872
|
+
/**
|
|
873
|
+
* Customer type for the subscription.
|
|
874
|
+
* - `user`: User owns the subscription (default)
|
|
875
|
+
* - `organization`: Organization owns the subscription
|
|
876
|
+
*/
|
|
877
|
+
customerType: z
|
|
878
|
+
.enum(["user", "organization"])
|
|
879
|
+
.meta({
|
|
880
|
+
description:
|
|
881
|
+
'Customer type for the subscription. Eg: "user" or "organization"',
|
|
882
|
+
})
|
|
883
|
+
.optional(),
|
|
676
884
|
returnUrl: z.string().meta({
|
|
677
885
|
description:
|
|
678
886
|
'URL to take customers to when they click on the billing portal\'s link to return to your website. Eg: "/account"',
|
|
@@ -718,13 +926,17 @@ export const cancelSubscription = (options: StripeOptions) => {
|
|
|
718
926
|
},
|
|
719
927
|
},
|
|
720
928
|
use: [
|
|
721
|
-
|
|
722
|
-
originCheck((ctx) => ctx.body.returnUrl),
|
|
929
|
+
stripeSessionMiddleware,
|
|
723
930
|
referenceMiddleware(subscriptionOptions, "cancel-subscription"),
|
|
931
|
+
originCheck((ctx) => ctx.body.returnUrl),
|
|
724
932
|
],
|
|
725
933
|
},
|
|
726
934
|
async (ctx) => {
|
|
727
|
-
const
|
|
935
|
+
const customerType = ctx.body.customerType || "user";
|
|
936
|
+
const referenceId =
|
|
937
|
+
ctx.body.referenceId ||
|
|
938
|
+
getReferenceId(ctx.context.session, customerType, options);
|
|
939
|
+
|
|
728
940
|
let subscription = ctx.body.subscriptionId
|
|
729
941
|
? await ctx.context.adapter.findOne<Subscription>({
|
|
730
942
|
model: "subscription",
|
|
@@ -863,6 +1075,18 @@ const restoreSubscriptionBodySchema = z.object({
|
|
|
863
1075
|
"The Stripe subscription ID to restore. Eg: 'sub_1ABC2DEF3GHI4JKL'",
|
|
864
1076
|
})
|
|
865
1077
|
.optional(),
|
|
1078
|
+
/**
|
|
1079
|
+
* Customer type for the subscription.
|
|
1080
|
+
* - `user`: User owns the subscription (default)
|
|
1081
|
+
* - `organization`: Organization owns the subscription
|
|
1082
|
+
*/
|
|
1083
|
+
customerType: z
|
|
1084
|
+
.enum(["user", "organization"])
|
|
1085
|
+
.meta({
|
|
1086
|
+
description:
|
|
1087
|
+
'Customer type for the subscription. Eg: "user" or "organization"',
|
|
1088
|
+
})
|
|
1089
|
+
.optional(),
|
|
866
1090
|
});
|
|
867
1091
|
|
|
868
1092
|
export const restoreSubscription = (options: StripeOptions) => {
|
|
@@ -879,12 +1103,15 @@ export const restoreSubscription = (options: StripeOptions) => {
|
|
|
879
1103
|
},
|
|
880
1104
|
},
|
|
881
1105
|
use: [
|
|
882
|
-
|
|
1106
|
+
stripeSessionMiddleware,
|
|
883
1107
|
referenceMiddleware(subscriptionOptions, "restore-subscription"),
|
|
884
1108
|
],
|
|
885
1109
|
},
|
|
886
1110
|
async (ctx) => {
|
|
887
|
-
const
|
|
1111
|
+
const customerType = ctx.body.customerType || "user";
|
|
1112
|
+
const referenceId =
|
|
1113
|
+
ctx.body.referenceId ||
|
|
1114
|
+
getReferenceId(ctx.context.session, customerType, options);
|
|
888
1115
|
|
|
889
1116
|
let subscription = ctx.body.subscriptionId
|
|
890
1117
|
? await ctx.context.adapter.findOne<Subscription>({
|
|
@@ -920,10 +1147,7 @@ export const restoreSubscription = (options: StripeOptions) => {
|
|
|
920
1147
|
STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
|
|
921
1148
|
);
|
|
922
1149
|
}
|
|
923
|
-
if (
|
|
924
|
-
subscription.status != "active" &&
|
|
925
|
-
subscription.status != "trialing"
|
|
926
|
-
) {
|
|
1150
|
+
if (!isActiveOrTrialing(subscription)) {
|
|
927
1151
|
throw APIError.from(
|
|
928
1152
|
"BAD_REQUEST",
|
|
929
1153
|
STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_ACTIVE,
|
|
@@ -995,6 +1219,18 @@ const listActiveSubscriptionsQuerySchema = z.optional(
|
|
|
995
1219
|
description: "Reference id of the subscription to list. Eg: '123'",
|
|
996
1220
|
})
|
|
997
1221
|
.optional(),
|
|
1222
|
+
/**
|
|
1223
|
+
* Customer type for the subscription.
|
|
1224
|
+
* - `user`: User owns the subscription (default)
|
|
1225
|
+
* - `organization`: Organization owns the subscription
|
|
1226
|
+
*/
|
|
1227
|
+
customerType: z
|
|
1228
|
+
.enum(["user", "organization"])
|
|
1229
|
+
.meta({
|
|
1230
|
+
description:
|
|
1231
|
+
'Customer type for the subscription. Eg: "user" or "organization"',
|
|
1232
|
+
})
|
|
1233
|
+
.optional(),
|
|
998
1234
|
}),
|
|
999
1235
|
);
|
|
1000
1236
|
/**
|
|
@@ -1025,17 +1261,22 @@ export const listActiveSubscriptions = (options: StripeOptions) => {
|
|
|
1025
1261
|
},
|
|
1026
1262
|
},
|
|
1027
1263
|
use: [
|
|
1028
|
-
|
|
1264
|
+
stripeSessionMiddleware,
|
|
1029
1265
|
referenceMiddleware(subscriptionOptions, "list-subscription"),
|
|
1030
1266
|
],
|
|
1031
1267
|
},
|
|
1032
1268
|
async (ctx) => {
|
|
1269
|
+
const customerType = ctx.query?.customerType || "user";
|
|
1270
|
+
const referenceId =
|
|
1271
|
+
ctx.query?.referenceId ||
|
|
1272
|
+
getReferenceId(ctx.context.session, customerType, options);
|
|
1273
|
+
|
|
1033
1274
|
const subscriptions = await ctx.context.adapter.findMany<Subscription>({
|
|
1034
1275
|
model: "subscription",
|
|
1035
1276
|
where: [
|
|
1036
1277
|
{
|
|
1037
1278
|
field: "referenceId",
|
|
1038
|
-
value:
|
|
1279
|
+
value: referenceId,
|
|
1039
1280
|
},
|
|
1040
1281
|
],
|
|
1041
1282
|
});
|
|
@@ -1083,14 +1324,12 @@ export const subscriptionSuccess = (options: StripeOptions) => {
|
|
|
1083
1324
|
if (!ctx.query || !ctx.query.callbackURL || !ctx.query.subscriptionId) {
|
|
1084
1325
|
throw ctx.redirect(getUrl(ctx, ctx.query?.callbackURL || "/"));
|
|
1085
1326
|
}
|
|
1086
|
-
const
|
|
1087
|
-
|
|
1088
|
-
);
|
|
1327
|
+
const { callbackURL, subscriptionId } = ctx.query;
|
|
1328
|
+
|
|
1329
|
+
const session = await getSessionFromCtx<User & WithStripeCustomerId>(ctx);
|
|
1089
1330
|
if (!session) {
|
|
1090
1331
|
throw ctx.redirect(getUrl(ctx, ctx.query?.callbackURL || "/"));
|
|
1091
1332
|
}
|
|
1092
|
-
const { user } = session;
|
|
1093
|
-
const { callbackURL, subscriptionId } = ctx.query;
|
|
1094
1333
|
|
|
1095
1334
|
const subscription = await ctx.context.adapter.findOne<Subscription>({
|
|
1096
1335
|
model: "subscription",
|
|
@@ -1101,81 +1340,89 @@ export const subscriptionSuccess = (options: StripeOptions) => {
|
|
|
1101
1340
|
},
|
|
1102
1341
|
],
|
|
1103
1342
|
});
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
return ctx.redirect(getUrl(ctx, callbackURL));
|
|
1343
|
+
if (!subscription) {
|
|
1344
|
+
ctx.context.logger.warn(
|
|
1345
|
+
`Subscription record not found for subscriptionId: ${subscriptionId}`,
|
|
1346
|
+
);
|
|
1347
|
+
throw ctx.redirect(getUrl(ctx, callbackURL));
|
|
1110
1348
|
}
|
|
1111
|
-
const customerId =
|
|
1112
|
-
subscription?.stripeCustomerId || user.stripeCustomerId;
|
|
1113
1349
|
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
customer: customerId,
|
|
1119
|
-
status: "active",
|
|
1120
|
-
})
|
|
1121
|
-
.then((res) => res.data[0]);
|
|
1350
|
+
// Already active or trialing, no need to update
|
|
1351
|
+
if (isActiveOrTrialing(subscription)) {
|
|
1352
|
+
throw ctx.redirect(getUrl(ctx, callbackURL));
|
|
1353
|
+
}
|
|
1122
1354
|
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
);
|
|
1355
|
+
const customerId =
|
|
1356
|
+
subscription.stripeCustomerId || session.user.stripeCustomerId;
|
|
1357
|
+
if (!customerId) {
|
|
1358
|
+
throw ctx.redirect(getUrl(ctx, callbackURL));
|
|
1359
|
+
}
|
|
1129
1360
|
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
status: stripeSubscription.status,
|
|
1135
|
-
seats: stripeSubscription.items.data[0]?.quantity || 1,
|
|
1136
|
-
plan: plan.name.toLowerCase(),
|
|
1137
|
-
periodEnd: new Date(
|
|
1138
|
-
stripeSubscription.items.data[0]?.current_period_end! *
|
|
1139
|
-
1000,
|
|
1140
|
-
),
|
|
1141
|
-
periodStart: new Date(
|
|
1142
|
-
stripeSubscription.items.data[0]?.current_period_start! *
|
|
1143
|
-
1000,
|
|
1144
|
-
),
|
|
1145
|
-
stripeSubscriptionId: stripeSubscription.id,
|
|
1146
|
-
cancelAtPeriodEnd: stripeSubscription.cancel_at_period_end,
|
|
1147
|
-
cancelAt: stripeSubscription.cancel_at
|
|
1148
|
-
? new Date(stripeSubscription.cancel_at * 1000)
|
|
1149
|
-
: null,
|
|
1150
|
-
canceledAt: stripeSubscription.canceled_at
|
|
1151
|
-
? new Date(stripeSubscription.canceled_at * 1000)
|
|
1152
|
-
: null,
|
|
1153
|
-
...(stripeSubscription.trial_start &&
|
|
1154
|
-
stripeSubscription.trial_end
|
|
1155
|
-
? {
|
|
1156
|
-
trialStart: new Date(
|
|
1157
|
-
stripeSubscription.trial_start * 1000,
|
|
1158
|
-
),
|
|
1159
|
-
trialEnd: new Date(stripeSubscription.trial_end * 1000),
|
|
1160
|
-
}
|
|
1161
|
-
: {}),
|
|
1162
|
-
},
|
|
1163
|
-
where: [
|
|
1164
|
-
{
|
|
1165
|
-
field: "id",
|
|
1166
|
-
value: subscription.id,
|
|
1167
|
-
},
|
|
1168
|
-
],
|
|
1169
|
-
});
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
|
-
} catch (error) {
|
|
1361
|
+
const stripeSubscription = await client.subscriptions
|
|
1362
|
+
.list({ customer: customerId, status: "active" })
|
|
1363
|
+
.then((res) => res.data[0])
|
|
1364
|
+
.catch((error) => {
|
|
1173
1365
|
ctx.context.logger.error(
|
|
1174
1366
|
"Error fetching subscription from Stripe",
|
|
1175
1367
|
error,
|
|
1176
1368
|
);
|
|
1177
|
-
|
|
1369
|
+
throw ctx.redirect(getUrl(ctx, callbackURL));
|
|
1370
|
+
});
|
|
1371
|
+
if (!stripeSubscription) {
|
|
1372
|
+
throw ctx.redirect(getUrl(ctx, callbackURL));
|
|
1178
1373
|
}
|
|
1374
|
+
|
|
1375
|
+
const subscriptionItem = stripeSubscription.items.data[0];
|
|
1376
|
+
if (!subscriptionItem) {
|
|
1377
|
+
ctx.context.logger.warn(
|
|
1378
|
+
`No subscription items found for Stripe subscription ${stripeSubscription.id}`,
|
|
1379
|
+
);
|
|
1380
|
+
throw ctx.redirect(getUrl(ctx, callbackURL));
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
const plan = await getPlanByPriceInfo(
|
|
1384
|
+
options,
|
|
1385
|
+
subscriptionItem.price.id,
|
|
1386
|
+
subscriptionItem.price.lookup_key,
|
|
1387
|
+
);
|
|
1388
|
+
if (!plan) {
|
|
1389
|
+
ctx.context.logger.warn(
|
|
1390
|
+
`Plan not found for price ${subscriptionItem.price.id}`,
|
|
1391
|
+
);
|
|
1392
|
+
throw ctx.redirect(getUrl(ctx, callbackURL));
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
await ctx.context.adapter.update({
|
|
1396
|
+
model: "subscription",
|
|
1397
|
+
update: {
|
|
1398
|
+
status: stripeSubscription.status,
|
|
1399
|
+
seats: subscriptionItem.quantity || 1,
|
|
1400
|
+
plan: plan.name.toLowerCase(),
|
|
1401
|
+
periodEnd: new Date(subscriptionItem.current_period_end * 1000),
|
|
1402
|
+
periodStart: new Date(subscriptionItem.current_period_start * 1000),
|
|
1403
|
+
stripeSubscriptionId: stripeSubscription.id,
|
|
1404
|
+
cancelAtPeriodEnd: stripeSubscription.cancel_at_period_end,
|
|
1405
|
+
cancelAt: stripeSubscription.cancel_at
|
|
1406
|
+
? new Date(stripeSubscription.cancel_at * 1000)
|
|
1407
|
+
: null,
|
|
1408
|
+
canceledAt: stripeSubscription.canceled_at
|
|
1409
|
+
? new Date(stripeSubscription.canceled_at * 1000)
|
|
1410
|
+
: null,
|
|
1411
|
+
...(stripeSubscription.trial_start && stripeSubscription.trial_end
|
|
1412
|
+
? {
|
|
1413
|
+
trialStart: new Date(stripeSubscription.trial_start * 1000),
|
|
1414
|
+
trialEnd: new Date(stripeSubscription.trial_end * 1000),
|
|
1415
|
+
}
|
|
1416
|
+
: {}),
|
|
1417
|
+
},
|
|
1418
|
+
where: [
|
|
1419
|
+
{
|
|
1420
|
+
field: "id",
|
|
1421
|
+
value: subscription.id,
|
|
1422
|
+
},
|
|
1423
|
+
],
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1179
1426
|
throw ctx.redirect(getUrl(ctx, callbackURL));
|
|
1180
1427
|
},
|
|
1181
1428
|
);
|
|
@@ -1188,6 +1435,18 @@ const createBillingPortalBodySchema = z.object({
|
|
|
1188
1435
|
})
|
|
1189
1436
|
.optional(),
|
|
1190
1437
|
referenceId: z.string().optional(),
|
|
1438
|
+
/**
|
|
1439
|
+
* Customer type for the subscription.
|
|
1440
|
+
* - `user`: User owns the subscription (default)
|
|
1441
|
+
* - `organization`: Organization owns the subscription
|
|
1442
|
+
*/
|
|
1443
|
+
customerType: z
|
|
1444
|
+
.enum(["user", "organization"])
|
|
1445
|
+
.meta({
|
|
1446
|
+
description:
|
|
1447
|
+
'Customer type for the subscription. Eg: "user" or "organization"',
|
|
1448
|
+
})
|
|
1449
|
+
.optional(),
|
|
1191
1450
|
returnUrl: z.string().default("/"),
|
|
1192
1451
|
/**
|
|
1193
1452
|
* Disable Redirect
|
|
@@ -1215,37 +1474,61 @@ export const createBillingPortal = (options: StripeOptions) => {
|
|
|
1215
1474
|
},
|
|
1216
1475
|
},
|
|
1217
1476
|
use: [
|
|
1218
|
-
|
|
1219
|
-
originCheck((ctx) => ctx.body.returnUrl),
|
|
1477
|
+
stripeSessionMiddleware,
|
|
1220
1478
|
referenceMiddleware(subscriptionOptions, "billing-portal"),
|
|
1479
|
+
originCheck((ctx) => ctx.body.returnUrl),
|
|
1221
1480
|
],
|
|
1222
1481
|
},
|
|
1223
1482
|
async (ctx) => {
|
|
1224
1483
|
const { user } = ctx.context.session;
|
|
1225
|
-
const
|
|
1484
|
+
const customerType = ctx.body.customerType || "user";
|
|
1485
|
+
const referenceId =
|
|
1486
|
+
ctx.body.referenceId ||
|
|
1487
|
+
getReferenceId(ctx.context.session, customerType, options);
|
|
1226
1488
|
|
|
1227
|
-
let customerId
|
|
1489
|
+
let customerId: string | undefined;
|
|
1228
1490
|
|
|
1229
|
-
if (
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
],
|
|
1239
|
-
})
|
|
1240
|
-
.then((subs) => subs.find((sub) => isActiveOrTrialing(sub)));
|
|
1491
|
+
if (customerType === "organization") {
|
|
1492
|
+
// Organization billing portal - get customer ID from organization
|
|
1493
|
+
const org = await ctx.context.adapter.findOne<
|
|
1494
|
+
Organization & WithStripeCustomerId
|
|
1495
|
+
>({
|
|
1496
|
+
model: "organization",
|
|
1497
|
+
where: [{ field: "id", value: referenceId }],
|
|
1498
|
+
});
|
|
1499
|
+
customerId = org?.stripeCustomerId;
|
|
1241
1500
|
|
|
1242
|
-
customerId
|
|
1243
|
-
|
|
1501
|
+
if (!customerId) {
|
|
1502
|
+
// Fallback to subscription's stripeCustomerId
|
|
1503
|
+
const subscription = await ctx.context.adapter
|
|
1504
|
+
.findMany<Subscription>({
|
|
1505
|
+
model: "subscription",
|
|
1506
|
+
where: [{ field: "referenceId", value: referenceId }],
|
|
1507
|
+
})
|
|
1508
|
+
.then((subs) => subs.find((sub) => isActiveOrTrialing(sub)));
|
|
1509
|
+
customerId = subscription?.stripeCustomerId;
|
|
1510
|
+
}
|
|
1511
|
+
} else {
|
|
1512
|
+
// User billing portal
|
|
1513
|
+
customerId = user.stripeCustomerId;
|
|
1514
|
+
if (!customerId) {
|
|
1515
|
+
const subscription = await ctx.context.adapter
|
|
1516
|
+
.findMany<Subscription>({
|
|
1517
|
+
model: "subscription",
|
|
1518
|
+
where: [
|
|
1519
|
+
{
|
|
1520
|
+
field: "referenceId",
|
|
1521
|
+
value: referenceId,
|
|
1522
|
+
},
|
|
1523
|
+
],
|
|
1524
|
+
})
|
|
1525
|
+
.then((subs) => subs.find((sub) => isActiveOrTrialing(sub)));
|
|
1244
1526
|
|
|
1527
|
+
customerId = subscription?.stripeCustomerId;
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1245
1530
|
if (!customerId) {
|
|
1246
|
-
throw
|
|
1247
|
-
message: "No Stripe customer found for this user",
|
|
1248
|
-
});
|
|
1531
|
+
throw APIError.from("NOT_FOUND", STRIPE_ERROR_CODES.CUSTOMER_NOT_FOUND);
|
|
1249
1532
|
}
|
|
1250
1533
|
|
|
1251
1534
|
try {
|
|
@@ -1264,9 +1547,10 @@ export const createBillingPortal = (options: StripeOptions) => {
|
|
|
1264
1547
|
"Error creating billing portal session",
|
|
1265
1548
|
error,
|
|
1266
1549
|
);
|
|
1267
|
-
throw
|
|
1268
|
-
|
|
1269
|
-
|
|
1550
|
+
throw APIError.from(
|
|
1551
|
+
"INTERNAL_SERVER_ERROR",
|
|
1552
|
+
STRIPE_ERROR_CODES.UNABLE_TO_CREATE_BILLING_PORTAL,
|
|
1553
|
+
);
|
|
1270
1554
|
}
|
|
1271
1555
|
},
|
|
1272
1556
|
);
|
|
@@ -1285,45 +1569,60 @@ export const stripeWebhook = (options: StripeOptions) => {
|
|
|
1285
1569
|
},
|
|
1286
1570
|
},
|
|
1287
1571
|
cloneRequest: true,
|
|
1288
|
-
//
|
|
1289
|
-
disableBody: true,
|
|
1572
|
+
disableBody: true, // Don't parse the body
|
|
1290
1573
|
},
|
|
1291
1574
|
async (ctx) => {
|
|
1292
1575
|
if (!ctx.request?.body) {
|
|
1293
|
-
throw
|
|
1576
|
+
throw APIError.from(
|
|
1577
|
+
"BAD_REQUEST",
|
|
1578
|
+
STRIPE_ERROR_CODES.INVALID_REQUEST_BODY,
|
|
1579
|
+
);
|
|
1294
1580
|
}
|
|
1295
|
-
|
|
1296
|
-
const sig = ctx.request.headers.get("stripe-signature")
|
|
1581
|
+
|
|
1582
|
+
const sig = ctx.request.headers.get("stripe-signature");
|
|
1583
|
+
if (!sig) {
|
|
1584
|
+
throw APIError.from(
|
|
1585
|
+
"BAD_REQUEST",
|
|
1586
|
+
STRIPE_ERROR_CODES.STRIPE_SIGNATURE_NOT_FOUND,
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1297
1590
|
const webhookSecret = options.stripeWebhookSecret;
|
|
1591
|
+
if (!webhookSecret) {
|
|
1592
|
+
throw APIError.from(
|
|
1593
|
+
"INTERNAL_SERVER_ERROR",
|
|
1594
|
+
STRIPE_ERROR_CODES.STRIPE_WEBHOOK_SECRET_NOT_FOUND,
|
|
1595
|
+
);
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
const payload = await ctx.request.text();
|
|
1599
|
+
|
|
1298
1600
|
let event: Stripe.Event;
|
|
1299
1601
|
try {
|
|
1300
|
-
if (!sig || !webhookSecret) {
|
|
1301
|
-
throw new APIError("BAD_REQUEST", {
|
|
1302
|
-
message: "Stripe webhook secret not found",
|
|
1303
|
-
});
|
|
1304
|
-
}
|
|
1305
1602
|
// Support both Stripe v18 (constructEvent) and v19+ (constructEventAsync)
|
|
1306
1603
|
if (typeof client.webhooks.constructEventAsync === "function") {
|
|
1307
1604
|
// Stripe v19+ - use async method
|
|
1308
1605
|
event = await client.webhooks.constructEventAsync(
|
|
1309
|
-
|
|
1606
|
+
payload,
|
|
1310
1607
|
sig,
|
|
1311
1608
|
webhookSecret,
|
|
1312
1609
|
);
|
|
1313
1610
|
} else {
|
|
1314
1611
|
// Stripe v18 - use sync method
|
|
1315
|
-
event = client.webhooks.constructEvent(
|
|
1612
|
+
event = client.webhooks.constructEvent(payload, sig, webhookSecret);
|
|
1316
1613
|
}
|
|
1317
1614
|
} catch (err: any) {
|
|
1318
1615
|
ctx.context.logger.error(`${err.message}`);
|
|
1319
|
-
throw
|
|
1320
|
-
|
|
1321
|
-
|
|
1616
|
+
throw APIError.from(
|
|
1617
|
+
"BAD_REQUEST",
|
|
1618
|
+
STRIPE_ERROR_CODES.FAILED_TO_CONSTRUCT_STRIPE_EVENT,
|
|
1619
|
+
);
|
|
1322
1620
|
}
|
|
1323
1621
|
if (!event) {
|
|
1324
|
-
throw
|
|
1325
|
-
|
|
1326
|
-
|
|
1622
|
+
throw APIError.from(
|
|
1623
|
+
"BAD_REQUEST",
|
|
1624
|
+
STRIPE_ERROR_CODES.FAILED_TO_CONSTRUCT_STRIPE_EVENT,
|
|
1625
|
+
);
|
|
1327
1626
|
}
|
|
1328
1627
|
try {
|
|
1329
1628
|
switch (event.type) {
|
|
@@ -1349,33 +1648,12 @@ export const stripeWebhook = (options: StripeOptions) => {
|
|
|
1349
1648
|
}
|
|
1350
1649
|
} catch (e: any) {
|
|
1351
1650
|
ctx.context.logger.error(`Stripe webhook failed. Error: ${e.message}`);
|
|
1352
|
-
throw
|
|
1353
|
-
|
|
1354
|
-
|
|
1651
|
+
throw APIError.from(
|
|
1652
|
+
"BAD_REQUEST",
|
|
1653
|
+
STRIPE_ERROR_CODES.STRIPE_WEBHOOK_ERROR,
|
|
1654
|
+
);
|
|
1355
1655
|
}
|
|
1356
1656
|
return ctx.json({ success: true });
|
|
1357
1657
|
},
|
|
1358
1658
|
);
|
|
1359
1659
|
};
|
|
1360
|
-
|
|
1361
|
-
const getUrl = (ctx: GenericEndpointContext, url: string) => {
|
|
1362
|
-
if (/^[a-zA-Z][a-zA-Z0-9+\-.]*:/.test(url)) {
|
|
1363
|
-
return url;
|
|
1364
|
-
}
|
|
1365
|
-
return `${ctx.context.options.baseURL}${
|
|
1366
|
-
url.startsWith("/") ? url : `/${url}`
|
|
1367
|
-
}`;
|
|
1368
|
-
};
|
|
1369
|
-
|
|
1370
|
-
async function resolvePriceIdFromLookupKey(
|
|
1371
|
-
stripeClient: Stripe,
|
|
1372
|
-
lookupKey: string,
|
|
1373
|
-
): Promise<string | undefined> {
|
|
1374
|
-
if (!lookupKey) return undefined;
|
|
1375
|
-
const prices = await stripeClient.prices.list({
|
|
1376
|
-
lookup_keys: [lookupKey],
|
|
1377
|
-
active: true,
|
|
1378
|
-
limit: 1,
|
|
1379
|
-
});
|
|
1380
|
-
return prices.data[0]?.id;
|
|
1381
|
-
}
|