fix(commercial) : validation tous-blocs des onglets collection client + fix 500 NonUniqueResult (ERP-110) #61

Merged
tristan merged 10 commits from fix/erp-107-validation-blocs-collection into develop 2026-06-04 14:06:04 +00:00
Owner

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

Backread: 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).

## 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).
matthieu added 5 commits 2026-06-04 09:45:43 +00:00
matthieu added the backfrontM1-Clienttype/fix labels 2026-06-04 09:46:09 +00:00
matthieu added 1 commit 2026-06-04 10:01:31 +00:00
fix(commercial) : ne sauter que les blocs contact/RIB totalement vides (bloc partiel sans nom -> 422 inline, ERP-110)
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 1m49s
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 1m16s
f407c3d46a
Author
Owner

Review ERP-110 — 2 points (faible sévérité)

PR solide. Le correctif back (read: false + linkParent durci) est correct et la suite ClientSubResourceApiTest passe (18 tests). J'ai notamment vérifié que read: false ne court-circuite pas l'expression security : testRibWriteWithoutAccountingManageReturns403 (POST sans permission → 403) passe toujours.

Deux points front à considérer :

🟡 1. Erreurs RIB « fantômes » si le PATCH scalaire comptable échoue

new.vue (~886) et [id]/edit.vue (~905) — submitAccounting

Le reset ribErrors.value = [] en tête de fonction a été retiré (le reset vit maintenant dans submitRows, donc à l'étape 2). Si l'étape 1 (PATCH des scalaires comptables) échoue, on return avant submitRows → les erreurs RIB d'une soumission précédente restent affichées.

Scénario : IBAN invalide → 422 inline sous le RIB. L'utilisateur corrige l'IBAN mais saisit mal un champ scalaire (ex. SIREN) → re-submit → l'étape 1 échoue, return → l'ancienne erreur IBAN reste affichée alors qu'elle est corrigée.

Fix : remettre ribErrors.value = [] en tête de submitAccounting (juste après accountingErrors.clearErrors()).

🟡 2. Bloc existant entièrement vidé → ignoré en silence + faux toast de succès

[id]/edit.vuesubmitContacts / submitAccounting (mode edit)

isContactBlank / isRibBlank skippent tout bloc vide, y compris un bloc id !== null (entité existante). Si l'utilisateur vide tous les champs d'un contact/RIB existant et valide → aucun PATCH envoyé, hasError reste falsetoast de succès affiché alors que rien n'a été persisté (perte silencieuse d'intention).

Cas limite (vider sans passer par la suppression), mais le faux « succès » est trompeur.

Option : ne skipper que les blocs vides dont id === null (amorces neuves), pas les entités existantes.

Note (pas un bug)

Le 404 « client inexistant » est désormais porté par *Processor::linkParent (au lieu du read stage). Aucun test ne couvre POST /api/clients/{idInexistant}/contacts → 404 — ajout de couverture suggéré.

## Review ERP-110 — 2 points (faible sévérité) PR solide. Le correctif back (`read: false` + `linkParent` durci) est correct et la suite `ClientSubResourceApiTest` passe (18 tests). J'ai notamment vérifié que `read: false` **ne court-circuite pas** l'expression `security` : `testRibWriteWithoutAccountingManageReturns403` (POST sans permission → 403) passe toujours. Deux points front à considérer : ### 🟡 1. Erreurs RIB « fantômes » si le PATCH scalaire comptable échoue `new.vue` (~886) et `[id]/edit.vue` (~905) — `submitAccounting` Le reset `ribErrors.value = []` en tête de fonction a été retiré (le reset vit maintenant dans `submitRows`, donc à l'**étape 2**). Si l'**étape 1** (PATCH des scalaires comptables) échoue, on `return` **avant** `submitRows` → les erreurs RIB d'une soumission précédente restent affichées. **Scénario** : IBAN invalide → 422 inline sous le RIB. L'utilisateur corrige l'IBAN mais saisit mal un champ scalaire (ex. SIREN) → re-submit → l'étape 1 échoue, `return` → l'ancienne erreur IBAN reste affichée alors qu'elle est corrigée. **Fix** : remettre `ribErrors.value = []` en tête de `submitAccounting` (juste après `accountingErrors.clearErrors()`). ### 🟡 2. Bloc existant entièrement vidé → ignoré en silence + faux toast de succès `[id]/edit.vue` — `submitContacts` / `submitAccounting` (mode edit) `isContactBlank` / `isRibBlank` skippent tout bloc vide, **y compris un bloc `id !== null`** (entité existante). Si l'utilisateur vide tous les champs d'un contact/RIB existant et valide → aucun PATCH envoyé, `hasError` reste `false` → **toast de succès** affiché alors que rien n'a été persisté (perte silencieuse d'intention). Cas limite (vider sans passer par la suppression), mais le faux « succès » est trompeur. **Option** : ne skipper que les blocs vides dont `id === null` (amorces neuves), pas les entités existantes. ### ⚪ Note (pas un bug) Le 404 « client inexistant » est désormais porté par `*Processor::linkParent` (au lieu du read stage). Aucun test ne couvre `POST /api/clients/{idInexistant}/contacts → 404` — ajout de couverture suggéré.
matthieu added 2 commits 2026-06-04 11:34:03 +00:00
test(commercial) : POST sous-ressource sur client inexistant -> 404 (ERP-110)
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 1m55s
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 1m27s
ad32d8147d
Author
Owner

