# TODO-API.md

## 📋 Liste des tâches à implémenter

### 🔴 PRIORITÉ HAUTE

#### 1. Système de backup pour les suppressions (DELETE)

**Demandé le :** 20/08/2025
**Objectif :** Sauvegarder toutes les données supprimées (soft delete) dans un fichier SQL pour pouvoir les restaurer en cas d'erreur humaine.

**Détails techniques :**

- Créer un système de backup automatique lors de chaque DELETE
- Stocker les données dans un fichier SQL avec structure permettant la réintégration facile
- Format suggéré : `/backups/deleted/{année}/{mois}/deleted_{table}_{YYYYMMDD}.sql`

**Tables concernées :**

- `ope_pass` (passages) - DELETE /passages/{id}
- `users` (utilisateurs) - DELETE /users/{id}
- `operations` (opérations) - DELETE /operations/{id}
- `ope_sectors` (secteurs) - DELETE /sectors/{id}

**Structure du backup suggérée :**

```sql
-- Backup deletion: ope_pass
-- Date: 2025-08-20 14:30:45
-- User: 9999985 (cv_mobile)
-- Entity: 5
-- Original ID: 19500576

INSERT INTO ope_pass_backup (
    original_id,
    deleted_at,
    deleted_by_user_id,
    deleted_by_entity_id,
    -- tous les champs originaux
    fk_operation,
    fk_sector,
    fk_user,
    montant,
    encrypted_name,
    encrypted_email,
    -- etc...
) VALUES (
    19500576,
    '2025-08-20 14:30:45',
    9999985,
    5,
    -- valeurs originales
    ...
);

-- Pour restauration facile :
-- UPDATE ope_pass SET chk_active = 1 WHERE id = 19500576;
```

**Fonctionnalités à implémenter :**

1. **Service de backup** : `BackupService.php`

   - Méthode `backupDeletedRecord($table, $id, $data)`
   - Génération automatique du SQL de restauration
   - Rotation des fichiers (garder 90 jours)

2. **Intégration dans les controllers**

   - Ajouter l'appel au BackupService avant chaque soft delete
   - Logger l'emplacement du backup

3. **Interface de restauration** (optionnel)

   - Endpoint GET /api/backups/deleted pour lister les backups
   - Endpoint POST /api/backups/restore/{backup_id} pour restaurer

4. **Commande de restauration manuelle**
   - Script PHP : `php scripts/restore_deleted.php --table=ope_pass --id=19500576`

**Avantages :**

- Traçabilité complète des suppressions
- Restauration rapide en cas d'erreur
- Audit trail pour conformité
- Tranquillité d'esprit pour le client

---

### 🔴 PRIORITÉ HAUTE

#### 2. Migration des bases de données vers container maria3 centralisé

**Demandé le :** 07/10/2025
**Objectif :** Migrer les bases de données locales des containers dva-geo et rca-geo vers un container MariaDB centralisé maria3 sur le même host IN3.

**Architecture actuelle :**

- **dva-geo** : MariaDB local avec base `geo_app` (localhost)
- **rca-geo** : MariaDB local avec base `geo_app` (localhost)
- **maria3** : Container MariaDB 11.4 existant (IP: 13.23.33.4) - utilisé uniquement pour la base `adresses`

**Architecture cible :**

- **maria3** : Container centralisé avec :
  - Base `dva_geo` pour l'environnement DEV
  - Base `rca_geo` pour l'environnement RECETTE
  - Base `adresses` (déjà existante)
- **dva-geo** : Suppression du serveur MariaDB local
- **rca-geo** : Suppression du serveur MariaDB local

**Avantages :**

- ✅ Centralisation des bases de données
- ✅ Facilite les sauvegardes
- ✅ Réduction de la consommation mémoire des containers API
- ✅ Séparation claire des responsabilités (API vs DB)
- ✅ Préparation pour architecture production (IN4/maria4/pra_geo)

---

#### 📋 TODOLIST DÉTAILLÉE - ENVIRONNEMENT DVA-GEO (DEV)

##### Phase 1️⃣ : Préparation et sauvegarde (AVANT migration)

- [x] **1.1** Vérifier l'état actuel de la base dans dva-geo

  ```bash
  incus exec dva-geo -- mysql -u root -pMyAlpineDb.90b -e "SHOW DATABASES;"
  incus exec dva-geo -- mysql -u root -pMyAlpineDb.90b geo_app -e "SHOW TABLES;"
  incus exec dva-geo -- mysql -u root -pMyAlpineDb.90b geo_app -e "SELECT COUNT(*) FROM users;"
  ```

- [x] **1.2** Créer une sauvegarde complète de la base actuelle

  ```bash
  # Dump avec skip-lock-tables (vue problématique v_stripe_amicale_dashboard)
  incus exec dva-geo -- mariadb-dump -u root -pMyAlpineDb.90b --skip-lock-tables geo_app > /var/back/dva_geo_backup_20251007.sql
  ```

- [x] **1.3** Vérifier l'intégrité de la sauvegarde

  ```bash
  ls -lh /var/back/dva_geo_backup_20251007.sql
  # Taille : 1.2GB (1217046899 bytes)
  ```

- [x] **1.4** Supprimer la vue problématique et refaire un dump propre

  ```bash
  # Suppression de la vue cassée
  incus exec dva-geo -- mysql -u root -pMyAlpineDb.90b geo_app -e "DROP VIEW IF EXISTS v_stripe_amicale_dashboard;"

  # Nouveau dump complet et propre
  incus exec dva-geo -- mariadb-dump -u root -pMyAlpineDb.90b geo_app > /var/back/dva_geo_backup_final_20251007.sql
  ```

##### Phase 2️⃣ : Configuration de maria3

- [x] **2.1** Se connecter à maria3 et créer la base dva_geo

  ```bash
  incus exec maria3 -- mysql -u root -p'MyAlpLocal,90b' -e "CREATE DATABASE IF NOT EXISTS dva_geo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
  ```

- [x] **2.2** Créer l'utilisateur dédié dva_geo_user

  ```bash
  incus exec maria3 -- mysql -u root -p'MyAlpLocal,90b' -e "
  CREATE USER IF NOT EXISTS 'dva_geo_user'@'%' IDENTIFIED BY 'CBq9tKHj6PGPZuTmAHV7';
  GRANT ALL PRIVILEGES ON dva_geo.* TO 'dva_geo_user'@'%';
  FLUSH PRIVILEGES;"
  ```

- [x] **2.3** Vérifier les permissions

  ```bash
  incus exec maria3 -- mysql -u root -p'MyAlpLocal,90b' -e "SHOW GRANTS FOR 'dva_geo_user'@'%';"
  # Résultat : ALL PRIVILEGES sur dva_geo.*
  ```

- [x] **2.4** Tester la connexion avec le nouvel utilisateur depuis dva-geo
  ```bash
  incus exec dva-geo -- mysql -h 13.23.33.4 -u dva_geo_user -p'CBq9tKHj6PGPZuTmAHV7' -e "SHOW DATABASES;"
  # ✅ Connexion réussie
  ```

##### Phase 3️⃣ : Migration des données

- [x] **3.1** Copier le dump depuis le host vers maria3

  ```bash
  incus file push /var/back/dva_geo_backup_final_20251007.sql maria3/tmp/
  # ✅ Fichier transféré
  ```

- [x] **3.2** Importer le dump dans maria3

  ```bash
  incus exec maria3 -- mysql -u root -p'MyAlpLocal,90b' dva_geo < /tmp/dva_geo_backup_final_20251007.sql
  # ✅ Import réussi
  ```

- [x] **3.3** Vérifier l'importation

  ```bash
  incus exec maria3 -- mysql -u root -p'MyAlpLocal,90b' dva_geo -e "SHOW TABLES;"
  incus exec maria3 -- mysql -u root -p'MyAlpLocal,90b' dva_geo -e "SELECT COUNT(*) FROM users;"
  incus exec maria3 -- mysql -u root -p'MyAlpLocal,90b' dva_geo -e "SELECT COUNT(*) FROM operations;"
  incus exec maria3 -- mysql -u root -p'MyAlpLocal,90b' dva_geo -e "SELECT COUNT(*) FROM ope_pass;"
  # ✅ Toutes les tables présentes
  ```

- [x] **3.4** Comparer les comptages avec la base source

  ```bash
  # Base source (dva-geo localhost)
  incus exec dva-geo -- mysql -u root -pMyAlpineDb.90b geo_app -e "
  SELECT 'users' as table_name, COUNT(*) as count FROM users
  UNION ALL SELECT 'operations', COUNT(*) FROM operations
  UNION ALL SELECT 'ope_pass', COUNT(*) FROM ope_pass
  UNION ALL SELECT 'entites', COUNT(*) FROM entites;"

  # Base cible (maria3)
  incus exec maria3 -- mysql -u root -p'MyAlpLocal,90b' dva_geo -e "
  SELECT 'users' as table_name, COUNT(*) as count FROM users
  UNION ALL SELECT 'operations', COUNT(*) FROM operations
  UNION ALL SELECT 'ope_pass', COUNT(*) FROM ope_pass
  UNION ALL SELECT 'entites', COUNT(*) FROM entites;"

  # ✅ Comptages identiques confirmés
  ```

##### Phase 4️⃣ : Configuration de l'API

- [x] **4.1** Mettre à jour AppConfig.php dans dva-geo

  ```php
  // Fichier: src/Config/AppConfig.php (local)
  // Lignes 153-165

  // Configuration maria3 activée (migration effectuée le 07/10/2025)
  'database' => [
      'host' => '13.23.33.4',  // Container maria3 sur IN3
      'name' => 'dva_geo',
      'username' => 'dva_geo_user',
      'password' => 'CBq9tKHj6PGPZuTmAHV7',
  ],
  ```

- [x] **4.2** Déployer la nouvelle configuration

  ```bash
  # Depuis /home/pierre/dev/geosector/api
  ./deploy-api.sh
  # ✅ Déploiement réussi sur dva-geo
  ```

- [x] **4.3** Redémarrer PHP-FPM
  ```bash
  incus exec dva-geo -- rc-service php-fpm83 restart
  # ✅ PHP-FPM redémarré
  ```

##### Phase 5️⃣ : Tests de l'API

- [x] **5.1** Tester la connexion API à la base

  ```bash
  curl -X GET https://dapp.geosector.fr/api/health
  # ✅ API opérationnelle
  ```

- [x] **5.2** Tester l'authentification

  ```bash
  curl -X POST https://dapp.geosector.fr/api/login \
    -H "Content-Type: application/json" \
    -d '{"username":"pv_admin","password":"mot_de_passe"}'
  # ✅ Login réussi
  ```

- [x] **5.3** Tester la récupération de données

  ```bash
  curl -X GET https://dapp.geosector.fr/api/users \
    -H "Authorization: Bearer {session_id}"
  # ✅ Données récupérées depuis maria3
  ```

- [x] **5.4** Tester l'application Flutter

  ```bash
  # Test depuis l'application mobile Flutter
  # ✅ Connexion, récupération données, création passages : OK
  ```

- [x] **5.5** Vérifier les logs
  ```bash
  incus exec dva-geo -- tail -50 /var/www/geosector/api/logs/app.log
  incus exec dva-geo -- tail -50 /var/log/nginx/dva-api-error.log
  # ✅ Aucune erreur détectée
  ```

##### Phase 6️⃣ : Nettoyage et suppression de MariaDB local

⚠️ **Migration validée et fonctionnelle** - Nettoyage effectué le 07/10/2025

- [x] **6.1** Faire une dernière sauvegarde de sécurité

  ```bash
  incus exec dva-geo -- mariadb-dump -u root -pMyAlpineDb.90b --skip-lock-tables geo_app > /var/back/dva_geo_FINAL_backup_$(date +%Y%m%d_%H%M%S).sql
  # ✅ Sauvegarde finale créée
  ```

- [x] **6.2** Arrêter le serveur MariaDB local dans dva-geo

  ```bash
  incus exec dva-geo -- rc-service mariadb stop
  # ✅ MariaDB arrêté
  ```

