/** * 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>( 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 { 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 { 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 { 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 { 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 { 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 { return finalizeRequired({ label: rib.label, bic: rib.bic, iban: rib.iban, }, RIB_REQUIRED_NON_NULLABLE_KEYS, options) }