---
suivi: 1230
date: 2026-08-06
sujet: Diag refonte arborescence SharePoint CHANTIER ENVOL
chantier: sharepoint / suivi-chantier / AO
type: diagnostic
statut: poussé
hash: f7a8856c
fichiers:
  - tools/diag/diag_arborescence_chantier_1230.php
  - docs/suivi/SUIVI_1230_diag_arborescence_chantier.md
---

# SUIVI #1230 — PHASE 1 DIAGNOSTIC — arborescence CHANTIER ENVOL

## PROMPT ENVOYÉ

Diag lecture seule avant refonte SharePoint du dossier CHANTIER ENVOL.
Décisions Robin déjà prises (classification unique `4_PRESTATAIRES`, suppression
`02_FACTURES` / `4_TAXES` / doublons AO, convergence lotissement). Objets : module
AO/DCE, mesures documents, résolution par nom, stratégie migration (web, pas
artisan), création paresseuse, nommage, comparaison HECTARE, découpage lots.
Script OVH sans Graph. Aucun code métier / migration / Vue / rebuild.

## FICHES LUES

- `#1098` — structure marché lotissement sous `TRAVAUX/4_PRESTATAIRES` ; LEÇON :
  ne pas détecter le métier via l’état SharePoint.
- `#1223` — `lie_aux_marches` / `est_concessionnaire` **NON FIABLES** →
  `resolveMarcheClassification()` doit disparaître (décision Robin).
- `#1226` — inventaire dépôts ; AO/DCE via `AoDocumentService`.
- `#1228` / `#1229` — hors périmètre (disque public / limite upload) ; DCE 500 Mo.
- `#1063` — CLI OVH ne sort pas vers Graph ; rebuild AO mentionné.

## ARBORESCENCE CIBLE (reconstituée des décisions Robin)

Périmètre = **uniquement** la racine chantier (autres racines programme
intouchées). Coquille retenue `4_DOC_LIBRES` (pas `4 DOC_LIBRES`).

```
CHANTIER/                         ← ou 05_SUIVI_CHANTIER (à trancher, objet 3)
├── 1_DCE/
├── 2_AO/
│   └── {num_ao}/                 ← aujourd’hui : AO/{AO_Ymd}/ (pas num_ao)
├── 3_DOSSIER_MARCHE/             ← ex DOSSIER_MARCHE (remonté ; aujourd’hui vide)
├── 4_PRESTATAIRES/               ← fusion 1_PRESTATAIRES + 2_ENTREPRISES + 3_CONCESSIONNAIRES
│   └── {POSTE}_{FOURNISSEUR}/    ← inchangé (libellé poste budgétaire)
│       ├── 1_ADMINISTRATIF/              ← ex 00_PIECES_ADMINISTRATIVES
│       ├── 2_CONTRATS_ET_AVENANTS/       ← ex 01_CONTRAT_AVENANTS
│       ├── 3_SUIVI_OPERATIONNEL/
│       │   ├── COMPTES_RENDUS
│       │   ├── COURRIERS
│       │   ├── MISES_EN_DEMEURE
│       │   ├── CONSTATS
│       │   ├── RAPPORTS_TECHNIQUES
│       │   └── AUTRE
│       └── 4_DOC_LIBRES/                 ← absorbe 03_DOCUMENTS + 04_JURIDIQUE
└── 5_PHOTOS/                     ← 5e racine probable (4_TAXES supprimé)
```

Supprimés : `PRESTATAIRES` (doublon), `02_FACTURES`, `4_TAXES`, `03_DOCUMENTS`,
`04_JURIDIQUE`, `SOUS_TRAITANTS` (ENVOL), classification triple.
`resolveMarcheClassification()` : **plus d’objet**.

---

## OBJET 1 — MODULE AO / DCE

### Tables réelles (pas `ao_documents`)

| Table | Rôle fichier / SP | Colonnes clés |
|-------|-------------------|---------------|
| `appels_offres` | DCE + estimatif MOE | `dce_local_path`, `dce_sharepoint_url`, `estimatif_moe_*`, `sharepoint_folder_url` |
| `ao_devis` | PDF devis | `devis_pdf_local_path`, `devis_pdf_sharepoint_url` (**pas** `sharepoint_item_id`) |
| `ao_lots` / `ao_consultations` | métier | **aucun** champ fichier |

Volumes / taux de remplissage → **sortie script OVH §1**.

### Où le module dépose

