feat(commercial) : validation back de la relation + suivi de revue MR (ERP-119)
- validation serveur « relation choisie => FK obligatoire » : champ transitoire relationType (non persiste) + Assert\Callback portant la 422 sur distributor / broker, que le back ne pouvait pas deriver des seules FK nullable - mutualisation des payloads d'ecriture clients : new.vue consomme buildMainPayload / buildAddressPayload / buildRibPayload (fin de la duplication create/edit) - COMMENT ON TABLE client_address : ajout des types Courtier / Distributeur (catalogue + migration Version20260609120000) - tests : violationsByPath remonte dans AbstractCommercialApiTestCase (fin des copies inline) + couverture de la nouvelle RG relation
This commit is contained in:
@@ -389,7 +389,6 @@ import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useClientReferentials, type RefOption } from '~/modules/commercial/composables/useClientReferentials'
|
||||
import { useClientFormErrors } from '~/modules/commercial/composables/useClientFormErrors'
|
||||
import {
|
||||
ADDRESS_REQUIRED_NON_NULLABLE_KEYS,
|
||||
buildClientFormTabKeys,
|
||||
CLIENT_FORM_PLACEHOLDER_TABS,
|
||||
isAddressValid,
|
||||
@@ -401,11 +400,13 @@ import {
|
||||
isRibComplete,
|
||||
isRibRequiredForPaymentType,
|
||||
lastFillableTabKey,
|
||||
MAIN_REQUIRED_NON_NULLABLE_KEYS,
|
||||
omitEmptyRequired,
|
||||
RIB_REQUIRED_NON_NULLABLE_KEYS,
|
||||
showsRelationAndTriageFields,
|
||||
} from '~/modules/commercial/utils/clientFormRules'
|
||||
import {
|
||||
buildAddressPayload,
|
||||
buildMainPayload,
|
||||
buildRibPayload,
|
||||
} from '~/modules/commercial/utils/clientEdit'
|
||||
import {
|
||||
emptyAddress,
|
||||
emptyContact,
|
||||
@@ -538,15 +539,9 @@ async function submitMain(): Promise<void> {
|
||||
mainSubmitting.value = true
|
||||
mainErrors.clearErrors()
|
||||
try {
|
||||
// companyName omis si vide -> 422 NotBlank au lieu d'un 400 de type (ERP-119).
|
||||
const payload: Record<string, unknown> = omitEmptyRequired({
|
||||
companyName: main.companyName,
|
||||
categories: main.categoryIris,
|
||||
distributor: main.relationType === 'distributeur' ? main.distributorIri : null,
|
||||
broker: main.relationType === 'courtier' ? main.brokerIri : null,
|
||||
triageService: main.triageService,
|
||||
}, MAIN_REQUIRED_NON_NULLABLE_KEYS)
|
||||
const created = await api.post<ClientResponse>('/clients', payload, {
|
||||
// Payload partage avec l'edition (buildMainPayload) : meme logique
|
||||
// d'omission des requis vides et meme envoi de relationType (ERP-119).
|
||||
const created = await api.post<ClientResponse>('/clients', buildMainPayload(main), {
|
||||
headers: { Accept: 'application/ld+json' },
|
||||
toast: false,
|
||||
})
|
||||
@@ -826,24 +821,8 @@ async function submitAddresses(): Promise<void> {
|
||||
addresses.value,
|
||||
addressErrors,
|
||||
async (address) => {
|
||||
// postalCode / city / street omis si vides -> 422 NotBlank (ERP-119).
|
||||
const body = omitEmptyRequired({
|
||||
isProspect: address.isProspect,
|
||||
isDelivery: address.isDelivery,
|
||||
isBilling: address.isBilling,
|
||||
isBroker: address.isBroker,
|
||||
isDistributor: address.isDistributor,
|
||||
country: address.country,
|
||||
postalCode: address.postalCode || null,
|
||||
city: address.city || null,
|
||||
street: address.street || null,
|
||||
streetComplement: address.streetComplement || null,
|
||||
categories: address.categoryIris,
|
||||
sites: address.siteIris,
|
||||
contacts: address.contactIris,
|
||||
billingEmail: isBillingEmailRequired(address) ? (address.billingEmail || null) : null,
|
||||
billingEmailSecondary: isBillingEmailRequired(address) && address.hasSecondaryBillingEmail ? (address.billingEmailSecondary || null) : null,
|
||||
}, ADDRESS_REQUIRED_NON_NULLABLE_KEYS)
|
||||
// Payload partage avec l'edition (buildAddressPayload, ERP-119).
|
||||
const body = buildAddressPayload(address, isBillingEmailRequired(address))
|
||||
if (address.id === null) {
|
||||
const created = await api.post<{ id: number }>(
|
||||
`/clients/${clientId.value}/addresses`,
|
||||
@@ -947,9 +926,8 @@ async function submitAccounting(): Promise<void> {
|
||||
ribs.value,
|
||||
ribErrors,
|
||||
async (rib) => {
|
||||
// label / bic / iban omis si vides -> 422 NotBlank au lieu d'un 400
|
||||
// de type sur un RIB partiel (ex. IBAN seul). ERP-119.
|
||||
const body = omitEmptyRequired({ label: rib.label, bic: rib.bic, iban: rib.iban }, RIB_REQUIRED_NON_NULLABLE_KEYS)
|
||||
// Payload partage avec l'edition (buildRibPayload, ERP-119).
|
||||
const body = buildRibPayload(rib)
|
||||
if (rib.id === null) {
|
||||
const created = await api.post<{ id: number }>(
|
||||
`/clients/${clientId.value}/ribs`,
|
||||
|
||||
@@ -61,7 +61,9 @@ function accountingDraft(overrides: Partial<AccountingFormDraft> = {}): Accounti
|
||||
// Le contact inline (nom/prenom/telephones/email) ne fait plus partie du groupe
|
||||
// main : les coordonnees vivent desormais sur la sous-ressource ClientContact.
|
||||
const MAIN_KEYS = [
|
||||
'companyName', 'categories', 'distributor', 'broker', 'triageService',
|
||||
// relationType : champ transitoire envoye au back pour la validation croisee
|
||||
// « relation choisie => FK obligatoire » (RG-1.03 bis, ERP-119).
|
||||
'companyName', 'categories', 'relationType', 'distributor', 'broker', 'triageService',
|
||||
]
|
||||
const INFORMATION_KEYS = [
|
||||
'description', 'competitors', 'foundedAt', 'employeesCount',
|
||||
@@ -100,6 +102,12 @@ describe('buildMainPayload — scoping strict groupe client:write:main', () => {
|
||||
expect(payload.broker).toBeNull()
|
||||
})
|
||||
|
||||
it('transmet relationType au back pour la validation croisee (RG-1.03 bis)', () => {
|
||||
expect(buildMainPayload(mainDraft({ relationType: 'distributeur' })).relationType).toBe('distributeur')
|
||||
expect(buildMainPayload(mainDraft({ relationType: 'courtier' })).relationType).toBe('courtier')
|
||||
expect(buildMainPayload(mainDraft({ relationType: null })).relationType).toBeNull()
|
||||
})
|
||||
|
||||
// ERP-119 : companyName est requis ET adosse a une colonne NON-nullable. Si le
|
||||
// champ est vide, on OMET la cle (au lieu d'envoyer null) pour que le back
|
||||
// renvoie une 422 NotBlank (propertyPath companyName) et non un 400 de type.
|
||||
|
||||
@@ -146,9 +146,16 @@ export function mapAccountingFormDraft(client: ClientDetail): AccountingFormDraf
|
||||
*/
|
||||
export function buildMainPayload(main: MainFormDraft): Record<string, unknown> {
|
||||
// companyName omis si vide -> 422 NotBlank au lieu d'un 400 de type (ERP-119).
|
||||
// relationType : champ transitoire (non persiste cote back) qui porte
|
||||
// l'intention UI « ce client depend d'un distributeur / courtier ». Il sert
|
||||
// a la validation croisee serveur (RG-1.03 bis) : si une relation est choisie,
|
||||
// la FK correspondante devient obligatoire -> 422 sur distributor / broker.
|
||||
// Sans equivalent derivable cote back (FK nullable), c'est la seule facon de
|
||||
// rester sur « on soumet, le back tranche » plutot qu'une garde front-only.
|
||||
return omitEmptyRequired({
|
||||
companyName: main.companyName,
|
||||
categories: main.categoryIris,
|
||||
relationType: main.relationType,
|
||||
distributor: main.relationType === 'distributeur' ? main.distributorIri : null,
|
||||
broker: main.relationType === 'courtier' ? main.brokerIri : null,
|
||||
triageService: main.triageService,
|
||||
|
||||
Reference in New Issue
Block a user