- Add welcome email sent on user registration - Add account deletion confirmation email - Add draw winner notification email with celebratory design - Remove email verification requirement on registration - All emails have HTML templates with responsive design 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
438 lines
12 KiB
JavaScript
438 lines
12 KiB
JavaScript
/**
|
|
* 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,
|
|
};
|