# HANDOFF — Flash Énergies (extraction PDF → Codial)

> Document d'entrée pour toute personne (ou agent IA) qui reprend le projet.
> Objectif : comprendre le système, le lancer, le débugger et l'étendre **sans avoir à re-explorer le code**.
> Dernière mise à jour : 2026-09-22 — branche `main`, dernier commit `4641eb1`.

---

## 1. En une phrase

Application **Laravel 11 + PHP 8.2** qui reçoit des **bons de commande PDF** de bailleurs sociaux,
les fait analyser par un **script Python (pdfplumber + OpenAI)**, stocke le résultat en **MySQL**,
laisse un opérateur corriger les données via une interface tablette, puis génère des **fichiers XML + PDF**
déposés dans `public/pdfs/codial/import/` pour être aspirés par le logiciel métier **Codial**.

---

## 2. Stack & prérequis

| Élément | Valeur |
|---|---|
| Framework | Laravel 11.31, PHP ^8.2 |
| Auth | Laravel Breeze (Blade) |
| Front | Blade + Tailwind 3 + Alpine.js + Vite 6 |
| BDD | MySQL (local : MAMP, **port 8889**, base `flash`, root/root) |
| Python | venv local `scripts/venv/bin/python3`, chemin dans `.env` → `PYTHON_PATH` |
| Libs Python | `pdfplumber`, `openai`, `dotenv`, `mysql-connector-python` (`scripts/requirements.txt`) |
| IA | OpenAI **`gpt-3.5-turbo`**, `temperature=0`, clé `OPENAI_API_KEY` dans `.env` |
| Queue | `QUEUE_CONNECTION=sync` en local (job exécuté en synchrone) |
| Tests | Aucun test métier — seulement les tests Breeze par défaut dans `tests/` |

### Démarrage local

```bash
composer install
npm install && npm run dev
php artisan migrate
php artisan serve            # http://localhost:8000

# Python (à recréer sur CHAQUE machine — le venv n'est pas versionné)
rm -rf scripts/venv
python3 -m venv scripts/venv
scripts/venv/bin/python3 -m pip install -r scripts/requirements.txt
scripts/venv/bin/python3 -c "import pdfplumber, openai, mysql.connector, dotenv; print('imports OK')"
```

Puis vérifier que `PYTHON_PATH` dans `.env` pointe bien sur le binaire absolu du venv.

---

## 3. Le pipeline, de bout en bout

```
 [Utilisateur] upload PDF sur /import
        │  UploadController@store  → stocke dans public/pdfs/<timestamp>-<nom>.pdf
        │                          → crée PdfFile{status: pending}
        ▼
 ProcessPdfFileJob (app/Jobs/ProcessPdfFileJob.php)  ← le cœur du système
        │  status → processing
        │  exec(PYTHON_PATH scripts/traitement.py <pdf absolu>)
        ▼
 scripts/traitement.py
        │  1. pdfplumber → texte brut
        │  2. BailleurDetector.detect_bailleur(texte)     ← SANS IA (mots-clés/patterns)
        │  3. PromptBuilder.build_prompt(code, texte)     ← prompt générique + règles du bailleur
        │  4. OpenAI gpt-3.5-turbo → JSON
        │  5. INSERT direct MySQL : interventions + intervention_lignes
        │  ↩︎ imprime UNE ligne JSON : {"code":0,"intervention_id":N,"detection":{...},"logs":[...]}
        ▼
 ProcessPdfFileJob (suite)
        │  parse la ligne JSON dans la sortie
        │  corrigerReference() sur CHAQUE ligne  ← préfixage des codes article par bailleur
        │     └─ si le code corrigé n'existe pas dans `intervention_imports` → la ligne est SUPPRIMÉE
        │  status → done, déplace le PDF vers public/pdfs/processed/
        │  createFiles() :
        │     ├─ copie le PDF   → public/pdfs/codial/import/{basename}.pdf
        │     ├─ matche le gestionnaire par similar_text (> 50 %) → code_gestionnaire
        │     └─ InterventionController::generateXml() → public/pdfs/codial/import/{basename}.xml
        ▼
 [Codial] aspire les fichiers à plat dans import/ (puis les supprime)
```

