# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Overview Castle Accounting is a double-entry bookkeeping extension for LNbits that enables collectives (co-living spaces, makerspaces, community projects) to track finances with proper accounting principles. It integrates Lightning Network payments with traditional accounting, supporting both cryptocurrency and fiat currency tracking. ## Architecture ### Core Design Principles **Double-Entry Accounting**: Every transaction affects at least two accounts. Debits must equal credits. Five account types: Assets, Liabilities, Equity, Revenue (Income), Expenses. **Fava/Beancount Backend**: Castle now uses [Fava](https://github.com/beancount/fava) as the primary accounting engine. Fava is a web interface for Beancount that provides a REST API for ledger operations. All accounting calculations (balance sheets, trial balances, account reports) are delegated to Fava/Beancount. Castle formats transactions as Beancount entries and submits them via Fava's API. **Required External Dependency**: Fava must be running as a separate service. Configure `fava_url` and `fava_ledger_slug` in Castle settings (default: `http://localhost:3333` with slug `castle-accounting`). Castle will not function without Fava. **Pure Functional Core**: The `core/` directory contains pure accounting logic independent of the database layer: - `core/validation.py` - Entry validation rules **Account Hierarchy**: Beancount-style hierarchical naming with `:` separators: - `Assets:Lightning:Balance` - `Assets:Receivable:User-af983632` - `Liabilities:Payable:User-af983632` - `Expenses:Food:Supplies` **Amount Format**: Recent architecture change uses string-based amounts with currency codes: - SATS amounts: `"200000 SATS"` - Fiat amounts: `"100.00 EUR"` or `"250.00 USD"` - Cost basis notation: `"200000 SATS {100.00 EUR}"` (200k sats acquired at 100 EUR) - Parsing handles both formats via `parse_amount_string()` in views_api.py **Metadata System**: Beancount metadata format stores original fiat amounts and exchange rates as key-value pairs. Critical: fiat balances are calculated by summing fiat amounts from journal entries, NOT by converting current satoshi balances. This prevents exchange rate fluctuations from affecting historical records. ### Key Files - `models.py` - Pydantic models for API I/O and data structures - `crud.py` - Database operations (create/read/update accounts, journal entries) - `views_api.py` - FastAPI endpoints for all operations - `views.py` - Web interface routing - `services.py` - Settings management layer - `migrations.py` - Database schema migrations - `tasks.py` - Background tasks (invoice payment monitoring) - `account_utils.py` - Hierarchical account naming utilities - `fava_client.py` - HTTP client for Fava REST API (add_entry, query, balance_sheet) - `beancount_format.py` - Converts Castle entries to Beancount transaction format - `core/validation.py` - Pure validation functions for accounting rules ### Database Schema **Note**: With Fava integration, Castle maintains a local cache of some data but delegates authoritative balance calculations to Beancount/Fava. **journal_entries**: Transaction headers stored locally and synced to Fava - `flag` field: `*` (cleared), `!` (pending), `#` (flagged), `x` (void) - `meta` field: JSON storing source, tags, audit info - `reference` field: Links to payment_hash, invoice numbers, etc. - Enriched with `username` field when retrieved via API (added from LNbits user data) **extension_settings**: Castle wallet configuration (admin-only) - `castle_wallet_id` - The LNbits wallet used for Castle operations - `fava_url` - Fava service URL (default: http://localhost:3333) - `fava_ledger_slug` - Ledger identifier in Fava (default: castle-accounting) - `fava_timeout` - API request timeout in seconds **user_wallet_settings**: Per-user wallet configuration **manual_payment_requests**: User requests for cash/manual payments ## Transaction Flows ### User Adds Expense (Liability) User pays cash for groceries, Castle owes them: ``` DR Expenses:Food 39,669 sats CR Liabilities:Payable:User-af983632 39,669 sats ``` Metadata preserves: `{"fiat_currency": "EUR", "fiat_amount": "36.93", "fiat_rate": "1074.192"}` ### Castle Adds Receivable User owes Castle for accommodation: ``` DR Assets:Receivable:User-af983632 268,548 sats CR Income:Accommodation 268,548 sats ``` ### User Pays with Lightning Invoice generated on **Castle's wallet** (not user's). After payment: ``` DR Assets:Lightning:Balance 268,548 sats CR Assets:Receivable:User-af983632 268,548 sats ``` ### Manual Payment Approval User requests cash payment → Admin approves → Journal entry created: ``` DR Liabilities:Payable:User-af983632 39,669 sats CR Assets:Lightning:Balance 39,669 sats ``` ## Balance Calculation Logic **User Balance** (calculated by Beancount via Fava): - Positive = Castle owes user (LIABILITY accounts have credit balance) - Negative = User owes Castle (ASSET accounts have debit balance) - Calculated by querying Fava for sum of all postings across user's accounts - Fiat balances calculated by Beancount from cost basis annotations, NOT converted from current sats **Perspective-Based UI**: - **User View**: Green = Castle owes them, Red = They owe Castle - **Castle Admin View**: Green = User owes Castle, Red = Castle owes user **Balance Retrieval**: Use `GET /api/v1/balance` which queries Fava's balance sheet or account reports for accurate, Beancount-calculated balances. ## API Endpoints ### Accounts - `GET /api/v1/accounts` - List all accounts - `POST /api/v1/accounts` - Create account (admin) - `GET /api/v1/accounts/{id}/balance` - Get account balance ### Journal Entries - `POST /api/v1/entries/expense` - User adds expense (creates liability or equity) - `POST /api/v1/entries/receivable` - Admin records what user owes (admin only) - `POST /api/v1/entries/revenue` - Admin records direct revenue (admin only) - `GET /api/v1/entries/user` - Get user's journal entries - `POST /api/v1/entries` - Create raw journal entry (admin only) ### Payments & Balances - `GET /api/v1/balance` - Get user balance (or Castle total if super user) - `GET /api/v1/balances/all` - Get all user balances (admin, enriched with usernames) - `POST /api/v1/generate-payment-invoice` - Generate invoice for user to pay Castle - `POST /api/v1/record-payment` - Record Lightning payment from user to Castle - `POST /api/v1/settle-receivable` - Manually settle receivable (cash/bank) - `POST /api/v1/pay-user` - Castle pays user (cash/bank/lightning) ### Manual Payment Requests - `POST /api/v1/manual-payment-requests` - User requests payment - `GET /api/v1/manual-payment-requests` - User's requests - `GET /api/v1/manual-payment-requests/all` - All requests (admin) - `POST /api/v1/manual-payment-requests/{id}/approve` - Approve (admin) - `POST /api/v1/manual-payment-requests/{id}/reject` - Reject (admin) ### Reconciliation - `POST /api/v1/assertions/balance` - Create balance assertion - `GET /api/v1/assertions/balance` - List balance assertions - `POST /api/v1/assertions/balance/{id}/check` - Check assertion - `POST /api/v1/tasks/daily-reconciliation` - Run daily reconciliation (admin) ### Settings - `GET /api/v1/settings` - Get Castle settings (super user) - `PUT /api/v1/settings` - Update Castle settings (super user) - `GET /api/v1/user/wallet` - Get user wallet settings - `PUT /api/v1/user/wallet` - Update user wallet settings ## Development Notes ### Testing Entry Creation When creating journal entries programmatically, use the helper endpoints: - `POST /api/v1/entries/expense` for user expenses (handles account creation automatically) - `POST /api/v1/entries/receivable` for what users owe - `POST /api/v1/entries/revenue` for direct revenue For custom entries, use `POST /api/v1/entries` with properly balanced lines. ### User Account Management User-specific accounts are created automatically with format: - Assets: `Assets:Receivable:User-{user_id[:8]}` - Liabilities: `Liabilities:Payable:User-{user_id[:8]}` - Equity: `Equity:MemberEquity:User-{user_id[:8]}` Use `get_or_create_user_account()` in crud.py to ensure consistency. ### Currency Handling **CRITICAL**: Use `Decimal` for all fiat amounts, never `float`. **New Amount String Format** (recent architecture change): - Input format: `"100.00 EUR"` or `"200000 SATS"` - Cost basis format: `"200000 SATS {100.00 EUR}"` (for recording acquisition cost) - Parse using `parse_amount_string(amount_str)` in views_api.py - Returns tuple: `(amount: Decimal, currency: str, cost_basis: Optional[tuple])` **Beancount Metadata Format**: ```python # Metadata attached to individual postings (legs of a transaction) metadata = { "fiat_currency": "EUR", "fiat_amount": "250.00", # String for precision "fiat_rate": "1074.192", # Sats per fiat unit } ``` **Important**: When creating entries to submit to Fava, use `beancount_format.format_transaction()` to ensure proper Beancount syntax. ### Fava Integration Patterns **Adding a Transaction**: ```python from .fava_client import get_fava_client from .beancount_format import format_transaction from datetime import date # Format as Beancount transaction entry = format_transaction( date_val=date.today(), flag="*", narration="Groceries purchase", postings=[ {"account": "Expenses:Food", "amount": "50000 SATS {46.50 EUR}"}, {"account": "Liabilities:Payable:User-abc123", "amount": "-50000 SATS"} ], tags=["groceries"], links=["castle-entry-123"] ) # Submit to Fava client = get_fava_client() result = await client.add_entry(entry) ``` **Querying Balances**: ```python # Query user balance from Fava balance_result = await client.query( f"SELECT sum(position) WHERE account ~ 'User-{user_id_short}'" ) ``` **Important**: Always use `sanitize_link()` from beancount_format.py when creating links to ensure Beancount compatibility (only A-Z, a-z, 0-9, -, _, /, . allowed). ### Permission Model - **Super User**: Full access (check via `wallet.wallet.user == lnbits_settings.super_user`) - **Admin Key**: Required for creating receivables, approving payments, viewing all balances - **Invoice Key**: Read access to user's own data - **Users**: Can only see/manage their own accounts and transactions ### Extension as LNbits Module This extension follows LNbits extension structure: - Registered via `castle_ext` router in `__init__.py` - Static files served from `static/` directory - Templates in `templates/castle/` - Database accessed via `db = Database("ext_castle")` **Startup Requirements**: - `castle_start()` initializes Fava client on extension load - Background task `wait_for_paid_invoices()` monitors Lightning invoice payments - Fava service MUST be running before starting LNbits with Castle extension ## Common Tasks ### Add New Account in Fava ```python from .fava_client import get_fava_client from datetime import date # Create Open directive for new account client = get_fava_client() entry = { "t": "Open", "date": str(date.today()), "account": "Expenses:Internet", "currencies": ["SATS", "EUR"] } await client.add_entry(entry) ``` ### Record Transaction to Fava ```python from .beancount_format import format_transaction entry = format_transaction( date_val=date.today(), flag="*", narration="Internet bill payment", postings=[ {"account": "Expenses:Internet", "amount": "50000 SATS {46.50 EUR}"}, {"account": "Assets:Lightning:Balance", "amount": "-50000 SATS"} ], tags=["utilities"], links=["castle-tx-123"] ) client = get_fava_client() await client.add_entry(entry) ``` ### Query User Balance from Fava ```python client = get_fava_client() # Query all accounts for a user user_short = user_id[:8] query = f"SELECT account, sum(position) WHERE account ~ 'User-{user_short}' GROUP BY account" result = await client.query(query) # Parse result to calculate net balance # (sum of all user accounts across Assets, Liabilities, Equity) ``` ## Data Integrity **Critical Invariants**: 1. Every transaction submitted to Fava MUST have balanced debits and credits (Beancount enforces this) 2. Fiat amounts tracked via cost basis notation: `"AMOUNT SATS {COST FIAT}"` 3. User accounts use `user_id` (NOT `wallet_id`) for consistency 4. All accounting calculations delegated to Beancount/Fava **Validation** is performed in `core/validation.py`: - Pure validation functions for entry correctness before submitting to Fava **Beancount String Sanitization**: - Links must match pattern: `[A-Za-z0-9\-_/.]` - Use `sanitize_link()` from beancount_format.py for all links and tags ## Recent Architecture Changes **Migration to Fava/Beancount** (2025): - Removed local balance calculation logic (now handled by Beancount) - Removed local `accounts` and `entry_lines` tables (Fava is source of truth) - Added `fava_client.py` and `beancount_format.py` modules - Changed amount format to string-based with currency codes - Username enrichment added to journal entries for UI display **Key Breaking Changes**: - All balance queries now go through Fava API - Account creation must use Fava's Open directive - Transaction format must follow Beancount syntax - Cost basis notation required for multi-currency tracking ## Development Setup ### Prerequisites 1. **LNbits**: This extension must be installed in the `lnbits/extensions/` directory 2. **Fava Service**: Must be running before starting LNbits with Castle enabled ```bash # Install Fava pip install fava # Create a basic Beancount file touch castle-ledger.beancount # Start Fava (default: http://localhost:3333) fava castle-ledger.beancount ``` 3. **Configure Castle Settings**: Set `fava_url` and `fava_ledger_slug` via settings API or UI ### Running Castle Extension Castle is loaded as part of LNbits. No separate build or test commands are needed for the extension itself. Development workflow: 1. Modify code in `lnbits/extensions/castle/` 2. Restart LNbits 3. Extension hot-reloads are supported by LNbits in development mode ### Testing Transactions Use the web UI or API endpoints to create test transactions. For API testing: ```bash # Create expense (user owes Castle) curl -X POST http://localhost:5000/castle/api/v1/entries/expense \ -H "X-Api-Key: YOUR_INVOICE_KEY" \ -d '{"description": "Test expense", "amount": "100.00 EUR", "account_name": "Expenses:Test"}' # Check user balance curl http://localhost:5000/castle/api/v1/balance \ -H "X-Api-Key: YOUR_INVOICE_KEY" ``` **Debugging Fava Connection**: Check logs for "Fava client initialized" message on startup. If missing, verify Fava is running and settings are correct. ## Related Documentation - `docs/README.md` - User-facing overview - `docs/DOCUMENTATION.md` - Comprehensive technical documentation - `docs/BEANCOUNT_PATTERNS.md` - Beancount-inspired design patterns - `docs/PHASE1_COMPLETE.md`, `PHASE2_COMPLETE.md`, `PHASE3_COMPLETE.md` - Development milestones - `docs/EXPENSE_APPROVAL.md` - Manual payment request workflow - `docs/DAILY_RECONCILIATION.md` - Automated reconciliation system