forked from syntaxbullet/AuroraBot-discord
refactor: convert TradeService to object export pattern
Convert from class-based to object-based export for consistency with other services (economy, inventory, quest, etc). Changes: - Move sessions Map and helper functions to module scope - Convert static methods to object properties - Update executeTrade to use withTransaction helper - Update all imports from TradeService to tradeService Updated files: - trade.service.ts (main refactor) - trade.interaction.ts (update usages) - trade.ts command (update import and usage) All tests passing with no breaking changes.
This commit is contained in:
@@ -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 }
|
||||
);
|
||||
|
||||
@@ -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: [] });
|
||||
|
||||
|
||||
@@ -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<string, TradeSession>();
|
||||
// Module-level session storage
|
||||
const sessions = new Map<string, TradeSession>();
|
||||
|
||||
/**
|
||||
* 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<void> {
|
||||
const session = this.getSession(threadId);
|
||||
executeTrade: async (threadId: string): Promise<void> => {
|
||||
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}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user