# Phase 2 Lot 1 — `createEntree` + `createSortie` — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Ajouter 2 méthodes d'écriture bovin (`createEntree`, `createSortie`) à `BovinApiInterface`, appelant les opérations SOAP `IpBCreateEntree` / `IpBCreateSortie` de `wsIpBNotif`, avec requests/responses DTOs typés et enums métier. **Architecture:** On ajoute 6 unités indépendantes (3 enums, 2 request DTOs, 2 response DTOs, 2 mappers) puis on câble le tout dans `BovinApi` + `config/services.php`. Les mappers réutilisent `StandardResponseMapper` (déjà existant) et le trait `BovinNodeMappingTrait` pour `mapIdentification`/`mapMovement`. Pas de validation client-side, pas de test d'intégration SOAP (les mappers couvrent 95% de la logique testable). **Tech Stack:** PHP 8.4 (backed enums, readonly DTOs, named args), PHPUnit 12, Symfony 8 DI. Spec de référence : `docs/superpowers/specs/2026-04-22-phase2-create-entree-sortie-design.md` --- ## File Structure ### À créer ``` src/Bovin/Enum/CauseEntree.php enum 3 cases (Achat/Naissance/PretOuPension) src/Bovin/Enum/CauseSortie.php enum 6 cases src/Bovin/Enum/CategorieBovinIPG.php enum 13 cases (code 2-lettres) src/Bovin/Dto/CreateEntreeRequest.php request DTO src/Bovin/Dto/CreateSortieRequest.php request DTO src/Bovin/Dto/CreateEntreeResponseDto.php response DTO src/Bovin/Dto/CreateSortieResponseDto.php response DTO src/Bovin/Mapper/CreateEntreeResponseMapper.php mapper dédié src/Bovin/Mapper/CreateSortieResponseMapper.php mapper dédié tests/Unit/Bovin/Mapper/CreateEntreeResponseMapperTest.php 2 tests tests/Unit/Bovin/Mapper/CreateSortieResponseMapperTest.php 2 tests ``` ### À modifier ``` src/Bovin/Api/BovinApiInterface.php +2 méthodes, +imports src/Bovin/Api/BovinApi.php +2 méthodes, +2 constructor deps, +imports config/services.php +2 mappers registered, +2 args on BovinApi ``` --- ## Task 1 — Les 3 enums Pure data, aucune dépendance. Un seul commit pour les trois. **Files:** - Create: `src/Bovin/Enum/CauseEntree.php` - Create: `src/Bovin/Enum/CauseSortie.php` - Create: `src/Bovin/Enum/CategorieBovinIPG.php` ### Steps - [ ] **Step 1: Créer `CauseEntree`** Contenu complet de `src/Bovin/Enum/CauseEntree.php` : ```php ReponseStandard = new stdClass(); $soapResponse->ReponseStandard->Resultat = true; $soapResponse->ReponseSpecifique = new stdClass(); $soapResponse->ReponseSpecifique->AttenteValidationBDNi = true; $dto = (new CreateEntreeResponseMapper(new StandardResponseMapper()))->map($soapResponse); self::assertInstanceOf(CreateEntreeResponseDto::class, $dto); self::assertTrue($dto->standardResponse->result); self::assertTrue($dto->pendingBdniValidation); self::assertNull($dto->identification); self::assertNull($dto->entryMovement); } public function testMapValidatedEntry(): void { $soapResponse = new stdClass(); $soapResponse->ReponseStandard = new stdClass(); $soapResponse->ReponseStandard->Resultat = true; $soapResponse->ReponseSpecifique = new stdClass(); $validee = new stdClass(); $validee->IdentiteBovin = new stdClass(); $validee->IdentiteBovin->Sexe = 'F'; $validee->IdentiteBovin->Bovin = new stdClass(); $validee->IdentiteBovin->Bovin->CodePays = 'FR'; $validee->IdentiteBovin->Bovin->NumeroNational = 'FR1234567890'; $validee->MouvementEntreeBovin = new stdClass(); $validee->MouvementEntreeBovin->DateEntree = '2026-04-22'; $validee->MouvementEntreeBovin->CauseEntree = 'A'; $soapResponse->ReponseSpecifique->EntreeValidee = $validee; $dto = (new CreateEntreeResponseMapper(new StandardResponseMapper()))->map($soapResponse); self::assertFalse($dto->pendingBdniValidation); self::assertNotNull($dto->identification); self::assertSame('F', $dto->identification->sex); self::assertSame('FR1234567890', $dto->identification->bovin?->nationalNumber); self::assertNotNull($dto->entryMovement); self::assertEquals(new DateTimeImmutable('2026-04-22'), $dto->entryMovement->date); self::assertSame('A', $dto->entryMovement->cause); } } ``` - [ ] **Step 2: Lancer le test (doit échouer)** Run : ``` make test FILES=tests/Unit/Bovin/Mapper/CreateEntreeResponseMapperTest.php ``` Expected : erreur `Class ... not found` sur `CreateEntreeResponseDto` ou `CreateEntreeResponseMapper`. - [ ] **Step 3: Créer `CreateEntreeResponseDto`** Contenu complet de `src/Bovin/Dto/CreateEntreeResponseDto.php` : ```php standardResponseMapper->map($soapResponse->ReponseStandard ?? null); $specific = $soapResponse->ReponseSpecifique ?? null; $pending = false; $identification = null; $entryMovement = null; if (is_object($specific)) { $pendingFlag = $specific->AttenteValidationBDNi ?? null; if (null !== $pendingFlag) { $pending = (bool) $this->toNullableBool($pendingFlag); } $validee = $specific->EntreeValidee ?? null; if (is_object($validee)) { $identityNode = $validee->IdentiteBovin ?? null; if (is_object($identityNode)) { $identification = $this->mapIdentification($identityNode); } $movementNode = $validee->MouvementEntreeBovin ?? null; if (is_object($movementNode)) { $entryMovement = $this->mapMovement($movementNode, 'entry'); } } } return new CreateEntreeResponseDto( standardResponse: $standardResponse, pendingBdniValidation: $pending, identification: $identification, entryMovement: $entryMovement, rawSoapResponse: $soapResponse, ); } } ``` - [ ] **Step 5: Vérifier GREEN** Run : ``` make test FILES=tests/Unit/Bovin/Mapper/CreateEntreeResponseMapperTest.php ``` Expected : 2 tests passent. - [ ] **Step 6: Vérifier la suite complète** Run : ``` make test ``` Expected : 58 tests verts (56 existants + 2 nouveaux). - [ ] **Step 7: Commit** ``` git add src/Bovin/Dto/CreateEntreeResponseDto.php \ src/Bovin/Mapper/CreateEntreeResponseMapper.php \ tests/Unit/Bovin/Mapper/CreateEntreeResponseMapperTest.php git commit -m "feat : DTO et mapper de réponse pour IpBCreateEntree" ``` --- ## Task 4 — Câbler `createEntree` dans `BovinApi` **Files:** - Modify: `src/Bovin/Api/BovinApiInterface.php` - Modify: `src/Bovin/Api/BovinApi.php` - Modify: `config/services.php` ### Steps - [ ] **Step 1: Ajouter la méthode à l'interface** Dans `src/Bovin/Api/BovinApiInterface.php`, ajouter l'import et la méthode. Ajouter ces imports en tête (après ceux déjà présents) : ```php use Malio\EdnotifBundle\Bovin\Dto\CreateEntreeRequest; use Malio\EdnotifBundle\Bovin\Dto\CreateEntreeResponseDto; ``` Ajouter la méthode juste après `getPresumedExits(): PresumedExitsDto;` : ```php public function createEntree(CreateEntreeRequest $request): CreateEntreeResponseDto; ``` - [ ] **Step 2: Étendre `BovinApi`** Dans `src/Bovin/Api/BovinApi.php`, ajouter les imports suivants (alphabétiques) : ```php use Malio\EdnotifBundle\Bovin\Dto\CreateEntreeRequest; use Malio\EdnotifBundle\Bovin\Dto\CreateEntreeResponseDto; use Malio\EdnotifBundle\Bovin\Mapper\CreateEntreeResponseMapper; ``` Étendre le constructeur en ajoutant la nouvelle dépendance **juste après `$presumedExitsMapper`**, avant `$zipMessageDecoder` : ```php private PresumedExitsMapper $presumedExitsMapper, private CreateEntreeResponseMapper $createEntreeResponseMapper, private ZipMessageDecoder $zipMessageDecoder, ``` Ajouter la méthode `createEntree` **juste après `getPresumedExits()`** : ```php public function createEntree(CreateEntreeRequest $request): CreateEntreeResponseDto { $token = $this->tokenProvider->getToken(); $payload = [ 'JetonAuthentification' => $token, 'Exploitation' => [ 'CodePays' => $this->exploitationCountryCode, 'NumeroExploitation' => $this->exploitationNumber, ], 'Bovin' => [ 'CodePays' => $request->bovin->countryCode, 'NumeroNational' => $request->bovin->nationalNumber, ], 'DateEntree' => $request->date->format('Y-m-d'), 'CauseEntree' => $request->cause->value, 'ExploitationProvenance' => [ 'CodePays' => $request->provenance->countryCode, 'NumeroExploitation' => $request->provenance->exploitationNumber, ], ]; if (null !== $request->codeAtelier) { $payload['CodeAtelier'] = $request->codeAtelier; } if (null !== $request->codeCategorieBovin) { $payload['CodeCategorieBovin'] = $request->codeCategorieBovin->value; } try { /** @var object $soapResponse */ $soapResponse = $this->businessClient->__soapCall('IpBCreateEntree', [$payload]); } catch (SoapFault $soapFault) { throw new RuntimeException('SOAP Fault on IpBCreateEntree: '.$soapFault->getMessage(), 0, $soapFault); } $this->assertSuccessfulResponse($soapResponse, 'IpBCreateEntree'); return $this->createEntreeResponseMapper->map($soapResponse); } ``` - [ ] **Step 3: Enregistrer le mapper dans `config/services.php`** Ajouter l'import : ```php use Malio\EdnotifBundle\Bovin\Mapper\CreateEntreeResponseMapper; ``` Enregistrer le mapper **juste après `PresumedExitsMapper`** (lignes 61-65 dans le fichier actuel) : ```php $services->set(CreateEntreeResponseMapper::class) ->args([ service(StandardResponseMapper::class), ]) ; ``` Mettre à jour le bloc `BovinApi` en insérant le service après `PresumedExitsMapper` et avant `ZipMessageDecoder` : ```php $services->set(BovinApi::class) ->args([ service(TokenProvider::class), service('ednotif.soap.business'), service(AnimalFileMapper::class), service(InventoryMapper::class), service(ReturnedDossiersMapper::class), service(PresumedExitsMapper::class), service(CreateEntreeResponseMapper::class), service(ZipMessageDecoder::class), '%ednotif.exploitation_country_code%', '%ednotif.exploitation_number%', ]) ; ``` - [ ] **Step 4: Vérifier la suite** Run : ``` make test ``` Expected : 58 tests toujours verts (pas de nouveau test, mais pas de régression non plus). - [ ] **Step 5: Commit** ``` git add src/Bovin/Api/BovinApiInterface.php src/Bovin/Api/BovinApi.php config/services.php git commit -m "feat : expose IpBCreateEntree via BovinApi::createEntree" ``` --- ## Task 5 — `CreateSortieResponseDto` + Mapper + Tests (TDD) **Files:** - Create: `src/Bovin/Dto/CreateSortieResponseDto.php` - Create: `src/Bovin/Mapper/CreateSortieResponseMapper.php` - Create: `tests/Unit/Bovin/Mapper/CreateSortieResponseMapperTest.php` ### Steps - [ ] **Step 1: Écrire le test (RED)** Contenu complet de `tests/Unit/Bovin/Mapper/CreateSortieResponseMapperTest.php` : ```php ReponseStandard = new stdClass(); $soapResponse->ReponseStandard->Resultat = true; $soapResponse->ReponseSpecifique = new stdClass(); $soapResponse->ReponseSpecifique->AttenteValidationBDNi = true; $dto = (new CreateSortieResponseMapper(new StandardResponseMapper()))->map($soapResponse); self::assertInstanceOf(CreateSortieResponseDto::class, $dto); self::assertTrue($dto->pendingBdniValidation); self::assertNull($dto->identification); self::assertNull($dto->entryMovement); self::assertNull($dto->exitMovement); } public function testMapValidatedExit(): void { $soapResponse = new stdClass(); $soapResponse->ReponseStandard = new stdClass(); $soapResponse->ReponseStandard->Resultat = true; $soapResponse->ReponseSpecifique = new stdClass(); $validee = new stdClass(); $validee->IdentiteBovin = new stdClass(); $validee->IdentiteBovin->Sexe = 'M'; $validee->IdentiteBovin->Bovin = new stdClass(); $validee->IdentiteBovin->Bovin->NumeroNational = 'FR9999999999'; $mouvement = new stdClass(); $mouvement->MouvementEntreeBovin = new stdClass(); $mouvement->MouvementEntreeBovin->DateEntree = '2024-01-10'; $mouvement->MouvementEntreeBovin->CauseEntree = 'A'; $mouvement->MouvementSortieBovin = new stdClass(); $mouvement->MouvementSortieBovin->DateSortie = '2026-04-22'; $mouvement->MouvementSortieBovin->CauseSortie = 'B'; $validee->MouvementBovin = $mouvement; $soapResponse->ReponseSpecifique->SortieValidee = $validee; $dto = (new CreateSortieResponseMapper(new StandardResponseMapper()))->map($soapResponse); self::assertFalse($dto->pendingBdniValidation); self::assertNotNull($dto->identification); self::assertSame('M', $dto->identification->sex); self::assertSame('FR9999999999', $dto->identification->bovin?->nationalNumber); self::assertNotNull($dto->entryMovement); self::assertEquals(new DateTimeImmutable('2024-01-10'), $dto->entryMovement->date); self::assertSame('A', $dto->entryMovement->cause); self::assertNotNull($dto->exitMovement); self::assertEquals(new DateTimeImmutable('2026-04-22'), $dto->exitMovement->date); self::assertSame('B', $dto->exitMovement->cause); } } ``` - [ ] **Step 2: Lancer le test (doit échouer)** Run : ``` make test FILES=tests/Unit/Bovin/Mapper/CreateSortieResponseMapperTest.php ``` Expected : classe introuvable. - [ ] **Step 3: Créer `CreateSortieResponseDto`** Contenu complet de `src/Bovin/Dto/CreateSortieResponseDto.php` : ```php standardResponseMapper->map($soapResponse->ReponseStandard ?? null); $specific = $soapResponse->ReponseSpecifique ?? null; $pending = false; $identification = null; $entryMovement = null; $exitMovement = null; if (is_object($specific)) { $pendingFlag = $specific->AttenteValidationBDNi ?? null; if (null !== $pendingFlag) { $pending = (bool) $this->toNullableBool($pendingFlag); } $validee = $specific->SortieValidee ?? null; if (is_object($validee)) { $identityNode = $validee->IdentiteBovin ?? null; if (is_object($identityNode)) { $identification = $this->mapIdentification($identityNode); } $mouvementBovin = $validee->MouvementBovin ?? null; if (is_object($mouvementBovin)) { $entryNode = $mouvementBovin->MouvementEntreeBovin ?? null; if (is_object($entryNode)) { $entryMovement = $this->mapMovement($entryNode, 'entry'); } $exitNode = $mouvementBovin->MouvementSortieBovin ?? null; if (is_object($exitNode)) { $exitMovement = $this->mapMovement($exitNode, 'exit'); } } } } return new CreateSortieResponseDto( standardResponse: $standardResponse, pendingBdniValidation: $pending, identification: $identification, entryMovement: $entryMovement, exitMovement: $exitMovement, rawSoapResponse: $soapResponse, ); } } ``` - [ ] **Step 5: Vérifier GREEN** Run : ``` make test FILES=tests/Unit/Bovin/Mapper/CreateSortieResponseMapperTest.php ``` Expected : 2 tests passent. - [ ] **Step 6: Vérifier la suite complète** Run : ``` make test ``` Expected : 60 tests verts (58 + 2). - [ ] **Step 7: Commit** ``` git add src/Bovin/Dto/CreateSortieResponseDto.php \ src/Bovin/Mapper/CreateSortieResponseMapper.php \ tests/Unit/Bovin/Mapper/CreateSortieResponseMapperTest.php git commit -m "feat : DTO et mapper de réponse pour IpBCreateSortie" ``` --- ## Task 6 — Câbler `createSortie` dans `BovinApi` **Files:** - Modify: `src/Bovin/Api/BovinApiInterface.php` - Modify: `src/Bovin/Api/BovinApi.php` - Modify: `config/services.php` ### Steps - [ ] **Step 1: Ajouter la méthode à l'interface** Dans `src/Bovin/Api/BovinApiInterface.php`, ajouter les imports : ```php use Malio\EdnotifBundle\Bovin\Dto\CreateSortieRequest; use Malio\EdnotifBundle\Bovin\Dto\CreateSortieResponseDto; ``` Ajouter la méthode après `createEntree(...): CreateEntreeResponseDto;` : ```php public function createSortie(CreateSortieRequest $request): CreateSortieResponseDto; ``` - [ ] **Step 2: Étendre `BovinApi`** Ajouter les imports dans `src/Bovin/Api/BovinApi.php` : ```php use Malio\EdnotifBundle\Bovin\Dto\CreateSortieRequest; use Malio\EdnotifBundle\Bovin\Dto\CreateSortieResponseDto; use Malio\EdnotifBundle\Bovin\Mapper\CreateSortieResponseMapper; ``` Ajouter la dépendance au constructeur **juste après `$createEntreeResponseMapper`** : ```php private CreateEntreeResponseMapper $createEntreeResponseMapper, private CreateSortieResponseMapper $createSortieResponseMapper, private ZipMessageDecoder $zipMessageDecoder, ``` Ajouter la méthode après `createEntree(...)` : ```php public function createSortie(CreateSortieRequest $request): CreateSortieResponseDto { $token = $this->tokenProvider->getToken(); $payload = [ 'JetonAuthentification' => $token, 'Exploitation' => [ 'CodePays' => $this->exploitationCountryCode, 'NumeroExploitation' => $this->exploitationNumber, ], 'Bovin' => [ 'CodePays' => $request->bovin->countryCode, 'NumeroNational' => $request->bovin->nationalNumber, ], 'DateSortie' => $request->date->format('Y-m-d'), 'CauseSortie' => $request->cause->value, 'ExploitationDestination' => [ 'CodePays' => $request->destination->countryCode, 'NumeroExploitation' => $request->destination->exploitationNumber, ], ]; try { /** @var object $soapResponse */ $soapResponse = $this->businessClient->__soapCall('IpBCreateSortie', [$payload]); } catch (SoapFault $soapFault) { throw new RuntimeException('SOAP Fault on IpBCreateSortie: '.$soapFault->getMessage(), 0, $soapFault); } $this->assertSuccessfulResponse($soapResponse, 'IpBCreateSortie'); return $this->createSortieResponseMapper->map($soapResponse); } ``` - [ ] **Step 3: Enregistrer le mapper dans `config/services.php`** Ajouter l'import : ```php use Malio\EdnotifBundle\Bovin\Mapper\CreateSortieResponseMapper; ``` Enregistrer le mapper juste après `CreateEntreeResponseMapper` : ```php $services->set(CreateSortieResponseMapper::class) ->args([ service(StandardResponseMapper::class), ]) ; ``` Mettre à jour le bloc `BovinApi` en insérant le service après `CreateEntreeResponseMapper` : ```php $services->set(BovinApi::class) ->args([ service(TokenProvider::class), service('ednotif.soap.business'), service(AnimalFileMapper::class), service(InventoryMapper::class), service(ReturnedDossiersMapper::class), service(PresumedExitsMapper::class), service(CreateEntreeResponseMapper::class), service(CreateSortieResponseMapper::class), service(ZipMessageDecoder::class), '%ednotif.exploitation_country_code%', '%ednotif.exploitation_number%', ]) ; ``` - [ ] **Step 4: Vérifier la suite complète** Run : ``` make test ``` Expected : 60 tests toujours verts. Le constructeur de `BovinApi` a maintenant 11 args. - [ ] **Step 5: Commit** ``` git add src/Bovin/Api/BovinApiInterface.php src/Bovin/Api/BovinApi.php config/services.php git commit -m "feat : expose IpBCreateSortie via BovinApi::createSortie" ``` --- ## Checklist finale - [ ] `make test` vert, 60 tests / ~115 assertions - [ ] 6 commits propres, un par Task (aucune tâche ne laisse la suite rouge entre deux commits) - [ ] `BovinApiInterface` expose 6 méthodes au total (4 reads + 2 writes) - [ ] `BovinApi` constructeur : 11 args - [ ] `config/services.php` : 11 services enregistrés dans le bloc `BovinApi->args()` - [ ] Pas de validation client-side, pas de test SOAP mock — conforme au scope exclu