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 :
- Côté API :
GET /sellers, un endpoint public qui retourne un annuaire restreint (identité minimale + adresse par défaut + nombre de produits actifs) des vendeursSELLER,isActive: trueet désormaisvendorStatus: APPROVED. - Côté Store : la page
/producteursré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: APPROVEDa été ajouté récemment, en même temps qu’un workflow complet de validation manuelle des comptes vendeur auto-inscrits par un administrateur (endpointsGET /sellers/pendingetPATCH /sellers/:id/validate, tous deux réservés au rôleADMIN). Ce workflow est documenté en détail dansvendor-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 vendeurPENDINGouREJECTEDn’apparaît ni dansGET /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
| Endpoint | Méthode | Rôle requis | Description |
|---|---|---|---|
/sellers | GET | Public | Annuaire des vendeurs actifs et approuvés — alimente la carte des producteurs (et tout futur annuaire vendeur côté Store). |
/sellers/pending | GET | ADMIN | Queue des comptes vendeur en attente de validation. Documenté dans vendor-validation.md. |
/sellers/:id/validate | PATCH | ADMIN | Approuve ou rejette un compte vendeur. Documenté dans vendor-validation.md. |
[!IMPORTANT]
GET /sellersretourne 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 /usersqui 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-Agentidentifiable obligatoire — respectée ici). En cas d’échec (service indisponible, requête trop imprécise, adresse introuvable),latitude/longituderestent simplementnull: 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
latitudeetlongitudenon 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], zoom6) — 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) :Un vendeur sans adresse par défaut apparaît tout de même dans la liste ([ { "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 } ]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/pendingetPATCH /sellers/:id/validate(queue de validation, motifs de rejet, notifications SSEvendor.status.decided), voirvendor-validation.md.