'use client'; import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import api from '@/lib/api'; import { toast } from 'react-toastify'; import type { PricingCatalog, PricingRateRow, BillingCycle, PricingResourceType, LifecycleSettings, } from '@/types'; import { DollarSign, Edit2, Shield, Clock, Layers } from 'lucide-react'; type AppRuntime = 'nodejs' | 'laravel' | 'wordpress'; const runtimeTabs: { value: AppRuntime; label: string }[] = [ { value: 'nodejs', label: 'Node.js' }, { value: 'laravel', label: 'Laravel' }, { value: 'wordpress', label: 'WordPress' }, ]; const addonResourceTypes: PricingResourceType[] = [ 'redis_addon', 'rabbitmq_addon', 'elasticsearch_addon', 'custom_domain_addon', ]; const resourceLabels: Record = { base_fee: 'Base fee', cpu_per_core: 'CPU (per core)', memory_per_gb: 'Memory (per GB)', storage_per_gb: 'Storage (per GB)', database_addon: 'Database addon', redis_addon: 'Redis', rabbitmq_addon: 'RabbitMQ', elasticsearch_addon: 'Elasticsearch', custom_domain_addon: 'Custom domain + SSL', }; const cycles: BillingCycle[] = ['hourly', 'monthly', 'yearly']; function cloneCatalog(catalog: PricingCatalog): PricingCatalog { const runtimes = {} as PricingCatalog['runtimes']; for (const rt of runtimeTabs) { runtimes[rt.value] = catalog.runtimes[rt.value].map((r) => ({ ...r })); } return { runtimes, addons: catalog.addons.map((a) => ({ ...a })), }; } function PricingMatrixTable({ rows, onChange, readOnly, }: { rows: PricingRateRow[]; onChange: (resourceType: PricingResourceType, cycle: BillingCycle, value: number) => void; readOnly: boolean; }) { return (
{cycles.map((cycle) => ( ))} {rows.map((row) => ( {cycles.map((cycle) => { const field = cycle === 'hourly' ? 'hourlyPrice' : cycle === 'monthly' ? 'monthlyPrice' : 'yearlyPrice'; const val = row[field]; return ( ); })} ))}
Resource {cycle} (T)
{resourceLabels[row.resourceType]} {readOnly ? ( {Number(val).toLocaleString('en-US')} ) : ( onChange( row.resourceType, cycle, e.target.value === '' ? 0 : Number(e.target.value), ) } /> )}
); } export default function AdminBillingPage() { const queryClient = useQueryClient(); const [activeRuntime, setActiveRuntime] = useState('nodejs'); const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(null); const { data: catalog, isLoading } = useQuery({ queryKey: ['pricing-catalog'], queryFn: () => api.get('/billing/pricing-catalog').then((r) => r.data), }); const saveMutation = useMutation({ mutationFn: (body: PricingCatalog) => api.patch('/billing/pricing-catalog', body), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['pricing-catalog'] }); queryClient.invalidateQueries({ queryKey: ['custom-domain-price'] }); queryClient.invalidateQueries({ queryKey: ['optional-services-pricing'] }); toast.success('Pricing catalog saved'); setEditing(false); setDraft(null); }, onError: (err: unknown) => { const message = err && typeof err === 'object' && 'response' in err ? (err as { response?: { data?: { message?: string } } }).response?.data?.message : undefined; toast.error(message || 'Failed to save pricing'); }, }); const display = editing && draft ? draft : catalog; const startEdit = () => { if (!catalog) return; setDraft(cloneCatalog(catalog)); setEditing(true); }; const updateRuntimePrice = ( resourceType: PricingResourceType, cycle: BillingCycle, value: number, ) => { if (!draft) return; const field = cycle === 'hourly' ? 'hourlyPrice' : cycle === 'monthly' ? 'monthlyPrice' : 'yearlyPrice'; setDraft({ ...draft, runtimes: { ...draft.runtimes, [activeRuntime]: draft.runtimes[activeRuntime].map((row) => row.resourceType === resourceType ? { ...row, [field]: value } : row, ), }, }); }; const updateAddonPrice = ( resourceType: PricingResourceType, cycle: BillingCycle, value: number, ) => { if (!draft) return; const field = cycle === 'hourly' ? 'hourlyPrice' : cycle === 'monthly' ? 'monthlyPrice' : 'yearlyPrice'; setDraft({ ...draft, addons: draft.addons.map((row) => row.resourceType === resourceType ? { ...row, [field]: value } : row, ), }); }; const fillYearlyFromMonthly = (scope: 'runtime' | 'addons') => { if (!draft) return; if (scope === 'runtime') { setDraft({ ...draft, runtimes: { ...draft.runtimes, [activeRuntime]: draft.runtimes[activeRuntime].map((row) => ({ ...row, yearlyPrice: Math.round(Number(row.monthlyPrice) * 12), })), }, }); } else { setDraft({ ...draft, addons: draft.addons.map((row) => ({ ...row, yearlyPrice: Math.round(Number(row.monthlyPrice) * 12), })), }); } }; const handleSave = () => { if (!draft) return; saveMutation.mutate(draft); }; const addonRows = display?.addons.filter((a) => addonResourceTypes.includes(a.resourceType)) ?? []; return (

Billing & Pricing

Usage-based prices per application type. Each resource has explicit hourly, monthly, and yearly rates — deploy cost uses the column for the cycle the user selects.

{!editing ? ( ) : (
)}
{isLoading ? (
Loading...
) : !display ? (
No pricing data
) : ( <>
{runtimeTabs.map((tab) => ( ))}

{runtimeTabs.find((t) => t.value === activeRuntime)?.label} resources

{editing && ( )}

Platform add-ons

{editing && ( )}

Redis, RabbitMQ, Elasticsearch, and custom domain — same prices for all application types.

)}
); } function LifecycleSettingsSection() { const queryClient = useQueryClient(); const [editing, setEditing] = useState(false); const [hourlyHours, setHourlyHours] = useState(''); const [monthlyDays, setMonthlyDays] = useState(''); const [yearlyDays, setYearlyDays] = useState(''); const { data: settings, isLoading } = useQuery({ queryKey: ['lifecycle-settings'], queryFn: () => api.get('/lifecycle/settings').then((r) => r.data), }); const saveMutation = useMutation({ mutationFn: (body: Record) => api.patch('/lifecycle/settings', body), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['lifecycle-settings'] }); toast.success('Lifecycle settings updated'); setEditing(false); }, onError: (err: unknown) => { const message = err && typeof err === 'object' && 'response' in err ? (err as { response?: { data?: { message?: string } } }).response?.data?.message : undefined; toast.error(message || 'Failed to save'); }, }); const startEditing = () => { if (settings) { setHourlyHours(String((settings.hourly.deleteAfterMs || 0) / 3600000)); setMonthlyDays(String((settings.monthly.deleteAfterMs || 0) / 86400000)); setYearlyDays(String((settings.yearly.deleteAfterMs || 0) / 86400000)); } setEditing(true); }; const handleSave = () => { const body: Record = {}; if (hourlyHours) body.hourlyDeleteAfterMs = Number(hourlyHours) * 3600000; if (monthlyDays) body.monthlyDeleteAfterMs = Number(monthlyDays) * 86400000; if (yearlyDays) body.yearlyDeleteAfterMs = Number(yearlyDays) * 86400000; saveMutation.mutate(body); }; return (

Data Retention & Deletion Policy

{!editing && ( )}

Configure how long user data is retained after plan expiration before permanent deletion.

{isLoading ? (
Loading...
) : editing ? (

Hourly Plans

setHourlyHours(e.target.value)} min={1} placeholder="24" />

Monthly Plans

setMonthlyDays(e.target.value)} min={1} placeholder="3" />

Yearly Plans

setYearlyDays(e.target.value)} min={1} placeholder="7" />
) : (

Hourly Plans

{settings?.hourly.deleteAfterHours ?? Math.round((settings?.hourly.deleteAfterMs || 0) / 3600000)} hours

Monthly Plans

{settings?.monthly.deleteAfterDays ?? Math.round((settings?.monthly.deleteAfterMs || 0) / 86400000)} days

Yearly Plans

{settings?.yearly.deleteAfterDays ?? Math.round((settings?.yearly.deleteAfterMs || 0) / 86400000)} days

)}
); }