diff --git a/frontend/modules/transport/components/CarrierPriceBlock.vue b/frontend/modules/transport/components/CarrierPriceBlock.vue
index c46ee46..da5bfae 100644
--- a/frontend/modules/transport/components/CarrierPriceBlock.vue
+++ b/frontend/modules/transport/components/CarrierPriceBlock.vue
@@ -10,9 +10,10 @@
@click="$emit('remove')"
/>
-
-
+
+
{
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)
@@ -786,17 +788,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 () => {
diff --git a/frontend/modules/transport/composables/useCarrierForm.ts b/frontend/modules/transport/composables/useCarrierForm.ts
index 787cff9..4657646 100644
--- a/frontend/modules/transport/composables/useCarrierForm.ts
+++ b/frontend/modules/transport/composables/useCarrierForm.ts
@@ -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([])
+ // Un bloc présent par défaut (sens CLIENT pré-sélectionné). L'utilisateur ajoute
+ // les suivants via « + Nouveau prix ».
+ const prices = ref([emptyCarrierPrice()])
// Erreurs 422 par ligne (alignées sur l'index du v-for), peuplées par submitRows.
const priceErrors = ref[]>([])
diff --git a/frontend/modules/transport/types/carrierForm.ts b/frontend/modules/transport/types/carrierForm.ts
index 9d8eb4e..b31db2d 100644
--- a/frontend/modules/transport/types/carrierForm.ts
+++ b/frontend/modules/transport/types/carrierForm.ts
@@ -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,
diff --git a/frontend/modules/transport/utils/forms/carrierPrice.ts b/frontend/modules/transport/utils/forms/carrierPrice.ts
index ee6f20c..3293234 100644
--- a/frontend/modules/transport/utils/forms/carrierPrice.ts
+++ b/frontend/modules/transport/utils/forms/carrierPrice.ts
@@ -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 {
- const common: Record = {
- direction: price.direction,
- containerType: price.containerType || null,
- pricingUnit: price.pricingUnit || null,
- price: price.price || null,
- priceState: price.priceState || null,
- }
+ const payload: Record = {}
+ // 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
}
/**