the-tip-top-backend/src/controllers/draw.controller.js
soufiane 7f4d4c35be feat: add email notifications for registration, account deletion, and draw winner
- 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>
2025-11-30 15:26:44 +01:00

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,
};