feat(bdd): réorganiser features en catégories api/ui/e2e et créer ADR-024
Résolution des incohérences #10, #11, et #12 de l'analyse d'architecture. ## Phase 1 : Réorganisation Features BDD (Point #10 - RÉSOLU) - Créer structure features/{api,ui,e2e} - Déplacer 83 features en 3 catégories via git mv (historique préservé) - features/api/ : 53 features (tests API backend) - features/ui/ : 22 features (tests UI mobile) - features/e2e/ : 8 features (tests end-to-end) Domaines déplacés : - API : authentication, recommendation, rgpd-compliance, content-creation, moderation, monetisation, premium, radio-live, publicites - UI : audio-guides, navigation, interest-gauges, mode-offline, partage, profil, recherche - E2E : abonnements, error-handling ## Phase 2 : Mise à jour Documentation ### ADR-007 - Tests BDD - Ajouter section "Convention de Catégorisation des Features" - Documenter règles api/ui/e2e avec exemples concrets - Spécifier step definitions (backend Go, mobile Dart) ### ADR-024 - Stratégie CI/CD Monorepo (NOUVEAU) - Créer ADR dédié pour stratégie CI/CD avec path filters - Architecture workflows séparés (backend.yml, mobile.yml, shared.yml) - Configuration path filters détaillée avec exemples YAML - Matrice de déclenchement et optimisations (~70% gain temps CI) - Plan d'implémentation (~2h, reporté jusqu'au développement) ### ADR-016 - Organisation Monorepo - Simplifier en retirant section CI/CD détaillée - Ajouter référence vers ADR-024 pour stratégie CI/CD ### INCONSISTENCIES-ANALYSIS.md - Point #10 (Tests BDD synchronisés) : ✅ RÉSOLU - Catégorisation features implémentée - ADR-007 mis à jour avec convention complète - Point #11 (70/30 Split paiements) : ✅ ANNULÉ (faux problème) - ADR-009 et Règle 18 parfaitement cohérents - Documentation exhaustive existante (formule, SQL, comparaisons) - Point #12 (Monorepo path filters) : ⏸️ DOCUMENTÉ - Architecture CI/CD complète dans ADR-024 - Implémentation reportée (projet en phase documentation) - Métriques mises à jour : - MODERATE : 6/9 traités (4 résolus + 1 annulé + 1 documenté) - ADR à jour : 100% (19/19 avec ADR-024) ## Phase 3 : Validation - Structure features validée (api/ui/e2e, aucun répertoire restant) - Historique Git préservé (git mv, renommages détectés) - 83 features total (API: 53, UI: 22, E2E: 8) Closes: Point #10 (résolu), Point #11 (annulé), Point #12 (documenté) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
240
features/api/recommendation/README.md
Normal file
240
features/api/recommendation/README.md
Normal file
@@ -0,0 +1,240 @@
|
||||
# Tests Gherkin - Algorithme de Recommandation
|
||||
|
||||
Tests BDD pour la section [04-algorithme-recommandation.md](../../docs/regles-metier/04-algorithme-recommandation.md)
|
||||
|
||||
## Fichiers de tests
|
||||
|
||||
### [classification-geo.feature](classification-geo.feature)
|
||||
**Couverture** : Section 2.1 des règles métier
|
||||
|
||||
- ✅ Classification 3 types (Géo-ancré 70%, Géo-contextuel 50%, Géo-neutre 20%)
|
||||
- ✅ Choix par créateur
|
||||
- ✅ Reclassification par modérateur
|
||||
- ✅ Modification après publication
|
||||
- ✅ Impact sur pondération algorithme
|
||||
|
||||
**Scénarios** : 11
|
||||
|
||||
---
|
||||
|
||||
### [scoring-recommandation.feature](scoring-recommandation.feature)
|
||||
**Couverture** : Sections 2.2, 2.3, 2.4 des règles métier
|
||||
|
||||
- ✅ Calcul score géographique linéaire (1 - distance/200km)
|
||||
- ✅ Calcul score d'intérêts (moyenne jauges tags)
|
||||
- ✅ Calcul score engagement (complétion 50%, likes 30%, abonnements 20%)
|
||||
- ✅ Seuil minimum 50 écoutes
|
||||
- ✅ Score final combiné selon type contenu
|
||||
- ✅ Bonus aléatoire 10% configurable
|
||||
- ✅ Contenu viral peut être recommandé loin
|
||||
- ✅ Pré-calcul 5 contenus suivants
|
||||
- ✅ Recalcul si >10 km ou >10 min
|
||||
|
||||
**Scénarios** : 23
|
||||
|
||||
---
|
||||
|
||||
### [contenu-politique.feature](contenu-politique.feature)
|
||||
**Couverture** : Section 2.5 des règles métier (MVP simplifié)
|
||||
|
||||
- ✅ Tag simple "Politique" sans classification gauche/droite
|
||||
- ✅ Filtrage utilisateur "Masquer contenu politique"
|
||||
- ✅ Par défaut tous contenus visibles
|
||||
- ✅ Mode Kids filtre automatiquement le politique
|
||||
- ✅ Pas d'équilibrage imposé en MVP
|
||||
|
||||
**Scénarios** : 13
|
||||
|
||||
---
|
||||
|
||||
### [mode-kids.feature](mode-kids.feature)
|
||||
**Couverture** : Section 2.6 des règles métier
|
||||
|
||||
- ✅ Activation manuelle (pas automatique car âge min 13 ans)
|
||||
- ✅ Filtrage contenus "Tous publics" uniquement
|
||||
- ✅ Exclusion automatique contenu politique
|
||||
- ✅ Pas de publicité (ou validée manuellement)
|
||||
- ✅ Interface standard (pas d'UI enfant)
|
||||
- ✅ Désactivation possible à tout moment
|
||||
|
||||
**Scénarios** : 15
|
||||
|
||||
---
|
||||
|
||||
### [declenchement-geo.feature](declenchement-geo.feature)
|
||||
**Couverture** : Section 2.7 des règles métier
|
||||
|
||||
- ✅ Notification sonore + visuelle au passage <500m
|
||||
- ✅ Délai réaction 5 secondes
|
||||
- ✅ Pas d'interruption contenu en cours
|
||||
- ✅ Logos différenciés (📍🏛️🍴🎭)
|
||||
- ✅ Publicité uniquement entre contenus
|
||||
- ✅ Gestion demi-tour (pas de répétition avant 24h)
|
||||
- ✅ Rayon configurable par admin
|
||||
|
||||
**Scénarios** : 17
|
||||
|
||||
---
|
||||
|
||||
### [historique-reproposition.feature](historique-reproposition.feature)
|
||||
**Couverture** : Section 2.8 des règles métier
|
||||
|
||||
- ✅ Contenu >80% jamais reproposé (sauf replayable=true)
|
||||
- ✅ Contenu <10s ne pas reproposer (signal négatif)
|
||||
- ✅ Contenu 10-80% reproposer avec reprise position
|
||||
- ✅ Stockage illimité PostgreSQL
|
||||
- ✅ Algorithme considère 100 derniers pour performance
|
||||
- ✅ Export complet RGPD
|
||||
|
||||
**Scénarios** : 17
|
||||
|
||||
---
|
||||
|
||||
### [parametrabilite-admin.feature](parametrabilite-admin.feature)
|
||||
**Couverture** : Section 2.9 des règles métier
|
||||
|
||||
- ✅ Dashboard admin avec tous paramètres configurables à chaud
|
||||
- ✅ Validation plages de valeurs
|
||||
- ✅ Aucun recalcul batch (économie CPU)
|
||||
- ✅ Versioning configurations (git-like)
|
||||
- ✅ Rollback 1 clic
|
||||
- ✅ A/B testing avec split 50/50
|
||||
- ✅ Métriques comparatives temps réel
|
||||
- ✅ Graphiques évolution engagement
|
||||
- ✅ Export CSV analyse externe
|
||||
|
||||
**Scénarios** : 17
|
||||
|
||||
---
|
||||
|
||||
### [parametrabilite-utilisateur.feature](parametrabilite-utilisateur.feature)
|
||||
**Couverture** : Section 2.10 des règles métier
|
||||
|
||||
- ✅ 3 curseurs (Géolocalisation, Découverte, Politique)
|
||||
- ✅ Profils sauvegardables (Trajet quotidien, Road trip, Enfants)
|
||||
- ✅ Synchronisation multi-devices
|
||||
- ✅ Auto-switch selon contexte GPS
|
||||
- ✅ Blocage modification si vitesse >10 km/h
|
||||
- ✅ Warning avant de prendre la route
|
||||
- ✅ Limite 10 profils par utilisateur
|
||||
|
||||
**Scénarios** : 22
|
||||
|
||||
---
|
||||
|
||||
### [medias-traditionnels.feature](medias-traditionnels.feature)
|
||||
**Couverture** : Section 2.11 des règles métier
|
||||
|
||||
- ✅ Compte média vérifié (badge ✓)
|
||||
- ✅ Pas de validation 3 premiers contenus
|
||||
- ✅ Modération a posteriori uniquement
|
||||
- ✅ Formats: flash info, chroniques, éditos, reportages
|
||||
- ✅ Classification âge obligatoire
|
||||
- ✅ Monétisation standard (3€/1000 écoutes)
|
||||
- ✅ Sponsoring direct autorisé
|
||||
- ✅ Statistiques détaillées
|
||||
|
||||
**Scénarios** : 19
|
||||
|
||||
---
|
||||
|
||||
## Statistiques
|
||||
|
||||
| Métrique | Valeur |
|
||||
|----------|--------|
|
||||
| **Fichiers** | 9 |
|
||||
| **Scénarios** | 154 |
|
||||
| **Règles métier** | 100% couverture section 2 |
|
||||
|
||||
## Formules mathématiques testées
|
||||
|
||||
### Score géographique
|
||||
```
|
||||
score_geo = 1 - (distance_km / distance_max_km)
|
||||
```
|
||||
|
||||
### Score intérêts
|
||||
```
|
||||
score_interets = moyenne(jauges_tags_correspondants)
|
||||
```
|
||||
|
||||
### Score engagement
|
||||
```
|
||||
score_engagement = (taux_completion × 0.5) + (ratio_likes × 0.3) + (ratio_abonnements × 0.2)
|
||||
```
|
||||
|
||||
### Score final
|
||||
```
|
||||
score_final = (score_geo × poids_geo_type)
|
||||
+ (score_interets × poids_interets_type)
|
||||
+ (score_engagement × 0.2)
|
||||
+ bonus_aleatoire
|
||||
```
|
||||
|
||||
## Paramètres par défaut
|
||||
|
||||
| Paramètre | Défaut | Plage |
|
||||
|-----------|--------|-------|
|
||||
| poids_geo_ancre | 0.7 | 0.5 - 1.0 |
|
||||
| poids_geo_contextuel | 0.5 | 0.3 - 0.7 |
|
||||
| poids_geo_neutre | 0.2 | 0.0 - 0.4 |
|
||||
| poids_engagement | 0.2 | 0.0 - 0.5 |
|
||||
| part_aleatoire_global | 0.1 | 0.0 - 0.3 |
|
||||
| distance_max_km | 200 | 50 - 500 |
|
||||
| rayon_gps_point_m | 500 | 100 - 2000 |
|
||||
| seuil_min_ecoutes_engagement | 50 | 10 - 200 |
|
||||
|
||||
## Exécution des tests
|
||||
|
||||
### Tous les tests de recommandation
|
||||
```bash
|
||||
godog run features/recommendation/
|
||||
```
|
||||
|
||||
### Un fichier spécifique
|
||||
```bash
|
||||
godog run features/recommendation/scoring-recommandation.feature
|
||||
```
|
||||
|
||||
### Tests calculs mathématiques uniquement
|
||||
```bash
|
||||
godog run features/recommendation/scoring-recommandation.feature --tags=@calcul
|
||||
```
|
||||
|
||||
## CI/CD
|
||||
|
||||
Ces tests sont exécutés :
|
||||
- ✅ Avant chaque release
|
||||
- ✅ Sur les PRs modifiant l'algorithme de recommandation
|
||||
- ✅ Nightly builds (tous les tests)
|
||||
|
||||
## Implémentation des steps
|
||||
|
||||
Les steps definitions seront implémentées dans :
|
||||
```
|
||||
features/steps/recommendation_steps.go
|
||||
```
|
||||
|
||||
Exemple avec calculs :
|
||||
```go
|
||||
func (s *RecommendationSteps) lalgorithmeCalculeLeScoreGeo(expectedScore float64) error {
|
||||
actualScore := 1.0 - (s.distance / s.distanceMax)
|
||||
if math.Abs(actualScore - expectedScore) > 0.01 {
|
||||
return fmt.Errorf("score_geo: attendu %.2f, obtenu %.2f", expectedScore, actualScore)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## Prochaines étapes
|
||||
|
||||
1. ⏳ Implémenter les steps definitions en Go
|
||||
2. ⏳ Tester les formules mathématiques avec valeurs edge cases
|
||||
3. ⏳ Configurer Testcontainers pour PostgreSQL + PostGIS
|
||||
4. ⏳ Tests de performance (calcul score pour 1000 contenus <100ms)
|
||||
5. ⏳ Créer les Gherkin pour les 14 autres sections
|
||||
|
||||
---
|
||||
|
||||
**Statut** : ✅ Spécifications complètes
|
||||
**Dernière mise à jour** : 2026-01-21
|
||||
100
features/api/recommendation/classification-geo.feature
Normal file
100
features/api/recommendation/classification-geo.feature
Normal file
@@ -0,0 +1,100 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Classification de géo-pertinence des contenus
|
||||
En tant que plateforme de contenu géolocalisé
|
||||
Je veux classifier les contenus selon leur pertinence géographique
|
||||
Afin d'adapter l'algorithme de recommandation
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est disponible
|
||||
|
||||
Scénario: Créateur choisit le type géo-ancré pour un audio-guide
|
||||
Étant donné que je suis un créateur connecté
|
||||
Quand je publie un audio-guide de la Tour Eiffel
|
||||
Et que je choisis la classification "Géo-ancré"
|
||||
Alors le contenu est enregistré avec:
|
||||
| champ | valeur |
|
||||
| type_geo | geo_ancre |
|
||||
| ponderation_geo | 0.7 |
|
||||
| ponderation_interets | 0.1 |
|
||||
|
||||
Scénario: Créateur choisit le type géo-contextuel pour actualité régionale
|
||||
Étant donné que je suis un créateur connecté
|
||||
Quand je publie une actualité régionale en Bretagne
|
||||
Et que je choisis la classification "Géo-contextuel"
|
||||
Alors le contenu est enregistré avec:
|
||||
| champ | valeur |
|
||||
| type_geo | geo_contextuel |
|
||||
| ponderation_geo | 0.5 |
|
||||
| ponderation_interets | 0.3 |
|
||||
|
||||
Scénario: Créateur choisit le type géo-neutre pour un podcast philosophie
|
||||
Étant donné que je suis un créateur connecté
|
||||
Quand je publie un podcast de philosophie
|
||||
Et que je choisis la classification "Géo-neutre"
|
||||
Alors le contenu est enregistré avec:
|
||||
| champ | valeur |
|
||||
| type_geo | geo_neutre |
|
||||
| ponderation_geo | 0.2 |
|
||||
| ponderation_interets | 0.6 |
|
||||
|
||||
Scénario: Publication impossible sans classification géographique
|
||||
Étant donné que je crée un contenu audio
|
||||
Quand j'essaie de publier sans sélectionner de type géographique
|
||||
Alors la publication échoue
|
||||
Et je vois le message "Vous devez sélectionner un type de géo-pertinence"
|
||||
|
||||
Scénario: Modérateur reclassifie un contenu mal catégorisé
|
||||
Étant donné qu'un contenu podcast générique est classifié "Géo-ancré"
|
||||
Et que le modérateur examine le contenu
|
||||
Quand le modérateur le reclassifie en "Géo-neutre"
|
||||
Alors la nouvelle classification est appliquée immédiatement
|
||||
Et l'algorithme utilise la pondération géo = 0.2
|
||||
Et le créateur reçoit une notification de reclassification
|
||||
|
||||
Scénario: Créateur modifie la classification après publication
|
||||
Étant donné que j'ai publié un contenu classifié "Géo-contextuel"
|
||||
Et que je réalise qu'il devrait être "Géo-neutre"
|
||||
Quand je modifie la classification en "Géo-neutre"
|
||||
Alors la modification est enregistrée
|
||||
Et l'algorithme utilise la nouvelle pondération
|
||||
Et je vois le message "Classification modifiée avec succès"
|
||||
|
||||
Scénario: Statistiques de classification dans le profil créateur
|
||||
Étant donné que je suis un créateur
|
||||
Et que j'ai publié 30 contenus:
|
||||
| type | nombre |
|
||||
| Géo-ancré | 10 |
|
||||
| Géo-contextuel | 15 |
|
||||
| Géo-neutre | 5 |
|
||||
Quand je consulte mes statistiques
|
||||
Alors je vois la répartition de mes classifications
|
||||
Et des suggestions pour optimiser la portée
|
||||
|
||||
Scénario: Contenu géo-ancré fortement pondéré par la proximité
|
||||
Étant donné qu'un audio-guide "Géo-ancré" existe à la Tour Eiffel
|
||||
Et qu'un utilisateur est à 100m de la Tour Eiffel
|
||||
Quand l'algorithme calcule le score
|
||||
Alors la pondération géo est de 0.7
|
||||
Et le score géo est proche de 1 (très proche)
|
||||
Et le contenu a un score final élevé
|
||||
|
||||
Scénario: Contenu géo-neutre moins sensible à la distance
|
||||
Étant donné qu'un podcast philosophie "Géo-neutre" existe à Paris
|
||||
Et qu'un utilisateur est à Marseille (750 km)
|
||||
Quand l'algorithme calcule le score
|
||||
Alors la pondération géo est de 0.2
|
||||
Et le score géo est bas (distance élevée)
|
||||
Mais le score intérêts (0.6) peut compenser
|
||||
Et le contenu peut quand même être recommandé si intérêts match
|
||||
|
||||
Scénario: Comparaison scores entre types géo pour même distance
|
||||
Étant donné 3 contenus au même endroit (Paris):
|
||||
| type | ponderation_geo |
|
||||
| Géo-ancré | 0.7 |
|
||||
| Géo-contextuel | 0.5 |
|
||||
| Géo-neutre | 0.2 |
|
||||
Et qu'un utilisateur est à 50 km de Paris
|
||||
Quand l'algorithme calcule les scores
|
||||
Alors le contenu "Géo-ancré" a le score géo le plus élevé
|
||||
Et le contenu "Géo-neutre" a le score géo le plus faible
|
||||
Mais peut avoir un score final plus élevé si forte correspondance intérêts
|
||||
100
features/api/recommendation/contenu-politique.feature
Normal file
100
features/api/recommendation/contenu-politique.feature
Normal file
@@ -0,0 +1,100 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Gestion du contenu politique (MVP simplifié)
|
||||
En tant qu'utilisateur
|
||||
Je veux pouvoir filtrer le contenu politique
|
||||
Afin de contrôler mon exposition à ce type de contenu
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est disponible
|
||||
|
||||
Scénario: Créateur tagge son contenu comme "Politique"
|
||||
Étant donné que je suis un créateur connecté
|
||||
Quand je publie un contenu sur un débat politique
|
||||
Et que je sélectionne le tag "Politique"
|
||||
Alors le contenu est enregistré avec le tag "Politique"
|
||||
Et aucune classification gauche/droite n'est demandée (MVP)
|
||||
|
||||
Scénario: Tag "Politique" au même niveau que les autres tags
|
||||
Étant donné que je crée un contenu
|
||||
Quand je consulte la liste des tags disponibles
|
||||
Alors je vois les tags suivants au même niveau:
|
||||
| tag |
|
||||
| Économie |
|
||||
| Sport |
|
||||
| Culture |
|
||||
| Politique |
|
||||
| Automobile |
|
||||
| Voyage |
|
||||
| Musique |
|
||||
|
||||
Scénario: Par défaut, tous les contenus politiques sont visibles
|
||||
Étant donné que je suis un nouvel utilisateur
|
||||
Et que je n'ai pas modifié les paramètres de contenu politique
|
||||
Quand je demande des recommandations
|
||||
Alors les contenus tagués "Politique" sont inclus normalement
|
||||
Et aucun filtrage n'est appliqué
|
||||
|
||||
Scénario: Activer le filtrage "Masquer contenu politique"
|
||||
Étant donné que je suis connecté
|
||||
Quand j'active l'option "Masquer contenu politique" dans les paramètres
|
||||
Alors tous les contenus tagués "Politique" sont exclus de mes recommandations
|
||||
Et je vois le message "Contenu politique masqué"
|
||||
|
||||
Scénario: Filtrage politique actif - aucun contenu politique recommandé
|
||||
Étant donné que j'ai activé "Masquer contenu politique"
|
||||
Et qu'il existe 100 contenus dont 20 tagués "Politique"
|
||||
Quand je demande 50 recommandations
|
||||
Alors je reçois 50 contenus parmi les 80 non-politiques
|
||||
Et 0% de contenus politiques sont proposés
|
||||
|
||||
Scénario: Désactiver le filtrage "Masquer contenu politique"
|
||||
Étant donné que j'ai activé "Masquer contenu politique"
|
||||
Quand je désactive cette option dans les paramètres
|
||||
Alors les contenus politiques sont à nouveau inclus dans mes recommandations
|
||||
Et le filtrage est levé immédiatement
|
||||
|
||||
Scénario: Mode Kids filtre automatiquement le contenu politique
|
||||
Étant donné que je suis un utilisateur de 14 ans
|
||||
Et que le mode Kids est activé
|
||||
Quand je demande des recommandations
|
||||
Alors tous les contenus tagués "Politique" sont automatiquement exclus
|
||||
Et ce indépendamment du paramètre "Masquer contenu politique"
|
||||
|
||||
Scénario: Statistiques créateur sur contenu politique
|
||||
Étant donné que je suis un créateur
|
||||
Et que j'ai publié 20 contenus dont 5 tagués "Politique"
|
||||
Quand je consulte mes statistiques
|
||||
Alors je vois le nombre d'utilisateurs ayant masqué le contenu politique
|
||||
Et le taux d'engagement comparé aux autres tags
|
||||
|
||||
Scénario: Recherche avec tag "Politique"
|
||||
Étant donné que je recherche du contenu
|
||||
Quand je filtre par tag "Politique"
|
||||
Alors seuls les contenus tagués "Politique" sont affichés
|
||||
Et ce même si j'ai activé "Masquer contenu politique" (recherche explicite)
|
||||
|
||||
Scénario: Partage de contenu politique avec filtre actif
|
||||
Étant donné que j'ai activé "Masquer contenu politique"
|
||||
Et qu'un ami me partage un lien vers un contenu tagué "Politique"
|
||||
Quand j'ouvre le lien
|
||||
Alors je peux accéder au contenu (partage explicite)
|
||||
Et je vois un avertissement "Ce contenu est tagué Politique"
|
||||
|
||||
Scénario: Pas de classification gauche/droite en MVP
|
||||
Étant donné que je suis un créateur
|
||||
Quand je publie un contenu tagué "Politique"
|
||||
Alors aucune option de classification idéologique n'est proposée
|
||||
Et je ne peux pas indiquer "Gauche", "Droite", "Centre", etc.
|
||||
|
||||
Scénario: Pas d'équilibrage imposé en MVP
|
||||
Étant donné qu'un utilisateur écoute majoritairement du contenu politique de gauche
|
||||
Quand l'algorithme génère des recommandations
|
||||
Alors aucun équilibrage droite/gauche n'est appliqué
|
||||
Et les recommandations suivent l'algorithme standard (intérêts, géo, engagement)
|
||||
|
||||
Scénario: Notification post-MVP pour classification avancée
|
||||
Étant donné que RoadWave passe en phase post-MVP
|
||||
Et que la classification politique avancée est activée
|
||||
Quand je me connecte
|
||||
Alors je reçois une notification m'informant des nouvelles options
|
||||
Et je peux configurer mes préférences d'équilibrage politique
|
||||
327
features/api/recommendation/declenchement-geo.feature
Normal file
327
features/api/recommendation/declenchement-geo.feature
Normal file
@@ -0,0 +1,327 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Contenus géolocalisés en mode voiture
|
||||
En tant qu'utilisateur en voiture
|
||||
Je veux recevoir des notifications de contenus géolocalisés au bon moment
|
||||
Afin de découvrir du contenu contextuel sans distraction au volant
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est disponible
|
||||
Et que l'application est ouverte (premier plan)
|
||||
Et que le GPS est activé
|
||||
Et que l'utilisateur est en mode voiture (vitesse ≥ 5 km/h)
|
||||
|
||||
# 17.2 - Détection et notification (Calcul ETA)
|
||||
|
||||
Scénario: Calcul ETA et notification 7 secondes avant le point GPS
|
||||
Étant donné qu'un contenu géolocalisé existe à la Tour Eiffel (48.8584, 2.2945)
|
||||
Et que je me déplace à 50 km/h vers ce point
|
||||
Et que je suis à 98 mètres du point (ETA = 7 secondes)
|
||||
Quand le système calcule l'ETA
|
||||
Alors une notification est déclenchée immédiatement
|
||||
Et le compteur "7" s'affiche avec l'icône 🏛️
|
||||
Et une notification sonore (bip court) est jouée
|
||||
|
||||
Plan du Scénario: Calcul ETA à différentes vitesses
|
||||
Étant donné qu'un contenu géolocalisé existe à un point GPS
|
||||
Et que je me déplace à <vitesse> km/h
|
||||
Quand je suis à <distance> mètres du point
|
||||
Alors l'ETA calculé est <eta> secondes
|
||||
Et la notification est déclenchée : <notification>
|
||||
|
||||
Exemples:
|
||||
| vitesse | distance | eta | notification |
|
||||
| 10 | 19 | 7 | Oui |
|
||||
| 50 | 98 | 7 | Oui |
|
||||
| 130 | 252 | 7 | Oui |
|
||||
| 50 | 200 | 14 | Non |
|
||||
| 10 | 50 | 18 | Non |
|
||||
|
||||
Scénario: Notification immédiate si vitesse <5 km/h ET distance <50m
|
||||
Étant donné qu'un contenu géolocalisé existe à 30m de ma position
|
||||
Et que ma vitesse est 3 km/h (arrêté à un feu rouge)
|
||||
Quand le système détecte cette situation
|
||||
Alors une notification est déclenchée immédiatement
|
||||
Et je n'ai pas besoin d'attendre le calcul ETA
|
||||
|
||||
# 17.2.2 - Format de notification minimaliste
|
||||
|
||||
Scénario: Notification minimaliste sans texte (sécurité routière)
|
||||
Étant donné qu'une notification géolocalisée est déclenchée
|
||||
Quand la notification s'affiche
|
||||
Alors les éléments suivants sont visibles:
|
||||
| élément | présent |
|
||||
| Icône du tag | ✅ |
|
||||
| Compteur 7→1 | ✅ |
|
||||
| Son bref (bip) | ✅ |
|
||||
| Titre texte | ❌ |
|
||||
| Description | ❌ |
|
||||
| Cover image | ❌ |
|
||||
| Bouton Annuler | ❌ |
|
||||
|
||||
Plan du Scénario: Icônes selon le tag du contenu
|
||||
Étant donné qu'un contenu géolocalisé avec le tag <tag> est disponible
|
||||
Quand la notification s'affiche
|
||||
Alors l'icône <icone> est affichée
|
||||
|
||||
Exemples:
|
||||
| tag | icone |
|
||||
| Culture générale | 🏛️ |
|
||||
| Histoire | 📜 |
|
||||
| Voyage | ✈️ |
|
||||
| Famille | 👨👩👧 |
|
||||
| Musique | 🎵 |
|
||||
| Sport | ⚽ |
|
||||
| Technologie | 💻 |
|
||||
| Automobile | 🚗 |
|
||||
|
||||
Scénario: Compteur décrémentant de 7 à 1
|
||||
Étant donné qu'une notification géolocalisée s'affiche
|
||||
Quand le compteur démarre
|
||||
Alors le compteur affiche "7"
|
||||
Et après 1 seconde, il affiche "6"
|
||||
Et après 2 secondes, il affiche "5"
|
||||
Et après 3 secondes, il affiche "4"
|
||||
Et après 4 secondes, il affiche "3"
|
||||
Et après 5 secondes, il affiche "2"
|
||||
Et après 6 secondes, il affiche "1"
|
||||
Et après 7 secondes, la notification disparaît
|
||||
|
||||
# 17.2.2b - Conformité CarPlay / Android Auto
|
||||
|
||||
Scénario: Notification sonore uniquement en mode CarPlay
|
||||
Étant donné que l'application est connectée à CarPlay
|
||||
Et qu'un contenu géolocalisé est détecté (ETA 7s)
|
||||
Quand la notification est déclenchée
|
||||
Alors seule la notification sonore (bip) est jouée
|
||||
Et aucun overlay visuel n'est affiché (icône, compteur)
|
||||
Et l'utilisateur peut valider via le bouton "Suivant" au volant
|
||||
|
||||
Scénario: Notification sonore uniquement en mode Android Auto
|
||||
Étant donné que l'application est connectée à Android Auto
|
||||
Et qu'un contenu géolocalisé est détecté (ETA 7s)
|
||||
Quand la notification est déclenchée
|
||||
Alors seule la notification sonore (bip) est jouée
|
||||
Et aucun overlay visuel n'est affiché
|
||||
Et l'utilisateur peut valider via le bouton "Suivant" au volant
|
||||
|
||||
Scénario: Notification complète (sonore + visuelle) en mode normal
|
||||
Étant donné que l'application n'est PAS connectée à CarPlay/Android Auto
|
||||
Et qu'un contenu géolocalisé est détecté
|
||||
Quand la notification est déclenchée
|
||||
Alors la notification sonore (bip) est jouée
|
||||
Et l'overlay visuel s'affiche (icône + compteur 7→1)
|
||||
|
||||
# 17.2.3 - Décompte après validation
|
||||
|
||||
Scénario: Validation via bouton "Suivant" et décompte 5 secondes
|
||||
Étant donné qu'une notification géolocalisée est affichée (compteur à 5)
|
||||
Et que j'écoute un podcast
|
||||
Quand j'appuie sur le bouton "Suivant"
|
||||
Alors le compteur bascule à "5" (décompte final)
|
||||
Et le contenu actuel continue de jouer normalement
|
||||
Et le compteur décrémente: 5→4→3→2→1
|
||||
Et après 5 secondes, le contenu géolocalisé démarre (fade in 0.3s)
|
||||
|
||||
Scénario: Transition fluide avec fade out/in
|
||||
Étant donné que le décompte atteint "0"
|
||||
Quand le contenu géolocalisé doit démarrer
|
||||
Alors le contenu actuel fait un fade out de 0.3s
|
||||
Et le contenu géolocalisé fait un fade in de 0.3s
|
||||
Et il n'y a pas de silence entre les deux
|
||||
|
||||
Scénario: Contenu actuel se termine pendant le décompte
|
||||
Étant donné que j'ai validé la notification (décompte 5s démarre)
|
||||
Et que mon contenu actuel se termine après 2 secondes
|
||||
Quand le contenu actuel se termine
|
||||
Alors le contenu suivant du buffer démarre immédiatement
|
||||
Et le décompte continue (3→2→1)
|
||||
Et à la fin du décompte, le contenu géolocalisé remplace le buffer
|
||||
|
||||
Scénario: Ignorance de la notification (pas de clic pendant 7s)
|
||||
Étant donné qu'une notification géolocalisée s'affiche (compteur 7)
|
||||
Quand 7 secondes s'écoulent sans que j'appuie sur "Suivant"
|
||||
Alors la notification disparaît automatiquement
|
||||
Et le contenu géolocalisé est perdu (pas d'insertion dans la file)
|
||||
Et un cooldown de 10 minutes est activé
|
||||
|
||||
# 17.3 - Limitation anti-spam
|
||||
|
||||
Scénario: Quota de 6 contenus géolocalisés par heure
|
||||
Étant donné que j'ai validé 6 notifications géolocalisées dans la dernière heure
|
||||
Quand un 7ème contenu géolocalisé est détecté
|
||||
Alors aucune notification n'est envoyée
|
||||
Et le contenu n'est pas inséré dans la file
|
||||
|
||||
Scénario: Fenêtre glissante de 60 minutes
|
||||
Étant donné que j'ai validé 6 contenus géolocalisés
|
||||
Et que le premier contenu a été validé il y a 61 minutes
|
||||
Quand un nouveau contenu géolocalisé est détecté
|
||||
Alors la notification est envoyée (quota libéré : 5/6)
|
||||
Et le compteur horaire est mis à jour
|
||||
|
||||
Plan du Scénario: Gestion du quota horaire
|
||||
Étant donné que <nb_valides> notifications ont été validées dans la dernière heure
|
||||
Quand un nouveau contenu géolocalisé est détecté
|
||||
Alors la notification est <action>
|
||||
|
||||
Exemples:
|
||||
| nb_valides | action |
|
||||
| 0 | envoyée |
|
||||
| 3 | envoyée |
|
||||
| 5 | envoyée |
|
||||
| 6 | non envoyée |
|
||||
| 7 | non envoyée |
|
||||
|
||||
Scénario: Exception audio-guides multi-séquences (comptent comme 1)
|
||||
Étant donné que j'ai démarré un audio-guide avec 8 séquences
|
||||
Et que cet audio-guide compte comme 1 contenu dans le quota
|
||||
Quand toutes les séquences de l'audio-guide sont lues
|
||||
Alors mon quota reste à 1/6
|
||||
Et je peux encore valider 5 contenus géolocalisés simples
|
||||
|
||||
# 17.3.2 - Cooldown après ignorance
|
||||
|
||||
Scénario: Cooldown de 10 minutes après notification ignorée
|
||||
Étant donné qu'une notification géolocalisée a été ignorée (pas de clic)
|
||||
Et qu'un cooldown de 10 minutes est activé
|
||||
Quand 5 minutes s'écoulent
|
||||
Et qu'un nouveau contenu géolocalisé est détecté
|
||||
Alors aucune notification n'est envoyée (cooldown actif)
|
||||
|
||||
Scénario: Cooldown expire après 10 minutes
|
||||
Étant donné qu'un cooldown a été activé il y a 10 minutes
|
||||
Quand un nouveau contenu géolocalisé est détecté
|
||||
Alors la notification est envoyée (cooldown expiré)
|
||||
|
||||
Scénario: Pas de cooldown si notification validée
|
||||
Étant donné qu'une notification géolocalisée est affichée
|
||||
Quand j'appuie sur "Suivant" dans les 7 secondes
|
||||
Alors aucun cooldown n'est activé
|
||||
Et la prochaine notification pourra être envoyée normalement
|
||||
|
||||
# 17.4 - Navigation avec contenus géolocalisés
|
||||
|
||||
Scénario: Contenu géolocalisé dans l'historique de navigation
|
||||
Étant donné que j'écoute un contenu du buffer
|
||||
Et que j'ai validé un contenu géolocalisé "Tour Eiffel"
|
||||
Et que j'ai écouté 42 secondes du contenu géolocalisé
|
||||
Quand j'appuie sur "Suivant" (skip)
|
||||
Et que j'appuie ensuite sur "Précédent"
|
||||
Alors le contenu géolocalisé reprend à 42 secondes
|
||||
|
||||
Scénario: Contenu ignoré n'entre pas dans l'historique
|
||||
Étant donné qu'une notification géolocalisée a été ignorée
|
||||
Quand j'appuie sur "Précédent"
|
||||
Alors le contenu géolocalisé ignoré n'apparaît PAS dans l'historique
|
||||
Et je reviens au contenu d'avant
|
||||
|
||||
Scénario: Skip pendant le décompte annule l'insertion
|
||||
Étant donné que j'ai validé une notification (décompte 5s en cours)
|
||||
Et que le compteur affiche "3"
|
||||
Quand j'appuie à nouveau sur "Suivant"
|
||||
Alors le décompte est annulé
|
||||
Et le contenu suivant du buffer démarre
|
||||
Et le contenu géolocalisé n'entre PAS dans l'historique
|
||||
|
||||
# 17.5 - Basculement automatique voiture ↔ piéton
|
||||
|
||||
Scénario: Détection mode piéton (vitesse <5 km/h stable 10s)
|
||||
Étant donné que je suis en mode voiture
|
||||
Et que ma vitesse passe à 3 km/h
|
||||
Quand cette vitesse reste stable pendant 10 secondes
|
||||
Alors le mode piéton est activé automatiquement
|
||||
Et les notifications passent en mode push arrière-plan (si permission accordée)
|
||||
|
||||
Scénario: Détection mode voiture (vitesse ≥5 km/h stable 10s)
|
||||
Étant donné que je suis en mode piéton
|
||||
Et que ma vitesse passe à 15 km/h
|
||||
Quand cette vitesse reste stable pendant 10 secondes
|
||||
Alors le mode voiture est activé automatiquement
|
||||
Et les notifications passent en mode sonore + icône (app premier plan requise)
|
||||
|
||||
Scénario: Hysteresis pour éviter basculements intempestifs
|
||||
Étant donné que ma vitesse passe de 20 km/h à 3 km/h (arrêt feu rouge)
|
||||
Et que ma vitesse remonte à 20 km/h après 8 secondes
|
||||
Quand le système vérifie le mode
|
||||
Alors aucun basculement n'a lieu (hysteresis de 10s non atteinte)
|
||||
Et je reste en mode voiture
|
||||
|
||||
Plan du Scénario: Effets du basculement voiture → piéton
|
||||
Étant donné que je bascule de voiture à piéton
|
||||
Quand le basculement est effectué
|
||||
Alors les paramètres suivants changent:
|
||||
| paramètre | voiture | piéton |
|
||||
| App requise | Premier plan | Arrière-plan OK |
|
||||
| Notification | Sonore + icône + compteur| Push système |
|
||||
| Rayon détection | ETA 7s (variable) | 200m fixes |
|
||||
| Type contenu | Tous géolocalisés | Audio-guides uniquement |
|
||||
|
||||
# 17.6 - Edge cases
|
||||
|
||||
Scénario: Haute vitesse (130 km/h sur autoroute)
|
||||
Étant donné que je roule à 130 km/h (36.1 m/s)
|
||||
Et qu'un contenu géolocalisé est à 252 mètres
|
||||
Quand l'ETA de 7s est atteint
|
||||
Et que je valide la notification
|
||||
Alors le décompte 5s démarre
|
||||
Et le contenu géolocalisé démarre encore avant le point GPS (72m avant)
|
||||
|
||||
Scénario: Multiples points géolocalisés proches (route touristique)
|
||||
Étant donné que 3 châteaux sont espacés de 800m chacun
|
||||
Et que je valide la notification du Château A
|
||||
Quand j'arrive près du Château B (57s plus tard à 50 km/h)
|
||||
Alors la notification du Château B est envoyée (quota 2/6, pas de cooldown)
|
||||
|
||||
Scénario: Mode stationnement (vitesse <1 km/h pendant 2 min)
|
||||
Étant donné que je me gare à 30m d'un château
|
||||
Et que ma vitesse est <1 km/h pendant 2 minutes
|
||||
Quand le mode stationnement est détecté
|
||||
Alors aucune notification de contenu géolocalisé n'est envoyée
|
||||
Et le système bascule automatiquement en mode piéton
|
||||
|
||||
Scénario: Reprise conduite après stationnement
|
||||
Étant donné que je suis en mode stationnement
|
||||
Et que ma vitesse passe à 20 km/h pendant 10 secondes
|
||||
Quand le système détecte la reprise de conduite
|
||||
Alors le mode voiture est réactivé
|
||||
Et les notifications géolocalisées reprennent (si quota non atteint)
|
||||
|
||||
# Distinction contenus géolocalisés simples vs audio-guides
|
||||
|
||||
Scénario: Contenu géolocalisé simple (1 séquence unique)
|
||||
Étant donné qu'un contenu géolocalisé simple existe à un point GPS
|
||||
Quand la notification est déclenchée (ETA 7s)
|
||||
Et que je valide
|
||||
Alors le contenu démarre après décompte 5s
|
||||
Et à la fin du contenu, le buffer normal reprend
|
||||
Et ce contenu compte 1/6 dans le quota
|
||||
|
||||
Scénario: Audio-guide multi-séquences (2+ séquences enchaînées)
|
||||
Étant donné qu'un audio-guide avec 8 séquences existe
|
||||
Quand je démarre l'audio-guide
|
||||
Et que les séquences s'enchaînent automatiquement (GPS ou manuel)
|
||||
Alors l'audio-guide entier compte 1/6 dans le quota
|
||||
Et les séquences ne déclenchent PAS de notification avec compteur 7s
|
||||
Et elles se déclenchent au point GPS exact (rayon 30m)
|
||||
|
||||
# Gestion erreurs
|
||||
|
||||
Scénario: GPS désactivé en mode voiture
|
||||
Étant donné que je suis en mode voiture
|
||||
Quand le GPS est désactivé
|
||||
Alors aucune notification géolocalisée ne peut être envoyée
|
||||
Et un message d'erreur s'affiche: "GPS requis pour les contenus géolocalisés"
|
||||
|
||||
Scénario: App en arrière-plan en mode voiture
|
||||
Étant donné que je suis en mode voiture
|
||||
Et que l'app passe en arrière-plan
|
||||
Quand un contenu géolocalisé est détecté
|
||||
Alors aucune notification n'est envoyée (app premier plan requise)
|
||||
Et le contenu n'est pas perdu (sera proposé si app rouverte dans le rayon)
|
||||
|
||||
Scénario: Permission "Always Location" refusée (mode piéton indisponible)
|
||||
Étant donné que je refuse la permission "Always Location"
|
||||
Quand ma vitesse passe <5 km/h
|
||||
Alors le mode piéton n'est PAS activé
|
||||
Et le mode voiture reste actif (avec permission "When In Use")
|
||||
Et aucune notification arrière-plan n'est envoyée
|
||||
140
features/api/recommendation/historique-reproposition.feature
Normal file
140
features/api/recommendation/historique-reproposition.feature
Normal file
@@ -0,0 +1,140 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Gestion de l'historique et reproposition
|
||||
En tant que système de recommandation
|
||||
Je veux gérer l'historique d'écoute intelligemment
|
||||
Afin d'éviter les répétitions et offrir une découverte maximale
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est disponible
|
||||
|
||||
Scénario: Contenu écouté complètement (>80%) - jamais reproposé
|
||||
Étant donné qu'un utilisateur a écouté un contenu à 85%
|
||||
Quand l'algorithme génère les recommandations
|
||||
Alors ce contenu n'est jamais reproposé
|
||||
Et il est marqué comme "écouté" dans l'historique
|
||||
|
||||
Scénario: Contenu écouté à 80% exactement - jamais reproposé
|
||||
Étant donné qu'un utilisateur a écouté un contenu exactement à 80%
|
||||
Quand l'algorithme génère les recommandations
|
||||
Alors ce contenu n'est pas reproposé (seuil >= 80%)
|
||||
|
||||
Scénario: Contenu skippé rapidement (<10s) - ne pas reproposer
|
||||
Étant donné qu'un utilisateur a skippé un contenu après 8 secondes
|
||||
Quand l'algorithme génère les recommandations
|
||||
Alors ce contenu n'est pas reproposé (signal négatif fort)
|
||||
Et la jauge d'intérêt correspondante est réduite de 0.5%
|
||||
|
||||
Scénario: Contenu skippé exactement à 10s - ne pas reproposer
|
||||
Étant donné qu'un utilisateur a skippé un contenu après exactement 10 secondes
|
||||
Quand l'algorithme génère les recommandations
|
||||
Alors ce contenu n'est pas reproposé (seuil < 10s strict)
|
||||
|
||||
Scénario: Contenu partiellement écouté (10-80%) - reproposer avec reprise
|
||||
Étant donné qu'un utilisateur a écouté un contenu à 45%
|
||||
Et qu'il est arrivé à la position 2:30 (150 secondes)
|
||||
Quand l'algorithme propose à nouveau ce contenu
|
||||
Alors le contenu peut être reproposé
|
||||
Et la position de reprise est 150 secondes
|
||||
Et l'utilisateur voit "Reprendre à 2:30"
|
||||
|
||||
Scénario: Contenu écouté à 11% - reproposition possible
|
||||
Étant donné qu'un utilisateur a écouté un contenu à 11%
|
||||
Quand l'algorithme génère les recommandations
|
||||
Alors ce contenu peut être reproposé (>10%)
|
||||
Et la position de reprise est sauvegardée
|
||||
|
||||
Scénario: Contenu écouté à 79% - reproposition possible
|
||||
Étant donné qu'un utilisateur a écouté un contenu à 79%
|
||||
Quand l'algorithme génère les recommandations
|
||||
Alors ce contenu peut être reproposé (<80%)
|
||||
Et l'utilisateur peut terminer l'écoute
|
||||
|
||||
Scénario: Audio-guide avec flag replayable=true
|
||||
Étant donné qu'un audio-guide a le flag "replayable = true"
|
||||
Et qu'un utilisateur l'a écouté à 95%
|
||||
Quand l'algorithme génère les recommandations
|
||||
Alors l'audio-guide peut être reproposé
|
||||
Et il est marqué comme "Écouté - Rejouable"
|
||||
|
||||
Scénario: Podcast standard sans flag replayable
|
||||
Étant donné qu'un podcast n'a pas de flag replayable
|
||||
Et qu'un utilisateur l'a écouté à 90%
|
||||
Quand l'algorithme génère les recommandations
|
||||
Alors le podcast n'est jamais reproposé
|
||||
|
||||
Scénario: Stockage dans user_content_history
|
||||
Étant donné qu'un utilisateur écoute un contenu
|
||||
Quand l'écoute se termine ou est skippée
|
||||
Alors les données suivantes sont enregistrées:
|
||||
| champ | exemple |
|
||||
| user_id | user-123 |
|
||||
| content_id | content-456 |
|
||||
| completion_rate | 0.45 (45%) |
|
||||
| last_position | 150 (secondes) |
|
||||
| listened_at | 2026-01-21 14:30:00 |
|
||||
|
||||
Scénario: Historique illimité stocké
|
||||
Étant donné qu'un utilisateur a écouté 5000 contenus
|
||||
Quand il consulte son historique
|
||||
Alors tous les 5000 contenus sont disponibles
|
||||
Et aucun contenu n'est supprimé automatiquement
|
||||
|
||||
Scénario: Algorithme considère les 100 derniers pour performance
|
||||
Étant donné qu'un utilisateur a écouté 500 contenus
|
||||
Quand l'algorithme génère les recommandations
|
||||
Alors il vérifie uniquement les 100 derniers contenus pour exclusion
|
||||
Et cette limite est une optimisation de requête SQL
|
||||
|
||||
Scénario: Export historique complet (RGPD)
|
||||
Étant donné qu'un utilisateur demande l'export RGPD
|
||||
Quand l'export est généré
|
||||
Alors l'historique complet est inclus avec:
|
||||
| information | inclus |
|
||||
| Tous les contenus | ✅ |
|
||||
| Dates d'écoute | ✅ |
|
||||
| Taux complétion | ✅ |
|
||||
| Positions reprise | ✅ |
|
||||
|
||||
Scénario: Reprise automatique d'un contenu partiellement écouté
|
||||
Étant donné que j'ai écouté un podcast à 60% (position 10:00)
|
||||
Quand ce podcast est reproposé par l'algorithme
|
||||
Et que je lance la lecture
|
||||
Alors l'écoute reprend automatiquement à 10:00
|
||||
Et je vois une notification "Reprise à 10:00"
|
||||
|
||||
Scénario: Option "Reprendre du début" pour contenu partiellement écouté
|
||||
Étant donné que j'ai écouté un podcast à 60%
|
||||
Quand ce podcast est reproposé
|
||||
Alors je vois deux options:
|
||||
| option | action |
|
||||
| Reprendre à 10:00 | Lecture à partir de 10:00 |
|
||||
| Depuis le début | Lecture à partir de 0:00 |
|
||||
|
||||
Scénario: Contenu écouté il y a 6 mois - toujours en historique
|
||||
Étant donné qu'un utilisateur a écouté un contenu il y a 6 mois à 90%
|
||||
Quand l'algorithme génère les recommandations
|
||||
Alors ce contenu n'est toujours pas reproposé
|
||||
Et l'historique n'a pas de limite temporelle
|
||||
|
||||
Scénario: Nouveau contenu du même créateur après écoute complète
|
||||
Étant donné qu'un utilisateur a écouté un contenu de "Créateur A" à 90%
|
||||
Et que "Créateur A" publie un nouveau contenu
|
||||
Quand l'algorithme génère les recommandations
|
||||
Alors le nouveau contenu peut être recommandé
|
||||
Et seul l'ancien contenu est exclu (pas tout le créateur)
|
||||
|
||||
Scénario: Statistiques personnelles d'historique
|
||||
Étant donné que je consulte mon profil
|
||||
Quand j'accède à la section "Historique"
|
||||
Alors je vois:
|
||||
| métrique | exemple |
|
||||
| Nombre total d'écoutes | 1,234 |
|
||||
| Heures écoutées | 456h |
|
||||
| Taux complétion moyen | 72% |
|
||||
| Top 5 catégories | Voyage, Sport |
|
||||
|
||||
Scénario: Filtrer l'historique par date
|
||||
Étant donné que je consulte mon historique
|
||||
Quand je filtre par "Dernière semaine"
|
||||
Alors seuls les contenus écoutés dans les 7 derniers jours sont affichés
|
||||
Et je peux exporter cette sélection
|
||||
162
features/api/recommendation/medias-traditionnels.feature
Normal file
162
features/api/recommendation/medias-traditionnels.feature
Normal file
@@ -0,0 +1,162 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Médias traditionnels sur RoadWave
|
||||
En tant que média établi
|
||||
Je veux publier du contenu géolocalisé sur RoadWave
|
||||
Afin d'atteindre une audience locale et mobile
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est disponible
|
||||
|
||||
Scénario: Création d'un compte média vérifié
|
||||
Étant donné que je représente Le Monde
|
||||
Quand je crée un compte média
|
||||
Et que je fournis les justificatifs (SIRET, documents officiels)
|
||||
Alors mon compte est créé en attente de vérification
|
||||
Et l'équipe RoadWave examine ma demande sous 48-72h
|
||||
|
||||
Scénario: Validation compte média par l'équipe RoadWave
|
||||
Étant donné qu'un compte média "Le Parisien" est en attente
|
||||
Quand l'équipe RoadWave valide le compte
|
||||
Alors le compte reçoit le badge vérifié ✓
|
||||
Et le média peut publier sans validation des 3 premiers contenus
|
||||
Et je vois le message "Compte média vérifié avec succès"
|
||||
|
||||
Scénario: Badge vérifié visible sur profil média
|
||||
Étant donné que "France Inter" a un compte vérifié
|
||||
Quand un utilisateur consulte le profil
|
||||
Alors il voit le badge ✓ à côté du nom
|
||||
Et une mention "Média vérifié"
|
||||
|
||||
Scénario: Pas de validation des 3 premiers contenus pour médias
|
||||
Étant donné que je suis un média vérifié
|
||||
Quand je publie mon premier contenu
|
||||
Alors le contenu est publié immédiatement sans validation
|
||||
Et il est visible pour tous les utilisateurs
|
||||
Et je ne passe pas par la modération initiale
|
||||
|
||||
Scénario: Modération a posteriori uniquement
|
||||
Étant donné que "Libération" publie un contenu
|
||||
Quand le contenu est publié
|
||||
Alors il est immédiatement disponible
|
||||
Mais peut être signalé et modéré a posteriori
|
||||
Et suit les mêmes règles de modération que les créateurs
|
||||
|
||||
Scénario: Publication flash info géolocalisé
|
||||
Étant donné que je suis "Ouest-France" (média régional)
|
||||
Quand je publie un flash info sur un événement à Rennes
|
||||
Et que je le géolocalise en Bretagne (géo-contextuel)
|
||||
Alors le contenu est publié immédiatement
|
||||
Et il est recommandé aux utilisateurs en Bretagne
|
||||
|
||||
Scénario: Publication chronique thématique
|
||||
Étant donné que je suis "France Culture"
|
||||
Quand je publie une chronique philosophie (géo-neutre)
|
||||
Alors le contenu est disponible partout en France
|
||||
Et suit l'algorithme de recommandation standard
|
||||
|
||||
Scénario: Publication édito politique
|
||||
Étant donné que je suis "Le Figaro"
|
||||
Quand je publie un édito politique
|
||||
Et que je le tague "Politique"
|
||||
Alors le contenu est publié immédiatement
|
||||
Et la classification politique MVP s'applique (pas gauche/droite)
|
||||
Et les utilisateurs ayant activé "Masquer politique" ne le voient pas
|
||||
|
||||
Scénario: Formats de contenu autorisés pour médias
|
||||
Étant donné que je suis un média vérifié
|
||||
Quand je publie du contenu
|
||||
Alors je peux publier:
|
||||
| format | exemple |
|
||||
| Flash info géolocalisé | Actualité régionale 2-5 min |
|
||||
| Chronique thématique | Culture, économie, sport 5-15min|
|
||||
| Édito et débats | Opinion 10-30 min |
|
||||
| Reportage | Investigation 15-45 min |
|
||||
|
||||
Scénario: Médias suivent les règles standard de classification âge
|
||||
Étant donné que je suis "RTL"
|
||||
Quand je publie un contenu sensible
|
||||
Alors je dois obligatoirement classifier par âge:
|
||||
| classification | type contenu |
|
||||
| Tout public | Info générale |
|
||||
| 13+ | Actualité avec sujets sensibles |
|
||||
| 16+ | Débats avec violence verbale |
|
||||
| 18+ | Sujets adultes |
|
||||
|
||||
Scénario: Monétisation médias - partage revenus pub standard
|
||||
Étant donné que je suis un média vérifié
|
||||
Et que mes contenus génèrent des écoutes
|
||||
Quand le mois se termine
|
||||
Alors je reçois 3€ / 1000 écoutes complètes (même taux que créateurs)
|
||||
Et le paiement suit les mêmes règles (seuil 50€, mensuel)
|
||||
|
||||
Scénario: Sponsoring direct non géré par plateforme
|
||||
Étant donné que je suis "Europe 1"
|
||||
Et que je veux intégrer un sponsor dans mon contenu
|
||||
Quand je mentionne le sponsor dans l'audio
|
||||
Alors c'est autorisé (sponsoring éditorial)
|
||||
Mais RoadWave ne gère pas la transaction
|
||||
Et je gère la relation sponsor directement
|
||||
|
||||
Scénario: Médias peuvent avoir plusieurs comptes créateurs
|
||||
Étant donné que je suis "Le Monde"
|
||||
Quand je veux créer des sous-comptes par rubrique
|
||||
Alors je peux créer:
|
||||
| compte | description |
|
||||
| @lemonde_politique | Actualité politique |
|
||||
| @lemonde_economie | Économie et entreprises |
|
||||
| @lemonde_culture | Culture et spectacles |
|
||||
Et tous sont liés au compte média principal
|
||||
|
||||
Scénario: Médias régionaux privilégiés localement
|
||||
Étant donné que "Sud-Ouest" publie du contenu géo-contextuel en Nouvelle-Aquitaine
|
||||
Et qu'un utilisateur est à Bordeaux
|
||||
Quand l'algorithme calcule les recommandations
|
||||
Alors le contenu de "Sud-Ouest" a un score géo élevé
|
||||
Et il est privilégié pour l'audience locale
|
||||
|
||||
Scénario: Médias nationaux accessibles partout
|
||||
Étant donné que "France Inter" publie un podcast géo-neutre
|
||||
Quand des utilisateurs à Paris, Lyon, Marseille demandent des recommandations
|
||||
Alors le podcast est accessible partout sans distinction géographique
|
||||
Et suit l'algorithme de recommandation standard
|
||||
|
||||
Scénario: Statistiques détaillées pour médias
|
||||
Étant donné que je suis un média vérifié
|
||||
Quand je consulte mes statistiques
|
||||
Alors je vois:
|
||||
| métrique | exemple |
|
||||
| Écoutes par région | Île-de-France: 45% |
|
||||
| Taux complétion | 72% |
|
||||
| Démographie auditeurs | 25-34 ans: 35% |
|
||||
| Top contenus | Flash info Paris|
|
||||
| Revenus générés | 1,234€ |
|
||||
|
||||
Scénario: Médias peuvent exporter analytics
|
||||
Étant donné que je suis "Libération"
|
||||
Quand je clique sur "Exporter analytics"
|
||||
Alors je reçois un CSV avec données détaillées
|
||||
Et je peux analyser les données avec mes outils internes
|
||||
|
||||
Scénario: Contact prioritaire équipe RoadWave
|
||||
Étant donné que je suis un média vérifié
|
||||
Quand j'ai un problème technique ou question
|
||||
Alors je peux contacter le support média prioritaire
|
||||
Et j'obtiens une réponse sous 24h (vs 48-72h standard)
|
||||
|
||||
Scénario: Médias peuvent programmer la publication
|
||||
Étant donné que je suis "France Culture"
|
||||
Quand je prépare un contenu à l'avance
|
||||
Alors je peux programmer la publication pour une date/heure future
|
||||
Et le contenu sera publié automatiquement au moment choisi
|
||||
|
||||
Scénario: API dédiée pour médias (post-MVP)
|
||||
Étant donné que je suis un grand média avec beaucoup de contenus
|
||||
Quand RoadWave développe l'API médias
|
||||
Alors je peux automatiser la publication via API
|
||||
Et intégrer RoadWave dans mon workflow de production
|
||||
|
||||
Scénario: Signalement d'un contenu média traité en priorité
|
||||
Étant donné qu'un contenu de "Le Monde" est signalé
|
||||
Quand le signalement arrive en modération
|
||||
Alors il est traité avec la même priorité qu'un créateur standard
|
||||
Et le badge vérifié ne donne pas d'immunité modération
|
||||
115
features/api/recommendation/mode-kids.feature
Normal file
115
features/api/recommendation/mode-kids.feature
Normal file
@@ -0,0 +1,115 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Mode Kids pour utilisateurs 13-15 ans
|
||||
En tant que parent ou adolescent
|
||||
Je veux activer un mode Kids avec filtrage de contenu
|
||||
Afin de protéger les mineurs des contenus inappropriés
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est disponible
|
||||
|
||||
Scénario: Activation manuelle du mode Kids
|
||||
Étant donné que je suis un utilisateur de 14 ans
|
||||
Et que le mode Kids n'est pas activé par défaut
|
||||
Quand j'active le mode Kids dans les paramètres
|
||||
Alors le mode Kids est activé sur mon compte
|
||||
Et je vois le message "Mode Kids activé - Contenus filtrés pour 13-15 ans"
|
||||
|
||||
Scénario: Parent active le mode Kids pour son enfant
|
||||
Étant donné que je suis le parent d'un utilisateur de 13 ans
|
||||
Et que j'ai accès au compte de mon enfant
|
||||
Quand j'active le mode Kids
|
||||
Alors le mode Kids est activé sur le compte enfant
|
||||
Et seuls les contenus "Tous publics" sont accessibles
|
||||
|
||||
Scénario: Filtrage contenu - uniquement "Tous publics"
|
||||
Étant donné que le mode Kids est activé sur mon compte
|
||||
Et qu'il existe des contenus avec les classifications:
|
||||
| classification | nombre |
|
||||
| Tous publics | 100 |
|
||||
| 13+ | 50 |
|
||||
| 16+ | 30 |
|
||||
| 18+ | 20 |
|
||||
Quand je demande des recommandations
|
||||
Alors seuls les 100 contenus "Tous publics" sont proposés
|
||||
Et les contenus 13+, 16+, 18+ sont exclus
|
||||
|
||||
Scénario: Exclusion automatique du contenu politique
|
||||
Étant donné que le mode Kids est activé
|
||||
Et qu'il existe 20 contenus "Tous publics" dont 5 tagués "Politique"
|
||||
Quand je demande des recommandations
|
||||
Alors seuls les 15 contenus non-politiques sont proposés
|
||||
Et les 5 contenus politiques sont automatiquement exclus
|
||||
|
||||
Scénario: Pas de publicité en mode Kids
|
||||
Étant donné que le mode Kids est activé
|
||||
Et que je suis un utilisateur gratuit
|
||||
Quand j'écoute du contenu
|
||||
Alors aucune publicité n'est diffusée
|
||||
Et je n'ai pas d'insertion publicitaire (règle 1/5 désactivée)
|
||||
|
||||
Scénario: Publicité validée manuellement en mode Kids (post-MVP)
|
||||
Étant donné que le mode Kids est activé
|
||||
Et qu'une publicité a été validée manuellement pour le mode Kids
|
||||
Quand j'écoute du contenu
|
||||
Alors cette publicité peut être diffusée
|
||||
Mais la fréquence reste inférieure au mode standard
|
||||
|
||||
Scénario: Interface standard même en mode Kids
|
||||
Étant donné que le mode Kids est activé
|
||||
Quand j'ouvre l'application
|
||||
Alors l'interface est identique au mode normal
|
||||
Et seul le filtrage de contenu est actif (pas d'UI enfant)
|
||||
|
||||
Scénario: Désactivation du mode Kids
|
||||
Étant donné que le mode Kids est activé
|
||||
Quand je désactive le mode Kids dans les paramètres
|
||||
Alors tous les contenus sont à nouveau accessibles selon mon âge
|
||||
Et je vois le message "Mode Kids désactivé"
|
||||
|
||||
Scénario: Utilisateur 16 ans ne peut pas activer le mode Kids 13-15 ans
|
||||
Étant donné que je suis un utilisateur de 16 ans
|
||||
Quand j'essaie d'activer le mode Kids
|
||||
Alors l'activation réussit
|
||||
Et le mode Kids filtre les contenus 16+ et 18+ (pas seulement 13+)
|
||||
Et je vois uniquement les contenus "Tous publics"
|
||||
|
||||
Scénario: Tentative d'accès direct à contenu 16+ en mode Kids
|
||||
Étant donné que le mode Kids est activé
|
||||
Et qu'un ami me partage un contenu 16+
|
||||
Quand j'essaie d'accéder au contenu via le lien
|
||||
Alors l'accès est refusé
|
||||
Et je vois le message "Ce contenu n'est pas accessible en mode Kids"
|
||||
|
||||
Scénario: Recherche en mode Kids filtre automatiquement
|
||||
Étant donné que le mode Kids est activé
|
||||
Quand je recherche "débat"
|
||||
Alors seuls les contenus "Tous publics" apparaissent dans les résultats
|
||||
Et les contenus 13+, 16+, 18+ sont exclus de la recherche
|
||||
|
||||
Scénario: Audio-guide en mode Kids
|
||||
Étant donné que le mode Kids est activé
|
||||
Et qu'un audio-guide "Tous publics" existe au musée du Louvre
|
||||
Quand je suis à proximité du Louvre
|
||||
Alors l'audio-guide est proposé normalement
|
||||
Et toutes les séquences sont accessibles
|
||||
|
||||
Scénario: Statistiques créateur - audience mode Kids
|
||||
Étant donné que je suis un créateur
|
||||
Et que mes contenus "Tous publics" sont écoutés par des utilisateurs mode Kids
|
||||
Quand je consulte mes statistiques
|
||||
Alors je vois le pourcentage d'écoutes en mode Kids
|
||||
Et je peux adapter mes contenus en conséquence
|
||||
|
||||
Scénario: Notification lors de l'activation du mode Kids
|
||||
Quand j'active le mode Kids
|
||||
Alors je reçois une notification explicative:
|
||||
| information | description |
|
||||
| Contenu | Seuls les contenus "Tous publics" accessibles |
|
||||
| Politique | Contenus politiques automatiquement masqués |
|
||||
| Publicité | Aucune publicité affichée |
|
||||
|
||||
Scénario: Badge mode Kids visible dans le profil
|
||||
Étant donné que le mode Kids est activé
|
||||
Quand je consulte mon profil
|
||||
Alors je vois un badge "Mode Kids actif 🛡️"
|
||||
Et je peux le désactiver en un clic
|
||||
163
features/api/recommendation/parametrabilite-admin.feature
Normal file
163
features/api/recommendation/parametrabilite-admin.feature
Normal file
@@ -0,0 +1,163 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Paramétrabilité admin et A/B testing
|
||||
En tant qu'administrateur RoadWave
|
||||
Je veux configurer les paramètres de l'algorithme à chaud
|
||||
Afin d'optimiser l'engagement sans redéploiement
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est disponible
|
||||
Et que je suis connecté en tant qu'admin
|
||||
|
||||
Scénario: Accès au dashboard admin
|
||||
Quand j'accède au dashboard admin
|
||||
Alors je vois tous les paramètres configurables de l'algorithme
|
||||
Et je vois les valeurs actuelles et par défaut
|
||||
|
||||
Scénario: Modifier le poids géo pour contenu ancré
|
||||
Étant donné que le poids_geo_ancre est à 0.7 (défaut)
|
||||
Quand je modifie le poids_geo_ancre à 0.8
|
||||
Et que je sauvegarde
|
||||
Alors la nouvelle valeur est appliquée immédiatement
|
||||
Et tous les nouveaux calculs utilisent 0.8
|
||||
Et je vois le message "Paramètre mis à jour avec succès"
|
||||
|
||||
Scénario: Validation des plages de valeurs
|
||||
Quand j'essaie de configurer poids_geo_ancre à 1.5 (hors plage 0.5-1.0)
|
||||
Alors la modification échoue
|
||||
Et je vois le message "Valeur hors plage autorisée (0.5 - 1.0)"
|
||||
|
||||
Plan du Scénario: Modification de tous les paramètres configurables
|
||||
Quand je modifie "<parametre>" à "<nouvelle_valeur>"
|
||||
Alors la modification est appliquée immédiatement
|
||||
Et la nouvelle valeur respecte la plage "<plage>"
|
||||
|
||||
Exemples:
|
||||
| parametre | nouvelle_valeur | plage |
|
||||
| poids_geo_ancre | 0.8 | 0.5 - 1.0 |
|
||||
| poids_geo_contextuel | 0.6 | 0.3 - 0.7 |
|
||||
| poids_geo_neutre | 0.3 | 0.0 - 0.4 |
|
||||
| poids_engagement | 0.3 | 0.0 - 0.5 |
|
||||
| part_aleatoire_global | 0.15 | 0.0 - 0.3 |
|
||||
| distance_max_km | 150 | 50 - 500 |
|
||||
| rayon_gps_point_m | 1000 | 100 - 2000 |
|
||||
| seuil_min_ecoutes_engagement | 100 | 10 - 200 |
|
||||
|
||||
Scénario: Aucun recalcul batch après modification
|
||||
Étant donné que le poids_geo_ancre est modifié de 0.7 à 0.8
|
||||
Quand la modification est appliquée
|
||||
Alors aucun recalcul batch n'est lancé (économie CPU)
|
||||
Et seuls les nouveaux calculs utilisent la valeur 0.8
|
||||
|
||||
Scénario: Versioning des configurations
|
||||
Étant donné que je modifie plusieurs paramètres
|
||||
Quand je sauvegarde la configuration
|
||||
Alors une nouvelle version est créée (ex: v1.2.3)
|
||||
Et je peux voir l'historique des versions
|
||||
Et je peux comparer deux versions
|
||||
|
||||
Scénario: Rollback en 1 clic
|
||||
Étant donné que la configuration actuelle est v1.2.3
|
||||
Et que la version précédente était v1.2.2
|
||||
Quand je clique sur "Restaurer v1.2.2"
|
||||
Alors tous les paramètres de v1.2.2 sont réappliqués
|
||||
Et je vois le message "Configuration restaurée à v1.2.2"
|
||||
|
||||
Scénario: Créer une variante A/B testing
|
||||
Quand je crée une nouvelle variante "Test engagement élevé"
|
||||
Et que je configure:
|
||||
| parametre | valeur |
|
||||
| poids_engagement | 0.4 |
|
||||
| poids_geo_ancre | 0.6 |
|
||||
Et que je lance le test A/B
|
||||
Alors 50% des utilisateurs reçoivent la Config A (défaut)
|
||||
Et 50% des utilisateurs reçoivent la Config B (test)
|
||||
|
||||
Scénario: Split utilisateurs aléatoire pour A/B test
|
||||
Étant donné qu'un test A/B est actif
|
||||
Quand 1000 nouveaux utilisateurs se connectent
|
||||
Alors environ 500 sont assignés à la Config A
|
||||
Et environ 500 sont assignés à la Config B
|
||||
Et l'assignation est aléatoire et équilibrée
|
||||
|
||||
Scénario: Utilisateur reste dans la même variante
|
||||
Étant donné qu'un utilisateur est assigné à la Config B
|
||||
Quand il se reconnecte plusieurs fois
|
||||
Alors il reste toujours dans la Config B
|
||||
Et il ne change pas de variante pendant le test
|
||||
|
||||
Scénario: Métriques comparatives A/B testing
|
||||
Étant donné qu'un test A/B est actif depuis 7 jours
|
||||
Quand je consulte le dashboard A/B testing
|
||||
Alors je vois les métriques suivantes pour chaque config:
|
||||
| metrique | Config A | Config B |
|
||||
| Taux complétion moyen | 68% | 72% |
|
||||
| Engagement (likes) | 15% | 18% |
|
||||
| Durée session moyenne | 23 min | 27 min |
|
||||
| Taux skip rapide (<10s) | 12% | 9% |
|
||||
|
||||
Scénario: Dashboard graphique temps réel
|
||||
Étant donné qu'un test A/B est actif
|
||||
Quand je consulte le dashboard
|
||||
Alors je vois des graphiques temps réel:
|
||||
| graphique | type |
|
||||
| Évolution engagement | Ligne |
|
||||
| Répartition utilisateurs| Camembert |
|
||||
| Taux complétion | Barres |
|
||||
| Durée session | Ligne |
|
||||
|
||||
Scénario: Terminer un test A/B et appliquer la meilleure config
|
||||
Étant donné qu'un test A/B montre que Config B est meilleure
|
||||
Quand je clique sur "Appliquer Config B pour tous"
|
||||
Alors la Config B devient la configuration par défaut
|
||||
Et tous les utilisateurs utilisent maintenant Config B
|
||||
Et l'ancien test est archivé
|
||||
|
||||
Scénario: Audit engagement global
|
||||
Quand je consulte la section "Audit engagement"
|
||||
Alors je vois:
|
||||
| metrique | valeur |
|
||||
| Temps écoute moyen/session | 25 min |
|
||||
| Temps écoute médian/session | 18 min |
|
||||
| Taux complétion moyen | 70% |
|
||||
| % sessions avec ≥1 like | 35% |
|
||||
|
||||
Scénario: Graphiques évolution engagement selon config
|
||||
Étant donné que plusieurs modifications de config ont été faites
|
||||
Quand je consulte les graphiques d'évolution
|
||||
Alors je vois l'impact de chaque changement de config
|
||||
Et je peux corréler changements config avec métriques
|
||||
|
||||
Scénario: Export CSV pour analyse externe
|
||||
Quand je clique sur "Exporter données"
|
||||
Alors je peux télécharger un CSV avec:
|
||||
| colonne | exemple |
|
||||
| date | 2026-01-21 |
|
||||
| version_config | v1.2.3 |
|
||||
| taux_completion | 0.72 |
|
||||
| engagement_moyen | 0.45 |
|
||||
| duree_session_min | 27 |
|
||||
|
||||
Scénario: Alerte automatique si métrique critique baisse
|
||||
Étant donné que le taux de complétion moyen est à 70%
|
||||
Quand une nouvelle config fait baisser le taux à 55%
|
||||
Alors je reçois une alerte email "Baisse critique du taux de complétion"
|
||||
Et je peux rollback rapidement
|
||||
|
||||
Scénario: Prévisualisation impact avant application
|
||||
Étant donné que je modifie poids_geo_ancre de 0.7 à 0.9
|
||||
Quand je clique sur "Prévisualiser impact"
|
||||
Alors je vois une simulation sur échantillon de 1000 utilisateurs
|
||||
Et je vois l'estimation d'impact sur les métriques clés
|
||||
|
||||
Scénario: Notes et commentaires sur modifications config
|
||||
Quand je modifie une configuration
|
||||
Alors je peux ajouter une note "Test pour améliorer contenu local"
|
||||
Et cette note est visible dans l'historique des versions
|
||||
Et l'équipe peut comprendre le contexte des changements
|
||||
|
||||
Scénario: Permissions admin pour modification config
|
||||
Étant donné que je suis un admin junior
|
||||
Quand j'essaie de modifier un paramètre critique
|
||||
Alors l'accès est refusé
|
||||
Et je vois "Permission admin senior requise"
|
||||
Et seuls les admins seniors peuvent modifier les paramètres
|
||||
188
features/api/recommendation/parametrabilite-utilisateur.feature
Normal file
188
features/api/recommendation/parametrabilite-utilisateur.feature
Normal file
@@ -0,0 +1,188 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Paramétrabilité utilisateur et profils
|
||||
En tant qu'utilisateur
|
||||
Je veux personnaliser mon expérience de recommandation
|
||||
Afin d'adapter l'application à mes différents contextes d'usage
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est disponible
|
||||
Et que je suis connecté
|
||||
|
||||
Scénario: Accès aux paramètres de personnalisation
|
||||
Quand j'ouvre les paramètres de personnalisation
|
||||
Alors je vois trois curseurs disponibles:
|
||||
| curseur | description |
|
||||
| Géolocalisation | Local ← slider → National |
|
||||
| Découverte | 0% ← slider → 50% |
|
||||
| Politique | Masquer / Équilibré / Mes préférences |
|
||||
|
||||
Scénario: Modifier le curseur Géolocalisation vers Local
|
||||
Étant donné que le curseur Géolocalisation est au centre (défaut)
|
||||
Quand je déplace le curseur vers "Local" (gauche)
|
||||
Alors l'algorithme privilégie fortement les contenus proches
|
||||
Et la pondération géographique augmente
|
||||
Et je vois le message "Recommandations locales privilégiées"
|
||||
|
||||
Scénario: Modifier le curseur Géolocalisation vers National
|
||||
Étant donné que le curseur Géolocalisation est au centre
|
||||
Quand je déplace le curseur vers "National" (droite)
|
||||
Alors l'algorithme privilégie la découverte nationale
|
||||
Et la pondération géographique diminue
|
||||
Et je reçois des contenus de toute la France
|
||||
|
||||
Scénario: Curseur Découverte à 0% - aucun aléatoire
|
||||
Quand je règle le curseur Découverte à 0%
|
||||
Alors 0% de contenus aléatoires dans mes recommandations
|
||||
Et 100% de contenus calculés selon score combiné
|
||||
Et je vois le message "Personnalisation maximale"
|
||||
|
||||
Scénario: Curseur Découverte à 10% - défaut équilibré
|
||||
Quand je règle le curseur Découverte à 10%
|
||||
Alors 10% de contenus aléatoires
|
||||
Et 90% de contenus calculés
|
||||
Et je vois le message "Équilibre découverte/personnalisation"
|
||||
|
||||
Scénario: Curseur Découverte à 30% - découverte élevée
|
||||
Quand je règle le curseur Découverte à 30%
|
||||
Alors 30% de contenus aléatoires
|
||||
Et 70% de contenus calculés
|
||||
Et je vois le message "Découverte élevée activée"
|
||||
|
||||
Scénario: Curseur Découverte à 50% - découverte maximale
|
||||
Quand je règle le curseur Découverte à 50%
|
||||
Alors 50% de contenus aléatoires
|
||||
Et 50% de contenus calculés
|
||||
Et je vois le message "Découverte maximale (équivaut à national)"
|
||||
|
||||
Scénario: Créer un profil personnalisé "Trajet quotidien"
|
||||
Quand je crée un nouveau profil nommé "🚗 Trajet quotidien"
|
||||
Et que je configure:
|
||||
| parametre | valeur |
|
||||
| Géolocalisation | Local |
|
||||
| Découverte | 5% |
|
||||
| Politique | Masquer |
|
||||
Et que je sauvegarde
|
||||
Alors le profil "🚗 Trajet quotidien" est créé
|
||||
Et je peux l'activer en un clic
|
||||
|
||||
Scénario: Créer un profil "Road trip"
|
||||
Quand je crée un profil "🛣️ Road trip"
|
||||
Et que je configure:
|
||||
| parametre | valeur |
|
||||
| Géolocalisation | Régional |
|
||||
| Découverte | 30% |
|
||||
| Politique | Équilibré |
|
||||
Alors le profil est sauvegardé
|
||||
Et je peux switcher entre profils facilement
|
||||
|
||||
Scénario: Créer un profil "Enfants"
|
||||
Quand je crée un profil "👶 Enfants"
|
||||
Et que j'active le Mode Kids
|
||||
Alors tous les paramètres sont adaptés pour enfants:
|
||||
| parametre | valeur |
|
||||
| Mode Kids | Activé |
|
||||
| Politique | Masquer (forcé) |
|
||||
| Publicité | Aucune |
|
||||
|
||||
Scénario: Activer un profil existant
|
||||
Étant donné que j'ai créé un profil "🚗 Trajet quotidien"
|
||||
Quand je clique sur "Activer" pour ce profil
|
||||
Alors tous les paramètres du profil sont appliqués
|
||||
Et je vois le message "Profil 'Trajet quotidien' activé"
|
||||
Et l'algorithme utilise ces paramètres immédiatement
|
||||
|
||||
Scénario: Synchronisation profils entre devices
|
||||
Étant donné que j'ai créé 3 profils sur mon iPhone
|
||||
Quand je me connecte sur mon iPad
|
||||
Alors mes 3 profils sont automatiquement synchronisés
|
||||
Et je peux les utiliser sur l'iPad
|
||||
|
||||
Scénario: Modification d'un profil synchronisée
|
||||
Étant donné que j'ai un profil "Road trip" sur iPhone
|
||||
Quand je modifie ce profil sur iPhone
|
||||
Alors la modification est synchronisée sur tous mes devices
|
||||
Et le profil est mis à jour partout en temps réel
|
||||
|
||||
Scénario: Pas de partage de profils entre utilisateurs
|
||||
Étant donné que j'ai créé des profils personnalisés
|
||||
Et que ma conjointe a un compte RoadWave
|
||||
Quand elle se connecte sur son compte
|
||||
Alors elle ne voit pas mes profils
|
||||
Et chaque utilisateur a ses propres profils
|
||||
|
||||
Scénario: Auto-switch selon contexte (détection trajet récurrent)
|
||||
Étant donné que j'utilise toujours le profil "Trajet quotidien"
|
||||
Et que je pars de chez moi vers mon travail tous les matins à 8h
|
||||
Quand le système détecte ce trajet récurrent
|
||||
Alors le profil "Trajet quotidien" est activé automatiquement
|
||||
Et je reçois une notification "Profil 'Trajet quotidien' activé"
|
||||
|
||||
Scénario: Désactiver l'auto-switch
|
||||
Étant donné que l'auto-switch de profil est actif
|
||||
Quand je désactive cette option dans les paramètres
|
||||
Alors les profils ne changent plus automatiquement
|
||||
Et je dois les activer manuellement
|
||||
|
||||
Scénario: Blocage modification si vitesse GPS >10 km/h
|
||||
Étant donné que je conduis à 50 km/h
|
||||
Quand j'essaie de modifier un curseur
|
||||
Alors la modification est bloquée
|
||||
Et je vois le message "Modification impossible pendant la conduite"
|
||||
Et je dois m'arrêter ou être passager pour modifier
|
||||
|
||||
Scénario: Modification possible si vitesse <10 km/h
|
||||
Étant donné que je suis arrêté à un feu rouge (5 km/h)
|
||||
Quand j'essaie de modifier un curseur
|
||||
Alors la modification est autorisée
|
||||
Et je peux ajuster les paramètres
|
||||
|
||||
Scénario: Warning au lancement app
|
||||
Quand je lance l'application pour la première fois
|
||||
Alors je vois un warning "Configurez vos préférences avant de prendre la route"
|
||||
Et un bouton "Configurer maintenant"
|
||||
Et je peux accéder rapidement aux paramètres
|
||||
|
||||
Scénario: Modification uniquement app arrêtée ou mode passager
|
||||
Étant donné que je suis passager dans une voiture
|
||||
Et que le mode passager est activé
|
||||
Quand j'essaie de modifier les paramètres
|
||||
Alors la modification est autorisée
|
||||
Et le blocage vitesse GPS ne s'applique pas
|
||||
|
||||
Scénario: Statistiques d'utilisation des profils
|
||||
Étant donné que j'utilise plusieurs profils
|
||||
Quand je consulte mes statistiques
|
||||
Alors je vois:
|
||||
| metrique | exemple |
|
||||
| Profil le plus utilisé | Trajet quotidien |
|
||||
| Heures par profil | 25h / 10h / 5h |
|
||||
| Dernier profil actif | Road trip |
|
||||
|
||||
Scénario: Supprimer un profil
|
||||
Étant donné que j'ai créé un profil "Test"
|
||||
Quand je supprime ce profil
|
||||
Alors le profil est définitivement supprimé
|
||||
Et je vois le message "Profil 'Test' supprimé"
|
||||
Et il disparaît de tous mes devices
|
||||
|
||||
Scénario: Limite de profils par utilisateur
|
||||
Étant donné que j'ai créé 10 profils
|
||||
Quand j'essaie de créer un 11ème profil
|
||||
Alors la création échoue
|
||||
Et je vois le message "Maximum 10 profils par utilisateur"
|
||||
|
||||
Scénario: Dupliquer un profil existant
|
||||
Étant donné que j'ai un profil "Trajet quotidien"
|
||||
Quand je clique sur "Dupliquer"
|
||||
Alors un nouveau profil "Trajet quotidien (copie)" est créé
|
||||
Et il a les mêmes paramètres que l'original
|
||||
Et je peux le modifier indépendamment
|
||||
|
||||
Scénario: Réinitialiser un profil aux valeurs par défaut
|
||||
Étant donné que j'ai modifié un profil
|
||||
Quand je clique sur "Réinitialiser"
|
||||
Alors tous les paramètres reviennent aux valeurs par défaut:
|
||||
| parametre | valeur défaut |
|
||||
| Géolocalisation | Équilibré |
|
||||
| Découverte | 10% |
|
||||
| Politique | Équilibré |
|
||||
180
features/api/recommendation/scoring-recommandation.feature
Normal file
180
features/api/recommendation/scoring-recommandation.feature
Normal file
@@ -0,0 +1,180 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Formule de scoring et recommandation
|
||||
En tant que système de recommandation
|
||||
Je veux calculer un score combiné pour chaque contenu
|
||||
Afin de proposer les contenus les plus pertinents à l'utilisateur
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est disponible
|
||||
|
||||
Scénario: Calcul du score géographique linéaire
|
||||
Étant donné qu'un contenu existe à Paris
|
||||
Et que la distance_max_km est configurée à 200 km
|
||||
Quand un utilisateur est à 50 km du contenu
|
||||
Alors le score_geo = 1 - (50 / 200) = 0.75
|
||||
|
||||
Scénario: Score géo à distance nulle (sur place)
|
||||
Étant donné qu'un contenu existe à un point GPS précis
|
||||
Quand un utilisateur est exactement au même point (0 km)
|
||||
Alors le score_geo = 1.0 (maximum)
|
||||
|
||||
Scénario: Score géo à distance_max (200 km)
|
||||
Étant donné qu'un contenu existe à Paris
|
||||
Quand un utilisateur est à 200 km du contenu
|
||||
Alors le score_geo = 1 - (200 / 200) = 0.0
|
||||
|
||||
Scénario: Score géo au-delà de distance_max
|
||||
Étant donné qu'un contenu existe à Paris
|
||||
Quand un utilisateur est à 250 km du contenu (au-delà de 200 km max)
|
||||
Alors le score_geo = 0.0 (minimum)
|
||||
Et le contenu a peu de chances d'être recommandé sauf engagement très élevé
|
||||
|
||||
Scénario: Calcul du score d'intérêts avec jauges utilisateur
|
||||
Étant donné qu'un utilisateur a les jauges suivantes:
|
||||
| categorie | niveau |
|
||||
| Automobile | 80% |
|
||||
| Voyage | 60% |
|
||||
| Musique | 40% |
|
||||
Et qu'un contenu est tagué "Automobile" et "Voyage"
|
||||
Quand l'algorithme calcule le score_interets
|
||||
Alors score_interets = (0.8 + 0.6) / 2 = 0.7
|
||||
|
||||
Scénario: Score d'intérêts avec un seul tag
|
||||
Étant donné qu'un utilisateur a la jauge "Économie" à 90%
|
||||
Et qu'un contenu est tagué uniquement "Économie"
|
||||
Quand l'algorithme calcule le score_interets
|
||||
Alors score_interets = 0.9
|
||||
|
||||
Scénario: Score d'intérêts avec tags non matchés
|
||||
Étant donné qu'un utilisateur a des jauges "Sport" et "Politique" élevées
|
||||
Et qu'un contenu est tagué "Musique" et "Philosophie"
|
||||
Et que l'utilisateur n'a pas ces catégories
|
||||
Quand l'algorithme calcule le score_interets
|
||||
Alors score_interets = 0.5 (neutre par défaut pour catégories inconnues)
|
||||
|
||||
Scénario: Calcul du score d'engagement avec métriques
|
||||
Étant donné qu'un contenu a:
|
||||
| metrique | valeur |
|
||||
| ecoutes | 1000 |
|
||||
| ecoutes_completes | 700 |
|
||||
| likes | 300 |
|
||||
| abonnements_apres | 50 |
|
||||
Quand l'algorithme calcule le score_engagement
|
||||
Alors taux_completion = 700 / 1000 = 0.7
|
||||
Et ratio_likes = 300 / 1000 = 0.3
|
||||
Et ratio_abonnements = 50 / 1000 = 0.05
|
||||
Et score_engagement = (0.7 × 0.5) + (0.3 × 0.3) + (0.05 × 0.2) = 0.35 + 0.09 + 0.01 = 0.45
|
||||
|
||||
Scénario: Contenu avec moins de 50 écoutes - score neutre
|
||||
Étant donné qu'un contenu a seulement 30 écoutes
|
||||
Quand l'algorithme calcule le score_engagement
|
||||
Alors score_engagement = 0.5 (neutre par défaut)
|
||||
Et le contenu n'est pas pénalisé pour manque de données
|
||||
|
||||
Scénario: Contenu avec exactement 50 écoutes - calcul réel
|
||||
Étant donné qu'un contenu a exactement 50 écoutes
|
||||
Et des métriques d'engagement complètes
|
||||
Quand l'algorithme calcule le score_engagement
|
||||
Alors le score est calculé normalement (pas de seuil neutre)
|
||||
|
||||
Scénario: Bonus aléatoire - 10% des recommandations
|
||||
Étant donné qu'un utilisateur demande 10 recommandations
|
||||
Et que la part_aleatoire_global est à 10%
|
||||
Quand l'algorithme génère les recommandations
|
||||
Alors 1 contenu sur 10 est tiré aléatoirement
|
||||
Et 9 contenus sont calculés avec le score combiné
|
||||
Et le contenu aléatoire n'est pas dans l'historique déjà écouté
|
||||
|
||||
Scénario: Curseur utilisateur découverte à 0% - aucun aléatoire
|
||||
Étant donné qu'un utilisateur configure le curseur découverte à 0%
|
||||
Quand l'utilisateur demande 20 recommandations
|
||||
Alors les 20 contenus sont calculés avec le score combiné
|
||||
Et aucun contenu aléatoire n'est proposé
|
||||
|
||||
Scénario: Curseur utilisateur découverte à 50% - découverte max
|
||||
Étant donné qu'un utilisateur configure le curseur découverte à 50%
|
||||
Quand l'utilisateur demande 20 recommandations
|
||||
Alors 10 contenus sont tirés aléatoirement
|
||||
Et 10 contenus sont calculés avec le score combiné
|
||||
|
||||
Scénario: Score final combiné pour contenu géo-ancré
|
||||
Étant donné qu'un contenu "Géo-ancré" a:
|
||||
| parametre | valeur |
|
||||
| score_geo | 0.9 |
|
||||
| score_interets | 0.6 |
|
||||
| score_engagement | 0.45 |
|
||||
| poids_geo | 0.7 |
|
||||
| poids_interets | 0.1 |
|
||||
| poids_engagement | 0.2 |
|
||||
Quand l'algorithme calcule le score_final
|
||||
Alors score_final = (0.9 × 0.7) + (0.6 × 0.1) + (0.45 × 0.2)
|
||||
Et score_final = 0.63 + 0.06 + 0.09 = 0.78
|
||||
|
||||
Scénario: Score final combiné pour contenu géo-neutre
|
||||
Étant donné qu'un contenu "Géo-neutre" a:
|
||||
| parametre | valeur |
|
||||
| score_geo | 0.3 |
|
||||
| score_interets | 0.9 |
|
||||
| score_engagement | 0.6 |
|
||||
| poids_geo | 0.2 |
|
||||
| poids_interets | 0.6 |
|
||||
| poids_engagement | 0.2 |
|
||||
Quand l'algorithme calcule le score_final
|
||||
Alors score_final = (0.3 × 0.2) + (0.9 × 0.6) + (0.6 × 0.2)
|
||||
Et score_final = 0.06 + 0.54 + 0.12 = 0.72
|
||||
Et le contenu peut être recommandé malgré la distance
|
||||
|
||||
Scénario: Contenu viral lointain peut être recommandé
|
||||
Étant donné qu'un contenu viral existe à Paris
|
||||
Et qu'il a un score_engagement très élevé de 0.95
|
||||
Et qu'un utilisateur est à Marseille (score_geo = 0.1)
|
||||
Quand l'algorithme calcule le score_final
|
||||
Alors le score_engagement élevé compense le score_geo faible
|
||||
Et le contenu peut apparaître dans les recommandations
|
||||
|
||||
Scénario: Ordre de recommandation par score décroissant
|
||||
Étant donné 5 contenus avec les scores suivants:
|
||||
| contenu | score_final |
|
||||
| Contenu A | 0.85 |
|
||||
| Contenu B | 0.72 |
|
||||
| Contenu C | 0.90 |
|
||||
| Contenu D | 0.65 |
|
||||
| Contenu E | 0.78 |
|
||||
Quand l'utilisateur demande des recommandations
|
||||
Alors l'ordre de proposition est:
|
||||
| position | contenu |
|
||||
| 1 | Contenu C |
|
||||
| 2 | Contenu A |
|
||||
| 3 | Contenu E |
|
||||
| 4 | Contenu B |
|
||||
| 5 | Contenu D |
|
||||
|
||||
Scénario: Exclusion de l'historique déjà écouté >80%
|
||||
Étant donné qu'un utilisateur a écouté les contenus suivants:
|
||||
| contenu | completion |
|
||||
| Contenu A | 85% |
|
||||
| Contenu B | 95% |
|
||||
| Contenu C | 30% |
|
||||
Quand l'algorithme génère les recommandations
|
||||
Alors "Contenu A" et "Contenu B" ne sont jamais proposés
|
||||
Mais "Contenu C" peut être reproposé
|
||||
|
||||
Scénario: Pré-calcul de 5 contenus suivants
|
||||
Étant donné qu'un utilisateur écoute un contenu
|
||||
Quand l'algorithme prépare les contenus suivants
|
||||
Alors 5 contenus sont pré-calculés selon le score
|
||||
Et ces contenus sont mis en cache pour performance
|
||||
|
||||
Scénario: Recalcul si déplacement >10 km
|
||||
Étant donné que 5 contenus suivants sont pré-calculés
|
||||
Et que l'utilisateur se déplace de 12 km
|
||||
Quand l'utilisateur demande le contenu suivant
|
||||
Alors l'algorithme recalcule les scores avec la nouvelle position
|
||||
Et propose de nouveaux contenus plus pertinents géographiquement
|
||||
|
||||
Scénario: Recalcul après 10 minutes d'inactivité
|
||||
Étant donné que 5 contenus suivants sont pré-calculés
|
||||
Et que 11 minutes se sont écoulées sans action
|
||||
Quand l'utilisateur demande le contenu suivant
|
||||
Alors l'algorithme recalcule les scores
|
||||
Et prend en compte les nouveaux contenus publiés
|
||||
Reference in New Issue
Block a user