| Couche | Chemin |
|--------|--------|
| Local | `storage/app/private/dce/ao_{id}/DCE.zip`, `estimatif_moe/ao_{id}/…`, `devis/ao_{id}/…` |
| SharePoint | `{programme}/05_SUIVI_CHANTIER/AO/AO_{YYYYMMDD}/` (date = `date_lancement`) |

Le fichier s’appelle `DCE.zip` mais vit sous **`AO/AO_{date}/`**, **pas** sous le
dossier tête `DCE/`.

### `AO_SUIVI_SUBFOLDERS` = `['DCE','AO','DOSSIER_MARCHE','PRESTATAIRES']`

| Dossier | Créé ? | Alimenté par le code ? |
|---------|--------|------------------------|
| `AO/` | oui | **oui** — seul dépôt AO |
| `DCE/` | oui | **non** (placeholder) |
| `DOSSIER_MARCHE/` | oui | **non** |
| `PRESTATAIRES/` (sans préfixe) | oui | **non** (≠ `1_PRESTATAIRES` marchés) |

Mesure réelle « peuplé ou vide » = inventaire GED (objet 2) + URLs en base (§3 script).

### `RebuildSharePointAoStructure`

- Commande : `sharepoint:rebuild-ao-structure {--programme=}`
- Action : `ensureAoStructureForProgramme` (assure les 4 placeholders) — **pas** les
  `AO_Ymd`, **pas** d’upload.
- **Non planifiée** (`routes/console.php`). Sur OVH CLI : Graph **bloqué** → inutile
  en SSH. Après refonte : à **réécrire ou supprimer** (nouveaux noms `1_DCE` / `2_AO`…).

### `2_AO/{num_ao}/` et `dossier.zip`

| Attendu cible | Réalité code |
|---------------|--------------|
| `{num_ao}` | **Absent** — dossier = `AO_{Ymd}` ; métier = `reference` (`AO-2026-001`) |
| `dossier.zip` consolidé | **Absent** — ZIP = fichier **uploadé** `DCE.zip` |
| Un dossier par AO | Oui, mais daté (collision si 2 AO même jour) |

→ `1_DCE` / `2_AO` / `3_DOSSIER_MARCHE` = **refonte du module**, pas un simple
renommage des placeholders.

### `3_DOSSIER_MARCHE`

Aucun objet métier ne l’alimente aujourd’hui. Les marchés vivent sous
`1_PRESTATAIRES|2_ENTREPRISES|3_CONCESSIONNAIRES/{POSTE}_{FOURNISSEUR}/`.
Lien AO ↔ marché : **manuel** (pas de FK). À définir : contenu futur de
`3_DOSSIER_MARCHE` (consultation attribuée ? pièce libre ?).

---

## OBJET 2 — CE QUI BOUGE (mesures)

### Acquis Robin (prod)

- 79 docs avec `sharepoint_url` sous `05_SUIVI_CHANTIER` : **68** `1_PRESTATAIRES`,
  **11** `2_ENTREPRISES` (dont 1 avenant), **0** `3_CONCESSIONNAIRES`.
- Pattern : `…/05_SUIVI_CHANTIER/{classif}/{POSTE}_{FOURNISSEUR}/{sous}/`
- `reservation_documents` : 1 128 avec `sharepoint_subfolder` → `04_ACQUEREURS`
  (**hors périmètre**).

### À compléter (script §3–§5)

Répartition des 79 par sous-dossier (`00_PIECES…` … `04_JURIDIQUE`) ; nb programmes
ENVOL avec `sharepoint_item_id` ; marchés / programme.

### Inventaire manuel (risque n°1)

Oui : `SharePointGedExplorer` → `GET /programmes/{id}/documents/contenu?item=`
(`GedController::contenu` → `listerContenuDossier` par **item id**).

**Proposition** (lot séparé, route web) :

1. Pour chaque programme ENVOL avec racine SP, lister enfants de `05_SUIVI_CHANTIER`.
2. Pour `4_TAXES`, `5_PHOTOS`, `DCE`, `AO`, `DOSSIER_MARCHE`, `PRESTATAIRES` (et
   classifications) : compter enfants + taille / date.
3. Exporter CSV : programme, dossier, nb items, peuplé_manuel (pas dans ERP).
4. **Ne supprimer aucun dossier peuplé** sans validation Robin ligne à ligne.

---

## OBJET 3 — RÉSOLUTION PAR NOM (extrait exhaustif code)

