Files
roadwave/docs/domains/recommendation/features/interest-gauges/evolution-jauges.feature
jpgiannetti 5e5fcf4714 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.
2026-02-07 17:15:02 +01:00

568 lines
19 KiB
Gherkin

# 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