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.
This commit is contained in:
Antigravity
2026-07-16 20:43:18 +00:00
parent 704fed1191
commit 556a0b2f3f
77 changed files with 35745 additions and 10430 deletions

View File

@@ -42,24 +42,47 @@ export async function getSystemConfig() {
return getCachedConfig()
}
/** Keys that may be cleared (empty string) to fall back to chat / defaults. */
const CLEARABLE_CONFIG_KEYS = new Set([
'AI_PROVIDER_SLIDES',
'AI_MODEL_SLIDES',
])
export async function updateSystemConfig(data: Record<string, string>) {
await checkAdmin()
try {
// Filter out empty values but keep 'false' as valid
const filteredData = Object.fromEntries(
Object.entries(data).filter(([key, value]) => value !== '' && value !== null && value !== undefined)
)
// Filter out empty values but keep 'false' as valid.
// Slides provider/model may be empty intentionally (= use Chat fallback).
const toUpsert: Record<string, string> = {}
const toDelete: string[] = []
const operations = Object.entries(filteredData).map(([key, value]) =>
prisma.systemConfig.upsert({
where: { key },
update: { value },
create: { key, value }
})
)
for (const [key, value] of Object.entries(data)) {
if (value === null || value === undefined) continue
if (value === '' && CLEARABLE_CONFIG_KEYS.has(key)) {
toDelete.push(key)
continue
}
if (value === '') continue
toUpsert[key] = value
}
await prisma.$transaction(operations)
const operations = [
...Object.entries(toUpsert).map(([key, value]) =>
prisma.systemConfig.upsert({
where: { key },
update: { value },
create: { key, value },
}),
),
...toDelete.map((key) =>
prisma.systemConfig.deleteMany({ where: { key } }),
),
]
if (operations.length > 0) {
await prisma.$transaction(operations)
}
return { success: true }
} catch (error) {