feat(transport) : sous-ressource contacts transporteur (ERP-160) #116

Merged
matthieu merged 4 commits from feat/erp-160-carrier-contacts into feat/erp-159-carrier-addresses 2026-06-16 13:49:05 +00:00
Owner

POST/PATCH/DELETE carrier_contact + RG-4.08 (≥1 champ, max 2 tel). Ticket ERP-160.

POST/PATCH/DELETE carrier_contact + RG-4.08 (≥1 champ, max 2 tel). Ticket ERP-160.
matthieu added the backM4-Transporteurtype/feat labels 2026-06-16 08:18:35 +00:00
tristan reviewed 2026-06-16 09:57:39 +00:00
tristan left a comment
Owner

Code review ERP-160 (sous-ressource Contacts transporteur) — relue contre spec-back § 4.5 (RG-4.08, RG-4.13).

Verdict : PR propre, aucun constat bloquant. Même squelette éprouvé que #115 (POST /carriers/{id}/contacts + Link/read:false, PATCH/DELETE sur /carrier_contacts/{id}, security manage/view, 404 parent géré). Spécificités M4 bien traitées :

  • RG-4.08 (≥1 champ rempli) en double garde : CHECK BDD chk_carrier_contact_filled + validateAtLeastOneField (422 propre rattachée à firstName plutôt qu'une 500 SQL).
  • Téléphones x1/x2 : design soigné via un tableau virtuel phones (write-only) mappé vers phonePrimary/phoneSecondary, max 2 → 422 sur phones, normalisation RG-4.13 (chiffres seuls), no-op si non fourni (PATCH partiel).
  • Email Assert\Email + Length, messages FR partout.

Tests : RG-4.08 (vide/mono), 3 téléphones→422, mapping+normalisation phones (avec assertion des valeurs), PATCH/DELETE manage, 403 sans manage. Quelques 🟡 mineurs en inline (propertyPath, 404, email format). Rien qui bloque.

Code review ERP-160 (sous-ressource Contacts transporteur) — relue contre spec-back § 4.5 (RG-4.08, RG-4.13). **Verdict : PR propre, aucun constat bloquant.** Même squelette éprouvé que #115 (POST `/carriers/{id}/contacts` + `Link`/`read:false`, PATCH/DELETE sur `/carrier_contacts/{id}`, security `manage`/`view`, 404 parent géré). Spécificités M4 bien traitées : - **RG-4.08** (≥1 champ rempli) en double garde : CHECK BDD `chk_carrier_contact_filled` + `validateAtLeastOneField` (422 propre rattachée à `firstName` plutôt qu'une 500 SQL). - **Téléphones x1/x2** : design soigné via un tableau virtuel `phones` (write-only) mappé vers `phonePrimary`/`phoneSecondary`, max 2 → 422 sur `phones`, normalisation RG-4.13 (chiffres seuls), no-op si non fourni (PATCH partiel). - Email `Assert\Email` + Length, messages FR partout. Tests : RG-4.08 (vide/mono), 3 téléphones→422, mapping+normalisation `phones` (avec assertion des valeurs), PATCH/DELETE manage, 403 sans manage. Quelques 🟡 mineurs en inline (propertyPath, 404, email format). Rien qui bloque.
@@ -66,0 +141,4 @@
*
* @var null|list<string>
*/
#[Groups(['carrier:write:contacts'])]
Owner

🟢 Bon design pour la liste dynamique de téléphones (« x1, +1 possible, max 2 ») : un champ virtuel phones en write-only (groupe carrier:write:contacts, non persisté) que le processor mappe vers phonePrimary/phoneSecondary, les colonnes scalaires restant en lecture seule. Ça évite d'exposer 2 champs distincts en écriture et centralise le « max 2 » + normalisation côté serveur. Asymétrie lecture (2 scalaires) / écriture (1 tableau) assumée et documentée — le front la gère. RAS.

🟢 Bon design pour la liste dynamique de téléphones (« x1, +1 possible, max 2 ») : un champ virtuel `phones` en write-only (groupe `carrier:write:contacts`, non persisté) que le processor mappe vers `phonePrimary`/`phoneSecondary`, les colonnes scalaires restant en lecture seule. Ça évite d'exposer 2 champs distincts en écriture et centralise le « max 2 » + normalisation côté serveur. Asymétrie lecture (2 scalaires) / écriture (1 tableau) assumée et documentée — le front la gère. RAS.
@@ -0,0 +106,4 @@
// read:false sur le POST : sans stade lecture, un parent introuvable n'est
// plus intercepte en amont -> 404 explicite (sinon 500 au persist sur la
// contrainte carrier_id NOT NULL).
if (!$carrier instanceof Carrier) {
Owner

🟡 Trou de test (mineur, identique à #115). Le 404 « Transporteur introuvable » (conséquence du read:false) n'est exercé par aucun test — un POST sur /api/carriers/999999/contacts devrait renvoyer 404. À ajouter pour figer le comportement.

🟡 Trou de test (mineur, identique à #115). Le 404 « Transporteur introuvable » (conséquence du `read:false`) n'est exercé par aucun test — un POST sur `/api/carriers/999999/contacts` devrait renvoyer 404. À ajouter pour figer le comportement.
@@ -0,0 +194,4 @@
* Joue apres normalisation + mapping telephones, donc les chaines vides sont
* deja ramenees a null.
*/
private function validateAtLeastOneField(CarrierContact $contact): void
Owner

🟢 RG-4.08 correcte : validateAtLeastOneField joue après normalisation + mapping téléphones (les chaînes vides sont déjà ramenées à null), 422 rattachée à firstName (mappable inline). Le périmètre exclut phoneSecondary à juste titre — applyPhones remplit toujours phonePrimary avant phoneSecondary (phones[0] puis phones[1]), donc un contact ne peut jamais avoir un secondaire sans primaire. Raisonnement sain, aligné sur le CHECK BDD.

🟢 RG-4.08 correcte : `validateAtLeastOneField` joue **après** normalisation + mapping téléphones (les chaînes vides sont déjà ramenées à null), 422 rattachée à `firstName` (mappable inline). Le périmètre exclut `phoneSecondary` à juste titre — `applyPhones` remplit toujours `phonePrimary` avant `phoneSecondary` (phones[0] puis phones[1]), donc un contact ne peut jamais avoir un secondaire sans primaire. Raisonnement sain, aligné sur le CHECK BDD.
@@ -0,0 +50,4 @@
self::ensureKernelShutdown();
}
public function testEmptyContactReturns422(): void
Owner

🟡 Rigueur d'assertion + 1 trou (mineurs, cohérence avec #113/#115) :

  • les tests 422 (testEmptyContactReturns422, testThirdPhoneReturns422) n'assertent que le code HTTP, pas le propertyPath — or RG-4.08 cible firstName et le max-2 cible phones ; sans l'assertion du path, une régression de mapping passerait au vert (le test phones mapping, lui, assert bien les valeurs — 👍) ;
  • pas de test du format email invalide → 422 (la contrainte Assert\Email est nouvelle sur ce champ).

Le reste du contrat est bien couvert, avec assertions de valeurs sur la normalisation (RG-4.13).

🟡 Rigueur d'assertion + 1 trou (mineurs, cohérence avec #113/#115) : - les tests 422 (`testEmptyContactReturns422`, `testThirdPhoneReturns422`) n'assertent que le **code HTTP**, pas le `propertyPath` — or RG-4.08 cible `firstName` et le max-2 cible `phones` ; sans l'assertion du path, une régression de mapping passerait au vert (le test `phones` mapping, lui, assert bien les valeurs — 👍) ; - pas de test du format email invalide → 422 (la contrainte `Assert\Email` est nouvelle sur ce champ). Le reste du contrat est bien couvert, avec assertions de valeurs sur la normalisation (RG-4.13).
matthieu force-pushed feat/erp-160-carrier-contacts from 8570bd09f0 to c576e4d504 2026-06-16 13:06:28 +00:00 Compare
matthieu added 1 commit 2026-06-16 13:15:02 +00:00
matthieu force-pushed feat/erp-160-carrier-contacts from c576e4d504 to daa8224b8b 2026-06-16 13:15:02 +00:00 Compare
matthieu added 3 commits 2026-06-16 13:47:30 +00:00
POST /api/carriers/{id}/prices + PATCH/DELETE /api/carrier_prices/{id}
(security transport.carriers.manage) via CarrierPriceProcessor.

RG-4.09->4.11 : coherence de branche CLIENT/FOURNISSEUR (champs requis +
appartenance de l'adresse de livraison au client / de l'adresse d'appro au
fournisseur, sinon 422), nettoyage de la branche opposee (CHECK BDD). Champs
communs obligatoires via Assert\NotBlank + Assert\Choice.

Les contrats Shared ClientAddressInterface / SupplierAddressInterface exposent
desormais getClient() / getSupplier() (canal cross-module, regle n°1) pour la
verification d'appartenance. Colonnes enum du prix whitelistees dans le miroir
Assert\Length (deja bornees par Choice).
GET /api/carriers/export.xlsx (mêmes filtres que la liste : includeArchived,
search, certificationType) et GET /api/carriers/{id}/prices/export.xlsx (tableau
Prix regroupé Benne / Fond Mouvant). Controllers Symfony custom avec
#[Route(priority: 1)] pour éviter le conflit API Platform {id}, génération
déléguée au service Shared SpreadsheetExporterInterface.
L'export XLSX du repertoire reflete la vue liste : il propage desormais
?archivedOnly comme CarrierProvider (sinon l'export divergerait de l'ecran
quand le toggle « Voir les archives » est actif).
matthieu merged commit c0fa00c9c5 into feat/erp-159-carrier-addresses 2026-06-16 13:49:05 +00:00
matthieu deleted branch feat/erp-160-carrier-contacts 2026-06-16 13:49:05 +00:00
Sign in to join this conversation.