feat(commercial) : types d'adresse Courtier et Distributeur (ERP-119)

Ajoute deux types d'adresse autonomes (exclusifs, comme la Prospection) :
- back : colonnes is_broker / is_distributor sur client_address (migration
  modulaire, append + 2 CHECK miroir d'exclusivite + COMMENT ON COLUMN),
  proprietes ClientAddress (getters Groups + SerializedName), Callback
  validateExclusiveAddressTypes, validateAddressTypeRequired etendue,
  catalogue des commentaires SQL mis a jour.
- front : type AddressType (+broker/distributor), drapeaux, mappers, option
  du select Type d'adresse, labels i18n, payloads create/edit et lecture.
- tests back (acceptation + exclusivite + contrat de serialisation) et front.
This commit is contained in:
2026-06-09 13:37:24 +02:00
parent 982f501b94
commit f6be671230
14 changed files with 314 additions and 52 deletions
@@ -20,6 +20,7 @@ import {
isRibRequiredForPaymentType,
omitEmptyRequired,
showsRelationAndTriageFields,
type AddressFlagsDraft,
type AddressValidityDraft,
type ContactDraft,
type ContactFillableDraft,
@@ -149,83 +150,79 @@ describe('hasAtLeastOneValidContact (RG-1.14)', () => {
})
})
/** Drapeaux d'adresse complets (5 types) avec surcharge partielle. */
function flags(overrides: Partial<AddressFlagsDraft> = {}): AddressFlagsDraft {
return {
isProspect: false, isDelivery: false, isBilling: false, isBroker: false, isDistributor: false,
...overrides,
}
}
describe('exclusivite Prospect / Livraison / Facturation (RG-1.06/07/08)', () => {
it('Prospect est selectionnable tant que ni Livraison ni Facturation', () => {
expect(canSelectProspect({ isProspect: false, isDelivery: false, isBilling: false })).toBe(true)
expect(canSelectProspect({ isProspect: false, isDelivery: true, isBilling: false })).toBe(false)
expect(canSelectProspect({ isProspect: false, isDelivery: false, isBilling: true })).toBe(false)
expect(canSelectProspect(flags())).toBe(true)
expect(canSelectProspect(flags({ isDelivery: true }))).toBe(false)
expect(canSelectProspect(flags({ isBilling: true }))).toBe(false)
})
it('Livraison / Facturation selectionnables tant que pas Prospect', () => {
expect(canSelectDeliveryOrBilling({ isProspect: false, isDelivery: false, isBilling: false })).toBe(true)
expect(canSelectDeliveryOrBilling({ isProspect: true, isDelivery: false, isBilling: false })).toBe(false)
expect(canSelectDeliveryOrBilling(flags())).toBe(true)
expect(canSelectDeliveryOrBilling(flags({ isProspect: true }))).toBe(false)
})
it('cocher Prospect efface Livraison et Facturation', () => {
const next = applyProspectExclusivity(
{ isProspect: false, isDelivery: true, isBilling: true },
'isProspect',
true,
)
expect(next).toEqual({ isProspect: true, isDelivery: false, isBilling: false })
const next = applyProspectExclusivity(flags({ isDelivery: true, isBilling: true }), 'isProspect', true)
expect(next).toEqual(flags({ isProspect: true }))
})
it('cocher Livraison efface Prospect', () => {
const next = applyProspectExclusivity(
{ isProspect: true, isDelivery: false, isBilling: false },
'isDelivery',
true,
)
expect(next).toEqual({ isProspect: false, isDelivery: true, isBilling: false })
const next = applyProspectExclusivity(flags({ isProspect: true }), 'isDelivery', true)
expect(next).toEqual(flags({ isDelivery: true }))
})
it('cocher Facturation efface Prospect mais conserve Livraison', () => {
const next = applyProspectExclusivity(
{ isProspect: true, isDelivery: true, isBilling: false },
'isBilling',
true,
)
expect(next).toEqual({ isProspect: false, isDelivery: true, isBilling: true })
const next = applyProspectExclusivity(flags({ isProspect: true, isDelivery: true }), 'isBilling', true)
expect(next).toEqual(flags({ isDelivery: true, isBilling: true }))
})
it('decocher un drapeau ne reactive rien d autre', () => {
const next = applyProspectExclusivity(
{ isProspect: false, isDelivery: true, isBilling: true },
'isBilling',
false,
)
expect(next).toEqual({ isProspect: false, isDelivery: true, isBilling: false })
const next = applyProspectExclusivity(flags({ isDelivery: true, isBilling: true }), 'isBilling', false)
expect(next).toEqual(flags({ isDelivery: true }))
})
})
describe('isBillingEmailRequired (RG-1.11)', () => {
it('obligatoire uniquement si Facturation est coche', () => {
expect(isBillingEmailRequired({ isProspect: false, isDelivery: false, isBilling: true })).toBe(true)
expect(isBillingEmailRequired({ isProspect: false, isDelivery: true, isBilling: false })).toBe(false)
expect(isBillingEmailRequired(flags({ isBilling: true }))).toBe(true)
expect(isBillingEmailRequired(flags({ isDelivery: true }))).toBe(false)
})
})
describe('type d\'adresse (Select front) <-> drapeaux back', () => {
it('addressFlagsFromType mappe chaque type vers les bons drapeaux', () => {
expect(addressFlagsFromType('prospect')).toEqual({ isProspect: true, isDelivery: false, isBilling: false })
expect(addressFlagsFromType('delivery')).toEqual({ isProspect: false, isDelivery: true, isBilling: false })
expect(addressFlagsFromType('billing')).toEqual({ isProspect: false, isDelivery: false, isBilling: true })
expect(addressFlagsFromType('delivery_billing')).toEqual({ isProspect: false, isDelivery: true, isBilling: true })
expect(addressFlagsFromType('prospect')).toEqual(flags({ isProspect: true }))
expect(addressFlagsFromType('delivery')).toEqual(flags({ isDelivery: true }))
expect(addressFlagsFromType('billing')).toEqual(flags({ isBilling: true }))
expect(addressFlagsFromType('delivery_billing')).toEqual(flags({ isDelivery: true, isBilling: true }))
expect(addressFlagsFromType('broker')).toEqual(flags({ isBroker: true }))
expect(addressFlagsFromType('distributor')).toEqual(flags({ isDistributor: true }))
})
it('addressTypeFromFlags reconstruit le type (Prospect prioritaire, livraison+facturation groupes)', () => {
expect(addressTypeFromFlags({ isProspect: true, isDelivery: false, isBilling: false })).toBe('prospect')
expect(addressTypeFromFlags({ isProspect: false, isDelivery: true, isBilling: false })).toBe('delivery')
expect(addressTypeFromFlags({ isProspect: false, isDelivery: false, isBilling: true })).toBe('billing')
expect(addressTypeFromFlags({ isProspect: false, isDelivery: true, isBilling: true })).toBe('delivery_billing')
it('addressTypeFromFlags reconstruit le type (Prospect/Courtier/Distributeur autonomes, livraison+facturation groupes)', () => {
expect(addressTypeFromFlags(flags({ isProspect: true }))).toBe('prospect')
expect(addressTypeFromFlags(flags({ isDelivery: true }))).toBe('delivery')
expect(addressTypeFromFlags(flags({ isBilling: true }))).toBe('billing')
expect(addressTypeFromFlags(flags({ isDelivery: true, isBilling: true }))).toBe('delivery_billing')
expect(addressTypeFromFlags(flags({ isBroker: true }))).toBe('broker')
expect(addressTypeFromFlags(flags({ isDistributor: true }))).toBe('distributor')
})
it('addressTypeFromFlags retourne null quand aucun drapeau (amorce vierge -> bouton bloque)', () => {
expect(addressTypeFromFlags({ isProspect: false, isDelivery: false, isBilling: false })).toBeNull()
expect(addressTypeFromFlags(flags())).toBeNull()
})
it('aller-retour type -> drapeaux -> type stable pour les 4 types', () => {
for (const type of ['prospect', 'delivery', 'billing', 'delivery_billing'] as const) {
it('aller-retour type -> drapeaux -> type stable pour les 6 types', () => {
for (const type of ['prospect', 'delivery', 'billing', 'delivery_billing', 'broker', 'distributor'] as const) {
expect(addressTypeFromFlags(addressFlagsFromType(type))).toBe(type)
}
})
@@ -325,6 +322,8 @@ describe('isAddressValid (gating « + Adresse » + validation onglet)', () => {
isProspect: false,
isDelivery: true,
isBilling: false,
isBroker: false,
isDistributor: false,
categoryIris: ['/api/client_categories/1'],
siteIris: ['/api/sites/1'],
billingEmail: null,