trading-platform/docs/02-definicion-modulos/OQI-004-investment-accounts/especificaciones/ET-INV-005-frontend.md
rckrdmrd a7cca885f0 feat: Major platform documentation and architecture updates
Changes include:
- Updated architecture documentation
- Enhanced module definitions (OQI-001 to OQI-008)
- ML integration documentation updates
- Trading strategies documentation
- Orchestration and inventory updates
- Docker configuration updates

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 05:33:35 -06:00

26 KiB

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

ET-INV-005: Componentes React Frontend

Epic: OQI-004 Cuentas de Inversión Versión: 1.0 Fecha: 2025-12-05 Responsable: Requirements-Analyst


1. Descripción

Define la implementación frontend para el módulo de cuentas de inversión usando React 18, TypeScript y Zustand:

  • Páginas principales (Products, Portfolio, AccountDetail)
  • Componentes reutilizables
  • Estado global con Zustand
  • Integración con API backend
  • Formularios con validación

2. Arquitectura Frontend

┌─────────────────────────────────────────────────────────────────┐
│                    Frontend Architecture                         │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  Pages                  Components              Stores           │
│  ┌──────────────┐      ┌──────────────┐      ┌──────────────┐  │
│  │              │      │              │      │              │  │
│  │ ProductsPage │─────►│ ProductCard  │      │ investment   │  │
│  │              │      │              │      │   Store      │  │
│  └──────────────┘      └──────────────┘      └──────────────┘  │
│                                 ▲                    ▲          │
│  ┌──────────────┐               │                    │          │
│  │              │               │                    │          │
│  │ PortfolioPage├───────────────┼────────────────────┤          │
│  │              │               │                    │          │
│  └──────────────┘      ┌──────────────┐             │          │
│                        │              │             │          │
│  ┌──────────────┐      │DepositForm   │             │          │
│  │              │      │              │             │          │
│  │AccountDetail │─────►│PerformanceChart◄──────────┘          │
│  │     Page     │      │              │                        │
│  └──────────────┘      │WithdrawalForm│                        │
│                        │              │                        │
│                        └──────────────┘                        │
│                                                                  │
│                        ┌──────────────┐                         │
│                        │   API Layer  │                         │
│                        │  (Axios)     │                         │
│                        └──────────────┘                         │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

3. Estructura de Archivos

src/
├── pages/
│   └── investment/
│       ├── ProductsPage.tsx
│       ├── PortfolioPage.tsx
│       ├── AccountDetailPage.tsx
│       └── WithdrawalsPage.tsx
├── components/
│   └── investment/
│       ├── ProductCard.tsx
│       ├── ProductList.tsx
│       ├── AccountCard.tsx
│       ├── DepositForm.tsx
│       ├── WithdrawalForm.tsx
│       ├── PerformanceChart.tsx
│       ├── TransactionList.tsx
│       └── PortfolioSummary.tsx
├── stores/
│   └── investmentStore.ts
├── api/
│   └── investment.api.ts
├── types/
│   └── investment.types.ts
└── hooks/
    └── useInvestment.ts

4. Store con Zustand

4.1 Investment Store

// src/stores/investmentStore.ts

import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import { investmentApi } from '../api/investment.api';
import {
  Product,
  Account,
  Transaction,
  WithdrawalRequest,
  DailyPerformance,
} from '../types/investment.types';

interface InvestmentState {
  // State
  products: Product[];
  accounts: Account[];
  selectedAccount: Account | null;
  transactions: Transaction[];
  withdrawalRequests: WithdrawalRequest[];
  dailyPerformance: DailyPerformance[];
  portfolioSummary: any | null;

  // Loading states
  loading: {
    products: boolean;
    accounts: boolean;
    transactions: boolean;
    portfolio: boolean;
  };

  // Error states
  error: string | null;

  // Actions
  fetchProducts: (filters?: any) => Promise<void>;
  fetchAccounts: () => Promise<void>;
  fetchAccountById: (id: string) => Promise<void>;
  fetchPortfolio: () => Promise<void>;
  fetchTransactions: (filters?: any) => Promise<void>;
  fetchWithdrawalRequests: () => Promise<void>;
  fetchPerformance: (accountId: string, filters?: any) => Promise<void>;
  createAccount: (data: any) => Promise<any>;
  deposit: (accountId: string, data: any) => Promise<any>;
  requestWithdrawal: (accountId: string, data: any) => Promise<any>;
  clearError: () => void;
}

