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:
@@ -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
|
||||||
|
|||||||
314
features/api/navigation/file-attente.feature
Normal file
314
features/api/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 |
|
||||||
390
features/api/navigation/notifications-geolocalisees.feature
Normal file
390
features/api/navigation/notifications-geolocalisees.feature
Normal 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 |
|
||||||
265
features/ui/navigation/mode-pieton-notifications-push.feature
Normal file
265
features/ui/navigation/mode-pieton-notifications-push.feature
Normal 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 |
|
||||||
Reference in New Issue
Block a user