@better-auth/stripe 1.5.0-beta.16 → 1.5.0-beta.18

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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @better-auth/stripe@1.5.0-beta.16 build /home/runner/work/better-auth/better-auth/packages/stripe
2
+ > @better-auth/stripe@1.5.0-beta.18 build /home/runner/work/better-auth/better-auth/packages/stripe
3
3
  > tsdown
4
4
 
5
5
  ℹ tsdown v0.20.3 powered by rolldown v1.0.0-rc.3
@@ -7,16 +7,16 @@
7
7
  ℹ entry: src/index.ts, src/client.ts
8
8
  ℹ tsconfig: tsconfig.json
9
9
  ℹ Build start
10
- ℹ dist/index.mjs  65.58 kB │ gzip: 12.29 kB
11
- [PLUGIN_TIMINGS] Warning: Your build spent significant time in plugin `rolldown-plugin-dts:generate`. See https://rolldown.rs/options/checks#plugintimings for more details.
10
+ ℹ dist/index.mjs  69.57 kB │ gzip: 13.19 kB
12
11
  ℹ dist/client.mjs  0.43 kB │ gzip: 0.29 kB
13
- ℹ dist/index.mjs.map 140.31 kB │ gzip: 26.79 kB
14
- ℹ dist/error-codes-CHMyMR5v.mjs.map  2.23 kB │ gzip: 1.01 kB
15
- ℹ dist/error-codes-CHMyMR5v.mjs  1.78 kB │ gzip: 0.82 kB
12
+ ℹ dist/index.mjs.map 150.44 kB │ gzip: 29.40 kB
13
+ ℹ dist/error-codes-CCosYkXx.mjs.map  2.43 kB │ gzip: 1.08 kB
14
+ ℹ dist/error-codes-CCosYkXx.mjs  1.89 kB │ gzip: 0.85 kB
16
15
  ℹ dist/client.mjs.map  1.26 kB │ gzip: 0.59 kB
17
- ℹ dist/index.d.mts  19.89 kB │ gzip: 3.12 kB
18
- ℹ dist/client.d.mts  4.62 kB │ gzip: 0.92 kB
19
- ℹ dist/types-Bzc6yg83.d.mts  12.67 kB │ gzip: 2.91 kB
20
- ℹ 9 files, total: 248.77 kB
21
- ✔ Build complete in 41456ms
16
+ ℹ dist/index.d.mts  19.78 kB │ gzip: 3.14 kB
17
+ ℹ dist/client.d.mts  4.80 kB │ gzip: 0.93 kB
18
+ ℹ dist/types-OT6L84x4.d.mts  12.92 kB │ gzip: 2.96 kB
19
+ ℹ 9 files, total: 263.52 kB
20
+ [PLUGIN_TIMINGS] Warning: Your build spent significant time in plugin `rolldown-plugin-dts:generate`. See https://rolldown.rs/options/checks#plugintimings for more details.
22
21
 
22
+ ✔ Build complete in 43653ms
package/dist/client.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as StripePlan } from "./types-Bzc6yg83.mjs";
1
+ import { a as StripePlan } from "./types-OT6L84x4.mjs";
2
2
  import { stripe } from "./index.mjs";
3
3
  import * as better_auth0 from "better-auth";
4
4
 
