3b2b441e5f
Les 5 champs inline (nom, prenom, telephones, email) sont retires des ecrans creation / consultation / modification du Client. Les coordonnees sont saisies exclusivement dans l'onglet Contacts (ClientContactBlock). Types, mappeurs, validations, payloads et cles i18n form.main.* associes nettoyes ; tests Vitest clientEdit ajustes. Ticket M1 2/3 (front), refonte-contact.
242 lines
10 KiB
TypeScript
242 lines
10 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
buildAccountingPayload,
|
|
buildAddressPayload,
|
|
buildContactPayload,
|
|
buildInformationPayload,
|
|
buildMainPayload,
|
|
buildRibPayload,
|
|
mapAccountingFormDraft,
|
|
mapInformationDraft,
|
|
mapMainDraft,
|
|
resolveTabEditability,
|
|
type AccountingFormDraft,
|
|
type InformationFormDraft,
|
|
type MainFormDraft,
|
|
} from '../clientEdit'
|
|
import type { ClientDetail } from '../clientConsultation'
|
|
import type { AddressFormDraft, ContactFormDraft, RibFormDraft } from '~/modules/commercial/types/clientForm'
|
|
|
|
// ── Fabriques de brouillons (valeurs distinctes pour reperer les fuites) ─────
|
|
|
|
function mainDraft(overrides: Partial<MainFormDraft> = {}): MainFormDraft {
|
|
return {
|
|
companyName: 'ACME',
|
|
categoryIris: ['/api/categories/1'],
|
|
relationType: null,
|
|
distributorIri: null,
|
|
brokerIri: null,
|
|
triageService: false,
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
function informationDraft(overrides: Partial<InformationFormDraft> = {}): InformationFormDraft {
|
|
return {
|
|
description: 'desc',
|
|
competitors: 'concurrents',
|
|
foundedAt: '2010-05-01',
|
|
employeesCount: '42',
|
|
revenueAmount: '1000000',
|
|
profitAmount: '50000',
|
|
directorName: 'PDG',
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
function accountingDraft(overrides: Partial<AccountingFormDraft> = {}): AccountingFormDraft {
|
|
return {
|
|
siren: '123456789',
|
|
accountNumber: 'C-001',
|
|
nTva: 'FR123',
|
|
tvaModeIri: '/api/tva_modes/1',
|
|
paymentDelayIri: '/api/payment_delays/1',
|
|
paymentTypeIri: '/api/payment_types/1',
|
|
bankIri: '/api/banks/1',
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
// Champs de chaque groupe de serialisation (miroir back ClientProcessor).
|
|
// Le contact inline (nom/prenom/telephones/email) ne fait plus partie du groupe
|
|
// main : les coordonnees vivent desormais sur la sous-ressource ClientContact.
|
|
const MAIN_KEYS = [
|
|
'companyName', 'categories', 'distributor', 'broker', 'triageService',
|
|
]
|
|
const INFORMATION_KEYS = [
|
|
'description', 'competitors', 'foundedAt', 'employeesCount',
|
|
'revenueAmount', 'profitAmount', 'directorName',
|
|
]
|
|
const ACCOUNTING_KEYS = ['siren', 'accountNumber', 'tvaMode', 'nTva', 'paymentDelay', 'paymentType', 'bank']
|
|
|
|
describe('buildMainPayload — scoping strict groupe client:write:main', () => {
|
|
it('n\'expose QUE les champs du groupe main (aucune fuite information/accounting)', () => {
|
|
expect(Object.keys(buildMainPayload(mainDraft())).sort()).toEqual([...MAIN_KEYS].sort())
|
|
})
|
|
|
|
it('relation distributeur : renseigne distributor, force broker a null (RG-1.03)', () => {
|
|
const payload = buildMainPayload(mainDraft({
|
|
relationType: 'distributeur',
|
|
distributorIri: '/api/clients/9',
|
|
brokerIri: '/api/clients/7',
|
|
}))
|
|
expect(payload.distributor).toBe('/api/clients/9')
|
|
expect(payload.broker).toBeNull()
|
|
})
|
|
|
|
it('relation courtier : renseigne broker, force distributor a null (RG-1.03)', () => {
|
|
const payload = buildMainPayload(mainDraft({
|
|
relationType: 'courtier',
|
|
distributorIri: '/api/clients/9',
|
|
brokerIri: '/api/clients/7',
|
|
}))
|
|
expect(payload.broker).toBe('/api/clients/7')
|
|
expect(payload.distributor).toBeNull()
|
|
})
|
|
|
|
it('sans relation : distributor et broker a null', () => {
|
|
const payload = buildMainPayload(mainDraft({ relationType: null }))
|
|
expect(payload.distributor).toBeNull()
|
|
expect(payload.broker).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('buildInformationPayload — scoping strict groupe client:write:information', () => {
|
|
it('n\'expose QUE les champs du groupe information (aucune fuite main/accounting)', () => {
|
|
expect(Object.keys(buildInformationPayload(informationDraft())).sort()).toEqual([...INFORMATION_KEYS].sort())
|
|
})
|
|
|
|
it('convertit employeesCount en nombre et vide -> null', () => {
|
|
expect(buildInformationPayload(informationDraft({ employeesCount: '42' })).employeesCount).toBe(42)
|
|
expect(buildInformationPayload(informationDraft({ employeesCount: null })).employeesCount).toBeNull()
|
|
expect(buildInformationPayload(informationDraft({ employeesCount: '' })).employeesCount).toBeNull()
|
|
})
|
|
|
|
it('chaines vides normalisees en null', () => {
|
|
const payload = buildInformationPayload(informationDraft({ description: '', directorName: '' }))
|
|
expect(payload.description).toBeNull()
|
|
expect(payload.directorName).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('buildAccountingPayload — scoping strict groupe client:write:accounting', () => {
|
|
it('n\'expose QUE les champs du groupe accounting (aucune fuite main/information)', () => {
|
|
expect(Object.keys(buildAccountingPayload(accountingDraft(), true)).sort()).toEqual([...ACCOUNTING_KEYS].sort())
|
|
})
|
|
|
|
it('banque conservee si requise (Virement), forcee a null sinon (RG-1.12)', () => {
|
|
expect(buildAccountingPayload(accountingDraft(), true).bank).toBe('/api/banks/1')
|
|
expect(buildAccountingPayload(accountingDraft(), false).bank).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('buildContactPayload / buildAddressPayload / buildRibPayload', () => {
|
|
it('contact : telephone secondaire ignore si non revele', () => {
|
|
const contact: ContactFormDraft = {
|
|
id: 5, iri: '/api/client_contacts/5', firstName: 'A', lastName: 'B',
|
|
jobTitle: null, phonePrimary: '0549112233', phoneSecondary: '0600000000',
|
|
email: null, hasSecondaryPhone: false,
|
|
}
|
|
expect(buildContactPayload(contact).phoneSecondary).toBeNull()
|
|
})
|
|
|
|
it('adresse : email facturation conserve uniquement si requis (RG-1.11)', () => {
|
|
const address: AddressFormDraft = {
|
|
id: 3, isProspect: false, isDelivery: false, isBilling: true, country: 'France',
|
|
postalCode: '86100', city: 'Châtellerault', street: '1 rue X', streetComplement: null,
|
|
categoryIris: ['/api/categories/2'], siteIris: ['/api/sites/1'], contactIris: [],
|
|
billingEmail: 'facturation@acme.fr',
|
|
}
|
|
expect(buildAddressPayload(address, true).billingEmail).toBe('facturation@acme.fr')
|
|
expect(buildAddressPayload(address, false).billingEmail).toBeNull()
|
|
})
|
|
|
|
it('rib : label / bic / iban transmis tels quels', () => {
|
|
const rib: RibFormDraft = { id: 1, label: 'Compte principal', bic: 'BNPAFRPP', iban: 'FR76...' }
|
|
expect(buildRibPayload(rib)).toEqual({ label: 'Compte principal', bic: 'BNPAFRPP', iban: 'FR76...' })
|
|
})
|
|
})
|
|
|
|
describe('mapMainDraft — pre-remplissage bloc principal', () => {
|
|
it('resout la relation et extrait les IRI (sans contact inline)', () => {
|
|
const client = {
|
|
'@id': '/api/clients/1', id: 1,
|
|
companyName: 'ACME', triageService: true,
|
|
categories: [{ '@id': '/api/categories/1', code: 'SECTEUR' }],
|
|
distributor: { '@id': '/api/clients/9', companyName: 'DISTRIB' },
|
|
} as ClientDetail
|
|
|
|
const draft = mapMainDraft(client)
|
|
expect(draft.companyName).toBe('ACME')
|
|
expect(draft.categoryIris).toEqual(['/api/categories/1'])
|
|
expect(draft.relationType).toBe('distributeur')
|
|
expect(draft.distributorIri).toBe('/api/clients/9')
|
|
expect(draft.brokerIri).toBeNull()
|
|
expect(draft.triageService).toBe(true)
|
|
})
|
|
|
|
it('gere les cles omises (skip_null_values) sans planter', () => {
|
|
const draft = mapMainDraft({ '@id': '/api/clients/2', id: 2 } as ClientDetail)
|
|
expect(draft.companyName).toBeNull()
|
|
expect(draft.categoryIris).toEqual([])
|
|
expect(draft.relationType).toBeNull()
|
|
expect(draft.triageService).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('mapInformationDraft — pre-remplissage onglet Information', () => {
|
|
it('tronque foundedAt en YYYY-MM-DD et stringifie employeesCount', () => {
|
|
const draft = mapInformationDraft({
|
|
'@id': '/api/clients/1', id: 1,
|
|
foundedAt: '2010-05-01T00:00:00+00:00', employeesCount: 42, revenueAmount: '1000000',
|
|
} as ClientDetail)
|
|
expect(draft.foundedAt).toBe('2010-05-01')
|
|
expect(draft.employeesCount).toBe('42')
|
|
expect(draft.revenueAmount).toBe('1000000')
|
|
})
|
|
|
|
it('cles omises -> null', () => {
|
|
const draft = mapInformationDraft({ '@id': '/api/clients/1', id: 1 } as ClientDetail)
|
|
expect(draft.foundedAt).toBeNull()
|
|
expect(draft.employeesCount).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/clients/1', id: 1,
|
|
siren: '123456789', accountNumber: 'C-001', nTva: 'FR123',
|
|
tvaMode: { '@id': '/api/tva_modes/2', label: 'Normal' },
|
|
paymentType: '/api/payment_types/3',
|
|
} as ClientDetail)
|
|
expect(draft.siren).toBe('123456789')
|
|
expect(draft.tvaModeIri).toBe('/api/tva_modes/2')
|
|
expect(draft.paymentTypeIri).toBe('/api/payment_types/3')
|
|
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 })
|
|
})
|
|
})
|