feat(transport) : CarrierProcessor (RG-4.01→4.03/4.12→4.14) (ERP-158) #113
Reference in New Issue
Block a user
Delete Branch "feat/erp-158-carrier-processor"
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?
Normalisation + champs conditionnels + archive. Ticket ERP-158.
Ecriture du formulaire principal transporteur (M4, WT4) : Post/Patch via CarrierProcessor + CarrierFieldNormalizer, contraintes conditionnelles sur l'entite Carrier. Cible la branche WT3 (feat/erp-155-carrier-schema-entities), pas develop — stack.
Perimetre
Regles de gestion
Tests
CarrierWriteApiTest, CarrierRBACMatrixTest, CarrierArchiveTest, CarrierFieldNormalizerTest. make test vert (750), php-cs-fixer clean.
Code review ERP-158 (CarrierProcessor + champs conditionnels du formulaire principal) — relue contre
docs/specs/M4-transporteurs/spec-back.md(RG-4.01→4.03 / 4.12→4.14) et les conventions backend.Verdict : PR solide, aucun constat bloquant. C'est la PR d'écriture, et tous les points sensibles sont conformes :
Post/Patchgatedtransport.carriers.manageau niveau opération, puis re-gating fintransport.carriers.archivedans leCarrierProcessor(uniquement quandisArchivedbascule réellement) → Bureau reçoit bien 403 sur l'archivage. Mode strict (archive + autre champ → 422) présent.#[Assert\*], miroirLength(max:255)↔Column(length:255)surname, enums exacts, et les RG conditionnelles en#[Assert\Callback]+->atPath()sur l'entité (meilleur que la spec qui disait « dans le Processor » : propertyPath mappableuseFormErrors).UniqueConstraintViolationException(pas de 500).EXCLUDED_LENGTH_MIRROR(+2) justifié (Choice borné < length).Tests : matrice RBAC complète (dont le piège Bureau-manage-sans-archive→403, non-faux-vert), RG couvertes avec assertions sur le
propertyPath, 409 doublon+restauration, normalizer unitaire solide.Quelques 🟡 mineurs en inline (valeurs conditionnelles résiduelles, hypothèse mono-index du catch 409, 2-3 trous de tests). Rien qui bloque le merge.
@@ -103,3 +135,3 @@#[ORM\Column(length: 255)]#[Groups(['carrier:read'])]#[Assert\NotBlank(message: 'Le nom du transporteur est obligatoire.', normalizer: 'trim')]🟢 Contraintes conformes (mon point « à vérifier sur la PR d'écriture » de #112 est levé) :
NotBlank+Length(min:2, max:255)avec messages FR,max:255miroir exact duORM\Column(length:255), cohérence 3 niveaux (colonne NOT NULL ↔ NotBlank). Les enums (certificationType,containerType) sont enAssert\Choiceavec les valeurs exactes de la spec + messages FR. RAS.@@ -179,0 +243,4 @@* pas de 422 sur la presence residuelle »). Le nom est compare en majuscules* car la normalisation UPPER n'intervient qu'au processor (apres validation).*/#[Assert\Callback]🟢 Les RG conditionnelles (RG-4.01 certif requise hors LIOT, RG-4.02 AUTRE→décharge, RG-4.03 affrété→indexation/contenant/volume) sont en
#[Assert\Callback]avec->atPath()sur le champ exact + messages FR, court-circuit LIOT en tête. À noter : la spec disait « Callback dans le CarrierProcessor » — l'avoir mis sur l'entité est un meilleur choix (chaque 422 porte un propertyPath consommable paruseFormErrors, objectif ERP-101). Divergence volontaire et supérieure à la spec.@@ -0,0 +99,4 @@try {return $this->persistProcessor->process($data, $operation, $uriVariables, $context);} catch (UniqueConstraintViolationException $e) {🟡 Mineur (piège latent). Le 409 repose sur le catch large de
UniqueConstraintViolationException, en attribuant TOUT conflit d'unicité au message « nom déjà pris ». C'est correct aujourd'hui caruq_carrier_name_activeest le seul index unique de la table — mais cet invariant est load-bearing : si un futur index unique est ajouté surcarrier, son conflit serait mal libellé. Reco (optionnel) : commenter explicitement que l'unicité repose sur cet unique index, ou discriminer via$e->getViolatedConstraint()si exposé. L'approche catch (vs pré-check) est par ailleurs le bon choix (pas de race condition).@@ -0,0 +143,4 @@return false;}if (!$this->security->isGranted(self::PERM_ARCHIVE)) {🟢 Séparation RBAC manage/archive correcte. La security d'opération (
manage) ne peut pas distinguer « ce PATCH est-il un archivage ? » : le re-gatingtransport.carriers.archiveest donc fait ici, et seulement quandisArchivedbascule réellement (guardArchivecourt-circuite si l'entité n'est pas managée = POST, et si la valeur ne change pas). Un Bureau (manage sans archive) reçoit bien 403. Mode strict (archive + autre champ → 422) également présent. Conforme RG-4.14 / matrice § 5.2.@@ -0,0 +171,4 @@* processor (sous-ressource, WT7). Les setters ne sont touches que si une valeur* est presente, pour ne jamais ecraser l'existant lors d'un PATCH partiel.*/private function normalize(Carrier $data): void🟡 Durcissement optionnel (non bloquant, conforme spec).
normalize()ne touche quename/liotPlates— aucun nettoyage des champs conditionnels. C'est conforme à la spec § (« le back ne maintient pas de state machine : il stocke ce qu'il reçoit et valide la cohérence »), et la validationCallbackgarantit le sens « affrété ⇒ champs requis ».Mais le sens inverse n'est pas borné : un PATCH
{isChartered:false, indexationRate:'12.5'}(ou décharge sur une certif ≠ AUTRE, ou champs d'affrètement sur un LIOT) persiste une valeur sémantiquement morte. Le front masque ces champs, donc en pratique le risque est faible. À considérer : soit vider activement le champ quand son déclencheur est off, soit une garde de cohérence inverse. À trancher avec l'auteur de la spec — le code actuel est défendable.@@ -0,0 +85,4 @@* RG-4.02 : certificationType=AUTRE sans dischargeDocument -> 422 cible ; une* certification != AUTRE sans decharge passe (201).*/public function testAutreCertificationRequiresDischarge(): void🟡 Trous de couverture (non bloquants) :
dischargeDocument→201. Le seul chemin qui prouve que la Callback laisse passer quand la décharge est fournie n'est pas exercé (l'infrauploaded_documentexiste).fieldChanged/originalData) est non triviale et non testée e2e — un PATCH{certificationType:'OVOCOM'}devrait conservername/liotPlates.Le reste des RG est très bien couvert, avec assertions sur le
propertyPath(pas seulement le code HTTP). À compléter quand les filtres seront consolidés (ERP-162/163).97d7cacd2ctocbabe8f9accbabe8f9acto13d4a08bc9