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>
25 KiB
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
- Overview
- PCI-DSS Compliance Rules
- Backend Development
- Frontend Development
- Testing Guidelines
- Common Pitfalls
- Code Review Checklist
- Deployment Checklist
- Troubleshooting
- 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:
// ✅ 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:
// ✅ 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:
// ❌ 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
// payments.routes.ts
router.post(
'/wallet/deposit',
requireAuth, // ← Always require auth
createDeposit // ← Controller function
);
Step 2: Create Controller
// 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
// 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
// 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:
// 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:
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:
-- ❌ 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
// 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
// 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):
// 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
// 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:
// 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):
// 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:
// 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:
export async function createPayment(req, res) {
const { cardNumber, cvv } = req.body; // ← VIOLATION!
// ...
}
✅ CORRECT:
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:
CREATE TABLE payment_methods (
card_number VARCHAR(16) -- ← SEVERE VIOLATION!
);
✅ CORRECT:
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:
<input type="text" name="cardNumber" /> {/* VIOLATION! */}
✅ CORRECT:
import { CardElement } from '@stripe/react-stripe-js';
<CardElement /> {/* Stripe iframe */}
Pitfall 4: Not Verifying Webhook Signatures
❌ WRONG:
export async function handleWebhook(req, res) {
const event = req.body; // ← NO VERIFICATION! Insecure!
// Anyone can spoof this endpoint
}
✅ CORRECT:
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:
console.log('Payment:', {
cardNumber: req.body.cardNumber, // ← VIOLATION!
cvv: req.body.cvv,
});
✅ CORRECT:
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_KEYset (sk_live_...)STRIPE_PUBLISHABLE_KEYset (pk_live_...)STRIPE_WEBHOOK_SECRETset (whsec_...)DATABASE_URLconfiguredJWT_SECRETset (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:
// 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:
// 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:
// 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:
# 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)
// 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)
// 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
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.