trading-platform/docs/02-definicion-modulos/OQI-005-payments-stripe/especificaciones/ET-PAY-005-frontend.md
Adrian Flores Cortes 930c3bec75 [OQI-005] docs: Complete CAPVED documentation and module updates
- Add 01-CONTEXTO.md, 02-ANALISIS.md, 03-PLANEACION.md, 04-VALIDACION.md
- Update _INDEX.yml with complete CAPVED file list
- Update ET-PAY-005-frontend.md with new components section
- Update TRACEABILITY.yml with frontend implementation status

Full SIMCO compliance achieved for TASK-2026-01-25-OQI-005-PAYMENTS-ADVANCED

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 10:02:20 -06:00

15 KiB
Raw Blame History

id title type status priority epic project version created_date updated_date
ET-PAY-005 Componentes React Frontend Technical Specification Done Alta OQI-005 trading-platform 1.1.0 2025-12-05 2026-01-25

ET-PAY-005: Componentes React Frontend

Epic: OQI-005 Pagos y Stripe Versión: 1.0 Fecha: 2025-12-05


1. Arquitectura Frontend

Pages                    Components              Store
┌──────────────┐        ┌──────────────┐        ┌──────────────┐
│ PricingPage  │───────►│ PricingCard  │        │              │
└──────────────┘        └──────────────┘        │  paymentStore│
                                                 │              │
┌──────────────┐        ┌──────────────┐        └──────────────┘
│CheckoutPage  │───────►│ PaymentForm  │               ▲
└──────────────┘        └──────────────┘               │
                                                        │
┌──────────────┐        ┌──────────────┐               │
│ BillingPage  │───────►│SubscriptionCard──────────────┘
└──────────────┘        │PaymentMethodList
                        └──────────────┘

2. Store con Zustand

// src/stores/paymentStore.ts

import { create } from 'zustand';
import { paymentApi } from '../api/payment.api';

interface PaymentState {
  payments: Payment[];
  subscriptions: Subscription[];
  paymentMethods: PaymentMethod[];
  loading: boolean;

  fetchPayments: () => Promise<void>;
  fetchSubscriptions: () => Promise<void>;
  fetchPaymentMethods: () => Promise<void>;
  createPaymentIntent: (amount: number, pmId: string) => Promise<any>;
  createSubscription: (priceId: string, pmId: string) => Promise<any>;
  cancelSubscription: (id: string, immediate: boolean) => Promise<void>;
}

export const usePaymentStore = create<PaymentState>((set) => ({
  payments: [],
  subscriptions: [],
  paymentMethods: [],
  loading: false,

  fetchPayments: async () => {
    set({ loading: true });
    const response = await paymentApi.getPayments();
    set({ payments: response.data.payments, loading: false });
  },

  fetchSubscriptions: async () => {
    const response = await subscriptionApi.list();
    set({ subscriptions: response.data.subscriptions });
  },

  fetchPaymentMethods: async () => {
    const response = await paymentApi.getPaymentMethods();
    set({ paymentMethods: response.data.payment_methods });
  },

  createPaymentIntent: async (amount, pmId) => {
    const response = await paymentApi.createPaymentIntent({
      amount,
      payment_method_id: pmId,
    });
    return response.data;
  },

  createSubscription: async (priceId, pmId) => {
    const response = await subscriptionApi.create({
      price_id: priceId,
      payment_method_id: pmId,
    });
    return response.data;
  },

  cancelSubscription: async (id, immediate) => {
    await subscriptionApi.cancel(id, immediate);
    // Refresh subscriptions
    await usePaymentStore.getState().fetchSubscriptions();
  },
}));

3. Pricing Page

// src/pages/payment/PricingPage.tsx

import React, { useState } from 'react';
import { PricingCard } from '../../components/payment/PricingCard';
import { CheckoutModal } from '../../components/payment/CheckoutModal';

const PLANS = [
  {
    id: 'basic',
    name: 'Basic',
    price: 29,
    interval: 'month',
    stripe_price_id: 'price_basic_monthly',
    features: ['Feature 1', 'Feature 2', 'Feature 3'],
  },
  {
    id: 'pro',
    name: 'Pro',
    price: 79,
    interval: 'month',
    stripe_price_id: 'price_pro_monthly',
    features: ['All Basic', 'Feature 4', 'Feature 5', 'Priority Support'],
    popular: true,
  },
  {
    id: 'enterprise',
    name: 'Enterprise',
    price: 199,
    interval: 'month',
    stripe_price_id: 'price_enterprise_monthly',
    features: ['All Pro', 'Custom ML Agents', 'Dedicated Support'],
  },
];

