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:
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user