export const useInvestmentStore = create<InvestmentState>()(
  devtools(
    (set, get) => ({
      // Initial state
      products: [],
      accounts: [],
      selectedAccount: null,
      transactions: [],
      withdrawalRequests: [],
      dailyPerformance: [],
      portfolioSummary: null,

      loading: {
        products: false,
        accounts: false,
        transactions: false,
        portfolio: false,
      },

      error: null,

      // Actions
      fetchProducts: async (filters) => {
        set((state) => ({
          loading: { ...state.loading, products: true },
          error: null,
        }));

        try {
          const response = await investmentApi.getProducts(filters);
          set({
            products: response.data.products,
            loading: { ...get().loading, products: false },
          });
        } catch (error: any) {
          set({
            error: error.message,
            loading: { ...get().loading, products: false },
          });
        }
      },

      fetchAccounts: async () => {
        set((state) => ({
          loading: { ...state.loading, accounts: true },
          error: null,
        }));

        try {
          const response = await investmentApi.getAccounts();
          set({
            accounts: response.data.accounts,
            loading: { ...get().loading, accounts: false },
          });
        } catch (error: any) {
          set({
            error: error.message,
            loading: { ...get().loading, accounts: false },
          });
        }
      },

      fetchAccountById: async (id) => {
        try {
          const response = await investmentApi.getAccountById(id);
          set({ selectedAccount: response.data.account });
        } catch (error: any) {
          set({ error: error.message });
        }
      },

      fetchPortfolio: async () => {
        set((state) => ({
          loading: { ...state.loading, portfolio: true },
          error: null,
        }));

        try {
          const response = await investmentApi.getPortfolio();
          set({
            portfolioSummary: response.data,
            loading: { ...get().loading, portfolio: false },
          });
        } catch (error: any) {
          set({
            error: error.message,
            loading: { ...get().loading, portfolio: false },
          });
        }
      },

      fetchTransactions: async (filters) => {
        set((state) => ({
          loading: { ...state.loading, transactions: true },
          error: null,
        }));

        try {
          const response = await investmentApi.getTransactions(filters);
          set({
            transactions: response.data.transactions,
            loading: { ...get().loading, transactions: false },
          });
        } catch (error: any) {
          set({
            error: error.message,
            loading: { ...get().loading, transactions: false },
          });
        }
      },

      fetchWithdrawalRequests: async () => {
        try {
          const response = await investmentApi.getWithdrawalRequests();
          set({ withdrawalRequests: response.data.requests });
        } catch (error: any) {
          set({ error: error.message });
        }
      },

      fetchPerformance: async (accountId, filters) => {
        try {
          const response = await investmentApi.getPerformance(accountId, filters);
          set({ dailyPerformance: response.data.performance });
        } catch (error: any) {
          set({ error: error.message });
        }
      },

      createAccount: async (data) => {
        set({ error: null });

        try {
          const response = await investmentApi.createAccount(data);
          return response.data;
        } catch (error: any) {
          set({ error: error.message });
          throw error;
        }
      },

      deposit: async (accountId, data) => {
        set({ error: null });

        try {
          const response = await investmentApi.deposit(accountId, data);
          return response.data;
        } catch (error: any) {
          set({ error: error.message });
          throw error;
        }
      },

      requestWithdrawal: async (accountId, data) => {
        set({ error: null });

        try {
          const response = await investmentApi.requestWithdrawal(accountId, data);
          return response.data;
        } catch (error: any) {
          set({ error: error.message });
          throw error;
        }
      },

      clearError: () => set({ error: null }),
    }),
    { name: 'InvestmentStore' }
  )
);

5. API Layer

5.1 Investment API Client

// src/api/investment.api.ts

import axios from './axios-instance';
import { AxiosResponse } from 'axios';

const BASE_PATH = '/api/v1/investment';