`{basename}` = `InterventionController::importBasename()` = `{code_bailleur}_{reference_bc}`
avec les `/` et `\` remplacés par `_` (**voir piège §7.3**).

### Suivi & correction côté opérateur

| URL | Rôle |
|---|---|
| `/import` | Upload des PDF (page d'accueil, `/` redirige ici) |
| `/admin/interventions/suivi` | Tableau de bord des jobs (polling JSON sur `.../suivi/data`), retry / delete / logs |
| `/admin/interventions` | Liste des interventions extraites |
| `/admin/interventions/{id}` | Édition d'une intervention + de ses lignes |
| `/admin/interventions/{id}/xml/preview` | Aperçu du XML généré (texte brut) |
| `/admin/codial/import-interventions` | Importe `public/pdfs/codial/export/articles.xml` dans `intervention_imports` (le référentiel BPU) |
| `/admin/codial/import-bailleurs` | Importe `public/pdfs/codial/export/bailleurs.xml` dans `bailleurs` + `bailleur_contacts` |
| `/admin/users` | Gestion des comptes |

**Les deux XML d'`export/` viennent de Codial** : ils alimentent le référentiel articles et la liste
bailleurs/gestionnaires. Les réimporter après chaque mise à jour du BPU côté client.

---

## 4. Cartographie du code

### PHP
| Fichier | Rôle |
|---|---|
| `app/Jobs/ProcessPdfFileJob.php` (477 l.) | **Fichier le plus important.** Orchestration, `corrigerReference()` (règles de préfixage par bailleur), `createFiles()`, `norm()` (normalisation texte pour le matching) |
| `app/Http/Controllers/InterventionController.php` (349 l.) | CRUD interventions, `importBasename()`, `generateXml()`, `previewXml()`, endpoints du suivi de jobs |
| `app/Http/Controllers/UploadController.php` | Upload PDF (max 5 Mo, `mimes:pdf`), disque `flash_energie` (= `public/pdfs`) |
| `app/Http/Controllers/BailleurImportController.php` / `InterventionImportController.php` | Import des XML d'export Codial |
| `app/Models/` | `Intervention` (hasMany `lignes`), `InterventionLigne`, `PdfFile` (hasMany `logs`, belongsTo `intervention`), `PdfFileLog`, `Bailleur` (hasMany `contacts`), `BailleurContact`, `InterventionImport` (référentiel articles) |
| `routes/web.php` | Tout est derrière `middleware('auth')` |
| `routes/console.php` | `Schedule::command('queue:work --tries=3 --stop-when-empty')->everyMinute()` → **nécessite un cron `schedule:run` sur le serveur** |
| `config/filesystems.php` | Disque `flash_energie` → `public_path('pdfs')` |

### Python (`scripts/`)
| Fichier | Rôle |
|---|---|
| `traitement.py` (282 l.) | **Le seul script appelé par Laravel.** Point d'entrée : `python3 traitement.py <chemin absolu du pdf>` |
| `bailleurs_detector.py` (267 l.) | Détection du bailleur sans IA : scoring mots-clés + logo + code fournisseur |
| `prompt_builder.py` | Assemble prompt générique + règles spécifiques du bailleur |
| `prompts/generic_prompt.py` | Schéma JSON attendu + règles communes (dont l'exclusion du tél. Flash Énergies `0328271260`) |
| `prompts/<bailleur>_rules.py` | Un fichier de règles par bailleur |
| ~~`traitement_old.py`, `traitement_old_backup.py`, `traitement_new.py`, `traitement_batch.py`~~ | **Supprimés le 2026-08-24** (code mort, aucun n'était appelé). Récupérables dans l'historique git si besoin. |

Docs Python existantes : `scripts/README.md` (venv/diagnostic) et `scripts/REFACTORING.md` (architecture détection + prompts).

---

## 5. Modèle de données

```
interventions
  nom_client, gestionnaire, code_gestionnaire*, locataire, lieu_nom,
  adresse_rue, adresse_complement, code_postal, ville, telephone,
  reference_bc, date_bc, date_limite, commentaire, code_bailleur, pdf
    └─ hasMany intervention_lignes
         code_ref, designation, quantite, prix_unitaire, montant, tva*, commentaire*