@@ -21,6 +21,7 @@ declare const STRIPE_ERROR_CODES: {
21
21
  EMAIL_VERIFICATION_REQUIRED: better_auth0.RawError<"EMAIL_VERIFICATION_REQUIRED">;
22
22
  SUBSCRIPTION_NOT_ACTIVE: better_auth0.RawError<"SUBSCRIPTION_NOT_ACTIVE">;
23
23
  SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION: better_auth0.RawError<"SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION">;
24
+ SUBSCRIPTION_NOT_PENDING_CHANGE: better_auth0.RawError<"SUBSCRIPTION_NOT_PENDING_CHANGE">;
24
25
  ORGANIZATION_NOT_FOUND: better_auth0.RawError<"ORGANIZATION_NOT_FOUND">;
25
26
  ORGANIZATION_SUBSCRIPTION_NOT_ENABLED: better_auth0.RawError<"ORGANIZATION_SUBSCRIPTION_NOT_ENABLED">;
26
27
  AUTHORIZE_REFERENCE_REQUIRED: better_auth0.RawError<"AUTHORIZE_REFERENCE_REQUIRED">;
@@ -66,6 +67,7 @@ declare const stripeClient: <O extends {
66
67
  EMAIL_VERIFICATION_REQUIRED: better_auth0.RawError<"EMAIL_VERIFICATION_REQUIRED">;
67
68
  SUBSCRIPTION_NOT_ACTIVE: better_auth0.RawError<"SUBSCRIPTION_NOT_ACTIVE">;
68
69
  SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION: better_auth0.RawError<"SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION">;
70
+ SUBSCRIPTION_NOT_PENDING_CHANGE: better_auth0.RawError<"SUBSCRIPTION_NOT_PENDING_CHANGE">;
69
71
  ORGANIZATION_NOT_FOUND: better_auth0.RawError<"ORGANIZATION_NOT_FOUND">;
70
72
  ORGANIZATION_SUBSCRIPTION_NOT_ENABLED: better_auth0.RawError<"ORGANIZATION_SUBSCRIPTION_NOT_ENABLED">;
71
73
  AUTHORIZE_REFERENCE_REQUIRED: better_auth0.RawError<"AUTHORIZE_REFERENCE_REQUIRED">;
package/dist/client.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { t as STRIPE_ERROR_CODES } from "./error-codes-CHMyMR5v.mjs";
1
+ import { t as STRIPE_ERROR_CODES } from "./error-codes-CCosYkXx.mjs";
2
2
 
3
3
  //#region src/client.ts
4
4
  const stripeClient = (options) => {
@@ -19,6 +19,7 @@ const STRIPE_ERROR_CODES = defineErrorCodes({
19
19
  EMAIL_VERIFICATION_REQUIRED: "Email verification is required before you can subscribe to a plan",
20
20
  SUBSCRIPTION_NOT_ACTIVE: "Subscription is not active",
21
21
  SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION: "Subscription is not scheduled for cancellation",
22
+ SUBSCRIPTION_NOT_PENDING_CHANGE: "Subscription has no pending cancellation or scheduled plan change",
22
23
  ORGANIZATION_NOT_FOUND: "Organization not found",
23
24
  ORGANIZATION_SUBSCRIPTION_NOT_ENABLED: "Organization subscription is not enabled",
24
25
  AUTHORIZE_REFERENCE_REQUIRED: "Organization subscriptions require authorizeReference callback to be configured",
@@ -28,4 +29,4 @@ const STRIPE_ERROR_CODES = defineErrorCodes({
28
29
 
29
30
  //#endregion
30
31
  export { STRIPE_ERROR_CODES as t };
31
- //# sourceMappingURL=error-codes-CHMyMR5v.mjs.map
32
+ //# sourceMappingURL=error-codes-CCosYkXx.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"error-codes-CHMyMR5v.mjs","names":[],"sources":["../src/error-codes.ts"],"sourcesContent":["import { defineErrorCodes } from \"@better-auth/core/utils/error-codes\";\n\nexport const STRIPE_ERROR_CODES = defineErrorCodes({\n\tUNAUTHORIZED: \"Unauthorized access\",\n\tINVALID_REQUEST_BODY: \"Invalid request body\",\n\tSUBSCRIPTION_NOT_FOUND: \"Subscription not found\",\n\tSUBSCRIPTION_PLAN_NOT_FOUND: \"Subscription plan not found\",\n\tALREADY_SUBSCRIBED_PLAN: \"You're already subscribed to this plan\",\n\tREFERENCE_ID_NOT_ALLOWED: \"Reference id is not allowed\",\n\tCUSTOMER_NOT_FOUND: \"Stripe customer not found for this user\",\n\tUNABLE_TO_CREATE_CUSTOMER: \"Unable to create customer\",\n\tUNABLE_TO_CREATE_BILLING_PORTAL: \"Unable to create billing portal session\",\n\tSTRIPE_SIGNATURE_NOT_FOUND: \"Stripe signature not found\",\n\tSTRIPE_WEBHOOK_SECRET_NOT_FOUND: \"Stripe webhook secret not found\",\n\tSTRIPE_WEBHOOK_ERROR: \"Stripe webhook error\",\n\tFAILED_TO_CONSTRUCT_STRIPE_EVENT: \"Failed to construct Stripe event\",\n\tFAILED_TO_FETCH_PLANS: \"Failed to fetch plans\",\n\tEMAIL_VERIFICATION_REQUIRED:\n\t\t\"Email verification is required before you can subscribe to a plan\",\n\tSUBSCRIPTION_NOT_ACTIVE: \"Subscription is not active\",\n\tSUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION:\n\t\t\"Subscription is not scheduled for cancellation\",\n\tORGANIZATION_NOT_FOUND: \"Organization not found\",\n\tORGANIZATION_SUBSCRIPTION_NOT_ENABLED:\n\t\t\"Organization subscription is not enabled\",\n\tAUTHORIZE_REFERENCE_REQUIRED:\n\t\t\"Organization subscriptions require authorizeReference callback to be configured\",\n\tORGANIZATION_HAS_ACTIVE_SUBSCRIPTION:\n\t\t\"Cannot delete organization with active subscription\",\n\tORGANIZATION_REFERENCE_ID_REQUIRED:\n\t\t\"Reference ID is required. Provide referenceId or set activeOrganizationId in session\",\n});\n"],"mappings":";;;AAEA,MAAa,qBAAqB,iBAAiB;CAClD,cAAc;CACd,sBAAsB;CACtB,wBAAwB;CACxB,6BAA6B;CAC7B,yBAAyB;CACzB,0BAA0B;CAC1B,oBAAoB;CACpB,2BAA2B;CAC3B,iCAAiC;CACjC,4BAA4B;CAC5B,iCAAiC;CACjC,sBAAsB;CACtB,kCAAkC;CAClC,uBAAuB;CACvB,6BACC;CACD,yBAAyB;CACzB,6CACC;CACD,wBAAwB;CACxB,uCACC;CACD,8BACC;CACD,sCACC;CACD,oCACC;CACD,CAAC"}
1
+ {"version":3,"file":"error-codes-CCosYkXx.mjs","names":[],"sources":["../src/error-codes.ts"],"sourcesContent":["import { defineErrorCodes } from \"@better-auth/core/utils/error-codes\";\n\nexport const STRIPE_ERROR_CODES = defineErrorCodes({\n\tUNAUTHORIZED: \"Unauthorized access\",\n\tINVALID_REQUEST_BODY: \"Invalid request body\",\n\tSUBSCRIPTION_NOT_FOUND: \"Subscription not found\",\n\tSUBSCRIPTION_PLAN_NOT_FOUND: \"Subscription plan not found\",\n\tALREADY_SUBSCRIBED_PLAN: \"You're already subscribed to this plan\",\n\tREFERENCE_ID_NOT_ALLOWED: \"Reference id is not allowed\",\n\tCUSTOMER_NOT_FOUND: \"Stripe customer not found for this user\",\n\tUNABLE_TO_CREATE_CUSTOMER: \"Unable to create customer\",\n\tUNABLE_TO_CREATE_BILLING_PORTAL: \"Unable to create billing portal session\",\n\tSTRIPE_SIGNATURE_NOT_FOUND: \"Stripe signature not found\",\n\tSTRIPE_WEBHOOK_SECRET_NOT_FOUND: \"Stripe webhook secret not found\",\n\tSTRIPE_WEBHOOK_ERROR: \"Stripe webhook error\",\n\tFAILED_TO_CONSTRUCT_STRIPE_EVENT: \"Failed to construct Stripe event\",\n\tFAILED_TO_FETCH_PLANS: \"Failed to fetch plans\",\n\tEMAIL_VERIFICATION_REQUIRED:\n\t\t\"Email verification is required before you can subscribe to a plan\",\n\tSUBSCRIPTION_NOT_ACTIVE: \"Subscription is not active\",\n\t/**\n\t * @deprecated Use `SUBSCRIPTION_NOT_PENDING_CHANGE` instead.\n\t */\n\tSUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION:\n\t\t\"Subscription is not scheduled for cancellation\",\n\tSUBSCRIPTION_NOT_PENDING_CHANGE:\n\t\t\"Subscription has no pending cancellation or scheduled plan change\",\n\tORGANIZATION_NOT_FOUND: \"Organization not found\",\n\tORGANIZATION_SUBSCRIPTION_NOT_ENABLED:\n\t\t\"Organization subscription is not enabled\",\n\tAUTHORIZE_REFERENCE_REQUIRED:\n\t\t\"Organization subscriptions require authorizeReference callback to be configured\",\n\tORGANIZATION_HAS_ACTIVE_SUBSCRIPTION:\n\t\t\"Cannot delete organization with active subscription\",\n\tORGANIZATION_REFERENCE_ID_REQUIRED:\n\t\t\"Reference ID is required. Provide referenceId or set activeOrganizationId in session\",\n});\n"],"mappings":";;;AAEA,MAAa,qBAAqB,iBAAiB;CAClD,cAAc;CACd,sBAAsB;CACtB,wBAAwB;CACxB,6BAA6B;CAC7B,yBAAyB;CACzB,0BAA0B;CAC1B,oBAAoB;CACpB,2BAA2B;CAC3B,iCAAiC;CACjC,4BAA4B;CAC5B,iCAAiC;CACjC,sBAAsB;CACtB,kCAAkC;CAClC,uBAAuB;CACvB,6BACC;CACD,yBAAyB;CAIzB,6CACC;CACD,iCACC;CACD,wBAAwB;CACxB,uCACC;CACD,8BACC;CACD,sCACC;CACD,oCACC;CACD,CAAC"}
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as StripePlan, c as WithActiveOrganizationId, i as StripeOptions, l as WithStripeCustomerId, n as CustomerType, o as Subscription, r as StripeCtxSession, s as SubscriptionOptions, t as AuthorizeReferenceAction } from "./types-Bzc6yg83.mjs";
1
+ import { a as StripePlan, c as WithActiveOrganizationId, i as StripeOptions, l as WithStripeCustomerId, n as CustomerType, o as Subscription, r as StripeCtxSession, s as SubscriptionOptions, t as AuthorizeReferenceAction } from "./types-OT6L84x4.mjs";
2
2
  import * as better_auth0 from "better-auth";
3
3
  import { User } from "better-auth";
4
4
  import * as better_call0 from "better-call";
@@ -49,6 +49,7 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
49
49
  successUrl: zod.ZodDefault<zod.ZodString>;
50
50
  cancelUrl: zod.ZodDefault<zod.ZodString>;
51
51
  returnUrl: zod.ZodOptional<zod.ZodString>;
52
+ scheduleAtPeriodEnd: zod.ZodDefault<zod.ZodBoolean>;
52
53
  disableRedirect: zod.ZodDefault<zod.ZodBoolean>;
53
54
  }, better_auth0.$strip>;
54
55
  metadata: {
@@ -165,16 +166,6 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
165
166
  stripeAccount?: string;
166
167
  };
167
168
  }>;
168
- cancelSubscriptionCallback: better_call0.StrictEndpoint<"/subscription/cancel/callback", {
169
- method: "GET";
170
- query: zod.ZodOptional<zod.ZodRecord<zod.ZodString, zod.ZodAny>>;
171
- metadata: {
172
- openapi: {
173
- operationId: string;
174
- };
175
- };
176
- use: ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<void>)[];
177
- }, never>;
178
169
  cancelSubscription: better_call0.StrictEndpoint<"/subscription/cancel", {
179
170
  method: "POST";
180
171
  body: zod.ZodObject<{
@@ -327,6 +318,7 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
327
318
  groupId?: string | undefined;
328
319
  seats?: number | undefined;
329
320
  billingInterval?: "day" | "week" | "month" | "year" | undefined;
321
+ stripeScheduleId?: string | undefined;
330
322
  }[]>;
331
323
  subscriptionSuccess: better_call0.StrictEndpoint<"/subscription/success", {
332
324
  method: "GET";
@@ -476,6 +468,10 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
476
468
  type: "string";
477
469
  required: false;
478
470
  };
471
+ stripeScheduleId: {
472
+ type: "string";
473
+ required: false;
474
+ };
479
475
  };
480
476
  };
481
477
  } : {}) & (O["organization"] extends {
@@ -509,6 +505,7 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
509
505
  EMAIL_VERIFICATION_REQUIRED: better_auth0.RawError<"EMAIL_VERIFICATION_REQUIRED">;
510
506
  SUBSCRIPTION_NOT_ACTIVE: better_auth0.RawError<"SUBSCRIPTION_NOT_ACTIVE">;
511
507
  SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION: better_auth0.RawError<"SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION">;
508
+ SUBSCRIPTION_NOT_PENDING_CHANGE: better_auth0.RawError<"SUBSCRIPTION_NOT_PENDING_CHANGE">;
512
509
  ORGANIZATION_NOT_FOUND: better_auth0.RawError<"ORGANIZATION_NOT_FOUND">;
513
510
  ORGANIZATION_SUBSCRIPTION_NOT_ENABLED: better_auth0.RawError<"ORGANIZATION_SUBSCRIPTION_NOT_ENABLED">;
514
511
  AUTHORIZE_REFERENCE_REQUIRED: better_auth0.RawError<"AUTHORIZE_REFERENCE_REQUIRED">;
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { t as STRIPE_ERROR_CODES } from "./error-codes-CHMyMR5v.mjs";
1
+ import { t as STRIPE_ERROR_CODES } from "./error-codes-CCosYkXx.mjs";
2
2
  import { APIError, HIDE_METADATA } from "better-auth";
3
3
  import { defu } from "defu";
4
4
  import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api";
@@ -355,7 +355,8 @@ async function onSubscriptionUpdated(ctx, options, event) {
355
355
  endedAt: subscriptionUpdated.ended_at ? /* @__PURE__ */ new Date(subscriptionUpdated.ended_at * 1e3) : null,
356
356
  seats,
357
357
  stripeSubscriptionId: subscriptionUpdated.id,
358
- billingInterval: subscriptionItem.price.recurring?.interval
358
+ billingInterval: subscriptionItem.price.recurring?.interval,
359
+ stripeScheduleId: subscriptionUpdated.schedule ? typeof subscriptionUpdated.schedule === "string" ? subscriptionUpdated.schedule : subscriptionUpdated.schedule.id : null
359
360
  },
360
361
  where: [{
361
362
  field: "id",
@@ -410,7 +411,8 @@ async function onSubscriptionDeleted(ctx, options, event) {
410
411
  cancelAtPeriodEnd: subscriptionDeleted.cancel_at_period_end,
411
412
  cancelAt: subscriptionDeleted.cancel_at ? /* @__PURE__ */ new Date(subscriptionDeleted.cancel_at * 1e3) : null,
412
413
  canceledAt: subscriptionDeleted.canceled_at ? /* @__PURE__ */ new Date(subscriptionDeleted.canceled_at * 1e3) : null,
413
- endedAt: subscriptionDeleted.ended_at ? /* @__PURE__ */ new Date(subscriptionDeleted.ended_at * 1e3) : null
414
+ endedAt: subscriptionDeleted.ended_at ? /* @__PURE__ */ new Date(subscriptionDeleted.ended_at * 1e3) : null,
415
+ stripeScheduleId: null
414
416
  }
415
417
  });
416
418
  await options.subscription.onSubscriptionDeleted?.({
@@ -514,6 +516,7 @@ const upgradeSubscriptionBodySchema = z.object({
514
516
  successUrl: z.string().meta({ description: "Callback URL to redirect back after successful subscription. Eg: \"https://example.com/success\"" }).default("/"),
515
517
  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("/"),
516
518
  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(),
519
+ scheduleAtPeriodEnd: z.boolean().meta({ description: "Schedule the plan change at the end of the current billing period instead of applying immediately." }).default(false),
517
520
  disableRedirect: z.boolean().meta({ description: "Disable redirect after successful subscription. Eg: true" }).default(false)
518
521
  });
519
522
  /**
@@ -721,26 +724,151 @@ const upgradeSubscription = (options) => {
721
724
  dbSubscription = activeOrTrialingSubscription;
722
725
  }
723
726
  if (!planItem) throw APIError$1.from("NOT_FOUND", STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND);
724
- const seatPortalItems = [];
725
- if (isAutoManagedSeats && plan.seatPriceId) {
726
- const oldPlan = activeOrTrialingSubscription ? await getPlanByName(options, activeOrTrialingSubscription.plan) : void 0;
727
- if (oldPlan?.seatPriceId && oldPlan.seatPriceId !== plan.seatPriceId) {
728
- const oldSeatItem = activeSubscription.items.data.find((item) => item.price.id === oldPlan.seatPriceId);
729
- if (oldSeatItem) seatPortalItems.push({
730
- id: oldSeatItem.id,
731
- price: plan.seatPriceId,
732
- quantity: memberCount
727
+ if (activeSubscription.schedule) {
728
+ const { data: existingSchedules } = await client.subscriptionSchedules.list({ customer: customerId });
729
+ const existingSchedule = existingSchedules.find((s) => (typeof s.subscription === "string" ? s.subscription : s.subscription?.id) === activeSubscription.id && s.status === "active");
730
+ if (existingSchedule && existingSchedule.metadata?.source === "@better-auth/stripe") {
731
+ await client.subscriptionSchedules.release(existingSchedule.id);
732
+ if (dbSubscription) await ctx.context.adapter.update({
733
+ model: "subscription",
734
+ update: {
735
+ stripeScheduleId: null,
736
+ updatedAt: /* @__PURE__ */ new Date()
737
+ },
738
+ where: [{
739
+ field: "id",
740
+ value: dbSubscription.id
741
+ }]
733
742
  });
734
743
  }