export const investmentApi = {
  // Products
  getProducts: (params?: any): Promise<AxiosResponse> => {
    return axios.get(`${BASE_PATH}/products`, { params });
  },

  getProductById: (id: string): Promise<AxiosResponse> => {
    return axios.get(`${BASE_PATH}/products/${id}`);
  },

  // Accounts
  getAccounts: (params?: any): Promise<AxiosResponse> => {
    return axios.get(`${BASE_PATH}/accounts`, { params });
  },

  getAccountById: (id: string): Promise<AxiosResponse> => {
    return axios.get(`${BASE_PATH}/accounts/${id}`);
  },

  createAccount: (data: {
    product_id: string;
    initial_investment: number;
    payment_method_id: string;
  }): Promise<AxiosResponse> => {
    return axios.post(`${BASE_PATH}/accounts`, data);
  },

  // Deposits
  deposit: (
    accountId: string,
    data: { amount: number; payment_method_id: string }
  ): Promise<AxiosResponse> => {
    return axios.post(`${BASE_PATH}/accounts/${accountId}/deposit`, data);
  },

  // Withdrawals
  requestWithdrawal: (
    accountId: string,
    data: {
      amount: number;
      withdrawal_method: string;
      destination_details: any;
    }
  ): Promise<AxiosResponse> => {
    return axios.post(`${BASE_PATH}/accounts/${accountId}/withdraw`, data);
  },

  getWithdrawalRequests: (params?: any): Promise<AxiosResponse> => {
    return axios.get(`${BASE_PATH}/withdrawal-requests`, { params });
  },

  // Portfolio
  getPortfolio: (): Promise<AxiosResponse> => {
    return axios.get(`${BASE_PATH}/portfolio`);
  },

  getPerformance: (accountId: string, params?: any): Promise<AxiosResponse> => {
    return axios.get(`${BASE_PATH}/accounts/${accountId}/performance`, { params });
  },

  // Transactions
  getTransactions: (params?: any): Promise<AxiosResponse> => {
    return axios.get(`${BASE_PATH}/transactions`, { params });
  },
};

6. Páginas Principales

6.1 Products Page

// src/pages/investment/ProductsPage.tsx

import React, { useEffect, useState } from 'react';
import { useInvestmentStore } from '../../stores/investmentStore';
import { ProductCard } from '../../components/investment/ProductCard';
import { DepositModal } from '../../components/investment/DepositModal';
import { Product } from '../../types/investment.types';
import './ProductsPage.css';

export const ProductsPage: React.FC = () => {
  const { products, loading, fetchProducts } = useInvestmentStore();
  const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
  const [showDepositModal, setShowDepositModal] = useState(false);

  useEffect(() => {
    fetchProducts({ status: 'active' });
  }, [fetchProducts]);

  const handleInvest = (product: Product) => {
    setSelectedProduct(product);
    setShowDepositModal(true);
  };

  if (loading.products) {
    return <div className="loading">Loading products...</div>;
  }

  return (
    <div className="products-page">
      <header className="page-header">
        <h1>Investment Products</h1>
        <p>Choose from our AI-powered trading agents</p>
      </header>

      <div className="products-grid">
        {products.map((product) => (
          <ProductCard
            key={product.id}
            product={product}
            onInvest={handleInvest}
          />
        ))}
      </div>

      {showDepositModal && selectedProduct && (
        <DepositModal
          product={selectedProduct}
          onClose={() => setShowDepositModal(false)}
        />
      )}
    </div>
  );
};

6.2 Portfolio Page

// src/pages/investment/PortfolioPage.tsx

import React, { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useInvestmentStore } from '../../stores/investmentStore';
import { PortfolioSummary } from '../../components/investment/PortfolioSummary';
import { AccountCard } from '../../components/investment/AccountCard';
import './PortfolioPage.css';

export const PortfolioPage: React.FC = () => {
  const navigate = useNavigate();
  const { portfolioSummary, loading, fetchPortfolio } = useInvestmentStore();

  useEffect(() => {
    fetchPortfolio();
  }, [fetchPortfolio]);

  if (loading.portfolio) {
    return <div className="loading">Loading portfolio...</div>;
  }

  if (!portfolioSummary) {
    return (
      <div className="empty-state">
        <h2>No investments yet</h2>
        <p>Start investing in AI-powered trading agents</p>
        <button onClick={() => navigate('/investment/products')}>
          Browse Products
        </button>
      </div>
    );
  }

  return (
    <div className="portfolio-page">
      <header className="page-header">
        <h1>My Portfolio</h1>
      </header>

      <PortfolioSummary summary={portfolioSummary.summary} />

      <section className="accounts-section">
        <h2>My Accounts</h2>
        <div className="accounts-grid">
          {portfolioSummary.accounts.map((account: any) => (
            <AccountCard
              key={account.account_id}
              account={account}
              onClick={() => navigate(`/investment/accounts/${account.account_id}`)}
            />
          ))}
        </div>
      </section>

      <section className="allocation-section">
        <h2>Risk Allocation</h2>
        <div className="risk-chart">
          {/* Implementar gráfico de dona con allocation_by_risk */}
        </div>
      </section>
    </div>
  );
};

6.3 Account Detail Page

// src/pages/investment/AccountDetailPage.tsx

