From c2ed7597eff010a60ca22d03c095316ec96c5483 Mon Sep 17 00:00:00 2001 From: tristan Date: Wed, 22 Apr 2026 09:06:51 +0200 Subject: [PATCH] =?UTF-8?q?feat=20:=20parser=20SerieBoucles=20dans=20EarTa?= =?UTF-8?q?gSeriesDto=20typ=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- src/Bovin/Dto/EarTagSeriesDto.php | 29 ++++++++--- src/Bovin/Mapper/InventoryMapper.php | 6 ++- tests/Unit/Bovin/Dto/EarTagSeriesDtoTest.php | 49 +++++++++++++++++++ .../Unit/Bovin/Mapper/InventoryMapperTest.php | 30 ++++++++---- 4 files changed, 96 insertions(+), 18 deletions(-) create mode 100644 tests/Unit/Bovin/Dto/EarTagSeriesDtoTest.php diff --git a/src/Bovin/Dto/EarTagSeriesDto.php b/src/Bovin/Dto/EarTagSeriesDto.php index 0e9e0d3..1098af2 100644 --- a/src/Bovin/Dto/EarTagSeriesDto.php +++ b/src/Bovin/Dto/EarTagSeriesDto.php @@ -5,18 +5,31 @@ declare(strict_types=1); namespace Malio\EdnotifBundle\Bovin\Dto; /** - * Wrapper minimal d'une entrée `SerieBoucles` du message de l'opération - * `IpBGetInventaire` (quand `includeEarTagStock: true`). + * Série de boucles (ear tags) en stock chez l'exploitation, retournée dans + * la réponse de `IpBGetInventaire` quand `includeEarTagStock: true`. * - * Le noeud XSD `typeSerieBoucles` est riche (plages de numéros, fournisseur, - * statut, dates...). En Phase 1 on garde volontairement la structure brute - * via `$rawNode` : les consommateurs qui ont besoin d'un champ précis y - * accèdent par `$serie->rawNode->NomDuChamp`. Si un cas d'usage métier concret - * remonte (rebouclage, commande de boucles), on parsera dans un DTO dédié. + * Correspond au type XSD `typeSerieBoucles` (resources/ednotif-ws/IpBNotif_v1.xsd) : + * une plage contigüe de `quantity` boucles à partir du numéro `startNumber` + * dans le pays `countryCode`. + * + * Les numéros sont stockés en string pour préserver le zero-padding du XSD + * (10 chiffres, pattern `0[1-9][0-9]{8}|[1-9][0-9]{9}`). */ final readonly class EarTagSeriesDto { public function __construct( - public object $rawNode, + public string $countryCode, + public string $startNumber, + public int $quantity, ) {} + + public function endNumber(): string + { + return str_pad( + (string) ((int) $this->startNumber + $this->quantity - 1), + 10, + '0', + STR_PAD_LEFT, + ); + } } diff --git a/src/Bovin/Mapper/InventoryMapper.php b/src/Bovin/Mapper/InventoryMapper.php index 3787ec1..50eaea3 100644 --- a/src/Bovin/Mapper/InventoryMapper.php +++ b/src/Bovin/Mapper/InventoryMapper.php @@ -50,7 +50,11 @@ final class InventoryMapper if (!is_object($serieNode)) { continue; } - $earTagSeries[] = new EarTagSeriesDto(rawNode: $serieNode); + $earTagSeries[] = new EarTagSeriesDto( + countryCode: $this->toNullableString($serieNode->CodePays ?? null) ?? '', + startNumber: $this->toNullableString($serieNode->DebutSerie ?? null) ?? '', + quantity: $this->toNullableInt($serieNode->Quantite ?? null) ?? 0, + ); } } diff --git a/tests/Unit/Bovin/Dto/EarTagSeriesDtoTest.php b/tests/Unit/Bovin/Dto/EarTagSeriesDtoTest.php new file mode 100644 index 0000000..ed2ffe5 --- /dev/null +++ b/tests/Unit/Bovin/Dto/EarTagSeriesDtoTest.php @@ -0,0 +1,49 @@ +endNumber()); + } + + public function testEndNumberPreservesLeadingZeroPadding(): void + { + $series = new EarTagSeriesDto( + countryCode: 'FR', + startNumber: '0000000001', + quantity: 5, + ); + + self::assertSame('0000000005', $series->endNumber()); + } + + public function testEndNumberWithQuantityOneEqualsStartNumber(): void + { + $series = new EarTagSeriesDto( + countryCode: 'FR', + startNumber: '0012345678', + quantity: 1, + ); + + self::assertSame('0012345678', $series->endNumber()); + } +} diff --git a/tests/Unit/Bovin/Mapper/InventoryMapperTest.php b/tests/Unit/Bovin/Mapper/InventoryMapperTest.php index 2c4a524..4dfefa2 100644 --- a/tests/Unit/Bovin/Mapper/InventoryMapperTest.php +++ b/tests/Unit/Bovin/Mapper/InventoryMapperTest.php @@ -34,6 +34,9 @@ final class InventoryMapperTest extends TestCase self::assertCount(2, $inventory->animals); self::assertCount(1, $inventory->earTagSeries); self::assertSame('FR123', $inventory->animals[0]->identification?->bovin?->nationalNumber); + self::assertSame('FR', $inventory->earTagSeries[0]->countryCode); + self::assertSame('0012345678', $inventory->earTagSeries[0]->startNumber); + self::assertSame(50, $inventory->earTagSeries[0]->quantity); } public function testMapInventoryWithoutMessageZipReturnsEmptyLists(): void @@ -99,10 +102,15 @@ final class InventoryMapperTest extends TestCase { $mapper = new InventoryMapper(new AnimalSummaryMapper(), new StandardResponseMapper()); - $serie1 = new stdClass(); - $serie1->NumeroSerieDebut = 'A0001'; - $serie2 = new stdClass(); - $serie2->NumeroSerieDebut = 'B0001'; + $serie1 = new stdClass(); + $serie1->CodePays = 'FR'; + $serie1->DebutSerie = '0012345678'; + $serie1->Quantite = 10; + + $serie2 = new stdClass(); + $serie2->CodePays = 'FR'; + $serie2->DebutSerie = '0055500000'; + $serie2->Quantite = 25; $message = new stdClass(); $message->Boucles = new stdClass(); @@ -111,8 +119,10 @@ final class InventoryMapperTest extends TestCase $inventory = $mapper->map($this->makeSoapResponse(), $message); self::assertCount(2, $inventory->earTagSeries); - self::assertSame('A0001', $inventory->earTagSeries[0]->rawNode->NumeroSerieDebut); - self::assertSame('B0001', $inventory->earTagSeries[1]->rawNode->NumeroSerieDebut); + self::assertSame('0012345678', $inventory->earTagSeries[0]->startNumber); + self::assertSame(10, $inventory->earTagSeries[0]->quantity); + self::assertSame('0055500000', $inventory->earTagSeries[1]->startNumber); + self::assertSame(25, $inventory->earTagSeries[1]->quantity); } public function testMapInventoryWithMissingNbBovinsDefaultsToZero(): void @@ -153,9 +163,11 @@ final class InventoryMapperTest extends TestCase $this->makeAnimalNode('FR123'), $this->makeAnimalNode('FR456'), ]; - $message->Boucles = new stdClass(); - $message->Boucles->SerieBoucles = new stdClass(); - $message->Boucles->SerieBoucles->NumeroSerieDebut = 'A0001'; + $message->Boucles = new stdClass(); + $message->Boucles->SerieBoucles = new stdClass(); + $message->Boucles->SerieBoucles->CodePays = 'FR'; + $message->Boucles->SerieBoucles->DebutSerie = '0012345678'; + $message->Boucles->SerieBoucles->Quantite = 50; return $message; }