# Audit approfondi — Module Compte Journalier (`/Comptes`, `/Compte`, `/CompteZone`, `/ComptePv`)

> Décrit par l'utilisateur comme **le cœur de l'application**. Audit fait en lecture seule (aucune
> modification) par 4 agents dédiés, un par tranche du module, le 26/07/2026. Ce document synthétise
> leurs 4 rapports en un état des lieux complet + une stratégie de refonte.

## Sommaire

1. [Cartographie du module](#1-cartographie-du-module)
2. [Inventaire fonctionnel complet](#2-inventaire-fonctionnel-complet)
3. [Carte des dépendances](#3-carte-des-dépendances)
4. [Bugs réels trouvés](#4-bugs-réels-trouvés)
5. [Code non optimisé](#5-code-non-optimisé)
6. [Design / UX](#6-design--ux)
7. [Rechargements de page à convertir en AJAX/JSON](#7-rechargements-de-page-à-convertir-en-ajaxjson)
8. [Stratégie de refonte](#8-stratégie-de-refonte)

---

## 1. Cartographie du module

Le "Compte Journalier" recouvre en réalité **3 sous-systèmes distincts**, qui partagent une liste
d'entrée commune (`/Comptes`) mais divergent ensuite complètement :

| Sous-système | Route | Contrôleur(s) | Taille | Table pivot |
|---|---|---|---|---|
| **Carburant** (le "classique") | `/Compte`, `/CompteZone` | `CompteController` (537 l.), `CompteZoneController` (626 l.), `EncaissementController` (788 l., moteur interne non routé) | ~1950 lignes | `carburant_compte` |
| **PV** (atelier/co-locataire) | `/ComptePv`, `/ComptePv/resultat` | `ComptePvController` (1204 lignes, monolithe) | 1204 lignes | `compte_pv` |
| **PS** (Produits/Services) | `/ComptePs` | *(hors périmètre de cet audit, non exploré)* | — | `compte_ps` |

`Compte::data()` (modèle) fait un `UNION ALL` SQL entre les 3 tables (`carburant_compte`,
`compte_ps`, `compte_pv`) pour produire la liste unifiée de `/Comptes` — c'est le **seul** point de
jonction réel entre les 3 systèmes. Au-delà, ils sont **totalement indépendants** : pas de modèle,
pas de logique, pas de table partagée entre Carburant et PV (hormis les tables *transverses*
communes à toute l'appli : `client_bon`, `client_avoir`, `coffre_piece`, `carte_bon`,
`compte_ps_caisse_flux`, `compte_bancaire_flux` — chaque système y écrit avec son propre `source`).

**Conséquence stratégique directe** : il n'existe **aucun moteur d'encaissement unifié**. Le
Carburant a `EncaissementController` (8 méthodes de paiement dupliquées), le PV a son propre jeu de
6 méthodes de paiement dans `ComptePvController` (dupliquées différemment) — deux implémentations
parallèles du même problème ("attacher un paiement à un montant dû"), avec des bugs différents dans
chacune. **Le PS n'a pas été audité** dans cette passe — à couvrir avant toute refonte transverse,
il a probablement sa propre 3ᵉ implémentation du même problème.

**Bonne nouvelle confirmée** : le déplacement récent de "Réglage PV" (`Pv`/`Atelier`/`Colocataire`)
vers `Parametre/ParaPvController` **n'a rien cassé** dans `ComptePvController` — les modèles/tables
(`compte_pv_base`, `compte_pv_atelier`, `compte_pv_colocataire`, `compte_pv_atelier_poste`,
`compte_pv_base_stock`) n'ont pas bougé, seule la *gestion* (CRUD des définitions) a été relocalisée,
pas les données ni les points de lecture utilisés par `ComptePvController` (confirmé explicitement
par l'agent PV, §2 de son rapport).

---

## 2. Inventaire fonctionnel complet

### 2.1 Carburant — `/Comptes` (liste)

| Action | Déclencheur | Effet |
|---|---|---|
| Liste des comptes (tous types confondus) | chargement page | DataTable server-side, `Compte::data()` (UNION carburant/ps/pv) |
| Nouveau compte | bouton "Ajouter" → modale | **form POST classique** vers `/Comptes/ajouter` → crée le compte + copie zones/pistolets du scénario + prix produits |
| Modifier un compte | bouton modifier → modale pré-remplie | même form POST, avec `id` |
| Supprimer un compte | bouton dans la modale | `window.location.href` vers `/Comptes/delete_compte/{id}` → supprime compte+zones+pistolets+prix, cascade |
| Calcul de marge/coût FIFO | ouverture modale Nouveau_compte | AJAX JSON `/Comptes/marge` (pré-remplissage des prix) |
| Total filtré de la liste | après chaque `draw` DataTable | 2ᵉ appel AJAX séparé `/Comptes/total_total` renvoyant un `<script>` à exécuter (anti-pattern) |
| Clic sur une ligne (voir compte) | clic ligne | `window.location.href` vers `/Compte/index/{id}` ou `/ComptePv/index/{id}` selon le type |

### 2.2 Carburant — `/Compte/{id}` (détail, si le scénario a plusieurs zones)

| Action | Effet |
|---|---|
| `header()` | Calcule badge résultat + bouton contextuel (Clôturer / Vérifier / Vérification / Correction) |
| `zones()` | Liste des zones du compte, encaissement/vente/statut par zone, bouton "voir" en rechargement complet |
| `principal()` | Synthèse : pompistes, encaissements, ventes, citernes, sorties de caisse |
| `secondaire()` | Détail d'un type d'encaissement, délègue à `EncaissementController::$methode()` |
| `validation()` | **Méthode fourre-tout à 4 responsabilités** : clôture / annulation / vérification stock / annulation-vérification, sélectionnées selon les clés du payload |
| `nouveau()` (statique) | Réouvre la modale de modification du compte |

**Si le scénario n'a qu'1 seule zone**, `/Compte/index/{id}` **redirige directement** vers
`/CompteZone/index/{id}/{id_zone}` — la vue "Compte" (multi-zones) n'existe jamais pour ces comptes.

### 2.3 Carburant — `/CompteZone/{id}/{id_zone}` (détail d'une zone/pompiste)

| Action | Effet |
|---|---|
| `zone()` | Agrège vente/encaissement/statut/pompistes d'une zone |
| `header()` | Bouton Clôturer/Vérification contextuel (zone) |
| `validation()` | Clôture zone, **répartition de l'écart entre pompistes** (division par `count-1`, bug voir §4), annulation |
| `pompistes()` | Attacher/détacher un pompiste à la zone |
| `ventes()` | Saisie des **index de pistolets** + retours en stock |
| `ajouter_bon()` | Point d'entrée générique : crée le pivot `carburant_compte_bon`, délègue à `EncaissementController::$methode()` |
| `principal()` / `secondaire()` | Synthèse zone / détail par type d'encaissement (avec suppression) |
| `avoir()` / `appareil()` / `matricule()` / `identifiant()` | Endpoints JSON utilitaires pour les formulaires |
| `info_attachement()` / `detacher()` | Rattache un `client_bon` "Direct" (créé côté Vente) au compte courant |
| `data_prepaye()` / `info_attachement_prepaye()` / `detacher_prepaye()` | Même pattern pour les tickets carte prépayée |
| `ajouter_sortie()` | CRUD sorties de caisse |

**8 méthodes de paiement** (moteur `EncaissementController`, instancié à la volée avec
`$id_compte`/`$id_zone`) : `client` (bon crédit), `ont` (vignette administration), `carte`,
`bon_baf` (pièce fournisseur), `espece`, `cheque_lcn`, `avoir_sortie` (émission d'avoir),
`avoir_retour` (attachement d'un avoir existant — concept "ATTACHEMENT" du CLAUDE.md). Chacune suit
le même squelette copié-collé : liste / suppression / création-modification / affichage formulaire.

### 2.4 PV — `/ComptePv/{id}` et `/ComptePv/resultat/{id}`

| Domaine | Actions |
|---|---|
| Cycle de vie compte | `nouveau()`, `cloture_compte()` |
| Tickets | `nouveau_ticket()` (CRUD), `encours()` (liste temps réel, **polling 5s**), `infos_ticket()`, `ticket()` (calcul reste/montant), `prestation()`, `cloture()` (ticket individuel) |
| Articles | `formulaire()` (ligne produit/service, mouvement de stock si `type_vente='produit'`), `ajax()` (stocks disponibles), `recharge()` (cas spécial carte prépayée), `top_service()` |
| **6 méthodes de paiement** | `espece`, `piece`, `operation`, `carte`, `client` (crée un `client_bon`), `gratuite` — même squelette dupliqué que côté Carburant, avec ses propres bugs |
| Employés/ateliers | `attachement()` (employé ↔ compte), `employes()` (performance) |
| Résultat | `chiffre_affaire()`, `encaissements()`, `resultat_final()` — **tout pollé toutes les 5s** sur la vue Résultat |
| Autres | `matricules()` (autocomplete), `impression()`, `infos_carte()`, `facture()` (**morte, inachevée**) |

---

## 3. Carte des dépendances

```
                      ┌──────────────────────┐
                      │   /Comptes  (liste)   │
                      │  Compte::data()       │  UNION carburant_compte / compte_ps / compte_pv
                      └───────────┬───────────┘
                 ┌────────────────┼────────────────┐
                 ▼                                  ▼
   ┌─────────────────────────┐          ┌─────────────────────────┐
   │  CARBURANT               │          │  PV                       │
   │  /Compte  → (si 1 zone)  │          │  /ComptePv                │
   │  redirige vers            │          │  /ComptePv/resultat       │
   │  /CompteZone directement │          └─────────────┬─────────────┘
   └──────────┬───────────────┘                        │
              ▼                                          │  lit (pas de CRUD ici depuis 26/07)
   ┌─────────────────────────┐              compte_pv_base / _atelier / _colocataire
   │  /CompteZone              │              / _atelier_poste / _base_stock
   │  (zone = pompiste)        │                        ▲
   └──────────┬───────────────┘                        │
              │ instancie                    ┌──────────┴──────────┐
              ▼                              │ Parametre/ParaPv     │  (CRUD des definitions,
   ┌─────────────────────────┐              │ (deplace le 26/07)   │   deplace cette session)
   │  EncaissementController   │              └──────────────────────┘
   │  (moteur interne, non     │
   │   route, dispatch dynamique
   │   via parametre_type_     │
   │   encaissement.methode)   │
   └──────────┬───────────────┘
              │ écrit selon la méthode
              ▼
   client_bon / ont_bon / carte_bon / coffre_bon_baf /
   carburant_compte_espece / coffre_piece / client_avoir
   (chacune reliée au pivot carburant_compte_bon via source='Carburant')
```

**Points de couplage à risque identifiés** :
- Le **dispatch dynamique** `$controller->$methode($param)` (Carburant : `CompteZoneController.php:326,347,395,397`, `CompteController.php:214`) suppose que `parametre_type_encaissement.methode` (une colonne **configurable en base**, éditable depuis Paramètre) correspond toujours à une méthode publique existante. Aucune garde : un typo de configuration = fatal error en production. Une vue orpheline (`forms/tpe.php`) existe déjà sans méthode correspondante — bombe à retardement si un type "TPE" est activé un jour.
- **3 copies indépendantes** de la logique "ce compte/cette zone est-il/elle clôturé(e) ?" (`CompteController::header()`, `principal()`, `validation()`) — déjà divergentes (`header()` gère un 4ᵉ état "correction" que `principal()` n'a pas).
- **Pas de transaction** autour des séquences multi-tables (suppression du pivot `carburant_compte_bon` avant l'appel métier dans `secondaire()`, création compte + zones + pistolets + prix dans `ComptesController::ajouter()`) — un échec en cours de séquence laisse des données incohérentes.

---

## 4. Bugs réels trouvés

Classés par sévérité. Chaque ligne = un bug **confirmé par lecture de code**, pas une supposition.

### 🔴 Critique (perte/incohérence de données, plantage utilisateur)

1. **Dispatch dynamique sans garde** — `parametre_type_encaissement.methode` invalide → fatal error (`CompteZoneController.php:326,347,395,397`, `CompteController.php:214`). Vue `forms/tpe.php` orpheline confirme le risque latent.
2. **Division par zéro** — `CompteZoneController::validation()` ligne 178 : répartition de l'écart entre pompistes divise par `count($pompistes) - 1` ; avec un seul pompiste sélectionné → division par 0.
3. **Callback JS exécuté au lieu d'être passé en référence** — `Compte.php:142,158` (`load_portion(...,$.callback())` au lieu de `$.callback`) : le toast de confirmation et le rafraîchissement s'exécutent **avant** la fin réelle de l'annulation de clôture — l'utilisateur voit "annulé" alors que ce n'est pas encore fait côté serveur. Reproduit à l'identique 3 fois (`#annuler`, `#corriger`, et le 3ᵉ cas similaire).
4. **Aucun contrôle serveur du plafond de paiement (PV)** — `espece()`, `piece()`, `operation()`, `carte()`, `gratuite()` de `ComptePvController` : le `max` du `reste` n'est qu'une contrainte HTML côté client. Un ticket peut être surpayé via un appel direct. Seule `client()` a un vrai garde-fou serveur.
5. **Branding station en dur, en contexte multi-tenant** — `Compte.php:2532-2535` (PV) : nom/adresse/téléphone de station codés en dur dans le ticket imprimé, alors que `$station` est chargé mais jamais utilisé. Deux tenants différents impriment le même en-tête.
6. **`facture()` (PV) inachevée/morte** — construit des données mais n'insère jamais rien, aucun appelant.
7. **`ComptePvTicketDescription::descriptions($id_ticket)`** — le paramètre est écrasé par `$_POST['id_ticket']` dans le corps de la fonction : la signature ment sur ce qu'elle fait réellement.

### 🟠 Important (comportement incorrect dans des cas limites)

8. Accès tableau non gardé après un `find()` potentiellement vide — motif répété **au moins 15 fois** à travers le module (`EncaissementController.php` ×11, `Vente.php` ×2, `Encaissement.php` ×2, `ProduitPrix` en validation ×2) : `$x->find(...)[0]['champ']` sans vérifier que `$x` n'est pas vide → warning PHP ou `null` propagé silencieusement dans un insert/update suivant.
9. **Ordre d'opérations dangereux sans transaction** — `CompteZoneController::secondaire()` supprime le pivot `carburant_compte_bon` **avant** d'appeler la méthode métier de suppression réelle : fenêtre où la donnée métier existe sans son pivot.
10. **Incohérence de garde entre `avoir_retour()` et les 7 autres méthodes** d'`EncaissementController` — seule celle-ci n'a pas le test `!isset($param['modif'])`, un appel "modification" peut tomber dans la mauvaise branche.
11. **Comparaison lâche vs stricte incohérente** — `avoir_sortie()` compare `id_client == 0` en modification et `id_client === 0` en création pour la même logique ; un POST (toujours string) fait échouer le test strict.
12. **Édition = suppression + réinsertion** (PV : `espece()`, `piece()`, `operation()`) au lieu d'un `update()` — perd l'historique de création d'origine (`id_creation`/`date_creation` écrasés), casse potentiellement le nouveau Journal d'activité (audit trail). `carte()` fait un vrai update — incohérence de pattern entre méthodes sœurs.
13. **Ticket à montant net nul jamais clôturable** (sauf colocataire) — un ticket entièrement couvert par une gratuité reste bloqué en l'état ouvert.
14. **Formulaire `Pompiste.php` référence l'ancien routage** (`index.php?page=Compte&options=CompteZone&action=attacher`) — mort aujourd'hui (soumission gérée en JS) mais un `<input type=submit>` ajouté par erreur enverrait vers une 404.
15. **`ZonePompiste::initialiser()` / `PistoletIndex::index_modif()`** — `die(print_r($req->errorInfo()))` en cas d'erreur SQL : debug de développement resté en production, écran blanc brutal pour l'utilisateur au lieu d'un message géré.
16. **Filtre zone masqué pour les utilisateurs à accès complet** — `Comptes.php:83` : `if (count($zones)==1 or $compte_verif==1)` cache le filtre de zone précisément pour l'utilisateur qui aurait le plus besoin de filtrer (accès total) — probable inversion de condition à valider avec le métier.

### 🟡 Mineur (dette silencieuse, pas de casse immédiate)

17. `$date_min` potentiellement indéfini dans `ComptesController::marge()` si la boucle FIFO ne s'exécute jamais.
18. `id_type_carte` forcé à 0 sans validation dans `carte()` → liste d'appareils vide sans message d'erreur.
19. Variable `$tableau` assignée puis jamais utilisée dans `PistoletIndex::pistolet()`.
20. Entrée de routage fantôme `ComptePvResultatController` (référencée dans `vendor/function.php`, fichier inexistant) — mort, seul un backup `Compte copy.php` y fait encore référence.
21. `entre`/`sortie` initialisés avec le même appel à la création d'un compte (`ComptesController::ajouter()` l.147-155) — probablement voulu mais non explicite/non commenté.

---

## 5. Code non optimisé

### Requêtes N+1 (le problème de performance n°1 du module)

| Endroit | Coût | Fréquence d'appel |
|---|---|---|
| `Encaissement::encaissement_compte()` | `types × zones` requêtes | Chaque affichage de `header()`/`principal()`/`validation()` |
| `ZonePompiste::pompiste_compte()`/`pompiste_associe()` | 1 requête/pompiste | Chaque affichage zone |
| `PistoletIndex::pistolet()`/`pistolet_compte()` | ~4 requêtes/pistolet | Chaque affichage zone |
| `Vente::vente_zone_total()`/`vente_zone()`/`vente_compte()` | requêtes dupliquées avec `pistolet()` (même pistolet requêté 2 fois) | idem |
| `CiterneFlux::citerne_zone()`/`citerne_compte()` | 1 requête SQL brute/citerne, sur **toutes** les citernes du paramétrage (pas juste celles du compte) | idem |
| 7 modèles `bons_compte_zone`/`bons_compte` (Espece, CarteBon, BonBaf, ClientBon, OntBon, ClientAvoir, Piece) | 1+ `findone()` par ligne affichée | Chaque liste de bons par type |
| `ComptePvController::encours()` (PV) | jusqu'à 5 requêtes/ticket ouvert | **Pollé toutes les 5 secondes** |
| `ComptePvController::employes()` (PV) | 1 requête/employé | Pollé toutes les 5s (Résultat) |
| `ComptesController::marge()` (FIFO) | requête avec clause `id not in (...)` qui grossit à chaque itération | O(n²) sur le nombre d'achats consommés |

### Duplication massive

- **8 méthodes d'`EncaissementController`** : squelette identique (liste/suppression/création-modification/formulaire) copié-collé, seuls les noms de champs et le modèle changent. Idem **6 méthodes de paiement PV**, en parallèle et indépendamment.
- **Logique "code séquentiel par date"** dupliquée 6 fois dans `EncaissementController` (`client`, `ont`, `bon_baf`, ×2 chacune).
- **Logique de statut clôturé/ouvert** dupliquée 3 fois dans `CompteController` (déjà divergente).
- **Logique statut/numéro de ticket PV** dupliquée 4 fois (`encours()`, `infos_ticket()`, `ticket()`, + son miroir JS `renderTickets()`).
- **JS quasi identique** entre `forms/bon.php` et `forms/ont.php` (ajout de ligne, calcul qte×prix, description encodée).
- **CSS dupliqué à l'identique** entre `Compte.php` et `Resultat.php` (PV) — dégradés/styles sticky copiés-collés au lieu de partagés.
- `index()`/`resultat()` (PV) quasi identiques (mêmes chargements compte/pv/top_bar).

### Fonctions trop longues / trop de responsabilités

- `CompteController::validation()` — 295 lignes, 4 responsabilités indépendantes dans un seul corps de fonction, aucune garantie de mutuelle exclusion des branches.
- `EncaissementController` — 788 lignes pour 8 variantes du même problème, sans factorisation.
- `ComptePvController` — 1204 lignes, un seul fichier pour cycle de vie compte + tickets + articles + 6 paiements + employés + résultat.

### Anti-pattern "totaux Page/Filtré" (règle permanente CLAUDE.md violée)

- `/Comptes` : total "Page" recalculé côté client en parsant du texte déjà formaté ; total "Filtré" via un **2ᵉ appel AJAX séparé** qui renvoie un `<script>` à exécuter — exactement l'anti-pattern interdit par la règle.
- `CompteZone/Secondaire.php` : total calculé côté PHP-vue, table pas vraiment server-side.
- `Principal.php` (Carburant) : tableaux HTML statiques, pas de DataTable du tout.
- **3 tables PV** (`ComptePvTicket::data()`, `ComptePvTicketDescription::data()`, `ComptePv::paiements()`) : aucun total filtré SQL fusionné.

---

## 6. Design / UX

**Verdict net et unanime des 4 agents : ce module n'a fait l'objet d'AUCUNE migration Phase 4.**
Zéro classe `ssm-*` trouvée dans les vues propres au module (`Comptes.php`, `Compte.php`,
`CompteZone.php`, tous les `portion/*.php`, tous les `modal/*.php`, tout `point_vente/Compte.php`/
`Resultat.php`). C'est très probablement **le module le plus en retard de toute l'application** sur
la modernisation — alors qu'il est décrit comme son cœur.

Symptômes concrets :
- AdminLTE/Bootstrap 4 brut partout (`card callout`, `btn btn-sm`, `dropdown-menu`, badges `badge-*`).
- Ancien système de thème (`$GLOBALS['style']['bg_header_modal']`) au lieu du système `ssm-*` actuel.
- Blocs `<style>` inline dupliqués fichier par fichier au lieu d'un CSS partagé.
- `window.open()` en popup séparée pour "Nouvel employé"/"Nouveau client" (PV et Carburant) au lieu
  d'une modale intégrée au flux actuel.
- Impression de ticket (PV) : document HTML généré et dupliqué côté client, hors du pipeline
  d'impression standard (`inc/impression*`) déjà en place ailleurs dans l'appli.
- Modales sans la checklist "icône/data-mode/field-icons/ssm-btn" appliquée systématiquement
  ailleurs depuis Phase 4.

---

## 7. Rechargements de page à convertir en AJAX/JSON

Liste consolidée (l'utilisateur veut **tout** en AJAX/JSON, zéro `window.location.href`/form POST
classique/`window.open` de page complète) :

| # | Endroit | Type |
|---|---|---|
| 1 | `ComptesController.php:85` — bouton action liste des comptes | `onclick=window.location.href` |
| 2 | `app/models/compte/Compte.php:206` — variante ancienne, même pattern | `onclick=window.location.href` |
| 3 | `Nouveau_compte.php:37` — **création/modification de compte** | `<form method=post>` classique → `redirect()` PHP |
| 4 | `Nouveau_compte.php:180` — suppression de compte | `onclick=window.location.href` |
| 5 | `Header_compte.php:41` — retour vers liste | `onclick=window.location.href` |
| 6 | `CompteController.php:156` (`zones()`) — bouton "voir" une zone | `onclick=window.location.href` |
| 7 | `ComptesController.php:187-196` (`total_total()`) — `<script>` renvoyé et exécuté | anti-pattern JSON |
| 8 | `Header_zone.php:72,76` — retours liste/compte | `onclick=window.location.href` ×2 |
| 9 | `Pompiste.php:13` — form vers ancien routage `index.php?page=...` | `<form>` mort mais dangereux |
| 10 | `Pompiste.php:65-69`, `forms/bon.php:594-598` — "Nouvel employé"/"Nouveau client" | `window.open()` popup page complète |
| 11 | `ComptePvController::nouveau()` — succès création compte PV | `<script>window.location.href=...</script>` en réponse AJAX |
| 12 | `Compte.php:422` (PV) / `Resultat.php:105` — bascule Compte ↔ Résultat | `<a href>` classique |
| 13 | `Compte.php:952-967` / `Resultat.php:394-409` — bouton "Quitter" | `window.location.href` (probablement acceptable tel quel, c'est une vraie sortie) |
| 14 | Impression ticket PV (`Compte.php:2379-2595`) | `window.open()` + HTML généré côté client, hors pipeline standard |

**Le point n°3 (création/modification de compte, formulaire le plus utilisé du module) et le n°11
(création de compte PV) sont les plus prioritaires** — ce sont des rechargements complets sur
l'action la plus fréquente de tout le module.

---

## 8. Stratégie de refonte

### Principes directeurs

1. **Ne rien casser en production** — ce module gère l'argent réel de la station chaque jour. Toute
   refonte se fait **module par module, en préservant le comportement métier exact**, avec des tests
   curl systématiques avant/après comme sur tous les chantiers Phase 4 précédents.
2. **Unifier avant de dupliquer davantage** — le vrai problème de fond n'est pas visuel, c'est
   l'absence d'un moteur d'encaissement générique. Corriger ça une fois profite aux 3 sous-systèmes
   (Carburant/PV/PS) au lieu de rafistoler 3 implémentations séparées indéfiniment.
3. **AJAX/JSON partout, y compris là où ça n'a jamais existé** — ce module utilise encore le pattern
   `<form> + redirect()` que le reste de l'appli a abandonné en Phase 4 ; c'est la conversion la plus
   visible et la plus demandée explicitement.
4. **Design system `ssm-*` complet** — alignement total avec le reste de l'appli (déjà fait pour ~15
   autres modules cette phase), incluant la règle permanente Total Page/Filtré.

### Phase 0 — Garde-fous avant tout (peu coûteux, haute valeur)

À faire en tout premier, indépendamment du reste, car ce sont des correctifs de sécurité/fiabilité
purs qui ne changent rien à l'UX :

- Ajouter une garde sur le dispatch dynamique (`$controller->$methode(...)`) dans les 5 points
  d'appel (Carburant) : vérifier `method_exists()` avant l'appel, logger/notifier proprement si la
  méthode n'existe pas au lieu d'un fatal error. Supprimer ou implémenter réellement `forms/tpe.php`.
- Corriger la division par zéro (répartition pompistes, `CompteZoneController::validation():178`).
- Corriger le bug de callback JS (`Compte.php:142,158` et son 3ᵉ cas) — un correctif de 3 lignes qui
  élimine un vrai bug de séquencement déjà en production.
- Ajouter un contrôle serveur du plafond de paiement sur les 5 méthodes PV qui n'en ont pas.
- Remplacer les `die(print_r(...))` de debug (`ZonePompiste::initialiser()`,
  `PistoletIndex::index_modif()`) par une gestion d'erreur cohérente avec le reste de l'appli.
- Nettoyer l'entrée de routage fantôme `ComptePvResultatController`.
- Corriger le branding en dur du ticket PV imprimé (utiliser `$station` déjà chargée mais inutilisée)
  — important en prévision du multi-tenant.

### Phase 1 — Moteur d'encaissement générique (le vrai chantier de fond)

Extraire un **moteur de paiement unique**, paramétré (nom de table, colonnes, libellé, callback de
résolution du "payeur"), réutilisable par Carburant (8 méthodes), PV (6 méthodes), et PS (à auditer,
probablement une 3ᵉ variante). Objectif concret : remplacer ~2000 lignes dupliquées à travers
`EncaissementController` + les 6 méthodes PV par une classe générique + une table de configuration
(quasi ce que `parametre_type_encaissement` fait déjà côté Carburant — à généraliser aux 2 autres).

C'est un chantier plus risqué (touche le cœur financier), donc à faire **après** avoir stabilisé le
comportement actuel avec des tests de non-régression exhaustifs (voir Vérification), pas en premier.

### Phase 2 — Conversion AJAX/JSON complète

Dans l'ordre de priorité (impact utilisateur × fréquence d'usage) :
1. Formulaire de création/modification de compte (`Nouveau_compte.php` → `/Comptes/ajouter`) — le
   plus utilisé, actuellement un vrai POST classique.
2. Création de compte PV (`ComptePvController::nouveau()`) — actuellement un `<script>` de
   redirection en réponse AJAX.
3. Tous les boutons `window.location.href` recensés en §7 (liste complète, 14 points) — remplacés
   par navigation AJAX interne (`load_portion`) ou navigation inter-page (`data-ssm-nav`/`ssm_nav_ajax`
   selon qu'on reste dans le même panel ou qu'on change de page, comme établi ailleurs dans l'appli).
4. Fusionner Total Page/Filtré en SQL pour les 4 tables identifiées (`/Comptes` liste,
   `CompteZone/Secondaire`, et les 3 tables PV) — remplace le `<script>` de `total_total()` par une
   vraie réponse JSON fusionnée à la DataTable, pattern déjà généralisé partout ailleurs
   (`ssm_table_totaux()`/`ssm_table_totaux_multi()`).
5. Remplacer les 2 `window.open()` popup ("Nouvel employé"/"Nouveau client") par des modales
   intégrées au flux courant.
6. Repenser l'impression de ticket PV pour réutiliser le pipeline `inc/impression*` existant plutôt
   que le HTML généré côté client actuel.

### Phase 3 — Élimination des N+1 et refactoring des fonctions fourre-tout

- Remplacer chaque motif `foreach + findone() par itération` par une jointure SQL unique (liste
  précise en §5 — 9 endroits identifiés, dont 2 pollés toutes les 5 secondes côté PV, donc à traiter
  en priorité dans cette phase pour réduire la charge serveur continue).
  découper `CompteController::validation()` (295 lignes, 4 responsabilités) en 4 méthodes distinctes.
- Factoriser la logique de statut clôturé/ouvert (3 copies divergentes côté Carburant) et la logique
  statut/numéro de ticket (4 copies côté PV) en un point unique réutilisé partout.
- Résoudre le bug "modification = suppression + réinsertion" (PV `espece`/`piece`/`operation`) en
  passant à un vrai `update()`, cohérent avec `carte()` et avec le nouveau Journal d'activité.

### Phase 4 — Design system `ssm-*` complet

Dernière étape (une fois le comportement fiabilisé et le flux AJAX en place), en suivant exactement
la recette déjà rodée sur les ~15 modules précédents (voir `SKILL_MODULE_VENTE.md`) :
`ssm-panel`/`ssm-panel-toolbelt` (réglage/plein écran), `ssm-table-toolbar`, DataTables avec
`ssm_table_totaux()`/`ssm_table_totaux_multi()`, modales avec icône/`data-mode`/field-icons/`ssm-btn`,
navigation `ssm-module-nav`/`ssm-tabs` pour les bascules Compte ↔ Zone ↔ Résultat au lieu des liens
classiques. Vu la taille du module (le plus gros de l'appli), prévoir de le découper en plusieurs
passes (ex: Carburant liste+détail, puis CompteZone, puis PV) plutôt qu'une seule session.

### Vérification (à chaque phase)

- `php -l` sur tous les fichiers touchés.
- Cycle complet rejoué par curl à chaque étape : création de compte → clôture zone → clôture compte
  → chaque méthode de paiement (les 8 Carburant + les 6 PV) → annulation de clôture → suppression —
  avec nettoyage des données de test après vérification (règle établie du projet).
- Comparaison explicite avant/après sur au moins 1 compte réel existant pour garantir qu'aucun
  montant/calcul n'a changé silencieusement (le risque n°1 d'une refonte de ce module est une
  régression silencieuse sur un calcul financier).

---

## Fichiers audités (référence)

**Carburant (core)** : `ComptesController.php`, `CompteController.php`, `app/models/compte/Compte.php`,
`Comptes.php`, `Compte.php` (vue), `portion/{Principal,Secondaire,Header_compte}.php`,
`modal/{Nouveau_compte,Validation_compte,Nouveau_bon,Vente,sortie_caisse,info_attachement}.php`.

**Carburant (zone)** : `CompteZoneController.php`, `app/models/compte/{CompteZone,ZonePompiste,
PistoletIndex,CiterneVerification,CiterneFlux,Bon,Espece,RetourStock,SortieCaisse}.php`,
`CompteZone.php` (vue), `portion/{Zones,Header_zone}.php`,
`modal/{Validation_zone,Pompiste,info_attachement,sortie_caisse}.php`,
`modal/forms/{bon,carte,cheque_lcn,ont,tpe,client,espece,bon_baf,sortie_avoir,retour_avoir}.php`.

**Moteur encaissement** : `EncaissementController.php` (788 lignes, intégral) + modèles associés.

**PV** : `ComptePvController.php` (1204 lignes, intégral), tous les modèles `point_vente/*`,
`Compte.php` (2608 lignes), `Resultat.php`, `portion/{top_service,periode_compte,infos_ticket}.php`,
`modal/{nouveau_ticket,nouveau,formulaire,attachement_employe,recharge}.php`,
`modal/paiement/{espece,piece,operation,carte,client,gratuite}.php`.

**Non audité dans cette passe, à couvrir avant la Phase 1** : le sous-système **PS** (`/ComptePs`,
`ComptePsController` s'il existe) — probablement une 3ᵉ implémentation parallèle du moteur de
paiement, non exploré ici.
