/** * Script d'auto-initialisation de la base de données * Appelé automatiquement au démarrage du backend * * Ce script vérifie si les tables existent et les crée si nécessaire * Il crée également les comptes admin/employé et génère les 500,000 tickets */ import { readFileSync } from 'fs'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; import bcrypt from 'bcrypt'; import { pool } from '../db.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const SALT_ROUNDS = 10; const TOTAL_TICKETS = 500000; const BATCH_SIZE = 5000; const PRIZE_DISTRIBUTION = { 'INFUSEUR': { percentage: 0.60, name: 'Infuseur à thé' }, 'THE_GRATUIT': { percentage: 0.20, name: 'Thé détox/infusion 100g' }, 'THE_SIGNATURE': { percentage: 0.10, name: 'Thé signature 100g' }, 'COFFRET_DECOUVERTE': { percentage: 0.06, name: 'Coffret découverte 39€' }, 'COFFRET_PRESTIGE': { percentage: 0.04, name: 'Coffret prestige 69€' } }; /** * Vérifie si les tables existent dans la base de données */ async function tablesExist() { try { const result = await pool.query(` SELECT COUNT(*) as count FROM information_schema.tables WHERE table_schema = 'public' AND table_name IN ('users', 'prizes', 'tickets', 'game_settings') `); return parseInt(result.rows[0].count) === 4; } catch (error) { console.error('❌ Erreur lors de la vérification des tables:', error.message); return false; } } /** * Vérifie si les tickets ont été générés */ async function ticketsExist() { try { const result = await pool.query('SELECT COUNT(*) as count FROM tickets'); return parseInt(result.rows[0].count) >= TOTAL_TICKETS; } catch { return false; } } /** * Vérifie si les utilisateurs admin/employé existent */ async function usersExist() { try { const result = await pool.query(` SELECT COUNT(*) as count FROM users WHERE email IN ('admin@thetiptop.com', 'employee1@thetiptop.com') `); return parseInt(result.rows[0].count) >= 2; } catch { return false; } } /** * Crée les tables à partir du schema.sql */ async function createTables() { console.log('📦 Création des tables...'); const schemaPath = join(__dirname, '..', 'database', 'schema.sql'); const schema = readFileSync(schemaPath, 'utf-8'); await pool.query(schema); console.log('✅ Tables créées avec succès'); } /** * Crée les utilisateurs par défaut (admin, employés) */ async function createDefaultUsers() { console.log('👥 Création des utilisateurs par défaut...'); const adminPassword = await bcrypt.hash('Admin123!', SALT_ROUNDS); const employeePassword = await bcrypt.hash('Employee123!', SALT_ROUNDS); // Admin await pool.query( `INSERT INTO users (email, password, first_name, last_name, phone, role, is_verified) VALUES ($1, $2, $3, $4, $5, 'ADMIN', TRUE) ON CONFLICT (email) DO UPDATE SET password = EXCLUDED.password, role = 'ADMIN', is_verified = TRUE`, ['admin@thetiptop.com', adminPassword, 'Admin', 'Principal', '+33123456789'] ); console.log(' ✅ Admin créé: admin@thetiptop.com'); // Employé 1 await pool.query( `INSERT INTO users (email, password, first_name, last_name, phone, role, is_verified) VALUES ($1, $2, $3, $4, $5, 'EMPLOYEE', TRUE) ON CONFLICT (email) DO UPDATE SET password = EXCLUDED.password, role = 'EMPLOYEE', is_verified = TRUE`, ['employee1@thetiptop.com', employeePassword, 'Marie', 'Dupont', '+33198765432'] ); console.log(' ✅ Employé créé: employee1@thetiptop.com'); // Employé 2 await pool.query( `INSERT INTO users (email, password, first_name, last_name, phone, role, is_verified) VALUES ($1, $2, $3, $4, $5, 'EMPLOYEE', TRUE) ON CONFLICT (email) DO UPDATE SET password = EXCLUDED.password, role = 'EMPLOYEE', is_verified = TRUE`, ['employee2@thetiptop.com', employeePassword, 'Pierre', 'Martin', '+33187654321'] ); console.log(' ✅ Employé créé: employee2@thetiptop.com'); console.log('✅ Utilisateurs créés avec succès'); } /** * Génère un code de ticket unique */ function generateCode() { const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; let code = 'TT'; for (let i = 0; i < 8; i++) { code += chars[Math.floor(Math.random() * chars.length)]; } return code; } /** * Génère les 500,000 tickets */ async function generateTickets() { console.log('🎫 Génération des 500,000 tickets...'); const prizesResult = await pool.query('SELECT id, name, type FROM prizes'); const prizes = {}; prizesResult.rows.forEach(p => prizes[p.type] = p); // Calculer la distribution const distribution = {}; let total = 0; for (const [type, config] of Object.entries(PRIZE_DISTRIBUTION)) { const count = Math.floor(TOTAL_TICKETS * config.percentage); distribution[type] = count; total += count; } // Ajuster si nécessaire if (total < TOTAL_TICKETS) { distribution['INFUSEUR'] += (TOTAL_TICKETS - total); } // Afficher la distribution console.log(' 📊 Distribution:'); for (const [type, count] of Object.entries(distribution)) { console.log(` - ${type}: ${count.toLocaleString('fr-FR')} tickets`); } // Générer les tickets let generated = 0; const start = Date.now(); for (const [type, count] of Object.entries(distribution)) { const prize = prizes[type]; if (!prize) { console.log(` ⚠️ Lot "${type}" introuvable, ignoré`); continue; } for (let i = 0; i < count; i += BATCH_SIZE) { const batch = Math.min(BATCH_SIZE, count - i); const values = []; for (let j = 0; j < batch; j++) { const code = generateCode(); values.push(`('${code}', '${prize.id}', NULL)`); } const query = `INSERT INTO tickets (code, prize_id, status) VALUES ${values.join(', ')}`; await pool.query(query); generated += batch; // Afficher la progression tous les 50,000 tickets if (generated % 50000 === 0) { console.log(` 📈 Progression: ${generated.toLocaleString('fr-FR')} / ${TOTAL_TICKETS.toLocaleString('fr-FR')}`); } } } const duration = ((Date.now() - start) / 1000).toFixed(2); console.log(`✅ ${generated.toLocaleString('fr-FR')} tickets générés en ${duration}s`); } /** * Applique les migrations (newsletter, email campaigns, etc.) */ async function applyMigrations() { console.log('🔄 Application des migrations...'); try { // Add is_active column to users if not exists await pool.query(` ALTER TABLE users ADD COLUMN IF NOT EXISTS is_active BOOLEAN DEFAULT TRUE `); console.log(' ✅ Colonne is_active ajoutée à users'); // Newsletter table await pool.query(` CREATE TABLE IF NOT EXISTS newsletters ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email VARCHAR(255) UNIQUE NOT NULL, is_subscribed BOOLEAN DEFAULT TRUE, subscribed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, unsubscribed_at TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); console.log(' ✅ Table newsletters créée'); // Email templates table await pool.query(` CREATE TABLE IF NOT EXISTS email_templates ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(255) NOT NULL, subject VARCHAR(500) NOT NULL, html_content TEXT NOT NULL, text_content TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); console.log(' ✅ Table email_templates créée'); // Email campaigns table await pool.query(` CREATE TABLE IF NOT EXISTS email_campaigns ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(255) NOT NULL, template_id UUID REFERENCES email_templates(id), status VARCHAR(50) DEFAULT 'DRAFT', scheduled_at TIMESTAMP, sent_at TIMESTAMP, total_recipients INTEGER DEFAULT 0, sent_count INTEGER DEFAULT 0, failed_count INTEGER DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); console.log(' ✅ Table email_campaigns créée'); // Email campaign recipients table await pool.query(` CREATE TABLE IF NOT EXISTS email_campaign_recipients ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), campaign_id UUID REFERENCES email_campaigns(id) ON DELETE CASCADE, email VARCHAR(255) NOT NULL, status VARCHAR(50) DEFAULT 'PENDING', sent_at TIMESTAMP, error_message TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); console.log(' ✅ Table email_campaign_recipients créée'); // Grand prize draws table await pool.query(` CREATE TABLE IF NOT EXISTS grand_prize_draws ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), winner_id UUID REFERENCES users(id), draw_date TIMESTAMP NOT NULL, prize_description TEXT, is_claimed BOOLEAN DEFAULT FALSE, claimed_at TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); console.log(' ✅ Table grand_prize_draws créée'); console.log('✅ Migrations appliquées avec succès'); } catch (error) { console.log(' ⚠️ Certaines migrations existent déjà:', error.message); } } /** * Fonction principale d'initialisation */ export async function initDatabase() { console.log('\n🚀 ========================================'); console.log(' AUTO-INITIALISATION DE LA BASE DE DONNÉES'); console.log(' ========================================\n'); try { // 1. Vérifier si les tables existent const hasTables = await tablesExist(); if (!hasTables) { console.log('📋 Tables non trouvées, création en cours...\n'); await createTables(); } else { console.log('✅ Tables déjà existantes\n'); } // 2. Vérifier si les utilisateurs existent const hasUsers = await usersExist(); if (!hasUsers) { console.log('👥 Utilisateurs non trouvés, création en cours...\n'); await createDefaultUsers(); } else { console.log('✅ Utilisateurs déjà existants\n'); } // 3. Vérifier si les tickets existent const hasTickets = await ticketsExist(); if (!hasTickets) { console.log('🎫 Tickets non trouvés, génération en cours...\n'); await generateTickets(); } else { console.log('✅ Tickets déjà générés\n'); } // 4. Appliquer les migrations await applyMigrations(); console.log('\n✨ ========================================'); console.log(' BASE DE DONNÉES INITIALISÉE AVEC SUCCÈS'); console.log(' ========================================'); console.log('\n🔐 Comptes disponibles:'); console.log(' Admin: admin@thetiptop.com / Admin123!'); console.log(' Employé 1: employee1@thetiptop.com / Employee123!'); console.log(' Employé 2: employee2@thetiptop.com / Employee123!\n'); return true; } catch (error) { console.error('\n❌ Erreur lors de l\'initialisation:', error.message); console.error(error.stack); return false; } } // Si exécuté directement (pas importé) if (process.argv[1] === fileURLToPath(import.meta.url)) { initDatabase() .then(() => process.exit(0)) .catch(() => process.exit(1)); }