pdf_files : filename, status(pending|processing|done|error), intervention_id,
            started_at, finished_at, error_message
    └─ hasMany pdf_file_logs (type, message)

bailleurs (code, nom) └─ hasMany bailleur_contacts (contact_id = ID Codial, nom)
intervention_imports (code, designation, designation_alt)  ← référentiel BPU / articles Codial
```

> ⚠️ **Dérive de schéma (schema drift)** : les colonnes marquées `*` — `interventions.code_gestionnaire`,
> `intervention_lignes.tva`, `intervention_lignes.commentaire` — sont **utilisées par le code mais
> n'existent dans AUCUNE migration**. Elles ont été ajoutées à la main en base. Une base recréée
> à partir de `php artisan migrate` **plantera**. À corriger par une migration de rattrapage avant
> tout redéploiement sur une base neuve.

Autres écarts de `$fillable` à connaître :
- `Intervention::$fillable` ne contient **pas** `date_limite` ni `code_gestionnaire` → non modifiables via le formulaire d'édition (le Python les écrit en SQL brut, `code_gestionnaire` est assigné directement dans `createFiles()`).
- `InterventionLigne::$fillable` ne contient **pas** `tva` ni `commentaire` → **perdus lors d'une édition manuelle** d'intervention (`update()` supprime puis recrée toutes les lignes).

---

## 6. Les bailleurs : la table de vérité

C'est le point central du métier. Trois codes cohabitent, ne pas les confondre.

| Clé dans `BAILLEUR_PATTERNS` | `code` renvoyé (= `code_bailleur` en BDD) | Bailleur | Règle de correction du code article (`corrigerReference`) | Fichier de règles |
|---|---|---|---|---|
| `COT01` | `COT01` | Cottage Social des Flandres | préfixe `C` | `cottage_rules.py` |
| `FLA04` | `FLA04` | Flandre Opale Habitat | préfixe `FOH`, et `IN0` inséré si le code n'a aucune lettre | `foh_rules.py` |
| `HAB01` | `HAB01` | Habitat du Nord | préfixe `H` | `hdn_rules.py` |
| `HAB04` | `HAB04` | HABILIANS *(fusion HAB01 + LOG01)* | **pas de code sur le bon** : rapprochement de la désignation avec les codes `HELEC%` | `habilians_rules.py` |

> 🚨 **HAB04 est bloqué à l'import Codial depuis le 15/09/2026** — Codial ne reconnaît pas ce code bailleur. Voir §8.
| `HHF01` | **`HAB02`** | Habitat Hauts de France | préfixe `HHF` | `hhf_rules.py` |
| `LMH01` | `LMH01` | LMH | **code inchangé** (⚠️ prix/quantité inversés dans les PDF) | `lmh_rules.py` |
| `LOG01` | `LOG01` | Logis Métropole | **pas de préfixe fixe** : recherche par similarité de désignation dans `intervention_imports` (passe 1 `ELEC2%`, fallback `E%`), seuil `similar_text ≥ 70 %`, préfixe `E` forcé | `logis_rules.py` |
| `LOG02` | `LOG02` | Logi FIM Vilogia | `LFV` + code privé de son 1er caractère | `logi_fim_rules.py` |
| `PART01` | **`PAR01`** | Partenord Habitat | préfixe `PM` (métropole) ou `PL` (littoral) selon `detection.is_littoral` | `partenord_rules.py` |

Points à retenir :
- **Si le code corrigé n'existe pas dans `intervention_imports`, la ligne est supprimée silencieusement**
  (seulement un `Log::warning` « Lignes ignorées »). C'est la cause n°1 d'un bon qui arrive « vide » dans Codial.
- **Aucun bailleur détecté ⇒ le détecteur renvoie `LOG01` par défaut** (`confidence: low`).
  Un bon inconnu est donc traité comme du Logis Métropole, pas rejeté. Toujours vérifier `detection.confidence` dans les logs.
- La distinction **littoral / métropole de Partenord** est faite dans `traitement.py`
  (liste `littoral_cities` appliquée aux 500 premiers caractères), **pas** par
  `BailleurDetector._resolve_partenord()` qui est du **code mort** — et les deux listes de villes divergent.
  Toute modification de la zone Partenord se fait dans `traitement.py`.

**HABILIANS = fusion d'HABITAT DU NORD (HAB01) et de LOGIS MÉTROPOLE (LOG01)**, confirmée par le client le
2026-09-11 et vérifiée sur les données : plus aucun bon HAB01 depuis le 11/05/2026, plus aucun LOG01 réel depuis
juin, et les bons HABILIANS démarrent le 06/07/2026. Sur les 27 gestionnaires relevés, 12 sont des contacts
HAB01 et 9 des contacts LOG01 — aucun ne vient d'un autre bailleur. Les contacts sont recopiés (pas déplacés)
vers HAB04 par la migration `2026_09_11_100000_add_habilians_bailleur.php`.

**Cas particulier — le groupe LDEV (HABILIANS et LOGIS MÉTROPOLE)** : leurs bons sont *textuellement
identiques* (même en-tête « ORDRE DE SERVICE », même « Fournisseur : 002161 », même marché, même ville
d'émission). Aucun mot-clé ne les sépare, et le mot « habilians » n'existe pas dans la couche texte : le logo
et le pied de page sont des images. La distinction se fait donc sur l'**empreinte SHA1 du logo**
(`bailleurs_detector.LOGO_SHA1`), vérifiée sans collision sur 166 bons de production. Repli si l'image est
illisible : le code « lieu » en début de ligne du tableau (`000…` pour HABILIANS, `615…` pour LOGIS), restreint
aux bons portant la signature LDEV pour ne pas polluer les autres bailleurs.

### Ajouter un bailleur — checklist complète

1. `scripts/bailleurs_detector.py` → nouvelle entrée dans `BAILLEUR_PATTERNS` (`keywords`, `logo_keywords`, `priority`, éventuel `supplier_code`).
2. `scripts/prompts/<nom>_rules.py` → constante `<NOM>_SPECIFIC_RULES`.
3. `scripts/prompts/__init__.py` → import + `__all__`.
4. `scripts/prompt_builder.py` → `BAILLEUR_RULES_MAP` + `BAILLEUR_NAMES`.
5. `app/Jobs/ProcessPdfFileJob.php` → branche dans `corrigerReference()`.
6. Table `bailleurs` : le code doit exister (via l'import XML Codial) sinon `NomBailleur` sort vide dans le XML.
7. Tester : `scripts/venv/bin/python3 scripts/traitement.py "/chemin/absolu/bon.pdf"` puis vérifier le XML produit.

---

## 7. Pièges connus (lire avant de débugger)

### 7.1 venv Python cassé
Le dossier `scripts/venv` a été commité par erreur dans l'historique, **sans les binaires Python et avec
des `.so` bloqués par macOS** (« library load disallowed by system policy »). Il est désormais dans
`.gitignore`. Symptôme : `traitement.py` échoue silencieusement (sortie vide, `status = error`).
Correctif : recréer le venv (§2). **Ne jamais copier un venv d'une machine à l'autre.**

### 7.2 Clé OpenAI
Une clé expirée renvoie un `401` et **aucun bon n'est traité** — statut `error`, rien ne part vers Codial.
Déjà arrivé le 2026-07-17. Se voit dans les `logs` du JSON à l'étape « Extraction des données avec prompt ».

### 7.3 Référence du bon de commande — le champ que Codial utilise pour rattacher

Codial rattache un bon à sa commande via le contenu de `<RefBc>`. Une référence erronée = bon jamais importé,
il reste indéfiniment dans `codial/import/`.

**Où est la vraie référence** : le numéro de commande de l'en-tête (`Commande n°I75639`, `Ordre de service n°…`),
court, sans espaces. **Jamais** la ligne d'objet des travaux (`GRC/071093/ REMISE EN SERVICE`), ni le numéro de
marché (`Marché n°027906-5-001 - 2024-0532`), ni une référence de patrimoine.

Le prompt générique ne donnait aucune règle pour ce champ jusqu'au 2026-08-24 : le modèle choisissait au jugé
selon la mise en page, d'où des erreurs **intermittentes** (35 bons Partenord sur 512). La règle est désormais
explicite dans `prompts/generic_prompt.py` (règle 4) et renforcée dans `prompts/partenord_rules.py`.

**Le fix « slash » de juillet traitait un symptôme.** Quand la référence erronée contenait un `/`, le fichier
atterrissait dans un sous-dossier (`import/LOG02_FIM/`) que Codial ne balaie pas. `importBasename()` neutralise
toujours les slashs dans le **nom de fichier** (le contenu XML garde la référence exacte) — c'est un garde-fou
utile, à conserver, mais la vraie question à se poser devant un bon non importé est : **la RefBc est-elle la
bonne ?**

### 7.4 Piège de détection : le code fournisseur `002161`
`002161` est le `supplier_code` de LOG01 (bonus +15 → confiance `high`). Il apparaît aussi en pied de page
de bons d'autres marques du groupe (cf. **HABILIANS**, ticket ouvert). Tout nouveau bailleur du groupe LDEV
sera détecté LOG01 s'il n'a pas de règle qui gagne explicitement.

### 7.5 `generateAllXml()` produit un XML DIFFÉRENT
La route `/admin/interventions/xml/generate-all` génère un format **incompatible** avec `generateXml()`
(balises `RefBailleur`/`Date`/`DatePrevue`, pas d'attribut `PU`) et nomme les fichiers `{code}_{id}.xml`
au lieu de `{code}_{reference_bc}.xml`. C'est du legacy — **ne pas l'utiliser en production**, ou l'aligner d'abord.

### 7.6 Divers
- `job_delete` et `job_retry` refusent tout fichier dont le statut n'est pas `error` (réponse 400).
- Le matching du gestionnaire (`createFiles`) accepte un `similar_text > 50 %` : seuil très bas, faux positifs possibles.
- Deux migrations portent le même nom (`..._110526_` et `..._110535_add_intervention_id_to_pdf_files_table`) ; la première est vide.
- `exec()` est utilisé pour appeler Python : la sortie doit contenir **une ligne JSON complète sur une seule ligne**. Tout `print()` ajouté dans le Python qui ressemble à `{...}` peut casser le parsing.
- `Tva` vaut `2` par défaut dans le XML si la ligne n'en porte pas.

---

## 8. Travail en cours

### 8.0 État au 2026-09-22 — ce qui bloque aujourd'hui

**🚨 HABILIANS (HAB04) rejeté par Codial depuis le 15/09.** Signalé par le client les 15, 16, 17 et 21/09.
Erreur Codial sur chaque `HAB04_*.xml` :
`Bailleur:  Référence:  Un objet qui autorise la valeur Null doit posséder une valeur.`

**Ce n'est pas un bug de notre code**, diagnostic vérifié en prod :

| Vérification | Résultat |
|---|---|
| XML HAB04 produit (intervention 1944) | complet, **structurellement identique** à un COT01 qui passe |
| Logs Codial | fichiers OK → `Bailleur:COT01` ; fichiers HAB04 → `Bailleur:` **vide** |
| `codial/export/bailleurs.xml` (04/03/2026) | ne contient **ni HAB04 ni HABILIANS** |
| 112 contacts HAB04 en base | `created_at = 2026-09-11 10:14:01` → **notre** migration, jamais un export Codial |
| Dernier vrai import de contacts Codial | **2025-11-04** |

Codial échoue donc à résoudre le code bailleur. « HAB04 » n'a été confirmé qu'oralement par le client.
De plus, les `contact_id` émis (78, 75, 44, 1019…) sont ceux de HAB01/LOG01 : même HAB04 créé, la
résolution échouera s'ils n'y sont pas rattachés. **Mail envoyé à Pierre (Codial) le 2026-09-22** :
confirmer le code exact + fournir un export récent. **19 bons HAB04 en attente, tous rejouables.**

**⚠️ Les référentiels Codial ne sont plus reçus depuis mars** (`bailleurs.xml` 04/03, `articles.xml` 24/03),
alors qu'ils devraient arriver quotidiennement. **L'import est 100 % manuel** : routes
`/admin/codial/import-bailleurs` et `/admin/codial/import-interventions`, aucun automatisme
(le crontab prod ne lance que `schedule:run`). Piège : `BailleurImportController::import()` fait
`contacts()->delete()` pour chaque bailleur **présent dans le XML** — réimporter un export contenant
HAB04 écrasera les 112 contacts recopiés (c'est voulu, mais à savoir).

**Bons d'intervention créés VIDES dans Codial** (BI 45524 et 45537) : le bon existe, le PDF est en GED,
mais la fiche est vide. Cause identifiée sur le cas LMH01 `F26592` → `CodeGestionnaire=73` = DUCROS
Florian, contact **PAR01**, émis sur un bon **LMH01**. Codial résout le bailleur, échoue sur le
gestionnaire, crée une coquille. C'est le défaut §9/anomalie « gestionnaires croisés », désormais à
**505 bons sur 1845 (27 %)** contre 291/1573 en août. Correction proposée au client (restreindre
`similar_text` aux contacts du bailleur du bon) : **en attente de son accord**, elle touche les 9 bailleurs.
**Contre-exemple non élucidé** : le bon PAR01 `I86766` (BI 45537) est vide alors que son gestionnaire 775
est bien rattaché à PAR01 et son XML valide → au moins une seconde cause, non identifiée.

**✅ Corrigé le 2026-09-22 (commit `4641eb1`, en prod)** : `traitement.py` insérait le `code_bailleur`
issu de la **réponse JSON d'OpenAI** au lieu du résultat de `BailleurDetector`, dont le travail ne servait
qu'à choisir le prompt. L'IA pouvait donc écrire n'importe quel code. Constaté en prod : **5 bons partis
avec `code_bailleur = "FIM"`** (inexistant dans Codial, rejetés) au lieu de `LOG02` — d'autant plus
probable que `logi_fim_rules.py` annonce « Format: LOG02 **ou FIM** ». `insert_data_to_db()` reçoit
désormais `bailleur_code` en paramètre. **Reste à faire : repasser les 5 interventions FIM
(1695, 1713, 1714, 1728, 1883) en `LOG02` et régénérer leurs XML.**

### 8.1 Remise à plat du dépôt (au 2026-08-24)

**Le dépôt a été remis à plat le 2026-08-24** : tout ce qui tournait en production sans être versionné
est désormais commité et poussé sur `main` (voir §9 pour le workflow de déploiement). L'historique repart
de `1495b9f` avec six commits : sortie des artefacts runtime, refactoring Python, fix Codial, suppression
du code mort, migrations, documentation.

**Dette identifiée, non traitée :**

- **`develop` porte 3 commits jamais mergés dans `main`** (`4e16ad8` ordre littoral/métropole Partenord,
  `2c3985b` champs manquants dans l'export XML + `code_gestionnaire` mis à jour au store/update, et leur merge).
  Ils touchent `InterventionController.php` et `traitement.py` — les mêmes fichiers que le travail actuel — et
  `develop` **ne contient pas** le refactoring Python. C'est une ligne de travail plus ancienne : la réconciliation
  demande une revue ligne à ligne, elle n'a volontairement pas été faite pour ne pas risquer une régression en production.
- **Migrations non alignées sur le schéma réel** (§9, anomalie 4) : à traiter avant toute base neuve.
- **Perte silencieuse de lignes** (§9, anomalie 2) : 37 interventions sans aucune ligne.
- **21 bons Partenord bloqués dans `codial/import/`** (§9, anomalie 1) : à valider avec le client.
- **Intégration du bailleur HABILIANS** (marque du groupe LDEV / Logis Métropole) :
  - BPU = codes `HELEC…` (175 codes déjà présents dans `intervention_imports`).
  - Détection : le mot-clé « habilians » doit **gagner sur LOG01** malgré le `002161` en pied de page (§7.4).
  - Correction : branche calquée sur LOG01 mais ciblant `code LIKE 'HELEC%'`. Matchs vérifiés : DCL→HELEC2062,
    détecteur→HELEC2067, recherche panne→HELEC2166, déplacement→HELEC2167, interphonie→HELEC2173.
  - **Bloqué côté client** : obtenir (1) le `CodeBailleur` attendu par Codial pour HABILIANS, (2) la liste des
    gestionnaires + leurs codes Codial (vus : DUBOIS PIERRE, DUMAZY CHRISTELLE, PLUQUET VALENTIN, LAURENT LENDZION).

`EMAIL_PROPOSITION_TESTS.md` (racine) = brouillon de mail client présentant la refonte et proposant une phase de tests sur site.

---

## 9. Production — état réel du serveur (vérifié le 2026-08-24)

**Accès** : `ssh rd-dev` (alias identique : `map-dev`) → hôte n0c `hc-gladlyfittarpon-eu.n0c.com`,
user `sppyxfzk`, port `5022`, clé `~/.ssh/map-dev`. Application dans `~/dev/flash`.
Site : **https://flash.dev-rd.com/**

| | Local | Production |
|---|---|---|
| `APP_ENV` / `APP_DEBUG` | `local` / `true` | `production` / `false` |
| BDD | `flash` (MAMP, port 8889) | `sppyxfzk_flash` (port 3306) |
| `QUEUE_CONNECTION` | `sync` | **`database`** (worker lancé par cron) |
| PHP | 8.2 | **8.3.31** |
| `PYTHON_PATH` | `scripts/venv/bin/python3` | **`/home/sppyxfzk/virtualenv/python-app/3.13/bin/python3`** (Python 3.13.14) |

**⚠️ Le venv Python n'est pas au même endroit en prod.** `scripts/venv` **n'existe plus** sur le serveur
(les résidus du venv commité par erreur ont été déplacés dans `~/backups/`). La prod utilise un virtualenv
cPanel séparé, `~/virtualenv/python-app/3.13/`, qui fonctionne (`imports OK` vérifié).
**Ne jamais pointer `PYTHON_PATH` vers `scripts/venv` en prod.**

**Cron actif** (`crontab -l`) — c'est ce qui fait tourner la queue :
```
* * * * * /usr/bin/php /home/sppyxfzk/dev/flash/artisan schedule:run >> /dev/null 2>&1
```

### Déploiement : local → git → pull → prod

Le dépôt manuel par `scp` a été abandonné le 2026-08-24. La production suit désormais `origin/main` et
**son working tree doit rester propre** (`git status` vide) : c'est ce qui garantit qu'un `git pull` ne
peut rien écraser.

```bash
# 1. En local : committer et pousser sur main
git push origin main

