Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 35 additions & 5 deletions lib/predictions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<SubmitResult> {
const h = Math.trunc(input.homeScore);
const a = Math.trunc(input.awayScore);
Expand Down Expand Up @@ -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,
};
}
19 changes: 15 additions & 4 deletions lib/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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 });
},
);
}
Loading