/** * Plans & Billing Page - Consolidated * Tabs: Current Plan, Upgrade/Downgrade, Credits Overview, Purchase Credits, Billing History, Payment Methods */ import { useState, useEffect, useRef } from 'react'; import { CreditCard, Package, TrendingUp, FileText, Wallet, ArrowUpCircle, Loader2, AlertCircle, CheckCircle, Download } from 'lucide-react'; import { Card } from '../../components/ui/card'; import Badge from '../../components/ui/badge/Badge'; import Button from '../../components/ui/button/Button'; import { useToast } from '../../components/ui/toast/ToastContainer'; import { getCreditBalance, getCreditPackages, getInvoices, getAvailablePaymentMethods, purchaseCreditPackage, downloadInvoicePDF, getPayments, submitManualPayment, createPaymentMethod, deletePaymentMethod, setDefaultPaymentMethod, type CreditBalance, type CreditPackage, type Invoice, type PaymentMethod, type Payment, getPlans, getSubscriptions, createSubscription, cancelSubscription, type Plan, type Subscription, } from '../../services/billing.api'; import { useAuthStore } from '../../store/authStore'; type TabType = 'plan' | 'credits' | 'billing-history'; export default function PlansAndBillingPage() { const [activeTab, setActiveTab] = useState('plan'); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [planLoadingId, setPlanLoadingId] = useState(null); const [purchaseLoadingId, setPurchaseLoadingId] = useState(null); // Data states const [creditBalance, setCreditBalance] = useState(null); const [packages, setPackages] = useState([]); const [invoices, setInvoices] = useState([]); const [payments, setPayments] = useState([]); const [paymentMethods, setPaymentMethods] = useState([]); const [plans, setPlans] = useState([]); const [subscriptions, setSubscriptions] = useState([]); const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(undefined); const [manualPayment, setManualPayment] = useState({ invoice_id: '', amount: '', payment_method: '', reference: '', notes: '', }); const [newPaymentMethod, setNewPaymentMethod] = useState({ type: 'bank_transfer', display_name: '', instructions: '', }); const { user } = useAuthStore.getState(); const hasLoaded = useRef(false); const isAwsAdmin = user?.account?.slug === 'aws-admin'; const handleBillingError = (err: any, fallback: string) => { const message = err?.message || fallback; setError(message); toast?.error?.(message); }; const toast = useToast(); useEffect(() => { if (hasLoaded.current) return; hasLoaded.current = true; loadData(); }, []); const loadData = async (allowRetry = true) => { try { setLoading(true); // Fetch in controlled sequence to avoid burst 429s on auth/system scopes const balanceData = await getCreditBalance(); // Small gap between auth endpoints to satisfy tight throttles const wait = (ms: number) => new Promise((res) => setTimeout(res, ms)); const packagesPromise = getCreditPackages(); const invoicesPromise = getInvoices({}); const paymentsPromise = getPayments({}); const methodsPromise = getAvailablePaymentMethods(); const plansData = await getPlans(); await wait(400); // Subscriptions: retry once on 429 after short backoff; do not hard-fail page let subsData: { results: Subscription[] } = { results: [] }; try { subsData = await getSubscriptions(); } catch (subErr: any) { if (subErr?.status === 429 && allowRetry) { await wait(2500); try { subsData = await getSubscriptions(); } catch { subsData = { results: [] }; } } else { subsData = { results: [] }; } } const [packagesData, invoicesData, paymentsData, methodsData] = await Promise.all([ packagesPromise, invoicesPromise, paymentsPromise, methodsPromise, ]); setCreditBalance(balanceData); setPackages(packagesData.results || []); setInvoices(invoicesData.results || []); setPayments(paymentsData.results || []); // Prefer manual payment method id 14 as default (tenant-facing) const methods = (methodsData.results || []).filter((m) => m.is_enabled !== false); setPaymentMethods(methods); if (methods.length > 0) { // Preferred ordering: bank_transfer (default), then manual const bank = methods.find((m) => m.type === 'bank_transfer'); const manual = methods.find((m) => m.type === 'manual'); const selected = bank || manual || methods.find((m) => m.is_default) || methods[0]; setSelectedPaymentMethod((prev) => prev || selected.type || selected.id); } // Surface all active plans (avoid hiding plans and showing empty state) const activePlans = (plansData.results || []).filter((p) => p.is_active !== false); // Exclude Enterprise plan for non aws-admin accounts const filteredPlans = activePlans.filter((p) => { const name = (p.name || '').toLowerCase(); const slug = (p.slug || '').toLowerCase(); const isEnterprise = name.includes('enterprise') || slug === 'enterprise'; return isAwsAdmin ? true : !isEnterprise; }); // Ensure the user's assigned plan is included even if subscriptions list is empty const accountPlan = user?.account?.plan; const isAccountEnterprise = (() => { if (!accountPlan) return false; const name = (accountPlan.name || '').toLowerCase(); const slug = (accountPlan.slug || '').toLowerCase(); return name.includes('enterprise') || slug === 'enterprise'; })(); const shouldIncludeAccountPlan = accountPlan && (!isAccountEnterprise || isAwsAdmin); if (shouldIncludeAccountPlan && !filteredPlans.find((p) => p.id === accountPlan.id)) { filteredPlans.push(accountPlan as any); } setPlans(filteredPlans); const subs = subsData.results || []; if (subs.length === 0 && shouldIncludeAccountPlan && accountPlan) { subs.push({ id: accountPlan.id || 0, plan: accountPlan, status: 'active', } as any); } setSubscriptions(subs); } catch (err: any) { // Handle throttling gracefully: don't block the page on subscriptions throttle if (err?.status === 429 && allowRetry) { setError('Request was throttled. Retrying...'); setTimeout(() => loadData(false), 2500); } else if (err?.status === 429) { setError(''); // suppress lingering banner } else { setError(err.message || 'Failed to load billing data'); console.error('Billing load error:', err); } } finally { setLoading(false); } }; const handleSelectPlan = async (planId: number) => { try { if (!selectedPaymentMethod && paymentMethods.length > 0) { setError('Select a payment method to continue'); return; } setPlanLoadingId(planId); await createSubscription({ plan_id: planId, payment_method: selectedPaymentMethod }); toast?.success?.('Subscription updated'); await loadData(); } catch (err: any) { handleBillingError(err, 'Failed to update subscription'); } finally { setPlanLoadingId(null); } }; const handleCancelSubscription = async () => { if (!currentSubscription?.id) { setError('No active subscription to cancel'); return; } try { setPlanLoadingId(currentSubscription.id); await cancelSubscription(currentSubscription.id); toast?.success?.('Subscription cancellation requested'); await loadData(); } catch (err: any) { handleBillingError(err, 'Failed to cancel subscription'); } finally { setPlanLoadingId(null); } }; const handlePurchase = async (packageId: number) => { try { if (!selectedPaymentMethod && paymentMethods.length > 0) { setError('Select a payment method to continue'); return; } setPurchaseLoadingId(packageId); await purchaseCreditPackage({ package_id: packageId, payment_method: (selectedPaymentMethod as any) || 'stripe', }); await loadData(); } catch (err: any) { handleBillingError(err, 'Failed to purchase credits'); } finally { setPurchaseLoadingId(null); setLoading(false); } }; const handleDownloadInvoice = async (invoiceId: number) => { try { const blob = await downloadInvoicePDF(invoiceId); const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `invoice-${invoiceId}.pdf`; document.body.appendChild(link); link.click(); link.remove(); window.URL.revokeObjectURL(url); } catch (err: any) { handleBillingError(err, 'Failed to download invoice'); } }; const handleSubmitManualPayment = async () => { try { const payload = { invoice_id: manualPayment.invoice_id ? Number(manualPayment.invoice_id) : undefined, amount: manualPayment.amount, payment_method: manualPayment.payment_method || (selectedPaymentMethod as any) || 'manual', reference: manualPayment.reference, notes: manualPayment.notes, }; await submitManualPayment(payload as any); toast?.success?.('Manual payment submitted'); setManualPayment({ invoice_id: '', amount: '', payment_method: '', reference: '', notes: '' }); await loadData(); } catch (err: any) { handleBillingError(err, 'Failed to submit payment'); } }; const handleAddPaymentMethod = async () => { if (!newPaymentMethod.display_name.trim()) { setError('Payment method name is required'); return; } try { await createPaymentMethod(newPaymentMethod as any); toast?.success?.('Payment method added'); setNewPaymentMethod({ type: 'bank_transfer', display_name: '', instructions: '' }); await loadData(); } catch (err: any) { handleBillingError(err, 'Failed to add payment method'); } }; const handleRemovePaymentMethod = async (id: string) => { try { await deletePaymentMethod(id); toast?.success?.('Payment method removed'); await loadData(); } catch (err: any) { handleBillingError(err, 'Failed to remove payment method'); } }; const handleSetDefaultPaymentMethod = async (id: string) => { try { await setDefaultPaymentMethod(id); toast?.success?.('Default payment method updated'); await loadData(); } catch (err: any) { handleBillingError(err, 'Failed to set default'); } }; if (loading) { return (
); } const currentSubscription = subscriptions.find((sub) => sub.status === 'active') || subscriptions[0]; const currentPlanId = typeof currentSubscription?.plan === 'object' ? currentSubscription.plan.id : currentSubscription?.plan; // Fallback to account plan if subscription is missing const accountPlanId = user?.account?.plan?.id; const effectivePlanId = currentPlanId || accountPlanId; const currentPlan = plans.find((p) => p.id === effectivePlanId) || user?.account?.plan; const hasActivePlan = Boolean(effectivePlanId); const hasPaymentMethods = paymentMethods.length > 0; const subscriptionStatus = currentSubscription?.status || (hasActivePlan ? 'active' : 'none'); const hasPendingManualPayment = payments.some((p) => p.status === 'pending_approval'); const tabs = [ { id: 'plan' as TabType, label: 'Current Plan', icon: }, { id: 'credits' as TabType, label: 'Credits Overview', icon: }, { id: 'billing-history' as TabType, label: 'Billing History', icon: }, ]; return (

