@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.
- package/.turbo/turbo-build.log +11 -11
- package/dist/client.d.mts +3 -1
- package/dist/client.mjs +1 -1
- package/dist/{error-codes-CHMyMR5v.mjs → error-codes-CCosYkXx.mjs} +2 -1
- package/dist/{error-codes-CHMyMR5v.mjs.map → error-codes-CCosYkXx.mjs.map} +1 -1
- package/dist/index.d.mts +8 -11
- package/dist/index.mjs +195 -80
- package/dist/index.mjs.map +1 -1
- package/dist/{types-Bzc6yg83.d.mts → types-OT6L84x4.d.mts} +10 -1
- package/package.json +5 -5
- package/src/error-codes.ts +5 -0
- package/src/hooks.ts +6 -0
- package/src/index.ts +0 -2
- package/src/routes.ts +350 -157
- package/src/schema.ts +4 -0
- package/src/types.ts +5 -0
- package/test/seat-based-billing.test.ts +6 -0
- package/test/stripe-organization.test.ts +1 -130
- package/test/stripe.test.ts +1768 -52
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @better-auth/stripe@1.5.0-beta.
|
|
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
|
[34mℹ[39m tsdown [2mv0.20.3[22m powered by rolldown [2mv1.0.0-rc.3[22m
|
|
@@ -7,16 +7,16 @@
|
|
|
7
7
|
[34mℹ[39m entry: [34msrc/index.ts, src/client.ts[39m
|
|
8
8
|
[34mℹ[39m tsconfig: [34mtsconfig.json[39m
|
|
9
9
|
[34mℹ[39m Build start
|
|
10
|
-
[34mℹ[39m [2mdist/[22m[1mindex.mjs[22m [2m
|
|
11
|
-
[33m[PLUGIN_TIMINGS] Warning:[0m Your build spent significant time in plugin `rolldown-plugin-dts:generate`. See https://rolldown.rs/options/checks#plugintimings for more details.
|
|
10
|
+
[34mℹ[39m [2mdist/[22m[1mindex.mjs[22m [2m 69.57 kB[22m [2m│ gzip: 13.19 kB[22m
|
|
12
11
|
[34mℹ[39m [2mdist/[22m[1mclient.mjs[22m [2m 0.43 kB[22m [2m│ gzip: 0.29 kB[22m
|
|
13
|
-
[34mℹ[39m [2mdist/[22mindex.mjs.map [
|
|
14
|
-
[34mℹ[39m [2mdist/[22merror-codes-
|
|
15
|
-
[34mℹ[39m [2mdist/[22merror-codes-
|
|
12
|
+
[34mℹ[39m [2mdist/[22mindex.mjs.map [2m150.44 kB[22m [2m│ gzip: 29.40 kB[22m
|
|
13
|
+
[34mℹ[39m [2mdist/[22merror-codes-CCosYkXx.mjs.map [2m 2.43 kB[22m [2m│ gzip: 1.08 kB[22m
|
|
14
|
+
[34mℹ[39m [2mdist/[22merror-codes-CCosYkXx.mjs [2m 1.89 kB[22m [2m│ gzip: 0.85 kB[22m
|
|
16
15
|
[34mℹ[39m [2mdist/[22mclient.mjs.map [2m 1.26 kB[22m [2m│ gzip: 0.59 kB[22m
|
|
17
|
-
[34mℹ[39m [2mdist/[22m[32m[1mindex.d.mts[22m[39m [2m 19.
|
|
18
|
-
[34mℹ[39m [2mdist/[22m[32m[1mclient.d.mts[22m[39m [2m 4.
|
|
19
|
-
[34mℹ[39m [2mdist/[22m[32mtypes-
|
|
20
|
-
[34mℹ[39m 9 files, total:
|
|
21
|
-
[
|
|
16
|
+
[34mℹ[39m [2mdist/[22m[32m[1mindex.d.mts[22m[39m [2m 19.78 kB[22m [2m│ gzip: 3.14 kB[22m
|
|
17
|
+
[34mℹ[39m [2mdist/[22m[32m[1mclient.d.mts[22m[39m [2m 4.80 kB[22m [2m│ gzip: 0.93 kB[22m
|
|
18
|
+
[34mℹ[39m [2mdist/[22m[32mtypes-OT6L84x4.d.mts[39m [2m 12.92 kB[22m [2m│ gzip: 2.96 kB[22m
|
|
19
|
+
[34mℹ[39m 9 files, total: 263.52 kB
|
|
20
|
+
[33m[PLUGIN_TIMINGS] Warning:[0m Your build spent significant time in plugin `rolldown-plugin-dts:generate`. See https://rolldown.rs/options/checks#plugintimings for more details.
|
|
22
21
|
|
|
22
|
+
[32m✔[39m Build complete in [32m43653ms[39m
|
package/dist/client.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as StripePlan } from "./types-
|
|
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
|
@@ -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-
|
|
32
|
+
//# sourceMappingURL=error-codes-CCosYkXx.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error-codes-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
725
|
-
|
|
726
|
-
const
|
|
727
|
-
if (
|
|
728
|
-
|
|
729
|
-
if (
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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 (
|
|
738
|
-
const
|
|
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:
|
|
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)}&
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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),
|