feat : écran bovins, refacto cases, enrichissement bovins, migrations

- Ajout page infrastructure/bovine avec CRUD
- Refacto BuildingCase (suppression Statut, simplification)
- Commande EnrichBovinesCommand pour enrichir les données bovins
- 4 migrations Doctrine
- Mise à jour composables shipment/weighing
- Mise à jour README et CHANGELOG

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthieu
2026-04-10 14:44:53 +02:00
parent 6eb2ee2578
commit 340aa2a3c0
30 changed files with 854 additions and 422 deletions

View File

@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace App\Command;
use App\Entity\Bovine;
use Doctrine\ORM\EntityManagerInterface;
use Malio\EdnotifBundle\Bovin\Api\BovinApiInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Throwable;
use function count;
#[AsCommand(
name: 'app:enrich-bovines',
description: 'Enrichit les bovins existants avec les données EdNotif (n° travail, date naissance, race).'
)]
class EnrichBovinesCommand extends Command
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly BovinApiInterface $bovinApi,
) {
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$bovines = $this->entityManager->getRepository(Bovine::class)->findBy(['workNumber' => null]);
if (0 === count($bovines)) {
$io->success('Tous les bovins sont déjà enrichis.');
return Command::SUCCESS;
}
$io->info(sprintf('%d bovin(s) à enrichir.', count($bovines)));
$enriched = 0;
$failed = 0;
foreach ($bovines as $bovine) {
try {
$animalFile = $this->bovinApi->getAnimalFile(
nationalNumber: $bovine->getNationalNumber(),
countryCode: 'FR',
);
$identification = $animalFile->identification;
if (null === $identification) {
$io->warning(sprintf(' %s — pas d\'identification retournée.', $bovine->getNationalNumber()));
++$failed;
continue;
}
$bovine->setWorkNumber($identification->workNumber);
$bovine->setBirthDate($identification->birthDate?->date);
$bovine->setBreedCode($this->normalizeBreedCode($identification->breedType));
++$enriched;
$io->text(sprintf(' ✓ %s → n° travail %s', $bovine->getNationalNumber(), $identification->workNumber ?? '—'));
} catch (Throwable $e) {
++$failed;
$io->warning(sprintf(' %s — erreur : %s', $bovine->getNationalNumber(), $e->getMessage()));
}
}
$this->entityManager->flush();
$io->success(sprintf('%d enrichi(s), %d échoué(s).', $enriched, $failed));
return Command::SUCCESS;
}
private function normalizeBreedCode(mixed $breedType): ?string
{
if (null === $breedType) {
return null;
}
if (is_numeric($breedType)) {
return (string) $breedType;
}
if (is_string($breedType) && preg_match('/\d+/', $breedType, $matches)) {
return $matches[0];
}
return null;
}
}

View File