export const PricingPage: React.FC = () => {
  const [selectedPlan, setSelectedPlan] = useState<any>(null);
  const [showCheckout, setShowCheckout] = useState(false);

  const handleSelectPlan = (plan: any) => {
    setSelectedPlan(plan);
    setShowCheckout(true);
  };

  return (
    <div className="pricing-page">
      <header>
        <h1>Choose Your Plan</h1>
        <p>Start your AI trading journey today</p>
      </header>

      <div className="pricing-grid">
        {PLANS.map((plan) => (
          <PricingCard
            key={plan.id}
            plan={plan}
            onSelect={() => handleSelectPlan(plan)}
          />
        ))}
      </div>

      {showCheckout && selectedPlan && (
        <CheckoutModal
          plan={selectedPlan}
          onClose={() => setShowCheckout(false)}
          onSuccess={() => {
            setShowCheckout(false);
            // Redirect to dashboard
          }}
        />
      )}
    </div>
  );
};

4. Pricing Card Component

// src/components/payment/PricingCard.tsx

import React from 'react';
import './PricingCard.css';

interface PricingCardProps {
  plan: {
    name: string;
    price: number;
    interval: string;
    features: string[];
    popular?: boolean;
  };
  onSelect: () => void;
}

export const PricingCard: React.FC<PricingCardProps> = ({ plan, onSelect }) => {
  return (
    <div className={`pricing-card ${plan.popular ? 'popular' : ''}`}>
      {plan.popular && <div className="badge">Most Popular</div>}

      <h3>{plan.name}</h3>

      <div className="price">
        <span className="amount">${plan.price}</span>
        <span className="interval">/{plan.interval}</span>
      </div>

      <ul className="features">
        {plan.features.map((feature, idx) => (
          <li key={idx}>
            <span className="checkmark"></span> {feature}
          </li>
        ))}
      </ul>

      <button className="select-button" onClick={onSelect}>
        Get Started
      </button>
    </div>
  );
};

5. Checkout Modal

// src/components/payment/CheckoutModal.tsx

import React, { useState } from 'react';
import { Elements, CardElement, useStripe, useElements } from '@stripe/react-stripe-js';
import { loadStripe } from '@stripe/stripe-js';
import { usePaymentStore } from '../../stores/paymentStore';

const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY!);

interface CheckoutModalProps {
  plan: any;
  onClose: () => void;
  onSuccess: () => void;
}

const CheckoutForm: React.FC<CheckoutModalProps> = ({ plan, onClose, onSuccess }) => {
  const stripe = useStripe();
  const elements = useElements();
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const { createSubscription } = usePaymentStore();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    if (!stripe || !elements) return;

    setLoading(true);
    setError(null);

    try {
      const cardElement = elements.getElement(CardElement);
      if (!cardElement) throw new Error('Card element not found');

      const { error: pmError, paymentMethod } = await stripe.createPaymentMethod({
        type: 'card',
        card: cardElement,
      });

      if (pmError) throw new Error(pmError.message);

      const result = await createSubscription(plan.stripe_price_id, paymentMethod!.id);

      if (result.client_secret) {
        const { error: confirmError } = await stripe.confirmCardPayment(result.client_secret);
        if (confirmError) throw new Error(confirmError.message);
      }

      onSuccess();
    } catch (err: any) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="checkout-modal">
      <div className="modal-content">
        <button className="close-button" onClick={onClose}>×</button>

        <h2>Subscribe to {plan.name}</h2>
        <p className="price">${plan.price}/{plan.interval}</p>

        <form onSubmit={handleSubmit}>
          <div className="form-group">
            <label>Card Details</label>
            <CardElement />
          </div>

          {error && <div className="error">{error}</div>}

          <button type="submit" disabled={!stripe || loading}>
            {loading ? 'Processing...' : 'Subscribe Now'}
          </button>
        </form>
      </div>
    </div>
  );
};

export const CheckoutModal: React.FC<CheckoutModalProps> = (props) => {
  return (
    <Elements stripe={stripePromise}>
      <CheckoutForm {...props} />
    </Elements>
  );
};

6. Billing Page

// src/pages/payment/BillingPage.tsx

import React, { useEffect } from 'react';
import { usePaymentStore } from '../../stores/paymentStore';
import { SubscriptionCard } from '../../components/payment/SubscriptionCard';
import { PaymentMethodList } from '../../components/payment/PaymentMethodList';
import { InvoiceList } from '../../components/payment/InvoiceList';

export const BillingPage: React.FC = () => {
  const {
    subscriptions,
    paymentMethods,
    fetchSubscriptions,
    fetchPaymentMethods,
  } = usePaymentStore();

  useEffect(() => {
    fetchSubscriptions();
    fetchPaymentMethods();
  }, []);

  return (
    <div className="billing-page">
      <h1>Billing & Subscriptions</h1>

      <section className="subscriptions-section">
        <h2>Active Subscriptions</h2>
        {subscriptions.map((sub) => (
          <SubscriptionCard key={sub.id} subscription={sub} />
        ))}
      </section>

      <section className="payment-methods-section">
        <h2>Payment Methods</h2>
        <PaymentMethodList methods={paymentMethods} />
      </section>

      <section className="invoices-section">
        <h2>Invoices</h2>
        <InvoiceList />
      </section>
    </div>
  );
};

