diff --git a/docs/working-docs/billing-account-final-plan-2025-12-05.md b/docs/billing/billing-account-final-plan-2025-12-05.md similarity index 100% rename from docs/working-docs/billing-account-final-plan-2025-12-05.md rename to docs/billing/billing-account-final-plan-2025-12-05.md diff --git a/docs/user-flow/user-flow-initial b/docs/user-flow/user-flow-initial new file mode 100644 index 00000000..e69de29b diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f4ae34ee..ea70b2b5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -67,23 +67,23 @@ const AccountSettingsPage = lazy(() => import("./pages/account/AccountSettingsPa const TeamManagementPage = lazy(() => import("./pages/account/TeamManagementPage")); const UsageAnalyticsPage = lazy(() => import("./pages/account/UsageAnalyticsPage")); -// Admin Module - Lazy loaded +// Admin Module - Lazy loaded (mixed folder casing in repo, match actual file paths) const AdminBilling = lazy(() => import("./pages/Admin/AdminBilling")); -const PaymentApprovalPage = lazy(() => import("./pages/Admin/PaymentApprovalPage.tsx")); -const AdminSystemDashboard = lazy(() => import("./pages/Admin/AdminSystemDashboard")); -const AdminAllAccountsPage = lazy(() => import("./pages/Admin/AdminAllAccountsPage")); -const AdminSubscriptionsPage = lazy(() => import("./pages/Admin/AdminSubscriptionsPage")); -const AdminAccountLimitsPage = lazy(() => import("./pages/Admin/AdminAccountLimitsPage")); -const AdminAllInvoicesPage = lazy(() => import("./pages/Admin/AdminAllInvoicesPage")); -const AdminAllPaymentsPage = lazy(() => import("./pages/Admin/AdminAllPaymentsPage")); -const AdminCreditPackagesPage = lazy(() => import("./pages/Admin/AdminCreditPackagesPage")); +const PaymentApprovalPage = lazy(() => import("./pages/admin/PaymentApprovalPage")); +const AdminSystemDashboard = lazy(() => import("./pages/admin/AdminSystemDashboard")); +const AdminAllAccountsPage = lazy(() => import("./pages/admin/AdminAllAccountsPage")); +const AdminSubscriptionsPage = lazy(() => import("./pages/admin/AdminSubscriptionsPage")); +const AdminAccountLimitsPage = lazy(() => import("./pages/admin/AdminAccountLimitsPage")); +const AdminAllInvoicesPage = lazy(() => import("./pages/admin/AdminAllInvoicesPage")); +const AdminAllPaymentsPage = lazy(() => import("./pages/admin/AdminAllPaymentsPage")); +const AdminCreditPackagesPage = lazy(() => import("./pages/admin/AdminCreditPackagesPage")); const AdminCreditCostsPage = lazy(() => import("./pages/Admin/AdminCreditCostsPage")); -const AdminAllUsersPage = lazy(() => import("./pages/Admin/AdminAllUsersPage")); -const AdminRolesPermissionsPage = lazy(() => import("./pages/Admin/AdminRolesPermissionsPage")); -const AdminActivityLogsPage = lazy(() => import("./pages/Admin/AdminActivityLogsPage")); -const AdminSystemSettingsPage = lazy(() => import("./pages/Admin/AdminSystemSettingsPage")); -const AdminSystemHealthPage = lazy(() => import("./pages/Admin/AdminSystemHealthPage")); -const AdminAPIMonitorPage = lazy(() => import("./pages/Admin/AdminAPIMonitorPage")); +const AdminAllUsersPage = lazy(() => import("./pages/admin/AdminAllUsersPage")); +const AdminRolesPermissionsPage = lazy(() => import("./pages/admin/AdminRolesPermissionsPage")); +const AdminActivityLogsPage = lazy(() => import("./pages/admin/AdminActivityLogsPage")); +const AdminSystemSettingsPage = lazy(() => import("./pages/admin/AdminSystemSettingsPage")); +const AdminSystemHealthPage = lazy(() => import("./pages/admin/AdminSystemHealthPage")); +const AdminAPIMonitorPage = lazy(() => import("./pages/admin/AdminAPIMonitorPage")); // Reference Data - Lazy loaded const SeedKeywords = lazy(() => import("./pages/Reference/SeedKeywords")); diff --git a/frontend/src/components/sidebar/ApiStatusIndicator.tsx b/frontend/src/components/sidebar/ApiStatusIndicator.tsx index b7e85cbc..6e19a04b 100644 --- a/frontend/src/components/sidebar/ApiStatusIndicator.tsx +++ b/frontend/src/components/sidebar/ApiStatusIndicator.tsx @@ -112,7 +112,7 @@ const endpointGroups = [ { path: "/v1/system/strategies/", method: "GET" }, { path: "/v1/system/settings/integrations/openai/test/", method: "POST" }, { path: "/v1/system/settings/account/", method: "GET" }, - { path: "/v1/billing/credits/balance/balance/", method: "GET" }, + { path: "/v1/billing/credits/balance/", method: "GET" }, { path: "/v1/billing/credits/usage/", method: "GET" }, { path: "/v1/billing/credits/usage/summary/", method: "GET" }, { path: "/v1/billing/credits/transactions/", method: "GET" }, diff --git a/frontend/src/pages/Admin/AdminBilling.tsx b/frontend/src/pages/Admin/AdminBilling.tsx index 04a8a3e6..007b7b59 100644 --- a/frontend/src/pages/Admin/AdminBilling.tsx +++ b/frontend/src/pages/Admin/AdminBilling.tsx @@ -21,23 +21,39 @@ import { interface UserAccount { id: number; - username: string; email: string; + username?: string; + account_name?: string; credits: number; - subscription_plan: string; + subscription_plan?: string; is_active: boolean; date_joined: string; } interface CreditCostConfig { id: number; - model_name: string; operation_type: string; - cost: number; + display_name: string; + credits_cost: number; + unit?: string; + description?: string; is_active: boolean; created_at: string; } +interface CreditPackageItem { + id: number; + name: string; + slug: string; + credits: number; + price: string; + discount_percentage: number; + is_featured: boolean; + description?: string; + is_active?: boolean; + sort_order?: number; +} + interface SystemStats { total_users: number; active_users: number; @@ -50,8 +66,9 @@ const AdminBilling: React.FC = () => { const [stats, setStats] = useState(null); const [users, setUsers] = useState([]); const [creditConfigs, setCreditConfigs] = useState([]); + const [creditPackages, setCreditPackages] = useState([]); const [loading, setLoading] = useState(true); - const [activeTab, setActiveTab] = useState<'overview' | 'users' | 'pricing'>('overview'); + const [activeTab, setActiveTab] = useState<'overview' | 'users' | 'pricing' | 'packages'>('overview'); const [searchTerm, setSearchTerm] = useState(''); const [selectedUser, setSelectedUser] = useState(null); const [creditAmount, setCreditAmount] = useState(''); @@ -65,17 +82,19 @@ const AdminBilling: React.FC = () => { try { setLoading(true); const [statsData, usersData, configsData] = await Promise.all([ - // Admin billing stats (credits, activity, revenue) - fetchAPI('/v1/billing/admin/stats/'), - // Admin billing users list (with credits) - fetchAPI('/v1/admin/billing/users/?limit=100'), - // Admin billing credit costs - fetchAPI('/v1/admin/billing/credit-costs/'), + // Admin billing stats (modules admin endpoints) + fetchAPI('/v1/admin/billing/stats/'), + // Admin users with credits + fetchAPI('/v1/admin/users/'), + // Admin credit costs (modules billing) + fetchAPI('/v1/admin/credit-costs/'), ]); + const packagesData = await fetchAPI('/v1/billing/credit-packages/'); setStats(statsData); setUsers(usersData.results || []); setCreditConfigs(configsData.results || []); + setCreditPackages(packagesData.results || []); } catch (error: any) { toast?.error(error?.message || 'Failed to load admin data'); } finally { @@ -90,7 +109,7 @@ const AdminBilling: React.FC = () => { } try { - await fetchAPI(`/v1/admin/billing/users/${selectedUser.id}/adjust-credits/`, { + await fetchAPI(`/v1/admin/users/${selectedUser.id}/adjust-credits/`, { method: 'POST', body: JSON.stringify({ amount: parseInt(creditAmount), @@ -110,9 +129,9 @@ const AdminBilling: React.FC = () => { const handleUpdateCreditCost = async (configId: number, newCost: number) => { try { - await fetchAPI(`/v1/admin/billing/credit-costs/${configId}/`, { - method: 'PATCH', - body: JSON.stringify({ cost: newCost }), + await fetchAPI('/v1/admin/credit-costs/', { + method: 'POST', + body: JSON.stringify({ updates: [{ id: configId, cost: newCost }] }), }); toast?.success('Credit cost updated successfully'); @@ -123,10 +142,26 @@ const AdminBilling: React.FC = () => { }; const filteredUsers = users.filter(user => - user.username.toLowerCase().includes(searchTerm.toLowerCase()) || - user.email.toLowerCase().includes(searchTerm.toLowerCase()) + (user.email || '').toLowerCase().includes(searchTerm.toLowerCase()) || + (user.username || '').toLowerCase().includes(searchTerm.toLowerCase()) || + (user.account_name || '').toLowerCase().includes(searchTerm.toLowerCase()) ); + const formatLabel = (value?: string) => + (value || '') + .split('_') + .map((w) => (w ? w[0].toUpperCase() + w.slice(1) : '')) + .join(' ') + .trim(); + + const updateLocalCost = (id: number, value: string) => { + setCreditConfigs((prev) => + prev.map((c) => + c.id === id ? { ...c, credits_cost: value === '' ? ('' as any) : Number(value) } : c + ) + ); + }; + if (loading) { return (
@@ -220,6 +255,16 @@ const AdminBilling: React.FC = () => { > Credit Pricing ({creditConfigs.length}) +
@@ -399,19 +444,22 @@ const AdminBilling: React.FC = () => { - + + + + - - @@ -419,27 +467,34 @@ const AdminBilling: React.FC = () => { {creditConfigs.map((config) => ( - - - - - + + @@ -447,16 +502,37 @@ const AdminBilling: React.FC = () => {
- Model - Operation + Display Name + + Credits Cost + + Unit + + Description + - Cost (Credits) - - Status - Actions
- {config.model_name} - + {config.operation_type} - {config.cost} + + {config.display_name || formatLabel(config.operation_type)} - - {config.is_active ? 'Active' : 'Inactive'} - + + updateLocalCost(config.id, e.target.value)} + className="w-24 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-800 dark:text-white" + /> + + {formatLabel(config.unit)} + + {config.description || '—'} +
-
- To add new credit costs or modify these settings, use the{' '} - - Django Admin Panel - + + )} + + {activeTab === 'packages' && ( + +
+ {creditPackages.map((pkg) => ( + +
+
{pkg.credits.toLocaleString()}
+
credits
+
${pkg.price}
+ {pkg.discount_percentage > 0 && ( +
Save {pkg.discount_percentage}%
+ )} +
+ + {pkg.is_active ? 'Active' : 'Inactive'} + + {pkg.is_featured && ( + + Featured + + )} +
+
+
+ ))} + {creditPackages.length === 0 && ( +
No credit packages found.
+ )}
)} diff --git a/frontend/src/pages/Admin/AdminCreditCostsPage.tsx b/frontend/src/pages/Admin/AdminCreditCostsPage.tsx index d8472730..3cfad9a7 100644 --- a/frontend/src/pages/Admin/AdminCreditCostsPage.tsx +++ b/frontend/src/pages/Admin/AdminCreditCostsPage.tsx @@ -158,164 +158,4 @@ export default function AdminCreditCostsPage() {
); } -/** - * Admin Credit Costs Page - * Manage credit pricing per billable action - */ -import { useEffect, useState } from 'react'; -import { AlertCircle, Loader2, Check } from 'lucide-react'; -import PageMeta from '../../components/common/PageMeta'; -import { Card } from '../../components/ui/card'; -import Button from '../../components/ui/button/Button'; -import { - getCreditCosts, - updateCreditCosts, - type CreditCostConfig, -} from '../../services/billing.api'; - -export default function AdminCreditCostsPage() { - const [costs, setCosts] = useState([]); - const [loading, setLoading] = useState(true); - const [savingId, setSavingId] = useState(null); - const [error, setError] = useState(''); - - useEffect(() => { - loadCosts(); - }, []); - - const loadCosts = async () => { - try { - setLoading(true); - const data = await getCreditCosts(); - setCosts(data.results || []); - } catch (err: any) { - setError(err.message || 'Failed to load credit costs'); - } finally { - setLoading(false); - } - }; - - const handleSave = async (cost: CreditCostConfig) => { - try { - setSavingId(cost.id); - await updateCreditCosts([ - { operation_type: cost.operation_type, credits_cost: Number(cost.credits_cost) || 0 }, - ]); - await loadCosts(); - } catch (err: any) { - setError(err.message || 'Failed to update credit cost'); - } finally { - setSavingId(null); - } - }; - - const updateLocalCost = (id: number, value: string) => { - setCosts((prev) => - prev.map((c) => (c.id === id ? { ...c, credits_cost: value as any } : c)), - ); - }; - - if (loading) { - return ( -
- -
- ); - } - - return ( -
- - -
-

Credit Costs

-

- Configure credits required for each billable operation. -

-
- - {error && ( -
- -

{error}

-
- )} - - -
- - - - - - - - - - - - - {costs.length === 0 ? ( - - - - ) : ( - costs.map((cost) => ( - - - - - - - - - )) - )} - -
- Operation - - Display Name - - Credits Cost - - Unit - - Description - - Actions -
- No credit costs configured -
- {cost.operation_type} - - {cost.display_name || '-'} - - updateLocalCost(cost.id, e.target.value)} - className="w-24 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-800 dark:text-white" - /> - - {cost.unit} - - {cost.description} - - -
-
-
-
- ); -} diff --git a/frontend/src/pages/Settings/ApiMonitor.tsx b/frontend/src/pages/Settings/ApiMonitor.tsx index 1aa4e56a..b05c22ef 100644 --- a/frontend/src/pages/Settings/ApiMonitor.tsx +++ b/frontend/src/pages/Settings/ApiMonitor.tsx @@ -107,7 +107,7 @@ const endpointGroups: EndpointGroup[] = [ { path: "/v1/system/strategies/", method: "GET", description: "List strategies" }, { path: "/v1/system/settings/integrations/openai/test/", method: "POST", description: "Test integration (OpenAI)" }, { path: "/v1/system/settings/account/", method: "GET", description: "Account settings" }, - { path: "/v1/billing/credits/balance/balance/", method: "GET", description: "Credit balance" }, + { path: "/v1/billing/credits/balance/", method: "GET", description: "Credit balance" }, { path: "/v1/billing/credits/usage/", method: "GET", description: "Usage logs" }, { path: "/v1/billing/credits/usage/summary/", method: "GET", description: "Usage summary" }, { diff --git a/frontend/src/pages/admin/AdminCreditPackagesPage.tsx b/frontend/src/pages/admin/AdminCreditPackagesPage.tsx index 243185f2..14d52c96 100644 --- a/frontend/src/pages/admin/AdminCreditPackagesPage.tsx +++ b/frontend/src/pages/admin/AdminCreditPackagesPage.tsx @@ -7,7 +7,7 @@ import { useState, useEffect } from 'react'; import { Plus, Loader2, AlertCircle, Edit, Trash } from 'lucide-react'; import { Card } from '../../components/ui/card'; import Badge from '../../components/ui/badge/Badge'; -import { getCreditPackages, type CreditPackage } from '../../services/billing.api'; +import { getAdminCreditPackages, type CreditPackage } from '../../services/billing.api'; export default function AdminCreditPackagesPage() { const [packages, setPackages] = useState([]); @@ -21,7 +21,7 @@ export default function AdminCreditPackagesPage() { const loadPackages = async () => { try { setLoading(true); - const data = await getCreditPackages(); + const data = await getAdminCreditPackages(); setPackages(data.results || []); } catch (err: any) { setError(err.message || 'Failed to load credit packages'); diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index fd8131f3..7fc813e5 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -1743,12 +1743,12 @@ export interface UsageSummary { export async function fetchCreditBalance(): Promise { try { - const response = await fetchAPI('/v1/billing/credits/balance/balance/'); - // fetchAPI automatically extracts data field from unified format + // Canonical balance endpoint (business billing CreditTransactionViewSet.balance) + const response = await fetchAPI('/v1/billing/transactions/balance/'); if (response && typeof response === 'object' && 'credits' in response) { return response as CreditBalance; } - // Return default if response is invalid + // Default if response is invalid return { credits: 0, plan_credits_per_month: 0, @@ -1756,7 +1756,7 @@ export async function fetchCreditBalance(): Promise { credits_remaining: 0, }; } catch (error: any) { - console.warn('Failed to fetch credit balance, using defaults:', error.message); + console.debug('Failed to fetch credit balance, using defaults:', error?.message || error); // Return default balance on error so UI can still render return { credits: 0, diff --git a/frontend/src/services/billing.api.ts b/frontend/src/services/billing.api.ts index 69611c8c..c9a74f77 100644 --- a/frontend/src/services/billing.api.ts +++ b/frontend/src/services/billing.api.ts @@ -1,6 +1,6 @@ /** * Billing API Service - * Uses STANDARD existing billing endpoints from /v1/billing/, /v1/system/, and /v1/admin/ + * Uses STANDARD billing endpoints from /api/v1/billing and /api/v1/admin/billing */ import { fetchAPI } from './api'; @@ -142,6 +142,9 @@ export interface CreditPackage { is_featured: boolean; description: string; display_order: number; + is_active?: boolean; + sort_order?: number; + stripe_price_id?: string | null; } export interface TeamMember { @@ -188,6 +191,7 @@ export interface PendingPayment extends Payment { // ============================================================================ export async function getCreditBalance(): Promise { + // Use business billing CreditTransactionViewSet.balance return fetchAPI('/v1/billing/transactions/balance/'); } @@ -196,7 +200,7 @@ export async function getCreditTransactions(): Promise<{ count: number; current_balance?: number; }> { - return fetchAPI('/v1/billing/transactions/'); + return fetchAPI('/api/v1/billing/transactions/'); } // ============================================================================ @@ -216,7 +220,7 @@ export async function getCreditUsage(params?: { if (params?.start_date) queryParams.append('start_date', params.start_date); if (params?.end_date) queryParams.append('end_date', params.end_date); - const url = `/v1/billing/credits/usage/${queryParams.toString() ? '?' + queryParams.toString() : ''}`; + const url = `/api/v1/billing/credits/usage/${queryParams.toString() ? '?' + queryParams.toString() : ''}`; return fetchAPI(url); } @@ -261,8 +265,8 @@ export async function getCreditUsageLimits(): Promise<{ // ============================================================================ export async function getAdminBillingStats(): Promise { - // Use business billing admin stats endpoint (returns all dashboard metrics) - return fetchAPI('/v1/billing/admin/stats/'); + // Admin billing dashboard metrics + return fetchAPI('/v1/admin/billing/stats/'); } export async function getAdminInvoices(params?: { status?: string; account_id?: number; search?: string }): Promise<{ @@ -332,7 +336,8 @@ export async function getCreditCosts(): Promise<{ results: CreditCostConfig[]; count: number; }> { - return fetchAPI('/v1/admin/billing/credit-costs/'); + // credit costs are served from the admin namespace (modules billing) + return fetchAPI('/v1/admin/credit-costs/'); } export async function updateCreditCosts( @@ -344,7 +349,7 @@ export async function updateCreditCosts( message: string; updated_count: number; }> { - return fetchAPI('/v1/admin/billing/credit-costs/', { + return fetchAPI('/v1/admin/credit-costs/', { method: 'POST', body: JSON.stringify({ costs }), }); @@ -434,6 +439,15 @@ export async function getCreditPackages(): Promise<{ return fetchAPI('/v1/billing/credit-packages/'); } +// Admin credit packages (CRUD capable backend; currently read-only here) +export async function getAdminCreditPackages(): Promise<{ + results: CreditPackage[]; + count: number; +}> { + // Backend does not expose a dedicated admin credit-packages list; fall back to the shared packages list + return fetchAPI('/v1/billing/credit-packages/'); +} + export async function purchaseCreditPackage(data: { package_id: number; payment_method: 'stripe' | 'paypal' | 'bank_transfer' | 'local_wallet'; @@ -447,7 +461,7 @@ export async function purchaseCreditPackage(data: { stripe_client_secret?: string; paypal_order_id?: string; }> { - return fetchAPI(`/v1/billing/credit-packages/${data.package_id}/purchase/`, { + return fetchAPI(`/api/v1/billing/credit-packages/${data.package_id}/purchase/`, { method: 'POST', body: JSON.stringify({ payment_method: data.payment_method }), }); @@ -495,7 +509,7 @@ export async function getAvailablePaymentMethods(): Promise<{ results: PaymentMethod[]; count: number; }> { - return fetchAPI('/v1/billing/payment-methods/'); + return fetchAPI('/api/v1/billing/payment-methods/'); } export async function createManualPayment(data: { @@ -509,7 +523,7 @@ export async function createManualPayment(data: { status: string; message: string; }> { - return fetchAPI('/v1/billing/payments/manual/', { + return fetchAPI('/api/v1/billing/payments/manual/', { method: 'POST', body: JSON.stringify(data), }); @@ -523,7 +537,7 @@ export async function getPendingPayments(): Promise<{ results: PendingPayment[]; count: number; }> { - return fetchAPI('/v1/billing/admin/pending_payments/'); + return fetchAPI('/api/v1/billing/admin/pending_payments/'); } export async function approvePayment(paymentId: number, data?: { @@ -532,7 +546,7 @@ export async function approvePayment(paymentId: number, data?: { message: string; payment: Payment; }> { - return fetchAPI(`/v1/billing/admin/${paymentId}/approve_payment/`, { + return fetchAPI(`/api/v1/billing/admin/${paymentId}/approve_payment/`, { method: 'POST', body: JSON.stringify(data || {}), }); @@ -545,7 +559,7 @@ export async function rejectPayment(paymentId: number, data: { message: string; payment: Payment; }> { - return fetchAPI(`/v1/billing/admin/${paymentId}/reject_payment/`, { + return fetchAPI(`/api/v1/billing/admin/${paymentId}/reject_payment/`, { method: 'POST', body: JSON.stringify(data), });