docs : plan d'implémentation parsing EarTagSeriesDto

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-22 09:02:13 +02:00
parent c0acc515fc
commit 08dbd9a72b

View File

@@ -0,0 +1,297 @@
# Parsing de `EarTagSeriesDto` — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Transformer `EarTagSeriesDto` d'un wrapper `rawNode` vers un DTO plat à 3 champs (`countryCode`, `startNumber`, `quantity`) avec un helper calculé `endNumber()`, et adapter `InventoryMapper` pour peupler ces champs.
**Architecture:** Single commit atomique — on casse et on rétablit la compat des tests dans la même opération. Un nouveau fichier `EarTagSeriesDtoTest` pin le comportement de `endNumber()` ; les deux tests existants de `InventoryMapperTest` sont adaptés pour consommer les nouveaux champs au lieu de `rawNode`. Aucun autre fichier touché.
**Tech Stack:** PHP 8.4, PHPUnit 12, pas de nouvelle dépendance.
Spec associée : `docs/superpowers/specs/2026-04-22-eartag-series-parsing-design.md`.
---
## File Structure
### À créer
```
tests/Unit/Bovin/Dto/EarTagSeriesDtoTest.php 3 tests sur endNumber()
```
### À modifier
```
src/Bovin/Dto/EarTagSeriesDto.php réécriture complète (3 fields + helper)
src/Bovin/Mapper/InventoryMapper.php 1 bloc modifié (instanciation du DTO)
tests/Unit/Bovin/Mapper/InventoryMapperTest.php 2 méthodes adaptées + helpers ajustés
```
---
## Task 1 — DTO typé + mapper adapté + tests
**Exécution en une seule tâche** parce que DTO et mapper sont liés — les modifier séparément laisserait la suite rouge entre commits. On fait un seul commit atomique.
**Files:**
- Create: `tests/Unit/Bovin/Dto/EarTagSeriesDtoTest.php`
- Modify: `src/Bovin/Dto/EarTagSeriesDto.php` (réécriture)
- Modify: `src/Bovin/Mapper/InventoryMapper.php` (bloc d'instanciation `SerieBoucles`)
- Modify: `tests/Unit/Bovin/Mapper/InventoryMapperTest.php` (2 tests + 1 helper)
### Steps
- [ ] **Step 1: Écrire les 3 nouveaux tests sur le DTO (RED phase)**
Contenu complet de `tests/Unit/Bovin/Dto/EarTagSeriesDtoTest.php` :
```php
<?php
declare(strict_types=1);
namespace Malio\EdnotifBundle\Tests\Unit\Bovin\Dto;
use Malio\EdnotifBundle\Bovin\Dto\EarTagSeriesDto;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(EarTagSeriesDto::class)]
final class EarTagSeriesDtoTest extends TestCase
{
public function testEndNumberComputesStartPlusQuantityMinusOne(): void
{
$series = new EarTagSeriesDto(
countryCode: 'FR',
startNumber: '0012345678',
quantity: 50,
);
self::assertSame('0012345727', $series->endNumber());
}
public function testEndNumberPreservesLeadingZeroPadding(): void
{
$series = new EarTagSeriesDto(
countryCode: 'FR',
startNumber: '0000000001',
quantity: 5,
);
self::assertSame('0000000005', $series->endNumber());
}
public function testEndNumberWithQuantityOneEqualsStartNumber(): void
{
$series = new EarTagSeriesDto(
countryCode: 'FR',
startNumber: '0012345678',
quantity: 1,
);
self::assertSame('0012345678', $series->endNumber());
}
}
```
- [ ] **Step 2: Lancer ce test seul (il doit échouer)**
Run :
```
make test FILES=tests/Unit/Bovin/Dto/EarTagSeriesDtoTest.php
```
Expected : les 3 tests échouent parce que le constructeur actuel de `EarTagSeriesDto` attend `rawNode: object` et pas les 3 nouveaux champs. Message d'erreur typique :
```
ArgumentCountError: Too few arguments to function ... 0 passed and exactly 1 expected
```
ou
```
TypeError: Malio\EdnotifBundle\Bovin\Dto\EarTagSeriesDto::__construct(): Argument #1 ($rawNode) must be of type object, string given
```
L'important : on voit bien une erreur liée à la signature du constructeur, pas un problème de syntax/autoload. Si erreur différente, investiguer avant de continuer.
- [ ] **Step 3: Réécrire `EarTagSeriesDto`**
Remplacer intégralement le contenu de `src/Bovin/Dto/EarTagSeriesDto.php` par :
```php
<?php
declare(strict_types=1);
namespace Malio\EdnotifBundle\Bovin\Dto;
/**
* Série de boucles (ear tags) en stock chez l'exploitation, retournée dans
* la réponse de `IpBGetInventaire` quand `includeEarTagStock: true`.
*
* Correspond au type XSD `typeSerieBoucles` (resources/ednotif-ws/IpBNotif_v1.xsd) :
* une plage contigüe de `quantity` boucles à partir du numéro `startNumber`
* dans le pays `countryCode`.
*
* Les numéros sont stockés en string pour préserver le zero-padding du XSD
* (10 chiffres, pattern `0[1-9][0-9]{8}|[1-9][0-9]{9}`).
*/
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,
);
}
}
```
- [ ] **Step 4: Vérifier les tests DTO (GREEN phase partielle)**
Run :
```
make test FILES=tests/Unit/Bovin/Dto/EarTagSeriesDtoTest.php
```
Expected : 3 tests passent, 3 assertions.
- [ ] **Step 5: Lancer la suite complète (la suite DOIT être rouge sur InventoryMapperTest)**
Run :
```
make test
```
Expected : au moins 2 tests en échec dans `InventoryMapperTest` (`testMapFullInventory` et `testMapInventoryWithSerieBouclesAsListPreservesAllEntries`) à cause de l'incompatibilité entre le mapper actuel qui fait `new EarTagSeriesDto(rawNode: $serieNode)` et la nouvelle signature. C'est attendu et temporaire — on va le corriger dans les steps suivants.
Si d'autres tests échouent, s'arrêter et investiguer avant de continuer.
- [ ] **Step 6: Adapter `InventoryMapper`**
Dans `src/Bovin/Mapper/InventoryMapper.php`, localiser la boucle qui construit `$earTagSeries`. Elle ressemble à :
```php
$seriesNode = $unzippedMessage->Boucles->SerieBoucles ?? null;
foreach ($this->normalizeToList($seriesNode) as $serieNode) {
if (!is_object($serieNode)) {
continue;
}
$earTagSeries[] = new EarTagSeriesDto(rawNode: $serieNode);
}
```
Remplacer la ligne d'instanciation par :
```php
$earTagSeries[] = new EarTagSeriesDto(
countryCode: $this->toNullableString($serieNode->CodePays ?? null) ?? '',
startNumber: $this->toNullableString($serieNode->DebutSerie ?? null) ?? '',
quantity: $this->toNullableInt($serieNode->Quantite ?? null) ?? 0,
);
```
Reste du mapper inchangé. Les imports actuels couvrent déjà `EarTagSeriesDto` et le trait fournit déjà `toNullableString` / `toNullableInt`.
- [ ] **Step 7: Lire `InventoryMapperTest` pour repérer les fixtures à adapter**
Run :
```
cat tests/Unit/Bovin/Mapper/InventoryMapperTest.php
```
Deux endroits utilisent aujourd'hui le champ `NumeroSerieDebut` (nom fictif, non-XSD — hérité d'une fixture approximative) :
1. La méthode privée `makeUnzippedMessage()` qui construit `$message->Boucles->SerieBoucles->NumeroSerieDebut = 'A0001';` (série unique en objet, pas en liste).
2. Le test `testMapInventoryWithSerieBouclesAsListPreservesAllEntries` qui construit 2 objets avec `NumeroSerieDebut = 'A0001'` et `'B0001'`.
Noter les lignes exactes avant de modifier.
- [ ] **Step 8: Adapter `makeUnzippedMessage()` — série unique**
Dans le helper privé `makeUnzippedMessage()` de `InventoryMapperTest`, remplacer :
```php
$message->Boucles = new stdClass();
$message->Boucles->SerieBoucles = new stdClass();
$message->Boucles->SerieBoucles->NumeroSerieDebut = 'A0001';
```
par :
```php
$message->Boucles = new stdClass();
$message->Boucles->SerieBoucles = new stdClass();
$message->Boucles->SerieBoucles->CodePays = 'FR';
$message->Boucles->SerieBoucles->DebutSerie = '0012345678';
$message->Boucles->SerieBoucles->Quantite = 50;
```
- [ ] **Step 9: Adapter `testMapFullInventory` — assertion sur la série**
Dans `testMapFullInventory`, trouver la ligne qui asserte une valeur sur le `rawNode` (s'il y en a une ; sinon juste `assertCount(1, $inventory->earTagSeries)` suffit). Si le test avait une assertion comme `$inventory->earTagSeries[0]->rawNode->NumeroSerieDebut`, la remplacer par :
```php
self::assertSame('FR', $inventory->earTagSeries[0]->countryCode);
self::assertSame('0012345678', $inventory->earTagSeries[0]->startNumber);
self::assertSame(50, $inventory->earTagSeries[0]->quantity);
```
Si le test se contentait de `assertCount(1, ...)`, **ajouter** les 3 assertions ci-dessus pour renforcer la couverture post-refactor.
- [ ] **Step 10: Adapter `testMapInventoryWithSerieBouclesAsListPreservesAllEntries`**
Localiser les 2 blocs qui construisent `$serie1` et `$serie2`. Les remplacer par :
```php
$serie1 = new stdClass();
$serie1->CodePays = 'FR';
$serie1->DebutSerie = '0012345678';
$serie1->Quantite = 10;
$serie2 = new stdClass();
$serie2->CodePays = 'FR';
$serie2->DebutSerie = '0055500000';
$serie2->Quantite = 25;
```
Puis remplacer les assertions qui lisaient `rawNode->NumeroSerieDebut` par :
```php
self::assertCount(2, $inventory->earTagSeries);
self::assertSame('0012345678', $inventory->earTagSeries[0]->startNumber);
self::assertSame(10, $inventory->earTagSeries[0]->quantity);
self::assertSame('0055500000', $inventory->earTagSeries[1]->startNumber);
self::assertSame(25, $inventory->earTagSeries[1]->quantity);
```
(Le `assertCount(2, …)` doit être conservé si présent ; sinon l'ajouter au-dessus.)
- [ ] **Step 11: Lancer la suite complète (GREEN phase finale)**
Run :
```
make test
```
Expected : tous les tests verts. Compte attendu : **56 tests** (53 précédents + 3 nouveaux sur le DTO). Les 2 tests de `InventoryMapperTest` sont modifiés, pas ajoutés.
Si un test est rouge, investiguer. Causes possibles :
- `endNumber()` calcule mal le padding → vérifier le `str_pad` et la conversion `(int) $this->startNumber`.
- Une assertion de la fixture a été mal recopiée → vérifier les noms de champs XSD.
- Un import manquant dans un fichier modifié → vérifier les `use` statements.
- [ ] **Step 12: Commit**
```
git add src/Bovin/Dto/EarTagSeriesDto.php \
src/Bovin/Mapper/InventoryMapper.php \
tests/Unit/Bovin/Dto/EarTagSeriesDtoTest.php \
tests/Unit/Bovin/Mapper/InventoryMapperTest.php
git commit -m "feat : parser SerieBoucles dans EarTagSeriesDto typé"
```
---
## Checklist finale
- [ ] `make test` vert, 56 tests au total
- [ ] Un seul commit atomique
- [ ] `EarTagSeriesDto` n'a plus de propriété `$rawNode`
- [ ] `InventoryMapper` instancie le DTO avec les 3 champs XSD (`CodePays`, `DebutSerie`, `Quantite`)
- [ ] Aucun autre fichier n'a été touché (pas de changement sur `InventoryDto`, `BovinApi`, `BovinApiInterface`, `config/services.php`)