932 lines
24 KiB
Markdown
932 lines
24 KiB
Markdown
# /specweave-payments:stripe-setup
|
|
|
|
Complete Stripe integration setup guide with production-ready code templates, security best practices, and testing workflows.
|
|
|
|
You are a payment integration expert who implements secure, PCI-compliant Stripe payment systems.
|
|
|
|
## Your Task
|
|
|
|
Set up complete Stripe payment integration with checkout flows, webhook handling, subscription billing, and customer management.
|
|
|
|
### 1. Environment Setup
|
|
|
|
**Install Dependencies**:
|
|
|
|
```bash
|
|
# Node.js
|
|
npm install stripe @stripe/stripe-js dotenv
|
|
|
|
# Python
|
|
pip install stripe python-dotenv
|
|
|
|
# Ruby
|
|
gem install stripe dotenv
|
|
|
|
# PHP
|
|
composer require stripe/stripe-php vlucas/phpdotenv
|
|
```
|
|
|
|
**Environment Variables**:
|
|
|
|
```bash
|
|
# .env (NEVER commit this file!)
|
|
# Get keys from https://dashboard.stripe.com/apikeys
|
|
|
|
# Test mode (development)
|
|
STRIPE_PUBLISHABLE_KEY=pk_test_51...
|
|
STRIPE_SECRET_KEY=sk_test_51...
|
|
STRIPE_WEBHOOK_SECRET=whsec_...
|
|
|
|
# Live mode (production)
|
|
# STRIPE_PUBLISHABLE_KEY=pk_live_51...
|
|
# STRIPE_SECRET_KEY=sk_live_51...
|
|
# STRIPE_WEBHOOK_SECRET=whsec_...
|
|
|
|
# App configuration
|
|
STRIPE_SUCCESS_URL=https://yourdomain.com/success
|
|
STRIPE_CANCEL_URL=https://yourdomain.com/cancel
|
|
STRIPE_CURRENCY=usd
|
|
```
|
|
|
|
### 2. Backend Setup (Node.js/Express)
|
|
|
|
**Stripe Client Initialization**:
|
|
|
|
```typescript
|
|
// src/config/stripe.ts
|
|
import Stripe from 'stripe';
|
|
import dotenv from 'dotenv';
|
|
|
|
dotenv.config();
|
|
|
|
if (!process.env.STRIPE_SECRET_KEY) {
|
|
throw new Error('STRIPE_SECRET_KEY is not set in environment variables');
|
|
}
|
|
|
|
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
|
|
apiVersion: '2023-10-16',
|
|
typescript: true,
|
|
maxNetworkRetries: 2,
|
|
timeout: 10000, // 10 seconds
|
|
});
|
|
|
|
export const STRIPE_CONFIG = {
|
|
publishableKey: process.env.STRIPE_PUBLISHABLE_KEY!,
|
|
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
|
|
successUrl: process.env.STRIPE_SUCCESS_URL || 'http://localhost:3000/success',
|
|
cancelUrl: process.env.STRIPE_CANCEL_URL || 'http://localhost:3000/cancel',
|
|
currency: process.env.STRIPE_CURRENCY || 'usd',
|
|
};
|
|
```
|
|
|
|
**Payment Service**:
|
|
|
|
```typescript
|
|
// src/services/payment.service.ts
|
|
import { stripe } from '../config/stripe';
|
|
import type Stripe from 'stripe';
|
|
|
|
export class PaymentService {
|
|
/**
|
|
* Create a one-time payment checkout session
|
|
*/
|
|
async createCheckoutSession(params: {
|
|
amount: number;
|
|
currency?: string;
|
|
customerId?: string;
|
|
metadata?: Record<string, string>;
|
|
}): Promise<Stripe.Checkout.Session> {
|
|
try {
|
|
const session = await stripe.checkout.sessions.create({
|
|
payment_method_types: ['card'],
|
|
line_items: [
|
|
{
|
|
price_data: {
|
|
currency: params.currency || 'usd',
|
|
product_data: {
|
|
name: 'Payment',
|
|
description: 'One-time payment',
|
|
},
|
|
unit_amount: params.amount, // Amount in cents
|
|
},
|
|
quantity: 1,
|
|
},
|
|
],
|
|
mode: 'payment',
|
|
success_url: `${process.env.STRIPE_SUCCESS_URL}?session_id={CHECKOUT_SESSION_ID}`,
|
|
cancel_url: process.env.STRIPE_CANCEL_URL,
|
|
customer: params.customerId,
|
|
metadata: params.metadata,
|
|
// Enable automatic tax calculation (optional)
|
|
automatic_tax: { enabled: false },
|
|
// Customer email collection
|
|
customer_email: params.customerId ? undefined : '',
|
|
});
|
|
|
|
return session;
|
|
} catch (error) {
|
|
console.error('Failed to create checkout session:', error);
|
|
throw new Error('Payment session creation failed');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a payment intent for custom checkout UI
|
|
*/
|
|
async createPaymentIntent(params: {
|
|
amount: number;
|
|
currency?: string;
|
|
customerId?: string;
|
|
paymentMethodTypes?: string[];
|
|
metadata?: Record<string, string>;
|
|
}): Promise<Stripe.PaymentIntent> {
|
|
try {
|
|
const paymentIntent = await stripe.paymentIntents.create({
|
|
amount: params.amount,
|
|
currency: params.currency || 'usd',
|
|
customer: params.customerId,
|
|
payment_method_types: params.paymentMethodTypes || ['card'],
|
|
metadata: params.metadata,
|
|
// Automatic payment methods (enables more payment methods)
|
|
automatic_payment_methods: {
|
|
enabled: true,
|
|
allow_redirects: 'never', // or 'always' for redirect-based methods
|
|
},
|
|
});
|
|
|
|
return paymentIntent;
|
|
} catch (error) {
|
|
console.error('Failed to create payment intent:', error);
|
|
throw new Error('Payment intent creation failed');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieve a payment intent
|
|
*/
|
|
async getPaymentIntent(paymentIntentId: string): Promise<Stripe.PaymentIntent> {
|
|
try {
|
|
return await stripe.paymentIntents.retrieve(paymentIntentId);
|
|
} catch (error) {
|
|
console.error('Failed to retrieve payment intent:', error);
|
|
throw new Error('Payment intent retrieval failed');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Confirm a payment intent (server-side confirmation)
|
|
*/
|
|
async confirmPaymentIntent(
|
|
paymentIntentId: string,
|
|
paymentMethodId: string
|
|
): Promise<Stripe.PaymentIntent> {
|
|
try {
|
|
return await stripe.paymentIntents.confirm(paymentIntentId, {
|
|
payment_method: paymentMethodId,
|
|
});
|
|
} catch (error) {
|
|
console.error('Failed to confirm payment intent:', error);
|
|
throw new Error('Payment confirmation failed');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create or update a customer
|
|
*/
|
|
async createCustomer(params: {
|
|
email: string;
|
|
name?: string;
|
|
phone?: string;
|
|
metadata?: Record<string, string>;
|
|
paymentMethodId?: string;
|
|
}): Promise<Stripe.Customer> {
|
|
try {
|
|
const customer = await stripe.customers.create({
|
|
email: params.email,
|
|
name: params.name,
|
|
phone: params.phone,
|
|
metadata: params.metadata,
|
|
payment_method: params.paymentMethodId,
|
|
invoice_settings: params.paymentMethodId
|
|
? {
|
|
default_payment_method: params.paymentMethodId,
|
|
}
|
|
: undefined,
|
|
});
|
|
|
|
return customer;
|
|
} catch (error) {
|
|
console.error('Failed to create customer:', error);
|
|
throw new Error('Customer creation failed');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Attach a payment method to a customer
|
|
*/
|
|
async attachPaymentMethod(
|
|
paymentMethodId: string,
|
|
customerId: string,
|
|
setAsDefault = true
|
|
): Promise<Stripe.PaymentMethod> {
|
|
try {
|
|
// Attach payment method
|
|
const paymentMethod = await stripe.paymentMethods.attach(paymentMethodId, {
|
|
customer: customerId,
|
|
});
|
|
|
|
// Set as default if requested
|
|
if (setAsDefault) {
|
|
await stripe.customers.update(customerId, {
|
|
invoice_settings: {
|
|
default_payment_method: paymentMethodId,
|
|
},
|
|
});
|
|
}
|
|
|
|
return paymentMethod;
|
|
} catch (error) {
|
|
console.error('Failed to attach payment method:', error);
|
|
throw new Error('Payment method attachment failed');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* List customer payment methods
|
|
*/
|
|
async listPaymentMethods(customerId: string): Promise<Stripe.PaymentMethod[]> {
|
|
try {
|
|
const paymentMethods = await stripe.paymentMethods.list({
|
|
customer: customerId,
|
|
type: 'card',
|
|
});
|
|
|
|
return paymentMethods.data;
|
|
} catch (error) {
|
|
console.error('Failed to list payment methods:', error);
|
|
throw new Error('Payment method listing failed');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a refund
|
|
*/
|
|
async createRefund(params: {
|
|
paymentIntentId?: string;
|
|
chargeId?: string;
|
|
amount?: number; // Partial refund amount in cents
|
|
reason?: 'duplicate' | 'fraudulent' | 'requested_by_customer';
|
|
metadata?: Record<string, string>;
|
|
}): Promise<Stripe.Refund> {
|
|
try {
|
|
const refund = await stripe.refunds.create({
|
|
payment_intent: params.paymentIntentId,
|
|
charge: params.chargeId,
|
|
amount: params.amount,
|
|
reason: params.reason,
|
|
metadata: params.metadata,
|
|
});
|
|
|
|
return refund;
|
|
} catch (error) {
|
|
console.error('Failed to create refund:', error);
|
|
throw new Error('Refund creation failed');
|
|
}
|
|
}
|
|
}
|
|
|
|
export const paymentService = new PaymentService();
|
|
```
|
|
|
|
**Express API Routes**:
|
|
|
|
```typescript
|
|
// src/routes/payment.routes.ts
|
|
import { Router, Request, Response } from 'express';
|
|
import { paymentService } from '../services/payment.service';
|
|
|
|
const router = Router();
|
|
|
|
/**
|
|
* POST /api/payments/checkout
|
|
* Create a checkout session
|
|
*/
|
|
router.post('/checkout', async (req: Request, res: Response) => {
|
|
try {
|
|
const { amount, currency, customerId, metadata } = req.body;
|
|
|
|
// Validate amount
|
|
if (!amount || amount <= 0) {
|
|
return res.status(400).json({ error: 'Invalid amount' });
|
|
}
|
|
|
|
const session = await paymentService.createCheckoutSession({
|
|
amount,
|
|
currency,
|
|
customerId,
|
|
metadata,
|
|
});
|
|
|
|
res.json({ sessionId: session.id, url: session.url });
|
|
} catch (error) {
|
|
console.error('Checkout error:', error);
|
|
res.status(500).json({ error: 'Failed to create checkout session' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/payments/intent
|
|
* Create a payment intent for custom UI
|
|
*/
|
|
router.post('/intent', async (req: Request, res: Response) => {
|
|
try {
|
|
const { amount, currency, customerId, metadata } = req.body;
|
|
|
|
if (!amount || amount <= 0) {
|
|
return res.status(400).json({ error: 'Invalid amount' });
|
|
}
|
|
|
|
const paymentIntent = await paymentService.createPaymentIntent({
|
|
amount,
|
|
currency,
|
|
customerId,
|
|
metadata,
|
|
});
|
|
|
|
res.json({ clientSecret: paymentIntent.client_secret });
|
|
} catch (error) {
|
|
console.error('Payment intent error:', error);
|
|
res.status(500).json({ error: 'Failed to create payment intent' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/payments/customers
|
|
* Create a customer
|
|
*/
|
|
router.post('/customers', async (req: Request, res: Response) => {
|
|
try {
|
|
const { email, name, phone, metadata } = req.body;
|
|
|
|
if (!email) {
|
|
return res.status(400).json({ error: 'Email is required' });
|
|
}
|
|
|
|
const customer = await paymentService.createCustomer({
|
|
email,
|
|
name,
|
|
phone,
|
|
metadata,
|
|
});
|
|
|
|
res.json({ customerId: customer.id });
|
|
} catch (error) {
|
|
console.error('Customer creation error:', error);
|
|
res.status(500).json({ error: 'Failed to create customer' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/payments/refunds
|
|
* Create a refund
|
|
*/
|
|
router.post('/refunds', async (req: Request, res: Response) => {
|
|
try {
|
|
const { paymentIntentId, amount, reason, metadata } = req.body;
|
|
|
|
if (!paymentIntentId) {
|
|
return res.status(400).json({ error: 'Payment Intent ID is required' });
|
|
}
|
|
|
|
const refund = await paymentService.createRefund({
|
|
paymentIntentId,
|
|
amount,
|
|
reason,
|
|
metadata,
|
|
});
|
|
|
|
res.json({ refundId: refund.id, status: refund.status });
|
|
} catch (error) {
|
|
console.error('Refund error:', error);
|
|
res.status(500).json({ error: 'Failed to create refund' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/payments/config
|
|
* Get public Stripe configuration
|
|
*/
|
|
router.get('/config', (req: Request, res: Response) => {
|
|
res.json({
|
|
publishableKey: process.env.STRIPE_PUBLISHABLE_KEY,
|
|
});
|
|
});
|
|
|
|
export default router;
|
|
```
|
|
|
|
### 3. Frontend Setup (React)
|
|
|
|
**Stripe Provider**:
|
|
|
|
```typescript
|
|
// src/providers/StripeProvider.tsx
|
|
import React from 'react';
|
|
import { Elements } from '@stripe/react-stripe-js';
|
|
import { loadStripe, Stripe } from '@stripe/stripe-js';
|
|
|
|
// Load Stripe.js outside of component to avoid recreating the instance
|
|
let stripePromise: Promise<Stripe | null>;
|
|
|
|
const getStripe = () => {
|
|
if (!stripePromise) {
|
|
const publishableKey = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY || '';
|
|
stripePromise = loadStripe(publishableKey);
|
|
}
|
|
return stripePromise;
|
|
};
|
|
|
|
interface StripeProviderProps {
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
export const StripeProvider: React.FC<StripeProviderProps> = ({ children }) => {
|
|
return (
|
|
<Elements stripe={getStripe()}>
|
|
{children}
|
|
</Elements>
|
|
);
|
|
};
|
|
```
|
|
|
|
**Payment Form Component**:
|
|
|
|
```typescript
|
|
// src/components/PaymentForm.tsx
|
|
import React, { useState } from 'react';
|
|
import {
|
|
useStripe,
|
|
useElements,
|
|
CardElement,
|
|
PaymentElement,
|
|
} from '@stripe/react-stripe-js';
|
|
import type { StripeError } from '@stripe/stripe-js';
|
|
|
|
interface PaymentFormProps {
|
|
amount: number;
|
|
currency?: string;
|
|
onSuccess: (paymentIntentId: string) => void;
|
|
onError: (error: string) => void;
|
|
customerId?: string;
|
|
metadata?: Record<string, string>;
|
|
}
|
|
|
|
export const PaymentForm: React.FC<PaymentFormProps> = ({
|
|
amount,
|
|
currency = 'usd',
|
|
onSuccess,
|
|
onError,
|
|
customerId,
|
|
metadata,
|
|
}) => {
|
|
const stripe = useStripe();
|
|
const elements = useElements();
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const handleSubmit = async (event: React.FormEvent) => {
|
|
event.preventDefault();
|
|
|
|
if (!stripe || !elements) {
|
|
// Stripe.js hasn't loaded yet
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
// Create payment intent on backend
|
|
const response = await fetch('/api/payments/intent', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
amount,
|
|
currency,
|
|
customerId,
|
|
metadata,
|
|
}),
|
|
});
|
|
|
|
const { clientSecret } = await response.json();
|
|
|
|
// Confirm payment with Stripe.js
|
|
const { error: stripeError, paymentIntent } = await stripe.confirmCardPayment(
|
|
clientSecret,
|
|
{
|
|
payment_method: {
|
|
card: elements.getElement(CardElement)!,
|
|
billing_details: {
|
|
// Add billing details if collected
|
|
},
|
|
},
|
|
}
|
|
);
|
|
|
|
if (stripeError) {
|
|
setError(stripeError.message || 'Payment failed');
|
|
onError(stripeError.message || 'Payment failed');
|
|
} else if (paymentIntent && paymentIntent.status === 'succeeded') {
|
|
onSuccess(paymentIntent.id);
|
|
}
|
|
} catch (err) {
|
|
const errorMessage = err instanceof Error ? err.message : 'Payment failed';
|
|
setError(errorMessage);
|
|
onError(errorMessage);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit} className="max-w-md mx-auto p-6">
|
|
<div className="mb-6">
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Card Details
|
|
</label>
|
|
<div className="border border-gray-300 rounded-lg p-3">
|
|
<CardElement
|
|
options={{
|
|
style: {
|
|
base: {
|
|
fontSize: '16px',
|
|
color: '#424770',
|
|
'::placeholder': {
|
|
color: '#aab7c4',
|
|
},
|
|
},
|
|
invalid: {
|
|
color: '#9e2146',
|
|
},
|
|
},
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={!stripe || loading}
|
|
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"
|
|
>
|
|
{loading ? 'Processing...' : `Pay $${(amount / 100).toFixed(2)}`}
|
|
</button>
|
|
</form>
|
|
);
|
|
};
|
|
```
|
|
|
|
**Checkout Session Flow**:
|
|
|
|
```typescript
|
|
// src/components/CheckoutButton.tsx
|
|
import React, { useState } from 'react';
|
|
import { loadStripe } from '@stripe/stripe-js';
|
|
|
|
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);
|
|
|
|
interface CheckoutButtonProps {
|
|
amount: number;
|
|
currency?: string;
|
|
buttonText?: string;
|
|
}
|
|
|
|
export const CheckoutButton: React.FC<CheckoutButtonProps> = ({
|
|
amount,
|
|
currency = 'usd',
|
|
buttonText = 'Checkout',
|
|
}) => {
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const handleCheckout = async () => {
|
|
setLoading(true);
|
|
|
|
try {
|
|
// Create checkout session
|
|
const response = await fetch('/api/payments/checkout', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ amount, currency }),
|
|
});
|
|
|
|
const { sessionId } = await response.json();
|
|
|
|
// Redirect to Stripe Checkout
|
|
const stripe = await stripePromise;
|
|
if (stripe) {
|
|
const { error } = await stripe.redirectToCheckout({ sessionId });
|
|
if (error) {
|
|
console.error('Checkout error:', error);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Checkout error:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<button
|
|
onClick={handleCheckout}
|
|
disabled={loading}
|
|
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"
|
|
>
|
|
{loading ? 'Loading...' : buttonText}
|
|
</button>
|
|
);
|
|
};
|
|
```
|
|
|
|
### 4. Testing
|
|
|
|
**Test Cards**:
|
|
|
|
```typescript
|
|
// Test card numbers for different scenarios
|
|
export const TEST_CARDS = {
|
|
// Success
|
|
VISA_SUCCESS: '4242424242424242',
|
|
VISA_DEBIT: '4000056655665556',
|
|
MASTERCARD: '5555555555554444',
|
|
|
|
// Authentication required
|
|
THREE_D_SECURE: '4000002500003155',
|
|
|
|
// Failure scenarios
|
|
CARD_DECLINED: '4000000000000002',
|
|
INSUFFICIENT_FUNDS: '4000000000009995',
|
|
LOST_CARD: '4000000000009987',
|
|
STOLEN_CARD: '4000000000009979',
|
|
EXPIRED_CARD: '4000000000000069',
|
|
INCORRECT_CVC: '4000000000000127',
|
|
PROCESSING_ERROR: '4000000000000119',
|
|
|
|
// Special cases
|
|
DISPUTE: '4000000000000259',
|
|
FRAUD: '4100000000000019',
|
|
};
|
|
|
|
// Any future expiry date (e.g., 12/34)
|
|
// Any 3-digit CVC
|
|
// Any postal code
|
|
```
|
|
|
|
**Integration Test**:
|
|
|
|
```typescript
|
|
// tests/integration/payment.test.ts
|
|
import { paymentService } from '../../src/services/payment.service';
|
|
import Stripe from 'stripe';
|
|
|
|
describe('Payment Service Integration', () => {
|
|
describe('Payment Intent', () => {
|
|
it('should create a payment intent', async () => {
|
|
const paymentIntent = await paymentService.createPaymentIntent({
|
|
amount: 1000,
|
|
currency: 'usd',
|
|
});
|
|
|
|
expect(paymentIntent).toBeDefined();
|
|
expect(paymentIntent.amount).toBe(1000);
|
|
expect(paymentIntent.currency).toBe('usd');
|
|
expect(paymentIntent.status).toBe('requires_payment_method');
|
|
});
|
|
|
|
it('should confirm payment intent with test card', async () => {
|
|
// Create payment intent
|
|
const paymentIntent = await paymentService.createPaymentIntent({
|
|
amount: 1000,
|
|
currency: 'usd',
|
|
});
|
|
|
|
// Create test payment method
|
|
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
|
|
const paymentMethod = await stripe.paymentMethods.create({
|
|
type: 'card',
|
|
card: {
|
|
number: '4242424242424242',
|
|
exp_month: 12,
|
|
exp_year: 2034,
|
|
cvc: '123',
|
|
},
|
|
});
|
|
|
|
// Confirm payment
|
|
const confirmed = await paymentService.confirmPaymentIntent(
|
|
paymentIntent.id,
|
|
paymentMethod.id
|
|
);
|
|
|
|
expect(confirmed.status).toBe('succeeded');
|
|
});
|
|
});
|
|
|
|
describe('Customer Management', () => {
|
|
it('should create a customer', async () => {
|
|
const customer = await paymentService.createCustomer({
|
|
email: 'test@example.com',
|
|
name: 'Test User',
|
|
});
|
|
|
|
expect(customer).toBeDefined();
|
|
expect(customer.email).toBe('test@example.com');
|
|
});
|
|
|
|
it('should attach payment method to customer', async () => {
|
|
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
|
|
|
|
// Create customer
|
|
const customer = await paymentService.createCustomer({
|
|
email: 'test@example.com',
|
|
});
|
|
|
|
// Create payment method
|
|
const paymentMethod = await stripe.paymentMethods.create({
|
|
type: 'card',
|
|
card: {
|
|
number: '4242424242424242',
|
|
exp_month: 12,
|
|
exp_year: 2034,
|
|
cvc: '123',
|
|
},
|
|
});
|
|
|
|
// Attach payment method
|
|
const attached = await paymentService.attachPaymentMethod(
|
|
paymentMethod.id,
|
|
customer.id
|
|
);
|
|
|
|
expect(attached.customer).toBe(customer.id);
|
|
});
|
|
});
|
|
|
|
describe('Refunds', () => {
|
|
it('should create a refund', async () => {
|
|
// First create and confirm a payment
|
|
const paymentIntent = await paymentService.createPaymentIntent({
|
|
amount: 1000,
|
|
currency: 'usd',
|
|
});
|
|
|
|
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
|
|
const paymentMethod = await stripe.paymentMethods.create({
|
|
type: 'card',
|
|
card: {
|
|
number: '4242424242424242',
|
|
exp_month: 12,
|
|
exp_year: 2034,
|
|
cvc: '123',
|
|
},
|
|
});
|
|
|
|
await paymentService.confirmPaymentIntent(paymentIntent.id, paymentMethod.id);
|
|
|
|
// Create refund
|
|
const refund = await paymentService.createRefund({
|
|
paymentIntentId: paymentIntent.id,
|
|
reason: 'requested_by_customer',
|
|
});
|
|
|
|
expect(refund).toBeDefined();
|
|
expect(refund.status).toBe('succeeded');
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
### 5. Security Checklist
|
|
|
|
**Backend Security**:
|
|
- [ ] NEVER log full card numbers or CVV
|
|
- [ ] Use HTTPS only (enforce TLS 1.2+)
|
|
- [ ] Validate webhook signatures
|
|
- [ ] Implement rate limiting on payment endpoints
|
|
- [ ] Store Stripe IDs, not card details
|
|
- [ ] Use environment variables for keys
|
|
- [ ] Implement idempotency keys for retries
|
|
- [ ] Sanitize user inputs
|
|
- [ ] Enable CSRF protection
|
|
- [ ] Use secure session management
|
|
|
|
**Frontend Security**:
|
|
- [ ] Use Stripe.js (never raw card inputs)
|
|
- [ ] Load Stripe.js from CDN (integrity check)
|
|
- [ ] Never send card data to your server
|
|
- [ ] Implement CSP headers
|
|
- [ ] Use HTTPS only
|
|
- [ ] Clear sensitive data from memory
|
|
- [ ] Disable autocomplete on card fields
|
|
- [ ] Implement proper error handling
|
|
|
|
**Monitoring**:
|
|
- [ ] Log all payment attempts
|
|
- [ ] Monitor failed payment rates
|
|
- [ ] Set up alerts for unusual activity
|
|
- [ ] Track refund rates
|
|
- [ ] Monitor webhook delivery
|
|
- [ ] Implement fraud detection
|
|
|
|
### 6. Production Deployment
|
|
|
|
**Pre-launch Checklist**:
|
|
|
|
1. **Update API Keys**:
|
|
- Switch from test keys (`sk_test_`, `pk_test_`) to live keys
|
|
- Update webhook endpoint with live webhook secret
|
|
- Test with live mode in Stripe Dashboard
|
|
|
|
2. **Webhook Configuration**:
|
|
```bash
|
|
# Register webhook in Stripe Dashboard
|
|
# URL: https://yourdomain.com/api/webhooks/stripe
|
|
# Events: payment_intent.succeeded, payment_intent.payment_failed,
|
|
# customer.subscription.*, charge.refunded
|
|
```
|
|
|
|
3. **Enable Radar** (fraud detection):
|
|
- Configure Radar rules in Stripe Dashboard
|
|
- Enable 3D Secure for high-risk payments
|
|
- Set up risk score thresholds
|
|
|
|
4. **Tax Configuration**:
|
|
- Enable Stripe Tax if needed
|
|
- Configure tax rates by location
|
|
- Set up tax reporting
|
|
|
|
5. **Business Verification**:
|
|
- Complete business verification in Stripe
|
|
- Add business information
|
|
- Verify bank account for payouts
|
|
|
|
6. **Monitoring**:
|
|
- Set up Sentry or similar for error tracking
|
|
- Configure log aggregation (Datadog, Splunk)
|
|
- Set up uptime monitoring for webhook endpoint
|
|
- Create alerts for failed payments
|
|
|
|
## Output Deliverables
|
|
|
|
When you complete this setup, provide:
|
|
|
|
1. **Configured Files**:
|
|
- `.env` template with all required variables
|
|
- Backend service with payment methods
|
|
- API routes with error handling
|
|
- Frontend components (PaymentForm, CheckoutButton)
|
|
|
|
2. **Documentation**:
|
|
- API endpoint documentation
|
|
- Testing guide with test cards
|
|
- Deployment checklist
|
|
- Security audit report
|
|
|
|
3. **Testing**:
|
|
- Integration tests for payment flows
|
|
- Test scenarios for edge cases
|
|
- Webhook handling tests
|
|
|
|
4. **Deployment**:
|
|
- Environment-specific configurations
|
|
- Database migration scripts (if storing payment records)
|
|
- Monitoring setup guide
|
|
|
|
## Resources
|
|
|
|
- **Stripe Documentation**: https://stripe.com/docs
|
|
- **Stripe.js Reference**: https://stripe.com/docs/js
|
|
- **Webhook Testing**: Use Stripe CLI (`stripe listen --forward-to localhost:3000/api/webhooks/stripe`)
|
|
- **Test Cards**: https://stripe.com/docs/testing
|
|
|
|
## Best Practices
|
|
|
|
1. **Always use Stripe.js** for card collection (PCI compliance)
|
|
2. **Verify webhooks** with signature validation
|
|
3. **Handle errors gracefully** with user-friendly messages
|
|
4. **Test thoroughly** with all test cards before production
|
|
5. **Monitor payment success rates** and investigate drops
|
|
6. **Implement retry logic** for API failures
|
|
7. **Use metadata** to link payments to your database records
|
|
8. **Never expose secret keys** in frontend code
|
|
9. **Implement idempotency** for payment operations
|
|
10. **Keep Stripe.js updated** to latest version
|
|
|
|
Start with test mode, verify all flows work correctly, then switch to live mode with the same code.
|