@better-auth/stripe 1.5.0-beta.18 → 1.5.0-beta.19
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/README.md +17 -0
- package/package.json +25 -20
- package/.turbo/turbo-build.log +0 -22
- package/src/client.ts +0 -37
- package/src/error-codes.ts +0 -37
- package/src/hooks.ts +0 -479
- package/src/index.ts +0 -460
- package/src/metadata.ts +0 -94
- package/src/middleware.ts +0 -104
- package/src/routes.ts +0 -2022
- package/src/schema.ts +0 -136
- package/src/types.ts +0 -476
- package/src/utils.ts +0 -99
- package/test/seat-based-billing.test.ts +0 -1187
- package/test/stripe-organization.test.ts +0 -1988
- package/test/stripe.test.ts +0 -7546
- package/test/utils.test.ts +0 -97
- package/tsconfig.json +0 -14
- package/tsdown.config.ts +0 -9
- package/vitest.config.ts +0 -9
package/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Better Auth Stripe Plugin
|
|
2
|
+
|
|
3
|
+
Stripe plugin for [Better Auth](https://www.better-auth.com) — integrate Stripe billing with your authentication system.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @better-auth/stripe
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Documentation
|
|
12
|
+
|
|
13
|
+
For full documentation, visit [better-auth.com/docs/plugins/stripe](https://www.better-auth.com/docs/plugins/stripe).
|
|
14
|
+
|
|
15
|
+
## License
|
|
16
|
+
|
|
17
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-auth/stripe",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
3
|
+
"version": "1.5.0-beta.19",
|
|
4
|
+
"description": "Stripe plugin for Better Auth",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "dist/index.mjs",
|
|
7
|
-
"types": "dist/index.d.mts",
|
|
8
6
|
"license": "MIT",
|
|
9
7
|
"homepage": "https://www.better-auth.com/docs/plugins/stripe",
|
|
10
8
|
"repository": {
|
|
@@ -12,16 +10,23 @@
|
|
|
12
10
|
"url": "git+https://github.com/better-auth/better-auth.git",
|
|
13
11
|
"directory": "packages/stripe"
|
|
14
12
|
},
|
|
13
|
+
"author": "Bereket Engida",
|
|
15
14
|
"keywords": [
|
|
16
15
|
"stripe",
|
|
17
16
|
"auth",
|
|
18
|
-
"
|
|
17
|
+
"payments",
|
|
18
|
+
"typescript",
|
|
19
|
+
"better-auth"
|
|
19
20
|
],
|
|
20
|
-
"module": "dist/index.mjs",
|
|
21
|
-
"description": "Stripe plugin for Better Auth",
|
|
22
21
|
"publishConfig": {
|
|
23
22
|
"access": "public"
|
|
24
23
|
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
27
|
+
"main": "./dist/index.mjs",
|
|
28
|
+
"module": "./dist/index.mjs",
|
|
29
|
+
"types": "./dist/index.d.mts",
|
|
25
30
|
"exports": {
|
|
26
31
|
".": {
|
|
27
32
|
"dev-source": "./src/index.ts",
|
|
@@ -48,26 +53,26 @@
|
|
|
48
53
|
"defu": "^6.1.4",
|
|
49
54
|
"zod": "^4.3.6"
|
|
50
55
|
},
|
|
51
|
-
"peerDependencies": {
|
|
52
|
-
"better-call": "1.3.2",
|
|
53
|
-
"stripe": "^18 || ^19 || ^20",
|
|
54
|
-
"@better-auth/core": "1.5.0-beta.18",
|
|
55
|
-
"better-auth": "1.5.0-beta.18"
|
|
56
|
-
},
|
|
57
56
|
"devDependencies": {
|
|
58
57
|
"better-call": "1.3.2",
|
|
59
58
|
"stripe": "^20.2.0",
|
|
60
59
|
"tsdown": "^0.20.3",
|
|
61
|
-
"@better-auth/core": "1.5.0-beta.
|
|
62
|
-
"better-auth": "1.5.0-beta.
|
|
60
|
+
"@better-auth/core": "1.5.0-beta.19",
|
|
61
|
+
"better-auth": "1.5.0-beta.19"
|
|
62
|
+
},
|
|
63
|
+
"peerDependencies": {
|
|
64
|
+
"better-call": "1.3.2",
|
|
65
|
+
"stripe": "^18 || ^19 || ^20",
|
|
66
|
+
"better-auth": "1.5.0-beta.19",
|
|
67
|
+
"@better-auth/core": "1.5.0-beta.19"
|
|
63
68
|
},
|
|
64
69
|
"scripts": {
|
|
65
|
-
"test": "vitest",
|
|
66
|
-
"coverage": "vitest run --coverage --coverage.provider=istanbul",
|
|
67
|
-
"lint:package": "publint run --strict",
|
|
68
|
-
"lint:types": "attw --profile esm-only --pack .",
|
|
69
70
|
"build": "tsdown",
|
|
70
71
|
"dev": "tsdown --watch",
|
|
71
|
-
"
|
|
72
|
+
"lint:package": "publint run --strict",
|
|
73
|
+
"lint:types": "attw --profile esm-only --pack .",
|
|
74
|
+
"typecheck": "tsc --project tsconfig.json",
|
|
75
|
+
"test": "vitest",
|
|
76
|
+
"coverage": "vitest run --coverage --coverage.provider=istanbul"
|
|
72
77
|
}
|
|
73
78
|
}
|
package/.turbo/turbo-build.log
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
> @better-auth/stripe@1.5.0-beta.18 build /home/runner/work/better-auth/better-auth/packages/stripe
|
|
3
|
-
> tsdown
|
|
4
|
-
|
|
5
|
-
[34mℹ[39m tsdown [2mv0.20.3[22m powered by rolldown [2mv1.0.0-rc.3[22m
|
|
6
|
-
[34mℹ[39m config file: [4m/home/runner/work/better-auth/better-auth/packages/stripe/tsdown.config.ts[24m
|
|
7
|
-
[34mℹ[39m entry: [34msrc/index.ts, src/client.ts[39m
|
|
8
|
-
[34mℹ[39m tsconfig: [34mtsconfig.json[39m
|
|
9
|
-
[34mℹ[39m Build start
|
|
10
|
-
[34mℹ[39m [2mdist/[22m[1mindex.mjs[22m [2m 69.57 kB[22m [2m│ gzip: 13.19 kB[22m
|
|
11
|
-
[34mℹ[39m [2mdist/[22m[1mclient.mjs[22m [2m 0.43 kB[22m [2m│ gzip: 0.29 kB[22m
|
|
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
|
|
15
|
-
[34mℹ[39m [2mdist/[22mclient.mjs.map [2m 1.26 kB[22m [2m│ gzip: 0.59 kB[22m
|
|
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.
|
|
21
|
-
|
|
22
|
-
[32m✔[39m Build complete in [32m43653ms[39m
|
package/src/client.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import type { BetterAuthClientPlugin } from "better-auth/client";
|
|
2
|
-
import { STRIPE_ERROR_CODES } from "./error-codes";
|
|
3
|
-
import type { StripePlan, stripe } from "./index";
|
|
4
|
-
export const stripeClient = <
|
|
5
|
-
O extends {
|
|
6
|
-
subscription: boolean;
|
|
7
|
-
},
|
|
8
|
-
>(
|
|
9
|
-
options?: O | undefined,
|
|
10
|
-
) => {
|
|
11
|
-
return {
|
|
12
|
-
id: "stripe-client",
|
|
13
|
-
$InferServerPlugin: {} as ReturnType<
|
|
14
|
-
typeof stripe<
|
|
15
|
-
O["subscription"] extends true
|
|
16
|
-
? {
|
|
17
|
-
stripeClient: any;
|
|
18
|
-
stripeWebhookSecret: string;
|
|
19
|
-
subscription: {
|
|
20
|
-
enabled: true;
|
|
21
|
-
plans: StripePlan[];
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
: {
|
|
25
|
-
stripeClient: any;
|
|
26
|
-
stripeWebhookSecret: string;
|
|
27
|
-
}
|
|
28
|
-
>
|
|
29
|
-
>,
|
|
30
|
-
pathMethods: {
|
|
31
|
-
"/subscription/billing-portal": "POST",
|
|
32
|
-
"/subscription/restore": "POST",
|
|
33
|
-
},
|
|
34
|
-
$ERROR_CODES: STRIPE_ERROR_CODES,
|
|
35
|
-
} satisfies BetterAuthClientPlugin;
|
|
36
|
-
};
|
|
37
|
-
export * from "./error-codes";
|
package/src/error-codes.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { defineErrorCodes } from "@better-auth/core/utils/error-codes";
|
|
2
|
-
|
|
3
|
-
export const STRIPE_ERROR_CODES = defineErrorCodes({
|
|
4
|
-
UNAUTHORIZED: "Unauthorized access",
|
|
5
|
-
INVALID_REQUEST_BODY: "Invalid request body",
|
|
6
|
-
SUBSCRIPTION_NOT_FOUND: "Subscription not found",
|
|
7
|
-
SUBSCRIPTION_PLAN_NOT_FOUND: "Subscription plan not found",
|
|
8
|
-
ALREADY_SUBSCRIBED_PLAN: "You're already subscribed to this plan",
|
|
9
|
-
REFERENCE_ID_NOT_ALLOWED: "Reference id is not allowed",
|
|
10
|
-
CUSTOMER_NOT_FOUND: "Stripe customer not found for this user",
|
|
11
|
-
UNABLE_TO_CREATE_CUSTOMER: "Unable to create customer",
|
|
12
|
-
UNABLE_TO_CREATE_BILLING_PORTAL: "Unable to create billing portal session",
|
|
13
|
-
STRIPE_SIGNATURE_NOT_FOUND: "Stripe signature not found",
|
|
14
|
-
STRIPE_WEBHOOK_SECRET_NOT_FOUND: "Stripe webhook secret not found",
|
|
15
|
-
STRIPE_WEBHOOK_ERROR: "Stripe webhook error",
|
|
16
|
-
FAILED_TO_CONSTRUCT_STRIPE_EVENT: "Failed to construct Stripe event",
|
|
17
|
-
FAILED_TO_FETCH_PLANS: "Failed to fetch plans",
|
|
18
|
-
EMAIL_VERIFICATION_REQUIRED:
|
|
19
|
-
"Email verification is required before you can subscribe to a plan",
|
|
20
|
-
SUBSCRIPTION_NOT_ACTIVE: "Subscription is not active",
|
|
21
|
-
/**
|
|
22
|
-
* @deprecated Use `SUBSCRIPTION_NOT_PENDING_CHANGE` instead.
|
|
23
|
-
*/
|
|
24
|
-
SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION:
|
|
25
|
-
"Subscription is not scheduled for cancellation",
|
|
26
|
-
SUBSCRIPTION_NOT_PENDING_CHANGE:
|
|
27
|
-
"Subscription has no pending cancellation or scheduled plan change",
|
|
28
|
-
ORGANIZATION_NOT_FOUND: "Organization not found",
|
|
29
|
-
ORGANIZATION_SUBSCRIPTION_NOT_ENABLED:
|
|
30
|
-
"Organization subscription is not enabled",
|
|
31
|
-
AUTHORIZE_REFERENCE_REQUIRED:
|
|
32
|
-
"Organization subscriptions require authorizeReference callback to be configured",
|
|
33
|
-
ORGANIZATION_HAS_ACTIVE_SUBSCRIPTION:
|
|
34
|
-
"Cannot delete organization with active subscription",
|
|
35
|
-
ORGANIZATION_REFERENCE_ID_REQUIRED:
|
|
36
|
-
"Reference ID is required. Provide referenceId or set activeOrganizationId in session",
|
|
37
|
-
});
|
package/src/hooks.ts
DELETED
|
@@ -1,479 +0,0 @@
|
|
|
1
|
-
import type { GenericEndpointContext } from "@better-auth/core";
|
|
2
|
-
import type { User } from "@better-auth/core/db";
|
|
3
|
-
import type { Organization } from "better-auth/plugins/organization";
|
|
4
|
-
import type Stripe from "stripe";
|
|
5
|
-
import { subscriptionMetadata } from "./metadata";
|
|
6
|
-
import type { CustomerType, StripeOptions, Subscription } from "./types";
|
|
7
|
-
import {
|
|
8
|
-
isActiveOrTrialing,
|
|
9
|
-
isPendingCancel,
|
|
10
|
-
isStripePendingCancel,
|
|
11
|
-
resolvePlanItem,
|
|
12
|
-
resolveQuantity,
|
|
13
|
-
} from "./utils";
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Find organization or user by stripeCustomerId.
|
|
17
|
-
* @internal
|
|
18
|
-
*/
|
|
19
|
-
async function findReferenceByStripeCustomerId(
|
|
20
|
-
ctx: GenericEndpointContext,
|
|
21
|
-
options: StripeOptions,
|
|
22
|
-
stripeCustomerId: string,
|
|
23
|
-
): Promise<{ customerType: CustomerType; referenceId: string } | null> {
|
|
24
|
-
if (options.organization?.enabled) {
|
|
25
|
-
const org = await ctx.context.adapter.findOne<Organization>({
|
|
26
|
-
model: "organization",
|
|
27
|
-
where: [{ field: "stripeCustomerId", value: stripeCustomerId }],
|
|
28
|
-
});
|
|
29
|
-
if (org) return { customerType: "organization", referenceId: org.id };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const user = await ctx.context.adapter.findOne<User>({
|
|
33
|
-
model: "user",
|
|
34
|
-
where: [{ field: "stripeCustomerId", value: stripeCustomerId }],
|
|
35
|
-
});
|
|
36
|
-
if (user) return { customerType: "user", referenceId: user.id };
|
|
37
|
-
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export async function onCheckoutSessionCompleted(
|
|
42
|
-
ctx: GenericEndpointContext,
|
|
43
|
-
options: StripeOptions,
|
|
44
|
-
event: Stripe.Event,
|
|
45
|
-
) {
|
|
46
|
-
try {
|
|
47
|
-
const client = options.stripeClient;
|
|
48
|
-
const checkoutSession = event.data.object as Stripe.Checkout.Session;
|
|
49
|
-
if (checkoutSession.mode === "setup" || !options.subscription?.enabled) {
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
const subscription = await client.subscriptions.retrieve(
|
|
53
|
-
checkoutSession.subscription as string,
|
|
54
|
-
);
|
|
55
|
-
const resolved = await resolvePlanItem(options, subscription.items.data);
|
|
56
|
-
if (!resolved) {
|
|
57
|
-
ctx.context.logger.warn(
|
|
58
|
-
`Stripe webhook warning: Subscription ${subscription.id} has no items matching a configured plan`,
|
|
59
|
-
);
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const { item: subscriptionItem, plan } = resolved;
|
|
64
|
-
if (plan) {
|
|
65
|
-
const checkoutMeta = subscriptionMetadata.get(checkoutSession?.metadata);
|
|
66
|
-
const referenceId =
|
|
67
|
-
checkoutSession?.client_reference_id || checkoutMeta.referenceId;
|
|
68
|
-
const { subscriptionId } = checkoutMeta;
|
|
69
|
-
const seats = resolveQuantity(
|
|
70
|
-
subscription.items.data,
|
|
71
|
-
subscriptionItem,
|
|
72
|
-
plan.seatPriceId,
|
|
73
|
-
);
|
|
74
|
-
if (referenceId && subscriptionId) {
|
|
75
|
-
const trial =
|
|
76
|
-
subscription.trial_start && subscription.trial_end
|
|
77
|
-
? {
|
|
78
|
-
trialStart: new Date(subscription.trial_start * 1000),
|
|
79
|
-
trialEnd: new Date(subscription.trial_end * 1000),
|
|
80
|
-
}
|
|
81
|
-
: {};
|
|
82
|
-
|
|
83
|
-
let dbSubscription = await ctx.context.adapter.update<Subscription>({
|
|
84
|
-
model: "subscription",
|
|
85
|
-
update: {
|
|
86
|
-
...trial,
|
|
87
|
-
plan: plan.name.toLowerCase(),
|
|
88
|
-
status: subscription.status,
|
|
89
|
-
updatedAt: new Date(),
|
|
90
|
-
periodStart: new Date(subscriptionItem.current_period_start * 1000),
|
|
91
|
-
periodEnd: new Date(subscriptionItem.current_period_end * 1000),
|
|
92
|
-
stripeSubscriptionId: checkoutSession.subscription as string,
|
|
93
|
-
cancelAtPeriodEnd: subscription.cancel_at_period_end,
|
|
94
|
-
cancelAt: subscription.cancel_at
|
|
95
|
-
? new Date(subscription.cancel_at * 1000)
|
|
96
|
-
: null,
|
|
97
|
-
canceledAt: subscription.canceled_at
|
|
98
|
-
? new Date(subscription.canceled_at * 1000)
|
|
99
|
-
: null,
|
|
100
|
-
endedAt: subscription.ended_at
|
|
101
|
-
? new Date(subscription.ended_at * 1000)
|
|
102
|
-
: null,
|
|
103
|
-
seats: seats,
|
|
104
|
-
billingInterval: subscriptionItem.price.recurring?.interval,
|
|
105
|
-
},
|
|
106
|
-
where: [
|
|
107
|
-
{
|
|
108
|
-
field: "id",
|
|
109
|
-
value: subscriptionId,
|
|
110
|
-
},
|
|
111
|
-
],
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
if (trial.trialStart && plan.freeTrial?.onTrialStart) {
|
|
115
|
-
await plan.freeTrial.onTrialStart(dbSubscription as Subscription);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (!dbSubscription) {
|
|
119
|
-
dbSubscription = await ctx.context.adapter.findOne<Subscription>({
|
|
120
|
-
model: "subscription",
|
|
121
|
-
where: [
|
|
122
|
-
{
|
|
123
|
-
field: "id",
|
|
124
|
-
value: subscriptionId,
|
|
125
|
-
},
|
|
126
|
-
],
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
await options.subscription?.onSubscriptionComplete?.(
|
|
130
|
-
{
|
|
131
|
-
event,
|
|
132
|
-
subscription: dbSubscription as Subscription,
|
|
133
|
-
stripeSubscription: subscription,
|
|
134
|
-
plan,
|
|
135
|
-
},
|
|
136
|
-
ctx,
|
|
137
|
-
);
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
} catch (e: any) {
|
|
142
|
-
ctx.context.logger.error(`Stripe webhook failed. Error: ${e.message}`);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export async function onSubscriptionCreated(
|
|
147
|
-
ctx: GenericEndpointContext,
|
|
148
|
-
options: StripeOptions,
|
|
149
|
-
event: Stripe.Event,
|
|
150
|
-
) {
|
|
151
|
-
try {
|
|
152
|
-
if (!options.subscription?.enabled) {
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const subscriptionCreated = event.data.object as Stripe.Subscription;
|
|
157
|
-
const stripeCustomerId = subscriptionCreated.customer?.toString();
|
|
158
|
-
if (!stripeCustomerId) {
|
|
159
|
-
ctx.context.logger.warn(
|
|
160
|
-
`Stripe webhook warning: customer.subscription.created event received without customer ID`,
|
|
161
|
-
);
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Check if subscription already exists in database
|
|
166
|
-
const { subscriptionId } = subscriptionMetadata.get(
|
|
167
|
-
subscriptionCreated.metadata,
|
|
168
|
-
);
|
|
169
|
-
const existingSubscription =
|
|
170
|
-
await ctx.context.adapter.findOne<Subscription>({
|
|
171
|
-
model: "subscription",
|
|
172
|
-
where: subscriptionId
|
|
173
|
-
? [{ field: "id", value: subscriptionId }]
|
|
174
|
-
: [{ field: "stripeSubscriptionId", value: subscriptionCreated.id }], // Probably won't match since it's not set yet
|
|
175
|
-
});
|
|
176
|
-
if (existingSubscription) {
|
|
177
|
-
ctx.context.logger.info(
|
|
178
|
-
`Stripe webhook: Subscription already exists in database (id: ${existingSubscription.id}), skipping creation`,
|
|
179
|
-
);
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Find reference
|
|
184
|
-
const reference = await findReferenceByStripeCustomerId(
|
|
185
|
-
ctx,
|
|
186
|
-
options,
|
|
187
|
-
stripeCustomerId,
|
|
188
|
-
);
|
|
189
|
-
if (!reference) {
|
|
190
|
-
ctx.context.logger.warn(
|
|
191
|
-
`Stripe webhook warning: No user or organization found with stripeCustomerId: ${stripeCustomerId}`,
|
|
192
|
-
);
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
const { referenceId, customerType } = reference;
|
|
196
|
-
|
|
197
|
-
const resolved = await resolvePlanItem(
|
|
198
|
-
options,
|
|
199
|
-
subscriptionCreated.items.data,
|
|
200
|
-
);
|
|
201
|
-
if (!resolved) {
|
|
202
|
-
ctx.context.logger.warn(
|
|
203
|
-
`Stripe webhook warning: Subscription ${subscriptionCreated.id} has no items matching a configured plan`,
|
|
204
|
-
);
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const { item: subscriptionItem, plan } = resolved;
|
|
209
|
-
if (!plan) {
|
|
210
|
-
ctx.context.logger.warn(
|
|
211
|
-
`Stripe webhook warning: No matching plan found for priceId: ${subscriptionItem.price.id}`,
|
|
212
|
-
);
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const seats = resolveQuantity(
|
|
217
|
-
subscriptionCreated.items.data,
|
|
218
|
-
subscriptionItem,
|
|
219
|
-
plan.seatPriceId,
|
|
220
|
-
);
|
|
221
|
-
const periodStart = new Date(subscriptionItem.current_period_start * 1000);
|
|
222
|
-
const periodEnd = new Date(subscriptionItem.current_period_end * 1000);
|
|
223
|
-
|
|
224
|
-
const trial =
|
|
225
|
-
subscriptionCreated.trial_start && subscriptionCreated.trial_end
|
|
226
|
-
? {
|
|
227
|
-
trialStart: new Date(subscriptionCreated.trial_start * 1000),
|
|
228
|
-
trialEnd: new Date(subscriptionCreated.trial_end * 1000),
|
|
229
|
-
}
|
|
230
|
-
: {};
|
|
231
|
-
|
|
232
|
-
// Create the subscription in the database
|
|
233
|
-
const newSubscription = await ctx.context.adapter.create<Subscription>({
|
|
234
|
-
model: "subscription",
|
|
235
|
-
data: {
|
|
236
|
-
...trial,
|
|
237
|
-
...(plan.limits ? { limits: plan.limits } : {}),
|
|
238
|
-
referenceId,
|
|
239
|
-
stripeCustomerId,
|
|
240
|
-
stripeSubscriptionId: subscriptionCreated.id,
|
|
241
|
-
status: subscriptionCreated.status,
|
|
242
|
-
plan: plan.name.toLowerCase(),
|
|
243
|
-
periodStart,
|
|
244
|
-
periodEnd,
|
|
245
|
-
seats,
|
|
246
|
-
billingInterval: subscriptionItem.price.recurring?.interval,
|
|
247
|
-
},
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
ctx.context.logger.info(
|
|
251
|
-
`Stripe webhook: Created subscription ${subscriptionCreated.id} for ${customerType} ${referenceId} from dashboard`,
|
|
252
|
-
);
|
|
253
|
-
|
|
254
|
-
await options.subscription.onSubscriptionCreated?.({
|
|
255
|
-
event,
|
|
256
|
-
subscription: newSubscription,
|
|
257
|
-
stripeSubscription: subscriptionCreated,
|
|
258
|
-
plan,
|
|
259
|
-
});
|
|
260
|
-
} catch (error: any) {
|
|
261
|
-
ctx.context.logger.error(`Stripe webhook failed. Error: ${error}`);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
export async function onSubscriptionUpdated(
|
|
266
|
-
ctx: GenericEndpointContext,
|
|
267
|
-
options: StripeOptions,
|
|
268
|
-
event: Stripe.Event,
|
|
269
|
-
) {
|
|
270
|
-
try {
|
|
271
|
-
if (!options.subscription?.enabled) {
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
const subscriptionUpdated = event.data.object as Stripe.Subscription;
|
|
275
|
-
const resolved = await resolvePlanItem(
|
|
276
|
-
options,
|
|
277
|
-
subscriptionUpdated.items.data,
|
|
278
|
-
);
|
|
279
|
-
if (!resolved) {
|
|
280
|
-
ctx.context.logger.warn(
|
|
281
|
-
`Stripe webhook warning: Subscription ${subscriptionUpdated.id} has no items matching a configured plan`,
|
|
282
|
-
);
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const { item: subscriptionItem, plan } = resolved;
|
|
287
|
-
|
|
288
|
-
const { subscriptionId } = subscriptionMetadata.get(
|
|
289
|
-
subscriptionUpdated.metadata,
|
|
290
|
-
);
|
|
291
|
-
const customerId = subscriptionUpdated.customer?.toString();
|
|
292
|
-
let subscription = await ctx.context.adapter.findOne<Subscription>({
|
|
293
|
-
model: "subscription",
|
|
294
|
-
where: subscriptionId
|
|
295
|
-
? [{ field: "id", value: subscriptionId }]
|
|
296
|
-
: [{ field: "stripeSubscriptionId", value: subscriptionUpdated.id }],
|
|
297
|
-
});
|
|
298
|
-
if (!subscription) {
|
|
299
|
-
const subs = await ctx.context.adapter.findMany<Subscription>({
|
|
300
|
-
model: "subscription",
|
|
301
|
-
where: [{ field: "stripeCustomerId", value: customerId }],
|
|
302
|
-
});
|
|
303
|
-
if (subs.length > 1) {
|
|
304
|
-
const activeSub = subs.find((sub: Subscription) =>
|
|
305
|
-
isActiveOrTrialing(sub),
|
|
306
|
-
);
|
|
307
|
-
if (!activeSub) {
|
|
308
|
-
ctx.context.logger.warn(
|
|
309
|
-
`Stripe webhook error: Multiple subscriptions found for customerId: ${customerId} and no active subscription is found`,
|
|
310
|
-
);
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
subscription = activeSub;
|
|
314
|
-
} else {
|
|
315
|
-
subscription = subs[0]!;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const seats = plan
|
|
320
|
-
? resolveQuantity(
|
|
321
|
-
subscriptionUpdated.items.data,
|
|
322
|
-
subscriptionItem,
|
|
323
|
-
plan.seatPriceId,
|
|
324
|
-
)
|
|
325
|
-
: subscriptionItem.quantity;
|
|
326
|
-
|
|
327
|
-
const trial =
|
|
328
|
-
subscriptionUpdated.trial_start && subscriptionUpdated.trial_end
|
|
329
|
-
? {
|
|
330
|
-
trialStart: new Date(subscriptionUpdated.trial_start * 1000),
|
|
331
|
-
trialEnd: new Date(subscriptionUpdated.trial_end * 1000),
|
|
332
|
-
}
|
|
333
|
-
: {};
|
|
334
|
-
|
|
335
|
-
const updatedSubscription = await ctx.context.adapter.update<Subscription>({
|
|
336
|
-
model: "subscription",
|
|
337
|
-
update: {
|
|
338
|
-
...trial,
|
|
339
|
-
...(plan
|
|
340
|
-
? {
|
|
341
|
-
plan: plan.name.toLowerCase(),
|
|
342
|
-
limits: plan.limits,
|
|
343
|
-
}
|
|
344
|
-
: {}),
|
|
345
|
-
updatedAt: new Date(),
|
|
346
|
-
status: subscriptionUpdated.status,
|
|
347
|
-
periodStart: new Date(subscriptionItem.current_period_start * 1000),
|
|
348
|
-
periodEnd: new Date(subscriptionItem.current_period_end * 1000),
|
|
349
|
-
cancelAtPeriodEnd: subscriptionUpdated.cancel_at_period_end,
|
|
350
|
-
cancelAt: subscriptionUpdated.cancel_at
|
|
351
|
-
? new Date(subscriptionUpdated.cancel_at * 1000)
|
|
352
|
-
: null,
|
|
353
|
-
canceledAt: subscriptionUpdated.canceled_at
|
|
354
|
-
? new Date(subscriptionUpdated.canceled_at * 1000)
|
|
355
|
-
: null,
|
|
356
|
-
endedAt: subscriptionUpdated.ended_at
|
|
357
|
-
? new Date(subscriptionUpdated.ended_at * 1000)
|
|
358
|
-
: null,
|
|
359
|
-
seats,
|
|
360
|
-
stripeSubscriptionId: subscriptionUpdated.id,
|
|
361
|
-
billingInterval: subscriptionItem.price.recurring?.interval,
|
|
362
|
-
stripeScheduleId: subscriptionUpdated.schedule
|
|
363
|
-
? typeof subscriptionUpdated.schedule === "string"
|
|
364
|
-
? subscriptionUpdated.schedule
|
|
365
|
-
: subscriptionUpdated.schedule.id
|
|
366
|
-
: null,
|
|
367
|
-
},
|
|
368
|
-
where: [
|
|
369
|
-
{
|
|
370
|
-
field: "id",
|
|
371
|
-
value: subscription.id,
|
|
372
|
-
},
|
|
373
|
-
],
|
|
374
|
-
});
|
|
375
|
-
const isNewCancellation =
|
|
376
|
-
subscriptionUpdated.status === "active" &&
|
|
377
|
-
isStripePendingCancel(subscriptionUpdated) &&
|
|
378
|
-
!isPendingCancel(subscription);
|
|
379
|
-
if (isNewCancellation) {
|
|
380
|
-
await options.subscription.onSubscriptionCancel?.({
|
|
381
|
-
subscription,
|
|
382
|
-
cancellationDetails:
|
|
383
|
-
subscriptionUpdated.cancellation_details || undefined,
|
|
384
|
-
stripeSubscription: subscriptionUpdated,
|
|
385
|
-
event,
|
|
386
|
-
});
|
|
387
|
-
}
|
|
388
|
-
await options.subscription.onSubscriptionUpdate?.({
|
|
389
|
-
event,
|
|
390
|
-
subscription: updatedSubscription || subscription,
|
|
391
|
-
});
|
|
392
|
-
if (plan) {
|
|
393
|
-
if (
|
|
394
|
-
subscriptionUpdated.status === "active" &&
|
|
395
|
-
subscription.status === "trialing" &&
|
|
396
|
-
plan.freeTrial?.onTrialEnd
|
|
397
|
-
) {
|
|
398
|
-
await plan.freeTrial.onTrialEnd({ subscription }, ctx);
|
|
399
|
-
}
|
|
400
|
-
if (
|
|
401
|
-
subscriptionUpdated.status === "incomplete_expired" &&
|
|
402
|
-
subscription.status === "trialing" &&
|
|
403
|
-
plan.freeTrial?.onTrialExpired
|
|
404
|
-
) {
|
|
405
|
-
await plan.freeTrial.onTrialExpired(subscription, ctx);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
} catch (error: any) {
|
|
409
|
-
ctx.context.logger.error(`Stripe webhook failed. Error: ${error}`);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
export async function onSubscriptionDeleted(
|
|
414
|
-
ctx: GenericEndpointContext,
|
|
415
|
-
options: StripeOptions,
|
|
416
|
-
event: Stripe.Event,
|
|
417
|
-
) {
|
|
418
|
-
if (!options.subscription?.enabled) {
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
try {
|
|
422
|
-
const subscriptionDeleted = event.data.object as Stripe.Subscription;
|
|
423
|
-
const subscriptionId = subscriptionDeleted.id;
|
|
424
|
-
const subscription = await ctx.context.adapter.findOne<Subscription>({
|
|
425
|
-
model: "subscription",
|
|
426
|
-
where: [
|
|
427
|
-
{
|
|
428
|
-
field: "stripeSubscriptionId",
|
|
429
|
-
value: subscriptionId,
|
|
430
|
-
},
|
|
431
|
-
],
|
|
432
|
-
});
|
|
433
|
-
if (subscription) {
|
|
434
|
-
const trial =
|
|
435
|
-
subscriptionDeleted.trial_start && subscriptionDeleted.trial_end
|
|
436
|
-
? {
|
|
437
|
-
trialStart: new Date(subscriptionDeleted.trial_start * 1000),
|
|
438
|
-
trialEnd: new Date(subscriptionDeleted.trial_end * 1000),
|
|
439
|
-
}
|
|
440
|
-
: {};
|
|
441
|
-
await ctx.context.adapter.update({
|
|
442
|
-
model: "subscription",
|
|
443
|
-
where: [
|
|
444
|
-
{
|
|
445
|
-
field: "id",
|
|
446
|
-
value: subscription.id,
|
|
447
|
-
},
|
|
448
|
-
],
|
|
449
|
-
update: {
|
|
450
|
-
...trial,
|
|
451
|
-
status: "canceled",
|
|
452
|
-
updatedAt: new Date(),
|
|
453
|
-
cancelAtPeriodEnd: subscriptionDeleted.cancel_at_period_end,
|
|
454
|
-
cancelAt: subscriptionDeleted.cancel_at
|
|
455
|
-
? new Date(subscriptionDeleted.cancel_at * 1000)
|
|
456
|
-
: null,
|
|
457
|
-
canceledAt: subscriptionDeleted.canceled_at
|
|
458
|
-
? new Date(subscriptionDeleted.canceled_at * 1000)
|
|
459
|
-
: null,
|
|
460
|
-
endedAt: subscriptionDeleted.ended_at
|
|
461
|
-
? new Date(subscriptionDeleted.ended_at * 1000)
|
|
462
|
-
: null,
|
|
463
|
-
stripeScheduleId: null,
|
|
464
|
-
},
|
|
465
|
-
});
|
|
466
|
-
await options.subscription.onSubscriptionDeleted?.({
|
|
467
|
-
event,
|
|
468
|
-
stripeSubscription: subscriptionDeleted,
|
|
469
|
-
subscription,
|
|
470
|
-
});
|
|
471
|
-
} else {
|
|
472
|
-
ctx.context.logger.warn(
|
|
473
|
-
`Stripe webhook error: Subscription not found for subscriptionId: ${subscriptionId}`,
|
|
474
|
-
);
|
|
475
|
-
}
|
|
476
|
-
} catch (error: any) {
|
|
477
|
-
ctx.context.logger.error(`Stripe webhook failed. Error: ${error}`);
|
|
478
|
-
}
|
|
479
|
-
}
|