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: `
Votre code ${result.ticket.code} est gagnant. Vous remportez :
${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 ».