refactor(docs): réorganiser la documentation selon principes DDD
Réorganise la documentation du projet selon les principes du Domain-Driven Design (DDD) pour améliorer la cohésion, la maintenabilité et l'alignement avec l'architecture modulaire du backend. **Structure cible:** ``` docs/domains/ ├── README.md (Context Map) ├── _shared/ (Core Domain) ├── recommendation/ (Supporting Subdomain) ├── content/ (Supporting Subdomain) ├── moderation/ (Supporting Subdomain) ├── advertising/ (Generic Subdomain) ├── premium/ (Generic Subdomain) └── monetization/ (Generic Subdomain) ``` **Changements effectués:** Phase 1: Création de l'arborescence des 7 bounded contexts Phase 2: Déplacement des règles métier (01-19) vers domains/*/rules/ Phase 3: Déplacement des diagrammes d'entités vers domains/*/entities/ Phase 4: Déplacement des diagrammes flux/états/séquences vers domains/*/ Phase 5: Création des README.md pour chaque domaine Phase 6: Déplacement des features Gherkin vers domains/*/features/ Phase 7: Création du Context Map (domains/README.md) Phase 8: Mise à jour de mkdocs.yml pour la nouvelle navigation Phase 9: Correction automatique des liens internes (script fix-markdown-links.sh) Phase 10: Nettoyage de l'ancienne structure (regles-metier/, diagrammes/, features/) **Configuration des tests:** - Makefile: godog run docs/domains/*/features/ - scripts/generate-bdd-docs.py: features_dir → docs/domains **Avantages:** ✅ Cohésion forte: toute la doc d'un domaine au même endroit ✅ Couplage faible: domaines indépendants, dépendances explicites ✅ Navigabilité améliorée: README par domaine = entrée claire ✅ Alignement code/docs: miroir de backend/internal/ ✅ Onboarding facilité: exploration domaine par domaine ✅ Tests BDD intégrés: features au plus près des règles métier Voir docs/REFACTOR-DDD.md pour le plan complet.
This commit is contained in:
@@ -0,0 +1,296 @@
|
||||
# language: fr
|
||||
Fonctionnalité: API - Pas de dégradation temporelle
|
||||
En tant qu'API backend
|
||||
Je veux que les jauges n'évoluent que par les actions utilisateur
|
||||
Afin d'avoir un comportement prévisible sans automatisme caché
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est disponible
|
||||
Et que la base de données PostgreSQL est accessible
|
||||
|
||||
Scénario: API ne dégrade jamais les jauges automatiquement
|
||||
Étant donné qu'un utilisateur "user123" a une jauge "Économie" à 80% en base
|
||||
Et que la colonne updated_at = "2026-01-01T10:00:00Z"
|
||||
Quand 30 jours s'écoulent sans activité
|
||||
Et je GET /api/v1/users/user123/interest-gauges le "2026-02-01T10:00:00Z"
|
||||
Alors le statut de réponse est 200
|
||||
Et la jauge "Économie" est toujours à 80%
|
||||
Et aucune dégradation temporelle n'a été appliquée
|
||||
Et la colonne updated_at n'a pas changé
|
||||
|
||||
Scénario: Aucun cron job de dégradation n'existe
|
||||
Étant donné que le système vérifie les tâches cron planifiées
|
||||
Quand je liste tous les cron jobs du backend
|
||||
Alors aucun job nommé "degrade_interest_gauges" n'existe
|
||||
Et aucun job périodique ne modifie la table interest_gauges
|
||||
Et aucune ressource CPU n'est consommée pour la dégradation
|
||||
|
||||
Scénario: API GET jauges après 6 mois d'inactivité
|
||||
Étant donné qu'un utilisateur "user123" a les jauges suivantes:
|
||||
| catégorie | niveau | updated_at |
|
||||
| Automobile | 75% | 2025-08-01T10:00:00 |
|
||||
| Voyage | 60% | 2025-08-01T10:00:00 |
|
||||
| Musique | 45% | 2025-08-01T10:00:00 |
|
||||
Et qu'il ne se connecte pas pendant 6 mois
|
||||
Quand je GET /api/v1/users/user123/interest-gauges le "2026-02-01T10:00:00Z"
|
||||
Alors le statut de réponse est 200
|
||||
Et les jauges sont exactement les mêmes:
|
||||
| catégorie | niveau |
|
||||
| Automobile | 75% |
|
||||
| Voyage | 60% |
|
||||
| Musique | 45% |
|
||||
Et aucune modification n'a été appliquée
|
||||
|
||||
Scénario: Évolution par actions utilisateur uniquement
|
||||
Étant donné qu'un utilisateur "user123" a une jauge "Économie" à 80%
|
||||
Et qu'il skip 50 contenus "Économie" en 1 an
|
||||
Quand je calcule l'évolution via les events
|
||||
Alors la jauge "Économie" descend via les skips:
|
||||
| action | impact | nouveau_niveau |
|
||||
| 50 skips × -0.5%| -25% | 55% |
|
||||
Et la dégradation vient des actions, pas du temps
|
||||
Et la colonne updated_at reflète la date du dernier skip
|
||||
|
||||
Scénario: API POST réinitialiser centres d'intérêt
|
||||
Étant donné qu'un utilisateur "user123" a des jauges personnalisées:
|
||||
| catégorie | niveau |
|
||||
| Automobile | 75% |
|
||||
| Voyage | 60% |
|
||||
| Économie | 34% |
|
||||
| Sport | 88% |
|
||||
Quand je POST /api/v1/users/user123/interest-gauges/reset
|
||||
"""json
|
||||
{
|
||||
"confirmation": true
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 200
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"message": "Vos centres d'intérêt ont été réinitialisés",
|
||||
"previous_gauges_saved": true,
|
||||
"new_gauges": {
|
||||
"all_categories": 50
|
||||
}
|
||||
}
|
||||
"""
|
||||
Et en base de données, toutes les jauges de "user123" sont à 50%:
|
||||
| catégorie | niveau |
|
||||
| Automobile | 50 |
|
||||
| Voyage | 50 |
|
||||
| Économie | 50 |
|
||||
| Sport | 50 |
|
||||
| Musique | 50 |
|
||||
| Technologie | 50 |
|
||||
| Santé | 50 |
|
||||
| Politique | 50 |
|
||||
| Cryptomonnaie | 50 |
|
||||
| Culture générale | 50 |
|
||||
| Famille | 50 |
|
||||
| Amour | 50 |
|
||||
|
||||
Scénario: API sauvegarde jauges précédentes avant réinitialisation
|
||||
Étant donné qu'un utilisateur "user123" a des jauges personnalisées
|
||||
Quand je POST /api/v1/users/user123/interest-gauges/reset
|
||||
"""json
|
||||
{
|
||||
"confirmation": true
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 200
|
||||
Et une ligne est insérée dans interest_gauges_snapshots:
|
||||
| user_id | snapshot_type | snapshot_date | gauges_json |
|
||||
| user123 | manual_reset | 2026-02-02T14:00:00 | {"Automobile": 75, "Voyage": 60, ...} |
|
||||
Et l'historique permet de restaurer si besoin
|
||||
|
||||
Scénario: API rejette réinitialisation sans confirmation
|
||||
Quand je POST /api/v1/users/user123/interest-gauges/reset
|
||||
"""json
|
||||
{
|
||||
"confirmation": false
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 400
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"error": "CONFIRMATION_REQUIRED",
|
||||
"message": "Vous devez confirmer la réinitialisation"
|
||||
}
|
||||
"""
|
||||
Et les jauges ne sont pas modifiées
|
||||
|
||||
Scénario: API recommandations après réinitialisation
|
||||
Étant donné qu'un utilisateur "user123" avait "Économie" à 85%
|
||||
Et qu'il réinitialise ses jauges (toutes à 50%)
|
||||
Quand je POST /api/v1/recommendations
|
||||
"""json
|
||||
{
|
||||
"user_id": "user123",
|
||||
"latitude": 48.8566,
|
||||
"longitude": 2.3522,
|
||||
"limit": 10
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 200
|
||||
Et les recommandations utilisent 50% pour toutes les catégories
|
||||
Et plus aucun biais "Économie" n'est appliqué
|
||||
Et la géolocalisation redevient le critère principal
|
||||
|
||||
Scénario: Historique d'écoute conservé après réinitialisation
|
||||
Étant donné qu'un utilisateur "user123" a écouté 500 contenus
|
||||
Et que la table listening_history contient 500 lignes pour "user123"
|
||||
Quand je POST /api/v1/users/user123/interest-gauges/reset
|
||||
Alors le statut de réponse est 200
|
||||
Et la table listening_history conserve toujours les 500 lignes
|
||||
Et aucune donnée d'historique n'est supprimée
|
||||
Et l'utilisateur peut toujours consulter ses anciens contenus écoutés
|
||||
|
||||
Scénario: API GET historique après réinitialisation
|
||||
Étant donné qu'un utilisateur "user123" a réinitialisé ses jauges
|
||||
Quand je GET /api/v1/users/user123/listening-history
|
||||
Alors le statut de réponse est 200
|
||||
Et la réponse contient tous les anciens contenus écoutés
|
||||
Et l'historique est intact
|
||||
|
||||
Scénario: API enregistre timestamp réinitialisation
|
||||
Étant donné qu'un utilisateur "user123" réinitialise ses jauges
|
||||
Quand je POST /api/v1/users/user123/interest-gauges/reset
|
||||
Alors en base de données, la table users est mise à jour:
|
||||
| user_id | interest_gauges_reset_at | reset_count |
|
||||
| user123 | 2026-02-02T14:00:00Z | 1 |
|
||||
Et un compteur permet de tracker les réinitialisations multiples
|
||||
|
||||
Scénario: API permet réinitialisations multiples
|
||||
Étant donné qu'un utilisateur "user123" a déjà réinitialisé une fois
|
||||
Quand je POST /api/v1/users/user123/interest-gauges/reset une 2ème fois
|
||||
Alors le statut de réponse est 200
|
||||
Et le reset_count passe à 2
|
||||
Et toutes les jauges reviennent à 50%
|
||||
|
||||
Scénario: API n'envoie jamais de suggestion de réinitialisation
|
||||
Étant donné qu'un utilisateur "user123" n'a pas utilisé l'app depuis 1 an
|
||||
Quand je GET /api/v1/users/user123/notifications
|
||||
Alors le statut de réponse est 200
|
||||
Et aucune notification "Réinitialiser vos centres d'intérêt" n'est présente
|
||||
Et le système ne suggère jamais de réinitialisation automatique
|
||||
|
||||
Scénario: API GET statistiques respect historique utilisateur
|
||||
Étant donné qu'un utilisateur "user123" aime "Cryptomonnaie" depuis 2 ans
|
||||
Et que sa jauge est à 90%
|
||||
Et qu'il n'a pas écouté de contenu "Cryptomonnaie" depuis 6 mois
|
||||
Quand je GET /api/v1/users/user123/interest-gauges
|
||||
Alors le statut de réponse est 200
|
||||
Et la jauge "Cryptomonnaie" est toujours à 90%
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"gauges": [
|
||||
{
|
||||
"category": "Cryptomonnaie",
|
||||
"level": 90,
|
||||
"last_updated": "2025-08-02T10:00:00Z",
|
||||
"days_since_update": 183,
|
||||
"preserved": true
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
Et le système respecte l'historique des goûts
|
||||
|
||||
Scénario: API métrique temps écoulé sans modifier la jauge
|
||||
Étant donné qu'un utilisateur "user123" a une jauge "Sport" à 65%
|
||||
Et que la dernière modification date de 90 jours
|
||||
Quand je GET /api/v1/users/user123/interest-gauges/sport
|
||||
Alors le statut de réponse est 200
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"category": "Sport",
|
||||
"level": 65,
|
||||
"last_updated": "2025-11-03T12:00:00Z",
|
||||
"days_since_update": 90
|
||||
}
|
||||
"""
|
||||
Et la métrique days_since_update est informative uniquement
|
||||
Et elle ne modifie jamais la jauge
|
||||
|
||||
Scénario: Requête SQL n'utilise jamais de calcul temporel
|
||||
Étant donné que je trace les requêtes SQL du backend
|
||||
Quand je GET /api/v1/users/user123/interest-gauges
|
||||
Alors la requête SQL exécutée est:
|
||||
"""sql
|
||||
SELECT category, level, updated_at
|
||||
FROM interest_gauges
|
||||
WHERE user_id = $1
|
||||
"""
|
||||
Et aucune clause WHERE avec date/timestamp n'est présente
|
||||
Et aucune fonction NOW(), CURRENT_TIMESTAMP, ou DATEDIFF n'est utilisée
|
||||
Et le calcul est minimal (simple SELECT)
|
||||
|
||||
Scénario: API coût CPU minimal - pas de calcul de dates
|
||||
Étant donné que 10000 utilisateurs consultent leurs jauges simultanément
|
||||
Quand les requêtes /api/v1/users/{id}/interest-gauges sont exécutées
|
||||
Alors aucun calcul de date n'est nécessaire
|
||||
Et aucun appel à time.Now() ou time.Since() n'est fait
|
||||
Et le coût CPU par requête est < 1ms
|
||||
Et aucune dégradation de performance liée aux dates
|
||||
|
||||
Scénario: API pas de risque de bug fuseau horaire
|
||||
Étant donné qu'aucune logique temporelle n'existe
|
||||
Quand un utilisateur change de fuseau horaire (Paris → Tokyo)
|
||||
Alors ses jauges ne sont pas affectées
|
||||
Et aucun bug de conversion UTC/local ne peut survenir
|
||||
Et le comportement reste déterministe
|
||||
|
||||
Scénario: Audit log réinitialisation manuelle
|
||||
Étant donné qu'un utilisateur "user123" réinitialise ses jauges
|
||||
Quand je POST /api/v1/users/user123/interest-gauges/reset
|
||||
Alors une ligne est insérée dans audit_log:
|
||||
| user_id | action | timestamp | details |
|
||||
| user123 | interest_gauges_reset | 2026-02-02T14:00:00 | {"previous_snapshot_id": 42} |
|
||||
Et l'audit permet de tracer toutes les réinitialisations
|
||||
|
||||
Scénario: API empêche réinitialisation trop fréquente
|
||||
Étant donné qu'un utilisateur "user123" a réinitialisé il y a 10 minutes
|
||||
Quand je POST /api/v1/users/user123/interest-gauges/reset
|
||||
Alors le statut de réponse est 429
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"error": "RATE_LIMIT_EXCEEDED",
|
||||
"message": "Vous ne pouvez réinitialiser qu'une fois par heure",
|
||||
"retry_after_seconds": 3000
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: API documentation endpoints réinitialisation
|
||||
Quand je GET /api/v1/openapi.json
|
||||
Alors le endpoint POST /api/v1/users/{id}/interest-gauges/reset est documenté:
|
||||
"""yaml
|
||||
/users/{id}/interest-gauges/reset:
|
||||
post:
|
||||
summary: Réinitialise toutes les jauges à 50%
|
||||
description: |
|
||||
Remet toutes les jauges d'intérêt à leur valeur par défaut (50%).
|
||||
Cette action est manuelle et requiert une confirmation.
|
||||
Les jauges précédentes sont sauvegardées.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
confirmation:
|
||||
type: boolean
|
||||
description: Doit être true
|
||||
responses:
|
||||
200:
|
||||
description: Réinitialisation réussie
|
||||
400:
|
||||
description: Confirmation manquante
|
||||
429:
|
||||
description: Trop de réinitialisations
|
||||
"""
|
||||
@@ -0,0 +1,567 @@
|
||||
# language: fr
|
||||
Fonctionnalité: API - Évolution des jauges d'intérêt
|
||||
En tant qu'API backend
|
||||
Je veux calculer et persister les évolutions de jauges d'intérêt
|
||||
Afin d'alimenter l'algorithme de recommandation
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est disponible
|
||||
Et que la base de données PostgreSQL est accessible
|
||||
Et qu'un utilisateur "user123" existe avec token JWT valide
|
||||
|
||||
Scénario: API calcule like automatique renforcé (≥80% écoute)
|
||||
Étant donné que l'utilisateur "user123" a une jauge "Automobile" à 45% en base
|
||||
Et qu'un contenu "content456" de 5 minutes est tagué "Automobile"
|
||||
Quand je POST /api/v1/listening-events
|
||||
"""json
|
||||
{
|
||||
"user_id": "user123",
|
||||
"content_id": "content456",
|
||||
"listened_duration_seconds": 270,
|
||||
"total_duration_seconds": 300,
|
||||
"completion_percentage": 90
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 201
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"like_type": "automatic_reinforced",
|
||||
"gauge_updates": [
|
||||
{
|
||||
"category": "Automobile",
|
||||
"previous_value": 45,
|
||||
"delta": 2,
|
||||
"new_value": 47
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
Et en base de données, la jauge "Automobile" de "user123" est à 47%
|
||||
|
||||
Scénario: API calcule like automatique standard (30-79% écoute)
|
||||
Étant donné que l'utilisateur "user123" a une jauge "Voyage" à 60% en base
|
||||
Et qu'un contenu "content789" de 10 minutes est tagué "Voyage"
|
||||
Quand je POST /api/v1/listening-events
|
||||
"""json
|
||||
{
|
||||
"user_id": "user123",
|
||||
"content_id": "content789",
|
||||
"listened_duration_seconds": 300,
|
||||
"total_duration_seconds": 600,
|
||||
"completion_percentage": 50
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 201
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"like_type": "automatic_standard",
|
||||
"gauge_updates": [
|
||||
{
|
||||
"category": "Voyage",
|
||||
"previous_value": 60,
|
||||
"delta": 1,
|
||||
"new_value": 61
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
Et en base de données, la jauge "Voyage" de "user123" est à 61%
|
||||
|
||||
Scénario: API applique like manuel explicite (+2%)
|
||||
Étant donné que l'utilisateur "user123" a une jauge "Musique" à 55% en base
|
||||
Et qu'un contenu "content999" est tagué "Musique"
|
||||
Quand je POST /api/v1/likes
|
||||
"""json
|
||||
{
|
||||
"user_id": "user123",
|
||||
"content_id": "content999",
|
||||
"like_type": "manual"
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 201
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"like_id": "<uuid>",
|
||||
"gauge_updates": [
|
||||
{
|
||||
"category": "Musique",
|
||||
"previous_value": 55,
|
||||
"delta": 2,
|
||||
"new_value": 57
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
Et en base de données, la jauge "Musique" de "user123" est à 57%
|
||||
|
||||
Scénario: API applique unlike (retire like manuel)
|
||||
Étant donné que l'utilisateur "user123" a une jauge "Sport" à 57% en base
|
||||
Et qu'il a liké manuellement le contenu "content888" tagué "Sport"
|
||||
Et que le like a l'ID "like_abc123"
|
||||
Quand je DELETE /api/v1/likes/like_abc123
|
||||
Alors le statut de réponse est 204
|
||||
Et en base de données, la jauge "Sport" de "user123" est à 55%
|
||||
Et le like "like_abc123" est supprimé de la table likes
|
||||
|
||||
Scénario: API refuse unlike d'un like automatique
|
||||
Étant donné que l'utilisateur "user123" a écouté un contenu à 90%
|
||||
Et qu'il a reçu un like automatique "like_auto456"
|
||||
Quand je DELETE /api/v1/likes/like_auto456
|
||||
Alors le statut de réponse est 403
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"error": "CANNOT_UNLIKE_AUTOMATIC",
|
||||
"message": "Les likes automatiques ne peuvent pas être retirés"
|
||||
}
|
||||
"""
|
||||
Et en base de données, le like "like_auto456" existe toujours
|
||||
|
||||
Scénario: API cumule like automatique + like manuel
|
||||
Étant donné que l'utilisateur "user123" a une jauge "Technologie" à 50% en base
|
||||
Et qu'un contenu "content777" de 10 minutes est tagué "Technologie"
|
||||
Quand je POST /api/v1/listening-events
|
||||
"""json
|
||||
{
|
||||
"user_id": "user123",
|
||||
"content_id": "content777",
|
||||
"completion_percentage": 50
|
||||
}
|
||||
"""
|
||||
Alors la jauge "Technologie" passe à 51% (+1% auto)
|
||||
Quand je POST ensuite /api/v1/likes
|
||||
"""json
|
||||
{
|
||||
"user_id": "user123",
|
||||
"content_id": "content777",
|
||||
"like_type": "manual"
|
||||
}
|
||||
"""
|
||||
Alors la jauge "Technologie" passe à 53% (+2% manuel)
|
||||
Et le delta total est de 3%
|
||||
|
||||
Scénario: API applique bonus abonnement créateur (+5% tous tags)
|
||||
Étant donné que l'utilisateur "user123" a les jauges suivantes:
|
||||
| catégorie | niveau |
|
||||
| Automobile | 50% |
|
||||
| Technologie | 45% |
|
||||
Et qu'un créateur "creator456" publie des contenus tagués "Automobile" et "Technologie"
|
||||
Quand je POST /api/v1/subscriptions
|
||||
"""json
|
||||
{
|
||||
"user_id": "user123",
|
||||
"creator_id": "creator456"
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 201
|
||||
Et en base de données:
|
||||
| catégorie | niveau |
|
||||
| Automobile | 55% |
|
||||
| Technologie | 50% |
|
||||
Et l'abonnement est créé avec bonus appliqué
|
||||
|
||||
Scénario: API retire bonus désabonnement créateur (-5% tous tags)
|
||||
Étant donné que l'utilisateur "user123" a les jauges suivantes:
|
||||
| catégorie | niveau |
|
||||
| Voyage | 65% |
|
||||
| Culture | 58% |
|
||||
Et qu'il est abonné au créateur "creator789" qui publie "Voyage" et "Culture"
|
||||
Quand je DELETE /api/v1/subscriptions/creator789
|
||||
Alors le statut de réponse est 204
|
||||
Et en base de données:
|
||||
| catégorie | niveau |
|
||||
| Voyage | 60% |
|
||||
| Culture | 53% |
|
||||
|
||||
Scénario: API applique pénalité skip rapide (<10s)
|
||||
Étant donné que l'utilisateur "user123" a une jauge "Politique" à 50% en base
|
||||
Et qu'un contenu "content555" de 300 secondes est tagué "Politique"
|
||||
Quand je POST /api/v1/skip-events
|
||||
"""json
|
||||
{
|
||||
"user_id": "user123",
|
||||
"content_id": "content555",
|
||||
"listened_duration_seconds": 5,
|
||||
"total_duration_seconds": 300
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 201
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"skip_type": "early",
|
||||
"gauge_updates": [
|
||||
{
|
||||
"category": "Politique",
|
||||
"previous_value": 50,
|
||||
"delta": -0.5,
|
||||
"new_value": 49.5
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
Et en base de données, la jauge "Politique" de "user123" est à 49.5%
|
||||
|
||||
Scénario: API n'applique pas de pénalité pour skip ≥10s et <30%
|
||||
Étant donné que l'utilisateur "user123" a une jauge "Économie" à 60% en base
|
||||
Et qu'un contenu "content333" de 600 secondes est tagué "Économie"
|
||||
Quand je POST /api/v1/skip-events
|
||||
"""json
|
||||
{
|
||||
"user_id": "user123",
|
||||
"content_id": "content333",
|
||||
"listened_duration_seconds": 120,
|
||||
"total_duration_seconds": 600,
|
||||
"completion_percentage": 20
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 201
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"skip_type": "neutral",
|
||||
"gauge_updates": []
|
||||
}
|
||||
"""
|
||||
Et en base de données, la jauge "Économie" de "user123" reste à 60%
|
||||
|
||||
Scénario: API n'applique pas de pénalité pour skip ≥30%
|
||||
Étant donné que l'utilisateur "user123" a une jauge "Sport" à 55% en base
|
||||
Et qu'un contenu "content222" de 600 secondes est tagué "Sport"
|
||||
Quand je POST /api/v1/skip-events
|
||||
"""json
|
||||
{
|
||||
"user_id": "user123",
|
||||
"content_id": "content222",
|
||||
"listened_duration_seconds": 300,
|
||||
"total_duration_seconds": 600,
|
||||
"completion_percentage": 50
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 201
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"skip_type": "late",
|
||||
"gauge_updates": []
|
||||
}
|
||||
"""
|
||||
Et en base de données, la jauge "Sport" de "user123" reste à 55%
|
||||
|
||||
Scénario: API applique évolution sur plusieurs tags simultanément
|
||||
Étant donné que l'utilisateur "user123" a les jauges suivantes:
|
||||
| catégorie | niveau |
|
||||
| Automobile | 45% |
|
||||
| Voyage | 60% |
|
||||
Et qu'un contenu "content111" est tagué "Automobile" et "Voyage"
|
||||
Quand je POST /api/v1/listening-events
|
||||
"""json
|
||||
{
|
||||
"user_id": "user123",
|
||||
"content_id": "content111",
|
||||
"completion_percentage": 90
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 201
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"like_type": "automatic_reinforced",
|
||||
"gauge_updates": [
|
||||
{
|
||||
"category": "Automobile",
|
||||
"previous_value": 45,
|
||||
"delta": 2,
|
||||
"new_value": 47
|
||||
},
|
||||
{
|
||||
"category": "Voyage",
|
||||
"previous_value": 60,
|
||||
"delta": 2,
|
||||
"new_value": 62
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
Et en base de données:
|
||||
| catégorie | niveau |
|
||||
| Automobile | 47% |
|
||||
| Voyage | 62% |
|
||||
|
||||
Scénario: API respecte la borne maximum 100%
|
||||
Étant donné que l'utilisateur "user123" a une jauge "Cryptomonnaie" à 99% en base
|
||||
Et qu'un contenu "content_crypto" est tagué "Cryptomonnaie"
|
||||
Quand je POST /api/v1/listening-events
|
||||
"""json
|
||||
{
|
||||
"user_id": "user123",
|
||||
"content_id": "content_crypto",
|
||||
"completion_percentage": 95
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 201
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"like_type": "automatic_reinforced",
|
||||
"gauge_updates": [
|
||||
{
|
||||
"category": "Cryptomonnaie",
|
||||
"previous_value": 99,
|
||||
"delta": 2,
|
||||
"new_value": 100,
|
||||
"capped": true
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
Et en base de données, la jauge "Cryptomonnaie" de "user123" est à 100%
|
||||
Et la jauge n'a pas dépassé 100%
|
||||
|
||||
Scénario: API respecte la borne minimum 0%
|
||||
Étant donné que l'utilisateur "user123" a une jauge "Politique" à 0.3% en base
|
||||
Et qu'un contenu "content_pol" est tagué "Politique"
|
||||
Quand je POST /api/v1/skip-events
|
||||
"""json
|
||||
{
|
||||
"user_id": "user123",
|
||||
"content_id": "content_pol",
|
||||
"listened_duration_seconds": 3,
|
||||
"total_duration_seconds": 300
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 201
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"skip_type": "early",
|
||||
"gauge_updates": [
|
||||
{
|
||||
"category": "Politique",
|
||||
"previous_value": 0.3,
|
||||
"delta": -0.5,
|
||||
"new_value": 0,
|
||||
"capped": true
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
Et en base de données, la jauge "Politique" de "user123" est à 0%
|
||||
Et la jauge n'est pas devenue négative
|
||||
|
||||
Scénario: API respecte borne minimum lors désabonnement
|
||||
Étant donné que l'utilisateur "user123" a une jauge "Économie" à 3% en base
|
||||
Et qu'il est abonné au créateur "creator_eco" qui publie "Économie"
|
||||
Quand je DELETE /api/v1/subscriptions/creator_eco
|
||||
Alors le statut de réponse est 204
|
||||
Et en base de données, la jauge "Économie" de "user123" est à 0% (et non -2%)
|
||||
|
||||
Scénario: API GET retourne toutes les jauges utilisateur
|
||||
Étant donné que l'utilisateur "user123" a les jauges suivantes en base:
|
||||
| catégorie | niveau |
|
||||
| Automobile | 67% |
|
||||
| Voyage | 82% |
|
||||
| Économie | 34% |
|
||||
| Sport | 50% |
|
||||
| Musique | 45% |
|
||||
| Technologie | 71% |
|
||||
Quand je GET /api/v1/users/user123/interest-gauges
|
||||
Alors le statut de réponse est 200
|
||||
Et la réponse contient les 12 catégories avec leurs niveaux:
|
||||
"""json
|
||||
{
|
||||
"user_id": "user123",
|
||||
"gauges": [
|
||||
{"category": "Automobile", "level": 67},
|
||||
{"category": "Voyage", "level": 82},
|
||||
{"category": "Économie", "level": 34},
|
||||
{"category": "Sport", "level": 50},
|
||||
{"category": "Musique", "level": 45},
|
||||
{"category": "Technologie", "level": 71}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: API calcule évolution immédiate (pas de batch différé)
|
||||
Étant donné que l'utilisateur "user123" a une jauge "Voyage" à 50% en base
|
||||
Quand je POST /api/v1/listening-events à 12:00:00
|
||||
"""json
|
||||
{
|
||||
"user_id": "user123",
|
||||
"content_id": "content_travel",
|
||||
"completion_percentage": 85
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 201
|
||||
Quand je GET /api/v1/users/user123/interest-gauges à 12:00:01 (1 seconde après)
|
||||
Alors la jauge "Voyage" est à 52%
|
||||
Et la mise à jour est visible immédiatement
|
||||
|
||||
Scénario: API rejette token JWT invalide
|
||||
Quand je POST /api/v1/listening-events sans token JWT
|
||||
Alors le statut de réponse est 401
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"error": "UNAUTHORIZED",
|
||||
"message": "Token JWT manquant ou invalide"
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: API valide format des données d'entrée
|
||||
Quand je POST /api/v1/listening-events
|
||||
"""json
|
||||
{
|
||||
"user_id": "user123",
|
||||
"content_id": "content456",
|
||||
"completion_percentage": 150
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 400
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"error": "VALIDATION_ERROR",
|
||||
"message": "completion_percentage doit être entre 0 et 100"
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: API gère contenu avec tags inexistants en base
|
||||
Étant donné qu'un contenu "content_new" est tagué "NouvelleCategorie" (non encore en base)
|
||||
Quand je POST /api/v1/listening-events
|
||||
"""json
|
||||
{
|
||||
"user_id": "user123",
|
||||
"content_id": "content_new",
|
||||
"completion_percentage": 90
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 201
|
||||
Et une nouvelle ligne est créée dans la table interest_gauges:
|
||||
| user_id | category | level |
|
||||
| user123 | NouvelleCategorie | 52 |
|
||||
Et l'initialisation démarre à 50% + 2% de like auto = 52%
|
||||
|
||||
Scénario: API persiste historique des modifications de jauges
|
||||
Étant donné que l'utilisateur "user123" a une jauge "Sport" à 50%
|
||||
Quand je POST /api/v1/listening-events qui applique +2%
|
||||
Alors une ligne est insérée dans interest_gauge_history:
|
||||
| user_id | category | previous_value | delta | new_value | event_type | event_id | timestamp |
|
||||
| user123 | Sport | 50 | 2 | 52 | listening_event| <event_uuid> | 2026-02-02T12:00:00 |
|
||||
Et cet historique permet d'auditer les évolutions
|
||||
|
||||
Scénario: API retourne métriques d'évolution utilisateur
|
||||
Étant donné que l'utilisateur "user123" a un historique d'évolution en base
|
||||
Quand je GET /api/v1/users/user123/interest-gauges/evolution?since=7d
|
||||
Alors le statut de réponse est 200
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"period": "7d",
|
||||
"evolution": [
|
||||
{
|
||||
"category": "Automobile",
|
||||
"start_value": 60,
|
||||
"end_value": 67,
|
||||
"delta": 7,
|
||||
"events_count": 15
|
||||
},
|
||||
{
|
||||
"category": "Voyage",
|
||||
"start_value": 80,
|
||||
"end_value": 82,
|
||||
"delta": 2,
|
||||
"events_count": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
# Architecture backend - Services séparés
|
||||
|
||||
Scénario: Gauge Calculation Service calcule l'ajustement (stateless)
|
||||
Étant donné un événement d'écoute avec 85% de complétion
|
||||
Quand le Gauge Calculation Service calcule l'ajustement
|
||||
Alors le service retourne:
|
||||
"""json
|
||||
{
|
||||
"adjustment_type": "automatic_reinforced",
|
||||
"adjustment_value": 2.0,
|
||||
"reason": "completion_percentage >= 80%"
|
||||
}
|
||||
"""
|
||||
Et le service est stateless (aucune lecture DB)
|
||||
Et le service est testable unitairement
|
||||
|
||||
Scénario: Gauge Update Service applique l'ajustement (stateful)
|
||||
Étant donné qu'un ajustement de +2% a été calculé
|
||||
Et que la jauge "Automobile" de "user123" est à 45%
|
||||
Quand le Gauge Update Service applique l'ajustement
|
||||
Alors la nouvelle valeur est 47% (45 + 2)
|
||||
Et la borne [0, 100] est respectée via fonction clamp
|
||||
Et la mise à jour est persistée en Redis (immédiat)
|
||||
Et la mise à jour est persistée en PostgreSQL (batch async)
|
||||
|
||||
Scénario: Pattern de calcul - Addition de points absolus
|
||||
Étant donné qu'une jauge est à 60%
|
||||
Et qu'un ajustement de +2% est calculé
|
||||
Quand le calcul est effectué
|
||||
Alors la formule est: newValue = currentValue + adjustment
|
||||
Et la formule est: newValue = clamp(newValue, 0.0, 100.0)
|
||||
Et la nouvelle valeur est 62%
|
||||
|
||||
Scénario: Pattern de calcul - Éviter multiplication relative
|
||||
Étant donné qu'une jauge est à 50%
|
||||
Et qu'un ajustement de +2% est calculé
|
||||
Quand le calcul est effectué
|
||||
Alors la formule utilisée est: 50 + 2 = 52
|
||||
Et la formule utilisée n'est PAS: 50 * (1 + 2/100) = 51
|
||||
Car l'ajustement est en points absolus, pas relatifs
|
||||
|
||||
Scénario: Multi-tags - Mise à jour de N jauges simultanément
|
||||
Étant donné qu'un contenu a 3 tags: ["Automobile", "Voyage", "Technologie"]
|
||||
Et qu'un ajustement de +2% est calculé (écoute 85%)
|
||||
Quand le Gauge Update Service applique
|
||||
Alors 3 jauges sont mises à jour:
|
||||
| catégorie | ajustement |
|
||||
| Automobile | +2% |
|
||||
| Voyage | +2% |
|
||||
| Technologie | +2% |
|
||||
Et toutes les mises à jour sont effectuées en une seule transaction
|
||||
|
||||
Scénario: Persistance Redis immédiate (latence <10ms)
|
||||
Étant donné qu'un ajustement doit être persisté
|
||||
Quand le Gauge Update Service écrit en Redis
|
||||
Alors la latence est < 10ms
|
||||
Et la jauge est immédiatement disponible pour recommandations
|
||||
Et Redis sert de cache haute performance
|
||||
|
||||
Scénario: Persistance PostgreSQL asynchrone (batch 5 min)
|
||||
Étant donné que 100 ajustements ont été appliqués en Redis
|
||||
Quand le batch async s'exécute toutes les 5 minutes
|
||||
Alors les 100 ajustements sont écrits en PostgreSQL en batch
|
||||
Et la cohérence finale est garantie
|
||||
Et l'application reste performante (pas de write sync)
|
||||
|
||||
Scénario: Séparation responsabilités - Calculation vs Update
|
||||
Étant donné un événement d'écoute
|
||||
Quand il est traité
|
||||
Alors le Gauge Calculation Service calcule l'ajustement (logique métier pure)
|
||||
Et le Gauge Update Service applique l'ajustement (persistance)
|
||||
Et les deux services sont indépendants
|
||||
Et chaque service a une responsabilité unique (SRP)
|
||||
|
||||
Scénario: Réutilisabilité Calculation Service
|
||||
Étant donné le Gauge Calculation Service
|
||||
Quand il est utilisé pour:
|
||||
| contexte |
|
||||
| Like automatique |
|
||||
| Skip rapide |
|
||||
| Like manuel |
|
||||
| Abonnement créateur |
|
||||
Alors le même service calcule tous les ajustements
|
||||
Et la logique métier est centralisée
|
||||
Et il n'y a pas de duplication de code
|
||||
@@ -0,0 +1,337 @@
|
||||
# language: fr
|
||||
Fonctionnalité: API - Jauge initiale et cold start
|
||||
En tant qu'API backend
|
||||
Je veux initialiser toutes les jauges à 50% lors de l'inscription
|
||||
Afin de garantir un démarrage neutre et équitable
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est disponible
|
||||
Et que la base de données PostgreSQL est accessible
|
||||
|
||||
Scénario: API initialise toutes les jauges à 50% lors inscription
|
||||
Quand je POST /api/v1/auth/register
|
||||
"""json
|
||||
{
|
||||
"email": "nouveau@example.com",
|
||||
"password": "SecureP@ss123",
|
||||
"birth_date": "1990-01-15",
|
||||
"username": "nouveau_user"
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 201
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"user_id": "<uuid>",
|
||||
"email": "nouveau@example.com",
|
||||
"username": "nouveau_user"
|
||||
}
|
||||
"""
|
||||
Et en base de données, la table interest_gauges contient 12 lignes pour ce user_id:
|
||||
| category | level |
|
||||
| Automobile | 50 |
|
||||
| Voyage | 50 |
|
||||
| Famille | 50 |
|
||||
| Amour | 50 |
|
||||
| Musique | 50 |
|
||||
| Économie | 50 |
|
||||
| Cryptomonnaie | 50 |
|
||||
| Politique | 50 |
|
||||
| Culture générale | 50 |
|
||||
| Sport | 50 |
|
||||
| Technologie | 50 |
|
||||
| Santé | 50 |
|
||||
|
||||
Scénario: API retourne les 12 catégories disponibles
|
||||
Quand je GET /api/v1/interest-gauges/categories
|
||||
Alors le statut de réponse est 200
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"categories": [
|
||||
{"id": "automobile", "name": "Automobile", "icon": "car"},
|
||||
{"id": "voyage", "name": "Voyage", "icon": "plane"},
|
||||
{"id": "famille", "name": "Famille", "icon": "users"},
|
||||
{"id": "amour", "name": "Amour", "icon": "heart"},
|
||||
{"id": "musique", "name": "Musique", "icon": "music"},
|
||||
{"id": "economie", "name": "Économie", "icon": "chart"},
|
||||
{"id": "cryptomonnaie", "name": "Cryptomonnaie", "icon": "bitcoin"},
|
||||
{"id": "politique", "name": "Politique", "icon": "landmark"},
|
||||
{"id": "culture-generale", "name": "Culture générale", "icon": "book"},
|
||||
{"id": "sport", "name": "Sport", "icon": "running"},
|
||||
{"id": "technologie", "name": "Technologie", "icon": "cpu"},
|
||||
{"id": "sante", "name": "Santé", "icon": "heart-pulse"}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: API GET retourne jauges utilisateur nouvellement inscrit
|
||||
Étant donné qu'un utilisateur "user_new" vient de s'inscrire
|
||||
Quand je GET /api/v1/users/user_new/interest-gauges
|
||||
Alors le statut de réponse est 200
|
||||
Et la réponse contient 12 jauges toutes à 50%:
|
||||
"""json
|
||||
{
|
||||
"user_id": "user_new",
|
||||
"gauges": [
|
||||
{"category": "Automobile", "level": 50, "evolution_since_signup": 0},
|
||||
{"category": "Voyage", "level": 50, "evolution_since_signup": 0},
|
||||
{"category": "Famille", "level": 50, "evolution_since_signup": 0},
|
||||
{"category": "Amour", "level": 50, "evolution_since_signup": 0},
|
||||
{"category": "Musique", "level": 50, "evolution_since_signup": 0},
|
||||
{"category": "Économie", "level": 50, "evolution_since_signup": 0},
|
||||
{"category": "Cryptomonnaie", "level": 50, "evolution_since_signup": 0},
|
||||
{"category": "Politique", "level": 50, "evolution_since_signup": 0},
|
||||
{"category": "Culture générale", "level": 50, "evolution_since_signup": 0},
|
||||
{"category": "Sport", "level": 50, "evolution_since_signup": 0},
|
||||
{"category": "Technologie", "level": 50, "evolution_since_signup": 0},
|
||||
{"category": "Santé", "level": 50, "evolution_since_signup": 0}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: API calcule recommandations avec jauges à 50% - pas de biais
|
||||
Étant donné qu'un utilisateur "user_new" vient de s'inscrire
|
||||
Et que toutes ses jauges sont à 50%
|
||||
Et qu'il est à la position GPS (48.8566, 2.3522) - Paris
|
||||
Quand je POST /api/v1/recommendations
|
||||
"""json
|
||||
{
|
||||
"user_id": "user_new",
|
||||
"latitude": 48.8566,
|
||||
"longitude": 2.3522,
|
||||
"limit": 10
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 200
|
||||
Et la réponse contient 10 contenus
|
||||
Et le scoring est basé uniquement sur:
|
||||
| critère | poids |
|
||||
| Distance géographique| 100% |
|
||||
| Intérêts (50% égal) | 0% |
|
||||
Et aucune catégorie n'a d'avantage initial
|
||||
|
||||
Scénario: API permet ajout de nouvelles catégories
|
||||
Étant donné qu'un admin ajoute une nouvelle catégorie "Gastronomie"
|
||||
Quand je POST /api/v1/admin/interest-gauges/categories
|
||||
"""json
|
||||
{
|
||||
"id": "gastronomie",
|
||||
"name": "Gastronomie",
|
||||
"icon": "utensils"
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 201
|
||||
Et pour tous les utilisateurs existants:
|
||||
| action |
|
||||
| Une ligne est créée dans interest_gauges |
|
||||
| category = "Gastronomie" |
|
||||
| level = 50 |
|
||||
Et les nouveaux utilisateurs auront aussi cette catégorie à 50%
|
||||
|
||||
Scénario: API calcule évolution depuis inscription
|
||||
Étant donné qu'un utilisateur "user123" s'est inscrit il y a 30 jours
|
||||
Et qu'il a les jauges suivantes en base:
|
||||
| catégorie | niveau | initial |
|
||||
| Automobile | 67% | 50% |
|
||||
| Voyage | 82% | 50% |
|
||||
| Économie | 34% | 50% |
|
||||
| Sport | 50% | 50% |
|
||||
Quand je GET /api/v1/users/user123/interest-gauges
|
||||
Alors le statut de réponse est 200
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"user_id": "user123",
|
||||
"signup_date": "2026-01-03T10:00:00Z",
|
||||
"gauges": [
|
||||
{
|
||||
"category": "Automobile",
|
||||
"level": 67,
|
||||
"evolution_since_signup": 17
|
||||
},
|
||||
{
|
||||
"category": "Voyage",
|
||||
"level": 82,
|
||||
"evolution_since_signup": 32
|
||||
},
|
||||
{
|
||||
"category": "Économie",
|
||||
"level": 34,
|
||||
"evolution_since_signup": -16
|
||||
},
|
||||
{
|
||||
"category": "Sport",
|
||||
"level": 50,
|
||||
"evolution_since_signup": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: API transaction atomique lors inscription
|
||||
Quand je POST /api/v1/auth/register
|
||||
"""json
|
||||
{
|
||||
"email": "test@example.com",
|
||||
"password": "SecureP@ss123",
|
||||
"birth_date": "1995-03-20",
|
||||
"username": "test_user"
|
||||
}
|
||||
"""
|
||||
Alors l'insertion en base de données est atomique:
|
||||
| action |
|
||||
| INSERT INTO users |
|
||||
| INSERT INTO interest_gauges (12 lignes) |
|
||||
| Tout ou rien (transaction) |
|
||||
Et si une erreur survient, aucune donnée partielle n'est créée
|
||||
|
||||
Scénario: API rollback si initialisation jauges échoue
|
||||
Étant donné que la table interest_gauges a une contrainte violée
|
||||
Quand je POST /api/v1/auth/register avec données valides
|
||||
Alors le statut de réponse est 500
|
||||
Et aucune ligne n'est créée dans la table users
|
||||
Et aucune ligne n'est créée dans la table interest_gauges
|
||||
Et la transaction est rollback complètement
|
||||
|
||||
Scénario: API POST questionnaire optionnel post-MVP
|
||||
Étant donné qu'un utilisateur "user123" a écouté 3 contenus
|
||||
Et qu'il décide de remplir le questionnaire optionnel
|
||||
Quand je POST /api/v1/users/user123/interest-gauges/quick-setup
|
||||
"""json
|
||||
{
|
||||
"selected_categories": ["Automobile", "Voyage", "Sport"]
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 200
|
||||
Et en base de données:
|
||||
| catégorie | niveau |
|
||||
| Automobile | 70 |
|
||||
| Voyage | 70 |
|
||||
| Sport | 70 |
|
||||
| Musique | 30 |
|
||||
| Économie | 30 |
|
||||
| Cryptomonnaie | 30 |
|
||||
| Politique | 30 |
|
||||
| Culture générale | 30 |
|
||||
| Technologie | 30 |
|
||||
| Santé | 30 |
|
||||
| Famille | 30 |
|
||||
| Amour | 30 |
|
||||
Et un flag quick_setup_completed = true est enregistré
|
||||
|
||||
Scénario: API rejette questionnaire optionnel si déjà rempli
|
||||
Étant donné qu'un utilisateur "user123" a déjà rempli le questionnaire optionnel
|
||||
Quand je POST /api/v1/users/user123/interest-gauges/quick-setup
|
||||
"""json
|
||||
{
|
||||
"selected_categories": ["Musique", "Technologie"]
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 409
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"error": "QUICK_SETUP_ALREADY_COMPLETED",
|
||||
"message": "Le questionnaire a déjà été rempli"
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: API valide nombre de catégories sélectionnées
|
||||
Quand je POST /api/v1/users/user123/interest-gauges/quick-setup
|
||||
"""json
|
||||
{
|
||||
"selected_categories": ["Automobile"]
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 400
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"error": "VALIDATION_ERROR",
|
||||
"message": "Vous devez sélectionner entre 2 et 5 catégories"
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: API déterministe - deux users identiques
|
||||
Étant donné que l'utilisateur "userA" s'inscrit à 10:00:00
|
||||
Et que l'utilisateur "userB" s'inscrit à 10:00:01
|
||||
Quand je GET /api/v1/users/userA/interest-gauges
|
||||
Et je GET /api/v1/users/userB/interest-gauges
|
||||
Alors les deux réponses ont des jauges identiques (toutes à 50%)
|
||||
Et le comportement est déterministe
|
||||
|
||||
Scénario: API retourne statistiques cold start
|
||||
Étant donné qu'un utilisateur "user_new" vient de s'inscrire
|
||||
Quand je GET /api/v1/users/user_new/stats
|
||||
Alors le statut de réponse est 200
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"user_id": "user_new",
|
||||
"signup_date": "2026-02-02T14:00:00Z",
|
||||
"total_listened_content": 0,
|
||||
"gauges_summary": {
|
||||
"all_at_default": true,
|
||||
"default_value": 50,
|
||||
"total_categories": 12,
|
||||
"personalization_level": "none"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: API recommandations cold start priorité géo
|
||||
Étant donné qu'un utilisateur "user_new" vient de s'inscrire
|
||||
Et qu'il est à Paris avec 100 contenus disponibles dans un rayon de 5km
|
||||
Et que ces contenus ont des catégories variées
|
||||
Quand je POST /api/v1/recommendations
|
||||
"""json
|
||||
{
|
||||
"user_id": "user_new",
|
||||
"latitude": 48.8566,
|
||||
"longitude": 2.3522,
|
||||
"limit": 10
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 200
|
||||
Et les 10 contenus retournés sont les plus proches géographiquement
|
||||
Et les catégories sont variées (pas de biais intérêts)
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"recommendations": [
|
||||
{
|
||||
"content_id": "<uuid>",
|
||||
"distance_meters": 150,
|
||||
"interest_match": 50,
|
||||
"final_score": 0.95
|
||||
}
|
||||
],
|
||||
"cold_start": true,
|
||||
"personalization_level": "none"
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: API index optimisé pour lecture jauges
|
||||
Étant donné que la table interest_gauges a 1 million de lignes
|
||||
Quand je GET /api/v1/users/user123/interest-gauges
|
||||
Alors la requête SQL utilise l'index (user_id, category)
|
||||
Et le temps de réponse est < 50ms
|
||||
Et le plan d'exécution confirme l'utilisation de l'index
|
||||
|
||||
Scénario: API cache jauges utilisateur en Redis
|
||||
Étant donné qu'un utilisateur "user123" a ses jauges en base
|
||||
Quand je GET /api/v1/users/user123/interest-gauges
|
||||
Alors le backend vérifie d'abord Redis avec clé "user:user123:gauges"
|
||||
Et si absent, lit depuis PostgreSQL
|
||||
Et met en cache dans Redis avec TTL = 300 secondes
|
||||
Quand je GET à nouveau dans les 5 minutes
|
||||
Alors la réponse vient directement de Redis
|
||||
Et aucune requête PostgreSQL n'est faite
|
||||
|
||||
Scénario: API invalide cache Redis lors mise à jour jauge
|
||||
Étant donné que les jauges de "user123" sont en cache Redis
|
||||
Quand je POST /api/v1/listening-events qui modifie une jauge
|
||||
Alors le cache Redis "user:user123:gauges" est supprimé
|
||||
Et le prochain GET recharge depuis PostgreSQL
|
||||
Et remet en cache avec nouvelles valeurs
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user