---
suivi: 1353
date: 2026-08-14
sujet: sharepoint_403_creation_dossier_marche
chantier: sharepoint_ged
type: fix
statut: poussé
hash: b53209d9
fichiers:
  - app/Services/SharePointService.php
  - tools/diag/preuve_sharepoint_creer_dossier_marche_1353.php
  - docs/suivi/SUIVI_1353_sharepoint_403_creation_dossier_marche.md
---

## PROMPT ENVOYÉ

SUIVI #1353 — 403 accessDenied création dossier marché SharePoint.
Inventorier écritures qui marchent vs chemin en échec ; aligner sur le
mécanisme qui marche. Traiter `/` dans libellés. Clause d’arrêt si aucune
différence. Ne pas changer arborescence / nommage. Rebaser origin/main.

## FICHES LUES

- `#1346` LEÇON : 403 = feuille sous 4_PRESTATAIRES ; 9/9 prog 10 sans
  folder_id ; jeton marché = délégué vs sync = app ; charset/longueur
  écartés. **Cause candidate = jeton.**
- `#1063` LEÇON : sync programme = `getAppAccessToken` ; listings GED /
  marchés = délégué.
- `#1231` LEÇON : `normalizeMarcheSegment` → `normalizeDynamicFolderName`
  (strip interdits, pas de troncature).
- `#1241` : arborescence VEFA 05_SUIVI_CHANTIER/4_PRESTATAIRES.
- `#1350/#1351/#1352` : non touchés.

## SYNTHÈSE

### Tableau comparatif (objet 2) — EN PREMIER

| Point d’écriture | Jeton | Endpoint | Contexte | Statut observé |
|---|---|---|---|---|
| `creerArborescenceProgramme` L423 | **app** `getAppAccessToken` | POST `/drives/{id}/items/{parent}/children` via `ensureFolder` | web (souvent) / CLI sync | OK (ex. prog 17) |
| `creerDossierAppelOffre` L3657 | **app** | idem `ensureFolder` | web | OK |
| `uploadFichierApp` L734 | **app** | PUT `…/items/{parent}:/{name}:/content` | web | OK |
| Documents marché (chemin App L2941) | **app** `uploadFichierApp` | PUT content | web | OK |
| `creerDossierMarche` L1914 **avant** | **délégué** `getAccessToken` | POST children `ensureFolder` | web | **403** systématique (#1346) |
| `creerDossierMarche` L1914 **après #1353** | **app** `getAppAccessToken` | même endpoint / même arbo | web / CLI preuve | à valider OVH |
| `uploadFichierItem` / GED L2454 | délégué | PUT content | web | OK (non modifié) |
| Listings GED | délégué | GET children | web | OK (lecture) |

**Différence exacte** : seul le jeton. Même `ensureFolder`, même POST children,
même chemin `…/4_PRESTATAIRES/{POSTE}_{FOURNISSEUR}`. Clause d’arrêt non
déclenchée : différence exploitable = jeton app vs délégué.

### Correction

`SharePointService::creerDossierMarche` : `getAccessToken()` →
`getAppAccessToken()` — aligné sur `creerArborescenceProgramme` /
`creerDossierAppelOffre`. Aucun autre consommateur de `ensureFolder` modifié
(le token est injecté par l’appelant). Consommateurs de `creerDossierMarche`
(MarcheController, SiwiRepriseActionService, DemandeLiberationRgService,
resolveMarcheContratAvenantsFolderId) bénéficient du fix sans autre diff.

Arborescence / convention de nommage : **inchangées**.

### Cas `/` (400 invalidRequest)

`normalizeDynamicFolderName` (déjà utilisé par VEFA + lotissement) strip `/`
via `[^A-Z0-9_-]`. Preuve locale :
`PROVISION SAV / JURIDIQUE` → `PROVISION_SAV_JURIDIQUE` (pas de `/`).
Aucun code supplémentaire : réutilisation du normaliseur existant (#1231).
Le nom `PROVISION_SAV_/_JURIDIQUE_…` du 400 était un artefact hors normaliseur
actuel (ou ancien chemin).

### Hors périmètre

Pas de renommage des dossiers existants. Pas de migration. Pas de journal
(correctif technique invisible). Pas de rebuild Vue. AppShell / useFormatters /
ObjectSubNavBar / #1350–#1352 non touchés.

## DÉPLOIEMENT-TEST

```bash
git pull origin main
php artisan optimize:clear && php artisan view:clear
```

Puis preuve Graph (OVH — Cursor sans base/Graph prod) :

```bash
cd /home/hectare-envol/envol.hectare.fr
php -d display_errors=1 tools/diag/preuve_sharepoint_creer_dossier_marche_1353.php 186
php -d display_errors=1 tools/diag/preuve_sharepoint_creer_dossier_marche_1353.php --all-programme=10
```

Attendu : `ok=OUI`, `folder_id_apres` non vide ; 9/9 programme 10.
Cas `/` : `normalize_slash=[PROVISION_SAV_JURIDIQUE] has_slash=NON`.

## LEÇON

Un 403 Graph `accessDenied` sur un sous-chemin alors que d’autres écritures
ERP réussissent pointe souvent vers un **jeton différent** (délégué vs app),
pas vers le nom du dossier. Comparer systématiquement
`getAccessToken` / `getAppAccessToken` avant d’incriminer le charset.
`normalizeDynamicFolderName` suffit pour le `/` SharePoint.
