forked from aiolabs/libra
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:
parent
9c577c740c
commit
c174cda48d
44 changed files with 953 additions and 953 deletions
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue