import { prisma } from './db.js'; import { formatFiat, paymentsProvider } from './shopCatalog.js'; import { getPurchasableOsuPack } from './shopOsu.js'; /** * Create a pending osu top-up order. * Mock provider completes client-side via /confirm. * Stripe provider (when configured) would return a clientSecret. */ export async function createOsuCheckout(userId, packId) { const pack = await getPurchasableOsuPack(packId); if (!pack) { const err = new Error('Unknown or disabled osu pack'); err.status = 400; throw err; } const provider = paymentsProvider(); const order = await prisma.paymentOrder.create({ data: { userId: Number(userId), packId: pack.id, osuAmount: pack.osu, priceCents: pack.priceCents, currency: pack.currency, status: 'pending', provider, meta: JSON.stringify({ packName: pack.name, displayPrice: formatFiat(pack.priceCents, pack.currency), }), }, }); if (provider === 'stripe') { // Placeholder for Stripe PaymentIntent wiring. // Set PAYMENTS_PROVIDER=stripe + STRIPE_SECRET_KEY when ready. const err = new Error( 'Stripe checkout is configured but not wired yet — set PAYMENTS_PROVIDER=mock for local buys' ); err.status = 501; throw err; } return { order: serializeOrder(order), pack: { id: pack.id, name: pack.name, osu: pack.osu, priceCents: pack.priceCents, currency: pack.currency, displayPrice: formatFiat(pack.priceCents, pack.currency), }, mode: 'mock', /** Client calls POST /api/shop/osu/confirm with this id in mock mode */ confirmable: true, }; } /** * Complete a mock checkout and credit osu (idempotent). */ export async function confirmMockOsuPayment(userId, orderId) { const id = String(orderId || ''); if (!id) { const err = new Error('Missing order id'); err.status = 400; throw err; } return prisma.$transaction(async (tx) => { const order = await tx.paymentOrder.findUnique({ where: { id } }); if (!order || order.userId !== Number(userId)) { const err = new Error('Order not found'); err.status = 404; throw err; } if (order.status === 'paid') { const user = await tx.user.findUnique({ where: { id: userId } }); return { order: serializeOrder(order), user, alreadyPaid: true }; } if (order.status !== 'pending') { const err = new Error(`Order is ${order.status}`); err.status = 400; throw err; } if (order.provider !== 'mock') { const err = new Error('This order must be completed by the payment provider'); err.status = 400; throw err; } const now = new Date(); const updatedOrder = await tx.paymentOrder.update({ where: { id: order.id }, data: { status: 'paid', paidAt: now }, }); const user = await tx.user.update({ where: { id: userId }, data: { osuBalance: { increment: order.osuAmount } }, }); await tx.transaction.create({ data: { userId, type: 'osu_purchase', amount: String(order.osuAmount), meta: JSON.stringify({ orderId: order.id, packId: order.packId, priceCents: order.priceCents, currency: order.currency, provider: order.provider, }), }, }); return { order: serializeOrder(updatedOrder), user, alreadyPaid: false }; }); } export function serializeOrder(order) { return { id: order.id, packId: order.packId, osuAmount: order.osuAmount, priceCents: order.priceCents, currency: order.currency, status: order.status, provider: order.provider, displayPrice: formatFiat(order.priceCents, order.currency), createdAt: order.createdAt, paidAt: order.paidAt || null, }; }