@better-auth/stripe 1.4.10-beta.1 → 1.5.0-beta.2
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 +7 -7
- package/dist/client.d.mts +2 -1
- package/dist/client.mjs +4 -1
- package/dist/{index-DpiQGYLJ.d.mts → index-SbT5j9k6.d.mts} +45 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +201 -59
- package/package.json +5 -5
- package/src/client.ts +1 -0
- package/src/hooks.ts +166 -17
- package/src/middleware.ts +1 -2
- package/src/routes.ts +126 -84
- package/src/schema.ts +12 -0
- package/src/stripe.test.ts +2434 -1291
- package/src/types.ts +30 -1
- package/src/utils.ts +25 -1
package/src/hooks.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import type { GenericEndpointContext } from "better-auth";
|
|
2
|
-
import {
|
|
1
|
+
import type { GenericEndpointContext } from "@better-auth/core";
|
|
2
|
+
import type { User } from "@better-auth/core/db";
|
|
3
3
|
import type Stripe from "stripe";
|
|
4
4
|
import type { InputSubscription, StripeOptions, Subscription } from "./types";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
getPlanByPriceInfo,
|
|
7
|
+
isActiveOrTrialing,
|
|
8
|
+
isPendingCancel,
|
|
9
|
+
isStripePendingCancel,
|
|
10
|
+
} from "./utils";
|
|
6
11
|
|
|
7
12
|
export async function onCheckoutSessionCompleted(
|
|
8
13
|
ctx: GenericEndpointContext,
|
|
@@ -54,7 +59,17 @@ export async function onCheckoutSessionCompleted(
|
|
|
54
59
|
subscription.items.data[0]!.current_period_end * 1000,
|
|
55
60
|
),
|
|
56
61
|
stripeSubscriptionId: checkoutSession.subscription as string,
|
|
57
|
-
|
|
62
|
+
cancelAtPeriodEnd: subscription.cancel_at_period_end,
|
|
63
|
+
cancelAt: subscription.cancel_at
|
|
64
|
+
? new Date(subscription.cancel_at * 1000)
|
|
65
|
+
: null,
|
|
66
|
+
canceledAt: subscription.canceled_at
|
|
67
|
+
? new Date(subscription.canceled_at * 1000)
|
|
68
|
+
: null,
|
|
69
|
+
endedAt: subscription.ended_at
|
|
70
|
+
? new Date(subscription.ended_at * 1000)
|
|
71
|
+
: null,
|
|
72
|
+
seats: seats,
|
|
58
73
|
...trial,
|
|
59
74
|
},
|
|
60
75
|
where: [
|
|
@@ -93,7 +108,123 @@ export async function onCheckoutSessionCompleted(
|
|
|
93
108
|
}
|
|
94
109
|
}
|
|
95
110
|
} catch (e: any) {
|
|
96
|
-
logger.error(`Stripe webhook failed. Error: ${e.message}`);
|
|
111
|
+
ctx.context.logger.error(`Stripe webhook failed. Error: ${e.message}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function onSubscriptionCreated(
|
|
116
|
+
ctx: GenericEndpointContext,
|
|
117
|
+
options: StripeOptions,
|
|
118
|
+
event: Stripe.Event,
|
|
119
|
+
) {
|
|
120
|
+
try {
|
|
121
|
+
if (!options.subscription?.enabled) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const subscriptionCreated = event.data.object as Stripe.Subscription;
|
|
126
|
+
const stripeCustomerId = subscriptionCreated.customer?.toString();
|
|
127
|
+
if (!stripeCustomerId) {
|
|
128
|
+
ctx.context.logger.warn(
|
|
129
|
+
`Stripe webhook warning: customer.subscription.created event received without customer ID`,
|
|
130
|
+
);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Check if subscription already exists in database
|
|
135
|
+
const existingSubscription =
|
|
136
|
+
await ctx.context.adapter.findOne<Subscription>({
|
|
137
|
+
model: "subscription",
|
|
138
|
+
where: [
|
|
139
|
+
{
|
|
140
|
+
field: "stripeSubscriptionId",
|
|
141
|
+
value: subscriptionCreated.id,
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
});
|
|
145
|
+
if (existingSubscription) {
|
|
146
|
+
ctx.context.logger.info(
|
|
147
|
+
`Stripe webhook: Subscription ${subscriptionCreated.id} already exists in database, skipping creation`,
|
|
148
|
+
);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Find user by stripeCustomerId
|
|
153
|
+
const user = await ctx.context.adapter.findOne<User>({
|
|
154
|
+
model: "user",
|
|
155
|
+
where: [
|
|
156
|
+
{
|
|
157
|
+
field: "stripeCustomerId",
|
|
158
|
+
value: stripeCustomerId,
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
});
|
|
162
|
+
if (!user) {
|
|
163
|
+
ctx.context.logger.warn(
|
|
164
|
+
`Stripe webhook warning: No user found with stripeCustomerId: ${stripeCustomerId}`,
|
|
165
|
+
);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const subscriptionItem = subscriptionCreated.items.data[0];
|
|
170
|
+
if (!subscriptionItem) {
|
|
171
|
+
ctx.context.logger.warn(
|
|
172
|
+
`Stripe webhook warning: Subscription ${subscriptionCreated.id} has no items`,
|
|
173
|
+
);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const priceId = subscriptionItem.price.id;
|
|
178
|
+
const priceLookupKey = subscriptionItem.price.lookup_key || null;
|
|
179
|
+
const plan = await getPlanByPriceInfo(options, priceId, priceLookupKey);
|
|
180
|
+
if (!plan) {
|
|
181
|
+
ctx.context.logger.warn(
|
|
182
|
+
`Stripe webhook warning: No matching plan found for priceId: ${priceId}`,
|
|
183
|
+
);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const seats = subscriptionItem.quantity;
|
|
188
|
+
const periodStart = new Date(subscriptionItem.current_period_start * 1000);
|
|
189
|
+
const periodEnd = new Date(subscriptionItem.current_period_end * 1000);
|
|
190
|
+
|
|
191
|
+
const trial =
|
|
192
|
+
subscriptionCreated.trial_start && subscriptionCreated.trial_end
|
|
193
|
+
? {
|
|
194
|
+
trialStart: new Date(subscriptionCreated.trial_start * 1000),
|
|
195
|
+
trialEnd: new Date(subscriptionCreated.trial_end * 1000),
|
|
196
|
+
}
|
|
197
|
+
: {};
|
|
198
|
+
|
|
199
|
+
// Create the subscription in the database
|
|
200
|
+
const newSubscription = await ctx.context.adapter.create<Subscription>({
|
|
201
|
+
model: "subscription",
|
|
202
|
+
data: {
|
|
203
|
+
referenceId: user.id,
|
|
204
|
+
stripeCustomerId: stripeCustomerId,
|
|
205
|
+
stripeSubscriptionId: subscriptionCreated.id,
|
|
206
|
+
status: subscriptionCreated.status,
|
|
207
|
+
plan: plan.name.toLowerCase(),
|
|
208
|
+
periodStart,
|
|
209
|
+
periodEnd,
|
|
210
|
+
seats,
|
|
211
|
+
...(plan.limits ? { limits: plan.limits } : {}),
|
|
212
|
+
...trial,
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
ctx.context.logger.info(
|
|
217
|
+
`Stripe webhook: Created subscription ${subscriptionCreated.id} for user ${user.id} from dashboard`,
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
await options.subscription?.onSubscriptionCreated?.({
|
|
221
|
+
event,
|
|
222
|
+
subscription: newSubscription,
|
|
223
|
+
stripeSubscription: subscriptionCreated,
|
|
224
|
+
plan,
|
|
225
|
+
});
|
|
226
|
+
} catch (error: any) {
|
|
227
|
+
ctx.context.logger.error(`Stripe webhook failed. Error: ${error}`);
|
|
97
228
|
}
|
|
98
229
|
}
|
|
99
230
|
|
|
@@ -126,12 +257,11 @@ export async function onSubscriptionUpdated(
|
|
|
126
257
|
where: [{ field: "stripeCustomerId", value: customerId }],
|
|
127
258
|
});
|
|
128
259
|
if (subs.length > 1) {
|
|
129
|
-
const activeSub = subs.find(
|
|
130
|
-
(sub
|
|
131
|
-
sub.status === "active" || sub.status === "trialing",
|
|
260
|
+
const activeSub = subs.find((sub: Subscription) =>
|
|
261
|
+
isActiveOrTrialing(sub),
|
|
132
262
|
);
|
|
133
263
|
if (!activeSub) {
|
|
134
|
-
logger.warn(
|
|
264
|
+
ctx.context.logger.warn(
|
|
135
265
|
`Stripe webhook error: Multiple subscriptions found for customerId: ${customerId} and no active subscription is found`,
|
|
136
266
|
);
|
|
137
267
|
return;
|
|
@@ -161,7 +291,16 @@ export async function onSubscriptionUpdated(
|
|
|
161
291
|
subscriptionUpdated.items.data[0]!.current_period_end * 1000,
|
|
162
292
|
),
|
|
163
293
|
cancelAtPeriodEnd: subscriptionUpdated.cancel_at_period_end,
|
|
164
|
-
|
|
294
|
+
cancelAt: subscriptionUpdated.cancel_at
|
|
295
|
+
? new Date(subscriptionUpdated.cancel_at * 1000)
|
|
296
|
+
: null,
|
|
297
|
+
canceledAt: subscriptionUpdated.canceled_at
|
|
298
|
+
? new Date(subscriptionUpdated.canceled_at * 1000)
|
|
299
|
+
: null,
|
|
300
|
+
endedAt: subscriptionUpdated.ended_at
|
|
301
|
+
? new Date(subscriptionUpdated.ended_at * 1000)
|
|
302
|
+
: null,
|
|
303
|
+
seats: seats,
|
|
165
304
|
stripeSubscriptionId: subscriptionUpdated.id,
|
|
166
305
|
},
|
|
167
306
|
where: [
|
|
@@ -171,11 +310,11 @@ export async function onSubscriptionUpdated(
|
|
|
171
310
|
},
|
|
172
311
|
],
|
|
173
312
|
});
|
|
174
|
-
const
|
|
313
|
+
const isNewCancellation =
|
|
175
314
|
subscriptionUpdated.status === "active" &&
|
|
176
|
-
subscriptionUpdated
|
|
177
|
-
!subscription
|
|
178
|
-
if (
|
|
315
|
+
isStripePendingCancel(subscriptionUpdated) &&
|
|
316
|
+
!isPendingCancel(subscription);
|
|
317
|
+
if (isNewCancellation) {
|
|
179
318
|
await options.subscription.onSubscriptionCancel?.({
|
|
180
319
|
subscription,
|
|
181
320
|
cancellationDetails:
|
|
@@ -205,7 +344,7 @@ export async function onSubscriptionUpdated(
|
|
|
205
344
|
}
|
|
206
345
|
}
|
|
207
346
|
} catch (error: any) {
|
|
208
|
-
logger.error(`Stripe webhook failed. Error: ${error}`);
|
|
347
|
+
ctx.context.logger.error(`Stripe webhook failed. Error: ${error}`);
|
|
209
348
|
}
|
|
210
349
|
}
|
|
211
350
|
|
|
@@ -241,6 +380,16 @@ export async function onSubscriptionDeleted(
|
|
|
241
380
|
update: {
|
|
242
381
|
status: "canceled",
|
|
243
382
|
updatedAt: new Date(),
|
|
383
|
+
cancelAtPeriodEnd: subscriptionDeleted.cancel_at_period_end,
|
|
384
|
+
cancelAt: subscriptionDeleted.cancel_at
|
|
385
|
+
? new Date(subscriptionDeleted.cancel_at * 1000)
|
|
386
|
+
: null,
|
|
387
|
+
canceledAt: subscriptionDeleted.canceled_at
|
|
388
|
+
? new Date(subscriptionDeleted.canceled_at * 1000)
|
|
389
|
+
: null,
|
|
390
|
+
endedAt: subscriptionDeleted.ended_at
|
|
391
|
+
? new Date(subscriptionDeleted.ended_at * 1000)
|
|
392
|
+
: null,
|
|
244
393
|
},
|
|
245
394
|
});
|
|
246
395
|
await options.subscription.onSubscriptionDeleted?.({
|
|
@@ -249,11 +398,11 @@ export async function onSubscriptionDeleted(
|
|
|
249
398
|
subscription,
|
|
250
399
|
});
|
|
251
400
|
} else {
|
|
252
|
-
logger.warn(
|
|
401
|
+
ctx.context.logger.warn(
|
|
253
402
|
`Stripe webhook error: Subscription not found for subscriptionId: ${subscriptionId}`,
|
|
254
403
|
);
|
|
255
404
|
}
|
|
256
405
|
} catch (error: any) {
|
|
257
|
-
logger.error(`Stripe webhook failed. Error: ${error}`);
|
|
406
|
+
ctx.context.logger.error(`Stripe webhook failed. Error: ${error}`);
|
|
258
407
|
}
|
|
259
408
|
}
|
package/src/middleware.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { createAuthMiddleware } from "@better-auth/core/api";
|
|
2
|
-
import { logger } from "better-auth";
|
|
3
2
|
import { APIError } from "better-auth/api";
|
|
4
3
|
import type { SubscriptionOptions } from "./types";
|
|
5
4
|
|
|
@@ -24,7 +23,7 @@ export const referenceMiddleware = (
|
|
|
24
23
|
referenceId !== session.user.id &&
|
|
25
24
|
!subscriptionOptions.authorizeReference
|
|
26
25
|
) {
|
|
27
|
-
logger.error(
|
|
26
|
+
ctx.context.logger.error(
|
|
28
27
|
`Passing referenceId into a subscription action isn't allowed if subscription.authorizeReference isn't defined in your stripe plugin config.`,
|
|
29
28
|
);
|
|
30
29
|
throw new APIError("BAD_REQUEST", {
|
package/src/routes.ts
CHANGED
|
@@ -13,6 +13,7 @@ import * as z from "zod/v4";
|
|
|
13
13
|
import { STRIPE_ERROR_CODES } from "./error-codes";
|
|
14
14
|
import {
|
|
15
15
|
onCheckoutSessionCompleted,
|
|
16
|
+
onSubscriptionCreated,
|
|
16
17
|
onSubscriptionDeleted,
|
|
17
18
|
onSubscriptionUpdated,
|
|
18
19
|
} from "./hooks";
|
|
@@ -23,7 +24,14 @@ import type {
|
|
|
23
24
|
Subscription,
|
|
24
25
|
SubscriptionOptions,
|
|
25
26
|
} from "./types";
|
|
26
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
getPlanByName,
|
|
29
|
+
getPlanByPriceInfo,
|
|
30
|
+
getPlans,
|
|
31
|
+
isActiveOrTrialing,
|
|
32
|
+
isPendingCancel,
|
|
33
|
+
isStripePendingCancel,
|
|
34
|
+
} from "./utils";
|
|
27
35
|
|
|
28
36
|
const upgradeSubscriptionBodySchema = z.object({
|
|
29
37
|
/**
|
|
@@ -263,19 +271,15 @@ export const upgradeSubscription = (options: StripeOptions) => {
|
|
|
263
271
|
],
|
|
264
272
|
});
|
|
265
273
|
|
|
266
|
-
const activeOrTrialingSubscription = subscriptions.find(
|
|
267
|
-
(sub)
|
|
274
|
+
const activeOrTrialingSubscription = subscriptions.find((sub) =>
|
|
275
|
+
isActiveOrTrialing(sub),
|
|
268
276
|
);
|
|
269
277
|
|
|
270
278
|
const activeSubscriptions = await client.subscriptions
|
|
271
279
|
.list({
|
|
272
280
|
customer: customerId,
|
|
273
281
|
})
|
|
274
|
-
.then((res) =>
|
|
275
|
-
res.data.filter(
|
|
276
|
-
(sub) => sub.status === "active" || sub.status === "trialing",
|
|
277
|
-
),
|
|
278
|
-
);
|
|
282
|
+
.then((res) => res.data.filter((sub) => isActiveOrTrialing(sub)));
|
|
279
283
|
|
|
280
284
|
const activeSubscription = activeSubscriptions.find((sub) => {
|
|
281
285
|
// If we have a specific subscription to update, match by ID
|
|
@@ -400,7 +404,7 @@ export const upgradeSubscription = (options: StripeOptions) => {
|
|
|
400
404
|
});
|
|
401
405
|
return ctx.json({
|
|
402
406
|
url,
|
|
403
|
-
redirect:
|
|
407
|
+
redirect: !ctx.body.disableRedirect,
|
|
404
408
|
});
|
|
405
409
|
}
|
|
406
410
|
|
|
@@ -457,7 +461,13 @@ export const upgradeSubscription = (options: StripeOptions) => {
|
|
|
457
461
|
ctx,
|
|
458
462
|
);
|
|
459
463
|
|
|
460
|
-
const
|
|
464
|
+
const allSubscriptions = await ctx.context.adapter.findMany<Subscription>(
|
|
465
|
+
{
|
|
466
|
+
model: "subscription",
|
|
467
|
+
where: [{ field: "referenceId", value: referenceId }],
|
|
468
|
+
},
|
|
469
|
+
);
|
|
470
|
+
const hasEverTrialed = allSubscriptions.some((s) => {
|
|
461
471
|
// Check if user has ever had a trial for any plan (not just the same plan)
|
|
462
472
|
// This prevents users from getting multiple trials by switching plans
|
|
463
473
|
const hadTrial =
|
|
@@ -591,8 +601,8 @@ export const cancelSubscriptionCallback = (options: StripeOptions) => {
|
|
|
591
601
|
});
|
|
592
602
|
if (
|
|
593
603
|
!subscription ||
|
|
594
|
-
subscription.
|
|
595
|
-
subscription
|
|
604
|
+
subscription.status === "canceled" ||
|
|
605
|
+
isPendingCancel(subscription)
|
|
596
606
|
) {
|
|
597
607
|
throw ctx.redirect(getUrl(ctx, callbackURL));
|
|
598
608
|
}
|
|
@@ -604,12 +614,24 @@ export const cancelSubscriptionCallback = (options: StripeOptions) => {
|
|
|
604
614
|
const currentSubscription = stripeSubscription.data.find(
|
|
605
615
|
(sub) => sub.id === subscription.stripeSubscriptionId,
|
|
606
616
|
);
|
|
607
|
-
|
|
617
|
+
|
|
618
|
+
const isNewCancellation =
|
|
619
|
+
currentSubscription &&
|
|
620
|
+
isStripePendingCancel(currentSubscription) &&
|
|
621
|
+
!isPendingCancel(subscription);
|
|
622
|
+
if (isNewCancellation) {
|
|
608
623
|
await ctx.context.adapter.update({
|
|
609
624
|
model: "subscription",
|
|
610
625
|
update: {
|
|
611
626
|
status: currentSubscription?.status,
|
|
612
|
-
cancelAtPeriodEnd:
|
|
627
|
+
cancelAtPeriodEnd:
|
|
628
|
+
currentSubscription?.cancel_at_period_end || false,
|
|
629
|
+
cancelAt: currentSubscription?.cancel_at
|
|
630
|
+
? new Date(currentSubscription.cancel_at * 1000)
|
|
631
|
+
: null,
|
|
632
|
+
canceledAt: currentSubscription?.canceled_at
|
|
633
|
+
? new Date(currentSubscription.canceled_at * 1000)
|
|
634
|
+
: null,
|
|
613
635
|
},
|
|
614
636
|
where: [
|
|
615
637
|
{
|
|
@@ -655,6 +677,16 @@ const cancelSubscriptionBodySchema = z.object({
|
|
|
655
677
|
description:
|
|
656
678
|
'URL to take customers to when they click on the billing portal\'s link to return to your website. Eg: "/account"',
|
|
657
679
|
}),
|
|
680
|
+
/**
|
|
681
|
+
* Disable Redirect
|
|
682
|
+
*/
|
|
683
|
+
disableRedirect: z
|
|
684
|
+
.boolean()
|
|
685
|
+
.meta({
|
|
686
|
+
description:
|
|
687
|
+
"Disable redirect after successful subscription cancellation. Eg: true",
|
|
688
|
+
})
|
|
689
|
+
.default(false),
|
|
658
690
|
});
|
|
659
691
|
|
|
660
692
|
/**
|
|
@@ -708,13 +740,7 @@ export const cancelSubscription = (options: StripeOptions) => {
|
|
|
708
740
|
model: "subscription",
|
|
709
741
|
where: [{ field: "referenceId", value: referenceId }],
|
|
710
742
|
})
|
|
711
|
-
.then((subs) =>
|
|
712
|
-
subs.find(
|
|
713
|
-
(sub) => sub.status === "active" || sub.status === "trialing",
|
|
714
|
-
),
|
|
715
|
-
);
|
|
716
|
-
|
|
717
|
-
// Ensure the specified subscription belongs to the (validated) referenceId.
|
|
743
|
+
.then((subs) => subs.find((sub) => isActiveOrTrialing(sub)));
|
|
718
744
|
if (
|
|
719
745
|
ctx.body.subscriptionId &&
|
|
720
746
|
subscription &&
|
|
@@ -733,11 +759,7 @@ export const cancelSubscription = (options: StripeOptions) => {
|
|
|
733
759
|
.list({
|
|
734
760
|
customer: subscription.stripeCustomerId,
|
|
735
761
|
})
|
|
736
|
-
.then((res) =>
|
|
737
|
-
res.data.filter(
|
|
738
|
-
(sub) => sub.status === "active" || sub.status === "trialing",
|
|
739
|
-
),
|
|
740
|
-
);
|
|
762
|
+
.then((res) => res.data.filter((sub) => isActiveOrTrialing(sub)));
|
|
741
763
|
if (!activeSubscriptions.length) {
|
|
742
764
|
/**
|
|
743
765
|
* If the subscription is not found, we need to delete the subscription
|
|
@@ -785,21 +807,30 @@ export const cancelSubscription = (options: StripeOptions) => {
|
|
|
785
807
|
},
|
|
786
808
|
})
|
|
787
809
|
.catch(async (e) => {
|
|
788
|
-
if (e.message
|
|
810
|
+
if (e.message?.includes("already set to be canceled")) {
|
|
789
811
|
/**
|
|
790
|
-
* in-case we missed the event from stripe, we
|
|
812
|
+
* in-case we missed the event from stripe, we sync the actual state
|
|
791
813
|
* this is a rare case and should not happen
|
|
792
814
|
*/
|
|
793
|
-
if (!subscription
|
|
794
|
-
await
|
|
815
|
+
if (!isPendingCancel(subscription)) {
|
|
816
|
+
const stripeSub = await client.subscriptions.retrieve(
|
|
817
|
+
activeSubscription.id,
|
|
818
|
+
);
|
|
819
|
+
await ctx.context.adapter.update({
|
|
795
820
|
model: "subscription",
|
|
796
821
|
update: {
|
|
797
|
-
cancelAtPeriodEnd:
|
|
822
|
+
cancelAtPeriodEnd: stripeSub.cancel_at_period_end,
|
|
823
|
+
cancelAt: stripeSub.cancel_at
|
|
824
|
+
? new Date(stripeSub.cancel_at * 1000)
|
|
825
|
+
: null,
|
|
826
|
+
canceledAt: stripeSub.canceled_at
|
|
827
|
+
? new Date(stripeSub.canceled_at * 1000)
|
|
828
|
+
: null,
|
|
798
829
|
},
|
|
799
830
|
where: [
|
|
800
831
|
{
|
|
801
|
-
field: "
|
|
802
|
-
value:
|
|
832
|
+
field: "id",
|
|
833
|
+
value: subscription.id,
|
|
803
834
|
},
|
|
804
835
|
],
|
|
805
836
|
});
|
|
@@ -810,10 +841,10 @@ export const cancelSubscription = (options: StripeOptions) => {
|
|
|
810
841
|
code: e.code,
|
|
811
842
|
});
|
|
812
843
|
});
|
|
813
|
-
return {
|
|
844
|
+
return ctx.json({
|
|
814
845
|
url,
|
|
815
|
-
redirect:
|
|
816
|
-
};
|
|
846
|
+
redirect: !ctx.body.disableRedirect,
|
|
847
|
+
});
|
|
817
848
|
},
|
|
818
849
|
);
|
|
819
850
|
};
|
|
@@ -875,11 +906,7 @@ export const restoreSubscription = (options: StripeOptions) => {
|
|
|
875
906
|
},
|
|
876
907
|
],
|
|
877
908
|
})
|
|
878
|
-
.then((subs) =>
|
|
879
|
-
subs.find(
|
|
880
|
-
(sub) => sub.status === "active" || sub.status === "trialing",
|
|
881
|
-
),
|
|
882
|
-
);
|
|
909
|
+
.then((subs) => subs.find((sub) => isActiveOrTrialing(sub)));
|
|
883
910
|
if (
|
|
884
911
|
ctx.body.subscriptionId &&
|
|
885
912
|
subscription &&
|
|
@@ -902,7 +929,7 @@ export const restoreSubscription = (options: StripeOptions) => {
|
|
|
902
929
|
STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_ACTIVE,
|
|
903
930
|
);
|
|
904
931
|
}
|
|
905
|
-
if (!subscription
|
|
932
|
+
if (!isPendingCancel(subscription)) {
|
|
906
933
|
throw APIError.from(
|
|
907
934
|
"BAD_REQUEST",
|
|
908
935
|
STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION,
|
|
@@ -913,12 +940,7 @@ export const restoreSubscription = (options: StripeOptions) => {
|
|
|
913
940
|
.list({
|
|
914
941
|
customer: subscription.stripeCustomerId,
|
|
915
942
|
})
|
|
916
|
-
.then(
|
|
917
|
-
(res) =>
|
|
918
|
-
res.data.filter(
|
|
919
|
-
(sub) => sub.status === "active" || sub.status === "trialing",
|
|
920
|
-
)[0],
|
|
921
|
-
);
|
|
943
|
+
.then((res) => res.data.filter((sub) => isActiveOrTrialing(sub))[0]);
|
|
922
944
|
if (!activeSubscription) {
|
|
923
945
|
throw APIError.from(
|
|
924
946
|
"BAD_REQUEST",
|
|
@@ -926,36 +948,41 @@ export const restoreSubscription = (options: StripeOptions) => {
|
|
|
926
948
|
);
|
|
927
949
|
}
|
|
928
950
|
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
951
|
+
// Clear scheduled cancellation based on Stripe subscription state
|
|
952
|
+
// Note: Stripe doesn't accept both `cancel_at` and `cancel_at_period_end` simultaneously
|
|
953
|
+
const updateParams: Stripe.SubscriptionUpdateParams = {};
|
|
954
|
+
if (activeSubscription.cancel_at) {
|
|
955
|
+
updateParams.cancel_at = "";
|
|
956
|
+
} else if (activeSubscription.cancel_at_period_end) {
|
|
957
|
+
updateParams.cancel_at_period_end = false;
|
|
958
|
+
}
|
|
936
959
|
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
{
|
|
945
|
-
field: "id",
|
|
946
|
-
value: subscription.id,
|
|
947
|
-
},
|
|
948
|
-
],
|
|
960
|
+
const newSub = await client.subscriptions
|
|
961
|
+
.update(activeSubscription.id, updateParams)
|
|
962
|
+
.catch((e) => {
|
|
963
|
+
throw ctx.error("BAD_REQUEST", {
|
|
964
|
+
message: e.message,
|
|
965
|
+
code: e.code,
|
|
966
|
+
});
|
|
949
967
|
});
|
|
950
968
|
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
969
|
+
await ctx.context.adapter.update({
|
|
970
|
+
model: "subscription",
|
|
971
|
+
update: {
|
|
972
|
+
cancelAtPeriodEnd: false,
|
|
973
|
+
cancelAt: null,
|
|
974
|
+
canceledAt: null,
|
|
975
|
+
updatedAt: new Date(),
|
|
976
|
+
},
|
|
977
|
+
where: [
|
|
978
|
+
{
|
|
979
|
+
field: "id",
|
|
980
|
+
value: subscription.id,
|
|
981
|
+
},
|
|
982
|
+
],
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
return ctx.json(newSub);
|
|
959
986
|
},
|
|
960
987
|
);
|
|
961
988
|
};
|
|
@@ -1030,9 +1057,7 @@ export const listActiveSubscriptions = (options: StripeOptions) => {
|
|
|
1030
1057
|
priceId: plan?.priceId,
|
|
1031
1058
|
};
|
|
1032
1059
|
})
|
|
1033
|
-
.filter((sub) =>
|
|
1034
|
-
return sub.status === "active" || sub.status === "trialing";
|
|
1035
|
-
});
|
|
1060
|
+
.filter((sub) => isActiveOrTrialing(sub));
|
|
1036
1061
|
return ctx.json(subs);
|
|
1037
1062
|
},
|
|
1038
1063
|
);
|
|
@@ -1118,6 +1143,13 @@ export const subscriptionSuccess = (options: StripeOptions) => {
|
|
|
1118
1143
|
1000,
|
|
1119
1144
|
),
|
|
1120
1145
|
stripeSubscriptionId: stripeSubscription.id,
|
|
1146
|
+
cancelAtPeriodEnd: stripeSubscription.cancel_at_period_end,
|
|
1147
|
+
cancelAt: stripeSubscription.cancel_at
|
|
1148
|
+
? new Date(stripeSubscription.cancel_at * 1000)
|
|
1149
|
+
: null,
|
|
1150
|
+
canceledAt: stripeSubscription.canceled_at
|
|
1151
|
+
? new Date(stripeSubscription.canceled_at * 1000)
|
|
1152
|
+
: null,
|
|
1121
1153
|
...(stripeSubscription.trial_start &&
|
|
1122
1154
|
stripeSubscription.trial_end
|
|
1123
1155
|
? {
|
|
@@ -1157,6 +1189,16 @@ const createBillingPortalBodySchema = z.object({
|
|
|
1157
1189
|
.optional(),
|
|
1158
1190
|
referenceId: z.string().optional(),
|
|
1159
1191
|
returnUrl: z.string().default("/"),
|
|
1192
|
+
/**
|
|
1193
|
+
* Disable Redirect
|
|
1194
|
+
*/
|
|
1195
|
+
disableRedirect: z
|
|
1196
|
+
.boolean()
|
|
1197
|
+
.meta({
|
|
1198
|
+
description:
|
|
1199
|
+
"Disable redirect after creating billing portal session. Eg: true",
|
|
1200
|
+
})
|
|
1201
|
+
.default(false),
|
|
1160
1202
|
});
|
|
1161
1203
|
|
|
1162
1204
|
export const createBillingPortal = (options: StripeOptions) => {
|
|
@@ -1195,11 +1237,7 @@ export const createBillingPortal = (options: StripeOptions) => {
|
|
|
1195
1237
|
},
|
|
1196
1238
|
],
|
|
1197
1239
|
})
|
|
1198
|
-
.then((subs) =>
|
|
1199
|
-
subs.find(
|
|
1200
|
-
(sub) => sub.status === "active" || sub.status === "trialing",
|
|
1201
|
-
),
|
|
1202
|
-
);
|
|
1240
|
+
.then((subs) => subs.find((sub) => isActiveOrTrialing(sub)));
|
|
1203
1241
|
|
|
1204
1242
|
customerId = subscription?.stripeCustomerId;
|
|
1205
1243
|
}
|
|
@@ -1219,7 +1257,7 @@ export const createBillingPortal = (options: StripeOptions) => {
|
|
|
1219
1257
|
|
|
1220
1258
|
return ctx.json({
|
|
1221
1259
|
url,
|
|
1222
|
-
redirect:
|
|
1260
|
+
redirect: !ctx.body.disableRedirect,
|
|
1223
1261
|
});
|
|
1224
1262
|
} catch (error: any) {
|
|
1225
1263
|
ctx.context.logger.error(
|
|
@@ -1293,6 +1331,10 @@ export const stripeWebhook = (options: StripeOptions) => {
|
|
|
1293
1331
|
await onCheckoutSessionCompleted(ctx, options, event);
|
|
1294
1332
|
await options.onEvent?.(event);
|
|
1295
1333
|
break;
|
|
1334
|
+
case "customer.subscription.created":
|
|
1335
|
+
await onSubscriptionCreated(ctx, options, event);
|
|
1336
|
+
await options.onEvent?.(event);
|
|
1337
|
+
break;
|
|
1296
1338
|
case "customer.subscription.updated":
|
|
1297
1339
|
await onSubscriptionUpdated(ctx, options, event);
|
|
1298
1340
|
await options.onEvent?.(event);
|
package/src/schema.ts
CHANGED
|
@@ -46,6 +46,18 @@ export const subscriptions = {
|
|
|
46
46
|
required: false,
|
|
47
47
|
defaultValue: false,
|
|
48
48
|
},
|
|
49
|
+
cancelAt: {
|
|
50
|
+
type: "date",
|
|
51
|
+
required: false,
|
|
52
|
+
},
|
|
53
|
+
canceledAt: {
|
|
54
|
+
type: "date",
|
|
55
|
+
required: false,
|
|
56
|
+
},
|
|
57
|
+
endedAt: {
|
|
58
|
+
type: "date",
|
|
59
|
+
required: false,
|
|
60
|
+
},
|
|
49
61
|
seats: {
|
|
50
62
|
type: "number",
|
|
51
63
|
required: false,
|