diff --git a/apps/api/src/guild/guild.service.ts b/apps/api/src/guild/guild.service.ts index a13129c3d..a8492b9c7 100644 --- a/apps/api/src/guild/guild.service.ts +++ b/apps/api/src/guild/guild.service.ts @@ -22,6 +22,24 @@ import { PlayerService } from "#player"; import { flatten } from "@statsify/util"; import type { ReturnModelType } from "@typegoose/typegoose"; +function getExpHistory(days: string[] = [], expHistory: number[] = []) { + return Object.fromEntries(days.map((day, index) => [day, expHistory[index] ?? 0])); +} + +function mergeExpHistory( + cachedDays: string[] | undefined, + cachedExpHistory: number[] | undefined, + currentExpHistory: Record +) { + const combinedExpHistory = getExpHistory(cachedDays, cachedExpHistory); + + Object.entries(currentExpHistory).forEach(([day, exp]) => { + combinedExpHistory[day] = Math.max(combinedExpHistory[day] ?? 0, exp); + }); + + return combinedExpHistory; +} + @Injectable() export class GuildService { private readonly logger = new Logger("GuildService"); @@ -84,10 +102,7 @@ export class GuildService { // Merge the exp history from hypixel and the cached guild const combinedExpHistory: Record = { - ...cacheMember?.expHistoryDays?.reduce( - (acc, day, index) => ({ ...acc, [day]: cacheMember.expHistory[index] }), - {} - ), + ...getExpHistory(cacheMember?.expHistoryDays, cacheMember?.expHistory), ...Object.fromEntries( member.expHistoryDays.map((day, index) => [day, member.expHistory[index]]) ), @@ -117,8 +132,14 @@ export class GuildService { .lean() .exec(); + const combinedGuildExpHistory = mergeExpHistory( + cachedGuild?.expHistoryDays, + cachedGuild?.expHistory, + guildExpHistory + ); + // Get scaled gexp - Object.entries(guildExpHistory) + Object.entries(combinedGuildExpHistory) .sort() .toReversed() .slice(0, 30) @@ -261,3 +282,26 @@ export class GuildService { return Math.round((exp - 700_000) / 33 + 250_000); } } + +if (import.meta.vitest) { + const { suite, it, expect } = import.meta.vitest; + + suite("GuildService", () => { + it("preserves cached guild exp history when refreshed member totals are lower", () => { + const combined = mergeExpHistory( + ["2026-05-12", "2026-05-11", "2026-05-10"], + [500, 400, 300], + { + "2026-05-12": 250, + "2026-05-11": 450, + } + ); + + expect(combined).toEqual({ + "2026-05-12": 500, + "2026-05-11": 450, + "2026-05-10": 300, + }); + }); + }); +} diff --git a/apps/api/src/guild/leaderboards/guild-leaderboard.service.ts b/apps/api/src/guild/leaderboards/guild-leaderboard.service.ts index 430f00294..674b34914 100644 --- a/apps/api/src/guild/leaderboards/guild-leaderboard.service.ts +++ b/apps/api/src/guild/leaderboards/guild-leaderboard.service.ts @@ -56,6 +56,7 @@ export class GuildLeaderboardService extends LeaderboardService { }, {} as Record); selector.nameFormatted = true; + selector.name = true; return await Promise.all( ids.map(async (id) => { @@ -67,8 +68,8 @@ export class GuildLeaderboardService extends LeaderboardService { .lean() .exec(); - const additionalStats = flatten(guild) as LeaderboardAdditionalStats; - additionalStats.name = additionalStats.nameFormatted; + const additionalStats = flatten(guild ?? {}) as LeaderboardAdditionalStats; + additionalStats.name = additionalStats.nameFormatted ?? guild?.name ?? id; return additionalStats; }) diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json index f1e6b476d..06810fb21 100644 --- a/apps/api/tsconfig.json +++ b/apps/api/tsconfig.json @@ -2,6 +2,7 @@ "extends": "../../tsconfig.base.json", "include": [ "src", - "eslint.config.js" + "eslint.config.js", + "vitest.config.ts" ] -} \ No newline at end of file +} diff --git a/apps/api/vitest.config.ts b/apps/api/vitest.config.ts new file mode 100644 index 000000000..2f7603e59 --- /dev/null +++ b/apps/api/vitest.config.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) Statsify + * + * This source code is licensed under the GNU GPL v3 license found in the + * LICENSE file in the root directory of this source tree. + * https://github.com/Statsify/statsify/blob/main/LICENSE + */ + +import { config } from "../../vitest.shared.js"; + +export default await config();