diff --git a/src/commands/economy/trade.ts b/src/commands/economy/trade.ts index 17d5b4d..2a85260 100644 --- a/src/commands/economy/trade.ts +++ b/src/commands/economy/trade.ts @@ -1,6 +1,6 @@ import { createCommand } from "@/lib/utils"; import { SlashCommandBuilder, ChannelType, ThreadAutoArchiveDuration, MessageFlags } from "discord.js"; -import { TradeService } from "@/modules/trade/trade.service"; +import { tradeService } from "@/modules/trade/trade.service"; import { getTradeDashboard } from "@/modules/trade/trade.view"; import { createErrorEmbed, createWarningEmbed } from "@lib/embeds"; @@ -59,7 +59,7 @@ export const trade = createCommand({ } // Setup Session - const session = TradeService.createSession(thread.id, + const session = tradeService.createSession(thread.id, { id: interaction.user.id, username: interaction.user.username }, { id: targetUser.id, username: targetUser.username } ); diff --git a/src/modules/trade/trade.interaction.ts b/src/modules/trade/trade.interaction.ts index 4ed3d7c..e47f3be 100644 --- a/src/modules/trade/trade.interaction.ts +++ b/src/modules/trade/trade.interaction.ts @@ -7,7 +7,7 @@ import { TextChannel, EmbedBuilder } from "discord.js"; -import { TradeService } from "./trade.service"; +import { tradeService } from "./trade.service"; import { inventoryService } from "@/modules/inventory/inventory.service"; import { createErrorEmbed, createWarningEmbed, createSuccessEmbed, createInfoEmbed } from "@lib/embeds"; import { getTradeDashboard, getTradeMoneyModal, getItemSelectMenu, getTradeCompletedEmbed } from "./trade.view"; @@ -55,10 +55,10 @@ export async function handleTradeInteraction(interaction: Interaction) { } async function handleCancel(interaction: ButtonInteraction | StringSelectMenuInteraction | ModalSubmitInteraction, threadId: string) { - const session = TradeService.getSession(threadId); + const session = tradeService.getSession(threadId); const user = interaction.user; - TradeService.endSession(threadId); + tradeService.endSession(threadId); await interaction.deferUpdate(); @@ -70,11 +70,11 @@ async function handleCancel(interaction: ButtonInteraction | StringSelectMenuInt async function handleLock(interaction: ButtonInteraction | StringSelectMenuInteraction | ModalSubmitInteraction, threadId: string) { await interaction.deferUpdate(); - const isLocked = TradeService.toggleLock(threadId, interaction.user.id); + const isLocked = tradeService.toggleLock(threadId, interaction.user.id); await updateTradeDashboard(interaction, threadId); // Check if trade executed (both locked) - const session = TradeService.getSession(threadId); + const session = tradeService.getSession(threadId); if (session && session.state === 'COMPLETED') { // Trade executed during updateTradeDashboard return; @@ -95,7 +95,7 @@ async function handleMoneySubmit(interaction: ModalSubmitInteraction, threadId: if (amount < 0n) throw new Error("Amount must be positive"); - TradeService.updateMoney(threadId, interaction.user.id, amount); + tradeService.updateMoney(threadId, interaction.user.id, amount); await interaction.deferUpdate(); // Acknowledge modal await updateTradeDashboard(interaction, threadId); } @@ -128,14 +128,14 @@ async function handleItemSelect(interaction: StringSelectMenuInteraction, thread const item = await inventoryService.getItem(itemId); if (!item) throw new Error("Item not found"); - TradeService.addItem(threadId, interaction.user.id, { id: item.id, name: item.name }, 1n); + tradeService.addItem(threadId, interaction.user.id, { id: item.id, name: item.name }, 1n); await interaction.update({ content: `Added ${item.name} x1`, components: [] }); await updateTradeDashboard(interaction, threadId); } async function handleRemoveItemClick(interaction: ButtonInteraction, threadId: string) { - const session = TradeService.getSession(threadId); + const session = tradeService.getSession(threadId); if (!session) return; const participant = session.userA.id === interaction.user.id ? session.userA : session.userB; @@ -158,7 +158,7 @@ async function handleRemoveItemSelect(interaction: StringSelectMenuInteraction, const value = interaction.values[0]; if (!value) return; const itemId = parseInt(value); - TradeService.removeItem(threadId, interaction.user.id, itemId); + tradeService.removeItem(threadId, interaction.user.id, itemId); await interaction.update({ content: `Removed item.`, components: [] }); await updateTradeDashboard(interaction, threadId); @@ -168,14 +168,14 @@ async function handleRemoveItemSelect(interaction: StringSelectMenuInteraction, // --- DASHBOARD UPDATER --- export async function updateTradeDashboard(interaction: Interaction, threadId: string) { - const session = TradeService.getSession(threadId); + const session = tradeService.getSession(threadId); if (!session) return; // Check Auto-Execute (If both locked) if (session.userA.locked && session.userB.locked) { // Execute Trade try { - await TradeService.executeTrade(threadId); + await tradeService.executeTrade(threadId); const embed = getTradeCompletedEmbed(session); await updateDashboardMessage(interaction, { embeds: [embed], components: [] }); diff --git a/src/modules/trade/trade.service.ts b/src/modules/trade/trade.service.ts index 33b4f89..4148c42 100644 --- a/src/modules/trade/trade.service.ts +++ b/src/modules/trade/trade.service.ts @@ -1,17 +1,80 @@ import type { TradeSession, TradeParticipant } from "./trade.types"; -import { DrizzleClient } from "@/lib/DrizzleClient"; import { economyService } from "@/modules/economy/economy.service"; import { inventoryService } from "@/modules/inventory/inventory.service"; import { itemTransactions } from "@/db/schema"; +import { withTransaction } from "@/lib/db"; import type { Transaction } from "@/lib/types"; -export class TradeService { - private static sessions = new Map(); +// 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, + 'TRADE_OUT', + `Trade with ${to.username} (Thread: ${threadId})`, + to.id, + tx + ); + await economyService.modifyUserBalance( + to.id, + from.offer.money, + '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: '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: 'TRADE_IN', + description: `Received from ${from.username}`, + }); + } +}; + +export const tradeService = { /** * Creates a new trade session */ - static createSession(threadId: string, userA: { id: string, username: string }, userB: { id: string, username: string }): TradeSession { + createSession: (threadId: string, userA: { id: string, username: string }, userB: { id: string, username: string }): TradeSession => { const session: TradeSession = { threadId, userA: { @@ -30,24 +93,24 @@ export class TradeService { lastInteraction: Date.now() }; - this.sessions.set(threadId, session); + sessions.set(threadId, session); return session; - } + }, - static getSession(threadId: string): TradeSession | undefined { - return this.sessions.get(threadId); - } + getSession: (threadId: string): TradeSession | undefined => { + return sessions.get(threadId); + }, - static endSession(threadId: string) { - this.sessions.delete(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). */ - static updateMoney(threadId: string, userId: string, amount: bigint) { - const session = this.getSession(threadId); + 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"); @@ -55,12 +118,12 @@ export class TradeService { if (!participant) throw new Error("User not in trade"); participant.offer.money = amount; - this.unlockAll(session); + unlockAll(session); session.lastInteraction = Date.now(); - } + }, - static addItem(threadId: string, userId: string, item: { id: number, name: string }, quantity: bigint) { - const session = this.getSession(threadId); + 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"); @@ -74,12 +137,12 @@ export class TradeService { participant.offer.items.push({ id: item.id, name: item.name, quantity }); } - this.unlockAll(session); + unlockAll(session); session.lastInteraction = Date.now(); - } + }, - static removeItem(threadId: string, userId: string, itemId: number) { - const session = this.getSession(threadId); + 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; @@ -87,12 +150,12 @@ export class TradeService { participant.offer.items = participant.offer.items.filter(i => i.id !== itemId); - this.unlockAll(session); + unlockAll(session); session.lastInteraction = Date.now(); - } + }, - static toggleLock(threadId: string, userId: string): boolean { - const session = this.getSession(threadId); + 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; @@ -102,12 +165,7 @@ export class TradeService { session.lastInteraction = Date.now(); return participant.locked; - } - - private static unlockAll(session: TradeSession) { - session.userA.locked = false; - session.userB.locked = false; - } + }, /** * Executes the trade atomically. @@ -116,8 +174,8 @@ export class TradeService { * 3. Swaps items. * 4. Logs transactions. */ - static async executeTrade(threadId: string): Promise { - const session = this.getSession(threadId); + 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) { @@ -126,65 +184,14 @@ export class TradeService { session.state = 'COMPLETED'; // Prevent double execution - await DrizzleClient.transaction(async (tx) => { + await withTransaction(async (tx) => { // -- Validate & Execute User A -> User B -- - await this.processTransfer(tx, session.userA, session.userB, session.threadId); + await processTransfer(tx, session.userA, session.userB, session.threadId); // -- Validate & Execute User B -> User A -- - await this.processTransfer(tx, session.userB, session.userA, session.threadId); + await processTransfer(tx, session.userB, session.userA, session.threadId); }); - this.endSession(threadId); + tradeService.endSession(threadId); } - - private static async processTransfer(tx: Transaction, from: TradeParticipant, to: TradeParticipant, threadId: string) { - // 1. Money - if (from.offer.money > 0n) { - await economyService.modifyUserBalance( - from.id, - -from.offer.money, - 'TRADE_OUT', - `Trade with ${to.username} (Thread: ${threadId})`, - to.id, - tx - ); - await economyService.modifyUserBalance( - to.id, - from.offer.money, - '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: '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: 'TRADE_IN', - description: `Received from ${from.username}`, - }); - } - } -} +};