From 7a4d5ceae98d44c62c2aa8eef9851913f745e9fc Mon Sep 17 00:00:00 2001 From: Lukas Date: Thu, 19 Dec 2024 16:03:03 -0500 Subject: [PATCH] Allows for league admin to have prediction points recalculated (#847) --- components/LeagueAdminPanel.tsx | 24 ++++ package.json | 2 +- pages/api/league/[league]/recalculate.ts | 80 ++++++++++++ scripts/calcPoints.spec.ts | 103 +++++++++++++++- scripts/calcPoints.ts | 148 +++++++++++++++++------ 5 files changed, 312 insertions(+), 45 deletions(-) create mode 100644 pages/api/league/[league]/recalculate.ts diff --git a/components/LeagueAdminPanel.tsx b/components/LeagueAdminPanel.tsx index 9b4d3ca9..6187975a 100644 --- a/components/LeagueAdminPanel.tsx +++ b/components/LeagueAdminPanel.tsx @@ -395,6 +395,30 @@ function AdminPanelAdmin({ > {t("Save admin settings")} +
+
+ ); } diff --git a/package.json b/package.json index b87986cf..b834f7c1 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "dev:part2": "ts-node --project=tsconfig2.json scripts/entrypoint.ts & next dev", "build": "export NODE_ENV=production APP_ENV=production && npm ci && npm run start2:part1 && next build", "start": "export NODE_ENV=production APP_ENV=production && npm run start2", - "test": "export NODE_ENV=test APP_ENV=test && ts-node --project=tsconfig2.json scripts/jestSetup.ts && jest", + "test": "export NODE_ENV=test APP_ENV=test && ts-node --project=tsconfig2.json scripts/jestSetup.ts && jest --watch", "start:test": "export NODE_ENV=test APP_ENV=test && npm run clean && npm run start2", "start2": "npm run start2:part1 && npm run start2:part2", "start2:part1": "ts-node --project=tsconfig2.json scripts/startup.ts", diff --git a/pages/api/league/[league]/recalculate.ts b/pages/api/league/[league]/recalculate.ts new file mode 100644 index 00000000..9e68db42 --- /dev/null +++ b/pages/api/league/[league]/recalculate.ts @@ -0,0 +1,80 @@ +import connect from "../../../../Modules/database"; +import { authOptions } from "#/pages/api/auth/[...nextauth]"; +import { getServerSession } from "next-auth"; +import { NextApiRequest, NextApiResponse } from "next"; +import { leagueUsers, points } from "#type/database"; +import { calcHistoricalPredictionPoints } from "#scripts/calcPoints"; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse, +) { + const session = await getServerSession(req, res, authOptions); + if (session) { + const connection = await connect(); + const league = parseInt(req.query.league as string); + // Variable to check if the league is archived + const isArchived = connection + .query("SELECT * FROM leagueSettings WHERE leagueID=? AND archived=0", [ + league, + ]) + .then((e) => e.length === 0); + switch (req.method) { + // Used to edit a league + case "POST": + if (await isArchived) { + res.status(400).end("This league is archived"); + break; + } + // Checks if the user is qualified to do this + const user: leagueUsers[] = await connection.query( + "SELECT * FROM leagueUsers WHERE leagueID=? AND user=? AND admin=1", + [league, session.user.id], + ); + if (user.length === 0) { + res.status(403).end("You are not admin of this league"); + break; + } + const matchdays: points[] = await connection.query( + "SELECT * FROM points WHERE leagueID=? ORDER BY user ASC", + [league], + ); + let curr_user = -1; + let curr_leagueID = -1; + let change_in_points = 0; + for (const matchday of matchdays) { + if (curr_user !== matchday.user) { + if (curr_user !== -1) { + await connection.query( + "UPDATE main.leagueUsers SET points=points+?, predictionPoints=predictionPoints+? WHERE leagueID=? AND user=?", + [change_in_points, change_in_points, curr_leagueID, curr_user], + ); + } + curr_user = matchday.user; + curr_leagueID = matchday.leagueID; + change_in_points = 0; + } + const points = await calcHistoricalPredictionPoints(matchday); + change_in_points += points - matchday.predictionPoints; + await connection.query( + "UPDATE points SET predictionPoints=?, points=points+? WHERE matchday=? AND leagueID=? AND user=?", + [ + points, + points - matchday.predictionPoints, + matchday.matchday, + matchday.leagueID, + matchday.user, + ], + ); + } + res.status(200).end("Updated prediction points"); + break; + default: + res.status(405).end(`Method ${req.method} Not Allowed`); + break; + } + connection.end(); + } else { + res.status(401).end("Not logged in"); + } +} diff --git a/scripts/calcPoints.spec.ts b/scripts/calcPoints.spec.ts index b6cd966f..7b785953 100644 --- a/scripts/calcPoints.spec.ts +++ b/scripts/calcPoints.spec.ts @@ -1,6 +1,17 @@ import connect from "../Modules/database"; -import { calcPredictionsPoints, calcStarredPoints } from "./calcPoints"; -import { leagueUsers, predictions } from "../types/database"; +import { + calcHistoricalPredictionPoints, + calcPredicitionPointsRaw, + calcPredictionsPointsNow, + calcStarredPoints, + predictions_raw, +} from "./calcPoints"; +import { + leagueSettings, + leagueUsers, + points, + predictions, +} from "../types/database"; import { describe } from "@jest/globals"; describe("calcStarredPoints", () => { @@ -27,7 +38,7 @@ describe("calcStarredPoints", () => { }); }); -describe("calcPredictionsPoints", () => { +describe("calcPredictionsPointsNow", () => { beforeEach(async () => { const connection = await connect(); await connection.query("DELETE FROM predictions"); @@ -57,11 +68,10 @@ describe("calcPredictionsPoints", () => { admin: false, tutorial: false, }; - await calcPredictionsPoints(user); + await calcPredictionsPointsNow(user); const prediction_list: predictions[] = await connection.query( "SELECT * FROM predictions WHERE leagueID=1 AND user=1", ); - console.log(prediction_list); for (let i = 0; i < prediction_list.length; i++) { expect(prediction_list[i].home).not.toBeNull(); expect(prediction_list[i].away).not.toBeNull(); @@ -69,3 +79,86 @@ describe("calcPredictionsPoints", () => { connection.end(); }); }); + +describe("calcPredictionsPoints", () => { + const defaultleagueSettings: leagueSettings = { + leagueID: 1, + leagueName: "test", + archived: 0, + startMoney: 0, + transfers: 0, + duplicatePlayers: 0, + starredPercentage: 0, + matchdayTransfers: false, + fantasyEnabled: false, + predictionsEnabled: true, + top11: false, + active: true, + inActiveDays: 0, + league: "league", + predictExact: 5, + predictDifference: 3, + predictWinner: 1, + }; + it("no predictions", () => { + expect(calcPredicitionPointsRaw([], [], defaultleagueSettings)); + }); + it("Simple with exact, difference and winner", () => { + const predictions: predictions_raw[] = [ + { home: 2, away: 2, club: "1" }, + { home: 5, away: 1, club: "2" }, + { home: 5, away: 1, club: "3" }, + ]; + const games: predictions_raw[] = [ + { home: 2, away: 2, club: "1" }, + { home: 3, away: 0, club: "2" }, + { home: 4, away: 0, club: "3" }, + ]; + expect( + calcPredicitionPointsRaw(predictions, games, defaultleagueSettings), + ).toBe(9); + }); +}); + +describe("calcHistoricalPredictionPoints", () => { + const point_data: points = { + leagueID: 1, + user: 1, + points: 0, + fantasyPoints: 0, + predictionPoints: 0, + time: 1, + matchday: 1, + money: 0, + }; + beforeEach(async () => { + const connection = await connect(); + await connection.query("DELETE FROM leagueSettings"); + await connection.query("DELETE FROM historicalClubs"); + await connection.query( + "INSERT INTO leagueSettings (leagueID, predictExact, league) VALUES (1, 10, 'league')", + ); + await connection.query( + "INSERT INTO historicalClubs (home, time, teamScore, opponentScore, club, league) VALUES (1, 1, 1, 1, 'test', 'league')", + ); + await connection.query( + "INSERT INTO leagueUsers (user, leagueID) VALUES (1, 1)", + ); + connection.end(); + }); + it("no prediction", async () => { + const connection = await connect(); + await connection.query("DELETE FROM historicalPredictions"); + expect(await calcHistoricalPredictionPoints(point_data)).toBe(0); + connection.end(); + }); + it("one prediction", async () => { + const connection = await connect(); + await connection.query("DELETE FROM historicalPredictions"); + await connection.query( + "INSERT INTO historicalPredictions (user, leagueID, matchday, home, away, club) VALUES (1, 1, 1, 1, 1, 'test')", + ); + expect(await calcHistoricalPredictionPoints(point_data)).toBe(10); + connection.end(); + }); +}); diff --git a/scripts/calcPoints.ts b/scripts/calcPoints.ts index fa957a4c..20588a87 100644 --- a/scripts/calcPoints.ts +++ b/scripts/calcPoints.ts @@ -1,6 +1,7 @@ import connect from "../Modules/database"; import { clubs, + historicalClubs, leagueSettings, leagueUsers, points, @@ -98,13 +99,103 @@ export async function calcStarredPoints(user: leagueUsers): Promise { connection.end(); return Math.ceil(points * starMultiplier); } +export interface predictions_raw { + club: string; + home?: number; + away?: number; +} +/** + * Calculates the total prediction points based on the provided predictions and actual game results. + * + * @param {predictions_raw[]} predictions - An array of predicted scores for various clubs. + * @param {predictions_raw[]} games - An array of actual game results for various clubs. + * @param {leagueSettings} settings - The league settings that contain the scoring rules for predictions. + * @return {number} The total points accumulated from the predictions based on the scoring rules. + * + * The function iterates through each prediction and compares it against the actual game result for the same club. + * Points are awarded based on: + * - Exact match of predicted and actual scores. + * - Correct prediction of the goal difference. + * - Correct prediction of the match outcome (winner). + */ +export function calcPredicitionPointsRaw( + predictions: predictions_raw[], + games: predictions_raw[], + settings: leagueSettings, +): number { + let points = 0; + for (const prediction of predictions) { + if (prediction.home === undefined || prediction.away === undefined) { + continue; + } + for (const game of games) { + if (game.home === undefined || game.away === undefined) { + continue; + } + if (prediction.club == game.club) { + // Checks if the score was exactly right + if (prediction.home === game.home && prediction.away === game.away) { + points += settings.predictExact; + } + // Checks if the correct difference in points was chosen + else if (prediction.home - prediction.away === game.home - game.away) { + points += settings.predictDifference; + } + // Checks if the correct winner was chosen + else if ( + prediction.home > prediction.away === game.home > game.away && + (prediction.home === prediction.away) === (game.home === game.away) + ) { + points += settings.predictWinner; + } + } + } + } + return points; +} +/** + * Calculates the total prediction points for a given user for a given matchday. + * + * @param {points} matchday - The matchday for which to calculate the prediction points. + * @return {Promise} The total prediction points for the user for the given matchday. + */ +export async function calcHistoricalPredictionPoints( + matchday: points, +): Promise { + const connection = await connect(); + const temp: leagueSettings[] = await connection.query( + "SELECT * FROM leagueSettings WHERE leagueID=?", + [matchday.leagueID], + ); + if (temp.length == 0) { + return 0; + } + const settings: leagueSettings = temp[0]; + const predictions: predictions[] = await connection.query( + "SELECT * FROM historicalPredictions WHERE user=? AND leagueID=? AND matchday=?", + [matchday.user, matchday.leagueID, matchday.matchday], + ); + const games: predictions_raw[] = ( + await connection.query( + "SELECT * FROM historicalClubs WHERE league=? AND home=1 AND time=?", + [settings.league, matchday.time], + ) + ).map((e: historicalClubs) => { + return { + home: e.teamScore, + away: e.opponentScore, + club: e.club, + }; + }); + return calcPredicitionPointsRaw(predictions, games, settings); +} /** * Calculates the prediction points for a given user. * * @param {leagueUsers} user - The user for whom to calculate the prediction points. * @return {Promise} The prediction points for the user. */ -export async function calcPredictionsPoints( +export async function calcPredictionsPointsNow( user: leagueUsers, ): Promise { const connection = await connect(); @@ -116,49 +207,28 @@ export async function calcPredictionsPoints( return 0; } const settings: leagueSettings = temp[0]; - // Changes all the nulls to 0's + // Changes all the nulls to 0's to prevent invalid predictions from existing await connection.query( "UPDATE predictions SET home=IFNULL(home, 0), away=IFNULL(away, 0) WHERE user=? AND leagueID=?", [user.user, user.leagueID], ); - // Gets all the predictions - const predictions: Promise[] = ( - await connection.query( - "SELECT * FROM predictions WHERE user=? AND leagueID=?", - [user.user, user.leagueID], - ) - ).map(async (e: predictions) => { - const game: clubs[] = await connection.query( - "SELECT * FROM clubs WHERE club=? AND league=? AND home=1 AND teamScore IS NOT NULL AND opponentScore IS NOT NULL", - [e.club, e.league], - ); - if (game.length == 0) { - return 0; - } - if ( - game[0].teamScore !== undefined && - game[0].opponentScore !== undefined - ) { - // Checks if the score was exactly right - if (e.home === game[0].teamScore && e.away === game[0].opponentScore) { - return settings.predictExact; - } - // Checks if the correct difference in points was chosen - if (e.home - e.away === game[0].teamScore - game[0].opponentScore) { - return settings.predictDifference; - } - // Checks if the correct winner was chosen - if ( - e.home > e.away === game[0].teamScore > game[0].opponentScore && - (e.home === e.away) === (game[0].teamScore === game[0].opponentScore) - ) { - return settings.predictWinner; - } - } - return 0; + const predictions: predictions[] = await connection.query( + "SELECT * FROM predictions WHERE user=? AND leagueID=?", + [user.user, user.leagueID], + ); + const games: predictions_raw[] = ( + await connection.query("SELECT * FROM clubs WHERE league=? AND home=1", [ + settings.league, + ]) + ).map((e: clubs) => { + return { + club: e.club, + home: e.teamScore, + away: e.opponentScore, + }; }); connection.end(); - return (await Promise.all(predictions)).reduce((a, b) => a + b, 0); + return calcPredicitionPointsRaw(predictions, games, settings); } /** * Calculates and updates the points for the specified league. @@ -250,7 +320,7 @@ export async function calcPoints(league: string | number) { ); }), // Calculates the amont of points the user should have for the matchday in predictions - calcPredictionsPoints(e), + calcPredictionsPointsNow(e), ]); // Checks if the matchday might be different if (e.leagueID !== currentleagueID) {