# Chantier — Bug : suppression rapport admin laisse le PDF + entrée pdf_generated orphelins

> ## ✅ CHANTIER LIVRÉ EN PRODUCTION
> **Vérifié le 2026-05-29** — Ne plus relire pour valider la livraison.

**Date** : 2026-04-29
**Repo** : coolcare-app (puis port à prévoir sur missioflow-app)
**Type** : Bug fix code + cleanup ponctuel résidu prod + amélioration logging
**Origine** : Investigation incident "rapport intervention 1295 supprimé" suite audit PWA offline 2026-04-29.

## Contexte

L'admin LAMBOUR Maxence a supprimé légitimement le rapport id=72 (intervention 1295 Dassault Vélizy) le 29/04 à 08:51:38 via le bouton 🗑️ dans `/admin/rapports.php`. Il s'agissait d'un rapport contenant une erreur ; il a fait recommencer le tech Marouene Cherif sur une nouvelle intervention.

La modal de confirmation de suppression promet :
> *"Le rapport [titre] sera supprimé définitivement. Le fichier PDF associé sera également retiré."*

**Mais en réalité, seule la ligne `rapports` est supprimée**. Le fichier PDF physique sur disque ET l'entrée `pdf_generated` sont conservés silencieusement → résidus orphelins.

## Diagnostic technique

Le code de [src/controllers/RapportController.php:363](src/controllers/RapportController.php) appelle simplement :
```php
$deleted = $this->rapportModel->delete($rapportId);
```

Et [src/models/BaseModel.php:72](src/models/BaseModel.php) (hérité par `Rapport`) ne fait que :
```php
$sql = "DELETE FROM rapports WHERE id = ?";
$stmt = $this->db->query($sql, [$id]);
```

→ Aucun `unlink()` du fichier physique, aucun `DELETE FROM pdf_generated`.

## État de la dette en prod (audit 2026-04-29)

