@better-auth/stripe 1.4.10-beta.1 → 1.4.11-beta.1
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 +8 -9
- package/dist/client.d.mts +3 -72
- package/dist/client.mjs +5 -5
- package/dist/{index-DpiQGYLJ.d.mts → index-CkO4CTbB.d.mts} +276 -187
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +652 -230
- package/package.json +5 -5
- package/src/client.ts +1 -3
- package/src/error-codes.ts +16 -0
- package/src/hooks.ts +229 -53
- package/src/index.ts +141 -46
- package/src/middleware.ts +89 -41
- package/src/routes.ts +638 -337
- package/src/schema.ts +30 -0
- package/src/types.ts +105 -20
- package/src/utils.ts +36 -1
- package/test/stripe-organization.test.ts +1993 -0
- package/{src → test}/stripe.test.ts +3350 -1404
- package/dist/error-codes-qqooUh6R.mjs +0 -16
package/dist/index.mjs
CHANGED
|
@@ -1,13 +1,37 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { defineErrorCodes } from "@better-auth/core/utils";
|
|
1
|
+
import { APIError, HIDE_METADATA } from "better-auth";
|
|
3
2
|
import { defu } from "defu";
|
|
3
|
+
import { defineErrorCodes } from "@better-auth/core/utils";
|
|
4
4
|
import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api";
|
|
5
|
-
import { APIError } from "@better-auth/core/error";
|
|
6
|
-
import { HIDE_METADATA, logger } from "better-auth";
|
|
7
5
|
import { APIError as APIError$1, getSessionFromCtx, originCheck, sessionMiddleware } from "better-auth/api";
|
|
8
6
|
import * as z from "zod/v4";
|
|
9
7
|
import { mergeSchema } from "better-auth/db";
|
|
10
8
|
|
|
9
|
+
//#region src/error-codes.ts
|
|
10
|
+
const STRIPE_ERROR_CODES = defineErrorCodes({
|
|
11
|
+
UNAUTHORIZED: "Unauthorized access",
|
|
12
|
+
INVALID_REQUEST_BODY: "Invalid request body",
|
|
13
|
+
SUBSCRIPTION_NOT_FOUND: "Subscription not found",
|
|
14
|
+
SUBSCRIPTION_PLAN_NOT_FOUND: "Subscription plan not found",
|
|
15
|
+
ALREADY_SUBSCRIBED_PLAN: "You're already subscribed to this plan",
|
|
16
|
+
REFERENCE_ID_NOT_ALLOWED: "Reference id is not allowed",
|
|
17
|
+
CUSTOMER_NOT_FOUND: "Stripe customer not found for this user",
|
|
18
|
+
UNABLE_TO_CREATE_CUSTOMER: "Unable to create customer",
|
|
19
|
+
UNABLE_TO_CREATE_BILLING_PORTAL: "Unable to create billing portal session",
|
|
20
|
+
STRIPE_SIGNATURE_NOT_FOUND: "Stripe signature not found",
|
|
21
|
+
STRIPE_WEBHOOK_SECRET_NOT_FOUND: "Stripe webhook secret not found",
|
|
22
|
+
STRIPE_WEBHOOK_ERROR: "Stripe webhook error",
|
|
23
|
+
FAILED_TO_CONSTRUCT_STRIPE_EVENT: "Failed to construct Stripe event",
|
|
24
|
+
FAILED_TO_FETCH_PLANS: "Failed to fetch plans",
|
|
25
|
+
EMAIL_VERIFICATION_REQUIRED: "Email verification is required before you can subscribe to a plan",
|
|
26
|
+
SUBSCRIPTION_NOT_ACTIVE: "Subscription is not active",
|
|
27
|
+
SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION: "Subscription is not scheduled for cancellation",
|
|
28
|
+
ORGANIZATION_NOT_FOUND: "Organization not found",
|
|
29
|
+
ORGANIZATION_SUBSCRIPTION_NOT_ENABLED: "Organization subscription is not enabled",
|
|
30
|
+
ORGANIZATION_HAS_ACTIVE_SUBSCRIPTION: "Cannot delete organization with active subscription",
|
|
31
|
+
ORGANIZATION_REFERENCE_ID_REQUIRED: "Reference ID is required. Provide referenceId or set activeOrganizationId in session"
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
//#endregion
|
|
11
35
|
//#region src/utils.ts
|
|
12
36
|
async function getPlans(subscriptionOptions) {
|
|
13
37
|
if (subscriptionOptions?.enabled) return typeof subscriptionOptions.plans === "function" ? await subscriptionOptions.plans() : subscriptionOptions.plans;
|
|
@@ -19,21 +43,86 @@ async function getPlanByPriceInfo(options, priceId, priceLookupKey) {
|
|
|
19
43
|
async function getPlanByName(options, name) {
|
|
20
44
|
return await getPlans(options.subscription).then((res) => res?.find((plan) => plan.name.toLowerCase() === name.toLowerCase()));
|
|
21
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Checks if a subscription is in an available state (active or trialing)
|
|
48
|
+
*/
|
|
49
|
+
function isActiveOrTrialing(sub) {
|
|
50
|
+
return sub.status === "active" || sub.status === "trialing";
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Check if a subscription is scheduled to be canceled (DB subscription object)
|
|
54
|
+
*/
|
|
55
|
+
function isPendingCancel(sub) {
|
|
56
|
+
return !!(sub.cancelAtPeriodEnd || sub.cancelAt);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Check if a Stripe subscription is scheduled to be canceled (Stripe API response)
|
|
60
|
+
*/
|
|
61
|
+
function isStripePendingCancel(stripeSub) {
|
|
62
|
+
return !!(stripeSub.cancel_at_period_end || stripeSub.cancel_at);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Escapes a value for use in Stripe search queries.
|
|
66
|
+
* Stripe search query uses double quotes for string values,
|
|
67
|
+
* and double quotes within the value need to be escaped with backslash.
|
|
68
|
+
*
|
|
69
|
+
* @see https://docs.stripe.com/search#search-query-language
|
|
70
|
+
*/
|
|
71
|
+
function escapeStripeSearchValue(value) {
|
|
72
|
+
return value.replace(/"/g, "\\\"");
|
|
73
|
+
}
|
|
22
74
|
|
|
23
75
|
//#endregion
|
|
24
76
|
//#region src/hooks.ts
|
|
77
|
+
/**
|
|
78
|
+
* Find organization or user by stripeCustomerId.
|
|
79
|
+
* @internal
|
|
80
|
+
*/
|
|
81
|
+
async function findReferenceByStripeCustomerId(ctx, options, stripeCustomerId) {
|
|
82
|
+
if (options.organization?.enabled) {
|
|
83
|
+
const org = await ctx.context.adapter.findOne({
|
|
84
|
+
model: "organization",
|
|
85
|
+
where: [{
|
|
86
|
+
field: "stripeCustomerId",
|
|
87
|
+
value: stripeCustomerId
|
|
88
|
+
}]
|
|
89
|
+
});
|
|
90
|
+
if (org) return {
|
|
91
|
+
customerType: "organization",
|
|
92
|
+
referenceId: org.id
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const user$1 = await ctx.context.adapter.findOne({
|
|
96
|
+
model: "user",
|
|
97
|
+
where: [{
|
|
98
|
+
field: "stripeCustomerId",
|
|
99
|
+
value: stripeCustomerId
|
|
100
|
+
}]
|
|
101
|
+
});
|
|
102
|
+
if (user$1) return {
|
|
103
|
+
customerType: "user",
|
|
104
|
+
referenceId: user$1.id
|
|
105
|
+
};
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
25
108
|
async function onCheckoutSessionCompleted(ctx, options, event) {
|
|
26
109
|
try {
|
|
27
110
|
const client = options.stripeClient;
|
|
28
111
|
const checkoutSession = event.data.object;
|
|
29
112
|
if (checkoutSession.mode === "setup" || !options.subscription?.enabled) return;
|
|
30
113
|
const subscription = await client.subscriptions.retrieve(checkoutSession.subscription);
|
|
31
|
-
const
|
|
32
|
-
|
|
114
|
+
const subscriptionItem = subscription.items.data[0];
|
|
115
|
+
if (!subscriptionItem) {
|
|
116
|
+
ctx.context.logger.warn(`Stripe webhook warning: Subscription ${subscription.id} has no items`);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const priceId = subscriptionItem.price.id;
|
|
120
|
+
const priceLookupKey = subscriptionItem.price.lookup_key;
|
|
121
|
+
const plan = await getPlanByPriceInfo(options, priceId, priceLookupKey);
|
|
33
122
|
if (plan) {
|
|
34
123
|
const referenceId = checkoutSession?.client_reference_id || checkoutSession?.metadata?.referenceId;
|
|
35
124
|
const subscriptionId = checkoutSession?.metadata?.subscriptionId;
|
|
36
|
-
const seats =
|
|
125
|
+
const seats = subscriptionItem.quantity;
|
|
37
126
|
if (referenceId && subscriptionId) {
|
|
38
127
|
const trial = subscription.trial_start && subscription.trial_end ? {
|
|
39
128
|
trialStart: /* @__PURE__ */ new Date(subscription.trial_start * 1e3),
|
|
@@ -45,9 +134,13 @@ async function onCheckoutSessionCompleted(ctx, options, event) {
|
|
|
45
134
|
plan: plan.name.toLowerCase(),
|
|
46
135
|
status: subscription.status,
|
|
47
136
|
updatedAt: /* @__PURE__ */ new Date(),
|
|
48
|
-
periodStart: /* @__PURE__ */ new Date(
|
|
49
|
-
periodEnd: /* @__PURE__ */ new Date(
|
|
137
|
+
periodStart: /* @__PURE__ */ new Date(subscriptionItem.current_period_start * 1e3),
|
|
138
|
+
periodEnd: /* @__PURE__ */ new Date(subscriptionItem.current_period_end * 1e3),
|
|
50
139
|
stripeSubscriptionId: checkoutSession.subscription,
|
|
140
|
+
cancelAtPeriodEnd: subscription.cancel_at_period_end,
|
|
141
|
+
cancelAt: subscription.cancel_at ? /* @__PURE__ */ new Date(subscription.cancel_at * 1e3) : null,
|
|
142
|
+
canceledAt: subscription.canceled_at ? /* @__PURE__ */ new Date(subscription.canceled_at * 1e3) : null,
|
|
143
|
+
endedAt: subscription.ended_at ? /* @__PURE__ */ new Date(subscription.ended_at * 1e3) : null,
|
|
51
144
|
seats,
|
|
52
145
|
...trial
|
|
53
146
|
},
|
|
@@ -74,15 +167,95 @@ async function onCheckoutSessionCompleted(ctx, options, event) {
|
|
|
74
167
|
}
|
|
75
168
|
}
|
|
76
169
|
} catch (e) {
|
|
77
|
-
logger.error(`Stripe webhook failed. Error: ${e.message}`);
|
|
170
|
+
ctx.context.logger.error(`Stripe webhook failed. Error: ${e.message}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async function onSubscriptionCreated(ctx, options, event) {
|
|
174
|
+
try {
|
|
175
|
+
if (!options.subscription?.enabled) return;
|
|
176
|
+
const subscriptionCreated = event.data.object;
|
|
177
|
+
const stripeCustomerId = subscriptionCreated.customer?.toString();
|
|
178
|
+
if (!stripeCustomerId) {
|
|
179
|
+
ctx.context.logger.warn(`Stripe webhook warning: customer.subscription.created event received without customer ID`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const subscriptionId = subscriptionCreated.metadata?.subscriptionId;
|
|
183
|
+
const existingSubscription = await ctx.context.adapter.findOne({
|
|
184
|
+
model: "subscription",
|
|
185
|
+
where: subscriptionId ? [{
|
|
186
|
+
field: "id",
|
|
187
|
+
value: subscriptionId
|
|
188
|
+
}] : [{
|
|
189
|
+
field: "stripeSubscriptionId",
|
|
190
|
+
value: subscriptionCreated.id
|
|
191
|
+
}]
|
|
192
|
+
});
|
|
193
|
+
if (existingSubscription) {
|
|
194
|
+
ctx.context.logger.info(`Stripe webhook: Subscription already exists in database (id: ${existingSubscription.id}), skipping creation`);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const reference = await findReferenceByStripeCustomerId(ctx, options, stripeCustomerId);
|
|
198
|
+
if (!reference) {
|
|
199
|
+
ctx.context.logger.warn(`Stripe webhook warning: No user or organization found with stripeCustomerId: ${stripeCustomerId}`);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const { referenceId, customerType } = reference;
|
|
203
|
+
const subscriptionItem = subscriptionCreated.items.data[0];
|
|
204
|
+
if (!subscriptionItem) {
|
|
205
|
+
ctx.context.logger.warn(`Stripe webhook warning: Subscription ${subscriptionCreated.id} has no items`);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const priceId = subscriptionItem.price.id;
|
|
209
|
+
const plan = await getPlanByPriceInfo(options, priceId, subscriptionItem.price.lookup_key || null);
|
|
210
|
+
if (!plan) {
|
|
211
|
+
ctx.context.logger.warn(`Stripe webhook warning: No matching plan found for priceId: ${priceId}`);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const seats = subscriptionItem.quantity;
|
|
215
|
+
const periodStart = /* @__PURE__ */ new Date(subscriptionItem.current_period_start * 1e3);
|
|
216
|
+
const periodEnd = /* @__PURE__ */ new Date(subscriptionItem.current_period_end * 1e3);
|
|
217
|
+
const trial = subscriptionCreated.trial_start && subscriptionCreated.trial_end ? {
|
|
218
|
+
trialStart: /* @__PURE__ */ new Date(subscriptionCreated.trial_start * 1e3),
|
|
219
|
+
trialEnd: /* @__PURE__ */ new Date(subscriptionCreated.trial_end * 1e3)
|
|
220
|
+
} : {};
|
|
221
|
+
const newSubscription = await ctx.context.adapter.create({
|
|
222
|
+
model: "subscription",
|
|
223
|
+
data: {
|
|
224
|
+
referenceId,
|
|
225
|
+
stripeCustomerId,
|
|
226
|
+
stripeSubscriptionId: subscriptionCreated.id,
|
|
227
|
+
status: subscriptionCreated.status,
|
|
228
|
+
plan: plan.name.toLowerCase(),
|
|
229
|
+
periodStart,
|
|
230
|
+
periodEnd,
|
|
231
|
+
seats,
|
|
232
|
+
...plan.limits ? { limits: plan.limits } : {},
|
|
233
|
+
...trial
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
ctx.context.logger.info(`Stripe webhook: Created subscription ${subscriptionCreated.id} for ${customerType} ${referenceId} from dashboard`);
|
|
237
|
+
await options.subscription.onSubscriptionCreated?.({
|
|
238
|
+
event,
|
|
239
|
+
subscription: newSubscription,
|
|
240
|
+
stripeSubscription: subscriptionCreated,
|
|
241
|
+
plan
|
|
242
|
+
});
|
|
243
|
+
} catch (error) {
|
|
244
|
+
ctx.context.logger.error(`Stripe webhook failed. Error: ${error}`);
|
|
78
245
|
}
|
|
79
246
|
}
|
|
80
247
|
async function onSubscriptionUpdated(ctx, options, event) {
|
|
81
248
|
try {
|
|
82
249
|
if (!options.subscription?.enabled) return;
|
|
83
250
|
const subscriptionUpdated = event.data.object;
|
|
84
|
-
const
|
|
85
|
-
|
|
251
|
+
const subscriptionItem = subscriptionUpdated.items.data[0];
|
|
252
|
+
if (!subscriptionItem) {
|
|
253
|
+
ctx.context.logger.warn(`Stripe webhook warning: Subscription ${subscriptionUpdated.id} has no items`);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const priceId = subscriptionItem.price.id;
|
|
257
|
+
const priceLookupKey = subscriptionItem.price.lookup_key;
|
|
258
|
+
const plan = await getPlanByPriceInfo(options, priceId, priceLookupKey);
|
|
86
259
|
const subscriptionId = subscriptionUpdated.metadata?.subscriptionId;
|
|
87
260
|
const customerId = subscriptionUpdated.customer?.toString();
|
|
88
261
|
let subscription = await ctx.context.adapter.findOne({
|
|
@@ -104,15 +277,14 @@ async function onSubscriptionUpdated(ctx, options, event) {
|
|
|
104
277
|
}]
|
|
105
278
|
});
|
|
106
279
|
if (subs.length > 1) {
|
|
107
|
-
const activeSub = subs.find((sub) => sub
|
|
280
|
+
const activeSub = subs.find((sub) => isActiveOrTrialing(sub));
|
|
108
281
|
if (!activeSub) {
|
|
109
|
-
logger.warn(`Stripe webhook error: Multiple subscriptions found for customerId: ${customerId} and no active subscription is found`);
|
|
282
|
+
ctx.context.logger.warn(`Stripe webhook error: Multiple subscriptions found for customerId: ${customerId} and no active subscription is found`);
|
|
110
283
|
return;
|
|
111
284
|
}
|
|
112
285
|
subscription = activeSub;
|
|
113
286
|
} else subscription = subs[0];
|
|
114
287
|
}
|
|
115
|
-
const seats = subscriptionUpdated.items.data[0].quantity;
|
|
116
288
|
const updatedSubscription = await ctx.context.adapter.update({
|
|
117
289
|
model: "subscription",
|
|
118
290
|
update: {
|
|
@@ -122,10 +294,13 @@ async function onSubscriptionUpdated(ctx, options, event) {
|
|
|
122
294
|
} : {},
|
|
123
295
|
updatedAt: /* @__PURE__ */ new Date(),
|
|
124
296
|
status: subscriptionUpdated.status,
|
|
125
|
-
periodStart: /* @__PURE__ */ new Date(
|
|
126
|
-
periodEnd: /* @__PURE__ */ new Date(
|
|
297
|
+
periodStart: /* @__PURE__ */ new Date(subscriptionItem.current_period_start * 1e3),
|
|
298
|
+
periodEnd: /* @__PURE__ */ new Date(subscriptionItem.current_period_end * 1e3),
|
|
127
299
|
cancelAtPeriodEnd: subscriptionUpdated.cancel_at_period_end,
|
|
128
|
-
|
|
300
|
+
cancelAt: subscriptionUpdated.cancel_at ? /* @__PURE__ */ new Date(subscriptionUpdated.cancel_at * 1e3) : null,
|
|
301
|
+
canceledAt: subscriptionUpdated.canceled_at ? /* @__PURE__ */ new Date(subscriptionUpdated.canceled_at * 1e3) : null,
|
|
302
|
+
endedAt: subscriptionUpdated.ended_at ? /* @__PURE__ */ new Date(subscriptionUpdated.ended_at * 1e3) : null,
|
|
303
|
+
seats: subscriptionItem.quantity,
|
|
129
304
|
stripeSubscriptionId: subscriptionUpdated.id
|
|
130
305
|
},
|
|
131
306
|
where: [{
|
|
@@ -133,7 +308,7 @@ async function onSubscriptionUpdated(ctx, options, event) {
|
|
|
133
308
|
value: subscription.id
|
|
134
309
|
}]
|
|
135
310
|
});
|
|
136
|
-
if (subscriptionUpdated.status === "active" && subscriptionUpdated
|
|
311
|
+
if (subscriptionUpdated.status === "active" && isStripePendingCancel(subscriptionUpdated) && !isPendingCancel(subscription)) await options.subscription.onSubscriptionCancel?.({
|
|
137
312
|
subscription,
|
|
138
313
|
cancellationDetails: subscriptionUpdated.cancellation_details || void 0,
|
|
139
314
|
stripeSubscription: subscriptionUpdated,
|
|
@@ -148,7 +323,7 @@ async function onSubscriptionUpdated(ctx, options, event) {
|
|
|
148
323
|
if (subscriptionUpdated.status === "incomplete_expired" && subscription.status === "trialing" && plan.freeTrial?.onTrialExpired) await plan.freeTrial.onTrialExpired(subscription, ctx);
|
|
149
324
|
}
|
|
150
325
|
} catch (error) {
|
|
151
|
-
logger.error(`Stripe webhook failed. Error: ${error}`);
|
|
326
|
+
ctx.context.logger.error(`Stripe webhook failed. Error: ${error}`);
|
|
152
327
|
}
|
|
153
328
|
}
|
|
154
329
|
async function onSubscriptionDeleted(ctx, options, event) {
|
|
@@ -172,7 +347,11 @@ async function onSubscriptionDeleted(ctx, options, event) {
|
|
|
172
347
|
}],
|
|
173
348
|
update: {
|
|
174
349
|
status: "canceled",
|
|
175
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
350
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
351
|
+
cancelAtPeriodEnd: subscriptionDeleted.cancel_at_period_end,
|
|
352
|
+
cancelAt: subscriptionDeleted.cancel_at ? /* @__PURE__ */ new Date(subscriptionDeleted.cancel_at * 1e3) : null,
|
|
353
|
+
canceledAt: subscriptionDeleted.canceled_at ? /* @__PURE__ */ new Date(subscriptionDeleted.canceled_at * 1e3) : null,
|
|
354
|
+
endedAt: subscriptionDeleted.ended_at ? /* @__PURE__ */ new Date(subscriptionDeleted.ended_at * 1e3) : null
|
|
176
355
|
}
|
|
177
356
|
});
|
|
178
357
|
await options.subscription.onSubscriptionDeleted?.({
|
|
@@ -180,41 +359,94 @@ async function onSubscriptionDeleted(ctx, options, event) {
|
|
|
180
359
|
stripeSubscription: subscriptionDeleted,
|
|
181
360
|
subscription
|
|
182
361
|
});
|
|
183
|
-
} else logger.warn(`Stripe webhook error: Subscription not found for subscriptionId: ${subscriptionId}`);
|
|
362
|
+
} else ctx.context.logger.warn(`Stripe webhook error: Subscription not found for subscriptionId: ${subscriptionId}`);
|
|
184
363
|
} catch (error) {
|
|
185
|
-
logger.error(`Stripe webhook failed. Error: ${error}`);
|
|
364
|
+
ctx.context.logger.error(`Stripe webhook failed. Error: ${error}`);
|
|
186
365
|
}
|
|
187
366
|
}
|
|
188
367
|
|
|
189
368
|
//#endregion
|
|
190
369
|
//#region src/middleware.ts
|
|
370
|
+
const stripeSessionMiddleware = createAuthMiddleware({ use: [sessionMiddleware] }, async (ctx) => {
|
|
371
|
+
return { session: ctx.context.session };
|
|
372
|
+
});
|
|
191
373
|
const referenceMiddleware = (subscriptionOptions, action) => createAuthMiddleware(async (ctx) => {
|
|
192
|
-
const
|
|
193
|
-
if (!
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
374
|
+
const ctxSession = ctx.context.session;
|
|
375
|
+
if (!ctxSession) throw new APIError$1("UNAUTHORIZED", { message: STRIPE_ERROR_CODES.UNAUTHORIZED });
|
|
376
|
+
const customerType = ctx.body?.customerType || ctx.query?.customerType;
|
|
377
|
+
const explicitReferenceId = ctx.body?.referenceId || ctx.query?.referenceId;
|
|
378
|
+
if (customerType === "organization") {
|
|
379
|
+
if (!subscriptionOptions.authorizeReference) {
|
|
380
|
+
ctx.context.logger.error(`Organization subscriptions require authorizeReference to be defined in your stripe plugin config.`);
|
|
381
|
+
throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.ORGANIZATION_SUBSCRIPTION_NOT_ENABLED);
|
|
382
|
+
}
|
|
383
|
+
const referenceId = explicitReferenceId || ctxSession.session.activeOrganizationId;
|
|
384
|
+
if (!referenceId) throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.ORGANIZATION_REFERENCE_ID_REQUIRED);
|
|
385
|
+
if (!await subscriptionOptions.authorizeReference({
|
|
386
|
+
user: ctxSession.user,
|
|
387
|
+
session: ctxSession.session,
|
|
388
|
+
referenceId,
|
|
389
|
+
action
|
|
390
|
+
}, ctx)) throw APIError$1.from("UNAUTHORIZED", STRIPE_ERROR_CODES.UNAUTHORIZED);
|
|
391
|
+
return;
|
|
198
392
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
393
|
+
if (!explicitReferenceId) return;
|
|
394
|
+
if (explicitReferenceId === ctxSession.user.id) return;
|
|
395
|
+
if (!subscriptionOptions.authorizeReference) {
|
|
396
|
+
ctx.context.logger.error(`Passing referenceId into a subscription action isn't allowed if subscription.authorizeReference isn't defined in your stripe plugin config.`);
|
|
397
|
+
throw new APIError$1("BAD_REQUEST", { message: STRIPE_ERROR_CODES.REFERENCE_ID_NOT_ALLOWED });
|
|
398
|
+
}
|
|
399
|
+
if (!await subscriptionOptions.authorizeReference({
|
|
400
|
+
user: ctxSession.user,
|
|
401
|
+
session: ctxSession.session,
|
|
402
|
+
referenceId: explicitReferenceId,
|
|
207
403
|
action
|
|
208
|
-
}, ctx)
|
|
404
|
+
}, ctx)) throw new APIError$1("UNAUTHORIZED", { message: STRIPE_ERROR_CODES.UNAUTHORIZED });
|
|
209
405
|
});
|
|
210
406
|
|
|
211
407
|
//#endregion
|
|
212
408
|
//#region src/routes.ts
|
|
409
|
+
/**
|
|
410
|
+
* Converts a relative URL to an absolute URL using baseURL.
|
|
411
|
+
* @internal
|
|
412
|
+
*/
|
|
413
|
+
function getUrl(ctx, url) {
|
|
414
|
+
if (/^[a-zA-Z][a-zA-Z0-9+\-.]*:/.test(url)) return url;
|
|
415
|
+
return `${ctx.context.options.baseURL}${url.startsWith("/") ? url : `/${url}`}`;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Resolves a Stripe price ID from a lookup key.
|
|
419
|
+
* @internal
|
|
420
|
+
*/
|
|
421
|
+
async function resolvePriceIdFromLookupKey(stripeClient, lookupKey) {
|
|
422
|
+
if (!lookupKey) return void 0;
|
|
423
|
+
return (await stripeClient.prices.list({
|
|
424
|
+
lookup_keys: [lookupKey],
|
|
425
|
+
active: true,
|
|
426
|
+
limit: 1
|
|
427
|
+
})).data[0]?.id;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Determines the reference ID based on customer type.
|
|
431
|
+
* - `user` (default): uses userId
|
|
432
|
+
* - `organization`: uses activeOrganizationId from session
|
|
433
|
+
* @internal
|
|
434
|
+
*/
|
|
435
|
+
function getReferenceId(ctxSession, customerType, options) {
|
|
436
|
+
const { user: user$1, session } = ctxSession;
|
|
437
|
+
if ((customerType || "user") === "organization") {
|
|
438
|
+
if (!options.organization?.enabled) throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.ORGANIZATION_SUBSCRIPTION_NOT_ENABLED);
|
|
439
|
+
if (!session.activeOrganizationId) throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.ORGANIZATION_NOT_FOUND);
|
|
440
|
+
return session.activeOrganizationId;
|
|
441
|
+
}
|
|
442
|
+
return user$1.id;
|
|
443
|
+
}
|
|
213
444
|
const upgradeSubscriptionBodySchema = z.object({
|
|
214
445
|
plan: z.string().meta({ description: "The name of the plan to upgrade to. Eg: \"pro\"" }),
|
|
215
446
|
annual: z.boolean().meta({ description: "Whether to upgrade to an annual plan. Eg: true" }).optional(),
|
|
216
|
-
referenceId: z.string().meta({ description: "Reference
|
|
447
|
+
referenceId: z.string().meta({ description: "Reference ID for the subscription. Eg: \"org_123\"" }).optional(),
|
|
217
448
|
subscriptionId: z.string().meta({ description: "The Stripe subscription ID to upgrade. Eg: \"sub_1ABC2DEF3GHI4JKL\"" }).optional(),
|
|
449
|
+
customerType: z.enum(["user", "organization"]).meta({ description: "Customer type for the subscription. Eg: \"user\" or \"organization\"" }).optional(),
|
|
218
450
|
metadata: z.record(z.string(), z.any()).optional(),
|
|
219
451
|
seats: z.number().meta({ description: "Number of seats to upgrade to (if applicable). Eg: 1" }).optional(),
|
|
220
452
|
successUrl: z.string().meta({ description: "Callback URL to redirect back after successful subscription. Eg: \"https://example.com/success\"" }).default("/"),
|
|
@@ -245,18 +477,19 @@ const upgradeSubscription = (options) => {
|
|
|
245
477
|
body: upgradeSubscriptionBodySchema,
|
|
246
478
|
metadata: { openapi: { operationId: "upgradeSubscription" } },
|
|
247
479
|
use: [
|
|
248
|
-
|
|
480
|
+
stripeSessionMiddleware,
|
|
481
|
+
referenceMiddleware(subscriptionOptions, "upgrade-subscription"),
|
|
249
482
|
originCheck((c) => {
|
|
250
483
|
return [c.body.successUrl, c.body.cancelUrl];
|
|
251
|
-
})
|
|
252
|
-
referenceMiddleware(subscriptionOptions, "upgrade-subscription")
|
|
484
|
+
})
|
|
253
485
|
]
|
|
254
486
|
}, async (ctx) => {
|
|
255
487
|
const { user: user$1, session } = ctx.context.session;
|
|
256
|
-
|
|
257
|
-
const referenceId = ctx.body.referenceId ||
|
|
488
|
+
const customerType = ctx.body.customerType || "user";
|
|
489
|
+
const referenceId = ctx.body.referenceId || getReferenceId(ctx.context.session, customerType, options);
|
|
490
|
+
if (!user$1.emailVerified && subscriptionOptions.requireEmailVerification) throw new APIError$1("BAD_REQUEST", { message: STRIPE_ERROR_CODES.EMAIL_VERIFICATION_REQUIRED });
|
|
258
491
|
const plan = await getPlanByName(options, ctx.body.plan);
|
|
259
|
-
if (!plan) throw APIError
|
|
492
|
+
if (!plan) throw new APIError$1("BAD_REQUEST", { message: STRIPE_ERROR_CODES.SUBSCRIPTION_PLAN_NOT_FOUND });
|
|
260
493
|
let subscriptionToUpdate = ctx.body.subscriptionId ? await ctx.context.adapter.findOne({
|
|
261
494
|
model: "subscription",
|
|
262
495
|
where: [{
|
|
@@ -271,49 +504,104 @@ const upgradeSubscription = (options) => {
|
|
|
271
504
|
}]
|
|
272
505
|
}) : null;
|
|
273
506
|
if (ctx.body.subscriptionId && subscriptionToUpdate && subscriptionToUpdate.referenceId !== referenceId) subscriptionToUpdate = null;
|
|
274
|
-
if (ctx.body.subscriptionId && !subscriptionToUpdate) throw APIError
|
|
275
|
-
let customerId
|
|
276
|
-
if (
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
507
|
+
if (ctx.body.subscriptionId && !subscriptionToUpdate) throw new APIError$1("BAD_REQUEST", { message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND });
|
|
508
|
+
let customerId;
|
|
509
|
+
if (customerType === "organization") {
|
|
510
|
+
customerId = subscriptionToUpdate?.stripeCustomerId;
|
|
511
|
+
if (!customerId) {
|
|
512
|
+
const org = await ctx.context.adapter.findOne({
|
|
513
|
+
model: "organization",
|
|
514
|
+
where: [{
|
|
515
|
+
field: "id",
|
|
516
|
+
value: referenceId
|
|
517
|
+
}]
|
|
518
|
+
});
|
|
519
|
+
if (!org) throw new APIError$1("BAD_REQUEST", { message: STRIPE_ERROR_CODES.ORGANIZATION_NOT_FOUND });
|
|
520
|
+
customerId = org.stripeCustomerId;
|
|
521
|
+
if (!customerId) try {
|
|
522
|
+
let stripeCustomer = (await client.customers.search({
|
|
523
|
+
query: `metadata["organizationId"]:"${org.id}"`,
|
|
524
|
+
limit: 1
|
|
525
|
+
})).data[0];
|
|
526
|
+
if (!stripeCustomer) {
|
|
527
|
+
let extraCreateParams = {};
|
|
528
|
+
if (options.organization?.getCustomerCreateParams) extraCreateParams = await options.organization.getCustomerCreateParams(org, ctx);
|
|
529
|
+
const customerParams = defu({
|
|
530
|
+
name: org.name,
|
|
531
|
+
metadata: {
|
|
532
|
+
...ctx.body.metadata,
|
|
533
|
+
organizationId: org.id,
|
|
534
|
+
customerType: "organization"
|
|
535
|
+
}
|
|
536
|
+
}, extraCreateParams);
|
|
537
|
+
stripeCustomer = await client.customers.create(customerParams);
|
|
538
|
+
await options.organization?.onCustomerCreate?.({
|
|
539
|
+
stripeCustomer,
|
|
540
|
+
organization: {
|
|
541
|
+
...org,
|
|
542
|
+
stripeCustomerId: stripeCustomer.id
|
|
543
|
+
}
|
|
544
|
+
}, ctx);
|
|
545
|
+
}
|
|
546
|
+
await ctx.context.adapter.update({
|
|
547
|
+
model: "organization",
|
|
548
|
+
update: { stripeCustomerId: stripeCustomer.id },
|
|
549
|
+
where: [{
|
|
550
|
+
field: "id",
|
|
551
|
+
value: org.id
|
|
552
|
+
}]
|
|
553
|
+
});
|
|
554
|
+
customerId = stripeCustomer.id;
|
|
555
|
+
} catch (e) {
|
|
556
|
+
ctx.context.logger.error(e);
|
|
557
|
+
throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.UNABLE_TO_CREATE_CUSTOMER);
|
|
287
558
|
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
}]
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
559
|
+
}
|
|
560
|
+
} else {
|
|
561
|
+
customerId = subscriptionToUpdate?.stripeCustomerId || user$1.stripeCustomerId;
|
|
562
|
+
if (!customerId) try {
|
|
563
|
+
let stripeCustomer = (await client.customers.search({
|
|
564
|
+
query: `email:"${escapeStripeSearchValue(user$1.email)}" AND -metadata["customerType"]:"organization"`,
|
|
565
|
+
limit: 1
|
|
566
|
+
})).data[0];
|
|
567
|
+
if (!stripeCustomer) stripeCustomer = await client.customers.create({
|
|
568
|
+
email: user$1.email,
|
|
569
|
+
name: user$1.name,
|
|
570
|
+
metadata: {
|
|
571
|
+
...ctx.body.metadata,
|
|
572
|
+
userId: user$1.id,
|
|
573
|
+
customerType: "user"
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
await ctx.context.adapter.update({
|
|
577
|
+
model: "user",
|
|
578
|
+
update: { stripeCustomerId: stripeCustomer.id },
|
|
579
|
+
where: [{
|
|
580
|
+
field: "id",
|
|
581
|
+
value: user$1.id
|
|
582
|
+
}]
|
|
583
|
+
});
|
|
584
|
+
customerId = stripeCustomer.id;
|
|
585
|
+
} catch (e) {
|
|
586
|
+
ctx.context.logger.error(e);
|
|
587
|
+
throw new APIError$1("BAD_REQUEST", { message: STRIPE_ERROR_CODES.UNABLE_TO_CREATE_CUSTOMER });
|
|
588
|
+
}
|
|
301
589
|
}
|
|
302
590
|
const subscriptions$1 = subscriptionToUpdate ? [subscriptionToUpdate] : await ctx.context.adapter.findMany({
|
|
303
591
|
model: "subscription",
|
|
304
592
|
where: [{
|
|
305
593
|
field: "referenceId",
|
|
306
|
-
value:
|
|
594
|
+
value: referenceId
|
|
307
595
|
}]
|
|
308
596
|
});
|
|
309
|
-
const activeOrTrialingSubscription = subscriptions$1.find((sub) => sub
|
|
310
|
-
const activeSubscription = (await client.subscriptions.list({ customer: customerId }).then((res) => res.data.filter((sub) => sub
|
|
597
|
+
const activeOrTrialingSubscription = subscriptions$1.find((sub) => isActiveOrTrialing(sub));
|
|
598
|
+
const activeSubscription = (await client.subscriptions.list({ customer: customerId }).then((res) => res.data.filter((sub) => isActiveOrTrialing(sub)))).find((sub) => {
|
|
311
599
|
if (subscriptionToUpdate?.stripeSubscriptionId || ctx.body.subscriptionId) return sub.id === subscriptionToUpdate?.stripeSubscriptionId || sub.id === ctx.body.subscriptionId;
|
|
312
600
|
if (activeOrTrialingSubscription?.stripeSubscriptionId) return sub.id === activeOrTrialingSubscription.stripeSubscriptionId;
|
|
313
601
|
return false;
|
|
314
602
|
});
|
|
315
603
|
const incompleteSubscription = subscriptions$1.find((sub) => sub.status === "incomplete");
|
|
316
|
-
if (activeOrTrialingSubscription && activeOrTrialingSubscription.status === "active" && activeOrTrialingSubscription.plan === ctx.body.plan && activeOrTrialingSubscription.seats === (ctx.body.seats || 1)) throw APIError
|
|
604
|
+
if (activeOrTrialingSubscription && activeOrTrialingSubscription.status === "active" && activeOrTrialingSubscription.plan === ctx.body.plan && activeOrTrialingSubscription.seats === (ctx.body.seats || 1)) throw new APIError$1("BAD_REQUEST", { message: STRIPE_ERROR_CODES.ALREADY_SUBSCRIBED_PLAN });
|
|
317
605
|
if (activeSubscription && customerId) {
|
|
318
606
|
let dbSubscription = await ctx.context.adapter.findOne({
|
|
319
607
|
model: "subscription",
|
|
@@ -371,7 +659,7 @@ const upgradeSubscription = (options) => {
|
|
|
371
659
|
});
|
|
372
660
|
return ctx.json({
|
|
373
661
|
url,
|
|
374
|
-
redirect:
|
|
662
|
+
redirect: !ctx.body.disableRedirect
|
|
375
663
|
});
|
|
376
664
|
}
|
|
377
665
|
let subscription = activeOrTrialingSubscription || incompleteSubscription;
|
|
@@ -399,7 +687,7 @@ const upgradeSubscription = (options) => {
|
|
|
399
687
|
});
|
|
400
688
|
if (!subscription) {
|
|
401
689
|
ctx.context.logger.error("Subscription ID not found");
|
|
402
|
-
throw new APIError("
|
|
690
|
+
throw new APIError$1("NOT_FOUND", { message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND });
|
|
403
691
|
}
|
|
404
692
|
const params = await subscriptionOptions.getCheckoutSessionParams?.({
|
|
405
693
|
user: user$1,
|
|
@@ -407,7 +695,13 @@ const upgradeSubscription = (options) => {
|
|
|
407
695
|
plan,
|
|
408
696
|
subscription
|
|
409
697
|
}, ctx.request, ctx);
|
|
410
|
-
const freeTrial = !
|
|
698
|
+
const freeTrial = !(await ctx.context.adapter.findMany({
|
|
699
|
+
model: "subscription",
|
|
700
|
+
where: [{
|
|
701
|
+
field: "referenceId",
|
|
702
|
+
value: referenceId
|
|
703
|
+
}]
|
|
704
|
+
})).some((s) => {
|
|
411
705
|
return !!(s.trialStart || s.trialEnd) || s.status === "trialing";
|
|
412
706
|
}) && plan.freeTrial ? { trial_period_days: plan.freeTrial.days } : void 0;
|
|
413
707
|
let priceIdToUse = void 0;
|
|
@@ -421,26 +715,36 @@ const upgradeSubscription = (options) => {
|
|
|
421
715
|
const checkoutSession = await client.checkout.sessions.create({
|
|
422
716
|
...customerId ? {
|
|
423
717
|
customer: customerId,
|
|
424
|
-
customer_update: {
|
|
718
|
+
customer_update: customerType !== "user" ? { address: "auto" } : {
|
|
425
719
|
name: "auto",
|
|
426
720
|
address: "auto"
|
|
427
721
|
}
|
|
428
|
-
} : { customer_email:
|
|
722
|
+
} : { customer_email: user$1.email },
|
|
429
723
|
success_url: getUrl(ctx, `${ctx.context.baseURL}/subscription/success?callbackURL=${encodeURIComponent(ctx.body.successUrl)}&subscriptionId=${encodeURIComponent(subscription.id)}`),
|
|
430
724
|
cancel_url: getUrl(ctx, ctx.body.cancelUrl),
|
|
431
725
|
line_items: [{
|
|
432
726
|
price: priceIdToUse,
|
|
433
727
|
quantity: ctx.body.seats || 1
|
|
434
728
|
}],
|
|
435
|
-
subscription_data: {
|
|
729
|
+
subscription_data: {
|
|
730
|
+
...freeTrial,
|
|
731
|
+
metadata: {
|
|
732
|
+
...ctx.body.metadata,
|
|
733
|
+
...params?.params?.subscription_data?.metadata,
|
|
734
|
+
userId: user$1.id,
|
|
735
|
+
subscriptionId: subscription.id,
|
|
736
|
+
referenceId
|
|
737
|
+
}
|
|
738
|
+
},
|
|
436
739
|
mode: "subscription",
|
|
437
740
|
client_reference_id: referenceId,
|
|
438
741
|
...params?.params,
|
|
439
742
|
metadata: {
|
|
743
|
+
...ctx.body.metadata,
|
|
744
|
+
...params?.params?.metadata,
|
|
440
745
|
userId: user$1.id,
|
|
441
746
|
subscriptionId: subscription.id,
|
|
442
|
-
referenceId
|
|
443
|
-
...params?.params?.metadata
|
|
747
|
+
referenceId
|
|
444
748
|
}
|
|
445
749
|
}, params?.options).catch(async (e) => {
|
|
446
750
|
throw ctx.error("BAD_REQUEST", {
|
|
@@ -477,17 +781,19 @@ const cancelSubscriptionCallback = (options) => {
|
|
|
477
781
|
value: subscriptionId
|
|
478
782
|
}]
|
|
479
783
|
});
|
|
480
|
-
if (!subscription || subscription.
|
|
784
|
+
if (!subscription || subscription.status === "canceled" || isPendingCancel(subscription)) throw ctx.redirect(getUrl(ctx, callbackURL));
|
|
481
785
|
const currentSubscription = (await client.subscriptions.list({
|
|
482
786
|
customer: user$1.stripeCustomerId,
|
|
483
787
|
status: "active"
|
|
484
788
|
})).data.find((sub) => sub.id === subscription.stripeSubscriptionId);
|
|
485
|
-
if (currentSubscription
|
|
789
|
+
if (currentSubscription && isStripePendingCancel(currentSubscription) && !isPendingCancel(subscription)) {
|
|
486
790
|
await ctx.context.adapter.update({
|
|
487
791
|
model: "subscription",
|
|
488
792
|
update: {
|
|
489
793
|
status: currentSubscription?.status,
|
|
490
|
-
cancelAtPeriodEnd:
|
|
794
|
+
cancelAtPeriodEnd: currentSubscription?.cancel_at_period_end || false,
|
|
795
|
+
cancelAt: currentSubscription?.cancel_at ? /* @__PURE__ */ new Date(currentSubscription.cancel_at * 1e3) : null,
|
|
796
|
+
canceledAt: currentSubscription?.canceled_at ? /* @__PURE__ */ new Date(currentSubscription.canceled_at * 1e3) : null
|
|
491
797
|
},
|
|
492
798
|
where: [{
|
|
493
799
|
field: "id",
|
|
@@ -510,7 +816,9 @@ const cancelSubscriptionCallback = (options) => {
|
|
|
510
816
|
const cancelSubscriptionBodySchema = z.object({
|
|
511
817
|
referenceId: z.string().meta({ description: "Reference id of the subscription to cancel. Eg: '123'" }).optional(),
|
|
512
818
|
subscriptionId: z.string().meta({ description: "The Stripe subscription ID to cancel. Eg: 'sub_1ABC2DEF3GHI4JKL'" }).optional(),
|
|
513
|
-
|
|
819
|
+
customerType: z.enum(["user", "organization"]).meta({ description: "Customer type for the subscription. Eg: \"user\" or \"organization\"" }).optional(),
|
|
820
|
+
returnUrl: z.string().meta({ description: "URL to take customers to when they click on the billing portal's link to return to your website. Eg: \"/account\"" }),
|
|
821
|
+
disableRedirect: z.boolean().meta({ description: "Disable redirect after successful subscription cancellation. Eg: true" }).default(false)
|
|
514
822
|
});
|
|
515
823
|
/**
|
|
516
824
|
* ### Endpoint
|
|
@@ -535,12 +843,13 @@ const cancelSubscription = (options) => {
|
|
|
535
843
|
body: cancelSubscriptionBodySchema,
|
|
536
844
|
metadata: { openapi: { operationId: "cancelSubscription" } },
|
|
537
845
|
use: [
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
846
|
+
stripeSessionMiddleware,
|
|
847
|
+
referenceMiddleware(subscriptionOptions, "cancel-subscription"),
|
|
848
|
+
originCheck((ctx) => ctx.body.returnUrl)
|
|
541
849
|
]
|
|
542
850
|
}, async (ctx) => {
|
|
543
|
-
const
|
|
851
|
+
const customerType = ctx.body.customerType || "user";
|
|
852
|
+
const referenceId = ctx.body.referenceId || getReferenceId(ctx.context.session, customerType, options);
|
|
544
853
|
let subscription = ctx.body.subscriptionId ? await ctx.context.adapter.findOne({
|
|
545
854
|
model: "subscription",
|
|
546
855
|
where: [{
|
|
@@ -553,10 +862,10 @@ const cancelSubscription = (options) => {
|
|
|
553
862
|
field: "referenceId",
|
|
554
863
|
value: referenceId
|
|
555
864
|
}]
|
|
556
|
-
}).then((subs) => subs.find((sub) => sub
|
|
865
|
+
}).then((subs) => subs.find((sub) => isActiveOrTrialing(sub)));
|
|
557
866
|
if (ctx.body.subscriptionId && subscription && subscription.referenceId !== referenceId) subscription = void 0;
|
|
558
|
-
if (!subscription || !subscription.stripeCustomerId) throw
|
|
559
|
-
const activeSubscriptions = await client.subscriptions.list({ customer: subscription.stripeCustomerId }).then((res) => res.data.filter((sub) => sub
|
|
867
|
+
if (!subscription || !subscription.stripeCustomerId) throw ctx.error("BAD_REQUEST", { message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND });
|
|
868
|
+
const activeSubscriptions = await client.subscriptions.list({ customer: subscription.stripeCustomerId }).then((res) => res.data.filter((sub) => isActiveOrTrialing(sub)));
|
|
560
869
|
if (!activeSubscriptions.length) {
|
|
561
870
|
/**
|
|
562
871
|
* If the subscription is not found, we need to delete the subscription
|
|
@@ -569,10 +878,10 @@ const cancelSubscription = (options) => {
|
|
|
569
878
|
value: referenceId
|
|
570
879
|
}]
|
|
571
880
|
});
|
|
572
|
-
throw
|
|
881
|
+
throw ctx.error("BAD_REQUEST", { message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND });
|
|
573
882
|
}
|
|
574
883
|
const activeSubscription = activeSubscriptions.find((sub) => sub.id === subscription.stripeSubscriptionId);
|
|
575
|
-
if (!activeSubscription) throw
|
|
884
|
+
if (!activeSubscription) throw ctx.error("BAD_REQUEST", { message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND });
|
|
576
885
|
const { url } = await client.billingPortal.sessions.create({
|
|
577
886
|
customer: subscription.stripeCustomerId,
|
|
578
887
|
return_url: getUrl(ctx, `${ctx.context.baseURL}/subscription/cancel/callback?callbackURL=${encodeURIComponent(ctx.body?.returnUrl || "/")}&subscriptionId=${encodeURIComponent(subscription.id)}`),
|
|
@@ -581,34 +890,42 @@ const cancelSubscription = (options) => {
|
|
|
581
890
|
subscription_cancel: { subscription: activeSubscription.id }
|
|
582
891
|
}
|
|
583
892
|
}).catch(async (e) => {
|
|
584
|
-
if (e.message
|
|
893
|
+
if (e.message?.includes("already set to be canceled")) {
|
|
585
894
|
/**
|
|
586
|
-
* in-case we missed the event from stripe, we
|
|
895
|
+
* in-case we missed the event from stripe, we sync the actual state
|
|
587
896
|
* this is a rare case and should not happen
|
|
588
897
|
*/
|
|
589
|
-
if (!subscription
|
|
590
|
-
|
|
591
|
-
update
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
898
|
+
if (!isPendingCancel(subscription)) {
|
|
899
|
+
const stripeSub = await client.subscriptions.retrieve(activeSubscription.id);
|
|
900
|
+
await ctx.context.adapter.update({
|
|
901
|
+
model: "subscription",
|
|
902
|
+
update: {
|
|
903
|
+
cancelAtPeriodEnd: stripeSub.cancel_at_period_end,
|
|
904
|
+
cancelAt: stripeSub.cancel_at ? /* @__PURE__ */ new Date(stripeSub.cancel_at * 1e3) : null,
|
|
905
|
+
canceledAt: stripeSub.canceled_at ? /* @__PURE__ */ new Date(stripeSub.canceled_at * 1e3) : null
|
|
906
|
+
},
|
|
907
|
+
where: [{
|
|
908
|
+
field: "id",
|
|
909
|
+
value: subscription.id
|
|
910
|
+
}]
|
|
911
|
+
});
|
|
912
|
+
}
|
|
597
913
|
}
|
|
598
914
|
throw ctx.error("BAD_REQUEST", {
|
|
599
915
|
message: e.message,
|
|
600
916
|
code: e.code
|
|
601
917
|
});
|
|
602
918
|
});
|
|
603
|
-
return {
|
|
919
|
+
return ctx.json({
|
|
604
920
|
url,
|
|
605
|
-
redirect:
|
|
606
|
-
};
|
|
921
|
+
redirect: !ctx.body.disableRedirect
|
|
922
|
+
});
|
|
607
923
|
});
|
|
608
924
|
};
|
|
609
925
|
const restoreSubscriptionBodySchema = z.object({
|
|
610
926
|
referenceId: z.string().meta({ description: "Reference id of the subscription to restore. Eg: '123'" }).optional(),
|
|
611
|
-
subscriptionId: z.string().meta({ description: "The Stripe subscription ID to restore. Eg: 'sub_1ABC2DEF3GHI4JKL'" }).optional()
|
|
927
|
+
subscriptionId: z.string().meta({ description: "The Stripe subscription ID to restore. Eg: 'sub_1ABC2DEF3GHI4JKL'" }).optional(),
|
|
928
|
+
customerType: z.enum(["user", "organization"]).meta({ description: "Customer type for the subscription. Eg: \"user\" or \"organization\"" }).optional()
|
|
612
929
|
});
|
|
613
930
|
const restoreSubscription = (options) => {
|
|
614
931
|
const client = options.stripeClient;
|
|
@@ -617,9 +934,10 @@ const restoreSubscription = (options) => {
|
|
|
617
934
|
method: "POST",
|
|
618
935
|
body: restoreSubscriptionBodySchema,
|
|
619
936
|
metadata: { openapi: { operationId: "restoreSubscription" } },
|
|
620
|
-
use: [
|
|
937
|
+
use: [stripeSessionMiddleware, referenceMiddleware(subscriptionOptions, "restore-subscription")]
|
|
621
938
|
}, async (ctx) => {
|
|
622
|
-
const
|
|
939
|
+
const customerType = ctx.body.customerType || "user";
|
|
940
|
+
const referenceId = ctx.body.referenceId || getReferenceId(ctx.context.session, customerType, options);
|
|
623
941
|
let subscription = ctx.body.subscriptionId ? await ctx.context.adapter.findOne({
|
|
624
942
|
model: "subscription",
|
|
625
943
|
where: [{
|
|
@@ -632,34 +950,42 @@ const restoreSubscription = (options) => {
|
|
|
632
950
|
field: "referenceId",
|
|
633
951
|
value: referenceId
|
|
634
952
|
}]
|
|
635
|
-
}).then((subs) => subs.find((sub) => sub
|
|
953
|
+
}).then((subs) => subs.find((sub) => isActiveOrTrialing(sub)));
|
|
636
954
|
if (ctx.body.subscriptionId && subscription && subscription.referenceId !== referenceId) subscription = void 0;
|
|
637
|
-
if (!subscription || !subscription.stripeCustomerId) throw
|
|
638
|
-
if (subscription
|
|
639
|
-
if (!subscription
|
|
640
|
-
const activeSubscription = await client.subscriptions.list({ customer: subscription.stripeCustomerId }).then((res) => res.data.filter((sub) => sub
|
|
641
|
-
if (!activeSubscription) throw
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
},
|
|
650
|
-
where: [{
|
|
651
|
-
field: "id",
|
|
652
|
-
value: subscription.id
|
|
653
|
-
}]
|
|
955
|
+
if (!subscription || !subscription.stripeCustomerId) throw ctx.error("BAD_REQUEST", { message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND });
|
|
956
|
+
if (!isActiveOrTrialing(subscription)) throw ctx.error("BAD_REQUEST", { message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_ACTIVE });
|
|
957
|
+
if (!isPendingCancel(subscription)) throw ctx.error("BAD_REQUEST", { message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION });
|
|
958
|
+
const activeSubscription = await client.subscriptions.list({ customer: subscription.stripeCustomerId }).then((res) => res.data.filter((sub) => isActiveOrTrialing(sub))[0]);
|
|
959
|
+
if (!activeSubscription) throw ctx.error("BAD_REQUEST", { message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND });
|
|
960
|
+
const updateParams = {};
|
|
961
|
+
if (activeSubscription.cancel_at) updateParams.cancel_at = "";
|
|
962
|
+
else if (activeSubscription.cancel_at_period_end) updateParams.cancel_at_period_end = false;
|
|
963
|
+
const newSub = await client.subscriptions.update(activeSubscription.id, updateParams).catch((e) => {
|
|
964
|
+
throw ctx.error("BAD_REQUEST", {
|
|
965
|
+
message: e.message,
|
|
966
|
+
code: e.code
|
|
654
967
|
});
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
968
|
+
});
|
|
969
|
+
await ctx.context.adapter.update({
|
|
970
|
+
model: "subscription",
|
|
971
|
+
update: {
|
|
972
|
+
cancelAtPeriodEnd: false,
|
|
973
|
+
cancelAt: null,
|
|
974
|
+
canceledAt: null,
|
|
975
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
976
|
+
},
|
|
977
|
+
where: [{
|
|
978
|
+
field: "id",
|
|
979
|
+
value: subscription.id
|
|
980
|
+
}]
|
|
981
|
+
});
|
|
982
|
+
return ctx.json(newSub);
|
|
660
983
|
});
|
|
661
984
|
};
|
|
662
|
-
const listActiveSubscriptionsQuerySchema = z.optional(z.object({
|
|
985
|
+
const listActiveSubscriptionsQuerySchema = z.optional(z.object({
|
|
986
|
+
referenceId: z.string().meta({ description: "Reference id of the subscription to list. Eg: '123'" }).optional(),
|
|
987
|
+
customerType: z.enum(["user", "organization"]).meta({ description: "Customer type for the subscription. Eg: \"user\" or \"organization\"" }).optional()
|
|
988
|
+
}));
|
|
663
989
|
/**
|
|
664
990
|
* ### Endpoint
|
|
665
991
|
*
|
|
@@ -681,13 +1007,15 @@ const listActiveSubscriptions = (options) => {
|
|
|
681
1007
|
method: "GET",
|
|
682
1008
|
query: listActiveSubscriptionsQuerySchema,
|
|
683
1009
|
metadata: { openapi: { operationId: "listActiveSubscriptions" } },
|
|
684
|
-
use: [
|
|
1010
|
+
use: [stripeSessionMiddleware, referenceMiddleware(subscriptionOptions, "list-subscription")]
|
|
685
1011
|
}, async (ctx) => {
|
|
1012
|
+
const customerType = ctx.query?.customerType || "user";
|
|
1013
|
+
const referenceId = ctx.query?.referenceId || getReferenceId(ctx.context.session, customerType, options);
|
|
686
1014
|
const subscriptions$1 = await ctx.context.adapter.findMany({
|
|
687
1015
|
model: "subscription",
|
|
688
1016
|
where: [{
|
|
689
1017
|
field: "referenceId",
|
|
690
|
-
value:
|
|
1018
|
+
value: referenceId
|
|
691
1019
|
}]
|
|
692
1020
|
});
|
|
693
1021
|
if (!subscriptions$1.length) return [];
|
|
@@ -700,9 +1028,7 @@ const listActiveSubscriptions = (options) => {
|
|
|
700
1028
|
limits: plan?.limits,
|
|
701
1029
|
priceId: plan?.priceId
|
|
702
1030
|
};
|
|
703
|
-
}).filter((sub) =>
|
|
704
|
-
return sub.status === "active" || sub.status === "trialing";
|
|
705
|
-
});
|
|
1031
|
+
}).filter((sub) => isActiveOrTrialing(sub));
|
|
706
1032
|
return ctx.json(subs);
|
|
707
1033
|
});
|
|
708
1034
|
};
|
|
@@ -716,10 +1042,9 @@ const subscriptionSuccess = (options) => {
|
|
|
716
1042
|
use: [originCheck((ctx) => ctx.query.callbackURL)]
|
|
717
1043
|
}, async (ctx) => {
|
|
718
1044
|
if (!ctx.query || !ctx.query.callbackURL || !ctx.query.subscriptionId) throw ctx.redirect(getUrl(ctx, ctx.query?.callbackURL || "/"));
|
|
1045
|
+
const { callbackURL, subscriptionId } = ctx.query;
|
|
719
1046
|
const session = await getSessionFromCtx(ctx);
|
|
720
1047
|
if (!session) throw ctx.redirect(getUrl(ctx, ctx.query?.callbackURL || "/"));
|
|
721
|
-
const { user: user$1 } = session;
|
|
722
|
-
const { callbackURL, subscriptionId } = ctx.query;
|
|
723
1048
|
const subscription = await ctx.context.adapter.findOne({
|
|
724
1049
|
model: "subscription",
|
|
725
1050
|
where: [{
|
|
@@ -727,38 +1052,53 @@ const subscriptionSuccess = (options) => {
|
|
|
727
1052
|
value: subscriptionId
|
|
728
1053
|
}]
|
|
729
1054
|
});
|
|
730
|
-
if (subscription
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
update: {
|
|
742
|
-
status: stripeSubscription.status,
|
|
743
|
-
seats: stripeSubscription.items.data[0]?.quantity || 1,
|
|
744
|
-
plan: plan.name.toLowerCase(),
|
|
745
|
-
periodEnd: /* @__PURE__ */ new Date(stripeSubscription.items.data[0]?.current_period_end * 1e3),
|
|
746
|
-
periodStart: /* @__PURE__ */ new Date(stripeSubscription.items.data[0]?.current_period_start * 1e3),
|
|
747
|
-
stripeSubscriptionId: stripeSubscription.id,
|
|
748
|
-
...stripeSubscription.trial_start && stripeSubscription.trial_end ? {
|
|
749
|
-
trialStart: /* @__PURE__ */ new Date(stripeSubscription.trial_start * 1e3),
|
|
750
|
-
trialEnd: /* @__PURE__ */ new Date(stripeSubscription.trial_end * 1e3)
|
|
751
|
-
} : {}
|
|
752
|
-
},
|
|
753
|
-
where: [{
|
|
754
|
-
field: "id",
|
|
755
|
-
value: subscription.id
|
|
756
|
-
}]
|
|
757
|
-
});
|
|
758
|
-
}
|
|
759
|
-
} catch (error) {
|
|
1055
|
+
if (!subscription) {
|
|
1056
|
+
ctx.context.logger.warn(`Subscription record not found for subscriptionId: ${subscriptionId}`);
|
|
1057
|
+
throw ctx.redirect(getUrl(ctx, callbackURL));
|
|
1058
|
+
}
|
|
1059
|
+
if (isActiveOrTrialing(subscription)) throw ctx.redirect(getUrl(ctx, callbackURL));
|
|
1060
|
+
const customerId = subscription.stripeCustomerId || session.user.stripeCustomerId;
|
|
1061
|
+
if (!customerId) throw ctx.redirect(getUrl(ctx, callbackURL));
|
|
1062
|
+
const stripeSubscription = await client.subscriptions.list({
|
|
1063
|
+
customer: customerId,
|
|
1064
|
+
status: "active"
|
|
1065
|
+
}).then((res) => res.data[0]).catch((error) => {
|
|
760
1066
|
ctx.context.logger.error("Error fetching subscription from Stripe", error);
|
|
1067
|
+
throw ctx.redirect(getUrl(ctx, callbackURL));
|
|
1068
|
+
});
|
|
1069
|
+
if (!stripeSubscription) throw ctx.redirect(getUrl(ctx, callbackURL));
|
|
1070
|
+
const subscriptionItem = stripeSubscription.items.data[0];
|
|
1071
|
+
if (!subscriptionItem) {
|
|
1072
|
+
ctx.context.logger.warn(`No subscription items found for Stripe subscription ${stripeSubscription.id}`);
|
|
1073
|
+
throw ctx.redirect(getUrl(ctx, callbackURL));
|
|
761
1074
|
}
|
|
1075
|
+
const plan = await getPlanByPriceInfo(options, subscriptionItem.price.id, subscriptionItem.price.lookup_key);
|
|
1076
|
+
if (!plan) {
|
|
1077
|
+
ctx.context.logger.warn(`Plan not found for price ${subscriptionItem.price.id}`);
|
|
1078
|
+
throw ctx.redirect(getUrl(ctx, callbackURL));
|
|
1079
|
+
}
|
|
1080
|
+
await ctx.context.adapter.update({
|
|
1081
|
+
model: "subscription",
|
|
1082
|
+
update: {
|
|
1083
|
+
status: stripeSubscription.status,
|
|
1084
|
+
seats: subscriptionItem.quantity || 1,
|
|
1085
|
+
plan: plan.name.toLowerCase(),
|
|
1086
|
+
periodEnd: /* @__PURE__ */ new Date(subscriptionItem.current_period_end * 1e3),
|
|
1087
|
+
periodStart: /* @__PURE__ */ new Date(subscriptionItem.current_period_start * 1e3),
|
|
1088
|
+
stripeSubscriptionId: stripeSubscription.id,
|
|
1089
|
+
cancelAtPeriodEnd: stripeSubscription.cancel_at_period_end,
|
|
1090
|
+
cancelAt: stripeSubscription.cancel_at ? /* @__PURE__ */ new Date(stripeSubscription.cancel_at * 1e3) : null,
|
|
1091
|
+
canceledAt: stripeSubscription.canceled_at ? /* @__PURE__ */ new Date(stripeSubscription.canceled_at * 1e3) : null,
|
|
1092
|
+
...stripeSubscription.trial_start && stripeSubscription.trial_end ? {
|
|
1093
|
+
trialStart: /* @__PURE__ */ new Date(stripeSubscription.trial_start * 1e3),
|
|
1094
|
+
trialEnd: /* @__PURE__ */ new Date(stripeSubscription.trial_end * 1e3)
|
|
1095
|
+
} : {}
|
|
1096
|
+
},
|
|
1097
|
+
where: [{
|
|
1098
|
+
field: "id",
|
|
1099
|
+
value: subscription.id
|
|
1100
|
+
}]
|
|
1101
|
+
});
|
|
762
1102
|
throw ctx.redirect(getUrl(ctx, callbackURL));
|
|
763
1103
|
});
|
|
764
1104
|
};
|
|
@@ -767,7 +1107,9 @@ const createBillingPortalBodySchema = z.object({
|
|
|
767
1107
|
return typeof localization === "string";
|
|
768
1108
|
}).optional(),
|
|
769
1109
|
referenceId: z.string().optional(),
|
|
770
|
-
|
|
1110
|
+
customerType: z.enum(["user", "organization"]).meta({ description: "Customer type for the subscription. Eg: \"user\" or \"organization\"" }).optional(),
|
|
1111
|
+
returnUrl: z.string().default("/"),
|
|
1112
|
+
disableRedirect: z.boolean().meta({ description: "Disable redirect after creating billing portal session. Eg: true" }).default(false)
|
|
771
1113
|
});
|
|
772
1114
|
const createBillingPortal = (options) => {
|
|
773
1115
|
const client = options.stripeClient;
|
|
@@ -777,22 +1119,41 @@ const createBillingPortal = (options) => {
|
|
|
777
1119
|
body: createBillingPortalBodySchema,
|
|
778
1120
|
metadata: { openapi: { operationId: "createBillingPortal" } },
|
|
779
1121
|
use: [
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
1122
|
+
stripeSessionMiddleware,
|
|
1123
|
+
referenceMiddleware(subscriptionOptions, "billing-portal"),
|
|
1124
|
+
originCheck((ctx) => ctx.body.returnUrl)
|
|
783
1125
|
]
|
|
784
1126
|
}, async (ctx) => {
|
|
785
1127
|
const { user: user$1 } = ctx.context.session;
|
|
786
|
-
const
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
1128
|
+
const customerType = ctx.body.customerType || "user";
|
|
1129
|
+
const referenceId = ctx.body.referenceId || getReferenceId(ctx.context.session, customerType, options);
|
|
1130
|
+
let customerId;
|
|
1131
|
+
if (customerType === "organization") {
|
|
1132
|
+
customerId = (await ctx.context.adapter.findOne({
|
|
1133
|
+
model: "organization",
|
|
1134
|
+
where: [{
|
|
1135
|
+
field: "id",
|
|
1136
|
+
value: referenceId
|
|
1137
|
+
}]
|
|
1138
|
+
}))?.stripeCustomerId;
|
|
1139
|
+
if (!customerId) customerId = (await ctx.context.adapter.findMany({
|
|
1140
|
+
model: "subscription",
|
|
1141
|
+
where: [{
|
|
1142
|
+
field: "referenceId",
|
|
1143
|
+
value: referenceId
|
|
1144
|
+
}]
|
|
1145
|
+
}).then((subs) => subs.find((sub) => isActiveOrTrialing(sub))))?.stripeCustomerId;
|
|
1146
|
+
} else {
|
|
1147
|
+
customerId = user$1.stripeCustomerId;
|
|
1148
|
+
if (!customerId) customerId = (await ctx.context.adapter.findMany({
|
|
1149
|
+
model: "subscription",
|
|
1150
|
+
where: [{
|
|
1151
|
+
field: "referenceId",
|
|
1152
|
+
value: referenceId
|
|
1153
|
+
}]
|
|
1154
|
+
}).then((subs) => subs.find((sub) => isActiveOrTrialing(sub))))?.stripeCustomerId;
|
|
1155
|
+
}
|
|
1156
|
+
if (!customerId) throw new APIError$1("NOT_FOUND", { message: STRIPE_ERROR_CODES.CUSTOMER_NOT_FOUND });
|
|
796
1157
|
try {
|
|
797
1158
|
const { url } = await client.billingPortal.sessions.create({
|
|
798
1159
|
locale: ctx.body.locale,
|
|
@@ -801,11 +1162,11 @@ const createBillingPortal = (options) => {
|
|
|
801
1162
|
});
|
|
802
1163
|
return ctx.json({
|
|
803
1164
|
url,
|
|
804
|
-
redirect:
|
|
1165
|
+
redirect: !ctx.body.disableRedirect
|
|
805
1166
|
});
|
|
806
1167
|
} catch (error) {
|
|
807
1168
|
ctx.context.logger.error("Error creating billing portal session", error);
|
|
808
|
-
throw new APIError("
|
|
1169
|
+
throw new APIError$1("INTERNAL_SERVER_ERROR", { message: STRIPE_ERROR_CODES.UNABLE_TO_CREATE_BILLING_PORTAL });
|
|
809
1170
|
}
|
|
810
1171
|
});
|
|
811
1172
|
};
|
|
@@ -820,26 +1181,31 @@ const stripeWebhook = (options) => {
|
|
|
820
1181
|
cloneRequest: true,
|
|
821
1182
|
disableBody: true
|
|
822
1183
|
}, async (ctx) => {
|
|
823
|
-
if (!ctx.request?.body) throw new APIError("
|
|
824
|
-
const buf = await ctx.request.text();
|
|
1184
|
+
if (!ctx.request?.body) throw new APIError$1("BAD_REQUEST", { message: STRIPE_ERROR_CODES.INVALID_REQUEST_BODY });
|
|
825
1185
|
const sig = ctx.request.headers.get("stripe-signature");
|
|
1186
|
+
if (!sig) throw new APIError$1("BAD_REQUEST", { message: STRIPE_ERROR_CODES.STRIPE_SIGNATURE_NOT_FOUND });
|
|
826
1187
|
const webhookSecret = options.stripeWebhookSecret;
|
|
1188
|
+
if (!webhookSecret) throw new APIError$1("INTERNAL_SERVER_ERROR", { message: STRIPE_ERROR_CODES.STRIPE_WEBHOOK_SECRET_NOT_FOUND });
|
|
1189
|
+
const payload = await ctx.request.text();
|
|
827
1190
|
let event;
|
|
828
1191
|
try {
|
|
829
|
-
if (
|
|
830
|
-
|
|
831
|
-
else event = client.webhooks.constructEvent(buf, sig, webhookSecret);
|
|
1192
|
+
if (typeof client.webhooks.constructEventAsync === "function") event = await client.webhooks.constructEventAsync(payload, sig, webhookSecret);
|
|
1193
|
+
else event = client.webhooks.constructEvent(payload, sig, webhookSecret);
|
|
832
1194
|
} catch (err) {
|
|
833
1195
|
ctx.context.logger.error(`${err.message}`);
|
|
834
|
-
throw new APIError("BAD_REQUEST", { message:
|
|
1196
|
+
throw new APIError$1("BAD_REQUEST", { message: STRIPE_ERROR_CODES.FAILED_TO_CONSTRUCT_STRIPE_EVENT });
|
|
835
1197
|
}
|
|
836
|
-
if (!event) throw new APIError("BAD_REQUEST", { message:
|
|
1198
|
+
if (!event) throw new APIError$1("BAD_REQUEST", { message: STRIPE_ERROR_CODES.FAILED_TO_CONSTRUCT_STRIPE_EVENT });
|
|
837
1199
|
try {
|
|
838
1200
|
switch (event.type) {
|
|
839
1201
|
case "checkout.session.completed":
|
|
840
1202
|
await onCheckoutSessionCompleted(ctx, options, event);
|
|
841
1203
|
await options.onEvent?.(event);
|
|
842
1204
|
break;
|
|
1205
|
+
case "customer.subscription.created":
|
|
1206
|
+
await onSubscriptionCreated(ctx, options, event);
|
|
1207
|
+
await options.onEvent?.(event);
|
|
1208
|
+
break;
|
|
843
1209
|
case "customer.subscription.updated":
|
|
844
1210
|
await onSubscriptionUpdated(ctx, options, event);
|
|
845
1211
|
await options.onEvent?.(event);
|
|
@@ -854,23 +1220,11 @@ const stripeWebhook = (options) => {
|
|
|
854
1220
|
}
|
|
855
1221
|
} catch (e) {
|
|
856
1222
|
ctx.context.logger.error(`Stripe webhook failed. Error: ${e.message}`);
|
|
857
|
-
throw new APIError("BAD_REQUEST", { message:
|
|
1223
|
+
throw new APIError$1("BAD_REQUEST", { message: STRIPE_ERROR_CODES.STRIPE_WEBHOOK_ERROR });
|
|
858
1224
|
}
|
|
859
1225
|
return ctx.json({ success: true });
|
|
860
1226
|
});
|
|
861
1227
|
};
|
|
862
|
-
const getUrl = (ctx, url) => {
|
|
863
|
-
if (/^[a-zA-Z][a-zA-Z0-9+\-.]*:/.test(url)) return url;
|
|
864
|
-
return `${ctx.context.options.baseURL}${url.startsWith("/") ? url : `/${url}`}`;
|
|
865
|
-
};
|
|
866
|
-
async function resolvePriceIdFromLookupKey(stripeClient, lookupKey) {
|
|
867
|
-
if (!lookupKey) return void 0;
|
|
868
|
-
return (await stripeClient.prices.list({
|
|
869
|
-
lookup_keys: [lookupKey],
|
|
870
|
-
active: true,
|
|
871
|
-
limit: 1
|
|
872
|
-
})).data[0]?.id;
|
|
873
|
-
}
|
|
874
1228
|
|
|
875
1229
|
//#endregion
|
|
876
1230
|
//#region src/schema.ts
|
|
@@ -916,6 +1270,18 @@ const subscriptions = { subscription: { fields: {
|
|
|
916
1270
|
required: false,
|
|
917
1271
|
defaultValue: false
|
|
918
1272
|
},
|
|
1273
|
+
cancelAt: {
|
|
1274
|
+
type: "date",
|
|
1275
|
+
required: false
|
|
1276
|
+
},
|
|
1277
|
+
canceledAt: {
|
|
1278
|
+
type: "date",
|
|
1279
|
+
required: false
|
|
1280
|
+
},
|
|
1281
|
+
endedAt: {
|
|
1282
|
+
type: "date",
|
|
1283
|
+
required: false
|
|
1284
|
+
},
|
|
919
1285
|
seats: {
|
|
920
1286
|
type: "number",
|
|
921
1287
|
required: false
|
|
@@ -925,6 +1291,10 @@ const user = { user: { fields: { stripeCustomerId: {
|
|
|
925
1291
|
type: "string",
|
|
926
1292
|
required: false
|
|
927
1293
|
} } } };
|
|
1294
|
+
const organization = { organization: { fields: { stripeCustomerId: {
|
|
1295
|
+
type: "string",
|
|
1296
|
+
required: false
|
|
1297
|
+
} } } };
|
|
928
1298
|
const getSchema = (options) => {
|
|
929
1299
|
let baseSchema = {};
|
|
930
1300
|
if (options.subscription?.enabled) baseSchema = {
|
|
@@ -932,6 +1302,10 @@ const getSchema = (options) => {
|
|
|
932
1302
|
...user
|
|
933
1303
|
};
|
|
934
1304
|
else baseSchema = { ...user };
|
|
1305
|
+
if (options.organization?.enabled) baseSchema = {
|
|
1306
|
+
...baseSchema,
|
|
1307
|
+
...organization
|
|
1308
|
+
};
|
|
935
1309
|
if (options.schema && !options.subscription?.enabled && "subscription" in options.schema) {
|
|
936
1310
|
const { subscription: _subscription, ...restSchema } = options.schema;
|
|
937
1311
|
return mergeSchema(baseSchema, restSchema);
|
|
@@ -941,16 +1315,6 @@ const getSchema = (options) => {
|
|
|
941
1315
|
|
|
942
1316
|
//#endregion
|
|
943
1317
|
//#region src/index.ts
|
|
944
|
-
const STRIPE_ERROR_CODES = defineErrorCodes({
|
|
945
|
-
SUBSCRIPTION_NOT_FOUND: "Subscription not found",
|
|
946
|
-
SUBSCRIPTION_PLAN_NOT_FOUND: "Subscription plan not found",
|
|
947
|
-
ALREADY_SUBSCRIBED_PLAN: "You're already subscribed to this plan",
|
|
948
|
-
UNABLE_TO_CREATE_CUSTOMER: "Unable to create customer",
|
|
949
|
-
FAILED_TO_FETCH_PLANS: "Failed to fetch plans",
|
|
950
|
-
EMAIL_VERIFICATION_REQUIRED: "Email verification is required before you can subscribe to a plan",
|
|
951
|
-
SUBSCRIPTION_NOT_ACTIVE: "Subscription is not active",
|
|
952
|
-
SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION: "Subscription is not scheduled for cancellation"
|
|
953
|
-
});
|
|
954
1318
|
const stripe = (options) => {
|
|
955
1319
|
const client = options.stripeClient;
|
|
956
1320
|
const subscriptionEndpoints = {
|
|
@@ -969,13 +1333,70 @@ const stripe = (options) => {
|
|
|
969
1333
|
...options.subscription?.enabled ? subscriptionEndpoints : {}
|
|
970
1334
|
},
|
|
971
1335
|
init(ctx) {
|
|
1336
|
+
if (options.organization?.enabled) {
|
|
1337
|
+
const orgPlugin = ctx.getPlugin("organization");
|
|
1338
|
+
if (!orgPlugin) {
|
|
1339
|
+
ctx.logger.error(`Organization plugin not found`);
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
const existingHooks = orgPlugin.options.organizationHooks ?? {};
|
|
1343
|
+
/**
|
|
1344
|
+
* Sync organization name to Stripe customer
|
|
1345
|
+
*/
|
|
1346
|
+
const afterUpdateStripeOrg = async (data) => {
|
|
1347
|
+
const { organization: organization$1 } = data;
|
|
1348
|
+
if (!organization$1?.stripeCustomerId) return;
|
|
1349
|
+
try {
|
|
1350
|
+
const stripeCustomer = await client.customers.retrieve(organization$1.stripeCustomerId);
|
|
1351
|
+
if (stripeCustomer.deleted) {
|
|
1352
|
+
ctx.logger.warn(`Stripe customer ${organization$1.stripeCustomerId} was deleted`);
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
if (organization$1.name !== stripeCustomer.name) {
|
|
1356
|
+
await client.customers.update(organization$1.stripeCustomerId, { name: organization$1.name });
|
|
1357
|
+
ctx.logger.info(`Synced organization name to Stripe: "${stripeCustomer.name}" → "${organization$1.name}"`);
|
|
1358
|
+
}
|
|
1359
|
+
} catch (e) {
|
|
1360
|
+
ctx.logger.error(`Failed to sync organization to Stripe: ${e.message}`);
|
|
1361
|
+
}
|
|
1362
|
+
};
|
|
1363
|
+
/**
|
|
1364
|
+
* Block deletion if organization has active subscriptions
|
|
1365
|
+
*/
|
|
1366
|
+
const beforeDeleteStripeOrg = async (data) => {
|
|
1367
|
+
const { organization: organization$1 } = data;
|
|
1368
|
+
if (!organization$1.stripeCustomerId) return;
|
|
1369
|
+
try {
|
|
1370
|
+
const subscriptions$1 = await client.subscriptions.list({
|
|
1371
|
+
customer: organization$1.stripeCustomerId,
|
|
1372
|
+
status: "all",
|
|
1373
|
+
limit: 100
|
|
1374
|
+
});
|
|
1375
|
+
for (const sub of subscriptions$1.data) if (sub.status !== "canceled" && sub.status !== "incomplete" && sub.status !== "incomplete_expired") throw APIError.from("BAD_REQUEST", STRIPE_ERROR_CODES.ORGANIZATION_HAS_ACTIVE_SUBSCRIPTION);
|
|
1376
|
+
} catch (error) {
|
|
1377
|
+
if (error instanceof APIError) throw error;
|
|
1378
|
+
ctx.logger.error(`Failed to check organization subscriptions: ${error.message}`);
|
|
1379
|
+
throw error;
|
|
1380
|
+
}
|
|
1381
|
+
};
|
|
1382
|
+
orgPlugin.options.organizationHooks = {
|
|
1383
|
+
...existingHooks,
|
|
1384
|
+
afterUpdateOrganization: existingHooks.afterUpdateOrganization ? async (data) => {
|
|
1385
|
+
await existingHooks.afterUpdateOrganization(data);
|
|
1386
|
+
await afterUpdateStripeOrg(data);
|
|
1387
|
+
} : afterUpdateStripeOrg,
|
|
1388
|
+
beforeDeleteOrganization: existingHooks.beforeDeleteOrganization ? async (data) => {
|
|
1389
|
+
await existingHooks.beforeDeleteOrganization(data);
|
|
1390
|
+
await beforeDeleteStripeOrg(data);
|
|
1391
|
+
} : beforeDeleteStripeOrg
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
972
1394
|
return { options: { databaseHooks: { user: {
|
|
973
1395
|
create: { async after(user$1, ctx$1) {
|
|
974
|
-
if (!ctx$1 || !options.createCustomerOnSignUp) return;
|
|
1396
|
+
if (!ctx$1 || !options.createCustomerOnSignUp || user$1.stripeCustomerId) return;
|
|
975
1397
|
try {
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
email: user$1.email,
|
|
1398
|
+
let stripeCustomer = (await client.customers.search({
|
|
1399
|
+
query: `email:"${escapeStripeSearchValue(user$1.email)}" AND -metadata["customerType"]:"organization"`,
|
|
979
1400
|
limit: 1
|
|
980
1401
|
})).data[0];
|
|
981
1402
|
if (stripeCustomer) {
|
|
@@ -995,7 +1416,10 @@ const stripe = (options) => {
|
|
|
995
1416
|
const params = defu({
|
|
996
1417
|
email: user$1.email,
|
|
997
1418
|
name: user$1.name,
|
|
998
|
-
metadata: {
|
|
1419
|
+
metadata: {
|
|
1420
|
+
userId: user$1.id,
|
|
1421
|
+
customerType: "user"
|
|
1422
|
+
}
|
|
999
1423
|
}, extraCreateParams);
|
|
1000
1424
|
stripeCustomer = await client.customers.create(params);
|
|
1001
1425
|
await ctx$1.context.internalAdapter.updateUser(user$1.id, { stripeCustomerId: stripeCustomer.id });
|
|
@@ -1012,17 +1436,15 @@ const stripe = (options) => {
|
|
|
1012
1436
|
}
|
|
1013
1437
|
} },
|
|
1014
1438
|
update: { async after(user$1, ctx$1) {
|
|
1015
|
-
if (!ctx$1) return;
|
|
1439
|
+
if (!ctx$1 || !user$1.stripeCustomerId) return;
|
|
1016
1440
|
try {
|
|
1017
|
-
const
|
|
1018
|
-
if (!userWithStripe.stripeCustomerId) return;
|
|
1019
|
-
const stripeCustomer = await client.customers.retrieve(userWithStripe.stripeCustomerId);
|
|
1441
|
+
const stripeCustomer = await client.customers.retrieve(user$1.stripeCustomerId);
|
|
1020
1442
|
if (stripeCustomer.deleted) {
|
|
1021
|
-
ctx$1.context.logger.warn(`Stripe customer ${
|
|
1443
|
+
ctx$1.context.logger.warn(`Stripe customer ${user$1.stripeCustomerId} was deleted, cannot update email`);
|
|
1022
1444
|
return;
|
|
1023
1445
|
}
|
|
1024
1446
|
if (stripeCustomer.email !== user$1.email) {
|
|
1025
|
-
await client.customers.update(
|
|
1447
|
+
await client.customers.update(user$1.stripeCustomerId, { email: user$1.email });
|
|
1026
1448
|
ctx$1.context.logger.info(`Updated Stripe customer email from ${stripeCustomer.email} to ${user$1.email}`);
|
|
1027
1449
|
}
|
|
1028
1450
|
} catch (e) {
|