test(gherkin): ajouter tests BDD pour toutes clarifications règles métier
Ajoute/modifie tests Gherkin pour couvrir les 7 sections clarifiées : 1. Algorithme recommandation (scoring intérêts nuls) : - Ajout scénarios scoring-recommandation.feature - Cas contenu géo-ancré proche avec intérêts nuls = recommandable - Comparaison scores géo vs intérêts 2. Audio-guides mode voiture (système double clic) : - Nouveau fichier systeme-double-clic-sortie.feature - Premier clic : passage mode manuel + séquence suivante - Deuxième clic <10s : sortie audio-guide - Détection hors itinéraire + reprise 3. Monétisation créateurs (soldes dormants + DAS2) : - Nouveau fichier soldes-dormants-inactifs.feature - Conservation indéfinie si actif - Emails 12/18 mois + versement forcé 18 mois + 30j - Exception soldes <10€ avec proposition don - Modification obligations-fiscales.feature - DAS2 systématique tous montants (même <1200€) 4. Skip et abonnement (neutralisation pénalités) : - Nouveau fichier skip-abonnes-neutralisation.feature - Skip <10s non-abonné : -0.5% - Skip <10s abonné : 0% (neutre) - Métriques engagement : abonnés ne pénalisent pas - Anti-raid naturel (sources non pertinentes) 5. Premium multi-devices (KISS) : - Nouveau fichier multi-devices-dernier-priorite.feature - Règle simple : dernier device prend toujours priorité - Offline connecté vs déconnecté - Détection abus post-MVP (pas automatique) 6. Mode offline (contenus supprimés) : - Nouveau fichier contenus-supprimes-pendant-offline.feature - Suppression immédiate à reconnexion - Modal si contenu en cours d'écoute - Popup récapitulative si 2+ contenus supprimés 7. Publicités (ciblage horaire + fuseaux horaires) : - Nouveau fichier ciblage-horaire-fuseaux-horaires.feature - Ciblage horaire = heure locale utilisateur - France entière = Métropole + DOM - Détection fuseau GPS/device/IP - Cas d'usage restaurant Guadeloupe, assureur national Couverture complète de toutes les règles métier clarifiées.
This commit is contained in:
204
features/api/interest-gauges/skip-abonnes-neutralisation.feature
Normal file
204
features/api/interest-gauges/skip-abonnes-neutralisation.feature
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
# language: fr
|
||||||
|
Fonctionnalité: Neutralisation des pénalités de skip pour abonnés
|
||||||
|
En tant que système de jauges d'intérêt
|
||||||
|
Je veux neutraliser les pénalités de skip pour les abonnés d'un créateur
|
||||||
|
Afin de reconnaître l'affinité globale malgré des skips ponctuels contextuels
|
||||||
|
|
||||||
|
Contexte:
|
||||||
|
Étant donné qu'un utilisateur existe avec les jauges suivantes:
|
||||||
|
| catégorie | niveau |
|
||||||
|
| Automobile | 45% |
|
||||||
|
| Voyage | 60% |
|
||||||
|
Et qu'un créateur "CreateurA" publie des contenus tagués "Automobile"
|
||||||
|
|
||||||
|
# Skip <10s - Utilisateur NON abonné
|
||||||
|
|
||||||
|
Scénario: Skip rapide <10s par non-abonné - Pénalité -0.5%
|
||||||
|
Étant donné que l'utilisateur n'est PAS abonné à "CreateurA"
|
||||||
|
Et qu'un contenu "Podcast Auto" de "CreateurA" est tagué "Automobile"
|
||||||
|
Et que la jauge "Automobile" est à 45%
|
||||||
|
Quand l'utilisateur skip le contenu après 5 secondes
|
||||||
|
Alors la jauge "Automobile" descend de -0.5%
|
||||||
|
Et la jauge "Automobile" passe de 45% à 44.5%
|
||||||
|
Et cela indique un désintérêt marqué pour ce contenu
|
||||||
|
|
||||||
|
Scénario: Skip rapide <10s par non-abonné - Colonne is_subscribed=false
|
||||||
|
Étant donné que l'utilisateur n'est PAS abonné à "CreateurA"
|
||||||
|
Et qu'un contenu est skippé après 8 secondes
|
||||||
|
Quand l'événement est enregistré dans user_listening_history
|
||||||
|
Alors la colonne is_subscribed = false
|
||||||
|
Et la colonne completion_rate = 0.05 (8s sur 160s)
|
||||||
|
Et la colonne source = "recommendation"
|
||||||
|
Et ce skip compte dans les métriques d'engagement du contenu
|
||||||
|
|
||||||
|
# Skip <10s - Utilisateur ABONNÉ
|
||||||
|
|
||||||
|
Scénario: Skip rapide <10s par abonné - Pénalité neutre 0%
|
||||||
|
Étant donné que l'utilisateur EST abonné à "CreateurA"
|
||||||
|
Et qu'un contenu "Podcast Auto" de "CreateurA" est tagué "Automobile"
|
||||||
|
Et que la jauge "Automobile" est à 45%
|
||||||
|
Quand l'utilisateur skip le contenu après 5 secondes
|
||||||
|
Alors la jauge "Automobile" reste à 45% (pénalité 0%)
|
||||||
|
Et aucune pénalité n'est appliquée
|
||||||
|
Et cela reflète que l'abonnement indique une affinité globale
|
||||||
|
|
||||||
|
Scénario: Skip rapide <10s par abonné - Colonne is_subscribed=true
|
||||||
|
Étant donné que l'utilisateur EST abonné à "CreateurA"
|
||||||
|
Et qu'un contenu est skippé après 7 secondes
|
||||||
|
Quand l'événement est enregistré dans user_listening_history
|
||||||
|
Alors la colonne is_subscribed = true
|
||||||
|
Et la colonne completion_rate = 0.04 (7s sur 180s)
|
||||||
|
Et la colonne source = "recommendation"
|
||||||
|
Et ce skip NE compte PAS dans les métriques d'engagement du contenu
|
||||||
|
|
||||||
|
# Calcul métriques engagement créateur
|
||||||
|
|
||||||
|
Scénario: Métriques engagement - Skip d'abonné ne pénalise pas
|
||||||
|
Étant donné qu'un contenu "Podcast A" de "CreateurA" a reçu:
|
||||||
|
| utilisateur | abonné ? | action | source | completion_rate |
|
||||||
|
| User1 | Non | Skip <10s | recommendation | 0.05 |
|
||||||
|
| User2 | Oui | Skip <10s | recommendation | 0.04 |
|
||||||
|
| User3 | Non | Écoute complète | recommendation | 0.90 |
|
||||||
|
| User4 | Oui | Skip <10s | recommendation | 0.03 |
|
||||||
|
| User5 | Non | Écoute partielle | recommendation | 0.60 |
|
||||||
|
Quand le système calcule les métriques d'engagement du contenu
|
||||||
|
Alors les écoutes pertinentes comptabilisées sont:
|
||||||
|
| utilisateur | comptabilisé ? | raison |
|
||||||
|
| User1 | ✅ Oui | Non-abonné, source pertinente |
|
||||||
|
| User2 | ❌ Non | Abonné, skip contextuel |
|
||||||
|
| User3 | ✅ Oui | Non-abonné, source pertinente |
|
||||||
|
| User4 | ❌ Non | Abonné, skip contextuel |
|
||||||
|
| User5 | ✅ Oui | Non-abonné, source pertinente |
|
||||||
|
Et le total écoutes pertinentes = 3 (User1, User3, User5)
|
||||||
|
Et le taux de complétion = 2 complètes (User3, User5 >80%) / 3 = 66.7%
|
||||||
|
|
||||||
|
# Sources d'écoute et neutralisation
|
||||||
|
|
||||||
|
Scénario: Skip d'abonné via "recommendation" - Ne compte pas
|
||||||
|
Étant donné que l'utilisateur EST abonné à "CreateurA"
|
||||||
|
Et qu'un contenu est recommandé via l'algorithme (source="recommendation")
|
||||||
|
Quand l'utilisateur skip après 5s
|
||||||
|
Alors l'écoute NE compte PAS dans "total écoutes" pour métriques
|
||||||
|
Et la pénalité jauge est neutralisée (0%)
|
||||||
|
|
||||||
|
Scénario: Skip d'abonné via "live_notification" - Ne compte pas
|
||||||
|
Étant donné que l'utilisateur EST abonné à "CreateurA"
|
||||||
|
Et que "CreateurA" publie un contenu en live
|
||||||
|
Et qu'une notification live est envoyée (source="live_notification")
|
||||||
|
Quand l'utilisateur skip après 6s
|
||||||
|
Alors l'écoute NE compte PAS dans "total écoutes" pour métriques
|
||||||
|
Et la pénalité jauge est neutralisée (0%)
|
||||||
|
|
||||||
|
Scénario: Skip d'abonné via "search" - Ne compte pas (indépendamment abonnement)
|
||||||
|
Étant donné que l'utilisateur EST abonné à "CreateurA"
|
||||||
|
Et qu'il trouve un contenu via la recherche (source="search")
|
||||||
|
Quand l'utilisateur skip après 4s
|
||||||
|
Alors l'écoute NE compte PAS dans "total écoutes" (source non pertinente)
|
||||||
|
Et la pénalité jauge est neutralisée (0%)
|
||||||
|
Et cela s'applique à tous users (abonnés ou non) pour source="search"
|
||||||
|
|
||||||
|
Scénario: Skip non-abonné via "direct_link" - Ne compte pas
|
||||||
|
Étant donné que l'utilisateur N'est PAS abonné à "CreateurA"
|
||||||
|
Et qu'il clique sur un lien direct partagé (source="direct_link")
|
||||||
|
Quand l'utilisateur skip après 3s
|
||||||
|
Alors l'écoute NE compte PAS dans "total écoutes" (source non pertinente)
|
||||||
|
Mais la pénalité jauge s'applique quand même (-0.5%) pour non-abonné
|
||||||
|
|
||||||
|
# Reproposition
|
||||||
|
|
||||||
|
Scénario: Skip <10s non-abonné - Pas de reproposition
|
||||||
|
Étant donné que l'utilisateur N'est PAS abonné à "CreateurA"
|
||||||
|
Et qu'un contenu est skippé après 8 secondes
|
||||||
|
Quand l'algorithme calcule les prochaines recommandations
|
||||||
|
Alors ce contenu n'est jamais reproposé à cet utilisateur
|
||||||
|
Car c'est un signal négatif clair de désintérêt
|
||||||
|
|
||||||
|
Scénario: Skip <10s abonné - Reproposition possible
|
||||||
|
Étant donné que l'utilisateur EST abonné à "CreateurA"
|
||||||
|
Et qu'un contenu est skippé après 7 secondes
|
||||||
|
Quand l'algorithme calcule les prochaines recommandations plusieurs jours plus tard
|
||||||
|
Alors ce contenu PEUT être reproposé à cet utilisateur
|
||||||
|
Car l'abonnement indique une affinité globale
|
||||||
|
Et le skip peut être contextuel ("pas maintenant", "pas ce sujet")
|
||||||
|
|
||||||
|
Scénario: Stockage is_subscribed dans user_content_history
|
||||||
|
Étant donné qu'un utilisateur EST abonné à "CreateurA" au moment de l'écoute
|
||||||
|
Quand un contenu de "CreateurA" est écouté/skippé
|
||||||
|
Alors la table user_content_history enregistre:
|
||||||
|
| colonne | valeur |
|
||||||
|
| user_id | 123 |
|
||||||
|
| content_id | 456 |
|
||||||
|
| creator_id | 789 (CreateurA) |
|
||||||
|
| is_subscribed | true |
|
||||||
|
| completion_rate| 0.05 |
|
||||||
|
| source | "recommendation" |
|
||||||
|
| listened_at | 2026-02-07 10:30:00 |
|
||||||
|
|
||||||
|
# Cohérence UX
|
||||||
|
|
||||||
|
Scénario: Abonnement = Signal affinité fort malgré skip ponctuel
|
||||||
|
Étant donné que l'utilisateur est abonné à "CreateurA" depuis 6 mois
|
||||||
|
Et qu'il a écouté 50 contenus de "CreateurA" avec 90% de complétion moyenne
|
||||||
|
Quand il skip 1 contenu après 5 secondes aujourd'hui
|
||||||
|
Alors ce skip ponctuel ne pénalise pas:
|
||||||
|
| aspect | impact |
|
||||||
|
| Jauges d'intérêt user | 0% (neutre) |
|
||||||
|
| Métriques engagement créateur | Ne compte pas dans total écoutes |
|
||||||
|
| Reproposition future | Contenu peut être reproposé |
|
||||||
|
Et cela reflète que le skip est contextuel, pas un rejet du créateur
|
||||||
|
|
||||||
|
# Anti-raid naturel
|
||||||
|
|
||||||
|
Scénario: Raid malveillant via liens directs - Inefficace
|
||||||
|
Étant donné qu'un groupe malveillant veut nuire à "CreateurA"
|
||||||
|
Et qu'ils partagent des liens directs pour inciter au skip massif
|
||||||
|
Quand 1000 personnes cliquent sur le lien et skip après 2s
|
||||||
|
Alors ces 1000 skips NE comptent PAS dans les métriques engagement
|
||||||
|
Car source="direct_link" n'est pas une source pertinente
|
||||||
|
Et "CreateurA" est protégé contre ce type de raid
|
||||||
|
|
||||||
|
Scénario: Raid malveillant via recherche - Inefficace
|
||||||
|
Étant donné qu'un groupe cherche à nuire à "CreateurA"
|
||||||
|
Et qu'ils trouvent le contenu via recherche et skip massivement
|
||||||
|
Quand 500 skips rapides arrivent via source="search"
|
||||||
|
Alors ces 500 skips NE comptent PAS dans les métriques engagement
|
||||||
|
Car source="search" n'est pas une source pertinente
|
||||||
|
Et "CreateurA" est protégé
|
||||||
|
|
||||||
|
# Cas limites
|
||||||
|
|
||||||
|
Scénario: Utilisateur s'abonne pendant l'écoute d'un contenu
|
||||||
|
Étant donné qu'un utilisateur N'est PAS abonné à "CreateurA"
|
||||||
|
Et qu'il démarre l'écoute d'un contenu de "CreateurA"
|
||||||
|
Et que is_subscribed=false est enregistré au démarrage
|
||||||
|
Quand l'utilisateur s'abonne à "CreateurA" pendant l'écoute
|
||||||
|
Et qu'il skip le contenu après 8 secondes
|
||||||
|
Alors is_subscribed=false reste enregistré (état au moment du démarrage)
|
||||||
|
Et la pénalité -0.5% s'applique (car non-abonné au démarrage)
|
||||||
|
|
||||||
|
Scénario: Utilisateur se désabonne puis écoute ancien contenu
|
||||||
|
Étant donné qu'un utilisateur ÉTAIT abonné à "CreateurA"
|
||||||
|
Et qu'il se désabonne aujourd'hui
|
||||||
|
Quand il écoute un ancien contenu de "CreateurA" demain
|
||||||
|
Et qu'il skip après 6 secondes
|
||||||
|
Alors is_subscribed=false (état au moment de l'écoute)
|
||||||
|
Et la pénalité -0.5% s'applique
|
||||||
|
Et l'écoute compte dans les métriques d'engagement
|
||||||
|
|
||||||
|
# Comparaison tableaux sources
|
||||||
|
|
||||||
|
Scénario: Table récapitulative sources et abonnements
|
||||||
|
Étant donné les règles de comptabilisation définies
|
||||||
|
Quand on résume le comportement par source et abonnement
|
||||||
|
Alors le tableau complet est:
|
||||||
|
| Source | Abonné ? | Skip <10s pénalise ? | Compte "total écoutes" ? |
|
||||||
|
| recommendation | Non | ✅ Oui (-0.5%) | ✅ Oui |
|
||||||
|
| recommendation | Oui | ❌ Non (0%) | ❌ Non |
|
||||||
|
| search | Peu imp. | Variable* | ❌ Non |
|
||||||
|
| direct_link | Peu imp. | Variable* | ❌ Non |
|
||||||
|
| profile | Peu imp. | Variable* | ❌ Non |
|
||||||
|
| history | Peu imp. | Variable* | ❌ Non |
|
||||||
|
| live_notification | Non | ✅ Oui (-0.5%) | ✅ Oui |
|
||||||
|
| live_notification | Oui | ❌ Non (0%) | ❌ Non |
|
||||||
|
| audio_guide | Peu imp. | ❌ Non | ❌ Non |
|
||||||
|
(* Variable = -0.5% si non-abonné, 0% si abonné, mais source non pertinente donc pas dans métriques)
|
||||||
@@ -319,3 +319,119 @@ Fonctionnalité: Obligations fiscales
|
|||||||
Et m'aider à comprendre ce que je dois déclarer
|
Et m'aider à comprendre ce que je dois déclarer
|
||||||
Mais il ne peut pas me conseiller fiscalement (pas expert-comptable)
|
Mais il ne peut pas me conseiller fiscalement (pas expert-comptable)
|
||||||
Et il me recommande de consulter un expert-comptable si nécessaire
|
Et il me recommande de consulter un expert-comptable si nécessaire
|
||||||
|
|
||||||
|
# Règle: DAS2 systématique tous montants (même <1200€)
|
||||||
|
|
||||||
|
Scénario: DAS2 systématique - Créateur avec revenus <1200€
|
||||||
|
Étant donné que j'ai touché 450.00€ en 2025
|
||||||
|
Et que le seuil légal DAS2 est 1200€/an
|
||||||
|
Quand RoadWave génère les DAS2 en janvier 2026
|
||||||
|
Alors ma DAS2 est quand même envoyée à la DGFIP
|
||||||
|
Et le montant déclaré est 450.00€
|
||||||
|
Et je reçois une copie par email
|
||||||
|
Et la DAS2 est disponible dans mon dashboard
|
||||||
|
|
||||||
|
Scénario: DAS2 systématique - Créateur avec 50€ seulement
|
||||||
|
Étant donné que j'ai touché seulement 50.00€ en 2025
|
||||||
|
Quand RoadWave génère les DAS2 en janvier 2026
|
||||||
|
Alors ma DAS2 est envoyée à la DGFIP avec 50.00€
|
||||||
|
Et je reçois un email de confirmation:
|
||||||
|
"""
|
||||||
|
Objet : Votre déclaration fiscale 2025 RoadWave
|
||||||
|
|
||||||
|
Bonjour [Créateur],
|
||||||
|
|
||||||
|
Vos revenus RoadWave 2025 ont été déclarés aux impôts (DAS2) :
|
||||||
|
- Revenus publicité : 30.00€
|
||||||
|
- Revenus Premium : 20.00€
|
||||||
|
- Total déclaré : 50.00€
|
||||||
|
|
||||||
|
Cette déclaration a été transmise à la DGFIP.
|
||||||
|
Vous devez inclure ce montant dans votre déclaration personnelle.
|
||||||
|
|
||||||
|
Télécharger le justificatif : [Lien PDF]
|
||||||
|
|
||||||
|
Cordialement,
|
||||||
|
L'équipe RoadWave
|
||||||
|
"""
|
||||||
|
|
||||||
|
Scénario: Justification juridique DAS2 systématique
|
||||||
|
Étant donné que le seuil légal DAS2 est 1200€/an
|
||||||
|
Et que RoadWave déclare tous montants (même <1200€)
|
||||||
|
Quand un créateur demande pourquoi sa DAS2 <1200€ est envoyée
|
||||||
|
Alors la justification est:
|
||||||
|
"""
|
||||||
|
Bien que le seuil légal DAS2 soit 1200€/an,
|
||||||
|
rien n'interdit de déclarer les montants inférieurs.
|
||||||
|
|
||||||
|
Au contraire, cela renforce la transparence et
|
||||||
|
protège RoadWave en cas de contrôle fiscal.
|
||||||
|
"""
|
||||||
|
|
||||||
|
Scénario: Avantages DAS2 systématique pour RoadWave
|
||||||
|
Étant donné que RoadWave déclare tous montants
|
||||||
|
Quand un audit fiscal a lieu
|
||||||
|
Alors les avantages sont:
|
||||||
|
| avantage | description |
|
||||||
|
| Conformité maximale | Aucune zone grise, 100% transparent |
|
||||||
|
| Protection juridique RoadWave | Traçabilité totale de tous les paiements |
|
||||||
|
| Simplicité technique | Même processus pour tous (pas de filtrage) |
|
||||||
|
| Créateur a justificatif | Justificatif fourni même pour petits montants |
|
||||||
|
| Coût 0€ | DAS2 = déclaration obligatoire gratuite |
|
||||||
|
|
||||||
|
Scénario: Avantages DAS2 systématique pour créateurs
|
||||||
|
Étant donné que j'ai touché 800€ en 2025
|
||||||
|
Et que je reçois une DAS2 même si <1200€
|
||||||
|
Quand je fais ma déclaration d'impôts
|
||||||
|
Alors j'ai un justificatif officiel pour déclarer mes revenus
|
||||||
|
Et je peux prouver l'origine de mes revenus en cas de contrôle
|
||||||
|
Et je suis protégé même si les montants sont faibles
|
||||||
|
|
||||||
|
Scénario: Statistiques admin - DAS2 systématique
|
||||||
|
Étant donné qu'un admin RoadWave consulte les métriques DAS2 2025
|
||||||
|
Quand il accède au dashboard admin
|
||||||
|
Alors il voit:
|
||||||
|
| métrique | valeur 2025 |
|
||||||
|
| Créateurs monétisés totaux | 1,247 |
|
||||||
|
| DAS2 transmises (tous montants) | 1,247 |
|
||||||
|
| Dont DAS2 <1200€ | 400 (32%) |
|
||||||
|
| Dont DAS2 ≥1200€ | 847 (68%) |
|
||||||
|
| Revenus totaux déclarés | 1,890,345€ |
|
||||||
|
| Taux conformité | 100% |
|
||||||
|
|
||||||
|
Scénario: Comparaison autres plateformes - Twitch, YouTube
|
||||||
|
Étant donné que Twitch et YouTube ont un seuil DAS2 1200€
|
||||||
|
Et que RoadWave déclare tous montants (même <1200€)
|
||||||
|
Quand on compare les pratiques
|
||||||
|
Alors RoadWave est plus transparent:
|
||||||
|
| Plateforme | Seuil DAS2 | Montants <1200€ déclarés ? |
|
||||||
|
| Twitch | 1200€ | ❌ Non (non documenté) |
|
||||||
|
| YouTube | 1200€ | ❌ Non (non documenté) |
|
||||||
|
| RoadWave | 0€ | ✅ Oui (tous montants) |
|
||||||
|
|
||||||
|
Scénario: Email créateur DAS2 <1200€ - Clarification obligation
|
||||||
|
Étant donné que j'ai touché 800€ en 2025
|
||||||
|
Quand je reçois l'email DAS2
|
||||||
|
Alors l'email contient une clarification:
|
||||||
|
"""
|
||||||
|
ℹ️ Bien que vos revenus soient inférieurs au seuil légal DAS2 (1200€),
|
||||||
|
RoadWave déclare tous les montants pour assurer une transparence maximale.
|
||||||
|
|
||||||
|
Vous devez déclarer ces 800€ dans votre déclaration d'impôts personnelle
|
||||||
|
(formulaire 2042 C PRO pour auto-entrepreneurs ou déclaration IS/IR selon votre statut).
|
||||||
|
|
||||||
|
Ce justificatif vous protège en cas de contrôle fiscal.
|
||||||
|
"""
|
||||||
|
|
||||||
|
Scénario: Créateur avec plusieurs plateformes - Cumul seuil 1200€
|
||||||
|
Étant donné que j'ai touché:
|
||||||
|
| plateforme | revenus 2025 |
|
||||||
|
| RoadWave | 800€ |
|
||||||
|
| YouTube | 600€ |
|
||||||
|
| Twitch | 400€ |
|
||||||
|
Et que le seuil DAS2 légal est 1200€ par plateforme
|
||||||
|
Quand les DAS2 sont envoyées
|
||||||
|
Alors RoadWave envoie une DAS2 pour 800€
|
||||||
|
Mais YouTube et Twitch n'envoient pas de DAS2 (<1200€)
|
||||||
|
Et je dois quand même déclarer les 1800€ totaux aux impôts
|
||||||
|
Et la DAS2 RoadWave me donne un justificatif partiel
|
||||||
|
|||||||
235
features/api/monetisation/soldes-dormants-inactifs.feature
Normal file
235
features/api/monetisation/soldes-dormants-inactifs.feature
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
# language: fr
|
||||||
|
Fonctionnalité: Gestion des soldes dormants et créateurs inactifs
|
||||||
|
En tant que plateforme RoadWave
|
||||||
|
Je veux gérer les soldes des créateurs inactifs de manière équitable
|
||||||
|
Afin de restituer l'argent aux créateurs plutôt que de le confisquer
|
||||||
|
|
||||||
|
Contexte:
|
||||||
|
Étant donné que je suis un créateur monétisé
|
||||||
|
Et que mon KYC est validé
|
||||||
|
Et que mon solde actuel est 45.00€
|
||||||
|
|
||||||
|
# Conservation du solde pour créateurs actifs
|
||||||
|
|
||||||
|
Scénario: Créateur actif - Solde conservé indéfiniment
|
||||||
|
Étant donné que je publie au moins 1 contenu par mois
|
||||||
|
Ou que je me connecte au dashboard au moins 1 fois par mois
|
||||||
|
Quand 24 mois se sont écoulés
|
||||||
|
Alors mon solde de 45.00€ est toujours conservé
|
||||||
|
Et aucun email de préavis n'est envoyé
|
||||||
|
Et le solde reste visible dans mon dashboard
|
||||||
|
|
||||||
|
Scénario: Créateur actif sporadique - Pas de pénalité
|
||||||
|
Étant donné que je publie 1 contenu tous les 2-3 mois
|
||||||
|
Ou que je me connecte au dashboard tous les 2 mois
|
||||||
|
Quand 18 mois d'activité sporadique se sont écoulés
|
||||||
|
Alors mon solde est conservé normalement
|
||||||
|
Et je ne reçois aucun email d'avertissement
|
||||||
|
Et mon statut reste "Créateur actif"
|
||||||
|
|
||||||
|
# Créateur inactif - Emails préventifs
|
||||||
|
|
||||||
|
Scénario: Inactivité 12 mois - Email préventif 1
|
||||||
|
Étant donné que je n'ai publié aucun contenu depuis 12 mois
|
||||||
|
Et que je ne me suis pas connecté au dashboard depuis 12 mois
|
||||||
|
Quand le système détecte 12 mois d'inactivité
|
||||||
|
Alors je reçois un email avec le sujet:
|
||||||
|
"""
|
||||||
|
⚠️ Votre solde RoadWave (45.00€) - Action requise
|
||||||
|
"""
|
||||||
|
Et l'email contient:
|
||||||
|
"""
|
||||||
|
Vous n'avez pas publié de contenu depuis 12 mois.
|
||||||
|
Votre solde actuel : 45.00€
|
||||||
|
|
||||||
|
🔔 Si vous restez inactif 6 mois supplémentaires :
|
||||||
|
→ Versement automatique (frais bancaires déduits)
|
||||||
|
→ Montant net estimé : 44.64€
|
||||||
|
|
||||||
|
💡 Pour éviter le versement anticipé :
|
||||||
|
- Publiez un nouveau contenu, OU
|
||||||
|
- Connectez-vous à votre dashboard créateur
|
||||||
|
"""
|
||||||
|
Et un lien vers le dashboard est fourni
|
||||||
|
|
||||||
|
Scénario: Inactivité 18 mois - Email préventif 2 (préavis final)
|
||||||
|
Étant donné que je n'ai publié aucun contenu depuis 18 mois
|
||||||
|
Et que je ne me suis pas connecté au dashboard depuis 18 mois
|
||||||
|
Quand le système détecte 18 mois d'inactivité
|
||||||
|
Alors je reçois un email avec le sujet:
|
||||||
|
"""
|
||||||
|
📢 Versement automatique dans 30 jours
|
||||||
|
"""
|
||||||
|
Et l'email contient:
|
||||||
|
"""
|
||||||
|
Vous n'avez pas publié de contenu depuis 18 mois.
|
||||||
|
|
||||||
|
⏰ Votre solde sera versé automatiquement dans 30 jours :
|
||||||
|
- Solde actuel : 45.00€
|
||||||
|
- Frais bancaires Mangopay SEPA : 0.36€ (1.8% + 0.18€)
|
||||||
|
- Montant net : 44.64€
|
||||||
|
|
||||||
|
Ce versement est automatique pour restituer votre argent.
|
||||||
|
|
||||||
|
💡 Pour conserver le solde jusqu'à 50€ :
|
||||||
|
- Publiez un nouveau contenu, OU
|
||||||
|
- Connectez-vous à votre dashboard
|
||||||
|
"""
|
||||||
|
|
||||||
|
Scénario: Inactivité 18 mois + 30 jours - Versement forcé
|
||||||
|
Étant donné que je n'ai publié aucun contenu depuis 18 mois + 30 jours
|
||||||
|
Et que je ne me suis pas connecté au dashboard depuis 18 mois + 30 jours
|
||||||
|
Quand le système détecte 18 mois + 30 jours d'inactivité
|
||||||
|
Alors un versement SEPA automatique est initié vers mon RIB
|
||||||
|
Et le montant versé est 45.00€ - 0.36€ = 44.64€
|
||||||
|
Et je reçois un email de confirmation:
|
||||||
|
"""
|
||||||
|
✅ Versement automatique effectué
|
||||||
|
|
||||||
|
Votre solde RoadWave a été versé :
|
||||||
|
- Montant brut : 45.00€
|
||||||
|
- Frais bancaires : 0.36€
|
||||||
|
- Montant net versé : 44.64€
|
||||||
|
|
||||||
|
Le virement devrait arriver sur votre compte dans 1-3 jours.
|
||||||
|
"""
|
||||||
|
|
||||||
|
Scénario: Inactivité 18 mois + 37 jours - Purge données comptables
|
||||||
|
Étant donné qu'un versement forcé a été effectué il y a 7 jours
|
||||||
|
Quand le système détecte 18 mois + 37 jours d'inactivité
|
||||||
|
Alors les données comptables temporaires sont purgées
|
||||||
|
Mais les logs sont conservés 10 ans (obligation RGPD)
|
||||||
|
Et l'historique de paiement reste visible dans mon profil
|
||||||
|
|
||||||
|
# Exception soldes <10€
|
||||||
|
|
||||||
|
Scénario: Solde <10€ après 18 mois - Proposition de don
|
||||||
|
Étant donné que mon solde actuel est 8.50€
|
||||||
|
Et que je suis inactif depuis 18 mois
|
||||||
|
Quand le système détecte 18 mois d'inactivité
|
||||||
|
Alors je reçois un email avec proposition:
|
||||||
|
"""
|
||||||
|
Votre solde actuel : 8.50€
|
||||||
|
|
||||||
|
⚠️ Les frais bancaires (0.36€) représentent 4.2% du montant.
|
||||||
|
|
||||||
|
💡 Options disponibles :
|
||||||
|
1. Don à une association (frais 0€, 100% reversé)
|
||||||
|
2. Conservation jusqu'à atteindre 50€
|
||||||
|
3. Versement avec frais déduits (8.14€ net)
|
||||||
|
|
||||||
|
Choisissez votre option : [Lien formulaire]
|
||||||
|
"""
|
||||||
|
|
||||||
|
Scénario: Solde <10€ - Choix option "Don association"
|
||||||
|
Étant donné que mon solde est 8.50€
|
||||||
|
Et que j'ai choisi "Don à une association"
|
||||||
|
Quand le don est effectué
|
||||||
|
Alors 8.50€ (100%) sont reversés à l'association
|
||||||
|
Et je reçois un reçu fiscal si applicable
|
||||||
|
Et mon solde est remis à 0€
|
||||||
|
|
||||||
|
Scénario: Solde <10€ - Choix option "Conservation jusqu'à 50€"
|
||||||
|
Étant donné que mon solde est 8.50€
|
||||||
|
Et que j'ai choisi "Conservation jusqu'à 50€"
|
||||||
|
Quand l'option est validée
|
||||||
|
Alors mon solde est conservé indéfiniment
|
||||||
|
Et je peux me reconnecter à tout moment pour publier du contenu
|
||||||
|
Et atteindre les 50€ pour un paiement standard
|
||||||
|
|
||||||
|
Scénario: Solde <10€ - Choix option "Versement avec frais"
|
||||||
|
Étant donné que mon solde est 8.50€
|
||||||
|
Et que j'ai choisi "Versement avec frais déduits"
|
||||||
|
Quand le versement est initié
|
||||||
|
Alors je reçois 8.50€ - 0.36€ = 8.14€ net
|
||||||
|
Et un email de confirmation est envoyé
|
||||||
|
|
||||||
|
Scénario: Solde <10€ - Pas de réponse + inactivité continue - Versement forcé
|
||||||
|
Étant donné que mon solde est 8.50€
|
||||||
|
Et que je n'ai pas répondu à l'email de proposition
|
||||||
|
Et que l'inactivité continue pendant 30 jours supplémentaires
|
||||||
|
Quand le délai de 18 mois + 30 jours est atteint
|
||||||
|
Alors le versement est effectué quand même (équité)
|
||||||
|
Et je reçois 8.14€ net après frais
|
||||||
|
|
||||||
|
# Frais bancaires
|
||||||
|
|
||||||
|
Scénario: Calcul frais bancaires Mangopay SEPA
|
||||||
|
Étant donné que mon solde est de <montant>
|
||||||
|
Quand le versement forcé est initié
|
||||||
|
Alors les frais Mangopay SEPA sont: 1.8% + 0.18€
|
||||||
|
Et le montant net est calculé comme suit:
|
||||||
|
| Solde brut | Frais (1.8% + 0.18€) | Montant net |
|
||||||
|
| 45.00€ | 0.36€ | 44.64€ |
|
||||||
|
| 30.00€ | 0.36€ | 29.64€ |
|
||||||
|
| 100.00€ | 1.98€ | 98.02€ |
|
||||||
|
| 8.50€ | 0.36€ | 8.14€ |
|
||||||
|
|
||||||
|
Scénario: Transparence frais bancaires dans emails
|
||||||
|
Étant donné qu'un email de préavis est envoyé
|
||||||
|
Quand je lis l'email
|
||||||
|
Alors les frais bancaires sont explicitement mentionnés:
|
||||||
|
"""
|
||||||
|
Frais bancaires Mangopay SEPA : 0.36€ (1.8% + 0.18€)
|
||||||
|
"""
|
||||||
|
Et le montant net final est clairement indiqué
|
||||||
|
|
||||||
|
# Comparaison Twitch
|
||||||
|
|
||||||
|
Scénario: Comparaison avec Twitch - Versement forcé vs Forfeiture
|
||||||
|
Étant donné que Twitch confisque (forfeiture) les soldes après 24 mois
|
||||||
|
Et que RoadWave verse (restitue) les soldes après 18 mois
|
||||||
|
Alors RoadWave est plus équitable:
|
||||||
|
| Critère | Twitch | RoadWave | Avantage |
|
||||||
|
| Seuil paiement | 50-100$ | 50€ | Aligné |
|
||||||
|
| Délai inactivité | 24 mois | 18 mois | Plus court |
|
||||||
|
| Action fin délai | Forfeiture (perte argent) | Versement forcé (récupère) | ✅ RoadWave |
|
||||||
|
| Emails préventifs | Non documenté | 12 mois + 18 mois + 30j | ✅ RoadWave |
|
||||||
|
| Frais bancaires | Non documenté | Déduits + annoncés | ✅ RoadWave |
|
||||||
|
|
||||||
|
# Réactivation après inactivité
|
||||||
|
|
||||||
|
Scénario: Reconnexion après 12 mois d'inactivité - Solde conservé
|
||||||
|
Étant donné que je suis inactif depuis 12 mois
|
||||||
|
Et que mon solde est 45.00€
|
||||||
|
Quand je me connecte au dashboard
|
||||||
|
Alors mon solde est toujours de 45.00€
|
||||||
|
Et le compteur d'inactivité est remis à 0
|
||||||
|
Et aucun versement forcé n'aura lieu
|
||||||
|
|
||||||
|
Scénario: Publication contenu après 17 mois d'inactivité - Annulation versement
|
||||||
|
Étant donné que je suis inactif depuis 17 mois
|
||||||
|
Et qu'un email de préavis a été envoyé
|
||||||
|
Quand je publie un nouveau contenu
|
||||||
|
Alors le processus de versement forcé est annulé
|
||||||
|
Et mon solde reste conservé normalement
|
||||||
|
Et je redeviens "Créateur actif"
|
||||||
|
|
||||||
|
Scénario: Reconnexion 1 jour avant versement forcé - Annulation
|
||||||
|
Étant donné que je suis inactif depuis 18 mois + 29 jours
|
||||||
|
Et que le versement forcé est prévu demain
|
||||||
|
Quand je me connecte au dashboard aujourd'hui
|
||||||
|
Alors le versement forcé est annulé
|
||||||
|
Et mon solde est conservé
|
||||||
|
Et un message confirme: "Versement automatique annulé. Solde conservé."
|
||||||
|
|
||||||
|
# Cas limites
|
||||||
|
|
||||||
|
Scénario: Créateur avec plusieurs contenus anciens mais inactif
|
||||||
|
Étant donné que j'ai publié 50 contenus il y a 2 ans
|
||||||
|
Et que ces contenus génèrent encore des revenus passifs
|
||||||
|
Mais que je ne me connecte plus au dashboard depuis 18 mois
|
||||||
|
Quand le système détecte 18 mois d'inactivité
|
||||||
|
Alors je suis considéré "inactif" (aucune connexion dashboard)
|
||||||
|
Et je reçois les emails de préavis normalement
|
||||||
|
Et le versement forcé s'applique après 18 mois + 30 jours
|
||||||
|
|
||||||
|
Scénario: Créateur avec abonnés fidèles mais inactif
|
||||||
|
Étant donné que j'ai 500 abonnés
|
||||||
|
Et que mes anciens contenus génèrent encore des revenus
|
||||||
|
Mais que je ne publie plus de contenus depuis 18 mois
|
||||||
|
Et que je ne me connecte plus au dashboard depuis 18 mois
|
||||||
|
Quand le système détecte l'inactivité
|
||||||
|
Alors je reçois les emails de préavis
|
||||||
|
Et le versement forcé s'applique normalement
|
||||||
|
Car l'activité comptabilisée = connexion dashboard OU publication contenu
|
||||||
233
features/api/premium/multi-devices-dernier-priorite.feature
Normal file
233
features/api/premium/multi-devices-dernier-priorite.feature
Normal file
@@ -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 |
|
||||||
238
features/api/publicites/ciblage-horaire-fuseaux-horaires.feature
Normal file
238
features/api/publicites/ciblage-horaire-fuseaux-horaires.feature
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
# language: fr
|
||||||
|
Fonctionnalité: Ciblage horaire publicités et gestion fuseaux horaires
|
||||||
|
En tant que publicitaire
|
||||||
|
Je veux cibler mes publicités sur des plages horaires en heure locale utilisateur
|
||||||
|
Afin d'optimiser mes campagnes selon les moments de la journée (rush matin/soir)
|
||||||
|
|
||||||
|
Contexte:
|
||||||
|
Étant donné qu'un publicitaire crée une campagne publicitaire
|
||||||
|
|
||||||
|
# Règle 1 : Ciblage horaire = Heure locale utilisateur
|
||||||
|
|
||||||
|
Scénario: Campagne "7h-9h" diffuse selon heure locale de chaque utilisateur
|
||||||
|
Étant donné qu'une campagne est configurée avec plage horaire "7h-9h"
|
||||||
|
Et que nous sommes le 7 février 2026 à 8h00 UTC
|
||||||
|
Quand le système vérifie la diffusion pour différents utilisateurs:
|
||||||
|
| utilisateur | localisation | fuseau horaire | heure locale | diffusion ? |
|
||||||
|
| User Marseille | Marseille | Europe/Paris | 9h00 | ✅ Oui |
|
||||||
|
| User Guadeloupe | Pointe-à-Pitre | America/Guadeloupe | 4h00 | ❌ Non |
|
||||||
|
| User Réunion | Saint-Denis | Indian/Reunion | 12h00 | ❌ Non |
|
||||||
|
| User Lyon | Lyon | Europe/Paris | 9h00 | ✅ Oui |
|
||||||
|
Alors la pub est diffusée uniquement aux utilisateurs dans la plage 7h-9h leur heure locale
|
||||||
|
|
||||||
|
Scénario: Campagne "17h-19h" (rush soir) - Heure locale de chaque utilisateur
|
||||||
|
Étant donné qu'une campagne est configurée avec plage horaire "17h-19h"
|
||||||
|
Et que nous sommes le 7 février 2026 à 17h30 UTC
|
||||||
|
Quand le système vérifie la diffusion pour:
|
||||||
|
| utilisateur | fuseau horaire | heure locale | diffusion ? |
|
||||||
|
| User Paris | Europe/Paris | 18h30 | ✅ Oui |
|
||||||
|
| User Martinique | America/Martinique | 13h30 | ❌ Non |
|
||||||
|
| User Réunion | Indian/Reunion | 21h30 | ❌ Non |
|
||||||
|
Alors la pub est diffusée uniquement à User Paris (18h30 dans 17h-19h)
|
||||||
|
|
||||||
|
Scénario: Campagne 24/7 (toute la journée) - Pas de restriction horaire
|
||||||
|
Étant donné qu'une campagne n'a AUCUNE restriction horaire
|
||||||
|
Quand le système vérifie la diffusion
|
||||||
|
Alors la pub est diffusée à tout moment (0h-23h)
|
||||||
|
Et tous les utilisateurs sont éligibles quelle que soit l'heure locale
|
||||||
|
|
||||||
|
# Détection fuseau horaire utilisateur
|
||||||
|
|
||||||
|
Scénario: Détection fuseau via GPS (méthode primaire)
|
||||||
|
Étant donné qu'un utilisateur a le GPS activé
|
||||||
|
Et que sa position GPS est (latitude: 48.8566, longitude: 2.3522)
|
||||||
|
Quand le système détecte le fuseau horaire
|
||||||
|
Alors le fuseau horaire déterminé est "Europe/Paris"
|
||||||
|
Et l'heure locale est calculée avec ce fuseau
|
||||||
|
|
||||||
|
Scénario: Détection fuseau via paramètres device (si GPS désactivé)
|
||||||
|
Étant donné qu'un utilisateur a le GPS désactivé
|
||||||
|
Mais que les paramètres OS indiquent fuseau "America/Guadeloupe"
|
||||||
|
Quand le système détecte le fuseau horaire
|
||||||
|
Alors le fuseau horaire utilisé est "America/Guadeloupe"
|
||||||
|
Et l'heure locale est calculée avec ce fuseau
|
||||||
|
|
||||||
|
Scénario: Fallback IP geolocation (si GPS désactivé ET paramètres indisponibles)
|
||||||
|
Étant donné qu'un utilisateur a le GPS désactivé
|
||||||
|
Et que les paramètres device ne sont pas accessibles
|
||||||
|
Mais que l'IP est géolocalisée à La Réunion
|
||||||
|
Quand le système détecte le fuseau horaire
|
||||||
|
Alors le fuseau horaire approximatif est "Indian/Reunion"
|
||||||
|
Et l'heure locale est calculée avec ce fuseau
|
||||||
|
|
||||||
|
# Règle 2 : Ciblage "France" = Métropole + DOM
|
||||||
|
|
||||||
|
Scénario: Ciblage "France entière" inclut Métropole + DOM
|
||||||
|
Étant donné qu'une campagne cible "France (nationale)"
|
||||||
|
Quand le système vérifie les utilisateurs éligibles
|
||||||
|
Alors les utilisateurs inclus sont:
|
||||||
|
| zone | départements / territoires |
|
||||||
|
| France métropolitaine | 96 départements (01 à 95, 2A, 2B, etc.) |
|
||||||
|
| Guadeloupe | 971 |
|
||||||
|
| Martinique | 972 |
|
||||||
|
| Guyane | 973 |
|
||||||
|
| Réunion | 974 |
|
||||||
|
| Mayotte | 976 |
|
||||||
|
|
||||||
|
Scénario: Publicitaire affine ciblage "Région Provence-Alpes-Côte d'Azur"
|
||||||
|
Étant donné qu'une campagne cible "Région Provence-Alpes-Côte d'Azur"
|
||||||
|
Quand le système vérifie les utilisateurs éligibles
|
||||||
|
Alors seuls les utilisateurs en Métropole dans cette région sont ciblés
|
||||||
|
Et les utilisateurs DOM (Guadeloupe, Réunion, etc.) ne sont PAS ciblés
|
||||||
|
|
||||||
|
Scénario: Publicitaire affine ciblage "Département 971 (Guadeloupe)"
|
||||||
|
Étant donné qu'une campagne cible "Département 971"
|
||||||
|
Quand le système vérifie les utilisateurs éligibles
|
||||||
|
Alors seuls les utilisateurs en Guadeloupe sont ciblés
|
||||||
|
Et les utilisateurs Métropole ne sont PAS ciblés
|
||||||
|
|
||||||
|
Scénario: Publicitaire affine ciblage "Ville Pointe-à-Pitre"
|
||||||
|
Étant donné qu'une campagne cible "Ville Pointe-à-Pitre"
|
||||||
|
Quand le système vérifie les utilisateurs éligibles
|
||||||
|
Alors seuls les utilisateurs à Pointe-à-Pitre (Guadeloupe) sont ciblés
|
||||||
|
|
||||||
|
# Interface publicitaire
|
||||||
|
|
||||||
|
Scénario: Interface création campagne - Note explicite sur inclusion DOM
|
||||||
|
Étant donné qu'un publicitaire accède au formulaire de création campagne
|
||||||
|
Quand il sélectionne "National (France entière)"
|
||||||
|
Alors une note informative s'affiche:
|
||||||
|
"""
|
||||||
|
ℹ️ Note : "National (France entière)" inclut les DOM
|
||||||
|
(Guadeloupe, Martinique, Réunion, Guyane, Mayotte)
|
||||||
|
"""
|
||||||
|
|
||||||
|
Scénario: Interface - Liste déroulante ciblage géographique
|
||||||
|
Étant donné qu'un publicitaire configure le ciblage géographique
|
||||||
|
Quand il consulte la liste déroulante
|
||||||
|
Alors les options disponibles sont:
|
||||||
|
| option | description |
|
||||||
|
| National (France entière) | Métropole + DOM |
|
||||||
|
| Région | Ex: Provence-Alpes-Côte d'Azur (Métropole)|
|
||||||
|
| Département | Ex: 13 (Métropole) ou 971 (Guadeloupe) |
|
||||||
|
| Ville | Ex: Marseille, Pointe-à-Pitre |
|
||||||
|
| Point GPS + rayon | Latitude/Longitude + rayon en km |
|
||||||
|
|
||||||
|
# Cas d'usage publicitaire
|
||||||
|
|
||||||
|
Scénario: Restaurant local Guadeloupe - Ciblage département 971 + horaires 12h-14h
|
||||||
|
Étant donné qu'un restaurant à Pointe-à-Pitre crée une campagne
|
||||||
|
Et que le ciblage est "Département 971 (Guadeloupe)"
|
||||||
|
Et que les horaires sont "12h-14h" (rush déjeuner)
|
||||||
|
Quand le système diffuse les pubs à 12h30
|
||||||
|
Alors les utilisateurs Guadeloupe à 12h30 heure locale reçoivent la pub
|
||||||
|
Et les utilisateurs Métropole ne reçoivent PAS la pub (hors zone géo)
|
||||||
|
Et les utilisateurs Martinique ne reçoivent PAS la pub (hors zone géo)
|
||||||
|
|
||||||
|
Scénario: Assureur national - Ciblage France + horaires 7h-9h + 17h-19h
|
||||||
|
Étant donné qu'un assureur national crée une campagne
|
||||||
|
Et que le ciblage est "France (nationale)"
|
||||||
|
Et que les horaires sont "7h-9h" et "17h-19h" (rush matin/soir)
|
||||||
|
Quand le système diffuse les pubs
|
||||||
|
Alors à 8h locale Marseille, User Marseille reçoit la pub
|
||||||
|
Et à 8h locale Réunion, User Réunion reçoit la pub
|
||||||
|
Et User Réunion reçoit la pub à 8h locale (= 5h métropole, mais c'est son rush matin)
|
||||||
|
|
||||||
|
Scénario: Utilisateur en vacances change de fuseau horaire
|
||||||
|
Étant donné qu'un utilisateur habite Paris (Europe/Paris)
|
||||||
|
Et qu'il part en vacances à La Réunion
|
||||||
|
Et qu'il télécharge 50 contenus + pubs avant de partir
|
||||||
|
Quand l'utilisateur écoute à 8h locale Réunion (device détecte fuseau)
|
||||||
|
Alors le filtrage des pubs utilise l'heure locale Réunion (8h)
|
||||||
|
Et les pubs ciblées "7h-9h" sont diffusées normalement
|
||||||
|
|
||||||
|
# Implémentation technique
|
||||||
|
|
||||||
|
Scénario: Calcul heure locale via PostgreSQL AT TIME ZONE
|
||||||
|
Étant donné qu'une campagne a une plage horaire "7h-9h"
|
||||||
|
Et qu'un utilisateur a le fuseau "Indian/Reunion" (UTC+4)
|
||||||
|
Quand le backend vérifie l'éligibilité à 08h00 UTC
|
||||||
|
Alors la requête SQL utilise:
|
||||||
|
"""sql
|
||||||
|
SELECT
|
||||||
|
EXTRACT(HOUR FROM NOW() AT TIME ZONE 'Indian/Reunion') AS local_hour
|
||||||
|
-- Résultat: 12 (8h UTC + 4h = 12h locale)
|
||||||
|
"""
|
||||||
|
Et local_hour = 12 n'est PAS dans [7, 8, 9], donc pas de diffusion
|
||||||
|
|
||||||
|
Scénario: Base IANA Time Zone pour conversion GPS → fuseau
|
||||||
|
Étant donné qu'un utilisateur a GPS (48.8566, 2.3522)
|
||||||
|
Quand le système convertit GPS → fuseau horaire
|
||||||
|
Alors la base IANA Time Zone est utilisée
|
||||||
|
Et le fuseau déterminé est "Europe/Paris"
|
||||||
|
Et la base est mise à jour régulièrement (changements DST, fuseaux)
|
||||||
|
|
||||||
|
# Justification
|
||||||
|
|
||||||
|
Scénario: Comparaison avec standard industrie - Google Ads, Facebook Ads
|
||||||
|
Étant donné qu'on compare avec Google Ads et Facebook Ads
|
||||||
|
Quand on évalue le comportement du ciblage horaire
|
||||||
|
Alors RoadWave suit le standard:
|
||||||
|
| plateforme | ciblage horaire | référence temporelle | norme |
|
||||||
|
| Google Ads | 7h-9h | Heure locale user | ✅ Standard |
|
||||||
|
| Facebook Ads | 7h-9h | Heure locale user | ✅ Standard |
|
||||||
|
| RoadWave | 7h-9h | Heure locale user | ✅ Standard |
|
||||||
|
|
||||||
|
Scénario: Avantages UX intuitive pour publicitaires
|
||||||
|
Étant donné qu'un publicitaire configure "7h-9h"
|
||||||
|
Quand il pense "rush matin"
|
||||||
|
Alors il n'a pas besoin de comprendre UTC
|
||||||
|
Et "7h-9h" signifie "matin partout en France"
|
||||||
|
Et l'UX est intuitive:
|
||||||
|
| avantage | description |
|
||||||
|
| UX intuitive publicitaires | "7h-9h" = matin partout, pas UTC compliqué |
|
||||||
|
| Équité géographique | Pas de discrimination DOM-TOM |
|
||||||
|
| Simplicité technique | Détection automatique fuseau (GPS/device) |
|
||||||
|
| Standard industrie | Même comportement Google/Facebook |
|
||||||
|
|
||||||
|
# Cas limites
|
||||||
|
|
||||||
|
Scénario: Utilisateur change de fuseau pendant campagne active
|
||||||
|
Étant donné qu'un utilisateur écoute des pubs en métropole (Europe/Paris)
|
||||||
|
Et qu'une campagne cible "7h-9h"
|
||||||
|
Quand l'utilisateur part en vacances Réunion (Indian/Reunion)
|
||||||
|
Alors le système détecte le nouveau fuseau horaire automatiquement
|
||||||
|
Et les pubs "7h-9h" sont filtrées selon l'heure locale Réunion
|
||||||
|
Et l'utilisateur reçoit les pubs à 7h-9h heure locale Réunion
|
||||||
|
|
||||||
|
Scénario: Changement heure d'été/hiver (DST) - Gestion automatique
|
||||||
|
Étant donné qu'une campagne cible "7h-9h" en Europe/Paris
|
||||||
|
Et que le changement heure d'été arrive (dernier dimanche mars)
|
||||||
|
Quand l'heure passe de 2h à 3h (UTC+1 → UTC+2)
|
||||||
|
Alors le système utilise automatiquement le nouveau décalage UTC
|
||||||
|
Et les pubs "7h-9h" continuent de se diffuser à 7h-9h heure locale
|
||||||
|
Et PostgreSQL AT TIME ZONE gère automatiquement le DST
|
||||||
|
|
||||||
|
# Métriques dashboard publicitaire
|
||||||
|
|
||||||
|
Scénario: Dashboard publicitaire - Répartition géographique diffusions
|
||||||
|
Étant donné qu'une campagne "France (nationale)" + "7h-9h" est active
|
||||||
|
Quand le publicitaire consulte le dashboard
|
||||||
|
Alors la répartition géographique affiche:
|
||||||
|
| zone | impressions | % total |
|
||||||
|
| Île-de-France | 45,000 | 60% |
|
||||||
|
| Provence-Alpes | 15,000 | 20% |
|
||||||
|
| Guadeloupe | 3,000 | 4% |
|
||||||
|
| Réunion | 4,500 | 6% |
|
||||||
|
| Autres | 7,500 | 10% |
|
||||||
|
|
||||||
|
Scénario: Dashboard publicitaire - Répartition horaire diffusions
|
||||||
|
Étant donné qu'une campagne "7h-9h" est active
|
||||||
|
Quand le publicitaire consulte le dashboard
|
||||||
|
Alors un graphique horaire affiche:
|
||||||
|
| heure locale | impressions |
|
||||||
|
| 7h | 12,000 |
|
||||||
|
| 8h | 18,000 |
|
||||||
|
| 9h | 5,000 |
|
||||||
|
Et les impressions hors plage (autres heures) = 0
|
||||||
|
|
||||||
|
Scénario: Validation création campagne - Cohérence géo + horaires
|
||||||
|
Étant donné qu'un publicitaire crée une campagne
|
||||||
|
Et qu'il sélectionne "Département 971 (Guadeloupe)"
|
||||||
|
Et qu'il configure horaires "7h-9h"
|
||||||
|
Quand il valide la campagne
|
||||||
|
Alors le système confirme:
|
||||||
|
"""
|
||||||
|
Votre campagne sera diffusée à 7h-9h heure locale Guadeloupe (UTC-4).
|
||||||
|
Estimation: 2,500 impressions/jour.
|
||||||
|
"""
|
||||||
@@ -178,3 +178,44 @@ Fonctionnalité: Formule de scoring et recommandation
|
|||||||
Quand l'utilisateur demande le contenu suivant
|
Quand l'utilisateur demande le contenu suivant
|
||||||
Alors l'algorithme recalcule les scores
|
Alors l'algorithme recalcule les scores
|
||||||
Et prend en compte les nouveaux contenus publiés
|
Et prend en compte les nouveaux contenus publiés
|
||||||
|
|
||||||
|
# Règle: Score géo excellent + intérêts nuls = recommandation possible (MVP)
|
||||||
|
Scénario: Contenu géo-ancré proche avec intérêts nuls reste recommandable
|
||||||
|
Étant donné qu'un contenu géo-ancré "Info trafic local" est à 100m de l'utilisateur
|
||||||
|
Et que le contenu est tagué "Actualités" et "Trafic"
|
||||||
|
Et que l'utilisateur a des jauges à 0% pour ces tags (aucun intérêt marqué)
|
||||||
|
Et que le score_geo = 1.0 (distance 100m, excellent)
|
||||||
|
Et que le score_interets = 0.0 (jauges nulles)
|
||||||
|
Et que le score_engagement = 0.6 (contenu récent, peu d'historique)
|
||||||
|
Quand l'algorithme calcule le score_final pour un contenu géo-ancré
|
||||||
|
Alors score_final = (1.0 × 0.7) + (0.0 × 0.1) + (0.6 × 0.2)
|
||||||
|
Et score_final = 0.7 + 0.0 + 0.12 = 0.82
|
||||||
|
Et le contenu peut être recommandé malgré l'intérêt nul
|
||||||
|
Et ce comportement est accepté pour MVP car:
|
||||||
|
| justification |
|
||||||
|
| Le quota 6 contenus géolocalisés/h protège du spam |
|
||||||
|
| L'info peut être utile contextuellement |
|
||||||
|
| La distinction info/divertissement est reportée post-MVP|
|
||||||
|
|
||||||
|
Scénario: Contenu géo-neutre loin avec intérêts élevés recommandé
|
||||||
|
Étant donné qu'un contenu géo-neutre "Podcast philosophie" est à 150 km
|
||||||
|
Et que le contenu est tagué "Philosophie" et "Culture"
|
||||||
|
Et que l'utilisateur a des jauges à 90% pour ces tags
|
||||||
|
Et que le score_geo = 0.25 (150 km de distance)
|
||||||
|
Et que le score_interets = 0.9 (jauges élevées)
|
||||||
|
Et que le score_engagement = 0.7
|
||||||
|
Quand l'algorithme calcule le score_final pour un contenu géo-neutre
|
||||||
|
Alors score_final = (0.25 × 0.2) + (0.9 × 0.6) + (0.7 × 0.2)
|
||||||
|
Et score_final = 0.05 + 0.54 + 0.14 = 0.73
|
||||||
|
Et le contenu est bien recommandé grâce aux intérêts élevés
|
||||||
|
|
||||||
|
Scénario: Comparaison scores - géo proche vs intérêts élevés
|
||||||
|
Étant donné deux contenus:
|
||||||
|
| contenu | type | distance | score_geo | tags | jauges_user | score_interets | score_engagement |
|
||||||
|
| Info trafic locale | Géo-ancré | 100m | 1.0 | Trafic | 0% | 0.0 | 0.6 |
|
||||||
|
| Podcast philosophie | Géo-neutre | 150 km | 0.25 | Philosophie | 90% | 0.9 | 0.7 |
|
||||||
|
Quand l'algorithme calcule les scores finaux
|
||||||
|
Alors score_final("Info trafic locale") = 0.82
|
||||||
|
Et score_final("Podcast philosophie") = 0.73
|
||||||
|
Et "Info trafic locale" sera proposé avant "Podcast philosophie"
|
||||||
|
Et les deux contenus sont recommandables selon leurs critères différents
|
||||||
|
|||||||
192
features/ui/audio-guides/systeme-double-clic-sortie.feature
Normal file
192
features/ui/audio-guides/systeme-double-clic-sortie.feature
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
# language: fr
|
||||||
|
Fonctionnalité: Système double clic et sortie audio-guide mode voiture
|
||||||
|
En tant qu'utilisateur en voiture
|
||||||
|
Je veux pouvoir désactiver le GPS automatique et sortir de l'audio-guide facilement
|
||||||
|
Afin de gérer les situations d'embouteillage ou de changement de plan
|
||||||
|
|
||||||
|
Contexte:
|
||||||
|
Étant donné qu'un utilisateur est en mode voiture
|
||||||
|
Et qu'un audio-guide de 8 séquences est actif
|
||||||
|
Et que le mode GPS automatique est activé par défaut
|
||||||
|
Et que la séquence 2 est en cours de lecture
|
||||||
|
|
||||||
|
# Comportement bouton [▶|] Suivant
|
||||||
|
|
||||||
|
Scénario: Premier clic Suivant - Passage en mode manuel
|
||||||
|
Étant donné que le mode GPS auto est actif
|
||||||
|
Et que la séquence 2 vient de se terminer
|
||||||
|
Et que le prochain point GPS (séquence 3) est à 2 km
|
||||||
|
Quand l'utilisateur clique sur le bouton [▶|] Suivant
|
||||||
|
Alors le GPS automatique est désactivé
|
||||||
|
Et le mode bascule en "mode manuel"
|
||||||
|
Et la séquence 3 démarre immédiatement
|
||||||
|
Et un toast s'affiche pendant 3 secondes:
|
||||||
|
"""
|
||||||
|
Mode manuel activé. Cliquez à nouveau pour quitter l'audio-guide.
|
||||||
|
"""
|
||||||
|
Et un timer de 10 secondes démarre en arrière-plan
|
||||||
|
|
||||||
|
Scénario: Deuxième clic Suivant dans les 10 secondes - Sortie audio-guide
|
||||||
|
Étant donné que le mode manuel vient d'être activé il y a 5 secondes
|
||||||
|
Et que la séquence 3 est en cours de lecture
|
||||||
|
Quand l'utilisateur clique à nouveau sur [▶|] Suivant
|
||||||
|
Alors l'audio-guide est mis en pause
|
||||||
|
Et l'historique de progression est conservé (séquence 3 à X:XX)
|
||||||
|
Et l'application retourne au flux normal de recommandation
|
||||||
|
Et un toast s'affiche pendant 2 secondes: "Audio-guide en pause"
|
||||||
|
|
||||||
|
Scénario: Clic Suivant après 10 secondes - Navigation normale
|
||||||
|
Étant donné que le mode manuel est actif depuis 12 secondes
|
||||||
|
Et que la séquence 3 est en cours
|
||||||
|
Quand l'utilisateur clique sur [▶|] Suivant
|
||||||
|
Alors la séquence 4 démarre immédiatement
|
||||||
|
Et le timer de 10 secondes redémarre
|
||||||
|
Et le mode reste en "mode manuel"
|
||||||
|
Et aucune sortie d'audio-guide ne se produit
|
||||||
|
|
||||||
|
Scénario: Clics multiples Suivant en mode manuel
|
||||||
|
Étant donné que le mode manuel est actif
|
||||||
|
Et que l'utilisateur clique sur [▶|] pour passer séquence 3 → 4
|
||||||
|
Et que 5 secondes se passent
|
||||||
|
Quand l'utilisateur clique à nouveau sur [▶|] pour passer séquence 4 → 5
|
||||||
|
Alors la séquence 5 démarre
|
||||||
|
Et le timer de 10 secondes redémarre à chaque clic
|
||||||
|
Et l'utilisateur peut naviguer normalement entre les séquences
|
||||||
|
|
||||||
|
Scénario: Double clic rapide accidentel - sortie immédiate
|
||||||
|
Étant donné que le mode GPS auto est actif
|
||||||
|
Et que la séquence 2 vient de se terminer
|
||||||
|
Quand l'utilisateur clique sur [▶|] (clic 1)
|
||||||
|
Et que l'utilisateur clique immédiatement sur [▶|] (clic 2 à <2s)
|
||||||
|
Alors l'audio-guide est mis en pause après le clic 2
|
||||||
|
Et l'utilisateur retourne au flux normal
|
||||||
|
Et un toast confirme: "Audio-guide en pause"
|
||||||
|
|
||||||
|
# Comportement bouton [|◀] Précédent
|
||||||
|
|
||||||
|
Scénario: Bouton Précédent dans audio-guide GPS auto
|
||||||
|
Étant donné que le mode GPS auto est actif
|
||||||
|
Et que la séquence 3 est en cours
|
||||||
|
Quand l'utilisateur clique sur [|◀] Précédent
|
||||||
|
Alors la séquence 2 démarre
|
||||||
|
Et l'audio-guide reste actif
|
||||||
|
Et le mode GPS auto reste actif
|
||||||
|
|
||||||
|
Scénario: Bouton Précédent dans audio-guide mode manuel
|
||||||
|
Étant donné que le mode manuel est actif
|
||||||
|
Et que la séquence 5 est en cours
|
||||||
|
Quand l'utilisateur clique sur [|◀] Précédent
|
||||||
|
Alors la séquence 4 démarre
|
||||||
|
Et l'audio-guide reste actif
|
||||||
|
Et le mode manuel reste actif
|
||||||
|
|
||||||
|
Scénario: Bouton Précédent hors audio-guide - Reprend audio-guide si contenu précédent
|
||||||
|
Étant donné que l'utilisateur a quitté l'audio-guide "Safari du Paugre"
|
||||||
|
Et que l'utilisateur écoute un contenu normal "Podcast A"
|
||||||
|
Quand l'utilisateur clique sur [|◀] Précédent
|
||||||
|
Alors l'audio-guide "Safari du Paugre" reprend
|
||||||
|
Et la dernière séquence écoutée (séquence 3) reprend
|
||||||
|
|
||||||
|
# Détection et reprise après détour
|
||||||
|
|
||||||
|
Scénario: Détection hors itinéraire >1 km pendant >10 min
|
||||||
|
Étant donné que l'audio-guide est actif (mode GPS auto ou manuel)
|
||||||
|
Et que l'utilisateur s'éloigne à 1.2 km de tous les points GPS
|
||||||
|
Et que cette situation dure 11 minutes
|
||||||
|
Quand le système détecte le hors itinéraire
|
||||||
|
Alors un toast s'affiche: "Audio-guide en pause (hors itinéraire)"
|
||||||
|
Et l'icône de l'audio-guide passe en gris (inactif)
|
||||||
|
Et la lecture continue du contenu en cours s'arrête
|
||||||
|
|
||||||
|
Scénario: Retour sur itinéraire <100m d'un point non écouté
|
||||||
|
Étant donné que l'audio-guide est en pause (hors itinéraire)
|
||||||
|
Et que l'utilisateur revient à 80m du point GPS séquence 5 (non écoutée)
|
||||||
|
Quand le système détecte le retour sur itinéraire
|
||||||
|
Alors une popup s'affiche:
|
||||||
|
"""
|
||||||
|
Reprendre l'audio-guide à la séquence 5 ?
|
||||||
|
[Reprendre] [Voir liste] [Ignorer]
|
||||||
|
"""
|
||||||
|
|
||||||
|
Scénario: Action "Reprendre" après retour sur itinéraire
|
||||||
|
Étant donné que la popup de reprise est affichée
|
||||||
|
Quand l'utilisateur clique sur [Reprendre]
|
||||||
|
Alors la séquence 5 démarre immédiatement
|
||||||
|
Et l'audio-guide redevient actif
|
||||||
|
Et l'icône repasse en couleur normale
|
||||||
|
|
||||||
|
Scénario: Action "Voir liste" après retour sur itinéraire
|
||||||
|
Étant donné que la popup de reprise est affichée
|
||||||
|
Quand l'utilisateur clique sur [Voir liste]
|
||||||
|
Alors la liste complète des séquences s'affiche
|
||||||
|
Et l'utilisateur peut choisir manuellement quelle séquence écouter
|
||||||
|
|
||||||
|
Scénario: Action "Ignorer" après retour sur itinéraire
|
||||||
|
Étant donné que la popup de reprise est affichée
|
||||||
|
Quand l'utilisateur clique sur [Ignorer]
|
||||||
|
Alors la popup se ferme
|
||||||
|
Et l'audio-guide reste en pause
|
||||||
|
Et l'utilisateur continue le flux normal de recommandation
|
||||||
|
|
||||||
|
# Respect des clics manuels
|
||||||
|
|
||||||
|
Scénario: Séquence skippée manuellement non reproposée automatiquement
|
||||||
|
Étant donné que l'utilisateur est en mode manuel
|
||||||
|
Et que l'utilisateur clique [▶|] pour passer de séquence 3 à séquence 4
|
||||||
|
Et que la séquence 3 est marquée "skippée volontairement"
|
||||||
|
Quand l'utilisateur revient à 50m du point GPS séquence 3
|
||||||
|
Alors aucune popup de reprise automatique ne s'affiche
|
||||||
|
Et l'utilisateur peut revenir manuellement via liste séquences s'il le souhaite
|
||||||
|
|
||||||
|
Scénario: Séquence skippée par GPS (point manqué) reproposable
|
||||||
|
Étant donné que l'utilisateur a dépassé un point GPS à 110m (rayon 30m)
|
||||||
|
Et que la séquence 3 a été marquée "point manqué" (pas de skip manuel)
|
||||||
|
Quand l'utilisateur revient à 80m du point GPS séquence 3
|
||||||
|
Alors une popup de reprise s'affiche:
|
||||||
|
"""
|
||||||
|
Reprendre la séquence 3 ?
|
||||||
|
[Reprendre] [Voir liste] [Ignorer]
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Mode manuel persistant
|
||||||
|
|
||||||
|
Scénario: Mode manuel persiste jusqu'à fin audio-guide
|
||||||
|
Étant donné que le mode manuel est activé en séquence 3
|
||||||
|
Quand l'utilisateur navigue jusqu'à la séquence 8 (dernière)
|
||||||
|
Alors le mode manuel reste actif durant toutes les séquences
|
||||||
|
Et le GPS automatique n'est jamais réactivé
|
||||||
|
|
||||||
|
Scénario: Reset mode GPS auto au redémarrage audio-guide
|
||||||
|
Étant donné que l'utilisateur a quitté l'audio-guide en mode manuel
|
||||||
|
Et que plusieurs heures se sont écoulées
|
||||||
|
Quand l'utilisateur relance l'audio-guide "Safari du Paugre"
|
||||||
|
Alors le mode GPS automatique est réactivé par défaut
|
||||||
|
Et l'utilisateur peut à nouveau passer en mode manuel s'il le souhaite
|
||||||
|
|
||||||
|
# Cas d'usage réel : embouteillage
|
||||||
|
|
||||||
|
Scénario: Embouteillage - Passage manuel puis sortie
|
||||||
|
Étant donné que l'utilisateur écoute la séquence 2 "Les lions"
|
||||||
|
Et que la séquence 2 se termine
|
||||||
|
Et que le prochain point GPS (séquence 3) est à 3 km
|
||||||
|
Et que l'utilisateur est bloqué dans un embouteillage
|
||||||
|
Et que l'ETA indique "≈ 30 minutes"
|
||||||
|
Quand l'utilisateur clique [▶|] (clic 1) pour passer en mode manuel
|
||||||
|
Alors la séquence 3 démarre immédiatement
|
||||||
|
Et le toast indique: "Mode manuel activé. Cliquez à nouveau pour quitter."
|
||||||
|
Quand l'utilisateur clique [▶|] (clic 2) dans les 8 secondes
|
||||||
|
Alors l'audio-guide est mis en pause
|
||||||
|
Et l'utilisateur retourne au flux normal (podcasts, musique)
|
||||||
|
Et la progression est sauvegardée (séquence 3 à X:XX)
|
||||||
|
|
||||||
|
Scénario: Reprise audio-guide après sortie embouteillage
|
||||||
|
Étant donné que l'utilisateur a quitté l'audio-guide en séquence 3
|
||||||
|
Et que plusieurs heures plus tard, l'utilisateur se reconnecte
|
||||||
|
Et que l'utilisateur est à 80m du point GPS séquence 4
|
||||||
|
Quand le système détecte la proximité
|
||||||
|
Alors une popup de reprise s'affiche:
|
||||||
|
"""
|
||||||
|
Reprendre l'audio-guide "Safari du Paugre" ?
|
||||||
|
Progression : 3/8 séquences
|
||||||
|
[Reprendre] [Recommencer] [Voir liste]
|
||||||
|
"""
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user