From f3094935fb20d06171ab1e602bc871c671115138 Mon Sep 17 00:00:00 2001 From: Cailyn Sinclair Date: Thu, 14 May 2026 12:12:45 -0700 Subject: [PATCH] Ignore overlap conflicts for cumulative time-limit rounds --- .../personAssignmentValidation.test.ts | 70 +++++++++++++++++++ .../validation/personAssignmentValidation.ts | 47 +++++++++++++ 2 files changed, 117 insertions(+) diff --git a/src/lib/wcif/validation/personAssignmentValidation.test.ts b/src/lib/wcif/validation/personAssignmentValidation.test.ts index 34cd3af..417418e 100644 --- a/src/lib/wcif/validation/personAssignmentValidation.test.ts +++ b/src/lib/wcif/validation/personAssignmentValidation.test.ts @@ -317,6 +317,76 @@ describe('validatePersonAssignmentScheduleConflicts', () => { expect(conflict?.assignmentA.room?.name).toBe('Room 1'); expect(conflict?.assignmentB.room).toBeDefined(); }); + + it('should not detect conflicts for overlapping assignments that share cumulative time limits', () => { + const wcif: Competition = { + ...mockWcif, + events: [ + { + id: '444bf', + rounds: [ + { + id: '444bf-r1', + timeLimit: { centiseconds: 360000, cumulativeRoundIds: ['444bf-r1', '555bf-r1'] }, + }, + ], + }, + { + id: '555bf', + rounds: [ + { + id: '555bf-r1', + timeLimit: { centiseconds: 360000, cumulativeRoundIds: ['444bf-r1', '555bf-r1'] }, + }, + ], + }, + ] as Competition['events'], + schedule: { + ...mockWcif.schedule, + venues: [ + { + ...mockWcif.schedule.venues[0], + rooms: [ + { + ...mockWcif.schedule.venues[0].rooms[0], + activities: [ + { + id: 11, + name: '4BLD Round 1', + activityCode: '444bf-r1', + startTime: '2024-01-01T09:00:00.000Z', + endTime: '2024-01-01T10:00:00.000Z', + childActivities: [], + extensions: [], + }, + { + id: 12, + name: '5BLD Round 1', + activityCode: '555bf-r1', + startTime: '2024-01-01T09:00:00.000Z', + endTime: '2024-01-01T10:00:00.000Z', + childActivities: [], + extensions: [], + }, + ], + }, + ], + }, + ], + }, + persons: [ + createPerson({ + assignments: [ + { activityId: 11, stationNumber: null, assignmentCode: 'competitor' }, + { activityId: 12, stationNumber: null, assignmentCode: 'staff-judge' }, + ], + }), + ], + }; + + const errors = validatePersonAssignmentScheduleConflicts(wcif); + expect(errors).toHaveLength(0); + }); }); describe('validatePersonAssignments', () => { diff --git a/src/lib/wcif/validation/personAssignmentValidation.ts b/src/lib/wcif/validation/personAssignmentValidation.ts index c8202de..023304c 100644 --- a/src/lib/wcif/validation/personAssignmentValidation.ts +++ b/src/lib/wcif/validation/personAssignmentValidation.ts @@ -2,6 +2,7 @@ import { activitiesOverlap, findActivityById, findAllActivities, + parseActivityCode, roomByActivity, } from '../../domain'; import { acceptedRegistrations } from '../../domain'; @@ -16,6 +17,48 @@ import type { Competition, Person } from '@wca/helpers'; const pluralizeWord = (count: number, singular: string, plural?: string) => count === 1 ? singular : plural || singular + 's'; +const roundsShareCumulativeTimeLimit = ( + wcif: Competition, + activityIdA: number, + activityIdB: number +) => { + const activityA = findActivityById(wcif, activityIdA); + const activityB = findActivityById(wcif, activityIdB); + + if (!activityA || !activityB) { + return false; + } + + const roundA = parseActivityCode(activityA.activityCode); + const roundB = parseActivityCode(activityB.activityCode); + if ( + !roundA.eventId || + !roundA.roundNumber || + !roundB.eventId || + !roundB.roundNumber || + roundA.eventId === roundB.eventId + ) { + return false; + } + + const eventA = wcif.events.find((event) => event.id === roundA.eventId); + const eventB = wcif.events.find((event) => event.id === roundB.eventId); + if (!eventA || !eventB) { + return false; + } + + const roundAData = eventA.rounds?.find((round) => round.id === `${roundA.eventId}-r${roundA.roundNumber}`); + const roundBData = eventB.rounds?.find((round) => round.id === `${roundB.eventId}-r${roundB.roundNumber}`); + if (!roundAData?.timeLimit?.cumulativeRoundIds || !roundBData?.timeLimit?.cumulativeRoundIds) { + return false; + } + + return ( + roundAData.timeLimit.cumulativeRoundIds.includes(roundBData.id) && + roundBData.timeLimit.cumulativeRoundIds.includes(roundAData.id) + ); +}; + /** * Validates that all person assignments reference existing activities */ @@ -74,6 +117,10 @@ const findConflictingAssignmentsForPerson = ( return; } + if (roundsShareCumulativeTimeLimit(wcif, assignment.activityId, otherAssignment.activityId)) { + return; + } + conflictingAssignments.push({ id: [ person.registrantId,