| Fichier:ligne | Nom | But | Cible |
|---------------|-----|-----|-------|
| `SharePointArborescenceStructures.php:57-68` | `05_SUIVI_CHANTIER` + 9 enfants | déclaration création programme | refonte déclaration |
| `SharePointService.php:34-39` | `AO_SUIVI_SUBFOLDERS` | ensure AO | → `1_DCE`/`2_AO`/… |
| `SharePointService.php:1704` | `05_SUIVI_CHANTIER` | find marché VEFA | racine |
| `SharePointService.php:1715-1719` | `1_`/`2_`/`3_` classif | create marché | → `4_PRESTATAIRES` ; supprimer `resolveMarcheClassification` |
| `SharePointService.php:1753-1767` | `00_`…`04_` | create sous-marché | renommages / suppressions |
| `SharePointService.php:3332-3592` | `05_SUIVI` + `AO` + `AO_Ymd` | AO path | refonte `2_AO/{ref}` |
| `RebuildSharePointAoStructure.php` | idem | rebuild | à adapter |
| `MarcheController.php` | via IDs + `01_CONTRAT` UI | upload / migrate PJ | IDs OK ; messages UI |
| `CommercialisationController.php:135` | `03_COMMERCIAL` | hors périmètre | — |
| `TMAController.php:725` | `05_TMA` (acquéreur) | hors périmètre | — |
| Lotissement `1827+` / `structureMarcheLotissement` | `TRAVAUX`/`4_PRESTATAIRES`/… | HECTARE | modèle cible marché |

### Racine `CHANTIER` vs `05_SUIVI_CHANTIER`

| Option | Coût code | Coût docs (79) | Raccourcis utilisateurs |
|--------|-----------|----------------|-------------------------|
| Renommer racine → `CHANTIER` | ~10+ find-by-name + déclaration + rebuild AO | 0 si move Graph par **item id** ; **webUrl** stockées path-dépendantes → risque | **Tous** les raccourcis racine cassés |
| Garder `05_SUIVI_CHANTIER`, refondre l’intérieur | moins de points racine | 79 docs : move interne | raccourcis racine OK ; enfants cassés |

**Recommandation** : garder `05_SUIVI_CHANTIER` pour la phase 1 (intérieur seulement) ;
renommer la racine en lot séparé + com’ utilisateurs si Robin insiste sur le libellé.

---

## OBJET 4 — STRATÉGIE MIGRATION (écrite, pas exécutée)

### `sharepoint_item_id` vs rename

- Dossiers marché : IDs (`sharepoint_folder_id`, `_pieces_admin`, `_contrat`) —
  **rename Graph ne casse pas** les uploads futurs.
- Fichiers : `sharepoint_item_id` stable au move ; **mais** download marché utilise
  surtout `sharepoint_url` (webUrl) → **peut casser** si path dans l’URL.
  Mitigation : après move, rafraîchir `sharepoint_url` depuis Graph, ou download par
  item id (évolution code).
- AO : souvent **pas** d’item_id → URL seule → plus fragile.

### Raccourcis bureaux utilisateurs

Communication préalable + période dual (anciens dossiers en lecture seule /
redirection manuelle SharePoint si dispo) + checklist « recréer raccourci ».
Pas de redirect HTTP côté ERP pour les raccourcis OneDrive.

### Trois natures

| Opération | Exemple | Volume indicatif |
|-----------|---------|------------------|
| Renommage | `00_PIECES` → `1_ADMINISTRATIF` | majorité des 68 |
| Déplacement | `2_ENTREPRISES/…` → `4_PRESTATAIRES/…` | 11 |
| Suppression | `02_FACTURES`, `DCE/` vide, `4_TAXES` | après inventaire manuel |

### Script futur

