36e947fd8e
Auto Tag Develop / tag (push) Successful in 8s
## 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
141 lines
6.4 KiB
PHP
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));
|
|
}
|
|
}
|