test(transport) : couverture RG-4.01→4.14 + contrat + fixtures (ERP-163)
- CarrierListTest : anti-N+1 liste (fetch-join qualimat), tri name ASC, echappatoire ?pagination=false (regle n°13) - CarrierAuditTest : POST/PATCH/archive -> audit_log entity_type='transport.Carrier' - CarrierAddressApiTest : CP/ville incoherents acceptes (RG-4.06, pas de controle de coherence serveur) - CarrierFixtures : fixtures dev completes et idempotentes (QUALIMAT validite passee, AUTRE+decharge, affrete, LIOT, complet prix CLIENT+FOURNISSEUR, archive) ; env-gated dev uniquement - spec-back § 4.0.bis : JSON reel capture (liste + detail) via CarrierSerializationContractTest
This commit is contained in:
@@ -586,7 +586,7 @@ class Carrier implements TimestampableInterface, BlamableInterface
|
||||
> 2. Sérialisation booléen `isArchived` (bug #3 M1) : clé présente dans le JSON réel.
|
||||
> 3. `qualimatCarrier` embarqué (statut + validité) pour RG-4.04.
|
||||
|
||||
> ✅ **CAPTURÉ (WT3, ERP-155/157)** — JSON réel produit par `CarrierSerializationContractTest` (transporteur complet seedé : lien QUALIMAT, 1 adresse, 1 contact, 2 prix CLIENT + FOURNISSEUR). Les 3 pièges sont vérifiés verts. **Le front peut démarrer sur ce contrat.**
|
||||
> ✅ **CAPTURÉ (ERP-163)** — JSON **RÉEL** produit par `CarrierSerializationContractTest::testDodReferenceJsonShape` (transporteur complet seedé : lien QUALIMAT, 1 adresse, 1 contact, 2 prix CLIENT + FOURNISSEUR), dumpé via la variable d'env `CARRIER_DOD_DUMP=1`. Les 3 pièges sont vérifiés verts. **Le front peut démarrer sur ce contrat.** Les valeurs cosmétiques (noms, SIRET) sont nettoyées du bruit de seed ; **toutes les clés ci-dessous sont présentes telles quelles dans la réponse réelle**.
|
||||
>
|
||||
> Contraintes d'architecture validées au passage :
|
||||
> - Relations cross-module des prix (`client`/`supplier`/adresses) câblées **sans import inter-module** (règle n°1) via des contrats `Shared/Domain/Contract/*Interface` + `resolve_target_entities`. L'embed JSON passe par les read-groups des entités concrètes (`client:read`, `client_address:read`, `supplier:read`, `supplier_address:read`, `site:read`). Un groupe `supplier_address:read` a été **ajouté aux champs scalaires de `SupplierAddress`** (M2) pour que `supplierSupplyAddress` s'embarque comme `clientDeliveryAddress` (M1 avait déjà `client_address:read`).
|
||||
@@ -596,19 +596,31 @@ class Carrier implements TimestampableInterface, BlamableInterface
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"@context": "/api/contexts/Carrier", "@id": "/api/carriers", "@type": "Collection",
|
||||
"@context": "/api/contexts/Carrier",
|
||||
"@id": "/api/carriers",
|
||||
"@type": "Collection",
|
||||
"totalItems": 1,
|
||||
"member": [
|
||||
{
|
||||
"@id": "/api/carriers/12", "@type": "Carrier", "id": 12,
|
||||
"@id": "/api/carriers/26",
|
||||
"@type": "Carrier",
|
||||
"id": 26,
|
||||
"name": "TRANSPORTS GRELILLIER",
|
||||
"qualimatCarrier": { // embarqué (objet), pas IRI — RG-4.04
|
||||
"@id": "/api/qualimat_carriers/8", "@type": "QualimatCarrier", "id": "8",
|
||||
"siret": "…", "name": "…", "address": "…", "postalCode": "86000", "city": "Poitiers",
|
||||
"status": "Valide", "validityDate": "2027-12-31T00:00:00+01:00"
|
||||
"@id": "/api/qualimat_carriers/22",
|
||||
"@type": "QualimatCarrier",
|
||||
"id": "22",
|
||||
"siret": "80012345600017",
|
||||
"name": "TRANSPORTS GRELILLIER",
|
||||
"address": "12 rue des Acacias",
|
||||
"postalCode": "86000",
|
||||
"city": "Poitiers",
|
||||
"status": "Valide",
|
||||
"validityDate": "2027-12-31T00:00:00+01:00"
|
||||
},
|
||||
"certificationType": "QUALIMAT",
|
||||
"createdAt": "…", "updatedAt": "…",
|
||||
"createdAt": "2026-06-15T19:12:39+02:00",
|
||||
"updatedAt": "2026-06-15T19:12:39+02:00",
|
||||
"isChartered": false, // bool présent (getter + SerializedName)
|
||||
"isArchived": false // bool présent (piège #3)
|
||||
}
|
||||
@@ -617,44 +629,169 @@ class Carrier implements TimestampableInterface, BlamableInterface
|
||||
}
|
||||
```
|
||||
|
||||
**`GET /api/carriers/{id}` (DÉTAIL)** — `qualimatCarrier` + `addresses[]` + `contacts[]` + `prices[]` avec relations cross-module embarquées en objet :
|
||||
**`GET /api/carriers/{id}` (DÉTAIL)** — `qualimatCarrier` + `addresses[]` + `contacts[]` + `prices[]` avec relations cross-module embarquées en objet (les `@id` des sous-collections sortent en `/.well-known/genid/…` : ce sont des IRI anonymes API Platform, normal pour des entités non exposées en ressource racine) :
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"@id": "/api/carriers/12", "@type": "Carrier", "id": 12,
|
||||
"@context": "/api/contexts/Carrier",
|
||||
"@id": "/api/carriers/26",
|
||||
"@type": "Carrier",
|
||||
"id": 26,
|
||||
"name": "TRANSPORTS GRELILLIER",
|
||||
"qualimatCarrier": { "@type": "QualimatCarrier", "status": "Valide", "validityDate": "…", "...": "…" },
|
||||
"qualimatCarrier": { // embarqué (statut + validité) — RG-4.04
|
||||
"@id": "/api/qualimat_carriers/22",
|
||||
"@type": "QualimatCarrier",
|
||||
"id": "22",
|
||||
"siret": "80012345600017",
|
||||
"name": "TRANSPORTS GRELILLIER",
|
||||
"address": "12 rue des Acacias",
|
||||
"postalCode": "86000",
|
||||
"city": "Poitiers",
|
||||
"status": "Valide",
|
||||
"validityDate": "2027-12-31T00:00:00+01:00"
|
||||
},
|
||||
"certificationType": "QUALIMAT",
|
||||
"addresses": [
|
||||
{ "@type": "CarrierAddress", "id": 4, "country": "France", "postalCode": "86000", "city": "Poitiers", "street": "…", "createdAt": "…", "updatedAt": "…" }
|
||||
{
|
||||
"@type": "CarrierAddress",
|
||||
"@id": "/api/.well-known/genid/9f597da33f73776f1c25",
|
||||
"id": 12,
|
||||
"country": "France",
|
||||
"postalCode": "86000",
|
||||
"city": "Poitiers",
|
||||
"street": "12 rue des Acacias",
|
||||
"createdAt": "2026-06-15T19:12:39+02:00",
|
||||
"updatedAt": "2026-06-15T19:12:39+02:00"
|
||||
}
|
||||
],
|
||||
"contacts": [
|
||||
{ "@type": "CarrierContact", "id": 5, "firstName": "Marie", "lastName": "Martin", "phonePrimary": "0612345678", "email": "…", "createdAt": "…", "updatedAt": "…" }
|
||||
{
|
||||
"@type": "CarrierContact",
|
||||
"@id": "/api/.well-known/genid/6c6335ead4557062774f",
|
||||
"id": 13,
|
||||
"firstName": "Marie",
|
||||
"lastName": "Martin",
|
||||
"phonePrimary": "0612345678",
|
||||
"email": "marie.martin@grelillier.fr",
|
||||
"createdAt": "2026-06-15T19:12:39+02:00",
|
||||
"updatedAt": "2026-06-15T19:12:39+02:00"
|
||||
}
|
||||
],
|
||||
"prices": [
|
||||
{
|
||||
"@type": "CarrierPrice", "id": 7, "direction": "CLIENT",
|
||||
"client": { "@type": "Client", "@id": "/api/clients/4", "id": 4, "companyName": "…", "isArchived": false, "...": "…" },
|
||||
"clientDeliveryAddress": { "@type": "ClientAddress", "@id": "/api/client_addresses/4", "postalCode": "86000", "city": "Poitiers", "street": "…", "...": "…" },
|
||||
"departureSite": { "@type": "Site", "@id": "/api/sites/1", "id": 1, "name": "Chatellerault", "postalCode": "86100", "city": "Châtellerault", "...": "…" },
|
||||
"containerType": "BENNE", "pricingUnit": "TONNE", "price": "42.50", "priceState": "VALIDE",
|
||||
"createdAt": "…", "updatedAt": "…"
|
||||
"@type": "CarrierPrice",
|
||||
"@id": "/api/.well-known/genid/ac0305352bb3751a5b76",
|
||||
"id": 23,
|
||||
"direction": "CLIENT",
|
||||
"client": { // OBJET embarqué (client:read), pas IRI nu — piège #1
|
||||
"@type": "Client",
|
||||
"@id": "/api/clients/117",
|
||||
"id": 117,
|
||||
"companyName": "NÉGOCE MÉTAUX ATLANTIQUE",
|
||||
"triageService": false,
|
||||
"categories": [],
|
||||
"createdAt": "2026-06-15T19:12:39+02:00",
|
||||
"updatedAt": "2026-06-15T19:12:39+02:00",
|
||||
"sites": [],
|
||||
"isArchived": false
|
||||
},
|
||||
"clientDeliveryAddress": { // OBJET embarqué (client_address:read)
|
||||
"@type": "ClientAddress",
|
||||
"@id": "/api/client_addresses/32",
|
||||
"id": 32,
|
||||
"country": "France",
|
||||
"postalCode": "86000",
|
||||
"city": "Poitiers",
|
||||
"street": "1 rue de la Livraison",
|
||||
"position": 0,
|
||||
"sites": [],
|
||||
"contacts": [],
|
||||
"categories": [],
|
||||
"createdAt": "2026-06-15T19:12:39+02:00",
|
||||
"updatedAt": "2026-06-15T19:12:39+02:00",
|
||||
"isProspect": false,
|
||||
"isDelivery": true,
|
||||
"isBilling": false,
|
||||
"isBroker": false,
|
||||
"isDistributor": false
|
||||
},
|
||||
"departureSite": { // OBJET embarqué (site:read)
|
||||
"@type": "Site",
|
||||
"@id": "/api/sites/1",
|
||||
"id": 1,
|
||||
"name": "Chatellerault",
|
||||
"street": "14 All. d'Argenson",
|
||||
"postalCode": "86100",
|
||||
"city": "Châtellerault",
|
||||
"color": "#056CF2",
|
||||
"createdAt": "2026-06-15T18:57:56+02:00",
|
||||
"updatedAt": "2026-06-15T18:57:56+02:00",
|
||||
"fullAddress": "14 All. d'Argenson\n86100 Châtellerault"
|
||||
},
|
||||
"containerType": "BENNE",
|
||||
"pricingUnit": "TONNE",
|
||||
"price": "42.50",
|
||||
"priceState": "VALIDE",
|
||||
"createdAt": "2026-06-15T19:12:39+02:00",
|
||||
"updatedAt": "2026-06-15T19:12:39+02:00"
|
||||
},
|
||||
{
|
||||
"@type": "CarrierPrice", "id": 8, "direction": "FOURNISSEUR",
|
||||
"supplier": { "@type": "Supplier", "@id": "/api/suppliers/4", "id": 4, "companyName": "…", "isArchived": false, "...": "…" },
|
||||
"supplierSupplyAddress": { "@type": "SupplierAddress", "@id": "/api/supplier_addresses/38", "id": 38, "addressType": "DEPART", "country": "France", "postalCode": "17000", "city": "La Rochelle", "street": "…" },
|
||||
"deliverySite": { "@type": "Site", "@id": "/api/sites/1", "name": "Chatellerault", "...": "…" },
|
||||
"containerType": "FOND_MOUVANT", "pricingUnit": "FORFAIT", "price": "320.00", "priceState": "EN_COURS",
|
||||
"createdAt": "…", "updatedAt": "…"
|
||||
"@type": "CarrierPrice",
|
||||
"@id": "/api/.well-known/genid/cfee3c4dda8fb899ff3e",
|
||||
"id": 24,
|
||||
"direction": "FOURNISSEUR",
|
||||
"supplier": { // OBJET embarqué (supplier:read), pas IRI nu — piège #1
|
||||
"@type": "Supplier",
|
||||
"@id": "/api/suppliers/102",
|
||||
"id": 102,
|
||||
"companyName": "FERRAILLEUR GRAND OUEST",
|
||||
"categories": [],
|
||||
"createdAt": "2026-06-15T19:12:39+02:00",
|
||||
"updatedAt": "2026-06-15T19:12:39+02:00",
|
||||
"sites": [],
|
||||
"isArchived": false
|
||||
},
|
||||
"supplierSupplyAddress": { // OBJET embarqué (supplier_address:read)
|
||||
"@type": "SupplierAddress",
|
||||
"@id": "/api/supplier_addresses/38",
|
||||
"id": 38,
|
||||
"addressType": "DEPART",
|
||||
"country": "France",
|
||||
"postalCode": "17000",
|
||||
"city": "La Rochelle",
|
||||
"street": "2 quai de l Appro",
|
||||
"createdAt": "2026-06-15T19:12:39+02:00",
|
||||
"updatedAt": "2026-06-15T19:12:39+02:00"
|
||||
},
|
||||
"deliverySite": { // OBJET embarqué (site:read)
|
||||
"@type": "Site",
|
||||
"@id": "/api/sites/1",
|
||||
"id": 1,
|
||||
"name": "Chatellerault",
|
||||
"street": "14 All. d'Argenson",
|
||||
"postalCode": "86100",
|
||||
"city": "Châtellerault",
|
||||
"color": "#056CF2",
|
||||
"createdAt": "2026-06-15T18:57:56+02:00",
|
||||
"updatedAt": "2026-06-15T18:57:56+02:00",
|
||||
"fullAddress": "14 All. d'Argenson\n86100 Châtellerault"
|
||||
},
|
||||
"containerType": "FOND_MOUVANT",
|
||||
"pricingUnit": "FORFAIT",
|
||||
"price": "320.00",
|
||||
"priceState": "EN_COURS",
|
||||
"createdAt": "2026-06-15T19:12:39+02:00",
|
||||
"updatedAt": "2026-06-15T19:12:39+02:00"
|
||||
}
|
||||
],
|
||||
"createdAt": "…", "updatedAt": "…",
|
||||
"isChartered": false, "isArchived": false
|
||||
"createdAt": "2026-06-15T19:12:39+02:00",
|
||||
"updatedAt": "2026-06-15T19:12:39+02:00",
|
||||
"isChartered": false, // bool présent (getter + SerializedName)
|
||||
"isArchived": false // bool présent (piège #3)
|
||||
}
|
||||
```
|
||||
|
||||
> Note WT3 : opérations exposées = `GetCollection` + `Get` (lecture). `POST`/`PATCH` (+ `CarrierProcessor`, normalisation, RG-4.01→4.14, 409 doublon, gating archive) et les sous-ressources d'écriture (adresses/contacts/prix) arrivent aux worktrees suivants (WT4+).
|
||||
> Note (ERP-163) : opérations exposées = `GetCollection` + `Get` (lecture) **et** `POST`/`PATCH` (`CarrierProcessor` : normalisation RG-4.13, RG-4.01→4.14, 409 doublon, gating archive mode strict) **et** les sous-ressources d'écriture adresses/contacts/prix (`Carrier*Processor`). La couverture RG-4.01→4.14 + RBAC + audit + anti-N+1 est portée par la matrice de tests `tests/Module/Transport/Api/` (ERP-163).
|
||||
|
||||
### 4.1 `GET /api/carriers` — Liste
|
||||
|
||||
@@ -909,7 +1046,7 @@ Synchronisation : `php bin/console app:sync-permissions`.
|
||||
|
||||
- [x] 3 maillons de sérialisation documentés pour chaque champ liste + détail (§ 4.0)
|
||||
- [x] Décision embed vs GetCollection explicite (embed détail + sous-ressources write — § 3.3 / § 3.4 / § 4.5)
|
||||
- [ ] **Réponses JSON RÉELLES** capturées (§ 4.0.bis) — à produire au ticket tests (`CarrierSerializationContractTest`)
|
||||
- [x] **Réponses JSON RÉELLES** capturées (§ 4.0.bis) — produites par `CarrierSerializationContractTest` (ERP-163, dump `CARRIER_DOD_DUMP=1`)
|
||||
- [x] Matrice RBAC rôle × permission + mode strict archive (§ 5.2 / RG-4.14)
|
||||
- [x] Pagination (n°13), COMMENT ON COLUMN (n°12), Timestampable/Blamable, Audit + i18n, routes à plat : rappelés
|
||||
- [x] Réutilisations identifiées (référentiel QUALIMAT, Client/Supplier/Site partagés, `usePaginatedList`, blocs, archive, normalisation, `useAddressAutocomplete`)
|
||||
|
||||
Reference in New Issue
Block a user