- [x] **6.3** Désactiver le démarrage automatique de MariaDB

  ```bash
  incus exec dva-geo -- rc-update del mariadb default
  # ✅ MariaDB retiré du runlevel default
  ```

- [x] **6.4** Désinstaller MariaDB

  ```bash
  incus exec dva-geo -- apk del mariadb mariadb-client mariadb-common
  # ✅ MariaDB désinstallé du container
  ```

- [x] **6.5** Archiver les données MariaDB locales

  ```bash
  incus exec dva-geo -- tar -czf /var/back/mysql_data_archive_$(date +%Y%m%d).tar.gz /var/lib/mysql
  # ✅ Archive créée
  ```

- [x] **6.6** Supprimer les données MariaDB locales
  ```bash
  incus exec dva-geo -- rm -rf /var/lib/mysql /run/mysqld
  # ✅ Données supprimées, espace disque libéré
  ```

##### Phase 7️⃣ : Documentation

- [x] **7.1** Mettre à jour TODO-API.md avec toutes les commandes

  - Toutes les phases documentées avec commandes réelles
  - Statut mis à jour : ✅ TERMINÉ

- [x] **7.2** Documenter les problèmes rencontrés

  - **Problème 1** : Vue `v_stripe_amicale_dashboard` cassée
    - **Solution** : Suppression de la vue avant dump final
    - **Fichier** : `scripts/migrations/stripe_tables.sql` (ligne 183)
  - **Problème 2** : Erreur LOCK TABLES lors du dump initial
    - **Solution** : Ajout de `--skip-lock-tables` au mysqldump

- [ ] **7.3** Mettre à jour TECHBOOK.md avec la nouvelle architecture
  - Section "Base de données" (lignes 136-154)
  - Mettre à jour le tableau pour refléter la configuration DVA-GEO → maria3

---

#### 📋 PROCHAINES ÉTAPES (APRÈS DVA-GEO)

Une fois la migration DVA-GEO validée et stable :

### ✅ Migration RCA-GEO (RECETTE) - TERMINÉE

**Statut :** ✅ **MIGRATION COMPLÉTÉE AVEC SUCCÈS** (16/10/2025)

**Préparation effectuée le 07/10/2025** :

- ✅ Base `rca_geo` créée dans maria3 (vide)
- ✅ Utilisateur `rca_geo_user` créé avec ALL PRIVILEGES
- ✅ Vue problématique `v_stripe_amicale_dashboard` supprimée de la base source (rca-geo localhost)

**Migration réalisée le 16/10/2025** :

- ✅ Dump de la base `geo_app` depuis rca-geo (localhost)
- ✅ Import dans maria3 `rca_geo`
- ✅ Validation des comptages (toutes les tables migrées correctement)
- ✅ Modification AppConfig.php lignes 113-128
- ✅ Déploiement manuel sur rca-geo
- ✅ Tests complets sur https://rapp.geosector.fr/api/
- ✅ Suppression complète de MariaDB local dans rca-geo
- ✅ Suppression des dossiers `/var/lib/mysql` et `/run/mysqld`

**Configuration finale** :

- User : `rca_geo_user`
- Password : `UPf3C0cQ805LypyM71iW`
- Host : `13.23.33.4` (maria3 sur IN3)
- Base : `rca_geo`
- Privileges : ALL PRIVILEGES sur rca_geo.\*

**Résultat** :

- Application fonctionnelle en RECETTE avec base centralisée sur maria3
- Container rca-geo allégé (plus de serveur MariaDB local)
- Architecture cohérente DVA-GEO et RCA-GEO → maria3

---

### 🔵 Création environnement PRODUCTION (PLANIFIÉ JEUDI 10/10/2025 16h)

**Architecture cible** :

- Serveur IN4 (51.159.7.190)
- Container `pra-geo` pour l'API (exporté depuis dva-geo)
- Container `maria4` pour la base de données
- Base : `pra_geo` avec utilisateur dédié
- Données dupliquées depuis rca_geo (IN3/maria3)

---

#### 📋 TODOLIST DÉTAILLÉE - ENVIRONNEMENT PRODUCTION (PRA-GEO)

##### Phase 0️⃣ : Préparation du serveur IN4

- [x] **0.1** Vérifier l'accès SSH au serveur IN4

  ```bash
  ssh root@51.159.7.190
  # ✅ Serveur accessible
  ```

- [x] **0.2** Vérifier Incus sur IN4

  ```bash
  ssh root@51.159.7.190 "incus list"
  # ✅ Incus opérationnel
  ```

- [x] **0.3** Vérifier l'espace disque disponible

  ```bash
  ssh root@51.159.7.190 "df -h"
  # ✅ Espace suffisant
  ```

- [x] **0.4** Préparer le répertoire de transfert
  ```bash
  ssh root@51.159.7.190 "mkdir -p /var/back/imports"
  # ✅ Répertoire créé
  ```

##### Phase 1️⃣ : Export du container dva-geo depuis IN3

- [x] **1.1** Export dva-geo réalisé

  ```bash
  # ✅ Container dva-geo exporté depuis IN3
  # Note: Export effectué, détails exacts non documentés
  ```

- [x] **1.2** Snapshot et archive créés
  ```bash
  # ✅ Archive dva-geo créée et transférée vers IN4
  ```

##### Phase 2️⃣ : Transfert vers IN4

- [x] **2.1** Archive transférée vers IN4
  ```bash
  # ✅ Archive transférée sur IN4
  ```

##### Phase 3️⃣ : Import et configuration de pra-geo sur IN4

- [x] **3.1** Container pra-geo importé et lancé

  ```bash
  # ✅ Container pra-geo créé sur IN4
  # IP: 13.23.33.22 (réseau Incus)
  ```

- [x] **3.2** Configuration réseau vérifiée

  ```bash
  # ✅ Réseau configuré, ping et connectivité OK
  ```

- [x] **3.3** Container pra-geo opérationnel

  ```bash
  # ✅ Container démarré et accessible
  ```

- [x] **3.4** Client MariaDB installé
  ```bash
  # ✅ mariadb-client installé sur pra-geo
  ```

##### Phase 4️⃣ : Création du container maria4 sur IN4

- [x] **4.1** Container maria4 créé

  ```bash
  # ✅ Container maria4 créé sur IN4
  # IP: 13.23.33.4 (même IP que maria3 pour cohérence)
  ```

- [x] **4.2** MariaDB installé et initialisé

  ```bash
  # ✅ MariaDB 11.4 installé et démarré
  # ✅ Mot de passe root: MyAlpLocal,90b
  ```

- [x] **4.3** Connexions distantes autorisées

  ```bash
  # ✅ bind-address = 0.0.0.0
  # ✅ MariaDB redémarré
  ```

- [x] **4.4** Base pra_geo créée

  ```bash
  # ✅ Base: pra_geo (utf8mb4_unicode_ci)
  # ✅ User: pra_geo_user / d2jAAGGWi8fxFrWgXjOA
  # ✅ ALL PRIVILEGES accordés
  ```

- [x] **4.5** Base adresses présente

  ```bash
  # ✅ Base adresses avec tables par département (cp01, cp02, etc.)
  # ✅ User: adr_geo_user@13.23.33.2% / d66,AdrGeoPrd.User
  # ✅ SELECT privileges accordés
  ```

- [x] **4.6** Firewall UFW configuré

  ```bash
  # ✅ UFW: allow from 13.23.33.0/24 to any port 3306
  # ✅ Connexions depuis pra-geo (13.23.33.22) opérationnelles
  ```

- [x] **4.7** Tests de connexion réussis
  ```bash
  # ✅ pra-geo → maria4 (pra_geo): OK - 24 090 users
  # ✅ pra-geo → maria4 (adresses): OK - Tables accessibles
  ```

##### Phase 5️⃣ : Migration des données depuis dva_geo (IN3/maria3)

- [x] **5.1** Dump dva_geo réalisé

  ```bash
  # ✅ Dump de dva_geo (sans vue problématique v_stripe_amicale_dashboard)
  # Source: dva_geo (maria3 sur IN3)
  ```

- [x] **5.2** Données importées dans pra_geo

  ```bash
  # ✅ Import réussi dans maria4/pra_geo
  ```

- [x] **5.3** Vérification des données
  ```bash
  # ✅ 24 090 utilisateurs dans pra_geo
  # ✅ Tables operations, ope_pass, entites présentes
  ```

**Note** : La base `pra_geo` a été initialisée depuis `dva_geo` (DEV) et non depuis `rca_geo` (REC).
Pour la mise en production finale, il faudra probablement migrer depuis `rca_geo` (données de recette validées).

##### Phase 6️⃣ : Configuration de l'API pour PRODUCTION

- [x] **6.1** AppConfig.php modifié localement

  ```php
  // Configuration PRODUCTION (lignes 84-111)
  'database' => [
    'host' => '13.23.33.4',  // ✅ maria4 sur IN4
    'name' => 'pra_geo',
    'username' => 'pra_geo_user',
    'password' => 'd2jAAGGWi8fxFrWgXjOA',
  ],
  'addresses_database' => [
    'host' => '13.23.33.4',  // ✅ maria4 sur IN4
    'name' => 'adresses',
    'username' => 'adr_geo_user',
    'password' => 'd66,AdrGeoPrd.User',
  ],
  ```

- [ ] **6.2** Déployer en PRODUCTION

  ```bash
  # ⏳ EN ATTENTE validation client
  ./deploy-api.sh pra
  ```

- [ ] **6.3** Redémarrer PHP-FPM
  ```bash
  # ⏳ À faire après déploiement
  ssh root@51.159.7.190 "incus exec pra-geo -- rc-service php-fpm83 restart"
  ```

##### Phase 7️⃣ : Configuration DNS et reverse proxy

- [ ] **7.1** Vérifier la configuration NGINX sur IN4

  ```bash
  ssh root@51.159.7.190 "cat /etc/nginx/sites-available/app.geosector.fr"
  ```

- [ ] **7.2** Configurer le reverse proxy vers pra-geo

  ```nginx
  # /etc/nginx/sites-available/app.geosector.fr
  upstream pra_geo_backend {
      server 13.23.34.43:80;
  }

  server {
      listen 443 ssl http2;
      server_name app.geosector.fr;

      ssl_certificate /etc/letsencrypt/live/app.geosector.fr/fullchain.pem;
      ssl_certificate_key /etc/letsencrypt/live/app.geosector.fr/privkey.pem;

      location / {
          proxy_pass http://pra_geo_backend;
          proxy_set_header Host $host;
          proxy_set_header X-Real-IP $remote_addr;
          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
          proxy_set_header X-Forwarded-Proto $scheme;
      }
  }
  ```

- [ ] **7.3** Activer le site et recharger NGINX

  ```bash
  ssh root@51.159.7.190 "ln -s /etc/nginx/sites-available/app.geosector.fr /etc/nginx/sites-enabled/"
  ssh root@51.159.7.190 "nginx -t"
  ssh root@51.159.7.190 "systemctl reload nginx"
  ```

- [ ] **7.4** Vérifier le certificat SSL
  ```bash
  ssh root@51.159.7.190 "certbot certificates | grep app3.geosector.fr"
  # Si pas de certificat, le créer :
  # certbot certonly --nginx -d app3.geosector.fr
  ```

##### Phase 8️⃣ : Tests de l'API PRODUCTION

- [ ] **8.1** Tester le endpoint health

  ```bash
  curl -X GET https://app3.geosector.fr/api/health
  # ✅ Doit retourner {"status":"ok"}
  ```

- [ ] **8.2** Tester l'authentification

  ```bash
  curl -X POST https://app3.geosector.fr/api/login \
    -H "Content-Type: application/json" \
    -d '{"username":"test_user","password":"test_pass"}'
  # ✅ Login doit fonctionner
  ```

- [ ] **8.3** Tester la récupération de données

  ```bash
  curl -X GET https://app3.geosector.fr/api/users \
    -H "Authorization: Bearer {session_id}"
  # ✅ Données récupérées depuis maria4/pra_geo
  ```

