fix(commercial) : validation tous-blocs des onglets collection client + fix 500 NonUniqueResult (ERP-110) (#61)
Auto Tag Develop / tag (push) Successful in 8s
Auto Tag Develop / tag (push) Successful in 8s
## 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 <tristan@yuno.malio.fr> Co-authored-by: Matthieu <contact@malio.fr> Reviewed-on: #61 Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr> Co-committed-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
This commit was merged in pull request #61.
This commit is contained in:
@@ -86,6 +86,58 @@ export function hasAtLeastOneValidContact(contacts: ContactDraft[]): boolean {
|
||||
return contacts.some(isContactNamed)
|
||||
}
|
||||
|
||||
/**
|
||||
* Primitive reutilisable : vrai si TOUTES les valeurs fournies sont vides (null /
|
||||
* undefined / espaces uniquement). Sert a detecter un bloc de collection
|
||||
* totalement vide (amorce non remplie). Un bloc qui porte la moindre donnee
|
||||
* n'est PAS « blank » : il doit etre soumis pour declencher sa 422 inline plutot
|
||||
* que d'etre saute silencieusement.
|
||||
*/
|
||||
export function isBlankRow(values: (string | null | undefined)[]): boolean {
|
||||
return values.every(value => !isFilled(value))
|
||||
}
|
||||
|
||||
/** Champs saisissables d'un bloc contact (pour detecter un bloc totalement vide). */
|
||||
export interface ContactFillableDraft extends ContactDraft {
|
||||
jobTitle: string | null
|
||||
phonePrimary: string | null
|
||||
phoneSecondary: string | null
|
||||
email: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Vrai si AUCUN champ saisissable du bloc contact n'est rempli. Distingue un bloc
|
||||
* d'amorce vide (a ignorer au submit) d'un bloc partiellement rempli sans nom
|
||||
* (email / telephone / fonction seul) : ce dernier doit etre soumis pour
|
||||
* declencher la 422 RG-1.05 (« prenom ou nom obligatoire ») affichee inline.
|
||||
*/
|
||||
export function isContactBlank(contact: ContactFillableDraft): boolean {
|
||||
return isBlankRow([
|
||||
contact.firstName,
|
||||
contact.lastName,
|
||||
contact.jobTitle,
|
||||
contact.phonePrimary,
|
||||
contact.phoneSecondary,
|
||||
contact.email,
|
||||
])
|
||||
}
|
||||
|
||||
/** Champs saisissables d'un bloc RIB (pour detecter un bloc totalement vide). */
|
||||
export interface RibFillableDraft {
|
||||
label: string | null
|
||||
bic: string | null
|
||||
iban: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Vrai si AUCUN champ du bloc RIB n'est rempli. Un RIB partiellement rempli (ex.
|
||||
* IBAN seul) n'est PAS « blank » : il doit etre soumis pour declencher les 422
|
||||
* NotBlank (label / bic / iban) inline plutot que d'etre saute silencieusement.
|
||||
*/
|
||||
export function isRibBlank(rib: RibFillableDraft): boolean {
|
||||
return isBlankRow([rib.label, rib.bic, rib.iban])
|
||||
}
|
||||
|
||||
/**
|
||||
* RG-1.06/07/08 : une adresse de prospection est exclusive d'une adresse de
|
||||
* livraison/facturation. Prospect n'est selectionnable que si ni Livraison ni
|
||||
@@ -135,6 +187,45 @@ export function isBillingEmailRequired(flags: AddressFlagsDraft): boolean {
|
||||
return flags.isBilling
|
||||
}
|
||||
|
||||
/**
|
||||
* Type d'adresse expose a l'utilisateur (Select unique remplacant les trois
|
||||
* cases a cocher). Sucre purement front : le back continue de recevoir les
|
||||
* drapeaux isProspect / isDelivery / isBilling (aucune RG modifiee). Les seules
|
||||
* combinaisons proposees respectent l'exclusivite Prospect (RG-1.06/07/08).
|
||||
*/
|
||||
export type AddressType = 'prospect' | 'delivery' | 'billing' | 'delivery_billing'
|
||||
|
||||
/**
|
||||
* Mappe le type d'adresse choisi vers les trois drapeaux back.
|
||||
* « Adresse + Facturation » = livraison ET facturation sur la meme adresse.
|
||||
*/
|
||||
export function addressFlagsFromType(type: AddressType): AddressFlagsDraft {
|
||||
switch (type) {
|
||||
case 'prospect':
|
||||
return { isProspect: true, isDelivery: false, isBilling: false }
|
||||
case 'delivery':
|
||||
return { isProspect: false, isDelivery: true, isBilling: false }
|
||||
case 'billing':
|
||||
return { isProspect: false, isDelivery: false, isBilling: true }
|
||||
case 'delivery_billing':
|
||||
return { isProspect: false, isDelivery: true, isBilling: true }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconstruit le type d'adresse a partir des drapeaux (consultation / edition
|
||||
* d'une adresse persistee, ou amorce vierge). Retourne null si aucun drapeau
|
||||
* n'est positionne — le Select reste alors a saisir (et bloque la validation).
|
||||
*/
|
||||
export function addressTypeFromFlags(flags: AddressFlagsDraft): AddressType | null {
|
||||
if (flags.isProspect) return 'prospect'
|
||||
if (flags.isDelivery && flags.isBilling) return 'delivery_billing'
|
||||
if (flags.isDelivery) return 'delivery'
|
||||
if (flags.isBilling) return 'billing'
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/** Code stable du type de reglement « virement » (cf. PaymentType.code, RG-1.12). */
|
||||
const PAYMENT_TYPE_TRANSFER = 'VIREMENT'
|
||||
|
||||
@@ -156,3 +247,32 @@ export function isBankRequiredForPaymentType(code: string | null | undefined): b
|
||||
export function isRibRequiredForPaymentType(code: string | null | undefined): boolean {
|
||||
return code === PAYMENT_TYPE_LCR
|
||||
}
|
||||
|
||||
/** Sous-ensemble du brouillon comptable portant les six champs obligatoires. */
|
||||
export interface AccountingRequiredDraft {
|
||||
siren: string | null
|
||||
accountNumber: string | null
|
||||
nTva: string | null
|
||||
tvaModeIri: string | null
|
||||
paymentDelayIri: string | null
|
||||
paymentTypeIri: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* RG-1.30 : les six champs scalaires de l'onglet Comptabilite sont obligatoires
|
||||
* pour valider l'onglet (SIREN, N de compte, Mode de TVA, N de TVA, Delai de
|
||||
* reglement, Type de reglement). bank / RIB restent conditionnels (RG-1.12 /
|
||||
* RG-1.13) et sont evalues a part. Miroir front du
|
||||
* ClientAccountingCompletenessValidator : meme gate que les onglets Contact /
|
||||
* Adresse (bouton « Valider » desactive tant que l'onglet n'est pas complet).
|
||||
*/
|
||||
export function hasAllRequiredAccountingFields(accounting: AccountingRequiredDraft): boolean {
|
||||
const filled = (v: string | null): boolean => v !== null && v.trim() !== ''
|
||||
|
||||
return filled(accounting.siren)
|
||||
&& filled(accounting.accountNumber)
|
||||
&& filled(accounting.nTva)
|
||||
&& filled(accounting.tvaModeIri)
|
||||
&& filled(accounting.paymentDelayIri)
|
||||
&& filled(accounting.paymentTypeIri)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user