import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { useInvestmentStore } from '../../stores/investmentStore';
import { PerformanceChart } from '../../components/investment/PerformanceChart';
import { TransactionList } from '../../components/investment/TransactionList';
import { DepositForm } from '../../components/investment/DepositForm';
import { WithdrawalForm } from '../../components/investment/WithdrawalForm';
import './AccountDetailPage.css';

export const AccountDetailPage: React.FC = () => {
  const { id } = useParams<{ id: string }>();
  const {
    selectedAccount,
    dailyPerformance,
    fetchAccountById,
    fetchPerformance,
    fetchTransactions,
  } = useInvestmentStore();

  const [activeTab, setActiveTab] = useState<'overview' | 'deposit' | 'withdraw'>('overview');

  useEffect(() => {
    if (id) {
      fetchAccountById(id);
      fetchPerformance(id, { period: 'month' });
      fetchTransactions({ account_id: id, limit: 20 });
    }
  }, [id, fetchAccountById, fetchPerformance, fetchTransactions]);

  if (!selectedAccount) {
    return <div className="loading">Loading account...</div>;
  }

  return (
    <div className="account-detail-page">
      <header className="account-header">
        <div className="account-info">
          <h1>{selectedAccount.product.name}</h1>
          <span className={`status-badge ${selectedAccount.status}`}>
            {selectedAccount.status}
          </span>
        </div>

        <div className="account-balance">
          <div className="balance-label">Current Balance</div>
          <div className="balance-amount">
            ${selectedAccount.current_balance.toLocaleString('en-US', {
              minimumFractionDigits: 2,
            })}
          </div>
          <div className="balance-return">
            {selectedAccount.total_return_percentage >= 0 ? '+' : ''}
            {selectedAccount.total_return_percentage?.toFixed(2)}%
          </div>
        </div>
      </header>

      <nav className="account-tabs">
        <button
          className={activeTab === 'overview' ? 'active' : ''}
          onClick={() => setActiveTab('overview')}
        >
          Overview
        </button>
        <button
          className={activeTab === 'deposit' ? 'active' : ''}
          onClick={() => setActiveTab('deposit')}
        >
          Deposit
        </button>
        <button
          className={activeTab === 'withdraw' ? 'active' : ''}
          onClick={() => setActiveTab('withdraw')}
        >
          Withdraw
        </button>
      </nav>

      <div className="account-content">
        {activeTab === 'overview' && (
          <>
            <section className="performance-section">
              <h2>Performance</h2>
              <PerformanceChart data={dailyPerformance} />
            </section>

            <section className="stats-section">
              <div className="stat-card">
                <div className="stat-label">Total Invested</div>
                <div className="stat-value">
                  ${selectedAccount.total_deposited.toLocaleString()}
                </div>
              </div>
              <div className="stat-card">
                <div className="stat-label">Total Profit</div>
                <div className="stat-value">
                  ${(selectedAccount.current_balance - selectedAccount.total_deposited).toLocaleString()}
                </div>
              </div>
              <div className="stat-card">
                <div className="stat-label">Annualized Return</div>
                <div className="stat-value">
                  {selectedAccount.annualized_return_percentage?.toFixed(2)}%
                </div>
              </div>
            </section>

            <section className="transactions-section">
              <h2>Recent Transactions</h2>
              <TransactionList />
            </section>
          </>
        )}

        {activeTab === 'deposit' && (
          <DepositForm
            accountId={selectedAccount.id}
            productId={selectedAccount.product_id}
            minAmount={50}
            onSuccess={() => {
              fetchAccountById(selectedAccount.id);
              setActiveTab('overview');
            }}
          />
        )}

        {activeTab === 'withdraw' && (
          <WithdrawalForm
            accountId={selectedAccount.id}
            maxAmount={selectedAccount.current_balance}
            onSuccess={() => {
              fetchAccountById(selectedAccount.id);
              setActiveTab('overview');
            }}
          />
        )}
      </div>
    </div>
  );
};

7. Componentes Reutilizables

7.1 Product Card

// src/components/investment/ProductCard.tsx

import React from 'react';
import { Product } from '../../types/investment.types';
import './ProductCard.css';

interface ProductCardProps {
  product: Product;
  onInvest: (product: Product) => void;
}

