Files

176 lines
6.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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).