# Instructions Claude Code — Exécution de l'audit ESTAIR Connect

**Document compagnon** : `AUDIT_ESTAIR_Connect_Pipeline_Bugs.md`
**Destinataire** : Claude Code (en mode file-by-file)
**Auteur du contexte** : Nicephore

---

## 1. Contexte et règles de base

Tu vas appliquer un audit en plusieurs sprints. **Lis d'abord le préambule** du document `AUDIT_ESTAIR_Connect_Pipeline_Bugs.md` pour absorber les 6 décisions architecturales (D1–D6) qui guident chaque patch — ne les remets pas en cause.

**Règles strictes** :
- Tu travailles **un item à la fois**, dans l'ordre du sprint en cours
- Pour chaque item, tu te bases **uniquement** sur le code actuel + le bloc "Fix" de l'item — pas d'improvisation
- Tu ne crées **pas** de tests de régression non demandés (ils sont listés explicitement, cf. §3)
- Tu ne touches **pas** aux items hors du sprint courant
- Tu présentes un récapitulatif après chaque item (fichiers modifiés, lignes ajoutées/supprimées) avant de passer au suivant
- **Tu attends ma validation explicite** entre deux items du même sprint si tu rencontres une ambiguïté ; sinon enchaîne

**Trois dépôts concernés** :
- `frontend/` — Nuxt 3 + Vuexy
- `estair/` — Laravel 11 backend ESTAIR
- `chaps/` — Laravel backend CHAPS

