refactor(docs): réorganiser la documentation selon principes DDD
Réorganise la documentation du projet selon les principes du Domain-Driven Design (DDD) pour améliorer la cohésion, la maintenabilité et l'alignement avec l'architecture modulaire du backend. **Structure cible:** ``` docs/domains/ ├── README.md (Context Map) ├── _shared/ (Core Domain) ├── recommendation/ (Supporting Subdomain) ├── content/ (Supporting Subdomain) ├── moderation/ (Supporting Subdomain) ├── advertising/ (Generic Subdomain) ├── premium/ (Generic Subdomain) └── monetization/ (Generic Subdomain) ``` **Changements effectués:** Phase 1: Création de l'arborescence des 7 bounded contexts Phase 2: Déplacement des règles métier (01-19) vers domains/*/rules/ Phase 3: Déplacement des diagrammes d'entités vers domains/*/entities/ Phase 4: Déplacement des diagrammes flux/états/séquences vers domains/*/ Phase 5: Création des README.md pour chaque domaine Phase 6: Déplacement des features Gherkin vers domains/*/features/ Phase 7: Création du Context Map (domains/README.md) Phase 8: Mise à jour de mkdocs.yml pour la nouvelle navigation Phase 9: Correction automatique des liens internes (script fix-markdown-links.sh) Phase 10: Nettoyage de l'ancienne structure (regles-metier/, diagrammes/, features/) **Configuration des tests:** - Makefile: godog run docs/domains/*/features/ - scripts/generate-bdd-docs.py: features_dir → docs/domains **Avantages:** ✅ Cohésion forte: toute la doc d'un domaine au même endroit ✅ Couplage faible: domaines indépendants, dépendances explicites ✅ Navigabilité améliorée: README par domaine = entrée claire ✅ Alignement code/docs: miroir de backend/internal/ ✅ Onboarding facilité: exploration domaine par domaine ✅ Tests BDD intégrés: features au plus près des règles métier Voir docs/REFACTOR-DDD.md pour le plan complet.
This commit is contained in:
38
docs/domains/premium/README.md
Normal file
38
docs/domains/premium/README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Domaine : Premium
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
Le domaine **Premium** gère les abonnements payants et les fonctionnalités exclusives pour les utilisateurs premium. C'est un **Generic Subdomain** essentiel au modèle économique freemium.
|
||||
|
||||
## Responsabilités
|
||||
|
||||
- **Abonnements** : Gestion des souscriptions et renouvellements
|
||||
- **Mode offline** : Téléchargement et synchronisation de contenus
|
||||
- **Notifications** : Alertes personnalisées pour abonnés
|
||||
- **Avantages premium** : Accès aux fonctionnalités exclusives (sans pub, qualité audio supérieure, etc.)
|
||||
|
||||
## Règles métier
|
||||
|
||||
- [Premium](rules/premium.md) - Fonctionnalités et avantages
|
||||
- [Mode offline](rules/mode-offline.md) - Téléchargement et synchronisation
|
||||
- [Abonnements et notifications](rules/abonnements-notifications.md)
|
||||
|
||||
## Modèle de données
|
||||
|
||||
- [Diagramme entités premium](entities/modele-premium.md) - Entités : PREMIUM_SUBSCRIPTIONS, ACTIVE_STREAMS, OFFLINE_DOWNLOADS
|
||||
|
||||
## Ubiquitous Language
|
||||
|
||||
**Termes métier du domaine** :
|
||||
- **Premium Subscription** : Abonnement payant mensuel ou annuel
|
||||
- **Offline Download** : Téléchargement pour écoute hors-ligne
|
||||
- **Sync Queue** : File d'attente de synchronisation offline
|
||||
- **Premium Tier** : Niveau d'abonnement (Basic, Plus, Family)
|
||||
- **Auto-Renewal** : Renouvellement automatique de l'abonnement
|
||||
- **Premium Notification** : Notification personnalisée pour abonnés
|
||||
|
||||
## Dépendances
|
||||
|
||||
- ✅ Dépend de : `_shared` (users, subscriptions)
|
||||
- ⚠️ Interactions avec : `advertising` (désactivation des pubs)
|
||||
- ⚠️ Interactions avec : `content` (téléchargement de contenus)
|
||||
58
docs/domains/premium/entities/modele-premium.md
Normal file
58
docs/domains/premium/entities/modele-premium.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Modèle de données - Premium
|
||||
|
||||
📖 Voir [Règles métier - Section 17 : Premium](../rules/premium.md) | [Entités globales](../../_shared/entities/modele-global.md)
|
||||
|
||||
## Diagramme
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
PREMIUM_SUBSCRIPTIONS }o--|| USERS : "abonnement"
|
||||
ACTIVE_STREAMS }o--|| USERS : "stream actif"
|
||||
ACTIVE_STREAMS }o--|| CONTENTS : "écoute"
|
||||
|
||||
OFFLINE_DOWNLOADS }o--|| USERS : "téléchargé par"
|
||||
OFFLINE_DOWNLOADS }o--|| CONTENTS : "contenu"
|
||||
|
||||
PREMIUM_SUBSCRIPTIONS {
|
||||
uuid id PK
|
||||
uuid user_id FK UK
|
||||
string provider
|
||||
string provider_subscription_id
|
||||
string provider_user_id
|
||||
string status
|
||||
string plan
|
||||
timestamp current_period_start
|
||||
timestamp current_period_end
|
||||
timestamp cancelled_at
|
||||
timestamp created_at
|
||||
}
|
||||
|
||||
ACTIVE_STREAMS {
|
||||
uuid id PK
|
||||
uuid user_id FK UK
|
||||
uuid content_id FK
|
||||
string device_id
|
||||
string mode
|
||||
int last_position_seconds
|
||||
timestamp started_at
|
||||
timestamp last_heartbeat
|
||||
}
|
||||
|
||||
OFFLINE_DOWNLOADS {
|
||||
uuid id PK
|
||||
uuid user_id FK
|
||||
uuid content_id FK
|
||||
string quality
|
||||
int file_size_bytes
|
||||
timestamp downloaded_at
|
||||
timestamp expires_at
|
||||
}
|
||||
```
|
||||
|
||||
## Légende
|
||||
|
||||
**Entités Premium** :
|
||||
|
||||
- **PREMIUM_SUBSCRIPTIONS** : Abonnements Premium - Provider : `mangopay` (web 4.99€), `apple` (IAP 5.99€), `google` (Play 5.99€) - Status : `active`, `cancelled`, `expired`, `past_due` - Plan : `monthly` (4.99€), `yearly` (49.99€ = 2 mois offerts) - Pas d'essai gratuit - Vérification temps réel via Redis cache (TTL 1h) + webhooks providers
|
||||
- **ACTIVE_STREAMS** : Streams actifs multi-devices - Limite 1 stream actif par compte (dernier device prioritaire KISS) - Mode : `online`, `offline` (si offline + WiFi/4G) - Heartbeat 30s, TTL Redis 5 min - Détection simultanée : WebSocket close device précédent - Exception : offline mode avion (pas de détection possible)
|
||||
- **OFFLINE_DOWNLOADS** : Téléchargements offline - Quality : `low` (24 kbps), `standard` (48 kbps), `high` (64 kbps Premium only) - Limite gratuit : 50 contenus max, Premium : illimité (espace disque) - Validité 30j, renouvellement auto si WiFi - Suppression auto après expiration
|
||||
@@ -0,0 +1,263 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Audio-guides multi-séquences pour piétons
|
||||
En tant qu'auditeur à pied
|
||||
Je veux profiter d'audio-guides structurés lors de mes visites
|
||||
Afin de découvrir des lieux de manière autonome et à mon rythme
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est disponible
|
||||
Et que je suis connecté en tant qu'auditeur
|
||||
Et que je suis en mode piéton (vitesse <5 km/h)
|
||||
|
||||
Scénario: Détection d'audio-guide à proximité
|
||||
Étant donné que je me trouve à 80 mètres du Musée du Louvre
|
||||
Et que 3 audio-guides sont disponibles pour ce lieu
|
||||
Quand le système détecte ma position
|
||||
Alors je reçois une notification push:
|
||||
"""
|
||||
📍 Audio-guide disponible : Musée du Louvre
|
||||
Choisissez parmi 3 guides pour Musée du Louvre
|
||||
Tap pour explorer
|
||||
"""
|
||||
|
||||
Scénario: Rayon de détection de 100m
|
||||
Étant donné qu'un audio-guide est centré aux coordonnées GPS du Louvre
|
||||
Quand je suis à exactement 100m du centre
|
||||
Alors la notification est déclenchée
|
||||
Et quand je suis à 101m, aucune notification n'est envoyée
|
||||
|
||||
Scénario: Page de sélection des audio-guides
|
||||
Étant donné que j'ai tapé sur la notification audio-guide
|
||||
Quand la page de sélection s'affiche
|
||||
Alors je vois une liste de guides disponibles:
|
||||
| titre | créateur | nb_sequences | durée | note | écoutes |
|
||||
| Visite complète | Créateur A | 12 | 45 min | 4.8 | 1.2K |
|
||||
| Œuvres majeures | Créateur B | 5 | 20 min | 4.9 | 3.5K |
|
||||
| Visite famille | Créateur C | 8 | 30 min | 4.7 | 850 |
|
||||
|
||||
Scénario: Sélection d'un audio-guide
|
||||
Étant donné que je suis sur la page de sélection
|
||||
Quand je tape sur "Visite complète (45 min)"
|
||||
Alors l'interface de lecture d'audio-guide s'ouvre
|
||||
Et la séquence 1 commence automatiquement
|
||||
Et je vois la liste complète des 12 séquences
|
||||
|
||||
Scénario: Interface de lecture audio-guide
|
||||
Étant donné que j'ai sélectionné un audio-guide de 12 séquences
|
||||
Quand l'interface s'affiche
|
||||
Alors je vois:
|
||||
| élément | exemple |
|
||||
| Titre guide | 🎨 Visite complète • Musée du Louvre |
|
||||
| Piste actuelle | Piste 2/12 |
|
||||
| Titre séquence | "La Joconde - Histoire et mystères" |
|
||||
| Barre de progression | 3:24 / 6:50 |
|
||||
| Liste séquences | ✅ 1. Intro, ▶️ 2. Joconde, ⏸️ 3. Vénus... |
|
||||
| Boutons navigation | Précédent, Play/Pause, Suivant |
|
||||
|
||||
Scénario: Navigation vers séquence suivante
|
||||
Étant donné que j'écoute la séquence 2
|
||||
Quand je tape sur "Suivant"
|
||||
Alors la séquence 3 commence immédiatement
|
||||
Et le titre de la séquence s'affiche: "Vénus de Milo"
|
||||
Et la barre de progression se réinitialise
|
||||
|
||||
Scénario: Navigation vers séquence précédente
|
||||
Étant donné que j'écoute la séquence 5
|
||||
Quand je tape sur "Précédent"
|
||||
Alors la séquence 4 recommence depuis le début
|
||||
Et je peux réécouter cette séquence
|
||||
|
||||
Scénario: Saut direct à une séquence spécifique
|
||||
Étant donné que j'écoute la séquence 2
|
||||
Et que la liste des séquences est affichée
|
||||
Quand je tape sur "7. Peintures Renaissance"
|
||||
Alors la séquence 7 démarre immédiatement
|
||||
Et je passe directement de la séquence 2 à la 7
|
||||
|
||||
Scénario: Commande vocale "Suivant"
|
||||
Étant donné que j'écoute la séquence 3
|
||||
Quand je dis "Suivant" via la commande vocale
|
||||
Alors la séquence 4 démarre
|
||||
Et la commande vocale fonctionne même si l'écran est verrouillé
|
||||
|
||||
Scénario: Commande vocale "Précédent"
|
||||
Étant donné que j'écoute la séquence 6
|
||||
Quand je dis "Précédent" via la commande vocale
|
||||
Alors la séquence 5 démarre depuis le début
|
||||
|
||||
Scénario: Pause et reprise à la position exacte
|
||||
Étant donné que j'écoute la séquence 4 à la position 2:30
|
||||
Quand je mets en pause
|
||||
Et que j'attends 5 minutes
|
||||
Et que je reprends la lecture
|
||||
Alors la séquence reprend exactement à 2:30
|
||||
Et aucune donnée n'est perdue
|
||||
|
||||
Scénario: Guidage vocal automatique entre séquences
|
||||
Étant donné que la séquence 2 se termine
|
||||
Quand la transition vers la séquence 3 se produit
|
||||
Alors j'entends un message vocal:
|
||||
"""
|
||||
Vous avez terminé la séquence 2. Dirigez-vous vers la Vénus de Milo pour la séquence 3.
|
||||
"""
|
||||
Et la séquence 3 ne démarre pas automatiquement (navigation manuelle)
|
||||
|
||||
Scénario: Avertissement si éloignement du point d'intérêt
|
||||
Étant donné que je suis dans le guide du Louvre
|
||||
Et que je devrais être devant la Vénus de Milo (séquence 3)
|
||||
Quand je m'éloigne de plus de 50m de ce point
|
||||
Alors j'entends un message vocal:
|
||||
"""
|
||||
Vous vous éloignez de la prochaine étape. Consultez le plan.
|
||||
"""
|
||||
Et un bouton "Voir le plan" apparaît dans l'interface
|
||||
|
||||
Scénario: Sauvegarde automatique de la progression
|
||||
Étant donné que j'écoute la séquence 5 à la position 1:45
|
||||
Quand je ferme l'application brutalement
|
||||
Et que je la rouvre 10 minutes plus tard
|
||||
Alors je vois une popup "Reprendre la visite du Musée du Louvre ?"
|
||||
Et si je choisis "Reprendre", je retourne à la séquence 5 à 1:45
|
||||
|
||||
Scénario: Option de recommencer depuis le début
|
||||
Étant donné que j'ai une progression sauvegardée à la séquence 7
|
||||
Quand je rouvre le guide
|
||||
Alors je vois 2 options:
|
||||
| option | action |
|
||||
| Reprendre à la séquence 7 | Reprend à la position exacte |
|
||||
| Recommencer depuis le début | Retourne à la séquence 1 |
|
||||
|
||||
Scénario: Expiration de la sauvegarde après 30 jours
|
||||
Étant donné que j'ai une progression sauvegardée depuis 30 jours
|
||||
Quand j'essaie de reprendre le guide
|
||||
Alors la sauvegarde est considérée comme expirée
|
||||
Et je recommence depuis la séquence 1
|
||||
Et je vois le message "Votre précédente visite date de plus de 30 jours. Recommençons depuis le début."
|
||||
|
||||
Scénario: Synchronisation multi-device de la progression
|
||||
Étant donné que j'écoute un guide sur mon iPhone à la séquence 4
|
||||
Quand je ferme l'app et ouvre sur mon iPad
|
||||
Alors je vois la progression synchronisée
|
||||
Et je peux reprendre à la séquence 4 sur l'iPad
|
||||
|
||||
Scénario: Marquage "Terminé" après toutes les séquences
|
||||
Étant donné que j'écoute la dernière séquence (12/12)
|
||||
Quand cette séquence se termine
|
||||
Alors le guide est marqué "✅ Terminé" dans mon historique
|
||||
Et je vois un message de félicitation:
|
||||
"""
|
||||
Félicitations ! Vous avez terminé la visite complète du Musée du Louvre.
|
||||
"""
|
||||
Et le créateur gagne les statistiques d'écoute complète
|
||||
|
||||
Scénario: Création d'audio-guide par un créateur
|
||||
Étant donné que je suis un créateur
|
||||
Quand je crée un nouvel audio-guide
|
||||
Alors je dois:
|
||||
| étape | détail |
|
||||
| Uploader plusieurs fichiers | 1 fichier MP3 par séquence |
|
||||
| Numéroter les séquences | Séquence 1, Séquence 2, etc. |
|
||||
| Titrer chaque séquence | "Introduction", "La Joconde", etc. |
|
||||
| Définir point GPS unique | Centre du lieu (ex: Louvre) |
|
||||
| Définir rayon de détection | Par défaut 100m |
|
||||
Et la durée totale est calculée automatiquement
|
||||
|
||||
Scénario: Structure JSON de stockage audio-guide
|
||||
Étant donné qu'un créateur publie un audio-guide du Louvre
|
||||
Quand les métadonnées sont stockées en base
|
||||
Alors le format JSON contient:
|
||||
```json
|
||||
{
|
||||
"guide_id": "abc123",
|
||||
"title": "Visite complète Musée du Louvre",
|
||||
"location": {"lat": 48.8606, "lon": 2.3376, "radius": 200},
|
||||
"sequences": [
|
||||
{
|
||||
"sequence_number": 1,
|
||||
"title": "Introduction et architecture",
|
||||
"audio_url": "https://cdn.../seq1.mp3",
|
||||
"duration_seconds": 180
|
||||
},
|
||||
{
|
||||
"sequence_number": 2,
|
||||
"title": "La Joconde",
|
||||
"audio_url": "https://cdn.../seq2.mp3",
|
||||
"duration_seconds": 410
|
||||
}
|
||||
],
|
||||
"total_duration_seconds": 2700,
|
||||
"creator_id": "creator_xyz"
|
||||
}
|
||||
```
|
||||
|
||||
Scénario: Limitation du nombre de séquences
|
||||
Étant donné que je crée un audio-guide
|
||||
Quand j'essaie d'ajouter plus de 50 séquences
|
||||
Alors je vois le message "Maximum 50 séquences par audio-guide"
|
||||
Et je dois structurer mon contenu différemment ou créer plusieurs guides
|
||||
|
||||
Scénario: Quitter le guide et sauvegarder
|
||||
Étant donné que j'écoute la séquence 6
|
||||
Quand je tape sur le bouton "×" (fermer)
|
||||
Alors je vois une confirmation:
|
||||
"""
|
||||
Voulez-vous quitter ce guide ?
|
||||
Votre progression sera sauvegardée.
|
||||
"""
|
||||
Et si je confirme, la progression est enregistrée
|
||||
Et je retourne à l'écran principal
|
||||
|
||||
Scénario: Statistiques créateur pour audio-guides
|
||||
Étant donné que je suis créateur d'un audio-guide
|
||||
Quand je consulte mes statistiques
|
||||
Alors je vois:
|
||||
| métrique | exemple valeur |
|
||||
| Nombre de démarrages | 1250 |
|
||||
| Nombre de complétions (100%) | 387 (31%) |
|
||||
| Séquence la plus skippée | Séquence 8 |
|
||||
| Durée moyenne d'écoute | 28 min (sur 45) |
|
||||
|
||||
Scénario: Audio-guide multilingue (post-MVP)
|
||||
Étant donné qu'un créateur peut publier plusieurs versions linguistiques
|
||||
Quand un touriste anglophone visite le Louvre
|
||||
Alors il voit les guides disponibles en anglais
|
||||
Et peut choisir parmi les guides traduits
|
||||
Mais cette fonctionnalité n'est pas disponible en MVP
|
||||
|
||||
Scénario: Publicité entre séquences d'audio-guide
|
||||
Étant donné que je suis un utilisateur gratuit
|
||||
Et que j'écoute un audio-guide
|
||||
Quand je passe de la séquence 5 à la séquence 6
|
||||
Alors une publicité peut être insérée (1 pub toutes les 5 séquences)
|
||||
Et la publicité est skippable après 5 secondes
|
||||
Et les utilisateurs Premium ne voient pas de publicité
|
||||
|
||||
Scénario: Audio-guide en mode offline
|
||||
Étant donné que j'ai téléchargé un audio-guide complet
|
||||
Quand je visite le lieu sans connexion internet
|
||||
Alors toutes les séquences sont disponibles hors ligne
|
||||
Et la navigation fonctionne normalement
|
||||
Et seule la sauvegarde cloud est différée jusqu'à reconnexion
|
||||
|
||||
Scénario: Notation d'un audio-guide après écoute
|
||||
Étant donné que j'ai terminé un audio-guide
|
||||
Quand je ferme l'interface
|
||||
Alors je vois une popup "Notez cette visite"
|
||||
Et je peux donner une note de 1 à 5 étoiles
|
||||
Et cette note contribue à la note globale visible par les autres utilisateurs
|
||||
|
||||
Scénario: Filtrage par langue dans la page de sélection
|
||||
Étant donné que plusieurs audio-guides sont disponibles en différentes langues
|
||||
Quand j'accède à la page de sélection
|
||||
Alors je peux filtrer par langue
|
||||
Et par défaut, les guides dans ma langue système sont affichés en premier
|
||||
|
||||
Scénario: Réutilisation de l'infrastructure existante
|
||||
Étant donné qu'un audio-guide est techniquement un contenu structuré
|
||||
Alors il réutilise:
|
||||
| composant | usage |
|
||||
| OVH Object Storage | Hébergement fichiers MP3 séquences |
|
||||
| Streaming HLS | Diffusion audio adaptative |
|
||||
| Cache Redis | Métadonnées guides + progressions |
|
||||
| PostgreSQL | Stockage structure JSON guides |
|
||||
Et aucune infrastructure dédiée n'est nécessaire
|
||||
@@ -0,0 +1,146 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Impact des abonnements sur l'algorithme
|
||||
En tant qu'auditeur
|
||||
Je veux que les contenus de mes créateurs suivis soient favorisés
|
||||
Afin de ne pas rater leurs publications tout en découvrant de nouveaux contenus
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est disponible
|
||||
Et que je suis connecté en tant qu'auditeur
|
||||
Et que je suis abonné au créateur "JeanDupont"
|
||||
|
||||
Scénario: Boost de +30% appliqué au score final
|
||||
Étant donné un contenu du créateur "JeanDupont" avec:
|
||||
| score_geo | 0.5 |
|
||||
| score_interet | 0.6 |
|
||||
| score_engage | 0.5 |
|
||||
Quand le score final est calculé
|
||||
Alors le score de base est 0.53
|
||||
Et le boost abonnement de +30% est appliqué
|
||||
Et le score final avec boost est 0.69
|
||||
|
||||
Scénario: Contenu non-suivi peut battre contenu suivi
|
||||
Étant donné que je suis à Paris
|
||||
Et que 2 contenus sont disponibles:
|
||||
| contenu | createur_suivi | score_geo | score_interet | score_engage | score_final_base | score_avec_boost |
|
||||
| Contenu A | Non | 0.9 | 0.8 | 0.7 | 0.80 | 0.80 |
|
||||
| Contenu B | Oui | 0.5 | 0.6 | 0.5 | 0.53 | 0.69 |
|
||||
Quand l'algorithme sélectionne le prochain contenu
|
||||
Alors le Contenu A est proposé en premier
|
||||
Car son score (0.80) est supérieur au Contenu B avec boost (0.69)
|
||||
|
||||
Scénario: Contenu suivi remporte grâce au boost
|
||||
Étant donné que je suis à Paris
|
||||
Et que 2 contenus sont disponibles:
|
||||
| contenu | createur_suivi | score_final_base | score_avec_boost |
|
||||
| Contenu A | Non | 0.70 | 0.70 |
|
||||
| Contenu B | Oui | 0.60 | 0.78 |
|
||||
Quand l'algorithme sélectionne le prochain contenu
|
||||
Alors le Contenu B est proposé en premier
|
||||
Car le boost fait pencher la balance (0.78 > 0.70)
|
||||
|
||||
Scénario: Contenu suivi avec faible engagement ne domine pas
|
||||
Étant donné que je suis abonné au créateur "CreateurMoyen"
|
||||
Et qu'il publie un contenu avec très faible engagement (score 0.30)
|
||||
Et qu'un contenu viral d'un créateur non-suivi a un score de 0.85
|
||||
Quand l'algorithme sélectionne le prochain contenu
|
||||
Alors le contenu viral est proposé en premier (0.85)
|
||||
Car même avec boost, le contenu suivi obtient seulement 0.39 (0.30 × 1.3)
|
||||
|
||||
Scénario: Pas de file dédiée aux abonnements
|
||||
Étant donné que je suis abonné à 50 créateurs
|
||||
Quand l'algorithme génère ma file d'attente de 5 contenus
|
||||
Alors les contenus suivis et non-suivis sont mélangés
|
||||
Et tous entrent en compétition selon leurs scores (avec boost si abonnement)
|
||||
Et il n'y a pas de section séparée "Contenus de vos abonnements"
|
||||
|
||||
Scénario: Vérification du calcul du boost
|
||||
Étant donné un contenu d'un créateur suivi
|
||||
Et que le score final de base est calculé à 0.65
|
||||
Quand le boost abonnement est appliqué
|
||||
Alors le multiplicateur utilisé est exactement 1.3
|
||||
Et le score final avec boost est 0.845 (0.65 × 1.3)
|
||||
Et le résultat est arrondi à 2 décimales: 0.85
|
||||
|
||||
Scénario: Boost appliqué à tous les contenus du créateur suivi
|
||||
Étant donné que je suis abonné au créateur "JeanDupont"
|
||||
Et qu'il a publié 10 contenus différents
|
||||
Quand l'algorithme évalue chacun de ces contenus
|
||||
Alors le boost de +30% est appliqué à tous les 10 contenus
|
||||
Et chaque contenu bénéficie du même multiplicateur 1.3
|
||||
|
||||
Scénario: Plusieurs créateurs suivis en compétition
|
||||
Étant donné que je suis abonné à "Créateur A" et "Créateur B"
|
||||
Et que les 2 ont des contenus disponibles dans ma zone:
|
||||
| createur | score_base | score_avec_boost |
|
||||
| Créateur A | 0.70 | 0.91 |
|
||||
| Créateur B | 0.65 | 0.85 |
|
||||
Quand l'algorithme sélectionne le prochain contenu
|
||||
Alors le contenu du Créateur A est proposé en premier (0.91 > 0.85)
|
||||
Et les 2 bénéficient du boost, mais le meilleur score gagne
|
||||
|
||||
Scénario: Contenu national d'un créateur suivi
|
||||
Étant donné que je suis abonné à "MediaNational"
|
||||
Et qu'il publie un contenu de type "National" (score_geo 0.2)
|
||||
Quand le score est calculé avec:
|
||||
| score_geo | score_interet | score_engage |
|
||||
| 0.2 | 0.7 | 0.6 |
|
||||
Alors le score de base est environ 0.50
|
||||
Et avec le boost abonnement, le score devient 0.65
|
||||
Et le contenu peut être proposé malgré son score géo faible
|
||||
|
||||
Scénario: Transparence du boost dans les paramètres
|
||||
Quand j'accède aux paramètres de l'algorithme de recommandation
|
||||
Alors je vois l'information: "Les contenus de vos créateurs suivis bénéficient d'un boost de +30%"
|
||||
Et je comprends que ce n'est pas une priorité absolue
|
||||
Et que la découverte de nouveaux contenus reste possible
|
||||
|
||||
Scénario: Boost désactivé si désabonnement
|
||||
Étant donné que je suis abonné au créateur "JeanDupont"
|
||||
Et qu'un de ses contenus bénéficiait du boost +30%
|
||||
Quand je me désabonne de "JeanDupont"
|
||||
Alors ses contenus n'ont plus le boost
|
||||
Et leur score revient au score de base sans multiplicateur
|
||||
|
||||
Scénario: Contenu d'un créateur nouvellement suivi
|
||||
Étant donné que je viens de m'abonner à "NouveauCreateur"
|
||||
Et qu'il a publié un contenu il y a 2 jours
|
||||
Quand l'algorithme recalcule les scores
|
||||
Alors le boost de +30% est immédiatement appliqué à ce contenu
|
||||
Et il peut apparaître dans ma prochaine file d'attente
|
||||
|
||||
Scénario: Impact sur le taux de contenu suivi dans le feed
|
||||
Étant donné que je suis abonné à 30 créateurs
|
||||
Et que j'écoute 100 contenus sur une semaine
|
||||
Quand j'analyse la répartition
|
||||
Alors environ 40-50% des contenus proviennent de créateurs suivis
|
||||
Et 50-60% proviennent de créateurs non-suivis (découverte)
|
||||
Car le boost favorise sans dominer
|
||||
|
||||
Scénario: Contenu suivi hors zone géographique
|
||||
Étant donné que je suis à Paris
|
||||
Et que je suis abonné à un créateur de Marseille
|
||||
Et qu'il publie un contenu ancré à Marseille (hors de portée)
|
||||
Quand l'algorithme évalue ce contenu
|
||||
Alors le score géo est quasi nul (0.05)
|
||||
Et même avec boost +30%, le score reste très faible
|
||||
Et le contenu n'est probablement pas proposé
|
||||
|
||||
Scénario: Performance de calcul du boost
|
||||
Étant donné que je suis abonné à 100 créateurs
|
||||
Et que l'algorithme évalue 1000 contenus potentiels
|
||||
Quand le calcul des scores avec boost est effectué
|
||||
Alors le temps de calcul reste inférieur à 50ms
|
||||
Car le boost est une simple multiplication (opération O(1))
|
||||
Et la requête SQL utilise un JOIN sur la table abonnements
|
||||
|
||||
Scénario: Boost combiné avec d'autres facteurs
|
||||
Étant donné un contenu d'un créateur suivi
|
||||
Et que le contenu bénéficie aussi de:
|
||||
| facteur | impact |
|
||||
| Score d'engagement élevé | +20% |
|
||||
| Contenu récent (<24h) | +10% |
|
||||
| Boost abonnement | +30% |
|
||||
Quand le score final est calculé
|
||||
Alors le boost abonnement s'applique au score final (après tous les autres calculs)
|
||||
Et les boosts ne s'additionnent pas, le boost abonnement est un multiplicateur final
|
||||
@@ -0,0 +1,244 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Limites d'abonnements et désabonnement
|
||||
En tant qu'auditeur
|
||||
Je veux gérer mes abonnements de manière équilibrée
|
||||
Afin de suivre mes créateurs préférés sans être submergé
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est disponible
|
||||
Et que je suis connecté en tant qu'auditeur
|
||||
|
||||
Scénario: Limite maximale de 200 abonnements
|
||||
Étant donné que je suis abonné à 199 créateurs
|
||||
Quand j'essaie de m'abonner à un 200ème créateur
|
||||
Alors l'abonnement réussit
|
||||
Et je suis maintenant abonné à 200 créateurs
|
||||
|
||||
Scénario: Impossible de dépasser 200 abonnements
|
||||
Étant donné que je suis déjà abonné à 200 créateurs
|
||||
Quand j'essaie de m'abonner à un nouveau créateur
|
||||
Alors l'action échoue
|
||||
Et je vois le message:
|
||||
"""
|
||||
Vous suivez déjà 200 créateurs. Désabonnez-vous d'un créateur pour en suivre un nouveau.
|
||||
"""
|
||||
|
||||
Scénario: Suggestion de désabonnement de créateurs inactifs
|
||||
Étant donné que je suis abonné à 200 créateurs
|
||||
Et que j'essaie de m'abonner à un nouveau créateur
|
||||
Quand je vois le message de limite atteinte
|
||||
Alors je vois aussi une suggestion:
|
||||
"""
|
||||
Vous n'avez pas écouté [Créateur X] depuis 6 mois, le désabonner ?
|
||||
"""
|
||||
Et un bouton "Désabonner" est proposé pour ce créateur
|
||||
|
||||
Scénario: Liste triable des abonnements
|
||||
Étant donné que je suis abonné à 150 créateurs
|
||||
Quand j'accède à ma liste d'abonnements
|
||||
Alors je peux trier par:
|
||||
| critère | ordre |
|
||||
| Date d'abonnement | Plus récent / Plus ancien |
|
||||
| Nombre de contenus écoutés| Plus écoutés / Moins écoutés |
|
||||
| Dernière activité créateur| Plus récent / Plus ancien |
|
||||
| Ordre alphabétique | A-Z / Z-A |
|
||||
|
||||
Scénario: Abonnement initial augmente les jauges de +5%
|
||||
Étant donné que mes jauges d'intérêt sont:
|
||||
| catégorie | valeur initiale |
|
||||
| Automobile | 60% |
|
||||
| Voyage | 55% |
|
||||
Et qu'un créateur tague ses contenus "Automobile" et "Voyage"
|
||||
Quand je m'abonne à ce créateur
|
||||
Alors mes jauges évoluent:
|
||||
| catégorie | nouvelle valeur |
|
||||
| Automobile | 65% (+5%) |
|
||||
| Voyage | 60% (+5%) |
|
||||
|
||||
Scénario: Abonnement avec créateur ayant 3 tags
|
||||
Étant donné qu'un créateur tague ses contenus:
|
||||
| tags |
|
||||
| Automobile, Voyage, Technologie |
|
||||
Et que mes jauges sont toutes à 50%
|
||||
Quand je m'abonne à ce créateur
|
||||
Alors les 3 jauges augmentent de +5%:
|
||||
| catégorie | nouvelle valeur |
|
||||
| Automobile | 55% |
|
||||
| Voyage | 55% |
|
||||
| Technologie | 55% |
|
||||
|
||||
Scénario: Désabonnement diminue les jauges de -5%
|
||||
Étant donné que je suis abonné à un créateur avec tags "Politique" et "Économie"
|
||||
Et que mes jauges sont:
|
||||
| catégorie | valeur actuelle |
|
||||
| Politique | 70% |
|
||||
| Économie | 65% |
|
||||
Quand je me désabonne de ce créateur
|
||||
Alors mes jauges évoluent:
|
||||
| catégorie | nouvelle valeur |
|
||||
| Politique | 65% (-5%) |
|
||||
| Économie | 60% (-5%) |
|
||||
|
||||
Scénario: Désabonnement sans confirmation
|
||||
Étant donné que je consulte le profil d'un créateur suivi
|
||||
Quand je clique sur "Se désabonner"
|
||||
Alors le désabonnement est immédiat
|
||||
Et aucune popup de confirmation n'apparaît
|
||||
Car l'action est réversible (je peux me réabonner)
|
||||
|
||||
Scénario: Réabonnement possible immédiatement
|
||||
Étant donné que je viens de me désabonner d'un créateur
|
||||
Quand je consulte à nouveau son profil
|
||||
Alors le bouton "S'abonner" est affiché
|
||||
Et je peux me réabonner immédiatement
|
||||
Et mes jauges augmentent à nouveau de +5%
|
||||
|
||||
Scénario: Effet symétrique abonnement/désabonnement
|
||||
Étant donné qu'un créateur a les tags "Musique" et "Culture"
|
||||
Et que ma jauge Musique est à 50%
|
||||
Quand je m'abonne puis me désabonne immédiatement
|
||||
Alors ma jauge revient exactement à 50%
|
||||
Et il n'y a pas de perte ou gain net
|
||||
|
||||
Scénario: Abonnement ne dépasse pas 100% de jauge
|
||||
Étant donné que ma jauge Automobile est à 97%
|
||||
Et qu'un créateur tague ses contenus "Automobile"
|
||||
Quand je m'abonne à ce créateur
|
||||
Alors ma jauge Automobile passe à 100% (limite max)
|
||||
Et l'augmentation effective est de +3% seulement
|
||||
|
||||
Scénario: Désabonnement ne descend pas sous 0%
|
||||
Étant donné que ma jauge Politique est à 3%
|
||||
Et que je suis abonné à un créateur avec tag "Politique"
|
||||
Quand je me désabonne de ce créateur
|
||||
Alors ma jauge Politique passe à 0% (limite min)
|
||||
Et la diminution effective est de -3% seulement
|
||||
|
||||
Scénario: Créateur ne voit pas qui est abonné (privacy)
|
||||
Étant donné que je suis abonné au créateur "JeanDupont"
|
||||
Quand "JeanDupont" consulte ses statistiques
|
||||
Alors il voit le nombre total d'abonnés (ex: "1,247 abonnés")
|
||||
Mais il ne voit pas la liste des utilisateurs abonnés
|
||||
Et mon identité reste privée
|
||||
|
||||
Scénario: Créateur voit uniquement le nombre total d'abonnés
|
||||
Étant donné que je suis créateur
|
||||
Et que j'ai 523 abonnés
|
||||
Quand je consulte mes statistiques
|
||||
Alors je vois "523 abonnés"
|
||||
Mais je ne peux pas:
|
||||
| action interdite |
|
||||
| Voir la liste des abonnés |
|
||||
| Contacter mes abonnés individuellement|
|
||||
| Voir leurs profils |
|
||||
|
||||
Scénario: Pas d'abonnement mutuel visible
|
||||
Étant donné que je suis abonné au créateur "Alice"
|
||||
Et qu'"Alice" est abonnée à mon compte créateur
|
||||
Quand je consulte le profil d'"Alice"
|
||||
Alors je ne vois pas d'indication qu'elle est abonnée à moi
|
||||
Et il n'y a pas de badge "Abonné mutuellement"
|
||||
|
||||
Scénario: Performance avec 200 abonnements
|
||||
Étant donné que je suis abonné à 200 créateurs
|
||||
Quand l'algorithme calcule ma recommandation
|
||||
Alors la requête SQL utilise un JOIN sur la table abonnements
|
||||
Et la table est indexée sur user_id et creator_id
|
||||
Et le temps de calcul reste inférieur à 50ms
|
||||
|
||||
Scénario: Impact sur la recommandation avec beaucoup d'abonnements
|
||||
Étant donné que je suis abonné à 150 créateurs très actifs
|
||||
Et qu'ils publient collectivement 100 contenus par jour
|
||||
Quand l'algorithme génère ma file de 5 contenus
|
||||
Alors environ 60-70% des contenus proviennent de créateurs suivis (grâce au boost +30%)
|
||||
Mais 30-40% proviennent de nouveaux créateurs (découverte)
|
||||
|
||||
Scénario: Notification de désabonnement au créateur (non implémenté)
|
||||
Étant donné que je me désabonne d'un créateur
|
||||
Alors le créateur ne reçoit aucune notification
|
||||
Et il ne peut pas savoir qui s'est désabonné
|
||||
Car cela préserve la privacy et évite le harcèlement
|
||||
|
||||
Scénario: Statistiques d'abonnements pour l'utilisateur
|
||||
Étant donné que je suis abonné à 87 créateurs
|
||||
Quand j'accède à mes statistiques d'abonnements
|
||||
Alors je vois:
|
||||
| métrique | exemple valeur |
|
||||
| Nombre total d'abonnements | 87 / 200 |
|
||||
| Créateurs les plus écoutés | Top 10 avec % écoute |
|
||||
| Créateurs non écoutés depuis 6 mois | 12 créateurs |
|
||||
| Nouveaux contenus non écoutés | 23 contenus |
|
||||
|
||||
Scénario: Recherche dans la liste d'abonnements
|
||||
Étant donné que je suis abonné à 120 créateurs
|
||||
Quand j'accède à ma liste d'abonnements
|
||||
Alors je peux chercher par nom de créateur
|
||||
Et les résultats sont filtrés en temps réel
|
||||
Et je trouve rapidement un créateur spécifique
|
||||
|
||||
Scénario: Export de la liste d'abonnements (RGPD)
|
||||
Étant donné que je demande l'export de mes données
|
||||
Quand l'export est généré
|
||||
Alors la liste de mes abonnements est incluse:
|
||||
```json
|
||||
{
|
||||
"subscriptions": [
|
||||
{
|
||||
"creator_name": "JeanDupont",
|
||||
"creator_id": "abc123",
|
||||
"subscribed_at": "2025-06-15T10:30:00Z"
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Scénario: Suppression compte utilisateur et impact sur abonnements
|
||||
Étant donné que je suis abonné à 50 créateurs
|
||||
Quand je supprime définitivement mon compte
|
||||
Alors tous mes abonnements sont supprimés
|
||||
Et le compteur d'abonnés de chaque créateur est décrémenté de -1
|
||||
Et les jauges n'existent plus (données supprimées)
|
||||
|
||||
Scénario: Suppression compte créateur et impact sur abonnés
|
||||
Étant donné que je suis abonné au créateur "Bob"
|
||||
Quand "Bob" supprime son compte créateur
|
||||
Alors je suis automatiquement désabonné
|
||||
Et mes jauges diminuent de -5% pour les tags de "Bob"
|
||||
Et je ne vois plus "Bob" dans ma liste d'abonnements
|
||||
|
||||
Scénario: Limite 200 justifiée par usage réaliste
|
||||
Étant donné que la moyenne d'abonnements sur YouTube est de ~50-100 chaînes
|
||||
Et que Twitter limite à 5000 follows (mais moyenne ~150)
|
||||
Quand RoadWave fixe la limite à 200
|
||||
Alors cela couvre largement 99% des utilisateurs
|
||||
Et évite les abus (comptes spam suivant tout le monde)
|
||||
|
||||
Scénario: Table PostgreSQL optimisée pour abonnements
|
||||
Étant donné la structure de table subscriptions:
|
||||
```sql
|
||||
CREATE TABLE subscriptions (
|
||||
user_id UUID,
|
||||
creator_id UUID,
|
||||
subscribed_at TIMESTAMP,
|
||||
PRIMARY KEY (user_id, creator_id)
|
||||
);
|
||||
CREATE INDEX idx_user_subscriptions ON subscriptions(user_id);
|
||||
CREATE INDEX idx_creator_subscribers ON subscriptions(creator_id);
|
||||
```
|
||||
Alors les requêtes d'abonnements sont O(1) avec index
|
||||
Et le count d'abonnés par créateur est rapide
|
||||
Et la vérification "est abonné ?" est instantanée
|
||||
|
||||
Scénario: Détection d'abonnements abusifs
|
||||
Étant donné qu'un utilisateur s'abonne à 200 créateurs en moins de 5 minutes
|
||||
Quand le système détecte cette activité suspecte
|
||||
Alors un rate limiting est appliqué (max 10 abonnements/minute)
|
||||
Et l'utilisateur voit "Trop d'actions rapides. Veuillez réessayer dans 1 minute"
|
||||
Et cela prévient les bots de spam
|
||||
|
||||
Scénario: Badge créateur vérifié visible dans abonnements
|
||||
Étant donné que je suis abonné à 3 créateurs dont 1 vérifié
|
||||
Quand je consulte ma liste d'abonnements
|
||||
Alors le créateur vérifié a un badge ✓ bleu
|
||||
Et les créateurs non vérifiés n'ont pas de badge
|
||||
@@ -0,0 +1,239 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Notifications contextuelles selon le mode de déplacement
|
||||
En tant qu'auditeur
|
||||
Je veux recevoir des notifications adaptées à mon contexte
|
||||
Afin d'être informé sans être distrait en conduisant
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est disponible
|
||||
Et que je suis connecté en tant qu'auditeur
|
||||
Et que j'ai activé les notifications
|
||||
|
||||
Scénario: Détection automatique du contexte en voiture
|
||||
Étant donné que ma vitesse GPS est de 50 km/h
|
||||
Quand le système détecte mon contexte
|
||||
Alors je suis identifié comme "En voiture"
|
||||
Et les notifications push sont désactivées
|
||||
Et seules les notifications in-app sont actives
|
||||
|
||||
Scénario: Détection automatique du contexte à pied
|
||||
Étant donné que ma vitesse GPS est de 3 km/h
|
||||
Quand le système détecte mon contexte
|
||||
Alors je suis identifié comme "À pied"
|
||||
Et les notifications push sont activées
|
||||
Et l'interface tactile et vocale sont disponibles
|
||||
|
||||
Scénario: Zone de transition 5-10 km/h
|
||||
Étant donné que ma vitesse GPS varie entre 5 et 10 km/h
|
||||
Quand le système détecte mon contexte
|
||||
Alors un algorithme de lissage est appliqué sur 30 secondes
|
||||
Et le mode est déterminé selon la vitesse moyenne
|
||||
Et les changements de mode ne sont pas trop fréquents
|
||||
|
||||
Scénario: Nouveau contenu créateur suivi - Mode voiture
|
||||
Étant donné que je suis en voiture (vitesse >10 km/h)
|
||||
Et que je suis abonné au créateur "JeanDupont"
|
||||
Quand "JeanDupont" publie un nouveau contenu dans ma zone
|
||||
Alors je ne reçois pas de notification push
|
||||
Mais je vois un badge compteur in-app
|
||||
Et le contenu apparaît dans ma file avec boost +30%
|
||||
|
||||
Scénario: Nouveau contenu créateur suivi - Mode piéton
|
||||
Étant donné que je suis à pied (vitesse <5 km/h)
|
||||
Et que je suis abonné au créateur "JeanDupont"
|
||||
Et que je suis situé en Île-de-France
|
||||
Quand "JeanDupont" publie un contenu géolocalisé en Île-de-France
|
||||
Alors je reçois une notification push:
|
||||
"""
|
||||
🎧 JeanDupont a publié : "Titre du contenu"
|
||||
Tap pour écouter
|
||||
"""
|
||||
|
||||
Scénario: Live créateur suivi - Mode voiture
|
||||
Étant donné que je suis en voiture
|
||||
Et que je suis abonné au créateur "RadioLive"
|
||||
Quand "RadioLive" démarre un live dans ma zone
|
||||
Alors je ne reçois pas de notification push
|
||||
Mais je vois un badge compteur in-app
|
||||
Et le live peut apparaître dans ma recommandation automatiquement
|
||||
|
||||
Scénario: Live créateur suivi - Mode piéton
|
||||
Étant donné que je suis à pied
|
||||
Et que je suis abonné au créateur "RadioLive"
|
||||
Et que je suis situé dans la zone du live
|
||||
Quand "RadioLive" démarre un live
|
||||
Alors je reçois une notification push:
|
||||
"""
|
||||
🔴 RadioLive est en direct : "Titre du live"
|
||||
Tap pour rejoindre
|
||||
"""
|
||||
|
||||
Scénario: Audio-guide disponible à proximité - Mode piéton
|
||||
Étant donné que je suis à pied
|
||||
Quand je passe à moins de 100m d'un lieu avec audio-guides
|
||||
Alors je reçois une notification push:
|
||||
"""
|
||||
📍 Audio-guide disponible : Musée du Louvre
|
||||
Choisissez parmi 3 guides pour Musée du Louvre
|
||||
Tap pour explorer
|
||||
"""
|
||||
|
||||
Scénario: Audio-guide disponible à proximité - Mode voiture
|
||||
Étant donné que je suis en voiture
|
||||
Quand je passe à moins de 100m d'un lieu avec audio-guides
|
||||
Alors je reçois une notification audio (bip)
|
||||
Et une annonce vocale: "Audio-guide disponible"
|
||||
Mais pas de notification push (sécurité)
|
||||
|
||||
Scénario: Filtrage géographique des notifications
|
||||
Étant donné que je suis abonné au créateur "CreateurMarseille"
|
||||
Et que je suis situé à Paris
|
||||
Quand "CreateurMarseille" publie un contenu ancré à Marseille
|
||||
Alors je ne reçois pas de notification
|
||||
Car le contenu est hors de ma zone géographique
|
||||
Et cela évite la frustration de contenus non écoutables
|
||||
|
||||
Scénario: Contenu national notifie tous les abonnés
|
||||
Étant donné que je suis abonné au créateur "MediaNational"
|
||||
Et que je suis situé n'importe où en France
|
||||
Quand "MediaNational" publie un contenu de type "National"
|
||||
Alors je reçois une notification (si mode piéton)
|
||||
Car les contenus nationaux ne sont pas filtrés géographiquement
|
||||
|
||||
Scénario: Limite de 10 notifications push par jour
|
||||
Étant donné que je suis abonné à 50 créateurs actifs
|
||||
Et que j'ai déjà reçu 10 notifications push aujourd'hui
|
||||
Quand un 11ème contenu est publié
|
||||
Alors je ne reçois pas de notification push individuelle
|
||||
Mais une notification groupée: "🎧 3 nouveaux contenus de créateurs suivis"
|
||||
|
||||
Scénario: Paramétrage de la limite quotidienne
|
||||
Étant donné que la limite par défaut est de 10 notifications/jour
|
||||
Quand j'accède aux paramètres de notifications
|
||||
Alors je peux modifier la limite entre 5 et 20
|
||||
Et si je choisis 15, je recevrai jusqu'à 15 notifications/jour
|
||||
|
||||
Scénario: Mode silencieux nocturne par défaut
|
||||
Étant donné que le mode silencieux est activé de 22h à 8h par défaut
|
||||
Et qu'il est 23h30
|
||||
Quand un créateur suivi publie un contenu
|
||||
Alors je ne reçois pas de notification push
|
||||
Mais les notifications sont empilées
|
||||
Et je les vois le lendemain matin à 8h01
|
||||
|
||||
Scénario: Exception du mode silencieux pour les lives
|
||||
Étant donné que le mode silencieux est activé (22h-8h)
|
||||
Et qu'il est 23h00
|
||||
Et que j'ai activé "Notifications importantes uniquement" (lives uniquement)
|
||||
Quand un créateur suivi démarre un live
|
||||
Alors je reçois quand même la notification push du live
|
||||
Car les lives sont des événements temps réel prioritaires
|
||||
|
||||
Scénario: Désactivation complète des notifications
|
||||
Étant donné que j'accède aux paramètres de notifications
|
||||
Quand je désactive toutes les notifications
|
||||
Alors je ne reçois plus aucune notification push
|
||||
Et les badges in-app sont également désactivés
|
||||
Et seule la recommandation algorithmique reste active
|
||||
|
||||
Scénario: Notification "Nouveaux contenus" activée par défaut
|
||||
Étant donné que je crée un nouveau compte
|
||||
Et que je m'abonne à mon premier créateur
|
||||
Quand je consulte les préférences de notifications
|
||||
Alors "Nouveaux contenus" est activé par défaut
|
||||
Et "Lives" est activé par défaut
|
||||
Et "Audio-guides proximité" est activé par défaut
|
||||
|
||||
Scénario: Désactivation sélective par type de notification
|
||||
Étant donné que j'ai activé toutes les notifications
|
||||
Quand je désactive uniquement "Nouveaux contenus"
|
||||
Alors je ne reçois plus de notifications pour nouveaux contenus
|
||||
Mais je reçois toujours les notifications de lives
|
||||
Et les notifications d'audio-guides restent actives
|
||||
|
||||
Scénario: Notification groupée après limite dépassée
|
||||
Étant donné que j'ai reçu 10 notifications push aujourd'hui
|
||||
Et que 5 nouveaux contenus sont publiés dans l'heure suivante
|
||||
Quand la 11ème notification devrait être envoyée
|
||||
Alors les 5 contenus sont regroupés en une seule notification:
|
||||
"""
|
||||
🎧 5 nouveaux contenus de créateurs suivis
|
||||
Tap pour voir la liste
|
||||
"""
|
||||
|
||||
Scénario: Détail de la notification groupée
|
||||
Étant donné que j'ai reçu une notification groupée "3 nouveaux contenus"
|
||||
Quand je tape sur la notification
|
||||
Alors l'app s'ouvre sur une liste des 3 contenus:
|
||||
| créateur | titre |
|
||||
| JeanDupont | "Actualité du jour" |
|
||||
| MarieDurand | "Podcast économie" |
|
||||
| PaulMartin | "Anecdote historique" |
|
||||
Et je peux choisir lequel écouter en premier
|
||||
|
||||
Scénario: Personnalisation des plages horaires du mode silencieux
|
||||
Étant donné que le mode silencieux est 22h-8h par défaut
|
||||
Quand j'accède aux paramètres
|
||||
Alors je peux modifier les heures: par exemple 23h-7h
|
||||
Et le mode silencieux s'applique dans la nouvelle plage horaire
|
||||
|
||||
Scénario: Format notification nouveau contenu complet
|
||||
Étant donné que je suis à pied
|
||||
Et qu'un créateur suivi publie un contenu
|
||||
Quand je reçois la notification push
|
||||
Alors elle contient:
|
||||
| élément | exemple |
|
||||
| Emoji | 🎧 |
|
||||
| Créateur | JeanDupont |
|
||||
| Action | a publié |
|
||||
| Titre | "Les secrets du Louvre" |
|
||||
| CTA | Tap pour écouter |
|
||||
|
||||
Scénario: Format notification live complet
|
||||
Étant donné que je suis à pied
|
||||
Et qu'un créateur suivi démarre un live
|
||||
Quand je reçois la notification push
|
||||
Alors elle contient:
|
||||
| élément | exemple |
|
||||
| Emoji | 🔴 |
|
||||
| Créateur | RadioLive |
|
||||
| Action | est en direct |
|
||||
| Titre | "Débat politique ce soir" |
|
||||
| CTA | Tap pour rejoindre |
|
||||
|
||||
Scénario: Notification disparaît si contenu supprimé
|
||||
Étant donné que j'ai reçu une notification pour un contenu
|
||||
Et que je n'ai pas encore tapé dessus
|
||||
Quand le créateur supprime le contenu
|
||||
Alors la notification est automatiquement retirée de mon centre de notifications
|
||||
Et si je tape dessus par erreur, je vois "Contenu non disponible"
|
||||
|
||||
Scénario: Badge compteur in-app en mode voiture
|
||||
Étant donné que je suis en voiture
|
||||
Et que 5 créateurs suivis publient des contenus
|
||||
Quand j'ouvre l'application
|
||||
Alors je vois un badge "5" sur l'onglet "Nouveautés"
|
||||
Et en consultant l'onglet, je vois les 5 nouveaux contenus
|
||||
Et le badge disparaît après consultation
|
||||
|
||||
Scénario: Coût des notifications push Firebase
|
||||
Étant donné que je reçois 10 notifications push par jour
|
||||
Et que je suis actif 365 jours par an
|
||||
Quand le système calcule le coût
|
||||
Alors 3650 notifications/an sont envoyées
|
||||
Et Firebase Cloud Messaging est gratuit jusqu'à plusieurs millions de notifications
|
||||
Et le coût reste 0€ pour le volume MVP/Growth
|
||||
|
||||
Scénario: Deep link depuis notification push
|
||||
Étant donné que je reçois une notification push pour un contenu
|
||||
Quand je tape sur la notification
|
||||
Alors l'app s'ouvre directement sur le contenu
|
||||
Et la lecture démarre automatiquement (si j'étais à pied)
|
||||
Ou le contenu est ajouté en première position dans la file (si je suis en voiture)
|
||||
|
||||
Scénario: Notification refusée si permissions désactivées au niveau OS
|
||||
Étant donné que j'ai désactivé les notifications dans les paramètres iOS/Android
|
||||
Quand un créateur suivi publie un contenu
|
||||
Alors aucune notification push n'est envoyée
|
||||
Et l'app propose de réactiver les permissions dans les paramètres
|
||||
Mais les badges in-app continuent de fonctionner
|
||||
@@ -0,0 +1,242 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Gestion des contenus supprimés pendant offline
|
||||
En tant qu'utilisateur offline
|
||||
Je veux être informé des contenus supprimés à la reconnexion
|
||||
Afin de comprendre pourquoi certains contenus ont disparu
|
||||
|
||||
Contexte:
|
||||
Étant donné qu'un utilisateur a téléchargé 50 contenus offline
|
||||
Et que l'utilisateur part offline pendant 7 jours
|
||||
|
||||
# Processus de synchronisation
|
||||
|
||||
Scénario: Reconnexion WiFi - Validation des contenus locaux
|
||||
Étant donné que l'utilisateur se reconnecte au WiFi
|
||||
Quand l'application détecte la connexion internet
|
||||
Alors une requête API est envoyée: GET /offline/validate
|
||||
Et l'API retourne:
|
||||
"""json
|
||||
{
|
||||
"valid_ids": [1, 2, 3, 5, 7, 8, ...],
|
||||
"deleted_ids": [4, 6, 10],
|
||||
"metadata_updates": [
|
||||
{"id": 9, "new_title": "Nouveau titre", "creator": "CreateurA"}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: Traitement réponse validation - Suppression fichiers locaux
|
||||
Étant donné que l'API retourne deleted_ids = [4, 6, 10]
|
||||
Quand l'application traite la réponse
|
||||
Alors les fichiers locaux des contenus 4, 6, 10 sont supprimés immédiatement
|
||||
Et les métadonnées locales SQLite sont mises à jour
|
||||
Et un compteur de suppressions est incrémenté
|
||||
|
||||
Scénario: Renouvellement validité contenus valides
|
||||
Étant donné que l'API retourne valid_ids = [1, 2, 3, 5, 7, 8, ...]
|
||||
Quand l'application traite la réponse
|
||||
Alors la validité de ces contenus est renouvelée pour 30 jours
|
||||
Et la date d'expiration est mise à jour: today + 30 jours
|
||||
|
||||
Scénario: Mise à jour métadonnées modifiées
|
||||
Étant donné que l'API retourne metadata_updates avec un nouveau titre
|
||||
Quand l'application traite la réponse
|
||||
Alors la base SQLite locale est mise à jour:
|
||||
| content_id | nouveau_titre | nouveau_creator |
|
||||
| 9 | Nouveau titre | CreateurA |
|
||||
Et les fichiers audio restent inchangés
|
||||
|
||||
# Gestion contenu en cours d'écoute
|
||||
|
||||
Scénario: Contenu supprimé en cours de lecture - Arrêt immédiat
|
||||
Étant donné que l'utilisateur écoute le contenu 4 (à 1:30 sur 3:00)
|
||||
Et que l'API retourne deleted_ids = [4]
|
||||
Quand la synchronisation détecte que contenu 4 est supprimé
|
||||
Alors la lecture s'arrête immédiatement
|
||||
Et une modal s'affiche:
|
||||
"""
|
||||
Contenu supprimé
|
||||
|
||||
Ce contenu n'est plus disponible
|
||||
et a été retiré par le créateur.
|
||||
|
||||
Passage au contenu suivant...
|
||||
|
||||
[OK]
|
||||
"""
|
||||
Et le fichier audio est supprimé localement
|
||||
Et après 2 secondes, le contenu suivant démarre
|
||||
|
||||
Scénario: Contenu supprimé PAS en cours - Suppression silencieuse
|
||||
Étant donné que l'utilisateur écoute le contenu 1
|
||||
Et que l'API retourne deleted_ids = [4, 6, 10]
|
||||
Quand la synchronisation traite les suppressions
|
||||
Alors les fichiers 4, 6, 10 sont supprimés silencieusement
|
||||
Et aucune interruption de lecture ne se produit
|
||||
Et un toast récapitulatif est affiché à la fin de la lecture actuelle
|
||||
|
||||
# Message récapitulatif
|
||||
|
||||
Scénario: Plusieurs contenus supprimés - Popup récapitulative
|
||||
Étant donné que 3 contenus ont été supprimés (4, 6, 10)
|
||||
Quand la synchronisation est terminée
|
||||
Alors une popup s'affiche:
|
||||
"""
|
||||
Contenus supprimés
|
||||
|
||||
3 contenus téléchargés ne sont plus
|
||||
disponibles et ont été retirés.
|
||||
|
||||
Les créateurs peuvent supprimer ou
|
||||
modifier leurs contenus à tout moment.
|
||||
|
||||
[Voir la liste] [OK]
|
||||
"""
|
||||
|
||||
Scénario: Bouton "Voir la liste" - Affichage détails
|
||||
Étant donné que la popup récapitulative est affichée
|
||||
Quand l'utilisateur clique sur [Voir la liste]
|
||||
Alors une nouvelle vue s'affiche avec:
|
||||
| Contenu | Créateur | Raison |
|
||||
| Podcast Auto Episode 4| CreateurA | Retiré créateur |
|
||||
| Musique Classique | CreateurB | Modération |
|
||||
| Audio-guide Paris | CreateurC | Retiré créateur |
|
||||
Et l'historique est conservé 7 jours puis purgé
|
||||
|
||||
Scénario: Bouton "OK" - Fermeture popup
|
||||
Étant donné que la popup récapitulative est affichée
|
||||
Quand l'utilisateur clique sur [OK]
|
||||
Alors la popup se ferme
|
||||
Et l'utilisateur continue normalement
|
||||
Et l'historique des suppressions reste accessible pendant 7 jours
|
||||
|
||||
# Toast notification
|
||||
|
||||
Scénario: Toast simple si 1 seul contenu supprimé
|
||||
Étant donné qu'un seul contenu a été supprimé (4)
|
||||
Quand la synchronisation est terminée
|
||||
Alors un toast s'affiche pendant 5 secondes:
|
||||
"""
|
||||
1 contenu supprimé a été retiré
|
||||
"""
|
||||
Et aucune popup n'est affichée (toast suffit)
|
||||
|
||||
Scénario: Popup si 2+ contenus supprimés
|
||||
Étant donné que 2 ou plus contenus ont été supprimés
|
||||
Quand la synchronisation est terminée
|
||||
Alors une popup complète est affichée (pas juste un toast)
|
||||
Car l'utilisateur doit être informé clairement
|
||||
|
||||
# Motifs de suppression
|
||||
|
||||
Scénario: Contenu supprimé par créateur volontairement
|
||||
Étant donné que le créateur a supprimé le contenu 4 volontairement
|
||||
Quand l'API retourne deleted_ids = [4] avec motif "creator_deleted"
|
||||
Alors le contenu est retiré immédiatement (pas de grace period MVP)
|
||||
Et la raison affichée est: "Retiré par le créateur"
|
||||
|
||||
Scénario: Contenu supprimé par modération (violation CGU)
|
||||
Étant donné que le contenu 6 a été modéré pour violation CGU
|
||||
Quand l'API retourne deleted_ids = [6] avec motif "moderation"
|
||||
Alors le contenu est retiré immédiatement
|
||||
Et la raison affichée est: "Retiré pour modération"
|
||||
|
||||
Scénario: Contenu passé en Premium (créateur change statut)
|
||||
Étant donné que le contenu 10 est passé en "Premium exclusif"
|
||||
Et que l'utilisateur est gratuit
|
||||
Quand l'API retourne deleted_ids = [10] avec motif "premium_only"
|
||||
Alors le contenu est retiré immédiatement
|
||||
Et la raison affichée est: "Réservé aux abonnés Premium"
|
||||
|
||||
# Justification KISS
|
||||
|
||||
Scénario: Comparaison avec grace period - Simplicité MVP
|
||||
Étant donné qu'on compare la suppression immédiate vs grace period
|
||||
Quand on évalue les avantages KISS
|
||||
Alors les avantages suppression immédiate sont:
|
||||
| avantage | description |
|
||||
| Simplicité technique | Pas de gestion états intermédiaires complexes |
|
||||
| Respect créateur | Volonté immédiate respectée |
|
||||
| Conformité légale | Contenu illégal retiré immédiatement |
|
||||
| Cas rare | Peu de créateurs suppriment contenus après publi |
|
||||
|
||||
# Post-MVP : Grace period (si feedback négatifs)
|
||||
|
||||
Scénario: Post-MVP - Grace period 7 jours pour suppression créateur volontaire
|
||||
Étant donné qu'on implémente la grace period post-MVP
|
||||
Et que le contenu 4 est supprimé volontairement par le créateur
|
||||
Quand l'API retourne deleted_ids = [4] avec motif "creator_deleted"
|
||||
Alors le contenu est marqué "Bientôt retiré" avec badge
|
||||
Et l'utilisateur peut encore l'écouter pendant 7 jours
|
||||
Et après 7 jours, le contenu est supprimé
|
||||
|
||||
Scénario: Post-MVP - Pas de grace period pour modération
|
||||
Étant donné qu'on implémente la grace period post-MVP
|
||||
Et que le contenu 6 est modéré (illégal, violation CGU)
|
||||
Quand l'API retourne deleted_ids = [6] avec motif "moderation"
|
||||
Alors le contenu est supprimé immédiatement (pas de grace period)
|
||||
Car sécurité/légalité prime
|
||||
|
||||
Scénario: Post-MVP - Grace period si user Premium et contenu devient Premium
|
||||
Étant donné qu'on implémente la grace period post-MVP
|
||||
Et que le contenu 10 passe en "Premium exclusif"
|
||||
Et que l'utilisateur a un abonnement Premium
|
||||
Quand l'API retourne deleted_ids = [10] avec motif "premium_only"
|
||||
Alors l'utilisateur Premium conserve l'accès (pas supprimé)
|
||||
Mais l'utilisateur gratuit perd l'accès après 7 jours
|
||||
|
||||
# Cas limites
|
||||
|
||||
Scénario: Tous les contenus offline supprimés - Message spécifique
|
||||
Étant donné que l'utilisateur avait 5 contenus offline
|
||||
Et que les 5 contenus ont été supprimés pendant offline
|
||||
Quand la synchronisation retourne deleted_ids = [1, 2, 3, 4, 5]
|
||||
Alors une popup spécifique s'affiche:
|
||||
"""
|
||||
Tous vos contenus offline ont été supprimés
|
||||
|
||||
Les 5 contenus téléchargés ne sont plus disponibles.
|
||||
|
||||
Téléchargez de nouveaux contenus pour écouter offline.
|
||||
|
||||
[Parcourir contenus] [OK]
|
||||
"""
|
||||
|
||||
Scénario: Contenu supprimé puis utilisateur re-télécharge
|
||||
Étant donné que le contenu 4 a été supprimé pendant offline
|
||||
Et que l'utilisateur a vu la notification
|
||||
Quand l'utilisateur se reconnecte et browse les contenus
|
||||
Alors le contenu 4 n'apparaît plus dans la liste
|
||||
Et l'utilisateur ne peut pas le re-télécharger (supprimé définitivement)
|
||||
|
||||
Scénario: Historique suppressions conservé 7 jours
|
||||
Étant donné que 3 contenus ont été supprimés le 1er février
|
||||
Quand l'utilisateur consulte l'historique des suppressions le 7 février
|
||||
Alors l'historique est toujours visible
|
||||
Quand l'utilisateur consulte l'historique le 9 février (7+ jours)
|
||||
Alors l'historique a été purgé automatiquement
|
||||
|
||||
# Statistiques côté API
|
||||
|
||||
Scénario: API - Comptabilisation contenus supprimés pendant offline
|
||||
Étant donné que 1000 utilisateurs étaient offline pendant 7 jours
|
||||
Et que 150 contenus ont été supprimés pendant cette période
|
||||
Quand l'API génère les métriques de synchronisation
|
||||
Alors les statistiques affichent:
|
||||
| métrique | valeur |
|
||||
| Utilisateurs reconnectés | 1000 |
|
||||
| Contenus supprimés détectés | 4500 |
|
||||
| Moyenne contenus supprimés/user | 4.5 |
|
||||
| Users affectés (≥1 suppression) | 850 |
|
||||
|
||||
# Performance
|
||||
|
||||
Scénario: Requête GET /offline/validate - Performance optimisée
|
||||
Étant donné qu'un utilisateur a 50 contenus offline
|
||||
Quand l'API reçoit GET /offline/validate avec la liste des 50 IDs
|
||||
Alors la requête SQL vérifie l'existence des contenus en batch:
|
||||
"""sql
|
||||
SELECT id FROM contents WHERE id IN (1,2,3,...,50) AND deleted_at IS NULL
|
||||
"""
|
||||
Et la réponse est générée en <200ms
|
||||
Et Redis cache les résultats pour éviter requêtes répétées
|
||||
@@ -0,0 +1,425 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Synchronisation actions offline
|
||||
En tant qu'utilisateur
|
||||
Je veux que mes actions offline soient synchronisées quand je me reconnecte
|
||||
Afin de ne perdre aucune interaction même sans connexion
|
||||
|
||||
Contexte:
|
||||
Étant donné que j'utilise l'application RoadWave
|
||||
|
||||
# ===== ACTIONS STOCKÉES LOCALEMENT =====
|
||||
|
||||
Scénario: Like d'un contenu en mode offline
|
||||
Étant donné que je n'ai aucune connexion Internet
|
||||
Quand je like un contenu téléchargé
|
||||
Alors l'action est enregistrée localement dans SQLite:
|
||||
```sql
|
||||
INSERT INTO pending_actions (type, content_id, created_at)
|
||||
VALUES ('like', 'abc123', '2025-06-15 14:30:00');
|
||||
```
|
||||
Et l'UI affiche immédiatement le like (optimistic update)
|
||||
|
||||
Scénario: Unlike d'un contenu en mode offline
|
||||
Étant donné que je n'ai aucune connexion Internet
|
||||
Et que j'avais liké un contenu
|
||||
Quand je retire mon like
|
||||
Alors l'action est enregistrée localement:
|
||||
```sql
|
||||
INSERT INTO pending_actions (type, content_id, created_at)
|
||||
VALUES ('unlike', 'abc123', '2025-06-15 14:35:00');
|
||||
```
|
||||
Et l'UI retire immédiatement le like
|
||||
|
||||
Scénario: Abonnement à un créateur en mode offline
|
||||
Étant donné que je n'ai aucune connexion Internet
|
||||
Quand je m'abonne à un créateur
|
||||
Alors l'action est enregistrée localement:
|
||||
```sql
|
||||
INSERT INTO pending_actions (type, creator_id, created_at)
|
||||
VALUES ('subscribe', 'creator456', '2025-06-15 14:40:00');
|
||||
```
|
||||
Et l'UI affiche immédiatement "Abonné ✓"
|
||||
|
||||
Scénario: Désabonnement d'un créateur en mode offline
|
||||
Étant donné que je n'ai aucune connexion Internet
|
||||
Et que j'étais abonné à un créateur
|
||||
Quand je me désabonne
|
||||
Alors l'action est enregistrée localement:
|
||||
```sql
|
||||
INSERT INTO pending_actions (type, creator_id, created_at)
|
||||
VALUES ('unsubscribe', 'creator456', '2025-06-15 14:45:00');
|
||||
```
|
||||
Et l'UI affiche "S'abonner"
|
||||
|
||||
Scénario: Signalement d'un contenu en mode offline
|
||||
Étant donné que je n'ai aucune connexion Internet
|
||||
Quand je signale un contenu pour "Contenu inapproprié"
|
||||
Alors l'action est enregistrée localement:
|
||||
```sql
|
||||
INSERT INTO pending_actions (type, content_id, reason, created_at)
|
||||
VALUES ('report', 'abc123', 'Contenu inapproprié', '2025-06-15 14:50:00');
|
||||
```
|
||||
Et je vois "Signalement enregistré. Sera envoyé à la reconnexion."
|
||||
|
||||
Scénario: Progression audio-guide en mode offline
|
||||
Étant donné que je n'ai aucune connexion Internet
|
||||
Et que j'écoute un audio-guide multi-séquences
|
||||
Quand je termine la séquence 3/10
|
||||
Alors la progression est enregistrée localement:
|
||||
```sql
|
||||
INSERT INTO pending_actions (type, guide_id, sequence_id, created_at)
|
||||
VALUES ('guide_progress', 'guide789', 'seq003', '2025-06-15 15:00:00');
|
||||
```
|
||||
Et ma progression est sauvegardée
|
||||
|
||||
Scénario: Multiple actions offline stockées en queue
|
||||
Étant donné que je n'ai aucune connexion Internet pendant 2 jours
|
||||
Quand j'effectue plusieurs actions:
|
||||
| action | cible |
|
||||
| like | contenu A |
|
||||
| like | contenu B |
|
||||
| subscribe | créateur X |
|
||||
| unlike | contenu C |
|
||||
| report | contenu D |
|
||||
Alors les 5 actions sont stockées dans pending_actions
|
||||
Et elles seront synchronisées dans l'ordre à la reconnexion
|
||||
|
||||
# ===== SYNCHRONISATION AUTOMATIQUE =====
|
||||
|
||||
Scénario: Détection reconnexion Internet
|
||||
Étant donné que j'étais en mode offline
|
||||
Quand l'app détecte une reconnexion Internet
|
||||
Alors le processus de synchronisation démarre automatiquement
|
||||
Et je vois une notification "Synchronisation en cours..."
|
||||
|
||||
Scénario: Récupération queue locale pendant sync
|
||||
Étant donné que la synchronisation démarre
|
||||
Quand l'app récupère les actions en attente
|
||||
Alors une requête SQL est exécutée:
|
||||
```sql
|
||||
SELECT * FROM pending_actions ORDER BY created_at ASC;
|
||||
```
|
||||
Et toutes les actions sont récupérées dans l'ordre chronologique
|
||||
|
||||
Scénario: Envoi batch API des actions
|
||||
Étant donné que 15 actions sont en attente
|
||||
Quand le batch est envoyé au backend
|
||||
Alors une requête POST /sync/actions est faite:
|
||||
```json
|
||||
{
|
||||
"actions": [
|
||||
{"type": "like", "content_id": "abc123", "timestamp": "2025-06-15T14:30:00Z"},
|
||||
{"type": "subscribe", "creator_id": "creator456", "timestamp": "2025-06-15T14:40:00Z"},
|
||||
{"type": "unlike", "content_id": "def789", "timestamp": "2025-06-15T14:50:00Z"},
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
Et toutes les actions sont groupées en une seule requête
|
||||
|
||||
Scénario: Backend traite chaque action
|
||||
Étant donné que le backend reçoit le batch d'actions
|
||||
Quand il traite chaque action
|
||||
Alors pour chaque action:
|
||||
| étape | détail |
|
||||
| Validation | Vérifier user_id, content_id valides |
|
||||
| Vérification existence | Contenu/créateur existe toujours ? |
|
||||
| Application action | INSERT/UPDATE/DELETE en base |
|
||||
| Mise à jour compteurs | Likes, abonnés, etc. |
|
||||
| Impact sur algorithme | Mise à jour jauges si nécessaire |
|
||||
|
||||
Scénario: Confirmation réception et suppression queue locale
|
||||
Étant donné que le backend a traité toutes les actions avec succès
|
||||
Quand la confirmation est reçue par l'app
|
||||
Alors les actions sont supprimées de la queue locale:
|
||||
```sql
|
||||
DELETE FROM pending_actions WHERE id IN (1, 2, 3, ..., 15);
|
||||
```
|
||||
Et la table pending_actions est vidée
|
||||
|
||||
Scénario: Toast confirmation synchronisation
|
||||
Étant donné que 15 actions ont été synchronisées
|
||||
Quand la synchronisation se termine
|
||||
Alors je vois un toast:
|
||||
"""
|
||||
✅ Synchronisation réussie
|
||||
|
||||
3 likes, 1 abonnement et 1 signalement synchronisés.
|
||||
"""
|
||||
|
||||
Scénario: Synchronisation silencieuse si peu d'actions
|
||||
Étant donné que j'ai seulement 2 actions en attente
|
||||
Quand la synchronisation se termine
|
||||
Alors aucun toast n'est affiché (sync silencieuse)
|
||||
Et l'expérience reste fluide
|
||||
Mais je peux voir le détail dans l'historique des syncs
|
||||
|
||||
# ===== GESTION ERREURS SYNC =====
|
||||
|
||||
Scénario: Échec synchronisation - Retry automatique
|
||||
Étant donné que la synchronisation échoue (erreur réseau)
|
||||
Quand l'échec est détecté
|
||||
Alors un retry automatique est programmé dans 30 secondes
|
||||
Et les actions restent dans pending_actions
|
||||
|
||||
Scénario: 3 tentatives échouées - Notification utilisateur
|
||||
Étant donné que 3 tentatives de synchronisation ont échoué
|
||||
Quand la 3ème tentative échoue
|
||||
Alors je reçois une notification:
|
||||
"""
|
||||
⚠️ Impossible de synchroniser vos actions
|
||||
|
||||
15 actions en attente de synchronisation.
|
||||
Vérifiez votre connexion et réessayez.
|
||||
|
||||
[Réessayer maintenant] [Plus tard]
|
||||
```
|
||||
|
||||
Scénario: Actions conservées jusqu'à sync réussie
|
||||
Étant donné que la synchronisation échoue plusieurs fois
|
||||
Quand les tentatives continuent d'échouer
|
||||
Alors les actions restent dans pending_actions
|
||||
Et aucune action n'est perdue
|
||||
Et elles seront envoyées dès que la connexion sera stable
|
||||
|
||||
Scénario: Rétention max 7 jours - Purge automatique
|
||||
Étant donné qu'une action est en attente depuis 7 jours
|
||||
Quand le système détecte cette ancienneté
|
||||
Alors l'action est automatiquement supprimée de la queue
|
||||
Et je vois "1 action trop ancienne supprimée (>7 jours)"
|
||||
Et cela évite une queue infinie
|
||||
|
||||
Scénario: Justification rétention 7 jours
|
||||
Étant donné qu'un utilisateur ne se connecte jamais pendant 2 semaines
|
||||
Quand ses actions ont >7 jours
|
||||
Alors elles sont purgées automatiquement
|
||||
Car après 7 jours, l'action perd sa pertinence
|
||||
Et évite une queue qui grandit indéfiniment
|
||||
|
||||
Scénario: Retry manuel après échec
|
||||
Étant donné que la synchronisation a échoué
|
||||
Quand je clique sur "Réessayer maintenant"
|
||||
Alors une nouvelle tentative de synchronisation est lancée immédiatement
|
||||
Et si elle réussit, les actions sont synchronisées
|
||||
|
||||
# ===== CONFLITS CONTENUS SUPPRIMÉS =====
|
||||
|
||||
Scénario: Backend retourne contenus supprimés
|
||||
Étant donné que j'ai liké un contenu offline
|
||||
Mais que le contenu a été supprimé entre temps
|
||||
Quand le backend traite la synchronisation
|
||||
Alors il retourne:
|
||||
```json
|
||||
{
|
||||
"status": "partial_success",
|
||||
"deleted_content_ids": [123, 456],
|
||||
"failed_actions": [
|
||||
{"type": "like", "content_id": "123", "reason": "content_deleted"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Scénario: App supprime fichiers locaux contenus supprimés
|
||||
Étant donné que le backend retourne deleted_content_ids: [123, 456]
|
||||
Quand l'app traite la réponse
|
||||
Alors elle supprime les fichiers locaux des contenus 123 et 456
|
||||
Et libère l'espace disque
|
||||
Et les actions associées sont retirées de la queue
|
||||
|
||||
Scénario: Contenu supprimé en cours d'écoute
|
||||
Étant donné que j'écoute le contenu 123 en offline
|
||||
Et que la sync détecte que le contenu a été supprimé
|
||||
Quand la lecture actuelle se termine
|
||||
Alors l'app attend 2 secondes
|
||||
Et passe automatiquement au contenu suivant
|
||||
Et le fichier du contenu 123 est supprimé en arrière-plan
|
||||
|
||||
Scénario: Toast notification contenu retiré
|
||||
Étant donné que 2 contenus téléchargés ont été supprimés
|
||||
Quand la synchronisation se termine
|
||||
Alors je vois un toast:
|
||||
"""
|
||||
🗑️ 2 contenus téléchargés ont été retirés
|
||||
|
||||
Raison: Violation des règles de la plateforme
|
||||
"""
|
||||
|
||||
Scénario: Contenu modéré après téléchargement
|
||||
Étant donné que j'ai téléchargé un contenu qui est ensuite modéré
|
||||
Quand la synchronisation détecte la modération
|
||||
Alors le contenu est immédiatement supprimé du device
|
||||
Et je ne peux plus l'écouter
|
||||
Et cela garantit la conformité même offline
|
||||
|
||||
# ===== JUSTIFICATIONS =====
|
||||
|
||||
Scénario: Justification pas de conflit possible
|
||||
Étant donné que les actions offline sont unilatérales (likes, abonnements)
|
||||
Quand elles sont synchronisées
|
||||
Alors il n'y a pas de conflit de version possible
|
||||
Car l'utilisateur ajoute/retire simplement des préférences
|
||||
Et pas de merge complexe nécessaire
|
||||
|
||||
Scénario: Justification UX fluide offline
|
||||
Étant donné que toutes les actions fonctionnent offline
|
||||
Quand l'utilisateur interagit sans connexion
|
||||
Alors l'expérience est identique au mode online
|
||||
Et l'utilisateur n'est pas bloqué
|
||||
Et peut utiliser l'app normalement
|
||||
|
||||
Scénario: Justification batch = Économie requêtes
|
||||
Étant donné que 15 actions sont en attente
|
||||
Quand elles sont synchronisées en batch
|
||||
Alors 1 seule requête HTTP est envoyée (vs 15 si individuelles)
|
||||
Et cela économise la bande passante et la batterie
|
||||
Et réduit la charge serveur
|
||||
|
||||
Scénario: Justification conformité modération offline
|
||||
Étant donné qu'un contenu illégal est modéré pendant qu'un user est offline
|
||||
Quand le user se reconnecte
|
||||
Alors le contenu est immédiatement supprimé de son device
|
||||
Et cela garantit que les contenus illégaux disparaissent même offline
|
||||
|
||||
# ===== STATISTIQUES ET MONITORING =====
|
||||
|
||||
Scénario: Historique synchronisations
|
||||
Étant donné que j'accède à "Paramètres > Synchronisation"
|
||||
Quand je consulte l'historique
|
||||
Alors je vois:
|
||||
| date | actions sync | statut |
|
||||
| 15/06/2025 14:30:00 | 15 | Réussi ✅ |
|
||||
| 14/06/2025 09:15:00 | 7 | Réussi ✅ |
|
||||
| 13/06/2025 18:45:00 | 3 | Échec ❌ |
|
||||
|
||||
Scénario: Détail d'une synchronisation
|
||||
Étant donné que je clique sur une ligne de l'historique
|
||||
Quand le détail s'affiche
|
||||
Alors je vois:
|
||||
```
|
||||
Synchronisation du 15/06/2025 14:30:00
|
||||
|
||||
Actions synchronisées:
|
||||
• 3 likes
|
||||
• 1 abonnement
|
||||
• 1 signalement
|
||||
• 10 progressions audio-guides
|
||||
|
||||
Durée: 1.2s
|
||||
Statut: Réussi ✅
|
||||
```
|
||||
|
||||
Scénario: Compteur actions en attente visible
|
||||
Étant donné que j'ai 12 actions en attente de synchronisation
|
||||
Quand j'accède à l'onglet Profil
|
||||
Alors je vois un badge "12" sur l'icône de synchronisation
|
||||
Et je sais qu'il y a des actions en attente
|
||||
|
||||
Scénario: Synchronisation manuelle forcée
|
||||
Étant donné que je veux forcer une synchronisation immédiate
|
||||
Quand je vais dans "Paramètres > Synchronisation"
|
||||
Et que je clique sur "Synchroniser maintenant"
|
||||
Alors la synchronisation démarre immédiatement
|
||||
Et toutes les actions en attente sont envoyées
|
||||
|
||||
Scénario: Statistiques utilisateur - Syncs effectuées
|
||||
Étant donné que j'accède à mes statistiques
|
||||
Quand je consulte la section Synchronisation
|
||||
Alors je vois:
|
||||
| métrique | valeur |
|
||||
| Synchronisations depuis début | 87 |
|
||||
| Actions synchronisées total | 1,234 |
|
||||
| Taux de succès | 94% |
|
||||
| Dernière sync | Il y a 2h|
|
||||
|
||||
Scénario: Statistiques admin - Volume synchronisations
|
||||
Étant donné qu'un admin consulte les métriques de synchronisation
|
||||
Quand il accède au dashboard
|
||||
Alors il voit:
|
||||
| métrique | valeur |
|
||||
| Synchronisations/jour | 45,678 |
|
||||
| Actions synchronisées/jour | 234,567 |
|
||||
| Taux succès sync | 96.5% |
|
||||
| Temps moyen traitement batch | 0.8s |
|
||||
| Actions en attente (global) | 12,345 |
|
||||
|
||||
Scénario: Alerte admin si taux échec sync >10%
|
||||
Étant donné que le taux d'échec sync dépasse 10%
|
||||
Quand le système détecte cette anomalie
|
||||
Alors une alerte est envoyée:
|
||||
"""
|
||||
⚠️ Taux échec synchronisation anormal: 12.3%
|
||||
|
||||
Échecs aujourd'hui: 5,621 / 45,678 syncs
|
||||
Causes principales:
|
||||
- Timeout serveur: 3,245
|
||||
- Erreur réseau client: 1,876
|
||||
- Données invalides: 500
|
||||
|
||||
Action recommandée: Vérifier charge serveur + logs erreurs
|
||||
"""
|
||||
|
||||
# ===== TESTS PERFORMANCE =====
|
||||
|
||||
Scénario: Synchronisation rapide <2s
|
||||
Étant donné que j'ai 20 actions en attente
|
||||
Quand la synchronisation démarre
|
||||
Alors le traitement prend <2 secondes
|
||||
Et je ne remarque aucun ralentissement de l'app
|
||||
|
||||
Scénario: Synchronisation de gros batch (100 actions)
|
||||
Étant donné que je n'ai pas synchronisé pendant 1 semaine
|
||||
Et que j'ai 100 actions en attente
|
||||
Quand la synchronisation démarre
|
||||
Alors le batch de 100 actions est traité en <5 secondes
|
||||
Et toutes les actions sont synchronisées avec succès
|
||||
|
||||
Scénario: Gestion charge serveur - 10 000 syncs simultanées
|
||||
Étant donné que 10 000 utilisateurs se reconnectent simultanément
|
||||
Quand chacun envoie un batch de 20 actions
|
||||
Alors le serveur traite 200 000 actions
|
||||
Et grâce au traitement asynchrone (queue Redis), le temps de réponse reste <3s
|
||||
Et aucun timeout n'est constaté
|
||||
|
||||
Scénario: Stockage SQLite optimisé
|
||||
Étant donné que la table pending_actions stocke des centaines d'actions
|
||||
Quand des requêtes sont exécutées
|
||||
Alors la table est indexée sur created_at
|
||||
Et les requêtes SELECT et DELETE sont instantanées (<10ms)
|
||||
Et l'expérience utilisateur reste fluide
|
||||
|
||||
Scénario: Nettoyage automatique table pending_actions
|
||||
Étant donné que la table pending_actions grossit avec le temps
|
||||
Quand les actions sont synchronisées et supprimées
|
||||
Alors la table est automatiquement optimisée (VACUUM sur SQLite)
|
||||
Et l'espace disque est libéré
|
||||
Et les performances restent optimales
|
||||
|
||||
# ===== EDGE CASES =====
|
||||
|
||||
Scénario: Action dupliquée - Idempotence
|
||||
Étant donné que j'ai liké un contenu offline
|
||||
Et que la sync échoue et retry
|
||||
Quand le backend reçoit 2 fois le même like
|
||||
Alors il applique l'idempotence (1 seul like enregistré)
|
||||
Et le compteur de likes n'est pas faussé
|
||||
|
||||
Scénario: Séquence like/unlike offline
|
||||
Étant donné que j'ai liké puis unliké un contenu offline
|
||||
Quand les 2 actions sont synchronisées
|
||||
Alors le backend applique les 2 actions dans l'ordre
|
||||
Et le résultat final est "pas de like" (état correct)
|
||||
|
||||
Scénario: Abonnement puis désabonnement offline
|
||||
Étant donné que je me suis abonné puis désabonné d'un créateur offline
|
||||
Quand les 2 actions sont synchronisées
|
||||
Alors le backend applique les 2 actions dans l'ordre
|
||||
Et le résultat final est "pas abonné"
|
||||
Et les jauges évoluent correctement (+5% puis -5% = 0% net)
|
||||
|
||||
Scénario: Créateur supprimé pendant offline
|
||||
Étant donné que je me suis abonné à un créateur offline
|
||||
Mais que le créateur a supprimé son compte entre temps
|
||||
Quand la sync traite l'abonnement
|
||||
Alors le backend retourne "creator_deleted"
|
||||
Et l'action est ignorée silencieusement
|
||||
Et aucune erreur n'est affichée à l'utilisateur
|
||||
@@ -0,0 +1,409 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Téléchargement de contenus offline
|
||||
En tant qu'utilisateur
|
||||
Je veux télécharger des contenus pour les écouter sans connexion
|
||||
Afin de profiter de RoadWave même dans les zones sans réseau
|
||||
|
||||
Contexte:
|
||||
Étant donné que je suis connecté à l'application RoadWave
|
||||
|
||||
# ===== SÉLECTION ZONE GÉOGRAPHIQUE =====
|
||||
|
||||
Scénario: Option "Autour de moi" - Rayon 50 km
|
||||
Étant donné que je suis à Paris (position GPS détectée)
|
||||
Quand je sélectionne "Télécharger > Autour de moi"
|
||||
Alors l'app recherche tous les contenus géolocalisés dans un rayon de 50 km
|
||||
Et je vois une liste de contenus de Paris et banlieue proche
|
||||
Et l'estimation affiche "~150 contenus disponibles"
|
||||
|
||||
Scénario: Option "Ma ville" - Limite administrative détectée
|
||||
Étant donné que je suis à Lyon (position GPS détectée)
|
||||
Quand je sélectionne "Télécharger > Ma ville"
|
||||
Alors l'app détecte automatiquement "Lyon" comme ville
|
||||
Et recherche tous les contenus géolocalisés "Lyon"
|
||||
Et je vois uniquement les contenus de la ville de Lyon (pas banlieue)
|
||||
|
||||
Scénario: Option "Mon département" - Sélection dans liste
|
||||
Étant donné que je veux télécharger des contenus pour un département
|
||||
Quand je sélectionne "Télécharger > Mon département"
|
||||
Alors je vois une liste de tous les départements français:
|
||||
| département |
|
||||
| 01 - Ain |
|
||||
| 02 - Aisne |
|
||||
| 75 - Paris |
|
||||
| 69 - Rhône |
|
||||
| ... |
|
||||
Et je peux choisir un département
|
||||
|
||||
Scénario: Sélection département et téléchargement contenus
|
||||
Étant donné que je sélectionne "75 - Paris" dans la liste des départements
|
||||
Quand la sélection est confirmée
|
||||
Alors l'app recherche tous les contenus géolocalisés "Paris"
|
||||
Et je vois "~234 contenus disponibles pour Paris"
|
||||
|
||||
Scénario: Option "Ma région" - Sélection dans liste
|
||||
Étant donné que je veux télécharger des contenus pour une région
|
||||
Quand je sélectionne "Télécharger > Ma région"
|
||||
Alors je vois une liste de toutes les régions françaises:
|
||||
| région |
|
||||
| Auvergne-Rhône-Alpes |
|
||||
| Bretagne |
|
||||
| Île-de-France |
|
||||
| Nouvelle-Aquitaine |
|
||||
| Occitanie |
|
||||
| ... |
|
||||
Et je peux choisir une région
|
||||
|
||||
Scénario: Sélection région et téléchargement contenus
|
||||
Étant donné que je sélectionne "Bretagne" dans la liste des régions
|
||||
Quand la sélection est confirmée
|
||||
Alors l'app recherche tous les contenus géolocalisés des départements bretons:
|
||||
| département |
|
||||
| Côtes-d'Armor (22) |
|
||||
| Finistère (29) |
|
||||
| Ille-et-Vilaine (35) |
|
||||
| Morbihan (56) |
|
||||
Et je vois "~487 contenus disponibles pour Bretagne"
|
||||
|
||||
Scénario: Recherche manuelle ville
|
||||
Étant donné que je veux télécharger des contenus pour une ville spécifique
|
||||
Quand je tape "Marseille" dans la barre de recherche
|
||||
Alors l'app propose des suggestions:
|
||||
| suggestion |
|
||||
| Marseille (13) |
|
||||
| Marseille-en-Beauvaisis |
|
||||
Et je peux sélectionner "Marseille (13)"
|
||||
|
||||
Scénario: Recherche manuelle avec autocomplétion
|
||||
Étant donné que je tape "Ly" dans la barre de recherche
|
||||
Quand l'autocomplétion s'active
|
||||
Alors je vois des suggestions:
|
||||
| suggestion |
|
||||
| Lyon (69) |
|
||||
| Lys-lez-Lannoy |
|
||||
Et je peux affiner ma recherche
|
||||
|
||||
# ===== LIMITES TÉLÉCHARGEMENT =====
|
||||
|
||||
Scénario: Utilisateur gratuit - Limite 50 contenus max
|
||||
Étant donné que je suis un utilisateur gratuit
|
||||
Et que j'ai déjà téléchargé 45 contenus
|
||||
Quand j'accède à la page Téléchargements
|
||||
Alors je vois "45 / 50 contenus téléchargés"
|
||||
Et je peux télécharger 5 contenus supplémentaires maximum
|
||||
|
||||
Scénario: Utilisateur gratuit - Tentative dépasser limite 50
|
||||
Étant donné que je suis gratuit et j'ai déjà 50 contenus téléchargés
|
||||
Quand j'essaie de télécharger un 51ème contenu
|
||||
Alors le téléchargement est refusé
|
||||
Et je vois le message:
|
||||
"""
|
||||
📥 Limite atteinte (50 contenus)
|
||||
|
||||
Vous avez atteint la limite de téléchargements gratuits.
|
||||
|
||||
Options:
|
||||
• Supprimez des contenus existants pour en télécharger de nouveaux
|
||||
• Passez Premium pour des téléchargements illimités
|
||||
|
||||
[Gérer mes téléchargements] [Découvrir Premium]
|
||||
"""
|
||||
|
||||
Scénario: Utilisateur Premium - Téléchargements illimités
|
||||
Étant donné que je suis un utilisateur Premium
|
||||
Et que j'ai déjà téléchargé 245 contenus
|
||||
Quand j'accède à la page Téléchargements
|
||||
Alors je vois "245 contenus (3.2 GB)"
|
||||
Et aucune limite n'est affichée
|
||||
Et je peux télécharger autant de contenus que je veux
|
||||
|
||||
Scénario: Limite Premium = Espace disque disponible
|
||||
Étant donné que je suis Premium
|
||||
Et que mon device a 500 MB d'espace disque disponible
|
||||
Quand j'essaie de télécharger 100 contenus (2 GB)
|
||||
Alors le téléchargement échoue après ~50 contenus (500 MB)
|
||||
Et je vois "Espace disque insuffisant. Libérez de l'espace pour continuer."
|
||||
|
||||
Scénario: Calcul temps écoute disponible gratuit
|
||||
Étant donné que je suis gratuit avec 50 contenus téléchargés
|
||||
Et que la durée moyenne d'un contenu est 5 minutes
|
||||
Quand je calcule le temps d'écoute disponible
|
||||
Alors 50 contenus × 5 min = 250 minutes = 4h10 d'écoute
|
||||
Et cela suffit pour un trajet quotidien ou road trip court
|
||||
|
||||
Scénario: Calcul temps écoute disponible Premium illimité
|
||||
Étant donné que je suis Premium avec 300 contenus téléchargés
|
||||
Et que la durée moyenne est 5 minutes
|
||||
Quand je calcule le temps d'écoute disponible
|
||||
Alors 300 contenus × 5 min = 1500 minutes = 25h d'écoute
|
||||
Et cela suffit pour un road trip de plusieurs jours
|
||||
|
||||
# ===== CONNEXION WIFI / MOBILE =====
|
||||
|
||||
Scénario: Téléchargement par défaut en WiFi uniquement
|
||||
Étant donné que je suis connecté en WiFi
|
||||
Quand je clique sur "Télécharger 20 contenus"
|
||||
Alors le téléchargement démarre immédiatement
|
||||
Et aucune popup de confirmation n'apparaît
|
||||
|
||||
Scénario: Tentative téléchargement en données mobiles - Popup confirmation
|
||||
Étant donné que je suis connecté en 4G (pas de WiFi)
|
||||
Quand je clique sur "Télécharger 20 contenus"
|
||||
Alors une popup apparaît:
|
||||
"""
|
||||
📡 Vous n'êtes pas connecté en WiFi
|
||||
|
||||
Télécharger via données mobiles consommera environ 72 MB.
|
||||
|
||||
[Attendre WiFi] [Continuer quand même]
|
||||
"""
|
||||
|
||||
Scénario: Calcul estimation consommation data mobile
|
||||
Étant donné que je veux télécharger 20 contenus
|
||||
Et que la durée moyenne est 5 minutes
|
||||
Et que la qualité Standard est 48 kbps Opus
|
||||
Quand l'estimation est calculée
|
||||
Alors consommation = 20 contenus × 5 min × 48 kbps / 8 = 72 MB
|
||||
Et ce montant est affiché dans la popup
|
||||
|
||||
Scénario: Confirmation téléchargement en données mobiles
|
||||
Étant donné que je vois la popup de confirmation données mobiles
|
||||
Quand je clique sur "Continuer quand même"
|
||||
Alors le téléchargement démarre immédiatement via 4G
|
||||
Et la consommation data est comptabilisée sur mon forfait mobile
|
||||
|
||||
Scénario: Refus téléchargement données mobiles - Attendre WiFi
|
||||
Étant donné que je vois la popup de confirmation données mobiles
|
||||
Quand je clique sur "Attendre WiFi"
|
||||
Alors les téléchargements sont mis en file d'attente
|
||||
Et ils démarreront automatiquement quand le WiFi sera détecté
|
||||
|
||||
Scénario: Détection automatique WiFi et reprise téléchargements
|
||||
Étant donné que j'ai mis 20 contenus en file d'attente (attente WiFi)
|
||||
Quand l'app détecte une connexion WiFi
|
||||
Alors les téléchargements démarrent automatiquement
|
||||
Et je reçois une notification "Téléchargements en cours via WiFi"
|
||||
|
||||
# ===== QUALITÉ AUDIO =====
|
||||
|
||||
Scénario: Qualité Standard (48 kbps) par défaut
|
||||
Étant donné que je configure mes téléchargements
|
||||
Quand j'accède aux paramètres de qualité
|
||||
Alors la qualité "Standard (48 kbps - ~20 MB/h)" est sélectionnée par défaut
|
||||
Et elle est disponible pour tous (gratuit + Premium)
|
||||
|
||||
Scénario: Qualité Basse (24 kbps) disponible pour tous
|
||||
Étant donné que j'ai peu d'espace disque disponible
|
||||
Quand je sélectionne qualité "Basse (24 kbps - ~10 MB/h)"
|
||||
Alors mes prochains téléchargements seront en 24 kbps
|
||||
Et l'espace utilisé sera divisé par 2 par rapport à Standard
|
||||
Et cette option est disponible pour gratuit + Premium
|
||||
|
||||
Scénario: Qualité Haute (64 kbps) réservée Premium
|
||||
Étant donné que je suis un utilisateur gratuit
|
||||
Quand je consulte les options de qualité
|
||||
Alors l'option "Haute (64 kbps - ~30 MB/h)" est grisée
|
||||
Et je vois "👑 Premium uniquement"
|
||||
Et je ne peux pas la sélectionner
|
||||
|
||||
Scénario: Utilisateur Premium peut choisir qualité Haute
|
||||
Étant donné que je suis un utilisateur Premium
|
||||
Quand je consulte les options de qualité
|
||||
Alors l'option "Haute (64 kbps - ~30 MB/h)" est disponible
|
||||
Et je peux la sélectionner pour mes téléchargements
|
||||
Et la qualité audio sera excellente (meilleure restitution voix et ambiances)
|
||||
|
||||
Scénario: Comparaison taille fichiers selon qualité
|
||||
Étant donné que je veux télécharger 50 contenus de 5 min chacun
|
||||
Quand je compare les qualités
|
||||
Alors les tailles totales sont:
|
||||
| qualité | bitrate | taille totale |
|
||||
| Basse | 24 kbps | ~250 MB |
|
||||
| Standard | 48 kbps | ~500 MB |
|
||||
| Haute | 64 kbps | ~650 MB |
|
||||
|
||||
Scénario: Justification Standard = Bon compromis
|
||||
Étant donné que le contenu RoadWave est principalement de la voix
|
||||
Quand la qualité Standard (48 kbps Opus) est utilisée
|
||||
Alors la qualité est très correcte pour la voix
|
||||
Et équivalente à la radio FM
|
||||
Et le compromis qualité/taille est optimal
|
||||
|
||||
Scénario: Justification Haute réservée Premium = Incitation upgrade
|
||||
Étant donné qu'un utilisateur gratuit veut la meilleure qualité
|
||||
Quand il voit que Haute est réservée Premium
|
||||
Alors cela l'incite à passer Premium pour 4.99€/mois
|
||||
Et c'est un avantage tangible supplémentaire de Premium
|
||||
|
||||
Scénario: Changement qualité après téléchargements existants
|
||||
Étant donné que j'ai déjà téléchargé 30 contenus en qualité Standard
|
||||
Quand je change la qualité vers Haute (si Premium)
|
||||
Alors les 30 contenus existants restent en Standard
|
||||
Et seuls les nouveaux téléchargements seront en Haute
|
||||
Et je peux manuellement re-télécharger les 30 contenus pour les avoir en Haute
|
||||
|
||||
# ===== PROCESSUS DE TÉLÉCHARGEMENT =====
|
||||
|
||||
Scénario: Téléchargement individuel d'un contenu
|
||||
Étant donné que je consulte la page d'un contenu
|
||||
Quand je clique sur l'icône de téléchargement 📥
|
||||
Alors le téléchargement démarre
|
||||
Et une barre de progression apparaît
|
||||
Et l'icône devient ✅ quand terminé
|
||||
|
||||
Scénario: Téléchargement batch de contenus sélectionnés
|
||||
Étant donné que je consulte une liste de contenus pour "Paris"
|
||||
Quand je sélectionne 15 contenus manuellement
|
||||
Et que je clique sur "Télécharger la sélection"
|
||||
Alors les 15 contenus sont téléchargés en parallèle (max 3 simultanés)
|
||||
Et une notification affiche "15 contenus téléchargés"
|
||||
|
||||
Scénario: Téléchargement automatique recommandations zone
|
||||
Étant donné que je sélectionne "Autour de moi" (Paris)
|
||||
Quand je clique sur "Télécharger les 50 meilleurs contenus"
|
||||
Alors l'algorithme sélectionne automatiquement les 50 contenus les mieux notés/récents
|
||||
Et les télécharge tous
|
||||
Et je n'ai pas besoin de choisir manuellement
|
||||
|
||||
Scénario: Barre de progression téléchargement global
|
||||
Étant donné que je télécharge 20 contenus
|
||||
Quand les téléchargements sont en cours
|
||||
Alors je vois une barre de progression globale:
|
||||
"""
|
||||
📥 Téléchargement en cours...
|
||||
7 / 20 contenus (35%)
|
||||
~45 MB restants
|
||||
Temps estimé: 2 min
|
||||
"""
|
||||
|
||||
Scénario: Téléchargements en tâche de fond
|
||||
Étant donné que je lance le téléchargement de 30 contenus
|
||||
Quand je ferme l'app ou passe à une autre activité
|
||||
Alors les téléchargements continuent en arrière-plan
|
||||
Et je reçois une notification quand tous sont terminés
|
||||
|
||||
Scénario: Pause et reprise téléchargements
|
||||
Étant donné que je télécharge 20 contenus
|
||||
Quand je clique sur "Pause"
|
||||
Alors les téléchargements en cours se terminent
|
||||
Et les téléchargements en attente sont mis en pause
|
||||
Et je peux cliquer sur "Reprendre" plus tard
|
||||
|
||||
Scénario: Annulation téléchargements
|
||||
Étant donné que je télécharge 20 contenus
|
||||
Quand je clique sur "Annuler"
|
||||
Alors tous les téléchargements sont arrêtés
|
||||
Et les fichiers partiels sont supprimés
|
||||
Et l'espace disque est libéré
|
||||
|
||||
Scénario: Gestion erreurs téléchargement
|
||||
Étant donné que je télécharge un contenu
|
||||
Mais que la connexion Internet coupe au milieu
|
||||
Quand la connexion revient
|
||||
Alors le téléchargement reprend automatiquement où il s'était arrêté
|
||||
Et aucune perte de progression n'a lieu
|
||||
|
||||
Scénario: Retry automatique après échec
|
||||
Étant donné qu'un téléchargement échoue 3 fois consécutives
|
||||
Quand l'échec est détecté
|
||||
Alors le contenu est marqué "Échec"
|
||||
Et je vois une notification "3 contenus n'ont pas pu être téléchargés"
|
||||
Et je peux retry manuellement en cliquant sur "Réessayer"
|
||||
|
||||
# ===== GESTION CONTENUS TÉLÉCHARGÉS =====
|
||||
|
||||
Scénario: Liste contenus téléchargés
|
||||
Étant donné que j'ai téléchargé 45 contenus
|
||||
Quand j'accède à "Téléchargements"
|
||||
Alors je vois la liste complète de mes 45 contenus
|
||||
Et pour chaque contenu: titre, créateur, durée, taille, date téléchargement
|
||||
|
||||
Scénario: Tri contenus téléchargés
|
||||
Étant donné que je consulte ma liste de téléchargements
|
||||
Quand je clique sur "Trier par"
|
||||
Alors je peux trier par:
|
||||
| critère | ordre |
|
||||
| Date téléchargement | Plus récent / Plus ancien|
|
||||
| Titre | A-Z / Z-A |
|
||||
| Créateur | A-Z / Z-A |
|
||||
| Durée | Plus long / Plus court |
|
||||
| Taille | Plus gros / Plus petit |
|
||||
|
||||
Scénario: Recherche dans contenus téléchargés
|
||||
Étant donné que j'ai 200 contenus téléchargés
|
||||
Quand je tape "Tesla" dans la barre de recherche
|
||||
Alors seuls les contenus contenant "Tesla" s'affichent
|
||||
Et je peux rapidement trouver un contenu spécifique
|
||||
|
||||
Scénario: Suppression individuelle contenu téléchargé
|
||||
Étant donné que je veux supprimer un contenu téléchargé
|
||||
Quand je swipe left (iOS) ou long press (Android) sur le contenu
|
||||
Et que je clique sur "Supprimer"
|
||||
Alors le fichier est supprimé du device
|
||||
Et l'espace disque est libéré
|
||||
Et le compteur est décrémenté (ex: 45/50 → 44/50)
|
||||
|
||||
Scénario: Suppression batch contenus téléchargés
|
||||
Étant donné que je veux supprimer plusieurs contenus
|
||||
Quand je sélectionne 10 contenus
|
||||
Et que je clique sur "Supprimer la sélection"
|
||||
Alors les 10 fichiers sont supprimés
|
||||
Et ~100 MB d'espace disque sont libérés
|
||||
Et une notification confirme "10 contenus supprimés"
|
||||
|
||||
Scénario: Suppression tous les contenus téléchargés
|
||||
Étant donné que j'ai 45 contenus téléchargés
|
||||
Quand je clique sur "Supprimer tout"
|
||||
Et que je confirme l'action
|
||||
Alors tous les 45 contenus sont supprimés
|
||||
Et l'espace disque total est libéré (~450 MB)
|
||||
Et le compteur repasse à 0/50
|
||||
|
||||
Scénario: Espace disque utilisé visible
|
||||
Étant donné que j'ai téléchargé 45 contenus
|
||||
Quand j'accède à la page Téléchargements
|
||||
Alors je vois l'espace disque utilisé:
|
||||
"""
|
||||
📥 Téléchargements
|
||||
45 / 50 contenus
|
||||
Espace utilisé: 478 MB
|
||||
"""
|
||||
|
||||
Scénario: Statistiques téléchargements
|
||||
Étant donné que j'accède à mes statistiques
|
||||
Quand je consulte la section Téléchargements
|
||||
Alors je vois:
|
||||
| métrique | valeur |
|
||||
| Contenus actuellement téléchargés | 45 |
|
||||
| Espace disque utilisé | 478 MB |
|
||||
| Contenus téléchargés depuis début | 287 |
|
||||
| Total data téléchargée | 3.2 GB |
|
||||
| Téléchargements via WiFi | 92% |
|
||||
| Téléchargements via mobile | 8% |
|
||||
|
||||
# ===== LECTURE OFFLINE =====
|
||||
|
||||
Scénario: Lecture contenu téléchargé sans connexion
|
||||
Étant donné que je n'ai aucune connexion Internet (mode avion)
|
||||
Et que j'ai des contenus téléchargés
|
||||
Quand je lance un contenu téléchargé
|
||||
Alors la lecture démarre normalement depuis le fichier local
|
||||
Et aucune erreur de connexion n'apparaît
|
||||
|
||||
Scénario: Badge "Téléchargé" sur contenus offline
|
||||
Étant donné que j'ai téléchargé certains contenus
|
||||
Quand je consulte une liste de contenus
|
||||
Alors les contenus téléchargés ont un badge ✅ "Offline"
|
||||
Et je sais immédiatement lesquels sont disponibles sans connexion
|
||||
|
||||
Scénario: Filtre "Téléchargés uniquement"
|
||||
Étant donné que je veux voir uniquement mes contenus offline
|
||||
Quand j'active le filtre "Téléchargés uniquement"
|
||||
Alors seuls les contenus téléchargés s'affichent
|
||||
Et je peux facilement naviguer dans mon catalogue offline
|
||||
|
||||
Scénario: Playlist offline automatique
|
||||
Étant donné que j'ai téléchargé 45 contenus
|
||||
Quand j'accède à "Téléchargements"
|
||||
Alors je peux lancer une playlist aléatoire de mes 45 contenus
|
||||
Et profiter d'une écoute continue offline
|
||||
@@ -0,0 +1,335 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Validité et renouvellement contenus offline
|
||||
En tant qu'utilisateur
|
||||
Je veux que mes contenus téléchargés restent valides un certain temps
|
||||
Afin de garantir la légalité et la fraîcheur du contenu
|
||||
|
||||
Contexte:
|
||||
Étant donné que je suis connecté à l'application RoadWave
|
||||
Et que j'ai des contenus téléchargés
|
||||
|
||||
# ===== DURÉE DE VALIDITÉ =====
|
||||
|
||||
Scénario: Validité de 30 jours après téléchargement
|
||||
Étant donné que je télécharge un contenu le 1er juin 2025
|
||||
Quand le téléchargement est terminé
|
||||
Alors le contenu est valide jusqu'au 1er juillet 2025 (30 jours)
|
||||
Et la date d'expiration est stockée en local
|
||||
|
||||
Scénario: Affichage date expiration sur contenu téléchargé
|
||||
Étant donné que j'ai téléchargé un contenu il y a 20 jours
|
||||
Quand je consulte les détails du contenu
|
||||
Alors je vois "Expire dans 10 jours"
|
||||
Et je sais combien de temps il reste avant expiration
|
||||
|
||||
Scénario: Standard industrie aligné (Spotify, YouTube, Deezer)
|
||||
Étant donné que Spotify, YouTube Music et Deezer utilisent 30 jours
|
||||
Quand RoadWave fixe également 30 jours
|
||||
Alors c'est le standard accepté par les utilisateurs
|
||||
Et il n'y a pas de confusion avec les autres plateformes
|
||||
|
||||
Scénario: Justification 30 jours - Force reconnexion régulière
|
||||
Étant donné qu'un utilisateur ne se connecte jamais
|
||||
Quand ses contenus expirent après 30 jours
|
||||
Alors il est obligé de se reconnecter pour les renouveler
|
||||
Et le système peut vérifier:
|
||||
| vérification |
|
||||
| Abonnement Premium toujours actif|
|
||||
| Contenus non modérés/supprimés |
|
||||
| Métadonnées à jour |
|
||||
|
||||
Scénario: Justification 30 jours - Évite stockage obsolète
|
||||
Étant donné qu'un contenu a été modéré après téléchargement
|
||||
Quand le contenu expire après 30 jours maximum
|
||||
Alors le contenu illégal est automatiquement supprimé
|
||||
Et ne reste pas indéfiniment sur le device
|
||||
|
||||
# ===== RENOUVELLEMENT AUTOMATIQUE =====
|
||||
|
||||
Scénario: Détection WiFi et contenus >25 jours
|
||||
Étant donné que j'ai des contenus téléchargés il y a 26 jours
|
||||
Quand l'app détecte une connexion WiFi
|
||||
Alors une requête GET /offline/contents/refresh est envoyée
|
||||
Et le backend vérifie chaque contenu
|
||||
|
||||
Scénario: Vérification abonnement Premium toujours actif
|
||||
Étant donné qu'un contenu téléchargé en Premium est à renouveler
|
||||
Quand le backend vérifie le statut
|
||||
Et que l'abonnement Premium est toujours actif
|
||||
Alors la validité est renouvelée à 30 jours supplémentaires
|
||||
|
||||
Scénario: Abonnement Premium expiré - Contenu non renouvelé
|
||||
Étant donné qu'un contenu Premium téléchargé est à renouveler
|
||||
Quand le backend vérifie le statut
|
||||
Et que l'abonnement Premium a expiré
|
||||
Alors le contenu n'est pas renouvelé
|
||||
Et il sera supprimé à l'expiration (J-0)
|
||||
Et l'utilisateur voit "Contenu Premium expiré (abonnement inactif)"
|
||||
|
||||
Scénario: Vérification contenu pas modéré/supprimé
|
||||
Étant donné qu'un contenu téléchargé est à renouveler
|
||||
Quand le backend vérifie le statut
|
||||
Et que le contenu a été modéré ou supprimé entre temps
|
||||
Alors le contenu n'est pas renouvelé
|
||||
Et sera supprimé immédiatement du device
|
||||
Et l'utilisateur voit "1 contenu retiré (violation règles)"
|
||||
|
||||
Scénario: Mise à jour métadonnées lors du renouvellement
|
||||
Étant donné qu'un contenu téléchargé est renouvelé
|
||||
Quand le backend traite le renouvellement
|
||||
Alors les métadonnées sont mises à jour:
|
||||
| métadonnée | mise à jour si changée |
|
||||
| Titre | ✅ |
|
||||
| Nom créateur | ✅ |
|
||||
| Description | ✅ |
|
||||
| Tags | ✅ |
|
||||
| Statut Premium | ✅ |
|
||||
Et l'utilisateur voit les infos à jour
|
||||
|
||||
Scénario: Pas de re-téléchargement audio si fichier OK
|
||||
Étant donné qu'un contenu est renouvelé
|
||||
Quand le fichier audio local est intact
|
||||
Alors seules les métadonnées sont mises à jour
|
||||
Et le fichier audio n'est pas re-téléchargé
|
||||
Et cela économise la bande passante
|
||||
|
||||
Scénario: Re-téléchargement audio si fichier corrompu
|
||||
Étant donné qu'un contenu est renouvelé
|
||||
Quand le fichier audio local est corrompu (checksum invalide)
|
||||
Alors le fichier audio est re-téléchargé entièrement
|
||||
Et le nouveau fichier remplace le corrompu
|
||||
|
||||
Scénario: Renouvellement silencieux si WiFi régulier
|
||||
Étant donné que je me connecte en WiFi tous les jours
|
||||
Quand mes contenus atteignent 25-30 jours
|
||||
Alors ils sont automatiquement renouvelés en arrière-plan
|
||||
Et je ne vois aucune notification (processus transparent)
|
||||
Et mes contenus restent valides indéfiniment
|
||||
|
||||
Scénario: Renouvellement batch de plusieurs contenus
|
||||
Étant donné que j'ai 30 contenus à renouveler
|
||||
Quand le renouvellement automatique se déclenche
|
||||
Alors une requête batch est envoyée:
|
||||
```json
|
||||
POST /offline/contents/refresh
|
||||
{
|
||||
"content_ids": ["abc123", "def456", "ghi789", ...]
|
||||
}
|
||||
```
|
||||
Et le backend traite les 30 contenus en une seule requête
|
||||
Et cela économise les requêtes HTTP
|
||||
|
||||
Scénario: Temps de traitement renouvellement
|
||||
Étant donné que 30 contenus sont à renouveler
|
||||
Quand la requête batch est traitée
|
||||
Alors le backend répond en <2 secondes
|
||||
Et les métadonnées sont mises à jour localement
|
||||
Et l'utilisateur ne remarque aucun ralentissement
|
||||
|
||||
# ===== NOTIFICATIONS EXPIRATION =====
|
||||
|
||||
Scénario: Notification J-3 avant expiration
|
||||
Étant donné que j'ai 15 contenus qui expirent dans 3 jours
|
||||
Quand le système vérifie les expirations
|
||||
Alors je reçois une notification:
|
||||
"""
|
||||
⚠️ 15 contenus expirent dans 3 jours
|
||||
|
||||
Connectez-vous en WiFi pour les renouveler automatiquement.
|
||||
"""
|
||||
Et je peux agir avant l'expiration
|
||||
|
||||
Scénario: Pas de notification si connexion WiFi régulière
|
||||
Étant donné que je me connecte en WiFi tous les jours
|
||||
Et que mes contenus sont automatiquement renouvelés
|
||||
Quand le système vérifie les expirations
|
||||
Alors aucune notification J-3 n'est envoyée
|
||||
Car les contenus sont déjà renouvelés silencieusement
|
||||
|
||||
Scénario: Notification uniquement si contenus non renouvelés
|
||||
Étant donné que j'ai 20 contenus dont 15 renouvelés et 5 non renouvelés
|
||||
Quand le J-3 arrive pour les 5 non renouvelés
|
||||
Alors je reçois "5 contenus expirent dans 3 jours"
|
||||
Et seuls les contenus à risque sont mentionnés
|
||||
|
||||
Scénario: Action utilisateur après notification J-3
|
||||
Étant donné que je reçois la notification J-3
|
||||
Quand je clique sur la notification
|
||||
Alors l'app s'ouvre sur la page Téléchargements
|
||||
Et je vois les contenus qui vont expirer en rouge
|
||||
Et je peux me connecter en WiFi pour les renouveler
|
||||
|
||||
Scénario: Suppression automatique J-0 (expiration)
|
||||
Étant donné qu'un contenu n'a pas été renouvelé
|
||||
Quand le jour d'expiration arrive (J-0)
|
||||
Alors le fichier est automatiquement supprimé du device
|
||||
Et l'espace disque est libéré
|
||||
Et le compteur est décrémenté (ex: 45/50 → 44/50)
|
||||
|
||||
Scénario: Toast après suppression automatique J-0
|
||||
Étant donné que 15 contenus viennent d'expirer
|
||||
Quand l'utilisateur ouvre l'app
|
||||
Alors il voit un toast:
|
||||
"""
|
||||
🗑️ 15 contenus expirés ont été supprimés
|
||||
|
||||
Reconnectez-vous en WiFi régulièrement pour éviter les expirations.
|
||||
"""
|
||||
|
||||
Scénario: Liste contenus supprimés après expiration
|
||||
Étant donné que 15 contenus ont expiré
|
||||
Quand je consulte l'historique des suppressions
|
||||
Alors je vois la liste des 15 contenus supprimés:
|
||||
| titre | créateur | date expiration |
|
||||
| Mon épisode préféré | JeanDupont | 15 juin 2025 |
|
||||
| Road trip Bretagne | MarieLambert| 15 juin 2025 |
|
||||
| ... | ... | ... |
|
||||
Et je peux les re-télécharger si je veux
|
||||
|
||||
Scénario: Re-téléchargement après expiration
|
||||
Étant donné qu'un contenu a expiré et été supprimé
|
||||
Quand je retrouve ce contenu dans l'app
|
||||
Alors le badge ✅ "Offline" n'est plus affiché
|
||||
Et je peux le re-télécharger normalement
|
||||
Et la validité repart à 30 jours
|
||||
|
||||
# ===== CAS PARTICULIERS =====
|
||||
|
||||
Scénario: Utilisateur ne se connecte jamais pendant 30 jours
|
||||
Étant donné que je télécharge 50 contenus le 1er juin
|
||||
Mais que je ne me connecte jamais en WiFi pendant 30 jours
|
||||
Quand le 1er juillet arrive
|
||||
Alors tous les 50 contenus expirent
|
||||
Et sont automatiquement supprimés
|
||||
Et je n'ai plus aucun contenu offline
|
||||
|
||||
Scénario: Utilisateur en zone blanche 30+ jours
|
||||
Étant donné que je télécharge 50 contenus avant de partir en zone sans réseau
|
||||
Et que je reste 45 jours sans connexion
|
||||
Quand les contenus expirent après 30 jours
|
||||
Alors ils sont supprimés même si je ne peux pas me connecter
|
||||
Et je perds l'accès à mes contenus offline
|
||||
|
||||
Scénario: Recommandation téléchargement avant zone blanche longue
|
||||
Étant donné que je prépare un road trip de 60 jours
|
||||
Quand je consulte la FAQ
|
||||
Alors je vois la recommandation:
|
||||
"""
|
||||
⚠️ Road trips >30 jours
|
||||
|
||||
Les contenus téléchargés expirent après 30 jours.
|
||||
Pour les longs voyages sans connexion:
|
||||
• Téléchargez de nouveaux contenus tous les 25 jours si possible
|
||||
• Ou planifiez une reconnexion WiFi tous les 25 jours
|
||||
"""
|
||||
|
||||
Scénario: Changement statut Premium en gratuit pendant validité
|
||||
Étant donné que je suis Premium et j'ai téléchargé 200 contenus
|
||||
Quand mon abonnement Premium expire
|
||||
Et que je repasse en gratuit
|
||||
Alors au prochain renouvellement, seulement 50 contenus sont conservés
|
||||
Et les 150 autres sont supprimés (limite gratuit)
|
||||
Et je vois "Limite gratuit (50 contenus) appliquée. 150 contenus supprimés."
|
||||
|
||||
Scénario: Sélection automatique 50 meilleurs contenus si passage gratuit
|
||||
Étant donné que je repasse en gratuit avec 200 contenus téléchargés
|
||||
Quand le système applique la limite de 50
|
||||
Alors les 50 contenus les plus récemment écoutés sont conservés
|
||||
Et les 150 autres sont supprimés
|
||||
Et cela maximise les chances de garder les contenus que j'aime
|
||||
|
||||
Scénario: Contenus Premium exclusifs supprimés si abonnement expire
|
||||
Étant donné que j'ai téléchargé 20 contenus Premium exclusifs
|
||||
Quand mon abonnement Premium expire
|
||||
Alors les 20 contenus Premium sont immédiatement supprimés
|
||||
Car ils ne sont accessibles qu'aux abonnés Premium actifs
|
||||
Et je vois "20 contenus Premium supprimés (abonnement expiré)"
|
||||
|
||||
# ===== STATISTIQUES ET MONITORING =====
|
||||
|
||||
Scénario: Affichage temps restant avant expiration
|
||||
Étant donné que j'ai 45 contenus téléchargés
|
||||
Quand je consulte la page Téléchargements
|
||||
Alors je vois pour chaque contenu:
|
||||
| contenu | temps restant |
|
||||
| Mon épisode (récent)| Expire dans 28 jours |
|
||||
| Road trip (ancien) | Expire dans 3 jours |
|
||||
Et je sais lesquels sont prioritaires pour renouvellement
|
||||
|
||||
Scénario: Tri par date expiration
|
||||
Étant donné que j'ai 45 contenus avec différentes dates d'expiration
|
||||
Quand je trie par "Expiration"
|
||||
Alors les contenus qui expirent le plus tôt apparaissent en premier
|
||||
Et je peux voir rapidement lesquels nécessitent une reconnexion urgente
|
||||
|
||||
Scénario: Badge rouge si expiration <3 jours
|
||||
Étant donné qu'un contenu expire dans 2 jours
|
||||
Quand je consulte la liste des téléchargements
|
||||
Alors le contenu a un badge rouge "⚠️ Expire bientôt"
|
||||
Et il est visuellement mis en avant
|
||||
|
||||
Scénario: Statistiques utilisateur - Taux de renouvellement
|
||||
Étant donné que j'accède à mes statistiques
|
||||
Quand je consulte la section Téléchargements
|
||||
Alors je vois:
|
||||
| métrique | valeur |
|
||||
| Contenus actuels | 45 |
|
||||
| Contenus expirés depuis début | 87 |
|
||||
| Contenus renouvelés (auto) | 234 |
|
||||
| Taux renouvellement automatique | 73% |
|
||||
|
||||
Scénario: Statistiques admin - Taux expiration global
|
||||
Étant donné qu'un admin consulte les métriques offline
|
||||
Quand il accède au dashboard
|
||||
Alors il voit:
|
||||
| métrique | valeur |
|
||||
| Contenus téléchargés actifs | 1,234,567 |
|
||||
| Expirations ce mois | 45,678 |
|
||||
| Taux expiration | 3.7% |
|
||||
| Renouvellements automatiques/mois | 234,567 |
|
||||
|
||||
Scénario: Alerte admin si taux expiration >10%
|
||||
Étant donné que le taux d'expiration mensuel dépasse 10%
|
||||
Quand le système détecte cette anomalie
|
||||
Alors une alerte est envoyée:
|
||||
"""
|
||||
⚠️ Taux d'expiration anormal: 12.3%
|
||||
|
||||
Nombre expirations ce mois: 152,345
|
||||
Causes possibles:
|
||||
- Utilisateurs ne se connectent plus en WiFi
|
||||
- Problème renouvellement automatique ?
|
||||
- Churn utilisateurs augmenté ?
|
||||
|
||||
Action recommandée: Enquête technique + email rappel utilisateurs
|
||||
"""
|
||||
|
||||
Scénario: Email rappel si pas de connexion WiFi depuis 20 jours
|
||||
Étant donné que je n'ai pas connecté l'app en WiFi depuis 20 jours
|
||||
Et que j'ai 45 contenus téléchargés
|
||||
Quand le système détecte cette inactivité WiFi
|
||||
Alors je reçois un email:
|
||||
"""
|
||||
📡 Connectez-vous en WiFi pour conserver vos téléchargements
|
||||
|
||||
Vous n'avez pas connecté RoadWave en WiFi depuis 20 jours.
|
||||
Vos 45 contenus téléchargés expireront dans 10 jours si non renouvelés.
|
||||
|
||||
Connectez-vous en WiFi avant le 30 juin pour les renouveler automatiquement.
|
||||
"""
|
||||
|
||||
Scénario: Performance renouvellement avec 10 000 utilisateurs simultanés
|
||||
Étant donné que 10 000 utilisateurs se connectent en WiFi simultanément
|
||||
Quand chacun demande le renouvellement de 50 contenus
|
||||
Alors le serveur traite 500 000 vérifications
|
||||
Et grâce au cache Redis et index PostgreSQL, le temps de réponse reste <3s
|
||||
Et les serveurs gèrent la charge sans problème
|
||||
|
||||
Scénario: Logs audit renouvellements
|
||||
Étant donné qu'un contenu est renouvelé
|
||||
Quand l'opération se termine
|
||||
Alors un log est enregistré:
|
||||
| timestamp | user_id | content_id | action | résultat |
|
||||
| 2025-06-15 14:30:00 | abc123 | xyz789 | renew | success (+30d) |
|
||||
| 2025-06-15 14:30:01 | abc123 | def456 | renew | failed (deleted)|
|
||||
Et ces logs aident à débugger les problèmes
|
||||
362
docs/domains/premium/features/premium/avantages-premium.feature
Normal file
362
docs/domains/premium/features/premium/avantages-premium.feature
Normal file
@@ -0,0 +1,362 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Avantages Premium
|
||||
En tant qu'abonné Premium
|
||||
Je veux bénéficier d'avantages exclusifs
|
||||
Afin de profiter d'une expérience audio améliorée sans publicité
|
||||
|
||||
Contexte:
|
||||
Étant donné que je suis connecté à l'application RoadWave
|
||||
|
||||
# ===== PUBLICITÉS =====
|
||||
|
||||
Scénario: Utilisateur gratuit voit 1 publicité tous les 5 contenus
|
||||
Étant donné que je suis un utilisateur gratuit
|
||||
Quand j'écoute ma file de contenus
|
||||
Alors je vois une publicité tous les 5 contenus
|
||||
Et la publicité dure 30 secondes en moyenne
|
||||
Et je ne peux pas la skip
|
||||
|
||||
Scénario: Utilisateur Premium ne voit aucune publicité
|
||||
Étant donné que je suis un utilisateur Premium
|
||||
Quand j'écoute mes contenus
|
||||
Alors aucune publicité n'est diffusée
|
||||
Et je passe directement d'un contenu à l'autre
|
||||
Et l'expérience d'écoute est fluide et ininterrompue
|
||||
|
||||
Scénario: Badge "0 publicité" sur page Premium
|
||||
Étant donné que je consulte la page des avantages Premium
|
||||
Quand je lis la liste des avantages
|
||||
Alors je vois en premier:
|
||||
"""
|
||||
🚫 0 publicité
|
||||
Profitez d'une écoute sans interruption
|
||||
"""
|
||||
Et c'est l'argument principal mis en avant
|
||||
|
||||
# ===== CONTENUS EXCLUSIFS =====
|
||||
|
||||
Scénario: Utilisateur gratuit voit contenus Premium bloqués
|
||||
Étant donné que je suis un utilisateur gratuit
|
||||
Quand je consulte les contenus d'un créateur
|
||||
Alors je vois les contenus marqués Premium avec badge 👑
|
||||
Mais je ne peux pas les lire (overlay bloquant)
|
||||
|
||||
Scénario: Utilisateur Premium accède à tous les contenus exclusifs
|
||||
Étant donné que je suis un utilisateur Premium
|
||||
Quand je consulte les contenus d'un créateur
|
||||
Alors tous les contenus Premium sont accessibles
|
||||
Et je peux les lire sans restriction
|
||||
Et j'ai accès à 100% du catalogue (gratuit + Premium)
|
||||
|
||||
Scénario: Nombre de contenus Premium disponibles
|
||||
Étant donné que je suis Premium
|
||||
Quand je consulte les statistiques
|
||||
Alors je vois combien de contenus Premium sont disponibles sur la plateforme
|
||||
Et par exemple: "8,547 contenus Premium exclusifs disponibles"
|
||||
Et cela justifie la valeur de l'abonnement
|
||||
|
||||
# ===== QUALITÉ AUDIO =====
|
||||
|
||||
Scénario: Utilisateur gratuit écoute en 48 kbps Opus
|
||||
Étant donné que je suis un utilisateur gratuit
|
||||
Quand je lance un contenu
|
||||
Alors l'audio est streamé en 48 kbps Opus
|
||||
Et cela consomme environ 20 MB/heure
|
||||
Et la qualité est très correcte pour de la voix
|
||||
|
||||
Scénario: Utilisateur Premium écoute en 64 kbps Opus
|
||||
Étant donné que je suis un utilisateur Premium
|
||||
Quand je lance un contenu
|
||||
Alors l'audio est streamé en 64 kbps Opus
|
||||
Et cela consomme environ 30 MB/heure
|
||||
Et la qualité est excellente (détails audio supérieurs)
|
||||
|
||||
Scénario: Comparaison qualité 48 kbps vs 64 kbps
|
||||
Étant donné que je consulte la page Premium
|
||||
Quand je lis la section qualité audio
|
||||
Alors je vois l'explication:
|
||||
"""
|
||||
📻 Qualité audio supérieure
|
||||
|
||||
Gratuit: 48 kbps Opus (~20 MB/h)
|
||||
Premium: 64 kbps Opus (~30 MB/h)
|
||||
|
||||
Profitez d'une qualité audio exceptionnelle avec plus de détails
|
||||
et une meilleure restitution des voix et ambiances.
|
||||
"""
|
||||
|
||||
Scénario: Justification 48 kbps suffisant pour gratuit
|
||||
Étant donné que le contenu RoadWave est principalement de la voix
|
||||
Quand la qualité est fixée à 48 kbps pour gratuit
|
||||
Alors c'est largement suffisant pour comprendre clairement
|
||||
Et équivalent à la qualité radio FM
|
||||
Et les utilisateurs gratuits ne sont pas frustrés
|
||||
|
||||
Scénario: Justification 64 kbps avantage tangible Premium
|
||||
Étant donné que les audiophiles et créateurs audio sont exigeants
|
||||
Quand la qualité Premium est à 64 kbps
|
||||
Alors la différence est perceptible à l'oreille
|
||||
Et les ambiances, musiques de fond, nuances de voix sont mieux rendues
|
||||
Et cela justifie l'abonnement Premium
|
||||
|
||||
Scénario: Switch automatique qualité selon abonnement
|
||||
Étant donné que je suis gratuit et j'écoute en 48 kbps
|
||||
Quand je souscris à Premium
|
||||
Alors dès le contenu suivant, je passe automatiquement en 64 kbps
|
||||
Et je peux entendre la différence de qualité immédiatement
|
||||
|
||||
Scénario: Consommation data Premium vs Gratuit
|
||||
Étant donné que je roule 1 heure par jour
|
||||
Quand je calcule la consommation mensuelle
|
||||
Alors en gratuit: 20 MB/h × 1h × 22 jours = 440 MB/mois
|
||||
Et en Premium: 30 MB/h × 1h × 22 jours = 660 MB/mois
|
||||
Et la différence est de 220 MB/mois (acceptable pour 4G/5G illimitée)
|
||||
|
||||
# ===== MODE OFFLINE =====
|
||||
|
||||
Scénario: Utilisateur gratuit limité à 50 contenus téléchargés
|
||||
Étant donné que je suis un utilisateur gratuit
|
||||
Quand j'accède au mode offline
|
||||
Alors je peux télécharger jusqu'à 50 contenus maximum
|
||||
Et si j'essaie de télécharger un 51ème, je vois:
|
||||
"""
|
||||
Limite atteinte (50 contenus max en gratuit).
|
||||
Passez Premium pour des téléchargements illimités.
|
||||
"""
|
||||
|
||||
Scénario: Utilisateur Premium téléchargements illimités
|
||||
Étant donné que je suis un utilisateur Premium
|
||||
Quand j'accède au mode offline
|
||||
Alors je peux télécharger autant de contenus que je veux
|
||||
Et la seule limite est l'espace de stockage de mon device
|
||||
Et par exemple 500 contenus × 10 MB = 5 GB
|
||||
|
||||
Scénario: Justification limite 50 contenus gratuit
|
||||
Étant donné que 50 contenus de 10 minutes = ~8 heures d'écoute
|
||||
Quand un utilisateur gratuit prépare un road trip
|
||||
Alors 8 heures couvrent largement une journée de trajet
|
||||
Et cela permet un usage offline raisonnable sans abuser
|
||||
|
||||
Scénario: Justification illimité Premium pour longs road trips
|
||||
Étant donné qu'un road trip de plusieurs jours nécessite 20-50h de contenu
|
||||
Quand un utilisateur Premium télécharge 200 contenus
|
||||
Alors il peut partir serein sans connexion internet pendant 1 semaine
|
||||
Et cela justifie pleinement l'abonnement Premium
|
||||
|
||||
Scénario: Affichage compteur téléchargements gratuit
|
||||
Étant donné que je suis gratuit et j'ai téléchargé 37 contenus
|
||||
Quand j'accède à la page Téléchargements
|
||||
Alors je vois:
|
||||
"""
|
||||
📥 Téléchargements offline
|
||||
|
||||
37 / 50 contenus téléchargés
|
||||
|
||||
Passez Premium pour des téléchargements illimités
|
||||
[Découvrir Premium]
|
||||
```
|
||||
|
||||
Scénario: Pas de compteur pour Premium
|
||||
Étant donné que je suis Premium et j'ai téléchargé 187 contenus
|
||||
Quand j'accède à la page Téléchargements
|
||||
Alors je vois simplement:
|
||||
"""
|
||||
📥 Téléchargements offline
|
||||
|
||||
187 contenus téléchargés (illimité)
|
||||
Espace utilisé: 1.8 GB
|
||||
```
|
||||
Et aucune limite n'est affichée
|
||||
|
||||
# ===== HISTORIQUE ÉCOUTE =====
|
||||
|
||||
Scénario: Utilisateur gratuit historique limité à 100 derniers
|
||||
Étant donné que je suis un utilisateur gratuit
|
||||
Quand j'accède à mon historique d'écoute
|
||||
Alors je vois les 100 derniers contenus écoutés
|
||||
Et les contenus plus anciens ne sont pas affichés
|
||||
Et je vois un message "Historique limité à 100 contenus. Passez Premium pour un historique illimité."
|
||||
|
||||
Scénario: Utilisateur Premium historique illimité
|
||||
Étant donné que je suis un utilisateur Premium
|
||||
Quand j'accède à mon historique d'écoute
|
||||
Alors je vois tous les contenus que j'ai écoutés depuis mon inscription
|
||||
Et je peux scroller jusqu'au premier contenu jamais écouté
|
||||
Et l'historique est complet et permanent
|
||||
|
||||
Scénario: Recherche dans historique Premium
|
||||
Étant donné que je suis Premium et j'ai 2 000 contenus dans mon historique
|
||||
Quand je recherche "Tesla" dans mon historique
|
||||
Alors tous les contenus écoutés contenant "Tesla" sont affichés
|
||||
Et je peux retrouver facilement un contenu écouté il y a 6 mois
|
||||
|
||||
Scénario: Justification limite 100 gratuit suffisante
|
||||
Étant donné que 100 contenus de 10 min = ~16 heures d'écoute
|
||||
Quand un utilisateur gratuit écoute 1h/jour
|
||||
Alors l'historique couvre les 16 derniers jours
|
||||
Et cela suffit pour retrouver un contenu récent
|
||||
|
||||
Scénario: Justification illimité Premium pour power users
|
||||
Étant donné qu'un power user écoute 3h/jour depuis 2 ans
|
||||
Quand il veut retrouver un contenu spécifique écouté il y a 1 an
|
||||
Alors l'historique illimité Premium lui permet de retrouver ce contenu
|
||||
Et cela apporte une vraie valeur ajoutée
|
||||
|
||||
Scénario: Export historique complet (Premium uniquement)
|
||||
Étant donné que je suis Premium
|
||||
Quand je demande l'export de mes données
|
||||
Alors l'historique complet est inclus dans l'export:
|
||||
```json
|
||||
{
|
||||
"listen_history": [
|
||||
{
|
||||
"content_title": "Mon épisode préféré",
|
||||
"creator_name": "JeanDupont",
|
||||
"listened_at": "2025-06-15T14:30:00Z",
|
||||
"completion_rate": 0.95
|
||||
},
|
||||
...
|
||||
],
|
||||
"total_listens": 2847
|
||||
}
|
||||
```
|
||||
|
||||
# ===== TABLEAU COMPARATIF =====
|
||||
|
||||
Scénario: Affichage tableau comparatif Gratuit vs Premium
|
||||
Étant donné que je consulte la page Premium
|
||||
Quand je vois le tableau comparatif
|
||||
Alors il affiche:
|
||||
```
|
||||
┌─────────────────────────┬──────────────┬──────────────┐
|
||||
│ Avantage │ Gratuit │ Premium │
|
||||
├─────────────────────────┼──────────────┼──────────────┤
|
||||
│ Publicités │ 1/5 contenus │ 0 (aucune) │
|
||||
│ Contenus exclusifs │ ❌ Bloqués │ ✅ Accès │
|
||||
│ Qualité audio │ 48 kbps Opus │ 64 kbps Opus │
|
||||
│ Mode offline │ 50 max │ Illimité │
|
||||
│ Historique écoute │ 100 derniers │ Illimité │
|
||||
│ Prix │ Gratuit │ 4.99€/mois │
|
||||
└─────────────────────────┴──────────────┴──────────────┘
|
||||
```
|
||||
|
||||
# ===== JUSTIFICATIONS GÉNÉRALES =====
|
||||
|
||||
Scénario: Justification 0 pub = argument principal
|
||||
Étant donné qu'une publicité de 30s tous les 5 contenus = 6 min/h de pub
|
||||
Quand un utilisateur écoute 1h/jour
|
||||
Alors il subit 180 min de pub/mois (3 heures !)
|
||||
Et payer 4.99€ pour éviter 3h de pub/mois est très rentable
|
||||
Et c'est l'argument de conversion n°1
|
||||
|
||||
Scénario: Justification qualité audio avantage tangible
|
||||
Étant donné que la différence 48 kbps → 64 kbps est audible
|
||||
Quand un audiophile compare les deux
|
||||
Alors il entend clairement la différence sur un bon système audio voiture
|
||||
Et cela justifie l'abonnement pour les exigeants
|
||||
|
||||
Scénario: Justification offline illimité pour road trips
|
||||
Étant donné qu'un road trip de 2 semaines nécessite 50-100h de contenu
|
||||
Quand un utilisateur Premium télécharge 300 contenus avant de partir
|
||||
Alors il peut partir en zone sans réseau sereinement
|
||||
Et cela apporte une vraie valeur pratique
|
||||
|
||||
Scénario: Justification pas d'over-engineering
|
||||
Étant donné que RoadWave se concentre sur l'essentiel
|
||||
Quand les avantages Premium sont définis
|
||||
Alors il n'y a pas de:
|
||||
| fonctionnalité superflue | raison exclusion |
|
||||
| Badges cosmétiques | Pas de valeur réelle |
|
||||
| Avatar Premium exclusif | Inutile pour audio |
|
||||
| Fonctionnalités sociales avancées | Pas prioritaire MVP |
|
||||
| Early access nouveaux contenus | Complexité > bénéfice |
|
||||
Et cela réduit la complexité et le coût de développement
|
||||
|
||||
# ===== CONVERSION ET INCITATION =====
|
||||
|
||||
Scénario: CTA Premium après 5ème publicité
|
||||
Étant donné que je suis gratuit et je viens d'entendre ma 5ème pub
|
||||
Quand la publicité se termine
|
||||
Alors je vois un message:
|
||||
"""
|
||||
😫 Marre des pubs ?
|
||||
|
||||
Passez Premium pour seulement 4.99€/mois :
|
||||
• 0 publicité
|
||||
• Qualité audio supérieure
|
||||
• Téléchargements illimités
|
||||
|
||||
[Essayer Premium] [Plus tard]
|
||||
```
|
||||
|
||||
Scénario: CTA Premium quand limite 50 téléchargements atteinte
|
||||
Étant donné que je suis gratuit et j'ai atteint 50 téléchargements
|
||||
Quand j'essaie de télécharger un 51ème contenu
|
||||
Alors je vois une popup:
|
||||
"""
|
||||
📥 Limite atteinte
|
||||
|
||||
Vous avez atteint la limite de 50 téléchargements.
|
||||
|
||||
Avec Premium (4.99€/mois), téléchargez autant de contenus que vous voulez
|
||||
pour vos longs road trips !
|
||||
|
||||
[Passer Premium] [Gérer mes téléchargements]
|
||||
```
|
||||
|
||||
Scénario: CTA Premium quand contenu exclusif bloqué
|
||||
Étant donné que je suis gratuit et je clique sur un contenu Premium
|
||||
Quand l'overlay bloquant apparaît
|
||||
Alors je vois:
|
||||
"""
|
||||
👑 Contenu Premium
|
||||
|
||||
Ce contenu est réservé aux abonnés Premium.
|
||||
|
||||
Débloquez 8,547 contenus Premium exclusifs pour 4.99€/mois !
|
||||
|
||||
[Passer Premium] [Découvrir d'autres contenus]
|
||||
```
|
||||
|
||||
Scénario: Statistiques conversion - Quel avantage convertit le mieux ?
|
||||
Étant donné qu'un admin consulte les statistiques de conversion
|
||||
Quand il analyse les sources de conversion
|
||||
Alors il voit:
|
||||
| source de conversion | % conversions |
|
||||
| CTA après 5ème pub | 42% |
|
||||
| CTA contenu Premium bloqué | 28% |
|
||||
| CTA limite 50 téléchargements | 18% |
|
||||
| Page Premium directe | 12% |
|
||||
Et cela aide à optimiser le placement des CTA
|
||||
|
||||
Scénario: A/B test message CTA
|
||||
Étant donné que RoadWave veut optimiser les conversions
|
||||
Quand un A/B test est lancé sur le CTA après pub
|
||||
Alors groupe A voit "Marre des pubs ?" (focus négatif)
|
||||
Et groupe B voit "Profitez de 0 publicité" (focus positif)
|
||||
Et le taux de conversion est mesuré
|
||||
Et le message le plus performant est déployé
|
||||
|
||||
Scénario: Notification Premium après 30 jours d'utilisation gratuite
|
||||
Étant donné que je suis utilisateur gratuit depuis 30 jours
|
||||
Et que j'écoute régulièrement (15h cumulées)
|
||||
Quand le 30ème jour arrive
|
||||
Alors je reçois une notification:
|
||||
"""
|
||||
🎉 Vous avez écouté 15h sur RoadWave !
|
||||
|
||||
Profitez encore plus avec Premium :
|
||||
• 0 publicité
|
||||
• Qualité supérieure
|
||||
• Téléchargements illimités
|
||||
|
||||
Offre découverte : -20% sur le premier mois (3.99€)
|
||||
[Découvrir Premium]
|
||||
```
|
||||
|
||||
Scénario: Trial gratuit refusé mais onboarding amélioré
|
||||
Étant donné qu'il n'y a pas de trial gratuit
|
||||
Quand un nouvel utilisateur s'inscrit
|
||||
Alors un onboarding explique clairement les avantages Premium
|
||||
Et il peut comparer gratuit vs Premium dès le premier lancement
|
||||
Et cela l'aide à décider rapidement s'il veut payer
|
||||
457
docs/domains/premium/features/premium/gestion-abonnement.feature
Normal file
457
docs/domains/premium/features/premium/gestion-abonnement.feature
Normal file
@@ -0,0 +1,457 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Gestion abonnement Premium
|
||||
En tant qu'utilisateur
|
||||
Je veux gérer facilement mon abonnement Premium
|
||||
Afin de souscrire, renouveler ou annuler en toute transparence
|
||||
|
||||
Contexte:
|
||||
Étant donné que je suis connecté à l'application RoadWave
|
||||
|
||||
# ===== SOUSCRIPTION =====
|
||||
|
||||
Scénario: Souscription via Web (desktop/mobile) avec Mangopay
|
||||
Étant donné que je consulte la page Premium sur le site web
|
||||
Quand je clique sur "S'abonner - Mensuel 4.99€"
|
||||
Alors je suis redirigé vers le formulaire de paiement Mangopay
|
||||
Et je saisis mes informations de carte bancaire
|
||||
Et le paiement de 4.99€ est prélevé immédiatement
|
||||
Et la commission Mangopay est de 1.8% + 0.18€ = 0.27€
|
||||
Et RoadWave reçoit 4.72€ net
|
||||
|
||||
Scénario: Calcul commission Mangopay
|
||||
Étant donné qu'un utilisateur paie 4.99€ via Mangopay
|
||||
Quand la commission est calculée
|
||||
Alors la commission est : 4.99€ × 1.8% + 0.18€ = 0.09€ + 0.18€ = 0.27€
|
||||
Et RoadWave reçoit : 4.99€ - 0.27€ = 4.72€
|
||||
Et la commission représente 5.4% du prix
|
||||
|
||||
Scénario: Souscription via iOS App avec Apple IAP
|
||||
Étant donné que j'utilise l'app iOS
|
||||
Quand je clique sur "S'abonner - Mensuel 5.99€"
|
||||
Alors je suis redirigé vers l'interface Apple In-App Purchase
|
||||
Et le prix affiché est 5.99€ (majoré de 20%)
|
||||
Et le paiement est effectué via mon compte Apple
|
||||
Et Apple prend 30% de commission = 1.80€
|
||||
Et RoadWave reçoit 4.19€ net
|
||||
|
||||
Scénario: Souscription via Android App avec Google Play Billing
|
||||
Étant donné que j'utilise l'app Android
|
||||
Quand je clique sur "S'abonner - Mensuel 5.99€"
|
||||
Alors je suis redirigé vers l'interface Google Play Billing
|
||||
Et le prix affiché est 5.99€ (majoré de 20%)
|
||||
Et le paiement est effectué via mon compte Google
|
||||
Et Google prend 30% de commission = 1.80€
|
||||
Et RoadWave reçoit 4.19€ net
|
||||
|
||||
Scénario: Majoration 20% sur mobile pour compenser commission 30%
|
||||
Étant donné que Apple/Google prennent 30% de commission
|
||||
Quand RoadWave fixe le prix mobile
|
||||
Alors le prix web est 4.99€ (commission Mangopay 5.4%)
|
||||
Et le prix mobile est 5.99€ (commission Apple/Google 30%)
|
||||
Et la majoration est de 1€ (+20%)
|
||||
Et cela compense partiellement la commission excessive
|
||||
|
||||
Scénario: Email incitation souscription web moins chère
|
||||
Étant donné que je consulte Premium depuis l'app mobile
|
||||
Quand je vois le prix 5.99€
|
||||
Alors je vois aussi un message:
|
||||
"""
|
||||
💡 Astuce : Abonnez-vous sur roadwave.com pour seulement 4.99€/mois
|
||||
Économisez 12€/an en évitant les frais Apple/Google !
|
||||
"""
|
||||
Et un lien vers le site web est fourni
|
||||
|
||||
Scénario: Calcul économie souscription web vs mobile
|
||||
Étant donné que le prix web est 4.99€/mois
|
||||
Et que le prix mobile est 5.99€/mois
|
||||
Quand je calcule l'économie annuelle
|
||||
Alors web : 4.99€ × 12 = 59.88€/an
|
||||
Et mobile : 5.99€ × 12 = 71.88€/an
|
||||
Et économie : 12€/an (soit 20% d'économie)
|
||||
|
||||
Scénario: Activation immédiate après paiement réussi
|
||||
Étant donné que je viens de payer mon abonnement Premium
|
||||
Quand le paiement est confirmé
|
||||
Alors mon statut passe immédiatement à "Premium"
|
||||
Et je peux accéder aux avantages Premium dès maintenant
|
||||
Et je reçois un email de confirmation
|
||||
|
||||
Scénario: Email confirmation souscription
|
||||
Étant donné que j'ai souscrit à Premium
|
||||
Quand la souscription est confirmée
|
||||
Alors je reçois un email:
|
||||
"""
|
||||
🎉 Bienvenue Premium !
|
||||
|
||||
Votre abonnement Premium est actif.
|
||||
|
||||
Formule: Mensuel 4.99€/mois
|
||||
Prochain renouvellement: 15 juillet 2025
|
||||
|
||||
Vos avantages:
|
||||
• 0 publicité
|
||||
• Contenus exclusifs
|
||||
• Qualité audio 64 kbps
|
||||
• Téléchargements illimités
|
||||
• Historique illimité
|
||||
|
||||
Profitez pleinement de RoadWave !
|
||||
|
||||
Gérer mon abonnement: [Lien]
|
||||
"""
|
||||
|
||||
# ===== RENOUVELLEMENT AUTOMATIQUE =====
|
||||
|
||||
Scénario: Email rappel 7 jours avant renouvellement
|
||||
Étant donné que mon abonnement mensuel se renouvelle le 15 juillet
|
||||
Quand le 8 juillet arrive (7 jours avant)
|
||||
Alors je reçois un email de rappel:
|
||||
"""
|
||||
📅 Votre abonnement Premium se renouvelle dans 7 jours
|
||||
|
||||
Formule: Mensuel 4.99€/mois
|
||||
Date de renouvellement: 15 juillet 2025
|
||||
Montant: 4.99€
|
||||
|
||||
Votre carte bancaire sera débitée automatiquement.
|
||||
|
||||
Vous souhaitez annuler ? [Gérer mon abonnement]
|
||||
"""
|
||||
|
||||
Scénario: Renouvellement automatique réussi
|
||||
Étant donné que mon abonnement mensuel arrive à échéance le 15 juillet
|
||||
Quand le 15 juillet arrive
|
||||
Alors Mangopay/Apple/Google prélève automatiquement 4.99€ (ou 5.99€)
|
||||
Et mon abonnement est renouvelé pour 1 mois supplémentaire
|
||||
Et je reçois un email de confirmation
|
||||
|
||||
Scénario: Email confirmation renouvellement
|
||||
Étant donné que mon abonnement vient d'être renouvelé
|
||||
Quand le paiement est confirmé
|
||||
Alors je reçois un email:
|
||||
"""
|
||||
✅ Abonnement Premium renouvelé
|
||||
|
||||
Votre abonnement a été renouvelé avec succès.
|
||||
|
||||
Montant débité: 4.99€
|
||||
Prochaine échéance: 15 août 2025
|
||||
|
||||
Merci de continuer à soutenir RoadWave et ses créateurs !
|
||||
|
||||
Gérer mon abonnement: [Lien]
|
||||
"""
|
||||
|
||||
Scénario: Échec paiement renouvellement - Tentative 1
|
||||
Étant donné que mon abonnement doit se renouveler le 15 juillet
|
||||
Mais que ma carte bancaire est expirée ou sans fonds
|
||||
Quand le prélèvement échoue
|
||||
Alors je reçois un email:
|
||||
"""
|
||||
⚠️ Échec renouvellement abonnement Premium
|
||||
|
||||
Le paiement de votre abonnement a échoué.
|
||||
Raison: Carte bancaire expirée
|
||||
|
||||
Nous allons réessayer automatiquement dans 3 jours.
|
||||
Veuillez mettre à jour vos informations de paiement: [Lien]
|
||||
|
||||
Votre accès Premium reste actif jusqu'au 22 juillet (7 jours).
|
||||
"""
|
||||
|
||||
Scénario: Retry automatique paiement après 3 jours
|
||||
Étant donné que le paiement a échoué le 15 juillet
|
||||
Quand le 18 juillet arrive (J+3)
|
||||
Alors Mangopay/Apple/Google tente automatiquement un nouveau prélèvement
|
||||
Et si le paiement réussit, l'abonnement est renouvelé normalement
|
||||
Et si le paiement échoue encore, un 2ème retry est programmé
|
||||
|
||||
Scénario: Retry automatique paiement après 7 jours
|
||||
Étant donné que 2 tentatives ont échoué (15 juillet et 18 juillet)
|
||||
Quand le 22 juillet arrive (J+7)
|
||||
Alors une 3ème et dernière tentative est effectuée
|
||||
Et si le paiement réussit, l'abonnement est sauvé
|
||||
Et si le paiement échoue, l'abonnement est annulé automatiquement
|
||||
|
||||
Scénario: Annulation automatique après 3 échecs paiement
|
||||
Étant donné que les 3 tentatives de renouvellement ont échoué (J+0, J+3, J+7)
|
||||
Quand la 3ème tentative échoue
|
||||
Alors mon abonnement Premium est annulé automatiquement
|
||||
Et mon statut repasse à "Gratuit"
|
||||
Et je perds accès aux avantages Premium
|
||||
Et je reçois un email d'annulation
|
||||
|
||||
Scénario: Email annulation automatique pour impayé
|
||||
Étant donné que mon abonnement a été annulé pour échec paiement
|
||||
Quand l'annulation devient effective
|
||||
Alors je reçois un email:
|
||||
"""
|
||||
❌ Abonnement Premium annulé
|
||||
|
||||
Votre abonnement a été annulé suite à 3 échecs de paiement.
|
||||
|
||||
Vous repassez en mode gratuit et perdez l'accès à:
|
||||
• Contenus Premium exclusifs
|
||||
• Qualité audio supérieure
|
||||
• Téléchargements illimités
|
||||
|
||||
Pour réactiver Premium, mettez à jour vos informations de paiement: [Lien]
|
||||
"""
|
||||
|
||||
# ===== ANNULATION =====
|
||||
|
||||
Scénario: Annulation self-service dans Settings
|
||||
Étant donné que je veux annuler mon abonnement
|
||||
Quand j'accède à "Paramètres > Abonnement"
|
||||
Alors je vois un bouton "Annuler l'abonnement"
|
||||
Et je peux annuler en 2 clics sans contacter le support
|
||||
|
||||
Scénario: Confirmation avant annulation
|
||||
Étant donné que je clique sur "Annuler l'abonnement"
|
||||
Quand une popup de confirmation apparaît
|
||||
Alors je vois:
|
||||
"""
|
||||
😢 Vous allez annuler votre abonnement Premium
|
||||
|
||||
Vous perdrez l'accès à:
|
||||
• 0 publicité
|
||||
• Contenus Premium exclusifs
|
||||
• Qualité audio supérieure
|
||||
• Téléchargements illimités
|
||||
|
||||
Accès maintenu jusqu'au: 15 juillet 2025 (fin période payée)
|
||||
Pas de remboursement prorata.
|
||||
|
||||
[Confirmer l'annulation] [Rester Premium]
|
||||
```
|
||||
|
||||
Scénario: Accès Premium maintenu jusqu'à fin période payée
|
||||
Étant donné que j'ai annulé mon abonnement le 1er juillet
|
||||
Et que mon abonnement mensuel était valable jusqu'au 15 juillet
|
||||
Quand l'annulation est confirmée
|
||||
Alors je garde l'accès Premium jusqu'au 15 juillet
|
||||
Et à partir du 16 juillet, je repasse en gratuit
|
||||
Et je ne suis pas remboursé pour les 14 jours restants
|
||||
|
||||
Scénario: Justification pas de remboursement prorata
|
||||
Étant donné que l'industrie (Spotify, Netflix, YouTube) ne rembourse pas prorata
|
||||
Quand RoadWave applique la même règle
|
||||
Alors c'est le standard accepté par les utilisateurs
|
||||
Et cela simplifie la gestion comptable
|
||||
Et évite les abus (souscription puis annulation immédiate pour remboursement)
|
||||
|
||||
Scénario: Email confirmation annulation
|
||||
Étant donné que j'ai annulé mon abonnement
|
||||
Quand l'annulation est enregistrée
|
||||
Alors je reçois un email:
|
||||
"""
|
||||
✅ Annulation confirmée
|
||||
|
||||
Votre abonnement Premium a été annulé.
|
||||
|
||||
Accès Premium maintenu jusqu'au: 15 juillet 2025
|
||||
Après cette date, vous repasserez en mode gratuit.
|
||||
|
||||
Pas de remboursement pour la période restante (standard industrie).
|
||||
|
||||
Vous pouvez vous réabonner à tout moment !
|
||||
|
||||
Nous espérons vous revoir bientôt.
|
||||
Réabonner: [Lien]
|
||||
"""
|
||||
|
||||
Scénario: Pas de renouvellement après annulation
|
||||
Étant donné que j'ai annulé mon abonnement le 1er juillet
|
||||
Quand le 15 juillet arrive (date de renouvellement prévue)
|
||||
Alors aucun prélèvement n'est effectué
|
||||
Et mon statut passe automatiquement à "Gratuit"
|
||||
Et je ne reçois pas d'email de renouvellement
|
||||
|
||||
# ===== RÉABONNEMENT =====
|
||||
|
||||
Scénario: Réabonnement possible immédiatement
|
||||
Étant donné que j'ai annulé mon abonnement il y a 5 jours
|
||||
Quand j'accède à la page Premium
|
||||
Alors je peux me réabonner immédiatement
|
||||
Et le processus de paiement est le même que la première fois
|
||||
|
||||
Scénario: Pas de nouvelle période d'essai au réabonnement
|
||||
Étant donné que j'ai annulé mon abonnement il y a 3 mois
|
||||
Quand je me réabonne
|
||||
Alors je paie immédiatement 4.99€ (pas d'essai gratuit)
|
||||
Car RoadWave ne propose jamais d'essai gratuit (ni première fois ni réabonnement)
|
||||
|
||||
Scénario: Offre win-back pour utilisateurs ayant annulé
|
||||
Étant donné que j'ai annulé mon abonnement il y a 1 mois
|
||||
Quand je reçois un email de win-back
|
||||
Alors je vois une offre spéciale:
|
||||
"""
|
||||
🎁 On vous a manqué ?
|
||||
|
||||
Revenez en Premium avec une offre exclusive:
|
||||
-30% sur les 3 premiers mois (3.49€/mois au lieu de 4.99€)
|
||||
|
||||
Offre valable jusqu'au 31 juillet.
|
||||
|
||||
[Réactiver Premium]
|
||||
```
|
||||
|
||||
# ===== ARCHITECTURE DONNÉES =====
|
||||
|
||||
Scénario: Table subscriptions en base PostgreSQL
|
||||
Étant donné qu'un utilisateur souscrit à Premium
|
||||
Quand les données sont enregistrées
|
||||
Alors la table subscriptions contient:
|
||||
```sql
|
||||
CREATE TABLE subscriptions (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id UUID NOT NULL REFERENCES users(id) UNIQUE,
|
||||
mangopay_recurring_payin_id VARCHAR(255), -- Null si IAP
|
||||
mangopay_user_id VARCHAR(255), -- Null si IAP
|
||||
apple_transaction_id VARCHAR(255), -- Null si Mangopay
|
||||
google_purchase_token VARCHAR(255), -- Null si Mangopay
|
||||
status VARCHAR(50) NOT NULL, -- 'active', 'cancelled', 'expired', 'past_due'
|
||||
plan VARCHAR(50) NOT NULL, -- 'monthly', 'yearly'
|
||||
current_period_start TIMESTAMP NOT NULL,
|
||||
current_period_end TIMESTAMP NOT NULL,
|
||||
cancelled_at TIMESTAMP,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
Scénario: Statuts possibles dans subscription.status
|
||||
Étant donné qu'un abonnement peut avoir différents statuts
|
||||
Quand le statut est stocké en base
|
||||
Alors les valeurs possibles sont:
|
||||
| statut | description |
|
||||
| active | Abonnement actif et payé |
|
||||
| cancelled | Annulé par utilisateur (accès jusqu'à fin période) |
|
||||
| expired | Période terminée, pas renouvelé |
|
||||
| past_due | Échec paiement, en retry automatique |
|
||||
|
||||
Scénario: Cache Redis pour vérification Premium temps réel
|
||||
Étant donné qu'un utilisateur lance un contenu
|
||||
Quand l'app vérifie s'il est Premium
|
||||
Alors une clé Redis est consultée:
|
||||
```
|
||||
Key: premium:{user_id}
|
||||
Value: true/false
|
||||
TTL: 1 heure
|
||||
```
|
||||
Et si la clé n'existe pas, elle est recalculée depuis PostgreSQL
|
||||
Et cela garantit des performances <10ms
|
||||
|
||||
Scénario: Refresh cache Redis via webhooks
|
||||
Étant donné qu'un paiement est confirmé par Mangopay/Apple/Google
|
||||
Quand un webhook est reçu par RoadWave
|
||||
Alors le cache Redis premium:{user_id} est mis à jour immédiatement
|
||||
Et l'utilisateur voit son statut Premium activé sans délai
|
||||
|
||||
Scénario: Webhooks Mangopay - PAYIN_NORMAL_SUCCEEDED
|
||||
Étant donné qu'un paiement Mangopay réussit
|
||||
Quand Mangopay envoie le webhook PAYIN_NORMAL_SUCCEEDED
|
||||
Alors RoadWave met à jour subscriptions.status = 'active'
|
||||
Et met à jour current_period_end = NOW() + 1 mois
|
||||
Et refresh le cache Redis premium:{user_id} = true
|
||||
|
||||
Scénario: Webhooks Mangopay - PAYIN_NORMAL_FAILED
|
||||
Étant donné qu'un paiement Mangopay échoue
|
||||
Quand Mangopay envoie le webhook PAYIN_NORMAL_FAILED
|
||||
Alors RoadWave met à jour subscriptions.status = 'past_due'
|
||||
Et programme un retry automatique dans 3 jours
|
||||
Et envoie un email à l'utilisateur
|
||||
|
||||
Scénario: Webhooks Apple - App Store Server Notifications
|
||||
Étant donné qu'un paiement Apple IAP change de statut
|
||||
Quand Apple envoie une notification serveur
|
||||
Alors RoadWave parse la notification (JSON)
|
||||
Et met à jour la subscription en conséquence
|
||||
Et refresh le cache Redis
|
||||
|
||||
Scénario: Webhooks Google - Real-time Developer Notifications
|
||||
Étant donné qu'un paiement Google Play change de statut
|
||||
Quand Google envoie une notification temps réel
|
||||
Alors RoadWave parse la notification (JSON)
|
||||
Et met à jour la subscription en conséquence
|
||||
Et refresh le cache Redis
|
||||
|
||||
# ===== STATISTIQUES ET MONITORING =====
|
||||
|
||||
Scénario: Dashboard admin - Métriques abonnements
|
||||
Étant donné qu'un admin consulte les métriques Premium
|
||||
Quand il accède au dashboard
|
||||
Alors il voit:
|
||||
| métrique | valeur |
|
||||
| Abonnés actifs | 12,547 |
|
||||
| Nouveaux abonnements ce mois | 1,234 |
|
||||
| Annulations ce mois | 287 (2.3%) |
|
||||
| Churn rate mensuel | 2.3% |
|
||||
| MRR (Revenus mensuels récurrents) | 58,890€ |
|
||||
| Taux conversion gratuit → Premium | 8.5% |
|
||||
|
||||
Scénario: Calcul churn rate mensuel
|
||||
Étant donné que 287 utilisateurs ont annulé ce mois
|
||||
Et qu'il y avait 12,547 abonnés au début du mois
|
||||
Quand le churn rate est calculé
|
||||
Alors churn = 287 / 12,547 = 2.3%
|
||||
Et un churn <5% est considéré comme excellent
|
||||
Et RoadWave surveille cette métrique de près
|
||||
|
||||
Scénario: Alerte si churn rate >5%
|
||||
Étant donné que le churn rate mensuel dépasse 5%
|
||||
Quand le système détecte cette anomalie
|
||||
Alors une alerte est envoyée à l'équipe:
|
||||
"""
|
||||
⚠️ Churn rate anormal: 6.2%
|
||||
|
||||
Nombre annulations ce mois: 778
|
||||
Causes principales:
|
||||
- Prix jugé trop élevé: 42%
|
||||
- Utilisation faible: 28%
|
||||
- Concurrent moins cher: 18%
|
||||
- Autre: 12%
|
||||
|
||||
Action recommandée: Enquête satisfaction + offres win-back
|
||||
"""
|
||||
|
||||
Scénario: Enquête satisfaction à l'annulation
|
||||
Étant donné que je viens d'annuler mon abonnement
|
||||
Quand l'annulation est confirmée
|
||||
Alors je vois un questionnaire rapide:
|
||||
"""
|
||||
Pourquoi annulez-vous Premium ?
|
||||
|
||||
☐ Prix trop élevé
|
||||
☐ Je n'utilise pas assez RoadWave
|
||||
☐ Pas assez de contenus exclusifs
|
||||
☐ Problèmes techniques
|
||||
☐ J'ai trouvé une alternative moins chère
|
||||
☐ Autre: [Texte libre]
|
||||
|
||||
[Envoyer] [Ignorer]
|
||||
```
|
||||
Et les réponses aident à améliorer l'offre Premium
|
||||
|
||||
Scénario: Répartition canaux souscription
|
||||
Étant donné qu'un admin analyse les canaux de souscription
|
||||
Quand il consulte les statistiques
|
||||
Alors il voit:
|
||||
| canal | abonnés | % total | revenus/mois |
|
||||
| Web (Mangopay) | 8,234 | 65.6% | 41,088€ |
|
||||
| iOS (Apple) | 2,845 | 22.7% | 17,042€ |
|
||||
| Android (Google)| 1,468 | 11.7% | 8,793€ |
|
||||
Et cela aide à orienter les efforts marketing (inciter web = moins de commission)
|
||||
|
||||
Scénario: Performance vérification Premium <10ms
|
||||
Étant donné que 100 000 utilisateurs consultent des contenus simultanément
|
||||
Quand chaque requête vérifie le statut Premium via Redis
|
||||
Alors le temps de réponse moyen est <10ms
|
||||
Et Redis gère facilement 100 000 requêtes/seconde
|
||||
Et l'expérience utilisateur est fluide
|
||||
|
||||
Scénario: Backup données abonnements
|
||||
Étant donné que les données d'abonnements sont critiques
|
||||
Quand un backup est effectué
|
||||
Alors PostgreSQL est répliqué en temps réel sur un replica
|
||||
Et un snapshot quotidien est stocké sur S3
|
||||
Et en cas de crash, les données peuvent être restaurées <5 minutes
|
||||
@@ -0,0 +1,233 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Multi-devices Premium - Dernier device prend priorité (KISS)
|
||||
En tant que système Premium
|
||||
Je veux appliquer une règle simple: le dernier device à démarrer prend toujours la priorité
|
||||
Afin d'éviter la complexité des exceptions temporelles et géographiques
|
||||
|
||||
Contexte:
|
||||
Étant donné qu'un utilisateur "UserA" a un abonnement Premium actif
|
||||
Et que l'utilisateur possède 2 devices:
|
||||
| device | type |
|
||||
| iPhone_123 | iOS |
|
||||
| iPad_456 | iOS |
|
||||
|
||||
# Règle KISS : Dernier device = Priorité
|
||||
|
||||
Scénario: Transition normale - Device 2 démarre après Device 1
|
||||
Étant donné que "UserA" écoute un contenu sur iPhone_123
|
||||
Et que le heartbeat iPhone_123 est actif (toutes les 30s)
|
||||
Quand "UserA" démarre la lecture sur iPad_456 (5 secondes après)
|
||||
Alors le serveur détecte une session active (iPhone_123)
|
||||
Et envoie un WebSocket close à iPhone_123
|
||||
Et iPhone_123 affiche le message:
|
||||
"""
|
||||
⚠️ Lecture interrompue
|
||||
|
||||
Votre compte est utilisé sur un autre appareil.
|
||||
|
||||
Un seul appareil peut écouter à la fois avec votre compte Premium.
|
||||
|
||||
Le partage de compte viole nos CGU et peut entraîner une suspension.
|
||||
|
||||
[Reprendre ici] [Sécuriser mon compte]
|
||||
"""
|
||||
Et Redis met à jour active_streams:UserA = {device_id: "iPad_456", started_at: timestamp}
|
||||
Et la lecture démarre sur iPad_456
|
||||
|
||||
# Offline connecté internet
|
||||
|
||||
Scénario: Device 1 offline MAIS connecté WiFi - Device 2 coupe Device 1
|
||||
Étant donné que "UserA" écoute un contenu téléchargé en offline sur iPhone_123
|
||||
Mais que iPhone_123 est connecté au WiFi
|
||||
Et que le heartbeat iPhone_123 est envoyé toutes les 30s
|
||||
Quand "UserA" démarre la lecture online sur iPad_456
|
||||
Alors le serveur détecte une session active (iPhone_123, mode offline)
|
||||
Et envoie un WebSocket close à iPhone_123
|
||||
Et iPhone_123 affiche le même message d'interruption
|
||||
Et iPad_456 démarre normalement
|
||||
|
||||
# Offline déconnecté internet (exception technique)
|
||||
|
||||
Scénario: Device 1 vraiment offline (mode avion) - Pas de détection possible
|
||||
Étant donné que "UserA" écoute un contenu offline sur iPhone_123 en mode avion
|
||||
Et qu'aucun heartbeat n'est envoyé (pas de connexion internet)
|
||||
Quand "UserA" démarre la lecture online sur iPad_456
|
||||
Alors le serveur ne détecte AUCUNE session active
|
||||
Car aucun heartbeat depuis iPhone_123
|
||||
Et iPad_456 démarre normalement
|
||||
Et iPhone_123 continue en offline (pas de coupure possible)
|
||||
Et c'est une exception technique acceptable ("Tant pis")
|
||||
|
||||
Scénario: Device 1 offline reconnecte - Heartbeat reprend
|
||||
Étant donné que "UserA" écoutait offline sur iPhone_123 en mode avion
|
||||
Quand iPhone_123 sort du mode avion et se connecte au WiFi
|
||||
Alors le heartbeat reprend immédiatement
|
||||
Et Redis crée/met à jour active_streams:UserA = {device_id: "iPhone_123", started_at: timestamp}
|
||||
Et si iPad_456 démarre, iPhone_123 sera coupé normalement
|
||||
|
||||
# Heartbeat et TTL Redis
|
||||
|
||||
Scénario: Heartbeat toutes les 30s maintient la session active
|
||||
Étant donné que "UserA" écoute sur iPhone_123
|
||||
Quand iPhone_123 envoie un heartbeat toutes les 30 secondes
|
||||
Alors Redis met à jour active_streams:UserA avec TTL 5 minutes
|
||||
Et la session reste active tant que les heartbeats arrivent
|
||||
|
||||
Scénario: Pas de heartbeat pendant 5 min - Session expirée
|
||||
Étant donné que "UserA" écoutait sur iPhone_123
|
||||
Mais que iPhone_123 a été éteint brutalement (pas de heartbeat final)
|
||||
Quand 5 minutes se sont écoulées sans heartbeat
|
||||
Alors Redis expire automatiquement l'entrée active_streams:UserA (TTL)
|
||||
Et iPad_456 peut démarrer sans détecter de conflit
|
||||
|
||||
# Boutons message coupure
|
||||
|
||||
Scénario: Bouton "Reprendre ici" sur device coupé
|
||||
Étant donné que iPhone_123 a été coupé par iPad_456
|
||||
Et que le message d'interruption est affiché sur iPhone_123
|
||||
Quand "UserA" clique sur [Reprendre ici] sur iPhone_123
|
||||
Alors le serveur détecte que iPad_456 est actif
|
||||
Et envoie un WebSocket close à iPad_456
|
||||
Et iPhone_123 reprend la lecture
|
||||
Et iPad_456 affiche le même message d'interruption
|
||||
|
||||
Scénario: Bouton "Sécuriser mon compte" sur device coupé
|
||||
Étant donné que iPhone_123 a été coupé par iPad_456
|
||||
Et que le message d'interruption est affiché sur iPhone_123
|
||||
Quand "UserA" clique sur [Sécuriser mon compte]
|
||||
Alors l'utilisateur est redirigé vers:
|
||||
- Page changement mot de passe
|
||||
- Option "Déconnecter tous les appareils"
|
||||
Et un email de sécurité est envoyé
|
||||
Et un log de sécurité est créé (suspicion piratage)
|
||||
|
||||
# Limite offline 30 jours
|
||||
|
||||
Scénario: Contenus offline expirés >30j - Force reconnexion
|
||||
Étant donné que "UserA" a téléchargé 50 contenus il y a 32 jours
|
||||
Et qu'il n'a pas reconnecté son device depuis 32 jours
|
||||
Quand "UserA" essaie d'écouter un contenu offline
|
||||
Alors un message s'affiche:
|
||||
"""
|
||||
Contenus expirés (>30 jours)
|
||||
|
||||
Reconnectez-vous au WiFi pour renouveler vos contenus.
|
||||
"""
|
||||
Et la lecture est bloquée
|
||||
Et cela force la reconnexion (détection multi-devices redevient possible)
|
||||
|
||||
Scénario: Notification J-3 avant expiration contenus offline
|
||||
Étant donné que "UserA" a des contenus offline qui expirent dans 3 jours
|
||||
Quand le système détecte l'approche de l'expiration
|
||||
Alors une notification push est envoyée:
|
||||
"""
|
||||
⚠️ 10 contenus offline expirent dans 3 jours
|
||||
|
||||
Reconnectez-vous au WiFi pour renouveler automatiquement.
|
||||
"""
|
||||
|
||||
# Détection abus post-MVP
|
||||
|
||||
Scénario: Monitoring patterns suspects - Flag modération manuelle
|
||||
Étant donné que le système monitore les patterns d'utilisation
|
||||
Quand "UserA" change de device >10 fois par jour pendant 7 jours
|
||||
Alors un flag de suspicion est créé dans le dashboard modération
|
||||
Et un email d'investigation est envoyé à "UserA":
|
||||
"""
|
||||
Activité suspecte détectée sur votre compte
|
||||
|
||||
Nous avons détecté des changements fréquents d'appareil.
|
||||
|
||||
Si ce n'est pas vous, sécurisez votre compte immédiatement.
|
||||
|
||||
[Sécuriser mon compte] [Contacter le support]
|
||||
"""
|
||||
Mais aucune action automatique n'est prise (évite faux positifs)
|
||||
|
||||
Scénario: Connexions alternées villes éloignées - Flag modération
|
||||
Étant donné que le système monitore les localisations approximatives
|
||||
Quand "UserA" se connecte à Paris à 10h puis Marseille à 10h30 (même jour)
|
||||
Et que ce pattern se répète 5 fois en 1 semaine
|
||||
Alors un flag de suspicion est créé
|
||||
Et un modérateur humain enquête manuellement
|
||||
Mais aucun blocage automatique (évite faux positifs: TGV légitime)
|
||||
|
||||
Scénario: Partage de compte confirmé - Suspension 7 jours
|
||||
Étant donné qu'un modérateur confirme manuellement un partage de compte
|
||||
Et que les preuves sont claires (signalements users + patterns suspects)
|
||||
Quand le modérateur applique la sanction
|
||||
Alors le compte est suspendu 7 jours
|
||||
Et un email de warning est envoyé:
|
||||
"""
|
||||
⚠️ Suspension de compte (7 jours)
|
||||
|
||||
Le partage de compte Premium viole nos CGU.
|
||||
|
||||
Votre compte est suspendu jusqu'au [Date].
|
||||
|
||||
En cas de récidive, le compte sera définitivement fermé.
|
||||
"""
|
||||
|
||||
Scénario: Récidive partage de compte - Ban définitif
|
||||
Étant donné qu'un compte a déjà été suspendu 7 jours pour partage
|
||||
Et que le partage continue après la réactivation
|
||||
Quand le modérateur confirme la récidive
|
||||
Alors le compte est définitivement fermé (ban)
|
||||
Et un email de fermeture définitive est envoyé
|
||||
Et le remboursement pro-rata de l'abonnement est effectué
|
||||
|
||||
# Cas d'usage réels
|
||||
|
||||
Scénario: User change de pièce (voiture → maison) rapidement
|
||||
Étant donné que "UserA" écoute dans sa voiture sur iPhone_123
|
||||
Quand il arrive chez lui et passe sur iPad_456 après 30 secondes
|
||||
Alors iPhone_123 est coupé immédiatement
|
||||
Et iPad_456 prend le relai
|
||||
Et l'utilisateur comprend le message (device unique)
|
||||
|
||||
Scénario: User oublie son téléphone en lecture et part
|
||||
Étant donné que "UserA" écoute sur iPhone_123
|
||||
Et qu'il part en laissant iPhone_123 en lecture
|
||||
Quand il démarre la lecture sur iPad_456 au travail
|
||||
Alors iPhone_123 est coupé à distance (WebSocket close)
|
||||
Et iPad_456 prend la priorité
|
||||
|
||||
Scénario: TGV Paris → Lyon (légitime) - Pas de faux positif
|
||||
Étant donné que "UserA" prend le TGV Paris → Lyon
|
||||
Et qu'il écoute pendant tout le trajet sur iPhone_123
|
||||
Quand le système détecte un changement de localisation (GPS)
|
||||
Alors aucun flag de suspicion n'est créé
|
||||
Car c'est un déplacement continu légitime
|
||||
Et aucune action automatique n'est prise
|
||||
|
||||
# Implémentation technique Redis
|
||||
|
||||
Scénario: Stockage Redis - Structure active_streams
|
||||
Étant donné que "UserA" démarre la lecture sur iPhone_123
|
||||
Quand le heartbeat est envoyé au serveur
|
||||
Alors Redis stocke:
|
||||
| clé | valeur |
|
||||
| active_streams:UserA | {"device_id": "iPhone_123", "started_at": 1707311400} |
|
||||
Et un TTL de 5 minutes (300 secondes) est défini
|
||||
Et la clé expire automatiquement si pas de heartbeat
|
||||
|
||||
Scénario: Mise à jour Redis à chaque heartbeat
|
||||
Étant donné qu'une session active existe sur iPhone_123
|
||||
Quand un heartbeat est envoyé toutes les 30 secondes
|
||||
Alors Redis met à jour le TTL à 5 minutes
|
||||
Et started_at reste inchangé (timestamp initial)
|
||||
Et la session reste active indéfiniment tant que les heartbeats arrivent
|
||||
|
||||
# Justification KISS
|
||||
|
||||
Scénario: Comparaison avec règle complexe - Avantages KISS
|
||||
Étant donné qu'on compare la règle KISS avec une règle complexe (exceptions temporelles, GPS, etc.)
|
||||
Quand on évalue les avantages
|
||||
Alors les avantages KISS sont:
|
||||
| avantage | description |
|
||||
| Simplicité technique | Pas de tracking GPS précis, pas de calcul distances |
|
||||
| Pas de faux positifs | TGV légitime ne déclenche pas d'alerte automatique |
|
||||
| Assume bonne foi | Majorité users honnêtes, gestion réactive suffit |
|
||||
| Message dissuasif clair | Avertissement CGU dans message, option sécurité |
|
||||
| Protection revenus créateurs | 1 abonnement = 1 personne = 1 écoute active |
|
||||
| UX claire | User comprend immédiatement le comportement |
|
||||
@@ -0,0 +1,279 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Multi-devices et détection simultanée
|
||||
En tant qu'abonné Premium
|
||||
Je veux utiliser mon compte sur plusieurs appareils
|
||||
Mais limité à 1 seul stream actif à la fois pour éviter le partage abusif
|
||||
|
||||
Contexte:
|
||||
Étant donné que je suis un utilisateur Premium actif
|
||||
Et que mon compte est valide
|
||||
|
||||
# ===== LIMITE 1 STREAM ACTIF =====
|
||||
|
||||
Scénario: 1 seul stream actif autorisé par compte
|
||||
Étant donné que je n'écoute rien actuellement
|
||||
Quand je lance un contenu sur mon iPhone
|
||||
Alors le stream démarre normalement
|
||||
Et Redis enregistre: active_streams:{user_id} = {device_id: "iPhone", started_at: timestamp}
|
||||
|
||||
Scénario: Détection connexion simultanée - Arrêt premier device
|
||||
Étant donné que j'écoute un contenu sur mon iPhone
|
||||
Quand je lance un contenu sur mon iPad
|
||||
Alors le système détecte une session active sur iPhone
|
||||
Et la lecture sur iPhone est arrêtée immédiatement (WebSocket close)
|
||||
Et je vois sur iPhone: "Lecture interrompue : votre compte est utilisé sur un autre appareil"
|
||||
Et la lecture démarre sur iPad normalement
|
||||
|
||||
Scénario: Message explicite sur device interrompu
|
||||
Étant donné que ma lecture sur iPhone vient d'être interrompue
|
||||
Quand je regarde l'écran de mon iPhone
|
||||
Alors je vois une overlay avec le message:
|
||||
"""
|
||||
🔴 Lecture interrompue
|
||||
|
||||
Votre compte Premium est utilisé sur un autre appareil.
|
||||
|
||||
Un seul stream actif est autorisé à la fois pour protéger
|
||||
les revenus des créateurs et éviter le partage de compte.
|
||||
"""
|
||||
Et un bouton "Reprendre ici" est disponible
|
||||
|
||||
Scénario: Reprendre lecture sur device interrompu
|
||||
Étant donné que ma lecture sur iPhone a été interrompue
|
||||
Et que je veux reprendre sur iPhone
|
||||
Quand je clique sur "Reprendre ici"
|
||||
Alors la lecture démarre sur iPhone
|
||||
Et l'iPad est à son tour interrompu avec le même message
|
||||
Et le "ping-pong" entre devices est possible (mais pénible)
|
||||
|
||||
# ===== IMPLÉMENTATION TECHNIQUE REDIS =====
|
||||
|
||||
Scénario: Enregistrement session active dans Redis
|
||||
Étant donné que je lance un contenu sur mon iPhone
|
||||
Quand la lecture démarre
|
||||
Alors une entrée Redis est créée:
|
||||
```
|
||||
Key: active_streams:{user_id}
|
||||
Value: {
|
||||
"device_id": "iPhone-ABC123",
|
||||
"started_at": "2025-06-15T14:30:00Z",
|
||||
"content_id": "xyz789"
|
||||
}
|
||||
TTL: 300 secondes (5 minutes)
|
||||
```
|
||||
|
||||
Scénario: Heartbeat toutes les 30 secondes pour maintenir session
|
||||
Étant donné que j'écoute un contenu sur mon iPhone
|
||||
Quand 30 secondes s'écoulent
|
||||
Alors l'app envoie un heartbeat au serveur
|
||||
Et le serveur refresh le TTL Redis à 300 secondes
|
||||
Et la session reste active
|
||||
|
||||
Scénario: Session considérée morte après 5 minutes sans heartbeat
|
||||
Étant donné que j'écoute un contenu sur mon iPhone
|
||||
Mais que l'app crash ou que le réseau coupe
|
||||
Quand 5 minutes s'écoulent sans heartbeat
|
||||
Alors l'entrée Redis expire automatiquement (TTL atteint)
|
||||
Et je peux relancer sur n'importe quel device sans conflit
|
||||
|
||||
Scénario: Vérification session avant démarrage lecture
|
||||
Étant donné que je veux lancer un contenu sur mon iPad
|
||||
Quand j'appuie sur Play
|
||||
Alors le serveur vérifie Redis: active_streams:{user_id}
|
||||
Et si une session existe sur un autre device, elle est tuée
|
||||
Et la nouvelle session iPad est enregistrée dans Redis
|
||||
|
||||
Scénario: Gestion multi-utilisateurs simultanés
|
||||
Étant donné que 100 000 utilisateurs Premium écoutent simultanément
|
||||
Quand Redis stocke 100 000 entrées active_streams
|
||||
Alors chaque entrée a un TTL de 5 minutes
|
||||
Et Redis gère facilement cette charge (~10 MB de RAM)
|
||||
Et les vérifications sont quasi-instantanées (O(1))
|
||||
|
||||
# ===== EXCEPTIONS ET CAS PARTICULIERS =====
|
||||
|
||||
Scénario: Contenus téléchargés (offline) ne comptent pas comme stream
|
||||
Étant donné que j'ai téléchargé 20 contenus en mode offline
|
||||
Quand j'écoute un contenu téléchargé sur mon iPhone sans réseau
|
||||
Alors aucune session active n'est enregistrée dans Redis
|
||||
Et je peux écouter offline pendant qu'un autre device stream online
|
||||
Car le contenu offline ne consomme pas de bande passante serveur
|
||||
|
||||
Scénario: Transition rapide device <10s tolérée
|
||||
Étant donné que j'écoute dans ma voiture sur mon iPhone
|
||||
Et que j'arrive chez moi
|
||||
Quand je lance la lecture sur mon iPad dans les 10 secondes
|
||||
Alors la transition est considérée comme un changement de device légitime
|
||||
Et aucun message d'erreur n'est affiché sur iPhone
|
||||
Et la lecture reprend exactement où j'étais sur iPad
|
||||
|
||||
Scénario: Détection transition rapide via timestamps
|
||||
Étant donné que la session iPhone a started_at = 14:30:00
|
||||
Quand je lance sur iPad à 14:30:05 (5 secondes après)
|
||||
Alors le serveur détecte: diff = 5s < 10s
|
||||
Et applique une "graceful transition" (pas de message d'erreur iPhone)
|
||||
Et Redis met à jour: active_streams:{user_id} = {device_id: "iPad", ...}
|
||||
|
||||
Scénario: Plusieurs devices disponibles mais 1 seul actif
|
||||
Étant donné que je possède:
|
||||
| device | status |
|
||||
| iPhone | Installé |
|
||||
| iPad | Installé |
|
||||
| MacBook (web) | Connecté |
|
||||
| Android (conjoint)| Installé |
|
||||
Quand je lance un stream sur n'importe quel device
|
||||
Alors seulement 1 peut être actif à la fois
|
||||
Et les autres devices sont en "standby"
|
||||
|
||||
# ===== JUSTIFICATIONS =====
|
||||
|
||||
Scénario: Justification anti-partage compte
|
||||
Étant donné qu'un utilisateur Premium partage son compte avec un ami
|
||||
Quand les 2 personnes essaient d'écouter simultanément
|
||||
Alors la lecture est constamment interrompue sur l'un ou l'autre
|
||||
Et l'expérience devient inutilisable
|
||||
Et cela décourage fortement le partage de compte
|
||||
|
||||
Scénario: Justification protection revenus créateurs
|
||||
Étant donné que 1 abonnement Premium = 4.99€/mois
|
||||
Quand 70% sont reversés aux créateurs (3.49€)
|
||||
Alors les créateurs sont rémunérés pour 1 personne
|
||||
Et si 2 personnes utilisent le même compte simultanément, c'est injuste
|
||||
Et la limite 1 stream protège l'équité du système
|
||||
|
||||
Scénario: Justification UX claire
|
||||
Étant donné qu'un stream est interrompu sur un device
|
||||
Quand l'utilisateur voit le message explicite
|
||||
Alors il comprend immédiatement pourquoi (autre device actif)
|
||||
Et il peut choisir de reprendre sur le device actuel ou l'autre
|
||||
Et il n'y a pas de confusion ou frustration
|
||||
|
||||
Scénario: Comparaison avec Spotify (limite 1 stream)
|
||||
Étant donné que Spotify Premium limite aussi à 1 stream actif
|
||||
Quand RoadWave applique la même règle
|
||||
Alors les utilisateurs connaissent déjà ce comportement
|
||||
Et cela paraît normal et accepté par l'industrie
|
||||
|
||||
Scénario: Comparaison avec Netflix (plusieurs streams selon formule)
|
||||
Étant donné que Netflix permet 1-4 streams selon la formule
|
||||
Quand RoadWave limite à 1 stream pour tous
|
||||
Alors c'est plus strict que Netflix
|
||||
Mais Netflix cible le foyer familial (TV partagée)
|
||||
Alors que RoadWave cible l'individu conducteur (usage personnel)
|
||||
|
||||
# ===== MONITORING ET DÉTECTION ABUS =====
|
||||
|
||||
Scénario: Détection pattern suspect - Changements devices fréquents
|
||||
Étant donné qu'un utilisateur change de device 50 fois en 1 heure
|
||||
Quand le système détecte ce pattern anormal
|
||||
Alors une alerte est générée pour l'équipe modération
|
||||
Et le compte peut être marqué pour surveillance
|
||||
Et si abus confirmé, suspension possible
|
||||
|
||||
Scénario: Logs des changements de device
|
||||
Étant donné que je change de device plusieurs fois par jour
|
||||
Quand les changements sont loggés
|
||||
Alors chaque événement est enregistré:
|
||||
| timestamp | from_device | to_device | content_id |
|
||||
| 2025-06-15 08:30:00 | null | iPhone | abc123 |
|
||||
| 2025-06-15 09:15:00 | iPhone | iPad | def456 |
|
||||
| 2025-06-15 18:30:00 | iPad | iPhone | ghi789 |
|
||||
Et ces logs aident à détecter les partages de compte
|
||||
|
||||
Scénario: Métriques admin - Changements devices par utilisateur
|
||||
Étant donné qu'un admin consulte les métriques de streaming
|
||||
Quand il accède au dashboard
|
||||
Alors il voit:
|
||||
| métrique | valeur |
|
||||
| Utilisateurs Premium actifs | 12,547 |
|
||||
| Changements de device/jour (médiane) | 2 |
|
||||
| Utilisateurs >10 changements/jour | 47 (0.4%) |
|
||||
| Comptes suspects (>20 changements/j) | 3 |
|
||||
|
||||
Scénario: Email d'avertissement si changements excessifs
|
||||
Étant donné que je change de device 30 fois par jour pendant 3 jours
|
||||
Quand le système détecte ce pattern
|
||||
Alors je reçois un email d'avertissement:
|
||||
"""
|
||||
⚠️ Activité inhabituelle détectée sur votre compte
|
||||
|
||||
Nous avons détecté un nombre anormalement élevé de changements de device (30/jour).
|
||||
|
||||
Rappel: Le partage de compte Premium est interdit selon nos CGU.
|
||||
Un seul stream actif est autorisé à la fois.
|
||||
|
||||
Si cette activité continue, votre compte pourra être suspendu.
|
||||
"""
|
||||
|
||||
Scénario: Suspension compte après avertissement ignoré
|
||||
Étant donné que j'ai reçu un email d'avertissement il y a 7 jours
|
||||
Mais que je continue à changer de device 30 fois par jour
|
||||
Quand l'équipe modération examine le compte
|
||||
Alors mon compte Premium peut être suspendu pour partage abusif
|
||||
Et je reçois un email de suspension avec justification
|
||||
|
||||
# ===== SUPPORT UTILISATEUR =====
|
||||
|
||||
Scénario: FAQ - Pourquoi ma lecture s'arrête quand j'utilise un autre device ?
|
||||
Étant donné que je consulte la FAQ Premium
|
||||
Quand je cherche "lecture interrompue"
|
||||
Alors je trouve la réponse:
|
||||
"""
|
||||
Q: Pourquoi ma lecture s'arrête quand j'utilise un autre appareil ?
|
||||
|
||||
R: Votre abonnement Premium autorise 1 seul stream actif à la fois.
|
||||
Si vous lancez la lecture sur un autre appareil, le premier est automatiquement arrêté.
|
||||
|
||||
Pourquoi cette limite ?
|
||||
- Protéger les revenus des créateurs (1 abonnement = 1 personne)
|
||||
- Éviter le partage de compte abusif
|
||||
|
||||
Vous pouvez utiliser autant d'appareils que vous voulez, mais un seul peut lire à la fois.
|
||||
"""
|
||||
|
||||
Scénario: Support - Utilisateur pense être piraté
|
||||
Étant donné qu'un utilisateur voit constamment "Lecture interrompue"
|
||||
Et qu'il pense que son compte est piraté
|
||||
Quand il contacte le support
|
||||
Alors le support vérifie les logs de changements de device
|
||||
Et peut identifier les devices (iPhone, iPad perso vs iPhone inconnu)
|
||||
Et conseille de changer le mot de passe si device inconnu détecté
|
||||
|
||||
Scénario: Changement mot de passe déconnecte tous les devices
|
||||
Étant donné que je pense que mon compte est compromis
|
||||
Quand je change mon mot de passe
|
||||
Alors tous mes devices sont déconnectés immédiatement
|
||||
Et les sessions actives dans Redis sont supprimées
|
||||
Et je dois me reconnecter sur chaque device
|
||||
Et cela sécurise mon compte
|
||||
|
||||
# ===== TESTS TECHNIQUES =====
|
||||
|
||||
Scénario: Test charge - 100 000 vérifications/seconde
|
||||
Étant donné que 100 000 utilisateurs Premium lancent des contenus
|
||||
Quand chaque lancement vérifie Redis (GET active_streams:{user_id})
|
||||
Alors Redis peut gérer facilement 100 000 requêtes/seconde
|
||||
Et le temps de réponse moyen est <1ms
|
||||
Et aucun ralentissement n'est constaté
|
||||
|
||||
Scénario: Test failover Redis
|
||||
Étant donné que le serveur Redis principal tombe en panne
|
||||
Quand le failover automatique vers le replica Redis s'active
|
||||
Alors les sessions actives peuvent être perdues temporairement (max 5 min)
|
||||
Mais les utilisateurs peuvent relancer immédiatement
|
||||
Et l'impact est minimal (pas de perte de données critiques)
|
||||
|
||||
Scénario: Test concurrence - Lancement simultané 2 devices
|
||||
Étant donné que je lance exactement au même instant sur iPhone et iPad
|
||||
Quand les 2 requêtes arrivent en parallèle au serveur
|
||||
Alors Redis utilise un lock (SETNX) pour atomicité
|
||||
Et 1 seul device gagne (par exemple iPhone)
|
||||
Et l'autre device (iPad) reçoit immédiatement une erreur
|
||||
Et l'utilisateur peut retry sur iPad si souhaité
|
||||
|
||||
Scénario: Nettoyage automatique sessions expirées
|
||||
Étant donné que 1000 sessions Redis ont expiré (TTL atteint)
|
||||
Quand Redis supprime automatiquement ces entrées
|
||||
Alors la mémoire est libérée
|
||||
Et les nouveaux streams peuvent démarrer sans conflit
|
||||
Et aucune intervention manuelle n'est nécessaire
|
||||
254
docs/domains/premium/features/premium/offre-tarification.feature
Normal file
254
docs/domains/premium/features/premium/offre-tarification.feature
Normal file
@@ -0,0 +1,254 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Offre et tarification Premium
|
||||
En tant qu'utilisateur
|
||||
Je veux pouvoir souscrire à un abonnement Premium
|
||||
Afin de profiter d'une expérience sans publicité avec des avantages exclusifs
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est disponible
|
||||
Et que je suis connecté en tant qu'utilisateur
|
||||
|
||||
# ===== FORMULES DISPONIBLES =====
|
||||
|
||||
Scénario: Formule mensuelle à 4.99€/mois
|
||||
Étant donné que je consulte les offres Premium
|
||||
Quand je vois la formule mensuelle
|
||||
Alors le prix affiché est 4.99€/mois
|
||||
Et il n'y a aucune réduction
|
||||
Et le prix effectif par mois est 4.99€
|
||||
|
||||
Scénario: Formule annuelle à 49.99€/an (2 mois offerts)
|
||||
Étant donné que je consulte les offres Premium
|
||||
Quand je vois la formule annuelle
|
||||
Alors le prix affiché est 49.99€/an
|
||||
Et l'économie affichée est "2 mois offerts"
|
||||
Et le prix effectif par mois est 4.16€
|
||||
Et je vois le badge "Meilleure offre"
|
||||
|
||||
Scénario: Calcul économie formule annuelle
|
||||
Étant donné que la formule mensuelle coûte 4.99€/mois
|
||||
Quand je calcule le coût annuel en mensuel
|
||||
Alors 12 mois × 4.99€ = 59.88€/an
|
||||
Et la formule annuelle coûte 49.99€
|
||||
Et l'économie est de 9.89€ (≈ 2 mois gratuits)
|
||||
Et la réduction est de 16.5%
|
||||
|
||||
Scénario: Pas d'essai gratuit disponible
|
||||
Étant donné que je consulte les offres Premium
|
||||
Quand je recherche une option "Essai gratuit"
|
||||
Alors aucune option d'essai gratuit n'est proposée
|
||||
Et je dois payer dès le premier jour pour accéder au Premium
|
||||
|
||||
Scénario: Justification absence essai gratuit - Anti-abus vacances
|
||||
Étant donné que RoadWave ne propose pas d'essai gratuit
|
||||
Quand un utilisateur envisage un road trip de 14 jours
|
||||
Alors il ne peut pas s'abonner pour l'essai gratuit puis annuler
|
||||
Et cela évite les inscriptions opportunistes
|
||||
Et protège les revenus des créateurs
|
||||
|
||||
Scénario: Justification absence essai gratuit - Protection revenus créateurs
|
||||
Étant donné qu'un utilisateur Premium écoute des contenus
|
||||
Quand il génère des écoutes dès le jour 1
|
||||
Alors les créateurs sont rémunérés immédiatement (70% de 4.99€)
|
||||
Et il n'y a pas de "période gratuite" sans rémunération créateurs
|
||||
|
||||
Scénario: Justification absence essai gratuit - Simplicité
|
||||
Étant donné que RoadWave gère les abonnements
|
||||
Quand il n'y a pas d'essai gratuit
|
||||
Alors pas de gestion complexe de période trial
|
||||
Et pas de workflow de conversion trial → payant
|
||||
Et cela réduit la complexité technique
|
||||
|
||||
Scénario: Justification absence essai gratuit - Engagement
|
||||
Étant donné qu'un utilisateur paie dès le début
|
||||
Quand il souscrit à Premium
|
||||
Alors il est plus engagé qu'un utilisateur en essai gratuit
|
||||
Et le taux de churn est généralement plus faible
|
||||
Et la lifetime value (LTV) est plus élevée
|
||||
|
||||
Scénario: Pas de partage familial au MVP
|
||||
Étant donné que je consulte les offres Premium
|
||||
Quand je recherche une option "Famille" ou "Partage"
|
||||
Alors aucune option de partage familial n'est disponible
|
||||
Et seuls les abonnements individuels sont proposés
|
||||
|
||||
Scénario: Justification absence partage familial - Complexité technique
|
||||
Étant donné que le partage familial nécessite:
|
||||
| fonctionnalité | complexité |
|
||||
| Gestion invitations | Moyenne |
|
||||
| Validation liens famille | Moyenne |
|
||||
| Limite devices par membre | Élevée |
|
||||
| Dashboard admin famille | Élevée |
|
||||
Quand RoadWave évalue le ROI
|
||||
Alors le coût dev/support est trop élevé pour le MVP
|
||||
Et la fonctionnalité est reportée post-MVP
|
||||
|
||||
Scénario: Justification absence partage familial - Risque abus
|
||||
Étant donné qu'une offre famille permet 5-6 membres
|
||||
Quand il n'y a pas de vérification stricte de lien familial
|
||||
Alors des "familles" de 6 inconnus pourraient se former
|
||||
Et cela réduirait fortement les revenus (6 personnes pour 1 abonnement)
|
||||
|
||||
Scénario: Justification absence partage familial - Cible individuelle
|
||||
Étant donné que RoadWave cible principalement les conducteurs
|
||||
Quand chaque conducteur utilise l'app individuellement en voiture
|
||||
Alors le besoin de partage familial est limité
|
||||
Et la plupart des utilisateurs sont des individus (pas des familles)
|
||||
|
||||
Scénario: Post-MVP - Offre Famille à 9.99€/mois pour 5 comptes
|
||||
Étant donné que RoadWave envisage une offre Famille post-MVP
|
||||
Quand la fonctionnalité est spécifiée
|
||||
Alors le prix serait 9.99€/mois pour 5 comptes
|
||||
Et cela représente 2€/mois/personne
|
||||
Mais cette offre n'est pas disponible au MVP
|
||||
|
||||
Scénario: Comparaison tarif - Spotify à 10.99€/mois
|
||||
Étant donné que Spotify Premium coûte 10.99€/mois
|
||||
Quand RoadWave fixe son prix à 4.99€/mois
|
||||
Alors RoadWave est 54.5% moins cher que Spotify
|
||||
Et cela positionne RoadWave comme très accessible
|
||||
|
||||
Scénario: Comparaison tarif - YouTube Premium à 11.99€/mois
|
||||
Étant donné que YouTube Premium coûte 11.99€/mois
|
||||
Quand RoadWave fixe son prix à 4.99€/mois
|
||||
Alors RoadWave est 58.4% moins cher que YouTube Premium
|
||||
Et cela est un argument commercial fort
|
||||
|
||||
Scénario: Comparaison tarif - Apple Music à 10.99€/mois
|
||||
Étant donné qu'Apple Music coûte 10.99€/mois
|
||||
Quand RoadWave fixe son prix à 4.99€/mois
|
||||
Alors RoadWave est 54.5% moins cher qu'Apple Music
|
||||
Et cela attire les utilisateurs sensibles au prix
|
||||
|
||||
Scénario: Justification tarif bas - Cible conducteurs quotidiens
|
||||
Étant donné que RoadWave cible les trajets quotidiens domicile-travail
|
||||
Quand le prix est fixé à 4.99€/mois
|
||||
Alors c'est un budget raisonnable pour un conducteur
|
||||
Et équivalent à ~1-2 cafés/mois
|
||||
Et psychologiquement acceptable pour un usage quotidien
|
||||
|
||||
Scénario: Justification formule annuelle - Engagement long terme
|
||||
Étant donné que la formule annuelle offre 2 mois gratuits
|
||||
Quand un utilisateur souscrit pour 1 an
|
||||
Alors il s'engage sur le long terme
|
||||
Et RoadWave sécurise 49.99€ de revenus immédiatement
|
||||
Et le cash flow est amélioré
|
||||
|
||||
Scénario: Justification formule annuelle - Réduction churn
|
||||
Étant donné qu'un utilisateur paie 49.99€ pour l'année
|
||||
Quand il envisage d'arrêter après 3 mois
|
||||
Alors il a déjà payé pour 12 mois
|
||||
Et il continuera probablement à utiliser l'app
|
||||
Et le taux de churn est réduit significativement
|
||||
|
||||
Scénario: Affichage comparatif des deux formules
|
||||
Étant donné que je consulte la page Premium
|
||||
Quand je vois les deux formules côte à côte
|
||||
Alors je vois:
|
||||
```
|
||||
┌─────────────────────────────────┐ ┌─────────────────────────────────┐
|
||||
│ MENSUEL │ │ ANNUEL ⭐ Meilleure offre │
|
||||
│ │ │ │
|
||||
│ 4.99€/mois │ │ 49.99€/an │
|
||||
│ │ │ soit 4.16€/mois │
|
||||
│ Engagement 1 mois │ │ │
|
||||
│ │ │ 💰 2 mois offerts ! │
|
||||
│ │ │ Économie: 9.89€/an │
|
||||
│ │ │ │
|
||||
│ [S'abonner] │ │ [S'abonner] │
|
||||
└─────────────────────────────────┘ └─────────────────────────────────┘
|
||||
```
|
||||
|
||||
Scénario: Mise en avant formule annuelle
|
||||
Étant donné que je consulte la page Premium
|
||||
Quand je vois les deux formules
|
||||
Alors la formule annuelle a un badge "Meilleure offre" ⭐
|
||||
Et elle est visuellement mise en avant (bordure colorée, taille plus grande)
|
||||
Et l'économie de 2 mois est affichée en gros
|
||||
Et cela incite à choisir la formule annuelle
|
||||
|
||||
Scénario: Lien "Pourquoi pas d'essai gratuit ?" en FAQ
|
||||
Étant donné que je consulte la page Premium
|
||||
Quand je clique sur "FAQ"
|
||||
Alors je vois une question "Pourquoi pas d'essai gratuit ?"
|
||||
Et la réponse explique:
|
||||
"""
|
||||
RoadWave ne propose pas d'essai gratuit pour 3 raisons:
|
||||
|
||||
1. Protection des créateurs: Vos écoutes rémunèrent les créateurs dès le premier jour.
|
||||
2. Engagement: Un abonnement payant dès le début garantit une meilleure expérience.
|
||||
3. Anti-abus: Éviter les inscriptions opportunistes (essai avant vacances puis annulation).
|
||||
|
||||
Le prix de 4.99€/mois reste très accessible (moitié prix de Spotify/YouTube Premium).
|
||||
"""
|
||||
|
||||
Scénario: A/B test formule annuelle (post-MVP)
|
||||
Étant donné que RoadWave veut optimiser la conversion annuelle
|
||||
Quand un A/B test est lancé
|
||||
Alors groupe A voit "2 mois offerts" (économie en durée)
|
||||
Et groupe B voit "Économisez 9.89€" (économie en argent)
|
||||
Et les taux de souscription sont mesurés
|
||||
Et le message le plus performant est déployé
|
||||
|
||||
Scénario: Promo temporaire exceptionnelle (Black Friday, etc.)
|
||||
Étant donné que c'est le Black Friday
|
||||
Quand une promo temporaire est activée
|
||||
Alors la formule annuelle peut passer à 39.99€/an (au lieu de 49.99€)
|
||||
Et l'économie affichée est "4 mois offerts !"
|
||||
Et la promo dure 3 jours uniquement
|
||||
Et cela génère un pic de souscriptions
|
||||
|
||||
Scénario: Code promo partenariat influenceur
|
||||
Étant donné qu'un influenceur promeut RoadWave
|
||||
Quand il partage un code promo "INFLUENCEUR20"
|
||||
Alors les utilisateurs obtiennent -20% sur le premier mois (3.99€ au lieu de 4.99€)
|
||||
Et le code est valable 1 mois
|
||||
Et les conversions sont trackées par code promo
|
||||
|
||||
Scénario: Statistiques admin - Répartition formules
|
||||
Étant donné qu'un admin consulte les métriques d'abonnements
|
||||
Quand il accède au dashboard
|
||||
Alors il voit:
|
||||
| métrique | valeur |
|
||||
| Abonnés Premium total | 12,547 |
|
||||
| Abonnés mensuels | 7,234 (58%)|
|
||||
| Abonnés annuels | 5,313 (42%)|
|
||||
| Revenus mensuels récurrents | 58,890€ |
|
||||
Et ces données aident à piloter la stratégie tarifaire
|
||||
|
||||
Scénario: Calcul revenus mensuels récurrents (MRR)
|
||||
Étant donné que RoadWave a:
|
||||
| formule | nombre abonnés | prix |
|
||||
| Mensuel | 7,234 | 4.99€/mois|
|
||||
| Annuel | 5,313 | 49.99€/an |
|
||||
Quand le MRR est calculé
|
||||
Alors MRR mensuel = 7,234 × 4.99€ = 36,098€
|
||||
Et MRR annuel ramené au mois = 5,313 × 49.99€ / 12 = 22,139€
|
||||
Et MRR total = 58,237€/mois
|
||||
|
||||
Scénario: Projection revenus annuels (ARR)
|
||||
Étant donné que le MRR est de 58,237€
|
||||
Quand l'ARR est calculé
|
||||
Alors ARR = 58,237€ × 12 = 698,844€/an
|
||||
Et cela aide à évaluer la valorisation de l'entreprise
|
||||
|
||||
Scénario: Affichage prix TTC (TVA incluse)
|
||||
Étant donné que RoadWave est une plateforme française
|
||||
Quand les prix sont affichés
|
||||
Alors tous les prix sont TTC (TVA 20% incluse)
|
||||
Et le prix 4.99€ inclut déjà la TVA
|
||||
Et cela respecte la réglementation française
|
||||
|
||||
Scénario: Performance page Premium avec cache
|
||||
Étant donné que la page Premium est consultée fréquemment
|
||||
Quand un utilisateur charge la page
|
||||
Alors les prix et avantages sont servis depuis un cache CDN
|
||||
Et le temps de chargement est <200ms
|
||||
Et cela garantit une expérience fluide
|
||||
|
||||
Scénario: Localisation prix selon pays (post-MVP)
|
||||
Étant donné que RoadWave se lance à l'international post-MVP
|
||||
Quand un utilisateur se connecte depuis l'Allemagne
|
||||
Alors les prix peuvent être ajustés (ex: 4.99€ en France, 4.49€ en Pologne)
|
||||
Et cela respecte le pouvoir d'achat local
|
||||
Mais cette fonctionnalité n'est pas au MVP (France uniquement)
|
||||
@@ -0,0 +1,223 @@
|
||||
# language: fr
|
||||
|
||||
@api @premium @multi-device @mvp
|
||||
Fonctionnalité: Détection et gestion des conflits de streaming multi-device Premium
|
||||
|
||||
En tant qu'abonné Premium
|
||||
Je peux écouter sur plusieurs appareils (iPhone, iPad, Android, Web)
|
||||
Mais un seul stream audio peut être actif à la fois
|
||||
Afin de respecter les conditions d'abonnement Premium individuel
|
||||
|
||||
Contexte:
|
||||
Étant donné un utilisateur avec abonnement Premium actif
|
||||
Et plusieurs appareils enregistrés sur le compte :
|
||||
| device_id | platform | nom | last_seen |
|
||||
| iphone-123 | iOS | iPhone de Jean | 2026-02-03 14:00:00 |
|
||||
| ipad-456 | iOS | iPad Pro | 2026-02-03 12:30:00 |
|
||||
| android-789 | Android | Samsung Galaxy | 2026-02-02 18:00:00 |
|
||||
| web-abc | Web | Chrome MacBook | 2026-02-03 10:00:00 |
|
||||
|
||||
# ============================================================================
|
||||
# DÉTECTION STREAM ACTIF ET CONFLIT
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Premier stream - aucun conflit
|
||||
Étant donné aucun stream n'est actuellement actif sur le compte
|
||||
Quand l'utilisateur démarre la lecture sur "iPhone de Jean"
|
||||
Alors le stream doit démarrer normalement
|
||||
Et une session active doit être enregistrée dans Redis :
|
||||
| user_id | user-123 |
|
||||
| device_id | iphone-123 |
|
||||
| started_at | 2026-02-03 14:00:00|
|
||||
| content_id | content-xyz |
|
||||
| stream_token | token-abc123 |
|
||||
Et le TTL Redis doit être de 2 heures
|
||||
|
||||
Scénario: Tentative de stream simultané sur second appareil
|
||||
Étant donné un stream actif sur "iPhone de Jean" depuis 10 minutes
|
||||
Quand l'utilisateur tente de démarrer la lecture sur "iPad Pro"
|
||||
Alors une réponse HTTP 409 Conflict doit être retournée
|
||||
Et le message doit indiquer :
|
||||
"""
|
||||
Un contenu est déjà en cours de lecture sur "iPhone de Jean".
|
||||
Voulez-vous arrêter la lecture sur cet appareil et continuer ici ?
|
||||
"""
|
||||
Et les options proposées doivent être :
|
||||
| option | action |
|
||||
| Continuer ici | kill_previous_session |
|
||||
| Annuler | cancel_new_session |
|
||||
|
||||
Scénario: Utilisateur choisit "Continuer ici" - kill de l'ancienne session
|
||||
Étant donné un stream actif sur "iPhone de Jean"
|
||||
Et l'utilisateur tente de lire sur "iPad Pro"
|
||||
Et le conflit est détecté
|
||||
Quand l'utilisateur choisit "Continuer ici"
|
||||
Alors l'ancienne session sur "iPhone de Jean" doit être terminée immédiatement
|
||||
Et un message WebSocket doit être envoyé à "iPhone de Jean" :
|
||||
"""
|
||||
{"type": "stream_stopped", "reason": "playback_started_on_other_device", "device": "iPad Pro"}
|
||||
"""
|
||||
Et la lecture sur "iPhone de Jean" doit s'arrêter avec notification :
|
||||
"""
|
||||
Lecture arrêtée : un autre appareil utilise votre compte Premium.
|
||||
"""
|
||||
Et le nouveau stream sur "iPad Pro" doit démarrer normalement
|
||||
|
||||
Scénario: Utilisateur choisit "Annuler" - maintien de la session actuelle
|
||||
Étant donné un stream actif sur "iPhone de Jean"
|
||||
Et l'utilisateur tente de lire sur "iPad Pro"
|
||||
Et le conflit est détecté
|
||||
Quand l'utilisateur choisit "Annuler"
|
||||
Alors la session sur "iPhone de Jean" doit continuer normalement
|
||||
Et aucun stream ne doit démarrer sur "iPad Pro"
|
||||
Et l'utilisateur doit être redirigé vers l'écran d'accueil sur "iPad Pro"
|
||||
|
||||
# ============================================================================
|
||||
# GESTION AUTOMATIQUE DES SESSIONS EXPIRÉES
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Session expirée automatiquement après pause prolongée
|
||||
Étant donné un stream actif sur "iPhone de Jean"
|
||||
Et l'utilisateur met en pause à 14:00:00
|
||||
Et le TTL Redis est configuré pour expirer après 30 minutes de pause
|
||||
Quand il est 14:35:00 (>30 min de pause)
|
||||
Alors la session Redis doit expirer automatiquement
|
||||
Et l'utilisateur peut démarrer un nouveau stream sur n'importe quel appareil
|
||||
Et aucun conflit ne doit être détecté
|
||||
|
||||
Scénario: Heartbeat maintient la session active pendant la lecture
|
||||
Étant donné un stream actif sur "iPhone de Jean"
|
||||
Quand l'application envoie un heartbeat toutes les 30 secondes
|
||||
Alors le TTL Redis doit être renouvelé à 2 heures à chaque heartbeat
|
||||
Et la session doit rester active tant que les heartbeats continuent
|
||||
Et si 3 heartbeats consécutifs échouent, la session doit expirer
|
||||
|
||||
Scénario: Fermeture propre de l'application libère la session
|
||||
Étant donné un stream actif sur "iPhone de Jean"
|
||||
Quand l'utilisateur ferme proprement l'application (swipe kill ou logout)
|
||||
Alors une requête "end_session" doit être envoyée à l'API
|
||||
Et la session Redis doit être immédiatement supprimée
|
||||
Et l'utilisateur peut démarrer un stream sur un autre appareil sans délai
|
||||
|
||||
# ============================================================================
|
||||
# CAS LIMITES ET EDGE CASES
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Crash application sans fermeture propre
|
||||
Étant donné un stream actif sur "iPhone de Jean"
|
||||
Quand l'application crash sans envoyer "end_session"
|
||||
Alors la session Redis reste active avec son TTL
|
||||
Et après 2 minutes sans heartbeat, la session doit être marquée "stale"
|
||||
Et un nouveau stream peut être démarré après 2 minutes sans heartbeat
|
||||
Ou l'utilisateur peut forcer le kill via le conflit modal
|
||||
|
||||
Scénario: Connexion réseau perdue pendant stream
|
||||
Étant donné un stream actif sur "iPhone de Jean"
|
||||
Quand la connexion réseau est perdue pendant 5 minutes
|
||||
Alors les heartbeats échouent mais l'app continue en buffer
|
||||
Et après 3 heartbeats manqués (90 secondes), la session est considérée "stale"
|
||||
Et un autre appareil peut démarrer un stream après 90 secondes
|
||||
|
||||
Scénario: Deux appareils tentent de démarrer simultanément (race condition)
|
||||
Étant donné aucun stream actif
|
||||
Quand "iPhone de Jean" et "iPad Pro" tentent de démarrer un stream à la même milliseconde
|
||||
Alors le premier à obtenir le lock Redis doit réussir
|
||||
Et le second doit recevoir un conflit 409
|
||||
Et un mécanisme de lock distribué (Redis SET NX) doit être utilisé
|
||||
|
||||
Scénario: Utilisateur bascule rapidement entre appareils (<10s)
|
||||
Étant donné un stream sur "iPhone de Jean"
|
||||
Quand l'utilisateur kill la session et démarre sur "iPad Pro"
|
||||
Et tente de redémarrer sur "iPhone de Jean" 5 secondes après
|
||||
Alors le système doit détecter le conflit avec "iPad Pro"
|
||||
Et proposer à nouveau de kill la session iPad
|
||||
Et un délai de grâce de 5 secondes doit être respecté pour éviter les boucles
|
||||
|
||||
# ============================================================================
|
||||
# GESTION UTILISATEUR GRATUIT (pas de multi-device)
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Utilisateur gratuit tente de streamer sur 2 appareils
|
||||
Étant donné un utilisateur avec abonnement gratuit (pas Premium)
|
||||
Et un stream actif sur "iPhone de Jean"
|
||||
Quand l'utilisateur tente de lire sur "iPad Pro"
|
||||
Alors une réponse HTTP 403 Forbidden doit être retournée
|
||||
Et le message doit indiquer :
|
||||
"""
|
||||
Le multi-device nécessite un abonnement Premium.
|
||||
Actuellement en lecture sur "iPhone de Jean".
|
||||
Passez à Premium pour écouter sur plusieurs appareils.
|
||||
"""
|
||||
Et un bouton "Passer à Premium" doit être proposé
|
||||
|
||||
# ============================================================================
|
||||
# INTERFACE ADMIN & GESTION DES CONFLITS
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Dashboard admin - voir sessions actives par utilisateur
|
||||
Étant donné un administrateur connecté au dashboard
|
||||
Quand l'admin recherche l'utilisateur "user-123"
|
||||
Alors les sessions actives doivent être affichées :
|
||||
| device_id | platform | started_at | content_id | duration |
|
||||
| iphone-123 | iOS | 2026-02-03 14:00:00 | content-xyz | 15:30 |
|
||||
Et l'admin doit pouvoir "Terminer la session" manuellement
|
||||
|
||||
Scénario: Support client - résolution conflit bloqué
|
||||
Étant donné un utilisateur signale ne pas pouvoir lire sur aucun appareil
|
||||
Et une session "fantôme" existe dans Redis (crash + heartbeat bloqué)
|
||||
Quand le support client force la suppression de la session Redis
|
||||
Alors la clé Redis "active_stream:user-123" doit être supprimée
|
||||
Et l'utilisateur doit pouvoir redémarrer immédiatement
|
||||
|
||||
# ============================================================================
|
||||
# MÉTRIQUES & MONITORING
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Logging des conflits pour analytics
|
||||
Étant donné un conflit est détecté entre "iPhone de Jean" et "iPad Pro"
|
||||
Quand le conflit est résolu par kill de session
|
||||
Alors un événement analytics doit être loggé :
|
||||
| event_type | stream_conflict_resolved |
|
||||
| user_id | user-123 |
|
||||
| previous_device | iphone-123 |
|
||||
| new_device | ipad-456 |
|
||||
| resolution | kill_previous |
|
||||
| previous_session_duration | 900 |
|
||||
| timestamp | 2026-02-03 14:15:00 |
|
||||
Et ces métriques doivent être disponibles dans le dashboard
|
||||
|
||||
Scénario: Alerte monitoring - taux de conflits élevé
|
||||
Étant donné le système monitore les conflits de stream
|
||||
Quand le taux de conflits dépasse 15% des nouvelles sessions sur 1 heure
|
||||
Alors une alerte Slack doit être envoyée à l'équipe technique
|
||||
Et le message doit indiquer :
|
||||
"""
|
||||
⚠️ Taux de conflits stream élevé : 18% (seuil : 15%)
|
||||
Sessions impactées : 234 sur 1300
|
||||
Action recommandée : vérifier expiration Redis et heartbeats
|
||||
"""
|
||||
|
||||
# ============================================================================
|
||||
# COMPATIBILITÉ AVEC D'AUTRES FEATURES
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Radio live avec conflit de stream
|
||||
Étant donné un utilisateur écoute une radio live sur "iPhone de Jean"
|
||||
Quand l'utilisateur démarre un stream sur "iPad Pro" et kill la session iPhone
|
||||
Alors la radio live doit s'arrêter sur iPhone
|
||||
Et le nouveau stream sur iPad peut être une radio live ou contenu normal
|
||||
Et la progression audio-guide (si applicable) doit être sauvegardée
|
||||
|
||||
Scénario: Mode offline ne déclenche PAS de conflit
|
||||
Étant donné un stream actif en ligne sur "iPhone de Jean"
|
||||
Quand l'utilisateur écoute un contenu téléchargé en mode offline sur "iPad Pro"
|
||||
Alors aucun conflit ne doit être détecté
|
||||
Car le mode offline ne consomme pas de stream en ligne
|
||||
Et les deux lectures peuvent coexister
|
||||
|
||||
Scénario: Multi-device avec partage familial (post-MVP)
|
||||
Étant donné la fonctionnalité de partage familial est activée (post-MVP)
|
||||
Et le compte principal a 3 profils famille
|
||||
Quand chaque profil démarre un stream sur son appareil
|
||||
Alors jusqu'à 3 streams simultanés doivent être autorisés
|
||||
Et la détection de conflit doit s'appliquer par profil (1 stream/profil)
|
||||
@@ -0,0 +1,57 @@
|
||||
# language: fr
|
||||
|
||||
@api @premium @pricing @mvp
|
||||
Fonctionnalité: Tarification différenciée multi-canal
|
||||
|
||||
En tant que plateforme
|
||||
Je veux différencier les tarifs selon le canal d'acquisition
|
||||
Afin d'optimiser la monétisation et les marges
|
||||
|
||||
Scénario: Tarif standard sur le web
|
||||
Étant donné un utilisateur sur roadwave.fr (web)
|
||||
Quand il consulte les tarifs Premium
|
||||
Alors il voit:
|
||||
| Offre | Prix mensuel | Prix annuel |
|
||||
| Premium | 4.99€ | 49.90€ |
|
||||
Et aucun frais de plateforme
|
||||
Et un événement "PRICING_WEB_DISPLAYED" est enregistré
|
||||
|
||||
Scénario: Tarif majoré sur iOS (In-App Purchase)
|
||||
Étant donné un utilisateur sur l'app iOS
|
||||
Quand il consulte les tarifs Premium
|
||||
Alors il voit:
|
||||
| Offre | Prix mensuel | Prix annuel |
|
||||
| Premium | 5.99€ | 59.99€ |
|
||||
Et la majoration compense la commission Apple (30%)
|
||||
Et un événement "PRICING_IOS_DISPLAYED" est enregistré
|
||||
|
||||
Scénario: Tarif majoré sur Android (Google Play)
|
||||
Étant donné un utilisateur sur l'app Android
|
||||
Alors il voit:
|
||||
| Offre | Prix mensuel | Prix annuel |
|
||||
| Premium | 5.99€ | 59.99€ |
|
||||
Et la majoration compense la commission Google (15-30%)
|
||||
Et un événement "PRICING_ANDROID_DISPLAYED" est enregistré
|
||||
|
||||
Scénario: Redirection vers le web pour optimiser le coût
|
||||
Étant donné un utilisateur sur mobile
|
||||
Quand il clique sur "S'abonner"
|
||||
Alors un message suggère: "Économisez 1€ en vous abonnant sur notre site web"
|
||||
Et un lien direct vers roadwave.fr/premium
|
||||
Et un événement "WEB_SUBSCRIPTION_SUGGESTED" est enregistré
|
||||
|
||||
Scénario: Gestion des abonnements multi-plateformes
|
||||
Étant donné un utilisateur abonné via iOS
|
||||
Quand il se connecte sur Android
|
||||
Alors son abonnement est reconnu et actif
|
||||
Et synchronisé automatiquement
|
||||
Et un événement "CROSS_PLATFORM_SUBSCRIPTION_SYNCED" est enregistré
|
||||
|
||||
Scénario: Métriques de conversion par canal
|
||||
Étant donné que 1000 abonnements ont été souscrits
|
||||
Alors la répartition par canal est:
|
||||
| Canal | Abonnements | Taux conversion | Revenu moyen |
|
||||
| Web | 450 (45%) | 8% | 49.90€ |
|
||||
| iOS | 350 (35%) | 6% | 59.99€ |
|
||||
| Android | 200 (20%) | 5% | 59.99€ |
|
||||
Et les métriques sont exportées vers le monitoring
|
||||
@@ -0,0 +1,83 @@
|
||||
# language: fr
|
||||
|
||||
@api @premium @payment @mvp
|
||||
Fonctionnalité: Webhooks et retry automatique des paiements
|
||||
|
||||
En tant que système de paiement
|
||||
Je veux gérer les échecs de paiement avec retry intelligent
|
||||
Afin de maximiser les conversions et minimiser le churn
|
||||
|
||||
Scénario: Webhook Mangopay de paiement réussi
|
||||
Étant donné un abonnement Premium en cours
|
||||
Quand Mangopay envoie un webhook "PAYIN_NORMAL_SUCCEEDED"
|
||||
Alors le système enregistre le paiement
|
||||
Et l'abonnement est prolongé de 30 jours
|
||||
Et un email de confirmation est envoyé
|
||||
Et un événement "PAYMENT_SUCCESS_WEBHOOK_RECEIVED" est enregistré
|
||||
|
||||
Scénario: Webhook de paiement échoué
|
||||
Étant donné un abonnement Premium
|
||||
Quand Mangopay envoie un webhook "PAYIN_NORMAL_FAILED"
|
||||
Alors le système programme un retry automatique
|
||||
Et l'utilisateur est notifié: "Échec de paiement. Nous réessayerons dans 3 jours."
|
||||
Et un événement "PAYMENT_FAILED_WEBHOOK_RECEIVED" est enregistré
|
||||
|
||||
Scénario: Retry automatique après 3 jours
|
||||
Étant donné un paiement échoué il y a 3 jours
|
||||
Quand le système tente un retry automatique
|
||||
Alors une nouvelle tentative de prélèvement est lancée
|
||||
Et l'utilisateur reçoit un email: "Nouvelle tentative de paiement en cours"
|
||||
Et un événement "PAYMENT_RETRY_ATTEMPTED" est enregistré
|
||||
|
||||
Scénario: Série de retries intelligents (3, 7, 14 jours)
|
||||
Étant donné un premier échec de paiement
|
||||
Alors le système programme:
|
||||
| Retry | Délai | Statut abonnement |
|
||||
| 1 | J+3 | Actif |
|
||||
| 2 | J+7 | Actif |
|
||||
| 3 | J+14 | Suspendu |
|
||||
Et après le 3ème échec, l'abonnement est annulé
|
||||
Et un événement "PAYMENT_RETRY_SERIES_CONFIGURED" est enregistré
|
||||
|
||||
Scénario: Suspension de l'abonnement après 3 échecs
|
||||
Étant donné 3 tentatives de paiement échouées
|
||||
Quand le 3ème retry échoue
|
||||
Alors l'abonnement est suspendu
|
||||
Et l'utilisateur repasse en mode Free
|
||||
Et un email explique comment mettre à jour la carte
|
||||
Et un événement "SUBSCRIPTION_SUSPENDED_PAYMENT_FAILURE" est enregistré
|
||||
|
||||
Scénario: Webhook de carte expirée
|
||||
Étant donné un abonnement avec carte expirant ce mois
|
||||
Quand Mangopay envoie un webhook "CARD_EXPIRING"
|
||||
Alors une notification est envoyée 30 jours avant
|
||||
Et rappelée 7 jours avant
|
||||
Et le jour de l'expiration
|
||||
Et un événement "CARD_EXPIRY_WARNING_SENT" est enregistré
|
||||
|
||||
Scénario: Mise à jour de carte et reprise de l'abonnement
|
||||
Étant donné un utilisateur avec abonnement suspendu
|
||||
Quand il met à jour sa carte bancaire
|
||||
Alors un paiement est immédiatement tenté
|
||||
Et si succès, l'abonnement est réactivé
|
||||
Et les jours perdus sont récupérés pro-rata
|
||||
Et un événement "SUBSCRIPTION_REACTIVATED_AFTER_PAYMENT" est enregistré
|
||||
|
||||
Scénario: Webhooks de remboursement
|
||||
Étant donné un utilisateur qui annule son abonnement
|
||||
Et demande un remboursement (satisfait ou remboursé 30j)
|
||||
Quand Mangopay envoie "PAYOUT_NORMAL_SUCCEEDED"
|
||||
Alors le remboursement est enregistré
|
||||
Et l'utilisateur reçoit confirmation
|
||||
Et un événement "REFUND_WEBHOOK_PROCESSED" est enregistré
|
||||
|
||||
Scénario: Métriques de performance des retries
|
||||
Étant donné que 1000 paiements ont échoué
|
||||
Alors les indicateurs suivants sont disponibles:
|
||||
| Métrique | Valeur |
|
||||
| Taux de succès au 1er retry (J+3)| 45% |
|
||||
| Taux de succès au 2ème retry (J+7)| 25% |
|
||||
| Taux de succès au 3ème retry (J+14)| 10% |
|
||||
| Taux de récupération global | 80% |
|
||||
| Taux d'annulation définitive | 20% |
|
||||
Et les métriques sont exportées vers le monitoring
|
||||
319
docs/domains/premium/rules/abonnements-notifications.md
Normal file
319
docs/domains/premium/rules/abonnements-notifications.md
Normal file
@@ -0,0 +1,319 @@
|
||||
## 8. Abonnements et notifications
|
||||
|
||||
### 8.1 Impact sur l'algorithme
|
||||
|
||||
**Décision** : Boost +30% au score + reste dans le mix
|
||||
|
||||
**Boost de score abonnements** :
|
||||
- **+30% au score final** pour contenus d'un créateur suivi
|
||||
- Application : multiplicateur sur le score calculé
|
||||
|
||||
```
|
||||
score_final_avec_boost = score_final × 1.3
|
||||
```
|
||||
|
||||
**Reste dans le mix** :
|
||||
- ❌ **Pas de priorité absolue** (pas de file dédiée abonnements)
|
||||
- ✅ Contenu suivi entre en **compétition avec autres contenus**
|
||||
- ✅ Si créateur suivi publie contenu faible engagement → peut être battu par contenu viral non-suivi
|
||||
|
||||
**Exemple concret** :
|
||||
```
|
||||
Utilisateur à Paris, 2 contenus disponibles :
|
||||
|
||||
Contenu A (créateur NON suivi) :
|
||||
- Score géo : 0.9 (très proche)
|
||||
- Score intérêts : 0.8
|
||||
- Score engagement : 0.7
|
||||
→ Score final : 0.80
|
||||
|
||||
Contenu B (créateur suivi) :
|
||||
- Score géo : 0.5 (moyennement proche)
|
||||
- Score intérêts : 0.6
|
||||
- Score engagement : 0.5
|
||||
→ Score final : 0.53
|
||||
→ Score avec boost : 0.53 × 1.3 = 0.69
|
||||
|
||||
→ Contenu A proposé en premier (0.80 > 0.69)
|
||||
```
|
||||
|
||||
**Cas où abonnement fait la différence** :
|
||||
```
|
||||
Contenu A (non suivi) : score 0.70
|
||||
Contenu B (suivi) : score 0.60 → avec boost 0.78
|
||||
→ Contenu B proposé (boost fait pencher la balance)
|
||||
```
|
||||
|
||||
**Justification** :
|
||||
- **Équilibre** : valorise abonnements sans enfermer utilisateur
|
||||
- **Découverte** : contenus viraux/locaux peuvent toujours émerger
|
||||
- **Prévisible** : boost fixe, pas de logique opaque
|
||||
- **Coût 0** : multiplicateur simple dans l'algo
|
||||
|
||||
---
|
||||
|
||||
### 8.2 Notifications contextuelles
|
||||
|
||||
**Décision** : Push adapté selon contexte (voiture vs à pied) + limite 10/jour
|
||||
|
||||
**Détection contexte utilisateur** :
|
||||
|
||||
| Contexte | Détection | Comportement |
|
||||
|----------|-----------|--------------|
|
||||
| **En voiture** | Vitesse GPS >10 km/h | Notifications silencieuses (in-app uniquement) + commandes volant |
|
||||
| **À pied** | Vitesse GPS <5 km/h | Notifications push actives + interface tactile/vocale |
|
||||
|
||||
**Notifications activées** :
|
||||
|
||||
#### En voiture (mode conduite)
|
||||
|
||||
| Événement | Notification | Comportement |
|
||||
|-----------|--------------|--------------|
|
||||
| **Nouveau contenu créateur suivi** | In-app uniquement | Badge compteur, pas de push (sécurité) |
|
||||
| **Live créateur suivi** | In-app uniquement | Badge compteur, pas de push |
|
||||
| **Point d'intérêt proche** | Audio notification | Bip + annonce vocale : "Audio-guide disponible" |
|
||||
|
||||
#### À pied (mode piéton)
|
||||
|
||||
| Événement | Notification | Comportement |
|
||||
|-----------|--------------|--------------|
|
||||
| **Nouveau contenu créateur suivi** | ✅ Push | Si utilisateur dans zone géo du contenu |
|
||||
| **Live créateur suivi** | ✅ Push | Si utilisateur dans zone géo |
|
||||
| **Audio-guide disponible** | ✅ Push | "📍 Audio-guide disponible : [Lieu]" |
|
||||
| **Séquence suivante suggérée** | Audio notification | Annonce vocale : "Pièce suivante disponible" |
|
||||
|
||||
**Format notifications** :
|
||||
|
||||
**Nouveau contenu** :
|
||||
```
|
||||
🎧 [Nom créateur] a publié : "[Titre contenu]"
|
||||
Tap pour écouter
|
||||
```
|
||||
|
||||
**Live en direct** :
|
||||
```
|
||||
🔴 [Nom créateur] est en direct : "[Titre live]"
|
||||
Tap pour rejoindre
|
||||
```
|
||||
|
||||
**Audio-guide à pied** :
|
||||
```
|
||||
📍 Audio-guide disponible : [Nom du lieu]
|
||||
Choisissez parmi 3 guides pour [Musée du Louvre]
|
||||
Tap pour explorer
|
||||
```
|
||||
|
||||
**Filtrage géographique** :
|
||||
- Si contenu/live hors zone utilisateur → **pas de notification**
|
||||
- Évite frustration : "notification pour contenu que je ne peux pas écouter"
|
||||
- Exception : contenu national → notifie tous les abonnés
|
||||
|
||||
**Fréquence maximale** :
|
||||
- **Maximum 10 notifications push/jour** par utilisateur (tous types confondus)
|
||||
- Si dépassement : notifications regroupées
|
||||
- Message groupé : "🎧 3 nouveaux contenus de créateurs suivis"
|
||||
|
||||
**Plages horaires** :
|
||||
- **Mode silencieux** : 22h-8h (pas de push, sauf live)
|
||||
- Paramétrable utilisateur (désactivation totale possible)
|
||||
- Option "Notifications importantes uniquement" (lives uniquement)
|
||||
|
||||
**Gestion préférences** :
|
||||
|
||||
| Préférence | Défaut | Description |
|
||||
|------------|--------|-------------|
|
||||
| **Nouveaux contenus** | ✅ Activé | Push à chaque nouveau contenu (à pied uniquement) |
|
||||
| **Lives** | ✅ Activé | Push au démarrage live (à pied uniquement) |
|
||||
| **Audio-guides proximité** | ✅ Activé | Push quand audio-guide détecté à <100m |
|
||||
| **Mode silencieux** | ✅ Activé (22h-8h) | Pas de push nocturne |
|
||||
| **Limite quotidienne** | 10 | Modifiable 5-20 |
|
||||
|
||||
**Justification** :
|
||||
- **Sécurité routière** : pas de push en conduite (distraction)
|
||||
- **Engagement piéton** : push actifs pour audio-guides (valeur ajoutée tourisme)
|
||||
- **Pas de spam** : limite 10/jour + mode silencieux
|
||||
- **Filtrage géo** : pertinence maximale (pas de notif inutiles)
|
||||
- **Coût** : APNS/FCM natifs (gratuit, aucune limite)
|
||||
|
||||
---
|
||||
|
||||
### 8.3 Mode Audio-guide (piéton)
|
||||
|
||||
**Décision** : Navigation manuelle multiséquence + choix parmi plusieurs guides
|
||||
|
||||
**Fonctionnement** :
|
||||
|
||||
#### Détection et proposition
|
||||
|
||||
1. Utilisateur à pied (<5 km/h) passe à <**100m** d'un lieu avec audio-guides
|
||||
2. **Notification push** : "📍 Audio-guide disponible : [Musée du Louvre]"
|
||||
3. Tap notification → **Page de sélection** audio-guides
|
||||
|
||||
#### Page de sélection
|
||||
|
||||
**Affichage** :
|
||||
```
|
||||
📍 Musée du Louvre
|
||||
|
||||
Choisissez votre guide :
|
||||
|
||||
┌─────────────────────────────────┐
|
||||
│ 🎨 Visite complète (45 min) │
|
||||
│ Par [Créateur A] • 12 séquences│
|
||||
│ ⭐ 4.8 • 1.2K écoutes │
|
||||
└─────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────┐
|
||||
│ 🏛️ Œuvres majeures (20 min) │
|
||||
│ Par [Créateur B] • 5 séquences │
|
||||
│ ⭐ 4.9 • 3.5K écoutes │
|
||||
└─────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────┐
|
||||
│ 👶 Visite famille (30 min) │
|
||||
│ Par [Créateur C] • 8 séquences │
|
||||
│ ⭐ 4.7 • 850 écoutes │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### Interface audio-guide
|
||||
|
||||
**Après sélection** :
|
||||
```
|
||||
🎨 Visite complète • Musée du Louvre
|
||||
|
||||
Piste actuelle : 2/12
|
||||
"La Joconde - Histoire et mystères"
|
||||
[████████────────────] 3:24 / 6:50
|
||||
|
||||
Liste des séquences :
|
||||
✅ 1. Introduction et architecture
|
||||
▶️ 2. La Joconde - Histoire et mystères
|
||||
⏸️ 3. Vénus de Milo
|
||||
⏸️ 4. Victoire de Samothrace
|
||||
⏸️ 5. Peintures Renaissance
|
||||
...
|
||||
⏸️ 12. Conclusion et boutique
|
||||
```
|
||||
|
||||
**Navigation** :
|
||||
|
||||
| Action | Geste | Effet |
|
||||
|--------|-------|-------|
|
||||
| **Séquence suivante** | Tap "Suivant" ou commande vocale "Suivant" | Passe à séquence N+1 |
|
||||
| **Séquence précédente** | Tap "Précédent" ou commande vocale "Précédent" | Revient à séquence N-1 |
|
||||
| **Saut direct** | Tap séquence dans liste | Lecture séquence choisie |
|
||||
| **Pause** | Tap bouton pause | Met en pause, reprise position exacte |
|
||||
| **Quitter** | Tap "×" | Sauvegarde progression, sortie guide |
|
||||
|
||||
**Guidage vocal automatique** :
|
||||
- Entre 2 séquences : "Vous avez terminé la séquence 2. Dirigez-vous vers la Vénus de Milo pour la séquence 3."
|
||||
- Si utilisateur s'éloigne (>50m de la prochaine pièce) : "Vous vous éloignez de la prochaine étape. Consultez le plan."
|
||||
|
||||
**Sauvegarde progression** :
|
||||
- Position dans guide sauvegardée automatiquement
|
||||
- Retour ultérieur : "Reprendre à la séquence 5 ?" ou "Recommencer depuis le début"
|
||||
- Historique : guide marqué "Terminé" si toutes séquences écoutées
|
||||
|
||||
**Création audio-guide multiséquence** :
|
||||
|
||||
**Processus créateur** :
|
||||
1. Créateur upload **plusieurs fichiers audio** (1 par séquence)
|
||||
2. Numérote les séquences : "Séquence 1", "Séquence 2", etc.
|
||||
3. Titre chaque séquence : "Introduction", "La Joconde", etc.
|
||||
4. Définit **point GPS unique** pour tout le guide (centre du lieu)
|
||||
5. Métadonnées : durée totale calculée automatiquement
|
||||
|
||||
**Format stockage** :
|
||||
```json
|
||||
{
|
||||
"guide_id": "abc123",
|
||||
"title": "Visite complète Musée du Louvre",
|
||||
"location": {"lat": 48.8606, "lon": 2.3376, "radius": 200},
|
||||
"sequences": [
|
||||
{
|
||||
"sequence_number": 1,
|
||||
"title": "Introduction et architecture",
|
||||
"audio_url": "https://cdn.../seq1.mp3",
|
||||
"duration_seconds": 180
|
||||
},
|
||||
{
|
||||
"sequence_number": 2,
|
||||
"title": "La Joconde - Histoire et mystères",
|
||||
"audio_url": "https://cdn.../seq2.mp3",
|
||||
"duration_seconds": 410
|
||||
},
|
||||
...
|
||||
],
|
||||
"total_duration_seconds": 2700,
|
||||
"creator_id": "creator_xyz"
|
||||
}
|
||||
```
|
||||
|
||||
**Justification** :
|
||||
- **UX piéton** : navigation tactile adaptée (pas de commandes volant)
|
||||
- **Autonomie** : utilisateur maître de son rythme (pas d'enchaînement forcé)
|
||||
- **Choix** : plusieurs guides = diversité styles (famille, expert, rapide)
|
||||
- **Engagement** : sauvegarde progression = incitation terminer
|
||||
- **Coût** : réutilise infra contenu standard (juste métadonnées séquences)
|
||||
|
||||
---
|
||||
|
||||
### 8.4 Limites et désabonnement
|
||||
|
||||
**Décision** : 200 abonnements max + désabonnement -5% jauges
|
||||
|
||||
**Nombre maximum d'abonnements** :
|
||||
- **200 créateurs maximum** par utilisateur
|
||||
- Raisons :
|
||||
- **Évite spam** : au-delà de 200, notifications ingérables
|
||||
- **Usage réaliste** : 200 créateurs = déjà énorme (vs 100-150 sur YouTube/Twitter)
|
||||
- **Performance** : requêtes SQL optimisées (index sur 200 max)
|
||||
|
||||
**Si limite atteinte** :
|
||||
- Message : "Vous suivez déjà 200 créateurs. Désabonnez-vous d'un créateur pour en suivre un nouveau."
|
||||
- Liste triable : par date abonnement, nb contenus écoutés, dernière activité
|
||||
- Suggestion : "Vous n'avez pas écouté [Créateur X] depuis 6 mois, le désabonner ?"
|
||||
|
||||
**Abonnement initial** :
|
||||
- Impact : **+5% toutes jauges tags du créateur** (voir [Règle 05 - Section 5.3](05-interactions-navigation.md#actions-complémentaires-mode-piéton-uniquement))
|
||||
- Action : Bouton "S'abonner" dans profil créateur (interface mobile)
|
||||
- Immédiat à l'action
|
||||
|
||||
**Désabonnement** :
|
||||
- Impact : **-5% toutes jauges tags du créateur** (symétrique)
|
||||
- Action : Bouton "Se désabonner" dans profil créateur
|
||||
- Immédiat à l'action
|
||||
- Pas de confirmation (action réversible)
|
||||
|
||||
**Exemple** :
|
||||
```
|
||||
Créateur tague ses contenus : Automobile, Voyage
|
||||
|
||||
Abonnement :
|
||||
→ Jauge Automobile : 60% → 65% (+5%)
|
||||
→ Jauge Voyage : 55% → 60% (+5%)
|
||||
|
||||
3 mois plus tard, désabonnement :
|
||||
→ Jauge Automobile : 65% → 60% (-5%)
|
||||
→ Jauge Voyage : 60% → 55% (-5%)
|
||||
```
|
||||
|
||||
**Gestion multi-tags** :
|
||||
- Si créateur a 3 tags → **+5% sur chacun des 3 tags**
|
||||
- Logique : abonnement = signal fort d'affinité à TOUS les sujets du créateur
|
||||
|
||||
**Abonnements réciproques** :
|
||||
- ❌ **Pas d'abonnement mutuel visible**
|
||||
- Créateur ne voit pas qui est abonné (privacy)
|
||||
- Créateur voit uniquement : nombre total abonnés (métrique globale)
|
||||
|
||||
**Justification** :
|
||||
- **Limite 200** : équilibre entre liberté et gestion spam
|
||||
- **Symétrie +5%/-5%** : cohérence mathématique, prévisibilité
|
||||
- **Privacy** : pas de liste publique abonnés (évite stalking)
|
||||
- **Coût** : table abonnements PostgreSQL standard
|
||||
|
||||
---
|
||||
|
||||
## Récapitulatif Section 8
|
||||
225
docs/domains/premium/rules/mode-offline.md
Normal file
225
docs/domains/premium/rules/mode-offline.md
Normal file
@@ -0,0 +1,225 @@
|
||||
## 11. Mode offline
|
||||
|
||||
### 11.1 Téléchargement
|
||||
|
||||
**Zone géographique** : Choix manuel utilisateur
|
||||
|
||||
**Options prédéfinies** :
|
||||
- "Autour de moi" (rayon 50 km position actuelle)
|
||||
- "Ma ville" (limite administrative détectée)
|
||||
- "Mon département" (sélection liste)
|
||||
- "Ma région" (sélection liste)
|
||||
- Recherche manuelle : "Paris", "Lyon", "Marseille", etc.
|
||||
|
||||
**Nombre de contenus téléchargeables** :
|
||||
|
||||
| Statut | Limite | Affichage |
|
||||
|--------|--------|-----------|
|
||||
| **Gratuit** | 50 contenus max | "12/50 contenus téléchargés" |
|
||||
| **Premium** | Illimité | "245 contenus (3.2 GB)" |
|
||||
|
||||
**Calcul temps disponible** :
|
||||
- 50 contenus × 5 min moyenne = 250 min = **4h d'écoute** (suffisant pour gratuits)
|
||||
- Premium illimité = limité uniquement par espace disque device
|
||||
|
||||
**Connexion WiFi/Mobile** :
|
||||
|
||||
**Par défaut** : WiFi uniquement
|
||||
|
||||
**Sur données mobiles** :
|
||||
1. User clique "Télécharger"
|
||||
2. Détection : pas de WiFi
|
||||
3. Popup : "Vous n'êtes pas connecté en WiFi. Télécharger via données mobiles consommera environ **X MB**. Continuer ?"
|
||||
4. Boutons : "Attendre WiFi" / "Continuer"
|
||||
|
||||
**Calcul estimation** :
|
||||
```
|
||||
Nombre contenus × durée moyenne × bitrate qualité
|
||||
Exemple : 20 contenus × 5 min × 48 kbps = ~72 MB
|
||||
```
|
||||
|
||||
**Qualité audio téléchargement** :
|
||||
|
||||
| Qualité | Bitrate | Taille | Disponibilité |
|
||||
|---------|---------|--------|---------------|
|
||||
| **Basse** | 24 kbps | ~10 MB/h | Gratuit + Premium |
|
||||
| **Standard** | 48 kbps | ~20 MB/h | Gratuit + Premium (défaut) |
|
||||
| **Haute** | 64 kbps | ~30 MB/h | **Premium uniquement** |
|
||||
|
||||
**Justification** :
|
||||
- Standard = bon compromis qualité/taille (Opus 48 kbps = très correct pour voix)
|
||||
- Haute réservée Premium = incitation upgrade
|
||||
- User peut réduire à "basse" si espace limité
|
||||
|
||||
---
|
||||
|
||||
### 11.2 Validité et renouvellement
|
||||
|
||||
**Durée de validité** : 30 jours après téléchargement
|
||||
|
||||
**Standard industrie** :
|
||||
- Spotify : 30 jours
|
||||
- YouTube Music : 30 jours
|
||||
- Deezer : 30 jours
|
||||
|
||||
**Renouvellement automatique** :
|
||||
|
||||
```
|
||||
App détecte WiFi + contenus >25 jours
|
||||
→ Requête API : GET /offline/contents/refresh
|
||||
→ Backend vérifie pour chaque contenu :
|
||||
- Abonnement Premium toujours actif ?
|
||||
- Contenu pas modéré/supprimé ?
|
||||
- Métadonnées à jour ?
|
||||
→ Renouvelle validité à 30 jours supplémentaires
|
||||
→ Mise à jour métadonnées (titre, créateur, statut)
|
||||
→ Pas de re-téléchargement audio (sauf si fichier corrompu)
|
||||
```
|
||||
|
||||
**Notification avant expiration** :
|
||||
- **J-3** : "X contenus expirent dans 3 jours. Connectez-vous en WiFi pour les renouveler"
|
||||
- **J-0** : Suppression automatique
|
||||
- **J+0** : Toast "15 contenus expirés ont été supprimés"
|
||||
|
||||
**Justification** :
|
||||
- **Force reconnexion** : vérifier abonnement actif, contenus légaux
|
||||
- **Évite stockage obsolète** : contenus supprimés/modérés ne restent pas
|
||||
- **UX transparente** : renouvellement silencieux si WiFi régulier
|
||||
|
||||
---
|
||||
|
||||
### 11.3 Synchronisation actions offline
|
||||
|
||||
**Actions stockées localement (SQLite)** :
|
||||
- Likes/unlikes
|
||||
- Abonnements/désabonnements
|
||||
- Signalements
|
||||
- Progression audio-guides
|
||||
|
||||
**Sync automatique à la reconnexion** :
|
||||
|
||||
```
|
||||
1. App détecte reconnexion Internet
|
||||
2. Récupération queue locale : SELECT * FROM pending_actions ORDER BY created_at
|
||||
3. Envoi batch API : POST /sync/actions
|
||||
4. Backend traite chaque action
|
||||
5. Confirmation réception : DELETE FROM pending_actions WHERE id IN (...)
|
||||
6. Toast : "3 likes et 1 abonnement synchronisés"
|
||||
```
|
||||
|
||||
**Gestion erreurs sync** :
|
||||
- Si échec après 3 tentatives → notification : "Impossible de synchroniser. Réessayez plus tard"
|
||||
- Actions conservées jusqu'à sync réussie (pas de perte)
|
||||
- **Rétention max 7 jours** : après = purge (évite queue infinie)
|
||||
|
||||
**Justification** :
|
||||
- **Pas de conflit possible** : actions unilatérales user (likes/abonnements)
|
||||
- **UX fluide** : pas de blocage offline
|
||||
- **Batch = économie** : requêtes HTTP groupées
|
||||
|
||||
---
|
||||
|
||||
### 11.4 Contenus supprimés pendant offline
|
||||
|
||||
**Problème** : Que se passe-t-il si un utilisateur télécharge des contenus, part offline plusieurs jours, et pendant ce temps certains contenus sont supprimés par les créateurs ou la modération ?
|
||||
|
||||
**Décision** : Suppression immédiate à la reconnexion (Option A - KISS)
|
||||
|
||||
#### Processus de synchronisation
|
||||
|
||||
```
|
||||
User se reconnecte (WiFi détecté)
|
||||
↓
|
||||
1. API sync : GET /offline/validate
|
||||
Backend retourne :
|
||||
{
|
||||
"valid_ids": [id1, id2, id3, ...],
|
||||
"deleted_ids": [id10, id12, id15],
|
||||
"metadata_updates": [{id: id5, new_title: "..."}]
|
||||
}
|
||||
|
||||
2. App mobile compare avec contenus locaux :
|
||||
- valid_ids : renouvelle validité 30j
|
||||
- deleted_ids : suppression immédiate fichiers locaux
|
||||
- metadata_updates : mise à jour titre/créateur/tags
|
||||
|
||||
3. Notification user :
|
||||
Toast : "3 contenus supprimés ont été retirés"
|
||||
```
|
||||
|
||||
#### Gestion contenu en cours d'écoute
|
||||
|
||||
**Si contenu supprimé en cours de lecture** :
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────┐
|
||||
│ Contenu supprimé │
|
||||
├────────────────────────────────────────┤
|
||||
│ Ce contenu n'est plus disponible │
|
||||
│ et a été retiré par le créateur. │
|
||||
│ │
|
||||
│ Passage au contenu suivant... │
|
||||
│ │
|
||||
│ [OK] │
|
||||
└────────────────────────────────────────┘
|
||||
|
||||
→ Lecture s'arrête
|
||||
→ Fichier supprimé localement
|
||||
→ Passage automatique au contenu suivant (après 2s)
|
||||
```
|
||||
|
||||
#### Message récapitulatif
|
||||
|
||||
**Si plusieurs contenus supprimés** :
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────┐
|
||||
│ Contenus supprimés │
|
||||
├────────────────────────────────────────┤
|
||||
│ 3 contenus téléchargés ne sont plus │
|
||||
│ disponibles et ont été retirés. │
|
||||
│ │
|
||||
│ Les créateurs peuvent supprimer ou │
|
||||
│ modifier leurs contenus à tout moment. │
|
||||
│ │
|
||||
│ [Voir la liste] [OK] │
|
||||
└────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Bouton "Voir la liste"** :
|
||||
- Affiche titres + créateurs des contenus supprimés
|
||||
- Permet comprendre ce qui a disparu
|
||||
- Historique conservé 7 jours (puis purge)
|
||||
|
||||
**Justification KISS** :
|
||||
- ✅ **Simplicité technique** : pas de grace period complexe, pas de gestion d'états intermédiaires
|
||||
- ✅ **Respect créateur** : si créateur supprime = volonté claire immédiate, pas de diffusion prolongée
|
||||
- ✅ **Conformité légale** : contenu modéré (illégal, violation CGU) retiré immédiatement, pas de risque juridique
|
||||
- ✅ **Cas rare** : peu de créateurs suppriment contenus après publication, impact user limité
|
||||
|
||||
**Post-MVP** : Si feedback négatifs users ("J'étais en train d'écouter et ça s'est coupé brutalement !"), ajouter grace period UNIQUEMENT pour suppression créateur volontaire :
|
||||
- Motif suppression = "modération RoadWave" → Suppression immédiate (sécurité/légalité)
|
||||
- Motif suppression = "créateur volontaire" → Grace period 7 jours + badge "Bientôt retiré"
|
||||
- Motif suppression = "passage Premium" → Si user Premium : conserve accès, si gratuit : grace period 7j
|
||||
|
||||
**Mais attendre feedback réel avant d'ajouter cette complexité.**
|
||||
|
||||
---
|
||||
|
||||
## Récapitulatif Section 11
|
||||
|
||||
| Aspect | Décision | Valeur |
|
||||
|--------|----------|--------|
|
||||
| **Zone téléchargement** | Choix | Manuel (autour/ville/département/région/recherche) |
|
||||
| **Limite gratuit** | Contenus | 50 max |
|
||||
| **Limite Premium** | Contenus | Illimité (espace disque) |
|
||||
| **Connexion** | Par défaut | WiFi (mobile avec confirmation) |
|
||||
| **Qualité Standard** | Bitrate | 48 kbps Opus |
|
||||
| **Qualité Haute** | Bitrate | 64 kbps (Premium uniquement) |
|
||||
| **Validité** | Durée | 30 jours |
|
||||
| **Renouvellement** | Mode | Automatique si WiFi |
|
||||
| **Notification expiration** | Délai | J-3 |
|
||||
| **Sync actions** | Mode | Batch automatique reconnexion |
|
||||
| **Rétention queue** | Durée | 7 jours max |
|
||||
|
||||
---
|
||||
245
docs/domains/premium/rules/premium.md
Normal file
245
docs/domains/premium/rules/premium.md
Normal file
@@ -0,0 +1,245 @@
|
||||
## 10. Premium
|
||||
|
||||
### 10.1 Offre et tarification
|
||||
|
||||
**Décision** : Deux formules sans essai gratuit
|
||||
|
||||
| Formule | Prix | Économie | Prix effectif |
|
||||
|---------|------|----------|---------------|
|
||||
| **Mensuel** | 4.99€/mois | - | 4.99€/mois |
|
||||
| **Annuel** | 49.99€/an | 2 mois offerts | 4.16€/mois |
|
||||
|
||||
**❌ Pas d'essai gratuit**
|
||||
|
||||
**Raisons** :
|
||||
- **Anti-abus vacances** : évite inscriptions opportunistes (essai 14j avant road trip vacances, puis annulation)
|
||||
- **Protection revenus créateurs** : les écoutes Premium rémunèrent créateurs dès jour 1
|
||||
- **Simplicité** : pas de gestion période trial + conversion
|
||||
- **Engagement** : utilisateur qui paie dès début = plus engagé
|
||||
|
||||
**❌ Pas de partage familial (MVP)**
|
||||
|
||||
**Raisons** :
|
||||
- Complexité technique (gestion invitations, validation liens, limite devices)
|
||||
- Risque abus ("familles" de 6 inconnus)
|
||||
- Coût dev/support élevé pour ROI incertain
|
||||
- La plupart des users RoadWave sont individuels (conducteurs)
|
||||
- **Post-MVP** : Si forte demande, offre "Famille" à 9.99€/mois pour 5 comptes
|
||||
|
||||
**Justification tarif** :
|
||||
- **Aligné marché bas** : Spotify = 10.99€, YouTube Premium = 11.99€, Apple Music = 10.99€
|
||||
- **Prix accessible** : cible conducteurs quotidiens (budget raisonnable)
|
||||
- **Incitation annuel** : 2 mois offerts = engagement long terme + réduction churn
|
||||
|
||||
---
|
||||
|
||||
### 10.2 Multi-devices et détection simultanée
|
||||
|
||||
**Décision** : 1 seul stream actif par compte à tout moment - Priorité au dernier device (KISS)
|
||||
|
||||
**Règle simple** : Le dernier device à démarrer prend toujours la priorité, sans exception temporelle ni géolocalisation.
|
||||
|
||||
#### Comportement multi-devices
|
||||
|
||||
| Scénario | Device 1 | Device 2 | Résultat | Message Device 1 |
|
||||
|----------|----------|----------|----------|------------------|
|
||||
| **Transition normale** | Écoute online | Démarre 5s après | Device 1 coupé, Device 2 actif | "Lecture interrompue : compte utilisé sur autre appareil" |
|
||||
| **Offline connecté internet** | Écoute offline (avec WiFi) | Démarre online | Device 1 coupé, Device 2 actif | Même message |
|
||||
| **Offline déconnecté internet** | Écoute offline (mode avion) | Démarre online | Device 1 continue, Device 2 actif | Pas de détection possible (exception technique) |
|
||||
|
||||
#### Implémentation technique détaillée
|
||||
|
||||
**1. Stream online** :
|
||||
```
|
||||
Device 1 écoute online
|
||||
→ Heartbeat 30s vers serveur
|
||||
→ Redis : active_streams:{user_id} = {device_id: "iPhone_123", started_at: timestamp}
|
||||
→ TTL : 5 minutes
|
||||
|
||||
Device 2 démarre
|
||||
→ Détection : active_stream existe
|
||||
→ Action : Envoi WebSocket close à Device 1
|
||||
→ Redis : mise à jour active_streams:{user_id} = {device_id: "iPad_456", started_at: timestamp}
|
||||
→ Device 2 lecture démarre
|
||||
```
|
||||
|
||||
**2. Offline connecté internet** :
|
||||
```
|
||||
Device 1 écoute offline MAIS connecté WiFi/4G
|
||||
→ Heartbeat 30s envoyé quand même
|
||||
→ Redis : active_streams:{user_id} = {device_id: "iPhone_123", started_at: timestamp, mode: "offline"}
|
||||
→ Même mécanique que online : Device 2 coupe Device 1
|
||||
```
|
||||
|
||||
**3. Offline déconnecté internet** :
|
||||
```
|
||||
Device 1 vraiment offline (mode avion, tunnel)
|
||||
→ Pas de heartbeat possible
|
||||
→ Redis : aucune entrée OU expiration TTL après 5 min
|
||||
→ Device 2 démarre : pas de détection Device 1
|
||||
→ "Tant pis" (exception technique acceptable)
|
||||
```
|
||||
|
||||
#### Message utilisateur (device coupé)
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────┐
|
||||
│ ⚠️ Lecture interrompue │
|
||||
├────────────────────────────────────────┤
|
||||
│ Votre compte est utilisé sur un │
|
||||
│ autre appareil. │
|
||||
│ │
|
||||
│ Un seul appareil peut écouter à la │
|
||||
│ fois avec votre compte Premium. │
|
||||
│ │
|
||||
│ Le partage de compte viole nos CGU │
|
||||
│ et peut entraîner une suspension. │
|
||||
│ │
|
||||
│ [Reprendre ici] [Sécuriser mon compte] │
|
||||
└────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Boutons** :
|
||||
- **Reprendre ici** : Coupe l'autre device, reprend lecture sur ce device
|
||||
- **Sécuriser mon compte** : Lien vers changement mot de passe + déconnexion tous devices
|
||||
|
||||
#### Limite offline 30 jours
|
||||
|
||||
**Référence** : [08-mode-offline.md](08-mode-offline.md) section 11.2
|
||||
|
||||
```
|
||||
Contenus téléchargés valides 30 jours
|
||||
→ Après 30j sans connexion : contenus bloqués
|
||||
→ Reconnexion WiFi : renouvellement auto si <30j
|
||||
→ Notification J-3 : "X contenus expirent dans 3 jours"
|
||||
```
|
||||
|
||||
**Cohérence** : User offline déconnecté >30j ne peut plus écouter → force reconnexion → détection multi-devices redevient possible.
|
||||
|
||||
#### Détection abus (post-MVP)
|
||||
|
||||
Monitoring patterns suspects (backend analytics) :
|
||||
- >10 changements devices/jour (suspect)
|
||||
- Connexions alternées 2 villes éloignées répétées
|
||||
- Signalements users : "je n'ai jamais été à Marseille"
|
||||
|
||||
Action :
|
||||
→ Email investigation : "Activité suspecte, sécurisez votre compte"
|
||||
→ Si confirmé partage : suspension 7j + warning
|
||||
→ Récidive : ban définitif
|
||||
|
||||
**Pas d'action automatique** (évite faux positifs), juste flag modération manuelle.
|
||||
|
||||
**Justification KISS** :
|
||||
- ✅ **Simplicité technique** : pas de tracking GPS précis, pas de calcul distances, pas de faux positifs (TGV Paris→Lyon = légitime)
|
||||
- ✅ **Assume bonne foi** : majorité users honnêtes, partage compte = minorité, gestion réactive suffit
|
||||
- ✅ **Message dissuasif clair** : avertissement CGU dans message coupure, possibilité "Sécuriser compte" si suspicion piratage
|
||||
- ✅ **Protection revenus créateurs** : 1 abonnement = 1 personne = 1 écoute active
|
||||
- ✅ **UX claire** : message explicite, user comprend immédiatement
|
||||
|
||||
---
|
||||
|
||||
### 10.3 Contenus exclusifs Premium
|
||||
|
||||
**Décision** : Créateur décide (déjà couvert section 9.6)
|
||||
|
||||
**Rappel règles** :
|
||||
- Toggle "Réservé Premium" par contenu
|
||||
- Aucune limite de ratio gratuit/premium
|
||||
- Badge 👑 visible
|
||||
- Users gratuits : lecture bloquée avec CTA "Passez Premium"
|
||||
|
||||
**Impact algorithme** :
|
||||
- Contenus premium inclus dans recommandations
|
||||
- Si user gratuit → skip automatique (ne consomme pas slot)
|
||||
- Si user premium → diffusé normalement selon score
|
||||
|
||||
---
|
||||
|
||||
### 10.4 Avantages Premium
|
||||
|
||||
**Inclus dans l'abonnement** :
|
||||
|
||||
| Avantage | Gratuit | Premium |
|
||||
|----------|---------|---------|
|
||||
| **Publicités** | 1/5 contenus | 0 (aucune) |
|
||||
| **Contenus exclusifs** | ❌ Bloqués | ✅ Accès complet |
|
||||
| **Qualité audio** | 48 kbps Opus | 64 kbps Opus |
|
||||
| **Mode offline** | 50 contenus max | Illimité |
|
||||
| **Historique écoute** | 100 derniers | Illimité |
|
||||
|
||||
**Qualité audio** :
|
||||
- Gratuit : 48 kbps Opus (~20 MB/h) = très correct pour voix
|
||||
- Premium : 64 kbps Opus (~30 MB/h) = excellente qualité
|
||||
|
||||
**Justification différences** :
|
||||
- **0 pub** = argument principal (confort écoute)
|
||||
- **Qualité audio** = avantage tangible audiophiles
|
||||
- **Offline illimité** = use case road trips longs
|
||||
- **Pas d'over-engineering** : pas de badges cosmétiques, fonctionnalités sociales, etc. (focus essentiel)
|
||||
|
||||
---
|
||||
|
||||
### 10.5 Gestion abonnement
|
||||
|
||||
**Souscription** :
|
||||
|
||||
| Canal | Prestataire | Prix | Commission |
|
||||
|-------|-------------|------|------------|
|
||||
| **Web (desktop/mobile)** | Mangopay | 4.99€ | 1.8% + 0.18€ = 0.27€ |
|
||||
| **iOS App** | Apple In-App Purchase | 5.99€ | 30% (Apple) |
|
||||
| **Android App** | Google Play Billing | 5.99€ | 30% (Google) |
|
||||
|
||||
**Majoration mobile (5.99€)** :
|
||||
- Apple/Google prennent 30% de commission
|
||||
- RoadWave majore prix de 20% pour compenser
|
||||
- **Incitation web** : Email aux users "Abonnez-vous sur roadwave.com pour 4.99€/mois" (38% moins cher en frais !)
|
||||
|
||||
**Renouvellement automatique** :
|
||||
- Email rappel **7 jours avant** renouvellement
|
||||
- Email confirmation **après** renouvellement réussi
|
||||
- Retry automatique si échec paiement (3 tentatives sur 7 jours)
|
||||
- Annulation automatique après 3 échecs
|
||||
|
||||
**Annulation** :
|
||||
- Self-service dans Settings app : "Abonnement > Annuler"
|
||||
- Accès Premium maintenu jusqu'à **fin période payée**
|
||||
- Pas de remboursement prorata (standard industrie)
|
||||
- Email confirmation annulation avec date fin d'accès
|
||||
|
||||
**Réabonnement** :
|
||||
- Possibilité immédiate
|
||||
- ❌ Pas de nouvelle période d'essai (pas d'essai du tout)
|
||||
|
||||
**Architecture données** :
|
||||
|
||||
```sql
|
||||
CREATE TABLE subscriptions (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id UUID NOT NULL REFERENCES users(id) UNIQUE,
|
||||
mangopay_recurring_payin_id VARCHAR(255), -- Null si IAP
|
||||
mangopay_user_id VARCHAR(255), -- Null si IAP
|
||||
apple_transaction_id VARCHAR(255), -- Null si Mangopay
|
||||
google_purchase_token VARCHAR(255), -- Null si Mangopay
|
||||
status VARCHAR(50) NOT NULL, -- 'active', 'cancelled', 'expired', 'past_due'
|
||||
plan VARCHAR(50) NOT NULL, -- 'monthly', 'yearly'
|
||||
current_period_start TIMESTAMP NOT NULL,
|
||||
current_period_end TIMESTAMP NOT NULL,
|
||||
cancelled_at TIMESTAMP,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
**Vérification Premium en temps réel** :
|
||||
|
||||
```
|
||||
Cache Redis : premium:{user_id} → boolean (TTL 1h)
|
||||
Refresh via webhooks :
|
||||
- Mangopay : PAYIN_NORMAL_SUCCEEDED, PAYIN_NORMAL_FAILED
|
||||
- Apple : App Store Server Notifications
|
||||
- Google : Real-time Developer Notifications
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Récapitulatif Section 10
|
||||
Reference in New Issue
Block a user