fix(ui): tarifs publics unifiés et prix annuel lisible
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 7m5s
CI / Deploy production (on server) (push) Successful in 24s

La page tarifs reprend la barre du site public. En annuel, Pro affiche
8,25 € par mois (99 € l’année), plus le 99 € comme gros chiffre.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Antigravity
2026-09-01 17:43:05 +00:00
parent dcd7f38c11
commit 4fc5e3ce04
21 changed files with 721 additions and 602 deletions

View File

@@ -0,0 +1,47 @@
export type BillingTier = 'PRO' | 'BUSINESS'
export type BillingInterval = 'month' | 'year'
export interface DynamicPrice {
display: string
amount: number
currency: string
}
export const DEFAULT_PRICES: Record<BillingTier, Record<BillingInterval, DynamicPrice>> = {
PRO: {
month: { display: '9,90 €', amount: 9.9, currency: 'EUR' },
year: { display: '99,00 €', amount: 99, currency: 'EUR' },
},
BUSINESS: {
month: { display: '29,90 €', amount: 29.9, currency: 'EUR' },
year: { display: '299,00 €', amount: 299, currency: 'EUR' },
},
}
export function formatBillingAmount(amount: number, currency = 'EUR'): string {
const c = currency.toUpperCase()
if (c === 'EUR') {
return `${amount.toLocaleString('fr-FR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} €`
}
if (c === 'USD') {
return `$${amount.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
}
if (c === 'GBP') {
return `£${amount.toLocaleString('en-GB', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
}
return `${amount.toLocaleString('fr-FR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ${c}`
}
/** Prix annuel ramené au mois (99 € / an → 8,25 € / mois). */
export function yearToMonthlyEquivalent(yearAmount: number): number {
return Math.round((yearAmount / 12) * 100) / 100
}
/** Remise réelle : 9,90 € × 12 vs 99 € / an → 17 %. */
export function annualDiscountPercent(monthlyAmount: number, yearlyAmount: number): number {
const paidMonthlyForYear = monthlyAmount * 12
if (paidMonthlyForYear <= 0) return 0
const raw = (1 - yearlyAmount / paidMonthlyForYear) * 100
if (!Number.isFinite(raw) || raw <= 0) return 0
return Math.round(raw)
}

View File

@@ -1,26 +1,16 @@
import type { SubscriptionTier } from '@/lib/plan-entitlements';
import { stripe } from '@/lib/stripe';
import { getConfigValue } from '@/lib/config';
import {
DEFAULT_PRICES,
formatBillingAmount,
type BillingInterval,
type BillingTier,
type DynamicPrice,
} from '@/lib/billing/price-catalog';
export type BillingTier = 'PRO' | 'BUSINESS';
export type BillingInterval = 'month' | 'year';
export interface DynamicPrice {
display: string;
amount: number;
currency: string;
}
export const DEFAULT_PRICES: Record<BillingTier, Record<BillingInterval, DynamicPrice>> = {
PRO: {
month: { display: '9,90 €', amount: 9.90, currency: 'EUR' },
year: { display: '99,00 €', amount: 99.00, currency: 'EUR' },
},
BUSINESS: {
month: { display: '29,90 €', amount: 29.90, currency: 'EUR' },
year: { display: '299,00 €', amount: 299.00, currency: 'EUR' },
},
};
export type { BillingInterval, BillingTier, DynamicPrice };
export { DEFAULT_PRICES, formatBillingAmount };
export async function isBillingEnabled(): Promise<boolean> {
const flag = await getConfigValue('BILLING_ENABLED', '');
@@ -54,19 +44,11 @@ export async function getDynamicPrices(): Promise<Record<BillingTier, Record<Bil
if (price.unit_amount !== null && price.unit_amount !== undefined) {
const amount = price.unit_amount / 100;
const currency = price.currency.toUpperCase();
let display = '';
if (currency === 'EUR') {
display = `${amount.toLocaleString('fr-FR', { minimumFractionDigits: 0, maximumFractionDigits: 2 })} €`;
} else if (currency === 'USD') {
display = `$${amount.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 2 })}`;
} else if (currency === 'GBP') {
display = `£${amount.toLocaleString('en-GB', { minimumFractionDigits: 0, maximumFractionDigits: 2 })}`;
} else {
display = `${amount} ${currency}`;
}
result[tier][interval] = { display, amount, currency };
result[tier][interval] = {
display: formatBillingAmount(amount, currency),
amount,
currency,
};
}
} catch (err) {
console.error(`[stripe-prices] Failed to retrieve price for ${tier}/${interval}:`, err);