| Type | Compte |
|------|-------:|
| Suppressions de rapports détectées (logs 21j) | 11 |
| Orphelins BDD `pdf_generated` (= sans rapport) | **1** (id=302, intervention 1295) |
| Suppressions historiques sans résidu BDD | 10 (tests pré-prod, pas de PDF généré à l'époque) |
| PDF disque sans entrée `pdf_generated` | ~86 (héritage pré-`pdf_generated`, **hors scope ce chantier**) |

→ **Une seule action de cleanup nécessaire** : entrée `pdf_generated.id=302` + fichier `intervention_1295_final_2026-04-28_13-32-31.pdf` (3.2 MB).

## Trou de logging connexe

[src/controllers/RapportController.php:365](src/controllers/RapportController.php#L365) loggue :
```
[INFO] Suppression du rapport ID: 72
```
**Sans l'auteur**. On a dû croiser avec `api_metrics` (table de monitoring) pour identifier Maxence. Pour les interventions, l'`ActivityLogger` est utilisé et trace l'auteur (`[admin] [ActivityLogger.php:315] Action administrateur: Intervention supprimée`). À aligner pour les rapports.

## Plan d'implémentation (4 étapes)

### Étape 1 — Cleanup résidu prod intervention 1295
**Action destructive en prod, valider explicitement avant exécution.**

```sql
-- BDD
DELETE FROM pdf_generated WHERE id = 302 AND intervention_id = 1295;
```
```bash
# Filesystem
rm /var/www/html/public/uploads/pdf/intervention_1295_final_2026-04-28_13-32-31.pdf
```

### Étape 2 — Audit lecture seule (déjà fait 2026-04-29)
Voir tableaux ci-dessus. Conclusion : 1 résidu BDD + 86 PDF historiques sur disque (hors scope).

### Étape 3 — Fix code `RapportController::destroy()`

Refactor pour tenir la promesse de la modal :

```php
public function destroy($id)
{
    Logger::info("Suppression du rapport ID: $id");
    try {
        $rapportId = (int)$id;
        if ($rapportId <= 0) {
            return ['success' => false, 'message' => 'ID rapport invalide'];
        }
        $existing = $this->rapportModel->getById($rapportId);
        if (!$existing) {
            return ['success' => false, 'message' => 'Rapport non trouvé'];
        }

        // 1. Récupérer les chemins PDF AVANT suppression (rapports.pdf_path
        //    + pdf_generated.file_path pour l'intervention).
        $pdfPaths = $this->collectPdfPathsForRapport($existing);

        // 2. Suppression atomique BDD : rapports + pdf_generated dans une
        //    transaction. Si l'un échoue, rollback.
        $this->db->beginTransaction();
        try {
            $deleted = $this->rapportModel->delete($rapportId);
            if (!$deleted) {
                $this->db->rollBack();
                return ['success' => false, 'message' => 'Échec de suppression du rapport'];
            }
            $this->pdfGeneratedModel->deleteByInterventionId((int)$existing['intervention_id']);
            $this->db->commit();
        } catch (Exception $txErr) {
            $this->db->rollBack();
            throw $txErr;
        }

        // 3. Suppression des fichiers physiques (best effort post-commit ;
        //    si unlink échoue on logge mais on ne rollback pas la BDD —
        //    un fichier orphelin est moins grave qu'une incohérence BDD).
        foreach ($pdfPaths as $path) {
            if ($path && file_exists($path) && !unlink($path)) {
                Logger::error("Suppression PDF physique echouee", ['path' => $path, 'rapport_id' => $rapportId]);
            }
        }

        // 4. Audit log avec auteur (combler le trou de traçabilité).
        ActivityLogger::log('rapport_supprime', [
            'rapport_id' => $rapportId,
            'intervention_id' => (int)$existing['intervention_id'],
            'pdf_files_removed' => count($pdfPaths),
            'admin_user_id' => $_SESSION['user_id'] ?? null,
            'admin_user_name' => $_SESSION['user_name'] ?? null,
        ]);

        return ['success' => true, 'message' => 'Rapport supprimé avec succès'];
    } catch (Exception $e) {
        Logger::error("Erreur lors de la suppression du rapport.", ['exception' => $e->getMessage()]);
        return ['success' => false, 'message' => 'Erreur lors de la suppression du rapport.'];
    }
}
```

Méthodes auxiliaires à créer :
- `collectPdfPathsForRapport(array $existing): array` — agrège `rapports.pdf_path` + tous les `pdf_generated.file_path` pour `intervention_id`.
- `PdfGenerated::deleteByInterventionId(int $id): int` — DELETE FROM pdf_generated WHERE intervention_id = ?.

Tests à ajouter dans `tests/Unit/RapportControllerTest.php` :
- Suppression réussie : rapport + pdf_generated + fichiers supprimés, retourne success=true.
- Fichier introuvable : suppression BDD réussie, log d'erreur sur unlink, retourne success=true.
- Échec BDD pdf_generated : rollback, retourne success=false, fichiers PAS supprimés.

### Étape 4 — Markdown chantier + port missioflow

- En local non commit jusqu'à validation prod : `/home/taaazzz/projects/CHANTIER_RAPPORT_DELETE_PDF_CLEANUP.md` (ce fichier).
- En fin de chantier coolcare validé : créer `missioflow-app/docs/CHANTIER_RAPPORT_DELETE_PDF_CLEANUP.md` avec instructions cherry-pick adaptées (le bug existe à l'identique sur missioflow car même base de code).

## Validation

- [x] Étape 2 (audit) terminée 2026-04-29 — 1 orphelin BDD (intervention 1295), 86 PDF disque historiques (hors scope)
- [x] Étape 3 (fix code) implémentée et testée
- [x] Pre-commit hook vert (php-cs-fixer + 3622 tests PHPUnit OK)
- [x] Push prod coolcare — commit `f715311` "fix(admin/rapports): destroy() nettoie aussi PDF + pdf_generated + log auteur"
- [x] Pipeline GitLab vert et deploy prod terminé — image `f715311e` deployée à ~16h35 (container `70e4b47c64bd`)
- [x] Étape 1 (cleanup résidu 1295) terminée :
  - `pdf_generated.id=302` supprimé manuellement par Bruno via SQL
  - `intervention_1295_final_2026-04-28_13-32-31.pdf` supprimé via `docker exec rm`
  - `rapports.id=72` déjà supprimé hier par Maxence (acte legitime)
  - Intervention 1295 elle-même conservée intacte (statut terminee)
- [x] Markdown port créé pour missioflow-app : `missioflow-app/docs/CHANTIER_RAPPORT_DELETE_PDF_CLEANUP.md`
- [ ] Vérification visuelle prod : suppression rapport test → vérifier filesystem + BDD propres après (à faire à la prochaine vraie suppression légitime, pas en test fictif sur une vraie intervention)
