@augmenting-integrations/billing 8.0.6 → 8.1.0

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.
@@ -0,0 +1,136 @@
1
+ import "server-only";
2
+ import { NextResponse } from "next/server";
3
+ export type BillingUser = {
4
+ id: bigint | string | number;
5
+ email: string;
6
+ name: string;
7
+ credit_balance: number | bigint | string;
8
+ stripe_customer_id: string | null;
9
+ stripe_default_payment_method: string | null;
10
+ stripe_default_payment_method_display: string | null;
11
+ auto_recharge_enabled: boolean;
12
+ auto_recharge_threshold: number | bigint | string | null;
13
+ auto_recharge_amount: number | bigint | string | null;
14
+ };
15
+ export type BillingPaymentMethodRow = {
16
+ id: bigint | string | number;
17
+ user_id: bigint | string | number;
18
+ stripe_payment_method_id: string;
19
+ brand: string;
20
+ last4: string;
21
+ exp_month: number;
22
+ exp_year: number;
23
+ is_default: boolean;
24
+ created_at: Date;
25
+ };
26
+ export type BillingCreditTransactionRow = {
27
+ id: bigint | string | number;
28
+ user_id: bigint | string | number;
29
+ type: string;
30
+ amount: number | bigint | string;
31
+ description: string | null;
32
+ stripe_payment_intent_id: string | null;
33
+ payment_method_display: string | null;
34
+ created_at: Date;
35
+ };
36
+ type AnyArgs = Record<string, unknown>;
37
+ /**
38
+ * Structural Prisma-like surface the library handlers consume. The spoke's
39
+ * generated PrismaClient satisfies this shape automatically as long as the
40
+ * canonical models are present in its schema.
41
+ */
42
+ export type BillingDb = {
43
+ user: {
44
+ findFirst: (args: AnyArgs) => Promise<BillingUser | null>;
45
+ update: (args: AnyArgs) => Promise<BillingUser | Partial<BillingUser>>;
46
+ };
47
+ paymentMethod: {
48
+ findMany: (args: AnyArgs) => Promise<BillingPaymentMethodRow[]>;
49
+ findUnique: (args: AnyArgs) => Promise<BillingPaymentMethodRow | null>;
50
+ upsert: (args: AnyArgs) => Promise<BillingPaymentMethodRow>;
51
+ update: (args: AnyArgs) => Promise<BillingPaymentMethodRow>;
52
+ updateMany: (args: AnyArgs) => Promise<{
53
+ count: number;
54
+ }>;
55
+ delete: (args: AnyArgs) => Promise<BillingPaymentMethodRow>;
56
+ };
57
+ creditTransaction: {
58
+ findMany: (args: AnyArgs) => Promise<BillingCreditTransactionRow[]>;
59
+ findUnique: (args: AnyArgs) => Promise<BillingCreditTransactionRow | null>;
60
+ aggregate: (args: AnyArgs) => Promise<{
61
+ _sum: {
62
+ amount: unknown;
63
+ };
64
+ }>;
65
+ create: (args: AnyArgs) => Promise<BillingCreditTransactionRow>;
66
+ };
67
+ $transaction: <T>(ops: unknown[] | ((tx: BillingDb) => Promise<T>)) => Promise<T>;
68
+ };
69
+ type SessionLike = {
70
+ user?: unknown;
71
+ } | null;
72
+ type AuthFn = () => Promise<SessionLike>;
73
+ export type CreateBillingHandlersOptions = {
74
+ auth: AuthFn;
75
+ getDb: () => Promise<BillingDb>;
76
+ getOrCreateAppUser: (session: SessionLike) => Promise<BillingUser>;
77
+ /**
78
+ * Recent-transactions limit on /api/billing aggregate + /api/billing/transactions.
79
+ * Default 10.
80
+ */
81
+ recentLimit?: number;
82
+ /**
83
+ * Minimum top-up cents. Default 500 ($5).
84
+ */
85
+ minTopUpCents?: number;
86
+ /**
87
+ * Maximum top-up cents. Default 100_000 ($1,000).
88
+ */
89
+ maxTopUpCents?: number;
90
+ };
91
+ export declare function createBillingHandlers(opts: CreateBillingHandlersOptions): {
92
+ aggregate: {
93
+ GET: () => Promise<NextResponse<unknown>>;
94
+ };
95
+ balance: {
96
+ GET: () => Promise<NextResponse<unknown>>;
97
+ };
98
+ transactions: {
99
+ GET: (request: Request) => Promise<NextResponse<unknown>>;
100
+ };
101
+ paymentIntent: {
102
+ POST: (request: Request) => Promise<NextResponse<unknown>>;
103
+ };
104
+ setupIntent: {
105
+ POST: () => Promise<NextResponse<unknown>>;
106
+ };
107
+ paymentMethods: {
108
+ GET: () => Promise<NextResponse<unknown>>;
109
+ };
110
+ paymentMethodById: {
111
+ DELETE: (_request: Request, context: {
112
+ params: Promise<{
113
+ id: string;
114
+ }>;
115
+ }) => Promise<NextResponse<unknown>>;
116
+ };
117
+ paymentMethodDefault: {
118
+ PATCH: (_request: Request, context: {
119
+ params: Promise<{
120
+ id: string;
121
+ }>;
122
+ }) => Promise<NextResponse<unknown>>;
123
+ };
124
+ autoRecharge: {
125
+ PATCH: (request: Request) => Promise<NextResponse<unknown>>;
126
+ };
127
+ webhook: {
128
+ POST: (request: Request) => Promise<NextResponse<{
129
+ error: string;
130
+ }> | NextResponse<{
131
+ received: boolean;
132
+ }>>;
133
+ };
134
+ };
135
+ export {};
136
+ //# sourceMappingURL=handlers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/server/handlers.ts"],"names":[],"mappings":"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"}
package/dist/server.cjs CHANGED
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/server.ts
31
31
  var server_exports = {};
