payemnt billing and credits refactoring
This commit is contained in:
@@ -130,13 +130,29 @@ export default function BillingUsagePanel({ showOnlyActivity = false }: BillingU
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{balance && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-4">
|
||||
<Card className="p-6">
|
||||
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400 mb-2">Current Balance</h3>
|
||||
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400 mb-2">Plan Credits</h3>
|
||||
<div className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
{(balance?.credits ?? 0).toLocaleString()}
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">Available credits</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">Reset on renewal</p>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6 border-l-4 border-l-success-500">
|
||||
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400 mb-2">Bonus Credits</h3>
|
||||
<div className="text-3xl font-bold text-success-600 dark:text-success-400">
|
||||
{((balance as any)?.bonus_credits ?? 0).toLocaleString()}
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">Never expire</p>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400 mb-2">Total Available</h3>
|
||||
<div className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
{((balance as any)?.total_credits ?? (balance?.credits ?? 0)).toLocaleString()}
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">Plan + Bonus</p>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
@@ -145,21 +161,32 @@ export default function BillingUsagePanel({ showOnlyActivity = false }: BillingU
|
||||
{(balance?.plan_credits_per_month ?? 0).toLocaleString()}
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
{(balance as any)?.subscription_plan || 'No plan'}
|
||||
{(balance as any)?.subscription_plan || 'From plan'}
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400 mb-2">Status</h3>
|
||||
<div className="mt-2">
|
||||
<Badge variant="light" className="text-lg">
|
||||
{(balance as any)?.subscription_status || 'No subscription'}
|
||||
</Badge>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Info box about credit consumption order */}
|
||||
{balance && ((balance as any)?.bonus_credits ?? 0) > 0 && (
|
||||
<Card className="p-4 bg-info-50 dark:bg-info-900/20 border-info-200 dark:border-info-800">
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="text-info-600 dark:text-info-400">
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-info-800 dark:text-info-200">Credit Consumption Order</h4>
|
||||
<p className="text-sm text-info-700 dark:text-info-300 mt-1">
|
||||
Plan credits are used first. Bonus credits are only consumed after plan credits are exhausted.
|
||||
Bonus credits never expire and are not affected by plan renewals.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{usageLimits && (
|
||||
<Card className="p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-800 dark:text-white mb-4">Plan Limits</h2>
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
} from '../../icons';
|
||||
import { API_BASE_URL } from '../../services/api';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import { subscribeToPlan, getAvailablePaymentMethods } from '../../services/billing.api';
|
||||
import { subscribeToPlan, getAvailablePaymentMethods, purchaseCredits } from '../../services/billing.api';
|
||||
|
||||
interface BankDetails {
|
||||
bank_name: string;
|
||||
@@ -38,6 +38,8 @@ interface BankDetails {
|
||||
interface Invoice {
|
||||
id: number;
|
||||
invoice_number: string;
|
||||
invoice_type?: 'subscription' | 'credit_package' | 'addon' | 'custom';
|
||||
credit_package_id?: string | number | null;
|
||||
total?: string;
|
||||
total_amount?: string;
|
||||
currency?: string;
|
||||
@@ -126,6 +128,8 @@ export default function PayInvoiceModal({
|
||||
const currency = invoice.currency?.toUpperCase() || 'USD';
|
||||
const planId = invoice.subscription?.plan?.id;
|
||||
const planSlug = invoice.subscription?.plan?.slug;
|
||||
const isCreditInvoice = invoice.invoice_type === 'credit_package';
|
||||
const creditPackageId = invoice.credit_package_id ? String(invoice.credit_package_id) : null;
|
||||
|
||||
// Check if user's default method is selected (for showing badge)
|
||||
const isDefaultMethod = (option: PaymentOption): boolean => {
|
||||
@@ -181,6 +185,29 @@ export default function PayInvoiceModal({
|
||||
}, [isOpen, isPakistan, selectedOption, userCountry, bankDetails]);
|
||||
|
||||
const handleStripePayment = async () => {
|
||||
if (isCreditInvoice) {
|
||||
if (!creditPackageId) {
|
||||
setError('Unable to process card payment. Credit package not found on invoice. Please contact support.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
const result = await purchaseCredits(creditPackageId, 'stripe', {
|
||||
return_url: `${window.location.origin}/account/usage?purchase=success`,
|
||||
cancel_url: `${window.location.origin}/account/usage?purchase=canceled`,
|
||||
});
|
||||
|
||||
window.location.href = result.redirect_url;
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to initiate card payment');
|
||||
setLoading(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Use plan slug if available, otherwise fall back to id
|
||||
const planIdentifier = planSlug || (planId ? String(planId) : null);
|
||||
|
||||
@@ -208,6 +235,29 @@ export default function PayInvoiceModal({
|
||||
};
|
||||
|
||||
const handlePayPalPayment = async () => {
|
||||
if (isCreditInvoice) {
|
||||
if (!creditPackageId) {
|
||||
setError('Unable to process PayPal payment. Credit package not found on invoice. Please contact support.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
const result = await purchaseCredits(creditPackageId, 'paypal', {
|
||||
return_url: `${window.location.origin}/account/usage?purchase=success`,
|
||||
cancel_url: `${window.location.origin}/account/usage?purchase=canceled`,
|
||||
});
|
||||
|
||||
window.location.href = result.redirect_url;
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to initiate PayPal payment');
|
||||
setLoading(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Use plan slug if available, otherwise fall back to id
|
||||
const planIdentifier = planSlug || (planId ? String(planId) : null);
|
||||
|
||||
|
||||
@@ -368,7 +368,7 @@ export default function SiteDashboard() {
|
||||
/>
|
||||
|
||||
<CreditAvailabilityWidget
|
||||
availableCredits={balance?.credits_remaining ?? 0}
|
||||
availableCredits={(balance as any)?.total_credits ?? balance?.credits_remaining ?? 0}
|
||||
totalCredits={balance?.plan_credits_per_month ?? 0}
|
||||
usedCredits={balance?.credits_used_this_month ?? 0}
|
||||
loading={loading}
|
||||
|
||||
@@ -921,16 +921,26 @@ export default function PlansAndBillingPage() {
|
||||
</div>
|
||||
|
||||
{/* Quick Stats */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
|
||||
<div className="p-4 bg-white/80 dark:bg-gray-800/60 rounded-xl shadow-sm">
|
||||
<div className="flex items-center gap-2 text-sm text-brand-700 dark:text-brand-300 mb-1">
|
||||
<ZapIcon className="w-4 h-4 text-brand-600" />
|
||||
Credits
|
||||
Plan Credits
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-brand-900 dark:text-white">
|
||||
{creditBalance?.credits?.toLocaleString() || 0}
|
||||
</div>
|
||||
<div className="text-xs text-brand-600 dark:text-brand-400 mt-1">Available now</div>
|
||||
<div className="text-xs text-brand-600 dark:text-brand-400 mt-1">Reset on renewal</div>
|
||||
</div>
|
||||
<div className="p-4 bg-white/80 dark:bg-gray-800/60 rounded-xl shadow-sm border-l-4 border-l-success-500">
|
||||
<div className="flex items-center gap-2 text-sm text-success-700 dark:text-success-300 mb-1">
|
||||
<ZapIcon className="w-4 h-4 text-success-600" />
|
||||
Bonus Credits
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-success-600 dark:text-success-400">
|
||||
{((creditBalance as any)?.bonus_credits || 0).toLocaleString()}
|
||||
</div>
|
||||
<div className="text-xs text-success-600 dark:text-success-400 mt-1">Never expire</div>
|
||||
</div>
|
||||
<div className="p-4 bg-white/80 dark:bg-gray-800/60 rounded-xl shadow-sm">
|
||||
<div className="flex items-center gap-2 text-sm text-purple-700 dark:text-purple-300 mb-1">
|
||||
@@ -972,6 +982,21 @@ export default function PlansAndBillingPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Total Credits Info Box */}
|
||||
{((creditBalance as any)?.bonus_credits || 0) > 0 && (
|
||||
<div className="mt-4 p-3 bg-white/60 dark:bg-gray-800/60 rounded-lg border border-success-200 dark:border-success-800">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Total Available:</span>
|
||||
<span className="text-lg font-bold text-brand-700 dark:text-brand-400">
|
||||
{((creditBalance?.credits || 0) + ((creditBalance as any)?.bonus_credits || 0)).toLocaleString()} credits
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">Plan + Bonus</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Credit Usage Bar */}
|
||||
<div className="mt-6 pt-6 border-t border-brand-200/50 dark:border-brand-700/30">
|
||||
<div className="flex justify-between text-sm mb-2">
|
||||
@@ -1319,17 +1344,22 @@ export default function PlansAndBillingPage() {
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-3 text-center">
|
||||
<Badge variant="soft" tone={invoice.status === 'paid' ? 'success' : 'warning'}>
|
||||
<Badge variant="soft" tone={
|
||||
invoice.status === 'paid' ? 'success' :
|
||||
invoice.status === 'failed' ? 'error' :
|
||||
invoice.status === 'overdue' ? 'error' :
|
||||
'warning'
|
||||
}>
|
||||
{invoice.status}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-6 py-3 text-end">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
{invoice.status === 'pending' && (
|
||||
{['pending', 'overdue', 'failed', 'sent'].includes(invoice.status) && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
tone="brand"
|
||||
tone={invoice.status === 'overdue' || invoice.status === 'failed' ? 'error' : 'brand'}
|
||||
startIcon={<DollarLineIcon className="w-4 h-4" />}
|
||||
onClick={() => {
|
||||
setSelectedInvoice(invoice);
|
||||
|
||||
@@ -469,27 +469,49 @@ export default function UsageDashboardPage() {
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-6">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<div className="text-4xl font-bold text-brand-700 dark:text-brand-400 mb-1">
|
||||
<div className="text-3xl font-bold text-brand-700 dark:text-brand-400 mb-1">
|
||||
{creditBalance?.credits.toLocaleString() || 0}
|
||||
</div>
|
||||
<div className="text-sm text-brand-600 dark:text-brand-300">Available Now</div>
|
||||
<div className="text-sm text-brand-600 dark:text-brand-300">Plan Credits</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-4xl font-bold text-purple-700 dark:text-purple-400 mb-1">
|
||||
<div className="text-3xl font-bold text-success-600 dark:text-success-400 mb-1">
|
||||
{((creditBalance as any)?.bonus_credits || 0).toLocaleString()}
|
||||
</div>
|
||||
<div className="text-sm text-success-600 dark:text-success-300">Bonus Credits</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">Never expire</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-3xl font-bold text-purple-700 dark:text-purple-400 mb-1">
|
||||
{creditBalance?.credits_used_this_month.toLocaleString() || 0}
|
||||
</div>
|
||||
<div className="text-sm text-purple-600 dark:text-purple-300">Used This Month</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-4xl font-bold text-indigo-800 dark:text-white mb-1">
|
||||
<div className="text-3xl font-bold text-indigo-800 dark:text-white mb-1">
|
||||
{creditBalance?.plan_credits_per_month.toLocaleString() || 0}
|
||||
</div>
|
||||
<div className="text-sm text-indigo-600 dark:text-indigo-300">Monthly Allowance</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Total Available Credits */}
|
||||
{((creditBalance as any)?.bonus_credits || 0) > 0 && (
|
||||
<div className="mt-4 p-3 bg-white/60 dark:bg-gray-800/60 rounded-lg border border-brand-200 dark:border-brand-700">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Total Available</span>
|
||||
<span className="text-lg font-bold text-brand-700 dark:text-brand-400">
|
||||
{((creditBalance?.credits || 0) + ((creditBalance as any)?.bonus_credits || 0)).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
Plan credits are used first, then bonus credits
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Credit Usage Bar */}
|
||||
<div className="mt-6">
|
||||
<div className="flex justify-between text-sm mb-2">
|
||||
|
||||
@@ -2087,7 +2087,9 @@ export async function fetchGlobalModuleSettings(): Promise<GlobalModuleSettings>
|
||||
|
||||
// Billing API functions
|
||||
export interface CreditBalance {
|
||||
credits: number;
|
||||
credits: number; // Plan credits (reset on renewal)
|
||||
bonus_credits: number; // Purchased credits (never expire)
|
||||
total_credits: number; // Sum of plan + bonus credits
|
||||
plan_credits_per_month: number;
|
||||
credits_used_this_month: number;
|
||||
credits_remaining: number;
|
||||
@@ -2143,6 +2145,8 @@ export async function fetchCreditBalance(): Promise<CreditBalance> {
|
||||
// Default if response is invalid
|
||||
return {
|
||||
credits: 0,
|
||||
bonus_credits: 0,
|
||||
total_credits: 0,
|
||||
plan_credits_per_month: 0,
|
||||
credits_used_this_month: 0,
|
||||
credits_remaining: 0,
|
||||
@@ -2152,6 +2156,8 @@ export async function fetchCreditBalance(): Promise<CreditBalance> {
|
||||
// Return default balance on error so UI can still render
|
||||
return {
|
||||
credits: 0,
|
||||
bonus_credits: 0,
|
||||
total_credits: 0,
|
||||
plan_credits_per_month: 0,
|
||||
credits_used_this_month: 0,
|
||||
credits_remaining: 0,
|
||||
|
||||
Reference in New Issue
Block a user