/** * Controller pour le tirage au sort du gros lot */ import { pool } from '../../db.js'; import { AppError, asyncHandler } from '../middleware/errorHandler.js'; import { sendDrawWinnerEmail } from '../services/email.service.js'; /** * Récupérer la liste des participants éligibles * GET /api/draw/eligible-participants */ export const getEligibleParticipants = asyncHandler(async (req, res) => { const { minTickets = 0, verified = true } = req.query; // Requête pour obtenir les utilisateurs éligibles // Pour le tirage au sort du gros lot, tous les participants ont une chance égale // Note: Les utilisateurs inactifs (compte supprimé) sont inclus s'ils ont des tickets validés const query = ` SELECT u.id, u.email, u.first_name, u.last_name, u.is_verified, u.is_active, u.created_at, COALESCE(COUNT(DISTINCT t.id), 0) as tickets_played, COALESCE(COUNT(DISTINCT CASE WHEN t.status = 'CLAIMED' THEN t.id END), 0) as prizes_won FROM users u LEFT JOIN tickets t ON u.id = t.user_id AND t.played_at IS NOT NULL WHERE u.role = 'CLIENT' ${verified === 'true' ? 'AND u.is_verified = TRUE' : ''} GROUP BY u.id, u.email, u.first_name, u.last_name, u.is_verified, u.is_active, u.created_at ${parseInt(minTickets) > 0 ? 'HAVING COUNT(DISTINCT t.id) >= $1' : ''} ORDER BY u.created_at ASC `; const params = parseInt(minTickets) > 0 ? [parseInt(minTickets)] : []; const result = await pool.query(query, params); res.json({ success: true, data: { participants: result.rows, total: result.rows.length, criteria: { minTickets: parseInt(minTickets), verified: verified === 'true', }, }, }); }); /** * Vérifier si un tirage a déjà été effectué * GET /api/draw/check-existing */ export const checkExistingDraw = asyncHandler(async (req, res) => { const result = await pool.query(` SELECT d.*, u.email as winner_email_current, u.first_name || ' ' || u.last_name as winner_name_current FROM grand_prize_draws d LEFT JOIN users u ON d.winner_id = u.id ORDER BY d.draw_date DESC LIMIT 1 `); res.json({ success: true, data: { hasExistingDraw: result.rows.length > 0, lastDraw: result.rows[0] || null, }, }); }); /** * Lancer le tirage au sort * POST /api/draw/conduct */ export const conductDraw = asyncHandler(async (req, res) => { const { criteria, prizeName, prizeValue } = req.body; const adminId = req.user.id; // Vérifier que le lot "An de thé" existe const grandPrizeExists = await pool.query( "SELECT id FROM prizes WHERE type = 'GRAND_PRIZE' LIMIT 1" ); if (grandPrizeExists.rows.length === 0) { throw new AppError('Le lot grand prize n\'existe pas dans la base de données', 400); } // Note: Le stock n'est plus vérifié pour permettre plusieurs tirages de test // L'administrateur peut annuler un tirage et en relancer un nouveau si nécessaire // Récupérer les participants éligibles const minTickets = criteria?.minTickets || 0; const verified = criteria?.verified !== false; const participantsQuery = ` SELECT u.id, u.email, u.first_name, u.last_name, COALESCE(COUNT(DISTINCT t.id), 0) as tickets_played FROM users u LEFT JOIN tickets t ON u.id = t.user_id AND t.played_at IS NOT NULL WHERE u.role = 'CLIENT' ${verified ? 'AND u.is_verified = TRUE' : ''} GROUP BY u.id, u.email, u.first_name, u.last_name ${parseInt(minTickets) > 0 ? 'HAVING COUNT(DISTINCT t.id) >= $1' : ''} `; const params = parseInt(minTickets) > 0 ? [minTickets] : []; const participantsResult = await pool.query(participantsQuery, params); const eligibleParticipants = participantsResult.rows; if (eligibleParticipants.length === 0) { throw new AppError('Aucun participant éligible trouvé avec ces critères', 400); } // Sélection aléatoire du gagnant const randomIndex = Math.floor(Math.random() * eligibleParticipants.length); const winner = eligibleParticipants[randomIndex]; // Compter le total de tous les participants (pour statistiques) const totalParticipantsResult = await pool.query( "SELECT COUNT(DISTINCT id) as total FROM users WHERE role = 'CLIENT'" ); const totalParticipants = parseInt(totalParticipantsResult.rows[0].total); // Enregistrer le tirage dans la base de données dans une transaction const client = await pool.connect(); try { await client.query('BEGIN'); // Enregistrer le tirage const drawResult = await client.query( `INSERT INTO grand_prize_draws ( conducted_by, winner_id, winner_email, winner_name, prize_name, prize_value, total_participants, eligible_participants, criteria, status ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, 'COMPLETED') RETURNING *`, [ adminId, winner.id, winner.email, `${winner.first_name} ${winner.last_name}`, prizeName || 'Gros lot final', prizeValue || '', totalParticipants, eligibleParticipants.length, JSON.stringify(criteria), ] ); // Note: Le stock n'est plus décrémenté pour permettre plusieurs tirages de test // En production, l'administrateur devra valider le tirage final définitif await client.query('COMMIT'); const draw = drawResult.rows[0]; client.release(); console.log('🎉 Tirage au sort effectué!'); console.log(` Gagnant: ${winner.email}`); console.log(` Participants éligibles: ${eligibleParticipants.length}`); console.log(` Total participants: ${totalParticipants}`); // Envoyer l'email de notification au gagnant try { await sendDrawWinnerEmail(winner.email, winner.first_name, prizeName || 'Un an de thé d\'une valeur de 360€'); console.log(` 📧 Email de notification envoyé à ${winner.email}`); } catch (error) { console.error('Erreur envoi email au gagnant:', error); } res.json({ success: true, message: 'Tirage au sort effectué avec succès!', data: { draw: { id: draw.id, drawDate: draw.draw_date, status: draw.status, }, winner: { id: winner.id, email: winner.email, name: `${winner.first_name} ${winner.last_name}`, ticketsPlayed: winner.tickets_played, }, statistics: { totalParticipants, eligibleParticipants: eligibleParticipants.length, criteria, }, prize: { name: prizeName || 'Gros lot final', value: prizeValue || '', }, }, }); } catch (error) { await client.query('ROLLBACK'); client.release(); throw error; } }); /** * Récupérer l'historique des tirages * GET /api/draw/history */ export const getDrawHistory = asyncHandler(async (req, res) => { const result = await pool.query(` SELECT d.*, u_admin.email as admin_email, u_admin.first_name || ' ' || u_admin.last_name as admin_name FROM grand_prize_draws d LEFT JOIN users u_admin ON d.conducted_by = u_admin.id ORDER BY d.draw_date DESC `); res.json({ success: true, data: { draws: result.rows, total: result.rows.length, }, }); }); /** * Marquer le gagnant comme notifié * PUT /api/draw/:id/notify */ export const markAsNotified = asyncHandler(async (req, res) => { const { id } = req.params; const result = await pool.query( `UPDATE grand_prize_draws SET status = 'NOTIFIED', notified_at = CURRENT_TIMESTAMP WHERE id = $1 RETURNING *`, [id] ); if (result.rows.length === 0) { throw new AppError('Tirage non trouvé', 404); } res.json({ success: true, message: 'Gagnant marqué comme notifié', data: result.rows[0], }); }); /** * Marquer le lot comme récupéré * PUT /api/draw/:id/claim */ export const markAsClaimed = asyncHandler(async (req, res) => { const { id } = req.params; const { notes } = req.body; const result = await pool.query( `UPDATE grand_prize_draws SET status = 'CLAIMED', claimed_at = CURRENT_TIMESTAMP, notes = $2 WHERE id = $1 RETURNING *`, [id, notes || null] ); if (result.rows.length === 0) { throw new AppError('Tirage non trouvé', 404); } res.json({ success: true, message: 'Lot marqué comme récupéré', data: result.rows[0], }); }); /** * Générer un rapport du tirage au sort * GET /api/draw/:id/report */ export const generateReport = asyncHandler(async (req, res) => { const { id } = req.params; const drawResult = await pool.query( `SELECT d.*, u_admin.email as admin_email, u_admin.first_name || ' ' || u_admin.last_name as admin_name, u_winner.email as winner_email_current, u_winner.first_name as winner_first_name, u_winner.last_name as winner_last_name, u_winner.phone, u_winner.city FROM grand_prize_draws d LEFT JOIN users u_admin ON d.conducted_by = u_admin.id LEFT JOIN users u_winner ON d.winner_id = u_winner.id WHERE d.id = $1`, [id] ); if (drawResult.rows.length === 0) { throw new AppError('Tirage non trouvé', 404); } const draw = drawResult.rows[0]; // Récupérer les tickets du gagnant const ticketsResult = await pool.query( `SELECT t.code, t.played_at, t.status, p.name as prize_name FROM tickets t LEFT JOIN prizes p ON t.prize_id = p.id WHERE t.user_id = $1 AND t.played_at IS NOT NULL ORDER BY t.played_at DESC`, [draw.winner_id] ); const report = { draw: { id: draw.id, date: draw.draw_date, conductedBy: { name: draw.admin_name, email: draw.admin_email, }, status: draw.status, notifiedAt: draw.notified_at, claimedAt: draw.claimed_at, notes: draw.notes, }, prize: { name: draw.prize_name, value: draw.prize_value, }, winner: { id: draw.winner_id, email: draw.winner_email_current, firstName: draw.winner_first_name, lastName: draw.winner_last_name, phone: draw.phone, city: draw.city, tickets: ticketsResult.rows, totalTickets: ticketsResult.rows.length, }, statistics: { totalParticipants: draw.total_participants, eligibleParticipants: draw.eligible_participants, criteria: draw.criteria, }, }; res.json({ success: true, data: report, }); }); /** * Annuler/Supprimer un tirage au sort * DELETE /api/draw/:id */ export const deleteDraw = asyncHandler(async (req, res) => { const { id } = req.params; const adminId = req.user.id; // Vérifier que le tirage existe const drawCheck = await pool.query( 'SELECT * FROM grand_prize_draws WHERE id = $1', [id] ); if (drawCheck.rows.length === 0) { throw new AppError('Tirage non trouvé', 404); } const draw = drawCheck.rows[0]; // Supprimer le tirage await pool.query('DELETE FROM grand_prize_draws WHERE id = $1', [id]); console.log('🗑️ Tirage au sort annulé!'); console.log(` ID du tirage: ${id}`); console.log(` Gagnant: ${draw.winner_email}`); console.log(` Annulé par: Admin ID ${adminId}`); res.json({ success: true, message: 'Tirage au sort annulé avec succès', data: { deletedDrawId: id, deletedDraw: { winner: draw.winner_name, email: draw.winner_email, prize: draw.prize_name, date: draw.draw_date, }, }, }); }); export default { getEligibleParticipants, checkExistingDraw, conductDraw, getDrawHistory, markAsNotified, markAsClaimed, generateReport, deleteDraw, };