From b0206ae56b6b1777a6f83ed92c63263f61728987 Mon Sep 17 00:00:00 2001 From: le king fu Date: Tue, 6 Oct 2026 20:35:00 -0400 Subject: [PATCH] fix(profiles): catch secondary profiles up on missed migrations (#390) tauri-plugin-sql migrates only the database registered with add_migrations, the default profile's. A secondary profile kept the consolidated schema of the version that created it and never received a migration afterwards. prepare_profile_db runs before every Database.load: - secondary profiles catch up with the same migrations() list (pinned SHA-384), history kept in _sqlx_migrations; a database without history gets a floor inferred from its schema (the seven released creation states are test fixtures), so nothing at or below it is ever replayed; - a VACUUM INTO copy is taken before the first write, synced, then renamed; - each version runs in its own transaction with foreign keys on; one that fails on a foreign key is retried once without them and its orphans are set to NULL where the key declares ON DELETE SET NULL; - every profile gets its TEXT manual-categorization flags repaired; - failures are logged with file names only and the profile still opens. New profiles are born with their full history (get_new_profile_init_sql). The plugin keeps databases under app_config_dir(), not app_data_dir(): the same folder on Windows and macOS, not on Linux. repair_migrations, delete_profile_db and compute_account_return looked in the wrong folder there. profile_db_path resolves the plugin's folder and validates the file name; deleting a profile now also removes its SQLite side files and catch-up copies. Co-Authored-By: Claude Opus 5.5 --- src-tauri/Cargo.toml | 3 + src-tauri/src/commands/balance_commands.rs | 17 +- src-tauri/src/commands/profile_commands.rs | 201 ++- .../src/database/consolidated_schema.sql | 17 +- .../fixtures/consolidated_v0.10.0.sql | 824 ++++++++++ .../database/fixtures/consolidated_v0.3.0.sql | 184 +++ .../database/fixtures/consolidated_v0.8.4.sql | 657 ++++++++ .../database/fixtures/consolidated_v0.9.0.sql | 747 +++++++++ .../database/fixtures/consolidated_v0.9.1.sql | 764 +++++++++ .../src/database/fixtures/seed_v0.3.0.sql | 232 +++ .../src/database/fixtures/seed_v0.4.0.sql | 241 +++ src-tauri/src/database/mod.rs | 6 + src-tauri/src/database/profile_db.rs | 1392 +++++++++++++++++ src-tauri/src/lib.rs | 29 +- src/services/db.test.ts | 121 +- src/services/db.ts | 69 + 16 files changed, 5447 insertions(+), 57 deletions(-) create mode 100644 src-tauri/src/database/fixtures/consolidated_v0.10.0.sql create mode 100644 src-tauri/src/database/fixtures/consolidated_v0.3.0.sql create mode 100644 src-tauri/src/database/fixtures/consolidated_v0.8.4.sql create mode 100644 src-tauri/src/database/fixtures/consolidated_v0.9.0.sql create mode 100644 src-tauri/src/database/fixtures/consolidated_v0.9.1.sql create mode 100644 src-tauri/src/database/fixtures/seed_v0.3.0.sql create mode 100644 src-tauri/src/database/fixtures/seed_v0.4.0.sql create mode 100644 src-tauri/src/database/profile_db.rs diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 797077b..3383a26 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -66,6 +66,9 @@ hmac = "0.12" ed25519-dalek = { version = "2", features = ["pkcs8", "rand_core"] } # HTTP mock server for balance_commands fetch_price tests (Issue #155). mockito = "1.6" +# MockRuntime for the profile database folder test (#390). The `test` +# feature adds no crate (`test = []` in tauri's manifest). +tauri = { version = "2", features = ["test"] } [features] # Dev-only escape hatch: when enabled, the SR_DEV_EDITION env var forces the diff --git a/src-tauri/src/commands/balance_commands.rs b/src-tauri/src/commands/balance_commands.rs index 5a75140..26ae3ea 100644 --- a/src-tauri/src/commands/balance_commands.rs +++ b/src-tauri/src/commands/balance_commands.rs @@ -9,8 +9,9 @@ //! `Authorization`, `Accept`, and `User-Agent` headers. //! //! Database access pattern: -//! - All reads use `rusqlite::Connection::open(app_data_dir / db_filename)`, -//! matching the existing `repair_migrations` helper in `profile_commands.rs`. +//! - All reads open the file `profile_db_path` resolves (`profile_commands.rs`): +//! the folder where tauri-plugin-sql keeps the databases, which is NOT +//! `app_data_dir` on Linux (#390). //! - The frontend passes `db_filename` (the active profile DB), exactly //! like it does for `repair_migrations` and `delete_profile_db`. Keeps //! the active-profile resolution where it already lives (in TS) and @@ -258,16 +259,10 @@ pub fn compute_account_return( let start_date = parse_iso_date(&period_start, "period_start")?; let end_date = parse_iso_date(&period_end, "period_end")?; - let app_dir = app - .path() - .app_data_dir() - .map_err(|e| format!("Cannot get app data dir: {}", e))?; - let db_path = app_dir.join(&db_filename); + let db_path = super::profile_commands::profile_db_path(&app, &db_filename)?; if !db_path.exists() { - return Err(format!( - "Profile database not found: {}", - db_path.display() - )); + // The file name only: the folder holds the OS user name. + return Err(format!("Profile database not found: {}", db_filename)); } let conn = Connection::open(&db_path) diff --git a/src-tauri/src/commands/profile_commands.rs b/src-tauri/src/commands/profile_commands.rs index f955ae6..49f99a4 100644 --- a/src-tauri/src/commands/profile_commands.rs +++ b/src-tauri/src/commands/profile_commands.rs @@ -3,10 +3,12 @@ use rand::RngCore; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256, Sha384}; use std::fs; +use std::path::{Path, PathBuf}; use subtle::ConstantTimeEq; use tauri::Manager; use crate::database; +use crate::database::profile_db; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Profile { @@ -32,6 +34,49 @@ fn get_profiles_path(app: &tauri::AppHandle) -> Result` under `app_config_dir()` (tauri-plugin-sql +/// 2.3.2, `wrapper.rs`: `DbPool::connect` then `path_mapper`), not under +/// `app_data_dir()`, where `profiles.json` and the licence files live. Both +/// are the same folder on Windows and macOS, not on Linux (`~/.config/` +/// and `~/.local/share/`): until #390, every command that looked for a +/// profile database under `app_data_dir()` found nothing on Linux. +pub(crate) fn profile_db_dir(app: &tauri::AppHandle) -> Result { + app.path() + .app_config_dir() + .map_err(|e| format!("Cannot get app config dir: {}", e)) +} + +/// Path of a profile database, from the bare file name the webview passes. +pub(crate) fn profile_db_path( + app: &tauri::AppHandle, + db_filename: &str, +) -> Result { + profile_db_path_in(&profile_db_dir(app)?, db_filename) +} + +/// Accept only the two shapes the app ever gives a profile database: +/// `simpl_resultat.db` and `profile_.db` (ProfileContext, since v0.3.0). +/// `Path::join` with an absolute value would replace the folder, and these +/// commands write and delete. +pub(crate) fn profile_db_path_in(dir: &Path, db_filename: &str) -> Result { + if !is_profile_db_filename(db_filename) { + return Err("invalid_profile_db_filename".to_string()); + } + Ok(dir.join(db_filename)) +} + +fn is_profile_db_filename(name: &str) -> bool { + if name == database::DEFAULT_PROFILE_DB { + return true; + } + match name.strip_prefix("profile_").and_then(|rest| rest.strip_suffix(".db")) { + Some(id) => !id.is_empty() && id.bytes().all(|b| b.is_ascii_hexdigit()), + None => false, + } +} + fn make_default_config() -> ProfilesConfig { let now = chrono_now(); let default_id = "default".to_string(); @@ -92,35 +137,63 @@ pub fn save_profiles(app: tauri::AppHandle, config: ProfilesConfig) -> Result<() fs::write(&path, json).map_err(|e| format!("Cannot write profiles.json: {}", e)) } +/// Delete a secondary profile's database, with its SQLite side files and the +/// copies a catch-up took (#390): the guide promises that deleting a profile +/// deletes all its data. #[tauri::command] pub fn delete_profile_db(app: tauri::AppHandle, db_filename: String) -> Result<(), String> { - if db_filename == "simpl_resultat.db" { + if db_filename == database::DEFAULT_PROFILE_DB { return Err("Cannot delete the default profile database".to_string()); } + let db_path = profile_db_path(&app, &db_filename)?; + delete_profile_files(&db_path, &db_filename) +} - let app_dir = app - .path() - .app_data_dir() - .map_err(|e| format!("Cannot get app data dir: {}", e))?; - let db_path = app_dir.join(&db_filename); - - if db_path.exists() { - fs::remove_file(&db_path) - .map_err(|e| format!("Cannot delete database file: {}", e))?; +fn delete_profile_files(db_path: &Path, db_filename: &str) -> Result<(), String> { + for suffix in ["", "-wal", "-shm", "-journal"] { + let path = db_path.with_file_name(format!("{}{}", db_filename, suffix)); + if path.exists() { + fs::remove_file(&path).map_err(|e| format!("Cannot delete database file: {}", e))?; + } + } + if let Some(dir) = db_path.parent() { + profile_db::remove_backups( + &dir.join(profile_db::MIGRATION_BACKUP_DIR), + db_filename.trim_end_matches(".db"), + )?; } - Ok(()) } +/// SQL that creates a secondary profile, run by the webview through the +/// plugin (`initializeNewProfileDb`). #[tauri::command] pub fn get_new_profile_init_sql() -> Result, String> { - // Brand-new profiles ship with the v1 IPC-aligned category taxonomy: the - // consolidated schema bakes the v1 seed (categories + keywords + the - // categories_schema_version='v1' preference) directly. The legacy v2 seed - // migration still runs first because tauri-plugin-sql applies every - // declared migration on `Database.load`; the consolidated script then - // deletes the v2 rows and re-inserts the v1 rows in the 1000+ id range. - Ok(vec![database::CONSOLIDATED_SCHEMA.to_string()]) + // The consolidated schema is the whole database: tauri-plugin-sql runs no + // migration for a `profile_.db`, only the default profile's URL is + // registered (lib.rs). It carries the v1 IPC-aligned taxonomy (categories, + // keywords, categories_schema_version='v1'). The second statement writes + // the complete migration history, so later versions reach the profile + // through `prepare_profile_db` without inferring anything (#390). + Ok(vec![ + database::CONSOLIDATED_SCHEMA.to_string(), + profile_db::history_sql(&crate::migrations()), + ]) +} + +/// Bring a profile database up to date and repair legacy values before the +/// webview opens it (#390, ADR 0021). Secondary profiles catch up on the +/// migrations they missed; every profile gets its text manual-categorization +/// flags repaired. Sync on purpose: Tauri runs sync commands on the main +/// thread, which serializes two opens of the same profile (React StrictMode +/// mounts twice in development). +#[tauri::command] +pub fn prepare_profile_db( + app: tauri::AppHandle, + db_filename: String, +) -> Result { + let db_path = profile_db_path(&app, &db_filename)?; + profile_db::prepare_at(&db_path, &db_filename, &crate::migrations()) } // Argon2id parameters for PIN hashing (same as export_import_commands.rs) @@ -225,11 +298,7 @@ fn hex_decode(hex: &str) -> Result, String> { /// of destructive migrations (e.g., migration 2 which DELETEs categories/keywords). #[tauri::command] pub fn repair_migrations(app: tauri::AppHandle, db_filename: String) -> Result { - let app_dir = app - .path() - .app_data_dir() - .map_err(|e| format!("Cannot get app data dir: {}", e))?; - let db_path = app_dir.join(&db_filename); + let db_path = profile_db_path(&app, &db_filename)?; if !db_path.exists() { return Ok(false); @@ -375,4 +444,90 @@ mod tests { let decoded = hex_decode(&encoded).unwrap(); assert_eq!(original, decoded); } + + // -- profile database location (#390) ------------------------------------ + + #[test] + fn profile_databases_live_where_the_sql_plugin_opens_them() { + // tauri-plugin-sql resolves `sqlite:` under app_config_dir(). + // On Linux (the CI platform) that differs from app_data_dir(), so + // going back to the data folder turns this red. + let app = tauri::test::mock_app(); + let path = profile_db_path(app.handle(), "profile_0a1b2c3d.db").unwrap(); + assert_eq!(path.parent(), Some(app.path().app_config_dir().unwrap().as_path())); + } + + #[test] + fn profile_db_path_accepts_the_two_shapes_the_app_uses() { + let dir = Path::new("/profiles"); + assert_eq!( + profile_db_path_in(dir, "simpl_resultat.db"), + Ok(dir.join("simpl_resultat.db")) + ); + assert_eq!( + profile_db_path_in(dir, "profile_0a1b2c3d.db"), + Ok(dir.join("profile_0a1b2c3d.db")) + ); + } + + #[test] + fn profile_db_path_refuses_anything_else() { + let dir = Path::new("/profiles"); + for name in [ + "", + "/etc/passwd", + "../simpl_resultat.db", + "profile_.db", + "profile_0a1b/../x.db", + "profile_0a1b2c3d.sqlite", + "profile_0a1b2c3d.db.bak", + "PROFILE_0a1b2c3d.db", + "profile_xyz.db", + "C:\\Users\\x\\profile_0a1b2c3d.db", + ] { + assert_eq!( + profile_db_path_in(dir, name), + Err("invalid_profile_db_filename".to_string()), + "{:?}", + name + ); + } + } + + #[test] + fn deleting_a_profile_removes_its_files_and_only_its_files() { + let dir = std::env::temp_dir().join(format!("simpl-resultat-delete-{}", std::process::id())); + let _ = fs::remove_dir_all(&dir); + let backups = dir.join(profile_db::MIGRATION_BACKUP_DIR); + fs::create_dir_all(&backups).unwrap(); + for name in [ + "profile_0a1b2c3d.db", + "profile_0a1b2c3d.db-wal", + "profile_0a1b2c3d.db-shm", + "profile_99999999.db", + ] { + fs::write(dir.join(name), b"x").unwrap(); + } + for name in ["profile_0a1b2c3d-before-v7.db", "profile_99999999-before-v17.db"] { + fs::write(backups.join(name), b"x").unwrap(); + } + + delete_profile_files(&dir.join("profile_0a1b2c3d.db"), "profile_0a1b2c3d.db").unwrap(); + + let mut left: Vec = fs::read_dir(&dir) + .unwrap() + .chain(fs::read_dir(&backups).unwrap()) + .map(|e| e.unwrap().file_name().into_string().unwrap()) + .collect(); + left.sort(); + assert_eq!( + left, + vec![ + "migration-backups".to_string(), + "profile_99999999-before-v17.db".to_string(), + "profile_99999999.db".to_string(), + ] + ); + let _ = fs::remove_dir_all(&dir); + } } diff --git a/src-tauri/src/database/consolidated_schema.sql b/src-tauri/src/database/consolidated_schema.sql index 44725bf..5266f87 100644 --- a/src-tauri/src/database/consolidated_schema.sql +++ b/src-tauri/src/database/consolidated_schema.sql @@ -15,12 +15,11 @@ CREATE TABLE IF NOT EXISTS import_sources ( column_mapping TEXT NOT NULL, skip_lines INTEGER NOT NULL DEFAULT 0, has_header INTEGER NOT NULL DEFAULT 1, - -- Full import format (migration v17). These four columns are INERT on the - -- production path: this script runs AFTER tauri-plugin-sql has applied every - -- migration and only uses CREATE TABLE IF NOT EXISTS, so a brand-new profile - -- actually receives them from v17. They are mirrored here to keep this file - -- the tested reference definition -- a parity test compares it against the - -- v1->v17 chain so the DEFAULT and CHECK of the two cannot drift apart. + -- Full import format (migration v17). A secondary profile receives these + -- four columns from this script, not from v17: tauri-plugin-sql runs no + -- migration on a `profile_.db` (#390, ADR 0021). A parity test compares + -- this definition with the v1->v17 chain so the DEFAULT and CHECK of the + -- two cannot drift apart. -- `amount_mode` admits 'absolute_indicator' from the start so the third mode -- can ship without another migration. `template_id` is a provenance tag -- only, never re-read as format: the eight format columns above are @@ -374,9 +373,9 @@ INSERT OR IGNORE INTO user_preferences (key, value) VALUES ('balance_starter_pro -- ============================================================================ -- Seed v1 — IPC Statistique Canada-aligned, 3 levels, Canada/Québec -- ---------------------------------------------------------------------------- --- Reset any pre-existing category data (possible when tauri-plugin-sql runs --- the historical v2 seed migration on this fresh DB before this script --- executes). Keywords/categories are wiped and re-inserted with the v1 IDs +-- Reset any pre-existing category data. Defensive only: tauri-plugin-sql runs +-- no migration on a secondary profile's database, so no v2 seed precedes this +-- script (#390). Keywords/categories are wiped and re-inserted with the v1 IDs -- (1000+ range) so existing references in migrations v3-v7 stay inert. -- ============================================================================ DELETE FROM keywords; diff --git a/src-tauri/src/database/fixtures/consolidated_v0.10.0.sql b/src-tauri/src/database/fixtures/consolidated_v0.10.0.sql new file mode 100644 index 0000000..17f5503 --- /dev/null +++ b/src-tauri/src/database/fixtures/consolidated_v0.10.0.sql @@ -0,0 +1,824 @@ +-- Consolidated schema for new profile databases +-- This file bakes in the base schema + all migrations (v3-v8) and pre-seeds +-- the v1 IPC-aligned category taxonomy so that brand-new profiles immediately +-- use the new standard. Existing profiles keep their v2 seed and are only +-- tagged via the migration (categories_schema_version = 'v2'). +-- Used ONLY for initializing new profile databases (not for the default profile). + +CREATE TABLE IF NOT EXISTS import_sources ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + description TEXT, + date_format TEXT NOT NULL DEFAULT '%d/%m/%Y', + delimiter TEXT NOT NULL DEFAULT ';', + encoding TEXT NOT NULL DEFAULT 'utf-8', + column_mapping TEXT NOT NULL, + skip_lines INTEGER NOT NULL DEFAULT 0, + has_header INTEGER NOT NULL DEFAULT 1, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS imported_files ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + source_id INTEGER NOT NULL, + filename TEXT NOT NULL, + file_hash TEXT NOT NULL, + import_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + row_count INTEGER NOT NULL DEFAULT 0, + status TEXT NOT NULL DEFAULT 'completed', + notes TEXT, + FOREIGN KEY (source_id) REFERENCES import_sources(id) ON DELETE CASCADE, + UNIQUE(source_id, filename) +); + +CREATE TABLE IF NOT EXISTS categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + parent_id INTEGER, + color TEXT, + icon TEXT, + type TEXT NOT NULL DEFAULT 'expense', + is_active INTEGER NOT NULL DEFAULT 1, + is_inputable INTEGER NOT NULL DEFAULT 1, + sort_order INTEGER NOT NULL DEFAULT 0, + i18n_key TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (parent_id) REFERENCES categories(id) ON DELETE SET NULL +); + +CREATE TABLE IF NOT EXISTS suppliers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + normalized_name TEXT NOT NULL, + category_id INTEGER, + is_active INTEGER NOT NULL DEFAULT 1, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL +); + +CREATE TABLE IF NOT EXISTS keywords ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + keyword TEXT NOT NULL, + category_id INTEGER NOT NULL, + supplier_id INTEGER, + priority INTEGER NOT NULL DEFAULT 0, + is_active INTEGER NOT NULL DEFAULT 1, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE, + FOREIGN KEY (supplier_id) REFERENCES suppliers(id) ON DELETE SET NULL, + UNIQUE(keyword, category_id) +); + +CREATE TABLE IF NOT EXISTS transactions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date DATE NOT NULL, + description TEXT NOT NULL, + amount REAL NOT NULL, + category_id INTEGER, + supplier_id INTEGER, + source_id INTEGER, + file_id INTEGER, + original_description TEXT, + notes TEXT, + is_manually_categorized INTEGER NOT NULL DEFAULT 0, + is_split INTEGER NOT NULL DEFAULT 0, + parent_transaction_id INTEGER, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL, + FOREIGN KEY (supplier_id) REFERENCES suppliers(id) ON DELETE SET NULL, + FOREIGN KEY (source_id) REFERENCES import_sources(id) ON DELETE SET NULL, + FOREIGN KEY (file_id) REFERENCES imported_files(id) ON DELETE SET NULL, + FOREIGN KEY (parent_transaction_id) REFERENCES transactions(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS adjustments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + description TEXT, + date DATE NOT NULL, + is_recurring INTEGER NOT NULL DEFAULT 0, + recurrence_rule TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS adjustment_entries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + adjustment_id INTEGER NOT NULL, + category_id INTEGER NOT NULL, + amount REAL NOT NULL, + description TEXT, + FOREIGN KEY (adjustment_id) REFERENCES adjustments(id) ON DELETE CASCADE, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS budget_entries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + category_id INTEGER NOT NULL, + year INTEGER NOT NULL, + month INTEGER NOT NULL, + amount REAL NOT NULL, + notes TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE, + UNIQUE(category_id, year, month) +); + +CREATE TABLE IF NOT EXISTS budget_templates ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + description TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS budget_template_entries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + template_id INTEGER NOT NULL, + category_id INTEGER NOT NULL, + amount REAL NOT NULL, + FOREIGN KEY (template_id) REFERENCES budget_templates(id) ON DELETE CASCADE, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE, + UNIQUE(template_id, category_id) +); + +CREATE TABLE IF NOT EXISTS import_config_templates ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + delimiter TEXT NOT NULL DEFAULT ';', + encoding TEXT NOT NULL DEFAULT 'utf-8', + date_format TEXT NOT NULL DEFAULT 'DD/MM/YYYY', + skip_lines INTEGER NOT NULL DEFAULT 0, + has_header INTEGER NOT NULL DEFAULT 1, + column_mapping TEXT NOT NULL, + amount_mode TEXT NOT NULL DEFAULT 'single', + sign_convention TEXT NOT NULL DEFAULT 'negative_expense', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS user_preferences ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Indexes +CREATE INDEX IF NOT EXISTS idx_transactions_date ON transactions(date); +CREATE INDEX IF NOT EXISTS idx_transactions_category ON transactions(category_id); +CREATE INDEX IF NOT EXISTS idx_transactions_supplier ON transactions(supplier_id); +CREATE INDEX IF NOT EXISTS idx_transactions_source ON transactions(source_id); +CREATE INDEX IF NOT EXISTS idx_transactions_file ON transactions(file_id); +CREATE INDEX IF NOT EXISTS idx_transactions_parent ON transactions(parent_transaction_id); +CREATE INDEX IF NOT EXISTS idx_categories_parent ON categories(parent_id); +CREATE INDEX IF NOT EXISTS idx_categories_type ON categories(type); +CREATE INDEX IF NOT EXISTS idx_suppliers_category ON suppliers(category_id); +CREATE INDEX IF NOT EXISTS idx_suppliers_normalized ON suppliers(normalized_name); +CREATE INDEX IF NOT EXISTS idx_keywords_category ON keywords(category_id); +CREATE INDEX IF NOT EXISTS idx_keywords_keyword ON keywords(keyword); +CREATE INDEX IF NOT EXISTS idx_budget_entries_period ON budget_entries(year, month); +CREATE INDEX IF NOT EXISTS idx_adjustment_entries_adjustment ON adjustment_entries(adjustment_id); +CREATE INDEX IF NOT EXISTS idx_imported_files_source ON imported_files(source_id); + +-- ============================================================================ +-- Balance sheet (Bilan) — Migration v9 mirror for new profiles +-- ============================================================================ +-- 5 tables + 7 indexes + seeded categories. Kept in sync with +-- `balance_schema.sql` (the source of truth applied by Migration v9 in lib.rs). +-- New profiles created from this consolidated schema get the balance feature +-- preinstalled without needing to replay v9 separately. + +CREATE TABLE IF NOT EXISTS balance_categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + key TEXT NOT NULL UNIQUE, + i18n_key TEXT NOT NULL, + kind TEXT NOT NULL CHECK(kind IN ('simple','priced')), + sort_order INTEGER NOT NULL DEFAULT 0, + is_active INTEGER NOT NULL DEFAULT 1, + is_seed INTEGER NOT NULL DEFAULT 0, + asset_type TEXT CHECK(asset_type IS NULL OR asset_type IN ('stock','crypto')), + -- User-facing override for the category label (migration v12). When set, + -- the UI shows this verbatim; otherwise it falls back to t(i18n_key). + custom_label TEXT +); + +CREATE TABLE IF NOT EXISTS balance_accounts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + balance_category_id INTEGER NOT NULL, + name TEXT NOT NULL, + symbol TEXT, + currency TEXT NOT NULL DEFAULT 'CAD' CHECK(currency = 'CAD'), + notes TEXT, + is_active INTEGER NOT NULL DEFAULT 1, + archived_at DATETIME, + -- Fiscal envelope / tax shelter (migration v12). NULL = no envelope (e.g. + -- a chequing account or crypto wallet). NOT an automobile type. + vehicle_type TEXT CHECK(vehicle_type IS NULL OR vehicle_type IN ('unregistered','tfsa','rrsp','rrif','fhsa','resp')), + -- Entry mode (migration v15). 'simple' = one denormalized value per + -- snapshot line; 'detailed' = a basket of per-security holdings (see + -- balance_snapshot_holdings). Accounts under a priced category are backfilled + -- to 'detailed' below. `detailed_since` is the authoritative pivot date from + -- which detailed entry is expected — NULL until the conversion flow sets it. + kind TEXT NOT NULL DEFAULT 'simple' CHECK (kind IN ('simple','detailed')), + detailed_since DATE, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (balance_category_id) REFERENCES balance_categories(id) ON DELETE RESTRICT +); + +CREATE TABLE IF NOT EXISTS balance_snapshots ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + snapshot_date DATE NOT NULL UNIQUE, + notes TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS balance_snapshot_lines ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + snapshot_id INTEGER NOT NULL, + account_id INTEGER NOT NULL, + quantity REAL, + unit_price REAL, + value REAL NOT NULL, + price_source TEXT, + price_fetched_at DATETIME, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (snapshot_id) REFERENCES balance_snapshots(id) ON DELETE CASCADE, + FOREIGN KEY (account_id) REFERENCES balance_accounts(id) ON DELETE RESTRICT, + UNIQUE(snapshot_id, account_id), + CHECK ( + (quantity IS NULL AND unit_price IS NULL) + OR (quantity IS NOT NULL AND unit_price IS NOT NULL) + ) +); + +CREATE TABLE IF NOT EXISTS balance_account_transfers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + account_id INTEGER NOT NULL, + transaction_id INTEGER NOT NULL, + direction TEXT NOT NULL CHECK(direction IN ('in','out')), + notes TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (account_id) REFERENCES balance_accounts(id) ON DELETE CASCADE, + FOREIGN KEY (transaction_id) REFERENCES transactions(id) ON DELETE RESTRICT, + UNIQUE(transaction_id, account_id) +); + +-- Détail par titre (Bilan Étape 2, migration v14). Kept in sync with +-- `balance_holdings_schema.sql` (the source of truth applied by Migration v14 +-- in lib.rs). balance_securities is the instrument catalogue (symbol normalized +-- upper/trim, COLLATE NOCASE UNIQUE); balance_snapshot_holdings is the per-title +-- breakdown of a detailed account's snapshot line. +CREATE TABLE IF NOT EXISTS balance_securities ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + symbol TEXT NOT NULL COLLATE NOCASE UNIQUE, + name TEXT, + currency TEXT NOT NULL DEFAULT 'CAD', + asset_type TEXT NOT NULL CHECK (asset_type IN ('stock','crypto')), + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS balance_snapshot_holdings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + snapshot_line_id INTEGER NOT NULL REFERENCES balance_snapshot_lines(id) ON DELETE CASCADE, + security_id INTEGER NOT NULL REFERENCES balance_securities(id) ON DELETE RESTRICT, + quantity REAL NOT NULL, + unit_price REAL NOT NULL, + value REAL NOT NULL, + book_cost REAL, + price_source TEXT, + price_fetched_at DATETIME, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE (snapshot_line_id, security_id) +); + +CREATE INDEX IF NOT EXISTS idx_balance_accounts_category ON balance_accounts(balance_category_id); +CREATE INDEX IF NOT EXISTS idx_balance_accounts_active ON balance_accounts(is_active) WHERE is_active = 1; +CREATE INDEX IF NOT EXISTS idx_balance_snapshot_lines_snapshot ON balance_snapshot_lines(snapshot_id); +CREATE INDEX IF NOT EXISTS idx_balance_snapshot_lines_account ON balance_snapshot_lines(account_id); +CREATE INDEX IF NOT EXISTS idx_balance_account_transfers_account ON balance_account_transfers(account_id); +CREATE INDEX IF NOT EXISTS idx_balance_account_transfers_transaction ON balance_account_transfers(transaction_id); +CREATE INDEX IF NOT EXISTS idx_balance_snapshots_date ON balance_snapshots(snapshot_date); +CREATE INDEX IF NOT EXISTS idx_balance_snapshot_holdings_line ON balance_snapshot_holdings(snapshot_line_id); +CREATE INDEX IF NOT EXISTS idx_balance_snapshot_holdings_security ON balance_snapshot_holdings(security_id); + +-- Asset classes only (Bilan axe véhicule, Étape 1). The former `tfsa` / `rrsp` +-- seeds were fiscal envelopes, not asset classes — they now live on the account +-- as `vehicle_type` (see balance_accounts.vehicle_type). New profiles ship with +-- exactly 5 asset classes: Liquidités, Fonds/FNB, Actions, Crypto, Autres. +INSERT OR IGNORE INTO balance_categories (key, i18n_key, kind, sort_order, is_seed, asset_type) VALUES + ('cash', 'balance.category.cash', 'simple', 10, 1, NULL), + ('fund', 'balance.category.fund', 'simple', 40, 1, NULL), + ('other', 'balance.category.other', 'simple', 50, 1, NULL), + ('stock', 'balance.category.stock', 'priced', 60, 1, 'stock'), + ('crypto', 'balance.category.crypto', 'priced', 70, 1, 'crypto'); + +-- Starter accounts (Issue #179): 4 plain accounts seeded for new profiles so +-- /balance lands non-empty. They are NOT marked as seed (no is_seed column on +-- balance_accounts) — once created they are indistinguishable from +-- user-created accounts and can be renamed/archived freely. Existing profiles +-- get the same 4 proposed via StarterAccountsModal on first /balance visit. +-- +-- Bilan axe véhicule (Étape 1): the CELI / REER starters are no longer linked +-- to a `tfsa` / `rrsp` category (those seeds are gone). They now attach to the +-- `other` asset class and carry the envelope in `vehicle_type`. Linking them to +-- a deleted key would make the subselect return NULL → NOT NULL violation → +-- broken new-profile init, so the asset class MUST resolve to `other`. +INSERT INTO balance_accounts (balance_category_id, name, currency, is_active, vehicle_type) VALUES + ((SELECT id FROM balance_categories WHERE key = 'cash'), 'Compte chèque', 'CAD', 1, NULL), + ((SELECT id FROM balance_categories WHERE key = 'other'), 'CELI', 'CAD', 1, 'tfsa'), + ((SELECT id FROM balance_categories WHERE key = 'other'), 'REER', 'CAD', 1, 'rrsp'), + ((SELECT id FROM balance_categories WHERE key = 'other'), 'Compte non-enregistré', 'CAD', 1, NULL); + +-- Détail par titre (migration v15 mirror): accounts under a priced category are +-- detailed-entry by default. The 4 starters above all sit under simple asset +-- classes (cash / other), so this is currently a no-op — kept at parity with the +-- v15 backfill so any future priced starter is stamped correctly. +UPDATE balance_accounts SET kind = 'detailed' + WHERE balance_category_id IN (SELECT id FROM balance_categories WHERE kind = 'priced'); + +-- Default preferences (new profiles ship with the v1 IPC taxonomy) +INSERT OR IGNORE INTO user_preferences (key, value) VALUES ('language', 'fr'); +INSERT OR IGNORE INTO user_preferences (key, value) VALUES ('theme', 'light'); +INSERT OR IGNORE INTO user_preferences (key, value) VALUES ('currency', 'EUR'); +INSERT OR IGNORE INTO user_preferences (key, value) VALUES ('date_format', 'DD/MM/YYYY'); +INSERT OR REPLACE INTO user_preferences (key, value) VALUES ('categories_schema_version', 'v1'); +-- Suppress StarterAccountsModal on first /balance visit for new profiles +-- (Issue #179). The 4 starter accounts are already seeded above, so the +-- modal would only show 4 collision rows with no actionable choice. Pre- +-- writing the pref skips that briefly-empty UX entirely. Suggestion S1 +-- from PR #185 review (#187). +INSERT OR IGNORE INTO user_preferences (key, value) VALUES ('balance_starter_proposed', '{"shown_at":"seed","accepted":[]}'); + +-- ============================================================================ +-- Seed v1 — IPC Statistique Canada-aligned, 3 levels, Canada/Québec +-- ---------------------------------------------------------------------------- +-- Reset any pre-existing category data (possible when tauri-plugin-sql runs +-- the historical v2 seed migration on this fresh DB before this script +-- executes). Keywords/categories are wiped and re-inserted with the v1 IDs +-- (1000+ range) so existing references in migrations v3-v7 stay inert. +-- ============================================================================ +DELETE FROM keywords; +UPDATE transactions SET category_id = NULL; +DELETE FROM categories; + +-- LEVEL 1 — Roots (9 IPC components + Revenus + Transferts) +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1000, 'Revenus', NULL, 'income', '#16a34a', 0, 1, 'categoriesSeed.revenus.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1100, 'Alimentation', NULL, 'expense', '#ea580c', 0, 2, 'categoriesSeed.alimentation.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1200, 'Logement', NULL, 'expense', '#dc2626', 0, 3, 'categoriesSeed.logement.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1300, 'Ménage & ameublement', NULL, 'expense', '#ca8a04', 0, 4, 'categoriesSeed.menage.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1400, 'Vêtements & chaussures', NULL, 'expense', '#d946ef', 0, 5, 'categoriesSeed.vetements.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1500, 'Transport', NULL, 'expense', '#2563eb', 0, 6, 'categoriesSeed.transport.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1600, 'Santé & soins personnels', NULL, 'expense', '#f43f5e', 0, 7, 'categoriesSeed.sante.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1700, 'Loisirs, formation & lecture', NULL, 'expense', '#8b5cf6', 0, 8, 'categoriesSeed.loisirs.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1800, 'Boissons, tabac & cannabis', NULL, 'expense', '#7c3aed', 0, 9, 'categoriesSeed.consommation.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1900, 'Finances & obligations', NULL, 'expense', '#6b7280', 0, 10, 'categoriesSeed.finances.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1950, 'Transferts & placements', NULL, 'transfer', '#0ea5e9', 0, 11, 'categoriesSeed.transferts.root'); + +-- 1000 — Revenus +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1010, 'Emploi', 1000, 'income', '#22c55e', 0, 1, 'categoriesSeed.revenus.emploi.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1011, 'Paie régulière', 1010, 'income', '#22c55e', 1, 1, 'categoriesSeed.revenus.emploi.paie'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1012, 'Primes & bonus', 1010, 'income', '#4ade80', 1, 2, 'categoriesSeed.revenus.emploi.primes'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1013, 'Travail autonome', 1010, 'income', '#86efac', 1, 3, 'categoriesSeed.revenus.emploi.autonome'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1020, 'Gouvernemental', 1000, 'income', '#16a34a', 0, 2, 'categoriesSeed.revenus.gouvernemental.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1021, 'Remboursement impôt', 1020, 'income', '#16a34a', 1, 1, 'categoriesSeed.revenus.gouvernemental.remboursementImpot'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1022, 'Allocations familiales', 1020, 'income', '#15803d', 1, 2, 'categoriesSeed.revenus.gouvernemental.allocations'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1023, 'Crédits TPS/TVQ', 1020, 'income', '#166534', 1, 3, 'categoriesSeed.revenus.gouvernemental.credits'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1024, 'Assurance-emploi / RQAP', 1020, 'income', '#14532d', 1, 4, 'categoriesSeed.revenus.gouvernemental.assuranceEmploi'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1030, 'Revenus de placement', 1000, 'income', '#10b981', 0, 3, 'categoriesSeed.revenus.placement.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1031, 'Intérêts & dividendes', 1030, 'income', '#10b981', 1, 1, 'categoriesSeed.revenus.placement.interets'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1032, 'Gains en capital', 1030, 'income', '#059669', 1, 2, 'categoriesSeed.revenus.placement.capital'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1033, 'Revenus locatifs', 1030, 'income', '#047857', 1, 3, 'categoriesSeed.revenus.placement.locatifs'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1090, 'Autres revenus', 1000, 'income', '#84cc16', 1, 9, 'categoriesSeed.revenus.autres'); + +-- 1100 — Alimentation +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1110, 'Épicerie & marché', 1100, 'expense', '#ea580c', 0, 1, 'categoriesSeed.alimentation.epicerie.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1111, 'Épicerie régulière', 1110, 'expense', '#ea580c', 1, 1, 'categoriesSeed.alimentation.epicerie.reguliere'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1112, 'Boucherie & poissonnerie', 1110, 'expense', '#c2410c', 1, 2, 'categoriesSeed.alimentation.epicerie.boucherie'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1113, 'Boulangerie & pâtisserie', 1110, 'expense', '#9a3412', 1, 3, 'categoriesSeed.alimentation.epicerie.boulangerie'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1114, 'Dépanneur', 1110, 'expense', '#7c2d12', 1, 4, 'categoriesSeed.alimentation.epicerie.depanneur'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1115, 'Marché & produits spécialisés', 1110, 'expense', '#fb923c', 1, 5, 'categoriesSeed.alimentation.epicerie.marche'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1120, 'Restauration', 1100, 'expense', '#f97316', 0, 2, 'categoriesSeed.alimentation.restauration.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1121, 'Restaurant', 1120, 'expense', '#f97316', 1, 1, 'categoriesSeed.alimentation.restauration.restaurant'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1122, 'Café & boulangerie rapide', 1120, 'expense', '#fb923c', 1, 2, 'categoriesSeed.alimentation.restauration.cafe'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1123, 'Restauration rapide', 1120, 'expense', '#fdba74', 1, 3, 'categoriesSeed.alimentation.restauration.fastfood'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1124, 'Livraison à domicile', 1120, 'expense', '#fed7aa', 1, 4, 'categoriesSeed.alimentation.restauration.livraison'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1125, 'Cantine & cafétéria', 1120, 'expense', '#ffedd5', 1, 5, 'categoriesSeed.alimentation.restauration.cantine'); + +-- 1200 — Logement +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1210, 'Habitation principale', 1200, 'expense', '#dc2626', 0, 1, 'categoriesSeed.logement.habitation.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1211, 'Loyer', 1210, 'expense', '#dc2626', 1, 1, 'categoriesSeed.logement.habitation.loyer'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1212, 'Hypothèque', 1210, 'expense', '#b91c1c', 1, 2, 'categoriesSeed.logement.habitation.hypotheque'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1213, 'Taxes municipales & scolaires', 1210, 'expense', '#991b1b', 1, 3, 'categoriesSeed.logement.habitation.taxes'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1214, 'Charges de copropriété', 1210, 'expense', '#7f1d1d', 1, 4, 'categoriesSeed.logement.habitation.copropriete'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1220, 'Services publics', 1200, 'expense', '#ef4444', 0, 2, 'categoriesSeed.logement.services.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1221, 'Électricité', 1220, 'expense', '#ef4444', 1, 1, 'categoriesSeed.logement.services.electricite'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1222, 'Gaz naturel', 1220, 'expense', '#f87171', 1, 2, 'categoriesSeed.logement.services.gaz'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1223, 'Chauffage (mazout, propane)', 1220, 'expense', '#fca5a5', 1, 3, 'categoriesSeed.logement.services.chauffage'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1224, 'Eau & égouts', 1220, 'expense', '#fecaca', 1, 4, 'categoriesSeed.logement.services.eau'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1230, 'Communications', 1200, 'expense', '#6366f1', 0, 3, 'categoriesSeed.logement.communications.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1231, 'Internet résidentiel', 1230, 'expense', '#6366f1', 1, 1, 'categoriesSeed.logement.communications.internet'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1232, 'Téléphonie mobile', 1230, 'expense', '#818cf8', 1, 2, 'categoriesSeed.logement.communications.mobile'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1233, 'Téléphonie résidentielle', 1230, 'expense', '#a5b4fc', 1, 3, 'categoriesSeed.logement.communications.residentielle'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1234, 'Câblodistribution & streaming TV', 1230, 'expense', '#c7d2fe', 1, 4, 'categoriesSeed.logement.communications.streamingTv'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1240, 'Entretien & réparations', 1200, 'expense', '#e11d48', 0, 4, 'categoriesSeed.logement.entretien.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1241, 'Entretien général', 1240, 'expense', '#e11d48', 1, 1, 'categoriesSeed.logement.entretien.general'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1242, 'Rénovations', 1240, 'expense', '#be123c', 1, 2, 'categoriesSeed.logement.entretien.renovations'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1243, 'Matériaux & outils', 1240, 'expense', '#9f1239', 1, 3, 'categoriesSeed.logement.entretien.materiaux'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1244, 'Aménagement paysager', 1240, 'expense', '#881337', 1, 4, 'categoriesSeed.logement.entretien.paysager'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1250, 'Assurance habitation', 1200, 'expense', '#14b8a6', 1, 5, 'categoriesSeed.logement.assurance'); + +-- 1300 — Ménage & ameublement +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1310, 'Ameublement', 1300, 'expense', '#ca8a04', 0, 1, 'categoriesSeed.menage.ameublement.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1311, 'Meubles', 1310, 'expense', '#ca8a04', 1, 1, 'categoriesSeed.menage.ameublement.meubles'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1312, 'Électroménagers', 1310, 'expense', '#a16207', 1, 2, 'categoriesSeed.menage.ameublement.electromenagers'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1313, 'Décoration', 1310, 'expense', '#854d0e', 1, 3, 'categoriesSeed.menage.ameublement.decoration'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1320, 'Fournitures ménagères', 1300, 'expense', '#eab308', 0, 2, 'categoriesSeed.menage.fournitures.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1321, 'Produits d''entretien', 1320, 'expense', '#eab308', 1, 1, 'categoriesSeed.menage.fournitures.entretien'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1322, 'Literie & linge de maison', 1320, 'expense', '#facc15', 1, 2, 'categoriesSeed.menage.fournitures.literie'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1323, 'Vaisselle & ustensiles', 1320, 'expense', '#fde047', 1, 3, 'categoriesSeed.menage.fournitures.vaisselle'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1330, 'Services domestiques', 1300, 'expense', '#fbbf24', 0, 3, 'categoriesSeed.menage.services.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1331, 'Ménage & nettoyage', 1330, 'expense', '#fbbf24', 1, 1, 'categoriesSeed.menage.services.nettoyage'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1332, 'Buanderie & pressing', 1330, 'expense', '#fcd34d', 1, 2, 'categoriesSeed.menage.services.buanderie'); + +-- 1400 — Vêtements & chaussures +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1410, 'Vêtements adultes', 1400, 'expense', '#d946ef', 1, 1, 'categoriesSeed.vetements.adultes'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1420, 'Vêtements enfants', 1400, 'expense', '#c026d3', 1, 2, 'categoriesSeed.vetements.enfants'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1430, 'Chaussures', 1400, 'expense', '#a21caf', 1, 3, 'categoriesSeed.vetements.chaussures'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1440, 'Accessoires & bijoux', 1400, 'expense', '#86198f', 1, 4, 'categoriesSeed.vetements.accessoires'); + +-- 1500 — Transport +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1510, 'Véhicule personnel', 1500, 'expense', '#2563eb', 0, 1, 'categoriesSeed.transport.vehicule.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1511, 'Achat / location véhicule', 1510, 'expense', '#2563eb', 1, 1, 'categoriesSeed.transport.vehicule.achat'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1512, 'Essence', 1510, 'expense', '#1d4ed8', 1, 2, 'categoriesSeed.transport.vehicule.essence'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1513, 'Entretien & réparations auto', 1510, 'expense', '#1e40af', 1, 3, 'categoriesSeed.transport.vehicule.entretien'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1514, 'Immatriculation & permis', 1510, 'expense', '#1e3a8a', 1, 4, 'categoriesSeed.transport.vehicule.immatriculation'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1515, 'Stationnement & péages', 1510, 'expense', '#3b82f6', 1, 5, 'categoriesSeed.transport.vehicule.stationnement'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1516, 'Assurance auto', 1510, 'expense', '#60a5fa', 1, 6, 'categoriesSeed.transport.vehicule.assurance'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1520, 'Transport public', 1500, 'expense', '#0ea5e9', 0, 2, 'categoriesSeed.transport.public.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1521, 'Autobus & métro', 1520, 'expense', '#0ea5e9', 1, 1, 'categoriesSeed.transport.public.autobus'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1522, 'Train de banlieue', 1520, 'expense', '#0284c7', 1, 2, 'categoriesSeed.transport.public.train'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1523, 'Taxi & covoiturage', 1520, 'expense', '#0369a1', 1, 3, 'categoriesSeed.transport.public.taxi'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1530, 'Voyages longue distance', 1500, 'expense', '#38bdf8', 0, 3, 'categoriesSeed.transport.voyages.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1531, 'Avion', 1530, 'expense', '#38bdf8', 1, 1, 'categoriesSeed.transport.voyages.avion'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1532, 'Train & autocar', 1530, 'expense', '#7dd3fc', 1, 2, 'categoriesSeed.transport.voyages.trainAutocar'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1533, 'Hébergement', 1530, 'expense', '#bae6fd', 1, 3, 'categoriesSeed.transport.voyages.hebergement'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1534, 'Location véhicule voyage', 1530, 'expense', '#e0f2fe', 1, 4, 'categoriesSeed.transport.voyages.location'); + +-- 1600 — Santé & soins personnels +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1610, 'Soins médicaux', 1600, 'expense', '#f43f5e', 0, 1, 'categoriesSeed.sante.medicaux.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1611, 'Pharmacie', 1610, 'expense', '#f43f5e', 1, 1, 'categoriesSeed.sante.medicaux.pharmacie'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1612, 'Consultations médicales', 1610, 'expense', '#e11d48', 1, 2, 'categoriesSeed.sante.medicaux.consultations'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1613, 'Dentiste & orthodontiste', 1610, 'expense', '#be123c', 1, 3, 'categoriesSeed.sante.medicaux.dentiste'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1614, 'Optométrie & lunettes', 1610, 'expense', '#9f1239', 1, 4, 'categoriesSeed.sante.medicaux.optometrie'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1615, 'Thérapies (physio, psycho, etc.)', 1610, 'expense', '#881337', 1, 5, 'categoriesSeed.sante.medicaux.therapies'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1616, 'Assurance santé complémentaire', 1610, 'expense', '#fb7185', 1, 6, 'categoriesSeed.sante.medicaux.assurance'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1620, 'Soins personnels', 1600, 'expense', '#fb7185', 0, 2, 'categoriesSeed.sante.personnels.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1621, 'Coiffure & esthétique', 1620, 'expense', '#fb7185', 1, 1, 'categoriesSeed.sante.personnels.coiffure'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1622, 'Produits de soins corporels', 1620, 'expense', '#fda4af', 1, 2, 'categoriesSeed.sante.personnels.soins'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1630, 'Assurance vie & invalidité', 1600, 'expense', '#14b8a6', 1, 3, 'categoriesSeed.sante.assuranceVie'); + +-- 1700 — Loisirs, formation & lecture +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1710, 'Divertissement', 1700, 'expense', '#8b5cf6', 0, 1, 'categoriesSeed.loisirs.divertissement.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1711, 'Cinéma & spectacles', 1710, 'expense', '#8b5cf6', 1, 1, 'categoriesSeed.loisirs.divertissement.cinema'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1712, 'Jeux vidéo & consoles', 1710, 'expense', '#a78bfa', 1, 2, 'categoriesSeed.loisirs.divertissement.jeux'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1713, 'Streaming vidéo', 1710, 'expense', '#c4b5fd', 1, 3, 'categoriesSeed.loisirs.divertissement.streamingVideo'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1714, 'Streaming musique & audio', 1710, 'expense', '#ddd6fe', 1, 4, 'categoriesSeed.loisirs.divertissement.streamingMusique'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1715, 'Jouets & passe-temps', 1710, 'expense', '#ede9fe', 1, 5, 'categoriesSeed.loisirs.divertissement.jouets'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1720, 'Sports & plein air', 1700, 'expense', '#22c55e', 0, 2, 'categoriesSeed.loisirs.sports.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1721, 'Abonnements sportifs', 1720, 'expense', '#22c55e', 1, 1, 'categoriesSeed.loisirs.sports.abonnements'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1722, 'Équipement sportif', 1720, 'expense', '#4ade80', 1, 2, 'categoriesSeed.loisirs.sports.equipement'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1723, 'Parcs & activités plein air', 1720, 'expense', '#86efac', 1, 3, 'categoriesSeed.loisirs.sports.parcs'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1730, 'Formation & éducation', 1700, 'expense', '#6366f1', 0, 3, 'categoriesSeed.loisirs.formation.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1731, 'Scolarité (frais)', 1730, 'expense', '#6366f1', 1, 1, 'categoriesSeed.loisirs.formation.scolarite'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1732, 'Matériel scolaire', 1730, 'expense', '#818cf8', 1, 2, 'categoriesSeed.loisirs.formation.materiel'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1733, 'Cours & certifications', 1730, 'expense', '#a5b4fc', 1, 3, 'categoriesSeed.loisirs.formation.cours'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1734, 'Abonnements professionnels', 1730, 'expense', '#c7d2fe', 1, 4, 'categoriesSeed.loisirs.formation.abonnements'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1740, 'Lecture & médias', 1700, 'expense', '#ec4899', 0, 4, 'categoriesSeed.loisirs.lecture.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1741, 'Livres', 1740, 'expense', '#ec4899', 1, 1, 'categoriesSeed.loisirs.lecture.livres'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1742, 'Journaux & magazines', 1740, 'expense', '#f472b6', 1, 2, 'categoriesSeed.loisirs.lecture.journaux'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1750, 'Animaux de compagnie', 1700, 'expense', '#a855f7', 0, 5, 'categoriesSeed.loisirs.animaux.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1751, 'Nourriture & accessoires animaux', 1750, 'expense', '#a855f7', 1, 1, 'categoriesSeed.loisirs.animaux.nourriture'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1752, 'Vétérinaire', 1750, 'expense', '#c084fc', 1, 2, 'categoriesSeed.loisirs.animaux.veterinaire'); + +-- 1800 — Boissons, tabac & cannabis +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1810, 'Alcool (SAQ, microbrasseries)', 1800, 'expense', '#7c3aed', 1, 1, 'categoriesSeed.consommation.alcool'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1820, 'Cannabis (SQDC)', 1800, 'expense', '#6d28d9', 1, 2, 'categoriesSeed.consommation.cannabis'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1830, 'Tabac', 1800, 'expense', '#5b21b6', 1, 3, 'categoriesSeed.consommation.tabac'); + +-- 1900 — Finances & obligations +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1910, 'Frais bancaires', 1900, 'expense', '#6b7280', 0, 1, 'categoriesSeed.finances.fraisBancaires.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1911, 'Frais de compte', 1910, 'expense', '#6b7280', 1, 1, 'categoriesSeed.finances.fraisBancaires.compte'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1912, 'Intérêts & frais de crédit', 1910, 'expense', '#9ca3af', 1, 2, 'categoriesSeed.finances.fraisBancaires.interets'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1913, 'Frais de change', 1910, 'expense', '#d1d5db', 1, 3, 'categoriesSeed.finances.fraisBancaires.change'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1920, 'Impôts & taxes', 1900, 'expense', '#dc2626', 0, 2, 'categoriesSeed.finances.impots.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1921, 'Impôt fédéral', 1920, 'expense', '#dc2626', 1, 1, 'categoriesSeed.finances.impots.federal'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1922, 'Impôt provincial', 1920, 'expense', '#b91c1c', 1, 2, 'categoriesSeed.finances.impots.provincial'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1923, 'Acomptes provisionnels', 1920, 'expense', '#991b1b', 1, 3, 'categoriesSeed.finances.impots.acomptes'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1930, 'Dons & cotisations', 1900, 'expense', '#ec4899', 0, 3, 'categoriesSeed.finances.dons.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1931, 'Dons de charité', 1930, 'expense', '#ec4899', 1, 1, 'categoriesSeed.finances.dons.charite'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1932, 'Cotisations professionnelles', 1930, 'expense', '#f472b6', 1, 2, 'categoriesSeed.finances.dons.professionnelles'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1933, 'Cotisations syndicales', 1930, 'expense', '#f9a8d4', 1, 3, 'categoriesSeed.finances.dons.syndicales'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1940, 'Cadeaux', 1900, 'expense', '#f43f5e', 1, 4, 'categoriesSeed.finances.cadeaux'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1945, 'Retrait cash', 1900, 'expense', '#57534e', 1, 5, 'categoriesSeed.finances.cash'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1946, 'Achats divers non catégorisés', 1900, 'expense', '#78716c', 1, 6, 'categoriesSeed.finances.divers'); + +-- 1950 — Transferts & placements +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1960, 'Épargne & placements', 1950, 'transfer', '#0ea5e9', 0, 1, 'categoriesSeed.transferts.epargne.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1961, 'REER / RRSP', 1960, 'transfer', '#0ea5e9', 1, 1, 'categoriesSeed.transferts.epargne.reer'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1962, 'CELI / TFSA', 1960, 'transfer', '#0284c7', 1, 2, 'categoriesSeed.transferts.epargne.celi'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1963, 'REEE / RESP', 1960, 'transfer', '#0369a1', 1, 3, 'categoriesSeed.transferts.epargne.reee'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1964, 'Compte non-enregistré', 1960, 'transfer', '#075985', 1, 4, 'categoriesSeed.transferts.epargne.nonEnregistre'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1965, 'Fonds d''urgence', 1960, 'transfer', '#0c4a6e', 1, 5, 'categoriesSeed.transferts.epargne.urgence'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1970, 'Remboursement de dette', 1950, 'transfer', '#7c3aed', 0, 2, 'categoriesSeed.transferts.dette.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1971, 'Paiement carte crédit', 1970, 'transfer', '#7c3aed', 1, 1, 'categoriesSeed.transferts.dette.carteCredit'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1972, 'Remboursement prêt étudiant', 1970, 'transfer', '#8b5cf6', 1, 2, 'categoriesSeed.transferts.dette.etudiant'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1973, 'Remboursement prêt perso', 1970, 'transfer', '#a78bfa', 1, 3, 'categoriesSeed.transferts.dette.personnel'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1980, 'Transferts internes', 1950, 'transfer', '#64748b', 1, 3, 'categoriesSeed.transferts.internes'); + +-- ============================================================================ +-- Keywords — Canadian suppliers (150+ entries) +-- ============================================================================ + +-- Alimentation > Épicerie régulière (1111) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('METRO', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('IGA', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('MAXI', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SUPER C', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('LOBLAWS', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PROVIGO', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ADONIS', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('WHOLE FOODS', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('AVRIL', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('RACHELLE-BERY', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('COSTCO', 1111, 50); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('WALMART', 1111, 50); + +-- Épicerie > Boucherie (1112) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BOUCHERIE', 1112, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('POISSONNERIE', 1112, 0); + +-- Épicerie > Boulangerie (1113) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BOULANGERIE', 1113, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PATISSERIE', 1113, 0); + +-- Épicerie > Dépanneur (1114) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('COUCHE-TARD', 1114, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DEPANNEUR', 1114, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('7-ELEVEN', 1114, 0); + +-- Restauration > Restaurant (1121) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('RESTAURANT', 1121, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BRASSERIE', 1121, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BISTRO', 1121, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SUSHI', 1121, 0); + +-- Restauration > Café (1122) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('STARBUCKS', 1122, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('TIM HORTONS', 1122, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SECOND CUP', 1122, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('VAN HOUTTE', 1122, 0); + +-- Restauration > Fast food (1123) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('MCDONALD', 1123, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SUBWAY', 1123, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('A&W', 1123, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BURGER KING', 1123, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('KFC', 1123, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DOMINOS', 1123, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PIZZA HUT', 1123, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BELLE PROVINCE', 1123, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ST-HUBERT', 1123, 0); + +-- Restauration > Livraison (1124) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DOORDASH', 1124, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DD/DOORDASH', 1124, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('UBER EATS', 1124, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SKIPTHEDISHES', 1124, 0); + +-- Logement > Hypothèque (1212) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('MTG/HYP', 1212, 0); + +-- Logement > Taxes municipales (1213) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('M-ST-HILAIRE TX', 1213, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('TAXES MUNICIPALES', 1213, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CSS PATRIOT', 1213, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('TAXE SCOLAIRE', 1213, 0); + +-- Électricité (1221) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('HYDRO-QUEBEC', 1221, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('HYDRO QUEBEC', 1221, 0); + +-- Gaz (1222) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ENERGIR', 1222, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('GAZ METRO', 1222, 0); + +-- Internet (1231) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('VIDEOTRON', 1231, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BELL INTERNET', 1231, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ORICOM', 1231, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('COGECO', 1231, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('EBOX', 1231, 0); + +-- Mobile (1232) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('FIZZ', 1232, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('KOODO', 1232, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PUBLIC MOBILE', 1232, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('VIRGIN', 1232, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BELL MOBILITE', 1232, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('TELUS MOBILE', 1232, 0); + +-- Entretien maison (1241) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('IKEA', 1241, 0); + +-- Matériaux & outils (1243) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CANADIAN TIRE', 1243, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CANAC', 1243, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('RONA', 1243, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('HOME DEPOT', 1243, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BMR', 1243, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PRINCESS AUTO', 1243, 0); + +-- Assurance habitation (1250) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BELAIR', 1250, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PRYSM', 1250, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('INTACT ASSURANCE', 1250, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DESJARDINS ASSURANCE', 1250, 0); + +-- Meubles (1311) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('TANGUAY', 1311, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('LEON', 1311, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('STRUCTUBE', 1311, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BRICK', 1311, 0); + +-- Électroménagers (1312) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BEST BUY', 1312, 0); + +-- Décoration (1313) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BOUCLAIR', 1313, 0); + +-- Vêtements (1410) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('UNIQLO', 1410, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('WINNERS', 1410, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SIMONS', 1410, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('MARKS', 1410, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('H&M', 1410, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('OLD NAVY', 1410, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('GAP', 1410, 0); + +-- Transport — Essence (1512) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SHELL', 1512, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ESSO', 1512, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ULTRAMAR', 1512, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PETRO-CANADA', 1512, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PETRO CANADA', 1512, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CREVIER', 1512, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('HARNOIS', 1512, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('COUCHE-TARD ESSENCE', 1512, 10); + +-- Permis / SAAQ (1514) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SAAQ', 1514, 0); + +-- Transport public — autobus/métro (1521) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('STM', 1521, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('RTC', 1521, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('STL', 1521, 0); + +-- Train de banlieue (1522) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('GARE MONT-SAINT', 1522, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('GARE SAINT-HUBERT', 1522, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('GARE CENTRALE', 1522, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('EXO', 1522, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('REM', 1522, 0); + +-- Taxi / Uber (1523) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('UBER', 1523, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('LYFT', 1523, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('TAXI', 1523, 0); + +-- Avion (1531) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('AIR CANADA', 1531, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('WESTJET', 1531, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('AIR TRANSAT', 1531, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PORTER', 1531, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('AEROPORTS DE MONTREAL', 1531, 0); + +-- Hébergement (1533) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('AIRBNB', 1533, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('HILTON', 1533, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('MARRIOTT', 1533, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BOOKING.COM', 1533, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('NORWEGIAN CRUISE', 1533, 0); + +-- Pharmacie (1611) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('JEAN COUTU', 1611, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('FAMILIPRIX', 1611, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PHARMAPRIX', 1611, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PROXIM', 1611, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('UNIPRIX', 1611, 0); + +-- Thérapies (1615) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PHYSIOACTIF', 1615, 0); + +-- Cinéma & spectacles (1711) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CINEPLEX', 1711, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CINEMA DU PARC', 1711, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('TICKETMASTER', 1711, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CLUB SODA', 1711, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('LEPOINTDEVENTE', 1711, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('EVENBRITE', 1711, 0); + +-- Jeux vidéo (1712) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('STEAMGAMES', 1712, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PLAYSTATION', 1712, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('NINTENDO', 1712, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('XBOX', 1712, 0); + +-- Streaming vidéo (1713) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('NETFLIX', 1713, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PRIMEVIDEO', 1713, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DISNEY PLUS', 1713, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CRAVE', 1713, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('APPLE TV', 1713, 0); + +-- Streaming musique (1714) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SPOTIFY', 1714, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('APPLE MUSIC', 1714, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('YOUTUBE MUSIC', 1714, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('TIDAL', 1714, 0); + +-- Jouets & hobbies (1715) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('LEGO', 1715, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('TOYS R US', 1715, 0); + +-- Équipement sportif (1722) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('MOUNTAIN EQUIPMENT', 1722, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('LA CORDEE', 1722, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DECATHLON', 1722, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SPORTS EXPERTS', 1722, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ATMOSPHERE', 1722, 0); + +-- Parcs & activités (1723) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SEPAQ', 1723, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BLOC SHOP', 1723, 0); + +-- Lecture (1741) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('RENAUD-BRAY', 1741, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('INDIGO', 1741, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ARCHAMBAULT', 1741, 0); + +-- Animaux — nourriture (1751) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('MONDOU', 1751, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PET SMART', 1751, 0); + +-- Alcool (1810) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SAQ', 1810, 0); + +-- Cannabis (1820) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SQDC', 1820, 0); + +-- Frais bancaires (1911) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PROGRAMME PERFORMANCE', 1911, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('FRAIS MENSUELS', 1911, 0); + +-- Impôts (1921, 1922) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('GOUV. QUEBEC', 1922, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('REVENU QUEBEC', 1922, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ARC IMPOT', 1921, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CRA TAX', 1921, 0); + +-- Dons de charité (1931) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('OXFAM', 1931, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CENTRAIDE', 1931, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('FPA', 1931, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CROIX-ROUGE', 1931, 0); + +-- Cotisations professionnelles (1932) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ORDRE DES COMPTABL', 1932, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CPA CANADA', 1932, 0); + +-- Cadeaux (1940) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DANS UN JARDIN', 1940, 0); + +-- Divers (1946) — catch-all marketplace +INSERT INTO keywords (keyword, category_id, priority) VALUES ('AMAZON', 1946, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('AMZN', 1946, 0); + +-- Placements (1964) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('WS INVESTMENTS', 1964, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('WEALTHSIMPLE', 1964, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PEAK INVESTMENT', 1964, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DYNAMIC FUND', 1964, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('FIDELITY', 1964, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('AGF', 1964, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('QUESTRADE', 1964, 0); + +-- Paie (1011) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PAY/PAY', 1011, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DEPOT PAIE', 1011, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PAYROLL', 1011, 0); diff --git a/src-tauri/src/database/fixtures/consolidated_v0.3.0.sql b/src-tauri/src/database/fixtures/consolidated_v0.3.0.sql new file mode 100644 index 0000000..227d3b7 --- /dev/null +++ b/src-tauri/src/database/fixtures/consolidated_v0.3.0.sql @@ -0,0 +1,184 @@ +-- Consolidated schema for new profile databases +-- This file bakes in the base schema + all migrations (v3-v6) +-- Used ONLY for initializing new profile databases (not for the default profile) + +CREATE TABLE IF NOT EXISTS import_sources ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + description TEXT, + date_format TEXT NOT NULL DEFAULT '%d/%m/%Y', + delimiter TEXT NOT NULL DEFAULT ';', + encoding TEXT NOT NULL DEFAULT 'utf-8', + column_mapping TEXT NOT NULL, + skip_lines INTEGER NOT NULL DEFAULT 0, + has_header INTEGER NOT NULL DEFAULT 1, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS imported_files ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + source_id INTEGER NOT NULL, + filename TEXT NOT NULL, + file_hash TEXT NOT NULL, + import_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + row_count INTEGER NOT NULL DEFAULT 0, + status TEXT NOT NULL DEFAULT 'completed', + notes TEXT, + FOREIGN KEY (source_id) REFERENCES import_sources(id) ON DELETE CASCADE, + UNIQUE(source_id, filename) +); + +CREATE TABLE IF NOT EXISTS categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + parent_id INTEGER, + color TEXT, + icon TEXT, + type TEXT NOT NULL DEFAULT 'expense', + is_active INTEGER NOT NULL DEFAULT 1, + is_inputable INTEGER NOT NULL DEFAULT 1, + sort_order INTEGER NOT NULL DEFAULT 0, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (parent_id) REFERENCES categories(id) ON DELETE SET NULL +); + +CREATE TABLE IF NOT EXISTS suppliers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + normalized_name TEXT NOT NULL, + category_id INTEGER, + is_active INTEGER NOT NULL DEFAULT 1, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL +); + +CREATE TABLE IF NOT EXISTS keywords ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + keyword TEXT NOT NULL, + category_id INTEGER NOT NULL, + supplier_id INTEGER, + priority INTEGER NOT NULL DEFAULT 0, + is_active INTEGER NOT NULL DEFAULT 1, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE, + FOREIGN KEY (supplier_id) REFERENCES suppliers(id) ON DELETE SET NULL, + UNIQUE(keyword, category_id) +); + +CREATE TABLE IF NOT EXISTS transactions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date DATE NOT NULL, + description TEXT NOT NULL, + amount REAL NOT NULL, + category_id INTEGER, + supplier_id INTEGER, + source_id INTEGER, + file_id INTEGER, + original_description TEXT, + notes TEXT, + is_manually_categorized INTEGER NOT NULL DEFAULT 0, + is_split INTEGER NOT NULL DEFAULT 0, + parent_transaction_id INTEGER, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL, + FOREIGN KEY (supplier_id) REFERENCES suppliers(id) ON DELETE SET NULL, + FOREIGN KEY (source_id) REFERENCES import_sources(id) ON DELETE SET NULL, + FOREIGN KEY (file_id) REFERENCES imported_files(id) ON DELETE SET NULL, + FOREIGN KEY (parent_transaction_id) REFERENCES transactions(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS adjustments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + description TEXT, + date DATE NOT NULL, + is_recurring INTEGER NOT NULL DEFAULT 0, + recurrence_rule TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS adjustment_entries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + adjustment_id INTEGER NOT NULL, + category_id INTEGER NOT NULL, + amount REAL NOT NULL, + description TEXT, + FOREIGN KEY (adjustment_id) REFERENCES adjustments(id) ON DELETE CASCADE, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS budget_entries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + category_id INTEGER NOT NULL, + year INTEGER NOT NULL, + month INTEGER NOT NULL, + amount REAL NOT NULL, + notes TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE, + UNIQUE(category_id, year, month) +); + +CREATE TABLE IF NOT EXISTS budget_templates ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + description TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS budget_template_entries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + template_id INTEGER NOT NULL, + category_id INTEGER NOT NULL, + amount REAL NOT NULL, + FOREIGN KEY (template_id) REFERENCES budget_templates(id) ON DELETE CASCADE, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE, + UNIQUE(template_id, category_id) +); + +CREATE TABLE IF NOT EXISTS import_config_templates ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + delimiter TEXT NOT NULL DEFAULT ';', + encoding TEXT NOT NULL DEFAULT 'utf-8', + date_format TEXT NOT NULL DEFAULT 'DD/MM/YYYY', + skip_lines INTEGER NOT NULL DEFAULT 0, + has_header INTEGER NOT NULL DEFAULT 1, + column_mapping TEXT NOT NULL, + amount_mode TEXT NOT NULL DEFAULT 'single', + sign_convention TEXT NOT NULL DEFAULT 'negative_expense', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS user_preferences ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Indexes +CREATE INDEX IF NOT EXISTS idx_transactions_date ON transactions(date); +CREATE INDEX IF NOT EXISTS idx_transactions_category ON transactions(category_id); +CREATE INDEX IF NOT EXISTS idx_transactions_supplier ON transactions(supplier_id); +CREATE INDEX IF NOT EXISTS idx_transactions_source ON transactions(source_id); +CREATE INDEX IF NOT EXISTS idx_transactions_file ON transactions(file_id); +CREATE INDEX IF NOT EXISTS idx_transactions_parent ON transactions(parent_transaction_id); +CREATE INDEX IF NOT EXISTS idx_categories_parent ON categories(parent_id); +CREATE INDEX IF NOT EXISTS idx_categories_type ON categories(type); +CREATE INDEX IF NOT EXISTS idx_suppliers_category ON suppliers(category_id); +CREATE INDEX IF NOT EXISTS idx_suppliers_normalized ON suppliers(normalized_name); +CREATE INDEX IF NOT EXISTS idx_keywords_category ON keywords(category_id); +CREATE INDEX IF NOT EXISTS idx_keywords_keyword ON keywords(keyword); +CREATE INDEX IF NOT EXISTS idx_budget_entries_period ON budget_entries(year, month); +CREATE INDEX IF NOT EXISTS idx_adjustment_entries_adjustment ON adjustment_entries(adjustment_id); +CREATE INDEX IF NOT EXISTS idx_imported_files_source ON imported_files(source_id); + +-- Default preferences +INSERT OR IGNORE INTO user_preferences (key, value) VALUES ('language', 'fr'); +INSERT OR IGNORE INTO user_preferences (key, value) VALUES ('theme', 'light'); +INSERT OR IGNORE INTO user_preferences (key, value) VALUES ('currency', 'EUR'); +INSERT OR IGNORE INTO user_preferences (key, value) VALUES ('date_format', 'DD/MM/YYYY'); diff --git a/src-tauri/src/database/fixtures/consolidated_v0.8.4.sql b/src-tauri/src/database/fixtures/consolidated_v0.8.4.sql new file mode 100644 index 0000000..cb593ec --- /dev/null +++ b/src-tauri/src/database/fixtures/consolidated_v0.8.4.sql @@ -0,0 +1,657 @@ +-- Consolidated schema for new profile databases +-- This file bakes in the base schema + all migrations (v3-v8) and pre-seeds +-- the v1 IPC-aligned category taxonomy so that brand-new profiles immediately +-- use the new standard. Existing profiles keep their v2 seed and are only +-- tagged via the migration (categories_schema_version = 'v2'). +-- Used ONLY for initializing new profile databases (not for the default profile). + +CREATE TABLE IF NOT EXISTS import_sources ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + description TEXT, + date_format TEXT NOT NULL DEFAULT '%d/%m/%Y', + delimiter TEXT NOT NULL DEFAULT ';', + encoding TEXT NOT NULL DEFAULT 'utf-8', + column_mapping TEXT NOT NULL, + skip_lines INTEGER NOT NULL DEFAULT 0, + has_header INTEGER NOT NULL DEFAULT 1, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS imported_files ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + source_id INTEGER NOT NULL, + filename TEXT NOT NULL, + file_hash TEXT NOT NULL, + import_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + row_count INTEGER NOT NULL DEFAULT 0, + status TEXT NOT NULL DEFAULT 'completed', + notes TEXT, + FOREIGN KEY (source_id) REFERENCES import_sources(id) ON DELETE CASCADE, + UNIQUE(source_id, filename) +); + +CREATE TABLE IF NOT EXISTS categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + parent_id INTEGER, + color TEXT, + icon TEXT, + type TEXT NOT NULL DEFAULT 'expense', + is_active INTEGER NOT NULL DEFAULT 1, + is_inputable INTEGER NOT NULL DEFAULT 1, + sort_order INTEGER NOT NULL DEFAULT 0, + i18n_key TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (parent_id) REFERENCES categories(id) ON DELETE SET NULL +); + +CREATE TABLE IF NOT EXISTS suppliers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + normalized_name TEXT NOT NULL, + category_id INTEGER, + is_active INTEGER NOT NULL DEFAULT 1, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL +); + +CREATE TABLE IF NOT EXISTS keywords ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + keyword TEXT NOT NULL, + category_id INTEGER NOT NULL, + supplier_id INTEGER, + priority INTEGER NOT NULL DEFAULT 0, + is_active INTEGER NOT NULL DEFAULT 1, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE, + FOREIGN KEY (supplier_id) REFERENCES suppliers(id) ON DELETE SET NULL, + UNIQUE(keyword, category_id) +); + +CREATE TABLE IF NOT EXISTS transactions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date DATE NOT NULL, + description TEXT NOT NULL, + amount REAL NOT NULL, + category_id INTEGER, + supplier_id INTEGER, + source_id INTEGER, + file_id INTEGER, + original_description TEXT, + notes TEXT, + is_manually_categorized INTEGER NOT NULL DEFAULT 0, + is_split INTEGER NOT NULL DEFAULT 0, + parent_transaction_id INTEGER, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL, + FOREIGN KEY (supplier_id) REFERENCES suppliers(id) ON DELETE SET NULL, + FOREIGN KEY (source_id) REFERENCES import_sources(id) ON DELETE SET NULL, + FOREIGN KEY (file_id) REFERENCES imported_files(id) ON DELETE SET NULL, + FOREIGN KEY (parent_transaction_id) REFERENCES transactions(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS adjustments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + description TEXT, + date DATE NOT NULL, + is_recurring INTEGER NOT NULL DEFAULT 0, + recurrence_rule TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS adjustment_entries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + adjustment_id INTEGER NOT NULL, + category_id INTEGER NOT NULL, + amount REAL NOT NULL, + description TEXT, + FOREIGN KEY (adjustment_id) REFERENCES adjustments(id) ON DELETE CASCADE, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS budget_entries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + category_id INTEGER NOT NULL, + year INTEGER NOT NULL, + month INTEGER NOT NULL, + amount REAL NOT NULL, + notes TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE, + UNIQUE(category_id, year, month) +); + +CREATE TABLE IF NOT EXISTS budget_templates ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + description TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS budget_template_entries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + template_id INTEGER NOT NULL, + category_id INTEGER NOT NULL, + amount REAL NOT NULL, + FOREIGN KEY (template_id) REFERENCES budget_templates(id) ON DELETE CASCADE, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE, + UNIQUE(template_id, category_id) +); + +CREATE TABLE IF NOT EXISTS import_config_templates ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + delimiter TEXT NOT NULL DEFAULT ';', + encoding TEXT NOT NULL DEFAULT 'utf-8', + date_format TEXT NOT NULL DEFAULT 'DD/MM/YYYY', + skip_lines INTEGER NOT NULL DEFAULT 0, + has_header INTEGER NOT NULL DEFAULT 1, + column_mapping TEXT NOT NULL, + amount_mode TEXT NOT NULL DEFAULT 'single', + sign_convention TEXT NOT NULL DEFAULT 'negative_expense', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS user_preferences ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Indexes +CREATE INDEX IF NOT EXISTS idx_transactions_date ON transactions(date); +CREATE INDEX IF NOT EXISTS idx_transactions_category ON transactions(category_id); +CREATE INDEX IF NOT EXISTS idx_transactions_supplier ON transactions(supplier_id); +CREATE INDEX IF NOT EXISTS idx_transactions_source ON transactions(source_id); +CREATE INDEX IF NOT EXISTS idx_transactions_file ON transactions(file_id); +CREATE INDEX IF NOT EXISTS idx_transactions_parent ON transactions(parent_transaction_id); +CREATE INDEX IF NOT EXISTS idx_categories_parent ON categories(parent_id); +CREATE INDEX IF NOT EXISTS idx_categories_type ON categories(type); +CREATE INDEX IF NOT EXISTS idx_suppliers_category ON suppliers(category_id); +CREATE INDEX IF NOT EXISTS idx_suppliers_normalized ON suppliers(normalized_name); +CREATE INDEX IF NOT EXISTS idx_keywords_category ON keywords(category_id); +CREATE INDEX IF NOT EXISTS idx_keywords_keyword ON keywords(keyword); +CREATE INDEX IF NOT EXISTS idx_budget_entries_period ON budget_entries(year, month); +CREATE INDEX IF NOT EXISTS idx_adjustment_entries_adjustment ON adjustment_entries(adjustment_id); +CREATE INDEX IF NOT EXISTS idx_imported_files_source ON imported_files(source_id); + +-- Default preferences (new profiles ship with the v1 IPC taxonomy) +INSERT OR IGNORE INTO user_preferences (key, value) VALUES ('language', 'fr'); +INSERT OR IGNORE INTO user_preferences (key, value) VALUES ('theme', 'light'); +INSERT OR IGNORE INTO user_preferences (key, value) VALUES ('currency', 'EUR'); +INSERT OR IGNORE INTO user_preferences (key, value) VALUES ('date_format', 'DD/MM/YYYY'); +INSERT OR REPLACE INTO user_preferences (key, value) VALUES ('categories_schema_version', 'v1'); + +-- ============================================================================ +-- Seed v1 — IPC Statistique Canada-aligned, 3 levels, Canada/Québec +-- ---------------------------------------------------------------------------- +-- Reset any pre-existing category data (possible when tauri-plugin-sql runs +-- the historical v2 seed migration on this fresh DB before this script +-- executes). Keywords/categories are wiped and re-inserted with the v1 IDs +-- (1000+ range) so existing references in migrations v3-v7 stay inert. +-- ============================================================================ +DELETE FROM keywords; +UPDATE transactions SET category_id = NULL; +DELETE FROM categories; + +-- LEVEL 1 — Roots (9 IPC components + Revenus + Transferts) +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1000, 'Revenus', NULL, 'income', '#16a34a', 0, 1, 'categoriesSeed.revenus.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1100, 'Alimentation', NULL, 'expense', '#ea580c', 0, 2, 'categoriesSeed.alimentation.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1200, 'Logement', NULL, 'expense', '#dc2626', 0, 3, 'categoriesSeed.logement.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1300, 'Ménage & ameublement', NULL, 'expense', '#ca8a04', 0, 4, 'categoriesSeed.menage.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1400, 'Vêtements & chaussures', NULL, 'expense', '#d946ef', 0, 5, 'categoriesSeed.vetements.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1500, 'Transport', NULL, 'expense', '#2563eb', 0, 6, 'categoriesSeed.transport.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1600, 'Santé & soins personnels', NULL, 'expense', '#f43f5e', 0, 7, 'categoriesSeed.sante.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1700, 'Loisirs, formation & lecture', NULL, 'expense', '#8b5cf6', 0, 8, 'categoriesSeed.loisirs.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1800, 'Boissons, tabac & cannabis', NULL, 'expense', '#7c3aed', 0, 9, 'categoriesSeed.consommation.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1900, 'Finances & obligations', NULL, 'expense', '#6b7280', 0, 10, 'categoriesSeed.finances.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1950, 'Transferts & placements', NULL, 'transfer', '#0ea5e9', 0, 11, 'categoriesSeed.transferts.root'); + +-- 1000 — Revenus +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1010, 'Emploi', 1000, 'income', '#22c55e', 0, 1, 'categoriesSeed.revenus.emploi.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1011, 'Paie régulière', 1010, 'income', '#22c55e', 1, 1, 'categoriesSeed.revenus.emploi.paie'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1012, 'Primes & bonus', 1010, 'income', '#4ade80', 1, 2, 'categoriesSeed.revenus.emploi.primes'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1013, 'Travail autonome', 1010, 'income', '#86efac', 1, 3, 'categoriesSeed.revenus.emploi.autonome'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1020, 'Gouvernemental', 1000, 'income', '#16a34a', 0, 2, 'categoriesSeed.revenus.gouvernemental.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1021, 'Remboursement impôt', 1020, 'income', '#16a34a', 1, 1, 'categoriesSeed.revenus.gouvernemental.remboursementImpot'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1022, 'Allocations familiales', 1020, 'income', '#15803d', 1, 2, 'categoriesSeed.revenus.gouvernemental.allocations'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1023, 'Crédits TPS/TVQ', 1020, 'income', '#166534', 1, 3, 'categoriesSeed.revenus.gouvernemental.credits'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1024, 'Assurance-emploi / RQAP', 1020, 'income', '#14532d', 1, 4, 'categoriesSeed.revenus.gouvernemental.assuranceEmploi'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1030, 'Revenus de placement', 1000, 'income', '#10b981', 0, 3, 'categoriesSeed.revenus.placement.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1031, 'Intérêts & dividendes', 1030, 'income', '#10b981', 1, 1, 'categoriesSeed.revenus.placement.interets'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1032, 'Gains en capital', 1030, 'income', '#059669', 1, 2, 'categoriesSeed.revenus.placement.capital'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1033, 'Revenus locatifs', 1030, 'income', '#047857', 1, 3, 'categoriesSeed.revenus.placement.locatifs'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1090, 'Autres revenus', 1000, 'income', '#84cc16', 1, 9, 'categoriesSeed.revenus.autres'); + +-- 1100 — Alimentation +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1110, 'Épicerie & marché', 1100, 'expense', '#ea580c', 0, 1, 'categoriesSeed.alimentation.epicerie.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1111, 'Épicerie régulière', 1110, 'expense', '#ea580c', 1, 1, 'categoriesSeed.alimentation.epicerie.reguliere'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1112, 'Boucherie & poissonnerie', 1110, 'expense', '#c2410c', 1, 2, 'categoriesSeed.alimentation.epicerie.boucherie'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1113, 'Boulangerie & pâtisserie', 1110, 'expense', '#9a3412', 1, 3, 'categoriesSeed.alimentation.epicerie.boulangerie'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1114, 'Dépanneur', 1110, 'expense', '#7c2d12', 1, 4, 'categoriesSeed.alimentation.epicerie.depanneur'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1115, 'Marché & produits spécialisés', 1110, 'expense', '#fb923c', 1, 5, 'categoriesSeed.alimentation.epicerie.marche'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1120, 'Restauration', 1100, 'expense', '#f97316', 0, 2, 'categoriesSeed.alimentation.restauration.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1121, 'Restaurant', 1120, 'expense', '#f97316', 1, 1, 'categoriesSeed.alimentation.restauration.restaurant'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1122, 'Café & boulangerie rapide', 1120, 'expense', '#fb923c', 1, 2, 'categoriesSeed.alimentation.restauration.cafe'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1123, 'Restauration rapide', 1120, 'expense', '#fdba74', 1, 3, 'categoriesSeed.alimentation.restauration.fastfood'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1124, 'Livraison à domicile', 1120, 'expense', '#fed7aa', 1, 4, 'categoriesSeed.alimentation.restauration.livraison'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1125, 'Cantine & cafétéria', 1120, 'expense', '#ffedd5', 1, 5, 'categoriesSeed.alimentation.restauration.cantine'); + +-- 1200 — Logement +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1210, 'Habitation principale', 1200, 'expense', '#dc2626', 0, 1, 'categoriesSeed.logement.habitation.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1211, 'Loyer', 1210, 'expense', '#dc2626', 1, 1, 'categoriesSeed.logement.habitation.loyer'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1212, 'Hypothèque', 1210, 'expense', '#b91c1c', 1, 2, 'categoriesSeed.logement.habitation.hypotheque'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1213, 'Taxes municipales & scolaires', 1210, 'expense', '#991b1b', 1, 3, 'categoriesSeed.logement.habitation.taxes'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1214, 'Charges de copropriété', 1210, 'expense', '#7f1d1d', 1, 4, 'categoriesSeed.logement.habitation.copropriete'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1220, 'Services publics', 1200, 'expense', '#ef4444', 0, 2, 'categoriesSeed.logement.services.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1221, 'Électricité', 1220, 'expense', '#ef4444', 1, 1, 'categoriesSeed.logement.services.electricite'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1222, 'Gaz naturel', 1220, 'expense', '#f87171', 1, 2, 'categoriesSeed.logement.services.gaz'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1223, 'Chauffage (mazout, propane)', 1220, 'expense', '#fca5a5', 1, 3, 'categoriesSeed.logement.services.chauffage'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1224, 'Eau & égouts', 1220, 'expense', '#fecaca', 1, 4, 'categoriesSeed.logement.services.eau'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1230, 'Communications', 1200, 'expense', '#6366f1', 0, 3, 'categoriesSeed.logement.communications.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1231, 'Internet résidentiel', 1230, 'expense', '#6366f1', 1, 1, 'categoriesSeed.logement.communications.internet'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1232, 'Téléphonie mobile', 1230, 'expense', '#818cf8', 1, 2, 'categoriesSeed.logement.communications.mobile'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1233, 'Téléphonie résidentielle', 1230, 'expense', '#a5b4fc', 1, 3, 'categoriesSeed.logement.communications.residentielle'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1234, 'Câblodistribution & streaming TV', 1230, 'expense', '#c7d2fe', 1, 4, 'categoriesSeed.logement.communications.streamingTv'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1240, 'Entretien & réparations', 1200, 'expense', '#e11d48', 0, 4, 'categoriesSeed.logement.entretien.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1241, 'Entretien général', 1240, 'expense', '#e11d48', 1, 1, 'categoriesSeed.logement.entretien.general'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1242, 'Rénovations', 1240, 'expense', '#be123c', 1, 2, 'categoriesSeed.logement.entretien.renovations'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1243, 'Matériaux & outils', 1240, 'expense', '#9f1239', 1, 3, 'categoriesSeed.logement.entretien.materiaux'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1244, 'Aménagement paysager', 1240, 'expense', '#881337', 1, 4, 'categoriesSeed.logement.entretien.paysager'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1250, 'Assurance habitation', 1200, 'expense', '#14b8a6', 1, 5, 'categoriesSeed.logement.assurance'); + +-- 1300 — Ménage & ameublement +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1310, 'Ameublement', 1300, 'expense', '#ca8a04', 0, 1, 'categoriesSeed.menage.ameublement.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1311, 'Meubles', 1310, 'expense', '#ca8a04', 1, 1, 'categoriesSeed.menage.ameublement.meubles'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1312, 'Électroménagers', 1310, 'expense', '#a16207', 1, 2, 'categoriesSeed.menage.ameublement.electromenagers'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1313, 'Décoration', 1310, 'expense', '#854d0e', 1, 3, 'categoriesSeed.menage.ameublement.decoration'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1320, 'Fournitures ménagères', 1300, 'expense', '#eab308', 0, 2, 'categoriesSeed.menage.fournitures.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1321, 'Produits d''entretien', 1320, 'expense', '#eab308', 1, 1, 'categoriesSeed.menage.fournitures.entretien'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1322, 'Literie & linge de maison', 1320, 'expense', '#facc15', 1, 2, 'categoriesSeed.menage.fournitures.literie'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1323, 'Vaisselle & ustensiles', 1320, 'expense', '#fde047', 1, 3, 'categoriesSeed.menage.fournitures.vaisselle'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1330, 'Services domestiques', 1300, 'expense', '#fbbf24', 0, 3, 'categoriesSeed.menage.services.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1331, 'Ménage & nettoyage', 1330, 'expense', '#fbbf24', 1, 1, 'categoriesSeed.menage.services.nettoyage'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1332, 'Buanderie & pressing', 1330, 'expense', '#fcd34d', 1, 2, 'categoriesSeed.menage.services.buanderie'); + +-- 1400 — Vêtements & chaussures +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1410, 'Vêtements adultes', 1400, 'expense', '#d946ef', 1, 1, 'categoriesSeed.vetements.adultes'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1420, 'Vêtements enfants', 1400, 'expense', '#c026d3', 1, 2, 'categoriesSeed.vetements.enfants'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1430, 'Chaussures', 1400, 'expense', '#a21caf', 1, 3, 'categoriesSeed.vetements.chaussures'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1440, 'Accessoires & bijoux', 1400, 'expense', '#86198f', 1, 4, 'categoriesSeed.vetements.accessoires'); + +-- 1500 — Transport +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1510, 'Véhicule personnel', 1500, 'expense', '#2563eb', 0, 1, 'categoriesSeed.transport.vehicule.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1511, 'Achat / location véhicule', 1510, 'expense', '#2563eb', 1, 1, 'categoriesSeed.transport.vehicule.achat'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1512, 'Essence', 1510, 'expense', '#1d4ed8', 1, 2, 'categoriesSeed.transport.vehicule.essence'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1513, 'Entretien & réparations auto', 1510, 'expense', '#1e40af', 1, 3, 'categoriesSeed.transport.vehicule.entretien'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1514, 'Immatriculation & permis', 1510, 'expense', '#1e3a8a', 1, 4, 'categoriesSeed.transport.vehicule.immatriculation'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1515, 'Stationnement & péages', 1510, 'expense', '#3b82f6', 1, 5, 'categoriesSeed.transport.vehicule.stationnement'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1516, 'Assurance auto', 1510, 'expense', '#60a5fa', 1, 6, 'categoriesSeed.transport.vehicule.assurance'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1520, 'Transport public', 1500, 'expense', '#0ea5e9', 0, 2, 'categoriesSeed.transport.public.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1521, 'Autobus & métro', 1520, 'expense', '#0ea5e9', 1, 1, 'categoriesSeed.transport.public.autobus'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1522, 'Train de banlieue', 1520, 'expense', '#0284c7', 1, 2, 'categoriesSeed.transport.public.train'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1523, 'Taxi & covoiturage', 1520, 'expense', '#0369a1', 1, 3, 'categoriesSeed.transport.public.taxi'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1530, 'Voyages longue distance', 1500, 'expense', '#38bdf8', 0, 3, 'categoriesSeed.transport.voyages.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1531, 'Avion', 1530, 'expense', '#38bdf8', 1, 1, 'categoriesSeed.transport.voyages.avion'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1532, 'Train & autocar', 1530, 'expense', '#7dd3fc', 1, 2, 'categoriesSeed.transport.voyages.trainAutocar'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1533, 'Hébergement', 1530, 'expense', '#bae6fd', 1, 3, 'categoriesSeed.transport.voyages.hebergement'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1534, 'Location véhicule voyage', 1530, 'expense', '#e0f2fe', 1, 4, 'categoriesSeed.transport.voyages.location'); + +-- 1600 — Santé & soins personnels +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1610, 'Soins médicaux', 1600, 'expense', '#f43f5e', 0, 1, 'categoriesSeed.sante.medicaux.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1611, 'Pharmacie', 1610, 'expense', '#f43f5e', 1, 1, 'categoriesSeed.sante.medicaux.pharmacie'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1612, 'Consultations médicales', 1610, 'expense', '#e11d48', 1, 2, 'categoriesSeed.sante.medicaux.consultations'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1613, 'Dentiste & orthodontiste', 1610, 'expense', '#be123c', 1, 3, 'categoriesSeed.sante.medicaux.dentiste'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1614, 'Optométrie & lunettes', 1610, 'expense', '#9f1239', 1, 4, 'categoriesSeed.sante.medicaux.optometrie'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1615, 'Thérapies (physio, psycho, etc.)', 1610, 'expense', '#881337', 1, 5, 'categoriesSeed.sante.medicaux.therapies'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1616, 'Assurance santé complémentaire', 1610, 'expense', '#fb7185', 1, 6, 'categoriesSeed.sante.medicaux.assurance'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1620, 'Soins personnels', 1600, 'expense', '#fb7185', 0, 2, 'categoriesSeed.sante.personnels.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1621, 'Coiffure & esthétique', 1620, 'expense', '#fb7185', 1, 1, 'categoriesSeed.sante.personnels.coiffure'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1622, 'Produits de soins corporels', 1620, 'expense', '#fda4af', 1, 2, 'categoriesSeed.sante.personnels.soins'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1630, 'Assurance vie & invalidité', 1600, 'expense', '#14b8a6', 1, 3, 'categoriesSeed.sante.assuranceVie'); + +-- 1700 — Loisirs, formation & lecture +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1710, 'Divertissement', 1700, 'expense', '#8b5cf6', 0, 1, 'categoriesSeed.loisirs.divertissement.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1711, 'Cinéma & spectacles', 1710, 'expense', '#8b5cf6', 1, 1, 'categoriesSeed.loisirs.divertissement.cinema'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1712, 'Jeux vidéo & consoles', 1710, 'expense', '#a78bfa', 1, 2, 'categoriesSeed.loisirs.divertissement.jeux'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1713, 'Streaming vidéo', 1710, 'expense', '#c4b5fd', 1, 3, 'categoriesSeed.loisirs.divertissement.streamingVideo'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1714, 'Streaming musique & audio', 1710, 'expense', '#ddd6fe', 1, 4, 'categoriesSeed.loisirs.divertissement.streamingMusique'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1715, 'Jouets & passe-temps', 1710, 'expense', '#ede9fe', 1, 5, 'categoriesSeed.loisirs.divertissement.jouets'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1720, 'Sports & plein air', 1700, 'expense', '#22c55e', 0, 2, 'categoriesSeed.loisirs.sports.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1721, 'Abonnements sportifs', 1720, 'expense', '#22c55e', 1, 1, 'categoriesSeed.loisirs.sports.abonnements'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1722, 'Équipement sportif', 1720, 'expense', '#4ade80', 1, 2, 'categoriesSeed.loisirs.sports.equipement'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1723, 'Parcs & activités plein air', 1720, 'expense', '#86efac', 1, 3, 'categoriesSeed.loisirs.sports.parcs'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1730, 'Formation & éducation', 1700, 'expense', '#6366f1', 0, 3, 'categoriesSeed.loisirs.formation.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1731, 'Scolarité (frais)', 1730, 'expense', '#6366f1', 1, 1, 'categoriesSeed.loisirs.formation.scolarite'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1732, 'Matériel scolaire', 1730, 'expense', '#818cf8', 1, 2, 'categoriesSeed.loisirs.formation.materiel'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1733, 'Cours & certifications', 1730, 'expense', '#a5b4fc', 1, 3, 'categoriesSeed.loisirs.formation.cours'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1734, 'Abonnements professionnels', 1730, 'expense', '#c7d2fe', 1, 4, 'categoriesSeed.loisirs.formation.abonnements'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1740, 'Lecture & médias', 1700, 'expense', '#ec4899', 0, 4, 'categoriesSeed.loisirs.lecture.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1741, 'Livres', 1740, 'expense', '#ec4899', 1, 1, 'categoriesSeed.loisirs.lecture.livres'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1742, 'Journaux & magazines', 1740, 'expense', '#f472b6', 1, 2, 'categoriesSeed.loisirs.lecture.journaux'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1750, 'Animaux de compagnie', 1700, 'expense', '#a855f7', 0, 5, 'categoriesSeed.loisirs.animaux.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1751, 'Nourriture & accessoires animaux', 1750, 'expense', '#a855f7', 1, 1, 'categoriesSeed.loisirs.animaux.nourriture'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1752, 'Vétérinaire', 1750, 'expense', '#c084fc', 1, 2, 'categoriesSeed.loisirs.animaux.veterinaire'); + +-- 1800 — Boissons, tabac & cannabis +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1810, 'Alcool (SAQ, microbrasseries)', 1800, 'expense', '#7c3aed', 1, 1, 'categoriesSeed.consommation.alcool'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1820, 'Cannabis (SQDC)', 1800, 'expense', '#6d28d9', 1, 2, 'categoriesSeed.consommation.cannabis'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1830, 'Tabac', 1800, 'expense', '#5b21b6', 1, 3, 'categoriesSeed.consommation.tabac'); + +-- 1900 — Finances & obligations +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1910, 'Frais bancaires', 1900, 'expense', '#6b7280', 0, 1, 'categoriesSeed.finances.fraisBancaires.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1911, 'Frais de compte', 1910, 'expense', '#6b7280', 1, 1, 'categoriesSeed.finances.fraisBancaires.compte'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1912, 'Intérêts & frais de crédit', 1910, 'expense', '#9ca3af', 1, 2, 'categoriesSeed.finances.fraisBancaires.interets'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1913, 'Frais de change', 1910, 'expense', '#d1d5db', 1, 3, 'categoriesSeed.finances.fraisBancaires.change'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1920, 'Impôts & taxes', 1900, 'expense', '#dc2626', 0, 2, 'categoriesSeed.finances.impots.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1921, 'Impôt fédéral', 1920, 'expense', '#dc2626', 1, 1, 'categoriesSeed.finances.impots.federal'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1922, 'Impôt provincial', 1920, 'expense', '#b91c1c', 1, 2, 'categoriesSeed.finances.impots.provincial'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1923, 'Acomptes provisionnels', 1920, 'expense', '#991b1b', 1, 3, 'categoriesSeed.finances.impots.acomptes'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1930, 'Dons & cotisations', 1900, 'expense', '#ec4899', 0, 3, 'categoriesSeed.finances.dons.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1931, 'Dons de charité', 1930, 'expense', '#ec4899', 1, 1, 'categoriesSeed.finances.dons.charite'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1932, 'Cotisations professionnelles', 1930, 'expense', '#f472b6', 1, 2, 'categoriesSeed.finances.dons.professionnelles'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1933, 'Cotisations syndicales', 1930, 'expense', '#f9a8d4', 1, 3, 'categoriesSeed.finances.dons.syndicales'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1940, 'Cadeaux', 1900, 'expense', '#f43f5e', 1, 4, 'categoriesSeed.finances.cadeaux'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1945, 'Retrait cash', 1900, 'expense', '#57534e', 1, 5, 'categoriesSeed.finances.cash'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1946, 'Achats divers non catégorisés', 1900, 'expense', '#78716c', 1, 6, 'categoriesSeed.finances.divers'); + +-- 1950 — Transferts & placements +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1960, 'Épargne & placements', 1950, 'transfer', '#0ea5e9', 0, 1, 'categoriesSeed.transferts.epargne.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1961, 'REER / RRSP', 1960, 'transfer', '#0ea5e9', 1, 1, 'categoriesSeed.transferts.epargne.reer'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1962, 'CELI / TFSA', 1960, 'transfer', '#0284c7', 1, 2, 'categoriesSeed.transferts.epargne.celi'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1963, 'REEE / RESP', 1960, 'transfer', '#0369a1', 1, 3, 'categoriesSeed.transferts.epargne.reee'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1964, 'Compte non-enregistré', 1960, 'transfer', '#075985', 1, 4, 'categoriesSeed.transferts.epargne.nonEnregistre'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1965, 'Fonds d''urgence', 1960, 'transfer', '#0c4a6e', 1, 5, 'categoriesSeed.transferts.epargne.urgence'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1970, 'Remboursement de dette', 1950, 'transfer', '#7c3aed', 0, 2, 'categoriesSeed.transferts.dette.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1971, 'Paiement carte crédit', 1970, 'transfer', '#7c3aed', 1, 1, 'categoriesSeed.transferts.dette.carteCredit'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1972, 'Remboursement prêt étudiant', 1970, 'transfer', '#8b5cf6', 1, 2, 'categoriesSeed.transferts.dette.etudiant'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1973, 'Remboursement prêt perso', 1970, 'transfer', '#a78bfa', 1, 3, 'categoriesSeed.transferts.dette.personnel'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1980, 'Transferts internes', 1950, 'transfer', '#64748b', 1, 3, 'categoriesSeed.transferts.internes'); + +-- ============================================================================ +-- Keywords — Canadian suppliers (150+ entries) +-- ============================================================================ + +-- Alimentation > Épicerie régulière (1111) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('METRO', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('IGA', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('MAXI', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SUPER C', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('LOBLAWS', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PROVIGO', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ADONIS', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('WHOLE FOODS', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('AVRIL', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('RACHELLE-BERY', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('COSTCO', 1111, 50); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('WALMART', 1111, 50); + +-- Épicerie > Boucherie (1112) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BOUCHERIE', 1112, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('POISSONNERIE', 1112, 0); + +-- Épicerie > Boulangerie (1113) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BOULANGERIE', 1113, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PATISSERIE', 1113, 0); + +-- Épicerie > Dépanneur (1114) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('COUCHE-TARD', 1114, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DEPANNEUR', 1114, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('7-ELEVEN', 1114, 0); + +-- Restauration > Restaurant (1121) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('RESTAURANT', 1121, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BRASSERIE', 1121, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BISTRO', 1121, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SUSHI', 1121, 0); + +-- Restauration > Café (1122) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('STARBUCKS', 1122, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('TIM HORTONS', 1122, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SECOND CUP', 1122, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('VAN HOUTTE', 1122, 0); + +-- Restauration > Fast food (1123) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('MCDONALD', 1123, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SUBWAY', 1123, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('A&W', 1123, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BURGER KING', 1123, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('KFC', 1123, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DOMINOS', 1123, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PIZZA HUT', 1123, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BELLE PROVINCE', 1123, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ST-HUBERT', 1123, 0); + +-- Restauration > Livraison (1124) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DOORDASH', 1124, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DD/DOORDASH', 1124, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('UBER EATS', 1124, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SKIPTHEDISHES', 1124, 0); + +-- Logement > Hypothèque (1212) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('MTG/HYP', 1212, 0); + +-- Logement > Taxes municipales (1213) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('M-ST-HILAIRE TX', 1213, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('TAXES MUNICIPALES', 1213, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CSS PATRIOT', 1213, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('TAXE SCOLAIRE', 1213, 0); + +-- Électricité (1221) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('HYDRO-QUEBEC', 1221, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('HYDRO QUEBEC', 1221, 0); + +-- Gaz (1222) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ENERGIR', 1222, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('GAZ METRO', 1222, 0); + +-- Internet (1231) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('VIDEOTRON', 1231, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BELL INTERNET', 1231, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ORICOM', 1231, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('COGECO', 1231, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('EBOX', 1231, 0); + +-- Mobile (1232) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('FIZZ', 1232, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('KOODO', 1232, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PUBLIC MOBILE', 1232, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('VIRGIN', 1232, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BELL MOBILITE', 1232, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('TELUS MOBILE', 1232, 0); + +-- Entretien maison (1241) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('IKEA', 1241, 0); + +-- Matériaux & outils (1243) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CANADIAN TIRE', 1243, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CANAC', 1243, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('RONA', 1243, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('HOME DEPOT', 1243, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BMR', 1243, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PRINCESS AUTO', 1243, 0); + +-- Assurance habitation (1250) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BELAIR', 1250, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PRYSM', 1250, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('INTACT ASSURANCE', 1250, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DESJARDINS ASSURANCE', 1250, 0); + +-- Meubles (1311) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('TANGUAY', 1311, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('LEON', 1311, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('STRUCTUBE', 1311, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BRICK', 1311, 0); + +-- Électroménagers (1312) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BEST BUY', 1312, 0); + +-- Décoration (1313) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BOUCLAIR', 1313, 0); + +-- Vêtements (1410) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('UNIQLO', 1410, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('WINNERS', 1410, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SIMONS', 1410, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('MARKS', 1410, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('H&M', 1410, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('OLD NAVY', 1410, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('GAP', 1410, 0); + +-- Transport — Essence (1512) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SHELL', 1512, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ESSO', 1512, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ULTRAMAR', 1512, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PETRO-CANADA', 1512, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PETRO CANADA', 1512, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CREVIER', 1512, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('HARNOIS', 1512, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('COUCHE-TARD ESSENCE', 1512, 10); + +-- Permis / SAAQ (1514) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SAAQ', 1514, 0); + +-- Transport public — autobus/métro (1521) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('STM', 1521, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('RTC', 1521, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('STL', 1521, 0); + +-- Train de banlieue (1522) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('GARE MONT-SAINT', 1522, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('GARE SAINT-HUBERT', 1522, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('GARE CENTRALE', 1522, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('EXO', 1522, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('REM', 1522, 0); + +-- Taxi / Uber (1523) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('UBER', 1523, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('LYFT', 1523, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('TAXI', 1523, 0); + +-- Avion (1531) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('AIR CANADA', 1531, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('WESTJET', 1531, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('AIR TRANSAT', 1531, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PORTER', 1531, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('AEROPORTS DE MONTREAL', 1531, 0); + +-- Hébergement (1533) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('AIRBNB', 1533, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('HILTON', 1533, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('MARRIOTT', 1533, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BOOKING.COM', 1533, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('NORWEGIAN CRUISE', 1533, 0); + +-- Pharmacie (1611) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('JEAN COUTU', 1611, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('FAMILIPRIX', 1611, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PHARMAPRIX', 1611, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PROXIM', 1611, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('UNIPRIX', 1611, 0); + +-- Thérapies (1615) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PHYSIOACTIF', 1615, 0); + +-- Cinéma & spectacles (1711) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CINEPLEX', 1711, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CINEMA DU PARC', 1711, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('TICKETMASTER', 1711, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CLUB SODA', 1711, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('LEPOINTDEVENTE', 1711, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('EVENBRITE', 1711, 0); + +-- Jeux vidéo (1712) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('STEAMGAMES', 1712, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PLAYSTATION', 1712, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('NINTENDO', 1712, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('XBOX', 1712, 0); + +-- Streaming vidéo (1713) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('NETFLIX', 1713, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PRIMEVIDEO', 1713, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DISNEY PLUS', 1713, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CRAVE', 1713, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('APPLE TV', 1713, 0); + +-- Streaming musique (1714) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SPOTIFY', 1714, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('APPLE MUSIC', 1714, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('YOUTUBE MUSIC', 1714, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('TIDAL', 1714, 0); + +-- Jouets & hobbies (1715) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('LEGO', 1715, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('TOYS R US', 1715, 0); + +-- Équipement sportif (1722) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('MOUNTAIN EQUIPMENT', 1722, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('LA CORDEE', 1722, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DECATHLON', 1722, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SPORTS EXPERTS', 1722, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ATMOSPHERE', 1722, 0); + +-- Parcs & activités (1723) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SEPAQ', 1723, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BLOC SHOP', 1723, 0); + +-- Lecture (1741) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('RENAUD-BRAY', 1741, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('INDIGO', 1741, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ARCHAMBAULT', 1741, 0); + +-- Animaux — nourriture (1751) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('MONDOU', 1751, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PET SMART', 1751, 0); + +-- Alcool (1810) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SAQ', 1810, 0); + +-- Cannabis (1820) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SQDC', 1820, 0); + +-- Frais bancaires (1911) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PROGRAMME PERFORMANCE', 1911, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('FRAIS MENSUELS', 1911, 0); + +-- Impôts (1921, 1922) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('GOUV. QUEBEC', 1922, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('REVENU QUEBEC', 1922, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ARC IMPOT', 1921, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CRA TAX', 1921, 0); + +-- Dons de charité (1931) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('OXFAM', 1931, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CENTRAIDE', 1931, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('FPA', 1931, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CROIX-ROUGE', 1931, 0); + +-- Cotisations professionnelles (1932) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ORDRE DES COMPTABL', 1932, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CPA CANADA', 1932, 0); + +-- Cadeaux (1940) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DANS UN JARDIN', 1940, 0); + +-- Divers (1946) — catch-all marketplace +INSERT INTO keywords (keyword, category_id, priority) VALUES ('AMAZON', 1946, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('AMZN', 1946, 0); + +-- Placements (1964) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('WS INVESTMENTS', 1964, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('WEALTHSIMPLE', 1964, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PEAK INVESTMENT', 1964, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DYNAMIC FUND', 1964, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('FIDELITY', 1964, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('AGF', 1964, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('QUESTRADE', 1964, 0); + +-- Paie (1011) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PAY/PAY', 1011, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DEPOT PAIE', 1011, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PAYROLL', 1011, 0); diff --git a/src-tauri/src/database/fixtures/consolidated_v0.9.0.sql b/src-tauri/src/database/fixtures/consolidated_v0.9.0.sql new file mode 100644 index 0000000..6341d9d --- /dev/null +++ b/src-tauri/src/database/fixtures/consolidated_v0.9.0.sql @@ -0,0 +1,747 @@ +-- Consolidated schema for new profile databases +-- This file bakes in the base schema + all migrations (v3-v8) and pre-seeds +-- the v1 IPC-aligned category taxonomy so that brand-new profiles immediately +-- use the new standard. Existing profiles keep their v2 seed and are only +-- tagged via the migration (categories_schema_version = 'v2'). +-- Used ONLY for initializing new profile databases (not for the default profile). + +CREATE TABLE IF NOT EXISTS import_sources ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + description TEXT, + date_format TEXT NOT NULL DEFAULT '%d/%m/%Y', + delimiter TEXT NOT NULL DEFAULT ';', + encoding TEXT NOT NULL DEFAULT 'utf-8', + column_mapping TEXT NOT NULL, + skip_lines INTEGER NOT NULL DEFAULT 0, + has_header INTEGER NOT NULL DEFAULT 1, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS imported_files ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + source_id INTEGER NOT NULL, + filename TEXT NOT NULL, + file_hash TEXT NOT NULL, + import_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + row_count INTEGER NOT NULL DEFAULT 0, + status TEXT NOT NULL DEFAULT 'completed', + notes TEXT, + FOREIGN KEY (source_id) REFERENCES import_sources(id) ON DELETE CASCADE, + UNIQUE(source_id, filename) +); + +CREATE TABLE IF NOT EXISTS categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + parent_id INTEGER, + color TEXT, + icon TEXT, + type TEXT NOT NULL DEFAULT 'expense', + is_active INTEGER NOT NULL DEFAULT 1, + is_inputable INTEGER NOT NULL DEFAULT 1, + sort_order INTEGER NOT NULL DEFAULT 0, + i18n_key TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (parent_id) REFERENCES categories(id) ON DELETE SET NULL +); + +CREATE TABLE IF NOT EXISTS suppliers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + normalized_name TEXT NOT NULL, + category_id INTEGER, + is_active INTEGER NOT NULL DEFAULT 1, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL +); + +CREATE TABLE IF NOT EXISTS keywords ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + keyword TEXT NOT NULL, + category_id INTEGER NOT NULL, + supplier_id INTEGER, + priority INTEGER NOT NULL DEFAULT 0, + is_active INTEGER NOT NULL DEFAULT 1, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE, + FOREIGN KEY (supplier_id) REFERENCES suppliers(id) ON DELETE SET NULL, + UNIQUE(keyword, category_id) +); + +CREATE TABLE IF NOT EXISTS transactions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date DATE NOT NULL, + description TEXT NOT NULL, + amount REAL NOT NULL, + category_id INTEGER, + supplier_id INTEGER, + source_id INTEGER, + file_id INTEGER, + original_description TEXT, + notes TEXT, + is_manually_categorized INTEGER NOT NULL DEFAULT 0, + is_split INTEGER NOT NULL DEFAULT 0, + parent_transaction_id INTEGER, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL, + FOREIGN KEY (supplier_id) REFERENCES suppliers(id) ON DELETE SET NULL, + FOREIGN KEY (source_id) REFERENCES import_sources(id) ON DELETE SET NULL, + FOREIGN KEY (file_id) REFERENCES imported_files(id) ON DELETE SET NULL, + FOREIGN KEY (parent_transaction_id) REFERENCES transactions(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS adjustments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + description TEXT, + date DATE NOT NULL, + is_recurring INTEGER NOT NULL DEFAULT 0, + recurrence_rule TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS adjustment_entries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + adjustment_id INTEGER NOT NULL, + category_id INTEGER NOT NULL, + amount REAL NOT NULL, + description TEXT, + FOREIGN KEY (adjustment_id) REFERENCES adjustments(id) ON DELETE CASCADE, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS budget_entries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + category_id INTEGER NOT NULL, + year INTEGER NOT NULL, + month INTEGER NOT NULL, + amount REAL NOT NULL, + notes TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE, + UNIQUE(category_id, year, month) +); + +CREATE TABLE IF NOT EXISTS budget_templates ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + description TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS budget_template_entries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + template_id INTEGER NOT NULL, + category_id INTEGER NOT NULL, + amount REAL NOT NULL, + FOREIGN KEY (template_id) REFERENCES budget_templates(id) ON DELETE CASCADE, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE, + UNIQUE(template_id, category_id) +); + +CREATE TABLE IF NOT EXISTS import_config_templates ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + delimiter TEXT NOT NULL DEFAULT ';', + encoding TEXT NOT NULL DEFAULT 'utf-8', + date_format TEXT NOT NULL DEFAULT 'DD/MM/YYYY', + skip_lines INTEGER NOT NULL DEFAULT 0, + has_header INTEGER NOT NULL DEFAULT 1, + column_mapping TEXT NOT NULL, + amount_mode TEXT NOT NULL DEFAULT 'single', + sign_convention TEXT NOT NULL DEFAULT 'negative_expense', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS user_preferences ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Indexes +CREATE INDEX IF NOT EXISTS idx_transactions_date ON transactions(date); +CREATE INDEX IF NOT EXISTS idx_transactions_category ON transactions(category_id); +CREATE INDEX IF NOT EXISTS idx_transactions_supplier ON transactions(supplier_id); +CREATE INDEX IF NOT EXISTS idx_transactions_source ON transactions(source_id); +CREATE INDEX IF NOT EXISTS idx_transactions_file ON transactions(file_id); +CREATE INDEX IF NOT EXISTS idx_transactions_parent ON transactions(parent_transaction_id); +CREATE INDEX IF NOT EXISTS idx_categories_parent ON categories(parent_id); +CREATE INDEX IF NOT EXISTS idx_categories_type ON categories(type); +CREATE INDEX IF NOT EXISTS idx_suppliers_category ON suppliers(category_id); +CREATE INDEX IF NOT EXISTS idx_suppliers_normalized ON suppliers(normalized_name); +CREATE INDEX IF NOT EXISTS idx_keywords_category ON keywords(category_id); +CREATE INDEX IF NOT EXISTS idx_keywords_keyword ON keywords(keyword); +CREATE INDEX IF NOT EXISTS idx_budget_entries_period ON budget_entries(year, month); +CREATE INDEX IF NOT EXISTS idx_adjustment_entries_adjustment ON adjustment_entries(adjustment_id); +CREATE INDEX IF NOT EXISTS idx_imported_files_source ON imported_files(source_id); + +-- ============================================================================ +-- Balance sheet (Bilan) — Migration v9 mirror for new profiles +-- ============================================================================ +-- 5 tables + 7 indexes + seeded categories. Kept in sync with +-- `balance_schema.sql` (the source of truth applied by Migration v9 in lib.rs). +-- New profiles created from this consolidated schema get the balance feature +-- preinstalled without needing to replay v9 separately. + +CREATE TABLE IF NOT EXISTS balance_categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + key TEXT NOT NULL UNIQUE, + i18n_key TEXT NOT NULL, + kind TEXT NOT NULL CHECK(kind IN ('simple','priced')), + sort_order INTEGER NOT NULL DEFAULT 0, + is_active INTEGER NOT NULL DEFAULT 1, + is_seed INTEGER NOT NULL DEFAULT 0, + asset_type TEXT CHECK(asset_type IS NULL OR asset_type IN ('stock','crypto')) +); + +CREATE TABLE IF NOT EXISTS balance_accounts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + balance_category_id INTEGER NOT NULL, + name TEXT NOT NULL, + symbol TEXT, + currency TEXT NOT NULL DEFAULT 'CAD' CHECK(currency = 'CAD'), + notes TEXT, + is_active INTEGER NOT NULL DEFAULT 1, + archived_at DATETIME, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (balance_category_id) REFERENCES balance_categories(id) ON DELETE RESTRICT +); + +CREATE TABLE IF NOT EXISTS balance_snapshots ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + snapshot_date DATE NOT NULL UNIQUE, + notes TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS balance_snapshot_lines ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + snapshot_id INTEGER NOT NULL, + account_id INTEGER NOT NULL, + quantity REAL, + unit_price REAL, + value REAL NOT NULL, + price_source TEXT, + price_fetched_at DATETIME, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (snapshot_id) REFERENCES balance_snapshots(id) ON DELETE CASCADE, + FOREIGN KEY (account_id) REFERENCES balance_accounts(id) ON DELETE RESTRICT, + UNIQUE(snapshot_id, account_id), + CHECK ( + (quantity IS NULL AND unit_price IS NULL) + OR (quantity IS NOT NULL AND unit_price IS NOT NULL) + ) +); + +CREATE TABLE IF NOT EXISTS balance_account_transfers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + account_id INTEGER NOT NULL, + transaction_id INTEGER NOT NULL, + direction TEXT NOT NULL CHECK(direction IN ('in','out')), + notes TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (account_id) REFERENCES balance_accounts(id) ON DELETE CASCADE, + FOREIGN KEY (transaction_id) REFERENCES transactions(id) ON DELETE RESTRICT, + UNIQUE(transaction_id, account_id) +); + +CREATE INDEX IF NOT EXISTS idx_balance_accounts_category ON balance_accounts(balance_category_id); +CREATE INDEX IF NOT EXISTS idx_balance_accounts_active ON balance_accounts(is_active) WHERE is_active = 1; +CREATE INDEX IF NOT EXISTS idx_balance_snapshot_lines_snapshot ON balance_snapshot_lines(snapshot_id); +CREATE INDEX IF NOT EXISTS idx_balance_snapshot_lines_account ON balance_snapshot_lines(account_id); +CREATE INDEX IF NOT EXISTS idx_balance_account_transfers_account ON balance_account_transfers(account_id); +CREATE INDEX IF NOT EXISTS idx_balance_account_transfers_transaction ON balance_account_transfers(transaction_id); +CREATE INDEX IF NOT EXISTS idx_balance_snapshots_date ON balance_snapshots(snapshot_date); + +INSERT OR IGNORE INTO balance_categories (key, i18n_key, kind, sort_order, is_seed, asset_type) VALUES + ('cash', 'balance.category.cash', 'simple', 10, 1, NULL), + ('tfsa', 'balance.category.tfsa', 'simple', 20, 1, NULL), + ('rrsp', 'balance.category.rrsp', 'simple', 30, 1, NULL), + ('fund', 'balance.category.fund', 'simple', 40, 1, NULL), + ('other', 'balance.category.other', 'simple', 50, 1, NULL), + ('stock', 'balance.category.stock', 'priced', 60, 1, 'stock'), + ('crypto', 'balance.category.crypto', 'priced', 70, 1, 'crypto'); + +-- Default preferences (new profiles ship with the v1 IPC taxonomy) +INSERT OR IGNORE INTO user_preferences (key, value) VALUES ('language', 'fr'); +INSERT OR IGNORE INTO user_preferences (key, value) VALUES ('theme', 'light'); +INSERT OR IGNORE INTO user_preferences (key, value) VALUES ('currency', 'EUR'); +INSERT OR IGNORE INTO user_preferences (key, value) VALUES ('date_format', 'DD/MM/YYYY'); +INSERT OR REPLACE INTO user_preferences (key, value) VALUES ('categories_schema_version', 'v1'); + +-- ============================================================================ +-- Seed v1 — IPC Statistique Canada-aligned, 3 levels, Canada/Québec +-- ---------------------------------------------------------------------------- +-- Reset any pre-existing category data (possible when tauri-plugin-sql runs +-- the historical v2 seed migration on this fresh DB before this script +-- executes). Keywords/categories are wiped and re-inserted with the v1 IDs +-- (1000+ range) so existing references in migrations v3-v7 stay inert. +-- ============================================================================ +DELETE FROM keywords; +UPDATE transactions SET category_id = NULL; +DELETE FROM categories; + +-- LEVEL 1 — Roots (9 IPC components + Revenus + Transferts) +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1000, 'Revenus', NULL, 'income', '#16a34a', 0, 1, 'categoriesSeed.revenus.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1100, 'Alimentation', NULL, 'expense', '#ea580c', 0, 2, 'categoriesSeed.alimentation.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1200, 'Logement', NULL, 'expense', '#dc2626', 0, 3, 'categoriesSeed.logement.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1300, 'Ménage & ameublement', NULL, 'expense', '#ca8a04', 0, 4, 'categoriesSeed.menage.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1400, 'Vêtements & chaussures', NULL, 'expense', '#d946ef', 0, 5, 'categoriesSeed.vetements.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1500, 'Transport', NULL, 'expense', '#2563eb', 0, 6, 'categoriesSeed.transport.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1600, 'Santé & soins personnels', NULL, 'expense', '#f43f5e', 0, 7, 'categoriesSeed.sante.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1700, 'Loisirs, formation & lecture', NULL, 'expense', '#8b5cf6', 0, 8, 'categoriesSeed.loisirs.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1800, 'Boissons, tabac & cannabis', NULL, 'expense', '#7c3aed', 0, 9, 'categoriesSeed.consommation.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1900, 'Finances & obligations', NULL, 'expense', '#6b7280', 0, 10, 'categoriesSeed.finances.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1950, 'Transferts & placements', NULL, 'transfer', '#0ea5e9', 0, 11, 'categoriesSeed.transferts.root'); + +-- 1000 — Revenus +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1010, 'Emploi', 1000, 'income', '#22c55e', 0, 1, 'categoriesSeed.revenus.emploi.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1011, 'Paie régulière', 1010, 'income', '#22c55e', 1, 1, 'categoriesSeed.revenus.emploi.paie'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1012, 'Primes & bonus', 1010, 'income', '#4ade80', 1, 2, 'categoriesSeed.revenus.emploi.primes'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1013, 'Travail autonome', 1010, 'income', '#86efac', 1, 3, 'categoriesSeed.revenus.emploi.autonome'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1020, 'Gouvernemental', 1000, 'income', '#16a34a', 0, 2, 'categoriesSeed.revenus.gouvernemental.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1021, 'Remboursement impôt', 1020, 'income', '#16a34a', 1, 1, 'categoriesSeed.revenus.gouvernemental.remboursementImpot'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1022, 'Allocations familiales', 1020, 'income', '#15803d', 1, 2, 'categoriesSeed.revenus.gouvernemental.allocations'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1023, 'Crédits TPS/TVQ', 1020, 'income', '#166534', 1, 3, 'categoriesSeed.revenus.gouvernemental.credits'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1024, 'Assurance-emploi / RQAP', 1020, 'income', '#14532d', 1, 4, 'categoriesSeed.revenus.gouvernemental.assuranceEmploi'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1030, 'Revenus de placement', 1000, 'income', '#10b981', 0, 3, 'categoriesSeed.revenus.placement.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1031, 'Intérêts & dividendes', 1030, 'income', '#10b981', 1, 1, 'categoriesSeed.revenus.placement.interets'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1032, 'Gains en capital', 1030, 'income', '#059669', 1, 2, 'categoriesSeed.revenus.placement.capital'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1033, 'Revenus locatifs', 1030, 'income', '#047857', 1, 3, 'categoriesSeed.revenus.placement.locatifs'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1090, 'Autres revenus', 1000, 'income', '#84cc16', 1, 9, 'categoriesSeed.revenus.autres'); + +-- 1100 — Alimentation +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1110, 'Épicerie & marché', 1100, 'expense', '#ea580c', 0, 1, 'categoriesSeed.alimentation.epicerie.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1111, 'Épicerie régulière', 1110, 'expense', '#ea580c', 1, 1, 'categoriesSeed.alimentation.epicerie.reguliere'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1112, 'Boucherie & poissonnerie', 1110, 'expense', '#c2410c', 1, 2, 'categoriesSeed.alimentation.epicerie.boucherie'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1113, 'Boulangerie & pâtisserie', 1110, 'expense', '#9a3412', 1, 3, 'categoriesSeed.alimentation.epicerie.boulangerie'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1114, 'Dépanneur', 1110, 'expense', '#7c2d12', 1, 4, 'categoriesSeed.alimentation.epicerie.depanneur'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1115, 'Marché & produits spécialisés', 1110, 'expense', '#fb923c', 1, 5, 'categoriesSeed.alimentation.epicerie.marche'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1120, 'Restauration', 1100, 'expense', '#f97316', 0, 2, 'categoriesSeed.alimentation.restauration.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1121, 'Restaurant', 1120, 'expense', '#f97316', 1, 1, 'categoriesSeed.alimentation.restauration.restaurant'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1122, 'Café & boulangerie rapide', 1120, 'expense', '#fb923c', 1, 2, 'categoriesSeed.alimentation.restauration.cafe'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1123, 'Restauration rapide', 1120, 'expense', '#fdba74', 1, 3, 'categoriesSeed.alimentation.restauration.fastfood'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1124, 'Livraison à domicile', 1120, 'expense', '#fed7aa', 1, 4, 'categoriesSeed.alimentation.restauration.livraison'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1125, 'Cantine & cafétéria', 1120, 'expense', '#ffedd5', 1, 5, 'categoriesSeed.alimentation.restauration.cantine'); + +-- 1200 — Logement +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1210, 'Habitation principale', 1200, 'expense', '#dc2626', 0, 1, 'categoriesSeed.logement.habitation.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1211, 'Loyer', 1210, 'expense', '#dc2626', 1, 1, 'categoriesSeed.logement.habitation.loyer'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1212, 'Hypothèque', 1210, 'expense', '#b91c1c', 1, 2, 'categoriesSeed.logement.habitation.hypotheque'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1213, 'Taxes municipales & scolaires', 1210, 'expense', '#991b1b', 1, 3, 'categoriesSeed.logement.habitation.taxes'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1214, 'Charges de copropriété', 1210, 'expense', '#7f1d1d', 1, 4, 'categoriesSeed.logement.habitation.copropriete'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1220, 'Services publics', 1200, 'expense', '#ef4444', 0, 2, 'categoriesSeed.logement.services.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1221, 'Électricité', 1220, 'expense', '#ef4444', 1, 1, 'categoriesSeed.logement.services.electricite'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1222, 'Gaz naturel', 1220, 'expense', '#f87171', 1, 2, 'categoriesSeed.logement.services.gaz'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1223, 'Chauffage (mazout, propane)', 1220, 'expense', '#fca5a5', 1, 3, 'categoriesSeed.logement.services.chauffage'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1224, 'Eau & égouts', 1220, 'expense', '#fecaca', 1, 4, 'categoriesSeed.logement.services.eau'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1230, 'Communications', 1200, 'expense', '#6366f1', 0, 3, 'categoriesSeed.logement.communications.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1231, 'Internet résidentiel', 1230, 'expense', '#6366f1', 1, 1, 'categoriesSeed.logement.communications.internet'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1232, 'Téléphonie mobile', 1230, 'expense', '#818cf8', 1, 2, 'categoriesSeed.logement.communications.mobile'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1233, 'Téléphonie résidentielle', 1230, 'expense', '#a5b4fc', 1, 3, 'categoriesSeed.logement.communications.residentielle'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1234, 'Câblodistribution & streaming TV', 1230, 'expense', '#c7d2fe', 1, 4, 'categoriesSeed.logement.communications.streamingTv'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1240, 'Entretien & réparations', 1200, 'expense', '#e11d48', 0, 4, 'categoriesSeed.logement.entretien.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1241, 'Entretien général', 1240, 'expense', '#e11d48', 1, 1, 'categoriesSeed.logement.entretien.general'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1242, 'Rénovations', 1240, 'expense', '#be123c', 1, 2, 'categoriesSeed.logement.entretien.renovations'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1243, 'Matériaux & outils', 1240, 'expense', '#9f1239', 1, 3, 'categoriesSeed.logement.entretien.materiaux'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1244, 'Aménagement paysager', 1240, 'expense', '#881337', 1, 4, 'categoriesSeed.logement.entretien.paysager'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1250, 'Assurance habitation', 1200, 'expense', '#14b8a6', 1, 5, 'categoriesSeed.logement.assurance'); + +-- 1300 — Ménage & ameublement +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1310, 'Ameublement', 1300, 'expense', '#ca8a04', 0, 1, 'categoriesSeed.menage.ameublement.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1311, 'Meubles', 1310, 'expense', '#ca8a04', 1, 1, 'categoriesSeed.menage.ameublement.meubles'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1312, 'Électroménagers', 1310, 'expense', '#a16207', 1, 2, 'categoriesSeed.menage.ameublement.electromenagers'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1313, 'Décoration', 1310, 'expense', '#854d0e', 1, 3, 'categoriesSeed.menage.ameublement.decoration'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1320, 'Fournitures ménagères', 1300, 'expense', '#eab308', 0, 2, 'categoriesSeed.menage.fournitures.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1321, 'Produits d''entretien', 1320, 'expense', '#eab308', 1, 1, 'categoriesSeed.menage.fournitures.entretien'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1322, 'Literie & linge de maison', 1320, 'expense', '#facc15', 1, 2, 'categoriesSeed.menage.fournitures.literie'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1323, 'Vaisselle & ustensiles', 1320, 'expense', '#fde047', 1, 3, 'categoriesSeed.menage.fournitures.vaisselle'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1330, 'Services domestiques', 1300, 'expense', '#fbbf24', 0, 3, 'categoriesSeed.menage.services.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1331, 'Ménage & nettoyage', 1330, 'expense', '#fbbf24', 1, 1, 'categoriesSeed.menage.services.nettoyage'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1332, 'Buanderie & pressing', 1330, 'expense', '#fcd34d', 1, 2, 'categoriesSeed.menage.services.buanderie'); + +-- 1400 — Vêtements & chaussures +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1410, 'Vêtements adultes', 1400, 'expense', '#d946ef', 1, 1, 'categoriesSeed.vetements.adultes'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1420, 'Vêtements enfants', 1400, 'expense', '#c026d3', 1, 2, 'categoriesSeed.vetements.enfants'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1430, 'Chaussures', 1400, 'expense', '#a21caf', 1, 3, 'categoriesSeed.vetements.chaussures'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1440, 'Accessoires & bijoux', 1400, 'expense', '#86198f', 1, 4, 'categoriesSeed.vetements.accessoires'); + +-- 1500 — Transport +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1510, 'Véhicule personnel', 1500, 'expense', '#2563eb', 0, 1, 'categoriesSeed.transport.vehicule.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1511, 'Achat / location véhicule', 1510, 'expense', '#2563eb', 1, 1, 'categoriesSeed.transport.vehicule.achat'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1512, 'Essence', 1510, 'expense', '#1d4ed8', 1, 2, 'categoriesSeed.transport.vehicule.essence'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1513, 'Entretien & réparations auto', 1510, 'expense', '#1e40af', 1, 3, 'categoriesSeed.transport.vehicule.entretien'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1514, 'Immatriculation & permis', 1510, 'expense', '#1e3a8a', 1, 4, 'categoriesSeed.transport.vehicule.immatriculation'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1515, 'Stationnement & péages', 1510, 'expense', '#3b82f6', 1, 5, 'categoriesSeed.transport.vehicule.stationnement'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1516, 'Assurance auto', 1510, 'expense', '#60a5fa', 1, 6, 'categoriesSeed.transport.vehicule.assurance'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1520, 'Transport public', 1500, 'expense', '#0ea5e9', 0, 2, 'categoriesSeed.transport.public.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1521, 'Autobus & métro', 1520, 'expense', '#0ea5e9', 1, 1, 'categoriesSeed.transport.public.autobus'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1522, 'Train de banlieue', 1520, 'expense', '#0284c7', 1, 2, 'categoriesSeed.transport.public.train'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1523, 'Taxi & covoiturage', 1520, 'expense', '#0369a1', 1, 3, 'categoriesSeed.transport.public.taxi'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1530, 'Voyages longue distance', 1500, 'expense', '#38bdf8', 0, 3, 'categoriesSeed.transport.voyages.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1531, 'Avion', 1530, 'expense', '#38bdf8', 1, 1, 'categoriesSeed.transport.voyages.avion'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1532, 'Train & autocar', 1530, 'expense', '#7dd3fc', 1, 2, 'categoriesSeed.transport.voyages.trainAutocar'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1533, 'Hébergement', 1530, 'expense', '#bae6fd', 1, 3, 'categoriesSeed.transport.voyages.hebergement'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1534, 'Location véhicule voyage', 1530, 'expense', '#e0f2fe', 1, 4, 'categoriesSeed.transport.voyages.location'); + +-- 1600 — Santé & soins personnels +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1610, 'Soins médicaux', 1600, 'expense', '#f43f5e', 0, 1, 'categoriesSeed.sante.medicaux.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1611, 'Pharmacie', 1610, 'expense', '#f43f5e', 1, 1, 'categoriesSeed.sante.medicaux.pharmacie'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1612, 'Consultations médicales', 1610, 'expense', '#e11d48', 1, 2, 'categoriesSeed.sante.medicaux.consultations'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1613, 'Dentiste & orthodontiste', 1610, 'expense', '#be123c', 1, 3, 'categoriesSeed.sante.medicaux.dentiste'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1614, 'Optométrie & lunettes', 1610, 'expense', '#9f1239', 1, 4, 'categoriesSeed.sante.medicaux.optometrie'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1615, 'Thérapies (physio, psycho, etc.)', 1610, 'expense', '#881337', 1, 5, 'categoriesSeed.sante.medicaux.therapies'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1616, 'Assurance santé complémentaire', 1610, 'expense', '#fb7185', 1, 6, 'categoriesSeed.sante.medicaux.assurance'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1620, 'Soins personnels', 1600, 'expense', '#fb7185', 0, 2, 'categoriesSeed.sante.personnels.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1621, 'Coiffure & esthétique', 1620, 'expense', '#fb7185', 1, 1, 'categoriesSeed.sante.personnels.coiffure'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1622, 'Produits de soins corporels', 1620, 'expense', '#fda4af', 1, 2, 'categoriesSeed.sante.personnels.soins'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1630, 'Assurance vie & invalidité', 1600, 'expense', '#14b8a6', 1, 3, 'categoriesSeed.sante.assuranceVie'); + +-- 1700 — Loisirs, formation & lecture +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1710, 'Divertissement', 1700, 'expense', '#8b5cf6', 0, 1, 'categoriesSeed.loisirs.divertissement.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1711, 'Cinéma & spectacles', 1710, 'expense', '#8b5cf6', 1, 1, 'categoriesSeed.loisirs.divertissement.cinema'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1712, 'Jeux vidéo & consoles', 1710, 'expense', '#a78bfa', 1, 2, 'categoriesSeed.loisirs.divertissement.jeux'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1713, 'Streaming vidéo', 1710, 'expense', '#c4b5fd', 1, 3, 'categoriesSeed.loisirs.divertissement.streamingVideo'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1714, 'Streaming musique & audio', 1710, 'expense', '#ddd6fe', 1, 4, 'categoriesSeed.loisirs.divertissement.streamingMusique'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1715, 'Jouets & passe-temps', 1710, 'expense', '#ede9fe', 1, 5, 'categoriesSeed.loisirs.divertissement.jouets'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1720, 'Sports & plein air', 1700, 'expense', '#22c55e', 0, 2, 'categoriesSeed.loisirs.sports.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1721, 'Abonnements sportifs', 1720, 'expense', '#22c55e', 1, 1, 'categoriesSeed.loisirs.sports.abonnements'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1722, 'Équipement sportif', 1720, 'expense', '#4ade80', 1, 2, 'categoriesSeed.loisirs.sports.equipement'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1723, 'Parcs & activités plein air', 1720, 'expense', '#86efac', 1, 3, 'categoriesSeed.loisirs.sports.parcs'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1730, 'Formation & éducation', 1700, 'expense', '#6366f1', 0, 3, 'categoriesSeed.loisirs.formation.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1731, 'Scolarité (frais)', 1730, 'expense', '#6366f1', 1, 1, 'categoriesSeed.loisirs.formation.scolarite'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1732, 'Matériel scolaire', 1730, 'expense', '#818cf8', 1, 2, 'categoriesSeed.loisirs.formation.materiel'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1733, 'Cours & certifications', 1730, 'expense', '#a5b4fc', 1, 3, 'categoriesSeed.loisirs.formation.cours'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1734, 'Abonnements professionnels', 1730, 'expense', '#c7d2fe', 1, 4, 'categoriesSeed.loisirs.formation.abonnements'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1740, 'Lecture & médias', 1700, 'expense', '#ec4899', 0, 4, 'categoriesSeed.loisirs.lecture.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1741, 'Livres', 1740, 'expense', '#ec4899', 1, 1, 'categoriesSeed.loisirs.lecture.livres'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1742, 'Journaux & magazines', 1740, 'expense', '#f472b6', 1, 2, 'categoriesSeed.loisirs.lecture.journaux'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1750, 'Animaux de compagnie', 1700, 'expense', '#a855f7', 0, 5, 'categoriesSeed.loisirs.animaux.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1751, 'Nourriture & accessoires animaux', 1750, 'expense', '#a855f7', 1, 1, 'categoriesSeed.loisirs.animaux.nourriture'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1752, 'Vétérinaire', 1750, 'expense', '#c084fc', 1, 2, 'categoriesSeed.loisirs.animaux.veterinaire'); + +-- 1800 — Boissons, tabac & cannabis +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1810, 'Alcool (SAQ, microbrasseries)', 1800, 'expense', '#7c3aed', 1, 1, 'categoriesSeed.consommation.alcool'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1820, 'Cannabis (SQDC)', 1800, 'expense', '#6d28d9', 1, 2, 'categoriesSeed.consommation.cannabis'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1830, 'Tabac', 1800, 'expense', '#5b21b6', 1, 3, 'categoriesSeed.consommation.tabac'); + +-- 1900 — Finances & obligations +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1910, 'Frais bancaires', 1900, 'expense', '#6b7280', 0, 1, 'categoriesSeed.finances.fraisBancaires.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1911, 'Frais de compte', 1910, 'expense', '#6b7280', 1, 1, 'categoriesSeed.finances.fraisBancaires.compte'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1912, 'Intérêts & frais de crédit', 1910, 'expense', '#9ca3af', 1, 2, 'categoriesSeed.finances.fraisBancaires.interets'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1913, 'Frais de change', 1910, 'expense', '#d1d5db', 1, 3, 'categoriesSeed.finances.fraisBancaires.change'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1920, 'Impôts & taxes', 1900, 'expense', '#dc2626', 0, 2, 'categoriesSeed.finances.impots.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1921, 'Impôt fédéral', 1920, 'expense', '#dc2626', 1, 1, 'categoriesSeed.finances.impots.federal'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1922, 'Impôt provincial', 1920, 'expense', '#b91c1c', 1, 2, 'categoriesSeed.finances.impots.provincial'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1923, 'Acomptes provisionnels', 1920, 'expense', '#991b1b', 1, 3, 'categoriesSeed.finances.impots.acomptes'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1930, 'Dons & cotisations', 1900, 'expense', '#ec4899', 0, 3, 'categoriesSeed.finances.dons.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1931, 'Dons de charité', 1930, 'expense', '#ec4899', 1, 1, 'categoriesSeed.finances.dons.charite'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1932, 'Cotisations professionnelles', 1930, 'expense', '#f472b6', 1, 2, 'categoriesSeed.finances.dons.professionnelles'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1933, 'Cotisations syndicales', 1930, 'expense', '#f9a8d4', 1, 3, 'categoriesSeed.finances.dons.syndicales'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1940, 'Cadeaux', 1900, 'expense', '#f43f5e', 1, 4, 'categoriesSeed.finances.cadeaux'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1945, 'Retrait cash', 1900, 'expense', '#57534e', 1, 5, 'categoriesSeed.finances.cash'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1946, 'Achats divers non catégorisés', 1900, 'expense', '#78716c', 1, 6, 'categoriesSeed.finances.divers'); + +-- 1950 — Transferts & placements +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1960, 'Épargne & placements', 1950, 'transfer', '#0ea5e9', 0, 1, 'categoriesSeed.transferts.epargne.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1961, 'REER / RRSP', 1960, 'transfer', '#0ea5e9', 1, 1, 'categoriesSeed.transferts.epargne.reer'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1962, 'CELI / TFSA', 1960, 'transfer', '#0284c7', 1, 2, 'categoriesSeed.transferts.epargne.celi'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1963, 'REEE / RESP', 1960, 'transfer', '#0369a1', 1, 3, 'categoriesSeed.transferts.epargne.reee'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1964, 'Compte non-enregistré', 1960, 'transfer', '#075985', 1, 4, 'categoriesSeed.transferts.epargne.nonEnregistre'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1965, 'Fonds d''urgence', 1960, 'transfer', '#0c4a6e', 1, 5, 'categoriesSeed.transferts.epargne.urgence'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1970, 'Remboursement de dette', 1950, 'transfer', '#7c3aed', 0, 2, 'categoriesSeed.transferts.dette.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1971, 'Paiement carte crédit', 1970, 'transfer', '#7c3aed', 1, 1, 'categoriesSeed.transferts.dette.carteCredit'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1972, 'Remboursement prêt étudiant', 1970, 'transfer', '#8b5cf6', 1, 2, 'categoriesSeed.transferts.dette.etudiant'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1973, 'Remboursement prêt perso', 1970, 'transfer', '#a78bfa', 1, 3, 'categoriesSeed.transferts.dette.personnel'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1980, 'Transferts internes', 1950, 'transfer', '#64748b', 1, 3, 'categoriesSeed.transferts.internes'); + +-- ============================================================================ +-- Keywords — Canadian suppliers (150+ entries) +-- ============================================================================ + +-- Alimentation > Épicerie régulière (1111) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('METRO', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('IGA', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('MAXI', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SUPER C', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('LOBLAWS', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PROVIGO', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ADONIS', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('WHOLE FOODS', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('AVRIL', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('RACHELLE-BERY', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('COSTCO', 1111, 50); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('WALMART', 1111, 50); + +-- Épicerie > Boucherie (1112) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BOUCHERIE', 1112, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('POISSONNERIE', 1112, 0); + +-- Épicerie > Boulangerie (1113) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BOULANGERIE', 1113, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PATISSERIE', 1113, 0); + +-- Épicerie > Dépanneur (1114) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('COUCHE-TARD', 1114, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DEPANNEUR', 1114, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('7-ELEVEN', 1114, 0); + +-- Restauration > Restaurant (1121) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('RESTAURANT', 1121, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BRASSERIE', 1121, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BISTRO', 1121, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SUSHI', 1121, 0); + +-- Restauration > Café (1122) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('STARBUCKS', 1122, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('TIM HORTONS', 1122, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SECOND CUP', 1122, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('VAN HOUTTE', 1122, 0); + +-- Restauration > Fast food (1123) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('MCDONALD', 1123, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SUBWAY', 1123, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('A&W', 1123, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BURGER KING', 1123, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('KFC', 1123, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DOMINOS', 1123, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PIZZA HUT', 1123, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BELLE PROVINCE', 1123, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ST-HUBERT', 1123, 0); + +-- Restauration > Livraison (1124) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DOORDASH', 1124, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DD/DOORDASH', 1124, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('UBER EATS', 1124, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SKIPTHEDISHES', 1124, 0); + +-- Logement > Hypothèque (1212) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('MTG/HYP', 1212, 0); + +-- Logement > Taxes municipales (1213) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('M-ST-HILAIRE TX', 1213, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('TAXES MUNICIPALES', 1213, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CSS PATRIOT', 1213, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('TAXE SCOLAIRE', 1213, 0); + +-- Électricité (1221) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('HYDRO-QUEBEC', 1221, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('HYDRO QUEBEC', 1221, 0); + +-- Gaz (1222) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ENERGIR', 1222, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('GAZ METRO', 1222, 0); + +-- Internet (1231) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('VIDEOTRON', 1231, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BELL INTERNET', 1231, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ORICOM', 1231, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('COGECO', 1231, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('EBOX', 1231, 0); + +-- Mobile (1232) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('FIZZ', 1232, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('KOODO', 1232, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PUBLIC MOBILE', 1232, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('VIRGIN', 1232, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BELL MOBILITE', 1232, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('TELUS MOBILE', 1232, 0); + +-- Entretien maison (1241) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('IKEA', 1241, 0); + +-- Matériaux & outils (1243) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CANADIAN TIRE', 1243, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CANAC', 1243, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('RONA', 1243, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('HOME DEPOT', 1243, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BMR', 1243, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PRINCESS AUTO', 1243, 0); + +-- Assurance habitation (1250) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BELAIR', 1250, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PRYSM', 1250, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('INTACT ASSURANCE', 1250, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DESJARDINS ASSURANCE', 1250, 0); + +-- Meubles (1311) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('TANGUAY', 1311, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('LEON', 1311, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('STRUCTUBE', 1311, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BRICK', 1311, 0); + +-- Électroménagers (1312) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BEST BUY', 1312, 0); + +-- Décoration (1313) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BOUCLAIR', 1313, 0); + +-- Vêtements (1410) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('UNIQLO', 1410, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('WINNERS', 1410, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SIMONS', 1410, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('MARKS', 1410, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('H&M', 1410, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('OLD NAVY', 1410, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('GAP', 1410, 0); + +-- Transport — Essence (1512) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SHELL', 1512, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ESSO', 1512, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ULTRAMAR', 1512, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PETRO-CANADA', 1512, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PETRO CANADA', 1512, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CREVIER', 1512, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('HARNOIS', 1512, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('COUCHE-TARD ESSENCE', 1512, 10); + +-- Permis / SAAQ (1514) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SAAQ', 1514, 0); + +-- Transport public — autobus/métro (1521) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('STM', 1521, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('RTC', 1521, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('STL', 1521, 0); + +-- Train de banlieue (1522) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('GARE MONT-SAINT', 1522, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('GARE SAINT-HUBERT', 1522, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('GARE CENTRALE', 1522, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('EXO', 1522, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('REM', 1522, 0); + +-- Taxi / Uber (1523) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('UBER', 1523, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('LYFT', 1523, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('TAXI', 1523, 0); + +-- Avion (1531) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('AIR CANADA', 1531, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('WESTJET', 1531, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('AIR TRANSAT', 1531, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PORTER', 1531, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('AEROPORTS DE MONTREAL', 1531, 0); + +-- Hébergement (1533) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('AIRBNB', 1533, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('HILTON', 1533, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('MARRIOTT', 1533, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BOOKING.COM', 1533, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('NORWEGIAN CRUISE', 1533, 0); + +-- Pharmacie (1611) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('JEAN COUTU', 1611, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('FAMILIPRIX', 1611, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PHARMAPRIX', 1611, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PROXIM', 1611, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('UNIPRIX', 1611, 0); + +-- Thérapies (1615) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PHYSIOACTIF', 1615, 0); + +-- Cinéma & spectacles (1711) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CINEPLEX', 1711, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CINEMA DU PARC', 1711, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('TICKETMASTER', 1711, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CLUB SODA', 1711, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('LEPOINTDEVENTE', 1711, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('EVENBRITE', 1711, 0); + +-- Jeux vidéo (1712) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('STEAMGAMES', 1712, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PLAYSTATION', 1712, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('NINTENDO', 1712, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('XBOX', 1712, 0); + +-- Streaming vidéo (1713) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('NETFLIX', 1713, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PRIMEVIDEO', 1713, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DISNEY PLUS', 1713, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CRAVE', 1713, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('APPLE TV', 1713, 0); + +-- Streaming musique (1714) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SPOTIFY', 1714, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('APPLE MUSIC', 1714, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('YOUTUBE MUSIC', 1714, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('TIDAL', 1714, 0); + +-- Jouets & hobbies (1715) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('LEGO', 1715, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('TOYS R US', 1715, 0); + +-- Équipement sportif (1722) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('MOUNTAIN EQUIPMENT', 1722, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('LA CORDEE', 1722, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DECATHLON', 1722, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SPORTS EXPERTS', 1722, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ATMOSPHERE', 1722, 0); + +-- Parcs & activités (1723) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SEPAQ', 1723, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BLOC SHOP', 1723, 0); + +-- Lecture (1741) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('RENAUD-BRAY', 1741, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('INDIGO', 1741, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ARCHAMBAULT', 1741, 0); + +-- Animaux — nourriture (1751) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('MONDOU', 1751, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PET SMART', 1751, 0); + +-- Alcool (1810) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SAQ', 1810, 0); + +-- Cannabis (1820) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SQDC', 1820, 0); + +-- Frais bancaires (1911) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PROGRAMME PERFORMANCE', 1911, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('FRAIS MENSUELS', 1911, 0); + +-- Impôts (1921, 1922) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('GOUV. QUEBEC', 1922, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('REVENU QUEBEC', 1922, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ARC IMPOT', 1921, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CRA TAX', 1921, 0); + +-- Dons de charité (1931) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('OXFAM', 1931, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CENTRAIDE', 1931, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('FPA', 1931, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CROIX-ROUGE', 1931, 0); + +-- Cotisations professionnelles (1932) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ORDRE DES COMPTABL', 1932, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CPA CANADA', 1932, 0); + +-- Cadeaux (1940) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DANS UN JARDIN', 1940, 0); + +-- Divers (1946) — catch-all marketplace +INSERT INTO keywords (keyword, category_id, priority) VALUES ('AMAZON', 1946, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('AMZN', 1946, 0); + +-- Placements (1964) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('WS INVESTMENTS', 1964, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('WEALTHSIMPLE', 1964, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PEAK INVESTMENT', 1964, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DYNAMIC FUND', 1964, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('FIDELITY', 1964, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('AGF', 1964, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('QUESTRADE', 1964, 0); + +-- Paie (1011) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PAY/PAY', 1011, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DEPOT PAIE', 1011, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PAYROLL', 1011, 0); diff --git a/src-tauri/src/database/fixtures/consolidated_v0.9.1.sql b/src-tauri/src/database/fixtures/consolidated_v0.9.1.sql new file mode 100644 index 0000000..83a08a9 --- /dev/null +++ b/src-tauri/src/database/fixtures/consolidated_v0.9.1.sql @@ -0,0 +1,764 @@ +-- Consolidated schema for new profile databases +-- This file bakes in the base schema + all migrations (v3-v8) and pre-seeds +-- the v1 IPC-aligned category taxonomy so that brand-new profiles immediately +-- use the new standard. Existing profiles keep their v2 seed and are only +-- tagged via the migration (categories_schema_version = 'v2'). +-- Used ONLY for initializing new profile databases (not for the default profile). + +CREATE TABLE IF NOT EXISTS import_sources ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + description TEXT, + date_format TEXT NOT NULL DEFAULT '%d/%m/%Y', + delimiter TEXT NOT NULL DEFAULT ';', + encoding TEXT NOT NULL DEFAULT 'utf-8', + column_mapping TEXT NOT NULL, + skip_lines INTEGER NOT NULL DEFAULT 0, + has_header INTEGER NOT NULL DEFAULT 1, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS imported_files ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + source_id INTEGER NOT NULL, + filename TEXT NOT NULL, + file_hash TEXT NOT NULL, + import_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + row_count INTEGER NOT NULL DEFAULT 0, + status TEXT NOT NULL DEFAULT 'completed', + notes TEXT, + FOREIGN KEY (source_id) REFERENCES import_sources(id) ON DELETE CASCADE, + UNIQUE(source_id, filename) +); + +CREATE TABLE IF NOT EXISTS categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + parent_id INTEGER, + color TEXT, + icon TEXT, + type TEXT NOT NULL DEFAULT 'expense', + is_active INTEGER NOT NULL DEFAULT 1, + is_inputable INTEGER NOT NULL DEFAULT 1, + sort_order INTEGER NOT NULL DEFAULT 0, + i18n_key TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (parent_id) REFERENCES categories(id) ON DELETE SET NULL +); + +CREATE TABLE IF NOT EXISTS suppliers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + normalized_name TEXT NOT NULL, + category_id INTEGER, + is_active INTEGER NOT NULL DEFAULT 1, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL +); + +CREATE TABLE IF NOT EXISTS keywords ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + keyword TEXT NOT NULL, + category_id INTEGER NOT NULL, + supplier_id INTEGER, + priority INTEGER NOT NULL DEFAULT 0, + is_active INTEGER NOT NULL DEFAULT 1, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE, + FOREIGN KEY (supplier_id) REFERENCES suppliers(id) ON DELETE SET NULL, + UNIQUE(keyword, category_id) +); + +CREATE TABLE IF NOT EXISTS transactions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date DATE NOT NULL, + description TEXT NOT NULL, + amount REAL NOT NULL, + category_id INTEGER, + supplier_id INTEGER, + source_id INTEGER, + file_id INTEGER, + original_description TEXT, + notes TEXT, + is_manually_categorized INTEGER NOT NULL DEFAULT 0, + is_split INTEGER NOT NULL DEFAULT 0, + parent_transaction_id INTEGER, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL, + FOREIGN KEY (supplier_id) REFERENCES suppliers(id) ON DELETE SET NULL, + FOREIGN KEY (source_id) REFERENCES import_sources(id) ON DELETE SET NULL, + FOREIGN KEY (file_id) REFERENCES imported_files(id) ON DELETE SET NULL, + FOREIGN KEY (parent_transaction_id) REFERENCES transactions(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS adjustments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + description TEXT, + date DATE NOT NULL, + is_recurring INTEGER NOT NULL DEFAULT 0, + recurrence_rule TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS adjustment_entries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + adjustment_id INTEGER NOT NULL, + category_id INTEGER NOT NULL, + amount REAL NOT NULL, + description TEXT, + FOREIGN KEY (adjustment_id) REFERENCES adjustments(id) ON DELETE CASCADE, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS budget_entries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + category_id INTEGER NOT NULL, + year INTEGER NOT NULL, + month INTEGER NOT NULL, + amount REAL NOT NULL, + notes TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE, + UNIQUE(category_id, year, month) +); + +CREATE TABLE IF NOT EXISTS budget_templates ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + description TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS budget_template_entries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + template_id INTEGER NOT NULL, + category_id INTEGER NOT NULL, + amount REAL NOT NULL, + FOREIGN KEY (template_id) REFERENCES budget_templates(id) ON DELETE CASCADE, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE, + UNIQUE(template_id, category_id) +); + +CREATE TABLE IF NOT EXISTS import_config_templates ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + delimiter TEXT NOT NULL DEFAULT ';', + encoding TEXT NOT NULL DEFAULT 'utf-8', + date_format TEXT NOT NULL DEFAULT 'DD/MM/YYYY', + skip_lines INTEGER NOT NULL DEFAULT 0, + has_header INTEGER NOT NULL DEFAULT 1, + column_mapping TEXT NOT NULL, + amount_mode TEXT NOT NULL DEFAULT 'single', + sign_convention TEXT NOT NULL DEFAULT 'negative_expense', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS user_preferences ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Indexes +CREATE INDEX IF NOT EXISTS idx_transactions_date ON transactions(date); +CREATE INDEX IF NOT EXISTS idx_transactions_category ON transactions(category_id); +CREATE INDEX IF NOT EXISTS idx_transactions_supplier ON transactions(supplier_id); +CREATE INDEX IF NOT EXISTS idx_transactions_source ON transactions(source_id); +CREATE INDEX IF NOT EXISTS idx_transactions_file ON transactions(file_id); +CREATE INDEX IF NOT EXISTS idx_transactions_parent ON transactions(parent_transaction_id); +CREATE INDEX IF NOT EXISTS idx_categories_parent ON categories(parent_id); +CREATE INDEX IF NOT EXISTS idx_categories_type ON categories(type); +CREATE INDEX IF NOT EXISTS idx_suppliers_category ON suppliers(category_id); +CREATE INDEX IF NOT EXISTS idx_suppliers_normalized ON suppliers(normalized_name); +CREATE INDEX IF NOT EXISTS idx_keywords_category ON keywords(category_id); +CREATE INDEX IF NOT EXISTS idx_keywords_keyword ON keywords(keyword); +CREATE INDEX IF NOT EXISTS idx_budget_entries_period ON budget_entries(year, month); +CREATE INDEX IF NOT EXISTS idx_adjustment_entries_adjustment ON adjustment_entries(adjustment_id); +CREATE INDEX IF NOT EXISTS idx_imported_files_source ON imported_files(source_id); + +-- ============================================================================ +-- Balance sheet (Bilan) — Migration v9 mirror for new profiles +-- ============================================================================ +-- 5 tables + 7 indexes + seeded categories. Kept in sync with +-- `balance_schema.sql` (the source of truth applied by Migration v9 in lib.rs). +-- New profiles created from this consolidated schema get the balance feature +-- preinstalled without needing to replay v9 separately. + +CREATE TABLE IF NOT EXISTS balance_categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + key TEXT NOT NULL UNIQUE, + i18n_key TEXT NOT NULL, + kind TEXT NOT NULL CHECK(kind IN ('simple','priced')), + sort_order INTEGER NOT NULL DEFAULT 0, + is_active INTEGER NOT NULL DEFAULT 1, + is_seed INTEGER NOT NULL DEFAULT 0, + asset_type TEXT CHECK(asset_type IS NULL OR asset_type IN ('stock','crypto')) +); + +CREATE TABLE IF NOT EXISTS balance_accounts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + balance_category_id INTEGER NOT NULL, + name TEXT NOT NULL, + symbol TEXT, + currency TEXT NOT NULL DEFAULT 'CAD' CHECK(currency = 'CAD'), + notes TEXT, + is_active INTEGER NOT NULL DEFAULT 1, + archived_at DATETIME, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (balance_category_id) REFERENCES balance_categories(id) ON DELETE RESTRICT +); + +CREATE TABLE IF NOT EXISTS balance_snapshots ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + snapshot_date DATE NOT NULL UNIQUE, + notes TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS balance_snapshot_lines ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + snapshot_id INTEGER NOT NULL, + account_id INTEGER NOT NULL, + quantity REAL, + unit_price REAL, + value REAL NOT NULL, + price_source TEXT, + price_fetched_at DATETIME, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (snapshot_id) REFERENCES balance_snapshots(id) ON DELETE CASCADE, + FOREIGN KEY (account_id) REFERENCES balance_accounts(id) ON DELETE RESTRICT, + UNIQUE(snapshot_id, account_id), + CHECK ( + (quantity IS NULL AND unit_price IS NULL) + OR (quantity IS NOT NULL AND unit_price IS NOT NULL) + ) +); + +CREATE TABLE IF NOT EXISTS balance_account_transfers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + account_id INTEGER NOT NULL, + transaction_id INTEGER NOT NULL, + direction TEXT NOT NULL CHECK(direction IN ('in','out')), + notes TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (account_id) REFERENCES balance_accounts(id) ON DELETE CASCADE, + FOREIGN KEY (transaction_id) REFERENCES transactions(id) ON DELETE RESTRICT, + UNIQUE(transaction_id, account_id) +); + +CREATE INDEX IF NOT EXISTS idx_balance_accounts_category ON balance_accounts(balance_category_id); +CREATE INDEX IF NOT EXISTS idx_balance_accounts_active ON balance_accounts(is_active) WHERE is_active = 1; +CREATE INDEX IF NOT EXISTS idx_balance_snapshot_lines_snapshot ON balance_snapshot_lines(snapshot_id); +CREATE INDEX IF NOT EXISTS idx_balance_snapshot_lines_account ON balance_snapshot_lines(account_id); +CREATE INDEX IF NOT EXISTS idx_balance_account_transfers_account ON balance_account_transfers(account_id); +CREATE INDEX IF NOT EXISTS idx_balance_account_transfers_transaction ON balance_account_transfers(transaction_id); +CREATE INDEX IF NOT EXISTS idx_balance_snapshots_date ON balance_snapshots(snapshot_date); + +INSERT OR IGNORE INTO balance_categories (key, i18n_key, kind, sort_order, is_seed, asset_type) VALUES + ('cash', 'balance.category.cash', 'simple', 10, 1, NULL), + ('tfsa', 'balance.category.tfsa', 'simple', 20, 1, NULL), + ('rrsp', 'balance.category.rrsp', 'simple', 30, 1, NULL), + ('fund', 'balance.category.fund', 'simple', 40, 1, NULL), + ('other', 'balance.category.other', 'simple', 50, 1, NULL), + ('stock', 'balance.category.stock', 'priced', 60, 1, 'stock'), + ('crypto', 'balance.category.crypto', 'priced', 70, 1, 'crypto'); + +-- Starter accounts (Issue #179): 4 plain accounts seeded for new profiles so +-- /balance lands non-empty. They are NOT marked as seed (no is_seed column on +-- balance_accounts) — once created they are indistinguishable from +-- user-created accounts and can be renamed/archived freely. Existing profiles +-- get the same 4 proposed via StarterAccountsModal on first /balance visit. +INSERT INTO balance_accounts (balance_category_id, name, currency, is_active) VALUES + ((SELECT id FROM balance_categories WHERE key = 'cash'), 'Compte chèque', 'CAD', 1), + ((SELECT id FROM balance_categories WHERE key = 'tfsa'), 'CELI', 'CAD', 1), + ((SELECT id FROM balance_categories WHERE key = 'rrsp'), 'REER', 'CAD', 1), + ((SELECT id FROM balance_categories WHERE key = 'other'), 'Compte non-enregistré', 'CAD', 1); + +-- Default preferences (new profiles ship with the v1 IPC taxonomy) +INSERT OR IGNORE INTO user_preferences (key, value) VALUES ('language', 'fr'); +INSERT OR IGNORE INTO user_preferences (key, value) VALUES ('theme', 'light'); +INSERT OR IGNORE INTO user_preferences (key, value) VALUES ('currency', 'EUR'); +INSERT OR IGNORE INTO user_preferences (key, value) VALUES ('date_format', 'DD/MM/YYYY'); +INSERT OR REPLACE INTO user_preferences (key, value) VALUES ('categories_schema_version', 'v1'); +-- Suppress StarterAccountsModal on first /balance visit for new profiles +-- (Issue #179). The 4 starter accounts are already seeded above, so the +-- modal would only show 4 collision rows with no actionable choice. Pre- +-- writing the pref skips that briefly-empty UX entirely. Suggestion S1 +-- from PR #185 review (#187). +INSERT OR IGNORE INTO user_preferences (key, value) VALUES ('balance_starter_proposed', '{"shown_at":"seed","accepted":[]}'); + +-- ============================================================================ +-- Seed v1 — IPC Statistique Canada-aligned, 3 levels, Canada/Québec +-- ---------------------------------------------------------------------------- +-- Reset any pre-existing category data (possible when tauri-plugin-sql runs +-- the historical v2 seed migration on this fresh DB before this script +-- executes). Keywords/categories are wiped and re-inserted with the v1 IDs +-- (1000+ range) so existing references in migrations v3-v7 stay inert. +-- ============================================================================ +DELETE FROM keywords; +UPDATE transactions SET category_id = NULL; +DELETE FROM categories; + +-- LEVEL 1 — Roots (9 IPC components + Revenus + Transferts) +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1000, 'Revenus', NULL, 'income', '#16a34a', 0, 1, 'categoriesSeed.revenus.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1100, 'Alimentation', NULL, 'expense', '#ea580c', 0, 2, 'categoriesSeed.alimentation.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1200, 'Logement', NULL, 'expense', '#dc2626', 0, 3, 'categoriesSeed.logement.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1300, 'Ménage & ameublement', NULL, 'expense', '#ca8a04', 0, 4, 'categoriesSeed.menage.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1400, 'Vêtements & chaussures', NULL, 'expense', '#d946ef', 0, 5, 'categoriesSeed.vetements.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1500, 'Transport', NULL, 'expense', '#2563eb', 0, 6, 'categoriesSeed.transport.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1600, 'Santé & soins personnels', NULL, 'expense', '#f43f5e', 0, 7, 'categoriesSeed.sante.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1700, 'Loisirs, formation & lecture', NULL, 'expense', '#8b5cf6', 0, 8, 'categoriesSeed.loisirs.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1800, 'Boissons, tabac & cannabis', NULL, 'expense', '#7c3aed', 0, 9, 'categoriesSeed.consommation.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1900, 'Finances & obligations', NULL, 'expense', '#6b7280', 0, 10, 'categoriesSeed.finances.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1950, 'Transferts & placements', NULL, 'transfer', '#0ea5e9', 0, 11, 'categoriesSeed.transferts.root'); + +-- 1000 — Revenus +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1010, 'Emploi', 1000, 'income', '#22c55e', 0, 1, 'categoriesSeed.revenus.emploi.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1011, 'Paie régulière', 1010, 'income', '#22c55e', 1, 1, 'categoriesSeed.revenus.emploi.paie'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1012, 'Primes & bonus', 1010, 'income', '#4ade80', 1, 2, 'categoriesSeed.revenus.emploi.primes'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1013, 'Travail autonome', 1010, 'income', '#86efac', 1, 3, 'categoriesSeed.revenus.emploi.autonome'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1020, 'Gouvernemental', 1000, 'income', '#16a34a', 0, 2, 'categoriesSeed.revenus.gouvernemental.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1021, 'Remboursement impôt', 1020, 'income', '#16a34a', 1, 1, 'categoriesSeed.revenus.gouvernemental.remboursementImpot'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1022, 'Allocations familiales', 1020, 'income', '#15803d', 1, 2, 'categoriesSeed.revenus.gouvernemental.allocations'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1023, 'Crédits TPS/TVQ', 1020, 'income', '#166534', 1, 3, 'categoriesSeed.revenus.gouvernemental.credits'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1024, 'Assurance-emploi / RQAP', 1020, 'income', '#14532d', 1, 4, 'categoriesSeed.revenus.gouvernemental.assuranceEmploi'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1030, 'Revenus de placement', 1000, 'income', '#10b981', 0, 3, 'categoriesSeed.revenus.placement.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1031, 'Intérêts & dividendes', 1030, 'income', '#10b981', 1, 1, 'categoriesSeed.revenus.placement.interets'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1032, 'Gains en capital', 1030, 'income', '#059669', 1, 2, 'categoriesSeed.revenus.placement.capital'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1033, 'Revenus locatifs', 1030, 'income', '#047857', 1, 3, 'categoriesSeed.revenus.placement.locatifs'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1090, 'Autres revenus', 1000, 'income', '#84cc16', 1, 9, 'categoriesSeed.revenus.autres'); + +-- 1100 — Alimentation +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1110, 'Épicerie & marché', 1100, 'expense', '#ea580c', 0, 1, 'categoriesSeed.alimentation.epicerie.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1111, 'Épicerie régulière', 1110, 'expense', '#ea580c', 1, 1, 'categoriesSeed.alimentation.epicerie.reguliere'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1112, 'Boucherie & poissonnerie', 1110, 'expense', '#c2410c', 1, 2, 'categoriesSeed.alimentation.epicerie.boucherie'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1113, 'Boulangerie & pâtisserie', 1110, 'expense', '#9a3412', 1, 3, 'categoriesSeed.alimentation.epicerie.boulangerie'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1114, 'Dépanneur', 1110, 'expense', '#7c2d12', 1, 4, 'categoriesSeed.alimentation.epicerie.depanneur'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1115, 'Marché & produits spécialisés', 1110, 'expense', '#fb923c', 1, 5, 'categoriesSeed.alimentation.epicerie.marche'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1120, 'Restauration', 1100, 'expense', '#f97316', 0, 2, 'categoriesSeed.alimentation.restauration.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1121, 'Restaurant', 1120, 'expense', '#f97316', 1, 1, 'categoriesSeed.alimentation.restauration.restaurant'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1122, 'Café & boulangerie rapide', 1120, 'expense', '#fb923c', 1, 2, 'categoriesSeed.alimentation.restauration.cafe'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1123, 'Restauration rapide', 1120, 'expense', '#fdba74', 1, 3, 'categoriesSeed.alimentation.restauration.fastfood'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1124, 'Livraison à domicile', 1120, 'expense', '#fed7aa', 1, 4, 'categoriesSeed.alimentation.restauration.livraison'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1125, 'Cantine & cafétéria', 1120, 'expense', '#ffedd5', 1, 5, 'categoriesSeed.alimentation.restauration.cantine'); + +-- 1200 — Logement +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1210, 'Habitation principale', 1200, 'expense', '#dc2626', 0, 1, 'categoriesSeed.logement.habitation.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1211, 'Loyer', 1210, 'expense', '#dc2626', 1, 1, 'categoriesSeed.logement.habitation.loyer'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1212, 'Hypothèque', 1210, 'expense', '#b91c1c', 1, 2, 'categoriesSeed.logement.habitation.hypotheque'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1213, 'Taxes municipales & scolaires', 1210, 'expense', '#991b1b', 1, 3, 'categoriesSeed.logement.habitation.taxes'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1214, 'Charges de copropriété', 1210, 'expense', '#7f1d1d', 1, 4, 'categoriesSeed.logement.habitation.copropriete'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1220, 'Services publics', 1200, 'expense', '#ef4444', 0, 2, 'categoriesSeed.logement.services.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1221, 'Électricité', 1220, 'expense', '#ef4444', 1, 1, 'categoriesSeed.logement.services.electricite'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1222, 'Gaz naturel', 1220, 'expense', '#f87171', 1, 2, 'categoriesSeed.logement.services.gaz'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1223, 'Chauffage (mazout, propane)', 1220, 'expense', '#fca5a5', 1, 3, 'categoriesSeed.logement.services.chauffage'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1224, 'Eau & égouts', 1220, 'expense', '#fecaca', 1, 4, 'categoriesSeed.logement.services.eau'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1230, 'Communications', 1200, 'expense', '#6366f1', 0, 3, 'categoriesSeed.logement.communications.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1231, 'Internet résidentiel', 1230, 'expense', '#6366f1', 1, 1, 'categoriesSeed.logement.communications.internet'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1232, 'Téléphonie mobile', 1230, 'expense', '#818cf8', 1, 2, 'categoriesSeed.logement.communications.mobile'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1233, 'Téléphonie résidentielle', 1230, 'expense', '#a5b4fc', 1, 3, 'categoriesSeed.logement.communications.residentielle'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1234, 'Câblodistribution & streaming TV', 1230, 'expense', '#c7d2fe', 1, 4, 'categoriesSeed.logement.communications.streamingTv'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1240, 'Entretien & réparations', 1200, 'expense', '#e11d48', 0, 4, 'categoriesSeed.logement.entretien.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1241, 'Entretien général', 1240, 'expense', '#e11d48', 1, 1, 'categoriesSeed.logement.entretien.general'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1242, 'Rénovations', 1240, 'expense', '#be123c', 1, 2, 'categoriesSeed.logement.entretien.renovations'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1243, 'Matériaux & outils', 1240, 'expense', '#9f1239', 1, 3, 'categoriesSeed.logement.entretien.materiaux'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1244, 'Aménagement paysager', 1240, 'expense', '#881337', 1, 4, 'categoriesSeed.logement.entretien.paysager'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1250, 'Assurance habitation', 1200, 'expense', '#14b8a6', 1, 5, 'categoriesSeed.logement.assurance'); + +-- 1300 — Ménage & ameublement +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1310, 'Ameublement', 1300, 'expense', '#ca8a04', 0, 1, 'categoriesSeed.menage.ameublement.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1311, 'Meubles', 1310, 'expense', '#ca8a04', 1, 1, 'categoriesSeed.menage.ameublement.meubles'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1312, 'Électroménagers', 1310, 'expense', '#a16207', 1, 2, 'categoriesSeed.menage.ameublement.electromenagers'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1313, 'Décoration', 1310, 'expense', '#854d0e', 1, 3, 'categoriesSeed.menage.ameublement.decoration'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1320, 'Fournitures ménagères', 1300, 'expense', '#eab308', 0, 2, 'categoriesSeed.menage.fournitures.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1321, 'Produits d''entretien', 1320, 'expense', '#eab308', 1, 1, 'categoriesSeed.menage.fournitures.entretien'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1322, 'Literie & linge de maison', 1320, 'expense', '#facc15', 1, 2, 'categoriesSeed.menage.fournitures.literie'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1323, 'Vaisselle & ustensiles', 1320, 'expense', '#fde047', 1, 3, 'categoriesSeed.menage.fournitures.vaisselle'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1330, 'Services domestiques', 1300, 'expense', '#fbbf24', 0, 3, 'categoriesSeed.menage.services.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1331, 'Ménage & nettoyage', 1330, 'expense', '#fbbf24', 1, 1, 'categoriesSeed.menage.services.nettoyage'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1332, 'Buanderie & pressing', 1330, 'expense', '#fcd34d', 1, 2, 'categoriesSeed.menage.services.buanderie'); + +-- 1400 — Vêtements & chaussures +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1410, 'Vêtements adultes', 1400, 'expense', '#d946ef', 1, 1, 'categoriesSeed.vetements.adultes'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1420, 'Vêtements enfants', 1400, 'expense', '#c026d3', 1, 2, 'categoriesSeed.vetements.enfants'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1430, 'Chaussures', 1400, 'expense', '#a21caf', 1, 3, 'categoriesSeed.vetements.chaussures'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1440, 'Accessoires & bijoux', 1400, 'expense', '#86198f', 1, 4, 'categoriesSeed.vetements.accessoires'); + +-- 1500 — Transport +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1510, 'Véhicule personnel', 1500, 'expense', '#2563eb', 0, 1, 'categoriesSeed.transport.vehicule.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1511, 'Achat / location véhicule', 1510, 'expense', '#2563eb', 1, 1, 'categoriesSeed.transport.vehicule.achat'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1512, 'Essence', 1510, 'expense', '#1d4ed8', 1, 2, 'categoriesSeed.transport.vehicule.essence'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1513, 'Entretien & réparations auto', 1510, 'expense', '#1e40af', 1, 3, 'categoriesSeed.transport.vehicule.entretien'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1514, 'Immatriculation & permis', 1510, 'expense', '#1e3a8a', 1, 4, 'categoriesSeed.transport.vehicule.immatriculation'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1515, 'Stationnement & péages', 1510, 'expense', '#3b82f6', 1, 5, 'categoriesSeed.transport.vehicule.stationnement'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1516, 'Assurance auto', 1510, 'expense', '#60a5fa', 1, 6, 'categoriesSeed.transport.vehicule.assurance'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1520, 'Transport public', 1500, 'expense', '#0ea5e9', 0, 2, 'categoriesSeed.transport.public.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1521, 'Autobus & métro', 1520, 'expense', '#0ea5e9', 1, 1, 'categoriesSeed.transport.public.autobus'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1522, 'Train de banlieue', 1520, 'expense', '#0284c7', 1, 2, 'categoriesSeed.transport.public.train'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1523, 'Taxi & covoiturage', 1520, 'expense', '#0369a1', 1, 3, 'categoriesSeed.transport.public.taxi'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1530, 'Voyages longue distance', 1500, 'expense', '#38bdf8', 0, 3, 'categoriesSeed.transport.voyages.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1531, 'Avion', 1530, 'expense', '#38bdf8', 1, 1, 'categoriesSeed.transport.voyages.avion'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1532, 'Train & autocar', 1530, 'expense', '#7dd3fc', 1, 2, 'categoriesSeed.transport.voyages.trainAutocar'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1533, 'Hébergement', 1530, 'expense', '#bae6fd', 1, 3, 'categoriesSeed.transport.voyages.hebergement'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1534, 'Location véhicule voyage', 1530, 'expense', '#e0f2fe', 1, 4, 'categoriesSeed.transport.voyages.location'); + +-- 1600 — Santé & soins personnels +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1610, 'Soins médicaux', 1600, 'expense', '#f43f5e', 0, 1, 'categoriesSeed.sante.medicaux.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1611, 'Pharmacie', 1610, 'expense', '#f43f5e', 1, 1, 'categoriesSeed.sante.medicaux.pharmacie'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1612, 'Consultations médicales', 1610, 'expense', '#e11d48', 1, 2, 'categoriesSeed.sante.medicaux.consultations'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1613, 'Dentiste & orthodontiste', 1610, 'expense', '#be123c', 1, 3, 'categoriesSeed.sante.medicaux.dentiste'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1614, 'Optométrie & lunettes', 1610, 'expense', '#9f1239', 1, 4, 'categoriesSeed.sante.medicaux.optometrie'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1615, 'Thérapies (physio, psycho, etc.)', 1610, 'expense', '#881337', 1, 5, 'categoriesSeed.sante.medicaux.therapies'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1616, 'Assurance santé complémentaire', 1610, 'expense', '#fb7185', 1, 6, 'categoriesSeed.sante.medicaux.assurance'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1620, 'Soins personnels', 1600, 'expense', '#fb7185', 0, 2, 'categoriesSeed.sante.personnels.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1621, 'Coiffure & esthétique', 1620, 'expense', '#fb7185', 1, 1, 'categoriesSeed.sante.personnels.coiffure'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1622, 'Produits de soins corporels', 1620, 'expense', '#fda4af', 1, 2, 'categoriesSeed.sante.personnels.soins'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1630, 'Assurance vie & invalidité', 1600, 'expense', '#14b8a6', 1, 3, 'categoriesSeed.sante.assuranceVie'); + +-- 1700 — Loisirs, formation & lecture +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1710, 'Divertissement', 1700, 'expense', '#8b5cf6', 0, 1, 'categoriesSeed.loisirs.divertissement.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1711, 'Cinéma & spectacles', 1710, 'expense', '#8b5cf6', 1, 1, 'categoriesSeed.loisirs.divertissement.cinema'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1712, 'Jeux vidéo & consoles', 1710, 'expense', '#a78bfa', 1, 2, 'categoriesSeed.loisirs.divertissement.jeux'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1713, 'Streaming vidéo', 1710, 'expense', '#c4b5fd', 1, 3, 'categoriesSeed.loisirs.divertissement.streamingVideo'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1714, 'Streaming musique & audio', 1710, 'expense', '#ddd6fe', 1, 4, 'categoriesSeed.loisirs.divertissement.streamingMusique'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1715, 'Jouets & passe-temps', 1710, 'expense', '#ede9fe', 1, 5, 'categoriesSeed.loisirs.divertissement.jouets'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1720, 'Sports & plein air', 1700, 'expense', '#22c55e', 0, 2, 'categoriesSeed.loisirs.sports.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1721, 'Abonnements sportifs', 1720, 'expense', '#22c55e', 1, 1, 'categoriesSeed.loisirs.sports.abonnements'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1722, 'Équipement sportif', 1720, 'expense', '#4ade80', 1, 2, 'categoriesSeed.loisirs.sports.equipement'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1723, 'Parcs & activités plein air', 1720, 'expense', '#86efac', 1, 3, 'categoriesSeed.loisirs.sports.parcs'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1730, 'Formation & éducation', 1700, 'expense', '#6366f1', 0, 3, 'categoriesSeed.loisirs.formation.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1731, 'Scolarité (frais)', 1730, 'expense', '#6366f1', 1, 1, 'categoriesSeed.loisirs.formation.scolarite'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1732, 'Matériel scolaire', 1730, 'expense', '#818cf8', 1, 2, 'categoriesSeed.loisirs.formation.materiel'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1733, 'Cours & certifications', 1730, 'expense', '#a5b4fc', 1, 3, 'categoriesSeed.loisirs.formation.cours'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1734, 'Abonnements professionnels', 1730, 'expense', '#c7d2fe', 1, 4, 'categoriesSeed.loisirs.formation.abonnements'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1740, 'Lecture & médias', 1700, 'expense', '#ec4899', 0, 4, 'categoriesSeed.loisirs.lecture.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1741, 'Livres', 1740, 'expense', '#ec4899', 1, 1, 'categoriesSeed.loisirs.lecture.livres'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1742, 'Journaux & magazines', 1740, 'expense', '#f472b6', 1, 2, 'categoriesSeed.loisirs.lecture.journaux'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1750, 'Animaux de compagnie', 1700, 'expense', '#a855f7', 0, 5, 'categoriesSeed.loisirs.animaux.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1751, 'Nourriture & accessoires animaux', 1750, 'expense', '#a855f7', 1, 1, 'categoriesSeed.loisirs.animaux.nourriture'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1752, 'Vétérinaire', 1750, 'expense', '#c084fc', 1, 2, 'categoriesSeed.loisirs.animaux.veterinaire'); + +-- 1800 — Boissons, tabac & cannabis +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1810, 'Alcool (SAQ, microbrasseries)', 1800, 'expense', '#7c3aed', 1, 1, 'categoriesSeed.consommation.alcool'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1820, 'Cannabis (SQDC)', 1800, 'expense', '#6d28d9', 1, 2, 'categoriesSeed.consommation.cannabis'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1830, 'Tabac', 1800, 'expense', '#5b21b6', 1, 3, 'categoriesSeed.consommation.tabac'); + +-- 1900 — Finances & obligations +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1910, 'Frais bancaires', 1900, 'expense', '#6b7280', 0, 1, 'categoriesSeed.finances.fraisBancaires.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1911, 'Frais de compte', 1910, 'expense', '#6b7280', 1, 1, 'categoriesSeed.finances.fraisBancaires.compte'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1912, 'Intérêts & frais de crédit', 1910, 'expense', '#9ca3af', 1, 2, 'categoriesSeed.finances.fraisBancaires.interets'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1913, 'Frais de change', 1910, 'expense', '#d1d5db', 1, 3, 'categoriesSeed.finances.fraisBancaires.change'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1920, 'Impôts & taxes', 1900, 'expense', '#dc2626', 0, 2, 'categoriesSeed.finances.impots.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1921, 'Impôt fédéral', 1920, 'expense', '#dc2626', 1, 1, 'categoriesSeed.finances.impots.federal'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1922, 'Impôt provincial', 1920, 'expense', '#b91c1c', 1, 2, 'categoriesSeed.finances.impots.provincial'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1923, 'Acomptes provisionnels', 1920, 'expense', '#991b1b', 1, 3, 'categoriesSeed.finances.impots.acomptes'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1930, 'Dons & cotisations', 1900, 'expense', '#ec4899', 0, 3, 'categoriesSeed.finances.dons.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1931, 'Dons de charité', 1930, 'expense', '#ec4899', 1, 1, 'categoriesSeed.finances.dons.charite'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1932, 'Cotisations professionnelles', 1930, 'expense', '#f472b6', 1, 2, 'categoriesSeed.finances.dons.professionnelles'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1933, 'Cotisations syndicales', 1930, 'expense', '#f9a8d4', 1, 3, 'categoriesSeed.finances.dons.syndicales'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1940, 'Cadeaux', 1900, 'expense', '#f43f5e', 1, 4, 'categoriesSeed.finances.cadeaux'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1945, 'Retrait cash', 1900, 'expense', '#57534e', 1, 5, 'categoriesSeed.finances.cash'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1946, 'Achats divers non catégorisés', 1900, 'expense', '#78716c', 1, 6, 'categoriesSeed.finances.divers'); + +-- 1950 — Transferts & placements +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1960, 'Épargne & placements', 1950, 'transfer', '#0ea5e9', 0, 1, 'categoriesSeed.transferts.epargne.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1961, 'REER / RRSP', 1960, 'transfer', '#0ea5e9', 1, 1, 'categoriesSeed.transferts.epargne.reer'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1962, 'CELI / TFSA', 1960, 'transfer', '#0284c7', 1, 2, 'categoriesSeed.transferts.epargne.celi'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1963, 'REEE / RESP', 1960, 'transfer', '#0369a1', 1, 3, 'categoriesSeed.transferts.epargne.reee'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1964, 'Compte non-enregistré', 1960, 'transfer', '#075985', 1, 4, 'categoriesSeed.transferts.epargne.nonEnregistre'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1965, 'Fonds d''urgence', 1960, 'transfer', '#0c4a6e', 1, 5, 'categoriesSeed.transferts.epargne.urgence'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1970, 'Remboursement de dette', 1950, 'transfer', '#7c3aed', 0, 2, 'categoriesSeed.transferts.dette.root'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1971, 'Paiement carte crédit', 1970, 'transfer', '#7c3aed', 1, 1, 'categoriesSeed.transferts.dette.carteCredit'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1972, 'Remboursement prêt étudiant', 1970, 'transfer', '#8b5cf6', 1, 2, 'categoriesSeed.transferts.dette.etudiant'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1973, 'Remboursement prêt perso', 1970, 'transfer', '#a78bfa', 1, 3, 'categoriesSeed.transferts.dette.personnel'); +INSERT INTO categories (id, name, parent_id, type, color, is_inputable, sort_order, i18n_key) VALUES (1980, 'Transferts internes', 1950, 'transfer', '#64748b', 1, 3, 'categoriesSeed.transferts.internes'); + +-- ============================================================================ +-- Keywords — Canadian suppliers (150+ entries) +-- ============================================================================ + +-- Alimentation > Épicerie régulière (1111) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('METRO', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('IGA', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('MAXI', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SUPER C', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('LOBLAWS', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PROVIGO', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ADONIS', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('WHOLE FOODS', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('AVRIL', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('RACHELLE-BERY', 1111, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('COSTCO', 1111, 50); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('WALMART', 1111, 50); + +-- Épicerie > Boucherie (1112) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BOUCHERIE', 1112, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('POISSONNERIE', 1112, 0); + +-- Épicerie > Boulangerie (1113) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BOULANGERIE', 1113, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PATISSERIE', 1113, 0); + +-- Épicerie > Dépanneur (1114) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('COUCHE-TARD', 1114, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DEPANNEUR', 1114, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('7-ELEVEN', 1114, 0); + +-- Restauration > Restaurant (1121) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('RESTAURANT', 1121, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BRASSERIE', 1121, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BISTRO', 1121, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SUSHI', 1121, 0); + +-- Restauration > Café (1122) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('STARBUCKS', 1122, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('TIM HORTONS', 1122, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SECOND CUP', 1122, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('VAN HOUTTE', 1122, 0); + +-- Restauration > Fast food (1123) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('MCDONALD', 1123, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SUBWAY', 1123, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('A&W', 1123, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BURGER KING', 1123, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('KFC', 1123, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DOMINOS', 1123, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PIZZA HUT', 1123, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BELLE PROVINCE', 1123, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ST-HUBERT', 1123, 0); + +-- Restauration > Livraison (1124) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DOORDASH', 1124, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DD/DOORDASH', 1124, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('UBER EATS', 1124, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SKIPTHEDISHES', 1124, 0); + +-- Logement > Hypothèque (1212) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('MTG/HYP', 1212, 0); + +-- Logement > Taxes municipales (1213) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('M-ST-HILAIRE TX', 1213, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('TAXES MUNICIPALES', 1213, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CSS PATRIOT', 1213, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('TAXE SCOLAIRE', 1213, 0); + +-- Électricité (1221) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('HYDRO-QUEBEC', 1221, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('HYDRO QUEBEC', 1221, 0); + +-- Gaz (1222) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ENERGIR', 1222, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('GAZ METRO', 1222, 0); + +-- Internet (1231) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('VIDEOTRON', 1231, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BELL INTERNET', 1231, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ORICOM', 1231, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('COGECO', 1231, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('EBOX', 1231, 0); + +-- Mobile (1232) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('FIZZ', 1232, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('KOODO', 1232, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PUBLIC MOBILE', 1232, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('VIRGIN', 1232, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BELL MOBILITE', 1232, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('TELUS MOBILE', 1232, 0); + +-- Entretien maison (1241) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('IKEA', 1241, 0); + +-- Matériaux & outils (1243) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CANADIAN TIRE', 1243, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CANAC', 1243, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('RONA', 1243, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('HOME DEPOT', 1243, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BMR', 1243, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PRINCESS AUTO', 1243, 0); + +-- Assurance habitation (1250) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BELAIR', 1250, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PRYSM', 1250, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('INTACT ASSURANCE', 1250, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DESJARDINS ASSURANCE', 1250, 0); + +-- Meubles (1311) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('TANGUAY', 1311, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('LEON', 1311, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('STRUCTUBE', 1311, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BRICK', 1311, 0); + +-- Électroménagers (1312) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BEST BUY', 1312, 0); + +-- Décoration (1313) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BOUCLAIR', 1313, 0); + +-- Vêtements (1410) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('UNIQLO', 1410, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('WINNERS', 1410, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SIMONS', 1410, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('MARKS', 1410, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('H&M', 1410, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('OLD NAVY', 1410, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('GAP', 1410, 0); + +-- Transport — Essence (1512) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SHELL', 1512, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ESSO', 1512, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ULTRAMAR', 1512, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PETRO-CANADA', 1512, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PETRO CANADA', 1512, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CREVIER', 1512, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('HARNOIS', 1512, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('COUCHE-TARD ESSENCE', 1512, 10); + +-- Permis / SAAQ (1514) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SAAQ', 1514, 0); + +-- Transport public — autobus/métro (1521) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('STM', 1521, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('RTC', 1521, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('STL', 1521, 0); + +-- Train de banlieue (1522) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('GARE MONT-SAINT', 1522, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('GARE SAINT-HUBERT', 1522, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('GARE CENTRALE', 1522, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('EXO', 1522, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('REM', 1522, 0); + +-- Taxi / Uber (1523) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('UBER', 1523, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('LYFT', 1523, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('TAXI', 1523, 0); + +-- Avion (1531) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('AIR CANADA', 1531, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('WESTJET', 1531, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('AIR TRANSAT', 1531, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PORTER', 1531, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('AEROPORTS DE MONTREAL', 1531, 0); + +-- Hébergement (1533) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('AIRBNB', 1533, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('HILTON', 1533, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('MARRIOTT', 1533, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BOOKING.COM', 1533, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('NORWEGIAN CRUISE', 1533, 0); + +-- Pharmacie (1611) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('JEAN COUTU', 1611, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('FAMILIPRIX', 1611, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PHARMAPRIX', 1611, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PROXIM', 1611, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('UNIPRIX', 1611, 0); + +-- Thérapies (1615) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PHYSIOACTIF', 1615, 0); + +-- Cinéma & spectacles (1711) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CINEPLEX', 1711, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CINEMA DU PARC', 1711, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('TICKETMASTER', 1711, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CLUB SODA', 1711, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('LEPOINTDEVENTE', 1711, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('EVENBRITE', 1711, 0); + +-- Jeux vidéo (1712) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('STEAMGAMES', 1712, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PLAYSTATION', 1712, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('NINTENDO', 1712, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('XBOX', 1712, 0); + +-- Streaming vidéo (1713) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('NETFLIX', 1713, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PRIMEVIDEO', 1713, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DISNEY PLUS', 1713, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CRAVE', 1713, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('APPLE TV', 1713, 0); + +-- Streaming musique (1714) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SPOTIFY', 1714, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('APPLE MUSIC', 1714, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('YOUTUBE MUSIC', 1714, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('TIDAL', 1714, 0); + +-- Jouets & hobbies (1715) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('LEGO', 1715, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('TOYS R US', 1715, 0); + +-- Équipement sportif (1722) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('MOUNTAIN EQUIPMENT', 1722, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('LA CORDEE', 1722, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DECATHLON', 1722, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SPORTS EXPERTS', 1722, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ATMOSPHERE', 1722, 0); + +-- Parcs & activités (1723) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SEPAQ', 1723, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('BLOC SHOP', 1723, 0); + +-- Lecture (1741) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('RENAUD-BRAY', 1741, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('INDIGO', 1741, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ARCHAMBAULT', 1741, 0); + +-- Animaux — nourriture (1751) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('MONDOU', 1751, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PET SMART', 1751, 0); + +-- Alcool (1810) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SAQ', 1810, 0); + +-- Cannabis (1820) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('SQDC', 1820, 0); + +-- Frais bancaires (1911) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PROGRAMME PERFORMANCE', 1911, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('FRAIS MENSUELS', 1911, 0); + +-- Impôts (1921, 1922) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('GOUV. QUEBEC', 1922, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('REVENU QUEBEC', 1922, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ARC IMPOT', 1921, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CRA TAX', 1921, 0); + +-- Dons de charité (1931) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('OXFAM', 1931, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CENTRAIDE', 1931, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('FPA', 1931, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CROIX-ROUGE', 1931, 0); + +-- Cotisations professionnelles (1932) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('ORDRE DES COMPTABL', 1932, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('CPA CANADA', 1932, 0); + +-- Cadeaux (1940) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DANS UN JARDIN', 1940, 0); + +-- Divers (1946) — catch-all marketplace +INSERT INTO keywords (keyword, category_id, priority) VALUES ('AMAZON', 1946, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('AMZN', 1946, 0); + +-- Placements (1964) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('WS INVESTMENTS', 1964, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('WEALTHSIMPLE', 1964, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PEAK INVESTMENT', 1964, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DYNAMIC FUND', 1964, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('FIDELITY', 1964, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('AGF', 1964, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('QUESTRADE', 1964, 0); + +-- Paie (1011) +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PAY/PAY', 1011, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('DEPOT PAIE', 1011, 0); +INSERT INTO keywords (keyword, category_id, priority) VALUES ('PAYROLL', 1011, 0); diff --git a/src-tauri/src/database/fixtures/seed_v0.3.0.sql b/src-tauri/src/database/fixtures/seed_v0.3.0.sql new file mode 100644 index 0000000..aeba40c --- /dev/null +++ b/src-tauri/src/database/fixtures/seed_v0.3.0.sql @@ -0,0 +1,232 @@ +-- Migration v2: Seed categories and keywords from SommaireDepense.csv + +-- Clear existing data (FK order: keywords -> categories) +DELETE FROM keywords; +UPDATE transactions SET category_id = NULL; +DELETE FROM categories; + +-- ========================================== +-- Parent categories (Regroupement level) +-- ========================================== +INSERT INTO categories (id, name, type, sort_order) VALUES (1, 'Revenus', 'income', 1); +INSERT INTO categories (id, name, type, sort_order) VALUES (2, 'Dépenses récurrentes', 'expense', 2); +INSERT INTO categories (id, name, type, sort_order) VALUES (3, 'Dépenses ponctuelles', 'expense', 3); +INSERT INTO categories (id, name, type, sort_order) VALUES (4, 'Maison', 'expense', 4); +INSERT INTO categories (id, name, type, sort_order) VALUES (5, 'Placements', 'transfer', 5); +INSERT INTO categories (id, name, type, sort_order) VALUES (6, 'Autres', 'expense', 6); + +-- ========================================== +-- Child categories +-- ========================================== + +-- Revenus children +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (10, 'Paie', 1, 'income', '#22c55e', 1); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (11, 'Autres revenus', 1, 'income', '#4ade80', 2); + +-- Dépenses récurrentes children +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (20, 'Loyer', 2, 'expense', '#ef4444', 1); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (21, 'Électricité', 2, 'expense', '#f59e0b', 2); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (22, 'Épicerie', 2, 'expense', '#10b981', 3); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (23, 'Dons', 2, 'expense', '#ec4899', 4); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (24, 'Restaurant', 2, 'expense', '#f97316', 5); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (25, 'Frais bancaires', 2, 'expense', '#6b7280', 6); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (26, 'Jeux, Films & Livres', 2, 'expense', '#8b5cf6', 7); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (27, 'Abonnements Musique', 2, 'expense', '#06b6d4', 8); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (28, 'Transport en commun', 2, 'expense', '#3b82f6', 9); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (29, 'Internet & Télécom', 2, 'expense', '#6366f1', 10); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (30, 'Animaux', 2, 'expense', '#a855f7', 11); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (31, 'Assurances', 2, 'expense', '#14b8a6', 12); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (32, 'Pharmacie', 2, 'expense', '#f43f5e', 13); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (33, 'Taxes municipales', 2, 'expense', '#78716c', 14); + +-- Dépenses ponctuelles children +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (40, 'Voiture', 3, 'expense', '#64748b', 1); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (41, 'Amazon', 3, 'expense', '#f59e0b', 2); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (42, 'Électroniques', 3, 'expense', '#3b82f6', 3); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (43, 'Alcool', 3, 'expense', '#7c3aed', 4); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (44, 'Cadeaux', 3, 'expense', '#ec4899', 5); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (45, 'Vêtements', 3, 'expense', '#d946ef', 6); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (46, 'CPA', 3, 'expense', '#0ea5e9', 7); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (47, 'Voyage', 3, 'expense', '#f97316', 8); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (48, 'Sports & Plein air', 3, 'expense', '#22c55e', 9); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (49, 'Spectacles & sorties', 3, 'expense', '#e11d48', 10); + +-- Maison children +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (50, 'Hypothèque', 4, 'expense', '#dc2626', 1); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (51, 'Achats maison', 4, 'expense', '#ea580c', 2); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (52, 'Entretien maison', 4, 'expense', '#ca8a04', 3); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (53, 'Électroménagers & Meubles', 4, 'expense', '#0d9488', 4); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (54, 'Outils', 4, 'expense', '#b45309', 5); + +-- Placements children +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (60, 'Placements', 5, 'transfer', '#2563eb', 1); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (61, 'Transferts', 5, 'transfer', '#7c3aed', 2); + +-- Autres children +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (70, 'Impôts', 6, 'expense', '#dc2626', 1); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (71, 'Paiement CC', 6, 'transfer', '#6b7280', 2); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (72, 'Retrait cash', 6, 'expense', '#57534e', 3); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (73, 'Projets', 6, 'expense', '#0ea5e9', 4); + +-- ========================================== +-- Keywords +-- ========================================== + +-- Paie (10) +INSERT INTO keywords (keyword, category_id) VALUES ('PAY/PAY', 10); + +-- Électricité (21) +INSERT INTO keywords (keyword, category_id) VALUES ('HYDRO-QUEBEC', 21); + +-- Épicerie (22) +INSERT INTO keywords (keyword, category_id) VALUES ('METRO', 22); +INSERT INTO keywords (keyword, category_id) VALUES ('IGA', 22); +INSERT INTO keywords (keyword, category_id) VALUES ('MAXI', 22); +INSERT INTO keywords (keyword, category_id) VALUES ('SUPER C', 22); +INSERT INTO keywords (keyword, category_id) VALUES ('BOUCHERIE LAFLECHE', 22); +INSERT INTO keywords (keyword, category_id) VALUES ('BOULANGERIE JARRY', 22); +INSERT INTO keywords (keyword, category_id) VALUES ('DOLLARAMA', 22); +INSERT INTO keywords (keyword, category_id) VALUES ('WALMART', 22); + +-- Dons (23) +INSERT INTO keywords (keyword, category_id) VALUES ('OXFAM', 23); +INSERT INTO keywords (keyword, category_id) VALUES ('CENTRAIDE', 23); +INSERT INTO keywords (keyword, category_id) VALUES ('FPA', 23); + +-- Restaurant (24) +INSERT INTO keywords (keyword, category_id) VALUES ('SUBWAY', 24); +INSERT INTO keywords (keyword, category_id) VALUES ('MCDONALD', 24); +INSERT INTO keywords (keyword, category_id) VALUES ('A&W', 24); +INSERT INTO keywords (keyword, category_id) VALUES ('DD/DOORDASH', 24); +INSERT INTO keywords (keyword, category_id) VALUES ('DOORDASH', 24); +INSERT INTO keywords (keyword, category_id) VALUES ('SUSHI', 24); +INSERT INTO keywords (keyword, category_id) VALUES ('DOMINOS', 24); +INSERT INTO keywords (keyword, category_id) VALUES ('BELLE PROVINCE', 24); + +-- Frais bancaires (25) +INSERT INTO keywords (keyword, category_id) VALUES ('PROGRAMME PERFORMANCE', 25); + +-- Jeux, Films & Livres (26) +INSERT INTO keywords (keyword, category_id) VALUES ('STEAMGAMES', 26); +INSERT INTO keywords (keyword, category_id) VALUES ('PLAYSTATION', 26); +INSERT INTO keywords (keyword, category_id) VALUES ('PRIMEVIDEO', 26); +INSERT INTO keywords (keyword, category_id) VALUES ('NINTENDO', 26); +INSERT INTO keywords (keyword, category_id) VALUES ('RENAUD-BRAY', 26); +INSERT INTO keywords (keyword, category_id) VALUES ('CINEMA DU PARC', 26); +INSERT INTO keywords (keyword, category_id) VALUES ('LEGO', 26); + +-- Abonnements Musique (27) +INSERT INTO keywords (keyword, category_id) VALUES ('SPOTIFY', 27); + +-- Transport en commun (28) +INSERT INTO keywords (keyword, category_id) VALUES ('STM', 28); +INSERT INTO keywords (keyword, category_id) VALUES ('GARE MONT-SAINT', 28); +INSERT INTO keywords (keyword, category_id) VALUES ('GARE SAINT-HUBERT', 28); +INSERT INTO keywords (keyword, category_id) VALUES ('GARE CENTRALE', 28); +INSERT INTO keywords (keyword, category_id) VALUES ('REM', 28); + +-- Internet & Télécom (29) +INSERT INTO keywords (keyword, category_id) VALUES ('VIDEOTRON', 29); +INSERT INTO keywords (keyword, category_id) VALUES ('ORICOM', 29); + +-- Animaux (30) +INSERT INTO keywords (keyword, category_id) VALUES ('MONDOU', 30); + +-- Assurances (31) +INSERT INTO keywords (keyword, category_id) VALUES ('BELAIR', 31); +INSERT INTO keywords (keyword, category_id) VALUES ('PRYSM', 31); +INSERT INTO keywords (keyword, category_id) VALUES ('INS/ASS', 31); + +-- Pharmacie (32) +INSERT INTO keywords (keyword, category_id) VALUES ('JEAN COUTU', 32); +INSERT INTO keywords (keyword, category_id) VALUES ('FAMILIPRIX', 32); +INSERT INTO keywords (keyword, category_id) VALUES ('PHARMAPRIX', 32); + +-- Taxes municipales (33) +INSERT INTO keywords (keyword, category_id) VALUES ('M-ST-HILAIRE TX', 33); +INSERT INTO keywords (keyword, category_id) VALUES ('CSS PATRIOT', 33); + +-- Voiture (40) +INSERT INTO keywords (keyword, category_id) VALUES ('SHELL', 40); +INSERT INTO keywords (keyword, category_id) VALUES ('ESSO', 40); +INSERT INTO keywords (keyword, category_id) VALUES ('ULTRAMAR', 40); +INSERT INTO keywords (keyword, category_id) VALUES ('PETRO-CANADA', 40); +INSERT INTO keywords (keyword, category_id) VALUES ('SAAQ', 40); +INSERT INTO keywords (keyword, category_id) VALUES ('CREVIER', 40); + +-- Amazon (41) +INSERT INTO keywords (keyword, category_id) VALUES ('AMAZON', 41); +INSERT INTO keywords (keyword, category_id) VALUES ('AMZN', 41); + +-- Électroniques (42) +INSERT INTO keywords (keyword, category_id) VALUES ('MICROSOFT', 42); +INSERT INTO keywords (keyword, category_id) VALUES ('ADDISON ELECTRONIQUE', 42); + +-- Alcool (43) +INSERT INTO keywords (keyword, category_id) VALUES ('SAQ', 43); +INSERT INTO keywords (keyword, category_id) VALUES ('SQDC', 43); + +-- Cadeaux (44) +INSERT INTO keywords (keyword, category_id) VALUES ('DANS UN JARDIN', 44); + +-- Vêtements (45) +INSERT INTO keywords (keyword, category_id) VALUES ('UNIQLO', 45); +INSERT INTO keywords (keyword, category_id) VALUES ('WINNERS', 45); +INSERT INTO keywords (keyword, category_id) VALUES ('SIMONS', 45); + +-- CPA (46) +INSERT INTO keywords (keyword, category_id) VALUES ('ORDRE DES COMPTABL', 46); + +-- Voyage (47) +INSERT INTO keywords (keyword, category_id) VALUES ('NORWEGIAN CRUISE', 47); +INSERT INTO keywords (keyword, category_id) VALUES ('AEROPORTS DE MONTREAL', 47); +INSERT INTO keywords (keyword, category_id) VALUES ('HILTON', 47); + +-- Sports & Plein air (48) +INSERT INTO keywords (keyword, category_id) VALUES ('BLOC SHOP', 48); +INSERT INTO keywords (keyword, category_id) VALUES ('SEPAQ', 48); +INSERT INTO keywords (keyword, category_id) VALUES ('LA CORDEE', 48); +INSERT INTO keywords (keyword, category_id) VALUES ('MOUNTAIN EQUIPMENT', 48); +INSERT INTO keywords (keyword, category_id) VALUES ('PHYSIOACTIF', 48); +INSERT INTO keywords (keyword, category_id) VALUES ('DECATHLON', 48); + +-- Spectacles & sorties (49) +INSERT INTO keywords (keyword, category_id) VALUES ('TICKETMASTER', 49); +INSERT INTO keywords (keyword, category_id) VALUES ('CLUB SODA', 49); +INSERT INTO keywords (keyword, category_id) VALUES ('LEPOINTDEVENTE', 49); + +-- Hypothèque (50) +INSERT INTO keywords (keyword, category_id) VALUES ('MTG/HYP', 50); + +-- Achats maison (51) +INSERT INTO keywords (keyword, category_id) VALUES ('CANADIAN TIRE', 51); +INSERT INTO keywords (keyword, category_id) VALUES ('CANAC', 51); +INSERT INTO keywords (keyword, category_id) VALUES ('RONA', 51); + +-- Entretien maison (52) +INSERT INTO keywords (keyword, category_id) VALUES ('IKEA', 52); + +-- Électroménagers & Meubles (53) +INSERT INTO keywords (keyword, category_id) VALUES ('TANGUAY', 53); +INSERT INTO keywords (keyword, category_id) VALUES ('BOUCLAIR', 53); + +-- Outils (54) +INSERT INTO keywords (keyword, category_id) VALUES ('BMR', 54); +INSERT INTO keywords (keyword, category_id) VALUES ('HOME DEPOT', 54); +INSERT INTO keywords (keyword, category_id) VALUES ('PRINCESS AUTO', 54); + +-- Placements (60) +INSERT INTO keywords (keyword, category_id) VALUES ('DYNAMIC FUND', 60); +INSERT INTO keywords (keyword, category_id) VALUES ('FIDELITY', 60); +INSERT INTO keywords (keyword, category_id) VALUES ('AGF', 60); + +-- Transferts (61) +INSERT INTO keywords (keyword, category_id) VALUES ('WS INVESTMENTS', 61); +INSERT INTO keywords (keyword, category_id) VALUES ('PEAK INVESTMENT', 61); + +-- Impôts (70) +INSERT INTO keywords (keyword, category_id) VALUES ('GOUV. QUEBEC', 70); + +-- Projets (73) +INSERT INTO keywords (keyword, category_id) VALUES ('CLAUDE.AI', 73); +INSERT INTO keywords (keyword, category_id) VALUES ('NAME-CHEAP', 73); diff --git a/src-tauri/src/database/fixtures/seed_v0.4.0.sql b/src-tauri/src/database/fixtures/seed_v0.4.0.sql new file mode 100644 index 0000000..0582449 --- /dev/null +++ b/src-tauri/src/database/fixtures/seed_v0.4.0.sql @@ -0,0 +1,241 @@ +-- Migration v2: Seed categories and keywords from SommaireDepense.csv + +-- Clear existing data (FK order: keywords -> categories) +DELETE FROM keywords; +UPDATE transactions SET category_id = NULL; +DELETE FROM categories; + +-- ========================================== +-- Parent categories (Regroupement level) +-- ========================================== +INSERT INTO categories (id, name, type, sort_order) VALUES (1, 'Revenus', 'income', 1); +INSERT INTO categories (id, name, type, sort_order) VALUES (2, 'Dépenses récurrentes', 'expense', 2); +INSERT INTO categories (id, name, type, sort_order) VALUES (3, 'Dépenses ponctuelles', 'expense', 3); +INSERT INTO categories (id, name, type, sort_order) VALUES (4, 'Maison', 'expense', 4); +INSERT INTO categories (id, name, type, sort_order) VALUES (5, 'Placements', 'transfer', 5); +INSERT INTO categories (id, name, type, sort_order) VALUES (6, 'Autres', 'expense', 6); + +-- ========================================== +-- Child categories +-- ========================================== + +-- Revenus children +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (10, 'Paie', 1, 'income', '#22c55e', 1); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (11, 'Autres revenus', 1, 'income', '#4ade80', 2); + +-- Dépenses récurrentes children +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (20, 'Loyer', 2, 'expense', '#ef4444', 1); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (21, 'Électricité', 2, 'expense', '#f59e0b', 2); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (22, 'Épicerie', 2, 'expense', '#10b981', 3); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (23, 'Dons', 2, 'expense', '#ec4899', 4); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (24, 'Restaurant', 2, 'expense', '#f97316', 5); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (25, 'Frais bancaires', 2, 'expense', '#6b7280', 6); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (26, 'Jeux, Films & Livres', 2, 'expense', '#8b5cf6', 7); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (27, 'Abonnements Musique', 2, 'expense', '#06b6d4', 8); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (28, 'Transport en commun', 2, 'expense', '#3b82f6', 9); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (29, 'Internet & Télécom', 2, 'expense', '#6366f1', 10); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (30, 'Animaux', 2, 'expense', '#a855f7', 11); +INSERT INTO categories (id, name, parent_id, type, color, sort_order, is_inputable) VALUES (31, 'Assurances', 2, 'expense', '#14b8a6', 12, 0); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (32, 'Pharmacie', 2, 'expense', '#f43f5e', 13); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (33, 'Taxes municipales', 2, 'expense', '#78716c', 14); + +-- Dépenses ponctuelles children +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (40, 'Voiture', 3, 'expense', '#64748b', 1); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (41, 'Amazon', 3, 'expense', '#f59e0b', 2); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (42, 'Électroniques', 3, 'expense', '#3b82f6', 3); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (43, 'Alcool', 3, 'expense', '#7c3aed', 4); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (44, 'Cadeaux', 3, 'expense', '#ec4899', 5); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (45, 'Vêtements', 3, 'expense', '#d946ef', 6); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (46, 'CPA', 3, 'expense', '#0ea5e9', 7); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (47, 'Voyage', 3, 'expense', '#f97316', 8); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (48, 'Sports & Plein air', 3, 'expense', '#22c55e', 9); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (49, 'Spectacles & sorties', 3, 'expense', '#e11d48', 10); + +-- Maison children +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (50, 'Hypothèque', 4, 'expense', '#dc2626', 1); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (51, 'Achats maison', 4, 'expense', '#ea580c', 2); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (52, 'Entretien maison', 4, 'expense', '#ca8a04', 3); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (53, 'Électroménagers & Meubles', 4, 'expense', '#0d9488', 4); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (54, 'Outils', 4, 'expense', '#b45309', 5); + +-- Placements children +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (60, 'Placements', 5, 'transfer', '#2563eb', 1); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (61, 'Transferts', 5, 'transfer', '#7c3aed', 2); + +-- Autres children +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (70, 'Impôts', 6, 'expense', '#dc2626', 1); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (71, 'Paiement CC', 6, 'transfer', '#6b7280', 2); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (72, 'Retrait cash', 6, 'expense', '#57534e', 3); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (73, 'Projets', 6, 'expense', '#0ea5e9', 4); + +-- ========================================== +-- Grandchild categories (Level 3 — under Assurances) +-- ========================================== +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (310, 'Assurance-auto', 31, 'expense', '#14b8a6', 1); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (311, 'Assurance-habitation', 31, 'expense', '#0d9488', 2); +INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES (312, 'Assurance-vie', 31, 'expense', '#0f766e', 3); + +-- ========================================== +-- Keywords +-- ========================================== + +-- Paie (10) +INSERT INTO keywords (keyword, category_id) VALUES ('PAY/PAY', 10); + +-- Électricité (21) +INSERT INTO keywords (keyword, category_id) VALUES ('HYDRO-QUEBEC', 21); + +-- Épicerie (22) +INSERT INTO keywords (keyword, category_id) VALUES ('METRO', 22); +INSERT INTO keywords (keyword, category_id) VALUES ('IGA', 22); +INSERT INTO keywords (keyword, category_id) VALUES ('MAXI', 22); +INSERT INTO keywords (keyword, category_id) VALUES ('SUPER C', 22); +INSERT INTO keywords (keyword, category_id) VALUES ('BOUCHERIE LAFLECHE', 22); +INSERT INTO keywords (keyword, category_id) VALUES ('BOULANGERIE JARRY', 22); +INSERT INTO keywords (keyword, category_id) VALUES ('DOLLARAMA', 22); +INSERT INTO keywords (keyword, category_id) VALUES ('WALMART', 22); + +-- Dons (23) +INSERT INTO keywords (keyword, category_id) VALUES ('OXFAM', 23); +INSERT INTO keywords (keyword, category_id) VALUES ('CENTRAIDE', 23); +INSERT INTO keywords (keyword, category_id) VALUES ('FPA', 23); + +-- Restaurant (24) +INSERT INTO keywords (keyword, category_id) VALUES ('SUBWAY', 24); +INSERT INTO keywords (keyword, category_id) VALUES ('MCDONALD', 24); +INSERT INTO keywords (keyword, category_id) VALUES ('A&W', 24); +INSERT INTO keywords (keyword, category_id) VALUES ('DD/DOORDASH', 24); +INSERT INTO keywords (keyword, category_id) VALUES ('DOORDASH', 24); +INSERT INTO keywords (keyword, category_id) VALUES ('SUSHI', 24); +INSERT INTO keywords (keyword, category_id) VALUES ('DOMINOS', 24); +INSERT INTO keywords (keyword, category_id) VALUES ('BELLE PROVINCE', 24); + +-- Frais bancaires (25) +INSERT INTO keywords (keyword, category_id) VALUES ('PROGRAMME PERFORMANCE', 25); + +-- Jeux, Films & Livres (26) +INSERT INTO keywords (keyword, category_id) VALUES ('STEAMGAMES', 26); +INSERT INTO keywords (keyword, category_id) VALUES ('PLAYSTATION', 26); +INSERT INTO keywords (keyword, category_id) VALUES ('PRIMEVIDEO', 26); +INSERT INTO keywords (keyword, category_id) VALUES ('NINTENDO', 26); +INSERT INTO keywords (keyword, category_id) VALUES ('RENAUD-BRAY', 26); +INSERT INTO keywords (keyword, category_id) VALUES ('CINEMA DU PARC', 26); +INSERT INTO keywords (keyword, category_id) VALUES ('LEGO', 26); + +-- Abonnements Musique (27) +INSERT INTO keywords (keyword, category_id) VALUES ('SPOTIFY', 27); + +-- Transport en commun (28) +INSERT INTO keywords (keyword, category_id) VALUES ('STM', 28); +INSERT INTO keywords (keyword, category_id) VALUES ('GARE MONT-SAINT', 28); +INSERT INTO keywords (keyword, category_id) VALUES ('GARE SAINT-HUBERT', 28); +INSERT INTO keywords (keyword, category_id) VALUES ('GARE CENTRALE', 28); +INSERT INTO keywords (keyword, category_id) VALUES ('REM', 28); + +-- Internet & Télécom (29) +INSERT INTO keywords (keyword, category_id) VALUES ('VIDEOTRON', 29); +INSERT INTO keywords (keyword, category_id) VALUES ('ORICOM', 29); + +-- Animaux (30) +INSERT INTO keywords (keyword, category_id) VALUES ('MONDOU', 30); + +-- Assurance-auto (310) +INSERT INTO keywords (keyword, category_id) VALUES ('BELAIR', 310); +-- Assurance-habitation (311) +INSERT INTO keywords (keyword, category_id) VALUES ('PRYSM', 311); +-- Assurance-vie (312) +INSERT INTO keywords (keyword, category_id) VALUES ('INS/ASS', 312); + +-- Pharmacie (32) +INSERT INTO keywords (keyword, category_id) VALUES ('JEAN COUTU', 32); +INSERT INTO keywords (keyword, category_id) VALUES ('FAMILIPRIX', 32); +INSERT INTO keywords (keyword, category_id) VALUES ('PHARMAPRIX', 32); + +-- Taxes municipales (33) +INSERT INTO keywords (keyword, category_id) VALUES ('M-ST-HILAIRE TX', 33); +INSERT INTO keywords (keyword, category_id) VALUES ('CSS PATRIOT', 33); + +-- Voiture (40) +INSERT INTO keywords (keyword, category_id) VALUES ('SHELL', 40); +INSERT INTO keywords (keyword, category_id) VALUES ('ESSO', 40); +INSERT INTO keywords (keyword, category_id) VALUES ('ULTRAMAR', 40); +INSERT INTO keywords (keyword, category_id) VALUES ('PETRO-CANADA', 40); +INSERT INTO keywords (keyword, category_id) VALUES ('SAAQ', 40); +INSERT INTO keywords (keyword, category_id) VALUES ('CREVIER', 40); + +-- Amazon (41) +INSERT INTO keywords (keyword, category_id) VALUES ('AMAZON', 41); +INSERT INTO keywords (keyword, category_id) VALUES ('AMZN', 41); + +-- Électroniques (42) +INSERT INTO keywords (keyword, category_id) VALUES ('MICROSOFT', 42); +INSERT INTO keywords (keyword, category_id) VALUES ('ADDISON ELECTRONIQUE', 42); + +-- Alcool (43) +INSERT INTO keywords (keyword, category_id) VALUES ('SAQ', 43); +INSERT INTO keywords (keyword, category_id) VALUES ('SQDC', 43); + +-- Cadeaux (44) +INSERT INTO keywords (keyword, category_id) VALUES ('DANS UN JARDIN', 44); + +-- Vêtements (45) +INSERT INTO keywords (keyword, category_id) VALUES ('UNIQLO', 45); +INSERT INTO keywords (keyword, category_id) VALUES ('WINNERS', 45); +INSERT INTO keywords (keyword, category_id) VALUES ('SIMONS', 45); + +-- CPA (46) +INSERT INTO keywords (keyword, category_id) VALUES ('ORDRE DES COMPTABL', 46); + +-- Voyage (47) +INSERT INTO keywords (keyword, category_id) VALUES ('NORWEGIAN CRUISE', 47); +INSERT INTO keywords (keyword, category_id) VALUES ('AEROPORTS DE MONTREAL', 47); +INSERT INTO keywords (keyword, category_id) VALUES ('HILTON', 47); + +-- Sports & Plein air (48) +INSERT INTO keywords (keyword, category_id) VALUES ('BLOC SHOP', 48); +INSERT INTO keywords (keyword, category_id) VALUES ('SEPAQ', 48); +INSERT INTO keywords (keyword, category_id) VALUES ('LA CORDEE', 48); +INSERT INTO keywords (keyword, category_id) VALUES ('MOUNTAIN EQUIPMENT', 48); +INSERT INTO keywords (keyword, category_id) VALUES ('PHYSIOACTIF', 48); +INSERT INTO keywords (keyword, category_id) VALUES ('DECATHLON', 48); + +-- Spectacles & sorties (49) +INSERT INTO keywords (keyword, category_id) VALUES ('TICKETMASTER', 49); +INSERT INTO keywords (keyword, category_id) VALUES ('CLUB SODA', 49); +INSERT INTO keywords (keyword, category_id) VALUES ('LEPOINTDEVENTE', 49); + +-- Hypothèque (50) +INSERT INTO keywords (keyword, category_id) VALUES ('MTG/HYP', 50); + +-- Achats maison (51) +INSERT INTO keywords (keyword, category_id) VALUES ('CANADIAN TIRE', 51); +INSERT INTO keywords (keyword, category_id) VALUES ('CANAC', 51); +INSERT INTO keywords (keyword, category_id) VALUES ('RONA', 51); + +-- Entretien maison (52) +INSERT INTO keywords (keyword, category_id) VALUES ('IKEA', 52); + +-- Électroménagers & Meubles (53) +INSERT INTO keywords (keyword, category_id) VALUES ('TANGUAY', 53); +INSERT INTO keywords (keyword, category_id) VALUES ('BOUCLAIR', 53); + +-- Outils (54) +INSERT INTO keywords (keyword, category_id) VALUES ('BMR', 54); +INSERT INTO keywords (keyword, category_id) VALUES ('HOME DEPOT', 54); +INSERT INTO keywords (keyword, category_id) VALUES ('PRINCESS AUTO', 54); + +-- Placements (60) +INSERT INTO keywords (keyword, category_id) VALUES ('DYNAMIC FUND', 60); +INSERT INTO keywords (keyword, category_id) VALUES ('FIDELITY', 60); +INSERT INTO keywords (keyword, category_id) VALUES ('AGF', 60); + +-- Transferts (61) +INSERT INTO keywords (keyword, category_id) VALUES ('WS INVESTMENTS', 61); +INSERT INTO keywords (keyword, category_id) VALUES ('PEAK INVESTMENT', 61); + +-- Impôts (70) +INSERT INTO keywords (keyword, category_id) VALUES ('GOUV. QUEBEC', 70); + +-- Projets (73) +INSERT INTO keywords (keyword, category_id) VALUES ('CLAUDE.AI', 73); +INSERT INTO keywords (keyword, category_id) VALUES ('NAME-CHEAP', 73); diff --git a/src-tauri/src/database/mod.rs b/src-tauri/src/database/mod.rs index 74cf233..3312ae9 100644 --- a/src-tauri/src/database/mod.rs +++ b/src-tauri/src/database/mod.rs @@ -1,5 +1,11 @@ +pub mod profile_db; + pub const SCHEMA: &str = include_str!("schema.sql"); pub const SEED_CATEGORIES: &str = include_str!("seed_categories.sql"); pub const CONSOLIDATED_SCHEMA: &str = include_str!("consolidated_schema.sql"); pub const BALANCE_SCHEMA: &str = include_str!("balance_schema.sql"); pub const BALANCE_HOLDINGS_SCHEMA: &str = include_str!("balance_holdings_schema.sql"); + +/// File name of the default profile's database: the only one tauri-plugin-sql +/// migrates itself (`add_migrations` in lib.rs). +pub const DEFAULT_PROFILE_DB: &str = "simpl_resultat.db"; diff --git a/src-tauri/src/database/profile_db.rs b/src-tauri/src/database/profile_db.rs new file mode 100644 index 0000000..9614d39 --- /dev/null +++ b/src-tauri/src/database/profile_db.rs @@ -0,0 +1,1392 @@ +//! Preparing a profile database before the webview opens it (#390, ADR 0021). +//! +//! tauri-plugin-sql migrates only the database registered with +//! `add_migrations` — the default profile, `simpl_resultat.db` (lib.rs) — and +//! drops that list after the first load. A secondary profile +//! (`profile_.db`) is created from the consolidated schema of the version +//! that creates it, and until #390 never received a migration afterwards. +//! +//! This module catches such a database up with the very same `Migration` list, +//! keeping the history in the `_sqlx_migrations` table sqlx uses for the +//! default profile, and repairs a value an old bug left in every profile. +//! It works on a plain `rusqlite::Connection`, so every rule is tested against +//! real SQLite and the databases each released version created. + +use rusqlite::{params, Connection, OpenFlags, TransactionBehavior}; +use serde::Serialize; +use sha2::{Digest, Sha384}; +use std::collections::BTreeSet; +use std::fs; +use std::path::Path; +use std::time::{Duration, Instant}; +use tauri_plugin_sql::{Migration, MigrationKind}; + +use super::DEFAULT_PROFILE_DB; + +/// Subfolder, next to the profile databases, that holds the copy taken before +/// a catch-up. Removed with the profile (`delete_profile_db`). +pub const MIGRATION_BACKUP_DIR: &str = "migration-backups"; + +/// The DDL sqlx 0.8 creates (sqlx-sqlite `migrate.rs`), so the default profile +/// and the secondary ones keep their history in one format. +const SQLX_MIGRATIONS_DDL: &str = "CREATE TABLE IF NOT EXISTS _sqlx_migrations ( + version BIGINT PRIMARY KEY, + description TEXT NOT NULL, + installed_on TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + success BOOLEAN NOT NULL, + checksum BLOB NOT NULL, + execution_time BIGINT NOT NULL +);"; + +/// Until #390 the Transactions page bound the JS boolean `true`, which +/// tauri-plugin-sql stores as the TEXT 'true' (an INTEGER column keeps a text +/// that is not a number). Only `'true'` was ever written; any other text is +/// read as "not set", like `toSqlFlag` on the TypeScript side. +const REPAIR_MANUAL_FLAGS_SQL: &str = "UPDATE transactions \ + SET is_manually_categorized = CASE WHEN is_manually_categorized = 'true' THEN 1 ELSE 0 END \ + WHERE typeof(is_manually_categorized) = 'text'"; + +/// What a catch-up did, for the Journal. +#[derive(Debug, Default, Clone, PartialEq, Serialize)] +pub struct CatchUp { + /// Highest version whose schema change the database already carried. + pub floor: Option, + /// Versions written to the history without being run: the database already + /// had their effect (first history of a secondary profile, or a gap left by + /// an interrupted run). + pub recorded: Vec, + /// Versions this run applied, in order. + pub applied: Vec, + /// Rows a version could only apply with foreign keys off, set back to NULL + /// as their key declares (ON DELETE SET NULL), as (table, rows). + pub fk_repaired: Vec<(String, i64)>, + /// Foreign key violations the database already had, found by the final + /// check, as (table, rows). Reported, never touched. + pub fk_violations: Vec<(String, i64)>, + /// File name of the copy taken before the first applied version. + pub backup_file: Option, +} + +/// What `prepare_at` did to one profile database. Paths never leave this +/// module: the report names files only, since error-level Journal lines can be +/// attached to a feedback report. +#[derive(Debug, Default, Clone, PartialEq, Serialize)] +pub struct ProfilePreparation { + pub catch_up: Option, + /// Why the catch-up stopped, when it did. The profile still opens. + pub catch_up_error: Option, + /// Rows whose manual-categorization flag was repaired. + pub repaired_flags: usize, + pub elapsed_ms: u64, +} + +fn read_err(e: rusqlite::Error) -> String { + format!("schema_read_failed: {}", e) +} + +fn table_exists(conn: &Connection, table: &str) -> Result { + conn.query_row( + "SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?1)", + [table], + |row| row.get(0), + ) + .map_err(read_err) +} + +fn column_exists(conn: &Connection, table: &str, column: &str) -> Result { + conn.query_row( + "SELECT EXISTS(SELECT 1 FROM pragma_table_info(?1) WHERE name = ?2)", + [table, column], + |row| row.get(0), + ) + .map_err(read_err) +} + +/// Highest migration version whose schema change this database carries, +/// checked newest first. Each marker is a column or table that version +/// introduced and that no earlier state had. +/// +/// Only databases created before #390 need it: since then a new profile gets +/// its history with its schema (`history_sql`). Those older databases form a +/// closed set, one state per consolidated schema a release shipped (fixtures +/// in `database/fixtures/`): v0.3.0-v0.8.3 → 6, v0.8.4 → 8, v0.9.0 and v0.9.1 +/// → 10, v0.10.0-v0.14.0 → 16, v0.15.0-v0.17.x → 17. The 14, 12 and 9 markers +/// cover databases a development build created between two releases. Never +/// add a marker for a later migration: no database without a history can +/// carry it. +pub fn infer_floor(conn: &Connection) -> Result { + // v17 — import format on the sources. `amount_mode` alone is no marker: + // import_config_templates has carried it since v5. + if column_exists(conn, "import_sources", "header_signature")? { + return Ok(17); + } + // v15 added `kind`. The only released state carrying it (v0.10.0-v0.14.0) + // was created with v16's data conversion already folded in. + if column_exists(conn, "balance_accounts", "kind")? { + return Ok(16); + } + if table_exists(conn, "balance_securities")? { + return Ok(14); + } + // v12; v13 is an idempotent reclass and is simply re-run. + if column_exists(conn, "balance_accounts", "vehicle_type")? { + return Ok(12); + } + // v10; v0.9.1 (v11, an idempotent orphan cleanup) has the same schema. + if column_exists(conn, "balance_categories", "asset_type")? { + return Ok(10); + } + if table_exists(conn, "balance_categories")? { + return Ok(9); + } + if column_exists(conn, "categories", "i18n_key")? { + return Ok(8); + } + // v3 to v6, all in the consolidated schema multi-profile shipped with. + if table_exists(conn, "import_config_templates")? + && column_exists(conn, "categories", "is_inputable")? + && column_exists(conn, "import_sources", "has_header")? + { + return Ok(6); + } + Err("unknown_layout".to_string()) +} + +fn checksum(sql: &str) -> Vec { + Sha384::digest(sql.as_bytes()).to_vec() +} + +fn recorded_versions(conn: &Connection) -> Result, String> { + if !table_exists(conn, "_sqlx_migrations")? { + return Ok(BTreeSet::new()); + } + let dirty: Option = conn + .query_row( + "SELECT MIN(version) FROM _sqlx_migrations WHERE success = 0", + [], + |row| row.get(0), + ) + .map_err(read_err)?; + if let Some(version) = dirty { + // Same refusal as sqlx: a half-applied version needs a human. + return Err(format!("dirty_history:v{}", version)); + } + let mut stmt = conn + .prepare("SELECT version FROM _sqlx_migrations") + .map_err(read_err)?; + let versions = stmt + .query_map([], |row| row.get::<_, i64>(0)) + .map_err(read_err)? + .collect::, _>>() + .map_err(read_err)?; + Ok(versions) +} + +fn record(conn: &Connection, migration: &Migration, execution_time: i64) -> rusqlite::Result { + conn.execute( + "INSERT INTO _sqlx_migrations (version, description, success, checksum, execution_time) \ + VALUES (?1, ?2, 1, ?3, ?4)", + params![ + migration.version, + migration.description, + checksum(migration.sql), + execution_time + ], + ) +} + +/// One row of `PRAGMA foreign_key_check`: (table, rowid, foreign key id). +type Violation = (String, Option, i64); + +fn violations(conn: &Connection) -> rusqlite::Result> { + let mut stmt = conn.prepare("PRAGMA foreign_key_check")?; + let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(3)?)))?; + rows.collect() +} + +fn count_by_table<'a>(tables: impl Iterator) -> Vec<(String, i64)> { + let mut counts: Vec<(String, i64)> = Vec::new(); + for table in tables { + match counts.iter_mut().find(|(name, _)| name == table) { + Some((_, n)) => *n += 1, + None => counts.push((table.to_string(), 1)), + } + } + counts.sort(); + counts +} + +fn quote_ident(name: &str) -> String { + format!("\"{}\"", name.replace('"', "\"\"")) +} + +fn is_foreign_key_failure(error: &rusqlite::Error) -> bool { + matches!( + error, + rusqlite::Error::SqliteFailure(failure, _) + if failure.extended_code == rusqlite::ffi::SQLITE_CONSTRAINT_FOREIGNKEY + ) +} + +/// Undo the violations a version introduced, as each key declares: only +/// `ON DELETE SET NULL` can be repaired (the column goes back to NULL). Any +/// other new violation is an error, so the caller rolls the version back. +fn repair_new_violations(conn: &Connection, before: &BTreeSet) -> Result, String> { + let new: Vec = violations(conn) + .map_err(|e| e.to_string())? + .into_iter() + .filter(|v| !before.contains(v)) + .collect(); + for (table, rowid, fkid) in &new { + let unrepairable = || format!("unrepairable foreign key in {}", table); + let rowid = rowid.ok_or_else(unrepairable)?; + let columns: Vec<(String, String)> = conn + .prepare("SELECT \"from\", on_delete FROM pragma_foreign_key_list(?1) WHERE id = ?2") + .and_then(|mut stmt| { + stmt.query_map(params![table, fkid], |row| Ok((row.get(0)?, row.get(1)?)))? + .collect() + }) + .map_err(|e| e.to_string())?; + if columns.is_empty() || columns.iter().any(|(_, action)| action != "SET NULL") { + return Err(unrepairable()); + } + let assignments = columns + .iter() + .map(|(column, _)| format!("{} = NULL", quote_ident(column))) + .collect::>() + .join(", "); + conn.execute( + &format!("UPDATE {} SET {} WHERE rowid = ?1", quote_ident(table), assignments), + [rowid], + ) + .map_err(|e| e.to_string())?; + } + let left = violations(conn) + .map_err(|e| e.to_string())? + .into_iter() + .any(|v| !before.contains(&v)); + if left { + return Err("foreign key violations left after the repair".to_string()); + } + Ok(count_by_table(new.iter().map(|(table, _, _)| table.as_str()))) +} + +fn run_in_transaction(conn: &mut Connection, migration: &Migration, started: Instant) -> rusqlite::Result<()> { + let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?; + tx.execute_batch(migration.sql)?; + record(&tx, migration, started.elapsed().as_nanos() as i64)?; + tx.commit() +} + +/// Second attempt of a version that failed on a foreign key: once, with +/// foreign keys off, then repair what it left. Foreign keys are back on +/// whatever happens. +fn retry_without_foreign_keys( + conn: &mut Connection, + migration: &Migration, + started: Instant, +) -> Result, String> { + conn.execute_batch("PRAGMA foreign_keys = OFF").map_err(|e| e.to_string())?; + let attempt = (|| -> Result, String> { + let before: BTreeSet = violations(conn).map_err(|e| e.to_string())?.into_iter().collect(); + let tx = conn + .transaction_with_behavior(TransactionBehavior::Immediate) + .map_err(|e| e.to_string())?; + tx.execute_batch(migration.sql).map_err(|e| e.to_string())?; + let repaired = repair_new_violations(&tx, &before)?; + record(&tx, migration, started.elapsed().as_nanos() as i64).map_err(|e| e.to_string())?; + tx.commit().map_err(|e| e.to_string())?; + Ok(repaired) + })(); + let restored = conn.execute_batch("PRAGMA foreign_keys = ON").map_err(|e| e.to_string()); + let repaired = attempt?; + restored?; + Ok(repaired) +} + +fn apply_pending(conn: &mut Connection, pending: &[&Migration], report: &mut CatchUp) -> Result<(), String> { + for migration in pending { + let started = Instant::now(); + // Dropping the transaction on an error rolls this version back; the + // earlier ones stay committed and recorded. + match run_in_transaction(conn, migration, started) { + Ok(()) => {} + Err(e) if is_foreign_key_failure(&e) => { + let repaired = retry_without_foreign_keys(conn, migration, started) + .map_err(|e| format!("migration_failed:v{}: {}", migration.version, e))?; + for (table, rows) in repaired { + match report.fk_repaired.iter_mut().find(|(name, _)| *name == table) { + Some((_, n)) => *n += rows, + None => report.fk_repaired.push((table, rows)), + } + } + } + Err(e) => return Err(format!("migration_failed:v{}: {}", migration.version, e)), + } + report.applied.push(migration.version); + } + Ok(()) +} + +/// Bring a secondary profile database to the last migration. +/// +/// - `floor` comes from the schema itself, not from the history, so an +/// interrupted first run can never make a version below it look pending. +/// That is what keeps the destructive v2 seed (DELETE FROM categories) from +/// ever being replayed onto a populated database. +/// - `backup(conn, first_pending)` runs before the first write, and only when +/// something is pending. A failed backup stops everything. +/// - The history (table and every version up to the floor) is written in one +/// transaction; then each pending version runs in its own transaction and +/// the first failure stops the run, earlier versions kept. +/// - Foreign keys stay on, as sqlx keeps them for the default profile. A +/// version that fails on a foreign key is retried once without them, then +/// the rows it left pointing nowhere are set back to NULL when their key +/// declares ON DELETE SET NULL; anything else rolls it back. The case it +/// exists for: v7 adds subcategories under category 31, which a profile +/// whose categories were restored from a newer export no longer has. +pub fn catch_up(conn: &mut Connection, migrations: &[Migration], backup: F) -> Result +where + F: FnOnce(&Connection, i64) -> Result, +{ + let floor = infer_floor(conn)?; + let recorded = recorded_versions(conn)?; + let has_history = table_exists(conn, "_sqlx_migrations")?; + + let mut ups: Vec<&Migration> = migrations + .iter() + .filter(|m| matches!(m.kind, MigrationKind::Up)) + .collect(); + ups.sort_by_key(|m| m.version); + let to_record: Vec<&Migration> = ups + .iter() + .copied() + .filter(|m| m.version <= floor && !recorded.contains(&m.version)) + .collect(); + let pending: Vec<&Migration> = ups + .iter() + .copied() + .filter(|m| m.version > floor && !recorded.contains(&m.version)) + .collect(); + + // Second line of defence, unreachable while `infer_floor` never returns + // below 6: the initial schema and the v2 seed must never run on a + // database that already holds transactions. + if pending.iter().any(|m| m.version <= 2) && table_exists(conn, "transactions")? { + return Err("refused_initial_migrations".to_string()); + } + + let mut report = CatchUp { + floor: Some(floor), + ..CatchUp::default() + }; + + if let Some(first) = pending.first() { + report.backup_file = Some(backup(conn, first.version)?); + } + + if !has_history || !to_record.is_empty() { + let tx = conn + .transaction_with_behavior(TransactionBehavior::Immediate) + .map_err(|e| format!("history_write_failed: {}", e))?; + tx.execute_batch(SQLX_MIGRATIONS_DDL) + .map_err(|e| format!("history_write_failed: {}", e))?; + for migration in &to_record { + record(&tx, migration, 0).map_err(|e| format!("history_write_failed: {}", e))?; + } + tx.commit().map_err(|e| format!("history_write_failed: {}", e))?; + report.recorded = to_record.iter().map(|m| m.version).collect(); + } + + if pending.is_empty() { + return Ok(report); + } + + // Foreign keys on, as sqlx runs the default profile's migrations. + conn.execute_batch("PRAGMA foreign_keys = ON") + .map_err(|e| format!("pragma_failed: {}", e))?; + if let Err(e) = apply_pending(conn, &pending, &mut report) { + return Err(if report.applied.is_empty() { + e + } else { + format!("{} (applied before stopping: {:?})", e, report.applied) + }); + } + let left = violations(conn).map_err(read_err)?; + report.fk_violations = count_by_table(left.iter().map(|(table, _, _)| table.as_str())); + Ok(report) +} + +/// Normalize the manual-categorization flag of every transaction stored as +/// text. Returns the number of rows changed; 0 on a database without the table. +pub fn repair_manual_flags(conn: &Connection) -> Result { + if !table_exists(conn, "transactions")? { + return Ok(0); + } + conn.execute(REPAIR_MANUAL_FLAGS_SQL, []) + .map_err(|e| format!("flag_repair_failed: {}", e)) +} + +/// Copy the database to `/-before-v.db` and return +/// the file name. The copy is written as `.tmp` and renamed only once +/// complete, so an existing final file is trusted as is and kept; a `.tmp` +/// left by an interrupted run is replaced. The path is bound, never formatted +/// into the SQL, so a folder such as `C:\Users\L'Heureux` works. +pub fn backup_to(conn: &Connection, dir: &Path, stem: &str, first_pending: i64) -> Result { + let name = format!("{}-before-v{}.db", stem, first_pending); + let final_path = dir.join(&name); + if final_path.exists() { + return Ok(name); + } + fs::create_dir_all(dir).map_err(|e| format!("backup_failed: {}", e))?; + let tmp_path = dir.join(format!("{}.tmp", name)); + if tmp_path.exists() { + fs::remove_file(&tmp_path).map_err(|e| format!("backup_failed: {}", e))?; + } + let tmp = tmp_path + .to_str() + .ok_or_else(|| "backup_failed: path is not valid UTF-8".to_string())?; + conn.execute("VACUUM INTO ?1", [tmp]) + .map_err(|e| format!("backup_failed: {}", e))?; + // VACUUM INTO does not sync its output; the rename must not publish a + // copy a power cut could still truncate. Write access: Windows refuses + // to flush a file opened read-only. + fs::OpenOptions::new() + .write(true) + .open(&tmp_path) + .and_then(|file| file.sync_all()) + .map_err(|e| format!("backup_failed: {}", e))?; + fs::rename(&tmp_path, &final_path).map_err(|e| format!("backup_failed: {}", e))?; + #[cfg(unix)] + if let Ok(folder) = fs::File::open(dir) { + // Best effort: makes the rename itself durable. + let _ = folder.sync_all(); + } + Ok(name) +} + +/// SQL that writes the complete history of a database created from the +/// current consolidated schema. `get_new_profile_init_sql` returns it right +/// after the schema, so a new profile is born with its history and never goes +/// through `infer_floor`. `INSERT OR IGNORE`: a version already recorded keeps +/// its row. +pub fn history_sql(migrations: &[Migration]) -> String { + let mut sql = String::from(SQLX_MIGRATIONS_DDL); + for migration in migrations.iter().filter(|m| matches!(m.kind, MigrationKind::Up)) { + let hex: String = checksum(migration.sql).iter().map(|b| format!("{:02X}", b)).collect(); + sql.push_str(&format!( + "\nINSERT OR IGNORE INTO _sqlx_migrations (version, description, success, checksum, execution_time) \ + VALUES ({}, '{}', 1, X'{}', 0);", + migration.version, + migration.description.replace('\'', "''"), + hex + )); + } + sql +} + +/// Replace the profile folder in a message with a placeholder. The folder +/// holds the OS user name, and error-level Journal lines can be attached to a +/// feedback report (Loi 25). +pub fn scrub(message: String, dir: &Path) -> String { + let full = dir.to_string_lossy(); + if full.is_empty() { + return message; + } + message.replace(full.as_ref(), "") +} + +/// Remove the catch-up copies of one profile (and any `.tmp` left behind). +/// Returns how many files were removed. +pub fn remove_backups(dir: &Path, stem: &str) -> Result { + let entries = match fs::read_dir(dir) { + Ok(entries) => entries, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(0), + Err(e) => return Err(format!("backup_cleanup_failed: {}", e)), + }; + let prefix = format!("{}-before-v", stem); + let mut removed = 0; + for entry in entries { + let entry = entry.map_err(|e| format!("backup_cleanup_failed: {}", e))?; + let name = entry.file_name(); + let Some(name) = name.to_str() else { continue }; + if name.starts_with(&prefix) && (name.ends_with(".db") || name.ends_with(".db.tmp")) { + fs::remove_file(entry.path()).map_err(|e| format!("backup_cleanup_failed: {}", e))?; + removed += 1; + } + } + Ok(removed) +} + +/// Prepare one profile database before tauri-plugin-sql opens it. +/// +/// A missing file is left alone: never create a database here, the plugin +/// does that on first load. The default profile is never caught up here (the +/// plugin migrates it); every profile gets the flag repair. A catch-up that +/// stops is reported in `catch_up_error` and the profile still opens, like it +/// did before #390; only a database that cannot be opened is an error. +pub fn prepare_at(db_path: &Path, db_filename: &str, migrations: &[Migration]) -> Result { + let started = Instant::now(); + let mut report = ProfilePreparation::default(); + if !db_path.exists() { + return Ok(report); + } + let dir = db_path + .parent() + .ok_or_else(|| "open_failed: database has no parent folder".to_string())?; + let mut conn = Connection::open_with_flags( + db_path, + OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_URI | OpenFlags::SQLITE_OPEN_NO_MUTEX, + ) + .map_err(|e| scrub(format!("open_failed: {}", e), dir))?; + conn.busy_timeout(Duration::from_secs(5)) + .map_err(|e| scrub(format!("open_failed: {}", e), dir))?; + + if db_filename != DEFAULT_PROFILE_DB { + let backup_dir = dir.join(MIGRATION_BACKUP_DIR); + let stem = db_filename.trim_end_matches(".db"); + match catch_up(&mut conn, migrations, |conn, first| backup_to(conn, &backup_dir, stem, first)) { + Ok(done) => report.catch_up = Some(done), + Err(e) => report.catch_up_error = Some(scrub(e, dir)), + } + } + + report.repaired_flags = repair_manual_flags(&conn).map_err(|e| scrub(e, dir))?; + report.elapsed_ms = started.elapsed().as_millis() as u64; + Ok(report) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::BTreeMap; + use std::path::PathBuf; + use std::sync::atomic::{AtomicUsize, Ordering}; + + // The SQL each release ran to create a secondary profile + // (`get_new_profile_init_sql` of that tag), copied verbatim from git. + const CONSOLIDATED_V0_3_0: &str = include_str!("fixtures/consolidated_v0.3.0.sql"); + const SEED_V0_3_0: &str = include_str!("fixtures/seed_v0.3.0.sql"); + const SEED_V0_4_0: &str = include_str!("fixtures/seed_v0.4.0.sql"); + const CONSOLIDATED_V0_8_4: &str = include_str!("fixtures/consolidated_v0.8.4.sql"); + const CONSOLIDATED_V0_9_0: &str = include_str!("fixtures/consolidated_v0.9.0.sql"); + const CONSOLIDATED_V0_9_1: &str = include_str!("fixtures/consolidated_v0.9.1.sql"); + const CONSOLIDATED_V0_10_0: &str = include_str!("fixtures/consolidated_v0.10.0.sql"); + + const SECONDARY: &str = "profile_0a1b2c3d.db"; + + struct State { + name: &'static str, + init: Vec<&'static str>, + floor: i64, + } + + fn released_states() -> Vec { + vec![ + State { name: "A1 v0.3.0-v0.8.3", init: vec![CONSOLIDATED_V0_3_0, SEED_V0_3_0], floor: 6 }, + State { name: "A2 v0.4.0", init: vec![CONSOLIDATED_V0_3_0, SEED_V0_4_0], floor: 6 }, + State { name: "B v0.8.4", init: vec![CONSOLIDATED_V0_8_4], floor: 8 }, + State { name: "C v0.9.0", init: vec![CONSOLIDATED_V0_9_0], floor: 10 }, + State { name: "D v0.9.1", init: vec![CONSOLIDATED_V0_9_1], floor: 10 }, + State { name: "E v0.10.0-v0.14.0", init: vec![CONSOLIDATED_V0_10_0], floor: 16 }, + ] + } + + fn migrations() -> Vec { + crate::migrations() + } + + fn max_version() -> i64 { + migrations().iter().map(|m| m.version).max().unwrap() + } + + fn versions(from: i64, to: i64) -> Vec { + (from..=to).collect() + } + + /// Cargo runs tests in parallel: every test gets its own folder. + fn unique_dir(tag: &str) -> PathBuf { + static COUNTER: AtomicUsize = AtomicUsize::new(0); + let dir = std::env::temp_dir().join(format!( + "simpl-resultat-profile-db-{}-{}-{}", + std::process::id(), + tag, + COUNTER.fetch_add(1, Ordering::SeqCst) + )); + let _ = fs::remove_dir_all(&dir); + fs::create_dir_all(&dir).unwrap(); + dir + } + + fn create_db(dir: &Path, name: &str, init: &[&str]) -> PathBuf { + let path = dir.join(name); + let conn = Connection::open(&path).unwrap(); + for sql in init { + conn.execute_batch(sql).unwrap(); + } + path + } + + fn open(path: &Path) -> Connection { + Connection::open(path).unwrap() + } + + fn count(conn: &Connection, sql: &str) -> i64 { + conn.query_row(sql, [], |row| row.get(0)).unwrap() + } + + fn has_table(conn: &Connection, table: &str) -> bool { + table_exists(conn, table).unwrap() + } + + fn no_backup(_: &Connection, _: i64) -> Result { + Ok("not-written.db".to_string()) + } + + /// One source whose mapping reads debit/credit columns, one file, and one + /// transaction classified by hand while the bug stood (TEXT 'true'). + /// Returns the transaction's category. + fn seed_user_rows(conn: &Connection) -> i64 { + conn.execute( + "INSERT INTO import_sources (name, date_format, delimiter, encoding, column_mapping, skip_lines, has_header) \ + VALUES ('Visa', 'YYYY-MM-DD', ',', 'utf-8', \ + '{\"date\":0,\"description\":1,\"debitAmount\":2,\"creditAmount\":3}', 0, 1)", + [], + ) + .unwrap(); + let source = conn.last_insert_rowid(); + conn.execute( + "INSERT INTO imported_files (source_id, filename, file_hash, row_count, status) \ + VALUES (?1, 'visa.csv', 'hash', 1, 'completed')", + [source], + ) + .unwrap(); + let file = conn.last_insert_rowid(); + let category = count(conn, "SELECT MIN(id) FROM categories"); + conn.execute( + "INSERT INTO transactions (date, description, amount, category_id, source_id, file_id, is_manually_categorized) \ + VALUES ('2026-03-02', 'IGA', -42.5, ?1, ?2, ?3, 'true')", + params![category, source, file], + ) + .unwrap(); + category + } + + /// An envelope account in the old `tfsa` category, a stock account with a + /// priced line, and an orphan snapshot: what v11, v12, v13, v15 and v16 + /// exist to transform. + fn seed_balance_rows(conn: &Connection) { + conn.execute( + "INSERT INTO balance_accounts (balance_category_id, name, currency) \ + VALUES ((SELECT id FROM balance_categories WHERE key = 'tfsa'), 'CELI', 'CAD')", + [], + ) + .unwrap(); + conn.execute( + "INSERT INTO balance_accounts (balance_category_id, name, symbol, currency) \ + VALUES ((SELECT id FROM balance_categories WHERE key = 'stock'), 'Apple', ' aapl ', 'CAD')", + [], + ) + .unwrap(); + let apple = conn.last_insert_rowid(); + conn.execute("INSERT INTO balance_snapshots (snapshot_date) VALUES ('2026-04-30')", []) + .unwrap(); + let snapshot = conn.last_insert_rowid(); + conn.execute( + "INSERT INTO balance_snapshot_lines (snapshot_id, account_id, quantity, unit_price, value) \ + VALUES (?1, ?2, 10, 150, 1500)", + params![snapshot, apple], + ) + .unwrap(); + conn.execute("INSERT INTO balance_snapshots (snapshot_date) VALUES ('2026-03-31')", []) + .unwrap(); + } + + // -- schema comparison -------------------------------------------------- + + type ColumnShape = (String, String, i64, Option, i64); + type ForeignKeyShape = (String, String, Option, String, String); + type IndexShape = (String, i64, Vec); + + #[derive(Debug, PartialEq)] + struct Shape { + columns: BTreeMap>, + foreign_keys: BTreeMap>, + indexes: BTreeMap>, + } + + /// Tables, columns (as a set: ALTER appends where the consolidated schema + /// declares mid-table), foreign keys and indexes. Automatic index names + /// depend on table history, so they compare by origin and columns. + fn shape(conn: &Connection) -> Shape { + let tables: Vec = conn + .prepare( + "SELECT name FROM sqlite_master WHERE type = 'table' \ + AND name NOT LIKE 'sqlite_%' AND name <> '_sqlx_migrations' ORDER BY name", + ) + .unwrap() + .query_map([], |row| row.get(0)) + .unwrap() + .collect::>() + .unwrap(); + let mut result = Shape { + columns: BTreeMap::new(), + foreign_keys: BTreeMap::new(), + indexes: BTreeMap::new(), + }; + for table in tables { + let columns: BTreeSet = conn + .prepare("SELECT name, type, \"notnull\", dflt_value, pk FROM pragma_table_info(?1)") + .unwrap() + .query_map([&table], |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?, r.get(4)?))) + .unwrap() + .collect::>() + .unwrap(); + let foreign_keys: BTreeSet = conn + .prepare( + "SELECT \"table\", \"from\", \"to\", on_update, on_delete FROM pragma_foreign_key_list(?1)", + ) + .unwrap() + .query_map([&table], |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?, r.get(4)?))) + .unwrap() + .collect::>() + .unwrap(); + let listed: Vec<(String, i64, String)> = conn + .prepare("SELECT name, \"unique\", origin FROM pragma_index_list(?1)") + .unwrap() + .query_map([&table], |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?))) + .unwrap() + .collect::>() + .unwrap(); + let mut indexes = BTreeSet::new(); + for (name, unique, origin) in listed { + let columns: Vec = conn + .prepare("SELECT name FROM pragma_index_info(?1) ORDER BY seqno") + .unwrap() + .query_map([&name], |r| r.get::<_, Option>(0)) + .unwrap() + .map(|c| c.unwrap().unwrap_or_else(|| "".to_string())) + .collect(); + let key = if origin == "c" { name } else { origin }; + indexes.insert((key, unique, columns)); + } + result.columns.insert(table.clone(), columns); + result.foreign_keys.insert(table.clone(), foreign_keys); + result.indexes.insert(table, indexes); + } + result + } + + fn fresh_profile_shape() -> Shape { + let dir = unique_dir("reference"); + let path = create_db(&dir, SECONDARY, &[crate::database::CONSOLIDATED_SCHEMA]); + shape(&open(&path)) + } + + /// Every version recorded once, applied, with the checksum sqlx computes. + fn assert_history_complete(conn: &Connection, context: &str) { + let rows: Vec<(i64, i64, Vec)> = conn + .prepare("SELECT version, success, checksum FROM _sqlx_migrations ORDER BY version") + .unwrap() + .query_map([], |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?))) + .unwrap() + .collect::>() + .unwrap(); + let expected: Vec<(i64, i64, Vec)> = migrations() + .iter() + .map(|m| (m.version, 1, checksum(m.sql))) + .collect(); + assert_eq!(rows, expected, "{}", context); + } + + // -- the released states ------------------------------------------------ + + #[test] + fn every_released_state_infers_its_floor() { + for state in released_states() { + let dir = unique_dir("floor"); + let path = create_db(&dir, SECONDARY, &state.init); + assert_eq!(infer_floor(&open(&path)), Ok(state.floor), "{}", state.name); + } + } + + #[test] + fn every_released_state_catches_up_to_the_consolidated_schema() { + let reference = fresh_profile_shape(); + let max = max_version(); + for state in released_states() { + let dir = unique_dir("released"); + let path = create_db(&dir, SECONDARY, &state.init); + let category = seed_user_rows(&open(&path)); + + let report = prepare_at(&path, SECONDARY, &migrations()).unwrap(); + + assert_eq!(report.catch_up_error, None, "{}", state.name); + let done = report.catch_up.clone().unwrap(); + assert_eq!(done.floor, Some(state.floor), "{}", state.name); + assert_eq!(done.recorded, versions(1, state.floor), "{}", state.name); + assert_eq!(done.applied, versions(state.floor + 1, max), "{}", state.name); + assert_eq!(done.fk_repaired, Vec::<(String, i64)>::new(), "{}", state.name); + assert_eq!(done.fk_violations, Vec::<(String, i64)>::new(), "{}", state.name); + let expected_backup = format!("profile_0a1b2c3d-before-v{}.db", state.floor + 1); + assert_eq!(done.backup_file.as_deref(), Some(expected_backup.as_str()), "{}", state.name); + assert_eq!(report.repaired_flags, 1, "{}", state.name); + + let conn = open(&path); + assert_eq!(shape(&conn), reference, "{}", state.name); + assert_history_complete(&conn, state.name); + let flag: (i64, String, i64) = conn + .query_row( + "SELECT is_manually_categorized, typeof(is_manually_categorized), category_id FROM transactions", + [], + |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)), + ) + .unwrap(); + assert_eq!(flag, (1, "integer".to_string(), category), "{}", state.name); + let mode: String = conn + .query_row("SELECT amount_mode FROM import_sources WHERE name = 'Visa'", [], |r| r.get(0)) + .unwrap(); + assert_eq!(mode, "debit_credit", "{}: v17 backfill", state.name); + assert!(dir.join(MIGRATION_BACKUP_DIR).join(&expected_backup).exists(), "{}", state.name); + } + } + + #[test] + fn the_bilan_data_of_a_v0_9_profile_is_converted() { + for state in released_states().into_iter().filter(|s| s.floor == 10) { + let dir = unique_dir("bilan"); + let path = create_db(&dir, SECONDARY, &state.init); + seed_balance_rows(&open(&path)); + + let report = prepare_at(&path, SECONDARY, &migrations()).unwrap(); + assert_eq!(report.catch_up_error, None, "{}", state.name); + + let conn = open(&path); + // v11: the snapshot without lines is gone. + assert_eq!(count(&conn, "SELECT COUNT(*) FROM balance_snapshots"), 1, "{}", state.name); + // v12 then v13: the envelope moves onto the account, which leaves the + // deprecated `tfsa` category for `other`. + let celi: (String, String) = conn + .query_row( + "SELECT a.vehicle_type, c.key FROM balance_accounts a \ + JOIN balance_categories c ON c.id = a.balance_category_id WHERE a.name = 'CELI'", + [], + |r| Ok((r.get(0)?, r.get(1)?)), + ) + .unwrap(); + assert_eq!(celi, ("tfsa".to_string(), "other".to_string()), "{}", state.name); + assert_eq!( + count(&conn, "SELECT COUNT(*) FROM balance_categories WHERE key IN ('tfsa','rrsp') AND is_active = 1"), + 0, + "{}", + state.name + ); + // v15 then v16: the priced account becomes detailed, holding one position. + let kind: String = conn + .query_row("SELECT kind FROM balance_accounts WHERE name = 'Apple'", [], |r| r.get(0)) + .unwrap(); + assert_eq!(kind, "detailed", "{}", state.name); + let holding: (String, String, f64, f64, f64) = conn + .query_row( + "SELECT s.symbol, s.asset_type, h.quantity, h.unit_price, h.value \ + FROM balance_snapshot_holdings h JOIN balance_securities s ON s.id = h.security_id", + [], + |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?, r.get(4)?)), + ) + .unwrap(); + assert_eq!(holding, ("AAPL".to_string(), "stock".to_string(), 10.0, 150.0, 1500.0), "{}", state.name); + let line: (Option, Option, f64) = conn + .query_row("SELECT quantity, unit_price, value FROM balance_snapshot_lines", [], |r| { + Ok((r.get(0)?, r.get(1)?, r.get(2)?)) + }) + .unwrap(); + assert_eq!(line, (None, None, 1500.0), "{}", state.name); + } + } + + #[test] + fn development_states_catch_up_too() { + let all = migrations(); + let run = |conn: &Connection, wanted: &[i64]| { + for version in wanted { + let migration = all.iter().find(|m| m.version == *version).unwrap(); + conn.execute_batch(migration.sql).unwrap(); + } + }; + let reference = fresh_profile_shape(); + let max = max_version(); + let cases: Vec<(&str, &str, Vec, i64)> = vec![ + ("v0.8.4 + v9", CONSOLIDATED_V0_8_4, vec![9], 9), + ("v0.9.0 + v12", CONSOLIDATED_V0_9_0, vec![12], 12), + ("v0.9.0 + v12..v14", CONSOLIDATED_V0_9_0, vec![12, 13, 14], 14), + ]; + for (name, init, extra, floor) in cases { + let dir = unique_dir("dev"); + let path = create_db(&dir, SECONDARY, &[init]); + run(&open(&path), &extra); + + let report = prepare_at(&path, SECONDARY, &migrations()).unwrap(); + + assert_eq!(report.catch_up_error, None, "{}", name); + let done = report.catch_up.unwrap(); + assert_eq!(done.floor, Some(floor), "{}", name); + assert_eq!(done.applied, versions(floor + 1, max), "{}", name); + let conn = open(&path); + assert_eq!(shape(&conn), reference, "{}", name); + assert_history_complete(&conn, name); + } + } + + #[test] + fn a_new_profile_is_born_with_its_history() { + // What createProfile runs, statement by statement, through the plugin. + let dir = unique_dir("new"); + let init = crate::commands::get_new_profile_init_sql().unwrap(); + let statements: Vec<&str> = init.iter().map(String::as_str).collect(); + let path = create_db(&dir, SECONDARY, &statements); + let mut conn = open(&path); + assert_history_complete(&conn, "new profile"); + + let report = catch_up(&mut conn, &migrations(), |_: &Connection, _: i64| -> Result { + panic!("a new profile needs no backup") + }) + .unwrap(); + + assert!(report.recorded.is_empty()); + assert!(report.applied.is_empty()); + assert_eq!(report.backup_file, None); + assert!(!dir.join(MIGRATION_BACKUP_DIR).exists()); + } + + #[test] + fn the_legacy_floor_stops_at_v17() { + // `infer_floor` serves only databases created before #390. The current + // consolidated schema must still carry the newest legacy marker; a + // later migration needs no marker of its own. + let dir = unique_dir("legacy-top"); + let path = create_db(&dir, SECONDARY, &[crate::database::CONSOLIDATED_SCHEMA]); + assert_eq!(infer_floor(&open(&path)), Ok(17)); + } + + #[test] + fn history_sql_escapes_descriptions() { + let list = vec![Migration { + version: 1, + description: "the user's file", + sql: "SELECT 1;", + kind: MigrationKind::Up, + }]; + let conn = Connection::open_in_memory().unwrap(); + conn.execute_batch(&history_sql(&list)).unwrap(); + let description: String = conn + .query_row("SELECT description FROM _sqlx_migrations", [], |r| r.get(0)) + .unwrap(); + assert_eq!(description, "the user's file"); + } + + #[test] + fn scrub_removes_the_profile_folder() { + let dir = Path::new("/home/someone/.config/com.simpl.resultat"); + let message = "backup_failed: unable to open database: /home/someone/.config/com.simpl.resultat/migration-backups/x.tmp".to_string(); + assert_eq!( + scrub(message, dir), + "backup_failed: unable to open database: /migration-backups/x.tmp" + ); + } + + #[test] + fn a_second_open_changes_nothing() { + for state in released_states() { + let dir = unique_dir("twice"); + let path = create_db(&dir, SECONDARY, &state.init); + seed_user_rows(&open(&path)); + prepare_at(&path, SECONDARY, &migrations()).unwrap(); + + let again = prepare_at(&path, SECONDARY, &migrations()).unwrap(); + + assert_eq!(again.catch_up_error, None, "{}", state.name); + let done = again.catch_up.unwrap(); + assert!(done.recorded.is_empty(), "{}", state.name); + assert!(done.applied.is_empty(), "{}", state.name); + assert_eq!(done.backup_file, None, "{}", state.name); + assert_eq!(again.repaired_flags, 0, "{}", state.name); + assert_eq!(fs::read_dir(dir.join(MIGRATION_BACKUP_DIR)).unwrap().count(), 1, "{}", state.name); + } + } + + // -- an interrupted first run ------------------------------------------- + + #[test] + fn an_interrupted_first_run_never_replays_the_seed() { + // A history table without its rows is what a crash between the CREATE + // and the INSERTs of a non-atomic first write would leave. Replaying v2 + // would empty the categories and every transaction's category. + for partial in [vec![], vec![1_i64, 2, 3]] { + let dir = unique_dir("partial"); + let path = create_db(&dir, SECONDARY, &[CONSOLIDATED_V0_3_0, SEED_V0_3_0]); + let category; + { + let conn = open(&path); + category = seed_user_rows(&conn); + conn.execute( + "INSERT INTO categories (id, name, type, sort_order) VALUES (9001, 'Mine', 'expense', 99)", + [], + ) + .unwrap(); + conn.execute_batch(SQLX_MIGRATIONS_DDL).unwrap(); + for migration in migrations().iter().filter(|m| partial.contains(&m.version)) { + record(&conn, migration, 0).unwrap(); + } + } + let categories_before = count(&open(&path), "SELECT COUNT(*) FROM categories"); + + let report = prepare_at(&path, SECONDARY, &migrations()).unwrap(); + + assert_eq!(report.catch_up_error, None, "{:?}", partial); + let done = report.catch_up.unwrap(); + let expected_recorded: Vec = versions(1, 6).into_iter().filter(|v| !partial.contains(v)).collect(); + assert_eq!(done.recorded, expected_recorded, "{:?}", partial); + assert_eq!(done.applied, versions(7, max_version()), "{:?}", partial); + let conn = open(&path); + // v7 adds the three insurance subcategories, nothing is deleted. + assert_eq!(count(&conn, "SELECT COUNT(*) FROM categories"), categories_before + 3, "{:?}", partial); + assert_eq!(count(&conn, "SELECT COUNT(*) FROM categories WHERE id = 9001"), 1, "{:?}", partial); + assert_eq!(count(&conn, "SELECT category_id FROM transactions"), category, "{:?}", partial); + assert_history_complete(&conn, "partial"); + } + } + + #[test] + fn a_profile_without_category_31_still_catches_up() { + // A restore of a newer export replaces the v2 taxonomy, so v7's + // subcategories have no parent to point at. v7 is retried without + // foreign keys, and its three rows become root categories, as the key's + // ON DELETE SET NULL declares. + let dir = unique_dir("no31"); + let path = create_db(&dir, SECONDARY, &[CONSOLIDATED_V0_3_0, SEED_V0_3_0]); + open(&path) + .execute_batch( + "DELETE FROM keywords; DELETE FROM categories; \ + INSERT INTO categories (id, name, type, sort_order) VALUES (1000, 'Logement', 'expense', 1);", + ) + .unwrap(); + + let report = prepare_at(&path, SECONDARY, &migrations()).unwrap(); + + assert_eq!(report.catch_up_error, None); + let done = report.catch_up.unwrap(); + assert_eq!(done.applied, versions(7, max_version())); + assert_eq!(done.fk_repaired, vec![("categories".to_string(), 3)]); + assert_eq!(done.fk_violations, Vec::<(String, i64)>::new()); + let conn = open(&path); + assert_history_complete(&conn, "no31"); + assert!(violations(&conn).unwrap().is_empty()); + assert_eq!( + count(&conn, "SELECT COUNT(*) FROM categories WHERE id IN (310, 311, 312) AND parent_id IS NULL"), + 3 + ); + // What a restore of this profile's own export does: every category + // inserted in id order, foreign keys on. + let create_sql: String = conn + .query_row("SELECT sql FROM sqlite_master WHERE name = 'categories'", [], |r| r.get(0)) + .unwrap(); + let copy = Connection::open_in_memory().unwrap(); + copy.execute_batch(&format!("PRAGMA foreign_keys = ON; {};", create_sql)).unwrap(); + let rows: Vec<(i64, String, Option, String)> = conn + .prepare("SELECT id, name, parent_id, type FROM categories ORDER BY id") + .unwrap() + .query_map([], |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?))) + .unwrap() + .collect::>() + .unwrap(); + for (id, name, parent, kind) in rows { + copy.execute( + "INSERT INTO categories (id, name, parent_id, type) VALUES (?1, ?2, ?3, ?4)", + params![id, name, parent, kind], + ) + .unwrap(); + } + } + + #[test] + fn a_foreign_key_that_cannot_be_set_to_null_rolls_the_version_back() { + let dir = unique_dir("unrepairable"); + let path = create_db(&dir, SECONDARY, &[CONSOLIDATED_V0_10_0]); + let mut list: Vec = migrations().into_iter().filter(|m| m.version <= 16).collect(); + // Both keys of a snapshot line are CASCADE / RESTRICT, never SET NULL. + list.push(Migration { + version: 17, + description: "points nowhere", + sql: "INSERT INTO balance_snapshot_lines (snapshot_id, account_id, value) VALUES (999, 999, 1);", + kind: MigrationKind::Up, + }); + let mut conn = open(&path); + + let error = catch_up(&mut conn, &list, no_backup).unwrap_err(); + + assert!(error.starts_with("migration_failed:v17: unrepairable foreign key"), "{}", error); + assert_eq!(count(&conn, "SELECT COUNT(*) FROM balance_snapshot_lines"), 0); + assert_eq!(count(&conn, "SELECT MAX(version) FROM _sqlx_migrations"), 16); + let foreign_keys: i64 = conn.query_row("PRAGMA foreign_keys", [], |r| r.get(0)).unwrap(); + assert_eq!(foreign_keys, 1); + } + + #[test] + fn an_unknown_layout_is_left_untouched() { + let dir = unique_dir("unknown"); + let path = create_db(&dir, SECONDARY, &["CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);"]); + + let report = prepare_at(&path, SECONDARY, &migrations()).unwrap(); + + assert_eq!(report.catch_up_error.as_deref(), Some("unknown_layout")); + assert_eq!(report.repaired_flags, 0); + let conn = open(&path); + assert!(!has_table(&conn, "_sqlx_migrations")); + assert!(!dir.join(MIGRATION_BACKUP_DIR).exists()); + } + + #[test] + fn a_dirty_history_is_refused() { + let dir = unique_dir("dirty"); + let path = create_db(&dir, SECONDARY, &[CONSOLIDATED_V0_10_0]); + open(&path) + .execute_batch(&format!( + "{} INSERT INTO _sqlx_migrations (version, description, success, checksum, execution_time) \ + VALUES (17, 'half', 0, x'00', -1);", + SQLX_MIGRATIONS_DDL + )) + .unwrap(); + + let report = prepare_at(&path, SECONDARY, &migrations()).unwrap(); + + assert_eq!(report.catch_up_error.as_deref(), Some("dirty_history:v17")); + assert!(!column_exists(&open(&path), "import_sources", "header_signature").unwrap()); + } + + #[test] + fn a_failing_version_stops_the_run_and_keeps_the_earlier_ones() { + let dir = unique_dir("failing"); + let path = create_db(&dir, SECONDARY, &[CONSOLIDATED_V0_10_0]); + let mut list: Vec = migrations().into_iter().filter(|m| m.version <= 16).collect(); + list.push(Migration { + version: 17, + description: "works", + sql: "CREATE TABLE ok_one (x INTEGER);", + kind: MigrationKind::Up, + }); + list.push(Migration { + version: 18, + description: "breaks halfway", + sql: "CREATE TABLE half_done (x INTEGER); INSERT INTO no_such_table VALUES (1);", + kind: MigrationKind::Up, + }); + let mut conn = open(&path); + + let error = catch_up(&mut conn, &list, no_backup).unwrap_err(); + + assert!(error.starts_with("migration_failed:v18"), "{}", error); + assert!(error.ends_with("(applied before stopping: [17])"), "{}", error); + assert!(has_table(&conn, "ok_one")); + assert!(!has_table(&conn, "half_done"), "the failing version is rolled back"); + assert_eq!(count(&conn, "SELECT MAX(version) FROM _sqlx_migrations"), 17); + let foreign_keys: i64 = conn.query_row("PRAGMA foreign_keys", [], |r| r.get(0)).unwrap(); + assert_eq!(foreign_keys, 1, "foreign keys are back on after a failure"); + } + + // -- the backup --------------------------------------------------------- + + #[test] + fn the_backup_holds_the_database_as_it_was() { + let dir = unique_dir("backup"); + let path = create_db(&dir, SECONDARY, &[CONSOLIDATED_V0_10_0]); + seed_user_rows(&open(&path)); + + let report = prepare_at(&path, SECONDARY, &migrations()).unwrap(); + + let name = report.catch_up.unwrap().backup_file.unwrap(); + assert_eq!(name, "profile_0a1b2c3d-before-v17.db"); + let copy = open(&dir.join(MIGRATION_BACKUP_DIR).join(&name)); + assert!(!column_exists(©, "import_sources", "header_signature").unwrap()); + assert!(!has_table(©, "_sqlx_migrations")); + let flag: String = copy + .query_row("SELECT typeof(is_manually_categorized) FROM transactions", [], |r| r.get(0)) + .unwrap(); + assert_eq!(flag, "text", "taken before the repair too"); + assert!(!dir.join(MIGRATION_BACKUP_DIR).join(format!("{}.tmp", name)).exists()); + } + + #[test] + fn an_interrupted_backup_is_replaced() { + let dir = unique_dir("stale-tmp"); + let path = create_db(&dir, SECONDARY, &[CONSOLIDATED_V0_10_0]); + let backups = dir.join(MIGRATION_BACKUP_DIR); + fs::create_dir_all(&backups).unwrap(); + let tmp = backups.join("profile_0a1b2c3d-before-v17.db.tmp"); + fs::write(&tmp, b"half a copy").unwrap(); + + let report = prepare_at(&path, SECONDARY, &migrations()).unwrap(); + + assert_eq!(report.catch_up_error, None); + assert!(!tmp.exists()); + let copy = open(&backups.join("profile_0a1b2c3d-before-v17.db")); + assert!(has_table(©, "transactions")); + } + + #[test] + fn a_complete_backup_is_kept() { + let dir = unique_dir("kept"); + let path = create_db(&dir, SECONDARY, &[CONSOLIDATED_V0_10_0]); + let backups = dir.join(MIGRATION_BACKUP_DIR); + fs::create_dir_all(&backups).unwrap(); + let existing = backups.join("profile_0a1b2c3d-before-v17.db"); + fs::write(&existing, b"earlier copy").unwrap(); + + let report = prepare_at(&path, SECONDARY, &migrations()).unwrap(); + + assert_eq!(report.catch_up.unwrap().applied, vec![17]); + assert_eq!(fs::read(&existing).unwrap(), b"earlier copy"); + } + + #[test] + fn a_failed_backup_writes_nothing() { + let dir = unique_dir("no-backup"); + let path = create_db(&dir, SECONDARY, &[CONSOLIDATED_V0_10_0]); + // A file where the folder should be: create_dir_all fails. + fs::write(dir.join(MIGRATION_BACKUP_DIR), b"not a folder").unwrap(); + + let report = prepare_at(&path, SECONDARY, &migrations()).unwrap(); + + let error = report.catch_up_error.unwrap(); + assert!(error.starts_with("backup_failed"), "{}", error); + assert!(!error.contains(dir.to_str().unwrap()), "{}", error); + let conn = open(&path); + assert!(!has_table(&conn, "_sqlx_migrations")); + assert!(!column_exists(&conn, "import_sources", "header_signature").unwrap()); + } + + #[test] + fn remove_backups_takes_only_this_profile() { + let dir = unique_dir("cleanup"); + for name in [ + "profile_0a1b2c3d-before-v7.db", + "profile_0a1b2c3d-before-v17.db.tmp", + "profile_99999999-before-v17.db", + "profile_0a1b2c3d-notes.txt", + ] { + fs::write(dir.join(name), b"x").unwrap(); + } + + assert_eq!(remove_backups(&dir, "profile_0a1b2c3d"), Ok(2)); + let mut left: Vec = fs::read_dir(&dir) + .unwrap() + .map(|e| e.unwrap().file_name().into_string().unwrap()) + .collect(); + left.sort(); + assert_eq!(left, vec!["profile_0a1b2c3d-notes.txt", "profile_99999999-before-v17.db"]); + assert_eq!(remove_backups(&dir.join("absent"), "profile_0a1b2c3d"), Ok(0)); + } + + // -- the default profile, missing files and the repair ------------------ + + #[test] + fn the_default_profile_is_only_repaired() { + let dir = unique_dir("default"); + let path = create_db(&dir, DEFAULT_PROFILE_DB, &[CONSOLIDATED_V0_3_0, SEED_V0_3_0]); + seed_user_rows(&open(&path)); + + let report = prepare_at(&path, DEFAULT_PROFILE_DB, &migrations()).unwrap(); + + assert_eq!(report.catch_up, None); + assert_eq!(report.catch_up_error, None); + assert_eq!(report.repaired_flags, 1); + let conn = open(&path); + assert!(!has_table(&conn, "_sqlx_migrations"), "tauri-plugin-sql migrates the default profile"); + assert!(!dir.join(MIGRATION_BACKUP_DIR).exists()); + } + + #[test] + fn a_missing_database_is_not_created() { + let dir = unique_dir("missing"); + let path = dir.join(SECONDARY); + + let report = prepare_at(&path, SECONDARY, &migrations()).unwrap(); + + assert_eq!(report, ProfilePreparation { elapsed_ms: report.elapsed_ms, ..ProfilePreparation::default() }); + assert!(!path.exists()); + } + + #[test] + fn repair_turns_text_flags_into_integers_and_leaves_integers_alone() { + let dir = unique_dir("repair"); + let path = create_db(&dir, SECONDARY, &[crate::database::CONSOLIDATED_SCHEMA]); + let conn = open(&path); + for flag in ["'true'", "1", "0", "'false'", "'yes'"] { + conn.execute( + &format!( + "INSERT INTO transactions (date, description, amount, is_manually_categorized) \ + VALUES ('2026-01-01', 'x', -1, {})", + flag + ), + [], + ) + .unwrap(); + } + + assert_eq!(repair_manual_flags(&conn), Ok(3)); + + let flags: Vec<(i64, String)> = conn + .prepare("SELECT is_manually_categorized, typeof(is_manually_categorized) FROM transactions ORDER BY id") + .unwrap() + .query_map([], |r| Ok((r.get(0)?, r.get(1)?))) + .unwrap() + .collect::>() + .unwrap(); + let integer = "integer".to_string(); + assert_eq!( + flags, + vec![(1, integer.clone()), (1, integer.clone()), (0, integer.clone()), (0, integer.clone()), (0, integer)] + ); + assert_eq!(repair_manual_flags(&conn), Ok(0)); + } + + #[test] + fn repair_skips_a_database_without_transactions() { + let conn = Connection::open_in_memory().unwrap(); + assert_eq!(repair_manual_flags(&conn), Ok(0)); + } + + // -- the migration list itself ------------------------------------------ + + #[test] + fn migration_checksums_match_the_released_ones() { + // SHA-384 of every migration's SQL, computed on main before + // `migrations()` was extracted from `run()` (#390). sqlx refuses to + // open the default profile when a recorded checksum changes, and a + // secondary profile's history must hold the same values. + let pinned: [(i64, &str); 17] = [ + (1, "a2f964547e0d0ebf141a978b1f2b5bfd030be6618e13dcdc6fa16504a269b6a036d3dbf7e82700db07c6aa85948bfedf"), + (2, "59d9c344713108fa6a1fb5c63635d5baab1d78bd8d5aa438a28dafdcb0e78c4cb66a11a6a2fec4358b61c45a9e055c96"), + (3, "fcb7bc6a921a68cb682f6441655c2152e3e131af6cacac8fec69b5ff73bd9c5a95bdf44a74fcfdfc5e322976f1c7b4d3"), + (4, "610fc4d778e3507bad1e3bfad5d682a534e753c326db6cf7d7af95a6772e37773518af0a7363bc0e47bbf6e9c49a9531"), + (5, "131c4b1e0fd6265e5afda9d4f9e505290b8b7108d71b33a957646068f8b116b0cddfca9f285742a93dc7101f4ac8794c"), + (6, "329e36c0cc63309292c10c92a9137a8f7150c2e5f84e4660ccf41fd4a85a3531e72b74f4ecd321c0236eb37bad934d8f"), + (7, "4c57798526796c44abe39116e4d498d6a6b330b58e134b67d23c7c1aad7da9e7856f8ac4296b9177591aaf127a2c54bc"), + (8, "a8763846054027b38f03f9d29219b08891ce931be07ceb221fe6e346d81a037cb9f77953ba230850810029bf25fd4cec"), + (9, "0f826db3a4bb2910e92c76be1de83a2d3858df783782b0278cbafcb0ae11b40ff1af540478e59a4cf1f7b4ba7933307a"), + (10, "646ae1048a55a04fbdd40eee045c75950f28f11492c31d94d2f0a32999e8f488650b3548c1d33e02b4b403ff91bf4077"), + (11, "037129d61869502d377ef0723dee830ad6bf023ef49564dfad9e4a1af1a342bee4161805b6b460bfac3f9748fb01cfd1"), + (12, "e4ad66f1b7fe8baa174d3fa3f898a98efb44293d35bae665fe4844b090fdfc57383f114f5f756066f09264cc30723f31"), + (13, "a85e83cea40c9bd70b435ead61db91eecd7bf4de2f80149e2cbf525f65fc1bbcf17eb5b29e6741f41bc8cb699d18b5e9"), + (14, "e1ef785df3777651f13bae14ebe8da3696f1b793a7676dffe1aaeed14e66f8214eece72466c8df002e746352c333c8b5"), + (15, "ee608cf4174c8127975aab5039348c1278c365c25c640186a6719b1cf056007772d0296a2efd9a05f5ecf351ef3a128f"), + (16, "e1be444fca469235871b89dfc264e6020a8ef1dbafff7bc487597eee29971baadd0bd694a352a4eea7e7b9d4ae752d99"), + (17, "0c60796250708aa2e4339640c99836f5cbdc52e1901231e3fbdc5668772b4d1e09c4fc697c94698523f56151492749b5"), + ]; + let actual: Vec<(i64, String)> = migrations() + .iter() + .map(|m| (m.version, checksum(m.sql).iter().map(|b| format!("{:02x}", b)).collect())) + .collect(); + let expected: Vec<(i64, String)> = pinned.iter().map(|(v, h)| (*v, h.to_string())).collect(); + assert_eq!(actual, expected); + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 5a0ea21..f22a134 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -6,9 +6,12 @@ use tauri::{Emitter, Manager}; use tauri_plugin_deep_link::DeepLinkExt; use tauri_plugin_sql::{Migration, MigrationKind}; -#[cfg_attr(mobile, tauri::mobile_entry_point)] -pub fn run() { - let migrations = vec![ +/// Every migration, in order. tauri-plugin-sql applies them to the default +/// profile on load; `database::profile_db` catches the secondary profiles up +/// with the same list (#390). The SQL bytes are checksummed by sqlx and pinned +/// by a test: never edit an existing entry, append a new version. +pub(crate) fn migrations() -> Vec { + vec![ Migration { version: 1, description: "create initial schema", @@ -365,7 +368,12 @@ pub fn run() { WHERE column_mapping LIKE '%debitAmount%';", kind: MigrationKind::Up, }, - ]; + ] +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + let migrations = migrations(); tauri::Builder::default() // Single-instance plugin MUST be registered first. With the `deep-link` @@ -429,7 +437,7 @@ pub fn run() { }) .plugin( tauri_plugin_sql::Builder::default() - .add_migrations("sqlite:simpl_resultat.db", migrations) + .add_migrations(&format!("sqlite:{}", database::DEFAULT_PROFILE_DB), migrations) .build(), ) .invoke_handler(tauri::generate_handler![ @@ -452,6 +460,7 @@ pub fn run() { commands::hash_pin, commands::verify_pin, commands::repair_migrations, + commands::prepare_profile_db, commands::validate_license_key, commands::store_license, commands::remove_license, @@ -3867,12 +3876,10 @@ mod tests { #[test] fn consolidated_schema_matches_v17_chain_on_import_sources_at_parity() { - // The consolidated schema is the tested reference definition for new - // profiles. Without this test the DEFAULT or the CHECK of the four v17 - // columns could drift between the two definitions unnoticed — the - // columns are inert on the production path (this script runs after every - // migration and only uses CREATE TABLE IF NOT EXISTS), so nothing else - // would surface the divergence. + // The consolidated schema is the definition a secondary profile is + // created with: tauri-plugin-sql runs no migration on its database + // (#390). Without this test the DEFAULT or the CHECK of the four v17 + // columns could drift between the two definitions unnoticed. let chain = db_pre_v17(); chain.execute_batch(V17_SQL).expect("apply v17"); let consolidated = consolidated_db(); diff --git a/src/services/db.test.ts b/src/services/db.test.ts index ef09826..9672428 100644 --- a/src/services/db.test.ts +++ b/src/services/db.test.ts @@ -12,7 +12,11 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; // Ordered log of every fake-db operation's start/end, shared with the fake db. const events: string[] = []; // Hoisted handle so the (hoisted) vi.mock factory can reach the per-test db. -const h = vi.hoisted(() => ({ fakeDb: null as unknown as FakeDb })); +const h = vi.hoisted(() => ({ + fakeDb: null as unknown as FakeDb, + // What `prepare_profile_db` answers (#390); a test may make it throw. + prepare: async (): Promise => null, +})); interface FakeDb { execute: (q: string, p?: unknown[]) => Promise<{ rowsAffected: number }>; @@ -23,9 +27,21 @@ interface FakeDb { vi.mock("@tauri-apps/plugin-sql", () => ({ default: { load: vi.fn(async () => h.fakeDb) }, })); -vi.mock("@tauri-apps/api/core", () => ({ invoke: vi.fn(async () => false) })); +vi.mock("@tauri-apps/api/core", () => ({ + invoke: vi.fn(async (command: string) => (command === "prepare_profile_db" ? h.prepare() : false)), +})); +vi.mock("./logService", () => ({ logInfo: vi.fn(), logError: vi.fn() })); -import { connectToProfile, getDb, withTransaction } from "./db"; +import Database from "@tauri-apps/plugin-sql"; +import { invoke } from "@tauri-apps/api/core"; +import { logError, logInfo } from "./logService"; +import { + connectToProfile, + describePreparation, + getDb, + withTransaction, + type ProfilePreparation, +} from "./db"; // Each op records start, yields a real macrotask (a window for interleaving if // the lock were broken), then records end. The lock must keep them ordered. @@ -50,6 +66,7 @@ function makeFakeDb(): FakeDb { beforeEach(async () => { events.length = 0; h.fakeDb = makeFakeDb(); + h.prepare = async () => null; await connectToProfile("test.db"); }); @@ -121,3 +138,101 @@ describe("db serialization (database-locked fix)", () => { ); }); }); + +// --------------------------------------------------------------------------- +// Profile preparation (#390) +// --------------------------------------------------------------------------- + +function report(overrides: Partial = {}): ProfilePreparation { + return { catch_up: null, catch_up_error: null, repaired_flags: 0, elapsed_ms: 12, ...overrides }; +} + +function catchUp(overrides: Partial> = {}) { + return { + floor: 6, + recorded: [], + applied: [], + fk_repaired: [], + fk_violations: [], + backup_file: null, + ...overrides, + }; +} + +describe("a profile is prepared before the plugin opens it (#390)", () => { + beforeEach(() => { + vi.mocked(invoke).mockClear(); + vi.mocked(Database.load).mockClear(); + vi.mocked(logInfo).mockClear(); + vi.mocked(logError).mockClear(); + }); + + it("prepares after the checksum repair and before Database.load", async () => { + await connectToProfile("profile_0a1b2c3d.db"); + + const invoked = vi.mocked(invoke).mock; + expect(invoked.calls.map((call) => call[0])).toEqual(["repair_migrations", "prepare_profile_db"]); + expect(invoked.calls[1][1]).toEqual({ dbFilename: "profile_0a1b2c3d.db" }); + expect(invoked.invocationCallOrder[1]).toBeLessThan( + vi.mocked(Database.load).mock.invocationCallOrder[0] + ); + }); + + it("still opens the profile when the preparation fails, and says so in the Journal", async () => { + h.prepare = async () => { + throw "invalid_profile_db_filename"; + }; + + await connectToProfile("profile_0a1b2c3d.db"); + + expect(Database.load).toHaveBeenCalledWith("sqlite:profile_0a1b2c3d.db"); + expect(logError).toHaveBeenCalledWith( + "Profile profile_0a1b2c3d.db: database preparation failed: invalid_profile_db_filename" + ); + }); + + it("writes what changed to the Journal", async () => { + h.prepare = async () => + report({ + catch_up: catchUp({ applied: [7, 8, 17], backup_file: "profile_0a1b2c3d-before-v7.db" }), + repaired_flags: 2, + }); + + await connectToProfile("profile_0a1b2c3d.db"); + + expect(vi.mocked(logInfo).mock.calls.map((call) => call[0])).toEqual([ + "Profile profile_0a1b2c3d.db: database updated with versions 7 to 17 in 12 ms, copy of the previous state kept as profile_0a1b2c3d-before-v7.db", + "Profile profile_0a1b2c3d.db: 2 manual categorization flag(s) repaired", + ]); + expect(logError).not.toHaveBeenCalled(); + }); +}); + +describe("describePreparation (#390)", () => { + it("says nothing when nothing changed", () => { + expect(describePreparation("x.db", report({ catch_up: catchUp() }))).toEqual({ info: [], errors: [] }); + }); + + it("tolerates a missing report", () => { + expect(describePreparation("x.db", null)).toEqual({ info: [], errors: [] }); + expect(describePreparation("x.db", false as never)).toEqual({ info: [], errors: [] }); + }); + + it("notes a history written without any update", () => { + const lines = describePreparation("x.db", report({ catch_up: catchUp({ recorded: [1, 2, 17] }) })); + expect(lines.info).toEqual(["Profile x.db: migration history recorded up to version 17"]); + }); + + it("names the rows a version detached from a missing parent", () => { + const lines = describePreparation( + "x.db", + report({ catch_up: catchUp({ applied: [7], fk_repaired: [["categories", 3]] }) }) + ); + expect(lines.info).toContain("Profile x.db: 3 row(s) of categories detached from a missing parent"); + }); + + it("reports a stopped catch-up as an error", () => { + const lines = describePreparation("x.db", report({ catch_up_error: "migration_failed:v12: boom" })); + expect(lines.errors).toEqual(["Profile x.db: database update stopped: migration_failed:v12: boom"]); + }); +}); diff --git a/src/services/db.ts b/src/services/db.ts index f0cae67..3682e38 100644 --- a/src/services/db.ts +++ b/src/services/db.ts @@ -1,5 +1,6 @@ import Database from "@tauri-apps/plugin-sql"; import { invoke } from "@tauri-apps/api/core"; +import { logError, logInfo } from "./logService"; let rawInstance: Database | null = null; let serializedInstance: Database | null = null; @@ -103,6 +104,61 @@ export async function withTransaction( }); } +/** What `prepare_profile_db` did to one profile database (#390). */ +export interface ProfilePreparation { + catch_up: { + floor: number | null; + recorded: number[]; + applied: number[]; + fk_repaired: Array<[string, number]>; + fk_violations: Array<[string, number]>; + backup_file: string | null; + } | null; + catch_up_error: string | null; + repaired_flags: number; + elapsed_ms: number; +} + +/** + * Journal lines for one preparation: only what changed. Names files, never + * folders: an error line can be attached to a feedback report, and the + * folder holds the OS user name. + */ +export function describePreparation( + dbFilename: string, + report: ProfilePreparation | null | undefined +): { info: string[]; errors: string[] } { + const info: string[] = []; + const errors: string[] = []; + if (!report || typeof report !== "object") return { info, errors }; + const catchUp = report.catch_up; + if (catchUp && catchUp.applied.length > 0) { + const first = catchUp.applied[0]; + const last = catchUp.applied[catchUp.applied.length - 1]; + info.push( + `Profile ${dbFilename}: database updated with versions ${first} to ${last} in ${report.elapsed_ms} ms` + + (catchUp.backup_file ? `, copy of the previous state kept as ${catchUp.backup_file}` : "") + ); + } else if (catchUp && catchUp.recorded.length > 0) { + info.push( + `Profile ${dbFilename}: migration history recorded up to version ${catchUp.recorded[catchUp.recorded.length - 1]}` + ); + } + for (const [table, rows] of catchUp?.fk_repaired ?? []) { + info.push(`Profile ${dbFilename}: ${rows} row(s) of ${table} detached from a missing parent`); + } + for (const [table, rows] of catchUp?.fk_violations ?? []) { + info.push(`Profile ${dbFilename}: ${rows} row(s) of ${table} point to a missing row`); + } + if (report.catch_up_error) { + errors.push(`Profile ${dbFilename}: database update stopped: ${report.catch_up_error}`); + } + if (report.repaired_flags > 0) { + info.push(`Profile ${dbFilename}: ${report.repaired_flags} manual categorization flag(s) repaired`); + } + return { info, errors }; +} + export async function connectToProfile(dbFilename: string): Promise { if (rawInstance) { await rawInstance.close(); @@ -118,6 +174,19 @@ export async function connectToProfile(dbFilename: string): Promise { } catch (e) { console.error("Migration repair failed:", e); } + // Catch a secondary profile up on the migrations tauri-plugin-sql never + // runs for it, and repair legacy values in every profile (#390). Fails + // open, like the repair above: the profile still opens. + try { + const report = await invoke("prepare_profile_db", { dbFilename }); + const lines = describePreparation(dbFilename, report); + lines.info.forEach((line) => logInfo(line)); + lines.errors.forEach((line) => logError(line)); + } catch (e) { + logError( + `Profile ${dbFilename}: database preparation failed: ${e instanceof Error ? e.message : String(e)}` + ); + } rawInstance = await Database.load(`sqlite:${dbFilename}`); serializedInstance = serialize(rawInstance); }