- [ ] **8.4** Tester depuis l'application Flutter

  ```bash
  # Configurer l'app Flutter avec l'URL de production
  # Tester : Login, récupération opérations, création passage
  # ✅ Toutes les fonctionnalités opérationnelles
  ```

- [ ] **8.5** Vérifier les logs
  ```bash
  ssh root@51.159.7.190 "incus exec pra-geo -- tail -50 /var/www/geosector/api/logs/app.log"
  ssh root@51.159.7.190 "incus exec pra-geo -- tail -50 /var/log/nginx/pra-api-error.log"
  # ✅ Aucune erreur critique
  ```

##### Phase 9️⃣ : Sécurisation et monitoring

- [ ] **9.1** Configurer les sauvegardes automatiques de maria4

  ```bash
  # Créer un script de backup quotidien
  ssh root@51.159.7.190 "cat > /root/backup-maria4.sh << 'EOF'
  #!/bin/bash
  DATE=$(date +%Y%m%d_%H%M%S)
  incus exec maria4 -- mysqldump -u root -p'MyAlpLocal,90b' --all-databases > /var/back/maria4_backup_${DATE}.sql
  # Garder les 30 derniers jours
  find /var/back/maria4_backup_*.sql -mtime +30 -delete
  EOF"
  ssh root@51.159.7.190 "chmod +x /root/backup-maria4.sh"
  ```

- [ ] **9.2** Configurer la tâche CRON pour les backups

  ```bash
  ssh root@51.159.7.190 "crontab -l | { cat; echo '0 2 * * * /root/backup-maria4.sh'; } | crontab -"
  ```

- [ ] **9.3** Configurer les logs de rotation

  ```bash
  # Vérifier que logrotate est configuré pour NGINX et PHP
  ssh root@51.159.7.190 "cat /etc/logrotate.d/nginx"
  ```

- [ ] **9.4** Configurer le monitoring (optionnel)

  ```bash
  # Installer un monitoring basique (htop, iotop, etc.)
  ssh root@51.159.7.190 "apt install htop iotop iftop -y"
  ```

- [ ] **9.5** Documenter les IPs et accès

  ```bash
  # Créer un fichier README sur IN4
  ssh root@51.159.7.190 "cat > /root/PRODUCTION_INFO.txt << 'EOF'
  ========================================
  ENVIRONNEMENT PRODUCTION GEOSECTOR
  ========================================

  Serveur : IN4 (51.159.7.190)
  Date de création : $(date +%Y-%m-%d)

  Containers :
  - pra-geo (API) : 13.23.34.43
  - maria4 (DB)   : 13.23.34.4

  Base de données :
  - Base : pra_geo
  - User : pra_geo_user
  - Password : d2jAAGGWi8fxFrWgXjOA

  URLs :
  - API : https://app3.geosector.fr/api/
  - Flutter : https://app3.geosector.fr/

  Backups :
  - Quotidien à 2h : /var/back/maria4_backup_*.sql
  - Rétention : 30 jours
  ========================================
  EOF"
  ```

##### Phase 🔟 : Documentation finale

- [ ] **10.1** Mettre à jour TECHBOOK.md

  - Ajouter la configuration PRODUCTION dans le tableau (ligne 142)
  - Documenter l'architecture complète 3 environnements

- [ ] **10.2** Mettre à jour ce TODO-API.md

  - Marquer toutes les étapes comme complétées
  - Statut : ✅ TERMINÉ

- [ ] **10.3** Créer un document de procédures d'urgence
  - Rollback en cas de problème
  - Contacts et escalade
  - Procédure de restauration depuis backup

---

**Date de création TODO :** 07/10/2025
**Date planifiée d'exécution :** Jeudi 10/10/2025 à 16h
**Date de début :** 07/10/2025
**Statut :** 🟡 EN COURS - Phases 0 à 5 complétées, en attente validation client
**Durée estimée :** 3-4 heures

---

#### 🔐 Informations sensibles (à ne pas commiter)

**Container maria3 (IN3)** :

- IP interne : 13.23.33.4
- Root password : `MyAlpLocal,90b`
- Port : 3306

**Utilisateurs bases de données** :

- DEV : `dva_geo_user` / `CBq9tKHj6PGPZuTmAHV7`
- REC : `rca_geo_user` / `UPf3C0cQ805LypyM71iW` (à créer)
- PROD : `pra_geo_user` / mot de passe à générer

---

**Date de création :** 07/10/2025
**Statut :** ✅ TERMINÉ - Migration DVA-GEO complétée avec succès
**Dernière mise à jour :** 07/10/2025

---

### 🟢 Migration de données PM7 → PRA-GEO (PRODUCTION)

#### 3. Script de migration depuis backup PM7 restauré

**Demandé le :** 07/10/2025
**Objectif :** Créer un script PHP standalone pour migrer les données depuis un backup PM7 (restauré dans maria4) vers la base pra_geo.

**Contexte :**

- Backup quotidien de PM7 (11.1.2.17) : `geosector_YYYYMMDD.sql.tar.gz.enc`
- Processus : Déchiffrement → SCP vers IN4 → Import dans maria4 → Migration vers pra_geo
- Script exécuté depuis le container **pra-geo** (pas maria4, car PHP nécessaire)

**Architecture de migration :**

```
PM7 (11.1.2.17) - Backup nocturne chiffré
  ↓ Déchiffrement (decpm7.sh)
  ↓ SCP vers IN4:/var/back/
  ↓ incus file push vers maria4:/var/back/
  ↓ Décompression et import
maria4 (IN4) - Base geosector_YYYYMMDD
  ↓ Migration (script PHP depuis pra-geo)
maria4 (IN4) - Base pra_geo
```

**Fichier créé :** `scripts/php/migrate_from_backup.php`

**Fonctionnalités implémentées :**

✅ **1. Respect des contraintes FK**

- Ordre de migration : x\_\* → entites → users → operations → ope_sectors → ope_pass → medias
- Gestion des dépendances : x_devises → x_pays → x_regions → x_departements → x_villes

✅ **2. Chiffrement AES-256-CBC**

- Utilisation de `ApiService::encryptData()` et `ApiService::encryptSearchableData()`
- Tables concernées : entites (name, phone, email, iban, bic), users (name, email, phone), ope_pass (name, email, phone)

✅ **3. Mappings de champs**

- `rowid` → `id`
- `active` → `chk_active`
- `date_creat` → `created_at`
- `date_modif` → `updated_at`
- `libelle` → `encrypted_name` (entites)
- `nom` → `encrypted_name` (users, passages)

✅ **4. Détection mobile/fixe**

- Numéros commençant par 06/07 → champ `encrypted_mobile`
- Autres numéros → champ `encrypted_phone`

✅ **5. Support deux modes**

- **Global** : Migration de toutes les amicales
- **Entity** : Migration d'une amicale spécifique (avec tables de référence)

✅ **6. Optimisations**

- Traitement par lots de 1000 pour `ope_pass` (table volumineuse)
- `ON DUPLICATE KEY UPDATE` pour idempotence
- Logging détaillé avec progression

**Utilisation :**

```bash
# Sur IN4, dans le container pra-geo
cd /var/www/geosector/api

# Migration globale (toutes les amicales)
php scripts/php/migrate_from_backup.php \
  --source-db=geosector_20251007 \
  --target-db=pra_geo \
  --mode=global

# Migration d'une amicale spécifique
php scripts/php/migrate_from_backup.php \
  --source-db=geosector_20251007 \
  --target-db=pra_geo \
  --mode=entity \
  --entity-id=45 \
  --log=/var/www/geosector/api/logs/migration_entity_45.log
```

