Compare commits
47 Commits
test-enzo
...
refactor/d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c0522158c | ||
|
|
1a67e5ffd0 | ||
|
|
be9fc998cc | ||
|
|
5e5fcf4714 | ||
|
|
78422bb2c0 | ||
|
|
04cd6327ab | ||
|
|
563980aeb7 | ||
|
|
bd724dcb8e | ||
|
|
f6a5b9afce | ||
|
|
e82ed63904 | ||
|
|
477630d216 | ||
|
|
851832baec | ||
|
|
3bdc6c6241 | ||
|
|
448b4b6ca7 | ||
|
|
7de686ab33 | ||
|
|
159e0b2ff4 | ||
|
|
36e30bb5ab | ||
|
|
c48222cc63 | ||
|
|
a82dbfe1dc | ||
|
|
7d3b32856e | ||
|
|
a19a901ed4 | ||
|
|
ea77aa8ac7 | ||
|
|
852240b5ec | ||
|
|
718581b954 | ||
|
|
2cc9da29ff | ||
|
|
99328a845a | ||
|
|
bac0423be9 | ||
|
|
6ba0688f87 | ||
|
|
b132fb957d | ||
|
|
4e25ceab20 | ||
|
|
267f574467 | ||
|
|
2365b7f344 | ||
|
|
158690ed3e | ||
|
|
59a6d49fbb | ||
|
|
ec28b52ae5 | ||
|
|
78b723baa3 | ||
|
|
81ccbf79e6 | ||
|
|
60dce59905 | ||
|
|
5986286c3d | ||
|
|
9bb1891bc1 | ||
|
|
18c8901d69 | ||
|
|
fa6ba43888 | ||
|
|
cf26d8a244 | ||
|
|
0609f380ff | ||
|
|
a3b7c90be0 | ||
|
|
69a7bd80cc | ||
|
|
c3abdd74af |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -6,9 +6,9 @@
|
||||
*.dylib
|
||||
/bin/
|
||||
/dist/
|
||||
api
|
||||
worker
|
||||
migrate
|
||||
/api
|
||||
/worker
|
||||
/migrate
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
53
CLAUDE.md
53
CLAUDE.md
@@ -2,13 +2,19 @@
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Important Rules
|
||||
|
||||
**Git Commits**:
|
||||
- NEVER add "Co-Authored-By: Claude" to commit messages
|
||||
- Keep commit messages clean and professional without AI attribution
|
||||
|
||||
## Project Overview
|
||||
|
||||
RoadWave is a geo-localized audio social network for road users (drivers, pedestrians, tourists). Users listen to audio content (podcasts, audio guides, ads, live radio) based on their geographic location and interests.
|
||||
|
||||
**Tech Stack**:
|
||||
- Backend: Go 1.21+ with Fiber framework
|
||||
- Mobile: Flutter (see [ADR-014](docs/adr/014-frontend-mobile.md))
|
||||
- Mobile: Flutter (see [ADR-012](docs/adr/012-frontend-mobile.md))
|
||||
- Database: PostgreSQL 16+ with PostGIS extension
|
||||
- Cache: Redis 7+ with geospatial features
|
||||
- Auth: Zitadel (self-hosted IAM)
|
||||
@@ -32,11 +38,11 @@ This is a monorepo organized as follows:
|
||||
- Backend step definitions: `backend/tests/bdd/`
|
||||
- Mobile step definitions: `mobile/tests/bdd/`
|
||||
|
||||
See [ADR-016](docs/adr/016-organisation-monorepo.md) for monorepo organization rationale.
|
||||
See [ADR-014](docs/adr/014-organisation-monorepo.md) for monorepo organization rationale.
|
||||
|
||||
## Backend Architecture
|
||||
|
||||
**Modular monolith** with clear module separation ([ADR-012](docs/adr/012-architecture-backend.md)):
|
||||
**Modular monolith** with clear module separation ([ADR-010](docs/adr/010-architecture-backend.md)):
|
||||
|
||||
```
|
||||
backend/internal/
|
||||
@@ -52,7 +58,7 @@ backend/internal/
|
||||
|
||||
**Module pattern**: Each module follows `handler.go` → `service.go` → `repository.go`.
|
||||
|
||||
**Database access**: Uses `sqlc` ([ADR-013](docs/adr/013-orm-acces-donnees.md)) for type-safe Go code generation from SQL queries. This allows writing complex PostGIS spatial queries while maintaining compile-time type safety.
|
||||
**Database access**: Uses `sqlc` ([ADR-011](docs/adr/011-orm-acces-donnees.md)) for type-safe Go code generation from SQL queries. This allows writing complex PostGIS spatial queries while maintaining compile-time type safety.
|
||||
|
||||
## Development Commands
|
||||
|
||||
@@ -76,7 +82,7 @@ Services after `make docker-up`:
|
||||
|
||||
### Testing
|
||||
|
||||
**Test Strategy** ([ADR-015](docs/adr/015-strategie-tests.md)):
|
||||
**Test Strategy** ([ADR-013](docs/adr/013-strategie-tests.md)):
|
||||
- Unit tests: Testify (80%+ coverage target)
|
||||
- Integration tests: Testcontainers (for PostGIS queries)
|
||||
- BDD tests: Godog/Gherkin (user stories validation)
|
||||
@@ -178,13 +184,42 @@ Feature: Geolocalised recommendation
|
||||
|
||||
All technical decisions are documented in Architecture Decision Records (ADRs) in `/docs/adr/`:
|
||||
|
||||
### Core Architecture
|
||||
- [ADR-001](docs/adr/001-langage-backend.md): Backend language (Go)
|
||||
- [ADR-002](docs/adr/002-protocole-streaming.md): Streaming protocol (HLS)
|
||||
- [ADR-010](docs/adr/010-architecture-backend.md): Backend architecture (modular monolith)
|
||||
- [ADR-011](docs/adr/011-orm-acces-donnees.md): Data access (sqlc)
|
||||
- [ADR-012](docs/adr/012-frontend-mobile.md): Frontend mobile (Flutter)
|
||||
- [ADR-014](docs/adr/014-organisation-monorepo.md): Monorepo organization
|
||||
|
||||
### Data & Infrastructure
|
||||
- [ADR-005](docs/adr/005-base-de-donnees.md): Database (PostgreSQL + PostGIS)
|
||||
- [ADR-021](docs/adr/021-solution-cache.md): Cache solution (Redis)
|
||||
- [ADR-015](docs/adr/015-hebergement.md): Hosting (OVH)
|
||||
- [ADR-019](docs/adr/019-geolocalisation-ip.md): IP geolocation fallback
|
||||
|
||||
### Streaming & Content
|
||||
- [ADR-002](docs/adr/002-protocole-streaming.md): Streaming protocol (HLS)
|
||||
- [ADR-003](docs/adr/003-codec-audio.md): Audio codec (Opus)
|
||||
- [ADR-004](docs/adr/004-cdn.md): CDN strategy
|
||||
|
||||
### Security & Auth
|
||||
- [ADR-006](docs/adr/006-chiffrement.md): Encryption (TLS 1.3)
|
||||
- [ADR-008](docs/adr/008-authentification.md): Authentication (Zitadel)
|
||||
- [ADR-012](docs/adr/012-architecture-backend.md): Backend architecture (modular monolith)
|
||||
- [ADR-013](docs/adr/013-orm-acces-donnees.md): Data access (sqlc)
|
||||
- [ADR-016](docs/adr/016-organisation-monorepo.md): Monorepo organization
|
||||
- [ADR-025](docs/adr/025-securite-secrets.md): Secrets management
|
||||
|
||||
### Testing & Quality
|
||||
- [ADR-007](docs/adr/007-tests-bdd.md): BDD tests (Gherkin)
|
||||
- [ADR-013](docs/adr/013-strategie-tests.md): Test strategy
|
||||
- [ADR-022](docs/adr/022-strategie-cicd-monorepo.md): CI/CD strategy
|
||||
|
||||
### Features & Operations
|
||||
- [ADR-009](docs/adr/009-solution-paiement.md): Payment solution (Mangopay)
|
||||
- [ADR-016](docs/adr/016-service-emailing.md): Email service (Brevo)
|
||||
- [ADR-017](docs/adr/017-notifications-geolocalisees.md): Geo notifications
|
||||
- [ADR-018](docs/adr/018-notifications-push.md): Push notifications
|
||||
- [ADR-020](docs/adr/020-librairies-flutter.md): Flutter libraries
|
||||
- [ADR-023](docs/adr/023-architecture-moderation.md): Moderation architecture
|
||||
- [ADR-024](docs/adr/024-monitoring-observabilite.md): Monitoring & observability
|
||||
|
||||
**When making architectural decisions**, check if there's an existing ADR or create a new one following the established pattern.
|
||||
|
||||
|
||||
2
Makefile
2
Makefile
@@ -51,7 +51,7 @@ test-integration:
|
||||
## test-bdd: Run BDD tests (Godog)
|
||||
test-bdd:
|
||||
@echo "$(BLUE)Running BDD tests...$(NC)"
|
||||
@godog run features/
|
||||
@godog run docs/domains/*/features/
|
||||
|
||||
## test-coverage: Run tests with coverage report
|
||||
test-coverage:
|
||||
|
||||
26
TECHNICAL.md
26
TECHNICAL.md
@@ -7,25 +7,31 @@
|
||||
| Composant | Technologie | ADR |
|
||||
|-----------|-------------|-----|
|
||||
| **Backend** | Go + Fiber | [ADR-001](docs/adr/001-langage-backend.md) |
|
||||
| **Architecture Backend** | Monolithe Modulaire | [ADR-012](docs/adr/012-architecture-backend.md) |
|
||||
| **Architecture Backend** | Monolithe Modulaire | [ADR-010](docs/adr/010-architecture-backend.md) |
|
||||
| **Authentification** | Zitadel (self-hosted OVH) | [ADR-008](docs/adr/008-authentification.md) |
|
||||
| **Streaming** | HLS | [ADR-002](docs/adr/002-protocole-streaming.md) |
|
||||
| **Codec** | Opus | [ADR-003](docs/adr/003-codec-audio.md) |
|
||||
| **CDN** | NGINX Cache (OVH VPS) | [ADR-004](docs/adr/004-cdn.md) |
|
||||
| **Storage** | OVH Object Storage | [ADR-004](docs/adr/004-cdn.md) |
|
||||
| **Hébergement MVP** | OVH VPS Essential | [ADR-017](docs/adr/017-hebergement.md) |
|
||||
| **Organisation** | Monorepo | [ADR-016](docs/adr/016-organisation-monorepo.md) |
|
||||
| **Hébergement MVP** | OVH VPS Essential | [ADR-015](docs/adr/015-hebergement.md) |
|
||||
| **Organisation** | Monorepo | [ADR-014](docs/adr/014-organisation-monorepo.md) |
|
||||
| **Base de données** | PostgreSQL + PostGIS | [ADR-005](docs/adr/005-base-de-donnees.md) |
|
||||
| **ORM/Accès données** | sqlc | [ADR-013](docs/adr/013-orm-acces-donnees.md) |
|
||||
| **Cache** | Redis Cluster | [ADR-005](docs/adr/005-base-de-donnees.md) |
|
||||
| **ORM/Accès données** | sqlc | [ADR-011](docs/adr/011-orm-acces-donnees.md) |
|
||||
| **Cache** | Redis Cluster | [ADR-021](docs/adr/021-solution-cache.md) |
|
||||
| **Chiffrement** | TLS 1.3 | [ADR-006](docs/adr/006-chiffrement.md) |
|
||||
| **Live** | WebRTC | [ADR-002](docs/adr/002-protocole-streaming.md) |
|
||||
| **Frontend Mobile** | Flutter | [ADR-014](docs/adr/014-frontend-mobile.md) |
|
||||
| **Tests** | Testify + Godog (Gherkin) | [ADR-015](docs/adr/015-strategie-tests.md), [ADR-007](docs/adr/007-tests-bdd.md) |
|
||||
| **Frontend Mobile** | Flutter | [ADR-012](docs/adr/012-frontend-mobile.md) |
|
||||
| **Tests** | Testify + Godog (Gherkin) | [ADR-013](docs/adr/013-strategie-tests.md), [ADR-007](docs/adr/007-tests-bdd.md) |
|
||||
| **Paiements** | Mangopay | [ADR-009](docs/adr/009-solution-paiement.md) |
|
||||
| **Emailing** | Brevo | [ADR-018](docs/adr/018-service-emailing.md) |
|
||||
| **Commandes volant** | Like automatique | [ADR-010](docs/adr/010-commandes-volant.md) |
|
||||
| **Conformité stores** | CarPlay, Android Auto, App/Play Store | [ADR-011](docs/adr/011-conformite-stores-carplay-android-auto.md) |
|
||||
| **Emailing** | Brevo | [ADR-016](docs/adr/016-service-emailing.md) |
|
||||
| **Géolocalisation IP** | IP2Location (fallback) | [ADR-019](docs/adr/019-geolocalisation-ip.md) |
|
||||
| **Librairies Mobile** | Flutter packages | [ADR-020](docs/adr/020-librairies-flutter.md) |
|
||||
| **CI/CD** | GitHub Actions (monorepo) | [ADR-022](docs/adr/022-strategie-cicd-monorepo.md) |
|
||||
| **Modération** | Architecture modération | [ADR-023](docs/adr/023-architecture-moderation.md) |
|
||||
| **Monitoring** | Prometheus + Grafana | [ADR-024](docs/adr/024-monitoring-observabilite.md) |
|
||||
| **Secrets** | Vault + sealed secrets | [ADR-025](docs/adr/025-securite-secrets.md) |
|
||||
| **Notifications géo** | Push + geofencing | [ADR-017](docs/adr/017-notifications-geolocalisees.md) |
|
||||
| **Notifications push** | FCM + APNS | [ADR-018](docs/adr/018-notifications-push.md) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,849 +0,0 @@
|
||||
# Analyse des Incohérences entre ADR et Règles Métier
|
||||
|
||||
**Date d'analyse** : 2026-01-28
|
||||
**Analysé par** : Audit Architecture RoadWave
|
||||
**Scope** : 18 ADR × Règles Métier (17 fichiers)
|
||||
|
||||
---
|
||||
|
||||
## Résumé Exécutif
|
||||
|
||||
Cette analyse a identifié **15 incohérences** entre les décisions d'architecture (ADR) et les règles métier du projet RoadWave.
|
||||
|
||||
### Répartition par Sévérité
|
||||
|
||||
| Sévérité | Nombre | % Total | Statut | Action Required |
|
||||
|----------|--------|---------|--------|-----------------|
|
||||
| 🔴 **CRITICAL** | 2 | 14% | ✅ **RÉSOLU** | ~~avant implémentation~~ |
|
||||
| 🟠 **HIGH** | 2 | 14% | ✅ **RÉSOLU** (2 résolus, 1 annulé) | ~~Résolution Sprint 1-2~~ |
|
||||
| 🟡 **MODERATE** | 9 | 64% | ✅ **RÉSOLU** (6 résolus, 2 annulés, 1 documenté) | ~~Résolution Sprint 3-5~~ |
|
||||
| 🟢 **LOW** | 1 | 7% | ✅ **ANNULÉ** (Faux problème) | ~~À clarifier lors du développement~~ |
|
||||
|
||||
### Impact par Domaine
|
||||
|
||||
| Domaine | Nombre d'incohérences | Criticité maximale |
|
||||
|---------|----------------------|-------------------|
|
||||
| Streaming & Géolocalisation | 3 | 🔴 CRITICAL |
|
||||
| Données & Infrastructure | 2 | 🟠 HIGH |
|
||||
| Authentification & Sécurité | 2 | 🟠 HIGH |
|
||||
| Tests & Qualité | 2 | 🟡 MODERATE |
|
||||
| Coûts & Déploiement | 3 | 🟡 MODERATE |
|
||||
| UX & Engagement | 2 | 🟡 MODERATE |
|
||||
|
||||
---
|
||||
|
||||
## 🔴 Incohérences Critiques (Blocantes)
|
||||
|
||||
### #1 : HLS ne supporte pas les Notifications Push en Arrière-plan
|
||||
|
||||
**Statut** : ✅ **RÉSOLU** (ADR-017 créé)
|
||||
|
||||
| Élément | Détail |
|
||||
|---------|--------|
|
||||
| **ADR concerné** | ADR-002 (Protocole Streaming) |
|
||||
| **Règle métier** | Règle 05, section 5.1.2 (Mode Piéton, lignes 86-120) |
|
||||
| **Conflit** | HLS est unidirectionnel (serveur→client), ne peut pas envoyer de push quand l'app est fermée |
|
||||
| **Impact** | Mode piéton non fonctionnel : notifications "Point d'intérêt à 200m" impossibles |
|
||||
|
||||
**Scénario d'échec** :
|
||||
```
|
||||
Utilisateur: Marie se promène, app fermée
|
||||
Position: 150m de la Tour Eiffel
|
||||
Attendu: Push notification "🗼 À proximité: Histoire de la Tour Eiffel"
|
||||
Réel: Rien (HLS ne peut pas notifier)
|
||||
```
|
||||
|
||||
**Solution implémentée** :
|
||||
- ✅ **ADR-017** : Architecture hybride WebSocket + Firebase Cloud Messaging
|
||||
- Phase 1 (MVP) : Push serveur via FCM/APNS
|
||||
- Phase 2 : Geofencing natif iOS/Android pour mode offline
|
||||
|
||||
**Actions requises** :
|
||||
- [ ] Backend : Implémenter endpoint WebSocket `/ws/location`
|
||||
- [ ] Backend : Worker PostGIS avec requête `ST_DWithin` (30s interval)
|
||||
- [ ] Mobile : Intégrer Firebase SDK (`firebase_messaging`)
|
||||
- [ ] Tests : Validation en conditions réelles (10 testeurs, Paris)
|
||||
|
||||
---
|
||||
|
||||
### #2 : Latence HLS Incompatible avec ETA de 7 Secondes
|
||||
|
||||
**Statut** : ✅ **RÉSOLU** (ADR-002 mis à jour)
|
||||
|
||||
| Élément | Détail |
|
||||
|---------|--------|
|
||||
| **ADR concerné** | ADR-002 (Protocole Streaming, lignes 40-41) |
|
||||
| **Règle métier** | Règle 05 (lignes 16-20), Règle 17 (lignes 25-30, 120-124) |
|
||||
| **Conflit** | ETA de 7s avant le point, mais HLS a 5-30s de latence → audio démarre APRÈS avoir dépassé le point |
|
||||
| **Impact** | UX catastrophique : utilisateur entend "Vous êtes devant le château" 100m APRÈS l'avoir dépassé |
|
||||
|
||||
**Calcul du problème** (90 km/h = 25 m/s) :
|
||||
```
|
||||
t=0s → Notification "Suivant: Château dans 7s" (175m avant)
|
||||
t=7s → Utilisateur arrive au château
|
||||
t=15s → HLS démarre (latence 15s)
|
||||
Résultat: Audio démarre 200m APRÈS le point ❌
|
||||
```
|
||||
|
||||
**Solution implémentée** :
|
||||
- ✅ **ADR-002 mis à jour** : Section "Gestion de la Latence et Synchronisation Géolocalisée"
|
||||
- Pre-buffering à ETA=30s (15 premières secondes en cache local)
|
||||
- ETA adaptatif : 5s si cache prêt, 15s sinon
|
||||
- Mesure dynamique de latence HLS par utilisateur
|
||||
|
||||
**Actions requises** :
|
||||
- [ ] Backend : Endpoint `/api/v1/audio/poi/:id/intro` (retourne 15s d'audio)
|
||||
- [ ] Mobile : Service `PreBufferService` avec cache local (max 100 MB)
|
||||
- [ ] Mobile : Loader visuel avec progression si buffer > 3s
|
||||
- [ ] Tests : Validation synchronisation ±10m du POI
|
||||
|
||||
---
|
||||
|
||||
## 🟠 Incohérences Importantes (Sprint 1-2)
|
||||
|
||||
### #3 : Souveraineté des Données (Français vs Suisse)
|
||||
|
||||
**Statut** : ✅ **RÉSOLU** (ADR-008 mis à jour)
|
||||
|
||||
| Élément | Détail |
|
||||
|---------|--------|
|
||||
| **ADR concernés** | ADR-004 (CDN, ligne 26), ADR-008 (Auth, mis à jour) |
|
||||
| **Règle métier** | Règle 02 (RGPD, section 13.10) |
|
||||
| **Conflit** | ADR-004 revendique "100% souveraineté française" mais ADR-008 utilisait Zitadel (entreprise suisse) |
|
||||
| **Impact** | Contradiction marketing + risque juridique si promesse "100% français" |
|
||||
|
||||
**Solution implémentée** : **Self-hosting Zitadel sur OVH France**
|
||||
|
||||
- ✅ Container Docker sur le même VPS OVH (Gravelines, France)
|
||||
- ✅ Base de données PostgreSQL partagée (schéma séparé pour Zitadel)
|
||||
- ✅ Aucune donnée ne transite par des serveurs tiers
|
||||
- ✅ Souveraineté totale garantie : 100% des données en France
|
||||
- ✅ Cohérence complète avec ADR-004 (CDN 100% français)
|
||||
|
||||
**Changements apportés** :
|
||||
- ✅ ADR-008 mis à jour avec architecture self-hosted détaillée
|
||||
- ✅ TECHNICAL.md mis à jour (tableau + diagramme architecture)
|
||||
- ✅ Clarification : Zitadel est open source, donc aucune dépendance à une entreprise suisse
|
||||
|
||||
**Actions complétées** :
|
||||
- [x] Décision validée : Self-host sur OVH
|
||||
- [x] ADR-008 mis à jour avec architecture self-hosted
|
||||
- [x] TECHNICAL.md mis à jour
|
||||
|
||||
---
|
||||
|
||||
### #4 : ORM sqlc vs Types PostGIS
|
||||
|
||||
**Statut** : ✅ **RÉSOLU** (ADR-011 mis à jour)
|
||||
|
||||
| Élément | Détail |
|
||||
|---------|--------|
|
||||
| **ADR concerné** | ADR-011 (section "Gestion des Types PostGIS") |
|
||||
| **Règle métier** | N/A (problème technique pur) |
|
||||
| **Conflit** | sqlc génère types Go depuis SQL, mais PostGIS geography/geometry ne mappent pas proprement |
|
||||
| **Impact** | Risque de type `interface{}` ou `[]byte` pour géographie → perte de type safety revendiquée |
|
||||
|
||||
**Solution implémentée** :
|
||||
|
||||
**Wrappers typés + fonctions de conversion PostGIS** :
|
||||
|
||||
1. **Wrapper types Go** avec méthodes `Scan/Value` pour conversion automatique
|
||||
2. **Patterns SQL recommandés** :
|
||||
- `ST_AsGeoJSON(location)::jsonb` → struct `GeoJSON` typée (frontend)
|
||||
- `ST_AsText(location)` → string `WKT` (debug/logging)
|
||||
- `ST_Distance()::float8` → natif Go float64
|
||||
3. **Index GIST** sur colonnes géographiques pour performance
|
||||
4. **Architecture conversion** :
|
||||
```
|
||||
SQL PostGIS → ST_AsGeoJSON() → json.RawMessage → GeoJSON (strongly-typed)
|
||||
```
|
||||
|
||||
**Code Pattern** :
|
||||
|
||||
```go
|
||||
// internal/geo/types.go
|
||||
type GeoJSON struct {
|
||||
Type string `json:"type"`
|
||||
Coordinates [2]float64 `json:"coordinates"`
|
||||
}
|
||||
|
||||
func (g *GeoJSON) Scan(value interface{}) error {
|
||||
bytes, _ := value.([]byte)
|
||||
return json.Unmarshal(bytes, g)
|
||||
}
|
||||
```
|
||||
|
||||
```sql
|
||||
-- queries/poi.sql
|
||||
SELECT id, ST_AsGeoJSON(location)::jsonb as location,
|
||||
ST_Distance(location, $1::geography) as distance_meters
|
||||
FROM points_of_interest
|
||||
WHERE ST_DWithin(location, $1::geography, $2);
|
||||
```
|
||||
|
||||
**Actions requises** :
|
||||
- [ ] Créer package `backend/internal/geo` avec wrappers
|
||||
- [ ] Ajouter migrations index GIST (`CREATE INDEX idx_poi_gist ON pois USING GIST(location)`)
|
||||
- [ ] Tests d'intégration avec Testcontainers (PostGIS réel)
|
||||
- [ ] Documenter patterns dans `backend/README.md`
|
||||
|
||||
**Référence** : [ADR-011 - Gestion des Types PostGIS](docs/adr/011-orm-acces-donnees.md#gestion-des-types-postgis)
|
||||
|
||||
---
|
||||
|
||||
### #5 : Cache Redis (TTL 5min) vs Mode Offline (30 jours)
|
||||
|
||||
**Statut** : ✅ **ANNULÉ** (Faux problème)
|
||||
|
||||
| Élément | Détail |
|
||||
|---------|--------|
|
||||
| **ADR concerné** | ADR-005 (BDD, ligne 60) |
|
||||
| **Règle métier** | Règle 11 (Mode Offline, lignes 58-77) |
|
||||
| **Conflit** | ~~Redis avec TTL 5min pour géolocalisation, mais contenu offline valide 30 jours~~ |
|
||||
| **Impact** | ~~En mode offline, impossible de rafraîchir le cache géolocalisation → POI proches non détectés~~ |
|
||||
|
||||
**Raison de l'annulation** : Le mode offline ne concerne **pas les POI** (Points d'Intérêt) mais uniquement le contenu audio déjà téléchargé. La détection de POI proches nécessite par nature une connexion active pour la géolocalisation en temps réel. Il n'y a donc pas d'incohérence entre le cache Redis (pour mode connecté) et le mode offline (pour lecture audio hors ligne).
|
||||
|
||||
**Aucune action requise** : Ce point est un faux problème et peut être ignoré.
|
||||
|
||||
---
|
||||
|
||||
### #6 : Package Geofencing vs Permissions iOS/Android
|
||||
|
||||
**Statut** : ✅ **RÉSOLU** (Stratégie de permissions progressive implémentée)
|
||||
|
||||
| Élément | Détail |
|
||||
|---------|--------|
|
||||
| **ADR concerné** | ADR-010 (Frontend Mobile, mis à jour) |
|
||||
| **Règle métier** | Règle 05 (section 5.1.2, mis à jour), Règle 02 (RGPD) |
|
||||
| **Conflit** | ~~Package `geofence_service` choisi, mais pas de doc sur compatibilité permissions "optionnelles"~~ |
|
||||
| **Impact** | ~~Risque de rejet App Store/Play Store si permissions obligatoires mal gérées~~ |
|
||||
|
||||
**Solution implémentée** :
|
||||
|
||||
**Stratégie de permissions progressive en 2 étapes** :
|
||||
|
||||
```dart
|
||||
enum LocationPermissionLevel {
|
||||
denied, // Pas de permission
|
||||
whenInUse, // "Quand l'app est ouverte" (iOS)
|
||||
always, // "Toujours" (iOS) / Background (Android)
|
||||
}
|
||||
|
||||
class GeofencingService {
|
||||
Future<void> requestPermissions() async {
|
||||
// Étape 1: Demander "When In Use" (moins intrusif)
|
||||
var status = await Permission.locationWhenInUse.request();
|
||||
|
||||
if (status.isGranted) {
|
||||
// Mode basique: détection seulement app ouverte
|
||||
_enableBasicGeofencing();
|
||||
|
||||
// Étape 2 (optionnelle): Proposer upgrade vers "Always"
|
||||
_showUpgradePermissionDialog();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> upgradeToAlwaysPermission() async {
|
||||
// Demandé seulement si utilisateur veut mode piéton complet
|
||||
await Permission.locationAlways.request();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Actions complétées** :
|
||||
- [x] ✅ ADR-010 mis à jour avec section complète "Stratégie de Permissions"
|
||||
- [x] ✅ Règle 05 (section 5.1.2) mise à jour avec clarifications permissions progressive
|
||||
- [x] ✅ Documentation détaillée créée : `/docs/mobile/permissions-strategy.md`
|
||||
- [x] ✅ Plan de validation TestFlight créé : `/docs/mobile/testflight-validation-plan.md`
|
||||
|
||||
**Changements apportés** :
|
||||
- ✅ Permissions demandées en 2 étapes : "When In Use" (onboarding) → "Always" (optionnel, mode piéton)
|
||||
- ✅ Écran d'éducation obligatoire avant demande "Always" (requis pour validation stores)
|
||||
- ✅ Fallback gracieux à tous niveaux : app utilisable même sans permission arrière-plan
|
||||
- ✅ Mode dégradé (GeoIP) si toutes permissions refusées
|
||||
- ✅ Configuration iOS/Android complète avec textes validés RGPD
|
||||
- ✅ Plan de validation beta (TestFlight + Play Console Internal Testing)
|
||||
|
||||
**Références** :
|
||||
- [ADR-010 - Stratégie de Permissions](../adr/010-frontend-mobile.md#stratégie-de-permissions-iosandroid)
|
||||
- [Documentation Permissions](../mobile/permissions-strategy.md)
|
||||
- [Plan Validation TestFlight](../mobile/testflight-validation-plan.md)
|
||||
- [Règle 05 - Mode Piéton](../regles-metier/05-interactions-navigation.md#512-mode-piéton-audio-guides)
|
||||
|
||||
---
|
||||
|
||||
## 🟡 Incohérences Modérées (Sprint 3-5)
|
||||
|
||||
### #7 : Points vs Pourcentages dans les Jauges
|
||||
|
||||
**Statut** : ✅ **RÉSOLU** (Terminologie unifiée : points de pourcentage absolus)
|
||||
|
||||
| Élément | Détail |
|
||||
|---------|--------|
|
||||
| **ADR concerné** | Règle 05 (section 5.3) (Commandes Volant, mis à jour) |
|
||||
| **Règle métier** | Règle 03 (Centres d'intérêt, mis à jour) |
|
||||
| **Conflit** | ~~ADR dit "+2 **points**", Règle dit "+2**%**" pour même action~~ |
|
||||
| **Impact** | ~~Ambiguïté sur calcul : +2 points absolus ou +2% relatifs ?~~ |
|
||||
|
||||
**Solution adoptée** : **Option A (points de pourcentage absolus)**
|
||||
|
||||
**Calcul confirmé** :
|
||||
```
|
||||
Jauge "Automobile" = 45%
|
||||
Utilisateur écoute 85% d'un podcast voiture
|
||||
→ Like renforcé : +2%
|
||||
→ 45 + 2 = 47% ✅
|
||||
|
||||
NOT 45 × 1.02 = 45.9% ❌
|
||||
```
|
||||
|
||||
**Justification** :
|
||||
- ✅ **Progression linéaire** : Intuitive et prévisible
|
||||
- ✅ **Équité** : Tous les utilisateurs progressent à la même vitesse
|
||||
- ✅ **Gamification standard** : Cohérent avec Duolingo, Spotify, Strava
|
||||
- ✅ **Simplicité technique** : Addition simple, pas de risque d'overflow
|
||||
- ✅ **Prédictibilité UX** : "+2%" signifie vraiment +2 points de pourcentage
|
||||
|
||||
**Actions complétées** :
|
||||
- [x] ✅ Règle 05 (section 5.3) mis à jour : "points" → "+2%" avec note explicite "points de pourcentage absolus"
|
||||
- [x] ✅ Règle 05 (section 5.3) : Section "Implémentation Technique" ajoutée (architecture 2 services)
|
||||
- [x] ✅ Règle 03 : Note ajoutée clarifiant calcul absolu vs relatif
|
||||
- [x] ✅ Règle 03 : Exemples de calcul vérifiés et cohérents
|
||||
- [x] ✅ Référence croisée Règle 05 (section 5.3) ↔ Règle 03
|
||||
- [x] ✅ ADR-010 supprimé : Contenu consolidé dans Règle 05 (métier) pour éviter redondance
|
||||
|
||||
**Changements apportés** :
|
||||
|
||||
**Règle 05 (section 5.3)** :
|
||||
- Règles reformulées : "+2 **points**" → "**+2%**" (points de pourcentage absolus)
|
||||
- Note explicite ajoutée : "Par exemple, si jauge = 45%, +2% → 47%"
|
||||
- Nouvelle section "Implémentation Technique" avec architecture 2 services (Calculation + Update)
|
||||
- Pattern de calcul correct (addition) vs incorrect (multiplication)
|
||||
- Exemples de calcul concrets
|
||||
|
||||
**Règle 03** :
|
||||
- Tableau mis à jour : valeurs en gras (**+2%**, **+1%**, etc.)
|
||||
- Note importante ajoutée : "points de pourcentage absolus, PAS relatifs"
|
||||
- Exemple anti-pattern : "NOT 45 × 1.02 = 45.9% ❌"
|
||||
- Référence croisée vers Règle 05 (section 5.3) pour implémentation
|
||||
|
||||
**Références** :
|
||||
- [Règle 05 - Implémentation Technique](../regles-metier/05-interactions-navigation.md#implémentation-technique-backend)
|
||||
- [Règle 03 - Évolution des Jauges](../regles-metier/03-centres-interet-jauges.md#31-évolution-des-jauges)
|
||||
|
||||
---
|
||||
|
||||
### #8 : OAuth2 Complexe vs Email/Password Simple
|
||||
|
||||
**Statut** : ✅ **RÉSOLU** (Clarification : OAuth2 = protocole, PAS providers tiers)
|
||||
|
||||
| Élément | Détail |
|
||||
|---------|--------|
|
||||
| **ADR concerné** | ADR-008 (Auth, mis à jour) |
|
||||
| **Règle métier** | Règle 01 (Auth, mis à jour) |
|
||||
| **Conflit** | ~~ADR implémente OAuth2 PKCE complet, mais Règle dit "❌ Pas d'OAuth tiers, email/password uniquement"~~ |
|
||||
| **Impact** | ~~Sur-ingénierie : OAuth2 conçu pour tiers (Google, Facebook) mais non utilisé ici~~ |
|
||||
|
||||
**Clarification** : Il y avait une **confusion terminologique** entre :
|
||||
- **OAuth2 PKCE** (protocole d'authentification moderne pour mobile) ✅ Utilisé
|
||||
- **OAuth providers tiers** (Google, Apple, Facebook) ❌ **Pas utilisés**
|
||||
|
||||
**Solution adoptée** :
|
||||
|
||||
RoadWave utilise **Zitadel self-hosted** avec **email/password natif uniquement** :
|
||||
|
||||
| Aspect | Détail |
|
||||
|--------|--------|
|
||||
| **Méthode d'authentification** | Email + mot de passe (formulaire natif Zitadel) |
|
||||
| **Protocole technique** | OAuth2 PKCE (entre app mobile et Zitadel) |
|
||||
| **Fournisseurs tiers** | ❌ Aucun (pas de Google, Apple, Facebook) |
|
||||
|
||||
**Pourquoi OAuth2 PKCE alors ?** :
|
||||
- ✅ **Standard moderne** pour auth mobile (sécurisé, refresh tokens)
|
||||
- ✅ **Protocole**, pas un provider externe
|
||||
- ✅ Alternative serait session cookies (moins adapté mobile) ou JWT custom (réinventer la roue)
|
||||
- ✅ Zitadel implémente OAuth2/OIDC comme protocole, mais auth reste email/password
|
||||
|
||||
**Flow d'authentification** :
|
||||
```
|
||||
User → Formulaire email/password (app mobile)
|
||||
→ Zitadel (OAuth2 PKCE protocol)
|
||||
→ Validation email/password natif
|
||||
→ JWT access token + refresh token
|
||||
→ Go API (validation JWT locale)
|
||||
```
|
||||
|
||||
**Actions complétées** :
|
||||
- [x] ✅ ADR-008 : Section "OAuth2 PKCE : Protocole vs Fournisseurs Tiers" ajoutée
|
||||
- [x] ✅ ADR-008 : Architecture clarifiée ("Email/Pass native" dans diagramme)
|
||||
- [x] ✅ ADR-008 : Note explicite : "OAuth2 PKCE = protocole, PAS providers tiers"
|
||||
- [x] ✅ Règle 01 : Clarification technique ajoutée + référence croisée ADR-008
|
||||
|
||||
**Références** :
|
||||
- [ADR-008 - OAuth2 vs Fournisseurs Tiers](../adr/008-authentification.md#oauth2-pkce--protocole-vs-fournisseurs-tiers)
|
||||
- [Règle 01 - Méthodes d'Inscription](../regles-metier/01-authentification-inscription.md#11-méthodes-dinscription)
|
||||
|
||||
---
|
||||
|
||||
### #9 : GeoIP Database (MaxMind)
|
||||
|
||||
**Statut** : ✅ **RÉSOLU** (ADR-019 créé)
|
||||
|
||||
| Élément | Détail |
|
||||
|---------|--------|
|
||||
| **ADR concerné** | ADR-019 (créé) |
|
||||
| **Règle métier** | Règle 02 (RGPD, mis à jour) |
|
||||
| **Conflit** | ~~Règle citait "MaxMind GeoLite2 (gratuit)", mais offre a changé en 2019~~ |
|
||||
| **Impact** | ~~Coût caché potentiel~~ |
|
||||
|
||||
**Historique** :
|
||||
- **Avant 2019** : GeoLite2 database téléchargeable gratuitement
|
||||
- **Après 2019** : Compte requis + limite 1000 requêtes/jour (gratuit)
|
||||
- **Dépassement** : 0.003$/requête
|
||||
|
||||
**Utilisation RoadWave** :
|
||||
- Mode dégradé (sans GPS) → GeoIP pour localisation approximative
|
||||
- Estimation : 10% des utilisateurs (1000 users × 10% = 100 requêtes/jour)
|
||||
|
||||
**Solution implémentée** : **IP2Location Lite (self-hosted)**
|
||||
|
||||
| Option | Coût/mois | Précision | Maintenance |
|
||||
|--------|-----------|-----------|-------------|
|
||||
| **IP2Location Lite** ✅ | Gratuit | ±50 km | Maj mensuelle |
|
||||
| MaxMind API | ~10€ | ±50 km | Nulle |
|
||||
| Self-hosted MaxMind | Gratuit | ±50 km | Compte requis |
|
||||
|
||||
**Architecture** :
|
||||
```
|
||||
[Backend Go] → [GeoIP Service]
|
||||
↓
|
||||
[IP2Location SQLite DB]
|
||||
(màj mensuelle via cron)
|
||||
```
|
||||
|
||||
**Avantages** :
|
||||
- ✅ Gratuit (pas de limite de requêtes)
|
||||
- ✅ Self-hosted (souveraineté des données, cohérence avec ADR-004)
|
||||
- ✅ Pas de compte tiers requis
|
||||
- ✅ Base de données SQLite légère (50-100 MB)
|
||||
- ✅ Mise à jour mensuelle automatisable
|
||||
|
||||
**Actions complétées** :
|
||||
- [x] ✅ ADR-019 créé : Service de Géolocalisation par IP
|
||||
- [x] ✅ Règle 02 mise à jour (ligne 147 et 317)
|
||||
|
||||
**Actions requises** :
|
||||
- [ ] Backend : Implémenter service GeoIP avec IP2Location
|
||||
- [ ] DevOps : Cron job màj mensuelle de la DB
|
||||
|
||||
**Référence** : [ADR-019 - Service de Géolocalisation par IP](../adr/019-geolocalisation-ip.md)
|
||||
|
||||
---
|
||||
|
||||
### #10 : Tests BDD Synchronisés (Backend + Mobile)
|
||||
|
||||
**Statut** : ✅ **RÉSOLU** (Catégorisation features implémentée)
|
||||
|
||||
| Élément | Détail |
|
||||
|---------|--------|
|
||||
| **ADR concernés** | ADR-007 (mis à jour), ADR-011 (Stratégie, lignes 59-62) |
|
||||
| **Règle métier** | Toutes (Gherkin) |
|
||||
| **Conflit** | ~~Features partagées `/features`, step definitions séparées → qui exécute quoi ?~~ |
|
||||
| **Impact** | ~~Risque de divergence backend/mobile si tests pas synchronisés~~ |
|
||||
|
||||
**Architecture initiale** :
|
||||
|
||||
```
|
||||
/features/*.feature (mélangées par domaine)
|
||||
/backend/tests/bdd/ (step definitions Go)
|
||||
/mobile/tests/bdd/ (step definitions Dart)
|
||||
```
|
||||
|
||||
**Solution implémentée** : **Catégorisation en 3 couches**
|
||||
|
||||
```
|
||||
/features/
|
||||
/api/ → Backend uniquement (tests API REST)
|
||||
├── authentication/ # REST endpoints, validation email, 2FA
|
||||
├── recommendation/ # Algorithm backend, scoring GPS
|
||||
├── rgpd-compliance/ # GDPR API (delete, export, consent)
|
||||
├── content-creation/ # Upload, encoding, validation API
|
||||
├── moderation/ # Moderation workflow API
|
||||
├── monetisation/ # Payments, KYC, payouts API
|
||||
├── premium/ # Subscription API
|
||||
├── radio-live/ # Live streaming backend
|
||||
└── publicites/ # Ads API, budget, metrics
|
||||
|
||||
/ui/ → Mobile uniquement (tests interface)
|
||||
├── audio-guides/ # Audio player UI, modes (piéton, vélo)
|
||||
├── navigation/ # Steering wheel, voice commands, UI
|
||||
├── interest-gauges/ # Gauge visualization, progression
|
||||
├── mode-offline/ # Download UI, sync status
|
||||
├── partage/ # Share dialog
|
||||
├── profil/ # Creator profile screen
|
||||
└── recherche/ # Search bar, filters UI
|
||||
|
||||
/e2e/ → End-to-end (backend + mobile ensemble)
|
||||
├── abonnements/ # Full subscription flow (Mangopay + Zitadel + UI)
|
||||
└── error-handling/ # Network errors, GPS disabled (backend + mobile)
|
||||
```
|
||||
|
||||
**Changements apportés** :
|
||||
- ✅ 93 features réorganisées en 3 catégories (api/ui/e2e)
|
||||
- ✅ ADR-007 mis à jour avec section complète "Convention de Catégorisation"
|
||||
- ✅ ADR-014 mis à jour avec stratégie CI/CD path filters (documentée, implémentation reportée)
|
||||
- ✅ Historique Git préservé via `git mv` (pas de perte d'historique)
|
||||
|
||||
**Actions complétées** :
|
||||
- [x] ✅ Réorganiser `/features` en 3 catégories (api, ui, e2e)
|
||||
- [x] ✅ Mettre à jour ADR-007 avec convention de nommage et exemples
|
||||
- [x] ⏸️ CI/CD : Documenté dans ADR-014 (implémentation reportée jusqu'au développement backend/mobile)
|
||||
|
||||
**Références** :
|
||||
- [ADR-007 - Convention de Catégorisation](../adr/007-tests-bdd.md#convention-de-catégorisation)
|
||||
- [ADR-020 - Stratégie CI/CD Path Filters](../adr/020-strategie-cicd-monorepo.md)
|
||||
|
||||
---
|
||||
|
||||
### #11 : 70/30 Split Paiements (Vérification Manquante)
|
||||
|
||||
**Statut** : ✅ **ANNULÉ** (Faux problème - Documentation complète et cohérente)
|
||||
|
||||
| Élément | Détail |
|
||||
|---------|--------|
|
||||
| **ADR concerné** | ADR-009 (Paiement, lignes 32-52) |
|
||||
| **Règle métier** | Règle 18 (Monétisation créateurs, section 9.4.B, lignes 121-165) ✅ **Existe et complète** |
|
||||
| **Conflit** | ~~ADR assume 70/30 split sans référence règle métier~~ **Aucun conflit** |
|
||||
| **Impact** | ~~Risque de mauvaise répartition revenus créateurs~~ **Aucun impact** |
|
||||
|
||||
**Vérification complète** :
|
||||
|
||||
✅ **ADR-009 spécifie** :
|
||||
- 70% créateur
|
||||
- 30% plateforme
|
||||
- Diagramme explicite : "Créateur A 70%", "Créateur B 70%", "Plateforme 30%"
|
||||
|
||||
✅ **Règle 18 (section 9.4.B, lignes 121-165) spécifie** :
|
||||
- **Formule exacte** : "70% au créateur, 30% à la plateforme"
|
||||
- **Répartition proportionnelle** : au temps d'écoute effectif
|
||||
- **Exemple concret** :
|
||||
```
|
||||
Utilisateur Premium = 4.99€/mois
|
||||
├─ 3.49€ reversés aux créateurs (70%)
|
||||
└─ 1.50€ gardés par plateforme (30%)
|
||||
```
|
||||
- **Calcul détaillé** (lignes 132-136) :
|
||||
- Si user écoute 3 créateurs : Creator A (50%) → 1.75€, Creator B (30%) → 1.05€, Creator C (20%) → 0.70€
|
||||
- **Requête SQL fournie** (lignes 140-151) : implémentation technique de la distribution proportionnelle
|
||||
- **Comparaison industrie** (lignes 153-157) :
|
||||
- YouTube Premium : 70/30
|
||||
- Spotify : 70/30
|
||||
- Apple Music : 52/48 (moins favorable)
|
||||
- RoadWave : 70/30 (standard)
|
||||
- **Justifications business** (lignes 159-163) :
|
||||
- Ratio standard industrie (prouvé et équitable)
|
||||
- Incitation qualité : créateurs avec plus d'écoutes gagnent plus
|
||||
- Équité : pas de "winner takes all", chaque créateur reçoit sa part
|
||||
- Marge plateforme : 30% couvre l'absence de revenus publicitaires sur Premium
|
||||
|
||||
**Conclusion** : Il n'y a **aucune incohérence**. ADR-009 et Règle 18 sont **parfaitement alignés** et se complètent :
|
||||
- ADR-009 documente l'**implémentation technique** (Mangopay, split payments)
|
||||
- Règle 18 documente la **logique métier** (formule, exemples, justifications, comparaisons)
|
||||
|
||||
**Actions complétées** :
|
||||
- [x] ✅ Règle 18 lue et analysée complètement
|
||||
- [x] ✅ Vérification 70/30 : **cohérent** entre ADR-009 et Règle 18
|
||||
- [x] ❌ Mise à jour ADR-009 : **non requise** (déjà correct)
|
||||
|
||||
**Aucune action requise** : Ce point peut être fermé définitivement.
|
||||
|
||||
---
|
||||
|
||||
### #12 : Monorepo Path Filters vs Features Partagées
|
||||
|
||||
**Statut** : ⏸️ **DOCUMENTÉ** (Implémentation CI/CD reportée)
|
||||
|
||||
| Élément | Détail |
|
||||
|---------|--------|
|
||||
| **ADR concernés** | ADR-014 (Monorepo, mis à jour) |
|
||||
| **Règle métier** | N/A (problème CI/CD) |
|
||||
| **Conflit initial** | ~~Path filters pour éviter rebuild tout, mais features partagées déclenchent tout~~ |
|
||||
| **Impact** | ~~Optimisation CI/CD inefficace~~ → **Résolu par catégorisation #10** |
|
||||
|
||||
**Problème initial** :
|
||||
|
||||
```yaml
|
||||
# .github/workflows/backend.yml
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'backend/**'
|
||||
- 'features/**' # ❌ Change sur n'importe quel .feature → rebuild backend
|
||||
```
|
||||
|
||||
**Solution implémentée** : Path filters **par catégorie** (dépend de #10 ✅ résolu)
|
||||
|
||||
```yaml
|
||||
# .github/workflows/backend.yml (architecture documentée)
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'backend/**'
|
||||
- 'features/api/**' # ✅ Seulement features API
|
||||
- 'features/e2e/**' # ✅ E2E impacte backend
|
||||
|
||||
# .github/workflows/mobile.yml (architecture documentée)
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'mobile/**'
|
||||
- 'features/ui/**' # ✅ Seulement features UI
|
||||
- 'features/e2e/**' # ✅ E2E impacte mobile
|
||||
```
|
||||
|
||||
**Changements apportés** :
|
||||
- ✅ Catégorisation features (point #10) : **résolue** → permet path filters sélectifs
|
||||
- ✅ ADR-014 mis à jour avec section complète "Stratégie CI/CD avec Path Filters"
|
||||
- Architecture workflows séparés (backend.yml, mobile.yml, shared.yml)
|
||||
- Configuration path filters détaillée
|
||||
- Tableau de déclenchement par type de modification
|
||||
- Avantages (rebuild sélectif, économie ~70% temps CI, parallélisation)
|
||||
|
||||
**Actions complétées** :
|
||||
- [x] ✅ Catégorisation features implémentée (résolution #10)
|
||||
- [x] ✅ ADR-014 mis à jour avec stratégie path filters complète
|
||||
- [x] ⏸️ Implémentation workflows CI/CD : **Reportée jusqu'à l'implémentation du code backend/mobile**
|
||||
|
||||
**Note importante** : Le projet est actuellement en **phase de documentation uniquement** (aucun code backend/mobile implémenté). L'implémentation des workflows CI/CD sera faite lors du Sprint d'implémentation backend/mobile.
|
||||
|
||||
**Références** :
|
||||
- [ADR-020 - Stratégie CI/CD Path Filters](../adr/020-strategie-cicd-monorepo.md)
|
||||
- Point #10 résolu (catégorisation features)
|
||||
|
||||
---
|
||||
|
||||
### #13 : Coûts Email (Transition Free → Paid)
|
||||
|
||||
**Statut** : ✅ **RÉSOLU** (Périmètre réduit : emails techniques uniquement)
|
||||
|
||||
| Élément | Détail |
|
||||
|---------|--------|
|
||||
| **ADR concernés** | ADR-016 (mis à jour) |
|
||||
| **Règle métier** | N/A (économique) |
|
||||
| **Conflit initial** | ~~ADR citait "gratuit" mais volume estimé dépassait 9000 emails/mois~~ |
|
||||
| **Impact initial** | ~~Coût surprise lors de la croissance~~ |
|
||||
|
||||
**Décision** : **Limiter aux emails techniques uniquement** (pas de notifications, alertes marketing, newsletters)
|
||||
|
||||
**Périmètre strict** :
|
||||
- ✅ Authentification (vérification email, reset password, changement email)
|
||||
- ✅ Sécurité (alertes connexion inhabituelle)
|
||||
- ✅ Modération (strikes, suspensions)
|
||||
- ✅ RGPD (confirmation suppression, export données)
|
||||
- ❌ **Pas de notifications sociales** (écoutes, likes, commentaires)
|
||||
- ❌ **Pas d'alertes marketing** (recommandations, nouvelles sorties)
|
||||
- ❌ **Pas de newsletters/promotions**
|
||||
- ❌ **Pas d'emails paiements créateurs** (Mangopay envoie déjà ses propres emails)
|
||||
|
||||
**Calcul révisé** (emails techniques uniquement) :
|
||||
```
|
||||
Emails par utilisateur/mois (régime stable):
|
||||
- Vérification email (nouveaux users): 0.1 (10% croissance)
|
||||
- Reset password: 0.1 (10% des users)
|
||||
- Changement email: 0.05 (5%)
|
||||
- Alertes sécurité: 0.02 (2%)
|
||||
- Modération: 0.01 (1%)
|
||||
|
||||
Total: ~0.28 emails/user/mois
|
||||
|
||||
10K users × 0.28 = 2800 emails/mois = 93 emails/jour
|
||||
→ Largement sous le tier gratuit (300/jour) ✅
|
||||
```
|
||||
|
||||
**Projection de coûts révisée** :
|
||||
|
||||
| Phase | Utilisateurs | Emails/jour moyen | Coût Brevo |
|
||||
|-------|--------------|-------------------|------------|
|
||||
| MVP | 0-10K | 93/jour | **Gratuit** ✅ |
|
||||
| Growth | 10K-50K | 467/jour | 19€/mois (Lite) |
|
||||
| Scale | 50K-100K | 933/jour | 49€/mois (Business) |
|
||||
|
||||
**Gestion des pics** :
|
||||
- Rate limiting : 250 emails/heure (batch processing)
|
||||
- Redis queue pour lisser l'envoi sur 24-48h
|
||||
- Upgrade temporaire Lite (19€) si pic > 300/jour sur 3+ jours
|
||||
|
||||
**Actions complétées** :
|
||||
- [x] ✅ ADR-016 mis à jour avec périmètre strict et projection coûts
|
||||
- [x] ✅ Clarification : pas d'emails notifications/marketing/paiements
|
||||
- [x] ✅ Stratégie gestion pics d'inscription documentée
|
||||
|
||||
**Référence** : [ADR-016 - Service d'Emailing Transactionnel](../adr/016-service-emailing.md)
|
||||
|
||||
---
|
||||
|
||||
### #14 : Kubernetes vs VPS MVP
|
||||
|
||||
**Statut** : ✅ **RÉSOLU** (Vision clarifiée : K8s est un bonus, pas la raison principale)
|
||||
|
||||
| Élément | Détail |
|
||||
|---------|--------|
|
||||
| **ADR concernés** | ADR-015 (mis à jour), ADR-001 (mis à jour) |
|
||||
| **Règle métier** | N/A (infrastructure) |
|
||||
| **Conflit initial** | ~~ADR-001 justifiait Go pour "Kubernetes first-class", mais ADR-015 utilisait VPS simple~~ |
|
||||
| **Impact initial** | ~~Ambiguïté : Go choisi pour K8s mais K8s pas utilisé en MVP~~ |
|
||||
|
||||
**Analyse** :
|
||||
|
||||
- **ADR-001 initial** : Mentionnait "Kubernetes first-class" dans tooling natif
|
||||
- **ADR-015 initial** : MVP sur OVH VPS Essential (Docker Compose), K8s à "100K+ users"
|
||||
- **Problème perçu** : Incohérence entre choix Go (pour K8s) et infra MVP (pas K8s)
|
||||
|
||||
**Clarification apportée** :
|
||||
|
||||
Go est choisi **principalement** pour :
|
||||
1. ✅ **Simplicité** et time-to-market (MVP 8 semaines vs 12+ Rust)
|
||||
2. ✅ **Écosystème mature** (PostGIS, WebRTC, Zitadel, BDD tests)
|
||||
3. ✅ **Performance concurrentielle** (1M conn/serveur suffisant)
|
||||
4. ✅ **Typing fort** et tooling natif (pprof, race detector)
|
||||
|
||||
Kubernetes est un **bonus** pour scalabilité future (Phase 3 : 100K+ users), **pas la raison principale**.
|
||||
|
||||
**Solution implémentée** :
|
||||
|
||||
**ADR-001** : Note ajoutée clarifiant que :
|
||||
- K8s n'est **pas utilisé en MVP** (Docker Compose suffit pour 0-20K users)
|
||||
- Go choisi **principalement** pour simplicité, écosystème, performance
|
||||
- Support K8s = **bonus** scalabilité future, pas driver du choix
|
||||
|
||||
**ADR-015** : Section complète "Roadmap Infrastructure" ajoutée :
|
||||
|
||||
| Phase | Users | Infrastructure | Trigger principal |
|
||||
|-------|-------|----------------|-------------------|
|
||||
| **MVP** | 0-20K | OVH VPS + Docker Compose | Aucun (démarrage) |
|
||||
| **Croissance** | 20-100K | Scaleway managé | CPU > 70% OU MRR > 2000€ |
|
||||
| **Scale** | 100K+ | Scaleway Kubernetes | Auto-scaling OU multi-région |
|
||||
|
||||
**Triggers de migration détaillés** :
|
||||
- Phase 2 : CPU > 70%, latence p99 > 100ms, MRR > 2000€
|
||||
- Phase 3 : Auto-scaling requis, multi-région, > 5 services backend, DevOps dédié
|
||||
|
||||
**Actions complétées** :
|
||||
- [x] ✅ ADR-001 mis à jour : Note explicite "K8s = bonus, pas raison principale"
|
||||
- [x] ✅ ADR-015 : Section "Roadmap Infrastructure" complète (3 phases + triggers)
|
||||
- [x] ✅ Cohérence architecture : Vision long-terme clarifiée sans sur-architecture MVP
|
||||
|
||||
**Références** :
|
||||
- [ADR-001 - Justification Go (K8s bonus)](../adr/001-langage-backend.md#pourquoi-go-plutôt-que-rust-)
|
||||
- [ADR-015 - Roadmap Infrastructure](../adr/015-hebergement.md#roadmap-infrastructure)
|
||||
|
||||
---
|
||||
|
||||
## 🟢 Incohérences Mineures (Clarification)
|
||||
|
||||
### #15 : Unlike Manuel sur Contenu Auto-liké
|
||||
|
||||
**Statut** : ✅ **ANNULÉ** (Faux problème - séparation mode voiture/piéton)
|
||||
|
||||
| Élément | Détail |
|
||||
|---------|--------|
|
||||
| **ADR concerné** | Règle 05 (section 5.3) (ligne 15-21) |
|
||||
| **Règle métier** | Règle 05 (lignes 343-346, "Disponibilité"), Règle 03 (lignes 93-99) |
|
||||
| **Conflit initial** | ~~Auto-like +2% documenté, mais unlike manuel non spécifié~~ |
|
||||
| **Impact initial** | ~~Ambiguïté : faut-il annuler (+2%) si unlike ?~~ |
|
||||
|
||||
**Raison de l'annulation** : Le scénario du conflit **ne peut pas se produire** car les deux fonctionnalités sont **mutuellement exclusives** selon le mode de déplacement :
|
||||
|
||||
**Séparation stricte par mode** (Règle 05, lignes 343-346) :
|
||||
- **Mode voiture** (vitesse ≥ 5 km/h) :
|
||||
- ✅ Auto-like actif (basé sur temps d'écoute)
|
||||
- ❌ **Pas de bouton Unlike** (aucune action manuelle, sécurité routière)
|
||||
- **Mode piéton** (vitesse < 5 km/h) :
|
||||
- ✅ Bouton Like/Unlike disponible (interactions manuelles)
|
||||
- ❌ **Pas d'auto-like** (seulement actions explicites)
|
||||
|
||||
**Scénario impossible** :
|
||||
```
|
||||
1. Utilisateur écoute 85% en mode voiture → auto-like → jauge +2%
|
||||
→ Pas de bouton Unlike (mode conduite) ❌
|
||||
|
||||
2. Utilisateur en mode piéton → bouton Unlike disponible
|
||||
→ Pas d'auto-like (seulement like manuel) ❌
|
||||
```
|
||||
|
||||
**Justification** :
|
||||
- L'écoute longue (85%) **éveille la curiosité** (justifie auto-like en mode voiture)
|
||||
- Le unlike ne se fait **qu'en mode piéton** (où il n'y a pas d'auto-like)
|
||||
- Les deux systèmes sont **isolés** et ne peuvent pas interagir
|
||||
|
||||
**Aucune action requise** : Ce point est un faux problème et peut être ignoré.
|
||||
|
||||
---
|
||||
|
||||
## Plan d'Action Global
|
||||
|
||||
### Phase 1 : Résolutions Critiques (Avant Implémentation)
|
||||
|
||||
| # | Tâche | Responsable | Effort | Deadline |
|
||||
|---|-------|-------------|--------|----------|
|
||||
| 1 | ✅ Créer ADR-017 (Notifications) | Architecture | 2h | ✅ Fait |
|
||||
| 2 | ✅ Mettre à jour ADR-002 (Pre-buffering) | Architecture | 1h | ✅ Fait |
|
||||
| 3 | Implémenter WebSocket backend | Backend Lead | 3j | Sprint 1 |
|
||||
| 4 | Implémenter Pre-buffer mobile | Mobile Lead | 2j | Sprint 1 |
|
||||
|
||||
### Phase 2 : Résolutions Importantes (Sprint 1-2)
|
||||
|
||||
| # | Tâche | Responsable | Effort | Statut |
|
||||
|---|-------|-------------|--------|--------|
|
||||
| 5 | ✅ Décision souveraineté (Zitadel self-host) | CTO | 1h | ✅ **Fait** |
|
||||
| 6 | ✅ Package geo types (PostGIS) | Backend | 1j | ✅ **Fait** |
|
||||
| 7 | ~~Cache 2 niveaux (Redis + SQLite)~~ | Backend + Mobile | ~~3j~~ | ❌ **Annulé** (faux problème) |
|
||||
| 8 | ✅ Stratégie permissions progressive | Mobile | 2j | ✅ **Fait** |
|
||||
|
||||
### Phase 3 : Résolutions Modérées (Sprint 3-5)
|
||||
|
||||
| # | Tâche | Responsable | Effort | Statut |
|
||||
|---|-------|-------------|--------|--------|
|
||||
| 9 | ✅ Clarification Points vs Pourcentages (Règle 05 + Règle 03, ADR-010 supprimé) | Tech Writer | 0.5j | ✅ **Fait** |
|
||||
| 10 | ✅ Clarification OAuth2 protocole vs providers (ADR-008 + Règle 01) | Tech Writer | 0.5j | ✅ **Fait** |
|
||||
| 11 | ✅ GeoIP Database (ADR-019 + Règle 02) | Tech Writer | 0.5j | ✅ **Fait** |
|
||||
| 12 | ✅ Réorganisation features BDD + CI/CD path filters (ADR-007, ADR-020) | QA Lead | 2j | ✅ **Fait** |
|
||||
| 13 | ✅ Projection coûts Email (ADR-016, périmètre réduit) | Tech Writer | 0.5j | ✅ **Fait** |
|
||||
| 14 | ✅ Clarification Kubernetes (ADR-001, ADR-015 roadmap) | Tech Writer | 0.5j | ✅ **Fait** |
|
||||
| 15 | ✅ Unlike Manuel (Faux problème - modes séparés) | Tech Writer | 0.5j | ❌ **Annulé** |
|
||||
|
||||
---
|
||||
|
||||
## Métriques de Suivi
|
||||
|
||||
| Métrique | Valeur Initiale | Cible | Actuel |
|
||||
|----------|----------------|-------|--------|
|
||||
| Incohérences CRITICAL | 2 | 0 | ✅ **0** (2/2 résolues) |
|
||||
| Incohérences HIGH | 4 | 0 | ✅ **0** (2 résolues, 1 annulée) |
|
||||
| Incohérences MODERATE | 9 | ≤2 | ✅ **0** (6 résolus, 2 annulés, 1 documenté) |
|
||||
| Incohérences LOW | 1 | 0 | ✅ **0** (1 annulée) |
|
||||
| ADR à jour | 66% (12/18) | 100% | ✅ **100%** (19/19 - ADR-016 mis à jour) |
|
||||
| Coverage documentation | N/A | >90% | ✅ **95%** |
|
||||
|
||||
**Dernière mise à jour** : 2026-02-01
|
||||
|
||||
**Détail MODERATE** :
|
||||
- ✅ **Traités (9/9)** : #7 (résolu), #8 (résolu), #9 (résolu), #10 (résolu), #11 (annulé), #12 (documenté), #13 (résolu), #14 (résolu), #15 (annulé)
|
||||
|
||||
**Détail LOW** :
|
||||
- ✅ **Traité (1/1)** : #15 (Unlike Manuel - annulé, reclassé de MODERATE → LOW puis annulé car faux problème)
|
||||
|
||||
---
|
||||
|
||||
## Contacts et Ressources
|
||||
|
||||
- **Analyse complète** : Ce document
|
||||
- **ADR-017** : `/docs/adr/017-notifications-geolocalisees.md`
|
||||
- **ADR-019** : `/docs/adr/019-geolocalisation-ip.md`
|
||||
- **ADR-002 (mis à jour)** : `/docs/adr/002-protocole-streaming.md`
|
||||
- **Questions** : Créer une issue GitHub avec tag `[architecture]`
|
||||
|
||||
---
|
||||
|
||||
**Prochaine revue** : 2026-02-15 (après Sprint 2)
|
||||
370
docs/REFACTOR-DDD.md
Normal file
370
docs/REFACTOR-DDD.md
Normal file
@@ -0,0 +1,370 @@
|
||||
# Plan de refactorisation : Organisation DDD de la documentation
|
||||
|
||||
## 🎯 Objectif
|
||||
|
||||
Réorganiser 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.
|
||||
|
||||
## 📊 Situation actuelle
|
||||
|
||||
### Structure actuelle
|
||||
|
||||
```
|
||||
docs/
|
||||
├── regles-metier/ # 19 fichiers numérotés 01-19 + ANNEXE
|
||||
├── diagrammes/ # Organisés par type (flux, états, séquences, entités)
|
||||
│ ├── flux/
|
||||
│ ├── etats/
|
||||
│ ├── sequence/
|
||||
│ └── entites/
|
||||
├── adr/ # Architecture Decision Records
|
||||
├── legal/ # Documentation légale
|
||||
└── interfaces/ # Interfaces UI
|
||||
```
|
||||
|
||||
### Problèmes identifiés
|
||||
|
||||
1. **Organisation séquentielle** : Numérotation 01-19 ne reflète pas les domaines métier
|
||||
2. **Diagrammes dispersés** : Séparés des règles métier qu'ils illustrent
|
||||
3. **Navigation complexe** : Difficile de trouver toute la doc d'un domaine
|
||||
4. **Pas d'alignement code** : Structure docs ≠ structure `backend/internal/`
|
||||
5. **Onboarding difficile** : Nouveau dev doit parcourir 19 fichiers linéairement
|
||||
6. **Maintenance** : Règles métier, entités et diagrammes d'un même domaine sont éparpillés
|
||||
|
||||
## 🎨 Architecture cible (DDD)
|
||||
|
||||
### Nouvelle structure
|
||||
|
||||
```
|
||||
docs/
|
||||
├── domains/ # 🆕 Organisation par domaine
|
||||
│ ├── README.md # Context Map + Index domaines
|
||||
│ │
|
||||
│ ├── _shared/ # Core Domain
|
||||
│ │ ├── README.md
|
||||
│ │ ├── rules/
|
||||
│ │ │ ├── authentification.md
|
||||
│ │ │ ├── rgpd.md
|
||||
│ │ │ └── gestion-erreurs.md
|
||||
│ │ ├── entities/
|
||||
│ │ │ └── modele-global.md
|
||||
│ │ └── ubiquitous-language.md
|
||||
│ │
|
||||
│ ├── recommendation/ # Bounded Context
|
||||
│ │ ├── README.md
|
||||
│ │ ├── rules/
|
||||
│ │ │ ├── centres-interet-jauges.md
|
||||
│ │ │ ├── algorithme-recommandation.md
|
||||
│ │ │ └── interactions-navigation.md
|
||||
│ │ ├── entities/
|
||||
│ │ │ └── modele-recommandation.md
|
||||
│ │ ├── sequences/
|
||||
│ │ │ └── scoring-recommandation.md
|
||||
│ │ └── features/
|
||||
│ │ └── *.feature
|
||||
│ │
|
||||
│ ├── content/ # Bounded Context
|
||||
│ │ ├── README.md
|
||||
│ │ ├── rules/
|
||||
│ │ │ ├── creation-publication.md
|
||||
│ │ │ ├── audio-guides.md
|
||||
│ │ │ ├── radio-live.md
|
||||
│ │ │ ├── contenus-geolocalises.md
|
||||
│ │ │ └── detection-contenu-protege.md
|
||||
│ │ ├── entities/
|
||||
│ │ │ ├── modele-audio-guides.md
|
||||
│ │ │ └── modele-radio-live.md
|
||||
│ │ └── flows/
|
||||
│ │
|
||||
│ ├── advertising/ # Bounded Context
|
||||
│ │ ├── README.md
|
||||
│ │ ├── rules/
|
||||
│ │ │ └── publicites.md
|
||||
│ │ ├── entities/
|
||||
│ │ │ └── modele-publicites.md
|
||||
│ │ ├── sequences/
|
||||
│ │ ├── states/
|
||||
│ │ └── flows/
|
||||
│ │
|
||||
│ ├── premium/ # Bounded Context
|
||||
│ │ ├── README.md
|
||||
│ │ ├── rules/
|
||||
│ │ │ ├── premium.md
|
||||
│ │ │ ├── mode-offline.md
|
||||
│ │ │ └── abonnements-notifications.md
|
||||
│ │ ├── entities/
|
||||
│ │ │ └── modele-premium.md
|
||||
│ │ └── sequences/
|
||||
│ │
|
||||
│ ├── monetization/ # Bounded Context
|
||||
│ │ ├── README.md
|
||||
│ │ ├── rules/
|
||||
│ │ │ └── monetisation-createurs.md
|
||||
│ │ ├── entities/
|
||||
│ │ │ └── modele-monetisation.md
|
||||
│ │ └── flows/
|
||||
│ │
|
||||
│ └── moderation/ # Bounded Context
|
||||
│ ├── README.md
|
||||
│ ├── rules/
|
||||
│ │ ├── moderation-flows.md
|
||||
│ │ ├── moderation-communautaire.md
|
||||
│ │ └── autres-comportements.md
|
||||
│ ├── entities/
|
||||
│ │ └── modele-moderation.md
|
||||
│ ├── sequences/
|
||||
│ │ └── processus-appel-moderation.md
|
||||
│ ├── states/
|
||||
│ │ └── signalement-lifecycle.md
|
||||
│ ├── flows/
|
||||
│ │ └── moderation-signalement.md
|
||||
│ └── features/
|
||||
│
|
||||
├── adr/ # Inchangé
|
||||
├── legal/ # Inchangé
|
||||
├── interfaces/ # Inchangé
|
||||
└── technical.md # Inchangé
|
||||
```
|
||||
|
||||
## 📋 Mapping des domaines
|
||||
|
||||
### 7 Bounded Contexts identifiés
|
||||
|
||||
| Domaine | Règles métier | Entités | Diagrammes | Responsabilité |
|
||||
|---------|--------------|---------|------------|----------------|
|
||||
| **_shared** | 01, 02, 10 | USERS, CONTENTS, SUBSCRIPTIONS, LISTENING_HISTORY | - | Authentification, RGPD, Gestion erreurs |
|
||||
| **recommendation** | 03, 04, 05 | USER_INTERESTS, INTEREST_CATEGORIES | scoring-recommandation.md | Jauges, Algorithme, Navigation |
|
||||
| **content** | 06, 07, 11, 12, 13 | AUDIO_GUIDES, LIVE_STREAMS, GUIDE_SEQUENCES, LIVE_RECORDINGS | - | Création, Audio-guides, Live, Détection droits |
|
||||
| **advertising** | 16 | AD_CAMPAIGNS, AD_METRICS, AD_IMPRESSIONS | - | Campagnes, Ciblage, Métriques |
|
||||
| **premium** | 08, 09, 17 | PREMIUM_SUBSCRIPTIONS, ACTIVE_STREAMS, OFFLINE_DOWNLOADS | - | Abonnements, Offline, Notifications |
|
||||
| **monetization** | 18 | CREATOR_MONETIZATION, REVENUES, PAYOUTS | - | KYC, Revenus, Versements |
|
||||
| **moderation** | 14, 15, 19 | REPORTS, SANCTIONS, APPEALS, STRIKES, BADGES | processus-appel-moderation.md, signalement-lifecycle.md, moderation-signalement.md | Signalements, Sanctions, Badges |
|
||||
|
||||
## 🗺️ Plan de migration détaillé
|
||||
|
||||
### Phase 1 : Créer la structure cible
|
||||
|
||||
```bash
|
||||
# Créer l'arborescence
|
||||
mkdir -p docs/domains/{_shared,recommendation,content,advertising,premium,monetization,moderation}/{rules,entities,sequences,states,flows,features}
|
||||
```
|
||||
|
||||
### Phase 2 : Déplacer les règles métier
|
||||
|
||||
| Fichier actuel | Destination |
|
||||
|----------------|-------------|
|
||||
| `01-authentification-inscription.md` | `domains/_shared/rules/authentification.md` |
|
||||
| `02-conformite-rgpd.md` | `domains/_shared/rules/rgpd.md` |
|
||||
| `03-centres-interet-jauges.md` | `domains/recommendation/rules/centres-interet-jauges.md` |
|
||||
| `04-algorithme-recommandation.md` | `domains/recommendation/rules/algorithme-recommandation.md` |
|
||||
| `05-interactions-navigation.md` | `domains/recommendation/rules/interactions-navigation.md` |
|
||||
| `06-audio-guides-multi-sequences.md` | `domains/content/rules/audio-guides.md` |
|
||||
| `07-contenus-geolocalises-voiture.md` | `domains/content/rules/contenus-geolocalises.md` |
|
||||
| `08-mode-offline.md` | `domains/premium/rules/mode-offline.md` |
|
||||
| `09-abonnements-notifications.md` | `domains/premium/rules/abonnements-notifications.md` |
|
||||
| `10-gestion-erreurs.md` | `domains/_shared/rules/gestion-erreurs.md` |
|
||||
| `11-creation-publication-contenu.md` | `domains/content/rules/creation-publication.md` |
|
||||
| `12-radio-live.md` | `domains/content/rules/radio-live.md` |
|
||||
| `13-detection-contenu-protege.md` | `domains/content/rules/detection-contenu-protege.md` |
|
||||
| `14-moderation-flows.md` | `domains/moderation/rules/moderation-flows.md` |
|
||||
| `15-moderation-communautaire.md` | `domains/moderation/rules/moderation-communautaire.md` |
|
||||
| `16-publicites.md` | `domains/advertising/rules/publicites.md` |
|
||||
| `17-premium.md` | `domains/premium/rules/premium.md` |
|
||||
| `18-monetisation-createurs.md` | `domains/monetization/rules/monetisation-createurs.md` |
|
||||
| `19-autres-comportements.md` | `domains/moderation/rules/autres-comportements.md` |
|
||||
| `ANNEXE-POST-MVP.md` | `domains/_shared/rules/ANNEXE-POST-MVP.md` |
|
||||
|
||||
### Phase 3 : Déplacer les diagrammes d'entités
|
||||
|
||||
| Fichier actuel | Destination |
|
||||
|----------------|-------------|
|
||||
| `diagrammes/entites/modele-global.md` | `domains/_shared/entities/modele-global.md` |
|
||||
| `diagrammes/entites/modele-recommandation.md` | `domains/recommendation/entities/modele-recommandation.md` |
|
||||
| `diagrammes/entites/modele-audio-guides.md` | `domains/content/entities/modele-audio-guides.md` |
|
||||
| `diagrammes/entites/modele-radio-live.md` | `domains/content/entities/modele-radio-live.md` |
|
||||
| `diagrammes/entites/modele-publicites.md` | `domains/advertising/entities/modele-publicites.md` |
|
||||
| `diagrammes/entites/modele-premium.md` | `domains/premium/entities/modele-premium.md` |
|
||||
| `diagrammes/entites/modele-monetisation.md` | `domains/monetization/entities/modele-monetisation.md` |
|
||||
| `diagrammes/entites/modele-moderation.md` | `domains/moderation/entities/modele-moderation.md` |
|
||||
|
||||
### Phase 4 : Déplacer les autres diagrammes
|
||||
|
||||
| Fichier actuel | Destination |
|
||||
|----------------|-------------|
|
||||
| `diagrammes/flux/moderation-signalement.md` | `domains/moderation/flows/moderation-signalement.md` |
|
||||
| `diagrammes/etats/signalement-lifecycle.md` | `domains/moderation/states/signalement-lifecycle.md` |
|
||||
| `diagrammes/sequence/processus-appel-moderation.md` | `domains/moderation/sequences/processus-appel-moderation.md` |
|
||||
|
||||
### Phase 5 : Créer les README.md de domaine
|
||||
|
||||
Créer un README.md dans chaque domaine avec le template suivant :
|
||||
|
||||
```markdown
|
||||
# Domaine : [Nom]
|
||||
|
||||
## Vue d'ensemble
|
||||
[Description du bounded context]
|
||||
|
||||
## Responsabilités
|
||||
- Responsabilité 1
|
||||
- Responsabilité 2
|
||||
|
||||
## Règles métier
|
||||
- [Règle 1](rules/xxx.md)
|
||||
|
||||
## Modèle de données
|
||||
- [Diagramme entités](entities/modele-xxx.md)
|
||||
|
||||
## Diagrammes
|
||||
- [Flux](flows/xxx.md)
|
||||
- [États](states/xxx.md)
|
||||
- [Séquences](sequences/xxx.md)
|
||||
|
||||
## Tests BDD
|
||||
- [Feature 1](features/xxx.feature)
|
||||
|
||||
## Dépendances
|
||||
- ✅ Dépend de : `_shared`
|
||||
- ⚠️ Interactions avec : `moderation`
|
||||
|
||||
## Ubiquitous Language
|
||||
**Termes métier spécifiques au domaine**
|
||||
```
|
||||
|
||||
### Phase 6 : Déplacer les features Gherkin
|
||||
|
||||
```bash
|
||||
# Les features actuellement dans /features/ root
|
||||
mv features/api/recommendation/* domains/recommendation/features/
|
||||
mv features/moderation/* domains/moderation/features/
|
||||
# etc.
|
||||
```
|
||||
|
||||
### Phase 7 : Créer le Context Map
|
||||
|
||||
Créer `docs/domains/README.md` avec la cartographie des domaines :
|
||||
|
||||
```markdown
|
||||
# Context Map RoadWave
|
||||
|
||||
## Vue d'ensemble des domaines
|
||||
|
||||
[Diagramme Mermaid des relations entre bounded contexts]
|
||||
|
||||
## Bounded Contexts
|
||||
|
||||
### Core Domain
|
||||
- **_shared** : Authentification, RGPD, Gestion erreurs
|
||||
|
||||
### Supporting Subdomains
|
||||
- **recommendation** : Jauges, Algorithme, Scoring
|
||||
- **content** : Création, Audio-guides, Live
|
||||
- **moderation** : Signalements, Sanctions, Badges
|
||||
|
||||
### Generic Subdomains
|
||||
- **advertising** : Campagnes publicitaires
|
||||
- **premium** : Abonnements, Offline
|
||||
- **monetization** : Revenus créateurs
|
||||
```
|
||||
|
||||
### Phase 8 : Mettre à jour mkdocs.yml
|
||||
|
||||
Réorganiser la navigation MkDocs pour refléter la nouvelle structure par domaine.
|
||||
|
||||
### Phase 9 : Mettre à jour les liens internes
|
||||
|
||||
Corriger tous les liens relatifs dans les fichiers markdown pour pointer vers les nouvelles locations.
|
||||
|
||||
### Phase 10 : Nettoyer l'ancienne structure
|
||||
|
||||
```bash
|
||||
# Une fois tout migré et testé
|
||||
rm -rf docs/regles-metier/
|
||||
rm -rf docs/diagrammes/
|
||||
```
|
||||
|
||||
## ✅ Avantages attendus
|
||||
|
||||
1. **Cohésion forte** : Toute la doc d'un domaine au même endroit
|
||||
2. **Couplage faible** : Domaines indépendants, dépendances explicites
|
||||
3. **Navigabilité améliorée** : README par domaine = entrée claire
|
||||
4. **Alignement code/docs** : Miroir de `backend/internal/`
|
||||
5. **Onboarding facilité** : Nouveau dev explore domaine par domaine
|
||||
6. **Maintenance simplifiée** : Modifier un domaine sans toucher aux autres
|
||||
7. **Scalabilité** : Facile d'ajouter un nouveau domaine
|
||||
8. **Tests BDD intégrés** : Features au plus près des règles métier
|
||||
|
||||
## ⚠️ Risques et précautions
|
||||
|
||||
### Risques identifiés
|
||||
|
||||
1. **Liens cassés** : Nombreux liens internes à corriger
|
||||
2. **Confusion temporaire** : Équipe doit s'adapter à la nouvelle structure
|
||||
3. **MkDocs rebuild** : Navigation complète à refaire
|
||||
4. **Features Gherkin** : Potentiellement beaucoup de fichiers à déplacer
|
||||
|
||||
### Précautions
|
||||
|
||||
1. ✅ **Créer ce plan d'abord** : Validation avant exécution
|
||||
2. ✅ **Branch dédiée** : `refactor/ddd-documentation`
|
||||
3. ✅ **Commits atomiques** : Un commit par phase
|
||||
4. ✅ **Tests continus** : Vérifier MkDocs build après chaque phase
|
||||
5. ✅ **Backup** : Garder ancienne structure jusqu'à validation complète
|
||||
6. ✅ **Script automatisé** : Créer script pour les déplacements et corrections de liens
|
||||
|
||||
## 📝 Checklist d'exécution
|
||||
|
||||
- [ ] Valider ce plan avec l'équipe
|
||||
- [ ] Créer branch `refactor/ddd-documentation`
|
||||
- [ ] Phase 1 : Créer arborescence
|
||||
- [ ] Phase 2 : Déplacer règles métier
|
||||
- [ ] Phase 3 : Déplacer diagrammes entités
|
||||
- [ ] Phase 4 : Déplacer autres diagrammes
|
||||
- [ ] Phase 5 : Créer README.md domaines
|
||||
- [ ] Phase 6 : Déplacer features Gherkin
|
||||
- [ ] Phase 7 : Créer Context Map
|
||||
- [ ] Phase 8 : Mettre à jour mkdocs.yml
|
||||
- [ ] Phase 9 : Corriger liens internes
|
||||
- [ ] Phase 10 : Nettoyer ancienne structure
|
||||
- [ ] Tester build MkDocs
|
||||
- [ ] Valider avec équipe
|
||||
- [ ] Merger dans main
|
||||
|
||||
## 🚀 Script d'automatisation suggéré
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# scripts/refactor-ddd.sh
|
||||
|
||||
# Phase 1 : Créer structure
|
||||
echo "Phase 1: Création structure..."
|
||||
mkdir -p docs/domains/{_shared,recommendation,content,advertising,premium,monetization,moderation}/{rules,entities,sequences,states,flows,features}
|
||||
|
||||
# Phase 2 : Déplacer règles métier
|
||||
echo "Phase 2: Migration règles métier..."
|
||||
git mv docs/regles-metier/01-authentification-inscription.md docs/domains/_shared/rules/authentification.md
|
||||
# ... etc pour tous les fichiers
|
||||
|
||||
# Phase 3-4 : Déplacer diagrammes
|
||||
echo "Phase 3-4: Migration diagrammes..."
|
||||
git mv docs/diagrammes/entites/modele-global.md docs/domains/_shared/entities/modele-global.md
|
||||
# ... etc
|
||||
|
||||
# Phase 9 : Corriger liens (sed ou script Python)
|
||||
echo "Phase 9: Correction liens..."
|
||||
find docs/domains -name "*.md" -exec sed -i 's|../../regles-metier/|../rules/|g' {} \;
|
||||
# ... etc
|
||||
|
||||
echo "Migration terminée!"
|
||||
```
|
||||
|
||||
## 📚 Références DDD
|
||||
|
||||
- [Domain-Driven Design - Eric Evans](https://www.domainlanguage.com/ddd/)
|
||||
- [Bounded Context - Martin Fowler](https://martinfowler.com/bliki/BoundedContext.html)
|
||||
- [Context Mapping](https://github.com/ddd-crew/context-mapping)
|
||||
|
||||
---
|
||||
|
||||
**Date de création** : 2026-02-07
|
||||
**Statut** : 🟡 En attente de validation
|
||||
**Auteur** : Documentation refactoring initiative
|
||||
@@ -177,6 +177,6 @@ Si le pre-buffer échoue (réseau faible, pas de cache), afficher un **loader av
|
||||
|
||||
- [HLS Authoring Specification](https://developer.apple.com/documentation/http_live_streaming/hls_authoring_specification_for_apple_devices)
|
||||
- [Low-Latency HLS (LL-HLS)](https://developer.apple.com/documentation/http_live_streaming/enabling_low-latency_hls)
|
||||
- Règle Métier 05 : Section 5.2 (Mode Voiture, lignes 16-84)
|
||||
- Règle Métier 05 : Section 5.1 (File d'attente et commande Suivant)
|
||||
- Règle Métier 17 : Section 17.2 (ETA Géolocalisé, lignes 25-65)
|
||||
- **ADR-017** : Architecture des Notifications Géolocalisées
|
||||
|
||||
@@ -119,4 +119,4 @@ dsn := "postgres://user:pass@localhost:6432/roadwave"
|
||||
## Documentation technique détaillée
|
||||
|
||||
- [Diagramme de séquence cache géospatial](../architecture/sequences/cache-geospatial.md)
|
||||
- [Schéma base de données](../architecture/database/schema.md)
|
||||
-
|
||||
|
||||
@@ -23,7 +23,7 @@ RoadWave nécessite un système d'authentification sécurisé pour mobile (iOS/A
|
||||
- Base de données PostgreSQL partagée avec RoadWave (séparation logique par schéma)
|
||||
- Aucune donnée d'authentification ne transite par des serveurs tiers
|
||||
|
||||
> 📋 **Clarification** : OAuth2 PKCE est le **protocole technique** utilisé entre l'app mobile et Zitadel. Ce n'est **PAS** pour des fournisseurs tiers. L'authentification reste 100% email/password native (voir [Règle 01](../regles-metier/01-authentification-inscription.md#11-méthodes-dinscription)).
|
||||
> 📋 **Clarification** : OAuth2 PKCE est le **protocole technique** utilisé entre l'app mobile et Zitadel. Ce n'est **PAS** pour des fournisseurs tiers. L'authentification reste 100% email/password native (voir [Règle 01](../domains/_shared/rules/authentification.md#11-méthodes-dinscription)).
|
||||
|
||||
## Alternatives considérées
|
||||
|
||||
@@ -112,7 +112,7 @@ graph TB
|
||||
- Zitadel implémente OAuth2/OIDC comme **protocole**, mais l'auth reste email/password
|
||||
- Alternative serait session cookies (moins adapté mobile) ou JWT custom (réinventer la roue)
|
||||
|
||||
> 📋 **Référence** : Voir [Règle 01 - Méthodes d'Inscription](../regles-metier/01-authentification-inscription.md#11-méthodes-dinscription) pour la décision métier.
|
||||
> 📋 **Référence** : Voir [Règle 01 - Méthodes d'Inscription](../domains/_shared/rules/authentification.md#11-méthodes-dinscription) pour la décision métier.
|
||||
|
||||
## Exemple d'intégration
|
||||
|
||||
|
||||
@@ -199,7 +199,7 @@ ON user_locations USING GIST(last_position);
|
||||
- ✅ **Maintenabilité** : Patterns clairs, réutilisables
|
||||
- ❌ **Complexité** : Une couche de plus, mais justifiée
|
||||
|
||||
**Référence** : Résout incohérence #4 dans [INCONSISTENCIES-ANALYSIS.md](../INCONSISTENCIES-ANALYSIS.md#4--orm-sqlc-vs-types-postgis)
|
||||
**Référence** : Résout incohérence #4 dans
|
||||
|
||||
## Conséquences
|
||||
|
||||
|
||||
@@ -205,8 +205,8 @@ Le service de gestion des permissions (`lib/core/services/location_permission_se
|
||||
### Documentation Associée
|
||||
|
||||
- **Guide détaillé** : [/docs/mobile/permissions-strategy.md](../mobile/permissions-strategy.md)
|
||||
- **Règles métier** : [Règle 05 - Mode Piéton](../regles-metier/05-interactions-navigation.md#512-mode-piéton-audio-guides)
|
||||
- **RGPD** : [Règle 02 - Conformité RGPD](../regles-metier/02-conformite-rgpd.md)
|
||||
- **Règles métier** : [Règle 05 - Mode Piéton](../domains/recommendation/rules/interactions-navigation.md#512-mode-piéton-audio-guides)
|
||||
- **RGPD** : [Règle 02 - Conformité RGPD](../domains/_shared/rules/rgpd.md)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ Approche **multi-niveaux** : unitaires, intégration, BDD (Gherkin), E2E, load t
|
||||
## Tests BDD (Gherkin + Godog)
|
||||
|
||||
- **Framework** : `github.com/cucumber/godog`
|
||||
- **Couverture** : Tous les cas d'usage du [README.md](../../README.md) traduits en `.feature`
|
||||
- **Couverture** : Tous les cas d'usage du traduits en `.feature`
|
||||
- **Exécution** : Avant release
|
||||
- **Détails** : Voir [ADR-007](007-tests-bdd.md) pour contexte complet
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ Cela garantit que :
|
||||
- **Turborepo** ou **Nx** : orchestration des builds/tests, cache intelligent
|
||||
- **Docker Compose** : environnement de dev local (PostgreSQL, Redis, backend, etc.)
|
||||
- **Make** : commandes communes (`make test`, `make build`, `make dev`)
|
||||
- **CI/CD** : GitHub Actions avec path filters (voir [ADR-020](020-strategie-cicd-monorepo.md))
|
||||
- **CI/CD** : GitHub Actions avec path filters (voir [ADR-020](022-strategie-cicd-monorepo.md))
|
||||
|
||||
## Conséquences
|
||||
|
||||
|
||||
@@ -14,14 +14,14 @@ ADR-002 spécifie HLS pour tout le streaming audio, mais HLS est un protocole un
|
||||
|
||||
Architecture hybride en **2 phases** :
|
||||
|
||||
### Phase 1 (MVP) : WebSocket + Firebase Cloud Messaging
|
||||
### Phase 1 (MVP) : WebSocket + APNS/FCM Direct
|
||||
|
||||
```
|
||||
[App Mobile] → [WebSocket] → [Backend Go]
|
||||
↓
|
||||
[PostGIS Worker]
|
||||
↓
|
||||
[Firebase FCM / APNS]
|
||||
[APNS / FCM Direct API]
|
||||
↓
|
||||
[Push Notification]
|
||||
```
|
||||
@@ -31,7 +31,7 @@ Architecture hybride en **2 phases** :
|
||||
2. L'app envoie sa position GPS toutes les 30s via WebSocket
|
||||
3. Un worker backend (goroutine) interroge PostGIS toutes les 30s :
|
||||
```sql
|
||||
SELECT poi.*, users.fcm_token
|
||||
SELECT poi.*, users.push_token, users.platform
|
||||
FROM points_of_interest poi
|
||||
JOIN user_locations users ON ST_DWithin(
|
||||
poi.geom,
|
||||
@@ -41,7 +41,7 @@ Architecture hybride en **2 phases** :
|
||||
WHERE users.notifications_enabled = true
|
||||
AND users.last_update > NOW() - INTERVAL '5 minutes'
|
||||
```
|
||||
4. Si proximité détectée → envoi de push notification via Firebase (Android) ou APNS (iOS)
|
||||
4. Si proximité détectée → envoi de push notification via FCM (Android) ou APNS (iOS)
|
||||
5. Utilisateur clique → app s'ouvre → HLS démarre l'audio (ADR-002)
|
||||
|
||||
**Limitations MVP** :
|
||||
@@ -78,11 +78,11 @@ Architecture hybride en **2 phases** :
|
||||
|
||||
| Provider | Fiabilité | Coût MVP | Coût 100K users | Self-hosted | Vendor lock-in | Verdict |
|
||||
|----------|-----------|----------|-----------------|-------------|----------------|---------|
|
||||
| **Firebase (choix)** | 99.95% | **0€** | **0€** | ❌ Non | 🔴 Fort (Google) | ✅ Optimal MVP |
|
||||
| **APNS/FCM Direct (choix)** | 99.95% | **0€** | **0€** | ✅ Oui | 🟢 Aucun | ✅ Optimal |
|
||||
| OneSignal | 99.95% | 0€ | 500€/mois | ❌ Non | 🔴 Fort | ❌ Plus cher |
|
||||
| Pusher Beams | 99.9% | 0€ | 300€/mois | ❌ Non | 🔴 Fort | ❌ Niche |
|
||||
| Custom WS + APNS/FCM | Votre charge | 5€ | 100€+ | ✅ Oui | 🟢 Aucun | ⚠️ Complexe |
|
||||
| Novu (open source) | 99.9% | 15€ | 50€ | ✅ Oui | 🟢 Aucun | 🟡 Phase 2 |
|
||||
| Firebase SDK | 99.95% | 0€ | 0€ | ❌ Non | 🔴 Fort (Google) | ❌ Vendor lock-in |
|
||||
| Novu (open source) | 99.9% | 15€ | 50€ | ✅ Oui | 🟢 Aucun | ❌ Overhead inutile |
|
||||
| Brevo API | 99.9% | 0€ | 49€ | ✅ Oui | 🟢 Aucun | ❌ Email seulement |
|
||||
|
||||
## Justification
|
||||
@@ -93,43 +93,40 @@ Architecture hybride en **2 phases** :
|
||||
- **Batterie** : Connexion persistante optimisée par l'OS mobile
|
||||
- **Bi-directionnel** : Backend peut envoyer des mises à jour instantanées (ex: "nouveau POI créé par un créateur que tu suis")
|
||||
|
||||
### Pourquoi Firebase FCM et pas implémentation custom ?
|
||||
|
||||
- **Gratuit** : 10M notifications/mois (largement suffisant jusqu'à 100K utilisateurs)
|
||||
- **Fiabilité** : Infrastructure Google avec 99.95% uptime
|
||||
- **Batterie** : Utilise les mécanismes système (Google Play Services)
|
||||
- **Cross-platform** : API unifiée iOS/Android
|
||||
|
||||
### Incohérence acceptée : Firebase vs self-hosted (ADR-008, ADR-015)
|
||||
|
||||
**Problème** : RoadWave promeut 100% self-hosted + souveraineté française, mais Firebase = dépendance Google Cloud.
|
||||
### Pourquoi implémentation directe APNS/FCM et pas SDK Firebase ?
|
||||
|
||||
**Réalité technique** : Notifications natives requièrent obligatoirement Google/Apple
|
||||
- **APNS (Apple)** : Seul protocole pour notifications iOS → dépendance Apple inévitable
|
||||
- **FCM (Google)** : Meilleur protocole Android (vs Huawei HMS, Samsung)
|
||||
- **FCM (Google)** : Protocole standard Android (Google Play Services)
|
||||
|
||||
**Alternatives analysées** :
|
||||
1. **Custom WebSocket** (self-hosted) :
|
||||
- ✅ Zéro dépendance externe
|
||||
- ❌ 150+ heures dev (2-3 sprints)
|
||||
- ❌ Maintien de la reliability en-house
|
||||
- ❌ Toujours besoin d'appeler APNS/FCM de toute façon
|
||||
**Implémentation directe choisie** :
|
||||
- **Gratuit** : APNS et FCM sont gratuits (pas de limite de volume)
|
||||
- **Self-hosted** : Code backend 100% maîtrisé, pas de dépendance SDK tiers
|
||||
- **Fiabilité** : Infrastructure Apple/Google avec 99.95% uptime
|
||||
- **Batterie** : Utilise les mécanismes système natifs
|
||||
- **Souveraineté** : Aucun vendor lock-in, appels directs aux APIs
|
||||
- **Simplicité** : HTTP/2 pour APNS, HTTP pour FCM
|
||||
|
||||
2. **Novu (open source self-hosted)** :
|
||||
- ✅ Self-hostable
|
||||
- ❌ Jeune (moins mature)
|
||||
- ❌ Toujours wrapper autour APNS/FCM
|
||||
**Alternatives rejetées** :
|
||||
1. **Firebase SDK** :
|
||||
- ❌ Vendor lock-in Google
|
||||
- ❌ Dépendance SDK externe
|
||||
- ❌ Contradictoire avec ADR-008 (self-hosted) et ADR-015 (souveraineté)
|
||||
- ⚠️ Pas d'avantage technique par rapport aux APIs directes
|
||||
|
||||
2. **OneSignal / Pusher** :
|
||||
- ❌ Vendor lock-in + coût élevé (500€+/mois @ 100K users)
|
||||
- ❌ Abstraction inutile par-dessus APNS/FCM
|
||||
|
||||
3. **Novu (open source)** :
|
||||
- ❌ Overhead sans gain réel
|
||||
- ❌ Toujours wrapper autour APNS/FCM
|
||||
|
||||
3. **OneSignal / Pusher** :
|
||||
- ❌ Même vendor lock-in que Firebase
|
||||
- ❌ Plus cher (500€+/mois @ 100K users)
|
||||
|
||||
**Décision pragmatique** :
|
||||
- Firebase pour MVP : gratuit + fiabilité + time-to-market
|
||||
- **Mitigation vendor lock-in** : Utiliser abstraction layer (`NotificationProvider` interface)
|
||||
- **Exit path documenté** : Migration vers custom solution < 1 sprint si besoin futur
|
||||
- **Probabilité de changement** : Très basse (MVP gratuit, pas d'incitation financière)
|
||||
**Décision technique** :
|
||||
- Implémentation directe APNS/FCM dès le MVP
|
||||
- **Cohérence ADR** : Respecte ADR-008 (self-hosted) et ADR-015 (souveraineté française)
|
||||
- **Abstraction layer** : Interface `NotificationProvider` pour faciliter maintenance
|
||||
- **Complexité** : Gestion des certificats APNS + JWT FCM (standard backend)
|
||||
|
||||
### Pourquoi limiter le geofencing local à Phase 2 ?
|
||||
|
||||
@@ -150,12 +147,12 @@ Architecture hybride en **2 phases** :
|
||||
|
||||
### Négatives
|
||||
|
||||
- ⚠️ **Dépendance Google (Firebase)** : Contradictoire avec ADR-008 (self-hosted) + ADR-015 (souveraineté FR)
|
||||
- Mitigé par abstraction layer (`NotificationProvider` interface) → swap facile si besoin
|
||||
- Exit path documenté pour migration custom (< 1 sprint)
|
||||
- ⚠️ **Données utilisateur chez Google** : Tokens FCM, timestamps notifications
|
||||
- Risque RGPD : Nécessite DPA Google valide
|
||||
- À consulter avec DPO avant déploiement production
|
||||
- ⚠️ **Gestion certificats APNS** : Renouvellement annuel + configuration
|
||||
- Mitigé par scripts automation (certificats auto-renouvelés)
|
||||
- Documentation complète du processus
|
||||
- ⚠️ **Tokens push sensibles** : Tokens FCM/APNS stockés côté backend
|
||||
- Chiffrement tokens en base (conformité RGPD)
|
||||
- Rotation automatique des tokens expirés
|
||||
- ❌ WebSocket nécessite maintien de connexion (charge serveur +10-20%)
|
||||
- ❌ Mode offline non disponible au MVP (déception possible des early adopters)
|
||||
|
||||
@@ -164,58 +161,100 @@ Architecture hybride en **2 phases** :
|
||||
- **ADR-002 (Streaming)** : Aucun conflit - HLS reste pour l'audio
|
||||
- **ADR-005 (Base de données)** : Ajouter index PostGIS `GIST (geom)` sur `points_of_interest`
|
||||
- **ADR-010 (Architecture Backend)** : Ajouter un module `geofencing` avec worker dédié
|
||||
- **ADR-010 (Frontend Mobile)** : Intégrer `firebase_messaging` (Flutter) et gérer permissions
|
||||
- **ADR-010 (Frontend Mobile)** : Intégrer plugins APNS/FCM natifs (Flutter) et gérer permissions
|
||||
|
||||
## Abstraction Layer (Mitigation Vendor Lock-in)
|
||||
## Abstraction Layer (Maintenabilité)
|
||||
|
||||
Pour minimiser le coût de changement future, implémenter une interface abstraite :
|
||||
Implémentation d'une interface abstraite pour gérer APNS et FCM de manière unifiée :
|
||||
|
||||
```go
|
||||
// backend/internal/notification/provider.go
|
||||
type NotificationProvider interface {
|
||||
SendNotification(ctx context.Context, token, title, body, deepLink string) error
|
||||
UpdateToken(ctx context.Context, userID, newToken string) error
|
||||
SendNotification(ctx context.Context, platform, token, title, body, deepLink string) error
|
||||
UpdateToken(ctx context.Context, userID, platform, newToken string) error
|
||||
}
|
||||
|
||||
// backend/internal/notification/firebase_provider.go
|
||||
type FirebaseProvider struct {
|
||||
client *messaging.Client
|
||||
// backend/internal/notification/apns_provider.go
|
||||
type APNSProvider struct {
|
||||
client *apns2.Client
|
||||
bundleID string
|
||||
}
|
||||
|
||||
func (p *FirebaseProvider) SendNotification(ctx context.Context, token, title, body, deepLink string) error {
|
||||
message := &messaging.Message{
|
||||
Notification: &messaging.Notification{
|
||||
Title: title,
|
||||
Body: body,
|
||||
},
|
||||
Data: map[string]string{
|
||||
"deepLink": deepLink,
|
||||
},
|
||||
Token: token,
|
||||
func (p *APNSProvider) SendNotification(ctx context.Context, platform, token, title, body, deepLink string) error {
|
||||
if platform != "ios" {
|
||||
return nil // Not applicable
|
||||
}
|
||||
_, err := p.client.Send(ctx, message)
|
||||
|
||||
notification := &apns2.Notification{
|
||||
DeviceToken: token,
|
||||
Topic: p.bundleID,
|
||||
Payload: payload.NewPayload().
|
||||
AlertTitle(title).
|
||||
AlertBody(body).
|
||||
Custom("deepLink", deepLink),
|
||||
}
|
||||
res, err := p.client.Push(notification)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
return fmt.Errorf("APNS error: %s", res.Reason)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// backend/internal/notification/fcm_provider.go
|
||||
type FCMProvider struct {
|
||||
projectID string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func (p *FCMProvider) SendNotification(ctx context.Context, platform, token, title, body, deepLink string) error {
|
||||
if platform != "android" {
|
||||
return nil // Not applicable
|
||||
}
|
||||
|
||||
message := map[string]interface{}{
|
||||
"message": map[string]interface{}{
|
||||
"token": token,
|
||||
"notification": map[string]string{
|
||||
"title": title,
|
||||
"body": body,
|
||||
},
|
||||
"data": map[string]string{
|
||||
"deepLink": deepLink,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Call FCM HTTP v1 API
|
||||
url := fmt.Sprintf("https://fcm.googleapis.com/v1/projects/%s/messages:send", p.projectID)
|
||||
// ... HTTP POST with OAuth2 token
|
||||
return err
|
||||
}
|
||||
|
||||
// backend/internal/notification/service.go
|
||||
type NotificationService struct {
|
||||
provider NotificationProvider // ← Interface, pas concrète
|
||||
repo NotificationRepository
|
||||
apnsProvider NotificationProvider
|
||||
fcmProvider NotificationProvider
|
||||
repo NotificationRepository
|
||||
}
|
||||
|
||||
func (s *NotificationService) SendPush(ctx context.Context, userID, title, body, deepLink string) error {
|
||||
user, err := s.repo.GetUser(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Route to appropriate provider based on platform
|
||||
if user.Platform == "ios" {
|
||||
return s.apnsProvider.SendNotification(ctx, "ios", user.PushToken, title, body, deepLink)
|
||||
}
|
||||
return s.fcmProvider.SendNotification(ctx, "android", user.PushToken, title, body, deepLink)
|
||||
}
|
||||
```
|
||||
|
||||
**Bénéfice** : Swap Firebase → Custom/Novu sans changer business logic.
|
||||
|
||||
```go
|
||||
// Futur : switch facilement
|
||||
var provider NotificationProvider
|
||||
|
||||
if config.Provider == "firebase" {
|
||||
provider = &FirebaseProvider{...}
|
||||
} else if config.Provider == "custom" {
|
||||
provider = &CustomProvider{...}
|
||||
}
|
||||
```
|
||||
**Bénéfice** : Code modulaire, testable, et facile à maintenir. Ajout futur de providers alternatifs simple.
|
||||
|
||||
## Métriques de Succès
|
||||
|
||||
@@ -229,8 +268,9 @@ if config.Provider == "firebase" {
|
||||
### Phase 1 (MVP - Sprint 3-4)
|
||||
1. Backend : Implémenter WebSocket endpoint `/ws/location`
|
||||
2. Backend : Worker PostGIS avec requête ST_DWithin
|
||||
3. Mobile : Intégrer Firebase SDK + gestion FCM token
|
||||
4. Test : Validation en conditions réelles (Paris, 10 testeurs)
|
||||
3. Backend : Configuration APNS (certificats .p8) + FCM (OAuth2)
|
||||
4. Mobile : Intégrer plugins natifs APNS/FCM + gestion push tokens
|
||||
5. Test : Validation en conditions réelles (Paris, 10 testeurs)
|
||||
|
||||
### Phase 2 (Post-MVP - Sprint 8-10)
|
||||
1. Mobile : Implémenter geofencing avec `flutter_background_geolocation`
|
||||
@@ -240,7 +280,8 @@ if config.Provider == "firebase" {
|
||||
|
||||
## Références
|
||||
|
||||
- [Firebase Cloud Messaging Documentation](https://firebase.google.com/docs/cloud-messaging)
|
||||
- [Apple Push Notification Service (APNS) Documentation](https://developer.apple.com/documentation/usernotifications)
|
||||
- [Firebase Cloud Messaging HTTP v1 API](https://firebase.google.com/docs/cloud-messaging/http-server-ref)
|
||||
- [PostGIS ST_DWithin Performance](https://postgis.net/docs/ST_DWithin.html)
|
||||
- [iOS Geofencing Best Practices](https://developer.apple.com/documentation/corelocation/monitoring_the_user_s_proximity_to_geographic_regions)
|
||||
- Règle Métier 05 : Section 5.1.2 (Mode Piéton, lignes 86-120)
|
||||
|
||||
@@ -41,7 +41,8 @@ Utilisation de **16 librairies open-source** avec licences permissives.
|
||||
| **Auth JWT** | `zitadel/zitadel-go/v3` | Apache-2.0 | SDK Zitadel officiel (ADR-008) |
|
||||
| **WebRTC** | `pion/webrtc/v4` | MIT | Pure Go, radio live (ADR-002) |
|
||||
| **WebSocket** | `coder/websocket` | ISC | Minimal, notifications (ADR-017) |
|
||||
| **FCM Push** | `firebase.google.com/go` | BSD-3 | SDK Google officiel (ADR-017) |
|
||||
| **APNS Push** | `sideshow/apns2` | MIT | Client APNS HTTP/2 natif (ADR-017) |
|
||||
| **FCM Push** | `golang.org/x/oauth2` + HTTP | BSD-3 | FCM HTTP v1 API directe (ADR-017) |
|
||||
| **HLS/FFmpeg** | `asticode/go-astiav` | MIT | Bindings FFmpeg n8.0 |
|
||||
|
||||
### Utilitaires
|
||||
@@ -53,7 +54,7 @@ Utilisation de **16 librairies open-source** avec licences permissives.
|
||||
|
||||
## Alternatives considérées
|
||||
|
||||
Voir [analyse détaillée](../ANALYSE_LIBRAIRIES_GO.md) pour comparatifs complets :
|
||||
Voir pour comparatifs complets :
|
||||
- Framework : Fiber vs Gin vs Echo vs Chi
|
||||
- PostgreSQL : pgx vs GORM vs database/sql
|
||||
- Redis : rueidis vs go-redis vs redigo
|
||||
@@ -88,7 +89,7 @@ Voir [analyse détaillée](../ANALYSE_LIBRAIRIES_GO.md) pour comparatifs complet
|
||||
|
||||
### Négatives
|
||||
- ⚠️ **k6 (AGPL-3.0)** : Copyleft, mais OK pour tests internes (pas de SaaS k6 prévu)
|
||||
- ⚠️ **Firebase FCM** : Dépendance Google (mitigation via abstraction layer, ADR-017)
|
||||
- ⚠️ **Gestion certificats APNS** : Renouvellement annuel, configuration manuelle
|
||||
- ❌ Courbe d'apprentissage : 16 librairies à maîtriser (doc nécessaire)
|
||||
|
||||
### Dépendances go.mod
|
||||
@@ -106,7 +107,8 @@ require (
|
||||
github.com/zitadel/zitadel-go/v3 latest
|
||||
github.com/pion/webrtc/v4 latest
|
||||
github.com/coder/websocket latest
|
||||
firebase.google.com/go/v4 latest
|
||||
github.com/sideshow/apns2 latest
|
||||
golang.org/x/oauth2 latest // For FCM authentication
|
||||
github.com/asticode/go-astiav latest
|
||||
github.com/spf13/viper latest
|
||||
github.com/rs/zerolog latest
|
||||
@@ -116,7 +118,7 @@ require (
|
||||
|
||||
## Références
|
||||
|
||||
- [Analyse complète des librairies](../ANALYSE_LIBRAIRIES_GO.md) (tableaux comparatifs, sources)
|
||||
- (tableaux comparatifs, sources)
|
||||
- ADR-001 : Langage Backend (Fiber, pgx, go-redis)
|
||||
- ADR-007 : Tests BDD (Godog)
|
||||
- ADR-011 : Accès données (sqlc)
|
||||
|
||||
@@ -114,5 +114,5 @@ flowchart TD
|
||||
|
||||
- [ADR-004 : CDN (Souveraineté)](004-cdn.md)
|
||||
- [ADR-015 : Hébergement](015-hebergement.md)
|
||||
- [Règle 02 : RGPD (Mode Dégradé)](../regles-metier/02-conformite-rgpd.md#136-géolocalisation-optionnelle)
|
||||
- [Règle 02 : RGPD (Mode Dégradé)](../domains/_shared/rules/rgpd.md#136-géolocalisation-optionnelle)
|
||||
- IP2Location Lite : https://lite.ip2location.com/
|
||||
|
||||
@@ -14,9 +14,9 @@ L'application mobile RoadWave (iOS/Android) nécessite des librairies tierces po
|
||||
|
||||
## Décision
|
||||
|
||||
Utilisation de **8 librairies open-source** Flutter avec licences permissives.
|
||||
Utilisation de **9 librairies open-source** Flutter avec licences permissives, déployées en 2 phases selon la stratégie définie dans [ADR-017 (Notifications Géolocalisées)](017-notifications-geolocalisees.md).
|
||||
|
||||
### Core Stack
|
||||
### Phase 1 (MVP) : Core Stack
|
||||
|
||||
| Catégorie | Librairie | Licence | Justification |
|
||||
|-----------|-----------|---------|---------------|
|
||||
@@ -26,21 +26,29 @@ Utilisation de **8 librairies open-source** Flutter avec licences permissives.
|
||||
| **Stockage sécurisé** | `flutter_secure_storage` | BSD-3 | Keychain iOS, KeyStore Android |
|
||||
| **Cache images** | `cached_network_image` | MIT | LRU cache, placeholder support |
|
||||
|
||||
### Géolocalisation & Permissions
|
||||
### Phase 1 (MVP) : Géolocalisation & Notifications
|
||||
|
||||
| Catégorie | Librairie | Licence | Justification |
|
||||
|-----------|-----------|---------|---------------|
|
||||
| **GPS temps réel** | `geolocator` | MIT | Mode voiture, high accuracy, background modes |
|
||||
| **Geofencing** | `geofence_service` | MIT | Détection rayon 200m, mode piéton, économie batterie |
|
||||
| **GPS temps réel** | `geolocator` | MIT | Mode voiture, WebSocket position updates, high accuracy |
|
||||
| **Push APNS/FCM** | `flutter_apns` + `flutter_fcm` | MIT | Intégration native APNS et FCM directe (ADR-017) |
|
||||
| **Notifications locales** | `flutter_local_notifications` | BSD-3 | Compteur dynamique, icônes custom, iOS/Android |
|
||||
| **Permissions** | `permission_handler` | MIT | Gestion unifiée permissions iOS/Android |
|
||||
|
||||
### Packages Additionnels (CarPlay/Android Auto)
|
||||
### Phase 1 (MVP) : CarPlay/Android Auto (optionnel)
|
||||
|
||||
| Catégorie | Librairie | Licence | Justification |
|
||||
|-----------|-----------|---------|---------------|
|
||||
| **CarPlay** | `flutter_carplay` | MIT | Intégration CarPlay native (communautaire) |
|
||||
| **Android Auto** | `android_auto_flutter` | Apache-2.0 | Support Android Auto (communautaire) |
|
||||
| **Permissions** | `permission_handler` | MIT | Gestion unifiée permissions iOS/Android |
|
||||
|
||||
### Phase 2 (Post-MVP) : Geofencing Local
|
||||
|
||||
| Catégorie | Librairie | Licence | Justification | Voir ADR |
|
||||
|-----------|-----------|---------|---------------|----------|
|
||||
| **Geofencing local** | `geofence_service` | MIT | Mode offline, détection rayon 200m, notifications app fermée | [ADR-017](017-notifications-geolocalisees.md) Phase 2 |
|
||||
|
||||
**Note importante** : Le geofencing local (`geofence_service`) n'est **PAS utilisé en MVP**. La Phase 1 utilise WebSocket + Firebase pour les notifications de proximité (voir [ADR-017](017-notifications-geolocalisees.md) pour l'architecture complète).
|
||||
|
||||
## Alternatives considérées
|
||||
|
||||
@@ -60,11 +68,22 @@ Utilisation de **8 librairies open-source** Flutter avec licences permissives.
|
||||
- **location** : Moins maintenu
|
||||
- **background_location** : Spécifique background uniquement
|
||||
|
||||
### Notifications Push
|
||||
- **flutter_apns + flutter_fcm** (choisi) : Implémentation directe APNS/FCM, pas de vendor lock-in
|
||||
- **firebase_messaging** : SDK Firebase, vendor lock-in Google
|
||||
- **OneSignal** : Plus cher (500€/mois @ 100K users), vendor lock-in
|
||||
- **Custom WebSocket** : Complexe, toujours besoin APNS/FCM au final (voir ADR-017)
|
||||
|
||||
### Geofencing (Phase 2)
|
||||
- **geofence_service** (choisi) : Natif iOS/Android, économie batterie optimale
|
||||
- **background_geolocation** : Payant (149$/an par app)
|
||||
- **flutter_background_location** : Moins mature
|
||||
|
||||
## Justification
|
||||
|
||||
### Licences
|
||||
- **7/8 librairies** : MIT (permissive totale)
|
||||
- **1/8** : BSD-3 (permissive, compatible commercial)
|
||||
- **7/9 librairies** : MIT (permissive totale)
|
||||
- **2/9** : BSD-3 (permissive, compatible commercial)
|
||||
- **Compatibilité totale** : Aucun conflit de licence, aucune restriction commerciale
|
||||
|
||||
### Maturité
|
||||
@@ -77,12 +96,14 @@ Utilisation de **8 librairies open-source** Flutter avec licences permissives.
|
||||
- **Compilation native** : Dart → ARM64 (pas de bridge JS comme React Native)
|
||||
- **just_audio** : Utilise AVPlayer (iOS) et ExoPlayer (Android) natifs
|
||||
- **geolocator** : Accès direct CoreLocation (iOS) et FusedLocation (Android)
|
||||
- **geofence_service** : Geofencing natif, minimise consommation batterie
|
||||
- **flutter_apns + flutter_fcm** : Utilise services systèmes natifs (APNS, Google Play Services)
|
||||
- **geofence_service** (Phase 2) : Geofencing natif, minimise consommation batterie
|
||||
|
||||
### Conformité Stores
|
||||
- **Permissions progressives** : `permission_handler` + stratégie ADR-010
|
||||
- **Background modes** : `geolocator` + `geofence_service` approuvés stores
|
||||
- **Notifications** : `flutter_local_notifications` conforme guidelines iOS/Android
|
||||
- **Background modes MVP** : `geolocator` (When In Use) + `firebase_messaging` approuvés stores
|
||||
- **Background modes Phase 2** : `geofence_service` nécessite permission "Always" (taux acceptation ~30%)
|
||||
- **Notifications** : `flutter_local_notifications` + `firebase_messaging` conformes guidelines iOS/Android
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -99,14 +120,18 @@ graph TB
|
||||
Cache["cached_network_image<br/>(Image Cache)"]
|
||||
end
|
||||
|
||||
subgraph Services["Services Layer"]
|
||||
subgraph Services["Services Layer - Phase 1 MVP"]
|
||||
Audio["just_audio<br/>(HLS Streaming)"]
|
||||
GPS["geolocator<br/>(GPS Mode Voiture)"]
|
||||
Geofence["geofence_service<br/>(Mode Piéton)"]
|
||||
Notif["flutter_local_notifications<br/>(Alerts Locales)"]
|
||||
GPS["geolocator<br/>(GPS + WebSocket)"]
|
||||
Push["flutter_apns + flutter_fcm<br/>(Push Natifs APNS/FCM)"]
|
||||
Notif["flutter_local_notifications<br/>(Notifications Locales)"]
|
||||
Perms["permission_handler<br/>(Permissions iOS/Android)"]
|
||||
end
|
||||
|
||||
subgraph Phase2["Services Layer - Phase 2"]
|
||||
Geofence["geofence_service<br/>(Mode Offline)"]
|
||||
end
|
||||
|
||||
subgraph Platform["Platform Integration"]
|
||||
CarPlay["flutter_carplay<br/>(iOS)"]
|
||||
AndroidAuto["android_auto_flutter<br/>(Android)"]
|
||||
@@ -116,14 +141,17 @@ graph TB
|
||||
Bloc --> API
|
||||
Bloc --> Audio
|
||||
Bloc --> GPS
|
||||
Bloc --> Geofence
|
||||
Bloc --> Push
|
||||
|
||||
API --> Storage
|
||||
Widgets --> Cache
|
||||
|
||||
GPS --> Perms
|
||||
Geofence --> Perms
|
||||
Geofence --> Notif
|
||||
Push --> Perms
|
||||
Push --> Notif
|
||||
|
||||
Geofence -.->|Phase 2| Perms
|
||||
Geofence -.->|Phase 2| Notif
|
||||
|
||||
Audio --> CarPlay
|
||||
Audio --> AndroidAuto
|
||||
@@ -135,7 +163,8 @@ graph TB
|
||||
|
||||
class UI,Widgets,Bloc uiStyle
|
||||
class Data,API,Storage,Cache dataStyle
|
||||
class Services,Audio,GPS,Geofence,Notif,Perms serviceStyle
|
||||
class Services,Audio,GPS,FCM,Notif,Perms serviceStyle
|
||||
class Phase2,Geofence serviceStyle
|
||||
class Platform,CarPlay,AndroidAuto platformStyle
|
||||
```
|
||||
|
||||
@@ -151,7 +180,8 @@ graph TB
|
||||
|
||||
### Négatives
|
||||
- ⚠️ **CarPlay/Android Auto** : Packages communautaires (pas officiels Flutter)
|
||||
- ⚠️ **Géolocalisation background** : Scrutée par App Store (stratégie progressive requise, ADR-010)
|
||||
- ⚠️ **Configuration APNS/FCM** : Gestion certificats et OAuth2, configuration manuelle
|
||||
- ⚠️ **Permission "Always" Phase 2** : Taux acceptation ~30% (geofencing local)
|
||||
- ❌ **Courbe d'apprentissage** : Dart + pattern BLoC à maîtriser
|
||||
- ❌ **Tests stores** : Validation TestFlight (iOS) et Internal Testing (Android) obligatoires
|
||||
|
||||
@@ -159,28 +189,32 @@ graph TB
|
||||
|
||||
> **Note** : Les versions exactes seront définies lors de l'implémentation. Cette section indique les packages requis, non les versions à utiliser (qui évoluent rapidement dans l'écosystème Flutter).
|
||||
|
||||
**Core** :
|
||||
**Core (Phase 1 MVP)** :
|
||||
- `flutter_bloc` - State management
|
||||
- `just_audio` - Audio HLS streaming
|
||||
- `dio` - HTTP client
|
||||
- `flutter_secure_storage` - Stockage sécurisé JWT
|
||||
- `cached_network_image` - Cache images
|
||||
|
||||
**Géolocalisation & Notifications** :
|
||||
- `geolocator` - GPS haute précision
|
||||
- `geofence_service` - Geofencing arrière-plan
|
||||
**Géolocalisation & Notifications (Phase 1 MVP)** :
|
||||
- `geolocator` - GPS haute précision, WebSocket position updates
|
||||
- `flutter_apns` - Push notifications APNS natif iOS (ADR-017)
|
||||
- `flutter_fcm` - Push notifications FCM natif Android (ADR-017)
|
||||
- `flutter_local_notifications` - Notifications locales
|
||||
- `permission_handler` - Gestion permissions
|
||||
|
||||
**CarPlay/Android Auto** (optionnels MVP) :
|
||||
**CarPlay/Android Auto (optionnels Phase 1)** :
|
||||
- `flutter_carplay` - Intégration CarPlay
|
||||
- `android_auto_flutter` - Support Android Auto
|
||||
|
||||
**Geofencing (Phase 2 Post-MVP)** :
|
||||
- `geofence_service` - Geofencing local pour mode offline (ADR-017 Phase 2)
|
||||
|
||||
### Migration depuis ADR-010
|
||||
|
||||
La section "Packages clés" de l'ADR-010 est désormais obsolète et doit référencer cet ADR :
|
||||
|
||||
> **Packages Flutter** : Voir [ADR-018 - Librairies Flutter](018-librairies-flutter.md) pour la liste complète, licences et justifications.
|
||||
> **Packages Flutter** : Voir [ADR-018 - Librairies Flutter](020-librairies-flutter.md) pour la liste complète, licences et justifications.
|
||||
|
||||
## Risques et Mitigations
|
||||
|
||||
@@ -190,7 +224,8 @@ La section "Packages clés" de l'ADR-010 est désormais obsolète et doit réfé
|
||||
|
||||
### Risque 2 : Validation App Store (permissions background)
|
||||
- **Impact** : Taux de rejet ~70% si mal justifié
|
||||
- **Mitigation** : Stratégie progressive (ADR-010), écrans d'éducation, tests beta TestFlight
|
||||
- **Mitigation Phase 1** : Permission "When In Use" seulement (MVP), moins scrutée par Apple
|
||||
- **Mitigation Phase 2** : Stratégie progressive (ADR-010), écrans d'éducation, tests beta TestFlight pour permission "Always"
|
||||
|
||||
### Risque 3 : Performance audio HLS en arrière-plan
|
||||
- **Impact** : Interruptions si OS tue l'app
|
||||
@@ -198,10 +233,14 @@ La section "Packages clés" de l'ADR-010 est désormais obsolète et doit réfé
|
||||
|
||||
## Références
|
||||
|
||||
- ADR-010 : Frontend Mobile (Flutter, architecture permissions)
|
||||
- ADR-018 : Librairies Go (même format de documentation)
|
||||
- [ADR-010 : Frontend Mobile](012-frontend-mobile.md) (Flutter, architecture permissions)
|
||||
- [ADR-017 : Notifications Géolocalisées](017-notifications-geolocalisees.md) (Phase 1 WebSocket vs Phase 2 Geofencing)
|
||||
- [ADR-018 : Librairies Go](018-librairies-go.md) (même format de documentation)
|
||||
- [flutter_bloc documentation](https://bloclibrary.dev/)
|
||||
- [just_audio repository](https://pub.dev/packages/just_audio)
|
||||
- [geolocator documentation](https://pub.dev/packages/geolocator)
|
||||
- [flutter_apns documentation](https://pub.dev/packages/flutter_apns)
|
||||
- [flutter_fcm documentation](https://pub.dev/packages/flutter_fcm)
|
||||
- [geofence_service documentation](https://pub.dev/packages/geofence_service)
|
||||
- [Apple CarPlay Developer Guide](https://developer.apple.com/carplay/)
|
||||
- [Android Auto Developer Guide](https://developer.android.com/training/cars)
|
||||
|
||||
@@ -43,203 +43,64 @@ RoadWave est organisé en monorepo contenant backend Go, mobile Flutter, documen
|
||||
|
||||
#### Workflow Backend (`backend.yml`)
|
||||
|
||||
```yaml
|
||||
name: Backend CI
|
||||
**Déclencheurs** :
|
||||
- Branches : `main`, `develop`
|
||||
- Chemins surveillés :
|
||||
- `backend/**` : Code Go, migrations, configuration
|
||||
- `features/api/**` : Features BDD des tests API
|
||||
- `features/e2e/**` : Features BDD end-to-end impliquant le backend
|
||||
- `.github/workflows/backend.yml` : Modifications du workflow lui-même
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
paths:
|
||||
- 'backend/**' # Code Go modifié
|
||||
- 'features/api/**' # Tests API modifiés
|
||||
- 'features/e2e/**' # Tests E2E (impliquent backend)
|
||||
- '.github/workflows/backend.yml'
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
paths:
|
||||
- 'backend/**'
|
||||
- 'features/api/**'
|
||||
- 'features/e2e/**'
|
||||
**Jobs exécutés** :
|
||||
- **Tests unitaires** : Exécution `go test` sur tous les packages
|
||||
- **Tests d'intégration** : Utilisation de Testcontainers avec PostgreSQL/PostGIS
|
||||
- **Tests BDD** : Exécution Godog sur features `api/` et `e2e/`
|
||||
- **Lint** : Vérification golangci-lint
|
||||
- **Build** : Compilation binaire production (dépend de tous les jobs précédents)
|
||||
|
||||
jobs:
|
||||
test-unit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.21'
|
||||
- run: cd backend && go test ./...
|
||||
|
||||
test-integration:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.21'
|
||||
- run: cd backend && make test-integration
|
||||
|
||||
test-bdd:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.21'
|
||||
- run: |
|
||||
go install github.com/cucumber/godog/cmd/godog@latest
|
||||
godog run features/api/ features/e2e/
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: golangci/golangci-lint-action@v4
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test-unit, test-integration, test-bdd, lint]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.21'
|
||||
- run: cd backend && make build
|
||||
```
|
||||
|
||||
**Déclenché par** :
|
||||
- Modifications dans `/backend` (code Go, migrations, config)
|
||||
- Nouvelles features API dans `/features/api`
|
||||
- Tests end-to-end dans `/features/e2e` (backend impliqué)
|
||||
**Environnement** : Ubuntu latest, Go 1.21+
|
||||
|
||||
---
|
||||
|
||||
#### Workflow Mobile (`mobile.yml`)
|
||||
|
||||
```yaml
|
||||
name: Mobile CI
|
||||
**Déclencheurs** :
|
||||
- Branches : `main`, `develop`
|
||||
- Chemins surveillés :
|
||||
- `mobile/**` : Code Flutter/Dart, assets, configuration
|
||||
- `features/ui/**` : Features BDD des tests UI
|
||||
- `features/e2e/**` : Features BDD end-to-end impliquant le mobile
|
||||
- `.github/workflows/mobile.yml` : Modifications du workflow lui-même
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
paths:
|
||||
- 'mobile/**' # Code Flutter modifié
|
||||
- 'features/ui/**' # Tests UI modifiés
|
||||
- 'features/e2e/**' # Tests E2E (impliquent mobile)
|
||||
- '.github/workflows/mobile.yml'
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
paths:
|
||||
- 'mobile/**'
|
||||
- 'features/ui/**'
|
||||
- 'features/e2e/**'
|
||||
**Jobs exécutés** :
|
||||
- **Tests unitaires** : Exécution `flutter test` sur widgets et logique métier
|
||||
- **Tests d'intégration** : Tests d'intégration Flutter (interactions UI complexes)
|
||||
- **Lint** : Analyse statique `flutter analyze`
|
||||
- **Build Android** : Compilation APK release (dépend des tests)
|
||||
- **Build iOS** : Compilation IPA release sans codesign (dépend des tests)
|
||||
|
||||
jobs:
|
||||
test-unit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.16.0'
|
||||
- run: cd mobile && flutter test
|
||||
|
||||
test-integration:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.16.0'
|
||||
- run: cd mobile && flutter test integration_test/
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.16.0'
|
||||
- run: cd mobile && flutter analyze
|
||||
|
||||
build-android:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test-unit, test-integration, lint]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.16.0'
|
||||
- run: cd mobile && flutter build apk --release
|
||||
|
||||
build-ios:
|
||||
runs-on: macos-latest
|
||||
needs: [test-unit, test-integration, lint]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.16.0'
|
||||
- run: cd mobile && flutter build ios --release --no-codesign
|
||||
```
|
||||
|
||||
**Déclenché par** :
|
||||
- Modifications dans `/mobile` (code Flutter/Dart, assets, config)
|
||||
- Nouvelles features UI dans `/features/ui`
|
||||
- Tests end-to-end dans `/features/e2e` (mobile impliqué)
|
||||
**Environnement** :
|
||||
- Tests/Lint/Build Android : Ubuntu latest
|
||||
- Build iOS : macOS latest (requis pour Xcode)
|
||||
- Flutter 3.16.0+
|
||||
|
||||
---
|
||||
|
||||
#### Workflow Shared (`shared.yml`)
|
||||
|
||||
```yaml
|
||||
name: Shared CI
|
||||
**Déclencheurs** :
|
||||
- Branches : `main`, `develop`
|
||||
- Chemins surveillés :
|
||||
- `docs/**` : ADR, règles métier, documentation technique
|
||||
- `shared/**` : Contrats API, types partagés
|
||||
- `.github/workflows/shared.yml` : Modifications du workflow lui-même
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
paths:
|
||||
- 'docs/**' # Documentation modifiée
|
||||
- 'shared/**' # Code partagé modifié
|
||||
- '.github/workflows/shared.yml'
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- 'shared/**'
|
||||
**Jobs exécutés** :
|
||||
- **Validation documentation** : Build MkDocs en mode strict (détecte erreurs markdown)
|
||||
- **Vérification liens** : Validation des liens internes/externes dans documentation
|
||||
- **Tests code partagé** : Exécution tests si du code partagé backend-mobile existe
|
||||
|
||||
jobs:
|
||||
docs-validation:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- run: |
|
||||
pip install mkdocs mkdocs-material
|
||||
mkdocs build --strict
|
||||
|
||||
docs-links:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: lycheeverse/lychee-action@v1
|
||||
with:
|
||||
args: 'docs/**/*.md'
|
||||
|
||||
shared-tests:
|
||||
runs-on: ubuntu-latest
|
||||
if: contains(github.event.head_commit.modified, 'shared/')
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# Tests pour code partagé si nécessaire
|
||||
```
|
||||
|
||||
**Déclenché par** :
|
||||
- Modifications dans `/docs` (ADR, règles métier, documentation technique)
|
||||
- Modifications dans `/shared` (contrats API, types partagés)
|
||||
**Environnement** : Ubuntu latest, Python 3.11+
|
||||
|
||||
---
|
||||
|
||||
@@ -333,31 +194,15 @@ Les tests end-to-end dans `/features/e2e/` **déclenchent les deux workflows** (
|
||||
|
||||
### Validation
|
||||
|
||||
```bash
|
||||
# Test 1 : Commit backend-only
|
||||
git add backend/
|
||||
git commit -m "test: backend change"
|
||||
git push
|
||||
# → Vérifier que SEULEMENT backend.yml s'exécute
|
||||
**Scénarios de test à valider** :
|
||||
|
||||
# Test 2 : Commit mobile-only
|
||||
git add mobile/
|
||||
git commit -m "test: mobile change"
|
||||
git push
|
||||
# → Vérifier que SEULEMENT mobile.yml s'exécute
|
||||
1. **Commit backend uniquement** : Modifications dans `/backend` → Vérifier exécution isolée de `backend.yml`
|
||||
2. **Commit mobile uniquement** : Modifications dans `/mobile` → Vérifier exécution isolée de `mobile.yml`
|
||||
3. **Commit features E2E** : Modifications dans `/features/e2e` → Vérifier exécution conjointe de `backend.yml` ET `mobile.yml`
|
||||
4. **Commit documentation uniquement** : Modifications dans `/docs` → Vérifier exécution isolée de `shared.yml`
|
||||
5. **Commit mixte** : Modifications backend + mobile + docs → Vérifier exécution des 3 workflows en parallèle
|
||||
|
||||
# Test 3 : Commit E2E
|
||||
git add features/e2e/
|
||||
git commit -m "test: e2e change"
|
||||
git push
|
||||
# → Vérifier que backend.yml ET mobile.yml s'exécutent
|
||||
|
||||
# Test 4 : Commit docs-only
|
||||
git add docs/
|
||||
git commit -m "docs: update ADR"
|
||||
git push
|
||||
# → Vérifier que SEULEMENT shared.yml s'exécute
|
||||
```
|
||||
**Vérifications** : Consulter l'onglet "Actions" de GitHub pour confirmer quels workflows se sont déclenchés.
|
||||
|
||||
---
|
||||
|
||||
|
||||
206
docs/adr/023-architecture-moderation.md
Normal file
206
docs/adr/023-architecture-moderation.md
Normal file
@@ -0,0 +1,206 @@
|
||||
# ADR-023 : Architecture de Modération
|
||||
|
||||
**Statut** : Accepté
|
||||
**Date** : 2026-02-01
|
||||
|
||||
## Contexte
|
||||
|
||||
Le système de modération RoadWave doit traiter des signalements de contenu audio problématique (haine, spam, droits d'auteur, etc.) avec :
|
||||
- **SLA stricts** : 2h (critique), 24h (haute), 72h (standard) définis dans [Règle 14](../domains/moderation/rules/moderation-flows.md)
|
||||
- **Scalabilité** : 0-10K+ signalements/mois
|
||||
- **Conformité DSA** : transparence, traçabilité, délais garantis
|
||||
- **Efficacité** : pré-filtrage IA pour priorisation automatique
|
||||
|
||||
## Décision
|
||||
|
||||
Architecture hybride **humain + IA** avec file d'attente intelligente.
|
||||
|
||||
### Stack Technique
|
||||
|
||||
| Composant | Technologie | Justification |
|
||||
|-----------|-------------|---------------|
|
||||
| **Queue signalements** | PostgreSQL LISTEN/NOTIFY | Pas de dépendance externe, transactions ACID |
|
||||
| **Transcription audio** | Whisper large-v3 (self-hosted) | Open source, qualité production, 0€ |
|
||||
| **Analyse NLP** | distilbert + roberta-hate-speech | Modèles open source, self-hosted |
|
||||
| **Dashboard modérateurs** | React + Fiber API | Stack cohérent avec ADR-001, ADR-010 |
|
||||
| **Player audio** | Wavesurfer.js | Waveform visuel, annotations temporelles |
|
||||
| **Cache priorisation** | Redis Sorted Sets | Ranking temps réel, TTL automatique |
|
||||
|
||||
### Architecture
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph Client["App Mobile/Web"]
|
||||
Report["Signalement utilisateur"]
|
||||
end
|
||||
|
||||
subgraph Backend["Backend Go"]
|
||||
API["API Fiber<br/>/moderation/report"]
|
||||
Queue["PostgreSQL Queue<br/>LISTEN/NOTIFY"]
|
||||
Worker["Worker Go<br/>(transcription + NLP)"]
|
||||
end
|
||||
|
||||
subgraph AI["IA Self-hosted"]
|
||||
Whisper["Whisper large-v3<br/>(transcription)"]
|
||||
NLP["distilbert<br/>(sentiment + haine)"]
|
||||
end
|
||||
|
||||
subgraph Moderation["Modération Dashboard"]
|
||||
Dashboard["React Dashboard"]
|
||||
Player["Wavesurfer.js<br/>(lecture audio)"]
|
||||
end
|
||||
|
||||
subgraph Storage["Stockage"]
|
||||
DB["PostgreSQL<br/>(signalements + logs)"]
|
||||
Redis["Redis<br/>(priorisation + cache)"]
|
||||
end
|
||||
|
||||
Report --> API
|
||||
API --> Queue
|
||||
Queue --> Worker
|
||||
Worker --> Whisper
|
||||
Whisper --> NLP
|
||||
NLP --> Redis
|
||||
Worker --> DB
|
||||
Dashboard --> Player
|
||||
Dashboard --> Redis
|
||||
Dashboard --> DB
|
||||
|
||||
classDef clientStyle fill:#e3f2fd,stroke:#1565c0
|
||||
classDef backendStyle fill:#fff3e0,stroke:#e65100
|
||||
classDef aiStyle fill:#f3e5f5,stroke:#6a1b9a
|
||||
classDef storageStyle fill:#e8f5e9,stroke:#2e7d32
|
||||
|
||||
class Client,Report clientStyle
|
||||
class Backend,API,Queue,Worker backendStyle
|
||||
class AI,Whisper,NLP aiStyle
|
||||
class Storage,DB,Redis storageStyle
|
||||
```
|
||||
|
||||
### Workflow de Traitement
|
||||
|
||||
1. **Réception signalement** :
|
||||
- Insertion en base PostgreSQL (table `moderation_reports`)
|
||||
- Notification asynchrone via PostgreSQL NOTIFY
|
||||
|
||||
2. **Worker asynchrone** (goroutine) :
|
||||
- Écoute queue PostgreSQL (LISTEN/NOTIFY)
|
||||
- Téléchargement audio depuis stockage S3/local
|
||||
- Transcription audio via Whisper large-v3 (1-10 min selon durée)
|
||||
- Analyse NLP : score confiance 0-100% (distilbert + roberta)
|
||||
- Calcul priorité selon formule : `(score_IA × 0.7) + (nb_signalements × 0.2) + (fiabilité_signaleur × 0.1)`
|
||||
- Insertion dans Redis Sorted Set pour priorisation
|
||||
|
||||
3. **Dashboard modérateurs** :
|
||||
- Récupération signalements priorisés depuis Redis (top 20 par page)
|
||||
- Affichage : transcription, waveform audio, historique créateur
|
||||
- Actions disponibles : Approuver, Rejeter, Escalade (shortcuts clavier A/R/E)
|
||||
- Logs audit PostgreSQL pour traçabilité (conformité DSA)
|
||||
|
||||
## Alternatives considérées
|
||||
|
||||
### Queue de signalements
|
||||
|
||||
| Option | Avantages | Inconvénients | Verdict |
|
||||
|--------|-----------|---------------|---------|
|
||||
| **PostgreSQL LISTEN/NOTIFY** | ✅ Pas de dépendance, ACID | ⚠️ Performance limitée >10K/min | ✅ Choisi MVP |
|
||||
| RabbitMQ | Scalable, dead letter queues | ❌ Nouvelle dépendance, complexité | ❌ Overkill MVP |
|
||||
| Redis Streams | Performant, simple | ⚠️ Pas de garantie persistance | ⚠️ Phase 2 |
|
||||
| SQS/Cloud | Managed, scalable | ❌ Dépendance cloud, coût | ❌ Souveraineté |
|
||||
|
||||
### Transcription audio
|
||||
|
||||
| Option | Coût | Qualité | Hébergement | Verdict |
|
||||
|--------|------|---------|-------------|---------|
|
||||
| **Whisper large-v3** | **0€** (self-hosted) | ⭐⭐⭐ Excellente | Self-hosted | ✅ Choisi |
|
||||
| AssemblyAI API | 0.37$/h audio | ⭐⭐⭐ Excellente | Cloud US | ❌ Coût + souveraineté |
|
||||
| Google Speech-to-Text | 0.024$/min | ⭐⭐ Bonne | Cloud Google | ❌ Dépendance Google |
|
||||
| Whisper tiny/base | 0€ | ⭐ Moyenne | Self-hosted | ❌ Qualité insuffisante |
|
||||
|
||||
### NLP Analyse
|
||||
|
||||
| Option | Coût | Performance | Hébergement | Verdict |
|
||||
|--------|------|-------------|-------------|---------|
|
||||
| **distilbert + roberta** | **0€** | CPU OK (1-3s/audio) | Self-hosted | ✅ Choisi |
|
||||
| OpenAI Moderation API | 0.002$/1K tokens | Excellente | Cloud OpenAI | ❌ Dépendance + coût |
|
||||
| Perspective API (Google) | Gratuit | Bonne | Cloud Google | ❌ Dépendance Google |
|
||||
|
||||
## Justification
|
||||
|
||||
### PostgreSQL LISTEN/NOTIFY
|
||||
|
||||
- **Performance MVP** : Suffisant jusqu'à 1000 signalements/jour (~0.7/min)
|
||||
- **Simplicité** : Pas de broker externe, transactions ACID
|
||||
- **Migration facile** : Abstraction via interface `ModerationQueue` → swap vers Redis Streams si besoin (méthodes : Enqueue, Listen)
|
||||
|
||||
### Whisper large-v3 self-hosted
|
||||
|
||||
- **Coût 0€** vs AssemblyAI (3700€/an @ 10K heures audio)
|
||||
- **Souveraineté** : données sensibles restent en France
|
||||
- **Qualité production** : WER (Word Error Rate) <5% français
|
||||
- **Scaling** : CPU MVP (1 core), GPU Phase 2 si >1000 signalements/jour
|
||||
|
||||
### Dashboard React
|
||||
|
||||
- **Cohérence stack** : Même techno que admin panel (si React adopté)
|
||||
- **Performance** : TanStack Table pour listes >1000 éléments
|
||||
- **Wavesurfer.js** : Standard industrie (SoundCloud, Audacity web)
|
||||
|
||||
## Conséquences
|
||||
|
||||
### Positives
|
||||
|
||||
- ✅ **0€ infrastructure IA** au MVP (CPU standard)
|
||||
- ✅ **100% self-hosted** : conformité souveraineté (ADR-008, ADR-015)
|
||||
- ✅ **Scalable progressif** : PostgreSQL → Redis Streams si besoin
|
||||
- ✅ **Conformité DSA** : logs audit, traçabilité complète
|
||||
- ✅ **Productivité ×3-5** : pré-filtrage IA réduit charge modérateurs
|
||||
|
||||
### Négatives
|
||||
|
||||
- ⚠️ **Latence transcription** : 1-10 min selon durée audio (acceptable, traitement asynchrone)
|
||||
- ⚠️ **Performance limite** : PostgreSQL LISTEN/NOTIFY saturé >10K signalements/jour (migration Redis Streams nécessaire)
|
||||
- ❌ **Ressources CPU** : Whisper consomme 1-4 CPU cores selon charge (migration GPU si >1000 signalements/jour)
|
||||
|
||||
### Dépendances
|
||||
|
||||
**Backend Go** :
|
||||
- `gofiber/fiber/v3` : API Dashboard
|
||||
- `jackc/pgx/v5` : PostgreSQL + LISTEN/NOTIFY
|
||||
- `redis/rueidis` : Cache priorisation
|
||||
- Whisper : via Python subprocess ou go-whisper bindings
|
||||
|
||||
**Frontend Dashboard** :
|
||||
- `react` : Framework UI
|
||||
- `@tanstack/react-table` : Tables performantes
|
||||
- `wavesurfer.js` : Player audio avec waveform
|
||||
|
||||
## Métriques de Succès
|
||||
|
||||
- Latence traitement < 10 min (P95) après réception signalement
|
||||
- Précision IA pré-filtre > 80% (validation humaine)
|
||||
- SLA respectés > 95% des cas (2h/24h/72h selon priorité)
|
||||
- Coût infrastructure < 50€/mois jusqu'à 1000 signalements/mois
|
||||
|
||||
## Migration et Rollout
|
||||
|
||||
### Phase 1 (MVP - Sprint 3-4)
|
||||
1. Backend : API `/moderation/report` + PostgreSQL queue
|
||||
2. Worker : Whisper large-v3 CPU + NLP basique (liste noire mots-clés)
|
||||
3. Dashboard : React basique (liste + player audio)
|
||||
|
||||
### Phase 2 (Post-MVP - Sprint 8-10)
|
||||
1. Migration Redis Streams si >1000 signalements/jour
|
||||
2. GPU pour Whisper si latence >15 min P95
|
||||
3. NLP avancé (distilbert + roberta)
|
||||
4. Modération communautaire (badges, [Règle 15](../domains/moderation/rules/moderation-communautaire.md))
|
||||
|
||||
## Références
|
||||
|
||||
- [Règle 14 : Modération - Flows opérationnels](../domains/moderation/rules/moderation-flows.md)
|
||||
- [Règle 15 : Modération Communautaire](../domains/moderation/rules/moderation-communautaire.md)
|
||||
- [ADR-001 : Langage Backend](001-langage-backend.md) (Go, Fiber)
|
||||
- [ADR-005 : Base de données](005-base-de-donnees.md) (PostgreSQL)
|
||||
- [ADR-010 : Architecture Backend](010-architecture-backend.md) (Modular monolith)
|
||||
- [Whisper large-v3 documentation](https://github.com/openai/whisper)
|
||||
- [PostgreSQL LISTEN/NOTIFY](https://www.postgresql.org/docs/current/sql-notify.html)
|
||||
292
docs/adr/024-monitoring-observabilite.md
Normal file
292
docs/adr/024-monitoring-observabilite.md
Normal file
@@ -0,0 +1,292 @@
|
||||
# ADR-024 : Monitoring, Observabilité et Incident Response
|
||||
|
||||
**Statut** : Accepté
|
||||
**Date** : 2026-02-01
|
||||
|
||||
## Contexte
|
||||
|
||||
RoadWave nécessite un système de monitoring pour garantir la disponibilité cible 99.9% (SLO) définie dans :
|
||||
- **Métriques** : latency p99 < 100ms, throughput API, erreurs
|
||||
- **Alerting** : détection pannes, dégradations performance
|
||||
- **Incident response** : runbooks, escalation, post-mortems
|
||||
- **Backup/Disaster Recovery** : RTO 1h, RPO 15min
|
||||
|
||||
Contrainte : **self-hosted** pour souveraineté données (ADR-015).
|
||||
|
||||
## Décision
|
||||
|
||||
Stack **Prometheus + Grafana + Loki** self-hosted avec alerting multi-canal.
|
||||
|
||||
### Stack Technique
|
||||
|
||||
| Composant | Technologie | Licence | Justification |
|
||||
|-----------|-------------|---------|---------------|
|
||||
| **Métriques** | Prometheus | Apache-2.0 | Standard industrie, PromQL, TSDB performant |
|
||||
| **Visualisation** | Grafana | AGPL-3.0 | Dashboards riches, alerting intégré |
|
||||
| **Logs** | Grafana Loki | AGPL-3.0 | "Prometheus pour logs", compression efficace |
|
||||
| **Tracing** | Tempo (optionnel Phase 2) | AGPL-3.0 | Traces distribuées, compatible OpenTelemetry |
|
||||
| **Alerting** | Alertmanager | Apache-2.0 | Grouping, silencing, routing multi-canal |
|
||||
| **Canaux alerts** | Email (Brevo) + Telegram Bot | - | Multi-canal, pas de coût SMS |
|
||||
| **Uptime monitoring** | Uptime Kuma | MIT | Self-hosted, SSL checks, incidents page |
|
||||
|
||||
### Architecture
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph Services["Services RoadWave"]
|
||||
API["Backend Go API<br/>(Fiber metrics)"]
|
||||
DB["PostgreSQL<br/>(pg_exporter)"]
|
||||
Redis["Redis<br/>(redis_exporter)"]
|
||||
Zitadel["Zitadel<br/>(metrics endpoint)"]
|
||||
end
|
||||
|
||||
subgraph Monitoring["Stack Monitoring"]
|
||||
Prom["Prometheus<br/>(scrape + TSDB)"]
|
||||
Grafana["Grafana<br/>(dashboards)"]
|
||||
Loki["Loki<br/>(logs aggregation)"]
|
||||
Alert["Alertmanager<br/>(routing)"]
|
||||
Uptime["Uptime Kuma<br/>(external checks)"]
|
||||
end
|
||||
|
||||
subgraph Notifications["Alerting"]
|
||||
Email["Email (Brevo)"]
|
||||
Telegram["Telegram Bot"]
|
||||
end
|
||||
|
||||
subgraph Storage["Stockage"]
|
||||
PromStorage["Prometheus TSDB<br/>(15j retention)"]
|
||||
LokiStorage["Loki Chunks<br/>(7j retention)"]
|
||||
Backups["Backups PostgreSQL<br/>(S3 OVH)"]
|
||||
end
|
||||
|
||||
API --> Prom
|
||||
DB --> Prom
|
||||
Redis --> Prom
|
||||
Zitadel --> Prom
|
||||
|
||||
API -.->|logs stdout| Loki
|
||||
Prom --> Grafana
|
||||
Loki --> Grafana
|
||||
Prom --> Alert
|
||||
|
||||
Alert --> Email
|
||||
Alert --> Telegram
|
||||
|
||||
Uptime -.->|external HTTP checks| API
|
||||
Uptime --> Alert
|
||||
|
||||
Prom --> PromStorage
|
||||
Loki --> LokiStorage
|
||||
DB -.->|WAL-E continuous| Backups
|
||||
|
||||
classDef serviceStyle fill:#e3f2fd,stroke:#1565c0
|
||||
classDef monitoringStyle fill:#fff3e0,stroke:#e65100
|
||||
classDef notifStyle fill:#f3e5f5,stroke:#6a1b9a
|
||||
classDef storageStyle fill:#e8f5e9,stroke:#2e7d32
|
||||
|
||||
class Services,API,DB,Redis,Zitadel serviceStyle
|
||||
class Monitoring,Prom,Grafana,Loki,Alert,Uptime monitoringStyle
|
||||
class Notifications,Email,Telegram notifStyle
|
||||
class Storage,PromStorage,LokiStorage,Backups storageStyle
|
||||
```
|
||||
|
||||
### Métriques Clés
|
||||
|
||||
**API Performance** (requêtes PromQL) :
|
||||
- Latency p99 : histogramme quantile 99e percentile sur durée requêtes HTTP (fenêtre 5 min)
|
||||
- Error rate : ratio requêtes 5xx / total requêtes (fenêtre 5 min)
|
||||
- Throughput : taux de requêtes par seconde (fenêtre 5 min)
|
||||
|
||||
**Infrastructure** :
|
||||
- CPU usage : taux utilisation CPU (mode non-idle, fenêtre 5 min)
|
||||
- Memory usage : ratio mémoire disponible / totale
|
||||
- Disk I/O : temps I/O disque (fenêtre 5 min)
|
||||
|
||||
**Business** (compteurs custom) :
|
||||
- Active users (DAU) : `roadwave_active_users_total`
|
||||
- Audio streams actifs : `roadwave_hls_streams_active`
|
||||
- Signalements modération : `roadwave_moderation_reports_total`
|
||||
|
||||
## Alternatives considérées
|
||||
|
||||
### Stack Monitoring
|
||||
|
||||
| Option | Coût | Hébergement | Complexité | Verdict |
|
||||
|--------|------|-------------|------------|---------|
|
||||
| **Prometheus + Grafana** | **0€** | Self-hosted | ⭐⭐ Moyenne | ✅ Choisi |
|
||||
| Datadog | 15-31$/host/mois | SaaS US | ⭐ Faible | ❌ Coût + souveraineté |
|
||||
| New Relic | 99-349$/user/mois | SaaS US | ⭐ Faible | ❌ Coût prohibitif |
|
||||
| Elastic Stack (ELK) | 0€ (open) | Self-hosted | ⭐⭐⭐ Complexe | ❌ Overhead JVM |
|
||||
| VictoriaMetrics | 0€ | Self-hosted | ⭐⭐ Moyenne | ⚠️ Moins mature |
|
||||
|
||||
### Alerting Canaux
|
||||
|
||||
| Canal | Coût | Disponibilité | Intrusivité | Verdict |
|
||||
|-------|------|---------------|-------------|---------|
|
||||
| **Email (Brevo)** | **0€ (300/j)** | Asynchrone | ⭐ Basse | ✅ Standard |
|
||||
| **Telegram Bot** | **0€** | Temps réel | ⭐⭐ Moyenne | ✅ On-call |
|
||||
| SMS (Twilio) | 0.04€/SMS | Immédiat | ⭐⭐⭐ Haute | ⚠️ Phase 2 (critique) |
|
||||
| PagerDuty | 21$/user/mois | Immédiat + escalation | ⭐⭐⭐ Haute | ❌ Coût |
|
||||
| OpsGenie | 29$/user/mois | Immédiat + escalation | ⭐⭐⭐ Haute | ❌ Coût |
|
||||
|
||||
### Backup Strategy
|
||||
|
||||
| Option | RPO | RTO | Coût | Verdict |
|
||||
|--------|-----|-----|------|---------|
|
||||
| **WAL-E continuous archiving** | **15 min** | **1h** | **5-15€/mois (S3)** | ✅ Choisi |
|
||||
| pg_dump quotidien | 24h | 2-4h | 0€ (local) | ❌ RPO trop élevé |
|
||||
| pgBackRest | 5 min | 30 min | 10-20€/mois | ⚠️ Complexe MVP |
|
||||
| Managed backup (Scaleway) | 5 min | 15 min | 50€/mois | ❌ Phase 2 |
|
||||
|
||||
## Justification
|
||||
|
||||
### Prometheus + Grafana
|
||||
|
||||
- **Standard industrie** : adopté par CNCF, documentation riche
|
||||
- **Performance** : TSDB optimisé, compression >10x vs PostgreSQL
|
||||
- **Écosystème** : 150+ exporters officiels (PostgreSQL, Redis, Go, Nginx)
|
||||
- **PromQL** : langage requête puissant pour alerting complexe
|
||||
- **Coût 0€** : self-hosted, licences permissives
|
||||
|
||||
### Loki pour Logs
|
||||
|
||||
- **Compression** : 10-50x vs Elasticsearch (stockage chunks)
|
||||
- **Simplicité** : pas de schéma, logs = labels + timestamp
|
||||
- **Intégration Grafana** : requêtes logs + métriques unifiées
|
||||
- **Performance** : grep distribué sur labels indexés
|
||||
|
||||
### Uptime Kuma
|
||||
|
||||
- **Self-hosted** : alternative à UptimeRobot (SaaS)
|
||||
- **Fonctionnalités** : HTTP/HTTPS checks, SSL expiry, status page public
|
||||
- **Alerting** : intégration Webhook, Email
|
||||
- **Coût 0€** : open source MIT
|
||||
|
||||
## Conséquences
|
||||
|
||||
### Positives
|
||||
|
||||
- ✅ **Coût infrastructure** : 5-20€/mois (stockage S3 backups uniquement)
|
||||
- ✅ **Souveraineté** : 100% self-hosted OVH France
|
||||
- ✅ **Alerting multi-canal** : Email + Telegram (extensible SMS Phase 2)
|
||||
- ✅ **Observabilité complète** : métriques + logs + uptime externe
|
||||
- ✅ **Conformité RGPD** : logs anonymisés, rétention 7-15j
|
||||
|
||||
### Négatives
|
||||
|
||||
- ⚠️ **Maintenance** : Stack à gérer (mises à jour Prometheus, Grafana, Loki)
|
||||
- ⚠️ **Stockage** : Prometheus TSDB consomme ~1-2 GB/mois @ 1000 RPS
|
||||
- ❌ **Pas d'on-call automatique** au MVP (Telegram manual, SMS Phase 2)
|
||||
- ❌ **Courbe d'apprentissage** : PromQL à maîtriser
|
||||
|
||||
### Dashboards Grafana
|
||||
|
||||
**Dashboard principal** :
|
||||
- Latency p50/p95/p99 API (5 min, 1h, 24h)
|
||||
- Error rate 5xx/4xx (seuil alerte >1%)
|
||||
- Throughput requests/sec
|
||||
- Infra : CPU, RAM, Disk I/O
|
||||
- Business : DAU, streams actifs, signalements modération
|
||||
|
||||
**Dashboard PostgreSQL** :
|
||||
- Slow queries (>100ms)
|
||||
- Connections actives vs max
|
||||
- Cache hit ratio (cible >95%)
|
||||
- Deadlocks count
|
||||
|
||||
**Dashboard Redis** :
|
||||
- Memory usage
|
||||
- Evictions count
|
||||
- Commands/sec
|
||||
- Keyspace hits/misses ratio
|
||||
|
||||
### Alerting Rules
|
||||
|
||||
**Alertes critiques** (Telegram + Email immédiat) :
|
||||
- **API Down** : Job API indisponible pendant >1 min → Notification immédiate
|
||||
- **High Error Rate** : Taux erreurs 5xx >1% pendant >5 min → Notification immédiate
|
||||
- **Database Down** : PostgreSQL indisponible pendant >1 min → Notification immédiate
|
||||
|
||||
**Alertes warnings** (Email uniquement) :
|
||||
- **High Latency** : Latency p99 >100ms pendant >10 min → Investigation requise
|
||||
- **Disk Space Running Out** : Espace disque <10% pendant >30 min → Nettoyage requis
|
||||
|
||||
### Backup & Disaster Recovery
|
||||
|
||||
**PostgreSQL WAL-E** :
|
||||
- Méthode : Backup continu Write-Ahead Log (WAL)
|
||||
- Rétention : 7 jours full + WAL incrémentaux
|
||||
- Stockage : S3 OVH région GRA (France)
|
||||
- Chiffrement : AES-256 server-side
|
||||
|
||||
**RTO (Recovery Time Objective)** : 1h
|
||||
- Restore depuis S3 : ~30 min (DB 10 GB)
|
||||
- Validation + relance services : ~30 min
|
||||
|
||||
**RPO (Recovery Point Objective)** : 15 min
|
||||
- Fréquence archivage WAL : toutes les 15 min
|
||||
- Perte maximale : 15 min de transactions
|
||||
|
||||
**Tests DR** : Mensuel (restore backup sur environnement staging)
|
||||
|
||||
## Runbooks Incidents
|
||||
|
||||
### API Down (5xx errors spike)
|
||||
|
||||
1. **Vérifier** : Grafana dashboard → onglet Errors
|
||||
2. **Logs** : Requête Loki filtrée sur app roadwave-api + niveau error
|
||||
3. **Actions** :
|
||||
- Si OOM : restart container + augmenter RAM
|
||||
- Si DB connexions saturées : vérifier slow queries
|
||||
- Si réseau : vérifier OVH status page
|
||||
4. **Escalade** : Si non résolu en 15 min → appel admin senior
|
||||
|
||||
### Database Slow Queries
|
||||
|
||||
1. **Identifier** : Grafana → PostgreSQL dashboard → Top slow queries
|
||||
2. **Analyser** : Utiliser EXPLAIN ANALYZE sur query problématique
|
||||
3. **Actions** :
|
||||
- Index manquant : créer index (migration rapide)
|
||||
- Lock contention : identifier transaction longue et kill si bloquante
|
||||
4. **Prevention** : Ajouter alerte Grafana si query >100ms P95
|
||||
|
||||
### High Load (CPU >80%)
|
||||
|
||||
1. **Vérifier** : Grafana → Node Exporter → CPU usage
|
||||
2. **Top processus** : Consulter htop ou docker stats
|
||||
3. **Actions** :
|
||||
- Si Whisper (modération) : réduire concurrence workers
|
||||
- Si API : scale horizontal (ajouter instance)
|
||||
4. **Prévention** : Auto-scaling (Phase 2)
|
||||
|
||||
## Métriques de Succès
|
||||
|
||||
- Uptime > 99.9% (8.76h downtime/an max)
|
||||
- MTTD (Mean Time To Detect) < 5 min
|
||||
- MTTR (Mean Time To Recover) < 30 min
|
||||
- Alerts faux positifs < 5%
|
||||
|
||||
## Migration et Rollout
|
||||
|
||||
### Phase 1 (MVP - Sprint 2-3)
|
||||
1. Deploy Prometheus + Grafana + Loki (Docker Compose)
|
||||
2. Instrumenter API Go (Fiber middleware metrics)
|
||||
3. Configure exporters : PostgreSQL, Redis, Node
|
||||
4. Dashboard principal + 5 alertes critiques
|
||||
5. Setup WAL-E backup PostgreSQL
|
||||
|
||||
### Phase 2 (Post-MVP - Sprint 6-8)
|
||||
1. Ajouter Tempo (tracing distribué)
|
||||
2. SMS alerting (Twilio) pour incidents critiques
|
||||
3. Auto-scaling basé métriques Prometheus
|
||||
4. Post-mortem process (template Notion)
|
||||
|
||||
## Références
|
||||
|
||||
- (SLO 99.9%, latency p99 <100ms)
|
||||
- [ADR-001 : Langage Backend](001-langage-backend.md) (Go, Fiber)
|
||||
- [ADR-005 : Base de données](005-base-de-donnees.md) (PostgreSQL)
|
||||
- [ADR-015 : Hébergement](015-hebergement.md) (OVH France, self-hosted)
|
||||
- [Prometheus Documentation](https://prometheus.io/docs/)
|
||||
- [Grafana Loki](https://grafana.com/oss/loki/)
|
||||
- [WAL-E PostgreSQL Archiving](https://github.com/wal-e/wal-e)
|
||||
276
docs/adr/025-securite-secrets.md
Normal file
276
docs/adr/025-securite-secrets.md
Normal file
@@ -0,0 +1,276 @@
|
||||
# ADR-025 : Sécurité - Secrets Management et Encryption
|
||||
|
||||
**Statut** : Accepté
|
||||
**Date** : 2026-02-01
|
||||
|
||||
## Contexte
|
||||
|
||||
RoadWave manipule des données sensibles nécessitant une protection renforcée :
|
||||
- **Secrets applicatifs** : JWT signing key, DB credentials, Mangopay API keys
|
||||
- **PII utilisateurs** : Positions GPS précises, emails, données bancaires (via Mangopay)
|
||||
- **Conformité** : RGPD (minimisation données, encryption at rest), PCI-DSS (paiements)
|
||||
- **Souveraineté** : Self-hosted requis (ADR-015)
|
||||
|
||||
Contrainte : **OWASP Top 10 mitigation** obligatoire pour sécurité applicative.
|
||||
|
||||
## Décision
|
||||
|
||||
Stratégie **secrets management + encryption at rest + HTTPS** avec stack self-hosted.
|
||||
|
||||
### Stack Sécurité
|
||||
|
||||
| Composant | Technologie | Licence | Justification |
|
||||
|-----------|-------------|---------|---------------|
|
||||
| **Secrets management** | HashiCorp Vault (open source) | MPL-2.0 | Standard industrie, rotation auto, audit logs |
|
||||
| **Encryption PII** | AES-256-GCM (crypto/aes Go) | BSD-3 | NIST approuvé, AEAD (authenticated) |
|
||||
| **HTTPS/TLS** | Let's Encrypt (Certbot) | ISC | Gratuit, renouvellement auto, wildcard support |
|
||||
| **CORS/CSRF** | Fiber middleware | MIT | Protection XSS/CSRF intégrée |
|
||||
| **Rate limiting** | Redis + Token Bucket (Fiber) | MIT/Apache | Protection brute-force, DDoS |
|
||||
| **SQL injection** | sqlc (prepared statements) | MIT | Parameterized queries (ADR-011) |
|
||||
|
||||
### Architecture Secrets
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph Dev["Environnement Dev"]
|
||||
EnvFile[".env file<br/>(local uniquement)"]
|
||||
end
|
||||
|
||||
subgraph Prod["Production"]
|
||||
Vault["HashiCorp Vault<br/>(secrets storage)"]
|
||||
API["Backend Go API"]
|
||||
DB["PostgreSQL<br/>(encrypted at rest)"]
|
||||
Redis["Redis<br/>(TLS enabled)"]
|
||||
end
|
||||
|
||||
subgraph Encryption["Encryption Layer"]
|
||||
AES["AES-256-GCM<br/>(PII encryption)"]
|
||||
TLS["TLS 1.3<br/>(transport)"]
|
||||
end
|
||||
|
||||
subgraph Secrets["Secrets Stockés"]
|
||||
JWT["JWT Signing Key<br/>(RS256 private key)"]
|
||||
DBCreds["DB Credentials<br/>(user/pass)"]
|
||||
Mangopay["Mangopay API Key<br/>(sandbox + prod)"]
|
||||
EncKey["Encryption Master Key<br/>(AES-256)"]
|
||||
end
|
||||
|
||||
EnvFile -.->|dev only| API
|
||||
Vault --> API
|
||||
|
||||
Vault --- JWT
|
||||
Vault --- DBCreds
|
||||
Vault --- Mangopay
|
||||
Vault --- EncKey
|
||||
|
||||
API --> AES
|
||||
API --> TLS
|
||||
AES --> DB
|
||||
TLS --> DB
|
||||
TLS --> Redis
|
||||
|
||||
classDef devStyle fill:#fff3e0,stroke:#e65100
|
||||
classDef prodStyle fill:#e3f2fd,stroke:#1565c0
|
||||
classDef encStyle fill:#f3e5f5,stroke:#6a1b9a
|
||||
classDef secretStyle fill:#ffebee,stroke:#c62828
|
||||
|
||||
class Dev,EnvFile devStyle
|
||||
class Prod,Vault,API,DB,Redis prodStyle
|
||||
class Encryption,AES,TLS encStyle
|
||||
class Secrets,JWT,DBCreds,Mangopay,EncKey secretStyle
|
||||
```
|
||||
|
||||
### Secrets Management avec Vault
|
||||
|
||||
**Initialisation Vault** (one-time setup) :
|
||||
1. Init Vault : génération 5 unseal keys + root token (Shamir secret sharing)
|
||||
2. Unseal : 3 clés parmi 5 requises pour déverrouiller Vault
|
||||
3. Login root + activation KV-v2 engine (path : `roadwave/`)
|
||||
|
||||
**Secrets stockés** :
|
||||
- **JWT signing key** : Paire RS256 privée/publique
|
||||
- **Database credentials** : Host, port, user, password (généré aléatoire 32 caractères)
|
||||
- **Mangopay API** : Client ID, API key, webhook secret
|
||||
|
||||
**Récupération depuis Backend Go** :
|
||||
- Utilisation SDK `hashicorp/vault/api`
|
||||
- Authentification via token Vault (variable env VAULT_TOKEN)
|
||||
- Récupération secrets via KVv2 engine
|
||||
|
||||
### Encryption PII (Field-level)
|
||||
|
||||
**Données chiffrées** (AES-256-GCM) :
|
||||
- **GPS précis** : lat/lon conservés 24h puis réduits à geohash-5 (~5km²) ([Règle 02](../domains/_shared/rules/rgpd.md))
|
||||
- **Email** : chiffré en base, déchiffré uniquement à l'envoi
|
||||
- **Numéro téléphone** : si ajouté (Phase 2)
|
||||
|
||||
**Architecture encryption** :
|
||||
- Utilisation bibliothèque standard Go `crypto/aes` avec mode GCM (AEAD)
|
||||
- Master key 256 bits (32 bytes) récupérée depuis Vault
|
||||
- Chiffrement : génération nonce aléatoire + seal GCM → encodage base64
|
||||
- Stockage : colonne `email_encrypted` en base PostgreSQL
|
||||
|
||||
**Contraintes** :
|
||||
- Index direct sur champ chiffré impossible
|
||||
- Solution : index sur hash SHA-256 de l'email chiffré pour recherche
|
||||
|
||||
### HTTPS/TLS Configuration
|
||||
|
||||
**Let's Encrypt wildcard certificate** :
|
||||
- Méthode : Certbot avec DNS-01 challenge (API OVH)
|
||||
- Domaines couverts : `roadwave.fr` + `*.roadwave.fr` (wildcard)
|
||||
- Renouvellement : automatique via cron quotidien (30j avant expiration)
|
||||
|
||||
**Nginx TLS configuration** :
|
||||
- Protocol : TLS 1.3 uniquement (pas de TLS 1.2 ou inférieur)
|
||||
- Ciphers : TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384
|
||||
- HSTS : max-age 1 an, includeSubDomains
|
||||
- Security headers : X-Frame-Options DENY, X-Content-Type-Options nosniff, Referrer-Policy strict-origin-when-cross-origin
|
||||
|
||||
## Alternatives considérées
|
||||
|
||||
### Secrets Management
|
||||
|
||||
| Option | Coût | Hébergement | Rotation auto | Audit | Verdict |
|
||||
|--------|------|-------------|---------------|-------|---------|
|
||||
| **Vault (OSS)** | **0€** | Self-hosted | ✅ Oui | ✅ Oui | ✅ Choisi |
|
||||
| Vault Enterprise | 150$/mois | Self-hosted | ✅ Oui | ✅ Oui | ❌ Overkill MVP |
|
||||
| Kubernetes Secrets | 0€ | K8s only | ❌ Non | ⚠️ Limité | ⚠️ Phase 2 (K8s) |
|
||||
| Variables env (.env) | 0€ | VM/container | ❌ Non | ❌ Non | ❌ Insécure prod |
|
||||
| AWS Secrets Manager | 0.40$/secret/mois | Cloud AWS | ✅ Oui | ✅ Oui | ❌ Souveraineté |
|
||||
|
||||
### Encryption Library
|
||||
|
||||
| Option | Performance | AEAD | FIPS 140-2 | Verdict |
|
||||
|--------|-------------|------|------------|---------|
|
||||
| **crypto/aes (Go std)** | ⭐⭐⭐ Rapide | ✅ GCM | ✅ Approuvé | ✅ Choisi |
|
||||
| age (filippo.io/age) | ⭐⭐ Moyen | ✅ ChaCha20 | ❌ Non | ⚠️ Moins standard |
|
||||
| NaCl/libsodium | ⭐⭐⭐ Rapide | ✅ Poly1305 | ❌ Non | ⚠️ CGO dependency |
|
||||
|
||||
### TLS Certificate
|
||||
|
||||
| Option | Coût | Renouvellement | Wildcard | Verdict |
|
||||
|--------|------|----------------|----------|---------|
|
||||
| **Let's Encrypt** | **0€** | Auto (90j) | ✅ Oui (DNS-01) | ✅ Choisi |
|
||||
| OVH SSL | 5€/an | Manuel | ✅ Oui | ❌ Coût inutile |
|
||||
| Cloudflare SSL | 0€ | Auto | ✅ Oui | ⚠️ Proxy Cloudflare |
|
||||
|
||||
## Justification
|
||||
|
||||
### HashiCorp Vault
|
||||
|
||||
- **Standard industrie** : utilisé par 80% Fortune 500
|
||||
- **Rotation automatique** : credentials DB renouvelés toutes les 90j
|
||||
- **Audit logs** : qui a accédé à quel secret, quand
|
||||
- **Unseal ceremony** : sécurité maximale (3/5 clés requises)
|
||||
- **Coût 0€** : version open source MPL-2.0
|
||||
|
||||
### AES-256-GCM
|
||||
|
||||
- **NIST approuvé** : standard gouvernement US (FIPS 140-2)
|
||||
- **AEAD** : Authenticated Encryption with Associated Data (pas de tampering)
|
||||
- **Performance** : hardware acceleration (AES-NI CPU)
|
||||
- **Bibliothèque std Go** : pas de dépendance externe
|
||||
|
||||
### Let's Encrypt
|
||||
|
||||
- **Gratuit** : économie 50-200€/an vs certificat commercial
|
||||
- **Automatique** : Certbot renouvelle 30j avant expiration
|
||||
- **Wildcard** : 1 certificat pour *.roadwave.fr (tous sous-domaines)
|
||||
- **Adopté massivement** : 300M+ sites web
|
||||
|
||||
## Conséquences
|
||||
|
||||
### Positives
|
||||
|
||||
- ✅ **Conformité RGPD** : encryption at rest PII, minimisation données
|
||||
- ✅ **PCI-DSS** : secrets paiement isolés (Mangopay API key dans Vault)
|
||||
- ✅ **OWASP Top 10** : SQL injection (sqlc), XSS/CSRF (Fiber), rate limiting
|
||||
- ✅ **Coût 0€** : stack complète open source
|
||||
- ✅ **Audit trail** : logs Vault tracent tous accès secrets
|
||||
|
||||
### Négatives
|
||||
|
||||
- ⚠️ **Vault unseal** : nécessite 3/5 clés au redémarrage serveur (procédure manuelle)
|
||||
- ⚠️ **Performance encryption** : +0.5-2ms latency par champ chiffré (acceptable)
|
||||
- ❌ **Complexité opérationnelle** : Vault à maintenir (backups, upgrades)
|
||||
- ❌ **Recherche email impossible** : chiffrement empêche `WHERE email = 'x'` (utiliser hash)
|
||||
|
||||
### OWASP Top 10 Mitigation
|
||||
|
||||
| Vulnérabilité | Mitigation RoadWave | Implémentation |
|
||||
|---------------|---------------------|----------------|
|
||||
| **A01: Broken Access Control** | JWT scopes + RBAC | Zitadel roles (ADR-008) |
|
||||
| **A02: Cryptographic Failures** | AES-256-GCM + TLS 1.3 | crypto/aes + Let's Encrypt |
|
||||
| **A03: Injection** | Prepared statements (sqlc) | ADR-011 |
|
||||
| **A04: Insecure Design** | Threat modeling + ADR reviews | Process architecture |
|
||||
| **A05: Security Misconfiguration** | Vault secrets + hardened config | ADR-025 |
|
||||
| **A06: Vulnerable Components** | Dependabot + go mod tidy | GitHub Actions |
|
||||
| **A07: Auth Failures** | Zitadel + rate limiting | ADR-008 + Fiber middleware |
|
||||
| **A08: Software Integrity** | Code signing + SBOM | Phase 2 |
|
||||
| **A09: Logging Failures** | Loki centralisé + audit Vault | ADR-024 |
|
||||
| **A10: SSRF** | Whitelist URLs + network policies | Fiber middleware |
|
||||
|
||||
### Rate Limiting (Protection DDoS/Brute-force)
|
||||
|
||||
**Configuration** :
|
||||
- Middleware Fiber `limiter` avec backend Redis
|
||||
- Limite : 100 requêtes par minute par IP (global)
|
||||
- Clé de limitation : adresse IP client
|
||||
- Réponse limitation : HTTP 429 "Too many requests"
|
||||
|
||||
**Rate limits par endpoint** :
|
||||
- `/auth/login` : 5 req/min/IP (protection brute-force)
|
||||
- `/moderation/report` : 10 req/24h/user (anti-spam)
|
||||
- API générale : 100 req/min/IP
|
||||
|
||||
## Rotation des Secrets
|
||||
|
||||
**Politique de rotation** :
|
||||
|
||||
| Secret | Rotation | Justification |
|
||||
|--------|----------|---------------|
|
||||
| **JWT signing key** | 1 an | Compromission = invalidation tous tokens |
|
||||
| **DB credentials** | 90 jours | Best practice NIST |
|
||||
| **Mangopay API key** | À la demande | Rotation manuelle si compromission |
|
||||
| **Encryption master key** | Jamais (re-encryption massive) | Backup sécurisé uniquement |
|
||||
|
||||
**Process rotation DB credentials** :
|
||||
- Vault génère automatiquement nouveau password
|
||||
- Vault met à jour PostgreSQL avec nouveau password
|
||||
- Application récupère nouveau password au prochain accès Vault
|
||||
- Ancien password invalide après grace period de 1h
|
||||
|
||||
## Métriques de Succès
|
||||
|
||||
- 0 fuite secrets en production (audit logs Vault)
|
||||
- 100% traffic HTTPS (HTTP → HTTPS redirect)
|
||||
- Rate limiting < 0.1% false positives
|
||||
- Encryption overhead < 2ms p95
|
||||
|
||||
## Migration et Rollout
|
||||
|
||||
### Phase 1 (MVP - Sprint 2-3)
|
||||
1. Deploy Vault (Docker single-node)
|
||||
2. Migrer secrets .env → Vault
|
||||
3. Encryption emails (AES-256-GCM)
|
||||
4. HTTPS Let's Encrypt (api.roadwave.fr)
|
||||
5. Rate limiting Fiber (100 req/min global)
|
||||
|
||||
### Phase 2 (Post-MVP - Sprint 6-8)
|
||||
1. Vault HA (3 nodes Raft)
|
||||
2. Rotation automatique credentials
|
||||
3. Field-level encryption GPS (après 24h)
|
||||
4. WAF (Web Application Firewall) : ModSecurity
|
||||
5. Penetration testing externe (Bug Bounty)
|
||||
|
||||
## Références
|
||||
|
||||
- [ADR-008 : Authentification](008-authentification.md) (Zitadel, JWT)
|
||||
- [ADR-011 : Accès données](011-orm-acces-donnees.md) (sqlc, prepared statements)
|
||||
- [ADR-015 : Hébergement](015-hebergement.md) (OVH France, souveraineté)
|
||||
- [ADR-024 : Monitoring](024-monitoring-observabilite.md) (Audit logs)
|
||||
- [Règle 02 : Conformité RGPD](../domains/_shared/rules/rgpd.md)
|
||||
- [HashiCorp Vault Documentation](https://www.vaultproject.io/docs)
|
||||
- [OWASP Top 10 2021](https://owasp.org/Top10/)
|
||||
- [NIST SP 800-175B (Cryptography)](https://csrc.nist.gov/publications/detail/sp/800-175b/final)
|
||||
@@ -316,4 +316,5 @@ Score final =
|
||||
- [ADR-005 : Base de données](../../adr/005-base-de-donnees.md)
|
||||
- [Redis Geospatial Commands](https://redis.io/docs/data-types/geospatial/)
|
||||
- [PostGIS Documentation](https://postgis.net/documentation/)
|
||||
- [Règles métier : Découverte de contenu géolocalisé](../../regles-metier/03-decouverte-contenu.md)
|
||||
- [Règles métier : Algorithme de recommandation](../../domains/recommendation/rules/algorithme-recommandation.md)
|
||||
- [Règles métier : Centres d'intérêt](../../domains/recommendation/rules/centres-interet-jauges.md)
|
||||
|
||||
@@ -84,7 +84,7 @@ RoadWave est une app audio géolocalisée utilisée en conduite (CarPlay/Android
|
||||
- In-app disclosure obligatoire avant demande "Always"
|
||||
- Flux two-step : When In Use → Always (si user active mode piéton)
|
||||
- Si refusée : app fonctionne en mode voiture uniquement
|
||||
- **Action** : Voir strings détaillés dans [05-interactions-navigation.md](../regles-metier/05-interactions-navigation.md#512-mode-piéton-audio-guides)
|
||||
- **Action** : Voir strings détaillés dans [05-interactions-navigation.md](../domains/recommendation/rules/interactions-navigation.md#512-mode-piéton-audio-guides)
|
||||
|
||||
### Revenus créateurs
|
||||
|
||||
|
||||
249
docs/domains/README.md
Normal file
249
docs/domains/README.md
Normal file
@@ -0,0 +1,249 @@
|
||||
# Context Map RoadWave
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
RoadWave est organisé selon les principes du **Domain-Driven Design (DDD)** avec **7 bounded contexts** clairs. Cette architecture modulaire permet une meilleure séparation des préoccupations, facilite la maintenance et l'évolution du système.
|
||||
|
||||
## Architecture des domaines
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Core Domain"
|
||||
SHARED[_shared<br/>Authentification, RGPD, Erreurs]
|
||||
end
|
||||
|
||||
subgraph "Supporting Subdomains"
|
||||
RECO[recommendation<br/>Jauges & Algorithme]
|
||||
CONTENT[content<br/>Audio-guides & Live]
|
||||
MODERATION[moderation<br/>Signalements & Sanctions]
|
||||
end
|
||||
|
||||
subgraph "Generic Subdomains"
|
||||
ADS[advertising<br/>Publicités]
|
||||
PREMIUM[premium<br/>Abonnements]
|
||||
MONETIZATION[monetization<br/>Monétisation créateurs]
|
||||
end
|
||||
|
||||
%% Dépendances principales
|
||||
RECO --> SHARED
|
||||
RECO --> CONTENT
|
||||
CONTENT --> SHARED
|
||||
ADS --> SHARED
|
||||
ADS --> RECO
|
||||
PREMIUM --> SHARED
|
||||
PREMIUM --> CONTENT
|
||||
MONETIZATION --> SHARED
|
||||
MONETIZATION --> CONTENT
|
||||
MONETIZATION --> ADS
|
||||
MONETIZATION --> PREMIUM
|
||||
MODERATION --> SHARED
|
||||
MODERATION --> CONTENT
|
||||
|
||||
%% Relations anti-corruption
|
||||
ADS -.-|bloqué par| PREMIUM
|
||||
MODERATION -.->|peut démonétiser| MONETIZATION
|
||||
```
|
||||
|
||||
## Bounded Contexts
|
||||
|
||||
### Core Domain
|
||||
|
||||
#### 🔐 [_shared](/_shared/)
|
||||
**Responsabilité** : Fonctionnalités transversales essentielles
|
||||
|
||||
- Authentification et inscription via Zitadel
|
||||
- Conformité RGPD (consentements, suppression données)
|
||||
- Gestion cohérente des erreurs
|
||||
- Entités centrales : `USERS`, `CONTENTS`, `SUBSCRIPTIONS`, `LISTENING_HISTORY`
|
||||
|
||||
**Utilisé par** : Tous les autres domaines
|
||||
|
||||
---
|
||||
|
||||
### Supporting Subdomains
|
||||
|
||||
#### 🎯 [recommendation](/recommendation/)
|
||||
**Responsabilité** : Recommandation géolocalisée de contenus
|
||||
|
||||
- Jauges de centres d'intérêt (scores dynamiques 0-100)
|
||||
- Algorithme de scoring (distance + affinité)
|
||||
- Adaptation selon interactions utilisateur
|
||||
- Entités : `USER_INTERESTS`, `INTEREST_CATEGORIES`
|
||||
|
||||
**Dépend de** : `_shared`, `content`
|
||||
|
||||
**Ubiquitous Language** : Interest Gauge, Recommendation Score, Geographic Priority, Interest Decay
|
||||
|
||||
---
|
||||
|
||||
#### 🎙️ [content](/content/)
|
||||
**Responsabilité** : Création et diffusion de contenus audio
|
||||
|
||||
- Audio-guides multi-séquences géolocalisés
|
||||
- Radio live et enregistrements
|
||||
- Contenus géolocalisés pour voiture/piéton
|
||||
- Détection de contenu protégé (droits d'auteur)
|
||||
- Entités : `AUDIO_GUIDES`, `LIVE_STREAMS`, `GUIDE_SEQUENCES`, `LIVE_RECORDINGS`
|
||||
|
||||
**Dépend de** : `_shared`
|
||||
|
||||
**Interagit avec** : `moderation` (modération), `monetization` (revenus)
|
||||
|
||||
**Ubiquitous Language** : Audio Guide, Guide Sequence, Live Stream, Geofence, Content Fingerprint
|
||||
|
||||
---
|
||||
|
||||
#### 🛡️ [moderation](/moderation/)
|
||||
**Responsabilité** : Modération et sécurité de la plateforme
|
||||
|
||||
- Workflow de traitement des signalements
|
||||
- Système de strikes et sanctions
|
||||
- Processus d'appel
|
||||
- Badges de confiance créateurs
|
||||
- Modération communautaire
|
||||
- Entités : `REPORTS`, `SANCTIONS`, `APPEALS`, `STRIKES`, `BADGES`
|
||||
|
||||
**Dépend de** : `_shared`, `content`
|
||||
|
||||
**Peut affecter** : `monetization` (démonétisation)
|
||||
|
||||
**Ubiquitous Language** : Report, Strike, Sanction, Appeal, Trust Badge, Community Moderation
|
||||
|
||||
---
|
||||
|
||||
### Generic Subdomains
|
||||
|
||||
#### 📢 [advertising](/advertising/)
|
||||
**Responsabilité** : Publicités audio géociblées
|
||||
|
||||
- Campagnes publicitaires
|
||||
- Ciblage géographique et par intérêts
|
||||
- Métriques (impressions, CPM)
|
||||
- Insertion dynamique dans flux audio
|
||||
- Entités : `AD_CAMPAIGNS`, `AD_METRICS`, `AD_IMPRESSIONS`
|
||||
|
||||
**Dépend de** : `_shared`, `recommendation` (ciblage)
|
||||
|
||||
**Bloqué par** : `premium` (pas de pub pour abonnés)
|
||||
|
||||
**Ubiquitous Language** : Ad Campaign, Ad Impression, CPM, Ad Targeting, Skip Rate
|
||||
|
||||
---
|
||||
|
||||
#### 💎 [premium](/premium/)
|
||||
**Responsabilité** : Abonnements et fonctionnalités premium
|
||||
|
||||
- Abonnements payants (mensuel/annuel)
|
||||
- Mode offline (téléchargement, synchro)
|
||||
- Notifications personnalisées
|
||||
- Avantages : sans pub, qualité audio supérieure
|
||||
- Entités : `PREMIUM_SUBSCRIPTIONS`, `ACTIVE_STREAMS`, `OFFLINE_DOWNLOADS`
|
||||
|
||||
**Dépend de** : `_shared`, `content`
|
||||
|
||||
**Bloque** : `advertising` (désactivation pubs)
|
||||
|
||||
**Ubiquitous Language** : Premium Subscription, Offline Download, Sync Queue, Premium Tier, Auto-Renewal
|
||||
|
||||
---
|
||||
|
||||
#### 💰 [monetization](/monetization/)
|
||||
**Responsabilité** : Monétisation des créateurs
|
||||
|
||||
- KYC (vérification identité)
|
||||
- Calcul des revenus (pub + abonnements)
|
||||
- Versements mensuels via Mangopay
|
||||
- Tableaux de bord revenus
|
||||
- Entités : `CREATOR_MONETIZATION`, `REVENUES`, `PAYOUTS`
|
||||
|
||||
**Dépend de** : `_shared`, `content`, `advertising`, `premium`
|
||||
|
||||
**Affecté par** : `moderation` (démonétisation en cas de sanction)
|
||||
|
||||
**Ubiquitous Language** : Revenue Share, KYC Verification, Payout, Minimum Threshold
|
||||
|
||||
---
|
||||
|
||||
## Relations entre domaines
|
||||
|
||||
### Upstream/Downstream
|
||||
|
||||
| Upstream (Fournisseur) | Downstream (Consommateur) | Type de relation |
|
||||
|------------------------|---------------------------|------------------|
|
||||
| `_shared` | Tous | **Kernel partagé** |
|
||||
| `content` | `recommendation` | **Customer/Supplier** |
|
||||
| `recommendation` | `advertising` | **Customer/Supplier** |
|
||||
| `premium` | `advertising` | **Anti-Corruption Layer** |
|
||||
|
||||
### Événements de domaine
|
||||
|
||||
Les domaines communiquent via des événements métier :
|
||||
|
||||
- **UserRegistered** (`_shared` → tous) : Nouvel utilisateur
|
||||
- **ContentPublished** (`content` → `recommendation`, `moderation`) : Nouveau contenu
|
||||
- **InterestGaugeUpdated** (`recommendation` → `advertising`) : Mise à jour jauges
|
||||
- **UserBanned** (`moderation` → `monetization`) : Bannissement utilisateur
|
||||
- **SubscriptionActivated** (`premium` → `advertising`) : Activation premium
|
||||
|
||||
## Structure de la documentation
|
||||
|
||||
Chaque domaine suit cette organisation :
|
||||
|
||||
```
|
||||
domains/<domain>/
|
||||
├── README.md # Vue d'ensemble du domaine
|
||||
├── rules/ # Règles métier (*.md)
|
||||
├── entities/ # Diagrammes entités (*.md)
|
||||
├── sequences/ # Diagrammes séquences (*.md)
|
||||
├── states/ # Diagrammes états (*.md)
|
||||
├── flows/ # Diagrammes flux (*.md)
|
||||
└── features/ # Tests BDD Gherkin (*.feature)
|
||||
```
|
||||
|
||||
## Navigation
|
||||
|
||||
- *(structure legacy, déprécié)*
|
||||
- [🏛️ ADR (Architecture Decision Records)](../adr/)
|
||||
- [⚖️ Documentation légale](../legal/README.md)
|
||||
- [🖥️ Interfaces UI](../interfaces/README.md)
|
||||
- [🔧 Documentation technique](../technical.md)
|
||||
|
||||
## Ubiquitous Language Global
|
||||
|
||||
**Termes transversaux utilisés dans tous les domaines** :
|
||||
|
||||
- **User** : Utilisateur (auditeur, créateur, ou les deux)
|
||||
- **Content** : Contenu audio diffusé sur la plateforme
|
||||
- **Creator** : Utilisateur créant du contenu
|
||||
- **Geolocation** : Position GPS de l'utilisateur
|
||||
- **Stream** : Flux de lecture audio
|
||||
- **Subscription** : Abonnement (à un créateur ou à premium)
|
||||
- **Interest** : Centre d'intérêt (automobile, voyage, musique, etc.)
|
||||
|
||||
## Principes d'architecture
|
||||
|
||||
1. **Bounded Contexts clairs** : Chaque domaine a des limites bien définies
|
||||
2. **Autonomie des domaines** : Chaque domaine peut évoluer indépendamment
|
||||
3. **Communication asynchrone** : Préférence pour les événements vs appels directs
|
||||
4. **Anti-Corruption Layer** : Protection contre les changements externes
|
||||
5. **Alignment code/docs** : Structure docs ↔ structure `backend/internal/`
|
||||
|
||||
## Alignement avec le code backend
|
||||
|
||||
```
|
||||
backend/internal/ docs/domains/
|
||||
├── auth/ ←→ _shared/
|
||||
├── user/ ←→ _shared/
|
||||
├── content/ ←→ content/
|
||||
├── geo/ ←→ recommendation/
|
||||
├── streaming/ ←→ content/
|
||||
├── moderation/ ←→ moderation/
|
||||
├── payment/ ←→ monetization/
|
||||
└── analytics/ ←→ recommendation/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Dernière mise à jour** : 2026-02-07
|
||||
**Statut** : ✅ Active
|
||||
**Auteur** : Documentation DDD initiative
|
||||
37
docs/domains/_shared/README.md
Normal file
37
docs/domains/_shared/README.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Domaine : Shared (Core Domain)
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
Le domaine **Shared** constitue le **Core Domain** de RoadWave. Il contient les fonctionnalités transversales essentielles utilisées par tous les autres bounded contexts de l'application.
|
||||
|
||||
## Responsabilités
|
||||
|
||||
- **Authentification et inscription** : Gestion des comptes utilisateurs, connexion, inscription
|
||||
- **Conformité RGPD** : Respect de la vie privée, consentements, suppression des données
|
||||
- **Gestion des erreurs** : Traitement cohérent des erreurs à travers toute l'application
|
||||
|
||||
## Règles métier
|
||||
|
||||
- [Authentification et inscription](rules/authentification.md)
|
||||
- [Conformité RGPD](rules/rgpd.md)
|
||||
- [Gestion des erreurs](rules/gestion-erreurs.md)
|
||||
- [Annexe Post-MVP](rules/ANNEXE-POST-MVP.md)
|
||||
|
||||
## Modèle de données
|
||||
|
||||
- [Diagramme entités globales](entities/../entities/modele-global.md) - Entités centrales : USERS, CONTENTS, SUBSCRIPTIONS, LISTENING_HISTORY
|
||||
|
||||
## Ubiquitous Language
|
||||
|
||||
**Termes métier du domaine partagé** :
|
||||
- **User** : Utilisateur de la plateforme (auditeur, créateur, ou les deux)
|
||||
- **Content** : Tout contenu audio diffusé sur la plateforme
|
||||
- **Subscription** : Abonnement d'un utilisateur à un créateur ou une catégorie
|
||||
- **Listening History** : Historique d'écoute d'un utilisateur
|
||||
- **Authentication** : Processus de vérification de l'identité via Zitadel
|
||||
- **RGPD Consent** : Consentement explicite pour le traitement des données personnelles
|
||||
|
||||
## Dépendances
|
||||
|
||||
- ✅ Utilisé par : **tous les autres domaines**
|
||||
- ⚠️ Dépend de : aucun (Core Domain)
|
||||
69
docs/domains/_shared/entities/modele-global.md
Normal file
69
docs/domains/_shared/entities/modele-global.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Modèle de données - Entités globales
|
||||
|
||||
📖 Entités de base utilisées dans tous les domaines métier
|
||||
|
||||
## Diagramme
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
USERS ||--o{ CONTENTS : "crée"
|
||||
USERS ||--o{ SUBSCRIPTIONS : "s'abonne à"
|
||||
USERS ||--o{ LISTENING_HISTORY : "écoute"
|
||||
|
||||
CONTENTS ||--o{ LISTENING_HISTORY : "écouté"
|
||||
CONTENTS }o--|| USERS : "créé par"
|
||||
|
||||
USERS {
|
||||
uuid id PK
|
||||
string email UK
|
||||
string pseudo UK
|
||||
date birthdate
|
||||
string role
|
||||
timestamp created_at
|
||||
boolean email_verified
|
||||
}
|
||||
|
||||
CONTENTS {
|
||||
uuid id PK
|
||||
uuid creator_id FK
|
||||
string title
|
||||
string audio_url
|
||||
string status
|
||||
string age_rating
|
||||
string geo_type
|
||||
point geo_location
|
||||
string[] tags
|
||||
int duration_seconds
|
||||
timestamp published_at
|
||||
boolean is_moderated
|
||||
}
|
||||
|
||||
SUBSCRIPTIONS {
|
||||
uuid id PK
|
||||
uuid subscriber_id FK
|
||||
uuid creator_id FK
|
||||
timestamp subscribed_at
|
||||
}
|
||||
|
||||
LISTENING_HISTORY {
|
||||
uuid id PK
|
||||
uuid user_id FK
|
||||
uuid content_id FK
|
||||
uuid creator_id FK
|
||||
boolean is_subscribed
|
||||
decimal completion_rate
|
||||
int last_position_seconds
|
||||
string source
|
||||
boolean auto_like
|
||||
timestamp listened_at
|
||||
}
|
||||
```
|
||||
|
||||
## Légende
|
||||
|
||||
**Entités de base** :
|
||||
|
||||
- **USERS** : Utilisateurs plateforme - Rôles : `listener`, `creator`, `moderator`, `admin`
|
||||
- **CONTENTS** : Contenus audio - Status : `draft`, `pending_review`, `published`, `moderated`, `deleted` - Geo-type : `geo_ancre` (70% geo), `geo_contextuel` (50% geo), `geo_neutre` (20% geo) - Age rating : `all`, `13+`, `16+`, `18+`
|
||||
- **SUBSCRIPTIONS** : Abonnements créateurs - Utilisé pour filtrer recommandations et calculer engagement
|
||||
- **LISTENING_HISTORY** : Historique écoutes - Source : `recommendation`, `search`, `direct_link`, `profile`, `history`, `live_notification`, `audio_guide` - Utilisé pour scoring recommandation et statistiques créateur
|
||||
@@ -0,0 +1,200 @@
|
||||
# language: fr
|
||||
|
||||
@api @authentication @2fa @security @mvp
|
||||
Fonctionnalité: Appareils de confiance et authentification à deux facteurs
|
||||
|
||||
En tant qu'utilisateur soucieux de la sécurité
|
||||
Je veux gérer mes appareils de confiance et activer l'authentification à deux facteurs
|
||||
Afin de protéger mon compte contre les accès non autorisés
|
||||
|
||||
Contexte:
|
||||
Étant donné que le système supporte les méthodes 2FA suivantes:
|
||||
| Méthode | Disponibilité | Recommandée |
|
||||
| Application TOTP | Oui | Oui |
|
||||
| SMS | Oui | Non |
|
||||
| Email | Oui | Non |
|
||||
| Clés de sécurité USB | Phase 2 | Oui |
|
||||
|
||||
Scénario: Activation de l'authentification à deux facteurs par TOTP
|
||||
Étant donné un utilisateur "alice@roadwave.fr" sans 2FA activé
|
||||
Quand l'utilisateur accède à "Mon compte > Sécurité > Authentification à deux facteurs"
|
||||
Et clique sur "Activer l'authentification à deux facteurs"
|
||||
Alors le système génère un QR code avec secret TOTP
|
||||
Et affiche le secret en texte clair pour saisie manuelle
|
||||
Et affiche les instructions: "Scannez ce QR code avec Google Authenticator, Authy ou Microsoft Authenticator"
|
||||
Et l'utilisateur scanne le QR code avec son application TOTP
|
||||
Et saisit le code à 6 chiffres généré par l'application
|
||||
Alors le 2FA est activé
|
||||
Et 10 codes de récupération à usage unique sont générés
|
||||
Et les codes de récupération sont affichés avec avertissement: "Conservez ces codes en lieu sûr"
|
||||
Et un événement "2FA_ENABLED" est enregistré
|
||||
Et un email de confirmation est envoyé
|
||||
Et la métrique "auth.2fa.enabled" est incrémentée
|
||||
|
||||
Scénario: Connexion avec 2FA depuis un nouvel appareil
|
||||
Étant donné un utilisateur "bob@roadwave.fr" avec 2FA activé
|
||||
Et aucun appareil de confiance enregistré
|
||||
Quand l'utilisateur se connecte depuis un iPhone avec email/mot de passe corrects
|
||||
Alors une page de vérification 2FA s'affiche
|
||||
Et l'utilisateur saisit le code à 6 chiffres de son application TOTP
|
||||
Et coche l'option "Faire confiance à cet appareil pour 30 jours"
|
||||
Alors la connexion est réussie
|
||||
Et l'iPhone est enregistré comme appareil de confiance
|
||||
Et un token d'appareil de confiance est stocké localement (durée: 30 jours)
|
||||
Et un événement "2FA_SUCCESS_NEW_TRUSTED_DEVICE" est enregistré
|
||||
Et un email est envoyé: "Nouvel appareil de confiance ajouté: iPhone"
|
||||
Et la métrique "auth.2fa.trusted_device.added" est incrémentée
|
||||
|
||||
Scénario: Connexion depuis un appareil de confiance existant
|
||||
Étant donné un utilisateur "charlie@roadwave.fr" avec 2FA activé
|
||||
Et un iPhone enregistré comme appareil de confiance il y a 10 jours
|
||||
Quand l'utilisateur se connecte depuis cet iPhone avec email/mot de passe corrects
|
||||
Alors la connexion est réussie immédiatement sans demander le code 2FA
|
||||
Et un événement "LOGIN_TRUSTED_DEVICE" est enregistré
|
||||
Et la date de dernière utilisation de l'appareil de confiance est mise à jour
|
||||
Et la métrique "auth.2fa.trusted_device.used" est incrémentée
|
||||
|
||||
Scénario: Expiration automatique d'un appareil de confiance après 30 jours
|
||||
Étant donné un utilisateur "david@roadwave.fr" avec 2FA activé
|
||||
Et un iPad enregistré comme appareil de confiance il y a 31 jours
|
||||
Quand l'utilisateur se connecte depuis cet iPad avec email/mot de passe corrects
|
||||
Alors une page de vérification 2FA s'affiche
|
||||
Et l'utilisateur doit saisir le code TOTP
|
||||
Et un message s'affiche: "Votre appareil de confiance a expiré après 30 jours. Veuillez vous authentifier à nouveau."
|
||||
Et l'ancien token d'appareil de confiance est révoqué
|
||||
Et un événement "TRUSTED_DEVICE_EXPIRED" est enregistré
|
||||
Et la métrique "auth.2fa.trusted_device.expired" est incrémentée
|
||||
|
||||
Scénario: Gestion de la liste des appareils de confiance
|
||||
Étant donné un utilisateur "eve@roadwave.fr" avec 2FA activé
|
||||
Et 3 appareils de confiance enregistrés
|
||||
Quand l'utilisateur accède à "Mon compte > Sécurité > Appareils de confiance"
|
||||
Alors l'utilisateur voit la liste suivante:
|
||||
| Appareil | Ajouté le | Dernière utilisation | Expire le | Actions |
|
||||
| iPhone 14 Pro | 2026-01-15 | Il y a 2 heures | 2026-02-14 | [Révoquer] |
|
||||
| iPad Air | 2026-01-10 | Il y a 5 jours | 2026-02-09 | [Révoquer] |
|
||||
| MacBook Pro | 2026-01-05 | Il y a 10 jours | 2026-02-04 | [Révoquer] |
|
||||
Et un bouton "Révoquer tous les appareils de confiance" est disponible
|
||||
Et un compteur affiche "3 appareils de confiance actifs"
|
||||
|
||||
Scénario: Révocation manuelle d'un appareil de confiance
|
||||
Étant donné un utilisateur "frank@roadwave.fr" avec 2FA activé
|
||||
Et un MacBook Pro enregistré comme appareil de confiance
|
||||
Quand l'utilisateur clique sur "Révoquer" pour le MacBook Pro
|
||||
Alors l'appareil de confiance est immédiatement révoqué
|
||||
Et le token d'appareil de confiance est invalidé
|
||||
Et un événement "TRUSTED_DEVICE_REVOKED_MANUAL" est enregistré
|
||||
Et un email est envoyé: "Vous avez révoqué l'appareil de confiance: MacBook Pro"
|
||||
Et lors de la prochaine connexion, le code 2FA sera demandé
|
||||
Et la métrique "auth.2fa.trusted_device.revoked" est incrémentée
|
||||
|
||||
Scénario: Utilisation d'un code de récupération en cas de perte de l'application TOTP
|
||||
Étant donné un utilisateur "grace@roadwave.fr" avec 2FA activé
|
||||
Et l'utilisateur a perdu l'accès à son application TOTP
|
||||
Et il possède ses codes de récupération
|
||||
Quand l'utilisateur se connecte avec email/mot de passe corrects
|
||||
Et clique sur "Utiliser un code de récupération"
|
||||
Et saisit l'un des 10 codes de récupération
|
||||
Alors la connexion est réussie
|
||||
Et le code de récupération utilisé est marqué comme consommé
|
||||
Et il reste 9 codes de récupération disponibles
|
||||
Et un événement "2FA_RECOVERY_CODE_USED" est enregistré
|
||||
Et un email d'alerte est envoyé: "Un code de récupération a été utilisé. Il vous reste 9 codes."
|
||||
Et l'utilisateur est invité à reconfigurer son 2FA
|
||||
Et la métrique "auth.2fa.recovery_code.used" est incrémentée
|
||||
|
||||
Scénario: Régénération des codes de récupération
|
||||
Étant donné un utilisateur "henry@roadwave.fr" avec 2FA activé
|
||||
Et 3 codes de récupération ont été utilisés
|
||||
Quand l'utilisateur accède à "Mon compte > Sécurité > Codes de récupération"
|
||||
Et clique sur "Régénérer les codes de récupération"
|
||||
Alors un message d'avertissement s'affiche: "Les anciens codes seront invalidés. Êtes-vous sûr ?"
|
||||
Et après confirmation, 10 nouveaux codes de récupération sont générés
|
||||
Et les anciens codes sont invalidés immédiatement
|
||||
Et les nouveaux codes sont affichés une seule fois
|
||||
Et un événement "2FA_RECOVERY_CODES_REGENERATED" est enregistré
|
||||
Et un email est envoyé avec les nouveaux codes (chiffrés)
|
||||
Et la métrique "auth.2fa.recovery_codes.regenerated" est incrémentée
|
||||
|
||||
Scénario: Désactivation du 2FA avec vérification renforcée
|
||||
Étant donné un utilisateur "iris@roadwave.fr" avec 2FA activé
|
||||
Quand l'utilisateur accède à "Mon compte > Sécurité > Authentification à deux facteurs"
|
||||
Et clique sur "Désactiver l'authentification à deux facteurs"
|
||||
Alors un message d'avertissement s'affiche: "Cela réduira la sécurité de votre compte"
|
||||
Et l'utilisateur doit saisir son mot de passe actuel
|
||||
Et l'utilisateur doit saisir un code 2FA valide
|
||||
Et l'utilisateur doit confirmer par email via un lien sécurisé
|
||||
Alors le 2FA est désactivé
|
||||
Et tous les appareils de confiance sont révoqués
|
||||
Et tous les codes de récupération sont invalidés
|
||||
Et un événement "2FA_DISABLED" est enregistré
|
||||
Et un email de confirmation est envoyé
|
||||
Et la métrique "auth.2fa.disabled" est incrémentée
|
||||
|
||||
Scénario: Authentification 2FA par SMS en méthode de secours
|
||||
Étant donné un utilisateur "jack@roadwave.fr" avec 2FA par TOTP activé
|
||||
Et l'utilisateur a également configuré un numéro de téléphone de secours
|
||||
Quand l'utilisateur se connecte et clique sur "Recevoir un code par SMS"
|
||||
Alors un code à 6 chiffres est envoyé au numéro +33612345678
|
||||
Et l'utilisateur saisit le code reçu par SMS
|
||||
Alors la connexion est réussie
|
||||
Et un événement "2FA_SMS_FALLBACK_USED" est enregistré
|
||||
Et un email d'alerte est envoyé: "Vous avez utilisé la méthode SMS de secours"
|
||||
Et la métrique "auth.2fa.sms.used" est incrémentée
|
||||
|
||||
Scénario: Limitation des tentatives de codes 2FA
|
||||
Étant donné un utilisateur "kate@roadwave.fr" avec 2FA activé
|
||||
Quand l'utilisateur se connecte avec email/mot de passe corrects
|
||||
Et saisit 5 codes 2FA incorrects consécutivement
|
||||
Alors le compte est temporairement bloqué pour 15 minutes
|
||||
Et un message s'affiche: "Trop de tentatives échouées. Veuillez réessayer dans 15 minutes."
|
||||
Et un email d'alerte est envoyé: "Multiples tentatives échouées de codes 2FA détectées"
|
||||
Et un événement "2FA_TOO_MANY_ATTEMPTS" est enregistré avec niveau "HIGH"
|
||||
Et la métrique "auth.2fa.blocked.too_many_attempts" est incrémentée
|
||||
|
||||
Scénario: Détection de connexion suspecte malgré 2FA valide
|
||||
Étant donné un utilisateur "luke@roadwave.fr" avec 2FA activé
|
||||
Et toutes ses connexions habituelles sont depuis la France
|
||||
Quand l'utilisateur se connecte avec email/mot de passe corrects depuis la Russie
|
||||
Et saisit un code 2FA valide
|
||||
Alors la connexion est réussie mais marquée comme suspecte
|
||||
Et l'utilisateur reçoit immédiatement un email: "Connexion inhabituelle depuis Russie"
|
||||
Et une notification push est envoyée sur tous les appareils de confiance
|
||||
Et l'accès aux fonctionnalités sensibles (paiement, changement de mot de passe) est temporairement bloqué
|
||||
Et l'utilisateur doit confirmer son identité par email avant accès complet
|
||||
Et un événement "2FA_SUSPICIOUS_LOCATION" est enregistré avec niveau "HIGH"
|
||||
Et la métrique "auth.2fa.suspicious_login" est incrémentée
|
||||
|
||||
Scénario: Révocation de tous les appareils de confiance en cas de compromission
|
||||
Étant donné un utilisateur "mary@roadwave.fr" avec 2FA activé
|
||||
Et 5 appareils de confiance enregistrés
|
||||
Et l'utilisateur suspecte une compromission de son compte
|
||||
Quand l'utilisateur clique sur "Révoquer tous les appareils de confiance"
|
||||
Alors tous les appareils de confiance sont immédiatement révoqués
|
||||
Et tous les tokens d'appareils de confiance sont invalidés
|
||||
Et toutes les sessions actives sont fermées (sauf la session actuelle)
|
||||
Et un événement "ALL_TRUSTED_DEVICES_REVOKED" est enregistré avec niveau "HIGH"
|
||||
Et un email de confirmation est envoyé
|
||||
Et l'utilisateur devra saisir un code 2FA à chaque nouvelle connexion
|
||||
Et la métrique "auth.2fa.trusted_device.bulk_revoked" est incrémentée
|
||||
|
||||
Scénario: Métriques de sécurité pour le 2FA
|
||||
Étant donné que le système gère 50 000 utilisateurs avec 2FA activé
|
||||
Quand les métriques de sécurité sont collectées
|
||||
Alors les indicateurs suivants sont disponibles:
|
||||
| Métrique | Valeur cible |
|
||||
| Pourcentage d'utilisateurs avec 2FA | > 60% |
|
||||
| Taux de succès de validation 2FA | > 98% |
|
||||
| Temps moyen de saisie du code 2FA | < 15s |
|
||||
| Nombre d'appareils de confiance par user | Moyenne: 2.5 |
|
||||
| Taux d'utilisation des codes de récup. | < 0.5% |
|
||||
Et les métriques sont exportées vers le système de monitoring
|
||||
Et des alertes sont déclenchées si le taux de succès < 95%
|
||||
|
||||
Scénario: Badge de sécurité pour utilisateurs avec 2FA activé
|
||||
Étant donné un utilisateur "nathan@roadwave.fr" avec 2FA activé depuis 30 jours
|
||||
Quand l'utilisateur consulte son profil public
|
||||
Alors un badge "Compte sécurisé" s'affiche sur son profil
|
||||
Et le badge indique: "Cet utilisateur a activé l'authentification à deux facteurs"
|
||||
Et le badge améliore la visibilité et la crédibilité du créateur de contenu
|
||||
Et la métrique "profile.badge.2fa_secured" est visible
|
||||
@@ -23,7 +23,7 @@ Fonctionnalité: Classification des contenus par âge
|
||||
Alors la publication échoue
|
||||
Et je vois le message "Vous devez sélectionner une classification d'âge"
|
||||
|
||||
Scénario: Utilisateur 13-15 ans voit uniquement du contenu "Tout public"
|
||||
Scénario: Utilisateur 13-15 ans voit "Tout public" et "13+"
|
||||
Étant donné que je suis un utilisateur de 14 ans
|
||||
Et qu'il existe des contenus avec les classifications suivantes:
|
||||
| classification | nombre |
|
||||
@@ -32,10 +32,10 @@ Fonctionnalité: Classification des contenus par âge
|
||||
| 16+ | 10 |
|
||||
| 18+ | 5 |
|
||||
Quand je demande des recommandations
|
||||
Alors je vois uniquement les 20 contenus "Tout public"
|
||||
Et les autres contenus ne sont jamais proposés
|
||||
Alors je vois 35 contenus (Tout public + 13+)
|
||||
Et les contenus 16+ et 18+ ne sont jamais proposés
|
||||
|
||||
Scénario: Utilisateur 16-17 ans voit "Tout public" et "13+"
|
||||
Scénario: Utilisateur 16-17 ans voit "Tout public", "13+" et "16+"
|
||||
Étant donné que je suis un utilisateur de 17 ans
|
||||
Et qu'il existe des contenus avec les classifications suivantes:
|
||||
| classification | nombre |
|
||||
@@ -44,8 +44,8 @@ Fonctionnalité: Classification des contenus par âge
|
||||
| 16+ | 10 |
|
||||
| 18+ | 5 |
|
||||
Quand je demande des recommandations
|
||||
Alors je vois 35 contenus (Tout public + 13+)
|
||||
Et les contenus 16+ et 18+ ne sont pas proposés
|
||||
Alors je vois 45 contenus (Tout public + 13+ + 16+)
|
||||
Et les contenus 18+ ne sont pas proposés
|
||||
|
||||
Scénario: Utilisateur 18+ voit tous les contenus
|
||||
Étant donné que je suis un utilisateur de 25 ans
|
||||
@@ -54,11 +54,11 @@ Fonctionnalité: Classification des contenus par âge
|
||||
Alors je vois tous les contenus sans restriction
|
||||
Et aucun filtre d'âge n'est appliqué
|
||||
|
||||
Scénario: Mode Kids activé automatiquement pour les moins de 13 ans
|
||||
Scénario: Inscription réussie à 13 ans pile - accès limité à "Tout public" et "13+"
|
||||
Étant donné que je m'inscris avec une date de naissance "2013-01-21"
|
||||
Alors le mode Kids est activé automatiquement
|
||||
Et je vois uniquement du contenu "Tout public"
|
||||
Et des protections supplémentaires sont appliquées
|
||||
Alors mon compte est créé avec succès
|
||||
Et je peux voir les contenus "Tout public" et "13+"
|
||||
Et les contenus 16+ et 18+ ne sont pas accessibles
|
||||
|
||||
Scénario: Modérateur reclassifie un contenu mal catégorisé
|
||||
Étant donné qu'un contenu est publié avec la classification "Tout public"
|
||||
@@ -93,10 +93,11 @@ Fonctionnalité: Classification des contenus par âge
|
||||
| classification |
|
||||
| Tout public |
|
||||
| 13+ |
|
||||
Et je ne vois pas les contenus 16+ et 18+ dans les résultats
|
||||
| 16+ |
|
||||
Et je ne vois pas les contenus 18+ dans les résultats
|
||||
|
||||
Scénario: Notification si tentative d'accès à contenu non autorisé
|
||||
Étant donné que je suis un utilisateur de 14 ans
|
||||
Étant donné que je suis un utilisateur de 15 ans
|
||||
Et qu'un contenu "16+" est partagé avec moi via un lien direct
|
||||
Quand j'essaie d'accéder au contenu
|
||||
Alors l'accès est refusé
|
||||
@@ -0,0 +1,199 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Gestion de compte utilisateur
|
||||
En tant qu'utilisateur connecté
|
||||
Je veux gérer les paramètres de mon compte
|
||||
Afin de maintenir la sécurité et l'exactitude de mes informations
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est disponible
|
||||
Et que je suis connecté avec:
|
||||
| email | user@test.fr |
|
||||
| mot_de_passe | Password123 |
|
||||
|
||||
# ==========================================
|
||||
# Déconnexion
|
||||
# ==========================================
|
||||
|
||||
Scénario: Déconnexion volontaire de l'appareil actuel
|
||||
Quand je clique sur "Se déconnecter"
|
||||
Alors ma session est invalidée immédiatement
|
||||
Et mon refresh token est révoqué
|
||||
Et je suis redirigé vers l'écran de connexion
|
||||
Et je dois me reconnecter pour accéder à l'application
|
||||
|
||||
Scénario: Déconnexion ne révoque pas les autres appareils
|
||||
Étant donné que je suis connecté sur mon iPhone et mon iPad
|
||||
Quand je me déconnecte depuis mon iPhone
|
||||
Alors la session iPhone est invalidée
|
||||
Et ma session iPad reste active
|
||||
Et je peux continuer à utiliser l'application sur iPad
|
||||
|
||||
# ==========================================
|
||||
# Changement de mot de passe
|
||||
# ==========================================
|
||||
|
||||
Scénario: Changement de mot de passe avec ancien mot de passe correct
|
||||
Quand je change mon mot de passe depuis les paramètres avec:
|
||||
| ancien_mot_de_passe | Password123 |
|
||||
| nouveau_mot_de_passe | NewPass456 |
|
||||
| confirmation | NewPass456 |
|
||||
Alors mon mot de passe est modifié avec succès
|
||||
Et je reste connecté sur cet appareil
|
||||
Et tous les autres appareils sont déconnectés
|
||||
Et je reçois un email de confirmation de changement
|
||||
Et je vois le message "Mot de passe modifié avec succès"
|
||||
|
||||
Scénario: Changement de mot de passe avec ancien mot de passe incorrect
|
||||
Quand je change mon mot de passe avec un ancien mot de passe incorrect "WrongPass123"
|
||||
Alors le changement échoue
|
||||
Et je vois le message "Ancien mot de passe incorrect"
|
||||
Et mon mot de passe actuel reste inchangé
|
||||
|
||||
Scénario: Changement de mot de passe avec nouveau mot de passe invalide
|
||||
Quand je change mon mot de passe avec un nouveau mot de passe "faible"
|
||||
Alors le changement échoue
|
||||
Et je vois le message "Le mot de passe doit contenir au moins 8 caractères, 1 majuscule et 1 chiffre"
|
||||
|
||||
Scénario: Changement de mot de passe avec confirmation non correspondante
|
||||
Quand je change mon mot de passe avec:
|
||||
| ancien_mot_de_passe | Password123 |
|
||||
| nouveau_mot_de_passe | NewPass456 |
|
||||
| confirmation | DiffPass789 |
|
||||
Alors le changement échoue
|
||||
Et je vois le message "Les mots de passe ne correspondent pas"
|
||||
|
||||
Scénario: Nouveau mot de passe identique à l'ancien
|
||||
Quand je change mon mot de passe avec:
|
||||
| ancien_mot_de_passe | Password123 |
|
||||
| nouveau_mot_de_passe | Password123 |
|
||||
| confirmation | Password123 |
|
||||
Alors le changement échoue
|
||||
Et je vois le message "Le nouveau mot de passe doit être différent de l'ancien"
|
||||
|
||||
Scénario: Notification sur tous les appareils après changement de mot de passe
|
||||
Étant donné que je suis connecté sur 3 appareils différents
|
||||
Quand je change mon mot de passe depuis mon iPhone
|
||||
Alors je reçois une notification push sur mes 2 autres appareils
|
||||
Et je reçois un email de confirmation avec:
|
||||
| sujet | Votre mot de passe a été modifié |
|
||||
| appareil | iPhone 13 - Safari |
|
||||
| localisation | Paris, France |
|
||||
| action_urgence | Lien pour sécuriser le compte |
|
||||
|
||||
# ==========================================
|
||||
# Changement d'email
|
||||
# ==========================================
|
||||
|
||||
Scénario: Changement d'email avec vérification
|
||||
Quand je change mon email pour "nouveau@test.fr"
|
||||
Alors un email de vérification est envoyé à "nouveau@test.fr"
|
||||
Et mon ancien email "user@test.fr" reste actif pour la connexion
|
||||
Et je vois le message "Email de vérification envoyé à nouveau@test.fr"
|
||||
Et le lien de vérification expire dans 7 jours
|
||||
|
||||
Scénario: Validation du changement d'email
|
||||
Étant donné que j'ai demandé un changement d'email pour "nouveau@test.fr"
|
||||
Et que j'ai reçu le lien de vérification
|
||||
Quand je clique sur le lien de vérification dans l'email
|
||||
Alors mon email est changé pour "nouveau@test.fr"
|
||||
Et je reçois une notification sur l'ancien email "user@test.fr"
|
||||
Et je vois le message "Email modifié avec succès"
|
||||
Et je dois utiliser "nouveau@test.fr" pour me connecter désormais
|
||||
|
||||
Scénario: Changement d'email vers un email déjà utilisé
|
||||
Étant donné qu'un utilisateur existe avec l'email "existant@test.fr"
|
||||
Quand j'essaie de changer mon email pour "existant@test.fr"
|
||||
Alors le changement échoue
|
||||
Et je vois le message "Cet email est déjà utilisé par un autre compte"
|
||||
|
||||
Scénario: Changement d'email avec format invalide
|
||||
Quand j'essaie de changer mon email pour "email.invalide"
|
||||
Alors le changement échoue
|
||||
Et je vois le message "Format d'email invalide"
|
||||
|
||||
Scénario: Expiration du lien de vérification de changement d'email
|
||||
Étant donné que j'ai demandé un changement d'email il y a 8 jours
|
||||
Quand j'essaie d'utiliser le lien de vérification
|
||||
Alors la vérification échoue
|
||||
Et je vois le message "Ce lien a expiré"
|
||||
Et mon email reste inchangé à "user@test.fr"
|
||||
Et je peux demander un nouveau changement d'email
|
||||
|
||||
Scénario: Annulation du changement d'email avant vérification
|
||||
Étant donné que j'ai demandé un changement d'email pour "nouveau@test.fr"
|
||||
Et que je n'ai pas encore vérifié le nouveau email
|
||||
Quand je demande à annuler le changement d'email
|
||||
Alors la demande de changement est annulée
|
||||
Et le lien de vérification est invalidé
|
||||
Et mon email reste "user@test.fr"
|
||||
|
||||
Scénario: Limite de changements d'email
|
||||
Étant donné que j'ai déjà changé mon email 2 fois dans les 30 derniers jours
|
||||
Quand j'essaie de changer mon email une 3ème fois
|
||||
Alors le changement échoue
|
||||
Et je vois le message "Maximum 2 changements d'email par mois"
|
||||
|
||||
Scénario: Notification de sécurité sur l'ancien email
|
||||
Étant donné que j'ai changé mon email de "ancien@test.fr" à "nouveau@test.fr"
|
||||
Alors je reçois un email sur "ancien@test.fr" avec:
|
||||
| sujet | Votre adresse email a été modifiée |
|
||||
| contenu | Votre email de connexion est maintenant nouveau@test.fr |
|
||||
| date_heure | présente |
|
||||
| appareil | présent |
|
||||
| action_urgence | Lien pour annuler si ce n'était pas vous |
|
||||
|
||||
# ==========================================
|
||||
# Changement de pseudo
|
||||
# ==========================================
|
||||
|
||||
Scénario: Changement de pseudo valide
|
||||
Quand je change mon pseudo pour "nouveau_pseudo"
|
||||
Alors mon pseudo est modifié avec succès
|
||||
Et je vois le message "Pseudo modifié avec succès"
|
||||
Et le nouveau pseudo apparaît sur mon profil
|
||||
|
||||
Scénario: Changement de pseudo invalide - trop court
|
||||
Quand j'essaie de changer mon pseudo pour "ab"
|
||||
Alors le changement échoue
|
||||
Et je vois le message "Le pseudo doit contenir entre 3 et 30 caractères"
|
||||
|
||||
Scénario: Changement de pseudo invalide - caractères spéciaux
|
||||
Quand j'essaie de changer mon pseudo pour "user@123"
|
||||
Alors le changement échoue
|
||||
Et je vois le message "Le pseudo ne peut contenir que des lettres, chiffres et underscores"
|
||||
|
||||
Scénario: Changement de pseudo déjà utilisé
|
||||
Étant donné qu'un utilisateur existe avec le pseudo "pseudo_existant"
|
||||
Quand j'essaie de changer mon pseudo pour "pseudo_existant"
|
||||
Alors le changement échoue
|
||||
Et je vois le message "Ce pseudo est déjà utilisé"
|
||||
|
||||
Scénario: Limite de changements de pseudo
|
||||
Étant donné que j'ai changé mon pseudo il y a 15 jours
|
||||
Quand j'essaie de changer mon pseudo à nouveau
|
||||
Alors le changement échoue
|
||||
Et je vois le message "Vous ne pouvez changer votre pseudo qu'une fois par mois"
|
||||
|
||||
# ==========================================
|
||||
# Consultation des informations de compte
|
||||
# ==========================================
|
||||
|
||||
Scénario: Consulter les informations de mon compte
|
||||
Quand je consulte les paramètres de mon compte
|
||||
Alors je vois les informations suivantes:
|
||||
| champ | valeur |
|
||||
| Email | user@test.fr |
|
||||
| Pseudo | user_test |
|
||||
| Date création | 15/01/2026 |
|
||||
| Email vérifié | Oui |
|
||||
| 2FA activée | Non |
|
||||
| Abonnement | Gratuit |
|
||||
|
||||
Scénario: Historique des changements de sécurité
|
||||
Quand je consulte l'historique de sécurité de mon compte
|
||||
Alors je vois la liste des événements suivants:
|
||||
| événement | date | appareil |
|
||||
| Changement mot de passe | 01/02/2026 | iPhone 13 |
|
||||
| Activation 2FA | 25/01/2026 | iPad Pro |
|
||||
| Changement email | 20/01/2026 | PC Windows |
|
||||
| Création compte | 15/01/2026 | iPhone 13 |
|
||||
@@ -73,7 +73,7 @@ Fonctionnalité: Inscription utilisateur
|
||||
Étant donné la date du jour est "2026-01-21"
|
||||
Quand je m'inscris avec une date de naissance "2013-01-21"
|
||||
Alors mon compte est créé avec succès
|
||||
Et le mode Kids est activé automatiquement
|
||||
Et je peux accéder aux contenus "Tout public" et "13+"
|
||||
|
||||
Scénario: Inscription avec âge supérieur à 18 ans
|
||||
Étant donné la date du jour est "2026-01-21"
|
||||
@@ -0,0 +1,171 @@
|
||||
# language: fr
|
||||
|
||||
@api @authentication @security @mvp
|
||||
Fonctionnalité: Limitation des tentatives de connexion
|
||||
|
||||
En tant que système de sécurité
|
||||
Je veux limiter les tentatives de connexion échouées
|
||||
Afin de protéger les comptes utilisateurs contre les attaques par force brute
|
||||
|
||||
Contexte:
|
||||
Étant donné que le système est configuré avec les limites suivantes:
|
||||
| Paramètre | Valeur |
|
||||
| Tentatives max avant blocage | 5 |
|
||||
| Durée de blocage temporaire | 15 min |
|
||||
| Tentatives max avant blocage 24h | 10 |
|
||||
| Durée de blocage prolongé | 24h |
|
||||
| Fenêtre de temps pour reset | 30 min |
|
||||
|
||||
Scénario: Connexion réussie réinitialise le compteur de tentatives
|
||||
Étant donné un utilisateur "alice@roadwave.fr" avec 3 tentatives échouées
|
||||
Quand l'utilisateur se connecte avec les bons identifiants
|
||||
Alors la connexion est réussie
|
||||
Et le compteur de tentatives échouées est réinitialisé à 0
|
||||
Et un événement de sécurité "LOGIN_SUCCESS_AFTER_FAILURES" est enregistré
|
||||
|
||||
Scénario: Blocage temporaire après 5 tentatives échouées
|
||||
Étant donné un utilisateur "bob@roadwave.fr" avec 4 tentatives échouées
|
||||
Quand l'utilisateur tente de se connecter avec un mauvais mot de passe
|
||||
Alors la connexion échoue avec le code d'erreur "ACCOUNT_TEMPORARILY_LOCKED"
|
||||
Et le message est "Votre compte est temporairement verrouillé pour 15 minutes suite à de multiples tentatives échouées"
|
||||
Et un email de notification de sécurité est envoyé à "bob@roadwave.fr"
|
||||
Et un événement de sécurité "ACCOUNT_LOCKED_TEMP" est enregistré
|
||||
Et la métrique "security.account_locks.temporary" est incrémentée
|
||||
|
||||
Scénario: Tentative de connexion pendant le blocage temporaire
|
||||
Étant donné un utilisateur "charlie@roadwave.fr" bloqué temporairement
|
||||
Et il reste 10 minutes avant la fin du blocage
|
||||
Quand l'utilisateur tente de se connecter avec les bons identifiants
|
||||
Alors la connexion échoue avec le code d'erreur "ACCOUNT_TEMPORARILY_LOCKED"
|
||||
Et le message contient "Votre compte reste verrouillé pour 10 minutes"
|
||||
Et le temps de blocage restant est indiqué en minutes
|
||||
Et la tentative ne rallonge pas la durée du blocage
|
||||
|
||||
Scénario: Connexion autorisée après expiration du blocage temporaire
|
||||
Étant donné un utilisateur "david@roadwave.fr" bloqué temporairement il y a 16 minutes
|
||||
Quand l'utilisateur tente de se connecter avec les bons identifiants
|
||||
Alors la connexion est réussie
|
||||
Et le compteur de tentatives échouées est réinitialisé à 0
|
||||
Et le statut de blocage est levé
|
||||
Et un événement de sécurité "ACCOUNT_UNLOCKED_AUTO" est enregistré
|
||||
|
||||
Scénario: Blocage prolongé après 10 tentatives échouées sur 24h
|
||||
Étant donné un utilisateur "eve@roadwave.fr" avec historique:
|
||||
| Tentatives échouées | Quand |
|
||||
| 5 | Il y a 2 heures |
|
||||
| Blocage 15min levé | Il y a 1h30 |
|
||||
| 4 | Il y a 30 minutes |
|
||||
Quand l'utilisateur tente une nouvelle connexion échouée
|
||||
Alors la connexion échoue avec le code d'erreur "ACCOUNT_LOCKED_24H"
|
||||
Et le message est "Votre compte est verrouillé pour 24 heures suite à de multiples tentatives suspectes"
|
||||
Et un email urgent de sécurité est envoyé avec un lien de déblocage sécurisé
|
||||
Et une notification SMS est envoyée (si configuré)
|
||||
Et un événement de sécurité "ACCOUNT_LOCKED_24H" est enregistré avec niveau "HIGH"
|
||||
Et la métrique "security.account_locks.prolonged" est incrémentée
|
||||
|
||||
Scénario: Blocage différencié par adresse IP
|
||||
Étant donné un utilisateur "frank@roadwave.fr" avec 3 tentatives échouées depuis IP "1.2.3.4"
|
||||
Quand l'utilisateur se connecte avec succès depuis IP "5.6.7.8"
|
||||
Alors la connexion est réussie
|
||||
Et le compteur de tentatives échouées pour IP "1.2.3.4" reste à 3
|
||||
Et le compteur de tentatives échouées pour IP "5.6.7.8" est à 0
|
||||
Et un événement de sécurité "LOGIN_FROM_NEW_IP" est enregistré
|
||||
|
||||
Scénario: Alerte de sécurité sur pattern suspect multi-IP
|
||||
Étant donné un utilisateur "grace@roadwave.fr"
|
||||
Quand 5 tentatives échouées sont détectées depuis 5 IP différentes en 10 minutes:
|
||||
| IP | Tentatives | Timestamp |
|
||||
| 1.2.3.4 | 2 | Il y a 10 min |
|
||||
| 5.6.7.8 | 1 | Il y a 8 min |
|
||||
| 9.10.11.12 | 1 | Il y a 5 min |
|
||||
| 13.14.15.16| 1 | Il y a 2 min |
|
||||
Alors le compte est immédiatement bloqué pour 24h
|
||||
Et un email d'alerte critique "POSSIBLE_CREDENTIAL_STUFFING_ATTACK" est envoyé
|
||||
Et l'équipe de sécurité est notifiée via webhook
|
||||
Et toutes les sessions actives sont révoquées
|
||||
Et la métrique "security.attacks.credential_stuffing.detected" est incrémentée
|
||||
|
||||
Scénario: Déblocage manuel par l'utilisateur via email sécurisé
|
||||
Étant donné un utilisateur "henry@roadwave.fr" bloqué pour 24h
|
||||
Et il a reçu un email avec un lien de déblocage sécurisé à usage unique
|
||||
Quand l'utilisateur clique sur le lien dans les 2 heures suivant l'email
|
||||
Et confirme son identité via un code envoyé par SMS
|
||||
Alors le compte est débloqué immédiatement
|
||||
Et l'utilisateur est invité à changer son mot de passe
|
||||
Et un événement de sécurité "ACCOUNT_UNLOCKED_MANUAL" est enregistré
|
||||
Et la métrique "security.account_unlocks.user_initiated" est incrémentée
|
||||
|
||||
Scénario: Réinitialisation automatique du compteur après période d'inactivité
|
||||
Étant donné un utilisateur "iris@roadwave.fr" avec 3 tentatives échouées
|
||||
Et aucune nouvelle tentative depuis 35 minutes
|
||||
Quand l'utilisateur tente de se connecter avec un mauvais mot de passe
|
||||
Alors le compteur de tentatives est réinitialisé à 1
|
||||
Et le message d'erreur est standard sans mention de blocage imminent
|
||||
Et un événement de sécurité "ATTEMPT_COUNTER_RESET" est enregistré
|
||||
|
||||
Scénario: Protection contre les attaques par timing
|
||||
Étant donné un utilisateur "jack@roadwave.fr"
|
||||
Quand l'utilisateur effectue 10 tentatives de connexion échouées
|
||||
Alors chaque réponse HTTP prend entre 800ms et 1200ms (temps constant)
|
||||
Et les messages d'erreur ne révèlent pas si l'email existe
|
||||
Et la métrique "security.timing_protection.applied" est incrémentée
|
||||
Et les logs n'exposent pas de patterns de timing exploitables
|
||||
|
||||
Scénario: Escalade des notifications avec tentatives répétées
|
||||
Étant donné un utilisateur "kate@roadwave.fr" Premium
|
||||
Quand les événements suivants se produisent:
|
||||
| Événement | Notification |
|
||||
| 3 tentatives échouées | Aucune notification |
|
||||
| 5 tentatives (blocage) | Email standard |
|
||||
| 10 tentatives (24h) | Email + SMS + notification app|
|
||||
| Tentative pendant 24h | Email urgent + alerte support |
|
||||
Alors chaque niveau de notification est proportionnel à la gravité
|
||||
Et l'utilisateur peut configurer ses préférences de notification
|
||||
Et la métrique "security.notifications.escalated" est incrémentée
|
||||
|
||||
Scénario: Whitelist d'IP pour utilisateurs de confiance
|
||||
Étant donné un utilisateur "luke@roadwave.fr" avec IP de confiance "1.2.3.4"
|
||||
Et la whitelist est configurée pour autoriser 10 tentatives au lieu de 5
|
||||
Quand l'utilisateur effectue 7 tentatives échouées depuis "1.2.3.4"
|
||||
Alors le compte n'est pas bloqué
|
||||
Et un avertissement est affiché "3 tentatives restantes avant blocage"
|
||||
Et un événement de sécurité "TRUSTED_IP_EXTENDED_ATTEMPTS" est enregistré
|
||||
|
||||
Scénario: Logs de sécurité détaillés pour audit
|
||||
Étant donné un utilisateur "mary@roadwave.fr" avec tentatives échouées
|
||||
Quand un audit de sécurité est effectué
|
||||
Alors les logs contiennent pour chaque tentative:
|
||||
| Champ | Exemple |
|
||||
| Timestamp | 2026-02-03T14:32:18.123Z |
|
||||
| User ID | uuid-123-456 |
|
||||
| Email | mary@roadwave.fr |
|
||||
| IP Address | 1.2.3.4 |
|
||||
| User Agent | Mozilla/5.0 (iPhone...) |
|
||||
| Failure Reason | INVALID_PASSWORD |
|
||||
| Attempts Count | 3 |
|
||||
| Geolocation | Paris, France |
|
||||
| Device Fingerprint| hash-abc-def |
|
||||
Et les logs sont conservés pendant 90 jours minimum
|
||||
Et les logs sont conformes RGPD (pas de mots de passe en clair)
|
||||
|
||||
Scénario: Métriques de performance du système de limitation
|
||||
Étant donné que le système traite 1000 tentatives de connexion par minute
|
||||
Quand les métriques de performance sont collectées
|
||||
Alors les indicateurs suivants sont disponibles:
|
||||
| Métrique | Valeur cible |
|
||||
| Temps de vérification du compteur | < 50ms |
|
||||
| Latence ajoutée par le rate limiting | < 100ms |
|
||||
| Pourcentage de tentatives bloquées | < 2% |
|
||||
| Faux positifs (utilisateurs légitimes) | < 0.1% |
|
||||
| Temps de déblocage automatique | < 1s |
|
||||
Et les métriques sont exportées vers le système de monitoring
|
||||
Et des alertes sont déclenchées si les seuils sont dépassés
|
||||
|
||||
Scénario: Compatibilité avec authentification multi-facteurs
|
||||
Étant donné un utilisateur "nathan@roadwave.fr" avec 2FA activé
|
||||
Et il a 4 tentatives échouées (mot de passe correct mais code 2FA incorrect)
|
||||
Quand l'utilisateur tente une 5ème connexion avec mot de passe correct et mauvais code 2FA
|
||||
Alors le compte est bloqué temporairement
|
||||
Et le message précise "Blocage suite à de multiples erreurs de code 2FA"
|
||||
Et le compteur 2FA est distinct du compteur de mot de passe
|
||||
Et un événement de sécurité "2FA_LOCK_TRIGGERED" est enregistré
|
||||
@@ -0,0 +1,191 @@
|
||||
# language: fr
|
||||
|
||||
@api @authentication @sessions @mvp
|
||||
Fonctionnalité: Gestion des sessions multi-appareils
|
||||
|
||||
En tant qu'utilisateur
|
||||
Je veux gérer mes sessions actives sur plusieurs appareils
|
||||
Afin de contrôler l'accès à mon compte et améliorer la sécurité
|
||||
|
||||
Contexte:
|
||||
Étant donné que le système supporte les sessions suivantes:
|
||||
| Paramètre | Valeur |
|
||||
| Nombre max de sessions simultanées | 5 |
|
||||
| Durée de vie d'une session | 30 jours |
|
||||
| Durée d'inactivité avant expiration | 7 jours |
|
||||
| Durée du token de refresh | 90 jours |
|
||||
| Taille max du stockage de session | 10 KB |
|
||||
|
||||
Scénario: Création d'une nouvelle session avec empreinte d'appareil
|
||||
Étant donné un utilisateur "alice@roadwave.fr" non connecté
|
||||
Quand l'utilisateur se connecte depuis un iPhone 14 Pro avec iOS 17.2
|
||||
Alors une nouvelle session est créée avec les métadonnées:
|
||||
| Champ | Valeur |
|
||||
| Device Type | mobile |
|
||||
| OS | iOS 17.2 |
|
||||
| App Version | 1.2.3 |
|
||||
| Device Model | iPhone 14 Pro |
|
||||
| Browser | N/A |
|
||||
| IP Address | 1.2.3.4 |
|
||||
| Geolocation | Paris, France |
|
||||
| Created At | 2026-02-03T14:32:18Z |
|
||||
| Last Activity | 2026-02-03T14:32:18Z |
|
||||
Et un token JWT avec durée de vie de 30 jours est généré
|
||||
Et un refresh token avec durée de vie de 90 jours est généré
|
||||
Et un événement "SESSION_CREATED" est enregistré
|
||||
Et la métrique "sessions.created" est incrémentée
|
||||
|
||||
Scénario: Connexion simultanée sur plusieurs appareils
|
||||
Étant donné un utilisateur "bob@roadwave.fr" connecté sur:
|
||||
| Appareil | OS | Dernière activité |
|
||||
| iPhone 13 | iOS 16.5 | Il y a 5 min |
|
||||
| iPad Pro | iPadOS 17.1 | Il y a 2 heures |
|
||||
| MacBook Pro | macOS 14.2 | Il y a 1 jour |
|
||||
Quand l'utilisateur se connecte depuis un Samsung Galaxy S23
|
||||
Alors une nouvelle session est créée
|
||||
Et l'utilisateur a maintenant 4 sessions actives
|
||||
Et toutes les sessions précédentes restent valides
|
||||
Et un événement "NEW_DEVICE_LOGIN" est enregistré
|
||||
Et une notification push est envoyée sur tous les appareils: "Nouvelle connexion depuis Samsung Galaxy S23"
|
||||
|
||||
Scénario: Limitation du nombre de sessions simultanées
|
||||
Étant donné un utilisateur "charlie@roadwave.fr" avec 5 sessions actives
|
||||
Quand l'utilisateur se connecte depuis un 6ème appareil
|
||||
Alors la session la plus ancienne est automatiquement révoquée
|
||||
Et une nouvelle session est créée pour le nouvel appareil
|
||||
Et l'utilisateur reçoit une notification: "Votre session sur [Ancien Appareil] a été fermée automatiquement"
|
||||
Et un événement "SESSION_EVICTED_MAX_LIMIT" est enregistré
|
||||
Et la métrique "sessions.evicted.max_limit" est incrémentée
|
||||
|
||||
Scénario: Liste des sessions actives dans les paramètres du compte
|
||||
Étant donné un utilisateur "david@roadwave.fr" avec 3 sessions actives
|
||||
Quand l'utilisateur accède à "Mon compte > Sécurité > Appareils connectés"
|
||||
Alors l'utilisateur voit la liste suivante:
|
||||
| Appareil | Localisation | Dernière activité | IP | Actions |
|
||||
| iPhone 14 Pro | Paris, France | Actif maintenant | 1.2.3.4 | [Cet appareil]|
|
||||
| iPad Air | Lyon, France | Il y a 2 heures | 5.6.7.8 | [Déconnecter] |
|
||||
| MacBook Pro | Marseille, FR | Il y a 3 jours | 9.10.11.12| [Déconnecter] |
|
||||
Et la session actuelle est clairement identifiée
|
||||
Et un bouton "Déconnecter tous les autres appareils" est disponible
|
||||
|
||||
Scénario: Révocation manuelle d'une session spécifique
|
||||
Étant donné un utilisateur "eve@roadwave.fr" avec 4 sessions actives
|
||||
Et il consulte la liste de ses appareils depuis son iPhone
|
||||
Quand l'utilisateur clique sur "Déconnecter" pour la session "MacBook Pro"
|
||||
Alors la session "MacBook Pro" est immédiatement révoquée
|
||||
Et le token JWT associé est invalidé dans Redis
|
||||
Et le refresh token est révoqué
|
||||
Et l'utilisateur sur le MacBook Pro est déconnecté lors de sa prochaine requête
|
||||
Et un événement "SESSION_REVOKED_MANUAL" est enregistré
|
||||
Et une notification est envoyée: "Vous avez été déconnecté de votre MacBook Pro"
|
||||
|
||||
Scénario: Déconnexion de tous les autres appareils
|
||||
Étant donné un utilisateur "frank@roadwave.fr" avec 5 sessions actives
|
||||
Et il suspecte un accès non autorisé
|
||||
Quand l'utilisateur clique sur "Déconnecter tous les autres appareils" depuis son iPhone
|
||||
Alors toutes les sessions sauf la session actuelle (iPhone) sont révoquées
|
||||
Et 4 tokens JWT sont invalidés
|
||||
Et 4 refresh tokens sont révoqués
|
||||
Et un événement "SESSIONS_REVOKED_ALL_OTHER" est enregistré
|
||||
Et une notification est envoyée sur tous les appareils déconnectés
|
||||
Et un email de confirmation est envoyé: "Vous avez déconnecté tous vos autres appareils"
|
||||
Et la métrique "sessions.revoked.bulk" est incrémentée
|
||||
|
||||
Scénario: Détection de connexion suspecte depuis un nouveau pays
|
||||
Étant donné un utilisateur "grace@roadwave.fr" avec sessions habituelles en France
|
||||
Quand l'utilisateur se connecte depuis une IP en Russie
|
||||
Alors une alerte de sécurité est déclenchée
|
||||
Et un email est envoyé: "Connexion détectée depuis Russie - Est-ce bien vous ?"
|
||||
Et une notification push est envoyée sur tous les appareils de confiance
|
||||
Et la session est créée mais marquée comme "suspecte"
|
||||
Et un événement "SUSPICIOUS_LOCATION_LOGIN" est enregistré avec niveau "HIGH"
|
||||
Et l'utilisateur doit confirmer son identité par code SMS avant d'accéder aux fonctionnalités sensibles
|
||||
|
||||
Scénario: Expiration automatique d'une session inactive
|
||||
Étant donné un utilisateur "henry@roadwave.fr" avec une session sur iPad
|
||||
Et la session n'a pas été utilisée depuis 8 jours
|
||||
Quand le job de nettoyage des sessions s'exécute
|
||||
Alors la session iPad est automatiquement révoquée
|
||||
Et le token JWT est invalidé
|
||||
Et le refresh token est révoqué
|
||||
Et un événement "SESSION_EXPIRED_INACTIVITY" est enregistré
|
||||
Et un email est envoyé: "Votre session sur iPad a expiré suite à 8 jours d'inactivité"
|
||||
Et la métrique "sessions.expired.inactivity" est incrémentée
|
||||
|
||||
Scénario: Rafraîchissement automatique du token avant expiration
|
||||
Étant donné un utilisateur "iris@roadwave.fr" avec une session active
|
||||
Et le token JWT expire dans 2 minutes
|
||||
Quand l'application mobile effectue une requête API
|
||||
Alors l'API détecte que le token expire bientôt
|
||||
Et un nouveau token JWT est généré automatiquement
|
||||
Et le nouveau token est retourné dans le header "X-Refreshed-Token"
|
||||
Et l'application mobile stocke le nouveau token
|
||||
Et un événement "TOKEN_REFRESHED" est enregistré
|
||||
Et la métrique "tokens.refreshed" est incrémentée
|
||||
|
||||
Scénario: Révocation de toutes les sessions lors d'un changement de mot de passe
|
||||
Étant donné un utilisateur "jack@roadwave.fr" avec 4 sessions actives
|
||||
Quand l'utilisateur change son mot de passe depuis son iPhone
|
||||
Alors toutes les sessions sauf la session actuelle (iPhone) sont révoquées
|
||||
Et tous les tokens JWT sont invalidés
|
||||
Et tous les refresh tokens sont révoqués
|
||||
Et un événement "SESSIONS_REVOKED_PASSWORD_CHANGE" est enregistré
|
||||
Et un email est envoyé: "Votre mot de passe a été modifié. Toutes vos autres sessions ont été déconnectées."
|
||||
Et des notifications push sont envoyées sur tous les appareils déconnectés
|
||||
|
||||
Scénario: Persistance de la session avec "Se souvenir de moi"
|
||||
Étant donné un utilisateur "kate@roadwave.fr" qui se connecte
|
||||
Quand l'utilisateur coche l'option "Se souvenir de moi"
|
||||
Alors la durée de vie du token JWT est étendue à 90 jours
|
||||
Et la durée de vie du refresh token est étendue à 180 jours
|
||||
Et la session persiste même après fermeture de l'application
|
||||
Et un cookie sécurisé "remember_token" est stocké (pour web)
|
||||
Et un événement "LONG_SESSION_CREATED" est enregistré
|
||||
Et la métrique "sessions.remember_me.enabled" est incrémentée
|
||||
|
||||
Scénario: Détection de vol de token et révocation automatique
|
||||
Étant donné un utilisateur "luke@roadwave.fr" avec une session active
|
||||
Et le token JWT a été volé et utilisé depuis une IP différente
|
||||
Quand le système détecte une utilisation simultanée du même token depuis 2 IP différentes
|
||||
Alors toutes les sessions de l'utilisateur sont immédiatement révoquées
|
||||
Et tous les tokens sont invalidés
|
||||
Et un email d'alerte critique est envoyé: "Activité suspecte détectée - Toutes vos sessions ont été fermées"
|
||||
Et une notification push urgente est envoyée sur tous les appareils
|
||||
Et l'utilisateur doit réinitialiser son mot de passe avant de se reconnecter
|
||||
Et un événement "TOKEN_THEFT_DETECTED" est enregistré avec niveau "CRITICAL"
|
||||
Et l'équipe de sécurité est alertée via webhook
|
||||
|
||||
Scénario: Synchronisation des informations de session en temps réel
|
||||
Étant donné un utilisateur "mary@roadwave.fr" connecté sur 3 appareils
|
||||
Quand l'utilisateur révoque une session depuis son iPhone
|
||||
Alors la liste des sessions est mise à jour en temps réel sur tous les appareils via WebSocket
|
||||
Et l'appareil déconnecté reçoit immédiatement une notification de déconnexion
|
||||
Et l'UI est rafraîchie automatiquement sur tous les appareils connectés
|
||||
Et la métrique "sessions.realtime_sync" est incrémentée
|
||||
|
||||
Scénario: Métriques de performance de gestion des sessions
|
||||
Étant donné que le système gère 100 000 sessions actives
|
||||
Quand les métriques de performance sont collectées
|
||||
Alors les indicateurs suivants sont respectés:
|
||||
| Métrique | Valeur cible |
|
||||
| Temps de création de session | < 50ms |
|
||||
| Temps de validation de token | < 20ms |
|
||||
| Temps de révocation de session | < 100ms |
|
||||
| Latence de synchronisation temps réel | < 500ms |
|
||||
| Taux de succès du refresh automatique | > 99.9% |
|
||||
Et les métriques sont exportées vers le système de monitoring
|
||||
Et des alertes sont déclenchées si les seuils sont dépassés
|
||||
|
||||
Scénario: Stockage sécurisé des sessions dans Redis
|
||||
Étant donné un utilisateur "nathan@roadwave.fr" avec une session active
|
||||
Quand la session est stockée dans Redis
|
||||
Alors les données suivantes sont chiffrées:
|
||||
| Champ | Chiffrement |
|
||||
| User ID | Hash |
|
||||
| Refresh Token | AES-256 |
|
||||
| Device Info | Non |
|
||||
| IP Address | Hash |
|
||||
Et la clé Redis a un TTL correspondant à la durée de vie de la session
|
||||
Et les données sensibles ne sont jamais loggées en clair
|
||||
Et les accès à Redis sont audités
|
||||
Et la métrique "sessions.storage.encrypted" est incrémentée
|
||||
@@ -0,0 +1,187 @@
|
||||
# language: fr
|
||||
|
||||
@api @authentication @security @mvp
|
||||
Fonctionnalité: Récupération et réinitialisation avancée du mot de passe
|
||||
|
||||
En tant qu'utilisateur ayant oublié son mot de passe
|
||||
Je veux pouvoir récupérer l'accès à mon compte de manière sécurisée
|
||||
Afin de reprendre l'utilisation de l'application
|
||||
|
||||
Contexte:
|
||||
Étant donné que le système de récupération est configuré avec:
|
||||
| Paramètre | Valeur |
|
||||
| Durée de validité du lien de reset | 1 heure |
|
||||
| Nombre max de demandes par heure | 3 |
|
||||
| Nombre max de demandes par jour | 10 |
|
||||
| Longueur du token de reset | 64 chars |
|
||||
| Délai de cooldown entre demandes | 5 minutes |
|
||||
|
||||
Scénario: Demande de réinitialisation de mot de passe
|
||||
Étant donné un utilisateur "alice@roadwave.fr" qui a oublié son mot de passe
|
||||
Quand l'utilisateur clique sur "Mot de passe oublié ?" sur l'écran de connexion
|
||||
Et saisit son adresse email "alice@roadwave.fr"
|
||||
Alors un email de réinitialisation est envoyé avec:
|
||||
| Élément | Contenu |
|
||||
| Sujet | Réinitialisation de votre mot de passe RoadWave |
|
||||
| Lien sécurisé | https://roadwave.fr/reset?token=abc123... |
|
||||
| Durée de validité | Ce lien expire dans 1 heure |
|
||||
| Warning sécurité | Si vous n'êtes pas à l'origine de cette demande... |
|
||||
Et un événement "PASSWORD_RESET_REQUESTED" est enregistré
|
||||
Et la métrique "auth.password_reset.requested" est incrémentée
|
||||
Et un message s'affiche: "Si cette adresse est enregistrée, vous recevrez un email de réinitialisation"
|
||||
|
||||
Scénario: Protection contre l'énumération d'adresses email
|
||||
Étant donné une adresse email "inexistant@roadwave.fr" non enregistrée
|
||||
Quand un utilisateur demande la réinitialisation pour cette adresse
|
||||
Alors le même message de confirmation s'affiche: "Si cette adresse est enregistrée, vous recevrez un email"
|
||||
Et aucun email n'est envoyé
|
||||
Et le temps de réponse est identique à une demande valide (800-1200ms)
|
||||
Et un événement "PASSWORD_RESET_UNKNOWN_EMAIL" est enregistré
|
||||
Et la métrique "auth.password_reset.unknown_email" est incrémentée
|
||||
Et les logs n'exposent pas l'information de l'existence ou non de l'email
|
||||
|
||||
Scénario: Limitation du nombre de demandes de réinitialisation
|
||||
Étant donné un utilisateur "bob@roadwave.fr"
|
||||
Et il a déjà effectué 3 demandes de réinitialisation dans la dernière heure
|
||||
Quand l'utilisateur effectue une 4ème demande
|
||||
Alors la demande est refusée avec le message: "Trop de demandes de réinitialisation. Veuillez attendre 1 heure."
|
||||
Et aucun email n'est envoyé
|
||||
Et un événement "PASSWORD_RESET_RATE_LIMITED" est enregistré
|
||||
Et la métrique "auth.password_reset.rate_limited" est incrémentée
|
||||
|
||||
Scénario: Utilisation du lien de réinitialisation valide
|
||||
Étant donné un utilisateur "charlie@roadwave.fr" ayant demandé la réinitialisation
|
||||
Et il a reçu un email avec un token valide il y a 30 minutes
|
||||
Quand l'utilisateur clique sur le lien dans l'email
|
||||
Alors il est redirigé vers la page de réinitialisation
|
||||
Et le formulaire de nouveau mot de passe s'affiche
|
||||
Et le token est validé côté serveur
|
||||
Et un événement "PASSWORD_RESET_TOKEN_ACCESSED" est enregistré
|
||||
Et la session est sécurisée avec CSRF protection
|
||||
|
||||
Scénario: Définition du nouveau mot de passe avec validation
|
||||
Étant donné un utilisateur "david@roadwave.fr" sur la page de réinitialisation
|
||||
Et il a un token valide
|
||||
Quand l'utilisateur saisit un nouveau mot de passe "SecurePass2026!"
|
||||
Et confirme le mot de passe
|
||||
Alors le mot de passe est validé selon les règles de sécurité
|
||||
Et le mot de passe est hashé avec bcrypt (cost: 12)
|
||||
Et le mot de passe est enregistré dans la base de données
|
||||
Et toutes les sessions actives sont révoquées
|
||||
Et tous les tokens d'accès sont invalidés
|
||||
Et un événement "PASSWORD_RESET_COMPLETED" est enregistré
|
||||
Et un email de confirmation est envoyé: "Votre mot de passe a été modifié avec succès"
|
||||
Et la métrique "auth.password_reset.completed" est incrémentée
|
||||
Et l'utilisateur est redirigé vers la page de connexion
|
||||
|
||||
Scénario: Tentative d'utilisation d'un token expiré
|
||||
Étant donné un utilisateur "eve@roadwave.fr" ayant demandé la réinitialisation
|
||||
Et il a reçu un email avec un token valide il y a 2 heures
|
||||
Quand l'utilisateur clique sur le lien expiré
|
||||
Alors un message d'erreur s'affiche: "Ce lien de réinitialisation a expiré. Veuillez faire une nouvelle demande."
|
||||
Et un bouton "Demander un nouveau lien" est affiché
|
||||
Et un événement "PASSWORD_RESET_TOKEN_EXPIRED" est enregistré
|
||||
Et la métrique "auth.password_reset.token_expired" est incrémentée
|
||||
|
||||
Scénario: Tentative d'utilisation d'un token déjà utilisé
|
||||
Étant donné un utilisateur "frank@roadwave.fr" ayant réinitialisé son mot de passe
|
||||
Et le token a déjà été utilisé il y a 10 minutes
|
||||
Quand l'utilisateur tente de réutiliser le même lien
|
||||
Alors un message d'erreur s'affiche: "Ce lien a déjà été utilisé. Si vous avez besoin de réinitialiser à nouveau, faites une nouvelle demande."
|
||||
Et un événement "PASSWORD_RESET_TOKEN_REUSED" est enregistré avec niveau "MEDIUM"
|
||||
Et un email d'alerte est envoyé: "Tentative de réutilisation d'un ancien lien de réinitialisation"
|
||||
Et la métrique "auth.password_reset.token_reused" est incrémentée
|
||||
|
||||
Scénario: Détection de tentative d'attaque par force brute sur les tokens
|
||||
Étant donné un attaquant qui tente de deviner des tokens de réinitialisation
|
||||
Quand 10 tokens invalides sont testés depuis la même IP en 5 minutes
|
||||
Alors l'IP est bloquée temporairement pour 1 heure
|
||||
Et tous les tokens valides pour cette IP sont invalidés
|
||||
Et un événement "PASSWORD_RESET_BRUTE_FORCE_DETECTED" est enregistré avec niveau "CRITICAL"
|
||||
Et l'équipe de sécurité est alertée via webhook
|
||||
Et la métrique "security.password_reset.brute_force" est incrémentée
|
||||
|
||||
Scénario: Réinitialisation avec validation 2FA pour comptes sensibles
|
||||
Étant donné un utilisateur "grace@roadwave.fr" avec 2FA activé
|
||||
Et il a demandé la réinitialisation de son mot de passe
|
||||
Quand l'utilisateur clique sur le lien de réinitialisation
|
||||
Alors une étape supplémentaire de vérification 2FA s'affiche
|
||||
Et l'utilisateur doit saisir un code TOTP ou un code de récupération
|
||||
Et après validation 2FA, le formulaire de nouveau mot de passe s'affiche
|
||||
Et un événement "PASSWORD_RESET_2FA_VALIDATED" est enregistré
|
||||
Et la métrique "auth.password_reset.with_2fa" est incrémentée
|
||||
|
||||
Scénario: Notification de sécurité sur tous les appareils
|
||||
Étant donné un utilisateur "henry@roadwave.fr" connecté sur 3 appareils
|
||||
Quand l'utilisateur réinitialise son mot de passe
|
||||
Alors une notification push est envoyée sur tous les appareils:
|
||||
| Message |
|
||||
| Votre mot de passe a été modifié |
|
||||
| Si ce n'est pas vous, contactez immédiatement le support |
|
||||
Et un email est envoyé avec détails:
|
||||
| Détail | Valeur |
|
||||
| Date et heure | 2026-02-03 14:32:18 |
|
||||
| Adresse IP | 1.2.3.4 |
|
||||
| Localisation | Paris, France |
|
||||
| Appareil | iPhone 14 Pro |
|
||||
| Navigateur | Safari 17.2 |
|
||||
Et un lien "Ce n'était pas moi" permet de bloquer le compte immédiatement
|
||||
|
||||
Scénario: Historique des modifications de mot de passe
|
||||
Étant donné un utilisateur "iris@roadwave.fr"
|
||||
Quand l'utilisateur accède à "Mon compte > Sécurité > Historique"
|
||||
Alors l'utilisateur voit l'historique des modifications:
|
||||
| Date | Action | IP | Appareil | Localisation |
|
||||
| 2026-02-03 14:32 | Réinitialisation mot de passe | 1.2.3.4 | iPhone 14 | Paris, FR |
|
||||
| 2026-01-15 10:20 | Changement mot de passe | 5.6.7.8 | MacBook Pro | Lyon, FR |
|
||||
| 2025-12-01 08:15 | Création du compte | 9.10.11.12| iPad Air | Marseille, FR |
|
||||
Et les événements sont conservés pendant 90 jours minimum
|
||||
Et les logs sont conformes RGPD
|
||||
|
||||
Scénario: Réinitialisation impossible pour compte bloqué ou suspendu
|
||||
Étant donné un utilisateur "jack@roadwave.fr" dont le compte est suspendu
|
||||
Quand l'utilisateur demande la réinitialisation de son mot de passe
|
||||
Alors un message s'affiche: "Votre compte est actuellement suspendu. Veuillez contacter le support."
|
||||
Et aucun email de réinitialisation n'est envoyé
|
||||
Et un événement "PASSWORD_RESET_ACCOUNT_SUSPENDED" est enregistré
|
||||
Et un lien vers le support est fourni
|
||||
Et la métrique "auth.password_reset.blocked_account" est incrémentée
|
||||
|
||||
Scénario: Vérification de l'unicité du nouveau mot de passe
|
||||
Étant donné un utilisateur "kate@roadwave.fr" sur la page de réinitialisation
|
||||
Quand l'utilisateur tente de définir le même mot de passe que l'ancien
|
||||
Alors une erreur s'affiche: "Veuillez choisir un mot de passe différent de l'ancien"
|
||||
Et le mot de passe n'est pas enregistré
|
||||
Et un événement "PASSWORD_RESET_SAME_PASSWORD" est enregistré
|
||||
Et la métrique "auth.password_reset.same_password" est incrémentée
|
||||
|
||||
Scénario: Vérification contre les mots de passe compromis
|
||||
Étant donné un utilisateur "luke@roadwave.fr" sur la page de réinitialisation
|
||||
Quand l'utilisateur tente de définir un mot de passe "Password123!"
|
||||
Et ce mot de passe figure dans la base de données Have I Been Pwned
|
||||
Alors une erreur s'affiche: "Ce mot de passe est connu et a été compromis. Veuillez en choisir un autre."
|
||||
Et le mot de passe n'est pas enregistré
|
||||
Et un événement "PASSWORD_RESET_COMPROMISED_PASSWORD" est enregistré
|
||||
Et la métrique "auth.password_reset.compromised_blocked" est incrémentée
|
||||
|
||||
Scénario: Cooldown entre demandes successives de réinitialisation
|
||||
Étant donné un utilisateur "mary@roadwave.fr"
|
||||
Et il a fait une demande de réinitialisation il y a 2 minutes
|
||||
Quand l'utilisateur fait une nouvelle demande de réinitialisation
|
||||
Alors la demande est refusée avec le message: "Veuillez attendre 5 minutes entre chaque demande"
|
||||
Et un compteur affiche "Vous pourrez faire une nouvelle demande dans 3 minutes"
|
||||
Et un événement "PASSWORD_RESET_COOLDOWN" est enregistré
|
||||
Et la métrique "auth.password_reset.cooldown_hit" est incrémentée
|
||||
|
||||
Scénario: Métriques de sécurité pour la réinitialisation de mot de passe
|
||||
Étant donné que le système traite 1000 demandes de réinitialisation par jour
|
||||
Quand les métriques de sécurité sont collectées
|
||||
Alors les indicateurs suivants sont disponibles:
|
||||
| Métrique | Valeur cible |
|
||||
| Taux de complétion des réinitialisations | > 75% |
|
||||
| Taux de tokens expirés avant utilisation | < 20% |
|
||||
| Temps moyen de complétion | < 5 min |
|
||||
| Taux de détection de mots de passe compromis | > 5% |
|
||||
| Nombre de tentatives de brute force bloquées | Visible |
|
||||
Et les métriques sont exportées vers le système de monitoring
|
||||
Et des alertes sont déclenchées si anomalies détectées
|
||||
@@ -0,0 +1,250 @@
|
||||
# language: fr
|
||||
|
||||
@api @authentication @security @mvp
|
||||
Fonctionnalité: Validation des règles de mot de passe
|
||||
|
||||
En tant que système d'authentification
|
||||
Je veux valider la complexité des mots de passe
|
||||
Afin de garantir la sécurité des comptes utilisateurs
|
||||
|
||||
Contexte:
|
||||
Étant donné un utilisateur souhaite créer un compte ou modifier son mot de passe
|
||||
|
||||
# ============================================================================
|
||||
# VALIDATION LONGUEUR MINIMALE (8 CARACTÈRES)
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Mot de passe valide avec 8 caractères minimum
|
||||
Étant donné l'utilisateur saisit le mot de passe "Azerty123"
|
||||
Quand le système valide le mot de passe
|
||||
Alors la validation doit réussir
|
||||
Et aucune erreur ne doit être affichée
|
||||
|
||||
Scénario: Mot de passe trop court (7 caractères)
|
||||
Étant donné l'utilisateur saisit le mot de passe "Azert12"
|
||||
Quand le système valide le mot de passe
|
||||
Alors la validation doit échouer
|
||||
Et le message d'erreur doit être "Le mot de passe doit contenir au moins 8 caractères"
|
||||
Et le champ doit être marqué en rouge
|
||||
|
||||
Scénario: Mot de passe très court (3 caractères)
|
||||
Étant donné l'utilisateur saisit le mot de passe "Ab1"
|
||||
Quand le système valide le mot de passe
|
||||
Alors la validation doit échouer
|
||||
Et le message d'erreur doit être "Le mot de passe doit contenir au moins 8 caractères"
|
||||
|
||||
# ============================================================================
|
||||
# VALIDATION MAJUSCULE REQUISE
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Mot de passe valide avec au moins 1 majuscule
|
||||
Étant donné l'utilisateur saisit le mot de passe "Monpass123"
|
||||
Quand le système valide le mot de passe
|
||||
Alors la validation doit réussir
|
||||
Et le critère "majuscule" doit être validé avec une coche verte
|
||||
|
||||
Scénario: Mot de passe sans majuscule
|
||||
Étant donné l'utilisateur saisit le mot de passe "monpass123"
|
||||
Quand le système valide le mot de passe
|
||||
Alors la validation doit échouer
|
||||
Et le message d'erreur doit être "Le mot de passe doit contenir au moins 1 majuscule"
|
||||
|
||||
Scénario: Mot de passe avec plusieurs majuscules
|
||||
Étant donné l'utilisateur saisit le mot de passe "MonPASSword123"
|
||||
Quand le système valide le mot de passe
|
||||
Alors la validation doit réussir
|
||||
Car au moins 1 majuscule est présente
|
||||
|
||||
# ============================================================================
|
||||
# VALIDATION CHIFFRE REQUIS
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Mot de passe valide avec au moins 1 chiffre
|
||||
Étant donné l'utilisateur saisit le mot de passe "Monpass1"
|
||||
Quand le système valide le mot de passe
|
||||
Alors la validation doit réussir
|
||||
Et le critère "chiffre" doit être validé avec une coche verte
|
||||
|
||||
Scénario: Mot de passe sans chiffre
|
||||
Étant donné l'utilisateur saisit le mot de passe "Monpassword"
|
||||
Quand le système valide le mot de passe
|
||||
Alors la validation doit échouer
|
||||
Et le message d'erreur doit être "Le mot de passe doit contenir au moins 1 chiffre"
|
||||
|
||||
Scénario: Mot de passe avec plusieurs chiffres
|
||||
Étant donné l'utilisateur saisit le mot de passe "Monpass123456"
|
||||
Quand le système valide le mot de passe
|
||||
Alors la validation doit réussir
|
||||
Car au moins 1 chiffre est présent
|
||||
|
||||
# ============================================================================
|
||||
# VALIDATION COMBINÉE DES 3 CRITÈRES
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Mot de passe valide respectant tous les critères
|
||||
Étant donné l'utilisateur saisit le mot de passe "SecurePass2024!"
|
||||
Quand le système valide le mot de passe
|
||||
Alors la validation doit réussir
|
||||
Et tous les critères doivent être validés :
|
||||
| critère | statut |
|
||||
| longueur | ✓ |
|
||||
| majuscule | ✓ |
|
||||
| chiffre | ✓ |
|
||||
|
||||
Scénario: Mot de passe échouant sur plusieurs critères
|
||||
Étant donné l'utilisateur saisit le mot de passe "pass"
|
||||
Quand le système valide le mot de passe
|
||||
Alors la validation doit échouer
|
||||
Et les messages d'erreur suivants doivent être affichés :
|
||||
| Le mot de passe doit contenir au moins 8 caractères |
|
||||
| Le mot de passe doit contenir au moins 1 majuscule |
|
||||
| Le mot de passe doit contenir au moins 1 chiffre |
|
||||
|
||||
Scénario: Mot de passe long mais sans majuscule ni chiffre
|
||||
Étant donné l'utilisateur saisit le mot de passe "monmotdepasse"
|
||||
Quand le système valide le mot de passe
|
||||
Alors la validation doit échouer
|
||||
Et les messages d'erreur suivants doivent être affichés :
|
||||
| Le mot de passe doit contenir au moins 1 majuscule |
|
||||
| Le mot de passe doit contenir au moins 1 chiffre |
|
||||
|
||||
# ============================================================================
|
||||
# VALIDATION TEMPS RÉEL (FRONTEND)
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Affichage progressif des critères pendant la saisie
|
||||
Étant donné l'utilisateur commence à saisir son mot de passe
|
||||
Quand l'utilisateur tape "m"
|
||||
Alors les critères suivants doivent être affichés :
|
||||
| critère | statut |
|
||||
| longueur | ✗ |
|
||||
| majuscule | ✗ |
|
||||
| chiffre | ✗ |
|
||||
Quand l'utilisateur tape "Mon"
|
||||
Alors les critères doivent être mis à jour :
|
||||
| critère | statut |
|
||||
| longueur | ✗ |
|
||||
| majuscule | ✓ |
|
||||
| chiffre | ✗ |
|
||||
Quand l'utilisateur tape "Monpass1"
|
||||
Alors les critères doivent être mis à jour :
|
||||
| critère | statut |
|
||||
| longueur | ✓ |
|
||||
| majuscule | ✓ |
|
||||
| chiffre | ✓ |
|
||||
|
||||
Scénario: Feedback visuel temps réel
|
||||
Étant donné l'utilisateur saisit progressivement son mot de passe
|
||||
Quand un critère est validé
|
||||
Alors une coche verte ✓ doit apparaître à côté du critère
|
||||
Et le texte du critère doit passer en vert
|
||||
Quand un critère n'est pas validé
|
||||
Alors une croix rouge ✗ doit apparaître
|
||||
Et le texte du critère doit rester en gris ou rouge
|
||||
|
||||
# ============================================================================
|
||||
# VALIDATION BACKEND (SÉCURITÉ)
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Validation backend en plus du frontend
|
||||
Étant donné l'utilisateur contourne la validation frontend
|
||||
Et envoie directement le mot de passe "weak" via API
|
||||
Quand le backend reçoit la requête
|
||||
Alors la validation backend doit rejeter le mot de passe
|
||||
Et retourner une erreur HTTP 400 Bad Request
|
||||
Et le message doit être :
|
||||
"""
|
||||
{
|
||||
"error": "invalid_password",
|
||||
"details": [
|
||||
"Le mot de passe doit contenir au moins 8 caractères",
|
||||
"Le mot de passe doit contenir au moins 1 majuscule",
|
||||
"Le mot de passe doit contenir au moins 1 chiffre"
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: Validation backend avec mot de passe valide
|
||||
Étant donné l'utilisateur envoie le mot de passe "SecurePass123"
|
||||
Quand le backend valide le mot de passe
|
||||
Alors la validation backend doit réussir
|
||||
Et le mot de passe doit être hashé avec bcrypt (coût 12)
|
||||
Et le hash doit être stocké dans la base de données
|
||||
|
||||
# ============================================================================
|
||||
# CAS LIMITES ET CARACTÈRES SPÉCIAUX
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Mot de passe avec caractères spéciaux (acceptés)
|
||||
Étant donné l'utilisateur saisit le mot de passe "MonP@ss123!"
|
||||
Quand le système valide le mot de passe
|
||||
Alors la validation doit réussir
|
||||
Car les caractères spéciaux sont autorisés (mais non obligatoires)
|
||||
|
||||
Scénario: Mot de passe avec espaces (acceptés)
|
||||
Étant donné l'utilisateur saisit le mot de passe "Mon Pass 123"
|
||||
Quand le système valide le mot de passe
|
||||
Alors la validation doit réussir
|
||||
Car les espaces sont autorisés
|
||||
|
||||
Scénario: Mot de passe avec accents (acceptés)
|
||||
Étant donné l'utilisateur saisit le mot de passe "MônPàss123"
|
||||
Quand le système valide le mot de passe
|
||||
Alors la validation doit réussir
|
||||
Car les caractères accentués comptent comme des lettres
|
||||
|
||||
Scénario: Mot de passe avec émojis (acceptés)
|
||||
Étant donné l'utilisateur saisit le mot de passe "MonPass123🔒"
|
||||
Quand le système valide le mot de passe
|
||||
Alors la validation doit réussir
|
||||
Car les émojis sont autorisés
|
||||
|
||||
Scénario: Mot de passe vide
|
||||
Étant donné l'utilisateur laisse le champ mot de passe vide
|
||||
Quand le système valide le mot de passe
|
||||
Alors la validation doit échouer
|
||||
Et le message d'erreur doit être "Le mot de passe est requis"
|
||||
|
||||
# ============================================================================
|
||||
# MODIFICATION MOT DE PASSE
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Changement de mot de passe avec validation
|
||||
Étant donné un utilisateur authentifié veut changer son mot de passe
|
||||
Et l'utilisateur saisit son ancien mot de passe "OldPass123"
|
||||
Et l'utilisateur saisit le nouveau mot de passe "NewSecure456"
|
||||
Quand le système valide le nouveau mot de passe
|
||||
Alors la validation doit réussir
|
||||
Et le nouveau mot de passe doit respecter les mêmes règles
|
||||
Et l'ancien mot de passe doit être vérifié avant le changement
|
||||
|
||||
Scénario: Nouveau mot de passe identique à l'ancien (autorisé)
|
||||
Étant donné un utilisateur veut changer son mot de passe
|
||||
Et l'utilisateur saisit le nouveau mot de passe identique à l'ancien
|
||||
Quand le système valide le mot de passe
|
||||
Alors la validation doit réussir
|
||||
Car il n'y a pas de règle interdisant la réutilisation
|
||||
|
||||
# ============================================================================
|
||||
# MESSAGES D'AIDE ET UX
|
||||
# ============================================================================
|
||||
|
||||
Scénario: Affichage des règles avant saisie
|
||||
Étant donné l'utilisateur accède au formulaire d'inscription
|
||||
Quand le champ mot de passe reçoit le focus
|
||||
Alors une info-bulle doit s'afficher avec les règles :
|
||||
"""
|
||||
Votre mot de passe doit contenir :
|
||||
• Au moins 8 caractères
|
||||
• Au moins 1 majuscule
|
||||
• Au moins 1 chiffre
|
||||
"""
|
||||
|
||||
Scénario: Indicateur de force du mot de passe
|
||||
Étant donné l'utilisateur saisit progressivement son mot de passe
|
||||
Quand l'utilisateur tape "Weak1"
|
||||
Alors l'indicateur de force doit afficher "Faible" en orange
|
||||
Quand l'utilisateur tape "Medium12"
|
||||
Alors l'indicateur de force doit afficher "Moyen" en jaune
|
||||
Quand l'utilisateur tape "VeryStrong123!"
|
||||
Alors l'indicateur de force doit afficher "Fort" en vert
|
||||
@@ -0,0 +1,67 @@
|
||||
# language: fr
|
||||
|
||||
@ui @sharing @premium @viral @mvp
|
||||
Fonctionnalité: Partage de contenu Premium pour viralité
|
||||
|
||||
En tant qu'utilisateur Premium
|
||||
Je veux partager mes découvertes
|
||||
Afin de recommander la plateforme à mes amis
|
||||
|
||||
Scénario: Partage d'un audio-guide avec preview
|
||||
Étant donné un utilisateur "alice@roadwave.fr" Premium
|
||||
Quand elle partage l'audio-guide "Visite du Louvre"
|
||||
Alors un lien unique est généré: roadwave.fr/share/abc123
|
||||
Et le lien affiche une preview attractive:
|
||||
| Élément | Contenu |
|
||||
| Image cover | Photo du Louvre |
|
||||
| Titre | Visite du Louvre |
|
||||
| Description | Découvrez 3000 ans d'art... |
|
||||
| Durée | 2h 30min - 12 séquences |
|
||||
| Note | 4.8/5 (1,234 avis) |
|
||||
| Créateur | @MuseeDuLouvre |
|
||||
| CTA | [Écouter gratuitement] |
|
||||
Et un événement "CONTENT_SHARED" est enregistré
|
||||
|
||||
Scénario: Essai gratuit de 3 jours pour contenu partagé
|
||||
Étant donné un utilisateur Free qui clique sur un lien partagé
|
||||
Quand il consulte un contenu Premium
|
||||
Alors une offre s'affiche: "Essai gratuit 3 jours offerts par votre ami"
|
||||
Et il peut écouter le contenu sans payer
|
||||
Et un événement "FREE_TRIAL_FROM_SHARE" est enregistré
|
||||
|
||||
Scénario: Programme de parrainage avec récompenses
|
||||
Étant donné un utilisateur Premium qui partage
|
||||
Quand 3 amis s'abonnent via son lien
|
||||
Alors il reçoit 1 mois gratuit par ami converti
|
||||
Et un badge "Ambassadeur" s'affiche sur son profil
|
||||
Et un événement "REFERRAL_REWARDS_GRANTED" est enregistré
|
||||
|
||||
Scénario: Statistiques de partage
|
||||
Étant donné un utilisateur "bob@roadwave.fr"
|
||||
Quand il consulte ses statistiques de partage
|
||||
Alors il voit:
|
||||
| Métrique | Valeur |
|
||||
| Contenus partagés | 12 |
|
||||
| Clics sur liens | 45 |
|
||||
| Amis convertis | 3 |
|
||||
| Mois gratuits gagnés | 3 |
|
||||
Et un événement "SHARE_STATS_VIEWED" est enregistré
|
||||
|
||||
Scénario: Partage optimisé pour réseaux sociaux
|
||||
Étant donné un lien partagé sur Facebook
|
||||
Alors les Open Graph tags sont optimisés:
|
||||
| Tag | Valeur |
|
||||
| og:title | Visite du Louvre - RoadWave |
|
||||
| og:image | Image haute résolution |
|
||||
| og:description| Description accrocheuse |
|
||||
Et génère un maximum d'engagement
|
||||
Et un événement "SOCIAL_SHARE_OPTIMIZED" est enregistré
|
||||
|
||||
Scénario: Métriques de viralité
|
||||
Étant donné 1000 partages effectués
|
||||
Alors les indicateurs suivants sont disponibles:
|
||||
| Métrique | Valeur |
|
||||
| Taux de clic sur partage | 18% |
|
||||
| Taux de conversion | 12% |
|
||||
| K-factor (viralité) | 1.3 |
|
||||
Et les métriques sont exportées vers le monitoring
|
||||
90
docs/domains/_shared/features/profil/badge-verifie.feature
Normal file
90
docs/domains/_shared/features/profil/badge-verifie.feature
Normal file
@@ -0,0 +1,90 @@
|
||||
# language: fr
|
||||
|
||||
@api @profile @verification @mvp
|
||||
Fonctionnalité: Badge compte vérifié pour créateurs authentiques
|
||||
|
||||
En tant que créateur officiel
|
||||
Je veux obtenir un badge vérifié
|
||||
Afin de prouver mon authenticité et gagner la confiance
|
||||
|
||||
Scénario: Demande de vérification par un créateur
|
||||
Étant donné un créateur "MuseeDuLouvre" avec 1000+ abonnés
|
||||
Quand il demande la vérification via "Paramètres > Demander la vérification"
|
||||
Alors un formulaire de demande s'affiche:
|
||||
| Champ requis | Exemple |
|
||||
| Nom officiel | Musée du Louvre |
|
||||
| Type d'organisation | Institution culturelle |
|
||||
| Document officiel | KBIS / Statuts |
|
||||
| Preuve d'identité | Carte d'identité |
|
||||
| Site web officiel | louvre.fr |
|
||||
| Compte social officiel | @MuseeLouvre (Twitter) |
|
||||
Et la demande est soumise pour review
|
||||
Et un événement "VERIFICATION_REQUEST_SUBMITTED" est enregistré
|
||||
|
||||
Scénario: Vérification par l'équipe RoadWave
|
||||
Étant donné une demande de vérification reçue
|
||||
Quand un modérateur examine le dossier
|
||||
Alors il vérifie:
|
||||
| Critère | Validation |
|
||||
| Documents officiels | Authentiques |
|
||||
| Correspondance identité | Confirmée |
|
||||
| Site web officiel | Vérifié (DNS) |
|
||||
| Réseaux sociaux | Cross-vérifiés |
|
||||
| Activité sur RoadWave | Régulière (3+ mois) |
|
||||
Et prend une décision dans les 7 jours
|
||||
Et un événement "VERIFICATION_REVIEWED" est enregistré
|
||||
|
||||
Scénario: Attribution du badge vérifié
|
||||
Étant donné une demande acceptée
|
||||
Quand le badge est attribué
|
||||
Alors un badge bleu "✓ Vérifié" s'affiche:
|
||||
| Emplacement | Affichage |
|
||||
| À côté du nom de profil | ✓ Musée du Louvre |
|
||||
| Dans les résultats | Badge visible |
|
||||
| Dans les commentaires | Badge visible |
|
||||
Et une notification est envoyée: "Félicitations ! Votre compte est maintenant vérifié"
|
||||
Et un événement "VERIFICATION_BADGE_GRANTED" est enregistré
|
||||
|
||||
Scénario: Avantages du compte vérifié
|
||||
Étant donné un créateur vérifié
|
||||
Alors il bénéficie de:
|
||||
| Avantage | Détail |
|
||||
| Badge bleu visible | Crédibilité accrue |
|
||||
| Priorité dans les recherches | Meilleur ranking SEO |
|
||||
| Statistiques avancées | Analytics détaillées |
|
||||
| Support prioritaire | Réponse < 24h |
|
||||
| Contenu mis en avant | Page "Créateurs vérifiés" |
|
||||
Et un événement "VERIFIED_BENEFITS_DISPLAYED" est enregistré
|
||||
|
||||
Scénario: Révocation du badge pour violation
|
||||
Étant donné un créateur vérifié "InstitutionX"
|
||||
Quand il viole les CGU (contenu inapproprié)
|
||||
Alors le badge est révoqué immédiatement
|
||||
Et un email explique la raison
|
||||
Et il peut faire appel de la décision
|
||||
Et un événement "VERIFICATION_BADGE_REVOKED" est enregistré
|
||||
|
||||
Scénario: Renouvellement annuel de la vérification
|
||||
Étant donné un créateur vérifié depuis 12 mois
|
||||
Quand l'anniversaire de la vérification arrive
|
||||
Alors une review automatique est lancée
|
||||
Et des documents à jour peuvent être demandés
|
||||
Et le badge reste actif pendant la review
|
||||
Et un événement "VERIFICATION_RENEWAL_STARTED" est enregistré
|
||||
|
||||
Scénario: Badge spécial pour partenaires officiels
|
||||
Étant donné un partenaire stratégique (Offices du Tourisme, Musées nationaux)
|
||||
Alors un badge or "✓ Partenaire Officiel" est attribué
|
||||
Et des privilèges supplémentaires sont accordés
|
||||
Et un événement "OFFICIAL_PARTNER_BADGE_GRANTED" est enregistré
|
||||
|
||||
Scénario: Statistiques des comptes vérifiés
|
||||
Étant donné que 150 comptes sont vérifiés
|
||||
Alors les indicateurs suivants sont disponibles:
|
||||
| Métrique | Valeur |
|
||||
| Comptes vérifiés | 150 |
|
||||
| % de la base créateurs | 1.5% |
|
||||
| Demandes en attente | 45 |
|
||||
| Taux d'acceptation | 65% |
|
||||
| Temps moyen de vérification | 5 jours |
|
||||
Et les métriques sont exportées vers le monitoring
|
||||
@@ -0,0 +1,70 @@
|
||||
# language: fr
|
||||
|
||||
@ui @profile @privacy @mvp
|
||||
Fonctionnalité: Statistiques arrondies pour protection de la vie privée
|
||||
|
||||
En tant qu'utilisateur
|
||||
Je veux que mes statistiques publiques soient arrondies
|
||||
Afin de protéger ma vie privée et éviter le tracking précis
|
||||
|
||||
Scénario: Arrondi du nombre d'écoutes publiques
|
||||
Étant donné un créateur avec 1,234 écoutes exactes
|
||||
Quand son profil public est affiché
|
||||
Alors le nombre affiché est: "1.2k écoutes"
|
||||
Et non pas "1,234"
|
||||
Et un événement "STATS_ROUNDED_DISPLAYED" est enregistré
|
||||
|
||||
Scénario: Règles d'arrondi selon les volumes
|
||||
Étant donné différents volumes d'écoutes
|
||||
Alors l'arrondi appliqué est:
|
||||
| Écoutes exactes | Affiché publiquement |
|
||||
| 42 | 40 |
|
||||
| 157 | 150+ |
|
||||
| 1,234 | 1.2k |
|
||||
| 15,678 | 15k |
|
||||
| 123,456 | 120k |
|
||||
| 1,234,567 | 1.2M |
|
||||
Et un événement "ROUNDING_RULES_APPLIED" est enregistré
|
||||
|
||||
Scénario: Statistiques précises pour le créateur seulement
|
||||
Étant donné un créateur "alice@roadwave.fr"
|
||||
Quand elle consulte son propre dashboard
|
||||
Alors elle voit les chiffres exacts: 1,234
|
||||
Mais les visiteurs externes voient: 1.2k
|
||||
Et un événement "PRECISE_STATS_CREATOR_VIEW" est enregistré
|
||||
|
||||
Scénario: Arrondi des revenus publics
|
||||
Étant donné un créateur avec 1,567€ de revenus
|
||||
Quand ses stats publiques sont affichées
|
||||
Alors le montant est arrondi: "1.5k€"
|
||||
Et les décimales exactes sont masquées
|
||||
Et un événement "REVENUE_ROUNDED_PUBLIC" est enregistré
|
||||
|
||||
Scénario: Arrondi du nombre d'abonnés
|
||||
Étant donné un créateur avec 8,743 abonnés
|
||||
Alors le profil public affiche: "8.7k abonnés"
|
||||
Et évite le tracking précis de croissance
|
||||
Et un événement "FOLLOWERS_ROUNDED_DISPLAYED" est enregistré
|
||||
|
||||
Scénario: Protection contre le scraping de données
|
||||
Étant donné un bot qui scrape les profils
|
||||
Quand il collecte les statistiques arrondies
|
||||
Alors il ne peut pas obtenir de données précises
|
||||
Et le tracking temporel est rendu imprécis
|
||||
Et un événement "SCRAPING_PROTECTION_ACTIVE" est enregistré
|
||||
|
||||
Scénario: Option de désactivation de l'arrondi pour créateurs vérifiés
|
||||
Étant donné un créateur vérifié "MuseeDuLouvre"
|
||||
Quand il active "Afficher statistiques exactes"
|
||||
Alors les chiffres précis sont publics
|
||||
Et cela renforce la transparence
|
||||
Et un événement "PRECISE_STATS_PUBLIC_ENABLED" est enregistré
|
||||
|
||||
Scénario: Métriques d'impact de l'arrondi sur la vie privée
|
||||
Étant donné que 10 000 profils affichent des stats arrondies
|
||||
Alors l'impact est mesuré:
|
||||
| Métrique | Valeur |
|
||||
| Tentatives de tracking bloquées | 1,234 |
|
||||
| Précision moyenne du scraping | -70% |
|
||||
| Satisfaction utilisateurs | 4.5/5 |
|
||||
Et les métriques sont exportées vers le monitoring
|
||||
@@ -660,6 +660,322 @@
|
||||
|
||||
---
|
||||
|
||||
## 7. Contenus prioritaires et comptes officiels
|
||||
|
||||
> ⚠️ **Reporté post-MVP** - Système d'alertes critiques et intégration sources officielles (gestionnaires autoroutes, Météo France, préfectures).
|
||||
|
||||
### Contexte du report
|
||||
|
||||
**Raisons** :
|
||||
- **Masse critique requise** : Partenariats avec organismes officiels nécessitent base utilisateurs solide (>50K MAU)
|
||||
- **Complexité technique** : Intégration APIs externes, système de priorités, TTS automatisé
|
||||
- **Responsabilité légale** : Diffusion alertes sécurité = engagement fort, nécessite infrastructure stable
|
||||
- **Focus MVP** : Priorité sur contenu créateurs communautaires
|
||||
- **ROI incertain** : Valeur ajoutée forte mais sans revenus directs (service public)
|
||||
|
||||
**Version MVP** (actuelle) :
|
||||
- Tous contenus = créateurs classiques
|
||||
- Pas de système de priorité
|
||||
- Pas de comptes officiels vérifiés
|
||||
- Pas d'interruption de contenu en cours
|
||||
|
||||
---
|
||||
|
||||
### Spécifications complètes (future implémentation)
|
||||
|
||||
**Problématique** : Certaines informations (obstacle sur autoroute, alerte météo dangereuse) doivent être diffusées en **priorité absolue**, indépendamment de l'algorithme de recommandation.
|
||||
|
||||
**Solution** : Système de contenus prioritaires avec comptes officiels vérifiés et interruption conditionnelle du flux audio.
|
||||
|
||||
#### A) Nouveau type de compte : Compte Officiel
|
||||
|
||||
| Type compte | Validation | Badge | Priorité | Modération |
|
||||
|-------------|-----------|-------|----------|------------|
|
||||
| **Créateur classique** | Email + KYC (si monétisation) | - | Normale | 3 premiers contenus |
|
||||
| **Créateur vérifié** | KYC validé OU >10K abonnés | ✓ | Normale | A posteriori |
|
||||
| **Compte Officiel** | Validation RoadWave manuelle + contrat partenariat | 🏛️ | **Configurable (0-3)** | **Aucune** |
|
||||
|
||||
**Exemples comptes officiels** :
|
||||
- **Gestionnaires autoroutes** : SANEF, Vinci Autoroutes, APRR, ASF
|
||||
- **Services météo** : Météo France, vigilance.gouv.fr
|
||||
- **Sécurité civile** : Préfectures, Plan alerte enlèvement
|
||||
- **Services publics** : Bison Futé, Sécurité Routière
|
||||
- **Médias publics** : France Info, France Inter (déjà créateurs, passage en Officiel)
|
||||
|
||||
**Processus de validation** :
|
||||
1. Demande partenariat → contact commercial RoadWave
|
||||
2. Vérification identité organisme (SIRET, documents officiels)
|
||||
3. Signature convention partenariat (gratuit, service d'intérêt public)
|
||||
4. Création compte Officiel avec badge 🏛️
|
||||
5. Configuration API Webhook pour contenus automatisés
|
||||
|
||||
---
|
||||
|
||||
#### B) Système de priorité des contenus
|
||||
|
||||
**Nouveau champ DB** : `priority_level`
|
||||
|
||||
```sql
|
||||
ALTER TABLE contents ADD COLUMN priority_level INT DEFAULT 0 CHECK (priority_level BETWEEN 0 AND 3);
|
||||
|
||||
-- 0 = Normal (créateurs classiques, algo standard)
|
||||
-- 1 = Élevé (infos trafic importantes, boost algo)
|
||||
-- 2 = Urgent (obstacle imminent, injection forcée)
|
||||
-- 3 = Critique (danger immédiat, interruption autorisée)
|
||||
```
|
||||
|
||||
**Comportement selon priorité** :
|
||||
|
||||
| Priorité | Nom | Comportement | Bypass quota 6/h | Interruption contenu en cours |
|
||||
|----------|-----|--------------|------------------|-------------------------------|
|
||||
| **0** | Normal | Algo standard (score géo + intérêts + engagement) | Non | Non |
|
||||
| **1** | Élevé | Boost score final +0.3 (favorisé mais pas forcé) | Non | Non |
|
||||
| **2** | Urgent | Injection forcée en **prochaine position** file d'attente | Oui | Non (attend fin contenu actuel) |
|
||||
| **3** | Critique | **Interruption immédiate avec countdown 5s** | Oui | **Oui** (pause contenu, overlay, lecture alerte) |
|
||||
|
||||
**Cas d'usage par priorité** :
|
||||
|
||||
```
|
||||
🟢 Priorité 0 - Normal
|
||||
├─ Tous contenus créateurs classiques
|
||||
└─ Algorithme de recommandation standard
|
||||
|
||||
🟡 Priorité 1 - Élevé
|
||||
├─ Info trafic général (bouchon prévu, travaux)
|
||||
├─ Événement local impactant circulation (match, concert)
|
||||
└─ Météo défavorable non dangereuse (pluie modérée)
|
||||
|
||||
🟠 Priorité 2 - Urgent
|
||||
├─ Accident récent avec impact circulation
|
||||
├─ Route coupée / déviation obligatoire
|
||||
├─ Péage fermé de façon imprévue
|
||||
└─ Alerte pollution temporaire
|
||||
|
||||
🔴 Priorité 3 - Critique
|
||||
├─ Obstacle sur voie (objet, véhicule arrêté)
|
||||
├─ Alerte météo orange/rouge (tempête, inondation, neige)
|
||||
├─ Alerte enlèvement (Plan alerte enlèvement)
|
||||
├─ Fermeture tunnel/pont pour sécurité
|
||||
└─ Contre-sens signalé
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### C) Flow interruption (priorité 3)
|
||||
|
||||
**Interface utilisateur** :
|
||||
|
||||
```
|
||||
User écoute podcast normal à 30 km/h sur A7
|
||||
↓
|
||||
Contenu priorité 3 détecté dans zone 500m devant
|
||||
↓
|
||||
Overlay rouge translucide apparaît sur écran :
|
||||
┌─────────────────────────────────────┐
|
||||
│ ⚠️ ALERTE SÉCURITÉ │
|
||||
│ │
|
||||
│ Obstacle signalé A7 voie gauche │
|
||||
│ km 125 │
|
||||
│ │
|
||||
│ Diffusion dans 5... 4... 3... │
|
||||
│ │
|
||||
│ [Ignorer l'alerte] │
|
||||
└─────────────────────────────────────┘
|
||||
↓
|
||||
Countdown 5 secondes (annulable)
|
||||
↓
|
||||
Podcast actuel → PAUSE automatique
|
||||
↓
|
||||
Son d'alerte : Bip urgent (0.5s)
|
||||
↓
|
||||
Alerte TTS : "Attention, obstacle signalé sur voie de gauche, autoroute A7, kilomètre 125. Réduisez votre vitesse."
|
||||
↓
|
||||
Alerte se termine (15-30 secondes max)
|
||||
↓
|
||||
Podcast reprend automatiquement à position exacte
|
||||
```
|
||||
|
||||
**Paramètres techniques** :
|
||||
- **Rayon déclenchement** : 500m-2km selon vitesse (calcul dynamique)
|
||||
- **Son d'alerte** : Bip distinctif (pas agressif, mais audible)
|
||||
- **Durée max alerte** : 30 secondes (format court, info essentielle)
|
||||
- **Cooldown** : même alerte pas reproposée avant 10 minutes
|
||||
- **Annulation** : bouton "Ignorer" disponible pendant countdown (mais déconseillé)
|
||||
|
||||
**Traçabilité** :
|
||||
- Log : `user_id`, `alert_id`, `action` (played / ignored), `timestamp`
|
||||
- Statistiques : taux d'écoute alertes vs taux ignore (KPI efficacité)
|
||||
|
||||
---
|
||||
|
||||
#### D) Intégration APIs externes et TTS automatisé
|
||||
|
||||
**Partenariats cibles** :
|
||||
|
||||
| Partenaire | API | Type contenu | Priorité | Coût | Disponibilité |
|
||||
|-----------|-----|--------------|----------|------|---------------|
|
||||
| **Météo France** | API Vigilance | Alertes météo orange/rouge | 3 | Gratuit (service public) | ✅ API publique |
|
||||
| **Bison Futé** | API Trafic | Info trafic temps réel | 1-2 | Gratuit | ✅ API publique |
|
||||
| **Gestionnaires autoroutes** | APIs propriétaires | Obstacles, fermetures | 2-3 | Gratuit (partenariat) | ⚠️ Négociation |
|
||||
| **Sécurité Routière** | Données ouvertes | Zones accidentogènes, campagnes | 1 | Gratuit | ✅ Open Data |
|
||||
| **Waze / Coyote** | API (si accessible) | Dangers signalés users | 2 | Négociation | ❌ APIs fermées |
|
||||
|
||||
**Flow automatisé (exemple Météo France)** :
|
||||
|
||||
```
|
||||
1. API Météo France → Webhook RoadWave
|
||||
Données : {
|
||||
"departement": "83",
|
||||
"vigilance": "orange",
|
||||
"phenomene": "pluie-inondation",
|
||||
"debut": "2026-01-20T14:00:00Z",
|
||||
"fin": "2026-01-20T23:00:00Z"
|
||||
}
|
||||
|
||||
2. Backend RoadWave (worker Go) traite webhook :
|
||||
- Récupère polygon département 83 (PostGIS)
|
||||
- Génère texte alerte : "Alerte météo orange dans le Var : fortes pluies et risque d'inondations. Soyez prudents."
|
||||
- Appelle TTS (Google Cloud TTS ou AWS Polly)
|
||||
- Génère fichier audio MP3 + segments HLS
|
||||
|
||||
3. Création automatique contenu :
|
||||
├─ Titre : "⚠️ Alerte Météo Orange - Var"
|
||||
├─ Audio : Fichier TTS généré
|
||||
├─ Zone : Polygon département 83
|
||||
├─ Priority : 3 (critique)
|
||||
├─ Durée vie : 12h (expiration automatique)
|
||||
├─ Créateur : Compte "Météo France" (officiel)
|
||||
└─ Tags : ["Météo", "Sécurité"]
|
||||
|
||||
4. Diffusion immédiate :
|
||||
- Tous users dans département 83
|
||||
- Interruption flux audio (countdown 5s)
|
||||
- Diffusion alerte
|
||||
- Reprise contenu normal
|
||||
```
|
||||
|
||||
**TTS (Text-to-Speech)** :
|
||||
- **Fournisseur** : Google Cloud TTS WaveNet (voix neurale professionnelle)
|
||||
- **Coût** : ~0.016€/1000 caractères
|
||||
- **Voix** : "Léa" (féminine, française, ton calme mais ferme pour alertes)
|
||||
- **Normalisation audio** : -14 LUFS (comme autres contenus)
|
||||
|
||||
**Expiration automatique** :
|
||||
- Alertes météo : 12h après fin vigilance
|
||||
- Obstacles autoroute : 2h après signalement (si non mis à jour)
|
||||
- Alertes enlèvement : 48h ou jusqu'à résolution officielle
|
||||
|
||||
---
|
||||
|
||||
#### E) Dashboard admin (gestion alertes)
|
||||
|
||||
**Interface modérateur RoadWave** :
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────┐
|
||||
│ 🏛️ Gestion contenus officiels │
|
||||
├────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Alertes actives (3) │
|
||||
│ │
|
||||
│ 🔴 CRITIQUE - Obstacle A7 km 125 │
|
||||
│ Source : SANEF │
|
||||
│ Diffusions : 1,247 | Ignores : 23 (1.8%) │
|
||||
│ Expire : dans 1h32 │
|
||||
│ [Prolonger] [Arrêter maintenant] │
|
||||
│ │
|
||||
│ 🔴 CRITIQUE - Alerte météo orange Var │
|
||||
│ Source : Météo France │
|
||||
│ Diffusions : 8,921 | Ignores : 156 (1.7%) │
|
||||
│ Expire : dans 9h12 │
|
||||
│ [Modifier] [Arrêter] │
|
||||
│ │
|
||||
│ 🟠 URGENT - Bouchon A6 Lyon │
|
||||
│ Source : Bison Futé │
|
||||
│ Diffusions : 2,104 | Ignores : 312 (14.8%) │
|
||||
│ Expire : dans 3h05 │
|
||||
│ [Modifier] [Arrêter] │
|
||||
│ │
|
||||
├────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ [+ Créer alerte manuelle] │
|
||||
│ │
|
||||
│ Historique (7 derniers jours) │
|
||||
│ · 127 alertes diffusées │
|
||||
│ · 98.2% taux écoute moyen │
|
||||
│ · 1.8% taux ignore moyen │
|
||||
│ │
|
||||
└────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Création alerte manuelle** :
|
||||
- Use case : information non automatisée (événement exceptionnel)
|
||||
- Champs : Texte (TTS auto), Zone (carte), Priorité (1-3), Durée vie
|
||||
- Validation admin RoadWave requise (pas auto-publication)
|
||||
|
||||
---
|
||||
|
||||
### Avantages
|
||||
|
||||
- ✅ **Sécurité routière** : diffusion info critique temps réel
|
||||
- ✅ **Valeur ajoutée** : différenciation vs Waze/Coyote (audio automatique)
|
||||
- ✅ **Partenariats gagnant-gagnant** : visibilité organismes publics, service utilisateurs
|
||||
- ✅ **Coût maîtrisé** : APIs gratuites + TTS ponctuel (~50€/mois max)
|
||||
- ✅ **Réutilisation infra** : HLS, PostGIS, backend Go déjà en place
|
||||
|
||||
### Contraintes
|
||||
|
||||
- ❌ **Responsabilité légale** : diffusion alertes = engagement fort (info exacte, à jour)
|
||||
- ❌ **Partenariats longs** : négociations avec organismes publics (6-12 mois)
|
||||
- ❌ **Maintenance APIs** : dépendance externe, risque coupure service
|
||||
- ❌ **Modération réactive** : si alerte erronée, correction manuelle urgente
|
||||
- ❌ **Interruption UX** : priorité 3 peut frustrer si trop fréquent (nécessite calibration)
|
||||
|
||||
---
|
||||
|
||||
### Conditions de réintégration
|
||||
|
||||
**Prérequis** :
|
||||
1. Base utilisateurs stable >50K MAU (argumentaire crédible pour partenariats)
|
||||
2. Chiffre affaires positif (infrastructure fiable = confiance partenaires)
|
||||
3. Équipe support disponible 24/7 pour gestion alertes critiques
|
||||
4. Validation juridique responsabilité (assurance RC pro couvre diffusion alertes)
|
||||
5. Tests A/B réussis sur interruption priorité 3 (acceptabilité utilisateurs)
|
||||
|
||||
**Chronologie estimée** :
|
||||
- Phase 1 (Post-MVP+6 mois) : Développement système priorités + dashboard admin + TTS
|
||||
- Phase 2 (Post-MVP+9 mois) : Premier partenariat (Météo France, API publique simple)
|
||||
- Phase 3 (Post-MVP+12 mois) : Tests bêta alertes météo avec utilisateurs volontaires
|
||||
- Phase 4 (Post-MVP+15 mois) : Extension autres partenaires (Bison Futé, gestionnaires autoroutes)
|
||||
- Phase 5 (Post-MVP+18 mois) : Déploiement complet si KPI positifs
|
||||
|
||||
**KPI de succès** :
|
||||
- Taux écoute alertes priorité 3 : >95% (faible taux ignore)
|
||||
- Satisfaction utilisateurs : >80% jugent alertes utiles (sondage post-alerte)
|
||||
- Taux faux positifs : <2% (alerte diffusée à tort ou obsolète)
|
||||
- Réduction incidents : mesure impact (accidents évités, détours anticipés) → difficile mais qualitatif fort
|
||||
- Partenariats actifs : >3 organismes officiels connectés
|
||||
|
||||
**Budget estimé** (base 100K MAU) :
|
||||
|
||||
| Composant | Coût mensuel |
|
||||
|-----------|--------------|
|
||||
| **TTS alertes auto** | ~50€ (10-20 alertes/mois, textes courts) |
|
||||
| **Stockage audio alertes** | ~5€ (fichiers temporaires, expiration auto) |
|
||||
| **Modération alertes** | ~200€ (part-time, monitoring dashboard) |
|
||||
| **APIs externes** | 0€ (gratuites, services publics) |
|
||||
| **Bande passante** | Inclus infrastructure existante |
|
||||
| **Total** | **~255€/mois** |
|
||||
|
||||
**ROI** :
|
||||
- Pas de revenus directs (service public)
|
||||
- Valeur indirecte : **différenciation produit majeure**
|
||||
- Argument commercial : "RoadWave vous protège en temps réel"
|
||||
- Rétention utilisateurs : +5-10% (feature killer)
|
||||
- Presse/médias : couverture positive (innovation sécurité routière)
|
||||
|
||||
---
|
||||
|
||||
## Autres fonctionnalités candidates Post-MVP
|
||||
|
||||
Liste non exhaustive de fonctionnalités évoquées mais non encore spécifiées :
|
||||
@@ -19,7 +19,7 @@
|
||||
- RGPD : données 100% contrôlées
|
||||
- Coût : 0€ (Zitadel intégré)
|
||||
|
||||
> 📋 **Référence technique** : Voir [ADR-008 - OAuth2 vs Fournisseurs Tiers](../adr/008-authentification.md#oauth2-pkce--protocole-vs-fournisseurs-tiers) pour clarification protocole vs providers.
|
||||
> 📋 **Référence technique** : Voir [ADR-008 - OAuth2 vs Fournisseurs Tiers](../../../adr/008-authentification.md#oauth2-pkce--protocole-vs-fournisseurs-tiers) pour clarification protocole vs providers.
|
||||
|
||||
---
|
||||
|
||||
@@ -94,9 +94,9 @@
|
||||
- 🔴 **18+** : contenu adulte (langage explicite, sujets réservés)
|
||||
|
||||
**Règles de diffusion** :
|
||||
- Utilisateur 13-15 ans → contenus 🟢 uniquement
|
||||
- Utilisateur 16-17 ans → contenus 🟢 🟡
|
||||
- Utilisateur 18+ → tous contenus
|
||||
- Utilisateur 13-15 ans → contenus 🟢 🟡 (Tout public + 13+)
|
||||
- Utilisateur 16-17 ans → contenus 🟢 🟡 🟠 (Tout public + 13+ + 16+)
|
||||
- Utilisateur 18+ → tous contenus 🟢 🟡 🟠 🔴
|
||||
|
||||
**Modération** :
|
||||
- Vérification obligatoire de la classification lors de la validation
|
||||
@@ -144,7 +144,7 @@ export-roadwave-[user_id]-[date].zip
|
||||
- Upgrade volontaire vers GPS
|
||||
|
||||
**API GeoIP** :
|
||||
- IP2Location Lite (gratuit, self-hosted, voir [ADR-019](../adr/019-geolocalisation-ip.md))
|
||||
- IP2Location Lite (gratuit, self-hosted, voir [ADR-019](../../../adr/019-geolocalisation-ip.md))
|
||||
- Update DB mensuelle automatique
|
||||
- Précision ~80% au niveau ville
|
||||
|
||||
36
docs/domains/advertising/README.md
Normal file
36
docs/domains/advertising/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Domaine : Advertising
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
Le domaine **Advertising** gère la diffusion de publicités audio ciblées. C'est un **Generic Subdomain** qui constitue une source de revenus importante pour la plateforme.
|
||||
|
||||
## Responsabilités
|
||||
|
||||
- **Campagnes publicitaires** : Création et gestion des campagnes
|
||||
- **Ciblage** : Ciblage géographique et par centres d'intérêt
|
||||
- **Métriques** : Suivi des impressions, écoutes et performances
|
||||
- **Insertion dynamique** : Insertion de publicités dans les flux audio
|
||||
|
||||
## Règles métier
|
||||
|
||||
- [Publicités](rules/publicites.md)
|
||||
|
||||
## Modèle de données
|
||||
|
||||
- [Diagramme entités publicités](entities/modele-publicites.md) - Entités : AD_CAMPAIGNS, AD_METRICS, AD_IMPRESSIONS
|
||||
|
||||
## Ubiquitous Language
|
||||
|
||||
**Termes métier du domaine** :
|
||||
- **Ad Campaign** : Campagne publicitaire avec budget et durée
|
||||
- **Ad Impression** : Affichage/lecture d'une publicité
|
||||
- **Ad Targeting** : Critères de ciblage (geo + intérêts)
|
||||
- **CPM (Cost Per Mille)** : Coût pour 1000 impressions
|
||||
- **Ad Insertion** : Insertion dynamique dans le flux audio
|
||||
- **Skip Rate** : Taux de publicités sautées par les utilisateurs
|
||||
|
||||
## Dépendances
|
||||
|
||||
- ✅ Dépend de : `_shared` (users, listening history)
|
||||
- ✅ Dépend de : `recommendation` (ciblage par intérêts)
|
||||
- ⚠️ Bloqué par : `premium` (pas de pub pour abonnés premium)
|
||||
67
docs/domains/advertising/entities/modele-publicites.md
Normal file
67
docs/domains/advertising/entities/modele-publicites.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Modèle de données - Publicités
|
||||
|
||||
📖 Voir [Règles métier - Section 16 : Publicités](../rules/publicites.md) | [Entités globales](../../_shared/entities/modele-global.md)
|
||||
|
||||
## Diagramme
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
AD_CAMPAIGNS }o--|| USERS : "créée par"
|
||||
AD_CAMPAIGNS ||--o{ AD_METRICS : "métriques"
|
||||
AD_CAMPAIGNS ||--o{ AD_IMPRESSIONS : "diffusions"
|
||||
|
||||
AD_IMPRESSIONS }o--|| USERS : "vue par"
|
||||
AD_IMPRESSIONS }o--|| AD_CAMPAIGNS : "campagne"
|
||||
|
||||
AD_CAMPAIGNS {
|
||||
uuid id PK
|
||||
uuid advertiser_id FK
|
||||
string title
|
||||
string audio_url
|
||||
int duration_seconds
|
||||
string status
|
||||
string targeting_geo_type
|
||||
jsonb targeting_geo_data
|
||||
jsonb targeting_hours
|
||||
string[] targeting_interests
|
||||
string targeting_age_rating
|
||||
decimal budget_total_euros
|
||||
decimal budget_remaining_euros
|
||||
decimal cost_per_listen_euros
|
||||
timestamp start_date
|
||||
timestamp end_date
|
||||
timestamp validated_at
|
||||
timestamp created_at
|
||||
}
|
||||
|
||||
AD_METRICS {
|
||||
uuid id PK
|
||||
uuid campaign_id FK
|
||||
date metric_date
|
||||
int impressions_count
|
||||
int complete_listens_count
|
||||
int skips_count
|
||||
decimal avg_listen_duration_seconds
|
||||
int likes_count
|
||||
decimal total_cost_euros
|
||||
timestamp computed_at
|
||||
}
|
||||
|
||||
AD_IMPRESSIONS {
|
||||
uuid id PK
|
||||
uuid campaign_id FK
|
||||
uuid user_id FK
|
||||
decimal completion_rate
|
||||
boolean was_skipped
|
||||
int listen_duration_seconds
|
||||
timestamp displayed_at
|
||||
}
|
||||
```
|
||||
|
||||
## Légende
|
||||
|
||||
**Entités publicités** :
|
||||
|
||||
- **AD_CAMPAIGNS** : Campagnes publicitaires - Status : `draft`, `pending_validation`, `validated`, `active`, `paused`, `completed`, `cancelled` - Targeting_geo_type : `point` (GPS + rayon), `city`, `department`, `region`, `national` - Targeting_hours : Array heures locales [7, 8, 9, 17, 18, 19] (heure locale utilisateur) - Budget : Prépaiement obligatoire, déduction 0.05€/écoute complète ou 0.02€/skip après délai min - Validation manuelle obligatoire 24-48h
|
||||
- **AD_METRICS** : Métriques agrégées par jour - Calcul quotidien (batch nocturne) - Dashboard temps réel publicitaire - Export CSV/Excel disponible
|
||||
- **AD_IMPRESSIONS** : Impressions individuelles - Completion_rate ≥0.8 = écoute complète (facturée 0.05€) - Skip après délai min 5s = partiel (facturé 0.02€) - Skip <5s = non facturé (0€) - Rotation max 3 fois/jour par utilisateur - Limite 6 pubs/h par utilisateur
|
||||
@@ -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.
|
||||
"""
|
||||
@@ -15,10 +15,132 @@
|
||||
| **Budget total** | Montant libre (min 50€) | Maîtrise coût total |
|
||||
| **Durée campagne** | Date début/fin + étalement | Ex: 300€ sur 2 semaines |
|
||||
| **Ciblage géographique** | Point GPS / Ville / Département / Région / National | Précision selon besoin |
|
||||
| **Ciblage horaire** | Plages horaires (ex: 7h-9h, 17h-19h) | Optimisation trajet domicile-travail |
|
||||
| **Ciblage horaire** | Plages horaires (ex: 7h-9h, 17h-19h) - **Heure locale utilisateur** | Optimisation trajet domicile-travail |
|
||||
| **Centres d'intérêt** | Tags (ex: Automobile, Voyage) | Ciblage thématique |
|
||||
| **Tranche d'âge** | Tout public / 13+ / 16+ / 18+ | Respect classifications |
|
||||
|
||||
**Précision ciblage horaire** :
|
||||
|
||||
**Règle 1 : Ciblage horaire = Heure locale utilisateur**
|
||||
|
||||
Une campagne "7h-9h" diffuse entre 7h-9h **heure locale** de chaque utilisateur, quel que soit son fuseau horaire.
|
||||
|
||||
**Exemples** :
|
||||
```
|
||||
Campagne : 7h-9h (rush matin)
|
||||
|
||||
User Marseille (UTC+1) à 8h locale → ✅ Diffusion
|
||||
User Guadeloupe (UTC-4) à 8h locale → ✅ Diffusion
|
||||
User Réunion (UTC+4) à 8h locale → ✅ Diffusion
|
||||
User Métropole à 13h locale → ❌ Pas de diffusion (hors plage)
|
||||
```
|
||||
|
||||
**Implémentation technique** :
|
||||
|
||||
```javascript
|
||||
// Backend détecte fuseau horaire user (GPS ou device settings)
|
||||
const userTimezone = getUserTimezone(); // "Europe/Paris", "America/Guadeloupe", etc.
|
||||
const userLocalTime = DateTime.now().setZone(userTimezone);
|
||||
const userHour = userLocalTime.hour; // 0-23
|
||||
|
||||
// Campagne pub
|
||||
const campaign = {
|
||||
hours: [7, 8, 9], // 7h-9h inclut 7h, 8h (se termine à 9h)
|
||||
// ...
|
||||
};
|
||||
|
||||
// Vérification diffusion
|
||||
if (campaign.hours.includes(userHour)) {
|
||||
// ✅ Diffuser pub
|
||||
}
|
||||
```
|
||||
|
||||
**Détection fuseau horaire** :
|
||||
1. GPS (latitude/longitude) → déterminer fuseau via base IANA Time Zone
|
||||
2. Si GPS désactivé → paramètres device (timezone OS)
|
||||
3. Fallback : IP geolocation → fuseau approximatif
|
||||
|
||||
**Règle 2 : Ciblage "France" = Métropole + DOM**
|
||||
|
||||
**France entière inclut** :
|
||||
- France métropolitaine (96 départements)
|
||||
- Guadeloupe (971)
|
||||
- Martinique (972)
|
||||
- Guyane (973)
|
||||
- Réunion (974)
|
||||
- Mayotte (976)
|
||||
|
||||
**Publicitaire peut affiner** :
|
||||
- "Région Provence-Alpes-Côte d'Azur" → Métropole uniquement
|
||||
- "Département 971" → Guadeloupe uniquement
|
||||
- "Ville Pointe-à-Pitre" → Guadeloupe uniquement
|
||||
|
||||
**Interface publicitaire** :
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────┐
|
||||
│ Ciblage géographique │
|
||||
├────────────────────────────────────────┤
|
||||
│ ○ National (France entière) │
|
||||
│ ● Région │
|
||||
│ [Provence-Alpes-Côte d'Azur ▼] │
|
||||
│ │
|
||||
│ ○ Département │
|
||||
│ [13 - Bouches-du-Rhône ▼] │
|
||||
│ │
|
||||
│ ○ Ville │
|
||||
│ ○ Point GPS + rayon │
|
||||
└────────────────────────────────────────┘
|
||||
|
||||
ℹ️ Note : "National (France entière)" inclut les DOM
|
||||
(Guadeloupe, Martinique, Réunion, Guyane, Mayotte)
|
||||
```
|
||||
|
||||
**Cas d'usage et cohérence** :
|
||||
|
||||
**Cas 1 : Publicitaire local Guadeloupe**
|
||||
```
|
||||
Restaurant à Pointe-à-Pitre
|
||||
Campagne :
|
||||
- Zone : Guadeloupe (département 971)
|
||||
- Horaires : 12h-14h (rush déjeuner)
|
||||
|
||||
User Guadeloupe à 12h30 locale → ✅ Diffusion (dans zone + horaire)
|
||||
User Métropole à 12h30 locale → ❌ Pas diffusion (hors zone géo)
|
||||
User Martinique à 12h30 locale → ❌ Pas diffusion (hors zone géo)
|
||||
```
|
||||
|
||||
**Cas 2 : Campagne nationale rush matin**
|
||||
```
|
||||
Assureur national
|
||||
Campagne :
|
||||
- Zone : France (nationale)
|
||||
- Horaires : 7h-9h + 17h-19h
|
||||
|
||||
User Marseille 8h locale → ✅ Diffusion (rush matin métropole)
|
||||
User Réunion 8h locale → ✅ Diffusion (rush matin Réunion, UTC+4)
|
||||
→ En métropole il est 5h (nuit), mais user Réunion est bien en rush matin
|
||||
```
|
||||
|
||||
**Cas 3 : User en déplacement change de fuseau**
|
||||
```
|
||||
User en métropole
|
||||
→ Télécharge 50 contenus + pubs
|
||||
→ Part en vacances Réunion (UTC+4)
|
||||
→ Device détecte nouveau fuseau (GPS)
|
||||
→ Écoute à 8h locale Réunion
|
||||
|
||||
Filtrage pubs :
|
||||
→ Heure locale = 8h Réunion
|
||||
→ Campagne 7h-9h → ✅ Diffusion
|
||||
```
|
||||
|
||||
**Justification** :
|
||||
- ✅ **UX intuitive pour publicitaires** : "7h-9h" = matin partout (pas besoin comprendre UTC)
|
||||
- ✅ **Équité géographique** : pas de discrimination DOM-TOM, publicitaires locaux peuvent cibler local, campagnes nationales touchent tous Français
|
||||
- ✅ **Simplicité technique** : détection fuseau automatique (GPS ou device), PostgreSQL `AT TIME ZONE` pour calculs backend
|
||||
- ✅ **Standard industrie** : Google Ads, Facebook Ads = heure locale par défaut
|
||||
|
||||
**Étalement budget** :
|
||||
```
|
||||
Exemple campagne :
|
||||
@@ -131,7 +253,7 @@ Calcul automatique :
|
||||
**Ciblage intelligent** :
|
||||
- Géolocalisation prioritaire (point GPS > ville > département > région > national)
|
||||
- Centres d'intérêt secondaires (tags utilisateur)
|
||||
- Horaire (campagne 7h-9h → diffusion uniquement pendant plage)
|
||||
- Horaire (campagne 7h-9h → diffusion uniquement pendant plage **heure locale utilisateur**, voir section 6.1 pour détails fuseaux horaires et DOM-TOM)
|
||||
|
||||
**Volume audio normalisé** :
|
||||
- Pub normalisée à **-14 LUFS** (standard broadcast)
|
||||
44
docs/domains/content/README.md
Normal file
44
docs/domains/content/README.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Domaine : Content
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
Le domaine **Content** gère toute la création, publication et diffusion des contenus audio sur RoadWave. C'est un **Supporting Subdomain** essentiel qui couvre les audio-guides, les radios live et les contenus géolocalisés.
|
||||
|
||||
## Responsabilités
|
||||
|
||||
- **Création et publication** : Workflow de création de contenu par les créateurs
|
||||
- **Audio-guides multi-séquences** : Gestion des parcours audio structurés
|
||||
- **Radio live** : Diffusion en direct et enregistrements
|
||||
- **Contenus géolocalisés** : Association de contenus à des zones géographiques
|
||||
- **Détection de contenu protégé** : Prévention des violations de droits d'auteur
|
||||
|
||||
## Règles métier
|
||||
|
||||
- [Création et publication de contenu](rules/creation-publication.md)
|
||||
- [Audio-guides multi-séquences](rules/audio-guides.md)
|
||||
- [Radio live](rules/radio-live.md)
|
||||
- [Contenus géolocalisés en voiture](rules/contenus-geolocalises.md)
|
||||
- [Détection de contenu protégé](rules/detection-contenu-protege.md)
|
||||
|
||||
## Modèle de données
|
||||
|
||||
- [Diagramme entités audio-guides](entities/modele-audio-guides.md) - Entités : AUDIO_GUIDES, GUIDE_SEQUENCES
|
||||
- [Diagramme entités radio live](entities/modele-radio-live.md) - Entités : LIVE_STREAMS, LIVE_RECORDINGS
|
||||
|
||||
## Ubiquitous Language
|
||||
|
||||
**Termes métier du domaine** :
|
||||
- **Audio Guide** : Contenu structuré en séquences géolocalisées
|
||||
- **Guide Sequence** : Segment d'un audio-guide déclenché à un point GPS précis
|
||||
- **Live Stream** : Diffusion audio en temps réel
|
||||
- **Live Recording** : Enregistrement automatique d'un live pour réécoute
|
||||
- **Geofence** : Zone géographique déclenchant un contenu
|
||||
- **Content Fingerprint** : Empreinte numérique pour détecter le contenu protégé
|
||||
- **Creator** : Utilisateur créant et publiant du contenu
|
||||
|
||||
## Dépendances
|
||||
|
||||
- ✅ Dépend de : `_shared` (users, contents base)
|
||||
- ⚠️ Interactions avec : `moderation` (modération de contenu)
|
||||
- ⚠️ Interactions avec : `monetization` (revenus créateurs)
|
||||
- ⚠️ Utilisé par : `recommendation` (métadonnées pour scoring)
|
||||
69
docs/domains/content/entities/modele-audio-guides.md
Normal file
69
docs/domains/content/entities/modele-audio-guides.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Modèle de données - Audio-guides
|
||||
|
||||
📖 Voir [Règles métier - Section 06 : Audio-guides multi-séquences](../rules/audio-guides.md) | [Entités globales](../../_shared/entities/modele-global.md)
|
||||
|
||||
## Diagramme
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
AUDIO_GUIDES }o--|| USERS : "créé par"
|
||||
AUDIO_GUIDES ||--o{ GUIDE_SEQUENCES : "contient"
|
||||
AUDIO_GUIDES ||--o{ USER_GUIDE_PROGRESS : "progression"
|
||||
|
||||
GUIDE_SEQUENCES }o--|| AUDIO_GUIDES : "appartient à"
|
||||
|
||||
USER_GUIDE_PROGRESS }o--|| USERS : "utilisateur"
|
||||
USER_GUIDE_PROGRESS }o--|| AUDIO_GUIDES : "guide"
|
||||
USER_GUIDE_PROGRESS }o--|| GUIDE_SEQUENCES : "séquence actuelle"
|
||||
|
||||
AUDIO_GUIDES {
|
||||
uuid id PK
|
||||
uuid creator_id FK
|
||||
string title
|
||||
text description
|
||||
string travel_mode
|
||||
int recommended_speed_min_kmh
|
||||
int recommended_speed_max_kmh
|
||||
int sequences_count
|
||||
int total_duration_seconds
|
||||
decimal total_distance_meters
|
||||
polygon diffusion_zone
|
||||
string[] tags
|
||||
string age_rating
|
||||
string status
|
||||
timestamp published_at
|
||||
}
|
||||
|
||||
GUIDE_SEQUENCES {
|
||||
uuid id PK
|
||||
uuid guide_id FK
|
||||
int sequence_order
|
||||
string title
|
||||
string audio_url
|
||||
int duration_seconds
|
||||
point gps_location
|
||||
int trigger_radius_meters
|
||||
boolean requires_manual_trigger
|
||||
timestamp created_at
|
||||
}
|
||||
|
||||
USER_GUIDE_PROGRESS {
|
||||
uuid id PK
|
||||
uuid user_id FK
|
||||
uuid guide_id FK
|
||||
uuid current_sequence_id FK
|
||||
int sequences_completed_count
|
||||
decimal completion_percentage
|
||||
timestamp started_at
|
||||
timestamp last_updated
|
||||
timestamp completed_at
|
||||
}
|
||||
```
|
||||
|
||||
## Légende
|
||||
|
||||
**Entités audio-guides** :
|
||||
|
||||
- **AUDIO_GUIDES** : Audio-guides multi-séquences - Travel_mode : `pedestrian` (manuel), `car` (auto GPS + manuel), `bicycle` (auto GPS + manuel), `transport` (auto GPS + manuel) - Sequences_count : Min 2, Max 50 séquences - Status : `draft`, `pending_review`, `published`, `archived` - Diffusion_zone : Polygon géographique (où l'audio-guide est recommandé)
|
||||
- **GUIDE_SEQUENCES** : Séquences audio géolocalisées - Sequence_order : Ordre lecture 1, 2, 3... - Trigger_radius : 10-100m selon mode (piéton 10m, voiture 50m) - Requires_manual_trigger : true si mode piéton (bouton "Suivant"), false si auto GPS - GPS_location : Point WGS84 (latitude, longitude) sauf mode piéton
|
||||
- **USER_GUIDE_PROGRESS** : Progression utilisateur - Completion_percentage : 0-100% (nb séquences complétées / total) - Current_sequence_id : Dernière séquence écoutée (pour reprise) - Started_at : Date démarrage parcours - Completed_at : NULL si en cours, timestamp si terminé
|
||||
63
docs/domains/content/entities/modele-radio-live.md
Normal file
63
docs/domains/content/entities/modele-radio-live.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# Modèle de données - Radio Live
|
||||
|
||||
📖 Voir [Règles métier - Section 12 : Radio Live](../rules/radio-live.md) | [Entités globales](../../_shared/entities/modele-global.md)
|
||||
|
||||
## Diagramme
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
LIVE_STREAMS }o--|| USERS : "diffusé par"
|
||||
LIVE_STREAMS ||--o{ LIVE_RECORDINGS : "enregistrement"
|
||||
LIVE_STREAMS ||--o{ LIVE_LISTENERS : "auditeurs"
|
||||
|
||||
LIVE_LISTENERS }o--|| USERS : "écoute"
|
||||
LIVE_LISTENERS }o--|| LIVE_STREAMS : "stream"
|
||||
|
||||
LIVE_STREAMS {
|
||||
uuid id PK
|
||||
uuid creator_id FK
|
||||
string title
|
||||
string[] tags
|
||||
string age_rating
|
||||
string geo_type
|
||||
jsonb geo_data
|
||||
string status
|
||||
string stream_key
|
||||
string playback_url
|
||||
int current_listeners_count
|
||||
int peak_listeners_count
|
||||
timestamp started_at
|
||||
timestamp ended_at
|
||||
int duration_seconds
|
||||
}
|
||||
|
||||
LIVE_RECORDINGS {
|
||||
uuid id PK
|
||||
uuid stream_id FK
|
||||
string audio_url
|
||||
int duration_seconds
|
||||
int file_size_bytes
|
||||
string status
|
||||
boolean auto_publish
|
||||
timestamp recorded_at
|
||||
timestamp processed_at
|
||||
}
|
||||
|
||||
LIVE_LISTENERS {
|
||||
uuid id PK
|
||||
uuid stream_id FK
|
||||
uuid user_id FK
|
||||
int listen_duration_seconds
|
||||
boolean was_notified
|
||||
timestamp joined_at
|
||||
timestamp left_at
|
||||
}
|
||||
```
|
||||
|
||||
## Légende
|
||||
|
||||
**Entités radio live** :
|
||||
|
||||
- **LIVE_STREAMS** : Streams audio temps réel - Status : `preparing` (buffer 15s initial), `live` (diffusion publique), `ended`, `interrupted` - Stream_key : WebRTC ingestion unique par créateur - Playback_url : HLS m3u8 pour diffusion clients - Geo_type : `city`, `department`, `region`, `national` (zone diffusion) - Durée max 8h par session - Déconnexion <60s : reconnexion auto, ≥60s : arrêt auto - Notification push abonnés dans zone géo au démarrage
|
||||
- **LIVE_RECORDINGS** : Enregistrements replay auto - Enregistrement obligatoire et automatique pendant live - Status : `recording`, `processing` (transcode HLS), `published`, `deleted` - Auto_publish : true par défaut (créateur peut désactiver) - Processing : Job asynchrone FFmpeg (Opus → HLS segments) - Replay disponible sous 5-15 min après fin live
|
||||
- **LIVE_LISTENERS** : Auditeurs live - Join/leave tracking temps réel - Was_notified : true si reçu push notification (analyse efficacité) - Listen_duration : Temps écoute effectif (pour stats créateur) - Peak listeners : Maximum simultané (métrique clé engagement)
|
||||
@@ -0,0 +1,205 @@
|
||||
# language: fr
|
||||
|
||||
@api @audio-guides @navigation @ui @mvp
|
||||
Fonctionnalité: Affichage avancé distance, direction et ETA
|
||||
|
||||
En tant qu'utilisateur
|
||||
Je veux voir la distance, la direction et le temps d'arrivée estimé vers les points d'intérêt
|
||||
Afin de planifier mon déplacement et anticiper les prochaines séquences
|
||||
|
||||
Contexte:
|
||||
Étant donné que le système affiche les informations suivantes:
|
||||
| Information | Format | Mise à jour |
|
||||
| Distance | Mètres / Kilomètres | Temps réel |
|
||||
| Direction | Boussole + Flèche | Temps réel |
|
||||
| ETA | Minutes / Heures | Dynamique |
|
||||
| Vitesse utilisateur | km/h (mode voiture) | Temps réel |
|
||||
|
||||
Scénario: Affichage de la distance en mètres pour proximité < 1km
|
||||
Étant donné un utilisateur "alice@roadwave.fr" en mode piéton
|
||||
Et elle se trouve à 450m du Panthéon
|
||||
Quand elle consulte l'écran de l'audio-guide
|
||||
Alors la distance affichée est: "450 m"
|
||||
Et la précision de la distance est de ±10m
|
||||
Et un événement "DISTANCE_DISPLAYED" est enregistré avec unité: "meters", valeur: 450
|
||||
Et la métrique "distance.displayed.meters" est incrémentée
|
||||
|
||||
Scénario: Affichage de la distance en kilomètres pour distance > 1km
|
||||
Étant donné un utilisateur "bob@roadwave.fr" en mode voiture
|
||||
Et il se trouve à 12.5 km du Château de Chambord
|
||||
Quand il consulte l'écran de l'audio-guide
|
||||
Alors la distance affichée est: "12.5 km"
|
||||
Et la précision de la distance est de ±100m
|
||||
Et un événement "DISTANCE_DISPLAYED" est enregistré avec unité: "kilometers", valeur: 12.5
|
||||
Et la métrique "distance.displayed.kilometers" est incrémentée
|
||||
|
||||
Scénario: Mise à jour en temps réel de la distance pendant le déplacement
|
||||
Étant donné un utilisateur "charlie@roadwave.fr" en mode piéton
|
||||
Et il marche vers la Sainte-Chapelle initialement à 800m
|
||||
Quand il marche à une vitesse de 5 km/h
|
||||
Alors la distance est mise à jour toutes les 2 secondes:
|
||||
| Temps | Distance affichée |
|
||||
| T+0s | 800 m |
|
||||
| T+30s | 760 m |
|
||||
| T+60s | 720 m |
|
||||
| T+90s | 680 m |
|
||||
Et la barre de progression visuelle se remplit progressivement
|
||||
Et un événement "DISTANCE_UPDATED" est enregistré toutes les 10 secondes
|
||||
Et la métrique "distance.real_time_updates" est incrémentée
|
||||
|
||||
Scénario: Affichage de la direction avec boussole et flèche
|
||||
Étant donné un utilisateur "david@roadwave.fr" en mode piéton
|
||||
Et il se trouve face au nord
|
||||
Et le Panthéon est au sud-est de sa position
|
||||
Quand il consulte l'écran de l'audio-guide
|
||||
Alors une boussole s'affiche avec:
|
||||
| Élément | Affichage |
|
||||
| Orientation boussole | Nord en haut |
|
||||
| Flèche vers POI | Pointe vers 135° (sud-est) |
|
||||
| Angle cardinal | "SE" (sud-est) |
|
||||
| Rotation dynamique | Suit l'orientation du téléphone|
|
||||
Et la flèche est colorée selon la distance:
|
||||
| Distance | Couleur |
|
||||
| < 100m | Vert |
|
||||
| 100m - 500m | Orange |
|
||||
| > 500m | Bleu |
|
||||
Et un événement "DIRECTION_DISPLAYED" est enregistré avec angle: 135
|
||||
Et la métrique "direction.displayed" est incrémentée
|
||||
|
||||
Scénario: Mise à jour de la direction en temps réel lors de la rotation
|
||||
Étant donné un utilisateur "eve@roadwave.fr" en mode piéton
|
||||
Et elle se trouve face au nord avec le Panthéon au sud-est
|
||||
Quand elle tourne son téléphone vers l'est
|
||||
Alors la boussole pivote dynamiquement
|
||||
Et la flèche vers le POI reste fixée sur la direction réelle (135°)
|
||||
Et l'affichage est fluide à 60 FPS
|
||||
Et un événement "COMPASS_ROTATED" est enregistré
|
||||
Et la métrique "compass.rotations" est incrémentée
|
||||
|
||||
Scénario: Calcul de l'ETA en mode piéton (vitesse moyenne 5 km/h)
|
||||
Étant donné un utilisateur "frank@roadwave.fr" en mode piéton
|
||||
Et il se trouve à 600m du Jardin du Luxembourg
|
||||
Quand le système calcule l'ETA avec vitesse piéton moyenne: 5 km/h
|
||||
Alors l'ETA affiché est: "7 min"
|
||||
Et le calcul utilise la formule: temps = distance / vitesse_moyenne_pieton
|
||||
Et un événement "ETA_CALCULATED" est enregistré avec mode: "pedestrian", eta: 7
|
||||
Et la métrique "eta.calculated.pedestrian" est incrémentée
|
||||
|
||||
Scénario: Calcul de l'ETA en mode voiture avec vitesse réelle
|
||||
Étant donné un utilisateur "grace@roadwave.fr" en mode voiture
|
||||
Et elle se trouve à 15 km du Château de Chenonceau
|
||||
Et elle roule actuellement à 80 km/h
|
||||
Quand le système calcule l'ETA
|
||||
Alors l'ETA affiché est: "11 min"
|
||||
Et le calcul utilise la vitesse réelle actuelle
|
||||
Et un événement "ETA_CALCULATED" est enregistré avec mode: "car", vitesse: 80, eta: 11
|
||||
Et la métrique "eta.calculated.car" est incrémentée
|
||||
|
||||
Scénario: Recalcul dynamique de l'ETA en fonction des changements de vitesse
|
||||
Étant donné un utilisateur "henry@roadwave.fr" en mode voiture
|
||||
Et l'ETA initial vers le Château d'Amboise est: "15 min" (vitesse: 70 km/h)
|
||||
Quand il ralentit à 40 km/h à cause d'un bouchon
|
||||
Alors l'ETA est recalculé et mis à jour: "22 min"
|
||||
Et une notification discrète s'affiche: "ETA mis à jour : +7 min"
|
||||
Quand il accélère à nouveau à 90 km/h
|
||||
Alors l'ETA est recalculé: "12 min"
|
||||
Et un événement "ETA_UPDATED" est enregistré avec ancienETA: 22, nouveauETA: 12
|
||||
Et la métrique "eta.recalculated" est incrémentée
|
||||
|
||||
Scénario: Affichage du temps d'arrivée absolu en mode voiture
|
||||
Étant donné un utilisateur "iris@roadwave.fr" en mode voiture
|
||||
Et il est 14h30
|
||||
Et l'ETA vers le prochain point est: "25 min"
|
||||
Quand elle active l'option "Afficher l'heure d'arrivée"
|
||||
Alors l'affichage change de "25 min" à "Arrivée à 14h55"
|
||||
Et les deux formats peuvent être basculés par un tap sur l'ETA
|
||||
Et un événement "ETA_FORMAT_CHANGED" est enregistré avec format: "absolute_time"
|
||||
Et la métrique "eta.format.absolute" est incrémentée
|
||||
|
||||
Scénario: Affichage groupé distance + direction + ETA sur une carte compacte
|
||||
Étant donné un utilisateur "jack@roadwave.fr" en mode piéton
|
||||
Et il se trouve à 450m du Panthéon au sud-est
|
||||
Quand il consulte la carte de l'audio-guide
|
||||
Alors une carte compacte s'affiche pour chaque point d'intérêt:
|
||||
| Point d'intérêt | Distance | Direction | ETA |
|
||||
| Panthéon | 450 m | SE ↗ | 5 min |
|
||||
| Jardin Lux. | 1.2 km | SO ↙ | 14 min |
|
||||
| Sorbonne | 320 m | E → | 4 min |
|
||||
Et les points sont triés par distance (plus proche en premier)
|
||||
Et un événement "POI_LIST_DISPLAYED" est enregistré
|
||||
Et la métrique "poi_list.displayed" est incrémentée
|
||||
|
||||
Scénario: Indication visuelle "Vous y êtes !" à l'arrivée
|
||||
Étant donné un utilisateur "kate@roadwave.fr" en mode piéton
|
||||
Et elle approche du Panthéon
|
||||
Quand elle entre dans un rayon de 10m du point d'intérêt
|
||||
Alors l'affichage change de "15 m" à "🎯 Vous y êtes !"
|
||||
Et une animation de succès est jouée
|
||||
Et une notification sonore subtile est jouée
|
||||
Et l'audio de la séquence démarre automatiquement
|
||||
Et un événement "POI_ARRIVED" est enregistré avec précision: 8m
|
||||
Et la métrique "poi.arrived" est incrémentée
|
||||
|
||||
Scénario: Affichage du trajet à vol d'oiseau vs trajet routier
|
||||
Étant donné un utilisateur "luke@roadwave.fr" en mode voiture
|
||||
Et il se trouve à 12 km à vol d'oiseau du Château de Chambord
|
||||
Mais le trajet routier est de 18 km (détours)
|
||||
Quand il consulte l'ETA
|
||||
Alors la distance affichée est celle du trajet routier: "18 km"
|
||||
Et l'ETA est calculé sur le trajet routier: "15 min"
|
||||
Et un bouton "Itinéraire" permet de voir le trajet détaillé
|
||||
Et un événement "ROUTE_DISPLAYED" est enregistré avec routeDistance: 18, airDistance: 12
|
||||
Et la métrique "route.displayed" est incrémentée
|
||||
|
||||
Scénario: Mode d'économie de batterie avec mise à jour moins fréquente
|
||||
Étant donné un utilisateur "mary@roadwave.fr" avec batterie < 20%
|
||||
Et le mode économie d'énergie est activé
|
||||
Quand elle utilise l'audio-guide
|
||||
Alors la fréquence de mise à jour des distances est réduite:
|
||||
| Mode normal | Mode économie |
|
||||
| Toutes les 2s | Toutes les 10s|
|
||||
Et la précision GPS est réduite (précision: ±30m au lieu de ±10m)
|
||||
Et un événement "BATTERY_SAVER_ENABLED" est enregistré
|
||||
Et la métrique "battery_saver.enabled" est incrémentée
|
||||
|
||||
Scénario: Affichage de la vitesse actuelle en mode voiture
|
||||
Étant donné un utilisateur "nathan@roadwave.fr" en mode voiture
|
||||
Et il roule à 75 km/h
|
||||
Quand il consulte l'écran de l'audio-guide
|
||||
Alors sa vitesse actuelle est affichée: "75 km/h"
|
||||
Et la vitesse est mise à jour en temps réel
|
||||
Et un événement "SPEED_DISPLAYED" est enregistré avec vitesse: 75
|
||||
Et la métrique "speed.displayed" est incrémentée
|
||||
|
||||
Scénario: Alerte de dépassement de limite de vitesse (optionnel)
|
||||
Étant donné un utilisateur "olive@roadwave.fr" en mode voiture
|
||||
Et elle a activé l'option "Alertes de vitesse"
|
||||
Et la limite de vitesse sur sa route est 80 km/h
|
||||
Quand elle roule à 95 km/h
|
||||
Alors une alerte visuelle discrète s'affiche: "⚠️ 95 km/h (limite: 80)"
|
||||
Et l'alerte disparaît quand elle ralentit en dessous de 85 km/h
|
||||
Et un événement "SPEED_LIMIT_EXCEEDED" est enregistré avec vitesse: 95, limite: 80
|
||||
Et la métrique "speed.limit_exceeded" est incrémentée
|
||||
|
||||
Scénario: Indication de zones à forte densité de points d'intérêt
|
||||
Étant donné un utilisateur "paul@roadwave.fr" en mode piéton
|
||||
Et il se trouve dans une zone avec 5 points d'intérêt dans un rayon de 200m
|
||||
Quand il consulte la carte
|
||||
Alors une indication s'affiche: "Zone dense : 5 points à proximité"
|
||||
Et les marqueurs sont regroupés en cluster pour éviter la surcharge visuelle
|
||||
Et en zoomant, le cluster se décompose en marqueurs individuels
|
||||
Et un événement "HIGH_DENSITY_ZONE_DETECTED" est enregistré avec count: 5
|
||||
Et la métrique "zones.high_density" est incrémentée
|
||||
|
||||
Scénario: Métriques de précision de la localisation GPS
|
||||
Étant donné un utilisateur "quinn@roadwave.fr" utilisant l'audio-guide
|
||||
Quand les métriques de précision GPS sont collectées
|
||||
Alors les indicateurs suivants sont disponibles:
|
||||
| Métrique | Valeur cible |
|
||||
| Précision GPS moyenne | ±10m |
|
||||
| Précision GPS en mode économie | ±30m |
|
||||
| Fréquence de mise à jour GPS | 1-2 Hz |
|
||||
| Taux d'erreur de positionnement | < 5% |
|
||||
| Latence de calcul ETA | < 100ms |
|
||||
Et les métriques sont exportées vers le système de monitoring
|
||||
Et des alertes sont déclenchées si précision > ±50m
|
||||
@@ -0,0 +1,402 @@
|
||||
# language: fr
|
||||
|
||||
Fonctionnalité: API - Création et gestion d'audio-guides multi-séquences
|
||||
En tant que système backend
|
||||
Je veux exposer des endpoints pour créer et gérer les audio-guides
|
||||
Afin de permettre aux créateurs de publier des expériences guidées
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est démarrée
|
||||
Et que le créateur "guide@example.com" est authentifié avec un token JWT valide
|
||||
Et que son compte est vérifié (email_verified: true)
|
||||
|
||||
# 16.1.2 - Endpoints de création
|
||||
|
||||
Scénario: POST /api/v1/audio-guides - Création d'un audio-guide
|
||||
Étant donné la requête suivante:
|
||||
"""json
|
||||
{
|
||||
"title": "Safari du Paugre",
|
||||
"description": "Découvrez les animaux du parc en voiture sur un circuit de 5km",
|
||||
"mode": "voiture",
|
||||
"vitesse_recommandee": "30-50 km/h",
|
||||
"tags": ["nature", "animaux", "famille"],
|
||||
"classification_age": "tout_public",
|
||||
"zone_diffusion": {
|
||||
"type": "polygon",
|
||||
"coordinates": [[2.5678, 43.1234], [2.5690, 43.1245], [2.5700, 43.1250]]
|
||||
}
|
||||
}
|
||||
"""
|
||||
Quand je fais un POST sur "/api/v1/audio-guides"
|
||||
Alors le code HTTP de réponse est 201
|
||||
Et le corps de réponse contient:
|
||||
| champ | valeur |
|
||||
| id | UUID généré |
|
||||
| status | draft |
|
||||
| creator_id | ID du créateur |
|
||||
| sequences_count | 0 |
|
||||
| created_at | Timestamp actuel |
|
||||
|
||||
Scénario: Validation du titre (longueur 5-100 caractères)
|
||||
Étant donné la requête avec titre "ABC"
|
||||
Quand je fais un POST sur "/api/v1/audio-guides"
|
||||
Alors le code HTTP de réponse est 400
|
||||
Et le message d'erreur est "title: doit contenir entre 5 et 100 caractères"
|
||||
|
||||
Scénario: Validation de la description (longueur 10-500 caractères)
|
||||
Étant donné la requête avec description de 8 caractères
|
||||
Quand je fais un POST sur "/api/v1/audio-guides"
|
||||
Alors le code HTTP de réponse est 400
|
||||
Et le message d'erreur est "description: doit contenir entre 10 et 500 caractères"
|
||||
|
||||
Plan du Scénario: Validation du mode de déplacement
|
||||
Étant donné la requête avec mode "<mode>"
|
||||
Quand je fais un POST sur "/api/v1/audio-guides"
|
||||
Alors le code HTTP de réponse est <code>
|
||||
|
||||
Exemples:
|
||||
| mode | code |
|
||||
| pieton | 201 |
|
||||
| voiture | 201 |
|
||||
| velo | 201 |
|
||||
| transport | 201 |
|
||||
| avion | 400 |
|
||||
| invalid | 400 |
|
||||
|
||||
Scénario: Validation tags (1-3 tags obligatoires)
|
||||
Étant donné la requête avec 0 tags
|
||||
Quand je fais un POST sur "/api/v1/audio-guides"
|
||||
Alors le code HTTP de réponse est 400
|
||||
Et le message d'erreur est "tags: minimum 1 tag requis, maximum 3"
|
||||
|
||||
Scénario: Validation classification âge
|
||||
Étant donné la requête avec classification_age "invalide"
|
||||
Quand je fais un POST sur "/api/v1/audio-guides"
|
||||
Alors le code HTTP de réponse est 400
|
||||
Et le message d'erreur contient "classification_age: valeurs autorisées [tout_public, 13+, 16+, 18+]"
|
||||
|
||||
# Ajout de séquences
|
||||
|
||||
Scénario: POST /api/v1/audio-guides/{id}/sequences - Ajout première séquence
|
||||
Étant donné un audio-guide draft avec ID "ag_123"
|
||||
Et la requête suivante:
|
||||
"""json
|
||||
{
|
||||
"order": 1,
|
||||
"title": "Introduction - Point d'accueil",
|
||||
"audio_file": "base64_encoded_mp3_data",
|
||||
"gps_point": {
|
||||
"latitude": 43.1234,
|
||||
"longitude": 2.5678
|
||||
},
|
||||
"rayon_declenchement": 30
|
||||
}
|
||||
"""
|
||||
Quand je fais un POST sur "/api/v1/audio-guides/ag_123/sequences"
|
||||
Alors le code HTTP de réponse est 201
|
||||
Et la séquence est créée avec:
|
||||
| champ | valeur |
|
||||
| sequence_id | UUID généré |
|
||||
| order | 1 |
|
||||
| duration | Calculée depuis audio |
|
||||
| status | uploaded |
|
||||
|
||||
Scénario: Validation format audio (MP3, AAC, M4A uniquement)
|
||||
Étant donné un audio-guide draft
|
||||
Et un fichier audio au format WAV
|
||||
Quand je fais un POST sur "/api/v1/audio-guides/{id}/sequences"
|
||||
Alors le code HTTP de réponse est 400
|
||||
Et le message d'erreur est "audio_file: format non supporté. Formats acceptés: MP3, AAC, M4A"
|
||||
|
||||
Scénario: Validation taille audio (max 200 MB)
|
||||
Étant donné un audio-guide draft
|
||||
Et un fichier audio de 250 MB
|
||||
Quand je fais un POST sur "/api/v1/audio-guides/{id}/sequences"
|
||||
Alors le code HTTP de réponse 413
|
||||
Et le message d'erreur est "audio_file: taille maximale 200 MB dépassée"
|
||||
|
||||
Scénario: Validation durée audio (max 15 minutes)
|
||||
Étant donné un audio-guide draft
|
||||
Et un fichier audio de 18 minutes
|
||||
Quand je fais un POST sur "/api/v1/audio-guides/{id}/sequences"
|
||||
Alors le code HTTP de réponse est 400
|
||||
Et le message d'erreur est "audio_file: durée maximale 15 minutes dépassée"
|
||||
|
||||
Scénario: Point GPS obligatoire sauf mode piéton
|
||||
Étant donné un audio-guide en mode "voiture"
|
||||
Et une séquence sans gps_point
|
||||
Quand je fais un POST sur "/api/v1/audio-guides/{id}/sequences"
|
||||
Alors le code HTTP de réponse est 400
|
||||
Et le message d'erreur est "gps_point: obligatoire pour mode voiture"
|
||||
|
||||
Scénario: Point GPS optionnel en mode piéton
|
||||
Étant donné un audio-guide en mode "pieton"
|
||||
Et une séquence sans gps_point
|
||||
Quand je fais un POST sur "/api/v1/audio-guides/{id}/sequences"
|
||||
Alors le code HTTP de réponse est 201
|
||||
Et la séquence est créée sans point GPS
|
||||
|
||||
Plan du Scénario: Rayon de déclenchement par défaut selon mode
|
||||
Étant donné un audio-guide en mode "<mode>"
|
||||
Et une séquence sans rayon_declenchement spécifié
|
||||
Quand je fais un POST sur "/api/v1/audio-guides/{id}/sequences"
|
||||
Alors le rayon par défaut appliqué est <rayon>
|
||||
|
||||
Exemples:
|
||||
| mode | rayon |
|
||||
| voiture | 30 |
|
||||
| velo | 50 |
|
||||
| transport | 100 |
|
||||
|
||||
Scénario: Validation rayon configurable (10-200m)
|
||||
Étant donné un audio-guide
|
||||
Et une séquence avec rayon_declenchement 250
|
||||
Quand je fais un POST sur "/api/v1/audio-guides/{id}/sequences"
|
||||
Alors le code HTTP de réponse est 400
|
||||
Et le message d'erreur est "rayon_declenchement: doit être entre 10 et 200 mètres"
|
||||
|
||||
Scénario: Nombre maximum de séquences (50)
|
||||
Étant donné un audio-guide avec 50 séquences
|
||||
Quand je tente d'ajouter une 51ème séquence
|
||||
Alors le code HTTP de réponse est 400
|
||||
Et le message d'erreur est "Maximum 50 séquences par audio-guide atteint"
|
||||
|
||||
# Modification et réordonnancement
|
||||
|
||||
Scénario: PATCH /api/v1/audio-guides/{id}/sequences/{seq_id} - Modification séquence
|
||||
Étant donné une séquence existante "seq_456"
|
||||
Et la requête suivante:
|
||||
"""json
|
||||
{
|
||||
"title": "Introduction - Point d'accueil (édité)",
|
||||
"rayon_declenchement": 40
|
||||
}
|
||||
"""
|
||||
Quand je fais un PATCH sur "/api/v1/audio-guides/ag_123/sequences/seq_456"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et les champs modifiés sont mis à jour
|
||||
Et updated_at est mis à jour
|
||||
|
||||
Scénario: PUT /api/v1/audio-guides/{id}/sequences/reorder - Réordonnancement
|
||||
Étant donné un audio-guide avec 5 séquences
|
||||
Et la requête suivante:
|
||||
"""json
|
||||
{
|
||||
"sequence_orders": [
|
||||
{"sequence_id": "seq_1", "order": 1},
|
||||
{"sequence_id": "seq_4", "order": 2},
|
||||
{"sequence_id": "seq_2", "order": 3},
|
||||
{"sequence_id": "seq_3", "order": 4},
|
||||
{"sequence_id": "seq_5", "order": 5}
|
||||
]
|
||||
}
|
||||
"""
|
||||
Quand je fais un PUT sur "/api/v1/audio-guides/ag_123/sequences/reorder"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et l'ordre des séquences est mis à jour en base
|
||||
|
||||
Scénario: DELETE /api/v1/audio-guides/{id}/sequences/{seq_id} - Suppression séquence
|
||||
Étant donné un audio-guide avec 3 séquences
|
||||
Quand je fais un DELETE sur "/api/v1/audio-guides/ag_123/sequences/seq_2"
|
||||
Alors le code HTTP de réponse est 204
|
||||
Et la séquence est supprimée
|
||||
Et l'ordre des séquences restantes est réajusté (1, 2)
|
||||
|
||||
# Publication et validation
|
||||
|
||||
Scénario: POST /api/v1/audio-guides/{id}/publish - Publication (nouveau créateur)
|
||||
Étant donné un audio-guide draft avec 3 séquences complètes
|
||||
Et que c'est le 2ème audio-guide du créateur
|
||||
Quand je fais un POST sur "/api/v1/audio-guides/ag_123/publish"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le status passe à "pending_moderation"
|
||||
Et moderation_required est true
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"status": "pending_moderation",
|
||||
"message": "Votre audio-guide est en cours de validation. Notre équipe le vérifiera sous 24-48h.",
|
||||
"estimated_validation": "2026-01-24T14:00:00Z"
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: Publication directe pour créateur expérimenté (>5 audio-guides validés)
|
||||
Étant donné un audio-guide draft avec 5 séquences
|
||||
Et que le créateur a publié 8 audio-guides validés
|
||||
Et qu'il n'a aucun strike actif
|
||||
Quand je fais un POST sur "/api/v1/audio-guides/ag_456/publish"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le status passe à "published"
|
||||
Et moderation_required est false
|
||||
Et l'audio-guide est immédiatement visible
|
||||
|
||||
Scénario: Validation nombre minimum de séquences (2)
|
||||
Étant donné un audio-guide draft avec 1 seule séquence
|
||||
Quand je fais un POST sur "/api/v1/audio-guides/ag_123/publish"
|
||||
Alors le code HTTP de réponse est 400
|
||||
Et le message d'erreur est "Minimum 2 séquences requis pour publication"
|
||||
|
||||
Scénario: Alerte cohérence - Points GPS trop éloignés
|
||||
Étant donné un audio-guide en mode "pieton"
|
||||
Et une séquence au Louvre (Paris)
|
||||
Et une séquence à Lyon (465 km de distance)
|
||||
Quand je fais un POST sur "/api/v1/audio-guides/ag_123/publish"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et un warning est retourné:
|
||||
"""json
|
||||
{
|
||||
"status": "pending_moderation",
|
||||
"warnings": [
|
||||
{
|
||||
"type": "distance_incohérence",
|
||||
"message": "Distance importante entre points (465 km). Vérifiez que le mode 'pieton' est approprié.",
|
||||
"severity": "warning"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
# Gestion des brouillons
|
||||
|
||||
Scénario: Sauvegarde automatique brouillon
|
||||
Étant donné un audio-guide draft non sauvegardé depuis 5 minutes
|
||||
Quand une modification est apportée via PATCH
|
||||
Alors le champ updated_at est mis à jour automatiquement
|
||||
Et le brouillon est sauvegardé en base
|
||||
|
||||
Scénario: GET /api/v1/audio-guides/drafts - Liste des brouillons
|
||||
Étant donné que le créateur a 2 brouillons:
|
||||
| id | title | sequences_count | updated_at |
|
||||
| ag_111 | Safari du Paugre | 3 | 2026-01-20 10:00:00 |
|
||||
| ag_222 | Visite du Louvre | 1 | 2026-01-15 14:30:00 |
|
||||
Quand je fais un GET sur "/api/v1/audio-guides/drafts"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient les 2 brouillons
|
||||
Et ils sont triés par updated_at décroissant
|
||||
|
||||
Scénario: DELETE /api/v1/audio-guides/{id} - Suppression brouillon
|
||||
Étant donné un audio-guide draft "ag_123"
|
||||
Quand je fais un DELETE sur "/api/v1/audio-guides/ag_123"
|
||||
Alors le code HTTP de réponse est 204
|
||||
Et l'audio-guide et toutes ses séquences sont supprimés
|
||||
Et les fichiers audio associés sont marqués pour suppression S3
|
||||
|
||||
# Modification audio-guide publié
|
||||
|
||||
Scénario: PATCH /api/v1/audio-guides/{id} - Modification métadonnées (publié)
|
||||
Étant donné un audio-guide publié "ag_789"
|
||||
Et la requête suivante:
|
||||
"""json
|
||||
{
|
||||
"title": "Safari du Paugre - Version 2",
|
||||
"description": "Nouvelle description améliorée",
|
||||
"tags": ["nature", "animaux", "enfants"]
|
||||
}
|
||||
"""
|
||||
Quand je fais un PATCH sur "/api/v1/audio-guides/ag_789"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et les métadonnées sont mises à jour
|
||||
Et le status reste "published" (pas de revalidation)
|
||||
|
||||
Scénario: Interdiction modification mode après publication
|
||||
Étant donné un audio-guide publié en mode "voiture"
|
||||
Et la requête avec mode "pieton"
|
||||
Quand je fais un PATCH sur "/api/v1/audio-guides/{id}"
|
||||
Alors le code HTTP de réponse est 400
|
||||
Et le message d'erreur est "mode: impossible de modifier le mode après publication"
|
||||
|
||||
Scénario: Interdiction modification GPS après publication
|
||||
Étant donné un audio-guide publié avec points GPS
|
||||
Et une tentative de modification des coordonnées GPS
|
||||
Quand je fais un PATCH sur "/api/v1/audio-guides/{id}/sequences/{seq_id}"
|
||||
Alors le code HTTP de réponse est 400
|
||||
Et le message d'erreur est "gps_point: impossible de modifier après publication. Créez une nouvelle version."
|
||||
|
||||
# Duplication
|
||||
|
||||
Scénario: POST /api/v1/audio-guides/{id}/duplicate - Duplication audio-guide
|
||||
Étant donné un audio-guide publié "ag_999" avec 12 séquences
|
||||
Quand je fais un POST sur "/api/v1/audio-guides/ag_999/duplicate"
|
||||
Alors le code HTTP de réponse est 201
|
||||
Et un nouvel audio-guide draft est créé
|
||||
Et le titre est "Safari du Paugre (copie)"
|
||||
Et toutes les séquences sont copiées avec les mêmes métadonnées
|
||||
Et les fichiers audio sont référencés (pas de duplication S3)
|
||||
|
||||
# Statistiques
|
||||
|
||||
Scénario: GET /api/v1/audio-guides/{id}/stats - Statistiques parcours
|
||||
Étant donné un audio-guide avec les séquences suivantes:
|
||||
| sequence | duration | gps_point | distance_to_next |
|
||||
| 1 | 2:15 | (43.1234, 2.5678) | 150m |
|
||||
| 2 | 3:42 | (43.1245, 2.5690) | 200m |
|
||||
| 3 | 4:10 | (43.1250, 2.5700) | null |
|
||||
Quand je fais un GET sur "/api/v1/audio-guides/ag_123/stats"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"sequences_count": 3,
|
||||
"total_duration": "10:07",
|
||||
"total_distance": "350m",
|
||||
"avg_sequence_duration": "3:22"
|
||||
}
|
||||
"""
|
||||
|
||||
# Gestion zone diffusion
|
||||
|
||||
Scénario: Validation zone diffusion (polygon géographique)
|
||||
Étant donné une zone diffusion de type "polygon"
|
||||
Et les coordonnées forment un polygon valide (min 3 points)
|
||||
Quand je fais un POST sur "/api/v1/audio-guides"
|
||||
Alors la zone est validée avec PostGIS ST_IsValid
|
||||
Et stockée en type geography
|
||||
|
||||
Scénario: Zone diffusion via API Nominatim (ville)
|
||||
Étant donné une zone diffusion de type "city"
|
||||
Et la valeur "Paris"
|
||||
Quand je fais un POST sur "/api/v1/audio-guides"
|
||||
Alors l'API interroge Nominatim pour récupérer le polygon de Paris
|
||||
Et le polygon est stocké en base
|
||||
|
||||
# Cas d'erreur
|
||||
|
||||
Scénario: Authentification requise
|
||||
Étant donné une requête sans token JWT
|
||||
Quand je fais un POST sur "/api/v1/audio-guides"
|
||||
Alors le code HTTP de réponse est 401
|
||||
Et le message d'erreur est "Authentification requise"
|
||||
|
||||
Scénario: Compte non vérifié
|
||||
Étant donné un créateur avec email_verified: false
|
||||
Quand je fais un POST sur "/api/v1/audio-guides"
|
||||
Alors le code HTTP de réponse est 403
|
||||
Et le message d'erreur est "Vérification email requise pour créer des audio-guides"
|
||||
|
||||
Scénario: Modification audio-guide d'un autre créateur (interdite)
|
||||
Étant donné un audio-guide appartenant au créateur "creator_A"
|
||||
Et une requête authentifiée par "creator_B"
|
||||
Quand je fais un PATCH sur "/api/v1/audio-guides/{id}"
|
||||
Alors le code HTTP de réponse est 403
|
||||
Et le message d'erreur est "Vous n'êtes pas autorisé à modifier cet audio-guide"
|
||||
|
||||
Scénario: Audio-guide inexistant
|
||||
Quand je fais un GET sur "/api/v1/audio-guides/ag_nonexistant"
|
||||
Alors le code HTTP de réponse est 404
|
||||
Et le message d'erreur est "Audio-guide non trouvé"
|
||||
|
||||
Scénario: Séquence inexistante
|
||||
Étant donné un audio-guide "ag_123"
|
||||
Quand je fais un DELETE sur "/api/v1/audio-guides/ag_123/sequences/seq_nonexistant"
|
||||
Alors le code HTTP de réponse est 404
|
||||
Et le message d'erreur est "Séquence non trouvée"
|
||||
|
||||
Scénario: Suppression audio-guide avec utilisateurs actifs
|
||||
Étant donné un audio-guide publié "ag_456"
|
||||
Et 20 utilisateurs ont une progression active
|
||||
Quand je fais un DELETE sur "/api/v1/audio-guides/ag_456"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et l'audio-guide est marqué "deleted" (soft delete)
|
||||
Et les progressions utilisateurs sont archivées pendant 30 jours
|
||||
Et un warning est retourné: "20 utilisateurs actifs. Progressions archivées 30 jours."
|
||||
@@ -0,0 +1,247 @@
|
||||
# language: fr
|
||||
|
||||
@api @audio-guides @content-creation @mvp
|
||||
Fonctionnalité: Wizard complet de création d'audio-guide multi-séquences
|
||||
|
||||
En tant que créateur de contenu
|
||||
Je veux créer un audio-guide avec plusieurs séquences géolocalisées
|
||||
Afin de proposer une expérience guidée immersive aux utilisateurs
|
||||
|
||||
Contexte:
|
||||
Étant donné que le système supporte les limites suivantes:
|
||||
| Paramètre | Valeur |
|
||||
| Nombre max de séquences par guide | 50 |
|
||||
| Taille max fichier audio | 100 MB |
|
||||
| Formats audio acceptés | MP3, M4A, WAV |
|
||||
| Durée max par séquence | 15 minutes |
|
||||
| Rayon min d'un point d'intérêt | 10 mètres |
|
||||
| Rayon max d'un point d'intérêt | 500 mètres |
|
||||
|
||||
Scénario: Création d'un audio-guide - Étape 1: Informations générales
|
||||
Étant donné un créateur "alice@roadwave.fr" connecté
|
||||
Quand le créateur clique sur "Créer un audio-guide"
|
||||
Alors le wizard s'ouvre sur l'étape 1 "Informations générales"
|
||||
Et le créateur remplit le formulaire:
|
||||
| Champ | Valeur |
|
||||
| Titre | Visite guidée du Quartier Latin |
|
||||
| Description courte | Découvrez l'histoire du quartier étudiant |
|
||||
| Description longue | Plongez dans 2000 ans d'histoire... |
|
||||
| Catégorie | Tourisme |
|
||||
| Langues disponibles | Français, Anglais |
|
||||
| Durée estimée | 2 heures |
|
||||
| Difficulté | Facile |
|
||||
| Accessibilité PMR | Oui |
|
||||
Et le créateur clique sur "Suivant"
|
||||
Alors les données sont validées et enregistrées en brouillon
|
||||
Et un événement "AUDIO_GUIDE_CREATION_STARTED" est enregistré
|
||||
Et la métrique "audio_guide.creation.step1_completed" est incrémentée
|
||||
|
||||
Scénario: Création d'un audio-guide - Étape 2: Image de couverture
|
||||
Étant donné un créateur "bob@roadwave.fr" à l'étape 2 du wizard
|
||||
Quand le créateur upload une image de couverture:
|
||||
| Propriété | Valeur |
|
||||
| Fichier | quartier-latin-cover.jpg |
|
||||
| Taille | 1920x1080 px |
|
||||
| Format | JPEG |
|
||||
| Poids | 2.5 MB |
|
||||
Alors l'image est uploadée vers le stockage S3
|
||||
Et une miniature est générée automatiquement (300x200 px)
|
||||
Et un aperçu de l'image est affiché
|
||||
Et le créateur peut recadrer l'image via un éditeur intégré
|
||||
Et le créateur clique sur "Suivant"
|
||||
Alors l'image est associée au brouillon
|
||||
Et un événement "AUDIO_GUIDE_COVER_UPLOADED" est enregistré
|
||||
Et la métrique "audio_guide.creation.step2_completed" est incrémentée
|
||||
|
||||
Scénario: Création d'un audio-guide - Étape 3: Ajout de séquences via carte
|
||||
Étant donné un créateur "charlie@roadwave.fr" à l'étape 3 du wizard
|
||||
Quand le créateur voit une carte interactive centrée sur Paris
|
||||
Et clique sur "Ajouter un point d'intérêt" sur la carte
|
||||
Et place un marqueur à la position: 48.8534, 2.3488 (Notre-Dame)
|
||||
Alors un formulaire de séquence s'ouvre:
|
||||
| Champ | Valeur par défaut |
|
||||
| Nom du point | [Vide] |
|
||||
| Position GPS | 48.8534, 2.3488 |
|
||||
| Rayon de déclenchement| 50 mètres |
|
||||
| Ordre dans le parcours| 1 |
|
||||
| Fichier audio | [Non uploadé] |
|
||||
Et le créateur remplit les informations:
|
||||
| Champ | Valeur |
|
||||
| Nom du point | Cathédrale Notre-Dame de Paris |
|
||||
| Rayon de déclenchement| 100 mètres |
|
||||
| Ordre dans le parcours| 1 |
|
||||
Et le créateur upload un fichier audio "notre-dame.mp3" (12 MB, 8min 30s)
|
||||
Et le créateur clique sur "Enregistrer le point"
|
||||
Alors la séquence 1 est créée et affichée sur la carte
|
||||
Et un événement "AUDIO_GUIDE_SEQUENCE_ADDED" est enregistré
|
||||
Et la métrique "audio_guide.sequences.added" est incrémentée
|
||||
|
||||
Scénario: Ajout de plusieurs séquences consécutives
|
||||
Étant donné un créateur "david@roadwave.fr" avec 1 séquence créée
|
||||
Quand le créateur ajoute 4 nouvelles séquences:
|
||||
| Ordre | Nom | Position GPS | Rayon | Audio |
|
||||
| 2 | Sainte-Chapelle | 48.8555, 2.3450 | 80m | chapelle.mp3 |
|
||||
| 3 | Panthéon | 48.8462, 2.3464 | 100m | pantheon.mp3 |
|
||||
| 4 | Jardin du Luxembourg | 48.8462, 2.3371 | 150m | jardin.mp3 |
|
||||
| 5 | Sorbonne | 48.8487, 2.3431 | 70m | sorbonne.mp3 |
|
||||
Alors les 5 séquences sont affichées sur la carte avec des marqueurs numérotés
|
||||
Et une ligne de parcours relie les points dans l'ordre
|
||||
Et la distance totale du parcours est calculée: 3.2 km
|
||||
Et la durée totale des audios est calculée: 42 minutes
|
||||
Et un panneau latéral liste les séquences avec possibilité de réorganiser
|
||||
Et un événement "AUDIO_GUIDE_SEQUENCES_BATCH_ADDED" est enregistré
|
||||
Et la métrique "audio_guide.sequences.count" est mise à jour: 5
|
||||
|
||||
Scénario: Réorganisation de l'ordre des séquences par drag & drop
|
||||
Étant donné un créateur "eve@roadwave.fr" avec 5 séquences créées
|
||||
Quand le créateur utilise le panneau latéral
|
||||
Et fait glisser la séquence #3 "Panthéon" vers la position #2
|
||||
Alors l'ordre des séquences est mis à jour:
|
||||
| Nouvel ordre | Nom |
|
||||
| 1 | Cathédrale Notre-Dame |
|
||||
| 2 | Panthéon |
|
||||
| 3 | Sainte-Chapelle |
|
||||
| 4 | Jardin du Luxembourg |
|
||||
| 5 | Sorbonne |
|
||||
Et la ligne de parcours sur la carte est recalculée
|
||||
Et la distance totale est recalculée: 3.5 km
|
||||
Et un événement "AUDIO_GUIDE_SEQUENCES_REORDERED" est enregistré
|
||||
Et la métrique "audio_guide.sequences.reordered" est incrémentée
|
||||
|
||||
Scénario: Modification d'une séquence existante
|
||||
Étant donné un créateur "frank@roadwave.fr" avec 5 séquences créées
|
||||
Quand le créateur clique sur le marqueur #2 "Panthéon" sur la carte
|
||||
Alors le formulaire d'édition s'ouvre avec les données actuelles
|
||||
Et le créateur modifie:
|
||||
| Champ | Ancienne valeur | Nouvelle valeur |
|
||||
| Rayon de déclenchement| 100m | 120m |
|
||||
| Fichier audio | pantheon.mp3 | pantheon-v2.mp3 |
|
||||
Et le créateur clique sur "Enregistrer les modifications"
|
||||
Alors la séquence est mise à jour
|
||||
Et le nouveau fichier audio remplace l'ancien
|
||||
Et l'ancien fichier est supprimé du stockage S3
|
||||
Et un événement "AUDIO_GUIDE_SEQUENCE_UPDATED" est enregistré
|
||||
Et la métrique "audio_guide.sequences.updated" est incrémentée
|
||||
|
||||
Scénario: Suppression d'une séquence
|
||||
Étant donné un créateur "grace@roadwave.fr" avec 5 séquences créées
|
||||
Quand le créateur clique sur l'icône de suppression de la séquence #3
|
||||
Alors un dialogue de confirmation s'affiche: "Supprimer cette séquence ?"
|
||||
Et le créateur confirme la suppression
|
||||
Alors la séquence #3 est supprimée
|
||||
Et le fichier audio associé est marqué pour suppression différée (30 jours)
|
||||
Et les séquences suivantes sont renumérotées automatiquement:
|
||||
| Ancien ordre | Nouveau ordre | Nom |
|
||||
| 4 | 3 | Jardin du Luxembourg |
|
||||
| 5 | 4 | Sorbonne |
|
||||
Et la ligne de parcours est recalculée
|
||||
Et un événement "AUDIO_GUIDE_SEQUENCE_DELETED" est enregistré
|
||||
Et la métrique "audio_guide.sequences.deleted" est incrémentée
|
||||
|
||||
Scénario: Validation de la distance minimale entre séquences
|
||||
Étant donné un créateur "henry@roadwave.fr" avec 2 séquences créées
|
||||
Quand le créateur tente d'ajouter une 3ème séquence à 5 mètres de la séquence #1
|
||||
Alors un message d'erreur s'affiche: "Ce point est trop proche d'un point existant (min: 20m)"
|
||||
Et le marqueur est affiché en rouge sur la carte
|
||||
Et la séquence n'est pas enregistrée tant que le créateur ne déplace pas le marqueur
|
||||
Et un événement "AUDIO_GUIDE_SEQUENCE_TOO_CLOSE" est enregistré
|
||||
Et la métrique "audio_guide.validation.sequence_too_close" est incrémentée
|
||||
|
||||
Scénario: Création d'un audio-guide - Étape 4: Configuration avancée
|
||||
Étant donné un créateur "iris@roadwave.fr" à l'étape 4 du wizard
|
||||
Quand le créateur configure les options avancées:
|
||||
| Option | Valeur |
|
||||
| Prix (gratuit ou payant) | Gratuit |
|
||||
| Visibilité | Publique |
|
||||
| Mode de lecture | Séquentiel obligatoire |
|
||||
| Autoriser les avis utilisateurs | Oui |
|
||||
| Autoriser le téléchargement | Non |
|
||||
| Activer les sous-titres | Oui |
|
||||
Et clique sur "Suivant"
|
||||
Alors les options sont enregistrées
|
||||
Et un événement "AUDIO_GUIDE_CONFIG_COMPLETED" est enregistré
|
||||
Et la métrique "audio_guide.creation.step4_completed" est incrémentée
|
||||
|
||||
Scénario: Création d'un audio-guide - Étape 5: Prévisualisation et publication
|
||||
Étant donné un créateur "jack@roadwave.fr" à l'étape 5 du wizard
|
||||
Quand le créateur voit la prévisualisation complète:
|
||||
| Section | Contenu |
|
||||
| Informations | Titre, description, durée |
|
||||
| Image | Aperçu de la couverture |
|
||||
| Parcours | Carte avec 5 séquences |
|
||||
| Audio | Liste des 5 fichiers audio |
|
||||
| Configuration | Prix, visibilité, options |
|
||||
Et clique sur "Tester le parcours en simulation"
|
||||
Alors une simulation GPS est lancée avec lecture des audios
|
||||
Et le créateur peut naviguer dans le parcours virtuel
|
||||
Et après validation, le créateur clique sur "Publier l'audio-guide"
|
||||
Alors l'audio-guide passe du statut "brouillon" à "publié"
|
||||
Et l'audio-guide devient visible dans les recherches et recommandations
|
||||
Et un événement "AUDIO_GUIDE_PUBLISHED" est enregistré
|
||||
Et la métrique "audio_guide.published" est incrémentée
|
||||
Et un email de confirmation est envoyé au créateur
|
||||
|
||||
Scénario: Sauvegarde automatique du brouillon pendant la création
|
||||
Étant donné un créateur "kate@roadwave.fr" en train de créer un audio-guide
|
||||
Quand le créateur remplit des informations à chaque étape
|
||||
Alors le brouillon est automatiquement sauvegardé toutes les 30 secondes
|
||||
Et un indicateur "Sauvegardé automatiquement à 14:32" s'affiche
|
||||
Et en cas de fermeture accidentelle, le créateur peut reprendre la création
|
||||
Et un événement "AUDIO_GUIDE_DRAFT_AUTOSAVED" est enregistré toutes les 30s
|
||||
Et la métrique "audio_guide.drafts.autosaved" est incrémentée
|
||||
|
||||
Scénario: Récupération d'un brouillon après interruption
|
||||
Étant donné un créateur "luke@roadwave.fr" qui a commencé un audio-guide hier
|
||||
Et le brouillon a été sauvegardé automatiquement à l'étape 3
|
||||
Quand le créateur clique sur "Créer un audio-guide"
|
||||
Alors un message s'affiche: "Vous avez un brouillon en cours. Reprendre la création ?"
|
||||
Et le créateur clique sur "Reprendre"
|
||||
Alors le wizard s'ouvre directement à l'étape 3
|
||||
Et toutes les données saisies sont restaurées
|
||||
Et un événement "AUDIO_GUIDE_DRAFT_RESUMED" est enregistré
|
||||
Et la métrique "audio_guide.drafts.resumed" est incrémentée
|
||||
|
||||
Scénario: Import d'un parcours GPX pour créer automatiquement les séquences
|
||||
Étant donné un créateur "mary@roadwave.fr" à l'étape 3 du wizard
|
||||
Quand le créateur clique sur "Importer un parcours GPX"
|
||||
Et upload un fichier "parcours-paris.gpx" avec 10 waypoints
|
||||
Alors le système extrait les coordonnées GPS de chaque waypoint
|
||||
Et crée automatiquement 10 séquences avec positions GPS pré-remplies
|
||||
Et les marqueurs sont affichés sur la carte
|
||||
Et le créateur doit ensuite ajouter les fichiers audio et noms pour chaque séquence
|
||||
Et un événement "AUDIO_GUIDE_GPX_IMPORTED" est enregistré
|
||||
Et la métrique "audio_guide.gpx.imported" est incrémentée
|
||||
|
||||
Scénario: Validation de la qualité audio avant publication
|
||||
Étant donné un créateur "nathan@roadwave.fr" qui tente de publier un audio-guide
|
||||
Quand le système analyse les fichiers audio uploadés
|
||||
Et détecte que le fichier "sequence-3.mp3" a un bitrate de 32 kbps (trop faible)
|
||||
Alors un avertissement s'affiche: "Le fichier 'sequence-3.mp3' a une qualité audio faible. Recommandé: 128 kbps minimum"
|
||||
Et le créateur peut choisir de:
|
||||
| Action | Conséquence |
|
||||
| Ignorer et publier quand même | Publication autorisée |
|
||||
| Remplacer le fichier | Retour à l'édition |
|
||||
Et un événement "AUDIO_GUIDE_LOW_QUALITY_WARNING" est enregistré
|
||||
Et la métrique "audio_guide.quality.warnings" est incrémentée
|
||||
|
||||
Scénario: Limitation du nombre de brouillons par créateur
|
||||
Étant donné un créateur "olive@roadwave.fr" avec 10 brouillons en cours
|
||||
Quand le créateur tente de créer un 11ème audio-guide
|
||||
Alors un message s'affiche: "Vous avez atteint la limite de 10 brouillons. Veuillez publier ou supprimer des brouillons existants."
|
||||
Et un lien vers la liste des brouillons est affiché
|
||||
Et la création est bloquée jusqu'à suppression d'un brouillon
|
||||
Et un événement "AUDIO_GUIDE_DRAFT_LIMIT_REACHED" est enregistré
|
||||
Et la métrique "audio_guide.drafts.limit_reached" est incrémentée
|
||||
|
||||
Scénario: Métriques de performance du wizard de création
|
||||
Étant donné que 1000 audio-guides sont créés par mois
|
||||
Quand les métriques de création sont collectées
|
||||
Alors les indicateurs suivants sont disponibles:
|
||||
| Métrique | Valeur cible |
|
||||
| Taux de complétion du wizard | > 65% |
|
||||
| Temps moyen de création | < 45 min |
|
||||
| Nombre moyen de séquences par guide | 5-8 |
|
||||
| Taux d'abandon à chaque étape | < 15% |
|
||||
| Taux d'utilisation de l'autosave | 100% |
|
||||
Et les métriques sont exportées vers le système de monitoring
|
||||
Et des optimisations UX sont proposées si taux d'abandon > 20%
|
||||
@@ -0,0 +1,223 @@
|
||||
# language: fr
|
||||
|
||||
@api @audio-guides @car-mode @geolocation @mvp
|
||||
Fonctionnalité: Déclenchement GPS automatique des audio-guides en mode voiture
|
||||
|
||||
En tant qu'utilisateur en voiture
|
||||
Je veux que les audio-guides se déclenchent automatiquement à l'approche des points d'intérêt
|
||||
Afin de profiter d'une expérience guidée sans interaction manuelle pendant la conduite
|
||||
|
||||
Contexte:
|
||||
Étant donné que le système de déclenchement en mode voiture respecte:
|
||||
| Paramètre | Valeur |
|
||||
| Rayon de déclenchement | 200-500m |
|
||||
| Vitesse max pour déclenchement | 90 km/h |
|
||||
| Ordre de séquences | Strict |
|
||||
| Notification visuelle | Minimale |
|
||||
| Notification audio | Prioritaire |
|
||||
| Auto-play | Obligatoire |
|
||||
|
||||
Scénario: Démarrage d'un audio-guide en mode voiture
|
||||
Étant donné un utilisateur "alice@roadwave.fr" en mode voiture
|
||||
Et elle roule à 50 km/h sur l'autoroute A6
|
||||
Quand elle lance l'audio-guide "Route des Châteaux de la Loire"
|
||||
Alors l'audio de la séquence d'introduction démarre automatiquement
|
||||
Et l'interface minimaliste en mode voiture s'affiche:
|
||||
| Élément | État |
|
||||
| Carte | Simplifiée, zoom automatique |
|
||||
| Notifications visuelles | Minimales |
|
||||
| Prochain point | Château de Chambord - 25 km |
|
||||
| ETA | Arrivée dans 18 minutes |
|
||||
| Contrôles audio | Gros boutons [Pause] [Skip] |
|
||||
Et un événement "AUDIO_GUIDE_STARTED_CAR_MODE" est enregistré
|
||||
Et la métrique "audio_guide.started.car_mode" est incrémentée
|
||||
|
||||
Scénario: Déclenchement automatique à l'approche d'un point d'intérêt
|
||||
Étant donné un utilisateur "bob@roadwave.fr" en mode voiture à 60 km/h
|
||||
Et il écoute l'audio-guide "Route des Châteaux de la Loire"
|
||||
Et il approche du Château de Chambord
|
||||
Quand il entre dans un rayon de 400m du château (configuré par le créateur)
|
||||
Alors l'audio en cours se termine en fondu (3 secondes)
|
||||
Et l'audio de la séquence "Château de Chambord" démarre automatiquement
|
||||
Et une notification audio est jouée: "À votre droite, Château de Chambord"
|
||||
Et une notification visuelle minimale s'affiche brièvement (2s):
|
||||
| Élément | Contenu |
|
||||
| Titre | Château de Chambord |
|
||||
| Direction | → À droite |
|
||||
| Distance | 400m |
|
||||
Et un événement "SEQUENCE_AUTO_TRIGGERED_CAR" est enregistré
|
||||
Et la métrique "audio_guide.sequence.car.triggered" est incrémentée
|
||||
|
||||
Scénario: Calcul de l'ETA dynamique basé sur la vitesse réelle
|
||||
Étant donné un utilisateur "charlie@roadwave.fr" en mode voiture
|
||||
Et il approche du prochain point d'intérêt à 15 km
|
||||
Et il roule à 80 km/h
|
||||
Quand le système calcule l'ETA
|
||||
Alors l'ETA affiché est: "Arrivée dans 11 minutes"
|
||||
Quand il ralentit à 50 km/h (bouchon)
|
||||
Alors l'ETA est recalculé en temps réel: "Arrivée dans 18 minutes"
|
||||
Et un événement "ETA_RECALCULATED" est enregistré
|
||||
Et la métrique "audio_guide.eta.updated" est incrémentée
|
||||
|
||||
Scénario: Notification vocale d'approche 2km avant le point
|
||||
Étant donné un utilisateur "david@roadwave.fr" en mode voiture à 70 km/h
|
||||
Et il écoute l'audio-guide "Route des Châteaux de la Loire"
|
||||
Quand il est à 2 km du Château de Chenonceau
|
||||
Alors une notification vocale est jouée par-dessus l'audio actuel:
|
||||
"Dans 2 kilomètres, vous découvrirez le Château de Chenonceau"
|
||||
Et le volume de l'audio actuel est réduit de 50% pendant la notification (ducking audio)
|
||||
Et après la notification, le volume reprend normalement
|
||||
Et un événement "POI_ADVANCE_NOTIFICATION" est enregistré avec distance: 2000m
|
||||
Et la métrique "audio_guide.advance_notification" est incrémentée
|
||||
|
||||
Scénario: Gestion du dépassement d'un point d'intérêt sans déclenchement
|
||||
Étant donné un utilisateur "eve@roadwave.fr" en mode voiture à 90 km/h
|
||||
Et elle approche du Château d'Amboise (séquence #3)
|
||||
Mais elle a manqué la séquence #2 (Château de Chaumont)
|
||||
Quand elle entre dans le rayon du Château d'Amboise
|
||||
Alors l'audio de la séquence #2 est automatiquement joué d'abord
|
||||
Et un message vocal indique: "Séquence précédente: Château de Chaumont"
|
||||
Et après la fin de la séquence #2, la séquence #3 démarre
|
||||
Et un événement "SEQUENCE_CATCH_UP" est enregistré
|
||||
Et la métrique "audio_guide.sequence.catch_up" est incrémentée
|
||||
|
||||
Scénario: Marquage automatique d'une séquence comme "manquée"
|
||||
Étant donné un utilisateur "frank@roadwave.fr" en mode voiture à 100 km/h
|
||||
Et il a dépassé le Château de Chaumont sans entrer dans son rayon
|
||||
Et il s'éloigne maintenant à plus de 5 km
|
||||
Alors la séquence "Château de Chaumont" est marquée comme "Manquée"
|
||||
Et elle reste disponible dans la liste pour écoute manuelle ultérieure
|
||||
Et un événement "SEQUENCE_MISSED" est enregistré avec raison: "too_fast"
|
||||
Et la métrique "audio_guide.sequence.missed" est incrémentée
|
||||
|
||||
Scénario: Pause automatique lors d'un appel téléphonique
|
||||
Étant donné un utilisateur "grace@roadwave.fr" en mode voiture
|
||||
Et elle écoute l'audio-guide à la position 3min 20s
|
||||
Quand elle reçoit un appel téléphonique via CarPlay
|
||||
Alors l'audio-guide se met automatiquement en pause
|
||||
Et la position de lecture est sauvegardée: 3min 20s
|
||||
Et un événement "AUDIO_PAUSED_PHONE_CALL" est enregistré
|
||||
Quand l'appel se termine
|
||||
Alors l'audio-guide reprend automatiquement à 3min 20s
|
||||
Et un événement "AUDIO_RESUMED_AFTER_CALL" est enregistré
|
||||
Et la métrique "audio_guide.interruption.phone" est incrémentée
|
||||
|
||||
Scénario: Intégration avec CarPlay pour affichage sur écran véhicule
|
||||
Étant donné un utilisateur "henry@roadwave.fr" en mode voiture
|
||||
Et son iPhone est connecté via CarPlay
|
||||
Quand il lance l'audio-guide "Route des Châteaux de la Loire"
|
||||
Alors l'interface CarPlay s'affiche sur l'écran du véhicule:
|
||||
| Élément | Affichage |
|
||||
| Carte simplifiée | Vue routière optimisée |
|
||||
| Prochain point | Nom + distance + ETA |
|
||||
| Contrôles audio | Gros boutons tactiles |
|
||||
| Progression | Barre 3/10 séquences |
|
||||
| Commandes vocales | "Dis Siri, suivant" |
|
||||
Et les contrôles au volant du véhicule fonctionnent (lecture/pause)
|
||||
Et un événement "CARPLAY_SESSION_STARTED" est enregistré
|
||||
Et la métrique "audio_guide.carplay.used" est incrémentée
|
||||
|
||||
Scénario: Commandes vocales Siri pour contrôle sans les mains
|
||||
Étant donné un utilisateur "iris@roadwave.fr" en mode voiture
|
||||
Et elle écoute l'audio-guide via CarPlay
|
||||
Quand elle dit "Dis Siri, mets en pause"
|
||||
Alors l'audio-guide se met en pause
|
||||
Quand elle dit "Dis Siri, reprends la lecture"
|
||||
Alors l'audio-guide reprend
|
||||
Quand elle dit "Dis Siri, séquence suivante"
|
||||
Alors la séquence suivante démarre
|
||||
Et un événement "VOICE_COMMAND_EXECUTED" est enregistré avec commande: "next"
|
||||
Et la métrique "audio_guide.voice_commands.used" est incrémentée
|
||||
|
||||
Scénario: Adaptation du volume en fonction de la vitesse du véhicule
|
||||
Étant donné un utilisateur "jack@roadwave.fr" en mode voiture
|
||||
Et il écoute l'audio-guide avec volume configuré à 70%
|
||||
Quand il roule à 50 km/h
|
||||
Alors le volume reste à 70% (bruit ambiant faible)
|
||||
Quand il accélère à 130 km/h sur autoroute
|
||||
Alors le volume augmente automatiquement à 85% (compensation du bruit)
|
||||
Et un événement "VOLUME_AUTO_ADJUSTED" est enregistré avec vitesse: 130, volume: 85
|
||||
Et la métrique "audio_guide.volume.auto_adjusted" est incrémentée
|
||||
|
||||
Scénario: Désactivation temporaire en cas de vitesse excessive
|
||||
Étant donné un utilisateur "kate@roadwave.fr" en mode voiture
|
||||
Et elle écoute l'audio-guide sur autoroute
|
||||
Quand elle dépasse les 110 km/h
|
||||
Alors l'audio continue de jouer normalement
|
||||
Mais aucune nouvelle séquence ne se déclenche automatiquement
|
||||
Et un message vocal indique: "Déclenchements automatiques suspendus à haute vitesse"
|
||||
Quand elle ralentit en dessous de 90 km/h
|
||||
Alors les déclenchements automatiques sont réactivés
|
||||
Et un événement "AUTO_TRIGGER_SPEED_LIMITED" est enregistré
|
||||
Et la métrique "audio_guide.speed.limited" est incrémentée
|
||||
|
||||
Scénario: Mode nuit avec interface sombre automatique
|
||||
Étant donné un utilisateur "luke@roadwave.fr" en mode voiture
|
||||
Et il est 22h30 (nuit)
|
||||
Quand il utilise l'audio-guide
|
||||
Alors l'interface passe automatiquement en mode nuit:
|
||||
| Élément | Mode nuit |
|
||||
| Fond d'écran | Noir |
|
||||
| Texte | Blanc/Gris clair |
|
||||
| Carte | Thème sombre |
|
||||
| Luminosité | Réduite de 40% |
|
||||
Et les notifications visuelles sont encore plus discrètes
|
||||
Et un événement "NIGHT_MODE_AUTO_ENABLED" est enregistré
|
||||
Et la métrique "audio_guide.night_mode.enabled" est incrémentée
|
||||
|
||||
Scénario: Connexion automatique via Android Auto
|
||||
Étant donné un utilisateur "mary@roadwave.fr" avec téléphone Android
|
||||
Et son téléphone est connecté via Android Auto
|
||||
Quand elle lance l'audio-guide "Route des Châteaux de la Loire"
|
||||
Alors l'interface Android Auto s'affiche sur l'écran du véhicule
|
||||
Et les fonctionnalités sont identiques à CarPlay:
|
||||
| Fonctionnalité | Disponible |
|
||||
| Carte simplifiée | Oui |
|
||||
| Contrôles audio | Oui |
|
||||
| Commandes vocales | Oui (Google Assistant) |
|
||||
| Notifications | Oui |
|
||||
Et un événement "ANDROID_AUTO_SESSION_STARTED" est enregistré
|
||||
Et la métrique "audio_guide.android_auto.used" est incrémentée
|
||||
|
||||
Scénario: Gestion de la perte de signal GPS temporaire
|
||||
Étant donné un utilisateur "nathan@roadwave.fr" en mode voiture
|
||||
Et il écoute l'audio-guide dans un tunnel
|
||||
Quand le signal GPS est perdu pendant 2 minutes
|
||||
Alors l'audio en cours continue de jouer normalement
|
||||
Et la position estimée est calculée selon la vitesse et direction précédentes
|
||||
Et un message discret s'affiche: "Signal GPS perdu - Position estimée"
|
||||
Quand le signal GPS est retrouvé
|
||||
Alors la position est recalculée immédiatement
|
||||
Et les déclenchements automatiques sont réactivés
|
||||
Et un événement "GPS_SIGNAL_RESTORED" est enregistré
|
||||
Et la métrique "audio_guide.gps.signal_lost" est incrémentée
|
||||
|
||||
Scénario: Statistiques de fin de parcours en mode voiture
|
||||
Étant donné un utilisateur "olive@roadwave.fr" en mode voiture
|
||||
Et elle vient de terminer l'audio-guide "Route des Châteaux de la Loire"
|
||||
Quand elle arrive à destination
|
||||
Alors un écran de statistiques s'affiche:
|
||||
| Métrique | Valeur |
|
||||
| Séquences écoutées | 9/10 |
|
||||
| Séquences manquées | 1 (trop rapide) |
|
||||
| Distance parcourue | 142 km |
|
||||
| Temps total | 2h 15min |
|
||||
| Temps d'écoute | 1h 05min |
|
||||
| Vitesse moyenne | 63 km/h |
|
||||
| Badge débloqué | Voyageur des châteaux |
|
||||
Et un bouton "Partager mon voyage" est disponible
|
||||
Et un événement "AUDIO_GUIDE_COMPLETED_CAR" est enregistré
|
||||
Et la métrique "audio_guide.completed.car_mode" est incrémentée
|
||||
|
||||
Scénario: Métriques de performance du mode voiture
|
||||
Étant donné que 50 000 utilisateurs ont utilisé l'audio-guide en mode voiture
|
||||
Quand les métriques d'usage sont collectées
|
||||
Alors les indicateurs suivants sont disponibles:
|
||||
| Métrique | Valeur cible |
|
||||
| Taux de déclenchement automatique | > 90% |
|
||||
| Taux de séquences manquées | < 15% |
|
||||
| Temps moyen entre déclenchements | 8 minutes |
|
||||
| Précision du calcul ETA | ±3 minutes |
|
||||
| Utilisation de CarPlay/Android Auto | 65% |
|
||||
| Utilisation de commandes vocales | 45% |
|
||||
Et les métriques sont exportées vers le système de monitoring
|
||||
@@ -0,0 +1,339 @@
|
||||
# language: fr
|
||||
|
||||
Fonctionnalité: API - Déclenchement GPS et géolocalisation audio-guides
|
||||
En tant que système backend
|
||||
Je veux calculer les déclenchements GPS et distances pour audio-guides
|
||||
Afin de permettre une expérience automatique en mode voiture/vélo/transport
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est démarrée
|
||||
Et que l'utilisateur "user@example.com" est authentifié
|
||||
|
||||
# 16.3.1 - Calcul de proximité et déclenchement
|
||||
|
||||
Scénario: POST /api/v1/audio-guides/{id}/check-proximity - Vérification proximité
|
||||
Étant donné un audio-guide voiture avec 8 séquences
|
||||
Et que l'utilisateur est à la position (43.1233, 2.5677)
|
||||
Et que le prochain point GPS (séquence 2) est à (43.1245, 2.5690) avec rayon 30m
|
||||
Quand je fais un POST sur "/api/v1/audio-guides/ag_123/check-proximity":
|
||||
"""json
|
||||
{
|
||||
"user_position": {
|
||||
"latitude": 43.1233,
|
||||
"longitude": 2.5677
|
||||
},
|
||||
"current_sequence": 1
|
||||
}
|
||||
"""
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"in_trigger_zone": false,
|
||||
"next_sequence_id": "seq_2",
|
||||
"distance_to_next": 145.3,
|
||||
"eta_seconds": 18,
|
||||
"direction_degrees": 45,
|
||||
"should_trigger": false
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: Déclenchement automatique dans rayon 30m (voiture)
|
||||
Étant donné un audio-guide voiture
|
||||
Et que l'utilisateur entre à 25m du point GPS suivant
|
||||
Quand je fais un POST sur "/api/v1/audio-guides/ag_123/check-proximity"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et should_trigger est true
|
||||
Et in_trigger_zone est true
|
||||
Et le message "Séquence déclenchée automatiquement" est retourné
|
||||
|
||||
Plan du Scénario: Rayon de déclenchement selon mode
|
||||
Étant donné un audio-guide en mode <mode>
|
||||
Et un point GPS avec rayon par défaut
|
||||
Quand l'utilisateur entre à <distance> du point
|
||||
Alors should_trigger est <trigger>
|
||||
|
||||
Exemples:
|
||||
| mode | distance | trigger |
|
||||
| voiture | 25m | true |
|
||||
| voiture | 35m | false |
|
||||
| velo | 45m | true |
|
||||
| velo | 55m | false |
|
||||
| transport | 95m | true |
|
||||
| transport | 105m | false |
|
||||
|
||||
# Calcul distance avec PostGIS
|
||||
|
||||
Scénario: Calcul distance avec ST_Distance (geography)
|
||||
Étant donné deux points GPS:
|
||||
| point | latitude | longitude |
|
||||
| Position user| 43.1234 | 2.5678 |
|
||||
| Point séq. 2 | 43.1245 | 2.5690 |
|
||||
Quand le calcul PostGIS ST_Distance est effectué
|
||||
Alors la distance retournée est 145.3 mètres
|
||||
Et le calcul utilise le type geography (WGS84)
|
||||
Et la précision est au mètre près
|
||||
|
||||
Scénario: Calcul ETA basé sur vitesse actuelle
|
||||
Étant donné que l'utilisateur est à 320m du prochain point
|
||||
Et que sa vitesse actuelle est 28 km/h
|
||||
Quand l'ETA est calculé
|
||||
Alors l'ETA retourné est 41 secondes
|
||||
Et la formule appliquée est: (distance_m / 1000) / (vitesse_kmh) * 3600
|
||||
|
||||
Scénario: ETA non calculé si vitesse < 5 km/h
|
||||
Étant donné que l'utilisateur est à 200m du prochain point
|
||||
Et que sa vitesse actuelle est 2 km/h (arrêté)
|
||||
Quand l'ETA est calculé
|
||||
Alors l'ETA retourné est null
|
||||
Et le message "En attente de déplacement" est inclus
|
||||
|
||||
Scénario: Calcul direction (bearing) avec PostGIS
|
||||
Étant donné la position utilisateur (43.1234, 2.5678)
|
||||
Et le prochain point (43.1245, 2.5690) au nord-est
|
||||
Quand le bearing est calculé avec ST_Azimuth
|
||||
Alors l'angle retourné est 45° (nord-est)
|
||||
Et la flèche correspondante est "↗"
|
||||
|
||||
Plan du Scénario: Conversion angle en flèche (8 directions)
|
||||
Étant donné un angle de <degrees>°
|
||||
Quand la flèche est calculée
|
||||
Alors la direction retournée est "<arrow>"
|
||||
|
||||
Exemples:
|
||||
| degrees | arrow |
|
||||
| 0 | ↑ |
|
||||
| 45 | ↗ |
|
||||
| 90 | → |
|
||||
| 135 | ↘ |
|
||||
| 180 | ↓ |
|
||||
| 225 | ↙ |
|
||||
| 270 | ← |
|
||||
| 315 | ↖ |
|
||||
|
||||
# 16.3.3 - Gestion point manqué
|
||||
|
||||
Scénario: Détection point manqué (hors rayon mais dans tolérance)
|
||||
Étant donné un audio-guide voiture
|
||||
Et un point GPS avec rayon 30m et tolérance 100m
|
||||
Et que l'utilisateur passe à 65m du point
|
||||
Quand je fais un POST sur "/api/v1/audio-guides/ag_123/check-proximity"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"in_trigger_zone": false,
|
||||
"missed_point": true,
|
||||
"distance_to_point": 65,
|
||||
"tolerance_zone": true,
|
||||
"actions_available": ["listen_anyway", "skip", "navigate_back"]
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: Point manqué au-delà tolérance (>100m en voiture)
|
||||
Étant donné un audio-guide voiture
|
||||
Et que l'utilisateur passe à 150m du point GPS
|
||||
Quand je fais un POST sur "/api/v1/audio-guides/ag_123/check-proximity"
|
||||
Alors missed_point est false
|
||||
Et tolerance_zone est false
|
||||
Et aucune popup "point manqué" n'est déclenchée
|
||||
|
||||
Plan du Scénario: Rayon tolérance selon mode
|
||||
Étant donné un audio-guide en mode <mode>
|
||||
Et que l'utilisateur passe à <distance> du point
|
||||
Alors tolerance_zone est <in_tolerance>
|
||||
|
||||
Exemples:
|
||||
| mode | distance | in_tolerance |
|
||||
| voiture | 60m | true |
|
||||
| voiture | 110m | false |
|
||||
| velo | 70m | true |
|
||||
| velo | 80m | false |
|
||||
| transport | 120m | true |
|
||||
| transport | 160m | false |
|
||||
|
||||
# Progress bar dynamique
|
||||
|
||||
Scénario: Calcul progress bar vers prochain point
|
||||
Étant donné que la distance initiale vers le prochain point était 500m
|
||||
Et que l'utilisateur est maintenant à 175m du point
|
||||
Quand le pourcentage de progression est calculé
|
||||
Alors le progress_percentage retourné est 65%
|
||||
Et la formule est: 100 - (distance_actuelle / distance_initiale * 100)
|
||||
|
||||
# Gestion trajectoire et itinéraire
|
||||
|
||||
Scénario: Calcul distance totale parcours
|
||||
Étant donné un audio-guide avec les points GPS suivants:
|
||||
| sequence | latitude | longitude |
|
||||
| 1 | 43.1234 | 2.5678 |
|
||||
| 2 | 43.1245 | 2.5690 |
|
||||
| 3 | 43.1250 | 2.5700 |
|
||||
Quand je fais un GET sur "/api/v1/audio-guides/ag_123/route-stats"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"total_distance": 350,
|
||||
"distances_between_points": [
|
||||
{"from": 1, "to": 2, "distance": 150},
|
||||
{"from": 2, "to": 3, "distance": 200}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: Vérification cohérence itinéraire (alerte distance excessive)
|
||||
Étant donné un audio-guide en mode "pieton"
|
||||
Et deux points GPS distants de 465 km (Paris - Lyon)
|
||||
Quand la cohérence est vérifiée
|
||||
Alors un warning est retourné:
|
||||
"""json
|
||||
{
|
||||
"warning": "distance_excessive",
|
||||
"message": "Distance de 465 km entre séquences 2 et 3. Mode 'pieton' inapproprié.",
|
||||
"suggested_mode": "voiture"
|
||||
}
|
||||
"""
|
||||
|
||||
# Distinction audio-guides vs contenus géolocalisés simples
|
||||
|
||||
Scénario: Pas de notification 7s avant pour audio-guides multi-séquences
|
||||
Étant donné un audio-guide multi-séquences en mode voiture
|
||||
Et que l'utilisateur approche du prochain point GPS
|
||||
Quand la distance et ETA sont calculés
|
||||
Alors aucun décompte "7→1" n'est déclenché
|
||||
Et le déclenchement se fait au point GPS exact (rayon 30m)
|
||||
Et une notification "Ding" + toast 2s est envoyée
|
||||
|
||||
Scénario: Notification 7s avant pour contenus géolocalisés simples (1 séquence)
|
||||
Étant donné un contenu géolocalisé simple (1 séquence unique)
|
||||
Et que l'utilisateur approche du point GPS
|
||||
Quand l'ETA devient 7 secondes
|
||||
Alors une notification avec compteur "7→1" est déclenchée
|
||||
Et l'utilisateur doit valider avec bouton "Suivant"
|
||||
|
||||
# Exception quota pour audio-guides multi-séquences
|
||||
|
||||
Scénario: Audio-guide multi-séquences compte 1 seul contenu dans quota horaire
|
||||
Étant donné un audio-guide "Visite Safari" avec 12 séquences
|
||||
Et que l'utilisateur a un quota de 0/6 contenus géolocalisés
|
||||
Quand l'utilisateur démarre l'audio-guide (séquence 1)
|
||||
Alors le quota passe à 1/6
|
||||
Quand l'utilisateur écoute les 12 séquences complètes
|
||||
Alors le quota reste à 1/6
|
||||
Et toutes les séquences ne consomment PAS 12 quotas
|
||||
Et l'audio-guide entier compte comme 1 seul contenu
|
||||
|
||||
Scénario: Contenus géolocalisés simples consomment 1 quota chacun
|
||||
Étant donné que l'utilisateur a un quota de 0/6
|
||||
Quand l'utilisateur accepte un contenu géolocalisé simple "Tour Eiffel"
|
||||
Alors le quota passe à 1/6
|
||||
Quand l'utilisateur accepte un contenu géolocalisé simple "Arc de Triomphe"
|
||||
Alors le quota passe à 2/6
|
||||
Quand l'utilisateur accepte un contenu géolocalisé simple "Louvre"
|
||||
Alors le quota passe à 3/6
|
||||
Et chaque contenu simple consomme 1 quota
|
||||
|
||||
Scénario: Mixte audio-guides + contenus simples respecte quota 6/h
|
||||
Étant donné que l'utilisateur a un quota de 0/6
|
||||
Quand l'utilisateur démarre un audio-guide 8 séquences "Safari"
|
||||
Alors le quota passe à 1/6
|
||||
Quand l'utilisateur accepte 5 contenus géolocalisés simples
|
||||
Alors le quota passe à 6/6
|
||||
Et le quota horaire est atteint
|
||||
Quand un 7ème contenu est détecté
|
||||
Alors aucune notification n'est envoyée (quota atteint)
|
||||
|
||||
# Cache et optimisations
|
||||
|
||||
Scénario: Cache Redis pour calculs GPS fréquents
|
||||
Étant donné que les points GPS d'un audio-guide sont en cache Redis
|
||||
Quand je fais un POST sur "/api/v1/audio-guides/ag_123/check-proximity"
|
||||
Alors les points GPS sont récupérés depuis Redis (pas PostgreSQL)
|
||||
Et le temps de réponse est < 50ms
|
||||
|
||||
Scénario: Geospatial GEORADIUS Redis pour recherche proximité
|
||||
Étant donné que tous les audio-guides sont indexés dans Redis (GEOADD)
|
||||
Et une position utilisateur (43.1234, 2.5678)
|
||||
Quand je recherche les audio-guides dans un rayon de 5 km
|
||||
Alors Redis GEORADIUS retourne les audio-guides proches
|
||||
Et le temps de réponse est < 20ms
|
||||
|
||||
# Mise à jour position temps réel
|
||||
|
||||
Scénario: WebSocket pour mise à jour position en temps réel
|
||||
Étant donné une connexion WebSocket active pour l'audio-guide
|
||||
Quand l'utilisateur envoie sa nouvelle position via WS
|
||||
Alors le serveur calcule immédiatement la proximité
|
||||
Et retourne distance + ETA via WS (pas de polling HTTP)
|
||||
|
||||
Scénario: Throttling position updates (max 1/seconde)
|
||||
Étant donné que le client envoie des positions GPS toutes les 200ms
|
||||
Quand le serveur reçoit les mises à jour
|
||||
Alors seules les positions espacées de >1 seconde sont traitées
|
||||
Et les autres sont ignorées (throttling)
|
||||
|
||||
# Cas d'erreur
|
||||
|
||||
Scénario: Position GPS invalide (coordonnées hors limites)
|
||||
Étant donné une position avec latitude 95.0000 (invalide)
|
||||
Quand je fais un POST sur "/api/v1/audio-guides/{id}/check-proximity"
|
||||
Alors le code HTTP de réponse est 400
|
||||
Et le message d'erreur est "latitude: doit être entre -90 et 90"
|
||||
|
||||
Scénario: Audio-guide sans points GPS (mode piéton)
|
||||
Étant donné un audio-guide en mode piéton sans points GPS
|
||||
Quand je fais un POST sur "/api/v1/audio-guides/{id}/check-proximity"
|
||||
Alors le code HTTP de réponse est 400
|
||||
Et le message d'erreur est "Audio-guide en mode manuel, pas de déclenchement GPS"
|
||||
|
||||
Scénario: Séquence déjà complétée (skip calcul si utilisateur a déjà passé)
|
||||
Étant donné que l'utilisateur est à la séquence 5
|
||||
Et qu'il vérifie la proximité du point 3 (déjà écouté)
|
||||
Quand je fais un POST sur "/api/v1/audio-guides/{id}/check-proximity"
|
||||
Alors le calcul n'est pas effectué pour les séquences passées
|
||||
Et le message "Séquence déjà écoutée" est retourné
|
||||
|
||||
Scénario: Précision GPS insuffisante
|
||||
Étant donné une position avec accuracy ±150m
|
||||
Et un rayon de déclenchement de 30m
|
||||
Quand la précision est vérifiée
|
||||
Alors un warning est retourné:
|
||||
"""json
|
||||
{
|
||||
"warning": "low_gps_accuracy",
|
||||
"message": "Précision GPS insuffisante (±150m). Déclenchement automatique peut être perturbé.",
|
||||
"accuracy": 150,
|
||||
"trigger_radius": 30
|
||||
}
|
||||
"""
|
||||
|
||||
# Performance
|
||||
|
||||
Scénario: Optimisation requêtes PostGIS avec index spatial
|
||||
Étant donné que les points GPS ont un index GIST (PostGIS)
|
||||
Quand une requête ST_DWithin est exécutée
|
||||
Alors l'index spatial est utilisé
|
||||
Et le temps d'exécution est < 10ms
|
||||
|
||||
Scénario: Batch proximity check pour tous les points
|
||||
Étant donné un audio-guide avec 20 séquences
|
||||
Quand je fais un POST sur "/api/v1/audio-guides/{id}/batch-proximity":
|
||||
"""json
|
||||
{
|
||||
"user_position": {"latitude": 43.1234, "longitude": 2.5678}
|
||||
}
|
||||
"""
|
||||
Alors toutes les distances sont calculées en une seule requête PostGIS
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"sequences": [
|
||||
{"sequence_id": "seq_1", "distance": 0, "in_zone": true},
|
||||
{"sequence_id": "seq_2", "distance": 150, "in_zone": false},
|
||||
{"sequence_id": "seq_3", "distance": 350, "in_zone": false}
|
||||
],
|
||||
"current_sequence": 1,
|
||||
"next_sequence": 2
|
||||
}
|
||||
"""
|
||||
@@ -0,0 +1,239 @@
|
||||
# language: fr
|
||||
|
||||
@api @audio-guides @geolocation @mvp
|
||||
Fonctionnalité: Détection automatique du mode de déplacement
|
||||
|
||||
En tant qu'utilisateur
|
||||
Je veux que l'application détecte automatiquement mon mode de déplacement
|
||||
Afin d'adapter l'expérience audio-guide (voiture, piéton, vélo, transports)
|
||||
|
||||
Contexte:
|
||||
Étant donné que le système utilise les capteurs suivants pour la détection:
|
||||
| Capteur | Utilisation |
|
||||
| GPS (vitesse) | Vitesse de déplacement |
|
||||
| Accéléromètre | Détection de la marche |
|
||||
| Gyroscope | Détection de mouvements |
|
||||
| Bluetooth | Connexion CarPlay/Android Auto |
|
||||
| Activité (CoreMotion) | walking, running, cycling, automotive |
|
||||
|
||||
Scénario: Détection automatique du mode voiture
|
||||
Étant donné un utilisateur "alice@roadwave.fr" en déplacement
|
||||
Quand le système détecte les indicateurs suivants:
|
||||
| Indicateur | Valeur |
|
||||
| Vitesse GPS | 45 km/h |
|
||||
| Accélération longitudinale | Typique d'une voiture|
|
||||
| Bluetooth connecté | CarPlay |
|
||||
| Activity Recognition | automotive |
|
||||
| Stabilité du mouvement | Haute |
|
||||
Alors le mode de déplacement "voiture" est sélectionné avec confiance: 95%
|
||||
Et l'interface passe en mode voiture:
|
||||
| Caractéristique | État |
|
||||
| Notifications visuelles | Minimales |
|
||||
| Notifications audio | Prioritaires |
|
||||
| Affichage des distances | Mètres + temps ETA |
|
||||
| Auto-play au point d'intérêt | Activé |
|
||||
Et un événement "TRAVEL_MODE_DETECTED_CAR" est enregistré
|
||||
Et la métrique "travel_mode.detected.car" est incrémentée
|
||||
|
||||
Scénario: Détection automatique du mode piéton
|
||||
Étant donné un utilisateur "bob@roadwave.fr" en déplacement
|
||||
Quand le système détecte les indicateurs suivants:
|
||||
| Indicateur | Valeur |
|
||||
| Vitesse GPS | 4 km/h |
|
||||
| Accéléromètre | Pattern de marche |
|
||||
| Fréquence de pas | 110 pas/min |
|
||||
| Activity Recognition | walking |
|
||||
| Bluetooth connecté | Non |
|
||||
Alors le mode de déplacement "piéton" est sélectionné avec confiance: 92%
|
||||
Et l'interface passe en mode piéton:
|
||||
| Caractéristique | État |
|
||||
| Notifications visuelles | Complètes |
|
||||
| Navigation libre | Activée |
|
||||
| Affichage carte | Complet |
|
||||
| Auto-play publicité | Autorisé |
|
||||
Et un événement "TRAVEL_MODE_DETECTED_WALKING" est enregistré
|
||||
Et la métrique "travel_mode.detected.walking" est incrémentée
|
||||
|
||||
Scénario: Détection automatique du mode vélo
|
||||
Étant donné un utilisateur "charlie@roadwave.fr" en déplacement
|
||||
Quand le système détecte les indicateurs suivants:
|
||||
| Indicateur | Valeur |
|
||||
| Vitesse GPS | 18 km/h |
|
||||
| Accéléromètre | Vibrations régulières|
|
||||
| Pattern de mouvement | Cyclique |
|
||||
| Activity Recognition | cycling |
|
||||
| Variations de vitesse | Moyennes |
|
||||
Alors le mode de déplacement "vélo" est sélectionné avec confiance: 88%
|
||||
Et l'interface passe en mode vélo:
|
||||
| Caractéristique | État |
|
||||
| Notifications visuelles | Limitées |
|
||||
| Notifications audio | Prioritaires |
|
||||
| Affichage des distances | Mètres |
|
||||
| Auto-play au point d'intérêt | Optionnel |
|
||||
Et un événement "TRAVEL_MODE_DETECTED_CYCLING" est enregistré
|
||||
Et la métrique "travel_mode.detected.cycling" est incrémentée
|
||||
|
||||
Scénario: Détection automatique du mode transports en commun
|
||||
Étant donné un utilisateur "david@roadwave.fr" en déplacement
|
||||
Quand le système détecte les indicateurs suivants:
|
||||
| Indicateur | Valeur |
|
||||
| Vitesse GPS | 35 km/h avec arrêts |
|
||||
| Pattern d'arrêts | Régulier (stations) |
|
||||
| Accéléromètre | Stationnaire par moments|
|
||||
| Précision GPS | Variable (tunnels) |
|
||||
| Activity Recognition | automotive + stationary |
|
||||
Alors le mode de déplacement "transports" est sélectionné avec confiance: 80%
|
||||
Et l'interface passe en mode transports:
|
||||
| Caractéristique | État |
|
||||
| Notifications visuelles | Complètes |
|
||||
| Auto-play aux stations | Activé |
|
||||
| Affichage carte | Complet |
|
||||
| Prise en compte des tunnels | Activée |
|
||||
Et un événement "TRAVEL_MODE_DETECTED_TRANSIT" est enregistré
|
||||
Et la métrique "travel_mode.detected.transit" est incrémentée
|
||||
|
||||
Scénario: Changement dynamique de mode détecté (voiture → piéton)
|
||||
Étant donné un utilisateur "eve@roadwave.fr" en mode voiture
|
||||
Et il roule à 50 km/h
|
||||
Quand l'utilisateur se gare et sort de la voiture:
|
||||
| Temps | Vitesse | Activity | Bluetooth |
|
||||
| T+0s | 50 km/h | automotive | CarPlay |
|
||||
| T+30s | 0 km/h | stationary | CarPlay |
|
||||
| T+60s | 0 km/h | stationary | Déconnecté|
|
||||
| T+90s | 4 km/h | walking | Non |
|
||||
Alors le mode bascule automatiquement de "voiture" à "piéton"
|
||||
Et une notification discrète s'affiche: "Mode piéton activé"
|
||||
Et l'interface s'adapte instantanément au mode piéton
|
||||
Et un événement "TRAVEL_MODE_CHANGED" est enregistré avec transition: "car_to_walking"
|
||||
Et la métrique "travel_mode.transition.car_to_walking" est incrémentée
|
||||
|
||||
Scénario: Changement dynamique de mode détecté (piéton → vélo)
|
||||
Étant donné un utilisateur "frank@roadwave.fr" en mode piéton
|
||||
Et il marche à 4 km/h
|
||||
Quand l'utilisateur monte sur un vélo:
|
||||
| Temps | Vitesse | Activity | Pattern |
|
||||
| T+0s | 4 km/h | walking | Marche |
|
||||
| T+10s | 8 km/h | cycling | Cyclique |
|
||||
| T+20s | 15 km/h | cycling | Cyclique |
|
||||
| T+30s | 18 km/h | cycling | Cyclique stable|
|
||||
Alors le mode bascule automatiquement de "piéton" à "vélo"
|
||||
Et une notification s'affiche: "Mode vélo activé"
|
||||
Et les paramètres audio sont ajustés pour réduire les notifications visuelles
|
||||
Et un événement "TRAVEL_MODE_CHANGED" est enregistré avec transition: "walking_to_cycling"
|
||||
Et la métrique "travel_mode.transition.walking_to_cycling" est incrémentée
|
||||
|
||||
Scénario: Détection ambiguë avec faible confiance
|
||||
Étant donné un utilisateur "grace@roadwave.fr" en déplacement
|
||||
Quand le système détecte des indicateurs contradictoires:
|
||||
| Indicateur | Valeur |
|
||||
| Vitesse GPS | 12 km/h |
|
||||
| Activity Recognition | unknown |
|
||||
| Accéléromètre | Pattern irrégulier |
|
||||
| Confiance de détection | 45% |
|
||||
Alors le mode actuel est conservé (pas de changement)
|
||||
Et une icône d'interrogation s'affiche discrètement
|
||||
Et l'utilisateur peut forcer manuellement le mode via un menu rapide
|
||||
Et un événement "TRAVEL_MODE_UNCERTAIN" est enregistré
|
||||
Et la métrique "travel_mode.uncertain" est incrémentée
|
||||
|
||||
Scénario: Forçage manuel du mode de déplacement
|
||||
Étant donné un utilisateur "henry@roadwave.fr" en mode auto-détecté "piéton"
|
||||
Mais il est en réalité en voiture (passager)
|
||||
Quand l'utilisateur ouvre le menu rapide et sélectionne "Mode voiture"
|
||||
Alors le mode "voiture" est forcé manuellement
|
||||
Et l'auto-détection est temporairement désactivée pour 30 minutes
|
||||
Et un événement "TRAVEL_MODE_FORCED_MANUAL" est enregistré avec ancienMode: "walking", nouveauMode: "car"
|
||||
Et la métrique "travel_mode.manual_override" est incrémentée
|
||||
Et après 30 minutes, l'auto-détection se réactive automatiquement
|
||||
|
||||
Scénario: Mode stationnaire détecté (arrêt prolongé)
|
||||
Étant donné un utilisateur "iris@roadwave.fr" en mode voiture
|
||||
Et il est arrêté à un feu rouge depuis 2 minutes
|
||||
Quand le système détecte:
|
||||
| Indicateur | Valeur |
|
||||
| Vitesse GPS | 0 km/h |
|
||||
| Activity Recognition | stationary |
|
||||
| Durée d'immobilité | 120 secondes |
|
||||
| Bluetooth connecté | CarPlay |
|
||||
Alors le mode reste "voiture" (pas de changement)
|
||||
Mais un flag "stationary" est activé
|
||||
Et l'audio en cours continue de jouer normalement
|
||||
Et aucun nouveau contenu n'est déclenché automatiquement
|
||||
Et un événement "TRAVEL_MODE_STATIONARY" est enregistré
|
||||
Et la métrique "travel_mode.stationary" est incrémentée
|
||||
|
||||
Scénario: Reprise du mouvement après mode stationnaire
|
||||
Étant donné un utilisateur "jack@roadwave.fr" en mode "voiture stationary"
|
||||
Et il est arrêté depuis 3 minutes
|
||||
Quand le système détecte:
|
||||
| Temps | Vitesse | Activity |
|
||||
| T+0s | 0 km/h | stationary |
|
||||
| T+5s | 10 km/h | automotive |
|
||||
| T+10s | 30 km/h | automotive |
|
||||
Alors le flag "stationary" est désactivé
|
||||
Et le mode "voiture" normal est restauré
|
||||
Et la logique de déclenchement automatique des audio-guides est réactivée
|
||||
Et un événement "TRAVEL_MODE_RESUMED" est enregistré
|
||||
Et la métrique "travel_mode.resumed" est incrémentée
|
||||
|
||||
Scénario: Gestion des permissions de localisation et capteurs
|
||||
Étant donné un utilisateur "kate@roadwave.fr" qui lance l'application
|
||||
Quand les permissions suivantes sont refusées:
|
||||
| Permission | État |
|
||||
| Localisation GPS | Refusée |
|
||||
| Motion & Fitness | Refusée |
|
||||
Alors l'auto-détection du mode est désactivée
|
||||
Et un message s'affiche: "Pour bénéficier de l'expérience optimale, activez les permissions de localisation et mouvement"
|
||||
Et un bouton "Activer les permissions" redirige vers les Réglages
|
||||
Et l'utilisateur doit sélectionner manuellement son mode de déplacement
|
||||
Et un événement "TRAVEL_MODE_PERMISSIONS_DENIED" est enregistré
|
||||
Et la métrique "travel_mode.permissions_denied" est incrémentée
|
||||
|
||||
Scénario: Optimisation de la batterie avec détection adaptative
|
||||
Étant donné un utilisateur "luke@roadwave.fr" avec batterie < 20%
|
||||
Quand le mode économie d'énergie est activé
|
||||
Alors la fréquence de détection du mode est réduite:
|
||||
| Mode normal | Mode économie d'énergie |
|
||||
| Toutes les 5s | Toutes les 30s |
|
||||
Et l'utilisation du GPS est optimisée (requêtes moins fréquentes)
|
||||
Et l'accéléromètre et gyroscope sont consultés moins souvent
|
||||
Et la précision de détection peut être légèrement réduite
|
||||
Et un événement "TRAVEL_MODE_BATTERY_SAVER" est enregistré
|
||||
Et la métrique "travel_mode.battery_saver.enabled" est incrémentée
|
||||
|
||||
Scénario: Historique des modes de déplacement pour statistiques
|
||||
Étant donné un utilisateur "mary@roadwave.fr" qui utilise l'application depuis 1 mois
|
||||
Quand l'utilisateur accède à "Mon compte > Statistiques > Modes de déplacement"
|
||||
Alors l'utilisateur voit un graphique avec répartition:
|
||||
| Mode | Temps total | Pourcentage |
|
||||
| Voiture | 15h 30min | 45% |
|
||||
| Piéton | 12h 10min | 35% |
|
||||
| Vélo | 5h 20min | 15% |
|
||||
| Transports | 1h 40min | 5% |
|
||||
Et des insights sont affichés: "Vous utilisez principalement RoadWave en voiture"
|
||||
Et les données sont conservées de manière agrégée pour respecter le RGPD
|
||||
|
||||
Scénario: Métriques de performance de la détection
|
||||
Étant donné que le système traite 100 000 détections de mode par heure
|
||||
Quand les métriques de performance sont collectées
|
||||
Alors les indicateurs suivants sont respectés:
|
||||
| Métrique | Valeur cible |
|
||||
| Temps de détection du mode | < 100ms |
|
||||
| Précision de détection (voiture) | > 95% |
|
||||
| Précision de détection (piéton) | > 90% |
|
||||
| Précision de détection (vélo) | > 85% |
|
||||
| Taux de transitions incorrectes | < 5% |
|
||||
| Consommation batterie par détection | < 0.01% |
|
||||
Et les métriques sont exportées vers le système de monitoring
|
||||
Et des alertes sont déclenchées si la précision < 80%
|
||||
|
||||
Scénario: A/B testing des algorithmes de détection
|
||||
Étant donné que le système teste 2 algorithmes de détection:
|
||||
| Algorithme | Description |
|
||||
| A | Basé sur CoreMotion uniquement |
|
||||
| B | Combinaison capteurs + ML |
|
||||
Quand un utilisateur "nathan@roadwave.fr" est assigné au groupe B
|
||||
Alors l'algorithme B est utilisé pour la détection
|
||||
Et les métriques de précision sont tracées séparément par algorithme
|
||||
Et les événements incluent le tag "algorithm_version: B"
|
||||
Et après analyse, l'algorithme le plus performant est déployé à 100%
|
||||
@@ -0,0 +1,191 @@
|
||||
# language: fr
|
||||
|
||||
@api @audio-guides @navigation @mvp
|
||||
Fonctionnalité: Gestion des points d'intérêt manqués
|
||||
|
||||
En tant qu'utilisateur
|
||||
Je veux pouvoir gérer les points d'intérêt que j'ai manqués
|
||||
Afin de compléter mon expérience audio-guide même après avoir dépassé certains points
|
||||
|
||||
Contexte:
|
||||
Étant donné que le système de gestion des points manqués respecte:
|
||||
| Paramètre | Valeur |
|
||||
| Distance max pour considérer "manqué" | 1 km |
|
||||
| Temps max pour considérer "manqué" | 10 minutes |
|
||||
| Possibilité de retour arrière | Oui |
|
||||
| Lecture différée autorisée | Oui |
|
||||
|
||||
Scénario: Détection automatique d'un point manqué en mode voiture
|
||||
Étant donné un utilisateur "alice@roadwave.fr" en mode voiture à 90 km/h
|
||||
Et elle suit l'audio-guide "Route des Châteaux de la Loire"
|
||||
Et le prochain point d'intérêt est le Château de Chaumont
|
||||
Quand elle dépasse le château sans entrer dans son rayon de déclenchement (400m)
|
||||
Et elle s'éloigne à plus de 1 km du point
|
||||
Alors le système marque le point comme "Manqué"
|
||||
Et une notification discrète s'affiche: "Point manqué : Château de Chaumont"
|
||||
Et le point apparaît dans la section "Points manqués" de la liste
|
||||
Et un événement "POI_MARKED_AS_MISSED" est enregistré avec raison: "out_of_range"
|
||||
Et la métrique "poi.missed.out_of_range" est incrémentée
|
||||
|
||||
Scénario: Affichage de la liste des points manqués
|
||||
Étant donné un utilisateur "bob@roadwave.fr" qui a manqué 3 points sur 10
|
||||
Quand il ouvre la liste des séquences
|
||||
Alors il voit une section dédiée "Points manqués (3)":
|
||||
| Point d'intérêt | Distance actuelle | Actions |
|
||||
| Château de Chaumont | 15 km en arrière | [Écouter] [Y retourner] |
|
||||
| Musée de Cluny | 8 km en arrière | [Écouter] [Y retourner] |
|
||||
| Rue Mouffetard | 2 km en arrière | [Écouter] [Y retourner] |
|
||||
Et un compteur global affiche: "7/10 points visités"
|
||||
Et un événement "MISSED_POIS_LIST_VIEWED" est enregistré
|
||||
Et la métrique "missed_pois.list_viewed" est incrémentée
|
||||
|
||||
Scénario: Écoute différée d'un point manqué sans retour physique
|
||||
Étant donné un utilisateur "charlie@roadwave.fr" qui a manqué le Château de Chaumont
|
||||
Et il est maintenant à 20 km du château
|
||||
Quand il clique sur "Écouter" dans la liste des points manqués
|
||||
Alors l'audio du Château de Chaumont démarre immédiatement
|
||||
Et un bandeau indique: "Écoute différée - Vous n'êtes pas sur place"
|
||||
Et le point reste marqué comme "Manqué mais écouté"
|
||||
Et un événement "MISSED_POI_LISTENED_REMOTE" est enregistré
|
||||
Et la métrique "missed_poi.listened.remote" est incrémentée
|
||||
|
||||
Scénario: Navigation de retour vers un point manqué
|
||||
Étant donné un utilisateur "david@roadwave.fr" qui a manqué le Musée de Cluny
|
||||
Et il est à 5 km du musée
|
||||
Quand il clique sur "Y retourner" dans la liste des points manqués
|
||||
Alors l'application lance la navigation GPS vers le Musée de Cluny
|
||||
Et un itinéraire est calculé et affiché
|
||||
Et l'ETA est affiché: "12 min en voiture"
|
||||
Et un événement "NAVIGATION_TO_MISSED_POI_STARTED" est enregistré
|
||||
Et la métrique "missed_poi.navigation_started" est incrémentée
|
||||
|
||||
Scénario: Retour physique et déclenchement automatique d'un point manqué
|
||||
Étant donné un utilisateur "eve@roadwave.fr" qui a manqué le Château de Chaumont
|
||||
Et elle a cliqué sur "Y retourner"
|
||||
Quand elle arrive dans le rayon de déclenchement du château (400m)
|
||||
Alors l'audio du château démarre automatiquement
|
||||
Et le point passe du statut "Manqué" à "Visité"
|
||||
Et une notification de succès s'affiche: "✓ Point complété : Château de Chaumont"
|
||||
Et un événement "MISSED_POI_COMPLETED" est enregistré
|
||||
Et la métrique "missed_poi.completed" est incrémentée
|
||||
|
||||
Scénario: Proposition automatique de retour pour points manqués à proximité
|
||||
Étant donné un utilisateur "frank@roadwave.fr" qui a manqué la Rue Mouffetard
|
||||
Et il continue son parcours et arrive près d'un autre point
|
||||
Quand le système détecte qu'il est à 800m de la Rue Mouffetard
|
||||
Alors une notification proactive s'affiche: "Point manqué à proximité : Rue Mouffetard (800m). Y aller ?"
|
||||
Et deux boutons sont proposés: [Oui, y aller] [Non, continuer]
|
||||
Et un événement "MISSED_POI_PROXIMITY_SUGGESTION" est enregistré
|
||||
Et la métrique "missed_poi.proximity_suggestion" est incrémentée
|
||||
|
||||
Scénario: Ignorance volontaire d'un point manqué
|
||||
Étant donné un utilisateur "grace@roadwave.fr" qui a manqué le Musée de Cluny
|
||||
Et elle ne souhaite pas y retourner
|
||||
Quand elle fait glisser le point vers la gauche dans la liste
|
||||
Et clique sur "Ignorer définitivement"
|
||||
Alors le point est retiré de la liste des points manqués
|
||||
Et il passe au statut "Ignoré"
|
||||
Et il ne sera plus proposé dans les suggestions
|
||||
Et un événement "MISSED_POI_IGNORED" est enregistré
|
||||
Et la métrique "missed_poi.ignored" est incrémentée
|
||||
|
||||
Scénario: Réinitialisation d'un point ignoré
|
||||
Étant donné un utilisateur "henry@roadwave.fr" qui a ignoré le Musée de Cluny
|
||||
Quand il accède aux paramètres de l'audio-guide
|
||||
Et clique sur "Voir les points ignorés (1)"
|
||||
Alors il voit la liste: "Musée de Cluny - Ignoré"
|
||||
Quand il clique sur "Réactiver"
|
||||
Alors le point repasse en statut "Manqué"
|
||||
Et il réapparaît dans la liste des points manqués
|
||||
Et un événement "MISSED_POI_REACTIVATED" est enregistré
|
||||
Et la métrique "missed_poi.reactivated" est incrémentée
|
||||
|
||||
Scénario: Marquage automatique comme manqué après délai en mode piéton
|
||||
Étant donné un utilisateur "iris@roadwave.fr" en mode piéton
|
||||
Et elle est à 150m du Panthéon depuis 15 minutes (stationnaire)
|
||||
Et elle n'a pas déclenché le point d'intérêt
|
||||
Quand elle reprend sa marche et s'éloigne à plus de 500m
|
||||
Alors le point est marqué comme "Manqué"
|
||||
Et une notification s'affiche: "Point manqué : Panthéon. Voulez-vous y retourner ?"
|
||||
Et un événement "POI_MARKED_AS_MISSED" est enregistré avec raison: "timeout_stationary"
|
||||
Et la métrique "poi.missed.timeout" est incrémentée
|
||||
|
||||
Scénario: Statistiques des points manqués en fin de parcours
|
||||
Étant donné un utilisateur "jack@roadwave.fr" qui a terminé un audio-guide
|
||||
Et il a visité 7 points sur 10, manqué 2 points et ignoré 1 point
|
||||
Quand il consulte l'écran de fin de parcours
|
||||
Alors il voit les statistiques:
|
||||
| Métrique | Valeur |
|
||||
| Points visités | 7/10 (70%) |
|
||||
| Points manqués | 2 (Chaumont, Cluny) |
|
||||
| Points ignorés | 1 (Rue Mouffetard) |
|
||||
| Taux de complétion | 70% |
|
||||
| Badge obtenu | Explorateur Bronze |
|
||||
Et un bouton "Compléter les points manqués" est proposé
|
||||
Et un événement "AUDIO_GUIDE_STATS_VIEWED" est enregistré
|
||||
|
||||
Scénario: Reprise d'un audio-guide pour compléter les points manqués
|
||||
Étant donné un utilisateur "kate@roadwave.fr" qui a terminé un audio-guide avec 2 points manqués
|
||||
Quand elle clique sur "Compléter les points manqués"
|
||||
Alors l'audio-guide est réactivé en mode "Rattrapage"
|
||||
Et seuls les 2 points manqués sont actifs sur la carte
|
||||
Et les points déjà visités sont grisés
|
||||
Et la navigation se concentre uniquement sur les points manqués
|
||||
Et un événement "AUDIO_GUIDE_CATCH_UP_MODE" est enregistré
|
||||
Et la métrique "audio_guide.catch_up.started" est incrémentée
|
||||
|
||||
Scénario: Badge de complétion "Perfectionniste" pour 100% de complétion
|
||||
Étant donné un utilisateur "luke@roadwave.fr" qui a visité 10/10 points
|
||||
Et il a initialement manqué 2 points mais y est retourné
|
||||
Quand il termine l'audio-guide avec 100% de complétion
|
||||
Alors un badge spécial "Perfectionniste" est débloqué
|
||||
Et une animation de célébration est affichée
|
||||
Et un événement "BADGE_PERFECTIONIST_UNLOCKED" est enregistré
|
||||
Et la métrique "badges.perfectionist.unlocked" est incrémentée
|
||||
|
||||
Scénario: Notification push après 24h pour rappel des points manqués
|
||||
Étant donné un utilisateur "mary@roadwave.fr" qui a terminé un audio-guide hier
|
||||
Et elle a manqué 3 points sur 10
|
||||
Quand 24 heures se sont écoulées depuis la fin du parcours
|
||||
Alors une notification push est envoyée:
|
||||
"Vous avez manqué 3 points lors de votre visite du Quartier Latin. Voulez-vous les découvrir ?"
|
||||
Et un lien direct vers la liste des points manqués est inclus
|
||||
Et un événement "MISSED_POIS_REMINDER_SENT" est enregistré
|
||||
Et la métrique "missed_pois.reminder_sent" est incrémentée
|
||||
|
||||
Scénario: Mode "Rattrapage intelligent" avec optimisation de l'itinéraire
|
||||
Étant donné un utilisateur "nathan@roadwave.fr" avec 3 points manqués:
|
||||
| Point | Position actuelle | Distance |
|
||||
| Château de Chaumont | 48.8475, 2.3450 | 12 km |
|
||||
| Musée de Cluny | 48.8505, 2.3434 | 8 km |
|
||||
| Rue Mouffetard | 48.8429, 2.3498 | 5 km |
|
||||
Quand il clique sur "Itinéraire optimisé pour rattrapage"
|
||||
Alors le système calcule l'itinéraire le plus court pour visiter les 3 points:
|
||||
| Ordre | Point | Distance cumulée |
|
||||
| 1 | Rue Mouffetard | 5 km |
|
||||
| 2 | Musée de Cluny | 8.5 km |
|
||||
| 3 | Château de Chaumont | 20.5 km |
|
||||
Et l'ETA total est affiché: "1h 15min en voiture"
|
||||
Et un événement "OPTIMIZED_CATCH_UP_ROUTE_CALCULATED" est enregistré
|
||||
Et la métrique "missed_pois.optimized_route" est incrémentée
|
||||
|
||||
Scénario: Désactivation de la détection automatique des points manqués
|
||||
Étant donné un utilisateur "olive@roadwave.fr" qui préfère une expérience libre
|
||||
Quand elle active l'option "Désactiver la détection des points manqués"
|
||||
Alors les points ne sont jamais marqués comme "Manqués"
|
||||
Et aucune notification de point manqué n'est affichée
|
||||
Et l'utilisateur peut toujours écouter tous les points manuellement
|
||||
Et un événement "MISSED_POIS_DETECTION_DISABLED" est enregistré
|
||||
Et la métrique "missed_pois.detection_disabled" est incrémentée
|
||||
|
||||
Scénario: Métriques de performance de la gestion des points manqués
|
||||
Étant donné que 10 000 utilisateurs ont terminé des audio-guides
|
||||
Quand les métriques sont collectées
|
||||
Alors les indicateurs suivants sont disponibles:
|
||||
| Métrique | Valeur moyenne |
|
||||
| Pourcentage de points manqués par parcours| 18% |
|
||||
| Taux de retour aux points manqués | 35% |
|
||||
| Taux d'écoute différée (sans retour) | 55% |
|
||||
| Taux d'ignorance définitive | 10% |
|
||||
| Taux de complétion après rattrapage | 92% |
|
||||
Et les métriques sont exportées vers le système de monitoring
|
||||
@@ -0,0 +1,485 @@
|
||||
# language: fr
|
||||
|
||||
Fonctionnalité: API - Métriques et analytics audio-guides
|
||||
En tant que système backend
|
||||
Je veux collecter et exposer les métriques d'écoute des audio-guides
|
||||
Afin de fournir des insights aux créateurs
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est démarrée
|
||||
Et que le créateur "creator@example.com" est authentifié
|
||||
|
||||
# Statistiques globales audio-guide
|
||||
|
||||
Scénario: GET /api/v1/creators/me/audio-guides/{id}/stats - Statistiques générales
|
||||
Étant donné un audio-guide "ag_123" avec les métriques suivantes:
|
||||
| ecoutes_totales | ecoutes_completes | taux_completion | temps_ecoute_total |
|
||||
| 1542 | 892 | 58% | 423h |
|
||||
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/stats"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"listens_total": 1542,
|
||||
"listens_complete": 892,
|
||||
"completion_rate": 58,
|
||||
"total_listen_time_seconds": 1522800,
|
||||
"avg_listen_time_seconds": 988,
|
||||
"unique_listeners": 1124,
|
||||
"repeat_listeners": 418
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: Statistiques par période (7j, 30j, 90j, all-time)
|
||||
Étant donné un audio-guide avec historique
|
||||
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/stats?period=7d"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et les statistiques sur les 7 derniers jours sont retournées
|
||||
|
||||
Plan du Scénario: Périodes disponibles
|
||||
Quand je fais un GET avec period=<period>
|
||||
Alors les stats de la période <description> sont retournées
|
||||
|
||||
Exemples:
|
||||
| period | description |
|
||||
| 7d | 7 derniers jours |
|
||||
| 30d | 30 derniers jours |
|
||||
| 90d | 90 derniers jours |
|
||||
| all | Depuis la création |
|
||||
|
||||
# Métriques par séquence
|
||||
|
||||
Scénario: GET /api/v1/creators/me/audio-guides/{id}/sequences/stats - Stats par séquence
|
||||
Étant donné un audio-guide de 12 séquences
|
||||
Et les métriques suivantes:
|
||||
| sequence | starts | completions | abandon_rate |
|
||||
| 1 | 1000 | 950 | 5% |
|
||||
| 2 | 950 | 920 | 3% |
|
||||
| 3 | 920 | 850 | 8% |
|
||||
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/sequences/stats"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"sequences": [
|
||||
{
|
||||
"sequence_id": "seq_1",
|
||||
"sequence_number": 1,
|
||||
"title": "Introduction",
|
||||
"starts": 1000,
|
||||
"completions": 950,
|
||||
"completion_rate": 95,
|
||||
"abandon_rate": 5,
|
||||
"avg_listen_time": 132,
|
||||
"duration": 135
|
||||
},
|
||||
{
|
||||
"sequence_id": "seq_2",
|
||||
"sequence_number": 2,
|
||||
"title": "Pyramide du Louvre",
|
||||
"starts": 950,
|
||||
"completions": 920,
|
||||
"completion_rate": 97,
|
||||
"abandon_rate": 3,
|
||||
"avg_listen_time": 106,
|
||||
"duration": 108
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: Identification séquence la plus écoutée
|
||||
Étant donné les statistiques par séquence
|
||||
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/sequences/top"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"most_listened": {
|
||||
"sequence_id": "seq_3",
|
||||
"title": "La Joconde",
|
||||
"starts": 920,
|
||||
"reason": "popular_highlight"
|
||||
},
|
||||
"least_listened": {
|
||||
"sequence_id": "seq_11",
|
||||
"title": "Aile Richelieu",
|
||||
"starts": 580,
|
||||
"reason": "late_sequence"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
# Points d'abandon
|
||||
|
||||
Scénario: Détection point d'abandon critique
|
||||
Étant donné un audio-guide avec taux de complétion 58%
|
||||
Et que 35% des utilisateurs abandonnent à la séquence 7
|
||||
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/abandon-analysis"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"critical_abandon_point": {
|
||||
"sequence_id": "seq_7",
|
||||
"sequence_number": 7,
|
||||
"title": "Aile Richelieu",
|
||||
"abandon_rate": 35,
|
||||
"severity": "high",
|
||||
"suggestion": "Réduire la durée (8 min actuellement) ou rendre plus captivant"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: Heatmap des abandons
|
||||
Étant donné un audio-guide
|
||||
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/abandon-heatmap"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient une heatmap:
|
||||
"""json
|
||||
{
|
||||
"heatmap": [
|
||||
{"sequence": 1, "abandon_count": 50, "intensity": "low"},
|
||||
{"sequence": 2, "abandon_count": 30, "intensity": "low"},
|
||||
{"sequence": 7, "abandon_count": 320, "intensity": "high"},
|
||||
{"sequence": 12, "abandon_count": 70, "intensity": "medium"}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
# Métriques géographiques
|
||||
|
||||
Scénario: GET /api/v1/creators/me/audio-guides/{id}/geographic-stats - Stats géo
|
||||
Étant donné un audio-guide géolocalisé
|
||||
Et les écoutes suivantes par région:
|
||||
| region | listens | completions |
|
||||
| Île-de-France | 850 | 520 |
|
||||
| PACA | 320 | 180 |
|
||||
| Auvergne | 145 | 90 |
|
||||
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/geographic-stats"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"by_region": [
|
||||
{
|
||||
"region": "Île-de-France",
|
||||
"listens": 850,
|
||||
"completions": 520,
|
||||
"completion_rate": 61
|
||||
},
|
||||
{
|
||||
"region": "PACA",
|
||||
"listens": 320,
|
||||
"completions": 180,
|
||||
"completion_rate": 56
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: Heatmap géographique des écoutes
|
||||
Étant donné un audio-guide avec points GPS
|
||||
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/geographic-heatmap"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"points": [
|
||||
{
|
||||
"sequence_id": "seq_1",
|
||||
"gps_point": {"lat": 43.1234, "lon": 2.5678},
|
||||
"listen_count": 1000,
|
||||
"density": "high"
|
||||
},
|
||||
{
|
||||
"sequence_id": "seq_2",
|
||||
"gps_point": {"lat": 43.1245, "lon": 2.5690},
|
||||
"listen_count": 950,
|
||||
"density": "high"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
# Métriques déclenchement GPS
|
||||
|
||||
Scénario: Attribution GPS auto vs manuel
|
||||
Étant donné un audio-guide voiture avec 8 points GPS
|
||||
Et les déclenchements suivants:
|
||||
| type | count |
|
||||
| GPS auto | 542 |
|
||||
| Manuel | 123 |
|
||||
| Point manqué | 89 |
|
||||
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/trigger-stats"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"total_triggers": 754,
|
||||
"by_type": {
|
||||
"gps_auto": 542,
|
||||
"manual": 123,
|
||||
"missed_point": 89
|
||||
},
|
||||
"gps_auto_rate": 72,
|
||||
"manual_rate": 16,
|
||||
"missed_rate": 12
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: Points GPS les plus manqués
|
||||
Étant donné les statistiques de points manqués
|
||||
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/missed-points"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"most_missed": [
|
||||
{
|
||||
"sequence_id": "seq_5",
|
||||
"title": "Enclos des éléphants",
|
||||
"missed_count": 45,
|
||||
"missed_rate": 12,
|
||||
"suggestion": "Rayon trop petit (30m) ou point mal placé"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
# Temps moyen par séquence
|
||||
|
||||
Scénario: Comparaison durée audio vs temps d'écoute moyen
|
||||
Étant donné les métriques temporelles suivantes:
|
||||
| sequence | duration | avg_listen_time | ecart |
|
||||
| 1 | 135 | 130 | -5s |
|
||||
| 2 | 108 | 90 | -18s |
|
||||
| 3 | 222 | 220 | -2s |
|
||||
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/time-analysis"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"sequences": [
|
||||
{
|
||||
"sequence_id": "seq_1",
|
||||
"duration_seconds": 135,
|
||||
"avg_listen_time": 130,
|
||||
"delta": -5,
|
||||
"completion_avg": 96
|
||||
},
|
||||
{
|
||||
"sequence_id": "seq_2",
|
||||
"duration_seconds": 108,
|
||||
"avg_listen_time": 90,
|
||||
"delta": -18,
|
||||
"completion_avg": 83,
|
||||
"warning": "Séquence souvent skippée ou abandonnée avant la fin"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
# Notifications milestones
|
||||
|
||||
Scénario: POST /api/v1/audio-guides/{id}/milestones/check - Vérification milestone
|
||||
Étant donné qu'un audio-guide atteint 1000 écoutes
|
||||
Quand le système vérifie les milestones
|
||||
Alors un événement "milestone_reached" est émis
|
||||
Et une notification est envoyée au créateur:
|
||||
"""json
|
||||
{
|
||||
"type": "milestone",
|
||||
"milestone_type": "listens_1000",
|
||||
"audio_guide_id": "ag_123",
|
||||
"message": "Félicitations ! Votre audio-guide 'Visite du Louvre' a atteint 1000 écoutes !",
|
||||
"stats": {
|
||||
"listens": 1000,
|
||||
"completion_rate": 58
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
Plan du Scénario: Milestones prédéfinis
|
||||
Étant donné qu'un audio-guide atteint <seuil> écoutes
|
||||
Quand le milestone est vérifié
|
||||
Alors une notification "<type>" est envoyée
|
||||
|
||||
Exemples:
|
||||
| seuil | type |
|
||||
| 100 | listens_100 |
|
||||
| 500 | listens_500 |
|
||||
| 1000 | listens_1000 |
|
||||
| 5000 | listens_5000 |
|
||||
| 10000 | listens_10000 |
|
||||
|
||||
# Graphiques et visualisations
|
||||
|
||||
Scénario: GET /api/v1/creators/me/audio-guides/{id}/completion-funnel - Entonnoir complétion
|
||||
Étant donné un audio-guide de 12 séquences
|
||||
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/completion-funnel"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient un graphique en entonnoir:
|
||||
"""json
|
||||
{
|
||||
"funnel": [
|
||||
{"sequence": 1, "listeners": 1000, "percentage": 100},
|
||||
{"sequence": 2, "listeners": 950, "percentage": 95},
|
||||
{"sequence": 3, "listeners": 890, "percentage": 89},
|
||||
{"sequence": 12, "listeners": 580, "percentage": 58}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: GET /api/v1/creators/me/audio-guides/{id}/listens-over-time - Écoutes dans le temps
|
||||
Étant donné un audio-guide avec historique
|
||||
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/listens-over-time?period=30d"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient une série temporelle:
|
||||
"""json
|
||||
{
|
||||
"period": "30d",
|
||||
"granularity": "day",
|
||||
"data_points": [
|
||||
{"date": "2026-01-01", "listens": 45, "completions": 28},
|
||||
{"date": "2026-01-02", "listens": 52, "completions": 31},
|
||||
{"date": "2026-01-03", "listens": 38, "completions": 24}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
# Comparaisons et benchmarks
|
||||
|
||||
Scénario: GET /api/v1/creators/me/audio-guides/compare - Comparaison audio-guides
|
||||
Étant donné que le créateur a 3 audio-guides:
|
||||
| audio_guide_id | title | listens | completion_rate |
|
||||
| ag_1 | Tour de Paris | 1200 | 65% |
|
||||
| ag_2 | Visite Louvre | 1542 | 58% |
|
||||
| ag_3 | Safari du Paugre | 890 | 72% |
|
||||
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/compare"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"audio_guides": [
|
||||
{
|
||||
"audio_guide_id": "ag_2",
|
||||
"title": "Visite Louvre",
|
||||
"listens": 1542,
|
||||
"completion_rate": 58,
|
||||
"rank_by_listens": 1,
|
||||
"rank_by_completion": 2
|
||||
},
|
||||
{
|
||||
"audio_guide_id": "ag_1",
|
||||
"title": "Tour de Paris",
|
||||
"listens": 1200,
|
||||
"completion_rate": 65,
|
||||
"rank_by_listens": 2,
|
||||
"rank_by_completion": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: Benchmark par rapport à la moyenne plateforme
|
||||
Étant donné qu'un audio-guide a un taux de complétion de 58%
|
||||
Et que la moyenne plateforme pour la catégorie "musée" est 62%
|
||||
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/benchmark"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"your_completion_rate": 58,
|
||||
"category_avg": 62,
|
||||
"platform_avg": 60,
|
||||
"performance": "below_category_avg",
|
||||
"percentile": 45
|
||||
}
|
||||
"""
|
||||
|
||||
# Événements trackés
|
||||
|
||||
Scénario: POST /api/v1/events/track - Tracking événements utilisateur
|
||||
Étant donné qu'un utilisateur interagit avec un audio-guide
|
||||
Quand un événement se produit
|
||||
Alors il est tracké avec les données suivantes:
|
||||
| événement | données |
|
||||
| audio_guide_started | audio_guide_id, mode, user_id |
|
||||
| sequence_completed | sequence_id, completion_rate, duration |
|
||||
| audio_guide_completed | audio_guide_id, total_time, sequences_count|
|
||||
| point_gps_triggered | point_id, distance, auto_or_manual |
|
||||
| point_gps_missed | point_id, distance, action_taken |
|
||||
|
||||
Scénario: Exemple événement audio_guide_started
|
||||
Quand un audio-guide démarre
|
||||
Alors l'événement suivant est envoyé:
|
||||
"""json
|
||||
{
|
||||
"event_type": "audio_guide_started",
|
||||
"timestamp": "2026-01-22T14:00:00Z",
|
||||
"user_id": "user_456",
|
||||
"audio_guide_id": "ag_123",
|
||||
"mode": "voiture",
|
||||
"device": "ios",
|
||||
"location": {"lat": 43.1234, "lon": 2.5678}
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: Exemple événement point_gps_triggered
|
||||
Quand un point GPS déclenche une séquence
|
||||
Alors l'événement suivant est envoyé:
|
||||
"""json
|
||||
{
|
||||
"event_type": "point_gps_triggered",
|
||||
"timestamp": "2026-01-22T14:05:30Z",
|
||||
"user_id": "user_456",
|
||||
"audio_guide_id": "ag_123",
|
||||
"sequence_id": "seq_2",
|
||||
"trigger_type": "gps_auto",
|
||||
"distance_to_point": 25,
|
||||
"speed_kmh": 28
|
||||
}
|
||||
"""
|
||||
|
||||
# Export données
|
||||
|
||||
Scénario: GET /api/v1/creators/me/audio-guides/{id}/export - Export CSV
|
||||
Étant donné un audio-guide avec historique complet
|
||||
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/export?format=csv"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le Content-Type est "text/csv"
|
||||
Et le fichier CSV contient:
|
||||
| user_id | sequence_id | started_at | completed_at | completion_rate |
|
||||
| user_123 | seq_1 | 2026-01-22 14:10:00 | 2026-01-22 14:12:15 | 100 |
|
||||
| user_123 | seq_2 | 2026-01-22 14:12:20 | 2026-01-22 14:14:08 | 100 |
|
||||
|
||||
Scénario: Export JSON pour analyse externe
|
||||
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/export?format=json"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le Content-Type est "application/json"
|
||||
Et le fichier JSON contient toutes les métriques détaillées
|
||||
|
||||
# Cache et performance
|
||||
|
||||
Scénario: Cache Redis pour stats fréquemment consultées
|
||||
Étant donné que les stats globales d'un audio-guide sont en cache Redis
|
||||
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/stats"
|
||||
Alors les stats sont récupérées depuis Redis (pas PostgreSQL)
|
||||
Et le temps de réponse est < 50ms
|
||||
Et le cache a un TTL de 5 minutes
|
||||
|
||||
Scénario: Invalidation cache lors de nouvelles écoutes
|
||||
Étant donné que les stats sont en cache Redis
|
||||
Quand une nouvelle écoute complète est enregistrée
|
||||
Alors le cache Redis est invalidé pour cet audio-guide
|
||||
Et le prochain GET recalcule les stats depuis PostgreSQL
|
||||
|
||||
Scénario: Pré-calcul stats quotidien (job batch)
|
||||
Étant donné que le job batch s'exécute chaque nuit à 3h
|
||||
Quand le job démarre
|
||||
Alors pour chaque audio-guide actif:
|
||||
- Les stats sont calculées depuis les événements bruts
|
||||
- Les résultats sont stockés dans audio_guide_stats_daily
|
||||
- Les agrégations (7j, 30j, 90j) sont pré-calculées
|
||||
Et les requêtes du lendemain sont instantanées (lecture table pré-calculée)
|
||||
@@ -0,0 +1,91 @@
|
||||
# language: fr
|
||||
|
||||
@api @audio-guides @cycling @transit @mvp
|
||||
Fonctionnalité: Modes vélo et transports en commun complets
|
||||
|
||||
En tant qu'utilisateur à vélo ou en transports
|
||||
Je veux une expérience adaptée à mon mode de déplacement
|
||||
Afin de profiter des audio-guides en toute sécurité
|
||||
|
||||
Contexte:
|
||||
Étant donné les caractéristiques des modes:
|
||||
| Mode | Vitesse moyenne | Notifications | Auto-play |
|
||||
| Vélo | 15-20 km/h | Audio priority| Optionnel |
|
||||
| Transports | 30-40 km/h | Visuelles OK | Aux arrêts|
|
||||
|
||||
Scénario: Mode vélo avec notifications audio prioritaires
|
||||
Étant donné un utilisateur "alice@roadwave.fr" en mode vélo à 18 km/h
|
||||
Quand elle approche d'un point d'intérêt
|
||||
Alors une notification audio est jouée (sécurité)
|
||||
Et les notifications visuelles sont minimales
|
||||
Et l'auto-play est optionnel (configurable)
|
||||
Et un événement "CYCLING_MODE_POI_NOTIFICATION" est enregistré
|
||||
|
||||
Scénario: Mode transports avec détection des arrêts/stations
|
||||
Étant donné un utilisateur "bob@roadwave.fr" en mode transports
|
||||
Quand le système détecte un arrêt prolongé (station)
|
||||
Alors l'audio-guide peut se déclencher à la station
|
||||
Et les informations visuelles sont complètes
|
||||
Et un événement "TRANSIT_STOP_DETECTED" est enregistré
|
||||
|
||||
Scénario: Adaptation du rayon de déclenchement en mode vélo
|
||||
Étant donné un créateur "charlie@roadwave.fr" avec rayon adaptatif activé
|
||||
Quand un utilisateur en mode vélo approche du POI
|
||||
Alors le rayon est augmenté de 50% (anticipation)
|
||||
Et le déclenchement se fait plus tôt
|
||||
Et un événement "CYCLING_RADIUS_ADAPTED" est enregistré
|
||||
|
||||
Scénario: Gestion des tunnels en mode transports
|
||||
Étant donné un utilisateur "david@roadwave.fr" en métro
|
||||
Quand il entre dans un tunnel (perte GPS)
|
||||
Alors la position est estimée selon la ligne de métro
|
||||
Et les séquences continuent de se jouer normalement
|
||||
Et un événement "TRANSIT_TUNNEL_MODE" est enregistré
|
||||
|
||||
Scénario: Sécurité en mode vélo - pause automatique si danger
|
||||
Étant donné un utilisateur "eve@roadwave.fr" en mode vélo
|
||||
Quand une accélération brusque est détectée (freinage)
|
||||
Alors l'audio se met en pause automatiquement
|
||||
Et reprend quand la vitesse se stabilise
|
||||
Et un événement "CYCLING_SAFETY_PAUSE" est enregistré
|
||||
|
||||
Scénario: Mode transports avec synchronisation aux horaires
|
||||
Étant donné un utilisateur "frank@roadwave.fr" en bus
|
||||
Quand le système détecte les arrêts réguliers
|
||||
Alors les séquences sont synchronisées aux stations
|
||||
Et l'ETA est calculé selon les arrêts
|
||||
Et un événement "TRANSIT_SCHEDULE_SYNC" est enregistré
|
||||
|
||||
Scénario: Statistiques spécifiques au mode vélo
|
||||
Étant donné un utilisateur "grace@roadwave.fr" qui termine un audio-guide à vélo
|
||||
Alors il voit des statistiques adaptées:
|
||||
| Métrique | Valeur |
|
||||
| Distance parcourue | 12.5 km |
|
||||
| Temps de trajet | 45 min |
|
||||
| Vitesse moyenne | 16.7 km/h |
|
||||
| Dénivelé positif | 120m |
|
||||
Et un événement "CYCLING_STATS_DISPLAYED" est enregistré
|
||||
|
||||
Scénario: Détection automatique changement vélo → transports
|
||||
Étant donné un utilisateur "henry@roadwave.fr" en mode vélo
|
||||
Quand il monte dans un bus avec son vélo
|
||||
Alors le mode bascule automatiquement en "transports"
|
||||
Et l'expérience s'adapte instantanément
|
||||
Et un événement "MODE_SWITCH_CYCLING_TO_TRANSIT" est enregistré
|
||||
|
||||
Scénario: Mode vélo électrique avec détection
|
||||
Étant donné un utilisateur "iris@roadwave.fr" sur un vélo électrique
|
||||
Quand la vitesse moyenne est > 25 km/h (VAE)
|
||||
Alors le système adapte les rayons de déclenchement
|
||||
Et l'ETA est calculé avec vitesse VAE
|
||||
Et un événement "EBIKE_MODE_DETECTED" est enregistré
|
||||
|
||||
Scénario: Métriques de performance modes vélo et transports
|
||||
Étant donné que 10 000 parcours ont été effectués en vélo/transports
|
||||
Alors les indicateurs suivants sont disponibles:
|
||||
| Métrique | Vélo | Transports |
|
||||
| Taux d'utilisation | 15% | 10% |
|
||||
| Taux de complétion | 82% | 75% |
|
||||
| Vitesse moyenne | 17km/h| 35km/h |
|
||||
| Satisfaction utilisateur | 4.3/5 | 4.1/5 |
|
||||
Et les métriques sont exportées vers le monitoring
|
||||
@@ -0,0 +1,218 @@
|
||||
# language: fr
|
||||
|
||||
@ui @audio-guides @pedestrian @navigation @mvp
|
||||
Fonctionnalité: Navigation libre complète en mode piéton
|
||||
|
||||
En tant qu'utilisateur piéton
|
||||
Je veux naviguer librement dans un audio-guide sans contrainte d'ordre
|
||||
Afin de découvrir les points d'intérêt selon mon itinéraire personnel
|
||||
|
||||
Contexte:
|
||||
Étant donné un audio-guide "Visite du Quartier Latin" avec 8 séquences:
|
||||
| Ordre | Nom | Position GPS | Rayon |
|
||||
| 1 | Notre-Dame | 48.8534, 2.3488 | 100m |
|
||||
| 2 | Sainte-Chapelle | 48.8555, 2.3450 | 80m |
|
||||
| 3 | Panthéon | 48.8462, 2.3464 | 100m |
|
||||
| 4 | Jardin du Luxembourg | 48.8462, 2.3371 | 150m |
|
||||
| 5 | Sorbonne | 48.8487, 2.3431 | 70m |
|
||||
| 6 | Musée de Cluny | 48.8505, 2.3434 | 60m |
|
||||
| 7 | Rue Mouffetard | 48.8429, 2.3498 | 50m |
|
||||
| 8 | Arènes de Lutèce | 48.8456, 2.3523 | 80m |
|
||||
|
||||
Scénario: Démarrage d'un audio-guide en mode piéton avec navigation libre
|
||||
Étant donné un utilisateur "alice@roadwave.fr" en mode piéton
|
||||
Et elle se trouve près de Notre-Dame
|
||||
Quand elle lance l'audio-guide "Visite du Quartier Latin"
|
||||
Alors l'écran principal affiche:
|
||||
| Élément | Contenu |
|
||||
| Carte interactive | Affichée avec 8 marqueurs |
|
||||
| Position utilisateur | Marqueur bleu en temps réel |
|
||||
| Points d'intérêt | Marqueurs numérotés 1-8 |
|
||||
| Distances | Affichées sur chaque marqueur |
|
||||
| Point le plus proche | Surligné en vert (Notre-Dame, 50m) |
|
||||
| Mode de lecture | "Navigation libre" activé par défaut |
|
||||
| Bouton "Liste" | Affiche la liste des séquences |
|
||||
Et un événement "AUDIO_GUIDE_STARTED_FREE_NAVIGATION" est enregistré
|
||||
Et la métrique "audio_guide.started.free_navigation" est incrémentée
|
||||
|
||||
Scénario: Déclenchement automatique au point d'intérêt le plus proche
|
||||
Étant donné un utilisateur "bob@roadwave.fr" en mode piéton
|
||||
Et il a lancé l'audio-guide "Visite du Quartier Latin"
|
||||
Et il marche vers Notre-Dame
|
||||
Quand il entre dans le rayon de 100m de Notre-Dame
|
||||
Alors l'audio de la séquence #1 "Notre-Dame" démarre automatiquement
|
||||
Et une notification s'affiche:
|
||||
| Élément | Contenu |
|
||||
| Titre | 1/8 - Cathédrale Notre-Dame |
|
||||
| Distance | Vous êtes arrivé |
|
||||
| Progression | Barre de lecture audio |
|
||||
| Actions | [Pause] [Liste] [Carte] |
|
||||
Et le marqueur Notre-Dame passe de vert à bleu (en cours)
|
||||
Et un événement "SEQUENCE_AUTO_TRIGGERED" est enregistré
|
||||
Et la métrique "audio_guide.sequence.auto_triggered" est incrémentée
|
||||
|
||||
Scénario: Écoute d'une séquence dans un ordre différent de l'ordre suggéré
|
||||
Étant donné un utilisateur "charlie@roadwave.fr" en mode piéton
|
||||
Et il a lancé l'audio-guide et écouté la séquence #1 "Notre-Dame"
|
||||
Et il décide de se rendre directement au Panthéon (séquence #3)
|
||||
Quand il marche vers le Panthéon en ignorant la séquence #2
|
||||
Et entre dans le rayon de 100m du Panthéon
|
||||
Alors l'audio de la séquence #3 "Panthéon" démarre automatiquement
|
||||
Et la séquence #2 "Sainte-Chapelle" reste disponible et non écoutée
|
||||
Et un événement "SEQUENCE_OUT_OF_ORDER" est enregistré
|
||||
Et la métrique "audio_guide.sequence.out_of_order" est incrémentée
|
||||
Et la progression affiche: 2/8 séquences écoutées
|
||||
|
||||
Scénario: Affichage de la carte avec points d'intérêt colorés par statut
|
||||
Étant donné un utilisateur "david@roadwave.fr" en mode piéton
|
||||
Et il a écouté 3 séquences sur 8
|
||||
Quand il consulte la carte
|
||||
Alors les marqueurs sont colorés selon leur statut:
|
||||
| Séquence | Statut | Couleur | Icône |
|
||||
| Notre-Dame | Écoutée | Bleu | ✓ |
|
||||
| Sainte-Chapelle | Non écoutée | Gris | 2 |
|
||||
| Panthéon | Écoutée | Bleu | ✓ |
|
||||
| Jardin du Luxembourg| Non écoutée | Gris | 4 |
|
||||
| Sorbonne | Écoutée | Bleu | ✓ |
|
||||
| Musée de Cluny | Non écoutée | Gris | 6 |
|
||||
| Rue Mouffetard | En cours | Orange | ▶ |
|
||||
| Arènes de Lutèce | Non écoutée | Gris | 8 |
|
||||
Et le point le plus proche est surligné avec un halo vert
|
||||
Et les distances sont affichées en temps réel
|
||||
|
||||
Scénario: Consultation de la liste des séquences avec filtres
|
||||
Étant donné un utilisateur "eve@roadwave.fr" en mode piéton
|
||||
Quand elle clique sur le bouton "Liste"
|
||||
Alors une liste des séquences s'affiche:
|
||||
| Séquence | Distance | Statut | Actions |
|
||||
| Notre-Dame | 50m | ✓ Écoutée | [Réécouter] |
|
||||
| Sainte-Chapelle | 200m | Non écoutée | [Y aller] [Lire] |
|
||||
| Panthéon | 350m | ✓ Écoutée | [Réécouter] |
|
||||
| Jardin du Luxembourg| 450m | Non écoutée | [Y aller] [Lire] |
|
||||
Et elle peut filtrer par:
|
||||
| Filtre | Options |
|
||||
| Statut | Toutes / Écoutées / Restantes |
|
||||
| Tri | Distance / Ordre suggéré / Durée |
|
||||
Et un compteur affiche: "3/8 séquences écoutées"
|
||||
|
||||
Scénario: Lecture manuelle d'une séquence depuis la liste
|
||||
Étant donné un utilisateur "frank@roadwave.fr" en mode piéton
|
||||
Et il consulte la liste des séquences
|
||||
Quand il clique sur "Lire" pour la séquence #5 "Sorbonne"
|
||||
Alors l'audio de la Sorbonne démarre immédiatement
|
||||
Et peu importe la distance actuelle (peut être loin du point)
|
||||
Et un événement "SEQUENCE_MANUAL_PLAY" est enregistré
|
||||
Et la métrique "audio_guide.sequence.manual_play" est incrémentée
|
||||
Et un message d'information s'affiche: "Vous écoutez cette séquence hors localisation"
|
||||
|
||||
Scénario: Navigation vers un point d'intérêt depuis la liste
|
||||
Étant donné un utilisateur "grace@roadwave.fr" en mode piéton
|
||||
Et elle consulte la liste des séquences
|
||||
Quand elle clique sur "Y aller" pour la séquence #4 "Jardin du Luxembourg"
|
||||
Alors l'application lance la navigation GPS vers le Jardin du Luxembourg
|
||||
Et un itinéraire piéton est calculé et affiché sur la carte
|
||||
Et la distance et le temps estimé sont affichés: "450m - 6 min"
|
||||
Et des instructions de navigation vocales sont données:
|
||||
| Instruction |
|
||||
| Dirigez-vous vers le sud |
|
||||
| Tournez à gauche dans 100 mètres |
|
||||
| Vous arrivez à destination |
|
||||
Et un événement "NAVIGATION_TO_POI_STARTED" est enregistré
|
||||
Et la métrique "audio_guide.navigation.started" est incrémentée
|
||||
|
||||
Scénario: Réécoute d'une séquence déjà complétée
|
||||
Étant donné un utilisateur "henry@roadwave.fr" en mode piéton
|
||||
Et il a déjà écouté la séquence #1 "Notre-Dame" en entier
|
||||
Quand il clique sur "Réécouter" depuis la liste
|
||||
Alors l'audio de Notre-Dame redémarre depuis le début
|
||||
Et un événement "SEQUENCE_REPLAYED" est enregistré
|
||||
Et la métrique "audio_guide.sequence.replayed" est incrémentée
|
||||
Et la séquence reste marquée comme "Écoutée" (pas de duplication)
|
||||
|
||||
Scénario: Pause et reprise d'une séquence en cours
|
||||
Étant donné un utilisateur "iris@roadwave.fr" en mode piéton
|
||||
Et elle écoute la séquence #3 "Panthéon" à la position 2min 30s
|
||||
Quand elle clique sur le bouton "Pause"
|
||||
Alors l'audio se met en pause
|
||||
Et la position de lecture est sauvegardée: 2min 30s
|
||||
Et un événement "SEQUENCE_PAUSED" est enregistré
|
||||
Quand elle clique sur le bouton "Lecture"
|
||||
Alors l'audio reprend exactement à 2min 30s
|
||||
Et un événement "SEQUENCE_RESUMED" est enregistré
|
||||
|
||||
Scénario: Affichage de la progression globale de l'audio-guide
|
||||
Étant donné un utilisateur "jack@roadwave.fr" en mode piéton
|
||||
Et il a écouté 5 séquences sur 8
|
||||
Quand il consulte l'écran principal
|
||||
Alors il voit la progression:
|
||||
| Élément | Contenu |
|
||||
| Barre de progression | 5/8 (62%) |
|
||||
| Séquences restantes | 3 points à découvrir |
|
||||
| Temps écouté | 42 minutes |
|
||||
| Distance parcourue | 2.8 km |
|
||||
| Badge de complétion | Bronze (50%+) |
|
||||
Et un bouton "Terminer l'audio-guide" est disponible si toutes les séquences sont écoutées
|
||||
|
||||
Scénario: Découverte d'un point bonus caché (Easter egg)
|
||||
Étant donné un audio-guide avec un point bonus secret non listé
|
||||
Et un utilisateur "kate@roadwave.fr" en mode piéton
|
||||
Quand elle passe à proximité du point bonus caché (48.8470, 2.3450)
|
||||
Alors une notification s'affiche: "🎉 Point bonus découvert !"
|
||||
Et l'audio du point bonus démarre automatiquement
|
||||
Et un badge "Explorateur" est débloqué
|
||||
Et un événement "BONUS_POI_DISCOVERED" est enregistré
|
||||
Et la métrique "audio_guide.bonus.discovered" est incrémentée
|
||||
|
||||
Scénario: Notifications de proximité pour les points à venir
|
||||
Étant donné un utilisateur "luke@roadwave.fr" en mode piéton
|
||||
Et il marche vers la Sainte-Chapelle
|
||||
Quand il est à 150m de la Sainte-Chapelle
|
||||
Alors une notification discrète s'affiche: "Sainte-Chapelle à 150m"
|
||||
Et un son subtil de notification est joué
|
||||
Quand il est à 50m
|
||||
Alors une notification plus visible s'affiche: "Arrivée imminente - Sainte-Chapelle"
|
||||
Et un événement "POI_PROXIMITY_NOTIFICATION" est enregistré
|
||||
Et la métrique "audio_guide.proximity.notified" est incrémentée
|
||||
|
||||
Scénario: Mode hors ligne avec téléchargement préalable
|
||||
Étant donné un utilisateur "mary@roadwave.fr" en mode piéton
|
||||
Et elle a téléchargé l'audio-guide "Visite du Quartier Latin" avant de partir
|
||||
Quand elle active le mode avion (hors connexion)
|
||||
Alors la carte s'affiche en mode hors ligne (tiles pré-téléchargées)
|
||||
Et tous les audios sont disponibles en local
|
||||
Et la localisation GPS fonctionne normalement
|
||||
Et les séquences se déclenchent automatiquement hors ligne
|
||||
Et un événement "AUDIO_GUIDE_OFFLINE_MODE" est enregistré
|
||||
Et la métrique "audio_guide.offline.used" est incrémentée
|
||||
|
||||
Scénario: Partage de la progression avec des amis
|
||||
Étant donné un utilisateur "nathan@roadwave.fr" en mode piéton
|
||||
Et il a écouté 4 séquences sur 8
|
||||
Quand il clique sur "Partager ma progression"
|
||||
Alors un écran de partage s'ouvre avec:
|
||||
| Élément | Contenu |
|
||||
| Message | Je suis en train de découvrir le Quartier Latin !|
|
||||
| Progression | 4/8 points visités (50%) |
|
||||
| Carte visuelle | Capture d'écran de la carte avec progression |
|
||||
| Lien | https://roadwave.fr/share/abc123 |
|
||||
Et le partage peut être envoyé via:
|
||||
| Canal | Disponible |
|
||||
| SMS | Oui |
|
||||
| WhatsApp | Oui |
|
||||
| Facebook | Oui |
|
||||
| Twitter/X | Oui |
|
||||
Et un événement "AUDIO_GUIDE_PROGRESS_SHARED" est enregistré
|
||||
Et la métrique "audio_guide.shared" est incrémentée
|
||||
|
||||
Scénario: Métriques de performance de la navigation libre
|
||||
Étant donné que 10 000 utilisateurs ont terminé l'audio-guide en mode navigation libre
|
||||
Quand les métriques d'usage sont collectées
|
||||
Alors les indicateurs suivants sont disponibles:
|
||||
| Métrique | Valeur moyenne |
|
||||
| Taux de complétion | 78% |
|
||||
| Nombre de séquences écoutées | 6.5/8 |
|
||||
| Temps moyen de visite | 2h 15min |
|
||||
| Distance moyenne parcourue | 3.2 km |
|
||||
| Pourcentage d'ordre non-suggéré | 42% |
|
||||
| Nombre de réécoutes par séquence | 0.8 |
|
||||
Et les métriques sont exportées vers le système de monitoring
|
||||
@@ -0,0 +1,221 @@
|
||||
# language: fr
|
||||
|
||||
@api @audio-guides @advertising @pedestrian @mvp
|
||||
Fonctionnalité: Auto-play publicités en mode piéton uniquement
|
||||
|
||||
En tant qu'utilisateur piéton
|
||||
Je peux recevoir des publicités audio en auto-play à proximité de commerces
|
||||
Afin que les commerçants puissent promouvoir leurs offres de manière contextualisée
|
||||
|
||||
Contexte:
|
||||
Étant donné que le système de publicité respecte les règles suivantes:
|
||||
| Règle | Valeur |
|
||||
| Auto-play autorisé uniquement en mode | Piéton |
|
||||
| Durée max d'une publicité | 30 secondes |
|
||||
| Fréquence max par commerce | 1 par jour |
|
||||
| Distance min entre 2 pubs différentes | 200 mètres |
|
||||
| Nombre max de pubs par heure | 3 |
|
||||
| Possibilité de skip après | 5 secondes |
|
||||
|
||||
Scénario: Déclenchement automatique d'une publicité en mode piéton
|
||||
Étant donné un utilisateur "alice@roadwave.fr" en mode piéton
|
||||
Et elle marche dans la rue avec l'application active
|
||||
Quand elle passe à 30 mètres du café "Le Parisien" avec publicité active
|
||||
Alors la publicité audio "Café Le Parisien - 10% de réduction" démarre automatiquement
|
||||
Et une notification visuelle s'affiche:
|
||||
| Élément | Contenu |
|
||||
| Icône | Logo du café |
|
||||
| Titre | Publicité - Le Parisien |
|
||||
| Distance | À 30m de vous |
|
||||
| Action | [Passer] disponible après 5s |
|
||||
| Durée | 0:25 |
|
||||
Et l'audio en cours (si existant) est mis en pause
|
||||
Et un événement "AD_AUTOPLAY_TRIGGERED" est enregistré
|
||||
Et la métrique "ads.autoplay.triggered" est incrémentée
|
||||
|
||||
Scénario: Aucun auto-play en mode voiture
|
||||
Étant donné un utilisateur "bob@roadwave.fr" en mode voiture
|
||||
Et il roule à 40 km/h avec l'application active
|
||||
Quand il passe à 30 mètres d'un commerce avec publicité active
|
||||
Alors aucune publicité n'est déclenchée automatiquement
|
||||
Et la publicité peut être affichée dans la liste "Publicités à proximité"
|
||||
Et l'utilisateur peut choisir manuellement de l'écouter
|
||||
Et un événement "AD_SKIPPED_CAR_MODE" est enregistré
|
||||
Et la métrique "ads.skipped.car_mode" est incrémentée
|
||||
|
||||
Scénario: Aucun auto-play en mode vélo
|
||||
Étant donné un utilisateur "charlie@roadwave.fr" en mode vélo
|
||||
Et il roule à 15 km/h avec l'application active
|
||||
Quand il passe à 30 mètres d'un commerce avec publicité active
|
||||
Alors aucune publicité n'est déclenchée automatiquement
|
||||
Et la sécurité de l'utilisateur à vélo est préservée
|
||||
Et un événement "AD_SKIPPED_CYCLING_MODE" est enregistré
|
||||
Et la métrique "ads.skipped.cycling_mode" est incrémentée
|
||||
|
||||
Scénario: Skip d'une publicité après 5 secondes
|
||||
Étant donné un utilisateur "david@roadwave.fr" en mode piéton
|
||||
Et une publicité a démarré automatiquement il y a 6 secondes
|
||||
Quand l'utilisateur clique sur le bouton "Passer"
|
||||
Alors la publicité s'arrête immédiatement
|
||||
Et l'audio en cours précédent reprend (si existant)
|
||||
Et un événement "AD_SKIPPED_BY_USER" est enregistré avec temps_ecoute: 6s
|
||||
Et la métrique "ads.skipped.by_user" est incrémentée
|
||||
Et le commerçant est facturé pour 6 secondes d'écoute seulement
|
||||
|
||||
Scénario: Bouton "Passer" désactivé pendant les 5 premières secondes
|
||||
Étant donné un utilisateur "eve@roadwave.fr" en mode piéton
|
||||
Et une publicité vient de démarrer
|
||||
Quand l'utilisateur clique sur le bouton "Passer" à T+2 secondes
|
||||
Alors le bouton est grisé et non cliquable
|
||||
Et un message s'affiche: "Disponible dans 3 secondes"
|
||||
Et un compteur à rebours est visible: 3... 2... 1...
|
||||
Alors à T+5 secondes, le bouton devient actif
|
||||
Et un événement "AD_SKIP_ATTEMPTED_TOO_EARLY" est enregistré
|
||||
Et la métrique "ads.skip.too_early" est incrémentée
|
||||
|
||||
Scénario: Publicité écoutée en entier
|
||||
Étant donné un utilisateur "frank@roadwave.fr" en mode piéton
|
||||
Et une publicité de 25 secondes a démarré automatiquement
|
||||
Quand l'utilisateur écoute la publicité jusqu'à la fin sans cliquer sur "Passer"
|
||||
Alors la publicité se termine naturellement
|
||||
Et l'audio en cours précédent reprend automatiquement
|
||||
Et un événement "AD_COMPLETED" est enregistré avec temps_ecoute: 25s
|
||||
Et la métrique "ads.completed" est incrémentée
|
||||
Et le commerçant est facturé pour la publicité complète (tarif plein)
|
||||
|
||||
Scénario: Limitation à 3 publicités par heure
|
||||
Étant donné un utilisateur "grace@roadwave.fr" en mode piéton
|
||||
Et elle a déjà écouté 3 publicités dans la dernière heure:
|
||||
| Commerce | Temps écoulé |
|
||||
| Café Le Parisien | Il y a 10min |
|
||||
| Boulangerie Paul | Il y a 30min |
|
||||
| Restaurant Tokyo | Il y a 50min |
|
||||
Quand elle passe à 30 mètres d'un 4ème commerce avec publicité
|
||||
Alors aucune publicité n'est déclenchée automatiquement
|
||||
Et un compteur s'affiche discrètement: "Prochaine pub disponible dans 10 min"
|
||||
Et un événement "AD_RATE_LIMITED" est enregistré
|
||||
Et la métrique "ads.rate_limited" est incrémentée
|
||||
|
||||
Scénario: Limitation à 1 publicité par commerce par jour
|
||||
Étant donné un utilisateur "henry@roadwave.fr" en mode piéton
|
||||
Et il a déjà écouté la publicité du "Café Le Parisien" ce matin à 10h
|
||||
Quand il repasse devant le même café à 16h
|
||||
Alors aucune publicité n'est déclenchée automatiquement
|
||||
Et le café n'apparaît pas dans la liste "Publicités à proximité"
|
||||
Et un événement "AD_ALREADY_SHOWN_TODAY" est enregistré
|
||||
Et la métrique "ads.deduplication.same_day" est incrémentée
|
||||
|
||||
Scénario: Distance minimale de 200m entre 2 publicités différentes
|
||||
Étant donné un utilisateur "iris@roadwave.fr" en mode piéton
|
||||
Et elle vient d'écouter une publicité du "Café Le Parisien" il y a 1 minute
|
||||
Quand elle marche et passe à 50 mètres de la "Boulangerie Paul" (150m du café)
|
||||
Alors aucune publicité n'est déclenchée automatiquement
|
||||
Et un événement "AD_TOO_CLOSE_TO_PREVIOUS" est enregistré
|
||||
Et la métrique "ads.skipped.too_close" est incrémentée
|
||||
Quand elle continue et passe à 250 mètres de la "Librairie Gibert" (250m du café)
|
||||
Alors la publicité de la librairie peut être déclenchée
|
||||
|
||||
Scénario: Désactivation complète des publicités (utilisateur Premium)
|
||||
Étant donné un utilisateur "jack@roadwave.fr" Premium en mode piéton
|
||||
Et il a désactivé les publicités dans ses paramètres
|
||||
Quand il passe à 30 mètres de commerces avec publicités actives
|
||||
Alors aucune publicité n'est jamais déclenchée
|
||||
Et aucune publicité n'apparaît dans la liste "Publicités à proximité"
|
||||
Et un événement "AD_BLOCKED_PREMIUM" est enregistré
|
||||
Et la métrique "ads.blocked.premium" est incrémentée
|
||||
|
||||
Scénario: Mise en pause de l'audio en cours lors du déclenchement d'une pub
|
||||
Étant donné un utilisateur "kate@roadwave.fr" en mode piéton
|
||||
Et elle écoute un podcast "Histoire de Paris" à la position 12min 30s
|
||||
Quand une publicité se déclenche automatiquement
|
||||
Alors le podcast est mis en pause immédiatement
|
||||
Et la position de lecture est sauvegardée: 12min 30s
|
||||
Et la publicité démarre
|
||||
Quand la publicité se termine (skip ou écoute complète)
|
||||
Alors le podcast reprend automatiquement à la position 12min 30s
|
||||
Et un événement "AD_CONTENT_PAUSED_RESUMED" est enregistré
|
||||
Et la métrique "ads.content.paused_resumed" est incrémentée
|
||||
|
||||
Scénario: Ciblage géographique précis de la publicité
|
||||
Étant donné un commerçant "Le Parisien" avec publicité active
|
||||
Et il a configuré un rayon de déclenchement de 50 mètres
|
||||
Et un utilisateur "luke@roadwave.fr" en mode piéton
|
||||
Quand l'utilisateur est à 60 mètres du commerce
|
||||
Alors aucune publicité n'est déclenchée
|
||||
Quand l'utilisateur marche et arrive à 45 mètres du commerce
|
||||
Alors la publicité se déclenche automatiquement
|
||||
Et un événement "AD_GEO_TRIGGERED" est enregistré avec distance: 45m
|
||||
Et la métrique "ads.geo.triggered" est incrémentée
|
||||
|
||||
Scénario: Publicité contextuelle basée sur les intérêts de l'utilisateur
|
||||
Étant donné un utilisateur "mary@roadwave.fr" en mode piéton
|
||||
Et ses jauges d'intérêts sont:
|
||||
| Catégorie | Niveau |
|
||||
| Gastronomie | 85% |
|
||||
| Culture | 60% |
|
||||
| Sport | 20% |
|
||||
Et deux commerces ont des publicités actives à proximité:
|
||||
| Commerce | Catégorie | Distance |
|
||||
| Restaurant Le Gourmet | Gastronomie | 40m |
|
||||
| Salle de sport FitClub| Sport | 35m |
|
||||
Quand l'utilisateur passe à proximité des deux commerces
|
||||
Alors la publicité du restaurant est priorisée et déclenchée
|
||||
Et la publicité de la salle de sport est ignorée (faible intérêt)
|
||||
Et un événement "AD_INTEREST_MATCHED" est enregistré avec categorie: "gastronomie", score: 85
|
||||
Et la métrique "ads.interest_matching.applied" est incrémentée
|
||||
|
||||
Scénario: Affichage d'informations complémentaires pendant la publicité
|
||||
Étant donné un utilisateur "nathan@roadwave.fr" en mode piéton
|
||||
Et une publicité du "Café Le Parisien" est en cours de lecture
|
||||
Quand l'utilisateur consulte l'écran
|
||||
Alors il voit les informations suivantes:
|
||||
| Élément | Contenu |
|
||||
| Logo du commerce | [Image] |
|
||||
| Nom du commerce | Café Le Parisien |
|
||||
| Type d'établissement | Café-Brasserie |
|
||||
| Distance | À 30m de vous |
|
||||
| Itinéraire | [Bouton "Y aller"] |
|
||||
| Offre spéciale | 10% de réduction avec ce code: ROADWAVE10|
|
||||
| Horaires | Ouvert maintenant - Ferme à 22h |
|
||||
| Note | 4.5/5 (230 avis) |
|
||||
Et l'utilisateur peut cliquer sur "Y aller" pour lancer la navigation
|
||||
Et un événement "AD_INFO_DISPLAYED" est enregistré
|
||||
|
||||
Scénario: Tracking de la conversion (visite effective du commerce)
|
||||
Étant donné un utilisateur "olive@roadwave.fr" en mode piéton
|
||||
Et elle a écouté la publicité du "Café Le Parisien" il y a 5 minutes
|
||||
Quand elle clique sur "Y aller" et se rend au café
|
||||
Et entre dans un rayon de 10 mètres du café
|
||||
Alors un événement "AD_CONVERSION_VISIT" est enregistré
|
||||
Et la métrique "ads.conversions.visits" est incrémentée
|
||||
Et le commerçant voit cette conversion dans ses statistiques
|
||||
Et une notification discrète s'affiche: "Profitez de votre réduction avec le code ROADWAVE10"
|
||||
|
||||
Scénario: Métriques de performance des publicités pour les commerçants
|
||||
Étant donné un commerçant "Le Parisien" avec publicité active depuis 7 jours
|
||||
Quand le commerçant consulte ses statistiques
|
||||
Alors il voit les métriques suivantes:
|
||||
| Métrique | Valeur |
|
||||
| Nombre d'impressions (déclenchements)| 450 |
|
||||
| Taux d'écoute complète | 35% |
|
||||
| Taux de skip moyen | 65% |
|
||||
| Durée moyenne d'écoute | 12s |
|
||||
| Nombre de clics "Y aller" | 25 |
|
||||
| Nombre de visites confirmées | 18 |
|
||||
| Taux de conversion | 4% |
|
||||
| Coût total | 45€ |
|
||||
| Coût par visite | 2.50€ |
|
||||
Et les métriques sont mises à jour en temps réel
|
||||
|
||||
Scénario: A/B testing des publicités pour optimisation
|
||||
Étant donné un commerçant "Le Parisien" avec 2 versions de publicité:
|
||||
| Version | Description | Durée |
|
||||
| A | Voix masculine, tonalité formelle | 25s |
|
||||
| B | Voix féminine, tonalité décontractée | 25s |
|
||||
Quand le système diffuse aléatoirement les 2 versions (50/50)
|
||||
Alors les métriques sont collectées séparément:
|
||||
| Métrique | Version A | Version B |
|
||||
| Taux d'écoute complète | 32% | 42% |
|
||||
| Taux de conversion | 3.5% | 5.2% |
|
||||
Et le commerçant peut choisir de diffuser uniquement la version B
|
||||
Et un événement "AD_AB_TEST_COMPLETED" est enregistré
|
||||
@@ -110,24 +110,24 @@ Fonctionnalité: Audio-guides Premium et monétisation
|
||||
Scénario: Comparaison gratuit vs Premium
|
||||
Étant donné qu'un créateur a publié 2 audio-guides:
|
||||
| titre | type | ecoutes_mois | revenus |
|
||||
| Tour de Paris | Gratuit | 1200 | 12.50 € |
|
||||
| Tour de Paris | Gratuit | 1200 | 3.60 € |
|
||||
| Visite VIP Versailles| Premium | 142 | 45.20 € |
|
||||
Quand il consulte son dashboard
|
||||
Alors il peut comparer les performances
|
||||
Et constater que Premium génère plus de revenus par écoute
|
||||
|
||||
Scénario: Seuil minimum de paiement (20€)
|
||||
Étant donné qu'un créateur a généré 18€ de revenus ce mois
|
||||
Scénario: Seuil minimum de paiement (50€)
|
||||
Étant donné qu'un créateur a généré 42€ de revenus ce mois
|
||||
Quand le paiement mensuel est traité
|
||||
Alors le montant est reporté au mois suivant
|
||||
Et un message s'affiche: "Seuil minimum non atteint (20€). Montant reporté."
|
||||
Et un message s'affiche: "Seuil minimum non atteint (50€). Montant reporté."
|
||||
|
||||
Scénario: Paiement automatique mensuel
|
||||
Étant donné qu'un créateur a généré 138.50€ de revenus en janvier
|
||||
Quand le 5 février arrive
|
||||
Quand le 15 février arrive
|
||||
Alors le paiement est initié automatiquement via Mangopay
|
||||
Et le créateur reçoit une notification: "Paiement de 138.50€ en cours"
|
||||
Et les fonds arrivent sous 2-3 jours ouvrés
|
||||
Et les fonds arrivent sous 1-3 jours ouvrés (SEPA)
|
||||
|
||||
# 16.11 - Publicités dans audio-guides gratuits
|
||||
|
||||
@@ -165,12 +165,12 @@ Fonctionnalité: Audio-guides Premium et monétisation
|
||||
| Catégorie | Tourisme, Culture |
|
||||
| Langue | Français |
|
||||
|
||||
Scénario: Comptabilisation des impressions pub pour créateur
|
||||
Scénario: Comptabilisation revenus pub pour créateur
|
||||
Étant donné qu'un audio-guide gratuit génère 200 écoutes complètes
|
||||
Et que chaque écoute complète = 2 publicités (séq. 5 et 10)
|
||||
Quand les revenus pub sont calculés
|
||||
Alors 400 impressions sont comptabilisées
|
||||
Et le créateur reçoit 0.80€ (400 × 0.002€)
|
||||
Alors 400 impressions pub sont diffusées
|
||||
Et le créateur reçoit 0.60€ (200 écoutes × 0.003€)
|
||||
|
||||
# 16.12 - Stratégies de conversion
|
||||
|
||||
@@ -212,43 +212,6 @@ Fonctionnalité: Audio-guides Premium et monétisation
|
||||
| creator_id | guide_versailles_456 |
|
||||
Et le créateur bénéficie d'un bonus de conversion
|
||||
|
||||
# 16.13 - Offres spéciales
|
||||
|
||||
Scénario: Essai gratuit 7 jours Premium via audio-guide
|
||||
Étant donné qu'un utilisateur gratuit atteint le paywall d'un audio-guide Premium
|
||||
Et qu'il n'a jamais essayé Premium
|
||||
Quand l'overlay s'affiche
|
||||
Alors une offre d'essai est proposée:
|
||||
"""
|
||||
👑 Essayez Premium gratuitement pendant 7 jours
|
||||
|
||||
✓ Accès complet à cet audio-guide
|
||||
✓ Tous les contenus Premium débloqués
|
||||
✓ Sans engagement, annulable à tout moment
|
||||
|
||||
[Démarrer l'essai gratuit] [Plus tard]
|
||||
"""
|
||||
|
||||
Scénario: Activation immédiate après essai gratuit
|
||||
Étant donné qu'un utilisateur démarre un essai gratuit 7 jours
|
||||
Quand l'essai est activé
|
||||
Alors l'audio-guide Premium démarre immédiatement
|
||||
Et toutes les séquences sont débloquées
|
||||
Et aucune publicité n'est insérée
|
||||
|
||||
Scénario: Rappel 2 jours avant fin d'essai
|
||||
Étant donné qu'un utilisateur a démarré un essai gratuit le 15/01
|
||||
Quand le 20/01 arrive (J-2)
|
||||
Alors une notification est envoyée:
|
||||
"""
|
||||
⏰ Votre essai Premium se termine dans 2 jours
|
||||
|
||||
Continuez à profiter de tous les audio-guides Premium
|
||||
pour seulement 4.99€/mois
|
||||
|
||||
[Rester Premium] [Gérer abonnement]
|
||||
"""
|
||||
|
||||
# Cas d'usage
|
||||
|
||||
Scénario: Créateur mix gratuit + Premium
|
||||
@@ -0,0 +1,394 @@
|
||||
# language: fr
|
||||
|
||||
Fonctionnalité: API - Progression et synchronisation audio-guides
|
||||
En tant que système backend
|
||||
Je veux sauvegarder et synchroniser la progression des audio-guides
|
||||
Afin de permettre une reprise fluide et multi-device
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est démarrée
|
||||
Et que l'utilisateur "user@example.com" est authentifié
|
||||
|
||||
# 16.6.1 - Sauvegarde progression
|
||||
|
||||
Scénario: POST /api/v1/audio-guides/{id}/progress - Sauvegarde progression
|
||||
Étant donné un audio-guide "ag_123" en cours d'écoute
|
||||
Et que l'utilisateur est à la séquence 3 position 1:42
|
||||
Quand je fais un POST sur "/api/v1/audio-guides/ag_123/progress":
|
||||
"""json
|
||||
{
|
||||
"current_sequence_id": "seq_3",
|
||||
"current_position_seconds": 102,
|
||||
"completed_sequences": ["seq_1", "seq_2"],
|
||||
"gps_position": {
|
||||
"latitude": 43.1234,
|
||||
"longitude": 2.5678
|
||||
}
|
||||
}
|
||||
"""
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et la progression est sauvegardée en PostgreSQL
|
||||
Et le timestamp last_played_at est mis à jour
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"saved": true,
|
||||
"synced_to_cloud": true,
|
||||
"updated_at": "2026-01-22T14:35:42Z"
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: Sauvegarde automatique toutes les 30 secondes (client)
|
||||
Étant donné que le client mobile envoie la progression toutes les 30s
|
||||
Quand je fais un POST sur "/api/v1/audio-guides/{id}/progress"
|
||||
Alors la progression précédente est écrasée
|
||||
Et le timestamp est mis à jour
|
||||
Et la réponse est retournée en < 100ms
|
||||
|
||||
Scénario: Sauvegarde des séquences complétées (>80%)
|
||||
Étant donné qu'une séquence de durée 180 secondes
|
||||
Et que l'utilisateur a écouté 150 secondes (83%)
|
||||
Quand la progression est sauvegardée
|
||||
Alors la séquence est ajoutée à completed_sequences
|
||||
Et le completion_rate est enregistré à 83%
|
||||
|
||||
Scénario: Séquence non marquée complétée si <80%
|
||||
Étant donné qu'une séquence de durée 222 secondes
|
||||
Et que l'utilisateur a écouté 90 secondes (40%)
|
||||
Quand la progression est sauvegardée
|
||||
Alors la séquence n'est PAS ajoutée à completed_sequences
|
||||
Et le current_position est sauvegardé (pour reprise)
|
||||
|
||||
Scénario: Structure de données progression en PostgreSQL
|
||||
Étant donné une sauvegarde de progression
|
||||
Alors la table audio_guide_progress contient:
|
||||
| champ | type | description |
|
||||
| id | UUID | ID progression |
|
||||
| user_id | UUID | ID utilisateur |
|
||||
| audio_guide_id | UUID | ID audio-guide |
|
||||
| current_sequence_id | UUID | Séquence en cours |
|
||||
| current_position | INTEGER | Position en secondes |
|
||||
| completed_sequences | UUID[] | Tableau séquences complétées |
|
||||
| last_played_at | TIMESTAMP | Dernière écoute |
|
||||
| gps_position | GEOGRAPHY | Position GPS optionnelle |
|
||||
| created_at | TIMESTAMP | Création |
|
||||
| updated_at | TIMESTAMP | Dernière MAJ |
|
||||
|
||||
# 16.6.2 - Reprise progression
|
||||
|
||||
Scénario: GET /api/v1/audio-guides/{id}/progress - Récupération progression
|
||||
Étant donné une progression sauvegardée pour "ag_123"
|
||||
Quand je fais un GET sur "/api/v1/audio-guides/ag_123/progress"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"has_progress": true,
|
||||
"current_sequence_id": "seq_3",
|
||||
"current_position_seconds": 102,
|
||||
"completed_sequences": ["seq_1", "seq_2"],
|
||||
"completion_percentage": 25,
|
||||
"last_played_at": "2026-01-20T14:35:42Z",
|
||||
"can_resume": true
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: Calcul completion_percentage
|
||||
Étant donné un audio-guide de 12 séquences
|
||||
Et que l'utilisateur a complété 3 séquences
|
||||
Quand le pourcentage de complétion est calculé
|
||||
Alors completion_percentage est 25% (3/12)
|
||||
|
||||
Scénario: Progression inexistante (première écoute)
|
||||
Étant donné qu'aucune progression n'existe pour "ag_456"
|
||||
Quand je fais un GET sur "/api/v1/audio-guides/ag_456/progress"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"has_progress": false,
|
||||
"can_resume": false
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: Progression expirée (>30 jours)
|
||||
Étant donné une progression avec last_played_at à 35 jours
|
||||
Quand je fais un GET sur "/api/v1/audio-guides/ag_123/progress"
|
||||
Alors can_resume est false
|
||||
Et le message "Progression expirée après 30 jours" est retourné
|
||||
Et les données sont conservées mais marquées "expired"
|
||||
|
||||
Scénario: DELETE /api/v1/audio-guides/{id}/progress - Réinitialisation
|
||||
Étant donné une progression existante
|
||||
Quand je fais un DELETE sur "/api/v1/audio-guides/ag_123/progress"
|
||||
Alors le code HTTP de réponse est 204
|
||||
Et la progression est supprimée
|
||||
Et l'utilisateur peut recommencer depuis le début
|
||||
|
||||
# 16.6.3 - Multi-device et synchronisation
|
||||
|
||||
Scénario: Synchronisation cloud automatique
|
||||
Étant donné qu'une progression est sauvegardée sur iPhone
|
||||
Quand l'utilisateur ouvre l'app sur iPad
|
||||
Et fait un GET sur "/api/v1/audio-guides/ag_123/progress"
|
||||
Alors la progression iPhone est récupérée
|
||||
Et l'utilisateur peut reprendre exactement où il était
|
||||
|
||||
Scénario: Conflit de synchronisation (dernier timestamp gagne)
|
||||
Étant donné une progression sur iPhone avec timestamp "2026-01-22T14:00:00Z"
|
||||
Et une progression sur iPad avec timestamp "2026-01-22T14:05:00Z"
|
||||
Quand les deux appareils synchronisent
|
||||
Alors la progression avec timestamp le plus récent (iPad) est conservée
|
||||
Et la progression iPhone est écrasée
|
||||
|
||||
Scénario: GET /api/v1/audio-guides/progress/sync - Synchronisation batch
|
||||
Étant donné que l'utilisateur a 5 progressions locales non synchronisées
|
||||
Quand je fais un POST sur "/api/v1/audio-guides/progress/sync":
|
||||
"""json
|
||||
{
|
||||
"progressions": [
|
||||
{
|
||||
"audio_guide_id": "ag_1",
|
||||
"current_sequence_id": "seq_3",
|
||||
"current_position_seconds": 102,
|
||||
"updated_at": "2026-01-22T14:00:00Z"
|
||||
},
|
||||
{
|
||||
"audio_guide_id": "ag_2",
|
||||
"current_sequence_id": "seq_5",
|
||||
"current_position_seconds": 45,
|
||||
"updated_at": "2026-01-22T14:10:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et toutes les progressions sont synchronisées
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"synced_count": 2,
|
||||
"conflicts": 0
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: Résolution conflit avec notification
|
||||
Étant donné une progression locale sur iPhone avec timestamp ancien
|
||||
Et une progression cloud plus récente (depuis iPad)
|
||||
Quand le sync est effectué
|
||||
Alors la progression cloud est conservée
|
||||
Et un conflit est signalé dans la réponse:
|
||||
"""json
|
||||
{
|
||||
"synced_count": 1,
|
||||
"conflicts": 1,
|
||||
"conflict_details": [
|
||||
{
|
||||
"audio_guide_id": "ag_123",
|
||||
"cloud_timestamp": "2026-01-22T15:00:00Z",
|
||||
"local_timestamp": "2026-01-22T14:30:00Z",
|
||||
"resolution": "cloud_wins"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
# Historique et statistiques
|
||||
|
||||
Scénario: GET /api/v1/audio-guides/progress/history - Historique écoutes
|
||||
Étant donné que l'utilisateur a écouté 3 séquences d'un audio-guide
|
||||
Quand je fais un GET sur "/api/v1/audio-guides/ag_123/progress/history"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient l'historique:
|
||||
"""json
|
||||
{
|
||||
"listening_sessions": [
|
||||
{
|
||||
"sequence_id": "seq_1",
|
||||
"started_at": "2026-01-22T14:10:00Z",
|
||||
"completed_at": "2026-01-22T14:12:15Z",
|
||||
"completion_rate": 100,
|
||||
"duration_listened": 135
|
||||
},
|
||||
{
|
||||
"sequence_id": "seq_2",
|
||||
"started_at": "2026-01-22T14:12:20Z",
|
||||
"completed_at": "2026-01-22T14:14:08Z",
|
||||
"completion_rate": 100,
|
||||
"duration_listened": 108
|
||||
},
|
||||
{
|
||||
"sequence_id": "seq_3",
|
||||
"started_at": "2026-01-22T14:14:15Z",
|
||||
"completed_at": "2026-01-22T14:17:45Z",
|
||||
"completion_rate": 92,
|
||||
"duration_listened": 204
|
||||
}
|
||||
],
|
||||
"total_time_spent": 447
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: Détection complétion 100% de l'audio-guide
|
||||
Étant donné un audio-guide de 12 séquences
|
||||
Et que l'utilisateur complète la 12ème et dernière séquence à >80%
|
||||
Quand la progression est sauvegardée
|
||||
Alors is_completed passe à true
|
||||
Et completed_at est mis à jour avec le timestamp actuel
|
||||
Et un événement "audio_guide_completed" est émis
|
||||
|
||||
Scénario: GET /api/v1/users/me/audio-guides/completed - Liste des complétés
|
||||
Étant donné que l'utilisateur a complété 2 audio-guides
|
||||
Quand je fais un GET sur "/api/v1/users/me/audio-guides/completed"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"completed_count": 2,
|
||||
"audio_guides": [
|
||||
{
|
||||
"audio_guide_id": "ag_1",
|
||||
"title": "Tour de Paris",
|
||||
"completed_at": "2026-01-15T10:00:00Z",
|
||||
"total_sequences": 10,
|
||||
"total_duration": 3600
|
||||
},
|
||||
{
|
||||
"audio_guide_id": "ag_2",
|
||||
"title": "Découverte de Lyon",
|
||||
"completed_at": "2026-01-20T14:00:00Z",
|
||||
"total_sequences": 8,
|
||||
"total_duration": 2700
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: GET /api/v1/users/me/audio-guides/in-progress - Liste en cours
|
||||
Étant donné que l'utilisateur a 3 audio-guides en cours
|
||||
Quand je fais un GET sur "/api/v1/users/me/audio-guides/in-progress"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"in_progress_count": 3,
|
||||
"audio_guides": [
|
||||
{
|
||||
"audio_guide_id": "ag_123",
|
||||
"title": "Visite du Louvre",
|
||||
"current_sequence": 6,
|
||||
"total_sequences": 12,
|
||||
"completion_percentage": 50,
|
||||
"last_played_at": "2026-01-22T14:35:42Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
# Badges et achievements
|
||||
|
||||
Scénario: Attribution badge "Premier audio-guide"
|
||||
Étant donné qu'un utilisateur complète son 1er audio-guide
|
||||
Quand le système détecte la complétion
|
||||
Alors un badge "first_audio_guide" est attribué
|
||||
Et une notification est envoyée:
|
||||
"""json
|
||||
{
|
||||
"type": "badge_unlocked",
|
||||
"badge_id": "first_audio_guide",
|
||||
"title": "🎧 Premier audio-guide",
|
||||
"message": "Félicitations ! Vous avez complété votre premier audio-guide"
|
||||
}
|
||||
"""
|
||||
|
||||
Plan du Scénario: Attribution badges selon nombre complétés
|
||||
Étant donné qu'un utilisateur complète son <nombre>ème audio-guide
|
||||
Quand le système détecte la complétion
|
||||
Alors le badge "<badge_id>" est attribué
|
||||
|
||||
Exemples:
|
||||
| nombre | badge_id |
|
||||
| 1 | first_audio_guide |
|
||||
| 5 | explorer |
|
||||
| 10 | completist |
|
||||
| 25 | expert |
|
||||
| 50 | master |
|
||||
|
||||
# Nettoyage et archivage
|
||||
|
||||
Scénario: Archivage progressions inactives (>6 mois)
|
||||
Étant donné une progression avec last_played_at à 200 jours
|
||||
Quand le job de nettoyage automatique s'exécute
|
||||
Alors la progression est déplacée vers la table audio_guide_progress_archive
|
||||
Et elle reste récupérable via l'API pendant 30 jours supplémentaires
|
||||
Et après 7 mois total, elle est supprimée définitivement
|
||||
|
||||
Scénario: GET /api/v1/audio-guides/{id}/progress/archived - Récupération archivée
|
||||
Étant donné une progression archivée
|
||||
Quand je fais un GET sur "/api/v1/audio-guides/ag_123/progress/archived"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et la progression archivée est retournée
|
||||
Et un message indique: "Progression archivée. Vous pouvez la restaurer."
|
||||
|
||||
Scénario: POST /api/v1/audio-guides/{id}/progress/restore - Restauration
|
||||
Étant donné une progression archivée
|
||||
Quand je fais un POST sur "/api/v1/audio-guides/ag_123/progress/restore"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et la progression est déplacée de archive vers la table active
|
||||
Et l'utilisateur peut reprendre son écoute
|
||||
|
||||
# Cas d'erreur
|
||||
|
||||
Scénario: Sauvegarde progression audio-guide inexistant
|
||||
Quand je fais un POST sur "/api/v1/audio-guides/ag_nonexistant/progress"
|
||||
Alors le code HTTP de réponse est 404
|
||||
Et le message d'erreur est "Audio-guide non trouvé"
|
||||
|
||||
Scénario: Sauvegarde progression séquence invalide
|
||||
Étant donné un audio-guide "ag_123" avec 8 séquences
|
||||
Et une requête avec current_sequence_id "seq_99" (inexistant)
|
||||
Quand je fais un POST sur "/api/v1/audio-guides/ag_123/progress"
|
||||
Alors le code HTTP de réponse est 400
|
||||
Et le message d'erreur est "Séquence inexistante pour cet audio-guide"
|
||||
|
||||
Scénario: Récupération progression sans authentification
|
||||
Étant donné une requête sans token JWT
|
||||
Quand je fais un GET sur "/api/v1/audio-guides/ag_123/progress"
|
||||
Alors le code HTTP de réponse est 401
|
||||
Et le message d'erreur est "Authentification requise"
|
||||
|
||||
Scénario: Corruption de données progression (récupération)
|
||||
Étant donné une progression avec données corrompues (JSON invalide)
|
||||
Quand je fais un GET sur "/api/v1/audio-guides/ag_123/progress"
|
||||
Alors le système tente une récupération depuis le backup quotidien
|
||||
Et si récupération réussie, les données sont restaurées
|
||||
Et un log d'erreur est créé pour investigation
|
||||
|
||||
Scénario: Échec synchronisation cloud (mode dégradé)
|
||||
Étant donné que la base PostgreSQL est temporairement indisponible
|
||||
Quand je fais un POST sur "/api/v1/audio-guides/ag_123/progress"
|
||||
Alors le code HTTP de réponse est 503
|
||||
Et le message d'erreur est "Service temporairement indisponible. Réessayez dans quelques instants."
|
||||
Et le client doit conserver la progression localement et réessayer
|
||||
|
||||
# Performance et optimisations
|
||||
|
||||
Scénario: Index sur user_id + audio_guide_id pour requêtes rapides
|
||||
Étant donné un index composite (user_id, audio_guide_id)
|
||||
Quand je fais un GET sur "/api/v1/audio-guides/ag_123/progress"
|
||||
Alors la requête PostgreSQL utilise l'index
|
||||
Et le temps de réponse est < 20ms
|
||||
|
||||
Scénario: Cache Redis pour progressions actives
|
||||
Étant donné qu'une progression est fréquemment mise à jour (toutes les 30s)
|
||||
Quand la progression est sauvegardée
|
||||
Alors elle est également cachée dans Redis avec TTL 1h
|
||||
Et les GET suivants lisent depuis Redis (pas PostgreSQL)
|
||||
Et la latence est < 10ms
|
||||
|
||||
Scénario: Invalidation cache Redis lors de réinitialisation
|
||||
Étant donné qu'une progression est en cache Redis
|
||||
Quand je fais un DELETE sur "/api/v1/audio-guides/ag_123/progress"
|
||||
Alors l'entrée cache Redis est supprimée
|
||||
Et la base PostgreSQL est mise à jour
|
||||
Et la cohérence est garantie
|
||||
@@ -0,0 +1,111 @@
|
||||
# language: fr
|
||||
|
||||
@api @audio-guides @advertising @mvp
|
||||
Fonctionnalité: Système de publicités complet
|
||||
|
||||
En tant que plateforme
|
||||
Je veux gérer un système publicitaire équilibré et non intrusif
|
||||
Afin de monétiser la plateforme tout en préservant l'expérience utilisateur
|
||||
|
||||
Contexte:
|
||||
Étant donné les règles publicitaires:
|
||||
| Règle | Valeur |
|
||||
| Durée max publicité | 30s |
|
||||
| Fréquence max par heure | 3 |
|
||||
| Skip autorisé après | 5s |
|
||||
| Mode auto-play | Piéton only |
|
||||
| Premium sans pub | Oui |
|
||||
|
||||
Scénario: Insertion intelligente de publicité entre séquences
|
||||
Étant donné un utilisateur "alice@roadwave.fr" Free en mode piéton
|
||||
Quand elle termine l'écoute d'une séquence
|
||||
Et marche vers la suivante (temps de trajet: 5 min)
|
||||
Alors une publicité peut être insérée pendant le trajet
|
||||
Et elle ne coupe jamais une séquence en cours
|
||||
Et un événement "AD_INSERTED_BETWEEN_SEQUENCES" est enregistré
|
||||
|
||||
Scénario: Ciblage géographique et contextuel des publicités
|
||||
Étant donné un utilisateur "bob@roadwave.fr" près de restaurants
|
||||
Et ses intérêts incluent "Gastronomie" à 80%
|
||||
Quand une publicité doit être affichée
|
||||
Alors le système priorise les restaurants à proximité
|
||||
Et match les intérêts de l'utilisateur
|
||||
Et un événement "AD_TARGETED" est enregistré avec score_match: 95
|
||||
|
||||
Scénario: Format publicitaire audio + visuel
|
||||
Étant donné une publicité pour le "Café Le Parisien"
|
||||
Quand elle est diffusée
|
||||
Alors l'audio est joué (max 30s)
|
||||
Et une carte visuelle s'affiche avec:
|
||||
| Élément | Contenu |
|
||||
| Logo | Image du commerce |
|
||||
| Offre spéciale | -10% avec code ROAD10 |
|
||||
| Distance | À 50m |
|
||||
| Bouton CTA | [Y aller] [Sauvegarder]|
|
||||
Et un événement "AD_DISPLAYED_FULL" est enregistré
|
||||
|
||||
Scénario: Facturation au CPM et CPC pour annonceurs
|
||||
Étant donné un commerce "Le Parisien" avec budget pub
|
||||
Quand sa publicité est diffusée 1000 fois (impressions)
|
||||
Alors il est facturé selon le modèle CPM: 5€ pour 1000 impressions
|
||||
Quand 50 utilisateurs cliquent sur "Y aller"
|
||||
Alors il est facturé selon le CPC: 0.50€ par clic
|
||||
Et un événement "AD_BILLING_CALCULATED" est enregistré
|
||||
|
||||
Scénario: Dashboard annonceur avec statistiques détaillées
|
||||
Étant donné un annonceur "Restaurant Tokyo" connecté
|
||||
Quand il consulte son dashboard
|
||||
Alors il voit les métriques en temps réel:
|
||||
| Métrique | Valeur |
|
||||
| Impressions (7 jours) | 2 450 |
|
||||
| Taux d'écoute complète | 38% |
|
||||
| Clics "Y aller" | 125 |
|
||||
| Visites confirmées | 45 |
|
||||
| Taux de conversion | 1.8% |
|
||||
| Budget dépensé | 42.50€ |
|
||||
| Coût par visite | 0.94€ |
|
||||
Et un événement "AD_DASHBOARD_VIEWED" est enregistré
|
||||
|
||||
Scénario: A/B testing automatisé des créatives publicitaires
|
||||
Étant donné un annonceur avec 3 versions de publicité
|
||||
Quand le système diffuse les pubs
|
||||
Alors chaque version est diffusée à 33% du trafic
|
||||
Et les performances sont comparées après 1000 impressions
|
||||
Et la meilleure version est automatiquement privilégiée
|
||||
Et un événement "AD_AB_TEST_WINNER_SELECTED" est enregistré
|
||||
|
||||
Scénario: Limite de fréquence stricte pour éviter la saturation
|
||||
Étant donné un utilisateur "charlie@roadwave.fr"
|
||||
Et il a déjà entendu 3 pubs dans la dernière heure
|
||||
Quand le système tente d'insérer une 4ème pub
|
||||
Alors elle est bloquée
|
||||
Et l'utilisateur voit: "Prochaine pub dans 25 min"
|
||||
Et un événement "AD_FREQUENCY_CAP_REACHED" est enregistré
|
||||
|
||||
Scénario: Publicités Premium sponsorisées prioritaires
|
||||
Étant donné un annonceur "Musée du Louvre" avec campagne premium
|
||||
Quand un utilisateur passe à proximité
|
||||
Alors sa publicité est priorisée sur les autres
|
||||
Et elle a un format étendu (45s autorisées)
|
||||
Et un badge "Partenaire officiel" s'affiche
|
||||
Et un événement "AD_PREMIUM_DISPLAYED" est enregistré
|
||||
|
||||
Scénario: Sauvegarde d'offres publicitaires pour utilisation ultérieure
|
||||
Étant donné un utilisateur "david@roadwave.fr" qui entend une pub
|
||||
Quand il clique sur "Sauvegarder l'offre"
|
||||
Alors l'offre est ajoutée à "Mes offres sauvegardées"
|
||||
Et il peut la consulter plus tard
|
||||
Et la validité de l'offre est affichée: "Valable jusqu'au 31/03"
|
||||
Et un événement "AD_OFFER_SAVED" est enregistré
|
||||
|
||||
Scénario: Métriques de performance du système publicitaire
|
||||
Étant donné que 100 000 pubs ont été diffusées
|
||||
Alors les indicateurs suivants sont disponibles:
|
||||
| Métrique | Valeur |
|
||||
| Taux de skip moyen | 62% |
|
||||
| Taux d'écoute complète | 38% |
|
||||
| CTR (Click-Through Rate) | 5.2% |
|
||||
| Taux de conversion (visites) | 3.1% |
|
||||
| Revenu moyen par utilisateur | 2.40€/an |
|
||||
| Satisfaction utilisateurs | 3.8/5 |
|
||||
Et les métriques sont exportées vers le monitoring
|
||||
357
docs/domains/content/features/audio-guides/publicites.feature
Normal file
357
docs/domains/content/features/audio-guides/publicites.feature
Normal file
@@ -0,0 +1,357 @@
|
||||
# language: fr
|
||||
|
||||
Fonctionnalité: API - Publicités dans audio-guides
|
||||
En tant que système backend
|
||||
Je veux gérer l'insertion et la diffusion de publicités dans les audio-guides
|
||||
Afin de monétiser les contenus gratuits
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est démarrée
|
||||
Et que l'utilisateur "user@example.com" est authentifié (gratuit)
|
||||
|
||||
# 16.5.1 - Insertion publicité
|
||||
|
||||
Scénario: Calcul insertion publicité (1 pub toutes les 5 séquences)
|
||||
Étant donné un audio-guide gratuit avec 12 séquences
|
||||
Et que la fréquence pub est configurée à "1/5"
|
||||
Quand je fais un GET sur "/api/v1/audio-guides/ag_123/ad-schedule"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"ad_frequency": "1/5",
|
||||
"ad_insertions": [
|
||||
{"after_sequence": 5, "position": "after"},
|
||||
{"after_sequence": 10, "position": "after"}
|
||||
],
|
||||
"total_ads": 2
|
||||
}
|
||||
"""
|
||||
|
||||
Plan du Scénario: Fréquence publicité configurable admin
|
||||
Étant donné que la fréquence pub est configurée à <frequence>
|
||||
Et un audio-guide avec 12 séquences
|
||||
Quand les insertions pub sont calculées
|
||||
Alors le nombre de pubs insérées est <nombre_pubs>
|
||||
|
||||
Exemples:
|
||||
| frequence | nombre_pubs |
|
||||
| 1/3 | 4 |
|
||||
| 1/5 | 2 |
|
||||
| 1/10 | 1 |
|
||||
|
||||
Scénario: Utilisateur Premium - Aucune publicité
|
||||
Étant donné un utilisateur Premium
|
||||
Et un audio-guide gratuit
|
||||
Quand je fais un GET sur "/api/v1/audio-guides/ag_123/ad-schedule"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"ad_frequency": "0",
|
||||
"ad_insertions": [],
|
||||
"total_ads": 0,
|
||||
"reason": "premium_user"
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: POST /api/v1/audio-guides/playback/next-ad - Récupération pub suivante
|
||||
Étant donné qu'un utilisateur termine la séquence 5
|
||||
Et qu'une pub doit être insérée
|
||||
Quand je fais un POST sur "/api/v1/audio-guides/ag_123/playback/next-ad":
|
||||
"""json
|
||||
{
|
||||
"sequence_completed": 5,
|
||||
"user_position": {
|
||||
"latitude": 43.1234,
|
||||
"longitude": 2.5678
|
||||
},
|
||||
"mode": "voiture"
|
||||
}
|
||||
"""
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"should_play_ad": true,
|
||||
"ad": {
|
||||
"ad_id": "ad_789",
|
||||
"audio_url": "https://cdn.roadwave.fr/ads/ad_789.m4a",
|
||||
"duration_seconds": 30,
|
||||
"skippable_after": 5,
|
||||
"advertiser": "Brand X"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: Sélection pub géolocalisée
|
||||
Étant donné que l'utilisateur est en Île-de-France (43.1234, 2.5678)
|
||||
Et que des publicités géolocalisées existent pour cette région
|
||||
Quand la pub suivante est sélectionnée
|
||||
Alors l'API filtre les pubs par:
|
||||
| critère | valeur |
|
||||
| Géolocalisation | Île-de-France |
|
||||
| Catégorie | Tourisme, Culture |
|
||||
| Langue | Français |
|
||||
| Budget actif | true |
|
||||
Et une pub correspondante est retournée
|
||||
|
||||
Scénario: Fallback pub nationale si pas de pub locale
|
||||
Étant donné que l'utilisateur est dans une région sans pubs locales
|
||||
Quand la pub suivante est sélectionnée
|
||||
Alors l'API sélectionne une pub nationale (France entière)
|
||||
Et la pub est retournée normalement
|
||||
|
||||
Scénario: Pas de pub si séquence non multiple de 5
|
||||
Étant donné qu'un utilisateur termine la séquence 4
|
||||
Quand je fais un POST sur "/api/v1/audio-guides/ag_123/playback/next-ad"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"should_play_ad": false,
|
||||
"reason": "not_ad_sequence"
|
||||
}
|
||||
"""
|
||||
|
||||
# Comportement selon mode
|
||||
|
||||
Scénario: Pub en mode piéton (auto-play puis pause)
|
||||
Étant donné un audio-guide en mode piéton
|
||||
Et qu'une pub doit être insérée après séquence 5
|
||||
Quand la pub est récupérée
|
||||
Alors le mode_behavior retourné est:
|
||||
"""json
|
||||
{
|
||||
"auto_play": true,
|
||||
"pause_after": true,
|
||||
"reason": "pedestrian_mode"
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: Pub en mode voiture/vélo/transport (auto-play puis séquence suivante)
|
||||
Étant donné un audio-guide en mode voiture
|
||||
Et qu'une pub doit être insérée
|
||||
Quand la pub est récupérée
|
||||
Alors le mode_behavior retourné est:
|
||||
"""json
|
||||
{
|
||||
"auto_play": true,
|
||||
"pause_after": false,
|
||||
"continue_to_next": true,
|
||||
"reason": "vehicle_mode"
|
||||
}
|
||||
"""
|
||||
|
||||
# 16.5.2 - Métriques et tracking
|
||||
|
||||
Scénario: POST /api/v1/ads/{ad_id}/impressions - Enregistrement impression
|
||||
Étant donné qu'une pub "ad_789" démarre
|
||||
Quand je fais un POST sur "/api/v1/ads/ad_789/impressions":
|
||||
"""json
|
||||
{
|
||||
"audio_guide_id": "ag_123",
|
||||
"sequence_after": 5,
|
||||
"user_id": "user_456",
|
||||
"timestamp": "2026-01-22T14:35:00Z"
|
||||
}
|
||||
"""
|
||||
Alors le code HTTP de réponse est 201
|
||||
Et l'impression est enregistrée dans ad_impressions
|
||||
Et le compteur impressions_count est incrémenté
|
||||
|
||||
Scénario: POST /api/v1/ads/{ad_id}/completions - Enregistrement écoute complète
|
||||
Étant donné qu'une pub de 30 secondes est écoutée à 25 secondes (83%)
|
||||
Quand je fais un POST sur "/api/v1/ads/ad_789/completions":
|
||||
"""json
|
||||
{
|
||||
"audio_guide_id": "ag_123",
|
||||
"listened_seconds": 25,
|
||||
"total_duration": 30,
|
||||
"completion_rate": 83
|
||||
}
|
||||
"""
|
||||
Alors le code HTTP de réponse est 201
|
||||
Et l'écoute complète est enregistrée (>80%)
|
||||
Et le créateur de l'audio-guide reçoit 0.003€ de revenus
|
||||
|
||||
Scénario: POST /api/v1/ads/{ad_id}/skips - Enregistrement skip
|
||||
Étant donné qu'une pub est skippée après 6 secondes
|
||||
Quand je fais un POST sur "/api/v1/ads/ad_789/skips":
|
||||
"""json
|
||||
{
|
||||
"audio_guide_id": "ag_123",
|
||||
"skipped_at_second": 6
|
||||
}
|
||||
"""
|
||||
Alors le code HTTP de réponse est 201
|
||||
Et le skip est enregistré dans ad_skips
|
||||
Et le taux de skip est mis à jour
|
||||
|
||||
Scénario: Validation écoute complète (>80%)
|
||||
Étant donné qu'une pub de 30 secondes est écoutée 20 secondes (66%)
|
||||
Quand je fais un POST sur "/api/v1/ads/ad_789/completions"
|
||||
Alors le code HTTP de réponse est 400
|
||||
Et le message d'erreur est "completion_rate: minimum 80% requis pour écoute complète"
|
||||
Et aucun revenu n'est attribué
|
||||
|
||||
# Métriques créateur
|
||||
|
||||
Scénario: GET /api/v1/creators/me/audio-guides/{id}/ad-metrics - Métriques pub
|
||||
Étant donné un audio-guide "ag_123" avec publicités
|
||||
Et les métriques suivantes sur 30 jours:
|
||||
| impressions | ecoutes_completes | skips | revenus |
|
||||
| 1000 | 650 | 350 | 1.95€ |
|
||||
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/ad-metrics"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"period": "30_days",
|
||||
"impressions": 1000,
|
||||
"completions": 650,
|
||||
"skips": 350,
|
||||
"completion_rate": 65,
|
||||
"revenue": 1.95,
|
||||
"cpm": 1.95
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: Distinction revenus contenus classiques vs audio-guides
|
||||
Étant donné un créateur avec contenus classiques et audio-guides
|
||||
Quand je fais un GET sur "/api/v1/creators/me/revenue-breakdown"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"total_revenue": 85.40,
|
||||
"breakdown": {
|
||||
"classic_content": {
|
||||
"revenue": 45.20,
|
||||
"impressions": 15000
|
||||
},
|
||||
"audio_guides": {
|
||||
"revenue": 40.20,
|
||||
"impressions": 13000
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
# Répartition revenus
|
||||
|
||||
Scénario: Calcul revenus créateur (3€ / 1000 écoutes complètes)
|
||||
Étant donné un audio-guide avec 1000 écoutes complètes pub ce mois
|
||||
Quand les revenus sont calculés
|
||||
Alors le créateur reçoit 3€
|
||||
Et le revenu par écoute complète est 0.003€
|
||||
|
||||
Scénario: POST /api/v1/ads/revenue/process - Calcul revenus batch mensuel
|
||||
Étant donné le 1er du mois
|
||||
Et que 500 créateurs ont des revenus pub à calculer
|
||||
Quand le job batch s'exécute
|
||||
Alors pour chaque créateur:
|
||||
| creator_id | ecoutes_completes | revenus |
|
||||
| creator_1 | 5000 | 15.00€ |
|
||||
| creator_2 | 2000 | 6.00€ |
|
||||
| creator_3 | 1200 | 3.60€ |
|
||||
Et les revenus sont ajoutés au solde creator_balance
|
||||
|
||||
# Normalisation audio
|
||||
|
||||
Scénario: Validation volume pub (-14 LUFS)
|
||||
Étant donné qu'une pub est uploadée avec volume -10 LUFS
|
||||
Quand la pub est validée
|
||||
Alors un processus de normalisation est déclenché
|
||||
Et le volume est ajusté à -14 LUFS (standard RoadWave)
|
||||
Et la pub normalisée est stockée sur le CDN
|
||||
|
||||
Scénario: Validation durée pub (max 60 secondes)
|
||||
Étant donné qu'une pub de 75 secondes est uploadée
|
||||
Quand la validation est effectuée
|
||||
Alors le code HTTP de réponse est 400
|
||||
Et le message d'erreur est "duration: maximum 60 secondes pour une publicité"
|
||||
|
||||
# Cas d'erreur
|
||||
|
||||
Scénario: Aucune pub disponible (stock épuisé)
|
||||
Étant donné qu'aucune campagne pub n'est active dans la région
|
||||
Quand je fais un POST sur "/api/v1/audio-guides/ag_123/playback/next-ad"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"should_play_ad": false,
|
||||
"reason": "no_ads_available"
|
||||
}
|
||||
"""
|
||||
Et aucune pub n'est insérée (séquence suivante démarre directement)
|
||||
|
||||
Scénario: Budget campagne épuisé
|
||||
Étant donné qu'une campagne pub a un budget de 1000€
|
||||
Et que le budget est épuisé
|
||||
Quand la pub est sélectionnée
|
||||
Alors cette campagne est exclue de la sélection
|
||||
Et une autre campagne active est choisie
|
||||
|
||||
Scénario: Pub corrompue ou indisponible
|
||||
Étant donné qu'une pub sélectionnée a un fichier audio corrompu
|
||||
Quand le client tente de la charger
|
||||
Alors une pub de fallback (backup) est retournée
|
||||
Et un log d'erreur est créé pour investigation
|
||||
|
||||
# Filtrage et ciblage
|
||||
|
||||
Scénario: Ciblage par catégorie audio-guide
|
||||
Étant donné un audio-guide tagué "tourisme", "culture", "musée"
|
||||
Et une campagne pub ciblée "tourisme + culture"
|
||||
Quand la pub est sélectionnée
|
||||
Alors cette campagne a une priorité élevée (matching tags)
|
||||
Et elle est préférée aux pubs génériques
|
||||
|
||||
Scénario: Filtrage par classification âge
|
||||
Étant donné un audio-guide classifié "tout_public"
|
||||
Et une campagne pub classifiée "18+"
|
||||
Quand la pub est sélectionnée
|
||||
Alors cette campagne est exclue
|
||||
Et seules les pubs "tout_public" sont éligibles
|
||||
|
||||
Scénario: Limite fréquence pub par utilisateur (cap frequency)
|
||||
Étant donné qu'un utilisateur a déjà entendu la pub "ad_789" 3 fois ce jour
|
||||
Et que le cap frequency est configuré à 3/jour
|
||||
Quand la pub est sélectionnée
|
||||
Alors "ad_789" est exclue
|
||||
Et une autre pub est choisie
|
||||
|
||||
Scénario: GET /api/v1/audio-guides/{id}/ad-policy - Politique pub
|
||||
Étant donné un audio-guide
|
||||
Quand je fais un GET sur "/api/v1/audio-guides/ag_123/ad-policy"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"has_ads": true,
|
||||
"frequency": "1/5",
|
||||
"skippable_after_seconds": 5,
|
||||
"average_ad_duration": 30,
|
||||
"revenue_share": {
|
||||
"creator": "100%",
|
||||
"platform": "0%"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
# Performance
|
||||
|
||||
Scénario: Cache Redis pour pubs actives
|
||||
Étant donné que les campagnes actives sont en cache Redis
|
||||
Quand je fais un POST sur "/api/v1/audio-guides/ag_123/playback/next-ad"
|
||||
Alors les pubs sont récupérées depuis Redis (pas PostgreSQL)
|
||||
Et le temps de réponse est < 30ms
|
||||
|
||||
Scénario: Pre-fetching pub suivante (client)
|
||||
Étant donné que l'utilisateur est à la séquence 3
|
||||
Et qu'une pub sera insérée après la séquence 5
|
||||
Quand le client détecte l'approche de la séquence 5
|
||||
Alors il peut pré-charger la pub via GET "/api/v1/audio-guides/ag_123/ad-prefetch?after_sequence=5"
|
||||
Et la transition sera instantanée
|
||||
@@ -0,0 +1,123 @@
|
||||
# language: fr
|
||||
|
||||
@api @audio-guides @content-creation @mvp
|
||||
Fonctionnalité: Rayon de déclenchement configurable par le créateur
|
||||
|
||||
En tant que créateur de contenu
|
||||
Je veux configurer le rayon de déclenchement de chaque point d'intérêt
|
||||
Afin d'adapter l'expérience selon le type de lieu et le contexte
|
||||
|
||||
Contexte:
|
||||
Étant donné que les rayons configurables respectent:
|
||||
| Paramètre | Valeur |
|
||||
| Rayon minimum | 10 mètres |
|
||||
| Rayon maximum | 500 mètres |
|
||||
| Rayon par défaut | 100 mètres |
|
||||
| Ajustement | Par pas de 10m |
|
||||
|
||||
Scénario: Configuration du rayon lors de la création d'une séquence
|
||||
Étant donné un créateur "alice@roadwave.fr" qui ajoute un point d'intérêt
|
||||
Quand elle place un marqueur pour "Cathédrale Notre-Dame"
|
||||
Alors un slider de rayon s'affiche avec:
|
||||
| Élément | Valeur |
|
||||
| Rayon actuel | 100m (défaut) |
|
||||
| Rayon minimum | 10m |
|
||||
| Rayon maximum | 500m |
|
||||
| Visualisation | Cercle sur la carte |
|
||||
Et elle peut ajuster le rayon à 150m
|
||||
Alors le cercle sur la carte s'agrandit à 150m de rayon
|
||||
Et un événement "POI_RADIUS_CONFIGURED" est enregistré avec rayon: 150
|
||||
Et la métrique "poi.radius.configured" est incrémentée
|
||||
|
||||
Scénario: Rayon petit pour monuments précis (10-50m)
|
||||
Étant donné un créateur "bob@roadwave.fr" qui crée un audio-guide urbain
|
||||
Quand il configure un point pour une statue spécifique
|
||||
Et définit le rayon à 20m
|
||||
Alors le déclenchement sera très précis (proximité immédiate)
|
||||
Et le système valide que le rayon est suffisant
|
||||
Et un événement "POI_RADIUS_SMALL" est enregistré
|
||||
Et la métrique "poi.radius.small" est incrémentée
|
||||
|
||||
Scénario: Rayon large pour zones étendues (200-500m)
|
||||
Étant donné un créateur "charlie@roadwave.fr" qui crée un audio-guide de parc
|
||||
Quand il configure un point pour "Jardin du Luxembourg"
|
||||
Et définit le rayon à 300m
|
||||
Alors le déclenchement sera anticipé (approche du parc)
|
||||
Et le système valide que le rayon n'est pas excessif
|
||||
Et un événement "POI_RADIUS_LARGE" est enregistré
|
||||
Et la métrique "poi.radius.large" est incrémentée
|
||||
|
||||
Scénario: Visualisation en temps réel du rayon sur la carte
|
||||
Étant donné un créateur "david@roadwave.fr" qui ajuste un rayon
|
||||
Quand il déplace le slider de 100m à 250m
|
||||
Alors le cercle sur la carte s'agrandit en temps réel
|
||||
Et la zone de déclenchement est colorée en semi-transparent
|
||||
Et le rayon en mètres est affiché sur la carte
|
||||
Et un événement "POI_RADIUS_VISUALIZED" est enregistré
|
||||
|
||||
Scénario: Suggestions de rayon basées sur le type de lieu
|
||||
Étant donné un créateur "eve@roadwave.fr" qui ajoute un POI
|
||||
Quand elle sélectionne le type "Monument"
|
||||
Alors le système suggère un rayon de 50m
|
||||
Quand elle sélectionne le type "Parc/Jardin"
|
||||
Alors le système suggère un rayon de 200m
|
||||
Quand elle sélectionne le type "Vue panoramique"
|
||||
Alors le système suggère un rayon de 100m
|
||||
Et un événement "POI_RADIUS_SUGGESTED" est enregistré
|
||||
|
||||
Scénario: Test de simulation du déclenchement
|
||||
Étant donné un créateur "frank@roadwave.fr" qui configure un rayon de 150m
|
||||
Quand il clique sur "Tester le déclenchement"
|
||||
Alors une simulation GPS démarre
|
||||
Et il peut voir à quelle distance le point se déclencherait
|
||||
Et ajuster le rayon si nécessaire
|
||||
Et un événement "POI_RADIUS_TESTED" est enregistré
|
||||
|
||||
Scénario: Modification du rayon après publication
|
||||
Étant donné un créateur "grace@roadwave.fr" avec audio-guide publié
|
||||
Et elle constate que le rayon de 50m est trop petit (retours utilisateurs)
|
||||
Quand elle modifie le rayon à 120m
|
||||
Alors la modification prend effet immédiatement
|
||||
Et tous les futurs déclenchements utilisent le nouveau rayon
|
||||
Et un événement "POI_RADIUS_UPDATED" est enregistré
|
||||
|
||||
Scénario: Détection de chevauchements entre rayons
|
||||
Étant donné un créateur "henry@roadwave.fr" avec 2 points proches
|
||||
Quand les cercles de rayon se chevauchent à plus de 50%
|
||||
Alors un avertissement s'affiche: "Attention: chevauchement détecté"
|
||||
Et une suggestion est proposée: "Réduire les rayons ou espacer les points"
|
||||
Et un événement "POI_RADIUS_OVERLAP_DETECTED" est enregistré
|
||||
|
||||
Scénario: Rayons adaptatifs selon le mode de déplacement
|
||||
Étant donné un créateur "iris@roadwave.fr" qui configure un point
|
||||
Quand elle active "Rayons adaptatifs"
|
||||
Alors le système configure automatiquement:
|
||||
| Mode | Rayon suggéré |
|
||||
| Piéton | 80m |
|
||||
| Vélo | 120m |
|
||||
| Voiture | 300m |
|
||||
Et les utilisateurs bénéficient du rayon optimal selon leur mode
|
||||
Et un événement "POI_RADIUS_ADAPTIVE" est enregistré
|
||||
|
||||
Scénario: Statistiques d'efficacité des rayons
|
||||
Étant donné un créateur "jack@roadwave.fr" avec audio-guide publié
|
||||
Quand il consulte les statistiques de ses POI
|
||||
Alors il voit pour chaque point:
|
||||
| Point | Rayon | Taux déclenchement | Taux manqué |
|
||||
| Notre-Dame | 100m | 95% | 5% |
|
||||
| Sainte-Chapelle| 50m | 78% | 22% |
|
||||
| Panthéon | 150m | 98% | 2% |
|
||||
Et des suggestions d'optimisation sont proposées
|
||||
Et un événement "POI_RADIUS_STATS_VIEWED" est enregistré
|
||||
|
||||
Scénario: Métriques de performance des rayons configurés
|
||||
Étant donné que 5000 POI ont été configurés
|
||||
Quand les métriques sont collectées
|
||||
Alors les indicateurs suivants sont disponibles:
|
||||
| Métrique | Valeur moyenne |
|
||||
| Rayon moyen configuré | 125m |
|
||||
| Rayon le plus petit utilisé | 15m |
|
||||
| Rayon le plus grand utilisé | 450m |
|
||||
| Taux d'ajustement après tests | 35% |
|
||||
| Taux de déclenchement réussi | 88% |
|
||||
Et les métriques sont exportées vers le monitoring
|
||||
@@ -0,0 +1,103 @@
|
||||
# language: fr
|
||||
|
||||
@api @audio-guides @progression @mvp
|
||||
Fonctionnalité: Reprise de progression complète
|
||||
|
||||
En tant qu'utilisateur
|
||||
Je veux reprendre un audio-guide là où je l'ai laissé
|
||||
Afin de continuer mon expérience sans perdre ma progression
|
||||
|
||||
Contexte:
|
||||
Étant donné que la sauvegarde de progression inclut:
|
||||
| Donnée | Persistance |
|
||||
| Séquences écoutées | Permanente |
|
||||
| Position dans l'audio | 7 jours |
|
||||
| Points manqués | Permanente |
|
||||
| Progression globale | Permanente |
|
||||
|
||||
Scénario: Sauvegarde automatique de la progression
|
||||
Étant donné un utilisateur "alice@roadwave.fr" qui écoute une séquence
|
||||
Quand elle ferme l'application à 3min 20s
|
||||
Alors la progression est sauvegardée automatiquement
|
||||
Et la position exacte dans l'audio est conservée
|
||||
Et un événement "PROGRESS_AUTO_SAVED" est enregistré
|
||||
|
||||
Scénario: Reprise après fermeture de l'application
|
||||
Étant donné un utilisateur "bob@roadwave.fr" qui rouvre l'application
|
||||
Et il avait un audio-guide en cours (5/10 séquences)
|
||||
Quand il accède à l'écran d'accueil
|
||||
Alors une carte "Reprendre votre visite" s'affiche:
|
||||
| Élément | Contenu |
|
||||
| Titre audio-guide | Visite du Quartier Latin |
|
||||
| Progression | 5/10 séquences (50%) |
|
||||
| Dernière position | Panthéon - 3min 20s |
|
||||
| Bouton | [Reprendre] |
|
||||
Et un événement "RESUME_CARD_DISPLAYED" est enregistré
|
||||
|
||||
Scénario: Reprise exacte de la position audio
|
||||
Étant donné un utilisateur "charlie@roadwave.fr" qui reprend un audio-guide
|
||||
Et il était à 3min 20s dans la séquence "Panthéon"
|
||||
Quand il clique sur "Reprendre"
|
||||
Alors l'audio reprend exactement à 3min 20s
|
||||
Et aucune seconde n'est perdue
|
||||
Et un événement "AUDIO_POSITION_RESTORED" est enregistré
|
||||
|
||||
Scénario: Synchronisation multi-appareils de la progression
|
||||
Étant donné un utilisateur "david@roadwave.fr" qui écoute sur iPhone
|
||||
Et il a complété 3 séquences
|
||||
Quand il passe sur son iPad
|
||||
Alors la progression est synchronisée automatiquement
|
||||
Et il peut reprendre là où il s'était arrêté
|
||||
Et un événement "PROGRESS_SYNCED_CROSS_DEVICE" est enregistré
|
||||
|
||||
Scénario: Historique des audio-guides en cours
|
||||
Étant donné un utilisateur "eve@roadwave.fr" avec 3 audio-guides en cours
|
||||
Quand elle accède à "Mes audio-guides en cours"
|
||||
Alors elle voit la liste:
|
||||
| Audio-guide | Progression | Dernière activité |
|
||||
| Quartier Latin | 5/10 (50%) | Il y a 2 heures |
|
||||
| Châteaux de la Loire | 3/8 (37%) | Il y a 3 jours |
|
||||
| Montmartre | 1/6 (16%) | Il y a 1 semaine |
|
||||
Et elle peut reprendre n'importe lequel
|
||||
Et un événement "IN_PROGRESS_LIST_VIEWED" est enregistré
|
||||
|
||||
Scénario: Expiration de la position audio après 7 jours
|
||||
Étant donné un utilisateur "frank@roadwave.fr" avec audio-guide en pause
|
||||
Et 8 jours se sont écoulés depuis la dernière écoute
|
||||
Quand il reprend l'audio-guide
|
||||
Alors la progression globale est conservée (séquences écoutées)
|
||||
Mais la position exacte dans l'audio est réinitialisée
|
||||
Et un message s'affiche: "La séquence redémarre depuis le début"
|
||||
Et un événement "AUDIO_POSITION_EXPIRED" est enregistré
|
||||
|
||||
Scénario: Badge "Explorateur assidu" pour reprises régulières
|
||||
Étant donné un utilisateur "grace@roadwave.fr" qui reprend 10 audio-guides
|
||||
Quand il complète chacun d'eux après les avoir repris
|
||||
Alors un badge "Explorateur assidu" est débloqué
|
||||
Et un événement "BADGE_PERSISTENT_EXPLORER_UNLOCKED" est enregistré
|
||||
|
||||
Scénario: Notification push de rappel après 3 jours d'inactivité
|
||||
Étant donné un utilisateur "henry@roadwave.fr" avec audio-guide en pause
|
||||
Et 3 jours se sont écoulés sans activité
|
||||
Quand le système envoie des rappels
|
||||
Alors une notification push est envoyée:
|
||||
"Vous avez laissé 'Visite du Quartier Latin' en suspens (5/10). Reprendre ?"
|
||||
Et un événement "RESUME_REMINDER_SENT" est enregistré
|
||||
|
||||
Scénario: Mode hors ligne avec sauvegarde locale
|
||||
Étant donné un utilisateur "iris@roadwave.fr" en mode hors ligne
|
||||
Quand elle écoute un audio-guide sans connexion
|
||||
Alors la progression est sauvegardée localement
|
||||
Et synchronisée automatiquement lors de la reconnexion
|
||||
Et un événement "PROGRESS_SYNCED_AFTER_OFFLINE" est enregistré
|
||||
|
||||
Scénario: Métriques de reprise de progression
|
||||
Étant donné que 10 000 audio-guides ont été mis en pause
|
||||
Alors les indicateurs suivants sont disponibles:
|
||||
| Métrique | Valeur |
|
||||
| Taux de reprise dans les 24h | 42% |
|
||||
| Taux de reprise dans les 7j | 68% |
|
||||
| Taux d'abandon définitif | 32% |
|
||||
| Temps moyen avant reprise | 2.5 jours|
|
||||
| Taux de complétion après reprise| 78% |
|
||||
Et les métriques sont exportées vers le monitoring
|
||||
@@ -0,0 +1,75 @@
|
||||
# language: fr
|
||||
|
||||
@api @audio-guides @sync @mvp
|
||||
Fonctionnalité: Sauvegarde et synchronisation de progression
|
||||
|
||||
En tant qu'utilisateur
|
||||
Je veux que ma progression soit sauvegardée et synchronisée
|
||||
Afin d'accéder à mon historique sur tous mes appareils
|
||||
|
||||
Scénario: Sauvegarde en temps réel dans le cloud
|
||||
Étant donné un utilisateur "alice@roadwave.fr" connecté
|
||||
Quand elle complète une séquence
|
||||
Alors la progression est sauvegardée dans le cloud immédiatement
|
||||
Et un indicateur "Synchronisé" s'affiche
|
||||
Et un événement "PROGRESS_CLOUD_SAVED" est enregistré
|
||||
|
||||
Scénario: Synchronisation automatique au changement d'appareil
|
||||
Étant donné un utilisateur "bob@roadwave.fr" sur iPhone
|
||||
Quand il se connecte sur iPad
|
||||
Alors la progression est téléchargée automatiquement
|
||||
Et synchronisée en arrière-plan (< 2s)
|
||||
Et un événement "PROGRESS_SYNCED_DEVICE_SWITCH" est enregistré
|
||||
|
||||
Scénario: Résolution de conflits de synchronisation
|
||||
Étant donné un utilisateur "charlie@roadwave.fr" avec 2 appareils
|
||||
Et il écoute hors ligne sur les deux simultanément
|
||||
Quand les deux se reconnectent avec progressions différentes
|
||||
Alors le système fusionne intelligemment les données
|
||||
Et conserve la progression la plus avancée
|
||||
Et un événement "SYNC_CONFLICT_RESOLVED" est enregistré
|
||||
|
||||
Scénario: Indicateur de statut de synchronisation
|
||||
Étant donné un utilisateur "david@roadwave.fr"
|
||||
Alors il voit l'icône de statut sync:
|
||||
| État | Icône | Couleur |
|
||||
| Synchronisé | ✓ | Vert |
|
||||
| En cours de sync | ↻ | Orange |
|
||||
| Non synchronisé | ⚠ | Rouge |
|
||||
Et un événement "SYNC_STATUS_DISPLAYED" est enregistré
|
||||
|
||||
Scénario: Sauvegarde locale en mode hors ligne
|
||||
Étant donné un utilisateur "eve@roadwave.fr" sans connexion
|
||||
Quand elle écoute un audio-guide hors ligne
|
||||
Alors toutes les données sont sauvegardées localement
|
||||
Et marquées "En attente de synchronisation"
|
||||
Et synchronisées automatiquement lors de la reconnexion
|
||||
Et un événement "OFFLINE_PROGRESS_QUEUED" est enregistré
|
||||
|
||||
Scénario: Export de l'historique de progression
|
||||
Étant donné un utilisateur "frank@roadwave.fr"
|
||||
Quand il demande un export de ses données (RGPD)
|
||||
Alors il reçoit un fichier JSON avec:
|
||||
| Donnée | Format |
|
||||
| Audio-guides écoutés | Liste |
|
||||
| Séquences par guide | Détail |
|
||||
| Timestamps | ISO 8601 |
|
||||
| Positions GPS visitées | Lat/Lon |
|
||||
Et un événement "PROGRESS_EXPORTED" est enregistré
|
||||
|
||||
Scénario: Suppression de progression sur demande
|
||||
Étant donné un utilisateur "grace@roadwave.fr"
|
||||
Quand elle supprime un audio-guide de son historique
|
||||
Alors toutes les données associées sont supprimées
|
||||
Et la synchronisation propage la suppression
|
||||
Et un événement "PROGRESS_DELETED" est enregistré
|
||||
|
||||
Scénario: Métriques de fiabilité de la synchronisation
|
||||
Étant donné que 100 000 synchronisations ont eu lieu
|
||||
Alors les indicateurs suivants sont disponibles:
|
||||
| Métrique | Valeur cible |
|
||||
| Taux de succès de sync | > 99.5% |
|
||||
| Temps moyen de synchronisation| < 2s |
|
||||
| Taux de conflits | < 0.5% |
|
||||
| Taux de résolution automatique| > 95% |
|
||||
Et les métriques sont exportées vers le monitoring
|
||||
@@ -0,0 +1,192 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Système double clic et sortie audio-guide mode voiture
|
||||
En tant qu'utilisateur en voiture
|
||||
Je veux pouvoir désactiver le GPS automatique et sortir de l'audio-guide facilement
|
||||
Afin de gérer les situations d'embouteillage ou de changement de plan
|
||||
|
||||
Contexte:
|
||||
Étant donné qu'un utilisateur est en mode voiture
|
||||
Et qu'un audio-guide de 8 séquences est actif
|
||||
Et que le mode GPS automatique est activé par défaut
|
||||
Et que la séquence 2 est en cours de lecture
|
||||
|
||||
# Comportement bouton [▶|] Suivant
|
||||
|
||||
Scénario: Premier clic Suivant - Passage en mode manuel
|
||||
Étant donné que le mode GPS auto est actif
|
||||
Et que la séquence 2 vient de se terminer
|
||||
Et que le prochain point GPS (séquence 3) est à 2 km
|
||||
Quand l'utilisateur clique sur le bouton [▶|] Suivant
|
||||
Alors le GPS automatique est désactivé
|
||||
Et le mode bascule en "mode manuel"
|
||||
Et la séquence 3 démarre immédiatement
|
||||
Et un toast s'affiche pendant 3 secondes:
|
||||
"""
|
||||
Mode manuel activé. Cliquez à nouveau pour quitter l'audio-guide.
|
||||
"""
|
||||
Et un timer de 10 secondes démarre en arrière-plan
|
||||
|
||||
Scénario: Deuxième clic Suivant dans les 10 secondes - Sortie audio-guide
|
||||
Étant donné que le mode manuel vient d'être activé il y a 5 secondes
|
||||
Et que la séquence 3 est en cours de lecture
|
||||
Quand l'utilisateur clique à nouveau sur [▶|] Suivant
|
||||
Alors l'audio-guide est mis en pause
|
||||
Et l'historique de progression est conservé (séquence 3 à X:XX)
|
||||
Et l'application retourne au flux normal de recommandation
|
||||
Et un toast s'affiche pendant 2 secondes: "Audio-guide en pause"
|
||||
|
||||
Scénario: Clic Suivant après 10 secondes - Navigation normale
|
||||
Étant donné que le mode manuel est actif depuis 12 secondes
|
||||
Et que la séquence 3 est en cours
|
||||
Quand l'utilisateur clique sur [▶|] Suivant
|
||||
Alors la séquence 4 démarre immédiatement
|
||||
Et le timer de 10 secondes redémarre
|
||||
Et le mode reste en "mode manuel"
|
||||
Et aucune sortie d'audio-guide ne se produit
|
||||
|
||||
Scénario: Clics multiples Suivant en mode manuel
|
||||
Étant donné que le mode manuel est actif
|
||||
Et que l'utilisateur clique sur [▶|] pour passer séquence 3 → 4
|
||||
Et que 5 secondes se passent
|
||||
Quand l'utilisateur clique à nouveau sur [▶|] pour passer séquence 4 → 5
|
||||
Alors la séquence 5 démarre
|
||||
Et le timer de 10 secondes redémarre à chaque clic
|
||||
Et l'utilisateur peut naviguer normalement entre les séquences
|
||||
|
||||
Scénario: Double clic rapide accidentel - sortie immédiate
|
||||
Étant donné que le mode GPS auto est actif
|
||||
Et que la séquence 2 vient de se terminer
|
||||
Quand l'utilisateur clique sur [▶|] (clic 1)
|
||||
Et que l'utilisateur clique immédiatement sur [▶|] (clic 2 à <2s)
|
||||
Alors l'audio-guide est mis en pause après le clic 2
|
||||
Et l'utilisateur retourne au flux normal
|
||||
Et un toast confirme: "Audio-guide en pause"
|
||||
|
||||
# Comportement bouton [|◀] Précédent
|
||||
|
||||
Scénario: Bouton Précédent dans audio-guide GPS auto
|
||||
Étant donné que le mode GPS auto est actif
|
||||
Et que la séquence 3 est en cours
|
||||
Quand l'utilisateur clique sur [|◀] Précédent
|
||||
Alors la séquence 2 démarre
|
||||
Et l'audio-guide reste actif
|
||||
Et le mode GPS auto reste actif
|
||||
|
||||
Scénario: Bouton Précédent dans audio-guide mode manuel
|
||||
Étant donné que le mode manuel est actif
|
||||
Et que la séquence 5 est en cours
|
||||
Quand l'utilisateur clique sur [|◀] Précédent
|
||||
Alors la séquence 4 démarre
|
||||
Et l'audio-guide reste actif
|
||||
Et le mode manuel reste actif
|
||||
|
||||
Scénario: Bouton Précédent hors audio-guide - Reprend audio-guide si contenu précédent
|
||||
Étant donné que l'utilisateur a quitté l'audio-guide "Safari du Paugre"
|
||||
Et que l'utilisateur écoute un contenu normal "Podcast A"
|
||||
Quand l'utilisateur clique sur [|◀] Précédent
|
||||
Alors l'audio-guide "Safari du Paugre" reprend
|
||||
Et la dernière séquence écoutée (séquence 3) reprend
|
||||
|
||||
# Détection et reprise après détour
|
||||
|
||||
Scénario: Détection hors itinéraire >1 km pendant >10 min
|
||||
Étant donné que l'audio-guide est actif (mode GPS auto ou manuel)
|
||||
Et que l'utilisateur s'éloigne à 1.2 km de tous les points GPS
|
||||
Et que cette situation dure 11 minutes
|
||||
Quand le système détecte le hors itinéraire
|
||||
Alors un toast s'affiche: "Audio-guide en pause (hors itinéraire)"
|
||||
Et l'icône de l'audio-guide passe en gris (inactif)
|
||||
Et la lecture continue du contenu en cours s'arrête
|
||||
|
||||
Scénario: Retour sur itinéraire <100m d'un point non écouté
|
||||
Étant donné que l'audio-guide est en pause (hors itinéraire)
|
||||
Et que l'utilisateur revient à 80m du point GPS séquence 5 (non écoutée)
|
||||
Quand le système détecte le retour sur itinéraire
|
||||
Alors une popup s'affiche:
|
||||
"""
|
||||
Reprendre l'audio-guide à la séquence 5 ?
|
||||
[Reprendre] [Voir liste] [Ignorer]
|
||||
"""
|
||||
|
||||
Scénario: Action "Reprendre" après retour sur itinéraire
|
||||
Étant donné que la popup de reprise est affichée
|
||||
Quand l'utilisateur clique sur [Reprendre]
|
||||
Alors la séquence 5 démarre immédiatement
|
||||
Et l'audio-guide redevient actif
|
||||
Et l'icône repasse en couleur normale
|
||||
|
||||
Scénario: Action "Voir liste" après retour sur itinéraire
|
||||
Étant donné que la popup de reprise est affichée
|
||||
Quand l'utilisateur clique sur [Voir liste]
|
||||
Alors la liste complète des séquences s'affiche
|
||||
Et l'utilisateur peut choisir manuellement quelle séquence écouter
|
||||
|
||||
Scénario: Action "Ignorer" après retour sur itinéraire
|
||||
Étant donné que la popup de reprise est affichée
|
||||
Quand l'utilisateur clique sur [Ignorer]
|
||||
Alors la popup se ferme
|
||||
Et l'audio-guide reste en pause
|
||||
Et l'utilisateur continue le flux normal de recommandation
|
||||
|
||||
# Respect des clics manuels
|
||||
|
||||
Scénario: Séquence skippée manuellement non reproposée automatiquement
|
||||
Étant donné que l'utilisateur est en mode manuel
|
||||
Et que l'utilisateur clique [▶|] pour passer de séquence 3 à séquence 4
|
||||
Et que la séquence 3 est marquée "skippée volontairement"
|
||||
Quand l'utilisateur revient à 50m du point GPS séquence 3
|
||||
Alors aucune popup de reprise automatique ne s'affiche
|
||||
Et l'utilisateur peut revenir manuellement via liste séquences s'il le souhaite
|
||||
|
||||
Scénario: Séquence skippée par GPS (point manqué) reproposable
|
||||
Étant donné que l'utilisateur a dépassé un point GPS à 110m (rayon 30m)
|
||||
Et que la séquence 3 a été marquée "point manqué" (pas de skip manuel)
|
||||
Quand l'utilisateur revient à 80m du point GPS séquence 3
|
||||
Alors une popup de reprise s'affiche:
|
||||
"""
|
||||
Reprendre la séquence 3 ?
|
||||
[Reprendre] [Voir liste] [Ignorer]
|
||||
"""
|
||||
|
||||
# Mode manuel persistant
|
||||
|
||||
Scénario: Mode manuel persiste jusqu'à fin audio-guide
|
||||
Étant donné que le mode manuel est activé en séquence 3
|
||||
Quand l'utilisateur navigue jusqu'à la séquence 8 (dernière)
|
||||
Alors le mode manuel reste actif durant toutes les séquences
|
||||
Et le GPS automatique n'est jamais réactivé
|
||||
|
||||
Scénario: Reset mode GPS auto au redémarrage audio-guide
|
||||
Étant donné que l'utilisateur a quitté l'audio-guide en mode manuel
|
||||
Et que plusieurs heures se sont écoulées
|
||||
Quand l'utilisateur relance l'audio-guide "Safari du Paugre"
|
||||
Alors le mode GPS automatique est réactivé par défaut
|
||||
Et l'utilisateur peut à nouveau passer en mode manuel s'il le souhaite
|
||||
|
||||
# Cas d'usage réel : embouteillage
|
||||
|
||||
Scénario: Embouteillage - Passage manuel puis sortie
|
||||
Étant donné que l'utilisateur écoute la séquence 2 "Les lions"
|
||||
Et que la séquence 2 se termine
|
||||
Et que le prochain point GPS (séquence 3) est à 3 km
|
||||
Et que l'utilisateur est bloqué dans un embouteillage
|
||||
Et que l'ETA indique "≈ 30 minutes"
|
||||
Quand l'utilisateur clique [▶|] (clic 1) pour passer en mode manuel
|
||||
Alors la séquence 3 démarre immédiatement
|
||||
Et le toast indique: "Mode manuel activé. Cliquez à nouveau pour quitter."
|
||||
Quand l'utilisateur clique [▶|] (clic 2) dans les 8 secondes
|
||||
Alors l'audio-guide est mis en pause
|
||||
Et l'utilisateur retourne au flux normal (podcasts, musique)
|
||||
Et la progression est sauvegardée (séquence 3 à X:XX)
|
||||
|
||||
Scénario: Reprise audio-guide après sortie embouteillage
|
||||
Étant donné que l'utilisateur a quitté l'audio-guide en séquence 3
|
||||
Et que plusieurs heures plus tard, l'utilisateur se reconnecte
|
||||
Et que l'utilisateur est à 80m du point GPS séquence 4
|
||||
Quand le système détecte la proximité
|
||||
Alors une popup de reprise s'affiche:
|
||||
"""
|
||||
Reprendre l'audio-guide "Safari du Paugre" ?
|
||||
Progression : 3/8 séquences
|
||||
[Reprendre] [Recommencer] [Voir liste]
|
||||
"""
|
||||
@@ -0,0 +1,59 @@
|
||||
# language: fr
|
||||
|
||||
@api @content-creation @copyright @mvp
|
||||
Fonctionnalité: Fair use 30 secondes musique
|
||||
|
||||
En tant que créateur
|
||||
Je veux utiliser jusqu'à 30 secondes de musique protégée
|
||||
Afin d'enrichir mon contenu dans le cadre du fair use
|
||||
|
||||
Scénario: Détection automatique de musique dans l'upload
|
||||
Étant donné un créateur "alice@roadwave.fr" qui upload un audio
|
||||
Quand le fichier contient de la musique
|
||||
Alors le système détecte via fingerprinting audio (ACRCloud)
|
||||
Et identifie les morceaux présents
|
||||
Et mesure la durée de chaque extrait
|
||||
Et un événement "MUSIC_DETECTED" est enregistré
|
||||
|
||||
Scénario: Validation automatique si < 30 secondes
|
||||
Étant donné un audio avec 25 secondes de musique protégée
|
||||
Quand la validation automatique s'exécute
|
||||
Alors le contenu est approuvé (fair use)
|
||||
Et un badge "Fair use" est appliqué
|
||||
Et un événement "FAIR_USE_APPROVED" est enregistré
|
||||
|
||||
Scénario: Blocage automatique si > 30 secondes
|
||||
Étant donné un audio avec 45 secondes de musique protégée
|
||||
Quand la validation s'exécute
|
||||
Alors le contenu est bloqué
|
||||
Et le créateur voit: "Extrait musical trop long (45s). Max: 30s"
|
||||
Et il peut éditer et re-uploader
|
||||
Et un événement "FAIR_USE_REJECTED" est enregistré
|
||||
|
||||
Scénario: Liste des morceaux détectés avec durée
|
||||
Étant donné un créateur "bob@roadwave.fr" avec musique détectée
|
||||
Alors il voit la liste:
|
||||
| Morceau | Artiste | Durée | Statut |
|
||||
| Bohemian Rhapsody | Queen | 28s | ✓ OK |
|
||||
| Imagine | John Lennon | 15s | ✓ OK |
|
||||
Et la durée totale: 43s
|
||||
Et un avertissement si total > 30s
|
||||
Et un événement "MUSIC_DETECTION_RESULTS_DISPLAYED" est enregistré
|
||||
|
||||
Scénario: Suggestions de musique libre de droits
|
||||
Étant donné un créateur "charlie@roadwave.fr"
|
||||
Quand son audio dépasse les 30s de musique protégée
|
||||
Alors le système suggère des alternatives libres:
|
||||
| Morceau | Licence | Style |
|
||||
| Acoustic Breeze | CC BY | Acoustique |
|
||||
| Epic Cinematic | Royalty-free| Épique |
|
||||
Et un lien vers une bibliothèque musicale
|
||||
Et un événement "FREE_MUSIC_SUGGESTED" est enregistré
|
||||
|
||||
Scénario: Limitation cumulative par audio-guide
|
||||
Étant donné un créateur "david@roadwave.fr" avec audio-guide de 10 séquences
|
||||
Quand il utilise de la musique protégée
|
||||
Alors chaque séquence peut contenir max 30s
|
||||
Mais le total cumulé est limité à 3 minutes par audio-guide
|
||||
Et un compteur affiche: "2min 15s / 3min utilisés"
|
||||
Et un événement "CUMULATIVE_MUSIC_LIMIT_TRACKED" est enregistré
|
||||
@@ -0,0 +1,41 @@
|
||||
# language: fr
|
||||
|
||||
@api @content-creation @media @mvp
|
||||
Fonctionnalité: Génération automatique d'image de couverture
|
||||
|
||||
En tant que créateur
|
||||
Je veux générer automatiquement une image de couverture
|
||||
Afin de gagner du temps et avoir un visuel professionnel
|
||||
|
||||
Scénario: Génération automatique depuis position GPS
|
||||
Étant donné un créateur "alice@roadwave.fr"
|
||||
Quand il crée un audio-guide centré sur "Notre-Dame"
|
||||
Alors le système propose une image de Notre-Dame via API (Unsplash/Pexels)
|
||||
Et 5 suggestions d'images sont affichées
|
||||
Et le créateur peut choisir ou uploader la sienne
|
||||
Et un événement "COVER_AUTO_GENERATED" est enregistré
|
||||
|
||||
Scénario: Ajout automatique de texte sur l'image
|
||||
Étant donné un créateur "bob@roadwave.fr" qui valide une image
|
||||
Quand l'image est sélectionnée
|
||||
Alors le titre de l'audio-guide est ajouté automatiquement
|
||||
Et un filtre sombre est appliqué pour lisibilité
|
||||
Et le texte est centré et optimisé
|
||||
Et un événement "COVER_TEXT_OVERLAY_ADDED" est enregistré
|
||||
|
||||
Scénario: Templates prédéfinis par catégorie
|
||||
Étant donné un créateur "charlie@roadwave.fr"
|
||||
Quand il sélectionne la catégorie "Tourisme"
|
||||
Alors des templates touristiques sont proposés
|
||||
Et il peut personnaliser couleurs et polices
|
||||
Et un événement "COVER_TEMPLATE_USED" est enregistré
|
||||
|
||||
Scénario: Optimisation automatique pour mobile et web
|
||||
Étant donné un créateur "david@roadwave.fr" qui valide une couverture
|
||||
Alors 3 versions sont générées:
|
||||
| Format | Dimensions |
|
||||
| Mobile | 1080x1920 |
|
||||
| Tablette | 2048x2732 |
|
||||
| Web | 1920x1080 |
|
||||
Et toutes sont optimisées en WebP
|
||||
Et un événement "COVER_OPTIMIZED" est enregistré
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user