@better-auth/stripe 1.5.0-beta.1 → 1.5.0-beta.10

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/src/hooks.ts CHANGED
@@ -1,8 +1,41 @@
1
- import type { GenericEndpointContext } from "better-auth";
2
- import { logger } from "better-auth";
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";
3
4
  import type Stripe from "stripe";
4
- import type { InputSubscription, StripeOptions, Subscription } from "./types";
5
- import { getPlanByPriceInfo } from "./utils";
5
+ import { subscriptionMetadata } from "./metadata";
6
+ import type { CustomerType, StripeOptions, Subscription } from "./types";
7
+ import {
8
+ getPlanByPriceInfo,
9
+ isActiveOrTrialing,
10
+ isPendingCancel,
11
+ isStripePendingCancel,
12
+ } from "./utils";
13
+
14
+ /**
15
+ * Find organization or user by stripeCustomerId.
16
+ * @internal
17
+ */
18
+ async function findReferenceByStripeCustomerId(
19
+ ctx: GenericEndpointContext,
20
+ options: StripeOptions,
21
+ stripeCustomerId: string,
22
+ ): Promise<{ customerType: CustomerType; referenceId: string } | null> {
23
+ if (options.organization?.enabled) {
24
+ const org = await ctx.context.adapter.findOne<Organization>({
25
+ model: "organization",
26
+ where: [{ field: "stripeCustomerId", value: stripeCustomerId }],
27
+ });
28
+ if (org) return { customerType: "organization", referenceId: org.id };
29
+ }
30
+
31
+ const user = await ctx.context.adapter.findOne<User>({
32
+ model: "user",
33
+ where: [{ field: "stripeCustomerId", value: stripeCustomerId }],
34
+ });
35
+ if (user) return { customerType: "user", referenceId: user.id };
36
+
37
+ return null;
38
+ }
6
39
 
