Files
Starseed/frontend/modules/commercial/utils/__tests__/supplierEdit.spec.ts
T
tristan d6790dd37d
Auto Tag Develop / tag (push) Successful in 7s
feat(front) : page Ajouter un fournisseur (/suppliers/new) + workflow par onglets (ERP-94) (#83)
ERP-94 (etape front 7/7 du M2). **Stack sur #97** (base = `feature/ERP-97-suppliers-i18n-sidebar`, elle-meme sur #93) pour un diff isole. A recibler sur `develop` une fois #93 (MR #81) et #97 (MR #82) mergees.

Page « Ajouter un fournisseur » — **replique a l'identique le fonctionnement de l'ecran Client** (workflow inline par onglets, blocs reutilisables, validation 422 inline ERP-101), avec les specificites M2.

## Architecture (miroir Client)
- Workflow par onglets **inline dans `suppliers/new.vue`** (comme `clients/new.vue` — il n'existe pas de `useClientForm` monolithique). Helpers paralleles : `useSupplierReferentials`, `useSupplierFormErrors`, `supplierFormRules`, `supplierEdit` (payloads), `types/supplierForm`.
- Blocs `SupplierContactBlock` / `SupplierAddressBlock` (miroir des blocs Client).
- POST `/suppliers` puis PATCH partiels par onglet (mode strict, groupes de serialisation). Sous-ressources : `/suppliers/{id}/contacts|addresses|ribs`.
- Validation ERP-101 : 422 `violations[].propertyPath` mappees inline par champ (`useFormErrors` / `mapViolationsToRecord`), `{ toast: false }`, bouton Valider toujours actif.

## Specificites M2 (vs M1)
- Formulaire principal **sans contact inline** (ERP-106) : Entreprise + Categorie (type FOURNISSEUR, `?typeCode=FOURNISSEUR`).
- Adresse : **radio exclusif** Prospect/Depart/Rendu (`addressType` enum, RG-2.09), champs **Bennes** (stepper) + **Prestation de triage**, **pas d'email de facturation**.
- Information : champ **Volume previsionnel** (8e champ).
- Compta (Admin+Compta) : banque si VIREMENT (RG-2.07), RIB si LCR (RG-2.08) ; RIB sous-ressource gardee par `accounting.manage`.

## Tests (mirroir strategie Client)
- `make nuxt-test` : 338 passed (specs ajoutees : supplierFormRules, supplierEdit, useSupplierReferentials, SupplierContactBlock, SupplierAddressBlock).
- ESLint propre ; `nuxi typecheck` (lance en container) : **0 erreur**.
- Golden path navigateur valide end-to-end : POST /suppliers OK, companyName normalise UPPERCASE (RG-2.12), gating des onglets (Information actif, Contacts deverrouille).

## Note de revue
~30 `WARN Duplicated imports` au typecheck : les helpers Supplier exportent les memes noms generiques que leurs equivalents Client (`buildMainPayload`, `omitEmptyRequired`, `RefOption`...), tous deux auto-importes par Nuxt. **Sans impact runtime** : tous les consommateurs utilisent des imports explicites (qui priment). Consequence directe du miroir 1:1 ; une factorisation des generiques dans `shared/` pourrait etre un suivi.

Reviewed-on: #83
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-06-11 07:14:51 +00:00

116 lines
4.9 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import {
buildAccountingPayload,
buildAddressPayload,
buildContactPayload,
buildInformationPayload,
buildMainPayload,
buildRibPayload,
} from '../supplierEdit'
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')
})
})