From c0acc515fc52df7da448f8c9c97c0da77c856e11 Mon Sep 17 00:00:00 2001 From: tristan Date: Wed, 22 Apr 2026 08:56:25 +0200 Subject: [PATCH] docs : spec de parsing EarTagSeriesDto Co-Authored-By: Claude Opus 4.7 (1M context) --- ...2026-04-22-eartag-series-parsing-design.md | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-22-eartag-series-parsing-design.md diff --git a/docs/superpowers/specs/2026-04-22-eartag-series-parsing-design.md b/docs/superpowers/specs/2026-04-22-eartag-series-parsing-design.md new file mode 100644 index 0000000..c182964 --- /dev/null +++ b/docs/superpowers/specs/2026-04-22-eartag-series-parsing-design.md @@ -0,0 +1,138 @@ +# Parsing de `SerieBoucles` → `EarTagSeriesDto` typé + +## Contexte + +`EarTagSeriesDto` a été introduit en Phase 1 comme wrapper minimal (`$rawNode: object`) autour du noeud `SerieBoucles` retourné par `IpBGetInventaire` quand le consommateur passe `includeEarTagStock: true`. Le docblock actuel dit que le XSD `typeSerieBoucles` est « riche » et qu'on différera le parsing — c'est **faux**. Le XSD (`resources/ednotif-ws/IpBNotif_v1.xsd:1181-1197`) ne contient que 3 champs : + +```xml + + + + + + + +``` + +Sémantique : « j'ai `Quantite` boucles consécutives à partir du numéro `DebutSerie` dans le pays `CodePays` ». Une série = une plage de boucles physiques non utilisées, en stock chez l'éleveur. + +## But + +Remplacer le wrapper `rawNode` par un DTO typé à 3 champs, plus un helper calculé `endNumber()` qui rend l'usage UI immédiat (afficher « 50 boucles de 0012345678 à 0012345727 »). + +## Scope + +### Inclus + +- `EarTagSeriesDto` plat : `countryCode: string`, `startNumber: string`, `quantity: int`. +- Une méthode `endNumber(): string` qui renvoie `startNumber + quantity - 1` avec zéro-padding à 10 chiffres. +- Adaptation du `InventoryMapper` : instancier le DTO avec les 3 champs lus depuis `$serieNode->CodePays`/`DebutSerie`/`Quantite` au lieu de passer le noeud brut. +- Tests unitaires sur le DTO (helper) et sur le mapper (les 2 tests existants pour `SerieBoucles` — full inventory + list — passent à travers les nouveaux champs). +- Mise à jour du docblock sur `EarTagSeriesDto` (le texte Phase 1 est à refaire). + +### Exclus (YAGNI) + +- `contains(string $tagNumber): bool` et `iterableNumbers(): Generator` — à ajouter le jour où `IpBCreateRebouclage` ou une UI de sélection de boucle en aura vraiment besoin. +- Validation métier (vérifier que `startNumber` matche le pattern XSD `0[1-9][0-9]{8}|[1-9][0-9]{9}`) — EDNOTIF garantit déjà la forme, on n'ajoute pas une seconde ligne de défense. +- Changement du type de `$earTagSeries` dans `InventoryDto` — la liste reste `list`. + +## Changement non-rétro-compatible assumé + +Le remplacement de `$rawNode` par 3 champs casse l'API publique de `EarTagSeriesDto`. Comme cette DTO n'a **aucun consommateur connu à ce jour** (Ferme n'utilise pas `includeEarTagStock: true`), on fait le changement direct, sans transition. Si un consommateur tiers existait, il faudrait un cycle de dépréciation ; ce n'est pas le cas. + +## Architecture + +### DTO + +```php +final readonly class EarTagSeriesDto +{ + public function __construct( + 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, + ); + } +} +``` + +Décisions à documenter dans le code : + +- **`startNumber` en string**, pas int : le XSD impose 10 chiffres avec zéro-padding possible. Convertir en int ferait perdre le format (`'0012345678'` → `12345678`). Laisser en string préserve le format d'affichage. +- **`endNumber()` renvoie aussi une string** zéro-paddée à 10 chiffres, via `str_pad`. Cohérent avec `startNumber`. +- **`quantity` en int** : xsd:unsignedInt se mappe naturellement sur int PHP (les 10 milliards de boucles possibles tiennent dans int64 sans problème). + +### Mapper + +Dans `InventoryMapper::map()`, remplacer : + +```php +$earTagSeries[] = new EarTagSeriesDto(rawNode: $serieNode); +``` + +par : + +```php +$earTagSeries[] = new EarTagSeriesDto( + countryCode: $this->toNullableString($serieNode->CodePays ?? null) ?? '', + startNumber: $this->toNullableString($serieNode->DebutSerie ?? null) ?? '', + quantity: $this->toNullableInt($serieNode->Quantite ?? null) ?? 0, +); +``` + +Valeurs par défaut (`''`, `0`) pour les cas où EDNOTIF renverrait un noeud partiel — plus simple qu'une DTO avec des nullable partout, et `quantity = 0` / `startNumber = ''` sont des valeurs sentinelles immédiatement repérables en debug. + +### Docblock actualisé + +Remplacer le texte Phase 1 sur `EarTagSeriesDto` par une description factuelle de la structure XSD et de la sémantique (plage de boucles en stock). + +## Edge cases + +- **Noeud `SerieBoucles` partiel** (un des 3 champs manque) : le DTO est construit avec les valeurs sentinelles `''` ou `0`. Pas de crash, pas d'exception. Responsabilité du consommateur de filtrer si besoin. +- **`quantity = 0`** : `endNumber()` renvoie `startNumber - 1` (zéro-padded). Techniquement absurde côté métier mais techniquement déterministe. Ne vaut pas la peine de documenter un `throw`. +- **`startNumber` vide** : `(int) '' === 0`, `endNumber()` renvoie `str_pad((string)(-1), ...)` = `'00000000-1'`. Déjà en terrain « shouldn't happen ». Pas de garde défensive ajoutée. +- **`CodePays` vide** : `endNumber()` ne dépend pas du pays, aucun impact. + +## Tests + +### DTO + +Un fichier `tests/Unit/Bovin/Dto/EarTagSeriesDtoTest.php` : + +- `testEndNumberComputesStartPlusQuantityMinusOne` — cas standard, ex : start `0012345678`, quantity 50, end `0012345727`. +- `testEndNumberPreservesLeadingZeroPadding` — cas avec leading zero, ex : start `0000000001`, quantity 5, end `0000000005`. +- `testEndNumberWithQuantityOne` — une seule boucle dans la série, end === start. + +3 tests, ~15 assertions. + +### Mapper + +Les 2 tests existants (`testMapFullInventory` et `testMapInventoryWithSerieBouclesAsListPreservesAllEntries`) sont **modifiés** pour : + +- Peupler `CodePays`, `DebutSerie`, `Quantite` dans les fixtures à la place de `NumeroSerieDebut`. +- Asserter sur `$inventory->earTagSeries[0]->startNumber` et `$inventory->earTagSeries[0]->quantity` au lieu de `rawNode->NumeroSerieDebut`. + +Aucun nouveau test sur le mapper — la structure est déjà couverte. + +## Impact sur les autres fichiers + +- `src/Bovin/Dto/EarTagSeriesDto.php` — réécrit. +- `src/Bovin/Mapper/InventoryMapper.php` — un bloc modifié (instanciation de la DTO). +- `tests/Unit/Bovin/Mapper/InventoryMapperTest.php` — fixtures `SerieBoucles` adaptées (2 tests impactés). +- Nouveau : `tests/Unit/Bovin/Dto/EarTagSeriesDtoTest.php`. + +Aucun autre fichier touché. `InventoryDto`, `BovinApi`, `BovinApiInterface`, `config/services.php` : inchangés. + +## Comptage de tests attendu + +Avant : 53 tests. +Après : 53 + 3 nouveaux sur le DTO = 56 tests (les 2 du mapper sont modifiés, pas ajoutés).