- **SIMULATION** défaut ; `--apply` explicite (#1191).
- **Route web** (comme #1073 / #1109), **pas** artisan (Graph bloqué en CLI).
- Progression : job sync sous PHP-FPM **ou** endpoint chunked (N dossiers / requête)
  + table de suivi / log ; rejouable (#1099).
- Ordre : inventaire manuel → créer cibles → move/rename item id → refresh URLs →
  dual-read → supprimer sources vides.
- Réversibilité : journal JSONL (id, from, to, hash) ; reverse script web.

---

## OBJET 5 — CRÉATION PARESSEUSE

`ensureFolder` crée à la demande (409 → find). **Mais** `creerDossierMarcheVefa`
appelle `ensureFolder` sur **tous** les sous-dossiers (`00_`…`04_`) à la création
du marché → **création immédiate**, pas paresseuse.

**Recommandation** :

- Immédiat : 5 racines chantier + dossier `{POSTE}_{FOURNISSEUR}` +
  `1_ADMINISTRATIF` + `2_CONTRATS_ET_AVENANTS` + `4_DOC_LIBRES` (besoins ERP).
- Paresseux : les 6 enfants de `3_SUIVI_OPERATIONNEL` (sinon 171×6 dossiers vides).
- Compromis manuel : bouton GED « Créer les dossiers opérationnels » ou création
  au premier dépôt GED dans ce marché.

---

## OBJET 6 — NOMMAGE FICHIERS

### Marché VEFA actuel

`buildMarcheDocumentFileName` → `{TYPE}.ext` (`RC_PRO.pdf`, …).
`persistDocumentUpload` : si destination existe → **unlink puis replace**
(`MarcheController.php:2236-2237`). Un seul fichier disque par type ; DB peut
avoir plusieurs lignes (script §4).

### `{objet}` / `{AAAA_MM_JJ}` (suivi opérationnel)

**Non gérés par l’ERP** aujourd’hui (dossiers créés vides côté lotissement ;
ENVOL n’a pas encore `3_SUIVI_OPERATIONNEL`). Convention **non imposable** tant
que dépôt = manuel GED. L’ERP ne peut garantir le double underscore que pour les
uploads qu’il pilote.

### Normalisation

`normalizeMarcheSegment` (VEFA) : `ascii` + `str_replace(' ','_')` — **ne collapse
pas** les espaces multiples, **ne strip pas** hors A-Z0-9 (contrairement à
`normalizeDynamicFolderName` lotissement). Espace résiduel possible (NBSP /
création manuelle) → ex. `…_A _ARCHITECTURE`. `SANS_PRESTATION_*` **absent du
code** (manuel ou legacy). Ne pas corriger dans ce lot.

---

## OBJET 7 — LOTISSEMENT

Cible ENVOL ≈ `structureMarcheLotissement()` (#1098).

| Écart résiduel | |
|----------------|--|
| Racine | ENVOL : sous CHANTIER ; HECTARE : sous `TRAVAUX` |
| `1_DCE`/`2_AO`/`3_DOSSIER_MARCHE` | ENVOL only |
| `SOUS_TRAITANTS` | HECTARE crée vide ; ENVOL **supprime** (décision) |
| Nommage fichier | HECTARE déjà `{prestataire}__{piece}` |

**Recommandation** : factoriser `structureMarcheCommun()` (admin / contrats /
opérationnel / doc_libres) + options (`avec_sous_traitants`, `racine_programme`).
Question Robin : aligner HECTARE (retirer `SOUS_TRAITANTS`) ou divergence volontaire ?

---

## OBJET 8 — DÉCOUPAGE LOTS

| Lot | Contenu | Fichiers | Risque | Dépendance |
|-----|---------|----------|--------|------------|
| A | Déclaration structures + suppression `resolveMarcheClassification` | `SharePointArborescenceStructures`, début `SharePointService` | moyen (zone 3807 L, 76 warnings) | inventaire manuel |
| B | Résolution par nom (chemins AO, marché VEFA, rebuild) | `SharePointService`, `RebuildSharePointAoStructure`, AO jobs | **élevé** | A |
| C | Route web migration SIMULATION/`--apply` + refresh URLs | nouveau controller web + diag | élevé (Graph/timeout) | B + inventaire |
| D | Création paresseuse opérationnel | `creerDossierMarche*` | faible | A |
| E | Normalisation + anti-écrasement type | `normalizeMarcheSegment`, `persistDocumentUpload` | moyen data | indépendant |

Signaler : `SharePointService` zone commune + échecs souvent `Log::warning` silencieux.

---

## SCRIPT

```text
php tools/diag/diag_arborescence_chantier_1230.php
```

## DÉPLOIEMENT-TEST

```text
git pull origin main
php tools/diag/diag_arborescence_chantier_1230.php
```

Coller §1–§6. Pas de migrate / journal / build / Graph.

## LEÇON

Placeholders AO (`DCE/`, `DOSSIER_MARCHE/`, `PRESTATAIRES/`) ≠ dépôt réel
(`AO/AO_Ymd/`). Classification marché pilotée par flags **non fiables** (#1223).
Mesure CLI ≠ web (#1229) ; Graph = web only. Inventaire GED avant toute suppression.
