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,234 @@
|
||||
# language: fr
|
||||
|
||||
@api @navigation @mode-detection @mvp
|
||||
Fonctionnalité: Basculement automatique entre modes voiture et piéton
|
||||
|
||||
En tant que système de navigation
|
||||
Je veux détecter automatiquement le mode de déplacement de l'utilisateur
|
||||
Et basculer entre mode voiture et mode piéton selon la vitesse GPS
|
||||
Afin d'optimiser l'expérience utilisateur sans action manuelle
|
||||
|
||||
Contexte:
|
||||
Étant donné un utilisateur authentifié avec l'application active
|
||||
Et la géolocalisation est activée et autorisée
|
||||
Et les permissions de géolocalisation en arrière-plan sont accordées
|
||||
|
||||
# ============================================================================
|
||||
# DÉTECTION INITIALE DU MODE AU DÉMARRAGE
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Démarrage application à vitesse piéton (0-4 km/h)
|
||||
Étant donné l'utilisateur ouvre l'application
|
||||
Et les 3 premières lectures GPS montrent des vitesses de [0, 2, 3] km/h
|
||||
Quand le système détermine le mode initial
|
||||
Alors le mode "piéton" doit être activé par défaut
|
||||
Et les notifications push géolocalisées doivent être activées
|
||||
Et un rayon de détection de 200m doit être appliqué
|
||||
|
||||
Scénario: Démarrage application en mouvement (≥5 km/h)
|
||||
Étant donné l'utilisateur ouvre l'application
|
||||
Et les 3 premières lectures GPS montrent des vitesses de [30, 35, 32] km/h
|
||||
Quand le système détermine le mode initial
|
||||
Alors le mode "voiture" doit être activé par défaut
|
||||
Et les notifications in-app avec compteur 7s doivent être activées
|
||||
Et l'ETA doit être calculé pour les contenus géolocalisés
|
||||
|
||||
Scénario: Démarrage avec GPS instable - mode par défaut
|
||||
Étant donné l'utilisateur ouvre l'application
|
||||
Et les lectures GPS sont erratiques : [0, 45, 2, 60, 1] km/h
|
||||
Quand le système ne peut pas déterminer le mode avec confiance
|
||||
Alors le mode "piéton" doit être activé par défaut (mode le plus sûr)
|
||||
Et une modal doit demander à l'utilisateur de confirmer son mode
|
||||
Et le choix utilisateur doit être mémorisé pour la session
|
||||
|
||||
# ============================================================================
|
||||
# BASCULEMENT VOITURE → PIÉTON (vitesse <5 km/h soutenue)
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Arrêt prolongé déclenche basculement piéton
|
||||
Étant donné l'utilisateur est en mode voiture
|
||||
Et la vitesse moyenne sur 2 minutes est de 2 km/h (embouteillage ou arrêt)
|
||||
Quand le système détecte cette condition pendant 2 minutes consécutives
|
||||
Alors le mode doit basculer automatiquement vers "piéton"
|
||||
Et une notification toast doit informer : "Mode piéton activé"
|
||||
Et les notifications push géolocalisées doivent être activées
|
||||
Et le cooldown voiture actif doit être annulé
|
||||
|
||||
Scénario: Stationnement confirmé (vitesse = 0 pendant 2 minutes)
|
||||
Étant donné l'utilisateur est en mode voiture
|
||||
Et la vitesse GPS est de 0 km/h pendant 2 minutes consécutives
|
||||
Et la précision GPS est <20m (pas de perte signal)
|
||||
Quand le système détecte l'arrêt prolongé
|
||||
Alors le mode doit basculer vers "piéton"
|
||||
Et une notification doit proposer : "Voulez-vous activer le mode piéton ?"
|
||||
Et si l'utilisateur ne répond pas, le basculement doit être automatique après 30 secondes
|
||||
|
||||
Scénario: Embouteillage prolongé (vitesse <5 km/h mais en mouvement)
|
||||
Étant donné l'utilisateur est en mode voiture à 35 km/h
|
||||
Et la vitesse chute à 3 km/h et oscille entre 0-4 km/h
|
||||
Et cette condition dure 3 minutes
|
||||
Quand le système détecte un mouvement résiduel (pas totalement arrêté)
|
||||
Alors le mode "voiture" doit être maintenu temporairement
|
||||
Mais le calcul ETA doit passer en mode "vitesse lente"
|
||||
Et après 5 minutes <5 km/h, le mode piéton doit être proposé
|
||||
|
||||
Scénario: Feu rouge ou arrêt temporaire ne déclenche PAS de basculement
|
||||
Étant donné l'utilisateur est en mode voiture à 50 km/h
|
||||
Et la vitesse chute à 0 km/h pendant 45 secondes (feu rouge)
|
||||
Quand le système évalue les conditions
|
||||
Alors le mode "voiture" doit être maintenu
|
||||
Car la durée <2 minutes (seuil de basculement)
|
||||
Et aucune notification ne doit être affichée
|
||||
|
||||
# ============================================================================
|
||||
# BASCULEMENT PIÉTON → VOITURE (vitesse ≥5 km/h soutenue)
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Démarrage voiture déclenche basculement automatique
|
||||
Étant donné l'utilisateur est en mode piéton
|
||||
Et la vitesse passe de 0 km/h à 15 km/h en 10 secondes
|
||||
Et la vitesse reste ≥10 km/h pendant 30 secondes
|
||||
Quand le système détecte une accélération soutenue
|
||||
Alors le mode doit basculer automatiquement vers "voiture"
|
||||
Et une notification toast doit informer : "Mode voiture activé"
|
||||
Et les notifications in-app avec ETA doivent être activées
|
||||
Et les notifications push piéton doivent être désactivées
|
||||
|
||||
Scénario: Course à pied ou vélo lent ne déclenche PAS de basculement voiture
|
||||
Étant donné l'utilisateur est en mode piéton
|
||||
Et la vitesse monte à 8 km/h et oscille entre 6-10 km/h
|
||||
Quand le système évalue les conditions
|
||||
Alors le mode "piéton" doit être maintenu
|
||||
Car la vitesse reste <15 km/h (seuil de confiance pour voiture)
|
||||
Et l'utilisateur peut basculer manuellement si nécessaire
|
||||
|
||||
Scénario: Trajet en bus ou vélo rapide (15-25 km/h)
|
||||
Étant donné l'utilisateur est en mode piéton
|
||||
Et la vitesse passe à 20 km/h et reste stable
|
||||
Quand le système détecte une vitesse ≥15 km/h pendant 1 minute
|
||||
Alors une notification doit proposer : "Passer en mode véhicule ?"
|
||||
Et si l'utilisateur accepte, basculer en mode "voiture"
|
||||
Et si l'utilisateur refuse, mémoriser le choix pour 30 minutes
|
||||
|
||||
Scénario: Sortie de transport en commun
|
||||
Étant donné l'utilisateur est en mode piéton
|
||||
Et la vitesse était de 0 km/h (dans le bus)
|
||||
Et la vitesse monte soudainement à 40 km/h (bus démarre)
|
||||
Puis retombe à 2 km/h après 5 minutes (descente du bus)
|
||||
Quand le système détecte ce pattern
|
||||
Alors le mode "piéton" doit être maintenu
|
||||
Et aucun basculement automatique ne doit être déclenché
|
||||
Car les transports en commun sont ambigus
|
||||
|
||||
# ============================================================================
|
||||
# BASCULEMENT AVEC AUDIO-GUIDE EN COURS
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Basculement voiture → piéton pendant audio-guide voiture
|
||||
Étant donné l'utilisateur écoute un audio-guide en mode voiture
|
||||
Et l'utilisateur se gare (vitesse = 0 km/h pendant 2 minutes)
|
||||
Quand le système bascule en mode piéton
|
||||
Alors l'audio-guide doit continuer en mode piéton
|
||||
Et les séquences restantes doivent s'adapter au mode piéton :
|
||||
| voiture | piéton |
|
||||
| Déclenchement automatique GPS | Navigation manuelle |
|
||||
| Distance affichée en mètres | Distance masquée |
|
||||
| Flèche direction | Pas de flèche |
|
||||
Et une notification doit informer : "Audio-guide adapté au mode piéton"
|
||||
|
||||
Scénario: Basculement piéton → voiture pendant audio-guide piéton
|
||||
Étant donné l'utilisateur écoute un audio-guide en mode piéton
|
||||
Et l'audio-guide est configuré pour "piéton uniquement"
|
||||
Et l'utilisateur monte en voiture (vitesse passe à 30 km/h)
|
||||
Quand le système bascule en mode voiture
|
||||
Alors l'audio-guide doit être mis en pause automatiquement
|
||||
Et une notification doit proposer : "Mettre l'audio-guide en pause ?"
|
||||
Et si confirmé, sauvegarder la progression pour reprise ultérieure
|
||||
|
||||
# ============================================================================
|
||||
# GESTION DES TRANSITIONS ET EDGE CASES
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Basculement rapide voiture → piéton → voiture (indécision)
|
||||
Étant donné l'utilisateur est en mode voiture
|
||||
Et la vitesse oscille : 40 km/h → 0 km/h (1 min) → 35 km/h → 0 km/h (1 min)
|
||||
Quand le système détecte une instabilité de mode
|
||||
Alors le mode "voiture" doit être maintenu par défaut
|
||||
Et un délai de stabilisation de 3 minutes doit être appliqué
|
||||
Et aucune notification de basculement ne doit être envoyée
|
||||
|
||||
Scénario: Tunnel ou perte GPS ne déclenche PAS de basculement
|
||||
Étant donné l'utilisateur est en mode voiture à 90 km/h
|
||||
Et le signal GPS est perdu pendant 2 minutes (tunnel)
|
||||
Quand le système détecte l'absence de signal GPS
|
||||
Alors le mode "voiture" doit être maintenu
|
||||
Et la dernière vitesse connue doit être utilisée
|
||||
Et aucun basculement ne doit être déclenché
|
||||
|
||||
Scénario: Utilisateur force le mode manuellement
|
||||
Étant donné l'utilisateur est en mode voiture automatique
|
||||
Et l'utilisateur bascule manuellement en mode piéton via l'interface
|
||||
Quand le basculement manuel est effectué
|
||||
Alors le mode manuel doit être prioritaire pendant 30 minutes
|
||||
Et les basculements automatiques doivent être désactivés pendant cette période
|
||||
Et un flag "manual_override" doit être loggé
|
||||
|
||||
Scénario: Reprise après override manuel
|
||||
Étant donné l'utilisateur a forcé le mode piéton il y a 35 minutes
|
||||
Et la vitesse actuelle est de 60 km/h depuis 5 minutes
|
||||
Quand la période d'override (30 min) expire
|
||||
Alors le système doit reprendre la détection automatique
|
||||
Et basculer en mode voiture car vitesse ≥5 km/h
|
||||
Et notifier l'utilisateur du basculement
|
||||
|
||||
# ============================================================================
|
||||
# IMPACT SUR LES AUTRES FONCTIONNALITÉS
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Basculement annule le cooldown notification voiture
|
||||
Étant donné l'utilisateur est en mode voiture
|
||||
Et un cooldown de 10 minutes est actif (notification ignorée)
|
||||
Quand le mode bascule vers piéton
|
||||
Alors le cooldown doit être immédiatement annulé
|
||||
Et les notifications piéton doivent être activées
|
||||
Car les mécanismes de notification sont différents entre modes
|
||||
|
||||
Scénario: Basculement adapte le rayon de détection géolocalisée
|
||||
Étant donné l'utilisateur est en mode voiture
|
||||
Et le rayon de détection pour contenus géolocalisés est adaptatif (basé sur vitesse)
|
||||
Quand le mode bascule vers piéton
|
||||
Alors le rayon de détection doit être fixé à 200m
|
||||
Et les contenus hors rayon doivent être retirés de la file d'attente
|
||||
Et une nouvelle recherche géospatiale doit être effectuée
|
||||
|
||||
Scénario: Quota notifications indépendant du mode
|
||||
Étant donné l'utilisateur a reçu 4 notifications en mode voiture (quota 4/6)
|
||||
Quand le mode bascule vers piéton
|
||||
Alors le compteur de quota doit être conservé (toujours 4/6)
|
||||
Car le quota de 6/heure s'applique globalement (tous modes confondus)
|
||||
|
||||
# ============================================================================
|
||||
# MÉTRIQUES & ANALYTICS
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Logging des basculements de mode pour analytics
|
||||
Étant donné l'utilisateur bascule de voiture à piéton
|
||||
Quand le basculement est effectué
|
||||
Alors un événement analytics doit être loggé :
|
||||
| event_type | mode_switch |
|
||||
| from_mode | car |
|
||||
| to_mode | pedestrian |
|
||||
| trigger | speed_threshold |
|
||||
| speed_kmh | 1.5 |
|
||||
| duration_previous_mode | 1800 |
|
||||
| manual_override | false |
|
||||
| timestamp | 2026-02-03 14:30:00 |
|
||||
Et ces métriques doivent alimenter le dashboard de monitoring
|
||||
|
||||
Scénario: Détection pattern utilisateur (majoritairement piéton vs voiture)
|
||||
Étant donné l'utilisateur utilise l'application depuis 30 jours
|
||||
Et 80% du temps d'écoute est en mode piéton
|
||||
Quand le système analyse le comportement
|
||||
Alors un flag "primary_mode: pedestrian" doit être défini
|
||||
Et le mode par défaut au démarrage doit être "piéton"
|
||||
Et l'algorithme de recommandation doit favoriser les audio-guides piétons
|
||||
@@ -0,0 +1,56 @@
|
||||
# language: fr
|
||||
|
||||
@api @navigation @transitions @mvp
|
||||
Fonctionnalité: Décompte 5 secondes pour transitions séquences
|
||||
|
||||
En tant qu'utilisateur
|
||||
Je veux un décompte avant le début d'une nouvelle séquence
|
||||
Afin de me préparer mentalement à l'écoute du nouveau contenu
|
||||
|
||||
Scénario: Décompte visuel et audio avant nouvelle séquence
|
||||
Étant donné un utilisateur "alice@roadwave.fr" qui termine une séquence
|
||||
Quand la suivante est sur le point de démarrer
|
||||
Alors un décompte de 5 secondes est affiché: "5... 4... 3... 2... 1..."
|
||||
Et un son subtil accompagne chaque seconde
|
||||
Et un événement "TRANSITION_COUNTDOWN_STARTED" est enregistré
|
||||
|
||||
Scénario: Possibilité de skip le décompte
|
||||
Étant donné un utilisateur "bob@roadwave.fr" pendant un décompte
|
||||
Quand il appuie sur "Passer"
|
||||
Alors la séquence suivante démarre immédiatement
|
||||
Et le décompte est interrompu
|
||||
Et un événement "TRANSITION_COUNTDOWN_SKIPPED" est enregistré
|
||||
|
||||
Scénario: Annulation du décompte si utilisateur s'éloigne
|
||||
Étant donné un utilisateur "charlie@roadwave.fr" avec décompte en cours
|
||||
Quand il s'éloigne du point d'intérêt pendant le décompte
|
||||
Alors le décompte est annulé
|
||||
Et la séquence ne démarre pas
|
||||
Et un événement "TRANSITION_COUNTDOWN_CANCELLED" est enregistré
|
||||
|
||||
Scénario: Prévisualisation du prochain point pendant le décompte
|
||||
Étant donné un utilisateur "david@roadwave.fr" pendant un décompte
|
||||
Alors il voit une carte de prévisualisation:
|
||||
| Élément | Contenu |
|
||||
| Nom de la séquence| Panthéon |
|
||||
| Durée | 8 min 30s |
|
||||
| Distance | Vous y êtes |
|
||||
| Image | Photo du Panthéon |
|
||||
Et un événement "TRANSITION_PREVIEW_DISPLAYED" est enregistré
|
||||
|
||||
Scénario: Désactivation du décompte dans les paramètres
|
||||
Étant donné un utilisateur "eve@roadwave.fr"
|
||||
Quand elle désactive "Décompte de transition" dans les paramètres
|
||||
Alors les séquences démarrent immédiatement
|
||||
Sans décompte de 5 secondes
|
||||
Et un événement "TRANSITION_COUNTDOWN_DISABLED" est enregistré
|
||||
|
||||
Scénario: Métriques d'utilisation du décompte
|
||||
Étant donné que 10 000 transitions ont eu lieu
|
||||
Alors les indicateurs suivants sont disponibles:
|
||||
| Métrique | Valeur |
|
||||
| Taux de skip du décompte | 25% |
|
||||
| Taux de complétion du décompte| 70% |
|
||||
| Taux d'annulation | 5% |
|
||||
| Satisfaction utilisateur | 4.2/5 |
|
||||
Et les métriques sont exportées vers le monitoring
|
||||
163
docs/domains/content/features/navigation/eta-calculation.feature
Normal file
163
docs/domains/content/features/navigation/eta-calculation.feature
Normal file
@@ -0,0 +1,163 @@
|
||||
# language: fr
|
||||
|
||||
@api @navigation @geolocation @mvp
|
||||
Fonctionnalité: Calcul ETA et notification contenus géolocalisés
|
||||
|
||||
En tant que système de recommandation
|
||||
Je veux calculer le temps d'arrivée estimé (ETA) pour les contenus géolocalisés
|
||||
Afin de déclencher la notification au bon moment (7 secondes avant)
|
||||
|
||||
Contexte:
|
||||
Étant donné un utilisateur authentifié en mode voiture
|
||||
Et la géolocalisation est activée et autorisée
|
||||
|
||||
# ============================================================================
|
||||
# CALCUL ETA VITESSE NORMALE (≥5 km/h)
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Calcul ETA avec vitesse constante 50 km/h
|
||||
Étant donné un contenu géolocalisé situé à 300m de l'utilisateur
|
||||
Et l'utilisateur se déplace à 50 km/h en direction du point
|
||||
Quand le système calcule l'ETA
|
||||
Alors l'ETA estimé doit être d'environ 21 secondes
|
||||
Et la notification doit être déclenchée dans 14 secondes (21s - 7s)
|
||||
|
||||
Scénario: Recalcul ETA en temps réel avec variation de vitesse
|
||||
Étant donné un contenu géolocalisé situé à 500m
|
||||
Et l'utilisateur se déplace initialement à 70 km/h
|
||||
Et l'ETA initial est de 25 secondes
|
||||
Quand l'utilisateur ralentit à 30 km/h
|
||||
Alors l'ETA doit être recalculé à 60 secondes
|
||||
Et le délai de notification doit être ajusté en conséquence
|
||||
|
||||
Scénario: Notification déclenchée exactement 7 secondes avant l'arrivée
|
||||
Étant donné un contenu géolocalisé situé à 150m
|
||||
Et l'utilisateur se déplace à 60 km/h
|
||||
Et l'ETA calculé est de 9 secondes
|
||||
Quand l'ETA atteint 7 secondes
|
||||
Alors une notification push doit être envoyée
|
||||
Et la notification doit contenir un compteur visuel de 7 à 1
|
||||
Et un son de notification doit être joué
|
||||
|
||||
Scénario: Pas de notification si l'utilisateur dévie de la trajectoire
|
||||
Étant donné un contenu géolocalisé situé à 200m au nord
|
||||
Et l'utilisateur se déplace à 50 km/h en direction du contenu
|
||||
Et l'ETA est de 14 secondes
|
||||
Quand l'utilisateur change de direction vers l'est
|
||||
Et la distance au contenu commence à augmenter
|
||||
Alors la notification ne doit pas être déclenchée
|
||||
Et l'ETA doit être invalidé
|
||||
|
||||
# ============================================================================
|
||||
# CAS VITESSE LENTE (<5 km/h)
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Notification immédiate si vitesse <5 km/h et distance <50m
|
||||
Étant donné un contenu géolocalisé situé à 30m
|
||||
Et l'utilisateur se déplace à 3 km/h (piéton ou embouteillage)
|
||||
Quand le système détecte la vitesse <5 km/h
|
||||
Alors la notification doit être déclenchée immédiatement
|
||||
Et le message doit indiquer "Contenu disponible à proximité"
|
||||
|
||||
Scénario: Pas de notification si vitesse <5 km/h mais distance >50m
|
||||
Étant donné un contenu géolocalisé situé à 80m
|
||||
Et l'utilisateur se déplace à 4 km/h
|
||||
Quand le système évalue les conditions de notification
|
||||
Alors aucune notification ne doit être envoyée
|
||||
Et le système doit attendre que la distance soit <50m ou vitesse ≥5 km/h
|
||||
|
||||
Scénario: Basculement vitesse normale → lente avec recalcul
|
||||
Étant donné un contenu géolocalisé situé à 100m
|
||||
Et l'utilisateur se déplace à 40 km/h (ETA = 9s, notification dans 2s)
|
||||
Quand l'utilisateur ralentit brutalement à 2 km/h (embouteillage)
|
||||
Et la vitesse reste <5 km/h pendant 5 secondes
|
||||
Et la distance est maintenant 85m
|
||||
Alors le mode de calcul ETA doit basculer en "vitesse lente"
|
||||
Et la notification immédiate ne doit pas être envoyée (distance >50m)
|
||||
|
||||
Scénario: Notification immédiate en approche lente continue
|
||||
Étant donné un contenu géolocalisé situé à 60m
|
||||
Et l'utilisateur se déplace à 3 km/h
|
||||
Quand l'utilisateur atteint 45m du point (distance <50m)
|
||||
Alors la notification doit être déclenchée immédiatement
|
||||
Et le compteur visuel doit afficher "À proximité"
|
||||
|
||||
# ============================================================================
|
||||
# GESTION TRAJECTOIRES COMPLEXES
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Route sinueuse avec distance GPS ≠ distance réelle
|
||||
Étant donné un contenu géolocalisé situé à 250m à vol d'oiseau
|
||||
Mais la route sinueuse fait 400m de trajet réel
|
||||
Et l'utilisateur se déplace à 50 km/h
|
||||
Quand le système calcule l'ETA
|
||||
Alors l'ETA doit utiliser la distance GPS (250m) par défaut
|
||||
Et l'ETA estimé doit être d'environ 18 secondes
|
||||
Et une marge d'erreur de ±3 secondes doit être tolérée
|
||||
|
||||
Scénario: Contenu sur autoroute parallèle non accessible
|
||||
Étant donné un contenu géolocalisé sur une autoroute parallèle à 100m
|
||||
Et l'utilisateur roule sur une route secondaire sans accès direct
|
||||
Et la distance GPS est de 100m mais l'accès réel est à 2 km
|
||||
Quand le système détecte que l'utilisateur s'éloigne après 10 secondes
|
||||
Alors la notification ne doit pas être déclenchée
|
||||
Et le contenu doit être retiré de la file d'attente
|
||||
|
||||
# ============================================================================
|
||||
# EDGE CASES & ERREURS
|
||||
# ============================================================================
|
||||
|
||||
Scénario: GPS imprécis (précision >50m)
|
||||
Étant donné un contenu géolocalisé situé à 150m
|
||||
Et la précision GPS actuelle est de 65m
|
||||
Quand le système calcule l'ETA
|
||||
Alors l'ETA doit être calculé avec une marge d'erreur élevée
|
||||
Et la notification doit être différée de 2 secondes supplémentaires
|
||||
Et un flag "low_accuracy" doit être ajouté aux métriques
|
||||
|
||||
Scénario: Vitesse nulle (stationnement) avec contenu proche
|
||||
Étant donné un contenu géolocalisé situé à 25m
|
||||
Et l'utilisateur est à l'arrêt (vitesse = 0 km/h) depuis 10 secondes
|
||||
Quand le système évalue les conditions
|
||||
Alors aucune notification ne doit être envoyée automatiquement
|
||||
Et le contenu doit être affiché dans la file d'attente manuelle
|
||||
Et l'utilisateur peut le sélectionner manuellement
|
||||
|
||||
Scénario: Perte signal GPS pendant le calcul ETA
|
||||
Étant donné un contenu géolocalisé avec ETA de 12 secondes
|
||||
Et une notification prévue dans 5 secondes
|
||||
Quand le signal GPS est perdu (tunnel)
|
||||
Alors le calcul ETA doit être gelé à la dernière valeur connue
|
||||
Et la notification doit être déclenchée selon l'ETA gelé
|
||||
Et un fallback de 10 secondes max doit être appliqué
|
||||
|
||||
Scénario: Vitesse erratique (GPS instable)
|
||||
Étant donné un contenu géolocalisé situé à 200m
|
||||
Et les lectures GPS montrent : 50 km/h → 5 km/h → 60 km/h → 10 km/h en 10 secondes
|
||||
Quand le système détecte une instabilité GPS
|
||||
Alors l'ETA doit être calculé avec une moyenne mobile sur 5 secondes
|
||||
Et la notification ne doit être déclenchée qu'avec une confiance >70%
|
||||
|
||||
# ============================================================================
|
||||
# MÉTRIQUES & LOGS
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Logging des calculs ETA pour analytics
|
||||
Étant donné un contenu géolocalisé avec notification déclenchée
|
||||
Quand la notification est envoyée
|
||||
Alors un événement analytics doit être loggé avec :
|
||||
| distance_meters | 180 |
|
||||
| speed_kmh | 55 |
|
||||
| eta_seconds | 11.7 |
|
||||
| notification_offset | 7 |
|
||||
| gps_accuracy_meters | 12 |
|
||||
| calculation_method | standard_eta |
|
||||
Et la timestamp de notification doit être enregistrée
|
||||
|
||||
Scénario: Comparaison ETA prédit vs ETA réel
|
||||
Étant donné un contenu géolocalisé avec ETA prédit de 15 secondes
|
||||
Et une notification déclenchée à 8 secondes avant
|
||||
Quand l'utilisateur atteint réellement le point
|
||||
Alors l'ETA réel doit être calculé et comparé
|
||||
Et l'écart doit être loggé pour amélioration de l'algorithme
|
||||
Et si l'écart >5 secondes, un flag "prediction_error" doit être levé
|
||||
314
docs/domains/content/features/navigation/file-attente.feature
Normal file
314
docs/domains/content/features/navigation/file-attente.feature
Normal file
@@ -0,0 +1,314 @@
|
||||
# language: fr
|
||||
Fonctionnalité: API - File d'attente et pré-calcul des contenus
|
||||
En tant qu'API backend
|
||||
Je veux pré-calculer et gérer la file d'attente de contenus
|
||||
Afin d'assurer une navigation fluide sans latence
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est disponible
|
||||
Et que Redis est accessible
|
||||
Et que PostgreSQL avec PostGIS est accessible
|
||||
Et qu'un utilisateur "user123" existe avec token JWT valide
|
||||
|
||||
# Pré-calcul initial
|
||||
|
||||
Scénario: API pré-calcule 5 contenus au démarrage de session
|
||||
Étant donné que l'utilisateur "user123" démarre une session
|
||||
Et qu'il est situé à Paris (48.8566, 2.3522)
|
||||
Et qu'il est en mode voiture (vitesse ≥ 5 km/h)
|
||||
Quand je POST /api/v1/queue/initialize
|
||||
"""json
|
||||
{
|
||||
"user_id": "user123",
|
||||
"latitude": 48.8566,
|
||||
"longitude": 2.3522,
|
||||
"mode": "voiture"
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 201
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"queue_size": 5,
|
||||
"contents": [
|
||||
{"id": "content1", "title": "...", "position": 1},
|
||||
{"id": "content2", "title": "...", "position": 2},
|
||||
{"id": "content3", "title": "...", "position": 3},
|
||||
{"id": "content4", "title": "...", "position": 4},
|
||||
{"id": "content5", "title": "...", "position": 5}
|
||||
]
|
||||
}
|
||||
"""
|
||||
Et en Redis, la clé "user:user123:queue" contient 5 contenus
|
||||
Et les métadonnées incluent:
|
||||
| champ | valeur |
|
||||
| last_lat | 48.8566 |
|
||||
| last_lon | 2.3522 |
|
||||
| mode | voiture |
|
||||
| computed_at | (timestamp actuel) |
|
||||
Et le TTL est de 900 secondes (15 minutes)
|
||||
|
||||
Scénario: API GET retourne la file d'attente en cache
|
||||
Étant donné qu'une file de 5 contenus existe en cache Redis pour "user123"
|
||||
Quand je GET /api/v1/queue
|
||||
Alors le statut de réponse est 200
|
||||
Et la réponse contient les 5 contenus pré-calculés
|
||||
Et la latence de réponse est < 50ms (lecture Redis)
|
||||
|
||||
Scénario: API retire un contenu de la file après lecture
|
||||
Étant donné qu'une file de 5 contenus [C1, C2, C3, C4, C5] existe pour "user123"
|
||||
Quand je POST /api/v1/queue/consume
|
||||
"""json
|
||||
{
|
||||
"user_id": "user123",
|
||||
"content_id": "C1"
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 200
|
||||
Et la file d'attente devient [C2, C3, C4, C5]
|
||||
Et la taille de la file est 4
|
||||
|
||||
# Recalcul automatique
|
||||
|
||||
Scénario: API recalcule après déplacement >10km
|
||||
Étant donné qu'une file a été calculée à Paris (48.8566, 2.3522)
|
||||
Et que l'utilisateur se déplace à Versailles (48.8049, 2.1204) soit 12km
|
||||
Quand je POST /api/v1/queue/update-location
|
||||
"""json
|
||||
{
|
||||
"user_id": "user123",
|
||||
"latitude": 48.8049,
|
||||
"longitude": 2.1204
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 200
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"queue_invalidated": true,
|
||||
"reason": "distance_threshold_exceeded",
|
||||
"distance_km": 12.1,
|
||||
"new_queue_size": 5
|
||||
}
|
||||
"""
|
||||
Et la nouvelle file est basée sur la position Versailles
|
||||
Et l'ancienne file a été supprimée de Redis
|
||||
|
||||
Scénario: API ne recalcule pas si déplacement ≤10km
|
||||
Étant donné qu'une file a été calculée à Paris (48.8566, 2.3522)
|
||||
Et que l'utilisateur se déplace de 8 km
|
||||
Quand je POST /api/v1/queue/update-location
|
||||
"""json
|
||||
{
|
||||
"user_id": "user123",
|
||||
"latitude": 48.8500,
|
||||
"longitude": 2.3600
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 200
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"queue_invalidated": false,
|
||||
"distance_km": 8.2,
|
||||
"threshold": 10
|
||||
}
|
||||
"""
|
||||
Et la file en cache reste inchangée
|
||||
|
||||
Scénario: API recalcule après 10 minutes
|
||||
Étant donné qu'une file a été calculée à 10:00:00
|
||||
Et que l'heure actuelle est 10:10:01
|
||||
Quand je GET /api/v1/queue
|
||||
Alors le statut de réponse est 200
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"queue_invalidated": true,
|
||||
"reason": "time_threshold_exceeded",
|
||||
"elapsed_minutes": 10,
|
||||
"new_queue_size": 5
|
||||
}
|
||||
"""
|
||||
Et une nouvelle file de 5 contenus est recalculée
|
||||
Et le timestamp "computed_at" est mis à jour
|
||||
|
||||
Scénario: API recalcule quand il reste <3 contenus
|
||||
Étant donné qu'il reste 3 contenus [C3, C4, C5] dans la file
|
||||
Quand je POST /api/v1/queue/consume (consomme C3)
|
||||
Alors le statut de réponse est 200
|
||||
Et la file devient [C4, C5]
|
||||
Et un recalcul asynchrone est déclenché
|
||||
Et 3 nouveaux contenus [C6, C7, C8] sont ajoutés
|
||||
Et la file finale est [C4, C5, C6, C7, C8]
|
||||
|
||||
# Invalidation immédiate
|
||||
|
||||
Scénario: API invalide après modification préférences utilisateur
|
||||
Étant donné qu'une file de 5 contenus existe pour "user123"
|
||||
Et que l'utilisateur est en mode piéton (vitesse < 5 km/h)
|
||||
Quand je PUT /api/v1/users/user123/preferences
|
||||
"""json
|
||||
{
|
||||
"geo_radius": 20,
|
||||
"discovery_factor": 0.7,
|
||||
"political_content": false
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 200
|
||||
Et la file d'attente est invalidée immédiatement
|
||||
Et une nouvelle file est recalculée avec les nouvelles préférences
|
||||
Et en Redis, l'ancienne file a été supprimée
|
||||
|
||||
Scénario: API refuse modification préférences si vitesse >10 km/h
|
||||
Étant donné que l'utilisateur roule à 50 km/h
|
||||
Quand je PUT /api/v1/users/user123/preferences
|
||||
"""json
|
||||
{
|
||||
"geo_radius": 30
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 403
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"error": "MODIFICATION_BLOCKED_WHILE_DRIVING",
|
||||
"message": "Modification des préférences interdite en conduite (vitesse > 10 km/h)",
|
||||
"current_speed_kmh": 50
|
||||
}
|
||||
"""
|
||||
Et les préférences ne sont pas modifiées
|
||||
|
||||
Scénario: API invalide après démarrage live d'un créateur suivi
|
||||
Étant donné que l'utilisateur "user123" suit le créateur "creator456"
|
||||
Et qu'une file de 5 contenus existe
|
||||
Et que l'utilisateur est dans la zone du créateur
|
||||
Quand le créateur "creator456" démarre un live
|
||||
Alors une notification push est envoyée à "user123"
|
||||
Et la file d'attente est recalculée
|
||||
Et le contenu live est inséré en tête de file
|
||||
Et la nouvelle file commence par le live
|
||||
|
||||
# Métadonnées et persistence
|
||||
|
||||
Scénario: API stocke métadonnées complètes en Redis
|
||||
Étant donné qu'une file est calculée à 10:30:00
|
||||
Quand je consulte Redis avec la clé "user:user123:queue"
|
||||
Alors la structure est:
|
||||
"""json
|
||||
{
|
||||
"contents": [
|
||||
{"id": "C1", "position": 1},
|
||||
{"id": "C2", "position": 2},
|
||||
{"id": "C3", "position": 3},
|
||||
{"id": "C4", "position": 4},
|
||||
{"id": "C5", "position": 5}
|
||||
],
|
||||
"metadata": {
|
||||
"last_lat": 48.8566,
|
||||
"last_lon": 2.3522,
|
||||
"computed_at": "2026-02-02T10:30:00Z",
|
||||
"mode": "voiture"
|
||||
}
|
||||
}
|
||||
"""
|
||||
Et le TTL est exactement 900 secondes
|
||||
|
||||
Scénario: API calcule distance avec PostGIS
|
||||
Étant donné que l'ancienne position est Paris (48.8566, 2.3522)
|
||||
Et que la nouvelle position est Versailles (48.8049, 2.1204)
|
||||
Quand l'API calcule la distance
|
||||
Alors la requête SQL utilise:
|
||||
"""sql
|
||||
SELECT ST_Distance(
|
||||
ST_MakePoint(2.3522, 48.8566)::geography,
|
||||
ST_MakePoint(2.1204, 48.8049)::geography
|
||||
) / 1000 AS distance_km
|
||||
"""
|
||||
Et le résultat est 12.1 km
|
||||
|
||||
# Gestion erreurs
|
||||
|
||||
Scénario: API gère échec Redis gracieusement
|
||||
Étant donné que Redis est indisponible
|
||||
Quand je GET /api/v1/queue
|
||||
Alors le statut de réponse est 503
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"error": "CACHE_UNAVAILABLE",
|
||||
"message": "Service de cache temporairement indisponible",
|
||||
"fallback": "Calcul direct sans cache"
|
||||
}
|
||||
"""
|
||||
Et une nouvelle file est calculée directement depuis PostgreSQL
|
||||
|
||||
Scénario: API gère aucun contenu disponible
|
||||
Étant donné qu'aucun contenu n'existe dans la zone de l'utilisateur
|
||||
Quand je POST /api/v1/queue/initialize
|
||||
Alors le statut de réponse est 200
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"queue_size": 0,
|
||||
"contents": [],
|
||||
"message": "Aucun contenu disponible dans cette zone",
|
||||
"suggested_action": "expand_radius"
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: API élargit la zone de recherche si aucun contenu
|
||||
Étant donné qu'aucun contenu n'existe dans un rayon de 20km
|
||||
Quand je POST /api/v1/queue/expand-radius
|
||||
"""json
|
||||
{
|
||||
"user_id": "user123",
|
||||
"additional_radius_km": 50
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 200
|
||||
Et la recherche utilise un rayon de 70km (20 + 50)
|
||||
Et une nouvelle file est calculée avec ce rayon
|
||||
|
||||
# Performance
|
||||
|
||||
Scénario: API répond en <100ms pour lecture cache
|
||||
Étant donné qu'une file existe en Redis
|
||||
Quand je GET /api/v1/queue à 10:30:00.000
|
||||
Alors la réponse est reçue à 10:30:00.050 (50ms)
|
||||
Et la latence est < 100ms
|
||||
|
||||
Scénario: API recalcule en arrière-plan sans bloquer
|
||||
Étant donné qu'il reste 2 contenus dans la file
|
||||
Quand je POST /api/v1/queue/consume
|
||||
Alors le statut de réponse est 200 (immédiat)
|
||||
Et la réponse est retournée en < 100ms
|
||||
Et le recalcul asynchrone démarre en parallèle
|
||||
Et le client ne perçoit aucune latence
|
||||
|
||||
Plan du Scénario: Conditions de recalcul selon distance
|
||||
Étant donné qu'une file a été calculée à une position donnée
|
||||
Quand l'utilisateur se déplace de <distance> km
|
||||
Alors la file est <action>
|
||||
|
||||
Exemples:
|
||||
| distance | action |
|
||||
| 5 | conservée |
|
||||
| 9.9 | conservée |
|
||||
| 10.0 | conservée |
|
||||
| 10.1 | invalidée et recalculée |
|
||||
| 15 | invalidée et recalculée |
|
||||
| 50 | invalidée et recalculée |
|
||||
|
||||
Plan du Scénario: Conditions de recalcul selon temps écoulé
|
||||
Étant donné qu'une file a été calculée il y a <temps> minutes
|
||||
Quand je consulte la file
|
||||
Alors la file est <action>
|
||||
|
||||
Exemples:
|
||||
| temps | action |
|
||||
| 5 | conservée |
|
||||
| 9 | conservée |
|
||||
| 10 | invalidée et recalculée |
|
||||
| 15 | invalidée et recalculée |
|
||||
| 60 | invalidée et recalculée |
|
||||
@@ -0,0 +1,58 @@
|
||||
# language: fr
|
||||
|
||||
@api @navigation @history @mvp
|
||||
Fonctionnalité: Historique géolocalisé des contenus écoutés
|
||||
|
||||
En tant qu'utilisateur
|
||||
Je veux consulter l'historique de mes écoutes avec localisation
|
||||
Afin de me souvenir de mes découvertes et parcours
|
||||
|
||||
Scénario: Enregistrement automatique de l'historique
|
||||
Étant donné un utilisateur "alice@roadwave.fr" qui écoute un contenu
|
||||
Quand l'écoute est terminée
|
||||
Alors l'historique enregistre:
|
||||
| Donnée | Exemple |
|
||||
| Contenu | Audio-guide Quartier Latin |
|
||||
| Date/heure | 2026-02-03 14:30 |
|
||||
| Position GPS | 48.8534, 2.3488 |
|
||||
| Durée d'écoute | 42 min |
|
||||
Et un événement "HISTORY_ENTRY_CREATED" est enregistré
|
||||
|
||||
Scénario: Visualisation de l'historique sur une carte
|
||||
Étant donné un utilisateur "bob@roadwave.fr"
|
||||
Quand il accède à "Mon historique"
|
||||
Alors une carte affiche tous les points écoutés
|
||||
Et chaque marqueur est cliquable pour voir les détails
|
||||
Et un événement "HISTORY_MAP_VIEWED" est enregistré
|
||||
|
||||
Scénario: Filtrage de l'historique par période
|
||||
Étant donné un utilisateur "charlie@roadwave.fr"
|
||||
Quand il filtre par "Ce mois-ci"
|
||||
Alors seuls les contenus du mois courant sont affichés
|
||||
Et un compteur indique: "23 contenus écoutés ce mois"
|
||||
Et un événement "HISTORY_FILTERED" est enregistré
|
||||
|
||||
Scénario: Export de l'historique pour souvenirs
|
||||
Étant donné un utilisateur "david@roadwave.fr"
|
||||
Quand il exporte son historique
|
||||
Alors il reçoit un fichier GPX avec tous ses parcours
|
||||
Et peut l'importer dans d'autres applications
|
||||
Et un événement "HISTORY_EXPORTED" est enregistré
|
||||
|
||||
Scénario: Suppression d'entrées d'historique
|
||||
Étant donné un utilisateur "eve@roadwave.fr"
|
||||
Quand elle supprime une entrée
|
||||
Alors elle est retirée de l'historique
|
||||
Et ne compte plus dans les statistiques
|
||||
Et un événement "HISTORY_ENTRY_DELETED" est enregistré
|
||||
|
||||
Scénario: Statistiques annuelles basées sur l'historique
|
||||
Étant donné un utilisateur "frank@roadwave.fr" en fin d'année
|
||||
Alors il voit son "Rétrospective RoadWave 2026":
|
||||
| Métrique | Valeur |
|
||||
| Contenus écoutés | 142 |
|
||||
| Distance parcourue | 523 km |
|
||||
| Villes visitées | 18 |
|
||||
| Pays visités | 3 |
|
||||
| Top catégorie | Tourisme |
|
||||
Et un événement "YEARLY_RETROSPECTIVE_VIEWED" est enregistré
|
||||
@@ -0,0 +1,52 @@
|
||||
# language: fr
|
||||
|
||||
@api @navigation @parking @mvp
|
||||
Fonctionnalité: Mode stationnement et pause automatique
|
||||
|
||||
En tant qu'utilisateur conducteur
|
||||
Je veux que l'application détecte quand je me gare
|
||||
Afin d'adapter l'expérience et proposer la suite à pied
|
||||
|
||||
Scénario: Détection automatique du stationnement
|
||||
Étant donné un utilisateur "alice@roadwave.fr" en mode voiture
|
||||
Quand la vitesse passe à 0 km/h pendant plus de 2 minutes
|
||||
Et le Bluetooth CarPlay se déconnecte
|
||||
Alors le mode "Stationnement" est activé
|
||||
Et un événement "PARKING_MODE_DETECTED" est enregistré
|
||||
|
||||
Scénario: Proposition de basculement en mode piéton
|
||||
Étant donné un utilisateur "bob@roadwave.fr" en mode stationnement
|
||||
Quand il sort de la voiture (détecté par capteurs)
|
||||
Alors une notification propose: "Continuer à pied ?"
|
||||
Et deux options: [Mode piéton] [Rester en voiture]
|
||||
Et un événement "PEDESTRIAN_MODE_SUGGESTED" est enregistré
|
||||
|
||||
Scénario: Mémorisation de la position de stationnement
|
||||
Étant donné un utilisateur "charlie@roadwave.fr" qui se gare
|
||||
Quand le mode stationnement est activé
|
||||
Alors la position GPS est sauvegardée
|
||||
Et un marqueur "Votre voiture" est placé sur la carte
|
||||
Et un événement "PARKING_LOCATION_SAVED" est enregistré
|
||||
|
||||
Scénario: Navigation retour vers la voiture
|
||||
Étant donné un utilisateur "david@roadwave.fr" qui a fini sa visite à pied
|
||||
Quand il clique sur "Retour à ma voiture"
|
||||
Alors un itinéraire piéton est calculé
|
||||
Et la distance est affichée: "650m - 8 min"
|
||||
Et un événement "NAVIGATION_TO_PARKING_STARTED" est enregistré
|
||||
|
||||
Scénario: Alerte d'expiration du stationnement payant
|
||||
Étant donné un utilisateur "eve@roadwave.fr" en stationnement
|
||||
Et elle a configuré une durée de stationnement: 2 heures
|
||||
Quand il reste 10 minutes
|
||||
Alors une notification push est envoyée: "Stationnement expire dans 10 min"
|
||||
Et un événement "PARKING_EXPIRY_WARNING" est enregistré
|
||||
|
||||
Scénario: Statistiques de stationnement
|
||||
Étant donné un utilisateur "frank@roadwave.fr"
|
||||
Alors il peut voir dans ses stats:
|
||||
| Métrique | Valeur |
|
||||
| Nombre de stationnements | 45 |
|
||||
| Durée moyenne | 1h 30min |
|
||||
| Distance voiture-POI moyen | 320m |
|
||||
Et un événement "PARKING_STATS_VIEWED" est enregistré
|
||||
@@ -0,0 +1,76 @@
|
||||
# language: fr
|
||||
|
||||
@api @navigation @car-mode @ui @mvp
|
||||
Fonctionnalité: Notifications minimalistes en mode voiture
|
||||
|
||||
En tant qu'utilisateur conducteur
|
||||
Je veux des notifications visuelles minimales et audio prioritaires
|
||||
Afin de rester concentré sur la route en toute sécurité
|
||||
|
||||
Scénario: Affichage minimal des notifications visuelles
|
||||
Étant donné un utilisateur "alice@roadwave.fr" en mode voiture
|
||||
Quand une notification est déclenchée
|
||||
Alors elle s'affiche pendant 3 secondes maximum
|
||||
Et occupe < 20% de l'écran
|
||||
Et disparaît automatiquement
|
||||
Et un événement "CAR_NOTIFICATION_MINIMAL_DISPLAYED" est enregistré
|
||||
|
||||
Scénario: Notifications audio prioritaires sur visuel
|
||||
Étant donné un utilisateur "bob@roadwave.fr" en mode voiture
|
||||
Quand un point d'intérêt approche
|
||||
Alors une notification audio est jouée
|
||||
Et la notification visuelle est optionnelle
|
||||
Et l'audio contient toutes les informations nécessaires
|
||||
Et un événement "CAR_AUDIO_NOTIFICATION_PRIORITY" est enregistré
|
||||
|
||||
Scénario: Pas de popups ou modales en mode voiture
|
||||
Étant donné un utilisateur "charlie@roadwave.fr" en mode voiture
|
||||
Quand une action nécessite une confirmation
|
||||
Alors aucune modale bloquante n'apparaît
|
||||
Et la confirmation est reportée ou faite par commande vocale
|
||||
Et un événement "CAR_MODAL_PREVENTED" est enregistré
|
||||
|
||||
Scénario: Gros boutons tactiles espacés pour conduite
|
||||
Étant donné un utilisateur "david@roadwave.fr" en mode voiture
|
||||
Alors tous les boutons ont une taille minimale de 60x60 pixels
|
||||
Et un espacement minimal de 20 pixels entre boutons
|
||||
Et sont facilement accessibles d'une main
|
||||
Et un événement "CAR_UI_TOUCH_OPTIMIZED" est enregistré
|
||||
|
||||
Scénario: Mode nuit automatique pour réduire éblouissement
|
||||
Étant donné un utilisateur "eve@roadwave.fr" en mode voiture la nuit
|
||||
Quand le capteur de luminosité détecte l'obscurité
|
||||
Alors l'interface passe en mode nuit (fond noir)
|
||||
Et la luminosité est réduite de 50%
|
||||
Et un événement "CAR_NIGHT_MODE_AUTO" est enregistré
|
||||
|
||||
Scénario: Notifications groupées pour éviter la surcharge
|
||||
Étant donné un utilisateur "frank@roadwave.fr" en mode voiture
|
||||
Quand plusieurs événements se produisent en 30 secondes
|
||||
Alors une seule notification résume les événements
|
||||
Et les détails sont accessibles après l'arrêt
|
||||
Et un événement "CAR_NOTIFICATIONS_GROUPED" est enregistré
|
||||
|
||||
Scénario: Intégration CarPlay avec notifications système
|
||||
Étant donné un utilisateur "grace@roadwave.fr" avec CarPlay
|
||||
Quand une notification RoadWave arrive
|
||||
Alors elle utilise le système de notification CarPlay natif
|
||||
Et respecte les guidelines Apple pour sécurité
|
||||
Et un événement "CARPLAY_NOTIFICATION_NATIVE" est enregistré
|
||||
|
||||
Scénario: Commandes vocales pour interactions sans les mains
|
||||
Étant donné un utilisateur "henry@roadwave.fr" en mode voiture
|
||||
Quand il dit "Dis Siri, mets en pause"
|
||||
Alors l'audio-guide se met en pause sans interaction tactile
|
||||
Et aucune confirmation visuelle n'est requise
|
||||
Et un événement "CAR_VOICE_COMMAND_EXECUTED" est enregistré
|
||||
|
||||
Scénario: Métriques de sécurité des notifications
|
||||
Étant donné que 50 000 notifications ont été affichées en mode voiture
|
||||
Alors les indicateurs suivants sont respectés:
|
||||
| Métrique | Valeur cible |
|
||||
| Temps d'affichage moyen | < 3s |
|
||||
| Taux d'utilisation audio vs visuel| 80% audio |
|
||||
| Taille moyenne des notifications | < 20% écran |
|
||||
| Taux d'accidents rapportés | 0 |
|
||||
Et les métriques sont exportées vers le monitoring
|
||||
@@ -0,0 +1,489 @@
|
||||
# language: fr
|
||||
Fonctionnalité: API - Notifications géolocalisées et quota anti-spam
|
||||
En tant qu'API backend
|
||||
Je veux gérer les notifications de contenus géolocalisés
|
||||
Afin de respecter les quotas et détecter le bon timing
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est disponible
|
||||
Et que Redis est accessible
|
||||
Et qu'un utilisateur "user123" existe en mode voiture
|
||||
|
||||
# Calcul ETA et déclenchement notification
|
||||
|
||||
Scénario: API calcule ETA vers point géolocalisé
|
||||
Étant donné qu'un contenu géolocalisé existe à la position (48.8584, 2.2945) Tour Eiffel
|
||||
Et que l'utilisateur roule à 50 km/h vers ce point
|
||||
Et qu'il est actuellement à 98 mètres
|
||||
Quand je POST /api/v1/geo-notifications/check-eta
|
||||
"""json
|
||||
{
|
||||
"user_id": "user123",
|
||||
"current_lat": 48.8577,
|
||||
"current_lon": 2.2950,
|
||||
"speed_kmh": 50,
|
||||
"target_content_id": "content_tower"
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 200
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"eta_seconds": 7,
|
||||
"distance_meters": 98,
|
||||
"should_notify": true,
|
||||
"notification_trigger": "eta_threshold"
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: API déclenche notification 7 secondes avant arrivée
|
||||
Étant donné qu'un contenu géolocalisé existe au point GPS
|
||||
Et que l'ETA calculé est 7 secondes
|
||||
Quand je POST /api/v1/geo-notifications/trigger
|
||||
"""json
|
||||
{
|
||||
"user_id": "user123",
|
||||
"content_id": "content_tower",
|
||||
"eta_seconds": 7
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 201
|
||||
Et une notification est envoyée au mobile
|
||||
Et la notification contient:
|
||||
| champ | valeur |
|
||||
| type | geo_anchored |
|
||||
| countdown | 7 |
|
||||
| icon | 🏛️ (selon type contenu) |
|
||||
| sound | bip court |
|
||||
|
||||
Scénario: API ne déclenche pas si ETA >7 secondes
|
||||
Étant donné qu'un contenu géolocalisé existe
|
||||
Et que l'ETA calculé est 10 secondes
|
||||
Quand je POST /api/v1/geo-notifications/check-eta
|
||||
Alors le statut de réponse est 200
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"eta_seconds": 10,
|
||||
"should_notify": false,
|
||||
"reason": "eta_above_threshold"
|
||||
}
|
||||
"""
|
||||
Et aucune notification n'est envoyée
|
||||
|
||||
Scénario: API déclenche notification immédiate si vitesse faible et proche
|
||||
Étant donné qu'un contenu géolocalisé existe
|
||||
Et que l'utilisateur roule à 3 km/h
|
||||
Et qu'il est à 40 mètres du point
|
||||
Quand je POST /api/v1/geo-notifications/check-eta
|
||||
Alors le statut de réponse est 200
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"eta_seconds": 0,
|
||||
"distance_meters": 40,
|
||||
"speed_kmh": 3,
|
||||
"should_notify": true,
|
||||
"notification_trigger": "proximity_and_low_speed"
|
||||
}
|
||||
"""
|
||||
Et une notification est envoyée immédiatement
|
||||
|
||||
# Quota anti-spam
|
||||
|
||||
Scénario: API vérifie quota avant notification (6 contenus max/heure)
|
||||
Étant donné que l'utilisateur a déjà reçu 5 notifications géo dans la dernière heure
|
||||
Quand je POST /api/v1/geo-notifications/check-quota
|
||||
"""json
|
||||
{
|
||||
"user_id": "user123"
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 200
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"quota_available": true,
|
||||
"notifications_count": 5,
|
||||
"quota_limit": 6,
|
||||
"remaining": 1
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: API refuse notification si quota atteint (6/6)
|
||||
Étant donné que l'utilisateur a déjà reçu 6 notifications géo dans la dernière heure
|
||||
Quand je POST /api/v1/geo-notifications/trigger
|
||||
"""json
|
||||
{
|
||||
"user_id": "user123",
|
||||
"content_id": "content_x"
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 429
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"error": "QUOTA_EXCEEDED",
|
||||
"message": "Quota horaire de notifications géolocalisées atteint (6/6)",
|
||||
"notifications_count": 6,
|
||||
"quota_limit": 6,
|
||||
"reset_in_seconds": 1800
|
||||
}
|
||||
"""
|
||||
Et aucune notification n'est envoyée
|
||||
|
||||
Scénario: API incrémente quota après notification envoyée
|
||||
Étant donné que l'utilisateur a reçu 3 notifications géo
|
||||
Quand je POST /api/v1/geo-notifications/trigger (succès)
|
||||
Alors le quota passe à 4/6
|
||||
Et en Redis, la clé "user:user123:geo_quota" contient 4 entrées
|
||||
Et le sorted set contient les timestamps des 4 notifications
|
||||
|
||||
Scénario: API utilise rolling window pour quota horaire
|
||||
Étant donné que l'utilisateur a reçu 6 notifications:
|
||||
| timestamp |
|
||||
| 10:00:00 |
|
||||
| 10:15:00 |
|
||||
| 10:30:00 |
|
||||
| 10:45:00 |
|
||||
| 11:00:00 |
|
||||
| 11:05:00 |
|
||||
Et que l'heure actuelle est 11:10:00
|
||||
Quand je vérifie le quota
|
||||
Alors les notifications avant 10:10:00 sont expirées
|
||||
Et seules 4 notifications sont comptées (10:15, 10:30, 10:45, 11:00, 11:05 = 5)
|
||||
Et le quota disponible est 1/6
|
||||
|
||||
Scénario: API exclut séquences d'audio-guide multi-séquences du quota
|
||||
Étant donné qu'un audio-guide "Visite Louvre" a 12 séquences
|
||||
Et que l'utilisateur a écouté les 12 séquences
|
||||
Quand je calcule le quota
|
||||
Alors l'audio-guide compte pour 1 seule notification
|
||||
Et les 12 séquences ne consomment pas 12 quota
|
||||
Et le quota utilisé est 1/6
|
||||
|
||||
# Cooldown après ignorance
|
||||
|
||||
Scénario: API active cooldown 10 min si notification ignorée
|
||||
Étant donné qu'une notification géo a été envoyée à 10:00:00
|
||||
Et que l'utilisateur n'a pas appuyé sur "Suivant" dans les 7 secondes
|
||||
Quand je POST /api/v1/geo-notifications/ignored
|
||||
"""json
|
||||
{
|
||||
"user_id": "user123",
|
||||
"notification_id": "notif123"
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 200
|
||||
Et un cooldown de 10 minutes est activé
|
||||
Et en Redis, la clé "user:user123:geo_cooldown" est créée
|
||||
Et le TTL est 600 secondes (10 minutes)
|
||||
|
||||
Scénario: API refuse notification si cooldown actif
|
||||
Étant donné qu'un cooldown est actif depuis 5 minutes
|
||||
Et qu'il reste 5 minutes de cooldown
|
||||
Quand je POST /api/v1/geo-notifications/trigger
|
||||
Alors le statut de réponse est 429
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"error": "COOLDOWN_ACTIVE",
|
||||
"message": "Cooldown actif après notification ignorée",
|
||||
"cooldown_remaining_seconds": 300
|
||||
}
|
||||
"""
|
||||
Et aucune notification n'est envoyée
|
||||
|
||||
Scénario: API autorise notification après expiration cooldown
|
||||
Étant donné qu'un cooldown a été activé à 10:00:00
|
||||
Et que l'heure actuelle est 10:11:00 (11 minutes après)
|
||||
Quand je POST /api/v1/geo-notifications/check-quota
|
||||
Alors le cooldown a expiré
|
||||
Et les notifications sont à nouveau autorisées
|
||||
|
||||
# Tracking GPS temps réel
|
||||
|
||||
Scénario: API reçoit positions GPS toutes les secondes
|
||||
Étant donné que le mobile envoie des positions GPS
|
||||
Quand je POST /api/v1/gps/track
|
||||
"""json
|
||||
{
|
||||
"user_id": "user123",
|
||||
"latitude": 48.8577,
|
||||
"longitude": 2.2950,
|
||||
"speed_kmh": 50,
|
||||
"timestamp": "2026-02-02T10:30:00Z"
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 200
|
||||
Et la position est stockée dans l'historique GPS (Redis)
|
||||
Et l'historique conserve les 30 derniers points
|
||||
|
||||
Scénario: API calcule vitesse moyenne sur 30 derniers points
|
||||
Étant donné que l'historique GPS contient 30 points
|
||||
Et que les vitesses enregistrées sont variables
|
||||
Quand je GET /api/v1/gps/average-speed
|
||||
Alors le statut de réponse est 200
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"average_speed_kmh": 48.5,
|
||||
"samples_count": 30,
|
||||
"period_seconds": 30
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: API détecte contenus géolocalisés dans rayon 500m
|
||||
Étant donné que l'utilisateur est à la position (48.8577, 2.2950)
|
||||
Et qu'un contenu géolocalisé existe à 300m
|
||||
Quand je GET /api/v1/geo-notifications/nearby
|
||||
"""json
|
||||
{
|
||||
"user_id": "user123",
|
||||
"latitude": 48.8577,
|
||||
"longitude": 2.2950,
|
||||
"radius_meters": 500
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 200
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"nearby_contents": [
|
||||
{
|
||||
"id": "content_tower",
|
||||
"distance_meters": 300,
|
||||
"eta_seconds": 21,
|
||||
"should_notify_soon": false
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
# Validation contenu géolocalisé
|
||||
|
||||
Scénario: API valide que contenu géolocalisé accepté
|
||||
Étant donné qu'une notification géo a été envoyée
|
||||
Et que l'utilisateur a appuyé sur "Suivant" dans les 7 secondes
|
||||
Quand je POST /api/v1/geo-notifications/accepted
|
||||
"""json
|
||||
{
|
||||
"user_id": "user123",
|
||||
"notification_id": "notif123",
|
||||
"content_id": "content_tower"
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 200
|
||||
Et le contenu géolocalisé est inséré en priorité
|
||||
Et un décompte de 5 secondes est déclenché côté client
|
||||
Et le quota n'est PAS incrémenté (acceptation = pas de spam)
|
||||
|
||||
Scénario: API enregistre contenu géolocalisé perdu
|
||||
Étant donné qu'une notification géo a été envoyée
|
||||
Et que l'utilisateur n'a pas appuyé sur "Suivant"
|
||||
Quand je POST /api/v1/geo-notifications/ignored
|
||||
Alors le contenu géolocalisé est perdu
|
||||
Et il n'est PAS inséré dans la file d'attente
|
||||
Et une métrique "geo_notification_ignored" est enregistrée
|
||||
Et le cooldown 10 min est activé
|
||||
|
||||
# Métriques
|
||||
|
||||
Scénario: API stocke métriques de notifications géolocalisées
|
||||
Étant donné que l'API envoie des notifications géo
|
||||
Quand je consulte les métriques
|
||||
Alors les données suivantes sont disponibles:
|
||||
| métrique | description |
|
||||
| geo_notifications_sent | Nombre total envoyées |
|
||||
| geo_notifications_accepted | Nombre acceptées (Suivant) |
|
||||
| geo_notifications_ignored | Nombre ignorées |
|
||||
| geo_acceptance_rate | % acceptation |
|
||||
| average_eta_seconds | ETA moyen au déclenchement |
|
||||
| quota_exceeded_count | Nombre de refus quota |
|
||||
|
||||
# Redis structures
|
||||
|
||||
Scénario: API utilise sorted set Redis pour quota
|
||||
Étant donné que l'utilisateur a reçu 3 notifications
|
||||
Quand je consulte Redis "user:user123:geo_quota"
|
||||
Alors la structure est un sorted set:
|
||||
"""
|
||||
ZADD user:user123:geo_quota
|
||||
1707047400 "notif1"
|
||||
1707047700 "notif2"
|
||||
1707048000 "notif3"
|
||||
"""
|
||||
Et les scores sont les timestamps Unix
|
||||
Et le TTL est 3600 secondes (1 heure)
|
||||
|
||||
Scénario: API compte notifications avec ZCOUNT
|
||||
Étant donné que le sorted set contient plusieurs notifications
|
||||
Et que l'heure actuelle est 11:00:00 (timestamp 1707048000)
|
||||
Quand l'API vérifie le quota
|
||||
Alors la commande Redis est:
|
||||
"""
|
||||
ZCOUNT user:user123:geo_quota 1707044400 1707048000
|
||||
"""
|
||||
Et seules les notifications des 60 dernières minutes sont comptées
|
||||
|
||||
# Gestion erreurs
|
||||
|
||||
Scénario: API gère échec calcul ETA gracieusement
|
||||
Étant donné qu'une position GPS est invalide
|
||||
Quand je POST /api/v1/geo-notifications/check-eta
|
||||
"""json
|
||||
{
|
||||
"user_id": "user123",
|
||||
"current_lat": 200,
|
||||
"current_lon": 300
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 400
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"error": "INVALID_GPS_COORDINATES",
|
||||
"message": "Coordonnées GPS invalides"
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: API gère vitesse nulle sans crash
|
||||
Étant donné que l'utilisateur est à l'arrêt (vitesse = 0)
|
||||
Et qu'un contenu géolocalisé est à 100m
|
||||
Quand je POST /api/v1/geo-notifications/check-eta
|
||||
Alors l'ETA est incalculable (vitesse nulle)
|
||||
Et aucune notification n'est envoyée
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"should_notify": false,
|
||||
"reason": "speed_zero"
|
||||
}
|
||||
"""
|
||||
|
||||
Plan du Scénario: Déclenchement selon ETA
|
||||
Étant donné qu'un contenu géolocalisé existe
|
||||
Et que l'ETA calculé est <eta> secondes
|
||||
Quand je vérifie si notification doit être envoyée
|
||||
Alors la décision est <decision>
|
||||
|
||||
Exemples:
|
||||
| eta | decision |
|
||||
| 3 | notifier |
|
||||
| 5 | notifier |
|
||||
| 7 | notifier |
|
||||
| 8 | ne pas notifier |
|
||||
| 10 | ne pas notifier |
|
||||
| 15 | ne pas notifier |
|
||||
|
||||
Plan du Scénario: Quota selon nombre notifications
|
||||
Étant donné que l'utilisateur a reçu <count> notifications dans l'heure
|
||||
Quand je vérifie le quota
|
||||
Alors le quota est <status>
|
||||
|
||||
Exemples:
|
||||
| count | status |
|
||||
| 0 | disponible |
|
||||
| 3 | disponible |
|
||||
| 5 | disponible |
|
||||
| 6 | atteint |
|
||||
| 7 | atteint |
|
||||
|
||||
# Edge cases : haute vitesse
|
||||
|
||||
Scénario: API calcule ETA correctement à 130 km/h
|
||||
Étant donné qu'un contenu géolocalisé existe
|
||||
Et que l'utilisateur roule à 130 km/h (36.1 m/s)
|
||||
Et qu'il est à 252 mètres du point
|
||||
Quand je POST /api/v1/geo-notifications/check-eta
|
||||
"""json
|
||||
{
|
||||
"user_id": "user123",
|
||||
"speed_kmh": 130,
|
||||
"distance_meters": 252
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 200
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"eta_seconds": 7,
|
||||
"speed_ms": 36.1,
|
||||
"should_notify": true,
|
||||
"notification_trigger": "eta_threshold"
|
||||
}
|
||||
"""
|
||||
Et une notification est envoyée 252m avant le point
|
||||
|
||||
Scénario: API calcule distance notification selon vitesse
|
||||
Étant donné qu'un contenu géolocalisé existe
|
||||
Quand l'utilisateur roule à 180 km/h (50 m/s)
|
||||
Alors la notification est déclenchée à 350 mètres avant le point
|
||||
Et après décompte 5s, user a parcouru 250m
|
||||
Et le contenu démarre 100m avant le point
|
||||
Et le système fonctionne même à vitesse extrême
|
||||
|
||||
# Edge cases : multiples points proches
|
||||
|
||||
Scénario: API gère multiples points géolocalisés proches (800m chacun)
|
||||
Étant donné que 3 châteaux existent espacés de 800m chacun
|
||||
| contenu | position_km |
|
||||
| Château A | 0 |
|
||||
| Château B | 0.8 |
|
||||
| Château C | 1.6 |
|
||||
Et que l'utilisateur roule à 50 km/h
|
||||
Quand une notification Château A est envoyée et acceptée
|
||||
Alors le quota passe à 1/6
|
||||
Et aucun cooldown n'est activé (notification acceptée)
|
||||
Quand 57 secondes s'écoulent (temps pour parcourir 800m)
|
||||
Et que l'utilisateur atteint Château B
|
||||
Alors une notification Château B est envoyée
|
||||
Car le quota n'est pas atteint (1/6)
|
||||
Et il n'y a pas de cooldown actif
|
||||
|
||||
Scénario: API active cooldown réduit après validations multiples
|
||||
Étant donné que l'utilisateur a validé 2 notifications consécutives
|
||||
Et que les notifications ont toutes été acceptées (clic "Suivant")
|
||||
Quand une 3ème notification est ignorée
|
||||
Alors le cooldown activé est 5 minutes (réduit)
|
||||
Et non 10 minutes (standard)
|
||||
Car l'utilisateur a montré de l'engagement précédemment
|
||||
|
||||
Scénario: API ignore notifications si cooldown actif après ignorance
|
||||
Étant donné qu'une notification a été ignorée à 10:00:00
|
||||
Et qu'un cooldown de 10 minutes est actif
|
||||
Et que 3 contenus géolocalisés existent à 10:05, 10:08, 10:11
|
||||
Quand l'utilisateur passe devant le contenu à 10:05
|
||||
Alors aucune notification n'est envoyée (cooldown actif, reste 5 min)
|
||||
Quand l'utilisateur passe devant le contenu à 10:08
|
||||
Alors aucune notification n'est envoyée (cooldown actif, reste 2 min)
|
||||
Quand l'utilisateur passe devant le contenu à 10:11
|
||||
Alors une notification est envoyée (cooldown expiré après 10 min)
|
||||
|
||||
# Edge cases : mode stationnement
|
||||
|
||||
Scénario: API détecte mode stationnement (vitesse < 1 km/h pendant 2 min)
|
||||
Étant donné que l'utilisateur roule à 50 km/h
|
||||
Quand la vitesse passe à 0.5 km/h (arrêt complet)
|
||||
Et que la vitesse reste < 1 km/h pendant 2 minutes consécutives
|
||||
Alors le mode "stationnement" est activé
|
||||
Et aucune notification géolocalisée n'est envoyée
|
||||
Et le système bascule automatiquement en mode piéton
|
||||
Et un événement "mode_stationnement_detected" est enregistré
|
||||
|
||||
Scénario: API sort du mode stationnement quand vitesse > 5 km/h
|
||||
Étant donné que le mode stationnement est actif depuis 30 minutes
|
||||
Et que l'utilisateur était à l'arrêt près d'un château
|
||||
Quand la vitesse passe à 20 km/h pendant 10 secondes
|
||||
Alors le mode voiture est réactivé
|
||||
Et les notifications géolocalisées reprennent
|
||||
Et le quota horaire est vérifié avant nouvelle notification
|
||||
|
||||
Scénario: API refuse notification si user arrêté longtemps près d'un point
|
||||
Étant donné qu'un contenu géolocalisé existe à 30 mètres
|
||||
Et que l'utilisateur est à l'arrêt (vitesse 0 km/h) depuis 3 minutes
|
||||
Quand le système détecte la proximité
|
||||
Alors le mode stationnement est actif
|
||||
Et aucune notification n'est envoyée
|
||||
Car l'utilisateur est probablement stationné (parking)
|
||||
Et pas en mode conduite
|
||||
226
docs/domains/content/features/navigation/quota-cooldown.feature
Normal file
226
docs/domains/content/features/navigation/quota-cooldown.feature
Normal file
@@ -0,0 +1,226 @@
|
||||
# language: fr
|
||||
|
||||
@api @navigation @anti-spam @mvp
|
||||
Fonctionnalité: Quota et cooldown pour contenus géolocalisés
|
||||
|
||||
En tant que système de navigation
|
||||
Je veux limiter les notifications géolocalisées à 6 par heure
|
||||
Et appliquer un cooldown de 10 minutes après refus
|
||||
Afin d'éviter le spam et préserver l'expérience utilisateur
|
||||
|
||||
Contexte:
|
||||
Étant donné un utilisateur authentifié en mode voiture
|
||||
Et la géolocalisation est activée
|
||||
|
||||
# ============================================================================
|
||||
# QUOTA 6 NOTIFICATIONS / HEURE (fenêtre glissante)
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Première notification de l'heure sans quota atteint
|
||||
Étant donné l'utilisateur n'a reçu aucune notification géolocalisée dans les 60 dernières minutes
|
||||
Et un contenu géolocalisé déclenche une notification (ETA = 7s)
|
||||
Quand la notification est envoyée
|
||||
Alors le compteur de quota doit être incrémenté à 1/6
|
||||
Et un timestamp doit être enregistré pour cette notification
|
||||
Et la notification doit être affichée normalement
|
||||
|
||||
Scénario: Notifications multiples sous le quota
|
||||
Étant donné l'utilisateur a reçu 3 notifications dans les 60 dernières minutes :
|
||||
| timestamp | contenu_id |
|
||||
| 2026-02-03 14:10:00 | content-1 |
|
||||
| 2026-02-03 14:25:00 | content-2 |
|
||||
| 2026-02-03 14:40:00 | content-3 |
|
||||
Et il est maintenant 14:50:00
|
||||
Et un nouveau contenu géolocalisé déclenche une notification
|
||||
Quand la notification est évaluée
|
||||
Alors le compteur doit afficher 4/6
|
||||
Et la notification doit être envoyée normalement
|
||||
|
||||
Scénario: 6ème notification - dernière autorisée
|
||||
Étant donné l'utilisateur a reçu 5 notifications dans l'heure
|
||||
Et un nouveau contenu géolocalisé déclenche une notification
|
||||
Quand la notification est envoyée
|
||||
Alors le compteur doit afficher 6/6
|
||||
Et la notification doit être affichée normalement
|
||||
Et un flag "quota_limit_reached" doit être ajouté aux métriques
|
||||
|
||||
Scénario: 7ème notification - quota dépassé
|
||||
Étant donné l'utilisateur a reçu 6 notifications dans les 60 dernières minutes
|
||||
Et un nouveau contenu géolocalisé déclenche une notification
|
||||
Quand le système évalue le quota
|
||||
Alors la notification ne doit PAS être envoyée
|
||||
Et le contenu doit être ajouté silencieusement à la file d'attente
|
||||
Et un événement "notification_blocked_quota" doit être loggé
|
||||
Et l'utilisateur ne doit voir aucun indicateur visuel de blocage
|
||||
|
||||
Scénario: Fenêtre glissante - libération progressive du quota
|
||||
Étant donné l'utilisateur a reçu 6 notifications :
|
||||
| timestamp | contenu_id |
|
||||
| 2026-02-03 13:00:00 | content-1 |
|
||||
| 2026-02-03 13:15:00 | content-2 |
|
||||
| 2026-02-03 13:30:00 | content-3 |
|
||||
| 2026-02-03 13:45:00 | content-4 |
|
||||
| 2026-02-03 14:00:00 | content-5 |
|
||||
| 2026-02-03 14:10:00 | content-6 |
|
||||
Et il est maintenant 14:05:00 (la 1ère notification a >60 min)
|
||||
Et un nouveau contenu déclenche une notification
|
||||
Quand le système calcule le quota sur les 60 dernières minutes
|
||||
Alors seules 5 notifications doivent être comptées (13:15 à 14:10)
|
||||
Et le compteur doit afficher 6/6 (5 + nouvelle = 6)
|
||||
Et la notification doit être envoyée
|
||||
|
||||
Scénario: Reset complet après 60 minutes sans notification
|
||||
Étant donné l'utilisateur a reçu 6 notifications entre 12:00 et 12:50
|
||||
Et il est maintenant 13:55 (>60 min après la dernière)
|
||||
Quand un nouveau contenu déclenche une notification
|
||||
Alors le compteur doit être reset à 1/6
|
||||
Et la notification doit être envoyée normalement
|
||||
|
||||
# ============================================================================
|
||||
# EXCEPTION AUDIO-GUIDES (1 quota = toutes les séquences)
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Audio-guide multi-séquences compte pour 1 seul quota
|
||||
Étant donné l'utilisateur a reçu 5 notifications normales dans l'heure
|
||||
Et l'utilisateur démarre un audio-guide avec 8 séquences géolocalisées
|
||||
Quand la 1ère séquence de l'audio-guide déclenche une notification
|
||||
Alors le quota doit être incrémenté à 6/6
|
||||
Quand les 7 séquences suivantes déclenchent des notifications
|
||||
Alors le quota doit rester à 6/6
|
||||
Et toutes les notifications d'audio-guide doivent être envoyées
|
||||
Et un flag "audio_guide_exception" doit être loggé
|
||||
|
||||
Scénario: Audio-guide suivi de contenus normaux bloqués
|
||||
Étant donné l'utilisateur a reçu 5 notifications normales
|
||||
Et l'utilisateur a terminé un audio-guide de 6 séquences (compte pour 1 quota)
|
||||
Et le quota est maintenant 6/6
|
||||
Quand un contenu normal déclenche une notification
|
||||
Alors la notification doit être bloquée (quota dépassé)
|
||||
Et le contenu doit être ajouté à la file d'attente
|
||||
|
||||
Scénario: Plusieurs audio-guides dans l'heure
|
||||
Étant donné l'utilisateur a démarré 3 audio-guides dans l'heure :
|
||||
| timestamp | audio_guide_id | sequences |
|
||||
| 2026-02-03 13:00:00 | guide-1 | 5 |
|
||||
| 2026-02-03 13:30:00 | guide-2 | 8 |
|
||||
| 2026-02-03 14:00:00 | guide-3 | 4 |
|
||||
Quand le système calcule le quota
|
||||
Alors chaque audio-guide doit compter pour 1 quota
|
||||
Et le compteur doit afficher 3/6
|
||||
Et 3 contenus normaux supplémentaires peuvent être notifiés
|
||||
|
||||
# ============================================================================
|
||||
# COOLDOWN 10 MINUTES APRÈS REFUS
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Notification ignorée déclenche cooldown de 10 minutes
|
||||
Étant donné une notification géolocalisée est affichée à 14:00:00
|
||||
Et le compteur de 7 secondes se termine sans action utilisateur
|
||||
Quand le délai de 7 secondes expire
|
||||
Alors un cooldown de 10 minutes doit être activé
|
||||
Et un timestamp "cooldown_until: 14:10:00" doit être enregistré
|
||||
Et la notification doit disparaître
|
||||
|
||||
Scénario: Notifications bloquées pendant le cooldown
|
||||
Étant donné un cooldown est actif jusqu'à 14:10:00
|
||||
Et il est maintenant 14:05:00
|
||||
Et 3 contenus géolocalisés déclenchent des notifications
|
||||
Quand le système évalue les conditions
|
||||
Alors aucune notification ne doit être affichée
|
||||
Et les 3 contenus doivent être ajoutés silencieusement à la file d'attente
|
||||
Et un événement "notification_blocked_cooldown" doit être loggé pour chacun
|
||||
|
||||
Scénario: Fin du cooldown - reprise normale
|
||||
Étant donné un cooldown actif jusqu'à 14:10:00
|
||||
Et il est maintenant 14:10:05 (cooldown expiré)
|
||||
Et un contenu géolocalisé déclenche une notification
|
||||
Quand le système évalue les conditions
|
||||
Alors le cooldown doit être considéré comme expiré
|
||||
Et la notification doit être envoyée normalement
|
||||
Et le flag "post_cooldown" doit être ajouté aux métriques
|
||||
|
||||
Scénario: Validation notification pendant le countdown annule le cooldown
|
||||
Étant donné une notification affichée avec compteur à 4 secondes restantes
|
||||
Quand l'utilisateur appuie sur "Écouter maintenant"
|
||||
Alors le contenu doit démarrer
|
||||
Et aucun cooldown ne doit être appliqué
|
||||
Et la prochaine notification peut être envoyée normalement (sous réserve du quota)
|
||||
|
||||
Scénario: Cooldown + Quota simultanés
|
||||
Étant donné l'utilisateur a reçu 6 notifications dans l'heure (quota atteint)
|
||||
Et la dernière notification a été ignorée (cooldown actif pour 10 min)
|
||||
Et il est maintenant 5 minutes plus tard
|
||||
Quand un nouveau contenu déclenche une notification
|
||||
Alors la notification doit être bloquée pour les 2 raisons :
|
||||
| raison | statut |
|
||||
| quota_exceeded | true |
|
||||
| cooldown_active | true |
|
||||
Et le contenu doit être ajouté à la file d'attente
|
||||
Et les deux raisons doivent être loggées
|
||||
|
||||
# ============================================================================
|
||||
# CAS LIMITES & EDGE CASES
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Cooldown ne s'applique pas en mode piéton
|
||||
Étant donné l'utilisateur est en mode piéton
|
||||
Et une notification push géolocalisée est ignorée
|
||||
Quand la notification expire
|
||||
Alors aucun cooldown ne doit être appliqué
|
||||
Car en mode piéton les notifications sont opt-in et moins intrusives
|
||||
|
||||
Scénario: Changement de mode pendant cooldown (voiture → piéton)
|
||||
Étant donné un cooldown actif en mode voiture jusqu'à 14:20:00
|
||||
Et il est maintenant 14:12:00 (cooldown encore actif)
|
||||
Quand l'utilisateur bascule en mode piéton
|
||||
Alors le cooldown doit être immédiatement annulé
|
||||
Et les notifications piéton doivent être activées normalement
|
||||
|
||||
Scénario: Déconnexion/reconnexion pendant cooldown
|
||||
Étant donné un cooldown actif jusqu'à 15:30:00
|
||||
Quand l'utilisateur se déconnecte puis se reconnecte à 15:20:00
|
||||
Alors le cooldown doit être persisté (Redis ou DB)
|
||||
Et le cooldown doit toujours être actif jusqu'à 15:30:00
|
||||
Et les notifications doivent rester bloquées
|
||||
|
||||
Scénario: Quota reset à minuit vs fenêtre glissante
|
||||
Étant donné le système utilise une fenêtre glissante de 60 minutes
|
||||
Et l'utilisateur a reçu 6 notifications entre 23:30 et 23:55
|
||||
Quand minuit passe et il est 00:05
|
||||
Alors le quota doit toujours être calculé sur les 60 dernières minutes
|
||||
Et les 6 notifications doivent encore être comptées
|
||||
Et aucune notification supplémentaire ne peut être envoyée avant 00:30
|
||||
|
||||
# ============================================================================
|
||||
# MÉTRIQUES & ANALYTICS
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Logging des blocages quota pour optimisation
|
||||
Étant donné le quota est atteint (6/6)
|
||||
Et 5 contenus tentent de déclencher une notification dans l'heure suivante
|
||||
Quand ces notifications sont bloquées
|
||||
Alors un événement analytics doit être loggé pour chacune :
|
||||
| event_type | notification_blocked_quota |
|
||||
| content_id | content-xyz |
|
||||
| user_quota_current | 6 |
|
||||
| user_quota_max | 6 |
|
||||
| timestamp_blocked | 2026-02-03 14:30:00 |
|
||||
| added_to_queue | true |
|
||||
Et ces métriques doivent alimenter le dashboard de monitoring
|
||||
|
||||
Scénario: Métriques de cooldown par utilisateur
|
||||
Étant donné un utilisateur ignore 3 notifications dans la journée
|
||||
Quand le système analyse le comportement
|
||||
Alors les métriques suivantes doivent être calculées :
|
||||
| cooldowns_triggered_today | 3 |
|
||||
| avg_cooldown_per_user | 2.1 |
|
||||
| ignore_rate | 50% |
|
||||
Et ces données doivent être utilisées pour ajuster l'algorithme de recommandation
|
||||
|
||||
Scénario: A/B testing - quota 6 vs 8 vs 10 par heure
|
||||
Étant donné l'utilisateur est dans le groupe de test "quota_8"
|
||||
Et le système teste différentes valeurs de quota
|
||||
Quand le système évalue les notifications
|
||||
Alors le quota max doit être de 8 au lieu de 6
|
||||
Et le groupe de test doit être loggé dans les événements
|
||||
Et les métriques d'engagement doivent être comparées entre groupes
|
||||
Reference in New Issue
Block a user