139 lines
3.8 KiB
JavaScript
139 lines
3.8 KiB
JavaScript
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,
|
|
};
|
|
}
|