| Numéro du ticket | Titre du ticket | |------------------|-----------------| | | | ## Description de la PR ## Modification du .env ## Check list - [ ] Pas de régression - [ ] TU/TI/TF rédigée - [ ] TU/TI/TF OK - [ ] CHANGELOG modifié Reviewed-on: #5 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
14 KiB
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 devalues()— 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.
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.
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
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/provenanceré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
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
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
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
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
public function createEntree(CreateEntreeRequest $request): CreateEntreeResponseDto;
public function createSortie(CreateSortieRequest $request): CreateSortieResponseDto;
BovinApi::createEntree
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
CreateEntreeResponseMapperetCreateSortieResponseMapperavecservice(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 avecAttenteValidationBDNi=true, assert quependingBdniValidation === trueet que les 2 fields bovin sont null.CreateEntreeResponseMapperTest::testMapValidatedEntry— réponse avecEntreeValideepopulée, assertpendingBdniValidation === false, identification + entryMovement bien mappés.- Idem pour
CreateSortieResponseMapperTest, avec la shape imbriquéeSortieValidee.MouvementBovin.
Compte attendu post-implémentation : 56 + 4 = 60 tests.
Gestion d'erreurs
Resultat=falsecôté EDNOTIF :assertSuccessfulResponse()lèveEdnotifException— même pattern que les reads, pas de nouveau code.- SoapFault : remonté en
RuntimeExceptionwrapping. - 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
CreateResponseMapperabstrait/trait — à reconsidérer quand le 3ᵉCreate*arrivera. - Gestion métier du statut
pendingBdniValidationcô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 :
$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
}