const prisma = require("../config/prisma"); // Le concours traverse cinq états, et non deux : confondre « pas encore // ouvert » avec « terminé » produit des messages faux côté joueur et laisse // passer le tirage au sort avant le début du jeu. const PHASES = { NOT_CONFIGURED: "NOT_CONFIGURED", SCHEDULED: "SCHEDULED", OPEN: "OPEN", CLAIM_ONLY: "CLAIM_ONLY", CLOSED: "CLOSED", }; const GAME_DURATION_DAYS = 30; const CLAIM_EXTENSION_DAYS = 30; const DAY_MS = 24 * 60 * 60 * 1000; // Deux échéances distinctes à partir de startDate : // - participationDeadline (30 j) : dernier moment pour tester un code ; // - claimDeadline (60 j) : dernier moment pour retirer un lot déjà gagné. // Les 30 jours supplémentaires servent uniquement au retrait, pas au jeu. // Tant qu'aucune GameSettings n'existe, le jeu est considéré comme non // configuré et donc ouvert (comportement de dev par défaut). async function getGameWindow(now = new Date()) { const settings = await prisma.gameSettings.findFirst({ orderBy: { id: "asc" } }); // Tant que l'administrateur n'a pas fixé de date, le concours n'existe pas : // rien n'est jouable et aucun tirage n'est possible. Ouvrir par défaut // reviendrait à faire gagner des lots dans un jeu qui n'a jamais été lancé. if (!settings) { return { configured: false, phase: PHASES.NOT_CONFIGURED, startDate: null, participationDeadline: null, claimDeadline: null, isParticipationOpen: false, isOpen: false, }; } const participationDeadline = new Date(settings.startDate.getTime() + GAME_DURATION_DAYS * DAY_MS); const claimDeadline = new Date( settings.startDate.getTime() + (GAME_DURATION_DAYS + CLAIM_EXTENSION_DAYS) * DAY_MS ); const hasStarted = now >= settings.startDate; const isParticipationOpen = hasStarted && now <= participationDeadline; const isOpen = hasStarted && now <= claimDeadline; let phase; if (!hasStarted) phase = PHASES.SCHEDULED; else if (isParticipationOpen) phase = PHASES.OPEN; else if (isOpen) phase = PHASES.CLAIM_ONLY; else phase = PHASES.CLOSED; return { configured: true, phase, startDate: settings.startDate, participationDeadline, claimDeadline, hasStarted, isParticipationOpen, isOpen, }; } // L'expiration d'un gain est *dérivée* de la fenêtre de jeu, jamais écrite en // base : un PENDING dont le délai de retrait est passé est présenté comme // CANCELLED, mais la ligne reste intacte. Corriger une startDate erronée // suffit donc à rétablir les gains, et aucune lecture ne modifie la base. function effectiveClaimStatus(status, gameWindow) { if (status === "PENDING" && !gameWindow.isOpen) return "CANCELLED"; return status; } // Message destiné au joueur qui tente de jouer hors de la fenêtre : chaque // phase a sa propre explication, avec la date utile quand elle existe. function participationClosedMessage(gameWindow) { switch (gameWindow.phase) { case PHASES.NOT_CONFIGURED: return "Le jeu-concours n'est pas encore ouvert. Revenez prochainement."; case PHASES.SCHEDULED: return `Le jeu-concours ouvrira le ${gameWindow.startDate.toLocaleDateString("fr-FR")}.`; case PHASES.CLAIM_ONLY: return "La période de participation est terminée. Vous pouvez encore retirer les lots déjà gagnés."; default: return "Le jeu-concours est terminé. Vous ne pouvez plus tester de code ni réclamer de lot."; } } module.exports = { getGameWindow, effectiveClaimStatus, participationClosedMessage, PHASES, GAME_DURATION_DAYS, CLAIM_EXTENSION_DAYS, };