# Dossier de passation — ERP « Envol » (Hectare Groupe)

> **Dernière révision : 2026-07-06** (créé au SUIVI #563, HEAD prod `6d43dc5f`).
>
> **Public visé :** un repreneur SANS aucune connaissance préalable du projet — qu'il soit
> développeur (mais étranger au métier de la promotion immobilière) OU chef de projet / DSI
> non-développeur. Ce document explique **ce qu'est l'ERP, comment le code est organisé, et
> surtout OÙ CHERCHER** pour comprendre ou modifier une fonctionnalité.
>
> **Ce que ce document N'EST PAS :** il ne remplace ni `PROJECT.md` (la mémoire de travail,
> mise à jour à chaque session), ni le journal des mises à jour (nouveautés côté utilisateur),
> ni les règles `.cursor/rules/` (conventions machine). Voir la section 9 pour la carte des
> documents et la règle d'entretien de ce fichier.
>
> ⚠️ Les points marqués **[À VÉRIFIER]** sont des éléments dont l'auteur n'était pas certain à
> 100 % ; à confirmer avec Robin (DSI) ou en lisant le code cité.

---

## 1. Ce qu'est « Envol »

**Envol** est un **ERP métier interne** (pas un logiciel commercialisé) développé pour
**Hectare Groupe**, un acteur de la **promotion immobilière** qui exerce deux métiers :

- **Lotisseur** : achète du foncier, le viabilise (VRD : voirie, réseaux divers) et revend des
  terrains à bâtir.
- **Promoteur** : construit des programmes immobiliers (immeubles, maisons) et vend les
  logements sur plan (**VEFA** — Vente en l'État Futur d'Achèvement).

Le groupe exploite plusieurs **enseignes commerciales** : principalement **ENVOL** et
**HECTARE** (voir §4 « Multi-enseigne »).

### À quoi ça sert (concrètement)

L'ERP couvre le cycle de vie complet d'une opération immobilière :

- **Suivi financier** d'un programme (bilan promoteur en 5 rubriques : Terrain, VRD,
  Construction, Frais généraux, Charges directes).
- **Marchés de travaux** avec les entreprises (contrats, avenants, situations de travaux,
  retenues de garantie, compte prorata, pénalités).
- **Factures fournisseurs** : dépôt, lecture automatique (OCR par IA), circuit de validation
  interne, puis **comptabilisation dans Sage** et suivi des paiements.
