Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e209316157 |
@@ -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
10
.gitignore
vendored
@@ -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/
|
|
||||||
|
|||||||
59
CLAUDE.md
59
CLAUDE.md
@@ -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%
|
||||||
|
|||||||
28
Makefile
28
Makefile
@@ -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)"
|
||||||
|
|||||||
@@ -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) |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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
|
|
||||||
849
docs/INCONSISTENCIES-ANALYSIS.md
Normal file
849
docs/INCONSISTENCIES-ANALYSIS.md
Normal 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)
|
||||||
282
docs/README.md
282
docs/README.md
@@ -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
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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{
|
Data: map[string]string{
|
||||||
"deepLink": deepLink,
|
"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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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/
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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
|
|
||||||
319
docs/architecture/sequences/cache-geospatial.md
Normal file
319
docs/architecture/sequences/cache-geospatial.md
Normal 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)
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"]
|
|
||||||
@@ -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
|
|
||||||
@@ -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)
|
|
||||||
@@ -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é"`)
|
|
||||||
@@ -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"]`
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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€
|
|
||||||
@@ -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é
|
|
||||||
@@ -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)
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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 |
|
|
||||||
@@ -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é
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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 |
|
|
||||||
@@ -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"
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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)
|
|
||||||
@@ -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
|
|
||||||
@@ -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]`
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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)
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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`
|
|
||||||
@@ -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)
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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`
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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)
|
|
||||||
@@ -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
|
|
||||||
@@ -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.
|
|
||||||
"""
|
|
||||||
@@ -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)
|
|
||||||
@@ -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é
|
|
||||||
@@ -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)
|
|
||||||
@@ -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
|
|
||||||
@@ -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."
|
|
||||||
@@ -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%
|
|
||||||
@@ -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
|
|
||||||
@@ -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 "7→1" n'est déclenché
|
|
||||||
Et le déclenchement se fait au point GPS exact (rayon 30m)
|
|
||||||
Et une notification "Ding" + toast 2s est envoyée
|
|
||||||
|
|
||||||
Scénario: Notification 7s avant pour contenus géolocalisés simples (1 séquence)
|
|
||||||
Étant donné un contenu géolocalisé simple (1 séquence unique)
|
|
||||||
Et que l'utilisateur approche du point GPS
|
|
||||||
Quand l'ETA devient 7 secondes
|
|
||||||
Alors une notification avec compteur "7→1" est déclenchée
|
|
||||||
Et l'utilisateur doit valider avec bouton "Suivant"
|
|
||||||
|
|
||||||
# Exception quota pour audio-guides multi-séquences
|
|
||||||
|
|
||||||
Scénario: Audio-guide multi-séquences compte 1 seul contenu dans quota horaire
|
|
||||||
Étant donné un audio-guide "Visite Safari" avec 12 séquences
|
|
||||||
Et que l'utilisateur a un quota de 0/6 contenus géolocalisés
|
|
||||||
Quand l'utilisateur démarre l'audio-guide (séquence 1)
|
|
||||||
Alors le quota passe à 1/6
|
|
||||||
Quand l'utilisateur écoute les 12 séquences complètes
|
|
||||||
Alors le quota reste à 1/6
|
|
||||||
Et toutes les séquences ne consomment PAS 12 quotas
|
|
||||||
Et l'audio-guide entier compte comme 1 seul contenu
|
|
||||||
|
|
||||||
Scénario: Contenus géolocalisés simples consomment 1 quota chacun
|
|
||||||
Étant donné que l'utilisateur a un quota de 0/6
|
|
||||||
Quand l'utilisateur accepte un contenu géolocalisé simple "Tour Eiffel"
|
|
||||||
Alors le quota passe à 1/6
|
|
||||||
Quand l'utilisateur accepte un contenu géolocalisé simple "Arc de Triomphe"
|
|
||||||
Alors le quota passe à 2/6
|
|
||||||
Quand l'utilisateur accepte un contenu géolocalisé simple "Louvre"
|
|
||||||
Alors le quota passe à 3/6
|
|
||||||
Et chaque contenu simple consomme 1 quota
|
|
||||||
|
|
||||||
Scénario: Mixte audio-guides + contenus simples respecte quota 6/h
|
|
||||||
Étant donné que l'utilisateur a un quota de 0/6
|
|
||||||
Quand l'utilisateur démarre un audio-guide 8 séquences "Safari"
|
|
||||||
Alors le quota passe à 1/6
|
|
||||||
Quand l'utilisateur accepte 5 contenus géolocalisés simples
|
|
||||||
Alors le quota passe à 6/6
|
|
||||||
Et le quota horaire est atteint
|
|
||||||
Quand un 7ème contenu est détecté
|
|
||||||
Alors aucune notification n'est envoyée (quota atteint)
|
|
||||||
|
|
||||||
# Cache et optimisations
|
|
||||||
|
|
||||||
Scénario: Cache Redis pour calculs GPS fréquents
|
|
||||||
Étant donné que les points GPS d'un audio-guide sont en cache Redis
|
|
||||||
Quand je fais un POST sur "/api/v1/audio-guides/ag_123/check-proximity"
|
|
||||||
Alors les points GPS sont récupérés depuis Redis (pas PostgreSQL)
|
|
||||||
Et le temps de réponse est < 50ms
|
|
||||||
|
|
||||||
Scénario: Geospatial GEORADIUS Redis pour recherche proximité
|
|
||||||
Étant donné que tous les audio-guides sont indexés dans Redis (GEOADD)
|
|
||||||
Et une position utilisateur (43.1234, 2.5678)
|
|
||||||
Quand je recherche les audio-guides dans un rayon de 5 km
|
|
||||||
Alors Redis GEORADIUS retourne les audio-guides proches
|
|
||||||
Et le temps de réponse est < 20ms
|
|
||||||
|
|
||||||
# Mise à jour position temps réel
|
|
||||||
|
|
||||||
Scénario: WebSocket pour mise à jour position en temps réel
|
|
||||||
Étant donné une connexion WebSocket active pour l'audio-guide
|
|
||||||
Quand l'utilisateur envoie sa nouvelle position via WS
|
|
||||||
Alors le serveur calcule immédiatement la proximité
|
|
||||||
Et retourne distance + ETA via WS (pas de polling HTTP)
|
|
||||||
|
|
||||||
Scénario: Throttling position updates (max 1/seconde)
|
|
||||||
Étant donné que le client envoie des positions GPS toutes les 200ms
|
|
||||||
Quand le serveur reçoit les mises à jour
|
|
||||||
Alors seules les positions espacées de >1 seconde sont traitées
|
|
||||||
Et les autres sont ignorées (throttling)
|
|
||||||
|
|
||||||
# Cas d'erreur
|
|
||||||
|
|
||||||
Scénario: Position GPS invalide (coordonnées hors limites)
|
|
||||||
Étant donné une position avec latitude 95.0000 (invalide)
|
|
||||||
Quand je fais un POST sur "/api/v1/audio-guides/{id}/check-proximity"
|
|
||||||
Alors le code HTTP de réponse est 400
|
|
||||||
Et le message d'erreur est "latitude: doit être entre -90 et 90"
|
|
||||||
|
|
||||||
Scénario: Audio-guide sans points GPS (mode piéton)
|
|
||||||
Étant donné un audio-guide en mode piéton sans points GPS
|
|
||||||
Quand je fais un POST sur "/api/v1/audio-guides/{id}/check-proximity"
|
|
||||||
Alors le code HTTP de réponse est 400
|
|
||||||
Et le message d'erreur est "Audio-guide en mode manuel, pas de déclenchement GPS"
|
|
||||||
|
|
||||||
Scénario: Séquence déjà complétée (skip calcul si utilisateur a déjà passé)
|
|
||||||
Étant donné que l'utilisateur est à la séquence 5
|
|
||||||
Et qu'il vérifie la proximité du point 3 (déjà écouté)
|
|
||||||
Quand je fais un POST sur "/api/v1/audio-guides/{id}/check-proximity"
|
|
||||||
Alors le calcul n'est pas effectué pour les séquences passées
|
|
||||||
Et le message "Séquence déjà écoutée" est retourné
|
|
||||||
|
|
||||||
Scénario: Précision GPS insuffisante
|
|
||||||
Étant donné une position avec accuracy ±150m
|
|
||||||
Et un rayon de déclenchement de 30m
|
|
||||||
Quand la précision est vérifiée
|
|
||||||
Alors un warning est retourné:
|
|
||||||
"""json
|
|
||||||
{
|
|
||||||
"warning": "low_gps_accuracy",
|
|
||||||
"message": "Précision GPS insuffisante (±150m). Déclenchement automatique peut être perturbé.",
|
|
||||||
"accuracy": 150,
|
|
||||||
"trigger_radius": 30
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Performance
|
|
||||||
|
|
||||||
Scénario: Optimisation requêtes PostGIS avec index spatial
|
|
||||||
Étant donné que les points GPS ont un index GIST (PostGIS)
|
|
||||||
Quand une requête ST_DWithin est exécutée
|
|
||||||
Alors l'index spatial est utilisé
|
|
||||||
Et le temps d'exécution est < 10ms
|
|
||||||
|
|
||||||
Scénario: Batch proximity check pour tous les points
|
|
||||||
Étant donné un audio-guide avec 20 séquences
|
|
||||||
Quand je fais un POST sur "/api/v1/audio-guides/{id}/batch-proximity":
|
|
||||||
"""json
|
|
||||||
{
|
|
||||||
"user_position": {"latitude": 43.1234, "longitude": 2.5678}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
Alors toutes les distances sont calculées en une seule requête PostGIS
|
|
||||||
Et le corps de réponse contient:
|
|
||||||
"""json
|
|
||||||
{
|
|
||||||
"sequences": [
|
|
||||||
{"sequence_id": "seq_1", "distance": 0, "in_zone": true},
|
|
||||||
{"sequence_id": "seq_2", "distance": 150, "in_zone": false},
|
|
||||||
{"sequence_id": "seq_3", "distance": 350, "in_zone": false}
|
|
||||||
],
|
|
||||||
"current_sequence": 1,
|
|
||||||
"next_sequence": 2
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
@@ -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%
|
|
||||||
@@ -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
|
|
||||||
@@ -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)
|
|
||||||
@@ -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
Reference in New Issue
Block a user