package db import ( "database/sql" "fmt" "log" "os" "time" _ "github.com/lib/pq" ) // ============================================ // STRUCTURES // ============================================ // Database encapsule la connexion à la base de données type Database struct { *sql.DB } // DB est l'instance globale de la base de données var DB *Database // ============================================ // INITIALISATION DE LA BASE DE DONNÉES // ============================================ // InitDB initialise la connexion à PostgreSQL et crée les tables func InitDB() *Database { // Récupérer les paramètres de connexion host := getEnv("DB_HOST", "localhost") port := getEnv("DB_PORT", "5432") user := getEnv("DB_USER", "postgres") password := getEnv("DB_PASSWORD", "postgres") dbname := getEnv("DB_NAME", "gestion_db") sslmode := getEnv("DB_SSLMODE", "disable") // Construire la chaîne de connexion connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s", host, port, user, password, dbname, sslmode) // Ouvrir la connexion db, err := sql.Open("postgres", connStr) if err != nil { log.Fatalf("❌ Erreur lors de l'ouverture de la base de données: %v", err) } // Configuration du pool de connexions db.SetMaxOpenConns(50) db.SetMaxIdleConns(10) db.SetConnMaxLifetime(5 * time.Minute) // Tester la connexion if err = db.Ping(); err != nil { log.Fatalf("❌ Erreur de connexion à la base de données: %v", err) } log.Println("✅ Connexion à PostgreSQL établie avec succès") // Créer l'instance Database database := &Database{db} // Assigner à la variable globale DB = database // Créer les tables if err = database.createTables(); err != nil { log.Fatalf("❌ Erreur lors de la création des tables: %v", err) } log.Println("✅ Tables créées avec succès") // Lancer le nettoyage périodique des tokens expirés go database.cleanExpiredTokensPeriodically() return database } // ============================================ // CRÉATION DES TABLES // ============================================ // createTables crée toutes les tables nécessaires func (db *Database) createTables() error { queries := []string{ // ============================ // TABLE users // ============================ `CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, username VARCHAR(255) UNIQUE NOT NULL, password TEXT NOT NULL, role VARCHAR(50) NOT NULL DEFAULT 'user', total NUMERIC(10,2) DEFAULT 0.0, livraison NUMERIC(10,2) DEFAULT 0.0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );`, // ============================ // TABLE clients - ✅ AVEC COLONNES DE TRACKING DES ANNULATIONS // ============================ `CREATE TABLE IF NOT EXISTS clients ( id SERIAL PRIMARY KEY, username VARCHAR(255) UNIQUE NOT NULL, password TEXT NOT NULL, nom VARCHAR(100) NOT NULL, prenom VARCHAR(100) NOT NULL, telephone VARCHAR(20) NOT NULL UNIQUE, command INTEGER DEFAULT 0, point INTEGER DEFAULT 0, point_zipette INTEGER DEFAULT 0, amende NUMERIC(10,2) DEFAULT 0.0, cancel_commande INTEGER DEFAULT 0, cancellations_count INTEGER DEFAULT 0 NOT NULL, last_penalty_reason TEXT DEFAULT NULL, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );`, // ============================ // TABLE jwt_tokens // ============================ `CREATE TABLE IF NOT EXISTS jwt_tokens ( id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL, user_type VARCHAR(20) NOT NULL CHECK (user_type IN ('client', 'admin', 'cabine', 'livreur')), token TEXT NOT NULL, date_save TIMESTAMP DEFAULT CURRENT_TIMESTAMP, date_fin TIMESTAMP NOT NULL, CHECK (date_fin > date_save) );`, // ============================ // TABLE products // ============================ `CREATE TABLE IF NOT EXISTS products ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, category VARCHAR(100) NOT NULL, stock DECIMAL(10,2) NOT NULL DEFAULT 0, description TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );`, // ============================ // TABLE product_prices // ============================ `CREATE TABLE IF NOT EXISTS product_prices ( id SERIAL PRIMARY KEY, product_id INTEGER NOT NULL REFERENCES products(id) ON DELETE CASCADE, quantity NUMERIC(10,3) NOT NULL, price NUMERIC(10,2) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(product_id, quantity) );`, // ============================ // TABLE media // ============================ `CREATE TABLE IF NOT EXISTS media ( id SERIAL PRIMARY KEY, product_id INTEGER NOT NULL REFERENCES products(id) ON DELETE CASCADE, url TEXT NOT NULL, type VARCHAR(50) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );`, // ============================ // TABLE commandes // ============================ `CREATE TABLE IF NOT EXISTS commandes ( id SERIAL PRIMARY KEY, username VARCHAR(255) NOT NULL, status VARCHAR(50) NOT NULL DEFAULT 'pending', livreur_assign VARCHAR(50), adresse TEXT DEFAULT 'Adresse non spécifiée', total_prix NUMERIC(10,2) NOT NULL DEFAULT 0.0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );`, // ============================ // TABLE command_items // ============================ `CREATE TABLE IF NOT EXISTS command_items ( id SERIAL PRIMARY KEY, command_id INTEGER NOT NULL REFERENCES commandes(id) ON DELETE CASCADE, produit VARCHAR(255) NOT NULL, product_id INTEGER REFERENCES products(id) ON DELETE SET NULL, quantite INTEGER NOT NULL, prix NUMERIC(10,2) NOT NULL, -- Nouvelles colonnes pour les infos client client_username VARCHAR(255), client_nom VARCHAR(100), client_prenom VARCHAR(100), client_telephone VARCHAR(20), delivery_address TEXT, status VARCHAR(50) DEFAULT 'pending', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );`, // ============================ // TABLE baskets // ============================ `CREATE TABLE IF NOT EXISTS baskets ( id SERIAL PRIMARY KEY, username VARCHAR(255) NOT NULL, product_id INTEGER NOT NULL REFERENCES products(id) ON DELETE CASCADE, quantity NUMERIC(10,3) NOT NULL, price NUMERIC(10,2) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );`, // ============================ // TABLE command_logs // ============================ `CREATE TABLE IF NOT EXISTS command_logs ( id SERIAL PRIMARY KEY, command_id INTEGER NOT NULL REFERENCES commandes(id) ON DELETE CASCADE, status VARCHAR(50) NOT NULL, message TEXT NOT NULL, author VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );`, // ============================ // TABLE delivery_issues // ============================ `CREATE TABLE IF NOT EXISTS delivery_issues ( id SERIAL PRIMARY KEY, command_id INTEGER NOT NULL REFERENCES commandes(id) ON DELETE CASCADE, issue_type VARCHAR(50) NOT NULL, description TEXT NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'open', reported_by VARCHAR(100) NOT NULL, resolved_by VARCHAR(100), resolution TEXT, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP );`, `CREATE TABLE IF NOT EXISTS alerte_policy ( id SERIAL PRIMARY KEY, username VARCHAR(100) NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'false', created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP );`, `CREATE TABLE IF NOT EXISTS adresse_correction ( id SERIAL PRIMARY KEY, invalid_address VARCHAR(255) NOT NULL UNIQUE, correct_address VARCHAR(255) NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP );`, // ============================ // INDEXES - ✅ AJOUT D'INDEX POUR CANCELLATIONS_COUNT // ============================ `CREATE INDEX IF NOT EXISTS idx_command_items_command_id ON command_items(command_id);`, `CREATE INDEX IF NOT EXISTS idx_command_items_client_username ON command_items(client_username);`, `CREATE INDEX IF NOT EXISTS idx_command_items_status ON command_items(status);`, `CREATE INDEX IF NOT EXISTS idx_command_items_produit ON command_items(produit);`, `CREATE INDEX IF NOT EXISTS idx_clients_point_zipette ON clients(point_zipette) WHERE point_zipette > 0;`, `CREATE INDEX IF NOT EXISTS idx_jwt_user_id ON jwt_tokens(user_id);`, `CREATE INDEX IF NOT EXISTS idx_jwt_user_type ON jwt_tokens(user_type);`, `CREATE INDEX IF NOT EXISTS idx_jwt_token ON jwt_tokens(token);`, `CREATE INDEX IF NOT EXISTS idx_jwt_date_fin ON jwt_tokens(date_fin);`, `CREATE INDEX IF NOT EXISTS idx_clients_telephone ON clients(telephone);`, `CREATE INDEX IF NOT EXISTS idx_clients_cancellations ON clients(cancellations_count);`, `CREATE INDEX IF NOT EXISTS idx_clients_amende ON clients(amende) WHERE amende > 0;`, `CREATE INDEX IF NOT EXISTS idx_cmd_username ON commandes(username);`, `CREATE INDEX IF NOT EXISTS idx_cmd_status ON commandes(status);`, `CREATE INDEX IF NOT EXISTS idx_cmd_livreur ON commandes(livreur_assign);`, `CREATE INDEX IF NOT EXISTS idx_products_category ON products(category);`, `CREATE INDEX IF NOT EXISTS idx_media_product_id ON media(product_id);`, `CREATE INDEX IF NOT EXISTS idx_price_product_id ON product_prices(product_id);`, `CREATE INDEX IF NOT EXISTS idx_items_command_id ON command_items(command_id);`, `CREATE INDEX IF NOT EXISTS idx_items_product_id ON command_items(product_id);`, `CREATE INDEX IF NOT EXISTS idx_baskets_username ON baskets(username);`, `CREATE INDEX IF NOT EXISTS idx_baskets_product_id ON baskets(product_id);`, `CREATE INDEX IF NOT EXISTS idx_logs_command_id ON command_logs(command_id);`, `CREATE INDEX IF NOT EXISTS idx_issues_command ON delivery_issues(command_id);`, `CREATE INDEX IF NOT EXISTS idx_issues_status ON delivery_issues(status);`, `CREATE INDEX IF NOT EXISTS idx_issues_reported_by ON delivery_issues(reported_by);`, } for _, query := range queries { if _, err := db.Exec(query); err != nil { return fmt.Errorf("❌ Erreur SQL: %v\nRequête: %s", err, query) } } log.Println("✅ Toutes les tables PostgreSQL créées avec succès.") return nil } // ============================================ // MÉTHODES POUR LES TOKENS JWT // ============================================ // CleanExpiredTokens supprime les tokens JWT expirés func (d *Database) CleanExpiredTokens() error { query := `DELETE FROM jwt_tokens WHERE date_fin < $1` result, err := d.Exec(query, time.Now()) if err != nil { return err } rowsAffected, _ := result.RowsAffected() if rowsAffected > 0 { log.Printf("🧹 %d token(s) expiré(s) supprimé(s)", rowsAffected) } return nil } // cleanExpiredTokensPeriodically nettoie les tokens expirés toutes les heures func (db *Database) cleanExpiredTokensPeriodically() { ticker := time.NewTicker(1 * time.Hour) defer ticker.Stop() for range ticker.C { if err := db.CleanExpiredTokens(); err != nil { log.Printf("⚠️ Erreur lors du nettoyage des tokens: %v", err) } } } // getEnv récupère une variable d'environnement ou retourne une valeur par défaut func getEnv(key, defaultValue string) string { if value := os.Getenv(key); value != "" { return value } return defaultValue }