Resolve external opponent TTR ratings and show lookup diagnostics
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 1m0s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 1m0s
This commit is contained in:
69
backend/services/ttrPreviewService.js
Normal file
69
backend/services/ttrPreviewService.js
Normal file
@@ -0,0 +1,69 @@
|
||||
export const normalizeTtrName = value => String(value || '').normalize('NFKD')
|
||||
.replace(/[\u0300-\u036f]/g, '').replace(/ß/g, 'ss')
|
||||
.replace(/[^\p{L}\p{N}]+/gu, ' ').trim().toLocaleLowerCase('de-DE');
|
||||
|
||||
const nameOf = player => [player?.firstname || player?.firstName, player?.lastname || player?.lastName].filter(Boolean).join(' ').trim() || String(player?.name || '').trim();
|
||||
const hasRating = value => value !== null && value !== undefined && String(value).trim() !== '' && Number.isFinite(Number(value));
|
||||
export const clubNameFromTeam = name => String(name || '').replace(/\s*\([^)]*\)/g, '')
|
||||
.replace(/\s+(?:[IVXL]+|[1-9])\s*$/i, '').trim();
|
||||
const matchesClub = (name, club) => normalizeTtrName(clubNameFromTeam(name)) === normalizeTtrName(club);
|
||||
|
||||
/** Dependencies are injected so the real lookup path can be tested without credentials. */
|
||||
export async function resolveTtrPreview({ homePlayers = [], guestPlayers = [], homeTeamName, guestTeamName,
|
||||
homeClubName, guestClubName, members = [], clubs = [], cachedPlayers = [] }, { getSession, client }) {
|
||||
const players = [];
|
||||
const diagnostics = [];
|
||||
let sessionPromise;
|
||||
const session = () => (sessionPromise ||= getSession());
|
||||
for (const side of ['home', 'guest']) {
|
||||
const lineup = side === 'home' ? homePlayers : guestPlayers;
|
||||
const teamName = side === 'home' ? homeTeamName : guestTeamName;
|
||||
const clubName = (side === 'home' ? homeClubName : guestClubName) || clubNameFromTeam(teamName);
|
||||
const localClub = clubs.find(club => matchesClub(teamName, club.name) || matchesClub(clubName, club.name));
|
||||
const sidePlayers = lineup.map(player => {
|
||||
const name = nameOf(player);
|
||||
// nuLigaPersonId and myTischtennisPlayerId are different ID namespaces.
|
||||
const candidates = members.filter(member => hasRating(member.ttr) &&
|
||||
normalizeTtrName(`${member.firstName} ${member.lastName}`) === normalizeTtrName(name) &&
|
||||
(!localClub || Number(member.clubId) === Number(localClub.id)));
|
||||
const member = candidates.length === 1 ? candidates[0] : null;
|
||||
const cached = cachedPlayers.find(entry => entry.side === side && normalizeTtrName(entry.name) === normalizeTtrName(name) &&
|
||||
String(entry.nuLigaPersonId || '') === String(player.nuLigaPersonId || '') && hasRating(entry.ttr));
|
||||
return { side, name, nuLigaPersonId: player.nuLigaPersonId || null,
|
||||
ttr: member ? Number(member.ttr) : cached ? Number(cached.ttr) : null,
|
||||
adjustmentConstant: Number(member?.ttrAdjustmentConstant || cached?.adjustmentConstant || 16),
|
||||
source: member ? 'database' : cached ? cached.source : 'unavailable' };
|
||||
});
|
||||
const pending = sidePlayers.filter(player => !hasRating(player.ttr));
|
||||
if (pending.length) {
|
||||
try {
|
||||
const activeSession = await session();
|
||||
let clubNr = localClub?.associationMemberNumber;
|
||||
let federation = localClub?.myTischtennisFedNickname;
|
||||
if (!clubNr || !federation) {
|
||||
if (!clubName) throw new Error('Vereinsname fehlt in den Spielberichtsdaten.');
|
||||
const search = await client.searchClubs(activeSession.cookie, clubNameFromTeam(clubName));
|
||||
if (!search.success) throw new Error(search.error || 'Vereinssuche fehlgeschlagen.');
|
||||
const matches = search.clubs.filter(club => matchesClub(clubName, club.clubname) || matchesClub(teamName, club.clubname));
|
||||
if (matches.length !== 1) throw new Error(matches.length ? 'Verein nicht eindeutig gefunden.' : 'Verein bei myTischtennis nicht gefunden.');
|
||||
clubNr = matches[0].clubnr;
|
||||
federation = matches[0].organization_short;
|
||||
if (!clubNr || !federation) throw new Error('Vereinsnummer oder Verbandskürzel fehlt im Suchergebnis.');
|
||||
}
|
||||
const ranking = await client.getClubRankings(activeSession.cookie, clubNr, federation);
|
||||
if (!ranking.success) throw new Error(ranking.error || 'Ranglistenabruf fehlgeschlagen.');
|
||||
for (const player of pending) {
|
||||
const entries = ranking.entries.filter(entry => normalizeTtrName(nameOf(entry)) === normalizeTtrName(player.name) && hasRating(entry.fedRank));
|
||||
if (entries.length === 1) Object.assign(player, { ttr: Number(entries[0].fedRank), source: 'myTischtennis' });
|
||||
}
|
||||
const missing = pending.filter(player => !hasRating(player.ttr));
|
||||
if (missing.length) diagnostics.push({ side, code: 'players_not_found',
|
||||
message: `${clubName || teamName}: Kein eindeutiger aktueller TTR für ${missing.map(player => player.name).join(', ')} in der Rangliste gefunden.` });
|
||||
} catch (error) {
|
||||
diagnostics.push({ side, code: 'lookup_failed', message: `${clubName || teamName || side}: ${error.message}` });
|
||||
}
|
||||
}
|
||||
players.push(...sidePlayers);
|
||||
}
|
||||
return { players, diagnostics };
|
||||
}
|
||||
Reference in New Issue
Block a user