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>
12 KiB
12 KiB
| 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.0.0 | 2025-12-05 | 2026-01-04 |
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. Referencias
- React Stripe Elements
- Zustand State Management
- Stripe Checkout Best Practices