map($this->makeSoapResponse(), $this->makeUnzippedMessage()); self::assertInstanceOf(InventoryDto::class, $inventory); self::assertTrue($inventory->standardResponse->result); self::assertSame(2, $inventory->nbBovins); self::assertEquals(new DateTimeImmutable('2026-01-01'), $inventory->startDate); self::assertEquals(new DateTimeImmutable('2026-01-31'), $inventory->endDate); self::assertTrue($inventory->includesEarTagStock); 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 { $mapper = new InventoryMapper(new AnimalSummaryMapper(), new StandardResponseMapper()); $soapResponse = new stdClass(); $soapResponse->ReponseStandard = new stdClass(); $soapResponse->ReponseStandard->Resultat = true; $soapResponse->ReponseSpecifique = new stdClass(); $soapResponse->ReponseSpecifique->NbBovins = 0; $inventory = $mapper->map($soapResponse, null); self::assertSame(0, $inventory->nbBovins); self::assertSame([], $inventory->animals); self::assertSame([], $inventory->earTagSeries); self::assertNull($inventory->startDate); } public function testMapInventoryWithStockBouclesAbsentYieldsNullFlag(): void { $mapper = new InventoryMapper(new AnimalSummaryMapper(), new StandardResponseMapper()); $message = new stdClass(); $message->InformationsMessage = new stdClass(); $message->InformationsMessage->DateDebut = '2026-01-01'; // StockBoucles deliberately omitted $inventory = $mapper->map($this->makeSoapResponse(), $message); self::assertNull($inventory->includesEarTagStock); } public function testMapInventoryWithStockBouclesZeroYieldsFalseFlag(): void { $mapper = new InventoryMapper(new AnimalSummaryMapper(), new StandardResponseMapper()); $message = new stdClass(); $message->InformationsMessage = new stdClass(); $message->InformationsMessage->StockBoucles = '0'; $inventory = $mapper->map($this->makeSoapResponse(), $message); self::assertFalse($inventory->includesEarTagStock); } public function testMapInventoryWithDateFinAbsentYieldsNullEndDate(): void { $mapper = new InventoryMapper(new AnimalSummaryMapper(), new StandardResponseMapper()); $message = new stdClass(); $message->InformationsMessage = new stdClass(); $message->InformationsMessage->DateDebut = '2026-01-01'; // DateFin deliberately omitted $inventory = $mapper->map($this->makeSoapResponse(), $message); self::assertNull($inventory->endDate); } public function testMapInventoryWithSerieBouclesAsListPreservesAllEntries(): void { $mapper = new InventoryMapper(new AnimalSummaryMapper(), new StandardResponseMapper()); $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(); $message->Boucles->SerieBoucles = [$serie1, $serie2]; $inventory = $mapper->map($this->makeSoapResponse(), $message); self::assertCount(2, $inventory->earTagSeries); 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 { $mapper = new InventoryMapper(new AnimalSummaryMapper(), new StandardResponseMapper()); $soapResponse = new stdClass(); $soapResponse->ReponseStandard = new stdClass(); $soapResponse->ReponseStandard->Resultat = true; $soapResponse->ReponseSpecifique = new stdClass(); // NbBovins deliberately omitted $inventory = $mapper->map($soapResponse, null); self::assertSame(0, $inventory->nbBovins); } private function makeSoapResponse(): object { $response = new stdClass(); $response->ReponseStandard = new stdClass(); $response->ReponseStandard->Resultat = true; $response->ReponseSpecifique = new stdClass(); $response->ReponseSpecifique->NbBovins = 2; return $response; } private function makeUnzippedMessage(): object { $message = new stdClass(); $message->InformationsMessage = new stdClass(); $message->InformationsMessage->DateDebut = '2026-01-01'; $message->InformationsMessage->DateFin = '2026-01-31'; $message->InformationsMessage->StockBoucles = '1'; $message->Bovins = new stdClass(); $message->Bovins->Bovin = [ $this->makeAnimalNode('FR123'), $this->makeAnimalNode('FR456'), ]; $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; } private function makeAnimalNode(string $nationalNumber): object { $node = new stdClass(); $node->IdentiteBovin = new stdClass(); $node->IdentiteBovin->Bovin = new stdClass(); $node->IdentiteBovin->Bovin->NumeroNational = $nationalNumber; $node->PeriodesPresences = new stdClass(); $node->PeriodesPresences->PeriodePresence = new stdClass(); $node->PeriodesPresences->PeriodePresence->Entree = new stdClass(); $node->PeriodesPresences->PeriodePresence->Entree->DateEntree = '2025-05-01'; return $node; } }