7. Subscription Card

// src/components/payment/SubscriptionCard.tsx

import React from 'react';
import { usePaymentStore } from '../../stores/paymentStore';

interface SubscriptionCardProps {
  subscription: Subscription;
}

export const SubscriptionCard: React.FC<SubscriptionCardProps> = ({ subscription }) => {
  const { cancelSubscription } = usePaymentStore();

  const handleCancel = async () => {
    if (confirm('Are you sure you want to cancel?')) {
      await cancelSubscription(subscription.id, false);
    }
  };

  return (
    <div className="subscription-card">
      <div className="plan-info">
        <h3>{subscription.plan_name}</h3>
        <span className="status">{subscription.status}</span>
      </div>

      <div className="billing-info">
        <p>
          <strong>${subscription.amount}</strong> / {subscription.plan_interval}
        </p>
        <p className="next-billing">
          Next billing: {new Date(subscription.current_period_end).toLocaleDateString()}
        </p>
      </div>

      <div className="actions">
        {subscription.cancel_at_period_end ? (
          <span className="canceling">Cancels at period end</span>
        ) : (
          <button onClick={handleCancel}>Cancel Subscription</button>
        )}
      </div>
    </div>
  );
};

8. Componentes Avanzados (2026-01-25)

Los siguientes componentes fueron agregados para mejorar la integración Stripe y gestión de reembolsos:

8.1 StripeElementsWrapper

Ubicación: src/components/payments/StripeElementsWrapper.tsx LOC: 220

Foundation para PCI-DSS compliance. Provee:

  • StripeElementsWrapper - Componente wrapper
  • withStripeElements - HOC para envolver componentes
  • useStripeAvailable - Hook para verificar disponibilidad
import { StripeElementsWrapper, useStripeAvailable } from '@/components/payments';

// Uso como wrapper
<StripeElementsWrapper config={{ publicKey: 'pk_test_...' }}>
  <PaymentForm />
</StripeElementsWrapper>

// Uso del hook
const isAvailable = useStripeAvailable();

8.2 InvoicePreview

Ubicación: src/components/payments/InvoicePreview.tsx LOC: 350

Vista previa de factura pre-checkout con:

  • Desglose de items
  • Descuentos (% o fijo)
  • Impuestos
  • Total calculado
  • Botones de acción
import { InvoicePreview, InvoicePreviewData } from '@/components/payments';

const data: InvoicePreviewData = {
  items: [{ id: '1', description: 'Pro Plan', quantity: 1, unitPrice: 79, total: 79 }],
  subtotal: 79,
  taxes: [{ name: 'IVA', rate: 16, amount: 12.64 }],
  total: 91.64
};

<InvoicePreview data={data} onConfirm={handleConfirm} />

8.3 RefundRequestModal

Ubicación: src/components/payments/RefundRequestModal.tsx LOC: 480

Modal para solicitar reembolsos:

  • Verificación de elegibilidad
  • Razones predefinidas
  • Reembolso parcial o total
  • Validación de montos
import { RefundRequestModal, RefundEligibility } from '@/components/payments';

const eligibility: RefundEligibility = {
  eligible: true,
  maxAmount: 79,
  daysRemaining: 25
};

<RefundRequestModal
  isOpen={true}
  transactionId="tx_123"
  transactionAmount={79}
  eligibility={eligibility}
  onSubmit={handleRefundRequest}
/>

8.4 RefundList

Ubicación: src/components/payments/RefundList.tsx LOC: 450

Lista paginada de reembolsos:

  • Filtro por estado
  • Paginación
  • Badges de estado
  • Detalles expandibles
import { RefundList, Refund } from '@/components/payments';

<RefundList
  refunds={refunds}
  totalCount={100}
  currentPage={1}
  pageSize={10}
  onPageChange={handlePageChange}
  onStatusFilter={handleStatusFilter}
/>

9. Resumen de Componentes

Componente Ubicación LOC Estado
PricingCard components/payments/ ~50 Done
CheckoutModal components/payments/ ~100 Done
SubscriptionCard components/payments/ ~50 Done
PaymentMethodList components/payments/ ~80 Done
InvoiceList components/payments/ ~100 Done
StripeElementsWrapper components/payments/ 220 Done (2026-01-25)
InvoicePreview components/payments/ 350 Done (2026-01-25)
RefundRequestModal components/payments/ 480 Done (2026-01-25)
RefundList components/payments/ 450 Done (2026-01-25)

10. Referencias