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

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

  1. Overview
  2. PCI-DSS Compliance Rules
  3. Backend Development
  4. Frontend Development
  5. Testing Guidelines
  6. Common Pitfalls
  7. Code Review Checklist
  8. Deployment Checklist
  9. Troubleshooting
  10. 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_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:

// 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?

Security Concerns?


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.