/** * Helpers purs de l'onglet Prix transporteur (M4 Transport, ERP-169 — RG-4.09→4.11). * Une ligne porte une branche CLIENT ou FOURNISSEUR selon `direction` ; les champs * de la branche INACTIVE doivent toujours partir à null (CHECK BDD * chk_carrier_price_client_branch / supplier_branch). Testables sans Vue ni API. */ import type { CarrierPriceFormDraft } from '~/modules/transport/types/carrierForm' /** Vrai si une chaîne porte au moins un caractère non-espace. */ function isFilled(value: string | null | undefined): boolean { return value !== null && value !== undefined && value.trim() !== '' } /** * 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). * * 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 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') { 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 } return payload } /** * Pré-check léger du gating « + Nouveau prix » : direction choisie, prix rempli, et * branche active complète (client/adresse/site OU fournisseur/adresse/site). Le back * reste la couche autoritaire (RG-4.09→4.11) ; ce pré-check évite d'empiler des * blocs vides. */ export function isCarrierPriceValid(price: CarrierPriceFormDraft): boolean { if (!isFilled(price.price) || !isFilled(price.containerType) || !isFilled(price.pricingUnit) || !isFilled(price.priceState)) { return false } if (price.direction === 'CLIENT') { return isFilled(price.clientIri) && isFilled(price.clientDeliveryAddressIri) && isFilled(price.departureSiteIri) } if (price.direction === 'FOURNISSEUR') { return isFilled(price.supplierIri) && isFilled(price.supplierSupplyAddressIri) && isFilled(price.deliverySiteIri) } return false }