374 lines
14 KiB
Python
374 lines
14 KiB
Python
from __future__ import annotations
|
|
|
|
SCHEMA_VERSION = "15"
|
|
|
|
SCHEMA_SQL = """
|
|
PRAGMA journal_mode=WAL;
|
|
PRAGMA foreign_keys=ON;
|
|
|
|
CREATE TABLE IF NOT EXISTS schema_meta (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS accounts (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
type TEXT NOT NULL DEFAULT 'bank',
|
|
institution TEXT NOT NULL DEFAULT '',
|
|
currency TEXT NOT NULL DEFAULT 'KRW',
|
|
active INTEGER NOT NULL DEFAULT 1,
|
|
memo TEXT NOT NULL DEFAULT '',
|
|
account_number_masked TEXT NOT NULL DEFAULT '',
|
|
holder_name TEXT NOT NULL DEFAULT '',
|
|
last_balance INTEGER,
|
|
last_balance_basis_date TEXT NOT NULL DEFAULT '',
|
|
last_synced_at TEXT NOT NULL DEFAULT '',
|
|
statement_status TEXT NOT NULL DEFAULT 'unknown',
|
|
liquidity_bucket TEXT NOT NULL DEFAULT 'other',
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS transactions (
|
|
id TEXT PRIMARY KEY,
|
|
transaction_date TEXT NOT NULL,
|
|
posted_date TEXT NOT NULL DEFAULT '',
|
|
type TEXT NOT NULL,
|
|
amount INTEGER NOT NULL,
|
|
currency TEXT NOT NULL DEFAULT 'KRW',
|
|
category TEXT NOT NULL DEFAULT '',
|
|
subcategory TEXT NOT NULL DEFAULT '',
|
|
merchant TEXT NOT NULL DEFAULT '',
|
|
description TEXT NOT NULL DEFAULT '',
|
|
account_id TEXT NOT NULL DEFAULT '',
|
|
payment_method TEXT NOT NULL DEFAULT '',
|
|
person TEXT NOT NULL DEFAULT '',
|
|
project TEXT NOT NULL DEFAULT '',
|
|
tags TEXT NOT NULL DEFAULT '[]',
|
|
is_fixed INTEGER NOT NULL DEFAULT 0,
|
|
source TEXT NOT NULL DEFAULT 'manual',
|
|
import_batch_id TEXT NOT NULL DEFAULT '',
|
|
external_id TEXT NOT NULL DEFAULT '',
|
|
dedupe_key TEXT NOT NULL DEFAULT '',
|
|
memo TEXT NOT NULL DEFAULT '',
|
|
status TEXT NOT NULL DEFAULT 'active',
|
|
transaction_datetime TEXT NOT NULL DEFAULT '',
|
|
counterparty TEXT NOT NULL DEFAULT '',
|
|
balance_after INTEGER,
|
|
branch TEXT NOT NULL DEFAULT '',
|
|
raw_summary TEXT NOT NULL DEFAULT '',
|
|
raw_payload TEXT NOT NULL DEFAULT '',
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_transactions_date ON transactions(transaction_date);
|
|
CREATE INDEX IF NOT EXISTS idx_transactions_category ON transactions(category, subcategory);
|
|
CREATE INDEX IF NOT EXISTS idx_transactions_person ON transactions(person);
|
|
CREATE INDEX IF NOT EXISTS idx_transactions_account ON transactions(account_id);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_transactions_dedupe ON transactions(dedupe_key) WHERE dedupe_key <> '';
|
|
|
|
CREATE TABLE IF NOT EXISTS budgets (
|
|
id TEXT PRIMARY KEY,
|
|
month TEXT NOT NULL,
|
|
category TEXT NOT NULL,
|
|
subcategory TEXT NOT NULL DEFAULT '',
|
|
person TEXT NOT NULL DEFAULT '',
|
|
amount INTEGER NOT NULL,
|
|
currency TEXT NOT NULL DEFAULT 'KRW',
|
|
memo TEXT NOT NULL DEFAULT '',
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_budgets_month ON budgets(month);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_budgets_unique ON budgets(month, category, subcategory, person);
|
|
|
|
CREATE TABLE IF NOT EXISTS import_batches (
|
|
id TEXT PRIMARY KEY,
|
|
source TEXT NOT NULL,
|
|
filename TEXT NOT NULL DEFAULT '',
|
|
imported_at TEXT NOT NULL,
|
|
row_count INTEGER NOT NULL DEFAULT 0,
|
|
status TEXT NOT NULL DEFAULT 'active',
|
|
memo TEXT NOT NULL DEFAULT ''
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS category_rules (
|
|
id TEXT PRIMARY KEY,
|
|
priority INTEGER NOT NULL DEFAULT 100,
|
|
match_field TEXT NOT NULL,
|
|
match_type TEXT NOT NULL DEFAULT 'contains',
|
|
pattern TEXT NOT NULL,
|
|
amount_min INTEGER,
|
|
amount_max INTEGER,
|
|
day_of_month INTEGER,
|
|
transaction_type TEXT NOT NULL DEFAULT '',
|
|
category TEXT NOT NULL,
|
|
subcategory TEXT NOT NULL DEFAULT '',
|
|
person TEXT NOT NULL DEFAULT '',
|
|
is_fixed INTEGER,
|
|
confidence REAL NOT NULL DEFAULT 1.0,
|
|
active INTEGER NOT NULL DEFAULT 1,
|
|
memo TEXT NOT NULL DEFAULT '',
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_category_rules_active_priority ON category_rules(active, priority);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_category_rules_unique ON category_rules(match_field, match_type, pattern, transaction_type, category, subcategory, person);
|
|
|
|
CREATE TABLE IF NOT EXISTS ledger_categories (
|
|
id TEXT PRIMARY KEY,
|
|
category TEXT NOT NULL,
|
|
subcategory TEXT NOT NULL DEFAULT '',
|
|
cashflow_kind TEXT NOT NULL DEFAULT 'expense',
|
|
active INTEGER NOT NULL DEFAULT 1,
|
|
sort_order INTEGER NOT NULL DEFAULT 100,
|
|
memo TEXT NOT NULL DEFAULT '',
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_ledger_categories_active_sort ON ledger_categories(active, sort_order, category, subcategory);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_ledger_categories_unique ON ledger_categories(category, subcategory);
|
|
|
|
CREATE TABLE IF NOT EXISTS account_snapshot (
|
|
id TEXT PRIMARY KEY,
|
|
basis_date TEXT NOT NULL,
|
|
source_type TEXT NOT NULL,
|
|
source_id TEXT NOT NULL DEFAULT '',
|
|
name TEXT NOT NULL,
|
|
amount INTEGER,
|
|
currency TEXT NOT NULL DEFAULT 'KRW',
|
|
liquidity_bucket TEXT NOT NULL DEFAULT 'other',
|
|
valuation_source TEXT NOT NULL DEFAULT '',
|
|
source_note TEXT NOT NULL DEFAULT '',
|
|
memo TEXT NOT NULL DEFAULT '',
|
|
monthly_settlement_id TEXT REFERENCES monthly_settlement(id) ON DELETE RESTRICT,
|
|
correction_of_run_id TEXT NOT NULL DEFAULT '',
|
|
correction_reason TEXT NOT NULL DEFAULT '',
|
|
is_correction INTEGER NOT NULL DEFAULT 0,
|
|
closed_at TEXT NOT NULL DEFAULT '',
|
|
valuation_id TEXT NOT NULL DEFAULT '',
|
|
selection_policy TEXT NOT NULL DEFAULT 'exact',
|
|
verification_status TEXT NOT NULL DEFAULT 'unverified',
|
|
evidence_kind TEXT NOT NULL DEFAULT 'applied',
|
|
exclusion_reason TEXT NOT NULL DEFAULT '',
|
|
created_at TEXT NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_account_snapshot_basis_date ON account_snapshot(basis_date);
|
|
CREATE INDEX IF NOT EXISTS idx_account_snapshot_source ON account_snapshot(source_type, source_id);
|
|
CREATE TABLE IF NOT EXISTS settlement_cashflow_snapshots (
|
|
id TEXT PRIMARY KEY,
|
|
monthly_settlement_id TEXT REFERENCES monthly_settlement(id) ON DELETE RESTRICT,
|
|
settlement_run_id TEXT REFERENCES asset_snapshot_runs(id) ON DELETE RESTRICT,
|
|
cashflow_kind TEXT NOT NULL CHECK (cashflow_kind IN ('income', 'expense', 'excluded')),
|
|
category TEXT NOT NULL DEFAULT '',
|
|
subcategory TEXT NOT NULL DEFAULT '',
|
|
amount INTEGER NOT NULL CHECK (typeof(amount) = 'integer'),
|
|
transaction_count INTEGER NOT NULL CHECK (typeof(transaction_count) = 'integer' AND transaction_count > 0),
|
|
currency TEXT NOT NULL,
|
|
CHECK ((monthly_settlement_id IS NOT NULL) <> (settlement_run_id IS NOT NULL)),
|
|
UNIQUE (settlement_run_id, cashflow_kind, category, subcategory, currency),
|
|
UNIQUE (monthly_settlement_id, cashflow_kind, category, subcategory, currency)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_settlement_cashflow_snapshots_month ON settlement_cashflow_snapshots(monthly_settlement_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS review_decisions (
|
|
id TEXT PRIMARY KEY,
|
|
target_type TEXT NOT NULL,
|
|
target_id TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
decision_type TEXT NOT NULL DEFAULT 'unknown',
|
|
reason_code TEXT NOT NULL DEFAULT '',
|
|
memo TEXT NOT NULL DEFAULT '',
|
|
operator TEXT NOT NULL DEFAULT 'operator',
|
|
decided_at TEXT NOT NULL DEFAULT '',
|
|
review_after TEXT NOT NULL DEFAULT '',
|
|
target_payload TEXT NOT NULL DEFAULT '{}',
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_review_decisions_target ON review_decisions(target_type, target_id, updated_at);
|
|
CREATE INDEX IF NOT EXISTS idx_review_decisions_status ON review_decisions(status, review_after, updated_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS reconciliation_sessions (
|
|
id TEXT PRIMARY KEY,
|
|
account_id TEXT NOT NULL,
|
|
basis_date TEXT NOT NULL,
|
|
expected_balance INTEGER,
|
|
actual_balance INTEGER,
|
|
difference INTEGER,
|
|
status TEXT NOT NULL DEFAULT 'open',
|
|
closed_at TEXT NOT NULL DEFAULT '',
|
|
source_note TEXT NOT NULL DEFAULT '',
|
|
memo TEXT NOT NULL DEFAULT '',
|
|
operator TEXT NOT NULL DEFAULT 'operator',
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_reconciliation_sessions_account_basis ON reconciliation_sessions(account_id, basis_date);
|
|
CREATE INDEX IF NOT EXISTS idx_reconciliation_sessions_status ON reconciliation_sessions(status, closed_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS monthly_settlement (
|
|
id TEXT PRIMARY KEY,
|
|
month TEXT NOT NULL UNIQUE CHECK (
|
|
length(month) = 7 AND substr(month, 5, 1) = '-' AND
|
|
CAST(substr(month, 1, 4) AS INTEGER) BETWEEN 1 AND 9999 AND
|
|
CAST(substr(month, 6, 2) AS INTEGER) BETWEEN 1 AND 12
|
|
),
|
|
basis_date TEXT NOT NULL CHECK (date(basis_date) = basis_date),
|
|
status TEXT NOT NULL CHECK (status IN ('draft', 'in_review', 'approved', 'closed', 'cancelled')),
|
|
version INTEGER NOT NULL CHECK (typeof(version) = 'integer' AND version > 0),
|
|
memo TEXT NOT NULL DEFAULT '',
|
|
created_by TEXT NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
updated_by TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
reviewed_by TEXT,
|
|
reviewed_at TEXT,
|
|
review_fingerprint TEXT,
|
|
approved_by TEXT,
|
|
approved_at TEXT,
|
|
approved_version INTEGER CHECK (approved_version IS NULL OR (typeof(approved_version) = 'integer' AND approved_version > 0)),
|
|
approved_fingerprint TEXT,
|
|
approved_cashflow_fingerprint TEXT,
|
|
approval_reason TEXT,
|
|
approval_flags_json TEXT NOT NULL DEFAULT '{}',
|
|
closed_by TEXT,
|
|
closed_at TEXT,
|
|
cancelled_by TEXT,
|
|
cancelled_at TEXT,
|
|
cancellation_reason TEXT,
|
|
CHECK (status NOT IN ('approved', 'closed') OR (
|
|
approved_by IS NOT NULL AND approved_at IS NOT NULL AND
|
|
approved_version IS NOT NULL AND approved_fingerprint IS NOT NULL
|
|
)),
|
|
CHECK (status <> 'closed' OR (closed_by IS NOT NULL AND closed_at IS NOT NULL)),
|
|
CHECK (status <> 'cancelled' OR (
|
|
cancelled_by IS NOT NULL AND cancelled_at IS NOT NULL AND cancellation_reason IS NOT NULL
|
|
))
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_month_settlement_status_month ON monthly_settlement(status, month);
|
|
|
|
CREATE TABLE IF NOT EXISTS manual_assets (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
liquidity_bucket TEXT NOT NULL DEFAULT 'other',
|
|
valuation_amount INTEGER,
|
|
valuation_date TEXT NOT NULL DEFAULT '',
|
|
currency TEXT NOT NULL DEFAULT 'KRW',
|
|
source_note TEXT NOT NULL DEFAULT '',
|
|
memo TEXT NOT NULL DEFAULT '',
|
|
active INTEGER NOT NULL DEFAULT 1,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS asset_snapshots (
|
|
id TEXT PRIMARY KEY,
|
|
basis_date TEXT NOT NULL,
|
|
source_type TEXT NOT NULL,
|
|
source_id TEXT NOT NULL DEFAULT '',
|
|
name TEXT NOT NULL,
|
|
amount INTEGER,
|
|
currency TEXT NOT NULL DEFAULT 'KRW',
|
|
liquidity_bucket TEXT NOT NULL DEFAULT 'other',
|
|
valuation_source TEXT NOT NULL DEFAULT '',
|
|
source_note TEXT NOT NULL DEFAULT '',
|
|
memo TEXT NOT NULL DEFAULT '',
|
|
snapshot_run_id TEXT NOT NULL DEFAULT '',
|
|
correction_of_run_id TEXT NOT NULL DEFAULT '',
|
|
correction_reason TEXT NOT NULL DEFAULT '',
|
|
is_correction INTEGER NOT NULL DEFAULT 0,
|
|
closed_at TEXT NOT NULL DEFAULT '',
|
|
valuation_id TEXT NOT NULL DEFAULT '',
|
|
selection_policy TEXT NOT NULL DEFAULT 'exact',
|
|
verification_status TEXT NOT NULL DEFAULT 'unverified',
|
|
evidence_kind TEXT NOT NULL DEFAULT 'applied',
|
|
exclusion_reason TEXT NOT NULL DEFAULT '',
|
|
created_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS asset_snapshot_runs (
|
|
id TEXT PRIMARY KEY,
|
|
basis_date TEXT NOT NULL,
|
|
close_status TEXT NOT NULL,
|
|
selection_policy TEXT NOT NULL DEFAULT 'exact',
|
|
idempotency_key TEXT NOT NULL DEFAULT '',
|
|
approved_by TEXT NOT NULL DEFAULT '',
|
|
approved_at TEXT NOT NULL DEFAULT '',
|
|
approval_reason TEXT NOT NULL DEFAULT '',
|
|
approval_reference TEXT NOT NULL DEFAULT '',
|
|
correction_of_run_id TEXT NOT NULL DEFAULT '',
|
|
correction_reason TEXT NOT NULL DEFAULT '',
|
|
applied_evidence TEXT NOT NULL DEFAULT '[]',
|
|
excluded_evidence TEXT NOT NULL DEFAULT '[]',
|
|
carried_evidence TEXT NOT NULL DEFAULT '[]',
|
|
request_fingerprint TEXT NOT NULL DEFAULT '',
|
|
created_at TEXT NOT NULL,
|
|
settlement_month TEXT,
|
|
currency TEXT,
|
|
income_total INTEGER,
|
|
expense_total INTEGER,
|
|
net_cashflow INTEGER,
|
|
total_assets INTEGER,
|
|
total_liabilities INTEGER,
|
|
net_worth INTEGER,
|
|
cashflow_policy_json TEXT,
|
|
aggregation_version TEXT,
|
|
snapshot_schema_version INTEGER,
|
|
source_snapshot_json TEXT
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS account_valuations (
|
|
id TEXT PRIMARY KEY,
|
|
account_id TEXT NOT NULL REFERENCES accounts(id),
|
|
basis_date TEXT NOT NULL,
|
|
amount INTEGER NOT NULL,
|
|
currency TEXT NOT NULL DEFAULT 'KRW',
|
|
valuation_kind TEXT NOT NULL DEFAULT 'balance',
|
|
source_type TEXT NOT NULL DEFAULT 'manual',
|
|
source_ref TEXT NOT NULL DEFAULT '',
|
|
verification_status TEXT NOT NULL DEFAULT 'unverified',
|
|
recorded_at TEXT NOT NULL,
|
|
memo TEXT NOT NULL DEFAULT '',
|
|
idempotency_key TEXT NOT NULL DEFAULT '',
|
|
supersedes_id TEXT REFERENCES account_valuations(id),
|
|
correction_reason TEXT NOT NULL DEFAULT '',
|
|
as_of_at TEXT,
|
|
timezone TEXT,
|
|
cost_basis INTEGER,
|
|
net_contributed INTEGER,
|
|
reported_principal_kind TEXT,
|
|
reported_principal_label TEXT,
|
|
profit INTEGER,
|
|
return_rate REAL,
|
|
return_basis TEXT,
|
|
principal INTEGER,
|
|
interest INTEGER,
|
|
fee INTEGER,
|
|
representative_liability_definition TEXT
|
|
);
|
|
|
|
INSERT OR REPLACE INTO schema_meta(key, value) VALUES ('schema_version', '15');
|
|
"""
|
|
|
|
ACCOUNT_ADDITIVE_COLUMNS: dict[str, str] = {
|
|
"account_number_masked": "TEXT NOT NULL DEFAULT ''",
|
|
"holder_name": "TEXT NOT NULL DEFAULT ''",
|
|
"last_balance": "INTEGER",
|
|
"last_balance_basis_date": "TEXT NOT NULL DEFAULT ''",
|
|
"last_synced_at": "TEXT NOT NULL DEFAULT ''",
|
|
"statement_status": "TEXT NOT NULL DEFAULT 'unknown'",
|
|
"liquidity_bucket": "TEXT NOT NULL DEFAULT 'other'",
|
|
}
|
|
|
|
TRANSACTION_ADDITIVE_COLUMNS: dict[str, str] = {
|
|
"transaction_datetime": "TEXT NOT NULL DEFAULT ''",
|
|
"counterparty": "TEXT NOT NULL DEFAULT ''",
|
|
"balance_after": "INTEGER",
|
|
"branch": "TEXT NOT NULL DEFAULT ''",
|
|
"raw_summary": "TEXT NOT NULL DEFAULT ''",
|
|
"raw_payload": "TEXT NOT NULL DEFAULT ''",
|
|
}
|