Files
Momento/memento-note/lib/billing/credit-packs.ts
Antigravity 556a0b2f3f feat(credits): solde IA unique (option M), packs Stripe et polish billing
Un pot de crédits global (BASIC 100 / PRO 1 000 / BUSINESS 4 000) remplace
les seaux par fonction côté débit. Packs one-shot S/M/L, admin sans seaux
legacy, usage multi-features, hints de coût, anti-flash thème et hydratation
admin. Inclut aussi le pipeline slides renforcé et la page publique polishée.
2026-07-16 20:43:18 +00:00

159 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Packs de crédits IA (achat one-shot Stripe, mode payment).
* Les crédits achetés sajoutent à `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 daffichage 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<CreditPackId, CreditPackDef> = {
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<string> {
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<PackPublicPrice[]> {
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,
configured: configured || isMock,
}
}),
)
}
/** 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
}