fix(quotas): unifier le décompte IA (BYOK, rollback) et combler les fuites
Centralise la réserve via ai-quota, corrige admin unavailable (-1), brancher les routes sans quota et le host-pays brainstorm, avec usage-meter élargi, noms de clusters, MCP et ajustements dashboard/insights. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -112,7 +112,7 @@ export function BillingHistory() {
|
||||
|
||||
{error && (
|
||||
<p className="text-xs text-rose-500 text-center py-4">
|
||||
Impossible de charger l'historique de facturation.
|
||||
{t('billing.fetchInvoicesFailed')}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -152,7 +152,7 @@ export function BillingHistory() {
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex items-center justify-center p-2 rounded-lg text-concrete hover:text-brand-accent hover:bg-brand-accent/10 transition-all"
|
||||
title="Download PDF"
|
||||
title={t('billing.downloadPdf')}
|
||||
>
|
||||
<Download size={14} />
|
||||
</a>
|
||||
|
||||
@@ -62,7 +62,7 @@ export function BillingPlans() {
|
||||
queryFn: async () => {
|
||||
const search = typeof window !== 'undefined' ? window.location.search : '';
|
||||
const res = await fetch(`/api/billing/status${search}`);
|
||||
if (!res.ok) throw new Error('Failed to fetch billing status');
|
||||
if (!res.ok) throw new Error(t('billing.fetchStatusFailed'));
|
||||
return res.json();
|
||||
},
|
||||
});
|
||||
@@ -71,7 +71,7 @@ export function BillingPlans() {
|
||||
queryKey: ['usage', 'current'],
|
||||
queryFn: async () => {
|
||||
const res = await fetch('/api/usage/current');
|
||||
if (!res.ok) throw new Error('Failed to fetch quotas');
|
||||
if (!res.ok) throw new Error(t('billing.fetchQuotasFailed'));
|
||||
return res.json();
|
||||
},
|
||||
refetchInterval: 30000,
|
||||
@@ -122,7 +122,7 @@ export function BillingPlans() {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[BillingPlans] checkout error:', err);
|
||||
toast.error('Failed to start checkout. Please try again.');
|
||||
toast.error(t('billing.checkoutFailed'));
|
||||
} finally {
|
||||
setCheckoutLoading(null);
|
||||
}
|
||||
@@ -139,20 +139,20 @@ export function BillingPlans() {
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
toast.error(data.error || 'Failed to open billing portal.');
|
||||
toast.error(data.error || t('billing.portalFailed'));
|
||||
return;
|
||||
}
|
||||
window.location.href = data.url;
|
||||
} catch (err) {
|
||||
console.error('[BillingPlans] portal error:', err);
|
||||
toast.error('Failed to open billing portal.');
|
||||
toast.error(t('billing.portalFailed'));
|
||||
} finally {
|
||||
setPortalLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelSubscription = async () => {
|
||||
const confirmMsg = t('billing.cancelConfirm') || "Êtes-vous sûr de vouloir résilier votre abonnement ? Vous conserverez vos accès Pro/Business jusqu'à la fin de la période en cours.";
|
||||
const confirmMsg = t('billing.cancelConfirm');
|
||||
if (!window.confirm(confirmMsg)) {
|
||||
return;
|
||||
}
|
||||
@@ -165,15 +165,15 @@ export function BillingPlans() {
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
toast.error(data.error || 'Failed to cancel subscription.');
|
||||
toast.error(data.error || t('billing.cancelFailed'));
|
||||
return;
|
||||
}
|
||||
toast.success(t('billing.cancelSuccess') || "Votre abonnement a été résilié avec succès. Il prendra fin à la fin de la période de facturation en cours.");
|
||||
toast.success(t('billing.cancelSuccess'));
|
||||
queryClient.invalidateQueries({ queryKey: ['billing', 'status'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['usage', 'current'] });
|
||||
} catch (err) {
|
||||
console.error('[BillingPlans] cancel error:', err);
|
||||
toast.error('Failed to cancel subscription.');
|
||||
toast.error(t('billing.cancelFailed'));
|
||||
} finally {
|
||||
setCancelLoading(false);
|
||||
}
|
||||
@@ -229,7 +229,7 @@ export function BillingPlans() {
|
||||
name: t('billing.proPlan'),
|
||||
price: status?.prices?.PRO?.[interval]?.display ??
|
||||
(interval === 'month' ? (t('billing.proPrice') || '9,90€') : (t('billing.proAnnualPrice') || '99€')),
|
||||
period: interval === 'month' ? '/mois' : '/an',
|
||||
period: interval === 'month' ? t('billing.perMonth') : t('billing.perYear'),
|
||||
description: t('billing.proDescription') || 'Pour les consultants et créateurs exigeants.',
|
||||
features: [
|
||||
t('billing.proFeature1') || 'Notes illimitées',
|
||||
@@ -252,8 +252,7 @@ export function BillingPlans() {
|
||||
name: t('billing.businessPlan'),
|
||||
price: status?.prices?.BUSINESS?.[interval]?.display ??
|
||||
(interval === 'month' ? (t('billing.businessPrice') || '29,90€') : (t('billing.businessAnnualPrice') || '299€')),
|
||||
period: interval === 'month' ? '/mois' : '/an',
|
||||
description: t('billing.businessDescription') || 'Pour les équipes et chefs de produit.',
|
||||
period: interval === 'month' ? t('billing.perMonth') : t('billing.perYear'),
|
||||
features: [
|
||||
t('billing.businessFeature1') || '10 Collaborateurs inclus',
|
||||
t('billing.businessFeature2') || 'BYOK (13 fournisseurs)',
|
||||
@@ -560,7 +559,7 @@ export function BillingPlans() {
|
||||
)}
|
||||
>
|
||||
{t('billing.annual')}
|
||||
<span className="ms-1 text-primary/80 dark:text-primary">{t('billing.save')} ~17%</span>
|
||||
<span className="ms-1 text-primary/80 dark:text-primary">{t('billing.savePercent')}</span>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -103,7 +103,7 @@ export function InlinePaywall({ feature, onDismiss }: InlinePaywallProps) {
|
||||
</button>
|
||||
|
||||
<div className="sm:ml-auto flex items-center justify-center gap-1.5 text-[10px] text-rose-500/60 dark:text-rose-400/40 font-mono">
|
||||
<span>Fermeture dans {timeLeft}s</span>
|
||||
<span>{t('quotaPaywall.closingIn').replace('{n}', String(timeLeft))}</span>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
Reference in New Issue
Block a user