@better-auth/stripe 1.5.0-beta.1 → 1.5.0-beta.10

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