Keep the tournament link of badge wins when a badge's owners are edited

This commit is contained in:
Kalle
2026-11-08 16:27:36 +02:00
parent 74ce542593
commit 7ef8bc6dba
2 changed files with 122 additions and 13 deletions

View File

@@ -1,7 +1,10 @@
import * as R from "remeda";
import { beforeEach, describe, expect, test } from "vitest";
import * as BadgeFactory from "~/db/seed/factories/BadgeFactory";
import * as TournamentFactory from "~/db/seed/factories/TournamentFactory";
import * as UserFactory from "~/db/seed/factories/UserFactory";
import * as XRankPlacementFactory from "~/db/seed/factories/XRankPlacementFactory";
import * as FeedRepository from "~/features/feed/FeedRepository.server";
import * as BadgeRepository from "./BadgeRepository.server";
import { SPLATOON_3_XP_BADGE_VALUES } from "./badges-constants";
@@ -73,8 +76,76 @@ describe("replaceOwners", () => {
const updated = await BadgeRepository.findById(badge.id);
expect(updated?.owners).toHaveLength(0);
});
describe("with a badge won at a tournament", () => {
const users = UserFactory.pool();
const winnerId = () => users.id(1);
const loserId = () => users.id(2);
const addedByManagerId = () => users.id(3);
let badgeId: number;
beforeEach(async () => {
await users.create(3);
badgeId = (await BadgeFactory.create()).id;
await TournamentFactory.createPlayed(
{ authorId: winnerId(), minMembersPerTeam: 1, badges: [badgeId] },
{ teamRosters: [[winnerId()], [loserId()]], playedOut: "all" },
);
});
test("keeps the win tied to the tournament when another owner is added", async () => {
await BadgeRepository.replaceOwners({
badgeId,
ownerIds: [winnerId(), addedByManagerId()],
});
expect(await tournamentWinnerIds()).toEqual([winnerId()]);
expect(await ownerCounts(badgeId)).toEqual([
{ id: winnerId(), count: 1 },
{ id: addedByManagerId(), count: 1 },
]);
});
test("grants extra copies to a winner on top of their tournament win", async () => {
await BadgeRepository.replaceOwners({
badgeId,
ownerIds: [winnerId(), winnerId(), winnerId()],
});
expect(await tournamentWinnerIds()).toEqual([winnerId()]);
expect(await ownerCounts(badgeId)).toEqual([
{ id: winnerId(), count: 3 },
]);
});
test("removes the win when the winner is removed from the owners", async () => {
await BadgeRepository.replaceOwners({
badgeId,
ownerIds: [addedByManagerId()],
});
expect(await tournamentWinnerIds()).toEqual([]);
expect(await ownerCounts(badgeId)).toEqual([
{ id: addedByManagerId(), count: 1 },
]);
});
});
});
async function tournamentWinnerIds() {
const owners =
await FeedRepository.findAllTournamentBadgeOwnersStartedAfter(0);
return owners.map((owner) => owner.userId);
}
async function ownerCounts(badgeId: number) {
const badge = await BadgeRepository.findById(badgeId);
return R.sortBy(
badge!.owners.map(({ id, count }) => ({ id, count })),
(owner) => owner.id,
);
}
/** Gives the user a linked X Rank player whose one placement is worth `power`. */
const givePeakXp = (userId: number, power: number) =>
XRankPlacementFactory.create(

View File

@@ -196,6 +196,12 @@ export function replaceManagers({
});
}
/**
* Sets who owns the badge and how many times, one `ownerIds` entry per copy. Copies won at a
* tournament keep their tournament link: a user's tournament wins count first towards their new
* total and the rest is a manual grant. Lowering a total below the user's wins removes the most
* recent ones.
*/
export function replaceOwners({
badgeId,
ownerIds,
@@ -204,26 +210,58 @@ export function replaceOwners({
ownerIds: number[];
}) {
return db.transaction().execute(async (trx) => {
await trx
.deleteFrom("TournamentBadgeOwner")
const newCounts = new Map<number, number>();
for (const userId of ownerIds) {
newCounts.set(userId, (newCounts.get(userId) ?? 0) + 1);
}
const tournamentWins = await trx
.selectFrom("TournamentBadgeOwner")
.select(["userId", "tournamentId"])
.where("badgeId", "=", badgeId)
.where("tournamentId", "is not", null)
.$narrowType<{ tournamentId: NotNull }>()
.orderBy("tournamentId", "asc")
.execute();
const counts = new Map<number, number>();
for (const userId of ownerIds) {
counts.set(userId, (counts.get(userId) ?? 0) + 1);
const keptWinCounts = new Map<number, number>();
const removedWins: typeof tournamentWins = [];
for (const win of tournamentWins) {
const keptCount = keptWinCounts.get(win.userId) ?? 0;
if (keptCount < (newCounts.get(win.userId) ?? 0)) {
keptWinCounts.set(win.userId, keptCount + 1);
} else {
removedWins.push(win);
}
}
await trx
.insertInto("TournamentBadgeOwner")
.values(
Array.from(counts, ([userId, count]) => ({
badgeId,
userId,
count,
})),
)
.deleteFrom("TournamentBadgeOwner")
.where("badgeId", "=", badgeId)
.where("tournamentId", "is", null)
.execute();
for (const win of removedWins) {
await trx
.deleteFrom("TournamentBadgeOwner")
.where("badgeId", "=", badgeId)
.where("userId", "=", win.userId)
.where("tournamentId", "=", win.tournamentId)
.execute();
}
const manualGrants = Array.from(newCounts, ([userId, count]) => ({
badgeId,
userId,
count: count - (keptWinCounts.get(userId) ?? 0),
})).filter((grant) => grant.count > 0);
if (manualGrants.length > 0) {
await trx
.insertInto("TournamentBadgeOwner")
.values(manualGrants)
.execute();
}
});
}