# 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.*
