1 Commits

Author SHA1 Message Date
Enzo
e209316157 Premier commit sur test-enzo 2026-02-01 19:31:00 +01:00
298 changed files with 83628 additions and 24201 deletions

View File

@@ -1,31 +0,0 @@
# Ignore backend files
backend/**
mobile/**
# Ignore git
.git/
.gitignore
# Ignore docker files
docker-compose.yml
*.dockerfile
# Ignore node_modules and build artifacts
node_modules/
dist/
build/
bin/
tmp/
# Ignore environment files
.env
.env.*
# Ignore IDE
.vscode/
.idea/
# Ignore documentation build artifacts
site/
docs/generated/
output/

10
.gitignore vendored
View File

@@ -6,9 +6,9 @@
*.dylib *.dylib
/bin/ /bin/
/dist/ /dist/
/api api
/worker worker
/migrate migrate
# Test binary, built with `go test -c` # Test binary, built with `go test -c`
*.test *.test
@@ -64,6 +64,4 @@ config/*.local.yaml
# MkDocs # MkDocs
site/ site/
.cache/ .cache/
docs/bdd/
# Generated documentation
docs/generated/

View File

@@ -2,19 +2,13 @@
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 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 ## 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. 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**: **Tech Stack**:
- Backend: Go 1.21+ with Fiber framework - Backend: Go 1.21+ with Fiber framework
- Mobile: Flutter (see [ADR-012](docs/adr/012-frontend-mobile.md)) - Mobile: Flutter (see [ADR-014](docs/adr/014-frontend-mobile.md))
- Database: PostgreSQL 16+ with PostGIS extension - Database: PostgreSQL 16+ with PostGIS extension
- Cache: Redis 7+ with geospatial features - Cache: Redis 7+ with geospatial features
- Auth: Zitadel (self-hosted IAM) - Auth: Zitadel (self-hosted IAM)
@@ -38,11 +32,11 @@ This is a monorepo organized as follows:
- Backend step definitions: `backend/tests/bdd/` - Backend step definitions: `backend/tests/bdd/`
- Mobile step definitions: `mobile/tests/bdd/` - Mobile step definitions: `mobile/tests/bdd/`
See [ADR-014](docs/adr/014-organisation-monorepo.md) for monorepo organization rationale. See [ADR-016](docs/adr/016-organisation-monorepo.md) for monorepo organization rationale.
## Backend Architecture ## Backend Architecture
**Modular monolith** with clear module separation ([ADR-010](docs/adr/010-architecture-backend.md)): **Modular monolith** with clear module separation ([ADR-012](docs/adr/012-architecture-backend.md)):
``` ```
backend/internal/ backend/internal/
@@ -58,7 +52,7 @@ backend/internal/
**Module pattern**: Each module follows `handler.go``service.go``repository.go`. **Module pattern**: Each module follows `handler.go``service.go``repository.go`.
**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. **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.
## Development Commands ## Development Commands
@@ -82,7 +76,7 @@ Services after `make docker-up`:
### Testing ### Testing
**Test Strategy** ([ADR-013](docs/adr/013-strategie-tests.md)): **Test Strategy** ([ADR-015](docs/adr/015-strategie-tests.md)):
- Unit tests: Testify (80%+ coverage target) - Unit tests: Testify (80%+ coverage target)
- Integration tests: Testcontainers (for PostGIS queries) - Integration tests: Testcontainers (for PostGIS queries)
- BDD tests: Godog/Gherkin (user stories validation) - BDD tests: Godog/Gherkin (user stories validation)
@@ -137,10 +131,6 @@ make docs-pdf # Generate PDF of all documentation
make docs-clean # Remove generated docs and PDF make docs-clean # Remove generated docs and PDF
``` ```
**First run**: The first `make docs-serve` will build a custom Docker image with mkdocs-kroki-plugin. This takes ~30s but is cached for subsequent runs.
**DBML Support**: The documentation now supports DBML (Database Markup Language) for database diagrams via the Kroki plugin. Use `kroki-dbml` code blocks in markdown files. See [docs/examples/dbml-example.md](docs/examples/dbml-example.md) for examples.
## Working with sqlc ## Working with sqlc
When adding or modifying database queries: When adding or modifying database queries:
@@ -188,42 +178,13 @@ Feature: Geolocalised recommendation
All technical decisions are documented in Architecture Decision Records (ADRs) in `/docs/adr/`: 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-001](docs/adr/001-langage-backend.md): Backend language (Go)
- [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-002](docs/adr/002-protocole-streaming.md): Streaming protocol (HLS)
- [ADR-003](docs/adr/003-codec-audio.md): Audio codec (Opus) - [ADR-005](docs/adr/005-base-de-donnees.md): Database (PostgreSQL + PostGIS)
- [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-008](docs/adr/008-authentification.md): Authentication (Zitadel)
- [ADR-025](docs/adr/025-securite-secrets.md): Secrets management - [ADR-012](docs/adr/012-architecture-backend.md): Backend architecture (modular monolith)
- [ADR-013](docs/adr/013-orm-acces-donnees.md): Data access (sqlc)
### Testing & Quality - [ADR-016](docs/adr/016-organisation-monorepo.md): Monorepo organization
- [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. **When making architectural decisions**, check if there's an existing ADR or create a new one following the established pattern.
@@ -292,7 +253,7 @@ Zitadel handles authentication ([ADR-008](docs/adr/008-authentification.md)):
## Performance Targets ## Performance Targets
See [TECHNICAL.md](docs/TECHNICAL.md) for detailed metrics: See [TECHNICAL.md](TECHNICAL.md) for detailed metrics:
- API latency p99: < 100ms - API latency p99: < 100ms
- Audio start time: < 3s - Audio start time: < 3s
- Target availability: 99.9% - Target availability: 99.9%

View File

@@ -51,7 +51,7 @@ test-integration:
## test-bdd: Run BDD tests (Godog) ## test-bdd: Run BDD tests (Godog)
test-bdd: test-bdd:
@echo "$(BLUE)Running BDD tests...$(NC)" @echo "$(BLUE)Running BDD tests...$(NC)"
@godog run docs/domains/*/features/ @godog run features/
## test-coverage: Run tests with coverage report ## test-coverage: Run tests with coverage report
test-coverage: test-coverage:
@@ -70,14 +70,14 @@ clean:
## docs-clean: Remove generated documentation (BDD docs and PDF) ## docs-clean: Remove generated documentation (BDD docs and PDF)
docs-clean: docs-clean:
@echo "$(YELLOW)Cleaning generated documentation...$(NC)" @echo "$(YELLOW)Cleaning generated documentation...$(NC)"
@rm -rf docs/generated/ site/ @rm -rf docs/bdd/ output/RoadWave_Documentation.pdf
@docker rmi roadwave-pdf-generator roadwave-mkdocs 2>/dev/null || true @docker rmi roadwave-pdf-generator 2>/dev/null || true
@echo "$(GREEN)✓ Documentation cleaned$(NC)" @echo "$(GREEN)✓ Documentation cleaned$(NC)"
## docker-up: Start all Docker services ## docker-up: Start all Docker services
docker-up: docker-up:
@echo "$(BLUE)Starting Docker services...$(NC)" @echo "$(BLUE)Starting Docker services...$(NC)"
@cd backend && docker compose up -d @docker compose up -d
@echo "$(GREEN)✓ Services started$(NC)" @echo "$(GREEN)✓ Services started$(NC)"
@echo "$(YELLOW)API: http://localhost:8080$(NC)" @echo "$(YELLOW)API: http://localhost:8080$(NC)"
@echo "$(YELLOW)Zitadel: http://localhost:8081$(NC)" @echo "$(YELLOW)Zitadel: http://localhost:8081$(NC)"
@@ -86,12 +86,12 @@ docker-up:
## docker-down: Stop all Docker services ## docker-down: Stop all Docker services
docker-down: docker-down:
@echo "$(YELLOW)Stopping Docker services...$(NC)" @echo "$(YELLOW)Stopping Docker services...$(NC)"
@cd backend && docker compose down @docker compose down
@echo "$(GREEN)✓ Services stopped$(NC)" @echo "$(GREEN)✓ Services stopped$(NC)"
## docker-logs: Show Docker logs ## docker-logs: Show Docker logs
docker-logs: docker-logs:
@cd backend && docker compose logs -f @docker compose logs -f
## migrate-up: Apply all migrations ## migrate-up: Apply all migrations
migrate-up: migrate-up:
@@ -151,34 +151,30 @@ run-api:
## docs-serve: Start documentation server (http://localhost:8000) ## docs-serve: Start documentation server (http://localhost:8000)
docs-serve: docs-serve:
@echo "$(BLUE)Building MkDocs Docker image with plugins...$(NC)"
@docker build -t roadwave-mkdocs -f docs/docker/mkdocs.Dockerfile . -q
@echo "$(BLUE)Generating BDD documentation from Gherkin files...$(NC)" @echo "$(BLUE)Generating BDD documentation from Gherkin files...$(NC)"
@python3 docs/scripts/generate-bdd-docs.py @python3 scripts/generate-bdd-docs.py
@echo "$(GREEN)✓ BDD documentation generated$(NC)" @echo "$(GREEN)✓ BDD documentation generated$(NC)"
@echo "$(BLUE)Starting documentation server...$(NC)" @echo "$(BLUE)Starting documentation server...$(NC)"
@echo "$(YELLOW)Documentation available at http://localhost:8000$(NC)" @echo "$(YELLOW)Documentation available at http://localhost:8000$(NC)"
@docker run --rm -p 8000:8000 -v "$(PWD):/docs" roadwave-mkdocs @docker run --rm -it -p 8000:8000 -v "$(PWD):/docs" squidfunk/mkdocs-material
## bdd-docs: Generate BDD docs from Gherkin and serve with MkDocs (http://localhost:8000) ## bdd-docs: Generate BDD docs from Gherkin and serve with MkDocs (http://localhost:8000)
bdd-docs: bdd-docs:
@echo "$(BLUE)Building MkDocs Docker image with plugins...$(NC)"
@docker build -t roadwave-mkdocs -f docs/docker/mkdocs.Dockerfile . -q
@echo "$(BLUE)Generating BDD documentation from Gherkin files...$(NC)" @echo "$(BLUE)Generating BDD documentation from Gherkin files...$(NC)"
@python3 docs/scripts/generate-bdd-docs.py @python3 scripts/generate-bdd-docs.py
@echo "$(GREEN)✓ BDD documentation generated$(NC)" @echo "$(GREEN)✓ BDD documentation generated$(NC)"
@echo "$(BLUE)Starting documentation server...$(NC)" @echo "$(BLUE)Starting documentation server...$(NC)"
@echo "$(YELLOW)Documentation available at http://localhost:8000$(NC)" @echo "$(YELLOW)Documentation available at http://localhost:8000$(NC)"
@echo "$(YELLOW)Navigate to 'Tests BDD' section$(NC)" @echo "$(YELLOW)Navigate to 'Tests BDD' section$(NC)"
@docker run --rm -p 8000:8000 -v "$(PWD):/docs" roadwave-mkdocs @docker run --rm -it -p 8000:8000 -v "$(PWD):/docs" squidfunk/mkdocs-material
## docs-pdf: Generate PDF of all documentation (output/RoadWave_Documentation.pdf) ## docs-pdf: Generate PDF of all documentation (output/RoadWave_Documentation.pdf)
docs-pdf: docs-pdf:
@echo "$(BLUE)Generating BDD documentation from Gherkin files...$(NC)" @echo "$(BLUE)Generating BDD documentation from Gherkin files...$(NC)"
@python3 docs/scripts/generate-bdd-docs.py @python3 scripts/generate-bdd-docs.py
@echo "$(GREEN)✓ BDD documentation generated$(NC)" @echo "$(GREEN)✓ BDD documentation generated$(NC)"
@echo "$(BLUE)Building PDF generator Docker image...$(NC)" @echo "$(BLUE)Building PDF generator Docker image...$(NC)"
@docker build -t roadwave-pdf-generator -f docs/docker/pdf.Dockerfile . -q @docker build -t roadwave-pdf-generator -f scripts/Dockerfile.pdf . -q
@echo "$(BLUE)Generating PDF documentation...$(NC)" @echo "$(BLUE)Generating PDF documentation...$(NC)"
@docker run --rm -u $(shell id -u):$(shell id -g) -v "$(PWD):/docs" roadwave-pdf-generator @docker run --rm -u $(shell id -u):$(shell id -g) -v "$(PWD):/docs" roadwave-pdf-generator
@echo "$(GREEN)✓ PDF generated: output/RoadWave_Documentation.pdf$(NC)" @echo "$(GREEN)✓ PDF generated: output/RoadWave_Documentation.pdf$(NC)"

View File

@@ -1,37 +1,31 @@
# RoadWave - Architecture Technique # RoadWave - Architecture Technique
> Les décisions techniques sont documentées dans [adr/](adr/) > Les décisions techniques sont documentées dans [docs/adr/](docs/adr/)
## Stack Technologique ## Stack Technologique
| Composant | Technologie | ADR | | Composant | Technologie | ADR |
|-----------|-------------|-----| |-----------|-------------|-----|
| **Backend** | Go + Fiber | [ADR-001](adr/001-langage-backend.md) | | **Backend** | Go + Fiber | [ADR-001](docs/adr/001-langage-backend.md) |
| **Architecture Backend** | Monolithe Modulaire | [ADR-010](adr/010-architecture-backend.md) | | **Architecture Backend** | Monolithe Modulaire | [ADR-012](docs/adr/012-architecture-backend.md) |
| **Librairies Backend** | Fiber, pgx, rueidis, sqlc, etc. | [ADR-018](adr/018-librairies-go.md) | | **Authentification** | Zitadel (self-hosted OVH) | [ADR-008](docs/adr/008-authentification.md) |
| **Authentification** | Zitadel (self-hosted OVH) | [ADR-008](adr/008-authentification.md) | | **Streaming** | HLS | [ADR-002](docs/adr/002-protocole-streaming.md) |
| **Streaming** | HLS | [ADR-002](adr/002-protocole-streaming.md) | | **Codec** | Opus | [ADR-003](docs/adr/003-codec-audio.md) |
| **Codec** | Opus | [ADR-003](adr/003-codec-audio.md) | | **CDN** | NGINX Cache (OVH VPS) | [ADR-004](docs/adr/004-cdn.md) |
| **CDN** | NGINX Cache (OVH VPS) | [ADR-004](adr/004-cdn.md) | | **Storage** | OVH Object Storage | [ADR-004](docs/adr/004-cdn.md) |
| **Storage** | OVH Object Storage | [ADR-004](adr/004-cdn.md) | | **Hébergement MVP** | OVH VPS Essential | [ADR-017](docs/adr/017-hebergement.md) |
| **Hébergement MVP** | OVH VPS Essential | [ADR-015](adr/015-hebergement.md) | | **Organisation** | Monorepo | [ADR-016](docs/adr/016-organisation-monorepo.md) |
| **Organisation** | Monorepo | [ADR-014](adr/014-organisation-monorepo.md) | | **Base de données** | PostgreSQL + PostGIS | [ADR-005](docs/adr/005-base-de-donnees.md) |
| **Base de données** | PostgreSQL + PostGIS | [ADR-005](adr/005-base-de-donnees.md) | | **ORM/Accès données** | sqlc | [ADR-013](docs/adr/013-orm-acces-donnees.md) |
| **ORM/Accès données** | sqlc | [ADR-011](adr/011-orm-acces-donnees.md) | | **Cache** | Redis Cluster | [ADR-005](docs/adr/005-base-de-donnees.md) |
| **Cache** | Redis Cluster | [ADR-021](adr/021-solution-cache.md) | | **Chiffrement** | TLS 1.3 | [ADR-006](docs/adr/006-chiffrement.md) |
| **Chiffrement** | TLS 1.3 | [ADR-006](adr/006-chiffrement.md) | | **Live** | WebRTC | [ADR-002](docs/adr/002-protocole-streaming.md) |
| **Live** | WebRTC | [ADR-002](adr/002-protocole-streaming.md) | | **Frontend Mobile** | Flutter | [ADR-014](docs/adr/014-frontend-mobile.md) |
| **Frontend Mobile** | Flutter | [ADR-012](adr/012-frontend-mobile.md) | | **Tests** | Testify + Godog (Gherkin) | [ADR-015](docs/adr/015-strategie-tests.md), [ADR-007](docs/adr/007-tests-bdd.md) |
| **Tests** | Testify + Godog (Gherkin) | [ADR-013](adr/013-strategie-tests.md), [ADR-007](adr/007-tests-bdd.md) | | **Paiements** | Mangopay | [ADR-009](docs/adr/009-solution-paiement.md) |
| **Paiements** | Mangopay | [ADR-009](adr/009-solution-paiement.md) | | **Emailing** | Brevo | [ADR-018](docs/adr/018-service-emailing.md) |
| **Emailing** | Brevo | [ADR-016](adr/016-service-emailing.md) | | **Commandes volant** | Like automatique | [ADR-010](docs/adr/010-commandes-volant.md) |
| **Géolocalisation IP** | IP2Location (fallback) | [ADR-019](adr/019-geolocalisation-ip.md) | | **Conformité stores** | CarPlay, Android Auto, App/Play Store | [ADR-011](docs/adr/011-conformite-stores-carplay-android-auto.md) |
| **Librairies Mobile** | Flutter packages | [ADR-020](adr/020-librairies-flutter.md) |
| **CI/CD** | GitHub Actions (monorepo) | [ADR-022](adr/022-strategie-cicd-monorepo.md) |
| **Modération** | Architecture modération | [ADR-023](adr/023-architecture-moderation.md) |
| **Monitoring** | Prometheus + Grafana | [ADR-024](adr/024-monitoring-observabilite.md) |
| **Secrets** | Vault + sealed secrets | [ADR-025](adr/025-securite-secrets.md) |
| **Notifications** | Push géolocalisées (FCM/APNS + geofencing) | [ADR-017](adr/017-notifications-geolocalisees.md) |
--- ---

View File

@@ -4,15 +4,15 @@ services:
# Backend API # Backend API
api: api:
build: build:
context: . context: ./backend
dockerfile: docker/dev.Dockerfile dockerfile: Dockerfile.dev
ports: ports:
- "8080:8080" - "8080:8080"
volumes: volumes:
- .:/app - ./backend:/app
- /app/tmp - /app/tmp
env_file: env_file:
- ./.env - ./backend/.env
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy

View File

