feat: enforce active quest limit in assignQuest

Adds a limit check to assignQuest that reads maxActiveQuests from game
settings and throws a UserError when the user has reached their active
quest limit. Completed quests are excluded from the count.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
syntaxbullet
2026-03-28 15:39:23 +01:00
parent 4ead7e60b1
commit 912ce5b942
2 changed files with 65 additions and 1 deletions

View File

@@ -75,6 +75,12 @@ describe("questService", () => {
describe("assignQuest", () => { describe("assignQuest", () => {
it("should assign quest", async () => { it("should assign quest", async () => {
const { gameSettingsService } = await import("@shared/modules/game-settings/game-settings.service");
const mockGetSettings = spyOn(gameSettingsService, 'getSettings').mockResolvedValue({
quest: { maxActiveQuests: 3 },
} as any);
mockFindMany.mockResolvedValue([]); // no active quests
mockReturning.mockResolvedValue([{ userId: 1n, questId: 101 }]); mockReturning.mockResolvedValue([{ userId: 1n, questId: 101 }]);
const result = await questService.assignQuest("1", 101); const result = await questService.assignQuest("1", 101);
@@ -86,6 +92,47 @@ describe("questService", () => {
questId: 101, questId: 101,
progress: 0 progress: 0
}); });
mockGetSettings.mockRestore();
});
it("should throw when user has reached active quest limit", async () => {
// Mock gameSettingsService to return maxActiveQuests: 2
const { gameSettingsService } = await import("@shared/modules/game-settings/game-settings.service");
const mockGetSettings = spyOn(gameSettingsService, 'getSettings').mockResolvedValue({
quest: { maxActiveQuests: 2 },
} as any);
// User has 2 incomplete quests
mockFindMany.mockResolvedValue([
{ userId: 1n, questId: 1, completedAt: null },
{ userId: 1n, questId: 2, completedAt: null },
]);
expect(questService.assignQuest("1", 3)).rejects.toThrow(
"You can only have 2 active quests at a time. Complete a quest before accepting a new one."
);
mockGetSettings.mockRestore();
});
it("should allow assignment when completed quests don't count toward limit", async () => {
const { gameSettingsService } = await import("@shared/modules/game-settings/game-settings.service");
const mockGetSettings = spyOn(gameSettingsService, 'getSettings').mockResolvedValue({
quest: { maxActiveQuests: 2 },
} as any);
// User has 2 quests but 1 is completed
mockFindMany.mockResolvedValue([
{ userId: 1n, questId: 1, completedAt: null },
{ userId: 1n, questId: 2, completedAt: new Date() },
]);
mockReturning.mockResolvedValue([{ userId: 1n, questId: 3 }]);
const result = await questService.assignQuest("1", 3);
expect(result).toEqual([{ userId: 1n, questId: 3 }]);
mockGetSettings.mockRestore();
}); });
}); });

View File

@@ -4,6 +4,7 @@ import { UserError } from "@shared/lib/errors";
import { DrizzleClient } from "@shared/db/DrizzleClient"; import { DrizzleClient } from "@shared/db/DrizzleClient";
import { economyService } from "@shared/modules/economy/economy.service"; import { economyService } from "@shared/modules/economy/economy.service";
import { levelingService } from "@shared/modules/leveling/leveling.service"; import { levelingService } from "@shared/modules/leveling/leveling.service";
import { gameSettingsService } from "@shared/modules/game-settings/game-settings.service";
import { withTransaction } from "@/lib/db"; import { withTransaction } from "@/lib/db";
import type { Transaction } from "@shared/lib/types"; import type { Transaction } from "@shared/lib/types";
import { TransactionType } from "@shared/lib/constants"; import { TransactionType } from "@shared/lib/constants";
@@ -12,13 +13,29 @@ import { systemEvents, EVENTS } from "@shared/lib/events";
export const questService = { export const questService = {
assignQuest: async (userId: string, questId: number, tx?: Transaction) => { assignQuest: async (userId: string, questId: number, tx?: Transaction) => {
return await withTransaction(async (txFn) => { return await withTransaction(async (txFn) => {
// Check active quest limit
const settings = await gameSettingsService.getSettings();
const maxActive = settings?.quest?.maxActiveQuests ?? 3;
const userQuestList = await txFn.query.userQuests.findMany({
where: eq(userQuests.userId, BigInt(userId)),
});
const activeCount = userQuestList.filter(uq => !uq.completedAt).length;
if (activeCount >= maxActive) {
throw new UserError(
`You can only have ${maxActive} active quests at a time. Complete a quest before accepting a new one.`
);
}
return await txFn.insert(userQuests) return await txFn.insert(userQuests)
.values({ .values({
userId: BigInt(userId), userId: BigInt(userId),
questId: questId, questId: questId,
progress: 0, progress: 0,
}) })
.onConflictDoNothing() // Ignore if already assigned .onConflictDoNothing()
.returning(); .returning();
}, tx); }, tx);
}, },