---
suivi: 1357
date: 2026-08-14
sujet: resync_documents_marche_sharepoint
chantier: sharepoint
type: feat
statut: poussé
hash: 935a1e3e
fichiers:
  - app/Services/MarcheDocumentSharePointResyncService.php
  - app/Services/SharePointService.php
  - app/Http/Controllers/Admin/MarcheDocumentSharePointResyncController.php
  - routes/web.php
  - resources/js/Pages/Admin/SharePoint/ResyncMarcheDocuments.vue
  - resources/js/Pages/Admin/Index.vue
  - tests/Unit/MarcheDocumentSharePointResync1357Test.php
  - tools/diag/inventaire_resync_marche_docs_1357.php
  - database/data/journal_mises_a_jour_post_2026_06_12.php
  - public/build/
  - docs/suivi/SUIVI_1357_resync_documents_marche_sharepoint.md
---

## PROMPT ENVOYÉ

SUIVI #1357 — resynchronisation additive documents marché locaux → SharePoint
(période 403 11–14/08, fix #1353). GS_ADMIN_ERP brut, décompte avant, lots web,
idempotence item_id, écriture item_id, pas de DELETE/replace/overwrite, seuil 200.

## FICHES LUES

- `#1353` : jeton app pour créations dossier / écritures marché.
- `#1355` / `#1356` : token app bloqué en CLI OVH → traitement WEB PHP-FPM ;
  lots budget 80 % max_execution_time.
- `#1236` / `#1239` / `#1254` : item_id seule référence stable ; dette 47 DCE.
- `#1231` : écrasement fichiers marché — interdiction absolue de replace.

## SYNTHÈSE

### Sélection

`marche_documents` où `sharepoint_item_id` vide/null ET `fichier_path` non vide.
Colonnes vérifiées (pas `factures.fichier_*` mortes). Preview peuplé + contrôle
marché 177 (`marche_signe` / `340836-CONTRAT-ENVOL-VIAS`).

### Action admin

- UI : Admin → SharePoint → « Resync docs marché » si
  `m365GroupNames.includes('GS_ADMIN_ERP')` (brut).
- Serveur : `in_array('GS_ADMIN_ERP', m365GroupNames(), true)` → 403 sinon.
- Routes `GET/POST /admin/sharepoint/resync-documents-marche*` (middleware
  `m365.groups:GS_ADMIN_ERP` + assert brut).
- Décompte avant exécution ; si total > 200 → inventaire only (422).
- Lots web, relance auto si budget temps ; compteurs traités/montés/sautés/échecs.
- Garde-fou : `fileNameExistsInFolderApp` avant `uploadFichierApp` ; si nom
  existe → sauté (pas de PUT). Aucun DELETE Graph. Locaux conservés.
- `sharepoint_item_id` + URL écrits à chaque montée réussie.
- Dossier marché absent → `creerDossierMarche` (jeton app #1353) puis montage.

### Preuves Cursor

- Unit tests 7 verts (droits, idempotence, fichier absent, anti-delete source).
- Compteur prod : **pas de base prod depuis Cursor** → script
  `tools/diag/inventaire_resync_marche_docs_1357.php` (SQL, CLI OK) + page preview
  web. Montage Graph réel = procédure Robin (interface).

## DÉPLOIEMENT-TEST

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

Procédure Robin (Graph prod) :
1. SSH : `php tools/diag/inventaire_resync_marche_docs_1357.php` → noter
   `eligible_total`, `controle_177=OUI`, détail par marché.
2. Si total > 200 : **ne pas lancer** — arbitrer sur inventaire.
3. Compte GS_ADMIN_ERP : `/admin` → SharePoint → Resync docs marché → vérifier
   décompte = inventaire.
4. Compte NON admin (ex. GS_COMPTABILITE) : carte absente ; GET/POST URL → 403.
5. Lancer resync ; vérifier marché 177 PDF dans le bon dossier SP +
   `sharepoint_item_id` en base.
6. Relancer → montés = 0 (idempotence).
7. Cas fichier local absent → sauté non bloquant.

## LEÇON

1. PUT Graph `/content` écrase par chemin → listage préalable obligatoire ;
   `resolveUniqueFileNameInFolder` créerait un doublon (`_2`), pas un skip.
2. Droit admin : `hasAnyM365Group` / `hasM365Groupe` FAUX positifs →
   `m365GroupNames()` brut côté UI et serveur.
3. Compteur prod inaccessible depuis Cursor → diag SQL OVH + preview web.