@@ -1,462 +0,0 @@
# Guide de Contribution à la Documentation
Bienvenue ! Ce guide explique comment contribuer à la documentation RoadWave tout en respectant son architecture et ses conventions.
## Principes Fondamentaux
### 1. Source Unique de Vérité (Single Source of Truth)
Chaque information doit exister à **un seul endroit** :
- **Règles métier** → `docs/domains/[domain]/rules/*.md`
- **Features BDD** → `domains/[domain]/features/**/*.feature` (sources Gherkin)
- **Documentation BDD** → `docs/generated/bdd/` (générée automatiquement)
- **Entités** → `docs/domains/[domain]/entities/*.md`
- **États** → `docs/domains/[domain]/states/*.md`
- **Séquences** → `docs/domains/[domain]/sequences/*.md`
**Ne jamais** dupliquer une information
**Toujours** référencer via des liens
### 2. Langue : 100% Français
Tous les fichiers de documentation doivent être en français :
- ✅ Noms de fichiers en français (kebab-case)
- ✅ Contenu en français
- ✅ Navigation `mkdocs.yml` en français
- ⚠️ Exception : code backend en anglais (convention internationale)
### 3. Dossier `generated/` = Read-Only
Le dossier `docs/generated/` contient **exclusivement** des fichiers générés automatiquement.
**Ne JAMAIS** créer de fichiers manuels dans `generated/`
**Ne JAMAIS** éditer des fichiers dans `generated/`
✅ Modifier les sources (`.feature`) et regénérer
## Ajouter une Nouvelle Feature BDD
### Étape 1 : Créer le fichier `.feature`
```bash
# Créer dans le bon domaine
touch domains/[domain]/features/[category]/ma-feature.feature
```
**Exemple** : Feature de recherche dans le domaine recommendation
```bash
touch domains/recommendation/features/recherche/recherche-avancee.feature
```
### Étape 2 : Écrire la feature en Gherkin
```gherkin
# language: fr
Fonctionnalité: Recherche avancée de contenus
En tant qu'utilisateur
Je veux rechercher des contenus avec des filtres avancés
Afin de trouver exactement ce que je cherche
Contexte:
Étant donné que je suis connecté
Et que je suis sur la page de recherche
Scénario: Recherche par catégorie et distance
Quand je sélectionne la catégorie "Tourisme"
Et je définis un rayon de 10 km
Alors je vois uniquement les contenus de type "Tourisme"
Et tous les résultats sont à moins de 10 km
Scénario: Recherche avec filtres multiples
# ... autres scénarios
```
**Conventions Gherkin** :
- Langue française (`# language: fr`)
- Mots-clés : `Fonctionnalité`, `Scénario`, `Étant donné`, `Quand`, `Alors`
- Commentaires pour référencer les règles DDD
### Étape 3 : Regénérer la documentation
```bash
make bdd-docs
```
Cette commande :
1. Parse les fichiers `.feature`
2. Génère les `.md` dans `generated/bdd/`
3. Démarre le serveur MkDocs (http://localhost:8000)
### Étape 4 : Ajouter dans `mkdocs.yml`
Si la catégorie n'existe pas encore, l'ajouter :
```yaml
- '🎯 Recommendation':
# ...
- Tests BDD:
- 'Recherche':
- Recherche de contenu: generated/bdd/recommendation/features/recherche/recherche.md
- Recherche avancée: generated/bdd/recommendation/features/recherche/recherche-avancee.md # ← NOUVEAU
```
### Étape 5 : Commit
```bash
git add domains/recommendation/features/recherche/recherche-avancee.feature
git add mkdocs.yml
git commit -m "feat(bdd): ajouter feature recherche avancée
- Ajout recherche par catégorie et distance
- Ajout filtres multiples
- Couverture domaine recommendation"
```
## Ajouter une Règle Métier
### Étape 1 : Identifier le domaine
Déterminer dans quel domaine DDD se situe la règle :
- **Shared** : Authentification, RGPD, erreurs, profil
- **Recommendation** : Algorithme, jauges d'intérêt, recherche
- **Content** : Audio-guides, création, streaming
- **Moderation** : Signalements, validation, sanctions
- **Advertising** : Publicités, ciblage
- **Premium** : Abonnements, mode offline
- **Monetization** : Paiements, revenus créateurs
### Étape 2 : Créer ou éditer le fichier de règles
```bash
# Éditer fichier existant
vim domains/[domain]/rules/[theme].md
# Ou créer un nouveau fichier
touch domains/[domain]/rules/nouveau-theme.md
```
### Étape 3 : Documenter la règle
```markdown
# Règles - Nouveau Thème
## Contexte
Description du contexte métier...
## Règles
### Règle 1 : Titre de la règle
**Contexte** : Quand...
**Règle** : L'utilisateur doit/peut/ne peut pas...
**Justification** : Parce que...
**Exemple** :
- Cas nominal : ...
- Cas d'erreur : ...
### Règle 2 : Autre règle
...
## Liens avec d'autres domaines
- [Entité X](../entities/entite-x.md)
- [Séquence Y](../sequences/sequence-y.md)
- [ADR-XXX](../../adr/XXX-decision.md)
```
### Étape 4 : Créer les features BDD correspondantes
Les règles métier doivent être testables → créer des features BDD.
### Étape 5 : Ajouter dans `mkdocs.yml`
```yaml
- '🎙️ Content':
- Règles:
- Création & Publication: domains/content/rules/creation-publication.md
- Nouveau Thème: domains/content/rules/nouveau-theme.md # ← NOUVEAU
```
## Ajouter une Entité
### Créer le fichier
```bash
touch domains/[domain]/entities/nouvelle-entite.md
```
### Documenter l'entité
```markdown
# Entité : NouvelleThing
## Description
Brève description de l'entité...
## Attributs
| Attribut | Type | Description | Contraintes |
|----------|------|-------------|-------------|
| `id` | UUID | Identifiant unique | PK, NOT NULL |
| `name` | String | Nom de la chose | NOT NULL, max 255 |
| `created_at` | Timestamp | Date de création | NOT NULL |
## Relations
- **BelongsTo** : [User](../../../_shared/entities/users.md)
- **HasMany** : [Items](./items.md)
## États du Cycle de Vie
Voir [Lifecycle](../states/nouvelle-thing-lifecycle.md)
## Règles Métier
- [Règle de création](../rules/creation.md#nouvelle-thing)
- [Règle de validation](../rules/validation.md#nouvelle-thing)
## Implémentation Backend
**Module** : `backend/internal/[domain]/`
**Table** : `nouvelle_things`
**Repository** : `nouvelle_thing_repository.go`
Requêtes sqlc : `backend/queries/nouvelle_thing.sql`
```
### Ajouter dans `mkdocs.yml`
```yaml
- Entités:
- Vue d'ensemble: domains/[domain]/entities/vue-ensemble.md
- Nouvelle Entité: domains/[domain]/entities/nouvelle-entite.md # ← NOUVEAU
```
## Ajouter un Diagramme
### Diagrammes de Séquence
```bash
touch domains/[domain]/sequences/nouveau-processus.md
```
```markdown
# Séquence - Nouveau Processus
## Diagramme
\`\`\`mermaid
sequenceDiagram
participant U as Utilisateur
participant A as API
participant DB as Database
U->>A: POST /nouvelle-action
A->>DB: Vérifier permissions
DB-->>A: OK
A->>DB: INSERT nouvelle_thing
A-->>U: 201 Created
\`\`\`
## Légende
**Acteurs** :
- Utilisateur : Client de l'API
- API : Backend RoadWave
- Database : PostgreSQL
**Étapes clés** :
1. Vérification des permissions
2. Insertion en base
3. Retour confirmation
## Cas d'Erreur
### Permissions insuffisantes
...
### Contrainte violée
...
```
### Diagrammes d'États
```bash
touch domains/[domain]/states/nouvelle-thing-lifecycle.md
```
```markdown
# États - Lifecycle NouvelleThing
## Diagramme
\`\`\`mermaid
stateDiagram-v2
[*] --> Draft
Draft --> Pending: Submit
Pending --> Approved: Approve
Pending --> Rejected: Reject
Approved --> Published: Publish
Published --> Archived: Archive
Archived --> [*]
\`\`\`
## États
| État | Description | Transitions Possibles |
|------|-------------|----------------------|
| Draft | Création en cours | → Pending |
| Pending | En attente validation | → Approved, Rejected |
| Approved | Validé | → Published |
| Rejected | Refusé | → Draft (correction) |
| Published | Publié | → Archived |
| Archived | Archivé | (final) |
## Règles de Transition
### Draft → Pending
**Conditions** : Tous les champs obligatoires remplis
**Actions** : Notification modérateurs
```
## Conventions de Nommage
### Fichiers Markdown
```bash
# Format : kebab-case-francais.md
✅ export-donnees.md
✅ suppression-compte.md
✅ moderation-communautaire.md
✅ vue-ensemble.md
❌ data-export.md (anglais)
❌ entities-overview.md (anglais)
❌ ExportDonnees.md (PascalCase)
❌ export_donnees.md (snake_case)
```
### Fichiers Features
```bash
# Format : kebab-case-francais.feature
✅ signalement.feature
✅ jauge-initiale.feature
✅ creation-audio-guide.feature
❌ reporting.feature (anglais)
❌ initial-gauge.feature (anglais)
```
### Répertoires
```bash
# Format : kebab-case-anglais (convention internationale)
✅ features/
✅ interest-gauges/
✅ content-creation/
✅ rgpd-compliance/
```
## Vérifications Avant Commit
### Checklist
- [ ] Tous les fichiers sont en français (sauf code backend)
- [ ] Pas de fichiers dans `generated/` (seulement sources modifiées)
- [ ] Les liens internes fonctionnent
- [ ] `mkdocs.yml` mis à jour si nécessaire
- [ ] Features BDD regénérées (`make bdd-docs`)
- [ ] Message de commit descriptif
### Tester Localement
```bash
# Vérifier syntaxe YAML
python3 -c "import yaml; yaml.safe_load(open('mkdocs.yml'))"
# Tester la navigation
make docs-serve
# → http://localhost:8000
```
### Vérifier les Liens
```bash
# Chercher liens cassés vers anciens noms
grep -r "entities-overview\.md" docs/
grep -r "user-account-lifecycle\.md" docs/
grep -r "data-export\.md" docs/
# Ne devrait rien retourner
```
## Messages de Commit
### Format
```
type(scope): description courte
Corps optionnel avec détails...
Refs: #issue-number
```
### Types
- `feat(bdd)` : Nouvelle feature BDD
- `docs(rules)` : Nouvelle règle métier
- `docs(entity)` : Nouvelle entité
- `fix(link)` : Correction de lien
- `refactor(nav)` : Réorganisation navigation
### Exemples
```bash
# Feature BDD
git commit -m "feat(bdd): ajouter recherche avancée dans recommendation"
# Règle métier
git commit -m "docs(rules): documenter règles de modération préventive"
# Entité
git commit -m "docs(entity): ajouter entité Campaign pour advertising"
# Fix
git commit -m "fix(link): corriger lien vers vue-ensemble.md"
```
## Ressources
- **Architecture DDD** : [domains/README.md](domains/README.md)
- **Tests BDD** : [ADR-007](adr/007-tests-bdd.md)
- **ADRs** : [adr/README.md](adr/README.md)
## Support
Questions ? Consultez :
1. Cette documentation
2. Les ADRs existants
3. Les exemples dans les domaines existants
4. L'équipe RoadWave
## Auteurs
- Équipe RoadWave
- Claude Sonnet 4.5 (documentation)
Dernière mise à jour : 2026-02-08

View File

@@ -0,0 +1,849 @@
# 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)

View File

@@ -1,282 +0,0 @@
# RoadWave
Réseau social audio géolocalisé pour les usagers de la route.
## Concept
RoadWave permet aux conducteurs d'écouter du contenu audio contextuel pendant leurs trajets. La navigation se fait par commandes au volant (suivant/précédent), inspirée des réseaux à scroll infini.
Le contenu est diffusé en fonction de la position géographique de l'utilisateur et de ses centres d'intérêt.
---
## Cas d'usage
| Utilisateur | Scénario |
|-------------|----------|
| **Conducteur** | Écoute contenu audio en conduisant, navigation par commandes au volant (suivant/précédent), reçoit notifications géolocalisées en passant près de points d'intérêt |
| **Routier** | Écoute podcasts et radios live pendant ses trajets longue distance |
| **Touriste à pied** | Visite guidée audio d'un musée, monument ou ville : choisit parmi plusieurs guides, navigue entre séquences à son rythme (tactile/vocal), reçoit notification push quand un audio-guide est disponible à proximité |
| **Commerçant** | Diffuse une publicité audio ciblée GPS devant son commerce |
| **Passionné auto** | Découvre du contenu automobile près de circuits ou concessionnaires |
| **Habitant local** | Partage anecdotes ou bons plans géolocalisés dans son quartier |
| **Média traditionnel** | Le Monde, Le Parisien diffusent actualités géolocalisées ou nationales |
---
## Utilisateurs
Tout utilisateur peut écouter et créer du contenu (rôle flexible).
| Rôle | Description |
|------|-------------|
| **Auditeur** | Écoute, like, s'abonne à des créateurs, signale des contenus |
| **Créateur** | Publie du contenu audio géolocalisé (individus, médias traditionnels) |
| **Publicitaire** | Diffuse des publicités ciblées géographiquement |
| **Modérateur** | Valide et modère les contenus signalés |
---
## Types de contenu
| Type | Description |
|------|-------------|
| **Contenu court** | Audio de quelques secondes à quelques minutes |
| **Podcast** | Épisodes plus longs, séries thématiques |
| **Radio live** | Diffusion en direct avec synchronisation approximative entre auditeurs |
| **Audio-guide** | Visite guidée multiséquence (musée, monument, ville) : plusieurs séquences numérotées, navigation manuelle entre pistes, liste complète visible, guidage vocal entre points d'intérêt |
---
## Géolocalisation
Le créateur définit la zone de diffusion de son contenu :
| Niveau | Portée |
|--------|--------|
| **Point GPS** | Rayon précis autour d'une coordonnée |
| **Ville** | Diffusion dans une ville |
| **Département** | Diffusion départementale |
| **Région** | Diffusion régionale |
| **Pays** | Diffusion nationale |
**Priorité de diffusion** : plus la zone est précise, plus le contenu a de chances d'être diffusé (GPS > ville > département > région > pays).
---
## Algorithme de recommandation
Le contenu proposé est calculé via un **score combiné** :
- **Proximité géographique** : distance entre l'utilisateur et la zone du contenu
- **Pertinence des intérêts** : correspondance avec les centres d'intérêt de l'utilisateur
Lorsque plusieurs contenus sont disponibles dans une zone, **seul le plus pertinent est diffusé**.
---
## Centres d'intérêt
Chaque utilisateur possède des **jauges d'intérêt** qui évoluent dynamiquement :
### Catégories
- Automobile
- Voyage
- Famille
- Amour
- Musique
- Économie
- Cryptomonnaie
- Politique
- *... (extensible)*
### Évolution des jauges
| Action | Effet |
|--------|-------|
| Temps d'écoute long | Augmente la jauge |
| Like | Augmente la jauge |
| Abonnement | Augmente fortement la jauge |
| Skip rapide | Diminue la jauge |
Les créateurs taguent leur contenu avec des centres d'intérêt. L'algorithme privilégie les correspondances mais n'exclut pas les utilisateurs sans correspondance.
---
## Interactions
### Commandes au volant (conduite)
Interactions simplifiées pour sécurité routière maximale :
| Commande | Action |
|----------|--------|
| **Suivant** | Passer au contenu suivant |
| **Précédent** | Revenir au contenu précédent |
| **Play/Pause** | Mettre en pause / reprendre la lecture |
**Like automatique** : Le système détecte automatiquement vos préférences selon votre temps d'écoute :
- Écoute ≥80% du contenu → Like renforcé (+2 points jauge)
- Écoute 30-79% du contenu → Like standard (+1 point jauge)
- Skip après <10s → Signal négatif (-0.5 point)
---
## Publicités
- Insertion **entre deux contenus** uniquement (jamais d'interruption)
- Ciblage géographique : point GPS, ville, département, région ou national
- Interface dédiée pour les publicitaires
---
## Radio live
- Diffusion en direct par des créateurs
- **Buffering** pour garantir une écoute fluide
- **Synchronisation approximative** entre les auditeurs (quelques secondes de décalage possible)
---
## Modération
Approche hybride combinant participation communautaire, IA et modérateurs dédiés.
### Contenus prohibés
| Catégorie | Description |
|-----------|-------------|
| **Haine et violence** | Incitation à la haine, violence, discrimination |
| **Contenu sexuel** | Pornographie ou contenu sexuellement explicite |
| **Illégalité** | Apologie du terrorisme, actes criminels |
| **Désinformation dangereuse** | Fausses informations sur la santé, sécurité routière |
| **Harcèlement** | Menaces, intimidation, doxxing |
| **Droits d'auteur** | Violation de propriété intellectuelle |
| **Fraude** | Arnaques, escroqueries |
### Rôles de modération
| Rôle | Capacités |
|------|-----------|
| **Auditeur lambda** | Signaler un contenu (1 clic) |
| **Auditeur de confiance** | Signalements priorisés après historique positif |
| **Modérateur junior** | Traiter signalements simples (spam, contenu évident) |
| **Modérateur senior** | Cas complexes, appels, décisions de ban |
| **Admin modération** | Définir les règles, superviser l'équipe |
### Flux de modération
```
1. Auditeur signale → File d'attente
2. IA pré-filtre → Cas évidents traités automatiquement
3. Modérateur junior → Traite 80% des cas restants
4. Modérateur senior → Cas complexes + recours
```
### Outils de modération automatique
| Outil | Fonction |
|-------|----------|
| **Transcription audio** | Conversion automatique en texte pour analyse |
| **Analyse vocale IA** | Détection de ton agressif, cris, insultes |
| **Empreinte audio** | Détection de contenus déjà modérés (réupload) |
| **Détection droits d'auteur** | Identification automatique de musique protégée |
| **Filtrage mots-clés** | Liste noire de termes inappropriés |
### Modération préventive
- **Nouveaux créateurs** : validation manuelle des 3 premiers contenus
- **Score de confiance** : évolution selon l'historique du créateur
- **Publicités** : validation manuelle obligatoire avant diffusion
### Système de strikes
| Strike | Sanction | Durée suspension |
|--------|----------|------------------|
| **Strike 1** | Avertissement + suppression contenu + suspension upload | 3 jours |
| **Strike 2** | Suppression contenu + suspension upload | 7 jours |
| **Strike 3** | Suppression contenu + suspension upload | 30 jours |
| **Strike 4** | Ban définitif du compte créateur | Permanent |
**Notes** :
- **Tolérance 1ère fois** (droits d'auteur uniquement) : avertissement sans strike
- **Violations graves** (haine, illégalité, violence) : strike immédiat sans tolérance
- **Réhabilitation** : -1 strike tous les 6 mois sans nouvelle violation
### Priorisation des signalements
| Priorité | Type de contenu |
|----------|-----------------|
| **CRITIQUE** | Violence, suicide, mise en danger immédiate |
| **HAUTE** | Harcèlement, haine, désinformation |
| **MOYENNE** | Spam, contenu inapproprié |
| **BASSE** | Qualité audio, tags incorrects |
### Transparence et recours
- **Notification explicite** lors de suppression (raison détaillée)
- **Processus d'appel** : le créateur peut contester une décision
- **Délai de traitement** : 48-72h pour les recours
- **Historique** : tableau de bord des sanctions pour le créateur
### Modération communautaire
- **Utilisateurs de confiance** : signalements priorisés après historique positif
- **Récompenses** : badges, réduction premium pour signalements pertinents
- Lutte contre les signalements abusifs (sanctions possibles)
---
## Modèle économique
### Offres
| Formule | Description |
|---------|-------------|
| **Gratuit** | Accès complet avec publicités entre les contenus |
| **Premium** | Sans publicité + accès aux contenus exclusifs |
### Monétisation créateurs
- **Partage des revenus pub** : rémunération basée sur le nombre d'écoutes
- **Pourboires** : les auditeurs peuvent faire des dons aux créateurs
---
## Conformité RGPD
### Permissions GPS
RoadWave adapte ses permissions selon l'usage :
| Mode | Permission GPS | Fonctionnalités |
|------|----------------|-----------------|
| **Mode voiture** | "When In Use" uniquement | Écoute contenu géolocalisé pendant conduite |
| **Mode piéton** | "Always Location" optionnelle | Notifications audio-guides en arrière-plan |
**Important** : La permission "Always Location" est **optionnelle** et réservée au mode piéton pour recevoir des notifications push lorsqu'un audio-guide est disponible à proximité. Elle n'est jamais requise pour l'usage principal en voiture.
### Données collectées
| Donnée | Finalité | Base légale |
|--------|----------|-------------|
| **Position GPS** | Diffusion de contenu géolocalisé | Consentement |
| **Historique d'écoute** | Personnalisation des recommandations | Intérêt légitime |
| **Centres d'intérêt** | Algorithme de recommandation | Consentement |
| **Identité créateur** | Publication de contenu | Exécution du contrat |
### Droits des utilisateurs
- **Accès** : consulter toutes ses données personnelles
- **Rectification** : modifier ses informations
- **Suppression** : supprimer son compte et toutes ses données
- **Portabilité** : exporter ses données dans un format standard
- **Opposition** : désactiver le profilage publicitaire
### Mesures techniques
- Consentement explicite requis pour la géolocalisation
- Anonymisation des données de localisation après 24h (sauf historique personnel)
- Possibilité d'utiliser l'app en mode dégradé (sans géolocalisation précise)
- Données hébergées dans l'UE

View File

@@ -47,7 +47,6 @@ Streaming audio vers des utilisateurs mobiles en voiture, avec réseaux instable
La latence HLS (5-30s) entre en conflit avec les notifications géolocalisées qui doivent déclencher l'audio **au moment précis** où l'utilisateur atteint un point d'intérêt. La latence HLS (5-30s) entre en conflit avec les notifications géolocalisées qui doivent déclencher l'audio **au moment précis** où l'utilisateur atteint un point d'intérêt.
**Exemple critique** : **Exemple critique** :
- Utilisateur en voiture à 90 km/h (25 m/s) - Utilisateur en voiture à 90 km/h (25 m/s)
- ETA de 7 secondes avant le point → notification affichée - ETA de 7 secondes avant le point → notification affichée
- Latence HLS de 15 secondes - Latence HLS de 15 secondes
@@ -70,7 +69,6 @@ La latence HLS (5-30s) entre en conflit avec les notifications géolocalisées q
``` ```
**Stratégie de cache** : **Stratégie de cache** :
- Télécharge les **15 premières secondes** de chaque POI à proximité - Télécharge les **15 premières secondes** de chaque POI à proximité
- Limite : 3 POI simultanés en cache (max ~25 MB) - Limite : 3 POI simultanés en cache (max ~25 MB)
- Purge automatique après 200m de distance passée - Purge automatique après 200m de distance passée
@@ -154,19 +152,16 @@ Si le pre-buffer échoue (réseau faible, pas de cache), afficher un **loader av
### Impact sur l'Infrastructure ### Impact sur l'Infrastructure
#### Backend (Go) #### Backend (Go)
- **Nouveau service** : `audiocache.Service` pour préparer les extraits M4A - **Nouveau service** : `audiocache.Service` pour préparer les extraits M4A
- **Endpoint** : `GET /api/v1/audio/poi/:id/intro` (retourne 15s d'audio) - **Endpoint** : `GET /api/v1/audio/poi/:id/intro` (retourne 15s d'audio)
- **CDN** : Cache NGINX avec TTL 7 jours sur `/audio/*/intro.m4a` - **CDN** : Cache NGINX avec TTL 7 jours sur `/audio/*/intro.m4a`
#### Mobile (Flutter) #### Mobile (Flutter)
- **Package** : `just_audio` avec cache local (`flutter_cache_manager`) - **Package** : `just_audio` avec cache local (`flutter_cache_manager`)
- **Stockage** : Max 100 MB de cache audio (auto-purge LRU) - **Stockage** : Max 100 MB de cache audio (auto-purge LRU)
- **Logique** : `PreBufferService` avec scoring de priorité POI - **Logique** : `PreBufferService` avec scoring de priorité POI
#### Coûts #### Coûts
- **Bande passante** : +10-15 MB/utilisateur/session (vs streaming pur) - **Bande passante** : +10-15 MB/utilisateur/session (vs streaming pur)
- **Stockage CDN** : +500 MB pour 1000 POI × 5 MB intro (négligeable) - **Stockage CDN** : +500 MB pour 1000 POI × 5 MB intro (négligeable)
- **Économie** : Cache CDN réduit les requêtes origin (-60% selon tests) - **Économie** : Cache CDN réduit les requêtes origin (-60% selon tests)
@@ -182,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) - [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) - [Low-Latency HLS (LL-HLS)](https://developer.apple.com/documentation/http_live_streaming/enabling_low-latency_hls)
- Règle Métier 05 : Section 5.1 (File d'attente et commande Suivant) - Règle Métier 05 : Section 5.2 (Mode Voiture, lignes 16-84)
- Règle Métier 17 : Section 17.2 (ETA Géolocalisé, lignes 25-65) - Règle Métier 17 : Section 17.2 (ETA Géolocalisé, lignes 25-65)
- **ADR-017** : Architecture des Notifications Géolocalisées - **ADR-017** : Architecture des Notifications Géolocalisées

View File

@@ -44,14 +44,12 @@ flowchart LR
## Justification ## Justification
### PostgreSQL + PostGIS ### PostgreSQL + PostGIS
- Requêtes géospatiales complexes et précises - Requêtes géospatiales complexes et précises
- Index GIST pour performance - Index GIST pour performance
- ACID, fiabilité éprouvée - ACID, fiabilité éprouvée
- Écosystème mature - Écosystème mature
### PgBouncer ### PgBouncer
- **Connection pooling** : Réduit l'overhead de création de connexions PostgreSQL - **Connection pooling** : Réduit l'overhead de création de connexions PostgreSQL
- **Mode transaction** : Connexion réutilisée entre transactions (optimal pour API stateless) - **Mode transaction** : Connexion réutilisée entre transactions (optimal pour API stateless)
- **Performance** : Permet de gérer 1000+ connexions concurrentes avec ~100 connexions réelles à PostgreSQL - **Performance** : Permet de gérer 1000+ connexions concurrentes avec ~100 connexions réelles à PostgreSQL
@@ -59,7 +57,6 @@ flowchart LR
- **Port** : :6432 (vs :5432 pour PostgreSQL direct) - **Port** : :6432 (vs :5432 pour PostgreSQL direct)
### Redis ### Redis
- Cache géo natif (`GEORADIUS`) : 100K+ requêtes/sec - Cache géo natif (`GEORADIUS`) : 100K+ requêtes/sec
- Sessions utilisateurs - Sessions utilisateurs
- Pub/sub pour temps réel - Pub/sub pour temps réel
@@ -84,13 +81,11 @@ LIMIT 20;
### Configuration PgBouncer ### Configuration PgBouncer
**Mode recommandé** : `transaction` **Mode recommandé** : `transaction`
- Connexion libérée après chaque transaction - Connexion libérée après chaque transaction
- Optimal pour API stateless (Go + Fiber) - Optimal pour API stateless (Go + Fiber)
- Maximise la réutilisation des connexions - Maximise la réutilisation des connexions
**Pool sizing** : **Pool sizing** :
- `default_pool_size` : 20 (connexions par base) - `default_pool_size` : 20 (connexions par base)
- `max_client_conn` : 1000 (connexions clients max) - `max_client_conn` : 1000 (connexions clients max)
- `reserve_pool_size` : 5 (connexions de secours) - `reserve_pool_size` : 5 (connexions de secours)
@@ -120,3 +115,8 @@ server_idle_timeout = 600
// Après (via PgBouncer) // Après (via PgBouncer)
dsn := "postgres://user:pass@localhost:6432/roadwave" 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)

View File

@@ -67,13 +67,11 @@ features/
**Exécuté par** : Backend (Godog + step definitions Go dans `/backend/tests/bdd/`) **Exécuté par** : Backend (Godog + step definitions Go dans `/backend/tests/bdd/`)
**Exemples de scénarios** : **Exemples de scénarios** :
- `POST /api/v1/auth/register` avec email invalide → retourne 400 - `POST /api/v1/auth/register` avec email invalide → retourne 400
- `GET /api/v1/contents/nearby` avec rayon 500m → retourne POI triés par distance - `GET /api/v1/contents/nearby` avec rayon 500m → retourne POI triés par distance
- `DELETE /api/v1/user/account` → supprime données RGPD conformément - `DELETE /api/v1/user/account` → supprime données RGPD conformément
**Caractéristiques** : **Caractéristiques** :
- Focus sur la **logique métier backend** (algorithme, validation, persistance) - Focus sur la **logique métier backend** (algorithme, validation, persistance)
- Pas d'interface utilisateur - Pas d'interface utilisateur
- Testable via requêtes HTTP directes - Testable via requêtes HTTP directes
@@ -87,13 +85,11 @@ features/
**Exécuté par** : Mobile (Flutter `integration_test` + step definitions Dart dans `/mobile/tests/bdd/`) **Exécuté par** : Mobile (Flutter `integration_test` + step definitions Dart dans `/mobile/tests/bdd/`)
**Exemples de scénarios** : **Exemples de scénarios** :
- Cliquer sur "Lecture" → widget audio player s'affiche avec progress bar - Cliquer sur "Lecture" → widget audio player s'affiche avec progress bar
- Mode piéton activé → carte interactive affichée + bouton "Télécharger zone" - Mode piéton activé → carte interactive affichée + bouton "Télécharger zone"
- Scroll dans liste podcasts → lazy loading déclenche pagination - Scroll dans liste podcasts → lazy loading déclenche pagination
**Caractéristiques** : **Caractéristiques** :
- Focus sur l'**expérience utilisateur mobile** - Focus sur l'**expérience utilisateur mobile**
- Validation visuelle (widgets, animations, navigation) - Validation visuelle (widgets, animations, navigation)
- Mock du backend si nécessaire (tests UI isolés) - Mock du backend si nécessaire (tests UI isolés)
@@ -107,13 +103,11 @@ features/
**Exécuté par** : Backend **ET** Mobile ensemble **Exécuté par** : Backend **ET** Mobile ensemble
**Exemples de scénarios** : **Exemples de scénarios** :
- Abonnement Premium : Formulaire mobile → API Zitadel → API RoadWave → Mangopay → Confirmation UI - Abonnement Premium : Formulaire mobile → API Zitadel → API RoadWave → Mangopay → Confirmation UI
- Erreur réseau : Perte connexion pendant streaming → Fallback mode offline → Reprise auto après reconnexion - Erreur réseau : Perte connexion pendant streaming → Fallback mode offline → Reprise auto après reconnexion
- Notification géolocalisée : GPS détecte POI → Backend calcule recommandation → Push notification → Ouverture app → Lecture audio - Notification géolocalisée : GPS détecte POI → Backend calcule recommandation → Push notification → Ouverture app → Lecture audio
**Caractéristiques** : **Caractéristiques** :
- Tests **cross-composants** (intégration complète) - Tests **cross-composants** (intégration complète)
- Implique souvent des **services tiers** (Zitadel, Mangopay, Firebase) - Implique souvent des **services tiers** (Zitadel, Mangopay, Firebase)
- Validation du **parcours utilisateur de bout en bout** - Validation du **parcours utilisateur de bout en bout**
@@ -123,12 +117,10 @@ features/
### Implémentation Step Definitions ### Implémentation Step Definitions
**Backend** : `/backend/tests/bdd/` **Backend** : `/backend/tests/bdd/`
- Step definitions Go pour features `api/` et `e2e/` - Step definitions Go pour features `api/` et `e2e/`
- Utilise `godog` et packages backend (`service`, `repository`) - Utilise `godog` et packages backend (`service`, `repository`)
**Mobile** : `/mobile/tests/bdd/` **Mobile** : `/mobile/tests/bdd/`
- Step definitions Dart pour features `ui/` et `e2e/` - Step definitions Dart pour features `ui/` et `e2e/`
- Utilise Flutter `integration_test` framework - Utilise Flutter `integration_test` framework

View File

@@ -14,18 +14,16 @@ RoadWave nécessite un système d'authentification sécurisé pour mobile (iOS/A
**Zitadel self-hosted sur OVH France** pour l'IAM avec validation JWT locale côté API Go. **Zitadel self-hosted sur OVH France** pour l'IAM avec validation JWT locale côté API Go.
**Méthode d'authentification** : **Email/Password uniquement** (pas d'OAuth tiers) **Méthode d'authentification** : **Email/Password uniquement** (pas d'OAuth tiers)
- ✅ Authentification native Zitadel (email + mot de passe) - ✅ Authentification native Zitadel (email + mot de passe)
-**Pas de fournisseurs OAuth externes** (Google, Apple, Facebook) -**Pas de fournisseurs OAuth externes** (Google, Apple, Facebook)
- **Protocole** : OAuth2 PKCE (entre app mobile et Zitadel uniquement) - **Protocole** : OAuth2 PKCE (entre app mobile et Zitadel uniquement)
**Architecture de déploiement** : **Architecture de déploiement** :
- Container Docker sur le même VPS OVH (Gravelines, France) que l'API - Container Docker sur le même VPS OVH (Gravelines, France) que l'API
- Base de données PostgreSQL partagée avec RoadWave (séparation logique par schéma) - 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 - 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](../domains/_shared/rules/authentification.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](../regles-metier/01-authentification-inscription.md#11-méthodes-dinscription)).
## Alternatives considérées ## Alternatives considérées
@@ -104,19 +102,17 @@ graph TB
4. App mobile → Go API avec JWT → validation locale 4. App mobile → Go API avec JWT → validation locale
**Ce que nous N'UTILISONS PAS** : **Ce que nous N'UTILISONS PAS** :
- ❌ "Sign in with Google" - ❌ "Sign in with Google"
- ❌ "Sign in with Apple" - ❌ "Sign in with Apple"
- ❌ "Sign in with Facebook" - ❌ "Sign in with Facebook"
- ❌ Aucun autre fournisseur externe - ❌ Aucun autre fournisseur externe
**Pourquoi OAuth2 alors ?** : **Pourquoi OAuth2 alors ?** :
- OAuth2 PKCE est le **standard moderne** pour auth mobile (sécurisé, refresh tokens, etc.) - OAuth2 PKCE est le **standard moderne** pour auth mobile (sécurisé, refresh tokens, etc.)
- Zitadel implémente OAuth2/OIDC comme **protocole**, mais l'auth reste email/password - 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) - 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](../domains/_shared/rules/authentification.md#11-méthodes-dinscription) pour la décision métier. > 📋 **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.
## Exemple d'intégration ## Exemple d'intégration

View File

@@ -199,7 +199,7 @@ ON user_locations USING GIST(last_position);
-**Maintenabilité** : Patterns clairs, réutilisables -**Maintenabilité** : Patterns clairs, réutilisables
-**Complexité** : Une couche de plus, mais justifiée -**Complexité** : Une couche de plus, mais justifiée
**Référence** : Résout incohérence #4 dans **Référence** : Résout incohérence #4 dans [INCONSISTENCIES-ANALYSIS.md](../INCONSISTENCIES-ANALYSIS.md#4--orm-sqlc-vs-types-postgis)
## Conséquences ## Conséquences

View File

@@ -45,7 +45,6 @@ RoadWave nécessite applications iOS et Android avec support CarPlay/Android Aut
- **Cache images** : `cached_network_image` (LRU cache) - **Cache images** : `cached_network_image` (LRU cache)
**Points d'attention** : **Points d'attention** :
- ⚠️ **Permissions progressives requises** pour `geofence_service` et `geolocator` (voir section "Stratégie de Permissions") - ⚠️ **Permissions progressives requises** pour `geofence_service` et `geolocator` (voir section "Stratégie de Permissions")
- ⚠️ **Licences** : 100% permissives (MIT, BSD-3) - voir ADR-020 - ⚠️ **Licences** : 100% permissives (MIT, BSD-3) - voir ADR-020
@@ -54,7 +53,6 @@ RoadWave nécessite applications iOS et Android avec support CarPlay/Android Aut
### Contexte et Enjeux ### Contexte et Enjeux
**Problème** : La géolocalisation en arrière-plan (requise pour le mode piéton) est **très scrutée** par Apple et Google : **Problème** : La géolocalisation en arrière-plan (requise pour le mode piéton) est **très scrutée** par Apple et Google :
- **iOS App Store** : Taux de rejet ~70% si permission "Always Location" mal justifiée - **iOS App Store** : Taux de rejet ~70% si permission "Always Location" mal justifiée
- **Android Play Store** : `ACCESS_BACKGROUND_LOCATION` nécessite déclaration spéciale depuis Android 10 - **Android Play Store** : `ACCESS_BACKGROUND_LOCATION` nécessite déclaration spéciale depuis Android 10
- **RGPD** : Permissions doivent être **optionnelles** et l'app **utilisable sans** - **RGPD** : Permissions doivent être **optionnelles** et l'app **utilisable sans**
@@ -76,7 +74,6 @@ Trois niveaux de permissions doivent être gérés :
**Quand** : Premier lancement de l'app **Quand** : Premier lancement de l'app
**Demande** : `locationWhenInUse` uniquement **Demande** : `locationWhenInUse` uniquement
- iOS : "Allow While Using App" - iOS : "Allow While Using App"
- Android : `ACCESS_FINE_LOCATION` - Android : `ACCESS_FINE_LOCATION`
@@ -139,25 +136,21 @@ Pour vous proposer du contenu audio adapté
Le service de gestion des permissions (`lib/core/services/location_permission_service.dart`) doit implémenter : Le service de gestion des permissions (`lib/core/services/location_permission_service.dart`) doit implémenter :
**Détection du niveau actuel** : **Détection du niveau actuel** :
- Vérifier le statut de la permission `location` (when in use) - Vérifier le statut de la permission `location` (when in use)
- Vérifier le statut de la permission `locationAlways` (background) - Vérifier le statut de la permission `locationAlways` (background)
- Retourner le niveau le plus élevé accordé - Retourner le niveau le plus élevé accordé
**Demande de permission de base** (Étape 1) : **Demande de permission de base** (Étape 1) :
- Demander uniquement la permission `location` (when in use) - Demander uniquement la permission `location` (when in use)
- Utilisée lors de l'onboarding - Utilisée lors de l'onboarding
- Aucun écran d'éducation requis - Aucun écran d'éducation requis
**Demande de permission arrière-plan** (Étape 2) : **Demande de permission arrière-plan** (Étape 2) :
- **Toujours** afficher un écran d'éducation AVANT la demande OS - **Toujours** afficher un écran d'éducation AVANT la demande OS
- Demander la permission `locationAlways` (iOS) ou `ACCESS_BACKGROUND_LOCATION` (Android) - Demander la permission `locationAlways` (iOS) ou `ACCESS_BACKGROUND_LOCATION` (Android)
- Si refusée de manière permanente, proposer l'ouverture des réglages système - Si refusée de manière permanente, proposer l'ouverture des réglages système
**Gestion des refus** : **Gestion des refus** :
- Détecter si la permission est refusée de manière permanente - Détecter si la permission est refusée de manière permanente
- Proposer l'ouverture des réglages de l'appareil avec un message clair - Proposer l'ouverture des réglages de l'appareil avec un message clair
- Permettre à l'utilisateur d'annuler - Permettre à l'utilisateur d'annuler
@@ -167,7 +160,6 @@ Le service de gestion des permissions (`lib/core/services/location_permission_se
#### iOS (`ios/Runner/Info.plist`) #### iOS (`ios/Runner/Info.plist`)
**Clés requises** : **Clés requises** :
- `NSLocationWhenInUseUsageDescription` : Décrire l'usage pour le mode voiture (contenu géolocalisé en temps réel) - `NSLocationWhenInUseUsageDescription` : Décrire l'usage pour le mode voiture (contenu géolocalisé en temps réel)
- `NSLocationAlwaysAndWhenInUseUsageDescription` : Décrire l'usage optionnel pour le mode piéton (notifications audio-guides en arrière-plan), mentionner explicitement que c'est optionnel et désactivable - `NSLocationAlwaysAndWhenInUseUsageDescription` : Décrire l'usage optionnel pour le mode piéton (notifications audio-guides en arrière-plan), mentionner explicitement que c'est optionnel et désactivable
- `UIBackgroundModes` : Activer les modes `location` et `remote-notification` - `UIBackgroundModes` : Activer les modes `location` et `remote-notification`
@@ -178,13 +170,11 @@ Le service de gestion des permissions (`lib/core/services/location_permission_se
#### Android (`android/app/src/main/AndroidManifest.xml`) #### Android (`android/app/src/main/AndroidManifest.xml`)
**Permissions requises** : **Permissions requises** :
- `ACCESS_FINE_LOCATION` et `ACCESS_COARSE_LOCATION` : Permission de base (when in use) - `ACCESS_FINE_LOCATION` et `ACCESS_COARSE_LOCATION` : Permission de base (when in use)
- `ACCESS_BACKGROUND_LOCATION` : Permission arrière-plan (Android 10+), nécessite justification Play Store - `ACCESS_BACKGROUND_LOCATION` : Permission arrière-plan (Android 10+), nécessite justification Play Store
- `FOREGROUND_SERVICE` et `FOREGROUND_SERVICE_LOCATION` : Service persistant pour mode piéton (Android 12+) - `FOREGROUND_SERVICE` et `FOREGROUND_SERVICE_LOCATION` : Service persistant pour mode piéton (Android 12+)
**Android Play Store** : Déclaration requise dans Play Console lors de la soumission : **Android Play Store** : Déclaration requise dans Play Console lors de la soumission :
- Justification : "Notifications géolocalisées pour audio-guides touristiques en arrière-plan" - Justification : "Notifications géolocalisées pour audio-guides touristiques en arrière-plan"
- Vidéo démo obligatoire montrant le flow de demande de permission - Vidéo démo obligatoire montrant le flow de demande de permission
@@ -201,14 +191,12 @@ Le service de gestion des permissions (`lib/core/services/location_permission_se
### Tests de Validation Stores ### Tests de Validation Stores
**Checklist App Store (iOS)** : **Checklist App Store (iOS)** :
- [ ] Permission "Always" demandée **uniquement** si user active mode piéton - [ ] Permission "Always" demandée **uniquement** si user active mode piéton
- [ ] Écran d'éducation **avant** demande OS (requis iOS 13+) - [ ] Écran d'éducation **avant** demande OS (requis iOS 13+)
- [ ] App fonctionne sans permission "Always" (validation critique) - [ ] App fonctionne sans permission "Always" (validation critique)
- [ ] Texte `Info.plist` clair et honnête (pas de tracking publicitaire) - [ ] Texte `Info.plist` clair et honnête (pas de tracking publicitaire)
**Checklist Play Store (Android)** : **Checklist Play Store (Android)** :
- [ ] Déclaration `ACCESS_BACKGROUND_LOCATION` avec justification détaillée - [ ] Déclaration `ACCESS_BACKGROUND_LOCATION` avec justification détaillée
- [ ] Vidéo démo flow de permissions (< 30s, requis Play Console) - [ ] Vidéo démo flow de permissions (< 30s, requis Play Console)
- [ ] App fonctionne sans permission background (validation critique) - [ ] App fonctionne sans permission background (validation critique)
@@ -217,8 +205,8 @@ Le service de gestion des permissions (`lib/core/services/location_permission_se
### Documentation Associée ### Documentation Associée
- **Guide détaillé** : [/docs/mobile/permissions-strategy.md](../mobile/permissions-strategy.md) - **Guide détaillé** : [/docs/mobile/permissions-strategy.md](../mobile/permissions-strategy.md)
- **Règles métier** : [Règle 05 - Mode Piéton](../domains/recommendation/rules/interactions-navigation.md#512-mode-piéton-audio-guides) - **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](../domains/_shared/rules/rgpd.md) - **RGPD** : [Règle 02 - Conformité RGPD](../regles-metier/02-conformite-rgpd.md)
--- ---

View File

@@ -30,7 +30,7 @@ Approche **multi-niveaux** : unitaires, intégration, BDD (Gherkin), E2E, load t
## Tests BDD (Gherkin + Godog) ## Tests BDD (Gherkin + Godog)
- **Framework** : `github.com/cucumber/godog` - **Framework** : `github.com/cucumber/godog`
- **Couverture** : Tous les cas d'usage du traduits en `.feature` - **Couverture** : Tous les cas d'usage du [README.md](../../README.md) traduits en `.feature`
- **Exécution** : Avant release - **Exécution** : Avant release
- **Détails** : Voir [ADR-007](007-tests-bdd.md) pour contexte complet - **Détails** : Voir [ADR-007](007-tests-bdd.md) pour contexte complet
@@ -79,7 +79,6 @@ Approche **multi-niveaux** : unitaires, intégration, BDD (Gherkin), E2E, load t
- `github.com/cucumber/godog` - `github.com/cucumber/godog`
- `github.com/testcontainers/testcontainers-go` - `github.com/testcontainers/testcontainers-go`
- `grafana/k6` (AGPL-3.0, usage interne OK) - `grafana/k6` (AGPL-3.0, usage interne OK)
- Temps CI : ~3-5 min (tests unitaires + BDD) - Temps CI : ~3-5 min (tests unitaires + BDD)
- Tests intégration/E2E : nightly builds (15-30 min) - Tests intégration/E2E : nightly builds (15-30 min)
- Load tests : avant chaque release majeure - Load tests : avant chaque release majeure

View File

@@ -68,7 +68,6 @@ mobile/tests/bdd/inscription_steps.dart → Teste l'UI mobile
``` ```
Cela garantit que : Cela garantit que :
- Les spécifications métier sont uniques et cohérentes - Les spécifications métier sont uniques et cohérentes
- Chaque couche teste sa responsabilité - Chaque couche teste sa responsabilité
- Les tests valident le contrat entre front et back - Les tests valident le contrat entre front et back
@@ -78,7 +77,7 @@ Cela garantit que :
- **Turborepo** ou **Nx** : orchestration des builds/tests, cache intelligent - **Turborepo** ou **Nx** : orchestration des builds/tests, cache intelligent
- **Docker Compose** : environnement de dev local (PostgreSQL, Redis, backend, etc.) - **Docker Compose** : environnement de dev local (PostgreSQL, Redis, backend, etc.)
- **Make** : commandes communes (`make test`, `make build`, `make dev`) - **Make** : commandes communes (`make test`, `make build`, `make dev`)
- **CI/CD** : GitHub Actions avec path filters (voir [ADR-020](022-strategie-cicd-monorepo.md)) - **CI/CD** : GitHub Actions avec path filters (voir [ADR-020](020-strategie-cicd-monorepo.md))
## Conséquences ## Conséquences

View File

@@ -52,7 +52,6 @@ OVH Object Storage (~1.20€/100GB)
| **Scale** | 100K+ | Scaleway Kubernetes (Kapsule) | ~500€ | Auto-scaling OU multi-région | | **Scale** | 100K+ | Scaleway Kubernetes (Kapsule) | ~500€ | Auto-scaling OU multi-région |
**Triggers détaillés** : **Triggers détaillés** :
- **Phase 2** : CPU > 70% (7j), latence p99 > 100ms, backups > 1h/semaine, MRR > 2000€ - **Phase 2** : CPU > 70% (7j), latence p99 > 100ms, backups > 1h/semaine, MRR > 2000€
- **Phase 3** : Auto-scaling horizontal requis, multi-région Europe, DevOps dédié, > 5 services - **Phase 3** : Auto-scaling horizontal requis, multi-région Europe, DevOps dédié, > 5 services

View File

@@ -8,7 +8,6 @@
RoadWave nécessite un service d'envoi d'emails **techniques uniquement** (pas de notifications sociales, alertes marketing, promotions). RoadWave nécessite un service d'envoi d'emails **techniques uniquement** (pas de notifications sociales, alertes marketing, promotions).
**Périmètre strict** : **Périmètre strict** :
-**Authentification** : Vérification email (inscription), réinitialisation mot de passe, changement email -**Authentification** : Vérification email (inscription), réinitialisation mot de passe, changement email
-**Sécurité** : Alertes connexion inhabituelle, changement password -**Sécurité** : Alertes connexion inhabituelle, changement password
-**Modération** : Strikes, suspensions, bannissements -**Modération** : Strikes, suspensions, bannissements

View File

@@ -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** : Architecture hybride en **2 phases** :
### Phase 1 (MVP) : WebSocket + APNS/FCM Direct ### Phase 1 (MVP) : WebSocket + Firebase Cloud Messaging
``` ```
[App Mobile] → [WebSocket] → [Backend Go] [App Mobile] → [WebSocket] → [Backend Go]
[PostGIS Worker] [PostGIS Worker]
[APNS / FCM Direct API] [Firebase FCM / APNS]
[Push Notification] [Push Notification]
``` ```
@@ -31,7 +31,7 @@ Architecture hybride en **2 phases** :
2. L'app envoie sa position GPS toutes les 30s via WebSocket 2. L'app envoie sa position GPS toutes les 30s via WebSocket
3. Un worker backend (goroutine) interroge PostGIS toutes les 30s : 3. Un worker backend (goroutine) interroge PostGIS toutes les 30s :
```sql ```sql
SELECT poi.*, users.push_token, users.platform SELECT poi.*, users.fcm_token
FROM points_of_interest poi FROM points_of_interest poi
JOIN user_locations users ON ST_DWithin( JOIN user_locations users ON ST_DWithin(
poi.geom, poi.geom,
@@ -41,11 +41,10 @@ Architecture hybride en **2 phases** :
WHERE users.notifications_enabled = true WHERE users.notifications_enabled = true
AND users.last_update > NOW() - INTERVAL '5 minutes' AND users.last_update > NOW() - INTERVAL '5 minutes'
``` ```
4. Si proximité détectée → envoi de push notification via FCM (Android) ou APNS (iOS) 4. Si proximité détectée → envoi de push notification via Firebase (Android) ou APNS (iOS)
5. Utilisateur clique → app s'ouvre → HLS démarre l'audio (ADR-002) 5. Utilisateur clique → app s'ouvre → HLS démarre l'audio (ADR-002)
**Limitations MVP** : **Limitations MVP** :
- Fonctionne uniquement si l'utilisateur a envoyé sa position < 5 minutes - Fonctionne uniquement si l'utilisateur a envoyé sa position < 5 minutes
- En voiture rapide (>80 km/h), possible de "manquer" un POI si position pas mise à jour - En voiture rapide (>80 km/h), possible de "manquer" un POI si position pas mise à jour
@@ -79,11 +78,11 @@ Architecture hybride en **2 phases** :
| Provider | Fiabilité | Coût MVP | Coût 100K users | Self-hosted | Vendor lock-in | Verdict | | Provider | Fiabilité | Coût MVP | Coût 100K users | Self-hosted | Vendor lock-in | Verdict |
|----------|-----------|----------|-----------------|-------------|----------------|---------| |----------|-----------|----------|-----------------|-------------|----------------|---------|
| **APNS/FCM Direct (choix)** | 99.95% | **0€** | **0€** | ✅ Oui | 🟢 Aucun | ✅ Optimal | | **Firebase (choix)** | 99.95% | **0€** | **0€** | ❌ Non | 🔴 Fort (Google) | ✅ Optimal MVP |
| OneSignal | 99.95% | 0€ | 500€/mois | ❌ Non | 🔴 Fort | ❌ Plus cher | | OneSignal | 99.95% | 0€ | 500€/mois | ❌ Non | 🔴 Fort | ❌ Plus cher |
| Pusher Beams | 99.9% | 0€ | 300€/mois | ❌ Non | 🔴 Fort | ❌ Niche | | Pusher Beams | 99.9% | 0€ | 300€/mois | ❌ Non | 🔴 Fort | ❌ Niche |
| Firebase SDK | 99.95% | 0€ | 0€ | ❌ Non | 🔴 Fort (Google) | ❌ Vendor lock-in | | Custom WS + APNS/FCM | Votre charge | 5€ | 100€+ | ✅ Oui | 🟢 Aucun | ⚠️ Complexe |
| Novu (open source) | 99.9% | 15€ | 50€ | ✅ Oui | 🟢 Aucun | ❌ Overhead inutile | | Novu (open source) | 99.9% | 15€ | 50€ | ✅ Oui | 🟢 Aucun | 🟡 Phase 2 |
| Brevo API | 99.9% | 0€ | 49€ | ✅ Oui | 🟢 Aucun | ❌ Email seulement | | Brevo API | 99.9% | 0€ | 49€ | ✅ Oui | 🟢 Aucun | ❌ Email seulement |
## Justification ## Justification
@@ -94,43 +93,43 @@ Architecture hybride en **2 phases** :
- **Batterie** : Connexion persistante optimisée par l'OS mobile - **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") - **Bi-directionnel** : Backend peut envoyer des mises à jour instantanées (ex: "nouveau POI créé par un créateur que tu suis")
### Pourquoi implémentation directe APNS/FCM et pas SDK Firebase ? ### 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.
**Réalité technique** : Notifications natives requièrent obligatoirement Google/Apple **Réalité technique** : Notifications natives requièrent obligatoirement Google/Apple
- **APNS (Apple)** : Seul protocole pour notifications iOS → dépendance Apple inévitable - **APNS (Apple)** : Seul protocole pour notifications iOS → dépendance Apple inévitable
- **FCM (Google)** : Protocole standard Android (Google Play Services) - **FCM (Google)** : Meilleur protocole Android (vs Huawei HMS, Samsung)
**Implémentation directe choisie** : **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
- **Gratuit** : APNS et FCM sont gratuits (pas de limite de volume) 2. **Novu (open source self-hosted)** :
- **Self-hosted** : Code backend 100% maîtrisé, pas de dépendance SDK tiers - ✅ Self-hostable
- **Fiabilité** : Infrastructure Apple/Google avec 99.95% uptime - ❌ Jeune (moins mature)
- **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
**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 - ❌ Toujours wrapper autour APNS/FCM
- ❌ Overhead sans gain réel
**Décision technique** : 3. **OneSignal / Pusher** :
- ❌ Même vendor lock-in que Firebase
- ❌ Plus cher (500€+/mois @ 100K users)
- Implémentation directe APNS/FCM dès le MVP **Décision pragmatique** :
- **Cohérence ADR** : Respecte ADR-008 (self-hosted) et ADR-015 (souveraineté française) - Firebase pour MVP : gratuit + fiabilité + time-to-market
- **Abstraction layer** : Interface `NotificationProvider` pour faciliter maintenance - **Mitigation vendor lock-in** : Utiliser abstraction layer (`NotificationProvider` interface)
- **Complexité** : Gestion des certificats APNS + JWT FCM (standard backend) - **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)
### Pourquoi limiter le geofencing local à Phase 2 ? ### Pourquoi limiter le geofencing local à Phase 2 ?
@@ -151,14 +150,12 @@ Architecture hybride en **2 phases** :
### Négatives ### Négatives
- ⚠️ **Gestion certificats APNS** : Renouvellement annuel + configuration - ⚠️ **Dépendance Google (Firebase)** : Contradictoire avec ADR-008 (self-hosted) + ADR-015 (souveraineté FR)
- Mitigé par scripts automation (certificats auto-renouvelés) - Mitigé par abstraction layer (`NotificationProvider` interface) → swap facile si besoin
- Documentation complète du processus - Exit path documenté pour migration custom (< 1 sprint)
- ⚠️ **Données utilisateur chez Google** : Tokens FCM, timestamps notifications
- ⚠️ **Tokens push sensibles** : Tokens FCM/APNS stockés côté backend - Risque RGPD : Nécessite DPA Google valide
- Chiffrement tokens en base (conformité RGPD) - À consulter avec DPO avant déploiement production
- Rotation automatique des tokens expirés
- ❌ WebSocket nécessite maintien de connexion (charge serveur +10-20%) - ❌ WebSocket nécessite maintien de connexion (charge serveur +10-20%)
- ❌ Mode offline non disponible au MVP (déception possible des early adopters) - ❌ Mode offline non disponible au MVP (déception possible des early adopters)
@@ -167,100 +164,58 @@ Architecture hybride en **2 phases** :
- **ADR-002 (Streaming)** : Aucun conflit - HLS reste pour l'audio - **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-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 (Architecture Backend)** : Ajouter un module `geofencing` avec worker dédié
- **ADR-010 (Frontend Mobile)** : Intégrer plugins APNS/FCM natifs (Flutter) et gérer permissions - **ADR-010 (Frontend Mobile)** : Intégrer `firebase_messaging` (Flutter) et gérer permissions
## Abstraction Layer (Maintenabilité) ## Abstraction Layer (Mitigation Vendor Lock-in)
Implémentation d'une interface abstraite pour gérer APNS et FCM de manière unifiée : Pour minimiser le coût de changement future, implémenter une interface abstraite :
```go ```go
// backend/internal/notification/provider.go // backend/internal/notification/provider.go
type NotificationProvider interface { type NotificationProvider interface {
SendNotification(ctx context.Context, platform, token, title, body, deepLink string) error SendNotification(ctx context.Context, token, title, body, deepLink string) error
UpdateToken(ctx context.Context, userID, platform, newToken string) error UpdateToken(ctx context.Context, userID, newToken string) error
} }
// backend/internal/notification/apns_provider.go // backend/internal/notification/firebase_provider.go
type APNSProvider struct { type FirebaseProvider struct {
client *apns2.Client client *messaging.Client
bundleID string
} }
func (p *APNSProvider) SendNotification(ctx context.Context, platform, token, title, body, deepLink string) error { func (p *FirebaseProvider) SendNotification(ctx context.Context, token, title, body, deepLink string) error {
if platform != "ios" { message := &messaging.Message{
return nil // Not applicable Notification: &messaging.Notification{
} Title: title,
Body: body,
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,
},
}, },
Data: map[string]string{
"deepLink": deepLink,
},
Token: token,
} }
_, err := p.client.Send(ctx, message)
// 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 return err
} }
// backend/internal/notification/service.go // backend/internal/notification/service.go
type NotificationService struct { type NotificationService struct {
apnsProvider NotificationProvider provider NotificationProvider // ← Interface, pas concrète
fcmProvider NotificationProvider repo NotificationRepository
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** : Code modulaire, testable, et facile à maintenir. Ajout futur de providers alternatifs simple. **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{...}
}
```
## Métriques de Succès ## Métriques de Succès
@@ -274,9 +229,8 @@ func (s *NotificationService) SendPush(ctx context.Context, userID, title, body,
### Phase 1 (MVP - Sprint 3-4) ### Phase 1 (MVP - Sprint 3-4)
1. Backend : Implémenter WebSocket endpoint `/ws/location` 1. Backend : Implémenter WebSocket endpoint `/ws/location`
2. Backend : Worker PostGIS avec requête ST_DWithin 2. Backend : Worker PostGIS avec requête ST_DWithin
3. Backend : Configuration APNS (certificats .p8) + FCM (OAuth2) 3. Mobile : Intégrer Firebase SDK + gestion FCM token
4. Mobile : Intégrer plugins natifs APNS/FCM + gestion push tokens 4. Test : Validation en conditions réelles (Paris, 10 testeurs)
5. Test : Validation en conditions réelles (Paris, 10 testeurs)
### Phase 2 (Post-MVP - Sprint 8-10) ### Phase 2 (Post-MVP - Sprint 8-10)
1. Mobile : Implémenter geofencing avec `flutter_background_geolocation` 1. Mobile : Implémenter geofencing avec `flutter_background_geolocation`
@@ -286,8 +240,7 @@ func (s *NotificationService) SendPush(ctx context.Context, userID, title, body,
## Références ## Références
- [Apple Push Notification Service (APNS) Documentation](https://developer.apple.com/documentation/usernotifications) - [Firebase Cloud Messaging Documentation](https://firebase.google.com/docs/cloud-messaging)
- [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) - [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) - [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) - Règle Métier 05 : Section 5.1.2 (Mode Piéton, lignes 86-120)

View File

@@ -6,7 +6,6 @@
## Contexte ## Contexte
Le backend Go de RoadWave nécessite des librairies tierces pour HTTP, base de données, tests, streaming, etc. Le choix doit privilégier : Le backend Go de RoadWave nécessite des librairies tierces pour HTTP, base de données, tests, streaming, etc. Le choix doit privilégier :
- **Licences permissives** (MIT, Apache-2.0, BSD) sans restrictions commerciales - **Licences permissives** (MIT, Apache-2.0, BSD) sans restrictions commerciales
- **Performance** (10M utilisateurs, 100K RPS, p99 < 100ms) - **Performance** (10M utilisateurs, 100K RPS, p99 < 100ms)
- **Maturité** et maintenance active - **Maturité** et maintenance active
@@ -42,8 +41,7 @@ Utilisation de **16 librairies open-source** avec licences permissives.
| **Auth JWT** | `zitadel/zitadel-go/v3` | Apache-2.0 | SDK Zitadel officiel (ADR-008) | | **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) | | **WebRTC** | `pion/webrtc/v4` | MIT | Pure Go, radio live (ADR-002) |
| **WebSocket** | `coder/websocket` | ISC | Minimal, notifications (ADR-017) | | **WebSocket** | `coder/websocket` | ISC | Minimal, notifications (ADR-017) |
| **APNS Push** | `sideshow/apns2` | MIT | Client APNS HTTP/2 natif (ADR-017) | | **FCM Push** | `firebase.google.com/go` | BSD-3 | SDK Google officiel (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 | | **HLS/FFmpeg** | `asticode/go-astiav` | MIT | Bindings FFmpeg n8.0 |
### Utilitaires ### Utilitaires
@@ -55,8 +53,7 @@ Utilisation de **16 librairies open-source** avec licences permissives.
## Alternatives considérées ## Alternatives considérées
Voir pour comparatifs complets : Voir [analyse détaillée](../ANALYSE_LIBRAIRIES_GO.md) pour comparatifs complets :
- Framework : Fiber vs Gin vs Echo vs Chi - Framework : Fiber vs Gin vs Echo vs Chi
- PostgreSQL : pgx vs GORM vs database/sql - PostgreSQL : pgx vs GORM vs database/sql
- Redis : rueidis vs go-redis vs redigo - Redis : rueidis vs go-redis vs redigo
@@ -66,20 +63,17 @@ Voir pour comparatifs complets :
## Justification ## Justification
### Licences ### Licences
- **15/16 librairies** : MIT, Apache-2.0, BSD, ISC (permissives) - **15/16 librairies** : MIT, Apache-2.0, BSD, ISC (permissives)
- **1/16** : AGPL-3.0 (k6 load testing, OK usage interne) - **1/16** : AGPL-3.0 (k6 load testing, OK usage interne)
- **Compatibilité totale** : Aucun conflit de licence - **Compatibilité totale** : Aucun conflit de licence
### Performance ### Performance
- **Fiber** : 36K RPS (5% plus rapide que Gin/Echo) - **Fiber** : 36K RPS (5% plus rapide que Gin/Echo)
- **pgx** : 30-50% plus rapide que GORM - **pgx** : 30-50% plus rapide que GORM
- **rueidis** : Client-side caching automatique - **rueidis** : Client-side caching automatique
- **zerolog** : Zero allocation, benchmarks 2025 - **zerolog** : Zero allocation, benchmarks 2025
### Maturité ### Maturité
- **Standards** : testify (27% adoption), golang-migrate, viper - **Standards** : testify (27% adoption), golang-migrate, viper
- **Production** : Fiber (33K stars), pgx (10K stars), pion (13K stars) - **Production** : Fiber (33K stars), pgx (10K stars), pion (13K stars)
- **Maintenance** : Toutes actives (commits 2025-2026) - **Maintenance** : Toutes actives (commits 2025-2026)
@@ -87,16 +81,14 @@ Voir pour comparatifs complets :
## Conséquences ## Conséquences
### Positives ### Positives
- ✅ Aucune restriction licence commerciale - ✅ Aucune restriction licence commerciale
- ✅ Stack cohérent avec ADR existants (001, 002, 007, 008, 013, 015, 019) - ✅ Stack cohérent avec ADR existants (001, 002, 007, 008, 013, 015, 019)
- ✅ Performance validée (benchmarks publics) - ✅ Performance validée (benchmarks publics)
- ✅ Écosystème mature et documenté - ✅ Écosystème mature et documenté
### Négatives ### Négatives
- ⚠️ **k6 (AGPL-3.0)** : Copyleft, mais OK pour tests internes (pas de SaaS k6 prévu) - ⚠️ **k6 (AGPL-3.0)** : Copyleft, mais OK pour tests internes (pas de SaaS k6 prévu)
- ⚠️ **Gestion certificats APNS** : Renouvellement annuel, configuration manuelle - ⚠️ **Firebase FCM** : Dépendance Google (mitigation via abstraction layer, ADR-017)
- ❌ Courbe d'apprentissage : 16 librairies à maîtriser (doc nécessaire) - ❌ Courbe d'apprentissage : 16 librairies à maîtriser (doc nécessaire)
### Dépendances go.mod ### Dépendances go.mod
@@ -114,8 +106,7 @@ require (
github.com/zitadel/zitadel-go/v3 latest github.com/zitadel/zitadel-go/v3 latest
github.com/pion/webrtc/v4 latest github.com/pion/webrtc/v4 latest
github.com/coder/websocket latest github.com/coder/websocket latest
github.com/sideshow/apns2 latest firebase.google.com/go/v4 latest
golang.org/x/oauth2 latest // For FCM authentication
github.com/asticode/go-astiav latest github.com/asticode/go-astiav latest
github.com/spf13/viper latest github.com/spf13/viper latest
github.com/rs/zerolog latest github.com/rs/zerolog latest
@@ -125,7 +116,7 @@ require (
## Références ## Références
- (tableaux comparatifs, sources) - [Analyse complète des librairies](../ANALYSE_LIBRAIRIES_GO.md) (tableaux comparatifs, sources)
- ADR-001 : Langage Backend (Fiber, pgx, go-redis) - ADR-001 : Langage Backend (Fiber, pgx, go-redis)
- ADR-007 : Tests BDD (Godog) - ADR-007 : Tests BDD (Godog)
- ADR-011 : Accès données (sqlc) - ADR-011 : Accès données (sqlc)

View File

@@ -8,12 +8,10 @@
RoadWave nécessite un service de géolocalisation par IP pour le mode dégradé (utilisateurs sans GPS activé). Ce service permet de détecter la ville/région de l'utilisateur à partir de son adresse IP et d'afficher du contenu régional même sans permission GPS. RoadWave nécessite un service de géolocalisation par IP pour le mode dégradé (utilisateurs sans GPS activé). Ce service permet de détecter la ville/région de l'utilisateur à partir de son adresse IP et d'afficher du contenu régional même sans permission GPS.
**Évolution du marché** : **Évolution du marché** :
- **Avant 2019** : MaxMind GeoLite2 était téléchargeable gratuitement (base de données locale) - **Avant 2019** : MaxMind GeoLite2 était téléchargeable gratuitement (base de données locale)
- **Depuis 2019** : MaxMind nécessite un compte + limite 1000 requêtes/jour (gratuit), puis 0.003$/requête au-delà - **Depuis 2019** : MaxMind nécessite un compte + limite 1000 requêtes/jour (gratuit), puis 0.003$/requête au-delà
**Usage RoadWave** : **Usage RoadWave** :
- Mode dégradé : ~10% des utilisateurs (estimation) - Mode dégradé : ~10% des utilisateurs (estimation)
- Volume : 1000 utilisateurs × 10% = 100 requêtes/jour (MVP) - Volume : 1000 utilisateurs × 10% = 100 requêtes/jour (MVP)
- Critère : Aucune dépendance à un service tiers payant - Critère : Aucune dépendance à un service tiers payant
@@ -35,7 +33,6 @@ RoadWave nécessite un service de géolocalisation par IP pour le mode dégradé
### IP2Location Lite (choix retenu) ### IP2Location Lite (choix retenu)
**Avantages** : **Avantages** :
- Gratuit (pas de limite de requêtes) - Gratuit (pas de limite de requêtes)
- Self-hosted (souveraineté des données, cohérence avec [ADR-004](004-cdn.md)) - Self-hosted (souveraineté des données, cohérence avec [ADR-004](004-cdn.md))
- Base de données SQLite légère (50-100 MB) - Base de données SQLite légère (50-100 MB)
@@ -44,14 +41,12 @@ RoadWave nécessite un service de géolocalisation par IP pour le mode dégradé
- Pas de compte tiers requis - Pas de compte tiers requis
**Inconvénients** : **Inconvénients** :
- Maintenance mensuelle (mise à jour DB) - Maintenance mensuelle (mise à jour DB)
- Précision équivalente à MaxMind (~±50 km) - Précision équivalente à MaxMind (~±50 km)
### MaxMind GeoLite2 API (rejeté) ### MaxMind GeoLite2 API (rejeté)
**Pourquoi rejeté** : **Pourquoi rejeté** :
- Coût potentiel en cas de dépassement quota (risque faible mais existant) - Coût potentiel en cas de dépassement quota (risque faible mais existant)
- Dépendance à un service tiers (perte de souveraineté) - Dépendance à un service tiers (perte de souveraineté)
- Compte requis (friction opérationnelle) - Compte requis (friction opérationnelle)
@@ -59,7 +54,6 @@ RoadWave nécessite un service de géolocalisation par IP pour le mode dégradé
### Self-hosted MaxMind (rejeté) ### Self-hosted MaxMind (rejeté)
**Pourquoi rejeté** : **Pourquoi rejeté** :
- Compte MaxMind obligatoire pour télécharger la DB (friction) - Compte MaxMind obligatoire pour télécharger la DB (friction)
- Complexité identique à IP2Location pour résultat équivalent - Complexité identique à IP2Location pour résultat équivalent
- IP2Location offre même fonctionnalité sans compte tiers - IP2Location offre même fonctionnalité sans compte tiers
@@ -90,33 +84,28 @@ flowchart TD
### Maintenance ### Maintenance
**Mise à jour mensuelle** : **Mise à jour mensuelle** :
- Cron job télécharge nouvelle DB IP2Location (1er du mois) - Cron job télécharge nouvelle DB IP2Location (1er du mois)
- Backup DB actuelle avant remplacement - Backup DB actuelle avant remplacement
- Rechargement service GeoIP (hot reload sans downtime) - Rechargement service GeoIP (hot reload sans downtime)
**Monitoring** : **Monitoring** :
- Alertes si DB > 60 jours (DB obsolète) - Alertes si DB > 60 jours (DB obsolète)
- Logs requêtes "IP non trouvée" (détection problèmes DB) - Logs requêtes "IP non trouvée" (détection problèmes DB)
## Conséquences ## Conséquences
### Positives ### Positives
- Aucun coût récurrent (gratuit à l'infini) - Aucun coût récurrent (gratuit à l'infini)
- Souveraineté complète des données (cohérence ADR-004) - Souveraineté complète des données (cohérence ADR-004)
- Pas de dépendance externe (service tiers) - Pas de dépendance externe (service tiers)
- Latence minimale (lookup local SQLite < 1ms) - Latence minimale (lookup local SQLite < 1ms)
### Négatives ### Négatives
- Maintenance mensuelle requise (automatisable) - Maintenance mensuelle requise (automatisable)
- Précision limitée (±50 km, acceptable pour mode dégradé) - Précision limitée (±50 km, acceptable pour mode dégradé)
- Taille base de données (~50-100 MB sur disque) - Taille base de données (~50-100 MB sur disque)
### Risques atténués ### Risques atténués
- **DB obsolète** : Alertes automatiques si > 60 jours - **DB obsolète** : Alertes automatiques si > 60 jours
- **IP non trouvée** : Fallback "France" par défaut (code pays FR) - **IP non trouvée** : Fallback "France" par défaut (code pays FR)
- **Perte DB** : Backup automatique avant chaque mise à jour - **Perte DB** : Backup automatique avant chaque mise à jour
@@ -125,5 +114,5 @@ flowchart TD
- [ADR-004 : CDN (Souveraineté)](004-cdn.md) - [ADR-004 : CDN (Souveraineté)](004-cdn.md)
- [ADR-015 : Hébergement](015-hebergement.md) - [ADR-015 : Hébergement](015-hebergement.md)
- [Règle 02 : RGPD (Mode Dégradé)](../domains/_shared/rules/rgpd.md#136-géolocalisation-optionnelle) - [Règle 02 : RGPD (Mode Dégradé)](../regles-metier/02-conformite-rgpd.md#136-géolocalisation-optionnelle)
- IP2Location Lite : https://lite.ip2location.com/ - IP2Location Lite : https://lite.ip2location.com/

View File

@@ -6,7 +6,6 @@
## Contexte ## Contexte
L'application mobile RoadWave (iOS/Android) nécessite des librairies tierces pour audio HLS, géolocalisation, notifications, state management, etc. Le choix doit privilégier : L'application mobile RoadWave (iOS/Android) nécessite des librairies tierces pour audio HLS, géolocalisation, notifications, state management, etc. Le choix doit privilégier :
- **Licences permissives** (MIT, Apache-2.0, BSD) sans restrictions commerciales - **Licences permissives** (MIT, Apache-2.0, BSD) sans restrictions commerciales
- **Maturité** et maintenance active (écosystème Flutter) - **Maturité** et maintenance active (écosystème Flutter)
- **Performance native** (pas de bridge JS) - **Performance native** (pas de bridge JS)
@@ -15,9 +14,9 @@ L'application mobile RoadWave (iOS/Android) nécessite des librairies tierces po
## Décision ## Décision
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). Utilisation de **8 librairies open-source** Flutter avec licences permissives.
### Phase 1 (MVP) : Core Stack ### Core Stack
| Catégorie | Librairie | Licence | Justification | | Catégorie | Librairie | Licence | Justification |
|-----------|-----------|---------|---------------| |-----------|-----------|---------|---------------|
@@ -27,93 +26,63 @@ Utilisation de **9 librairies open-source** Flutter avec licences permissives, d
| **Stockage sécurisé** | `flutter_secure_storage` | BSD-3 | Keychain iOS, KeyStore Android | | **Stockage sécurisé** | `flutter_secure_storage` | BSD-3 | Keychain iOS, KeyStore Android |
| **Cache images** | `cached_network_image` | MIT | LRU cache, placeholder support | | **Cache images** | `cached_network_image` | MIT | LRU cache, placeholder support |
### Phase 1 (MVP) : Géolocalisation & Notifications ### Géolocalisation & Permissions
| Catégorie | Librairie | Licence | Justification | | Catégorie | Librairie | Licence | Justification |
|-----------|-----------|---------|---------------| |-----------|-----------|---------|---------------|
| **GPS temps réel** | `geolocator` | MIT | Mode voiture, WebSocket position updates, high accuracy | | **GPS temps réel** | `geolocator` | MIT | Mode voiture, high accuracy, background modes |
| **Push APNS/FCM** | `flutter_apns` + `flutter_fcm` | MIT | Intégration native APNS et FCM directe (ADR-017) | | **Geofencing** | `geofence_service` | MIT | Détection rayon 200m, mode piéton, économie batterie |
| **Notifications locales** | `flutter_local_notifications` | BSD-3 | Compteur dynamique, icônes custom, iOS/Android | | **Notifications locales** | `flutter_local_notifications` | BSD-3 | Compteur dynamique, icônes custom, iOS/Android |
| **Permissions** | `permission_handler` | MIT | Gestion unifiée permissions iOS/Android |
### Phase 1 (MVP) : CarPlay/Android Auto (optionnel) ### Packages Additionnels (CarPlay/Android Auto)
| Catégorie | Librairie | Licence | Justification | | Catégorie | Librairie | Licence | Justification |
|-----------|-----------|---------|---------------| |-----------|-----------|---------|---------------|
| **CarPlay** | `flutter_carplay` | MIT | Intégration CarPlay native (communautaire) | | **CarPlay** | `flutter_carplay` | MIT | Intégration CarPlay native (communautaire) |
| **Android Auto** | `android_auto_flutter` | Apache-2.0 | Support Android Auto (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 ## Alternatives considérées
### State Management ### State Management
- **flutter_bloc** (choisi) : Pattern BLoC, testable, reactive - **flutter_bloc** (choisi) : Pattern BLoC, testable, reactive
- **riverpod** : Plus moderne, moins mature - **riverpod** : Plus moderne, moins mature
- **provider** : Simple mais limité pour app complexe - **provider** : Simple mais limité pour app complexe
- **getx** : Performance mais opinions controversées - **getx** : Performance mais opinions controversées
### Audio ### Audio
- **just_audio** (choisi) : HLS natif, communauté active - **just_audio** (choisi) : HLS natif, communauté active
- **audioplayers** : Moins mature pour streaming - **audioplayers** : Moins mature pour streaming
- **flutter_sound** : Orienté recording, pas streaming - **flutter_sound** : Orienté recording, pas streaming
### Géolocalisation ### Géolocalisation
- **geolocator** (choisi) : Standard Flutter, 1.2K+ stars - **geolocator** (choisi) : Standard Flutter, 1.2K+ stars
- **location** : Moins maintenu - **location** : Moins maintenu
- **background_location** : Spécifique background uniquement - **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 ## Justification
### Licences ### Licences
- **7/8 librairies** : MIT (permissive totale)
- **7/9 librairies** : MIT (permissive totale) - **1/8** : BSD-3 (permissive, compatible commercial)
- **2/9** : BSD-3 (permissive, compatible commercial)
- **Compatibilité totale** : Aucun conflit de licence, aucune restriction commerciale - **Compatibilité totale** : Aucun conflit de licence, aucune restriction commerciale
### Maturité ### Maturité
- **flutter_bloc** : 11.6K stars, adoption large (state management standard) - **flutter_bloc** : 11.6K stars, adoption large (state management standard)
- **just_audio** : 900+ stars, utilisé production (podcasts apps) - **just_audio** : 900+ stars, utilisé production (podcasts apps)
- **geolocator** : 1.2K stars, maintenu BaseFlow (entreprise Flutter) - **geolocator** : 1.2K stars, maintenu BaseFlow (entreprise Flutter)
- **dio** : 12K+ stars, client HTTP le plus utilisé Flutter - **dio** : 12K+ stars, client HTTP le plus utilisé Flutter
### Performance ### Performance
- **Compilation native** : Dart → ARM64 (pas de bridge JS comme React Native) - **Compilation native** : Dart → ARM64 (pas de bridge JS comme React Native)
- **just_audio** : Utilise AVPlayer (iOS) et ExoPlayer (Android) natifs - **just_audio** : Utilise AVPlayer (iOS) et ExoPlayer (Android) natifs
- **geolocator** : Accès direct CoreLocation (iOS) et FusedLocation (Android) - **geolocator** : Accès direct CoreLocation (iOS) et FusedLocation (Android)
- **flutter_apns + flutter_fcm** : Utilise services systèmes natifs (APNS, Google Play Services) - **geofence_service** : Geofencing natif, minimise consommation batterie
- **geofence_service** (Phase 2) : Geofencing natif, minimise consommation batterie
### Conformité Stores ### Conformité Stores
- **Permissions progressives** : `permission_handler` + stratégie ADR-010 - **Permissions progressives** : `permission_handler` + stratégie ADR-010
- **Background modes MVP** : `geolocator` (When In Use) + `firebase_messaging` approuvés stores - **Background modes** : `geolocator` + `geofence_service` approuvés stores
- **Background modes Phase 2** : `geofence_service` nécessite permission "Always" (taux acceptation ~30%) - **Notifications** : `flutter_local_notifications` conforme guidelines iOS/Android
- **Notifications** : `flutter_local_notifications` + `firebase_messaging` conformes guidelines iOS/Android
## Architecture ## Architecture
@@ -130,18 +99,14 @@ graph TB
Cache["cached_network_image<br/>(Image Cache)"] Cache["cached_network_image<br/>(Image Cache)"]
end end
subgraph Services["Services Layer - Phase 1 MVP"] subgraph Services["Services Layer"]
Audio["just_audio<br/>(HLS Streaming)"] Audio["just_audio<br/>(HLS Streaming)"]
GPS["geolocator<br/>(GPS + WebSocket)"] GPS["geolocator<br/>(GPS Mode Voiture)"]
Push["flutter_apns + flutter_fcm<br/>(Push Natifs APNS/FCM)"] Geofence["geofence_service<br/>(Mode Piéton)"]
Notif["flutter_local_notifications<br/>(Notifications Locales)"] Notif["flutter_local_notifications<br/>(Alerts Locales)"]
Perms["permission_handler<br/>(Permissions iOS/Android)"] Perms["permission_handler<br/>(Permissions iOS/Android)"]
end end
subgraph Phase2["Services Layer - Phase 2"]
Geofence["geofence_service<br/>(Mode Offline)"]
end
subgraph Platform["Platform Integration"] subgraph Platform["Platform Integration"]
CarPlay["flutter_carplay<br/>(iOS)"] CarPlay["flutter_carplay<br/>(iOS)"]
AndroidAuto["android_auto_flutter<br/>(Android)"] AndroidAuto["android_auto_flutter<br/>(Android)"]
@@ -151,17 +116,14 @@ graph TB
Bloc --> API Bloc --> API
Bloc --> Audio Bloc --> Audio
Bloc --> GPS Bloc --> GPS
Bloc --> Push Bloc --> Geofence
API --> Storage API --> Storage
Widgets --> Cache Widgets --> Cache
GPS --> Perms GPS --> Perms
Push --> Perms Geofence --> Perms
Push --> Notif Geofence --> Notif
Geofence -.->|Phase 2| Perms
Geofence -.->|Phase 2| Notif
Audio --> CarPlay Audio --> CarPlay
Audio --> AndroidAuto Audio --> AndroidAuto
@@ -173,15 +135,13 @@ graph TB
class UI,Widgets,Bloc uiStyle class UI,Widgets,Bloc uiStyle
class Data,API,Storage,Cache dataStyle class Data,API,Storage,Cache dataStyle
class Services,Audio,GPS,FCM,Notif,Perms serviceStyle class Services,Audio,GPS,Geofence,Notif,Perms serviceStyle
class Phase2,Geofence serviceStyle
class Platform,CarPlay,AndroidAuto platformStyle class Platform,CarPlay,AndroidAuto platformStyle
``` ```
## Conséquences ## Conséquences
### Positives ### Positives
- ✅ Aucune restriction licence commerciale (100% permissif) - ✅ Aucune restriction licence commerciale (100% permissif)
- ✅ Stack cohérent avec ADR-010 (Frontend Mobile) - ✅ Stack cohérent avec ADR-010 (Frontend Mobile)
- ✅ Performance native (compilation ARM64 directe) - ✅ Performance native (compilation ARM64 directe)
@@ -190,10 +150,8 @@ graph TB
- ✅ Conformité stores (permissions progressives) - ✅ Conformité stores (permissions progressives)
### Négatives ### Négatives
- ⚠️ **CarPlay/Android Auto** : Packages communautaires (pas officiels Flutter) - ⚠️ **CarPlay/Android Auto** : Packages communautaires (pas officiels Flutter)
- ⚠️ **Configuration APNS/FCM** : Gestion certificats et OAuth2, configuration manuelle - ⚠️ **Géolocalisation background** : Scrutée par App Store (stratégie progressive requise, ADR-010)
- ⚠️ **Permission "Always" Phase 2** : Taux acceptation ~30% (geofencing local)
-**Courbe d'apprentissage** : Dart + pattern BLoC à maîtriser -**Courbe d'apprentissage** : Dart + pattern BLoC à maîtriser
-**Tests stores** : Validation TestFlight (iOS) et Internal Testing (Android) obligatoires -**Tests stores** : Validation TestFlight (iOS) et Internal Testing (Android) obligatoires
@@ -201,65 +159,49 @@ 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). > **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 (Phase 1 MVP)** : **Core** :
- `flutter_bloc` - State management - `flutter_bloc` - State management
- `just_audio` - Audio HLS streaming - `just_audio` - Audio HLS streaming
- `dio` - HTTP client - `dio` - HTTP client
- `flutter_secure_storage` - Stockage sécurisé JWT - `flutter_secure_storage` - Stockage sécurisé JWT
- `cached_network_image` - Cache images - `cached_network_image` - Cache images
**Géolocalisation & Notifications (Phase 1 MVP)** : **Géolocalisation & Notifications** :
- `geolocator` - GPS haute précision
- `geolocator` - GPS haute précision, WebSocket position updates - `geofence_service` - Geofencing arrière-plan
- `flutter_apns` - Push notifications APNS natif iOS (ADR-017)
- `flutter_fcm` - Push notifications FCM natif Android (ADR-017)
- `flutter_local_notifications` - Notifications locales - `flutter_local_notifications` - Notifications locales
- `permission_handler` - Gestion permissions - `permission_handler` - Gestion permissions
**CarPlay/Android Auto (optionnels Phase 1)** : **CarPlay/Android Auto** (optionnels MVP) :
- `flutter_carplay` - Intégration CarPlay - `flutter_carplay` - Intégration CarPlay
- `android_auto_flutter` - Support Android Auto - `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 ### 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 : 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](020-librairies-flutter.md) pour la liste complète, licences et justifications. > **Packages Flutter** : Voir [ADR-018 - Librairies Flutter](018-librairies-flutter.md) pour la liste complète, licences et justifications.
## Risques et Mitigations ## Risques et Mitigations
### Risque 1 : CarPlay/Android Auto packages communautaires ### Risque 1 : CarPlay/Android Auto packages communautaires
- **Impact** : Maintenance non garantie par Flutter team - **Impact** : Maintenance non garantie par Flutter team
- **Mitigation** : Fork privé si besoin, contribution upstream, ou développement custom si critique - **Mitigation** : Fork privé si besoin, contribution upstream, ou développement custom si critique
### Risque 2 : Validation App Store (permissions background) ### Risque 2 : Validation App Store (permissions background)
- **Impact** : Taux de rejet ~70% si mal justifié - **Impact** : Taux de rejet ~70% si mal justifié
- **Mitigation Phase 1** : Permission "When In Use" seulement (MVP), moins scrutée par Apple - **Mitigation** : Stratégie progressive (ADR-010), écrans d'éducation, tests beta TestFlight
- **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 ### Risque 3 : Performance audio HLS en arrière-plan
- **Impact** : Interruptions si OS tue l'app - **Impact** : Interruptions si OS tue l'app
- **Mitigation** : Background audio task iOS, foreground service Android (natif dans `just_audio`) - **Mitigation** : Background audio task iOS, foreground service Android (natif dans `just_audio`)
## Références ## Références
- [ADR-010 : Frontend Mobile](012-frontend-mobile.md) (Flutter, architecture permissions) - ADR-010 : Frontend Mobile (Flutter, architecture permissions)
- [ADR-017 : Notifications Géolocalisées](017-notifications-geolocalisees.md) (Phase 1 WebSocket vs Phase 2 Geofencing) - ADR-018 : Librairies Go (même format de documentation)
- [ADR-018 : Librairies Go](018-librairies-go.md) (même format de documentation)
- [flutter_bloc documentation](https://bloclibrary.dev/) - [flutter_bloc documentation](https://bloclibrary.dev/)
- [just_audio repository](https://pub.dev/packages/just_audio) - [just_audio repository](https://pub.dev/packages/just_audio)
- [geolocator documentation](https://pub.dev/packages/geolocator) - [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/) - [Apple CarPlay Developer Guide](https://developer.apple.com/carplay/)
- [Android Auto Developer Guide](https://developer.android.com/training/cars) - [Android Auto Developer Guide](https://developer.android.com/training/cars)

View File

@@ -14,7 +14,6 @@ L'application nécessite un système de cache performant pour plusieurs cas d'us
- **Rate limiting** : Protection contre les abus API - **Rate limiting** : Protection contre les abus API
Les contraintes de performance sont strictes : Les contraintes de performance sont strictes :
- Latence p99 < 5ms pour les requêtes de cache - Latence p99 < 5ms pour les requêtes de cache
- Support de 100K+ requêtes/seconde en lecture - Support de 100K+ requêtes/seconde en lecture
- Persistance optionnelle (données non critiques) - Persistance optionnelle (données non critiques)
@@ -25,7 +24,6 @@ Les contraintes de performance sont strictes :
**Redis 7+ en mode Cluster** sera utilisé comme solution de cache principale. **Redis 7+ en mode Cluster** sera utilisé comme solution de cache principale.
Configuration : Configuration :
- Mode Cluster avec 3 nœuds minimum (haute disponibilité) - Mode Cluster avec 3 nœuds minimum (haute disponibilité)
- Persistence RDB désactivée pour les caches chauds (performance maximale) - Persistence RDB désactivée pour les caches chauds (performance maximale)
- AOF activé uniquement pour les sessions utilisateurs (durabilité) - AOF activé uniquement pour les sessions utilisateurs (durabilité)
@@ -78,7 +76,6 @@ Ces commandes permettent de servir les requêtes de proximité directement depui
### Écosystème Go ### Écosystème Go
Librairie `go-redis/redis` (13K+ stars GitHub) : Librairie `go-redis/redis` (13K+ stars GitHub) :
- Support complet Redis Cluster - Support complet Redis Cluster
- Pipeline et transactions - Pipeline et transactions
- Context-aware (intégration Go idiomatique) - Context-aware (intégration Go idiomatique)
@@ -87,7 +84,6 @@ Librairie `go-redis/redis` (13K+ stars GitHub) :
### Pub/Sub pour temps réel ### Pub/Sub pour temps réel
Support natif de messaging publish/subscribe pour : Support natif de messaging publish/subscribe pour :
- Notifications push (invalidation de cache) - Notifications push (invalidation de cache)
- Événements temps réel (nouveau contenu géolocalisé) - Événements temps réel (nouveau contenu géolocalisé)
- Coordination entre instances API (scaling horizontal) - Coordination entre instances API (scaling horizontal)
@@ -110,14 +106,12 @@ Support natif de messaging publish/subscribe pour :
### Stratégie de cache ### Stratégie de cache
**TTL par type de donnée** : **TTL par type de donnée** :
- Métadonnées de contenu : 15 minutes (mise à jour rare) - Métadonnées de contenu : 15 minutes (mise à jour rare)
- Résultats géolocalisés : 5 minutes (contenus statiques géographiquement) - Résultats géolocalisés : 5 minutes (contenus statiques géographiquement)
- Sessions utilisateurs : 24 heures (renouvellement automatique) - Sessions utilisateurs : 24 heures (renouvellement automatique)
- Rate limiting : 1 minute (fenêtre glissante) - Rate limiting : 1 minute (fenêtre glissante)
**Invalidation** : **Invalidation** :
- Publication de contenu → `DEL` métadonnées + publication Pub/Sub - Publication de contenu → `DEL` métadonnées + publication Pub/Sub
- Modification géolocalisation → `GEOREM` puis `GEOADD` - Modification géolocalisation → `GEOREM` puis `GEOADD`
- Logout utilisateur → `DEL` session - Logout utilisateur → `DEL` session
@@ -125,25 +119,21 @@ Support natif de messaging publish/subscribe pour :
### Configuration production ### Configuration production
**Cluster 3 nœuds** (minimum haute disponibilité) : **Cluster 3 nœuds** (minimum haute disponibilité) :
- 1 master + 2 replicas - 1 master + 2 replicas
- Répartition sur 3 zones de disponibilité (anti-affinité) - Répartition sur 3 zones de disponibilité (anti-affinité)
- `cluster-require-full-coverage no` → lecture dégradée si nœud down - `cluster-require-full-coverage no` → lecture dégradée si nœud down
**Mémoire** : **Mémoire** :
- `maxmemory 2gb` par nœud (ajustable selon charge) - `maxmemory 2gb` par nœud (ajustable selon charge)
- `maxmemory-policy allkeys-lru` → éviction automatique anciennes clés - `maxmemory-policy allkeys-lru` → éviction automatique anciennes clés
**Persistance** : **Persistance** :
- RDB désactivé (`save ""`) pour caches chauds - RDB désactivé (`save ""`) pour caches chauds
- AOF `appendonly yes` uniquement pour sessions (nœud dédié optionnel) - AOF `appendonly yes` uniquement pour sessions (nœud dédié optionnel)
### Monitoring ### Monitoring
Métriques critiques à suivre : Métriques critiques à suivre :
- Taux de hit/miss par namespace (target >95% hit rate) - Taux de hit/miss par namespace (target >95% hit rate)
- Latence p99 par commande (alerter si >10ms) - Latence p99 par commande (alerter si >10ms)
- Fragmentation mémoire (rebalance si >1.5) - Fragmentation mémoire (rebalance si >1.5)

View File

@@ -8,7 +8,6 @@
RoadWave est organisé en monorepo contenant backend Go, mobile Flutter, documentation et features BDD ([ADR-014](014-organisation-monorepo.md)). Sans optimisation, chaque commit déclencherait **tous** les builds (backend + mobile + docs), même si seul un composant a changé. RoadWave est organisé en monorepo contenant backend Go, mobile Flutter, documentation et features BDD ([ADR-014](014-organisation-monorepo.md)). Sans optimisation, chaque commit déclencherait **tous** les builds (backend + mobile + docs), même si seul un composant a changé.
**Problématique** : **Problématique** :
- ❌ Temps de CI/CD inutilement longs (rebuild complet ~15 min) - ❌ Temps de CI/CD inutilement longs (rebuild complet ~15 min)
- ❌ Gaspillage de ressources GitHub Actions - ❌ Gaspillage de ressources GitHub Actions
- ❌ Ralentissement du feedback développeur - ❌ Ralentissement du feedback développeur
@@ -44,71 +43,203 @@ RoadWave est organisé en monorepo contenant backend Go, mobile Flutter, documen
#### Workflow Backend (`backend.yml`) #### Workflow Backend (`backend.yml`)
**Déclencheurs** : ```yaml
name: Backend CI
- Branches : `main`, `develop` on:
- Chemins surveillés : push:
- `backend/**` : Code Go, migrations, configuration branches: [main, develop]
- `features/api/**` : Features BDD des tests API paths:
- `features/e2e/**` : Features BDD end-to-end impliquant le backend - 'backend/**' # Code Go modifié
- `.github/workflows/backend.yml` : Modifications du workflow lui-même - '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** : 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 ./...
- **Tests unitaires** : Exécution `go test` sur tous les packages test-integration:
- **Tests d'intégration** : Utilisation de Testcontainers avec PostgreSQL/PostGIS runs-on: ubuntu-latest
- **Tests BDD** : Exécution Godog sur features `api/` et `e2e/` steps:
- **Lint** : Vérification golangci-lint - uses: actions/checkout@v4
- **Build** : Compilation binaire production (dépend de tous les jobs précédents) - uses: actions/setup-go@v5
with:
go-version: '1.21'
- run: cd backend && make test-integration
**Environnement** : Ubuntu latest, Go 1.21+ 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é)
--- ---
#### Workflow Mobile (`mobile.yml`) #### Workflow Mobile (`mobile.yml`)
**Déclencheurs** : ```yaml
name: Mobile CI
- Branches : `main`, `develop` on:
- Chemins surveillés : push:
- `mobile/**` : Code Flutter/Dart, assets, configuration branches: [main, develop]
- `features/ui/**` : Features BDD des tests UI paths:
- `features/e2e/**` : Features BDD end-to-end impliquant le mobile - 'mobile/**' # Code Flutter modifié
- `.github/workflows/mobile.yml` : Modifications du workflow lui-même - '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** : 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
- **Tests unitaires** : Exécution `flutter test` sur widgets et logique métier test-integration:
- **Tests d'intégration** : Tests d'intégration Flutter (interactions UI complexes) runs-on: ubuntu-latest
- **Lint** : Analyse statique `flutter analyze` steps:
- **Build Android** : Compilation APK release (dépend des tests) - uses: actions/checkout@v4
- **Build iOS** : Compilation IPA release sans codesign (dépend des tests) - uses: subosito/flutter-action@v2
with:
flutter-version: '3.16.0'
- run: cd mobile && flutter test integration_test/
**Environnement** : 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
- Tests/Lint/Build Android : Ubuntu latest build-android:
- Build iOS : macOS latest (requis pour Xcode) runs-on: ubuntu-latest
- Flutter 3.16.0+ 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é)
--- ---
#### Workflow Shared (`shared.yml`) #### Workflow Shared (`shared.yml`)
**Déclencheurs** : ```yaml
name: Shared CI
- Branches : `main`, `develop` on:
- Chemins surveillés : push:
- `docs/**` : ADR, règles métier, documentation technique branches: [main, develop]
- `shared/**` : Contrats API, types partagés paths:
- `.github/workflows/shared.yml` : Modifications du workflow lui-même - 'docs/**' # Documentation modifiée
- 'shared/**' # Code partagé modifié
- '.github/workflows/shared.yml'
pull_request:
branches: [main, develop]
paths:
- 'docs/**'
- 'shared/**'
**Jobs exécutés** : 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
- **Validation documentation** : Build MkDocs en mode strict (détecte erreurs markdown) docs-links:
- **Vérification liens** : Validation des liens internes/externes dans documentation runs-on: ubuntu-latest
- **Tests code partagé** : Exécution tests si du code partagé backend-mobile existe steps:
- uses: actions/checkout@v4
- uses: lycheeverse/lychee-action@v1
with:
args: 'docs/**/*.md'
**Environnement** : Ubuntu latest, Python 3.11+ 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)
--- ---
@@ -125,7 +256,6 @@ RoadWave est organisé en monorepo contenant backend Go, mobile Flutter, documen
| **Commit mixte (backend + mobile + docs)** | ✅ | ✅ | ✅ | ~13 min (parallèle) | | **Commit mixte (backend + mobile + docs)** | ✅ | ✅ | ✅ | ~13 min (parallèle) |
**Économie de temps** : **Économie de temps** :
- Commit backend-only : ~5 min (vs 15 min sans path filters) = **67% plus rapide** - Commit backend-only : ~5 min (vs 15 min sans path filters) = **67% plus rapide**
- Commit docs-only : ~30s (vs 15 min) = **97% plus rapide** - Commit docs-only : ~30s (vs 15 min) = **97% plus rapide**
@@ -134,7 +264,6 @@ RoadWave est organisé en monorepo contenant backend Go, mobile Flutter, documen
Les tests end-to-end dans `/features/e2e/` **déclenchent les deux workflows** (backend ET mobile) car ils testent l'intégration complète : Les tests end-to-end dans `/features/e2e/` **déclenchent les deux workflows** (backend ET mobile) car ils testent l'intégration complète :
**Exemples de features E2E** : **Exemples de features E2E** :
- `features/e2e/abonnements/` : Formulaire mobile → API Zitadel → API RoadWave → Mangopay → Confirmation UI - `features/e2e/abonnements/` : Formulaire mobile → API Zitadel → API RoadWave → Mangopay → Confirmation UI
- `features/e2e/error-handling/` : Perte réseau → Fallback mode offline → Reprise auto après reconnexion - `features/e2e/error-handling/` : Perte réseau → Fallback mode offline → Reprise auto après reconnexion
@@ -169,7 +298,6 @@ Les tests end-to-end dans `/features/e2e/` **déclenchent les deux workflows** (
**Complexité initiale** : setup plus complexe que workflow monolithique **Complexité initiale** : setup plus complexe que workflow monolithique
**Mitigation** : **Mitigation** :
- Utiliser des **composite actions** pour partager la config commune - Utiliser des **composite actions** pour partager la config commune
- Documentation claire dans ce ADR - Documentation claire dans ce ADR
- Coût initial faible (~2h setup) vs gains à long terme importants - Coût initial faible (~2h setup) vs gains à long terme importants
@@ -188,18 +316,15 @@ Les tests end-to-end dans `/features/e2e/` **déclenchent les deux workflows** (
### Plan d'Implémentation ### Plan d'Implémentation
**Phase 1** : Setup workflows de base (~1h) **Phase 1** : Setup workflows de base (~1h)
- Créer `backend.yml` avec jobs test + lint + build - Créer `backend.yml` avec jobs test + lint + build
- Créer `mobile.yml` avec jobs test + lint + build - Créer `mobile.yml` avec jobs test + lint + build
- Créer `shared.yml` avec validation docs - Créer `shared.yml` avec validation docs
**Phase 2** : Configuration path filters (~30 min) **Phase 2** : Configuration path filters (~30 min)
- Ajouter `paths:` à chaque workflow - Ajouter `paths:` à chaque workflow
- Tester avec commits isolés (backend-only, mobile-only, docs-only) - Tester avec commits isolés (backend-only, mobile-only, docs-only)
**Phase 3** : Optimisations (~30 min) **Phase 3** : Optimisations (~30 min)
- Ajouter caching (Go modules, Flutter dependencies, node_modules) - Ajouter caching (Go modules, Flutter dependencies, node_modules)
- Créer composite actions pour config partagée - Créer composite actions pour config partagée
- Ajouter badges status dans README - Ajouter badges status dans README
@@ -208,15 +333,31 @@ Les tests end-to-end dans `/features/e2e/` **déclenchent les deux workflows** (
### Validation ### Validation
**Scénarios de test à valider** : ```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
1. **Commit backend uniquement** : Modifications dans `/backend` → Vérifier exécution isolée de `backend.yml` # Test 2 : Commit mobile-only
2. **Commit mobile uniquement** : Modifications dans `/mobile` → Vérifier exécution isolée de `mobile.yml` git add mobile/
3. **Commit features E2E** : Modifications dans `/features/e2e` → Vérifier exécution conjointe de `backend.yml` ET `mobile.yml` git commit -m "test: mobile change"
4. **Commit documentation uniquement** : Modifications dans `/docs` → Vérifier exécution isolée de `shared.yml` git push
5. **Commit mixte** : Modifications backend + mobile + docs → Vérifier exécution des 3 workflows en parallèle # → Vérifier que SEULEMENT mobile.yml s'exécute
**Vérifications** : Consulter l'onglet "Actions" de GitHub pour confirmer quels workflows se sont déclenchés. # 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
```
--- ---
@@ -240,7 +381,6 @@ Les tests end-to-end dans `/features/e2e/` **déclenchent les deux workflows** (
⚠️ **Faux négatifs** : path filter mal configuré → test non exécuté → bug en production ⚠️ **Faux négatifs** : path filter mal configuré → test non exécuté → bug en production
**Mitigation** : **Mitigation** :
- Features E2E déclenchent toujours backend + mobile (safety net) - Features E2E déclenchent toujours backend + mobile (safety net)
- Tests de validation dans le plan d'implémentation - Tests de validation dans le plan d'implémentation
- Review obligatoire des modifications de workflows - Review obligatoire des modifications de workflows

View File

@@ -1,209 +0,0 @@
# 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)

View File

@@ -1,304 +0,0 @@
# 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)

View File

@@ -1,287 +0,0 @@
# 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)

View File

@@ -1,126 +0,0 @@
# Architecture Decision Records (ADR)
> Documentation des décisions architecturales importantes du projet RoadWave
## Vue d'ensemble
Les Architecture Decision Records (ADR) documentent les décisions techniques importantes prises au cours du développement de RoadWave. Chaque ADR suit un format standardisé : contexte, décision, alternatives considérées et conséquences.
## Index des ADR
### Core Architecture
| ADR | Titre | Statut | Date |
|-----|-------|--------|------|
| [ADR-001](001-langage-backend.md) | Langage Backend | ✅ Accepté | 2025-01-17 |
| [ADR-010](010-architecture-backend.md) | Architecture Backend | ✅ Accepté | 2025-01-25 |
| [ADR-011](011-orm-acces-donnees.md) | ORM et Accès Données | ✅ Accepté | 2025-01-26 |
| [ADR-012](012-frontend-mobile.md) | Frontend Mobile | ✅ Accepté | 2025-01-26 |
| [ADR-014](014-organisation-monorepo.md) | Organisation en Monorepo | ✅ Accepté | 2025-01-28 |
### Data & Infrastructure
| ADR | Titre | Statut | Date |
|-----|-------|--------|------|
| [ADR-005](005-base-de-donnees.md) | Base de données | ✅ Accepté | 2025-01-20 |
| [ADR-021](021-solution-cache.md) | Solution de Cache | ✅ Accepté | 2025-02-01 |
| [ADR-015](015-hebergement.md) | Hébergement | ✅ Accepté | 2025-01-28 |
| [ADR-019](019-geolocalisation-ip.md) | Géolocalisation par IP | ✅ Accepté | 2025-01-30 |
### Streaming & Content
| ADR | Titre | Statut | Date |
|-----|-------|--------|------|
| [ADR-002](002-protocole-streaming.md) | Protocole Streaming | ✅ Accepté | 2025-01-18 |
| [ADR-003](003-codec-audio.md) | Codec Audio | ✅ Accepté | 2025-01-19 |
| [ADR-004](004-cdn.md) | CDN | ✅ Accepté | 2025-01-20 |
### Security & Auth
| ADR | Titre | Statut | Date |
|-----|-------|--------|------|
| [ADR-006](006-chiffrement.md) | Chiffrement | ✅ Accepté | 2025-01-21 |
| [ADR-008](008-authentification.md) | Authentification | ✅ Accepté | 2025-01-24 |
| [ADR-025](025-securite-secrets.md) | Sécurité & Secrets | ✅ Accepté | 2025-02-04 |
### Testing & Quality
| ADR | Titre | Statut | Date |
|-----|-------|--------|------|
| [ADR-007](007-tests-bdd.md) | Tests BDD | ✅ Accepté | 2025-01-22 |
| [ADR-013](013-strategie-tests.md) | Stratégie Tests | ✅ Accepté | 2025-01-27 |
| [ADR-022](022-strategie-cicd-monorepo.md) | CI/CD Monorepo | ✅ Accepté | 2025-02-02 |
### Features & Operations
| ADR | Titre | Statut | Date |
|-----|-------|--------|------|
| [ADR-009](009-solution-paiement.md) | Solution Paiement | ✅ Accepté | 2025-01-24 |
| [ADR-016](016-service-emailing.md) | Service Emailing | ✅ Accepté | 2025-01-29 |
| [ADR-017](017-notifications-geolocalisees.md) | Notifications Géolocalisées | ✅ Accepté | 2025-01-30 |
| [ADR-018](018-librairies-go.md) | Librairies Go | ✅ Accepté | 2025-01-30 |
| [ADR-020](020-librairies-flutter.md) | Librairies Flutter | ✅ Accepté | 2025-01-31 |
| [ADR-023](023-architecture-moderation.md) | Architecture Modération | ✅ Accepté | 2025-02-03 |
| [ADR-024](024-monitoring-observabilite.md) | Monitoring & Observabilité | ✅ Accepté | 2025-02-03 |
## Vue d'ensemble technique consolidée
Pour une vue d'ensemble de l'architecture et de la stack technique, consultez [docs/TECHNICAL.md](../TECHNICAL.md).
## Légende des statuts
-**Accepté** : Décision validée et appliquée
- 🟡 **Proposé** : En discussion
- ⏸️ **Suspendu** : Temporairement en attente
-**Rejeté** : Décision rejetée (conservée pour historique)
- 🔄 **Révisé** : Décision modifiée, voir ADR plus récent
## Créer un nouvel ADR
Pour documenter une nouvelle décision architecturale :
1. Créer un fichier `XXX-titre-court.md` (numérotation séquentielle)
2. Utiliser le template suivant :
```markdown
# ADR-XXX : Titre de la décision
**Statut** : Proposé/Accepté/Rejeté
**Date** : YYYY-MM-DD
## Contexte
Pourquoi cette décision est nécessaire ? Quel problème résout-elle ?
## Décision
Quelle solution avons-nous choisie ?
## Alternatives considérées
Quelles autres options ont été évaluées ?
## Conséquences
### Positives
- Avantage 1
- Avantage 2
### Négatives
- Limitation 1
- Compromis accepté
## Références
- Liens vers documentation externe
- Benchmarks
- Articles de référence
```
3. Ajouter l'ADR dans ce fichier README.md et dans `mkdocs.yml`
---
**Dernière mise à jour** : 2026-02-07

View File

@@ -0,0 +1,319 @@
# Diagramme de Séquence : Cache Géospatial Redis
> Architecture du cache Redis Geospatial pour l'optimisation des requêtes de découverte de contenu géolocalisé.
## Vue d'ensemble
Le cache Redis Geospatial permet d'accélérer la recherche de contenus audio à proximité d'une position GPS en évitant des calculs PostGIS coûteux sur PostgreSQL à chaque requête.
**Performance** :
- Sans cache : ~200-500ms (calcul PostGIS sur 100K points)
- Avec cache : ~5-10ms (filtrage Redis en mémoire)
---
## Flux complet : Cold Start → Warm Cache
```mermaid
sequenceDiagram
participant User as 📱 Utilisateur<br/>(Paris)
participant Backend as 🔧 Backend Go
participant Redis as 🔴 Redis Geospatial<br/>(Cache)
participant PostgreSQL as 🗄️ PostgreSQL<br/>+ PostGIS
participant CDN as 🌐 NGINX Cache (OVH VPS)
Note over User,CDN: 🥶 Cold Start - Cache vide
User->>Backend: GET /contents?lat=48.8566&lon=2.3522&radius=50km
Backend->>Redis: EXISTS geo:catalog
Redis-->>Backend: false (cache vide)
Backend->>PostgreSQL: SELECT id, lat, lon, title, geo_level<br/>FROM contents WHERE active=true
Note over PostgreSQL: Tous les contenus actifs<br/>de la plateforme (métadonnées)
PostgreSQL-->>Backend: 100K contenus (métadonnées)
Backend->>Redis: GEOADD geo:catalog<br/>lon1 lat1 "content:1"<br/>lon2 lat2 "content:2"<br/>... (100K entrées)
Note over Redis: Stockage index spatial<br/>en mémoire (~20 MB)
Redis-->>Backend: OK (100000)
Backend->>Redis: EXPIRE geo:catalog 300
Note over Redis: TTL 5 minutes
Backend->>Redis: GEORADIUS geo:catalog<br/>2.3522 48.8566 50 km
Note over Redis: Filtrage spatial instantané<br/>(index geohash)
Redis-->>Backend: [content:123, content:456, ...]<br/>(~500 IDs dans rayon)
Backend->>PostgreSQL: SELECT * FROM contents<br/>WHERE id IN (123, 456, ...)<br/>AND geo_level = 'gps_precise'
Note over PostgreSQL: Récupération détails complets<br/>uniquement contenus proches
PostgreSQL-->>Backend: Détails complets (500 contenus GPS)
Backend->>PostgreSQL: SELECT * FROM contents<br/>WHERE city='Paris' OR dept='75'<br/>OR region='IDF' OR geo_level='national'
Note over PostgreSQL: Contenus par niveau géographique
PostgreSQL-->>Backend: Contenus ville/région/national
Backend->>Backend: Scoring & mixage :<br/>- GPS proche : 70%<br/>- Ville : 20%<br/>- Région : 8%<br/>- National : 2%
Backend-->>User: JSON: [{id, title, creator, audioUrl, score}, ...]<br/>(playlist mixée et scorée)
Note over User,CDN: 🎵 Lecture audio (requêtes séparées)
User->>CDN: GET /audio/content-123.m3u8
CDN-->>User: Playlist HLS
User->>CDN: GET /audio/content-123-segment-001.ts
CDN-->>User: Segment audio Opus
Note over User,CDN: 🔥 Warm Cache - Utilisateur 2 à Lyon (45km+)
participant User2 as 📱 Utilisateur 2<br/>(Lyon)
User2->>Backend: GET /contents?lat=45.7640&lon=4.8357&radius=50km
Backend->>Redis: EXISTS geo:catalog
Redis-->>Backend: true ✅ (cache chaud)
Backend->>Redis: GEORADIUS geo:catalog<br/>4.8357 45.7640 50 km
Note over Redis: Filtrage instantané<br/>sur cache existant
Redis-->>Backend: [content:789, content:012, ...]<br/>(~300 IDs différents)
Backend->>PostgreSQL: SELECT * FROM contents<br/>WHERE id IN (789, 012, ...)<br/>AND geo_level = 'gps_precise'
PostgreSQL-->>Backend: Détails complets
Backend->>PostgreSQL: SELECT * FROM contents<br/>WHERE city='Lyon' OR dept='69'<br/>OR region='Auvergne-RA' OR geo_level='national'
PostgreSQL-->>Backend: Contenus ville/région/national
Backend->>Backend: Scoring & mixage
Backend-->>User2: JSON: [{id, title, creator, audioUrl, score}, ...]
```
---
## Stratégie de cache
### Cache du catalogue complet (approche choisie)
**Principe** : Au premier cache miss, charger **TOUS** les contenus géolocalisés en une seule fois dans Redis.
**Avantages** :
- ✅ 1 seul cache miss au démarrage de l'instance
- ✅ Toutes les requêtes suivantes servies par Redis (n'importe quelle position GPS)
- ✅ Simple à gérer (1 seule clé Redis : `geo:catalog`)
- ✅ Pas de duplication de données
**Inconvénients** :
- ⚠️ Premier utilisateur subit le cold start (~500ms-1s)
- ⚠️ Nécessite charger toute la base (acceptable : ~20 MB pour 100K contenus)
**Alternatives non retenues** :
- Cache par zone géographique → cache miss fréquents, complexité gestion chevauchements
- Cache à la demande → trop de cache miss, pas d'optimisation réelle
---
## Détails techniques
### 1. Données stockées dans Redis
**Clé Redis** : `geo:catalog`
**Structure** :
```
GEOADD geo:catalog
2.3522 48.8566 "content:12345" # lon, lat, member
4.8357 45.7640 "content:67890"
...
```
**Taille mémoire** :
- ~200 bytes par entrée (ID + coordonnées + index geohash)
- 100K contenus = ~20 MB
- Négligeable pour Redis (plusieurs GB RAM disponibles)
**TTL** : 5 minutes (300 secondes)
- Le contenu géolocalisé est quasi-statique (change peu)
- Rechargement automatique toutes les 5 minutes si cache expiré
- Permet de propager les nouveaux contenus rapidement
### 2. Niveaux géographiques
Le cache Redis ne contient que les contenus avec **GPS précis** (`geo_level = 'gps_precise'`).
Les autres niveaux géographiques sont gérés par filtrage applicatif :
| Niveau | Stockage | Requête |
|--------|----------|---------|
| **GPS précis** | Redis + PostgreSQL | `GEORADIUS` puis `SELECT WHERE id IN (...)` |
| **Ville** | PostgreSQL uniquement | `SELECT WHERE city = ?` |
| **Département** | PostgreSQL uniquement | `SELECT WHERE department = ?` |
| **Région** | PostgreSQL uniquement | `SELECT WHERE region = ?` |
| **National** | PostgreSQL uniquement | `SELECT WHERE geo_level = 'national'` |
### 3. Commandes Redis utilisées
```bash
# Vérifier existence du cache
EXISTS geo:catalog
# Retour : 0 (n'existe pas) ou 1 (existe)
# Charger le catalogue complet (cold start)
GEOADD geo:catalog 2.3522 48.8566 "content:1" 4.8357 45.7640 "content:2" ...
# Retour : nombre d'éléments ajoutés
# Définir TTL 5 minutes
EXPIRE geo:catalog 300
# Rechercher contenus dans un rayon
GEORADIUS geo:catalog 2.3522 48.8566 50 km
# Retour : ["content:123", "content:456", ...]
# Optionnel : obtenir distance et coordonnées
GEORADIUS geo:catalog 2.3522 48.8566 50 km WITHDIST WITHCOORD
# Retour : [["content:123", "12.5", ["2.35", "48.85"]], ...]
```
### 4. Algorithme de scoring
Le backend mixe les résultats selon une pondération :
```
Score final =
(Pertinence GPS × 0.70) +
(Pertinence Ville × 0.20) +
(Pertinence Région × 0.08) +
(Pertinence National × 0.02)
```
**Critères de pertinence** :
- **GPS** : Plus proche = score élevé (distance inversée)
- **Ville/Région** : Matching exact = score maximal
- **National** : Score fixe faible (contenu générique)
**Mixage playlist** :
1. Trier tous les contenus par score décroissant
2. Appliquer diversité créateurs (pas 3 contenus du même créateur d'affilée)
3. Injecter contenus sponsorisés/mis en avant (futurs)
4. Retourner top 50 pour la session d'écoute
---
## Métriques de performance
### Temps de réponse typiques
| Scénario | Latence | Détail |
|----------|---------|--------|
| **Cold start** | 500-1000ms | Chargement 100K contenus dans Redis + requête |
| **Warm cache** | 5-10ms | `GEORADIUS` + `SELECT WHERE id IN (...)` |
| **TTL expiré** | 500-1000ms | Rechargement automatique |
### Charge serveurs
| Composant | Sans cache | Avec cache |
|-----------|------------|------------|
| **PostgreSQL CPU** | 60-80% | 10-20% |
| **Redis CPU** | N/A | 5-15% |
| **Throughput** | ~50 req/s | ~500 req/s |
---
## Cas limites et optimisations futures
### Cas limite 1 : Contenu très dense (Paris intra-muros)
**Problème** : 10K contenus dans rayon 5km → trop de résultats
**Solution actuelle** :
- Limiter résultats Redis à 1000 premiers (tri par distance)
- Scorer et filtrer côté application
**Optimisation future** :
- Ajuster rayon dynamiquement selon densité
- Utiliser `GEORADIUS ... COUNT 500` pour limiter côté Redis
### Cas limite 2 : Zones rurales (peu de contenu)
**Problème** : Rayon 50km retourne <10 contenus
**Solution actuelle** :
- Augmenter poids contenus région/national dans le scoring
- Suggérer contenus nationaux populaires
**Optimisation future** :
- Augmenter rayon automatiquement jusqu'à obtenir min 20 contenus
- `GEORADIUS ... 100 km` si rayon initial insuffisant
### Cas limite 3 : Nombreux créateurs actifs (évolutivité)
**Problème** : Cache 100K → 1M contenus (200 MB Redis)
**Solution actuelle** :
- 200 MB reste acceptable pour Redis
**Optimisation future** :
- Sharding géographique : cache Europe, cache USA, etc.
- Limiter cache aux contenus actifs 90 derniers jours
---
## Invalidation du cache
### Stratégies d'invalidation
| Événement | Action cache | Détail |
|-----------|--------------|--------|
| **Nouveau contenu publié** | Lazy (TTL) | Visible sous 5 minutes max |
| **Contenu supprimé/modéré** | Lazy (TTL) | Disparaît sous 5 minutes max |
| **Mise à jour GPS contenu** | Lazy (TTL) | Nouvelle position sous 5 minutes |
| **Déploiement backend** | Flush volontaire | `DEL geo:catalog` si schema change |
**Pas d'invalidation immédiate** pour simplifier l'architecture (cohérence éventuelle acceptable).
**Alternative future** :
- Pub/Sub Redis : notifier toutes les instances backend lors d'un changement
- `GEOADD geo:catalog 2.35 48.85 "content:new"` pour ajout immédiat
---
## Schéma simplifié
```
┌─────────────────────────────────────────────────────────┐
│ Utilisateur │
│ (Position GPS actuelle) │
└────────────────────────┬────────────────────────────────┘
│ GET /contents?lat=X&lon=Y&radius=Z
┌─────────────────────────────────────────────────────────┐
│ Backend Go (Fiber) │
│ │
│ 1. Vérifier cache Redis │
│ ├─ Cache HIT → GEORADIUS rapide │
│ └─ Cache MISS → Charger catalogue complet │
│ │
│ 2. Filtrer contenus GPS proches (Redis) │
│ 3. Récupérer contenus ville/région/national (PG) │
│ 4. Scorer et mixer selon pondération │
│ 5. Retourner playlist │
└────────────┬───────────────────────────┬────────────────┘
│ │
│ GEORADIUS │ SELECT détails
▼ ▼
┌─────────────────────┐ ┌───────────────────────────┐
│ Redis Geospatial │ │ PostgreSQL + PostGIS │
│ (Index spatial) │ │ (Données complètes) │
│ │ │ │
│ • geo:catalog │ │ • contents (détails) │
│ • TTL 5 min │ │ • users │
│ • ~20 MB mémoire │ │ • playlists │
└─────────────────────┘ └───────────────────────────┘
```
---
## Références
- [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)

View File

@@ -7,7 +7,6 @@
## Contexte ## Contexte
RoadWave est une app audio géolocalisée utilisée en conduite (CarPlay/Android Auto) avec : RoadWave est une app audio géolocalisée utilisée en conduite (CarPlay/Android Auto) avec :
- Contenu généré par utilisateurs (UGC) - Contenu généré par utilisateurs (UGC)
- Monétisation : publicités géolocalisées + Premium (4.99€ web / 5.99€ IAP) - Monétisation : publicités géolocalisées + Premium (4.99€ web / 5.99€ IAP)
- GPS en arrière-plan - GPS en arrière-plan
@@ -16,7 +15,6 @@ RoadWave est une app audio géolocalisée utilisée en conduite (CarPlay/Android
## Stratégie de conformité ## Stratégie de conformité
**Approche multi-plateforme** avec : **Approche multi-plateforme** avec :
- Modération UGC robuste (IA + humain) - Modération UGC robuste (IA + humain)
- Prix différenciés selon région (US/EU/Monde) - Prix différenciés selon région (US/EU/Monde)
- GPS avec disclosure complète - GPS avec disclosure complète
@@ -34,7 +32,6 @@ RoadWave est une app audio géolocalisée utilisée en conduite (CarPlay/Android
## Conformité détaillée ## Conformité détaillée
### Android Auto / CarPlay ✅ ### Android Auto / CarPlay ✅
- 100% audio (pas de vidéo) - 100% audio (pas de vidéo)
- Commandes standard au volant - Commandes standard au volant
- Aucun achat in-car - Aucun achat in-car
@@ -45,19 +42,16 @@ RoadWave est une app audio géolocalisée utilisée en conduite (CarPlay/Android
### Google Play ⚠️ ### Google Play ⚠️
**UGC (critique)** : **UGC (critique)** :
- Modération hybride IA + humain ✅ - Modération hybride IA + humain ✅
- 3 premiers contenus validés manuellement ✅ - 3 premiers contenus validés manuellement ✅
- Système de strikes (4 = ban) ✅ - Système de strikes (4 = ban) ✅
- Signalement + blocage utilisateurs ✅ - Signalement + blocage utilisateurs ✅
**GPS Background (critique)** : **GPS Background (critique)** :
- Permission "Always Location" = **OPTIONNELLE** - Permission "Always Location" = **OPTIONNELLE**
- Demandée uniquement pour mode piéton (notifications arrière-plan audio-guides) - Demandée uniquement pour mode piéton (notifications arrière-plan audio-guides)
- Justification Play Console : - Justification Play Console :
> "RoadWave permet aux utilisateurs de recevoir des alertes audio-guides lorsqu'ils passent à pied près de monuments/musées, même quand l'app est en arrière-plan. Cette fonctionnalité est optionnelle et peut être désactivée dans les paramètres." > "RoadWave permet aux utilisateurs de recevoir des alertes audio-guides lorsqu'ils passent à pied près de monuments/musées, même quand l'app est en arrière-plan. Cette fonctionnalité est optionnelle et peut être désactivée dans les paramètres."
- In-app disclosure obligatoire (écran dédié avant demande permission) - In-app disclosure obligatoire (écran dédié avant demande permission)
- Si refusée : app fonctionne en mode voiture uniquement - Si refusée : app fonctionne en mode voiture uniquement
- **Action** : Remplir formulaire background location Play Console avec justification - **Action** : Remplir formulaire background location Play Console avec justification
@@ -74,32 +68,27 @@ RoadWave est une app audio géolocalisée utilisée en conduite (CarPlay/Android
### App Store ⚠️ ### App Store ⚠️
**Prix différenciés (légaux depuis 2025-2026)** : **Prix différenciés (légaux depuis 2025-2026)** :
- 🇺🇸 US : Lien externe autorisé (0% commission) - 🇺🇸 US : Lien externe autorisé (0% commission)
- 🇪🇺 EU : Paiement externe DMA (7-20% commission réduite) - 🇪🇺 EU : Paiement externe DMA (7-20% commission réduite)
- 🌍 Monde : IAP obligatoire (30% commission) - 🌍 Monde : IAP obligatoire (30% commission)
**UGC** : **UGC** :
- Mode Kids obligatoire (filtrage selon âge) ✅ - Mode Kids obligatoire (filtrage selon âge) ✅
- Système de modération + signalement ✅ - Système de modération + signalement ✅
**GPS Background (critique)** : **GPS Background (critique)** :
- Permission "Always Location" = **OPTIONNELLE** - Permission "Always Location" = **OPTIONNELLE**
- Deux strings Info.plist requises : - Deux strings Info.plist requises :
- `NSLocationWhenInUseUsageDescription` : explication mode voiture - `NSLocationWhenInUseUsageDescription` : explication mode voiture
- `NSLocationAlwaysAndWhenInUseUsageDescription` : explication mode piéton (optionnel) - `NSLocationAlwaysAndWhenInUseUsageDescription` : explication mode piéton (optionnel)
- In-app disclosure obligatoire avant demande "Always" - In-app disclosure obligatoire avant demande "Always"
- Flux two-step : When In Use → Always (si user active mode piéton) - Flux two-step : When In Use → Always (si user active mode piéton)
- Si refusée : app fonctionne en mode voiture uniquement - Si refusée : app fonctionne en mode voiture uniquement
- **Action** : Voir strings détaillés dans [05-interactions-navigation.md](../domains/recommendation/rules/interactions-navigation.md#512-mode-piéton-audio-guides) - **Action** : Voir strings détaillés dans [05-interactions-navigation.md](../regles-metier/05-interactions-navigation.md#512-mode-piéton-audio-guides)
### Revenus créateurs ### Revenus créateurs
**Position** : Paiements créateurs = "services" (comme YouTube/Uber), pas IAP **Position** : Paiements créateurs = "services" (comme YouTube/Uber), pas IAP
- Paiement via Mangopay Connect (externe) - Paiement via Mangopay Connect (externe)
- Commission stores uniquement sur Premium (IAP) - Commission stores uniquement sur Premium (IAP)
- Comparables : YouTube AdSense, TikTok Creator Fund, Uber - Comparables : YouTube AdSense, TikTok Creator Fund, Uber
@@ -121,14 +110,12 @@ RoadWave est une app audio géolocalisée utilisée en conduite (CarPlay/Android
## Stratégie de lancement ## Stratégie de lancement
**Phase 1 - MVP** : **Phase 1 - MVP** :
- IAP uniquement (5.99€/mois mondial) - IAP uniquement (5.99€/mois mondial)
- Modération UGC active - Modération UGC active
- GPS avec disclosure - GPS avec disclosure
- CarPlay/Android Auto basique - CarPlay/Android Auto basique
**Phase 2 - Post-validation** : **Phase 2 - Post-validation** :
- Prix différenciés US (lien externe 4.99€) - Prix différenciés US (lien externe 4.99€)
- Paiement externe EU (DMA) - Paiement externe EU (DMA)
- Monétisation créateurs (Mangopay) - Monétisation créateurs (Mangopay)

View File

@@ -1,14 +0,0 @@
FROM squidfunk/mkdocs-material:latest
# Install mkdocs-kroki-plugin
RUN pip install --no-cache-dir mkdocs-kroki-plugin
# Set working directory
WORKDIR /docs
# Expose MkDocs port
EXPOSE 8000
# Default command
ENTRYPOINT ["mkdocs"]
CMD ["serve", "--dev-addr=0.0.0.0:8000"]

View File

@@ -1,241 +0,0 @@
# 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)
```
## 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

View File

@@ -1,38 +0,0 @@
# 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/vue-ensemble.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)

View File

@@ -1,57 +0,0 @@
# Account Deletions
📖 Suppressions de compte avec grace period 30 jours (Article 17 RGPD)
## Diagramme
```kroki-dbml
Table users {
id uuid [primary key]
email varchar(255)
status varchar(20)
}
Table account_deletions {
id uuid [primary key]
user_id uuid [not null, unique, ref: - users.id, note: 'One-to-one: un user ne peut avoir qu une seule demande active']
status deletion_status_enum [not null, default: 'pending']
cancellation_token varchar(64) [unique, note: 'Token dans email pour annuler (expire après 30j)']
requested_at timestamp [not null, default: `now()`]
effective_at timestamp [not null, note: 'Auto-calculated: requested_at + 30 days']
cancelled_at timestamp [note: 'Timestamp annulation via lien email (NULL si non annulé)']
deleted_at timestamp [note: 'Timestamp suppression effective (NULL si pending/cancelled)']
deletion_reason text [note: 'Raison optionnelle fournie par l utilisateur']
deleted_data_summary jsonb [note: 'Résumé des données supprimées (audit trail)']
indexes {
(user_id) [unique]
(status, effective_at) [note: 'Daily cron job: WHERE status = pending AND effective_at < NOW()']
(cancellation_token) [unique]
}
}
Enum deletion_status_enum {
pending [note: 'Grace period actif (30j), compte désactivé, annulation possible']
cancelled [note: 'Utilisateur a annulé via lien email']
completed [note: 'Suppression effective réalisée après 30j']
}
```
## Légende
**Statuts** :
- `pending`: Grace period actif (30j), compte désactivé, annulation possible
- `cancelled`: Utilisateur a annulé via lien email
- `completed`: Suppression effective réalisée après 30j
**Processus** :
1. Demande → compte désactivé, contenus cachés
2. Email avec `cancellation_token` (valide 30j)
3. Si annulation → `status = cancelled`, compte réactivé
4. Si 30j écoulés → job cron supprime données, anonymise contenus
**Données supprimées** :
- Profil utilisateur, historique GPS/écoute, sessions
- Contenus créés : anonymisés (`créateur = "Utilisateur supprimé"`)

View File

@@ -1,78 +0,0 @@
# Breach Incidents
📖 Registre violations de données (Article 33 RGPD)
## Diagramme
```kroki-dbml
Table breach_incidents {
id uuid [primary key]
severity breach_severity_enum [not null]
description text [not null, note: 'Description détaillée de l incident']
data_categories_affected jsonb [not null, note: 'Array: ["gps", "email", "listening_history"]']
estimated_users_count int [not null, note: 'Estimation nombre users impactés']
detected_at timestamp [not null, default: `now()`, note: 'H+0: Détection initiale']
contained_at timestamp [note: 'Timestamp confinement de la faille']
cnil_notified_at timestamp [note: 'H+48: Notification CNIL si requis']
users_notified_at timestamp [note: 'H+72: Notification users si risque élevé']
mitigation_actions text [note: 'Actions correctives mises en place']
cnil_notification_required boolean [not null, default: false]
user_notification_required boolean [not null, default: false]
indexes {
(severity, detected_at) [note: 'Incidents par gravité et chronologie']
(cnil_notification_required, cnil_notified_at) [note: 'Track CNIL notification compliance']
}
}
Table users {
id uuid [primary key]
}
Table breach_affected_users {
id uuid [primary key]
breach_id uuid [not null, ref: > breach_incidents.id]
user_id uuid [not null, ref: > users.id]
notified_at timestamp [note: 'Timestamp notification user (NULL si pas encore notifié)']
notification_channel notification_channel_enum [note: 'Canal utilisé pour notifier']
indexes {
(breach_id, user_id) [unique, note: 'Un user ne peut être listé qu une fois par incident']
(breach_id, notified_at) [note: 'Track notification progress']
(user_id) [note: 'Historique incidents pour un user']
}
}
Enum breach_severity_enum {
low [note: 'Pas de notification requise (mesures techniques suffisantes)']
medium [note: 'Notification CNIL uniquement']
high [note: 'Notification CNIL + utilisateurs']
critical [note: 'Notification immédiate tous canaux + SMS fondateur']
}
Enum notification_channel_enum {
email [note: 'Email notification']
push [note: 'Push notification mobile']
sms [note: 'SMS (critical only)']
}
```
## Légende
**Sévérité** :
- `low`: Pas de notification requise (mesures techniques suffisantes)
- `medium`: Notification CNIL uniquement
- `high`: Notification CNIL + utilisateurs
- `critical`: Notification immédiate tous canaux + SMS fondateur
**Timeline 72h** :
- H+0 : Détection, confinement
- H+24 : Évaluation gravité
- H+48 : Notification CNIL si requis
- H+72 : Notification utilisateurs si risque élevé
**Catégories de données** :
- `data_categories_affected`: JSON `["gps", "email", "listening_history"]`

View File

@@ -1,56 +0,0 @@
# User Consents
📖 Consentements RGPD avec historique et versioning
## Diagramme
```kroki-dbml
Table users {
id uuid [primary key]
}
Table user_consents {
id uuid [primary key]
user_id uuid [not null, ref: > users.id]
consent_type consent_type_enum [not null]
consent_version varchar(10) [not null, note: 'Format: v1.0, v2.0, etc.']
accepted boolean [not null, note: 'true = opted-in, false = opted-out']
given_at timestamp [not null, default: `now()`]
ip_address inet [not null, note: 'Proof of consent for CNIL audits']
user_agent text [not null, note: 'Device/browser proof']
indexes {
(user_id, consent_type, consent_version) [note: 'Latest consent per type']
(user_id, given_at) [note: 'Consent history timeline']
}
}
Enum consent_type_enum {
geolocation_precise [note: 'Géolocalisation GPS précise (obligatoire pour contenu hyperlocal)']
analytics [note: 'Analytics Matomo (optionnel)']
push_notifications [note: 'Notifications push (optionnel)']
cookies_analytics [note: 'Cookies analytiques (optionnel)']
}
```
## Légende
**Types de consentement** :
- `geolocation_precise` : Géolocalisation GPS précise (obligatoire pour contenu hyperlocal)
- `analytics` : Analytics Matomo (optionnel)
- `push_notifications` : Notifications push (optionnel)
- `cookies_analytics` : Cookies analytiques (optionnel)
**Versioning** :
- Chaque changement de CGU/politique = nouvelle version
- Historique complet conservé (preuve légale)
- Format version : `v1.0`, `v2.0`, etc.
**Conformité RGPD** :
- Granularité : fonctionnel / analytique / marketing
- Consentement libre et éclairé
- Révocable à tout moment
- Historique = preuve en cas de contrôle CNIL

View File

@@ -1,56 +0,0 @@
# Data Retention Logs
📖 Logs purges automatiques inactivité (Article 5 RGPD - Minimisation)
## Diagramme
```kroki-dbml
Table data_retention_logs {
id uuid [primary key]
action_type retention_action_enum [not null]
users_processed int [not null, default: 0, note: 'Nombre total users analysés']
users_warned int [not null, default: 0, note: 'Nombre users notifiés (90j/30j/7j)']
users_deleted int [not null, default: 0, note: 'Nombre users supprimés effectivement']
details jsonb [note: 'Détails: threshold_date, user_ids_deleted, notifications_sent']
executed_at timestamp [not null, default: `now()`, note: 'Timestamp exécution du job cron']
execution_duration_ms bigint [not null, note: 'Durée d exécution en millisecondes']
indexes {
(action_type, executed_at) [note: 'Historique jobs par type']
(executed_at) [note: 'Timeline complète des jobs']
}
}
Enum retention_action_enum {
check_inactive [note: 'Vérification quotidienne comptes inactifs > 5 ans']
send_warnings [note: 'Envoi notifications (90j/30j/7j avant suppression)']
delete_accounts [note: 'Suppression effective comptes inactifs']
}
```
## Légende
**Action types** :
- `check_inactive`: Vérification quotidienne comptes inactifs > 5 ans
- `send_warnings`: Envoi notifications (90j/30j/7j avant suppression)
- `delete_accounts`: Suppression effective comptes inactifs
**Règles de conservation** :
- Auditeur : 5 ans inactivité → suppression
- Créateur actif : jamais (tant que contenus écoutés)
- Créateur inactif : 5 ans + 2 ans sans écoute → suppression
**Details JSON** :
```json
{
"threshold_date": "2021-02-08",
"user_ids_deleted": ["uuid1", "uuid2"],
"notifications_sent": {
"90_days": 15,
"30_days": 8,
"7_days": 3
}
}
```

View File

@@ -1,65 +0,0 @@
# Devices
📖 Appareils de confiance et gestion multi-device
## Diagramme
```kroki-dbml
Table users {
id uuid [primary key]
}
Table devices {
id uuid [primary key]
user_id uuid [not null, ref: > users.id]
device_name varchar(255) [note: 'User-defined device name']
os varchar(50) [note: 'iOS, Android, Windows, macOS, Linux']
browser varchar(50) [note: 'Safari, Chrome, Firefox, etc.']
device_type device_type_enum [not null, note: 'mobile, tablet, desktop, car']
is_trusted boolean [not null, default: false, note: 'Bypass 2FA for 30 days if true']
trusted_until timestamp [note: 'NULL if not trusted, expires after 30 days']
first_seen_at timestamp [not null, default: `now()`]
last_seen_at timestamp [not null, default: `now()`]
last_ip inet [not null]
last_city varchar(100)
last_country_code char(2)
indexes {
(user_id, last_seen_at) [note: 'List user devices by recent activity']
(user_id, is_trusted) [note: 'Find trusted devices for user']
}
}
Table sessions {
id uuid [primary key]
device_id uuid [ref: > devices.id]
}
Enum device_type_enum {
mobile [note: 'Smartphone Android/iOS']
tablet [note: 'Tablette']
desktop [note: 'Ordinateur']
car [note: 'Système embarqué (CarPlay/Android Auto)']
}
```
## Légende
**Types d'appareil** :
- `mobile` : Smartphone Android/iOS
- `tablet` : Tablette
- `desktop` : Ordinateur
- `car` : Système embarqué (CarPlay/Android Auto)
**Appareil de confiance** :
- Option "Ne plus demander sur cet appareil" → bypass 2FA pendant **30 jours**
- Révocable depuis paramètres compte
- Liste des appareils de confiance visible
**Sécurité** :
- Détection automatique nouveau device → notification push + email
- Localisation suspecte (pays différent) → alerte
- Révocation individuelle ou globale possible

View File

@@ -1,77 +0,0 @@
# Data Exports
📖 Exports de données utilisateur (portabilité RGPD Article 20)
## Diagramme
```kroki-dbml
Table users {
id uuid [primary key]
}
Table data_exports {
id uuid [primary key]
user_id uuid [not null, ref: > users.id]
status export_status_enum [not null, default: 'pending']
export_url varchar(512) [note: 'S3/CDN signed URL (NULL until generated)']
size_bytes bigint [note: 'File size in bytes (NULL until generated)']
format export_format_enum [not null, default: 'json']
requested_at timestamp [not null, default: `now()`]
generated_at timestamp [note: 'When export file was created (NULL if pending/generating)']
expires_at timestamp [note: 'Auto-calculated: generated_at + 7 days']
downloaded_at timestamp [note: 'First download timestamp (NULL if not yet downloaded)']
indexes {
(user_id, requested_at) [note: 'User export history']
(status, requested_at) [note: 'Background worker queue (WHERE status = pending)']
(expires_at) [note: 'Daily cleanup job (DELETE WHERE expires_at < NOW())']
}
}
Enum export_status_enum {
pending [note: 'Demande en file d attente']
generating [note: 'Génération en cours (worker background)']
ready [note: 'Export disponible au téléchargement']
downloaded [note: 'Export téléchargé par l utilisateur']
expired [note: 'Export expiré (supprimé automatiquement)']
}
Enum export_format_enum {
json [note: 'Machine-readable (données brutes)']
html [note: 'Human-readable (page web stylée)']
zip [note: 'Archive complète (JSON + HTML + audio files)']
}
```
## Légende
**Formats d'export** :
- `json` : Machine-readable (données brutes)
- `html` : Human-readable (page web stylée)
- `zip` : Archive complète (JSON + HTML + audio files)
**Contenu de l'export** :
- Profil utilisateur (email, pseudo, date inscription, bio)
- Historique d'écoute (titres, dates, durées)
- Contenus créés (audio + métadonnées)
- Abonnements et likes
- Centres d'intérêt (jauges)
- Historique consentements RGPD
**Statuts** :
- `pending` : Demande en file d'attente
- `generating` : Génération en cours (worker background)
- `ready` : Export disponible au téléchargement
- `downloaded` : Export téléchargé par l'utilisateur
- `expired` : Export expiré (supprimé automatiquement)
**Règles** :
- Génération asynchrone (worker background)
- Délai max : **48h** (conformité RGPD)
- Conservation : **7 jours** après génération
- Limite : **1 export/mois** (anti-abus)
- Notification par email avec lien de téléchargement

View File

@@ -1,67 +0,0 @@
# Interest Gauges
📖 Jauges de centres d'intérêt dynamiques pour recommandation personnalisée
## Diagramme
```kroki-dbml
Table users {
id uuid [primary key]
}
Table interest_gauges {
id uuid [primary key]
user_id uuid [not null, ref: > users.id]
category interest_category_enum [not null]
score decimal(5,2) [not null, default: 0, note: 'Range: 0.00 to 100.00']
last_updated timestamp [not null, default: `now()`]
interactions_count int [not null, default: 0, note: 'Total interactions for this category']
indexes {
(user_id, category) [unique, note: 'One gauge per user per category']
(user_id, score) [note: 'Order categories by score for recommendations']
}
}
Enum interest_category_enum {
automobile [note: 'Voitures, mécanique, course automobile']
travel [note: 'Voyages, tourisme, découverte']
music [note: 'Musique, concerts, artistes']
news [note: 'Actualités, politique, économie']
sport [note: 'Sports, événements sportifs']
culture [note: 'Cinéma, livres, expositions']
food [note: 'Gastronomie, restaurants, recettes']
tech [note: 'Technologie, innovation, gadgets']
history [note: 'Histoire, patrimoine, musées']
nature [note: 'Nature, randonnée, écologie']
}
```
## Légende
**Catégories** :
- `automobile` : Voitures, mécanique, course automobile
- `travel` : Voyages, tourisme, découverte
- `music` : Musique, concerts, artistes
- `news` : Actualités, politique, économie
- `sport` : Sports, événements sportifs
- `culture` : Cinéma, livres, expositions
- `food` : Gastronomie, restaurants, recettes
- `tech` : Technologie, innovation, gadgets
- `history` : Histoire, patrimoine, musées
- `nature` : Nature, randonnée, écologie
**Score** :
- Échelle : **0-100**
- Augmentation : +2% par like, +5% par abonnement créateur
- Diminution : -1% par skip rapide (<30s), -5% par signalement
- Calcul combiné : Distance GPS + matching intérêts
**Algorithme recommandation** :
- **70% géolocalisation** : Proximité GPS
- **30% centres d'intérêt** : Score jauges
- Boost si créateur suivi : +0.3 au score final
- Limite : 6 contenus/heure pour éviter spam

View File

@@ -1,66 +0,0 @@
# Location History
📖 Historique de géolocalisation avec anonymisation automatique
## Diagramme
```kroki-dbml
Table users {
id uuid [primary key]
}
Table location_history {
id uuid [primary key]
user_id uuid [not null, ref: > users.id]
location geography [note: 'PostGIS geography type: POINT with SRID 4326 (WGS84)']
geohash varchar(12) [note: 'Precision 5 geohash (~5km²) after anonymization']
anonymized boolean [not null, default: false, note: 'true after 24h auto-anonymization']
context location_context_enum [not null]
speed_kmh float [note: 'GPS speed in km/h (NULL if stationary)']
accuracy_meters float [not null, note: 'GPS accuracy radius in meters']
created_at timestamp [not null, default: `now()`]
anonymized_at timestamp [note: 'When precise location was replaced by geohash']
indexes {
(user_id, created_at) [note: 'User location timeline']
(created_at, anonymized) [note: 'Daily anonymization job (WHERE anonymized = false AND created_at < NOW() - 24h)']
(location) [type: gist, note: 'PostGIS spatial index for proximity queries']
(geohash) [note: 'Analytics queries on anonymized data']
}
}
Enum location_context_enum {
listening [note: 'Position pendant écoute de contenu']
search [note: 'Position lors d une recherche']
background [note: 'Tracking en arrière-plan']
manual [note: 'Position partagée manuellement']
}
```
## Légende
**Anonymisation progressive** :
- Données précises conservées **24h** (recommandation personnalisée)
- Après 24h : conversion en **geohash précision 5** (~5km²)
- Coordonnées originales supprimées définitivement
- Job quotidien PostGIS automatique
**Exceptions** :
- Historique personnel visible (liste trajets) : conservation intégrale tant que compte actif
- Analytics globales : uniquement geohash anonyme
- Suppression complète si suppression du compte
**Contexte** :
- `listening` : Position pendant écoute de contenu
- `search` : Position lors d'une recherche
- `background` : Tracking en arrière-plan
- `manual` : Position partagée manuellement
**Conformité RGPD** :
- Vraie anonymisation (CNIL compliant)
- Permet analytics agrégées (heatmaps trafic)
- PostGIS natif, 0€

View File

@@ -1,61 +0,0 @@
# Parental Consents
📖 Consentements parentaux pour utilisateurs 13-15 ans (Article 8 RGPD)
## Diagramme
```kroki-dbml
Table users {
id uuid [primary key]
birthdate date [not null]
}
Table parental_consents {
id uuid [primary key]
user_id uuid [not null, unique, ref: > users.id, note: 'Ado 13-15 ans (1 consent par user max)']
parent_email varchar(255) [not null, note: 'Email du parent pour validation']
validation_token varchar(64) [unique, note: 'Token de validation envoyé par email (expire 7j)']
validated boolean [not null, default: false, note: 'true après clic parent sur lien email']
token_expires_at timestamp [not null, note: 'validation_token expire après 7 jours']
validated_at timestamp [note: 'Timestamp de validation parent (NULL si non validé)']
parent_ip inet [note: 'IP du parent lors de la validation']
parent_user_agent text [note: 'User agent parent (preuve validation)']
revoked_at timestamp [note: 'Révocation du consentement parental']
revocation_reason text [note: 'Raison de la révocation (optionnel)']
indexes {
(user_id) [unique, note: 'Un seul consentement parental actif par user']
(validation_token) [unique, note: 'Lookup rapide pour validation lien email']
(validated, token_expires_at) [note: 'Cleanup des tokens expirés non validés']
}
}
Table parental_controls {
id uuid [primary key]
parental_consent_id uuid [not null, unique, ref: - parental_consents.id, note: 'One-to-one relationship']
gps_enabled boolean [not null, default: false, note: 'Autoriser GPS précis (false = GeoIP uniquement)']
messaging_enabled boolean [not null, default: false, note: 'Autoriser messagerie privée']
content_16plus_enabled boolean [not null, default: false, note: 'Autoriser contenu 16+']
weekly_digest_config jsonb [note: 'Config notifications hebdo parent (email, contenu, format)']
updated_at timestamp [not null, default: `now()`]
indexes {
(parental_consent_id) [unique]
}
}
```
## Légende
**Workflow** :
1. Ado saisit email parent → `validation_token` généré (expire 7j)
2. Parent clique lien → `validated = true`
3. Parent configure `PARENTAL_CONTROLS`
4. Révocation possible → `revoked_at` renseigné
**Restrictions par défaut (13-15 ans)** :
- `gps_enabled`: `false` (GeoIP uniquement)
- `messaging_enabled`: `false`
- `content_16plus_enabled`: `false`
- Dashboard parent : notifications hebdomadaires activité

View File

@@ -1,54 +0,0 @@
# Privacy Policy Versions
📖 Versioning politique de confidentialité (Articles 13-14 RGPD)
## Diagramme
```kroki-dbml
Table privacy_policy_versions {
id uuid [primary key]
version varchar(10) [not null, unique, note: 'Format: v1.0, v2.0, etc.']
content_markdown text [not null, note: 'Source: docs/legal/politique-confidentialite.md (versionné Git)']
major_change boolean [not null, default: false, note: 'true = popup obligatoire pour tous les users']
changelog text [not null, note: 'Résumé des changements pour communication']
effective_date timestamp [not null, note: 'Date d entrée en vigueur de cette version']
created_at timestamp [not null, default: `now()`]
indexes {
(version) [unique]
(effective_date) [note: 'Order versions chronologically']
}
}
Table users {
id uuid [primary key]
}
Table user_policy_acceptances {
id uuid [primary key]
user_id uuid [not null, ref: > users.id]
policy_version_id uuid [not null, ref: > privacy_policy_versions.id]
accepted boolean [not null, note: 'true = accepté, false = refusé (compte gelé)']
accepted_at timestamp [not null, default: `now()`]
ip_address inet [not null, note: 'IP de l utilisateur lors de l acceptation (preuve CNIL)']
indexes {
(user_id, policy_version_id) [unique, note: 'Un user ne peut accepter qu une fois une version']
(user_id, accepted_at) [note: 'Historique acceptations user']
(policy_version_id) [note: 'Count acceptances par version']
}
}
```
## Légende
**Versioning** :
- `major_change`: `true` → popup obligatoire pour tous les utilisateurs
- `major_change`: `false` → notification simple
- Fichier source : `docs/legal/politique-confidentialite.md` (versionné Git)
**Popup si changement majeur** :
- Utilisateur doit accepter nouvelle version pour continuer
- Refus → compte gelé (lecture seule)

View File

@@ -1,105 +0,0 @@
# Reports
📖 Signalements de contenu et workflow de modération
## Diagramme
```kroki-dbml
Table users {
id uuid [primary key]
username varchar(50)
}
Table contents {
id uuid [primary key]
title varchar(255)
user_id uuid [not null]
}
Table reports {
id uuid [primary key]
content_id uuid [not null, ref: > contents.id, note: 'Content being reported']
reporter_id uuid [not null, ref: > users.id, note: 'User who filed the report']
moderator_id uuid [ref: > users.id, note: 'Moderator assigned to review (NULL if pending)']
category report_category_enum [not null]
status report_status_enum [not null, default: 'pending']
comment text [note: 'Reporter explanation (mandatory for "other" category)']
evidence_url varchar(512) [note: 'Screenshot or additional proof URL']
reported_at timestamp [not null, default: `now()`]
reviewed_at timestamp [note: 'When moderator reviewed the report']
moderator_notes text [note: 'Internal moderator notes']
action_taken report_action_enum [note: 'Action decided by moderator']
indexes {
(content_id, status) [note: 'Find all reports for a content']
(status, reported_at) [note: 'Queue for moderators (pending first)']
(reporter_id) [note: 'User report history (detect abuse)']
(moderator_id) [note: 'Reports assigned to moderator']
}
}
Enum report_category_enum {
spam [note: 'Contenu publicitaire non sollicité']
hate_speech [note: 'Discours haineux, discrimination']
violence [note: 'Violence explicite']
sexual_content [note: 'Contenu sexuel inapproprié']
misinformation [note: 'Désinformation, fake news']
copyright [note: 'Violation de droits d auteur']
wrong_age_rating [note: 'Classification d âge incorrecte']
other [note: 'Autre raison (commentaire obligatoire)']
}
Enum report_status_enum {
pending [note: 'En attente de revue']
under_review [note: 'En cours d examen par modérateur']
actioned [note: 'Action prise (contenu retiré/édité)']
dismissed [note: 'Signalement rejeté (contenu valide)']
duplicate [note: 'Doublon d un signalement existant']
}
Enum report_action_enum {
content_removed [note: 'Contenu supprimé']
content_edited [note: 'Métadonnées modifiées (âge, tags)']
warning_sent [note: 'Avertissement au créateur']
strike_issued [note: 'Strike ajouté au créateur']
account_suspended [note: 'Compte créateur suspendu']
no_action [note: 'Aucune action (signalement infondé)']
}
```
## Légende
**Catégories de signalement** :
- `spam` : Contenu publicitaire non sollicité
- `hate_speech` : Discours haineux, discrimination
- `violence` : Violence explicite
- `sexual_content` : Contenu sexuel inapproprié
- `misinformation` : Désinformation, fake news
- `copyright` : Violation de droits d'auteur
- `wrong_age_rating` : Classification d'âge incorrecte
- `other` : Autre raison (commentaire obligatoire)
**Statuts** :
- `pending` : En attente de revue
- `under_review` : En cours d'examen par modérateur
- `actioned` : Action prise (contenu retiré/édité)
- `dismissed` : Signalement rejeté (contenu valide)
- `duplicate` : Doublon d'un signalement existant
**Actions possibles** :
- `content_removed` : Contenu supprimé
- `content_edited` : Métadonnées modifiées (âge, tags)
- `warning_sent` : Avertissement au créateur
- `strike_issued` : Strike ajouté au créateur
- `account_suspended` : Compte créateur suspendu
- `no_action` : Aucune action (signalement infondé)
**Workflow modération** :
- **3 premiers contenus** : Modération préalable obligatoire
- **Après validation** : Modération a posteriori (signalements)
- **Priorisation** : Nombre de signalements (>3 = urgent)
- **Délai de traitement** : <48h pour signalements critiques

View File

@@ -1,64 +0,0 @@
# Sessions
📖 Gestion des sessions utilisateur et tokens d'authentification OAuth2/OIDC
## Diagramme
```kroki-dbml
Table users {
id uuid [primary key]
email varchar(255) [not null, unique]
username varchar(50) [not null, unique]
}
Table devices {
id uuid [primary key]
user_id uuid [not null, ref: > users.id]
device_name varchar(255)
device_type varchar(50)
}
Table sessions {
id uuid [primary key]
user_id uuid [not null, ref: > users.id]
device_id uuid [ref: > devices.id]
access_token_hash varchar(64) [not null, note: 'SHA256 hash, never stored in clear']
refresh_token_hash varchar(64) [not null, note: 'SHA256 hash, auto-rotated']
access_token_expires_at timestamp [not null, note: 'Lifetime: 15 minutes']
refresh_token_expires_at timestamp [not null, note: 'Lifetime: 30 days (rolling)']
ip_address inet [not null]
user_agent text [not null]
city varchar(100)
country_code char(2)
created_at timestamp [not null, default: `now()`]
last_activity_at timestamp [not null, default: `now()`]
revoked_at timestamp [note: 'NULL if active, timestamp if manually revoked']
indexes {
(user_id, revoked_at) [note: 'Find active sessions for user']
(device_id)
(refresh_token_hash) [unique, note: 'Detect replay attacks']
(last_activity_at) [note: 'Auto-cleanup inactive sessions']
}
}
```
## Légende
**Durées de vie** :
- Access token : **15 minutes**
- Refresh token : **30 jours** (rotatif)
- Inactivité : Déconnexion automatique après **30 jours**
**Sécurité** :
- Tokens stockés en **SHA256** (jamais en clair)
- Rotation automatique des refresh tokens
- Détection replay attack
**Multi-device** :
- Sessions simultanées **illimitées**
- Révocation individuelle ou globale possible
- Alertes si connexion depuis nouveau device ou pays différent

View File

@@ -1,65 +0,0 @@
# User Profile History
📖 Audit trail modifications profil (Article 16 RGPD - Droit de rectification)
## Diagramme
```kroki-dbml
Table users {
id uuid [primary key]
email varchar(255)
username varchar(50)
bio text
}
Table user_profile_history {
id uuid [primary key]
user_id uuid [not null, ref: > users.id]
field_name profile_field_enum [not null, note: 'Champ modifié (email, username, bio, etc.)']
old_value text [note: 'Valeur avant modification (NULL si création)']
new_value text [not null, note: 'Nouvelle valeur']
change_reason change_reason_enum [not null]
ip_address inet [not null, note: 'IP de l origine du changement']
changed_at timestamp [not null, default: `now()`]
indexes {
(user_id, changed_at) [note: 'Timeline modifications user (ordre chronologique)']
(field_name, changed_at) [note: 'Track modifications par type de champ']
(user_id, field_name) [note: 'Historique d un champ spécifique']
}
}
Enum profile_field_enum {
email [note: 'Re-vérification requise après changement']
username [note: 'Limite: 1 changement/30j']
bio [note: 'Biographie utilisateur']
avatar_url [note: 'URL de l avatar']
date_of_birth [note: 'Date de naissance']
}
Enum change_reason_enum {
user_edit [note: 'Modification self-service utilisateur']
admin_correction [note: 'Correction par admin (via backoffice)']
gdpr_request [note: 'Suite demande RGPD formelle (droit de rectification)']
}
```
## Légende
**Champs trackés** :
- `email`: Re-vérification requise
- `username`: Limite 1 changement/30j
- `bio`, `avatar_url`, `date_of_birth`
**Change reasons** :
- `user_edit`: Modification self-service utilisateur
- `admin_correction`: Correction par admin
- `gdpr_request`: Suite demande RGPD formelle
**Audit** :
- Historique complet conservé (preuve légale)
- Accessible utilisateur : "Historique de mes modifications"
- Accessible DPO : investigations

View File

@@ -1,320 +0,0 @@
# 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"
USERS ||--o{ SESSIONS : "possède"
USERS ||--o{ DEVICES : "possède"
USERS ||--o{ USER_CONSENTS : "donne"
USERS ||--o{ LOCATION_HISTORY : "génère"
USERS ||--o{ INTEREST_GAUGES : "possède"
USERS ||--o{ REPORTS : "signale"
USERS ||--o{ DATA_EXPORTS : "demande"
USERS ||--o{ PARENTAL_CONSENTS : "a"
USERS ||--o{ ACCOUNT_DELETIONS : "demande"
USERS ||--o{ USER_PROFILE_HISTORY : "modifie"
PARENTAL_CONSENTS ||--|| PARENTAL_CONTROLS : "configure"
PRIVACY_POLICY_VERSIONS ||--o{ USER_POLICY_ACCEPTANCES : "acceptée par"
USERS ||--o{ USER_POLICY_ACCEPTANCES : "accepte"
BREACH_INCIDENTS ||--o{ BREACH_AFFECTED_USERS : "impacte"
USERS ||--o{ BREACH_AFFECTED_USERS : "est impacté"
CONTENTS ||--o{ LISTENING_HISTORY : "écouté"
CONTENTS }o--|| USERS : "créé par"
CONTENTS ||--o{ REPORTS : "reçoit"
DEVICES ||--o{ SESSIONS : "a"
USERS {
uuid id PK
string email UK
string pseudo UK
date birthdate
string role
string account_status
timestamp created_at
timestamp last_login_at
boolean email_verified
boolean kyc_verified
string phone_number
int trust_score
timestamp deletion_requested_at
timestamp inactivity_notified_at
}
CONTENTS {
uuid id PK
uuid creator_id FK
string title
string audio_url
string status
string moderation_status
string age_rating
string geo_type
point geo_location
string[] tags
int duration_seconds
timestamp published_at
int reports_count
text moderation_notes
}
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
}
SESSIONS {
uuid id PK
uuid user_id FK
uuid device_id FK
string access_token_hash
string refresh_token_hash
timestamp access_token_expires_at
timestamp refresh_token_expires_at
inet ip_address
string user_agent
string city
string country_code
timestamp created_at
timestamp last_activity_at
timestamp revoked_at
}
DEVICES {
uuid id PK
uuid user_id FK
string device_name
string os
string browser
string device_type
boolean is_trusted
timestamp trusted_until
timestamp first_seen_at
timestamp last_seen_at
}
USER_CONSENTS {
uuid id PK
uuid user_id FK
string consent_type
string consent_version
boolean accepted
timestamp given_at
}
LOCATION_HISTORY {
uuid id PK
uuid user_id FK
geography location
string geohash
boolean anonymized
string context
timestamp created_at
timestamp anonymized_at
}
INTEREST_GAUGES {
uuid id PK
uuid user_id FK
string category
decimal score
timestamp last_updated
int interactions_count
}
REPORTS {
uuid id PK
uuid content_id FK
uuid reporter_id FK
uuid moderator_id FK
string category
string status
text comment
timestamp reported_at
timestamp reviewed_at
text moderator_notes
string action_taken
}
DATA_EXPORTS {
uuid id PK
uuid user_id FK
string status
string export_url
bigint size_bytes
string format
timestamp requested_at
timestamp generated_at
timestamp expires_at
}
PARENTAL_CONSENTS {
uuid id PK
uuid user_id FK
string parent_email
boolean validated
timestamp validated_at
timestamp revoked_at
}
PARENTAL_CONTROLS {
uuid id PK
uuid parental_consent_id FK
boolean gps_enabled
boolean messaging_enabled
boolean content_16plus_enabled
}
PRIVACY_POLICY_VERSIONS {
uuid id PK
string version
boolean major_change
timestamp effective_date
}
USER_POLICY_ACCEPTANCES {
uuid id PK
uuid user_id FK
uuid policy_version_id FK
boolean accepted
timestamp accepted_at
}
ACCOUNT_DELETIONS {
uuid id PK
uuid user_id FK
string status
timestamp requested_at
timestamp effective_at
timestamp deleted_at
}
BREACH_INCIDENTS {
uuid id PK
string severity
int estimated_users_count
timestamp detected_at
timestamp cnil_notified_at
boolean user_notification_required
}
BREACH_AFFECTED_USERS {
uuid id PK
uuid breach_id FK
uuid user_id FK
timestamp notified_at
}
USER_PROFILE_HISTORY {
uuid id PK
uuid user_id FK
string field_name
text old_value
text new_value
timestamp changed_at
}
DATA_RETENTION_LOGS {
uuid id PK
string action_type
int users_processed
int users_deleted
timestamp executed_at
}
```
## Légende
**Entités de base** :
- **USERS** : Utilisateurs plateforme
- Rôles : `listener`, `creator`, `moderator`, `admin`
- Account status : `active`, `suspended`, `grace_period`, `deleted`
- Trust score : 0-100 (anti-spam, accès fonctionnalités avancées)
- **CONTENTS** : Contenus audio
- Status : `draft`, `pending_review`, `published`, `moderated`, `deleted`
- Moderation status : `pending_review`, `approved`, `rejected`
- 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
**Entités authentification & sécurité** :
- **SESSIONS** : Sessions utilisateur OAuth2/OIDC ([détails](sessions.md))
- Access token : 15 min, Refresh token : 30 jours (rotatif)
- **DEVICES** : Appareils de confiance ([détails](devices.md))
- Types : `mobile`, `tablet`, `desktop`, `car`
- Appareils de confiance : bypass 2FA pendant 30 jours
**Entités RGPD & conformité** :
- **USER_CONSENTS** : Consentements RGPD avec versioning ([détails](consents.md))
- Types : `geolocation_precise`, `analytics`, `push_notifications`
- **LOCATION_HISTORY** : Historique GPS avec anonymisation 24h ([détails](location-history.md))
- Geohash précision 5 (~5km²) après 24h
- **DATA_EXPORTS** : Exports de données utilisateur ([détails](exports.md))
- Portabilité RGPD Article 20, délai 48h max
- **PARENTAL_CONSENTS** : Consentements parentaux 13-15 ans ([détails](parental-consents.md))
- Workflow validation email parent, token expire 7j
- **PARENTAL_CONTROLS** : Paramètres contrôle parental ([détails](parental-consents.md))
- GPS, messagerie, contenus +16 configurables par parent
- **PRIVACY_POLICY_VERSIONS** : Versioning politique confidentialité ([détails](privacy-policy-versions.md))
- Popup si changement majeur, historique acceptations
- **ACCOUNT_DELETIONS** : Suppressions avec grace period 30j ([détails](account-deletions.md))
- Annulation possible, suppression effective automatique
- **BREACH_INCIDENTS** : Registre violations de données ([détails](breach-incidents.md))
- Procédure 72h CNIL, notification utilisateurs si risque élevé
- **USER_PROFILE_HISTORY** : Audit trail modifications profil ([détails](user-profile-history.md))
- Droit rectification Article 16, preuve légale
- **DATA_RETENTION_LOGS** : Logs purges automatiques ([détails](data-retention-logs.md))
- Inactivité 5 ans, notifications 90j/30j/7j
**Entités recommandation & modération** :
- **INTEREST_GAUGES** : Jauges de centres d'intérêt ([détails](interest-gauges.md))
- Score 0-100 par catégorie (automobile, travel, music, etc.)
- Algorithme : 70% géo + 30% intérêts
- **REPORTS** : Signalements de contenu ([détails](reports.md))
- Catégories : `spam`, `hate_speech`, `violence`, `sexual_content`, `misinformation`, etc.
- Workflow modération avec priorisation

View File

@@ -1,200 +0,0 @@
# 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

View File

@@ -1,199 +0,0 @@
# 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 |

View File

@@ -1,171 +0,0 @@
# 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é

View File

@@ -1,191 +0,0 @@
# 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

View File

@@ -1,187 +0,0 @@
# 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

View File

@@ -1,250 +0,0 @@
# 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

View File

@@ -1,67 +0,0 @@
# 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

View File

@@ -1,90 +0,0 @@
# 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

View File

@@ -1,70 +0,0 @@
# 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

View File

@@ -1,110 +0,0 @@
# language: fr
@privacy @rgpd @security
Fonctionnalité: Sécurité des données (Article 32 RGPD)
En tant que responsable de traitement
Je veux garantir la sécurité des données personnelles
Afin de prévenir les violations et fuites
# Chiffrement transport
@encryption @tls
Scénario: TLS 1.3 obligatoire pour toutes les communications
Quand un client tente de se connecter à l'API
Alors la connexion utilise TLS 1.3 uniquement
Et les protocoles TLS 1.0, 1.1, 1.2 sont refusés
Et le certificat est valide et à jour
# Chiffrement stockage
@encryption @database
Scénario: Encryption at rest pour la base de données
Étant donné que PostgreSQL stocke des données utilisateurs
Alors le chiffrement AES-256 at rest est activé
Et les backups sont également chiffrés AES-256
Et les backups sont stockés offsite (règle 3-2-1)
# Tokens JWT sécurisés
@encryption @jwt
Scénario: Rotation des clés JWT tous les 90 jours
Étant donné que l'API utilise des tokens JWT RS256
Quand 90 jours se sont écoulés depuis la dernière rotation
Alors une nouvelle paire de clés RSA est générée
Et l'ancienne clé reste valide 7 jours (overlap)
Et tous les tokens sont progressivement re-signés
# CDN avec signed URLs
@cdn @signed-urls
Scénario: URLs signées expirables pour les fichiers audio
Quand un utilisateur demande à écouter un contenu
Alors l'API génère une signed URL valide 1 heure
Et l'URL contient un token HMAC
Et après expiration, l'URL retourne 403 Forbidden
# Détection breach
@breach @monitoring
Scénario: Alerte immédiate si erreurs critiques backend
Quand une erreur critique survient dans l'API (500, crash)
Alors Sentry déclenche une alerte Discord/Slack immédiate
Et l'équipe est notifiée en temps réel
Et les logs sont consultables dans Grafana
@breach @monitoring
Scénario: Alerte si pic de requêtes anormal (potentiel DDoS)
Étant donné que le trafic habituel est ~1000 req/min
Quand le trafic atteint 10000 req/min
Alors Grafana déclenche une alerte email
Et l'équipe vérifie s'il s'agit d'une attaque
@breach @monitoring
Scénario: Alerte si accès DB non autorisé
Quand une connexion PostgreSQL provient d'une IP non whitelistée
Alors PostgreSQL bloque la connexion
Et un SMS est envoyé au fondateur
Et l'IP est loggée pour investigation
# Procédure breach 72h CNIL
@breach @procedure
Scénario: Notification CNIL si violation de données sous 72h
Étant donné qu'une violation de données est détectée
Et que des données GPS utilisateurs ont fuité
Quand l'équipe évalue la gravité
Alors le runbook "docs/rgpd/procedure-breach.md" est suivi:
| Étape | Délai | Action |
| 1 | H+0 | Détection et confinement |
| 2 | H+24 | Évaluation gravité et impact |
| 3 | H+48 | Notification CNIL si risque |
| 4 | H+72 | Notification utilisateurs si élevé|
Et un email pré-rédigé est envoyé à la CNIL
Et le formulaire en ligne CNIL est rempli
# Mesures organisationnelles
@security @access-control
Scénario: Accès DB uniquement via VPN et whitelist IP
Étant donné que je suis un développeur
Quand je tente d'accéder à la DB de production
Alors je dois être connecté au VPN OVH
Et mon IP doit être dans la whitelist
Sinon la connexion est refusée
@security @2fa
Scénario: 2FA obligatoire pour les comptes administrateurs
Étant donné que je suis un administrateur
Quand je me connecte à Zitadel admin
Alors le 2FA (TOTP) est obligatoire
Et je ne peux pas désactiver le 2FA
Et chaque connexion est loggée avec IP et timestamp
@security @logs
Scénario: Anonymisation des IP dans les logs (rotation 90j)
Quand un utilisateur fait une requête API
Alors son IP est loggée pour debug
Mais les 2 derniers octets sont masqués (ex: 192.168.x.x)
Et les logs sont automatiquement supprimés après 90 jours
# Tests de sécurité
@security @pentest
Scénario: Pentest annuel obligatoire
Étant donné que l'application est en production
Quand une année s'est écoulée depuis le dernier pentest
Alors un pentest externe est planifié
Et les vulnérabilités découvertes sont corrigées sous 30 jours
Et un rapport est produit pour audit

View File

@@ -1,112 +0,0 @@
# language: fr
@privacy @rgpd @minors @parental-consent
Fonctionnalité: Protection des mineurs et consentement parental (Article 8 RGPD)
En tant que plateforme responsable
Je veux protéger les mineurs avec consentement parental
Afin de respecter l'Article 8 RGPD (13-15 ans)
Contexte:
Étant donné que je suis sur la page d'inscription
Scénario: Blocage inscription si moins de 13 ans
Quand je saisis ma date de naissance "15/03/2014"
Et que je valide le formulaire
Alors je vois le message "RoadWave est réservé aux personnes de 13 ans et plus"
Et je vois un lien vers "RoadWave Kids"
Et mon inscription est bloquée
Scénario: Inscription directe si 16 ans ou plus
Quand je saisis ma date de naissance "10/01/2008"
Et que je valide le formulaire
Alors mon compte est créé immédiatement
Et aucun consentement parental n'est requis
Et j'ai accès à toutes les fonctionnalités
Plan du Scénario: Workflow consentement parental pour 13-15 ans
Quand je saisis ma date de naissance "<date_naissance>"
Et que je saisis l'email de mon parent "parent@example.com"
Et que je valide le formulaire
Alors un email est envoyé à "parent@example.com"
Et le lien de validation expire dans 7 jours
Et mon compte est créé mais inactif
Et je vois "En attente validation parentale"
Exemples:
| date_naissance | âge |
| 15/03/2011 | 13 |
| 20/06/2010 | 14 |
| 01/12/2009 | 15 |
Scénario: Validation du consentement parental
Étant donné que je suis un mineur de 14 ans avec compte inactif
Et que mon parent a reçu l'email de validation
Quand mon parent clique sur le lien de validation
Alors il voit une page avec:
| Section |
| Résumé données collectées |
| Paramètres contrôle parental|
| Checkbox consentement |
Et quand il coche "J'autorise mon enfant" et valide
Alors mon compte est activé
Et je reçois un email "Compte activé par ton parent"
Et les restrictions 13-15 ans sont appliquées
Scénario: Restrictions pour comptes 13-15 ans
Étant donné que je suis un utilisateur de 14 ans avec compte validé
Alors je peux écouter des contenus autorisés
Mais je NE peux PAS:
| Restriction |
| Activer le GPS précis sans accord parent |
| Utiliser la messagerie privée |
| Voir les contenus marqués +16 |
| Afficher ma ville précise sur le profil |
Scénario: Dashboard parent - Visualisation activité enfant
Étant donné que je suis un parent avec enfant de 14 ans
Quand je me connecte à "roadwave.fr/parent/[child_id]"
Alors je vois:
| Information |
| Historique d'écoute |
| Temps d'écoute hebdomadaire |
| Dernière connexion |
Et je peux:
| Action |
| Activer/désactiver GPS précis |
| Activer/désactiver messagerie |
| Révoquer le consentement |
Scénario: Révocation du consentement parental
Étant donné que je suis un parent
Et que mon enfant de 14 ans a un compte actif
Quand je clique sur "Révoquer le consentement"
Alors le compte de mon enfant est immédiatement désactivé
Et il ne peut plus se connecter
Et je reçois un email de confirmation
@roadwave-kids
Scénario: RoadWave Kids - Inscription via compte parent
Étant donné que je suis un parent avec compte RoadWave actif
Quand je vais dans "Mon compte > Ajouter un profil enfant"
Et que je saisis:
| Champ | Valeur |
| Pseudo enfant | "Emma" |
| Date naissance | "12/08/2016" |
Alors un profil Kids est créé
Et je reçois un QR code pour l'app RoadWave Kids
Et l'enfant ne peut écouter que les contenus whitelist
@roadwave-kids
Scénario: RoadWave Kids - Restrictions strictes
Étant donné que je suis connecté sur l'app RoadWave Kids
Alors je peux uniquement:
| Fonctionnalité |
| Écouter contenus présélectionnés |
| Voir ma position ville (GeoIP) |
Mais je NE peux PAS:
| Restriction |
| Activer le GPS précis |
| Créer du contenu |
| Avoir un profil public |
| Utiliser la messagerie |
| Voir du contenu UGC |

View File

@@ -1,99 +0,0 @@
# language: fr
@privacy @rgpd @transparency
Fonctionnalité: Politique de confidentialité et transparence (Articles 13-14 RGPD)
En tant qu'utilisateur
Je veux comprendre comment mes données sont utilisées
Afin de donner un consentement éclairé
Contexte:
Étant donné que l'application RoadWave est disponible
# Popup première connexion
@first-launch
Scénario: Affichage de la politique de confidentialité à l'inscription
Quand je m'inscris pour la première fois
Alors une popup s'affiche avec la politique de confidentialité
Et je dois scroller jusqu'en bas pour activer le bouton "J'accepte"
Et je dois cocher "J'ai lu et j'accepte la politique de confidentialité"
Et je ne peux pas créer de compte sans accepter
# Contenu obligatoire
@content
Scénario: Vérification du contenu de la politique de confidentialité
Quand je consulte la page "roadwave.fr/confidentialite"
Alors je vois les informations suivantes:
| Section |
| Identité responsable traitement + DPO |
| Finalités détaillées par traitement |
| Base légale (consentement/contrat/intérêt)|
| Destinataires données (CDN, Matomo, etc.) |
| Durées de conservation |
| Droits utilisateurs (accès, rectif, etc.) |
| Droit réclamation CNIL |
| Transferts hors UE (aucun) |
# Versioning
@versioning
Scénario: Versioning de la politique avec Git et DB
Étant donné que la politique de confidentialité est modifiée
Quand l'équipe commit les changements
Alors le fichier "docs/legal/politique-confidentialite.md" est versionné Git
Et une entrée est créée dans "privacy_policy_versions"
Avec les champs:
| Champ | Valeur |
| version | "2.0" |
| effective_date| "2026-03-01" |
| major_change | true |
| changelog | "Ajout tracking..." |
@versioning
Scénario: Notification utilisateurs si changement majeur
Étant donné qu'une nouvelle version majeure de la politique est publiée
Quand un utilisateur se connecte
Alors une popup s'affiche "Politique de confidentialité mise à jour"
Et il doit l'accepter à nouveau pour continuer
Et s'il refuse, son compte est gelé (accès lecture seule)
# Transparence algorithme
@algorithm-transparency
Scénario: Explication simplifiée de l'algorithme de recommandation
Quand je vais sur "roadwave.fr/comment-ca-marche"
Alors je vois une page "Comment fonctionne la recommandation ?"
Avec les explications suivantes:
| Critère | Explication |
| Distance géographique | Contenus près de vous en priorité |
| Centres d'intérêt | Jauges automatiques selon écoutes |
| Popularité | Contenus les plus écoutés |
Et je peux désactiver la personnalisation (mode anonyme)
# Contact DPO
@dpo-contact
Scénario: Accès facile au contact DPO
Quand je vais dans "Paramètres > Confidentialité et données"
Alors je vois un bouton "Contacter le DPO"
Et le lien email "dpo@roadwave.fr" est cliquable
Et je vois "Délai de réponse : 1 mois maximum"
# Profilage et décisions automatisées
@profiling
Scénario: Information sur le profilage et opposition possible
Quand je consulte la politique de confidentialité
Alors je vois une section "Profilage et décisions automatisées"
Qui explique:
| Type décision | Impact | Opposition possible |
| Recommandations | Faible | Oui (mode anonyme) |
| Modération automatique| Élevé | Oui (contestation) |
Et je peux activer le mode anonyme à tout moment
# Sous-traitants
@subprocessors
Scénario: Liste transparente des sous-traitants
Quand je consulte "roadwave.fr/confidentialite#sous-traitants"
Alors je vois la liste complète:
| Service | Finalité | Localisation | DPA |
| OVH | Hébergement | France | |
| Bunny.net | CDN audio | UE | |
| Brevo | Emails | France | |
| Mangopay | Paiements | Luxembourg | |
Et je vois "Aucun transfert hors UE"

View File

@@ -1,77 +0,0 @@
# language: fr
@privacy @rgpd @user-rights
Fonctionnalité: Exercice des droits utilisateurs RGPD
En tant qu'utilisateur
Je veux exercer mes droits RGPD facilement
Afin de contrôler mes données personnelles
Contexte:
Étant donné que je suis connecté à mon compte
# Article 16 - Droit de rectification
@rectification
Scénario: Modification du pseudo (limite 1/30j)
Quand je vais dans "Paramètres > Mon profil"
Et que je modifie mon pseudo de "Alice123" à "AliceM"
Alors le changement est immédiat
Et je ne peux plus modifier mon pseudo pendant 30 jours
Et l'historique est enregistré dans "user_profile_history"
@rectification
Scénario: Modification de l'email avec re-vérification
Quand je modifie mon email de "old@example.com" à "new@example.com"
Alors je reçois un email de vérification sur "new@example.com"
Et je dois cliquer sur le lien dans les 24h
Et mon ancien email reste actif jusqu'à validation
# Article 21 - Droit d'opposition
@opposition
Scénario: Opposition au marketing par email
Quand je vais dans "Paramètres > Notifications"
Et que je décoche "Recevoir les emails marketing"
Alors je ne reçois plus d'emails promotionnels
Mais je reçois toujours les emails transactionnels
@opposition @anonymous-mode
Scénario: Activation du mode anonyme (recommandations génériques)
Quand je vais dans "Paramètres > Confidentialité"
Et que j'active "Mode anonyme"
Alors mes jauges d'intérêt sont ignorées
Et je reçois uniquement les top contenus de ma zone géographique
Et mon historique d'écoute n'est pas utilisé pour les recommandations
Et je vois un badge "Mode anonyme actif"
# Article 18 - Droit à la limitation du traitement
@limitation
Scénario: Gel temporaire du compte
Quand je vais dans "Paramètres > Gestion du compte"
Et que je clique sur "Mettre en pause mon compte"
Alors mon compte est gelé immédiatement
Et mes contenus sont cachés (non diffusés)
Et mon profil est invisible
Mais je peux toujours me connecter en lecture seule
Et je peux réactiver à tout moment
@limitation
Scénario: Réactivation compte gelé
Étant donné que mon compte est gelé depuis 15 jours
Quand je me connecte
Et que je clique sur "Réactiver mon compte"
Alors mon compte redevient actif immédiatement
Et mes contenus sont à nouveau visibles
# Article 12 - Délai de réponse 1 mois max
@response-time
Scénario: Demande d'accès aux données (délai 48h)
Quand je demande un export de mes données
Alors je reçois un email sous 48h maximum
Avec un lien de téléchargement valide 7 jours
@response-time
Scénario: Contestation d'une décision automatisée (délai 24h)
Étant donné que mon contenu a été bloqué par la modération automatique
Quand je clique sur "Contester cette décision"
Alors un humain examine mon cas sous 24h
Et je reçois une réponse motivée
Et mon contenu est rétabli si la décision était erronée

View File

@@ -1,619 +0,0 @@
## 13. Conformité RGPD
### 13.1 Gestion du consentement
**Décision** : Tarteaucitron.js + PostgreSQL backend
**Implémentation web** :
- ✅ Tarteaucitron.js (opensource, self-hosted)
- ✅ Banner RGPD français, customisable
- ✅ Granularité : fonctionnel / analytique / marketing
**Implémentation backend** :
- Table `user_consents` avec versioning
- Champs : user_id, consent_type, version, accepted, timestamp
- Historique complet conservé (preuve légale)
**Consentements requis** :
- **Géolocalisation précise** : obligatoire (banner + permission OS)
- **Analytics** : optionnel (Matomo)
- **Notifications push** : optionnel (permission OS)
**Justification** :
- Opensource, 0€, conformité RGPD garantie
- Historique backend = preuve légale en cas de contrôle
- Granularité conforme recommandations CNIL
---
### 13.2 Anonymisation des données GPS
**Décision** : Geohash après 24h
**Processus** :
1. Données précises conservées **24h** (recommandation personnalisée)
2. Après 24h : conversion en geohash précision 5 (~5km²)
3. Coordonnées originales supprimées définitivement
4. Job quotidien automatique via cron
**Exceptions** :
- ✅ Historique personnel visible (liste trajets) : conservation intégrale tant que compte actif
- ❌ Analytics globales : uniquement geohash anonyme
**Justification** :
- Vraie anonymisation RGPD (CNIL compliant)
- Permet analytics agrégées (heatmaps trafic)
- PostGIS natif, 0€
---
### 13.3 Export des données (portabilité)
**Décision** : JSON + HTML + ZIP, génération asynchrone
**Format export** : Archive ZIP contenant JSON (machine-readable), HTML (human-readable), fichiers audio, README
**Données exportées** :
- Profil utilisateur (email, pseudo, date inscription, bio)
- Historique d'écoute (titres, dates, durées)
- Contenus créés (audio + métadonnées)
- Abonnements et likes
- Centres d'intérêt (jauges)
- Historique consentements
**Processus** :
1. Demande via paramètres compte
2. Génération asynchrone (worker background)
3. Email avec lien download (expire **7 jours**)
4. Délai : **48h maximum** (conformité RGPD)
**Limite** :
- Maximum **1 export/mois** (anti-abus)
**Justification** :
- Conformité article 20 RGPD (portabilité)
- Double format (human + machine)
- Worker asynchrone évite timeout
---
### 13.4 Suppression du compte
**Décision** : Grace period 30j + anonymisation contenus
**Processus** :
1. Utilisateur clique "Supprimer mon compte"
2. Compte désactivé immédiatement (login impossible)
3. Contenus cachés pendant 30 jours (non diffusés)
4. Email confirmation + lien annulation (valide 30j)
5. Après 30j sans annulation : suppression effective
**Suppression effective** :
- ✅ Compte utilisateur supprimé (données personnelles)
- ✅ Historique d'écoute supprimé
- ✅ GPS historique supprimé
- ✅ Sessions et tokens révoqués
- ⚠️ Contenus créés **anonymisés** (créateur = "Utilisateur supprimé")
- ⚠️ Likes et abonnements supprimés (mais compteurs préservés)
**Contenus conservés anonymement** :
- Audio files (CDN)
- Métadonnées (titre, description, tags, géolocalisation)
- Statistiques d'écoute
**Justification** :
- Grace period évite suppressions impulsives
- Anonymisation contenus = intérêt légitime communauté
- Conforme RGPD si créateur = donnée supprimée
---
### 13.5 Mode dégradé (sans GPS précis)
**Décision** : GeoIP par défaut, GPS optionnel
**Niveaux de précision** :
| Niveau | Technologie | Contenus accessibles | Consentement |
|--------|-------------|---------------------|--------------|
| **Pays** | Aucune géoloc | Contenus nationaux uniquement | ❌ Non requis |
| **Ville** | GeoIP (IP2Location) | Contenus régionaux/ville | ❌ Non requis |
| **Précis** | GPS | Tous contenus (hyperlocaux inclus) | ✅ Requis |
**Implémentation** :
- Démarrage app : GeoIP automatique (IP → ville)
- Banner in-app : "Activez la géolocalisation pour découvrir du contenu près de chez vous"
- Upgrade volontaire vers GPS
**API GeoIP** :
- IP2Location Lite (gratuit, self-hosted, voir [ADR-019](../../../adr/019-geolocalisation-ip.md))
- Update DB mensuelle automatique
- Précision ~80% au niveau ville
**Justification** :
- RGPD : pas de consentement requis pour GeoIP (pas de donnée personnelle)
- UX dégradée acceptable (contenus disponibles)
- Progressive disclosure (upgrade optionnel)
---
### 13.6 Durée de conservation des données
**Décision** : 5 ans inactivité → purge automatique
**Règles** :
| Type de compte | Seuil inactivité | Action |
|----------------|------------------|--------|
| **Auditeur uniquement** | 5 ans sans connexion | Suppression automatique |
| **Créateur avec contenus actifs** | Jamais (tant qu'écoutes) | Conservation indéfinie |
| **Créateur inactif** | 5 ans sans connexion + 2 ans sans écoute | Suppression automatique |
**Notifications avant suppression** :
- Email + push : **90 jours** avant
- Email + push : **30 jours** avant
- Email + push : **7 jours** avant
- Toute connexion = reset compteur inactivité
**Contenu conservé** :
- Contenus créés par comptes supprimés (anonymisés) : conservation indéfinie
**Justification** :
- Conformité principe minimisation RGPD
- 5 ans = équilibre raisonnable (standard industrie)
- Exception créateurs actifs = intérêt légitime plateforme
---
### 13.7 Cookies et trackers web
**Décision** : Matomo self-hosted, zéro cookie tiers
**Cookies utilisés** :
| Cookie | Type | Durée | Finalité | Consentement |
|--------|------|-------|----------|--------------|
| `session` | Technique | 30j | Authentification | ❌ Non requis |
| `refresh_token` | Technique | 30j | Session persistante | ❌ Non requis |
| `_pk_id` | Analytique | 13 mois | Matomo (IP anonyme) | ✅ Requis |
**Analytics : Matomo self-hosted** :
- Hébergé sur nos serveurs (Docker)
- IP anonymisées automatiquement (2 derniers octets)
- Pas de cookie si consentement refusé
- Alternative : Plausible (SaaS EU, 9€/mois)
**Trackers interdits** :
- ❌ Google Analytics
- ❌ Facebook Pixel
- ❌ Hotjar, Mixpanel, etc.
**Justification** :
- Souveraineté données (pas de transfert US)
- Conformité RGPD max (CNIL compatible)
- Matomo = opensource, 0€ infra
---
### 13.8 Registre des traitements
**Décision** : Document Markdown versionné Git (MVP)
**Emplacement** :
- `docs/rgpd/registre-traitements.md`
- Versionné Git (historique modifications)
**Contenu obligatoire par traitement** :
- Nom et finalité du traitement
- Catégories de données collectées
- Base légale (consentement / contrat / intérêt légitime)
- Durée de conservation
- Destinataires (sous-traitants, CDN, etc.)
- Transferts hors UE (aucun prévu)
**Responsable** :
- DPO / Fondateur
- Review trimestrielle obligatoire
- Update immédiate si nouveau traitement
**Migration future** :
- Si > 100K utilisateurs : interface admin PostgreSQL
**Justification** :
- Obligation RGPD Article 30
- Markdown = simple, versionné, auditable
- 0€
---
### 13.9 Notification violations de données (breach)
**Décision** : Monitoring + alertes + runbook
**Détection automatique** :
| Événement | Outil | Alerte |
|-----------|-------|--------|
| Erreurs backend critiques | Sentry | Discord/Slack immédiat |
| Pic requêtes anormal | Grafana | Email équipe |
| Accès non autorisé DB | PostgreSQL logs | SMS fondateur |
| Authentification suspecte | Zitadel alerts | Email équipe |
**Procédure breach** :
- Runbook : `docs/rgpd/procedure-breach.md`
- Checklist 72h CNIL :
1. H+0 : Détection et confinement
2. H+24 : Évaluation gravité (données concernées, utilisateurs impactés)
3. H+48 : Notification CNIL si risque pour utilisateurs
4. H+72 : Notification utilisateurs si risque élevé
**Contact CNIL** :
- Email pré-rédigé (template)
- Formulaire en ligne (account CNIL créé)
**Justification** :
- Obligation RGPD Article 33 (notification 72h)
- Monitoring proactif évite découverte tardive
- Sentry gratuit < 5K events/mois
---
### 13.10 DPO (Délégué à la Protection des Données)
**Décision** : Fondateur = DPO temporaire (MVP)
**Raison légale** :
- Non obligatoire si :
- < 250 employés
- Pas de traitement à grande échelle de données sensibles
- RoadWave : données localisation = sensible MAIS échelle MVP
**Formation** :
- CNIL : formation gratuite en ligne (4h)
- Certification CNIL "Atelier RGPD" (gratuit)
**Contact** :
- Email : dpo@roadwave.fr
- Publié dans CGU et mentions légales
- Délai réponse : **1 mois** (RGPD)
**Migration future** :
- Si > 100K utilisateurs : DPO externe mutualisé (~200€/mois)
- Ou recrutement DPO interne si > 10 employés
**Justification** :
- Conforme RGPD (non obligatoire en phase MVP)
- 0€, contrôle total
- Bonne pratique : avoir un contact identifié
---
### 13.11 Droit de rectification
**Décision** : Interface self-service + validation immédiate
**Données rectifiables** :
- Email (avec re-vérification)
- Pseudo (unique, disponibilité vérifiée)
- Bio / description
- Centres d'intérêt (jauges)
- Photo de profil
**Processus** :
- Changements immédiats (sauf email)
- Email : lien vérification → validation sous 24h
- Historique modifications conservé (audit trail)
**Limitations** :
- Pseudo : max 1 changement/30j (anti-squat)
**Justification** : Conformité Article 16 RGPD, self-service 0€
---
### 13.12 Droit d'opposition
**Décision** : Opt-out granulaire, effet immédiat
| Traitement | Toggle | Effet |
|------------|--------|-------|
| **Marketing email** | Paramètres | Stop emails promo |
| **Notifications push** | Paramètres | Stop push marketing |
| **Analytics** | Banner RGPD | Exclusion Matomo |
| **Recommandations personnalisées** | "Mode anonyme" | Reco génériques uniquement |
**Mode anonyme** :
- Désactive algorithme (jauges ignorées)
- Recommandations = top contenus zone géo uniquement
- Historique non utilisé
**Justification** : Conformité Article 21 RGPD
---
### 13.13 Droit à la limitation du traitement
**Décision** : "Geler mon compte" temporaire
**Effets** :
- Compte gelé, contenus cachés, profil invisible
- Connexion lecture seule OK
- Réactivation à tout moment
**Justification** : Conformité Article 18 RGPD
---
### 13.14 Politique de confidentialité
**Décision** : Page web + popup in-app + versioning Git
**Emplacement** :
- Web : `roadwave.fr/confidentialite`
- App : page dédiée paramètres
- Popup première connexion (scroll requis)
**Contenu** : Identité responsable, finalités, base légale, destinataires, durées, droits, transferts UE
**Versioning** : Git + DB `privacy_policy_versions`, popup si changement majeur
**Justification** : Conformité Articles 13-14 RGPD
---
### 13.15 Minimisation des données
**Décision** : Collecte strictement nécessaire
| Donnée | Finalité | Optionnel |
|--------|----------|-----------|
| Email | Authentification | ❌ |
| Pseudo | Identité publique | ❌ |
| GPS précis | Reco hyperlocales | ✅ (GeoIP fallback) |
| Jauges intérêt | Reco thématiques | ✅ |
| Date naissance | Vérifier âge minimum | ❌ (année seule) |
**Non collecté** : nom/prénom réels, adresse postale (sauf créateurs payés), téléphone (sauf 2FA optionnel)
**Justification** : Conformité Article 5.1.c RGPD
---
### 13.16 Sécurité des données
**Décision** : Chiffrement multi-niveaux
| Couche | Implémentation |
|--------|----------------|
| Transport | TLS 1.3 ([ADR-006](../../../adr/006-chiffrement.md)) |
| DB | PostgreSQL encryption at rest AES-256 |
| Tokens | JWT RS256 + rotation 90j |
| CDN | Signed URLs expirables |
| Backups | AES-256 + offsite |
**Mesures orga** : Whitelist IP, Vault secrets, logs anonymisés, 2FA admins, pentest annuel
**Justification** : Conformité Article 32 RGPD
---
### 13.17 Transferts hors UE
**Décision** : Hébergement 100% France/UE
| Service | Localisation | Transfert UE |
|---------|--------------|--------------|
| Hébergement | OVH France | ❌ |
| Database | OVH France | ❌ |
| CDN | Bunny.net EU | ❌ |
| Matomo | Self-hosted France | ❌ |
**Si CDN global futur** : Clauses Contractuelles Types (CCE) 2021
**Justification** : Conformité Articles 44-50 RGPD, souveraineté données
---
### 13.18 Profilage et décisions automatisées
**Décision** : Transparence + droit opposition
| Traitement | Impact | Intervention humaine | Opposition |
|------------|--------|---------------------|------------|
| Recommandations | Faible | ❌ | ✅ (mode anonyme) |
| Modération auto | Élevé | ✅ (review 24h) | ✅ (appeal) |
**Transparence** : Page "Comment fonctionne l'algo ?", explications simplifiées
**Justification** : Conformité Article 22 RGPD
---
### 13.19 Gestion des mineurs
**Décision** : 13 ans minimum + consentement parental 13-15 ans + RoadWave Kids
#### App principale (RoadWave)
**Âge minimum** : **13 ans** (alignement YouTube/TikTok)
**Processus inscription** :
1. Saisie date naissance (JJ/MM/AAAA)
2. **Si < 13 ans** : blocage + message redirection RoadWave Kids
3. **Si 13-15 ans** : workflow consentement parental
4. **Si ≥ 16 ans** : inscription directe
**Workflow consentement parental (13-15 ans)** :
1. Ado saisit email parent
2. Email automatique parent avec lien validation (expire 7j)
3. Parent clique lien → page dédiée avec résumé données collectées, paramètres contrôle parental, checkbox consentement
4. Validation parent → compte ado activé avec restrictions
**Restrictions 13-15 ans** :
- ✅ Écoute contenus autorisés
- ✅ Création contenus (modération renforcée)
- ⚠️ GPS précis : consentement parental explicite requis
- ⚠️ Messagerie privée : désactivée par défaut
- ⚠️ Contenus sensibles : filtrés (pas de contenu +16)
- ⚠️ Profil public limité (pas d'affichage ville précise)
**Contrôles parentaux** :
- Dashboard parent : `roadwave.fr/parent/[child_id]`
- Visualisation historique écoute
- Activation/désactivation GPS précis
- Activation/désactivation messagerie
- Révocation consentement à tout moment
- Notification hebdomadaire activité
**Vérification légère identité parent** :
- Email parent ≠ email ado (vérification domaine)
- Lien expiration 7 jours
- Pas de vérification identité forte (MVP)
#### RoadWave Kids (< 13 ans)
**App dédiée** : Version séparée avec contrôles renforcés
**Caractéristiques** :
- ❌ Pas de GPS précis (GeoIP ville uniquement)
- ❌ Pas de création contenu
- ❌ Pas de profil public
- ❌ Pas de messagerie
- ✅ Contenus présélectionnés (whitelist éditoriale)
- ✅ Mode lecture seule
- ✅ Contrôle parental obligatoire
**Contenus autorisés** :
- Contes audio enfants
- Guides touristiques famille
- Podcasts éducatifs labellisés
- Histoires locales patrimoine
**Workflow inscription** :
1. Création compte parent (RoadWave standard)
2. Ajout profil enfant dans dashboard parent
3. App Kids : login via QR code parent
4. Pas de compte autonome enfant
**Modération** :
- 100% contenus présélectionnés par équipe éditoriale
- Aucun UGC accessible
- Whitelist créateurs vérifiés uniquement
**Justification** :
- Conformité Article 8 RGPD (13-16 ans selon pays)
- 13 ans France = seuil légal avec consentement parental
- App Kids = protection renforcée < 13 ans
- Alignement marché (YouTube 13+, YouTube Kids)
**Roadmap** :
- **MVP** : App principale 16+ uniquement (simplicité)
- **Phase 2** : Workflow 13-15 ans + consentement parental
- **Phase 3** : RoadWave Kids (app séparée)
---
### 13.20 Sous-traitants et DPA
**Décision** : DPA systématique, audit annuel
| Service | Traitement | Localisation | DPA | Certifications |
|---------|------------|--------------|-----|----------------|
| OVH | Hébergement | France | ✅ | ISO 27001, HDS |
| Bunny.net | CDN | UE | ✅ | ISO 27001 |
| Brevo | Emailing | France | ✅ | RGPD certified |
| Mangopay | Paiements | Luxembourg | ✅ | PCI-DSS, ACPR |
**Obligations DPA** : Traitement selon instructions, confidentialité, sécurité, assistance droits, suppression fin contrat
**Gestion** : `docs/rgpd/sous-traitants.md`, review annuelle
**Justification** : Conformité Article 28 RGPD
---
### 13.21 Analyse d'impact (DPIA)
**Décision** : DPIA obligatoire (GPS + profilage grande échelle)
**Raisons** :
- Traitement grande échelle données GPS sensibles
- Profilage automatisé recommandations
- Surveillance zones publiques
**Contenu** : Description traitement, finalités, nécessité, risques (tracking, profilage, fuite), mesures atténuation (anonymisation 24h, consentement, chiffrement, mode dégradé)
**Fichier** : `docs/rgpd/dpia-geolocalisation.md`, review annuelle
**Justification** : Conformité Article 35 RGPD (critères CNIL remplis)
---
### 13.22 Délai de réponse aux demandes
**Décision** : 1 mois max, automatisation maximale
**Canaux** : Email dpo@roadwave.fr, formulaire in-app, courrier postal
| Droit | Délai cible | Automatisation |
|-------|-------------|----------------|
| Accès (export) | 48h | ✅ Worker |
| Rectification | Immédiat | ✅ Self-service |
| Suppression | Immédiat | ✅ Self-service |
| Opposition | Immédiat | ✅ Toggles |
| Limitation | Immédiat | ✅ Gel compte |
| Portabilité | 48h | ✅ Export |
| Contestation décision | 24h | ⚠️ Manuel |
**Vérification identité** : Si email vérifié = aucune vérif supplémentaire
**Justification** : Conformité Article 12 RGPD

View File

@@ -1,56 +0,0 @@
# Anonymisation automatique GPS après 24h
```mermaid
sequenceDiagram
participant User as Utilisateur
participant App as Application
participant DB as Base de données (PostGIS)
participant Cron as Job Cron Quotidien
Note over User,App: Écoute de contenu avec GPS
User->>App: Écoute contenu (GPS activé)
App->>App: Capturer position GPS précise
App->>DB: INSERT position (lat, lon, anonymized: false)
Note over DB: Position précise stockée<br/>Utilisée pour recommandations
Note over DB,Cron: Moins de 24h : position précise conservée
App->>DB: SELECT positions pour recommandations
DB->>App: Positions GPS précises (< 24h)
App->>User: Recommandations hyperlocales
Note over Cron: 24h+ plus tard
Cron->>DB: SELECT positions WHERE created_at < NOW() - 24h AND anonymized = false
DB->>Cron: Liste positions à anonymiser
loop Pour chaque position
Cron->>DB: Convertir (lat, lon) → geohash précision 5 (~5km²)
Cron->>DB: UPDATE position avec geohash
Cron->>DB: Supprimer coordonnées précises
Cron->>DB: SET anonymized = true
end
Cron->>DB: Log anonymisation (nombre positions traitées)
Note over DB: Positions anonymisées utilisées pour analytics
App->>DB: SELECT positions anonymisées (analytics globales)
DB->>App: Positions geohash uniquement
App->>App: Générer heatmap trafic (~5km² précision)
Note over User: Exception : historique personnel
User->>App: Consulter "Mon historique d'écoute"
App->>DB: SELECT historique personnel utilisateur
DB->>App: Positions précises conservées (tant que compte actif)
App->>User: Trajets détaillés
```
**Légende** :
- **< 24h** : GPS précis conservé (recommandations hyperlocales)
- **> 24h** : Conversion automatique en geohash précision 5 (~5km²)
- **Exception** : Historique personnel conservé intact tant que compte actif
- **Analytics** : Uniquement positions anonymisées (geohash)

View File

@@ -1,45 +0,0 @@
# Séquence - Authentification
## Diagramme
```mermaid
sequenceDiagram
participant U as Utilisateur
participant A as App Mobile
participant Z as Zitadel
participant API as Backend API
participant DB as PostgreSQL
U->>A: Saisie email/password
A->>Z: POST /oauth/token (email, password)
Z->>Z: Validation credentials
Z-->>A: access_token (15min) + refresh_token (30j)
A->>API: GET /api/user/profile (Bearer token)
API->>Z: Validation JWT
Z-->>API: Token valide + user_id
API->>DB: SELECT user WHERE id = ?
DB-->>API: Données utilisateur
API->>DB: INSERT session (hash tokens, IP, device)
DB-->>API: Session créée
API-->>A: Profil utilisateur
A->>U: Connexion réussie
```
## Légende
**Acteurs** :
- Zitadel : Gère l'authentification OAuth2/OIDC
- Backend API : Valide les tokens et accède aux données
**Tokens** :
- Access token : 15 min (JWT), utilisé pour chaque requête API
- Refresh token : 30 jours, permet renouvellement access token
**Sécurité** :
- Tokens stockés hashés (SHA256) en DB
- Device fingerprinting (OS, navigateur, IP)
- Notification si nouveau device

View File

@@ -1,44 +0,0 @@
# Consentement parental (13-15 ans)
```mermaid
sequenceDiagram
participant Ado as Adolescent (13-15 ans)
participant App as Application
participant DB as Base de données
participant Email as Service Email
participant Parent as Parent
Ado->>App: Inscription (date naissance 13-15 ans)
App->>Ado: Demande email parent
Ado->>App: Saisit email parent
App->>DB: Créer compte (statut: pending_parental_consent)
App->>Email: Envoyer email validation parent
Email->>Parent: Email avec lien (expire 7j)
App->>Ado: "En attente validation parentale"
Note over Parent: Parent clique lien validation
Parent->>App: Accès page consentement
App->>Parent: Afficher résumé données + contrôles
Parent->>App: Valider consentement + paramètres
App->>DB: Enregistrer consentement parental
App->>DB: Activer compte (statut: active_minor)
App->>DB: Appliquer restrictions 13-15 ans
App->>Email: Notification ado (compte activé)
Email->>Ado: Email confirmation
App->>Email: Notification parent (récapitulatif)
Email->>Parent: Email + lien dashboard parental
Ado->>App: Connexion
App->>Ado: Accès restreint (GPS/messagerie selon config parent)
```
**Légende** :
- Délai expiration lien : 7 jours
- Restrictions 13-15 ans : GPS précis, messagerie, contenus +16 (configurables par parent)
- Dashboard parent : `roadwave.fr/parent/[child_id]`

View File

@@ -1,66 +0,0 @@
# Export de données (portabilité)
```mermaid
sequenceDiagram
participant User as Utilisateur
participant App as Application
participant DB as Base de données
participant Queue as File d'attente
participant Worker as Worker Background
participant CDN as CDN (fichiers audio)
participant Storage as Stockage temporaire
participant Email as Service Email
User->>App: Demande export données
App->>DB: Vérifier dernière demande
alt Dernière demande < 30 jours
DB->>App: Demande refusée
App->>User: "Prochain export disponible dans X jours"
else Demande autorisée
App->>DB: Créer demande export (statut: pending)
App->>Queue: Ajouter job export
App->>User: "Export en cours, email sous 48h"
Queue->>Worker: Job export disponible
Note over Worker: Génération asynchrone
Worker->>DB: Récupérer profil utilisateur
Worker->>DB: Récupérer historique d'écoute
Worker->>DB: Récupérer contenus créés (métadonnées)
Worker->>DB: Récupérer centres d'intérêt
Worker->>DB: Récupérer historique consentements
Worker->>CDN: Télécharger fichiers audio utilisateur
CDN->>Worker: Fichiers audio (.opus)
Worker->>Worker: Générer export.json (machine-readable)
Worker->>Worker: Générer index.html (human-readable)
Worker->>Worker: Générer README.txt
Worker->>Worker: Créer archive ZIP
Worker->>Storage: Stocker ZIP (expire 7j)
Storage->>Worker: URL signée (expire 7j)
Worker->>DB: Mettre à jour demande (statut: completed)
Worker->>DB: Enregistrer URL + date expiration
Worker->>Email: Envoyer email avec lien
Email->>User: Email + lien téléchargement (valide 7j)
User->>Storage: Clic lien téléchargement
Storage->>User: Téléchargement ZIP
Note over Storage: Après 7 jours
Storage->>Storage: Suppression automatique ZIP
end
```
**Légende** :
- Limite : 1 export / 30 jours (anti-abus)
- Délai génération : 48h maximum (conformité RGPD Article 20)
- Expiration lien : 7 jours
- Format : ZIP contenant JSON, HTML, audio, README

View File

@@ -1,44 +0,0 @@
# Séquence - Modération de contenu
## Diagramme
```mermaid
sequenceDiagram
participant C as Créateur
participant API as Backend API
participant DB as PostgreSQL
participant Q as Queue
participant M as Modérateur
participant N as Notification
C->>API: POST /contents (3 premiers contenus)
API->>DB: INSERT content (status=pending_review)
API->>Q: Ajout file modération
API-->>C: Contenu soumis
Q->>M: Notification nouveau contenu
M->>API: GET /moderation/contents/pending
API-->>M: Liste contenus à modérer
M->>M: Écoute + vérification
alt Validation
M->>API: POST /moderation/approve/{id}
API->>DB: UPDATE status=published
API->>N: Email + push créateur
N-->>C: Contenu publié ✓
else Rejet
M->>API: POST /moderation/reject/{id} (motif)
API->>DB: UPDATE status=rejected
API->>DB: INSERT strike (si grave)
API->>N: Email créateur (motif)
N-->>C: Contenu rejeté + motif
end
```
## Légende
**Modération préalable** : 3 premiers contenus uniquement
**Après validation** : Publication directe (modération a posteriori via signalements)
**Délai** : 48h maximum
**Strike** : +1 si violation grave

View File

@@ -1,64 +0,0 @@
# Notification violation de données (breach) - Procédure 72h CNIL
```mermaid
sequenceDiagram
participant Monitoring as Monitoring (Sentry/Grafana)
participant Equipe as Équipe Technique
participant DPO as DPO
participant DB as Base de données
participant CNIL as CNIL
participant Users as Utilisateurs impactés
Note over Monitoring: H+0 - Détection
Monitoring->>Equipe: Alerte breach détecté
Equipe->>Equipe: Confinement immédiat
Equipe->>DB: Bloquer accès compromis
Equipe->>DPO: Notification breach
Note over DPO,Equipe: H+0 à H+24 - Évaluation
DPO->>DB: Investigation périmètre
DB->>DPO: Données compromises
DPO->>DB: Liste utilisateurs impactés
DB->>DPO: X utilisateurs
DPO->>DPO: Évaluation gravité
Note over DPO: - Type données (GPS, email, etc.)<br/>- Nombre utilisateurs<br/>- Risque pour droits/libertés
alt Risque pour utilisateurs
Note over DPO: H+24 à H+48 - Préparation notification CNIL
DPO->>DPO: Rédaction rapport breach
Note over DPO: - Nature violation<br/>- Catégories/nb données<br/>- Conséquences probables<br/>- Mesures prises/envisagées
DPO->>CNIL: Notification sous 72h (email + formulaire en ligne)
CNIL->>DPO: Accusé réception
alt Risque élevé pour utilisateurs
Note over DPO: H+48 à H+72 - Notification utilisateurs
DPO->>Users: Email notification breach
Note over Users: - Nature violation<br/>- Coordonnées DPO<br/>- Mesures prises<br/>- Recommandations (changer mdp, etc.)
DPO->>Users: Notification in-app
end
else Risque faible (mesures techniques suffisantes)
DPO->>DPO: Documentation interne uniquement
Note over DPO: Pas de notification CNIL requise
end
Note over DPO: Post-incident
DPO->>Equipe: Audit sécurité complet
DPO->>DB: Enregistrement incident (registre violations)
DPO->>DPO: Plan correctif
```
**Légende** :
- **H+0 à H+24** : Détection, confinement, évaluation périmètre
- **H+24 à H+48** : Évaluation gravité, préparation rapport
- **H+48 à H+72** : Notification CNIL (si risque) + utilisateurs (si risque élevé)
- Délai CNIL : **72h maximum** (Article 33 RGPD)
- Notification utilisateurs obligatoire si **risque élevé** pour droits/libertés (Article 34 RGPD)

View File

@@ -1,40 +0,0 @@
# Séquence - Refresh Token
## Diagramme
```mermaid
sequenceDiagram
participant A as App Mobile
participant Z as Zitadel
participant DB as PostgreSQL
Note over A: Access token expiré (15min)
A->>Z: POST /oauth/token (refresh_token)
Z->>DB: Vérification hash refresh_token
alt Token valide
Z->>Z: Génération nouveaux tokens
Z->>DB: Update session (nouveau hash)
Z->>DB: Invalidation ancien refresh_token
Z-->>A: Nouveaux tokens
Note over DB: Rotation complète
else Token invalide ou rejoué
Z->>DB: Révocation toutes sessions user
Z-->>A: 401 Unauthorized
Note over A: Reconnexion requise
end
```
## Légende
**Rotation** :
- Nouveau refresh_token à chaque refresh
- Ancien token invalidé immédiatement
- Prévient replay attack
**Sécurité** :
- Si ancien token réutilisé → révocation globale
- Logs sécurité + notification utilisateur

View File

@@ -1,51 +0,0 @@
# Séquence - Signalement de contenu
## Diagramme
```mermaid
sequenceDiagram
participant U as Utilisateur
participant API as Backend API
participant DB as PostgreSQL
participant M as Modérateur
participant C as Créateur
participant N as Notification
U->>API: POST /reports (content_id, category, comment)
API->>DB: INSERT report (status=pending)
API->>DB: UPDATE contents.reports_count++
alt Priorité haute (3+ reports)
API->>N: Alerte modérateurs
end
API-->>U: Signalement enregistré
M->>API: GET /moderation/reports/pending
API-->>M: Reports triés par priorité
M->>API: PUT /reports/{id} (status=under_review)
M->>M: Écoute contenu + contexte
alt Violation confirmée
M->>API: POST /moderation/action (action_taken)
API->>DB: UPDATE content.status=moderated
API->>DB: UPDATE report.status=actioned
API->>DB: INSERT strike (créateur)
API->>N: Notification créateur + signaleur
N-->>C: Contenu retiré (motif + appel)
N-->>U: Action prise
else Infondé
M->>API: PUT /reports/{id} (status=dismissed)
API->>DB: UPDATE report
API->>N: Notification signaleur
N-->>U: Signalement rejeté
end
```
## Légende
**Priorité haute** : 3+ signalements ou catégories critiques (hate_speech, violence)
**Délai** : < 24h priorité haute, < 48h normale
**Anti-abus** : > 5 dismissed → warning signaleur
**Appel** : Créateur peut contester 7j après moderation

View File

@@ -1,52 +0,0 @@
# Suppression compte avec grace period
```mermaid
sequenceDiagram
participant User as Utilisateur
participant App as Application
participant DB as Base de données
participant Email as Service Email
participant Cron as Job Cron
User->>App: Demande suppression compte
App->>User: Confirmation (êtes-vous sûr ?)
User->>App: Confirmer
App->>DB: Désactiver compte (statut: pending_deletion)
App->>DB: Cacher contenus (visible: false)
App->>DB: Révoquer sessions/tokens
App->>DB: Enregistrer date suppression effective (J+30)
App->>Email: Email confirmation + lien annulation
Email->>User: Email (lien valide 30j)
App->>User: "Compte désactivé. Suppression dans 30 jours."
Note over User,App: Grace period 30 jours
alt Utilisateur annule
User->>App: Clic lien annulation
App->>DB: Réactiver compte (statut: active)
App->>DB: Restaurer visibilité contenus
App->>Email: Email confirmation réactivation
Email->>User: "Compte réactivé"
else 30 jours sans annulation
Cron->>DB: Job quotidien (vérif comptes pending_deletion)
DB->>Cron: Liste comptes J+30 dépassé
loop Pour chaque compte
Cron->>DB: Supprimer données personnelles
Cron->>DB: Anonymiser contenus (créateur: "Utilisateur supprimé")
Cron->>DB: Supprimer historique GPS/écoute
Cron->>DB: Marquer statut: deleted
end
Cron->>Email: Email confirmation suppression effective
Email->>User: "Compte définitivement supprimé"
end
```
**Légende** :
- Grace period : 30 jours
- Pendant grace period : compte inaccessible, contenus cachés
- Après 30j : suppression définitive, contenus anonymisés conservés

View File

@@ -1,44 +0,0 @@
# Cycle de vie - Suppression de compte
## Diagramme
```mermaid
stateDiagram-v2
[*] --> Requested: Utilisateur demande suppression
Requested --> GracePeriod: Compte désactivé, email envoyé
GracePeriod --> Cancelled: Clic lien annulation (< 30j)
GracePeriod --> PendingDeletion: Délai 30j écoulé
Cancelled --> [*]
PendingDeletion --> Deleted: Job cron suppression effective
Deleted --> [*]
```
## Règles
| État | Valeur | Description |
|------|--------|-------------|
| Requested | `requested` | Demande initiée, validation requise |
| Grace Period | `grace_period` | 30j annulation possible, compte inaccessible |
| Cancelled | `cancelled` | Utilisateur a annulé, compte réactivé |
| Pending Deletion | `pending_deletion` | File job cron (< 24h) |
| Deleted | `deleted` | Données supprimées, contenus anonymisés |
**Grace period** : 30 jours
**Pendant grace period** :
- Compte désactivé (login impossible)
- Contenus cachés (non diffusés)
- Sessions/tokens révoqués
- Email avec token annulation (valide 30j)
**Après 30j** :
- Données personnelles supprimées
- Contenus créés anonymisés (créateur = "Utilisateur supprimé")
- Historique GPS/écoute supprimé
- Irréversible

View File

@@ -1,44 +0,0 @@
# Cycle de vie - Incident de violation de données
## Diagramme
```mermaid
stateDiagram-v2
[*] --> Detected: Alerte monitoring
Detected --> Contained: Confinement immédiat (H+0)
Contained --> UnderInvestigation: Évaluation gravité (H+24)
UnderInvestigation --> Resolved: Risque faible (mesures suffisantes)
UnderInvestigation --> CNILNotificationRequired: Risque utilisateurs
CNILNotificationRequired --> CNILNotified: Notification CNIL (< H+72)
CNILNotified --> Resolved: Pas de risque élevé utilisateurs
CNILNotified --> UsersNotificationRequired: Risque élevé
UsersNotificationRequired --> UsersNotified: Email + push utilisateurs (< H+72)
UsersNotified --> Resolved: Post-mortem + correctifs
Resolved --> [*]
```
## Règles
| État | Valeur | Délai max |
|------|--------|-----------|
| Detected | `detected` | H+0 |
| Contained | `contained` | H+0 (immédiat) |
| Under Investigation | `under_investigation` | H+24 |
| CNIL Notification Required | `cnil_notification_required` | H+48 |
| CNIL Notified | `cnil_notified` | H+72 (Article 33 RGPD) |
| Users Notification Required | `users_notification_required` | H+48 |
| Users Notified | `users_notified` | H+72 (Article 34 RGPD) |
| Resolved | `resolved` | Post-incident |
**Sévérité** : `low` / `medium` / `high` / `critical`
**Notification CNIL** : Obligatoire si risque pour droits/libertés utilisateurs
**Notification utilisateurs** : Obligatoire si risque **élevé**
**Runbook** : `docs/rgpd/procedure-breach.md`

View File

@@ -1,56 +0,0 @@
# Cycle de vie - Compte utilisateur
## Diagramme
```mermaid
stateDiagram-v2
[*] --> PendingEmailVerification: Inscription
[*] --> PendingParentalConsent: Inscription 13-15 ans
PendingEmailVerification --> Active: Email vérifié (16+ ans)
PendingParentalConsent --> ActiveMinor: Parent valide
PendingParentalConsent --> Expired: Token expiré (7j)
Active --> Suspended: Strikes 3/4/5
Active --> GracePeriod: Demande suppression
Active --> Frozen: Gel temporaire (limitation traitement)
Active --> Deleted: Inactivité 5 ans
ActiveMinor --> Active: 16 ans atteints
ActiveMinor --> Suspended: Modération
ActiveMinor --> Deleted: Parent révoque
Frozen --> Active: Réactivation utilisateur
Suspended --> Active: Fin suspension / Appel
Suspended --> Deleted: Suspension définitive
GracePeriod --> Active: Annulation < 30j
GracePeriod --> Deleted: Après 30j
Expired --> [*]
Deleted --> [*]
```
## Règles
| État | Valeur | Durée/Condition |
|------|--------|-----------------|
| Pending Email Verification | `pending_email_verification` | Email non vérifié (expire 24h) |
| Pending Parental Consent | `pending_parental_consent` | Ado 13-15 ans, attente validation parent (expire 7j) |
| Active | `active` | Compte fonctionnel standard (16+ ans) |
| Active Minor | `active_minor` | Compte 13-15 ans avec restrictions parentales |
| Frozen | `frozen` | Gel temporaire (lecture seule), réactivable à tout moment |
| Suspended | `suspended` | Strike 3: 7j, Strike 4: 30j, Strike 5: définitif |
| Grace Period | `grace_period` | 30j avant suppression, annulable |
| Expired | `expired` | Token expiré sans validation |
| Deleted | `deleted` | Données supprimées, contenus anonymisés, irréversible |
**Restrictions Active Minor** :
- GPS précis : configurable par parent
- Messagerie privée : désactivée par défaut
- Contenus +16 : filtrés
- Transition auto vers `active` à 16 ans
**Purge inactivité** : 5 ans sans connexion (notifications 90j/30j/7j avant)

View File

@@ -1,32 +0,0 @@
# Cycle de vie - Consentement parental
## Diagramme
```mermaid
stateDiagram-v2
[*] --> PendingValidation: Ado saisit email parent
PendingValidation --> Validated: Parent clique lien (< 7j)
PendingValidation --> Expired: Délai 7j écoulé
Validated --> Revoked: Parent révoque consentement
Validated --> AutoRevoked: Ado atteint 16 ans
Expired --> [*]
Revoked --> [*]
AutoRevoked --> [*]
```
## Règles
| État | Valeur | Description |
|------|--------|-------------|
| Pending Validation | `pending_validation` | Email envoyé parent, token valide 7j |
| Validated | `validated` | Parent a validé, restrictions 13-15 ans actives |
| Expired | `expired` | Token expiré sans validation, compte inactif |
| Revoked | `revoked` | Parent révoque, compte désactivé immédiatement |
| Auto-Revoked | `auto_revoked` | Ado atteint 16 ans, restrictions levées automatiquement |
**Délai expiration** : 7 jours
**Révocation** : Possible à tout moment via dashboard parent
**Transition automatique** : À 16 ans → compte passe en `active` standard

View File

@@ -1,39 +0,0 @@
# Cycle de vie - Contenu
## Diagramme
```mermaid
stateDiagram-v2
[*] --> Draft: Création
Draft --> PendingReview: Soumission (3 premiers contenus)
Draft --> Published: Soumission (si > 3 validés)
PendingReview --> Published: Validation modérateur
PendingReview --> Rejected: Refus modérateur
Published --> Moderated: Signalement validé
Published --> Deleted: Suppression
Rejected --> Draft: Édition corrections
Rejected --> Deleted: Abandon
Moderated --> Published: Appel accepté
Moderated --> Deleted: Suppression définitive
Deleted --> [*]
```
## Règles
| État | Valeur | Description |
|------|--------|-------------|
| Draft | `draft` | Brouillon non visible |
| Pending Review | `pending_review` | Modération préalable (3 premiers, < 48h) |
| Published | `published` | Diffusé et visible |
| Moderated | `moderated` | Retiré après signalement |
| Rejected | `rejected` | Refusé par modération (éditable) |
| Deleted | `deleted` | Supprimé (fichiers conservés 30j) |
**Modération** : Préalable pour 3 premiers contenus, puis a posteriori (signalements)
**Appel** : Possible 7j après moderation

View File

@@ -1,38 +0,0 @@
# Cycle de vie - Export de données
## Diagramme
```mermaid
stateDiagram-v2
[*] --> Pending: Demande export
Pending --> Generating: Worker démarre
Generating --> Ready: Génération OK (< 48h)
Generating --> Failed: Erreur (retry 3x)
Ready --> Downloaded: Téléchargement
Ready --> Expired: Délai 7j écoulé
Downloaded --> Expired: Délai 7j écoulé
Failed --> Pending: Retry manuel
Expired --> [*]
Failed --> [*]
```
## Règles
| État | Valeur | Description |
|------|--------|-------------|
| Pending | `pending` | File d'attente (< 5 min) |
| Generating | `generating` | Worker background actif (< 48h RGPD) |
| Ready | `ready` | Disponible, lien email valide 7j |
| Downloaded | `downloaded` | Téléchargé (reste 7j) |
| Expired | `expired` | Supprimé automatiquement |
| Failed | `failed` | Échec après retry 3x |
**Format** : ZIP (JSON + HTML + audio files)
**Limite** : 1 export/mois
**Sécurité** : URL signée, token unique 7j

View File

@@ -1,44 +0,0 @@
# Cycle de vie - Incident de violation de données
## Diagramme
```mermaid
stateDiagram-v2
[*] --> Detected: Alerte monitoring
Detected --> Contained: Confinement immédiat (H+0)
Contained --> UnderInvestigation: Évaluation gravité (H+24)
UnderInvestigation --> Resolved: Risque faible (mesures suffisantes)
UnderInvestigation --> CNILNotificationRequired: Risque utilisateurs
CNILNotificationRequired --> CNILNotified: Notification CNIL (< H+72)
CNILNotified --> Resolved: Pas de risque élevé utilisateurs
CNILNotified --> UsersNotificationRequired: Risque élevé
UsersNotificationRequired --> UsersNotified: Email + push utilisateurs (< H+72)
UsersNotified --> Resolved: Post-mortem + correctifs
Resolved --> [*]
```
## Règles
| État | Valeur | Délai max |
|------|--------|-----------|
| Detected | `detected` | H+0 |
| Contained | `contained` | H+0 (immédiat) |
| Under Investigation | `under_investigation` | H+24 |
| CNIL Notification Required | `cnil_notification_required` | H+48 |
| CNIL Notified | `cnil_notified` | H+72 (Article 33 RGPD) |
| Users Notification Required | `users_notification_required` | H+48 |
| Users Notified | `users_notified` | H+72 (Article 34 RGPD) |
| Resolved | `resolved` | Post-incident |
**Sévérité** : `low` / `medium` / `high` / `critical`
**Notification CNIL** : Obligatoire si risque pour droits/libertés utilisateurs
**Notification utilisateurs** : Obligatoire si risque **élevé**
**Runbook** : `docs/rgpd/procedure-breach.md`

View File

@@ -1,32 +0,0 @@
# Cycle de vie - Consentement parental
## Diagramme
```mermaid
stateDiagram-v2
[*] --> PendingValidation: Ado saisit email parent
PendingValidation --> Validated: Parent clique lien (< 7j)
PendingValidation --> Expired: Délai 7j écoulé
Validated --> Revoked: Parent révoque consentement
Validated --> AutoRevoked: Ado atteint 16 ans
Expired --> [*]
Revoked --> [*]
AutoRevoked --> [*]
```
## Règles
| État | Valeur | Description |
|------|--------|-------------|
| Pending Validation | `pending_validation` | Email envoyé parent, token valide 7j |
| Validated | `validated` | Parent a validé, restrictions 13-15 ans actives |
| Expired | `expired` | Token expiré sans validation, compte inactif |
| Revoked | `revoked` | Parent révoque, compte désactivé immédiatement |
| Auto-Revoked | `auto_revoked` | Ado atteint 16 ans, restrictions levées automatiquement |
**Délai expiration** : 7 jours
**Révocation** : Possible à tout moment via dashboard parent
**Transition automatique** : À 16 ans → compte passe en `active` standard

View File

@@ -1,29 +0,0 @@
# Cycle de vie - Session
## Diagramme
```mermaid
stateDiagram-v2
[*] --> Active: Connexion
Active --> Active: Refresh token
Active --> Expired: Inactivité 30j
Active --> Revoked: Déconnexion manuelle
Active --> Revoked: Changement mot de passe
Active --> Revoked: Replay attack
Expired --> [*]
Revoked --> [*]
```
## Règles
| État | Condition | Description |
|------|-----------|-------------|
| Active | `revoked_at IS NULL` | Access token 15min, Refresh token 30j |
| Expired | `refresh_token_expires_at < NOW()` | Inactivité 30j |
| Revoked | `revoked_at IS NOT NULL` | Révoquée manuellement |
**Rotation** : Refresh token rotatif (nouveau à chaque refresh)
**Sécurité** : Tokens hashés SHA256, révocation globale si replay attack
**Nettoyage** : Suppression sessions expirées/révoquées > 7j/30j

View File

@@ -1,32 +0,0 @@
# Cycle de vie - Signalement
## Diagramme
```mermaid
stateDiagram-v2
[*] --> Pending: Signalement créé
Pending --> UnderReview: Prise en charge
Pending --> Duplicate: Doublon détecté
UnderReview --> Actioned: Violation confirmée
UnderReview --> Dismissed: Infondé
Actioned --> [*]
Dismissed --> [*]
Duplicate --> [*]
```
## Règles
| État | Valeur | Description |
|------|--------|-------------|
| Pending | `pending` | En attente (< 48h, < 24h si priorité haute) |
| Under Review | `under_review` | En cours d'examen |
| Actioned | `actioned` | Contenu retiré/modifié + strike |
| Dismissed | `dismissed` | Signalement rejeté |
| Duplicate | `duplicate` | Doublon (même contenu < 7j) |
**Priorité haute** : 3+ signalements ou catégories `hate_speech`, `violence`
**Actions** : Contenu retiré, strike, suspension selon gravité
**Anti-abus** : > 5 dismissed → warning, limite 3 signalements/jour

View File

@@ -1,44 +0,0 @@
# Cycle de vie - Suppression de compte
## Diagramme
```mermaid
stateDiagram-v2
[*] --> Requested: Utilisateur demande suppression
Requested --> GracePeriod: Compte désactivé, email envoyé
GracePeriod --> Cancelled: Clic lien annulation (< 30j)
GracePeriod --> PendingDeletion: Délai 30j écoulé
Cancelled --> [*]
PendingDeletion --> Deleted: Job cron suppression effective
Deleted --> [*]
```
## Règles
| État | Valeur | Description |
|------|--------|-------------|
| Requested | `requested` | Demande initiée, validation requise |
| Grace Period | `grace_period` | 30j annulation possible, compte inaccessible |
| Cancelled | `cancelled` | Utilisateur a annulé, compte réactivé |
| Pending Deletion | `pending_deletion` | File job cron (< 24h) |
| Deleted | `deleted` | Données supprimées, contenus anonymisés |
**Grace period** : 30 jours
**Pendant grace period** :
- Compte désactivé (login impossible)
- Contenus cachés (non diffusés)
- Sessions/tokens révoqués
- Email avec token annulation (valide 30j)
**Après 30j** :
- Données personnelles supprimées
- Contenus créés anonymisés (créateur = "Utilisateur supprimé")
- Historique GPS/écoute supprimé
- Irréversible

View File

@@ -1,37 +0,0 @@
# 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)

View File

@@ -1,67 +0,0 @@
# Modèle de données - Publicités
📖 Voir [Règles métier - Section 16 : Publicités](../rules/publicites.md) | [Entités globales](../../_shared/entities/vue-ensemble.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

View File

@@ -1,238 +0,0 @@
# 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.
"""

View File

@@ -1,45 +0,0 @@
# 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)

View File

@@ -1,69 +0,0 @@
# 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/vue-ensemble.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é

View File

@@ -1,63 +0,0 @@
# 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/vue-ensemble.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)

View File

@@ -1,205 +0,0 @@
# 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

View File

@@ -1,402 +0,0 @@
# 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."

View File

@@ -1,247 +0,0 @@
# 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%

View File

@@ -1,223 +0,0 @@
# 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

View File

@@ -1,339 +0,0 @@
# 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 "71" 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 "71" 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
}
"""

View File

@@ -1,239 +0,0 @@
# 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%

View File

@@ -1,191 +0,0 @@
# 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

View File

@@ -1,485 +0,0 @@
# 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)

View File

@@ -1,91 +0,0 @@
# 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

Some files were not shown because too many files have changed in this diff Show More