365 lines
10 KiB
Markdown
365 lines
10 KiB
Markdown
# 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*
|