trading-platform/docs/02-definicion-modulos/OQI-005-payments-stripe/DEVELOPER-GUIDELINES.md
Adrian Flores Cortes 3d8bf17b72 docs(payments): Add Developer Guidelines (ST4.2.5)
Comprehensive developer guidelines for payment system development.

New Files:
- docs/.../OQI-005-payments-stripe/DEVELOPER-GUIDELINES.md (900+ lines)
  - Complete reference for payment development
  - PCI-DSS compliance rules (DO's and DON'Ts)
  - Backend development guidelines
  - Frontend development guidelines
  - Testing guidelines (unit + E2E)
  - Common pitfalls and how to avoid them
  - Code review checklist
  - Deployment checklist
  - Troubleshooting guide
  - Examples and templates

Sections:
1. Overview - Architecture summary, tech stack, compliance level
2. PCI-DSS Compliance Rules - What's allowed vs prohibited
3. Backend Development - File structure, endpoints, webhooks, database
4. Frontend Development - Stripe Elements, checkout flow, error handling
5. Testing Guidelines - Unit tests, E2E tests, component tests
6. Common Pitfalls - 5 common mistakes and how to avoid them
7. Code Review Checklist - Security, quality, Stripe integration
8. Deployment Checklist - Environment, security, testing, monitoring
9. Troubleshooting - Common issues and solutions
10. Examples & Templates - Complete flow examples

Key Guidelines:
 DO's:
  - Use Payment Intents (server-side processing)
  - Use Stripe Elements (client-side tokenization)
  - Verify webhook signatures
  - Store only tokens/IDs (pm_xxx, pi_xxx)
  - Use HTTPS everywhere
  - Log payment events (without sensitive data)
  - Write E2E tests for PCI-DSS compliance

 DON'Ts:
  - Accept card data in backend
  - Store PAN, CVV, or expiry in database
  - Create native card inputs
  - Store card data in React state
  - Skip webhook signature verification
  - Use HTTP (only HTTPS)
  - Log sensitive data

PCI-DSS Compliance:
 ALLOWED:
  - Store last 4 digits
  - Store card brand
  - Store Stripe tokens (pm_xxx, pi_xxx, cus_xxx)
  - Store customer name

 PROHIBITED:
  - Store full PAN (card number)
  - Store CVV/CVC
  - Store expiry date
  - Store PIN

Common Pitfalls:
1. Accepting card data in backend → Block sensitive fields
2. Storing full PAN in database → Use tokens only
3. Native card inputs → Use Stripe CardElement
4. Not verifying webhook signatures → Use constructEvent
5. Logging sensitive data → Filter sensitive fields

Code Examples:
- Wallet deposit flow (complete end-to-end)
- Subscription checkout (Stripe hosted)
- Payment Intent creation (backend)
- Stripe Elements integration (frontend)
- Webhook signature verification
- Database schema (safe vs prohibited)

Testing Examples:
- Unit tests (Stripe service mocked)
- E2E tests (PCI-DSS compliance)
- Component tests (CardElement rendering)
- Integration tests (webhook handling)

Deployment Checklist:
- Environment variables configured
- Stripe webhooks set up
- SSL/TLS enabled
- Security headers configured
- Rate limiting enabled
- All tests passing (45+ PCI-DSS tests)
- Monitoring and alerts configured

Target Audience:
- Backend developers (Express.js, TypeScript)
- Frontend developers (React, Stripe.js)
- DevOps engineers (deployment, monitoring)
- Code reviewers (security validation)
- New team members (onboarding)

Status: BLOCKER-002 (ST4.2) - Developer guidelines complete
Task: #5 ST4.2.5 - Actualizar developer guidelines pagos

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 22:03:47 -06:00

998 lines
25 KiB
Markdown

