Files
Momento/memento-note/lib/billing/credit-packs.ts
Antigravity a77f3ffb6e
Some checks failed
CI / Lint, Unit Tests & Build (push) Failing after 1m15s
CI / Deploy production (on server) (push) Has been skipped
fix: batch priorités — thème, scroll, wizard, agents, slides, charts, packs
Anti-flash dark (color-scheme + applyDocumentTheme), scroll pages publiques,
prompts wizard/slides moins génériques, cron agents rattrapage nextRun null,
slides carnet via notebookId, graphiques à 2 crédits + hint, audit dashboard,
packs Stripe marqués non configurés sans price ID.
2026-07-16 20:48:55 +00:00

160 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,
// 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
}