import { HttpError } from "../middleware/errors"; import { getGame, listByDateCached } from "../repositories/gameRepository"; import { getUserVote, submitVote, changeVote, getCounts, getUserDateVotes, } from "../repositories/voteRepository"; import { DRAW_TEAM_CODE, type Game, type VoteSide } from "../types/panit"; import { fromTimestamp, parseDateString, type DateString } from "../types/dateString"; import { MemCache } from "../lib/memCache"; import { toGameDto, type GameDto, type MyVotesDto, type PredictionMutationDto, type VoteSummaryDto, } from "../types/dto/predictionDto"; type SummaryCounts = { homeCount: number; awayCount: number; drawCount: number }; const summaryCache = new MemCache(5_000); function gameDate(game: Game): DateString { return fromTimestamp(game.time); } function sideOf(game: Game, teamCode: string): VoteSide { if (teamCode === DRAW_TEAM_CODE) return "draw"; if (teamCode === game.homeTeamCode) return "home"; if (teamCode === game.awayTeamCode) return "away"; throw new HttpError(400, `teamCode ${teamCode} not in game ${game.homeTeamCode}/${game.awayTeamCode}`); } async function loadWaitingGame(gameId: string): Promise { const game = await getGame(gameId); if (!game) throw new HttpError(404, `game not found: ${gameId}`); if (game.status !== "scheduled") { throw new HttpError(409, `game status is ${game.status}, voting closed`); } return game; } export async function createPrediction( uid: string, body: { gameId?: string; selectedTeamCode?: string } ): Promise { if (!body.gameId || !body.selectedTeamCode) { throw new HttpError(400, "gameId and selectedTeamCode required"); } const game = await loadWaitingGame(body.gameId); const side = sideOf(game, body.selectedTeamCode); const existing = await getUserVote(body.gameId, uid); if (existing) throw new HttpError(409, "already voted; use PUT to change"); await submitVote({ gameId: body.gameId, uid, date: gameDate(game), side, team: body.selectedTeamCode, }); summaryCache.delete(body.gameId); return { ok: true }; } export async function updatePrediction( uid: string, body: { gameId?: string; selectedTeamCode?: string } ): Promise { if (!body.gameId || !body.selectedTeamCode) { throw new HttpError(400, "gameId and selectedTeamCode required"); } const game = await loadWaitingGame(body.gameId); const newSide = sideOf(game, body.selectedTeamCode); const existing = await getUserVote(body.gameId, uid); if (!existing) throw new HttpError(400, "no prior vote; use POST"); if (existing.team === body.selectedTeamCode) return { ok: true, changed: false }; const oldSide = sideOf(game, existing.team); await changeVote({ gameId: body.gameId, uid, date: gameDate(game), oldSide, newSide, newTeam: body.selectedTeamCode, }); summaryCache.delete(body.gameId); return { ok: true, changed: true }; } export async function getMyVotes( uid: string, date: string ): Promise { try { return getUserDateVotes(uid, parseDateString(date)); } catch (err) { throw new HttpError(400, (err as Error).message); } } export async function listGamesByDate(date: string): Promise { try { const games = await listByDateCached(parseDateString(date)); return games.map(toGameDto); } catch (err) { throw new HttpError(400, (err as Error).message); } } export async function getSummary(gameId: string): Promise { if (!gameId) throw new HttpError(400, "gameId required"); return summaryCache.getOrFetch(gameId, () => getCounts(gameId)); }