Only count saved tournaments that haven't started towards the saved limit

This commit is contained in:
Kalle
2026-11-08 12:14:42 +02:00
parent 1c2b73cb45
commit 5c75dfbd41
5 changed files with 105 additions and 6 deletions

View File

@@ -2,6 +2,7 @@ import { db } from "~/db/sql";
import { actorId } from "~/features/auth/core/user.server";
import type { ShowcaseCalendarEvent } from "~/features/calendar/calendar-types";
import * as ShowcaseTournaments from "~/features/front-page/core/ShowcaseTournaments.server";
import { databaseTimestampNow } from "~/utils/dates";
export function saveOwn(tournamentId: number) {
return db
@@ -58,11 +59,18 @@ export async function isSaved({
return Boolean(row);
}
export async function countByUserId(userId: number): Promise<number> {
/** Counts the saved tournaments of the user that haven't started yet. */
export async function countUpcomingByUserId(userId: number): Promise<number> {
const result = await db
.selectFrom("SavedCalendarEvent")
.select((eb) => eb.fn.countAll<number>().as("count"))
.where("userId", "=", userId)
.where("SavedCalendarEvent.userId", "=", userId)
.where("SavedCalendarEvent.calendarEventId", "in", (eb) =>
eb
.selectFrom("CalendarEventDate")
.select("CalendarEventDate.eventId")
.where("CalendarEventDate.startsAt", ">", databaseTimestampNow()),
)
.executeTakeFirstOrThrow();
return result.count;

View File

@@ -0,0 +1,83 @@
import { addDays, subDays } from "date-fns";
import { beforeEach, describe, expect, test } from "vitest";
import * as SavedCalendarEventFactory from "~/db/seed/factories/SavedCalendarEventFactory";
import * as TournamentFactory from "~/db/seed/factories/TournamentFactory";
import * as UserFactory from "~/db/seed/factories/UserFactory";
import { dateToDatabaseTimestamp } from "~/utils/dates";
import { wrappedAction } from "~/utils/Test";
import * as SavedCalendarEventRepository from "../SavedCalendarEventRepository.server";
import { TOURNAMENT } from "../tournament-constants";
import type { saveTournamentSchema } from "../tournament-schemas";
import { action as infoAction } from "./to.$id.info.server";
const info = wrappedAction<typeof saveTournamentSchema>({ action: infoAction });
const users = UserFactory.pool();
const saverId = () => users.id(1);
describe("SAVE_TOURNAMENT", () => {
beforeEach(async () => {
await users.create(1);
});
test("tournaments saved in the past don't count towards the saved limit", async () => {
for (let i = 0; i < TOURNAMENT.MAX_SAVED_COUNT; i++) {
await saveTournamentStartingAt(subDays(new Date(), 30 + i));
}
const upcoming = await createTournamentStartingAt(addDays(new Date(), 7));
const response = await save(upcoming.id);
expect(errorToastOf(response)).toBeNull();
expect(await isSaved(upcoming.id)).toBe(true);
});
test("rejects saving once the limit of upcoming saved tournaments is reached", async () => {
for (let i = 0; i < TOURNAMENT.MAX_SAVED_COUNT; i++) {
await saveTournamentStartingAt(addDays(new Date(), 1 + i));
}
const upcoming = await createTournamentStartingAt(addDays(new Date(), 30));
const response = await save(upcoming.id);
expect(errorToastOf(response)).toBe("Maximum saved tournaments reached");
expect(await isSaved(upcoming.id)).toBe(false);
});
});
function createTournamentStartingAt(startTime: Date) {
return TournamentFactory.create({
authorId: saverId(),
startTimes: [dateToDatabaseTimestamp(startTime)],
});
}
async function saveTournamentStartingAt(startTime: Date) {
const tournament = await createTournamentStartingAt(startTime);
await SavedCalendarEventFactory.create({
userId: saverId(),
tournamentId: tournament.id,
});
}
function save(tournamentId: number) {
return info(
{ _action: "SAVE_TOURNAMENT" },
{ user: saverId(), params: { id: String(tournamentId) } },
);
}
function isSaved(tournamentId: number) {
return SavedCalendarEventRepository.isSaved({
userId: saverId(),
tournamentId,
});
}
function errorToastOf(response: Response | null | undefined) {
if (!response) return null;
return new URLSearchParams(
response.headers.get("location")?.split("?")[1],
).get("__error");
}

View File

@@ -14,9 +14,10 @@ export const action = defineAction(
switch (body._action) {
case "SAVE_TOURNAMENT": {
const count = await SavedCalendarEventRepository.countByUserId(user.id);
const upcomingCount =
await SavedCalendarEventRepository.countUpcomingByUserId(user.id);
errorToastIfFalsy(
count < TOURNAMENT.MAX_SAVED_COUNT,
upcomingCount < TOURNAMENT.MAX_SAVED_COUNT,
"Maximum saved tournaments reached",
);

View File

@@ -0,0 +1,5 @@
---
navItem: medal
type: bug
---
Fixed saving tournaments failing with "Maximum saved tournaments reached" once 20 of your saved tournaments had already happened

View File

@@ -2013,8 +2013,10 @@ export function buildCases(fx: Fixtures): {
tournamentId,
}),
);
add("SavedCalendarEventRepository.countByUserId", fx.heavyUser, (user) =>
SavedCalendarEventRepository.countByUserId(user.id),
add(
"SavedCalendarEventRepository.countUpcomingByUserId",
fx.heavyUser,
(user) => SavedCalendarEventRepository.countUpcomingByUserId(user.id),
);
add(
"SavedCalendarEventRepository.findAllUpcomingByUserId",