@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/README.md ADDED
@@ -0,0 +1,331 @@
1
+ # xStripe
2
+
3
+ Fastify v5 plugin for simplified, testable Stripe webhook handling. Focuses on subscription lifecycle management with clean, readable code.
4
+
5
+ ## Requirements
6
+
7
+ - **Fastify v5.0.0+**
8
+ - **Node.js v20+**
9
+
10
+ ## Features
11
+
12
+ - **Simple webhook handling** - Clean event-based architecture
13
+ - **Built-in handlers** - 20+ default handlers for common events
14
+ - **Easy to test** - Handlers are pure functions
15
+ - **Type-safe** - Full TypeScript support
16
+ - **Readable** - Clear, self-documenting code
17
+ - **Flexible** - Override any default handler
18
+ - **Production-ready** - Signature verification, error handling, logging
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ npm install @xenterprises/fastify-xstripe fastify@5
24
+ ```
25
+
26
+ ## Quick Start
27
+
28
+ ```javascript
29
+ import Fastify from 'fastify';
30
+ import xStripe from '@xenterprises/fastify-xstripe';
31
+
32
+ const fastify = Fastify({ logger: true });
33
+
34
+ await fastify.register(xStripe, {
35
+ apiKey: process.env.STRIPE_API_KEY,
36
+ webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
37
+ webhookPath: '/stripe/webhook', // Optional, defaults to /stripe/webhook
38
+ });
39
+
40
+ await fastify.listen({ port: 3000 });
41
+ ```
42
+
43
+ ## Custom Handlers
44
+
45
+ Override default handlers with your business logic:
46
+
47
+ ```javascript
48
+ await fastify.register(xStripe, {
49
+ apiKey: process.env.STRIPE_API_KEY,
50
+ webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
51
+ handlers: {
52
+ // Handle new subscription
53
+ 'customer.subscription.created': async (event, fastify, stripe) => {
54
+ const subscription = event.data.object;
55
+
56
+ // Update your database
57
+ await fastify.prisma.user.update({
58
+ where: { stripeCustomerId: subscription.customer },
59
+ data: {
60
+ subscriptionId: subscription.id,
61
+ subscriptionStatus: subscription.status,
62
+ planId: subscription.items.data[0]?.price.id,
63
+ },
64
+ });
65
+
66
+ // Send welcome email
67
+ await fastify.email.send(
68
+ subscription.customer.email,
69
+ 'Welcome to Premium!',
70
+ '<h1>Thanks for subscribing!</h1>'
71
+ );
72
+ },
73
+
74
+ // Handle subscription cancellation
75
+ 'customer.subscription.deleted': async (event, fastify, stripe) => {
76
+ const subscription = event.data.object;
77
+
78
+ // Revoke access
79
+ await fastify.prisma.user.update({
80
+ where: { stripeSubscriptionId: subscription.id },
81
+ data: {
82
+ subscriptionStatus: 'canceled',
83
+ hasAccess: false,
84
+ },
85
+ });
86
+ },
87
+
88
+ // Handle failed payment
89
+ 'invoice.payment_failed': async (event, fastify, stripe) => {
90
+ const invoice = event.data.object;
91
+
92
+ // Send payment failure email
93
+ const customer = await stripe.customers.retrieve(invoice.customer);
94
+ await fastify.email.send(
95
+ customer.email,
96
+ 'Payment Failed',
97
+ '<p>Please update your payment method.</p>'
98
+ );
99
+ },
100
+ },
101
+ });
102
+ ```
103
+
104
+ ## Handler Function Signature
105
+
106
+ All handlers receive three parameters:
107
+
108
+ ```javascript
109
+ async function handler(event, fastify, stripe) {
110
+ // event - The Stripe webhook event object
111
+ // fastify - The Fastify instance (access to decorators)
112
+ // stripe - The Stripe client instance
113
+ }
114
+ ```
115
+
116
+ ## Supported Events
117
+
118
+ ### Subscription Events
119
+ - `customer.subscription.created` - New subscription
120
+ - `customer.subscription.updated` - Subscription changed
121
+ - `customer.subscription.deleted` - Subscription canceled
122
+ - `customer.subscription.trial_will_end` - Trial ending in 3 days
123
+ - `customer.subscription.paused` - Subscription paused
124
+ - `customer.subscription.resumed` - Subscription resumed
125
+
126
+ ### Invoice Events
127
+ - `invoice.created` - Invoice created
128
+ - `invoice.finalized` - Invoice ready for payment
129
+ - `invoice.paid` - Payment succeeded
130
+ - `invoice.payment_failed` - Payment failed
131
+ - `invoice.upcoming` - Upcoming charge notification
132
+
133
+ ### Payment Events
134
+ - `payment_intent.succeeded` - Payment successful
135
+ - `payment_intent.payment_failed` - Payment failed
136
+
137
+ ### Customer Events
138
+ - `customer.created` - New customer
139
+ - `customer.updated` - Customer details changed
140
+ - `customer.deleted` - Customer deleted
141
+
142
+ ### Payment Method Events
143
+ - `payment_method.attached` - Payment method added
144
+ - `payment_method.detached` - Payment method removed
145
+
146
+ ### Checkout Events
147
+ - `checkout.session.completed` - Checkout completed
148
+ - `checkout.session.expired` - Checkout session expired
149
+
150
+ ## Testing Webhooks Locally
151
+
152
+ ### 1. Use Stripe CLI
153
+
154
+ ```bash
155
+ # Install Stripe CLI
156
+ brew install stripe/stripe-cli/stripe
157
+
158
+ # Login to Stripe
159
+ stripe login
160
+
161
+ # Forward webhooks to your local server
162
+ stripe listen --forward-to localhost:3000/stripe/webhook
163
+
164
+ # Trigger test events
165
+ stripe trigger customer.subscription.created
166
+ stripe trigger invoice.payment_failed
167
+ ```
168
+
169
+ ### 2. Test Handlers Directly
170
+
171
+ Since handlers are pure functions, they're easy to test:
172
+
173
+ ```javascript
174
+ import { test } from 'node:test';
175
+ import assert from 'node:assert';
176
+
177
+ test('subscription.created handler', async () => {
178
+ const mockEvent = {
179
+ type: 'customer.subscription.created',
180
+ data: {
181
+ object: {
182
+ id: 'sub_123',
183
+ customer: 'cus_123',
184
+ status: 'active',
185
+ },
186
+ },
187
+ };
188
+
189
+ const mockFastify = {
190
+ log: { info: () => {} },
191
+ prisma: {
192
+ user: {
193
+ update: async (data) => {
194
+ assert.equal(data.where.stripeCustomerId, 'cus_123');
195
+ return {};
196
+ },
197
+ },
198
+ },
199
+ };
200
+
201
+ const mockStripe = {};
202
+
203
+ await handlers['customer.subscription.created'](
204
+ mockEvent,
205
+ mockFastify,
206
+ mockStripe
207
+ );
208
+ });
209
+ ```
210
+
211
+ ## Common Patterns
212
+
213
+ ### Update Database on Subscription Change
214
+
215
+ ```javascript
216
+ 'customer.subscription.updated': async (event, fastify, stripe) => {
217
+ const subscription = event.data.object;
218
+ const previous = event.data.previous_attributes || {};
219
+
220
+ // Check what changed
221
+ if ('status' in previous) {
222
+ await fastify.prisma.user.update({
223
+ where: { stripeSubscriptionId: subscription.id },
224
+ data: { subscriptionStatus: subscription.status },
225
+ });
226
+
227
+ // Handle specific status changes
228
+ if (subscription.status === 'past_due') {
229
+ // Send payment reminder
230
+ }
231
+ }
232
+ }
233
+ ```
234
+
235
+ ### Send Notification Emails
236
+
237
+ ```javascript
238
+ 'customer.subscription.trial_will_end': async (event, fastify, stripe) => {
239
+ const subscription = event.data.object;
240
+ const customer = await stripe.customers.retrieve(subscription.customer);
241
+
242
+ await fastify.email.send(
243
+ customer.email,
244
+ 'Your trial ends soon!',
245
+ '<p>Convert to a paid plan to keep access.</p>'
246
+ );
247
+ }
248
+ ```
249
+
250
+ ### Track Failed Payments
251
+
252
+ ```javascript
253
+ 'invoice.payment_failed': async (event, fastify, stripe) => {
254
+ const invoice = event.data.object;
255
+
256
+ await fastify.prisma.user.update({
257
+ where: { stripeSubscriptionId: invoice.subscription },
258
+ data: {
259
+ failedPaymentCount: { increment: 1 },
260
+ lastFailedPayment: new Date(),
261
+ },
262
+ });
263
+
264
+ // After 3 failed payments, suspend account
265
+ const user = await fastify.prisma.user.findUnique({
266
+ where: { stripeSubscriptionId: invoice.subscription },
267
+ });
268
+
269
+ if (user.failedPaymentCount >= 3) {
270
+ await fastify.prisma.user.update({
271
+ where: { id: user.id },
272
+ data: { accountSuspended: true },
273
+ });
274
+ }
275
+ }
276
+ ```
277
+
278
+ ## Configuration Options
279
+
280
+ | Option | Type | Required | Default | Description |
281
+ |--------|------|----------|---------|-------------|
282
+ | `apiKey` | string | Yes | - | Stripe API key |
283
+ | `webhookSecret` | string | No | - | Stripe webhook signing secret |
284
+ | `webhookPath` | string | No | `/stripe/webhook` | Webhook endpoint path |
285
+ | `handlers` | object | No | `{}` | Custom event handlers |
286
+ | `apiVersion` | string | No | `2024-11-20.acacia` | Stripe API version |
287
+
288
+ ## Security
289
+
290
+ - **Signature Verification**: All webhooks are verified using Stripe's signature
291
+ - **Raw Body Required**: Plugin automatically handles raw body parsing
292
+ - **Error Isolation**: Handler errors don't prevent webhook acknowledgment
293
+ - **Logging**: All events and errors are logged
294
+
295
+ ## Best Practices
296
+
297
+ 1. **Always acknowledge webhooks quickly** - Do heavy processing async
298
+ 2. **Make handlers idempotent** - Stripe may send events multiple times
299
+ 3. **Log everything** - Use structured logging for debugging
300
+ 4. **Test with Stripe CLI** - Test all event types before production
301
+ 5. **Monitor failed handlers** - Set up alerts for handler errors
302
+
303
+ ## Environment Variables
304
+
305
+ ```bash
306
+ STRIPE_API_KEY=sk_test_...
307
+ STRIPE_WEBHOOK_SECRET=whsec_...
308
+ ```
309
+
310
+ ## Integration with Other xPlugins
311
+
312
+ Works seamlessly with other x-series plugins:
313
+
314
+ ```javascript
315
+ await fastify.register(xStripe, { /* ... */ });
316
+ await fastify.register(xTwilio, { /* ... */ }); // Send SMS notifications
317
+ await fastify.register(xConfig, { /* ... */ }); // Email notifications
318
+
319
+ // In your handlers:
320
+ 'invoice.payment_failed': async (event, fastify, stripe) => {
321
+ // Send email via xConfig/SendGrid
322
+ await fastify.email.send(/* ... */);
323
+
324
+ // Send SMS via xTwilio
325
+ await fastify.sms.send(/* ... */);
326
+ }
327
+ ```
328
+
329
+ ## License
330
+
331
+ ISC