Skip to Content
Annuaire public des vendeurs et carte des producteurs

Annuaire public des vendeurs et carte des producteurs

Ce document présente l’annuaire public des vendeurs (apps/api/src/sellers) et son utilisation la plus visible côté Store : la carte interactive des producteurs (apps/store/app/producteurs), qui géolocalise les vendeurs approuvés grâce au géocodage best-effort des adresses (apps/api/src/addresses/geocoding.service.ts).


1. Vue d’ensemble

L’objectif est de permettre à un visiteur du Store — sans compte, sans connexion — de découvrir les vendeurs actifs de la marketplace et de les localiser géographiquement, pour privilégier un producteur proche de chez lui. Deux briques coopèrent :

  1. Côté API : GET /sellers, un endpoint public qui retourne un annuaire restreint (identité minimale + adresse par défaut + nombre de produits actifs) des vendeurs SELLER, isActive: true et désormais vendorStatus: APPROVED.
  2. Côté Store : la page /producteurs récupère cet annuaire et le pays disponibles, puis les affiche sous forme de carte Leaflet avec un marqueur par vendeur géolocalisé.

[!NOTE] Le filtre vendorStatus: APPROVED a été ajouté récemment, en même temps qu’un workflow complet de validation manuelle des comptes vendeur auto-inscrits par un administrateur (endpoints GET /sellers/pending et PATCH /sellers/:id/validate, tous deux réservés au rôle ADMIN). Ce workflow est documenté en détail dans vendor-validation.md — il n’est mentionné ici que parce qu’il conditionne désormais qui apparaît dans l’annuaire public et sur la carte : un vendeur PENDING ou REJECTED n’apparaît ni dans GET /sellers, ni sur la carte des producteurs, tant qu’un administrateur ne l’a pas approuvé.


2. Modèle de données

GET /sellers ne sélectionne que l’adresse par défaut (isDefault: true, take: 1) de chaque vendeur — un vendeur avec plusieurs adresses n’apparaît qu’une fois sur la carte, à l’adresse qu’il a désignée comme principale.


3. Rôles et Autorisations

EndpointMéthodeRôle requisDescription
/sellersGETPublicAnnuaire des vendeurs actifs et approuvés — alimente la carte des producteurs (et tout futur annuaire vendeur côté Store).
/sellers/pendingGETADMINQueue des comptes vendeur en attente de validation. Documenté dans vendor-validation.md.
/sellers/:id/validatePATCHADMINApprouve ou rejette un compte vendeur. Documenté dans vendor-validation.md.

[!IMPORTANT] GET /sellers retourne volontairement une sélection restreinte de champs (id, firstName, lastName, username, adresse par défaut, nombre de produits actifs) — jamais l’email, le mot de passe ou le téléphone, contrairement à GET /users qui est réservé aux administrateurs. C’est une propriété de sécurité délibérée du endpoint public, à préserver si de nouveaux champs sont ajoutés à User.


4. Géocodage des adresses (GeocodingService)

Les positions affichées sur la carte proviennent de Address.latitude / Address.longitude, deux colonnes Float? ajoutées par la migration 20260922130000_add_address_geo (ALTER TABLE "Address" ADD COLUMN "latitude" ... "longitude" ...).

Le géocodage est réalisé par geocoding.service.ts, appelé automatiquement à la création/modification d’une adresse (AddressesService) :

[!WARNING] Le géocodage est du best-effort et ne doit jamais bloquer la création ou la modification d’une adresse. Nominatim est un service gratuit et sans clé API, mais avec une politique d’usage stricte (limite d’1 requête/seconde, User-Agent identifiable obligatoire — respectée ici). En cas d’échec (service indisponible, requête trop imprécise, adresse introuvable), latitude/longitude restent simplement null : l’adresse est tout de même créée normalement, et le vendeur concerné n’apparaîtra pas sur la carte tant que le géocodage n’aura pas réussi (un script de rattrapage, prisma/backfill-geocoding.ts, existe pour retenter en lot en respectant le même débit d’1 req/s).


5. Carte des producteurs côté Store

La page apps/store/app/producteurs/page.tsx est un composant serveur qui récupère GET /sellers et GET /countries (sans authentification, cache: 'no-store') puis délègue l’affichage à ProducersMap.tsx.

Pourquoi un composant intermédiaire (ProducersMap → ProducersMapInner)

La bibliothèque de cartographie utilisée est Leaflet, via react-leaflet (import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet' et import L from 'leaflet' dans ProducersMapInner.tsx). Leaflet dépend de l’objet window, indisponible côté rendu serveur (SSR) de Next.js. ProducersMap.tsx charge donc ProducersMapInner dynamiquement avec next/dynamic et ssr: false, avec un état de chargement (« Chargement de la carte… ») affiché en attendant l’hydratation côté client.

Logique d’affichage (ProducersMapInner.tsx)

  • Seuls les vendeurs dont l’adresse par défaut possède à la fois latitude et longitude non nulles sont épinglés (pinned = sellers.filter(...)) — les autres sont comptabilisés mais invisibles sur la carte (« N producteur(s) localisé(s) sur M au total »).
  • Un filtre par pays (<select>) n’est affiché que si plus d’un pays est représenté (countries.length > 1).
  • Carte centrée par défaut sur le Cameroun (DEFAULT_CENTER = [5.5, 12], zoom 6) — cohérent avec le fait qu’un seul pays est actif aujourd’hui ; le centrage s’ajustera naturellement une fois d’autres pays actifs avec des vendeurs géocodés.
  • Chaque marqueur ouvre une popup avec nom du vendeur, ville/province, nombre de produits actifs en vente, et un lien direct vers /sellers/:id.
  • Les icônes de marqueur par défaut de Leaflet sont chargées depuis le CDN unpkg (L.Icon.Default.mergeOptions(...)) — contournement d’un bug connu de résolution d’assets Leaflet avec les bundlers modernes, plutôt qu’un import local des images.
  • Le fond de carte (tuiles) provient directement des tuiles publiques OpenStreetMap ({s}.tile.openstreetmap.org), avec l’attribution requise affichée sur la carte.

6. Guide des Endpoints et Exemples

Annuaire public des vendeurs (GET /sellers)

  • Accès : public, aucun header d’authentification requis.
  • Paramètre optionnel : countryId (filtre par pays).
  • Réponse (200 OK) :
    [ { "id": "usr-vendeur-uuid-123", "firstName": "Awa", "lastName": "Nguema", "username": "awa.nguema", "address": { "city": "Yaoundé", "province": "Centre", "country": "Cameroun", "countryId": "country-uuid-cmr", "latitude": 3.848, "longitude": 11.502 }, "activeProductCount": 12 }, { "id": "usr-vendeur-uuid-456", "firstName": "Paul", "lastName": "Biya Jr.", "username": "paul.biyajr", "address": null, "activeProductCount": 3 } ]
    Un vendeur sans adresse par défaut apparaît tout de même dans la liste (address: null), mais n’est pas épinglé sur la carte.

Requête utilisée par la page /producteurs

GET /sellers → alimente les marqueurs de la carte GET /countries → alimente le filtre par pays (masqué si un seul pays actif)

Pour la documentation complète des endpoints GET /sellers/pending et PATCH /sellers/:id/validate (queue de validation, motifs de rejet, notifications SSE vendor.status.decided), voir vendor-validation.md.

Last updated on