docs : plan d'implémentation Phase 2 lot 1 (createEntree + createSortie)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-22 14:48:04 +02:00
parent cd9393f62f
commit a538608ab4

View File

@@ -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
<?php
declare(strict_types=1);
namespace Malio\EdnotifBundle\Bovin\Enum;
/**
* Cause d'une entrée de bovin sur l'exploitation (opération `IpBCreateEntree`).
*
* Source : `resources/ednotif-ws/CauseEntree.XSD` + doc IPG Table 9.
* Le `.value` est le code IPG transmis dans le payload SOAP.
*/
enum CauseEntree: string
{
/** Entrée par achat. */
case Achat = 'A';
/** Entrée par naissance. */
case Naissance = 'N';
/** Entrée par prêt ou pension. */
case PretOuPension = 'P';
}
```
- [ ] **Step 2: Créer `CauseSortie`**
Contenu complet de `src/Bovin/Enum/CauseSortie.php` :
```php
<?php
declare(strict_types=1);
namespace Malio\EdnotifBundle\Bovin\Enum;
/**
* Cause d'une sortie de bovin de l'exploitation (opération `IpBCreateSortie`).
*
* Source : `resources/ednotif-ws/CauseSortie.XSD` + doc IPG Table 9.
* Le `.value` est le code IPG transmis dans le payload SOAP.
*
* Le code `H` porte ici le sens "Sortie pour prêt ou pension" (équivalent du `P`
* sur une entrée) ; le WSDL garantit que chaque code n'apparaît que dans son sens,
* pas d'ambiguïté à gérer côté consommateur.
*/
enum CauseSortie: string
{
/** Sortie pour boucherie. */
case Boucherie = 'B';
/** Sortie pour auto-consommation. */
case Consommation = 'C';
/** Sortie pour élevage ou vente. */
case Elevage = 'E';
/** Sortie pour mort. */
case Mort = 'M';
/** Sortie pour prêt ou pension. */
case PretOuPension = 'H';
/** Autre cause (réservée aux reprises / données historiques). */
case Autre = 'X';
}
```
- [ ] **Step 3: Créer `CategorieBovinIPG`**
Contenu complet de `src/Bovin/Enum/CategorieBovinIPG.php` :
```php
<?php
declare(strict_types=1);
namespace Malio\EdnotifBundle\Bovin\Enum;
/**
* Catégorie IPG d'un bovin (champ optionnel de `IpBCreateEntree`).
*
* Source : `resources/ednotif-ws/CategorieBovinIPG.XSD`.
* Le `.value` est le code IPG (2 lettres) transmis dans le payload SOAP.
* Les case names suivent le code XSD, les libellés sont en docblock.
*/
enum CategorieBovinIPG: string
{
/** Boeuf. */
case BO = 'BO';
/** Broutard. */
case BR = 'BR';
/** Femelle à l'engraissement. */
case FE = 'FE';
/** Génisse laitière. */
case GL = 'GL';
/** Génisse viande. */
case GV = 'GV';
/** Mâle. */
case MA = 'MA';
/** Mâle reproducteur. */
case MR = 'MR';
/** Taurillon. */
case TA = 'TA';
/** Vache allaitante. */
case VA = 'VA';
/** Veau de boucherie. */
case VB = 'VB';
/** Veau. */
case VE = 'VE';
/** Vache laitière. */
case VL = 'VL';
/** Vache de réforme. */
case VR = 'VR';
}
```
- [ ] **Step 4: Vérifier la suite (pas d'impact)**
Run :
```
make test
```
Expected : toujours 56 tests / 107 assertions verts (les enums ne sont pas encore référencés).
- [ ] **Step 5: Commit**
```
git add src/Bovin/Enum/
git commit -m "feat : enums CauseEntree, CauseSortie, CategorieBovinIPG"
```
---
## Task 2 — Les 2 request DTOs
Pure data, dépend des enums de Task 1 et de `BovinRef`/`ExploitationRef` existants.
**Files:**
- Create: `src/Bovin/Dto/CreateEntreeRequest.php`
- Create: `src/Bovin/Dto/CreateSortieRequest.php`
### Steps
- [ ] **Step 1: Créer `CreateEntreeRequest`**
Contenu complet de `src/Bovin/Dto/CreateEntreeRequest.php` :
```php
<?php
declare(strict_types=1);
namespace Malio\EdnotifBundle\Bovin\Dto;
use DateTimeInterface;
use Malio\EdnotifBundle\Bovin\Enum\CategorieBovinIPG;
use Malio\EdnotifBundle\Bovin\Enum\CauseEntree;
/**
* Paramètres d'une déclaration d'entrée bovin (opération `IpBCreateEntree`).
*
* `codeAtelier` suit le pattern XSD `[LABEM][1-9]` (L=Lait, A=Allaitant,
* B=Veaux de boucherie, E=Engraissement autre, M=Manade). Non validé côté
* bundle : EDNOTIF rejette la valeur malformée via `EdnotifException`.
*/
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,
) {}
}
```
- [ ] **Step 2: Créer `CreateSortieRequest`**
Contenu complet de `src/Bovin/Dto/CreateSortieRequest.php` :
```php
<?php
declare(strict_types=1);
namespace Malio\EdnotifBundle\Bovin\Dto;
use DateTimeInterface;
use Malio\EdnotifBundle\Bovin\Enum\CauseSortie;
/**
* Paramètres d'une déclaration de sortie bovin (opération `IpBCreateSortie`).
*/
final readonly class CreateSortieRequest
{
public function __construct(
public BovinRef $bovin,
public DateTimeInterface $date,
public CauseSortie $cause,
public ExploitationRef $destination,
) {}
}
```
- [ ] **Step 3: Vérifier la suite (pas d'impact)**
Run :
```
make test
```
Expected : 56 tests toujours verts.
- [ ] **Step 4: Commit**
```
git add src/Bovin/Dto/CreateEntreeRequest.php src/Bovin/Dto/CreateSortieRequest.php
git commit -m "feat : request DTOs pour createEntree et createSortie"
```
---
## Task 3 — `CreateEntreeResponseDto` + Mapper + Tests (TDD)
**Files:**
- Create: `src/Bovin/Dto/CreateEntreeResponseDto.php`
- Create: `src/Bovin/Mapper/CreateEntreeResponseMapper.php`
- Create: `tests/Unit/Bovin/Mapper/CreateEntreeResponseMapperTest.php`
### Steps
- [ ] **Step 1: Écrire le test (RED)**
Contenu complet de `tests/Unit/Bovin/Mapper/CreateEntreeResponseMapperTest.php` :
```php
<?php
declare(strict_types=1);
namespace Malio\EdnotifBundle\Tests\Unit\Bovin\Mapper;
use DateTimeImmutable;
use Malio\EdnotifBundle\Bovin\Dto\CreateEntreeResponseDto;
use Malio\EdnotifBundle\Bovin\Mapper\CreateEntreeResponseMapper;
use Malio\EdnotifBundle\Shared\Mapper\StandardResponseMapper;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use stdClass;
#[CoversClass(CreateEntreeResponseMapper::class)]
final class CreateEntreeResponseMapperTest extends TestCase
{
public function testMapPendingBdniValidation(): void
{
$soapResponse = new stdClass();
$soapResponse->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
<?php
declare(strict_types=1);
namespace Malio\EdnotifBundle\Bovin\Dto;
use Malio\EdnotifBundle\Shared\Dto\StandardResponseDto;
/**
* Réponse de `IpBCreateEntree`.
*
* Si `$pendingBdniValidation === true`, EDNOTIF a accepté la requête mais
* attend la validation asynchrone de la BDNi — `identification` et
* `entryMovement` sont `null`. Sinon, les deux sont populés avec les données
* validées du bovin et du mouvement d'entrée.
*/
final readonly class CreateEntreeResponseDto
{
public function __construct(
public StandardResponseDto $standardResponse,
public bool $pendingBdniValidation,
public ?BovinIdentificationDto $identification,
public ?MovementDto $entryMovement,
public ?object $rawSoapResponse,
) {}
}
```
- [ ] **Step 4: Créer `CreateEntreeResponseMapper`**
Contenu complet de `src/Bovin/Mapper/CreateEntreeResponseMapper.php` :
```php
<?php
declare(strict_types=1);
namespace Malio\EdnotifBundle\Bovin\Mapper;
use Malio\EdnotifBundle\Bovin\Dto\CreateEntreeResponseDto;
use Malio\EdnotifBundle\Shared\Mapper\StandardResponseMapper;
final class CreateEntreeResponseMapper
{
use BovinNodeMappingTrait;
public function __construct(
private readonly StandardResponseMapper $standardResponseMapper,
) {}
public function map(object $soapResponse): CreateEntreeResponseDto
{
$standardResponse = $this->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
<?php
declare(strict_types=1);
namespace Malio\EdnotifBundle\Tests\Unit\Bovin\Mapper;
use DateTimeImmutable;
use Malio\EdnotifBundle\Bovin\Dto\CreateSortieResponseDto;
use Malio\EdnotifBundle\Bovin\Mapper\CreateSortieResponseMapper;
use Malio\EdnotifBundle\Shared\Mapper\StandardResponseMapper;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use stdClass;
#[CoversClass(CreateSortieResponseMapper::class)]
final class CreateSortieResponseMapperTest extends TestCase
{
public function testMapPendingBdniValidation(): void
{
$soapResponse = new stdClass();
$soapResponse->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
<?php
declare(strict_types=1);
namespace Malio\EdnotifBundle\Bovin\Dto;
use Malio\EdnotifBundle\Shared\Dto\StandardResponseDto;
/**
* Réponse de `IpBCreateSortie`.
*
* Si `$pendingBdniValidation === true`, EDNOTIF a accepté la requête mais
* attend la validation asynchrone de la BDNi — `identification`, `entryMovement`
* et `exitMovement` sont `null`. Sinon, EDNOTIF renvoie la **période de présence
* clôturée** : l'entrée initiale du bovin sur l'exploitation (`entryMovement`)
* **et** la sortie qui vient d'être déclarée (`exitMovement`).
*/
final readonly class CreateSortieResponseDto
{
public function __construct(
public StandardResponseDto $standardResponse,
public bool $pendingBdniValidation,
public ?BovinIdentificationDto $identification,
public ?MovementDto $entryMovement,
public ?MovementDto $exitMovement,
public ?object $rawSoapResponse,
) {}
}
```
- [ ] **Step 4: Créer `CreateSortieResponseMapper`**
Contenu complet de `src/Bovin/Mapper/CreateSortieResponseMapper.php` :
```php
<?php
declare(strict_types=1);
namespace Malio\EdnotifBundle\Bovin\Mapper;
use Malio\EdnotifBundle\Bovin\Dto\CreateSortieResponseDto;
use Malio\EdnotifBundle\Shared\Mapper\StandardResponseMapper;
final class CreateSortieResponseMapper
{
use BovinNodeMappingTrait;
public function __construct(
private readonly StandardResponseMapper $standardResponseMapper,
) {}
public function map(object $soapResponse): CreateSortieResponseDto
{
$standardResponse = $this->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