- **Commercialisation** : grille de prix des lots, offres commerciales, réservations,
  actes de vente, **TMA** (travaux modificatifs demandés par l'acquéreur).
- **GED** (gestion documentaire) adossée à **SharePoint**.
- **Tâches de suivi**, **compte inter-entreprises (CIE)**, module **juridique** (en
  construction), tableau de bord, annuaire, etc.

### Ce que ça remplace : Pegao

L'ERP a été bâti pour **remplacer Pegao**, une solution externe jugée trop coûteuse
(~2 400 €/mois, essentiellement en développements spécifiques). Envol tourne pour un coût
d'infrastructure de l'ordre de ~100-135 €/mois (hébergement OVH + API Claude + Cursor). Le but
est l'**indépendance** : stack 100 % open source standard (Laravel/Vue), code sur GitHub,
données MySQL exportables, reprise possible par n'importe quelle ESN Laravel.

### Qui l'utilise

Utilisateurs internes du groupe, répartis par **groupes Microsoft 365** (`GS_*`) qui pilotent
les droits : comptabilité, direction générale, commercial par enseigne, DSI, etc. (voir §4 et
le glossaire §8). Il n'y a **pas d'accès externe** (pas de portail client à ce stade).

---

## 2. Vue d'ensemble technique — et POURQUOI c'est structuré ainsi

### La stack

| Couche | Technologie | Version |
|---|---|---|
| Backend | **Laravel** (PHP) | Laravel 13 / PHP 8.3 |
| Base de données | **MySQL** (OVH cluster mutualisé) | — |
| Pont back↔front | **Inertia.js** | `inertiajs/inertia-laravel` ^3, `@inertiajs/vue3` ^2 |
| Frontend | **Vue 3** (Composition API, `<script setup>`) | Vue ^3.5 |
| Composants UI | **PrimeVue** | ^4.5 |
| CSS | **Tailwind CSS** | ^4 |
| Build front | **Vite** | ^8 |
| Auth | **SSO Microsoft 365** via **Laravel Socialite** (driver Azure) | ^5.26 |
| Intégrations | **Microsoft Graph** (SharePoint, users, Teams), **Sage Intacct** (compta), **API Claude/Anthropic** (OCR) | — |

**Le modèle Inertia en une phrase :** il n'y a **pas d'API REST séparée + SPA**. Les
controllers Laravel renvoient des **pages Inertia** (`Inertia::render('Factures/Global', [...])`)
qui montent directement un composant Vue avec ses props. On garde donc le routage et
l'autorisation côté Laravel, tout en ayant une UI Vue réactive. Conséquence pratique : pour
comprendre un écran, on suit **route → controller → `Inertia::render('<Page>')` → composant Vue
dans `resources/js/Pages/<Page>.vue`** (voir §3).

### ⚠️ LE point le plus contre-intuitif : hébergement OVH MUTUALISÉ

C'est la chose qu'un repreneur DOIT comprendre avant de toucher à quoi que ce soit, sous peine
de **casser la prod**. La production tourne sur un **hébergement OVH mutualisé** (cluster
partagé), pas sur un serveur/VM que l'on maîtrise. Trois conséquences majeures :

1. **On ne build JAMAIS sur le serveur.** Pas de `npm run build`, pas de `composer install`,
   pas de `php artisan optimize`/`config:cache`/`route:cache`. En corollaire :
   - **`vendor/` (dépendances PHP) est COMMITÉ dans le repo.**
   - **`public/build/` (bundles Vite compilés) est COMMITÉ dans le repo.**
   - Le build Vite se fait **en local**, avant le push, et le résultat compilé est commité au
     **même commit** que les sources `.vue`. Si on oublie `public/build/`, le changement front
     **n'apparaît pas en prod**.

2. **Les sorties réseau HTTPS depuis SSH/CLI sont BLOQUÉES** sur cet hébergement (Sage Intacct,
   Microsoft Graph, API Claude, webhooks…). Donc **aucun appel externe ne peut passer par
   `php artisan` en ligne de commande**. Tout appel externe doit passer par le **web
   (PHP-FPM)**, c.-à-d. un **endpoint HTTP** (souvent `POST /admin/...`). C'est pourquoi
   chaque commande utile qui appelle une API a un **équivalent endpoint web** (voir §5).

3. **Limite d'upload PHP** plafonnée (128 Mo sur ce cluster) — impacte les gros documents/AO.

> Ces règles sont formalisées et **permanentes** dans `.cursor/rules/deploiement-ovh.mdc`.
> Un outil (Cursor) re-suggère régulièrement `npm run build` sur le serveur : **c'est une
> erreur, il faut l'ignorer.**

### Autres conventions front permanentes

- **Ziggy n'est PAS installé** : le helper `route()` côté Vue renvoie `undefined`. Depuis Vue,
  on utilise **toujours des URLs directes** (`/programmes/{id}/...`), jamais `route('...')`.
- Les **props Vue doivent être déclarées en camelCase** dans `defineProps`. Un binding
  template kebab-case (`:lots-commerciaux`) est résolu par Vue vers `lotsCommerciaux` : si
  `defineProps` déclare `lots_commerciaux` (snake_case), la prop reste vide **silencieusement**
  (bug invisible aux tests, cf. leçon #421 dans `PROJECT.md`).

---

## 3. Architecture du code / OÙ CHERCHER (section clé)

L'application suit une architecture Laravel classique **enrichie d'une couche `Services`
métier**. La règle d'or du projet :

> **La logique métier lourde vit dans des `Services` dédiés (`app/Services/`), PAS dans les
> controllers.** Les controllers restent minces : ils valident l'entrée, appellent un ou
> plusieurs services, et renvoient une page Inertia. Quand on cherche « comment est calculé
> X », on cherche presque toujours dans un `Service`.

### Carte des dossiers

| Rôle | Emplacement | Remarques |
|---|---|---|
| **Controllers HTTP** | `app/Http/Controllers/` | Un fichier par domaine (ex. `MarcheController`, `DepotFactureController`). Sous-dossiers `Admin/`, `Auth/`, `Marketing/`, `Concerns/` (traits d'autorisation partagés). |
| **Services métier** | `app/Services/` | Cœur de la logique. ~130 fichiers. Sous-dossiers `DepotFacture/`, `Tasks/`, `Dashboard/`, `Marketing/`, `Notifications/`. |
| **Modèles Eloquent** | `app/Models/` | ~107 modèles (une table = un modèle). |
| **Presenters / helpers de présentation** | `app/Support/` | ⚠️ **Les « Presenters » ne sont PAS dans un dossier `Presenters/`** : ils vivent dans `app/Support/` (ex. `FactureFriseEtapePresenter`, `TacheSuiviPresenter`, `WorkflowValidationStepPresenter`). `app/Support/` contient aussi des **résolveurs**, **filtres de liste**, **règles de visibilité** (`FactureVisibility`), **mappings de comptes**. C'est la « boîte à outils » métier sans état. |
| **FormRequests (validation)** | `app/Http/Requests/` | Peu nombreux (`FacturePdfZipRequest`, `PaiementTraiterRequest`, sous-dossiers `Juridique/`, `TacheSuivi/`…). Beaucoup de validation se fait aussi **inline** dans les controllers via `$request->validate([...])`. |
| **Policies** | `app/Policies/` | Autorisations Laravel (ex. `TacheSuiviPolicy`, `AppelOffrePolicy`). Complétées par des traits `Concerns/Authorizes*Access.php` et le `PermissionService`. |
| **Intégration Sage (XML)** | `app/Intacct/` + `app/Services/IntacctService.php` | `app/Intacct/Functions/AccountsPayable/ExtBillCreate.php` = construction d'une facture fournisseur (APBILL). Voir §5. |
| **Observers / Events / Listeners / Jobs / Notifications** | `app/Observers/`, `app/Events/`, `app/Listeners/`, `app/Jobs/`, `app/Notifications/` | Ex. `TMAObserver` recalcule les montants au save. |
| **Commandes artisan** | `app/Console/Commands/` | Backfills, rattrapages, diagnostics (ex. `RattrapageMarcheFactureCommand`, `JournalSyncCommand`). ⚠️ celles qui appellent une API externe ont un équivalent web (voir §2). |
| **Composants Vue — Pages** | `resources/js/Pages/` | **Une page = un écran** rendu par `Inertia::render()`. Organisées par domaine (`Factures/`, `Marches/`, `Commercial/`, `Cie/`, `CompteProrata/`, `Juridique/`, `MesTaches/`…). |
| **Composants Vue — réutilisables** | `resources/js/Components/` | Briques partagées (`ProgrammeHeader`, `ProgrammeFrise`, `Workflow/`, `Comptabilisation/`, `Facture/`…). Aussi `Layouts/`, `Composables/`, `utils/`, `constants/`. |
| **Routes** | `routes/web.php` (~80 Ko, l'essentiel), `routes/api.php`, `routes/console.php` | Tout le routing applicatif est dans `web.php`. |
| **Migrations** | `database/migrations/` | ~263 migrations, datées. Voir §7 pour les pièges MySQL. |
| **Données de référence** | `database/data/` + `database/seeders/` | ⚠️ `database/data/` contient notamment `journal_mises_a_jour_post_2026_06_12.php` (le journal utilisateur). Les référentiels (prestations, comptes, étapes) sont dans les **seeders** et le **code** (`app/Support/EtapesModelesEnvolCatalog.php`, `EnseignesSetup.php`, etc.). |
| **Configuration** | `config/` | Notamment `services.php` (clés externes), `feature_permissions.php` (catalogue des droits), `notifications.php`, `erp.php`, `dashboard_widgets.php`. |

### Comment retrouver le code derrière un écran (méthode générale)

Prenons un exemple : **« Je veux comprendre l'écran de comptabilisation des factures. »**

1. **Route** — chercher dans `routes/web.php` le chemin de l'écran :
   `Route::get('/factures/comptabilisation', [ComptabilisationController::class, 'index'])`.
2. **Controller** — ouvrir `app/Http/Controllers/ComptabilisationController.php`, méthode
   `index()`. Il valide les droits, charge les données (souvent via des `Service`) et termine
   par `Inertia::render('Factures/Comptabilisation/Index', [...])`.
3. **Service(s)** — la logique lourde (calcul des écritures, envoi Sage) est déléguée. Ici :
   `app/Services/DepotFacture/EcrituresComptablesCalculator.php` (calcul des lignes comptables)
   et `app/Services/IntacctService.php` (envoi vers Sage).
4. **Vue** — ouvrir `resources/js/Pages/Factures/Comptabilisation/Index.vue`, qui reçoit les
   props renvoyées par le controller.

Cette chaîne **route → controller → service(s) → page Inertia** est valable pour tout l'ERP.
Astuces de recherche :
- Depuis un **libellé d'écran**, `grep` le texte dans `resources/js/Pages/`.
- Depuis une **URL**, `grep` le chemin dans `routes/web.php` → on obtient controller + méthode.
- Depuis un **nom de règle métier** (ex. « retenue de garantie »), `grep` dans `app/Services/`
  et `app/Support/`.

### Carte « fonctionnalité → dossier »

| À l'écran… | Où chercher dans le code |
|---|---|
| Liste globale des factures | `FactureGlobaleController` → `Pages/Factures/Global.vue` ; visibilité : `app/Support/FactureVisibility.php` |
| Fiche d'une facture / workflow validation | `FactureController` (préfixe `factures/fiche`), `FactureValidationService`, `FactureWorkflow*Service` → `Pages/Factures/Show.vue` |
| Dépôt & OCR d'une facture | `DepotFactureController`, `DepotFactureClaudeAnalysisService`, `OcrFactureService` → `Pages/DepotFactures/` |
| Comptabilisation Sage | `ComptabilisationController`, `IntacctService`, `app/Intacct/…/ExtBillCreate.php` → `Pages/Factures/Comptabilisation/` |
| Marché de travaux | `MarcheController` + `Marche*Service` → `Pages/Marches/` |
| Retenues (RG/CP/OPC/pénalités) | `RetenueGarantieService`, `CompteProrataService`, `PenaliteController` ; extraction : `app/Support/LignesComptablesRetenueExtractor.php` |
| Compte prorata | `CompteProrataService`, `CompteProrataSortieService`, `CompteProrataController` → `Pages/CompteProrata/` |
| CIE (compte inter-entreprises) | `CieService`, `CieController` → `Pages/Cie/` |
| Commercialisation / lots / réservations | `CommercialisationController`, `LotCommercialController`, `ReservationController`, `AcquereurController` → `Pages/Commercial/`, `Pages/Programmes/Commercialisation.vue` |
| TMA | `TMAController`, `TmaBibliothequeController` → `Pages/Programmes/Tma.vue`, `Pages/Tma/` |
| Tâches de suivi / mes tâches | `TacheSuiviController`, `MesTachesController`, `TacheSuiviService`, `TaskSyncService`, `app/Services/Tasks/` → `Pages/TachesSuivi/`, `Pages/MesTaches/` |
| Juridique | `DemandeJuridiqueController`, `DemandeJuridiqueService` → `Pages/Juridique/` |
| Documents (GED) | `GedController`, `GedGlobalController`, `SharePointService` |
| Permissions / droits | `PermissionService`, `ProgrammeAccessService`, `config/feature_permissions.php`, `app/Policies/` |
| Admin Sage / synchros | `app/Http/Controllers/Admin/IntacctController.php`, `ServiceIntacctController` |

---

## 4. Les modules métier, un par un

Pour chaque module : à quoi ça sert, les fichiers concernés, et les **règles métier non
évidentes** (le genre d'information qu'on ne devine pas en lisant le code froid).

### 4.1 Factures & comptabilisation (Sage)

**But :** dématérialiser le circuit d'une facture fournisseur, du dépôt jusqu'au paiement,
avec comptabilisation dans **Sage Intacct**.

**Fichiers clés :**
- Controllers : `DepotFactureController` (dépôt, OCR, saisie), `FactureController` (fiche +
  validation), `ComptabilisationController` (envoi Sage), `FactureGlobaleController` (liste),
  `FactureSyncPaiementsSageController` (récup statuts paiement).
- Services : `DepotFactureEngagerWorkflowService`, `FactureValidationService`,
  `FactureWorkflowApresNv1Service`, `FactureWorkflowService`, `DepotFacture/…` (calcul des
  écritures), `IntacctService`, `DepotFactureValiderEtComptabiliserService` (raccourci « déjà
  réglée »).
- Support : `app/Support/EtatPaiementSage.php`, `FactureVisibility.php`,
  `FactureFriseEtapePresenter.php`, `LignesComptablesRetenueExtractor.php`.
- Vue : `Pages/DepotFactures/`, `Pages/Factures/Global.vue`, `Pages/Factures/Show.vue`,
  `Pages/Factures/Comptabilisation/`.

**Circuit (chaîne d'états) :**
1. **Dépôt** d'un PDF (`DepotFactureController@store`) → analyse **OCR par IA Claude**
   (`DepotFactureClaudeAnalysisService`) qui pré-remplit fournisseur, montants, programme.
2. **Engagement dans le workflow** (`DepotFactureEngagerWorkflowService`) → création d'une
   `Facture` + d'une `WorkflowValidation` niveau **NV1**.
3. **Validation NV1** (`FactureValidationService`) puis, si le programme l'exige, **NV2**
   (direction générale). Les factures de **frais généraux (FG)** n'ont **pas de NV2**.
4. **Comptabilisation** : calcul des écritures comptables, puis envoi à Sage (création d'une
   facture fournisseur **APBILL**, éventuellement suivie d'un paiement **APPYMT**).
5. **Suivi du paiement Sage** : état `etat_paiement_sage` qui progresse
   `preparation → paiement_disponible → virement_execute → paye`.

**Règles métier non évidentes :**
- **NV1 / NV2** = deux niveaux de validation. NV1 = validation « métier » (le validateur
  désigné). NV2 = validation direction générale, **conditionnelle au programme** et **absente
  pour les FG**.
- **« Déjà réglée »** (`depot_factures.deja_reglee`) : statut posé AVANT tout paiement Sage,
  pour les factures payées hors circuit. Le raccourci
  `DepotFactureValiderEtComptabiliserService` valide NV1+NV2 d'un coup (réservé
  GS_COMPTABILITE/DSI/ADMIN) et n'ouvre pas de suivi de paiement. Garde-fous : bloqué si un
  paiement existe déjà, si l'état est avancé, ou si statut = payée.
- **`etat_paiement_sage`** (voir `app/Support/EtatPaiementSage.php`) : les états ont un **rang**
  ordonné ; pour une facture multi-fournisseur, l'état consolidé prend le **minimum** des
  signaux (un signal `null` ne fait pas régresser l'état — évite les faux retours arrière).
- Les tests SQLite de l'exécutant sont souvent *skipped* (pas de `pdo_sqlite`) → **toujours
  tester la compta manuellement** après un commit touchant `DepotFactureController` /
  `ComptabilisationController`, et diagnostiquer les 500 via les logs (voir §6).

### 4.2 Marchés & retenues

**But :** gérer les contrats de travaux avec les entreprises et les diverses **retenues**
prélevées sur les situations de travaux.

**Fichiers clés :**
- Modèle : `app/Models/Marche.php` (+ `Avenant`, `MarchesSousTraitant`, `Penalite`,
  `MarcheRepriseLigne`, `RetenueGarantie`).
- Controller : `MarcheController` (+ `PenaliteController`, `MarcheRepriseLigneController`).
- Services : `MarcheMontantsAgregesService`, `MarcheRepriseMontantsService`,
  `MarcheRapprocherFactureService`, `MarcheDeplacerFactureService`,
  `FactureMarcheDoubleAncrageService`, `RetenueGarantieService`, `CompteProrataService`,
  `MontantFactureSousTraitantService`.
- Vue : `Pages/Marches/`.

**Les retenues (vocabulaire) :**
- **RG** — *Retenue de Garantie* : ~5 % retenu sur chaque situation, libéré à la fin (garantie
  de parfait achèvement). Champ `retenue_garantie_pct`.
- **CP** — *Compte Prorata* : ~1,5 % mutualisé pour les charges communes de chantier
  (nettoyage, gardiennage…). Champ `compte_prorata_pct` (voir §4.4).
- **OPC** — retenue liée à l'*Ordonnancement, Pilotage, Coordination*. Champ `retenue_opc_pct`
  (source : `taux_opc_pct` du marché).
- **Finitions** — retenue pour finitions, champ `retenue_finitions_pct`.
- **Pénalités** — retards, non-conformités… (`Penalite`, déductibles d'une facture).

**Règles métier non évidentes :**
- **La RG est la source de vérité côté LIGNES COMPTABLES, pas côté pourcentage.** Le montant
  retenu n'est **pas** recalculé `pct × montant` : il est **extrait des lignes comptables**
  de la saisie (`ocr_raw.saisie.lignes_comptables`, crédit sur le compte RG) via
  `LignesComptablesRetenueExtractor`. Le `retenue_garantie_pct` n'est stocké **que pour
  référence**. Même logique pour le CP.
- **Reconstitution du TTC brut** : pour asseoir les retenues sur une base cohérente, on
  reconstitue un TTC « brut » = HT × (1 + TVA notionnelle). En **autoliquidation** (TVA
  sous-traitant à 0 %), on utilise **quand même un taux notionnel de 20 %** pour la base de
  retenue — l'autoliquidation ne supprime que les **lignes de TVA**, pas la base des retenues.
  Voir la méthode `tauxTvaNotionnelPourBaseRetenue()` dans le calculateur d'écritures.
- **Autoliquidation sous-traitants** : quand un sous-traitant est en autoliquidation, sa
  facture porte une TVA à 0 % (c'est le donneur d'ordre qui déclare la TVA). Modélisé par
  `MarchesSousTraitant.autoliquidation` et par ventilation (`FactureVentilation`).
- **Pattern « principal à 0 € »** (ventilation multi-fournisseur) : une facture peut être
  ventilée à **100 % sur des sous-traitants**, à condition d'avoir une ligne « principal » à
  0 € pour **porter les retenues** (RG/CP/finitions/OPC/CIE) et les pénalités. Règle
  (`validateVentilationsBusinessRules`) : plus d'un principal = interdit ; zéro principal
  autorisé **seulement s'il n'y a AUCUNE retenue**.
- **Double ancrage marché↔facture** (`FactureMarcheDoubleAncrageService`) : le lien marché est
  stocké à **trois endroits** qui doivent rester alignés (`factures.marche_id`,
  `depot_factures.marche_id`, `ocr_raw.saisie.marche_id`). La priorité effective est
  `factures.marche_id`. Toute réaffectation (rapprochement / déplacement de facture)
  **resynchronise RG et CP**.

### 4.3 Commercialisation & lots

**But :** gérer la vente des logements, de la mise en stock à l'acte notarié.

**Fichiers clés :**
- Modèles : `Lot`, `LotCommercial`, `OffreCommerciale`, `Reservation` (+ `ReservationDocument`,
  `ReservationObservation`), `Contact`.
- Controllers : `CommercialisationController`, `CommercialController`, `LotCommercialController`,
  `OffreCommercialeController`, `ReservationController`, `AcquereurController`.
- Vue : `Pages/Programmes/Commercialisation.vue`, `Pages/Commercial/`, `Pages/Programmes/Acquereur/`.

**Règles métier non évidentes :**
- **Pipeline de statut du lot** (transitions **uniquement vers l'avant**, sauf annulation) :
  `disponible/en_stock → en_vente → réservé → acté → (livré, à venir)`. Les gardes sont dans
  le modèle : `peutMettreEnVente()`, `peutReserver()`, `peutActer()`, `peutAnnuler()`.
- **Prix effectif** : en stock/en vente, on affiche le prix d'**offre promo** s'il y en a une
  active (dates valides), sinon le **prix grille**. En réservé/acté, on affiche le **prix de
  vente négocié** (figé à la réservation).
- **Flux VEFA réel** (important, cf. `PROJECT.md`) : avant l'acte, **rien** ne se passe avec le
  client. À l'acte, le notaire collecte la somme due **selon l'avancement chantier**, puis
  reverse au promoteur **déduction faite de ses frais**. La créance de vente est **au nom du
  client (acquéreur), jamais du notaire** ; le notaire est un intermédiaire de flux.
- **Facture de vente / actage** : brique **cadrée mais non implémentée** (créance ARINVOICE
  Sage soldée par les appels de fonds). Le module « Préparation acte » est également **cadré
  mais non implémenté** (onglet grisé, cf. `PROJECT.md`).

### 4.4 Compte prorata

**But :** modéliser la « cagnotte » des charges communes de chantier alimentée par les
retenues CP, et suivre les sorties (dépenses imputées dessus).

**Fichiers clés :** `CompteProrataService` (entrées / KPI), `CompteProrataSortieService`
(sorties + contrôle de solde), `CompteProrataController`, modèles `CompteProrataSortie`,
`MarcheCpHistorique` → `Pages/CompteProrata/`.

**Règles métier non évidentes :**
- Deux sources d'alimentation par marché : **`en_appro`** (CP des factures comptabilisées mais
  non encore payées) et **`provisionné`** (CP des factures **payées**). Le montant CP est
  **extrait des lignes comptables** (comme la RG), pas recalculé.
- **`MarcheCpHistorique`** permet un **ajustement manuel** du provisionné par marché (avec
  auteur/date/commentaire), quand le réel diffère du calcul.
- **Soldes** : `solde cagnotte = provisionné − dépenses payées` ; `solde disponible =
  provisionné − sorties`. Une comptabilisation avec `deduire_cagnotte_cp = true` crée une
  sortie automatique et est **bloquée si le solde est insuffisant**
  (`verifierSoldeAvantComptabilisation`).

### 4.5 CIE — Compte Inter-Entreprises

**But :** gérer les **avances entre entreprises** d'un même chantier (une entreprise avance une
dépense pour le compte du groupe), avec **compensation des dettes croisées**.

**Fichiers clés :** `CieService`, `CieController`, modèles `CieOperation`, `CieRepartition`,
trait `Concerns/AuthorizesCieAccess` → `Pages/Cie/`.

**Règles métier non évidentes (dont l'opération récente #560) :**
- **Répartition en MONTANTS, pas en pourcentages**, et **bloquée sur le HT** (les montants de
  répartition se saisissent en HT ; leur somme doit égaler le montant total HT de l'opération,
  tolérance ±0,01 €).
- Le libellé UI du fournisseur qui a avancé est **« fournisseur qui facture »**.
- La liste des fournisseurs proposés est **complète** (plus filtrée).
- **Retrait du lien facture** : une opération CIE **n'est plus liée obligatoirement** à une
  facture (association devenue optionnelle).
- **Compensation des dettes croisées** (`compenserDettesCroisees`) : si A doit 100 à B et B
  doit 60 à A, on réduit à un flux net A→B de 40. Un contrôle de **cohérence** vérifie que la
  somme de tous les soldes est ~nulle.

### 4.6 Tâches de suivi

**But :** deux sous-systèmes de tâches coexistent.

- **`TacheSuivi`** = tâches de suivi **créées manuellement** par les utilisateurs (objet,
  échéance, importance, assignés, étiquettes, commentaires + @mentions, documents). Fichiers :
  `TacheSuiviService`, `TacheSuiviController`, modèle `TacheSuivi`, `TacheSuiviPolicy` →
  `Pages/TachesSuivi/`.
- **`Task`** = tâches **opérationnelles auto-générées** par les événements ERP (validation
  facture NV1/NV2, virement à exécuter…). Fichiers : `TaskSyncService`, `app/Services/Tasks/`
  (`ValidationFactureNv1TaskSync`, `ValidationFactureNv2TaskSync`, `VirementAExecuterTaskSync`…),
  modèles `Task`, `TaskType`.
- Vue combinée : `MesTachesController` → `Pages/MesTaches/`.

**Règles métier non évidentes :**
- Les `Task` opérationnelles se **ferment automatiquement** sur l'action ERP correspondante
  (validation/rejet/paiement) ; les `TacheSuivi` se ferment **manuellement**.
- **Idempotence** : chaque `Task` a une `idempotency_key` (type + morph du taskable) pour
  éviter les doublons lors des re-synchros.
- **Assignation « virement à exécuter »** par enseigne (config `notifications.php`, jamais en
  dur). **AUCUNE tâche** pour les FG (return early si pas de programme/enseigne).
- ⚠️ `ensureUserFromGraph` : la résolution d'un utilisateur via Graph fait un appel HTTPS →
  **bloqué en CLI** ; d'où la résolution **par email en base d'abord**. Attention lors des
  backfills en SSH (voir §6).

### 4.7 Juridique — ⚠️ MODULE EN CONSTRUCTION

> **État : socle de récolte livré, qualification à venir.** Le formulaire de dépôt d'une
> demande juridique, le stockage (pièces jointes), la liste, la fiche et la notification à la
> soumission **existent**. En revanche, le **workflow de qualification / traitement**
> (transitions de statut au-delà de `a_qualifier`, affectation à une équipe, note d'enjeu,
> liaison à un dossier externe, SLA) **n'est pas encore développé**.

**Fichiers clés :** `DemandeJuridiqueService`, `DemandeJuridiqueController`, modèle
`DemandeJuridique` (⚠️ `protected $table = 'demandes_juridiques'` explicite — piège de
pluralisation FR, voir §7), `app/Http/Requests/Juridique/`, `app/Support/Juridique/` →
`Pages/Juridique/`.

### 4.8 Multi-enseigne : enseigne applicative vs entité Sage

C'est une distinction **fondamentale** à ne pas confondre :

- **Enseigne (applicative)** — `app/Models/Enseigne.php`, `programmes.enseigne_id`. C'est la
  **marque commerciale** (ENVOL, HECTARE) qui pilote la **segmentation applicative** : couleurs,
  modules actifs, et surtout la **visibilité** (un membre `GS_ENVOL` voit les programmes
  d'enseigne ENVOL). Un programme appartient à **exactement une** enseigne.
- **Entité (Sage / Intacct)** — `app/Models/Entite.php`, clé `intacct_id`. C'est l'**entité
  comptable/légale** dans Sage, sur laquelle sont **postées les écritures** (`programmes.entite_id`
  et `factures.entite_id`). Codes réels : **ENVOL → `E-13`**, **HECTARE → `E-04`**.

En résumé : **enseigne = « qui voit / quelle marque » (côté appli)** ; **entité = « où c'est
comptabilisé » (côté Sage)**. Les deux sont liés (l'enseigne a une entité par défaut) mais
répondent à deux questions différentes. `IntacctFinancialEntity` est un cache de référentiel
Sage (comptes, banques…).

### 4.9 Matrice de visibilité des factures

Qui voit quelles factures est centralisé dans `app/Support/FactureVisibility.php` (méthodes
`applyScope()`, `userCanAccessFacturesPage()`, etc.). Logique (par ordre de priorité) :

| Profil (groupe M365) | Périmètre visible |
|---|---|
| `GS_ADMIN_ERP`, `GS_DSI`, `GS_DIRECTION_GENERALE`, `GS_COMPTABILITE` | **Toutes** les factures/dépôts (socle total) |
| `GS_ENVOL` / `GS_HECTARE` | Les factures dont le programme est de **leur enseigne** (+ celles où ils sont validateur NV1/NV2) |
| Auteur du dépôt | **Ses propres** dépôts |
| Validateur désigné (NV1/NV2) | Les factures qu'il doit **valider** (résolu aussi via `ocr_raw.saisie.validateur_id`/`_email`) |
| Permission `factures.lecture` | Accès lecture selon le catalogue de droits |

Le périmètre **programme** (distinct de la visibilité facture) est géré par
`ProgrammeAccessService` (rôles transverses + `ProgrammeResponsable` + enseigne).

---

## 5. Intégrations / connecteurs

> ⚠️ **SÉCURITÉ.** Ce document ne cite que les **noms** des variables de configuration. Les
> **valeurs** (mots de passe, clés, tokens, `APP_KEY`, secret client Azure…) vivent dans le
> fichier **`.env`** (jamais commité) et **ne doivent JAMAIS être recopiées** ici ni ailleurs.
> Toutes les clés sont lues via `config/services.php` (`env(...)`). Pour le renouvellement d'un
> secret, suivre la procédure standard de la plateforme concernée — **[À VÉRIFIER avec
> l'admin]** dans chaque cas.

**Rappel transverse :** aucun de ces connecteurs ne peut fonctionner en **CLI** (`php artisan`)
sur OVH, car les sorties HTTPS sont bloquées (voir §2). **Tous** passent par le **web
(PHP-FPM)** : soit un endpoint utilisateur (login, upload, dépôt facture), soit un endpoint
admin (`POST /admin/...`).

### 5.1 Sage Intacct (comptabilité)

- **Rôle :** synchroniser les fournisseurs (objet **VENDOR**), pousser les factures
  fournisseurs (**APBILL**) et les paiements (**APPYMT**), lire les statuts de paiement,
  synchroniser comptes/classes/entités.
- **Où est le code :** `app/Services/IntacctService.php` (client XML + gestion de session),
  `app/Intacct/Functions/AccountsPayable/ExtBillCreate.php` (construction APBILL),
  `IntacctPaiementStatusService`, `ApBillVendorIdResolver`, controllers
  `app/Http/Controllers/Admin/IntacctController.php` et `ServiceIntacctController`.
- **Comment c'est câblé :** API **XML** vers `INTACCT_ENDPOINT`
  (`https://api.intacct.com/ia/xml/xmlgw.phtml`). Authentification par identifiants
  expéditeur + utilisateur ; **session mise en cache** (~30 min). Session **top-level** pour les
  lectures globales (fournisseurs, classes…), et **session scopée par entité** (`locationid`,
  ex. `E-04`) pour les écritures fournisseur/factures — car les VENDOR sont partagés au niveau
  société mais les écritures sont par entité.
- **Endpoints web qui l'appellent :** `POST /admin/intacct/sync-fournisseurs`,
  `.../sync-classes`, `.../sync-comptes`, `.../sync-entites`, etc. ; envoi d'une facture :
  `POST /factures/comptabilisation/{uuid}/envoyer-intacct*`.
- **Config (noms) :** `INTACCT_SENDER_ID`, `INTACCT_SENDER_PASSWORD`, `INTACCT_COMPANY_ID`,
  `INTACCT_USER_ID`, `INTACCT_USER_PASSWORD`, `INTACCT_ENDPOINT`
  (bloc `config('services.intacct.*')`), plus divers réglages `config('erp.intacct.*')`.
- **Pièges Sage documentés :** ne jamais envoyer `<VENDORCONTACTS></VENDORCONTACTS>` vide
  (supprime les listes) ; l'update est partiel (seuls les champs envoyés changent) ; le
  `TAXID` est chiffré en base et **non relisible** par l'API.

### 5.2 Microsoft 365 / Graph

- **Rôle :** **SSO Entra ID** (connexion), **SharePoint** (GED des programmes), recherche
  d'utilisateurs du tenant (pour les @mentions), **Teams** (notifications, via Power Automate).
- **Où est le code :** `app/Services/GraphApiService.php`, `SharePointService.php`,
  `Microsoft365GroupService.php`, `TeamsNotificationService.php`,
  `GraphUserSearchController`, `app/Http/Controllers/Auth/` (callback Socialite),
  `GedController` / `GedGlobalController`.
- **Comment c'est câblé :** **Laravel Socialite** (driver Azure) pour l'OAuth Entra ID ;
  l'application récupère les **groupes M365** de l'utilisateur (les fameux `GS_*`) et les
  stocke → ils pilotent les droits. La GED lit/écrit dans **SharePoint** via l'API Graph
  (`/v1.0/sites/{siteId}/drives/...`). Teams est notifié via un **webhook Power Automate**
  (l'app native « HECTARION » de notification Teams étant, à date, bloquée par la propagation
  Microsoft — cf. `PROJECT.md`).
- **Endpoints web :** `/login`, `/auth/microsoft/callback`, `/api/graph/users/search`,
  `/ged`, `/programmes/{id}/documents...`.
- **Config (noms) :** `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`, `AZURE_TENANT_ID`,
  `AZURE_REDIRECT_URI`, `AZURE_MAIL_EXPEDITEUR`, `AZURE_GROUP_*` (mapping des groupes),
  `SHAREPOINT_SITE_ID`, `SHAREPOINT_DRIVE_NAME`, `SHAREPOINT_ALERT_EMAILS`,
  `TEAMS_WEBHOOK_URL` (bloc `config('services.azure.*')`, `.sharepoint.*`, `.teams.*`).

### 5.3 API Claude (Anthropic) — OCR des factures

- **Rôle :** **lecture automatique des factures** déposées (extraction fournisseur, montants,
  dates, déduction du programme/entité), et extraction de rapports **NF Habitat**.
- **Où est le code :** `app/Services/OcrFactureService.php` (modèle
  `claude-haiku-4-5-20251001`), `app/Services/DepotFactureClaudeAnalysisService.php` (modèle
  `claude-sonnet-4-5`), `app/Services/NfHabitatClaudeExtractionService.php` (`claude-sonnet-4-5`).
- **Comment c'est câblé :** appels HTTP vers `https://api.anthropic.com/v1/messages`, le PDF
  étant encodé en base64 et envoyé avec un schéma JSON attendu en sortie.
- **Endpoint web qui l'appelle :** `POST /depot-factures` (analyse automatique à l'upload) et
  `POST /depot-factures/{uuid}/analyser` (relance manuelle). **Toujours via PHP-FPM**, jamais
  en CLI (sortie HTTPS bloquée).
- **Config (nom) :** `ANTHROPIC_API_KEY` (`config('services.anthropic.key')`).

---

## 6. Déployer & débugger

### Séquence de déploiement OVH standard

Robin (DSI) déploie en SSH sur le serveur OVH. **Le repreneur ne déploie pas à la place de
Robin** ; cette section décrit la séquence pour comprendre.

```bash
cd /home/hectare-envol/envol.hectare.fr        # [À VÉRIFIER] chemin exact du webroot
git pull origin main
php artisan optimize:clear
php artisan view:clear
# UNIQUEMENT si des routes ont été ajoutées/modifiées :
php artisan route:clear
# UNIQUEMENT si une migration a été ajoutée :
php artisan migrate --force
# UNIQUEMENT si une entrée de journal a été ajoutée :
php artisan journal:sync
# puis, côté navigateur : hard refresh (Ctrl+F5)
```

**Rappels impératifs :**
- **JAMAIS** `npm run build`, `composer install`, `optimize`, `config:cache`, `route:cache`,
  `view:cache` sur le serveur (voir §2). `vendor/` et `public/build/` sont commités.
- Vérifier le hash du bundle Vite dans `public/build/manifest.json` après un changement front.
- Avant de déployer, **prouver que le push est bien parti** :
  `git log --oneline -1 origin/main` doit montrer le commit attendu (règle
  `.cursor/rules/preuve-de-push.mdc`).

### Lire un 500 en prod

Les erreurs fatales (HTTP 500) — souvent un `use`/trait manquant, une redéclaration, une
relation Eloquent inexistante — ne sont **pas** détectées par `php -l` ni par les tests SQLite
skippés. Pour les diagnostiquer :

```bash
grep "production.ERROR" storage/logs/laravel.log | tail -n 30
```

En amont, la règle du projet est de faire, avant chaque push : `php -l` sur chaque `.php`
modifié **+** une instanciation via `ReflectionClass` (voir §7 et
`.cursor/rules/verifs-avant-push.mdc`).

---

## 7. Pièges connus (MySQL / Eloquent / vérifs)

Version expliquée de `.cursor/rules/pieges-mysql-eloquent.mdc` et
`.cursor/rules/verifs-avant-push.mdc` :

1. **DDL hors transaction.** Ne **jamais** wrapper du DDL (`CREATE TABLE`, `ALTER TABLE`) dans
   `DB::transaction()` : MySQL fait un **commit implicite** sur le DDL → erreur « no active
   transaction ». Les migrations de structure ne se mettent pas en transaction.

2. **Clés étrangères sur tables au nom long.** Le nom d'index/FK auto-généré par Laravel peut
   dépasser la **limite MySQL de 64 caractères** (erreur 1059 « Identifier name too long »). Sur
   une table au nom long, **nommer explicitement et court** les contraintes FK (ex. l'index
   `fcm_commentaire_user_idx` au #453).

3. **`$table` explicite pour les noms français.** Laravel pluralise « à l'anglaise » (ajoute un
   « s » au dernier mot seulement). Un modèle au nom français se pluralise mal :
   `DemandeJuridique` → Laravel devinerait `demande_juridiques`, alors que la table est
   `demandes_juridiques`. **Toujours déclarer `protected $table = '...'`** dans ce cas.

4. **Relations Eloquent réellement déclarées.** Toute relation chargée en `with()` / `load()`
   doit exister **vraiment** sur le modèle. Une `RelationNotFoundException` est un **fatal
   runtime** invisible au lint et souvent aux tests.

5. **Colonnes préfixées dans les requêtes avec JOIN.** Préfixer **toutes** les colonnes des
   clauses `WHERE`/scope par le nom de table pour éviter « Column is ambiguous ».

6. **`php -l` ne suffit pas.** Il ne détecte **pas** un `use`/import de trait manquant, un
   namespace erroné, une redéclaration, une relation inexistante. **Toujours compléter** par un
   `ReflectionClass` + instanciation sur chaque classe modifiée (controller, modèle, service).
   Les tests SQLite étant souvent *skipped*, ils ne rattrapent pas ces erreurs.

7. **Front :** un commentaire mal placé dans un `<template>` Vue peut **casser le build** sans
   casser la syntaxe JS → toujours **rebuild Vite en local** après une modif `.vue` et
   committer `public/build/` au même commit.

---

## 8. Glossaire

| Terme | Signification |
|---|---|
| **Envol** | Nom de l'ERP interne (ce projet). |
| **Pegao** | Ancienne solution externe remplacée par Envol. |
| **VEFA** | Vente en l'État Futur d'Achèvement (vente sur plan). |
| **Lotisseur / Promoteur** | Les deux métiers de Hectare Groupe (voir §1). |
| **Enseigne** | Marque commerciale applicative (ENVOL, HECTARE) — segmentation & visibilité. |
| **Entité** | Entité comptable/légale Sage (Intacct). ENVOL → `E-13`, HECTARE → `E-04`. |
| **Marché** | Contrat de travaux passé avec une entreprise. |
| **Avenant** | Modification d'un marché (montant/délai). |
| **Situation (de travaux)** | Facturation d'avancement d'un marché. |
| **RG** | Retenue de Garantie (~5 %, libérée à la fin des travaux). |
| **CP** | Compte Prorata (~1,5 %, charges communes de chantier). |
| **OPC** | Ordonnancement, Pilotage, Coordination (et sa retenue). |
| **Pénalité** | Somme déduite (retard, non-conformité…). |
| **Autoliquidation** | TVA déclarée par le donneur d'ordre (facture sous-traitant à 0 % TVA). |
| **CIE** | Compte Inter-Entreprises (avances entre entreprises d'un chantier). |
| **TMA** | Travaux Modificatifs Acquéreurs (modifs demandées par l'acheteur). |
| **TS** | Travaux Supplémentaires (proches des TMA). |
| **GFA** | Garantie Financière d'Achèvement (garantie bancaire VEFA). |
| **GED** | Gestion Électronique de Documents (ici adossée à SharePoint). |
| **NV1 / NV2** | Niveaux de validation d'une facture (métier / direction générale). |
| **FG** | Frais Généraux (factures sans programme/enseigne, pas de NV2). |
| **APBILL / APPYMT** | Objets Sage : facture fournisseur / paiement fournisseur. |
| **VENDOR** | Objet Sage : fournisseur. |
| **ARINVOICE / ARPAYMENT** | Objets Sage : facture client / encaissement client. |
| **`etat_paiement_sage`** | Suivi du paiement (`preparation → paiement_disponible → virement_execute → paye`). |
| **GS_\*** | Groupes Microsoft 365 pilotant les droits (`GS_ADMIN_ERP`, `GS_DSI`, `GS_COMPTABILITE`, `GS_DIRECTION_GENERALE`, `GS_ENVOL`, `GS_HECTARE`, `GS_COMMUNICATION`…). |
| **Entra ID** | Annuaire d'identité Microsoft (ex-Azure AD), fournit le SSO. |
| **Inertia** | Pont Laravel↔Vue (pages Vue rendues par les controllers, sans API REST séparée). |
| **OVH mutualisé** | Hébergement partagé de la prod (contraintes fortes, voir §2). |
| **AO** | Appel d'Offres (module consultations/devis). |
| **SRU** | Loi SRU — délai de rétractation de l'acquéreur (suivi dans les réservations). |

---

## 9. Où trouver quoi & entretien du document

### Carte des documents du repo

| Document | Rôle | Cadence de mise à jour |
|---|---|---|
| **`docs/PASSATION.md`** (ce fichier) | **Architecture stable** : comprendre l'ERP et où chercher. | **Rarement** — seulement quand un **module majeur** ou une **intégration** change (pas à chaque lot). Mettre à jour la ligne « dernière révision » en tête. |
| **`PROJECT.md`** | **Mémoire de travail** : contexte de chaque session, décisions, SUIVI #NNN, bugs, cadrages en cours. | À chaque session. C'est le fichier à coller en début de session. |
| `database/data/journal_mises_a_jour_post_2026_06_12.php` | **Journal des nouveautés côté UTILISATEUR** (vulgarisé). Affiché dans l'appli. | À chaque changement visible utilisateur (règle #412), dans le même commit, puis `php artisan journal:sync` au déploiement. |
| `.cursor/rules/*.mdc` | **Conventions machine** (OVH, vérifs avant push, preuve de push, pièges MySQL, journal). Lues par l'assistant de code. | Rarement — quand une convention permanente évolue. |
| `README.md` | Présentation courte du repo. | Rarement. |
| `docs/` (autres) | Notes ponctuelles (intégrations, droits, dashboard, étapes de refactor). | Ad hoc. |

### Distinction essentielle à garder en tête

- **PASSATION.md décrit l'ARCHITECTURE** (stable dans le temps).
- **PROJECT.md décrit l'AVANCEMENT** (vivant, mis à jour en continu).
- **Le journal décrit les NOUVEAUTÉS UTILISATEUR** (langage non technique).

Ne pas transformer PASSATION.md en journal de bord : s'il faut le mettre à jour à chaque lot,
c'est qu'on y a mis du contenu qui aurait dû aller dans `PROJECT.md`.

---

> **Fin du dossier de passation.** Points à faire confirmer par Robin : tous les blocs marqués
> **[À VÉRIFIER]** ci-dessus (chemin webroot OVH, noms exacts de quelques classes/modèles
> `EcrituresComptablesCalculator` et `Reservation`, statut précis du module « Préparation
> acte »).
