18fdf9354f
- page edit.vue : champs pre-remplis depuis GET /suppliers/{id}, PATCH partiel
INDEPENDANT par onglet (mode strict RG-2.16 : un seul groupe de serialisation
par appel), pas de formulaire principal masque mais editable via son propre PATCH
- pas de contact inline (ERP-106) ; onglets metier readonly sans manage, Comptabilite
visible/editable selon accounting.view / accounting.manage (resolveTabEditability)
- collections contacts/adresses/RIB : POST/PATCH par ligne + DELETE differe des
retraits ; erreurs 422 mappees inline par champ (propertyPath) via useSupplierFormErrors
- supplierEdit : mappers d'hydratation (mapMainDraft/mapInformationDraft/
mapAccountingFormDraft, + volumeForecast) et resolveTabEditability
- tests Vitest : mappers d'hydratation + gating par role (matrice 2.7)
- miroir de l'ecran Modification client (M1), adapte M2 (addressType/bennes/
triageProvider/volumeForecast, pas de relation Distributeur/Courtier)
203 lines
9.0 KiB
TypeScript
203 lines
9.0 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import {
|
|
buildAccountingPayload,
|
|
buildAddressPayload,
|
|
buildContactPayload,
|
|
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)', () => {
|
|
it('envoie companyName + categories quand renseignes', () => {
|
|
expect(buildMainPayload({ companyName: 'ACME', categoryIris: ['/api/categories/1'] })).toEqual({
|
|
companyName: 'ACME',
|
|
categories: ['/api/categories/1'],
|
|
})
|
|
})
|
|
|
|
it('omet companyName vide (-> 422 NotBlank, ERP-119)', () => {
|
|
const payload = buildMainPayload({ companyName: null, categoryIris: [] })
|
|
expect('companyName' in payload).toBe(false)
|
|
expect(payload.categories).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('buildInformationPayload (groupe supplier:write:information)', () => {
|
|
const base = {
|
|
description: null, competitors: null, foundedAt: null, employeesCount: null,
|
|
revenueAmount: null, profitAmount: null, directorName: null, volumeForecast: null,
|
|
}
|
|
|
|
it('convertit employeesCount et volumeForecast en nombre, null si vide', () => {
|
|
expect(buildInformationPayload({ ...base, employeesCount: '42', volumeForecast: '1000' })).toMatchObject({
|
|
employeesCount: 42,
|
|
volumeForecast: 1000,
|
|
})
|
|
expect(buildInformationPayload(base)).toMatchObject({ employeesCount: null, volumeForecast: null })
|
|
})
|
|
})
|
|
|
|
describe('buildContactPayload (sous-ressource supplier_contact)', () => {
|
|
it('n\'envoie le 2e telephone que si revele (hasSecondaryPhone)', () => {
|
|
const contact = { ...emptyContact(), phonePrimary: '0102030405', phoneSecondary: '0607080910' }
|
|
expect(buildContactPayload({ ...contact, hasSecondaryPhone: false }).phoneSecondary).toBeNull()
|
|
expect(buildContactPayload({ ...contact, hasSecondaryPhone: true }).phoneSecondary).toBe('0607080910')
|
|
})
|
|
})
|
|
|
|
describe('buildAddressPayload (sous-ressource supplier_address — specificites M2)', () => {
|
|
it('envoie addressType (enum), bennes (nombre) et triageProvider', () => {
|
|
const address = {
|
|
...emptyAddress(),
|
|
addressType: 'RENDU' as const,
|
|
postalCode: '86100', city: 'Châtellerault', street: '1 rue de la Paix',
|
|
siteIris: ['/api/sites/1'], categoryIris: ['/api/categories/2'],
|
|
bennes: '3', triageProvider: true,
|
|
}
|
|
expect(buildAddressPayload(address)).toMatchObject({
|
|
addressType: 'RENDU',
|
|
bennes: 3,
|
|
triageProvider: true,
|
|
sites: ['/api/sites/1'],
|
|
categories: ['/api/categories/2'],
|
|
})
|
|
})
|
|
|
|
it('bennes null quand le champ est vide', () => {
|
|
expect(buildAddressPayload({ ...emptyAddress(), bennes: '' }).bennes).toBeNull()
|
|
})
|
|
|
|
it('omet postalCode / city / street vides (-> 422 NotBlank, ERP-119)', () => {
|
|
const payload = buildAddressPayload({ ...emptyAddress(), addressType: 'PROSPECT' })
|
|
expect('postalCode' in payload).toBe(false)
|
|
expect('city' in payload).toBe(false)
|
|
expect('street' in payload).toBe(false)
|
|
// Les champs non requis restent presents.
|
|
expect('streetComplement' in payload).toBe(true)
|
|
expect(payload.addressType).toBe('PROSPECT')
|
|
})
|
|
|
|
it('omet addressType quand aucun radio n\'est choisi (-> 422 NotBlank au lieu d\'un 400 de type)', () => {
|
|
// emptyAddress() laisse addressType a null : la cle doit etre absente du
|
|
// payload pour que le back renvoie une 422 propertyPath addressType.
|
|
const payload = buildAddressPayload(emptyAddress())
|
|
expect('addressType' in payload).toBe(false)
|
|
})
|
|
|
|
it('n\'expose jamais d\'email de facturation (difference M1)', () => {
|
|
const payload = buildAddressPayload({ ...emptyAddress(), addressType: 'DEPART' })
|
|
expect('billingEmail' in payload).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('buildAccountingPayload (groupe supplier:write:accounting)', () => {
|
|
const base = {
|
|
siren: '123456789', accountNumber: '00012345678', nTva: 'FR123',
|
|
tvaModeIri: '/api/tva_modes/1', paymentDelayIri: '/api/payment_delays/1',
|
|
paymentTypeIri: '/api/payment_types/1', bankIri: '/api/banks/1',
|
|
}
|
|
|
|
it('envoie la banque seulement si requise (VIREMENT, RG-2.07)', () => {
|
|
expect(buildAccountingPayload(base, true).bank).toBe('/api/banks/1')
|
|
expect(buildAccountingPayload(base, false).bank).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('buildRibPayload (sous-ressource supplier_rib)', () => {
|
|
it('omet les champs requis vides (-> 422 NotBlank, ERP-119)', () => {
|
|
const payload = buildRibPayload({ ...emptyRib(), iban: 'FR1420041010050500013M02606' })
|
|
expect('label' in payload).toBe(false)
|
|
expect('bic' in payload).toBe(false)
|
|
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 })
|
|
})
|
|
})
|