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.
315 lines
10 KiB
Gherkin
315 lines
10 KiB
Gherkin
# language: fr
|
|
Fonctionnalité: API - File d'attente et pré-calcul des contenus
|
|
En tant qu'API backend
|
|
Je veux pré-calculer et gérer la file d'attente de contenus
|
|
Afin d'assurer une navigation fluide sans latence
|
|
|
|
Contexte:
|
|
Étant donné que l'API RoadWave est disponible
|
|
Et que Redis est accessible
|
|
Et que PostgreSQL avec PostGIS est accessible
|
|
Et qu'un utilisateur "user123" existe avec token JWT valide
|
|
|
|
# Pré-calcul initial
|
|
|
|
Scénario: API pré-calcule 5 contenus au démarrage de session
|
|
Étant donné que l'utilisateur "user123" démarre une session
|
|
Et qu'il est situé à Paris (48.8566, 2.3522)
|
|
Et qu'il est en mode voiture (vitesse ≥ 5 km/h)
|
|
Quand je POST /api/v1/queue/initialize
|
|
"""json
|
|
{
|
|
"user_id": "user123",
|
|
"latitude": 48.8566,
|
|
"longitude": 2.3522,
|
|
"mode": "voiture"
|
|
}
|
|
"""
|
|
Alors le statut de réponse est 201
|
|
Et la réponse contient:
|
|
"""json
|
|
{
|
|
"queue_size": 5,
|
|
"contents": [
|
|
{"id": "content1", "title": "...", "position": 1},
|
|
{"id": "content2", "title": "...", "position": 2},
|
|
{"id": "content3", "title": "...", "position": 3},
|
|
{"id": "content4", "title": "...", "position": 4},
|
|
{"id": "content5", "title": "...", "position": 5}
|
|
]
|
|
}
|
|
"""
|
|
Et en Redis, la clé "user:user123:queue" contient 5 contenus
|
|
Et les métadonnées incluent:
|
|
| champ | valeur |
|
|
| last_lat | 48.8566 |
|
|
| last_lon | 2.3522 |
|
|
| mode | voiture |
|
|
| computed_at | (timestamp actuel) |
|
|
Et le TTL est de 900 secondes (15 minutes)
|
|
|
|
Scénario: API GET retourne la file d'attente en cache
|
|
Étant donné qu'une file de 5 contenus existe en cache Redis pour "user123"
|
|
Quand je GET /api/v1/queue
|
|
Alors le statut de réponse est 200
|
|
Et la réponse contient les 5 contenus pré-calculés
|
|
Et la latence de réponse est < 50ms (lecture Redis)
|
|
|
|
Scénario: API retire un contenu de la file après lecture
|
|
Étant donné qu'une file de 5 contenus [C1, C2, C3, C4, C5] existe pour "user123"
|
|
Quand je POST /api/v1/queue/consume
|
|
"""json
|
|
{
|
|
"user_id": "user123",
|
|
"content_id": "C1"
|
|
}
|
|
"""
|
|
Alors le statut de réponse est 200
|
|
Et la file d'attente devient [C2, C3, C4, C5]
|
|
Et la taille de la file est 4
|
|
|
|
# Recalcul automatique
|
|
|
|
Scénario: API recalcule après déplacement >10km
|
|
Étant donné qu'une file a été calculée à Paris (48.8566, 2.3522)
|
|
Et que l'utilisateur se déplace à Versailles (48.8049, 2.1204) soit 12km
|
|
Quand je POST /api/v1/queue/update-location
|
|
"""json
|
|
{
|
|
"user_id": "user123",
|
|
"latitude": 48.8049,
|
|
"longitude": 2.1204
|
|
}
|
|
"""
|
|
Alors le statut de réponse est 200
|
|
Et la réponse contient:
|
|
"""json
|
|
{
|
|
"queue_invalidated": true,
|
|
"reason": "distance_threshold_exceeded",
|
|
"distance_km": 12.1,
|
|
"new_queue_size": 5
|
|
}
|
|
"""
|
|
Et la nouvelle file est basée sur la position Versailles
|
|
Et l'ancienne file a été supprimée de Redis
|
|
|
|
Scénario: API ne recalcule pas si déplacement ≤10km
|
|
Étant donné qu'une file a été calculée à Paris (48.8566, 2.3522)
|
|
Et que l'utilisateur se déplace de 8 km
|
|
Quand je POST /api/v1/queue/update-location
|
|
"""json
|
|
{
|
|
"user_id": "user123",
|
|
"latitude": 48.8500,
|
|
"longitude": 2.3600
|
|
}
|
|
"""
|
|
Alors le statut de réponse est 200
|
|
Et la réponse contient:
|
|
"""json
|
|
{
|
|
"queue_invalidated": false,
|
|
"distance_km": 8.2,
|
|
"threshold": 10
|
|
}
|
|
"""
|
|
Et la file en cache reste inchangée
|
|
|
|
Scénario: API recalcule après 10 minutes
|
|
Étant donné qu'une file a été calculée à 10:00:00
|
|
Et que l'heure actuelle est 10:10:01
|
|
Quand je GET /api/v1/queue
|
|
Alors le statut de réponse est 200
|
|
Et la réponse contient:
|
|
"""json
|
|
{
|
|
"queue_invalidated": true,
|
|
"reason": "time_threshold_exceeded",
|
|
"elapsed_minutes": 10,
|
|
"new_queue_size": 5
|
|
}
|
|
"""
|
|
Et une nouvelle file de 5 contenus est recalculée
|
|
Et le timestamp "computed_at" est mis à jour
|
|
|
|
Scénario: API recalcule quand il reste <3 contenus
|
|
Étant donné qu'il reste 3 contenus [C3, C4, C5] dans la file
|
|
Quand je POST /api/v1/queue/consume (consomme C3)
|
|
Alors le statut de réponse est 200
|
|
Et la file devient [C4, C5]
|
|
Et un recalcul asynchrone est déclenché
|
|
Et 3 nouveaux contenus [C6, C7, C8] sont ajoutés
|
|
Et la file finale est [C4, C5, C6, C7, C8]
|
|
|
|
# Invalidation immédiate
|
|
|
|
Scénario: API invalide après modification préférences utilisateur
|
|
Étant donné qu'une file de 5 contenus existe pour "user123"
|
|
Et que l'utilisateur est en mode piéton (vitesse < 5 km/h)
|
|
Quand je PUT /api/v1/users/user123/preferences
|
|
"""json
|
|
{
|
|
"geo_radius": 20,
|
|
"discovery_factor": 0.7,
|
|
"political_content": false
|
|
}
|
|
"""
|
|
Alors le statut de réponse est 200
|
|
Et la file d'attente est invalidée immédiatement
|
|
Et une nouvelle file est recalculée avec les nouvelles préférences
|
|
Et en Redis, l'ancienne file a été supprimée
|
|
|
|
Scénario: API refuse modification préférences si vitesse >10 km/h
|
|
Étant donné que l'utilisateur roule à 50 km/h
|
|
Quand je PUT /api/v1/users/user123/preferences
|
|
"""json
|
|
{
|
|
"geo_radius": 30
|
|
}
|
|
"""
|
|
Alors le statut de réponse est 403
|
|
Et la réponse contient:
|
|
"""json
|
|
{
|
|
"error": "MODIFICATION_BLOCKED_WHILE_DRIVING",
|
|
"message": "Modification des préférences interdite en conduite (vitesse > 10 km/h)",
|
|
"current_speed_kmh": 50
|
|
}
|
|
"""
|
|
Et les préférences ne sont pas modifiées
|
|
|
|
Scénario: API invalide après démarrage live d'un créateur suivi
|
|
Étant donné que l'utilisateur "user123" suit le créateur "creator456"
|
|
Et qu'une file de 5 contenus existe
|
|
Et que l'utilisateur est dans la zone du créateur
|
|
Quand le créateur "creator456" démarre un live
|
|
Alors une notification push est envoyée à "user123"
|
|
Et la file d'attente est recalculée
|
|
Et le contenu live est inséré en tête de file
|
|
Et la nouvelle file commence par le live
|
|
|
|
# Métadonnées et persistence
|
|
|
|
Scénario: API stocke métadonnées complètes en Redis
|
|
Étant donné qu'une file est calculée à 10:30:00
|
|
Quand je consulte Redis avec la clé "user:user123:queue"
|
|
Alors la structure est:
|
|
"""json
|
|
{
|
|
"contents": [
|
|
{"id": "C1", "position": 1},
|
|
{"id": "C2", "position": 2},
|
|
{"id": "C3", "position": 3},
|
|
{"id": "C4", "position": 4},
|
|
{"id": "C5", "position": 5}
|
|
],
|
|
"metadata": {
|
|
"last_lat": 48.8566,
|
|
"last_lon": 2.3522,
|
|
"computed_at": "2026-02-02T10:30:00Z",
|
|
"mode": "voiture"
|
|
}
|
|
}
|
|
"""
|
|
Et le TTL est exactement 900 secondes
|
|
|
|
Scénario: API calcule distance avec PostGIS
|
|
Étant donné que l'ancienne position est Paris (48.8566, 2.3522)
|
|
Et que la nouvelle position est Versailles (48.8049, 2.1204)
|
|
Quand l'API calcule la distance
|
|
Alors la requête SQL utilise:
|
|
"""sql
|
|
SELECT ST_Distance(
|
|
ST_MakePoint(2.3522, 48.8566)::geography,
|
|
ST_MakePoint(2.1204, 48.8049)::geography
|
|
) / 1000 AS distance_km
|
|
"""
|
|
Et le résultat est 12.1 km
|
|
|
|
# Gestion erreurs
|
|
|
|
Scénario: API gère échec Redis gracieusement
|
|
Étant donné que Redis est indisponible
|
|
Quand je GET /api/v1/queue
|
|
Alors le statut de réponse est 503
|
|
Et la réponse contient:
|
|
"""json
|
|
{
|
|
"error": "CACHE_UNAVAILABLE",
|
|
"message": "Service de cache temporairement indisponible",
|
|
"fallback": "Calcul direct sans cache"
|
|
}
|
|
"""
|
|
Et une nouvelle file est calculée directement depuis PostgreSQL
|
|
|
|
Scénario: API gère aucun contenu disponible
|
|
Étant donné qu'aucun contenu n'existe dans la zone de l'utilisateur
|
|
Quand je POST /api/v1/queue/initialize
|
|
Alors le statut de réponse est 200
|
|
Et la réponse contient:
|
|
"""json
|
|
{
|
|
"queue_size": 0,
|
|
"contents": [],
|
|
"message": "Aucun contenu disponible dans cette zone",
|
|
"suggested_action": "expand_radius"
|
|
}
|
|
"""
|
|
|
|
Scénario: API élargit la zone de recherche si aucun contenu
|
|
Étant donné qu'aucun contenu n'existe dans un rayon de 20km
|
|
Quand je POST /api/v1/queue/expand-radius
|
|
"""json
|
|
{
|
|
"user_id": "user123",
|
|
"additional_radius_km": 50
|
|
}
|
|
"""
|
|
Alors le statut de réponse est 200
|
|
Et la recherche utilise un rayon de 70km (20 + 50)
|
|
Et une nouvelle file est calculée avec ce rayon
|
|
|
|
# Performance
|
|
|
|
Scénario: API répond en <100ms pour lecture cache
|
|
Étant donné qu'une file existe en Redis
|
|
Quand je GET /api/v1/queue à 10:30:00.000
|
|
Alors la réponse est reçue à 10:30:00.050 (50ms)
|
|
Et la latence est < 100ms
|
|
|
|
Scénario: API recalcule en arrière-plan sans bloquer
|
|
Étant donné qu'il reste 2 contenus dans la file
|
|
Quand je POST /api/v1/queue/consume
|
|
Alors le statut de réponse est 200 (immédiat)
|
|
Et la réponse est retournée en < 100ms
|
|
Et le recalcul asynchrone démarre en parallèle
|
|
Et le client ne perçoit aucune latence
|
|
|
|
Plan du Scénario: Conditions de recalcul selon distance
|
|
Étant donné qu'une file a été calculée à une position donnée
|
|
Quand l'utilisateur se déplace de <distance> km
|
|
Alors la file est <action>
|
|
|
|
Exemples:
|
|
| distance | action |
|
|
| 5 | conservée |
|
|
| 9.9 | conservée |
|
|
| 10.0 | conservée |
|
|
| 10.1 | invalidée et recalculée |
|
|
| 15 | invalidée et recalculée |
|
|
| 50 | invalidée et recalculée |
|
|
|
|
Plan du Scénario: Conditions de recalcul selon temps écoulé
|
|
Étant donné qu'une file a été calculée il y a <temps> minutes
|
|
Quand je consulte la file
|
|
Alors la file est <action>
|
|
|
|
Exemples:
|
|
| temps | action |
|
|
| 5 | conservée |
|
|
| 9 | conservée |
|
|
| 10 | invalidée et recalculée |
|
|
| 15 | invalidée et recalculée |
|
|
| 60 | invalidée et recalculée |
|