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

566 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
id: "ET-PAY-005"
title: "Componentes React Frontend"
type: "Technical Specification"
status: "Done"
priority: "Alta"
epic: "OQI-005"
project: "trading-platform"
version: "1.1.0"
created_date: "2025-12-05"
updated_date: "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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
- React Stripe Elements
- Zustand State Management
- Stripe Checkout Best Practices
- [TASK-2026-01-25-OQI-005-PAYMENTS-ADVANCED](../../../orchestration/tareas/TASK-2026-01-25-OQI-005-PAYMENTS-ADVANCED/)