477f77a6b5
Auto Tag Develop / tag (push) Successful in 7s
## ERP-95 — Consultation fournisseur (lecture seule) Étape 6/7 (front). Dépend de #92 (contrat JSON figé) et #94 (blocs/types fournisseur). Bloque #96. > ⚠️ MR **stackée sur `feature/ERP-94-suppliers-new`** (ERP-94 pas encore mergée dans develop) pour garder le diff limité aux 5 fichiers d'ERP-95. À recibler sur `develop` une fois la 94 mergée. Squash au merge. ### Périmètre - `useSupplier(id)` : GET /api/suppliers/{id} en Hydra (embed contacts/adresses/ribs + scalaires compta si `accounting.view`), `archive()`/`restore()` via PATCH `isArchived` seul + rechargement complet. - `supplierConsultation` : mappers purs de l'embed (enum `addressType`, `bennes`/`triageProvider`, `volumeForecast`, gating compta par **omission de clé** → null) + helpers de permissions. - Page `[id]/index.vue` lecture seule : bloc principal + onglets Information / Contacts / Adresses / Comptabilité (si permission) / 4 coquilles « À venir » ; boutons Modifier (`manage`), Archiver/Restaurer (`archive`) ; flèche retour → répertoire. Miroir de l'écran Consultation client (M1). ### Tests - Vitest : `supplierConsultation.spec.ts` (mappers + permissions, gating compta) + `useSupplier.spec.ts` (GET/PATCH + propagation 403/409). `make nuxt-test` → 365/365 ✅. ESLint ✅. - `nuxi typecheck` non lancé sur l'hôte (régénère .nuxt/tailwind en chemins hôte et casse le conteneur dev-nuxt). Reviewed-on: #84 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
225 lines
8.9 KiB
TypeScript
225 lines
8.9 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
canEditSupplier,
|
|
categoryOptionsOf,
|
|
contactOptionsOf,
|
|
iriOf,
|
|
mapAccountingDraft,
|
|
mapAddressToDraft,
|
|
mapAddressView,
|
|
mapContactToDraft,
|
|
mapRibToDraft,
|
|
referentialOptionOf,
|
|
showArchiveAction,
|
|
showRestoreAction,
|
|
siteOptionsOf,
|
|
type SupplierDetail,
|
|
} from '../supplierConsultation'
|
|
|
|
describe('iriOf', () => {
|
|
it('retourne l\'@id d\'une relation embarquee (objet)', () => {
|
|
expect(iriOf({ '@id': '/api/payment_types/14', code: 'LCR' })).toBe('/api/payment_types/14')
|
|
})
|
|
|
|
it('retourne la chaine telle quelle si la relation est deja un IRI', () => {
|
|
expect(iriOf('/api/banks/3')).toBe('/api/banks/3')
|
|
})
|
|
|
|
it('retourne null pour une relation absente (null / undefined / skip_null_values)', () => {
|
|
expect(iriOf(null)).toBeNull()
|
|
expect(iriOf(undefined)).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('mapContactToDraft', () => {
|
|
it('formate les telephones en XX XX XX XX XX et conserve l\'iri', () => {
|
|
const draft = mapContactToDraft({
|
|
'@id': '/api/supplier_contacts/39',
|
|
id: 39,
|
|
firstName: 'Marie',
|
|
lastName: 'Martin',
|
|
jobTitle: 'Responsable achats',
|
|
phonePrimary: '0612345678',
|
|
email: 'marie.martin@seed.test',
|
|
})
|
|
expect(draft.id).toBe(39)
|
|
expect(draft.iri).toBe('/api/supplier_contacts/39')
|
|
expect(draft.phonePrimary).toBe('06 12 34 56 78')
|
|
expect(draft.hasSecondaryPhone).toBe(false)
|
|
})
|
|
|
|
it('revele le 2e telephone quand phoneSecondary est present', () => {
|
|
const draft = mapContactToDraft({
|
|
'@id': '/api/supplier_contacts/40',
|
|
id: 40,
|
|
phonePrimary: '0600000000',
|
|
phoneSecondary: '0611111111',
|
|
})
|
|
expect(draft.hasSecondaryPhone).toBe(true)
|
|
expect(draft.phoneSecondary).toBe('06 11 11 11 11')
|
|
})
|
|
})
|
|
|
|
describe('mapAddressToDraft', () => {
|
|
it('mappe l\'enum addressType, les champs fournisseur et extrait les iris', () => {
|
|
const draft = mapAddressToDraft({
|
|
'@id': '/api/supplier_addresses/33',
|
|
id: 33,
|
|
addressType: 'DEPART',
|
|
country: 'France',
|
|
postalCode: '86000',
|
|
city: 'Poitiers',
|
|
street: '12 rue des Acacias',
|
|
bennes: 3,
|
|
triageProvider: true,
|
|
sites: [{ '@id': '/api/sites/87', name: 'Chatellerault', color: '#056CF2' }],
|
|
categories: [{ '@id': '/api/categories/2279', code: 'NEGOCIANT' }],
|
|
contacts: [{ '@id': '/api/supplier_contacts/39' }, '/api/supplier_contacts/41'],
|
|
})
|
|
expect(draft.addressType).toBe('DEPART')
|
|
expect(draft.siteIris).toEqual(['/api/sites/87'])
|
|
expect(draft.categoryIris).toEqual(['/api/categories/2279'])
|
|
expect(draft.contactIris).toEqual(['/api/supplier_contacts/39', '/api/supplier_contacts/41'])
|
|
// bennes (entier) → chaine pour MalioInputNumber.
|
|
expect(draft.bennes).toBe('3')
|
|
expect(draft.triageProvider).toBe(true)
|
|
expect(draft.city).toBe('Poitiers')
|
|
expect(draft.country).toBe('France')
|
|
})
|
|
|
|
it('tolere les champs absents (defauts : France, bennes « 0 », triage faux, type null)', () => {
|
|
const draft = mapAddressToDraft({ '@id': '/api/supplier_addresses/9', id: 9 })
|
|
expect(draft.addressType).toBeNull()
|
|
expect(draft.siteIris).toEqual([])
|
|
expect(draft.categoryIris).toEqual([])
|
|
expect(draft.contactIris).toEqual([])
|
|
expect(draft.country).toBe('France')
|
|
expect(draft.bennes).toBe('0')
|
|
expect(draft.triageProvider).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('mapRibToDraft', () => {
|
|
it('mappe label / bic / iban et l\'id serveur', () => {
|
|
const draft = mapRibToDraft({ '@id': '/api/supplier_ribs/27', id: 27, label: 'Compte principal', bic: 'BNPAFRPPXXX', iban: 'FR14...' })
|
|
expect(draft).toEqual({ id: 27, label: 'Compte principal', bic: 'BNPAFRPPXXX', iban: 'FR14...' })
|
|
})
|
|
})
|
|
|
|
describe('mapAccountingDraft', () => {
|
|
it('mappe les scalaires et resout les iris des referentiels embarques', () => {
|
|
const acc = mapAccountingDraft({
|
|
'@id': '/api/suppliers/85',
|
|
id: 85,
|
|
siren: '123456789',
|
|
accountNumber: 'F0001',
|
|
nTva: 'FR00123456789',
|
|
tvaMode: { '@id': '/api/tva_modes/30' },
|
|
paymentDelay: { '@id': '/api/payment_delays/11' },
|
|
paymentType: { '@id': '/api/payment_types/14', code: 'LCR' },
|
|
bank: { '@id': '/api/banks/3' },
|
|
} as SupplierDetail)
|
|
expect(acc).toEqual({
|
|
siren: '123456789',
|
|
accountNumber: 'F0001',
|
|
nTva: 'FR00123456789',
|
|
tvaModeIri: '/api/tva_modes/30',
|
|
paymentDelayIri: '/api/payment_delays/11',
|
|
paymentTypeIri: '/api/payment_types/14',
|
|
bankIri: '/api/banks/3',
|
|
})
|
|
})
|
|
|
|
it('renvoie des null quand les champs comptables sont absents (gating par omission, sans accounting.view)', () => {
|
|
const acc = mapAccountingDraft({} as SupplierDetail)
|
|
expect(acc).toEqual({
|
|
siren: null,
|
|
accountNumber: null,
|
|
nTva: null,
|
|
tvaModeIri: null,
|
|
paymentDelayIri: null,
|
|
paymentTypeIri: null,
|
|
bankIri: null,
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('options construites depuis l\'embed (role-independantes)', () => {
|
|
it('categoryOptionsOf expose value=IRI, label=nom, code', () => {
|
|
expect(categoryOptionsOf([{ '@id': '/api/categories/2279', name: 'Negociant', code: 'NEGOCIANT' }])).toEqual([
|
|
{ value: '/api/categories/2279', label: 'Negociant', code: 'NEGOCIANT' },
|
|
])
|
|
})
|
|
|
|
it('siteOptionsOf expose value=IRI, label=nom', () => {
|
|
expect(siteOptionsOf([{ '@id': '/api/sites/87', name: 'Chatellerault', color: '#000' }])).toEqual([
|
|
{ value: '/api/sites/87', label: 'Chatellerault' },
|
|
])
|
|
})
|
|
|
|
it('contactOptionsOf compose le libelle (nom complet, sinon email)', () => {
|
|
expect(contactOptionsOf([
|
|
{ '@id': '/api/supplier_contacts/1', id: 1, firstName: 'Marie', lastName: 'Martin' },
|
|
{ '@id': '/api/supplier_contacts/2', id: 2, email: 'a@b.fr' },
|
|
])).toEqual([
|
|
{ value: '/api/supplier_contacts/1', label: 'Marie Martin' },
|
|
{ value: '/api/supplier_contacts/2', label: 'a@b.fr' },
|
|
])
|
|
})
|
|
|
|
it('referentialOptionOf : option unique depuis l\'embed, vide pour IRI nu / absent', () => {
|
|
expect(referentialOptionOf({ '@id': '/api/payment_types/14', label: 'LCR' })).toEqual([
|
|
{ value: '/api/payment_types/14', label: 'LCR' },
|
|
])
|
|
expect(referentialOptionOf('/api/banks/3')).toEqual([])
|
|
expect(referentialOptionOf(null)).toEqual([])
|
|
})
|
|
|
|
it('mapAddressView assemble brouillon + options propres a l\'adresse', () => {
|
|
const view = mapAddressView({
|
|
'@id': '/api/supplier_addresses/33',
|
|
id: 33,
|
|
addressType: 'RENDU',
|
|
city: 'Poitiers',
|
|
sites: [{ '@id': '/api/sites/87', name: 'Chatellerault' }],
|
|
categories: [{ '@id': '/api/categories/2279', name: 'Negociant', code: 'NEGOCIANT' }],
|
|
})
|
|
expect(view.draft.id).toBe(33)
|
|
expect(view.draft.addressType).toBe('RENDU')
|
|
expect(view.siteOptions).toEqual([{ value: '/api/sites/87', label: 'Chatellerault' }])
|
|
expect(view.categoryOptions).toEqual([{ value: '/api/categories/2279', label: 'Negociant', code: 'NEGOCIANT' }])
|
|
})
|
|
})
|
|
|
|
describe('canEditSupplier', () => {
|
|
const can = (granted: string[]) => (codes: string[]) => codes.some(c => granted.includes(c))
|
|
|
|
it('visible pour manage', () => {
|
|
expect(canEditSupplier(can(['commercial.suppliers.manage']))).toBe(true)
|
|
})
|
|
|
|
it('visible pour accounting.manage (role Compta)', () => {
|
|
expect(canEditSupplier(can(['commercial.suppliers.accounting.manage']))).toBe(true)
|
|
})
|
|
|
|
it('masque sans aucune des deux permissions (role Usine)', () => {
|
|
expect(canEditSupplier(can(['commercial.suppliers.view']))).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('showArchiveAction / showRestoreAction', () => {
|
|
const can = (granted: string[]) => (code: string) => granted.includes(code)
|
|
|
|
it('Archiver : visible avec la permission archive ET fournisseur non archive', () => {
|
|
expect(showArchiveAction(can(['commercial.suppliers.archive']), false)).toBe(true)
|
|
expect(showArchiveAction(can(['commercial.suppliers.archive']), true)).toBe(false)
|
|
expect(showArchiveAction(can([]), false)).toBe(false)
|
|
})
|
|
|
|
it('Restaurer : visible avec la permission archive ET fournisseur archive', () => {
|
|
expect(showRestoreAction(can(['commercial.suppliers.archive']), true)).toBe(true)
|
|
expect(showRestoreAction(can(['commercial.suppliers.archive']), false)).toBe(false)
|
|
expect(showRestoreAction(can([]), true)).toBe(false)
|
|
})
|
|
})
|