Billing and account fixed - final
This commit is contained in:
0
docs/user-flow/user-flow-initial
Normal file
0
docs/user-flow/user-flow-initial
Normal file
@@ -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"));
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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<SystemStats | null>(null);
|
||||
const [users, setUsers] = useState<UserAccount[]>([]);
|
||||
const [creditConfigs, setCreditConfigs] = useState<CreditCostConfig[]>([]);
|
||||
const [creditPackages, setCreditPackages] = useState<CreditPackageItem[]>([]);
|
||||
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<UserAccount | null>(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 (
|
||||
<div className="p-6">
|
||||
@@ -220,6 +255,16 @@ const AdminBilling: React.FC = () => {
|
||||
>
|
||||
Credit Pricing ({creditConfigs.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('packages')}
|
||||
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'packages'
|
||||
? 'border-primary-500 text-primary-600 dark:text-primary-400'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
Credit Packages ({creditPackages.length})
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -399,19 +444,22 @@ const AdminBilling: React.FC = () => {
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead className="bg-gray-50 dark:bg-gray-800">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
|
||||
Model
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
|
||||
Operation
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
|
||||
Display Name
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
|
||||
Credits Cost
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
|
||||
Unit
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
|
||||
Description
|
||||
</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
|
||||
Cost (Credits)
|
||||
</th>
|
||||
<th className="px-6 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
|
||||
Status
|
||||
</th>
|
||||
<th className="px-6 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
@@ -419,27 +467,34 @@ const AdminBilling: React.FC = () => {
|
||||
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{creditConfigs.map((config) => (
|
||||
<tr key={config.id}>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
|
||||
{config.model_name}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-700 dark:text-gray-300">
|
||||
{config.operation_type}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-bold text-amber-600 dark:text-amber-400">
|
||||
{config.cost}
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
|
||||
{config.display_name || formatLabel(config.operation_type)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-center">
|
||||
<Badge tone={config.is_active ? 'success' : 'warning'}>
|
||||
{config.is_active ? 'Active' : 'Inactive'}
|
||||
</Badge>
|
||||
<td className="px-6 py-4">
|
||||
<input
|
||||
type="number"
|
||||
value={config.credits_cost as any}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-center">
|
||||
<td className="px-6 py-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
{formatLabel(config.unit)}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-600 dark:text-gray-400 max-w-md">
|
||||
{config.description || '—'}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => window.open(`/admin/igny8_core/creditcostconfig/${config.id}/change/`, '_blank')}
|
||||
variant="primary"
|
||||
tone="brand"
|
||||
onClick={() => handleUpdateCreditCost(config.id, Number(config.credits_cost) || 0)}
|
||||
>
|
||||
Edit
|
||||
Save
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -447,16 +502,37 @@ const AdminBilling: React.FC = () => {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="mt-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
To add new credit costs or modify these settings, use the{' '}
|
||||
<a
|
||||
href="/admin/igny8_core/creditcostconfig/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary-600 dark:text-primary-400 hover:underline"
|
||||
>
|
||||
Django Admin Panel
|
||||
</a>
|
||||
</ComponentCard>
|
||||
)}
|
||||
|
||||
{activeTab === 'packages' && (
|
||||
<ComponentCard title="Credit Packages">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{creditPackages.map((pkg) => (
|
||||
<ComponentCard key={pkg.id} title={pkg.name} desc={pkg.description || ''}>
|
||||
<div className="space-y-3">
|
||||
<div className="text-3xl font-bold text-blue-600">{pkg.credits.toLocaleString()}</div>
|
||||
<div className="text-sm text-gray-500">credits</div>
|
||||
<div className="text-2xl font-semibold text-gray-900 dark:text-white">${pkg.price}</div>
|
||||
{pkg.discount_percentage > 0 && (
|
||||
<div className="text-sm text-green-600">Save {pkg.discount_percentage}%</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="light" color={pkg.is_active ? 'success' : 'warning'}>
|
||||
{pkg.is_active ? 'Active' : 'Inactive'}
|
||||
</Badge>
|
||||
{pkg.is_featured && (
|
||||
<Badge variant="light" color="primary">
|
||||
Featured
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
))}
|
||||
{creditPackages.length === 0 && (
|
||||
<div className="col-span-3 text-center py-8 text-gray-500">No credit packages found.</div>
|
||||
)}
|
||||
</div>
|
||||
</ComponentCard>
|
||||
)}
|
||||
|
||||
@@ -158,164 +158,4 @@ export default function AdminCreditCostsPage() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 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<CreditCostConfig[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [savingId, setSavingId] = useState<number | null>(null);
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
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 (
|
||||
<div className="p-6 flex items-center justify-center min-h-screen">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-blue-600" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<PageMeta title="Admin - Credit Costs" description="Manage credit pricing per action" />
|
||||
|
||||
<div className="mb-6">
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Credit Costs</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
Configure credits required for each billable operation.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-6 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg flex items-center gap-3">
|
||||
<AlertCircle className="w-5 h-5 text-red-600" />
|
||||
<p className="text-red-800 dark:text-red-200">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Card className="overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Operation
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Display Name
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Credits Cost
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Unit
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Description
|
||||
</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{costs.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="px-6 py-8 text-center text-gray-500">
|
||||
No credit costs configured
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
costs.map((cost) => (
|
||||
<tr key={cost.id} className="hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||
<td className="px-6 py-4 text-sm font-mono text-gray-700 dark:text-gray-300">
|
||||
{cost.operation_type}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-900 dark:text-white">
|
||||
{cost.display_name || '-'}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<input
|
||||
type="number"
|
||||
value={cost.credits_cost as any}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
{cost.unit}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-600 dark:text-gray-400 max-w-md">
|
||||
{cost.description}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
tone="brand"
|
||||
startIcon={savingId === cost.id ? <Loader2 className="w-4 h-4 animate-spin" /> : <Check className="w-4 h-4" />}
|
||||
disabled={savingId === cost.id}
|
||||
onClick={() => handleSave(cost)}
|
||||
>
|
||||
{savingId === cost.id ? 'Saving...' : 'Save'}
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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" },
|
||||
{
|
||||
|
||||
@@ -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<CreditPackage[]>([]);
|
||||
@@ -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');
|
||||
|
||||
@@ -1743,12 +1743,12 @@ export interface UsageSummary {
|
||||
|
||||
export async function fetchCreditBalance(): Promise<CreditBalance> {
|
||||
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<CreditBalance> {
|
||||
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,
|
||||
|
||||
@@ -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<CreditBalance> {
|
||||
// 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<AdminBillingStats> {
|
||||
// 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),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user