specweave 0.24.0 → 0.24.6
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/.claude-plugin/marketplace.json +55 -0
- package/CLAUDE.md +42 -0
- package/dist/src/cli/commands/init.d.ts.map +1 -1
- package/dist/src/cli/commands/init.js +80 -41
- package/dist/src/cli/commands/init.js.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/types.d.ts +1 -1
- package/dist/src/cli/helpers/issue-tracker/types.d.ts.map +1 -1
- package/dist/src/config/types.d.ts +24 -24
- package/dist/src/core/config/types.d.ts +25 -0
- package/dist/src/core/config/types.d.ts.map +1 -1
- package/dist/src/core/config/types.js +6 -0
- package/dist/src/core/config/types.js.map +1 -1
- package/dist/src/core/repo-structure/repo-bulk-discovery.d.ts +33 -0
- package/dist/src/core/repo-structure/repo-bulk-discovery.d.ts.map +1 -0
- package/dist/src/core/repo-structure/repo-bulk-discovery.js +275 -0
- package/dist/src/core/repo-structure/repo-bulk-discovery.js.map +1 -0
- package/dist/src/core/repo-structure/repo-structure-manager.d.ts +9 -0
- package/dist/src/core/repo-structure/repo-structure-manager.d.ts.map +1 -1
- package/dist/src/core/repo-structure/repo-structure-manager.js +255 -87
- package/dist/src/core/repo-structure/repo-structure-manager.js.map +1 -1
- package/dist/src/init/architecture/types.d.ts +6 -6
- package/dist/src/utils/plugin-validator.d.ts.map +1 -1
- package/dist/src/utils/plugin-validator.js +15 -14
- package/dist/src/utils/plugin-validator.js.map +1 -1
- package/package.json +4 -4
- package/plugins/specweave/.claude-plugin/plugin.json +4 -4
- package/plugins/specweave/agents/pm/AGENT.md +2 -0
- package/plugins/specweave/commands/specweave-do.md +0 -47
- package/plugins/specweave/commands/specweave-increment.md +0 -82
- package/plugins/specweave/commands/specweave-next.md +0 -47
- package/plugins/specweave/hooks/post-task-completion.sh +67 -6
- package/plugins/specweave/hooks/pre-edit-spec.sh +11 -0
- package/plugins/specweave/hooks/pre-task-completion.sh +69 -2
- package/plugins/specweave/hooks/pre-write-spec.sh +11 -0
- package/plugins/specweave/skills/increment-planner/SKILL.md +124 -4
- package/plugins/specweave-frontend/agents/frontend-architect/AGENT.md +21 -0
- package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +150 -0
- package/plugins/specweave-payments/commands/stripe-setup.md +931 -0
- package/plugins/specweave-payments/commands/subscription-flow.md +1193 -0
- package/plugins/specweave-payments/commands/subscription-manage.md +386 -0
- package/plugins/specweave-payments/commands/webhook-setup.md +295 -0
- package/plugins/specweave-testing/agents/qa-engineer/AGENT.md +21 -0
|
@@ -0,0 +1,931 @@
|
|
|
1
|
+
# /specweave-payments:stripe-setup
|
|
2
|
+
|
|
3
|
+
Complete Stripe integration setup guide with production-ready code templates, security best practices, and testing workflows.
|
|
4
|
+
|
|
5
|
+
You are a payment integration expert who implements secure, PCI-compliant Stripe payment systems.
|
|
6
|
+
|
|
7
|
+
## Your Task
|
|
8
|
+
|
|
9
|
+
Set up complete Stripe payment integration with checkout flows, webhook handling, subscription billing, and customer management.
|
|
10
|
+
|
|
11
|
+
### 1. Environment Setup
|
|
12
|
+
|
|
13
|
+
**Install Dependencies**:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Node.js
|
|
17
|
+
npm install stripe @stripe/stripe-js dotenv
|
|
18
|
+
|
|
19
|
+
# Python
|
|
20
|
+
pip install stripe python-dotenv
|
|
21
|
+
|
|
22
|
+
# Ruby
|
|
23
|
+
gem install stripe dotenv
|
|
24
|
+
|
|
25
|
+
# PHP
|
|
26
|
+
composer require stripe/stripe-php vlucas/phpdotenv
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Environment Variables**:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# .env (NEVER commit this file!)
|
|
33
|
+
# Get keys from https://dashboard.stripe.com/apikeys
|
|
34
|
+
|
|
35
|
+
# Test mode (development)
|
|
36
|
+
STRIPE_PUBLISHABLE_KEY=pk_test_51...
|
|
37
|
+
STRIPE_SECRET_KEY=sk_test_51...
|
|
38
|
+
STRIPE_WEBHOOK_SECRET=whsec_...
|
|
39
|
+
|
|
40
|
+
# Live mode (production)
|
|
41
|
+
# STRIPE_PUBLISHABLE_KEY=pk_live_51...
|
|
42
|
+
# STRIPE_SECRET_KEY=sk_live_51...
|
|
43
|
+
# STRIPE_WEBHOOK_SECRET=whsec_...
|
|
44
|
+
|
|
45
|
+
# App configuration
|
|
46
|
+
STRIPE_SUCCESS_URL=https://yourdomain.com/success
|
|
47
|
+
STRIPE_CANCEL_URL=https://yourdomain.com/cancel
|
|
48
|
+
STRIPE_CURRENCY=usd
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 2. Backend Setup (Node.js/Express)
|
|
52
|
+
|
|
53
|
+
**Stripe Client Initialization**:
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
// src/config/stripe.ts
|
|
57
|
+
import Stripe from 'stripe';
|
|
58
|
+
import dotenv from 'dotenv';
|
|
59
|
+
|
|
60
|
+
dotenv.config();
|
|
61
|
+
|
|
62
|
+
if (!process.env.STRIPE_SECRET_KEY) {
|
|
63
|
+
throw new Error('STRIPE_SECRET_KEY is not set in environment variables');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
|
|
67
|
+
apiVersion: '2023-10-16',
|
|
68
|
+
typescript: true,
|
|
69
|
+
maxNetworkRetries: 2,
|
|
70
|
+
timeout: 10000, // 10 seconds
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
export const STRIPE_CONFIG = {
|
|
74
|
+
publishableKey: process.env.STRIPE_PUBLISHABLE_KEY!,
|
|
75
|
+
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
|
|
76
|
+
successUrl: process.env.STRIPE_SUCCESS_URL || 'http://localhost:3000/success',
|
|
77
|
+
cancelUrl: process.env.STRIPE_CANCEL_URL || 'http://localhost:3000/cancel',
|
|
78
|
+
currency: process.env.STRIPE_CURRENCY || 'usd',
|
|
79
|
+
};
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Payment Service**:
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
// src/services/payment.service.ts
|
|
86
|
+
import { stripe } from '../config/stripe';
|
|
87
|
+
import type Stripe from 'stripe';
|
|
88
|
+
|
|
89
|
+
export class PaymentService {
|
|
90
|
+
/**
|
|
91
|
+
* Create a one-time payment checkout session
|
|
92
|
+
*/
|
|
93
|
+
async createCheckoutSession(params: {
|
|
94
|
+
amount: number;
|
|
95
|
+
currency?: string;
|
|
96
|
+
customerId?: string;
|
|
97
|
+
metadata?: Record<string, string>;
|
|
98
|
+
}): Promise<Stripe.Checkout.Session> {
|
|
99
|
+
try {
|
|
100
|
+
const session = await stripe.checkout.sessions.create({
|
|
101
|
+
payment_method_types: ['card'],
|
|
102
|
+
line_items: [
|
|
103
|
+
{
|
|
104
|
+
price_data: {
|
|
105
|
+
currency: params.currency || 'usd',
|
|
106
|
+
product_data: {
|
|
107
|
+
name: 'Payment',
|
|
108
|
+
description: 'One-time payment',
|
|
109
|
+
},
|
|
110
|
+
unit_amount: params.amount, // Amount in cents
|
|
111
|
+
},
|
|
112
|
+
quantity: 1,
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
mode: 'payment',
|
|
116
|
+
success_url: `${process.env.STRIPE_SUCCESS_URL}?session_id={CHECKOUT_SESSION_ID}`,
|
|
117
|
+
cancel_url: process.env.STRIPE_CANCEL_URL,
|
|
118
|
+
customer: params.customerId,
|
|
119
|
+
metadata: params.metadata,
|
|
120
|
+
// Enable automatic tax calculation (optional)
|
|
121
|
+
automatic_tax: { enabled: false },
|
|
122
|
+
// Customer email collection
|
|
123
|
+
customer_email: params.customerId ? undefined : '',
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return session;
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.error('Failed to create checkout session:', error);
|
|
129
|
+
throw new Error('Payment session creation failed');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Create a payment intent for custom checkout UI
|
|
135
|
+
*/
|
|
136
|
+
async createPaymentIntent(params: {
|
|
137
|
+
amount: number;
|
|
138
|
+
currency?: string;
|
|
139
|
+
customerId?: string;
|
|
140
|
+
paymentMethodTypes?: string[];
|
|
141
|
+
metadata?: Record<string, string>;
|
|
142
|
+
}): Promise<Stripe.PaymentIntent> {
|
|
143
|
+
try {
|
|
144
|
+
const paymentIntent = await stripe.paymentIntents.create({
|
|
145
|
+
amount: params.amount,
|
|
146
|
+
currency: params.currency || 'usd',
|
|
147
|
+
customer: params.customerId,
|
|
148
|
+
payment_method_types: params.paymentMethodTypes || ['card'],
|
|
149
|
+
metadata: params.metadata,
|
|
150
|
+
// Automatic payment methods (enables more payment methods)
|
|
151
|
+
automatic_payment_methods: {
|
|
152
|
+
enabled: true,
|
|
153
|
+
allow_redirects: 'never', // or 'always' for redirect-based methods
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return paymentIntent;
|
|
158
|
+
} catch (error) {
|
|
159
|
+
console.error('Failed to create payment intent:', error);
|
|
160
|
+
throw new Error('Payment intent creation failed');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Retrieve a payment intent
|
|
166
|
+
*/
|
|
167
|
+
async getPaymentIntent(paymentIntentId: string): Promise<Stripe.PaymentIntent> {
|
|
168
|
+
try {
|
|
169
|
+
return await stripe.paymentIntents.retrieve(paymentIntentId);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
console.error('Failed to retrieve payment intent:', error);
|
|
172
|
+
throw new Error('Payment intent retrieval failed');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Confirm a payment intent (server-side confirmation)
|
|
178
|
+
*/
|
|
179
|
+
async confirmPaymentIntent(
|
|
180
|
+
paymentIntentId: string,
|
|
181
|
+
paymentMethodId: string
|
|
182
|
+
): Promise<Stripe.PaymentIntent> {
|
|
183
|
+
try {
|
|
184
|
+
return await stripe.paymentIntents.confirm(paymentIntentId, {
|
|
185
|
+
payment_method: paymentMethodId,
|
|
186
|
+
});
|
|
187
|
+
} catch (error) {
|
|
188
|
+
console.error('Failed to confirm payment intent:', error);
|
|
189
|
+
throw new Error('Payment confirmation failed');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Create or update a customer
|
|
195
|
+
*/
|
|
196
|
+
async createCustomer(params: {
|
|
197
|
+
email: string;
|
|
198
|
+
name?: string;
|
|
199
|
+
phone?: string;
|
|
200
|
+
metadata?: Record<string, string>;
|
|
201
|
+
paymentMethodId?: string;
|
|
202
|
+
}): Promise<Stripe.Customer> {
|
|
203
|
+
try {
|
|
204
|
+
const customer = await stripe.customers.create({
|
|
205
|
+
email: params.email,
|
|
206
|
+
name: params.name,
|
|
207
|
+
phone: params.phone,
|
|
208
|
+
metadata: params.metadata,
|
|
209
|
+
payment_method: params.paymentMethodId,
|
|
210
|
+
invoice_settings: params.paymentMethodId
|
|
211
|
+
? {
|
|
212
|
+
default_payment_method: params.paymentMethodId,
|
|
213
|
+
}
|
|
214
|
+
: undefined,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
return customer;
|
|
218
|
+
} catch (error) {
|
|
219
|
+
console.error('Failed to create customer:', error);
|
|
220
|
+
throw new Error('Customer creation failed');
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Attach a payment method to a customer
|
|
226
|
+
*/
|
|
227
|
+
async attachPaymentMethod(
|
|
228
|
+
paymentMethodId: string,
|
|
229
|
+
customerId: string,
|
|
230
|
+
setAsDefault = true
|
|
231
|
+
): Promise<Stripe.PaymentMethod> {
|
|
232
|
+
try {
|
|
233
|
+
// Attach payment method
|
|
234
|
+
const paymentMethod = await stripe.paymentMethods.attach(paymentMethodId, {
|
|
235
|
+
customer: customerId,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Set as default if requested
|
|
239
|
+
if (setAsDefault) {
|
|
240
|
+
await stripe.customers.update(customerId, {
|
|
241
|
+
invoice_settings: {
|
|
242
|
+
default_payment_method: paymentMethodId,
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return paymentMethod;
|
|
248
|
+
} catch (error) {
|
|
249
|
+
console.error('Failed to attach payment method:', error);
|
|
250
|
+
throw new Error('Payment method attachment failed');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* List customer payment methods
|
|
256
|
+
*/
|
|
257
|
+
async listPaymentMethods(customerId: string): Promise<Stripe.PaymentMethod[]> {
|
|
258
|
+
try {
|
|
259
|
+
const paymentMethods = await stripe.paymentMethods.list({
|
|
260
|
+
customer: customerId,
|
|
261
|
+
type: 'card',
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
return paymentMethods.data;
|
|
265
|
+
} catch (error) {
|
|
266
|
+
console.error('Failed to list payment methods:', error);
|
|
267
|
+
throw new Error('Payment method listing failed');
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Create a refund
|
|
273
|
+
*/
|
|
274
|
+
async createRefund(params: {
|
|
275
|
+
paymentIntentId?: string;
|
|
276
|
+
chargeId?: string;
|
|
277
|
+
amount?: number; // Partial refund amount in cents
|
|
278
|
+
reason?: 'duplicate' | 'fraudulent' | 'requested_by_customer';
|
|
279
|
+
metadata?: Record<string, string>;
|
|
280
|
+
}): Promise<Stripe.Refund> {
|
|
281
|
+
try {
|
|
282
|
+
const refund = await stripe.refunds.create({
|
|
283
|
+
payment_intent: params.paymentIntentId,
|
|
284
|
+
charge: params.chargeId,
|
|
285
|
+
amount: params.amount,
|
|
286
|
+
reason: params.reason,
|
|
287
|
+
metadata: params.metadata,
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
return refund;
|
|
291
|
+
} catch (error) {
|
|
292
|
+
console.error('Failed to create refund:', error);
|
|
293
|
+
throw new Error('Refund creation failed');
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export const paymentService = new PaymentService();
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
**Express API Routes**:
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
// src/routes/payment.routes.ts
|
|
305
|
+
import { Router, Request, Response } from 'express';
|
|
306
|
+
import { paymentService } from '../services/payment.service';
|
|
307
|
+
|
|
308
|
+
const router = Router();
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* POST /api/payments/checkout
|
|
312
|
+
* Create a checkout session
|
|
313
|
+
*/
|
|
314
|
+
router.post('/checkout', async (req: Request, res: Response) => {
|
|
315
|
+
try {
|
|
316
|
+
const { amount, currency, customerId, metadata } = req.body;
|
|
317
|
+
|
|
318
|
+
// Validate amount
|
|
319
|
+
if (!amount || amount <= 0) {
|
|
320
|
+
return res.status(400).json({ error: 'Invalid amount' });
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const session = await paymentService.createCheckoutSession({
|
|
324
|
+
amount,
|
|
325
|
+
currency,
|
|
326
|
+
customerId,
|
|
327
|
+
metadata,
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
res.json({ sessionId: session.id, url: session.url });
|
|
331
|
+
} catch (error) {
|
|
332
|
+
console.error('Checkout error:', error);
|
|
333
|
+
res.status(500).json({ error: 'Failed to create checkout session' });
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* POST /api/payments/intent
|
|
339
|
+
* Create a payment intent for custom UI
|
|
340
|
+
*/
|
|
341
|
+
router.post('/intent', async (req: Request, res: Response) => {
|
|
342
|
+
try {
|
|
343
|
+
const { amount, currency, customerId, metadata } = req.body;
|
|
344
|
+
|
|
345
|
+
if (!amount || amount <= 0) {
|
|
346
|
+
return res.status(400).json({ error: 'Invalid amount' });
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const paymentIntent = await paymentService.createPaymentIntent({
|
|
350
|
+
amount,
|
|
351
|
+
currency,
|
|
352
|
+
customerId,
|
|
353
|
+
metadata,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
res.json({ clientSecret: paymentIntent.client_secret });
|
|
357
|
+
} catch (error) {
|
|
358
|
+
console.error('Payment intent error:', error);
|
|
359
|
+
res.status(500).json({ error: 'Failed to create payment intent' });
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* POST /api/payments/customers
|
|
365
|
+
* Create a customer
|
|
366
|
+
*/
|
|
367
|
+
router.post('/customers', async (req: Request, res: Response) => {
|
|
368
|
+
try {
|
|
369
|
+
const { email, name, phone, metadata } = req.body;
|
|
370
|
+
|
|
371
|
+
if (!email) {
|
|
372
|
+
return res.status(400).json({ error: 'Email is required' });
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const customer = await paymentService.createCustomer({
|
|
376
|
+
email,
|
|
377
|
+
name,
|
|
378
|
+
phone,
|
|
379
|
+
metadata,
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
res.json({ customerId: customer.id });
|
|
383
|
+
} catch (error) {
|
|
384
|
+
console.error('Customer creation error:', error);
|
|
385
|
+
res.status(500).json({ error: 'Failed to create customer' });
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* POST /api/payments/refunds
|
|
391
|
+
* Create a refund
|
|
392
|
+
*/
|
|
393
|
+
router.post('/refunds', async (req: Request, res: Response) => {
|
|
394
|
+
try {
|
|
395
|
+
const { paymentIntentId, amount, reason, metadata } = req.body;
|
|
396
|
+
|
|
397
|
+
if (!paymentIntentId) {
|
|
398
|
+
return res.status(400).json({ error: 'Payment Intent ID is required' });
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const refund = await paymentService.createRefund({
|
|
402
|
+
paymentIntentId,
|
|
403
|
+
amount,
|
|
404
|
+
reason,
|
|
405
|
+
metadata,
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
res.json({ refundId: refund.id, status: refund.status });
|
|
409
|
+
} catch (error) {
|
|
410
|
+
console.error('Refund error:', error);
|
|
411
|
+
res.status(500).json({ error: 'Failed to create refund' });
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* GET /api/payments/config
|
|
417
|
+
* Get public Stripe configuration
|
|
418
|
+
*/
|
|
419
|
+
router.get('/config', (req: Request, res: Response) => {
|
|
420
|
+
res.json({
|
|
421
|
+
publishableKey: process.env.STRIPE_PUBLISHABLE_KEY,
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
export default router;
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
### 3. Frontend Setup (React)
|
|
429
|
+
|
|
430
|
+
**Stripe Provider**:
|
|
431
|
+
|
|
432
|
+
```typescript
|
|
433
|
+
// src/providers/StripeProvider.tsx
|
|
434
|
+
import React from 'react';
|
|
435
|
+
import { Elements } from '@stripe/react-stripe-js';
|
|
436
|
+
import { loadStripe, Stripe } from '@stripe/stripe-js';
|
|
437
|
+
|
|
438
|
+
// Load Stripe.js outside of component to avoid recreating the instance
|
|
439
|
+
let stripePromise: Promise<Stripe | null>;
|
|
440
|
+
|
|
441
|
+
const getStripe = () => {
|
|
442
|
+
if (!stripePromise) {
|
|
443
|
+
const publishableKey = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY || '';
|
|
444
|
+
stripePromise = loadStripe(publishableKey);
|
|
445
|
+
}
|
|
446
|
+
return stripePromise;
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
interface StripeProviderProps {
|
|
450
|
+
children: React.ReactNode;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export const StripeProvider: React.FC<StripeProviderProps> = ({ children }) => {
|
|
454
|
+
return (
|
|
455
|
+
<Elements stripe={getStripe()}>
|
|
456
|
+
{children}
|
|
457
|
+
</Elements>
|
|
458
|
+
);
|
|
459
|
+
};
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
**Payment Form Component**:
|
|
463
|
+
|
|
464
|
+
```typescript
|
|
465
|
+
// src/components/PaymentForm.tsx
|
|
466
|
+
import React, { useState } from 'react';
|
|
467
|
+
import {
|
|
468
|
+
useStripe,
|
|
469
|
+
useElements,
|
|
470
|
+
CardElement,
|
|
471
|
+
PaymentElement,
|
|
472
|
+
} from '@stripe/react-stripe-js';
|
|
473
|
+
import type { StripeError } from '@stripe/stripe-js';
|
|
474
|
+
|
|
475
|
+
interface PaymentFormProps {
|
|
476
|
+
amount: number;
|
|
477
|
+
currency?: string;
|
|
478
|
+
onSuccess: (paymentIntentId: string) => void;
|
|
479
|
+
onError: (error: string) => void;
|
|
480
|
+
customerId?: string;
|
|
481
|
+
metadata?: Record<string, string>;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export const PaymentForm: React.FC<PaymentFormProps> = ({
|
|
485
|
+
amount,
|
|
486
|
+
currency = 'usd',
|
|
487
|
+
onSuccess,
|
|
488
|
+
onError,
|
|
489
|
+
customerId,
|
|
490
|
+
metadata,
|
|
491
|
+
}) => {
|
|
492
|
+
const stripe = useStripe();
|
|
493
|
+
const elements = useElements();
|
|
494
|
+
const [loading, setLoading] = useState(false);
|
|
495
|
+
const [error, setError] = useState<string | null>(null);
|
|
496
|
+
|
|
497
|
+
const handleSubmit = async (event: React.FormEvent) => {
|
|
498
|
+
event.preventDefault();
|
|
499
|
+
|
|
500
|
+
if (!stripe || !elements) {
|
|
501
|
+
// Stripe.js hasn't loaded yet
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
setLoading(true);
|
|
506
|
+
setError(null);
|
|
507
|
+
|
|
508
|
+
try {
|
|
509
|
+
// Create payment intent on backend
|
|
510
|
+
const response = await fetch('/api/payments/intent', {
|
|
511
|
+
method: 'POST',
|
|
512
|
+
headers: { 'Content-Type': 'application/json' },
|
|
513
|
+
body: JSON.stringify({
|
|
514
|
+
amount,
|
|
515
|
+
currency,
|
|
516
|
+
customerId,
|
|
517
|
+
metadata,
|
|
518
|
+
}),
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
const { clientSecret } = await response.json();
|
|
522
|
+
|
|
523
|
+
// Confirm payment with Stripe.js
|
|
524
|
+
const { error: stripeError, paymentIntent } = await stripe.confirmCardPayment(
|
|
525
|
+
clientSecret,
|
|
526
|
+
{
|
|
527
|
+
payment_method: {
|
|
528
|
+
card: elements.getElement(CardElement)!,
|
|
529
|
+
billing_details: {
|
|
530
|
+
// Add billing details if collected
|
|
531
|
+
},
|
|
532
|
+
},
|
|
533
|
+
}
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
if (stripeError) {
|
|
537
|
+
setError(stripeError.message || 'Payment failed');
|
|
538
|
+
onError(stripeError.message || 'Payment failed');
|
|
539
|
+
} else if (paymentIntent && paymentIntent.status === 'succeeded') {
|
|
540
|
+
onSuccess(paymentIntent.id);
|
|
541
|
+
}
|
|
542
|
+
} catch (err) {
|
|
543
|
+
const errorMessage = err instanceof Error ? err.message : 'Payment failed';
|
|
544
|
+
setError(errorMessage);
|
|
545
|
+
onError(errorMessage);
|
|
546
|
+
} finally {
|
|
547
|
+
setLoading(false);
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
return (
|
|
552
|
+
<form onSubmit={handleSubmit} className="max-w-md mx-auto p-6">
|
|
553
|
+
<div className="mb-6">
|
|
554
|
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
555
|
+
Card Details
|
|
556
|
+
</label>
|
|
557
|
+
<div className="border border-gray-300 rounded-lg p-3">
|
|
558
|
+
<CardElement
|
|
559
|
+
options={{
|
|
560
|
+
style: {
|
|
561
|
+
base: {
|
|
562
|
+
fontSize: '16px',
|
|
563
|
+
color: '#424770',
|
|
564
|
+
'::placeholder': {
|
|
565
|
+
color: '#aab7c4',
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
invalid: {
|
|
569
|
+
color: '#9e2146',
|
|
570
|
+
},
|
|
571
|
+
},
|
|
572
|
+
}}
|
|
573
|
+
/>
|
|
574
|
+
</div>
|
|
575
|
+
</div>
|
|
576
|
+
|
|
577
|
+
{error && (
|
|
578
|
+
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
|
|
579
|
+
{error}
|
|
580
|
+
</div>
|
|
581
|
+
)}
|
|
582
|
+
|
|
583
|
+
<button
|
|
584
|
+
type="submit"
|
|
585
|
+
disabled={!stripe || loading}
|
|
586
|
+
className="w-full bg-blue-600 text-white py-3 px-4 rounded-lg font-medium hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
|
|
587
|
+
>
|
|
588
|
+
{loading ? 'Processing...' : `Pay $${(amount / 100).toFixed(2)}`}
|
|
589
|
+
</button>
|
|
590
|
+
</form>
|
|
591
|
+
);
|
|
592
|
+
};
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
**Checkout Session Flow**:
|
|
596
|
+
|
|
597
|
+
```typescript
|
|
598
|
+
// src/components/CheckoutButton.tsx
|
|
599
|
+
import React, { useState } from 'react';
|
|
600
|
+
import { loadStripe } from '@stripe/stripe-js';
|
|
601
|
+
|
|
602
|
+
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);
|
|
603
|
+
|
|
604
|
+
interface CheckoutButtonProps {
|
|
605
|
+
amount: number;
|
|
606
|
+
currency?: string;
|
|
607
|
+
buttonText?: string;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
export const CheckoutButton: React.FC<CheckoutButtonProps> = ({
|
|
611
|
+
amount,
|
|
612
|
+
currency = 'usd',
|
|
613
|
+
buttonText = 'Checkout',
|
|
614
|
+
}) => {
|
|
615
|
+
const [loading, setLoading] = useState(false);
|
|
616
|
+
|
|
617
|
+
const handleCheckout = async () => {
|
|
618
|
+
setLoading(true);
|
|
619
|
+
|
|
620
|
+
try {
|
|
621
|
+
// Create checkout session
|
|
622
|
+
const response = await fetch('/api/payments/checkout', {
|
|
623
|
+
method: 'POST',
|
|
624
|
+
headers: { 'Content-Type': 'application/json' },
|
|
625
|
+
body: JSON.stringify({ amount, currency }),
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
const { sessionId } = await response.json();
|
|
629
|
+
|
|
630
|
+
// Redirect to Stripe Checkout
|
|
631
|
+
const stripe = await stripePromise;
|
|
632
|
+
if (stripe) {
|
|
633
|
+
const { error } = await stripe.redirectToCheckout({ sessionId });
|
|
634
|
+
if (error) {
|
|
635
|
+
console.error('Checkout error:', error);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
} catch (error) {
|
|
639
|
+
console.error('Checkout error:', error);
|
|
640
|
+
} finally {
|
|
641
|
+
setLoading(false);
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
return (
|
|
646
|
+
<button
|
|
647
|
+
onClick={handleCheckout}
|
|
648
|
+
disabled={loading}
|
|
649
|
+
className="bg-blue-600 text-white py-2 px-6 rounded-lg font-medium hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
|
|
650
|
+
>
|
|
651
|
+
{loading ? 'Loading...' : buttonText}
|
|
652
|
+
</button>
|
|
653
|
+
);
|
|
654
|
+
};
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
### 4. Testing
|
|
658
|
+
|
|
659
|
+
**Test Cards**:
|
|
660
|
+
|
|
661
|
+
```typescript
|
|
662
|
+
// Test card numbers for different scenarios
|
|
663
|
+
export const TEST_CARDS = {
|
|
664
|
+
// Success
|
|
665
|
+
VISA_SUCCESS: '4242424242424242',
|
|
666
|
+
VISA_DEBIT: '4000056655665556',
|
|
667
|
+
MASTERCARD: '5555555555554444',
|
|
668
|
+
|
|
669
|
+
// Authentication required
|
|
670
|
+
THREE_D_SECURE: '4000002500003155',
|
|
671
|
+
|
|
672
|
+
// Failure scenarios
|
|
673
|
+
CARD_DECLINED: '4000000000000002',
|
|
674
|
+
INSUFFICIENT_FUNDS: '4000000000009995',
|
|
675
|
+
LOST_CARD: '4000000000009987',
|
|
676
|
+
STOLEN_CARD: '4000000000009979',
|
|
677
|
+
EXPIRED_CARD: '4000000000000069',
|
|
678
|
+
INCORRECT_CVC: '4000000000000127',
|
|
679
|
+
PROCESSING_ERROR: '4000000000000119',
|
|
680
|
+
|
|
681
|
+
// Special cases
|
|
682
|
+
DISPUTE: '4000000000000259',
|
|
683
|
+
FRAUD: '4100000000000019',
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
// Any future expiry date (e.g., 12/34)
|
|
687
|
+
// Any 3-digit CVC
|
|
688
|
+
// Any postal code
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
**Integration Test**:
|
|
692
|
+
|
|
693
|
+
```typescript
|
|
694
|
+
// tests/integration/payment.test.ts
|
|
695
|
+
import { paymentService } from '../../src/services/payment.service';
|
|
696
|
+
import Stripe from 'stripe';
|
|
697
|
+
|
|
698
|
+
describe('Payment Service Integration', () => {
|
|
699
|
+
describe('Payment Intent', () => {
|
|
700
|
+
it('should create a payment intent', async () => {
|
|
701
|
+
const paymentIntent = await paymentService.createPaymentIntent({
|
|
702
|
+
amount: 1000,
|
|
703
|
+
currency: 'usd',
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
expect(paymentIntent).toBeDefined();
|
|
707
|
+
expect(paymentIntent.amount).toBe(1000);
|
|
708
|
+
expect(paymentIntent.currency).toBe('usd');
|
|
709
|
+
expect(paymentIntent.status).toBe('requires_payment_method');
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
it('should confirm payment intent with test card', async () => {
|
|
713
|
+
// Create payment intent
|
|
714
|
+
const paymentIntent = await paymentService.createPaymentIntent({
|
|
715
|
+
amount: 1000,
|
|
716
|
+
currency: 'usd',
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
// Create test payment method
|
|
720
|
+
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
|
|
721
|
+
const paymentMethod = await stripe.paymentMethods.create({
|
|
722
|
+
type: 'card',
|
|
723
|
+
card: {
|
|
724
|
+
number: '4242424242424242',
|
|
725
|
+
exp_month: 12,
|
|
726
|
+
exp_year: 2034,
|
|
727
|
+
cvc: '123',
|
|
728
|
+
},
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
// Confirm payment
|
|
732
|
+
const confirmed = await paymentService.confirmPaymentIntent(
|
|
733
|
+
paymentIntent.id,
|
|
734
|
+
paymentMethod.id
|
|
735
|
+
);
|
|
736
|
+
|
|
737
|
+
expect(confirmed.status).toBe('succeeded');
|
|
738
|
+
});
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
describe('Customer Management', () => {
|
|
742
|
+
it('should create a customer', async () => {
|
|
743
|
+
const customer = await paymentService.createCustomer({
|
|
744
|
+
email: 'test@example.com',
|
|
745
|
+
name: 'Test User',
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
expect(customer).toBeDefined();
|
|
749
|
+
expect(customer.email).toBe('test@example.com');
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
it('should attach payment method to customer', async () => {
|
|
753
|
+
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
|
|
754
|
+
|
|
755
|
+
// Create customer
|
|
756
|
+
const customer = await paymentService.createCustomer({
|
|
757
|
+
email: 'test@example.com',
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
// Create payment method
|
|
761
|
+
const paymentMethod = await stripe.paymentMethods.create({
|
|
762
|
+
type: 'card',
|
|
763
|
+
card: {
|
|
764
|
+
number: '4242424242424242',
|
|
765
|
+
exp_month: 12,
|
|
766
|
+
exp_year: 2034,
|
|
767
|
+
cvc: '123',
|
|
768
|
+
},
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
// Attach payment method
|
|
772
|
+
const attached = await paymentService.attachPaymentMethod(
|
|
773
|
+
paymentMethod.id,
|
|
774
|
+
customer.id
|
|
775
|
+
);
|
|
776
|
+
|
|
777
|
+
expect(attached.customer).toBe(customer.id);
|
|
778
|
+
});
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
describe('Refunds', () => {
|
|
782
|
+
it('should create a refund', async () => {
|
|
783
|
+
// First create and confirm a payment
|
|
784
|
+
const paymentIntent = await paymentService.createPaymentIntent({
|
|
785
|
+
amount: 1000,
|
|
786
|
+
currency: 'usd',
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
|
|
790
|
+
const paymentMethod = await stripe.paymentMethods.create({
|
|
791
|
+
type: 'card',
|
|
792
|
+
card: {
|
|
793
|
+
number: '4242424242424242',
|
|
794
|
+
exp_month: 12,
|
|
795
|
+
exp_year: 2034,
|
|
796
|
+
cvc: '123',
|
|
797
|
+
},
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
await paymentService.confirmPaymentIntent(paymentIntent.id, paymentMethod.id);
|
|
801
|
+
|
|
802
|
+
// Create refund
|
|
803
|
+
const refund = await paymentService.createRefund({
|
|
804
|
+
paymentIntentId: paymentIntent.id,
|
|
805
|
+
reason: 'requested_by_customer',
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
expect(refund).toBeDefined();
|
|
809
|
+
expect(refund.status).toBe('succeeded');
|
|
810
|
+
});
|
|
811
|
+
});
|
|
812
|
+
});
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
### 5. Security Checklist
|
|
816
|
+
|
|
817
|
+
**Backend Security**:
|
|
818
|
+
- [ ] NEVER log full card numbers or CVV
|
|
819
|
+
- [ ] Use HTTPS only (enforce TLS 1.2+)
|
|
820
|
+
- [ ] Validate webhook signatures
|
|
821
|
+
- [ ] Implement rate limiting on payment endpoints
|
|
822
|
+
- [ ] Store Stripe IDs, not card details
|
|
823
|
+
- [ ] Use environment variables for keys
|
|
824
|
+
- [ ] Implement idempotency keys for retries
|
|
825
|
+
- [ ] Sanitize user inputs
|
|
826
|
+
- [ ] Enable CSRF protection
|
|
827
|
+
- [ ] Use secure session management
|
|
828
|
+
|
|
829
|
+
**Frontend Security**:
|
|
830
|
+
- [ ] Use Stripe.js (never raw card inputs)
|
|
831
|
+
- [ ] Load Stripe.js from CDN (integrity check)
|
|
832
|
+
- [ ] Never send card data to your server
|
|
833
|
+
- [ ] Implement CSP headers
|
|
834
|
+
- [ ] Use HTTPS only
|
|
835
|
+
- [ ] Clear sensitive data from memory
|
|
836
|
+
- [ ] Disable autocomplete on card fields
|
|
837
|
+
- [ ] Implement proper error handling
|
|
838
|
+
|
|
839
|
+
**Monitoring**:
|
|
840
|
+
- [ ] Log all payment attempts
|
|
841
|
+
- [ ] Monitor failed payment rates
|
|
842
|
+
- [ ] Set up alerts for unusual activity
|
|
843
|
+
- [ ] Track refund rates
|
|
844
|
+
- [ ] Monitor webhook delivery
|
|
845
|
+
- [ ] Implement fraud detection
|
|
846
|
+
|
|
847
|
+
### 6. Production Deployment
|
|
848
|
+
|
|
849
|
+
**Pre-launch Checklist**:
|
|
850
|
+
|
|
851
|
+
1. **Update API Keys**:
|
|
852
|
+
- Switch from test keys (`sk_test_`, `pk_test_`) to live keys
|
|
853
|
+
- Update webhook endpoint with live webhook secret
|
|
854
|
+
- Test with live mode in Stripe Dashboard
|
|
855
|
+
|
|
856
|
+
2. **Webhook Configuration**:
|
|
857
|
+
```bash
|
|
858
|
+
# Register webhook in Stripe Dashboard
|
|
859
|
+
# URL: https://yourdomain.com/api/webhooks/stripe
|
|
860
|
+
# Events: payment_intent.succeeded, payment_intent.payment_failed,
|
|
861
|
+
# customer.subscription.*, charge.refunded
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
3. **Enable Radar** (fraud detection):
|
|
865
|
+
- Configure Radar rules in Stripe Dashboard
|
|
866
|
+
- Enable 3D Secure for high-risk payments
|
|
867
|
+
- Set up risk score thresholds
|
|
868
|
+
|
|
869
|
+
4. **Tax Configuration**:
|
|
870
|
+
- Enable Stripe Tax if needed
|
|
871
|
+
- Configure tax rates by location
|
|
872
|
+
- Set up tax reporting
|
|
873
|
+
|
|
874
|
+
5. **Business Verification**:
|
|
875
|
+
- Complete business verification in Stripe
|
|
876
|
+
- Add business information
|
|
877
|
+
- Verify bank account for payouts
|
|
878
|
+
|
|
879
|
+
6. **Monitoring**:
|
|
880
|
+
- Set up Sentry or similar for error tracking
|
|
881
|
+
- Configure log aggregation (Datadog, Splunk)
|
|
882
|
+
- Set up uptime monitoring for webhook endpoint
|
|
883
|
+
- Create alerts for failed payments
|
|
884
|
+
|
|
885
|
+
## Output Deliverables
|
|
886
|
+
|
|
887
|
+
When you complete this setup, provide:
|
|
888
|
+
|
|
889
|
+
1. **Configured Files**:
|
|
890
|
+
- `.env` template with all required variables
|
|
891
|
+
- Backend service with payment methods
|
|
892
|
+
- API routes with error handling
|
|
893
|
+
- Frontend components (PaymentForm, CheckoutButton)
|
|
894
|
+
|
|
895
|
+
2. **Documentation**:
|
|
896
|
+
- API endpoint documentation
|
|
897
|
+
- Testing guide with test cards
|
|
898
|
+
- Deployment checklist
|
|
899
|
+
- Security audit report
|
|
900
|
+
|
|
901
|
+
3. **Testing**:
|
|
902
|
+
- Integration tests for payment flows
|
|
903
|
+
- Test scenarios for edge cases
|
|
904
|
+
- Webhook handling tests
|
|
905
|
+
|
|
906
|
+
4. **Deployment**:
|
|
907
|
+
- Environment-specific configurations
|
|
908
|
+
- Database migration scripts (if storing payment records)
|
|
909
|
+
- Monitoring setup guide
|
|
910
|
+
|
|
911
|
+
## Resources
|
|
912
|
+
|
|
913
|
+
- **Stripe Documentation**: https://stripe.com/docs
|
|
914
|
+
- **Stripe.js Reference**: https://stripe.com/docs/js
|
|
915
|
+
- **Webhook Testing**: Use Stripe CLI (`stripe listen --forward-to localhost:3000/api/webhooks/stripe`)
|
|
916
|
+
- **Test Cards**: https://stripe.com/docs/testing
|
|
917
|
+
|
|
918
|
+
## Best Practices
|
|
919
|
+
|
|
920
|
+
1. **Always use Stripe.js** for card collection (PCI compliance)
|
|
921
|
+
2. **Verify webhooks** with signature validation
|
|
922
|
+
3. **Handle errors gracefully** with user-friendly messages
|
|
923
|
+
4. **Test thoroughly** with all test cards before production
|
|
924
|
+
5. **Monitor payment success rates** and investigate drops
|
|
925
|
+
6. **Implement retry logic** for API failures
|
|
926
|
+
7. **Use metadata** to link payments to your database records
|
|
927
|
+
8. **Never expose secret keys** in frontend code
|
|
928
|
+
9. **Implement idempotency** for payment operations
|
|
929
|
+
10. **Keep Stripe.js updated** to latest version
|
|
930
|
+
|
|
931
|
+
Start with test mode, verify all flows work correctly, then switch to live mode with the same code.
|