export const ProductCard: React.FC<ProductCardProps> = ({ product, onInvest }) => {
  const riskColors = {
    low: 'green',
    medium: 'yellow',
    high: 'orange',
    very_high: 'red',
  };

  return (
    <div className="product-card">
      <div className="product-header">
        <h3>{product.name}</h3>
        <span className={`risk-badge ${riskColors[product.risk_level]}`}>
          {product.risk_level.toUpperCase()}
        </span>
      </div>

      <p className="product-description">{product.description}</p>

      <div className="product-stats">
        <div className="stat">
          <span className="stat-label">Agent Type</span>
          <span className="stat-value">{product.agent_type}</span>
        </div>
        <div className="stat">
          <span className="stat-label">Target Return</span>
          <span className="stat-value">{product.target_annual_return}%</span>
        </div>
        <div className="stat">
          <span className="stat-label">Performance Fee</span>
          <span className="stat-value">{product.performance_fee_percentage}%</span>
        </div>
        <div className="stat">
          <span className="stat-label">Min Investment</span>
          <span className="stat-value">${product.min_investment}</span>
        </div>
      </div>

      <div className="product-metrics">
        <div className="metric">
          <span>{product.total_investors} investors</span>
        </div>
        <div className="metric">
          <span>${(product.total_aum / 1000).toFixed(0)}K AUM</span>
        </div>
      </div>

      <button
        className="invest-button"
        onClick={() => onInvest(product)}
        disabled={!product.is_accepting_new_investors}
      >
        {product.is_accepting_new_investors ? 'Invest Now' : 'Not Accepting'}
      </button>
    </div>
  );
};

7.2 Performance Chart

// src/components/investment/PerformanceChart.tsx

import React from 'react';
import {
  LineChart,
  Line,
  XAxis,
  YAxis,
  CartesianGrid,
  Tooltip,
  Legend,
  ResponsiveContainer,
} from 'recharts';
import { DailyPerformance } from '../../types/investment.types';

interface PerformanceChartProps {
  data: DailyPerformance[];
}

export const PerformanceChart: React.FC<PerformanceChartProps> = ({ data }) => {
  const chartData = data.map((item) => ({
    date: new Date(item.date).toLocaleDateString('en-US', {
      month: 'short',
      day: 'numeric',
    }),
    balance: item.closing_balance,
    return: item.cumulative_return_percentage,
  }));

  return (
    <ResponsiveContainer width="100%" height={400}>
      <LineChart data={chartData}>
        <CartesianGrid strokeDasharray="3 3" />
        <XAxis dataKey="date" />
        <YAxis yAxisId="left" />
        <YAxis yAxisId="right" orientation="right" />
        <Tooltip />
        <Legend />
        <Line
          yAxisId="left"
          type="monotone"
          dataKey="balance"
          stroke="#8884d8"
          name="Balance ($)"
        />
        <Line
          yAxisId="right"
          type="monotone"
          dataKey="return"
          stroke="#82ca9d"
          name="Return (%)"
        />
      </LineChart>
    </ResponsiveContainer>
  );
};

8. Hooks Personalizados

// src/hooks/useInvestment.ts

import { useEffect } from 'react';
import { useInvestmentStore } from '../stores/investmentStore';

export const usePortfolio = () => {
  const { portfolioSummary, loading, fetchPortfolio } = useInvestmentStore();

  useEffect(() => {
    fetchPortfolio();
  }, [fetchPortfolio]);

  return { portfolio: portfolioSummary, loading: loading.portfolio };
};

export const useAccount = (accountId: string) => {
  const { selectedAccount, fetchAccountById } = useInvestmentStore();

  useEffect(() => {
    if (accountId) {
      fetchAccountById(accountId);
    }
  }, [accountId, fetchAccountById]);

  return { account: selectedAccount };
};

9. Configuración

9.1 Variables de Entorno

# Frontend .env
REACT_APP_API_URL=http://localhost:3000
REACT_APP_STRIPE_PUBLISHABLE_KEY=pk_test_...

10. Testing

10.1 Component Tests

// tests/components/ProductCard.test.tsx

import { render, screen, fireEvent } from '@testing-library/react';
import { ProductCard } from '../src/components/investment/ProductCard';

describe('ProductCard', () => {
  const mockProduct = {
    id: '1',
    name: 'Swing Trader Pro',
    risk_level: 'medium',
    // ... otros campos
  };

  it('renders product information', () => {
    render(<ProductCard product={mockProduct} onInvest={jest.fn()} />);

    expect(screen.getByText('Swing Trader Pro')).toBeInTheDocument();
  });

  it('calls onInvest when button clicked', () => {
    const onInvest = jest.fn();
    render(<ProductCard product={mockProduct} onInvest={onInvest} />);

    fireEvent.click(screen.getByText('Invest Now'));
    expect(onInvest).toHaveBeenCalledWith(mockProduct);
  });
});

11. Referencias

  • React 18 Documentation
  • Zustand State Management
  • Recharts for Charts
  • Stripe React Elements