204 lines
6.2 KiB
JavaScript
204 lines
6.2 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.",
|
|
});
|
|
}
|
|
};
|
|
|
|
// 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,
|
|
}; |