feat: billing system — service plans, wallet, cost calculation
- Backend: billing module with ServicePlan, PricingRule, Wallet, WalletTransaction entities - Admin can CRUD service plans with hourly/monthly/yearly billing cycles - Each plan has flexible pricing rules (base_fee, cpu, memory, storage, db addon) - Wallet system: auto-created per user, charge, deduct, refund, transaction history - Cost calculation endpoint: cross-cycle conversion (hourly*720=monthly, monthly*12=yearly) - Frontend: admin billing management page (/dashboard/admin/billing) - Frontend: user wallet page with balance, quick-charge, transaction history - Deploy page: cost breakdown shown in Review step (hourly/monthly/yearly) - Navigation: Wallet link for users, Billing Plans link for admins
This commit is contained in:
@@ -6,8 +6,8 @@ import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import api from '@/lib/api';
|
||||
import { useAuthStore } from '@/lib/store';
|
||||
import { toast } from 'react-toastify';
|
||||
import type { CreateApplicationDto, ClusterPublic, ClusterPoolPublic } from '@/types';
|
||||
import { Hexagon, FolderUp, Link as LinkIcon, CheckCircle, Package, Server, Scale, Home, Target, BarChart3, RotateCw, KeyRound, Rocket, Clock, Upload, XCircle, Database, Eye, EyeOff, RefreshCw } from 'lucide-react';
|
||||
import type { CreateApplicationDto, ClusterPublic, ClusterPoolPublic, CostBreakdown } from '@/types';
|
||||
import { Hexagon, FolderUp, Link as LinkIcon, CheckCircle, Package, Server, Scale, Home, Target, BarChart3, RotateCw, KeyRound, Rocket, Clock, Upload, XCircle, Database, Eye, EyeOff, RefreshCw, DollarSign } from 'lucide-react';
|
||||
|
||||
const steps = ['Basic Info', 'Versions & Database', 'Resources', 'Review'];
|
||||
|
||||
@@ -61,6 +61,20 @@ export default function DeployPage() {
|
||||
enabled: isAdmin,
|
||||
});
|
||||
|
||||
// Cost calculation for the review step
|
||||
const { data: costData, isLoading: costLoading } = useQuery<CostBreakdown>({
|
||||
queryKey: ['deploy-cost', form.runtime, form.databaseType, form.cpuLimit, form.memoryLimit, form.replicas, form.dbStorageSize],
|
||||
queryFn: () => api.post('/billing/calculate', {
|
||||
runtime: form.runtime,
|
||||
databaseType: form.databaseType,
|
||||
cpuLimit: form.cpuLimit,
|
||||
memoryLimit: form.memoryLimit,
|
||||
replicas: form.replicas,
|
||||
dbStorageSize: form.databaseType !== 'none' ? `${parseInt(form.dbStorageSize || '1', 10) || 1}` : undefined,
|
||||
}).then((r) => r.data),
|
||||
enabled: step === 3,
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: async (data: CreateApplicationDto) => {
|
||||
const res = await api.post('/applications', data);
|
||||
@@ -1098,6 +1112,49 @@ export default function DeployPage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Cost Breakdown */}
|
||||
<div className="bg-gradient-to-br from-emerald-50 to-teal-50 rounded-xl p-5 sm:p-6 border border-emerald-200">
|
||||
<h3 className="text-sm font-semibold text-gray-700 flex items-center gap-2 mb-3">
|
||||
<DollarSign className="w-4 h-4 text-emerald-600" /> برآورد هزینه
|
||||
</h3>
|
||||
{costLoading ? (
|
||||
<div className="text-sm text-gray-400 text-center py-3">در حال محاسبه...</div>
|
||||
) : costData ? (
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-3 gap-3 text-center">
|
||||
<div className="bg-white rounded-lg p-3 shadow-sm">
|
||||
<p className="text-xs text-gray-500">ساعتی</p>
|
||||
<p className="text-lg font-bold text-emerald-700">{Number(costData.hourly).toLocaleString('fa-IR')}</p>
|
||||
<p className="text-xs text-gray-400">تومان</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg p-3 shadow-sm ring-2 ring-emerald-200">
|
||||
<p className="text-xs text-gray-500">ماهانه</p>
|
||||
<p className="text-lg font-bold text-emerald-700">{Number(costData.monthly).toLocaleString('fa-IR')}</p>
|
||||
<p className="text-xs text-gray-400">تومان</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg p-3 shadow-sm">
|
||||
<p className="text-xs text-gray-500">سالانه</p>
|
||||
<p className="text-lg font-bold text-emerald-700">{Number(costData.yearly).toLocaleString('fa-IR')}</p>
|
||||
<p className="text-xs text-gray-400">تومان</p>
|
||||
</div>
|
||||
</div>
|
||||
{costData.breakdown && costData.breakdown.length > 0 && (
|
||||
<div className="mt-2 pt-3 border-t border-emerald-200/50">
|
||||
<p className="text-xs font-medium text-gray-500 mb-2">جزئیات</p>
|
||||
{costData.breakdown.map((item, i) => (
|
||||
<div key={i} className="flex justify-between text-xs py-1">
|
||||
<span className="text-gray-600">{item.label}</span>
|
||||
<span className="text-gray-900 font-medium">{Number(item.monthly).toLocaleString('fa-IR')} ت/ماه</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-gray-400 text-center py-3">هنوز پلنی تعریف نشده</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user