feat: ajout des notifications d'entrées/sorties #5
@@ -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%',
|
||||
|
||||
997
docs/superpowers/plans/2026-04-22-phase2-create-entree-sortie.md
Normal file
997
docs/superpowers/plans/2026-04-22-phase2-create-entree-sortie.md
Normal 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
|
||||
@@ -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 `<documentation>` 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
|
||||
}
|
||||
```
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
28
src/Bovin/Dto/CreateEntreeRequest.php
Normal file
28
src/Bovin/Dto/CreateEntreeRequest.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?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,
|
||||
) {}
|
||||
}
|
||||
26
src/Bovin/Dto/CreateEntreeResponseDto.php
Normal file
26
src/Bovin/Dto/CreateEntreeResponseDto.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?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,
|
||||
) {}
|
||||
}
|
||||
21
src/Bovin/Dto/CreateSortieRequest.php
Normal file
21
src/Bovin/Dto/CreateSortieRequest.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?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,
|
||||
) {}
|
||||
}
|
||||
28
src/Bovin/Dto/CreateSortieResponseDto.php
Normal file
28
src/Bovin/Dto/CreateSortieResponseDto.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?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,
|
||||
) {}
|
||||
}
|
||||
54
src/Bovin/Enum/CategorieBovinIPG.php
Normal file
54
src/Bovin/Enum/CategorieBovinIPG.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?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';
|
||||
}
|
||||
23
src/Bovin/Enum/CauseEntree.php
Normal file
23
src/Bovin/Enum/CauseEntree.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?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';
|
||||
}
|
||||
36
src/Bovin/Enum/CauseSortie.php
Normal file
36
src/Bovin/Enum/CauseSortie.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?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';
|
||||
}
|
||||
56
src/Bovin/Mapper/CreateEntreeResponseMapper.php
Normal file
56
src/Bovin/Mapper/CreateEntreeResponseMapper.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?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,
|
||||
);
|
||||
}
|
||||
}
|
||||
66
src/Bovin/Mapper/CreateSortieResponseMapper.php
Normal file
66
src/Bovin/Mapper/CreateSortieResponseMapper.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?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,
|
||||
);
|
||||
}
|
||||
}
|
||||
68
tests/Unit/Bovin/Mapper/CreateEntreeResponseMapperTest.php
Normal file
68
tests/Unit/Bovin/Mapper/CreateEntreeResponseMapperTest.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
77
tests/Unit/Bovin/Mapper/CreateSortieResponseMapperTest.php
Normal file
77
tests/Unit/Bovin/Mapper/CreateSortieResponseMapperTest.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user