feat: budget-tracker — spec, constitution, data-model, API contracts, plan
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
# Modèle de Données — Budget Tracker
|
||||
|
||||
## Principes directeurs
|
||||
- Tous les montants sont stockés en **centimes** (INTEGER) — principe II de la Constitution
|
||||
- Les suppressions sont des **soft-deletes** via `deleted_at` — principe II (tracabilité)
|
||||
- Les identifiants sont des **UUID v4** — évite l'exposition de séquences prédictibles
|
||||
- Les timestamps sont en **UTC** sans timezone stockée
|
||||
|
||||
---
|
||||
|
||||
## Entités
|
||||
|
||||
### User
|
||||
| Champ | Type | Contraintes |
|
||||
|-------|------|-------------|
|
||||
| id | UUID | PK, auto (gen_random_uuid()) |
|
||||
| email | VARCHAR(255) | UNIQUE, NOT NULL |
|
||||
| hashed_password | VARCHAR(255) | NOT NULL |
|
||||
| full_name | VARCHAR(100) | NOT NULL |
|
||||
| is_active | BOOLEAN | DEFAULT true, NOT NULL |
|
||||
| created_at | TIMESTAMP | NOT NULL, DEFAULT now() |
|
||||
| updated_at | TIMESTAMP | NOT NULL, DEFAULT now() |
|
||||
|
||||
**Notes** :
|
||||
- `email` indexé (recherche lors de l'authentification)
|
||||
- `hashed_password` : bcrypt, jamais exposé en API
|
||||
- `is_active` : permet de désactiver un compte sans le supprimer
|
||||
|
||||
---
|
||||
|
||||
### Category
|
||||
| Champ | Type | Contraintes |
|
||||
|-------|------|-------------|
|
||||
| id | UUID | PK, auto |
|
||||
| user_id | UUID | FK → User.id, NOT NULL |
|
||||
| name | VARCHAR(50) | NOT NULL |
|
||||
| type | ENUM(income, expense) | NOT NULL |
|
||||
| color | VARCHAR(7) | NULL, format hex (#RRGGBB) |
|
||||
| icon | VARCHAR(50) | NULL, nom d'icône (ex: "shopping-cart") |
|
||||
| is_default | BOOLEAN | DEFAULT false, NOT NULL |
|
||||
| deleted_at | TIMESTAMP | NULL = actif |
|
||||
| created_at | TIMESTAMP | NOT NULL, DEFAULT now() |
|
||||
|
||||
**Notes** :
|
||||
- `is_default = true` : catégories système créées au premier login (Alimentation, Transport, Logement, Loisirs, Santé, Revenus)
|
||||
- Les catégories par défaut ont `user_id` de l'utilisateur propriétaire — pas de catégories globales partagées (simplicité)
|
||||
- Index : `(user_id, deleted_at)` pour les requêtes de listing
|
||||
- Contrainte : impossible de supprimer si transactions actives liées (contrôlé en service, pas en DB)
|
||||
|
||||
---
|
||||
|
||||
### Transaction
|
||||
| Champ | Type | Contraintes |
|
||||
|-------|------|-------------|
|
||||
| id | UUID | PK, auto |
|
||||
| user_id | UUID | FK → User.id, NOT NULL |
|
||||
| category_id | UUID | FK → Category.id, NOT NULL |
|
||||
| amount_cents | INTEGER | NOT NULL, CHECK (amount_cents > 0) |
|
||||
| type | ENUM(income, expense) | NOT NULL |
|
||||
| description | VARCHAR(255) | NULL autorisé |
|
||||
| transaction_date | DATE | NOT NULL |
|
||||
| deleted_at | TIMESTAMP | NULL = actif |
|
||||
| created_at | TIMESTAMP | NOT NULL, DEFAULT now() |
|
||||
| updated_at | TIMESTAMP | NOT NULL, DEFAULT now() |
|
||||
|
||||
**Notes** :
|
||||
- `amount_cents > 0` : le signe est porté par `type`, jamais par le montant
|
||||
- `transaction_date` : date métier saisie par l'utilisateur (≠ `created_at` technique)
|
||||
- Index : `(user_id, transaction_date, deleted_at)` pour les requêtes par période
|
||||
- Index : `(user_id, category_id, deleted_at)` pour les agrégats par catégorie
|
||||
- Le soft-delete (`deleted_at IS NOT NULL`) masque la transaction de l'UI sans la détruire
|
||||
|
||||
---
|
||||
|
||||
### Budget
|
||||
| Champ | Type | Contraintes |
|
||||
|-------|------|-------------|
|
||||
| id | UUID | PK, auto |
|
||||
| user_id | UUID | FK → User.id, NOT NULL |
|
||||
| category_id | UUID | FK → Category.id, NOT NULL |
|
||||
| month | CHAR(7) | NOT NULL, format YYYY-MM |
|
||||
| limit_cents | INTEGER | NOT NULL, CHECK (limit_cents > 0) |
|
||||
| created_at | TIMESTAMP | NOT NULL, DEFAULT now() |
|
||||
| updated_at | TIMESTAMP | NOT NULL, DEFAULT now() |
|
||||
| UNIQUE | | (user_id, category_id, month) |
|
||||
|
||||
**Notes** :
|
||||
- `month` en CHAR(7) format ISO `YYYY-MM` : simple, triable lexicographiquement, pas de confusion timezone
|
||||
- La contrainte UNIQUE garantit un seul budget par catégorie par mois par utilisateur
|
||||
- Index implicite sur la contrainte UNIQUE
|
||||
- Pas de soft-delete : un budget supprimé est réellement supprimé (pas de données financières dans l'entité elle-même)
|
||||
|
||||
---
|
||||
|
||||
### RefreshToken
|
||||
| Champ | Type | Contraintes |
|
||||
|-------|------|-------------|
|
||||
| id | UUID | PK, auto |
|
||||
| user_id | UUID | FK → User.id, NOT NULL |
|
||||
| token_hash | VARCHAR(255) | UNIQUE, NOT NULL |
|
||||
| expires_at | TIMESTAMP | NOT NULL |
|
||||
| revoked_at | TIMESTAMP | NULL = actif |
|
||||
| created_at | TIMESTAMP | NOT NULL, DEFAULT now() |
|
||||
|
||||
**Notes** :
|
||||
- Seul le hash du token est stocké (SHA-256), jamais le token brut
|
||||
- Permet la révocation explicite (logout) et l'invalidation par rotation
|
||||
- Nettoyage périodique des tokens expirés recommandé (tâche cron ou au login)
|
||||
|
||||
---
|
||||
|
||||
## Relations
|
||||
|
||||
```
|
||||
User 1 ──────────────────────────── N Category
|
||||
User 1 ──────────────────────────── N Transaction
|
||||
User 1 ──────────────────────────── N Budget
|
||||
User 1 ──────────────────────────── N RefreshToken
|
||||
Category 1 ──────────────────────── N Transaction
|
||||
Category 1 ──────────────────────── N Budget
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Requêtes critiques et leurs index
|
||||
|
||||
### Solde courant d'un utilisateur
|
||||
```sql
|
||||
SELECT
|
||||
SUM(CASE WHEN type = 'income' THEN amount_cents ELSE -amount_cents END)
|
||||
FROM transactions
|
||||
WHERE user_id = :uid AND deleted_at IS NULL;
|
||||
```
|
||||
→ Index : `(user_id, deleted_at)`
|
||||
|
||||
### Résumé mensuel (revenus/dépenses d'un mois)
|
||||
```sql
|
||||
SELECT type, SUM(amount_cents)
|
||||
FROM transactions
|
||||
WHERE user_id = :uid
|
||||
AND transaction_date >= :month_start
|
||||
AND transaction_date < :month_end
|
||||
AND deleted_at IS NULL
|
||||
GROUP BY type;
|
||||
```
|
||||
→ Index : `(user_id, transaction_date, deleted_at)`
|
||||
|
||||
### Consommation d'un budget (dépenses catégorie × mois)
|
||||
```sql
|
||||
SELECT SUM(t.amount_cents)
|
||||
FROM transactions t
|
||||
JOIN budgets b ON b.category_id = t.category_id
|
||||
WHERE t.user_id = :uid
|
||||
AND t.category_id = :cat_id
|
||||
AND t.type = 'expense'
|
||||
AND t.transaction_date >= :month_start
|
||||
AND t.transaction_date < :month_end
|
||||
AND t.deleted_at IS NULL;
|
||||
```
|
||||
→ Index : `(user_id, category_id, deleted_at)`
|
||||
|
||||
---
|
||||
|
||||
## Valeurs par défaut — Catégories système
|
||||
|
||||
| Nom | Type | Couleur | Icône |
|
||||
|-----|------|---------|-------|
|
||||
| Alimentation | expense | #22c55e | utensils |
|
||||
| Transport | expense | #3b82f6 | car |
|
||||
| Logement | expense | #f59e0b | home |
|
||||
| Loisirs | expense | #a855f7 | gamepad-2 |
|
||||
| Santé | expense | #ef4444 | heart-pulse |
|
||||
| Revenus | income | #10b981 | trending-up |
|
||||
|
||||
Créées automatiquement lors du premier login utilisateur (dans la logique de service, pas en seed DB globale).
|
||||
Reference in New Issue
Block a user