Rename Castle Accounting extension to Libra

Full identifier rename: module path lnbits.extensions.castle →
lnbits.extensions.libra, DB ext_castle → ext_libra, URL prefix
/castle/ → /libra/, manifest id castle → libra, fava ledger slug
default castle-ledger → libra-ledger, Beancount source metadata
castle-api → libra-api and link prefixes castle-{entry,tx}- →
libra-{entry,tx}-, column castle_wallet_id → libra_wallet_id, all
Python/JS/HTML identifiers (castle_ext, CastleSettings,
castle_reference, castleWalletConfigured, etc.).

Display name "Castle Accounting" → "Libra" (the scales/balance
metaphor — fits double-entry bookkeeping).

No backward compat: production hosts will be force-updated. Old
castle-prefixed Beancount metadata in existing Fava ledgers is
historical; new entries use libra-* prefixes going forward.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-05 10:24:46 +02:00
commit c174cda48d
44 changed files with 953 additions and 953 deletions

View file

@ -1,8 +1,8 @@
# Castle Accounting Extension - Comprehensive Documentation
# Libra Extension - Comprehensive Documentation
## Overview
The Castle Accounting extension for LNbits implements a double-entry bookkeeping system designed for cooperative/communal living spaces (like "castles"). It tracks financial relationships between a central entity (the Castle) and multiple users, handling both Lightning Network payments and manual/cash transactions.
The Libra extension for LNbits implements a double-entry bookkeeping system designed for cooperative/communal living spaces (like cooperatives). It tracks financial relationships between a central entity (the Libra) and multiple users, handling both Lightning Network payments and manual/cash transactions.
## Architecture
@ -19,23 +19,23 @@ The system implements traditional **double-entry bookkeeping** principles:
| Account Type | Normal Balance | Increases With | Decreases With | Purpose |
|--------------|----------------|----------------|----------------|---------|
| Asset | Debit | Debit | Credit | What Castle owns or is owed |
| Liability | Credit | Credit | Debit | What Castle owes to others |
| Asset | Debit | Debit | Credit | What Libra owns or is owed |
| Liability | Credit | Credit | Debit | What Libra owes to others |
| Equity | Credit | Credit | Debit | Member contributions, retained earnings |
| Revenue | Credit | Credit | Debit | Income earned by Castle |
| Expense | Debit | Debit | Credit | Costs incurred by Castle |
| Revenue | Credit | Credit | Debit | Income earned by Libra |
| Expense | Debit | Debit | Credit | Costs incurred by Libra |
### User-Specific Accounts
The system creates **per-user accounts** for tracking individual balances:
- `Accounts Receivable - {user_id[:8]}` (Asset) - User owes Castle
- `Accounts Payable - {user_id[:8]}` (Liability) - Castle owes User
- `Accounts Receivable - {user_id[:8]}` (Asset) - User owes Libra
- `Accounts Payable - {user_id[:8]}` (Liability) - Libra owes User
- `Member Equity - {user_id[:8]}` (Equity) - User's equity contributions
**Balance Interpretation:**
- `balance > 0` and account is Liability → Castle owes user (user is creditor)
- `balance < 0` (or positive Asset balance) → User owes Castle (user is debtor)
- `balance > 0` and account is Liability → Libra owes user (user is creditor)
- `balance < 0` (or positive Asset balance) → User owes Libra (user is debtor)
### Database Schema
@ -81,7 +81,7 @@ CREATE TABLE entry_lines (
```sql
CREATE TABLE extension_settings (
id TEXT NOT NULL PRIMARY KEY, -- Always "admin"
castle_wallet_id TEXT, -- LNbits wallet ID for Castle operations
libra_wallet_id TEXT, -- LNbits wallet ID for Libra operations
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
```
@ -129,11 +129,11 @@ Each `entry_line` can store metadata as JSON to preserve original fiat amounts:
### 1. User Adds Expense (Liability Model)
**Use Case:** User pays for groceries with cash, Castle reimburses them
**Use Case:** User pays for groceries with cash, Libra reimburses them
**User Action:** Add expense via UI
```javascript
POST /castle/api/v1/entries/expense
POST /libra/api/v1/entries/expense
{
"description": "Biocoop groceries",
"amount": 36.93,
@ -162,15 +162,15 @@ Metadata on both lines:
}
```
**Effect:** Castle owes user €36.93 (39,669 sats)
**Effect:** Libra owes user €36.93 (39,669 sats)
### 2. Castle Adds Receivable
### 2. Libra Adds Receivable
**Use Case:** User stays in a room, owes Castle for accommodation
**Use Case:** User stays in a room, owes Libra for accommodation
**Castle Admin Action:** Add receivable via UI
**Libra Admin Action:** Add receivable via UI
```javascript
POST /castle/api/v1/entries/receivable
POST /libra/api/v1/entries/receivable
{
"description": "room 5 days",
"amount": 250.0,
@ -198,7 +198,7 @@ Metadata:
}
```
**Effect:** User owes Castle €250.00 (268,548 sats)
**Effect:** User owes Libra €250.00 (268,548 sats)
### 3. User Pays with Lightning
@ -206,7 +206,7 @@ Metadata:
**Step A: Generate Invoice**
```javascript
POST /castle/api/v1/generate-payment-invoice
POST /libra/api/v1/generate-payment-invoice
{
"amount": 268548
}
@ -218,19 +218,19 @@ Returns:
"payment_hash": "...",
"payment_request": "lnbc...",
"amount": 268548,
"memo": "Payment from user af983632 to Castle",
"check_wallet_key": "castle_wallet_inkey"
"memo": "Payment from user af983632 to Libra",
"check_wallet_key": "libra_wallet_inkey"
}
```
**Note:** Invoice is generated on **Castle's wallet**, not user's wallet. User polls using `check_wallet_key`.
**Note:** Invoice is generated on **Libra's wallet**, not user's wallet. User polls using `check_wallet_key`.
**Step B: User Pays Invoice**
(External Lightning wallet or LNbits wallet)
**Step C: Record Payment**
```javascript
POST /castle/api/v1/record-payment
POST /libra/api/v1/record-payment
{
"payment_hash": "..."
}
@ -250,11 +250,11 @@ DR Lightning Balance 268,548 sats
### 4. Manual Payment Request Flow
**Use Case:** User wants Castle to pay them in cash instead of Lightning
**Use Case:** User wants Libra to pay them in cash instead of Lightning
**Step A: User Requests Payment**
```javascript
POST /castle/api/v1/manual-payment-requests
POST /libra/api/v1/manual-payment-requests
{
"amount": 39669,
"description": "Please pay me in cash for groceries"
@ -263,16 +263,16 @@ POST /castle/api/v1/manual-payment-requests
Creates `manual_payment_request` with status='pending'
**Step B: Castle Admin Reviews**
**Step B: Libra Admin Reviews**
Admin sees pending request in UI:
- User: af983632
- Amount: 39,669 sats (€36.93)
- Description: "Please pay me in cash for groceries"
**Step C: Castle Admin Approves**
**Step C: Libra Admin Approves**
```javascript
POST /castle/api/v1/manual-payment-requests/{id}/approve
POST /libra/api/v1/manual-payment-requests/{id}/approve
```
**Journal Entry Created:**
@ -285,11 +285,11 @@ DR Accounts Payable - af983632 39,669 sats
CR Lightning Balance 39,669 sats
```
**Effect:** Castle's liability to user reduced by 39,669 sats
**Effect:** Libra's liability to user reduced by 39,669 sats
**Alternative: Castle Admin Rejects**
**Alternative: Libra Admin Rejects**
```javascript
POST /castle/api/v1/manual-payment-requests/{id}/reject
POST /libra/api/v1/manual-payment-requests/{id}/reject
```
No journal entry created, request marked as 'rejected'.
@ -308,20 +308,20 @@ for account in user_accounts:
# Calculate satoshi balance
if account.account_type == AccountType.LIABILITY:
total_balance += account_balance # Positive = Castle owes user
total_balance += account_balance # Positive = Libra owes user
elif account.account_type == AccountType.ASSET:
total_balance -= account_balance # Positive asset = User owes Castle, so negative balance
total_balance -= account_balance # Positive asset = User owes Libra, so negative balance
# Calculate fiat balance from metadata
# Beancount-style: positive amount = debit, negative amount = credit
for line in account_entry_lines:
if line.metadata.fiat_currency and line.metadata.fiat_amount:
if account.account_type == AccountType.LIABILITY:
# For liabilities, negative amounts (credits) increase what castle owes
# For liabilities, negative amounts (credits) increase what libra owes
if line.amount < 0:
fiat_balances[currency] += fiat_amount # Castle owes more
fiat_balances[currency] += fiat_amount # Libra owes more
else:
fiat_balances[currency] -= fiat_amount # Castle owes less
fiat_balances[currency] -= fiat_amount # Libra owes less
elif account.account_type == AccountType.ASSET:
# For assets, positive amounts (debits) increase what user owes
if line.amount > 0:
@ -331,19 +331,19 @@ for account in user_accounts:
```
**Result:**
- `balance > 0`: Castle owes user (LIABILITY side dominates)
- `balance < 0`: User owes Castle (ASSET side dominates)
- `balance > 0`: Libra owes user (LIABILITY side dominates)
- `balance < 0`: User owes Libra (ASSET side dominates)
- `fiat_balances`: Net fiat position per currency
### Castle Balance Calculation
### Libra Balance Calculation
From `views_api.py:api_get_my_balance()` (super user):
```python
all_balances = get_all_user_balances()
total_liabilities = sum(b.balance for b in all_balances if b.balance > 0) # What Castle owes
total_receivables = sum(abs(b.balance) for b in all_balances if b.balance < 0) # What is owed to Castle
total_liabilities = sum(b.balance for b in all_balances if b.balance > 0) # What Libra owes
total_receivables = sum(abs(b.balance) for b in all_balances if b.balance < 0) # What is owed to Libra
net_balance = total_liabilities - total_receivables
# Aggregate all fiat balances
@ -354,34 +354,34 @@ for user_balance in all_balances:
```
**Result:**
- `net_balance > 0`: Castle owes users (net liability)
- `net_balance < 0`: Users owe Castle (net receivable)
- `net_balance > 0`: Libra owes users (net liability)
- `net_balance < 0`: Users owe Libra (net receivable)
## UI/UX Design
### Perspective-Based Display
The UI adapts based on whether the viewer is a regular user or Castle admin (super user):
The UI adapts based on whether the viewer is a regular user or Libra admin (super user):
#### User View
**Balance Display:**
- Green text: Castle owes them (positive balance, incoming money)
- Red text: They owe Castle (negative balance, outgoing money)
- Green text: Libra owes them (positive balance, incoming money)
- Red text: They owe Libra (negative balance, outgoing money)
**Transaction Badges:**
- Green "Receivable": Castle owes them (Accounts Payable entry)
- Red "Payable": They owe Castle (Accounts Receivable entry)
- Green "Receivable": Libra owes them (Accounts Payable entry)
- Red "Payable": They owe Libra (Accounts Receivable entry)
#### Castle Admin View (Super User)
#### Libra Admin View (Super User)
**Balance Display:**
- Red text: Castle owes users (positive balance, outgoing money)
- Green text: Users owe Castle (negative balance, incoming money)
- Red text: Libra owes users (positive balance, outgoing money)
- Green text: Users owe Libra (negative balance, incoming money)
**Transaction Badges:**
- Green "Receivable": User owes Castle (Accounts Receivable entry)
- Red "Payable": Castle owes user (Accounts Payable entry)
- Green "Receivable": User owes Libra (Accounts Receivable entry)
- Red "Payable": Libra owes user (Accounts Payable entry)
**Outstanding Balances Table:**
Shows all users with non-zero balances:
@ -411,10 +411,10 @@ Created by `m001_initial` migration:
- `cash` - Cash on hand
- `bank` - Bank Account
- `lightning` - Lightning Balance
- `accounts_receivable` - Money owed to the Castle
- `accounts_receivable` - Money owed to the Libra
### Liabilities
- `accounts_payable` - Money owed by the Castle
- `accounts_payable` - Money owed by the Libra
### Equity
- `member_equity` - Member contributions
@ -449,11 +449,11 @@ Created by `m001_initial` migration:
- `POST /api/v1/entries/revenue` - Create direct revenue entry (admin only)
### Balance & Payments
- `GET /api/v1/balance` - Get current user's balance (or Castle total if super user)
- `GET /api/v1/balance` - Get current user's balance (or Libra total if super user)
- `GET /api/v1/balance/{user_id}` - Get specific user's balance
- `GET /api/v1/balances/all` - Get all user balances (admin only, enriched with usernames)
- `POST /api/v1/generate-payment-invoice` - Generate invoice for user to pay Castle
- `POST /api/v1/record-payment` - Record Lightning payment to Castle
- `POST /api/v1/generate-payment-invoice` - Generate invoice for user to pay Libra
- `POST /api/v1/record-payment` - Record Lightning payment to Libra
### Manual Payments
- `POST /api/v1/manual-payment-requests` - User creates manual payment request
@ -463,8 +463,8 @@ Created by `m001_initial` migration:
- `POST /api/v1/manual-payment-requests/{id}/reject` - Admin rejects request
### Settings
- `GET /api/v1/settings` - Get Castle settings (super user only)
- `PUT /api/v1/settings` - Update Castle settings (super user only)
- `GET /api/v1/settings` - Get Libra settings (super user only)
- `PUT /api/v1/settings` - Update Libra settings (super user only)
- `GET /api/v1/user/wallet` - Get user's wallet settings
- `PUT /api/v1/user/wallet` - Update user's wallet settings
- `GET /api/v1/users` - Get all users with configured wallets (admin only)
@ -712,7 +712,7 @@ GET /api/v1/entries/user?start_date=2025-01-01&end_date=2025-03-31
**Add Endpoint:**
```python
@castle_api_router.get("/api/v1/export/beancount")
@libra_api_router.get("/api/v1/export/beancount")
async def export_beancount(
start_date: Optional[str] = None,
end_date: Optional[str] = None,
@ -812,7 +812,7 @@ async def export_beancount(
**UI Addition:**
Add export button to Castle admin UI:
Add export button to Libra admin UI:
```html
<q-btn color="primary" @click="exportBeancount">
Export to Beancount
@ -825,7 +825,7 @@ async exportBeancount() {
try {
const response = await LNbits.api.request(
'GET',
'/castle/api/v1/export/beancount',
'/libra/api/v1/export/beancount',
this.g.user.wallets[0].adminkey
)
@ -834,7 +834,7 @@ async exportBeancount() {
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `castle-accounting-${new Date().toISOString().split('T')[0]}.beancount`
link.download = `libra-accounting-${new Date().toISOString().split('T')[0]}.beancount`
link.click()
window.URL.revokeObjectURL(url)
@ -854,12 +854,12 @@ After export, users can verify with Beancount:
```bash
# Check file is valid
bean-check castle-accounting-2025-10-22.beancount
bean-check libra-accounting-2025-10-22.beancount
# Generate reports
bean-report castle-accounting-2025-10-22.beancount balances
bean-report castle-accounting-2025-10-22.beancount income
bean-web castle-accounting-2025-10-22.beancount
bean-report libra-accounting-2025-10-22.beancount balances
bean-report libra-accounting-2025-10-22.beancount income
bean-web libra-accounting-2025-10-22.beancount
```
## Testing Strategy
@ -891,7 +891,7 @@ bean-web castle-accounting-2025-10-22.beancount
1. **End-to-End User Flow**
- User adds expense
- Castle adds receivable
- Libra adds receivable
- User pays via Lightning
- Verify balances at each step
@ -904,7 +904,7 @@ bean-web castle-accounting-2025-10-22.beancount
3. **Multi-User Scenarios**
- Multiple users with positive balances
- Multiple users with negative balances
- Verify Castle net balance calculation
- Verify Libra net balance calculation
## Security Considerations
@ -916,12 +916,12 @@ bean-web castle-accounting-2025-10-22.beancount
2. **User Isolation**
- Users can only see their own balances and transactions
- Users cannot create receivables (only Castle admin can)
- Users cannot create receivables (only Libra admin can)
- Users cannot approve their own manual payment requests
3. **Wallet Key Requirements**
- `require_invoice_key`: Read access to user's data
- `require_admin_key`: Write access, Castle admin operations
- `require_admin_key`: Write access, Libra admin operations
### Potential Vulnerabilities
@ -959,7 +959,7 @@ bean-web castle-accounting-2025-10-22.beancount
limiter = Limiter(key_func=get_remote_address)
@limiter.limit("10/minute")
@castle_api_router.post("/api/v1/entries/expense")
@libra_api_router.post("/api/v1/entries/expense")
async def api_create_expense_entry(...):
...
```
@ -1020,7 +1020,7 @@ bean-web castle-accounting-2025-10-22.beancount
2. **Add Pagination**
```python
@castle_api_router.get("/api/v1/entries/user")
@libra_api_router.get("/api/v1/entries/user")
async def api_get_user_entries(
wallet: WalletTypeInfo = Depends(require_invoice_key),
limit: int = 100,
@ -1092,7 +1092,7 @@ bean-web castle-accounting-2025-10-22.beancount
## Migration Path for Existing Data
If Castle is already in production with the old code:
If Libra is already in production with the old code:
### Migration Script: `m005_fix_user_accounts.py`
@ -1185,7 +1185,7 @@ async def m005_fix_user_accounts(db):
## Conclusion
The Castle Accounting extension provides a solid foundation for double-entry bookkeeping in LNbits. The core accounting logic is sound, with proper debit/credit handling and user-specific account isolation.
The Libra extension provides a solid foundation for double-entry bookkeeping in LNbits. The core accounting logic is sound, with proper debit/credit handling and user-specific account isolation.
### Strengths
✅ Correct double-entry bookkeeping implementation
@ -1193,7 +1193,7 @@ The Castle Accounting extension provides a solid foundation for double-entry boo
✅ Metadata preservation for fiat amounts
✅ Lightning payment integration
✅ Manual payment workflow
✅ Perspective-based UI (user vs Castle view)
✅ Perspective-based UI (user vs Libra view)
### Immediate Action Items
1. ✅ Fix user account creation bug (COMPLETED)