feat: centralized constants and enums for project-wide use

This commit is contained in:
syntaxbullet
2026-01-06 18:15:52 +01:00
parent c807fd4fd0
commit 34347f0c63
17 changed files with 144 additions and 63 deletions

View File

@@ -7,8 +7,9 @@ import { userTimers, users } from "@/db/schema";
import { eq, and, sql } from "drizzle-orm"; import { eq, and, sql } from "drizzle-orm";
import { DrizzleClient } from "@/lib/DrizzleClient"; import { DrizzleClient } from "@/lib/DrizzleClient";
import { config } from "@lib/config"; import { config } from "@lib/config";
import { TimerType } from "@/lib/constants";
const EXAM_TIMER_TYPE = 'EXAM_SYSTEM'; const EXAM_TIMER_TYPE = TimerType.EXAM_SYSTEM;
const EXAM_TIMER_KEY = 'default'; const EXAM_TIMER_KEY = 'default';
interface ExamMetadata { interface ExamMetadata {

65
src/lib/constants.ts Normal file
View File

@@ -0,0 +1,65 @@
/**
* Global Constants and Enums
*/
export enum TimerType {
COOLDOWN = 'COOLDOWN',
EFFECT = 'EFFECT',
ACCESS = 'ACCESS',
EXAM_SYSTEM = 'EXAM_SYSTEM',
}
export enum EffectType {
ADD_XP = 'ADD_XP',
ADD_BALANCE = 'ADD_BALANCE',
REPLY_MESSAGE = 'REPLY_MESSAGE',
XP_BOOST = 'XP_BOOST',
TEMP_ROLE = 'TEMP_ROLE',
COLOR_ROLE = 'COLOR_ROLE',
LOOTBOX = 'LOOTBOX',
}
export enum TransactionType {
TRANSFER_IN = 'TRANSFER_IN',
TRANSFER_OUT = 'TRANSFER_OUT',
DAILY_REWARD = 'DAILY_REWARD',
ITEM_USE = 'ITEM_USE',
LOOTBOX = 'LOOTBOX',
EXAM_REWARD = 'EXAM_REWARD',
PURCHASE = 'PURCHASE',
TRADE_IN = 'TRADE_IN',
TRADE_OUT = 'TRADE_OUT',
QUEST_REWARD = 'QUEST_REWARD',
}
export enum ItemTransactionType {
TRADE_IN = 'TRADE_IN',
TRADE_OUT = 'TRADE_OUT',
SHOP_BUY = 'SHOP_BUY',
DROP = 'DROP',
GIVE = 'GIVE',
USE = 'USE',
}
export enum ItemType {
MATERIAL = 'MATERIAL',
CONSUMABLE = 'CONSUMABLE',
EQUIPMENT = 'EQUIPMENT',
QUEST = 'QUEST',
}
export enum CaseType {
WARN = 'warn',
TIMEOUT = 'timeout',
KICK = 'kick',
BAN = 'ban',
NOTE = 'note',
PRUNE = 'prune',
}
export enum LootType {
NOTHING = 'NOTHING',
CURRENCY = 'CURRENCY',
XP = 'XP',
ITEM = 'ITEM',
}

View File

@@ -1,4 +1,6 @@
import type { AutocompleteInteraction, ChatInputCommandInteraction, ClientEvents, SlashCommandBuilder, SlashCommandOptionsOnlyBuilder, SlashCommandSubcommandsOnlyBuilder } from "discord.js"; import type { AutocompleteInteraction, ChatInputCommandInteraction, ClientEvents, SlashCommandBuilder, SlashCommandOptionsOnlyBuilder, SlashCommandSubcommandsOnlyBuilder } from "discord.js";
import { LootType, EffectType } from "./constants";
import { DrizzleClient } from "./DrizzleClient";
export interface Command { export interface Command {
data: SlashCommandBuilder | SlashCommandOptionsOnlyBuilder | SlashCommandSubcommandsOnlyBuilder; data: SlashCommandBuilder | SlashCommandOptionsOnlyBuilder | SlashCommandSubcommandsOnlyBuilder;
@@ -14,16 +16,16 @@ export interface Event<K extends keyof ClientEvents> {
} }
export type ItemEffect = export type ItemEffect =
| { type: 'ADD_XP'; amount: number } | { type: EffectType.ADD_XP; amount: number }
| { type: 'ADD_BALANCE'; amount: number } | { type: EffectType.ADD_BALANCE; amount: number }
| { type: 'XP_BOOST'; multiplier: number; durationSeconds?: number; durationMinutes?: number; durationHours?: number } | { type: EffectType.XP_BOOST; multiplier: number; durationSeconds?: number; durationMinutes?: number; durationHours?: number }
| { type: 'TEMP_ROLE'; roleId: string; durationSeconds?: number; durationMinutes?: number; durationHours?: number } | { type: EffectType.TEMP_ROLE; roleId: string; durationSeconds?: number; durationMinutes?: number; durationHours?: number }
| { type: 'REPLY_MESSAGE'; message: string } | { type: EffectType.REPLY_MESSAGE; message: string }
| { type: 'COLOR_ROLE'; roleId: string } | { type: EffectType.COLOR_ROLE; roleId: string }
| { type: 'LOOTBOX'; pool: LootTableItem[] }; | { type: EffectType.LOOTBOX; pool: LootTableItem[] };
export interface LootTableItem { export interface LootTableItem {
type: 'CURRENCY' | 'ITEM' | 'XP' | 'NOTHING'; type: LootType;
weight: number; weight: number;
amount?: number; // For CURRENCY, XP amount?: number; // For CURRENCY, XP
itemId?: number; // For ITEM itemId?: number; // For ITEM
@@ -37,7 +39,5 @@ export interface ItemUsageData {
effects: ItemEffect[]; effects: ItemEffect[];
} }
import { DrizzleClient } from "./DrizzleClient";
export type DbClient = typeof DrizzleClient; export type DbClient = typeof DrizzleClient;
export type Transaction = Parameters<Parameters<DbClient['transaction']>[0]>[0]; export type Transaction = Parameters<Parameters<DbClient['transaction']>[0]>[0];

View File

@@ -4,6 +4,7 @@ import { DrizzleClient } from "@/lib/DrizzleClient";
import type { ItemUsageData, ItemEffect } from "@/lib/types"; import type { ItemUsageData, ItemEffect } from "@/lib/types";
import { getItemWizardEmbed, getItemTypeSelection, getEffectTypeSelection, getDetailsModal, getEconomyModal, getVisualsModal, getEffectConfigModal } from "./item_wizard.view"; import { getItemWizardEmbed, getItemTypeSelection, getEffectTypeSelection, getDetailsModal, getEconomyModal, getVisualsModal, getEffectConfigModal } from "./item_wizard.view";
import type { DraftItem } from "./item_wizard.types"; import type { DraftItem } from "./item_wizard.types";
import { ItemType, EffectType } from "@/lib/constants";
// --- Types --- // --- Types ---
@@ -23,7 +24,7 @@ export const renderWizard = (userId: string, isDraft = true) => {
name: "New Item", name: "New Item",
description: "No description", description: "No description",
rarity: "Common", rarity: "Common",
type: "MATERIAL", type: ItemType.MATERIAL,
price: null, price: null,
iconUrl: "", iconUrl: "",
imageUrl: "", imageUrl: "",
@@ -176,26 +177,26 @@ export const handleItemWizardInteraction = async (interaction: Interaction) => {
if (type) { if (type) {
let effect: ItemEffect | null = null; let effect: ItemEffect | null = null;
if (type === "ADD_XP" || type === "ADD_BALANCE") { if (type === EffectType.ADD_XP || type === EffectType.ADD_BALANCE) {
const amount = parseInt(interaction.fields.getTextInputValue("amount")); const amount = parseInt(interaction.fields.getTextInputValue("amount"));
if (!isNaN(amount)) effect = { type: type as any, amount }; if (!isNaN(amount)) effect = { type: type as any, amount };
} }
else if (type === "REPLY_MESSAGE") { else if (type === EffectType.REPLY_MESSAGE) {
effect = { type: "REPLY_MESSAGE", message: interaction.fields.getTextInputValue("message") }; effect = { type: EffectType.REPLY_MESSAGE, message: interaction.fields.getTextInputValue("message") };
} }
else if (type === "XP_BOOST") { else if (type === EffectType.XP_BOOST) {
const multiplier = parseFloat(interaction.fields.getTextInputValue("multiplier")); const multiplier = parseFloat(interaction.fields.getTextInputValue("multiplier"));
const duration = parseInt(interaction.fields.getTextInputValue("duration")); const duration = parseInt(interaction.fields.getTextInputValue("duration"));
if (!isNaN(multiplier) && !isNaN(duration)) effect = { type: "XP_BOOST", multiplier, durationSeconds: duration }; if (!isNaN(multiplier) && !isNaN(duration)) effect = { type: EffectType.XP_BOOST, multiplier, durationSeconds: duration };
} }
else if (type === "TEMP_ROLE") { else if (type === EffectType.TEMP_ROLE) {
const roleId = interaction.fields.getTextInputValue("role_id"); const roleId = interaction.fields.getTextInputValue("role_id");
const duration = parseInt(interaction.fields.getTextInputValue("duration")); const duration = parseInt(interaction.fields.getTextInputValue("duration"));
if (roleId && !isNaN(duration)) effect = { type: "TEMP_ROLE", roleId: roleId, durationSeconds: duration }; if (roleId && !isNaN(duration)) effect = { type: EffectType.TEMP_ROLE, roleId: roleId, durationSeconds: duration };
} }
else if (type === "COLOR_ROLE") { else if (type === EffectType.COLOR_ROLE) {
const roleId = interaction.fields.getTextInputValue("role_id"); const roleId = interaction.fields.getTextInputValue("role_id");
if (roleId) effect = { type: "COLOR_ROLE", roleId: roleId }; if (roleId) effect = { type: EffectType.COLOR_ROLE, roleId: roleId };
} }
if (effect) { if (effect) {

View File

@@ -10,12 +10,13 @@ import {
} from "discord.js"; } from "discord.js";
import { createBaseEmbed } from "@lib/embeds"; import { createBaseEmbed } from "@lib/embeds";
import type { DraftItem } from "./item_wizard.types"; import type { DraftItem } from "./item_wizard.types";
import { ItemType } from "@/lib/constants";
const getItemTypeOptions = () => [ const getItemTypeOptions = () => [
{ label: "Material", value: "MATERIAL", description: "Used for crafting or trading" }, { label: "Material", value: ItemType.MATERIAL, description: "Used for crafting or trading" },
{ label: "Consumable", value: "CONSUMABLE", description: "Can be used to gain effects" }, { label: "Consumable", value: ItemType.CONSUMABLE, description: "Can be used to gain effects" },
{ label: "Equipment", value: "EQUIPMENT", description: "Can be equipped (Not yet implemented)" }, { label: "Equipment", value: ItemType.EQUIPMENT, description: "Can be equipped (Not yet implemented)" },
{ label: "Quest Item", value: "QUEST", description: "Required for quests" }, { label: "Quest Item", value: ItemType.QUEST, description: "Required for quests" },
]; ];
const getEffectTypeOptions = () => [ const getEffectTypeOptions = () => [

View File

@@ -4,6 +4,7 @@ import { config } from "@/lib/config";
import { withTransaction } from "@/lib/db"; import { withTransaction } from "@/lib/db";
import type { Transaction } from "@/lib/types"; import type { Transaction } from "@/lib/types";
import { UserError } from "@/lib/errors"; import { UserError } from "@/lib/errors";
import { TimerType, TransactionType } from "@/lib/constants";
export const economyService = { export const economyService = {
transfer: async (fromUserId: string, toUserId: string, amount: bigint, tx?: Transaction) => { transfer: async (fromUserId: string, toUserId: string, amount: bigint, tx?: Transaction) => {
@@ -48,7 +49,7 @@ export const economyService = {
await txFn.insert(transactions).values({ await txFn.insert(transactions).values({
userId: BigInt(fromUserId), userId: BigInt(fromUserId),
amount: -amount, amount: -amount,
type: 'TRANSFER_OUT', type: TransactionType.TRANSFER_OUT,
description: `Transfer to ${toUserId}`, description: `Transfer to ${toUserId}`,
}); });
@@ -56,7 +57,7 @@ export const economyService = {
await txFn.insert(transactions).values({ await txFn.insert(transactions).values({
userId: BigInt(toUserId), userId: BigInt(toUserId),
amount: amount, amount: amount,
type: 'TRANSFER_IN', type: TransactionType.TRANSFER_IN,
description: `Transfer from ${fromUserId}`, description: `Transfer from ${fromUserId}`,
}); });
@@ -74,7 +75,7 @@ export const economyService = {
const cooldown = await txFn.query.userTimers.findFirst({ const cooldown = await txFn.query.userTimers.findFirst({
where: and( where: and(
eq(userTimers.userId, BigInt(userId)), eq(userTimers.userId, BigInt(userId)),
eq(userTimers.type, 'COOLDOWN'), eq(userTimers.type, TimerType.COOLDOWN),
eq(userTimers.key, 'daily') eq(userTimers.key, 'daily')
), ),
}); });
@@ -127,7 +128,7 @@ export const economyService = {
await txFn.insert(userTimers) await txFn.insert(userTimers)
.values({ .values({
userId: BigInt(userId), userId: BigInt(userId),
type: 'COOLDOWN', type: TimerType.COOLDOWN,
key: 'daily', key: 'daily',
expiresAt: nextReadyAt, expiresAt: nextReadyAt,
}) })
@@ -140,7 +141,7 @@ export const economyService = {
await txFn.insert(transactions).values({ await txFn.insert(transactions).values({
userId: BigInt(userId), userId: BigInt(userId),
amount: totalReward, amount: totalReward,
type: 'DAILY_REWARD', type: TransactionType.DAILY_REWARD,
description: `Daily reward (Streak: ${streak})`, description: `Daily reward (Streak: ${streak})`,
}); });

View File

@@ -5,6 +5,7 @@ import type { EffectHandler } from "./types";
import type { LootTableItem } from "@/lib/types"; import type { LootTableItem } from "@/lib/types";
import { inventoryService } from "@/modules/inventory/inventory.service"; import { inventoryService } from "@/modules/inventory/inventory.service";
import { inventory, items } from "@/db/schema"; import { inventory, items } from "@/db/schema";
import { TimerType, TransactionType, LootType } from "@/lib/constants";
// Helper to extract duration in seconds // Helper to extract duration in seconds
@@ -20,7 +21,7 @@ export const handleAddXp: EffectHandler = async (userId, effect, txFn) => {
}; };
export const handleAddBalance: EffectHandler = async (userId, effect, txFn) => { export const handleAddBalance: EffectHandler = async (userId, effect, txFn) => {
await economyService.modifyUserBalance(userId, BigInt(effect.amount), 'ITEM_USE', `Used Item`, null, txFn); await economyService.modifyUserBalance(userId, BigInt(effect.amount), TransactionType.ITEM_USE, `Used Item`, null, txFn);
return `Gained ${effect.amount} 🪙`; return `Gained ${effect.amount} 🪙`;
}; };
@@ -33,7 +34,7 @@ export const handleXpBoost: EffectHandler = async (userId, effect, txFn) => {
const expiresAt = new Date(Date.now() + boostDuration * 1000); const expiresAt = new Date(Date.now() + boostDuration * 1000);
await txFn.insert(userTimers).values({ await txFn.insert(userTimers).values({
userId: BigInt(userId), userId: BigInt(userId),
type: 'EFFECT', type: TimerType.EFFECT,
key: 'xp_boost', key: 'xp_boost',
expiresAt: expiresAt, expiresAt: expiresAt,
metadata: { multiplier: effect.multiplier } metadata: { multiplier: effect.multiplier }
@@ -49,7 +50,7 @@ export const handleTempRole: EffectHandler = async (userId, effect, txFn) => {
const roleExpiresAt = new Date(Date.now() + roleDuration * 1000); const roleExpiresAt = new Date(Date.now() + roleDuration * 1000);
await txFn.insert(userTimers).values({ await txFn.insert(userTimers).values({
userId: BigInt(userId), userId: BigInt(userId),
type: 'ACCESS', type: TimerType.ACCESS,
key: `role_${effect.roleId}`, key: `role_${effect.roleId}`,
expiresAt: roleExpiresAt, expiresAt: roleExpiresAt,
metadata: { roleId: effect.roleId } metadata: { roleId: effect.roleId }
@@ -84,22 +85,22 @@ export const handleLootbox: EffectHandler = async (userId, effect, txFn) => {
if (!winner) return "The box is empty..."; // Should not happen if (!winner) return "The box is empty..."; // Should not happen
// Process Winner // Process Winner
if (winner.type === 'NOTHING') { if (winner.type === LootType.NOTHING) {
return winner.message || "You found nothing inside."; return winner.message || "You found nothing inside.";
} }
if (winner.type === 'CURRENCY') { if (winner.type === LootType.CURRENCY) {
let amount = winner.amount || 0; let amount = winner.amount || 0;
if (winner.minAmount && winner.maxAmount) { if (winner.minAmount && winner.maxAmount) {
amount = Math.floor(Math.random() * (winner.maxAmount - winner.minAmount + 1)) + winner.minAmount; amount = Math.floor(Math.random() * (winner.maxAmount - winner.minAmount + 1)) + winner.minAmount;
} }
if (amount > 0) { if (amount > 0) {
await economyService.modifyUserBalance(userId, BigInt(amount), 'LOOTBOX', 'Lootbox Reward', null, txFn); await economyService.modifyUserBalance(userId, BigInt(amount), TransactionType.LOOTBOX, 'Lootbox Reward', null, txFn);
return winner.message || `You found ${amount} 🪙!`; return winner.message || `You found ${amount} 🪙!`;
} }
} }
if (winner.type === 'XP') { if (winner.type === LootType.XP) {
let amount = winner.amount || 0; let amount = winner.amount || 0;
if (winner.minAmount && winner.maxAmount) { if (winner.minAmount && winner.maxAmount) {
amount = Math.floor(Math.random() * (winner.maxAmount - winner.minAmount + 1)) + winner.minAmount; amount = Math.floor(Math.random() * (winner.maxAmount - winner.minAmount + 1)) + winner.minAmount;
@@ -110,7 +111,7 @@ export const handleLootbox: EffectHandler = async (userId, effect, txFn) => {
} }
} }
if (winner.type === 'ITEM') { if (winner.type === LootType.ITEM) {
if (winner.itemId) { if (winner.itemId) {
const quantity = BigInt(winner.amount || 1); const quantity = BigInt(winner.amount || 1);

View File

@@ -7,6 +7,7 @@ import { config } from "@/lib/config";
import { UserError } from "@/lib/errors"; import { UserError } from "@/lib/errors";
import { withTransaction } from "@/lib/db"; import { withTransaction } from "@/lib/db";
import type { Transaction, ItemUsageData } from "@/lib/types"; import type { Transaction, ItemUsageData } from "@/lib/types";
import { TransactionType } from "@/lib/constants";
@@ -121,7 +122,7 @@ export const inventoryService = {
const totalPrice = item.price * quantity; const totalPrice = item.price * quantity;
// Deduct Balance using economy service (passing tx ensures atomicity) // Deduct Balance using economy service (passing tx ensures atomicity)
await economyService.modifyUserBalance(userId, -totalPrice, 'PURCHASE', `Bought ${quantity}x ${item.name}`, null, txFn); await economyService.modifyUserBalance(userId, -totalPrice, TransactionType.PURCHASE, `Bought ${quantity}x ${item.name}`, null, txFn);
await inventoryService.addItem(userId, itemId, quantity, txFn); await inventoryService.addItem(userId, itemId, quantity, txFn);

View File

@@ -1,5 +1,6 @@
import { EmbedBuilder } from "discord.js"; import { EmbedBuilder } from "discord.js";
import type { ItemUsageData } from "@/lib/types"; import type { ItemUsageData } from "@/lib/types";
import { EffectType } from "@/lib/constants";
/** /**
* Inventory entry with item details * Inventory entry with item details
@@ -34,7 +35,7 @@ export function getItemUseResultEmbed(results: string[], item?: { name: string,
const description = results.map(r => `${r}`).join("\n"); const description = results.map(r => `${r}`).join("\n");
// Check if it was a lootbox // Check if it was a lootbox
const isLootbox = item?.usageData?.effects?.some((e: any) => e.type === 'LOOTBOX'); const isLootbox = item?.usageData?.effects?.some((e: any) => e.type === EffectType.LOOTBOX);
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setDescription(description) .setDescription(description)

View File

@@ -3,6 +3,7 @@ import { eq, sql, and } from "drizzle-orm";
import { withTransaction } from "@/lib/db"; import { withTransaction } from "@/lib/db";
import { config } from "@/lib/config"; import { config } from "@/lib/config";
import type { Transaction } from "@/lib/types"; import type { Transaction } from "@/lib/types";
import { TimerType } from "@/lib/constants";
export const levelingService = { export const levelingService = {
// Calculate total XP required to REACH a specific level (Cumulative) // Calculate total XP required to REACH a specific level (Cumulative)
@@ -78,7 +79,7 @@ export const levelingService = {
const cooldown = await txFn.query.userTimers.findFirst({ const cooldown = await txFn.query.userTimers.findFirst({
where: and( where: and(
eq(userTimers.userId, BigInt(id)), eq(userTimers.userId, BigInt(id)),
eq(userTimers.type, 'COOLDOWN'), eq(userTimers.type, TimerType.COOLDOWN),
eq(userTimers.key, 'chat_xp') eq(userTimers.key, 'chat_xp')
), ),
}); });
@@ -95,7 +96,7 @@ export const levelingService = {
const xpBoost = await txFn.query.userTimers.findFirst({ const xpBoost = await txFn.query.userTimers.findFirst({
where: and( where: and(
eq(userTimers.userId, BigInt(id)), eq(userTimers.userId, BigInt(id)),
eq(userTimers.type, 'EFFECT'), eq(userTimers.type, TimerType.EFFECT),
eq(userTimers.key, 'xp_boost') eq(userTimers.key, 'xp_boost')
) )
}); });
@@ -114,7 +115,7 @@ export const levelingService = {
await txFn.insert(userTimers) await txFn.insert(userTimers)
.values({ .values({
userId: BigInt(id), userId: BigInt(id),
type: 'COOLDOWN', type: TimerType.COOLDOWN,
key: 'chat_xp', key: 'chat_xp',
expiresAt: nextReadyAt, expiresAt: nextReadyAt,
}) })

View File

@@ -2,6 +2,7 @@
import { describe, it, expect, mock, beforeEach } from "bun:test"; import { describe, it, expect, mock, beforeEach } from "bun:test";
import { ModerationService } from "./moderation.service"; import { ModerationService } from "./moderation.service";
import { moderationCases } from "@/db/schema"; import { moderationCases } from "@/db/schema";
import { CaseType } from "@/lib/constants";
// Mock Drizzle Functions // Mock Drizzle Functions
const mockFindFirst = mock(); const mockFindFirst = mock();
@@ -83,7 +84,7 @@ describe("ModerationService", () => {
it("should issue a warning and attempt to DM the user", async () => { it("should issue a warning and attempt to DM the user", async () => {
mockFindFirst.mockResolvedValue({ caseId: "CASE-0001" }); mockFindFirst.mockResolvedValue({ caseId: "CASE-0001" });
mockReturning.mockResolvedValue([{ caseId: "CASE-0002" }]); mockReturning.mockResolvedValue([{ caseId: "CASE-0002" }]);
mockFindMany.mockResolvedValue([{ type: 'warn', active: true }]); // 1 warning total mockFindMany.mockResolvedValue([{ type: CaseType.WARN, active: true }]); // 1 warning total
const mockDmTarget = { send: mock() }; const mockDmTarget = { send: mock() };
@@ -178,7 +179,7 @@ describe("ModerationService", () => {
mockFindFirst.mockResolvedValue({ caseId: "CASE-0001" }); mockFindFirst.mockResolvedValue({ caseId: "CASE-0001" });
const mockNewCase = { const mockNewCase = {
caseId: "CASE-0002", caseId: "CASE-0002",
type: 'warn', type: CaseType.WARN,
userId: 123456789n, userId: 123456789n,
username: "testuser", username: "testuser",
moderatorId: 987654321n, moderatorId: 987654321n,
@@ -190,7 +191,7 @@ describe("ModerationService", () => {
mockReturning.mockResolvedValue([mockNewCase]); mockReturning.mockResolvedValue([mockNewCase]);
const result = await ModerationService.createCase({ const result = await ModerationService.createCase({
type: 'warn', type: CaseType.WARN,
userId: "123456789", userId: "123456789",
username: "testuser", username: "testuser",
moderatorId: "987654321", moderatorId: "987654321",
@@ -202,7 +203,7 @@ describe("ModerationService", () => {
expect(mockInsert).toHaveBeenCalled(); expect(mockInsert).toHaveBeenCalled();
expect(mockValues).toHaveBeenCalledWith(expect.objectContaining({ expect(mockValues).toHaveBeenCalledWith(expect.objectContaining({
caseId: "CASE-0002", caseId: "CASE-0002",
type: 'warn', type: CaseType.WARN,
userId: 123456789n, userId: 123456789n,
reason: "test reason" reason: "test reason"
})); }));
@@ -213,7 +214,7 @@ describe("ModerationService", () => {
mockReturning.mockImplementation((values) => [values]); // Simplified mock mockReturning.mockImplementation((values) => [values]); // Simplified mock
const result = await ModerationService.createCase({ const result = await ModerationService.createCase({
type: 'ban', type: CaseType.BAN,
userId: "123456789", userId: "123456789",
username: "testuser", username: "testuser",
moderatorId: "987654321", moderatorId: "987654321",
@@ -273,8 +274,8 @@ describe("ModerationService", () => {
describe("getActiveWarningCount", () => { describe("getActiveWarningCount", () => {
it("should return the number of active warnings", async () => { it("should return the number of active warnings", async () => {
mockFindMany.mockResolvedValue([ mockFindMany.mockResolvedValue([
{ id: 1n, type: 'warn', active: true }, { id: 1n, type: CaseType.WARN, active: true },
{ id: 2n, type: 'warn', active: true } { id: 2n, type: CaseType.WARN, active: true }
]); ]);
const count = await ModerationService.getActiveWarningCount("123456789"); const count = await ModerationService.getActiveWarningCount("123456789");

View File

@@ -4,6 +4,7 @@ import { DrizzleClient } from "@/lib/DrizzleClient";
import type { CreateCaseOptions, ClearCaseOptions, SearchCasesFilter } from "./moderation.types"; import type { CreateCaseOptions, ClearCaseOptions, SearchCasesFilter } from "./moderation.types";
import { config } from "@/lib/config"; import { config } from "@/lib/config";
import { getUserWarningEmbed } from "./moderation.view"; import { getUserWarningEmbed } from "./moderation.view";
import { CaseType } from "@/lib/constants";
export class ModerationService { export class ModerationService {
/** /**
@@ -43,7 +44,7 @@ export class ModerationService {
moderatorName: options.moderatorName, moderatorName: options.moderatorName,
reason: options.reason, reason: options.reason,
metadata: options.metadata || {}, metadata: options.metadata || {},
active: options.type === 'warn' ? true : false, // Only warnings are "active" by default active: options.type === CaseType.WARN ? true : false, // Only warnings are "active" by default
}).returning(); }).returning();
return newCase; return newCase;
@@ -63,7 +64,7 @@ export class ModerationService {
timeoutTarget?: { timeout: (duration: number, reason: string) => Promise<any> }; timeoutTarget?: { timeout: (duration: number, reason: string) => Promise<any> };
}) { }) {
const moderationCase = await this.createCase({ const moderationCase = await this.createCase({
type: 'warn', type: CaseType.WARN,
userId: options.userId, userId: options.userId,
username: options.username, username: options.username,
moderatorId: options.moderatorId, moderatorId: options.moderatorId,
@@ -105,7 +106,7 @@ export class ModerationService {
// Create a timeout case // Create a timeout case
await this.createCase({ await this.createCase({
type: 'timeout', type: CaseType.TIMEOUT,
userId: options.userId, userId: options.userId,
username: options.username, username: options.username,
moderatorId: "0", // System/Bot moderatorId: "0", // System/Bot
@@ -154,7 +155,7 @@ export class ModerationService {
return await DrizzleClient.query.moderationCases.findMany({ return await DrizzleClient.query.moderationCases.findMany({
where: and( where: and(
eq(moderationCases.userId, BigInt(userId)), eq(moderationCases.userId, BigInt(userId)),
eq(moderationCases.type, 'warn'), eq(moderationCases.type, CaseType.WARN),
eq(moderationCases.active, true) eq(moderationCases.active, true)
), ),
orderBy: [desc(moderationCases.createdAt)], orderBy: [desc(moderationCases.createdAt)],
@@ -168,7 +169,7 @@ export class ModerationService {
return await DrizzleClient.query.moderationCases.findMany({ return await DrizzleClient.query.moderationCases.findMany({
where: and( where: and(
eq(moderationCases.userId, BigInt(userId)), eq(moderationCases.userId, BigInt(userId)),
eq(moderationCases.type, 'note') eq(moderationCases.type, CaseType.NOTE)
), ),
orderBy: [desc(moderationCases.createdAt)], orderBy: [desc(moderationCases.createdAt)],
}); });

View File

@@ -1,4 +1,6 @@
export type CaseType = 'warn' | 'timeout' | 'kick' | 'ban' | 'note' | 'prune'; import { CaseType } from "@/lib/constants";
export { CaseType };
export interface CreateCaseOptions { export interface CreateCaseOptions {
type: CaseType; type: CaseType;

View File

@@ -6,6 +6,7 @@ import { economyService } from "@/modules/economy/economy.service";
import { levelingService } from "@/modules/leveling/leveling.service"; import { levelingService } from "@/modules/leveling/leveling.service";
import { withTransaction } from "@/lib/db"; import { withTransaction } from "@/lib/db";
import type { Transaction } from "@/lib/types"; import type { Transaction } from "@/lib/types";
import { TransactionType } from "@/lib/constants";
export const questService = { export const questService = {
assignQuest: async (userId: string, questId: number, tx?: Transaction) => { assignQuest: async (userId: string, questId: number, tx?: Transaction) => {
@@ -62,7 +63,7 @@ export const questService = {
if (rewards?.balance) { if (rewards?.balance) {
const bal = BigInt(rewards.balance); const bal = BigInt(rewards.balance);
await economyService.modifyUserBalance(userId, bal, 'QUEST_REWARD', `Reward for quest ${questId}`, null, txFn); await economyService.modifyUserBalance(userId, bal, TransactionType.QUEST_REWARD, `Reward for quest ${questId}`, null, txFn);
results.balance = bal; results.balance = bal;
} }

View File

@@ -4,6 +4,7 @@ import { DrizzleClient } from "@/lib/DrizzleClient";
import { AuroraClient } from "@/lib/BotClient"; import { AuroraClient } from "@/lib/BotClient";
import { env } from "@/lib/env"; import { env } from "@/lib/env";
import { config } from "@/lib/config"; import { config } from "@/lib/config";
import { TimerType } from "@/lib/constants";
export const cleanupService = { export const cleanupService = {
/** /**
@@ -57,7 +58,7 @@ export const cleanupService = {
// This is migrated from scheduler.ts // This is migrated from scheduler.ts
const expiredAccess = await DrizzleClient.query.userTimers.findMany({ const expiredAccess = await DrizzleClient.query.userTimers.findMany({
where: and( where: and(
eq(userTimers.type, 'ACCESS'), eq(userTimers.type, TimerType.ACCESS),
lt(userTimers.expiresAt, now) lt(userTimers.expiresAt, now)
) )
}); });

View File

@@ -4,6 +4,7 @@ import { inventoryService } from "@/modules/inventory/inventory.service";
import { itemTransactions } from "@/db/schema"; import { itemTransactions } from "@/db/schema";
import { withTransaction } from "@/lib/db"; import { withTransaction } from "@/lib/db";
import type { Transaction } from "@/lib/types"; import type { Transaction } from "@/lib/types";
import { TransactionType, ItemTransactionType } from "@/lib/constants";
// Module-level session storage // Module-level session storage
const sessions = new Map<string, TradeSession>(); const sessions = new Map<string, TradeSession>();
@@ -25,7 +26,7 @@ const processTransfer = async (tx: Transaction, from: TradeParticipant, to: Trad
await economyService.modifyUserBalance( await economyService.modifyUserBalance(
from.id, from.id,
-from.offer.money, -from.offer.money,
'TRADE_OUT', TransactionType.TRADE_OUT,
`Trade with ${to.username} (Thread: ${threadId})`, `Trade with ${to.username} (Thread: ${threadId})`,
to.id, to.id,
tx tx
@@ -33,7 +34,7 @@ const processTransfer = async (tx: Transaction, from: TradeParticipant, to: Trad
await economyService.modifyUserBalance( await economyService.modifyUserBalance(
to.id, to.id,
from.offer.money, from.offer.money,
'TRADE_IN', TransactionType.TRADE_IN,
`Trade with ${from.username} (Thread: ${threadId})`, `Trade with ${from.username} (Thread: ${threadId})`,
from.id, from.id,
tx tx
@@ -54,7 +55,7 @@ const processTransfer = async (tx: Transaction, from: TradeParticipant, to: Trad
relatedUserId: BigInt(to.id), relatedUserId: BigInt(to.id),
itemId: item.id, itemId: item.id,
quantity: -item.quantity, quantity: -item.quantity,
type: 'TRADE_OUT', type: ItemTransactionType.TRADE_OUT,
description: `Traded to ${to.username}`, description: `Traded to ${to.username}`,
}); });
@@ -64,7 +65,7 @@ const processTransfer = async (tx: Transaction, from: TradeParticipant, to: Trad
relatedUserId: BigInt(from.id), relatedUserId: BigInt(from.id),
itemId: item.id, itemId: item.id,
quantity: item.quantity, quantity: item.quantity,
type: 'TRADE_IN', type: ItemTransactionType.TRADE_IN,
description: `Received from ${from.username}`, description: `Received from ${from.username}`,
}); });
} }

View File

@@ -1,8 +1,9 @@
import { userTimers } from "@/db/schema"; import { userTimers } from "@/db/schema";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import { DrizzleClient } from "@/lib/DrizzleClient"; import { DrizzleClient } from "@/lib/DrizzleClient";
import { TimerType } from "@/lib/constants";
export type TimerType = 'COOLDOWN' | 'EFFECT' | 'ACCESS'; export { TimerType };
export const userTimerService = { export const userTimerService = {
/** /**