fix(ui): tarifs publics unifiés et prix annuel lisible
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:
47
memento-note/lib/billing/price-catalog.ts
Normal file
47
memento-note/lib/billing/price-catalog.ts
Normal 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)
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user