Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/components/GroupCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
activityDurationString,
type ActivityWithParent,
} from '../lib/domain/activities';
import { mayMakeCutoff, mayMakeTimeLimit } from '../lib/domain/persons';
import { getPersonalBestValue, mayMakeCutoff, mayMakeTimeLimit } from '../lib/domain/persons';
import { useAppSelector } from '../store';
import { selectPersonsAssignedToActivitiyId } from '../store/selectors';
import ConfigureGroupDialog from '../dialogs/ConfigureGroupDialog';
Expand Down Expand Up @@ -124,7 +124,7 @@ const GroupCard = ({ groupActivity }: GroupCardProps) => {
const pr = person.personalBests?.find(
(pb) => pb.eventId === eventId && pb.type === 'average'
);
return pr?.best;
return pr ? getPersonalBestValue(pr) : undefined;
})
.filter((pr) => !!pr) as number[],
[competitors, eventId]
Expand Down
40 changes: 40 additions & 0 deletions src/lib/api/wcaAPI.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,46 @@ describe('wcaAPI', () => {
expect(json).not.toHaveBeenCalled();
});

it('omits read-only v2 personal bests from the WCIF check payload', async () => {
const wcif = {
formatVersion: '2.1.1',
persons: [{ registrantId: 1, personalBests: [{ eventId: '333', value: 1000 }] }],
} as any;
mockFetch({});

await checkWcif(wcif);

expect(globalThis.fetch).toHaveBeenCalledWith(
'https://wca.test/api/v0/competitions/wcif/check',
expect.objectContaining({
body: JSON.stringify({
formatVersion: '2.1.1',
persons: [{ registrantId: 1 }],
}),
})
);
});

it('omits read-only v2 personal bests from patch payloads', async () => {
const wcif = {
formatVersion: '2.1.1',
persons: [{ registrantId: 1, personalBests: [{ eventId: '333', value: 1000 }] }],
} as any;
mockFetch({ json: vi.fn().mockResolvedValue({}) });

await patchWcif('Comp', wcif);

expect(globalThis.fetch).toHaveBeenCalledWith(
'https://wca.test/api/v0/competitions/Comp/wcif',
expect.objectContaining({
body: JSON.stringify({
formatVersion: '2.1.1',
persons: [{ registrantId: 1 }],
}),
})
);
});

it('builds upcoming and past competition queries', async () => {
vi.spyOn(Date, 'now').mockReturnValue(0);
mockFetch({ json: vi.fn().mockResolvedValue([]) });
Expand Down
19 changes: 17 additions & 2 deletions src/lib/api/wcaAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,21 @@ const wcifPath = (competitionId: string) => `/competitions/${competitionId}/wcif
const versionedWcifPath = (competitionId: string) =>
`${wcifPath(competitionId)}/version/${WCIF_VERSION}`;

/**
* Personal bests are read-only data. The v2 endpoint returns them, but the
* current WCIF checker does not accept them in a submitted v2 payload.
*/
const withoutV2PersonalBests = <T extends Partial<Competition>>(wcif: T): T => {
if (!wcif.formatVersion?.startsWith('2.') || !wcif.persons) {
return wcif;
}

return {
...wcif,
persons: wcif.persons.map(({ personalBests: _personalBests, ...person }) => person),
} as T;
};

export const getMe = (): Promise<{ me: WcaUser }> => {
return wcaApiFetch(`/me`);
};
Expand Down Expand Up @@ -57,15 +72,15 @@ export const patchWcif = (
): Promise<Competition> =>
wcaApiFetch(wcifPath(competitionId), {
method: 'PATCH',
body: JSON.stringify(wcif),
body: JSON.stringify(withoutV2PersonalBests(wcif)),
});

export const checkWcif = (wcif: Competition): Promise<void> =>
wcaApiFetch(
'/competitions/wcif/check',
{
method: 'PUT',
body: JSON.stringify(wcif),
body: JSON.stringify(withoutV2PersonalBests(wcif)),
},
false
);
Expand Down
29 changes: 29 additions & 0 deletions src/lib/domain/persons.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
shouldBeInRound,
personsShouldBeInRound,
findPR,
getPersonalBestValue,
byPsychsheet,
byResult,
addAssignmentsToPerson,
Expand Down Expand Up @@ -308,6 +309,34 @@ describe('findPR', () => {
});
});

describe('getPersonalBestValue', () => {
it('reads the v1 best field', () => {
expect(
getPersonalBestValue({
eventId: '333',
type: 'single',
best: 1000,
worldRanking: 1,
continentalRanking: 1,
nationalRanking: 1,
})
).toBe(1000);
});

it('reads the v2 value field', () => {
expect(
getPersonalBestValue({
eventId: '333',
type: 'single',
value: 1000,
worldRanking: 1,
continentalRanking: 1,
nationalRanking: 1,
} as never)
).toBe(1000);
});
});

describe('byPsychsheet', () => {
it('sorts people with WCA IDs before those without', () => {
const personWithId = createMockPerson({ wcaId: 'TEST2025' });
Expand Down
13 changes: 11 additions & 2 deletions src/lib/domain/persons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { parseActivityCode } from './activities';
import {
type Activity,
type Assignment,
type AttemptResult,
type Event,
type EventId,
type Person,
Expand Down Expand Up @@ -77,6 +78,14 @@ export const assignedInGroupsForRoles =
export const findPR = (personalBests: PersonalBest[], eventId: EventId, type: RankingType) =>
personalBests.find((pr) => pr.eventId === eventId && pr.type === type);

type V2PersonalBest = Omit<PersonalBest, 'best'> & { value: AttemptResult };

/**
* WCIF v1 calls this field `best`. WCIF v2 calls it `value`.
*/
export const getPersonalBestValue = (personalBest: PersonalBest | V2PersonalBest) =>
'value' in personalBest ? personalBest.value : personalBest.best;

/**
* Comparator for array.sort
* TODO: cleanup
Expand Down Expand Up @@ -215,7 +224,7 @@ export const mayMakeTimeLimit = (eventId: EventId, round?: Round, persons?: Pers
return false;
}

return PR.best <= timeLimit.centiseconds;
return getPersonalBestValue(PR) <= timeLimit.centiseconds;
}) || []
);
};
Expand All @@ -233,7 +242,7 @@ export const mayMakeCutoff = (eventId: EventId, round?: Round, persons?: Person[
return false;
}

return PR.best <= cutoff.attemptResult;
return getPersonalBestValue(PR) <= cutoff.attemptResult;
}) || []
);
};
Expand Down
6 changes: 3 additions & 3 deletions src/lib/wcif/persons.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { parseActivityCode } from '../domain/activities/activityCode';
import { roundFormatById } from '../domain/events';
import { findPR } from '../domain/persons';
import { findPR, getPersonalBestValue } from '../domain/persons';
import { type Competition, type Person, type AttemptResult } from '@wca/helpers';

/** WCIF Person Lookup Functions */
Expand Down Expand Up @@ -79,8 +79,8 @@ export const getSeedResult = (
const single = findPR(person.personalBests || [], eventId, 'single');

return {
average: average?.best,
single: single?.best,
average: average ? getPersonalBestValue(average) : undefined,
single: single ? getPersonalBestValue(single) : undefined,
};
}

Expand Down
Loading