# 2. Sur le serveur
ssh rd-dev
cd ~/dev/flash
git status --short        # DOIT être vide avant de pull
git pull origin main
php artisan view:clear && php artisan config:clear && php artisan route:clear
```

Règles à respecter pour que ce workflow tienne :
- **Ne jamais éditer un fichier directement sur le serveur.** Toute correction passe par un commit local.
- `public/pdfs/**` et `scripts/venv` sont **hors dépôt** (données runtime et environnement machine).
  Un `git pull` ne peut donc plus toucher aux 564 Mo de PDF ni au venv.
- `.env` est ignoré : celui du serveur n'est jamais écrasé (sauvegardes dans `~/backups/`).
- Si une migration est ajoutée, la lancer explicitement — et lire d'abord l'anomalie 4 ci-dessous.

En cas de problème, sauvegardes du 2026-08-24 sur le serveur : `~/backups/flash-code-*.tgz`,
`~/backups/flash-env-*.bak`, `~/backups/scripts-venv-mort-*`.

### Chiffres de production

- `pdf_files` : **877 done**, 1 error, 1 bloqué en `processing` depuis le 2026-04-14 (`1776169556-LMH_F04539.pdf`)
- `interventions` : **1580** · `intervention_lignes` : 3070 · `intervention_imports` (BPU) : **29 359** · `bailleurs` : 9
- Répartition : PAR01 512 · LOG01 274 · COT01 240 · LMH01 166 · FLA04 139 · HAB02 136 · HAB01 72 · LOG02 41
- `jobs` en attente : 0 · `failed_jobs` : 20 (les 2 plus récents = `TimeoutExceededException` / `MaxAttemptsExceeded` du 2026-04-14)
- `public/pdfs` : **564 Mo**, 1070 fichiers dans `processed/`, **236 PDF orphelins à la racine**
- `storage/logs/laravel.log` : **17 Mo, 154 678 lignes, jamais tourné** (`LOG_STACK=single` + `LOG_LEVEL=debug` en prod)

### Anomalies constatées en production

1. ~~**21 bons Partenord bloqués dans `codial/import/`**~~ — **résolu le 2026-08-24.**
   Cause : `<RefBc>` contenait la ligne d'objet des travaux au lieu du numéro de commande (§7.3). Codial ne
   pouvait rattacher ces bons à aucune commande. Correctif appliqué : règle explicite dans le prompt, puis
   rattrapage des 21 bons (numéro ré-extrait des PDF par regex, `reference_bc` corrigée, XML+PDF régénérés en
   `PAR01_I75639.*`, anciennes paires supprimées). Vérifié : 21 paires, toutes les `RefBc` conformes.
   **Reste à traiter** : 24 autres interventions Partenord ont encore une référence non conforme (numéro de
   marché `027896-5-001`, numéro de patrimoine seul, etc.) mais **n'ont plus de fichier dans `import/`**.
   12 sont récupérables — leur PDF source est dans `processed/` et le nom d'upload porte le bon numéro
   (ex. `1786707071-PART I78945.pdf`). À réimporter seulement si le client le demande.
   **À signaler aussi** : 50 groupes d'interventions Partenord partagent la même référence, c'est-à-dire le
   **même bon uploadé plusieurs fois** (jusqu'à 4 fois). Sans incidence sur Codial — un seul fichier est produit —
   mais cela gonfle la base et mérite un garde-fou à l'upload.

2. **Perte silencieuse de lignes** : 45 `WARNING "Lignes ignorées"` dans le log, dont **21 sur le seul mois d'août 2026**,
   et **37 interventions sur 1580 n'ont aucune ligne**. Cause : `corrigerReference()` supprime toute ligne dont le
   code corrigé est absent de `intervention_imports` (§6). C'est le défaut fonctionnel n°1 à traiter.

3. **`created_at` / `updated_at` sont NULL sur les 1580 interventions.** Le script Python fait un `INSERT` SQL brut
   sans renseigner les timestamps Laravel. Impossible de dater une intervention autrement que via `pdf_files`.

4. **Les migrations ne sont pas jouées en prod** — le schéma réel diverge du dossier `database/migrations/` :
   - présentes en base mais dans aucune migration : `interventions.code_gestionnaire` (varchar 100),
     `intervention_lignes.tva` (int), `intervention_lignes.commentaire` (text)
   - `interventions.pdf` est en `varchar(500)` en prod contre `varchar(255)` dans la migration
   - **`interventions.lieu_nom` n'existe PAS en prod** alors que la migration `2025_07_31_094044` l'ajoute et que
     le champ est dans `Intervention::$fillable`
   → Ne **jamais** lancer `php artisan migrate` sur la prod sans avoir d'abord écrit une migration de rattrapage
   alignée sur le schéma réel, et sans sauvegarde.

5. Le dernier bon traité date du **2026-08-24 09:55** (`1787558114-PART I80426.pdf`, statut `done`) — le système tourne.

---

## 10. Où chercher quand…

| Symptôme | Où regarder |
|---|---|
| « Ça ne bascule pas dans Codial » | 1) clé OpenAI valide ? 2) venv OK ? 3) `PYTHON_PATH` ? 4) fichiers bien **à plat** dans `import/` ? |
| Un bon passe en `error` | `/admin/interventions/suivi` → bouton logs ; puis `storage/logs/laravel.log` ; puis rejouer le PDF à la main avec `traitement.py` |
| Le bon arrive sans lignes | `corrigerReference()` a supprimé les lignes : chercher « Lignes ignorées » dans `laravel.log`, vérifier que le code corrigé existe dans `intervention_imports` |
| Mauvais bailleur détecté | `detection.confidence` / `matches` dans les logs du job ; ajuster `BAILLEUR_PATTERNS` |
| Données mal extraites (prix, adresse, tél.) | `scripts/prompts/<bailleur>_rules.py` — c'est là que se règlent les particularités de format |
| Le XML sort mal formé / champ vide | `InterventionController::generateXml()` + vérifier que le `code_bailleur` existe dans la table `bailleurs` |

---

*Le Réservoir Digital — Mehdi (mehdi.boukhalfa@reservoir-digital.fr)*
