@xenterprises/fastify-xstripe 1.0.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/.dockerignore +62 -0
- package/.env.example +116 -0
- package/API.md +574 -0
- package/CHANGELOG.md +96 -0
- package/EXAMPLES.md +883 -0
- package/LICENSE +15 -0
- package/MIGRATION.md +374 -0
- package/QUICK_START.md +179 -0
- package/README.md +331 -0
- package/SECURITY.md +465 -0
- package/TESTING.md +357 -0
- package/index.d.ts +309 -0
- package/package.json +53 -0
- package/server/app.js +557 -0
- package/src/handlers/defaultHandlers.js +355 -0
- package/src/handlers/exampleHandlers.js +278 -0
- package/src/handlers/index.js +8 -0
- package/src/index.js +10 -0
- package/src/utils/helpers.js +220 -0
- package/src/webhooks/webhooks.js +72 -0
- package/src/xStripe.js +45 -0
- package/test/handlers.test.js +959 -0
- package/test/xStripe.integration.test.js +409 -0
package/server/app.js
ADDED
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
// server/app.js - Example Fastify server with xStripe
|
|
2
|
+
import Fastify from 'fastify';
|
|
3
|
+
import xStripe from '../src/xStripe.js';
|
|
4
|
+
|
|
5
|
+
const fastify = Fastify({
|
|
6
|
+
logger: true,
|
|
7
|
+
// Enable raw body for webhook signature verification
|
|
8
|
+
rawBody: true,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
// Register xStripe with custom handlers
|
|
12
|
+
await fastify.register(xStripe, {
|
|
13
|
+
apiKey: process.env.STRIPE_API_KEY,
|
|
14
|
+
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
|
|
15
|
+
webhookPath: '/stripe/webhook',
|
|
16
|
+
handlers: {
|
|
17
|
+
// Custom handler for new subscriptions
|
|
18
|
+
'customer.subscription.created': async (event, fastify, stripe) => {
|
|
19
|
+
const subscription = event.data.object;
|
|
20
|
+
|
|
21
|
+
fastify.log.info({
|
|
22
|
+
msg: '🎉 New subscription!',
|
|
23
|
+
customerId: subscription.customer,
|
|
24
|
+
subscriptionId: subscription.id,
|
|
25
|
+
plan: subscription.items.data[0]?.price.product,
|
|
26
|
+
status: subscription.status,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Example: Update database (uncomment if using Prisma)
|
|
30
|
+
// await fastify.prisma.user.update({
|
|
31
|
+
// where: { stripeCustomerId: subscription.customer },
|
|
32
|
+
// data: {
|
|
33
|
+
// subscriptionId: subscription.id,
|
|
34
|
+
// subscriptionStatus: subscription.status,
|
|
35
|
+
// planId: subscription.items.data[0]?.price.id,
|
|
36
|
+
// trialEnd: subscription.trial_end
|
|
37
|
+
// ? new Date(subscription.trial_end * 1000)
|
|
38
|
+
// : null,
|
|
39
|
+
// },
|
|
40
|
+
// });
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
// Custom handler for subscription updates
|
|
44
|
+
'customer.subscription.updated': async (event, fastify, stripe) => {
|
|
45
|
+
const subscription = event.data.object;
|
|
46
|
+
const previous = event.data.previous_attributes || {};
|
|
47
|
+
|
|
48
|
+
if ('status' in previous) {
|
|
49
|
+
fastify.log.info({
|
|
50
|
+
msg: 'Subscription status changed',
|
|
51
|
+
subscriptionId: subscription.id,
|
|
52
|
+
oldStatus: previous.status,
|
|
53
|
+
newStatus: subscription.status,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Handle specific status transitions
|
|
57
|
+
if (subscription.status === 'active' && previous.status === 'trialing') {
|
|
58
|
+
fastify.log.info('✅ Trial converted to paid subscription');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (subscription.status === 'past_due') {
|
|
62
|
+
fastify.log.warn('⚠️ Subscription past due - payment failed');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (subscription.status === 'canceled') {
|
|
66
|
+
fastify.log.info('❌ Subscription canceled');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
// Custom handler for failed payments
|
|
72
|
+
'invoice.payment_failed': async (event, fastify, stripe) => {
|
|
73
|
+
const invoice = event.data.object;
|
|
74
|
+
|
|
75
|
+
fastify.log.error({
|
|
76
|
+
msg: '💳 Payment failed',
|
|
77
|
+
invoiceId: invoice.id,
|
|
78
|
+
customerId: invoice.customer,
|
|
79
|
+
amount: invoice.amount_due / 100,
|
|
80
|
+
attemptCount: invoice.attempt_count,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Example: Track failed payments
|
|
84
|
+
// await fastify.prisma.user.update({
|
|
85
|
+
// where: { stripeCustomerId: invoice.customer },
|
|
86
|
+
// data: {
|
|
87
|
+
// failedPaymentCount: { increment: 1 },
|
|
88
|
+
// lastFailedPayment: new Date(),
|
|
89
|
+
// },
|
|
90
|
+
// });
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
// Custom handler for successful payments
|
|
94
|
+
'invoice.paid': async (event, fastify, stripe) => {
|
|
95
|
+
const invoice = event.data.object;
|
|
96
|
+
|
|
97
|
+
fastify.log.info({
|
|
98
|
+
msg: '✅ Payment successful',
|
|
99
|
+
invoiceId: invoice.id,
|
|
100
|
+
customerId: invoice.customer,
|
|
101
|
+
amount: invoice.amount_paid / 100,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Example: Reset failed payment count
|
|
105
|
+
// await fastify.prisma.user.update({
|
|
106
|
+
// where: { stripeCustomerId: invoice.customer },
|
|
107
|
+
// data: {
|
|
108
|
+
// failedPaymentCount: 0,
|
|
109
|
+
// lastSuccessfulPayment: new Date(),
|
|
110
|
+
// },
|
|
111
|
+
// });
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// ============================================================================
|
|
117
|
+
// Example API Routes - Subscription Management
|
|
118
|
+
// ============================================================================
|
|
119
|
+
|
|
120
|
+
// Create subscription checkout session
|
|
121
|
+
fastify.post('/create-checkout-session', async (request, reply) => {
|
|
122
|
+
const { priceId, customerId } = request.body;
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const session = await fastify.stripe.checkout.sessions.create({
|
|
126
|
+
customer: customerId,
|
|
127
|
+
mode: 'subscription',
|
|
128
|
+
line_items: [{ price: priceId, quantity: 1 }],
|
|
129
|
+
success_url: `${process.env.DOMAIN}/success?session_id={CHECKOUT_SESSION_ID}`,
|
|
130
|
+
cancel_url: `${process.env.DOMAIN}/cancel`,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return { sessionId: session.id, url: session.url };
|
|
134
|
+
} catch (error) {
|
|
135
|
+
fastify.log.error(error);
|
|
136
|
+
return reply.code(500).send({ error: error.message });
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Create customer
|
|
141
|
+
fastify.post('/create-customer', async (request, reply) => {
|
|
142
|
+
const { email, name } = request.body;
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const customer = await fastify.stripe.customers.create({
|
|
146
|
+
email,
|
|
147
|
+
name,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return { customerId: customer.id };
|
|
151
|
+
} catch (error) {
|
|
152
|
+
fastify.log.error(error);
|
|
153
|
+
return reply.code(500).send({ error: error.message });
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Get subscription status
|
|
158
|
+
fastify.get('/subscription/:id', async (request, reply) => {
|
|
159
|
+
const { id } = request.params;
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const subscription = await fastify.stripe.subscriptions.retrieve(id);
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
id: subscription.id,
|
|
166
|
+
status: subscription.status,
|
|
167
|
+
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
|
|
168
|
+
cancelAtPeriodEnd: subscription.cancel_at_period_end,
|
|
169
|
+
};
|
|
170
|
+
} catch (error) {
|
|
171
|
+
fastify.log.error(error);
|
|
172
|
+
return reply.code(404).send({ error: 'Subscription not found' });
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Cancel subscription
|
|
177
|
+
fastify.post('/cancel-subscription/:id', async (request, reply) => {
|
|
178
|
+
const { id } = request.params;
|
|
179
|
+
const { immediately = false } = request.body;
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
let subscription;
|
|
183
|
+
|
|
184
|
+
if (immediately) {
|
|
185
|
+
subscription = await fastify.stripe.subscriptions.cancel(id);
|
|
186
|
+
} else {
|
|
187
|
+
subscription = await fastify.stripe.subscriptions.update(id, {
|
|
188
|
+
cancel_at_period_end: true,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return { subscriptionId: subscription.id, status: subscription.status };
|
|
193
|
+
} catch (error) {
|
|
194
|
+
fastify.log.error(error);
|
|
195
|
+
return reply.code(500).send({ error: error.message });
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// ============================================================================
|
|
200
|
+
// FEATURE 1: Plan Listing
|
|
201
|
+
// ============================================================================
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* GET /plans
|
|
205
|
+
* List all available products and their prices
|
|
206
|
+
* @returns Array of products with pricing information
|
|
207
|
+
*/
|
|
208
|
+
fastify.get('/plans', async (request, reply) => { // request unused but available for future pagination params
|
|
209
|
+
try {
|
|
210
|
+
// Get all products
|
|
211
|
+
const products = await fastify.stripe.products.list({
|
|
212
|
+
active: true,
|
|
213
|
+
limit: 100,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Get prices for each product
|
|
217
|
+
const plans = await Promise.all(
|
|
218
|
+
products.data.map(async (product) => {
|
|
219
|
+
const prices = await fastify.stripe.prices.list({
|
|
220
|
+
product: product.id,
|
|
221
|
+
active: true,
|
|
222
|
+
type: 'recurring',
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
productId: product.id,
|
|
227
|
+
name: product.name,
|
|
228
|
+
description: product.description,
|
|
229
|
+
images: product.images,
|
|
230
|
+
metadata: product.metadata,
|
|
231
|
+
prices: prices.data.map((price) => ({
|
|
232
|
+
priceId: price.id,
|
|
233
|
+
amount: price.unit_amount / 100, // Convert cents to dollars
|
|
234
|
+
currency: price.currency.toUpperCase(),
|
|
235
|
+
interval: price.recurring?.interval,
|
|
236
|
+
intervalCount: price.recurring?.interval_count || 1,
|
|
237
|
+
trialPeriodDays: price.recurring?.trial_period_days,
|
|
238
|
+
nickname: price.nickname,
|
|
239
|
+
metadata: price.metadata,
|
|
240
|
+
})),
|
|
241
|
+
};
|
|
242
|
+
})
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
return { plans };
|
|
246
|
+
} catch (error) {
|
|
247
|
+
fastify.log.error(error);
|
|
248
|
+
return reply.code(500).send({ error: error.message });
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* GET /plans/:productId
|
|
254
|
+
* Get a specific product with all its prices
|
|
255
|
+
*/
|
|
256
|
+
fastify.get('/plans/:productId', async (request, reply) => {
|
|
257
|
+
const { productId } = request.params;
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
const product = await fastify.stripe.products.retrieve(productId);
|
|
261
|
+
const prices = await fastify.stripe.prices.list({
|
|
262
|
+
product: productId,
|
|
263
|
+
active: true,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
productId: product.id,
|
|
268
|
+
name: product.name,
|
|
269
|
+
description: product.description,
|
|
270
|
+
images: product.images,
|
|
271
|
+
metadata: product.metadata,
|
|
272
|
+
prices: prices.data.map((price) => ({
|
|
273
|
+
priceId: price.id,
|
|
274
|
+
amount: price.unit_amount / 100,
|
|
275
|
+
currency: price.currency.toUpperCase(),
|
|
276
|
+
interval: price.recurring?.interval,
|
|
277
|
+
intervalCount: price.recurring?.interval_count || 1,
|
|
278
|
+
trialPeriodDays: price.recurring?.trial_period_days,
|
|
279
|
+
nickname: price.nickname,
|
|
280
|
+
})),
|
|
281
|
+
};
|
|
282
|
+
} catch (error) {
|
|
283
|
+
fastify.log.error(error);
|
|
284
|
+
return reply.code(404).send({ error: 'Product not found' });
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// ============================================================================
|
|
289
|
+
// FEATURE 2: One-Time Payments
|
|
290
|
+
// ============================================================================
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* POST /create-payment-session
|
|
294
|
+
* Create a checkout session for one-time payment
|
|
295
|
+
* @body { customerId, priceId, quantity, metadata }
|
|
296
|
+
*/
|
|
297
|
+
fastify.post('/create-payment-session', async (request, reply) => {
|
|
298
|
+
const { customerId, priceId, quantity = 1, metadata = {} } = request.body;
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
const session = await fastify.stripe.checkout.sessions.create({
|
|
302
|
+
customer: customerId,
|
|
303
|
+
mode: 'payment',
|
|
304
|
+
line_items: [
|
|
305
|
+
{
|
|
306
|
+
price: priceId,
|
|
307
|
+
quantity,
|
|
308
|
+
},
|
|
309
|
+
],
|
|
310
|
+
success_url: `${process.env.DOMAIN}/success?session_id={CHECKOUT_SESSION_ID}`,
|
|
311
|
+
cancel_url: `${process.env.DOMAIN}/cancel`,
|
|
312
|
+
metadata,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
return { sessionId: session.id, url: session.url };
|
|
316
|
+
} catch (error) {
|
|
317
|
+
fastify.log.error(error);
|
|
318
|
+
return reply.code(500).send({ error: error.message });
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// ============================================================================
|
|
323
|
+
// FEATURE 3: Subscription Listing & Management
|
|
324
|
+
// ============================================================================
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* GET /customer/:customerId/subscriptions
|
|
328
|
+
* List all subscriptions for a customer
|
|
329
|
+
*/
|
|
330
|
+
fastify.get('/customer/:customerId/subscriptions', async (request, reply) => {
|
|
331
|
+
const { customerId } = request.params;
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
const subscriptions = await fastify.stripe.subscriptions.list({
|
|
335
|
+
customer: customerId,
|
|
336
|
+
limit: 100,
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
subscriptions: subscriptions.data.map((sub) => ({
|
|
341
|
+
id: sub.id,
|
|
342
|
+
status: sub.status,
|
|
343
|
+
planId: sub.items.data[0]?.price.id,
|
|
344
|
+
planName: sub.items.data[0]?.price.nickname,
|
|
345
|
+
amount: sub.items.data[0]?.price.unit_amount / 100,
|
|
346
|
+
currency: sub.items.data[0]?.price.currency.toUpperCase(),
|
|
347
|
+
interval: sub.items.data[0]?.price.recurring?.interval,
|
|
348
|
+
createdAt: new Date(sub.created * 1000),
|
|
349
|
+
currentPeriodEnd: new Date(sub.current_period_end * 1000),
|
|
350
|
+
cancelAtPeriodEnd: sub.cancel_at_period_end,
|
|
351
|
+
canceledAt: sub.canceled_at ? new Date(sub.canceled_at * 1000) : null,
|
|
352
|
+
})),
|
|
353
|
+
total: subscriptions.data.length,
|
|
354
|
+
};
|
|
355
|
+
} catch (error) {
|
|
356
|
+
fastify.log.error(error);
|
|
357
|
+
return reply.code(500).send({ error: error.message });
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* POST /subscription/:id/update
|
|
363
|
+
* Update a subscription (change plan, apply coupon, etc.)
|
|
364
|
+
* @body { priceId, quantity, coupon, metadata, prorationBehavior }
|
|
365
|
+
*/
|
|
366
|
+
fastify.post('/subscription/:id/update', async (request, reply) => {
|
|
367
|
+
const { id } = request.params;
|
|
368
|
+
const {
|
|
369
|
+
priceId,
|
|
370
|
+
quantity = 1,
|
|
371
|
+
coupon,
|
|
372
|
+
metadata,
|
|
373
|
+
prorationBehavior = 'create_prorations',
|
|
374
|
+
} = request.body;
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
const subscription = await fastify.stripe.subscriptions.retrieve(id);
|
|
378
|
+
const itemId = subscription.items.data[0]?.id;
|
|
379
|
+
|
|
380
|
+
const updateData = {
|
|
381
|
+
items: [
|
|
382
|
+
{
|
|
383
|
+
id: itemId,
|
|
384
|
+
price: priceId,
|
|
385
|
+
quantity,
|
|
386
|
+
},
|
|
387
|
+
],
|
|
388
|
+
proration_behavior: prorationBehavior, // 'create_prorations', 'none', 'always_invoice'
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
if (coupon) {
|
|
392
|
+
updateData.coupon = coupon;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (metadata) {
|
|
396
|
+
updateData.metadata = metadata;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const updated = await fastify.stripe.subscriptions.update(id, updateData);
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
id: updated.id,
|
|
403
|
+
status: updated.status,
|
|
404
|
+
priceId: updated.items.data[0]?.price.id,
|
|
405
|
+
amount: updated.items.data[0]?.price.unit_amount / 100,
|
|
406
|
+
nextBillingDate: new Date(updated.current_period_end * 1000),
|
|
407
|
+
message: 'Subscription updated successfully',
|
|
408
|
+
};
|
|
409
|
+
} catch (error) {
|
|
410
|
+
fastify.log.error(error);
|
|
411
|
+
return reply.code(500).send({ error: error.message });
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
// ============================================================================
|
|
416
|
+
// FEATURE 4: Payment Methods Management
|
|
417
|
+
// ============================================================================
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* GET /customer/:customerId/payment-methods
|
|
421
|
+
* List all payment methods for a customer
|
|
422
|
+
*/
|
|
423
|
+
fastify.get('/customer/:customerId/payment-methods', async (request, reply) => {
|
|
424
|
+
const { customerId } = request.params;
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
const paymentMethods = await fastify.stripe.paymentMethods.list({
|
|
428
|
+
customer: customerId,
|
|
429
|
+
type: 'card',
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// Get default payment method from customer
|
|
433
|
+
const customer = await fastify.stripe.customers.retrieve(customerId);
|
|
434
|
+
|
|
435
|
+
return {
|
|
436
|
+
paymentMethods: paymentMethods.data.map((pm) => ({
|
|
437
|
+
id: pm.id,
|
|
438
|
+
type: pm.type,
|
|
439
|
+
isDefault: pm.id === customer.invoice_settings?.default_payment_method,
|
|
440
|
+
card: {
|
|
441
|
+
brand: pm.card?.brand,
|
|
442
|
+
last4: pm.card?.last4,
|
|
443
|
+
expMonth: pm.card?.exp_month,
|
|
444
|
+
expYear: pm.card?.exp_year,
|
|
445
|
+
},
|
|
446
|
+
billingDetails: pm.billing_details,
|
|
447
|
+
createdAt: new Date(pm.created * 1000),
|
|
448
|
+
})),
|
|
449
|
+
defaultPaymentMethodId: customer.invoice_settings?.default_payment_method,
|
|
450
|
+
total: paymentMethods.data.length,
|
|
451
|
+
};
|
|
452
|
+
} catch (error) {
|
|
453
|
+
fastify.log.error(error);
|
|
454
|
+
return reply.code(500).send({ error: error.message });
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* POST /customer/:customerId/payment-methods
|
|
460
|
+
* Add a new payment method to a customer (requires payment method ID from frontend)
|
|
461
|
+
* @body { paymentMethodId }
|
|
462
|
+
*/
|
|
463
|
+
fastify.post('/customer/:customerId/payment-methods', async (request, reply) => {
|
|
464
|
+
const { customerId } = request.params;
|
|
465
|
+
const { paymentMethodId } = request.body;
|
|
466
|
+
|
|
467
|
+
try {
|
|
468
|
+
// Attach payment method to customer
|
|
469
|
+
await fastify.stripe.paymentMethods.attach(paymentMethodId, {
|
|
470
|
+
customer: customerId,
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
const paymentMethod = await fastify.stripe.paymentMethods.retrieve(
|
|
474
|
+
paymentMethodId
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
return {
|
|
478
|
+
id: paymentMethod.id,
|
|
479
|
+
type: paymentMethod.type,
|
|
480
|
+
card: {
|
|
481
|
+
brand: paymentMethod.card?.brand,
|
|
482
|
+
last4: paymentMethod.card?.last4,
|
|
483
|
+
expMonth: paymentMethod.card?.exp_month,
|
|
484
|
+
expYear: paymentMethod.card?.exp_year,
|
|
485
|
+
},
|
|
486
|
+
message: 'Payment method added successfully',
|
|
487
|
+
};
|
|
488
|
+
} catch (error) {
|
|
489
|
+
fastify.log.error(error);
|
|
490
|
+
return reply.code(500).send({ error: error.message });
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* POST /customer/:customerId/payment-methods/:paymentMethodId/default
|
|
496
|
+
* Set a payment method as default for a customer
|
|
497
|
+
*/
|
|
498
|
+
fastify.post(
|
|
499
|
+
'/customer/:customerId/payment-methods/:paymentMethodId/default',
|
|
500
|
+
async (request, reply) => {
|
|
501
|
+
const { customerId, paymentMethodId } = request.params;
|
|
502
|
+
|
|
503
|
+
try {
|
|
504
|
+
const customer = await fastify.stripe.customers.update(customerId, {
|
|
505
|
+
invoice_settings: {
|
|
506
|
+
default_payment_method: paymentMethodId,
|
|
507
|
+
},
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
return {
|
|
511
|
+
customerId: customer.id,
|
|
512
|
+
defaultPaymentMethodId: customer.invoice_settings?.default_payment_method,
|
|
513
|
+
message: 'Default payment method updated successfully',
|
|
514
|
+
};
|
|
515
|
+
} catch (error) {
|
|
516
|
+
fastify.log.error(error);
|
|
517
|
+
return reply.code(500).send({ error: error.message });
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* DELETE /customer/:customerId/payment-methods/:paymentMethodId
|
|
524
|
+
* Remove a payment method from a customer
|
|
525
|
+
*/
|
|
526
|
+
fastify.delete(
|
|
527
|
+
'/customer/:customerId/payment-methods/:paymentMethodId',
|
|
528
|
+
async (request, reply) => {
|
|
529
|
+
const { paymentMethodId } = request.params;
|
|
530
|
+
|
|
531
|
+
try {
|
|
532
|
+
await fastify.stripe.paymentMethods.detach(paymentMethodId);
|
|
533
|
+
|
|
534
|
+
return {
|
|
535
|
+
paymentMethodId,
|
|
536
|
+
message: 'Payment method removed successfully',
|
|
537
|
+
};
|
|
538
|
+
} catch (error) {
|
|
539
|
+
fastify.log.error(error);
|
|
540
|
+
return reply.code(500).send({ error: error.message });
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
// ============================================================================
|
|
546
|
+
// Health Check
|
|
547
|
+
// ============================================================================
|
|
548
|
+
|
|
549
|
+
fastify.get('/health', async () => {
|
|
550
|
+
return {
|
|
551
|
+
status: 'ok',
|
|
552
|
+
stripe: !!fastify.stripe,
|
|
553
|
+
timestamp: new Date().toISOString(),
|
|
554
|
+
};
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
export default fastify;
|