diff --git a/frontend/modules/catalog/composables/__tests__/useProductForm.spec.ts b/frontend/modules/catalog/composables/__tests__/useProductForm.spec.ts index 2c0b24a..9294f47 100644 --- a/frontend/modules/catalog/composables/__tests__/useProductForm.spec.ts +++ b/frontend/modules/catalog/composables/__tests__/useProductForm.spec.ts @@ -171,6 +171,20 @@ describe('useProductForm', () => { expect(payload.containsMolasses).toBe(false) }) + it('omet `category` du payload quand aucune categorie n\'est choisie', async () => { + // Envoyer category:null casserait la denormalisation back (type IRI + // attendu) et court-circuiterait les autres violations -> on l'omet. + mockPost.mockResolvedValueOnce({ id: 40 }) + const { form, submit } = useProductForm() + fillValidForm(form) + form.categoryIri = null + + await submit() + + const payload = mockPost.mock.calls[0][1] + expect(payload).not.toHaveProperty('category') + }) + it('mappe un 409 doublon de code sur errors.code + toast explicite', async () => { mockPost.mockRejectedValueOnce({ response: { status: 409, _data: {} } }) const { form, errors, submit } = useProductForm() diff --git a/frontend/modules/catalog/composables/useProductForm.ts b/frontend/modules/catalog/composables/useProductForm.ts index 094fea5..0d6237b 100644 --- a/frontend/modules/catalog/composables/useProductForm.ts +++ b/frontend/modules/catalog/composables/useProductForm.ts @@ -126,7 +126,7 @@ export function useProductForm() { formErrors.clearErrors() const editing = productId.value !== null try { - const payload = { + const payload: Record = { code: form.code || null, name: form.name || null, states: form.states, @@ -134,10 +134,17 @@ export function useProductForm() { // re-force, on garde le payload coherent). manufactured: isSale.value ? form.manufactured : false, containsMolasses: isSale.value ? form.containsMolasses : false, - category: form.categoryIri, sites: form.siteIris, storageTypes: form.storageTypeIris, } + // `category` attend un IRI (string) : envoyer null declencherait une + // erreur de denormalisation API Platform qui court-circuiterait TOUTES + // les autres violations. On omet la cle quand aucune categorie n'est + // choisie -> la contrainte NotNull renvoie un message propre, et les + // autres champs sont valides dans la meme 422 (mapping inline ERP-101). + if (form.categoryIri) { + payload.category = form.categoryIri + } const options = { headers: { Accept: 'application/ld+json' }, toast: false } if (editing) { await api.patch(`/products/${productId.value}`, payload, options) diff --git a/src/Module/Catalog/Domain/Entity/Product.php b/src/Module/Catalog/Domain/Entity/Product.php index 75605e7..7cc774b 100644 --- a/src/Module/Catalog/Domain/Entity/Product.php +++ b/src/Module/Catalog/Domain/Entity/Product.php @@ -81,12 +81,18 @@ use function in_array; security: "is_granted('catalog.products.manage')", normalizationContext: ['groups' => ['product:read', 'product:item:read', 'category:read', 'site:read', 'storage_type:read', 'default:read']], denormalizationContext: ['groups' => ['product:write']], + // Convertit les erreurs de denormalisation (type invalide / null sur une + // relation : category, sites, storageTypes) en violations 422 portant un + // propertyPath, au lieu d'un 400 qui court-circuite toute la validation + // (cf. Client/Supplier/WeighingTicket — mapping inline useFormErrors). + collectDenormalizationErrors: true, processor: ProductProcessor::class, ), new Patch( security: "is_granted('catalog.products.manage')", normalizationContext: ['groups' => ['product:read', 'product:item:read', 'category:read', 'site:read', 'storage_type:read', 'default:read']], denormalizationContext: ['groups' => ['product:write']], + collectDenormalizationErrors: true, provider: ProductProvider::class, processor: ProductProcessor::class, ),