**Tables migrées (dans l'ordre) :**

1. `x_devises`, `x_entites_types`, `x_types_passages`, `x_types_reglements`, `x_users_roles`, `x_users_titres`
2. `x_pays`, `x_regions`, `x_departements`, `x_villes`
3. `entites` (avec chiffrement)
4. `users` (avec chiffrement et détection mobile)
5. `operations`
6. `ope_sectors`
7. `sectors_adresses`
8. `ope_users`
9. `ope_users_sectors`
10. `ope_pass` (avec chiffrement, traitement par lots)
11. `ope_pass_histo`
12. `medias`

**Configuration connexion :**

- Host : `13.23.33.4` (maria4 sur IN4)
- Base source : `geosector_YYYYMMDD` (user root)
- Base cible : `pra_geo` (user pra_geo_user)

**Documentation :** `scripts/README-migration.md`

**Statut :** ✅ TERMINÉ - Script standalone créé et prêt à être testé
**Date de réalisation :** 07/10/2025

---

#### 🔧 Correction critique du mapping des secteurs

**Date :** 08/10/2025
**Problème identifié :** Bug majeur dans la migration des secteurs

**Contexte du bug :**
Le script initial lisait depuis la table `ope_sectors` dans la base SOURCE, alors que cette table n'existe QUE dans la base CIBLE. Dans la base source (geosector), les secteurs sont stockés dans la table `sectors`.

**Architecture des secteurs :**

```
SOURCE (geosector_YYYYMMDD):
  - sectors (rowid, libelle, sector, color)
  - ope_users_sectors (fk_operation, fk_user, fk_sector → sectors.rowid)
  - ope_pass (fk_sector → sectors.rowid)
  - sectors_adresses (fk_sector → sectors.rowid)

CIBLE (pra_geo):
  - ope_sectors (id AUTO_INCREMENT, fk_operation, fk_old_sector, libelle, sector, color)
  - ope_users_sectors (fk_sector → ope_sectors.id NOUVEAU)
  - ope_pass (fk_sector → ope_sectors.id NOUVEAU)
  - sectors_adresses (fk_sector → ope_sectors.id NOUVEAU)
```

**Corrections apportées :**

✅ **1. Migration ope_sectors (lignes 650-772)**

- Lecture depuis `sectors` (source) via JOIN avec `ope_users_sectors`
- Création dans `ope_sectors` (cible) avec ID auto-increment
- Génération d'un mapping : `fk_operation . '_' . old_sector_id → new_sector_id`
- Stockage dans `$this->sectorMapping` pour utilisation par les tables suivantes

✅ **2. Migration sectors_adresses (lignes 774-881)**

- Utilise le mapping pour remplacer `fk_sector` ancien par nouveau ID
- Ignore les adresses dont le secteur n'a pas été migré (compteur `$skipped`)

✅ **3. Migration ope_users_sectors (lignes 952-1040)**

- Utilise le mapping pour remplacer `fk_sector` ancien par nouveau ID
- Ignore les associations dont le secteur n'a pas été migré

✅ **4. Migration ope_pass (lignes 1042-1200)**

- Ajout du mapping au début de la boucle (lignes 1120-1131)
- Utilise le mapping pour remplacer `fk_sector` ancien par nouveau ID
- Ignore les passages dont le secteur n'a pas été migré

**Exemple de mapping créé :**

```php
$this->sectorMapping = [
    '12345_789' => 1001,  // opération 12345, ancien secteur 789 → nouveau secteur 1001
    '12345_790' => 1002,
    '12346_789' => 1003,  // Même ancien secteur, mais autre opération = autre nouveau ID
];
```

**Impact :** CRITIQUE - Sans cette correction, la migration aurait échoué complètement

**Fichiers modifiés :**

- `scripts/php/migrate_from_backup.php` (corrections lignes 650-1200)

**Statut :** ✅ CORRIGÉ - Prêt pour tests
**Date de correction :** 08/10/2025

---

#### 📋 Préparation migration en batch (406 entités éligibles)

**Date :** 08/10/2025
**Objectif :** Migrer progressivement les entités actives depuis geosector_20251008

**Critères de sélection des entités :**

- Plus de 4 utilisateurs actifs
- Au moins 1 opération créée après le 2023-08-01
- Code postal renseigné (nettoyage de 3 entités de test)

**Fichiers créés :**

1. **`scripts/migrations_entites.json`** (406 entités)

   - Liste complète des entités éligibles avec statistiques
   - Format : entity_id, code_postal, nom, ville, nb_users, nb_operations, nb_passages

2. **`scripts/migrate_batch.sh`** (script bash orchestrateur)
   - Migration entité par entité avec progression
   - Options : `--start N`, `--limit N`, `--dry-run`, `--continue`
   - Logs détaillés par entité + log global
   - Gestion d'erreurs et possibilité de reprise
   - Résumé final avec durée, succès, erreurs

**Utilisation du script batch :**

```bash
# Test avec 1 entité
./migrate_batch.sh --start 1 --limit 1

# Migration de 50 entités
./migrate_batch.sh --start 1 --limit 50

# Migration complète (406 entités) avec continuité sur erreur
./migrate_batch.sh --continue

# Mode simulation (dry-run)
./migrate_batch.sh --dry-run
```

**Statistiques des 406 entités :**

- Total utilisateurs : ~15 000 - 20 000 (estimation)
- Total passages : ~500 000 - 1 000 000 (estimation)
- Durée estimée migration complète : 8-12 heures

**Statut :** ✅ PRÉPARÉ - Scripts prêts, tests à effectuer
**Date de préparation :** 08/10/2025

---

#### 🔧 Correction critique des doublons ope_users et ope_users_sectors

**Date :** 10/10/2025
**Problème identifié :** Doublons massifs dans les tables `ope_users` et `ope_users_sectors` lors de la migration

**Diagnostic :**

- Table `ope_users` : 186+ doublons pour la même paire (fk_operation, fk_user)
- Table `ope_users_sectors` : Risque de doublons sur (fk_operation, fk_user, fk_sector)
- Cause : Absence de contraintes UNIQUE sur ces combinaisons de FK
- Impact : Le `ON DUPLICATE KEY UPDATE` ne fonctionnait pas, permettant la création de lignes en double

**Corrections appliquées :**

1. **Mise à jour de la structure de référence** (`geo_app_structure.sql`) :

   - Ligne 403 : Ajout de `UNIQUE KEY idx_operation_user (fk_operation, fk_user)` sur `ope_users`
   - Ligne 430 : Ajout de `UNIQUE KEY idx_operation_user_sector (fk_operation, fk_user, fk_sector)` sur `ope_users_sectors`

2. **Correction du code PHP** (`migrate_from_backup.php`) :

   ```php
   // AVANT (lignes 1125-1140) - INCORRECT
   $sql = "SELECT ou.rowid, ou.fk_operation, ou.fk_user, ...
   INSERT INTO ope_users (id, fk_operation, fk_user, ...) VALUES (:id, ...

   // APRÈS (lignes 1125-1191) - CORRECT
   $sql = "SELECT DISTINCT ou.fk_operation, ou.fk_user, ...  // Suppression de rowid
   INSERT INTO ope_users (fk_operation, fk_user, ...) VALUES (...  // Suppression de id, utilise auto-increment
   ```

3. **Requêtes SQL pour appliquer les contraintes** :

   ```sql
   -- Vérifier les doublons existants
   SELECT fk_operation, fk_user, COUNT(*) as count
   FROM ope_users GROUP BY fk_operation, fk_user HAVING count > 1;

   -- Ajouter les contraintes UNIQUE
   ALTER TABLE ope_users
   ADD UNIQUE KEY `idx_operation_user` (`fk_operation`, `fk_user`);

   ALTER TABLE ope_users_sectors
   ADD UNIQUE KEY `idx_operation_user_sector` (`fk_operation`, `fk_user`, `fk_sector`);

   -- Vérifier
   SHOW INDEX FROM ope_users WHERE Key_name = 'idx_operation_user';
   SHOW INDEX FROM ope_users_sectors WHERE Key_name = 'idx_operation_user_sector';
   ```

**À appliquer sur tous les environnements :**

- DEV (dva_geo sur maria3/IN3) : mysql -h 13.23.33.4 -u dva_geo_user
- REC (rca_geo sur maria3/IN3) : mysql -h 13.23.33.4 -u rca_geo_user
- PROD (pra_geo sur maria4/IN4) : mysql -h 13.23.33.4 -u pra_geo_user

**Procédure de re-migration après correction :**

```bash
# Supprimer les données de l'entité avant re-migration
php scripts/php/migrate_from_backup.php \
  --source-db=geosector_20251008 \
  --mode=entity \
  --entity-id=5 \
  --delete-before
```

**Fichiers modifiés :**

- `scripts/php/geo_app_structure.sql` (lignes 403, 430)
- `scripts/php/migrate_from_backup.php` (lignes 1125-1191)
- `scripts/README-migration.md` (section Corrections Critiques ajoutée)
- `scripts/CORRECTIONS_MIGRATE.md` (correction #15 documentée)

**Statut :** ✅ CORRIGÉ - Code et structure mis à jour, contraintes à appliquer sur bases
**Date de correction :** 10/10/2025

---

#### 🎯 Planning de déploiement (09-10/10/2025)

**Jeudi 09/10/2025 - Environnement DEV (dva-geo)**

- [ ] Vider la base `dva_geo` (données de test)
- [ ] Déployer les scripts corrigés (`deploy-api.sh`)
- [ ] Test migration 1 entité pilote (#1178 - 5 users, 1317 passages)
- [ ] Validation des mappings secteurs
- [ ] Si OK : Migration batch des 406 entités
- [ ] Vérification intégrité données (comptages, FK)
- [ ] Tests fonctionnels API + Flutter

**Jeudi 09/10/2025 PM - Environnement REC (rca-geo)**

- [ ] Dump `dva_geo` → Import dans `rca_geo`
- [ ] Tests de non-régression complets
- [ ] Validation client sur REC
- [ ] Tests de charge (si applicable)

**Vendredi 10/10/2025 - Environnement PROD (pra-geo)**

- [ ] Sauvegarde complète `pra_geo` (avant migration)
- [ ] Dump `rca_geo` → Import dans `pra_geo`
- [ ] Tests de smoke en production
- [ ] Monitoring des performances
- [ ] Documentation post-migration

**Rollback plan :**

- Restauration depuis sauvegarde pré-migration : 15-30 minutes
- Scripts de vérification disponibles dans `scripts/php/verify_migration_structure.php`

**Statut :** 📅 PLANIFIÉ - Démarrage jeudi 09/10/2025 matin
**Durée estimée totale :** 2 jours

---

### 🟡 PRIORITÉ MOYENNE

#### 4. Amélioration des logs

- Ajouter plus de contexte dans les logs
- Rotation automatique des logs
- Dashboard de monitoring

#### 5. Optimisation des performances

- Cache des requêtes fréquentes
- Index sur les tables volumineuses
- Pagination optimisée

#### 6. Sécurisation des clés Stripe par environnement

**Statut :** ✅ **PARTIELLEMENT RÉSOLU** (16/10/2025)

**Ce qui a été fait :**

- ✅ Configuration complète des clés API Stripe par environnement dans `AppConfig.php`
  - DEV : Clés TEST Pierre (compte plateforme développeur)
  - RECETTE : Clés TEST Client (compte plateforme client mode test)
  - PRODUCTION : Clés LIVE Client (compte plateforme client mode live)
- ✅ Configuration des webhooks Stripe avec secrets dédiés
  - RECETTE : `webhook-rca` → `whsec_avExshr0MeWTI7wXP8478XVUkrbYG8hs`
  - PRODUCTION : `webhook-pra` → `whsec_gFnA6pR92RLdbAS2T6CSC18xsSdNBZHR`
- ✅ Mise à jour de la version API Stripe : `2025-08-27.basil`
- ✅ Correction du bug StripeWebhookController (ligne 46)
- ✅ Correction de l'URL webhook : `/api/stripe/webhooks` (avec 's')
- ✅ Documentation complète dans `TECHBOOK.md`

**Ce qui reste (amélioration future) :**

- ⏳ Migration vers variables d'environnement pour sécurité renforcée
- ⏳ Isolation des secrets par container (actuellement tous visibles dans AppConfig.php)

**Solutions à étudier pour amélioration future :**

1. **Variables d'environnement** (`.env` par container)

   - Fichier `.env.dev`, `.env.rec`, `.env.prod`
   - Chargement dynamique selon l'environnement
   - Exclusion des `.env` du versionning Git

2. **Fichiers de config séparés**

   - `config/stripe.dev.php`, `config/stripe.rec.php`, `config/stripe.prod.php`
   - Déploiement sélectif selon l'environnement
   - Non versionnés (ajoutés au .gitignore)

3. **Secrets management** (avancé)
   - HashiCorp Vault, AWS Secrets Manager, etc.
   - API de récupération sécurisée des secrets

**Recommandation :** Approche #1 (variables d'environnement) pour équilibre sécurité/simplicité

---

#### 7. Statistiques Events pour Admin Flutter

**Demandé le :** 22/12/2025
**Objectif :** Permettre aux admins Flutter de consulter les logs Events avec des stats quotidiennes, hebdomadaires et mensuelles, et drill-down vers le détail.

**Architecture choisie :** Stats pré-agrégées en SQL + détail JSONL à la demande

**Pourquoi cette approche :**

- Évite de parser les fichiers JSONL à chaque requête Flutter
- Transfert minimal (~1-10 KB par requête)
- Calculs hebdo/mensuel à la volée depuis `daily` (pas de tables supplémentaires)
- Détail paginé uniquement sur demande

**Phase 1 : Base de données** ✅ (22/12/2025)

- [x] Créer la table `event_stats_daily`
  - Colonnes : `stat_date`, `entity_id`, `event_type`, `count`, `sum_amount`, `unique_users`, `metadata`
  - Index : `(entity_id, stat_date)`, unique `(stat_date, entity_id, event_type)`
- [x] Script SQL de création : `scripts/sql/create_event_stats_daily.sql`

**Phase 2 : CRON d'agrégation** ✅ (22/12/2025)

- [x] Créer `scripts/cron/aggregate_event_stats.php`
  - Parse le fichier JSONL de J-1 (ou date passée en paramètre)
  - Agrège par entity_id et event_type
  - INSERT/UPDATE dans `event_stats_daily`
  - Calcule `unique_users` (COUNT DISTINCT sur user_id)
  - Calcule `sum_amount` pour les passages
  - Stocke metadata JSON (top 5 secteurs, erreurs fréquentes, etc.)
- [x] Ajouter au crontab : exécution à 01h00 chaque nuit (via deploy-api.sh)
- [x] Script de rattrapage : `php aggregate_event_stats.php --from=2025-01-01 --to=2025-12-21`

**Phase 3 : Service EventStatsService** ✅ (22/12/2025)

- [x] Créer `src/Services/EventStatsService.php`
  - `getSummary(?int $entityId, ?string $date)` : Stats du jour
  - `getDaily(?int $entityId, string $from, string $to, array $eventTypes)` : Stats journalières
  - `getWeekly(?int $entityId, string $from, string $to, array $eventTypes)` : Calculé depuis daily
  - `getMonthly(?int $entityId, int $year, array $eventTypes)` : Calculé depuis daily
  - `getDetails(?int $entityId, string $date, ?string $eventType, int $limit, int $offset)` : Lecture JSONL paginée
  - `getEventTypes()` : Liste des types d'événements disponibles
  - `hasStatsForDate(string $date)` : Vérifie si stats existent

**Phase 4 : Controller et Routes** ✅ (22/12/2025)

- [x] Créer `src/Controllers/EventStatsController.php`
  - `summary()` : GET /api/events/stats/summary?date=
  - `daily()` : GET /api/events/stats/daily?from=&to=&events=
  - `weekly()` : GET /api/events/stats/weekly?from=&to=&events=
  - `monthly()` : GET /api/events/stats/monthly?year=&events=
  - `details()` : GET /api/events/stats/details?date=&event=&limit=&offset=
  - `types()` : GET /api/events/stats/types
- [x] Ajouter les routes dans `Router.php`
- [x] Vérification des droits : Admin entité (role_id = 2) ou Super-admin (role_id = 1)
- [x] Super-admin : peut voir toutes les entités (entity_id = NULL ou ?entity_id=X)

**Phase 5 : Optimisations** ✅ (22/12/2025)

- [x] Compression gzip sur les réponses JSON (si >1KB et client supporte)
- [x] Header `ETag` sur /summary et /daily (cache 5 min, 304 Not Modified)
- [x] Filtrage des champs sensibles dans /details (IP tronquée, user_agent supprimé)
- [x] Limite max 100 events par requête /details

**Phase 6 : Tests et documentation**

- [ ] Tests unitaires EventStatsService
- [ ] Tests endpoints avec différents rôles
- [ ] Documentation Postman/Swagger des endpoints
- [ ] Mise à jour TECHBOOK.md avec exemples de réponses JSON

**Estimation :** 2-3 jours de développement

**Dépendances :**

- EventLogService déjà en place ✅
- Fichiers JSONL générés quotidiennement ✅

---

### 🟢 PRIORITÉ BASSE

#### 7. Amélioration de la suppression des utilisateurs

**Demandé le :** 19/10/2025
**Objectif :** Empêcher la suppression d'un utilisateur ayant des passages et gérer correctement le soft delete.

**Problème actuel :**

- Erreur SQL lors de `DELETE /api/users/{id}` si l'utilisateur a des passages dans `ope_pass`
- Contrainte FK `ope_pass_ibfk_3` empêche la suppression (comportement attendu)
- Génère des emails d'alerte "GEOSECTOR SECURITY" de type SQL_ERROR

**Solution à implémenter (côté Flutter) :**

1. **Vérification avant suppression**

   - Endpoint suggéré : `GET /api/users/{id}/can-delete`
   - Retourne : total passages, passages opérations actives, passages opérations inactives

2. **Logique métier Flutter**

   - Si passages sur **opération active** → Demander réassignation à un autre user
   - Si passages uniquement sur **opérations inactives** → Proposer soft delete
   - Si aucun passage → Autoriser suppression physique

3. **Soft delete (recommandé)**
   - Ajouter `deleted_at TIMESTAMP NULL` dans table `users`
   - Modifier `UserController::deleteUser()` pour faire un soft delete
   - Les utilisateurs soft-deleted ne sont plus affichés mais conservés pour l'historique

**Côté API (préparation) :**

- [ ] Ajouter endpoint `GET /api/users/{id}/can-delete` pour vérification
- [ ] Ajouter colonne `deleted_at` dans table `users`
- [ ] Modifier `UserController::deleteUser()` pour gérer le soft delete
- [ ] Adapter les requêtes SQL pour filtrer `WHERE deleted_at IS NULL`

**Côté Flutter (à implémenter par le développeur) :**

- [ ] Appeler endpoint de vérification avant suppression
- [ ] Afficher dialogue selon le contexte (réassignation vs soft delete)
- [ ] Gérer le workflow de réassignation si nécessaire

**Note :** En attendant l'implémentation, les emails SQL_ERROR pour `DELETE /api/users/*` sont normaux et attendus (violation de contrainte FK légitime).

**Statut :** 📝 NOTÉ - À implémenter côté Flutter en priorité
**Date :** 19/10/2025

---

### 🔴 PRIORITÉ HAUTE

#### 8. Connexion de l'API à un broker Mosquitto MQTT

**Demandé le :** 08/11/2025
**Objectif :** Intégrer un système de communication MQTT permettant à l'API de publier et recevoir des messages en temps réel pour notifier les clients (applications Flutter) des changements de données.

**Contexte :**
- Les applications Flutter doivent être notifiées en temps réel lors de modifications de données (passages, secteurs, opérations)
- Alternative moderne aux webhooks pour la communication bidirectionnelle
- Permet de réduire le polling constant des applications mobiles
- Architecture publish/subscribe pour une meilleure scalabilité

**Cas d'usage principaux :**
```
Création/Modification passage
  ↓ API publie sur MQTT: geosector/{entity_id}/passages/{operation_id}
  ↓ Payload JSON avec les données du passage
  ↓ Applications Flutter abonnées reçoivent la notification
  ↓ Mise à jour automatique de l'UI sans refresh manuel

Modification secteur
  ↓ API publie sur MQTT: geosector/{entity_id}/sectors/{operation_id}
  ↓ Applications concernées rechargent les données

Changement statut opération
  ↓ API publie sur MQTT: geosector/{entity_id}/operations/{operation_id}
  ↓ Notification push vers tous les membres
```

---

#### 📋 Plan d'action détaillé

##### Étape 1 : Installation et configuration Mosquitto

**A. Installation du broker Mosquitto** (sur serveur DVA/RCA/PROD) :

```bash
# Sur le host ou dans un container dédié
apk add mosquitto mosquitto-clients  # Alpine
# ou
apt install mosquitto mosquitto-clients  # Debian/Ubuntu

# Démarrer et activer au boot
rc-service mosquitto start  # Alpine
rc-update add mosquitto default
# ou
systemctl start mosquitto  # Systemd
systemctl enable mosquitto
```

**B. Configuration Mosquitto** (`/etc/mosquitto/mosquitto.conf`) :

```conf
# Listeners
listener 1883 0.0.0.0
protocol mqtt

listener 8883 0.0.0.0
protocol mqtt
certfile /etc/mosquitto/certs/cert.pem
keyfile /etc/mosquitto/certs/key.pem
cafile /etc/mosquitto/certs/ca.pem

# WebSockets (pour Flutter Web)
listener 9001
protocol websockets

# Authentification
allow_anonymous false
password_file /etc/mosquitto/passwd

# ACL (Access Control List)
acl_file /etc/mosquitto/acl.conf

# Persistence
persistence true
persistence_location /var/lib/mosquitto/

# Logs
log_dest file /var/log/mosquitto/mosquitto.log
log_type all
log_timestamp true
```

**C. Créer les utilisateurs MQTT** :

```bash
# Utilisateur pour l'API
mosquitto_passwd -c /etc/mosquitto/passwd geosector_api
# Password: généré de manière sécurisée

# Utilisateur pour les applications Flutter
mosquitto_passwd /etc/mosquitto/passwd geosector_client
```

**D. Configuration ACL** (`/etc/mosquitto/acl.conf`) :

```conf
# API peut publier et s'abonner à tous les topics geosector
user geosector_api
topic readwrite geosector/#

# Clients peuvent seulement s'abonner
user geosector_client
topic read geosector/#
```

**E. Tests de connexion** :

```bash
# Test publication
mosquitto_pub -h localhost -p 1883 -u geosector_api -P password \
  -t "geosector/test" -m "Test message"

# Test souscription
mosquitto_sub -h localhost -p 1883 -u geosector_client -P password \
  -t "geosector/#"
```

**Fichiers impactés :**
- [ ] Installer Mosquitto sur DVA (13.23.33.43)
- [ ] Installer Mosquitto sur RCA (13.23.33.23)
- [ ] Installer Mosquitto sur PROD (13.23.33.22 / IN4)
- [ ] Configurer firewall (port 1883, 8883, 9001)

---

##### Étape 2 : Installation bibliothèque PHP MQTT

```bash
# Via Composer
composer require php-mqtt/client
```

**Fichiers impactés :**
- [ ] Modifier `composer.json`
- [ ] Exécuter sur DVA, RCA, PROD

---

##### Étape 3 : Configuration AppConfig.php

Ajouter la configuration MQTT :

```php
// Dans src/Config/AppConfig.php
'mqtt' => [
    'enabled' => true,
    'broker' => [
        'host' => 'localhost',  // ou IP du container mosquitto
        'port' => 1883,
        'username' => 'geosector_api',
        'password' => '',  // À définir par environnement
        'client_id' => 'geosector_api_' . gethostname(),
    ],
    'tls' => [
        'enabled' => false,  // true en production
        'port' => 8883,
        'ca_file' => '/etc/mosquitto/certs/ca.pem',
    ],
    'topics' => [
        'passages' => 'geosector/{entity_id}/passages/{operation_id}',
        'sectors' => 'geosector/{entity_id}/sectors/{operation_id}',
        'operations' => 'geosector/{entity_id}/operations/{operation_id}',
        'users' => 'geosector/{entity_id}/users',
        'system' => 'geosector/system',
    ],
    'qos' => 1,  // Quality of Service (0, 1, ou 2)
    'retain' => false,
],
```

**Fichiers impactés :**
- [ ] Modifier `src/Config/AppConfig.php` (3 environnements)

---

##### Étape 4 : Créer le service MqttService

Créer `src/Services/MqttService.php` :

**Méthodes principales :**

```php
namespace App\Services;

use PhpMqtt\Client\MqttClient;
use PhpMqtt\Client\ConnectionSettings;
use App\Services\LogService;
use Exception;

class MqttService {
    private static ?self $instance = null;
    private ?MqttClient $client = null;
    private array $config;
    private bool $connected = false;

    private function __construct() {
        $this->config = AppConfig::getInstance()->get('mqtt');
    }

    public static function getInstance(): self {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    /**
     * Connecte au broker MQTT
     */
    public function connect(): bool

    /**
     * Déconnecte du broker MQTT
     */
    public function disconnect(): void

    /**
     * Publie un message sur un topic
     * @param string $topic Topic MQTT
     * @param array $payload Données à publier (sera converti en JSON)
     * @param int $qos Quality of Service (0, 1, 2)
     * @param bool $retain Retenir le message
     */
    public function publish(string $topic, array $payload, int $qos = 1, bool $retain = false): bool

    /**
     * Publie une notification de passage créé/modifié
     */
    public function publishPassageUpdate(int $entityId, int $operationId, array $passageData): bool

    /**
     * Publie une notification de secteur créé/modifié
     */
    public function publishSectorUpdate(int $entityId, int $operationId, array $sectorData): bool

    /**
     * Publie une notification d'opération créée/modifiée
     */
    public function publishOperationUpdate(int $entityId, int $operationId, array $operationData): bool

    /**
     * Publie un message système (maintenance, déconnexion forcée, etc.)
     */
    public function publishSystemMessage(string $message, array $metadata = []): bool

    /**
     * Génère le topic en remplaçant les placeholders
     */
    private function buildTopic(string $topicTemplate, array $params): string

    /**
     * Vérifie si le service est activé et connecté
     */
    public function isAvailable(): bool
}
```

**Gestion des erreurs :**
- Reconnexion automatique en cas de déconnexion
- Timeout configurable
- Logs détaillés des publications
- Mode dégradé si MQTT indisponible (ne pas bloquer l'API)

**Fichiers impactés :**
- [ ] Créer `src/Services/MqttService.php`

---

##### Étape 5 : Intégrer MQTT dans les Controllers

**A. PassageController** - Notifier création/modification passage :

```php
// Dans PassageController::create() après ligne 602
if ($passageId) {
    // Publier notification MQTT (non bloquant)
    try {
        $mqttService = MqttService::getInstance();
        if ($mqttService->isAvailable()) {
            $mqttService->publishPassageUpdate(
                $entityId,
                $operationId,
                [
                    'passage_id' => $passageId,
                    'fk_type' => $fkType,
                    'montant' => $montant,
                    'action' => 'created',
                    'timestamp' => time()
                ]
            );
        }
    } catch (Exception $e) {
        // Logger mais ne pas bloquer l'API
        LogService::log('Erreur publication MQTT passage', [
            'level' => 'warning',
            'error' => $e->getMessage()
        ]);
    }
}
```

**B. SectorController** - Notifier création/modification secteur :

```php
// Dans SectorController::create() après ligne 531
$mqttService = MqttService::getInstance();
if ($mqttService->isAvailable()) {
    $mqttService->publishSectorUpdate(
        $entityId,
        $operationId,
        [
            'sector_id' => $sectorId,
            'libelle' => $data['libelle'],
            'passages_created' => $passagesCreated,
            'action' => 'created',
            'timestamp' => time()
        ]
    );
}
```

**C. OperationController** - Notifier changements opération :

```php
// Notifications pour :
// - Changement de statut (chk_active)
// - Ajout/retrait d'utilisateurs
// - Modification des dates
```

**Fichiers impactés :**
- [ ] Modifier `src/Controllers/PassageController.php`
- [ ] Modifier `src/Controllers/SectorController.php`
- [ ] Modifier `src/Controllers/OperationController.php`
- [ ] Modifier `src/Controllers/UserController.php` (optionnel)

---

##### Étape 6 : Structure des messages MQTT

**Format JSON standardisé :**

```json
{
  "event": "passage.created",
  "entity_id": 5,
  "operation_id": 12345,
  "timestamp": 1699456789,
  "data": {
    "passage_id": 19500576,
    "fk_type": 1,
    "montant": 10.00,
    "encrypted_name": "...",
    "rue": "Rue Example",
    "ville": "Rennes"
  },
  "metadata": {
    "user_id": 9999985,
    "api_version": "3.3.5"
  }
}
```

**Types d'événements :**
- `passage.created`
- `passage.updated`
- `passage.deleted`
- `sector.created`
- `sector.updated`
- `sector.deleted`
- `operation.created`
- `operation.updated`
- `operation.status_changed`
- `user.added`
- `user.removed`
- `system.maintenance`

---

##### Étape 7 : Sécurisation

**A. Topics hiérarchiques par entité :**
```
geosector/{entity_id}/*  → Seuls les membres de cette entité peuvent s'abonner
```

**B. Validation côté API :**
- Vérifier que l'utilisateur appartient à l'entité avant publication
- Ne jamais publier de données sensibles non chiffrées

**C. Authentification JWT pour Flutter :**
```json
{
  "mqtt_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "mqtt_host": "mqtt.geosector.fr",
  "mqtt_port": 8883,
  "topics": ["geosector/5/#"]
}
```

**Fichiers impactés :**
- [ ] Créer endpoint `POST /api/mqtt/token` (génération token MQTT)
- [ ] Modifier ACL Mosquitto pour utiliser JWT

---

##### Étape 8 : Tests et validation

**Tests sur DVA :**
- [ ] Publier message test via API
- [ ] S'abonner avec `mosquitto_sub` et vérifier réception
- [ ] Créer passage et vérifier publication MQTT
- [ ] Créer secteur et vérifier publication MQTT
- [ ] Vérifier logs MQTT dans `/var/log/mosquitto/`
- [ ] Tester reconnexion après redémarrage broker

**Tests de charge :**
- [ ] 100 publications simultanées
- [ ] 50 clients connectés en parallèle
- [ ] Vérifier latence (doit être < 100ms)

**Tests de non-régression :**
- [ ] API fonctionne si MQTT désactivé
- [ ] API fonctionne si broker MQTT inaccessible (mode dégradé)

**Fichiers impactés :**
- [ ] Créer `scripts/test/test_mqtt_integration.php`

---

##### Étape 9 : Intégration côté Flutter (information)

**Package Flutter à utiliser :**
```yaml
dependencies:
  mqtt_client: ^10.0.0
```

**Exemple de souscription :**
```dart
final client = MqttServerClient('mqtt.geosector.fr', '');
await client.connect(username, password);

client.subscribe('geosector/$entityId/passages/#', MqttQos.atLeastOnce);

client.updates!.listen((List<MqttReceivedMessage<MqttMessage>> messages) {
  final recMess = messages[0].payload as MqttPublishMessage;
  final payload = MqttPublishPayload.bytesToStringAsString(recMess.payload.message);
  final data = jsonDecode(payload);

  // Mettre à jour l'UI en temps réel
  if (data['event'] == 'passage.created') {
    _refreshPassages();
  }
});
```

---

##### Étape 10 : Documentation

**Mettre à jour la documentation :**
- [ ] `docs/TECHBOOK.md` - Section "MQTT Real-Time Communication"
- [ ] `TODO-API.md` - Marquer la tâche comme terminée
- [ ] Créer `docs/MQTT-INTEGRATION.md` avec :
  - Architecture MQTT
  - Topics disponibles
  - Format des messages
  - Exemples de souscription (PHP, Flutter, JavaScript)
  - Configuration ACL et sécurité

---

#### 📊 Résumé des impacts

**Infrastructure :**
- Broker Mosquitto installé sur chaque environnement
- Ports ouverts : 1883 (MQTT), 8883 (MQTTS), 9001 (WebSockets)

**Services créés :**
- `MqttService.php` : Publication et gestion des messages

**Controllers modifiés :**
- `PassageController.php` : Publication événements passages
- `SectorController.php` : Publication événements secteurs
- `OperationController.php` : Publication événements opérations

**Configuration modifiée :**
- `AppConfig.php` : Section 'mqtt'
- `composer.json` : Dépendance `php-mqtt/client`

**Nouveaux endpoints :**
- `POST /api/mqtt/token` : Génération token JWT pour Flutter

---

#### ⚠️ Points d'attention

1. **Performance** : MQTT est asynchrone, ne doit JAMAIS bloquer l'API
2. **Mode dégradé** : L'API doit fonctionner même si MQTT est HS
3. **Sécurité** : ACL strictes, authentification obligatoire
4. **Scalabilité** : Prévoir un broker MQTT dédié si > 1000 clients
5. **Monitoring** : Logs Mosquitto à surveiller (connexions, déconnexions, erreurs)
6. **Certificats SSL** : Obligatoires en production (Let's Encrypt)
7. **QoS** : Utiliser QoS 1 (at least once) pour garantir la livraison

---

#### 🔧 Architecture déployée

**Environnement DVA (IN3) :**
```
Container dva-geo (13.23.33.43)
  ├── API PHP (nginx + php-fpm)
  └── Mosquitto broker (1883, 8883, 9001)
```

**Environnement RCA (IN3) :**
```
Container rca-geo (13.23.33.23)
  ├── API PHP
  └── Mosquitto broker
```

**Environnement PROD (IN4) :**
```
Container pra-geo (13.23.33.22)
  ├── API PHP
  └── Mosquitto broker
```

**Alternative (scalable) :**
```
Container mqtt-broker (dédié)
  ├── Mosquitto avec authentification JWT
  └── Monitoring (Prometheus + Grafana)
```

---

**Statut :** 📋 PLANIFIÉ - En attente de validation pour démarrage
**Date de création :** 08/11/2025
**Durée estimée :** 2-3 jours (installation + développement + tests)
**Dépendances :** Aucune
**Priorité :** HAUTE - Améliore significativement l'expérience utilisateur

---

#### 9. Envoi de SMS de reçu en alternative à l'email

**Demandé le :** 08/11/2025
**Objectif :** Permettre l'envoi de SMS de reçu au contributeur lors de la création/modification de passages de type 1 ou 5, comme alternative à l'email.

**Contexte :**
- La table `entites` possède déjà le champ `chk_accept_sms` (boolean)
- Actuellement, les reçus sont envoyés par email pour les passages `fk_type = 1` ou `5`
- Besoin d'envoyer un SMS de reçu si numéro mobile valide (10 chiffres commençant par 06 ou 07)
- Comptabilisation par entité pour facturation ultérieure

**Cas d'usage :**
```
Création passage avec fk_type = 1 ou 5
  ↓ Si entite.chk_accept_sms = 1
  ↓ Si encrypted_phone est mobile (06/07...)
  ↓ Envoyer SMS reçu au lieu d'email
  ↓ Comptabiliser dans sms_sent_count
```

---

#### 📋 Plan d'action détaillé

##### Étape 1 : Modifications base de données

**A. Table `entites`** - Ajouter compteur et gestion pack SMS :

```sql
ALTER TABLE entites
ADD COLUMN sms_sent_count INT UNSIGNED DEFAULT 0 COMMENT 'Nombre total de SMS envoyés pour facturation',
ADD COLUMN sms_credits INT UNSIGNED DEFAULT 0 COMMENT 'Crédits SMS disponibles (si pack dédié)',
ADD COLUMN sms_pack_type ENUM('global', 'entity') DEFAULT 'global' COMMENT 'Type de pack SMS (global partagé ou dédié)',
ADD KEY idx_chk_accept_sms (chk_accept_sms);
```

**B. Nouvelle table `sms_log`** - Historique des envois pour audit :

```sql
CREATE TABLE IF NOT EXISTS sms_log (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    fk_entite INT UNSIGNED NOT NULL,
    fk_passage INT UNSIGNED NULL COMMENT 'Passage concerné par le SMS',
    phone VARCHAR(20) NOT NULL COMMENT 'Numéro destinataire (format international)',
    message TEXT NOT NULL COMMENT 'Contenu du SMS envoyé',
    status ENUM('pending', 'sent', 'failed', 'error') DEFAULT 'pending',
    ovh_message_id VARCHAR(100) NULL COMMENT 'ID retourné par API OVH',
    credits_used DECIMAL(5,2) DEFAULT 1.00 COMMENT 'Crédits SMS consommés',
    error_message TEXT NULL,
    sent_at TIMESTAMP NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

    KEY idx_fk_entite (fk_entite),
    KEY idx_fk_passage (fk_passage),
    KEY idx_status (status),
    KEY idx_sent_at (sent_at),

    CONSTRAINT sms_log_ibfk_1 FOREIGN KEY (fk_entite) REFERENCES entites(id) ON DELETE CASCADE,
    CONSTRAINT sms_log_ibfk_2 FOREIGN KEY (fk_passage) REFERENCES ope_pass(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

**C. Table de configuration `sms_config`** :

```sql
CREATE TABLE IF NOT EXISTS sms_config (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    config_key VARCHAR(50) UNIQUE NOT NULL,
    config_value TEXT NOT NULL,
    description VARCHAR(255) NULL,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    KEY idx_config_key (config_key)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Configuration initiale
INSERT INTO sms_config (config_key, config_value, description) VALUES
('ovh_service_name', '', 'Nom du service SMS OVH'),
('ovh_app_key', '', 'Application Key OVH'),
('ovh_app_secret', '', 'Application Secret OVH (chiffré)'),
('ovh_consumer_key', '', 'Consumer Key OVH (chiffré)'),
('global_sms_credits', '0', 'Crédits SMS globaux disponibles'),
('default_sender', 'GEOSECTOR', 'Nom d\'expéditeur (11 chars max)'),
('sms_enabled', '1', 'Activation globale du système SMS'),
('sms_receipt_template', 'Merci pour votre don de {montant}EUR par {reglement}. Recu fiscal disponible sur demande. {entite_nom}', 'Template du SMS de reçu');
```

**Fichiers impactés :**
- [ ] Créer `scripts/migrations/add_sms_receipts.sql`
- [ ] Appliquer sur DVA, REC, PROD

---

##### Étape 2 : Configuration AppConfig.php

Ajouter la configuration OVH SMS :

```php
// Dans src/Config/AppConfig.php
'sms' => [
    'enabled' => true,
    'provider' => 'ovh',
    'ovh' => [
        'endpoint' => 'ovh-eu',
        'application_key' => '',      // Chargé depuis sms_config
        'application_secret' => '',   // Chargé depuis sms_config
        'consumer_key' => '',         // Chargé depuis sms_config
        'service_name' => '',         // Chargé depuis sms_config
    ],
    'default_sender' => 'GEOSECTOR',
    'max_length' => 160,
],
```

**Fichiers impactés :**
- [ ] Modifier `src/Config/AppConfig.php` (3 environnements)

---

##### Étape 3 : Installation SDK OVH

```bash
composer require ovh/ovh
```

**Fichiers impactés :**
- [ ] Modifier `composer.json`
- [ ] Exécuter sur DVA, REC, PROD

---

##### Étape 4 : Nouveau service SmsService

Créer `src/Services/SmsService.php` avec les méthodes principales :

- `sendReceiptSms()` - Envoie un SMS de reçu après un passage payé
- `canEntitySendSms()` - Vérifie si l'entité peut envoyer des SMS (crédits, autorisation)
- `isValidMobile()` - Valide un numéro mobile français (06/07)
- `formatPhoneNumber()` - Formate au format international (+33...)
- `generateReceiptMessage()` - Génère le message depuis template
- `incrementSmsCounter()` - Incrémente le compteur de facturation
- `decrementCredits()` - Décremente les crédits (global ou entity)
- `logSms()` - Enregistre dans sms_log

**Template de message par défaut :**
```
Merci pour votre don de {montant}EUR par {reglement}. Recu fiscal disponible sur demande. {entite_nom}
```

**Fichiers impactés :**
- [ ] Créer `src/Services/SmsService.php`

---

##### Étape 5 : Modifier PassageController

**Modifications dans `PassageController::create()` (lignes 606-639) :**

Ajouter logique de décision SMS vs Email :

```php
$fkType = isset($data['fk_type']) ? (int)$data['fk_type'] : 0;
if ($fkType === 1 || $fkType === 5) {
    // Récupérer l'entity_id de l'opération
    $stmtEntity = $this->db->prepare('SELECT fk_entite FROM operations WHERE id = ?');
    $stmtEntity->execute([$operationId]);
    $entityId = (int)$stmtEntity->fetchColumn();

    // Vérifier si l'entité accepte les SMS
    $stmtEntite = $this->db->prepare('SELECT chk_accept_sms FROM entites WHERE id = ?');
    $stmtEntite->execute([$entityId]);
    $acceptSms = (bool)$stmtEntite->fetchColumn();

    // Déterminer si on envoie SMS ou Email
    $shouldSendSms = false;
    $phone = null;

    if ($acceptSms && !empty($encryptedPhone)) {
        $phone = ApiService::decryptData($encryptedPhone);
        $smsService = new \App\Services\SmsService($this->db);
        if ($smsService->isValidMobile($phone)) {
            $shouldSendSms = true;
        }
    }

    if ($shouldSendSms && $phone) {
        // ENVOI SMS via register_shutdown_function
        // Récupérer type de règlement pour le message
        // Appeler sendReceiptSms()
    } else {
        // ENVOI EMAIL (logique existante)
    }
}
```

**Même logique dans `PassageController::update()` si changement de type.**

**Fichiers impactés :**
- [ ] Modifier `src/Controllers/PassageController.php`

---

##### Étape 6 : Endpoints API pour gestion SMS (optionnel)

**Créer `SmsController.php`** avec :

- `GET /api/sms/stats/{entity_id}` - Statistiques SMS d'une entité
- `GET /api/sms/history/{entity_id}` - Historique des envois
- `GET /api/sms/credits` - Solde crédits global et par entité
- `PUT /api/sms/config` - Configuration SMS (admin)

**Fichiers impactés :**
- [ ] Créer `src/Controllers/SmsController.php` (optionnel)
- [ ] Modifier `src/Core/Router.php` (optionnel)

---

##### Étape 7 : Tests et validation

**Tests sur DVA :**

- [ ] Créer entité test avec `chk_accept_sms = 1`
- [ ] Créer passage `fk_type = 1` avec mobile valide (06/07)
- [ ] Vérifier envoi SMS via OVH
- [ ] Vérifier enregistrement dans `sms_log`
- [ ] Vérifier incrémentation de `sms_sent_count`
- [ ] Vérifier débit des crédits

**Tests de non-régression :**

- [ ] Entité avec `chk_accept_sms = 0` → Email uniquement
- [ ] Numéro fixe (01-05, 08, 09) → Email uniquement
- [ ] Sans téléphone mais avec email → Email uniquement
- [ ] Passage `fk_type = 2, 3, 4` → Pas d'envoi

**Fichiers impactés :**
- [ ] Créer `scripts/test/test_sms_receipts.php`

---

##### Étape 8 : Documentation

**Mettre à jour la documentation :**

- [ ] `docs/TECHBOOK.md` - Section "Envoi de SMS de reçu"
- [ ] `TODO-API.md` - Marquer la tâche comme terminée
- [ ] Créer `docs/SMS-RECEIPTS.md` avec :
  - Configuration OVH SMS
  - Template de message personnalisable
  - Gestion des crédits (global vs entité)
  - Facturation basée sur `sms_sent_count`

---

#### 📊 Résumé des impacts

**Tables créées :**
- `sms_log` : Historique des envois
- `sms_config` : Configuration OVH

**Tables modifiées :**
- `entites` : +3 colonnes (sms_sent_count, sms_credits, sms_pack_type)

**Services créés :**
- `SmsService.php` : Gestion envoi SMS via OVH

**Controllers modifiés :**
- `PassageController.php` : Logique SMS vs Email selon contexte

**Configuration modifiée :**
- `AppConfig.php` : Section 'sms'
- `composer.json` : Dépendance `ovh/ovh`

---

#### ⚠️ Points d'attention

1. **Validation numéro mobile** : Uniquement 06/07 (10 chiffres)
2. **Longueur SMS** : 160 caractères max (template à optimiser)
3. **Coût** : ~0.035€/SMS en France
4. **Priorité** : SMS si mobile valide, sinon Email en fallback
5. **Comptabilisation** : `sms_sent_count` pour facturation
6. **Credentials OVH** : À ne JAMAIS commiter, stockés en base chiffrés
7. **Template personnalisable** : Dans `sms_config.sms_receipt_template`

---

#### 🔧 Configuration OVH requise

**Étapes préalables :**

1. Créer application OVH : https://eu.api.ovh.com/createApp/
2. Générer Consumer Key avec droits `/sms/*`
3. Commander service SMS OVH
4. Créditer le compte (minimum 100-200 SMS pour tests)

**Stockage en base :**

```sql
UPDATE sms_config SET config_value = 'sms-ab123456-1' WHERE config_key = 'ovh_service_name';
UPDATE sms_config SET config_value = 'xxxxx' WHERE config_key = 'ovh_app_key';
UPDATE sms_config SET config_value = 'xxxxx' WHERE config_key = 'ovh_app_secret';
UPDATE sms_config SET config_value = 'xxxxx' WHERE config_key = 'ovh_consumer_key';
UPDATE sms_config SET config_value = '1000' WHERE config_key = 'global_sms_credits';
```

---

**Statut :** 📋 PLANIFIÉ - En attente de démarrage
**Date de création :** 08/11/2025
**Durée estimée :** 2-3 jours (développement + tests + configuration OVH)
**Dépendances :** Compte OVH avec service SMS actif

---

#### 9. Intégration de la base "batiments" dans la gestion des secteurs

**Demandé le :** 07/11/2025
**Objectif :** Intégrer la base de données "batiments" pour identifier les immeubles et créer automatiquement un passage par logement lors de la création/modification des secteurs.

**Contexte :**

- **Nouvelle base** : `batiments` (même host/credentials que base `adresses`)
- **Structure** : Tables par département `bat01`, `bat02`, etc.
- **Lien** : `bat{dept}.cle_interop_adr` → `cp{dept}.id`
- **Contenu** : Uniquement les immeubles avec métadonnées (nb_niveau, nb_log, residence, etc.)

**Structure de la table bat{dept} :**

```sql
batiment_groupe_id VARCHAR(50) PRIMARY KEY
code_departement_insee VARCHAR(5)
cle_interop_adr VARCHAR(50)          -- Lien vers cp{dept}.id
nb_niveau INT                         -- Nombre d'étages
nb_log INT                            -- Nombre de logements
nb_pdl_tot INT                        -- Compteurs électriques
annee_construction INT
residence VARCHAR(200)                -- Nom de la copropriété
usage_principal VARCHAR(100)
altitude_sol_mean DECIMAL(10,2)
gps_lat DECIMAL(10,7)
gps_lng DECIMAL(10,7)
```

**Filtre appliqué lors de l'import :**

- `usage_principal IN ('Résidentiel individuel', 'Résidentiel collectif', 'Secondaire', 'Tertiaire')`
- `nb_log > 1` (immeubles uniquement, pas les maisons individuelles)

---

#### 📋 Plan d'action détaillé

##### Étape 1 : Modifications base de données

**A. Table `sectors_adresses`** - Ajouter colonnes bâtiment :

```sql
ALTER TABLE sectors_adresses
ADD COLUMN fk_batiment VARCHAR(50) NULL COMMENT 'batiment_groupe_id' AFTER fk_adresse,
ADD COLUMN fk_habitat TINYINT NULL COMMENT '1=individuel, 2=collectif',
ADD COLUMN nb_niveau INT NULL COMMENT 'Nombre d\'étages',
ADD COLUMN nb_log INT NULL COMMENT 'Nombre de logements',
ADD COLUMN residence VARCHAR(200) NULL COMMENT 'Nom copropriété',
ADD COLUMN alt_sol DECIMAL(10,2) NULL COMMENT 'Altitude sol',
ADD KEY idx_fk_batiment (fk_batiment),
ADD KEY idx_fk_habitat (fk_habitat);
```

**B. Table `ope_pass`** - Vérifier/modifier colonne habitat :

```sql
-- Vérifier si fk_habitat existe déjà
SHOW COLUMNS FROM ope_pass LIKE 'fk_habitat';

-- Si nécessaire, modifier la colonne
ALTER TABLE ope_pass
MODIFY COLUMN fk_habitat TINYINT NULL COMMENT '1=individuel, 2=collectif';

-- Ajouter index si manquant
ALTER TABLE ope_pass ADD KEY idx_fk_habitat (fk_habitat);
```

**Fichiers impactés :**

- [ ] Créer `scripts/migrations/add_batiments_integration.sql`
- [ ] Appliquer sur DVA, REC, PROD

---

##### Étape 2 : Configuration AppConfig.php

Ajouter la configuration de la base batiments :

```php
// Dans src/Config/AppConfig.php
'buildings_database' => [
    'host' => '13.23.33.46',      // DVA: 13.23.33.46
    'name' => 'batiments',        // RCA: 13.23.33.36
    'username' => 'adr_geo_user', // PROD: 13.23.33.26
    'password' => 'd66,AdrGeoDev.User',
],
```

**Fichiers impactés :**

- [ ] Modifier `src/Config/AppConfig.php` (3 environnements DEV/REC/PROD)

---

##### Étape 3 : Nouveau service BuildingService

Créer `src/Services/BuildingService.php` :

**Méthodes à implémenter :**

```php
class BuildingService {
    private \PDO $dbBuildings;

    public function __construct()
    {
        // Connexion PDO vers base "batiments"
        $config = AppConfig::getInstance()->get('buildings_database');
        $this->dbBuildings = new \PDO(...);
    }

    /**
     * Récupère les bâtiments dans un polygone
     * @param array $coordinates Format [[lat, lng], ...]
     * @param int|null $entityId Pour déterminer les départements
     * @return array Liste des bâtiments avec métadonnées
     */
    public function getBuildingsInPolygon(array $coordinates, ?int $entityId = null): array

    /**
     * Compte les bâtiments dans un polygone
     */
    public function countBuildingsInPolygon(array $coordinates, ?int $entityId = null): int

    /**
     * Vérifie si la connexion à la base batiments est active
     */
    public function isConnected(): bool
}
```

**Logique similaire à AddressService :**

- Détection automatique des départements touchés par le polygone
- Requête SQL spatiale sur toutes les tables `bat{dept}` concernées
- Utilisation de `ST_Contains()` pour filtrer les bâtiments

**Fichiers impactés :**

- [ ] Créer `src/Services/BuildingService.php`

---

##### Étape 4 : Enrichir AddressService

Modifier `src/Services/AddressService.php` :

**Nouvelle méthode à ajouter :**

```php
/**
 * Enrichit les adresses avec les données bâtiment si disponibles
 * @param array $addresses Liste des adresses depuis getAddressesInPolygon()
 * @param int|null $entityId
 * @return array Adresses enrichies avec fk_batiment, fk_habitat, nb_log, etc.
 */
public function enrichAddressesWithBuildings(array $addresses, ?int $entityId = null): array
{
    // Pour chaque adresse, chercher si cp{dept}.id correspond à bat{dept}.cle_interop_adr
    // Enrichir avec : fk_batiment, fk_habitat, nb_niveau, nb_log, residence, alt_sol
    // Retourner les adresses enrichies
}
```

**Logique :**

1. Grouper les adresses par département
2. Pour chaque département, faire un JOIN entre `cp{dept}` et `bat{dept}`
3. Enrichir les adresses avec les données bâtiment trouvées
4. Définir `fk_habitat = 2` (collectif) si bâtiment trouvé, sinon `fk_habitat = 1` (individuel)

**Fichiers impactés :**

- [ ] Modifier `src/Services/AddressService.php`

---

##### Étape 5 : Modifier SectorController::create()

**Modifications dans la méthode `create()` (ligne 88-534) :**

```php
// APRÈS ligne 289 - Récupération des adresses
$addresses = $this->addressService->getAddressesInPolygon($coordinates, $entityId);

// NOUVEAU : Enrichir avec données batiments
$addresses = $this->addressService->enrichAddressesWithBuildings($addresses, $entityId);

// MODIFIER ligne 292-311 : Stockage dans sectors_adresses avec nouvelles colonnes
foreach ($addresses as $address) {
    $stmtAddress->execute([
        'sector_id' => $sectorId,
        'address_id' => $address['id'],
        'numero' => $address['numero'],
        'rue' => $address['voie'],
        'rue_bis' => '',
        'cp' => $address['code_postal'],
        'ville' => $address['commune'],
        'gps_lat' => $address['latitude'],
        'gps_lng' => $address['longitude'],
        // NOUVELLES COLONNES
        'fk_batiment' => $address['fk_batiment'] ?? null,
        'fk_habitat' => $address['fk_habitat'] ?? 1,
        'nb_niveau' => $address['nb_niveau'] ?? null,
        'nb_log' => $address['nb_log'] ?? null,
        'residence' => $address['residence'] ?? null,
        'alt_sol' => $address['alt_sol'] ?? null,
    ]);
}

// MODIFIER ligne 314-377 : Création des passages selon fk_habitat
foreach ($addresses as $address) {
    // Exclure les adresses déjà utilisées par passages orphelins
    if (in_array($address['id'], $addressesToExclude)) {
        continue;
    }

    if ($address['fk_habitat'] == 2 && !empty($address['nb_log']) && $address['nb_log'] > 1) {
        // IMMEUBLE : créer nb_log passages (1 par logement)
        for ($appt = 1; $appt <= $address['nb_log']; $appt++) {
            $passageStmt->execute([
                'operation_id' => $operationId,
                'sector_id' => $sectorId,
                'user_id' => $firstOpeUserId,
                'fk_adresse' => $address['id'],
                'numero' => $address['numero'],
                'rue' => $address['voie'],
                'rue_bis' => '',
                'ville' => $address['commune'],
                'gps_lat' => $address['latitude'],
                'gps_lng' => $address['longitude'],
                'fk_habitat' => 2,
                'appt' => $appt,  // Numéro d'appartement
                'residence' => $address['residence'] ?? null,
                'user_creat' => $userId
            ]);
            $passagesCreated++;
        }
    } else {
        // MAISON INDIVIDUELLE : 1 seul passage
        $passageStmt->execute([
            // ... même structure avec fk_habitat=1, appt=null
        ]);
        $passagesCreated++;
    }
}
```

**Fichiers impactés :**

- [ ] Modifier `src/Controllers/SectorController.php` (méthode `create()`)

---

##### Étape 6 : Modifier SectorController::update()

**Modifications dans la méthode `update()` (ligne 539-938) :**

1. Ligne 702-773 : Mise à jour de `sectors_adresses` avec nouvelles colonnes
2. Ligne 1243-1557 : Modifier `updatePassagesForSector()` pour gérer les immeubles

**Dans `updatePassagesForSector()` :**

```php
// Ligne 1346-1350 : Récupérer adresses depuis sectors_adresses
$addressesQuery = "SELECT * FROM sectors_adresses WHERE fk_sector = :sector_id";
$addresses = $addressesStmt->fetchAll();

// Ligne 1456-1495 : INSERT MULTIPLE - Adapter pour créer nb_log passages si immeuble
foreach ($toInsert as $addr) {
    if ($addr['fk_habitat'] == 2 && $addr['nb_log'] > 1) {
        // Créer nb_log passages pour cet immeuble
        for ($appt = 1; $appt <= $addr['nb_log']; $appt++) {
            // Ajouter à la liste d'insertion
        }
    } else {
        // Créer 1 seul passage
    }
}
```

**Fichiers impactés :**

- [ ] Modifier `src/Controllers/SectorController.php` (méthodes `update()` et `updatePassagesForSector()`)

---

##### Étape 7 : Tests et validation

**Tests sur DVA :**

- [ ] Créer un secteur test contenant des immeubles connus
- [ ] Vérifier le nombre de passages créés (doit correspondre au total de logements)
- [ ] Valider les données dans `sectors_adresses` (colonnes batiments remplies)
- [ ] Valider les données dans `ope_pass` (fk_habitat, appt, residence)
- [ ] Tester la modification d'un secteur (agrandissement/réduction)
- [ ] Vérifier le comportement avec passages orphelins

**Tests de non-régression :**

- [ ] Secteurs sans immeubles (maisons individuelles uniquement)
- [ ] Secteurs multi-départements
- [ ] Secteurs sans adresses (base adresses inaccessible)

**Fichiers impactés :**

- [ ] Créer `scripts/test/test_batiments_integration.php`

---

##### Étape 8 : Documentation

**Mettre à jour la documentation :**

- [ ] `docs/GESTION-SECTORS.md` - Section "Gestion des bâtiments"
- [ ] `docs/TECHBOOK.md` - Section "Base de données"
- [ ] `TODO-API.md` - Marquer la tâche comme terminée

**Contenu à documenter :**

- Structure de la table `bat{dept}`
- Logique de création de passages pour immeubles (1 passage par logement)
- Format de réponse API enrichi avec données bâtiments
- Exemples de requêtes SQL pour interroger les bâtiments

---

#### 📊 Résumé des impacts

**Tables modifiées :**

- `sectors_adresses` : +6 colonnes
- `ope_pass` : Modification colonne `fk_habitat` (si nécessaire)

**Services créés/modifiés :**

- **Nouveau** : `BuildingService.php`
- **Modifié** : `AddressService.php` (enrichissement)

**Controllers modifiés :**

- `SectorController.php` : Méthodes `create()`, `update()`, `updatePassagesForSector()`

**Configuration modifiée :**

- `AppConfig.php` : Ajout `buildings_database`

**Impact sur la création de secteur :**

- **AVANT** : 1 passage par adresse
- **APRÈS** : 1 passage par logement (immeubles = nb_log passages)
- **Exemple** : Secteur avec 100 adresses dont 20 immeubles de 10 logements
  - Avant : 100 passages
  - Après : 80 maisons + (20 × 10 immeubles) = 280 passages

---

#### ⚠️ Points d'attention

1. **Performance** : Les immeubles génèrent beaucoup plus de passages
   - Optimiser les INSERT multiple dans `updatePassagesForSector()`
   - Prévoir un timeout plus long pour les gros secteurs

2. **Cohérence des données** : La base `batiments` doit être à jour
   - Vérifier la présence des départements concernés
   - Gérer le cas où la base batiments est inaccessible (fallback)

3. **Migration des données existantes** : Les secteurs déjà créés
   - Option 1 : Ne pas toucher aux secteurs existants
   - Option 2 : Script de migration pour enrichir les secteurs existants

4. **Synchronisation** : Base `batiments` vs `adresses`
   - Le lien `cle_interop_adr` doit être valide
   - Gérer les adresses sans bâtiment correspondant

---

**Statut :** 📋 PLANIFIÉ - En attente de validation pour démarrage
**Date de création :** 07/11/2025
**Durée estimée :** 2-3 jours (développement + tests)

---

## 🟡 PRIORITÉ MOYENNE

### 9. Migration complète des Services vers namespace App\Services

**Date :** 07/11/2025
**Objectif :** Migrer tous les services vers le namespace `App\Services` pour améliorer l'organisation du code et l'autoloading.

**Contexte :**
Lors de l'intégration de la base batiments, nous avons commencé à migrer certains services vers le namespace `App\Services`. Cette migration partielle crée des conflits d'autoloading et des incohérences dans le code.

**État actuel :**

✅ **Services AVEC namespace App\Services (10)** :
- AddressService.php
- BuildingService.php
- DepartmentBoundaryService.php
- LogService.php
- PasswordSecurityService.php
- PDFGenerator.php
- ReceiptPDFGenerator.php
- ReceiptService.php
- SimplePDF.php
- StripeService.php

❌ **Services SANS namespace (8 à migrer)** :
- ApiService.php
- BackupEncryptionService.php
- EmailTemplates.php
- EventLogService.php
- ExportService.php
- FileService.php
- MigrationService.php
- OperationDataService.php

✅ **Services Security (tous OK)** :
- Tous les 5 services dans `App\Services\Security` ont déjà le namespace

**Plan de migration :**

#### Étape 1 : Préparer l'autoloading PSR-4
```json
// composer.json - remplacer classmap par psr-4
"autoload": {
    "psr-4": {
        "App\\": "src/"
    }
}
```

Puis exécuter sur DVA, RCA et PROD :
```bash
composer dump-autoload -o
```

#### Étape 2 : Migrer les 8 services restants

Pour chaque service :

1. **Ajouter le namespace** au début du fichier
```php
namespace App\Services;
```

2. **Ajouter les imports** pour les classes globales
```php
use PDO;
use Exception;
// etc.
```

3. **Vérifier les dépendances** : si le service utilise d'autres services, s'assurer que les imports sont corrects

4. **Tester** : s'assurer que le service fonctionne correctement

#### Étape 3 : Mettre à jour les fichiers qui utilisent ces services

Pour chaque fichier utilisant les services migrés :
- Ajouter `use App\Services\NomDuService;` si le fichier a un namespace différent
- Ou utiliser le nom court si dans le même namespace

#### Ordre de migration recommandé :

1. **EmailTemplates.php** (utilisé par ApiService, ReceiptService)
2. **ApiService.php** (utilisé par beaucoup de contrôleurs)
3. **FileService.php** (utilisé par ExportService, ReceiptService)
4. **ExportService.php** (autonome)
5. **OperationDataService.php** (autonome)
6. **BackupEncryptionService.php** (autonome)
7. **EventLogService.php** (autonome)
8. **MigrationService.php** (scripts uniquement)

**Fichiers à vérifier après migration :**

Controllers utilisant ces services :
- `src/Controllers/*.php` - vérifier tous les imports
- `scripts/cron/*.php` - vérifier les scripts cron
- `scripts/php/*.php` - vérifier les scripts de migration

**Tests à effectuer :**

1. **Sur DVA** :
   - Tester tous les endpoints principaux
   - Vérifier les logs (pas d'erreur Class not found)
   - Tester l'envoi d'emails (ApiService)
   - Tester les exports (ExportService)
   - Tester les reçus PDF (ReceiptService + EmailTemplates)

2. **Sur RCA** :
   - Tests de non-régression complets
   - Validation par un utilisateur test

3. **Sur PROD** :
   - Déploiement progressif
   - Monitoring des logs pendant 24h

**Risques et mitigation :**

⚠️ **Risque** : Erreur "Class not found" si l'autoloading ne fonctionne pas
✅ **Mitigation** : Tester d'abord sur DVA avec rollback rapide possible

⚠️ **Risque** : Services utilisés par scripts cron qui cassent
✅ **Mitigation** : Lister tous les scripts et les tester individuellement

⚠️ **Risque** : Cache d'autoload pas régénéré
✅ **Mitigation** : Forcer `composer dump-autoload -o` sur chaque environnement

**Impact sur les environnements :**

- **DVA** : Test complet de la migration
- **RCA** : Validation avant production
- **PROD** : Déploiement final

**Prérequis :**

- ✅ Accès SSH aux 3 environnements (DVA, RCA, PROD)
- ✅ Composer installé sur chaque environnement
- ✅ Backup de la version actuelle du code

---

**Statut :** 🔄 EN COURS - Migration partielle effectuée (10/18 services)
**Date de création :** 07/11/2025
**Durée estimée :** 1 jour (migration + tests)
**Priorité :** MOYENNE - Bloque le déploiement de l'intégration batiments

---

### 🟢 PRIORITÉ BASSE

#### 10. Documentation API

- Génération automatique OpenAPI/Swagger
- Documentation interactive
- Exemples de code pour chaque endpoint

#### 11. Tests automatisés

- Tests unitaires pour les services critiques
- Tests d'intégration pour les endpoints
- Tests de charge

---

## 📝 Notes

- Les tâches marquées 🔴 doivent être traitées en priorité
- Chaque tâche implémentée doit être documentée
- Prévoir des tests pour chaque nouvelle fonctionnalité

---

**Dernière mise à jour :** 07/11/2025
