merge: integrate Emmanuel latest changes
This commit is contained in:
@@ -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>"
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) },
|
||||
});
|
||||
|
||||
@@ -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: {
|
||||
|
||||
51
backend/src/utils/gameWindow.js
Normal file
51
backend/src/utils/gameWindow.js
Normal 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 };
|
||||
Reference in New Issue
Block a user