refactor(docs): réorganiser la documentation selon principes DDD
Réorganise la documentation du projet selon les principes du Domain-Driven Design (DDD) pour améliorer la cohésion, la maintenabilité et l'alignement avec l'architecture modulaire du backend. **Structure cible:** ``` docs/domains/ ├── README.md (Context Map) ├── _shared/ (Core Domain) ├── recommendation/ (Supporting Subdomain) ├── content/ (Supporting Subdomain) ├── moderation/ (Supporting Subdomain) ├── advertising/ (Generic Subdomain) ├── premium/ (Generic Subdomain) └── monetization/ (Generic Subdomain) ``` **Changements effectués:** Phase 1: Création de l'arborescence des 7 bounded contexts Phase 2: Déplacement des règles métier (01-19) vers domains/*/rules/ Phase 3: Déplacement des diagrammes d'entités vers domains/*/entities/ Phase 4: Déplacement des diagrammes flux/états/séquences vers domains/*/ Phase 5: Création des README.md pour chaque domaine Phase 6: Déplacement des features Gherkin vers domains/*/features/ Phase 7: Création du Context Map (domains/README.md) Phase 8: Mise à jour de mkdocs.yml pour la nouvelle navigation Phase 9: Correction automatique des liens internes (script fix-markdown-links.sh) Phase 10: Nettoyage de l'ancienne structure (regles-metier/, diagrammes/, features/) **Configuration des tests:** - Makefile: godog run docs/domains/*/features/ - scripts/generate-bdd-docs.py: features_dir → docs/domains **Avantages:** ✅ Cohésion forte: toute la doc d'un domaine au même endroit ✅ Couplage faible: domaines indépendants, dépendances explicites ✅ Navigabilité améliorée: README par domaine = entrée claire ✅ Alignement code/docs: miroir de backend/internal/ ✅ Onboarding facilité: exploration domaine par domaine ✅ Tests BDD intégrés: features au plus près des règles métier Voir docs/REFACTOR-DDD.md pour le plan complet.
This commit is contained in:
@@ -0,0 +1,325 @@
|
||||
# language: fr
|
||||
|
||||
@api @search @geolocation @mvp
|
||||
Fonctionnalité: Recherche géographique de contenus avec Nominatim
|
||||
|
||||
En tant qu'utilisateur de RoadWave
|
||||
Je veux rechercher des contenus audio dans une zone géographique spécifique
|
||||
En utilisant un nom de lieu (ville, monument, région) et un rayon de recherche
|
||||
Afin de découvrir des contenus avant de me déplacer ou planifier un trajet
|
||||
|
||||
Contexte:
|
||||
Étant donné un utilisateur authentifié
|
||||
Et l'API Nominatim est accessible
|
||||
|
||||
# ============================================================================
|
||||
# GEOCODAGE AVEC NOMINATIM (lieu → coordonnées GPS)
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Recherche par nom de ville (cas simple)
|
||||
Étant donné l'utilisateur saisit "Paris" dans la recherche géographique
|
||||
Quand le système interroge Nominatim avec la requête "Paris, France"
|
||||
Alors Nominatim doit retourner les coordonnées :
|
||||
| lat | 48.8566 |
|
||||
| lon | 2.3522 |
|
||||
| type | city |
|
||||
| bbox | (48.815, 48.902, 2.224, 2.470) |
|
||||
Et le système doit utiliser ces coordonnées comme centre de recherche
|
||||
|
||||
Scénario: Recherche par monument ou POI (Point of Interest)
|
||||
Étant donné l'utilisateur saisit "Tour Eiffel"
|
||||
Quand le système interroge Nominatim
|
||||
Alors Nominatim doit retourner :
|
||||
| lat | 48.8584 |
|
||||
| lon | 2.2945 |
|
||||
| display_name| Tour Eiffel, Paris, France |
|
||||
| type | tourism |
|
||||
Et ces coordonnées doivent être utilisées pour la recherche de contenus
|
||||
|
||||
Scénario: Recherche avec ambiguïté (plusieurs résultats)
|
||||
Étant donné l'utilisateur saisit "Montmartre"
|
||||
Quand le système interroge Nominatim
|
||||
Alors plusieurs résultats doivent être retournés :
|
||||
| display_name | lat | lon |
|
||||
| Montmartre, Paris 18e, France | 48.8867 | 2.3431 |
|
||||
| Montmartre, Saskatchewan, Canada | 49.3000 | -106.0 |
|
||||
| Montmartre-de-Bretagne, Ille-et-Vilaine | 48.1867 | -1.1833|
|
||||
Et l'utilisateur doit choisir dans une liste déroulante
|
||||
Et le choix par défaut doit être le résultat français si détecté
|
||||
|
||||
Scénario: Recherche avec contexte géographique (biais local)
|
||||
Étant donné l'utilisateur est actuellement à Lyon (GPS actif)
|
||||
Et l'utilisateur saisit "Bellecour"
|
||||
Quand le système interroge Nominatim avec viewbox="Lyon area"
|
||||
Alors Nominatim doit prioriser les résultats proches de Lyon
|
||||
Et "Place Bellecour, Lyon" doit apparaître en premier
|
||||
Avant "Bellecour, Jura" ou d'autres homonymes
|
||||
|
||||
Scénario: Recherche par code postal
|
||||
Étant donné l'utilisateur saisit "75001"
|
||||
Quand le système interroge Nominatim
|
||||
Alors Nominatim doit retourner le centre du 1er arrondissement de Paris
|
||||
Et un rayon par défaut de 1km doit être appliqué
|
||||
|
||||
Scénario: Recherche invalide ou introuvable
|
||||
Étant donné l'utilisateur saisit "Azertyuiopqsdfghjklm"
|
||||
Quand le système interroge Nominatim
|
||||
Alors Nominatim doit retourner 0 résultat
|
||||
Et un message d'erreur doit être affiché :
|
||||
"""
|
||||
Aucun lieu trouvé pour "Azertyuiopqsdfghjklm".
|
||||
Essayez un nom de ville, monument ou adresse.
|
||||
"""
|
||||
|
||||
# ============================================================================
|
||||
# RAYON DE RECHERCHE CONFIGURABLE
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Recherche avec rayon par défaut (5 km)
|
||||
Étant donné l'utilisateur cherche "Louvre, Paris"
|
||||
Et Nominatim retourne les coordonnées (48.8606, 2.3376)
|
||||
Et aucun rayon n'est spécifié
|
||||
Quand le système effectue la recherche de contenus
|
||||
Alors un rayon par défaut de 5 km doit être appliqué
|
||||
Et une requête PostGIS doit être exécutée :
|
||||
"""
|
||||
SELECT * FROM contents
|
||||
WHERE ST_DWithin(location::geography, ST_MakePoint(2.3376, 48.8606)::geography, 5000)
|
||||
"""
|
||||
|
||||
Scénario: Recherche avec rayon personnalisé (slider 1-50 km)
|
||||
Étant donné l'utilisateur cherche "Lyon"
|
||||
Et l'utilisateur ajuste le slider de rayon à 15 km
|
||||
Quand le système effectue la recherche de contenus
|
||||
Alors une requête PostGIS avec rayon 15000m doit être exécutée
|
||||
Et tous les contenus dans un rayon de 15 km autour de Lyon doivent être retournés
|
||||
|
||||
Scénario: Rayon minimum (1 km) - recherche ultra locale
|
||||
Étant donné l'utilisateur cherche "Place de la Concorde"
|
||||
Et l'utilisateur définit le rayon à 1 km (minimum)
|
||||
Quand le système effectue la recherche
|
||||
Alors seuls les contenus très proches (<1 km) doivent être retournés
|
||||
Et le nombre de résultats peut être très faible (0-10)
|
||||
|
||||
Scénario: Rayon maximum (50 km) - recherche large
|
||||
Étant donné l'utilisateur cherche "Versailles"
|
||||
Et l'utilisateur définit le rayon à 50 km (maximum)
|
||||
Quand le système effectue la recherche
|
||||
Alors tous les contenus dans un rayon de 50 km doivent être retournés
|
||||
Et cela peut inclure Paris, banlieue et zones périphériques
|
||||
Et un avertissement "Résultats nombreux, affinez votre recherche" peut s'afficher si >500 résultats
|
||||
|
||||
Scénario: Mise à jour temps réel du rayon avec slider
|
||||
Étant donné l'utilisateur visualise les résultats pour "Bordeaux" avec rayon 10 km
|
||||
Quand l'utilisateur ajuste le slider de 10 km à 20 km
|
||||
Alors une nouvelle requête API doit être déclenchée automatiquement
|
||||
Et les résultats doivent être mis à jour en temps réel
|
||||
Et la carte (si affichée) doit ajuster le cercle de rayon
|
||||
|
||||
# ============================================================================
|
||||
# FILTRES COMBINÉS AVEC RECHERCHE GÉO
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Recherche géo + filtre catégorie
|
||||
Étant donné l'utilisateur cherche "Marseille" avec rayon 10 km
|
||||
Et l'utilisateur filtre par catégorie "Tourisme"
|
||||
Quand le système effectue la recherche
|
||||
Alors seuls les contenus touristiques dans le rayon doivent être retournés
|
||||
Et la requête SQL doit combiner :
|
||||
"""
|
||||
WHERE ST_DWithin(...) AND category_id IN (SELECT id FROM categories WHERE name = 'Tourisme')
|
||||
"""
|
||||
|
||||
Scénario: Recherche géo + filtre type de contenu
|
||||
Étant donné l'utilisateur cherche "Strasbourg" avec rayon 5 km
|
||||
Et l'utilisateur filtre par type "Audio-guides"
|
||||
Quand le système effectue la recherche
|
||||
Alors seuls les audio-guides dans le rayon doivent être retournés
|
||||
Et les podcasts et radios live doivent être exclus
|
||||
|
||||
Scénario: Recherche géo + filtre durée
|
||||
Étant donné l'utilisateur cherche "Nice" avec rayon 15 km
|
||||
Et l'utilisateur filtre par durée "Court (<10 min)"
|
||||
Quand le système effectue la recherche
|
||||
Alors seuls les contenus de <10 minutes dans le rayon doivent être retournés
|
||||
|
||||
Scénario: Recherche géo + mode Kids actif
|
||||
Étant donné l'utilisateur cherche "Disneyland Paris" avec rayon 5 km
|
||||
Et le mode Kids est activé (utilisateur 13-15 ans)
|
||||
Quand le système effectue la recherche
|
||||
Alors seuls les contenus "Tout public" doivent être retournés
|
||||
Et les contenus 16+ et 18+ doivent être exclus automatiquement
|
||||
|
||||
Scénario: Recherche géo + filtre politique désactivé
|
||||
Étant donné l'utilisateur cherche "Assemblée Nationale" avec rayon 2 km
|
||||
Et l'utilisateur a désactivé les contenus politiques dans ses préférences
|
||||
Quand le système effectue la recherche
|
||||
Alors les contenus taggés "politique" doivent être exclus
|
||||
Même s'ils sont géographiquement pertinents
|
||||
|
||||
# ============================================================================
|
||||
# AFFICHAGE DES RÉSULTATS
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Résultats triés par distance croissante
|
||||
Étant donné l'utilisateur cherche "Arc de Triomphe" avec rayon 3 km
|
||||
Quand les résultats sont retournés
|
||||
Alors ils doivent être triés par distance croissante :
|
||||
| contenu | distance |
|
||||
| Balade sur les Champs | 0.1 km |
|
||||
| Histoire de l'Arc | 0.2 km |
|
||||
| Quartier de l'Étoile | 0.5 km |
|
||||
| Secrets du 16e | 2.8 km |
|
||||
Et la distance doit être affichée pour chaque résultat
|
||||
|
||||
Scénario: Affichage carte avec marqueurs de contenus
|
||||
Étant donné l'utilisateur cherche "Toulouse" avec rayon 10 km
|
||||
Quand les résultats sont affichés en mode "Carte"
|
||||
Alors une carte Leaflet doit être affichée
|
||||
Et un marqueur doit être placé pour chaque contenu :
|
||||
| contenu | lat | lon | icone |
|
||||
| Capitole de Toulouse | 43.6045 | 1.4442 | pin-tourisme |
|
||||
| Bords de Garonne | 43.5986 | 1.4330 | pin-nature |
|
||||
Et un cercle représentant le rayon de 10 km doit être affiché
|
||||
Et le centre doit être le point géocodé de Toulouse
|
||||
|
||||
Scénario: Clustering de marqueurs si résultats nombreux
|
||||
Étant donné l'utilisateur cherche "Paris" avec rayon 20 km
|
||||
Et la recherche retourne 350 contenus
|
||||
Quand la carte est affichée
|
||||
Alors les marqueurs proches doivent être groupés en clusters :
|
||||
| cluster_center | nb_contenus |
|
||||
| Centre Paris | 120 |
|
||||
| La Défense | 45 |
|
||||
| Bois de Vincennes | 18 |
|
||||
Et en cliquant sur un cluster, le zoom doit s'approcher
|
||||
Et révéler les marqueurs individuels
|
||||
|
||||
Scénario: Affichage liste + carte simultanés (vue hybride)
|
||||
Étant donné l'utilisateur cherche "Nantes" avec rayon 8 km
|
||||
Quand l'utilisateur active la vue "Hybride"
|
||||
Alors la liste de résultats doit s'afficher à gauche (60% écran)
|
||||
Et la carte doit s'afficher à droite (40% écran)
|
||||
Et en cliquant sur un résultat dans la liste, le marqueur doit être highlighté sur la carte
|
||||
Et vice-versa
|
||||
|
||||
# ============================================================================
|
||||
# PERFORMANCES & OPTIMISATIONS
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Cache résultats recherche géo fréquente (Paris, Lyon, Marseille)
|
||||
Étant donné l'utilisateur cherche "Paris" avec rayon 5 km
|
||||
Quand la requête est exécutée pour la 1ère fois
|
||||
Alors les résultats doivent être mis en cache Redis pendant 10 minutes
|
||||
Et les requêtes suivantes identiques doivent utiliser le cache
|
||||
Et un header "X-Cache: HIT" doit être retourné
|
||||
|
||||
Scénario: Index PostGIS pour performances requêtes spatiales
|
||||
Étant donné la table "contents" contient 100 000 contenus
|
||||
Quand une recherche géo est exécutée avec ST_DWithin
|
||||
Alors un index GIST sur la colonne "location" doit être utilisé
|
||||
Et le temps de réponse doit être <100ms (p95)
|
||||
|
||||
Scénario: Pagination résultats nombreux
|
||||
Étant donné l'utilisateur cherche "Île-de-France" avec rayon 50 km
|
||||
Et la recherche retourne 1200 contenus
|
||||
Quand les résultats sont affichés
|
||||
Alors seuls les 50 premiers résultats doivent être chargés initialement
|
||||
Et un scroll infini doit charger les résultats suivants par batch de 50
|
||||
Et le total "1200 contenus trouvés" doit être affiché en haut
|
||||
|
||||
# ============================================================================
|
||||
# EDGE CASES & ERREURS
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Nominatim API indisponible (fallback)
|
||||
Étant donné l'utilisateur cherche "Lille"
|
||||
Quand l'API Nominatim retourne une erreur 503 (service unavailable)
|
||||
Alors un message d'erreur doit être affiché :
|
||||
"""
|
||||
Le service de recherche géographique est temporairement indisponible.
|
||||
Veuillez réessayer dans quelques instants.
|
||||
"""
|
||||
Et l'utilisateur doit pouvoir utiliser la recherche textuelle classique
|
||||
|
||||
Scénario: Recherche géo sans connexion Internet
|
||||
Étant donné l'utilisateur est en mode offline
|
||||
Quand l'utilisateur tente une recherche géographique
|
||||
Alors un message doit indiquer :
|
||||
"""
|
||||
La recherche géographique nécessite une connexion Internet.
|
||||
Vous pouvez parcourir les contenus téléchargés.
|
||||
"""
|
||||
|
||||
Scénario: Rate limiting Nominatim (1 req/s)
|
||||
Étant donné Nominatim impose un rate limit de 1 requête/seconde
|
||||
Et l'utilisateur tape rapidement "Par" → "Pari" → "Paris"
|
||||
Quand le système détecte plusieurs requêtes rapides
|
||||
Alors un debounce de 500ms doit être appliqué
|
||||
Et seule la dernière requête ("Paris") doit être envoyée à Nominatim
|
||||
|
||||
Scénario: Recherche avec caractères spéciaux ou injection SQL
|
||||
Étant donné l'utilisateur saisit "Paris'; DROP TABLE contents; --"
|
||||
Quand le système traite la requête
|
||||
Alors les caractères spéciaux doivent être échappés
|
||||
Et aucune injection SQL ne doit être possible
|
||||
Et Nominatim doit retourner 0 résultat (lieu invalide)
|
||||
|
||||
# ============================================================================
|
||||
# SAUVEGARDE DES RECHERCHES (max 5)
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Sauvegarde automatique d'une recherche géo
|
||||
Étant donné l'utilisateur effectue une recherche "Bordeaux" avec rayon 10 km
|
||||
Quand la recherche est validée
|
||||
Alors elle doit être sauvegardée dans l'historique :
|
||||
| search_query | Bordeaux |
|
||||
| radius_km | 10 |
|
||||
| lat | 44.8378 |
|
||||
| lon | -0.5792 |
|
||||
| timestamp | 2026-02-03 14:30:00 |
|
||||
Et l'utilisateur peut la rappeler via "Recherches récentes"
|
||||
|
||||
Scénario: Limite de 5 recherches sauvegardées (FIFO)
|
||||
Étant donné l'utilisateur a déjà 5 recherches sauvegardées
|
||||
Quand l'utilisateur effectue une 6ème recherche "Montpellier"
|
||||
Alors la recherche la plus ancienne doit être supprimée
|
||||
Et "Montpellier" doit être ajoutée en 1ère position
|
||||
Et la limite de 5 recherches doit être respectée
|
||||
|
||||
Scénario: Rejouer une recherche sauvegardée
|
||||
Étant donné l'utilisateur a une recherche sauvegardée "Lyon - 15 km"
|
||||
Quand l'utilisateur clique sur cette recherche dans l'historique
|
||||
Alors la recherche doit être ré-exécutée avec les mêmes paramètres
|
||||
Et les résultats actualisés doivent être affichés
|
||||
Et le rayon slider doit être positionné à 15 km
|
||||
|
||||
# ============================================================================
|
||||
# MÉTRIQUES & ANALYTICS
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Logging des recherches géographiques pour analytics
|
||||
Étant donné l'utilisateur effectue une recherche "Grenoble" avec rayon 12 km
|
||||
Quand la recherche est exécutée
|
||||
Alors un événement analytics doit être loggé :
|
||||
| event_type | geo_search_executed |
|
||||
| search_query | Grenoble |
|
||||
| radius_km | 12 |
|
||||
| results_count | 87 |
|
||||
| response_time_ms | 145 |
|
||||
| cache_hit | false |
|
||||
| user_id | user-123 |
|
||||
| timestamp | 2026-02-03 14:30:00 |
|
||||
Et ces données doivent alimenter le dashboard de monitoring
|
||||
|
||||
Scénario: Top recherches géographiques (analytics)
|
||||
Étant donné le système analyse les recherches sur 30 jours
|
||||
Quand le dashboard analytics est consulté
|
||||
Alors les lieux les plus recherchés doivent être affichés :
|
||||
| lieu | nb_recherches |
|
||||
| Paris | 12450 |
|
||||
| Lyon | 5632 |
|
||||
| Marseille | 4521 |
|
||||
| Toulouse | 3890 |
|
||||
| Nice | 3124 |
|
||||
Et ces données doivent guider la création de contenus ciblés
|
||||
Reference in New Issue
Block a user