feat(gherkin): ajouter features interactions et navigation

Couverture complète des règles métier 05-interactions-navigation.md :

API (Backend) :
- File d'attente : pré-calcul 5 contenus, recalcul auto (>10km, 10min, <3 contenus), invalidation, Redis cache
- Notifications géolocalisées : calcul ETA, déclenchement 7s avant, quota 6/h, cooldown 10min, tracking GPS
- Jauges d'intérêt : architecture services séparés (Calculation + Update), pattern addition points absolus, persistance Redis/PostgreSQL

UI (Frontend) :
- Mode piéton : notifications push arrière-plan, rayon 200m, permissions stratégie progressive, geofencing iOS/Android
- Basculement automatique voiture↔piéton : détection vitesse GPS, hysteresis 10s, transition transparente

Fichiers créés :
- features/api/navigation/file-attente.feature
- features/api/navigation/notifications-geolocalisees.feature
- features/ui/navigation/mode-pieton-notifications-push.feature

Fichiers enrichis :
- features/api/interest-gauges/evolution-jauges.feature (ajout scénarios architecture backend)
This commit is contained in:
jpgiannetti
2026-02-02 22:41:00 +01:00
parent 852240b5ec
commit ea77aa8ac7
4 changed files with 1055 additions and 0 deletions

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

View File

@@ -0,0 +1,390 @@
# 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 |