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

@@ -479,3 +479,89 @@ Fonctionnalité: API - Évolution des jauges d'intérêt
]
}
"""
# Architecture backend - Services séparés
Scénario: Gauge Calculation Service calcule l'ajustement (stateless)
Étant donné un événement d'écoute avec 85% de complétion
Quand le Gauge Calculation Service calcule l'ajustement
Alors le service retourne:
"""json
{
"adjustment_type": "automatic_reinforced",
"adjustment_value": 2.0,
"reason": "completion_percentage >= 80%"
}
"""
Et le service est stateless (aucune lecture DB)
Et le service est testable unitairement
Scénario: Gauge Update Service applique l'ajustement (stateful)
Étant donné qu'un ajustement de +2% a été calculé
Et que la jauge "Automobile" de "user123" est à 45%
Quand le Gauge Update Service applique l'ajustement
Alors la nouvelle valeur est 47% (45 + 2)
Et la borne [0, 100] est respectée via fonction clamp
Et la mise à jour est persistée en Redis (immédiat)
Et la mise à jour est persistée en PostgreSQL (batch async)
Scénario: Pattern de calcul - Addition de points absolus
Étant donné qu'une jauge est à 60%
Et qu'un ajustement de +2% est calculé
Quand le calcul est effectué
Alors la formule est: newValue = currentValue + adjustment
Et la formule est: newValue = clamp(newValue, 0.0, 100.0)
Et la nouvelle valeur est 62%
Scénario: Pattern de calcul - Éviter multiplication relative
Étant donné qu'une jauge est à 50%
Et qu'un ajustement de +2% est calculé
Quand le calcul est effectué
Alors la formule utilisée est: 50 + 2 = 52
Et la formule utilisée n'est PAS: 50 * (1 + 2/100) = 51
Car l'ajustement est en points absolus, pas relatifs
Scénario: Multi-tags - Mise à jour de N jauges simultanément
Étant donné qu'un contenu a 3 tags: ["Automobile", "Voyage", "Technologie"]
Et qu'un ajustement de +2% est calculé (écoute 85%)
Quand le Gauge Update Service applique
Alors 3 jauges sont mises à jour:
| catégorie | ajustement |
| Automobile | +2% |
| Voyage | +2% |
| Technologie | +2% |
Et toutes les mises à jour sont effectuées en une seule transaction
Scénario: Persistance Redis immédiate (latence <10ms)
Étant donné qu'un ajustement doit être persisté
Quand le Gauge Update Service écrit en Redis
Alors la latence est < 10ms
Et la jauge est immédiatement disponible pour recommandations
Et Redis sert de cache haute performance
Scénario: Persistance PostgreSQL asynchrone (batch 5 min)
Étant donné que 100 ajustements ont été appliqués en Redis
Quand le batch async s'exécute toutes les 5 minutes
Alors les 100 ajustements sont écrits en PostgreSQL en batch
Et la cohérence finale est garantie
Et l'application reste performante (pas de write sync)
Scénario: Séparation responsabilités - Calculation vs Update
Étant donné un événement d'écoute
Quand il est traité
Alors le Gauge Calculation Service calcule l'ajustement (logique métier pure)
Et le Gauge Update Service applique l'ajustement (persistance)
Et les deux services sont indépendants
Et chaque service a une responsabilité unique (SRP)
Scénario: Réutilisabilité Calculation Service
Étant donné le Gauge Calculation Service
Quand il est utilisé pour:
| contexte |
| Like automatique |
| Skip rapide |
| Like manuel |
| Abonnement créateur |
Alors le même service calcule tous les ajustements
Et la logique métier est centralisée
Et il n'y a pas de duplication de code

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 |

View File

@@ -0,0 +1,265 @@
# language: fr
Fonctionnalité: Mode piéton - Notifications push et basculement automatique
En tant qu'utilisateur à pied
Je veux recevoir des notifications push pour audio-guides à proximité
Afin d'être alerté même avec l'application en arrière-plan
Contexte:
Étant donné que l'application RoadWave est installée
Et que l'utilisateur est connecté
# Détection automatique mode piéton
Scénario: Basculement automatique vers mode piéton (vitesse < 5 km/h)
Étant donné que je suis en mode voiture (vitesse 50 km/h)
Quand ma vitesse GPS moyenne passe à 3 km/h pendant 10 secondes
Alors l'application bascule automatiquement en mode piéton
Et aucune popup de confirmation n'est affichée
Et les notifications passent de "sonores + icône" à "push arrière-plan"
Et le rayon de détection passe de 7s ETA à 200 mètres
Scénario: Basculement automatique vers mode voiture (vitesse ≥ 5 km/h)
Étant donné que je suis en mode piéton (vitesse 3 km/h)
Quand ma vitesse GPS moyenne passe à 20 km/h pendant 10 secondes
Alors l'application bascule automatiquement en mode voiture
Et aucune popup de confirmation n'est affichée
Et les notifications passent de "push arrière-plan" à "sonores + icône"
Et le rayon de détection passe de 200m à 7s ETA
Scénario: Hysteresis pour éviter basculements intempestifs
Étant donné que je suis en mode piéton (vitesse 3 km/h)
Quand ma vitesse passe brièvement à 6 km/h pendant 2 secondes
Et qu'elle redescend à 3 km/h
Alors le mode piéton est conservé
Et aucun basculement n'a lieu
Car la durée de 10 secondes stables n'a pas été atteinte
Scénario: Vitesse moyenne calculée sur 30 secondes
Étant donné que je suis en mode voiture
Et que mes vitesses sur 30 secondes sont: [50, 48, 52, 3, 2, 4, 3, 2, ...]
Quand la vitesse moyenne sur 30s devient < 5 km/h
Et qu'elle reste stable pendant 10 secondes
Alors le basculement vers mode piéton s'effectue
# Permissions et activation
Scénario: Permission "While Using App" demandée au premier lancement
Étant donné que c'est le premier lancement de l'application
Quand j'arrive sur l'onboarding
Alors une demande de permission "Autoriser la localisation pendant l'utilisation" s'affiche
Et l'explication est: "RoadWave utilise votre position pour proposer des contenus audio géolocalisés adaptés à votre trajet"
Quand j'accepte
Alors le mode voiture est pleinement fonctionnel
Et le mode piéton notifications push n'est PAS encore activé
Scénario: Permission "Always" demandée uniquement si user active mode piéton
Étant donné que j'utilise RoadWave avec permission "While Using App"
Et que je vais dans Réglages > Notifications audio-guides piéton
Quand je clique sur le toggle "Activer notifications piéton"
Alors un écran d'éducation s'affiche:
"""
📍 Notifications audio-guides piéton
Pour vous alerter d'audio-guides à proximité même
quand vous marchez avec l'app fermée, RoadWave a
besoin de votre position en arrière-plan.
Votre position sera utilisée pour :
Détecter monuments à 200m
Vous envoyer une notification
Votre position ne sera jamais :
Vendue à des tiers
Utilisée pour de la publicité
Cette fonctionnalité est optionnelle.
Vous pouvez utiliser RoadWave sans cette permission.
[Continuer] [Non merci]
"""
Quand je clique sur "Continuer"
Alors la demande système "Autoriser toujours" (iOS) ou "Autoriser tout le temps" (Android) s'affiche
Et le mode piéton push est activé si j'accepte
Scénario: Permission arrière-plan refusée désactive mode piéton push
Étant donné que je refuse la permission "Autoriser toujours"
Quand je reviens dans l'application
Alors le toggle "Notifications piéton" est grisé et désactivé
Et un message s'affiche: "Permission refusée. Mode piéton désactivé."
Et le mode voiture reste pleinement fonctionnel
Et je peux toujours utiliser les audio-guides en mode manuel
Scénario: Permission arrière-plan peut être accordée plus tard
Étant donné que j'ai refusé la permission arrière-plan
Quand je vais dans Réglages de l'app
Alors un bouton "Activer notifications piéton" est disponible
Et un lien vers les réglages système iOS/Android est fourni
Quand j'accorde la permission dans les réglages système
Et que je reviens dans l'app
Alors le mode piéton push est automatiquement activé
# Notifications push en arrière-plan
Scénario: Notification push quand app en arrière-plan (mode piéton)
Étant donné que je suis en mode piéton
Et que l'application est en arrière-plan (fermée)
Et que je marche à proximité du Louvre
Quand je passe dans le rayon de 200 mètres d'un audio-guide
Alors une notification push système s'affiche:
"""
Audio-guide à proximité
Musée du Louvre : La Joconde - @paris_museum
"""
Et je reçois la notification même si l'app est fermée
Scénario: Tap sur notification ouvre app sur le contenu
Étant donné qu'une notification push "Audio-guide à proximité" s'affiche
Quand je tape sur la notification
Alors l'application s'ouvre
Et je suis redirigé vers la page du contenu audio-guide
Et je peux démarrer la lecture manuellement
Et je peux voir la description, le créateur, la durée, etc.
Scénario: Geofencing iOS/Android pour économie batterie
Étant donné que le mode piéton est activé
Et que l'application utilise geofencing natif
Quand je me déplace à pied
Alors l'OS iOS/Android gère la détection de proximité
Et l'application n'a pas besoin d'être constamment active
Et la batterie est préservée (< 5% consommation supplémentaire)
Scénario: Rayon de détection 200 mètres en mode piéton
Étant donné que je suis en mode piéton
Et qu'un audio-guide existe à 250 mètres
Quand je me déplace
Alors aucune notification n'est envoyée (hors rayon)
Quand je passe à 180 mètres
Alors une notification push est envoyée
Car je suis dans le rayon de 200m
# Quota anti-spam mode piéton
Scénario: Même quota 6 notifications/heure en mode piéton
Étant donné que je suis en mode piéton
Et que j'ai reçu 6 notifications push dans la dernière heure
Quand je passe près d'un 7ème audio-guide
Alors aucune notification n'est envoyée
Et le quota horaire est respecté
Scénario: Cooldown 10 min si notification ignorée (app pas ouverte)
Étant donné qu'une notification push a été envoyée
Et que je ne l'ai pas ouverte dans les 10 minutes
Quand le système détecte l'ignorance
Alors un cooldown de 10 minutes est activé
Et aucune nouvelle notification n'est envoyée pendant 10 min
# Basculement automatique
Scénario: Basculement transparent sans friction
Étant donné que je marche à 3 km/h (mode piéton)
Et que je monte dans ma voiture
Quand ma vitesse passe à 50 km/h (stable 10s)
Alors le basculement vers mode voiture s'effectue automatiquement
Et je n'ai rien à faire
Et aucune popup ne m'interrompt
Et l'expérience est transparente
Scénario: Notifications adaptées automatiquement
Étant donné que je passe de piéton (3 km/h) à voiture (50 km/h)
Quand le basculement s'effectue
Alors les notifications push arrière-plan sont désactivées
Et les notifications sonores + icône (app ouverte) sont activées
Et le rayon passe de 200m à ETA 7 secondes
Et l'adaptation est immédiate
# Garantie RGPD et fonctionnalité optionnelle
Scénario: Application utilisable sans permission arrière-plan
Étant donné que je refuse la permission "Autoriser toujours"
Quand j'utilise RoadWave
Alors le mode voiture fonctionne à 100%
Et je peux accéder à tous les audio-guides en mode manuel
Et je peux télécharger des contenus offline
Et seules les notifications push piéton sont désactivées
Scénario: Révocation permission arrière-plan désactive mode piéton
Étant donné que le mode piéton push était actif
Quand je révoque la permission dans les réglages iOS/Android
Et que j'ouvre l'application
Alors un message s'affiche: "Permission arrière-plan révoquée. Mode piéton désactivé."
Et le toggle est grisé dans les réglages
Et le mode voiture reste fonctionnel
Scénario: Désactivation mode piéton dans réglages
Étant donné que le mode piéton push est actif
Quand je vais dans Réglages > Notifications audio-guides piéton
Et que je désactive le toggle
Alors les notifications push sont arrêtées
Et le geofencing est désactivé
Et la permission arrière-plan reste accordée (mais non utilisée)
Et je peux réactiver plus tard
# Messages d'information
Scénario: Message clair sur usage permission arrière-plan
Étant donné que je consulte les réglages
Quand j'affiche les informations sur le mode piéton
Alors je vois:
"""
RoadWave utilise votre position en arrière-plan uniquement
pour vous alerter d'audio-guides à proximité quand vous
marchez. Cette fonctionnalité peut être désactivée à tout
moment. Votre position n'est jamais partagée avec des tiers.
"""
# Cas limites
Scénario: Mode indéterminé si vitesse exactement 5 km/h
Étant donné que ma vitesse moyenne est exactement 5.0 km/h
Quand le système vérifie le mode
Alors le mode actuel est conservé (pas de basculement)
Car le seuil est strict (< 5 km/h pour piéton, ≥ 5 km/h pour voiture)
Scénario: Basculement impossible si GPS désactivé
Étant donné que je désactive le GPS
Quand le système tente de détecter la vitesse
Alors aucun basculement automatique ne se produit
Et le mode actuel est conservé par défaut
Et un message "GPS désactivé" s'affiche si je tente de naviguer
Scénario: Notification push ignorée ne consomme pas de quota
Étant donné qu'une notification push est envoyée
Et que je ne l'ouvre pas (ignorée)
Quand je consulte mon quota
Alors cette notification compte quand même dans le quota 6/h
Et le cooldown 10 min est activé
Plan du Scénario: Basculement selon vitesse
Étant donné que ma vitesse moyenne est <vitesse> km/h pendant 10s
Et que je suis en mode <mode_initial>
Quand le système détecte la vitesse stable
Alors le mode devient <mode_final>
Exemples:
| vitesse | mode_initial | mode_final |
| 2 | voiture | piéton |
| 3 | voiture | piéton |
| 4 | voiture | piéton |
| 5 | piéton | voiture |
| 10 | piéton | voiture |
| 50 | piéton | voiture |
| 130 | piéton | voiture |
Plan du Scénario: Rayon de détection selon mode
Étant donné que je suis en mode <mode>
Quand un contenu géolocalisé existe à <distance>
Alors une notification est <decision>
Exemples:
| mode | distance | decision |
| piéton | 150m | envoyée |
| piéton | 199m | envoyée |
| piéton | 201m | non envoyée |
| piéton | 300m | non envoyée |
| voiture | 98m (ETA 7s) | envoyée |
| voiture | 150m (ETA 10s) | non envoyée |