diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..6376629 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,280 @@ +# 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. + +**Pure Functional Core**: The `core/` directory contains pure accounting logic independent of the database layer: +- `core/balance.py` - Balance calculation from journal entries +- `core/inventory.py` - Multi-currency position tracking (similar to Beancount's Inventory) +- `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` + +**Metadata System**: Each `entry_line` stores JSON metadata preserving original fiat amounts. Critical: fiat balances are calculated by summing `fiat_amount` from metadata, 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 (daily reconciliation checks) +- `account_utils.py` - Hierarchical account naming utilities + +### Database Schema + +**accounts**: Chart of accounts with hierarchical names +- `user_id` field for per-user accounts (Receivable, Payable, Equity) +- Indexed on `user_id` and `account_type` + +**journal_entries**: Transaction headers +- `flag` field: `*` (cleared), `!` (pending), `#` (flagged), `x` (void) +- `meta` field: JSON storing source, tags, audit info +- `reference` field: Links to payment_hash, invoice numbers, etc. + +**entry_lines**: Individual debit/credit lines +- Always balanced (sum of debits = sum of credits per entry) +- `metadata` field stores fiat currency info as JSON +- Indexed on `journal_entry_id` and `account_id` + +**balance_assertions**: Reconciliation checkpoints (Beancount-style) +- Assert expected balance at a date +- Status: pending, passed, failed +- Used for daily reconciliation checks + +**extension_settings**: Castle wallet configuration (admin-only) + +**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**: +- Positive = Castle owes user (LIABILITY accounts have credit balance) +- Negative = User owes Castle (ASSET accounts have debit balance) +- Calculated from sum of all entry lines across user's accounts +- Fiat balances summed from metadata, NOT converted from 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 + +## 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`. Fiat amounts are stored in metadata as strings to preserve precision: +```python +from decimal import Decimal + +metadata = { + "fiat_currency": "EUR", + "fiat_amount": str(Decimal("250.00")), + "fiat_rate": str(Decimal("1074.192")), + "btc_rate": str(Decimal("0.000931")) +} +``` + +When reading: `fiat_amount = Decimal(metadata["fiat_amount"])` + +### Balance Assertions for Reconciliation + +Create balance assertions to verify accounting accuracy: +```python +await create_balance_assertion( + account_id="lightning_account_id", + expected_balance_sats=1000000, + expected_balance_fiat=Decimal("500.00"), + fiat_currency="EUR", + tolerance_sats=100 +) +``` + +Run `POST /api/v1/tasks/daily-reconciliation` to check all assertions. + +### 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")` + +## Common Tasks + +### Add New Expense Account +```python +await create_account(CreateAccount( + name="Expenses:Internet", + account_type=AccountType.EXPENSE, + description="Internet service costs" +)) +``` + +### Manually Record Cash Payment +```python +await create_journal_entry(CreateJournalEntry( + description="Cash payment for groceries", + lines=[ + CreateEntryLine(account_id=expense_account_id, debit=50000), + CreateEntryLine(account_id=cash_account_id, credit=50000) + ], + flag=JournalEntryFlag.CLEARED, + meta={"source": "manual", "payment_method": "cash"} +)) +``` + +### Check User Balance +```python +balance = await get_user_balance(user_id) +print(f"Sats: {balance.balance}") # Positive = Castle owes user +print(f"Fiat: {balance.fiat_balances}") # {"EUR": Decimal("36.93")} +``` + +### Export to Beancount (Future) +Follow patterns in `docs/BEANCOUNT_PATTERNS.md` for implementing Beancount export. Use hierarchical account names and preserve metadata in Beancount comments. + +## Data Integrity + +**Critical Invariants**: +1. Every journal entry MUST have balanced debits and credits +2. Fiat balances calculated from metadata, not from converting sats +3. User accounts use `user_id` (NOT `wallet_id`) for consistency +4. Balance assertions checked daily via background task + +**Validation** is performed in `core/validation.py`: +- `validate_journal_entry()` - Checks balance, minimum lines +- `validate_balance()` - Verifies account balance calculation +- `validate_receivable_entry()` - Ensures receivable entries are valid +- `validate_expense_entry()` - Ensures expense entries are valid + +## Known Issues & Future Work + +See `docs/DOCUMENTATION.md` for comprehensive list. Key items: +- No journal entry editing/deletion (use reversing entries) +- No date range filtering on list endpoints (hardcoded limit of 100) +- No batch operations for bulk imports +- Plugin system architecture designed but not implemented +- Beancount export endpoint not yet implemented + +## 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