Compare commits

...

2 Commits

Author SHA1 Message Date
gitea-actions
de39207102 chore: bump version to v0.0.103
All checks were successful
Auto Tag Develop / tag (push) Successful in 7s
Build Release Artefact / build (push) Successful in 1m34s
2026-05-18 07:40:20 +00:00
5da0003c4d [#FER-25] Ajout un cron pour la synchro de l'inventaire bovin (!55)
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
| Numéro du ticket | Titre du ticket |
|------------------|-----------------|
|                  |                 |

## Description de la PR

## Modification du .env

## Check list

- [x] Pas de régression
- [ ] TU/TI/TF rédigée
- [x] TU/TI/TF OK
- [x] CHANGELOG modifié

Reviewed-on: #55
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-05-18 07:40:10 +00:00
5 changed files with 193 additions and 123 deletions

View File

@@ -67,6 +67,7 @@ Ajouter dans le fichier .env du frontend
* [#FER-18] Mise à jour du tableau d'arrivage * [#FER-18] Mise à jour du tableau d'arrivage
* [#FER-26] Passeport du bovin * [#FER-26] Passeport du bovin
* [#FER-27] Fix export inventaire bovin * [#FER-27] Fix export inventaire bovin
* [#FER-25] Ajout un cron pour la synchro de l'inventaire bovin
### Changed ### Changed

View File

@@ -1,2 +1,2 @@
parameters: parameters:
app.version: '0.0.102' app.version: '0.0.103'

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Command;
use App\Service\BovineInventorySyncer;
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;
#[AsCommand(
name: 'app:sync-bovine-inventory',
description: "Synchronise l'inventaire bovin avec EDNOTIF (équivalent du bouton Rafraîchir de l'interface)."
)]
final class SyncBovineInventoryCommand extends Command
{
public function __construct(
private readonly BovineInventorySyncer $syncer,
) {
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
try {
$result = $this->syncer->sync();
} catch (Throwable $e) {
$io->error(sprintf('Échec de la synchronisation : %s', $e->getMessage()));
return Command::FAILURE;
}
$io->success(sprintf(
'Inventaire synchronisé · Créés : %d · Mis à jour : %d · Sortis : %d · Total EDNOTIF : %d',
$result->created,
$result->updated,
$result->exited,
$result->total,
));
return Command::SUCCESS;
}
}

View File

@@ -0,0 +1,138 @@
<?php
declare(strict_types=1);
namespace App\Service;
use App\ApiResource\BovineSyncInventoryResult;
use App\Entity\Bovine;
use App\Entity\BovineType;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Malio\EdnotifBundle\Bovin\Api\BovinApiInterface;
use Malio\EdnotifBundle\Bovin\Dto\AnimalSummaryDto;
final class BovineInventorySyncer
{
/**
* @var array<string, BovineType>
*/
private array $bovineTypeCache = [];
public function __construct(
private readonly BovinApiInterface $bovinApi,
private readonly EntityManagerInterface $em,
) {}
public function sync(): BovineSyncInventoryResult
{
$inventory = $this->bovinApi->getInventory(new DateTimeImmutable('today'));
$result = new BovineSyncInventoryResult();
$result->total = count($inventory->animals);
$this->bovineTypeCache = [];
foreach ($this->em->getRepository(BovineType::class)->findAll() as $bovineType) {
if (null !== $bovineType->getCode()) {
$this->bovineTypeCache[$bovineType->getCode()] = $bovineType;
}
}
$existingByNationalNumber = [];
foreach ($this->em->getRepository(Bovine::class)->findAll() as $bovine) {
$existingByNationalNumber[$bovine->getNationalNumber()] = $bovine;
}
$seen = [];
foreach ($inventory->animals as $animal) {
$nationalNumber = $animal->identification?->bovin?->nationalNumber;
if (null === $nationalNumber || '' === $nationalNumber) {
continue;
}
$seen[$nationalNumber] = true;
if (isset($existingByNationalNumber[$nationalNumber])) {
$bovine = $existingByNationalNumber[$nationalNumber];
++$result->updated;
} else {
$bovine = new Bovine();
$bovine->setNationalNumber($nationalNumber);
$this->em->persist($bovine);
++$result->created;
}
$this->applyEdnotifData($bovine, $animal);
$bovine->setExitedAt(null);
}
$now = new DateTimeImmutable();
foreach ($existingByNationalNumber as $nationalNumber => $bovine) {
if (isset($seen[$nationalNumber])) {
continue;
}
if (null !== $bovine->getExitedAt()) {
continue;
}
$bovine->setExitedAt($now);
++$result->exited;
}
$this->em->flush();
return $result;
}
private function applyEdnotifData(Bovine $bovine, AnimalSummaryDto $animal): void
{
$identification = $animal->identification;
if (null !== $identification) {
$bovine->setSex($identification->sex);
$bovine->setBovineType($this->resolveBovineType($identification->breedType));
$bovine->setWorkNumber($identification->workNumber);
$bovine->setBirthDate($identification->birthDate?->date);
$bovine->setMotherNationalNumber($identification->motherCarrier?->bovin?->nationalNumber);
$bovine->setMotherBovineType($this->resolveBovineType($identification->motherCarrier?->breedType));
$bovine->setFatherNationalNumber($identification->fatherIpg?->bovin?->nationalNumber);
$bovine->setFatherBovineType($this->resolveBovineType($identification->fatherIpg?->breedType));
}
$latestEntry = null;
$latestExit = null;
foreach ($animal->presencePeriods as $period) {
if (null !== $period->entry?->date && (null === $latestEntry || $period->entry->date > $latestEntry)) {
$latestEntry = $period->entry->date;
}
if (null !== $period->exit?->date && (null === $latestExit || $period->exit->date > $latestExit)) {
$latestExit = $period->exit->date;
}
}
$bovine->setArrivalDate($latestEntry);
$bovine->setExitDate($latestExit);
$bovine->refreshAgeMonths();
}
/**
* Trouve un BovineType existant par code, sinon en crée un placeholder
* que l'admin pourra renommer dans /admin/bovin/bovin-list.
*/
private function resolveBovineType(?string $code): ?BovineType
{
if (null === $code || '' === $code) {
return null;
}
if (isset($this->bovineTypeCache[$code])) {
return $this->bovineTypeCache[$code];
}
$bovineType = new BovineType();
$bovineType->setCode($code);
$bovineType->setLabel(sprintf('À renommer (%s)', $code));
$this->em->persist($bovineType);
$this->bovineTypeCache[$code] = $bovineType;
return $bovineType;
}
}

View File

@@ -7,26 +7,15 @@ namespace App\State\Bovin;
use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface; use ApiPlatform\State\ProcessorInterface;
use App\ApiResource\BovineSyncInventoryResult; use App\ApiResource\BovineSyncInventoryResult;
use App\Entity\Bovine; use App\Service\BovineInventorySyncer;
use App\Entity\BovineType;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Malio\EdnotifBundle\Bovin\Api\BovinApiInterface;
use Malio\EdnotifBundle\Bovin\Dto\AnimalSummaryDto;
/** /**
* @implements ProcessorInterface<mixed, BovineSyncInventoryResult> * @implements ProcessorInterface<mixed, BovineSyncInventoryResult>
*/ */
final class BovineSyncInventoryProcessor implements ProcessorInterface final readonly class BovineSyncInventoryProcessor implements ProcessorInterface
{ {
/**
* @var array<string, BovineType>
*/
private array $bovineTypeCache = [];
public function __construct( public function __construct(
private BovinApiInterface $bovinApi, private BovineInventorySyncer $syncer,
private EntityManagerInterface $em,
) {} ) {}
public function process( public function process(
@@ -35,113 +24,6 @@ final class BovineSyncInventoryProcessor implements ProcessorInterface
array $uriVariables = [], array $uriVariables = [],
array $context = [], array $context = [],
): BovineSyncInventoryResult { ): BovineSyncInventoryResult {
$inventory = $this->bovinApi->getInventory(new DateTimeImmutable('today')); return $this->syncer->sync();
$result = new BovineSyncInventoryResult();
$result->total = count($inventory->animals);
$this->bovineTypeCache = [];
foreach ($this->em->getRepository(BovineType::class)->findAll() as $bovineType) {
if (null !== $bovineType->getCode()) {
$this->bovineTypeCache[$bovineType->getCode()] = $bovineType;
}
}
$existingByNationalNumber = [];
foreach ($this->em->getRepository(Bovine::class)->findAll() as $bovine) {
$existingByNationalNumber[$bovine->getNationalNumber()] = $bovine;
}
$seen = [];
foreach ($inventory->animals as $animal) {
$nationalNumber = $animal->identification?->bovin?->nationalNumber;
if (null === $nationalNumber || '' === $nationalNumber) {
continue;
}
$seen[$nationalNumber] = true;
if (isset($existingByNationalNumber[$nationalNumber])) {
$bovine = $existingByNationalNumber[$nationalNumber];
++$result->updated;
} else {
$bovine = new Bovine();
$bovine->setNationalNumber($nationalNumber);
$this->em->persist($bovine);
++$result->created;
}
$this->applyEdnotifData($bovine, $animal);
$bovine->setExitedAt(null);
}
$now = new DateTimeImmutable();
foreach ($existingByNationalNumber as $nationalNumber => $bovine) {
if (isset($seen[$nationalNumber])) {
continue;
}
if (null !== $bovine->getExitedAt()) {
continue;
}
$bovine->setExitedAt($now);
++$result->exited;
}
$this->em->flush();
return $result;
}
private function applyEdnotifData(Bovine $bovine, AnimalSummaryDto $animal): void
{
$identification = $animal->identification;
if (null !== $identification) {
$bovine->setSex($identification->sex);
$bovine->setBovineType($this->resolveBovineType($identification->breedType));
$bovine->setWorkNumber($identification->workNumber);
$bovine->setBirthDate($identification->birthDate?->date);
$bovine->setMotherNationalNumber($identification->motherCarrier?->bovin?->nationalNumber);
$bovine->setMotherBovineType($this->resolveBovineType($identification->motherCarrier?->breedType));
$bovine->setFatherNationalNumber($identification->fatherIpg?->bovin?->nationalNumber);
$bovine->setFatherBovineType($this->resolveBovineType($identification->fatherIpg?->breedType));
}
$latestEntry = null;
$latestExit = null;
foreach ($animal->presencePeriods as $period) {
if (null !== $period->entry?->date && (null === $latestEntry || $period->entry->date > $latestEntry)) {
$latestEntry = $period->entry->date;
}
if (null !== $period->exit?->date && (null === $latestExit || $period->exit->date > $latestExit)) {
$latestExit = $period->exit->date;
}
}
$bovine->setArrivalDate($latestEntry);
$bovine->setExitDate($latestExit);
$bovine->refreshAgeMonths();
}
/**
* Trouve un BovineType existant par code, sinon en crée un placeholder
* que l'admin pourra renommer dans /admin/bovin/bovin-list.
*/
private function resolveBovineType(?string $code): ?BovineType
{
if (null === $code || '' === $code) {
return null;
}
if (isset($this->bovineTypeCache[$code])) {
return $this->bovineTypeCache[$code];
}
$bovineType = new BovineType();
$bovineType->setCode($code);
$bovineType->setLabel(sprintf('À renommer (%s)', $code));
$this->em->persist($bovineType);
$this->bovineTypeCache[$code] = $bovineType;
return $bovineType;
} }
} }