2026-05-03 19:51:57 -05:00
|
|
|
PRAGMA journal_mode = WAL;
|
|
|
|
|
PRAGMA foreign_keys = ON;
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS categories (
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
|
|
|
name TEXT NOT NULL,
|
2026-05-30 20:04:50 -05:00
|
|
|
sort_order INTEGER,
|
2026-05-16 10:34:32 -05:00
|
|
|
deleted_at TEXT,
|
2026-05-03 19:51:57 -05:00
|
|
|
created_at TEXT DEFAULT (datetime('now')),
|
|
|
|
|
updated_at TEXT DEFAULT (datetime('now'))
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS bills (
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
|
|
|
name TEXT NOT NULL,
|
|
|
|
|
category_id INTEGER REFERENCES categories(id) ON DELETE SET NULL,
|
|
|
|
|
due_day INTEGER NOT NULL CHECK(due_day BETWEEN 1 AND 31),
|
|
|
|
|
override_due_date TEXT,
|
|
|
|
|
bucket TEXT CHECK(bucket IN ('1st', '15th')),
|
|
|
|
|
expected_amount REAL NOT NULL DEFAULT 0,
|
|
|
|
|
interest_rate REAL CHECK(interest_rate IS NULL OR (interest_rate >= 0 AND interest_rate <= 100)),
|
|
|
|
|
billing_cycle TEXT DEFAULT 'monthly' CHECK(billing_cycle IN ('monthly', 'quarterly', 'annually', 'irregular')),
|
|
|
|
|
autopay_enabled INTEGER NOT NULL DEFAULT 0,
|
|
|
|
|
autodraft_status TEXT NOT NULL DEFAULT 'none' CHECK(autodraft_status IN ('none', 'pending', 'assumed_paid', 'confirmed')),
|
2026-05-16 15:38:28 -05:00
|
|
|
auto_mark_paid INTEGER NOT NULL DEFAULT 0,
|
2026-05-03 19:51:57 -05:00
|
|
|
website TEXT,
|
|
|
|
|
username TEXT,
|
|
|
|
|
account_info TEXT,
|
|
|
|
|
has_2fa INTEGER NOT NULL DEFAULT 0,
|
|
|
|
|
active INTEGER NOT NULL DEFAULT 1,
|
2026-05-14 03:00:01 -05:00
|
|
|
current_balance REAL,
|
|
|
|
|
minimum_payment REAL,
|
|
|
|
|
snowball_order INTEGER,
|
2026-05-30 16:13:37 -05:00
|
|
|
sort_order INTEGER,
|
2026-05-14 03:00:01 -05:00
|
|
|
snowball_include INTEGER NOT NULL DEFAULT 0,
|
|
|
|
|
snowball_exempt INTEGER NOT NULL DEFAULT 0,
|
2026-05-28 22:54:07 -05:00
|
|
|
is_subscription INTEGER NOT NULL DEFAULT 0,
|
|
|
|
|
subscription_type TEXT,
|
|
|
|
|
reminder_days_before INTEGER NOT NULL DEFAULT 3,
|
|
|
|
|
subscription_source TEXT NOT NULL DEFAULT 'manual',
|
|
|
|
|
subscription_detected_at TEXT,
|
2026-05-16 10:34:32 -05:00
|
|
|
deleted_at TEXT,
|
2026-05-03 19:51:57 -05:00
|
|
|
notes TEXT,
|
2026-05-30 14:33:55 -05:00
|
|
|
drift_snoozed_until TEXT,
|
2026-05-03 19:51:57 -05:00
|
|
|
created_at TEXT DEFAULT (datetime('now')),
|
|
|
|
|
updated_at TEXT DEFAULT (datetime('now'))
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS payments (
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
|
bill_id INTEGER NOT NULL REFERENCES bills(id) ON DELETE CASCADE,
|
|
|
|
|
amount REAL NOT NULL,
|
|
|
|
|
paid_date TEXT NOT NULL,
|
|
|
|
|
method TEXT,
|
|
|
|
|
notes TEXT,
|
2026-05-14 03:00:01 -05:00
|
|
|
balance_delta REAL,
|
2026-05-16 20:26:09 -05:00
|
|
|
payment_source TEXT NOT NULL DEFAULT 'manual',
|
|
|
|
|
transaction_id INTEGER,
|
2026-05-16 21:36:04 -05:00
|
|
|
deleted_at TEXT,
|
2026-05-03 19:51:57 -05:00
|
|
|
created_at TEXT DEFAULT (datetime('now')),
|
|
|
|
|
updated_at TEXT DEFAULT (datetime('now'))
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
|
|
|
key TEXT PRIMARY KEY,
|
|
|
|
|
value TEXT NOT NULL,
|
|
|
|
|
updated_at TEXT DEFAULT (datetime('now'))
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
|
username TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
|
|
|
|
password_hash TEXT NOT NULL,
|
|
|
|
|
role TEXT NOT NULL DEFAULT 'user' CHECK(role IN ('admin', 'user')),
|
2026-05-04 23:34:24 -05:00
|
|
|
active INTEGER NOT NULL DEFAULT 1,
|
|
|
|
|
is_default_admin INTEGER NOT NULL DEFAULT 0,
|
2026-05-03 19:51:57 -05:00
|
|
|
must_change_password INTEGER NOT NULL DEFAULT 0,
|
|
|
|
|
first_login INTEGER NOT NULL DEFAULT 1,
|
2026-05-14 03:00:01 -05:00
|
|
|
snowball_extra_payment REAL NOT NULL DEFAULT 0,
|
2026-05-30 14:33:55 -05:00
|
|
|
notify_amount_change INTEGER NOT NULL DEFAULT 1,
|
2026-05-03 19:51:57 -05:00
|
|
|
created_at TEXT DEFAULT (datetime('now')),
|
|
|
|
|
updated_at TEXT DEFAULT (datetime('now'))
|
|
|
|
|
);
|
|
|
|
|
|
2026-05-15 22:45:38 -05:00
|
|
|
CREATE TABLE IF NOT EXISTS user_settings (
|
|
|
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
|
|
|
key TEXT NOT NULL,
|
|
|
|
|
value TEXT NOT NULL,
|
|
|
|
|
updated_at TEXT DEFAULT (datetime('now')),
|
|
|
|
|
PRIMARY KEY (user_id, key)
|
|
|
|
|
);
|
|
|
|
|
|
2026-05-16 20:26:09 -05:00
|
|
|
CREATE TABLE IF NOT EXISTS data_sources (
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
|
|
|
type TEXT NOT NULL,
|
|
|
|
|
provider TEXT,
|
|
|
|
|
name TEXT NOT NULL,
|
|
|
|
|
status TEXT NOT NULL DEFAULT 'active',
|
|
|
|
|
config_json TEXT,
|
|
|
|
|
encrypted_secret TEXT,
|
|
|
|
|
last_sync_at TEXT,
|
|
|
|
|
last_error TEXT,
|
|
|
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS financial_accounts (
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
|
|
|
data_source_id INTEGER,
|
|
|
|
|
provider_account_id TEXT,
|
|
|
|
|
name TEXT NOT NULL,
|
|
|
|
|
org_name TEXT,
|
|
|
|
|
account_type TEXT,
|
|
|
|
|
currency TEXT,
|
|
|
|
|
balance INTEGER,
|
|
|
|
|
available_balance INTEGER,
|
|
|
|
|
raw_data TEXT,
|
|
|
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
|
|
|
|
|
|
FOREIGN KEY (data_source_id) REFERENCES data_sources(id) ON DELETE SET NULL,
|
|
|
|
|
UNIQUE(data_source_id, provider_account_id)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS transactions (
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
|
|
|
data_source_id INTEGER,
|
|
|
|
|
account_id INTEGER,
|
|
|
|
|
provider_transaction_id TEXT,
|
|
|
|
|
source_type TEXT NOT NULL,
|
|
|
|
|
transaction_type TEXT,
|
|
|
|
|
posted_date TEXT,
|
|
|
|
|
transacted_at TEXT,
|
|
|
|
|
amount INTEGER NOT NULL,
|
|
|
|
|
currency TEXT,
|
|
|
|
|
description TEXT,
|
|
|
|
|
payee TEXT,
|
|
|
|
|
memo TEXT,
|
|
|
|
|
category TEXT,
|
|
|
|
|
raw_data TEXT,
|
|
|
|
|
matched_bill_id INTEGER,
|
|
|
|
|
match_status TEXT NOT NULL DEFAULT 'unmatched',
|
|
|
|
|
ignored INTEGER NOT NULL DEFAULT 0,
|
|
|
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
|
|
|
|
|
|
FOREIGN KEY (data_source_id) REFERENCES data_sources(id) ON DELETE SET NULL,
|
|
|
|
|
FOREIGN KEY (account_id) REFERENCES financial_accounts(id) ON DELETE SET NULL,
|
|
|
|
|
FOREIGN KEY (matched_bill_id) REFERENCES bills(id) ON DELETE SET NULL
|
|
|
|
|
);
|
|
|
|
|
|
2026-05-03 19:51:57 -05:00
|
|
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
|
|
|
id TEXT PRIMARY KEY,
|
|
|
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
|
|
|
expires_at TEXT NOT NULL,
|
|
|
|
|
created_at TEXT DEFAULT (datetime('now'))
|
|
|
|
|
);
|
|
|
|
|
|
2026-05-15 22:45:38 -05:00
|
|
|
CREATE TABLE IF NOT EXISTS user_login_history (
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
|
|
|
logged_in_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
|
|
|
ip_address TEXT,
|
|
|
|
|
user_agent TEXT,
|
|
|
|
|
browser TEXT,
|
|
|
|
|
os TEXT,
|
|
|
|
|
device_type TEXT,
|
|
|
|
|
device_fingerprint TEXT
|
|
|
|
|
);
|
|
|
|
|
|
2026-05-03 19:51:57 -05:00
|
|
|
CREATE TABLE IF NOT EXISTS notifications (
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
|
bill_id INTEGER NOT NULL REFERENCES bills(id) ON DELETE CASCADE,
|
|
|
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
|
|
|
year INTEGER NOT NULL,
|
|
|
|
|
month INTEGER NOT NULL,
|
|
|
|
|
type TEXT NOT NULL, -- due_3d | due_1d | due_today | overdue
|
|
|
|
|
sent_date TEXT NOT NULL DEFAULT (date('now')),
|
|
|
|
|
UNIQUE(bill_id, user_id, year, month, type, sent_date)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_notifications_lookup ON notifications(bill_id, user_id, year, month);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_bills_active ON bills(active);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_payments_bill_id ON payments(bill_id);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_payments_paid_date ON payments(paid_date);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);
|
2026-05-16 20:26:09 -05:00
|
|
|
CREATE INDEX IF NOT EXISTS idx_data_sources_user_type ON data_sources(user_id, type, status);
|
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_data_sources_user_manual
|
|
|
|
|
ON data_sources(user_id, type, provider)
|
|
|
|
|
WHERE type = 'manual' AND provider = 'manual';
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_financial_accounts_user_source ON financial_accounts(user_id, data_source_id);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_transactions_user_date ON transactions(user_id, posted_date, transacted_at);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_transactions_user_match ON transactions(user_id, match_status, ignored);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_transactions_account ON transactions(account_id);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_transactions_matched_bill ON transactions(matched_bill_id);
|
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_transactions_provider_dedupe
|
|
|
|
|
ON transactions (data_source_id, provider_transaction_id)
|
|
|
|
|
WHERE provider_transaction_id IS NOT NULL;
|
2026-05-03 19:51:57 -05:00
|
|
|
|
2026-05-16 21:36:04 -05:00
|
|
|
CREATE TABLE IF NOT EXISTS match_suggestion_rejections (
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
|
|
|
transaction_id INTEGER NOT NULL REFERENCES transactions(id) ON DELETE CASCADE,
|
|
|
|
|
bill_id INTEGER NOT NULL REFERENCES bills(id) ON DELETE CASCADE,
|
|
|
|
|
rejected_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
|
|
|
UNIQUE(user_id, transaction_id, bill_id)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_match_suggestion_rejections_user
|
|
|
|
|
ON match_suggestion_rejections(user_id, transaction_id, bill_id);
|
|
|
|
|
|
2026-05-03 19:51:57 -05:00
|
|
|
CREATE TABLE IF NOT EXISTS monthly_bill_state (
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
|
bill_id INTEGER NOT NULL REFERENCES bills(id) ON DELETE CASCADE,
|
|
|
|
|
year INTEGER NOT NULL CHECK(year BETWEEN 2000 AND 2100),
|
|
|
|
|
month INTEGER NOT NULL CHECK(month BETWEEN 1 AND 12),
|
|
|
|
|
actual_amount REAL, -- NULL = use bill.expected_amount for this month
|
|
|
|
|
notes TEXT, -- month-specific notes, NULL = no notes
|
|
|
|
|
is_skipped INTEGER NOT NULL DEFAULT 0, -- 1 = hidden/removed for this month only
|
2026-05-30 13:19:09 -05:00
|
|
|
snoozed_until TEXT, -- ISO date: hide from overdue command center until this date
|
2026-05-03 19:51:57 -05:00
|
|
|
created_at TEXT DEFAULT (datetime('now')),
|
|
|
|
|
updated_at TEXT DEFAULT (datetime('now')),
|
|
|
|
|
UNIQUE(bill_id, year, month)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_monthly_bill_state_lookup
|
|
|
|
|
ON monthly_bill_state(bill_id, year, month);
|
2026-05-16 15:38:28 -05:00
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS autopay_suggestion_dismissals (
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
|
|
|
bill_id INTEGER NOT NULL REFERENCES bills(id) ON DELETE CASCADE,
|
|
|
|
|
year INTEGER NOT NULL CHECK(year BETWEEN 2000 AND 2100),
|
|
|
|
|
month INTEGER NOT NULL CHECK(month BETWEEN 1 AND 12),
|
|
|
|
|
dismissed_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
|
|
|
UNIQUE(user_id, bill_id, year, month)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_autopay_suggestion_dismissals_user_month
|
|
|
|
|
ON autopay_suggestion_dismissals(user_id, year, month);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS bill_templates (
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
|
|
|
name TEXT NOT NULL,
|
|
|
|
|
data TEXT NOT NULL,
|
|
|
|
|
created_at TEXT DEFAULT (datetime('now')),
|
|
|
|
|
updated_at TEXT DEFAULT (datetime('now')),
|
|
|
|
|
UNIQUE(user_id, name)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_bill_templates_user_name
|
|
|
|
|
ON bill_templates(user_id, name);
|
2026-05-29 18:06:12 -05:00
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS advisory_non_bill_filters (
|
|
|
|
|
id TEXT PRIMARY KEY,
|
|
|
|
|
pattern TEXT NOT NULL,
|
|
|
|
|
confidence TEXT NOT NULL CHECK(confidence IN ('high', 'medium')),
|
|
|
|
|
category TEXT NOT NULL,
|
|
|
|
|
rationale TEXT
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_advisory_filters_confidence
|
|
|
|
|
ON advisory_non_bill_filters(confidence);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS advisory_bill_like_overrides (
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
|
term TEXT NOT NULL UNIQUE
|
|
|
|
|
);
|
2026-05-30 17:27:15 -05:00
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS snowball_plans (
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
|
|
|
name TEXT NOT NULL DEFAULT 'Snowball Plan',
|
|
|
|
|
method TEXT NOT NULL DEFAULT 'snowball',
|
|
|
|
|
status TEXT NOT NULL DEFAULT 'active',
|
|
|
|
|
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
|
|
|
paused_at TEXT,
|
|
|
|
|
completed_at TEXT,
|
|
|
|
|
extra_payment REAL NOT NULL DEFAULT 0,
|
|
|
|
|
plan_snapshot TEXT NOT NULL,
|
|
|
|
|
notes TEXT,
|
|
|
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
|
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_snowball_plans_user
|
|
|
|
|
ON snowball_plans(user_id, status, created_at);
|