# Phase 3: Core Logic Refactoring - COMPLETE ✅ ## Summary Phase 3 of the Beancount-inspired refactor focused on **separating business logic from database operations** and creating a clean, testable core module. This phase improves code quality, maintainability, and follows best practices from Beancount's architecture. ## Completed Features ### 1. Core Module Structure ✅ **Purpose**: Separate pure accounting logic from database and API concerns **Implementation** (`core/__init__.py`): - Created `core/` module package - Exports main classes and functions - Clean separation of concerns **Benefits**: - Testable without database - Reusable across different storage backends - Easier to audit and verify - Clear architecture ### 2. CastleInventory for Position Tracking ✅ **Purpose**: Track balances across multiple currencies with cost basis information (following Beancount's Inventory pattern) **Implementation** (`core/inventory.py`): **CastlePosition** (Lines 11-84): - Immutable dataclass representing a single position - Tracks currency, amount, cost basis, and metadata - Supports addition and negation operations - Automatic Decimal conversion in `__post_init__` ```python @dataclass(frozen=True) class CastlePosition: currency: str # "SATS", "EUR", "USD" amount: Decimal cost_currency: Optional[str] = None cost_amount: Optional[Decimal] = None date: Optional[datetime] = None metadata: Dict[str, Any] = field(default_factory=dict) ``` **CastleInventory** (Lines 87-201): - Container for multiple positions - Positions keyed by `(currency, cost_currency)` tuple - Methods for querying balances: - `get_balance_sats()` - Total satoshis - `get_balance_fiat(currency)` - Fiat balance for specific currency - `get_all_fiat_balances()` - All fiat balances - Utility methods: - `is_empty()` - Check if no positions - `is_zero()` - Check if all positions sum to zero - `to_dict()` - Export to dictionary ### 3. BalanceCalculator ✅ **Purpose**: Pure logic for calculating balances from journal entries **Implementation** (`core/balance.py`): **AccountType Enum** (Lines 13-19): ```python class AccountType(str, Enum): ASSET = "asset" LIABILITY = "liability" EQUITY = "equity" REVENUE = "revenue" EXPENSE = "expense" ``` **BalanceCalculator Class** (Lines 22-217): **Static Methods**: 1. **`calculate_account_balance()`** (Lines 29-54): - Calculate balance based on account type - Normal balances: - Assets/Expenses: Debit balance (debit - credit) - Liabilities/Equity/Revenue: Credit balance (credit - debit) 2. **`build_inventory_from_entry_lines()`** (Lines 56-117): - Build CastleInventory from journal entry lines - Handles both sats and fiat currency tracking - Accounts for account type when determining sign 3. **`calculate_user_balance()`** (Lines 119-168): - Calculate user's total balance across all accounts - Returns both sats balance and fiat balances by currency - Properly handles asset (receivable) vs liability (payable) accounts 4. **`check_balance_matches()`** (Lines 170-187): - Verify balance assertion for sats 5. **`check_fiat_balance_matches()`** (Lines 189-202): - Verify balance assertion for fiat currency ### 4. Comprehensive Validation ✅ **Purpose**: Validation rules for accounting operations **Implementation** (`core/validation.py`): **ValidationError Exception** (Lines 10-18): - Custom exception for validation failures - Includes detailed error information **Validation Functions**: 1. **`validate_journal_entry()`** (Lines 21-124): - Checks: - At least 2 lines (double-entry requirement) - Entry is balanced (debits = credits) - Valid amounts (non-negative) - No line has both debit and credit - All lines have account_id 2. **`validate_balance()`** (Lines 127-177): - Validates balance assertions - Checks both sats and fiat within tolerance 3. **`validate_receivable_entry()`** (Lines 180-199): - Validates receivable (user owes castle) entries - Ensures positive amount - Ensures revenue account type 4. **`validate_expense_entry()`** (Lines 202-227): - Validates expense entries - Ensures positive amount - Checks account type (expense or equity) 5. **`validate_payment_entry()`** (Lines 230-245): - Validates payment entries - Ensures positive amount 6. **`validate_metadata()`** (Lines 248-284): - Validates entry line metadata - Checks for required keys - Validates fiat currency/amount consistency - Validates Decimal conversion ### 5. Refactored CRUD Operations ✅ **Purpose**: Use core logic in database operations **Modified Files**: `crud.py` **Changes**: 1. **Imports** (Lines 26-36): - Import core accounting logic - Import validation functions 2. **`get_account_balance()`** (Lines 347-377): - Refactored to use `BalanceCalculator.calculate_account_balance()` - Removed duplicate logic 3. **`get_user_balance()`** (Lines 380-435): - Completely refactored to use: - `BalanceCalculator.build_inventory_from_entry_lines()` - `BalanceCalculator.calculate_user_balance()` - Cleaner separation of database queries vs business logic 4. **`get_all_user_balances()`** (Lines 438-459): - Simplified to call `get_user_balance()` for each user - Eliminates code duplication ## Architecture ### Before Phase 3 ``` views_api.py → crud.py (mixed DB + logic) ↓ database ``` All accounting logic was embedded in crud.py alongside database operations. ### After Phase 3 ``` views_api.py → crud.py → core/ ↓ ↓ database Pure Logic (testable) ``` **Separation of Concerns**: - `core/` - Pure accounting logic (no DB dependencies) - `crud.py` - Database operations + orchestration - `views_api.py` - HTTP API layer ## Benefits ### Code Quality - ✅ **Testability**: Core logic can be tested without database - ✅ **Maintainability**: Clear separation makes code easier to understand - ✅ **Reusability**: Core logic can be used in different contexts - ✅ **Consistency**: Centralized accounting rules ### Developer Experience - ✅ **Type Safety**: Immutable dataclasses with proper types - ✅ **Documentation**: Well-documented core functions - ✅ **Debugging**: Easier to trace accounting logic - ✅ **Refactoring**: Safer to make changes ### Reliability - ✅ **Validation**: Comprehensive validation rules - ✅ **Correctness**: Pure functions easier to verify - ✅ **Auditability**: Clear accounting rules ## File Structure ``` lnbits/extensions/castle/ ├── core/ │ ├── __init__.py # Module exports │ ├── inventory.py # CastleInventory, CastlePosition │ ├── balance.py # BalanceCalculator │ └── validation.py # Validation functions ├── crud.py # DB operations (refactored to use core/) ├── models.py # Pydantic models ├── views_api.py # API endpoints └── PHASE3_COMPLETE.md # This file ``` ## Usage Examples ### Using CastleInventory ```python from decimal import Decimal from castle.core.inventory import CastleInventory, CastlePosition # Create inventory inv = CastleInventory() # Add positions inv.add_position(CastlePosition( currency="SATS", amount=Decimal("100000") )) inv.add_position(CastlePosition( currency="SATS", amount=Decimal("50000"), cost_currency="EUR", cost_amount=Decimal("25.00") )) # Query balances total_sats = inv.get_balance_sats() # Decimal("150000") eur_balance = inv.get_balance_fiat("EUR") # Decimal("25.00") # Export data = inv.to_dict() # {"sats": 150000, "fiat": {"EUR": 25.00}} ``` ### Using BalanceCalculator ```python from castle.core.balance import BalanceCalculator, AccountType # Calculate account balance balance = BalanceCalculator.calculate_account_balance( total_debit=100000, total_credit=50000, account_type=AccountType.ASSET ) # Returns: 50000 (debit balance for asset) # Build inventory from entry lines entry_lines = [ {"debit": 100000, "credit": 0, "metadata": '{"fiat_currency": "EUR", "fiat_amount": "50.00"}'}, {"debit": 0, "credit": 50000, "metadata": "{}"} ] inventory = BalanceCalculator.build_inventory_from_entry_lines( entry_lines, AccountType.ASSET ) # Check balance matches is_valid = BalanceCalculator.check_balance_matches( actual_balance_sats=100000, expected_balance_sats=99900, tolerance_sats=100 ) # Returns: True (within tolerance) ``` ### Using Validation ```python from castle.core.validation import validate_journal_entry, ValidationError entry = { "id": "abc123", "description": "Test entry", "entry_date": datetime.now() } entry_lines = [ {"account_id": "acc1", "debit": 100000, "credit": 0}, {"account_id": "acc2", "debit": 0, "credit": 100000} ] try: validate_journal_entry(entry, entry_lines) print("Valid!") except ValidationError as e: print(f"Invalid: {e.message}") print(f"Details: {e.details}") ``` ## Testing Checklist - [x] CastleInventory created and tested - [x] CastlePosition addition works - [x] Inventory balance calculations work - [x] BalanceCalculator account balance calculation works - [x] BalanceCalculator inventory building works - [x] BalanceCalculator user balance calculation works - [x] Validation functions work - [x] crud.py refactored to use core logic - [x] Existing balance calculations still work - [ ] Unit tests for core module (future work) ## Next Steps **Phase 4: Validation Plugins** (Medium Priority) - Create plugin system architecture - Implement `check_balanced` plugin - Implement `check_receivables` plugin - Add plugin configuration UI **Future Enhancements**: - Add unit tests for core/ module - Add integration tests - Add lot tracking to inventory - Support multi-currency in single entry - Add more validation plugins ## Conclusion Phase 3 successfully refactors Castle's accounting logic into a clean, testable core module. By following Beancount's architecture patterns, we've created: - **Pure accounting logic** separated from database concerns - **CastleInventory** for position tracking across currencies - **BalanceCalculator** for consistent balance calculations - **Comprehensive validation** for data integrity The refactoring improves code quality, maintainability, and sets the foundation for Phase 4's plugin system. **Phase 3 Status**: ✅ COMPLETE --- *Generated: 2025-10-23* *Next: Phase 4 - Validation Plugins*