Ajout et enrichissement des fichiers Gherkin pour les contenus géolocalisés en mode voiture selon règles métier section 17: API Backend (notifications-geolocalisees.feature): - Edge cases haute vitesse (130 km/h, 180 km/h) - Gestion multiples points géolocalisés proches (800m) - Cooldown réduit après validations multiples - Mode stationnement (vitesse < 1 km/h pendant 2 min) UI Mobile (contenus-geolocalises-voiture.feature) - nouveau fichier: - Notification visuelle minimaliste (icône + compteur, pas de texte) - Validation "Suivant" et décompte 5 secondes - Transitions audio fluides (fade in/out) - Conformité CarPlay/Android Auto (sonore uniquement) - Navigation avec contenus géolocalisés - Annulation décompte et gestion historique UI Navigation (commande-precedent.feature): - Comportement "Précédent" avec contenus géolocalisés - Historique mixte buffer et géolocalisés - Règle 10s pour replay/retour - Notification ignorée/annulée n'entre pas dans historique
490 lines
17 KiB
Gherkin
490 lines
17 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 |
|
||
|
||
# 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
|