Files
Starseed/tests/Module/Logistique/Api/WeighingTicketSerializationContractTest.php
T
tristan 819ac5e608 feat : cycle de vie brouillon/validé du ticket de pesée (ERP-193)
Une pesée (bascule ou manuelle) s'enregistre désormais dès la validation de sa
modale, sans exiger la contrepartie ni l'immatriculation : le ticket naît
« brouillon » (status DRAFT, sans numéro). Le bouton « Valider » finalise quand
les 3 champs du haut (contrepartie + champ associé + immatriculation) ET les 2
pesées sont renseignés : attribution du numéro {siteCode}-TP-{NNNN} et passage
en VALIDATED, puis ouverture du bon de pesée PDF.

Back : counterparty_type/immatriculation/number nullables + colonne status
(migration racine), contraintes strictes déplacées en groupe de validation
finalize, opération PATCH /weighing_tickets/{id}/validate, numéro attribué à la
validation. Front : 4 champs en haut hors blocs, persistance immédiate des
pesées, écrans Ajouter/Modifier refondus, colonne Statut dans la liste, form à
plat pleine largeur. Tests back (lifecycle brouillon/validate) + front à jour.
2026-06-24 15:13:12 +02:00

142 lines
6.6 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');
// Brouillon cree puis valide (numero attribue a la validation, ERP-193).
$createdBody = $this->createValidatedTicket($http, $this->validClientTicketPayload($clientEntity));
$id = (int) $createdBody['id'];
$number = (string) $createdBody['number'];
self::assertSame('VALIDATED', $createdBody['status']);
$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);
// Statut du cycle de vie expose en liste (colonne « En attente / Terminée »).
self::assertSame('VALIDATED', $row['status']);
// === 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');
$createdBody = $this->createValidatedTicket($http, $this->validSupplierTicketPayload($supplierEntity));
$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));
}
}