@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/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
|