diff --git a/app/features/scrims/ScrimPostRepository.server.test.ts b/app/features/scrims/ScrimPostRepository.server.test.ts index 63d966c78..cc60711e3 100644 --- a/app/features/scrims/ScrimPostRepository.server.test.ts +++ b/app/features/scrims/ScrimPostRepository.server.test.ts @@ -20,6 +20,40 @@ const WINDOW = { endTime: dbTs(add(BOOKED_AT, { hours: 1 })), }; +/** + * A "flexible time" post whose window opened `windowOpensInMinutes` from now, + * booked by a request that picked a start `bookedInMinutes` from now. + */ +async function createBookedRangeScrim({ + windowOpensInMinutes, + bookedInMinutes, +}: { + windowOpensInMinutes: number; + bookedInMinutes: number; +}) { + const now = new Date(); + const bookedAt = add(now, { minutes: bookedInMinutes }); + + const { id } = await ScrimPostFactory.create( + { + startsAt: dbTs(add(now, { minutes: windowOpensInMinutes })), + rangeEndsAt: dbTs(add(now, { minutes: windowOpensInMinutes + 120 })), + users: [{ userId: users.id(1), isOwner: 1 }], + }, + { + requests: [ + { + startsAt: dbTs(bookedAt), + users: [{ userId: users.id(2), isOwner: 1 }], + isAccepted: true, + }, + ], + }, + ); + + return { id, bookedAt: dbTs(bookedAt) }; +} + describe("findPendingOverlapsForUsers", () => { beforeEach(async () => { await users.create(5); @@ -251,6 +285,68 @@ describe("findUserScrims", () => { expect(postOwnerScrims).toHaveLength(1); expect(postOwnerScrims[0]!.status).toBe("booked"); }); + + test("lists a booked range scrim whose post window opened before the booked start", async () => { + const { id } = await createBookedRangeScrim({ + windowOpensInMinutes: -30, + bookedInMinutes: 45, + }); + + const scrims = await ScrimPostRepository.findUserScrims(users.id(1)); + + expect(scrims.map((scrim) => scrim.id)).toContain(id); + }); + + test("reports the booked start of a range scrim, not the start of its window", async () => { + const { id, bookedAt } = await createBookedRangeScrim({ + windowOpensInMinutes: 30, + bookedInMinutes: 90, + }); + + const scrims = await ScrimPostRepository.findUserScrims(users.id(2)); + const scrim = scrims.find((scrim) => scrim.id === id); + + expect(scrim?.status).toBe("booked"); + expect(scrim?.startsAt).toBe(bookedAt); + }); +}); + +describe("findAcceptedScrimsBetweenTwoTimestamps", () => { + beforeEach(async () => { + await users.create(2); + }); + + const startingWithinTheHour = async () => { + const now = new Date(); + + return ScrimPostRepository.findAcceptedScrimsBetweenTwoTimestamps({ + startTime: now, + endTime: add(now, { hours: 1 }), + excludeRecentlyCreated: add(now, { minutes: 1 }), + }); + }; + + test("finds a range scrim booked to start inside the window", async () => { + const { id } = await createBookedRangeScrim({ + windowOpensInMinutes: -30, + bookedInMinutes: 45, + }); + + const scrims = await startingWithinTheHour(); + + expect(scrims.map((scrim) => scrim.id)).toContain(id); + }); + + test("leaves out a range scrim booked to start after the window", async () => { + const { id } = await createBookedRangeScrim({ + windowOpensInMinutes: 30, + bookedInMinutes: 120, + }); + + const scrims = await startingWithinTheHour(); + + expect(scrims.map((scrim) => scrim.id)).not.toContain(id); + }); }); describe("insertRequest", () => { diff --git a/app/features/scrims/ScrimPostRepository.server.ts b/app/features/scrims/ScrimPostRepository.server.ts index 736390e92..7a530392a 100644 --- a/app/features/scrims/ScrimPostRepository.server.ts +++ b/app/features/scrims/ScrimPostRepository.server.ts @@ -239,6 +239,9 @@ const baseFindQuery = db ).as("requests"), ]); +/** The booked start of a scrim: the accepted request's chosen time for a range post, the post's own otherwise. */ +const bookedStartsAt = sql`coalesce((select "ScrimPostRequest"."startsAt" from "ScrimPostRequest" where "ScrimPostRequest"."scrimPostId" = "ScrimPost"."id" and "ScrimPostRequest"."isAccepted" = 1), "ScrimPost"."startsAt")`; + function findMany() { const min = sub(new Date(), { hours: 3 }); @@ -541,8 +544,8 @@ export async function findAcceptedScrimsBetweenTwoTimestamps({ excludeRecentlyCreated: Date; }) { const rows = await baseFindQuery - .where("ScrimPost.startsAt", ">=", dateToDatabaseTimestamp(startTime)) - .where("ScrimPost.startsAt", "<", dateToDatabaseTimestamp(endTime)) + .where(bookedStartsAt, ">=", dateToDatabaseTimestamp(startTime)) + .where(bookedStartsAt, "<", dateToDatabaseTimestamp(endTime)) .where("ScrimPost.canceledAt", "is", null) .where( "ScrimPost.createdAt", @@ -711,7 +714,7 @@ export async function findUserScrims(userId: number): Promise { const rows = await baseFindQuery .where("ScrimPost.canceledAt", "is", null) - .where("ScrimPost.startsAt", ">=", now) + .where(bookedStartsAt, ">=", now) .where((eb) => eb.or([ eb.exists( @@ -735,7 +738,7 @@ export async function findUserScrims(userId: number): Promise { ), ]), ) - .orderBy("ScrimPost.startsAt", "asc") + .orderBy(bookedStartsAt, "asc") .execute(); return rows diff --git a/changelog/2026-09-02-flexible-scrim-booked-time.md b/changelog/2026-09-02-flexible-scrim-booked-time.md new file mode 100644 index 000000000..0bc0f5125 --- /dev/null +++ b/changelog/2026-09-02-flexible-scrim-booked-time.md @@ -0,0 +1,5 @@ +--- +navItem: scrims +type: bug +--- +Booked scrims from "flexible time" posts now stay on your events list until their booked start, and the "starting soon" notification is sent at the right time