import { creditTransactions, eq, sql, wallets, type Tx } from "@spinza/database"; import type { TransactionType } from "@spinza/shared"; import { errors } from "../lib/errors"; export interface LockedWallet { userId: string; balance: number; lifetimeWagered: number; lifetimeWon: number; lifetimeGranted: number; } /** SELECT ... FOR UPDATE on the wallet row. Every balance change must start here. */ export async function lockWallet(tx: Tx, userId: string): Promise { const rows = await tx.execute(sql`select user_id, balance, lifetime_wagered, lifetime_won, lifetime_granted from wallets where user_id = ${userId} for update`); const r = (rows.rows as Record[])[0]; if (!r) throw errors.notFound("Wallet not found"); return { userId, balance: Number(r.balance), lifetimeWagered: Number(r.lifetime_wagered), lifetimeWon: Number(r.lifetime_won), lifetimeGranted: Number(r.lifetime_granted), }; } /** * Apply a signed amount to a locked wallet and write the immutable ledger row. * Never call without holding the wallet lock (see lockWallet). */ export async function applyCredit( tx: Tx, wallet: LockedWallet, type: TransactionType, amount: number, reference: string | null, meta?: Record, ): Promise { if (!Number.isInteger(amount)) throw new Error("credit amounts must be integers"); const next = wallet.balance + amount; if (next < 0) throw errors.insufficient(wallet.balance, -amount); wallet.balance = next; if (type === "BET") wallet.lifetimeWagered += -amount; else if (type === "WIN") wallet.lifetimeWon += amount; else if (amount > 0) wallet.lifetimeGranted += amount; await tx.insert(creditTransactions).values({ userId: wallet.userId, type, amount, balanceAfter: next, reference, meta: meta ?? null }); return next; } /** Persist the wallet aggregate after one or more applyCredit calls. */ export async function saveWallet(tx: Tx, wallet: LockedWallet): Promise { await tx .update(wallets) .set({ balance: wallet.balance, lifetimeWagered: wallet.lifetimeWagered, lifetimeWon: wallet.lifetimeWon, lifetimeGranted: wallet.lifetimeGranted, updatedAt: new Date() }) .where(eq(wallets.userId, wallet.userId)); } /** Convenience: single grant in its own transaction. */ export async function grant(tx: Tx, userId: string, type: TransactionType, amount: number, reference: string | null, meta?: Record): Promise { const w = await lockWallet(tx, userId); const b = await applyCredit(tx, w, type, amount, reference, meta); await saveWallet(tx, w); return b; }