import type { TradeSession, TradeParticipant } from "@/modules/trade/trade.types"; import { economyService } from "@shared/modules/economy/economy.service"; import { inventoryService } from "@shared/modules/inventory/inventory.service"; import { itemTransactions } from "@db/schema"; import { withTransaction } from "@/lib/db"; import type { Transaction } from "@shared/lib/types"; import { TransactionType, ItemTransactionType } from "@shared/lib/constants"; // Module-level session storage const sessions = new Map(); /** * Unlocks both participants in a trade session */ const unlockAll = (session: TradeSession) => { session.userA.locked = false; session.userB.locked = false; }; /** * Processes a one-way transfer from one participant to another */ const processTransfer = async (tx: Transaction, from: TradeParticipant, to: TradeParticipant, threadId: string) => { // 1. Money if (from.offer.money > 0n) { await economyService.modifyUserBalance( from.id, -from.offer.money, TransactionType.TRADE_OUT, `Trade with ${to.username} (Thread: ${threadId})`, to.id, tx ); await economyService.modifyUserBalance( to.id, from.offer.money, TransactionType.TRADE_IN, `Trade with ${from.username} (Thread: ${threadId})`, from.id, tx ); } // 2. Items for (const item of from.offer.items) { // Remove from sender await inventoryService.removeItem(from.id, item.id, item.quantity, tx); // Add to receiver await inventoryService.addItem(to.id, item.id, item.quantity, tx); // Log Item Transaction (Sender) await tx.insert(itemTransactions).values({ userId: BigInt(from.id), relatedUserId: BigInt(to.id), itemId: item.id, quantity: -item.quantity, type: ItemTransactionType.TRADE_OUT, description: `Traded to ${to.username}`, }); // Log Item Transaction (Receiver) await tx.insert(itemTransactions).values({ userId: BigInt(to.id), relatedUserId: BigInt(from.id), itemId: item.id, quantity: item.quantity, type: ItemTransactionType.TRADE_IN, description: `Received from ${from.username}`, }); } }; export const tradeService = { // Expose for testing _sessions: sessions, /** * Creates a new trade session */ createSession: (threadId: string, userA: { id: string, username: string }, userB: { id: string, username: string }): TradeSession => { const session: TradeSession = { threadId, userA: { id: userA.id, username: userA.username, locked: false, offer: { money: 0n, items: [] } }, userB: { id: userB.id, username: userB.username, locked: false, offer: { money: 0n, items: [] } }, state: 'NEGOTIATING', lastInteraction: Date.now() }; sessions.set(threadId, session); return session; }, getSession: (threadId: string): TradeSession | undefined => { return sessions.get(threadId); }, endSession: (threadId: string) => { sessions.delete(threadId); }, /** * Updates an offer. If allowed, validation checks should be done BEFORE calling this. * unlocking logic is handled here (if offer changes, unlock both). */ updateMoney: (threadId: string, userId: string, amount: bigint) => { const session = tradeService.getSession(threadId); if (!session) throw new Error("Session not found"); if (session.state !== 'NEGOTIATING') throw new Error("Trade is not active"); const participant = session.userA.id === userId ? session.userA : session.userB.id === userId ? session.userB : null; if (!participant) throw new Error("User not in trade"); participant.offer.money = amount; unlockAll(session); session.lastInteraction = Date.now(); }, addItem: (threadId: string, userId: string, item: { id: number, name: string }, quantity: bigint) => { const session = tradeService.getSession(threadId); if (!session) throw new Error("Session not found"); if (session.state !== 'NEGOTIATING') throw new Error("Trade is not active"); const participant = session.userA.id === userId ? session.userA : session.userB.id === userId ? session.userB : null; if (!participant) throw new Error("User not in trade"); const existing = participant.offer.items.find(i => i.id === item.id); if (existing) { existing.quantity += quantity; } else { participant.offer.items.push({ id: item.id, name: item.name, quantity }); } unlockAll(session); session.lastInteraction = Date.now(); }, removeItem: (threadId: string, userId: string, itemId: number) => { const session = tradeService.getSession(threadId); if (!session) throw new Error("Session not found"); const participant = session.userA.id === userId ? session.userA : session.userB.id === userId ? session.userB : null; if (!participant) throw new Error("User not in trade"); participant.offer.items = participant.offer.items.filter(i => i.id !== itemId); unlockAll(session); session.lastInteraction = Date.now(); }, toggleLock: (threadId: string, userId: string): boolean => { const session = tradeService.getSession(threadId); if (!session) throw new Error("Session not found"); const participant = session.userA.id === userId ? session.userA : session.userB.id === userId ? session.userB : null; if (!participant) throw new Error("User not in trade"); participant.locked = !participant.locked; session.lastInteraction = Date.now(); return participant.locked; }, /** * Executes the trade atomically. * 1. Validates balances/inventory for both users. * 2. Swaps money. * 3. Swaps items. * 4. Logs transactions. */ executeTrade: async (threadId: string): Promise => { const session = tradeService.getSession(threadId); if (!session) throw new Error("Session not found"); if (!session.userA.locked || !session.userB.locked) { throw new Error("Both players must accept the trade first."); } session.state = 'COMPLETED'; // Prevent double execution await withTransaction(async (tx) => { // -- Validate & Execute User A -> User B -- await processTransfer(tx, session.userA, session.userB, session.threadId); // -- Validate & Execute User B -> User A -- await processTransfer(tx, session.userB, session.userA, session.threadId); }); tradeService.endSession(threadId); }, clearSessions: () => { sessions.clear(); console.log("[TradeService] All active trade sessions cleared."); } };