From 3d8bf17b72ed9e324602742fe180781a01f40a03 Mon Sep 17 00:00:00 2001 From: Adrian Flores Cortes Date: Mon, 26 Jan 2026 22:03:47 -0600 Subject: [PATCH] docs(payments): Add Developer Guidelines (ST4.2.5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../DEVELOPER-GUIDELINES.md | 997 ++++++++++++++++++ 1 file changed, 997 insertions(+) create mode 100644 docs/02-definicion-modulos/OQI-005-payments-stripe/DEVELOPER-GUIDELINES.md diff --git a/docs/02-definicion-modulos/OQI-005-payments-stripe/DEVELOPER-GUIDELINES.md b/docs/02-definicion-modulos/OQI-005-payments-stripe/DEVELOPER-GUIDELINES.md new file mode 100644 index 0000000..bb12325 --- /dev/null +++ b/docs/02-definicion-modulos/OQI-005-payments-stripe/DEVELOPER-GUIDELINES.md @@ -0,0 +1,997 @@ +# 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'; + + + +// ✅ 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 + + +// ❌ 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 ( + + + + ); +} +``` + +**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(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 ( +
+ setAmount(Number(e.target.value))} + min="1" + /> + + {/* ✅ Stripe CardElement = iframe from stripe.com */} + + + {error &&
{error}
} + + + + ); +} +``` + +### 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( + + + + ); + + // ✅ 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( + + + + ); + + 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 + {/* VIOLATION! */} +``` + +**✅ CORRECT:** +```tsx +import { CardElement } from '@stripe/react-stripe-js'; + + {/* 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 + + + + +// 2. Check Stripe loaded +const stripe = useStripe(); +if (!stripe) { + return
Loading...
; +} +``` + +#### 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 ( +
+ + + + ); +} +``` + +### 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.*