docs : spec de parsing EarTagSeriesDto
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
<xsd:complexType name="typeSerieBoucles">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="CodePays" type="tns:typeCodePaysFr"/>
|
||||
<xsd:element name="DebutSerie"> <!-- string, 10 digits, pattern 0[1-9][0-9]{8}|[1-9][0-9]{9} -->
|
||||
<xsd:element name="Quantite" type="xsd:unsignedInt"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
```
|
||||
|
||||
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<string>` — à 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<EarTagSeriesDto>`.
|
||||
|
||||
## 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).
|
||||
Reference in New Issue
Block a user