diff --git a/frontend/i18n/locales/fr.json b/frontend/i18n/locales/fr.json index d0acd43..d2f1c9d 100644 --- a/frontend/i18n/locales/fr.json +++ b/frontend/i18n/locales/fr.json @@ -573,7 +573,8 @@ } }, "errors": { - "nameRequired": "Le nom du transporteur est obligatoire." + "nameRequired": "Le nom du transporteur est obligatoire.", + "certificationRequired": "Le type de certification est obligatoire." } } } diff --git a/frontend/modules/transport/composables/__tests__/useCarrierForm.test.ts b/frontend/modules/transport/composables/__tests__/useCarrierForm.test.ts index c49816d..b6e123a 100644 --- a/frontend/modules/transport/composables/__tests__/useCarrierForm.test.ts +++ b/frontend/modules/transport/composables/__tests__/useCarrierForm.test.ts @@ -65,6 +65,30 @@ describe('useCarrierForm', () => { expect(form.mainErrors.errors.name).toBe('transport.carriers.form.errors.nameRequired') }) + it('front : certification vide (hors LIOT) → erreur inline sur certificationType, pas de POST', async () => { + const form = useCarrierForm() + form.main.name = 'Acme' + // certificationType laissé null → bloqué côté front (RG-4.01). + + const created = await form.submitMain() + + expect(created).toBe(false) + expect(mockPost).not.toHaveBeenCalled() + expect(form.mainErrors.errors.certificationType).toBe('transport.carriers.form.errors.certificationRequired') + }) + + it('front : cas LIOT → certification non requise (aucune erreur de certification)', async () => { + mockPost.mockResolvedValueOnce({ id: 5, name: 'LIOT', certificationType: null }) + const form = useCarrierForm() + form.main.name = 'LIOT' + form.main.liotPlates = 'AA-123-BB' + + const created = await form.submitMain() + + expect(created).toBe(true) + expect(form.mainErrors.errors.certificationType).toBeUndefined() + }) + it('POST /carriers avec Accept ld+json, verrouille et bascule sur Qualimat', async () => { mockPost.mockResolvedValueOnce({ id: 42, name: 'TRANSPORTS ACME', certificationType: 'GMP_PLUS' }) const form = useCarrierForm() @@ -93,15 +117,11 @@ describe('useCarrierForm', () => { expect(form.unlockedIndex.value).toBe(0) }) - it('payload : omet name et certificationType vides, garde isChartered', async () => { - mockPost.mockRejectedValueOnce({ response: { status: 422, _data: { violations: [] } } }) + it('buildMainPayload : omet certificationType vide, garde isChartered', () => { const form = useCarrierForm() - form.main.name = 'X' // nom présent pour passer le pré-check front - // certificationType laissé null → omis pour que la 422 « obligatoire » porte. + form.main.name = 'X' - await form.submitMain() - - const body = mockPost.mock.calls[0]?.[1] as Record + const body = form.buildMainPayload() expect(body).toEqual({ name: 'X', isChartered: false }) expect('certificationType' in body).toBe(false) }) @@ -119,20 +139,24 @@ describe('useCarrierForm', () => { expect(form.mainLocked.value).toBe(false) }) - it('422 : mappe les violations serveur inline par champ', async () => { + it('422 : mappe les violations serveur inline par champ (RG conditionnelle back)', async () => { + // Champ re-validé côté back (RG-4.03) : le pré-check front laisse passer le POST, + // la 422 mappe inline sur le champ via son propertyPath. mockPost.mockRejectedValueOnce({ response: { status: 422, - _data: { violations: [{ propertyPath: 'certificationType', message: 'Le type de certification est obligatoire.' }] }, + _data: { violations: [{ propertyPath: 'indexationRate', message: "Le taux d'indexation est obligatoire pour un transporteur affrété." }] }, }, }) const form = useCarrierForm() - form.main.name = 'Sans Certif' + form.main.name = 'Acme' + form.main.certificationType = 'GMP_PLUS' + form.main.isChartered = true const created = await form.submitMain() expect(created).toBe(false) - expect(form.mainErrors.errors.certificationType).toBe('Le type de certification est obligatoire.') + expect(form.mainErrors.errors.indexationRate).toBe("Le taux d'indexation est obligatoire pour un transporteur affrété.") expect(form.mainLocked.value).toBe(false) }) diff --git a/frontend/modules/transport/composables/useCarrierForm.ts b/frontend/modules/transport/composables/useCarrierForm.ts index dba641f..6864d9e 100644 --- a/frontend/modules/transport/composables/useCarrierForm.ts +++ b/frontend/modules/transport/composables/useCarrierForm.ts @@ -95,10 +95,9 @@ export function useCarrierForm() { /** * Validation FRONT du formulaire principal : seul le nom est requis côté front - * (RG-4.01). Le back reste la couche autoritaire (ERP-101) — la certification - * obligatoire (sauf LIOT) et les RG conditionnelles sont re-validées serveur et - * remontées en 422 inline, sans pré-check front (qui devrait connaître le cas - * LIOT, hors périmètre ERP-165). + * (RG-4.01) : nom requis, et certification requise hors cas LIOT (où elle est + * masquée). Le back reste la couche autoritaire (ERP-101) — les RG conditionnelles + * (affrètement, décharge AUTRE) sont re-validées serveur et remontées en 422 inline. */ function validateMainFront(): boolean { let valid = true @@ -106,6 +105,11 @@ export function useCarrierForm() { mainErrors.setError('name', t('transport.carriers.form.errors.nameRequired')) valid = false } + // RG-4.01 : la certification est obligatoire SAUF en cas LIOT (champ masqué). + if (!isLiot.value && !main.certificationType) { + mainErrors.setError('certificationType', t('transport.carriers.form.errors.certificationRequired')) + valid = false + } return valid }