castle/migrations.py
padreug 9ac3494f1b Squash 16 migrations into single clean initial migration
Since Castle extension has not been released yet, squash all database migrations
for cleaner initial deployments. This reduces migration complexity and improves
maintainability.

Changes:
- Squash migrations m001-m016 into single m001_initial migration
- Reduced from 651 lines (16 functions) to 327 lines (1 function)
- 50% code reduction, 93.75% fewer migration functions

Final database schema (7 tables):
- castle_accounts: Chart of accounts with 40+ default accounts
- castle_extension_settings: Castle configuration
- castle_user_wallet_settings: User wallet associations
- castle_manual_payment_requests: Payment approval workflow
- castle_balance_assertions: Reconciliation with Beancount integration
- castle_user_equity_status: Equity eligibility tracking
- castle_account_permissions: Granular access control

Tables removed (intentionally):
- castle_journal_entries: Now managed by Fava/Beancount (dropped in m016)
- castle_entry_lines: Now managed by Fava/Beancount (dropped in m016)

New migration includes:
- All 7 tables in their final state
- All indexes properly prefixed with idx_castle_
- All foreign key constraints
- 40+ default accounts with hierarchical names (Assets:Bitcoin:Lightning, etc.)
- Comprehensive documentation

Files:
- migrations.py: Single clean m001_initial migration
- migrations_old.py.bak: Backup of original 16 migrations for reference
- MIGRATION_SQUASH_SUMMARY.md: Complete documentation of squash process

Benefits:
- Simpler initial deployments (1 migration instead of 16)
- Easier to understand final schema
- Faster migration execution
- Cleaner codebase

See MIGRATION_SQUASH_SUMMARY.md for full details and testing instructions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 21:51:11 +01:00

327 lines
11 KiB
Python

