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:
jpgiannetti
2026-02-07 17:15:02 +01:00
parent 78422bb2c0
commit 5e5fcf4714
227 changed files with 1413 additions and 1967 deletions

View File

@@ -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