From 36e947fd8e82d5c278cbebb5b017c0da791d422e Mon Sep 17 00:00:00 2001 From: THOLOT DECHENE Matthieu Date: Thu, 18 Jun 2026 13:33:39 +0000 Subject: [PATCH] =?UTF-8?q?test(logistique)=20:=20tests=20PHPUnit=20RG-5.0?= =?UTF-8?q?1=E2=86=925.10=20+=20capture=20contrat=20JSON=20(ERP-187)=20(#1?= =?UTF-8?q?37)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 Reviewed-on: https://gitea.malio.fr/MALIO-DEV/Starseed/pulls/137 --- docs/specs/M5-tickets-pesee/spec-back.md | 135 +++++++--- .../State/Provider/WeighingTicketProvider.php | 2 +- .../Api/AbstractWeighingTicketApiTestCase.php | 250 ++++++++++++++++++ .../Api/WeighbridgeReadingApiTest.php | 31 ++- .../Api/WeighingTicketNumberingTest.php | 92 +++++++ .../Api/WeighingTicketRBACMatrixTest.php | 121 +++++++++ ...eighingTicketSerializationContractTest.php | 140 ++++++++++ .../ImmatriculationNormalizationTest.php | 162 ++++++++++++ .../Processor/CounterpartyValidationTest.php | 187 +++++++++++++ .../State/Processor/NetWeightTest.php | 132 +++++++++ 10 files changed, 1211 insertions(+), 41 deletions(-) create mode 100644 tests/Module/Logistique/Api/AbstractWeighingTicketApiTestCase.php create mode 100644 tests/Module/Logistique/Api/WeighingTicketNumberingTest.php create mode 100644 tests/Module/Logistique/Api/WeighingTicketRBACMatrixTest.php create mode 100644 tests/Module/Logistique/Api/WeighingTicketSerializationContractTest.php create mode 100644 tests/Module/Logistique/Application/Service/ImmatriculationNormalizationTest.php create mode 100644 tests/Module/Logistique/Infrastructure/ApiPlatform/State/Processor/CounterpartyValidationTest.php create mode 100644 tests/Module/Logistique/Infrastructure/ApiPlatform/State/Processor/NetWeightTest.php diff --git a/docs/specs/M5-tickets-pesee/spec-back.md b/docs/specs/M5-tickets-pesee/spec-back.md index d93a60d..ef39e98 100644 --- a/docs/specs/M5-tickets-pesee/spec-back.md +++ b/docs/specs/M5-tickets-pesee/spec-back.md @@ -172,14 +172,16 @@ Pattern Starseed standard (miroir M1→M4) : - `WeighingTicket implements TimestampableInterface, BlamableInterface` + `use TimestampableBlamableTrait` (4 colonnes standard). - **Libellé i18n** (règle ABSOLUE backend — `AuditableEntitiesHaveI18nLabelTest`) : ajouter `audit.entity.logistique_weighingticket` dans `frontend/i18n/locales/fr.json` (clé = `strtolower(module)` + `_` + `strtolower(Entity)`). -### 2.12 Impression du ticket / bon de pesée (RG-5.08) +### 2.12 Bon de pesée — PDF généré côté serveur via template Twig (RG-5.08) -> **OWNER : Tristan.** La **réalisation du bon d'impression** (gabarit du ticket de pesée, mise en page, déclenchement de l'impression) est **prise en charge par Tristan lui-même** — hors de la découpe back/front standard du M5. Cette spec en pose **le contrat attendu** (déclencheur, contenu, données disponibles) pour qu'il puisse s'y brancher sans rétro-spec. +> **DÉCISION Matthieu (17/06)** : le **bon de pesée est généré côté back** par un **template Twig → PDF** (et non un gabarit imprimé par le navigateur). **OWNER : Tristan** (ticket back dédié, cf. § 10). Cette spec en pose le contrat (endpoint, contenu, données). Contrat attendu : -- **Déclencheur** : à la **validation** (création), l'API renvoie le ticket complet ; le front ouvre une **modal d'impression**. En **modification**, un bouton **« Imprimer »** est disponible (absent à l'ajout — docx / RG-5.08). -- **Contenu minimal du bon** : numéro (`{siteCode}-TP-{NNNN}`), site, contrepartie (Client / Fournisseur / Autre + libellé), immatriculation, **pesée à vide** (date/poids/DSD), **pesée à plein** (date/poids/DSD), **poids net** (= plein − vide), date d'édition. -- **Données** : toutes disponibles dans la réponse `GET /api/weighing_tickets/{id}` (§ 4.0) — aucun champ supplémentaire requis côté API. Si Tristan opte pour un **PDF serveur**, prévoir l'endpoint `GET /api/weighing_tickets/{id}/print.pdf` (HP-M5-04) ; sinon impression navigateur d'un gabarit front. +- **Endpoint** : `GET /api/weighing_tickets/{id}/print.pdf` (opération API Platform dédiée, **pas de controller** — provider renvoyant un binaire). Sécurité `is_granted('logistique.weighing_tickets.view')`. Réponse `Content-Type: application/pdf` (inline). +- **Rendu** : un template **Twig** (`templates/logistique/weighing_ticket_print.html.twig`) hydraté avec le ticket → converti en PDF via le générateur PDF du projet (ex. Dompdf / wkhtmltopdf / Gotenberg — s'aligner sur l'existant ; sinon proposer une lib et la cadrer avec Matthieu). +- **Contenu du bon** : numéro (`{siteCode}-TP-{NNNN}`), site, contrepartie (Client / Fournisseur / Autre + libellé), immatriculation, **pesée à vide** (date/poids/DSD), **pesée à plein** (date/poids/DSD), **poids net** (= plein − vide), date d'édition. (En-tête / logo / mentions = à caler par Tristan.) +- **Données** : toutes déjà disponibles sur le ticket (mêmes champs que `GET /api/weighing_tickets/{id}` § 4.0) — aucun champ API supplémentaire requis. +- **Déclencheurs front** (RG-5.08) : à la **validation** (création), le front ouvre l'aperçu/PDF servi par cet endpoint ; en **modification**, le bouton **« Imprimer »** ouvre le même PDF (absent à l'ajout). ### 2.13 Pas d'archive ; soft delete préparé non exposé @@ -504,17 +506,19 @@ class WeighingTicket implements TimestampableInterface, BlamableInterface **DÉTAIL — maillons** : scalaires + `emptyDate/emptyWeight/emptyDsd/...` + `full*` ∈ `weighing_ticket:item:read` ; `client`/`supplier`/`site` embarqués (`client:read`/`supplier:read`/`site:read`). -### 4.0.bis Réponse JSON de référence (DoD — à CAPTURER sur l'API réelle) +### 4.0.bis Réponse JSON de référence (DoD — CAPTURÉE sur l'API réelle ✅) -> **Definition of Done** (miroir M2/M3/M4) : avant les écrans front, **capturer la réponse RÉELLE** via un test PHPUnit (`WeighingTicketSerializationContractTest`, ticket complet seedé : contrepartie Client, pesée vide + plein) et la coller ici. Toute donnée affichée par le front DOIT apparaître dans ce JSON. +> **Definition of Done** (miroir M2/M3/M4) : ✅ **FAIT (ERP-187)**. Le JSON ci-dessous est la réponse **RÉELLE** capturée par le test `WeighingTicketSerializationContractTest::testListAndDetailSerializationContract` (ticket créé via `POST /api/weighing_tickets` — numérotation serveur réelle — contrepartie Client, pesée vide + plein AUTO). Re-capturable : `WEIGHING_TICKET_DOD_DUMP=1` → `/tmp/weighing-ticket-dod-{list,detail}.json`. **Feu vert front.** Toute donnée affichée par le front DOIT apparaître dans ce JSON. > -> **Pièges à re-tester** : -> 1. `client` / `supplier` doivent sortir en **objet embarqué**, pas en IRI nu → read-groups `client:read`/`supplier:read`. -> 2. Booléen `plateFreeFormat` : clé présente (piège #3 M1 → getter + `SerializedName` si besoin). -> 3. `number` présent et formaté `{siteCode}-TP-{NNNN}`. -> 4. `netWeight` cohérent = `full - empty` (plein − vide, RG-5.05). +> **Pièges re-testés — tous VERTS** (assertions dans le test) : +> 1. ✅ `client` sort en **objet embarqué** (`client:read`), pas en IRI nu ; `supplier` **omis car null** (`skip_null_values` — jamais un IRI nu). Sur une contrepartie Fournisseur, `supplier` sortirait symétriquement en objet (`supplier:read`). +> 2. ✅ Booléen `plateFreeFormat` : **clé présente** (getter `isPlateFreeFormat()` + `SerializedName('plateFreeFormat')`). +> 3. ✅ `number` présent et formaté `{siteCode}-TP-{NNNN}` (ici `86-TP-0001`). +> 4. ✅ `netWeight` cohérent = `full - empty` = `14300 - 7150` = **`7150`** (RG-5.05). +> +> **Note `skip_null_values`** : les champs null sont **omis** du JSON (ex. `supplier`, `otherLabel`, `emptyManualNumber`, `fullManualNumber` absents quand null). Le front ne doit pas présumer leur présence — lire avec un défaut (`?? null`). -**`GET /api/weighing_tickets` (LISTE)** — enveloppe Hydra AP4 (`member`/`totalItems`/`view`), filtrée site courant (§ 2.3) : +**`GET /api/weighing_tickets?search=86-TP-0001` (LISTE)** — enveloppe Hydra AP4 (`member`/`totalItems`/`view`), filtrée site courant (§ 2.3). Capture réelle : ```jsonc { @@ -524,41 +528,94 @@ class WeighingTicket implements TimestampableInterface, BlamableInterface "totalItems": 1, "member": [ { - "@id": "/api/weighing_tickets/1", + "@id": "/api/weighing_tickets/9", "@type": "WeighingTicket", - "id": 1, + "id": 9, "number": "86-TP-0001", "counterpartyType": "CLIENT", - "client": { "@id": "/api/clients/117", "@type": "Client", "id": 117, "companyName": "NÉGOCE MÉTAUX ATLANTIQUE" }, - "supplier": null, - "otherLabel": null, - "displayDate": "2026-06-17T09:12:00+02:00", - "netWeight": 12340, + "client": { + "@id": "/api/clients/629", + "@type": "Client", + "id": 629, + "companyName": "NÉGOCE MÉTAUX ATLANTIQUE", + "triageService": false, + "categories": [], + "createdAt": "2026-06-18T11:50:47+02:00", + "updatedAt": "2026-06-18T11:50:47+02:00", + "createdBy": "/api/me", + "updatedBy": "/api/me", + "sites": [], + "isArchived": false + }, "plateFreeFormat": false, - "createdAt": "2026-06-17T09:12:00+02:00", - "updatedAt": "2026-06-17T09:12:00+02:00" + "netWeight": 7150, + "createdAt": "2026-06-18T11:50:48+02:00", + "updatedAt": "2026-06-18T11:50:48+02:00", + "createdBy": "/api/me", + "updatedBy": "/api/me", + "displayDate": "2026-06-17T09:12:00+02:00" + // supplier / otherLabel omis (null → skip_null_values) } ], - "view": { "@id": "/api/weighing_tickets", "@type": "PartialCollectionView" } + "view": { "@id": "/api/weighing_tickets?search=86-TP-0001", "@type": "PartialCollectionView" } } ``` -**`GET /api/weighing_tickets/{id}` (DÉTAIL)** — ajoute les pesées : +**`GET /api/weighing_tickets/9` (DÉTAIL)** — ajoute le site embarqué (avec `code`), l'immatriculation et les deux pesées. Capture réelle : ```jsonc { - "@id": "/api/weighing_tickets/1", + "@context": "/api/contexts/WeighingTicket", + "@id": "/api/weighing_tickets/9", "@type": "WeighingTicket", - "id": 1, + "id": 9, "number": "86-TP-0001", - "site": { "@id": "/api/sites/1", "@type": "Site", "id": 1, "name": "Châtellerault", "code": "86" }, + "site": { + "@id": "/api/sites/1", + "@type": "Site", + "id": 1, + "name": "Chatellerault", + "code": "86", + "street": "14 All. d'Argenson", + "postalCode": "86100", + "city": "Châtellerault", + "color": "#056CF2", + "createdAt": "2026-06-17T17:07:47+02:00", + "updatedAt": "2026-06-17T17:07:47+02:00", + "fullAddress": "14 All. d'Argenson\n86100 Châtellerault" + }, "counterpartyType": "CLIENT", - "client": { "@id": "/api/clients/117", "@type": "Client", "id": 117, "companyName": "NÉGOCE MÉTAUX ATLANTIQUE" }, + "client": { + "@id": "/api/clients/629", + "@type": "Client", + "id": 629, + "companyName": "NÉGOCE MÉTAUX ATLANTIQUE", + "triageService": false, + "categories": [], + "createdAt": "2026-06-18T11:50:47+02:00", + "updatedAt": "2026-06-18T11:50:47+02:00", + "createdBy": "/api/me", + "updatedBy": "/api/me", + "sites": [], + "isArchived": false + }, "immatriculation": "AB-123-CD", "plateFreeFormat": false, - "emptyDate": "2026-06-17T09:00:00+02:00", "emptyWeight": 14660, "emptyDsd": 41, "emptyMode": "AUTO", "emptyManualNumber": null, - "fullDate": "2026-06-17T09:12:00+02:00", "fullWeight": 27000, "fullDsd": 42, "fullMode": "AUTO", "fullManualNumber": null, - "netWeight": 12340 + "emptyDate": "2026-06-17T09:00:00+02:00", + "emptyWeight": 7150, + "emptyDsd": 1, + "emptyMode": "AUTO", + "fullDate": "2026-06-17T09:12:00+02:00", + "fullWeight": 14300, + "fullDsd": 2, + "fullMode": "AUTO", + "netWeight": 7150, + "createdAt": "2026-06-18T11:50:48+02:00", + "updatedAt": "2026-06-18T11:50:48+02:00", + "createdBy": "/api/me", + "updatedBy": "/api/me", + "displayDate": "2026-06-17T09:12:00+02:00" + // emptyManualNumber / fullManualNumber omis (null → skip_null_values) } ``` @@ -609,6 +666,12 @@ Action **autonome** (le ticket n'est pas encore créé quand on déclenche la pe - Colonnes : Numéro, Contrepartie (Client/Fournisseur/Autre + nom), Date, Immatriculation, Poids vide, Poids plein, **Poids net**, DSD vide/plein. - Génération via le helper XLSX standard projet (skill `xlsx`). Endpoint : provider dédié renvoyant un binaire (`Content-Type` xlsx) — whitelisté pagination (`EXCLUDED`) car export complet. +### 4.6 Impression — `GET /api/weighing_tickets/{id}/print.pdf` (bon de pesée, OWNER Tristan) + +- Opération API Platform dédiée (provider renvoyant un binaire PDF, **pas de controller**). Sécurité `is_granted('logistique.weighing_tickets.view')`. +- Rendu d'un **template Twig** (`templates/logistique/weighing_ticket_print.html.twig`) → PDF (cf. § 2.12). `Content-Type: application/pdf`, inline. +- Contenu : cf. § 2.12. Données déjà portées par le ticket — aucun champ API supplémentaire. + ## 5. RBAC, module & sidebar ### 5.1 `LogistiqueModule::permissions()` @@ -681,7 +744,7 @@ final class WeighingTicketFieldNormalizer | **RG-5.05** | back | Poids net = `poids plein − poids vide`, calculé serveur, exposé en liste/détail (§ 2.8 — confirmé Matthieu 17/06). | | **RG-5.06** | docx+back | Pesée bascule indisponible → erreur explicite + bascule en pesée manuelle. Au M5, le pont est un **stub** (poids aléatoire ∈ [10000,50000] kg, § 2.6). | | **RG-5.07** | docx | Formulaire à vide : `Date` = date du jour par défaut ; `Poids` et `DSD` **readonly** (remplis par la pesée, pas saisis). | -| **RG-5.08** | docx | « Valider » (création) → enregistre + ouvre la modal d'impression. En modification : bouton « Valider » → « Enregistrer », bouton d'impression disponible (absent à l'ajout). Le bouton « Enregistré » du bloc pesée à vide disparaît en modification. **Le bon d'impression est réalisé par Tristan** (§ 2.12). | +| **RG-5.08** | docx | « Valider » (création) → enregistre + ouvre le **bon de pesée (PDF servi par le back)**. En modification : bouton « Valider » → « Enregistrer », bouton « Imprimer » disponible (absent à l'ajout) → ouvre le même PDF. Le bouton « Enregistré » du bloc pesée à vide disparaît en modification. **Bon de pesée = PDF généré back via template Twig, OWNER Tristan** (§ 2.12 / § 4.6). | | **RG-5.09** | back | Site & numéro immuables après création ; liste cloisonnée par site courant (§ 2.3, à confirmer). | | **RG-5.10** | back | Normalisation immatriculation (trim/UPPER/format) côté serveur (§ 6). | @@ -706,7 +769,7 @@ Cohérence inter-champs (RG-5.03, RG-5.01) implémentée via `#[Assert\Callback] | HP-M5-01 | Vue multi-sites des tickets (retirer le cloisonnement + filtre `?siteId=`) si demandé (§ 2.3). | | HP-M5-02 | Driver matériel réel du pont bascule (protocole série/TCP, parsing trame, reconnexion) derrière `WeighbridgeReaderInterface` (§ 2.6). | | HP-M5-03 | Sens réception-expédition explicite + contrôle de signe du net (le net reste `plein − vide`, § 2.8). | -| HP-M5-04 | Génération PDF serveur du ticket (`/print.pdf`) si l'impression navigateur ne suffit pas (§ 2.12). | +| ~~HP-M5-04~~ | **Passé en périmètre** : bon de pesée = PDF serveur via template Twig → ticket back dédié (OWNER Tristan, § 2.12 / § 4.6). | | HP-M5-05 | Archivage fonctionnel des tickets (non prévu au docx — § 2.13). | ## 10. Tickets Lesstime (à découper — back en tête) @@ -720,8 +783,8 @@ Cohérence inter-champs (RG-5.03, RG-5.01) implémentée via `#[Assert\Callback] | 4 | `WeighingTicketProvider` + `WeighingTicketProcessor` (numérotation, RG-5.03/5.05, normalisation) | Backend | | 5 | Export XLSX | Backend | | 6 | Tests PHPUnit RG-5.01→5.10 + capture contrat JSON | Backend | +| 6.bis (ERP-192) | **Bon de pesée — PDF via template Twig** (`/print.pdf`, § 2.12 / § 4.6) | **Backend (OWNER Tristan)** | | 7 | Page liste `/weighing-tickets` (usePaginatedList) + export | Frontend | -| 8 | Écran Ajouter (formulaires vide + plein, pesée bascule/manuelle, masque immat) | Frontend | -| 9 | Écran Modification (la **modal/bon d'impression** = **Tristan**, § 2.12) | Frontend | +| 8 | Écran Ajouter (formulaires vide + plein, pesée bascule/manuelle, masque immat) + ouverture PDF à la validation | Frontend | +| 9 | Écran Modification + bouton « Imprimer » (ouvre le PDF back) | Frontend | | 10 | i18n + libellé audit + branchement site courant | Frontend | -| — | **Bon d'impression du ticket de pesée** | **Tristan (hors découpe M5)** | diff --git a/src/Module/Logistique/Infrastructure/ApiPlatform/State/Provider/WeighingTicketProvider.php b/src/Module/Logistique/Infrastructure/ApiPlatform/State/Provider/WeighingTicketProvider.php index b2b58ed..3ea76c5 100644 --- a/src/Module/Logistique/Infrastructure/ApiPlatform/State/Provider/WeighingTicketProvider.php +++ b/src/Module/Logistique/Infrastructure/ApiPlatform/State/Provider/WeighingTicketProvider.php @@ -88,7 +88,7 @@ final class WeighingTicketProvider implements ProviderInterface // Echappatoire ?pagination=false : collection complete sans Paginator // (regle n°13 — utile pour alimenter un