fix(transport) : bloc prix par défaut (CLIENT), sens seul en ligne 1, payload omet scalaires vides (422 inline au lieu de 400) (ERP-169)
This commit is contained in:
@@ -10,9 +10,10 @@
|
||||
@click="$emit('remove')"
|
||||
/>
|
||||
|
||||
<!-- RG-4.09 : sens du prix (CLIENT / FOURNISSEUR), empilé en colonne 1 (pas de
|
||||
label de groupe). Tout le reste masqué tant qu'aucun sens n'est choisi. -->
|
||||
<div class="flex flex-col justify-center gap-2">
|
||||
<!-- RG-4.09 : sens du prix (CLIENT / FOURNISSEUR), seul sur la ligne 1, empilé
|
||||
en colonne 1 (pas de label de groupe). Les champs de branche démarrent
|
||||
donc en ligne 2. col-span-4 = la ligne 1 ne porte que ce bloc. -->
|
||||
<div class="col-span-4 flex flex-col gap-2">
|
||||
<MalioRadioButton
|
||||
:model-value="model.direction"
|
||||
name="price-direction"
|
||||
|
||||
@@ -744,6 +744,8 @@ describe('carrierPrice (util) — bascule CLIENT/FOURNISSEUR + champs requis par
|
||||
it('isCarrierPriceValid : faux si branche incomplète, vrai si branche complète + communs', () => {
|
||||
const base = { ...emptyCarrierPrice(), containerType: 'BENNE', pricingUnit: 'FORFAIT', price: '10', priceState: 'EN_COURS' }
|
||||
// Direction non choisie → invalide.
|
||||
expect(isCarrierPriceValid({ ...base, direction: null })).toBe(false)
|
||||
// Sens CLIENT par défaut mais branche incomplète → invalide.
|
||||
expect(isCarrierPriceValid(base)).toBe(false)
|
||||
// CLIENT sans adresse/site → invalide.
|
||||
expect(isCarrierPriceValid({ ...base, direction: 'CLIENT', clientIri: CLIENT })).toBe(false)
|
||||
@@ -769,17 +771,28 @@ describe('useCarrierForm — onglet Prix (ERP-169)', () => {
|
||||
return form
|
||||
}
|
||||
|
||||
it('démarre vide ; « + Nouveau prix » autorisé sur liste vide, bloqué si dernier bloc incomplet', () => {
|
||||
it('démarre avec un bloc CLIENT par défaut ; « + Nouveau prix » bloqué tant qu\'il est incomplet', () => {
|
||||
const form = createdForm()
|
||||
expect(form.prices.value).toHaveLength(0)
|
||||
expect(form.canAddPrice.value).toBe(true)
|
||||
|
||||
form.addPrice()
|
||||
// Un bloc présent d'office, sens CLIENT pré-sélectionné.
|
||||
expect(form.prices.value).toHaveLength(1)
|
||||
// Bloc vide → on ne peut pas en ajouter un autre.
|
||||
expect(form.prices.value[0]?.direction).toBe('CLIENT')
|
||||
// Bloc incomplet → on ne peut pas en ajouter un autre.
|
||||
expect(form.canAddPrice.value).toBe(false)
|
||||
form.addPrice()
|
||||
expect(form.prices.value).toHaveLength(1)
|
||||
|
||||
// Une fois le bloc complété, l'ajout est autorisé.
|
||||
const p = form.prices.value[0]
|
||||
if (p) {
|
||||
p.clientIri = '/api/clients/3'
|
||||
p.clientDeliveryAddressIri = '/api/client_addresses/8'
|
||||
p.departureSiteIri = '/api/sites/1'
|
||||
p.price = '120'
|
||||
p.priceState = 'EN_COURS'
|
||||
}
|
||||
expect(form.canAddPrice.value).toBe(true)
|
||||
form.addPrice()
|
||||
expect(form.prices.value).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('submitPrices : POST des nouveaux prix (branche CLIENT), capture l\'id, finalise', async () => {
|
||||
|
||||
@@ -469,8 +469,9 @@ export function useCarrierForm() {
|
||||
}
|
||||
|
||||
// ── Onglet Prix (ERP-169) ─────────────────────────────────────────────────
|
||||
// Démarre VIDE : aucun bloc auto, l'utilisateur ajoute via « + Nouveau prix ».
|
||||
const prices = ref<CarrierPriceFormDraft[]>([])
|
||||
// Un bloc présent par défaut (sens CLIENT pré-sélectionné). L'utilisateur ajoute
|
||||
// les suivants via « + Nouveau prix ».
|
||||
const prices = ref<CarrierPriceFormDraft[]>([emptyCarrierPrice()])
|
||||
// Erreurs 422 par ligne (alignées sur l'index du v-for), peuplées par submitRows.
|
||||
const priceErrors = ref<Record<string, string>[]>([])
|
||||
|
||||
|
||||
@@ -157,7 +157,9 @@ export interface CarrierPriceFormDraft {
|
||||
export function emptyCarrierPrice(): CarrierPriceFormDraft {
|
||||
return {
|
||||
id: null,
|
||||
direction: null,
|
||||
// Défaut métier : sens CLIENT pré-sélectionné (un bloc prix CLIENT est présent
|
||||
// d'office à l'ouverture de l'onglet).
|
||||
direction: 'CLIENT',
|
||||
clientIri: null,
|
||||
clientDeliveryAddressIri: null,
|
||||
departureSiteIri: null,
|
||||
|
||||
@@ -15,46 +15,46 @@ function isFilled(value: string | null | undefined): boolean {
|
||||
/**
|
||||
* Payload de la sous-ressource prix (groupe `carrier:write:prices`). Envoie les
|
||||
* communs + UNIQUEMENT la branche active (l'autre branche à null, exigée par les
|
||||
* CHECK BDD). Les relations partent en IRI (string|null). Le back re-valide
|
||||
* l'obligation conditionnelle + l'appartenance de l'adresse (422 inline).
|
||||
* CHECK BDD). Les relations partent en IRI (string|null).
|
||||
*
|
||||
* IMPORTANT : les scalaires obligatoires (direction / containerType / pricingUnit /
|
||||
* price / priceState) sont OMIS s'ils sont vides — on n'envoie JAMAIS `null` sur un
|
||||
* champ string. Sinon API Platform lève un 400 « The type of the "price" attribute
|
||||
* must be "string", "NULL" given. » AVANT la validation (non mappable inline). Omis,
|
||||
* le champ reste null côté entité → l'Assert\NotBlank renvoie un 422 propre rattaché
|
||||
* au champ, affiché sous l'input comme les autres blocs (ERP-101). Le back re-valide
|
||||
* aussi l'obligation conditionnelle de branche + l'appartenance de l'adresse.
|
||||
*/
|
||||
export function buildCarrierPricePayload(price: CarrierPriceFormDraft): Record<string, unknown> {
|
||||
const common: Record<string, unknown> = {
|
||||
direction: price.direction,
|
||||
containerType: price.containerType || null,
|
||||
pricingUnit: price.pricingUnit || null,
|
||||
price: price.price || null,
|
||||
priceState: price.priceState || null,
|
||||
}
|
||||
const payload: Record<string, unknown> = {}
|
||||
|
||||
// Scalaires : présents seulement si remplis (jamais `null` → évite le 400 de type).
|
||||
if (isFilled(price.direction)) payload.direction = price.direction
|
||||
if (isFilled(price.containerType)) payload.containerType = price.containerType
|
||||
if (isFilled(price.pricingUnit)) payload.pricingUnit = price.pricingUnit
|
||||
if (isFilled(price.price)) payload.price = price.price
|
||||
if (isFilled(price.priceState)) payload.priceState = price.priceState
|
||||
|
||||
// Branche active en IRI (null toléré sur une relation, ne déclenche pas le 400 de
|
||||
// type) ; branche inactive forcée à null (CHECK BDD chk_carrier_price_*_branch).
|
||||
if (price.direction === 'CLIENT') {
|
||||
return {
|
||||
...common,
|
||||
client: price.clientIri || null,
|
||||
clientDeliveryAddress: price.clientDeliveryAddressIri || null,
|
||||
departureSite: price.departureSiteIri || null,
|
||||
// Branche FOURNISSEUR forcée à null (CHECK chk_carrier_price_client_branch).
|
||||
supplier: null,
|
||||
supplierSupplyAddress: null,
|
||||
deliverySite: null,
|
||||
}
|
||||
payload.client = price.clientIri || null
|
||||
payload.clientDeliveryAddress = price.clientDeliveryAddressIri || null
|
||||
payload.departureSite = price.departureSiteIri || null
|
||||
payload.supplier = null
|
||||
payload.supplierSupplyAddress = null
|
||||
payload.deliverySite = null
|
||||
}
|
||||
else if (price.direction === 'FOURNISSEUR') {
|
||||
payload.supplier = price.supplierIri || null
|
||||
payload.supplierSupplyAddress = price.supplierSupplyAddressIri || null
|
||||
payload.deliverySite = price.deliverySiteIri || null
|
||||
payload.client = null
|
||||
payload.clientDeliveryAddress = null
|
||||
payload.departureSite = null
|
||||
}
|
||||
|
||||
if (price.direction === 'FOURNISSEUR') {
|
||||
return {
|
||||
...common,
|
||||
supplier: price.supplierIri || null,
|
||||
supplierSupplyAddress: price.supplierSupplyAddressIri || null,
|
||||
deliverySite: price.deliverySiteIri || null,
|
||||
// Branche CLIENT forcée à null (CHECK chk_carrier_price_supplier_branch).
|
||||
client: null,
|
||||
clientDeliveryAddress: null,
|
||||
departureSite: null,
|
||||
}
|
||||
}
|
||||
|
||||
// Direction non choisie : on envoie les communs ; le back 422 sur `direction`.
|
||||
return common
|
||||
return payload
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user