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.
136 lines
4.0 KiB
TypeScript
136 lines
4.0 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server'
|
|
import { auth } from '@/auth'
|
|
import { stripe } from '@/lib/stripe'
|
|
import { isBillingEnabled } from '@/lib/billing/stripe-prices'
|
|
import {
|
|
isCreditPackId,
|
|
resolvePackPriceId,
|
|
getCreditPack,
|
|
} from '@/lib/billing/credit-packs'
|
|
import { prisma } from '@/lib/prisma'
|
|
import { z } from 'zod'
|
|
|
|
const bodySchema = z.object({
|
|
packId: z.enum(['S', 'M', 'L']),
|
|
})
|
|
|
|
export async function POST(req: NextRequest) {
|
|
const session = await auth()
|
|
if (!session?.user?.id || !session.user.email) {
|
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
}
|
|
|
|
if (!(await isBillingEnabled())) {
|
|
return NextResponse.json({ error: 'Billing is not enabled' }, { status: 403 })
|
|
}
|
|
|
|
const secret = process.env.STRIPE_SECRET_KEY
|
|
if (!secret || secret === 'sk_test_placeholder') {
|
|
return NextResponse.json(
|
|
{ error: 'Stripe is not configured (STRIPE_SECRET_KEY missing)' },
|
|
{ status: 503 },
|
|
)
|
|
}
|
|
|
|
const parsed = bodySchema.safeParse(await req.json())
|
|
if (!parsed.success || !isCreditPackId(parsed.data.packId)) {
|
|
return NextResponse.json({ error: 'Invalid pack' }, { status: 400 })
|
|
}
|
|
|
|
const packId = parsed.data.packId
|
|
const pack = getCreditPack(packId)
|
|
const userId = session.user.id
|
|
const userEmail = session.user.email
|
|
|
|
try {
|
|
let priceId: string
|
|
try {
|
|
priceId = await resolvePackPriceId(packId)
|
|
} catch (e) {
|
|
console.error('[billing/create-pack-checkout] price resolve failed:', e)
|
|
return NextResponse.json(
|
|
{ error: 'Credit pack price IDs not configured. Set them in Admin > Billing.' },
|
|
{ status: 503 },
|
|
)
|
|
}
|
|
|
|
if (priceId.startsWith('price_mock_')) {
|
|
return NextResponse.json(
|
|
{ error: 'Credit pack price IDs not configured. Set them in Admin > Billing.' },
|
|
{ status: 503 },
|
|
)
|
|
}
|
|
|
|
const subscription = await prisma.subscription.findUnique({ where: { userId } })
|
|
let customerId = subscription?.stripeCustomerId ?? undefined
|
|
|
|
if (customerId && customerId.startsWith('cus_mock')) {
|
|
customerId = undefined
|
|
}
|
|
|
|
if (!customerId) {
|
|
const customer = await stripe.customers.create({
|
|
email: userEmail,
|
|
metadata: { userId },
|
|
})
|
|
customerId = customer.id
|
|
|
|
await prisma.subscription.upsert({
|
|
where: { userId },
|
|
update: { stripeCustomerId: customerId },
|
|
create: {
|
|
userId,
|
|
stripeCustomerId: customerId,
|
|
tier: 'BASIC',
|
|
status: 'ACTIVE',
|
|
currentPeriodStart: new Date(),
|
|
currentPeriodEnd: new Date(Date.now() + 30 * 24 * 3600 * 1000),
|
|
},
|
|
})
|
|
}
|
|
|
|
const host = req.headers.get('x-forwarded-host') ?? req.headers.get('host') ?? 'localhost:3000'
|
|
const proto = req.headers.get('x-forwarded-proto') ?? 'http'
|
|
const origin = `${proto}://${host}`
|
|
|
|
const checkoutSession = await stripe.checkout.sessions.create({
|
|
customer: customerId,
|
|
mode: 'payment',
|
|
line_items: [{ price: priceId, quantity: 1 }],
|
|
success_url: `${origin}/settings/billing?session_id={CHECKOUT_SESSION_ID}&pack=1`,
|
|
cancel_url: `${origin}/settings/billing?canceled=1`,
|
|
metadata: {
|
|
userId,
|
|
type: 'credit_pack',
|
|
packId,
|
|
credits: String(pack.credits),
|
|
},
|
|
payment_intent_data: {
|
|
metadata: {
|
|
userId,
|
|
type: 'credit_pack',
|
|
packId,
|
|
credits: String(pack.credits),
|
|
},
|
|
},
|
|
customer_update: { address: 'auto' },
|
|
allow_promotion_codes: true,
|
|
})
|
|
|
|
if (!checkoutSession.url) {
|
|
return NextResponse.json({ error: 'Checkout session has no URL' }, { status: 500 })
|
|
}
|
|
|
|
return NextResponse.json({
|
|
url: checkoutSession.url,
|
|
sessionId: checkoutSession.id,
|
|
packId,
|
|
credits: pack.credits,
|
|
})
|
|
} catch (error) {
|
|
console.error('[billing/create-pack-checkout]', error)
|
|
const msg = error instanceof Error ? error.message : 'Failed to create pack checkout'
|
|
return NextResponse.json({ error: msg }, { status: 500 })
|
|
}
|
|
}
|