# Architecture — smartcentraldeveloppement

> Document généré par exploration automatisée du code le 2026-08-18, **rafraîchi le 2026-09-07** (intégration Massia, refonte du module `bl` en onglets, routes `devis_laravel`/`formule_modification_groupee`), **puis le 2026-09-09** (migrations de données pour `t_norme_contrainte` et `t_code_norme`, voir §4 et §8), **puis le 2026-09-29** (checksum sur le transfert des formules, voir §6sexies ; correctif des routes manquantes + ajout d'un mode « ajout produit/dosage » avec droit dédié sur la modification groupée de formule, voir §6septies). Objectif : servir de carte de référence pour naviguer le projet sans avoir à tout relire à chaque demande de modification. À mettre à jour périodiquement (le code évolue vite, ~160 migrations en ~1 an).
>
> **Portée de l'analyse** : exploration exhaustive de la structure des dossiers (tous les fichiers listés), lecture approfondie d'un échantillon représentatif (~65 fichiers : contrôleurs les plus gros et les plus petits, modèles, migrations, config, middlewares, providers, services, traits, vues, + le module Massia ajouté le 2026-09-07). Les généralisations sur les ~140 contrôleurs et ~150 modèles s'appuient sur cet échantillon et sur la convention de nommage très régulière observée — à affiner si un module précis s'avère être une exception.

---

## 1. Vue d'ensemble

**smartcentraldeveloppement** est un ERP métier pour **centrales à béton** (production, logistique, commande, facturation, qualité/traçabilité, conformité normative béton NF EN 206). C'est une application Laravel monolithique, développée et enrichie en continu depuis plusieurs années (traces d'un existant PHP procédural progressivement porté vers Laravel).

**Stack technique :**

| Couche | Techno |
|---|---|
| Framework | Laravel **10.10** (PHP ^8.1) |
| Auth web | Laravel **Jetstream 4.3** + **Fortify** (session, 2FA activé) |
| Auth API | Laravel **Sanctum 3.3** (tokens Bearer + "abilities") |
| Front-end module métier | Blade + **jQuery/Ajax** + **DataTables** (`yajra/laravel-datatables-oracle`) |
| Front-end socle Jetstream | Blade components + **Livewire 3** + Alpine.js |
| Build assets | **Vite** + **Tailwind 3** |
| Base de données | MySQL (via `doctrine/dbal`), driver session/queue `database` |
| Génération documents | `phpoffice/phpspreadsheet`, `phpoffice/phpword`, `maatwebsite/excel`, `setasign/fpdf`+`fpdi`, `spipu/html2pdf`, `mikehaertl/php-pdftk` |
| Reporting | `koolreport/core` + `koolreport/laravel` |
| Fichiers distants | `league/flysystem-ftp`, `league/flysystem-sftp-v3`, `phpseclib/phpseclib` |
| Qualité code (dev) | `larastan/larastan` (PHPStan), `laravel/pint`, `barryvdh/laravel-ide-helper`, `laravel-debugbar` |

---

## 2. Structure des dossiers

```
app/
├── Actions/        → Fortify/Jetstream (scaffolding standard, peu touché)
├── Console/
│   ├── Commands/   → tâches planifiées (export tarifs, récupération FTP...)
│   └── Kernel.php
├── Exceptions/
├── Exports/        → exports Excel (formules, pesées) via Maatwebsite/PhpSpreadsheet
├── fonctions/       → ⚠️ helpers procéduraux "legacy" (voir §7)
├── Http/
│   ├── Controllers/ → ~140 contrôleurs, 1 par module métier
│   └── Middleware/   → middlewares standards Laravel + 3 middlewares custom (voir §5)
├── Jobs/            → envoi de mails asynchrones (queue)
├── Listeners/        → hooks sur login/logout, échec d'envoi de mail
├── Mail/             → Mailables (commande, facture, mailing, 2FA...) ; trait `envoi_mail_fonction` (`ConfirmReadingTo()` = accusé de lecture façon PHPMailer)
├── Models/           → ~150 modèles, 1 par table `t_xxx`
├── Providers/
├── Reports/          → rapports KoolReport (ex. chiffre d'affaires par société)
├── Services/         → logique métier orientée objet (tarification, PEPPOL, mailing...)
├── Traits/            → ⚠️ traits utilisés comme "actions de contrôleur" (voir §7)
└── View/Components/  → composants Blade Jetstream (AppLayout, GuestLayout)

routes/
├── api.php    → routes API (Sanctum), petit fichier, peu de groupes
├── web.php    → routes web, fichier UNIQUE de ~2200 lignes pour tous les modules
├── console.php
└── channels.php

resources/
├── views/     → ~140 sous-dossiers (1 par module), pattern index.blade.php + form.blade.php
│   ├── fiche_annexe/    → mini-tableaux réutilisables en onglets (le seul vrai composant partagé)
│   ├── doc_utilisateur/ → documentation utilisateur intégrée, par module (bonne pratique rare)
│   └── components/       → composants Jetstream par défaut
├── js/ et css/  → très légers, l'essentiel du JS métier est inline dans les vues Blade
└── lang/         → fr/en

database/
├── migrations/  → ~160 fichiers, convention de nommage mixte (voir §6)
└── seeders/      → DatabaseSeeder par défaut uniquement

tests/
├── Feature/  → tests Jetstream par défaut + quelques modules métier (Ajout, Chauffeur,
│               Facture, Produit, Service, Zone, SocieteMailing)
│             + ApiTransfert/ : 1 test par fiche de /transfert/{fiche} (voir §6)
└── Unit/     → quasiment vide (ExampleTest uniquement)
```

---

## 3. Cartographie des modules métier

Les contrôleurs/modèles/vues suivent presque tous la convention `<domaine>_controller.php` / `<domaine>.php` (modèle) / `views/<domaine>/`. Voici le regroupement par grand domaine fonctionnel (utile pour savoir "où chercher") :

**Commercial / tiers**
client, contact, commercial, categorie_client, famille_client, sous_famille_client, typologie_client, region_commerciale, secteur, concurrent, adresse_facturation, banque, mode_reg, echeance, assurance_credit, categorie_assurance_credit, position_compte, commentaire_stocker, devis.

**Chantiers**
chantier, planning, planning_commun, planning_prev, ouvrage, type_ouvrage, ventilation, zone.

**Formulation béton (production)**
formule, formule_variant, formule_producteur, composition, classe_consistance, classe_exposition, classe_resistance, classe_chlorure, classe_reduction_prg, designation_beton, type_beton, code_norme, norme, norme_contrainte, specificite, nature_addition, reglage_trappe, groupe_centrale_formule, famille_beton_echantillon, gwr, ajout, article_associe, formule_modification_groupee (remplacement/ajout groupé de produit sur plusieurs formules, voir §6septies).

> `norme_contrainte` (table `t_norme_contrainte`) porte les valeurs limites par classe d'exposition (rapport Eeff/liant, classe de résistance mini, et surtout le rapport maximal Addition/(ciment+Addition) par type de ciment/addition) issues du tableau **NA.F.1 du PR NF EN 206** — consommée en lecture par `composition_calcul_trait::calculer_ratio_max_addition()` (nom indicatif, voir le trait). `code_norme` (table `t_code_norme`) est en réalité une **simple table de vocabulaire contrôlé** sans clé étrangère (colonnes `classe_resistance`/`classe_exposition`/`type_ciment`/`type_addition` lues indépendamment via `->distinct()` pour peupler des listes déroulantes "Code norme") — elle n'est pas liée à `norme` (`t_norme`, actuellement **0 ligne**) ni à `norme_contrainte` malgré la proximité de nom.

