diff --git a/features/api/interest-gauges/skip-abonnes-neutralisation.feature b/features/api/interest-gauges/skip-abonnes-neutralisation.feature new file mode 100644 index 0000000..99f2002 --- /dev/null +++ b/features/api/interest-gauges/skip-abonnes-neutralisation.feature @@ -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) diff --git a/features/api/monetisation/obligations-fiscales.feature b/features/api/monetisation/obligations-fiscales.feature index b613378..d5f6df1 100644 --- a/features/api/monetisation/obligations-fiscales.feature +++ b/features/api/monetisation/obligations-fiscales.feature @@ -319,3 +319,119 @@ Fonctionnalité: Obligations fiscales Et m'aider à comprendre ce que je dois déclarer Mais il ne peut pas me conseiller fiscalement (pas expert-comptable) 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 diff --git a/features/api/monetisation/soldes-dormants-inactifs.feature b/features/api/monetisation/soldes-dormants-inactifs.feature new file mode 100644 index 0000000..9516fca --- /dev/null +++ b/features/api/monetisation/soldes-dormants-inactifs.feature @@ -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 + 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 diff --git a/features/api/premium/multi-devices-dernier-priorite.feature b/features/api/premium/multi-devices-dernier-priorite.feature new file mode 100644 index 0000000..7b3eb0a --- /dev/null +++ b/features/api/premium/multi-devices-dernier-priorite.feature @@ -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 | diff --git a/features/api/publicites/ciblage-horaire-fuseaux-horaires.feature b/features/api/publicites/ciblage-horaire-fuseaux-horaires.feature new file mode 100644 index 0000000..8785fc7 --- /dev/null +++ b/features/api/publicites/ciblage-horaire-fuseaux-horaires.feature @@ -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. + """ diff --git a/features/api/recommendation/scoring-recommandation.feature b/features/api/recommendation/scoring-recommandation.feature index 67a8cd4..581345c 100644 --- a/features/api/recommendation/scoring-recommandation.feature +++ b/features/api/recommendation/scoring-recommandation.feature @@ -178,3 +178,44 @@ Fonctionnalité: Formule de scoring et recommandation Quand l'utilisateur demande le contenu suivant Alors l'algorithme recalcule les scores 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 diff --git a/features/ui/audio-guides/systeme-double-clic-sortie.feature b/features/ui/audio-guides/systeme-double-clic-sortie.feature new file mode 100644 index 0000000..b3c8b9d --- /dev/null +++ b/features/ui/audio-guides/systeme-double-clic-sortie.feature @@ -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] + """ diff --git a/features/ui/mode-offline/contenus-supprimes-pendant-offline.feature b/features/ui/mode-offline/contenus-supprimes-pendant-offline.feature new file mode 100644 index 0000000..9d778cc --- /dev/null +++ b/features/ui/mode-offline/contenus-supprimes-pendant-offline.feature @@ -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