fix(front) : 422 sur champ requis vide en edition fournisseur (ERP-96)
En edition (PATCH merge), omettre la cle d'un champ requis vide laissait la valeur serveur inchangee -> faux 200 (l'ancien code postal etait conserve). Nouveau helper blankEmptyRequired + flag forUpdate sur les builders : a la creation (POST) on omet toujours la cle (NotBlank), en edition d'une ligne existante on envoie '' (chaine valide, pas de 400 de type) pour declencher NotBlank 422 inline sous le champ. Applique au bloc principal, aux adresses et aux RIB (selon id !== null).
This commit is contained in:
@@ -590,7 +590,7 @@ async function submitMain(): Promise<void> {
|
|||||||
mainSubmitting.value = true
|
mainSubmitting.value = true
|
||||||
mainErrors.clearErrors()
|
mainErrors.clearErrors()
|
||||||
try {
|
try {
|
||||||
const updated = await api.patch<SupplierDetail>(`/suppliers/${supplierId}`, buildMainPayload(main), {
|
const updated = await api.patch<SupplierDetail>(`/suppliers/${supplierId}`, buildMainPayload(main, { forUpdate: true }), {
|
||||||
headers: { Accept: 'application/ld+json' },
|
headers: { Accept: 'application/ld+json' },
|
||||||
toast: false,
|
toast: false,
|
||||||
})
|
})
|
||||||
@@ -750,7 +750,10 @@ async function submitAddresses(): Promise<void> {
|
|||||||
addresses.value,
|
addresses.value,
|
||||||
addressErrors,
|
addressErrors,
|
||||||
async (address) => {
|
async (address) => {
|
||||||
const body = buildAddressPayload(address)
|
// Edition d'une adresse existante : champ requis vide envoye en `''`
|
||||||
|
// (NotBlank 422) au lieu d'etre omis — sinon le PATCH garderait
|
||||||
|
// l'ancienne valeur (faux 200). Creation (id null) : omit classique.
|
||||||
|
const body = buildAddressPayload(address, { forUpdate: address.id !== null })
|
||||||
if (address.id === null) {
|
if (address.id === null) {
|
||||||
const created = await api.post<{ id: number }>(
|
const created = await api.post<{ id: number }>(
|
||||||
`/suppliers/${supplierId}/addresses`,
|
`/suppliers/${supplierId}/addresses`,
|
||||||
@@ -843,7 +846,9 @@ async function submitAccounting(): Promise<void> {
|
|||||||
ribs.value,
|
ribs.value,
|
||||||
ribErrors,
|
ribErrors,
|
||||||
async (rib) => {
|
async (rib) => {
|
||||||
const body = buildRibPayload(rib)
|
// Edition d'un RIB existant : champ requis vide envoye en `''` (NotBlank
|
||||||
|
// 422) au lieu d'etre omis (sinon le PATCH garderait l'ancienne valeur).
|
||||||
|
const body = buildRibPayload(rib, { forUpdate: rib.id !== null })
|
||||||
if (rib.id === null) {
|
if (rib.id === null) {
|
||||||
const created = await api.post<{ id: number }>(
|
const created = await api.post<{ id: number }>(
|
||||||
`/suppliers/${supplierId}/ribs`,
|
`/suppliers/${supplierId}/ribs`,
|
||||||
|
|||||||
@@ -22,11 +22,17 @@ describe('buildMainPayload (groupe supplier:write:main)', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('omet companyName vide (-> 422 NotBlank, ERP-119)', () => {
|
it('CREATION : omet companyName vide (-> 422 NotBlank, ERP-119)', () => {
|
||||||
const payload = buildMainPayload({ companyName: null, categoryIris: [] })
|
const payload = buildMainPayload({ companyName: null, categoryIris: [] })
|
||||||
expect('companyName' in payload).toBe(false)
|
expect('companyName' in payload).toBe(false)
|
||||||
expect(payload.categories).toEqual([])
|
expect(payload.categories).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('EDITION (forUpdate) : companyName vide envoye en `\'\'` (PATCH -> 422 NotBlank, pas un faux 200)', () => {
|
||||||
|
const payload = buildMainPayload({ companyName: '', categoryIris: [] }, { forUpdate: true })
|
||||||
|
expect('companyName' in payload).toBe(true)
|
||||||
|
expect(payload.companyName).toBe('')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('buildInformationPayload (groupe supplier:write:information)', () => {
|
describe('buildInformationPayload (groupe supplier:write:information)', () => {
|
||||||
@@ -91,6 +97,16 @@ describe('buildAddressPayload (sous-ressource supplier_address — specificites
|
|||||||
expect('addressType' in payload).toBe(false)
|
expect('addressType' in payload).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('EDITION (forUpdate) : un champ requis vide est envoye en `\'\'` (et NON omis) pour declencher la 422 NotBlank au PATCH', () => {
|
||||||
|
// Bug edition : omettre la cle d'un champ requis vide laisse le PATCH garder
|
||||||
|
// l'ancienne valeur (faux 200). En `forUpdate`, on envoie `''` -> NotBlank 422.
|
||||||
|
const payload = buildAddressPayload({ ...emptyAddress(), addressType: 'DEPART', postalCode: '' }, { forUpdate: true })
|
||||||
|
expect('postalCode' in payload).toBe(true)
|
||||||
|
expect(payload.postalCode).toBe('')
|
||||||
|
// Un champ requis renseigne reste tel quel.
|
||||||
|
expect(payload.addressType).toBe('DEPART')
|
||||||
|
})
|
||||||
|
|
||||||
it('n\'expose jamais d\'email de facturation (difference M1)', () => {
|
it('n\'expose jamais d\'email de facturation (difference M1)', () => {
|
||||||
const payload = buildAddressPayload({ ...emptyAddress(), addressType: 'DEPART' })
|
const payload = buildAddressPayload({ ...emptyAddress(), addressType: 'DEPART' })
|
||||||
expect('billingEmail' in payload).toBe(false)
|
expect('billingEmail' in payload).toBe(false)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
ADDRESS_REQUIRED_NON_NULLABLE_KEYS,
|
ADDRESS_REQUIRED_NON_NULLABLE_KEYS,
|
||||||
|
blankEmptyRequired,
|
||||||
MAIN_REQUIRED_NON_NULLABLE_KEYS,
|
MAIN_REQUIRED_NON_NULLABLE_KEYS,
|
||||||
omitEmptyRequired,
|
omitEmptyRequired,
|
||||||
RIB_REQUIRED_NON_NULLABLE_KEYS,
|
RIB_REQUIRED_NON_NULLABLE_KEYS,
|
||||||
@@ -138,14 +139,37 @@ export function resolveTabEditability(abilities: SupplierEditAbilities): TabEdit
|
|||||||
// ── Scoping strict des payloads PATCH/POST ──────────────────────────────────
|
// ── Scoping strict des payloads PATCH/POST ──────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Payload du bloc principal — groupe supplier:write:main UNIQUEMENT.
|
* Options de construction d'un payload d'ecriture.
|
||||||
* companyName omis si vide -> 422 NotBlank au lieu d'un 400 de type (ERP-119).
|
* - `forUpdate: false` (defaut, CREATION/POST) : les champs requis vides sont OMIS
|
||||||
|
* -> 422 NotBlank a l'insert (le back ne reçoit pas la cle).
|
||||||
|
* - `forUpdate: true` (EDITION/PATCH d'une ligne existante) : les champs requis
|
||||||
|
* vides sont envoyes en `''` -> 422 NotBlank (sinon une cle omise laisse la valeur
|
||||||
|
* serveur inchangee, faux 200 — cf. blankEmptyRequired).
|
||||||
*/
|
*/
|
||||||
export function buildMainPayload(main: MainFormDraft): Record<string, unknown> {
|
export interface BuildPayloadOptions {
|
||||||
return omitEmptyRequired({
|
forUpdate?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Selectionne le finaliseur des champs requis selon création (omit) vs édition (blank). */
|
||||||
|
function finalizeRequired<T extends Record<string, unknown>>(
|
||||||
|
payload: T,
|
||||||
|
requiredKeys: readonly string[],
|
||||||
|
options: BuildPayloadOptions,
|
||||||
|
): T {
|
||||||
|
return options.forUpdate
|
||||||
|
? blankEmptyRequired(payload, requiredKeys)
|
||||||
|
: omitEmptyRequired(payload, requiredKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Payload du bloc principal — groupe supplier:write:main UNIQUEMENT.
|
||||||
|
* companyName vide -> 422 NotBlank (omis a la creation, `''` en edition — ERP-119).
|
||||||
|
*/
|
||||||
|
export function buildMainPayload(main: MainFormDraft, options: BuildPayloadOptions = {}): Record<string, unknown> {
|
||||||
|
return finalizeRequired({
|
||||||
companyName: main.companyName,
|
companyName: main.companyName,
|
||||||
categories: main.categoryIris,
|
categories: main.categoryIris,
|
||||||
}, MAIN_REQUIRED_NON_NULLABLE_KEYS)
|
}, MAIN_REQUIRED_NON_NULLABLE_KEYS, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Payload de l'onglet Information — groupe supplier:write:information UNIQUEMENT. */
|
/** Payload de l'onglet Information — groupe supplier:write:information UNIQUEMENT. */
|
||||||
@@ -200,8 +224,8 @@ export function buildContactPayload(contact: SupplierContactFormDraft): Record<s
|
|||||||
* `bennes` (entier, 0 par defaut) + `triageProvider` (booleen). Pas d'email de
|
* `bennes` (entier, 0 par defaut) + `triageProvider` (booleen). Pas d'email de
|
||||||
* facturation (difference M1).
|
* facturation (difference M1).
|
||||||
*/
|
*/
|
||||||
export function buildAddressPayload(address: SupplierAddressFormDraft): Record<string, unknown> {
|
export function buildAddressPayload(address: SupplierAddressFormDraft, options: BuildPayloadOptions = {}): Record<string, unknown> {
|
||||||
return omitEmptyRequired({
|
return finalizeRequired({
|
||||||
addressType: address.addressType,
|
addressType: address.addressType,
|
||||||
country: address.country,
|
country: address.country,
|
||||||
postalCode: address.postalCode || null,
|
postalCode: address.postalCode || null,
|
||||||
@@ -213,14 +237,14 @@ export function buildAddressPayload(address: SupplierAddressFormDraft): Record<s
|
|||||||
contacts: address.contactIris,
|
contacts: address.contactIris,
|
||||||
bennes: address.bennes !== null && address.bennes !== '' ? Number(address.bennes) : null,
|
bennes: address.bennes !== null && address.bennes !== '' ? Number(address.bennes) : null,
|
||||||
triageProvider: address.triageProvider,
|
triageProvider: address.triageProvider,
|
||||||
}, ADDRESS_REQUIRED_NON_NULLABLE_KEYS)
|
}, ADDRESS_REQUIRED_NON_NULLABLE_KEYS, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Payload d'un RIB (sous-ressource supplier_rib). */
|
/** Payload d'un RIB (sous-ressource supplier_rib). */
|
||||||
export function buildRibPayload(rib: SupplierRibFormDraft): Record<string, unknown> {
|
export function buildRibPayload(rib: SupplierRibFormDraft, options: BuildPayloadOptions = {}): Record<string, unknown> {
|
||||||
return omitEmptyRequired({
|
return finalizeRequired({
|
||||||
label: rib.label,
|
label: rib.label,
|
||||||
bic: rib.bic,
|
bic: rib.bic,
|
||||||
iban: rib.iban,
|
iban: rib.iban,
|
||||||
}, RIB_REQUIRED_NON_NULLABLE_KEYS)
|
}, RIB_REQUIRED_NON_NULLABLE_KEYS, options)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -217,3 +217,28 @@ export function omitEmptyRequired<T extends Record<string, unknown>>(
|
|||||||
|
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variante PATCH (edition d'une ligne EXISTANTE) : remplace les cles requises
|
||||||
|
* laissees vides par une chaine vide `''` au lieu de les OMETTRE.
|
||||||
|
*
|
||||||
|
* Pourquoi pas `omitEmptyRequired` en edition : un PATCH a semantique merge — une
|
||||||
|
* cle absente laisse la valeur serveur INCHANGEE. Vider un champ requis puis valider
|
||||||
|
* renverrait alors un 200 trompeur (l'ancienne valeur est conservee). En envoyant
|
||||||
|
* `''`, la propriete `?string` est bien deserialisee (pas de 400 de type, contrairement
|
||||||
|
* a `null` sur une colonne non-nullable), puis le Validator `NotBlank(trim)` la rejette
|
||||||
|
* -> 422 avec propertyPath, mappee inline sous le champ. Mute et retourne le payload.
|
||||||
|
*/
|
||||||
|
export function blankEmptyRequired<T extends Record<string, unknown>>(
|
||||||
|
payload: T,
|
||||||
|
requiredKeys: readonly string[],
|
||||||
|
): T {
|
||||||
|
for (const key of requiredKeys) {
|
||||||
|
const value = payload[key]
|
||||||
|
if (value === null || value === undefined || value === '') {
|
||||||
|
(payload as Record<string, unknown>)[key] = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user