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 { createCommand } from "@/lib/utils";
|
||||||
import { SlashCommandBuilder, ChannelType, ThreadAutoArchiveDuration, MessageFlags } from "discord.js";
|
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 { getTradeDashboard } from "@/modules/trade/trade.view";
|
||||||
import { createErrorEmbed, createWarningEmbed } from "@lib/embeds";
|
import { createErrorEmbed, createWarningEmbed } from "@lib/embeds";
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ export const trade = createCommand({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Setup Session
|
// Setup Session
|
||||||
const session = TradeService.createSession(thread.id,
|
const session = tradeService.createSession(thread.id,
|
||||||
{ id: interaction.user.id, username: interaction.user.username },
|
{ id: interaction.user.id, username: interaction.user.username },
|
||||||
{ id: targetUser.id, username: targetUser.username }
|
{ id: targetUser.id, username: targetUser.username }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
TextChannel,
|
TextChannel,
|
||||||
EmbedBuilder
|
EmbedBuilder
|
||||||
} from "discord.js";
|
} from "discord.js";
|
||||||
import { TradeService } from "./trade.service";
|
import { tradeService } from "./trade.service";
|
||||||
import { inventoryService } from "@/modules/inventory/inventory.service";
|
import { inventoryService } from "@/modules/inventory/inventory.service";
|
||||||
import { createErrorEmbed, createWarningEmbed, createSuccessEmbed, createInfoEmbed } from "@lib/embeds";
|
import { createErrorEmbed, createWarningEmbed, createSuccessEmbed, createInfoEmbed } from "@lib/embeds";
|
||||||
import { getTradeDashboard, getTradeMoneyModal, getItemSelectMenu, getTradeCompletedEmbed } from "./trade.view";
|
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) {
|
async function handleCancel(interaction: ButtonInteraction | StringSelectMenuInteraction | ModalSubmitInteraction, threadId: string) {
|
||||||
const session = TradeService.getSession(threadId);
|
const session = tradeService.getSession(threadId);
|
||||||
const user = interaction.user;
|
const user = interaction.user;
|
||||||
|
|
||||||
TradeService.endSession(threadId);
|
tradeService.endSession(threadId);
|
||||||
|
|
||||||
await interaction.deferUpdate();
|
await interaction.deferUpdate();
|
||||||
|
|
||||||
@@ -70,11 +70,11 @@ async function handleCancel(interaction: ButtonInteraction | StringSelectMenuInt
|
|||||||
|
|
||||||
async function handleLock(interaction: ButtonInteraction | StringSelectMenuInteraction | ModalSubmitInteraction, threadId: string) {
|
async function handleLock(interaction: ButtonInteraction | StringSelectMenuInteraction | ModalSubmitInteraction, threadId: string) {
|
||||||
await interaction.deferUpdate();
|
await interaction.deferUpdate();
|
||||||
const isLocked = TradeService.toggleLock(threadId, interaction.user.id);
|
const isLocked = tradeService.toggleLock(threadId, interaction.user.id);
|
||||||
await updateTradeDashboard(interaction, threadId);
|
await updateTradeDashboard(interaction, threadId);
|
||||||
|
|
||||||
// Check if trade executed (both locked)
|
// Check if trade executed (both locked)
|
||||||
const session = TradeService.getSession(threadId);
|
const session = tradeService.getSession(threadId);
|
||||||
if (session && session.state === 'COMPLETED') {
|
if (session && session.state === 'COMPLETED') {
|
||||||
// Trade executed during updateTradeDashboard
|
// Trade executed during updateTradeDashboard
|
||||||
return;
|
return;
|
||||||
@@ -95,7 +95,7 @@ async function handleMoneySubmit(interaction: ModalSubmitInteraction, threadId:
|
|||||||
|
|
||||||
if (amount < 0n) throw new Error("Amount must be positive");
|
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 interaction.deferUpdate(); // Acknowledge modal
|
||||||
await updateTradeDashboard(interaction, threadId);
|
await updateTradeDashboard(interaction, threadId);
|
||||||
}
|
}
|
||||||
@@ -128,14 +128,14 @@ async function handleItemSelect(interaction: StringSelectMenuInteraction, thread
|
|||||||
const item = await inventoryService.getItem(itemId);
|
const item = await inventoryService.getItem(itemId);
|
||||||
if (!item) throw new Error("Item not found");
|
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 interaction.update({ content: `Added ${item.name} x1`, components: [] });
|
||||||
await updateTradeDashboard(interaction, threadId);
|
await updateTradeDashboard(interaction, threadId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRemoveItemClick(interaction: ButtonInteraction, threadId: string) {
|
async function handleRemoveItemClick(interaction: ButtonInteraction, threadId: string) {
|
||||||
const session = TradeService.getSession(threadId);
|
const session = tradeService.getSession(threadId);
|
||||||
if (!session) return;
|
if (!session) return;
|
||||||
|
|
||||||
const participant = session.userA.id === interaction.user.id ? session.userA : session.userB;
|
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];
|
const value = interaction.values[0];
|
||||||
if (!value) return;
|
if (!value) return;
|
||||||
const itemId = parseInt(value);
|
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 interaction.update({ content: `Removed item.`, components: [] });
|
||||||
await updateTradeDashboard(interaction, threadId);
|
await updateTradeDashboard(interaction, threadId);
|
||||||
@@ -168,14 +168,14 @@ async function handleRemoveItemSelect(interaction: StringSelectMenuInteraction,
|
|||||||
// --- DASHBOARD UPDATER ---
|
// --- DASHBOARD UPDATER ---
|
||||||
|
|
||||||
export async function updateTradeDashboard(interaction: Interaction, threadId: string) {
|
export async function updateTradeDashboard(interaction: Interaction, threadId: string) {
|
||||||
const session = TradeService.getSession(threadId);
|
const session = tradeService.getSession(threadId);
|
||||||
if (!session) return;
|
if (!session) return;
|
||||||
|
|
||||||
// Check Auto-Execute (If both locked)
|
// Check Auto-Execute (If both locked)
|
||||||
if (session.userA.locked && session.userB.locked) {
|
if (session.userA.locked && session.userB.locked) {
|
||||||
// Execute Trade
|
// Execute Trade
|
||||||
try {
|
try {
|
||||||
await TradeService.executeTrade(threadId);
|
await tradeService.executeTrade(threadId);
|
||||||
const embed = getTradeCompletedEmbed(session);
|
const embed = getTradeCompletedEmbed(session);
|
||||||
await updateDashboardMessage(interaction, { embeds: [embed], components: [] });
|
await updateDashboardMessage(interaction, { embeds: [embed], components: [] });
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,80 @@
|
|||||||
import type { TradeSession, TradeParticipant } from "./trade.types";
|
import type { TradeSession, TradeParticipant } from "./trade.types";
|
||||||
import { DrizzleClient } from "@/lib/DrizzleClient";
|
|
||||||
import { economyService } from "@/modules/economy/economy.service";
|
import { economyService } from "@/modules/economy/economy.service";
|
||||||
import { inventoryService } from "@/modules/inventory/inventory.service";
|
import { inventoryService } from "@/modules/inventory/inventory.service";
|
||||||
import { itemTransactions } from "@/db/schema";
|
import { itemTransactions } from "@/db/schema";
|
||||||
|
import { withTransaction } from "@/lib/db";
|
||||||
import type { Transaction } from "@/lib/types";
|
import type { Transaction } from "@/lib/types";
|
||||||
|
|
||||||
export class TradeService {
|
// Module-level session storage
|
||||||
private static sessions = new Map<string, TradeSession>();
|
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
|
* 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 = {
|
const session: TradeSession = {
|
||||||
threadId,
|
threadId,
|
||||||
userA: {
|
userA: {
|
||||||
@@ -30,24 +93,24 @@ export class TradeService {
|
|||||||
lastInteraction: Date.now()
|
lastInteraction: Date.now()
|
||||||
};
|
};
|
||||||
|
|
||||||
this.sessions.set(threadId, session);
|
sessions.set(threadId, session);
|
||||||
return session;
|
return session;
|
||||||
}
|
},
|
||||||
|
|
||||||
static getSession(threadId: string): TradeSession | undefined {
|
getSession: (threadId: string): TradeSession | undefined => {
|
||||||
return this.sessions.get(threadId);
|
return sessions.get(threadId);
|
||||||
}
|
},
|
||||||
|
|
||||||
static endSession(threadId: string) {
|
endSession: (threadId: string) => {
|
||||||
this.sessions.delete(threadId);
|
sessions.delete(threadId);
|
||||||
}
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates an offer. If allowed, validation checks should be done BEFORE calling this.
|
* Updates an offer. If allowed, validation checks should be done BEFORE calling this.
|
||||||
* unlocking logic is handled here (if offer changes, unlock both).
|
* unlocking logic is handled here (if offer changes, unlock both).
|
||||||
*/
|
*/
|
||||||
static updateMoney(threadId: string, userId: string, amount: bigint) {
|
updateMoney: (threadId: string, userId: string, amount: bigint) => {
|
||||||
const session = this.getSession(threadId);
|
const session = tradeService.getSession(threadId);
|
||||||
if (!session) throw new Error("Session not found");
|
if (!session) throw new Error("Session not found");
|
||||||
if (session.state !== 'NEGOTIATING') throw new Error("Trade is not active");
|
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");
|
if (!participant) throw new Error("User not in trade");
|
||||||
|
|
||||||
participant.offer.money = amount;
|
participant.offer.money = amount;
|
||||||
this.unlockAll(session);
|
unlockAll(session);
|
||||||
session.lastInteraction = Date.now();
|
session.lastInteraction = Date.now();
|
||||||
}
|
},
|
||||||
|
|
||||||
static addItem(threadId: string, userId: string, item: { id: number, name: string }, quantity: bigint) {
|
addItem: (threadId: string, userId: string, item: { id: number, name: string }, quantity: bigint) => {
|
||||||
const session = this.getSession(threadId);
|
const session = tradeService.getSession(threadId);
|
||||||
if (!session) throw new Error("Session not found");
|
if (!session) throw new Error("Session not found");
|
||||||
if (session.state !== 'NEGOTIATING') throw new Error("Trade is not active");
|
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 });
|
participant.offer.items.push({ id: item.id, name: item.name, quantity });
|
||||||
}
|
}
|
||||||
|
|
||||||
this.unlockAll(session);
|
unlockAll(session);
|
||||||
session.lastInteraction = Date.now();
|
session.lastInteraction = Date.now();
|
||||||
}
|
},
|
||||||
|
|
||||||
static removeItem(threadId: string, userId: string, itemId: number) {
|
removeItem: (threadId: string, userId: string, itemId: number) => {
|
||||||
const session = this.getSession(threadId);
|
const session = tradeService.getSession(threadId);
|
||||||
if (!session) throw new Error("Session not found");
|
if (!session) throw new Error("Session not found");
|
||||||
|
|
||||||
const participant = session.userA.id === userId ? session.userA : session.userB.id === userId ? session.userB : null;
|
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);
|
participant.offer.items = participant.offer.items.filter(i => i.id !== itemId);
|
||||||
|
|
||||||
this.unlockAll(session);
|
unlockAll(session);
|
||||||
session.lastInteraction = Date.now();
|
session.lastInteraction = Date.now();
|
||||||
}
|
},
|
||||||
|
|
||||||
static toggleLock(threadId: string, userId: string): boolean {
|
toggleLock: (threadId: string, userId: string): boolean => {
|
||||||
const session = this.getSession(threadId);
|
const session = tradeService.getSession(threadId);
|
||||||
if (!session) throw new Error("Session not found");
|
if (!session) throw new Error("Session not found");
|
||||||
|
|
||||||
const participant = session.userA.id === userId ? session.userA : session.userB.id === userId ? session.userB : null;
|
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();
|
session.lastInteraction = Date.now();
|
||||||
|
|
||||||
return participant.locked;
|
return participant.locked;
|
||||||
}
|
},
|
||||||
|
|
||||||
private static unlockAll(session: TradeSession) {
|
|
||||||
session.userA.locked = false;
|
|
||||||
session.userB.locked = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes the trade atomically.
|
* Executes the trade atomically.
|
||||||
@@ -116,8 +174,8 @@ export class TradeService {
|
|||||||
* 3. Swaps items.
|
* 3. Swaps items.
|
||||||
* 4. Logs transactions.
|
* 4. Logs transactions.
|
||||||
*/
|
*/
|
||||||
static async executeTrade(threadId: string): Promise<void> {
|
executeTrade: async (threadId: string): Promise<void> => {
|
||||||
const session = this.getSession(threadId);
|
const session = tradeService.getSession(threadId);
|
||||||
if (!session) throw new Error("Session not found");
|
if (!session) throw new Error("Session not found");
|
||||||
|
|
||||||
if (!session.userA.locked || !session.userB.locked) {
|
if (!session.userA.locked || !session.userB.locked) {
|
||||||
@@ -126,65 +184,14 @@ export class TradeService {
|
|||||||
|
|
||||||
session.state = 'COMPLETED'; // Prevent double execution
|
session.state = 'COMPLETED'; // Prevent double execution
|
||||||
|
|
||||||
await DrizzleClient.transaction(async (tx) => {
|
await withTransaction(async (tx) => {
|
||||||
// -- Validate & Execute User A -> User B --
|
// -- 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 --
|
// -- 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