-- M0: Local household ledger skeleton. -- SQLite-compatible baseline schema. The SQLAlchemy models are the runtime source for dev -- create_all(); this file documents the first relational shape for migration tooling. CREATE TABLE IF NOT EXISTS households ( id TEXT PRIMARY KEY, name TEXT NOT NULL, default_currency TEXT NOT NULL DEFAULT 'EUR', created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, household_id TEXT NOT NULL REFERENCES households(id), display_name TEXT NOT NULL, email TEXT, created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS login_identities ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id), provider TEXT NOT NULL, provider_subject TEXT NOT NULL, email TEXT, display_name TEXT, created_at TEXT NOT NULL, UNIQUE(provider, provider_subject) ); CREATE TABLE IF NOT EXISTS local_sessions ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id), household_id TEXT NOT NULL REFERENCES households(id), token_hash TEXT NOT NULL UNIQUE, created_at TEXT NOT NULL, last_seen_at TEXT NOT NULL, expires_at TEXT NOT NULL, revoked_at TEXT ); CREATE INDEX IF NOT EXISTS ix_local_session_token ON local_sessions(token_hash); CREATE INDEX IF NOT EXISTS ix_local_session_user ON local_sessions(user_id, expires_at); CREATE TABLE IF NOT EXISTS accounts ( id TEXT PRIMARY KEY, household_id TEXT NOT NULL REFERENCES households(id), owner_user_id TEXT REFERENCES users(id), name TEXT NOT NULL, account_type TEXT NOT NULL, institution_name TEXT, currency TEXT NOT NULL DEFAULT 'EUR', visibility_mode TEXT NOT NULL, is_active INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS raw_observations ( id TEXT PRIMARY KEY, household_id TEXT NOT NULL REFERENCES households(id), source_type TEXT NOT NULL, source_account_id TEXT REFERENCES accounts(id), owner_user_id TEXT REFERENCES users(id), observed_at TEXT, received_at TEXT NOT NULL, raw_hash TEXT NOT NULL, source_reference TEXT, provider_transaction_id TEXT, message_id TEXT, raw_text TEXT, raw_file_path TEXT, raw_payload_json TEXT, parsed_json TEXT, parser_name TEXT, parser_version TEXT, confidence NUMERIC(14, 2) NOT NULL DEFAULT 1.00, status TEXT NOT NULL, created_at TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS ix_observation_dedupe ON raw_observations(household_id, source_type, raw_hash); CREATE INDEX IF NOT EXISTS ix_observation_external_refs ON raw_observations(provider_transaction_id, message_id); CREATE TABLE IF NOT EXISTS categories ( id TEXT PRIMARY KEY, household_id TEXT NOT NULL REFERENCES households(id), parent_id TEXT REFERENCES categories(id), name TEXT NOT NULL, path TEXT NOT NULL, is_active INTEGER NOT NULL DEFAULT 1 ); CREATE TABLE IF NOT EXISTS projects ( id TEXT PRIMARY KEY, household_id TEXT NOT NULL REFERENCES households(id), name TEXT NOT NULL, description TEXT, start_date TEXT, end_date TEXT, default_split_rule_id TEXT, status TEXT NOT NULL, currency TEXT NOT NULL DEFAULT 'EUR', created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS financial_events ( id TEXT PRIMARY KEY, household_id TEXT NOT NULL REFERENCES households(id), source_account_id TEXT REFERENCES accounts(id), payment_account_id TEXT REFERENCES accounts(id), payer_user_id TEXT REFERENCES users(id), event_type TEXT NOT NULL, status TEXT NOT NULL, event_date TEXT NOT NULL, booking_date TEXT, due_date TEXT, amount NUMERIC(14, 2) NOT NULL, currency TEXT NOT NULL DEFAULT 'EUR', merchant TEXT, counterparty TEXT, description TEXT, category_id TEXT REFERENCES categories(id), project_id TEXT REFERENCES projects(id), created_by_user_id TEXT REFERENCES users(id), fingerprint TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS ix_events_household_status_date ON financial_events(household_id, status, event_date); CREATE TABLE IF NOT EXISTS event_observation_links ( id TEXT PRIMARY KEY, event_id TEXT NOT NULL REFERENCES financial_events(id), observation_id TEXT NOT NULL REFERENCES raw_observations(id), relation TEXT NOT NULL, confidence NUMERIC(14, 2) NOT NULL DEFAULT 1.00, created_at TEXT NOT NULL, UNIQUE(event_id, observation_id, relation) ); CREATE TABLE IF NOT EXISTS event_relations ( id TEXT PRIMARY KEY, from_event_id TEXT NOT NULL REFERENCES financial_events(id), to_event_id TEXT NOT NULL REFERENCES financial_events(id), relation_type TEXT NOT NULL, confidence NUMERIC(14, 2) NOT NULL DEFAULT 1.00, created_by TEXT NOT NULL DEFAULT 'system', created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS split_lines ( id TEXT PRIMARY KEY, event_id TEXT NOT NULL REFERENCES financial_events(id), user_id TEXT NOT NULL REFERENCES users(id), share_amount NUMERIC(14, 2) NOT NULL, share_percent NUMERIC(14, 2), reason TEXT NOT NULL, created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS tags ( id TEXT PRIMARY KEY, household_id TEXT NOT NULL REFERENCES households(id), name TEXT NOT NULL, UNIQUE(household_id, name) ); CREATE TABLE IF NOT EXISTS event_tags ( event_id TEXT NOT NULL REFERENCES financial_events(id), tag_id TEXT NOT NULL REFERENCES tags(id), PRIMARY KEY(event_id, tag_id) ); CREATE TABLE IF NOT EXISTS project_participants ( id TEXT PRIMARY KEY, project_id TEXT NOT NULL REFERENCES projects(id), user_id TEXT NOT NULL REFERENCES users(id), role TEXT NOT NULL, default_share_percent NUMERIC(14, 2), UNIQUE(project_id, user_id) ); CREATE TABLE IF NOT EXISTS project_budget_lines ( id TEXT PRIMARY KEY, project_id TEXT NOT NULL REFERENCES projects(id), category_id TEXT REFERENCES categories(id), name TEXT NOT NULL, budgeted_amount NUMERIC(14, 2) NOT NULL DEFAULT 0.00, committed_amount_cached NUMERIC(14, 2), paid_amount_cached NUMERIC(14, 2), sort_order INTEGER NOT NULL DEFAULT 0, notes TEXT ); CREATE TABLE IF NOT EXISTS project_planned_items ( id TEXT PRIMARY KEY, project_id TEXT NOT NULL REFERENCES projects(id), budget_line_id TEXT REFERENCES project_budget_lines(id), description TEXT NOT NULL, estimated_amount NUMERIC(14, 2) NOT NULL DEFAULT 0.00, committed_amount NUMERIC(14, 2), expected_date TEXT, due_date TEXT, expected_payer_account_id TEXT REFERENCES accounts(id), split_rule_id TEXT, planning_status TEXT NOT NULL, payment_status TEXT NOT NULL, reconciliation_status TEXT NOT NULL, settlement_status TEXT NOT NULL, notes TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS payment_lines ( id TEXT PRIMARY KEY, planned_item_id TEXT REFERENCES project_planned_items(id), event_id TEXT REFERENCES financial_events(id), amount NUMERIC(14, 2) NOT NULL, currency TEXT NOT NULL DEFAULT 'EUR', date TEXT NOT NULL, paid_by_user_id TEXT REFERENCES users(id), payment_account_id TEXT REFERENCES accounts(id), source_observation_id TEXT REFERENCES raw_observations(id), status TEXT NOT NULL, created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS recurring_commitments ( id TEXT PRIMARY KEY, household_id TEXT NOT NULL REFERENCES households(id), provider TEXT NOT NULL, description TEXT, amount_expected NUMERIC(14, 2) NOT NULL, amount_min NUMERIC(14, 2), amount_max NUMERIC(14, 2), currency TEXT NOT NULL DEFAULT 'EUR', interval TEXT NOT NULL, next_due_date TEXT NOT NULL, payer_account_id TEXT REFERENCES accounts(id), split_rule_id TEXT, category_id TEXT REFERENCES categories(id), project_id TEXT REFERENCES projects(id), notice_period TEXT, renewal_date TEXT, status TEXT NOT NULL, matching_rule_json TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS promised_payments ( id TEXT PRIMARY KEY, household_id TEXT NOT NULL REFERENCES households(id), related_event_id TEXT REFERENCES financial_events(id), provider TEXT NOT NULL, amount NUMERIC(14, 2) NOT NULL, currency TEXT NOT NULL DEFAULT 'EUR', created_date TEXT NOT NULL, due_date TEXT NOT NULL, expected_funding_account_id TEXT REFERENCES accounts(id), responsible_user_id TEXT REFERENCES users(id), status TEXT NOT NULL, source_observation_id TEXT REFERENCES raw_observations(id), created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS import_connections ( id TEXT PRIMARY KEY, household_id TEXT NOT NULL REFERENCES households(id), owner_user_id TEXT NOT NULL REFERENCES users(id), connection_type TEXT NOT NULL, display_name TEXT NOT NULL, status TEXT NOT NULL, last_sync_at TEXT, last_error TEXT, secret_storage_ref TEXT, runs_locally INTEGER NOT NULL DEFAULT 1, connector_config_json TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS import_runs ( id TEXT PRIMARY KEY, household_id TEXT NOT NULL REFERENCES households(id), connection_id TEXT REFERENCES import_connections(id), actor_user_id TEXT REFERENCES users(id), source_type TEXT, status TEXT NOT NULL DEFAULT 'running', imported_count INTEGER NOT NULL DEFAULT 0, duplicate_count INTEGER NOT NULL DEFAULT 0, created_event_count INTEGER NOT NULL DEFAULT 0, skipped_count INTEGER NOT NULL DEFAULT 0, error TEXT, summary_json TEXT, started_at TEXT NOT NULL, finished_at TEXT ); CREATE INDEX IF NOT EXISTS ix_import_runs_household_started ON import_runs(household_id, started_at); CREATE INDEX IF NOT EXISTS ix_import_runs_connection_started ON import_runs(connection_id, started_at); CREATE TABLE IF NOT EXISTS audit_log ( id TEXT PRIMARY KEY, household_id TEXT NOT NULL REFERENCES households(id), actor_user_id TEXT REFERENCES users(id), action TEXT NOT NULL, entity_type TEXT NOT NULL, entity_id TEXT, details_json TEXT, created_at TEXT NOT NULL );