feat(front) : page Modification fournisseur (/suppliers/{id}/edit) (ERP-96) (#85)
Auto Tag Develop / tag (push) Successful in 7s
Auto Tag Develop / tag (push) Successful in 7s
## ERP-96 — Modification fournisseur Étape 7/7 (front). Dépend de #94 (Ajouter) + #95 (Consultation). > ⚠️ MR **stackée sur `feature/ERP-95-suppliers-show`** (95 → 94, pas encore mergées dans develop) pour limiter le diff aux 3 fichiers d'ERP-96. À recibler sur `develop` une fois 94 puis 95 mergées. Squash au merge. ### Périmètre - Route `/suppliers/{id}/edit` : champs **pré-remplis** depuis GET /suppliers/{id}, **PATCH partiel indépendant par onglet**. Bloc principal conservé (éditable via son propre PATCH `supplier:write:main`), pas de contact inline (ERP-106). - **Mode strict (RG-2.16)** : chaque onglet n'envoie QUE les champs de son groupe de sérialisation (jamais de mélange → sinon 403). Builders de payload scopés (`supplierEdit`). - Éditabilité par rôle (`resolveTabEditability`) : métier readonly sans `manage` ; Comptabilité visible/éditable selon `accounting.view`/`accounting.manage` ; placeholders non éditables. - Collections contacts/adresses/RIB : POST/PATCH par ligne + DELETE différé des retraits ; 422 mappées **inline par champ** (`propertyPath` → `useSupplierFormErrors`/`extractApiViolations`), jamais un toast fourre-tout (ERP-101). ### Tests - Vitest : `supplierEdit.spec.ts` enrichi (mappers d'hydratation `mapMainDraft`/`mapInformationDraft` avec `volumeForecast`/`mapAccountingFormDraft` + `resolveTabEditability` matrice § 2.7). `make nuxt-test` → 375/375 ✅. ESLint ✅. - `nuxi typecheck` non lancé sur l'hôte (casse le conteneur dev-nuxt). Miroir de l'écran Modification client (M1), adapté M2 (enum `addressType`, `bennes`/`triageProvider`/`volumeForecast`, pas de relation Distributeur/Courtier). Reviewed-on: #85 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #85.
This commit is contained in:
@@ -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 = {
|
||||
|
||||
@@ -6,7 +6,12 @@ import {
|
||||
buildInformationPayload,
|
||||
buildMainPayload,
|
||||
buildRibPayload,
|
||||
mapAccountingFormDraft,
|
||||
mapInformationDraft,
|
||||
mapMainDraft,
|
||||
resolveTabEditability,
|
||||
} from '../supplierEdit'
|
||||
import type { SupplierDetail } from '~/modules/commercial/utils/supplierConsultation'
|
||||
import { emptyAddress, emptyContact, emptyRib } from '~/modules/commercial/types/supplierForm'
|
||||
|
||||
describe('buildMainPayload (groupe supplier:write:main)', () => {
|
||||
@@ -17,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: [] })
|
||||
expect('companyName' in payload).toBe(false)
|
||||
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)', () => {
|
||||
@@ -86,6 +97,16 @@ describe('buildAddressPayload (sous-ressource supplier_address — specificites
|
||||
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)', () => {
|
||||
const payload = buildAddressPayload({ ...emptyAddress(), addressType: 'DEPART' })
|
||||
expect('billingEmail' in payload).toBe(false)
|
||||
@@ -113,3 +134,85 @@ describe('buildRibPayload (sous-ressource supplier_rib)', () => {
|
||||
expect(payload.iban).toBe('FR1420041010050500013M02606')
|
||||
})
|
||||
})
|
||||
|
||||
describe('mapMainDraft — pre-remplissage bloc principal (companyName + categories, pas de relation M2)', () => {
|
||||
it('extrait companyName et les IRI de categories', () => {
|
||||
const draft = mapMainDraft({
|
||||
'@id': '/api/suppliers/85', id: 85,
|
||||
companyName: 'DOD862875',
|
||||
categories: [{ '@id': '/api/categories/2279', code: 'NEGOCIANT' }],
|
||||
} as SupplierDetail)
|
||||
expect(draft.companyName).toBe('DOD862875')
|
||||
expect(draft.categoryIris).toEqual(['/api/categories/2279'])
|
||||
})
|
||||
|
||||
it('gere les cles omises (skip_null_values) sans planter', () => {
|
||||
const draft = mapMainDraft({ '@id': '/api/suppliers/2', id: 2 } as SupplierDetail)
|
||||
expect(draft.companyName).toBeNull()
|
||||
expect(draft.categoryIris).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('mapInformationDraft — pre-remplissage onglet Information (+ volumeForecast M2)', () => {
|
||||
it('tronque foundedAt, stringifie employeesCount et volumeForecast', () => {
|
||||
const draft = mapInformationDraft({
|
||||
'@id': '/api/suppliers/85', id: 85,
|
||||
foundedAt: '2008-04-01T00:00:00+02:00', employeesCount: 42, volumeForecast: 8000,
|
||||
} as SupplierDetail)
|
||||
expect(draft.foundedAt).toBe('2008-04-01')
|
||||
expect(draft.employeesCount).toBe('42')
|
||||
expect(draft.volumeForecast).toBe('8000')
|
||||
})
|
||||
|
||||
it('cles omises -> null (volumeForecast inclus)', () => {
|
||||
const draft = mapInformationDraft({ '@id': '/api/suppliers/1', id: 1 } as SupplierDetail)
|
||||
expect(draft.foundedAt).toBeNull()
|
||||
expect(draft.employeesCount).toBeNull()
|
||||
expect(draft.volumeForecast).toBeNull()
|
||||
expect(draft.description).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('mapAccountingFormDraft — pre-remplissage onglet Comptabilite', () => {
|
||||
it('extrait les scalaires et les IRI des referentiels embarques', () => {
|
||||
const draft = mapAccountingFormDraft({
|
||||
'@id': '/api/suppliers/85', id: 85,
|
||||
siren: '123456789', accountNumber: 'F0001', nTva: 'FR00123456789',
|
||||
tvaMode: { '@id': '/api/tva_modes/30', label: 'France (ventes)' },
|
||||
paymentType: '/api/payment_types/14',
|
||||
} as SupplierDetail)
|
||||
expect(draft.siren).toBe('123456789')
|
||||
expect(draft.tvaModeIri).toBe('/api/tva_modes/30')
|
||||
expect(draft.paymentTypeIri).toBe('/api/payment_types/14')
|
||||
expect(draft.bankIri).toBeNull()
|
||||
})
|
||||
|
||||
it('cles comptables absentes (gating par omission) -> scalaires/IRI null', () => {
|
||||
const draft = mapAccountingFormDraft({ '@id': '/api/suppliers/1', id: 1 } as SupplierDetail)
|
||||
expect(draft.siren).toBeNull()
|
||||
expect(draft.tvaModeIri).toBeNull()
|
||||
expect(draft.bankIri).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolveTabEditability — gating par role (matrice § 2.7)', () => {
|
||||
it('Admin : tout editable', () => {
|
||||
expect(resolveTabEditability({ canManage: true, canAccountingView: true, canAccountingManage: true }))
|
||||
.toEqual({ businessEditable: true, accountingVisible: true, accountingEditable: true })
|
||||
})
|
||||
|
||||
it('Bureau / Commerciale (manage seul) : metier editable, Comptabilite masquee', () => {
|
||||
expect(resolveTabEditability({ canManage: true, canAccountingView: false, canAccountingManage: false }))
|
||||
.toEqual({ businessEditable: true, accountingVisible: false, accountingEditable: false })
|
||||
})
|
||||
|
||||
it('Compta (accounting seul) : metier readonly, Comptabilite editable', () => {
|
||||
expect(resolveTabEditability({ canManage: false, canAccountingView: true, canAccountingManage: true }))
|
||||
.toEqual({ businessEditable: false, accountingVisible: true, accountingEditable: true })
|
||||
})
|
||||
|
||||
it('Sans permission d\'edition : rien d\'editable', () => {
|
||||
expect(resolveTabEditability({ canManage: false, canAccountingView: false, canAccountingManage: false }))
|
||||
.toEqual({ businessEditable: false, accountingVisible: false, accountingEditable: false })
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user