"use server"; import { revalidatePath } from "next/cache"; import { auth } from "@/lib/auth"; import { prisma } from "@/lib/prisma"; import { rcon } from "@/lib/services/rcon"; import { sendCurrency } from "@/lib/services/send-currency"; /** Feedback returned to the client component via useActionState. */ export type RedeemState = { ok: boolean; message: string } | null; /** * Redeem a shop voucher for the SIGNED-IN user. Faithful to AtomCMS's * ShopVoucherController: * - the user id is re-read from the session (auth()), NEVER from FormData, * so a crafted form cannot redeem on another account; * - a code that is missing or expired is rejected; * - each voucher may be redeemed once per user (website_used_shop_vouchers); * - on success the reward `amount` is granted, the used-row is inserted, * use_count is incremented, and the voucher is expired once max_uses is hit. * * The reward is delivered through sendCurrency({ rcon, db: prisma }); the * voucher schema carries a single `amount`, granted as the website credits * wallet currency. */ export async function redeem( _prev: RedeemState, formData: FormData, ): Promise { const session = await auth(); if (!session?.user?.id) { return { ok: false, message: "You must be signed in to redeem a voucher." }; } const userId = Number(session.user.id); if (!Number.isFinite(userId)) { return { ok: false, message: "Your session is invalid. Please sign in again.", }; } const code = String(formData.get("code") ?? "") .normalize("NFC") .trim(); if (!code) { return { ok: false, message: "Please enter a voucher code." }; } // Look up the code (website_shop_vouchers.code is unique). let voucher: { id: bigint; amount: number; maxUses: number; useCount: number; expiresAt: Date | null; } | null; try { voucher = await prisma.websiteShopVouchers.findUnique({ where: { code }, select: { id: true, amount: true, maxUses: true, useCount: true, expiresAt: true, }, }); } catch { return { ok: false, message: "We couldn't reach the server. Please try again.", }; } // Not found OR already expired -> generic "no active voucher" (matches AtomCMS). if ( !voucher || (voucher.expiresAt && voucher.expiresAt.getTime() <= Date.now()) ) { return { ok: false, message: "No active voucher with the given code was found.", }; } // One redemption per user. try { const already = await prisma.websiteUsedShopVouchers.findFirst({ where: { userId, voucherId: voucher.id }, select: { id: true }, }); if (already) { return { ok: false, message: "You can only use each shop voucher once." }; } } catch { return { ok: false, message: "We couldn't reach the server. Please try again.", }; } // Record the redemption first so a successful grant can never be double-claimed. try { await prisma.websiteUsedShopVouchers.create({ data: { userId, voucherId: voucher.id }, }); } catch { // Most likely a race (another tab redeemed it) — treat as already used. return { ok: false, message: "You can only use each shop voucher once." }; } // Grant the reward. The voucher carries a single amount, delivered as credits. try { await sendCurrency({ rcon, db: prisma }, userId, "credits", voucher.amount); } catch { // sendCurrency already falls back to a direct DB write; if it still throws, // the used-row stands and the balance simply wasn't credited — surface that. return { ok: false, message: "Your voucher was accepted but the reward could not be delivered. Contact staff.", }; } // Bump use_count and expire the voucher once the cap is reached. try { const updated = await prisma.websiteShopVouchers.update({ where: { id: voucher.id }, data: { useCount: { increment: 1 } }, select: { maxUses: true, useCount: true }, }); if (updated.maxUses && updated.useCount >= updated.maxUses) { await prisma.websiteShopVouchers.update({ where: { id: voucher.id }, data: { expiresAt: new Date() }, }); } } catch { // Reward already delivered; the counter bump is best-effort. } revalidatePath("/redeem"); return { ok: true, message: `Success! Your balance has been increased by ${voucher.amount.toLocaleString()} credits.`, }; }