Plans & Billing

Manage your subscription, credits, and billing information

{/* Activation / pending payment notice */} {!hasActivePlan && (
No active plan. Choose a plan below to activate your account.
)} {hasPendingManualPayment && (
We received your manual payment. It’s pending admin approval; activation will complete once approved.
)} {error && (

{error}

)} {/* Tabs */}
{/* Tab Content */}
{/* Current Plan Tab */} {activeTab === 'plan' && (

Your Current Plan

{!hasActivePlan && (
No active plan found. Please choose a plan to activate your account.
)}
{currentPlan?.name || 'No Plan Selected'}
{currentPlan?.description || 'Select a plan to unlock full access.'}
{hasActivePlan ? subscriptionStatus : 'plan required'}
Monthly Credits
{creditBalance?.plan_credits_per_month?.toLocaleString?.() || 0}
Current Balance
{creditBalance?.credits?.toLocaleString?.() || 0}
Period Ends
{currentSubscription?.current_period_end ? new Date(currentSubscription.current_period_end).toLocaleDateString() : '—'}
{hasActivePlan && ( )}

Plan Features

    {(currentPlan?.features && currentPlan.features.length > 0 ? currentPlan.features : ['Credits included each month', 'Module access per plan limits', 'Email support']) .map((feature) => (
  • {feature}
  • ))}
)} {/* Upgrade/Downgrade Tab */} {activeTab === 'upgrade' && (

Available Plans

Choose the plan that best fits your needs

{hasPaymentMethods ? (
Select payment method
{paymentMethods.map((method) => ( ))}
) : (
No payment methods available. Please contact support or add one from the Payment Methods tab.
)}
{plans.map((plan) => { const isCurrent = plan.id === currentPlanId; const price = plan.price ? `$${plan.price}/${plan.interval || 'month'}` : 'Custom'; return (

{plan.name}

{price}
{plan.description || 'Standard plan'}
{(plan.features && plan.features.length > 0 ? plan.features : ['Monthly credits included', 'Module access per plan', 'Email support']).map((feature) => (
{feature}
))}
); })} {plans.length === 0 && (
No plans available. Please contact support.
)}

Plan Change Policy

  • • Upgrades take effect immediately and you'll be charged a prorated amount
  • • Downgrades take effect at the end of your current billing period
  • • Unused credits from your current plan will carry over
  • • You can cancel your subscription at any time
)} {/* Credits Overview Tab */} {activeTab === 'credits' && (
Current Balance
{creditBalance?.credits.toLocaleString() || 0}
credits available
Used This Month
{creditBalance?.credits_used_this_month.toLocaleString() || 0}
credits consumed
Monthly Included
{creditBalance?.plan_credits_per_month.toLocaleString() || 0}
from your plan

Credit Usage Summary

Remaining Credits {creditBalance?.credits_remaining.toLocaleString() || 0}
)} {/* Purchase Credits Tab */} {activeTab === 'purchase' && (
{hasPaymentMethods ? (
Select payment method
{paymentMethods.map((method) => ( ))}
) : (
No payment methods available. Please contact support or add one from the Payment Methods tab.
)}

Credit Packages

{packages.map((pkg) => (
{pkg.name}
{pkg.credits.toLocaleString()} credits
${pkg.price}
{pkg.description && (
{pkg.description}
)}
))} {packages.length === 0 && (
No credit packages available at this time
)}
)} {/* Billing History Tab */} {activeTab === 'invoices' && (
{invoices.length === 0 ? ( ) : ( invoices.map((invoice) => ( )) )}
Invoice Date Amount Status Actions
No invoices yet
{invoice.invoice_number} {new Date(invoice.created_at).toLocaleDateString()} ${invoice.total_amount} {invoice.status}
)} {/* Payments Tab */} {activeTab === 'payments' && (

Payments

Recent payments and manual submissions

{payments.length === 0 ? ( ) : ( payments.map((payment) => ( )) )}
Invoice Amount Method Status Date
No payments yet
{payment.invoice_number || payment.invoice_id || '-'} ${payment.amount} {payment.payment_method} {payment.status} {new Date(payment.created_at).toLocaleDateString()}

Submit Manual Payment

setManualPayment((p) => ({ ...p, invoice_id: e.target.value }))} className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white" placeholder="Invoice ID" />
setManualPayment((p) => ({ ...p, amount: e.target.value }))} className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white" placeholder="e.g., 99.00" />
setManualPayment((p) => ({ ...p, payment_method: e.target.value }))} className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white" placeholder="bank_transfer / local_wallet / manual" />
setManualPayment((p) => ({ ...p, reference: e.target.value }))} className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white" placeholder="Reference or transaction id" />