407 lines
14 KiB
Go
407 lines
14 KiB
Go
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")
|
|
|
|
// Migration: ajouter colonne must_change_password si elle n'existe pas (DEFAULT FALSE pour les clients existants)
|
|
if _, err = database.Exec(`ALTER TABLE clients ADD COLUMN IF NOT EXISTS must_change_password BOOLEAN NOT NULL DEFAULT FALSE`); err != nil {
|
|
log.Fatalf("❌ Erreur migration must_change_password: %v", err)
|
|
}
|
|
|
|
// Migration: ajouter colonne push_token pour les notifications push (clients)
|
|
if _, err = database.Exec(`ALTER TABLE clients ADD COLUMN IF NOT EXISTS push_token TEXT`); err != nil {
|
|
log.Fatalf("❌ Erreur migration push_token: %v", err)
|
|
}
|
|
|
|
// Migration: ajouter colonne push_token pour les notifications push (livreurs/users)
|
|
if _, err = database.Exec(`ALTER TABLE users ADD COLUMN IF NOT EXISTS push_token TEXT`); err != nil {
|
|
log.Fatalf("❌ Erreur migration push_token users: %v", err)
|
|
}
|
|
|
|
// Migration: proposition de modification d'adresse par admin/cabine
|
|
if _, err = database.Exec(`ALTER TABLE commandes ADD COLUMN IF NOT EXISTS proposed_address TEXT`); err != nil {
|
|
log.Fatalf("❌ Erreur migration proposed_address: %v", err)
|
|
}
|
|
if _, err = database.Exec(`ALTER TABLE commandes ADD COLUMN IF NOT EXISTS address_proposal_status VARCHAR(20) NOT NULL DEFAULT 'none'`); err != nil {
|
|
log.Fatalf("❌ Erreur migration address_proposal_status: %v", err)
|
|
}
|
|
|
|
// Migration: ajouter colonne unit pour l'unité de mesure des produits (kg, g, bag, l, cl, pcs, u)
|
|
if _, err = database.Exec(`ALTER TABLE products ADD COLUMN IF NOT EXISTS unit VARCHAR(10) NOT NULL DEFAULT 'u'`); err != nil {
|
|
log.Fatalf("❌ Erreur migration unit products: %v", err)
|
|
}
|
|
|
|
// Migration: baskets.quantity INTEGER → NUMERIC(10,3) pour supporter les quantités fractionnaires (ex: 0.5g)
|
|
if _, err = database.Exec(`
|
|
DO $$
|
|
BEGIN
|
|
IF EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_name = 'baskets' AND column_name = 'quantity'
|
|
AND data_type = 'integer'
|
|
) THEN
|
|
ALTER TABLE baskets ALTER COLUMN quantity TYPE NUMERIC(10,3) USING quantity::NUMERIC(10,3);
|
|
END IF;
|
|
END
|
|
$$;
|
|
`); err != nil {
|
|
log.Fatalf("❌ Erreur migration baskets.quantity: %v", err)
|
|
}
|
|
|
|
// Migration: command_items.quantite INTEGER → NUMERIC(10,3) pour supporter les quantités fractionnaires
|
|
if _, err = database.Exec(`
|
|
DO $$
|
|
BEGIN
|
|
IF EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_name = 'command_items' AND column_name = 'quantite'
|
|
AND data_type = 'integer'
|
|
) THEN
|
|
ALTER TABLE command_items ALTER COLUMN quantite TYPE NUMERIC(10,3) USING quantite::NUMERIC(10,3);
|
|
END IF;
|
|
END
|
|
$$;
|
|
`); err != nil {
|
|
log.Fatalf("❌ Erreur migration command_items.quantite: %v", err)
|
|
}
|
|
|
|
// 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,
|
|
unit VARCHAR(10) NOT NULL DEFAULT 'u',
|
|
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
|
|
}
|