a442d124a3
Auto Tag Develop / tag (push) Successful in 11s
## Contexte — ERP-121 Le passage d'un tiers de **LCR** vers **virement** (ou autre) supprimait ses RIB en base : au changement de type de règlement, le front marquait les `ClientRib` / `SupplierRib` existants pour suppression puis envoyait des `DELETE`. Le métier veut **conserver** le RIB (coordonnée bancaire du tiers, découplée du mode de règlement) pour un éventuel retour en LCR. ## Décisions métier (validées) 1. **Affichage hors-LCR** : RIB **totalement masqué**, ré-affiché au retour LCR — jamais supprimé en base. 2. **RGPD / IBAN** : conservation telle quelle, hors-scope de ce ticket. 3. **Données déjà perdues** : acceptable, le fix ne vaut que pour l'avenir. ## Modifications (100% frontend — clients **et** fournisseurs) - `new.vue` / `[id]/edit.vue` : `onPaymentTypeChange` ne marque plus les RIB pour suppression et ne jette plus la saisie ; ils sont seulement masqués (`visibleRibs`) et réapparaissent au retour LCR. - `submitAccounting` ne (re)soumet les RIB que **sous LCR** ; seules les suppressions **explicites** (corbeille d'un bloc) restent en `DELETE`. - Consultation `[id]/index.vue` : RIB dormants masqués hors-LCR via le helper pur type-safe `paymentTypeCodeOf` (+ tests Vitest). ## Back **Aucune modification** : la seule règle est `LCR → ≥1 RIB` (RG-1.13 / RG-2.08) ; rien n'interdit un RIB sur un tiers non-LCR. Le guard `Client/SupplierRibProcessor` (refus de supprimer le dernier RIB sous LCR) reste inchangé. **Pas de migration.** ## Vérifications - ✅ Vitest : **384/384** (`make nuxt-test`) - ✅ ESLint : clean sur les 10 fichiers - ⏭️ PHPUnit non lancé : aucun fichier back modifié Reviewed-on: #86 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
251 lines
10 KiB
TypeScript
251 lines
10 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
canEditClient,
|
|
categoryOptionsOf,
|
|
contactOptionsOf,
|
|
iriOf,
|
|
mapAccountingDraft,
|
|
mapAddressToDraft,
|
|
mapAddressView,
|
|
mapContactToDraft,
|
|
mapRibToDraft,
|
|
paymentTypeCodeOf,
|
|
referentialOptionOf,
|
|
relationOf,
|
|
showArchiveAction,
|
|
showRestoreAction,
|
|
siteOptionsOf,
|
|
type ClientDetail,
|
|
} from '../clientConsultation'
|
|
|
|
describe('iriOf', () => {
|
|
it('retourne l\'@id d\'une relation embarquee (objet)', () => {
|
|
expect(iriOf({ '@id': '/api/payment_types/10', code: 'LCR' })).toBe('/api/payment_types/10')
|
|
})
|
|
|
|
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('relationOf', () => {
|
|
it('detecte une relation distributeur et expose son nom', () => {
|
|
const client = { distributor: { '@id': '/api/clients/15', companyName: 'DISTRIB GRAND SUD-OUEST' } } as ClientDetail
|
|
expect(relationOf(client)).toEqual({ type: 'distributeur', name: 'DISTRIB GRAND SUD-OUEST' })
|
|
})
|
|
|
|
it('detecte une relation courtier et expose son nom', () => {
|
|
const client = { broker: { '@id': '/api/clients/16', companyName: 'CABINET LEONARD' } } as ClientDetail
|
|
expect(relationOf(client)).toEqual({ type: 'courtier', name: 'CABINET LEONARD' })
|
|
})
|
|
|
|
it('retourne type null quand aucune relation n\'est posee (cles omises)', () => {
|
|
expect(relationOf({} as ClientDetail)).toEqual({ type: null, name: null })
|
|
})
|
|
})
|
|
|
|
describe('mapContactToDraft', () => {
|
|
it('formate les telephones en XX XX XX XX XX et conserve l\'iri', () => {
|
|
const draft = mapContactToDraft({
|
|
'@id': '/api/client_contacts/18',
|
|
id: 18,
|
|
firstName: 'Sophie',
|
|
lastName: 'Léonard',
|
|
jobTitle: 'Gérante',
|
|
phonePrimary: '0549112233',
|
|
email: 'sophie@x.fr',
|
|
})
|
|
expect(draft.id).toBe(18)
|
|
expect(draft.iri).toBe('/api/client_contacts/18')
|
|
expect(draft.phonePrimary).toBe('05 49 11 22 33')
|
|
expect(draft.hasSecondaryPhone).toBe(false)
|
|
})
|
|
|
|
it('revele le 2e telephone quand phoneSecondary est present', () => {
|
|
const draft = mapContactToDraft({
|
|
'@id': '/api/client_contacts/19',
|
|
id: 19,
|
|
phonePrimary: '0600000000',
|
|
phoneSecondary: '0611111111',
|
|
})
|
|
expect(draft.hasSecondaryPhone).toBe(true)
|
|
expect(draft.phoneSecondary).toBe('06 11 11 11 11')
|
|
})
|
|
})
|
|
|
|
describe('mapAddressToDraft', () => {
|
|
it('extrait les iris de sites / categories / contacts (objets ou chaines)', () => {
|
|
const draft = mapAddressToDraft({
|
|
'@id': '/api/client_addresses/18',
|
|
id: 18,
|
|
country: 'France',
|
|
postalCode: '86100',
|
|
city: 'Châtellerault',
|
|
street: '5 rue des Courtiers',
|
|
billingEmail: 'factures@x.fr',
|
|
isProspect: false,
|
|
isDelivery: false,
|
|
isBilling: true,
|
|
sites: [{ '@id': '/api/sites/4', name: 'Chatellerault', color: '#056CF2' }],
|
|
categories: [{ '@id': '/api/categories/3', code: 'SECTEUR' }],
|
|
contacts: [{ '@id': '/api/client_contacts/18' }, '/api/client_contacts/20'],
|
|
})
|
|
expect(draft.siteIris).toEqual(['/api/sites/4'])
|
|
expect(draft.categoryIris).toEqual(['/api/categories/3'])
|
|
expect(draft.contactIris).toEqual(['/api/client_contacts/18', '/api/client_contacts/20'])
|
|
expect(draft.isBilling).toBe(true)
|
|
expect(draft.city).toBe('Châtellerault')
|
|
expect(draft.country).toBe('France')
|
|
})
|
|
|
|
it('tolere les sous-collections absentes (defaut tableau vide, pays France)', () => {
|
|
const draft = mapAddressToDraft({ '@id': '/api/client_addresses/9', id: 9 })
|
|
expect(draft.siteIris).toEqual([])
|
|
expect(draft.categoryIris).toEqual([])
|
|
expect(draft.contactIris).toEqual([])
|
|
expect(draft.country).toBe('France')
|
|
expect(draft.isBilling).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('mapRibToDraft', () => {
|
|
it('mappe label / bic / iban et l\'id serveur', () => {
|
|
const draft = mapRibToDraft({ '@id': '/api/client_ribs/3', id: 3, label: 'Compte', bic: 'BNPAFRPPXXX', iban: 'FR14...' })
|
|
expect(draft).toEqual({ id: 3, label: 'Compte', bic: 'BNPAFRPPXXX', iban: 'FR14...' })
|
|
})
|
|
})
|
|
|
|
describe('mapAccountingDraft', () => {
|
|
it('mappe les scalaires et resout les iris des referentiels embarques', () => {
|
|
const acc = mapAccountingDraft({
|
|
'@id': '/api/clients/1',
|
|
id: 1,
|
|
siren: '123456789',
|
|
accountNumber: '411000',
|
|
nTva: 'FR123',
|
|
tvaMode: { '@id': '/api/tva_modes/1' },
|
|
paymentDelay: { '@id': '/api/payment_delays/2' },
|
|
paymentType: { '@id': '/api/payment_types/10', code: 'LCR' },
|
|
bank: { '@id': '/api/banks/3' },
|
|
} as ClientDetail)
|
|
expect(acc).toEqual({
|
|
siren: '123456789',
|
|
accountNumber: '411000',
|
|
nTva: 'FR123',
|
|
tvaModeIri: '/api/tva_modes/1',
|
|
paymentDelayIri: '/api/payment_delays/2',
|
|
paymentTypeIri: '/api/payment_types/10',
|
|
bankIri: '/api/banks/3',
|
|
})
|
|
})
|
|
|
|
it('renvoie des null quand les champs comptables sont absents (sans accounting.view)', () => {
|
|
const acc = mapAccountingDraft({} as ClientDetail)
|
|
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/3', name: 'Secteur', code: 'SECTEUR' }])).toEqual([
|
|
{ value: '/api/categories/3', label: 'Secteur', code: 'SECTEUR' },
|
|
])
|
|
})
|
|
|
|
it('siteOptionsOf expose value=IRI, label=nom', () => {
|
|
expect(siteOptionsOf([{ '@id': '/api/sites/4', name: 'Chatellerault', color: '#000' }])).toEqual([
|
|
{ value: '/api/sites/4', label: 'Chatellerault' },
|
|
])
|
|
})
|
|
|
|
it('contactOptionsOf compose le libelle (nom complet, sinon email)', () => {
|
|
expect(contactOptionsOf([
|
|
{ '@id': '/api/client_contacts/1', id: 1, firstName: 'Jean', lastName: 'Dupont' },
|
|
{ '@id': '/api/client_contacts/2', id: 2, email: 'a@b.fr' },
|
|
])).toEqual([
|
|
{ value: '/api/client_contacts/1', label: 'Jean Dupont' },
|
|
{ value: '/api/client_contacts/2', label: 'a@b.fr' },
|
|
])
|
|
})
|
|
|
|
it('referentialOptionOf : option unique depuis l\'embed, vide pour IRI nu / absent', () => {
|
|
expect(referentialOptionOf({ '@id': '/api/payment_types/10', label: 'LCR' })).toEqual([
|
|
{ value: '/api/payment_types/10', 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/client_addresses/18',
|
|
id: 18,
|
|
city: 'Châtellerault',
|
|
sites: [{ '@id': '/api/sites/4', name: 'Chatellerault' }],
|
|
categories: [{ '@id': '/api/categories/3', name: 'Secteur', code: 'SECTEUR' }],
|
|
})
|
|
expect(view.draft.id).toBe(18)
|
|
expect(view.siteOptions).toEqual([{ value: '/api/sites/4', label: 'Chatellerault' }])
|
|
expect(view.categoryOptions).toEqual([{ value: '/api/categories/3', label: 'Secteur', code: 'SECTEUR' }])
|
|
})
|
|
})
|
|
|
|
describe('canEditClient', () => {
|
|
const can = (granted: string[]) => (codes: string[]) => codes.some(c => granted.includes(c))
|
|
|
|
it('visible pour manage', () => {
|
|
expect(canEditClient(can(['commercial.clients.manage']))).toBe(true)
|
|
})
|
|
|
|
it('visible pour accounting.manage (role Compta)', () => {
|
|
expect(canEditClient(can(['commercial.clients.accounting.manage']))).toBe(true)
|
|
})
|
|
|
|
it('masque sans aucune des deux permissions (role Usine)', () => {
|
|
expect(canEditClient(can(['commercial.clients.view']))).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('showArchiveAction / showRestoreAction', () => {
|
|
const can = (granted: string[]) => (code: string) => granted.includes(code)
|
|
|
|
it('Archiver : visible avec la permission archive ET client non archive', () => {
|
|
expect(showArchiveAction(can(['commercial.clients.archive']), false)).toBe(true)
|
|
expect(showArchiveAction(can(['commercial.clients.archive']), true)).toBe(false)
|
|
expect(showArchiveAction(can([]), false)).toBe(false)
|
|
})
|
|
|
|
it('Restaurer : visible avec la permission archive ET client archive', () => {
|
|
expect(showRestoreAction(can(['commercial.clients.archive']), true)).toBe(true)
|
|
expect(showRestoreAction(can(['commercial.clients.archive']), false)).toBe(false)
|
|
expect(showRestoreAction(can([]), true)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('paymentTypeCodeOf (ERP-121 : RIB masques hors-LCR en consultation)', () => {
|
|
it('retourne le code metier quand le type de reglement est embarque', () => {
|
|
expect(paymentTypeCodeOf({ '@id': '/api/payment_types/1', code: 'LCR' })).toBe('LCR')
|
|
expect(paymentTypeCodeOf({ '@id': '/api/payment_types/2', code: 'VIREMENT' })).toBe('VIREMENT')
|
|
})
|
|
|
|
it('retourne null pour un IRI nu, un objet sans code, ou une relation absente', () => {
|
|
expect(paymentTypeCodeOf('/api/payment_types/1')).toBeNull()
|
|
expect(paymentTypeCodeOf({ '@id': '/api/payment_types/1' })).toBeNull()
|
|
expect(paymentTypeCodeOf(null)).toBeNull()
|
|
expect(paymentTypeCodeOf(undefined)).toBeNull()
|
|
})
|
|
})
|