This repository has been archived on 2026-05-06. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
tippspiel/backend/src/services/syncService.ts
T
2026-04-03 21:41:19 +02:00

177 lines
5.2 KiB
TypeScript

import { query, withTransaction } from '../db/client';
import { fetchAllMatches, fetchFinishedMatches } from './footballApi';
import { calculatePoints } from './pointsService';
import { FootballApiMatch } from '../types';
import { logger } from './logger';
/**
* Synchronisiert alle WM-Spiele von football-data.org in die Datenbank.
* Legt neue Spiele an und aktualisiert bestehende (Status + Score).
*
* Sollte täglich per Cron laufen, oder manuell getriggert werden.
*/
export const syncMatches = async (): Promise<{
total: number;
created: number;
updated: number;
}> => {
logger.info('Starting match sync...');
const startTime = Date.now();
let created = 0;
let updated = 0;
try {
const apiMatches = await fetchAllMatches();
await withTransaction(async (client) => {
for (const match of apiMatches) {
const existing = await client.query<{ id: number; status: string }>(
'SELECT id, status FROM matches WHERE external_id = $1',
[match.id]
);
if (existing.rows.length === 0) {
// Neues Spiel anlegen
await client.query(
`INSERT INTO matches (
external_id, utc_date, status, stage, group_name,
home_team_id, away_team_id,
home_team_name, away_team_name,
home_team_short, away_team_short,
home_team_crest, away_team_crest,
score_home, score_away
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15)`,
[
match.id,
match.utcDate,
match.status,
match.stage,
match.group,
match.homeTeam?.id ?? null,
match.awayTeam?.id ?? null,
match.homeTeam?.name ?? 'TBD',
match.awayTeam?.name ?? 'TBD',
match.homeTeam?.shortName || match.homeTeam?.tla || 'TBD',
match.awayTeam?.shortName || match.awayTeam?.tla || 'TBD',
match.homeTeam?.crest ?? null,
match.awayTeam?.crest ?? null,
match.score.fullTime.home,
match.score.fullTime.away,
]
);
created++;
} else {
// Bestehendes Spiel aktualisieren
await client.query(
`UPDATE matches SET
status = $1,
score_home = $2,
score_away = $3,
utc_date = $4
WHERE external_id = $5`,
[
match.status,
match.score.fullTime.home,
match.score.fullTime.away,
match.utcDate,
match.id,
]
);
updated++;
}
}
});
// Sync-Log schreiben
await query(
`INSERT INTO sync_log (matches_total, matches_new, matches_upd, status)
VALUES ($1, $2, $3, 'success')`,
[apiMatches.length, created, updated]
);
const duration = Date.now() - startTime;
logger.info(`Match sync completed in ${duration}ms`, {
total: apiMatches.length,
created,
updated,
});
return { total: apiMatches.length, created, updated };
} catch (error) {
// Fehler loggen
await query(
`INSERT INTO sync_log (matches_total, matches_new, matches_upd, status, error_msg)
VALUES (0, 0, 0, 'error', $1)`,
[(error as Error).message]
).catch(() => {}); // Fehler beim Fehler-Logging ignorieren
logger.error('Match sync failed', { error });
throw error;
}
};
/**
* Wertet Tipps für alle abgeschlossenen Spiele aus.
* Berechnet Punkte und aktualisiert die tips-Tabelle.
* Refreshed danach die Leaderboard Materialized View.
*
* Sollte nach jedem abgeschlossenen Spiel laufen.
*/
export const evaluateTips = async (): Promise<{
matchesEvaluated: number;
tipsUpdated: number;
}> => {
logger.info('Starting tip evaluation...');
// Alle abgeschlossenen Spiele mit noch nicht ausgewerteten Tipps
const pendingMatches = await query<{
match_id: number;
score_home: number;
score_away: number;
}>(
`SELECT DISTINCT m.id AS match_id, m.score_home, m.score_away
FROM matches m
JOIN tips t ON t.match_id = m.id
WHERE m.status = 'FINISHED'
AND m.score_home IS NOT NULL
AND m.score_away IS NOT NULL
AND t.points IS NULL`
);
let tipsUpdated = 0;
for (const match of pendingMatches) {
// Alle Tipps für dieses Spiel
const tips = await query<{
id: number;
tip_home: number;
tip_away: number;
}>(
'SELECT id, tip_home, tip_away FROM tips WHERE match_id = $1 AND points IS NULL',
[match.match_id]
);
for (const tip of tips) {
const { points } = calculatePoints(
{ home: tip.tip_home, away: tip.tip_away },
{ home: match.score_home, away: match.score_away }
);
await query('UPDATE tips SET points = $1 WHERE id = $2', [points, tip.id]);
tipsUpdated++;
}
}
// Leaderboard Materialized View aktualisieren
if (tipsUpdated > 0) {
await query('REFRESH MATERIALIZED VIEW CONCURRENTLY leaderboard');
logger.info(`Leaderboard refreshed after evaluating ${tipsUpdated} tips`);
}
return {
matchesEvaluated: pendingMatches.length,
tipsUpdated,
};
};