import { Injectable, BadRequestException } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { Decimal } from '@prisma/client/runtime/library'; import { generateTransactionId } from '../common/decorators'; @Injectable() export class WalletService { constructor(private prisma: PrismaService) {} async getWallet(userId: bigint) { const wallet = await this.prisma.wallet.findUnique({ where: { userId } }); if (!wallet) throw new BadRequestException('Wallet not found'); return wallet; } async createWallet(userId: bigint, currency = 'USD') { return this.prisma.wallet.create({ data: { userId, currency }, }); } private async lockWallet(tx: Parameters[0]>[0], userId: bigint) { const wallets = await tx.$queryRaw>` SELECT id, available_balance, frozen_balance, version FROM wallets WHERE user_id = ${userId} FOR UPDATE `; if (!wallets.length) throw new BadRequestException('Wallet not found'); return wallets[0]; } async deposit( userId: bigint, amount: Decimal | number, operatorId: bigint, remark?: string, referenceId?: string, ) { const amt = new Decimal(amount); if (amt.lte(0)) throw new BadRequestException('Amount must be positive'); return this.prisma.$transaction(async (tx) => { const w = await this.lockWallet(tx, userId); const balanceBefore = new Decimal(w.available_balance); const balanceAfter = balanceBefore.add(amt); await tx.wallet.update({ where: { id: w.id }, data: { availableBalance: balanceAfter, version: { increment: 1 }, }, }); await tx.walletTransaction.create({ data: { transactionId: generateTransactionId(), userId, walletId: w.id, transactionType: 'MANUAL_DEPOSIT', amount: amt, balanceBefore, balanceAfter, frozenBefore: w.frozen_balance, frozenAfter: w.frozen_balance, referenceType: 'DEPOSIT', referenceId, operatorId, remark, }, }); return { balanceAfter }; }); } async withdraw( userId: bigint, amount: Decimal | number, operatorId: bigint, remark?: string, referenceId?: string, ) { const amt = new Decimal(amount); if (amt.lte(0)) throw new BadRequestException('Amount must be positive'); return this.prisma.$transaction(async (tx) => { const w = await this.lockWallet(tx, userId); const balanceBefore = new Decimal(w.available_balance); if (balanceBefore.lt(amt)) throw new BadRequestException('Insufficient balance'); const balanceAfter = balanceBefore.sub(amt); await tx.wallet.update({ where: { id: w.id }, data: { availableBalance: balanceAfter, version: { increment: 1 }, }, }); await tx.walletTransaction.create({ data: { transactionId: generateTransactionId(), userId, walletId: w.id, transactionType: 'MANUAL_WITHDRAW', amount: amt.neg(), balanceBefore, balanceAfter, frozenBefore: w.frozen_balance, frozenAfter: w.frozen_balance, referenceType: 'WITHDRAW', referenceId, operatorId, remark, }, }); return { balanceAfter }; }); } async freezeForBet(userId: bigint, stake: Decimal | number, betId: string) { const amt = new Decimal(stake); return this.prisma.$transaction(async (tx) => { const w = await this.lockWallet(tx, userId); const avail = new Decimal(w.available_balance); if (avail.lt(amt)) throw new BadRequestException('Insufficient balance'); const balanceAfter = avail.sub(amt); const frozenAfter = new Decimal(w.frozen_balance).add(amt); await tx.wallet.update({ where: { id: w.id }, data: { availableBalance: balanceAfter, frozenBalance: frozenAfter, version: { increment: 1 }, }, }); await tx.walletTransaction.create({ data: { transactionId: generateTransactionId(), userId, walletId: w.id, transactionType: 'BET_FREEZE', amount: amt.neg(), balanceBefore: avail, balanceAfter, frozenBefore: w.frozen_balance, frozenAfter, referenceType: 'BET', referenceId: betId, }, }); }); } async settleBet( userId: bigint, stake: Decimal, payout: Decimal, betId: string, result: 'WIN' | 'LOSE' | 'PUSH' | 'VOID' | 'HALF_WIN' | 'HALF_LOSE', ) { const txTypeMap: Record = { WIN: 'BET_SETTLE_WIN', LOSE: 'BET_SETTLE_LOSE', PUSH: 'BET_SETTLE_PUSH', VOID: 'BET_VOID_REFUND', HALF_WIN: 'BET_SETTLE_WIN', HALF_LOSE: 'BET_SETTLE_LOSE', }; return this.prisma.$transaction(async (tx) => { const w = await this.lockWallet(tx, userId); const avail = new Decimal(w.available_balance); const frozen = new Decimal(w.frozen_balance); const frozenAfter = frozen.sub(stake); const balanceAfter = avail.add(payout); await tx.wallet.update({ where: { id: w.id }, data: { availableBalance: balanceAfter, frozenBalance: frozenAfter.lt(0) ? new Decimal(0) : frozenAfter, version: { increment: 1 }, }, }); await tx.walletTransaction.create({ data: { transactionId: generateTransactionId(), userId, walletId: w.id, transactionType: txTypeMap[result] || 'BET_SETTLE_WIN', amount: payout, balanceBefore: avail, balanceAfter, frozenBefore: frozen, frozenAfter: frozenAfter.lt(0) ? new Decimal(0) : frozenAfter, referenceType: 'BET', referenceId: betId, }, }); }); } async getTransactions(userId: bigint, page = 1, pageSize = 20) { const skip = (page - 1) * pageSize; const [items, total] = await Promise.all([ this.prisma.walletTransaction.findMany({ where: { userId }, orderBy: { createdAt: 'desc' }, skip, take: pageSize, }), this.prisma.walletTransaction.count({ where: { userId } }), ]); return { items, total, page, pageSize }; } }