# Developer Guidelines: Payment System
**Epic:** OQI-005 - Payments & Stripe
**Blocker:** BLOCKER-002 (ST4.2.5)
**Version:** 1.0.0
**Last Updated:** 2026-01-26
**Status:** ✅ Complete
---
## Table of Contents
1. [Overview](#overview)
2. [PCI-DSS Compliance Rules](#pci-dss-compliance-rules)
3. [Backend Development](#backend-development)
4. [Frontend Development](#frontend-development)
5. [Testing Guidelines](#testing-guidelines)
6. [Common Pitfalls](#common-pitfalls)
7. [Code Review Checklist](#code-review-checklist)
8. [Deployment Checklist](#deployment-checklist)
9. [Troubleshooting](#troubleshooting)
10. [Examples & Templates](#examples--templates)
---
## Overview
### Architecture Summary
```
Frontend (React) → Backend (Express.js) → Stripe API
↓ ↓ ↓
CardElement Payment Intent Processing
(Stripe.js) (server-side) (Stripe servers)
```
**Key Principle:** ⚠️ **Card data NEVER touches our servers**
### Technology Stack
- **Backend:** Express.js, TypeScript, Stripe Node.js SDK
- **Frontend:** React, TypeScript, @stripe/stripe-js, @stripe/react-stripe-js
- **Database:** PostgreSQL (NO card data stored)
- **Payment Processor:** Stripe (Level 1 PCI-DSS certified)
### Compliance Level
**PCI-DSS SAQ-A** (Self-Assessment Questionnaire A)
- Simplest compliance path (22 requirements vs 300+)
- Achieved by delegating ALL card processing to Stripe
- **Required:** NO card data on our systems
---
## PCI-DSS Compliance Rules
### ✅ ALLOWED
**Backend:**
```typescript
// ✅ Create Payment Intent (server-side)
const paymentIntent = await stripe.paymentIntents.create({
amount: 10000,
currency: 'usd',
metadata: { userId, transactionId },
});
// ✅ Store safe identifiers
await db.query(
'INSERT INTO transactions (payment_intent_id, amount) VALUES ($1, $2)',
[paymentIntent.id, 100] // ← Safe: token ID, not card data
);
// ✅ Store card metadata (last4, brand)
await db.query(
'UPDATE payment_methods SET card_last4 = $1, card_brand = $2',
['4242', 'visa'] // ← Safe: not full PAN
);
```
**Frontend:**
```typescript
// ✅ Use Stripe Elements (hosted iframe)
import { CardElement } from '@stripe/react-stripe-js';
<CardElement options={cardElementOptions} />
// ✅ Confirm payment with Stripe
const { error, paymentIntent } = await stripe.confirmCardPayment(
clientSecret,
{ payment_method: { card: cardElement } } // ← Card data goes to Stripe
);
```
### ❌ PROHIBITED
**NEVER DO THIS:**
```typescript
// ❌ PROHIBITED: Accept card data in backend
export async function createPayment(req, res) {
const { cardNumber, cvv, expiryDate } = req.body; // ← VIOLATION!
// ... DO NOT DO THIS
}
// ❌ PROHIBITED: Store card data in database
await db.query(
'INSERT INTO payment_methods (card_number, cvv) VALUES ($1, $2)',
['4242424242424242', '123'] // ← SEVERE VIOLATION!
);
// ❌ PROHIBITED: Create native card input
<input
type="text"
name="cardNumber" // VIOLATION!
placeholder="Card Number"
/>
// ❌ PROHIBITED: Store card data in React state
const [cardNumber, setCardNumber] = useState(''); // ← VIOLATION!
// ❌ PROHIBITED: Log sensitive data
console.log('Card:', cardNumber, cvv); // ← VIOLATION!
```
**Consequences:**
- 💰 **Fines:** Up to $500,000 per incident
- 🚫 **Loss of payment processing:** Stripe account terminated
- ⚖️ **Legal liability:** GDPR violations
- 📰 **Reputation damage:** Customer trust lost
### PCI-DSS Quick Reference
| Action | Allowed | Example |
|--------|---------|---------|
| Store full PAN (card number) | ❌ NO | 4242424242424242 |
| Store CVV/CVC | ❌ NO | 123 |
| Store expiry date | ❌ NO | 12/25 |
| Store PIN | ❌ NO | 1234 |
| Store last 4 digits | ✅ YES | 4242 |
| Store card brand | ✅ YES | visa |
| Store Stripe token | ✅ YES | pm_xxx, pi_xxx |
| Store customer name | ✅ YES | John Doe |
**Rule of Thumb:** If it can be used to make a fraudulent charge, **DON'T STORE IT**.
---
## Backend Development
### File Structure
```
src/modules/payments/
├── controllers/
│ └── payments.controller.ts # REST API endpoints
├── services/
│ ├── stripe.service.ts # Stripe API wrapper
│ ├── wallet.service.ts # Wallet management
│ └── subscription.service.ts # Subscription logic
├── types/
│ └── payments.types.ts # TypeScript types
└── payments.routes.ts # Express routes
```
### Creating a New Payment Endpoint
**Step 1: Define Route**
```typescript
// payments.routes.ts
router.post(
'/wallet/deposit',
requireAuth, // ← Always require auth
createDeposit // ← Controller function
);
```
**Step 2: Create Controller**
```typescript
// controllers/payments.controller.ts
export async function createDeposit(req, res, next) {
try {
const authReq = req as AuthenticatedRequest;
const { amount, currency } = req.body;
// ✅ Validation
if (!amount || amount <= 0) {
res.status(400).json({ error: 'Invalid amount' });
return;
}
// ❌ Block sensitive data (CRITICAL!)
const sensitiveFields = ['cardNumber', 'cvv', 'pan', 'expiryDate'];
if (sensitiveFields.some(field => req.body[field])) {
logger.warn('Sensitive data blocked', { userId: authReq.user.id });
res.status(400).json({ error: 'Card data not allowed' });
return;
}
// ✅ Call service
const result = await stripeService.createPaymentIntent({
userId: authReq.user.id,
amount,
currency,
});
res.json({ success: true, data: result });
} catch (error) {
next(error); // ← Always use error middleware
}
}
```
**Step 3: Implement Service**
```typescript
// services/stripe.service.ts
export class StripeService {
async createPaymentIntent(params: {
userId: string;
amount: number;
currency: string;
}): Promise<{ clientSecret: string; paymentIntentId: string }> {
// ✅ Create Payment Intent (server-side)
const paymentIntent = await this.stripe.paymentIntents.create({
amount: Math.round(params.amount * 100), // Convert to cents
currency: params.currency.toLowerCase(),
metadata: {
userId: params.userId,
type: 'wallet_deposit',
},
});
// ✅ Store transaction in database (safe data only)
await db.query(
`INSERT INTO payments.transactions (
user_id, amount, currency, status, payment_intent_id
) VALUES ($1, $2, $3, $4, $5)`,
[params.userId, params.amount, params.currency, 'pending', paymentIntent.id]
);
// ✅ Return client secret (frontend will use this)
return {
clientSecret: paymentIntent.client_secret!,
paymentIntentId: paymentIntent.id,
};
}
}
```
### Webhook Implementation
**Critical:** Webhooks MUST verify Stripe signature
```typescript
// controllers/payments.controller.ts
export async function handleStripeWebhook(req, res, next) {
const signature = req.headers['stripe-signature'] as string;
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
try {
// ✅ CRITICAL: Verify signature (prevents spoofing)
const event = stripe.webhooks.constructEvent(
req.body, // ← Must be raw body (not JSON parsed)
signature,
webhookSecret
);
logger.info('Webhook received', {
type: event.type,
id: event.id,
});
// Handle event types
switch (event.type) {
case 'payment_intent.succeeded':
await handlePaymentSucceeded(event.data.object);
break;
case 'payment_intent.payment_failed':
await handlePaymentFailed(event.data.object);
break;
case 'customer.subscription.created':
await handleSubscriptionCreated(event.data.object);
break;
default:
logger.warn('Unhandled webhook event', { type: event.type });
}
res.json({ received: true });
} catch (err) {
// ❌ Invalid signature - REJECT
logger.error('Webhook verification failed', { error: err });
res.status(400).json({ error: 'Invalid signature' });
}
}
```
**Important:** Webhook endpoint must use raw body parser:
```typescript
// app.ts
import { raw } from 'express';
app.post(
'/api/v1/payments/webhook',
raw({ type: 'application/json' }), // ← Raw body for signature verification
handleStripeWebhook
);
```
### Database Schema Guidelines
**✅ ALLOWED columns:**
```sql
CREATE TABLE payments.transactions (
id UUID PRIMARY KEY,
user_id UUID NOT NULL,
amount DECIMAL(10,2),
currency VARCHAR(3),
status VARCHAR(50),
payment_intent_id VARCHAR(255), -- ✅ Stripe token
stripe_customer_id VARCHAR(255), -- ✅ Stripe ID
created_at TIMESTAMPTZ
);
```
**❌ PROHIBITED columns:**
```sql
-- ❌ NEVER create these columns!
card_number VARCHAR(16) -- PAN
cvv VARCHAR(4) -- Security code
expiry_date DATE -- Expiration
card_holder_name VARCHAR(100) -- Sensitive
pin VARCHAR(4) -- PIN
```
---
## Frontend Development
### Stripe Elements Setup
**Step 1: Wrap App with Stripe Provider**
```typescript
// App.tsx
import { Elements } from '@stripe/react-stripe-js';
import { loadStripe } from '@stripe/stripe-js';
const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY!);
function App() {
return (
<Elements stripe={stripePromise}>
<YourRoutes />
</Elements>
);
}
```
**Step 2: Create Payment Form**
```typescript
// DepositForm.tsx
import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js';
function DepositForm() {
const stripe = useStripe();
const elements = useElements();
const [amount, setAmount] = useState(100);
const [processing, setProcessing] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!stripe || !elements) {
return; // Stripe not loaded yet
}
setProcessing(true);
setError(null);
try {
// Step 1: Backend creates Payment Intent (NO card data sent)
const response = await apiClient.post('/api/v1/payments/wallet/deposit', {
amount, // ✅ Safe
currency: 'USD', // ✅ Safe
// ❌ NO cardNumber, cvv, expiryDate
});
const { clientSecret } = response.data.data;
// Step 2: Confirm payment with Stripe (card data goes to Stripe)
const cardElement = elements.getElement(CardElement);
const { error, paymentIntent } = await stripe.confirmCardPayment(
clientSecret,
{
payment_method: {
card: cardElement!, // ← Stripe iframe reference
},
}
);
if (error) {
setError(error.message || 'Payment failed');
} else if (paymentIntent.status === 'succeeded') {
// ✅ Success! (Backend webhook will update database)
onSuccess();
}
} catch (err) {
setError(err.message || 'An error occurred');
} finally {
setProcessing(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="number"
value={amount}
onChange={(e) => setAmount(Number(e.target.value))}
min="1"
/>
{/* ✅ Stripe CardElement = iframe from stripe.com */}
<CardElement
options={{
style: {
base: {
fontSize: '16px',
color: '#424770',
'::placeholder': { color: '#aab7c4' },
},
invalid: { color: '#9e2146' },
},
}}
/>
{error && <div className="error">{error}</div>}
<button type="submit" disabled={processing || !stripe}>
{processing ? 'Processing...' : `Deposit $${amount}`}
</button>
</form>
);
}
```
### Checkout Session Flow (Stripe Hosted)
For subscription payments, use Stripe Checkout (hosted page):
```typescript
// SubscriptionButton.tsx
async function handleSubscribe() {
// Backend creates Checkout Session
const response = await apiClient.post('/api/v1/payments/checkout', {
planId: 'plan_basic',
billingCycle: 'monthly',
successUrl: `${window.location.origin}/success`,
cancelUrl: `${window.location.origin}/cancel`,
});
const { url } = response.data.data;
// ✅ Redirect to Stripe hosted page
window.location.href = url; // ← User enters card on checkout.stripe.com
}
```
### Error Handling
```typescript
// Handle Stripe errors
const { error, paymentIntent } = await stripe.confirmCardPayment(clientSecret, {...});
if (error) {
// Stripe validation errors
switch (error.type) {
case 'card_error':
// Card was declined, invalid number, etc.
setError(error.message);
break;
case 'validation_error':
// Invalid parameters sent to Stripe
setError('Please check your input');
break;
case 'api_error':
// Stripe API issue
setError('Payment service unavailable');
break;
default:
setError('An unexpected error occurred');
}
}
```
---
## Testing Guidelines
### Backend Tests
**Unit Tests:**
```typescript
// stripe.service.test.ts
describe('StripeService', () => {
it('should create Payment Intent', async () => {
const mockStripe = {
paymentIntents: {
create: jest.fn().mockResolvedValue({
id: 'pi_test_123',
client_secret: 'pi_test_123_secret_456',
}),
},
};
const service = new StripeService(mockStripe);
const result = await service.createPaymentIntent({
userId: 'user_123',
amount: 100,
currency: 'usd',
});
expect(result.clientSecret).toBe('pi_test_123_secret_456');
expect(mockStripe.paymentIntents.create).toHaveBeenCalledWith({
amount: 10000,
currency: 'usd',
metadata: expect.objectContaining({ userId: 'user_123' }),
});
});
});
```
**E2E Tests (PCI-DSS Compliance):**
```typescript
// payments-pci-dss.test.ts
describe('PCI-DSS Compliance', () => {
it('should reject request with card data', async () => {
const response = await request(app)
.post('/api/v1/payments/wallet/deposit')
.set('Authorization', `Bearer ${token}`)
.send({
amount: 100,
cardNumber: '4242424242424242', // ← Attempt to send card data
})
.expect(400);
expect(response.body.error).toContain('Card data not allowed');
});
});
```
### Frontend Tests
**Component Tests:**
```typescript
// DepositForm.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Elements } from '@stripe/react-stripe-js';
describe('DepositForm', () => {
it('should render Stripe CardElement', () => {
render(
<Elements stripe={mockStripe}>
<DepositForm />
</Elements>
);
// ✅ Stripe CardElement rendered
expect(screen.getByTestId('stripe-card-element')).toBeInTheDocument();
// ❌ NO native card inputs
expect(screen.queryByPlaceholderText(/card number/i)).not.toBeInTheDocument();
});
it('should NOT store card data in state', () => {
const { container } = render(
<Elements stripe={mockStripe}>
<DepositForm />
</Elements>
);
const html = container.innerHTML;
// ❌ NO card data in DOM
expect(html).not.toContain('cardNumber');
expect(html).not.toContain('cvv');
});
});
```
---
## Common Pitfalls
### Pitfall 1: Accepting Card Data in Backend
**❌ WRONG:**
```typescript
export async function createPayment(req, res) {
const { cardNumber, cvv } = req.body; // ← VIOLATION!
// ...
}
```
**✅ CORRECT:**
```typescript
export async function createPayment(req, res) {
const { amount, currency } = req.body;
// Block sensitive data
if (req.body.cardNumber || req.body.cvv) {
return res.status(400).json({ error: 'Card data not allowed' });
}
// Create Payment Intent (safe)
const paymentIntent = await stripe.paymentIntents.create({ amount, currency });
res.json({ clientSecret: paymentIntent.client_secret });
}
```
### Pitfall 2: Storing Full PAN in Database
**❌ WRONG:**
```sql
CREATE TABLE payment_methods (
card_number VARCHAR(16) -- ← SEVERE VIOLATION!
);
```
**✅ CORRECT:**
```sql
CREATE TABLE payment_methods (
stripe_payment_method_id VARCHAR(255), -- ✅ Token
card_last4 VARCHAR(4), -- ✅ Last 4 digits
card_brand VARCHAR(50) -- ✅ Metadata
);
```
### Pitfall 3: Native Card Inputs
**❌ WRONG:**
```tsx
<input type="text" name="cardNumber" /> {/* VIOLATION! */}
```
**✅ CORRECT:**
```tsx
import { CardElement } from '@stripe/react-stripe-js';
<CardElement /> {/* Stripe iframe */}
```
### Pitfall 4: Not Verifying Webhook Signatures
**❌ WRONG:**
```typescript
export async function handleWebhook(req, res) {
const event = req.body; // ← NO VERIFICATION! Insecure!
// Anyone can spoof this endpoint
}
```
**✅ CORRECT:**
```typescript
export async function handleWebhook(req, res) {
const signature = req.headers['stripe-signature'];
const event = stripe.webhooks.constructEvent(
req.body,
signature,
webhookSecret // ← VERIFIED signature
);
}
```
### Pitfall 5: Logging Sensitive Data
**❌ WRONG:**
```typescript
console.log('Payment:', {
cardNumber: req.body.cardNumber, // ← VIOLATION!
cvv: req.body.cvv,
});
```
**✅ CORRECT:**
```typescript
logger.info('Payment created', {
userId,
amount,
paymentIntentId, // ← Safe token
// NO card data
});
```
---
## Code Review Checklist
**Before approving any payment-related PR, verify:**
### Security
- [ ] ❌ NO card data accepted in API (cardNumber, cvv, expiryDate)
- [ ] ❌ NO card data stored in database (PAN, CVV, expiry)
- [ ] ✅ Only Stripe tokens/IDs stored (pm_xxx, pi_xxx, cus_xxx)
- [ ] ✅ Webhook signatures verified (constructEvent)
- [ ] ✅ HTTPS enforced (no HTTP endpoints)
- [ ] ✅ Input validation on all endpoints
- [ ] ✅ No sensitive data in logs
### Code Quality
- [ ] TypeScript types defined
- [ ] Error handling implemented
- [ ] Logging added (info, warn, error)
- [ ] Tests added/updated (unit + E2E)
- [ ] Documentation updated
### Stripe Integration
- [ ] Payment Intents used (not deprecated APIs)
- [ ] Stripe Elements used in frontend (not native inputs)
- [ ] Client Secret format validated (pi_xxx_secret_yyy)
- [ ] Metadata included in Stripe calls
### Database
- [ ] Parameterized queries (NO SQL injection)
- [ ] Transactions used for multi-step operations
- [ ] Indexes created for query performance
- [ ] Migrations tested
---
## Deployment Checklist
**Before deploying payment features to production:**
### Environment Variables
- [ ] `STRIPE_SECRET_KEY` set (sk_live_...)
- [ ] `STRIPE_PUBLISHABLE_KEY` set (pk_live_...)
- [ ] `STRIPE_WEBHOOK_SECRET` set (whsec_...)
- [ ] `DATABASE_URL` configured
- [ ] `JWT_SECRET` set (strong random key)
### Stripe Configuration
- [ ] Live mode API keys configured
- [ ] Webhooks configured (payment_intent.*, customer.subscription.*)
- [ ] Webhook endpoint HTTPS URL added
- [ ] Test webhooks with Stripe CLI
- [ ] Radar fraud detection enabled (recommended)
### Security
- [ ] SSL/TLS certificate valid (HTTPS)
- [ ] HSTS header enabled
- [ ] CSP header configured (allow stripe.com)
- [ ] Rate limiting enabled (60 req/min recommended)
- [ ] Firewall rules configured
- [ ] Database backups enabled
### Testing
- [ ] All E2E tests passing (45+ tests)
- [ ] Manual testing completed
- [ ] Test payment with real card (Stripe test mode)
- [ ] Webhook handling verified
- [ ] Error scenarios tested
### Monitoring
- [ ] Logging configured (info, warn, error)
- [ ] Error tracking enabled (Sentry, etc.)
- [ ] Payment metrics dashboard
- [ ] Alerts configured (failed payments, webhook errors)
---
## Troubleshooting
### Common Issues
#### Issue 1: "No such payment_intent"
**Cause:** Invalid Payment Intent ID
**Solution:**
```typescript
// Verify Payment Intent ID format
if (!paymentIntentId.startsWith('pi_')) {
throw new Error('Invalid Payment Intent ID');
}
// Check Stripe dashboard for actual ID
```
#### Issue 2: "Webhook signature verification failed"
**Cause:** Invalid signature or wrong webhook secret
**Solution:**
```typescript
// 1. Verify webhook secret is correct
console.log('Webhook Secret:', process.env.STRIPE_WEBHOOK_SECRET);
// 2. Check raw body is used (not JSON parsed)
app.post('/webhook', raw({ type: 'application/json' }), handler);
// 3. Test with Stripe CLI
// stripe listen --forward-to localhost:3000/api/v1/payments/webhook
```
#### Issue 3: "CardElement not rendering"
**Cause:** Stripe not loaded or Elements context missing
**Solution:**
```typescript
// 1. Wrap with Elements provider
<Elements stripe={stripePromise}>
<DepositForm />
</Elements>
// 2. Check Stripe loaded
const stripe = useStripe();
if (!stripe) {
return <div>Loading...</div>;
}
```
#### Issue 4: "Payment stuck in 'pending'"
**Cause:** Webhook not received or failed
**Solution:**
```bash
# 1. Check webhook logs
stripe webhook logs --limit 10
# 2. Manually trigger webhook (for testing)
stripe trigger payment_intent.succeeded
# 3. Verify webhook endpoint is reachable
curl -X POST https://api.example.com/api/v1/payments/webhook
```
---
## Examples & Templates
### Example 1: Wallet Deposit Flow (Complete)
```typescript
// Backend: payments.controller.ts
export async function createDeposit(req, res) {
const { amount, currency } = req.body;
const paymentIntent = await stripe.paymentIntents.create({
amount: amount * 100,
currency,
metadata: { userId: req.user.id, type: 'deposit' },
});
await db.query(
'INSERT INTO transactions (user_id, amount, payment_intent_id, status) VALUES ($1, $2, $3, $4)',
[req.user.id, amount, paymentIntent.id, 'pending']
);
res.json({ clientSecret: paymentIntent.client_secret });
}
// Frontend: DepositForm.tsx
function DepositForm() {
const stripe = useStripe();
const elements = useElements();
const handleSubmit = async (e) => {
e.preventDefault();
const { clientSecret } = await fetch('/api/v1/payments/wallet/deposit', {
method: 'POST',
body: JSON.stringify({ amount: 100, currency: 'USD' }),
}).then(r => r.json());
const { error } = await stripe.confirmCardPayment(clientSecret, {
payment_method: { card: elements.getElement(CardElement) },
});
if (!error) {
alert('Deposit successful!');
}
};
return (
<form onSubmit={handleSubmit}>
<CardElement />
<button type="submit">Deposit</button>
</form>
);
}
```
### Example 2: Subscription Checkout (Stripe Hosted)
```typescript
// Backend: createCheckoutSession
export async function createCheckoutSession(req, res) {
const { planId, billingCycle } = req.body;
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
customer_email: req.user.email,
line_items: [{
price: getPriceId(planId, billingCycle),
quantity: 1,
}],
success_url: `${req.body.successUrl}?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: req.body.cancelUrl,
metadata: { userId: req.user.id, planId },
});
res.json({ sessionId: session.id, url: session.url });
}
// Frontend: SubscribeButton.tsx
async function handleSubscribe() {
const { url } = await fetch('/api/v1/payments/checkout', {
method: 'POST',
body: JSON.stringify({
planId: 'basic',
billingCycle: 'monthly',
successUrl: window.location.origin + '/success',
cancelUrl: window.location.origin + '/cancel',
}),
}).then(r => r.json());
window.location.href = url; // Redirect to Stripe
}
```
---
## Quick Reference
### DO's ✅
- ✅ Use Payment Intents (server-side processing)
- ✅ Use Stripe Elements (client-side tokenization)
- ✅ Verify webhook signatures
- ✅ Store only tokens/IDs (pm_xxx, pi_xxx)
- ✅ Use HTTPS everywhere
- ✅ Log payment events (without sensitive data)
- ✅ Write E2E tests for PCI-DSS compliance
- ✅ Validate input on all endpoints
- ✅ Use parameterized SQL queries
### DON'Ts ❌
- ❌ Accept card data in backend
- ❌ Store PAN, CVV, or expiry in database
- ❌ Create native card inputs
- ❌ Store card data in React state
- ❌ Skip webhook signature verification
- ❌ Use HTTP (only HTTPS)
- ❌ Log sensitive data
- ❌ Concatenate SQL queries
---
## Support
**Questions or Issues?**
- Slack: #payments-support
- Email: payments-team@trading-platform.com
- Documentation: [ET-PAY-006: PCI-DSS Architecture](./especificaciones/ET-PAY-006-pci-dss-architecture.md)
**Security Concerns?**
- Email: security@trading-platform.com
- **DO NOT** discuss card data in public channels
---
**Version:** 1.0.0
**Last Updated:** 2026-01-26
**Next Review:** 2026-07-26
---
*These guidelines are mandatory for all developers working on payment features. Non-compliance may result in PCI-DSS violations and severe penalties.*