"""
Castle Extension Database Migrations
This file contains a single squashed migration that creates the complete
database schema for the Castle extension.
MIGRATION HISTORY:
This is a squashed migration that combines m001-m016 from the original
incremental migration history. The complete historical migrations are
preserved in migrations_old.py.bak for reference.
Key schema decisions reflected in this migration:
1. Hierarchical Beancount-style account names (e.g., "Assets:Bitcoin:Lightning")
2. No journal_entries/entry_lines tables (Fava is source of truth)
3. User-specific equity accounts created dynamically (Equity:User-{user_id})
4. Parent-only accounts removed (hierarchy implicit in colon-separated names)
5. Multi-currency support via balance_assertions
6. Granular permission system via account_permissions
Original migration sequence (Nov 2025):
- m001: Initial accounts, journal_entries, entry_lines tables
- m002: Extension settings
- m003: User wallet settings
- m004: Manual payment requests
- m005: Added flag/meta to journal entries
- m006: Migrated to hierarchical account names
- m007: Balance assertions
- m008: Renamed Lightning account
- m009: Added OnChain Bitcoin account
- m010: User equity status
- m011: Account permissions
- m012: Updated default accounts with detailed hierarchy
- m013: Removed parent-only accounts (Assets:Bitcoin, Equity)
- m014: Removed legacy equity accounts (MemberEquity, RetainedEarnings)
- m015: Converted entry_lines to single amount field
- m016: Dropped journal_entries and entry_lines tables (Fava integration)
"""
async def m001_initial(db):
"""
Initial Castle database schema (squashed from m001-m016).
Creates complete database structure for Castle accounting extension:
- Accounts: Chart of accounts with hierarchical Beancount-style names
- Extension settings: Castle-wide configuration
- User wallet settings: Per-user wallet configuration
- Manual payment requests: User-submitted payment requests to Castle
- Balance assertions: Reconciliation and balance checking
- User equity status: Equity contribution eligibility
- Account permissions: Granular access control
Note: Journal entries are managed by Fava/Beancount (external source of truth).
Castle submits entries to Fava and queries Fava for journal data.
"""
# =========================================================================
# ACCOUNTS TABLE
# =========================================================================
# Core chart of accounts with hierarchical Beancount-style naming.
# Examples: "Assets:Bitcoin:Lightning", "Expenses:Food:Groceries"
# User-specific accounts: "Assets:Receivable:User-af983632"
await db.execute(
f"""
CREATE TABLE castle_accounts (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
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_castle_accounts_user_id ON castle_accounts (user_id);
"""
)
await db.execute(
"""
CREATE INDEX idx_castle_accounts_type ON castle_accounts (account_type);
"""
)
# =========================================================================
# EXTENSION SETTINGS TABLE
# =========================================================================
# Castle-wide configuration settings
await db.execute(
f"""
CREATE TABLE castle_extension_settings (
id TEXT NOT NULL PRIMARY KEY,
castle_wallet_id TEXT,
updated_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
);
"""
)
# =========================================================================
# USER WALLET SETTINGS TABLE
# =========================================================================
# Per-user wallet configuration
await db.execute(
f"""
CREATE TABLE castle_user_wallet_settings (
id TEXT NOT NULL PRIMARY KEY,
user_wallet_id TEXT,
updated_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
);
"""
)
# =========================================================================
# MANUAL PAYMENT REQUESTS TABLE
# =========================================================================
# User-submitted payment requests to Castle (reviewed by admins)
await db.execute(
f"""
CREATE TABLE castle_manual_payment_requests (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
amount INTEGER NOT NULL,
description TEXT NOT NULL,
notes TEXT,
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_castle_manual_payment_requests_user_id
ON castle_manual_payment_requests (user_id);
"""
)
await db.execute(
"""
CREATE INDEX idx_castle_manual_payment_requests_status
ON castle_manual_payment_requests (status);
"""
)
# =========================================================================
# BALANCE ASSERTIONS TABLE
# =========================================================================
# Reconciliation and balance checking at specific dates
# Supports multi-currency (satoshis + fiat) with tolerance checking
await db.execute(
f"""
CREATE TABLE castle_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,
notes 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 castle_accounts (id)
);
"""
)
await db.execute(
"""
CREATE INDEX idx_castle_balance_assertions_account_id
ON castle_balance_assertions (account_id);
"""
)
await db.execute(
"""
CREATE INDEX idx_castle_balance_assertions_status
ON castle_balance_assertions (status);
"""
)
await db.execute(
"""
CREATE INDEX idx_castle_balance_assertions_date
ON castle_balance_assertions (date);
"""
)
# =========================================================================
# USER EQUITY STATUS TABLE
# =========================================================================
# Manages equity contribution eligibility for users
# Equity-eligible users can convert expenses to equity contributions
# Creates dynamic user-specific equity accounts: Equity:User-{user_id}
await db.execute(
f"""
CREATE TABLE castle_user_equity_status (
user_id TEXT PRIMARY KEY,
is_equity_eligible BOOLEAN NOT NULL DEFAULT FALSE,
equity_account_name TEXT,
notes TEXT,
granted_by TEXT NOT NULL,
granted_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
revoked_at TIMESTAMP
);
"""
)
await db.execute(
"""
CREATE INDEX idx_castle_user_equity_status_eligible
ON castle_user_equity_status (is_equity_eligible)
WHERE is_equity_eligible = TRUE;
"""
)
# =========================================================================
# ACCOUNT PERMISSIONS TABLE
# =========================================================================
# Granular access control for accounts
# Permission types: read, submit_expense, manage
# Supports hierarchical inheritance (parent account permissions cascade)
await db.execute(
f"""
CREATE TABLE castle_account_permissions (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
account_id TEXT NOT NULL,
permission_type TEXT NOT NULL,
granted_by TEXT NOT NULL,
granted_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
expires_at TIMESTAMP,
notes TEXT,
FOREIGN KEY (account_id) REFERENCES castle_accounts (id)
);
"""
)
# Index for looking up permissions by user
await db.execute(
"""
CREATE INDEX idx_castle_account_permissions_user_id
ON castle_account_permissions (user_id);
"""
)
# Index for looking up permissions by account
await db.execute(
"""
CREATE INDEX idx_castle_account_permissions_account_id
ON castle_account_permissions (account_id);
"""
)
# Composite index for checking specific user+account permissions
await db.execute(
"""
CREATE INDEX idx_castle_account_permissions_user_account
ON castle_account_permissions (user_id, account_id);
"""
)
# Index for finding permissions by type
await db.execute(
"""
CREATE INDEX idx_castle_account_permissions_type
ON castle_account_permissions (permission_type);
"""
)
# Index for finding expired permissions
await db.execute(
"""
CREATE INDEX idx_castle_account_permissions_expires
ON castle_account_permissions (expires_at)
WHERE expires_at IS NOT NULL;
"""
)
# =========================================================================
# DEFAULT CHART OF ACCOUNTS
# =========================================================================
# Insert comprehensive default accounts with hierarchical names.
# These accounts cover common use cases and can be extended by admins.
#
# Note: User-specific accounts (e.g., Assets:Receivable:User-xxx) are
# created dynamically when users interact with the system.
#
# Note: Equity accounts (Equity:User-xxx) are created dynamically when
# admins grant equity eligibility to users.
import uuid
from .account_utils import DEFAULT_HIERARCHICAL_ACCOUNTS
for name, account_type, description in DEFAULT_HIERARCHICAL_ACCOUNTS:
await db.execute(
f"""
INSERT INTO castle_accounts (id, name, account_type, description, created_at)
VALUES (:id, :name, :type, :description, {db.timestamp_now})
""",
{
"id": str(uuid.uuid4()),
"name": name,
"type": account_type.value,
"description": description
}
)