---
suivi: 1063
date: 2026-07-29
sujet: Diagnostic SharePoint multi-site / multi-arborescence par enseigne
chantier: sharepoint_ged
type: diagnostic
statut: poussé
hash: d9e6cb29
fichiers:
  - tools/diag/diag_sharepoint_1063.php
  - docs/suivi/SUIVI_1063_diag_sharepoint_par_enseigne.md
---

## PROMPT ENVOYÉ

SUIVI #1063 — PHASE 1 diagnostic uniquement. ENVOL et HECTARE : même tenant Microsoft,
deux sites SharePoint distincts, deux arborescences sous programme. Établir config,
services Graph, modèle de données, création d’arborescence ENVOL complète, contraintes
CLI/OVH, comparer 3 approches. Aucun fix / migration / .vue. Script BASE ONLY, zéro Graph,
zéro secret.

## SYNTHÈSE

### A — Où vit la configuration SharePoint

#### A1. Racine site / drive
- Clés lues via `config/services.php` → bloc `sharepoint` :
  - `SHAREPOINT_SITE_ID` → `config('services.sharepoint.site_id')`
  - `SHAREPOINT_DRIVE_NAME` → `config('services.sharepoint.drive_name')` (défaut code `PROGRAMMES`)
  - `SHAREPOINT_ALERT_EMAILS` → `config('services.sharepoint.alert_emails')`
- Auth Azure (app) : `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`, `AZURE_TENANT_ID`
  via `config('services.azure.*')`.
- **Second site déjà présent** pour les leads marketing (hors GED programmes) :
  `LEADS_SHAREPOINT_SITE_ID` / `LEADS_SHAREPOINT_LIST_ID` → `config('services.leads_marketing.*')`.
- Aucune colonne SharePoint sur `enseignes`. Pas de site/drive en base pour l’enseigne.
- Constante legacy dans le code : `SharePointService::SITE_ID = 'SHAREPOINT_SITE_ID_A_CONFIGURER'`
  (placeholder ; la vraie lecture passe par `config()`).
- `.env.example` documente les clés leads, **pas** explicitement `SHAREPOINT_SITE_ID` /
  `SHAREPOINT_DRIVE_NAME` (présentes pourtant dans `config/services.php`).

#### A2. Services / classes
| Classe | Rôle |
|---|---|
| `App\Services\SharePointService` | Cœur GED programmes : résolution drive, arborescence, marchés, AO, uploads, listing, preview, delete |
| `App\Services\GraphApiService` | Tokens app (`client_credentials`) et délégué (session user) ; appels Graph génériques |
| `App\Services\OutlookAttachmentService` | Drag & drop mail : `ewsId→restId` + `Mail.Read` ; **ne fait PAS** l’upload SharePoint (renvoie le binaire) |
| `App\Jobs\CreateSharePointArborescence` | Job création arborescence à la création programme |
| `App\Jobs\CreateSharePointAoFolder` | Dossier AO sous suivi chantier |
| `App\Jobs\UploadAoDocumentToSharePoint` / `UploadDevisToSharePoint` | Sync DCE / estimatif / devis |
| `App\Jobs\UploadAppelDeFondSharePoint` | PDF appels de fonds |
| `App\Jobs\CreateLeadSharePointItem` | Item liste SharePoint leads (autre site) |
| `App\Console\Commands\RebuildSharePointAoStructure` | Recrée DCE/AO/DOSSIER_MARCHE/PRESTATAIRES |
| `App\Console\Commands\TestSharePointFields` | Test champs liste leads |
| `App\Console\Commands\RetryFailedAoDevisSharePoint` / `RetryFailedAoDocumentsSharePoint` | Relance jobs AO |
| `App\Console\Commands\PurgeAoConsultations` | Purge BDD + fichiers SP |
| `App\Console\Commands\SimulateMetaLead` | option `--sync` → SharePoint leads |
| Controllers | `GedController`, `ProgrammeController` (sync/create), `MarcheController`, AO… |
| Vue | `resources/js/Components/SharePoint/SharePointGedExplorer.vue` |

#### A3. Multi-site / multi-drive aujourd’hui ?
- **Création** : racine **unique** globale (`SHAREPOINT_SITE_ID` + nom de drive).
  `creerArborescenceProgramme()` appelle `getDriveId()` (config), puis **persiste**
  `sharepoint_drive_id` / `item_id` / `url` sur le programme.
- **Lecture ultérieure** : `resolveDriveIdForProgramme()` préfère le `drive_id` **stocké**
  sur le programme, sinon fallback config.
- Incohérence : `creerDossierMarche()` utilise encore `getDriveId()` (config globale),
  pas `resolveDriveIdForProgramme()` — fragile si un programme est sur un autre drive.
- Leads marketing = **déjà un 2e site** (liste), indépendant de la GED programmes.
- Verdict franc : **pas de multi-site programmes** côté config ; le stockage
  `drive_id` par programme **ouvre techniquement** la lecture multi-drive **sans**
  migration de colonnes, mais la **création** et plusieurs chemins restent monolithe config.

### B — Modèle de données

