8eff37186d
En edition (PATCH merge), omettre la cle d'un champ requis vide laissait la valeur serveur inchangee -> faux 200 (l'ancien code postal etait conserve). Nouveau helper blankEmptyRequired + flag forUpdate sur les builders : a la creation (POST) on omet toujours la cle (NotBlank), en edition d'une ligne existante on envoie '' (chaine valide, pas de 400 de type) pour declencher NotBlank 422 inline sous le champ. Applique au bloc principal, aux adresses et aux RIB (selon id !== null).
251 lines
10 KiB
TypeScript
251 lines
10 KiB
TypeScript
/**
|
|
* Helpers purs des ecrans « Ajouter » / « Modifier » un fournisseur (M2
|
|
* Commercial) — miroir de `clientEdit.ts` (M1). Deux responsabilites, toutes deux
|
|
* testables unitairement (cf. supplierEdit.spec.ts) :
|
|
* 1. Pre-remplissage : mapper le payload `GET /api/suppliers/{id}` (embed +
|
|
* scalaires) vers les brouillons « plats » edites par la page de modification.
|
|
* 2. Scoping STRICT des payloads PATCH (mode strict RG-2.16 / ERP-74) : chaque
|
|
* onglet n'envoie QUE les champs de SON groupe de serialisation, jamais un
|
|
* payload mixte (un champ hors-permission = 403 sur l'integralite cote back).
|
|
*
|
|
* Ces helpers ne touchent ni a l'API ni a l'etat reactif.
|
|
*/
|
|
|
|
import {
|
|
ADDRESS_REQUIRED_NON_NULLABLE_KEYS,
|
|
blankEmptyRequired,
|
|
MAIN_REQUIRED_NON_NULLABLE_KEYS,
|
|
omitEmptyRequired,
|
|
RIB_REQUIRED_NON_NULLABLE_KEYS,
|
|
} from '~/modules/commercial/utils/supplierFormRules'
|
|
import { iriOf, type SupplierDetail } from '~/modules/commercial/utils/supplierConsultation'
|
|
import type {
|
|
SupplierAddressFormDraft,
|
|
SupplierContactFormDraft,
|
|
SupplierRibFormDraft,
|
|
} from '~/modules/commercial/types/supplierForm'
|
|
|
|
/** Etat « plat » du bloc principal (groupe supplier:write:main). */
|
|
export interface MainFormDraft {
|
|
companyName: string | null
|
|
/** IRI des categories rattachees (M2M, type FOURNISSEUR). */
|
|
categoryIris: string[]
|
|
}
|
|
|
|
/** Etat « plat » de l'onglet Information (groupe supplier:write:information). */
|
|
export interface InformationFormDraft {
|
|
description: string | null
|
|
competitors: string | null
|
|
/** Date de creation de l'entreprise au format YYYY-MM-DD (MalioDate). */
|
|
foundedAt: string | null
|
|
/** Nombre de salaries en chaine (saisie masquee), converti en number au PATCH. */
|
|
employeesCount: string | null
|
|
revenueAmount: string | null
|
|
profitAmount: string | null
|
|
directorName: string | null
|
|
/** Volume previsionnel (entier >= 0, specifique fournisseur) en chaine. */
|
|
volumeForecast: string | null
|
|
}
|
|
|
|
/** Etat « plat » de l'onglet Comptabilite (groupe supplier:write:accounting). */
|
|
export interface AccountingFormDraft {
|
|
siren: string | null
|
|
accountNumber: string | null
|
|
nTva: string | null
|
|
tvaModeIri: string | null
|
|
paymentDelayIri: string | null
|
|
paymentTypeIri: string | null
|
|
bankIri: string | null
|
|
}
|
|
|
|
/** Permissions de l'utilisateur courant pertinentes pour l'edition d'un fournisseur. */
|
|
export interface SupplierEditAbilities {
|
|
/** `commercial.suppliers.manage` : bloc principal + onglets metier. */
|
|
canManage: boolean
|
|
/** `commercial.suppliers.accounting.view` : visibilite de l'onglet Comptabilite. */
|
|
canAccountingView: boolean
|
|
/** `commercial.suppliers.accounting.manage` : edition de l'onglet Comptabilite. */
|
|
canAccountingManage: boolean
|
|
}
|
|
|
|
/** Editabilite resolue par zone d'onglet (deduite des permissions). */
|
|
export interface TabEditability {
|
|
/** Bloc principal + onglets Information / Contacts / Adresses editables. */
|
|
businessEditable: boolean
|
|
/** Onglet Comptabilite present (affiche). */
|
|
accountingVisible: boolean
|
|
/** Onglet Comptabilite editable. */
|
|
accountingEditable: boolean
|
|
}
|
|
|
|
// ── Pre-remplissage (GET detail -> brouillons) ──────────────────────────────
|
|
|
|
/** Mappe le detail fournisseur vers le brouillon du bloc principal. */
|
|
export function mapMainDraft(supplier: SupplierDetail): MainFormDraft {
|
|
return {
|
|
companyName: supplier.companyName ?? null,
|
|
categoryIris: (supplier.categories ?? []).map(c => c['@id']),
|
|
}
|
|
}
|
|
|
|
/** Mappe le detail fournisseur vers le brouillon de l'onglet Information. */
|
|
export function mapInformationDraft(supplier: SupplierDetail): InformationFormDraft {
|
|
return {
|
|
description: supplier.description ?? null,
|
|
competitors: supplier.competitors ?? null,
|
|
// MalioDate attend strictement YYYY-MM-DD : on tronque l'ISO datetime.
|
|
foundedAt: supplier.foundedAt ? supplier.foundedAt.slice(0, 10) : null,
|
|
employeesCount: supplier.employeesCount != null ? String(supplier.employeesCount) : null,
|
|
revenueAmount: supplier.revenueAmount ?? null,
|
|
profitAmount: supplier.profitAmount ?? null,
|
|
directorName: supplier.directorName ?? null,
|
|
// Volume previsionnel (entier, specifique fournisseur) en chaine pour la saisie.
|
|
volumeForecast: supplier.volumeForecast != null ? String(supplier.volumeForecast) : null,
|
|
}
|
|
}
|
|
|
|
/** Mappe les champs comptables du detail vers le brouillon de l'onglet (scalaires + IRI). */
|
|
export function mapAccountingFormDraft(supplier: SupplierDetail): AccountingFormDraft {
|
|
return {
|
|
siren: supplier.siren ?? null,
|
|
accountNumber: supplier.accountNumber ?? null,
|
|
nTva: supplier.nTva ?? null,
|
|
tvaModeIri: iriOf(supplier.tvaMode),
|
|
paymentDelayIri: iriOf(supplier.paymentDelay),
|
|
paymentTypeIri: iriOf(supplier.paymentType),
|
|
bankIri: iriOf(supplier.bank),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resout l'editabilite par zone a partir des permissions (option 1 ERP-74,
|
|
* miroir UI du re-gating champ-par-champ du SupplierProcessor) :
|
|
* - bloc principal + Information/Contacts/Adresses : editables ssi `manage` ;
|
|
* - Comptabilite : visible ssi `accounting.view`, editable ssi `accounting.manage`.
|
|
*
|
|
* Produit le comportement attendu :
|
|
* - Admin : tout editable.
|
|
* - Bureau / Commerciale (manage, sans accounting) : metier editable, Compta masquee.
|
|
* - Compta (accounting seul, sans manage) : metier readonly, Compta editable.
|
|
*/
|
|
export function resolveTabEditability(abilities: SupplierEditAbilities): TabEditability {
|
|
return {
|
|
businessEditable: abilities.canManage,
|
|
accountingVisible: abilities.canAccountingView,
|
|
accountingEditable: abilities.canAccountingManage,
|
|
}
|
|
}
|
|
|
|
// ── Scoping strict des payloads PATCH/POST ──────────────────────────────────
|
|
|
|
/**
|
|
* Options de construction d'un payload d'ecriture.
|
|
* - `forUpdate: false` (defaut, CREATION/POST) : les champs requis vides sont OMIS
|
|
* -> 422 NotBlank a l'insert (le back ne reçoit pas la cle).
|
|
* - `forUpdate: true` (EDITION/PATCH d'une ligne existante) : les champs requis
|
|
* vides sont envoyes en `''` -> 422 NotBlank (sinon une cle omise laisse la valeur
|
|
* serveur inchangee, faux 200 — cf. blankEmptyRequired).
|
|
*/
|
|
export interface BuildPayloadOptions {
|
|
forUpdate?: boolean
|
|
}
|
|
|
|
/** Selectionne le finaliseur des champs requis selon création (omit) vs édition (blank). */
|
|
function finalizeRequired<T extends Record<string, unknown>>(
|
|
payload: T,
|
|
requiredKeys: readonly string[],
|
|
options: BuildPayloadOptions,
|
|
): T {
|
|
return options.forUpdate
|
|
? blankEmptyRequired(payload, requiredKeys)
|
|
: omitEmptyRequired(payload, requiredKeys)
|
|
}
|
|
|
|
/**
|
|
* Payload du bloc principal — groupe supplier:write:main UNIQUEMENT.
|
|
* companyName vide -> 422 NotBlank (omis a la creation, `''` en edition — ERP-119).
|
|
*/
|
|
export function buildMainPayload(main: MainFormDraft, options: BuildPayloadOptions = {}): Record<string, unknown> {
|
|
return finalizeRequired({
|
|
companyName: main.companyName,
|
|
categories: main.categoryIris,
|
|
}, MAIN_REQUIRED_NON_NULLABLE_KEYS, options)
|
|
}
|
|
|
|
/** Payload de l'onglet Information — groupe supplier:write:information UNIQUEMENT. */
|
|
export function buildInformationPayload(information: InformationFormDraft): Record<string, unknown> {
|
|
return {
|
|
description: information.description || null,
|
|
competitors: information.competitors || null,
|
|
foundedAt: information.foundedAt || null,
|
|
employeesCount: information.employeesCount ? Number(information.employeesCount) : null,
|
|
revenueAmount: information.revenueAmount || null,
|
|
profitAmount: information.profitAmount || null,
|
|
directorName: information.directorName || null,
|
|
volumeForecast: information.volumeForecast ? Number(information.volumeForecast) : null,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Payload des scalaires de l'onglet Comptabilite — groupe supplier:write:accounting
|
|
* UNIQUEMENT (les RIB passent par la sous-ressource /suppliers/{id}/ribs). La
|
|
* banque n'a de sens que pour un Virement (RG-2.07) : forcee a null sinon.
|
|
*/
|
|
export function buildAccountingPayload(
|
|
accounting: AccountingFormDraft,
|
|
isBankRequired: boolean,
|
|
): Record<string, unknown> {
|
|
return {
|
|
siren: accounting.siren || null,
|
|
accountNumber: accounting.accountNumber || null,
|
|
tvaMode: accounting.tvaModeIri,
|
|
nTva: accounting.nTva || null,
|
|
paymentDelay: accounting.paymentDelayIri,
|
|
paymentType: accounting.paymentTypeIri,
|
|
bank: isBankRequired ? accounting.bankIri : null,
|
|
}
|
|
}
|
|
|
|
/** Payload d'un contact (sous-ressource supplier_contact). */
|
|
export function buildContactPayload(contact: SupplierContactFormDraft): Record<string, unknown> {
|
|
return {
|
|
firstName: contact.firstName || null,
|
|
lastName: contact.lastName || null,
|
|
jobTitle: contact.jobTitle || null,
|
|
phonePrimary: contact.phonePrimary || null,
|
|
phoneSecondary: contact.hasSecondaryPhone ? (contact.phoneSecondary || null) : null,
|
|
email: contact.email || null,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Payload d'une adresse (sous-ressource supplier_address). postalCode / city /
|
|
* street omis si vides -> 422 NotBlank (ERP-119). Specifique fournisseur :
|
|
* `bennes` (entier, 0 par defaut) + `triageProvider` (booleen). Pas d'email de
|
|
* facturation (difference M1).
|
|
*/
|
|
export function buildAddressPayload(address: SupplierAddressFormDraft, options: BuildPayloadOptions = {}): Record<string, unknown> {
|
|
return finalizeRequired({
|
|
addressType: address.addressType,
|
|
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,
|
|
bennes: address.bennes !== null && address.bennes !== '' ? Number(address.bennes) : null,
|
|
triageProvider: address.triageProvider,
|
|
}, ADDRESS_REQUIRED_NON_NULLABLE_KEYS, options)
|
|
}
|
|
|
|
/** Payload d'un RIB (sous-ressource supplier_rib). */
|
|
export function buildRibPayload(rib: SupplierRibFormDraft, options: BuildPayloadOptions = {}): Record<string, unknown> {
|
|
return finalizeRequired({
|
|
label: rib.label,
|
|
bic: rib.bic,
|
|
iban: rib.iban,
|
|
}, RIB_REQUIRED_NON_NULLABLE_KEYS, options)
|
|
}
|