feat(transport) : sous-ressource contacts transporteur (ERP-160) #116
Reference in New Issue
Block a user
Delete Branch "feat/erp-160-carrier-contacts"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
POST/PATCH/DELETE carrier_contact + RG-4.08 (≥1 champ, max 2 tel). Ticket ERP-160.
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}, securitymanage/view, 404 parent géré). Spécificités M4 bien traitées :chk_carrier_contact_filled+validateAtLeastOneField(422 propre rattachée àfirstNameplutôt qu'une 500 SQL).phones(write-only) mappé versphonePrimary/phoneSecondary, max 2 → 422 surphones, normalisation RG-4.13 (chiffres seuls), no-op si non fourni (PATCH partiel).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'])]🟢 Bon design pour la liste dynamique de téléphones (« x1, +1 possible, max 2 ») : un champ virtuel
phonesen write-only (groupecarrier:write:contacts, non persisté) que le processor mappe versphonePrimary/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) {🟡 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/contactsdevrait 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🟢 RG-4.08 correcte :
validateAtLeastOneFieldjoue 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 exclutphoneSecondaryà juste titre —applyPhonesremplit toujoursphonePrimaryavantphoneSecondary(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🟡 Rigueur d'assertion + 1 trou (mineurs, cohérence avec #113/#115) :
testEmptyContactReturns422,testThirdPhoneReturns422) n'assertent que le code HTTP, pas lepropertyPath— or RG-4.08 ciblefirstNameet le max-2 ciblephones; sans l'assertion du path, une régression de mapping passerait au vert (le testphonesmapping, lui, assert bien les valeurs — 👍) ;Assert\Emailest nouvelle sur ce champ).Le reste du contrat est bien couvert, avec assertions de valeurs sur la normalisation (RG-4.13).
8570bd09f0toc576e4d504c576e4d504todaa8224b8bPOST /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.