**Centrales & parc matériel**
centrale, chauffeur, vehicule, type_vehicule, gamme_vidange, moyen_dechargement, produit, produit_centrale, produit_transport, fournisseur, typologie_article, famille_article, entree_matiere, energie, trajet, transporteur.

**Commandes / logistique / livraison**
commande, ligne_commande, bl, ligne_bl, pesee, depart_cycle, suivi_numero_bl, reservation_pompe, info_commande, transfert_vers_production, suivi_commande, api_transfert (API). Voir aussi devis (module `devis_laravel`, routes dédiées) qui se transforme en commande/facture/BL.

> ⚠️ **`resources/views/bl/*` est actuellement non commité (untracked)** alors que `bl_controller.php` (tracké) en dépend directement (`view('bl.index')`, `view('bl.form')`...). Le module `bl` a été **récemment décomposé en onglets** (`bl_onglet_general`, `bl_onglet_divers`, `bl_onglet_heures`, `bl_onglet_incident`, `bl_onglet_demat`, `bl_onglet_adresse_facturation/livraison`, `bl_onglet_observation`) — première application concrète de la recommandation §9 étape 3.12 (factoriser les formulaires monolithiques à la manière de `fiche_annexe/`). À committer avant toute synchronisation/déploiement pour ne pas perdre ce travail.

**Facturation**
facture, ligne_facture, pre_facture, ligne_pre_facture, relever_facture, operation_facture, reglement, tva, groupe_tarif, groupe_centrale_tarif, groupe_societe_tarif, tarif, decalage_facturation, motif_avoir, export_facture_electronique (PEPPOL), export_comptabilite.

**Qualité / traçabilité**
tracabilite, tracabilite_composition, tracabilite_envoi, incident, gravite_incident, famille_incident, sous_famille_incident, nature_incident, type_attaque, type_exposition, type_finition.

**Administration / paramétrage**
parametre, profil, utilisateur, societe, civilite, pays, commune, domaine, unite, motif_action, motif_absence, motif_echec_reussite, type_action, type_demande_action, demande_action, message_demande_action, action, activite, fonction.

**Communication**
envoi_mail, envoi_sms, message_alarme_mails, mailing, mailing_template, mailing_history.

**Intégrations externes / imports / IA**
importation_externe, migration_client_centrale, migration_utilisateur, trans_externe, gpt (appel IA externe), etat_stock, cbao.

**Outils internes / dev**
test_copier_fichier_ftp, test_excel, test_impression, test_trans_externe, statistique, cocher_fiche_centrale, modele_impression, modele_qr_code.

---

## 4. Conventions de code observées

