From 9c577c740c872dc7f60fac1d050bba1652b6493d Mon Sep 17 00:00:00 2001
From: Padreug
Date: Tue, 5 May 2026 08:51:16 +0200
Subject: [PATCH 01/39] Add account sync button to super user toolbar
Wires the existing POST /api/v1/admin/accounts/sync endpoint into the
Castle index toolbar (sync icon between permissions and settings).
Surfaces sync stats (added/reactivated/deactivated/virtual_parents/errors)
via a Quasar notification and refreshes the accounts list on success.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
static/js/index.js | 27 +++++++++++++++++++++++++++
templates/castle/index.html | 3 +++
2 files changed, 30 insertions(+)
diff --git a/static/js/index.js b/static/js/index.js
index bd52e39..4cc5c06 100644
--- a/static/js/index.js
+++ b/static/js/index.js
@@ -34,6 +34,7 @@ window.app = Vue.createApp({
settingsLoaded: false, // Flag to prevent race conditions on toolbar buttons
castleWalletConfigured: false,
userWalletConfigured: false,
+ syncingAccounts: false,
currentExchangeRate: null, // BTC/EUR rate (sats per EUR)
expenseDialog: {
show: false,
@@ -539,6 +540,32 @@ window.app = Vue.createApp({
this.userWalletConfigured = false
}
},
+ async syncAccounts() {
+ this.syncingAccounts = true
+ try {
+ const {data} = await LNbits.api.request(
+ 'POST',
+ '/castle/api/v1/admin/accounts/sync',
+ this.g.user.wallets[0].adminkey
+ )
+ const errors = (data?.errors || []).length
+ const message = `Synced: ${data?.accounts_added ?? 0} added, ` +
+ `${data?.accounts_reactivated ?? 0} reactivated, ` +
+ `${data?.accounts_deactivated ?? 0} deactivated, ` +
+ `${data?.virtual_parents_created ?? 0} virtual parents` +
+ (errors ? `, ${errors} errors` : '')
+ this.$q.notify({
+ type: errors ? 'warning' : 'positive',
+ message,
+ timeout: errors ? 8000 : 4000
+ })
+ await this.loadAccounts()
+ } catch (error) {
+ LNbits.utils.notifyApiError(error)
+ } finally {
+ this.syncingAccounts = false
+ }
+ },
showSettingsDialog() {
this.settingsDialog.castleWalletId = this.settings?.castle_wallet_id || ''
this.settingsDialog.favaUrl = this.settings?.fava_url || 'http://localhost:3333'
diff --git a/templates/castle/index.html b/templates/castle/index.html
index 2a1e665..98b1625 100644
--- a/templates/castle/index.html
+++ b/templates/castle/index.html
@@ -24,6 +24,9 @@
Manage Permissions (Admin)
+
+ Sync Accounts from Beancount
+
Castle Settings (Super User Only)
From c174cda48d9db0ace9a26353d71e6a1aab91f3ba Mon Sep 17 00:00:00 2001
From: Padreug
Date: Tue, 5 May 2026 10:24:46 +0200
Subject: [PATCH 02/39] Rename Castle Accounting extension to Libra
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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)
---
CLAUDE.md | 78 ++--
MIGRATION_SQUASH_SUMMARY.md | 46 +-
README.md | 20 +-
__init__.py | 46 +-
account_sync.py | 70 ++--
account_utils.py | 4 +-
auth.py | 8 +-
beancount_format.py | 38 +-
config.json | 8 +-
core/__init__.py | 2 +-
core/validation.py | 4 +-
crud.py | 64 +--
description.md | 18 +-
...CCOUNT-SYNC-AND-PERMISSION-IMPROVEMENTS.md | 72 ++--
docs/ACCOUNTING-ANALYSIS-NET-SETTLEMENT.html | 40 +-
docs/ACCOUNTING-ANALYSIS-NET-SETTLEMENT.md | 40 +-
docs/BEANCOUNT_PATTERNS.md | 74 ++--
docs/BQL-BALANCE-QUERIES.md | 8 +-
docs/BQL-PRICE-NOTATION-SOLUTION.md | 6 +-
docs/DAILY_RECONCILIATION.md | 42 +-
docs/DOCUMENTATION.md | 156 +++----
docs/EXPENSE_APPROVAL.md | 10 +-
docs/PERMISSIONS-SYSTEM.md | 8 +-
docs/PHASE2_COMPLETE.md | 18 +-
docs/PHASE3_COMPLETE.md | 38 +-
docs/SATS-EQUIVALENT-METADATA.md | 24 +-
docs/UI-IMPROVEMENTS-PLAN.md | 12 +-
fava_client.py | 30 +-
helper/README.md | 26 +-
helper/import_beancount.py | 82 ++--
manifest.json | 4 +-
migrations.py | 24 +-
models.py | 24 +-
package.json | 2 +-
permission_management.py | 4 +-
services.py | 22 +-
static/image/{castle.png => libra.png} | Bin
static/js/index.js | 144 +++----
static/js/permissions.js | 38 +-
tasks.py | 64 +--
templates/{castle => libra}/index.html | 74 ++--
templates/{castle => libra}/permissions.html | 2 +-
views.py | 16 +-
views_api.py | 396 +++++++++---------
44 files changed, 953 insertions(+), 953 deletions(-)
rename static/image/{castle.png => libra.png} (100%)
rename templates/{castle => libra}/index.html (95%)
rename templates/{castle => libra}/permissions.html (99%)
diff --git a/CLAUDE.md b/CLAUDE.md
index 3086441..b58591c 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## 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.
+Libra 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
@@ -12,9 +12,9 @@ Castle Accounting is a double-entry bookkeeping extension for LNbits that enable
**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.
+**Fava/Beancount Backend**: Libra 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. Libra 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.
+**Required External Dependency**: Fava must be running as a separate service. Configure `fava_url` and `fava_ledger_slug` in Libra settings (default: `http://localhost:3333` with slug `libra-accounting`). Libra 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
@@ -44,12 +44,12 @@ Castle Accounting is a double-entry bookkeeping extension for LNbits that enable
- `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
+- `beancount_format.py` - Converts Libra 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.
+**Note**: With Fava integration, Libra 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)
@@ -57,10 +57,10 @@ Castle Accounting is a double-entry bookkeeping extension for LNbits that enable
- `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
+**extension_settings**: Libra wallet configuration (admin-only)
+- `libra_wallet_id` - The LNbits wallet used for Libra operations
- `fava_url` - Fava service URL (default: http://localhost:3333)
-- `fava_ledger_slug` - Ledger identifier in Fava (default: castle-accounting)
+- `fava_ledger_slug` - Ledger identifier in Fava (default: libra-accounting)
- `fava_timeout` - API request timeout in seconds
**user_wallet_settings**: Per-user wallet configuration
@@ -70,22 +70,22 @@ Castle Accounting is a double-entry bookkeeping extension for LNbits that enable
## Transaction Flows
### User Adds Expense (Liability)
-User pays cash for groceries, Castle owes them:
+User pays cash for groceries, Libra 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:
+### Libra Adds Receivable
+User owes Libra 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:
+Invoice generated on **Libra's wallet** (not user's). After payment:
```
DR Assets:Lightning:Balance 268,548 sats
CR Assets:Receivable:User-af983632 268,548 sats
@@ -101,14 +101,14 @@ DR Liabilities:Payable:User-af983632 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)
+- Positive = Libra owes user (LIABILITY accounts have credit balance)
+- Negative = User owes Libra (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
+- **User View**: Green = Libra owes them, Red = They owe Libra
+- **Libra Admin View**: Green = User owes Libra, Red = Libra owes user
**Balance Retrieval**: Use `GET /api/v1/balance` which queries Fava's balance sheet or account reports for accurate, Beancount-calculated balances.
@@ -127,12 +127,12 @@ DR Liabilities:Payable:User-af983632 39,669 sats
- `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/balance` - Get user balance (or Libra 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/generate-payment-invoice` - Generate invoice for user to pay Libra
+- `POST /api/v1/record-payment` - Record Lightning payment from user to Libra
- `POST /api/v1/settle-receivable` - Manually settle receivable (cash/bank)
-- `POST /api/v1/pay-user` - Castle pays user (cash/bank/lightning)
+- `POST /api/v1/pay-user` - Libra pays user (cash/bank/lightning)
### Manual Payment Requests
- `POST /api/v1/manual-payment-requests` - User requests payment
@@ -148,8 +148,8 @@ DR Liabilities:Payable:User-af983632 39,669 sats
- `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/settings` - Get Libra settings (super user)
+- `PUT /api/v1/settings` - Update Libra settings (super user)
- `GET /api/v1/user/wallet` - Get user wallet settings
- `PUT /api/v1/user/wallet` - Update user wallet settings
@@ -213,7 +213,7 @@ entry = format_transaction(
{"account": "Liabilities:Payable:User-abc123", "amount": "-50000 SATS"}
],
tags=["groceries"],
- links=["castle-entry-123"]
+ links=["libra-entry-123"]
)
# Submit to Fava
@@ -241,15 +241,15 @@ balance_result = await client.query(
### Extension as LNbits Module
This extension follows LNbits extension structure:
-- Registered via `castle_ext` router in `__init__.py`
+- Registered via `libra_ext` router in `__init__.py`
- Static files served from `static/` directory
-- Templates in `templates/castle/`
-- Database accessed via `db = Database("ext_castle")`
+- Templates in `templates/libra/`
+- Database accessed via `db = Database("ext_libra")`
**Startup Requirements**:
-- `castle_start()` initializes Fava client on extension load
+- `libra_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
+- Fava service MUST be running before starting LNbits with Libra extension
## Common Tasks
@@ -282,7 +282,7 @@ entry = format_transaction(
{"account": "Assets:Lightning:Balance", "amount": "-50000 SATS"}
],
tags=["utilities"],
- links=["castle-tx-123"]
+ links=["libra-tx-123"]
)
client = get_fava_client()
@@ -337,24 +337,24 @@ result = await client.query(query)
### 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
+2. **Fava Service**: Must be running before starting LNbits with Libra enabled
```bash
# Install Fava
pip install fava
# Create a basic Beancount file
- touch castle-ledger.beancount
+ touch libra-ledger.beancount
# Start Fava (default: http://localhost:3333)
- fava castle-ledger.beancount
+ fava libra-ledger.beancount
```
-3. **Configure Castle Settings**: Set `fava_url` and `fava_ledger_slug` via settings API or UI
+3. **Configure Libra Settings**: Set `fava_url` and `fava_ledger_slug` via settings API or UI
-### Running Castle Extension
+### Running Libra Extension
-Castle is loaded as part of LNbits. No separate build or test commands are needed for the extension itself. Development workflow:
+Libra 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/`
+1. Modify code in `lnbits/extensions/libra/`
2. Restart LNbits
3. Extension hot-reloads are supported by LNbits in development mode
@@ -363,13 +363,13 @@ Castle is loaded as part of LNbits. No separate build or test commands are neede
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 \
+# Create expense (user owes Libra)
+curl -X POST http://localhost:5000/libra/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 \
+curl http://localhost:5000/libra/api/v1/balance \
-H "X-Api-Key: YOUR_INVOICE_KEY"
```
diff --git a/MIGRATION_SQUASH_SUMMARY.md b/MIGRATION_SQUASH_SUMMARY.md
index 8d86b9b..4b03ed0 100644
--- a/MIGRATION_SQUASH_SUMMARY.md
+++ b/MIGRATION_SQUASH_SUMMARY.md
@@ -1,11 +1,11 @@
-# Castle Migration Squash Summary
+# Libra Migration Squash Summary
**Date:** November 10, 2025
**Action:** Squashed 16 incremental migrations into a single clean initial migration
## Overview
-The Castle extension had accumulated 16 migrations (m001-m016) during development. Since the software has not been released yet, we safely squashed all migrations into a single clean `m001_initial` migration.
+The Libra extension had accumulated 16 migrations (m001-m016) during development. Since the software has not been released yet, we safely squashed all migrations into a single clean `m001_initial` migration.
## Files Changed
@@ -16,37 +16,37 @@ The Castle extension had accumulated 16 migrations (m001-m016) during developmen
The squashed migration creates **7 tables**:
-### 1. castle_accounts
+### 1. libra_accounts
- Core chart of accounts with hierarchical Beancount-style names
- Examples: "Assets:Bitcoin:Lightning", "Expenses:Food:Groceries"
- User-specific accounts: "Assets:Receivable:User-af983632"
- Includes comprehensive default account set (40+ accounts)
-### 2. castle_extension_settings
-- Castle-wide configuration
-- Stores castle_wallet_id for Lightning payments
+### 2. libra_extension_settings
+- Libra-wide configuration
+- Stores libra_wallet_id for Lightning payments
-### 3. castle_user_wallet_settings
+### 3. libra_user_wallet_settings
- Per-user wallet configuration
- Allows users to have separate wallet preferences
-### 4. castle_manual_payment_requests
-- User-submitted payment requests to Castle
+### 4. libra_manual_payment_requests
+- User-submitted payment requests to Libra
- Reviewed by admins before processing
- Includes notes field for additional context
-### 5. castle_balance_assertions
+### 5. libra_balance_assertions
- Reconciliation and balance checking at specific dates
- Multi-currency support (satoshis + fiat)
- Tolerance checking for small discrepancies
- Includes notes field for reconciliation comments
-### 6. castle_user_equity_status
+### 6. libra_user_equity_status
- Manages equity contribution eligibility
- Equity-eligible users can convert expenses to equity
- Creates dynamic user-specific equity accounts: Equity:User-{user_id}
-### 7. castle_account_permissions
+### 7. libra_account_permissions
- Granular access control for accounts
- Permission types: read, submit_expense, manage
- Supports hierarchical inheritance (parent permissions cascade)
@@ -56,10 +56,10 @@ The squashed migration creates **7 tables**:
The following tables were **intentionally NOT included** in the final schema (they were dropped in m016):
-- **castle_journal_entries** - Journal entries now managed by Fava/Beancount (external source of truth)
-- **castle_entry_lines** - Entry lines now managed by Fava/Beancount
+- **libra_journal_entries** - Journal entries now managed by Fava/Beancount (external source of truth)
+- **libra_entry_lines** - Entry lines now managed by Fava/Beancount
-Castle now uses Fava as the single source of truth for accounting data. Journal operations:
+Libra now uses Fava as the single source of truth for accounting data. Journal operations:
- **Write:** Submit to Fava via FavaClient.add_entry()
- **Read:** Query Fava via FavaClient.get_entries()
@@ -106,7 +106,7 @@ For reference, the original migration sequence (preserved in migrations_old.py.b
For new installations:
```bash
-# Castle's migration system will run m001_initial automatically
+# Libra's migration system will run m001_initial automatically
# No manual intervention needed
```
@@ -174,20 +174,20 @@ After squashing, verify the migration works:
```bash
# 1. Backup existing database (if any)
-cp castle.sqlite3 castle.sqlite3.backup
+cp libra.sqlite3 libra.sqlite3.backup
# 2. Drop and recreate database to test fresh install
-rm castle.sqlite3
+rm libra.sqlite3
# 3. Start LNbits - migration should run automatically
poetry run lnbits
# 4. Verify tables created
-sqlite3 castle.sqlite3 ".tables"
-# Should show: castle_accounts, castle_extension_settings, etc.
+sqlite3 libra.sqlite3 ".tables"
+# Should show: libra_accounts, libra_extension_settings, etc.
# 5. Verify default accounts
-sqlite3 castle.sqlite3 "SELECT COUNT(*) FROM castle_accounts;"
+sqlite3 libra.sqlite3 "SELECT COUNT(*) FROM libra_accounts;"
# Should show: 40 (default accounts)
```
@@ -200,12 +200,12 @@ If issues are discovered:
cp migrations_old.py.bak migrations.py
# Restore database
-cp castle.sqlite3.backup castle.sqlite3
+cp libra.sqlite3.backup libra.sqlite3
```
## Notes
-- This squash is safe because Castle has not been released yet
+- This squash is safe because Libra has not been released yet
- No existing production databases need migration
- Historical migrations preserved in migrations_old.py.bak
- All functionality preserved in final schema
diff --git a/README.md b/README.md
index 6174bfb..f67e15c 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,10 @@
-# Castle Accounting Extension for LNbits
+# Libra Extension for LNbits
A full-featured double-entry accounting system for collective projects, integrated with LNbits Lightning payments.
## Overview
-Castle Accounting enables collectives like co-living spaces, makerspaces, and community projects to:
+Libra enables collectives like co-living spaces, makerspaces, and community projects to:
- Track expenses and revenue with proper accounting
- Manage individual member balances
- Record contributions as equity or reimbursable expenses
@@ -17,7 +17,7 @@ This extension is designed to be installed in the `lnbits/extensions/` directory
```bash
cd lnbits/extensions/
-# Copy or clone the castle directory here
+# Copy or clone the libra directory here
```
Enable the extension through the LNbits admin interface or by adding it to your configuration.
@@ -30,7 +30,7 @@ Enable the extension through the LNbits admin interface or by adding it to your
- Choose "Liability" if you want reimbursement
- Choose "Equity" if it's a contribution
-2. **View Your Balance**: See if the Castle owes you money or vice versa
+2. **View Your Balance**: See if the Libra owes you money or vice versa
3. **Pay Outstanding Balance**: Generate a Lightning invoice to settle what you owe
@@ -54,8 +54,8 @@ Enable the extension through the LNbits admin interface or by adding it to your
### Account Types
-- **Assets**: Things the Castle owns (Cash, Bank, Accounts Receivable)
-- **Liabilities**: What the Castle owes (Accounts Payable to members)
+- **Assets**: Things the Libra owns (Cash, Bank, Accounts Receivable)
+- **Liabilities**: What the Libra owes (Accounts Payable to members)
- **Equity**: Member contributions and retained earnings
- **Revenue**: Income streams
- **Expenses**: Operating costs
@@ -63,9 +63,9 @@ Enable the extension through the LNbits admin interface or by adding it to your
### Database Schema
The extension creates three tables:
-- `castle.accounts` - Chart of accounts
-- `castle.journal_entries` - Transaction headers
-- `castle.entry_lines` - Debit/credit lines
+- `libra.accounts` - Chart of accounts
+- `libra.journal_entries` - Transaction headers
+- `libra.entry_lines` - Debit/credit lines
## API Reference
@@ -79,7 +79,7 @@ To modify this extension:
2. Add database migrations in `migrations.py`
3. Implement business logic in `crud.py`
4. Create API endpoints in `views_api.py`
-5. Update UI in `templates/castle/index.html`
+5. Update UI in `templates/libra/index.html`
## Contributing
diff --git a/__init__.py b/__init__.py
index 438d0f1..614a8ba 100644
--- a/__init__.py
+++ b/__init__.py
@@ -5,24 +5,24 @@ from loguru import logger
from .crud import db
from .tasks import wait_for_paid_invoices
-from .views import castle_generic_router
-from .views_api import castle_api_router
+from .views import libra_generic_router
+from .views_api import libra_api_router
-castle_ext: APIRouter = APIRouter(prefix="/castle", tags=["Castle"])
-castle_ext.include_router(castle_generic_router)
-castle_ext.include_router(castle_api_router)
+libra_ext: APIRouter = APIRouter(prefix="/libra", tags=["Libra"])
+libra_ext.include_router(libra_generic_router)
+libra_ext.include_router(libra_api_router)
-castle_static_files = [
+libra_static_files = [
{
- "path": "/castle/static",
- "name": "castle_static",
+ "path": "/libra/static",
+ "name": "libra_static",
}
]
scheduled_tasks: list[asyncio.Task] = []
-def castle_stop():
+def libra_stop():
"""Clean up background tasks on extension shutdown"""
for task in scheduled_tasks:
try:
@@ -31,32 +31,32 @@ def castle_stop():
logger.warning(ex)
-def castle_start():
- """Initialize Castle extension background tasks"""
+def libra_start():
+ """Initialize Libra extension background tasks"""
from lnbits.tasks import create_permanent_unique_task
from .fava_client import init_fava_client
- from .models import CastleSettings
+ from .models import LibraSettings
from .tasks import wait_for_account_sync
async def _init_fava():
"""Load saved settings from DB, fall back to defaults."""
- from .crud import db as castle_db
+ from .crud import db as libra_db
settings = None
try:
- row = await castle_db.fetchone(
+ row = await libra_db.fetchone(
"SELECT * FROM extension_settings LIMIT 1",
- model=CastleSettings,
+ model=LibraSettings,
)
if row:
settings = row
- logger.info(f"Loaded Castle settings from DB: {settings.fava_url}/{settings.fava_ledger_slug}")
+ logger.info(f"Loaded Libra settings from DB: {settings.fava_url}/{settings.fava_ledger_slug}")
except Exception as e:
logger.warning(f"Could not load settings from DB: {e}")
if not settings:
- settings = CastleSettings()
- logger.info(f"Using default Castle settings: {settings.fava_url}/{settings.fava_ledger_slug}")
+ settings = LibraSettings()
+ logger.info(f"Using default Libra settings: {settings.fava_url}/{settings.fava_ledger_slug}")
init_fava_client(
fava_url=settings.fava_url,
@@ -69,16 +69,16 @@ def castle_start():
asyncio.get_event_loop().create_task(_init_fava())
except Exception as e:
logger.error(f"Failed to initialize Fava client: {e}")
- logger.warning("Castle will not function without Fava. Please configure Fava settings.")
+ logger.warning("Libra will not function without Fava. Please configure Fava settings.")
# Start background tasks
- task = create_permanent_unique_task("ext_castle", wait_for_paid_invoices)
+ task = create_permanent_unique_task("ext_libra", wait_for_paid_invoices)
scheduled_tasks.append(task)
# Start account sync task (runs hourly)
- sync_task = create_permanent_unique_task("ext_castle_account_sync", wait_for_account_sync)
+ sync_task = create_permanent_unique_task("ext_libra_account_sync", wait_for_account_sync)
scheduled_tasks.append(sync_task)
- logger.info("Castle account sync task started (runs hourly)")
+ logger.info("Libra account sync task started (runs hourly)")
-__all__ = ["castle_ext", "castle_static_files", "db", "castle_start", "castle_stop"]
+__all__ = ["libra_ext", "libra_static_files", "db", "libra_start", "libra_stop"]
diff --git a/account_sync.py b/account_sync.py
index 95fe41c..7e875f8 100644
--- a/account_sync.py
+++ b/account_sync.py
@@ -1,11 +1,11 @@
"""
Account Synchronization Module
-Syncs accounts from Beancount (source of truth) to Castle DB (metadata store).
+Syncs accounts from Beancount (source of truth) to Libra DB (metadata store).
This implements the hybrid approach:
- Beancount owns account existence (Open directives)
-- Castle DB stores permissions and user associations
+- Libra DB stores permissions and user associations
- Background sync keeps them in sync
Related: ACCOUNTS-TABLE-REMOVAL-FEASIBILITY.md - Phase 2 implementation
@@ -89,14 +89,14 @@ def extract_user_id_from_account_name(account_name: str) -> Optional[str]:
async def sync_accounts_from_beancount(force_full_sync: bool = False) -> dict:
"""
- Sync accounts from Beancount to Castle DB.
+ Sync accounts from Beancount to Libra DB.
- This ensures Castle DB has metadata entries for all accounts that exist
+ This ensures Libra DB has metadata entries for all accounts that exist
in Beancount, enabling permissions and user associations to work properly.
New behavior (soft delete + virtual parents):
- - Accounts in Beancount but not in Castle DB: Added as active
- - Accounts in Castle DB but not in Beancount: Marked as inactive (soft delete)
+ - Accounts in Beancount but not in Libra DB: Added as active
+ - Accounts in Libra DB but not in Beancount: Marked as inactive (soft delete)
- Inactive accounts that return to Beancount: Reactivated
- Missing intermediate parents: Auto-created as virtual accounts
@@ -113,7 +113,7 @@ async def sync_accounts_from_beancount(force_full_sync: bool = False) -> dict:
dict with sync statistics:
{
"total_beancount_accounts": 150,
- "total_castle_accounts": 148,
+ "total_libra_accounts": 148,
"accounts_added": 2,
"accounts_updated": 0,
"accounts_skipped": 148,
@@ -123,7 +123,7 @@ async def sync_accounts_from_beancount(force_full_sync: bool = False) -> dict:
"errors": []
}
"""
- logger.info("Starting account sync from Beancount to Castle DB")
+ logger.info("Starting account sync from Beancount to Libra DB")
fava = get_fava_client()
@@ -134,7 +134,7 @@ async def sync_accounts_from_beancount(force_full_sync: bool = False) -> dict:
logger.error(f"Failed to fetch accounts from Beancount: {e}")
return {
"total_beancount_accounts": 0,
- "total_castle_accounts": 0,
+ "total_libra_accounts": 0,
"accounts_added": 0,
"accounts_updated": 0,
"accounts_skipped": 0,
@@ -143,16 +143,16 @@ async def sync_accounts_from_beancount(force_full_sync: bool = False) -> dict:
"errors": [str(e)],
}
- # Get all accounts from Castle DB (including inactive ones for sync)
- castle_accounts = await get_all_accounts(include_inactive=True)
+ # Get all accounts from Libra DB (including inactive ones for sync)
+ libra_accounts = await get_all_accounts(include_inactive=True)
# Build lookup maps
beancount_account_names = {acc["account"] for acc in beancount_accounts}
- castle_accounts_by_name = {acc.name: acc for acc in castle_accounts}
+ libra_accounts_by_name = {acc.name: acc for acc in libra_accounts}
stats = {
"total_beancount_accounts": len(beancount_accounts),
- "total_castle_accounts": len(castle_accounts),
+ "total_libra_accounts": len(libra_accounts),
"accounts_added": 0,
"accounts_updated": 0,
"accounts_skipped": 0,
@@ -162,15 +162,15 @@ async def sync_accounts_from_beancount(force_full_sync: bool = False) -> dict:
"errors": [],
}
- # Step 1: Sync accounts from Beancount to Castle DB
+ # Step 1: Sync accounts from Beancount to Libra DB
for bc_account in beancount_accounts:
account_name = bc_account["account"]
try:
- existing = castle_accounts_by_name.get(account_name)
+ existing = libra_accounts_by_name.get(account_name)
if existing:
- # Account exists in Castle DB
+ # Account exists in Libra DB
# Check if it needs to be reactivated
if not existing.is_active:
await update_account_is_active(existing.id, True)
@@ -181,7 +181,7 @@ async def sync_accounts_from_beancount(force_full_sync: bool = False) -> dict:
logger.debug(f"Account already active: {account_name}")
continue
- # Create new account in Castle DB
+ # Create new account in Libra DB
account_type = infer_account_type_from_name(account_name)
user_id = extract_user_id_from_account_name(account_name)
@@ -207,25 +207,25 @@ async def sync_accounts_from_beancount(force_full_sync: bool = False) -> dict:
logger.error(error_msg)
stats["errors"].append(error_msg)
- # Step 2: Mark orphaned accounts (in Castle DB but not in Beancount) as inactive
+ # Step 2: Mark orphaned accounts (in Libra DB but not in Beancount) as inactive
# SKIP virtual accounts (they're intentionally metadata-only)
- for castle_account in castle_accounts:
- if castle_account.is_virtual:
+ for libra_account in libra_accounts:
+ if libra_account.is_virtual:
# Virtual accounts are metadata-only, never deactivate them
continue
- if castle_account.name not in beancount_account_names:
+ if libra_account.name not in beancount_account_names:
# Account no longer exists in Beancount
- if castle_account.is_active:
+ if libra_account.is_active:
try:
- await update_account_is_active(castle_account.id, False)
+ await update_account_is_active(libra_account.id, False)
stats["accounts_deactivated"] += 1
logger.info(
- f"Deactivated orphaned account: {castle_account.name}"
+ f"Deactivated orphaned account: {libra_account.name}"
)
except Exception as e:
error_msg = (
- f"Failed to deactivate account {castle_account.name}: {e}"
+ f"Failed to deactivate account {libra_account.name}: {e}"
)
logger.error(error_msg)
stats["errors"].append(error_msg)
@@ -236,8 +236,8 @@ async def sync_accounts_from_beancount(force_full_sync: bool = False) -> dict:
# IMPORTANT: Re-fetch accounts from DB after Step 1 added new accounts
# Otherwise we'll be checking against stale data and miss newly synced children
- current_castle_accounts = await get_all_accounts(include_inactive=True)
- all_account_names = {acc.name for acc in current_castle_accounts}
+ current_libra_accounts = await get_all_accounts(include_inactive=True)
+ all_account_names = {acc.name for acc in current_libra_accounts}
for bc_account in beancount_accounts:
account_name = bc_account["account"]
@@ -287,9 +287,9 @@ async def sync_accounts_from_beancount(force_full_sync: bool = False) -> dict:
async def sync_single_account_from_beancount(account_name: str) -> bool:
"""
- Sync a single account from Beancount to Castle DB.
+ Sync a single account from Beancount to Libra DB.
- Useful for ensuring a specific account exists in Castle DB before
+ Useful for ensuring a specific account exists in Libra DB before
granting permissions on it.
Args:
@@ -318,7 +318,7 @@ async def sync_single_account_from_beancount(account_name: str) -> bool:
logger.error(f"Account not found in Beancount: {account_name}")
return False
- # Create in Castle DB
+ # Create in Libra DB
account_type = infer_account_type_from_name(account_name)
user_id = extract_user_id_from_account_name(account_name)
@@ -343,9 +343,9 @@ async def sync_single_account_from_beancount(account_name: str) -> bool:
return False
-async def ensure_account_exists_in_castle(account_name: str) -> bool:
+async def ensure_account_exists_in_libra(account_name: str) -> bool:
"""
- Ensure account exists in Castle DB, creating from Beancount if needed.
+ Ensure account exists in Libra DB, creating from Beancount if needed.
This is the recommended function to call before granting permissions.
@@ -355,7 +355,7 @@ async def ensure_account_exists_in_castle(account_name: str) -> bool:
Returns:
True if account exists (or was created), False if failed
"""
- # Check Castle DB first
+ # Check Libra DB first
existing = await get_account_by_name(account_name)
if existing:
return True
@@ -367,9 +367,9 @@ async def ensure_account_exists_in_castle(account_name: str) -> bool:
# Background sync task (can be scheduled with cron or async scheduler)
async def scheduled_account_sync():
"""
- Scheduled task to sync accounts from Beancount to Castle DB.
+ Scheduled task to sync accounts from Beancount to Libra DB.
- Run this periodically (e.g., every hour) to keep Castle DB in sync with Beancount.
+ Run this periodically (e.g., every hour) to keep Libra DB in sync with Beancount.
Example with APScheduler:
from apscheduler.schedulers.asyncio import AsyncIOScheduler
diff --git a/account_utils.py b/account_utils.py
index 46db327..7e71630 100644
--- a/account_utils.py
+++ b/account_utils.py
@@ -200,11 +200,11 @@ DEFAULT_HIERARCHICAL_ACCOUNTS = [
("Assets:FixedAssets:ProductionFacility", AccountType.ASSET, "Production facilities"),
("Assets:Inventory", AccountType.ASSET, "Inventory and stock"),
("Assets:Livestock", AccountType.ASSET, "Livestock and animals"),
- ("Assets:Receivable", AccountType.ASSET, "Money owed to the Castle"),
+ ("Assets:Receivable", AccountType.ASSET, "Money owed to the Libra"),
("Assets:Tools", AccountType.ASSET, "Tools and hand equipment"),
# Liabilities
- ("Liabilities:Payable", AccountType.LIABILITY, "Money owed by the Castle"),
+ ("Liabilities:Payable", AccountType.LIABILITY, "Money owed by the Libra"),
# Equity - User equity accounts created dynamically as Equity:User-{user_id}
# No parent "Equity" account needed - hierarchy is implicit in the name
diff --git a/auth.py b/auth.py
index b729347..8cc9c17 100644
--- a/auth.py
+++ b/auth.py
@@ -1,5 +1,5 @@
"""
-Centralized Authorization Module for Castle Extension.
+Centralized Authorization Module for Libra Extension.
Provides consistent, secure authorization patterns across all endpoints.
@@ -55,9 +55,9 @@ class AuthContext:
@property
def is_admin(self) -> bool:
"""
- Check if user is a Castle admin (super user).
+ Check if user is a Libra admin (super user).
- Note: In Castle, admin = super_user. There's no separate admin concept.
+ Note: In Libra, admin = super_user. There's no separate admin concept.
"""
return self.is_super_user
@@ -130,7 +130,7 @@ async def require_super_user(
Require super user access.
Raises HTTPException 403 if not super user.
- Use for Castle admin operations.
+ Use for Libra admin operations.
"""
auth = _build_auth_context(wallet)
if not auth.is_super_user:
diff --git a/beancount_format.py b/beancount_format.py
index 74ba53c..956a4ee 100644
--- a/beancount_format.py
+++ b/beancount_format.py
@@ -1,8 +1,8 @@
"""
-Format Castle entries as Beancount transactions for Fava API.
+Format Libra entries as Beancount transactions for Fava API.
All entries submitted to Fava must follow Beancount syntax.
-This module converts Castle data models to Fava API format.
+This module converts Libra data models to Fava API format.
Key concepts:
- Amounts are strings: "200000 SATS" or "100.00 EUR"
@@ -35,8 +35,8 @@ def sanitize_link(text: str) -> str:
'Test-pending'
>>> sanitize_link("Invoice #123")
'Invoice-123'
- >>> sanitize_link("castle-abc123")
- 'castle-abc123'
+ >>> sanitize_link("libra-abc123")
+ 'libra-abc123'
"""
# Replace any character that's not alphanumeric, dash, underscore, slash, or period with a hyphen
sanitized = re.sub(r'[^A-Za-z0-9\-_/.]', '-', text)
@@ -67,7 +67,7 @@ def format_transaction(
postings: List of posting dicts (formatted by format_posting)
payee: Optional payee
tags: Optional tags (e.g., ["expense-entry", "approved"])
- links: Optional links (e.g., ["castle-abc123", "^invoice-xyz"])
+ links: Optional links (e.g., ["libra-abc123", "^invoice-xyz"])
meta: Optional transaction metadata
Returns:
@@ -93,8 +93,8 @@ def format_transaction(
)
],
tags=["expense-entry"],
- links=["castle-abc123"],
- meta={"user-id": "abc123", "source": "castle-expense-entry"}
+ links=["libra-abc123"],
+ meta={"user-id": "abc123", "source": "libra-expense-entry"}
)
"""
return {
@@ -150,7 +150,7 @@ def format_posting_with_cost(
"""
Format a posting with cost basis for Fava API.
- This is the RECOMMENDED format for all Castle transactions.
+ This is the RECOMMENDED format for all Libra transactions.
Uses Beancount's cost basis syntax to preserve exchange rates.
IMPORTANT: Beancount cost syntax uses PER-UNIT cost, not total cost.
@@ -381,7 +381,7 @@ def format_expense_entry(
# Build entry metadata
entry_meta = {
"user-id": user_id,
- "source": "castle-api",
+ "source": "libra-api",
"entry-id": entry_id
}
@@ -419,7 +419,7 @@ def format_receivable_entry(
entry_id: Optional[str] = None
) -> Dict[str, Any]:
"""
- Format a receivable entry (user owes castle).
+ Format a receivable entry (user owes libra).
Uses price notation (@@ SATS) for BQL-queryable SATS tracking.
Generates unique receivable link (^rcv-{entry_id}) for settlement tracking.
@@ -466,7 +466,7 @@ def format_receivable_entry(
entry_meta = {
"user-id": user_id,
- "source": "castle-api",
+ "source": "libra-api",
"entry-id": entry_id
}
@@ -512,7 +512,7 @@ def format_payment_entry(
amount_sats: Amount in satoshis (unsigned)
description: Payment description
entry_date: Date of payment
- is_payable: True if castle paying user (payable), False if user paying castle (receivable)
+ is_payable: True if libra paying user (payable), False if user paying libra (receivable)
fiat_currency: Optional fiat currency
fiat_amount: Optional fiat amount (unsigned)
payment_hash: Lightning payment hash
@@ -531,7 +531,7 @@ def format_payment_entry(
# Example: 908.44 EUR / 996896 SATS = 0.000911268 EUR/SAT (matches original receivable rate)
if fiat_currency and fiat_amount_abs and amount_sats_abs > 0:
if is_payable:
- # Castle paying user: DR Payable, CR Lightning
+ # Libra paying user: DR Payable, CR Lightning
postings = [
format_posting_with_cost(
account=payable_or_receivable_account,
@@ -546,7 +546,7 @@ def format_payment_entry(
)
]
else:
- # User paying castle: DR Lightning, CR Receivable
+ # User paying libra: DR Lightning, CR Receivable
postings = [
format_posting_simple(
account=payment_account,
@@ -633,7 +633,7 @@ def format_fiat_settlement_entry(
amount_sats: Equivalent amount in satoshis
description: Payment description
entry_date: Date of settlement
- is_payable: True if castle paying user (payable), False if user paying castle (receivable)
+ is_payable: True if libra paying user (payable), False if user paying libra (receivable)
payment_method: Payment method (cash, bank_transfer, check, etc.)
reference: Optional reference
settled_entry_links: List of expense/receivable links being settled (e.g., ["exp-abc123", "exp-def456"])
@@ -646,7 +646,7 @@ def format_fiat_settlement_entry(
# Build postings using price notation (@@ SATS) for BQL queryability
if is_payable:
- # Castle paying user: DR Payable, CR Cash/Bank
+ # Libra paying user: DR Payable, CR Cash/Bank
postings = [
{
"account": payable_or_receivable_account,
@@ -658,7 +658,7 @@ def format_fiat_settlement_entry(
}
]
else:
- # User paying castle: DR Cash/Bank, CR Receivable
+ # User paying libra: DR Cash/Bank, CR Receivable
postings = [
{
"account": payment_account,
@@ -815,7 +815,7 @@ def format_revenue_entry(
reference: Optional[str] = None
) -> Dict[str, Any]:
"""
- Format a revenue entry (castle receives payment directly).
+ Format a revenue entry (libra receives payment directly).
Creates a cleared transaction (flag="*") since payment was received.
@@ -869,7 +869,7 @@ def format_revenue_entry(
# Note: created-via is redundant with #revenue-entry tag
entry_meta = {
- "source": "castle-api"
+ "source": "libra-api"
}
links = []
diff --git a/config.json b/config.json
index b83f290..00c8c50 100644
--- a/config.json
+++ b/config.json
@@ -1,11 +1,11 @@
{
- "name": "Castle Accounting",
+ "name": "Libra",
"short_description": "Double-entry accounting system for collective projects",
- "tile": "/castle/static/image/castle.png",
+ "tile": "/libra/static/image/libra.png",
"contributors": [
"Your Name"
],
"hidden": false,
- "migration_module": "lnbits.extensions.castle.migrations",
- "db_name": "ext_castle"
+ "migration_module": "lnbits.extensions.libra.migrations",
+ "db_name": "ext_libra"
}
diff --git a/core/__init__.py b/core/__init__.py
index 662bb20..10c362c 100644
--- a/core/__init__.py
+++ b/core/__init__.py
@@ -1,5 +1,5 @@
"""
-Castle Core Module - Pure accounting logic separated from database operations.
+Libra Core Module - Pure accounting logic separated from database operations.
This module contains the core business logic for double-entry accounting,
following Beancount patterns for clean architecture:
diff --git a/core/validation.py b/core/validation.py
index d2372b8..c29f069 100644
--- a/core/validation.py
+++ b/core/validation.py
@@ -1,5 +1,5 @@
"""
-Validation rules for Castle accounting.
+Validation rules for Libra accounting.
Comprehensive validation following Beancount's plugin system approach,
but implemented as simple functions that can be called directly.
@@ -159,7 +159,7 @@ def validate_receivable_entry(
revenue_account_type: str
) -> None:
"""
- Validate a receivable entry (user owes castle).
+ Validate a receivable entry (user owes libra).
Args:
user_id: User ID
diff --git a/crud.py b/crud.py
index cd610b1..b2b43dd 100644
--- a/crud.py
+++ b/crud.py
@@ -14,7 +14,7 @@ from .models import (
AssertionStatus,
AssignUserRole,
BalanceAssertion,
- CastleSettings,
+ LibraSettings,
CreateAccount,
CreateAccountPermission,
CreateBalanceAssertion,
@@ -32,7 +32,7 @@ from .models import (
StoredUserWalletSettings,
UpdateRole,
UserBalance,
- UserCastleSettings,
+ UserLibraSettings,
UserEquityStatus,
UserRole,
UserWalletSettings,
@@ -49,7 +49,7 @@ from .core.validation import (
validate_payment_entry,
)
-db = Database("ext_castle")
+db = Database("ext_libra")
# ===== CACHING =====
# Cache for account and permission lookups to reduce DB queries
@@ -197,7 +197,7 @@ async def get_or_create_user_account(
Get or create a user-specific account with hierarchical naming.
This function checks if the account exists in Fava/Beancount and creates it
- if it doesn't exist. The account is also registered in Castle's database for
+ if it doesn't exist. The account is also registered in Libra's database for
metadata tracking (permissions, descriptions, etc.).
Examples:
@@ -214,7 +214,7 @@ async def get_or_create_user_account(
# Generate hierarchical account name
account_name = format_hierarchical_account_name(account_type, base_name, user_id)
- # Try to find existing account with this hierarchical name in Castle DB
+ # Try to find existing account with this hierarchical name in Libra DB
account = await db.fetchone(
"""
SELECT * FROM accounts
@@ -224,9 +224,9 @@ async def get_or_create_user_account(
Account,
)
- logger.info(f"[ACCOUNT CHECK] User {user_id[:8]}, Account: {account_name}, In Castle DB: {account is not None}")
+ logger.info(f"[ACCOUNT CHECK] User {user_id[:8]}, Account: {account_name}, In Libra DB: {account is not None}")
- # Always check/create in Fava, even if account exists in Castle DB
+ # Always check/create in Fava, even if account exists in Libra DB
# This ensures Beancount has the Open directive
fava_account_exists = False
if True: # Always check Fava
@@ -262,23 +262,23 @@ async def get_or_create_user_account(
except Exception as e:
logger.error(f"[FAVA ERROR] Could not check/create account in Fava: {e}", exc_info=True)
- # Continue anyway - account creation in Castle DB is still useful for metadata
+ # Continue anyway - account creation in Libra DB is still useful for metadata
- # Ensure account exists in Castle DB (sync from Beancount if needed)
+ # Ensure account exists in Libra DB (sync from Beancount if needed)
# This uses the account sync module for consistency
if not account:
- logger.info(f"[CASTLE DB] Syncing account from Beancount to Castle DB: {account_name}")
+ logger.info(f"[LIBRA DB] Syncing account from Beancount to Libra DB: {account_name}")
from .account_sync import sync_single_account_from_beancount
- # Sync from Beancount to Castle DB
+ # Sync from Beancount to Libra DB
created = await sync_single_account_from_beancount(account_name)
if created:
- logger.info(f"[CASTLE DB] Account synced from Beancount: {account_name}")
+ logger.info(f"[LIBRA DB] Account synced from Beancount: {account_name}")
else:
- logger.warning(f"[CASTLE DB] Failed to sync account from Beancount: {account_name}")
+ logger.warning(f"[LIBRA DB] Failed to sync account from Beancount: {account_name}")
- # Fetch the account from Castle DB
+ # Fetch the account from Libra DB
account = await db.fetchone(
"""
SELECT * FROM accounts
@@ -289,9 +289,9 @@ async def get_or_create_user_account(
)
if not account:
- logger.error(f"[CASTLE DB] Account still not found after sync: {account_name}")
- # Fallback: create directly in Castle DB if sync failed
- logger.info(f"[CASTLE DB] Creating account directly in Castle DB: {account_name}")
+ logger.error(f"[LIBRA DB] Account still not found after sync: {account_name}")
+ # Fallback: create directly in Libra DB if sync failed
+ logger.info(f"[LIBRA DB] Creating account directly in Libra DB: {account_name}")
try:
account = await create_account(
CreateAccount(
@@ -304,7 +304,7 @@ async def get_or_create_user_account(
except Exception as e:
# Handle UNIQUE constraint error - account already exists
if "UNIQUE constraint failed" in str(e) and "accounts.name" in str(e):
- logger.warning(f"[CASTLE DB] Account already exists (UNIQUE constraint), fetching by name: {account_name}")
+ logger.warning(f"[LIBRA DB] Account already exists (UNIQUE constraint), fetching by name: {account_name}")
# Fetch existing account by name only (ignore user_id in query)
account = await db.fetchone(
"""
@@ -315,10 +315,10 @@ async def get_or_create_user_account(
Account,
)
if account:
- logger.info(f"[CASTLE DB] Found existing account: {account_name} (user_id: {account.user_id})")
+ logger.info(f"[LIBRA DB] Found existing account: {account_name} (user_id: {account.user_id})")
# Update user_id if it's NULL or different
if account.user_id != user_id:
- logger.info(f"[CASTLE DB] Updating account user_id from {account.user_id} to {user_id}")
+ logger.info(f"[LIBRA DB] Updating account user_id from {account.user_id} to {user_id}")
await db.execute(
"""
UPDATE accounts
@@ -340,7 +340,7 @@ async def get_or_create_user_account(
# Re-raise if it's a different error
raise
else:
- logger.info(f"[CASTLE DB] Account already exists in Castle DB: {account_name}")
+ logger.info(f"[LIBRA DB] Account already exists in Libra DB: {account_name}")
return account
@@ -351,7 +351,7 @@ async def get_or_create_user_account(
# ===== JOURNAL ENTRY OPERATIONS (REMOVED) =====
#
# All journal entry operations have been moved to Fava/Beancount.
-# Castle no longer maintains its own journal_entries and entry_lines tables.
+# Libra no longer maintains its own journal_entries and entry_lines tables.
#
# For journal entry operations, see:
# - views_api.py: api_create_journal_entry() - writes to Fava via FavaClient
@@ -375,29 +375,29 @@ async def get_or_create_user_account(
# ===== SETTINGS =====
-async def create_castle_settings(
- user_id: str, data: CastleSettings
-) -> CastleSettings:
- settings = UserCastleSettings(**data.dict(), id=user_id)
+async def create_libra_settings(
+ user_id: str, data: LibraSettings
+) -> LibraSettings:
+ settings = UserLibraSettings(**data.dict(), id=user_id)
await db.insert("extension_settings", settings)
return settings
-async def get_castle_settings(user_id: str) -> Optional[CastleSettings]:
+async def get_libra_settings(user_id: str) -> Optional[LibraSettings]:
return await db.fetchone(
"""
SELECT * FROM extension_settings
WHERE id = :user_id
""",
{"user_id": user_id},
- CastleSettings,
+ LibraSettings,
)
-async def update_castle_settings(
- user_id: str, data: CastleSettings
-) -> CastleSettings:
- settings = UserCastleSettings(**data.dict(), id=user_id)
+async def update_libra_settings(
+ user_id: str, data: LibraSettings
+) -> LibraSettings:
+ settings = UserLibraSettings(**data.dict(), id=user_id)
await db.update("extension_settings", settings)
return settings
diff --git a/description.md b/description.md
index b3628a2..294a3b3 100644
--- a/description.md
+++ b/description.md
@@ -1,4 +1,4 @@
-# Castle Accounting
+# Libra
A comprehensive double-entry accounting system for collective projects, designed specifically for LNbits.
@@ -7,29 +7,29 @@ A comprehensive double-entry accounting system for collective projects, designed
- **Double-Entry Bookkeeping**: Full accounting system with debits and credits
- **Chart of Accounts**: Pre-configured accounts for Assets, Liabilities, Equity, Revenue, and Expenses
- **User Expense Tracking**: Members can record out-of-pocket expenses as either:
- - **Liabilities**: Castle owes them money (reimbursable)
+ - **Liabilities**: Libra owes them money (reimbursable)
- **Equity**: Their contribution to the collective
-- **Accounts Receivable**: Track what users owe the Castle (e.g., accommodation fees)
+- **Accounts Receivable**: Track what users owe the Libra (e.g., accommodation fees)
- **Revenue Tracking**: Record revenue received by the collective
-- **User Balance Dashboard**: Each user sees their balance with the Castle
+- **User Balance Dashboard**: Each user sees their balance with the Libra
- **Lightning Integration**: Generate invoices for outstanding balances
- **Transaction History**: View all accounting entries and transactions
## Use Cases
### 1. User Pays Expense Out of Pocket
-When a member buys supplies for the Castle:
+When a member buys supplies for the Libra:
- They can choose to be reimbursed (Liability)
- Or contribute it as equity (Equity)
### 2. Accounts Receivable
-When someone stays at the Castle and owes money:
+When someone stays at the Libra and owes money:
- Admin creates an AR entry (e.g., "5 nights @ 10€/night = 50€")
- User sees they owe 50€ in their dashboard
- They can generate an invoice to pay it off
### 3. Revenue Recording
-When the Castle receives revenue:
+When the Libra receives revenue:
- Record revenue with the payment method (Cash, Lightning, Bank)
- Properly categorized in the accounting system
@@ -58,8 +58,8 @@ When the Castle receives revenue:
## Getting Started
-1. Enable the Castle extension in LNbits
-2. Visit the Castle page to see your dashboard
+1. Enable the Libra extension in LNbits
+2. Visit the Libra page to see your dashboard
3. Start tracking expenses and balances!
The extension automatically creates a default chart of accounts on first run.
diff --git a/docs/ACCOUNT-SYNC-AND-PERMISSION-IMPROVEMENTS.md b/docs/ACCOUNT-SYNC-AND-PERMISSION-IMPROVEMENTS.md
index f5528f5..ea37aaa 100644
--- a/docs/ACCOUNT-SYNC-AND-PERMISSION-IMPROVEMENTS.md
+++ b/docs/ACCOUNT-SYNC-AND-PERMISSION-IMPROVEMENTS.md
@@ -8,9 +8,9 @@
## Summary
-Implemented two major improvements for Castle administration:
+Implemented two major improvements for Libra administration:
-1. **Account Synchronization** - Automatically sync accounts from Beancount → Castle DB
+1. **Account Synchronization** - Automatically sync accounts from Beancount → Libra DB
2. **Bulk Permission Management** - Tools for managing permissions at scale
**Total Implementation Time**: ~4 hours
@@ -23,24 +23,24 @@ Implemented two major improvements for Castle administration:
### Problem Solved
-**Before**: Accounts existed in both Beancount and Castle DB, with manual sync required.
-**After**: Automatic sync keeps Castle DB in sync with Beancount (source of truth).
+**Before**: Accounts existed in both Beancount and Libra DB, with manual sync required.
+**After**: Automatic sync keeps Libra DB in sync with Beancount (source of truth).
### Implementation
-**New Module**: `castle/account_sync.py`
+**New Module**: `libra/account_sync.py`
**Core Functions**:
```python
-# 1. Full sync from Beancount to Castle
+# 1. Full sync from Beancount to Libra
stats = await sync_accounts_from_beancount(force_full_sync=False)
# 2. Sync single account
success = await sync_single_account_from_beancount("Expenses:Food")
# 3. Ensure account exists (recommended before granting permissions)
-exists = await ensure_account_exists_in_castle("Expenses:Marketing")
+exists = await ensure_account_exists_in_libra("Expenses:Marketing")
# 4. Scheduled background sync (run hourly)
stats = await scheduled_account_sync()
@@ -77,7 +77,7 @@ stats = await scheduled_account_sync()
```python
# Sync all accounts from Beancount
-from castle.account_sync import sync_accounts_from_beancount
+from libra.account_sync import sync_accounts_from_beancount
stats = await sync_accounts_from_beancount()
@@ -96,11 +96,11 @@ Errors: 0
#### Before Granting Permission (Best Practice)
```python
-from castle.account_sync import ensure_account_exists_in_castle
-from castle.crud import create_account_permission
+from libra.account_sync import ensure_account_exists_in_libra
+from libra.crud import create_account_permission
-# Ensure account exists in Castle DB first
-account_exists = await ensure_account_exists_in_castle("Expenses:Marketing")
+# Ensure account exists in Libra DB first
+account_exists = await ensure_account_exists_in_libra("Expenses:Marketing")
if account_exists:
# Now safe to grant permission
@@ -116,9 +116,9 @@ if account_exists:
```python
# Add to your scheduler (cron, APScheduler, etc.)
-from castle.account_sync import scheduled_account_sync
+from libra.account_sync import scheduled_account_sync
-# Run every hour to keep Castle DB in sync
+# Run every hour to keep Libra DB in sync
scheduler.add_job(
scheduled_account_sync,
'interval',
@@ -142,7 +142,7 @@ Authorization: Bearer {admin_key}
```json
{
"total_beancount_accounts": 150,
- "total_castle_accounts": 150,
+ "total_libra_accounts": 150,
"accounts_added": 2,
"accounts_updated": 0,
"accounts_skipped": 148,
@@ -152,8 +152,8 @@ Authorization: Bearer {admin_key}
### Benefits
-1. **Beancount as Source of Truth**: Castle DB automatically reflects Beancount state
-2. **Reduced Manual Work**: No more manual account creation in Castle
+1. **Beancount as Source of Truth**: Libra DB automatically reflects Beancount state
+2. **Reduced Manual Work**: No more manual account creation in Libra
3. **Prevents Permission Errors**: Cannot grant permission on non-existent account
4. **Audit Trail**: Tracks which accounts were synced and when
5. **Safe Operations**: Continues on errors, never deletes accounts
@@ -169,7 +169,7 @@ Authorization: Bearer {admin_key}
### Implementation
-**New Module**: `castle/permission_management.py`
+**New Module**: `libra/permission_management.py`
**Core Functions**:
@@ -471,19 +471,19 @@ print(f"Permission types removed: {result['permission_types_removed']}")
# OLD: Manual permission creation (risky)
await create_account_permission(
user_id="alice",
- account_id="acc123", # What if account doesn't exist in Castle DB?
+ account_id="acc123", # What if account doesn't exist in Libra DB?
permission_type=PermissionType.SUBMIT_EXPENSE,
granted_by="admin"
)
# NEW: Safe permission creation with account sync
-from castle.account_sync import ensure_account_exists_in_castle
+from libra.account_sync import ensure_account_exists_in_libra
# Ensure account exists first
-account_exists = await ensure_account_exists_in_castle("Expenses:Marketing")
+account_exists = await ensure_account_exists_in_libra("Expenses:Marketing")
if account_exists:
- # Now safe - account guaranteed to be in Castle DB
+ # Now safe - account guaranteed to be in Libra DB
await create_account_permission(
user_id="alice",
account_id=account_id,
@@ -497,10 +497,10 @@ else:
### Scheduler Integration
```python
-# Add to your Castle extension startup
+# Add to your Libra extension startup
from apscheduler.schedulers.asyncio import AsyncIOScheduler
-from castle.account_sync import scheduled_account_sync
-from castle.permission_management import cleanup_expired_permissions
+from libra.account_sync import scheduled_account_sync
+from libra.permission_management import cleanup_expired_permissions
scheduler = AsyncIOScheduler()
@@ -610,7 +610,7 @@ async def test_copy_permissions():
async def test_onboarding_workflow():
"""Test complete onboarding workflow"""
# 1. Sync account
- await ensure_account_exists_in_castle("Expenses:Food")
+ await ensure_account_exists_in_libra("Expenses:Food")
# 2. Copy permissions from template user
result = await copy_permissions(
@@ -745,19 +745,19 @@ logger.error(f"Account sync error: {error}")
## Migration Guide
-### For Existing Castle Installations
+### For Existing Libra Installations
**Step 1: Deploy New Modules**
```bash
-# Copy new files to Castle extension
-cp account_sync.py /path/to/castle/
-cp permission_management.py /path/to/castle/
+# Copy new files to Libra extension
+cp account_sync.py /path/to/libra/
+cp permission_management.py /path/to/libra/
```
**Step 2: Initial Account Sync**
```python
# Run once to sync existing accounts
-from castle.account_sync import sync_accounts_from_beancount
+from libra.account_sync import sync_accounts_from_beancount
stats = await sync_accounts_from_beancount(force_full_sync=True)
print(f"Synced {stats['accounts_added']} accounts")
@@ -784,14 +784,14 @@ await bulk_grant_permission(...)
## Documentation Updates
**New files created**:
-- ✅ `castle/account_sync.py` (230 lines)
-- ✅ `castle/permission_management.py` (400 lines)
+- ✅ `libra/account_sync.py` (230 lines)
+- ✅ `libra/permission_management.py` (400 lines)
- ✅ `docs/PERMISSIONS-SYSTEM.md` (full permission system docs)
- ✅ `docs/ACCOUNT-SYNC-AND-PERMISSION-IMPROVEMENTS.md` (this file)
**Files to update**:
-- `castle/views_api.py` - Add new admin endpoints
-- `castle/README.md` - Document new features
+- `libra/views_api.py` - Add new admin endpoints
+- `libra/README.md` - Document new features
- `tests/` - Add comprehensive tests
---
@@ -801,7 +801,7 @@ await bulk_grant_permission(...)
### What Was Built
1. **Account Sync Module** (230 lines)
- - Automatic sync from Beancount → Castle DB
+ - Automatic sync from Beancount → Libra DB
- Type inference and user ID extraction
- Background scheduling support
diff --git a/docs/ACCOUNTING-ANALYSIS-NET-SETTLEMENT.html b/docs/ACCOUNTING-ANALYSIS-NET-SETTLEMENT.html
index d0e9bfe..6271865 100644
--- a/docs/ACCOUNTING-ANALYSIS-NET-SETTLEMENT.html
+++ b/docs/ACCOUNTING-ANALYSIS-NET-SETTLEMENT.html
@@ -195,13 +195,13 @@ id="toc-professional-assessment">Professional Assessment
Accounting
Analysis: Net Settlement Entry Pattern
Date: 2025-01-12 Prepared By:
-Senior Accounting Review Subject: Castle Extension -
+Senior Accounting Review Subject: Libra Extension -
Lightning Payment Settlement Entries Status: Technical
Review
Executive Summary
This document provides a professional accounting assessment of
-Castle’s net settlement entry pattern used for recording Lightning
+Libra’s net settlement entry pattern used for recording Lightning
Network payments that settle fiat-denominated receivables. The analysis
identifies areas where the implementation deviates from traditional
accounting best practices and provides specific recommendations for
@@ -214,7 +214,7 @@ hierarchy
Background: The Technical
Challenge
-Castle operates as a Lightning Network-integrated accounting system
+
Libra operates as a Lightning Network-integrated accounting system
for collectives (co-living spaces, makerspaces). It faces a unique
accounting challenge:
Scenario: User creates a receivable in EUR (e.g.,
@@ -223,7 +223,7 @@ accounting challenge:
Challenge: Record the payment while: 1. Clearing the
exact EUR receivable amount 2. Recording the exact satoshi amount
received 3. Handling cases where users have both receivables (owe
-Castle) and payables (Castle owes them) 4. Maintaining Beancount
+Libra) and payables (Libra owes them) 4. Maintaining Beancount
double-entry balance
Current Implementation
@@ -231,7 +231,7 @@ double-entry balance
; Step 1: Receivable Created
2025-11-12 * "room (200.00 EUR)" #receivable-entry
user-id: "375ec158"
- source: "castle-api"
+ source: "libra-api"
sats-amount: "225033"
Assets:Receivable:User-375ec158 200.00 EUR
sats-equivalent: "225033"
@@ -344,7 +344,7 @@ class="sourceCode sql">Assets:Bitcoin:Lightning 200.00 EUR
sats-received: "225033"
@@ -452,8 +452,8 @@ OR payable)
(receivable AND payable)
When Net Settlement is Appropriate:
-User owes Castle: 555.00 EUR (receivable)
-Castle owes User: 38.00 EUR (payable)
+User owes Libra: 555.00 EUR (receivable)
+Libra owes User: 38.00 EUR (payable)
Net amount due: 517.00 EUR (true settlement)
Proper three-posting entry:
Assets:Bitcoin:Lightning 565251 SATS @@ 517.00 EUR
@@ -461,8 +461,8 @@ Assets:Receivable:User -555.00 EUR
Liabilities:Payable:User 38.00 EUR
; Net: 517.00 = -555.00 + 38.00 ✓
When Two Postings Suffice:
-User owes Castle: 200.00 EUR (receivable)
-Castle owes User: 0.00 EUR (no payable)
+User owes Libra: 200.00 EUR (receivable)
+Libra owes User: 0.00 EUR (no payable)
Amount due: 200.00 EUR (simple payment)
Simpler two-posting entry:
Assets:Bitcoin:Lightning 225033 SATS @@ 200.00 EUR
@@ -515,7 +515,7 @@ positions - ❌ Requires metadata parsing for SATS balances
id="approach-3-true-net-settlement-when-both-obligations-exist">Approach
3: True Net Settlement (When Both Obligations Exist)
2025-11-12 * "Net settlement via Lightning"
- ; User owes 555 EUR, Castle owes 38 EUR, net: 517 EUR
+ ; User owes 555 EUR, Libra owes 38 EUR, net: 517 EUR
Assets:Bitcoin:Lightning 517.00 EUR
sats-received: "565251"
Assets:Receivable:User-375ec158 -555.00 EUR
@@ -570,7 +570,7 @@ Method
Decision Required: Select either position-based OR
metadata-based satoshi tracking.
Option A - Keep Metadata Approach (recommended for
-Castle):
+Libra):
# In format_net_settlement_entry()
postings = [
@@ -604,7 +604,7 @@ class="sourceCode python"> }
]
Recommendation: Choose Option A (metadata) for
-consistency with Castle’s architecture.
+consistency with Libra’s architecture.
1.3 Rename Function for
Clarity
@@ -713,7 +713,7 @@ class="sourceCode python"> payment_hash=payment_hash
)
else:
- # PAYABLE PAYMENT: Castle paying user (different flow)
+ # PAYABLE PAYMENT: Libra paying user (different flow)
return await format_payable_payment_entry(...)
Priority 3:
@@ -742,7 +742,7 @@ architectures:
Recommendation: Architecture A (EUR primary)
because: 1. Most receivables created in EUR 2. Financial reporting
requirements typically in fiat 3. Tax obligations calculated in fiat 4.
-Aligns with current Castle metadata approach
+Aligns with current Libra metadata approach
3.2
Consider Separate Ledger for Cryptocurrency Holdings
@@ -754,7 +754,7 @@ from fiat accounting
Assets:Receivable:User-375ec158 -200.00 EUR
Cryptocurrency Sub-Ledger (SATS-denominated):
2025-11-12 * "Lightning payment received"
- Assets:Bitcoin:Lightning:Castle 225033 SATS
+ Assets:Bitcoin:Lightning:Libra 225033 SATS
Assets:Bitcoin:Custody:User-375ec 225033 SATS
Benefits: - ✅ Clean separation of concerns - ✅
Cryptocurrency movements tracked independently - ✅ Fiat accounting
@@ -902,7 +902,7 @@ Entry balances
Is this “best practice” accounting?
No, this implementation deviates from traditional
accounting standards in several ways.
-Is it acceptable for Castle’s use case? Yes,
+Is it acceptable for Libra’s use case? Yes,
with modifications, it’s a reasonable pragmatic solution for a
novel problem (cryptocurrency payments of fiat debts).
Critical improvements needed: 1. ✅ Remove
@@ -912,7 +912,7 @@ Separate payment vs. settlement logic (accuracy and clarity)
The fundamental challenge: Traditional accounting
wasn’t designed for this scenario. There is no established “standard”
for recording cryptocurrency payments of fiat-denominated receivables.
-Castle’s approach is functional, but should be refined to align better
+Libra’s approach is functional, but should be refined to align better
with accounting principles where possible.
Next Steps
@@ -935,7 +935,7 @@ Characteristics of Accounting Information
- ASC 105-10-05: Substance Over Form
- Beancount Documentation:
http://furius.ca/beancount/doc/index
-- Castle Extension:
+
- Libra Extension:
docs/SATS-EQUIVALENT-METADATA.md
- BQL Analysis:
docs/BQL-BALANCE-QUERIES.md
@@ -948,6 +948,6 @@ implemented
This analysis was prepared for internal review and development
planning. It represents a professional accounting assessment of the
current implementation and should be used to guide improvements to
-Castle’s payment recording system.
+Libra’s payment recording system.