forked from syntaxbullet/AuroraBot-discord
feat: Add user existence checks to economy commands and refactor trade service to expose sessions for testing.
This commit is contained in:
@@ -23,6 +23,8 @@ export const balance = createCommand({
|
|||||||
|
|
||||||
const user = await userService.getOrCreateUser(targetUser.id, targetUser.username);
|
const user = await userService.getOrCreateUser(targetUser.id, targetUser.username);
|
||||||
|
|
||||||
|
if (!user) throw new Error("Failed to retrieve user data.");
|
||||||
|
|
||||||
const embed = createBaseEmbed(undefined, `**Balance**: ${user.balance || 0n} AU`, "Yellow")
|
const embed = createBaseEmbed(undefined, `**Balance**: ${user.balance || 0n} AU`, "Yellow")
|
||||||
.setAuthor({ name: targetUser.username, iconURL: targetUser.displayAvatarURL() });
|
.setAuthor({ name: targetUser.username, iconURL: targetUser.displayAvatarURL() });
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ export const exam = createCommand({
|
|||||||
execute: async (interaction) => {
|
execute: async (interaction) => {
|
||||||
await interaction.deferReply();
|
await interaction.deferReply();
|
||||||
const user = await userService.getOrCreateUser(interaction.user.id, interaction.user.username);
|
const user = await userService.getOrCreateUser(interaction.user.id, interaction.user.username);
|
||||||
|
if (!user) {
|
||||||
|
await interaction.editReply({ embeds: [createErrorEmbed("Failed to retrieve user data.")] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const currentDay = now.getDay();
|
const currentDay = now.getDay();
|
||||||
|
|
||||||
@@ -47,7 +51,7 @@ export const exam = createCommand({
|
|||||||
|
|
||||||
const metadata: ExamMetadata = {
|
const metadata: ExamMetadata = {
|
||||||
examDay: currentDay,
|
examDay: currentDay,
|
||||||
lastXp: user.xp.toString()
|
lastXp: (user.xp ?? 0n).toString()
|
||||||
};
|
};
|
||||||
|
|
||||||
await DrizzleClient.insert(userTimers).values({
|
await DrizzleClient.insert(userTimers).values({
|
||||||
@@ -98,7 +102,7 @@ export const exam = createCommand({
|
|||||||
|
|
||||||
const newMetadata: ExamMetadata = {
|
const newMetadata: ExamMetadata = {
|
||||||
examDay: examDay,
|
examDay: examDay,
|
||||||
lastXp: user.xp.toString() // Reset tracking
|
lastXp: (user.xp ?? 0n).toString()
|
||||||
};
|
};
|
||||||
|
|
||||||
await DrizzleClient.update(userTimers)
|
await DrizzleClient.update(userTimers)
|
||||||
@@ -125,7 +129,7 @@ export const exam = createCommand({
|
|||||||
|
|
||||||
// 5. Reward Calculation
|
// 5. Reward Calculation
|
||||||
const lastXp = BigInt(metadata.lastXp || "0"); // Fallback just in case
|
const lastXp = BigInt(metadata.lastXp || "0"); // Fallback just in case
|
||||||
const currentXp = user.xp;
|
const currentXp = user.xp ?? 0n;
|
||||||
const diff = currentXp - lastXp;
|
const diff = currentXp - lastXp;
|
||||||
|
|
||||||
// Calculate Reward
|
// Calculate Reward
|
||||||
|
|||||||
@@ -33,6 +33,11 @@ export const pay = createCommand({
|
|||||||
|
|
||||||
const amount = BigInt(interaction.options.getInteger("amount", true));
|
const amount = BigInt(interaction.options.getInteger("amount", true));
|
||||||
const senderId = interaction.user.id;
|
const senderId = interaction.user.id;
|
||||||
|
if (!targetUser) {
|
||||||
|
await interaction.reply({ embeds: [createErrorEmbed("User not found.")], flags: MessageFlags.Ephemeral });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const receiverId = targetUser.id;
|
const receiverId = targetUser.id;
|
||||||
|
|
||||||
if (amount < config.economy.transfers.minAmount) {
|
if (amount < config.economy.transfers.minAmount) {
|
||||||
@@ -40,14 +45,14 @@ export const pay = createCommand({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (senderId === receiverId) {
|
if (senderId === receiverId.toString()) {
|
||||||
await interaction.reply({ embeds: [createErrorEmbed("You cannot pay yourself.")], flags: MessageFlags.Ephemeral });
|
await interaction.reply({ embeds: [createErrorEmbed("You cannot pay yourself.")], flags: MessageFlags.Ephemeral });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await interaction.deferReply();
|
await interaction.deferReply();
|
||||||
await economyService.transfer(senderId, receiverId, amount);
|
await economyService.transfer(senderId, receiverId.toString(), amount);
|
||||||
|
|
||||||
const embed = createSuccessEmbed(`Successfully sent ** ${amount}** Astral Units to <@${targetUser.id}>.`, "💸 Transfer Successful");
|
const embed = createSuccessEmbed(`Successfully sent ** ${amount}** Astral Units to <@${targetUser.id}>.`, "💸 Transfer Successful");
|
||||||
await interaction.editReply({ embeds: [embed], content: `<@${receiverId}>` });
|
await interaction.editReply({ embeds: [embed], content: `<@${receiverId}>` });
|
||||||
|
|||||||
@@ -77,8 +77,8 @@ describe("levelingService", () => {
|
|||||||
// base 100, exp 1.5
|
// base 100, exp 1.5
|
||||||
// lvl 1: 100 * 1^1.5 = 100
|
// lvl 1: 100 * 1^1.5 = 100
|
||||||
// lvl 2: 100 * 2^1.5 = 100 * 2.828 = 282
|
// lvl 2: 100 * 2^1.5 = 100 * 2.828 = 282
|
||||||
expect(levelingService.getXpForLevel(1)).toBe(100);
|
expect(levelingService.getXpForNextLevel(1)).toBe(100);
|
||||||
expect(levelingService.getXpForLevel(2)).toBe(282);
|
expect(levelingService.getXpForNextLevel(2)).toBe(282);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ describe("levelingService", () => {
|
|||||||
|
|
||||||
expect(result.levelUp).toBe(true);
|
expect(result.levelUp).toBe(true);
|
||||||
expect(result.currentLevel).toBe(2);
|
expect(result.currentLevel).toBe(2);
|
||||||
expect(mockSet).toHaveBeenCalledWith({ xp: 20n, level: 2 });
|
expect(mockSet).toHaveBeenCalledWith({ xp: 120n, level: 2 });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle multiple level ups", async () => {
|
it("should handle multiple level ups", async () => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect, mock, beforeEach, afterEach, spyOn } from "bun:test";
|
import { describe, it, expect, mock, beforeEach, afterEach, spyOn } from "bun:test";
|
||||||
import { TradeService } from "./trade.service";
|
import { tradeService } from "./trade.service";
|
||||||
import { itemTransactions } from "@/db/schema";
|
import { itemTransactions } from "@/db/schema";
|
||||||
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";
|
||||||
@@ -37,7 +37,7 @@ describe("TradeService", () => {
|
|||||||
mockValues.mockClear();
|
mockValues.mockClear();
|
||||||
|
|
||||||
// Clear sessions
|
// Clear sessions
|
||||||
(TradeService as any).sessions.clear();
|
(tradeService as any)._sessions.clear();
|
||||||
|
|
||||||
// Spies
|
// Spies
|
||||||
mockModifyUserBalance = spyOn(economyService, 'modifyUserBalance').mockResolvedValue({} as any);
|
mockModifyUserBalance = spyOn(economyService, 'modifyUserBalance').mockResolvedValue({} as any);
|
||||||
@@ -54,68 +54,68 @@ describe("TradeService", () => {
|
|||||||
|
|
||||||
describe("createSession", () => {
|
describe("createSession", () => {
|
||||||
it("should create a new session", () => {
|
it("should create a new session", () => {
|
||||||
const session = TradeService.createSession("thread1", userA, userB);
|
const session = tradeService.createSession("thread1", userA, userB);
|
||||||
|
|
||||||
expect(session.threadId).toBe("thread1");
|
expect(session.threadId).toBe("thread1");
|
||||||
expect(session.state).toBe("NEGOTIATING");
|
expect(session.state).toBe("NEGOTIATING");
|
||||||
expect(session.userA.id).toBe("1");
|
expect(session.userA.id).toBe("1");
|
||||||
expect(session.userB.id).toBe("2");
|
expect(session.userB.id).toBe("2");
|
||||||
expect(TradeService.getSession("thread1")).toBe(session);
|
expect(tradeService.getSession("thread1")).toBe(session);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("updateMoney", () => {
|
describe("updateMoney", () => {
|
||||||
it("should update money offer", () => {
|
it("should update money offer", () => {
|
||||||
TradeService.createSession("thread1", userA, userB);
|
tradeService.createSession("thread1", userA, userB);
|
||||||
TradeService.updateMoney("thread1", "1", 100n);
|
tradeService.updateMoney("thread1", "1", 100n);
|
||||||
|
|
||||||
const session = TradeService.getSession("thread1");
|
const session = tradeService.getSession("thread1");
|
||||||
expect(session?.userA.offer.money).toBe(100n);
|
expect(session?.userA.offer.money).toBe(100n);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should unlock participants when offer changes", () => {
|
it("should unlock participants when offer changes", () => {
|
||||||
const session = TradeService.createSession("thread1", userA, userB);
|
const session = tradeService.createSession("thread1", userA, userB);
|
||||||
session.userA.locked = true;
|
session.userA.locked = true;
|
||||||
session.userB.locked = true;
|
session.userB.locked = true;
|
||||||
|
|
||||||
TradeService.updateMoney("thread1", "1", 100n);
|
tradeService.updateMoney("thread1", "1", 100n);
|
||||||
|
|
||||||
expect(session.userA.locked).toBe(false);
|
expect(session.userA.locked).toBe(false);
|
||||||
expect(session.userB.locked).toBe(false);
|
expect(session.userB.locked).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw if not in trade", () => {
|
it("should throw if not in trade", () => {
|
||||||
TradeService.createSession("thread1", userA, userB);
|
tradeService.createSession("thread1", userA, userB);
|
||||||
expect(() => TradeService.updateMoney("thread1", "3", 100n)).toThrow("User not in trade");
|
expect(() => tradeService.updateMoney("thread1", "3", 100n)).toThrow("User not in trade");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("addItem", () => {
|
describe("addItem", () => {
|
||||||
it("should add item to offer", () => {
|
it("should add item to offer", () => {
|
||||||
TradeService.createSession("thread1", userA, userB);
|
tradeService.createSession("thread1", userA, userB);
|
||||||
TradeService.addItem("thread1", "1", { id: 10, name: "Sword" }, 1n);
|
tradeService.addItem("thread1", "1", { id: 10, name: "Sword" }, 1n);
|
||||||
|
|
||||||
const session = TradeService.getSession("thread1");
|
const session = tradeService.getSession("thread1");
|
||||||
expect(session?.userA.offer.items).toHaveLength(1);
|
expect(session?.userA.offer.items).toHaveLength(1);
|
||||||
expect(session?.userA.offer.items[0]).toEqual({ id: 10, name: "Sword", quantity: 1n });
|
expect(session?.userA.offer.items[0]).toEqual({ id: 10, name: "Sword", quantity: 1n });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should stack items if already offered", () => {
|
it("should stack items if already offered", () => {
|
||||||
TradeService.createSession("thread1", userA, userB);
|
tradeService.createSession("thread1", userA, userB);
|
||||||
TradeService.addItem("thread1", "1", { id: 10, name: "Sword" }, 1n);
|
tradeService.addItem("thread1", "1", { id: 10, name: "Sword" }, 1n);
|
||||||
TradeService.addItem("thread1", "1", { id: 10, name: "Sword" }, 2n);
|
tradeService.addItem("thread1", "1", { id: 10, name: "Sword" }, 2n);
|
||||||
|
|
||||||
const session = TradeService.getSession("thread1");
|
const session = tradeService.getSession("thread1");
|
||||||
expect(session?.userA.offer.items[0]!.quantity).toBe(3n);
|
expect(session?.userA.offer.items[0]!.quantity).toBe(3n);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("removeItem", () => {
|
describe("removeItem", () => {
|
||||||
it("should remove item from offer", () => {
|
it("should remove item from offer", () => {
|
||||||
const session = TradeService.createSession("thread1", userA, userB);
|
const session = tradeService.createSession("thread1", userA, userB);
|
||||||
session.userA.offer.items.push({ id: 10, name: "Sword", quantity: 1n });
|
session.userA.offer.items.push({ id: 10, name: "Sword", quantity: 1n });
|
||||||
|
|
||||||
TradeService.removeItem("thread1", "1", 10);
|
tradeService.removeItem("thread1", "1", 10);
|
||||||
|
|
||||||
expect(session.userA.offer.items).toHaveLength(0);
|
expect(session.userA.offer.items).toHaveLength(0);
|
||||||
});
|
});
|
||||||
@@ -123,19 +123,19 @@ describe("TradeService", () => {
|
|||||||
|
|
||||||
describe("toggleLock", () => {
|
describe("toggleLock", () => {
|
||||||
it("should toggle lock status", () => {
|
it("should toggle lock status", () => {
|
||||||
TradeService.createSession("thread1", userA, userB);
|
tradeService.createSession("thread1", userA, userB);
|
||||||
|
|
||||||
const locked1 = TradeService.toggleLock("thread1", "1");
|
const locked1 = tradeService.toggleLock("thread1", "1");
|
||||||
expect(locked1).toBe(true);
|
expect(locked1).toBe(true);
|
||||||
|
|
||||||
const locked2 = TradeService.toggleLock("thread1", "1");
|
const locked2 = tradeService.toggleLock("thread1", "1");
|
||||||
expect(locked2).toBe(false);
|
expect(locked2).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("executeTrade", () => {
|
describe("executeTrade", () => {
|
||||||
it("should execute trade successfully", async () => {
|
it("should execute trade successfully", async () => {
|
||||||
const session = TradeService.createSession("thread1", userA, userB);
|
const session = tradeService.createSession("thread1", userA, userB);
|
||||||
|
|
||||||
// Setup offers
|
// Setup offers
|
||||||
session.userA.offer.money = 100n;
|
session.userA.offer.money = 100n;
|
||||||
@@ -148,7 +148,7 @@ describe("TradeService", () => {
|
|||||||
session.userA.locked = true;
|
session.userA.locked = true;
|
||||||
session.userB.locked = true;
|
session.userB.locked = true;
|
||||||
|
|
||||||
await TradeService.executeTrade("thread1");
|
await tradeService.executeTrade("thread1");
|
||||||
|
|
||||||
expect(session.state).toBe("COMPLETED");
|
expect(session.state).toBe("COMPLETED");
|
||||||
|
|
||||||
@@ -171,11 +171,11 @@ describe("TradeService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should throw if not locked", async () => {
|
it("should throw if not locked", async () => {
|
||||||
const session = TradeService.createSession("thread1", userA, userB);
|
const session = tradeService.createSession("thread1", userA, userB);
|
||||||
session.userA.locked = true;
|
session.userA.locked = true;
|
||||||
// B not locked
|
// B not locked
|
||||||
|
|
||||||
expect(TradeService.executeTrade("thread1")).rejects.toThrow("Both players must accept");
|
expect(tradeService.executeTrade("thread1")).rejects.toThrow("Both players must accept");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ const processTransfer = async (tx: Transaction, from: TradeParticipant, to: Trad
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const tradeService = {
|
export const tradeService = {
|
||||||
|
// Expose for testing
|
||||||
|
_sessions: sessions,
|
||||||
/**
|
/**
|
||||||
* Creates a new trade session
|
* Creates a new trade session
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user