# Documentation des Scripts de Migration GeoSector

## 📋 Vue d'ensemble

Ce dossier contient l'ensemble des scripts PHP permettant de migrer les données de l'ancienne base **geosector** (MySQL distante) vers la nouvelle base **pra_geo** (MariaDB sur maria4/IN4).

### Statistiques
- **Nombre total de scripts** : 21 fichiers
- **Base source** : MySQL geosector (sauvegardée depuis PM7)
- **Base cible** : MariaDB 11.4 pra_geo (maria4 sur IN4)
- **Méthode** : Restauration backup PM7 → Migration sur maria4
- **Ordre d'exécution** : Défini dans `migrate.php`

---

## 🚀 Guide Rapide de Migration depuis Backup PM7

### Architecture de migration

```
PM7 (11.1.2.17)
  └─ Backup nocturne chiffré : geosector_YYYYMMDD.sql.tar.gz.enc
       ↓
  Déchiffrement avec decpm7.sh
       ↓
  Transfert SCP vers IN4:/var/back/
       ↓
  Push vers maria4:/var/back/
       ↓
  Décompression et import dans maria4
       ↓
  Base geosector_YYYYMMDD créée dans maria4
       ↓
  Migration vers pra_geo (même serveur maria4)
```

### Étape 1️⃣ : Récupération et déchiffrement du backup depuis PM7

**Sur le serveur PM7 (11.1.2.17)** :

```bash
# Se connecter à PM7
ssh root@11.1.2.17

# Aller dans le dossier des backups
cd /var/back/

# Lister les backups disponibles
ls -lh geosector_*.sql.tar.gz.enc | tail -5

# Identifier le dernier backup (exemple : geosector_20251007.sql.tar.gz.enc)
BACKUP_DATE=$(date +%Y%m%d)
BACKUP_FILE="geosector_${BACKUP_DATE}.sql.tar.gz.enc"

# Déchiffrer le backup avec le script decpm7.sh
./decpm7.sh ${BACKUP_FILE}

# Résultat : fichier geosector_20251007.sql.tar.gz
```

### Étape 2️⃣ : Transfert du backup vers IN4

**Depuis PM7 ou depuis votre poste local** :

```bash
# Définir les variables
BACKUP_DATE=$(date +%Y%m%d)
BACKUP_FILE="geosector_${BACKUP_DATE}.sql.tar.gz"

# Transférer depuis PM7 vers IN4
scp root@11.1.2.17:/var/back/${BACKUP_FILE} root@51.159.7.190:/var/back/

# Vérifier la présence du fichier sur IN4
ssh root@51.159.7.190 "ls -lh /var/back/${BACKUP_FILE}"
```

### Étape 3️⃣ : Push du backup vers le container maria4

**Sur le serveur IN4 (51.159.7.190)** :

```bash
# Se connecter à IN4
ssh root@51.159.7.190

# Définir la variable
BACKUP_DATE=$(date +%Y%m%d)
BACKUP_FILE="geosector_${BACKUP_DATE}.sql.tar.gz"

# Pousser le fichier vers le container maria4
incus file push /var/back/${BACKUP_FILE} maria4/var/back/

# Vérifier le fichier dans le container
incus exec maria4 -- ls -lh /var/back/${BACKUP_FILE}
```

### Étape 4️⃣ : Décompression et import dans maria4

**Dans le container maria4** :

```bash
# Se connecter au container maria4
incus exec maria4 bash

# Définir les variables
BACKUP_DATE=$(date +%Y%m%d)
BACKUP_FILE="geosector_${BACKUP_DATE}.sql.tar.gz"
SQL_FILE="geosector_${BACKUP_DATE}.sql"

# Décompresser l'archive
cd /var/back
tar -xzf ${BACKUP_FILE}

# Vérifier la présence du fichier SQL
ls -lh ${SQL_FILE}

# Importer le SQL dans MariaDB
# Note : Le fichier SQL contient déjà CREATE DATABASE et USE geosector_YYYYMMDD
mariadb -u root -p'MyAlpLocal,90b' < ${SQL_FILE}

# Vérifier la création de la base
mariadb -u root -p'MyAlpLocal,90b' -e "SHOW DATABASES LIKE 'geosector_%';"

# Vérifier le nombre de tables
mariadb -u root -p'MyAlpLocal,90b' geosector_${BACKUP_DATE} -e "SHOW TABLES;"

# Vérifier quelques comptages
mariadb -u root -p'MyAlpLocal,90b' geosector_${BACKUP_DATE} -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 users_entites;"

# Sortir du container
exit
```

### Étape 5️⃣ : Migration des données vers pra_geo

**Important** : Le script PHP s'exécute sur le container **pra-geo** (pas maria4), car :
- ✅ PHP 8.3 est disponible sur pra-geo
- ✅ L'API GeoSector avec `ApiService::encryptData()` est présente
- ✅ pra-geo se connecte à maria4 via l'IP 13.23.33.4

**Option A : Migration globale (toutes les amicales)**

```bash
# Sur IN4, exécuter le script depuis le container pra-geo
incus exec pra-geo bash

# Aller dans le dossier de l'API
cd /var/www/geosector/api

# Lister les bases disponibles dans maria4 pour vérifier
# (optionnel - pour voir les backups restaurés)
echo "SHOW DATABASES LIKE 'geosector_%';" | \
  mysql -h 13.23.33.4 -u root -p'MyAlpLocal,90b'

# Lancer la migration complète
php scripts/php/migrate_from_backup.php \
  --source-db=geosector_20251007 \
  --target-db=pra_geo \
  --mode=global \
  --log=/var/www/geosector/api/logs/migration_global_20251007.log

# Suivre la progression dans les logs
tail -f /var/www/geosector/api/logs/migration_global_20251007.log
```

**Option B : Migration par amicale (recommandé)**

```bash
# Sur IN4, dans le container pra-geo
incus exec pra-geo bash

# Aller dans le dossier de l'API
cd /var/www/geosector/api

# Lister les amicales disponibles dans la base source
mysql -h 13.23.33.4 -u root -p'MyAlpLocal,90b' geosector_20251007 -e "
SELECT rowid, libelle, cp, ville, active
FROM users_entites
WHERE active = 1
ORDER BY rowid;"

# Migrer une amicale spécifique (exemple : ID 45)
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_20251007.log

# Suivre la progression dans un autre terminal
incus exec pra-geo -- tail -f /var/www/geosector/api/logs/migration_entity_45_20251007.log
```

### Étape 6️⃣ : Vérification de la migration

```bash
# Depuis pra-geo ou directement sur IN4, vérifier les données migrées

# Comparer les comptages source vs cible pour l'entité 45
mysql -h 13.23.33.4 -u root -p'MyAlpLocal,90b' << 'EOF'
SELECT
  'Users' as table_name,
  (SELECT COUNT(*) FROM geosector_20251007.users WHERE fk_entite = 45) as source,
  (SELECT COUNT(*) FROM pra_geo.users WHERE fk_entite = 45) as cible
UNION ALL SELECT
  'Operations',
  (SELECT COUNT(*) FROM geosector_20251007.operations WHERE fk_entite = 45),
  (SELECT COUNT(*) FROM pra_geo.operations WHERE fk_entite = 45)
UNION ALL SELECT
  'Passages',
  (SELECT COUNT(*) FROM geosector_20251007.ope_pass
   WHERE fk_operation IN (SELECT rowid FROM geosector_20251007.operations WHERE fk_entite = 45)),
  (SELECT COUNT(*) FROM pra_geo.ope_pass
   WHERE fk_operation IN (SELECT id FROM pra_geo.operations WHERE fk_entite = 45));
EOF

# Vérifier l'intégrité des montants
mariadb -u root -p'MyAlpLocal,90b' pra_geo << 'EOF'
SELECT p.fk_type, COUNT(*) as nb_passages, SUM(p.montant) as total_montant
FROM ope_pass p
JOIN operations o ON p.fk_operation = o.id
WHERE o.fk_entite = 45
GROUP BY p.fk_type;
EOF
```

### Étape 7️⃣ : Nettoyage (optionnel)

```bash
# Supprimer la base temporaire geosector_YYYYMMDD après migration réussie
mariadb -u root -p'MyAlpLocal,90b' -e "DROP DATABASE IF EXISTS geosector_20251007;"

# Supprimer les fichiers de backup
rm -f /var/back/geosector_20251007.sql.tar.gz
rm -f /var/back/geosector_20251007.sql
```

