async def m001_initial(db): """ Initial migration for Castle accounting extension. Creates tables for double-entry bookkeeping system. """ await db.execute( f""" CREATE TABLE accounts ( id TEXT PRIMARY KEY, name TEXT NOT NULL, account_type TEXT NOT NULL, description TEXT, user_id TEXT, created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now} ); """ ) await db.execute( """ CREATE INDEX idx_accounts_user_id ON accounts (user_id); """ ) await db.execute( """ CREATE INDEX idx_accounts_type ON accounts (account_type); """ ) await db.execute( f""" CREATE TABLE journal_entries ( id TEXT PRIMARY KEY, description TEXT NOT NULL, entry_date TIMESTAMP NOT NULL, created_by TEXT NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}, reference TEXT ); """ ) await db.execute( """ CREATE INDEX idx_journal_entries_created_by ON journal_entries (created_by); """ ) await db.execute( """ CREATE INDEX idx_journal_entries_date ON journal_entries (entry_date); """ ) await db.execute( f""" CREATE TABLE entry_lines ( id TEXT PRIMARY KEY, journal_entry_id TEXT NOT NULL, account_id TEXT NOT NULL, debit INTEGER NOT NULL DEFAULT 0, credit INTEGER NOT NULL DEFAULT 0, description TEXT, metadata TEXT DEFAULT '{{}}' ); """ ) await db.execute( """ CREATE INDEX idx_entry_lines_journal_entry ON entry_lines (journal_entry_id); """ ) await db.execute( """ CREATE INDEX idx_entry_lines_account ON entry_lines (account_id); """ ) # Insert default chart of accounts default_accounts = [ # Assets ("cash", "Cash", "asset", "Cash on hand"), ("bank", "Bank Account", "asset", "Bank account"), ("lightning", "Lightning Balance", "asset", "Lightning Network balance"), ("accounts_receivable", "Accounts Receivable", "asset", "Money owed to the Castle"), # Liabilities ("accounts_payable", "Accounts Payable", "liability", "Money owed by the Castle"), # Equity ("member_equity", "Member Equity", "equity", "Member contributions"), ("retained_earnings", "Retained Earnings", "equity", "Accumulated profits"), # Revenue ("accommodation_revenue", "Accommodation Revenue", "revenue", "Revenue from stays"), ("service_revenue", "Service Revenue", "revenue", "Revenue from services"), ("other_revenue", "Other Revenue", "revenue", "Other revenue"), # Expenses ("utilities", "Utilities", "expense", "Electricity, water, internet"), ("food", "Food & Supplies", "expense", "Food and supplies"), ("maintenance", "Maintenance", "expense", "Repairs and maintenance"), ("other_expense", "Other Expenses", "expense", "Miscellaneous expenses"), ] for acc_id, name, acc_type, desc in default_accounts: await db.execute( """ INSERT INTO accounts (id, name, account_type, description) VALUES (:id, :name, :type, :description) """, {"id": acc_id, "name": name, "type": acc_type, "description": desc} ) async def m002_extension_settings(db): """ Create extension_settings table for Castle configuration. """ await db.execute( f""" CREATE TABLE extension_settings ( id TEXT NOT NULL PRIMARY KEY, castle_wallet_id TEXT, updated_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now} ); """ ) async def m003_user_wallet_settings(db): """ Create user_wallet_settings table for per-user wallet configuration. """ await db.execute( f""" CREATE TABLE user_wallet_settings ( id TEXT NOT NULL PRIMARY KEY, user_wallet_id TEXT, updated_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now} ); """ ) async def m004_manual_payment_requests(db): """ Create manual_payment_requests table for user payment requests to Castle. """ await db.execute( f""" CREATE TABLE manual_payment_requests ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, amount INTEGER NOT NULL, description TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending', created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}, reviewed_at TIMESTAMP, reviewed_by TEXT, journal_entry_id TEXT ); """ ) await db.execute( """ CREATE INDEX idx_manual_payment_requests_user_id ON manual_payment_requests (user_id); """ ) await db.execute( """ CREATE INDEX idx_manual_payment_requests_status ON manual_payment_requests (status); """ ) async def m005_add_flag_and_meta(db): """ Add flag and meta columns to journal_entries table. - flag: Transaction status (* = cleared, ! = pending, # = flagged, x = void) - meta: JSON metadata for audit trail (source, tags, links, notes) """ await db.execute( """ ALTER TABLE journal_entries ADD COLUMN flag TEXT DEFAULT '*'; """ ) await db.execute( """ ALTER TABLE journal_entries ADD COLUMN meta TEXT DEFAULT '{}'; """ ) async def m006_hierarchical_account_names(db): """ Migrate account names to hierarchical Beancount-style format. - "Cash" → "Assets:Cash" - "Accounts Receivable" → "Assets:Receivable" - "Food & Supplies" → "Expenses:Food:Supplies" - "Accounts Receivable - af983632" → "Assets:Receivable:User-af983632" """ from .account_utils import migrate_account_name from .models import AccountType # Get all existing accounts accounts = await db.fetchall("SELECT * FROM accounts") # Mapping of old names to new names name_mappings = { # Assets "cash": "Assets:Cash", "bank": "Assets:Bank", "lightning": "Assets:Bitcoin:Lightning", "accounts_receivable": "Assets:Receivable", # Liabilities "accounts_payable": "Liabilities:Payable", # Equity "member_equity": "Equity:MemberEquity", "retained_earnings": "Equity:RetainedEarnings", # Revenue → Income "accommodation_revenue": "Income:Accommodation", "service_revenue": "Income:Service", "other_revenue": "Income:Other", # Expenses "utilities": "Expenses:Utilities", "food": "Expenses:Food:Supplies", "maintenance": "Expenses:Maintenance", "other_expense": "Expenses:Other", } # Update default accounts using ID-based mapping for old_id, new_name in name_mappings.items(): await db.execute( """ UPDATE accounts SET name = :new_name WHERE id = :old_id """, {"new_name": new_name, "old_id": old_id} ) # Update user-specific accounts (those with user_id set) user_accounts = await db.fetchall( "SELECT * FROM accounts WHERE user_id IS NOT NULL" ) for account in user_accounts: # Parse account type account_type = AccountType(account["account_type"]) # Migrate name new_name = migrate_account_name(account["name"], account_type) await db.execute( """ UPDATE accounts SET name = :new_name WHERE id = :id """, {"new_name": new_name, "id": account["id"]} ) async def m007_balance_assertions(db): """ Create balance_assertions table for reconciliation. Allows admins to assert expected balances at specific dates. """ await db.execute( f""" CREATE TABLE balance_assertions ( id TEXT PRIMARY KEY, date TIMESTAMP NOT NULL, account_id TEXT NOT NULL, expected_balance_sats INTEGER NOT NULL, expected_balance_fiat TEXT, fiat_currency TEXT, tolerance_sats INTEGER DEFAULT 0, tolerance_fiat TEXT DEFAULT '0', checked_balance_sats INTEGER, checked_balance_fiat TEXT, difference_sats INTEGER, difference_fiat TEXT, status TEXT NOT NULL DEFAULT 'pending', created_by TEXT NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}, checked_at TIMESTAMP, FOREIGN KEY (account_id) REFERENCES accounts (id) ); """ ) await db.execute( """ CREATE INDEX idx_balance_assertions_account_id ON balance_assertions (account_id); """ ) await db.execute( """ CREATE INDEX idx_balance_assertions_status ON balance_assertions (status); """ ) await db.execute( """ CREATE INDEX idx_balance_assertions_date ON balance_assertions (date); """ ) async def m008_rename_lightning_account(db): """ Rename Lightning account from Assets:Lightning:Balance to Assets:Bitcoin:Lightning for better naming consistency. """ await db.execute( """ UPDATE accounts SET name = 'Assets:Bitcoin:Lightning' WHERE name = 'Assets:Lightning:Balance' """ )