@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/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2024 Tim Mushen
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/MIGRATION.md ADDED
@@ -0,0 +1,374 @@
1
+ # Migration Guide
2
+
3
+ How to migrate from raw Stripe webhook handling to xStripe.
4
+
5
+ ## Before: Raw Webhook Handling
6
+
7
+ ### Typical raw implementation:
8
+
9
+ ```javascript
10
+ // Before - Manual webhook handling
11
+ import Stripe from 'stripe';
12
+
13
+ const stripe = new Stripe(process.env.STRIPE_API_KEY);
14
+
15
+ fastify.post('/webhook', async (request, reply) => {
16
+ const sig = request.headers['stripe-signature'];
17
+ let event;
18
+
19
+ try {
20
+ event = stripe.webhooks.constructEvent(
21
+ request.rawBody,
22
+ sig,
23
+ process.env.STRIPE_WEBHOOK_SECRET
24
+ );
25
+ } catch (err) {
26
+ return reply.code(400).send(`Webhook Error: ${err.message}`);
27
+ }
28
+
29
+ // Handle the event
30
+ switch (event.type) {
31
+ case 'customer.subscription.created':
32
+ const subscription = event.data.object;
33
+
34
+ try {
35
+ await prisma.user.update({
36
+ where: { stripeCustomerId: subscription.customer },
37
+ data: {
38
+ subscriptionId: subscription.id,
39
+ subscriptionStatus: subscription.status,
40
+ planId: subscription.items.data[0]?.price.id,
41
+ },
42
+ });
43
+
44
+ console.log('Subscription created:', subscription.id);
45
+ } catch (error) {
46
+ console.error('Error processing subscription:', error);
47
+ }
48
+ break;
49
+
50
+ case 'invoice.payment_failed':
51
+ const invoice = event.data.object;
52
+
53
+ try {
54
+ const customer = await stripe.customers.retrieve(invoice.customer);
55
+
56
+ await sendEmail(
57
+ customer.email,
58
+ 'Payment Failed',
59
+ 'Please update your payment method'
60
+ );
61
+
62
+ await prisma.user.update({
63
+ where: { stripeCustomerId: invoice.customer },
64
+ data: { failedPaymentCount: { increment: 1 } },
65
+ });
66
+
67
+ console.log('Payment failed for:', invoice.customer);
68
+ } catch (error) {
69
+ console.error('Error handling payment failure:', error);
70
+ }
71
+ break;
72
+
73
+ case 'customer.subscription.deleted':
74
+ const deletedSub = event.data.object;
75
+
76
+ try {
77
+ await prisma.user.update({
78
+ where: { stripeSubscriptionId: deletedSub.id },
79
+ data: {
80
+ subscriptionStatus: 'canceled',
81
+ hasAccess: false,
82
+ },
83
+ });
84
+
85
+ console.log('Subscription canceled:', deletedSub.id);
86
+ } catch (error) {
87
+ console.error('Error canceling subscription:', error);
88
+ }
89
+ break;
90
+
91
+ // ... 20+ more event types
92
+ default:
93
+ console.log(`Unhandled event type: ${event.type}`);
94
+ }
95
+
96
+ reply.send({ received: true });
97
+ });
98
+ ```
99
+
100
+ ### Problems with this approach:
101
+
102
+ 1. ❌ **Hundreds of lines in one file** - Hard to read and maintain
103
+ 2. ❌ **Difficult to test** - Everything coupled together
104
+ 3. ❌ **Error handling scattered** - Try/catch everywhere
105
+ 4. ❌ **No separation of concerns** - Business logic mixed with webhook handling
106
+ 5. ❌ **Hard to debug** - Console.log statements everywhere
107
+ 6. ❌ **Not reusable** - Can't share handler logic
108
+
109
+ ## After: xStripe
110
+
111
+ ### Same functionality with xStripe:
112
+
113
+ ```javascript
114
+ // After - Clean, testable handlers
115
+ import xStripe from '@xenterprises/fastify-xstripe';
116
+
117
+ await fastify.register(xStripe, {
118
+ apiKey: process.env.STRIPE_API_KEY,
119
+ webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
120
+ handlers: {
121
+ 'customer.subscription.created': async (event, fastify, stripe) => {
122
+ const subscription = event.data.object;
123
+
124
+ await fastify.prisma.user.update({
125
+ where: { stripeCustomerId: subscription.customer },
126
+ data: {
127
+ subscriptionId: subscription.id,
128
+ subscriptionStatus: subscription.status,
129
+ planId: subscription.items.data[0]?.price.id,
130
+ },
131
+ });
132
+
133
+ fastify.log.info('Subscription created:', subscription.id);
134
+ },
135
+
136
+ 'invoice.payment_failed': async (event, fastify, stripe) => {
137
+ const invoice = event.data.object;
138
+ const customer = await stripe.customers.retrieve(invoice.customer);
139
+
140
+ await fastify.email.send(
141
+ customer.email,
142
+ 'Payment Failed',
143
+ 'Please update your payment method'
144
+ );
145
+
146
+ await fastify.prisma.user.update({
147
+ where: { stripeCustomerId: invoice.customer },
148
+ data: { failedPaymentCount: { increment: 1 } },
149
+ });
150
+
151
+ fastify.log.info('Payment failed for:', invoice.customer);
152
+ },
153
+
154
+ 'customer.subscription.deleted': async (event, fastify, stripe) => {
155
+ const subscription = event.data.object;
156
+
157
+ await fastify.prisma.user.update({
158
+ where: { stripeSubscriptionId: subscription.id },
159
+ data: {
160
+ subscriptionStatus: 'canceled',
161
+ hasAccess: false,
162
+ },
163
+ });
164
+
165
+ fastify.log.info('Subscription canceled:', subscription.id);
166
+ },
167
+ },
168
+ });
169
+ ```
170
+
171
+ ### Benefits:
172
+
173
+ 1. ✅ **Clean, readable code** - Each handler is self-contained
174
+ 2. ✅ **Easy to test** - Pure functions with clear inputs/outputs
175
+ 3. ✅ **Error handling built-in** - Automatic error catching and logging
176
+ 4. ✅ **Separation of concerns** - Business logic separate from webhook mechanics
177
+ 5. ✅ **Better debugging** - Structured logging with Fastify
178
+ 6. ✅ **Reusable** - Can extract handlers to separate files
179
+
180
+ ## Step-by-Step Migration
181
+
182
+ ### Step 1: Install xStripe
183
+
184
+ ```bash
185
+ npm install @xenterprises/fastify-xstripe
186
+ ```
187
+
188
+ ### Step 2: Extract Your Handlers
189
+
190
+ Create a file for your handlers:
191
+
192
+ ```javascript
193
+ // handlers/stripeHandlers.js
194
+ export const stripeHandlers = {
195
+ 'customer.subscription.created': async (event, fastify, stripe) => {
196
+ // Copy your logic from the switch case
197
+ },
198
+
199
+ 'invoice.payment_failed': async (event, fastify, stripe) => {
200
+ // Copy your logic from the switch case
201
+ },
202
+
203
+ // ... other handlers
204
+ };
205
+ ```
206
+
207
+ ### Step 3: Register xStripe
208
+
209
+ ```javascript
210
+ import xStripe from '@xenterprises/fastify-xstripe';
211
+ import { stripeHandlers } from './handlers/stripeHandlers.js';
212
+
213
+ await fastify.register(xStripe, {
214
+ apiKey: process.env.STRIPE_API_KEY,
215
+ webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
216
+ handlers: stripeHandlers,
217
+ });
218
+ ```
219
+
220
+ ### Step 4: Remove Old Webhook Route
221
+
222
+ Delete your old `/webhook` endpoint - xStripe handles this now.
223
+
224
+ ### Step 5: Update Stripe Dashboard
225
+
226
+ Update your webhook endpoint URL if it changed:
227
+ - Old: `https://yourdomain.com/webhook`
228
+ - New: `https://yourdomain.com/stripe/webhook` (or custom path)
229
+
230
+ ### Step 6: Test
231
+
232
+ ```bash
233
+ # Test with Stripe CLI
234
+ stripe listen --forward-to localhost:3000/stripe/webhook
235
+ stripe trigger customer.subscription.created
236
+ ```
237
+
238
+ ## Migration Checklist
239
+
240
+ - [ ] Install xStripe package
241
+ - [ ] Create handlers file
242
+ - [ ] Move business logic to handlers
243
+ - [ ] Register xStripe plugin
244
+ - [ ] Remove old webhook endpoint
245
+ - [ ] Update Stripe webhook URL
246
+ - [ ] Test all event types
247
+ - [ ] Update environment variables
248
+ - [ ] Deploy to production
249
+ - [ ] Monitor webhook deliveries
250
+
251
+ ## Common Migration Issues
252
+
253
+ ### Issue: Signature Verification Fails
254
+
255
+ **Before:**
256
+ ```javascript
257
+ const event = stripe.webhooks.constructEvent(request.rawBody, sig, secret);
258
+ ```
259
+
260
+ **After:**
261
+ xStripe handles this automatically. Make sure:
262
+ 1. `webhookSecret` is set correctly
263
+ 2. Fastify has `rawBody: true` option
264
+
265
+ ### Issue: Can't Access Database
266
+
267
+ **Before:**
268
+ ```javascript
269
+ const prisma = new PrismaClient();
270
+ await prisma.user.update(...);
271
+ ```
272
+
273
+ **After:**
274
+ Use Fastify decorator:
275
+ ```javascript
276
+ await fastify.prisma.user.update(...);
277
+ ```
278
+
279
+ ### Issue: Custom Logging
280
+
281
+ **Before:**
282
+ ```javascript
283
+ console.log('Subscription created');
284
+ ```
285
+
286
+ **After:**
287
+ Use Fastify logger:
288
+ ```javascript
289
+ fastify.log.info('Subscription created');
290
+ ```
291
+
292
+ ### Issue: Accessing Stripe Client
293
+
294
+ **Before:**
295
+ ```javascript
296
+ const stripe = new Stripe(apiKey);
297
+ const customer = await stripe.customers.retrieve(id);
298
+ ```
299
+
300
+ **After:**
301
+ Stripe client is passed to handler:
302
+ ```javascript
303
+ 'my.event': async (event, fastify, stripe) => {
304
+ const customer = await stripe.customers.retrieve(id);
305
+ }
306
+ ```
307
+
308
+ ## Gradual Migration
309
+
310
+ You can migrate gradually by handling some events with xStripe and others with your old code:
311
+
312
+ ```javascript
313
+ // Keep old endpoint for some events
314
+ fastify.post('/old-webhook', oldWebhookHandler);
315
+
316
+ // Use xStripe for new events
317
+ await fastify.register(xStripe, {
318
+ apiKey: process.env.STRIPE_API_KEY,
319
+ webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
320
+ webhookPath: '/new-webhook',
321
+ handlers: {
322
+ 'customer.subscription.created': newHandler,
323
+ },
324
+ });
325
+ ```
326
+
327
+ Then gradually move handlers over and update Stripe webhook configuration.
328
+
329
+ ## Testing After Migration
330
+
331
+ 1. **Test locally with Stripe CLI**
332
+ ```bash
333
+ stripe listen --forward-to localhost:3000/stripe/webhook
334
+ ```
335
+
336
+ 2. **Trigger test events**
337
+ ```bash
338
+ stripe trigger customer.subscription.created
339
+ stripe trigger invoice.payment_failed
340
+ ```
341
+
342
+ 3. **Check logs**
343
+ ```bash
344
+ # Should see structured logs from Fastify
345
+ {"level":30,"msg":"Received Stripe webhook: customer.subscription.created"}
346
+ {"level":30,"msg":"Successfully processed customer.subscription.created"}
347
+ ```
348
+
349
+ 4. **Verify database changes**
350
+ - Check that database updates happen correctly
351
+ - Verify emails are sent
352
+ - Confirm business logic runs
353
+
354
+ ## Rollback Plan
355
+
356
+ If issues arise, you can quickly rollback:
357
+
358
+ 1. Keep old webhook handler in a separate file
359
+ 2. Comment out xStripe registration
360
+ 3. Re-enable old endpoint
361
+ 4. Update Stripe webhook URL back
362
+
363
+ ```javascript
364
+ // Rollback: Re-enable old handler
365
+ // await fastify.register(xStripe, { ... });
366
+ fastify.post('/webhook', oldWebhookHandler);
367
+ ```
368
+
369
+ ## Need Help?
370
+
371
+ - Check the [README](./README.md) for full documentation
372
+ - See [TESTING.md](./TESTING.md) for testing guide
373
+ - Review example handlers in `src/handlers/exampleHandlers.js`
374
+ - Open an issue on GitHub
package/QUICK_START.md ADDED
@@ -0,0 +1,179 @@
1
+ # Quick Start Guide
2
+
3
+ Get up and running with xStripe in 5 minutes.
4
+
5
+ ## 1. Install
6
+
7
+ ```bash
8
+ npm install @xenterprises/fastify-xstripe stripe
9
+ ```
10
+
11
+ ## 2. Setup
12
+
13
+ ```javascript
14
+ import Fastify from 'fastify';
15
+ import xStripe from '@xenterprises/fastify-xstripe';
16
+
17
+ const fastify = Fastify({
18
+ logger: true,
19
+ rawBody: true, // Important for webhook signature verification
20
+ });
21
+
22
+ await fastify.register(xStripe, {
23
+ apiKey: process.env.STRIPE_API_KEY,
24
+ webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
25
+ });
26
+
27
+ await fastify.listen({ port: 3000 });
28
+ ```
29
+
30
+ ## 3. Environment Variables
31
+
32
+ Create `.env`:
33
+
34
+ ```bash
35
+ STRIPE_API_KEY=sk_test_...
36
+ STRIPE_WEBHOOK_SECRET=whsec_...
37
+ PORT=3000
38
+ ```
39
+
40
+ ## 4. Add Custom Handlers
41
+
42
+ ```javascript
43
+ await fastify.register(xStripe, {
44
+ apiKey: process.env.STRIPE_API_KEY,
45
+ webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
46
+ handlers: {
47
+ // New subscription
48
+ 'customer.subscription.created': async (event, fastify, stripe) => {
49
+ const sub = event.data.object;
50
+ console.log(`New subscriber: ${sub.customer}`);
51
+
52
+ // Your business logic here
53
+ // await fastify.prisma.user.update(...)
54
+ // await fastify.email.send(...)
55
+ },
56
+
57
+ // Payment failed
58
+ 'invoice.payment_failed': async (event, fastify, stripe) => {
59
+ const invoice = event.data.object;
60
+ console.log(`Payment failed: ${invoice.customer}`);
61
+
62
+ // Send notification
63
+ // await fastify.email.send(...)
64
+ },
65
+ },
66
+ });
67
+ ```
68
+
69
+ ## 5. Test Locally
70
+
71
+ Install Stripe CLI:
72
+ ```bash
73
+ brew install stripe/stripe-cli/stripe
74
+ ```
75
+
76
+ Forward webhooks:
77
+ ```bash
78
+ stripe listen --forward-to localhost:3000/stripe/webhook
79
+ ```
80
+
81
+ Trigger test events:
82
+ ```bash
83
+ stripe trigger customer.subscription.created
84
+ stripe trigger invoice.payment_failed
85
+ ```
86
+
87
+ ## 6. Deploy
88
+
89
+ 1. Deploy your app
90
+ 2. Add webhook endpoint in Stripe Dashboard:
91
+ - URL: `https://yourdomain.com/stripe/webhook`
92
+ - Events: Select events you handle
93
+ 3. Copy webhook signing secret to production env
94
+
95
+ ## Common Use Cases
96
+
97
+ ### Update Database on New Subscription
98
+
99
+ ```javascript
100
+ 'customer.subscription.created': async (event, fastify, stripe) => {
101
+ const subscription = event.data.object;
102
+
103
+ await fastify.prisma.user.update({
104
+ where: { stripeCustomerId: subscription.customer },
105
+ data: {
106
+ subscriptionId: subscription.id,
107
+ subscriptionStatus: subscription.status,
108
+ planId: subscription.items.data[0]?.price.id,
109
+ },
110
+ });
111
+ }
112
+ ```
113
+
114
+ ### Send Email on Payment Failure
115
+
116
+ ```javascript
117
+ 'invoice.payment_failed': async (event, fastify, stripe) => {
118
+ const invoice = event.data.object;
119
+ const customer = await stripe.customers.retrieve(invoice.customer);
120
+
121
+ await fastify.email.send(
122
+ customer.email,
123
+ 'Payment Failed',
124
+ 'Please update your payment method'
125
+ );
126
+ }
127
+ ```
128
+
129
+ ### Revoke Access on Cancellation
130
+
131
+ ```javascript
132
+ 'customer.subscription.deleted': async (event, fastify, stripe) => {
133
+ const subscription = event.data.object;
134
+
135
+ await fastify.prisma.user.update({
136
+ where: { stripeSubscriptionId: subscription.id },
137
+ data: {
138
+ hasAccess: false,
139
+ subscriptionStatus: 'canceled',
140
+ },
141
+ });
142
+ }
143
+ ```
144
+
145
+ ## Handler Function Signature
146
+
147
+ ```javascript
148
+ async function handler(event, fastify, stripe) {
149
+ // event - Stripe webhook event
150
+ // fastify - Fastify instance (access decorators)
151
+ // stripe - Stripe client
152
+ }
153
+ ```
154
+
155
+ ## Supported Events
156
+
157
+ - **Subscriptions**: created, updated, deleted, paused, resumed, trial_will_end
158
+ - **Invoices**: created, finalized, paid, payment_failed, upcoming
159
+ - **Payments**: payment_intent.succeeded, payment_intent.payment_failed
160
+ - **Customers**: created, updated, deleted
161
+ - **Payment Methods**: attached, detached
162
+ - **Checkout**: session.completed, session.expired
163
+
164
+ See [README.md](./README.md) for full list.
165
+
166
+ ## Next Steps
167
+
168
+ - [Full Documentation](./README.md)
169
+ - [Testing Guide](./TESTING.md)
170
+ - [Migration Guide](./MIGRATION.md)
171
+ - [Example Handlers](./src/handlers/exampleHandlers.js)
172
+
173
+ ## Tips
174
+
175
+ 1. **Use structured logging**: `fastify.log.info()` instead of `console.log()`
176
+ 2. **Make handlers idempotent**: Safe to run multiple times
177
+ 3. **Return 200 quickly**: Do heavy work async
178
+ 4. **Test locally first**: Use Stripe CLI before deploying
179
+ 5. **Monitor webhooks**: Check Stripe Dashboard for failures