---

## 📝 Notes importantes

### Sécurité et mot de passe decpm7.sh

Le script `decpm7.sh` sur PM7 nécessite un mot de passe de déchiffrement. Assurez-vous de :
- Connaître le mot de passe avant de lancer le déchiffrement
- Ne jamais commiter ce mot de passe dans Git
- Le stocker de manière sécurisée (gestionnaire de mots de passe)

### Durée estimée de la migration

| Taille de l'amicale | Temps estimé |
|---------------------|--------------|
| Petite (< 5000 passages) | 2-5 minutes |
| Moyenne (5000-20000 passages) | 5-15 minutes |
| Grande (> 20000 passages) | 15-60 minutes |

### Espace disque requis

- Backup chiffré : ~500 MB
- Backup déchiffré : ~1.2 GB
- Import dans MariaDB : ~2 GB
- Total recommandé : **5 GB d'espace libre**

### Gestion des erreurs courantes

**Erreur : "Disk full"**
```bash
# Vérifier l'espace disque
df -h /var/back

# Nettoyer les anciens backups
find /var/back -name "geosector_*.sql" -mtime +7 -delete
```

**Erreur : "Table already exists"**
```bash
# Si migration échouée à moitié, supprimer les données partielles
mariadb -u root -p'MyAlpLocal,90b' pra_geo -e "
DELETE FROM ope_pass WHERE fk_operation IN (SELECT id FROM operations WHERE fk_entite = 45);
DELETE FROM operations WHERE fk_entite = 45;
DELETE FROM users WHERE fk_entite = 45;
DELETE FROM entites WHERE id = 45;"
```

---

## 🏗️ Architecture

### Fichiers de configuration

#### `config.php`
**Rôle** : Configuration principale des connexions bases de données et tunnel SSH

**Fonctionnalités** :
- Constantes de connexion SSH (host, port, clé privée)
- Constantes de connexion base source (via tunnel SSH sur port 13306)
- Constantes de connexion base cible (localhost:3306)
- Fonctions utilitaires :
  - `createSshTunnel()` : Établit le tunnel SSH
  - `closeSshTunnel()` : Ferme le tunnel SSH
  - `getSourceConnection()` : Retourne PDO vers base source
  - `getTargetConnection()` : Retourne PDO vers base cible
  - `logOperation()` : Journalise les opérations dans `logs/migration_YYYY-MM-DD.log`

**⚠️ Problèmes identifiés** :
- Mots de passe et clés SSH en dur (lignes 8-26)
- Pas de gestion de fichier `.env`
- Credentials visibles dans le code source

#### `MigrationConfig.php`
**Rôle** : Configuration simplifiée pour le chiffrement

**Fonctionnalités** :
- Classe `AppConfig` singleton
- Fournit la clé de chiffrement pour les données sensibles
- Pas de dépendance aux en-têtes HTTP (adapté pour CLI)

#### `migrate.php`
**Rôle** : Script orchestrateur principal

**Fonctionnalités** :
- Exécute toutes les migrations dans l'ordre
- Gestion des arguments CLI (`--truncate`, `--create-table`, `--help`)
- Peut exécuter une migration spécifique : `php migrate.php users`
- Peut exécuter toutes les migrations : `php migrate.php`
- Affiche des statistiques de migration (temps, succès, erreurs)

