Major changes across backend, frontend, infrastructure: - Provider system with model selection (Google, DeepL, OpenAI, Ollama, Google Cloud) - Admin panel: user management, pricing, settings - Glossary system with CSV import/export - Subscription and tier quota management - Security hardening (rate limiting, API key auth, path traversal fixes) - Docker compose for dev, prod, and IONOS deployment - Alembic migrations for new tables - Frontend: dashboard, pricing page, landing page, i18n (en/fr) - Test suite and verification scripts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
153 lines
13 KiB
Markdown
153 lines
13 KiB
Markdown
# Story 1.8: Tracking Usage pour Billing
|
||
|
||
Status: done
|
||
|
||
<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
|
||
|
||
## Story
|
||
|
||
As a **system**,
|
||
I want **to track translation usage per user**,
|
||
so that **I can bill Pro users and monitor usage**.
|
||
|
||
## Acceptance Criteria
|
||
|
||
1. **AC1 : Incrément du compteur quotidien** — Given a user completes a translation, when the translation succeeds, then the user's `daily_translation_count` is incremented (déjà assuré par Story 1.6 / tier_quota + sync DB dans `/translate`).
|
||
2. **AC2 : Entrée translation_logs** — When the translation succeeds, a `translation_logs` entry is created (table `translations`) with: `user_id`, `file_name` (original_filename), `file_size` (file_size_bytes), `timestamp` (created_at), `status` ("completed"), `provider_used` (provider). Aucun contenu de fichier n'est jamais enregistré (NFR11, NFR16).
|
||
3. **AC3 : Pas de contenu dans les logs** — No file content is logged; only metadata (file name, size, timestamp, status, provider). NFR11 et NFR16 respectés.
|
||
|
||
## Tasks / Subtasks
|
||
|
||
- [x] **Task 1 : Créer l'entrée Translation après succès** (AC: 2, 3)
|
||
- [x] 1.1 Après une traduction réussie dans `POST /translate`, si l'utilisateur est authentifié (`current_user`), créer un enregistrement dans la table `translations` (modèle `Translation`) avec : user_id, original_filename, file_type, file_size_bytes, source_language, target_language, provider, status="completed", completed_at=now. Ne jamais stocker ni logger de contenu de fichier.
|
||
- [x] 1.2 Utiliser le repository existant (TranslationRepository.create puis update_status "completed" ou créer en "completed" directement si l'API le permet). Vérifier la signature de `create` dans database/repositories.py.
|
||
- [x] 1.3 S'assurer que le provider utilisé (google, deepl, ollama, openai, openrouter, libre) est bien enregistré (variable `provider` dans l'endpoint).
|
||
- [x] **Task 2 : Synchronisation daily_translation_count** (AC: 1)
|
||
- [x] 2.1 Vérifier que l'incrément Redis (tier_quota_service.increment_on_success) et la mise à jour DB (auth_update_user avec daily_translation_count) restent en place après succès (déjà présents dans main.py).
|
||
- [x] 2.2 Si le backend utilise uniquement le stockage JSON (sans DB SQL), documenter le comportement : soit pas d'entrée Translation (optionnel), soit un fichier JSON de "translation_logs" avec les mêmes champs métadonnées uniquement.
|
||
- [x] **Task 3 : Tests** (AC: 1–3)
|
||
- [x] 3.1 Test : après une traduction réussie par un utilisateur authentifié, une entrée existe dans `translations` avec user_id, original_filename, file_size_bytes, status="completed", provider correct.
|
||
- [x] 3.2 Test : aucun champ ne contient de contenu de document (vérifier schéma et politiques de log).
|
||
- [x] 3.3 Test : daily_translation_count utilisateur est incrémenté (déjà couvert par tests 1.6/1.7 si besoin de régression).
|
||
|
||
## Dev Notes
|
||
|
||
- **Contexte** : La table `translations` (modèle `Translation`) existe déjà dans database/models.py avec user_id, original_filename, file_type, file_size_bytes, source_language, target_language, provider, status, created_at, completed_at. TranslationRepository a une méthode `create()` et `update_status()`. Actuellement, après succès de `/translate`, on incrémente le quota (Redis + daily_translation_count en DB) mais on ne crée pas d'entrée dans `translations`. La story consiste à créer cette entrée à chaque traduction réussie (utilisateur authentifié).
|
||
- **Provider** : Dans main.py l'endpoint reçoit `provider` (openrouter, google, ollama, deepl, libre, openai). Utiliser cette valeur pour `provider_used`.
|
||
- **Fichier** : file.filename (original), file_size peut être obtenu après save ou via validation_result / output_info. Utiliser file_size_bytes du fichier source (input_path) ou de la réponse si disponible.
|
||
|
||
### Project Structure Notes
|
||
|
||
- Backend : main.py (POST /translate) ; database/models.py (Translation) ; database/repositories.py (TranslationRepository.create, update_status). Pas de nouveau module requis ; brancher l’appel au repository après la traduction réussie (après tier_quota increment et avant/suite auth_update_user).
|
||
|
||
### References
|
||
|
||
- [Source: _bmad-output/planning-artifacts/epics.md#Story 1.8]
|
||
- [Source: database/models.py — Translation model]
|
||
- [Source: database/repositories.py — TranslationRepository]
|
||
- [Source: main.py — POST /translate, tier_quota increment]
|
||
- [Source: NFR11, NFR16 — no document content in logs]
|
||
|
||
---
|
||
|
||
## Developer Context (Guardrails)
|
||
|
||
### Technical requirements
|
||
|
||
- **Backend** : FastAPI. Après une traduction réussie dans `POST /translate`, si `current_user` est défini : (1) garder l’appel à `tier_quota_service.increment_on_success(current_user.id)` et `auth_update_user(..., daily_translation_count + 1)` ; (2) créer un enregistrement `Translation` via le repository (DB) avec user_id, original_filename, file_type, file_size_bytes, source_language, target_language, provider, status="completed", completed_at=now. Ne jamais persister ni logger de contenu de fichier.
|
||
- **DB** : Utiliser la table existante `translations`. TranslationRepository.create() attend source_language, target_language, provider, file_type, file_size_bytes, page_count (optionnel). Puis appeler update_status(id, "completed") ou adapter create pour accepter status="completed" et completed_at si besoin.
|
||
- **Session/DB** : S’assurer d’utiliser la même session/engine que le reste de l’app (get_db ou équivalent dans main.py). Si l’app utilise uniquement auth en JSON sans DB, ne pas créer d’entrée Translation (ou prévoir un stockage fichier JSON pour “translation_logs” métadonnées uniquement).
|
||
|
||
### Architecture compliance
|
||
|
||
- Conventions API : pas de changement d’API publique ; comportement interne uniquement (enregistrement pour billing/analytics).
|
||
- Format des données : snake_case en DB et en JSON. Pas d’exposition du contenu des documents (NFR11, NFR16).
|
||
|
||
### Library / framework
|
||
|
||
- SQLAlchemy / repository existant (TranslationRepository). Pas de nouvelle lib.
|
||
|
||
### File structure
|
||
|
||
- main.py : après succès de la traduction (après excel_translator.translate_file / word / pptx), appeler le repository pour créer l’entrée Translation (avec get_db ou session injectée). Si plusieurs chemins (sync/async), utiliser la même session que pour auth_update_user (asyncio.to_thread si blocage).
|
||
- database/repositories.py : utiliser create() existant ; ajouter si besoin une méthode create_completed() qui crée directement en status "completed" avec completed_at pour éviter deux appels (create + update_status).
|
||
|
||
### Testing requirements
|
||
|
||
- Tests d’intégration ou unitaires : (1) traduction réussie avec utilisateur authentifié → une ligne dans `translations` avec les bons champs (user_id, original_filename, file_size_bytes, status="completed", provider) ; (2) aucun champ ne contient de texte traduit ou contenu binaire ; (3) régression : daily_translation_count et quota Redis inchangés (toujours incrémentés). Utiliser un mock du translate_file pour ne pas dépendre des vrais providers.
|
||
|
||
---
|
||
|
||
## Previous Story Intelligence (1-7 Admin - Changement de Tier Manuel)
|
||
|
||
- **Fichiers modifiés** : main.py (PATCH /admin/users/{user_id}), services/auth_service.py (update_user_plan), database/repositories.py (updated_at UTC), tests/test_admin_tier_change.py.
|
||
- **Patterns** : Réponses erreur `{ error, message, details? }` sans champ `data`. Utiliser Depends(require_admin) pour les routes admin. Pour la story 1.8, pas de route admin ; uniquement logique côté POST /translate et DB.
|
||
- **DB** : User avec tier/plan ; pas de changement de schéma User pour 1.8. Table Translation déjà présente.
|
||
|
||
---
|
||
|
||
## Git Intelligence Summary
|
||
|
||
- Codebase : POST /translate dans main.py fait déjà increment_on_success + auth_update_user(daily_translation_count+1). Il manque la création de l’entrée Translation (table `translations`) pour le billing/analytics. TranslationRepository.create() et update_status() existent ; il reste à les appeler au bon endroit avec les paramètres disponibles (file.filename, provider, target_language, source_language, file size depuis input_path ou file).
|
||
|
||
---
|
||
|
||
## Latest Tech Information
|
||
|
||
- **FastAPI** : Pour accéder à la DB dans un endpoint async, utiliser soit `Depends(get_db)` (session synchrone) puis asyncio.to_thread pour les appels repository, soit une session async si le projet est passé en SQLAlchemy async. Vérifier database/connection.py et comment get_db est utilisé dans main.py.
|
||
- **SQLAlchemy** : Translation model a déjà les champs nécessaires ; completed_at doit être renseigné manuellement si on crée en "completed" (datetime.now(timezone.utc)).
|
||
|
||
---
|
||
|
||
## Project Context Reference
|
||
|
||
- **Structure** : main.py contient POST /translate ; après la traduction (excel_translator.translate_file, etc.), le bloc actuel fait tier_quota increment et auth_update_user. Insérer la création de l’enregistrement Translation dans ce même bloc (après succès, avec user_id, filename, size, provider, status completed). get_db : vérifier si disponible dans main (FastAPI Depends) pour obtenir une session et appeler TranslationRepository(session).create(...).
|
||
- **Auth** : current_user peut être None (traduction sans auth) ; dans ce cas ne pas créer d’entrée Translation (ou créer avec user_id nullable si le schéma le permet — actuellement Translation.user_id est non nullable, donc ne créer que si current_user présent).
|
||
- **Périmètre** : Le translation log (AC2) s’applique à **POST /translate** uniquement. **POST /translate-batch** est hors scope pour cette story (pas d’auth utilisateur ni de log par fichier dans l’implémentation actuelle).
|
||
|
||
---
|
||
|
||
## Story Completion Status
|
||
|
||
- **Status** : done
|
||
- **Note** : Code review (adversarial) 2026-02-21 : correctifs HIGH/MEDIUM/LOW appliqués ; AC2 limité à POST /translate (batch hors scope).
|
||
|
||
## Change Log
|
||
|
||
- 2026-02-20: Story 1.8 created from epics + architecture + previous story 1-7.
|
||
- 2026-02-20: Story 1.8 implémentée : création entrée Translation après succès, get_sync_session, create_completed, tests unitaires et intégration.
|
||
- 2026-02-21: Code review (option 1 – correctifs auto) : timezone UTC dans repositories, logger.exception pour échec log, tests unauthenticated + échec log, file_type String(20), double commit supprimé, périmètre batch documenté. Corrections compatibilité auth_service/repository (lazy import get_sync_session, create/update, _db_user_to_model) pour tests intégration.
|
||
|
||
## Dev Agent Record
|
||
|
||
### Agent Model Used
|
||
|
||
{{agent_model_name_version}}
|
||
|
||
### Debug Log References
|
||
|
||
### Completion Notes List
|
||
|
||
- Task 1 : Après succès de POST /translate, si current_user et DB (USE_DATABASE + DATABASE_AVAILABLE), création d’un enregistrement Translation via TranslationRepository.create_completed (user_id, original_filename, file_type, file_size_bytes, source_language, target_language, provider, status=completed, completed_at). Taille fichier source via input_path.stat().st_size. Aucun contenu de fichier stocké (AC2, AC3, NFR11, NFR16).
|
||
- Task 2 : Vérifié : tier_quota_service.increment_on_success et auth_update_user(daily_translation_count+1) restent en place dans main.py. Sans DB (JSON only), pas d’entrée Translation.
|
||
- Task 3 : Tests ajoutés dans tests/test_translation_log_1_8.py : test_create_completed_inserts_row_with_metadata_only, test_translation_model_has_no_content_columns, test_translate_creates_translation_log_when_authenticated_and_db (intégration, skip si requests absent).
|
||
- database/connection.py : ajout sync_engine et get_sync_session (context manager) pour utilisation par auth_service et translation log.
|
||
- database/repositories.py : ajout TranslationRepository.create_completed() pour créer directement en status "completed" avec completed_at.
|
||
|
||
### File List
|
||
|
||
- database/connection.py (sync_engine, get_sync_session)
|
||
- database/repositories.py (create_completed, completed_at timezone UTC dans update_status)
|
||
- database/models.py (Translation.file_type String(20))
|
||
- main.py (création translation log après succès si current_user + DB ; logger.exception si échec)
|
||
- tests/test_translation_log_1_8.py (nouveau ; tests unauthenticated + échec log ; fixture client_with_db moteur dédié)
|
||
- services/auth_service.py (lazy import get_sync_session/UserRepository ; create/update compat repo ; _db_user_to_model getattr champs optionnels — pour tests intégration 1.8)
|
||
- _bmad-output/implementation-artifacts/sprint-status.yaml (1-8 → done)
|
||
- _bmad-output/implementation-artifacts/1-8-tracking-usage-pour-billing.md (statut, tâches, change log, file list)
|
||
|
||
## Senior Developer Review (AI)
|
||
|
||
- **Date :** 2026-02-21
|
||
- **Résultat :** Approuvé après correctifs automatiques (option 1).
|
||
- **Problèmes traités :** H2 timezone UTC dans update_status ; H3 test sans auth → pas de log ; H4 statut story aligné ; H1 batch documenté hors scope ; M1 logger.exception ; M2/M3 tests ; L1 file_type String(20), L2 double commit, L3 importorskip supprimé. Corrections additionnelles pour faire passer les tests intégration : auth_service lazy import get_sync_session, repo.create/update et _db_user_to_model champs optionnels, fixture client_with_db avec moteur dédié.
|