Vérifie avant chaque patch dans quel dépôt tu te trouves (le chemin du fichier dans l'audit est relatif au dépôt concerné).

---

## 2. Ordre d'exécution

### ⚠️ Sprint S0 — P0 ABSOLUE (à faire en premier, avant tout autre)

L'écriture est cassée bout-en-bout depuis le frontend. Sans S0, le reste de l'audit n'est pas testable manuellement.

| Item | Dépôt | Fichier(s) principal(aux) |
|---|---|---|
| **E-05** | estair | `app/Http/Controllers/api/v1/CrudController.php` (méthodes `create` et `updateOne`) |
| **E-06** | estair | `app/Http/Controllers/api/v1/CrudController.php` (`deleteOne`) + `app/Librairies/BoService.php` (ajouter `deleteByUuid()`) |
| **E-07** | frontend | `composables/useEstair.ts` |

À la fin de S0, exécute les **scénarios 0a, 0b, 0c, 0d** (cf. §3) pour valider. Ne passe pas à S1 tant que les 4 scénarios ne sont pas verts.

### Sprints suivants

Suis ensuite l'ordre du tableau "6. Plan d'exécution recommandé" :

```
S0bis : D-01 staging + T-01 étapes 1-3
S1    : F-04, F-05, F-06, F-01, F-02
S2    : F-03, F-11, F-13
S3    : F-07, F-08, F-09, F-10, F-15, F-14
S4    : C-01, C-02, C-03, C-04
S5    : C-05
S6    : E-01, E-02, E-03, E-08, E-09
S7    : T-01 étapes 4-8 + F-12
S8    : E-04
S9    : D-02 (optionnel)
```

À la fin de chaque sprint, lance les scénarios d'acceptation correspondants de la section 7 du document audit.

---

## 3. Tests d'acceptation à implémenter

Les scénarios 0a → 0f de la section 7 décrivent le comportement attendu en langage métier. Pour les industrialiser, **génère les tests automatisés** suivants. Les noms de fichiers, dossiers et frameworks à utiliser sont indiqués pour chaque test.

### Préparation — fixtures et compte de test

Avant tout, crée un compte ESTAIR de test (dans la DB ESTAIR de staging, jamais en prod) :

```sql
-- À exécuter une fois sur estair (DB staging)
INSERT INTO accounts (uuid, name, office_id, estair_id, status, type, created_at, updated_at)
VALUES (
  'test-acceptance-fixture-uuid',
  'AcceptanceFixture',
  'TST00',
  'TSTFX',
  'Actif',
  'Corporate',
  NOW(),
  NOW()
);
```

Cet enregistrement est utilisé par les tests qui ont besoin d'un record pré-existant. Il est protégé du soft-delete par les tests (le test 0c crée un nouvel enregistrement spécifique pour la suppression, il ne touche pas la fixture).

### Tests backend ESTAIR (PHPUnit)

**Fichier à créer** : `tests/Feature/Crud/AcceptanceCrudTest.php`

Génère un test PHPUnit qui couvre les scénarios 0a, 0b, 0c, 0e, 0f. Caractéristiques :

- Hérite de `Tests\TestCase` (ou équivalent ESTAIR)
- Utilise `RefreshDatabase` ou des transactions DB (à vérifier ce qui est en place dans le repo)
- Authentifie via un JWT mocké ou un `actingAs($user)` en court-circuitant Keycloak (vérifie le pattern existant dans les tests ESTAIR)
- Pour chaque scénario, fait une assertion HTTP + une assertion DB

**Squelette à générer** :

```php
<?php

namespace Tests\Feature\Crud;

use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;

class AcceptanceCrudTest extends TestCase
{
    use RefreshDatabase;  // adapte si le repo utilise une autre stratégie

    private string $module = 'accounts';
    private array $authHeaders;

    protected function setUp(): void
    {
        parent::setUp();
        $this->authHeaders = $this->createAdminAuthHeaders();
        // Crée la fixture
        DB::table('accounts')->insert([
            'uuid' => 'test-acceptance-fixture-uuid',
            'name' => 'AcceptanceFixture',
            'office_id' => 'TST00',
            'estair_id' => 'TSTFX',
            'status' => 'Actif',
            'type' => 'Corporate',
            'created_at' => now(),
            'updated_at' => now(),
        ]);
    }

    /** @test  Scénario 0a — CREATE single réussit avec payload à plat */
    public function it_creates_a_record_with_flat_payload(): void
    {
        $payload = [
            'name' => 'Test Acceptance Create',
            'office_id' => 'TEST01',
            'estair_id' => 'TST',
            'status' => 'Actif',
            'type' => 'Corporate',
        ];

        $response = $this->withHeaders($this->authHeaders)
            ->postJson("/v1/admin/{$this->module}", $payload);

        $response->assertStatus(201)
            ->assertJson(['success' => true])
            ->assertJsonStructure(['record' => ['uuid', 'name']]);

        $this->assertDatabaseHas('accounts', [
            'name' => 'Test Acceptance Create',
            'office_id' => 'TEST01',
            'is_deleted' => 0,
        ]);
    }

    /** @test  Scénario 0a bis — CREATE accepte aussi le wrapper element (rétro-compat) */
    public function it_creates_a_record_with_element_wrapper(): void
    {
        $payload = ['element' => [
            'name' => 'Test Wrapper',
            'office_id' => 'WRP01',
            'estair_id' => 'WRP',
            'status' => 'Actif',
            'type' => 'Corporate',
        ]];

        $response = $this->withHeaders($this->authHeaders)
            ->postJson("/v1/admin/{$this->module}", $payload);

        $response->assertStatus(201);
        $this->assertDatabaseHas('accounts', ['name' => 'Test Wrapper']);
    }

    /** @test  Scénario 0a — CREATE rejette payload vide */
    public function it_rejects_empty_create_payload(): void
    {
        $response = $this->withHeaders($this->authHeaders)
            ->postJson("/v1/admin/{$this->module}", []);

        $response->assertStatus(400)
            ->assertJson(['success' => false]);
    }

    /** @test  Scénario 0b — UPDATE single avec payload à plat */
    public function it_updates_a_record_with_flat_payload(): void
    {
        $uuid = 'test-acceptance-fixture-uuid';

        $response = $this->withHeaders($this->authHeaders)
            ->putJson("/v1/admin/{$this->module}/{$uuid}", [
                'status' => 'Inactif',
            ]);

        $response->assertStatus(200)
            ->assertJson(['success' => true]);

        $this->assertDatabaseHas('accounts', [
            'uuid' => $uuid,
            'status' => 'Inactif',
        ]);
    }

    /** @test  Scénario 0b bis — UPDATE refuse de modifier account_id */
    public function it_strips_account_id_from_update_payload(): void
    {
        $uuid = 'test-acceptance-fixture-uuid';
        $existing = DB::table('accounts')->where('uuid', $uuid)->first();
        $originalAccountId = $existing->account_id ?? null;

        $this->withHeaders($this->authHeaders)
            ->putJson("/v1/admin/{$this->module}/{$uuid}", [
                'account_id' => 99999,  // tentative d'injection
                'status' => 'Inactif',
            ])
            ->assertStatus(200);

        $this->assertDatabaseHas('accounts', [
            'uuid' => $uuid,
            'account_id' => $originalAccountId,
        ]);
    }

    /** @test  Scénario 0c — DELETE single (par UUID) */
    public function it_soft_deletes_a_record_by_uuid(): void
    {
        // Crée un record dédié à la suppression (pas la fixture)
        $uuid = (string) \Str::uuid();
        DB::table('accounts')->insert([
            'uuid' => $uuid,
            'name' => 'ToDelete',
            'office_id' => 'DEL01',
            'estair_id' => 'DEL',
            'status' => 'Actif',
            'type' => 'Corporate',
            'created_at' => now(),
            'updated_at' => now(),
        ]);

        $response = $this->withHeaders($this->authHeaders)
            ->deleteJson("/v1/admin/{$this->module}/{$uuid}");

        $response->assertStatus(200)
            ->assertJson(['success' => true]);

        $this->assertDatabaseHas('accounts', [
            'uuid' => $uuid,
            'is_deleted' => 1,
        ]);
    }

    /** @test  Scénario 0c — DELETE retourne 404 sur UUID inconnu */
    public function it_returns_404_on_unknown_uuid(): void
    {
        $this->withHeaders($this->authHeaders)
            ->deleteJson("/v1/admin/{$this->module}/non-existent-uuid")
            ->assertStatus(404);
    }

    /** @test  Scénario 0e — MASS UPDATE par UUIDs */
    public function it_mass_updates_by_uuid_array(): void
    {
        $uuid1 = (string) \Str::uuid();
        $uuid2 = (string) \Str::uuid();
        DB::table('accounts')->insert([
            ['uuid' => $uuid1, 'name' => 'Mass1', 'office_id' => 'MS1', 'estair_id' => 'MS1', 'status' => 'Actif', 'type' => 'Corporate', 'created_at' => now(), 'updated_at' => now()],
            ['uuid' => $uuid2, 'name' => 'Mass2', 'office_id' => 'MS2', 'estair_id' => 'MS2', 'status' => 'Actif', 'type' => 'Corporate', 'created_at' => now(), 'updated_at' => now()],
        ]);

        $response = $this->withHeaders($this->authHeaders)
            ->putJson("/v1/admin/{$this->module}", [
                'ids' => [$uuid1, $uuid2],
                'data' => ['status' => 'Inactif'],
            ]);

        $response->assertStatus(200);
        $this->assertDatabaseHas('accounts', ['uuid' => $uuid1, 'status' => 'Inactif']);
        $this->assertDatabaseHas('accounts', ['uuid' => $uuid2, 'status' => 'Inactif']);
    }

    /** @test  Scénario 0e — MASS UPDATE retourne 207 si certains ids échouent */
    public function it_returns_207_on_partial_mass_update(): void
    {
        $uuid = 'test-acceptance-fixture-uuid';

        $response = $this->withHeaders($this->authHeaders)
            ->putJson("/v1/admin/{$this->module}", [
                'ids' => [$uuid, 'invalid-uuid'],
                'data' => ['status' => 'Inactif'],
            ]);

        $response->assertStatus(207)
            ->assertJsonPath('success', false)
            ->assertJsonStructure(['errors']);
    }

    /** @test  Scénario 0f — MASS DELETE par UUIDs */
    public function it_mass_deletes_by_uuid_array(): void
    {
        $uuid1 = (string) \Str::uuid();
        $uuid2 = (string) \Str::uuid();
        DB::table('accounts')->insert([
            ['uuid' => $uuid1, 'name' => 'MD1', 'office_id' => 'MD1', 'estair_id' => 'MD1', 'status' => 'Actif', 'type' => 'Corporate', 'created_at' => now(), 'updated_at' => now()],
            ['uuid' => $uuid2, 'name' => 'MD2', 'office_id' => 'MD2', 'estair_id' => 'MD2', 'status' => 'Actif', 'type' => 'Corporate', 'created_at' => now(), 'updated_at' => now()],
        ]);

        $response = $this->withHeaders($this->authHeaders)
            ->postJson("/v1/admin/{$this->module}/delete", [
                'ids' => [$uuid1, $uuid2],
            ]);

        $response->assertStatus(200);
        $this->assertDatabaseHas('accounts', ['uuid' => $uuid1, 'is_deleted' => 1]);
        $this->assertDatabaseHas('accounts', ['uuid' => $uuid2, 'is_deleted' => 1]);
    }

    /** @test  Scénario 0f — MASS DELETE accepte ids numériques (rétro-compat) */
    public function it_mass_deletes_by_numeric_ids(): void
    {
        $existing = DB::table('accounts')
            ->where('uuid', 'test-acceptance-fixture-uuid')
            ->first();

        $response = $this->withHeaders($this->authHeaders)
            ->postJson("/v1/admin/{$this->module}/delete", [
                'ids' => [$existing->id],
            ]);

        $response->assertStatus(200);
        $this->assertDatabaseHas('accounts', [
            'id' => $existing->id,
            'is_deleted' => 1,
        ]);
    }

    /**
     * Helper : crée des en-têtes JWT mockés pour un user admin.
     * À adapter selon le pattern de mock JWT existant dans le projet.
     */
    private function createAdminAuthHeaders(): array
    {
        // TODO Claude Code : inspecter `tests/` pour trouver le pattern utilisé
        //  - soit un middleware bypass dans tests/TestCase.php
        //  - soit un mock du package Alcyon\Identity
        //  - soit un user::factory()->create() avec actingAs()
        // Si rien n'existe : créer un trait `tests/Concerns/MocksKeycloakAuth.php`
        return [];
    }
}
```

**Action attendue de Claude Code** :
1. Créer le fichier `tests/Feature/Crud/AcceptanceCrudTest.php` avec ce squelette
2. Inspecter les tests existants (`tests/Feature/`) pour comprendre comment l'authentification JWT est mockée
3. Implémenter `createAdminAuthHeaders()` selon le pattern existant (sans inventer un nouveau mécanisme si ce n'est pas nécessaire)
4. Lancer `php artisan test --filter=AcceptanceCrudTest` et résoudre les éventuels échecs
5. Reporter le statut final : nombre de tests verts / total, et toute adaptation nécessaire au squelette

### Tests frontend (Vitest)

**Fichier à créer** : `tests/unit/useEstair-acceptance.test.ts`

Pour le scénario 0d (find-by + fetchOne fallback) :

```ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { useEstair } from '@/composables/useEstair'

// Mock $api global
const $apiMock = vi.fn()
vi.stubGlobal('$api', $apiMock)

describe('useEstair — acceptance CRUD', () => {
  beforeEach(() => {
    $apiMock.mockReset()
  })

  describe('Scénario 0d — findBy', () => {
    it('appelle GET /find/{field}/{value} et retourne un tableau', async () => {
      $apiMock.mockResolvedValueOnce({
        success: true,
        data: { id: 1, uuid: 'u1', email: 'foo@bar.com' },
      })

      const { findBy } = useEstair('accounts')
      const result = await findBy('email', 'foo@bar.com')

      expect($apiMock).toHaveBeenCalledWith(
        '/estair/admin/accounts/find/email/foo%40bar.com',
      )
      expect(result).toEqual([{ id: 1, uuid: 'u1', email: 'foo@bar.com' }])
    })

    it('retourne un tableau vide si la réponse est vide', async () => {
      $apiMock.mockResolvedValueOnce({ success: true, data: [] })
      const { findBy } = useEstair('accounts')
      const result = await findBy('email', 'inconnu@test.com')
      expect(result).toEqual([])
    })

    it('retourne tableau vide en cas d\'erreur réseau', async () => {
      $apiMock.mockRejectedValueOnce(new Error('Network'))
      const { findBy } = useEstair('accounts')
      const result = await findBy('email', 'test@test.com')
      expect(result).toEqual([])
    })
  })

  describe('Scénario 0d — fetchOne fallback id numérique', () => {
    it('résout id numérique en UUID puis fetch detail', async () => {
      // Premier appel : find/id/{n} → renvoie record avec uuid
      $apiMock.mockResolvedValueOnce({
        success: true,
        data: { id: 42, uuid: 'resolved-uuid', name: 'Foo' },
      })
      // Deuxième appel : /{uuid} → renvoie detail
      $apiMock.mockResolvedValueOnce({
        data: { id: 42, uuid: 'resolved-uuid', name: 'Foo' },
        tableau: {},
        availableActions: [],
      })

      const { fetchOne } = useEstair('accounts')
      const result = await fetchOne(42)

      expect($apiMock).toHaveBeenNthCalledWith(1, '/estair/admin/accounts/find/id/42')
      expect($apiMock).toHaveBeenNthCalledWith(2, '/estair/admin/accounts/resolved-uuid')
      expect(result?.data).toMatchObject({ id: 42, uuid: 'resolved-uuid' })
    })

    it('appelle directement avec UUID si l\'id est déjà un UUID', async () => {
      $apiMock.mockResolvedValueOnce({
        data: { uuid: 'direct-uuid' },
        tableau: {},
        availableActions: [],
      })

      const { fetchOne } = useEstair('accounts')
      await fetchOne('direct-uuid')

      expect($apiMock).toHaveBeenCalledTimes(1)
      expect($apiMock).toHaveBeenCalledWith('/estair/admin/accounts/direct-uuid')
    })
  })

  describe('Scénarios 0a/0b/0c — wrappers d\'écriture', () => {
    it('create envoie body à plat', async () => {
      $apiMock.mockResolvedValueOnce({ success: true, record: { uuid: 'new' } })
      $apiMock.mockResolvedValueOnce({ data: [], pagination: { totalRecords: 0 } })  // refresh

      const { create } = useEstair('accounts')
      await create({ name: 'X', status: 'Actif' })

      const callArgs = $apiMock.mock.calls[0]
      expect(callArgs[0]).toBe('/estair/admin/accounts')
      expect(callArgs[1]).toMatchObject({
        method: 'POST',
        body: { name: 'X', status: 'Actif' },  // pas de wrapper "element"
      })
    })

    it('update envoie body à plat avec UUID en URL', async () => {
      $apiMock.mockResolvedValueOnce({ success: true, record: { uuid: 'u1' } })
      $apiMock.mockResolvedValueOnce({ data: [], pagination: { totalRecords: 0 } })

      const { update } = useEstair('accounts')
      await update('u1', { status: 'Inactif' })

      const callArgs = $apiMock.mock.calls[0]
      expect(callArgs[0]).toBe('/estair/admin/accounts/u1')
      expect(callArgs[1]).toMatchObject({
        method: 'PUT',
        body: { status: 'Inactif' },
      })
    })

    it('remove envoie DELETE avec UUID en URL', async () => {
      $apiMock.mockResolvedValueOnce({ success: true })
      $apiMock.mockResolvedValueOnce({ data: [], pagination: { totalRecords: 0 } })

      const { remove } = useEstair('accounts')
      await remove('u1')

      const callArgs = $apiMock.mock.calls[0]
      expect(callArgs[0]).toBe('/estair/admin/accounts/u1')
      expect(callArgs[1]).toMatchObject({ method: 'DELETE' })
    })
  })
})
```

**Action attendue de Claude Code** :
1. Créer le fichier `tests/unit/useEstair-acceptance.test.ts`
2. Inspecter `tests/unit/` pour voir comment les autres tests gèrent les mocks (`$api`, `useNuxtApp`, etc.)
3. Adapter les mocks au pattern existant si nécessaire
4. Lancer `npm run test -- useEstair-acceptance` et corriger les échecs
5. Reporter le statut final

### Tests intégration end-to-end (manuel)

Les scénarios 0a–0f doivent **aussi** être validés manuellement après chaque déploiement, pour vérifier la chaîne complète Frontend → Proxy Nuxt → ESTAIR Laravel → MySQL. Crée un script bash `scripts/acceptance-crud.sh` qui automatise les appels curl pour les scénarios 0e et 0f (mass operations sans UI) :

```bash
#!/usr/bin/env bash
# scripts/acceptance-crud.sh
# Tests d'acceptation E-08 / E-09 contre ESTAIR de staging.
# Prérequis : variables d'env JWT_TOKEN et ESTAIR_URL.

set -euo pipefail

: "${JWT_TOKEN:?JWT_TOKEN env var is required}"
: "${ESTAIR_URL:?ESTAIR_URL env var is required (e.g. https://estair-staging.alcyon-partners.com)}"

MODULE="accounts"

echo "→ Création de fixtures..."
UUID1=$(uuidgen)
UUID2=$(uuidgen)

curl -sf -X POST "${ESTAIR_URL}/v1/admin/${MODULE}" \
  -H "Authorization: Bearer ${JWT_TOKEN}" \
  -H "Content-Type: application/json" \
  -d "{\"uuid\":\"${UUID1}\",\"name\":\"AcceptMass1\",\"office_id\":\"AM1\",\"estair_id\":\"AM1\",\"status\":\"Actif\",\"type\":\"Corporate\"}" \
  > /dev/null

curl -sf -X POST "${ESTAIR_URL}/v1/admin/${MODULE}" \
  -H "Authorization: Bearer ${JWT_TOKEN}" \
  -H "Content-Type: application/json" \
  -d "{\"uuid\":\"${UUID2}\",\"name\":\"AcceptMass2\",\"office_id\":\"AM2\",\"estair_id\":\"AM2\",\"status\":\"Actif\",\"type\":\"Corporate\"}" \
  > /dev/null

echo "→ Mass update (E-08)..."
RESP=$(curl -sf -X PUT "${ESTAIR_URL}/v1/admin/${MODULE}" \
  -H "Authorization: Bearer ${JWT_TOKEN}" \
  -H "Content-Type: application/json" \
  -d "{\"ids\":[\"${UUID1}\",\"${UUID2}\"],\"data\":{\"status\":\"Inactif\"}}")
echo "  réponse: $RESP"
[[ "$RESP" == *'"success":true'* ]] || { echo "✗ E-08 échec"; exit 1; }
echo "✓ E-08 OK"

echo "→ Mass delete (E-09)..."
RESP=$(curl -sf -X POST "${ESTAIR_URL}/v1/admin/${MODULE}/delete" \
  -H "Authorization: Bearer ${JWT_TOKEN}" \
  -H "Content-Type: application/json" \
  -d "{\"ids\":[\"${UUID1}\",\"${UUID2}\"]}")
echo "  réponse: $RESP"
[[ "$RESP" == *'"success":true'* ]] || { echo "✗ E-09 échec"; exit 1; }
echo "✓ E-09 OK"

echo "✅ Tous les scénarios mass CRUD valident."
```

**Action attendue de Claude Code** :
1. Créer le fichier `scripts/acceptance-crud.sh` (côté ESTAIR repo) avec exec bit
2. Documenter dans `README.md` du repo ESTAIR comment l'utiliser
3. Vérifier que `bash -n scripts/acceptance-crud.sh` passe (pas d'erreur de syntaxe)

---

## 4. Reporting attendu

Après chaque sprint, génère un rapport au format :

```markdown
## Sprint S<N> — Statut

**Items traités** : E-05, E-06, E-07
**Durée réelle** : 4h
**Fichiers modifiés** :
- estair/app/Http/Controllers/api/v1/CrudController.php (+87/-32 lignes)
- estair/app/Librairies/BoService.php (+45/-0 lignes, méthode deleteByUuid ajoutée)
- frontend/composables/useEstair.ts (+12/-8 lignes)

**Tests automatisés** :
- PHPUnit AcceptanceCrudTest : 9/9 ✓
- Vitest useEstair-acceptance : 7/7 ✓

**Tests manuels** :
- Scénario 0a CREATE single via UI : ✓
- Scénario 0b UPDATE single via UI : ✓
- Scénario 0c DELETE single via UI : ✓

**Points d'attention** :
- (rien) ou liste des décisions ad-hoc prises pendant l'implémentation

**Prochain sprint** : S0bis (D-01 + T-01 étapes 1-3)
```

---

## 5. Cas particuliers

### Si tu trouves une divergence entre l'audit et le code réel

L'audit a été produit à partir d'un snapshot daté du 09 mai 2026. Si tu vois que le code a évolué entre-temps (un fix similaire déjà appliqué, un fichier renommé, etc.) :

1. Documente la divergence dans ton rapport
2. Adapte le patch en conservant l'**intention** de l'item (pas la lettre du code original)
3. Ne saute pas l'item — vérifie que l'intention est bien remplie même si le code de départ a changé

### Si un patch nécessite une décision non prévue

- Vérifie d'abord les sections "Préambule" et "Décisions" en début du document audit
- Si la décision y est, applique-la
- Sinon, **arrête-toi** et demande validation explicite avant de continuer

### Si un test acceptance échoue après un patch

1. **Ne masque pas l'échec** en ajustant le test
2. Relis l'item du patch et vérifie que tu as appliqué exactement le code "Fix"
3. Si le code Fix a un bug, signale-le avec : extrait du code Fix, comportement observé, ce qui paraît incohérent
4. **N'invente pas un fix alternatif** sans validation

---

## 6. Checklist finale par sprint

Avant de marquer un sprint comme terminé :

- [ ] Tous les items du sprint sont commités, un par commit avec message clair (`fix(<scope>): <id> — <titre>`)
- [ ] Tests automatisés ajoutés (s'il y en a pour ce sprint) passent en local
- [ ] Tests manuels du sprint validés (scénarios de la section 7 du document audit)
- [ ] Rapport généré (cf. §4)
- [ ] Pas de TODO/FIXME laissé dans le code modifié sans ticket associé
- [ ] La branche est rebasée sur `dev` à jour

---

**Fin des instructions.**

Pour démarrer : commence par **S0** avec l'item **E-05**. Lis l'item complet dans le document audit, puis applique le fix `create()` dans `app/Http/Controllers/api/v1/CrudController.php`. Reporte avant de passer à `updateOne()`.
