const prisma = require("../config/prisma"); const transporter = require("../config/mailer"); const { getGameWindow, effectiveClaimStatus, participationClosedMessage, } = require("../utils/gameWindow"); // Levée par la transaction quand le ticket a été consommé entre-temps. class TicketDejaUtiliseError extends Error {} const participate = async (req, res) => { try { const { code } = req.body; const userId = req.user.id; const gameWindow = await getGameWindow(); // Un code n'est jouable que pendant les 30 jours de jeu : ni avant // l'ouverture, ni pendant la prolongation réservée aux retraits. if (!gameWindow.isParticipationOpen) { return res.status(410).json({ message: participationClosedMessage(gameWindow), phase: gameWindow.phase, }); } const ticket = await prisma.ticket.findUnique({ where: { code }, include: { lot: true, }, }); if (!ticket) { return res.status(404).json({ message: "Code invalide.", }); } if (ticket.isUsed) { return res.status(409).json({ message: "Ce code a déjà été utilisé.", }); } const result = await prisma.$transaction(async (tx) => { // Le contrôle isUsed ci-dessus ne suffit pas : deux requêtes simultanées // peuvent le franchir ensemble. On repasse donc par un update // conditionnel, atomique, qui ne touche le ticket que s'il est encore // libre — le perdant de la course obtient 0 ligne modifiée. const { count } = await tx.ticket.updateMany({ where: { id: ticket.id, isUsed: false }, data: { isUsed: true, usedAt: new Date(), userId, }, }); if (count === 0) throw new TicketDejaUtiliseError(); const updatedTicket = await tx.ticket.findUnique({ where: { id: ticket.id }, include: { lot: true }, }); const participation = await tx.participation.create({ data: { userId, ticketId: updatedTicket.id, lotId: updatedTicket.lotId, }, }); const claim = await tx.claim.create({ data: { participationId: participation.id, status: "PENDING", }, }); return { ticket: updatedTicket, participation, claim, }; }); // La participation est déjà enregistrée et consultable dans « Mes gains » : // un email qui ne part pas ne doit pas l'annuler ni consommer le ticket // pour rien. On se contente de signaler l'échec. let emailSent = true; try { await transporter.sendMail({ from: process.env.MAIL_FROM, to: req.user.email, subject: `Vous avez gagné : ${result.ticket.lot.name} !`, html: `

Bravo ${req.user.firstname} !

Votre code ${result.ticket.code} est gagnant. Vous remportez :

${result.ticket.lot.name}
${result.ticket.lot.description ? `

${result.ticket.lot.description}

` : ""}

Présentez-vous en boutique avec ce code pour retirer votre lot${ gameWindow.claimDeadline ? `, avant le ${gameWindow.claimDeadline.toLocaleDateString("fr-FR")}` : "" }.

Retrouvez à tout moment vos gains dans votre espace « Mes gains ».

`, }); } catch (mailError) { emailSent = false; console.error("Erreur envoi email de gain:", mailError); } return res.status(201).json({ message: "Participation enregistrée avec succès.", emailSent, gain: { name: result.ticket.lot.name, description: result.ticket.lot.description, value: result.ticket.lot.value, }, claim: { id: result.claim.id, status: result.claim.status, }, }); } catch (error) { if (error instanceof TicketDejaUtiliseError) { return res.status(409).json({ message: "Ce code a déjà été utilisé.", }); } console.error("Erreur participation:", error); return res.status(500).json({ message: "Erreur serveur lors de la participation.", }); } }; const myGains = async (req, res) => { try { const userId = req.user.id; const gameWindow = await getGameWindow(); const participations = await prisma.participation.findMany({ where: { userId }, include: { ticket: true, lot: true, claim: true, }, orderBy: { createdAt: "desc", }, }); return res.json({ gains: participations.map((participation) => ({ participationId: participation.id, ticketCode: participation.ticket.code, lot: participation.lot.name, description: participation.lot.description, value: participation.lot.value, claimStatus: participation.claim ? effectiveClaimStatus(participation.claim.status, gameWindow) : undefined, wonAt: participation.createdAt, })), }); } catch (error) { console.error("Erreur historique gains:", error); return res.status(500).json({ message: "Erreur serveur lors de la récupération des gains.", }); } }; // Dates publiques du jeu-concours, affichées sur la page d'accueil : elles // suivent la date de début fixée par l'admin. const getPublicGameWindow = async (req, res) => { try { const { configured, phase, startDate, participationDeadline, claimDeadline } = await getGameWindow(); return res.json({ configured, phase, startDate, participationDeadline, claimDeadline }); } catch (error) { console.error("Erreur lecture dates du jeu:", error); return res.status(500).json({ message: "Erreur serveur lors de la lecture des dates du jeu-concours." }); } }; module.exports = { participate, myGains, getPublicGameWindow, };