Files
THE-TIP-TOP/backend/src/controllers/game.controller.js

191 lines
5.6 KiB
JavaScript

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: `
<div style="font-family:sans-serif;max-width:480px;margin:auto">
<h2 style="color:#006B6B">Bravo ${req.user.firstname} !</h2>
<p>Votre code <strong>${result.ticket.code}</strong> est gagnant. Vous remportez :</p>
<div style="font-size:20px;font-weight:bold;color:#006B6B;text-align:center;padding:20px;background:#F0F0F0;border-radius:12px">
${result.ticket.lot.name}
</div>
${result.ticket.lot.description ? `<p style="text-align:center;color:#666">${result.ticket.lot.description}</p>` : ""}
<p>Présentez-vous en boutique avec ce code pour retirer votre lot${
gameWindow.claimDeadline
? `, avant le <strong>${gameWindow.claimDeadline.toLocaleDateString("fr-FR")}</strong>`
: ""
}.</p>
<p style="color:#888;font-size:13px">Retrouvez à tout moment vos gains dans votre espace « Mes gains ».</p>
</div>
`,
});
} 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.",
});
}
};
module.exports = {
participate,
myGains,
};