- 0008 — Modified Dietz: justifies the choice over ROI / TWR / IRR; references commands/return_calculator.rs and the 7 TDD cases. - 0009 — Proxy price-fetching via maximus-api: documents the privacy proxy architecture (header stripping, no log correlation, fixed simpl-resultat UA), the Yahoo + CoinGecko adapter abstraction, the Bearer activation_token auth strategy, the rate limiting (client + server), and the dual-side premium gating. Implementation stays BLOCKED in #143; this ADR documents the agreed-upon design. - 0010 — FK ON DELETE RESTRICT on balance_account_transfers .transaction_id: justifies the integrity-over-friction trade-off for Modified Dietz reproducibility. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
85 lines
7.7 KiB
Markdown
85 lines
7.7 KiB
Markdown
# ADR 0010 — `ON DELETE RESTRICT` sur `balance_account_transfers.transaction_id`
|
|
|
|
- Status: Accepted
|
|
- Date: 2026-04-25
|
|
- Milestone: `overnight-2026-04-26-bilan` (Issues #138 → #145)
|
|
|
|
## Context
|
|
|
|
La table `balance_account_transfers` lie une `transaction` existante à un `balance_account` avec une direction (`'in'` = capital ajouté au compte, `'out'` = capital retiré). Cette table est l'input du calcul Modified Dietz (cf. [ADR 0008](0008-modified-dietz-pour-rendement.md)) qui sépare les **apports** des **gains réels** pour calculer la performance d'un compte d'investissement.
|
|
|
|
La question structurante : que se passe-t-il si l'utilisateur supprime une transaction qui est liée à un transfert de bilan ?
|
|
|
|
Trois politiques de FK sont possibles côté SQL :
|
|
|
|
| Politique | Comportement | Intégrité historique | Friction utilisateur |
|
|
|-----------|--------------|----------------------|----------------------|
|
|
| `ON DELETE CASCADE` | Suppression de la transaction supprime aussi le transfert | ❌ Le rendement Modified Dietz d'une période passée change rétroactivement | ✅ Aucune friction : tout disparaît silencieusement |
|
|
| `ON DELETE SET NULL` | Le transfert reste mais perd son `transaction_id` | ⚠ Le transfert devient "orphelin" : direction connue mais montant introuvable (les montants vivent dans `transactions.amount`) | ⚠ État partiellement valide |
|
|
| **`ON DELETE RESTRICT`** | La suppression est bloquée par SQLite tant que des transferts pointent vers la transaction | ✅ Préservée : un rendement déjà calculé reste reproductible | ⚠ L'utilisateur doit délier explicitement avant suppression |
|
|
|
|
Contraintes du contexte :
|
|
- Modified Dietz produit un rendement **R** sur une période **[t1, t2]** à partir de `(V_début, V_fin, [(date, montant)])`. Si une `transaction` liée disparaît silencieusement (CASCADE), la fonction reste pure mais ses inputs changent — `R` calculé hier ≠ `R` calculé aujourd'hui sur la même période. C'est exactement l'antithèse de la reproductibilité financière.
|
|
- Le calcul est déclenché à la demande (chargement de `BalanceAccountsTable`), il n'y a pas de cache server-side. Donc l'historique de "ce que le user a vu hier" n'existe pas : si les inputs bougent, le résultat affiché change sans que l'utilisateur sache pourquoi.
|
|
- L'usage attendu de la suppression de transactions est rare et lié à des erreurs d'import (doublons, mauvaise source). Bloquer ce cas avec un message clair est acceptable.
|
|
|
|
## Decision
|
|
|
|
**Adopter `ON DELETE RESTRICT` sur `balance_account_transfers.transaction_id`** :
|
|
|
|
```sql
|
|
CREATE TABLE balance_account_transfers (
|
|
...
|
|
transaction_id INTEGER NOT NULL,
|
|
...
|
|
FOREIGN KEY (transaction_id) REFERENCES transactions(id) ON DELETE RESTRICT
|
|
);
|
|
```
|
|
|
|
### UX correspondante
|
|
|
|
La couche service `transactionService.ts` détecte l'erreur SQLite `FOREIGN KEY constraint failed` et la transforme en `TransactionLinkedToBalanceError` typée, qui porte la liste des comptes liés. La UI affiche alors :
|
|
|
|
> **Cette transaction est liée au compte de bilan _<nom du compte>_.**
|
|
> Pour la supprimer, déliez-la d'abord : ouvrez le compte → Lier transferts → décochez cette transaction.
|
|
|
|
Avec un lien direct vers la `LinkTransfersModal` du compte concerné. L'utilisateur ne peut pas se retrouver bloqué : le chemin de déliaison est toujours dispo, à un clic du message d'erreur.
|
|
|
|
Pour les chemins bulk (`deleteImportWithTransactions`, `deleteAllImportsWithTransactions`), une pré-vérification SELECT (`LIMIT 50`) liste les premiers transferts liés AVANT de tenter la suppression — l'utilisateur voit un message agrégé "X transactions de cet import sont liées à des comptes de bilan" plutôt qu'un raw FK error toast.
|
|
|
|
### Direction CASCADE conservée pour `account_id`
|
|
|
|
À noter : la même table a une autre FK, `account_id`, configurée en `ON DELETE CASCADE`. Si l'utilisateur supprime un compte de bilan, ses transferts disparaissent — c'est cohérent puisque les rendements de ce compte n'ont plus lieu d'être.
|
|
|
|
L'asymétrie est délibérée :
|
|
- `account_id` ON DELETE CASCADE : le compte de bilan est l'objet "principal" du domaine Bilan, sa suppression nettoie ses dépendances internes
|
|
- `transaction_id` ON DELETE RESTRICT : la transaction est externe au domaine Bilan, sa suppression ne doit pas casser silencieusement les calculs
|
|
|
|
## Consequences
|
|
|
|
### Positive
|
|
|
|
- **Reproductibilité Modified Dietz garantie.** Un rendement calculé sur une période passée ne peut pas changer à cause d'une suppression invisible côté `transactions`.
|
|
- **Audit trail préservé.** L'utilisateur qui consulte un compte de bilan voit toujours les mêmes flux pour les mêmes périodes, peu importe quand il consulte.
|
|
- **Erreur visible et actionnable.** L'utilisateur reçoit un message concret avec un chemin clair pour résoudre, plutôt qu'une suppression silencieuse qui invaliderait l'historique financier.
|
|
- **Aligné avec la convention SQL existante du projet.** D'autres FK utilisent déjà `RESTRICT` quand l'intégrité est critique (cf. `balance_accounts.balance_category_id`, `balance_snapshot_lines.account_id`).
|
|
|
|
### Negative / trade-offs
|
|
|
|
- **Friction utilisateur** : forcer l'unlink explicite avant suppression ajoute 2 clics (ouvrir le compte → ouvrir LinkTransfersModal → décocher → revenir → supprimer). Acceptable car le cas est rare et le coût d'un rendement faux est élevé.
|
|
- **Couplage UI ↔ erreur SQL** : `transactionService.ts` doit détecter le format d'erreur SQLite (`FOREIGN KEY constraint failed`) et le mapper sur `TransactionLinkedToBalanceError`. Si tauri-plugin-sql change le format du message d'erreur, le mapping casse silencieusement (mitigé par les tests d'intégration co-localisés dans `transactionService.test.ts`).
|
|
- **Pré-vérification bulk a un coût** : un `SELECT ... LIMIT 50` sur `balance_account_transfers` à chaque suppression d'import. Négligeable en pratique (la table reste petite), mais à surveiller si un utilisateur a des dizaines de milliers de transferts.
|
|
|
|
## Alternatives considered
|
|
|
|
- **`ON DELETE CASCADE`.** Rejeté : trahit la promesse de reproductibilité du calcul Modified Dietz. Un rendement vu hier peut changer sans signal vers l'utilisateur.
|
|
- **`ON DELETE SET NULL` + transferts orphelins.** Rejeté : laisse la base dans un état "valide mais incohérent". Le transfert sait sa direction mais a perdu son montant (qui vit dans `transactions.amount`). Le code Modified Dietz devrait alors filtrer les orphelins, et l'utilisateur ne saurait plus pourquoi son rendement a changé. Pire que CASCADE, qui au moins est explicite.
|
|
- **Pas de FK du tout, juste un INTEGER orphelin possible.** Rejeté : retire toute garantie d'intégrité référentielle, et les calculs de rendement deviendraient une chasse aux pointeurs cassés.
|
|
- **Soft-delete des transactions (`deleted_at` au lieu de DELETE)** pour préserver les données liées tout en cachant la transaction de l'UI. Rejeté pour l'instant : les transactions n'ont pas de soft-delete dans le schéma actuel et l'introduire ouvrirait un chantier transversal (toutes les requêtes de transactions devraient filtrer `WHERE deleted_at IS NULL`). À reconsidérer si plusieurs domaines en font la demande.
|
|
|
|
## References
|
|
|
|
- Implémentation : `src-tauri/src/database/balance_schema.sql` (FK definition), `src/services/transactionService.ts` (`TransactionLinkedToBalanceError` mapping)
|
|
- Tests : `src/services/transactionService.test.ts` (mapping FK error → typed error), `src/__integration__/balance-flow.test.ts` (lien + tentative de suppression bloquée)
|
|
- Spec : [`spec-decisions-bilan.md`](../../spec-decisions-bilan.md) — décision "FK `balance_account_transfers.transaction_id` : `ON DELETE RESTRICT` + UI force unlink avec message clair"
|
|
- ADR liée : [0008 — Modified Dietz pour le calcul du rendement](0008-modified-dietz-pour-rendement.md)
|