Resolve external opponent TTR ratings and show lookup diagnostics
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 1m0s

This commit is contained in:
Torsten Schulz (local)
2026-10-28 11:03:46 +01:00
parent c473b64150
commit ecee9e0a00
6 changed files with 249 additions and 70 deletions

View 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 };
}