#### B1. `programmes`
Colonnes : `sharepoint_folder_url`, `sharepoint_drive_id`, `sharepoint_item_id`.
Question prod (script) : couverture, **nb DISTINCT drive_id**, par enseigne.
Si DISTINCT = 1 → une seule racine en pratique. Si > 1 → multi-drive déjà en données.

#### B2. `marches`
`sharepoint_folder_url`, `sharepoint_folder_id`, `sharepoint_folder_pieces_admin`,
`sharepoint_folder_factures`, `sharepoint_folder_contrat`. Pas de `drive_id` propre
(dépendance implicite au drive du programme / config).

#### B3. Autres tables avec refs SharePoint
`appels_offres` (folder + DCE + estimatif), `ao_devis`, `reservations` (+
`reservation_documents.sharepoint_subfolder`), `factures.fichier_sharepoint_url`,
`tmas.document_sharepoint_url`, `appels_de_fonds`, `programme_etapes` (justificatif +
drive_id), `marche_documents`, `avenant_documents`, `leads_marketing`,
`ged_actions_log`.
Hors sujet SharePoint : SUPDOC RIB / factures = **Intacct**, pas Graph.

#### B4. EQUILIBRE (id 13, HEC34-001)
À lire en prod via le script (section B4 + B4b hosts). Hypothèse code : s’il a une URL,
elle a été créée via le site **global** (probablement ENVOL) avec l’arborescence VEFA —
ou bien les champs sont vides (création échouée / jamais sync).

### C — Création de l’arborescence

#### C1. Quand / où
- À la **création** du programme : `ProgrammeController::store` →
  `CreateSharePointArborescence::dispatch($programme)` (L287).
- Avec `QUEUE_CONNECTION=sync` (prod rappelée) : exécution **immédiate** dans la requête
  web (PHP-FPM) — OK pour Graph ; en CLI artisan ≠ web.
- Relance manuelle DSI : `ProgrammeController::sharepointSync` →
  `creerArborescenceProgramme()` synchrone (web).
- Implémentation : `SharePointService::creerArborescenceProgramme` (~L308–461).

#### C2. Arborescence ENVOL actuelle (intégrale, code)

Chemin racine bibliothèque (drive configuré) :

```
{DEPARTEMENT}/
  {COMMUNE}/
    {CODE_ENVOL}--{LIBELLE}/
      00_COMITES_ENGAGEMENT/
        01_DEPOT_PC/
        02_LANCEMENT_COMMERCIAL/
        03_ACQUISITION_FONCIER/
        04_DEMARRAGE_TRAVAUX/
        05_BILAN_CLOTURE/
      01_ADMINISTRATIF/
      02_FINANCIER/
      03_COMMERCIAL/
      04_ACQUEREURS/
        {NOM_ACQUEREUR_LOT}/          ← créé à la réservation, pas à la création programme
          01_Identité/
          02_Financement/
          03_Contrat_réservation/
          04_Acte_notarié/
          05_TMA/
      05_SUIVI_CHANTIER/
        1_PRESTATAIRES/
          {POSTE}_{FOURNISSEUR}/     ← dossier marché (classification)
            00_PIECES_ADMINISTRATIVES/
            01_CONTRAT_AVENANTS/
            02_FACTURES/
            03_DOCUMENTS/            ← créés avec le marché
            04_JURIDIQUE/
        2_ENTREPRISES/               ← idem structure marché
        3_CONCESSIONNAIRES/          ← idem
        4_TAXES/
        5_PHOTOS/
        DCE/                         ← AO_SUIVI_SUBFOLDERS (constante)
        AO/
        DOSSIER_MARCHE/
        PRESTATAIRES/
      06_TIERS/
      07_JURIDIQUE/
      08_ARCHIVE/
```

Normalisation : `departement`, `commune`, `libelle` passent par
`normalizeDynamicFolderName` (ASCII upper, underscores). Nom programme =
`{code_envol}--{libelle_normalisé}`.

#### C3. Déclaratif ou impératif ?
- Sous-dossiers programme : **tableau local** `$sousDossiers` + boucles (semi-déclaratif
  dans la méthode, **pas** une config externe / enseigne).
- AO : constante `AO_SUIVI_SUBFOLDERS` (déclaratif partiel).
- Marchés / acquéreurs : logique impérative + match classification.
- Ajouter une 2e arborescence = aujourd’hui **fork de code** ou extraction d’une
  structure déclarative (pas encore en place).

#### C4. Programme HECTARE créé aujourd’hui
Même pipeline : job → `creerArborescenceProgramme` → site/drive **global** + structure
**VEFA ci-dessus**. Pas de branche enseigne. Résultat typique : arborescence VEFA sur le
site configuré (ENVOL), ou échec silencieux (warning) si token/site/données manquants.
Pas d’erreur métier « enseigne HECTARE ».

### D — Contraintes techniques

#### D1. Auth Graph
- Écritures arborescence / uploads app : **app-only**
  (`GraphApiService::getAppAccessToken`, `client_credentials`, scope
  `https://graph.microsoft.com/.default`).
