From 902e304a5eb5e6ff30f756199c3b9b9db2dc47ab Mon Sep 17 00:00:00 2001 From: Emma781227 Date: Thu, 23 Jul 2026 13:45:53 +0200 Subject: [PATCH] changement --- backend/.env.example | 7 ++- .../migration.sql | 8 +++ backend/prisma/schema.prisma | 9 ++++ backend/prisma/seed.js | 8 +++ backend/src/controllers/admin.controller.js | 37 ++++++++++++++ .../src/controllers/employee.controller.js | 5 ++ backend/src/controllers/game.controller.js | 10 ++++ backend/src/utils/gameWindow.js | 51 +++++++++++++++++++ 8 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 backend/prisma/migrations/20260721093615_add_game_settings/migration.sql create mode 100644 backend/src/utils/gameWindow.js diff --git a/backend/.env.example b/backend/.env.example index 2ca9d51..08da568 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -7,7 +7,6 @@ JWT_SECRET="your_secret_key" APP_URL="http://localhost:5000" MAIL_HOST="smtp.gmail.com" MAIL_PORT="587" -MAIL_USER="engongomo.emma27@gmail.com" -MAIL_PASS="rczo zqps hsnm msdm" -MAIL_FROM="Thé Tip Top " - +MAIL_USER="your_email@gmail.com" +MAIL_PASS="your_app_password" +MAIL_FROM="Thé Tip Top " diff --git a/backend/prisma/migrations/20260721093615_add_game_settings/migration.sql b/backend/prisma/migrations/20260721093615_add_game_settings/migration.sql new file mode 100644 index 0000000..7146bc8 --- /dev/null +++ b/backend/prisma/migrations/20260721093615_add_game_settings/migration.sql @@ -0,0 +1,8 @@ +-- CreateTable +CREATE TABLE "GameSettings" ( + "id" SERIAL NOT NULL, + "startDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "GameSettings_pkey" PRIMARY KEY ("id") +); diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index a2f3543..7ab6470 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -116,3 +116,12 @@ model GrandPrizeWinner { prize String @default("Un an de thé d'une valeur de 360€") drawnAt DateTime @default(now()) } + +// Ligne unique de configuration : date de début du jeu-concours. +// Participation et réclamation des lots sont autorisées pendant 60 jours +// (30 jours de jeu + 30 jours supplémentaires) à compter de startDate. +model GameSettings { + id Int @id @default(autoincrement()) + startDate DateTime @default(now()) + createdAt DateTime @default(now()) +} diff --git a/backend/prisma/seed.js b/backend/prisma/seed.js index 206ba20..370608d 100644 --- a/backend/prisma/seed.js +++ b/backend/prisma/seed.js @@ -39,6 +39,14 @@ async function main() { }); console.log("Lots créés avec succès"); + + const existingSettings = await prisma.gameSettings.findFirst(); + if (!existingSettings) { + await prisma.gameSettings.create({ data: {} }); + console.log("Date de début du jeu-concours initialisée à aujourd'hui."); + } else { + console.log("Date de début du jeu-concours déjà définie, inchangée."); + } } main() diff --git a/backend/src/controllers/admin.controller.js b/backend/src/controllers/admin.controller.js index 2260d3c..8651bf0 100644 --- a/backend/src/controllers/admin.controller.js +++ b/backend/src/controllers/admin.controller.js @@ -1,8 +1,41 @@ const bcrypt = require("bcrypt"); const prisma = require("../config/prisma"); +const { getGameWindow, expireOverdueClaims } = require("../utils/gameWindow"); + +const AGE_BRACKETS = ["-18", "18-24", "25-34", "35-44", "45-54", "55-64", "65+"]; + +function getAgeBracket(birthdate) { + if (!birthdate) return "unknown"; + + const now = new Date(); + let age = now.getFullYear() - birthdate.getFullYear(); + const hasHadBirthdayThisYear = + now.getMonth() > birthdate.getMonth() || + (now.getMonth() === birthdate.getMonth() && now.getDate() >= birthdate.getDate()); + if (!hasHadBirthdayThisYear) age -= 1; + + if (age < 18) return "-18"; + if (age <= 24) return "18-24"; + if (age <= 34) return "25-34"; + if (age <= 44) return "35-44"; + if (age <= 54) return "45-54"; + if (age <= 64) return "55-64"; + return "65+"; +} + +function computeAgeStats(users) { + const counts = Object.fromEntries([...AGE_BRACKETS, "unknown"].map((b) => [b, 0])); + for (const user of users) { + counts[getAgeBracket(user.birthdate)] += 1; + } + return counts; +} const getStats = async (req, res) => { try { + await expireOverdueClaims(); + const gameWindow = await getGameWindow(); + const [ ticketsTotal, ticketsUsed, @@ -13,6 +46,7 @@ const getStats = async (req, res) => { users, men, women, + clientsWithBirthdate, ] = await Promise.all([ prisma.ticket.count(), prisma.ticket.count({ where: { isUsed: true } }), @@ -23,6 +57,7 @@ const getStats = async (req, res) => { prisma.user.count({ where: { role: "CLIENT" } }), prisma.user.count({ where: { role: "CLIENT", gender: "Homme" } }), prisma.user.count({ where: { role: "CLIENT", gender: "Femme" } }), + prisma.user.findMany({ where: { role: "CLIENT" }, select: { birthdate: true } }), ]); const lotsStats = await prisma.lot.findMany({ @@ -48,6 +83,8 @@ const getStats = async (req, res) => { claimsDelivered, claimsCancelled, users, + gameWindow, + ageStats: computeAgeStats(clientsWithBirthdate), genderStats: { men, women, diff --git a/backend/src/controllers/employee.controller.js b/backend/src/controllers/employee.controller.js index be501ac..7dff989 100644 --- a/backend/src/controllers/employee.controller.js +++ b/backend/src/controllers/employee.controller.js @@ -1,9 +1,12 @@ const prisma = require("../config/prisma"); +const { expireOverdueClaims } = require("../utils/gameWindow"); const getGainByTicketCode = async (req, res) => { try { const { code } = req.params; + await expireOverdueClaims(); + const ticket = await prisma.ticket.findUnique({ where: { code: code.toUpperCase() }, include: { @@ -66,6 +69,8 @@ const deliverClaim = async (req, res) => { const { id } = req.params; const employeeId = req.user.id; + await expireOverdueClaims(); + const claim = await prisma.claim.findUnique({ where: { id: Number(id) }, }); diff --git a/backend/src/controllers/game.controller.js b/backend/src/controllers/game.controller.js index 5477fca..31f3345 100644 --- a/backend/src/controllers/game.controller.js +++ b/backend/src/controllers/game.controller.js @@ -1,10 +1,18 @@ const prisma = require("../config/prisma"); +const { getGameWindow, expireOverdueClaims } = require("../utils/gameWindow"); const participate = async (req, res) => { try { const { code } = req.body; const userId = req.user.id; + const gameWindow = await getGameWindow(); + if (!gameWindow.isOpen) { + return res.status(410).json({ + message: "Le jeu-concours est terminé. Vous ne pouvez plus tester de code ni réclamer de lot.", + }); + } + const ticket = await prisma.ticket.findUnique({ where: { code }, include: { @@ -83,6 +91,8 @@ const myGains = async (req, res) => { try { const userId = req.user.id; + await expireOverdueClaims(); + const participations = await prisma.participation.findMany({ where: { userId }, include: { diff --git a/backend/src/utils/gameWindow.js b/backend/src/utils/gameWindow.js new file mode 100644 index 0000000..eaa34f1 --- /dev/null +++ b/backend/src/utils/gameWindow.js @@ -0,0 +1,51 @@ +const prisma = require("../config/prisma"); + +const GAME_DURATION_DAYS = 30; +const CLAIM_EXTENSION_DAYS = 30; +const DAY_MS = 24 * 60 * 60 * 1000; + +// Fenêtre pendant laquelle un joueur peut à la fois tester son code et +// réclamer son lot : 30 jours de jeu + 30 jours supplémentaires à compter +// de startDate. Tant qu'aucune GameSettings n'existe, le jeu est considéré +// comme non démarré et donc ouvert (comportement de dev par défaut). +async function getGameWindow() { + const settings = await prisma.gameSettings.findFirst({ orderBy: { id: "asc" } }); + + if (!settings) { + return { + configured: false, + startDate: null, + participationDeadline: null, + claimDeadline: null, + isOpen: true, + }; + } + + 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 + ); + + return { + configured: true, + startDate: settings.startDate, + participationDeadline, + claimDeadline, + isOpen: new Date() <= claimDeadline, + }; +} + +// Bascule en CANCELLED les gains PENDING non réclamés une fois la fenêtre +// fermée. Appelé au fil de l'eau (pas de cron) avant toute lecture/écriture +// de claims, ce qui suffit pour ce projet. +async function expireOverdueClaims() { + const window = await getGameWindow(); + if (window.isOpen) return; + + await prisma.claim.updateMany({ + where: { status: "PENDING" }, + data: { status: "CANCELLED" }, + }); +} + +module.exports = { getGameWindow, expireOverdueClaims };