/** * Packs de crédits IA (achat one-shot Stripe, mode payment). * Les crédits achetés s’ajoutent à `purchasedBalance` (reportables entre mois). */ import { getConfigValue } from '@/lib/config' import { stripe } from '@/lib/stripe' export type CreditPackId = 'S' | 'M' | 'L' export type CreditPackDef = { id: CreditPackId credits: number /** Prix d’affichage par défaut (si Stripe non configuré) */ defaultDisplay: string defaultAmount: number currency: string priceConfigKey: string } /** Catalogue fixe — crédits par pack (option M). */ export const CREDIT_PACKS: Record = { S: { id: 'S', credits: 100, defaultDisplay: '4,90 €', defaultAmount: 4.9, currency: 'EUR', priceConfigKey: 'STRIPE_PRICE_CREDITS_S', }, M: { id: 'M', credits: 500, defaultDisplay: '19,90 €', defaultAmount: 19.9, currency: 'EUR', priceConfigKey: 'STRIPE_PRICE_CREDITS_M', }, L: { id: 'L', credits: 2000, defaultDisplay: '49,90 €', defaultAmount: 49.9, currency: 'EUR', priceConfigKey: 'STRIPE_PRICE_CREDITS_L', }, } export const CREDIT_PACK_IDS = Object.keys(CREDIT_PACKS) as CreditPackId[] export function isCreditPackId(value: string): value is CreditPackId { return value === 'S' || value === 'M' || value === 'L' } export function getCreditPack(id: CreditPackId): CreditPackDef { return CREDIT_PACKS[id] } export async function resolvePackPriceId(packId: CreditPackId): Promise { const pack = CREDIT_PACKS[packId] const fromDb = await getConfigValue(pack.priceConfigKey, '') const priceId = fromDb || process.env[pack.priceConfigKey] || '' if (priceId) return priceId const isMock = !process.env.STRIPE_SECRET_KEY || process.env.STRIPE_SECRET_KEY === 'sk_test_placeholder' if (isMock && process.env.NODE_ENV !== 'test') { return `price_mock_credits_${packId.toLowerCase()}` } throw new Error(`No Stripe price ID configured for credit pack ${packId}`) } export type PackPublicPrice = { id: CreditPackId credits: number display: string amount: number currency: string configured: boolean } function formatMoney(amount: number, currency: string): string { const cur = currency.toUpperCase() if (cur === 'EUR') { return `${amount.toLocaleString('fr-FR', { minimumFractionDigits: 0, maximumFractionDigits: 2 })} €` } if (cur === 'USD') { return `$${amount.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 2 })}` } if (cur === 'GBP') { return `£${amount.toLocaleString('en-GB', { minimumFractionDigits: 0, maximumFractionDigits: 2 })}` } return `${amount} ${cur}` } export async function getPackPublicPrices(): Promise { const isMock = !process.env.STRIPE_SECRET_KEY || process.env.STRIPE_SECRET_KEY === 'sk_test_placeholder' return Promise.all( CREDIT_PACK_IDS.map(async (id) => { const pack = CREDIT_PACKS[id] let display = pack.defaultDisplay let amount = pack.defaultAmount let currency = pack.currency let configured = false try { const priceId = await resolvePackPriceId(id) if (priceId && !priceId.startsWith('price_mock_')) { configured = true if (!isMock) { const price = await stripe.prices.retrieve(priceId) if (price.unit_amount != null) { amount = price.unit_amount / 100 currency = price.currency.toUpperCase() display = formatMoney(amount, currency) } } } } catch { /* pack non configuré — on garde le prix catalogue */ } return { id, credits: pack.credits, display, amount, currency, // true seulement si un vrai price_… est en admin/env (pas les mocks) configured, } }), ) } /** Retrouve packId + crédits depuis un price ID Stripe (webhook). */ export async function resolvePackFromPriceId( priceId: string, ): Promise<{ packId: CreditPackId; credits: number } | null> { if (!priceId) return null if (priceId.startsWith('price_mock_credits_')) { const suffix = priceId.replace('price_mock_credits_', '').toUpperCase() if (isCreditPackId(suffix)) { return { packId: suffix, credits: CREDIT_PACKS[suffix].credits } } } for (const id of CREDIT_PACK_IDS) { const pack = CREDIT_PACKS[id] const fromDb = await getConfigValue(pack.priceConfigKey, '') const configured = fromDb || process.env[pack.priceConfigKey] || '' if (configured && configured === priceId) { return { packId: id, credits: pack.credits } } } return null }