@better-auth/stripe 1.5.0-beta.2 → 1.5.0-beta.4

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,10 +1,9 @@
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-Bkj5yJMT.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 } 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
 
@@ -37,21 +36,68 @@ function isPendingCancel(sub) {
37
36
  function isStripePendingCancel(stripeSub) {
38
37
  return !!(stripeSub.cancel_at_period_end || stripeSub.cancel_at);
39
38
  }
39
+ /**
40
+ * Escapes a value for use in Stripe search queries.
41
+ * Stripe search query uses double quotes for string values,
42
+ * and double quotes within the value need to be escaped with backslash.
43
+ *
44
+ * @see https://docs.stripe.com/search#search-query-language
45
+ */
46
+ function escapeStripeSearchValue(value) {
47
+ return value.replace(/"/g, "\\\"");
48
+ }
40
49
 
41
50
  //#endregion
42
51
  //#region src/hooks.ts
52
+ /**
53
+ * Find organization or user by stripeCustomerId.
54
+ * @internal
55
+ */
56
+ async function findReferenceByStripeCustomerId(ctx, options, stripeCustomerId) {
57
+ if (options.organization?.enabled) {
58
+ const org = await ctx.context.adapter.findOne({
59
+ model: "organization",
60
+ where: [{
61
+ field: "stripeCustomerId",
62
+ value: stripeCustomerId
63
+ }]
64
+ });
65
+ if (org) return {
66
+ customerType: "organization",
67
+ referenceId: org.id
68
+ };
69
+ }
70
+ const user$1 = await ctx.context.adapter.findOne({
71
+ model: "user",
72
+ where: [{
73
+ field: "stripeCustomerId",
74
+ value: stripeCustomerId
75
+ }]
76
+ });
77
+ if (user$1) return {
78
+ customerType: "user",
79
+ referenceId: user$1.id
80
+ };
81
+ return null;
82
+ }
43
83
  async function onCheckoutSessionCompleted(ctx, options, event) {
44
84
  try {
45
85
  const client = options.stripeClient;
46
86
  const checkoutSession = event.data.object;
47
87
  if (checkoutSession.mode === "setup" || !options.subscription?.enabled) return;
48
88
  const subscription = await client.subscriptions.retrieve(checkoutSession.subscription);
49
- const priceId = subscription.items.data[0]?.price.id;
50
- const plan = await getPlanByPriceInfo(options, priceId, subscription.items.data[0]?.price.lookup_key || null);
89
+ const subscriptionItem = subscription.items.data[0];
90
+ if (!subscriptionItem) {
91
+ ctx.context.logger.warn(`Stripe webhook warning: Subscription ${subscription.id} has no items`);
92
+ return;
93
+ }
94
+ const priceId = subscriptionItem.price.id;
95
+ const priceLookupKey = subscriptionItem.price.lookup_key;
96
+ const plan = await getPlanByPriceInfo(options, priceId, priceLookupKey);
51
97
  if (plan) {
52
98
  const referenceId = checkoutSession?.client_reference_id || checkoutSession?.metadata?.referenceId;
53
99
  const subscriptionId = checkoutSession?.metadata?.subscriptionId;
54
- const seats = subscription.items.data[0].quantity;
100
+ const seats = subscriptionItem.quantity;
55
101
  if (referenceId && subscriptionId) {
56
102
  const trial = subscription.trial_start && subscription.trial_end ? {
57
103
  trialStart: /* @__PURE__ */ new Date(subscription.trial_start * 1e3),
@@ -63,8 +109,8 @@ async function onCheckoutSessionCompleted(ctx, options, event) {
63
109
  plan: plan.name.toLowerCase(),
64
110
  status: subscription.status,
65
111
  updatedAt: /* @__PURE__ */ new Date(),
66
- periodStart: /* @__PURE__ */ new Date(subscription.items.data[0].current_period_start * 1e3),
67
- periodEnd: /* @__PURE__ */ new Date(subscription.items.data[0].current_period_end * 1e3),
112
+ periodStart: /* @__PURE__ */ new Date(subscriptionItem.current_period_start * 1e3),
113
+ periodEnd: /* @__PURE__ */ new Date(subscriptionItem.current_period_end * 1e3),
68
114
  stripeSubscriptionId: checkoutSession.subscription,
69
115
  cancelAtPeriodEnd: subscription.cancel_at_period_end,
70
116
  cancelAt: subscription.cancel_at ? /* @__PURE__ */ new Date(subscription.cancel_at * 1e3) : null,
@@ -108,27 +154,27 @@ async function onSubscriptionCreated(ctx, options, event) {
108
154
  ctx.context.logger.warn(`Stripe webhook warning: customer.subscription.created event received without customer ID`);
109
155
  return;
110
156
  }
111
- if (await ctx.context.adapter.findOne({
157
+ const subscriptionId = subscriptionCreated.metadata?.subscriptionId;
158
+ const existingSubscription = await ctx.context.adapter.findOne({
112
159
  model: "subscription",
113
- where: [{
160
+ where: subscriptionId ? [{
161
+ field: "id",
162
+ value: subscriptionId
163
+ }] : [{
114
164
  field: "stripeSubscriptionId",
115
165
  value: subscriptionCreated.id
116
166
  }]
117
- })) {
118
- ctx.context.logger.info(`Stripe webhook: Subscription ${subscriptionCreated.id} already exists in database, skipping creation`);
167
+ });
168
+ if (existingSubscription) {
169
+ ctx.context.logger.info(`Stripe webhook: Subscription already exists in database (id: ${existingSubscription.id}), skipping creation`);
119
170
  return;
120
171
  }
121
- const user$1 = await ctx.context.adapter.findOne({
122
- model: "user",
123
- where: [{
124
- field: "stripeCustomerId",
125
- value: stripeCustomerId
126
- }]
127
- });
128
- if (!user$1) {
129
- ctx.context.logger.warn(`Stripe webhook warning: No user found with stripeCustomerId: ${stripeCustomerId}`);
172
+ const reference = await findReferenceByStripeCustomerId(ctx, options, stripeCustomerId);
173
+ if (!reference) {
174
+ ctx.context.logger.warn(`Stripe webhook warning: No user or organization found with stripeCustomerId: ${stripeCustomerId}`);
130
175
  return;
131
176
  }
177
+ const { referenceId, customerType } = reference;
132
178
  const subscriptionItem = subscriptionCreated.items.data[0];
133
179
  if (!subscriptionItem) {
134
180
  ctx.context.logger.warn(`Stripe webhook warning: Subscription ${subscriptionCreated.id} has no items`);
@@ -150,7 +196,7 @@ async function onSubscriptionCreated(ctx, options, event) {
150
196
  const newSubscription = await ctx.context.adapter.create({
151
197
  model: "subscription",
152
198
  data: {
153
- referenceId: user$1.id,
199
+ referenceId,
154
200
  stripeCustomerId,
155
201
  stripeSubscriptionId: subscriptionCreated.id,
156
202
  status: subscriptionCreated.status,
@@ -162,8 +208,8 @@ async function onSubscriptionCreated(ctx, options, event) {
162
208
  ...trial
163
209
  }
164
210
  });
165
- ctx.context.logger.info(`Stripe webhook: Created subscription ${subscriptionCreated.id} for user ${user$1.id} from dashboard`);
166
- await options.subscription?.onSubscriptionCreated?.({
211
+ ctx.context.logger.info(`Stripe webhook: Created subscription ${subscriptionCreated.id} for ${customerType} ${referenceId} from dashboard`);
212
+ await options.subscription.onSubscriptionCreated?.({
167
213
  event,
168
214
  subscription: newSubscription,
169
215
  stripeSubscription: subscriptionCreated,
@@ -177,8 +223,14 @@ async function onSubscriptionUpdated(ctx, options, event) {
177
223
  try {
178
224
  if (!options.subscription?.enabled) return;
179
225
  const subscriptionUpdated = event.data.object;
180
- const priceId = subscriptionUpdated.items.data[0].price.id;
181
- const plan = await getPlanByPriceInfo(options, priceId, subscriptionUpdated.items.data[0].price.lookup_key || null);
226
+ const subscriptionItem = subscriptionUpdated.items.data[0];
227
+ if (!subscriptionItem) {
228
+ ctx.context.logger.warn(`Stripe webhook warning: Subscription ${subscriptionUpdated.id} has no items`);
229
+ return;
230
+ }
231
+ const priceId = subscriptionItem.price.id;
232
+ const priceLookupKey = subscriptionItem.price.lookup_key;
233
+ const plan = await getPlanByPriceInfo(options, priceId, priceLookupKey);
182
234
  const subscriptionId = subscriptionUpdated.metadata?.subscriptionId;
183
235
  const customerId = subscriptionUpdated.customer?.toString();
184
236
  let subscription = await ctx.context.adapter.findOne({
@@ -208,7 +260,6 @@ async function onSubscriptionUpdated(ctx, options, event) {
208
260
  subscription = activeSub;
209
261
  } else subscription = subs[0];
210
262
  }
211
- const seats = subscriptionUpdated.items.data[0].quantity;
212
263
  const updatedSubscription = await ctx.context.adapter.update({
213
264
  model: "subscription",
214
265
  update: {
@@ -218,13 +269,13 @@ async function onSubscriptionUpdated(ctx, options, event) {
218
269
  } : {},
219
270
  updatedAt: /* @__PURE__ */ new Date(),
220
271
  status: subscriptionUpdated.status,
221
- periodStart: /* @__PURE__ */ new Date(subscriptionUpdated.items.data[0].current_period_start * 1e3),
222
- periodEnd: /* @__PURE__ */ new Date(subscriptionUpdated.items.data[0].current_period_end * 1e3),
272
+ periodStart: /* @__PURE__ */ new Date(subscriptionItem.current_period_start * 1e3),
273
+ periodEnd: /* @__PURE__ */ new Date(subscriptionItem.current_period_end * 1e3),
223
274
  cancelAtPeriodEnd: subscriptionUpdated.cancel_at_period_end,
224
275
  cancelAt: subscriptionUpdated.cancel_at ? /* @__PURE__ */ new Date(subscriptionUpdated.cancel_at * 1e3) : null,
225
276
  canceledAt: subscriptionUpdated.canceled_at ? /* @__PURE__ */ new Date(subscriptionUpdated.canceled_at * 1e3) : null,
226
277
  endedAt: subscriptionUpdated.ended_at ? /* @__PURE__ */ new Date(subscriptionUpdated.ended_at * 1e3) : null,
227
- seats,
278
+ seats: subscriptionItem.quantity,
228
279
  stripeSubscriptionId: subscriptionUpdated.id
229
280
  },
230
281
  where: [{
@@ -291,33 +342,86 @@ async function onSubscriptionDeleted(ctx, options, event) {
291
342
 
292
343
  //#endregion
293
344
  //#region src/middleware.ts
345
+ const stripeSessionMiddleware = createAuthMiddleware({ use: [sessionMiddleware] }, async (ctx) => {
346
+ return { session: ctx.context.session };
347
+ });
294
348
  const referenceMiddleware = (subscriptionOptions, action) => createAuthMiddleware(async (ctx) => {
295
- const session = ctx.context.session;
296
- if (!session) throw new APIError$1("UNAUTHORIZED");
297
- const referenceId = ctx.body?.referenceId || ctx.query?.referenceId || session.user.id;
298
- if (referenceId !== session.user.id && !subscriptionOptions.authorizeReference) {
349
+ const ctxSession = ctx.context.session;
350
+ if (!ctxSession) throw APIError$1.from("UNAUTHORIZED", STRIPE_ERROR_CODES.UNAUTHORIZED);
351
+ const customerType = ctx.body?.customerType || ctx.query?.customerType;
352
+ const explicitReferenceId = ctx.body?.referenceId || ctx.query?.referenceId;
353
+ if (customerType === "organization") {
354
+ if (!subscriptionOptions.authorizeReference) {
355
+ ctx.context.logger.error(`Organization subscriptions require authorizeReference to be defined in your stripe plugin config.`);
356
+ throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.ORGANIZATION_SUBSCRIPTION_NOT_ENABLED);
357
+ }
358
+ const referenceId = explicitReferenceId || ctxSession.session.activeOrganizationId;
359
+ if (!referenceId) throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.ORGANIZATION_REFERENCE_ID_REQUIRED);
360
+ if (!await subscriptionOptions.authorizeReference({
361
+ user: ctxSession.user,
362
+ session: ctxSession.session,
363
+ referenceId,
364
+ action
365
+ }, ctx)) throw APIError$1.from("UNAUTHORIZED", STRIPE_ERROR_CODES.UNAUTHORIZED);
366
+ return;
367
+ }
368
+ if (!explicitReferenceId) return;
369
+ if (explicitReferenceId === ctxSession.user.id) return;
370
+ if (!subscriptionOptions.authorizeReference) {
299
371
  ctx.context.logger.error(`Passing referenceId into a subscription action isn't allowed if subscription.authorizeReference isn't defined in your stripe plugin config.`);
300
- throw new APIError$1("BAD_REQUEST", { message: "Reference id is not allowed. Read server logs for more details." });
372
+ throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.REFERENCE_ID_NOT_ALLOWED);
301
373
  }
302
- /**
303
- * if referenceId is the same as the active session user's id
304
- */
305
- const sameReference = ctx.query?.referenceId === session.user.id || ctx.body?.referenceId === session.user.id;
306
- if (!(ctx.body?.referenceId || ctx.query?.referenceId ? await subscriptionOptions.authorizeReference?.({
307
- user: session.user,
308
- session: session.session,
309
- referenceId,
374
+ if (!await subscriptionOptions.authorizeReference({
375
+ user: ctxSession.user,
376
+ session: ctxSession.session,
377
+ referenceId: explicitReferenceId,
310
378
  action
311
- }, ctx) || sameReference : true)) throw new APIError$1("UNAUTHORIZED", { message: "Unauthorized" });
379
+ }, ctx)) throw APIError$1.from("UNAUTHORIZED", STRIPE_ERROR_CODES.UNAUTHORIZED);
312
380
  });
313
381
 
314
382
  //#endregion
315
383
  //#region src/routes.ts
384
+ /**
385
+ * Converts a relative URL to an absolute URL using baseURL.
386
+ * @internal
387
+ */
388
+ function getUrl(ctx, url) {
389
+ if (/^[a-zA-Z][a-zA-Z0-9+\-.]*:/.test(url)) return url;
390
+ return `${ctx.context.options.baseURL}${url.startsWith("/") ? url : `/${url}`}`;
391
+ }
392
+ /**
393
+ * Resolves a Stripe price ID from a lookup key.
394
+ * @internal
395
+ */
396
+ async function resolvePriceIdFromLookupKey(stripeClient, lookupKey) {
397
+ if (!lookupKey) return void 0;
398
+ return (await stripeClient.prices.list({
399
+ lookup_keys: [lookupKey],
400
+ active: true,
401
+ limit: 1
402
+ })).data[0]?.id;
403
+ }
404
+ /**
405
+ * Determines the reference ID based on customer type.
406
+ * - `user` (default): uses userId
407
+ * - `organization`: uses activeOrganizationId from session
408
+ * @internal
409
+ */
410
+ function getReferenceId(ctxSession, customerType, options) {
411
+ const { user: user$1, session } = ctxSession;
412
+ if ((customerType || "user") === "organization") {
413
+ if (!options.organization?.enabled) throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.ORGANIZATION_SUBSCRIPTION_NOT_ENABLED);
414
+ if (!session.activeOrganizationId) throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.ORGANIZATION_NOT_FOUND);
415
+ return session.activeOrganizationId;
416
+ }
417
+ return user$1.id;
418
+ }
316
419
  const upgradeSubscriptionBodySchema = z.object({
317
420
  plan: z.string().meta({ description: "The name of the plan to upgrade to. Eg: \"pro\"" }),
318
421
  annual: z.boolean().meta({ description: "Whether to upgrade to an annual plan. Eg: true" }).optional(),
319
- referenceId: z.string().meta({ description: "Reference id of the subscription to upgrade. Eg: \"123\"" }).optional(),
422
+ referenceId: z.string().meta({ description: "Reference ID for the subscription. Eg: \"org_123\"" }).optional(),
320
423
  subscriptionId: z.string().meta({ description: "The Stripe subscription ID to upgrade. Eg: \"sub_1ABC2DEF3GHI4JKL\"" }).optional(),
424
+ customerType: z.enum(["user", "organization"]).meta({ description: "Customer type for the subscription. Eg: \"user\" or \"organization\"" }).optional(),
321
425
  metadata: z.record(z.string(), z.any()).optional(),
322
426
  seats: z.number().meta({ description: "Number of seats to upgrade to (if applicable). Eg: 1" }).optional(),
323
427
  successUrl: z.string().meta({ description: "Callback URL to redirect back after successful subscription. Eg: \"https://example.com/success\"" }).default("/"),
@@ -348,18 +452,19 @@ const upgradeSubscription = (options) => {
348
452
  body: upgradeSubscriptionBodySchema,
349
453
  metadata: { openapi: { operationId: "upgradeSubscription" } },
350
454
  use: [
351
- sessionMiddleware,
455
+ stripeSessionMiddleware,
456
+ referenceMiddleware(subscriptionOptions, "upgrade-subscription"),
352
457
  originCheck((c) => {
353
458
  return [c.body.successUrl, c.body.cancelUrl];
354
- }),
355
- referenceMiddleware(subscriptionOptions, "upgrade-subscription")
459
+ })
356
460
  ]
357
461
  }, async (ctx) => {
358
462
  const { user: user$1, session } = ctx.context.session;
359
- if (!user$1.emailVerified && subscriptionOptions.requireEmailVerification) throw APIError.from("BAD_REQUEST", STRIPE_ERROR_CODES$1.EMAIL_VERIFICATION_REQUIRED);
360
- const referenceId = ctx.body.referenceId || user$1.id;
463
+ const customerType = ctx.body.customerType || "user";
464
+ const referenceId = ctx.body.referenceId || getReferenceId(ctx.context.session, customerType, options);
465
+ if (!user$1.emailVerified && subscriptionOptions.requireEmailVerification) throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.EMAIL_VERIFICATION_REQUIRED);
361
466
  const plan = await getPlanByName(options, ctx.body.plan);
362
- if (!plan) throw APIError.from("BAD_REQUEST", STRIPE_ERROR_CODES$1.SUBSCRIPTION_PLAN_NOT_FOUND);
467
+ if (!plan) throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.SUBSCRIPTION_PLAN_NOT_FOUND);
363
468
  let subscriptionToUpdate = ctx.body.subscriptionId ? await ctx.context.adapter.findOne({
364
469
  model: "subscription",
365
470
  where: [{
@@ -374,39 +479,94 @@ const upgradeSubscription = (options) => {
374
479
  }]
375
480
  }) : null;
376
481
  if (ctx.body.subscriptionId && subscriptionToUpdate && subscriptionToUpdate.referenceId !== referenceId) subscriptionToUpdate = null;
377
- if (ctx.body.subscriptionId && !subscriptionToUpdate) throw APIError.from("BAD_REQUEST", STRIPE_ERROR_CODES$1.SUBSCRIPTION_NOT_FOUND);
378
- let customerId = subscriptionToUpdate?.stripeCustomerId || user$1.stripeCustomerId;
379
- if (!customerId) try {
380
- let stripeCustomer = (await client.customers.list({
381
- email: user$1.email,
382
- limit: 1
383
- })).data[0];
384
- if (!stripeCustomer) stripeCustomer = await client.customers.create({
385
- email: user$1.email,
386
- name: user$1.name,
387
- metadata: {
388
- ...ctx.body.metadata,
389
- userId: user$1.id
482
+ if (ctx.body.subscriptionId && !subscriptionToUpdate) throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND);
483
+ let customerId;
484
+ if (customerType === "organization") {
485
+ customerId = subscriptionToUpdate?.stripeCustomerId;
486
+ if (!customerId) {
487
+ const org = await ctx.context.adapter.findOne({
488
+ model: "organization",
489
+ where: [{
490
+ field: "id",
491
+ value: referenceId
492
+ }]
493
+ });
494
+ if (!org) throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.ORGANIZATION_NOT_FOUND);
495
+ customerId = org.stripeCustomerId;
496
+ if (!customerId) try {
497
+ let stripeCustomer = (await client.customers.search({
498
+ query: `metadata["organizationId"]:"${org.id}"`,
499
+ limit: 1
500
+ })).data[0];
501
+ if (!stripeCustomer) {
502
+ let extraCreateParams = {};
503
+ if (options.organization?.getCustomerCreateParams) extraCreateParams = await options.organization.getCustomerCreateParams(org, ctx);
504
+ const customerParams = defu({
505
+ name: org.name,
506
+ metadata: {
507
+ ...ctx.body.metadata,
508
+ organizationId: org.id,
509
+ customerType: "organization"
510
+ }
511
+ }, extraCreateParams);
512
+ stripeCustomer = await client.customers.create(customerParams);
513
+ await options.organization?.onCustomerCreate?.({
514
+ stripeCustomer,
515
+ organization: {
516
+ ...org,
517
+ stripeCustomerId: stripeCustomer.id
518
+ }
519
+ }, ctx);
520
+ }
521
+ await ctx.context.adapter.update({
522
+ model: "organization",
523
+ update: { stripeCustomerId: stripeCustomer.id },
524
+ where: [{
525
+ field: "id",
526
+ value: org.id
527
+ }]
528
+ });
529
+ customerId = stripeCustomer.id;
530
+ } catch (e) {
531
+ ctx.context.logger.error(e);
532
+ throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.UNABLE_TO_CREATE_CUSTOMER);
390
533
  }
391
- });
392
- await ctx.context.adapter.update({
393
- model: "user",
394
- update: { stripeCustomerId: stripeCustomer.id },
395
- where: [{
396
- field: "id",
397
- value: user$1.id
398
- }]
399
- });
400
- customerId = stripeCustomer.id;
401
- } catch (e) {
402
- ctx.context.logger.error(e);
403
- throw APIError.from("BAD_REQUEST", STRIPE_ERROR_CODES$1.UNABLE_TO_CREATE_CUSTOMER);
534
+ }
535
+ } else {
536
+ customerId = subscriptionToUpdate?.stripeCustomerId || user$1.stripeCustomerId;
537
+ if (!customerId) try {
538
+ let stripeCustomer = (await client.customers.search({
539
+ query: `email:"${escapeStripeSearchValue(user$1.email)}" AND -metadata["customerType"]:"organization"`,
540
+ limit: 1
541
+ })).data[0];
542
+ if (!stripeCustomer) stripeCustomer = await client.customers.create({
543
+ email: user$1.email,
544
+ name: user$1.name,
545
+ metadata: {
546
+ ...ctx.body.metadata,
547
+ userId: user$1.id,
548
+ customerType: "user"
549
+ }
550
+ });
551
+ await ctx.context.adapter.update({
552
+ model: "user",
553
+ update: { stripeCustomerId: stripeCustomer.id },
554
+ where: [{
555
+ field: "id",
556
+ value: user$1.id
557
+ }]
558
+ });
559
+ customerId = stripeCustomer.id;
560
+ } catch (e) {
561
+ ctx.context.logger.error(e);
562
+ throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.UNABLE_TO_CREATE_CUSTOMER);
563
+ }
404
564
  }
405
565
  const subscriptions$1 = subscriptionToUpdate ? [subscriptionToUpdate] : await ctx.context.adapter.findMany({
406
566
  model: "subscription",
407
567
  where: [{
408
568
  field: "referenceId",
409
- value: ctx.body.referenceId || user$1.id
569
+ value: referenceId
410
570
  }]
411
571
  });
412
572
  const activeOrTrialingSubscription = subscriptions$1.find((sub) => isActiveOrTrialing(sub));
@@ -416,7 +576,7 @@ const upgradeSubscription = (options) => {
416
576
  return false;
417
577
  });
418
578
  const incompleteSubscription = subscriptions$1.find((sub) => sub.status === "incomplete");
419
- 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);
579
+ if (activeOrTrialingSubscription && activeOrTrialingSubscription.status === "active" && activeOrTrialingSubscription.plan === ctx.body.plan && activeOrTrialingSubscription.seats === (ctx.body.seats || 1)) throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.ALREADY_SUBSCRIBED_PLAN);
420
580
  if (activeSubscription && customerId) {
421
581
  let dbSubscription = await ctx.context.adapter.findOne({
422
582
  model: "subscription",
@@ -502,7 +662,7 @@ const upgradeSubscription = (options) => {
502
662
  });
503
663
  if (!subscription) {
504
664
  ctx.context.logger.error("Subscription ID not found");
505
- throw new APIError("INTERNAL_SERVER_ERROR");
665
+ throw APIError$1.from("NOT_FOUND", STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND);
506
666
  }
507
667
  const params = await subscriptionOptions.getCheckoutSessionParams?.({
508
668
  user: user$1,
@@ -530,26 +690,36 @@ const upgradeSubscription = (options) => {
530
690
  const checkoutSession = await client.checkout.sessions.create({
531
691
  ...customerId ? {
532
692
  customer: customerId,
533
- customer_update: {
693
+ customer_update: customerType !== "user" ? { address: "auto" } : {
534
694
  name: "auto",
535
695
  address: "auto"
536
696
  }
537
- } : { customer_email: session.user.email },
697
+ } : { customer_email: user$1.email },
538
698
  success_url: getUrl(ctx, `${ctx.context.baseURL}/subscription/success?callbackURL=${encodeURIComponent(ctx.body.successUrl)}&subscriptionId=${encodeURIComponent(subscription.id)}`),
539
699
  cancel_url: getUrl(ctx, ctx.body.cancelUrl),
540
700
  line_items: [{
541
701
  price: priceIdToUse,
542
702
  quantity: ctx.body.seats || 1
543
703
  }],
544
- subscription_data: { ...freeTrial },
704
+ subscription_data: {
705
+ ...freeTrial,
706
+ metadata: {
707
+ ...ctx.body.metadata,
708
+ ...params?.params?.subscription_data?.metadata,
709
+ userId: user$1.id,
710
+ subscriptionId: subscription.id,
711
+ referenceId
712
+ }
713
+ },
545
714
  mode: "subscription",
546
715
  client_reference_id: referenceId,
547
716
  ...params?.params,
548
717
  metadata: {
718
+ ...ctx.body.metadata,
719
+ ...params?.params?.metadata,
549
720
  userId: user$1.id,
550
721
  subscriptionId: subscription.id,
551
- referenceId,
552
- ...params?.params?.metadata
722
+ referenceId
553
723
  }
554
724
  }, params?.options).catch(async (e) => {
555
725
  throw ctx.error("BAD_REQUEST", {
@@ -621,6 +791,7 @@ const cancelSubscriptionCallback = (options) => {
621
791
  const cancelSubscriptionBodySchema = z.object({
622
792
  referenceId: z.string().meta({ description: "Reference id of the subscription to cancel. Eg: '123'" }).optional(),
623
793
  subscriptionId: z.string().meta({ description: "The Stripe subscription ID to cancel. Eg: 'sub_1ABC2DEF3GHI4JKL'" }).optional(),
794
+ customerType: z.enum(["user", "organization"]).meta({ description: "Customer type for the subscription. Eg: \"user\" or \"organization\"" }).optional(),
624
795
  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\"" }),
625
796
  disableRedirect: z.boolean().meta({ description: "Disable redirect after successful subscription cancellation. Eg: true" }).default(false)
626
797
  });
@@ -647,12 +818,13 @@ const cancelSubscription = (options) => {
647
818
  body: cancelSubscriptionBodySchema,
648
819
  metadata: { openapi: { operationId: "cancelSubscription" } },
649
820
  use: [
650
- sessionMiddleware,
651
- originCheck((ctx) => ctx.body.returnUrl),
652
- referenceMiddleware(subscriptionOptions, "cancel-subscription")
821
+ stripeSessionMiddleware,
822
+ referenceMiddleware(subscriptionOptions, "cancel-subscription"),
823
+ originCheck((ctx) => ctx.body.returnUrl)
653
824
  ]
654
825
  }, async (ctx) => {
655
- const referenceId = ctx.body?.referenceId || ctx.context.session.user.id;
826
+ const customerType = ctx.body.customerType || "user";
827
+ const referenceId = ctx.body.referenceId || getReferenceId(ctx.context.session, customerType, options);
656
828
  let subscription = ctx.body.subscriptionId ? await ctx.context.adapter.findOne({
657
829
  model: "subscription",
658
830
  where: [{
@@ -667,7 +839,7 @@ const cancelSubscription = (options) => {
667
839
  }]
668
840
  }).then((subs) => subs.find((sub) => isActiveOrTrialing(sub)));
669
841
  if (ctx.body.subscriptionId && subscription && subscription.referenceId !== referenceId) subscription = void 0;
670
- if (!subscription || !subscription.stripeCustomerId) throw APIError.from("BAD_REQUEST", STRIPE_ERROR_CODES$1.SUBSCRIPTION_NOT_FOUND);
842
+ if (!subscription || !subscription.stripeCustomerId) throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND);
671
843
  const activeSubscriptions = await client.subscriptions.list({ customer: subscription.stripeCustomerId }).then((res) => res.data.filter((sub) => isActiveOrTrialing(sub)));
672
844
  if (!activeSubscriptions.length) {
673
845
  /**
@@ -681,10 +853,10 @@ const cancelSubscription = (options) => {
681
853
  value: referenceId
682
854
  }]
683
855
  });
684
- throw APIError.from("BAD_REQUEST", STRIPE_ERROR_CODES$1.SUBSCRIPTION_NOT_FOUND);
856
+ throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND);
685
857
  }
686
858
  const activeSubscription = activeSubscriptions.find((sub) => sub.id === subscription.stripeSubscriptionId);
687
- if (!activeSubscription) throw APIError.from("BAD_REQUEST", STRIPE_ERROR_CODES$1.SUBSCRIPTION_NOT_FOUND);
859
+ if (!activeSubscription) throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND);
688
860
  const { url } = await client.billingPortal.sessions.create({
689
861
  customer: subscription.stripeCustomerId,
690
862
  return_url: getUrl(ctx, `${ctx.context.baseURL}/subscription/cancel/callback?callbackURL=${encodeURIComponent(ctx.body?.returnUrl || "/")}&subscriptionId=${encodeURIComponent(subscription.id)}`),
@@ -727,7 +899,8 @@ const cancelSubscription = (options) => {
727
899
  };
728
900
  const restoreSubscriptionBodySchema = z.object({
729
901
  referenceId: z.string().meta({ description: "Reference id of the subscription to restore. Eg: '123'" }).optional(),
730
- subscriptionId: z.string().meta({ description: "The Stripe subscription ID to restore. Eg: 'sub_1ABC2DEF3GHI4JKL'" }).optional()
902
+ subscriptionId: z.string().meta({ description: "The Stripe subscription ID to restore. Eg: 'sub_1ABC2DEF3GHI4JKL'" }).optional(),
903
+ customerType: z.enum(["user", "organization"]).meta({ description: "Customer type for the subscription. Eg: \"user\" or \"organization\"" }).optional()
731
904
  });
732
905
  const restoreSubscription = (options) => {
733
906
  const client = options.stripeClient;
@@ -736,9 +909,10 @@ const restoreSubscription = (options) => {
736
909
  method: "POST",
737
910
  body: restoreSubscriptionBodySchema,
738
911
  metadata: { openapi: { operationId: "restoreSubscription" } },
739
- use: [sessionMiddleware, referenceMiddleware(subscriptionOptions, "restore-subscription")]
912
+ use: [stripeSessionMiddleware, referenceMiddleware(subscriptionOptions, "restore-subscription")]
740
913
  }, async (ctx) => {
741
- const referenceId = ctx.body?.referenceId || ctx.context.session.user.id;
914
+ const customerType = ctx.body.customerType || "user";
915
+ const referenceId = ctx.body.referenceId || getReferenceId(ctx.context.session, customerType, options);
742
916
  let subscription = ctx.body.subscriptionId ? await ctx.context.adapter.findOne({
743
917
  model: "subscription",
744
918
  where: [{
@@ -753,11 +927,11 @@ const restoreSubscription = (options) => {
753
927
  }]
754
928
  }).then((subs) => subs.find((sub) => isActiveOrTrialing(sub)));
755
929
  if (ctx.body.subscriptionId && subscription && subscription.referenceId !== referenceId) subscription = void 0;
756
- if (!subscription || !subscription.stripeCustomerId) throw APIError.from("BAD_REQUEST", STRIPE_ERROR_CODES$1.SUBSCRIPTION_NOT_FOUND);
757
- if (subscription.status != "active" && subscription.status != "trialing") throw APIError.from("BAD_REQUEST", STRIPE_ERROR_CODES$1.SUBSCRIPTION_NOT_ACTIVE);
758
- if (!isPendingCancel(subscription)) throw APIError.from("BAD_REQUEST", STRIPE_ERROR_CODES$1.SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION);
930
+ if (!subscription || !subscription.stripeCustomerId) throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND);
931
+ if (!isActiveOrTrialing(subscription)) throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_ACTIVE);
932
+ if (!isPendingCancel(subscription)) throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION);
759
933
  const activeSubscription = await client.subscriptions.list({ customer: subscription.stripeCustomerId }).then((res) => res.data.filter((sub) => isActiveOrTrialing(sub))[0]);
760
- if (!activeSubscription) throw APIError.from("BAD_REQUEST", STRIPE_ERROR_CODES$1.SUBSCRIPTION_NOT_FOUND);
934
+ if (!activeSubscription) throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND);
761
935
  const updateParams = {};
762
936
  if (activeSubscription.cancel_at) updateParams.cancel_at = "";
763
937
  else if (activeSubscription.cancel_at_period_end) updateParams.cancel_at_period_end = false;
@@ -783,7 +957,10 @@ const restoreSubscription = (options) => {
783
957
  return ctx.json(newSub);
784
958
  });
785
959
  };
786
- const listActiveSubscriptionsQuerySchema = z.optional(z.object({ referenceId: z.string().meta({ description: "Reference id of the subscription to list. Eg: '123'" }).optional() }));
960
+ const listActiveSubscriptionsQuerySchema = z.optional(z.object({
961
+ referenceId: z.string().meta({ description: "Reference id of the subscription to list. Eg: '123'" }).optional(),
962
+ customerType: z.enum(["user", "organization"]).meta({ description: "Customer type for the subscription. Eg: \"user\" or \"organization\"" }).optional()
963
+ }));
787
964
  /**
788
965
  * ### Endpoint
789
966
  *
@@ -805,13 +982,15 @@ const listActiveSubscriptions = (options) => {
805
982
  method: "GET",
806
983
  query: listActiveSubscriptionsQuerySchema,
807
984
  metadata: { openapi: { operationId: "listActiveSubscriptions" } },
808
- use: [sessionMiddleware, referenceMiddleware(subscriptionOptions, "list-subscription")]
985
+ use: [stripeSessionMiddleware, referenceMiddleware(subscriptionOptions, "list-subscription")]
809
986
  }, async (ctx) => {
987
+ const customerType = ctx.query?.customerType || "user";
988
+ const referenceId = ctx.query?.referenceId || getReferenceId(ctx.context.session, customerType, options);
810
989
  const subscriptions$1 = await ctx.context.adapter.findMany({
811
990
  model: "subscription",
812
991
  where: [{
813
992
  field: "referenceId",
814
- value: ctx.query?.referenceId || ctx.context.session.user.id
993
+ value: referenceId
815
994
  }]
816
995
  });
817
996
  if (!subscriptions$1.length) return [];
@@ -838,10 +1017,9 @@ const subscriptionSuccess = (options) => {
838
1017
  use: [originCheck((ctx) => ctx.query.callbackURL)]
839
1018
  }, async (ctx) => {
840
1019
  if (!ctx.query || !ctx.query.callbackURL || !ctx.query.subscriptionId) throw ctx.redirect(getUrl(ctx, ctx.query?.callbackURL || "/"));
1020
+ const { callbackURL, subscriptionId } = ctx.query;
841
1021
  const session = await getSessionFromCtx(ctx);
842
1022
  if (!session) throw ctx.redirect(getUrl(ctx, ctx.query?.callbackURL || "/"));
843
- const { user: user$1 } = session;
844
- const { callbackURL, subscriptionId } = ctx.query;
845
1023
  const subscription = await ctx.context.adapter.findOne({
846
1024
  model: "subscription",
847
1025
  where: [{
@@ -849,41 +1027,53 @@ const subscriptionSuccess = (options) => {
849
1027
  value: subscriptionId
850
1028
  }]
851
1029
  });
852
- if (subscription?.status === "active" || subscription?.status === "trialing") return ctx.redirect(getUrl(ctx, callbackURL));
853
- const customerId = subscription?.stripeCustomerId || user$1.stripeCustomerId;
854
- if (customerId) try {
855
- const stripeSubscription = await client.subscriptions.list({
856
- customer: customerId,
857
- status: "active"
858
- }).then((res) => res.data[0]);
859
- if (stripeSubscription) {
860
- const plan = await getPlanByPriceInfo(options, stripeSubscription.items.data[0]?.price.id, stripeSubscription.items.data[0]?.price.lookup_key);
861
- if (plan && subscription) await ctx.context.adapter.update({
862
- model: "subscription",
863
- update: {
864
- status: stripeSubscription.status,
865
- seats: stripeSubscription.items.data[0]?.quantity || 1,
866
- plan: plan.name.toLowerCase(),
867
- periodEnd: /* @__PURE__ */ new Date(stripeSubscription.items.data[0]?.current_period_end * 1e3),
868
- periodStart: /* @__PURE__ */ new Date(stripeSubscription.items.data[0]?.current_period_start * 1e3),
869
- stripeSubscriptionId: stripeSubscription.id,
870
- cancelAtPeriodEnd: stripeSubscription.cancel_at_period_end,
871
- cancelAt: stripeSubscription.cancel_at ? /* @__PURE__ */ new Date(stripeSubscription.cancel_at * 1e3) : null,
872
- canceledAt: stripeSubscription.canceled_at ? /* @__PURE__ */ new Date(stripeSubscription.canceled_at * 1e3) : null,
873
- ...stripeSubscription.trial_start && stripeSubscription.trial_end ? {
874
- trialStart: /* @__PURE__ */ new Date(stripeSubscription.trial_start * 1e3),
875
- trialEnd: /* @__PURE__ */ new Date(stripeSubscription.trial_end * 1e3)
876
- } : {}
877
- },
878
- where: [{
879
- field: "id",
880
- value: subscription.id
881
- }]
882
- });
883
- }
884
- } catch (error) {
1030
+ if (!subscription) {
1031
+ ctx.context.logger.warn(`Subscription record not found for subscriptionId: ${subscriptionId}`);
1032
+ throw ctx.redirect(getUrl(ctx, callbackURL));
1033
+ }
1034
+ if (isActiveOrTrialing(subscription)) throw ctx.redirect(getUrl(ctx, callbackURL));
1035
+ const customerId = subscription.stripeCustomerId || session.user.stripeCustomerId;
1036
+ if (!customerId) throw ctx.redirect(getUrl(ctx, callbackURL));
1037
+ const stripeSubscription = await client.subscriptions.list({
1038
+ customer: customerId,
1039
+ status: "active"
1040
+ }).then((res) => res.data[0]).catch((error) => {
885
1041
  ctx.context.logger.error("Error fetching subscription from Stripe", error);
1042
+ throw ctx.redirect(getUrl(ctx, callbackURL));
1043
+ });
1044
+ if (!stripeSubscription) throw ctx.redirect(getUrl(ctx, callbackURL));
1045
+ const subscriptionItem = stripeSubscription.items.data[0];
1046
+ if (!subscriptionItem) {
1047
+ ctx.context.logger.warn(`No subscription items found for Stripe subscription ${stripeSubscription.id}`);
1048
+ throw ctx.redirect(getUrl(ctx, callbackURL));
1049
+ }
1050
+ const plan = await getPlanByPriceInfo(options, subscriptionItem.price.id, subscriptionItem.price.lookup_key);
1051
+ if (!plan) {
1052
+ ctx.context.logger.warn(`Plan not found for price ${subscriptionItem.price.id}`);
1053
+ throw ctx.redirect(getUrl(ctx, callbackURL));
886
1054
  }
1055
+ await ctx.context.adapter.update({
1056
+ model: "subscription",
1057
+ update: {
1058
+ status: stripeSubscription.status,
1059
+ seats: subscriptionItem.quantity || 1,
1060
+ plan: plan.name.toLowerCase(),
1061
+ periodEnd: /* @__PURE__ */ new Date(subscriptionItem.current_period_end * 1e3),
1062
+ periodStart: /* @__PURE__ */ new Date(subscriptionItem.current_period_start * 1e3),
1063
+ stripeSubscriptionId: stripeSubscription.id,
1064
+ cancelAtPeriodEnd: stripeSubscription.cancel_at_period_end,
1065
+ cancelAt: stripeSubscription.cancel_at ? /* @__PURE__ */ new Date(stripeSubscription.cancel_at * 1e3) : null,
1066
+ canceledAt: stripeSubscription.canceled_at ? /* @__PURE__ */ new Date(stripeSubscription.canceled_at * 1e3) : null,
1067
+ ...stripeSubscription.trial_start && stripeSubscription.trial_end ? {
1068
+ trialStart: /* @__PURE__ */ new Date(stripeSubscription.trial_start * 1e3),
1069
+ trialEnd: /* @__PURE__ */ new Date(stripeSubscription.trial_end * 1e3)
1070
+ } : {}
1071
+ },
1072
+ where: [{
1073
+ field: "id",
1074
+ value: subscription.id
1075
+ }]
1076
+ });
887
1077
  throw ctx.redirect(getUrl(ctx, callbackURL));
888
1078
  });
889
1079
  };
@@ -892,6 +1082,7 @@ const createBillingPortalBodySchema = z.object({
892
1082
  return typeof localization === "string";
893
1083
  }).optional(),
894
1084
  referenceId: z.string().optional(),
1085
+ customerType: z.enum(["user", "organization"]).meta({ description: "Customer type for the subscription. Eg: \"user\" or \"organization\"" }).optional(),
895
1086
  returnUrl: z.string().default("/"),
896
1087
  disableRedirect: z.boolean().meta({ description: "Disable redirect after creating billing portal session. Eg: true" }).default(false)
897
1088
  });
@@ -903,22 +1094,41 @@ const createBillingPortal = (options) => {
903
1094
  body: createBillingPortalBodySchema,
904
1095
  metadata: { openapi: { operationId: "createBillingPortal" } },
905
1096
  use: [
906
- sessionMiddleware,
907
- originCheck((ctx) => ctx.body.returnUrl),
908
- referenceMiddleware(subscriptionOptions, "billing-portal")
1097
+ stripeSessionMiddleware,
1098
+ referenceMiddleware(subscriptionOptions, "billing-portal"),
1099
+ originCheck((ctx) => ctx.body.returnUrl)
909
1100
  ]
910
1101
  }, async (ctx) => {
911
1102
  const { user: user$1 } = ctx.context.session;
912
- const referenceId = ctx.body.referenceId || user$1.id;
913
- let customerId = user$1.stripeCustomerId;
914
- if (!customerId) customerId = (await ctx.context.adapter.findMany({
915
- model: "subscription",
916
- where: [{
917
- field: "referenceId",
918
- value: referenceId
919
- }]
920
- }).then((subs) => subs.find((sub) => isActiveOrTrialing(sub))))?.stripeCustomerId;
921
- if (!customerId) throw new APIError("BAD_REQUEST", { message: "No Stripe customer found for this user" });
1103
+ const customerType = ctx.body.customerType || "user";
1104
+ const referenceId = ctx.body.referenceId || getReferenceId(ctx.context.session, customerType, options);
1105
+ let customerId;
1106
+ if (customerType === "organization") {
1107
+ customerId = (await ctx.context.adapter.findOne({
1108
+ model: "organization",
1109
+ where: [{
1110
+ field: "id",
1111
+ value: referenceId
1112
+ }]
1113
+ }))?.stripeCustomerId;
1114
+ if (!customerId) customerId = (await ctx.context.adapter.findMany({
1115
+ model: "subscription",
1116
+ where: [{
1117
+ field: "referenceId",
1118
+ value: referenceId
1119
+ }]
1120
+ }).then((subs) => subs.find((sub) => isActiveOrTrialing(sub))))?.stripeCustomerId;
1121
+ } else {
1122
+ customerId = user$1.stripeCustomerId;
1123
+ if (!customerId) customerId = (await ctx.context.adapter.findMany({
1124
+ model: "subscription",
1125
+ where: [{
1126
+ field: "referenceId",
1127
+ value: referenceId
1128
+ }]
1129
+ }).then((subs) => subs.find((sub) => isActiveOrTrialing(sub))))?.stripeCustomerId;
1130
+ }
1131
+ if (!customerId) throw APIError$1.from("NOT_FOUND", STRIPE_ERROR_CODES.CUSTOMER_NOT_FOUND);
922
1132
  try {
923
1133
  const { url } = await client.billingPortal.sessions.create({
924
1134
  locale: ctx.body.locale,
@@ -931,7 +1141,7 @@ const createBillingPortal = (options) => {
931
1141
  });
932
1142
  } catch (error) {
933
1143
  ctx.context.logger.error("Error creating billing portal session", error);
934
- throw new APIError("BAD_REQUEST", { message: error.message });
1144
+ throw APIError$1.from("INTERNAL_SERVER_ERROR", STRIPE_ERROR_CODES.UNABLE_TO_CREATE_BILLING_PORTAL);
935
1145
  }
936
1146
  });
937
1147
  };
@@ -946,20 +1156,21 @@ const stripeWebhook = (options) => {
946
1156
  cloneRequest: true,
947
1157
  disableBody: true
948
1158
  }, async (ctx) => {
949
- if (!ctx.request?.body) throw new APIError("INTERNAL_SERVER_ERROR");
950
- const buf = await ctx.request.text();
1159
+ if (!ctx.request?.body) throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.INVALID_REQUEST_BODY);
951
1160
  const sig = ctx.request.headers.get("stripe-signature");
1161
+ if (!sig) throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.STRIPE_SIGNATURE_NOT_FOUND);
952
1162
  const webhookSecret = options.stripeWebhookSecret;
1163
+ if (!webhookSecret) throw APIError$1.from("INTERNAL_SERVER_ERROR", STRIPE_ERROR_CODES.STRIPE_WEBHOOK_SECRET_NOT_FOUND);
1164
+ const payload = await ctx.request.text();
953
1165
  let event;
954
1166
  try {
955
- if (!sig || !webhookSecret) throw new APIError("BAD_REQUEST", { message: "Stripe webhook secret not found" });
956
- if (typeof client.webhooks.constructEventAsync === "function") event = await client.webhooks.constructEventAsync(buf, sig, webhookSecret);
957
- else event = client.webhooks.constructEvent(buf, sig, webhookSecret);
1167
+ if (typeof client.webhooks.constructEventAsync === "function") event = await client.webhooks.constructEventAsync(payload, sig, webhookSecret);
1168
+ else event = client.webhooks.constructEvent(payload, sig, webhookSecret);
958
1169
  } catch (err) {
959
1170
  ctx.context.logger.error(`${err.message}`);
960
- throw new APIError("BAD_REQUEST", { message: `Webhook Error: ${err.message}` });
1171
+ throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.FAILED_TO_CONSTRUCT_STRIPE_EVENT);
961
1172
  }
962
- if (!event) throw new APIError("BAD_REQUEST", { message: "Failed to construct event" });
1173
+ if (!event) throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.FAILED_TO_CONSTRUCT_STRIPE_EVENT);
963
1174
  try {
964
1175
  switch (event.type) {
965
1176
  case "checkout.session.completed":
@@ -984,23 +1195,11 @@ const stripeWebhook = (options) => {
984
1195
  }
985
1196
  } catch (e) {
986
1197
  ctx.context.logger.error(`Stripe webhook failed. Error: ${e.message}`);
987
- throw new APIError("BAD_REQUEST", { message: "Webhook error: See server logs for more information." });
1198
+ throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.STRIPE_WEBHOOK_ERROR);
988
1199
  }
989
1200
  return ctx.json({ success: true });
990
1201
  });
991
1202
  };
992
- const getUrl = (ctx, url) => {
993
- if (/^[a-zA-Z][a-zA-Z0-9+\-.]*:/.test(url)) return url;
994
- return `${ctx.context.options.baseURL}${url.startsWith("/") ? url : `/${url}`}`;
995
- };
996
- async function resolvePriceIdFromLookupKey(stripeClient, lookupKey) {
997
- if (!lookupKey) return void 0;
998
- return (await stripeClient.prices.list({
999
- lookup_keys: [lookupKey],
1000
- active: true,
1001
- limit: 1
1002
- })).data[0]?.id;
1003
- }
1004
1203
 
1005
1204
  //#endregion
1006
1205
  //#region src/schema.ts
@@ -1067,6 +1266,10 @@ const user = { user: { fields: { stripeCustomerId: {
1067
1266
  type: "string",
1068
1267
  required: false
1069
1268
  } } } };
1269
+ const organization = { organization: { fields: { stripeCustomerId: {
1270
+ type: "string",
1271
+ required: false
1272
+ } } } };
1070
1273
  const getSchema = (options) => {
1071
1274
  let baseSchema = {};
1072
1275
  if (options.subscription?.enabled) baseSchema = {
@@ -1074,6 +1277,10 @@ const getSchema = (options) => {
1074
1277
  ...user
1075
1278
  };
1076
1279
  else baseSchema = { ...user };
1280
+ if (options.organization?.enabled) baseSchema = {
1281
+ ...baseSchema,
1282
+ ...organization
1283
+ };
1077
1284
  if (options.schema && !options.subscription?.enabled && "subscription" in options.schema) {
1078
1285
  const { subscription: _subscription, ...restSchema } = options.schema;
1079
1286
  return mergeSchema(baseSchema, restSchema);
@@ -1083,16 +1290,6 @@ const getSchema = (options) => {
1083
1290
 
1084
1291
  //#endregion
1085
1292
  //#region src/index.ts
1086
- const STRIPE_ERROR_CODES = defineErrorCodes({
1087
- SUBSCRIPTION_NOT_FOUND: "Subscription not found",
1088
- SUBSCRIPTION_PLAN_NOT_FOUND: "Subscription plan not found",
1089
- ALREADY_SUBSCRIBED_PLAN: "You're already subscribed to this plan",
1090
- UNABLE_TO_CREATE_CUSTOMER: "Unable to create customer",
1091
- FAILED_TO_FETCH_PLANS: "Failed to fetch plans",
1092
- EMAIL_VERIFICATION_REQUIRED: "Email verification is required before you can subscribe to a plan",
1093
- SUBSCRIPTION_NOT_ACTIVE: "Subscription is not active",
1094
- SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION: "Subscription is not scheduled for cancellation"
1095
- });
1096
1293
  const stripe = (options) => {
1097
1294
  const client = options.stripeClient;
1098
1295
  const subscriptionEndpoints = {
@@ -1111,13 +1308,70 @@ const stripe = (options) => {
1111
1308
  ...options.subscription?.enabled ? subscriptionEndpoints : {}
1112
1309
  },
1113
1310
  init(ctx) {
1311
+ if (options.organization?.enabled) {
1312
+ const orgPlugin = ctx.getPlugin("organization");
1313
+ if (!orgPlugin) {
1314
+ ctx.logger.error(`Organization plugin not found`);
1315
+ return;
1316
+ }
1317
+ const existingHooks = orgPlugin.options.organizationHooks ?? {};
1318
+ /**
1319
+ * Sync organization name to Stripe customer
1320
+ */
1321
+ const afterUpdateStripeOrg = async (data) => {
1322
+ const { organization: organization$1 } = data;
1323
+ if (!organization$1?.stripeCustomerId) return;
1324
+ try {
1325
+ const stripeCustomer = await client.customers.retrieve(organization$1.stripeCustomerId);
1326
+ if (stripeCustomer.deleted) {
1327
+ ctx.logger.warn(`Stripe customer ${organization$1.stripeCustomerId} was deleted`);
1328
+ return;
1329
+ }
1330
+ if (organization$1.name !== stripeCustomer.name) {
1331
+ await client.customers.update(organization$1.stripeCustomerId, { name: organization$1.name });
1332
+ ctx.logger.info(`Synced organization name to Stripe: "${stripeCustomer.name}" → "${organization$1.name}"`);
1333
+ }
1334
+ } catch (e) {
1335
+ ctx.logger.error(`Failed to sync organization to Stripe: ${e.message}`);
1336
+ }
1337
+ };
1338
+ /**
1339
+ * Block deletion if organization has active subscriptions
1340
+ */
1341
+ const beforeDeleteStripeOrg = async (data) => {
1342
+ const { organization: organization$1 } = data;
1343
+ if (!organization$1.stripeCustomerId) return;
1344
+ try {
1345
+ const subscriptions$1 = await client.subscriptions.list({
1346
+ customer: organization$1.stripeCustomerId,
1347
+ status: "all",
1348
+ limit: 100
1349
+ });
1350
+ 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);
1351
+ } catch (error) {
1352
+ if (error instanceof APIError) throw error;
1353
+ ctx.logger.error(`Failed to check organization subscriptions: ${error.message}`);
1354
+ throw error;
1355
+ }
1356
+ };
1357
+ orgPlugin.options.organizationHooks = {
1358
+ ...existingHooks,
1359
+ afterUpdateOrganization: existingHooks.afterUpdateOrganization ? async (data) => {
1360
+ await existingHooks.afterUpdateOrganization(data);
1361
+ await afterUpdateStripeOrg(data);
1362
+ } : afterUpdateStripeOrg,
1363
+ beforeDeleteOrganization: existingHooks.beforeDeleteOrganization ? async (data) => {
1364
+ await existingHooks.beforeDeleteOrganization(data);
1365
+ await beforeDeleteStripeOrg(data);
1366
+ } : beforeDeleteStripeOrg
1367
+ };
1368
+ }
1114
1369
  return { options: { databaseHooks: { user: {
1115
1370
  create: { async after(user$1, ctx$1) {
1116
- if (!ctx$1 || !options.createCustomerOnSignUp) return;
1371
+ if (!ctx$1 || !options.createCustomerOnSignUp || user$1.stripeCustomerId) return;
1117
1372
  try {
1118
- if (user$1.stripeCustomerId) return;
1119
- let stripeCustomer = (await client.customers.list({
1120
- email: user$1.email,
1373
+ let stripeCustomer = (await client.customers.search({
1374
+ query: `email:"${escapeStripeSearchValue(user$1.email)}" AND -metadata["customerType"]:"organization"`,
1121
1375
  limit: 1
1122
1376
  })).data[0];
1123
1377
  if (stripeCustomer) {
@@ -1137,7 +1391,10 @@ const stripe = (options) => {
1137
1391
  const params = defu({
1138
1392
  email: user$1.email,
1139
1393
  name: user$1.name,
1140
- metadata: { userId: user$1.id }
1394
+ metadata: {
1395
+ userId: user$1.id,
1396
+ customerType: "user"
1397
+ }
1141
1398
  }, extraCreateParams);
1142
1399
  stripeCustomer = await client.customers.create(params);
1143
1400
  await ctx$1.context.internalAdapter.updateUser(user$1.id, { stripeCustomerId: stripeCustomer.id });
@@ -1154,17 +1411,15 @@ const stripe = (options) => {
1154
1411
  }
1155
1412
  } },
1156
1413
  update: { async after(user$1, ctx$1) {
1157
- if (!ctx$1) return;
1414
+ if (!ctx$1 || !user$1.stripeCustomerId) return;
1158
1415
  try {
1159
- const userWithStripe = user$1;
1160
- if (!userWithStripe.stripeCustomerId) return;
1161
- const stripeCustomer = await client.customers.retrieve(userWithStripe.stripeCustomerId);
1416
+ const stripeCustomer = await client.customers.retrieve(user$1.stripeCustomerId);
1162
1417
  if (stripeCustomer.deleted) {
1163
- ctx$1.context.logger.warn(`Stripe customer ${userWithStripe.stripeCustomerId} was deleted, cannot update email`);
1418
+ ctx$1.context.logger.warn(`Stripe customer ${user$1.stripeCustomerId} was deleted, cannot update email`);
1164
1419
  return;
1165
1420
  }
1166
1421
  if (stripeCustomer.email !== user$1.email) {
1167
- await client.customers.update(userWithStripe.stripeCustomerId, { email: user$1.email });
1422
+ await client.customers.update(user$1.stripeCustomerId, { email: user$1.email });
1168
1423
  ctx$1.context.logger.info(`Updated Stripe customer email from ${stripeCustomer.email} to ${user$1.email}`);
1169
1424
  }
1170
1425
  } catch (e) {