import { eq, desc } from "drizzle-orm"; import { db } from "@/lib/db/client"; import { wallets, coinTransactions } from "@/lib/db/schema"; export type CoinReason = "signup_bonus" | "manual_save" | "purchase"; export interface WalletDTO { userId: string; balance: number; } export interface CoinTransactionDTO { id: string; amount: number; reason: CoinReason; relatedGameId: string | null; createdAt: number; } export async function getOrCreateWallet(userId: string): Promise { const existing = db.select().from(wallets).where(eq(wallets.userId, userId)).get(); if (existing) return existing; db.insert(wallets).values({ userId, balance: 0 }).onConflictDoNothing().run(); const created = db.select().from(wallets).where(eq(wallets.userId, userId)).get(); return created!; } export async function getBalance(userId: string): Promise { const wallet = await getOrCreateWallet(userId); return wallet.balance; } export async function listRecentTransactions(userId: string, limit = 20): Promise { const rows = await db.query.coinTransactions.findMany({ where: eq(coinTransactions.userId, userId), orderBy: desc(coinTransactions.createdAt), limit, }); return rows.map((row) => ({ id: row.id, amount: row.amount, reason: row.reason, relatedGameId: row.relatedGameId, createdAt: row.createdAt.getTime(), })); } /** * Credits coins and logs the ledger entry atomically. The better-sqlite3 * driver's db.transaction() callback must stay fully synchronous — no * await inside — or the transaction commits before the awaited work * finishes; every query here uses the sync .get()/.run() methods. */ export async function grantCoins( userId: string, amount: number, reason: CoinReason, relatedGameId?: string, ): Promise { await getOrCreateWallet(userId); db.transaction((tx) => { const wallet = tx.select().from(wallets).where(eq(wallets.userId, userId)).get(); const balance = wallet?.balance ?? 0; tx.update(wallets) .set({ balance: balance + amount, updatedAt: new Date() }) .where(eq(wallets.userId, userId)) .run(); tx.insert(coinTransactions) .values({ userId, amount, reason, relatedGameId: relatedGameId ?? null }) .run(); }); } /** * Debits coins and logs the ledger entry atomically — returns false without * writing anything if the balance is insufficient. This is the one place * correctness really matters: the balance check and the debit happen inside * the same transaction, so two concurrent spends can't both pass the check * against a stale balance. */ export async function spendCoins( userId: string, amount: number, reason: CoinReason, relatedGameId?: string, ): Promise { await getOrCreateWallet(userId); return db.transaction((tx) => { const wallet = tx.select().from(wallets).where(eq(wallets.userId, userId)).get(); const balance = wallet?.balance ?? 0; if (balance < amount) return false; tx.update(wallets) .set({ balance: balance - amount, updatedAt: new Date() }) .where(eq(wallets.userId, userId)) .run(); tx.insert(coinTransactions) .values({ userId, amount: -amount, reason, relatedGameId: relatedGameId ?? null }) .run(); return true; }); }