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.
159 lines
4.6 KiB
TypeScript
159 lines
4.6 KiB
TypeScript
/**
|
||
* 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<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
|
||
}
|