36 KiB
Simpl-Résultat - Spécification Détaillée v2
Table des matières
- Résumé exécutif
- Description générale enrichie
- Architecture proposée
- Spécifications fonctionnelles détaillées
- Modèle de données
- Interface utilisateur
- Points faibles identifiés et recommandations
- Considérations techniques
- Plan de développement suggéré
Résumé exécutif
Simpl-Résultat est une application de bureau locale permettant aux utilisateurs de suivre leurs dépenses personnelles à partir d'exports CSV bancaires. L'application vise à catégoriser automatiquement les transactions, permettre des ajustements manuels, et fournir des rapports visuels pour le suivi budgétaire.
Objectifs principaux
- Centraliser les données financières de multiples sources (comptes chèques, épargne, cartes de crédit)
- Automatiser la catégorisation des dépenses via mots-clés
- Permettre un suivi budgétaire mensuel avec comparaisons historiques
- Offrir une expérience utilisateur simple et locale (pas de cloud)
Description générale enrichie
Vision du produit
Une application de finances personnelles 100% locale (privacy-first) qui transforme des exports CSV bancaires bruts en insights financiers actionnables, sans nécessiter de connexion à des services tiers ou de partage de données sensibles.
Proposition de valeur
| Aspect | Description |
|---|---|
| Confidentialité | Données stockées localement uniquement |
| Simplicité | Import CSV sans configuration bancaire complexe |
| Flexibilité | Support multi-sources, multi-formats |
| Automatisation | Catégorisation intelligente par mots-clés |
| Contrôle | Écritures d'ajustement et personnalisation complète |
Utilisateurs cibles
- Particuliers souhaitant suivre leur budget sans services cloud
- Utilisateurs soucieux de la confidentialité de leurs données financières
- Personnes ayant plusieurs comptes bancaires à consolider
Architecture proposée
Stack technologique recommandé
┌─────────────────────────────────────────────────────────────┐
│ PRÉSENTATION │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Electron / Tauri (Frontend) │ │
│ │ React/Vue/Svelte + TailwindCSS │ │
│ │ Charts: Recharts / Chart.js │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ LOGIQUE │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Service Layer (TypeScript) │ │
│ │ • ImportService • CategoryService │ │
│ │ • TransactionService • BudgetService │ │
│ │ • ReportingService • AdjustmentService │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ DONNÉES │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ SQLite (Base locale) │ │
│ │ + better-sqlite3 / sql.js │ │
│ └─────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Fichiers de configuration │ │
│ │ JSON (préférences) │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Justification des choix
| Composant | Option recommandée | Raison |
|---|---|---|
| Framework desktop | Tauri (P1) ou Electron | Tauri = plus léger, Rust backend, meilleure sécurité. Electron = écosystème plus mature |
| Frontend | Svelte ou React | Svelte = bundle léger, React = plus de ressources/libs |
| Base de données | SQLite | Locale, performante, pas de serveur, portable |
| Charts | Recharts ou Chart.js | Intégration React facile, bonne personnalisation |
| Parsing CSV | PapaParse | Robuste, gère les edge cases (quotes, encoding) |
Architecture des fichiers
simpl-resultat/
├── src/
│ ├── main/ # Process principal (Electron/Tauri)
│ │ ├── database/
│ │ │ ├── schema.sql
│ │ │ ├── migrations/
│ │ │ └── repository/
│ │ ├── services/
│ │ │ ├── import.service.ts
│ │ │ ├── category.service.ts
│ │ │ ├── transaction.service.ts
│ │ │ ├── budget.service.ts
│ │ │ ├── adjustment.service.ts
│ │ │ └── report.service.ts
│ │ └── ipc/ # Communication main/renderer
│ │
│ ├── renderer/ # Interface utilisateur
│ │ ├── components/
│ │ ├── pages/
│ │ ├── stores/
│ │ └── utils/
│ │
│ └── shared/ # Types partagés
│ ├── types/
│ └── constants/
│
├── data/ # Données utilisateur (gitignored)
│ ├── database.sqlite
│ └── backups/
│
├── imports/ # Dossiers d'import configurés
│ ├── carte_credit/
│ ├── compte_cheque/
│ └── epargne/
│
└── config/
├── categories_default.json
└── import_profiles.json
Spécifications fonctionnelles détaillées
1. Module d'importation
1.1 Configuration des sources
interface ImportSource {
id: string;
name: string; // Ex: "Carte de crédit Visa"
directoryPath: string; // Chemin du répertoire surveillé
filePattern: string; // Ex: "export_cc*.csv"
encoding: 'utf-8' | 'iso-8859-1' | 'windows-1252';
delimiter: ',' | ';' | '\t';
hasHeader: boolean;
dateFormat: string; // Ex: "YYYY-MM-DD", "DD/MM/YYYY"
columnMapping: ColumnMapping;
amountRule: AmountRule;
skipLines?: number; // Lignes à ignorer en début de fichier
lastImportDate?: Date;
}
interface ColumnMapping {
date: number | string; // Index ou nom de colonne
description: number | string;
transactionCode?: number | string;
debitAmount?: number | string;
creditAmount?: number | string;
amount?: number | string; // Si montant unique
}
interface AmountRule {
type: 'separate' | 'combined';
// Si combined:
signConvention?: 'negative_is_debit' | 'negative_is_credit';
}
1.2 Processus d'importation
[Sélection source] → [Détection fichiers] → [Preview données]
↓ ↓
[Validation mapping] ← [Ajustement mapping si nécessaire]
↓
[Détection doublons] → [Confirmation import]
↓
[Import en base] → [Catégorisation auto] → [Rapport d'import]
1.3 Gestion des doublons
Critères de détection de doublon:
- Date + Description + Montant identiques
- Ou: Hash SHA256 de la ligne complète déjà présent
Options utilisateur:
- Ignorer les doublons (défaut)
- Forcer l'import
- Demander confirmation pour chaque
2. Module de catégorisation
2.1 Structure hiérarchique
interface Category {
id: string;
name: string;
type: 'recurring' | 'occasional' | 'special' | 'transfer' | 'income';
parentId?: string; // Pour sous-catégories
color?: string; // Pour les graphiques
icon?: string;
isSystem: boolean; // Non supprimable si true
}
interface Supplier {
id: string;
name: string;
categoryId: string;
keywords: string[]; // Mots-clés pour matching
priority: number; // En cas de conflit de matching
}
2.2 Algorithme de matching
Pour chaque transaction non catégorisée:
1. Normaliser la description (minuscules, sans accents, trim)
2. Pour chaque fournisseur (trié par priorité DESC):
a. Pour chaque mot-clé du fournisseur:
- Si description.contains(keyword) → MATCH
b. Si MATCH → Assigner catégorie du fournisseur, STOP
3. Si aucun match → Catégorie "Autres dépenses"
Améliorations suggérées:
- Support des expressions régulières
- Matching par montant (ex: "Netflix" toujours ~15.99$)
- Apprentissage des assignations manuelles
2.3 Interface de gestion des non-matchés
| Élément | Description |
|---|---|
| Liste des transactions | Triées par date, regroupables par description similaire |
| Recherche rapide | Filtrer par texte dans description |
| Action rapide | Créer mot-clé à partir de la sélection |
| Action groupée | Catégoriser plusieurs transactions similaires en un clic |
3. Module d'ajustements
3.1 Types d'écritures
interface Adjustment {
id: string;
originalTransactionId: string;
adjustmentDate: Date; // Fin du mois concerné
effectiveMonth: string; // Format "YYYY-MM"
entries: AdjustmentEntry[];
reason?: string;
createdAt: Date;
}
interface AdjustmentEntry {
categoryId: string;
amount: number; // Positif = augmente cette catégorie
}
3.2 Exemple d'usage
Transaction Amazon de 150$:
- Original: 100% → "Achats divers"
- Ajustement:
- -150$ "Achats divers"
- +80$ "Sport"
- +70$ "Musique"
4. Module Budget
4.1 Structure
interface BudgetEntry {
id: string;
categoryId: string;
month: string; // "YYYY-MM"
plannedAmount: number;
notes?: string;
}
interface BudgetTemplate {
id: string;
name: string;
entries: Omit<BudgetEntry, 'id' | 'month'>[];
}
4.2 Fonctionnalités
- Templates: Créer des budgets types réutilisables
- Copie: Dupliquer le budget d'un mois vers un autre
- Alertes: Notification visuelle si dépassement (>80%, >100%)
- Carry-over: Option pour reporter le surplus/déficit
5. Module Reporting
5.1 Rapports disponibles
| Rapport | Description | Visualisation |
|---|---|---|
| Suivi mensuel 12 mois | Évolution des dépenses par catégorie | Graphique barres empilées |
| Mois courant vs précédent | Comparaison détaillée | Tableau + barres horizontales |
| Cumulatif YTD | Année courante vs précédente | Graphique ligne + aires |
| Répartition | Distribution par catégorie | Pie chart / Treemap |
| Tendances | Évolution sur période personnalisée | Graphique ligne |
| Budget vs Réel | Écarts budgétaires | Barres avec indicateurs |
5.2 Filtres et dimensions
- Par source (compte)
- Par catégorie / sous-catégorie
- Par type de dépense
- Par période personnalisée
- Inclusion/exclusion des transferts
Modèle de données
Schéma SQL
-- Sources d'import
CREATE TABLE import_sources (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
directory_path TEXT NOT NULL,
file_pattern TEXT DEFAULT '*.csv',
encoding TEXT DEFAULT 'utf-8',
delimiter TEXT DEFAULT ',',
has_header INTEGER DEFAULT 1,
date_format TEXT NOT NULL,
column_mapping TEXT NOT NULL, -- JSON
amount_rule TEXT NOT NULL, -- JSON
skip_lines INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Fichiers importés (pour éviter les doublons)
CREATE TABLE imported_files (
id TEXT PRIMARY KEY,
source_id TEXT NOT NULL REFERENCES import_sources(id),
filename TEXT NOT NULL,
file_hash TEXT NOT NULL, -- SHA256
row_count INTEGER NOT NULL,
imported_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(source_id, file_hash)
);
-- Catégories
CREATE TABLE categories (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
type TEXT NOT NULL CHECK(type IN ('recurring','occasional','special','transfer','income')),
parent_id TEXT REFERENCES categories(id),
color TEXT,
icon TEXT,
is_system INTEGER DEFAULT 0,
display_order INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Fournisseurs
CREATE TABLE suppliers (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
category_id TEXT NOT NULL REFERENCES categories(id),
priority INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Mots-clés
CREATE TABLE keywords (
id TEXT PRIMARY KEY,
supplier_id TEXT NOT NULL REFERENCES suppliers(id) ON DELETE CASCADE,
keyword TEXT NOT NULL,
is_regex INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(supplier_id, keyword)
);
-- Transactions
CREATE TABLE transactions (
id TEXT PRIMARY KEY,
source_id TEXT NOT NULL REFERENCES import_sources(id),
imported_file_id TEXT NOT NULL REFERENCES imported_files(id),
transaction_date DATE NOT NULL,
description TEXT NOT NULL,
transaction_code TEXT,
debit_amount REAL DEFAULT 0, -- Note: SQLite REAL = 8 bytes IEEE float
credit_amount REAL DEFAULT 0, -- Pour précision crypto, stocker en INTEGER (satoshis/cents)
-- Alternative crypto-safe:
-- debit_amount_raw INTEGER DEFAULT 0, -- Montant × 10^8
-- credit_amount_raw INTEGER DEFAULT 0,
currency TEXT DEFAULT 'CAD',
supplier_id TEXT REFERENCES suppliers(id),
category_id TEXT REFERENCES categories(id),
is_manually_categorized INTEGER DEFAULT 0,
raw_data TEXT, -- Ligne CSV originale
row_hash TEXT NOT NULL, -- Pour détection doublons
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(row_hash)
);
-- Index pour performance
CREATE INDEX idx_transactions_date ON transactions(transaction_date);
CREATE INDEX idx_transactions_category ON transactions(category_id);
CREATE INDEX idx_transactions_month ON transactions(strftime('%Y-%m', transaction_date));
-- Écritures d'ajustement
CREATE TABLE adjustments (
id TEXT PRIMARY KEY,
original_transaction_id TEXT NOT NULL REFERENCES transactions(id),
adjustment_date DATE NOT NULL,
effective_month TEXT NOT NULL, -- Format YYYY-MM
reason TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE adjustment_entries (
id TEXT PRIMARY KEY,
adjustment_id TEXT NOT NULL REFERENCES adjustments(id) ON DELETE CASCADE,
category_id TEXT NOT NULL REFERENCES categories(id),
amount REAL NOT NULL
);
-- Budget
CREATE TABLE budget_entries (
id TEXT PRIMARY KEY,
category_id TEXT NOT NULL REFERENCES categories(id),
month TEXT NOT NULL, -- Format YYYY-MM
planned_amount REAL NOT NULL,
notes TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(category_id, month)
);
-- Templates de budget
CREATE TABLE budget_templates (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE budget_template_entries (
id TEXT PRIMARY KEY,
template_id TEXT NOT NULL REFERENCES budget_templates(id) ON DELETE CASCADE,
category_id TEXT NOT NULL REFERENCES categories(id),
planned_amount REAL NOT NULL
);
-- Préférences utilisateur
CREATE TABLE user_preferences (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Valeurs par défaut suggérées:
-- 'language' : 'fr' | 'en'
-- 'fiscal_year_start_month' : '1' (janvier par défaut, '4' pour avril, etc.)
-- 'fiscal_year_start_day' : '1'
-- 'default_currency' : 'CAD'
-- 'date_format' : 'DD/MM/YYYY' | 'YYYY-MM-DD' | 'MM/DD/YYYY'
-- 'decimal_separator' : ',' | '.'
-- 'theme' : 'light' | 'dark' | 'system'
Interface utilisateur
Navigation principale
┌─────────────────────────────────────────────────────────────┐
│ [Logo] Simpl-Résultat [⚙️ Paramètres] [?] │
├─────────────────────────────────────────────────────────────┤
│ │
│ 📊 Tableau de bord │
│ 📥 Importation │
│ 💳 Transactions │
│ 🏷️ Catégories │
│ 📝 Ajustements │
│ 💰 Budget │
│ 📈 Rapports │
│ │
├─────────────────────────────────────────────────────────────┤
│ [Version Free/Premium] │
└─────────────────────────────────────────────────────────────┘
Écrans principaux
Tableau de bord
- Résumé du mois courant
- Graphique dépenses vs budget
- Alertes (transactions non catégorisées, dépassements)
- Raccourcis actions fréquentes
Importation
- Liste des sources configurées
- Bouton "Nouvelle source"
- Pour chaque source: dernière import, fichiers en attente
- Assistant d'import avec preview
Transactions
- Liste paginée avec recherche/filtres
- Colonnes: Date, Description, Catégorie, Débit, Crédit, Source
- Actions: Modifier catégorie, Voir détails, Créer ajustement
Points faibles identifiés et recommandations
🔴 Points critiques
| # | Problème | Impact | Recommandation |
|---|---|---|---|
| 1 | Pas de gestion des devises | Impossible de consolider des comptes multi-devises | Ajouter un champ currency aux sources et transactions, prévoir conversion |
| 2 | Pas de backup/export | Perte de données si crash | Implémenter export SQLite + JSON, backup auto |
| 3 | Pas de gestion des revenus | Budget incomplet sans les entrées | Ajouter type "income" aux catégories, rapport balance |
| 4 | Règle montant unique simpliste | Banques ont des formats variés | Support regex, formules personnalisées |
| 5 | Pas de gestion de l'encodage | CSV corrompus si mauvais encoding | Détection auto (chardet) + sélection manuelle |
🟠 Points importants
| # | Problème | Impact | Recommandation |
|---|---|---|---|
| 6 | Pas de détection de doublons explicite | Imports multiples du même fichier | Hash des lignes, table des fichiers importés |
| 7 | Matching mots-clés basique | Faux positifs/négatifs fréquents | Priorités, regex, matching partiel pondéré |
| 8 | Pas de gestion des dates de valeur | Date opération ≠ date valeur | Ajouter champ optionnel value_date |
| 9 | Forfait freemium flou | Confusion utilisateur | Détailler précisément les limitations |
| 10 | Pas de recherche/filtre | Difficile de retrouver une transaction | Recherche full-text, filtres avancés |
🟡 Améliorations suggérées
| # | Amélioration | Bénéfice |
|---|---|---|
| 11 | Import récurrent automatisé | Surveiller les dossiers, import auto |
| 12 | Règles de catégorisation avancées | Combinaison montant + description + source |
| 13 | Mode sombre | Confort utilisateur |
| 14 | Raccourcis clavier | Productivité power users |
| 15 | Export PDF des rapports | Partage/archivage facile |
| 16 | Graphiques interactifs | Drill-down par clic |
| 17 | Objectifs d'épargne | Fonctionnalité motivante |
| 18 | Tags personnalisés | Catégorisation orthogonale (ex: "Vacances 2024") |
| 19 | Notes sur transactions | Contexte personnel |
| 20 | Historique des modifications | Audit trail |
🔵 Décisions prises
| Question | Décision | Notes |
|---|---|---|
| Multilinguisme | ✅ Français + Anglais | i18n dès le départ |
| Période fiscale | ✅ Configurable | Année calendaire par défaut, option personnalisée |
| Précision décimale | ✅ 8 décimales | Support crypto-friendly |
| Pièces jointes | ❌ V2 | Hors scope pour MVP |
| Licence freemium | ✅ Code d'activation + serveur | Validation en ligne |
🔵 Questions encore ouvertes
- Synchronisation multi-appareils? Local seulement ou option sync future?
- Comptes joints? Gestion multi-utilisateurs sur mêmes données?
- API/Plugins? Extensibilité future?
- Format date système? Respect des préférences OS ou configurable?
- Transactions programmées? Dépenses récurrentes futures?
Considérations techniques
Internationalisation (i18n)
Structure recommandée pour le support FR/EN :
src/
└── locales/
├── fr.json
├── en.json
└── index.ts
// fr.json
{
"app.name": "Simpl-Résultat",
"nav.dashboard": "Tableau de bord",
"nav.import": "Importation",
"nav.transactions": "Transactions",
"transaction.date": "Date",
"transaction.description": "Description",
"transaction.debit": "Débit",
"transaction.credit": "Crédit",
"category.other": "Autres dépenses",
"budget.planned": "Prévu",
"budget.actual": "Réel",
"budget.remaining": "Restant"
}
// en.json
{
"app.name": "Simpl-Result",
"nav.dashboard": "Dashboard",
"nav.import": "Import",
"nav.transactions": "Transactions",
"transaction.date": "Date",
"transaction.description": "Description",
"transaction.debit": "Debit",
"transaction.credit": "Credit",
"category.other": "Other expenses",
"budget.planned": "Planned",
"budget.actual": "Actual",
"budget.remaining": "Remaining"
}
Librairies recommandées :
- Svelte : svelte-i18n
- React : react-i18next
- Python/Flet : gettext ou babel
Architecture de licence Freemium
┌─────────────────┐ ┌──────────────────────┐
│ Application │────▶│ Serveur licence │
│ (locale) │◀────│ (ton serveur) │
└─────────────────┘ └──────────────────────┘
│ │
│ POST /api/activate │
│ {license_key, hw_id} │
│ │
│ Response: │
│ {valid, features, │
│ expires_at} │
│ │
▼ ▼
┌─────────────────┐ ┌──────────────────────┐
│ License cache │ │ Base de données │
│ (local, chiffré)│ │ - licenses │
│ │ │ - activations │
└─────────────────┘ └──────────────────────┘
Schéma serveur (PostgreSQL/SQLite)
-- Licences générées à l'achat
CREATE TABLE licenses (
id UUID PRIMARY KEY,
license_key VARCHAR(32) UNIQUE NOT NULL, -- Ex: XXXX-XXXX-XXXX-XXXX
email VARCHAR(255),
plan VARCHAR(20) NOT NULL, -- 'premium', 'lifetime'
max_activations INT DEFAULT 3,
created_at TIMESTAMP DEFAULT NOW(),
expires_at TIMESTAMP, -- NULL = lifetime
is_revoked BOOLEAN DEFAULT FALSE
);
-- Activations (machines)
CREATE TABLE activations (
id UUID PRIMARY KEY,
license_id UUID REFERENCES licenses(id),
hardware_id VARCHAR(64) NOT NULL, -- Hash unique de la machine
app_version VARCHAR(20),
os_info VARCHAR(100),
activated_at TIMESTAMP DEFAULT NOW(),
last_seen_at TIMESTAMP DEFAULT NOW(),
is_active BOOLEAN DEFAULT TRUE,
UNIQUE(license_id, hardware_id)
);
Génération du Hardware ID
// Côté client - génère un ID unique par machine
import { machineIdSync } from 'node-machine-id';
import * as crypto from 'crypto';
function getHardwareId(): string {
const machineId = machineIdSync();
// Hash pour anonymisation
return crypto
.createHash('sha256')
.update(machineId + 'simpl-resultat-salt')
.digest('hex')
.substring(0, 32);
}
API Serveur (exemple Node/Express)
// POST /api/license/activate
app.post('/api/license/activate', async (req, res) => {
const { licenseKey, hardwareId, appVersion, osInfo } = req.body;
// 1. Vérifier la licence
const license = await db.licenses.findByKey(licenseKey);
if (!license || license.is_revoked) {
return res.status(400).json({ error: 'invalid_license' });
}
// 2. Vérifier expiration
if (license.expires_at && license.expires_at < new Date()) {
return res.status(400).json({ error: 'license_expired' });
}
// 3. Vérifier nombre d'activations
const activeCount = await db.activations.countActive(license.id);
const existing = await db.activations.findByHardware(license.id, hardwareId);
if (!existing && activeCount >= license.max_activations) {
return res.status(400).json({
error: 'max_activations_reached',
max: license.max_activations
});
}
// 4. Créer ou mettre à jour l'activation
await db.activations.upsert({
licenseId: license.id,
hardwareId,
appVersion,
osInfo,
lastSeenAt: new Date()
});
// 5. Retourner le token de session
const token = generateSignedToken({
licenseId: license.id,
plan: license.plan,
features: getPlanFeatures(license.plan),
expiresAt: license.expires_at
});
res.json({
valid: true,
token,
plan: license.plan,
features: getPlanFeatures(license.plan)
});
});
function getPlanFeatures(plan: string): string[] {
const features = {
free: ['import', 'basic_categories', 'monthly_report'],
premium: ['import', 'custom_categories', 'adjustments',
'all_reports', 'export_pdf', 'budget_templates']
};
return features[plan] || features.free;
}
Côté client - Vérification
class LicenseManager {
private cache: LicenseCache;
async checkLicense(): Promise<LicenseStatus> {
// 1. Vérifier le cache local d'abord
const cached = await this.cache.get();
if (cached && !this.isExpired(cached) && !this.needsRevalidation(cached)) {
return cached;
}
// 2. Sinon, valider en ligne
try {
const response = await this.validateOnline();
await this.cache.set(response);
return response;
} catch (e) {
// 3. Mode hors-ligne : grace period de 7 jours
if (cached && this.withinGracePeriod(cached)) {
return { ...cached, offline: true };
}
return { valid: false, plan: 'free' };
}
}
private needsRevalidation(cached: LicenseCache): boolean {
// Revalider toutes les 72h
const lastCheck = new Date(cached.lastValidated);
const hoursSince = (Date.now() - lastCheck.getTime()) / (1000 * 60 * 60);
return hoursSince > 72;
}
private withinGracePeriod(cached: LicenseCache): boolean {
const lastCheck = new Date(cached.lastValidated);
const daysSince = (Date.now() - lastCheck.getTime()) / (1000 * 60 * 60 * 24);
return daysSince <= 7;
}
}
Sécurité
| Risque | Mitigation |
|---|---|
| Clé partagée | Limite d'activations (3 machines) |
| Crack local | Fonctionnalités premium côté validation, pas juste UI |
| Man-in-the-middle | HTTPS + signature des tokens |
| Reverse engineering | Obfuscation (limité mais aide) |
| Serveur down | Grace period 7 jours + mode dégradé |
Note réaliste : Aucune protection n'est parfaite pour une app desktop. L'objectif est de rendre le crack plus difficile que l'achat pour l'utilisateur moyen, pas d'être inviolable.
Précision décimale (8 décimales)
Pour supporter les cryptomonnaies (Bitcoin = 8 décimales, certains tokens = plus), deux approches :
Approche 1 : Stockage INTEGER (recommandé)
// Stocker les montants en plus petite unité (satoshis, cents × 10^6)
const DECIMAL_PLACES = 8;
const MULTIPLIER = 10 ** DECIMAL_PLACES; // 100_000_000
function toStorageAmount(displayAmount: number): bigint {
return BigInt(Math.round(displayAmount * MULTIPLIER));
}
function toDisplayAmount(storageAmount: bigint): number {
return Number(storageAmount) / MULTIPLIER;
}
// Exemple
const btcAmount = 0.00123456;
const stored = toStorageAmount(btcAmount); // 123456n
const display = toDisplayAmount(stored); // 0.00123456
Avantages :
- Pas de floating point errors (0.1 + 0.2 ≠ 0.3)
- Calculs exacts
- SQLite INTEGER = 64 bits signés = jusqu'à ~92 milliards en unités de base
Approche 2 : TEXT avec Decimal.js
import Decimal from 'decimal.js';
Decimal.set({ precision: 20 });
const amount = new Decimal('0.12345678');
const total = amount.plus('0.00000001'); // Exact: 0.12345679
// Stockage en TEXT dans SQLite
db.run('INSERT INTO transactions (amount_text) VALUES (?)',
[amount.toString()]);
Formatage d'affichage
function formatAmount(amount: number, currency: string): string {
const decimals = currency === 'BTC' ? 8
: currency === 'ETH' ? 8
: 2; // CAD, USD, EUR
return new Intl.NumberFormat('fr-CA', {
minimumFractionDigits: decimals > 2 ? 2 : decimals,
maximumFractionDigits: decimals,
}).format(amount);
}
// 0.00123456 BTC → "0,00123456"
// 1234.50 CAD → "1 234,50"
Performance
// Indexes recommandés pour queries fréquentes
const CRITICAL_QUERIES = {
// Dashboard: dépenses du mois courant par catégorie
monthlyByCategory: `
SELECT category_id, SUM(debit_amount) as total
FROM transactions
WHERE strftime('%Y-%m', transaction_date) = ?
GROUP BY category_id
`,
// Budget vs Réel
budgetComparison: `
SELECT
b.category_id,
b.planned_amount,
COALESCE(SUM(t.debit_amount), 0) as actual_amount
FROM budget_entries b
LEFT JOIN transactions t
ON t.category_id = b.category_id
AND strftime('%Y-%m', t.transaction_date) = b.month
WHERE b.month = ?
GROUP BY b.category_id
`
};
Sécurité
| Risque | Mitigation |
|---|---|
| Données sensibles en clair | Chiffrement SQLite optionnel (SQLCipher) |
| Injection CSV | Sanitization des imports |
| Accès fichiers arbitraires | Sandbox des chemins d'import |
Tests
tests/
├── unit/
│ ├── services/
│ │ ├── import.service.test.ts
│ │ └── categorization.service.test.ts
│ └── utils/
│ └── csv-parser.test.ts
├── integration/
│ ├── database.test.ts
│ └── import-flow.test.ts
└── e2e/
├── import-wizard.test.ts
└── reporting.test.ts
Plan de développement suggéré
Phase 1 - MVP (4-6 semaines)
Objectif: Import et visualisation basique
- Setup projet (Tauri + React/Svelte)
- Schéma BDD SQLite
- Import CSV basique (1 format fixe)
- Liste transactions
- Catégories par défaut
- Matching mots-clés simple
- Rapport mensuel simple (tableau)
Phase 2 - Core (4-6 semaines)
Objectif: Fonctionnalités complètes freemium
- Configuration sources multiples
- Assistant mapping colonnes
- Gestion catégories/fournisseurs
- Interface transactions non-matchées
- Budget mensuel
- 3 rapports de base
- Graphiques simples
Phase 3 - Premium (4-6 semaines)
Objectif: Différenciation payant
- Écritures d'ajustement
- Personnalisation catégories
- Rapports avancés
- Export PDF
- Graphiques interactifs
- Templates budget
Phase 4 - Polish (2-4 semaines)
Objectif: Production-ready
- Tests complets
- Installateurs (Windows, macOS, Linux)
- Documentation utilisateur
- Système de licence
- Backup/restore
- Performance tuning
Annexe: Catégories par défaut suggérées
{
"categories": [
{
"name": "Logement",
"type": "recurring",
"subcategories": ["Loyer/Hypothèque", "Électricité", "Chauffage", "Internet", "Assurance habitation", "Taxes"]
},
{
"name": "Transport",
"type": "recurring",
"subcategories": ["Essence", "Transport en commun", "Assurance auto", "Entretien véhicule", "Stationnement"]
},
{
"name": "Alimentation",
"type": "recurring",
"subcategories": ["Épicerie", "Restaurant", "Livraison", "Café"]
},
{
"name": "Santé",
"type": "occasional",
"subcategories": ["Pharmacie", "Médecin", "Dentiste", "Optométriste", "Assurance santé"]
},
{
"name": "Loisirs",
"type": "occasional",
"subcategories": ["Sport", "Culture", "Streaming", "Jeux", "Hobbies"]
},
{
"name": "Shopping",
"type": "occasional",
"subcategories": ["Vêtements", "Électronique", "Maison", "Cadeaux"]
},
{
"name": "Services",
"type": "recurring",
"subcategories": ["Téléphone", "Abonnements", "Banque"]
},
{
"name": "Transferts",
"type": "transfer",
"subcategories": ["Entre comptes", "Épargne", "Investissement"]
},
{
"name": "Revenus",
"type": "income",
"subcategories": ["Salaire", "Freelance", "Remboursements", "Autres revenus"]
},
{
"name": "Autres dépenses",
"type": "occasional",
"subcategories": []
}
]
}