feat(commercial) : géolocalisation des adresses Tiers (lat/lng + géocodage BAN + pin ajustable) (ERP-122)

Ajoute la géolocalisation aux adresses Client et Fournisseur, socle de la
tournée commerciale (M6 field-sales).

Back :
- migration : latitude/longitude NUMERIC(10,7), geo_manual BOOLEAN, geocoded_at
  TIMESTAMPTZ sur client_address et supplier_address (+ COMMENT ON COLUMN FR)
- GeolocatableAddressInterface (Shared/Domain/Contract) implémenté par les deux
  entités ; bornes WGS84 validées (Range -90/90, -180/180, messages FR)
- GeocoderInterface + BanGeocoder (api-adresse.data.gouv.fr), branché via
  AddressGeocoder dans les processors ; géocodage auto au create/update
- RG-6.08 : geo_manual=true fige les coordonnées (pas de réécriture auto)
- symfony/http-client passe en dépendance de production

Front :
- AddressGeoPin (Leaflet + OSM) : marqueur déplaçable -> PATCH lat/lng +
  geoManual=true, bouton Re-géocoder, badges « à géolocaliser » / « pin manuel »
- intégration dans les blocs adresse Client et Fournisseur

Tests : PHPUnit (géocodage create, non-réécriture RG-6.08, mapping BAN, bornes) +
Vitest (drag du pin, badges, re-géocodage).
This commit is contained in:
Matthieu
2026-06-11 14:31:35 +02:00
parent 431d831c8b
commit de4aaa1d64
34 changed files with 1966 additions and 204 deletions
@@ -69,6 +69,10 @@ export interface AddressRead extends HydraRef {
isBilling?: boolean
isBroker?: boolean
isDistributor?: boolean
/** Geolocalisation (M6.1) : chaines decimales NUMERIC(10,7) cote API. */
latitude?: string | null
longitude?: string | null
geoManual?: boolean
sites?: SiteRead[]
categories?: CategoryRead[]
// L'embed M2M des contacts d'adresse peut etre un objet (partiel) ou un IRI nu.
@@ -225,6 +229,9 @@ export function mapAddressToDraft(address: AddressRead): AddressFormDraft {
billingEmail: address.billingEmail ?? null,
billingEmailSecondary: address.billingEmailSecondary ?? null,
hasSecondaryBillingEmail: (address.billingEmailSecondary ?? null) !== null && address.billingEmailSecondary !== '',
latitude: address.latitude ?? null,
longitude: address.longitude ?? null,
geoManual: address.geoManual === true,
}
}
@@ -254,6 +254,11 @@ export function buildAddressPayload(
contacts: address.contactIris,
billingEmail: isBillingEmailRequired ? (address.billingEmail || null) : null,
billingEmailSecondary: isBillingEmailRequired && address.hasSecondaryBillingEmail ? (address.billingEmailSecondary || null) : null,
// Geolocalisation (M6.1) : pin manuel persiste avec geoManual=true ;
// geoManual=false laisse le back regeocoder depuis l'adresse postale.
latitude: address.latitude || null,
longitude: address.longitude || null,
geoManual: address.geoManual,
}, ADDRESS_REQUIRED_NON_NULLABLE_KEYS, options)
}
@@ -76,6 +76,10 @@ export interface AddressRead extends HydraRef {
streetComplement?: string | null
bennes?: number | null
triageProvider?: boolean
/** Geolocalisation (M6.1) : chaines decimales NUMERIC(10,7) cote API. */
latitude?: string | null
longitude?: string | null
geoManual?: boolean
sites?: SiteRead[]
categories?: CategoryRead[]
// L'embed M2M des contacts d'adresse peut etre un objet (partiel) ou un IRI nu.
@@ -200,6 +204,9 @@ export function mapAddressToDraft(address: AddressRead): SupplierAddressFormDraf
contactIris: (address.contacts ?? []).map(c => (typeof c === 'string' ? c : c['@id'])),
bennes: address.bennes != null ? String(address.bennes) : '0',
triageProvider: address.triageProvider ?? false,
latitude: address.latitude ?? null,
longitude: address.longitude ?? null,
geoManual: address.geoManual === true,
}
}
@@ -237,6 +237,11 @@ export function buildAddressPayload(address: SupplierAddressFormDraft, options:
contacts: address.contactIris,
bennes: address.bennes !== null && address.bennes !== '' ? Number(address.bennes) : null,
triageProvider: address.triageProvider,
// Geolocalisation (M6.1) : pin manuel persiste avec geoManual=true ;
// geoManual=false laisse le back regeocoder depuis l'adresse postale.
latitude: address.latitude || null,
longitude: address.longitude || null,
geoManual: address.geoManual,
}, ADDRESS_REQUIRED_NON_NULLABLE_KEYS, options)
}