Files
Starseed/tests/Module/Logistique/Api/WeighingTicketSerializationContractTest.php
T
matthieu 36e947fd8e
Auto Tag Develop / tag (push) Successful in 8s
test(logistique) : tests PHPUnit RG-5.01→5.10 + capture contrat JSON (ERP-187) (#137)
## ERP-187 (1.7) — Tests PHPUnit RG-5.01→5.10 + capture contrat JSON

Couvre les règles de gestion du M5 (tickets de pesée) par des tests PHPUnit et capture la **réponse JSON réelle** (DoD § 4.0.bis) collée dans `spec-back.md` avant les écrans front.

### Tests unitaires (Processor / Normalizer / Callback — sans BDD ni HTTP)
- **NetWeightTest** (RG-5.05) : net = plein − vide, `null` si une pesée manque, recalcul au PATCH.
- **CounterpartyValidationTest** (RG-5.03) : présence du champ requis par branche (propertyPath `client`/`supplier`/`otherLabel`) + exclusivité (null-ification hors-branche).
- **ImmatriculationNormalizationTest** (RG-5.01/5.10) : masque `XX-000-XX`, « Tout format », mapping 422 sur `immatriculation`.

### Tests fonctionnels (API réelle)
- **WeighingTicketNumberingTest** (RG-5.02/5.09) : format `{siteCode}-TP-{NNNN}`, séquence par site, isolation inter-sites, immuabilité numéro/site au PATCH.
- **WeighingTicketSerializationContractTest** (DoD § 4.0.bis) : 4 pièges verts (client embarqué, `plateFreeFormat` présent, `number` formaté, `netWeight` = full − empty) + dump JSON via `WEIGHING_TICKET_DOD_DUMP`.
- **WeighingTicketRBACMatrixTest** (§ 5.2) : admin/bureau/usine OK, compta/commerciale 403, anonyme 401.

> DSD / stub pont bascule / endpoint pesée déjà couverts (ERP-184/185).

### DoD
- `spec-back.md § 4.0.bis` : **JSON réel** (liste + détail) collé, 4 pièges marqués  — feu vert front.

### Vérifications
- `make test` complet **vert** : 848 tests, 6302 assertions (0 échec ; deprecations/notices PHPUnit seuls).
- `make php-cs-fixer-allow-risky` : 0 correction.

Empilée sur ERP-186 (stack M5).

---------

Co-authored-by: Matthieu <contact@malio.fr>
Reviewed-on: #137
2026-06-18 13:33:39 +00:00

141 lines
6.4 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\Module\Logistique\Api;
/**
* Contrat de serialisation du ticket de pesee (M5, spec-back § 4.0 / § 4.0.bis).
* Jumeau du test de contrat M4 CarrierSerializationContractTest (module Transport,
* reference en prose pour ne pas materialiser d'import inter-module).
*
* Capture le JSON REEL (liste + detail) via un ticket cree par l'API (numerotation
* serveur reelle) et reverifie les 4 pieges du RETEX M1 transposes au M5 :
* #1 : `client` (et `supplier`) sortent en OBJET embarque, pas en IRI nu
* (read-groups client:read / supplier:read).
* #2 : booleen `plateFreeFormat` present dans le JSON (getter + SerializedName).
* #3 : `number` present, formate {siteCode}-TP-{NNNN}.
* #4 : `netWeight` coherent = full - empty (plein - vide, RG-5.05).
*
* REGLE D'OR : on asserte sur le CORPS JSON reel, jamais sur les annotations.
* DoD (§ 4.0.bis) : avec WEIGHING_TICKET_DOD_DUMP positionnee, ecrit les corps
* liste + detail sous /tmp pour les coller dans la spec avant les ecrans front.
*
* @internal
*/
final class WeighingTicketSerializationContractTest extends AbstractWeighingTicketApiTestCase
{
public function testListAndDetailSerializationContract(): void
{
$site = $this->siteByCode('86');
$http = $this->authManageOnSite($site);
$clientEntity = $this->seedTestClient('Negoce');
$created = $this->postTicket($http, $this->validClientTicketPayload($clientEntity));
self::assertResponseStatusCodeSame(201);
$createdBody = $created->toArray();
$id = (int) $createdBody['id'];
$number = (string) $createdBody['number'];
$detail = $http->request('GET', '/api/weighing_tickets/'.$id, ['headers' => ['Accept' => self::LD]])->toArray();
$list = $http->request('GET', '/api/weighing_tickets?search='.$number, ['headers' => ['Accept' => self::LD]])->toArray();
// Enveloppe Hydra AP4 (member/totalItems sans prefixe hydra:).
self::assertArrayHasKey('member', $list);
self::assertArrayNotHasKey('hydra:member', $list);
$row = $this->memberById($list, $id);
self::assertNotNull($row, 'Le ticket cree doit apparaitre dans la liste filtree.');
// === Piege #1 : relations embarquees en OBJET (pas IRI nu) ===
self::assertIsArray($row['client'], 'client doit etre un objet embarque (client:read), pas un IRI nu.');
self::assertArrayHasKey('companyName', $row['client']);
// supplier null sur une contrepartie Client (cle potentiellement omise par
// skip_null_values — tolerant aux deux cas, jamais un IRI nu).
self::assertNull($row['supplier'] ?? null);
// === Piege #2 : booleen plateFreeFormat present ===
self::assertArrayHasKey('plateFreeFormat', $row);
self::assertFalse($row['plateFreeFormat']);
// === Piege #3 : number formate {siteCode}-TP-{NNNN} ===
self::assertArrayHasKey('number', $row);
self::assertMatchesRegularExpression('/^86-TP-\d{4}$/', $row['number']);
// === Piege #4 : netWeight = full - empty (14300 - 7150) ===
self::assertSame(7150, $row['netWeight']);
// displayDate (date du ticket = fullDate ?? emptyDate) expose en liste.
self::assertArrayHasKey('displayDate', $row);
// === DETAIL : site embarque (avec code), immatriculation, les 2 pesees ===
self::assertIsArray($detail['site']);
self::assertSame('86', $detail['site']['code']);
self::assertSame('AB-123-CD', $detail['immatriculation']);
self::assertSame(7150, $detail['emptyWeight']);
self::assertSame(14300, $detail['fullWeight']);
self::assertSame(7150, $detail['netWeight']);
self::assertIsArray($detail['client']);
self::assertArrayHasKey('companyName', $detail['client']);
$this->dumpDodIfRequested($list, $detail);
}
/**
* Piege #1 symetrique (spec § 4.0.bis) : sur une contrepartie FOURNISSEUR,
* `supplier` doit sortir en OBJET embarque (supplier:read) et `client` etre
* null (jamais un IRI nu). Le cas Client est couvert ci-dessus ; ce test
* verrouille l'autre branche pour qu'un drift de read-group cote Supplier ne
* passe pas inapercu.
*/
public function testSupplierCounterpartyEmbedsSupplier(): void
{
$site = $this->siteByCode('86');
$http = $this->authManageOnSite($site);
$supplierEntity = $this->seedTestSupplier('Ferraille');
$created = $this->postTicket($http, $this->validSupplierTicketPayload($supplierEntity));
self::assertResponseStatusCodeSame(201);
$createdBody = $created->toArray();
$id = (int) $createdBody['id'];
$number = (string) $createdBody['number'];
$detail = $http->request('GET', '/api/weighing_tickets/'.$id, ['headers' => ['Accept' => self::LD]])->toArray();
$list = $http->request('GET', '/api/weighing_tickets?search='.$number, ['headers' => ['Accept' => self::LD]])->toArray();
$row = $this->memberById($list, $id);
self::assertNotNull($row, 'Le ticket fournisseur cree doit apparaitre dans la liste filtree.');
// Liste : supplier embarque en objet, client omis/null (skip_null_values).
self::assertIsArray($row['supplier'], 'supplier doit etre un objet embarque (supplier:read), pas un IRI nu.');
self::assertArrayHasKey('companyName', $row['supplier']);
self::assertNull($row['client'] ?? null);
self::assertSame('FOURNISSEUR', $row['counterpartyType']);
// Detail : meme contrat cote item.
self::assertIsArray($detail['supplier']);
self::assertArrayHasKey('companyName', $detail['supplier']);
self::assertNull($detail['client'] ?? null);
}
/**
* DoD (§ 4.0.bis) : ecrit les corps JSON reels sous /tmp si WEIGHING_TICKET_DOD_DUMP
* est positionnee (sinon no-op). A coller dans spec-back.md § 4.0.bis.
*
* @param array<string, mixed> $list
* @param array<string, mixed> $detail
*/
private function dumpDodIfRequested(array $list, array $detail): void
{
if (false === getenv('WEIGHING_TICKET_DOD_DUMP')) {
return;
}
$flags = JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;
file_put_contents('/tmp/weighing-ticket-dod-list.json', json_encode($list, $flags));
file_put_contents('/tmp/weighing-ticket-dod-detail.json', json_encode($detail, $flags));
}
}