changement

This commit is contained in:
Emma781227
2026-07-23 13:45:53 +02:00
parent 7f816c165b
commit 902e304a5e
8 changed files with 131 additions and 4 deletions

View File

@@ -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 <engongomo.emma27@gmail.com>"
MAIL_USER="your_email@gmail.com"
MAIL_PASS="your_app_password"
MAIL_FROM="Thé Tip Top <your_email@gmail.com>"

View File

@@ -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")
);

View File

@@ -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())
}

View File

@@ -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()

View File

@@ -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,

View File

@@ -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) },
});

View File

@@ -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: {

View File

@@ -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 };