# Documents AO — SharePoint et devis PDF

## Arborescence SharePoint

À la création d’un **programme** (`CreateSharePointArborescence`), sous `05_SUIVI_CHANTIER/` :

| Dossier | Rôle |
|---------|------|
| `DCE` | DCE |
| `AO` | Racine des dossiers par appel d’offres |
| `DOSSIER_MARCHE` | Dossiers marché |
| `PRESTATAIRES` | Prestataires |

Ces dossiers **coexistent** avec `1_PRESTATAIRES`, `2_ENTREPRISES`, `3_CONCESSIONNAIRES`, `4_TAXES`, `5_PHOTOS`.

**Rétrofit programmes existants :**

```bash
php artisan sharepoint:rebuild-ao-structure
# ou un programme : php artisan sharepoint:rebuild-ao-structure --programme=12
```

## Dossier par appel d’offres

À la création d’un AO (`CreateSharePointAoFolder`) :

`05_SUIVI_CHANTIER/AO/AO_{YYYYMMDD}/` où `YYYYMMDD` = `date_lancement` de l’AO.

URL stockée sur `appels_offres.sharepoint_folder_url`.

## Devis PDF (consultation)

### Création

- Modale « Ajouter une consultation » : upload PDF **obligatoire** (10 Mo max).
- Fichier local (disque `local` Laravel 11+) : `storage/app/private/devis/ao_{ao_id}/{nom_normalise}.pdf`
- Chemin BDD : relatif au disque, ex. `devis/ao_2/xxx.pdf` — toujours via `Storage::disk('local')`
- Nom via `App\Support\FileNameNormalizer` : `{libellé_lot}_{entreprise}_{aaaammjj}.pdf`
- Job `UploadDevisToSharePoint` (retries 1 / 5 / 15 min).

## Documents AO (DCE + Estimatif MOE)

Architecture partagée : `AoDocumentService`, job `UploadAoDocumentToSharePoint` (paramètre `dce` | `estimatif_moe`), trait `HasAoDocument` sur `AppelOffre`.

### DCE.zip

| Élément | Valeur |
|---------|--------|
| Fichier | `DCE.zip` (MIME zip) |
| Local | `storage/app/private/dce/ao_{id}/DCE.zip` |
| SharePoint | `05_SUIVI_CHANTIER/AO/AO_{YYYYMMDD}/DCE.zip` |
| Colonnes BDD | `dce_*` sur `appels_offres` |

### Estimatif MOE.pdf

| Élément | Valeur |
|---------|--------|
| Fichier | `ESTIMATIF_MOE.pdf` (MIME pdf) |
| Local | `storage/app/private/estimatif_moe/ao_{id}/ESTIMATIF_MOE.pdf` |
| SharePoint | `05_SUIVI_CHANTIER/AO/AO_{YYYYMMDD}/ESTIMATIF_MOE.pdf` |
| Colonnes BDD | `estimatif_moe_*` sur `appels_offres` |

### Routes

| Action | DCE | Estimatif MOE |
|--------|-----|----------------|
| Dépôt | `POST …/dce` | `POST …/estimatif-moe` |
| Remplacement | `POST …/dce/replace` | `POST …/estimatif-moe/replace` |
| Suppression | `DELETE …/dce` | `DELETE …/estimatif-moe` |
| Retry SP | `POST …/dce/retry-sharepoint` | `POST …/estimatif-moe/retry-sharepoint` |
| Téléchargement | `GET …/dce/download` | `GET …/estimatif-moe/download` |

Champ formulaire : `document` (multipart).

### Droits

- **Lecture** (téléchargement, statut) : accès programme (middleware existant).
- **Modification** : `AppelOffrePolicy::manageDocuments` — responsable de l’AO ou `GS_DSI` / `GS_ADMIN_ERP`.

### UI

Encart « Documents AO » sur la fiche (`AoDocumentsSection` + `AoDocumentCard`), 2 colonnes desktop.

### Commandes

```bash
php artisan ao:retry-failed-documents
```

Relance tous les AO avec `dce_sharepoint_error` ou `estimatif_moe_sharepoint_error` (fichier local présent).

Job : pas de `throw` en mode `QUEUE_CONNECTION=sync` ; mail admin en échec définitif.

### Colonnes `ao_consultations`

- `devis_pdf_local_path`, `devis_pdf_filename`
- `devis_pdf_sharepoint_url`, `devis_pdf_sharepoint_synced_at`, `devis_pdf_sharepoint_error`

### UI fiche AO

| Statut | Badge |
|--------|--------|
| `pending` | Orange — en attente de synchro |
| `synced` | Vert — lien SharePoint |
| `error` | Rouge — bouton « Relancer la synchro » |

Tant que la synchro n’est pas faite, téléchargement via route protégée `devis-pdf` (fichier local).

### Commandes

```bash
php artisan sharepoint:retry-failed-devis
```

### Alertes

En échec définitif du job : mail vers `SHAREPOINT_ALERT_EMAILS` (liste séparée par des virgules), sinon `mail.from.address`.

## Sécurité

- Téléchargement local devis : vérification programme + scope AO + anti path traversal (`AoDevisStorage`).
- Écriture consultation : `GS_ENVOL`, `GS_DSI`, `GS_ADMIN_ERP`.

## Suppression d'une consultation

### Interface (fiche AO)

- Bouton **Supprimer** (rouge) dans la colonne Actions du tableau consultations.
- Modale de confirmation avant suppression.
- Désactivé si la consultation est l'**entreprise retenue** du lot (tooltip : « Désattribuer le lot d'abord »).

### Workflow backend

Route existante : `DELETE .../consultations/{ao_consultation}` (`appels-offres.consultations.destroy`).

Service `AoConsultationDeletionService` :

1. Refus 422 si entreprise retenue du lot.
2. Transaction : suppression `ao_consultations` (+ `ao_devis` en cascade).
3. Best-effort : `Storage::disk('local')->delete()` du devis PDF.
4. Best-effort : `SharePointService::deleteFile(webUrl)` si synchro réussie.
5. Échec SharePoint → log WARNING, pas d'erreur HTTP (suppression métier déjà actée).

### Commande purge en masse

```bash
php artisan ao:purge-consultations --names="xdvxdv,test" --dry-run
php artisan ao:purge-consultations --names="xdvxdv" --force
php artisan ao:purge-consultations --orphan-hours=24 --dry-run
```

| Option | Effet |
|--------|--------|
| `--names` | Filtre `nom_entreprise` (insensible à la casse) |
| `--orphan-hours` | Sans `devis_pdf_sharepoint_url` et créées il y a plus de N h |
| `--dry-run` | Liste sans supprimer |
| `--force` | Sans confirmation ; autorise noms courts |

Ne supprime jamais une consultation **entreprise retenue** d'un lot.

## Hors périmètre V1

- Purge automatique des PDF locaux après 30 jours.
- Table `deletion_pending` pour fichiers SharePoint orphelins (logs uniquement pour l'instant).
