feat(front) : page Modification fournisseur (/suppliers/{id}/edit) (ERP-96) #85
@@ -689,7 +689,7 @@ async function submitMain(): Promise<void> {
|
||||
mainSubmitting.value = true
|
||||
mainErrors.clearErrors()
|
||||
try {
|
||||
const updated = await api.patch<ClientDetail>(`/clients/${clientId}`, buildMainPayload(main), {
|
||||
const updated = await api.patch<ClientDetail>(`/clients/${clientId}`, buildMainPayload(main, { forUpdate: true }), {
|
||||
headers: { Accept: 'application/ld+json' },
|
||||
toast: false,
|
||||
})
|
||||
@@ -859,7 +859,10 @@ async function submitAddresses(): Promise<void> {
|
||||
addresses.value,
|
||||
addressErrors,
|
||||
async (address) => {
|
||||
const body = buildAddressPayload(address, isBillingEmailRequired(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, isBillingEmailRequired(address), { forUpdate: address.id !== null })
|
||||
if (address.id === null) {
|
||||
const created = await api.post<{ id: number }>(
|
||||
`/clients/${clientId}/addresses`,
|
||||
@@ -956,7 +959,9 @@ async function submitAccounting(): Promise<void> {
|
||||
ribs.value,
|
||||
ribErrors,
|
||||
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) {
|
||||
const created = await api.post<{ id: number }>(
|
||||
`/clients/${clientId}/ribs`,
|
||||
|
||||
@@ -211,6 +211,38 @@ describe('buildContactPayload / buildAddressPayload / buildRibPayload', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Bug edition : en PATCH (merge), une cle de champ requis OMISE laisse la valeur
|
||||
// serveur inchangee -> faux 200 quand l'utilisateur vide le champ. En `forUpdate`,
|
||||
// on envoie `''` (chaine valide, pas de 400 de type) -> NotBlank 422 inline.
|
||||
describe('forUpdate (EDITION/PATCH) : champ requis vide -> `\'\'` au lieu d\'etre omis', () => {
|
||||
it('buildMainPayload : companyName vide envoye en `\'\'`', () => {
|
||||
const payload = buildMainPayload(mainDraft({ companyName: '' }), { forUpdate: true })
|
||||
expect('companyName' in payload).toBe(true)
|
||||
expect(payload.companyName).toBe('')
|
||||
})
|
||||
|
||||
it('buildAddressPayload : postalCode / city / street vides envoyes en `\'\'`', () => {
|
||||
const address: AddressFormDraft = {
|
||||
id: 7, isProspect: false, isDelivery: true, isBilling: false, isBroker: false, isDistributor: false, country: 'France',
|
||||
postalCode: '', city: null, street: '1 rue X', streetComplement: null,
|
||||
categoryIris: ['/api/categories/2'], siteIris: ['/api/sites/1'], contactIris: [],
|
||||
billingEmail: null, billingEmailSecondary: null, hasSecondaryBillingEmail: false,
|
||||
}
|
||||
const payload = buildAddressPayload(address, false, { forUpdate: true })
|
||||
expect(payload.postalCode).toBe('')
|
||||
expect(payload.city).toBe('')
|
||||
// Un champ requis renseigne reste tel quel.
|
||||
expect(payload.street).toBe('1 rue X')
|
||||
})
|
||||
|
||||
it('buildRibPayload : label / bic vides envoyes en `\'\'`, iban conserve', () => {
|
||||
const payload = buildRibPayload({ id: 4, label: '', bic: null, iban: 'FR7612345' }, { forUpdate: true })
|
||||
expect(payload.label).toBe('')
|
||||
expect(payload.bic).toBe('')
|
||||
expect(payload.iban).toBe('FR7612345')
|
||||
})
|
||||
})
|
||||
|
||||
describe('mapMainDraft — pre-remplissage bloc principal', () => {
|
||||
it('resout la relation et extrait les IRI (sans contact inline)', () => {
|
||||
const client = {
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from '~/modules/commercial/utils/clientConsultation'
|
||||
import {
|
||||
ADDRESS_REQUIRED_NON_NULLABLE_KEYS,
|
||||
blankEmptyRequired,
|
||||
MAIN_REQUIRED_NON_NULLABLE_KEYS,
|
||||
omitEmptyRequired,
|
||||
RIB_REQUIRED_NON_NULLABLE_KEYS,
|
||||
@@ -139,12 +140,35 @@ export function mapAccountingFormDraft(client: ClientDetail): AccountingFormDraf
|
||||
|
||||
// ── Scoping strict des payloads PATCH ────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Options de construction d'un payload d'ecriture.
|
||||
* - `forUpdate: false` (defaut, CREATION/POST) : champs requis vides OMIS -> 422
|
||||
* NotBlank (le back ne reçoit pas la cle, la propriete garde son defaut).
|
||||
* - `forUpdate: true` (EDITION/PATCH d'une ligne existante) : champs requis vides
|
||||
* envoyes en `''` -> 422 NotBlank (sinon une cle omise laisse la valeur serveur
|
||||
* inchangee, faux 200 — cf. blankEmptyRequired).
|
||||
*/
|
||||
export interface BuildPayloadOptions {
|
||||
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 client:write:main UNIQUEMENT. La relation
|
||||
* Distributeur/Courtier est mutuellement exclusive (RG-1.03) : on ne renseigne
|
||||
* que la FK correspondant au type choisi, l'autre est forcee a null.
|
||||
*/
|
||||
export function buildMainPayload(main: MainFormDraft): Record<string, unknown> {
|
||||
export function buildMainPayload(main: MainFormDraft, options: BuildPayloadOptions = {}): Record<string, unknown> {
|
||||
// companyName omis si vide -> 422 NotBlank au lieu d'un 400 de type (ERP-119).
|
||||
// relationType : champ transitoire (non persiste cote back) qui porte
|
||||
// l'intention UI « ce client depend d'un distributeur / courtier ». Il sert
|
||||
@@ -152,14 +176,14 @@ export function buildMainPayload(main: MainFormDraft): Record<string, unknown> {
|
||||
// la FK correspondante devient obligatoire -> 422 sur distributor / broker.
|
||||
// Sans equivalent derivable cote back (FK nullable), c'est la seule facon de
|
||||
// rester sur « on soumet, le back tranche » plutot qu'une garde front-only.
|
||||
return omitEmptyRequired({
|
||||
return finalizeRequired({
|
||||
companyName: main.companyName,
|
||||
categories: main.categoryIris,
|
||||
relationType: main.relationType,
|
||||
distributor: main.relationType === 'distributeur' ? main.distributorIri : null,
|
||||
broker: main.relationType === 'courtier' ? main.brokerIri : null,
|
||||
triageService: main.triageService,
|
||||
}, MAIN_REQUIRED_NON_NULLABLE_KEYS)
|
||||
}, MAIN_REQUIRED_NON_NULLABLE_KEYS, options)
|
||||
}
|
||||
|
||||
/** Payload de l'onglet Information — groupe client:write:information UNIQUEMENT. */
|
||||
@@ -211,9 +235,10 @@ export function buildContactPayload(contact: ContactFormDraft): Record<string, u
|
||||
export function buildAddressPayload(
|
||||
address: AddressFormDraft,
|
||||
isBillingEmailRequired: boolean,
|
||||
options: BuildPayloadOptions = {},
|
||||
): Record<string, unknown> {
|
||||
// postalCode / city / street omis si vides -> 422 NotBlank (ERP-119).
|
||||
return omitEmptyRequired({
|
||||
// postalCode / city / street : omis a la creation, `''` en edition -> 422 NotBlank (ERP-119).
|
||||
return finalizeRequired({
|
||||
isProspect: address.isProspect,
|
||||
isDelivery: address.isDelivery,
|
||||
isBilling: address.isBilling,
|
||||
@@ -229,18 +254,18 @@ export function buildAddressPayload(
|
||||
contacts: address.contactIris,
|
||||
billingEmail: isBillingEmailRequired ? (address.billingEmail || null) : null,
|
||||
billingEmailSecondary: isBillingEmailRequired && address.hasSecondaryBillingEmail ? (address.billingEmailSecondary || null) : null,
|
||||
}, ADDRESS_REQUIRED_NON_NULLABLE_KEYS)
|
||||
}, ADDRESS_REQUIRED_NON_NULLABLE_KEYS, options)
|
||||
}
|
||||
|
||||
/** Payload d'un RIB (sous-ressource client_rib). */
|
||||
export function buildRibPayload(rib: RibFormDraft): Record<string, unknown> {
|
||||
// label / bic / iban omis si vides -> 422 NotBlank au lieu d'un 400 de type
|
||||
// sur un RIB partiel (ex. IBAN seul). ERP-119.
|
||||
return omitEmptyRequired({
|
||||
export function buildRibPayload(rib: RibFormDraft, options: BuildPayloadOptions = {}): Record<string, unknown> {
|
||||
// label / bic / iban : omis a la creation, `''` en edition -> 422 NotBlank au lieu
|
||||
// d'un 400 de type (ou d'un faux 200 PATCH qui garderait l'ancienne valeur). ERP-119.
|
||||
return finalizeRequired({
|
||||
label: rib.label,
|
||||
bic: rib.bic,
|
||||
iban: rib.iban,
|
||||
}, RIB_REQUIRED_NON_NULLABLE_KEYS)
|
||||
}, RIB_REQUIRED_NON_NULLABLE_KEYS, options)
|
||||
}
|
||||
|
||||
// ── Gating par permission ────────────────────────────────────────────────────
|
||||
|
||||
@@ -419,3 +419,28 @@ export function omitEmptyRequired<T extends Record<string, unknown>>(
|
||||
|
||||
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
|
||||
* `''` (chaine valide), on evite le 400 de type (« must be string, NULL given ») et
|
||||
* le Validator `NotBlank(trim)` rejette la valeur -> 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