**Ordre d'exécution** :
1. Tables de référence `x_*` (devises, types, pays, régions, départements, villes)
2. `entites` (entities)
3. `users` (utilisateurs)
4. `operations` (opérations)
5. `ope_sectors` (secteurs d'opération)
6. `sectors_adresses` (adresses de secteurs)
7. `ope_users` (associations utilisateurs-opérations)
8. `ope_users_sectors` (associations utilisateurs-secteurs)
9. `ope_pass` (passages)
10. `ope_pass_histo` (historique des passages)
11. `medias` (fichiers médias)

---

## 🔄 Différences Structurelles entre les Bases

### Vue d'ensemble

**Base source (geosector)** - MariaDB 10.11.9 :
- Serveur version : `10.11.9-MariaDB-deb12`
- Clés primaires : `rowid` (int(11))
- Statut : `active` (tinyint(1))
- Dates : `date_creat`, `date_modif` (datetime)
- **Données en clair** (pas de chiffrement)
- **Pas de contraintes FK définies**

**Base cible (geo_app)** - MariaDB 11.4.5 :
- Serveur version : `11.4.5-MariaDB`
- Clés primaires : `id` (int(10) unsigned)
- Statut : `chk_active` (tinyint(1) unsigned)
- Dates : `created_at`, `updated_at` (timestamp avec ON UPDATE CURRENT_TIMESTAMP)
- **Données chiffrées** : `encrypted_name`, `encrypted_email`, `encrypted_phone`, `encrypted_user_name`, etc.
- **Contraintes FK définies** avec ON DELETE/ON UPDATE CASCADE

### Mappings globaux systématiques

```
rowid          → id
active         → chk_active
date_creat     → created_at
date_modif     → updated_at
date_eve       → passed_at (ope_pass uniquement)
support_rowid  → support_id (medias uniquement)
```

### Tables absentes de la source (nouvelles fonctionnalités)

Ces tables n'existent que dans `geo_app` et **ne sont pas migrées** :

#### Système de chat (4 tables)
- `chat_rooms` : Salles de conversation
- `chat_messages` : Messages
- `chat_participants` : Participants aux conversations
- `chat_read_receipts` : Accusés de lecture

#### Intégration Stripe (7 tables)
- `stripe_accounts` : Comptes Stripe connectés
- `stripe_terminal_readers` : Lecteurs de cartes (Tap to Pay)
- `stripe_android_certified_devices` : Devices Android certifiés
- `stripe_payment_history` : Historique des paiements
- `stripe_refunds` : Remboursements
- `stripe_webhooks` : Événements webhook Stripe

#### Sécurité et monitoring (4 tables)
- `sec_alerts` : Alertes de sécurité
- `sec_blocked_ips` : IPs bloquées
- `sec_failed_login_attempts` : Tentatives de connexion échouées
- `sec_performance_metrics` : Métriques de performance

#### Autres nouvelles tables
- `user_devices` : Informations des devices mobiles
- `x_departements_contours` : Contours géographiques des départements
- `x_users_titres` : Titres des utilisateurs (Mme, M., etc.)

### Tables renommées

| Source | Cible | Notes |
|--------|-------|-------|
| `users_entites` | `entites` | Migration via `migrate_entites.php` |
| `sectors` | `ope_sectors` | Intégrée dans les opérations |

### Différences par table migrée

#### ⚠️ `x_villes`
**Modifications structurelles** :
- Source : `cp` → Cible : `code_postal`
- Source : `departement` (varchar 65) → **Supprimé** (redondant avec `fk_departement`)

#### ⚠️ `x_departements`
**Ajouts dans cible** :
- `dept_limitrophes` (varchar 100) : Départements limitrophes
- `contour` (geometry) : Contour géographique

#### ✅ `entites` (source: `users_entites`)
**Champs chiffrés** :
- `libelle` → `encrypted_name`
- `email` → `encrypted_email`
- `tel1`, `tel2` → `encrypted_phone`, `encrypted_mobile` (avec détection 06/07)
- `iban` → `encrypted_iban`
- `bic` → `encrypted_bic`

**Nouveaux champs** :
- `chk_stripe` (tinyint) : Intégration Stripe activée
- `encrypted_stripe_id` (varchar 255) : ID Stripe chiffré
- `chk_username_manuel` (tinyint) : Gestion usernames manuelle/auto
- `chk_user_delete_pass` (tinyint) : Autorisation suppression passages
- `chk_lot_actif` (tinyint) : Lots actifs

**Champs supprimés** :
- Tous les champs métier spécifiques (`appname`, `http_host`, `tva_intra`, `rcs`, `siret`, `ape`, `couleur`, `prefecture`, `gerant_*`, `banque_*`, `genbase`, `groupebase`, `userbase`, `passbase`, `demo`, `lib_*`, `icon_*`, `btn_width`, `nbmembres`, `nbconnex`)

#### ✅ `users`
**Champs chiffrés** :
- `libelle` → `encrypted_name`
- `username` → `encrypted_user_name` (chiffrement recherchable)
- `telephone` → `encrypted_phone`
- `mobile` → `encrypted_mobile`
- `email` → `encrypted_email` (chiffrement recherchable)

**Mappings spécifiques** :
- `userpswd` ou `userpass` → `user_pass_hash`
- `prenom` → `first_name`
- `nom_tournee` → `sect_name`
- `alert_email` → `chk_alert_email`

**Champs supprimés** :
- Nombreux champs métier : `num_adherent`, `libelle_naissance`, `josh`, `email_secondaire`, `infos`, `ltt`, `lng`, `sector`, `dept_naissance`, `commune_naissance`, `anciennete`, `fk_categorie`, `fk_sous_categorie`, `adresse_*`, `cp`, `ville`, `matricule`, `fk_grade`, `chk_adherent_*`, `chk_archive`, `chk_double_affectation`

#### ✅ `operations`
**Modifications** :
- `chk_api_adresse` : **Supprimé** dans cible
- Dates : `date_deb`, `date_fin` changent de `date` à `NOT NULL DEFAULT '0000-00-00'`

#### ✅ `ope_sectors`
**Nouveaux champs** :
- `fk_old_sector` (int unsigned) : Référence à l'ancien `sectors.rowid` pour le mapping

#### ✅ `ope_users`
**Nouveaux champs dans cible** :
- `fk_role` (int unsigned) : Rôle de l'utilisateur dans l'opération
- `first_name` (varchar 45) : Prénom
- `encrypted_name` (varchar 255) : Nom chiffré
- `sect_name` (varchar 60) : Nom de tournée

**Impact** : Le script `migrate_ope_users.php` doit remplir ces champs ou les laisser vides

#### ✅ `ope_pass`
**Champs chiffrés** :
- `libelle` → `encrypted_name`
- `email` → `encrypted_email` (chiffrement recherchable)
- `phone` → `encrypted_phone`

**Nouveaux champs dans cible** :
- `residence` (varchar 75) : Nom de la résidence
- `date_recu` (timestamp) : Date de réception
- `date_creat_recu` (timestamp) : Date de création du reçu
- `date_sent_recu` (timestamp) : Date d'envoi du reçu
- `stripe_payment_id` (varchar 50) : ID du PaymentIntent Stripe

**Champs supprimés** :
- `lieudit` (varchar 75)
- `chk_habitat_vide` (tinyint)
- `lot_nb_passages` (int)

**Mappings spécifiques** :
- `recu` → `nom_recu`
- `date_eve` → `passed_at`
- `fk_type` : transformation 8→5, 9→6

#### ✅ `ope_pass_histo`
**Champ supprimé** :
- `fk_user` : **N'existe plus** dans la nouvelle structure

#### ✅ `medias`
**Mapping spécifique** :
- `support_rowid` → `support_id`

**Nouveaux champs dans cible** :
- `fk_entite` (int unsigned) : Propriétaire du média
- `fk_operation` (int unsigned) : Opération liée
- `file_type`, `file_category`, `file_size`, `mime_type` : Métadonnées fichier
- `original_name`, `file_path` : Informations fichier
- `original_width`, `original_height`, `processed_width`, `processed_height` : Dimensions images
- `is_processed` : Statut traitement image

**Champs supprimés** :
- `dir0`, `dir1`, `dir2` : Ancienne structure de dossiers
- `type_fichier`, `position`, `hauteur`, `largeur`, `niveaugris` : Anciens champs métier

#### ✅ `sectors_adresses`
**Nouveaux champs dans cible** :
- `id` (auto-increment) : Clé primaire ajoutée
- `osm_id` (int) : ID OpenStreetMap
- `osm_name` (varchar 50) : Nom OSM
- `osm_date_creat` (timestamp) : Date de création OSM
- `created_at`, `updated_at` : Timestamps standards

**Mappings** :
- `fk_sector` : ancien ID → nouvel ID via mapping `ope_sectors`

### Tables non migrées de la source

Ces tables existent dans `geosector` mais **ne sont pas migrées** vers `geo_app` :

- `articles`, `articles_pages` : Système d'articles (obsolète ?)
- `blog_articles`, `blog_pages` : Système de blog (obsolète ?)
- `email_counter`, `email_queue` : File d'attente emails (recréée dans cible)
- `ope_pass_recus` : Table séparée des reçus (intégrée dans `ope_pass`)
- `ope_users_suivis` : Suivi GPS des utilisateurs (archivé ?)
- `operations_docs`, `operations_eve_docs` : Documents opérations (archivé ?)
- `params` : Paramètres globaux (reconfiguré ?)
- `sectors`, `sectors_streets` : Tables sectors (transformée en `ope_sectors`)
- `users_lastpos` : Dernière position utilisateurs (archivé ?)
- `x_civilites` : Civilités (remplacé par `x_users_titres`)
- `x_users_categories`, `x_users_sous_categories`, `x_users_grades` : Catégories utilisateurs (supprimé)
- `y_conf`, `y_menus`, `y_modules`, `y_modules_rules`, `y_pages` : Configuration interface (obsolète)
- `z_logs`, `z_sessions`, `z_stats` : Logs et sessions (recréés dans cible)

### Contraintes de clés étrangères

**Source** : Aucune contrainte FK définie explicitement

**Cible** : Toutes les FK définies avec :
- `ON UPDATE CASCADE` : Mise à jour en cascade
- `ON DELETE CASCADE` ou `ON DELETE SET NULL` : Suppression gérée

**Impact migration** : Les scripts doivent respecter l'ordre des dépendances pour éviter les erreurs d'intégrité référentielle.

### Vues dans geo_app

- `chat_rooms_with_last_message` : Vue des salles de chat avec dernier message
- `v_stripe_entite_stats` : Statistiques Stripe par entité
- `v_stripe_payment_stats` : Statistiques paiements Stripe

**Note** : Les vues ne contiennent pas de données à migrer.

---

## 📊 Analyse Script par Script

### 1. Tables de référence `x_*`

#### `migrate_x_devises.php`
**Table** : `x_devises` (Devises)
**Mappings** :
- `rowid` → `id`
- `active` → `chk_active`

**Particularités** :
- Crée automatiquement la table si elle n'existe pas
- Utilise `logOperation()` pour le logging
- Pattern : `ON DUPLICATE KEY UPDATE`

**Statut** : ✅ Fonctionnel

---

#### `migrate_x_entites_types.php`
**Table** : `x_entites_types` (Types d'entités)
**Mappings** :
- `rowid` → `id`
- `active` → `chk_active`

**Particularités** :
- Logging simple via `echo`
- Pattern : `ON DUPLICATE KEY UPDATE`

**Statut** : ✅ Fonctionnel

---

#### `migrate_x_types_passages.php`, `migrate_x_types_reglements.php`, `migrate_x_users_roles.php`
**Tables** : Types de passages, types de règlements, rôles utilisateurs
**Pattern** : Identique aux autres tables `x_*`

**Statut** : ✅ Fonctionnel (non lu mais présumé identique)

---

#### `migrate_x_pays.php`
**Table** : `x_pays` (Pays)
**Mappings** :
- `rowid` → `id`
- `active` → `chk_active`
- Conservation de `code`, `fk_continent`, `fk_devise`, `libelle`

**Statut** : ✅ Fonctionnel

---

#### `migrate_x_regions.php`
**Table** : `x_regions` (Régions)
**Mappings** :
- `rowid` → `id`
- `active` → `chk_active`
- Conservation de tous les champs métier (`fk_pays`, `libelle`, `libelle_long`, `table_osm`, `departements`)

**Statut** : ✅ Fonctionnel

---

#### `migrate_x_departements.php`, `migrate_x_villes.php`
**Tables** : Départements, Villes
**Pattern** : Identique aux autres tables `x_*`

**Statut** : ✅ Fonctionnel (non lu mais présumé identique)

---

### 2. Tables métier principales

#### `migrate_entites.php`
**Table source** : `users_entites`
**Table cible** : `entites`

**Mappings** :
- `rowid` → `id`
- `active` → `chk_active`
- `libelle` → `encrypted_name` (chiffré)
- `tel1`, `tel2` → `encrypted_phone`, `encrypted_mobile` (logique de détection 06/07)
- `email` → `encrypted_email` (chiffré et recherchable)
- `iban` → `encrypted_iban` (chiffré)
- `bic` → `encrypted_bic` (chiffré)
- `cp` → `code_postal`

**Particularités** :
- **Chiffrement** : Utilise `ApiService::encryptData()` et `ApiService::encryptSearchableData()`
- **Logique téléphones** : Détecte mobiles (06/07) vs fixes
- **Valeur par défaut** : `chk_demo = 0` (forcé)

**Statut** : ✅ Fonctionnel

---

#### `migrate_users.php`
**Table** : `users`

**Mappings** :
- `rowid` → `id`
- `active` → `chk_active`
- `libelle` → `encrypted_name` (chiffré)
- `prenom` → `first_name`
- `nom_tournee` → `sect_name`
- `username` → `encrypted_user_name` (chiffré et recherchable)
- `userpswd` ou `userpass` → `user_pass_hash`
- `telephone` → `encrypted_phone` (chiffré)
- `mobile` → `encrypted_mobile` (chiffré)
- `email` → `encrypted_email` (chiffré et recherchable)
- `alert_email` → `chk_alert_email`

**Particularités** :
- **Tests de chiffrement** : Pour les 100 premiers utilisateurs (lignes 130-158)
  - Chiffre et déchiffre email et username
  - Affiche les valeurs pour vérification
  - **⚠️ Impact performance** : Ralentit la migration
- **Gestion rôle** : Force `fk_role=1` si `fk_role=0`
- **Gestion titre** : Force `fk_titre=1` si différent de 1 ou 2

**⚠️ PROBLÈME CRITIQUE** (lignes 227-239) :
```php
if ($exists) {
    $insertStmt->execute($userData);  // OK : update
    $successCount++;
} else {
    $errorCount++;  // ❌ ERREUR : devrait insérer, pas compter comme erreur
}
```
**Conséquence** : Les nouveaux utilisateurs ne sont jamais insérés, comptés comme erreurs

**Statut** : 🔴 Bug critique - logique d'insertion inversée

---

#### `migrate_operations.php`
**Table** : `operations`

**Mappings** :
- `rowid` → `id`
- `active` → `chk_active`
- `date_creat` → `created_at`
- `date_modif` → `updated_at`

**Particularités** :
- **Limitation arbitraire** : Ne migre que les **3 dernières opérations par entité** (lignes 54-70)
- Vérifie que les entités référencées ont été migrées
- Filtre basé sur `fk_entite IN (IDs des entités migrées)`
- Option `--truncate` pour vider la table avant migration

**⚠️ Problème** :
- Limite de 3 opérations non documentée et non paramétrable
- Pourrait perdre des données historiques importantes

**Statut** : ⚠️ Limitation fonctionnelle - à valider métier

---

#### `migrate_ope_sectors.php`
**Table** : `ope_sectors`

**Mappings** :
- Fusionne données de `sectors` et `ope_users_sectors`
- `rowid` de `sectors` → `fk_old_sector`
- Génère un nouvel `id` auto-incrémenté
- Conservation de `libelle`, `sector` (géométrie), `color`

**Particularités** :
- Crée une correspondance `fk_operation + fk_old_sector → id`
- Utile pour les migrations suivantes (ope_users_sectors, ope_pass)
- Ne migre que les secteurs liés aux opérations migrées
- Filtre `active = 1` sur source

**Statut** : ✅ Fonctionnel

---

#### `migrate_sectors_adresses.php`
**Table** : `sectors_adresses`

**Mappings** :
- `fk_sector` (ancien ID) → `fk_sector` (nouvel ID via mapping)
- Ajout de colonnes OSM avec valeurs par défaut :
  - `osm_id = 0`
  - `osm_name = ''`
  - `osm_date_creat = '0000-00-00 00:00:00'`

**Particularités** :
- Utilise le mapping créé par `migrate_ope_sectors.php`
- Recherche `fk_old_sector → id` dans `ope_sectors`
- Ignore les adresses dont le secteur n'a pas été migré

**Statut** : ✅ Fonctionnel

---

#### `migrate_ope_users.php`
**Table** : `ope_users`

**Mappings** :
- `rowid` → `id`
- `active` → `chk_active`
- `date_creat` → `created_at`
- `date_modif` → `updated_at`

**Particularités** :
- Vérifie que `fk_operation` et `fk_user` existent dans les tables cibles
- Double filtrage par IDs migrés
- Ne migre que les associations valides

**Statut** : ✅ Fonctionnel

---

#### `migrate_ope_users_sectors.php`
**Table** : `ope_users_sectors`

**Mappings** :
- `fk_sector` (ancien ID) → `fk_sector` (nouvel ID via mapping ope_sectors)
- Pas de `rowid` → utilise clé composite `(fk_operation, fk_user, fk_sector)`

**Particularités** :
- Triple vérification :
  1. Opération migrée
  2. Utilisateur migré
  3. Secteur existe dans mapping ope_sectors
- Génère la clé de recherche : `fk_operation . '_' . fk_old_sector`
- Compteur `skipped` pour associations ignorées

**Statut** : ✅ Fonctionnel

---

#### `migrate_ope_pass.php` ⭐ Script le plus complexe
**Table** : `ope_pass` (Passages)

**Mappings** :
- `date_eve` → `passed_at` 📅
- `libelle` → `encrypted_name` 🔒 (chiffré)
- `email` → `encrypted_email` 🔒 (chiffré et recherchable)
- `phone` → `encrypted_phone` 🔒 (chiffré)
- `recu` → `nom_recu`
- `fk_sector` (ancien ID) → `fk_sector` (nouvel ID via mapping)
- `fk_type` : transformation 8→5, 9→6

**Particularités** :
- **Gestion avancée des timeouts** (lignes 46-80) :
  - Configure `PDO::ATTR_TIMEOUT = 600` (10 minutes)
  - Configure variables MariaDB 10.11 :
    - `wait_timeout = 3600` (1h)
    - `net_read_timeout = 3600` (1h)
    - `net_write_timeout = 3600` (1h)
    - `innodb_lock_wait_timeout = 3600` (1h)
- **Suppression des contraintes FK** avant migration (lignes 98-120)
- **Suppression par lots** : Delete par lots de 100 000 (lignes 136-160)
- **Migration par lots** : 5 000 passages par lot (lignes 238-543)
- **Transactions par lot** : `BEGIN TRANSACTION` → traitement → `COMMIT`
- **Garbage collector** : Appel explicite `gc_collect_cycles()` pour libérer mémoire
- **Validation email** : `filter_var($email, FILTER_VALIDATE_EMAIL)` avant chiffrement
- **Gestion type_reglement** : Force à 4 si différent de 1, 2 ou 3
- **Recréation des FK** après migration (lignes 547-575)

**⚠️ Points d'attention** :
- Tue le processus SSH sur port 13306 au démarrage (ligne 23)
- Désactive `FOREIGN_KEY_CHECKS` pendant suppression
- Gestion silencieuse des passages dont secteur/utilisateur non migré

**Statut** : ✅ Fonctionnel - Optimisé pour gros volumes

---

#### `migrate_ope_pass_histo.php`
**Table** : `ope_pass_histo`

**Mappings** :
- `rowid` (non conservé, auto-increment)
- `date_histo` (conversion datetime)
- `fk_user` → **supprimé** (n'existe plus dans nouvelle structure)

**Particularités** :
- Ne migre que si `fk_pass` existe dans `ope_pass` cible
- Suppression complète avant migration (`DELETE FROM ope_pass_histo`)
- Mode "silencieux" : affiche uniquement les erreurs

**Statut** : ✅ Fonctionnel

---

#### `migrate_medias.php`
**Table** : `medias`

**Mappings** :
- `support_rowid` → `support_id` 📝
- `date_creat` → `created_at`
- `date_modif` → `updated_at`

**Particularités** :
- Vérifie que `fk_user_creat` et `fk_user_modif` existent
- Force à `0` (système) si utilisateur non migré
- Suppression complète avant migration
- Pas de chiffrement (fichiers référencés, pas de données sensibles)

**Statut** : ✅ Fonctionnel

---

## 🔍 Problèmes Identifiés par Priorité

### 🔴 CRITIQUES (Bloquants)

#### 1. **Bug logique `migrate_users.php` (lignes 227-239)**
**Impact** : Aucun nouvel utilisateur ne peut être inséré
```php
// Logique actuelle (INCORRECTE)
if ($exists) {
    $insertStmt->execute($userData);  // Update OK
    $successCount++;
} else {
    $errorCount++;  // ❌ Devrait faire INSERT
}
```
**Solution** :
```php
// Logique corrigée
$insertStmt->execute($userData);  // ON DUPLICATE KEY fait le travail
$successCount++;
```

---

### ⚠️ MAJEURS (Fonctionnels)

#### 2. **Limitation arbitraire opérations `migrate_operations.php` (lignes 54-70)**
**Impact** : Perte potentielle de données historiques
- Ne migre que 3 dernières opérations par entité
- Pas paramétrable
- Pas documenté dans le code

**Solution proposée** :
- Ajouter paramètre `--limit-operations=N` (défaut : 0 = toutes)
- Documenter dans `--help`

---

#### 3. **Tests de chiffrement dans production `migrate_users.php` (lignes 130-158)**
**Impact** : Ralentissement x2 de la migration users
- Teste chiffrement/déchiffrement pour 100 premiers users
- S'exécute à chaque migration

**Solution proposée** :
- Créer flag `--test-encryption`
- Désactiver par défaut

---

#### 4. **Configuration sensible en dur `config.php`**
**Impact** : Sécurité compromise
- Mots de passe en clair
- Clé SSH en dur
- Visible dans Git

**Solution proposée** :
- Créer `scripts/.env.example`
- Utiliser `vlucas/phpdotenv` ou parser manuel
- Ajouter `scripts/.env` au `.gitignore`

---

### ℹ️ MINEURS (Améliorations)

#### 5. **Incohérence logging**
**Impact** : Difficulté debugging
- Certains scripts : `logOperation()`
- D'autres : `echo` direct

**Solution** : Uniformiser avec `logOperation()` partout

---

#### 6. **Pas de transaction globale**
**Impact** : État incohérent si échec en milieu de migration
- Chaque table = migration indépendante
- Si échec table N, tables 1..N-1 déjà modifiées

**Solution proposée** :
- Option `--transactional` pour tout englobé dans 1 transaction
- Par défaut : comportement actuel (plus sûr)

---

#### 7. **Gestion FK manuelle dans `migrate_ope_pass.php`**
**Impact** : Complexité maintenance
- Désactivation/réactivation manuelle
- Suppression/recréation manuelle

**Solution** :
- Vérifier si vraiment nécessaire
- Documenter pourquoi (gros volumes)

---

#### 8. **Dates avec valeurs `0000-00-00`**
**Impact** : Warnings MariaDB en mode strict
- Plusieurs scripts utilisent `'0000-00-00'` ou `'0000-00-00 00:00:00'`
- MariaDB 10.11 en mode strict refuse ces valeurs

**Solution** : Remplacer par `NULL`

---

#### 9. **Pas de rapport détaillé post-migration**
**Impact** : Difficile de valider la migration
- Pas de récapitulatif des données migrées
- Pas de comparaison source vs cible

**Solution proposée** :
- Créer script `scripts/php/verify_migration.php`
- Compare counts par table
- Liste les incohérences

---

## ✅ Points Positifs

1. ✨ **Idempotence** : Utilisation systématique de `ON DUPLICATE KEY UPDATE`
2. 🔒 **Sécurité données** : Chiffrement des données sensibles
3. 🎯 **Filtrage intelligent** : Ne migre que les données liées (pas d'orphelins)
4. 📊 **Optimisation gros volumes** : Migration par lots (`ope_pass`)
5. 🔗 **Gestion dépendances** : Ordre d'exécution respecté
6. 🧹 **Nettoyage mémoire** : Garbage collection explicite
7. 📝 **Logging** : Historique des migrations dans `logs/`
8. 🚀 **Tunnel SSH automatique** : Connexion transparente

---

## ⚙️ Incohérences Détectées (Scripts vs Structures Réelles)

Suite à l'analyse comparative des scripts de migration avec les structures SQL réelles, voici les incohérences identifiées :

### 🔴 CRITIQUES

#### 1. `migrate_users.php` - Logique d'insertion inversée
**Lignes** : 227-239
**Problème** : Les nouveaux utilisateurs ne sont jamais insérés
```php
// Code actuel (INCORRECT)
if ($exists) {
    $insertStmt->execute($userData);  // Update OK
    $successCount++;
} else {
    $errorCount++;  // ❌ Devrait faire INSERT, pas compter comme erreur
}
```
**Impact** : **BLOQUANT** - Impossible d'ajouter de nouveaux utilisateurs
**Solution** : Utiliser directement `ON DUPLICATE KEY UPDATE` sans vérification préalable

---

### ⚠️ MAJEURS

#### 2. `migrate_x_departements.php` - Nouveaux champs non remplis
**Champs manquants** :
- `dept_limitrophes` (varchar 100) : Départements limitrophes
- `contour` (geometry) : Contour géographique

**Impact** : Ces champs resteront `NULL` après migration
**Solution** : Acceptable si ces données seront ajoutées ultérieurement

---

#### 3. `migrate_ope_users.php` - Nouveaux champs non remplis
**Champs manquants** :
- `fk_role` (int) : Rôle de l'utilisateur dans l'opération
- `first_name` (varchar 45) : Prénom
- `encrypted_name` (varchar 255) : Nom chiffré
- `sect_name` (varchar 60) : Nom de tournée

**Impact** : Fonctionnalités limitées - ces informations manqueront dans les opérations
**Solution** : Enrichir le script pour copier ces données depuis `users` si disponibles

---

#### 4. `migrate_medias.php` - Métadonnées fichiers manquantes
**Champs manquants** :
- `fk_entite`, `fk_operation` : Liens vers entités/opérations
- `file_type`, `file_category`, `file_size`, `mime_type` : Métadonnées
- `original_name`, `file_path` : Informations fichier
- `original_width`, `original_height`, `processed_width`, `processed_height` : Dimensions images
- `is_processed` : Statut traitement

**Impact** : Fonctionnalités de gestion de médias limitées
**Solution** :
- Analyser les fichiers existants pour extraire les métadonnées
- Déduire `fk_entite` et `fk_operation` depuis `support` et `support_id`

---

### ℹ️ MINEURS (Informations)

#### 5. `migrate_x_villes.php` - Champ `departement` ignoré
**Champ supprimé** : `departement` (varchar 65)
**Raison** : Redondant avec `fk_departement`
**Impact** : ✅ Correct - pas de perte de données

---

#### 6. `migrate_ope_pass.php` - Nouveaux champs Stripe
**Champs non remplis** :
- `residence` (varchar 75)
- `date_recu`, `date_creat_recu`, `date_sent_recu` (timestamps)
- `stripe_payment_id` (varchar 50)

**Impact** : Attendu - ces fonctionnalités sont nouvelles
**Solution** : ✅ Aucune action requise - champs remplis lors de l'utilisation future

---

#### 7. `migrate_entites.php` - Nouveaux champs Stripe/Config
**Champs non remplis** :
- `chk_stripe`, `encrypted_stripe_id` : Intégration Stripe
- `chk_username_manuel` : Gestion usernames
- `chk_user_delete_pass` : Autorisation suppression
- `chk_lot_actif` : Gestion lots

**Impact** : Attendu - nouvelles fonctionnalités
**Solution** : ✅ Valeurs par défaut appropriées définies dans la structure

---

### 📊 Résumé des vérifications

| Script | Statut | Problèmes critiques | Problèmes majeurs | Avertissements |
|--------|--------|---------------------|-------------------|----------------|
| `migrate_users.php` | 🔴 | 1 | 0 | 0 |
| `migrate_x_departements.php` | ⚠️ | 0 | 1 | 0 |
| `migrate_ope_users.php` | ⚠️ | 0 | 1 | 0 |
| `migrate_medias.php` | ⚠️ | 0 | 1 | 0 |
| `migrate_x_villes.php` | ✅ | 0 | 0 | 1 |
| `migrate_ope_pass.php` | ✅ | 0 | 0 | 1 |
| `migrate_entites.php` | ✅ | 0 | 0 | 1 |
| Autres scripts `x_*` | ✅ | 0 | 0 | 0 |

### 🛠️ Outil de vérification

Un script de vérification automatique a été créé : **`verify_migration_structure.php`**

**Usage** :
```bash
php scripts/php/verify_migration_structure.php
```

**Fonctionnalités** :
- Compare les colonnes source vs cible pour chaque table
- Identifie les colonnes non mappées
- Liste les nouvelles colonnes qui seront NULL
- Affichage coloré avec compteurs d'erreurs/avertissements

---

---

## 🚀 Migration via Endpoint API (Approche Recommandée)

### Vue d'ensemble

Au lieu d'exécuter les scripts PHP en ligne de commande, nous recommandons d'utiliser un **endpoint API REST** qui permet de migrer **UNE entité (amicale) à la fois** de manière contrôlée et testable.

### Avantages de cette approche

✅ **Migration progressive** : Une entité à la fois, avec validation entre chaque étape
✅ **Interface utilisateur** : Suivi visuel de la progression depuis Flutter/Web
✅ **Tests granulaires** : Vérification table par table avant de continuer
✅ **Rollback possible** : Annulation par entité en cas de problème
✅ **Logs détaillés** : Traçabilité complète dans l'API
✅ **Sécurité renforcée** : Authentification et autorisation via l'API
✅ **Moins risqué** : Pas de migration globale "big bang"

### Architecture de l'endpoint

#### Endpoint principal
```http
POST /api/migrations/entity
Authorization: Bearer {session_id}
Content-Type: application/json

{
  "entity_id": 45,           // ID de l'entité dans l'ancienne base
  "steps": ["users", "operations", "ope_pass"],  // Étapes à exécuter (optionnel)
  "dry_run": false,          // Mode simulation (optionnel)
  "truncate": false          // Vider les tables cible avant migration (optionnel)
}
```

#### Réponse
```json
{
  "status": "success",
  "entity_id": 45,
  "entity_name": "Amicale de Grenoble",
  "migration_id": "mig_abc123",
  "steps_completed": [
    {
      "step": "x_devises",
      "status": "success",
      "records_migrated": 1,
      "duration_ms": 45
    },
    {
      "step": "users",
      "status": "success",
      "records_migrated": 38,
      "duration_ms": 234
    }
  ],
  "total_duration_ms": 1234,
  "summary": {
    "total_records": 156,
    "total_errors": 0,
    "total_warnings": 2
  }
}
```

### Plan de migration étape par étape

#### Phase 0️⃣ : Préparation

**Actions à effectuer :**
- [ ] Créer le controller `MigrationController.php`
- [ ] Créer le service `MigrationService.php`
- [ ] Ajouter les routes dans `index.php`
- [ ] Configurer la connexion à la base source dans `AppConfig.php`
- [ ] Tester la connexion aux deux bases

**Tests de préparation :**
```bash
# Tester la connexion aux bases
GET /api/migrations/test-connections

# Lister les entités disponibles à migrer
GET /api/migrations/entities/available

# Récupérer les détails d'une entité source
GET /api/migrations/entities/{source_id}
```

---

#### Phase 1️⃣ : Tables de référence `x_*`

**Ordre d'exécution :**
1. `x_devises`
2. `x_entites_types`
3. `x_types_passages`
4. `x_types_reglements`
5. `x_users_roles`
6. `x_pays`
7. `x_regions`
8. `x_departements`
9. `x_villes`

**Endpoint de test :**
```http
POST /api/migrations/entity/step
{
  "entity_id": 45,
  "step": "x_devises",
  "dry_run": true
}
```

**Vérifications à effectuer :**
- [ ] Nombre d'enregistrements source == cible
- [ ] Champs `rowid` → `id` correctement mappés
- [ ] Champs `active` → `chk_active` correctement mappés
- [ ] Aucune erreur de contrainte FK

**Requêtes SQL de vérification :**
```sql
-- Comparer les counts
SELECT 'source' as db, COUNT(*) as count FROM geosector.x_devises
UNION ALL
SELECT 'cible' as db, COUNT(*) as count FROM geo_app.x_devises;

-- Vérifier les IDs manquants
SELECT s.rowid
FROM geosector.x_devises s
LEFT JOIN geo_app.x_devises t ON s.rowid = t.id
WHERE t.id IS NULL;
```

---

#### Phase 2️⃣ : Entité (Amicale)

**Tables concernées :**
- `users_entites` → `entites`

**Endpoint :**
```http
POST /api/migrations/entity/step
{
  "entity_id": 45,
  "step": "entites"
}
```

**Mappings critiques à vérifier :**
- [ ] `libelle` → `encrypted_name` (chiffrement AES-256)
- [ ] `email` → `encrypted_email` (chiffrement recherchable)
- [ ] `tel1`, `tel2` → `encrypted_phone`, `encrypted_mobile` (détection 06/07)
- [ ] `iban` → `encrypted_iban`
- [ ] `bic` → `encrypted_bic`
- [ ] `cp` → `code_postal`

**Tests fonctionnels :**
```http
# Récupérer l'entité migrée
GET /api/entites/45

# Vérifier le déchiffrement
# Le nom doit être lisible dans la réponse
```

**Vérifications SQL :**
```sql
-- Vérifier la présence de l'entité
SELECT id, encrypted_name, encrypted_email
FROM geo_app.entites
WHERE id = 45;

-- Les champs chiffrés doivent contenir des données base64
-- encrypted_name devrait ressembler à : "eyJpdiI6Ij..."
```

---

#### Phase 3️⃣ : Utilisateurs de l'entité

**Tables concernées :**
- `users` (filtrés par `fk_entite = 45`)

**Endpoint :**
```http
POST /api/migrations/entity/step
{
  "entity_id": 45,
  "step": "users"
}
```

**Mappings critiques :**
- [ ] `libelle` → `encrypted_name`
- [ ] `username` → `encrypted_user_name` (chiffrement recherchable)
- [ ] `userpswd` ou `userpass` → `user_pass_hash`
- [ ] `prenom` → `first_name`
- [ ] `nom_tournee` → `sect_name`
- [ ] `telephone` → `encrypted_phone`
- [ ] `mobile` → `encrypted_mobile`
- [ ] `email` → `encrypted_email`

**Tests fonctionnels :**
```http
# Lister les utilisateurs de l'entité
GET /api/users?fk_entite=45

# Tester un login avec un utilisateur migré
POST /api/login
{
  "username": "j.dupont",
  "password": "MotDePasseOriginal123"
}
```

**Vérifications SQL :**
```sql
-- Comparer les counts
SELECT COUNT(*) FROM geosector.users WHERE fk_entite = 45;
SELECT COUNT(*) FROM geo_app.users WHERE fk_entite = 45;

-- Vérifier les hash de mots de passe (doivent être identiques)
SELECT u1.rowid, u1.userpswd, u2.user_pass_hash
FROM geosector.users u1
JOIN geo_app.users u2 ON u1.rowid = u2.id
WHERE u1.fk_entite = 45
LIMIT 5;
```

**⚠️ Point critique :**
- Les mots de passe doivent être migrés **tels quels** (hash déjà fait)
- Ne PAS re-hasher les mots de passe
- Vérifier que le login fonctionne avec les anciens identifiants

---

#### Phase 4️⃣ : Opérations de l'entité

**Tables concernées :**
- `operations` (filtrées par `fk_entite = 45`)

**Endpoint :**
```http
POST /api/migrations/entity/step
{
  "entity_id": 45,
  "step": "operations",
  "options": {
    "limit": 0  // 0 = toutes les opérations (pas de limite à 3)
  }
}
```

**Mappings :**
- [ ] `rowid` → `id`
- [ ] `date_creat` → `created_at`
- [ ] `date_modif` → `updated_at`
- [ ] `active` → `chk_active`

**Tests fonctionnels :**
```http
# Lister les opérations de l'entité
GET /api/operations?fk_entite=45

# Récupérer une opération spécifique
GET /api/operations/{operation_id}
```

**Vérifications SQL :**
```sql
-- Comparer les counts
SELECT COUNT(*) FROM geosector.operations WHERE fk_entite = 45;
SELECT COUNT(*) FROM geo_app.operations WHERE fk_entite = 45;

-- Vérifier les 3 dernières opérations
SELECT id, libelle, date_deb, date_fin
FROM geo_app.operations
WHERE fk_entite = 45
ORDER BY id DESC
LIMIT 3;
```

---

#### Phase 5️⃣ : Secteurs des opérations

**Tables concernées :**
- `sectors` + `ope_users_sectors` → `ope_sectors`

**Endpoint :**
```http
POST /api/migrations/entity/step
{
  "entity_id": 45,
  "step": "ope_sectors"
}
```

**Logique spécifique :**
- Fusion de `sectors.rowid` → `ope_sectors.fk_old_sector`
- Génération de nouveaux `ope_sectors.id` (auto-increment)
- Création d'un mapping `old_id → new_id` pour les étapes suivantes

**Vérifications SQL :**
```sql
-- Vérifier le mapping des secteurs
SELECT os.id, os.fk_old_sector, os.libelle, os.fk_operation
FROM geo_app.ope_sectors os
JOIN geo_app.operations o ON os.fk_operation = o.id
WHERE o.fk_entite = 45
ORDER BY os.fk_operation, os.id;
```

---

#### Phase 6️⃣ : Adresses des secteurs

**Tables concernées :**
- `sectors_adresses`

**Endpoint :**
```http
POST /api/migrations/entity/step
{
  "entity_id": 45,
  "step": "sectors_adresses"
}
```

**Mappings :**
- [ ] Utilisation du mapping `fk_old_sector → new_id` créé en Phase 5
- [ ] Ajout de colonnes OSM avec valeurs par défaut

**Vérifications SQL :**
```sql
-- Comparer les counts
SELECT COUNT(*) FROM geosector.sectors_adresses sa
JOIN geosector.sectors s ON sa.fk_sector = s.rowid
JOIN geosector.ope_users_sectors ous ON ous.fk_sector = s.rowid
JOIN geosector.operations o ON ous.fk_operation = o.rowid
WHERE o.fk_entite = 45;

-- Vérifier dans la cible
SELECT COUNT(*) FROM geo_app.sectors_adresses sa
JOIN geo_app.ope_sectors os ON sa.fk_sector = os.id
JOIN geo_app.operations o ON os.fk_operation = o.id
WHERE o.fk_entite = 45;
```

---

#### Phase 7️⃣ : Associations opérations-utilisateurs

**Tables concernées :**
- `ope_users`
- `ope_users_sectors`

**Endpoint :**
```http
POST /api/migrations/entity/step
{
  "entity_id": 45,
  "step": "ope_users"
}
```

**⚠️ Point d'attention :**
- Nouveaux champs dans cible : `fk_role`, `first_name`, `encrypted_name`, `sect_name`
- **Solution recommandée** : Enrichir avec les données depuis `users` si disponibles

**Vérifications SQL :**
```sql
-- Vérifier les associations
SELECT ou.id, ou.fk_operation, ou.fk_user, u.encrypted_user_name
FROM geo_app.ope_users ou
JOIN geo_app.users u ON ou.fk_user = u.id
JOIN geo_app.operations o ON ou.fk_operation = o.id
WHERE o.fk_entite = 45
LIMIT 10;
```

---

#### Phase 8️⃣ : Passages (Données critiques)

**Tables concernées :**
- `ope_pass`

**Endpoint :**
```http
POST /api/migrations/entity/step
{
  "entity_id": 45,
  "step": "ope_pass",
  "options": {
    "batch_size": 5000  // Migration par lots de 5000
  }
}
```

**Mappings critiques :**
- [ ] `date_eve` → `passed_at`
- [ ] `libelle` → `encrypted_name` (chiffrement)
- [ ] `email` → `encrypted_email` (chiffrement recherchable + validation)
- [ ] `phone` → `encrypted_phone` (chiffrement)
- [ ] `recu` → `nom_recu`
- [ ] `fk_type` : transformation 8→5, 9→6
- [ ] `fk_sector` : ancien ID → nouvel ID via mapping Phase 5

**Tests fonctionnels :**
```http
# Compter les passages de l'entité
GET /api/passages/count?entity_id=45

# Récupérer les derniers passages
GET /api/passages?entity_id=45&limit=10&order=desc
```

**Vérifications SQL :**
```sql
-- Comparer les counts par opération
SELECT o.id, o.libelle, COUNT(p.id) as nb_passages
FROM geo_app.operations o
LEFT JOIN geo_app.ope_pass p ON p.fk_operation = o.id
WHERE o.fk_entite = 45
GROUP BY o.id, o.libelle
ORDER BY o.id;

-- Vérifier les montants totaux
SELECT SUM(montant) FROM geosector.ope_pass
WHERE fk_operation IN (SELECT rowid FROM geosector.operations WHERE fk_entite = 45);

SELECT SUM(montant) FROM geo_app.ope_pass
WHERE fk_operation IN (SELECT id FROM geo_app.operations WHERE fk_entite = 45);
```

**⚠️ TRÈS IMPORTANT :**
- Cette phase peut prendre du temps (nombreux passages)
- Utiliser la migration par lots (5000 par 5000)
- Afficher une progression dans l'interface
- Vérifier que les emails sont valides avant chiffrement

---

#### Phase 9️⃣ : Historique et médias

**Tables concernées :**
- `ope_pass_histo`
- `medias`

**Endpoint :**
```http
POST /api/migrations/entity/step
{
  "entity_id": 45,
  "step": "ope_pass_histo"
}
```

**Mappings :**
- [ ] `ope_pass_histo` : Suppression du champ `fk_user` (n'existe plus)
- [ ] `medias` : Mapping `support_rowid` → `support_id`

**Vérifications SQL :**
```sql
-- Historique des passages
SELECT COUNT(*) FROM geo_app.ope_pass_histo h
JOIN geo_app.ope_pass p ON h.fk_pass = p.id
JOIN geo_app.operations o ON p.fk_operation = o.id
WHERE o.fk_entite = 45;

-- Médias de l'entité
SELECT COUNT(*) FROM geo_app.medias
WHERE support = 'entite' AND support_id = 45;
```

---

### Endpoints de gestion et monitoring

#### Endpoints de vérification
```http
# Statut de la migration d'une entité
GET /api/migrations/entity/{entity_id}/status

# Logs de migration
GET /api/migrations/entity/{entity_id}/logs

# Rapport de migration
GET /api/migrations/entity/{entity_id}/report
```

#### Endpoints de rollback
```http
# Annuler la migration d'une entité
DELETE /api/migrations/entity/{entity_id}

# Supprimer uniquement une étape
DELETE /api/migrations/entity/{entity_id}/step/{step_name}
```

#### Endpoints de comparaison
```http
# Comparer les données source vs cible
GET /api/migrations/entity/{entity_id}/compare

# Vérifier l'intégrité des données
GET /api/migrations/entity/{entity_id}/verify
```

---

### Checklist finale de validation

#### ✅ Données de base
- [ ] L'entité existe dans `geo_app.entites` avec toutes les données chiffrées
- [ ] Le déchiffrement fonctionne (appel `GET /api/entites/{id}`)
- [ ] Le logo de l'entité est présent (si applicable)

#### ✅ Utilisateurs
- [ ] Tous les utilisateurs de l'entité sont présents
- [ ] Le login fonctionne avec les anciens identifiants
- [ ] Les données chiffrées sont déchiffrables
- [ ] Les rôles sont corrects (`fk_role`)

#### ✅ Opérations
- [ ] Toutes les opérations sont migrées (pas de limite à 3)
- [ ] Les dates `date_deb` et `date_fin` sont correctes
- [ ] Les secteurs associés sont présents

#### ✅ Passages
- [ ] Le nombre total de passages correspond
- [ ] Les montants totaux correspondent
- [ ] Les emails chiffrés sont valides et déchiffrables
- [ ] Les `fk_type` sont corrects (vérifier transformations 8→5, 9→6)

#### ✅ Intégrité référentielle
- [ ] Aucune contrainte FK violée
- [ ] Tous les `fk_user` existent dans `users`
- [ ] Tous les `fk_operation` existent dans `operations`
- [ ] Tous les `fk_sector` existent dans `ope_sectors`

#### ✅ Tests fonctionnels
- [ ] Login avec un utilisateur de l'entité
- [ ] Affichage des opérations dans l'interface
- [ ] Affichage des passages dans une opération
- [ ] Création d'un nouveau passage (test post-migration)
- [ ] Génération d'un reçu fiscal (si applicable)

---

### Scripts SQL utiles

#### Comparer les totaux globaux
```sql
-- Script à exécuter après migration complète
SELECT
  'Entité' as type,
  (SELECT COUNT(*) FROM geosector.users_entites WHERE rowid = 45) as source,
  (SELECT COUNT(*) FROM geo_app.entites WHERE id = 45) as cible
UNION ALL
SELECT
  'Users',
  (SELECT COUNT(*) FROM geosector.users WHERE fk_entite = 45),
  (SELECT COUNT(*) FROM geo_app.users WHERE fk_entite = 45)
UNION ALL
SELECT
  'Operations',
  (SELECT COUNT(*) FROM geosector.operations WHERE fk_entite = 45),
  (SELECT COUNT(*) FROM geo_app.operations WHERE fk_entite = 45)
UNION ALL
SELECT
  'Passages',
  (SELECT COUNT(*) FROM geosector.ope_pass WHERE fk_operation IN
    (SELECT rowid FROM geosector.operations WHERE fk_entite = 45)),
  (SELECT COUNT(*) FROM geo_app.ope_pass WHERE fk_operation IN
    (SELECT id FROM geo_app.operations WHERE fk_entite = 45));
```

#### Vérifier l'intégrité des montants
```sql
-- Totaux des passages par type
SELECT p.fk_type, COUNT(*) as nb, SUM(p.montant) as total
FROM geo_app.ope_pass p
JOIN geo_app.operations o ON p.fk_operation = o.id
WHERE o.fk_entite = 45
GROUP BY p.fk_type;
```

---

## 📋 TODO List

### 🔴 Priorité CRITIQUE (À faire IMMÉDIATEMENT)

- [ ] **Créer l'endpoint API de migration**
  - Fichier : `src/Controllers/MigrationController.php`
  - Service : `src/Services/MigrationService.php`
  - Routes : Ajouter dans `index.php`
  - Action : Implémenter la logique de migration par étape

- [ ] **Configurer la connexion à la base source**
  - Fichier : `src/Config/AppConfig.php`
  - Action : Ajouter les paramètres de connexion à `geosector` via tunnel SSH

### ⚠️ Priorité HAUTE (Cette semaine)

- [ ] **Externaliser configuration sensible**
  - Créer `scripts/.env.example`
  - Parser `.env` dans `config.php`
  - Ajouter `scripts/.env` au `.gitignore`
  - Mettre à jour documentation

- [ ] **Paramétrer limite opérations**
  - Fichier : `scripts/php/migrate_operations.php`
  - Ajouter argument CLI `--limit-operations=N`
  - Documenter dans `--help`
  - Valeur par défaut : 0 (toutes)

- [ ] **Tests de chiffrement optionnels**
  - Fichier : `scripts/php/migrate_users.php`
  - Créer flag `--test-encryption`
  - Désactiver par défaut

### 📊 Priorité MOYENNE (Ce mois-ci)

- [ ] **Uniformiser logging**
  - Remplacer tous les `echo` par `logOperation()`
  - Ajouter niveaux : DEBUG, INFO, WARNING, ERROR
  - Format uniforme

- [ ] **Script de vérification post-migration**
  - Créer `scripts/php/verify_migration.php`
  - Comparer counts source vs cible
  - Vérifier intégrité référentielle
  - Générer rapport HTML

- [ ] **Remplacer dates `0000-00-00` par `NULL`**
  - Fichiers concernés : `migrate_operations.php`, `sectors_adresses.php`
  - Remplacer par `NULL` pour compatibilité MariaDB strict mode

- [ ] **Documentation utilisateur**
  - Guide pas-à-pas migration complète
  - Prérequis (tunnel SSH, accès bases)
  - Procédure de rollback
  - FAQ troubleshooting

### 💡 Priorité BASSE (Nice to have)

- [ ] **Option transaction globale**
  - Flag `--transactional` dans `migrate.php`
  - Englober toutes migrations dans 1 transaction
  - Warning si échec = rollback complet

- [ ] **Barre de progression**
  - Utiliser library CLI (ex: `symfony/console`)
  - Afficher progression en temps réel
  - ETA par table

- [ ] **Mode dry-run**
  - Flag `--dry-run` pour simuler sans écrire
  - Afficher ce qui serait fait
  - Utile pour tests

- [ ] **Export/Import rapide**
  - Alternative : dump SQL + sed pour mapping IDs
  - Plus rapide pour gros volumes
  - Comparer perf vs PHP

- [ ] **Tests automatisés**
  - PHPUnit tests pour chaque script
  - Mock des connexions DB
  - CI/CD avec GitHub Actions

---

## 📚 Références

### Fichiers clés
- **Schéma DB** : `docs/geo_app.sql`
- **Configuration** : `scripts/config.php`
- **Orchestrateur** : `scripts/php/migrate.php`
- **Logs** : `scripts/logs/migration_YYYY-MM-DD.log`

### Ordre d'exécution (défini dans `migrate.php:64-85`)
```
x_devises → x_entites_types → x_types_passages → x_types_reglements →
x_users_roles → x_pays → x_regions → x_departements → x_villes →
entites → users → operations → ope_sectors → sectors_adresses →
ope_users → ope_users_sectors → ope_pass → ope_pass_histo → medias
```

### Commandes utiles
```bash
# Migration complète
php scripts/php/migrate.php

# Migration d'une table spécifique
php scripts/php/migrate.php users

# Migration avec truncate
php scripts/php/migrate.php users --truncate

# Afficher l'aide
php scripts/php/migrate.php --help

# Consulter les logs
tail -f scripts/logs/migration_$(date +%Y-%m-%d).log
```

---

## 🔧 Corrections Critiques Appliquées (10/10/2025)

### Correction #15 : Ajout de contraintes UNIQUE pour éviter les doublons

**Problème identifié** : Les tables `ope_users` et `ope_users_sectors` n'avaient PAS de contrainte UNIQUE sur leurs combinaisons de FK, permettant des doublons massifs lors de la migration.

**Diagnostic** :
- Table `ope_users` : Possibilité de centaines de doublons pour la même paire (fk_operation, fk_user)
- Table `ope_users_sectors` : Risque de doublons sur (fk_operation, fk_user, fk_sector)
- Le `ON DUPLICATE KEY UPDATE` dans le script PHP ne fonctionnait pas car aucune contrainte UNIQUE n'existait

**Solution appliquée** :

1. **Mise à jour de `geo_app_structure.sql`** (structure de référence) :
   - Ajout de `UNIQUE KEY idx_operation_user (fk_operation, fk_user)` sur `ope_users` (ligne 403)
   - Ajout de `UNIQUE KEY idx_operation_user_sector (fk_operation, fk_user, fk_sector)` sur `ope_users_sectors` (ligne 430)

2. **Correction du code PHP** dans `migrate_from_backup.php` :
   ```php
   // AVANT (INCORRECT - créait des doublons)
   $sql = "SELECT ou.rowid, ou.fk_operation, ou.fk_user, ...
   INSERT INTO ope_users (id, fk_operation, fk_user, ...) VALUES (:id, ...

   // APRÈS (CORRECT - évite les doublons)
   $sql = "SELECT DISTINCT ou.fk_operation, ou.fk_user, ...  // Pas de rowid
   INSERT INTO ope_users (fk_operation, fk_user, ...) VALUES (...  // Pas d'id, auto-increment
   ```

3. **Application sur les bases de données** :
   ```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 que les contraintes sont bien créées
   SHOW INDEX FROM ope_users WHERE Key_name = 'idx_operation_user';
   SHOW INDEX FROM ope_users_sectors WHERE Key_name = 'idx_operation_user_sector';
   ```

**Impact** :
- ✅ Empêche définitivement les doublons dans `ope_users` et `ope_users_sectors`
- ✅ Le `ON DUPLICATE KEY UPDATE` fonctionne désormais correctement
- ✅ Migration idempotente (peut être relancée sans créer de duplicatas)

**Fichiers modifiés** :
- `scripts/php/geo_app_structure.sql` - Structure de référence mise à jour (lignes 403, 430)
- `scripts/php/migrate_from_backup.php` - Code corrigé avec SELECT DISTINCT (lignes 1125-1191)

**À appliquer sur les environnements** :
```bash
# DEV (dva_geo sur maria3/IN3)
mysql -h 13.23.33.4 -u dva_geo_user -p'CBq9tKHj6PGPZuTmAHV7' dva_geo
# Puis exécuter les ALTER TABLE ci-dessus

# REC (rca_geo sur maria3/IN3)
mysql -h 13.23.33.4 -u rca_geo_user -p'UPf3C0cQ805LypyM71iW' rca_geo
# Puis exécuter les ALTER TABLE ci-dessus

# PROD (pra_geo sur maria4/IN4)
mysql -h 13.23.33.4 -u pra_geo_user -p'd2jAAGGWi8fxFrWgXjOA' pra_geo
# Puis exécuter les ALTER TABLE ci-dessus
```

**Pour re-migrer 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
```

---

**Dernière mise à jour** : 2025-10-10
**Auteur de l'analyse** : Claude Code
**Version** : 1.1