7
40
  export async function onCheckoutSessionCompleted(
8
41
  ctx: GenericEndpointContext,
@@ -18,19 +51,27 @@ export async function onCheckoutSessionCompleted(
18
51
  const subscription = await client.subscriptions.retrieve(
19
52
  checkoutSession.subscription as string,
20
53
  );
21
- const priceId = subscription.items.data[0]?.price.id;
22
- const priceLookupKey = subscription.items.data[0]?.price.lookup_key || null;
54
+ const subscriptionItem = subscription.items.data[0];
55
+ if (!subscriptionItem) {
56
+ ctx.context.logger.warn(
57
+ `Stripe webhook warning: Subscription ${subscription.id} has no items`,
58
+ );
59
+ return;
60
+ }
61
+
62
+ const priceId = subscriptionItem.price.id;
63
+ const priceLookupKey = subscriptionItem.price.lookup_key;
23
64
  const plan = await getPlanByPriceInfo(
24
65
  options,
25
66
  priceId as string,
26
67
  priceLookupKey,
27
68
  );
28
69
  if (plan) {
70
+ const checkoutMeta = subscriptionMetadata.get(checkoutSession?.metadata);
29
71
  const referenceId =
30
- checkoutSession?.client_reference_id ||
31
- checkoutSession?.metadata?.referenceId;
32
- const subscriptionId = checkoutSession?.metadata?.subscriptionId;
33
- const seats = subscription.items.data[0]!.quantity;
72
+ checkoutSession?.client_reference_id || checkoutMeta.referenceId;
73
+ const { subscriptionId } = checkoutMeta;
74
+ const seats = subscriptionItem.quantity;
34
75
  if (referenceId && subscriptionId) {
35
76
  const trial =
36
77
  subscription.trial_start && subscription.trial_end
@@ -40,30 +81,35 @@ export async function onCheckoutSessionCompleted(
40
81
  }
41
82
  : {};
42
83
 
43
- let dbSubscription =
44
- await ctx.context.adapter.update<InputSubscription>({
45
- model: "subscription",
46
- update: {
47
- plan: plan.name.toLowerCase(),
48
- status: subscription.status,
49
- updatedAt: new Date(),
50
- periodStart: new Date(
51
- subscription.items.data[0]!.current_period_start * 1000,
52
- ),
53
- periodEnd: new Date(
54
- subscription.items.data[0]!.current_period_end * 1000,
55
- ),
56
- stripeSubscriptionId: checkoutSession.subscription as string,
57
- seats,
58
- ...trial,
84
+ let dbSubscription = await ctx.context.adapter.update<Subscription>({
85
+ model: "subscription",
86
+ update: {
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
+ ...trial,
105
+ },
106
+ where: [
107
+ {
108
+ field: "id",
109
+ value: subscriptionId,
59
110
  },
60
- where: [
61
- {
62
- field: "id",
63
- value: subscriptionId,
64
- },
65
- ],
66
- });
111
+ ],
112
+ });
67
113
 
68
114
  if (trial.trialStart && plan.freeTrial?.onTrialStart) {
69
115
  await plan.freeTrial.onTrialStart(dbSubscription as Subscription);
@@ -93,7 +139,120 @@ export async function onCheckoutSessionCompleted(
93
139
  }
94
140
  }
95
141
  } catch (e: any) {
96
- logger.error(`Stripe webhook failed. Error: ${e.message}`);
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 subscriptionItem = subscriptionCreated.items.data[0];
198
+ if (!subscriptionItem) {
199
+ ctx.context.logger.warn(
200
+ `Stripe webhook warning: Subscription ${subscriptionCreated.id} has no items`,
201
+ );
202
+ return;
203
+ }
204
+
205
+ const priceId = subscriptionItem.price.id;
206
+ const priceLookupKey = subscriptionItem.price.lookup_key || null;
207
+ const plan = await getPlanByPriceInfo(options, priceId, priceLookupKey);
208
+ if (!plan) {
209
+ ctx.context.logger.warn(
210
+ `Stripe webhook warning: No matching plan found for priceId: ${priceId}`,
211
+ );
212
+ return;
213
+ }
214
+
215
+ const seats = subscriptionItem.quantity;
216
+ const periodStart = new Date(subscriptionItem.current_period_start * 1000);
217
+ const periodEnd = new Date(subscriptionItem.current_period_end * 1000);
218
+
219
+ const trial =
220
+ subscriptionCreated.trial_start && subscriptionCreated.trial_end
221
+ ? {
222
+ trialStart: new Date(subscriptionCreated.trial_start * 1000),
223
+ trialEnd: new Date(subscriptionCreated.trial_end * 1000),
224
+ }
225
+ : {};
226
+
227
+ // Create the subscription in the database
228
+ const newSubscription = await ctx.context.adapter.create<Subscription>({
229
+ model: "subscription",
230
+ data: {
231
+ referenceId,
232
+ stripeCustomerId,
233
+ stripeSubscriptionId: subscriptionCreated.id,
234
+ status: subscriptionCreated.status,
235
+ plan: plan.name.toLowerCase(),
236
+ periodStart,
237
+ periodEnd,
238
+ seats,
239
+ ...(plan.limits ? { limits: plan.limits } : {}),
240
+ ...trial,
241
+ },
242
+ });
243
+
244
+ ctx.context.logger.info(
245
+ `Stripe webhook: Created subscription ${subscriptionCreated.id} for ${customerType} ${referenceId} from dashboard`,
246
+ );
247
+
248
+ await options.subscription.onSubscriptionCreated?.({
249
+ event,
250
+ subscription: newSubscription,
251
+ stripeSubscription: subscriptionCreated,
252
+ plan,
253
+ });
254
+ } catch (error: any) {
255
+ ctx.context.logger.error(`Stripe webhook failed. Error: ${error}`);
97
256
  }
98
257
  }
99
258
 
@@ -107,12 +266,21 @@ export async function onSubscriptionUpdated(
107
266
  return;
108
267
  }
109
268
  const subscriptionUpdated = event.data.object as Stripe.Subscription;
110
- const priceId = subscriptionUpdated.items.data[0]!.price.id;
111
- const priceLookupKey =
112
- subscriptionUpdated.items.data[0]!.price.lookup_key || null;
269
+ const subscriptionItem = subscriptionUpdated.items.data[0];
270
+ if (!subscriptionItem) {
271
+ ctx.context.logger.warn(
272
+ `Stripe webhook warning: Subscription ${subscriptionUpdated.id} has no items`,
273
+ );
274
+ return;
275
+ }
276
+
277
+ const priceId = subscriptionItem.price.id;
278
+ const priceLookupKey = subscriptionItem.price.lookup_key;
113
279
  const plan = await getPlanByPriceInfo(options, priceId, priceLookupKey);
114
280
 
115
- const subscriptionId = subscriptionUpdated.metadata?.subscriptionId;
281
+ const { subscriptionId } = subscriptionMetadata.get(
282
+ subscriptionUpdated.metadata,
283
+ );
116
284
  const customerId = subscriptionUpdated.customer?.toString();
117
285
  let subscription = await ctx.context.adapter.findOne<Subscription>({
118
286
  model: "subscription",
@@ -126,12 +294,11 @@ export async function onSubscriptionUpdated(
126
294
  where: [{ field: "stripeCustomerId", value: customerId }],
127
295
  });
128
296
  if (subs.length > 1) {
129
- const activeSub = subs.find(
130
- (sub: Subscription) =>
131
- sub.status === "active" || sub.status === "trialing",
297
+ const activeSub = subs.find((sub: Subscription) =>
298
+ isActiveOrTrialing(sub),
132
299
  );
133
300
  if (!activeSub) {
134
- logger.warn(
301
+ ctx.context.logger.warn(
135
302
  `Stripe webhook error: Multiple subscriptions found for customerId: ${customerId} and no active subscription is found`,
136
303
  );
137
304
  return;
@@ -142,7 +309,6 @@ export async function onSubscriptionUpdated(
142
309
  }
143
310
  }
144
311
 
145
- const seats = subscriptionUpdated.items.data[0]!.quantity;
146
312
  const updatedSubscription = await ctx.context.adapter.update<Subscription>({
147
313
  model: "subscription",
148
314
  update: {
@@ -154,14 +320,19 @@ export async function onSubscriptionUpdated(
154
320
  : {}),
155
321
  updatedAt: new Date(),
156
322
  status: subscriptionUpdated.status,
157
- periodStart: new Date(
158
- subscriptionUpdated.items.data[0]!.current_period_start * 1000,
159
- ),
160
- periodEnd: new Date(
161
- subscriptionUpdated.items.data[0]!.current_period_end * 1000,
162
- ),
323
+ periodStart: new Date(subscriptionItem.current_period_start * 1000),
324
+ periodEnd: new Date(subscriptionItem.current_period_end * 1000),
163
325
  cancelAtPeriodEnd: subscriptionUpdated.cancel_at_period_end,
164
- seats,
326
+ cancelAt: subscriptionUpdated.cancel_at
327
+ ? new Date(subscriptionUpdated.cancel_at * 1000)
328
+ : null,
329
+ canceledAt: subscriptionUpdated.canceled_at
330
+ ? new Date(subscriptionUpdated.canceled_at * 1000)
331
+ : null,
332
+ endedAt: subscriptionUpdated.ended_at
333
+ ? new Date(subscriptionUpdated.ended_at * 1000)
334
+ : null,
335
+ seats: subscriptionItem.quantity,
165
336
  stripeSubscriptionId: subscriptionUpdated.id,
166
337
  },
167
338
  where: [
@@ -171,11 +342,11 @@ export async function onSubscriptionUpdated(
171
342
  },
172
343
  ],
173
344
  });
174
- const subscriptionCanceled =
345
+ const isNewCancellation =
175
346
  subscriptionUpdated.status === "active" &&
176
- subscriptionUpdated.cancel_at_period_end &&
177
- !subscription.cancelAtPeriodEnd; //if this is true, it means the subscription was canceled before the event was triggered
178
- if (subscriptionCanceled) {
347
+ isStripePendingCancel(subscriptionUpdated) &&
348
+ !isPendingCancel(subscription);
349
+ if (isNewCancellation) {
179
350
  await options.subscription.onSubscriptionCancel?.({
180
351
  subscription,
181
352
  cancellationDetails:
@@ -205,7 +376,7 @@ export async function onSubscriptionUpdated(
205
376
  }
206
377
  }
207
378
  } catch (error: any) {
208
- logger.error(`Stripe webhook failed. Error: ${error}`);
379
+ ctx.context.logger.error(`Stripe webhook failed. Error: ${error}`);
209
380
  }
210
381
  }
211
382
 
@@ -241,6 +412,16 @@ export async function onSubscriptionDeleted(
241
412
  update: {
242
413
  status: "canceled",
243
414
  updatedAt: new Date(),
415
+ cancelAtPeriodEnd: subscriptionDeleted.cancel_at_period_end,
416
+ cancelAt: subscriptionDeleted.cancel_at
417
+ ? new Date(subscriptionDeleted.cancel_at * 1000)
418
+ : null,
419
+ canceledAt: subscriptionDeleted.canceled_at
420
+ ? new Date(subscriptionDeleted.canceled_at * 1000)
421
+ : null,
422
+ endedAt: subscriptionDeleted.ended_at
423
+ ? new Date(subscriptionDeleted.ended_at * 1000)
424
+ : null,
244
425
  },
245
426
  });
246
427
  await options.subscription.onSubscriptionDeleted?.({
@@ -249,11 +430,11 @@ export async function onSubscriptionDeleted(
249
430
  subscription,
250
431
  });
251
432
  } else {
252
- logger.warn(
433
+ ctx.context.logger.warn(
253
434
  `Stripe webhook error: Subscription not found for subscriptionId: ${subscriptionId}`,
254
435
  );
255
436
  }
256
437
  } catch (error: any) {
257
- logger.error(`Stripe webhook failed. Error: ${error}`);
438
+ ctx.context.logger.error(`Stripe webhook failed. Error: ${error}`);
258
439
  }
259
440
  }
package/src/index.ts CHANGED
@@ -1,7 +1,10 @@
1
- import { defineErrorCodes } from "@better-auth/core/utils";
2
- import type { BetterAuthPlugin } from "better-auth";
1
+ import type { BetterAuthPlugin, User } from "better-auth";
2
+ import { APIError } from "better-auth";
3
+ import type { Organization } from "better-auth/plugins/organization";
3
4
  import { defu } from "defu";
4
5
  import type Stripe from "stripe";
6
+ import { STRIPE_ERROR_CODES } from "./error-codes";
7
+ import { customerMetadata } from "./metadata";
5
8
  import {
6
9
  cancelSubscription,
7
10
  cancelSubscriptionCallback,
@@ -18,20 +21,17 @@ import type {
18
21
  StripePlan,
19
22
  Subscription,
20
23
  SubscriptionOptions,
24
+ WithStripeCustomerId,
21
25
  } from "./types";
26
+ import { escapeStripeSearchValue } from "./utils";
22
27
 
23
- const STRIPE_ERROR_CODES = defineErrorCodes({
24
- SUBSCRIPTION_NOT_FOUND: "Subscription not found",
25
- SUBSCRIPTION_PLAN_NOT_FOUND: "Subscription plan not found",
26
- ALREADY_SUBSCRIBED_PLAN: "You're already subscribed to this plan",
27
- UNABLE_TO_CREATE_CUSTOMER: "Unable to create customer",
28
- FAILED_TO_FETCH_PLANS: "Failed to fetch plans",
29
- EMAIL_VERIFICATION_REQUIRED:
30
- "Email verification is required before you can subscribe to a plan",
31
- SUBSCRIPTION_NOT_ACTIVE: "Subscription is not active",
32
- SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION:
33
- "Subscription is not scheduled for cancellation",
34
- });
28
+ declare module "@better-auth/core" {
29
+ interface BetterAuthPluginRegistry<AuthOptions, Options> {
30
+ stripe: {
31
+ creator: typeof stripe;
32
+ };
33
+ }
34
+ }
35
35
 
36
36
  export const stripe = <O extends StripeOptions>(options: O) => {
37
37
  const client = options.stripeClient;
@@ -59,31 +59,134 @@ export const stripe = <O extends StripeOptions>(options: O) => {
59
59
  : {}),
60
60
  },
61
61
  init(ctx) {
62
+ if (options.organization?.enabled) {
63
+ const orgPlugin = ctx.getPlugin("organization");
64
+ if (!orgPlugin) {
65
+ ctx.logger.error(`Organization plugin not found`);
66
+ return;
67
+ }
68
+
69
+ const existingHooks = orgPlugin.options.organizationHooks ?? {};
70
+
71
+ /**
72
+ * Sync organization name to Stripe customer
73
+ */
74
+ const afterUpdateStripeOrg = async (data: {
75
+ organization: (Organization & WithStripeCustomerId) | null;
76
+ user: User;
77
+ }) => {
78
+ const { organization } = data;
79
+ if (!organization?.stripeCustomerId) return;
80
+
81
+ try {
82
+ const stripeCustomer = await client.customers.retrieve(
83
+ organization.stripeCustomerId,
84
+ );
85
+
86
+ if (stripeCustomer.deleted) {
87
+ ctx.logger.warn(
88
+ `Stripe customer ${organization.stripeCustomerId} was deleted`,
89
+ );
90
+ return;
91
+ }
92
+
93
+ // Update Stripe customer if name changed
94
+ if (organization.name !== stripeCustomer.name) {
95
+ await client.customers.update(organization.stripeCustomerId, {
96
+ name: organization.name,
97
+ });
98
+ ctx.logger.info(
99
+ `Synced organization name to Stripe: "${stripeCustomer.name}" → "${organization.name}"`,
100
+ );
101
+ }
102
+ } catch (e: any) {
103
+ ctx.logger.error(
104
+ `Failed to sync organization to Stripe: ${e.message}`,
105
+ );
106
+ }
107
+ };
108
+
109
+ /**
110
+ * Block deletion if organization has active subscriptions
111
+ */
112
+ const beforeDeleteStripeOrg = async (data: {
113
+ organization: Organization & WithStripeCustomerId;
114
+ user: User;
115
+ }) => {
116
+ const { organization } = data;
117
+ if (!organization.stripeCustomerId) return;
118
+
119
+ try {
120
+ // Check if organization has any active subscriptions
121
+ const subscriptions = await client.subscriptions.list({
122
+ customer: organization.stripeCustomerId,
123
+ status: "all",
124
+ limit: 100, // 1 ~ 100
125
+ });
126
+ for (const sub of subscriptions.data) {
127
+ if (
128
+ sub.status !== "canceled" &&
129
+ sub.status !== "incomplete" &&
130
+ sub.status !== "incomplete_expired"
131
+ ) {
132
+ throw APIError.from(
133
+ "BAD_REQUEST",
134
+ STRIPE_ERROR_CODES.ORGANIZATION_HAS_ACTIVE_SUBSCRIPTION,
135
+ );
136
+ }
137
+ }
138
+ } catch (error: any) {
139
+ if (error instanceof APIError) {
140
+ throw error;
141
+ }
142
+ ctx.logger.error(
143
+ `Failed to check organization subscriptions: ${error.message}`,
144
+ );
145
+ throw error;
146
+ }
147
+ };
148
+
149
+ orgPlugin.options.organizationHooks = {
150
+ ...existingHooks,
151
+ afterUpdateOrganization: existingHooks.afterUpdateOrganization
152
+ ? async (data) => {
153
+ await existingHooks.afterUpdateOrganization!(data);
154
+ await afterUpdateStripeOrg(data);
155
+ }
156
+ : afterUpdateStripeOrg,
157
+ beforeDeleteOrganization: existingHooks.beforeDeleteOrganization
158
+ ? async (data) => {
159
+ await existingHooks.beforeDeleteOrganization!(data);
160
+ await beforeDeleteStripeOrg(data);
161
+ }
162
+ : beforeDeleteStripeOrg,
163
+ };
164
+ }
165
+
62
166
  return {
63
167
  options: {
64
168
  databaseHooks: {
65
169
  user: {
66
170
  create: {
67
- async after(user, ctx) {
68
- if (!ctx || !options.createCustomerOnSignUp) return;
171
+ async after(user: User & WithStripeCustomerId, ctx) {
172
+ if (
173
+ !ctx ||
174
+ !options.createCustomerOnSignUp ||
175
+ user.stripeCustomerId // Skip if user already has a Stripe customer ID
176
+ ) {
177
+ return;
178
+ }
69
179
 
70
180
  try {
71
- const userWithStripe = user as typeof user & {
72
- stripeCustomerId?: string;
73
- };
74
-
75
- // Skip if user already has a Stripe customer ID
76
- if (userWithStripe.stripeCustomerId) return;
77
-
78
- // Check if customer already exists in Stripe by email
79
- const existingCustomers = await client.customers.list({
80
- email: user.email,
181
+ // Check if user customer already exists in Stripe by email
182
+ const existingCustomers = await client.customers.search({
183
+ query: `email:"${escapeStripeSearchValue(user.email)}" AND -metadata["${customerMetadata.keys.customerType}"]:"organization"`,
81
184
  limit: 1,
82
185
  });
83
186
 
84
187
  let stripeCustomer = existingCustomers.data[0];
85
188
 
86
- // If customer exists, link it to prevent duplicate creation
189
+ // If user customer exists, link it to prevent duplicate creation
87
190
  if (stripeCustomer) {
88
191
  await ctx.context.internalAdapter.updateUser(user.id, {
89
192
  stripeCustomerId: stripeCustomer.id,
@@ -114,13 +217,17 @@ export const stripe = <O extends StripeOptions>(options: O) => {
114
217
  );
115
218
  }
116
219
 
117
- const params: Stripe.CustomerCreateParams = defu(
220
+ const params = defu(
118
221
  {
119
222
  email: user.email,
120
223
  name: user.name,
121
- metadata: {
122
- userId: user.id,
123
- },
224
+ metadata: customerMetadata.set(
225
+ {
226
+ userId: user.id,
227
+ customerType: "user",
228
+ },
229
+ extraCreateParams?.metadata,
230
+ ),
124
231
  },
125
232
  extraCreateParams,
126
233
  );
@@ -150,41 +257,34 @@ export const stripe = <O extends StripeOptions>(options: O) => {
150
257
  },
151
258
  },
152
259
  update: {
153
- async after(user, ctx) {
154
- if (!ctx) return;
260
+ async after(user: User & WithStripeCustomerId, ctx) {
261
+ if (
262
+ !ctx ||
263
+ !user.stripeCustomerId // Only proceed if user has a Stripe customer ID
264
+ )
265
+ return;
155
266
 
156
267
  try {
157
- // Cast user to include stripeCustomerId (added by the stripe plugin schema)
158
- const userWithStripe = user as typeof user & {
159
- stripeCustomerId?: string;
160
- };
161
-
162
- // Only proceed if user has a Stripe customer ID
163
- if (!userWithStripe.stripeCustomerId) return;
164
-
165
268
  // Get the user from the database to check if email actually changed
166
269
  // The 'user' parameter here is the freshly updated user
167
270
  // We need to check if the Stripe customer's email matches
168
271
  const stripeCustomer = await client.customers.retrieve(
169
- userWithStripe.stripeCustomerId,
272
+ user.stripeCustomerId,
170
273
  );
171
274
 
172
275
  // Check if customer was deleted
173
276
  if (stripeCustomer.deleted) {
174
277
  ctx.context.logger.warn(
175
- `Stripe customer ${userWithStripe.stripeCustomerId} was deleted, cannot update email`,
278
+ `Stripe customer ${user.stripeCustomerId} was deleted, cannot update email`,
176
279
  );
177
280
  return;
178
281
  }
179
282
 
180
283
  // If Stripe customer email doesn't match the user's current email, update it
181
284
  if (stripeCustomer.email !== user.email) {
182
- await client.customers.update(
183
- userWithStripe.stripeCustomerId,
184
- {
185
- email: user.email,
186
- },
187
- );
285
+ await client.customers.update(user.stripeCustomerId, {
286
+ email: user.email,
287
+ });
188
288
  ctx.context.logger.info(
189
289
  `Updated Stripe customer email from ${stripeCustomer.email} to ${user.email}`,
190
290
  );