fix(quotas): unifier le décompte IA (BYOK, rollback) et combler les fuites
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 6m55s
CI / Deploy production (on server) (push) Successful in 36s

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:
Antigravity
2026-07-15 20:42:25 +00:00
parent 30da592ba2
commit 4fe31ebc99
75 changed files with 2949 additions and 785 deletions

View File

@@ -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>

View File

@@ -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>
) : (

View File

@@ -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>