From e27a537011cedaf10b96b688eb6fb49144a7914b Mon Sep 17 00:00:00 2001 From: "Tom C." Date: Sun, 28 Jun 2026 15:35:35 +0200 Subject: [PATCH] =?UTF-8?q?fix(predictions):=20g=C3=A9rer=20winner=5Fteam?= =?UTF-8?q?=5Fcode=20(nouvelle=20r=C3=A8gle=20KO)=20dans=20submit=5Fpredic?= =?UTF-8?q?tion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le MCP n'avait pas suivi la nouvelle règle KO : submitPredictionFor faisait un upsert sans jamais toucher winner_team_code. Modifier un prono nul-KO déjà qualifié (winner posé via le web) vers un score décisif laissait le qualifié périmé → home ≠ away + winner ≠ null → violation du CHECK predictions_winner_implies_draw (le bug signalé par Grégory : le MCP tentait de remplacer un 1-1 et plantait). Correctif (parité avec submitPrediction de l'app principale) : normalisation de winner_team_code dans l'upsert — • hors nul KO → null (efface tout qualifié périmé) → plus de crash sur un score décisif ; • nul KO → pose le qualifié fourni (validé ∈ {dom, ext}), sinon PRÉSERVE celui déjà choisi (web), pour ne pas le perdre en éditant juste le score. L'outil submit_prediction expose désormais un param optionnel winnerTeamCode (+ description de la règle KO) et renvoie un indice quand un nul KO n'a pas encore de qualifié (pour le +1). tsc + build OK. --- lib/predictions.ts | 40 +++++++++++++++++++++++++++++++++++----- lib/tools.ts | 19 +++++++++++++++---- 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/lib/predictions.ts b/lib/predictions.ts index a88a061..6ba36f7 100644 --- a/lib/predictions.ts +++ b/lib/predictions.ts @@ -7,12 +7,12 @@ import { jokerBucketFor, jokerBucketLabelFor, jokerQuotaFor } from "@/lib/jokers import type { Phase } from "@/lib/types"; export type SubmitResult = - | { ok: true; saved: { matchId: string; home: number; away: number; joker: boolean }; locked: false } + | { ok: true; saved: { matchId: string; home: number; away: number; joker: boolean; winnerTeamCode: string | null; koDrawNeedsQualifier: boolean }; locked: false } | { ok: false; error: string }; export async function submitPredictionFor( userId: string, - input: { matchId: string; homeScore: number; awayScore: number; joker: boolean }, + input: { matchId: string; homeScore: number; awayScore: number; joker: boolean; winnerTeamCode?: string | null }, ): Promise { const h = Math.trunc(input.homeScore); const a = Math.trunc(input.awayScore); @@ -44,13 +44,43 @@ export async function submitPredictionFor( } } + // Qualifié (nouvelle règle KO) : n'a de sens que sur un NUL prédit en match à + // élimination. On NORMALISE pour ne jamais violer le CHECK predictions_winner_implies_draw : + // - hors nul KO → winner_team_code = null. C'ÉTAIT LE BUG : passer un nul KO déjà + // qualifié (winner posé via le web) à un score décisif laissait le qualifié périmé → + // winner ≠ null + home ≠ away → violation du CHECK. On l'efface désormais. + // - nul KO → on pose le qualifié fourni (validé ∈ {dom, ext}), sinon on PRÉSERVE celui + // déjà choisi (ex. via le web), pour ne pas le perdre en éditant juste le score. + const isKoDraw = m.match.scoringRule === "knockout" && h === a; + const validWinner = + isKoDraw && input.winnerTeamCode && (input.winnerTeamCode === m.match.homeTeamCode || input.winnerTeamCode === m.match.awayTeamCode) + ? input.winnerTeamCode + : null; + let winnerTeamCode: string | null = null; + if (isKoDraw) { + if (input.winnerTeamCode !== undefined) { + winnerTeamCode = validWinner; + } else { + const [existing] = await db + .select({ w: predictions.winnerTeamCode }) + .from(predictions) + .where(and(eq(predictions.userId, userId), eq(predictions.matchId, m.match.id))) + .limit(1); + winnerTeamCode = existing?.w ?? null; + } + } + await db .insert(predictions) - .values({ userId, matchId: m.match.id, homeScore: h, awayScore: a, jokerApplied: input.joker }) + .values({ userId, matchId: m.match.id, homeScore: h, awayScore: a, jokerApplied: input.joker, winnerTeamCode }) .onConflictDoUpdate({ target: [predictions.userId, predictions.matchId], - set: { homeScore: h, awayScore: a, jokerApplied: input.joker, updatedAt: new Date() }, + set: { homeScore: h, awayScore: a, jokerApplied: input.joker, winnerTeamCode, updatedAt: new Date() }, }); - return { ok: true, saved: { matchId: m.match.id, home: h, away: a, joker: input.joker }, locked: false }; + return { + ok: true, + saved: { matchId: m.match.id, home: h, away: a, joker: input.joker, winnerTeamCode, koDrawNeedsQualifier: isKoDraw && winnerTeamCode == null }, + locked: false, + }; } diff --git a/lib/tools.ts b/lib/tools.ts index 44fe3ce..7c508eb 100644 --- a/lib/tools.ts +++ b/lib/tools.ts @@ -199,12 +199,18 @@ export function registerTools(server: McpServer): void { { title: "Poser un prono", description: - "Pose ou modifie MON pronostic sur un match (scores 0-20, joker optionnel). Refusé si le coup d'envoi est passé ou si je n'ai plus de joker pour ce bucket. Confirme toujours avec l'utilisateur avant d'écrire.", + "Pose ou modifie MON pronostic sur un match (scores 0-20, joker optionnel). Refusé si le coup d'envoi est passé ou si je n'ai plus de joker pour ce bucket. Sur un match à élimination directe : le prono porte sur le score à la fin du temps réglementaire (90'), et si tu prédis un NUL, précise winnerTeamCode (l'équipe qui se qualifie) pour le +1 bonus. Confirme toujours avec l'utilisateur avant d'écrire.", inputSchema: { matchId: z.string().min(1).describe("L'id (uuid) du match — voir list_matches/search."), homeScore: z.number().int().min(0).max(20).describe("Score prédit de l'équipe à domicile (0-20)."), awayScore: z.number().int().min(0).max(20).describe("Score prédit de l'équipe à l'extérieur (0-20)."), joker: z.boolean().default(false).describe("Appliquer un joker (double les points). Défaut: false."), + winnerTeamCode: z + .string() + .min(2) + .max(3) + .optional() + .describe("Match à élimination directe + nul prédit UNIQUEMENT : code FIFA-3 de l'équipe qui se qualifie (pour le +1). Doit être l'une des 2 équipes. Ignoré sur un score décisif ou en phase de poules."), }, }, async (args, extra) => { @@ -214,9 +220,14 @@ export function registerTools(server: McpServer): void { if (!ENABLE_WRITES) { return jsonErr("L'écriture de pronos via MCP est désactivée sur ce serveur (MCP_ENABLE_WRITES=false)."); } - const a = args as { matchId: string; homeScore: number; awayScore: number; joker: boolean }; - const res = await submitPredictionFor(me.userId, { matchId: a.matchId, homeScore: a.homeScore, awayScore: a.awayScore, joker: a.joker }); - return res.ok ? jsonOk({ ok: true, saved: res.saved }) : jsonErr(res.error); + const a = args as { matchId: string; homeScore: number; awayScore: number; joker: boolean; winnerTeamCode?: string }; + const res = await submitPredictionFor(me.userId, { matchId: a.matchId, homeScore: a.homeScore, awayScore: a.awayScore, joker: a.joker, winnerTeamCode: a.winnerTeamCode }); + if (!res.ok) return jsonErr(res.error); + // Nul prédit en KO sans qualifié → on invite à le préciser (sinon pas de +1). + const note = res.saved.koDrawNeedsQualifier + ? "Nul prédit sur un match à élimination directe : appelle à nouveau submit_prediction avec winnerTeamCode (l'équipe qui se qualifie) pour activer le +1 bonus." + : undefined; + return jsonOk(note ? { ok: true, saved: res.saved, note } : { ok: true, saved: res.saved }); }, ); }