32
32
  __export(server_exports, {
33
+ createBillingHandlers: () => createBillingHandlers,
33
34
  findOrCreateStripeCustomer: () => findOrCreateStripeCustomer,
34
35
  getStripe: () => getStripe,
35
36
  verifyStripeWebhook: () => verifyStripeWebhook
@@ -111,8 +112,511 @@ async function findOrCreateStripeCustomer(input, stripe) {
111
112
  });
112
113
  return { customerId: created.id, created: true };
113
114
  }
115
+
116
+ // src/server/handlers.ts
117
+ var import_server_only = require("server-only");
118
+ var import_server2 = require("next/server");
119
+ var import_zod = require("zod");
120
+ function buildSchemas(minCents, maxCents) {
121
+ const PaymentIntentBodySchema = import_zod.z.object({
122
+ amountCents: import_zod.z.number().int().min(minCents, `minimum top-up is $${(minCents / 100).toFixed(2)}`).max(maxCents, `maximum top-up is $${(maxCents / 100).toFixed(2)}`)
123
+ });
124
+ const AutoRechargeBodySchema = import_zod.z.object({
125
+ enabled: import_zod.z.boolean(),
126
+ thresholdCents: import_zod.z.number().int().min(minCents).max(maxCents).nullable().optional(),
127
+ amountCents: import_zod.z.number().int().min(minCents).max(maxCents).nullable().optional()
128
+ });
129
+ return { PaymentIntentBodySchema, AutoRechargeBodySchema };
130
+ }
131
+ async function requireUser(auth, getOrCreateAppUser) {
132
+ const session = await auth();
133
+ if (!session || !session.user) {
134
+ return { res: import_server2.NextResponse.json({ error: "unauthorized" }, { status: 401 }) };
135
+ }
136
+ const user = await getOrCreateAppUser(session);
137
+ return { user };
138
+ }
139
+ async function readJson(request, schema) {
140
+ let body;
141
+ try {
142
+ body = await request.json();
143
+ } catch {
144
+ return {
145
+ res: import_server2.NextResponse.json(
146
+ { error: "invalid_json", code: "validation" },
147
+ { status: 400 }
148
+ )
149
+ };
150
+ }
151
+ const parsed = schema.safeParse(body);
152
+ if (!parsed.success) {
153
+ return {
154
+ res: import_server2.NextResponse.json(
155
+ { error: "invalid_body", issues: parsed.error.issues },
156
+ { status: 400 }
157
+ )
158
+ };
159
+ }
160
+ return { data: parsed.data };
161
+ }
162
+ function parsePkId(raw) {
163
+ try {
164
+ return BigInt(raw);
165
+ } catch {
166
+ return { res: import_server2.NextResponse.json({ error: "invalid_id" }, { status: 400 }) };
167
+ }
168
+ }
169
+ function createBillingHandlers(opts) {
170
+ const { auth, getDb, getOrCreateAppUser } = opts;
171
+ const recentLimit = opts.recentLimit ?? 10;
172
+ const minCents = opts.minTopUpCents ?? 500;
173
+ const maxCents = opts.maxTopUpCents ?? 1e5;
174
+ const { PaymentIntentBodySchema, AutoRechargeBodySchema } = buildSchemas(
175
+ minCents,
176
+ maxCents
177
+ );
178
+ const aggregateGET = async () => {
179
+ const r = await requireUser(auth, getOrCreateAppUser);
180
+ if ("res" in r) return r.res;
181
+ const { user } = r;
182
+ const db = await getDb();
183
+ const stripePromise = getStripe().catch(() => null);
184
+ const [recent, purchaseAgg, topupAgg, savedCards, stripeBundle] = await Promise.all([
185
+ db.creditTransaction.findMany({
186
+ where: { user_id: user.id },
187
+ orderBy: { created_at: "desc" },
188
+ take: recentLimit
189
+ }),
190
+ db.creditTransaction.aggregate({
191
+ where: { user_id: user.id, type: "purchase" },
192
+ _sum: { amount: true }
193
+ }),
194
+ db.creditTransaction.aggregate({
195
+ where: { user_id: user.id, amount: { gt: 0 } },
196
+ _sum: { amount: true }
197
+ }),
198
+ db.paymentMethod.findMany({
199
+ where: { user_id: user.id },
200
+ orderBy: [{ is_default: "desc" }, { created_at: "desc" }]
201
+ }),
202
+ stripePromise
203
+ ]);
204
+ return import_server2.NextResponse.json({
205
+ credit_balance: Number(user.credit_balance),
206
+ publishableKey: stripeBundle?.publishableKey ?? null,
207
+ auto_recharge_enabled: user.auto_recharge_enabled,
208
+ auto_recharge_threshold: user.auto_recharge_threshold != null ? Number(user.auto_recharge_threshold) : null,
209
+ auto_recharge_amount: user.auto_recharge_amount != null ? Number(user.auto_recharge_amount) : null,
210
+ saved_cards: savedCards.map((m) => ({
211
+ id: m.id.toString(),
212
+ brand: m.brand,
213
+ last4: m.last4,
214
+ exp_month: m.exp_month,
215
+ exp_year: m.exp_year,
216
+ is_default: m.is_default
217
+ })),
218
+ recent_transactions: recent.map((t) => ({
219
+ id: t.id.toString(),
220
+ type: t.type,
221
+ amount: Number(t.amount),
222
+ description: t.description,
223
+ stripe_payment_intent_id: t.stripe_payment_intent_id,
224
+ payment_method_display: t.payment_method_display,
225
+ created_at: t.created_at.toISOString()
226
+ })),
227
+ total_purchased: Math.abs(Number(purchaseAgg._sum.amount ?? 0)),
228
+ total_topped_up: Number(topupAgg._sum.amount ?? 0)
229
+ });
230
+ };
231
+ const balanceGET = async () => {
232
+ const r = await requireUser(auth, getOrCreateAppUser);
233
+ if ("res" in r) return r.res;
234
+ return import_server2.NextResponse.json({ credit_balance: Number(r.user.credit_balance) });
235
+ };
236
+ const transactionsGET = async (request) => {
237
+ const r = await requireUser(auth, getOrCreateAppUser);
238
+ if ("res" in r) return r.res;
239
+ const db = await getDb();
240
+ const url = new URL(request.url);
241
+ const limitParam = Number(url.searchParams.get("limit"));
242
+ const take = Number.isFinite(limitParam) && limitParam > 0 && limitParam <= 100 ? Math.floor(limitParam) : recentLimit;
243
+ const rows = await db.creditTransaction.findMany({
244
+ where: { user_id: r.user.id },
245
+ orderBy: { created_at: "desc" },
246
+ take
247
+ });
248
+ return import_server2.NextResponse.json({
249
+ items: rows.map((t) => ({
250
+ id: t.id.toString(),
251
+ type: t.type,
252
+ amount: Number(t.amount),
253
+ description: t.description,
254
+ stripe_payment_intent_id: t.stripe_payment_intent_id,
255
+ payment_method_display: t.payment_method_display,
256
+ created_at: t.created_at.toISOString()
257
+ }))
258
+ });
259
+ };
260
+ const paymentIntentPOST = async (request) => {
261
+ const r = await requireUser(auth, getOrCreateAppUser);
262
+ if ("res" in r) return r.res;
263
+ const body = await readJson(request, PaymentIntentBodySchema);
264
+ if ("res" in body) return body.res;
265
+ const { user } = r;
266
+ const { client: stripe, publishableKey } = await getStripe();
267
+ const { customerId, created } = await findOrCreateStripeCustomer(
268
+ {
269
+ existingId: user.stripe_customer_id,
270
+ email: user.email,
271
+ name: user.name,
272
+ appUserId: user.id.toString()
273
+ },
274
+ stripe
275
+ );
276
+ if (created) {
277
+ const db = await getDb();
278
+ await db.user.update({
279
+ where: { id: user.id },
280
+ data: { stripe_customer_id: customerId }
281
+ });
282
+ }
283
+ const intent = await stripe.paymentIntents.create({
284
+ amount: body.data.amountCents,
285
+ currency: "usd",
286
+ customer: customerId,
287
+ automatic_payment_methods: { enabled: true },
288
+ metadata: { app_user_id: user.id.toString(), kind: "credit_topup" }
289
+ });
290
+ return import_server2.NextResponse.json({
291
+ clientSecret: intent.client_secret,
292
+ publishableKey
293
+ });
294
+ };
295
+ const setupIntentPOST = async () => {
296
+ const r = await requireUser(auth, getOrCreateAppUser);
297
+ if ("res" in r) return r.res;
298
+ const { user } = r;
299
+ const { client: stripe, publishableKey } = await getStripe();
300
+ const { customerId, created } = await findOrCreateStripeCustomer(
301
+ {
302
+ existingId: user.stripe_customer_id,
303
+ email: user.email,
304
+ name: user.name,
305
+ appUserId: user.id.toString()
306
+ },
307
+ stripe
308
+ );
309
+ if (created) {
310
+ const db = await getDb();
311
+ await db.user.update({
312
+ where: { id: user.id },
313
+ data: { stripe_customer_id: customerId }
314
+ });
315
+ }
316
+ const intent = await stripe.setupIntents.create({
317
+ customer: customerId,
318
+ automatic_payment_methods: { enabled: true },
319
+ usage: "off_session",
320
+ metadata: { app_user_id: user.id.toString() }
321
+ });
322
+ return import_server2.NextResponse.json({
323
+ clientSecret: intent.client_secret,
324
+ publishableKey
325
+ });
326
+ };
327
+ const paymentMethodsListGET = async () => {
328
+ const r = await requireUser(auth, getOrCreateAppUser);
329
+ if ("res" in r) return r.res;
330
+ const db = await getDb();
331
+ const methods = await db.paymentMethod.findMany({
332
+ where: { user_id: r.user.id },
333
+ orderBy: [{ is_default: "desc" }, { created_at: "desc" }]
334
+ });
335
+ return import_server2.NextResponse.json({
336
+ items: methods.map((m) => ({
337
+ id: m.id.toString(),
338
+ stripe_payment_method_id: m.stripe_payment_method_id,
339
+ brand: m.brand,
340
+ last4: m.last4,
341
+ exp_month: m.exp_month,
342
+ exp_year: m.exp_year,
343
+ is_default: m.is_default,
344
+ created_at: m.created_at.toISOString()
345
+ }))
346
+ });
347
+ };
348
+ const paymentMethodDELETE = async (_request, context) => {
349
+ const r = await requireUser(auth, getOrCreateAppUser);
350
+ if ("res" in r) return r.res;
351
+ const { id } = await context.params;
352
+ const pk = parsePkId(id);
353
+ if (typeof pk !== "bigint") return pk.res;
354
+ const db = await getDb();
355
+ const row = await db.paymentMethod.findUnique({ where: { id: pk } });
356
+ if (!row || row.user_id !== r.user.id) {
357
+ return import_server2.NextResponse.json({ error: "not_found" }, { status: 404 });
358
+ }
359
+ const { client: stripe } = await getStripe();
360
+ await stripe.paymentMethods.detach(row.stripe_payment_method_id);
361
+ await db.paymentMethod.delete({ where: { id: pk } });
362
+ return import_server2.NextResponse.json({ ok: true });
363
+ };
364
+ const paymentMethodDefaultPATCH = async (_request, context) => {
365
+ const r = await requireUser(auth, getOrCreateAppUser);
366
+ if ("res" in r) return r.res;
367
+ const { id } = await context.params;
368
+ const pk = parsePkId(id);
369
+ if (typeof pk !== "bigint") return pk.res;
370
+ const db = await getDb();
371
+ const row = await db.paymentMethod.findUnique({ where: { id: pk } });
372
+ if (!row || row.user_id !== r.user.id) {
373
+ return import_server2.NextResponse.json({ error: "not_found" }, { status: 404 });
374
+ }
375
+ if (!r.user.stripe_customer_id) {
376
+ return import_server2.NextResponse.json({ error: "no_stripe_customer" }, { status: 409 });
377
+ }
378
+ const { client: stripe } = await getStripe();
379
+ await stripe.customers.update(r.user.stripe_customer_id, {
380
+ invoice_settings: { default_payment_method: row.stripe_payment_method_id }
381
+ });
382
+ await db.$transaction([
383
+ db.paymentMethod.updateMany({
384
+ where: { user_id: r.user.id, is_default: true },
385
+ data: { is_default: false }
386
+ }),
387
+ db.paymentMethod.update({ where: { id: pk }, data: { is_default: true } }),
388
+ db.user.update({
389
+ where: { id: r.user.id },
390
+ data: {
391
+ stripe_default_payment_method: row.stripe_payment_method_id,
392
+ stripe_default_payment_method_display: `${row.brand} \u2022\u2022\u2022\u2022 ${row.last4}`
393
+ }
394
+ })
395
+ ]);
396
+ return import_server2.NextResponse.json({ ok: true });
397
+ };
398
+ const autoRechargePATCH = async (request) => {
399
+ const r = await requireUser(auth, getOrCreateAppUser);
400
+ if ("res" in r) return r.res;
401
+ const body = await readJson(request, AutoRechargeBodySchema);
402
+ if ("res" in body) return body.res;
403
+ if (body.data.enabled && !r.user.stripe_default_payment_method) {
404
+ return import_server2.NextResponse.json(
405
+ {
406
+ error: "no_default_card",
407
+ message: "Save a card and mark it default before enabling auto-recharge."
408
+ },
409
+ { status: 409 }
410
+ );
411
+ }
412
+ const threshold = body.data.thresholdCents != null ? (body.data.thresholdCents / 100).toFixed(2) : null;
413
+ const amount = body.data.amountCents != null ? (body.data.amountCents / 100).toFixed(2) : null;
414
+ const db = await getDb();
415
+ const updated = await db.user.update({
416
+ where: { id: r.user.id },
417
+ data: {
418
+ auto_recharge_enabled: body.data.enabled,
419
+ auto_recharge_threshold: body.data.enabled ? threshold : null,
420
+ auto_recharge_amount: body.data.enabled ? amount : null
421
+ },
422
+ select: {
423
+ auto_recharge_enabled: true,
424
+ auto_recharge_threshold: true,
425
+ auto_recharge_amount: true
426
+ }
427
+ });
428
+ return import_server2.NextResponse.json({
429
+ enabled: updated.auto_recharge_enabled,
430
+ thresholdCents: updated.auto_recharge_threshold != null ? Math.round(Number(updated.auto_recharge_threshold) * 100) : null,
431
+ amountCents: updated.auto_recharge_amount != null ? Math.round(Number(updated.auto_recharge_amount) * 100) : null
432
+ });
433
+ };
434
+ const webhookPOST = async (request) => {
435
+ const rawBody = await request.text();
436
+ const signature = request.headers.get("stripe-signature");
437
+ if (!signature) {
438
+ return import_server2.NextResponse.json({ error: "missing_signature" }, { status: 400 });
439
+ }
440
+ const { client: stripe, webhookSecret } = await getStripe();
441
+ let event;
442
+ try {
443
+ event = stripe.webhooks.constructEvent(rawBody, signature, webhookSecret);
444
+ } catch (err) {
445
+ const msg = err instanceof Error ? err.message : String(err);
446
+ console.error("Stripe webhook signature verification failed:", msg);
447
+ return import_server2.NextResponse.json({ error: "bad_signature" }, { status: 400 });
448
+ }
449
+ try {
450
+ await dispatchWebhookEvent(event, getDb);
451
+ } catch (err) {
452
+ const msg = err instanceof Error ? err.message : String(err);
453
+ console.error(`Stripe webhook handler failed for ${event.type}:`, msg);
454
+ return import_server2.NextResponse.json({ error: "handler_failed" }, { status: 500 });
455
+ }
456
+ return import_server2.NextResponse.json({ received: true });
457
+ };
458
+ return {
459
+ aggregate: { GET: aggregateGET },
460
+ balance: { GET: balanceGET },
461
+ transactions: { GET: transactionsGET },
462
+ paymentIntent: { POST: paymentIntentPOST },
463
+ setupIntent: { POST: setupIntentPOST },
464
+ paymentMethods: { GET: paymentMethodsListGET },
465
+ paymentMethodById: { DELETE: paymentMethodDELETE },
466
+ paymentMethodDefault: { PATCH: paymentMethodDefaultPATCH },
467
+ autoRecharge: { PATCH: autoRechargePATCH },
468
+ webhook: { POST: webhookPOST }
469
+ };
470
+ }
471
+ async function dispatchWebhookEvent(event, getDb) {
472
+ const db = await getDb();
473
+ switch (event.type) {
474
+ case "payment_intent.succeeded": {
475
+ const intent = event.data.object;
476
+ await onPaymentIntentSucceeded(intent, db);
477
+ break;
478
+ }
479
+ case "payment_intent.payment_failed": {
480
+ const intent = event.data.object;
481
+ console.warn(
482
+ `Stripe payment_intent.payment_failed: id=${intent.id} user=${intent.metadata?.app_user_id} reason=${intent.last_payment_error?.message}`
483
+ );
484
+ break;
485
+ }
486
+ case "setup_intent.succeeded": {
487
+ const intent = event.data.object;
488
+ console.log(
489
+ `Stripe setup_intent.succeeded: id=${intent.id} pm=${intent.payment_method}`
490
+ );
491
+ break;
492
+ }
493
+ case "payment_method.attached": {
494
+ await onPaymentMethodAttached(event.data.object, db);
495
+ break;
496
+ }
497
+ case "payment_method.detached": {
498
+ await onPaymentMethodDetached(event.data.object, db);
499
+ break;
500
+ }
501
+ case "payment_method.updated": {
502
+ await onPaymentMethodUpdated(event.data.object, db);
503
+ break;
504
+ }
505
+ case "charge.refunded": {
506
+ await onChargeRefunded(event.data.object, db);
507
+ break;
508
+ }
509
+ default:
510
+ console.log(`Stripe webhook: unhandled event type ${event.type}`);
511
+ }
512
+ }
513
+ async function onPaymentIntentSucceeded(intent, db) {
514
+ const appUserIdStr = intent.metadata?.app_user_id;
515
+ const kind = intent.metadata?.kind;
516
+ if (!appUserIdStr || kind !== "credit_topup") return;
517
+ const userId = BigInt(appUserIdStr);
518
+ const dollars = (intent.amount / 100).toFixed(2);
519
+ try {
520
+ await db.$transaction([
521
+ db.creditTransaction.create({
522
+ data: {
523
+ user_id: userId,
524
+ type: "stripe_topup",
525
+ amount: dollars,
526
+ description: `Stripe credit top-up ($${dollars})`,
527
+ stripe_payment_intent_id: intent.id,
528
+ payment_method_display: typeof intent.payment_method === "string" ? intent.payment_method : null
529
+ }
530
+ }),
531
+ db.user.update({
532
+ where: { id: userId },
533
+ data: { credit_balance: { increment: dollars } }
534
+ })
535
+ ]);
536
+ } catch (err) {
537
+ const code = err.code;
538
+ if (code === "P2002") return;
539
+ throw err;
540
+ }
541
+ }
542
+ async function onPaymentMethodAttached(pm, db) {
543
+ if (pm.type !== "card" || !pm.card || !pm.customer) return;
544
+ const customerId = typeof pm.customer === "string" ? pm.customer : pm.customer.id;
545
+ const user = await db.user.findFirst({ where: { stripe_customer_id: customerId } });
546
+ if (!user) return;
547
+ await db.paymentMethod.upsert({
548
+ where: { stripe_payment_method_id: pm.id },
549
+ update: {
550
+ brand: pm.card.brand,
551
+ last4: pm.card.last4,
552
+ exp_month: pm.card.exp_month,
553
+ exp_year: pm.card.exp_year
554
+ },
555
+ create: {
556
+ user_id: user.id,
557
+ stripe_payment_method_id: pm.id,
558
+ brand: pm.card.brand,
559
+ last4: pm.card.last4,
560
+ exp_month: pm.card.exp_month,
561
+ exp_year: pm.card.exp_year,
562
+ is_default: false
563
+ }
564
+ });
565
+ }
566
+ async function onPaymentMethodDetached(pm, db) {
567
+ try {
568
+ await db.paymentMethod.delete({ where: { stripe_payment_method_id: pm.id } });
569
+ } catch (err) {
570
+ const code = err.code;
571
+ if (code === "P2025") return;
572
+ throw err;
573
+ }
574
+ }
575
+ async function onPaymentMethodUpdated(pm, db) {
576
+ if (pm.type !== "card" || !pm.card) return;
577
+ try {
578
+ await db.paymentMethod.update({
579
+ where: { stripe_payment_method_id: pm.id },
580
+ data: {
581
+ brand: pm.card.brand,
582
+ last4: pm.card.last4,
583
+ exp_month: pm.card.exp_month,
584
+ exp_year: pm.card.exp_year
585
+ }
586
+ });
587
+ } catch (err) {
588
+ const code = err.code;
589
+ if (code === "P2025") return;
590
+ throw err;
591
+ }
592
+ }
593
+ async function onChargeRefunded(charge, db) {
594
+ const intentId = typeof charge.payment_intent === "string" ? charge.payment_intent : charge.payment_intent?.id;
595
+ if (!intentId) return;
596
+ const topup = await db.creditTransaction.findUnique({
597
+ where: { stripe_payment_intent_id: intentId }
598
+ });
599
+ if (!topup) return;
600
+ const refundedAmount = (charge.amount_refunded / 100).toFixed(2);
601
+ await db.$transaction([
602
+ db.creditTransaction.create({
603
+ data: {
604
+ user_id: topup.user_id,
605
+ type: "refund_reversal",
606
+ amount: `-${refundedAmount}`,
607
+ description: `Refund reversal for ${intentId}`,
608
+ stripe_payment_intent_id: `${intentId}-refund-${charge.id}`
609
+ }
610
+ }),
611
+ db.user.update({
612
+ where: { id: topup.user_id },
613
+ data: { credit_balance: { decrement: refundedAmount } }
614
+ })
615
+ ]);
616
+ }
114
617
  // Annotate the CommonJS export names for ESM import in node:
115
618
  0 && (module.exports = {
619
+ createBillingHandlers,
116
620
  findOrCreateStripeCustomer,
117
621
  getStripe,
118
622
  verifyStripeWebhook