@@ -18,7 +18,6 @@ use App\Entity\MerchandiseType;
use App\Entity\PelletType;
use App\Entity\ReceptionType;
use App\Entity\ShipmentType;
use App\Entity\Statut;
use App\Entity\Supplier;
use App\Entity\Truck;
use App\Entity\Vehicle;
@@ -230,24 +229,6 @@ class SeedCommand extends Command
private function seedBuildingInfrastructure(): void
{
$statusByCode = [];
$statusRows = [
['label' => 'Libre', 'code' => 'LB', 'color' => '#A3B18A'],
['label' => 'Occupé', 'code' => 'OC', 'color' => '#3A506B'],
['label' => 'Malade', 'code' => 'ML', 'color' => '#E07A5F'],
];
foreach ($statusRows as $statusRow) {
/** @var Statut $status */
$status = $this->upsertByCode(Statut::class, $statusRow['code'], static function (Statut $entity) use ($statusRow) {
$entity
->setLabel($statusRow['label'])
->setCode($statusRow['code'])
->setColor($statusRow['color'])
;
});
$statusByCode[$statusRow['code']] = $status;
}
$buildingRepo = $this->entityManager->getRepository(Building::class);
$layoutByBuildingCode = [];
$layoutRows = [
@@ -274,25 +255,15 @@ class SeedCommand extends Command
}
$caseRows = [
['buildingCode' => 'B1', 'from' => 1, 'to' => 12, 'status' => 'LB'],
['buildingCode' => 'B1', 'from' => 13, 'to' => 24, 'status' => 'OC'],
['buildingCode' => 'B1', 'from' => 25, 'to' => 32, 'status' => 'ML'],
['buildingCode' => 'B1', 'from' => 33, 'to' => 44, 'status' => 'LB'],
['buildingCode' => 'B2', 'from' => 1, 'to' => 10, 'status' => 'OC'],
['buildingCode' => 'B2', 'from' => 11, 'to' => 22, 'status' => 'LB'],
['buildingCode' => 'B2', 'from' => 23, 'to' => 30, 'status' => 'ML'],
['buildingCode' => 'B2', 'from' => 31, 'to' => 44, 'status' => 'OC'],
['buildingCode' => 'B3', 'from' => 1, 'to' => 8, 'status' => 'ML'],
['buildingCode' => 'B3', 'from' => 9, 'to' => 20, 'status' => 'LB'],
['buildingCode' => 'B3', 'from' => 21, 'to' => 34, 'status' => 'OC'],
['buildingCode' => 'B3', 'from' => 35, 'to' => 44, 'status' => 'ML'],
['buildingCode' => 'B1', 'from' => 1, 'to' => 44],
['buildingCode' => 'B2', 'from' => 1, 'to' => 44],
['buildingCode' => 'B3', 'from' => 1, 'to' => 44],
];
$caseByCode = [];
foreach ($caseRows as $caseRow) {
$building = $buildingRepo->findOneBy(['code' => $caseRow['buildingCode']]);
$status = $statusByCode[$caseRow['status']] ?? null;
if (!$building instanceof Building || !$status instanceof Statut) {
if (!$building instanceof Building) {
continue;
}
@@ -300,13 +271,12 @@ class SeedCommand extends Command
$code = sprintf('%s-C%d', $caseRow['buildingCode'], $caseNumber);
/** @var BuildingCase $buildingCase */
$buildingCase = $this->upsertByCode(BuildingCase::class, $code, static function (BuildingCase $entity) use ($code, $caseNumber, $building, $status) {
$buildingCase = $this->upsertByCode(BuildingCase::class, $code, static function (BuildingCase $entity) use ($code, $caseNumber, $building) {
$entity
->setCode($code)
->setCaseNumber($caseNumber)
->setCapacity(15)
->setIdBuilding($building)
->setStatut($status)
;
});
$caseByCode[$code] = $buildingCase;

View File

@@ -8,7 +8,6 @@ use App\Entity\Building;
use App\Entity\BuildingCase;
use App\Entity\BuildingCasePosition;
use App\Entity\BuildingLayout;
use App\Entity\Statut;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
use Doctrine\Persistence\ObjectManager;
@@ -18,10 +17,9 @@ class BuildingInfrastructureFixtures extends Fixture implements DependentFixture
{
public function load(ObjectManager $manager): void
{
$statuts = $this->loadStatuts($manager);
$buildings = $this->getBuildingsByCode($manager, ['B1', 'B2', 'B3']);
$layouts = $this->loadLayouts($manager, $buildings);
$cases = $this->loadBuildingCases($manager, $buildings, $statuts);
$cases = $this->loadBuildingCases($manager, $buildings);
$this->loadCasePositions($manager, $layouts, $cases);
$manager->flush();
@@ -34,38 +32,6 @@ class BuildingInfrastructureFixtures extends Fixture implements DependentFixture
];
}
/**
* @return array<string, Statut>
*/
private function loadStatuts(ObjectManager $manager): array
{
$repo = $manager->getRepository(Statut::class);
$data = [
['label' => 'Libre', 'code' => 'LB', 'color' => '#A3B18A'],
['label' => 'Occupé', 'code' => 'OC', 'color' => '#3A506B'],
['label' => 'Malade', 'code' => 'ML', 'color' => '#E07A5F'],
];
$result = [];
foreach ($data as $row) {
/** @var null|Statut $statut */
$statut = $repo->findOneBy(['code' => $row['code']]);
if (!$statut instanceof Statut) {
$statut = new Statut()
->setLabel($row['label'])
->setCode($row['code'])
->setColor($row['color'])
;
$manager->persist($statut);
}
$result[$row['code']] = $statut;
}
return $result;
}
/**
* @param list<string> $codes
*
@@ -126,34 +92,21 @@ class BuildingInfrastructureFixtures extends Fixture implements DependentFixture
/**
* @param array<string, Building> $buildings
* @param array<string, Statut> $statuts
*
* @return array<string, BuildingCase>
*/
private function loadBuildingCases(ObjectManager $manager, array $buildings, array $statuts): array
private function loadBuildingCases(ObjectManager $manager, array $buildings): array
{
$repo = $manager->getRepository(BuildingCase::class);
$statusRanges = [
// B1
['buildingCode' => 'B1', 'from' => 1, 'to' => 12, 'statut' => 'LB'],
['buildingCode' => 'B1', 'from' => 13, 'to' => 24, 'statut' => 'OC'],
['buildingCode' => 'B1', 'from' => 25, 'to' => 32, 'statut' => 'ML'],
['buildingCode' => 'B1', 'from' => 33, 'to' => 44, 'statut' => 'LB'],
// B2
['buildingCode' => 'B2', 'from' => 1, 'to' => 10, 'statut' => 'OC'],
['buildingCode' => 'B2', 'from' => 11, 'to' => 22, 'statut' => 'LB'],
['buildingCode' => 'B2', 'from' => 23, 'to' => 30, 'statut' => 'ML'],
['buildingCode' => 'B2', 'from' => 31, 'to' => 44, 'statut' => 'OC'],
// B3
['buildingCode' => 'B3', 'from' => 1, 'to' => 8, 'statut' => 'ML'],
['buildingCode' => 'B3', 'from' => 9, 'to' => 20, 'statut' => 'LB'],
['buildingCode' => 'B3', 'from' => 21, 'to' => 34, 'statut' => 'OC'],
['buildingCode' => 'B3', 'from' => 35, 'to' => 44, 'statut' => 'ML'],
$caseRanges = [
['buildingCode' => 'B1', 'from' => 1, 'to' => 44],
['buildingCode' => 'B2', 'from' => 1, 'to' => 44],
['buildingCode' => 'B3', 'from' => 1, 'to' => 44],
];
$result = [];
foreach ($statusRanges as $range) {
foreach ($caseRanges as $range) {
for ($caseNumber = $range['from']; $caseNumber <= $range['to']; ++$caseNumber) {
$code = sprintf('%s-C%d', $range['buildingCode'], $caseNumber);
@@ -169,7 +122,6 @@ class BuildingInfrastructureFixtures extends Fixture implements DependentFixture
->setCode($code)
->setCapacity(15)
->setIdBuilding($buildings[$range['buildingCode']])
->setStatut($statuts[$range['statut']])
;
$manager->persist($buildingCase);
}

View File

@@ -9,6 +9,7 @@ use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use App\State\BovineProcessor;
use DateTimeImmutable;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Context;
@@ -31,12 +32,14 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
normalizationContext: ['groups' => ['bovine:read']],
denormalizationContext: ['groups' => ['bovine:write']],
security: "is_granted('ROLE_ADMIN')",
processor: BovineProcessor::class,
),
new Patch(
requirements: ['id' => '\d+'],
normalizationContext: ['groups' => ['bovine:read']],
denormalizationContext: ['groups' => ['bovine:write']],
security: "is_granted('ROLE_ADMIN')",
processor: BovineProcessor::class,
),
],
security: "is_granted('ROLE_USER')",
@@ -46,19 +49,19 @@ class Bovine
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['bovine:read'])]
#[Groups(['bovine:read', 'building_case:read'])]
private ?int $id = null;
#[ORM\Column(length: 50)]
#[Groups(['bovine:read', 'bovine:write'])]
#[Groups(['bovine:read', 'bovine:write', 'building_case:read'])]
private string $nationalNumber = '';
#[ORM\Column(nullable: true)]
#[Groups(['bovine:read', 'bovine:write'])]
#[Groups(['bovine:read', 'bovine:write', 'building_case:read'])]
private ?int $receivedWeight = null;
#[ORM\Column(type: 'date_immutable', nullable: true)]
#[Groups(['bovine:read', 'bovine:write'])]
#[Groups(['bovine:read', 'bovine:write', 'building_case:read'])]
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
private ?DateTimeImmutable $arrivalDate = null;
@@ -66,6 +69,23 @@ class Bovine
#[Groups(['bovine:read', 'bovine:write'])]
private ?BuildingCase $buildingCase = null;
#[ORM\ManyToOne]
#[Groups(['bovine:read', 'bovine:write', 'building_case:read'])]
private ?Supplier $supplier = null;
#[ORM\Column(length: 50, nullable: true)]
#[Groups(['bovine:read', 'building_case:read'])]
private ?string $workNumber = null;
#[ORM\Column(type: 'date_immutable', nullable: true)]
#[Groups(['bovine:read', 'building_case:read'])]
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
private ?DateTimeImmutable $birthDate = null;
#[ORM\Column(length: 20, nullable: true)]
#[Groups(['bovine:read', 'building_case:read'])]
private ?string $breedCode = null;
public function getId(): ?int
{
return $this->id;
@@ -118,4 +138,52 @@ class Bovine
return $this;
}
public function getSupplier(): ?Supplier
{
return $this->supplier;
}
public function setSupplier(?Supplier $supplier): static
{
$this->supplier = $supplier;
return $this;
}
public function getWorkNumber(): ?string
{
return $this->workNumber;
}
public function setWorkNumber(?string $workNumber): static
{
$this->workNumber = $workNumber;
return $this;
}
public function getBirthDate(): ?DateTimeImmutable
{
return $this->birthDate;
}
public function setBirthDate(?DateTimeImmutable $birthDate): static
{
$this->birthDate = $birthDate;
return $this;
}
public function getBreedCode(): ?string
{
return $this->breedCode;
}
public function setBreedCode(?string $breedCode): static
{
$this->breedCode = $breedCode;
return $this;
}
}

View File

@@ -32,15 +32,15 @@ class Building
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['building:read', 'reception:read'])]
#[Groups(['building:read', 'building:summary', 'reception:read'])]
private ?int $id = null;
#[ORM\Column(length: 120)]
#[Groups(['building:read', 'reception:read'])]
#[Groups(['building:read', 'building:summary', 'reception:read'])]
private string $label = '';
#[ORM\Column(length: 50)]
#[Groups(['building:read', 'reception:read'])]
#[Groups(['building:read', 'building:summary', 'reception:read'])]
private string $code = '';
/**

View File

@@ -19,7 +19,7 @@ use Symfony\Component\Serializer\Attribute\SerializedName;
operations: [
new Get(
requirements: ['id' => '\d+'],
normalizationContext: ['groups' => ['building:read']],
normalizationContext: ['groups' => ['building_case:read', 'building:summary']],
),
new Get(
uriTemplate: '/building_cases/{id}/weights-report',
@@ -39,20 +39,20 @@ class BuildingCase
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['building:read'])]
#[Groups(['building:read', 'building_case:read'])]
private ?int $id = null;
#[ORM\Column]
#[Groups(['building:read'])]
#[Groups(['building:read', 'building_case:read'])]
#[SerializedName('caseNumber')]
private ?int $case_number = null;
#[ORM\Column(length: 255)]
#[Groups(['building:read'])]
#[Groups(['building:read', 'building_case:read'])]
private ?string $code = null;
#[ORM\Column]
#[Groups(['building:read'])]
#[Groups(['building:read', 'building_case:read'])]
private ?int $capacity = null;
/**
@@ -62,16 +62,15 @@ class BuildingCase
private Collection $id_case_position;
#[ORM\ManyToOne(inversedBy: 'buildingCases')]
#[Groups(['building_case:read'])]
#[SerializedName('building')]
private ?Building $id_building = null;
#[ORM\ManyToOne(inversedBy: 'id_case')]
#[Groups(['building:read'])]
private ?Statut $statut = null;
/**
* @var Collection<int, Bovine>
*/
#[ORM\OneToMany(targetEntity: Bovine::class, mappedBy: 'buildingCase')]
#[Groups(['building_case:read'])]
private Collection $bovines;
public function __construct()
@@ -170,16 +169,17 @@ class BuildingCase
return $this;
}
public function getStatut(): ?Statut
/**
* @return array{label: string, couleur: string}
*/
#[Groups(['building:read', 'building_case:read'])]
public function getStatut(): array
{
return $this->statut;
}
if ($this->bovines->count() > 0) {
return ['label' => 'Occupé', 'couleur' => '#3A506B'];
}
public function setStatut(?Statut $statut): static
{
$this->statut = $statut;
return $this;
return ['label' => 'Libre', 'couleur' => '#A3B18A'];
}
/**

View File

@@ -1,138 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Serializer\Attribute\SerializedName;
#[ORM\Entity]
#[ApiResource(
operations: [
new Get(
requirements: ['id' => '\d+'],
normalizationContext: ['groups' => ['building:read']],
),
new GetCollection(
normalizationContext: ['groups' => ['building:read']],
),
],
security: "is_granted('ROLE_USER')",
)]
class Statut
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['building:read'])]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Groups(['building:read'])]
private ?string $label = null;
#[ORM\Column(length: 255)]
#[Groups(['building:read'])]
private ?string $code = null;
#[ORM\Column(length: 255)]
#[Groups(['building:read'])]
#[SerializedName('couleur')]
private ?string $color = null;
/**
* @var Collection<int, BuildingCase>
*/
#[ORM\OneToMany(targetEntity: BuildingCase::class, mappedBy: 'statut')]
private Collection $id_case;
public function __construct()
{
$this->id_case = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function setId(int $id): static
{
$this->id = $id;
return $this;
}
public function getLabel(): ?string
{
return $this->label;
}
public function setLabel(string $label): static
{
$this->label = $label;
return $this;
}
public function getCode(): ?string
{
return $this->code;
}
public function setCode(string $code): static
{
$this->code = $code;
return $this;
}
public function getColor(): ?string
{
return $this->color;
}
public function setColor(string $color): static
{
$this->color = $color;
return $this;
}
/**
* @return Collection<int, BuildingCase>
*/
public function getIdCase(): Collection
{
return $this->id_case;
}
public function addIdCase(BuildingCase $idCase): static
{
if (!$this->id_case->contains($idCase)) {
$this->id_case->add($idCase);
$idCase->setStatut($this);
}
return $this;
}
public function removeIdCase(BuildingCase $idCase): static
{
if ($this->id_case->removeElement($idCase)) {
// set the owning side to null (unless already changed)
if ($idCase->getStatut() === $this) {
$idCase->setStatut(null);
}
}
return $this;
}
}

View File

@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Entity\Bovine;
use Malio\EdnotifBundle\Bovin\Api\BovinApiInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Throwable;
final class BovineProcessor implements ProcessorInterface
{
public function __construct(
private readonly BovinApiInterface $bovinApi,
#[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')]
private readonly ProcessorInterface $persistProcessor,
) {}
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
{
if ($data instanceof Bovine && '' !== $data->getNationalNumber()) {
$this->enrichFromEdnotif($data);
}
return $this->persistProcessor->process($data, $operation, $uriVariables, $context);
}
private function enrichFromEdnotif(Bovine $bovine): void
{
try {
$animalFile = $this->bovinApi->getAnimalFile(
nationalNumber: $bovine->getNationalNumber(),
countryCode: 'FR',
);
$identification = $animalFile->identification;
if (null === $identification) {
return;
}
$bovine->setWorkNumber($identification->workNumber);
$bovine->setBirthDate($identification->birthDate?->date);
$bovine->setBreedCode($this->normalizeBreedCode($identification->breedType));
} catch (Throwable) {
// External service unavailable — persist bovine without enrichment.
}
}
private function normalizeBreedCode(mixed $breedType): ?string
{
if (null === $breedType) {
return null;
}
if (is_numeric($breedType)) {
return (string) $breedType;
}
if (is_string($breedType) && preg_match('/\d+/', $breedType, $matches)) {
return $matches[0];
}
return null;
}
}

View File

@@ -11,10 +11,8 @@ use App\Entity\BuildingCase;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Dompdf\Dompdf;
use Malio\EdnotifBundle\Bovin\Api\BovinApiInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Throwable;
use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
@@ -40,7 +38,6 @@ final readonly class BuildingCaseWeightsReportProvider implements ProviderInterf
public function __construct(
private Environment $twig,
private EntityManagerInterface $entityManager,
private BovinApiInterface $bovinApi,
) {}
/**
@@ -68,24 +65,9 @@ final readonly class BuildingCaseWeightsReportProvider implements ProviderInterf
continue;
}
$workNumber = null;
$birthDate = null;
$breedCode = null;
try {
$animalFileDto = $this->bovinApi->getAnimalFile(
nationalNumber: $bovine->getNationalNumber(),
countryCode: 'FR',
);
$workNumber = $animalFileDto->identification?->workNumber;
$birthDate = $animalFileDto->identification?->birthDate?->date?->format('d/m/y');
$breedCode = $this->normalizeBreedCode($animalFileDto->identification?->breedType);
if (null === $headerBreedCode && null !== $breedCode) {
$headerBreedCode = $breedCode;
}
} catch (Throwable) {
// Keep row data even if external identification service is unavailable.
$breedCode = $bovine->getBreedCode();
if (null === $headerBreedCode && null !== $breedCode) {
$headerBreedCode = $breedCode;
}
$arrivalDate = $bovine->getArrivalDate();
@@ -101,8 +83,8 @@ final readonly class BuildingCaseWeightsReportProvider implements ProviderInterf
$rows[] = [
'nationalNumber' => $bovine->getNationalNumber(),
'workNumber' => $workNumber,
'birthDate' => $birthDate,
'workNumber' => $bovine->getWorkNumber(),
'birthDate' => $bovine->getBirthDate()?->format('d/m/y'),
'receivedWeight' => $bovine->getReceivedWeight(),
'arrivalDate' => $bovine->getArrivalDate()?->format('d/m/Y'),
'projectedWeights' => $projectedWeights,
@@ -131,23 +113,6 @@ final readonly class BuildingCaseWeightsReportProvider implements ProviderInterf
]);
}
private function normalizeBreedCode(mixed $breedType): ?string
{
if (null === $breedType) {
return null;
}
if (is_numeric($breedType)) {
return (string) $breedType;
}
if (is_string($breedType) && preg_match('/\d+/', $breedType, $matches)) {
return $matches[0];
}
return null;
}
private function resolveDailyGainKg(?string $breedCode): float
{
return 1.3;