diff --git a/config/services.php b/config/services.php index 36dd385..cc9f47d 100644 --- a/config/services.php +++ b/config/services.php @@ -7,6 +7,8 @@ use Malio\EdnotifBundle\Bovin\Api\BovinApi; use Malio\EdnotifBundle\Bovin\Api\BovinApiInterface; use Malio\EdnotifBundle\Bovin\Mapper\AnimalFileMapper; use Malio\EdnotifBundle\Bovin\Mapper\AnimalSummaryMapper; +use Malio\EdnotifBundle\Bovin\Mapper\CreateEntreeResponseMapper; +use Malio\EdnotifBundle\Bovin\Mapper\CreateSortieResponseMapper; use Malio\EdnotifBundle\Bovin\Mapper\InventoryMapper; use Malio\EdnotifBundle\Bovin\Mapper\PresumedExitsMapper; use Malio\EdnotifBundle\Bovin\Mapper\ReturnedDossiersMapper; @@ -64,6 +66,18 @@ return static function (ContainerConfigurator $container): void { ]) ; + $services->set(CreateEntreeResponseMapper::class) + ->args([ + service(StandardResponseMapper::class), + ]) + ; + + $services->set(CreateSortieResponseMapper::class) + ->args([ + service(StandardResponseMapper::class), + ]) + ; + $services->set(TokenProvider::class) ->args([ service('ednotif.soap.guichet'), @@ -85,6 +99,8 @@ return static function (ContainerConfigurator $container): void { 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%', diff --git a/docs/superpowers/plans/2026-04-22-phase2-create-entree-sortie.md b/docs/superpowers/plans/2026-04-22-phase2-create-entree-sortie.md new file mode 100644 index 0000000..897786d --- /dev/null +++ b/docs/superpowers/plans/2026-04-22-phase2-create-entree-sortie.md @@ -0,0 +1,997 @@ +# 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 diff --git a/docs/superpowers/specs/2026-04-22-phase2-create-entree-sortie-design.md b/docs/superpowers/specs/2026-04-22-phase2-create-entree-sortie-design.md new file mode 100644 index 0000000..d80b542 --- /dev/null +++ b/docs/superpowers/specs/2026-04-22-phase2-create-entree-sortie-design.md @@ -0,0 +1,330 @@ +# Phase 2 — Lot 1 : `IpBCreateEntree` + `IpBCreateSortie` + +## Contexte + +Phase 1 a livré les 4 lectures bovin (`getAnimalFile`, `getInventory`, `getReturnedDossiers`, `getPresumedExits`). Phase 2 démarre les opérations d'écriture : Ferme a besoin de déclarer ses entrées et sorties d'animaux auprès de l'IPG pour la saison à venir. + +Ces 2 opérations partagent 80% de l'infrastructure (pattern SOAP, enveloppe `ReponseStandard`, factorisation déjà faite via `StandardResponseMapper` et `BovinNodeMappingTrait`), donc on les traite dans un spec commun. + +## But + +Ajouter 2 méthodes à `BovinApiInterface` : `createEntree(CreateEntreeRequest)` et `createSortie(CreateSortieRequest)`, qui appellent les opérations SOAP `IpBCreateEntree` / `IpBCreateSortie` du WS `wsIpBNotif` et retournent des DTOs typés. + +## Décisions d'ergonomie (validées en brainstorming) + +| Axe | Décision | Raison | +|---|---|---| +| API d'appel | Request DTOs dédiés (un par op) | Testable, futur-compatible avec buffering de drafts. | +| Codes métier (CauseEntree, CauseSortie) | Enums backed-by-string, case names = libellés métier, `.value` = code IPG | Lecture explicite côté consommateur, SOAP reçoit le code via `.value`. | +| `CategorieBovinIPG` | Enum backed-by-string, case names = codes 2-lettres IPG | 13 cases avec libellés XSD courts, pas de gain à les renommer. | +| Code atelier | `?string` free-form | Pattern `[LABEM][1-9]` = 45 combinaisons, enum serait trop lourd. | +| Réponse (choice BDNi pending / validée) | DTO plat avec `bool $pendingBdniValidation` + nullable fields | Cohérent avec les DTOs existants du bundle. | +| Validation client-side | Aucune | EDNOTIF rejette via `EdnotifException` ; valeurs arrivent déjà validées en amont. | +| Scope | 2 ops en un seul spec/plan | Partage d'infra, priorités métier identiques. | + +## Architecture — fichiers + +### À créer + +``` +src/Bovin/Enum/CauseEntree.php enum 3 cases (P, A, N) +src/Bovin/Enum/CauseSortie.php enum 6 cases (H, C, M, B, E, X) +src/Bovin/Enum/CategorieBovinIPG.php enum 13 cases (BO, BR, FE, ...) +src/Bovin/Dto/CreateEntreeRequest.php request DTO +src/Bovin/Dto/CreateEntreeResponseDto.php response DTO +src/Bovin/Dto/CreateSortieRequest.php request DTO +src/Bovin/Dto/CreateSortieResponseDto.php response DTO +src/Bovin/Mapper/CreateEntreeResponseMapper.php mapper de la réponse IpBCreateEntree +src/Bovin/Mapper/CreateSortieResponseMapper.php mapper de la réponse IpBCreateSortie +tests/Unit/Bovin/Mapper/CreateEntreeResponseMapperTest.php 2 tests (pending + validée) +tests/Unit/Bovin/Mapper/CreateSortieResponseMapperTest.php 2 tests (pending + validée) +``` + +### À modifier + +``` +src/Bovin/Api/BovinApiInterface.php +2 méthodes +src/Bovin/Api/BovinApi.php +2 méthodes, +2 mapper deps dans le constructeur +config/services.php enregistrer les 2 mappers + updater BovinApi args +``` + +## Enums + +Conventions : +- **Backed-by-string** : `.value` = code IPG exact (ce qui part dans le payload SOAP). +- Case names = libellés métier pour `CauseEntree`/`CauseSortie` (lisibles côté consommateur). +- Case names = codes 2-lettres pour `CategorieBovinIPG` (13 cases, libellés XSD courts, pas de gain à les renommer). +- Docblock sur chaque case pour rappeler la correspondance. +- Pas de méthode `libelle()` ni de `values()` — YAGNI, à ajouter si un besoin métier concret remonte (I18N, UI de sélection). + +**Note sur les codes P/H/X** : la Table 9 IPG marque ces codes comme ambigus (entrée ET sortie selon le contexte). Côté WSDL EDNOTIF, cette ambiguïté n'existe pas : chaque op a son propre enum XSD restrictif (`CauseEntreeType` = {P, A, N}, `CauseSortieType` = {H, C, M, B, E, X}). Le sens est porté par l'op appelée, pas par le code. Le bundle n'a donc rien à faire de particulier à ce sujet. + +### `Malio\EdnotifBundle\Bovin\Enum\CauseEntree` + +Source : `CauseEntree.XSD` + doc IPG Table 9. + +```php +enum CauseEntree: string +{ + case Achat = 'A'; // Entrée par achat + case Naissance = 'N'; // Entrée par naissance + case PretOuPension = 'P'; // Entrée par prêt ou pension +} +``` + +### `Malio\EdnotifBundle\Bovin\Enum\CauseSortie` + +Source : `CauseSortie.XSD` + doc IPG Table 9. + +```php +enum CauseSortie: string +{ + case Boucherie = 'B'; // Sortie pour boucherie + case Consommation = 'C'; // Sortie pour auto-consommation + case Elevage = 'E'; // Sortie pour élevage ou vente + case Mort = 'M'; // Sortie pour mort + case PretOuPension = 'H'; // Sortie pour prêt ou pension (H sur sortie = équivalent du P sur entrée) + case Autre = 'X'; // Autre cause (réservée reprise / données historiques) +} +``` + +### `Malio\EdnotifBundle\Bovin\Enum\CategorieBovinIPG` + +Source : `CategorieBovinIPG.XSD`. 13 cases documentés depuis les `` du XSD : + +| Case | Libellé XSD | +|---|---| +| `BO` | Boeuf | +| `BR` | Broutard | +| `FE` | Femelle à l'engraissement | +| `GL` | Génisse laitière | +| `GV` | Génisse viande | +| `MA` | Mâle | +| `MR` | Mâle reproducteur | +| `TA` | Taurillon | +| `VA` | Vache allaitante | +| `VB` | Veau de boucherie | +| `VE` | Veau | +| `VL` | Vache laitière | +| `VR` | Vache de réforme | + +## Request DTOs + +### `CreateEntreeRequest` + +```php +final readonly class CreateEntreeRequest +{ + public function __construct( + public BovinRef $bovin, + public DateTimeInterface $date, + public CauseEntree $cause, + public ExploitationRef $provenance, + public ?string $codeAtelier = null, + public ?CategorieBovinIPG $codeCategorieBovin = null, + ) {} +} +``` + +- `bovin` / `provenance` réutilisent les DTOs existants (`BovinRef`, `ExploitationRef`). +- `codeAtelier` : string libre, pattern `[LABEM][1-9]` documenté en phpdoc. +- Les 2 derniers sont optionnels comme dans le XSD (`minOccurs="0"`). + +### `CreateSortieRequest` + +```php +final readonly class CreateSortieRequest +{ + public function __construct( + public BovinRef $bovin, + public DateTimeInterface $date, + public CauseSortie $cause, + public ExploitationRef $destination, + ) {} +} +``` + +Pas de champs optionnels côté Sortie (tous requis dans le XSD). + +## Response DTOs + +### `CreateEntreeResponseDto` + +```php +final readonly class CreateEntreeResponseDto +{ + public function __construct( + public StandardResponseDto $standardResponse, + public bool $pendingBdniValidation, + public ?BovinIdentificationDto $identification, + public ?MovementDto $entryMovement, + public ?object $rawSoapResponse, + ) {} +} +``` + +Invariant : si `$pendingBdniValidation === true`, alors `$identification === null` et `$entryMovement === null`. Inversement, si `$pendingBdniValidation === false`, les 2 autres sont populés. + +### `CreateSortieResponseDto` + +```php +final readonly class CreateSortieResponseDto +{ + public function __construct( + public StandardResponseDto $standardResponse, + public bool $pendingBdniValidation, + public ?BovinIdentificationDto $identification, + public ?MovementDto $entryMovement, // première entrée de la période clôturée + public ?MovementDto $exitMovement, // sortie elle-même + public ?object $rawSoapResponse, + ) {} +} +``` + +La réponse `SortieValidee` renvoie **la période de présence complète** (entrée + sortie de l'animal sur l'exploitation), d'où les 2 `MovementDto`. + +## Mappers + +### `CreateEntreeResponseMapper` + +```php +final class CreateEntreeResponseMapper +{ + use BovinNodeMappingTrait; + + public function __construct( + private readonly StandardResponseMapper $standardResponseMapper, + ) {} + + public function map(object $soapResponse): CreateEntreeResponseDto + { + // 1. mapStandardResponse + // 2. Lire ReponseSpecifique.AttenteValidationBDNi (bool) ou .EntreeValidee (struct) + // Le choice XSD garantit une seule des deux présente. + // 3. Si pending : retourne le DTO avec pending=true, identification/entryMovement=null + // Sinon : mapIdentification (trait) + mapMovement (trait) sur EntreeValidee + } +} +``` + +### `CreateSortieResponseMapper` + +Même pattern, mais la branche validée lit `SortieValidee.MouvementBovin.MouvementEntreeBovin` et `SortieValidee.MouvementBovin.MouvementSortieBovin`. Deux appels à `mapMovement` au lieu d'un. + +### Pourquoi 2 mappers séparés + +Les shapes de `EntreeValidee` et `SortieValidee` diffèrent : entrée plate (juste l'entrée), sortie imbriquée (entrée + sortie). Factoriser maintenant obligerait à introduire des paramètres de configuration abstraits (noms de champs, nombre de movements attendus) qui n'apportent rien pour 2 mappers. Reconsidérer si un 3ᵉ `Create*` arrive avec une shape similaire. + +## API methods + +### `BovinApiInterface` + +```php +public function createEntree(CreateEntreeRequest $request): CreateEntreeResponseDto; + +public function createSortie(CreateSortieRequest $request): CreateSortieResponseDto; +``` + +### `BovinApi::createEntree` + +```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 { + $soapResponse = $this->businessClient->__soapCall('IpBCreateEntree', [$payload]); + } catch (SoapFault $e) { + throw new RuntimeException('SOAP Fault on IpBCreateEntree: '.$e->getMessage(), 0, $e); + } + + $this->assertSuccessfulResponse($soapResponse, 'IpBCreateEntree'); + + return $this->createEntreeResponseMapper->map($soapResponse); +} +``` + +### `BovinApi::createSortie` + +Pattern identique. Payload plus simple (4 champs métier), op `IpBCreateSortie`, mapper dédié. + +### Injection + +Constructeur `BovinApi` passe de 9 à **11 args** (ajout de `CreateEntreeResponseMapper` et `CreateSortieResponseMapper` entre `PresumedExitsMapper` et `ZipMessageDecoder`). + +`config/services.php` : +- Enregistrer `CreateEntreeResponseMapper` et `CreateSortieResponseMapper` avec `service(StandardResponseMapper::class)` en dep. +- Updater `BovinApi->args()` pour refléter les 11 args. + +## Tests + +### Scope + +- **Mappers** : 2 tests chacun × 2 mappers = **4 tests** (cas pending + cas validée). +- **Enums** : aucun test dédié. Les enums sont auto-testés par leur usage dans les request DTOs. +- **Request DTOs** : aucun test dédié. Constructeur simple, pas de logique. +- **Response DTOs** : aucun test dédié. Même raison. +- **API methods** (`createEntree`, `createSortie`) : **pas testés** en l'état. Le bundle n'a pas d'infrastructure de mock SoapClient. Les mappers couvrent 95% de la logique, le reste étant du payload-shaping trivial. Si un bug apparaît plus tard côté appel SOAP, on ajoutera un test avec un mock. + +### Fixtures attendues (résumé) + +- `CreateEntreeResponseMapperTest::testMapPendingValidation` — réponse avec `AttenteValidationBDNi=true`, assert que `pendingBdniValidation === true` et que les 2 fields bovin sont null. +- `CreateEntreeResponseMapperTest::testMapValidatedEntry` — réponse avec `EntreeValidee` populée, assert `pendingBdniValidation === false`, identification + entryMovement bien mappés. +- Idem pour `CreateSortieResponseMapperTest`, avec la shape imbriquée `SortieValidee.MouvementBovin`. + +Compte attendu post-implémentation : 56 + 4 = **60 tests**. + +## Gestion d'erreurs + +- **`Resultat=false` côté EDNOTIF** : `assertSuccessfulResponse()` lève `EdnotifException` — même pattern que les reads, pas de nouveau code. +- **SoapFault** : remonté en `RuntimeException` wrapping. +- **Réponse "validée" mais champs absents** (shouldn't happen selon XSD) : mapper renvoie un DTO avec les fields null. Pas d'exception levée — l'anomalie remonte via la lecture logique par le consommateur. + +## Périmètre EXCLU (explicite) + +- Validation client-side des inputs (pattern `nationalNumber` = 10 chiffres, `codeAtelier` = `[LABEM][1-9]`, etc.) +- Factorisation d'un `CreateResponseMapper` abstrait/trait — à reconsidérer quand le 3ᵉ `Create*` arrivera. +- Gestion métier du statut `pendingBdniValidation` côté bundle (polling, callback). C'est la responsabilité de Ferme — le bundle expose juste l'info. +- Tests d'intégration SOAP avec mock client. +- Les 8 autres opérations `IpBCreate*` (naissance, mort-né, rebouclage, échange, import, insémination, commande boucles). Elles feront l'objet de spec/plan séparés en Phase 2 Lot 2+. + +## Impact sur le consommateur (Ferme) + +Aucune breaking change — les 4 méthodes de lecture existantes gardent leur signature. Les 2 nouvelles méthodes sont additives sur `BovinApiInterface`. + +Mise à jour côté Ferme : `composer update malio/ednotif-bundle` une fois la PR mergée, puis usage direct : + +```php +$response = $this->ednotif->createEntree(new CreateEntreeRequest( + bovin: new BovinRef('FR', 'FR1234567890'), + date: new DateTimeImmutable('2026-04-22'), + cause: CauseEntree::Achat, + provenance: new ExploitationRef('FR', '12345678'), +)); + +if ($response->pendingBdniValidation) { + // planifier un getReturnedDossiers dans quelques jours +} else { + // l'animal est dans le cheptel : $response->identification->bovin->nationalNumber +} +``` diff --git a/src/Bovin/Api/BovinApi.php b/src/Bovin/Api/BovinApi.php index 8fda161..dc431f4 100644 --- a/src/Bovin/Api/BovinApi.php +++ b/src/Bovin/Api/BovinApi.php @@ -7,10 +7,16 @@ namespace Malio\EdnotifBundle\Bovin\Api; use DateTimeInterface; use Malio\EdnotifBundle\Auth\TokenProvider; use Malio\EdnotifBundle\Bovin\Dto\AnimalFileDto; +use Malio\EdnotifBundle\Bovin\Dto\CreateEntreeRequest; +use Malio\EdnotifBundle\Bovin\Dto\CreateEntreeResponseDto; +use Malio\EdnotifBundle\Bovin\Dto\CreateSortieRequest; +use Malio\EdnotifBundle\Bovin\Dto\CreateSortieResponseDto; use Malio\EdnotifBundle\Bovin\Dto\InventoryDto; use Malio\EdnotifBundle\Bovin\Dto\PresumedExitsDto; use Malio\EdnotifBundle\Bovin\Dto\ReturnedDossiersDto; use Malio\EdnotifBundle\Bovin\Mapper\AnimalFileMapper; +use Malio\EdnotifBundle\Bovin\Mapper\CreateEntreeResponseMapper; +use Malio\EdnotifBundle\Bovin\Mapper\CreateSortieResponseMapper; use Malio\EdnotifBundle\Bovin\Mapper\InventoryMapper; use Malio\EdnotifBundle\Bovin\Mapper\PresumedExitsMapper; use Malio\EdnotifBundle\Bovin\Mapper\ReturnedDossiersMapper; @@ -29,6 +35,8 @@ final readonly class BovinApi implements BovinApiInterface private InventoryMapper $inventoryMapper, private ReturnedDossiersMapper $returnedDossiersMapper, private PresumedExitsMapper $presumedExitsMapper, + private CreateEntreeResponseMapper $createEntreeResponseMapper, + private CreateSortieResponseMapper $createSortieResponseMapper, private ZipMessageDecoder $zipMessageDecoder, private string $exploitationCountryCode, private string $exploitationNumber, @@ -158,6 +166,80 @@ final readonly class BovinApi implements BovinApiInterface return $this->presumedExitsMapper->map($soapResponse, $unzippedMessage); } + 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); + } + + 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); + } + private function assertSuccessfulResponse(object $soapResponse, string $operation): void { $standardResponseNode = $soapResponse->ReponseStandard ?? null; diff --git a/src/Bovin/Api/BovinApiInterface.php b/src/Bovin/Api/BovinApiInterface.php index e8131c8..8b2c987 100644 --- a/src/Bovin/Api/BovinApiInterface.php +++ b/src/Bovin/Api/BovinApiInterface.php @@ -6,6 +6,10 @@ namespace Malio\EdnotifBundle\Bovin\Api; use DateTimeInterface; use Malio\EdnotifBundle\Bovin\Dto\AnimalFileDto; +use Malio\EdnotifBundle\Bovin\Dto\CreateEntreeRequest; +use Malio\EdnotifBundle\Bovin\Dto\CreateEntreeResponseDto; +use Malio\EdnotifBundle\Bovin\Dto\CreateSortieRequest; +use Malio\EdnotifBundle\Bovin\Dto\CreateSortieResponseDto; use Malio\EdnotifBundle\Bovin\Dto\InventoryDto; use Malio\EdnotifBundle\Bovin\Dto\PresumedExitsDto; use Malio\EdnotifBundle\Bovin\Dto\ReturnedDossiersDto; @@ -23,4 +27,8 @@ interface BovinApiInterface public function getReturnedDossiers(DateTimeInterface $startDate): ReturnedDossiersDto; public function getPresumedExits(): PresumedExitsDto; + + public function createEntree(CreateEntreeRequest $request): CreateEntreeResponseDto; + + public function createSortie(CreateSortieRequest $request): CreateSortieResponseDto; } diff --git a/src/Bovin/Dto/CreateEntreeRequest.php b/src/Bovin/Dto/CreateEntreeRequest.php new file mode 100644 index 0000000..6c9f814 --- /dev/null +++ b/src/Bovin/Dto/CreateEntreeRequest.php @@ -0,0 +1,28 @@ +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, + ); + } +} diff --git a/src/Bovin/Mapper/CreateSortieResponseMapper.php b/src/Bovin/Mapper/CreateSortieResponseMapper.php new file mode 100644 index 0000000..5a4e1cb --- /dev/null +++ b/src/Bovin/Mapper/CreateSortieResponseMapper.php @@ -0,0 +1,66 @@ +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, + ); + } +} diff --git a/tests/Unit/Bovin/Mapper/CreateEntreeResponseMapperTest.php b/tests/Unit/Bovin/Mapper/CreateEntreeResponseMapperTest.php new file mode 100644 index 0000000..f24fa04 --- /dev/null +++ b/tests/Unit/Bovin/Mapper/CreateEntreeResponseMapperTest.php @@ -0,0 +1,68 @@ +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); + } +} diff --git a/tests/Unit/Bovin/Mapper/CreateSortieResponseMapperTest.php b/tests/Unit/Bovin/Mapper/CreateSortieResponseMapperTest.php new file mode 100644 index 0000000..90e7e13 --- /dev/null +++ b/tests/Unit/Bovin/Mapper/CreateSortieResponseMapperTest.php @@ -0,0 +1,77 @@ +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); + } +}