@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/TESTING.md ADDED
@@ -0,0 +1,357 @@
1
+ # Testing Guide for xStripe
2
+
3
+ This guide shows how to test your Stripe webhook handlers locally and in production.
4
+
5
+ ## Local Testing with Stripe CLI
6
+
7
+ ### 1. Install Stripe CLI
8
+
9
+ **macOS:**
10
+ ```bash
11
+ brew install stripe/stripe-cli/stripe
12
+ ```
13
+
14
+ **Other platforms:**
15
+ Download from https://stripe.com/docs/stripe-cli
16
+
17
+ ### 2. Login to Stripe
18
+
19
+ ```bash
20
+ stripe login
21
+ ```
22
+
23
+ This will open your browser for authentication.
24
+
25
+ ### 3. Forward Webhooks to Local Server
26
+
27
+ Start your local server first:
28
+ ```bash
29
+ npm run dev
30
+ ```
31
+
32
+ Then forward webhooks:
33
+ ```bash
34
+ stripe listen --forward-to localhost:3000/stripe/webhook
35
+ ```
36
+
37
+ This will output a webhook signing secret. Copy it to your `.env` file:
38
+ ```
39
+ STRIPE_WEBHOOK_SECRET=whsec_...
40
+ ```
41
+
42
+ ### 4. Trigger Test Events
43
+
44
+ In a new terminal, trigger specific events:
45
+
46
+ ```bash
47
+ # Test subscription creation
48
+ stripe trigger customer.subscription.created
49
+
50
+ # Test payment failure
51
+ stripe trigger invoice.payment_failed
52
+
53
+ # Test subscription cancellation
54
+ stripe trigger customer.subscription.deleted
55
+
56
+ # Test trial ending
57
+ stripe trigger customer.subscription.trial_will_end
58
+ ```
59
+
60
+ ## Testing Specific Scenarios
61
+
62
+ ### Subscription Lifecycle
63
+
64
+ ```bash
65
+ # 1. Create subscription
66
+ stripe trigger customer.subscription.created
67
+
68
+ # 2. Update subscription (plan change)
69
+ stripe trigger customer.subscription.updated
70
+
71
+ # 3. Trial ending soon
72
+ stripe trigger customer.subscription.trial_will_end
73
+
74
+ # 4. Cancel subscription
75
+ stripe trigger customer.subscription.deleted
76
+ ```
77
+
78
+ ### Payment Scenarios
79
+
80
+ ```bash
81
+ # Successful payment
82
+ stripe trigger invoice.paid
83
+
84
+ # Failed payment
85
+ stripe trigger invoice.payment_failed
86
+
87
+ # Upcoming payment reminder
88
+ stripe trigger invoice.upcoming
89
+ ```
90
+
91
+ ### Customer Events
92
+
93
+ ```bash
94
+ # New customer
95
+ stripe trigger customer.created
96
+
97
+ # Customer updated
98
+ stripe trigger customer.updated
99
+
100
+ # Payment method attached
101
+ stripe trigger payment_method.attached
102
+ ```
103
+
104
+ ## Testing Handlers Directly
105
+
106
+ Since handlers are pure functions, you can test them in isolation:
107
+
108
+ ### Example Test File
109
+
110
+ ```javascript
111
+ // test/handlers.test.js
112
+ import { test } from 'node:test';
113
+ import assert from 'node:assert';
114
+
115
+ test('subscription created handler updates database', async () => {
116
+ const mockEvent = {
117
+ type: 'customer.subscription.created',
118
+ data: {
119
+ object: {
120
+ id: 'sub_123',
121
+ customer: 'cus_123',
122
+ status: 'active',
123
+ items: {
124
+ data: [{
125
+ price: {
126
+ id: 'price_123',
127
+ product: 'prod_123',
128
+ },
129
+ }],
130
+ },
131
+ },
132
+ },
133
+ };
134
+
135
+ let updatedUser = null;
136
+
137
+ const mockFastify = {
138
+ log: {
139
+ info: () => {},
140
+ error: () => {},
141
+ warn: () => {},
142
+ },
143
+ prisma: {
144
+ user: {
145
+ update: async (data) => {
146
+ updatedUser = data;
147
+ return { id: 1 };
148
+ },
149
+ },
150
+ },
151
+ };
152
+
153
+ const mockStripe = {};
154
+
155
+ // Import your handler
156
+ const handler = customHandlers['customer.subscription.created'];
157
+ await handler(mockEvent, mockFastify, mockStripe);
158
+
159
+ // Assert database was updated
160
+ assert.equal(updatedUser.where.stripeCustomerId, 'cus_123');
161
+ assert.equal(updatedUser.data.subscriptionId, 'sub_123');
162
+ });
163
+
164
+ test('payment failure handler sends notification', async () => {
165
+ const mockEvent = {
166
+ type: 'invoice.payment_failed',
167
+ data: {
168
+ object: {
169
+ id: 'in_123',
170
+ customer: 'cus_123',
171
+ subscription: 'sub_123',
172
+ attempt_count: 2,
173
+ amount_due: 2000,
174
+ },
175
+ },
176
+ };
177
+
178
+ let emailSent = false;
179
+
180
+ const mockFastify = {
181
+ log: {
182
+ info: () => {},
183
+ error: () => {},
184
+ warn: () => {},
185
+ },
186
+ email: {
187
+ send: async (to, subject, body) => {
188
+ emailSent = true;
189
+ assert.equal(subject, 'Payment Failed');
190
+ return { success: true };
191
+ },
192
+ },
193
+ };
194
+
195
+ const mockStripe = {
196
+ customers: {
197
+ retrieve: async (id) => ({
198
+ id,
199
+ email: 'test@example.com',
200
+ }),
201
+ },
202
+ };
203
+
204
+ const handler = customHandlers['invoice.payment_failed'];
205
+ await handler(mockEvent, mockFastify, mockStripe);
206
+
207
+ assert.equal(emailSent, true);
208
+ });
209
+ ```
210
+
211
+ Run tests:
212
+ ```bash
213
+ node --test test/**/*.test.js
214
+ ```
215
+
216
+ ## Manual Testing with cURL
217
+
218
+ You can also send webhook events manually (useful for CI/CD):
219
+
220
+ ```bash
221
+ # Get a test event from Stripe
222
+ stripe events list --limit 1
223
+
224
+ # Send it to your local server
225
+ curl -X POST http://localhost:3000/stripe/webhook \
226
+ -H "Content-Type: application/json" \
227
+ -H "Stripe-Signature: YOUR_SIGNATURE" \
228
+ -d @test-event.json
229
+ ```
230
+
231
+ ## Testing in Production
232
+
233
+ ### 1. Configure Webhook Endpoint
234
+
235
+ In your Stripe Dashboard:
236
+ 1. Go to Developers → Webhooks
237
+ 2. Click "Add endpoint"
238
+ 3. Enter your production URL: `https://yourdomain.com/stripe/webhook`
239
+ 4. Select events to listen for (or select "Select all events")
240
+ 5. Copy the signing secret to your production environment
241
+
242
+ ### 2. Monitor Webhook Deliveries
243
+
244
+ In Stripe Dashboard:
245
+ - View webhook delivery attempts
246
+ - See request/response details
247
+ - Retry failed deliveries
248
+
249
+ ### 3. Set Up Alerts
250
+
251
+ Monitor for:
252
+ - Failed webhook deliveries
253
+ - Handler errors in your logs
254
+ - Unusual event patterns
255
+
256
+ ## Common Test Scenarios
257
+
258
+ ### Test New Subscriber Flow
259
+
260
+ 1. Create customer
261
+ 2. Create checkout session
262
+ 3. Complete checkout (use test card `4242 4242 4242 4242`)
263
+ 4. Verify `customer.subscription.created` handler runs
264
+ 5. Verify `invoice.paid` handler runs
265
+ 6. Check database for new subscription
266
+
267
+ ### Test Failed Payment Flow
268
+
269
+ 1. Update payment method to failing card (`4000 0000 0000 0341`)
270
+ 2. Wait for billing date or trigger manually
271
+ 3. Verify `invoice.payment_failed` handler runs
272
+ 4. Check email notifications sent
273
+ 5. Verify retry logic
274
+
275
+ ### Test Subscription Cancellation
276
+
277
+ 1. Cancel subscription via API
278
+ 2. Verify `customer.subscription.updated` handler runs
279
+ 3. If `cancel_at_period_end=false`, verify `customer.subscription.deleted` runs
280
+ 4. Check access revoked in database
281
+
282
+ ## Debugging Tips
283
+
284
+ ### Enable Verbose Logging
285
+
286
+ ```javascript
287
+ const fastify = Fastify({
288
+ logger: {
289
+ level: 'debug',
290
+ transport: {
291
+ target: 'pino-pretty',
292
+ },
293
+ },
294
+ });
295
+ ```
296
+
297
+ ### Log All Webhook Events
298
+
299
+ Add this to see raw events:
300
+
301
+ ```javascript
302
+ handlers: {
303
+ '*': async (event, fastify, stripe) => {
304
+ fastify.log.debug({
305
+ event: event.type,
306
+ data: event.data.object,
307
+ });
308
+ },
309
+ }
310
+ ```
311
+
312
+ ### Check Webhook Signatures
313
+
314
+ If signatures fail:
315
+ 1. Verify webhook secret matches Stripe Dashboard
316
+ 2. Check raw body is being used (not parsed JSON)
317
+ 3. Verify headers are forwarded correctly (if using proxy)
318
+
319
+ ### Test Mode vs Live Mode
320
+
321
+ - Use `sk_test_` keys for development
322
+ - Use `sk_live_` keys for production
323
+ - Test and live webhooks are separate endpoints
324
+ - Events don't cross between modes
325
+
326
+ ## Best Practices
327
+
328
+ 1. **Test all critical paths** - Subscription creation, payment failures, cancellations
329
+ 2. **Use idempotency** - Handlers should be safe to run multiple times
330
+ 3. **Log everything** - You can't debug what you can't see
331
+ 4. **Monitor production** - Set up alerts for failed handlers
332
+ 5. **Test error cases** - What happens if database is down?
333
+ 6. **Verify async operations** - Do emails actually send?
334
+ 7. **Check edge cases** - What if customer has no email?
335
+
336
+ ## Troubleshooting
337
+
338
+ ### Webhooks Not Received
339
+
340
+ - Check webhook endpoint is accessible publicly
341
+ - Verify webhook secret is correct
342
+ - Check firewall/security group allows Stripe IPs
343
+ - Review Stripe Dashboard for delivery attempts
344
+
345
+ ### Handler Errors
346
+
347
+ - Check logs for error messages
348
+ - Verify database connections
349
+ - Test handler functions in isolation
350
+ - Ensure all required services are available
351
+
352
+ ### Signature Verification Fails
353
+
354
+ - Verify using raw body, not parsed JSON
355
+ - Check webhook secret matches exactly
356
+ - Ensure no middleware modifies the body
357
+ - Verify Content-Type header is correct
package/index.d.ts ADDED
@@ -0,0 +1,309 @@
1
+ /**
2
+ * xStripe - Fastify Plugin for Stripe Webhook Handling
3
+ * TypeScript Type Definitions
4
+ *
5
+ * @module @xenterprises/fastify-xstripe
6
+ * @version 1.0.0
7
+ */
8
+
9
+ import { FastifyPluginAsync, FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
10
+ import Stripe from 'stripe';
11
+
12
+ /**
13
+ * Stripe Webhook Event (from Stripe SDK)
14
+ */
15
+ export type StripeWebhookEvent = Stripe.Event;
16
+
17
+ /**
18
+ * Webhook Event Handler Function
19
+ */
20
+ export type WebhookEventHandler = (
21
+ event: StripeWebhookEvent,
22
+ fastify: FastifyInstance,
23
+ stripe: Stripe
24
+ ) => Promise<void>;
25
+
26
+ /**
27
+ * Default Event Handlers Map
28
+ */
29
+ export interface DefaultHandlers {
30
+ // Subscription Events
31
+ 'customer.subscription.created': WebhookEventHandler;
32
+ 'customer.subscription.updated': WebhookEventHandler;
33
+ 'customer.subscription.deleted': WebhookEventHandler;
34
+ 'customer.subscription.paused': WebhookEventHandler;
35
+ 'customer.subscription.resumed': WebhookEventHandler;
36
+ 'customer.subscription.trial_will_end': WebhookEventHandler;
37
+
38
+ // Invoice Events
39
+ 'invoice.created': WebhookEventHandler;
40
+ 'invoice.finalized': WebhookEventHandler;
41
+ 'invoice.paid': WebhookEventHandler;
42
+ 'invoice.payment_failed': WebhookEventHandler;
43
+ 'invoice.upcoming': WebhookEventHandler;
44
+
45
+ // Payment Intent Events
46
+ 'payment_intent.succeeded': WebhookEventHandler;
47
+ 'payment_intent.payment_failed': WebhookEventHandler;
48
+
49
+ // Customer Events
50
+ 'customer.created': WebhookEventHandler;
51
+ 'customer.updated': WebhookEventHandler;
52
+ 'customer.deleted': WebhookEventHandler;
53
+
54
+ // Payment Method Events
55
+ 'payment_method.attached': WebhookEventHandler;
56
+ 'payment_method.detached': WebhookEventHandler;
57
+
58
+ // Charge Events
59
+ 'charge.succeeded': WebhookEventHandler;
60
+ 'charge.failed': WebhookEventHandler;
61
+ 'charge.refunded': WebhookEventHandler;
62
+
63
+ // Checkout Events
64
+ 'checkout.session.completed': WebhookEventHandler;
65
+ 'checkout.session.expired': WebhookEventHandler;
66
+
67
+ // Allow any other event type
68
+ [eventType: string]: WebhookEventHandler;
69
+ }
70
+
71
+ /**
72
+ * Plugin Configuration Options
73
+ */
74
+ export interface XStripePluginOptions {
75
+ /** Stripe API Key (Secret Key) */
76
+ apiKey?: string;
77
+
78
+ /** Stripe Webhook Signing Secret */
79
+ webhookSecret?: string;
80
+
81
+ /** Webhook endpoint path */
82
+ webhookPath?: string;
83
+
84
+ /** Custom event handlers (override defaults) */
85
+ handlers?: Partial<DefaultHandlers>;
86
+
87
+ /** Whether to fail on handler errors */
88
+ failOnError?: boolean;
89
+
90
+ /** Request timeout in milliseconds */
91
+ requestTimeout?: number;
92
+
93
+ /** Enable event logging */
94
+ logEvents?: boolean;
95
+ }
96
+
97
+ /**
98
+ * Helper Functions
99
+ */
100
+ export namespace helpers {
101
+ /**
102
+ * Format amount as currency string
103
+ * @param amount Amount in cents
104
+ * @param currency Currency code (USD, EUR, etc.)
105
+ * @returns Formatted currency string (e.g., "$20.00")
106
+ */
107
+ function formatAmount(amount: number, currency: string): string;
108
+
109
+ /**
110
+ * Get plan name from subscription
111
+ * @param subscription Subscription object
112
+ * @returns Plan name or product name
113
+ */
114
+ function getPlanName(subscription: Stripe.Subscription): string;
115
+
116
+ /**
117
+ * Check if subscription is active
118
+ * @param subscription Subscription object
119
+ * @returns True if subscription is active
120
+ */
121
+ function isActiveSubscription(subscription: Stripe.Subscription): boolean;
122
+
123
+ /**
124
+ * Get customer email from various objects
125
+ * @param obj Stripe object with customer reference
126
+ * @param stripe Stripe client instance
127
+ * @returns Customer email
128
+ */
129
+ function getCustomerEmail(obj: any, stripe: Stripe): Promise<string>;
130
+
131
+ /**
132
+ * Create idempotent request ID from event
133
+ * @param event Stripe webhook event
134
+ * @returns Idempotency key
135
+ */
136
+ function createIdempotencyKey(event: StripeWebhookEvent): string;
137
+ }
138
+
139
+ /**
140
+ * xStripe Service Methods (available on fastify.xStripe)
141
+ */
142
+ export interface XStripeService {
143
+ /**
144
+ * Register custom event handler
145
+ * @param eventType Stripe event type
146
+ * @param handler Event handler function
147
+ */
148
+ onEvent(eventType: string, handler: WebhookEventHandler): void;
149
+
150
+ /**
151
+ * Get registered handler for event type
152
+ * @param eventType Stripe event type
153
+ * @returns Handler function or undefined
154
+ */
155
+ getHandler(eventType: string): WebhookEventHandler | undefined;
156
+
157
+ /**
158
+ * Execute event handler
159
+ * @param event Stripe webhook event
160
+ * @returns Promise that resolves when handler completes
161
+ */
162
+ executeHandler(event: StripeWebhookEvent): Promise<void>;
163
+ }
164
+
165
+ /**
166
+ * Fastify Instance with xStripe Decoration
167
+ */
168
+ declare module 'fastify' {
169
+ interface FastifyInstance {
170
+ /** xStripe service methods */
171
+ xStripe: XStripeService;
172
+
173
+ /** Stripe client instance */
174
+ stripe: Stripe;
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Webhook Request with Stripe Event
180
+ */
181
+ export interface WebhookRequest extends FastifyRequest {
182
+ body: {
183
+ /** Stripe event object */
184
+ id: string;
185
+ type: string;
186
+ data: any;
187
+ [key: string]: any;
188
+ };
189
+ }
190
+
191
+ /**
192
+ * Webhook Response
193
+ */
194
+ export interface WebhookResponse {
195
+ /** Status message */
196
+ status: 'success' | 'error';
197
+
198
+ /** Event ID that was processed */
199
+ eventId: string;
200
+
201
+ /** Event type that was processed */
202
+ eventType: string;
203
+
204
+ /** Optional error message */
205
+ error?: string;
206
+
207
+ /** Timestamp of processing */
208
+ processedAt: string;
209
+ }
210
+
211
+ /**
212
+ * API Response Types
213
+ */
214
+ export interface Plan {
215
+ productId: string;
216
+ name: string;
217
+ description?: string;
218
+ images?: string[];
219
+ prices: PriceInfo[];
220
+ metadata?: Record<string, any>;
221
+ }
222
+
223
+ export interface PriceInfo {
224
+ priceId: string;
225
+ amount: number;
226
+ currency: string;
227
+ interval?: string;
228
+ intervalCount?: number;
229
+ trialPeriodDays?: number;
230
+ nickname?: string;
231
+ metadata?: Record<string, any>;
232
+ }
233
+
234
+ export interface PaymentMethod {
235
+ id: string;
236
+ type: string;
237
+ isDefault: boolean;
238
+ card?: {
239
+ brand: string;
240
+ last4: string;
241
+ expMonth: number;
242
+ expYear: number;
243
+ };
244
+ billingDetails?: Record<string, any>;
245
+ createdAt: Date;
246
+ }
247
+
248
+ export interface SubscriptionInfo {
249
+ id: string;
250
+ status: string;
251
+ planId?: string;
252
+ planName?: string;
253
+ amount?: number;
254
+ currency?: string;
255
+ interval?: string;
256
+ createdAt: Date;
257
+ currentPeriodEnd: Date;
258
+ cancelAtPeriodEnd: boolean;
259
+ canceledAt?: Date;
260
+ }
261
+
262
+ /**
263
+ * xStripe Plugin
264
+ * Registers Stripe webhook handling and provides convenient API endpoints
265
+ *
266
+ * @example
267
+ * ```typescript
268
+ * import Fastify from 'fastify';
269
+ * import xStripe from '@xenterprises/fastify-xstripe';
270
+ *
271
+ * const fastify = Fastify();
272
+ *
273
+ * await fastify.register(xStripe, {
274
+ * apiKey: process.env.STRIPE_API_KEY,
275
+ * webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
276
+ * webhookPath: '/webhooks/stripe'
277
+ * });
278
+ *
279
+ * // Register custom handler
280
+ * fastify.xStripe.onEvent('customer.subscription.created', async (event, fastify, stripe) => {
281
+ * const subscription = event.data.object;
282
+ * console.log('New subscription:', subscription.id);
283
+ * });
284
+ *
285
+ * // Webhook route automatically registered at webhookPath
286
+ * // POST /webhooks/stripe
287
+ *
288
+ * // Use included API endpoints
289
+ * // GET /plans
290
+ * // GET /plans/:productId
291
+ * // POST /create-checkout-session
292
+ * // POST /create-payment-session
293
+ * // GET /customer/:customerId/subscriptions
294
+ * // POST /subscription/:id/update
295
+ * // GET /customer/:customerId/payment-methods
296
+ * // POST /customer/:customerId/payment-methods
297
+ * // POST /customer/:customerId/payment-methods/:paymentMethodId/default
298
+ * // DELETE /customer/:customerId/payment-methods/:paymentMethodId
299
+ * ```
300
+ */
301
+ declare const xStripe: FastifyPluginAsync<XStripePluginOptions>;
302
+
303
+ export default xStripe;
304
+
305
+ /**
306
+ * Re-export Stripe types for convenience
307
+ */
308
+ export { Stripe };
309
+ export type { StripeWebhookEvent as WebhookEvent };
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@xenterprises/fastify-xstripe",
3
+ "type": "module",
4
+ "version": "1.0.0",
5
+ "description": "Fastify plugin for Stripe webhooks with simplified, testable handlers for subscription events.",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js",
9
+ "./handlers": "./src/handlers/index.js",
10
+ "./helpers": "./src/utils/helpers.js"
11
+ },
12
+ "scripts": {
13
+ "start": "fastify start -l info server/app.js",
14
+ "dev": "fastify start -w -l info -P server/app.js",
15
+ "test": "node --test test/handlers.test.js"
16
+ },
17
+ "author": "Tim Mushen",
18
+ "license": "ISC",
19
+ "engines": {
20
+ "node": ">=20.0.0",
21
+ "npm": ">=10.0.0"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://gitlab.com/x-enterprises/fastify-plugins/fastify-xstripe"
26
+ },
27
+ "bugs": {
28
+ "url": "https://gitlab.com/x-enterprises/fastify-plugins/fastify-xstripe/-/issues"
29
+ },
30
+ "homepage": "https://gitlab.com/x-enterprises/fastify-plugins/fastify-xstripe#readme",
31
+ "keywords": [
32
+ "fastify",
33
+ "stripe",
34
+ "webhooks",
35
+ "subscription",
36
+ "billing",
37
+ "payments",
38
+ "plugin"
39
+ ],
40
+ "devDependencies": {
41
+ "@types/node": "^22.7.4",
42
+ "fastify": "^5.1.0",
43
+ "fastify-plugin": "^5.0.0",
44
+ "typescript": "^5.6.3"
45
+ },
46
+ "dependencies": {
47
+ "fastify-plugin": "^5.0.0",
48
+ "stripe": "^16.12.0"
49
+ },
50
+ "peerDependencies": {
51
+ "fastify": "^5.0.0"
52
+ }
53
+ }