- Certains listings GED / marchés : token **délégué** (`getAccessToken` session user).
- Un 2e site : le **même** enregistrement d’application peut suffire **si** les
  permissions applicatives couvrent les deux sites (ex. `Sites.ReadWrite.All` tenant-wide,
  ou `Sites.Selected` + grant explicite sur le site HECTARE).
  → **Action Entra / admin Microsoft probable** avant dev : confirmer le modèle de
  permission actuel et ajouter le site HECTARE si `Sites.Selected`.

#### D2. CLI OVH / HTTPS bloqué
Commandes artisan qui appellent Graph (cassées en CLI prod si exécutées hors web) :
- `sharepoint:rebuild-ao-structure` (`RebuildSharePointAoStructure`)
- `TestSharePointFields` (leads)
- `RetryFailedAo*` (dispatch jobs ; avec queue sync en CLI → Graph en CLI)
- `PurgeAoConsultations` (delete SP)
- `SimulateMetaLead --sync`
Rappel : `QUEUE_CONNECTION=sync` → jobs = synchrone dans le processus appelant.
Création programme OK car déclenchée en **web**.

#### D3. Erreurs en `warning` (leçon #1043)
Nombreux échecs SharePoint loggés en `Log::warning` (pas `error`) : résolution drive,
création arborescence (sauf exception catch → `error`), job
`CreateSharePointArborescence` (échec + exception → **warning**), uploads AO, deleteFile,
createListItem leads, sync programme (`ProgrammeController`), etc.
Surveillance sur `production.ERROR` **sous-détecte** les pannes GED.

### E — Approches

| | (1) Config par enseigne | (2) Config par programme | (3) Service dédié / enseigne |
|---|---|---|---|
| Effort | Moyen | Faible–moyen (drive déjà stocké) | Élevé |
| Risque ENVOL | Moyen si mal branché | Faible si création reste défaut ENVOL | Faible si isolation stricte |
| Extensibilité | Bonne (GEMME) | Moyenne (structure encore code) | Haute mais coût N implémentations |
| Structure | Colonnes enseigne ou JSON dédié (pas `modules_actifs` bool) | Structure déduite de l’enseigne ; drive/url sur programme | Interface + 2 classes |

**Question de fond :** ce n’est **pas** seulement une racine différente.
Robin indique **deux arborescences** → paramétrer le site/drive **et** abstraire la
structure (déclarative par enseigne / type métier). Un seul `if (HECTARE)` dans le
service actuel = dette.

#### E1. Recommandation
1. **Site/drive par enseigne** (colonnes ou JSON config dédié sur `enseignes`, pas le
   booléen `modules_actifs`) + conservation du stockage `drive_id` / `item_id` sur
   `programmes` (déjà là).
2. **Structure déclarative** (tableau/config par code enseigne ou « type métier »)
   consommée par `creerArborescenceProgramme` — option (1)+(2) hybrides.
3. Unifier les chemins pour toujours passer par `resolveDriveIdForProgramme` /
   résolution enseigne (corriger `creerDossierMarche`).
4. Ne pas inventer un service complet par enseigne tant que les opérations Graph sont
   identiques (folder/upload/list) — surcoût (3) sans gain si seule la structure change.
5. Découpage lots :
   - A : config enseigne (site_id / drive_name) + admin + permissions Entra
   - B : structure déclarative HECTARE + branche création (nouveaux programmes)
   - C : migration EQUILIBRE (déplacer / recréer / laisser) selon tranchage Robin
   - D : aligner marchés/AO/GED sur resolveDriveId + logs `error` pour échecs bloquants

#### E2. Questions Robin
1. Arborescence HECTARE souhaitée (liste dossiers) — à partir du C2 ENVOL ?
2. EQUILIBRE : **déplacer** l’existant, **recréer** sur le site HECTARE, ou **laisser** ?
3. Migration programmes déjà créés vs **nouveaux seulement** ?
4. Permissions Entra : qui gère l’ajout du site HECTARE à l’app ?
5. Les modules AO / marchés / 04_ACQUEREURS existent-ils côté lotissement HECTARE ?

#### E3. Risques ENVOL
Ne pas changer le défaut `SHAREPOINT_SITE_ID` / drive PROGRAMMES ; ne pas réécrire
aveuglément `creerArborescenceProgramme` ; GED + Outlook drop (#878 refresh) + marchés +
AO + réservations s’appuient sur `item_id` / `drive_id` existants — toute migration
EQUILIBRE mal faite peut casser les liens stockés.

## DÉPLOIEMENT-TEST

```bash
git pull origin main
php tools/diag/diag_sharepoint_1063.php
```

Coller A1 (flags), B1 (distinct drive_id), B4 (EQUILIBRE), B4b (hosts).
Pas de migrate, pas de journal:sync, pas de rebuild, pas de Graph depuis CLI.

## LEÇON
Un `drive_id` déjà stocké par programme ne signifie pas multi-site métier : la création
reste branchée sur une seule clé `.env`. Les échecs GED en `Log::warning` restent
invisibles si on ne surveille que `ERROR`. Les commandes artisan SharePoint sont
inutilisables en CLI sur OVH (HTTPS sortant bloqué) — tout Graph doit rester en
contexte web/PHP-FPM.