Corrigé dans b564838 (fix) + ad32d81 (test) :

  • #1ribErrors.value = [] remis en tête de submitAccounting (new.vue + edit.vue) : plus d'erreurs RIB obsolètes si l'étape 1 (PATCH scalaires) échoue.
  • #2shouldSkip passe à row.id === null && isXBlank(row) : un contact/RIB existant vidé part désormais en PATCH → 422 inline (plus de faux toast de succès). Aucun impact en création.
  • #3 — 3 tests ajoutés (testPost{Contact,Address,Rib}OnMissingClientReturns404) couvrant le 404 porté par *Processor::linkParent.

Vérifs : ClientSubResourceApiTest 21/21 · nuxt-test 227/227 · php-cs-fixer 0 fix.

Corrigé dans `b564838` (fix) + `ad32d81` (test) : - **#1** — `ribErrors.value = []` remis en tête de `submitAccounting` (new.vue + edit.vue) : plus d'erreurs RIB obsolètes si l'étape 1 (PATCH scalaires) échoue. - **#2** — `shouldSkip` passe à `row.id === null && isXBlank(row)` : un contact/RIB **existant** vidé part désormais en PATCH → 422 inline (plus de faux toast de succès). Aucun impact en création. - **#3** — 3 tests ajoutés (`testPost{Contact,Address,Rib}OnMissingClientReturns404`) couvrant le 404 porté par `*Processor::linkParent`. Vérifs : `ClientSubResourceApiTest` 21/21 ✅ · `nuxt-test` 227/227 ✅ · php-cs-fixer 0 fix.
tristan added 2 commits 2026-06-04 14:02:36 +00:00
spec-front marquait SIREN / N° compte / Mode TVA / N° TVA / Délai / Type de règlement obligatoires, mais ni le back (colonnes nullable, aucun NotBlank) ni le front (canValidateAccounting ne gardait que bank/RIB) ne l'imposaient : on validait un onglet vide.

- Back : ClientAccountingCompletenessValidator (calqué sur RG-1.04) invoqué par le ClientProcessor quand les 6 champs sont présents dans le payload (validation d'onglet) → 422 par propertyPath. PATCH partiel ciblé non impacté.
- Front : helper hasAllRequiredAccountingFields + canValidateAccounting étendu dans new.vue / edit.vue (cohérent avec les onglets Contact/Adresse).
- Spec-back : RG-1.30 documente la règle et résout l'incohérence spec-front/spec-back.
feat(commercial) : onglet adresse — Select « Type d'adresse » + Sites en multiselect, ligne 1 réagencée (ERP-110)
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 2m0s
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 1m19s
5834d7b225
- Sites Starseed : 3 cases -> multiselect a tags « Sites » (required).
- Usage adresse : 3 cases Prospect/Livraison/Facturation -> Select unique « Type d'adresse » (Prospect / Livraison / Facturation / Adresse + Facturation), obligatoire sans option vide, conditionnant le bouton « Valider ». Pur sucre front : le back recoit toujours isProspect/isDelivery/isBilling (aucune RG modifiee), exclusivite Prospect devenue structurelle.
- Email de facturation conditionnel (Facturation / Adresse + Facturation) deplace en ligne 1.
- Ligne 1 : Type d'adresse | Sites | Contact rattache | Email ; le reste (Categorie, Pays, CP, Ville, Adresse...) en lignes suivantes.
- Email : MalioInputText -> MalioInputEmail (lowercase, ERP-101/RG-1.21) sur facturation ET contact.
- Helpers front testables addressFlagsFromType / addressTypeFromFlags + gating canValidateAddresses (type obligatoire) dans new.vue / edit.vue.
tristan merged commit e139d234a9 into develop 2026-06-04 14:06:04 +00:00
tristan deleted branch fix/erp-107-validation-blocs-collection 2026-06-04 14:06:04 +00:00
Sign in to join this conversation.