- **Contrôleurs** : `<domaine>_controller.php`, `extends BaseController`. Méthodes standard récurrentes : `index()` (vue liste), `data_table()` (endpoint JSON pour DataTables), `afficher_formulaire()` (retourne le HTML du form, chargé en Ajax), `enregistrer()` (create/update), `supprimer()`, `<domaine>_recherche()`. `BaseController` charge `$this->user` et injecte plusieurs helpers de `App\fonctions` + traits métier, mais contient peu de logique propre.
- **Modèles** : `<domaine>.php`, `extends BaseModel` (sauf `utilisateur`/`User` qui restent proches d'Eloquent standard pour Jetstream/Sanctum). Convention systématique `$table = 't_xxx'`, `$primaryKey = 'id_xxx'` (PK non standard), `$guarded = ['created_at','updated_at']` (pas de `$fillable`). Relations **toutes préfixées `lier_`** (`lier_client()`, `lier_centrale()`...) — convention maison, pas du Eloquent idiomatique. `BaseModel` ajoute un hook `saving` qui interroge `SHOW COLUMNS` pour poser des valeurs par défaut par type — mécanisme non standard.
- **Tables** : préfixe `t_` systématique (`t_client`, `t_commande`, `t_formule`...) sauf les tables du socle framework (`users`, `personal_access_tokens`).
- **Vues** : `views/<domaine>/index.blade.php` (grille DataTables serveur) + `form.blade.php` (formulaire chargé en Ajax et injecté dans une `div`, sans rechargement de page). `fiche_annexe/table_xxx_fiche_annexe.blade.php` = mini-composant générique réutilisé en onglet sur plusieurs fiches (client, chantier...) — seul vrai mécanisme de réutilisation de vue identifié.
- **Migrations** : deux formats coexistent — le format standard Laravel (`YYYY_MM_DD_HHMMSS_nom.php`) pour l'historique ancien/framework, et un format maison récent `YYYY_MM_DD_HHmm_V<version>_<INITIALES>_<description>.php` (ex. `2025_08_12_0920_V900_NB_create_t_reglage_trappe.php`) qui trace la version de release et l'auteur directement dans le nom de fichier.
- **Migrations de données** (pas seulement de schéma) : pattern récurrent observé sur plusieurs migrations (`2026_03_23_1330_V904000_BAM_t_client_assurance.php`, `2026_07_29_1410_V905000_GC_t_formule_variant_active.php`, et les deux ajoutées le 2026-09 pour `t_norme_contrainte`/`t_code_norme`) : `Schema::hasTable()`/`Schema::hasColumn()` en garde avant un `Schema::create`/`table`, puis `DB::table(...)->updateOrInsert(...)` ou `->insert()` conditionné (table vide / valeur absente) pour rendre la migration rejouable sans doublon sur un environnement où les données existent déjà. Il n'existe **aucun seeder** dans `database/seeders/` (`DatabaseSeeder` par défaut, vide) — toutes les données de référence "métier" sont poussées via ce type de migration plutôt que via des seeders Laravel classiques.

---

## 5. Authentification & sécurité

- **Web** : Jetstream + Fortify, guard `session`, 2FA activable par utilisateur (`TwoFactorAuthenticatable`), rate limiters natifs sur `login` (5/min) et `two-factor` (5/min). Authentification custom (`FortifyServiceProvider`) : recherche par `email`, `name` OU `code_auto` ; migration legacy des mots de passe SHA1 → bcrypt à la première connexion réussie (`user_ancien_mdp`).
- **API** : Laravel Sanctum, tokens personnels (`HasApiTokens` sur `User`), système d'**abilities** natif Sanctum exploité via le middleware custom `CheckTokenAbility` (alias `ability`, ex. `ability:read`).
- **Middlewares custom** :
  - `CheckUserInterne` (alias `interne`) : bloque l'accès si `user_type != 0` (distingue utilisateurs internes/externes), redirige vers `/bl` sinon.
  - `VerifierSessionUtilisateur` (groupe `web`) : recopie les attributs de l'utilisateur connecté dans `$_SESSION` PHP natif — pont vers du code legacy hors cycle de vie Laravel.
  - `CheckTokenAbility` (alias `ability`) : vérifie `tokenCan()` côté Sanctum.
- **`routes/api.php` (état actuel, mis à jour ce jour)** : `/user` et le groupe `v1/classe_consistance` sont sous `auth:sanctum` ; le groupe `/transfert` vient d'être protégé par `auth:sanctum` également (voir historique de conversation) ; `/centrale_connectee` reste **sans middleware d'authentification** — à examiner si ce point d'entrée doit rester public. `POST /connexion_centrale` (ajouté 2026-09-16, sous `auth:sanctum`) : check-in envoyé par l'automate/gestion d'une centrale avec ses versions logicielles (`centrale_controller::connexion_centrale()`) — réutilise les colonnes `centrale_version_bhp_gestion`/`centrale_version_bhp_process`/`centrale_version_prog_automate` déjà existantes sur `t_centrale` (pas de nouvelle migration) et met à jour `centrale_heure_derniere_communication` à `now()`. Payload en liste d'objets `{centrale_code, version_gestion, version_process, version_plc, user_bhp}`, une réponse par centrale dans le même ordre — même convention que `/transfert/{fiche}`.
- **Points d'attention sécurité relevés pendant le scan (à traiter, voir §8) :**
  1. **Clé API OpenAI en clair dans `gpt_controller.php`** — à révoquer/rotater immédiatement et déplacer en variable d'environnement.
  2. **Mots de passe FTP/SFTP/Trakkeo en valeur littérale par défaut** dans `config/filesystems.php` (`env('FTP_PASSWORD', 'valeur réelle')`) — mêmes recommandations.
  3. CORS totalement ouvert sur `api/*` (`allowed_origins/methods/headers => ['*']`).
  4. CSRF désactivé sur plusieurs routes à effet de bord (téléchargement PDF, envoi de mail, validation de transfert production).
  5. Dans `routes/web.php`, plusieurs groupes (chantier, commande, facture) n'appliquent le middleware d'auth qu'à la route `index`, laissant les routes d'écriture/suppression reposer sur des vérifications ad hoc dans le code des contrôleurs plutôt que sur le routeur — exactement le même type de trou que celui corrigé aujourd'hui sur `/transfert` dans `api.php`.
  6. `$guarded` limité à 2 colonnes sur des modèles sensibles (`facture`, `client`, `commande`) = mass assignment quasiment ouvert.
  7. `SESSION_SECURE_COOKIE` non forcé à `true`.

---

## 6. Intégrations externes

| Intégration | Où | Nature |
|---|---|---|
| Automate de centrale (temps réel) | `app/Traits/envoyer_socket.php` | Socket TCP brut (`socket_create`/`socket_connect`), requête/réponse synchrone vers `IP_AUTOMATE:PORT_AUTOMATE` — pas de WebSocket web, protocole propriétaire point-à-point |
| Automate de centrale (fichiers) | `app/Traits/copier_fichier_sftp.php` + `Console/Commands/tache_recuperer_fichier_xml_ftp.php` | Récupération SFTP de paires `.dat`/`.flg`, suppression distante après copie. ⚠️ La commande planifiée censée orchestrer ce flux est **vide** (juste un `Log::info`) — flux probablement non opérationnel actuellement, à vérifier |
| Facturation électronique | `app/Services/generer_peppol_bis3_xml_service.php` + `generer_data_peppol_par_facture_service.php` | Génération XML conforme **PEPPOL BIS3 / UBL 2.1**, mapping tolérant aux champs manquants. Pas de validation XSD/Schematron visible dans ce service |
| IA générative | `app/Http/Controllers/gpt_controller.php` | Appel à l'API OpenAI — ⚠️ clé en dur, voir §5 |
| Mail | Mailgun / Postmark / SES / SMTP selon environnement | Config dans `config/mail.php` + `services.php` |
| Fichiers plats clients externes | `app/Services/structure_client_externe_service.php` | Parsing de fichiers à largeur fixe |
| Trakkeo (télématique véhicules ?) | disque `ftp_trakkeo` dans `config/filesystems.php` | FTP dédié |
| Broadcasting | Pusher configuré (`config/broadcasting.php`) | Présence de config, usage réel non confirmé dans l'échantillon |
| Reporting | KoolReport (`koolreport/core`, `koolreport/laravel`, `koolreport/amazing`, `koolreport/datagrid`, `koolreport/printpdf`, `koolreport/excel`) | Voir §6bis — module `statistique_laravel`, portage complet de l'ancien système `public/statistique/*` |
| MASSIA (logiciel qualité béton, éditeur Arcade) | `app/Services/Massia/`, `app/Http/Controllers/Massia/` | Voir §6ter — échange bidirectionnel BL (export) / Formule (import), **ajouté le 2026-09-07, pas encore branché** (aucune route, config ou migration) |
| Impression PDF (SumatraPDF) | `app/Services/impression_pdf_service.php`, `app/Http/Controllers/impression_pdf_controller.php` | Voir §6quater — impression du BL dématérialisé à partir d'un `bl_no_exec`, exposée en API, **ajouté le 2026-09-11** |
| Programme centrale (transferts de données) | `public/trans/action_trans_*.php` (scripts PHP procéduraux hors Laravel, via `public/inc/init.inc.php`) | Échange par requêtes GET, paramètres encodés en JSON (`_deserialize()`), `action=integration` (centrale → web) / `action=extraction` (web → centrale), file d'envoi `t_tracabilite_envoi`. Les formules sont contrôlées par checksum depuis le 2026-09-29, voir §6sexies |

---

## 6bis. Module statistiques (`statistique_laravel`)

Portage Laravel complet de l'ancien système de rapports PHP procédural `public/statistique/*` (piloté historiquement par `public/statistique/index.php` + un rapport par sous-dossier). **44 statistiques migrées** (toutes celles du menu `index.php`, plus les dossiers orphelins présents dans `public/statistique/` mais non reliés au menu). L'ancien système reste en place tel quel dans `public/statistique/` (non supprimé).

**Architecture** :
- `app/Http/Controllers/statistique_laravel_controller.php` : contrôleur unique avec un **registre** `slug => classe de rapport` (méthode `registry()`) et des routes génériques :
  - `GET /statistique_laravel/` → page maître de filtres (`index()`, vue `resources/views/statistique/index.blade.php`, reprend le formulaire de l'ancien `index.php` : dates, société/centrale/client/chantier/formule/..., affichage dynamique des critères pertinents par stat via `criteriaMap` en JS).
  - `GET /statistique_laravel/chantiers_par_client` : endpoint JSON pour la liste déroulante chantier dépendante du client choisi.
  - `GET /statistique_laravel/{slug}` → `show()` : rend la statistique en HTML (widget `\koolreport\datagrid\DataTables`).
  - `GET /statistique_laravel/{slug}/pdf` → `pdf()` : export PDF.
  - `GET /statistique_laravel/{slug}/excel` → `excel()` : export Excel.
  - ⚠️ Les recherches "avancées" en popup du formulaire legacy (`public/statistique/recherche_generale/*`) n'ont **pas** été portées ; remplacées par de simples listes déroulantes.
- Chaque statistique vit dans `app/Reports/<slug>/<slug>.php` (classe `extends App\Reports\Support\BaseReport`) + `<slug>.view.php` (web) + `<slug>.pdf.view.php` + `<slug>.excel.view.php`, sur le modèle de `chiffre_affaire_par_societe`/`bl_service_par_societe_centrale` (rapports d'origine, conservés tels quels, non ré-enregistrés dans le nouveau registre pour éviter les collisions de slug).

**Classes partagées** (`app/Reports/Support/`) :
- `BaseReport` : mutualise les traits communs (source Laravel/Eloquent, thème `amazing`, export PDF, export Excel, filtres).
- `FilterableReport` : construit les clauses `WHERE` + tableau de bindings PDO nommés (`buildFilters()`, `dateRangeFilter()`, `societeOuCentraleFilter()`) — **tous les filtres utilisent des requêtes préparées**, contrairement au legacy qui interpolait les valeurs `$_GET` directement dans le SQL (injection SQL possible, corrigée dans ce portage).
- `PdfGroupedTable` : rendu HTML générique d'un tableau groupé à N niveaux pour mpdf (qui n'exécute pas le JS du widget web), avec sous-totaux par niveau.
- `ExcelGroupedTable` : construit la config `rowGroup` du widget `\koolreport\excel\Table` (regroupement + sommes natif côté Excel, pas de recalcul PHP).
- `FastPdfExportable` / `FastPdfHandler` : variante de `koolreport/printpdf` sans `autoScriptToLang`/`autoLangToFont` (gain mesuré modeste, ~9% sur un run moyenné — l'essentiel du temps de génération PDF vient de la mise en page mpdf elle-même sur des tableaux de plusieurs milliers de lignes).
- `ZoneFactureQuantite` / `LigneBlAggregation` : logique métier partagée (quantité facturée d'une ligne "zone" encodée dans `ligne_facture_liste_bl`, calcul du total TTC ligne à ligne d'un BL avec TVA par type d'article) — reprise à l'identique du legacy sur les rapports où elle apparaissait dupliquée.
- `PalmaresReport` : classement décroissant générique pour les rapports "Palmarès" (client/formule/formule M3), basé sur des tables `t_palmares_*` pré-agrégées par un autre processus (pas de filtre de date).

**Corrections apportées par rapport au legacy** (au fil de la migration, cf. commentaires dans chaque classe concernée) :
- Filtres passés en binding PDO au lieu d'être concaténés dans le SQL (faille d'injection).
- Plusieurs rapports lisaient un filtre "chantier" (ou "société") sans jamais l'appliquer à la requête (variable calculée puis oubliée) — filtre effectivement branché.
- `facture_chiffre_affaire_chantier` groupait en réalité par client (copie de `facture_chiffre_affaire_client` jamais adaptée) — corrigé pour grouper par chantier.
- `facture_chiffre_affaire_formule` n'appliquait pas le flip de signe sur avoir (incohérent avec les rapports similaires) — corrigé.
- Bug de famille d'article : plusieurs rapports vérifiaient `famille_service_id` quel que soit le type de ligne (service/ajout/adjuvant/zone/formule n'ont pourtant pas tous cette colonne) — corrigé en résolvant la bonne colonne de famille selon le type réel.
- Date de fin avec un `$` littéral en trop dans une comparaison SQL (`bl_societe_centrale_formule_normee`) — corrigé.
- `journal_comptable`/`journal_vente` lisaient `date_debut`/`date_fin` sans jamais filtrer dessus — filtre de date ajouté sur `facture_date`.

**Limites connues** : le mécanisme d'export PDF du rapport `bl_service` legacy (`koolreport/export` + Chrome/Node headless) n'a pas été repris — ni Chrome ni Node ne sont installés sur cette machine de dev. Tous les nouveaux exports PDF utilisent `koolreport/printpdf` (mpdf, 100% PHP), avec un rendu HTML recalculé côté PHP (le widget web `DataTables` étant du JS, non exécutable par mpdf). Les packages premium KoolReport (`amazing`, `datagrid`, `printpdf`, `excel`) sont installés depuis le dépôt Composer privé `https://repo.koolreport.com` (identifiants dans `auth.json`, gitignored, non commité).

---

## 6ter. Intégration MASSIA (`app/Services/Massia/`, `app/Http/Controllers/Massia/`) — en cours, non branchée

Échange bidirectionnel avec **MASSIA**, le logiciel qualité/formulation béton de l'éditeur Arcade utilisé en centrale, ajouté le 2026-09-07 et **absent de tout autre point d'entrée du code à ce jour** (pas de route déclarée dans `web.php`/`api.php`, pas de clé `services.massia` dans `config/services.php`, pas de colonne `bl_exporte` en migration bien qu'utilisée par le code). Traiter ce module comme un **chantier ouvert**, pas encore fonctionnel en l'état.

**Sens sortant — export d'un BL vers Massia** :
- `App\Services\Massia\MassiaApiClient` : client HTTP bas niveau (`Illuminate\Support\Facades\Http`), authentification par en-tête `X-API-KEY` lu dans `config('services.massia.api_key')` (config à créer), timeout 30 s.
- `App\Services\Massia\BlMassiaMapper::mapper(bl $bl)` : construit le payload JSON `BatchProtocolDto` attendu par Massia à partir d'un `bl` (chantier, client, formule, gâchées/pesées groupées par `pesee_no_gachee`). Documente explicitement dans ses docblocks les champs du schéma Massia **sans équivalent actuel** (envoyés à `null` : `MDRefMassia`, `Certification`, `Hygrometry`, etc.) — bonne pratique de traçabilité des limites connues.
- `App\Services\Massia\BlMassiaExportService::exporter(bl $bl)` : appelle le mapper puis le client, journalise succès/échec (`Log::warning`/`Log::error` + `report()`), marque `$bl->bl_exporte = 1` en cas de succès. Retourne un tableau `['success' => bool, 'message' => ..., 'payload' => ..., 'reponse' => ...]` — jamais d'exception non interceptée, comportement adapté à un déclenchement batch.
- Deux points d'entrée pour déclencher l'export : `App\Console\Commands\massia_exporter_bl_command` (`php artisan massia:exporter-bl {bl_code?}` — sans argument, exporte tous les BL avec `bl_annule = 0` et `bl_exporte = 0` ; **non planifiée dans `Console/Kernel.php`**, à lancer manuellement pour l'instant) et `App\Http\Controllers\Massia\massia_bl_export_controller::exporter()` (pensé pour `POST /v1/massia/bl/{bl}/exporter`, route à créer).

**Sens entrant — import d'une formule depuis Massia** :
- `App\Http\Controllers\Massia\massia_formule_import_controller::importer()` (pensé pour `POST /v1/massia/formule`) valide `CodeBeton`/`Centrale.Code` puis délègue à `App\Services\Massia\FormuleMassiaImportService::importer(array $payload)`.
- Le service fait un `firstOrNew`/upsert sur `formule` (+ `formule_detail` par centrale) et **remplace intégralement** la composition (`composition::where(...)->delete()` puis recréation) pour la centrale concernée, dans une transaction DB unique.
- Depuis le 2026-09-17, `formule_detail` et `composition` sont en plus scopés par `formule_variant_id` (variante active résolue via `formule_variant::getActive()`, fallback `0`), pour rester cohérent avec le reste de l'appli (`formule_controller`, `composition_calcul_trait`) qui filtre toujours ces deux tables par la variante active — sans quoi un import Massia écrasait/laissait invisible la composition selon la variante en base. Voir [docs/massia_integration.md](docs/massia_integration.md).
- Les règles de correspondance de codes (préfixe centrale sur le code béton, transformation `X<code>` → `<code>` pour l'exposition, concaténation classe+résistance, `Cl0<code>` pour le chlorure, suffixe `--` sur les codes fournisseurs...) sont **documentées en docblock avec leur origine** ("Interface Massia-Alfi_Formules/Produits" fournie par Arcade) et leurs limites (règle chlorure déduite d'un seul exemple) — traçabilité métier soignée, à conserver comme modèle pour documenter les futures intégrations tierces.

**À faire avant mise en service** (voir aussi §9) : déclarer les routes `v1/massia/...` (avec middleware d'auth adapté — probablement une clé API dédiée côté entrant, symétrique à celle utilisée en sortant), ajouter `config/services.php['massia']` + variables `.env`, migrer la colonne `bl_exporte` sur `t_bl`, décider si `massia:exporter-bl` doit être planifiée (`Console/Kernel.php`) ou rester déclenchée à la demande.
| Reporting | KoolReport | Rapport "chiffre d'affaire par société" (`app/Reports/`) |
| Intégration BHP (fiches référentiel) | `app/Http/Controllers/api_transfert_controller.php` (`/transfert/{fiche}`, `auth:sanctum`) | Endpoint générique GET/POST/DELETE couvrant plusieurs fiches (`$modeles`/`$controleurs` : client, chantier, chauffeur, formule, vehicule, origine, fournisseur). |

`api_transfert_controller::enregistrer_fiche()` normalise le corps (objet unique ou liste) et, pour chaque fiche, appelle `resoudre_liaisons()` **avant** de déléguer au contrôleur métier : elle résout génériquement toute relation `belongsTo` `lier_*` transmise en objet imbriqué (ex. `"fournisseur": {"fournisseur_code": "..."}` → `fournisseur_id`), y compris le cas `null` explicite (→ FK à `0`, convention métier existante). Chaque contrôleur de fiche n'a donc plus qu'à exposer `enregistrer_api()` (résout son propre PK via son `*_code`, merge `<fiche>_id`, délègue à `enregistrer()`) et `supprimer_api()` (cherche par `code`, délègue à `supprimer($id, $api=true, $tracabilite_envoi_bhp, $user)`), suivant le format de réponse d'`origine_controller` (`success`/`message`/`errors`/`id_tracabilite_envoi_bhp`, `success` forcé à `true` en cas de blocage métier quand `$api` est vrai, pour éviter les boucles de retry côté BHP). ⚠️ Piège identifié : ne pas copier tel quel le fallback `''` d'`origine_controller`/`fournisseur_controller` pour l'auteur de traçabilité quand il n'y a pas de token Sanctum (formulaire web) — pour une fiche activement éditée en UI, garder `$this->user->name` en repli sous peine de blanchir la traça web. État au 2026-09-15 : origine et fournisseur historiquement câblés ; chauffeur, classe_chlorure, classe_consistance, classe_exposition, transporteur, vehicule ajoutés ; client/chantier/formule et les autres fiches référentiel restantes sont câblées fiche par fiche à la demande (voir `docs/api_transfert_bhp.md` pour le détail à jour).

`enregistrer_fiche()` résout aussi, pour les fiches listées dans `$modeles_centrale` (chauffeur, client, vehicule pour l'instant — celles qui ont une relation `lier_<fiche>_centrale`), un tableau `centrales` consommé par le `sync()` du contrôleur métier : `t_parametre.parametre_integration_<fiche>` (colonne par fiche) à `1` → toutes les centrales actives ; à `0`/absent → uniquement la centrale émettrice (`centrale_code` du payload), en conservant les centrales déjà liées pour ne pas les effacer lors d'une mise à jour. Reprend le comportement de l'ancien endpoint legacy `public/trans/action_trans_chauffeur.php`. ⚠️ Les clés de tableau PHP sont sensibles à la casse (contrairement aux colonnes MySQL) : `vehicule` a une colonne `Vehicule_Code` et le payload BHP l'envoie avec cette casse exacte — `api_transfert_controller::$colonnes_code_speciales` mappe ces exceptions fiche par fiche. Détail complet (payloads, conventions par contrôleur, état d'avancement par fiche) : `docs/api_transfert_bhp.md`.

**Tests de l'API transfert** (`tests/Feature/ApiTransfert/`, ajoutés 2026-09-24) : une classe PHPUnit par fiche (`<Fiche>TransfertTest`, ordre du tableau de `docs/api_transfert_bhp.md`) + `GeneriqueTransfertTest` (401/404/casse/`tracabilite_envoi`). Toutes héritent d'`ApiTransfertTestCase`, qui fournit les 13 scénarios standard (GET liste/code, POST création/mise à jour/liste/validation, DELETE ok/inconnu/sans droit, traça BHP `BHP_TEST - Brody` + horodatage) et crée un vrai token Sanctum ; chaque fiche déclare sa config (`$table`, `$colonneCode`, `$droitsProfil`, `payloadValide()`) et ajoute ses scénarios propres (liaisons imbriquées, centrales, blocages de suppression...). Base de dev + `DatabaseTransactions` (rollback). Lancer : `php artisan test --filter=<Fiche>TransfertTest` ou tout le dossier `php artisan test tests/Feature/ApiTransfert`. Les tests en échec au 2026-09-24 documentent de vrais bugs applicatifs (type_vehicule, formule, famille_beton_echantillon, mode_reg, position_compte, zone, et `resoudre_centrales()` en mode local pour client/chantier).

---

## 6quater. Impression d'un lot de fiches par no_exec (`impression_pdf_controller`)

Route API (`POST /impression_pdf/imprimer`, sans middleware d'auth, ajoutée
le 2026-09-11, adaptée le 2026-09-14 au format d'un appelant externe —
automate de centrale) qui reçoit un **tableau JSON** de fiches, chacune avec
`fichier_type` (`0` = bl, `1` = pesee), `fichier_no_exec` (→ `t_bl.bl_no_exec`),
et `fichier_id`/`centrale_code` (reçus, non exploités pour l'instant —
`centrale_code` ne filtre pas la recherche). Chaque fiche est traitée
indépendamment et le contrôleur renvoie un tableau de résultats dans le même
ordre (statut HTTP `200` global, succès/échec porté par fiche). Pour
`fichier_type=0` : retrouve le BL, régénère son PDF dématérialisé via
`App\Services\bl_demat_service::genererPdfPourBl()` (§ ci-dessus) puis
l'imprime via `App\Services\impression_pdf_service::imprimerPdf()` (appel CLI
à SumatraPDF via `Illuminate\Support\Facades\Process` — plus `exec()` brut —
borné à **30 s** avec `Process::timeout(30)`, chemin lu dans
`env('SUMATRA_PATH')`, imprimante par défaut dans `env('IMPRIMANTE')` ; si
SumatraPDF ne répond pas dans ce délai — imprimante introuvable/hors ligne,
boîte de dialogue Windows bloquante — le process est tué et un message
d'erreur dédié est retourné au lieu de bloquer la requête indéfiniment, comme
c'était le cas avant ce correctif du 2026-09-14). Pour `fichier_type=1` : résout l'`id_bl`
correspondant (`bl::where('bl_no_exec', ...)->value('id_bl')`), génère le PDF
des pesées via `App\Services\pesee_pdf_service::genererPdfPourBl()`
(enregistré dans `storage/app/public/pdf_pesee/{id_bl}.pdf`) puis l'imprime de
la même façon. `pesee_pdf_service` a été extrait de
`pesee_controller::imprimer_pdf()` (qui construisait son PDF `Html2Pdf` en
ligne pour le renvoyer en téléchargement navigateur, mode `'D'`) afin d'être
**partagé entre `pesee_controller`** (méthode `construireHtml2Pdf()`, mode
`'D'` inchangé) **et `impression_pdf_controller`** (méthode
`genererPdfPourBl()`, mode `'F'` = enregistrement disque + impression) — même
logique d'extraction que pour `bl_demat_service`, à réutiliser si un futur
type de fiche imprimable apparaît. Voir
[`docs/impression_pdf_controller.md`](docs/impression_pdf_controller.md) pour
le détail des paramètres/réponses. `bl_no_exec` n'a pas de contrainte
d'unicité en base (incrémenté globalement via `bl::max('bl_no_exec') + 1`),
point à surveiller si des doublons apparaissent un jour.

---

## 6quinquies. Création du BL de cycle (`depart_cycle_service`)

`App\Services\depart_cycle_service::creerBl()` (extrait le 2026-09-15 de
`depart_cycle_controller::lancer_cycle_valider()`) reprend à l'identique le
remplissage des ~150 champs du BL, la création des `ligne_bl` (une par
`ligne_commande` de la commande) et la mise à jour de `commande`/`ventilation`
(numéro de séquence), le tout dans un `DB::transaction()` — une erreur en
cours de route (échec de sauvegarde du BL ou d'une ligne) annule maintenant
tout au lieu de laisser un BL orphelin sans lignes, ce qui pouvait arriver
avant ce correctif. Le contrôleur valide désormais `commande_id`,
`ventilation_id`, `depart_cycle_vehicule_id`, `chauffeur_id` (présence +
existence en base) et `depart_cycle_qte` (numérique, `> 0`) via
`Validator::make()` avant d'appeler le service, et renvoie
`{success:false, message, errors}` en cas d'échec — ce format était déjà
attendu par le JS de `resources/views/depart_cycle/index.blade.php`
(`retour.errors[champ]` pour surligner le champ en erreur) mais jamais
réellement produit par le contrôleur jusqu'ici (chemin d'erreur mort côté
front). L'appel à `envoyer_socket()` (trait qui construit lui-même une
`response()->json()`, cf. §7) reste dans le contrôleur plutôt que dans le
service, pour ne pas faire dépendre la couche service de ce trait HTTP.

---

## 6sexies. Checksum sur le transfert des formules (`public/trans/`)

Ajouté le 2026-09-29, détail dans [docs/transfert_formule_checksum.md](docs/transfert_formule_checksum.md).

Contrôle de cohérence entre la trame formule et ses lignes de composition. **Checksum** = somme des `composition_quantite` de la variante active (formule + centrale), en millièmes (`round(q*1000)` par ligne), complétée par des zéros à gauche sur 10 caractères. Les fonctions communes sont dans `public/inc/fonction.inc.php` : `calculer_checksum_composition()`, `formater_checksum_composition()` et `propager_formule_groupe_centrale()`.

- **Web → centrale** : `action_trans_formule.php?action=extraction` ajoute la clé `formule_checksum` en **dernière position** du JSON. C'est le programme centrale qui fait la vérification.
- **Centrale → web** : en trois temps.
  1. L'en-tête formule arrive avec le paramètre `checksum`, stocké dans `t_formule_detail.formule_checksum_attendu` avec le statut `formule_checksum_statut = 'ATTENTE'`.
  2. Les lignes de composition sont enregistrées pour la centrale source uniquement.
  3. `action_trans_formule_composition.php?action=controle_checksum` compare le checksum attendu à celui recalculé.
     - OK : recopie vers les centrales du même `groupe_centrale_formule_id` et une seule `t_tracabilite_envoi` par centrale.
     - KO : statut `KO`, `erreur=1`, **aucune recopie ni renvoi vers les autres centrales**.
- **Compatibilité** : sans paramètre `checksum` (ancien programme centrale, import CBAO `public/cbao/integrer_cbao.php`), le fonctionnement historique est conservé. La recopie vers le groupe se fait alors ligne par ligne, avec une `t_tracabilite_envoi` par ligne, et les compositions du groupe sont supprimées dès la réception de l'en-tête.

---

## 6septies. Modification groupée de formule — routes manquantes + mode « ajout produit/dosage »

Ajouté le 2026-09-29, détail dans [docs/formule_modification_groupee_ajout.md](docs/formule_modification_groupee_ajout.md).

- **Routes absentes de `routes/web.php`** : la feuille `formule_modification_groupee` (contrôleur, vue, entrée de menu déjà en place depuis le 2026-09-07 selon l'historique de ce document) n'avait en réalité **aucune route déclarée** — la page renvoyait 404. Corrigé par l'ajout du groupe `Route::prefix('/formule_modification_groupee')->name('formule_modification_groupee.')->...` (5 routes : `index`, `liste_produit`, `liste_produit_substitution`, `table`, `valider`), sur le modèle de `groupe_centrale_formule`. À noter pour la suite : la mention "routes ... `formule_modification_groupee`" dans l'historique du 2026-09-07 ci-dessus était donc inexacte — leçon pour ce document : une route citée comme "ajoutée" mérite d'être vérifiée dans `web.php`, pas seulement déduite de la présence du contrôleur/vue.
- **Nouveau mode « Ajouter un produit et un dosage » (`action=3`)** : jusqu'ici la feuille ne permettait que 3 modes de *remplacement* de produit existant. Le contrôleur contenait déjà une logique d'ajout (« MODE 2 : AJOUT/CREATION » dans `valider()`) mais inatteignable depuis l'UI, car `data_table()` exigeait la sélection d'un produit déjà présent pour lister la moindre formule. En mode ajout, ce filtre obligatoire est levé et la contrainte `composition_type` sur la requête de base également, pour lister les formules d'une centrale même si elles ne possèdent pas encore le type de produit à ajouter.
- **Droit dédié** `t_profil.profil_formule_modification_groupee_ajout` (booléen, migration `2026_09_29_1200_V905000_NB_...`), indépendant du droit global `profil_outil_formule` qui gère l'accès à la feuille — même pattern que `profil_devis_comptant_uniquement` (§4). Vérifié côté serveur à 3 endroits (affichage de l'option dans `index()`, `data_table()`, `valider()`) plutôt que côté client uniquement, pour rester robuste à une requête forgée.

---

## 6octies. Facture d'acompte et lignes de facture (module `facture_laravel`)

Ajouté le 2026-09-30, détail dans [docs/facture_acompte.md](docs/facture_acompte.md) et [docs/profil_facture_defacturer_bl.md](docs/profil_facture_defacturer_bl.md).

- **Facture d'acompte** : une facture normale (même numérotation, `facture_avoir` = 0) avec `t_facture.facture_acompte` = 1, créée depuis l'écran de création manuelle (choix « Facture acompte »). Le titre de la fiche indique « Facture », « Avoir » ou « Facture d'acompte ».
- **Déduction** : sur une facture normale, le bouton « + Factures d'acomptes » liste les acomptes non déduits du client, puis `facture_controller::deduire_acomptes()` ajoute en fin de corps deux lignes vides (`E`) puis une ligne `S` par acompte et par taux de TVA. Chaque ligne porte l'article `t_parametre.parametre_article_deduction_acompte_id`, le libellé « Déduction acompte N° du JJ/MM/AAAA » et le HT de l'acompte en négatif. L'acompte est marqué `facture_acompte_deduit` = 1 avec `facture_acompte_num_facture_deduite`.
- **Lien ligne → acompte** : il n'y a pas de clé étrangère. Le lien est le **libellé initial** de la ligne (`ligne_facture_libelle_initial`, non modifiable depuis la fiche pour une ligne de service), relu par `numero_acompte_ligne_deduction()`. La suppression de la ligne, ou de la facture, libère l'acompte. À garder en tête si on modifie un jour ce libellé ou si l'on permet de l'éditer.
- **Ajout de ligne Laravel** : `valider_ligne()` est un portage de `public/facture/valider_ligne_facture.php` (droits, type de ligne et TVA selon l'article, insertion sous la ligne sélectionnée, doublons). La recherche d'article **réutilise les pages legacy** `public/facture/recherche_produit/*.php`, qui écrivent directement dans les champs de la fiche (mêmes `id` DOM). Toute modification de ces `id` dans `facture/form.blade.php` casse la recherche. L'unité renvoyée par ces pages (code ou libellé au lieu de l'id) est traduite côté Laravel par une surcharge de `$.valHooks.select` limitée à `#unite_ligne_facture`.
- **Fenêtres dans la fiche** : `#affiche_facture` a un `transform` CSS (`style.css`), qui rend `position: fixed` relatif au cadre. Les fenêtres ouvertes depuis la fiche (acomptes, recherche produit) sont donc déplacées dans le `body` au chargement et supprimées à la fermeture (`facture/index.blade.php`). Même piège à prévoir pour toute nouvelle fenêtre dans une fiche chargée dans un cadre `#affiche_*`.
- **Droit** `t_profil.profil_facture_defacturer_bl` : le bouton « Défacturer BL » demande ce droit **et** un droit de modification facture (3, 4 ou 5), contrôlé aussi côté serveur. Laravel uniquement : la fiche legacy garde l'ancienne règle (profil 5).
- **Liste des factures** : le filtre « Facture acompte » s'ajoute à Factures et Avoirs. Les types cochés se cumulent, et « Factures » n'inclut plus les acomptes.

---

## 7. Patterns transverses (Services / Traits / fonctions)

Trois styles coexistent sans frontière claire :

- **`app/Services/`** : classes orientées objet, responsabilité unique, injectables — le style "cible" (`tarif_service`, `article_tarif_service`, `MailingService`, `generer_peppol_bis3_xml_service`, `structure_client_externe_service`). Le nouveau sous-namespace `App\Services\Massia\` (client HTTP / mapper / service métier séparés, injection par constructeur, docblocks qui tracent les règles métier et leurs limites) est à ce jour l'exemple le plus abouti de ce style — bon gabarit à réutiliser pour la prochaine intégration tierce plutôt que de repartir du legacy `app/fonctions/`.
- **`app/Traits/`** : utilisés ici de façon détournée comme des **actions de contrôleur entières** (`composition_calcul_trait`, `dupliquer_commande_trait`, `envoyer_socket` — ils appellent `response()->json()`, `auth()->user()` directement). Ce n'est pas l'usage classique d'un trait (comportement réutilisable indépendant du contexte HTTP), ce qui nuit à la testabilité.
- **`app/fonctions/`** (namespace `App\fonctions`, minuscule) : classes "sac de fonctions" procédurales héritées d'un style pré-Laravel, portant parfois une logique métier aussi lourde que les Services (ex. calcul de taxes sur 12-19 Ko) sans règle documentée de placement.

**Numérotation des lignes de devis / lignes de taxe (2026-09-30)** : les lignes de taxe d'un devis (`ligne_devis_type_article = 1` et `article_id` égal à l'un des 9 articles taxe/frais de `t_parametre` : `parametre_article_contribution_environ_id`, `_taxe_carbone_id`, `_taxe_energetique_id`, `_ecotaxe_id`, `_frais_fac_beton_id`, `_tgap_id`, `parametre_rep_granulat_1_article_id`, `parametre_contrib_env_granulat_article_id`, `parametre_article_frais_fac_carriere_id`) ne sont plus numérotées `999`. Après chaque ajout, modification, suppression, déplacement ou duplication, `renumeroter_lignes_devis` renumérote 0..n-1 : les **commentaires (type 9) gardent leur position** (placement libre, y compris entre/sous les taxes), et sur les autres positions les lignes normales passent d'abord puis les taxes. `reserver_numero_ligne_devis($devis_code, 'normale'|'taxe'|'commentaire')` donne le numéro d'une nouvelle ligne et décale les suivantes : normale → après la dernière ligne normale, taxe → après la dernière taxe, commentaire → tout en dernier. Deux implémentations identiques : méthodes de `ajout_lignes_taxes_fonction` (Laravel) et fonctions `renumeroter_lignes_devis($pdo, …)` / `reserver_numero_ligne_devis($pdo, …)` / `est_ligne_taxe_devis()` / `est_ligne_commentaire_devis()` dans `public/inc/fonction.inc.php` (legacy `public/devis/*`). `monter_ligne_devis.php` / `descendre_ligne_devis.php` empêchent une taxe de passer au-dessus d'une ligne normale (les commentaires ne sont jamais bloqués). Une ligne encore à `>= 999` (ancienne donnée) est traitée comme une taxe. Migration de reprise : `2026_09_30_1123_V905000_BAM_t_ligne_devis_renumeroter_lignes_taxes.php`.

Le trait `champs_invisibles_api_si_exterieur_trait` (masquage de champs API interne/externe, utilisé dans `BaseModel`) est défini et exposé sur tous les modèles, mais **aucun consommateur** (Resource API, middleware) n'a été trouvé dans l'échantillon exploré — à vérifier avant de le documenter comme actif.

---

## 8. Bilan : ce qui va bien / ce qui est à surveiller

### Points forts
- **Conventions de nommage très régulières** sur 140+ modules (contrôleur/modèle/vue/table alignés) : une fois le pattern connu, on retrouve n'importe quel module en quelques secondes.
- **Modélisation métier riche et conforme au domaine** (normes béton NF EN 206 : classes de résistance, exposition, chlorure, consistance...) — un vrai savoir métier est capitalisé dans le schéma de données.
- **Documentation utilisateur intégrée** (`views/doc_utilisateur/*`) par module — pratique rare et précieuse pour l'onboarding/support, à conserver et étendre.
- **`fiche_annexe/`** : un vrai mécanisme de composant Blade réutilisable et paramétrable, îlot de bonne factorisation dans un océan de vues monolithiques.
- **Gestion asynchrone du mail correcte** : Jobs + Listeners + hook `Queue::failing()` qui repropage l'échec vers le statut métier en base — un vrai souci de fiabilité.
- **Effort de conformité réglementaire réel** (PEPPOL/UBL2.1) et **couverture de tests Feature** sur plusieurs modules métier sensibles (facturation, produit, service, zone, chauffeur) en plus des tests Jetstream par défaut.
- **Traçabilité version/auteur dans les migrations récentes** : intention louable de savoir quelle migration correspond à quelle livraison.
- **Le module `bl` vient d'être redécoupé en onglets** (`bl_onglet_*`) et **la nouvelle intégration Massia suit un vrai style Service découplé** (client HTTP / mapping / logique métier séparés, règles de correspondance documentées avec leur source) — deux signaux concrets que les recommandations de la précédente version de ce document sont suivies d'effet.

### Points de vigilance
- **Secrets en clair dans le code versionné** (clé OpenAI, mots de passe FTP/SFTP) — **toujours présents et inchangés au 2026-09-07** (vérifiés à nouveau : `gpt_controller.php:17`, `config/filesystems.php:42,53,66`) — le point le plus urgent, indépendant de toute réécriture de code, et d'autant plus critique si ce dépôt est poussé sur un remote partagé/GitHub.
- **Travail non commité qui casse silencieusement le module `bl` s'il est perdu** : `resources/views/bl/*` (19 fichiers, dont la nouvelle décomposition en onglets) est untracked alors que `bl_controller.php` en dépend directement — aucune protection Git dessus pour l'instant.
- **Intégration Massia livrée "à moitié branchée"** : code Services/Controllers présent mais aucune route, config ou migration associée — risque d'oubli si une autre tâche prend le dessus avant la finalisation (voir §6ter et §9).
- **Contrôleurs géants** (jusqu'à 73 Ko / plusieurs centaines de lignes par méthode) mélangeant Eloquent, SQL brut, génération PDF et appels externes — coûteux à faire évoluer sans régression.
- **Vues formulaire monolithiques** (jusqu'à 83 Ko sur un seul fichier `form.blade.php`) — un seul fichier porte tout le cycle de vie d'une fiche complète.
- **Protection d'accès incohérente entre routes d'un même module** (middleware appliqué sur l'index mais pas sur les routes d'écriture) — même défaut que celui qu'on vient de corriger sur `/transfert`.
- **Trois styles de "logique métier transverse"** (Services/Traits-actions/fonctions procédurales) sans règle de placement — risque de duplication de règles métier.
- **Mass assignment quasi ouvert** sur des modèles sensibles.
- **Absence de docblocks métier** (relations, colonnes, règles) au-delà du tag généré par l'IDE helper.
- **Dossier `Unit/` de tests quasiment vide** — la logique de calcul (tarification, taxes, composition béton) n'est pas couverte par des tests unitaires isolés, seulement par des tests Feature bout-en-bout sur certains modules.
- **Bug confirmé dans `composition_calcul_trait.php` (switch `$type_addition`)** : deux `case` correspondent tous les deux à "Addition calcaire cat A" (au lieu d'un pour cat A et un pour cat B) — la colonne `*_calcaire_b_na` de `t_norme_contrainte` n'est donc **jamais** sélectionnée par ce calcul, quelle que soit l'addition réellement utilisée sur le produit. Repéré le 2026-09-08, pas encore corrigé (tâche flaggée en session, non traitée).
- **`t_code_norme` contient des données historiques de mauvaise qualité** (20 lignes : doublons `X0`/`XS3`, 4 lignes avec `classe_resistance`/`type_ciment`/`type_addition` vides, pas de couverture systématique des valeurs valides) — une migration (`2026_09_09_1000_V905000_NB_t_code_norme_remplissage.php`) les a rendues reproductibles sur un nouvel environnement mais n'a **pas** nettoyé ni complété ce jeu de données ; à trancher avec le métier avant d'aller plus loin (cf. `docs/t_code_norme_remplissage.md`).
- **Fiche facture Laravel encore adossée au legacy (2026-09-30)** : la recherche d'article passe par `public/facture/recherche_produit/*.php` (SQL concaténé depuis `$_GET`, dépendance aux `id` DOM de la fiche). Le lien entre une ligne de déduction et son acompte repose sur le libellé de la ligne (voir §6octies).
- **Checksum formule livré côté web seulement (2026-09-29)** : le contrôle reste inactif tant que le programme centrale n'envoie pas `checksum` et n'appelle pas `controle_checksum`. En cas de checksum KO, la composition reçue reste enregistrée pour la centrale source (statut `KO` dans `t_formule_detail`) : seule la propagation au groupe est bloquée. Aucun écran web n'affiche encore ce statut (voir §6sexies).

---

## 9. Feuille de route d'amélioration progressive

Pensée pour être appliquée **par petites touches, au fil des besoins**, sans chantier de refonte globale.

**Étape 0 — Corrections de sécurité immédiates (peu de risque, à faire dès que possible)**
1. Révoquer/rotater la clé OpenAI codée en dur dans `gpt_controller.php` (**toujours en clair au 2026-09-07**), la passer en `.env`.
2. Sortir les mots de passe FTP/SFTP/Trakkeo des valeurs par défaut littérales de `config/filesystems.php` (**toujours en clair au 2026-09-07**).
3. Forcer `SESSION_SECURE_COOKIE=true` en production.
4. Restreindre `allowed_origins` dans `config/cors.php` aux domaines réellement utilisés par l'API.
5. Committer (ou au moins sauvegarder) `resources/views/bl/*` avant toute autre manipulation Git sur ce dossier — actuellement le seul filet de sécurité est le disque local.

**Étape 1 — Cohérence des accès (module par module, dès qu'on retouche un module)**
6. Auditer et harmoniser, module par module, l'application du middleware d'auth **au niveau du groupe de routes** plutôt qu'à la seule route `index` (web.php ET api.php) — commencer par les modules les plus sensibles (facture, commande, chantier).
7. Décider explicitement d'un `$fillable` (plutôt que `$guarded` à 2 colonnes) sur les modèles manipulant de la donnée sensible (facture, client, commande, reglement), en commençant par celui qu'on modifie déjà.
8. Documenter/retirer la commande planifiée vide `tache_recuperer_fichier_xml_ftp` pour éviter de croire ce flux opérationnel.
9. Terminer le branchement de l'intégration Massia avant de l'oublier : routes `v1/massia/...` + middleware d'auth dédié, entrée `config/services.php['massia']` + `.env`, migration de la colonne `bl_exporte` sur `t_bl` (voir §6ter).

**Étape 2 — Nettoyage progressif de la couche métier transverse (opportuniste)**
10. Chaque fois qu'un trait "action de contrôleur" (`composition_calcul_trait`, `dupliquer_commande_trait`, `envoyer_socket`) est touché pour un besoin métier, en extraire la logique dans un vrai Service testable, sans dépendance à `Request`/`response()`.
11. Migrer progressivement les fichiers de `app/fonctions/` les plus consultés vers `app/Services/` (commencer par ceux dont la complexité rivalise déjà avec les Services existants, ex. calcul de taxes/transport) — le style adopté dans `app/Services/Massia/` (client/mapper/service séparés, docblocks de règles métier) est un bon gabarit à suivre.
12. Ajouter un test unitaire chaque fois qu'une règle de calcul (tarif, taxe, composition) est extraite en Service — combler petit à petit le dossier `tests/Unit/`.

**Étape 3 — Refactor ciblé des zones les plus lourdes (quand elles doivent être modifiées de toute façon)**
13. Découper les contrôleurs les plus gros (`commande_controller`, `formule_controller`, `client_controller`, `facture_controller`) en sous-contrôleurs ou Services par sous-fonction, au moment où une évolution touche déjà cette zone — pas de réécriture préventive.
14. Découper les formulaires Blade monolithiques (`client/form.blade.php` et équivalents) en composants Blade par onglet, en profitant du pattern `fiche_annexe/` déjà en place comme modèle de réutilisation — **le module `bl` vient de le faire** (`bl_onglet_*`, non commité, voir §3) : une fois validé et committé, il peut servir de référence concrète pour les prochains modules plutôt que `fiche_annexe/` seul.
15. Pour les nouvelles migrations, conserver la traçabilité version/auteur mais dans le corps du fichier (commentaire/docblock) plutôt que dans le nom, pour revenir au format standard `YYYY_MM_DD_HHMMSS_nom.php` et retrouver la compatibilité totale avec l'outillage Laravel — sans renommer l'historique existant.

---

## 10. Où chercher quoi — aide-mémoire rapide

- **Une route ne répond pas comme attendu ?** → `routes/web.php` (front métier) ou `routes/api.php` (API), puis le contrôleur `<domaine>_controller.php` correspondant, méthode `index/data_table/afficher_formulaire/enregistrer/supprimer`.
- **Un champ n'apparaît pas dans l'API pour un client externe ?** → vérifier `$champs_invisibles_api_exterieur` sur le modèle concerné et le trait `champs_invisibles_api_si_exterieur_trait`.
- **Un calcul de prix/tarif semble faux ?** → `app/Services/tarif_service.php` et `article_tarif_service.php` (et vérifier qu'il n'y a pas une règle concurrente dans `app/fonctions/`).
- **Un envoi de mail échoue silencieusement ?** → `app/Jobs/`, `AppServiceProvider::boot()` (hook `Queue::failing`), table `t_envoi_mail` (`envoi_mail_statut`).
- **Un problème de communication avec une centrale ?** → `envoyer_socket.php` (échange direct automate) ou `copier_fichier_sftp.php`/commande FTP (échange par fichiers).
- **Une facture électronique PEPPOL est mal formée ?** → `generer_peppol_bis3_xml_service.php` + `generer_data_peppol_par_facture_service.php`, contrôleur `facture_controller::telecharger_facture_peppol()`.
- **Un acompte reste « déduit » à tort, ou n'est plus proposé ?** → `t_facture.facture_acompte_deduit` / `facture_acompte_num_facture_deduite`, puis le libellé initial des lignes « Déduction acompte … » de la facture indiquée ; code dans `facture_controller::deduire_acomptes()` / `supprimer_ligne()` (§6octies).
- **Un droit d'accès API à ajuster ?** → middleware `ability` (`CheckTokenAbility`) et `interne` (`CheckUserInterne`), tokens Sanctum.
- **Une formule n'est pas arrivée sur les autres centrales du groupe ?** → regarder `t_formule_detail.formule_checksum_statut` de la centrale source (`KO` = propagation bloquée volontairement, `ATTENTE` = `controle_checksum` jamais appelé), puis `public/trans/action_trans_formule_composition.php` (§6sexies).
- **Un échange avec Massia (export BL / import formule) ne fonctionne pas ?** → `app/Services/Massia/*` + `app/Http/Controllers/Massia/*` ; vérifier d'abord que les routes/config manquantes ont bien été ajoutées depuis (voir §6ter, module non branché au 2026-09-07).
- **Un ratio maximal d'addition (composition béton) semble faux ?** → `app/Traits/composition_calcul_trait.php` (lit `t_norme_contrainte` via `classe_exposition`) — vérifier en premier le bug connu de `case` dupliqué sur "cat A" (§8) qui empêche la sélection de la colonne calcaire cat B.
