From e139d234a9e37ad83d86d006df8d12eaf8a6ec67 Mon Sep 17 00:00:00 2001 From: THOLOT DECHENE Matthieu Date: Thu, 4 Jun 2026 14:06:03 +0000 Subject: [PATCH] fix(commercial) : validation tous-blocs des onglets collection client + fix 500 NonUniqueResult (ERP-110) (#61) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Contexte (ERP-110, dérivé de ERP-107) Sur les onglets à blocs dynamiques d'un client (Contacts / Adresses / RIB), le POST d'une sous-ressource sur un client ayant déjà **≥2 enfants** renvoyait une **500 `NonUniqueResultException`**, court-circuitant la validation (aucune 422 par champ). ## Cause racine Au stade « read » du POST, le `Link` `toProperty` faisait résoudre la collection enfant via `getOneOrNullResult()` (`ItemProvider`) : `SELECT o FROM ClientContact o INNER JOIN o.client c WHERE c.id = :clientId`. Dès 2 enfants → `NonUniqueResult` → 500 **avant** la déserialisation/validation. Les 3 sous-ressources partageaient la même config (même bug latent). Cause secondaire front : la boucle de soumission s'arrêtait au 1er bloc en erreur (`return` dans le `catch`). ## Correctif **Back** — `read: false` sur les 3 opérations `Post` (`ClientContact` / `ClientAddress` / `ClientRib`) : le parent est déjà rattaché manuellement par le `*Processor::linkParent`. Les 3 `linkParent` sont durcis (`NotFoundHttpException` si parent absent → **404 préservé**, sinon régression 500 au persist sur `client_id NOT NULL`). **Front** — nouveau helper `useClientFormErrors().submitRows()` qui tente **tous** les blocs et collecte les erreurs 422 par index (`hasError`), branché sur les 6 sites (`new.vue` + `edit.vue` × contacts/adresses/RIB). Feedback **inline seul** : pas de toast récap, pas de toast succès tant qu'un bloc reste en erreur. ## Tests - Back : non-régression POST contact/adresse/RIB sur client déjà peuplé (≥2 enfants) → 201, + 422 `propertyPath=email` (validation atteinte). Rouge avant fix (500), vert après. - Front : `submitRows` (Vitest) — tente tous les blocs, mappe les erreurs par index, n'arrête pas au 1er échec, délègue le fallback non-422, saute les blocs filtrés. ## Vérifications - `make test` : 474/474 OK - `make php-cs-fixer-allow-risky` : 0 fichier à corriger - `make nuxt-test` : 219/219 OK > Golden path manuel navigateur non exécuté (couvert par les tests automatisés). --------- Co-authored-by: tristan Co-authored-by: Matthieu Reviewed-on: https://gitea.malio.fr/MALIO-DEV/Starseed/pulls/61 Co-authored-by: THOLOT DECHENE Matthieu Co-committed-by: THOLOT DECHENE Matthieu --- docs/specs/M1-clients/spec-back.md | 1 + frontend/i18n/locales/fr.json | 7 +- .../components/ClientAddressBlock.vue | 143 ++++++-------- .../components/ClientContactBlock.vue | 1 + .../__tests__/useClientFormErrors.spec.ts | 68 +++++++ .../composables/useClientFormErrors.ts | 39 ++++ .../commercial/pages/clients/[id]/edit.vue | 96 +++++---- .../modules/commercial/pages/clients/new.vue | 151 +++++++------- .../utils/__tests__/clientFormRules.spec.ts | 121 ++++++++++++ .../commercial/utils/clientFormRules.ts | 120 ++++++++++++ .../ClientAccountingCompletenessValidator.php | 77 ++++++++ .../Domain/Entity/ClientAddress.php | 5 + .../Domain/Entity/ClientContact.php | 5 + .../Commercial/Domain/Entity/ClientRib.php | 5 + .../Processor/ClientAddressProcessor.php | 10 +- .../Processor/ClientContactProcessor.php | 10 +- .../State/Processor/ClientProcessor.php | 34 ++++ .../State/Processor/ClientRibProcessor.php | 10 +- .../Api/ClientSubResourceApiTest.php | 184 +++++++++++++++++- .../Commercial/Unit/ClientProcessorTest.php | 82 ++++++++ 20 files changed, 967 insertions(+), 202 deletions(-) create mode 100644 src/Module/Commercial/Application/Validator/ClientAccountingCompletenessValidator.php diff --git a/docs/specs/M1-clients/spec-back.md b/docs/specs/M1-clients/spec-back.md index 0476b45..fb30551 100644 --- a/docs/specs/M1-clients/spec-back.md +++ b/docs/specs/M1-clients/spec-back.md @@ -883,6 +883,7 @@ Cf. § 2.6. Pattern Shared standard. ### Onglet Comptabilité +- **RG-1.30** _(ajoutée — correctif incohérence spec-front/spec-back)_ : à la **validation complète de l'onglet Comptabilité**, les six champs scalaires `siren`, `accountNumber`, `tvaMode`, `nTva`, `paymentDelay`, `paymentType` sont **obligatoires** (alignement sur spec-front § Onglet Comptabilité). Colonnes `nullable` en base (l'onglet est rempli dans un second temps, et l'onglet principal ne les envoie pas) + validateur contextuel `ClientAccountingCompletenessValidator` invoqué par le `ClientProcessor` — même parti que RG-1.04 (Information). Déclenchement : uniquement quand **les six champs sont présents dans le payload** (le front les envoie toujours ensemble via « Valider ») ; un PATCH ciblant un sous-ensemble de champs comptables (édition ponctuelle) n'est pas soumis à la complétude. Chaque champ manquant → 422 sur son `propertyPath` (mapping inline front, ERP-101). `bank` reste hors complétude (conditionnel RG-1.12). - **RG-1.12** : Le champ `bank` est visible et obligatoire **uniquement** si `paymentType.code = 'VIREMENT'`. Validation server-side dans le `ClientProcessor` : si `payment_type.code = VIREMENT` et `bank IS NULL` → 422. - **RG-1.13** : Les champs RIB (`label`, `bic`, `iban`) sont obligatoires si **au moins un bloc RIB est présent ET** `paymentType.code = 'LCR'`. C'est-à-dire : - Si `paymentType.code = LCR` ET `client.ribs.count() = 0` → 422 « Au moins un RIB est obligatoire pour le type LCR ». diff --git a/frontend/i18n/locales/fr.json b/frontend/i18n/locales/fr.json index 5744828..cf7bc63 100644 --- a/frontend/i18n/locales/fr.json +++ b/frontend/i18n/locales/fr.json @@ -168,13 +168,18 @@ "prospect": "Prospect", "delivery": "Adresse de livraison", "billing": "Facturation", + "addressType": "Type d'adresse", + "addressTypeProspect": "Prospect", + "addressTypeDelivery": "Livraison", + "addressTypeBilling": "Facturation", + "addressTypeDeliveryBilling": "Adresse + Facturation", "categories": "Catégorie", "country": "Pays", "postalCode": "Code postal", "city": "Ville", "street": "Adresse", "streetComplement": "Adresse complémentaire", - "sites": "Sites Starseed", + "sites": "Sites", "contacts": "Contact(s) rattaché(s)", "billingEmail": "Email de facturation", "remove": "Supprimer l'adresse", diff --git a/frontend/modules/commercial/components/ClientAddressBlock.vue b/frontend/modules/commercial/components/ClientAddressBlock.vue index ee6e08b..1745a4b 100644 --- a/frontend/modules/commercial/components/ClientAddressBlock.vue +++ b/frontend/modules/commercial/components/ClientAddressBlock.vue @@ -10,34 +10,53 @@ @click="$emit('remove')" /> - - + - - - -