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)
391 lines
13 KiB
Gherkin
391 lines
13 KiB
Gherkin
# 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 |
|