735
744
  }
745
+ const oldPlan = activeOrTrialingSubscription ? await getPlanByName(options, activeOrTrialingSubscription.plan) : void 0;
746
+ const priceMap = /* @__PURE__ */ new Map();
747
+ if (isAutoManagedSeats && plan.seatPriceId) {
748
+ if (oldPlan?.seatPriceId && oldPlan.seatPriceId !== plan.seatPriceId) priceMap.set(oldPlan.seatPriceId, {
749
+ newPrice: plan.seatPriceId,
750
+ quantity: memberCount
751
+ });
752
+ }
753
+ const lineItemDelta = /* @__PURE__ */ new Map();
754
+ for (const li of oldPlan?.lineItems ?? []) if (typeof li.price === "string") lineItemDelta.set(li.price, (lineItemDelta.get(li.price) ?? 0) - 1);
755
+ for (const li of plan.lineItems ?? []) if (typeof li.price === "string") lineItemDelta.set(li.price, (lineItemDelta.get(li.price) ?? 0) + 1);
756
+ for (const [price, delta] of lineItemDelta) if (delta === 0) lineItemDelta.delete(price);
736
757
  let upgradeUrl;
737
- if (seatPortalItems.length > 0) {
738
- const isSeatItem = seatPortalItems.some((s) => s.id === planItem.id);
758
+ if (ctx.body.scheduleAtPeriodEnd) {
759
+ const schedule = await client.subscriptionSchedules.create({ from_subscription: activeSubscription.id }).catch(async (e) => {
760
+ throw ctx.error("BAD_REQUEST", {
761
+ message: e.message,
762
+ code: e.code
763
+ });
764
+ });
765
+ const currentPhase = schedule.phases[0];
766
+ if (!currentPhase) throw ctx.error("BAD_REQUEST", { message: "Subscription schedule has no phases" });
767
+ const removeQuota = /* @__PURE__ */ new Map();
768
+ for (const [p, d] of lineItemDelta) if (d < 0) removeQuota.set(p, -d);
769
+ const newPhaseItems = [];
770
+ for (const item of currentPhase.items) {
771
+ const itemPriceId = typeof item.price === "string" ? item.price : item.price.id;
772
+ const quota = removeQuota.get(itemPriceId) ?? 0;
773
+ if (quota > 0) {
774
+ removeQuota.set(itemPriceId, quota - 1);
775
+ continue;
776
+ }
777
+ const replacement = priceMap.get(itemPriceId);
778
+ if (replacement) {
779
+ newPhaseItems.push({
780
+ price: replacement.newPrice,
781
+ quantity: replacement.quantity ?? item.quantity
782
+ });
783
+ continue;
784
+ }
785
+ if (itemPriceId === stripeSubscriptionPriceId) {
786
+ newPhaseItems.push({
787
+ price: priceIdToUse,
788
+ quantity: isAutoManagedSeats ? 1 : ctx.body.seats || 1
789
+ });
790
+ continue;
791
+ }
792
+ newPhaseItems.push({
793
+ price: itemPriceId,
794
+ quantity: item.quantity
795
+ });
796
+ const d = lineItemDelta.get(itemPriceId);
797
+ if (d !== void 0 && d > 0) if (d === 1) lineItemDelta.delete(itemPriceId);
798
+ else lineItemDelta.set(itemPriceId, d - 1);
799
+ }
800
+ for (const [price, delta] of lineItemDelta) for (let i = 0; i < delta; i++) newPhaseItems.push({ price });
801
+ await client.subscriptionSchedules.update(schedule.id, {
802
+ metadata: { source: "@better-auth/stripe" },
803
+ end_behavior: "release",
804
+ phases: [{
805
+ items: currentPhase.items.map((item) => ({
806
+ price: typeof item.price === "string" ? item.price : item.price.id,
807
+ quantity: item.quantity
808
+ })),
809
+ start_date: currentPhase.start_date,
810
+ end_date: currentPhase.end_date
811
+ }, {
812
+ items: newPhaseItems,
813
+ start_date: currentPhase.end_date,
814
+ proration_behavior: "none"
815
+ }]
816
+ }).catch(async (e) => {
817
+ throw ctx.error("BAD_REQUEST", {
818
+ message: e.message,
819
+ code: e.code
820
+ });
821
+ });
822
+ if (dbSubscription) await ctx.context.adapter.update({
823
+ model: "subscription",
824
+ update: {
825
+ stripeScheduleId: schedule.id,
826
+ updatedAt: /* @__PURE__ */ new Date()
827
+ },
828
+ where: [{
829
+ field: "id",
830
+ value: dbSubscription.id
831
+ }]
832
+ });
833
+ upgradeUrl = getUrl(ctx, ctx.body.returnUrl || "/");
834
+ } else if (priceMap.size > 0 || lineItemDelta.size > 0) {
835
+ const removeQuota = /* @__PURE__ */ new Map();
836
+ for (const [p, d] of lineItemDelta) if (d < 0) removeQuota.set(p, -d);
837
+ const itemUpdates = [];
838
+ for (const si of activeSubscription.items.data) {
839
+ const quota = removeQuota.get(si.price.id) ?? 0;
840
+ if (quota > 0) {
841
+ removeQuota.set(si.price.id, quota - 1);
842
+ itemUpdates.push({
843
+ id: si.id,
844
+ deleted: true
845
+ });
846
+ continue;
847
+ }
848
+ const replacement = priceMap.get(si.price.id);
849
+ if (replacement) {
850
+ itemUpdates.push({
851
+ id: si.id,
852
+ price: replacement.newPrice,
853
+ quantity: replacement.quantity
854
+ });
855
+ continue;
856
+ }
857
+ if (si.price.id === stripeSubscriptionPriceId) {
858
+ itemUpdates.push({
859
+ id: si.id,
860
+ price: priceIdToUse,
861
+ quantity: isAutoManagedSeats ? 1 : ctx.body.seats || 1
862
+ });
863
+ continue;
864
+ }
865
+ const d = lineItemDelta.get(si.price.id);
866
+ if (d !== void 0 && d > 0) if (d === 1) lineItemDelta.delete(si.price.id);
867
+ else lineItemDelta.set(si.price.id, d - 1);
868
+ }
869
+ for (const [price, delta] of lineItemDelta) for (let i = 0; i < delta; i++) itemUpdates.push({ price });
739
870
  await client.subscriptions.update(activeSubscription.id, {
740
- items: isSeatItem ? seatPortalItems : [{
741
- id: planItem.id,
742
- price: priceIdToUse
743
- }, ...seatPortalItems],
871
+ items: itemUpdates,
744
872
  proration_behavior: "create_prorations"
745
873
  }).catch(async (e) => {
746
874
  throw ctx.error("BAD_REQUEST", {
@@ -841,7 +969,7 @@ const upgradeSubscription = (options) => {
841
969
  }
842
970
  } : { customer_email: user.email },
843
971
  locale: ctx.body.locale,
844
- success_url: getUrl(ctx, `${ctx.context.baseURL}/subscription/success?callbackURL=${encodeURIComponent(ctx.body.successUrl)}&subscriptionId=${encodeURIComponent(subscription.id)}`),
972
+ success_url: getUrl(ctx, `${ctx.context.baseURL}/subscription/success?callbackURL=${encodeURIComponent(ctx.body.successUrl)}&checkoutSessionId={CHECKOUT_SESSION_ID}`),
845
973
  cancel_url: getUrl(ctx, ctx.body.cancelUrl),
846
974
  line_items: [
847
975
  ...!isSeatOnlyPlan ? [{
@@ -882,61 +1010,6 @@ const upgradeSubscription = (options) => {
882
1010
  });
883
1011
  });
884
1012
  };
885
- const cancelSubscriptionCallbackQuerySchema = z.record(z.string(), z.any()).optional();
886
- const cancelSubscriptionCallback = (options) => {
887
- const client = options.stripeClient;
888
- const subscriptionOptions = options.subscription;
889
- return createAuthEndpoint("/subscription/cancel/callback", {
890
- method: "GET",
891
- query: cancelSubscriptionCallbackQuerySchema,
892
- metadata: { openapi: { operationId: "cancelSubscriptionCallback" } },
893
- use: [originCheck((ctx) => ctx.query.callbackURL)]
894
- }, async (ctx) => {
895
- if (!ctx.query || !ctx.query.callbackURL || !ctx.query.subscriptionId) throw ctx.redirect(getUrl(ctx, ctx.query?.callbackURL || "/"));
896
- if (!await getSessionFromCtx(ctx)) throw ctx.redirect(getUrl(ctx, ctx.query?.callbackURL || "/"));
897
- const { callbackURL, subscriptionId } = ctx.query;
898
- try {
899
- const subscription = await ctx.context.adapter.findOne({
900
- model: "subscription",
901
- where: [{
902
- field: "id",
903
- value: subscriptionId
904
- }]
905
- });
906
- if (!subscription || subscription.status === "canceled" || isPendingCancel(subscription)) throw ctx.redirect(getUrl(ctx, callbackURL));
907
- const customerId = subscription.stripeCustomerId;
908
- if (!customerId) throw ctx.redirect(getUrl(ctx, callbackURL));
909
- const currentSubscription = (await client.subscriptions.list({
910
- customer: customerId,
911
- status: "active"
912
- })).data.find((sub) => sub.id === subscription.stripeSubscriptionId);
913
- if (currentSubscription && isStripePendingCancel(currentSubscription) && !isPendingCancel(subscription)) {
914
- await ctx.context.adapter.update({
915
- model: "subscription",
916
- update: {
917
- status: currentSubscription?.status,
918
- cancelAtPeriodEnd: currentSubscription?.cancel_at_period_end || false,
919
- cancelAt: currentSubscription?.cancel_at ? /* @__PURE__ */ new Date(currentSubscription.cancel_at * 1e3) : null,
920
- canceledAt: currentSubscription?.canceled_at ? /* @__PURE__ */ new Date(currentSubscription.canceled_at * 1e3) : null
921
- },
922
- where: [{
923
- field: "id",
924
- value: subscription.id
925
- }]
926
- });
927
- await subscriptionOptions.onSubscriptionCancel?.({
928
- subscription,
929
- cancellationDetails: currentSubscription.cancellation_details,
930
- stripeSubscription: currentSubscription,
931
- event: void 0
932
- });
933
- }
934
- } catch (error) {
935
- ctx.context.logger.error("Error checking subscription status from Stripe", error);
936
- }
937
- throw ctx.redirect(getUrl(ctx, callbackURL));
938
- });
939
- };
940
1013
  const cancelSubscriptionBodySchema = z.object({
941
1014
  referenceId: z.string().meta({ description: "Reference id of the subscription to cancel. Eg: '123'" }).optional(),
942
1015
  subscriptionId: z.string().meta({ description: "The Stripe subscription ID to cancel. Eg: 'sub_1ABC2DEF3GHI4JKL'" }).optional(),
@@ -1008,7 +1081,7 @@ const cancelSubscription = (options) => {
1008
1081
  if (!activeSubscription) throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND);
1009
1082
  const { url } = await client.billingPortal.sessions.create({
1010
1083
  customer: subscription.stripeCustomerId,
1011
- return_url: getUrl(ctx, `${ctx.context.baseURL}/subscription/cancel/callback?callbackURL=${encodeURIComponent(ctx.body?.returnUrl || "/")}&subscriptionId=${encodeURIComponent(subscription.id)}`),
1084
+ return_url: getUrl(ctx, ctx.body?.returnUrl || "/"),
1012
1085
  flow_data: {
1013
1086
  type: "subscription_cancel",
1014
1087
  subscription_cancel: { subscription: activeSubscription.id }
@@ -1078,7 +1151,36 @@ const restoreSubscription = (options) => {
1078
1151
  if (ctx.body.subscriptionId && subscription && subscription.referenceId !== referenceId) subscription = void 0;
1079
1152
  if (!subscription || !subscription.stripeCustomerId) throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND);
1080
1153
  if (!isActiveOrTrialing(subscription)) throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_ACTIVE);
1081
- if (!isPendingCancel(subscription)) throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION);
1154
+ const hasPendingCancel = isPendingCancel(subscription);
1155
+ const { stripeScheduleId } = subscription;
1156
+ if (!hasPendingCancel && !stripeScheduleId) throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_PENDING_CHANGE);
1157
+ if (stripeScheduleId) {
1158
+ if (!subscription.stripeSubscriptionId) throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND);
1159
+ if ((await client.subscriptionSchedules.retrieve(stripeScheduleId).catch((e) => {
1160
+ throw ctx.error("BAD_REQUEST", {
1161
+ message: e.message,
1162
+ code: e.code
1163
+ });
1164
+ })).status === "active") await client.subscriptionSchedules.release(stripeScheduleId).catch((e) => {
1165
+ throw ctx.error("BAD_REQUEST", {
1166
+ message: e.message,
1167
+ code: e.code
1168
+ });
1169
+ });
1170
+ await ctx.context.adapter.update({
1171
+ model: "subscription",
1172
+ update: {
1173
+ stripeScheduleId: null,
1174
+ updatedAt: /* @__PURE__ */ new Date()
1175
+ },
1176
+ where: [{
1177
+ field: "id",
1178
+ value: subscription.id
1179
+ }]
1180
+ });
1181
+ const releasedSub = await client.subscriptions.retrieve(subscription.stripeSubscriptionId);
1182
+ return ctx.json(releasedSub);
1183
+ }
1082
1184
  const activeSubscription = await client.subscriptions.list({ customer: subscription.stripeCustomerId }).then((res) => res.data.filter((sub) => isActiveOrTrialing(sub))[0]);
1083
1185
  if (!activeSubscription) throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND);
1084
1186
  const updateParams = {};
@@ -1165,10 +1267,20 @@ const subscriptionSuccess = (options) => {
1165
1267
  metadata: { openapi: { operationId: "handleSubscriptionSuccess" } },
1166
1268
  use: [originCheck((ctx) => ctx.query.callbackURL)]
1167
1269
  }, async (ctx) => {
1168
- if (!ctx.query || !ctx.query.callbackURL || !ctx.query.subscriptionId) throw ctx.redirect(getUrl(ctx, ctx.query?.callbackURL || "/"));
1169
- const { callbackURL, subscriptionId } = ctx.query;
1270
+ const callbackURL = ctx.query?.callbackURL || "/";
1170
1271
  const session = await getSessionFromCtx(ctx);
1171
- if (!session) throw ctx.redirect(getUrl(ctx, ctx.query?.callbackURL || "/"));
1272
+ if (!session) throw ctx.redirect(getUrl(ctx, callbackURL));
1273
+ if (!ctx.query?.checkoutSessionId) throw ctx.redirect(getUrl(ctx, callbackURL));
1274
+ const checkoutSession = await client.checkout.sessions.retrieve(ctx.query.checkoutSessionId).catch((error) => {
1275
+ ctx.context.logger.error("Error retrieving checkout session from Stripe", error);
1276
+ return null;
1277
+ });
1278
+ if (!checkoutSession) throw ctx.redirect(getUrl(ctx, callbackURL));
1279
+ const { subscriptionId } = subscriptionMetadata.get(checkoutSession.metadata);
1280
+ if (!subscriptionId) {
1281
+ ctx.context.logger.warn(`No subscriptionId in checkout session metadata: ${checkoutSession.id}`);
1282
+ throw ctx.redirect(getUrl(ctx, callbackURL));
1283
+ }
1172
1284
  const subscription = await ctx.context.adapter.findOne({
1173
1285
  model: "subscription",
1174
1286
  where: [{
@@ -1415,6 +1527,10 @@ const subscriptions = { subscription: { fields: {
1415
1527
  billingInterval: {
1416
1528
  type: "string",
1417
1529
  required: false
1530
+ },
1531
+ stripeScheduleId: {
1532
+ type: "string",
1533
+ required: false
1418
1534
  }
1419
1535
  } } };
1420
1536
  const user = { user: { fields: { stripeCustomerId: {
@@ -1449,7 +1565,6 @@ const stripe = (options) => {
1449
1565
  const client = options.stripeClient;
1450
1566
  const subscriptionEndpoints = {
1451
1567
  upgradeSubscription: upgradeSubscription(options),
1452
- cancelSubscriptionCallback: cancelSubscriptionCallback(options),
1453
1568
  cancelSubscription: cancelSubscription(options),
1454
1569
  restoreSubscription: restoreSubscription(options),
1455
1570
  listActiveSubscriptions: listActiveSubscriptions(options),