@augmenting-integrations/billing 8.0.5 → 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.
- package/dist/server/handlers.d.ts +136 -0
- package/dist/server/handlers.d.ts.map +1 -0
- package/dist/server.cjs +504 -0
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +503 -0
- package/dist/server.js.map +1 -1
- package/package.json +9 -6
|
@@ -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
|