From c174cda48d9db0ace9a26353d71e6a1aab91f3ba Mon Sep 17 00:00:00 2001 From: Padreug Date: Tue, 5 May 2026 10:24:46 +0200 Subject: [PATCH] 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
  1. ASC 105-10-05: Substance Over Form
  2. Beancount Documentation: http://furius.ca/beancount/doc/index
  3. -
  4. Castle Extension: +
  5. Libra Extension: docs/SATS-EQUIVALENT-METADATA.md
  6. BQL Analysis: docs/BQL-BALANCE-QUERIES.md
  7. @@ -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.

    diff --git a/docs/ACCOUNTING-ANALYSIS-NET-SETTLEMENT.md b/docs/ACCOUNTING-ANALYSIS-NET-SETTLEMENT.md index b145128..8725145 100644 --- a/docs/ACCOUNTING-ANALYSIS-NET-SETTLEMENT.md +++ b/docs/ACCOUNTING-ANALYSIS-NET-SETTLEMENT.md @@ -2,14 +2,14 @@ **Date**: 2025-01-12 **Prepared By**: Senior Accounting Review -**Subject**: Castle Extension - Lightning Payment Settlement Entries +**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 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 improvement. +This document provides a professional accounting assessment of 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 improvement. **Key Findings**: - ✅ Double-entry integrity maintained @@ -23,14 +23,14 @@ This document provides a professional accounting assessment of Castle's net sett ## Background: The Technical Challenge -Castle operates as a Lightning Network-integrated accounting system for collectives (co-living spaces, makerspaces). It faces a unique accounting challenge: +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., €200 for room rent), then pays via Lightning Network in satoshis (225,033 sats). **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) +3. Handling cases where users have both receivables (owe Libra) and payables (Libra owes them) 4. Maintaining Beancount double-entry balance --- @@ -43,7 +43,7 @@ Castle operates as a Lightning Network-integrated accounting system for collecti ; 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" @@ -187,7 +187,7 @@ Assets:Receivable:User-375ec158 -200.00 EUR ; No sats-equivalent needed here ``` -**Option B - Use EUR positions with metadata** (Castle's current approach): +**Option B - Use EUR positions with metadata** (Libra's current approach): ```beancount Assets:Bitcoin:Lightning 200.00 EUR sats-received: "225033" @@ -314,8 +314,8 @@ Assets:Receivable:User-375ec158 -200.00 EUR **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) ``` @@ -330,8 +330,8 @@ Liabilities:Payable:User 38.00 EUR **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) ``` @@ -405,7 +405,7 @@ Assets:Receivable:User -200.00 EUR ```beancount 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 @@ -469,7 +469,7 @@ if total_payable_fiat > 0: **Decision Required**: Select either position-based OR metadata-based satoshi tracking. -**Option A - Keep Metadata Approach** (recommended for Castle): +**Option A - Keep Metadata Approach** (recommended for Libra): ```python # In format_net_settlement_entry() postings = [ @@ -506,7 +506,7 @@ postings = [ ] ``` -**Recommendation**: Choose Option A (metadata) for consistency with Castle's architecture. +**Recommendation**: Choose Option A (metadata) for consistency with Libra's architecture. --- @@ -625,7 +625,7 @@ async def create_payment_entry( 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(...) ``` @@ -663,7 +663,7 @@ async def create_payment_entry( 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 +4. Aligns with current Libra metadata approach --- @@ -681,7 +681,7 @@ async def create_payment_entry( **Cryptocurrency Sub-Ledger** (SATS-denominated): ```beancount 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 ``` @@ -821,7 +821,7 @@ async def create_payment_entry( **Is this "best practice" accounting?** **No**, this implementation deviates from traditional accounting standards in several ways. -**Is it acceptable for Castle's use case?** +**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**: @@ -829,7 +829,7 @@ async def create_payment_entry( 2. ✅ Implement exchange gain/loss tracking (required for compliance) 3. ✅ 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 with accounting principles where possible. +**The fundamental challenge**: Traditional accounting wasn't designed for this scenario. There is no established "standard" for recording cryptocurrency payments of fiat-denominated receivables. Libra's approach is functional, but should be refined to align better with accounting principles where possible. ### Next Steps @@ -847,7 +847,7 @@ async def create_payment_entry( - **FASB Concept Statement No. 2**: Qualitative Characteristics of Accounting Information - **ASC 105-10-05**: Substance Over Form - **Beancount Documentation**: http://furius.ca/beancount/doc/index -- **Castle Extension**: `docs/SATS-EQUIVALENT-METADATA.md` +- **Libra Extension**: `docs/SATS-EQUIVALENT-METADATA.md` - **BQL Analysis**: `docs/BQL-BALANCE-QUERIES.md` --- @@ -858,4 +858,4 @@ async def create_payment_entry( --- -*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.* +*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 Libra's payment recording system.* diff --git a/docs/BEANCOUNT_PATTERNS.md b/docs/BEANCOUNT_PATTERNS.md index 2124c92..29b65c9 100644 --- a/docs/BEANCOUNT_PATTERNS.md +++ b/docs/BEANCOUNT_PATTERNS.md @@ -1,8 +1,8 @@ -# Beancount Patterns Analysis for Castle Extension +# Beancount Patterns Analysis for Libra Extension ## Overview -After analyzing the [Beancount repository](https://github.com/beancount/beancount), I've identified several excellent design patterns and architectural decisions that we should adopt or consider for the Castle Accounting extension. +After analyzing the [Beancount repository](https://github.com/beancount/beancount), I've identified several excellent design patterns and architectural decisions that we should adopt or consider for the Libra extension. ## Key Patterns to Adopt @@ -38,7 +38,7 @@ class Posting(NamedTuple): - More memory efficient than regular classes - Thread-safe by design -**Castle Application:** +**Libra Application:** ```python # In models.py from typing import NamedTuple, Optional @@ -109,15 +109,15 @@ def validate_commodity_directives(entries, options_map, config): return entries, errors ``` -**Castle Application:** +**Libra Application:** ```python # Create plugins/ directory -# lnbits/extensions/castle/plugins/__init__.py +# lnbits/extensions/libra/plugins/__init__.py from typing import Protocol, Tuple, List, Any -class CastlePlugin(Protocol): - """Protocol for Castle plugins""" +class LibraPlugin(Protocol): + """Protocol for Libra plugins""" def __call__( self, @@ -130,7 +130,7 @@ class CastlePlugin(Protocol): Args: entries: Journal entries to process - settings: Castle settings + settings: Libra settings config: Plugin-specific configuration Returns: @@ -212,7 +212,7 @@ class PluginManager: if plugin_file.name.startswith('_'): continue - module_name = f"castle.plugins.{plugin_file.stem}" + module_name = f"libra.plugins.{plugin_file.stem}" module = importlib.import_module(module_name) if hasattr(module, '__plugins__'): @@ -276,7 +276,7 @@ class Inventory(dict[tuple[str, Optional[Cost]], Position]): ) ``` -**Castle Application:** +**Libra Application:** ```python # core/inventory.py from decimal import Decimal @@ -284,8 +284,8 @@ from typing import Optional, Dict, Tuple from dataclasses import dataclass @dataclass(frozen=True) -class CastlePosition: - """A position in the Castle inventory""" +class LibraPosition: + """A position in the Libra inventory""" currency: str # "SATS", "EUR", "USD" amount: Decimal cost_currency: Optional[str] = None # Original currency if converted @@ -293,22 +293,22 @@ class CastlePosition: date: Optional[datetime] = None metadata: Dict[str, Any] = None -class CastleInventory: +class LibraInventory: """ Track user balances across multiple currencies with conversion tracking. - Similar to Beancount's Inventory but optimized for Castle's use case. + Similar to Beancount's Inventory but optimized for Libra's use case. """ def __init__(self): - self.positions: Dict[Tuple[str, Optional[str]], CastlePosition] = {} + self.positions: Dict[Tuple[str, Optional[str]], LibraPosition] = {} - def add_position(self, position: CastlePosition): + def add_position(self, position: LibraPosition): """Add or merge a position""" key = (position.currency, position.cost_currency) if key in self.positions: existing = self.positions[key] - self.positions[key] = CastlePosition( + self.positions[key] = LibraPosition( currency=position.currency, amount=existing.amount + position.amount, cost_currency=position.cost_currency, @@ -353,9 +353,9 @@ class CastleInventory: } # Usage in balance calculation: -async def get_user_inventory(user_id: str) -> CastleInventory: +async def get_user_inventory(user_id: str) -> LibraInventory: """Calculate user's inventory from journal entries""" - inventory = CastleInventory() + inventory = LibraInventory() user_accounts = await get_user_accounts(user_id) for account in user_accounts: @@ -369,7 +369,7 @@ async def get_user_inventory(user_id: str) -> CastleInventory: # Beancount-style: positive = debit, negative = credit # Adjust sign for cost amount based on amount direction cost_sign = 1 if line.amount > 0 else -1 - inventory.add_position(CastlePosition( + inventory.add_position(LibraPosition( currency="SATS", amount=Decimal(line.amount), cost_currency=metadata.get("fiat_currency"), @@ -397,7 +397,7 @@ Every directive has a `meta: dict[str, Any]` attribute that stores: - `lineno`: Line number - Custom metadata like tags, links, notes -**Castle Application:** +**Libra Application:** ```python class JournalEntryMeta(BaseModel): """Metadata for journal entries""" @@ -447,7 +447,7 @@ entry = await create_journal_entry( This asserts that the account balance should be exactly 268,548 sats on that date. If it's not, Beancount throws an error. -**Castle Application:** +**Libra Application:** ```python # models.py class BalanceAssertion(BaseModel): @@ -464,7 +464,7 @@ class BalanceAssertion(BaseModel): created_at: datetime # API endpoint -@castle_api_router.post("/api/v1/assertions/balance") +@libra_api_router.post("/api/v1/assertions/balance") async def create_balance_assertion( data: CreateBalanceAssertion, wallet: WalletTypeInfo = Depends(require_admin_key), @@ -554,7 +554,7 @@ Liabilities:US:CreditCard:Amex Accounts are organized hierarchically with `:` separator. -**Castle Application:** +**Libra Application:** ```python # Currently: "Accounts Receivable - af983632" # Better: "Assets:Receivable:User-af983632" @@ -617,7 +617,7 @@ def format_account_name( Flags: `*` = cleared, `!` = pending, `#` = flagged for review -**Castle Application:** +**Libra Application:** ```python # Add flag field to journal_entries class JournalEntryFlag(str, Enum): @@ -661,7 +661,7 @@ from decimal import Decimal amount = Decimal("19.99") ``` -**Castle Current Issue:** +**Libra Current Issue:** We're using `int` for satoshis (good!) but `float` for fiat amounts (bad!). **Fix:** @@ -709,10 +709,10 @@ WHERE account = 'Assets:Checking' AND date >= 2025-01-01; ``` -**Castle Application (Future):** +**Libra Application (Future):** ```python # Add query endpoint -@castle_api_router.post("/api/v1/query") +@libra_api_router.post("/api/v1/query") async def execute_query( query: str, wallet: WalletTypeInfo = Depends(require_invoice_key), @@ -756,12 +756,12 @@ beancount/ tools/ # Reporting and analysis ``` -**Castle Should Adopt:** +**Libra Should Adopt:** ``` -castle/ +libra/ core/ # NEW: Pure accounting logic __init__.py - inventory.py # CastleInventory for position tracking + inventory.py # LibraInventory for position tracking balance.py # Balance calculation logic validation.py # Entry validation (debits=credits, etc) account.py # Account hierarchy and naming @@ -805,11 +805,11 @@ def validate_entries(entries): return errors ``` -**Castle Application:** +**Libra Application:** ```python from typing import NamedTuple, Optional -class CastleError(NamedTuple): +class LibraError(NamedTuple): """Base error type""" source: dict # {'endpoint': '...', 'user_id': '...'} message: str @@ -828,7 +828,7 @@ class UnbalancedEntryError(NamedTuple): difference: int # Return errors from validation -async def validate_journal_entry(entry: CreateJournalEntry) -> list[CastleError]: +async def validate_journal_entry(entry: CreateJournalEntry) -> list[LibraError]: errors = [] # Beancount-style: sum of amounts must equal 0 @@ -870,7 +870,7 @@ async def validate_journal_entry(entry: CreateJournalEntry) -> list[CastleError] ### Phase 3: Core Logic Refactoring (Medium Priority) ✅ COMPLETE 9. ✅ Create `core/` module with pure accounting logic -10. ✅ Implement `CastleInventory` for position tracking +10. ✅ Implement `LibraInventory` for position tracking 11. ✅ Move balance calculation to `core/balance.py` 12. ✅ Add comprehensive validation in `core/validation.py` @@ -900,7 +900,7 @@ async def validate_journal_entry(entry: CreateJournalEntry) -> list[CastleError] 7. ✅ Separation of core logic from I/O 8. ✅ Comprehensive validation -**What Castle Should Adopt First:** +**What Libra Should Adopt First:** 1. **Decimal for fiat amounts** (prevent rounding errors) 2. **Meta field** (audit trail, source tracking) 3. **Flag field** (transaction status) @@ -916,7 +916,7 @@ async def validate_journal_entry(entry: CreateJournalEntry) -> list[CastleError] ## Conclusion -Beancount's architecture is battle-tested for serious accounting work. By adopting these patterns, Castle can: +Beancount's architecture is battle-tested for serious accounting work. By adopting these patterns, Libra can: - Prevent financial calculation errors (Decimal) - Support complex workflows (plugins) - Build user trust (balance assertions, audit trail) diff --git a/docs/BQL-BALANCE-QUERIES.md b/docs/BQL-BALANCE-QUERIES.md index d4997ab..45cd738 100644 --- a/docs/BQL-BALANCE-QUERIES.md +++ b/docs/BQL-BALANCE-QUERIES.md @@ -496,7 +496,7 @@ Improvement: 5-10x faster ## Test Results and Findings **Date**: November 10, 2025 -**Status**: ⚠️ **NOT FEASIBLE for Castle's Current Data Structure** +**Status**: ⚠️ **NOT FEASIBLE for Libra's Current Data Structure** ### Implementation Completed @@ -523,7 +523,7 @@ Improvement: 5-10x faster ### Root Cause: Architecture Limitation -**Current Castle Ledger Structure:** +**Current Libra Ledger Structure:** ``` Posting format: Amount: -360.00 EUR ← Position (BQL can query this) @@ -549,7 +549,7 @@ SELECT account, sum(meta["sats-equivalent"]) WHERE account ~ 'User-' ### Why Manual Aggregation is Necessary -1. **SATS are Castle's primary currency** for balance tracking +1. **SATS are Libra's primary currency** for balance tracking 2. **SATS values are in metadata**, not positions 3. **BQL has no metadata query capability** 4. **Must iterate through postings** to read `meta["sats-equivalent"]` @@ -590,7 +590,7 @@ SELECT account, sum(meta["sats-equivalent"]) WHERE account ~ 'User-' ## Future Consideration: Ledger Format Change -**If** Castle's ledger format changes to use SATS as position amounts: +**If** Libra's ledger format changes to use SATS as position amounts: ```beancount ; Current format (EUR position, SATS in metadata): diff --git a/docs/BQL-PRICE-NOTATION-SOLUTION.md b/docs/BQL-PRICE-NOTATION-SOLUTION.md index 24cd073..5a8df9b 100644 --- a/docs/BQL-PRICE-NOTATION-SOLUTION.md +++ b/docs/BQL-PRICE-NOTATION-SOLUTION.md @@ -63,7 +63,7 @@ Expenses:Food -360.00 EUR @@ 337096 SATS **Total calculation**: Exact 337,096 SATS (no rounding) **Precision**: Preserves exact SATS amount from original calculation -**Why `@@` is better for Castle:** +**Why `@@` is better for Libra:** - ✅ Preserves exact SATS amount (no rounding errors) - ✅ Matches current metadata storage exactly - ✅ Clearer intent: "this transaction equals X SATS total" @@ -124,7 +124,7 @@ GROUP BY account; ### Step 1: Run Metadata Test ```bash -cd /home/padreug/projects/castle-beancounter +cd /home/padreug/projects/libra-beancounter ./test_metadata_simple.sh ``` @@ -166,7 +166,7 @@ Add one test entry to your ledger: Then query: ```bash -curl -s "http://localhost:3333/castle-ledger/api/query" \ +curl -s "http://localhost:3333/libra-ledger/api/query" \ -G \ --data-urlencode "query_string=SELECT account, position, price_number, price_currency WHERE account ~ 'TEST'" \ | jq '.' diff --git a/docs/DAILY_RECONCILIATION.md b/docs/DAILY_RECONCILIATION.md index af38a8d..e284441 100644 --- a/docs/DAILY_RECONCILIATION.md +++ b/docs/DAILY_RECONCILIATION.md @@ -1,6 +1,6 @@ # Automated Daily Reconciliation -The Castle extension includes automated daily balance checking to ensure accounting accuracy. +The Libra extension includes automated daily balance checking to ensure accounting accuracy. ## Overview @@ -16,7 +16,7 @@ You can manually trigger the reconciliation check from the UI or via API: ### Via API ```bash -curl -X POST https://your-lnbits-instance.com/castle/api/v1/tasks/daily-reconciliation \ +curl -X POST https://your-lnbits-instance.com/libra/api/v1/tasks/daily-reconciliation \ -H "X-Api-Key: YOUR_ADMIN_KEY" ``` @@ -28,7 +28,7 @@ Add to your crontab: ```bash # Run daily at 2 AM -0 2 * * * curl -X POST http://localhost:5000/castle/api/v1/tasks/daily-reconciliation -H "X-Api-Key: YOUR_ADMIN_KEY" >> /var/log/castle-reconciliation.log 2>&1 +0 2 * * * curl -X POST http://localhost:5000/libra/api/v1/tasks/daily-reconciliation -H "X-Api-Key: YOUR_ADMIN_KEY" >> /var/log/libra-reconciliation.log 2>&1 ``` To edit crontab: @@ -38,22 +38,22 @@ crontab -e ### Option 2: Systemd Timer -Create `/etc/systemd/system/castle-reconciliation.service`: +Create `/etc/systemd/system/libra-reconciliation.service`: ```ini [Unit] -Description=Castle Daily Reconciliation Check +Description=Libra Daily Reconciliation Check After=network.target [Service] Type=oneshot User=lnbits -ExecStart=/usr/bin/curl -X POST http://localhost:5000/castle/api/v1/tasks/daily-reconciliation -H "X-Api-Key: YOUR_ADMIN_KEY" +ExecStart=/usr/bin/curl -X POST http://localhost:5000/libra/api/v1/tasks/daily-reconciliation -H "X-Api-Key: YOUR_ADMIN_KEY" ``` -Create `/etc/systemd/system/castle-reconciliation.timer`: +Create `/etc/systemd/system/libra-reconciliation.timer`: ```ini [Unit] -Description=Run Castle reconciliation daily +Description=Run Libra reconciliation daily [Timer] OnCalendar=daily @@ -66,8 +66,8 @@ WantedBy=timers.target Enable and start: ```bash -sudo systemctl enable castle-reconciliation.timer -sudo systemctl start castle-reconciliation.timer +sudo systemctl enable libra-reconciliation.timer +sudo systemctl start libra-reconciliation.timer ``` ### Option 3: Docker/Kubernetes CronJob @@ -78,7 +78,7 @@ For containerized deployments: apiVersion: batch/v1 kind: CronJob metadata: - name: castle-reconciliation + name: libra-reconciliation spec: schedule: "0 2 * * *" # Daily at 2 AM jobTemplate: @@ -91,7 +91,7 @@ spec: args: - /bin/sh - -c - - curl -X POST http://lnbits:5000/castle/api/v1/tasks/daily-reconciliation -H "X-Api-Key: ${ADMIN_KEY}" + - curl -X POST http://lnbits:5000/libra/api/v1/tasks/daily-reconciliation -H "X-Api-Key: ${ADMIN_KEY}" restartPolicy: OnFailure ``` @@ -129,7 +129,7 @@ The endpoint returns: grep CRON /var/log/syslog # View custom log (if using cron with redirect) -tail -f /var/log/castle-reconciliation.log +tail -f /var/log/libra-reconciliation.log ``` ### Success Criteria @@ -142,7 +142,7 @@ tail -f /var/log/castle-reconciliation.log If `failed > 0`: 1. Check the `failed_assertions` array for details -2. Investigate discrepancies in the Castle UI +2. Investigate discrepancies in the Libra UI 3. Review recent transactions 4. Check for data entry errors 5. Verify exchange rate conversions (for fiat) @@ -172,7 +172,7 @@ Planned features: 3. **Check network connectivity**: ```bash - curl http://localhost:5000/castle/api/v1/reconciliation/summary -H "X-Api-Key: YOUR_KEY" + curl http://localhost:5000/libra/api/v1/reconciliation/summary -H "X-Api-Key: YOUR_KEY" ``` ### Permission Denied @@ -202,31 +202,31 @@ Planned features: ```bash #!/bin/bash -# setup-castle-reconciliation.sh +# setup-libra-reconciliation.sh # Configuration LNBITS_URL="http://localhost:5000" ADMIN_KEY="your_admin_key_here" -LOG_FILE="/var/log/castle-reconciliation.log" +LOG_FILE="/var/log/libra-reconciliation.log" # Create log file touch "$LOG_FILE" chmod 644 "$LOG_FILE" # Add cron job -(crontab -l 2>/dev/null; echo "0 2 * * * curl -X POST $LNBITS_URL/castle/api/v1/tasks/daily-reconciliation -H 'X-Api-Key: $ADMIN_KEY' >> $LOG_FILE 2>&1") | crontab - +(crontab -l 2>/dev/null; echo "0 2 * * * curl -X POST $LNBITS_URL/libra/api/v1/tasks/daily-reconciliation -H 'X-Api-Key: $ADMIN_KEY' >> $LOG_FILE 2>&1") | crontab - echo "Daily reconciliation scheduled for 2 AM" echo "Logs will be written to: $LOG_FILE" # Test the endpoint echo "Running test reconciliation..." -curl -X POST "$LNBITS_URL/castle/api/v1/tasks/daily-reconciliation" \ +curl -X POST "$LNBITS_URL/libra/api/v1/tasks/daily-reconciliation" \ -H "X-Api-Key: $ADMIN_KEY" ``` Make executable and run: ```bash -chmod +x setup-castle-reconciliation.sh -./setup-castle-reconciliation.sh +chmod +x setup-libra-reconciliation.sh +./setup-libra-reconciliation.sh ``` diff --git a/docs/DOCUMENTATION.md b/docs/DOCUMENTATION.md index ac79f03..12b239b 100644 --- a/docs/DOCUMENTATION.md +++ b/docs/DOCUMENTATION.md @@ -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 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) diff --git a/docs/EXPENSE_APPROVAL.md b/docs/EXPENSE_APPROVAL.md index 3123b32..78dfbe5 100644 --- a/docs/EXPENSE_APPROVAL.md +++ b/docs/EXPENSE_APPROVAL.md @@ -2,7 +2,7 @@ ## Overview -The Castle extension now requires admin approval for all user-submitted expenses. This prevents invalid or incorrect expenses from affecting balances until they are verified by the Castle admin. +The Libra extension now requires admin approval for all user-submitted expenses. This prevents invalid or incorrect expenses from affecting balances until they are verified by the Libra admin. ## How It Works @@ -61,7 +61,7 @@ AND je.flag = '*' -- Only cleared entries ### Get Pending Entries (Admin Only) ``` -GET /castle/api/v1/entries/pending +GET /libra/api/v1/entries/pending Authorization: Admin Key Returns: list[JournalEntry] @@ -69,7 +69,7 @@ Returns: list[JournalEntry] ### Approve Expense (Admin Only) ``` -POST /castle/api/v1/entries/{entry_id}/approve +POST /libra/api/v1/entries/{entry_id}/approve Authorization: Admin Key Returns: JournalEntry (with flag='*') @@ -77,7 +77,7 @@ Returns: JournalEntry (with flag='*') ### Reject Expense (Admin Only) ``` -POST /castle/api/v1/entries/{entry_id}/reject +POST /libra/api/v1/entries/{entry_id}/reject Authorization: Admin Key Returns: JournalEntry (with flag='x') @@ -133,7 +133,7 @@ Returns: JournalEntry (with flag='x') 1. **Submit test expense as regular user** ``` - POST /castle/api/v1/entries/expense + POST /libra/api/v1/entries/expense { "description": "Test groceries", "amount": 50.00, diff --git a/docs/PERMISSIONS-SYSTEM.md b/docs/PERMISSIONS-SYSTEM.md index c3c88b7..b3f86ea 100644 --- a/docs/PERMISSIONS-SYSTEM.md +++ b/docs/PERMISSIONS-SYSTEM.md @@ -1,4 +1,4 @@ -# Castle Permissions System - Overview & Administration Guide +# Libra Permissions System - Overview & Administration Guide **Date**: November 10, 2025 **Status**: 📚 **Documentation** + 🔧 **Improvement Recommendations** @@ -7,7 +7,7 @@ ## Executive Summary -Castle implements a **granular, hierarchical permission system** that controls who can access which accounts and perform what actions. The system supports permission inheritance, making it easy to grant access to entire account hierarchies with a single permission. +Libra implements a **granular, hierarchical permission system** that controls who can access which accounts and perform what actions. The system supports permission inheritance, making it easy to grant access to entire account hierarchies with a single permission. **Key Features:** - ✅ **Three permission levels**: READ, SUBMIT_EXPENSE, MANAGE @@ -680,7 +680,7 @@ CREATE TABLE account_permissions ( expires_at TIMESTAMP, notes TEXT, - FOREIGN KEY (account_id) REFERENCES castle_accounts (id) + FOREIGN KEY (account_id) REFERENCES libra_accounts (id) ); CREATE INDEX idx_account_permissions_user_id ON account_permissions (user_id); @@ -840,7 +840,7 @@ async def test_expense_submission_without_permission(): ## Summary -The Castle permissions system is **well-designed** with strong features: +The Libra permissions system is **well-designed** with strong features: - Hierarchical inheritance reduces admin burden - Caching provides good performance - Expiration and audit trail support compliance diff --git a/docs/PHASE2_COMPLETE.md b/docs/PHASE2_COMPLETE.md index a9574a0..cc45614 100644 --- a/docs/PHASE2_COMPLETE.md +++ b/docs/PHASE2_COMPLETE.md @@ -36,7 +36,7 @@ Phase 2 of the Beancount-inspired refactor focused on **reconciliation and autom - `POST /api/v1/assertions/{id}/check` - Re-check assertion - `DELETE /api/v1/assertions/{id}` - Delete assertion -- **UI** (`templates/castle/index.html:254-378`): +- **UI** (`templates/libra/index.html:254-378`): - Balance Assertions card (super user only) - Failed assertions prominently displayed with red banner - Passed assertions in collapsible panel @@ -77,7 +77,7 @@ Phase 2 of the Beancount-inspired refactor focused on **reconciliation and autom **Purpose**: Visual dashboard for reconciliation status and quick access to reconciliation tools -**Implementation** (`templates/castle/index.html:380-499`): +**Implementation** (`templates/libra/index.html:380-499`): - **Summary Cards**: - Balance Assertions stats (total, passed, failed, pending) - Journal Entries stats (total, cleared, pending, flagged) @@ -161,7 +161,7 @@ Phase 2 of the Beancount-inspired refactor focused on **reconciliation and autom 2. `migrations.py` - Added `m007_balance_assertions` migration 3. `crud.py` - Added balance assertion CRUD operations 4. `views_api.py` - Added assertion, reconciliation, and task endpoints -5. `templates/castle/index.html` - Added assertions and reconciliation UI +5. `templates/libra/index.html` - Added assertions and reconciliation UI 6. `static/js/index.js` - Added assertion and reconciliation functionality 7. `BEANCOUNT_PATTERNS.md` - Updated roadmap to mark Phase 2 complete @@ -186,7 +186,7 @@ Phase 2 of the Beancount-inspired refactor focused on **reconciliation and autom ### Create a Balance Assertion ```bash -curl -X POST http://localhost:5000/castle/api/v1/assertions \ +curl -X POST http://localhost:5000/libra/api/v1/assertions \ -H "X-Api-Key: ADMIN_KEY" \ -H "Content-Type: application/json" \ -d '{ @@ -198,20 +198,20 @@ curl -X POST http://localhost:5000/castle/api/v1/assertions \ ### Get Reconciliation Summary ```bash -curl http://localhost:5000/castle/api/v1/reconciliation/summary \ +curl http://localhost:5000/libra/api/v1/reconciliation/summary \ -H "X-Api-Key: ADMIN_KEY" ``` ### Run Full Reconciliation ```bash -curl -X POST http://localhost:5000/castle/api/v1/reconciliation/check-all \ +curl -X POST http://localhost:5000/libra/api/v1/reconciliation/check-all \ -H "X-Api-Key: ADMIN_KEY" ``` ### Schedule Daily Reconciliation (Cron) ```bash # Add to crontab -0 2 * * * curl -X POST http://localhost:5000/castle/api/v1/tasks/daily-reconciliation -H "X-Api-Key: ADMIN_KEY" +0 2 * * * curl -X POST http://localhost:5000/libra/api/v1/tasks/daily-reconciliation -H "X-Api-Key: ADMIN_KEY" ``` ## Testing Checklist @@ -238,7 +238,7 @@ curl -X POST http://localhost:5000/castle/api/v1/reconciliation/check-all \ **Phase 3: Core Logic Refactoring (Medium Priority)** - Create `core/` module with pure accounting logic -- Implement `CastleInventory` for position tracking +- Implement `LibraInventory` for position tracking - Move balance calculation to `core/balance.py` - Add comprehensive validation in `core/validation.py` @@ -256,7 +256,7 @@ curl -X POST http://localhost:5000/castle/api/v1/reconciliation/check-all \ ## Conclusion -Phase 2 successfully implements Beancount's reconciliation philosophy in the Castle extension. With balance assertions, comprehensive reconciliation APIs, a visual dashboard, and automated daily checks, users can: +Phase 2 successfully implements Beancount's reconciliation philosophy in the Libra extension. With balance assertions, comprehensive reconciliation APIs, a visual dashboard, and automated daily checks, users can: - **Trust their data** with automated verification - **Catch errors early** through regular reconciliation diff --git a/docs/PHASE3_COMPLETE.md b/docs/PHASE3_COMPLETE.md index 1a3dbb6..b53625a 100644 --- a/docs/PHASE3_COMPLETE.md +++ b/docs/PHASE3_COMPLETE.md @@ -21,13 +21,13 @@ Phase 3 of the Beancount-inspired refactor focused on **separating business logi - Easier to audit and verify - Clear architecture -### 2. CastleInventory for Position Tracking ✅ +### 2. LibraInventory for Position Tracking ✅ **Purpose**: Track balances across multiple currencies with cost basis information (following Beancount's Inventory pattern) **Implementation** (`core/inventory.py`): -**CastlePosition** (Lines 11-84): +**LibraPosition** (Lines 11-84): - Immutable dataclass representing a single position - Tracks currency, amount, cost basis, and metadata - Supports addition and negation operations @@ -35,7 +35,7 @@ Phase 3 of the Beancount-inspired refactor focused on **separating business logi ```python @dataclass(frozen=True) -class CastlePosition: +class LibraPosition: currency: str # "SATS", "EUR", "USD" amount: Decimal cost_currency: Optional[str] = None @@ -44,7 +44,7 @@ class CastlePosition: metadata: Dict[str, Any] = field(default_factory=dict) ``` -**CastleInventory** (Lines 87-201): +**LibraInventory** (Lines 87-201): - Container for multiple positions - Positions keyed by `(currency, cost_currency)` tuple - Methods for querying balances: @@ -83,7 +83,7 @@ class AccountType(str, Enum): - Liabilities/Equity/Revenue: Credit balance (credit - debit) 2. **`build_inventory_from_entry_lines()`** (Lines 56-117): - - Build CastleInventory from journal entry lines + - Build LibraInventory from journal entry lines - Handles both sats and fiat currency tracking - Accounts for account type when determining sign @@ -123,7 +123,7 @@ class AccountType(str, Enum): - Checks both sats and fiat within tolerance 3. **`validate_receivable_entry()`** (Lines 180-199): - - Validates receivable (user owes castle) entries + - Validates receivable (user owes libra) entries - Ensures positive amount - Ensures revenue account type @@ -216,10 +216,10 @@ views_api.py → crud.py → core/ ## File Structure ``` -lnbits/extensions/castle/ +lnbits/extensions/libra/ ├── core/ │ ├── __init__.py # Module exports -│ ├── inventory.py # CastleInventory, CastlePosition +│ ├── inventory.py # LibraInventory, LibraPosition │ ├── balance.py # BalanceCalculator │ └── validation.py # Validation functions ├── crud.py # DB operations (refactored to use core/) @@ -230,22 +230,22 @@ lnbits/extensions/castle/ ## Usage Examples -### Using CastleInventory +### Using LibraInventory ```python from decimal import Decimal -from castle.core.inventory import CastleInventory, CastlePosition +from libra.core.inventory import LibraInventory, LibraPosition # Create inventory -inv = CastleInventory() +inv = LibraInventory() # Add positions -inv.add_position(CastlePosition( +inv.add_position(LibraPosition( currency="SATS", amount=Decimal("100000") )) -inv.add_position(CastlePosition( +inv.add_position(LibraPosition( currency="SATS", amount=Decimal("50000"), cost_currency="EUR", @@ -264,7 +264,7 @@ data = inv.to_dict() ### Using BalanceCalculator ```python -from castle.core.balance import BalanceCalculator, AccountType +from libra.core.balance import BalanceCalculator, AccountType # Calculate account balance balance = BalanceCalculator.calculate_account_balance( @@ -297,7 +297,7 @@ is_valid = BalanceCalculator.check_balance_matches( ### Using Validation ```python -from castle.core.validation import validate_journal_entry, ValidationError +from libra.core.validation import validate_journal_entry, ValidationError entry = { "id": "abc123", @@ -320,8 +320,8 @@ except ValidationError as e: ## Testing Checklist -- [x] CastleInventory created and tested -- [x] CastlePosition addition works +- [x] LibraInventory created and tested +- [x] LibraPosition addition works - [x] Inventory balance calculations work - [x] BalanceCalculator account balance calculation works - [x] BalanceCalculator inventory building works @@ -348,10 +348,10 @@ except ValidationError as e: ## Conclusion -Phase 3 successfully refactors Castle's accounting logic into a clean, testable core module. By following Beancount's architecture patterns, we've created: +Phase 3 successfully refactors Libra's accounting logic into a clean, testable core module. By following Beancount's architecture patterns, we've created: - **Pure accounting logic** separated from database concerns -- **CastleInventory** for position tracking across currencies +- **LibraInventory** for position tracking across currencies - **BalanceCalculator** for consistent balance calculations - **Comprehensive validation** for data integrity diff --git a/docs/SATS-EQUIVALENT-METADATA.md b/docs/SATS-EQUIVALENT-METADATA.md index 48ab36c..6a4e61f 100644 --- a/docs/SATS-EQUIVALENT-METADATA.md +++ b/docs/SATS-EQUIVALENT-METADATA.md @@ -8,21 +8,21 @@ ## Overview -The `sats-equivalent` metadata field is Castle's solution for **dual-currency tracking** in a fiat-denominated ledger. It preserves Bitcoin (satoshi) amounts alongside fiat currency amounts without violating accounting principles or creating multi-currency complexity in position balances. +The `sats-equivalent` metadata field is Libra's solution for **dual-currency tracking** in a fiat-denominated ledger. It preserves Bitcoin (satoshi) amounts alongside fiat currency amounts without violating accounting principles or creating multi-currency complexity in position balances. ### Quick Summary - **Purpose**: Track Bitcoin/Lightning amounts in a EUR-denominated ledger - **Location**: Beancount posting metadata (not position amounts) - **Format**: String containing absolute satoshi amount (e.g., `"337096"`) -- **Primary Use**: Calculate user balances in satoshis (Castle's primary currency) +- **Primary Use**: Calculate user balances in satoshis (Libra's primary currency) - **Key Principle**: Satoshis are for reference; EUR is the actual transaction currency --- ## The Problem: Dual-Currency Tracking -Castle needs to track both: +Libra needs to track both: 1. **Fiat amounts** (EUR, USD) - The actual transaction currency 2. **Bitcoin amounts** (satoshis) - The Lightning Network settlement currency @@ -34,7 +34,7 @@ Castle needs to track both: - ❌ Complicate traditional accounting reconciliation - ❌ Make fiat-based reporting difficult -**Castle's Philosophy**: Record transactions in their **actual currency**, with Bitcoin as supplementary data. +**Libra's Philosophy**: Record transactions in their **actual currency**, with Bitcoin as supplementary data. --- @@ -88,7 +88,7 @@ if fiat_currency and fiat_amount: ### Primary Use Case: User Balances -Castle's core function is tracking **who owes whom** in satoshis. The `sats-equivalent` metadata enables this. +Libra's core function is tracking **who owes whom** in satoshis. The `sats-equivalent` metadata enables this. **Flow** (`fava_client.py:220-248`): @@ -147,7 +147,7 @@ SELECT account, sum(meta["sats-equivalent"]) WHERE account ~ 'User-5987ae95' -- Error: BQL cannot access metadata ``` -### Why Castle Accepts This Trade-off +### Why Libra Accepts This Trade-off **Performance Analysis** (see `docs/BQL-BALANCE-QUERIES.md`): 1. **Caching solves the bottleneck**: 60-80% performance improvement from caching account/permission lookups @@ -196,9 +196,9 @@ See `docs/BQL-PRICE-NOTATION-SOLUTION.md` for full analysis. **User Action**: "I paid €36.93 cash for groceries" -**Castle's Internal Representation**: +**Libra's Internal Representation**: ```python -# User provides or Castle calculates: +# User provides or Libra calculates: fiat_amount = Decimal("36.93") # EUR fiat_currency = "EUR" amount_sats = 39669 # Calculated from exchange rate @@ -232,16 +232,16 @@ line = CreateEntryLine( # - Apply sign: -36.93 is negative → sats = -39669 # - Accumulate: user_balance_sats += -39669 -# Result: negative balance = Castle owes user +# Result: negative balance = Libra owes user ``` **User Balance Response**: ```json { "user_id": "5987ae95", - "balance": -39669, // Castle owes user 39,669 sats + "balance": -39669, // Libra owes user 39,669 sats "fiat_balances": { - "EUR": "-36.93" // Castle owes user €36.93 + "EUR": "-36.93" // Libra owes user €36.93 } } ``` @@ -306,7 +306,7 @@ The `sats-equivalent` is the **exact satoshi amount at transaction time**. It do ### 3. Separate Fiat and Sats Balances -Castle tracks TWO independent balances: +Libra tracks TWO independent balances: - **Satoshi balance**: Sum of `sats-equivalent` metadata (primary) - **Fiat balances**: Sum of EUR/USD position amounts (secondary) diff --git a/docs/UI-IMPROVEMENTS-PLAN.md b/docs/UI-IMPROVEMENTS-PLAN.md index 97bc9d3..ca3d828 100644 --- a/docs/UI-IMPROVEMENTS-PLAN.md +++ b/docs/UI-IMPROVEMENTS-PLAN.md @@ -1,4 +1,4 @@ -# Castle UI Improvements Plan +# Libra UI Improvements Plan **Date**: November 10, 2025 **Status**: 📋 **Planning Document** @@ -8,7 +8,7 @@ ## Overview -Enhance the Castle permissions UI to showcase new bulk permission management and account sync features, making admin tasks faster and more intuitive. +Enhance the Libra permissions UI to showcase new bulk permission management and account sync features, making admin tasks faster and more intuitive. --- @@ -230,7 +230,7 @@ Enhance the Castle permissions UI to showcase new bulk permission management and │ │ │ ⚠️ Warning: This will revoke ALL │ │ permissions for this user. They will │ -│ immediately lose access to Castle. │ +│ immediately lose access to Libra. │ │ │ │ Reason for Offboarding │ │ [Employee departure - last day] │ @@ -257,13 +257,13 @@ Enhance the Castle permissions UI to showcase new bulk permission management and ├───────────────────────────────────────────┤ │ │ │ Sync accounts from your Beancount ledger │ -│ to Castle database for permission mgmt. │ +│ to Libra database for permission mgmt. │ │ │ │ Last Sync: 2 hours ago │ │ Status: ✅ Up to date │ │ │ │ Accounts in Beancount: 150 │ -│ Accounts in Castle DB: 150 │ +│ Accounts in Libra DB: 150 │ │ │ │ Options: │ │ ☐ Force full sync (re-check all) │ @@ -509,7 +509,7 @@ permissions.html syncStatus: { lastSync: null, beancountAccounts: 0, - castleAccounts: 0, + libraAccounts: 0, status: 'idle' } } diff --git a/fava_client.py b/fava_client.py index 27e147d..572cd95 100644 --- a/fava_client.py +++ b/fava_client.py @@ -1,5 +1,5 @@ """ -Fava API client for Castle. +Fava API client for Libra. This module provides an async HTTP client for interacting with Fava's JSON API. All accounting logic is delegated to Fava/Beancount. @@ -46,7 +46,7 @@ class FavaClient: Args: fava_url: Base URL of Fava server (e.g., http://localhost:3333) - ledger_slug: URL-safe ledger identifier (e.g., castle-accounting) + ledger_slug: URL-safe ledger identifier (e.g., libra-accounting) timeout: Request timeout in seconds """ self.fava_url = fava_url.rstrip('/') @@ -169,7 +169,7 @@ class FavaClient: Args: entry: Beancount entry dict (same format as add_entry) - idempotency_key: Unique key for this operation (e.g., "castle-{uuid}" or "ln-{payment_hash}") + idempotency_key: Unique key for this operation (e.g., "libra-{uuid}" or "ln-{payment_hash}") Returns: Response from Fava if entry was created, or existing entry data if already exists @@ -289,18 +289,18 @@ class FavaClient: async def get_user_balance(self, user_id: str) -> Dict[str, Any]: """ - Get user's balance from castle's perspective. + Get user's balance from libra's perspective. Aggregates: - - Liabilities:Payable:User-{user_id} (negative = castle owes user) - - Assets:Receivable:User-{user_id} (positive = user owes castle) + - Liabilities:Payable:User-{user_id} (negative = libra owes user) + - Assets:Receivable:User-{user_id} (positive = user owes libra) Args: user_id: User ID Returns: { - "balance": int (sats, positive = user owes castle, negative = castle owes user), + "balance": int (sats, positive = user owes libra, negative = libra owes user), "fiat_balances": {"EUR": Decimal("100.50")}, "accounts": [list of account dicts with balances] } @@ -676,12 +676,12 @@ class FavaClient: Use this for efficient aggregations, filtering, and data retrieval. ⚠️ LIMITATION: BQL can only query position amounts and transaction-level data. - It CANNOT access posting metadata (like 'sats-equivalent'). For Castle's current + It CANNOT access posting metadata (like 'sats-equivalent'). For Libra's current ledger format where SATS are stored in metadata, manual aggregation is required. See: docs/BQL-BALANCE-QUERIES.md for detailed analysis and test results. - FUTURE CONSIDERATION: If Castle's ledger format changes to use SATS as position + FUTURE CONSIDERATION: If Libra's ledger format changes to use SATS as position amounts (instead of metadata), BQL could provide significant performance benefits. Args: @@ -1031,7 +1031,7 @@ class FavaClient: Get total expense contributions per user using BQL. Uses sum(weight) to aggregate all expenses each user has submitted - that created liabilities (castle owes user). + that created liabilities (libra owes user). Returns: List of user contribution summaries: @@ -1601,8 +1601,8 @@ class FavaClient: Args: user_id: User ID (first 8 characters used for account matching) - entry_type: "expense" (payables - castle owes user) or - "receivable" (user owes castle) + entry_type: "expense" (payables - libra owes user) or + "receivable" (user owes libra) Returns: List of unsettled entries with: @@ -1742,8 +1742,8 @@ class FavaClient: Args: user_id: User ID (first 8 characters used for account matching) - entry_type: "expense" (payables - castle owes user) or - "receivable" (user owes castle) + entry_type: "expense" (payables - libra owes user) or + "receivable" (user owes libra) Returns: List of unsettled entries with: @@ -1896,6 +1896,6 @@ def get_fava_client() -> FavaClient: if _fava_client is None: raise RuntimeError( "Fava client not initialized. Call init_fava_client() first. " - "Castle requires Fava for all accounting operations." + "Libra requires Fava for all accounting operations." ) return _fava_client diff --git a/helper/README.md b/helper/README.md index 648b987..c9dabaa 100644 --- a/helper/README.md +++ b/helper/README.md @@ -1,6 +1,6 @@ -# Castle Beancount Import Helper +# Libra Beancount Import Helper -Import Beancount ledger transactions into Castle accounting extension. +Import Beancount ledger transactions into Libra accounting extension. ## 📁 Files @@ -40,14 +40,14 @@ USER_MAPPINGS = { ### 3. Set API Key ```bash -export CASTLE_ADMIN_KEY="your_lnbits_admin_invoice_key" +export LIBRA_ADMIN_KEY="your_lnbits_admin_invoice_key" export LNBITS_URL="http://localhost:5000" # Optional ``` ## 📖 Usage ```bash -cd /path/to/castle/helper +cd /path/to/libra/helper # Test with dry run python import_beancount.py ledger.beancount --dry-run @@ -72,30 +72,30 @@ Your Beancount transactions must have an `Equity:` account: **Requirements:** - Every transaction must have an `Equity:` account -- Account names must match exactly what's in Castle +- Account names must match exactly what's in Libra - The name after `Equity:` must be in `USER_MAPPINGS` ## 🔄 How It Works 1. **Loads rates** from `btc_eur_rates.csv` -2. **Loads accounts** from Castle API automatically +2. **Loads accounts** from Libra API automatically 3. **Maps users** - Extracts user name from `Equity:Name` accounts 4. **Parses** Beancount transactions 5. **Converts** EUR → sats using daily rate -6. **Uploads** to Castle with metadata +6. **Uploads** to Libra with metadata ## 📊 Example Output ```bash $ python import_beancount.py ledger.beancount ====================================================================== -🏰 Beancount to Castle Import Script +🏰 Beancount to Libra Import Script ====================================================================== 📊 Loaded 15 daily rates from btc_eur_rates.csv Date range: 2025-07-01 to 2025-07-15 -🏦 Loaded 28 accounts from Castle +🏦 Loaded 28 accounts from Libra 👥 User ID mappings: - Pat → wallet_abc123 @@ -112,15 +112,15 @@ $ python import_beancount.py ledger.beancount 📊 Summary: 25 succeeded, 0 failed, 0 skipped ====================================================================== -✅ Successfully imported 25 transactions to Castle! +✅ Successfully imported 25 transactions to Libra! ``` ## ❓ Troubleshooting -### "No account found in Castle" -**Error:** `No account found in Castle with name 'Expenses:XYZ'` +### "No account found in Libra" +**Error:** `No account found in Libra with name 'Expenses:XYZ'` -**Solution:** Create the account in Castle first with that exact name. +**Solution:** Create the account in Libra first with that exact name. ### "No user ID mapping found" **Error:** `No user ID mapping found for 'Pat'` diff --git a/helper/import_beancount.py b/helper/import_beancount.py index 417d0fd..0632364 100755 --- a/helper/import_beancount.py +++ b/helper/import_beancount.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 """ -Beancount to Castle Import Script +Beancount to Libra Import Script ⚠️ NOTE: This script is for ONE-OFF MIGRATION purposes only. - Now that Castle uses Fava/Beancount as the single source of truth, - the data flow is: Castle → Fava/Beancount (not the reverse). + Now that Libra uses Fava/Beancount as the single source of truth, + the data flow is: Libra → Fava/Beancount (not the reverse). This script was used for initial data import from existing Beancount files. @@ -14,7 +14,7 @@ Beancount to Castle Import Script - REPURPOSE for bidirectional sync if that becomes a requirement - ARCHIVE to misc-docs/old-helpers/ if keeping for reference -Imports Beancount ledger transactions into Castle accounting extension. +Imports Beancount ledger transactions into Libra accounting extension. Reads daily BTC/EUR rates from btc_eur_rates.csv in the same directory. Usage: @@ -35,14 +35,14 @@ from typing import Dict, Optional # LNbits URL and API Key LNBITS_URL = os.environ.get("LNBITS_URL", "http://localhost:5000") -ADMIN_API_KEY = os.environ.get("CASTLE_ADMIN_KEY", "48d787d862484a6c89d6a557b4d5be9d") +ADMIN_API_KEY = os.environ.get("LIBRA_ADMIN_KEY", "48d787d862484a6c89d6a557b4d5be9d") # Rates CSV file (looks in same directory as this script) SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) RATES_CSV_FILE = os.path.join(SCRIPT_DIR, "btc_eur_rates.csv") -# User ID mappings: Equity account name -> Castle user ID (wallet ID) -# TODO: Update these with your actual Castle user/wallet IDs +# User ID mappings: Equity account name -> Libra user ID (wallet ID) +# TODO: Update these with your actual Libra user/wallet IDs USER_MAPPINGS = { "Pat": "75be145a42884b22b60bf97510ed46e3", "Coco": "375ec158ceca46de86cf6561ca20f881", @@ -116,7 +116,7 @@ class RateLookup: # ===== ACCOUNT LOOKUP ===== class AccountLookup: - """Fetch and lookup Castle accounts from API""" + """Fetch and lookup Libra accounts from API""" def __init__(self, lnbits_url: str, api_key: str): self.accounts = {} # name -> account_id @@ -125,8 +125,8 @@ class AccountLookup: self._fetch_accounts(lnbits_url, api_key) def _fetch_accounts(self, lnbits_url: str, api_key: str): - """Fetch all accounts from Castle API""" - url = f"{lnbits_url}/castle/api/v1/accounts" + """Fetch all accounts from Libra API""" + url = f"{lnbits_url}/libra/api/v1/accounts" headers = {"X-Api-Key": api_key} try: @@ -153,28 +153,28 @@ class AccountLookup: self.accounts_by_user[user_id] = {} self.accounts_by_user[user_id][account_type] = account_id - print(f"🏦 Loaded {len(self.accounts)} accounts from Castle") + print(f"🏦 Loaded {len(self.accounts)} accounts from Libra") except requests.RequestException as e: - raise ConnectionError(f"Failed to fetch accounts from Castle API: {e}") + raise ConnectionError(f"Failed to fetch accounts from Libra API: {e}") def get_account_id(self, account_name: str) -> Optional[str]: """ - Get Castle account ID for a Beancount account name. + Get Libra account ID for a Beancount account name. Special handling for user-specific accounts: - - "Liabilities:Payable:Pat" -> looks up Pat's user_id and finds their Castle payable account - - "Assets:Receivable:Pat" -> looks up Pat's user_id and finds their Castle receivable account - - "Equity:Pat" -> looks up Pat's user_id and finds their Castle equity account + - "Liabilities:Payable:Pat" -> looks up Pat's user_id and finds their Libra payable account + - "Assets:Receivable:Pat" -> looks up Pat's user_id and finds their Libra receivable account + - "Equity:Pat" -> looks up Pat's user_id and finds their Libra equity account Args: account_name: Beancount account name (e.g., "Expenses:Food:Supplies", "Liabilities:Payable:Pat", "Assets:Receivable:Pat", "Equity:Pat") Returns: - Castle account UUID or None if not found + Libra account UUID or None if not found """ # Check if this is a Liabilities:Payable: account - # Map Beancount Liabilities:Payable:Pat to Castle Liabilities:Payable:User- + # Map Beancount Liabilities:Payable:Pat to Libra Liabilities:Payable:User- if account_name.startswith("Liabilities:Payable:"): user_name = extract_user_from_user_account(account_name) if user_name: @@ -182,7 +182,7 @@ class AccountLookup: user_id = USER_MAPPINGS.get(user_name) if user_id: # Find this user's liability (payable) account - # This is the Liabilities:Payable:User- account in Castle + # This is the Liabilities:Payable:User- account in Libra if user_id in self.accounts_by_user: liability_account_id = self.accounts_by_user[user_id].get('liability') if liability_account_id: @@ -196,7 +196,7 @@ class AccountLookup: ) # Check if this is an Assets:Receivable: account - # Map Beancount Assets:Receivable:Pat to Castle Assets:Receivable:User- + # Map Beancount Assets:Receivable:Pat to Libra Assets:Receivable:User- elif account_name.startswith("Assets:Receivable:"): user_name = extract_user_from_user_account(account_name) if user_name: @@ -204,7 +204,7 @@ class AccountLookup: user_id = USER_MAPPINGS.get(user_name) if user_id: # Find this user's asset (receivable) account - # This is the Assets:Receivable:User- account in Castle + # This is the Assets:Receivable:User- account in Libra if user_id in self.accounts_by_user: asset_account_id = self.accounts_by_user[user_id].get('asset') if asset_account_id: @@ -218,7 +218,7 @@ class AccountLookup: ) # Check if this is an Equity: account - # Map Beancount Equity:Pat to Castle Equity:User- + # Map Beancount Equity:Pat to Libra Equity:User- elif account_name.startswith("Equity:"): user_name = extract_user_from_user_account(account_name) if user_name: @@ -226,7 +226,7 @@ class AccountLookup: user_id = USER_MAPPINGS.get(user_name) if user_id: # Find this user's equity account - # This is the Equity:User- account in Castle + # This is the Equity:User- account in Libra if user_id in self.accounts_by_user: equity_account_id = self.accounts_by_user[user_id].get('equity') if equity_account_id: @@ -235,7 +235,7 @@ class AccountLookup: # If not found, provide helpful error raise ValueError( f"User '{user_name}' (ID: {user_id}) does not have an equity account.\n" - f"Equity eligibility must be enabled for this user in Castle.\n" + f"Equity eligibility must be enabled for this user in Libra.\n" f"Please enable equity for user ID: {user_id}" ) @@ -282,7 +282,7 @@ def eur_to_sats(eur_amount: Decimal, btc_eur_rate: float) -> int: def build_metadata(eur_amount: Decimal, btc_eur_rate: float) -> dict: """ - Build metadata dict for Castle entry line. + Build metadata dict for Libra entry line. The API will extract fiat_currency and fiat_amount and use them to create proper EUR-based postings with SATS in metadata. @@ -441,13 +441,13 @@ def determine_user_id(postings: list) -> Optional[str]: # No user-specific account found - this shouldn't happen for typical transactions return None -# ===== CASTLE CONVERTER ===== +# ===== LIBRA CONVERTER ===== -def convert_to_castle_entry(parsed: dict, btc_eur_rate: float, account_lookup: AccountLookup) -> dict: +def convert_to_libra_entry(parsed: dict, btc_eur_rate: float, account_lookup: AccountLookup) -> dict: """ - Convert parsed Beancount transaction to Castle format. + Convert parsed Beancount transaction to Libra format. - Sends SATS amounts with fiat metadata. The Castle API will automatically + Sends SATS amounts with fiat metadata. The Libra API will automatically convert to EUR-based postings with SATS stored in metadata. """ @@ -469,8 +469,8 @@ def convert_to_castle_entry(parsed: dict, btc_eur_rate: float, account_lookup: A account_id = account_lookup.get_account_id(posting['account']) if not account_id: raise ValueError( - f"No account found in Castle with name '{posting['account']}'.\n" - f"Please create this account in Castle first." + f"No account found in Libra with name '{posting['account']}'.\n" + f"Please create this account in Libra first." ) eur_amount = posting['eur_amount'] @@ -510,7 +510,7 @@ def convert_to_castle_entry(parsed: dict, btc_eur_rate: float, account_lookup: A # ===== API UPLOAD ===== def upload_entry(entry: dict, api_key: str, dry_run: bool = False) -> dict: - """Upload journal entry to Castle API""" + """Upload journal entry to Libra API""" if dry_run: print(f"\n[DRY RUN] Entry preview:") print(f" Description: {entry['description']}") @@ -525,7 +525,7 @@ def upload_entry(entry: dict, api_key: str, dry_run: bool = False) -> dict: print(f" Balance check: {total_sats} (should be 0)") return {"id": "dry-run"} - url = f"{LNBITS_URL}/castle/api/v1/entries" + url = f"{LNBITS_URL}/libra/api/v1/entries" headers = { "X-Api-Key": api_key, "Content-Type": "application/json" @@ -551,7 +551,7 @@ def import_beancount_file(beancount_file: str, dry_run: bool = False): # Validate configuration if not ADMIN_API_KEY: - print("❌ Error: CASTLE_ADMIN_KEY not set!") + print("❌ Error: LIBRA_ADMIN_KEY not set!") print(" Set it as environment variable or update ADMIN_API_KEY in the script.") return @@ -562,7 +562,7 @@ def import_beancount_file(beancount_file: str, dry_run: bool = False): print(f"❌ Error loading rates: {e}") return - # Load accounts from Castle + # Load accounts from Libra try: account_lookup = AccountLookup(LNBITS_URL, ADMIN_API_KEY) except (ConnectionError, ValueError) as e: @@ -574,7 +574,7 @@ def import_beancount_file(beancount_file: str, dry_run: bool = False): for name, user_id in USER_MAPPINGS.items(): has_equity = user_id in account_lookup.accounts_by_user and 'equity' in account_lookup.accounts_by_user[user_id] status = "✅" if has_equity else "❌" - print(f" {status} {name} → {user_id} {'(has equity account)' if has_equity else '(NO EQUITY ACCOUNT - create in Castle!)'}") + print(f" {status} {name} → {user_id} {'(has equity account)' if has_equity else '(NO EQUITY ACCOUNT - create in Libra!)'}") # Read beancount file if not os.path.exists(beancount_file): @@ -612,8 +612,8 @@ def import_beancount_file(beancount_file: str, dry_run: bool = False): if not btc_eur_rate: raise ValueError(f"No BTC/EUR rate found for {parsed['date'].date()}") - castle_entry = convert_to_castle_entry(parsed, btc_eur_rate, account_lookup) - result = upload_entry(castle_entry, ADMIN_API_KEY, dry_run) + libra_entry = convert_to_libra_entry(parsed, btc_eur_rate, account_lookup) + result = upload_entry(libra_entry, ADMIN_API_KEY, dry_run) # Get user name for display user_name = None @@ -643,7 +643,7 @@ def import_beancount_file(beancount_file: str, dry_run: bool = False): print(f" {item}") if success_count > 0 and not dry_run: - print(f"\n✅ Successfully imported {success_count} transactions to Castle!") + print(f"\n✅ Successfully imported {success_count} transactions to Libra!") print(f"\n💡 Note: Transactions are stored in EUR with SATS in metadata.") print(f" Check Fava to see the imported entries.") @@ -653,7 +653,7 @@ if __name__ == "__main__": import sys print("=" * 70) - print("🏰 Beancount to Castle Import Script") + print("🏰 Beancount to Libra Import Script") print("=" * 70) if len(sys.argv) < 2: @@ -664,7 +664,7 @@ if __name__ == "__main__": print("\nConfiguration:") print(f" LNBITS_URL: {LNBITS_URL}") print(f" RATES_CSV: {RATES_CSV_FILE}") - print(f" API Key set: {'Yes' if ADMIN_API_KEY else 'No (set CASTLE_ADMIN_KEY env var)'}") + print(f" API Key set: {'Yes' if ADMIN_API_KEY else 'No (set LIBRA_ADMIN_KEY env var)'}") sys.exit(1) beancount_file = sys.argv[1] diff --git a/manifest.json b/manifest.json index 1a8c320..d7e25d2 100644 --- a/manifest.json +++ b/manifest.json @@ -1,9 +1,9 @@ { "repos": [ { - "id": "castle", + "id": "libra", "organisation": "lnbits", - "repository": "castle" + "repository": "libra" } ] } diff --git a/migrations.py b/migrations.py index c9a7e30..3cff47b 100644 --- a/migrations.py +++ b/migrations.py @@ -1,8 +1,8 @@ """ -Castle Extension Database Migrations +Libra Extension Database Migrations This file contains a single squashed migration that creates the complete -database schema for the Castle extension. +database schema for the Libra extension. MIGRATION HISTORY: This is a squashed migration that combines m001-m016 from the original @@ -39,19 +39,19 @@ Original migration sequence (Nov 2025): async def m001_initial(db): """ - Initial Castle database schema (squashed from m001-m016). + Initial Libra database schema (squashed from m001-m016). - Creates complete database structure for Castle accounting extension: + Creates complete database structure for Libra accounting extension: - Accounts: Chart of accounts with hierarchical Beancount-style names - - Extension settings: Castle-wide configuration + - Extension settings: Libra-wide configuration - User wallet settings: Per-user wallet configuration - - Manual payment requests: User-submitted payment requests to Castle + - Manual payment requests: User-submitted payment requests to Libra - Balance assertions: Reconciliation and balance checking - User equity status: Equity contribution eligibility - Account permissions: Granular access control Note: Journal entries are managed by Fava/Beancount (external source of truth). - Castle submits entries to Fava and queries Fava for journal data. + Libra submits entries to Fava and queries Fava for journal data. """ # ========================================================================= @@ -89,15 +89,15 @@ async def m001_initial(db): # ========================================================================= # EXTENSION SETTINGS TABLE # ========================================================================= - # Castle-wide configuration settings + # Libra-wide configuration settings await db.execute( f""" CREATE TABLE extension_settings ( id TEXT NOT NULL PRIMARY KEY, - castle_wallet_id TEXT, + libra_wallet_id TEXT, fava_url TEXT NOT NULL DEFAULT 'http://localhost:3333', - fava_ledger_slug TEXT NOT NULL DEFAULT 'castle-ledger', + fava_ledger_slug TEXT NOT NULL DEFAULT 'libra-ledger', fava_timeout REAL NOT NULL DEFAULT 10.0, updated_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now} ); @@ -122,7 +122,7 @@ async def m001_initial(db): # ========================================================================= # MANUAL PAYMENT REQUESTS TABLE # ========================================================================= - # User-submitted payment requests to Castle (reviewed by admins) + # User-submitted payment requests to Libra (reviewed by admins) await db.execute( f""" @@ -362,7 +362,7 @@ async def m003_add_account_is_virtual(db): Add is_virtual field to accounts table for virtual parent accounts. Virtual parent accounts: - - Exist only in Castle DB (metadata-only, not in Beancount) + - Exist only in Libra DB (metadata-only, not in Beancount) - Used solely for permission inheritance - Allow granting permissions on top-level accounts like "Expenses", "Assets" - Are not synced to/from Beancount diff --git a/models.py b/models.py index 3b47210..eaf75c4 100644 --- a/models.py +++ b/models.py @@ -87,7 +87,7 @@ class CreateJournalEntry(BaseModel): class UserBalance(BaseModel): user_id: str - balance: int # positive = castle owes user, negative = user owes castle + balance: int # positive = libra owes user, negative = user owes libra accounts: list[Account] = [] fiat_balances: dict[str, Decimal] = {} # e.g. {"EUR": Decimal("250.0"), "USD": Decimal("100.0")} @@ -98,7 +98,7 @@ class ExpenseEntry(BaseModel): description: str amount: Decimal # Amount in the specified currency (or satoshis if currency is None) expense_account: str # account name or ID - is_equity: bool = False # True = equity contribution, False = liability (castle owes user) + is_equity: bool = False # True = equity contribution, False = liability (libra owes user) user_wallet: str reference: Optional[str] = None currency: Optional[str] = None # If None, amount is in satoshis. Otherwise, fiat currency code (EUR, USD, etc.) @@ -111,7 +111,7 @@ class ReceivableEntry(BaseModel): description: str amount: Decimal # Amount in the specified currency (or satoshis if currency is None) revenue_account: str # account name or ID - user_id: str # The user_id (not wallet_id) of the user who owes the castle + user_id: str # The user_id (not wallet_id) of the user who owes the libra reference: Optional[str] = None currency: Optional[str] = None # If None, amount is in satoshis. Otherwise, fiat currency code @@ -127,14 +127,14 @@ class RevenueEntry(BaseModel): currency: Optional[str] = None # If None, amount is in satoshis. Otherwise, fiat currency code -class CastleSettings(BaseModel): - """Settings for the Castle extension""" +class LibraSettings(BaseModel): + """Settings for the Libra extension""" - castle_wallet_id: Optional[str] = None # The wallet ID that represents the Castle + libra_wallet_id: Optional[str] = None # The wallet ID that represents the Libra # Fava/Beancount integration - ALL accounting is done via Fava fava_url: str = "http://localhost:3333" # Base URL of Fava server - fava_ledger_slug: str = "castle-ledger" # Ledger identifier in Fava URL + fava_ledger_slug: str = "libra-ledger" # Ledger identifier in Fava URL fava_timeout: float = 10.0 # Request timeout in seconds updated_at: datetime = Field(default_factory=lambda: datetime.now()) @@ -144,7 +144,7 @@ class CastleSettings(BaseModel): return True -class UserCastleSettings(CastleSettings): +class UserLibraSettings(LibraSettings): """User-specific settings (stored with user_id)""" id: str @@ -164,7 +164,7 @@ class StoredUserWalletSettings(UserWalletSettings): class ManualPaymentRequest(BaseModel): - """Manual payment request from user to castle""" + """Manual payment request from user to libra""" id: str user_id: str @@ -173,7 +173,7 @@ class ManualPaymentRequest(BaseModel): status: str = "pending" # pending, approved, rejected created_at: datetime reviewed_at: Optional[datetime] = None - reviewed_by: Optional[str] = None # user_id of castle admin who reviewed + reviewed_by: Optional[str] = None # user_id of libra admin who reviewed journal_entry_id: Optional[str] = None # set when approved @@ -198,7 +198,7 @@ class RecordPayment(BaseModel): class SettleReceivable(BaseModel): - """Manually settle a receivable (user pays castle in person)""" + """Manually settle a receivable (user pays libra in person)""" user_id: str amount: Decimal # Amount in the specified currency (or satoshis if currency is None) @@ -213,7 +213,7 @@ class SettleReceivable(BaseModel): class PayUser(BaseModel): - """Pay a user (castle pays user for expense/liability)""" + """Pay a user (libra pays user for expense/liability)""" user_id: str amount: Decimal # Amount in the specified currency (or satoshis if currency is None) diff --git a/package.json b/package.json index f479115..8965642 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "castle", + "name": "libra", "version": "0.0.2", "description": "Accounting for a collective entity", "main": "index.js", diff --git a/permission_management.py b/permission_management.py index 7dea217..80f63ff 100644 --- a/permission_management.py +++ b/permission_management.py @@ -361,7 +361,7 @@ async def get_permission_analytics() -> dict: """ SELECT ap.*, a.name as account_name FROM account_permissions ap - JOIN castle_accounts a ON ap.account_id = a.id + JOIN libra_accounts a ON ap.account_id = a.id WHERE ap.expires_at IS NOT NULL AND ap.expires_at > :now AND ap.expires_at <= :seven_days @@ -385,7 +385,7 @@ async def get_permission_analytics() -> dict: top_accounts_result = await db.fetchall( """ SELECT a.name, COUNT(ap.id) as permission_count - FROM castle_accounts a + FROM libra_accounts a LEFT JOIN account_permissions ap ON a.id = ap.account_id GROUP BY a.id, a.name HAVING COUNT(ap.id) > 0 diff --git a/services.py b/services.py index 51c4bd8..6776c05 100644 --- a/services.py +++ b/services.py @@ -1,32 +1,32 @@ from .crud import ( - create_castle_settings, + create_libra_settings, create_user_wallet_settings, - get_castle_settings, + get_libra_settings, get_or_create_user_account, get_user_wallet_settings, - update_castle_settings, + update_libra_settings, update_user_wallet_settings, ) -from .models import AccountType, CastleSettings, UserWalletSettings +from .models import AccountType, LibraSettings, UserWalletSettings -async def get_settings(user_id: str) -> CastleSettings: - settings = await get_castle_settings(user_id) +async def get_settings(user_id: str) -> LibraSettings: + settings = await get_libra_settings(user_id) if not settings: - settings = await create_castle_settings(user_id, CastleSettings()) + settings = await create_libra_settings(user_id, LibraSettings()) return settings -async def update_settings(user_id: str, data: CastleSettings) -> CastleSettings: +async def update_settings(user_id: str, data: LibraSettings) -> LibraSettings: from loguru import logger from .fava_client import init_fava_client - settings = await get_castle_settings(user_id) + settings = await get_libra_settings(user_id) if not settings: - settings = await create_castle_settings(user_id, data) + settings = await create_libra_settings(user_id, data) else: - settings = await update_castle_settings(user_id, data) + settings = await update_libra_settings(user_id, data) # Reinitialize Fava client with new settings try: diff --git a/static/image/castle.png b/static/image/libra.png similarity index 100% rename from static/image/castle.png rename to static/image/libra.png diff --git a/static/js/index.js b/static/js/index.js index 4cc5c06..7c01c07 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -32,7 +32,7 @@ window.app = Vue.createApp({ isAdmin: false, isSuperUser: false, settingsLoaded: false, // Flag to prevent race conditions on toolbar buttons - castleWalletConfigured: false, + libraWalletConfigured: false, userWalletConfigured: false, syncingAccounts: false, currentExchangeRate: null, // BTC/EUR rate (sats per EUR) @@ -58,9 +58,9 @@ window.app = Vue.createApp({ }, settingsDialog: { show: false, - castleWalletId: '', + libraWalletId: '', favaUrl: 'http://localhost:3333', - favaLedgerSlug: 'castle-ledger', + favaLedgerSlug: 'libra-ledger', favaTimeout: 10.0, loading: false }, @@ -208,8 +208,8 @@ window.app = Vue.createApp({ accountTypeOptions() { return [ { label: 'All Types', value: null }, - { label: 'Receivable (User owes Castle)', value: 'asset' }, - { label: 'Payable (Castle owes User)', value: 'liability' }, + { label: 'Receivable (User owes Libra)', value: 'asset' }, + { label: 'Payable (Libra owes User)', value: 'liability' }, { label: 'Equity (User Balance)', value: 'equity' } ] }, @@ -318,7 +318,7 @@ window.app = Vue.createApp({ try { const response = await LNbits.api.request( 'GET', - '/castle/api/v1/balance', + '/libra/api/v1/balance', this.g.user.wallets[0].inkey ) this.balance = response.data @@ -341,7 +341,7 @@ window.app = Vue.createApp({ try { const response = await LNbits.api.request( 'GET', - '/castle/api/v1/balances/all', + '/libra/api/v1/balances/all', this.g.user.wallets[0].adminkey ) this.allUserBalances = response.data @@ -389,7 +389,7 @@ window.app = Vue.createApp({ const response = await LNbits.api.request( 'GET', - `/castle/api/v1/entries/user?${queryParams}`, + `/libra/api/v1/entries/user?${queryParams}`, this.g.user.wallets[0].inkey ) @@ -458,7 +458,7 @@ window.app = Vue.createApp({ try { const response = await LNbits.api.request( 'GET', - '/castle/api/v1/accounts?filter_by_user=true&exclude_virtual=true', + '/libra/api/v1/accounts?filter_by_user=true&exclude_virtual=true', this.g.user.wallets[0].inkey ) this.accounts = response.data @@ -472,7 +472,7 @@ window.app = Vue.createApp({ try { const response = await LNbits.api.request( 'GET', - '/castle/api/v1/currencies', + '/libra/api/v1/currencies', this.g.user.wallets[0].inkey ) this.currencies = response.data @@ -484,7 +484,7 @@ window.app = Vue.createApp({ try { const response = await LNbits.api.request( 'GET', - '/castle/api/v1/users', + '/libra/api/v1/users', this.g.user.wallets[0].adminkey ) this.users = response.data @@ -496,7 +496,7 @@ window.app = Vue.createApp({ try { const response = await LNbits.api.request( 'GET', - '/castle/api/v1/user/info', + '/libra/api/v1/user/info', this.g.user.wallets[0].inkey ) this.userInfo = response.data @@ -510,18 +510,18 @@ window.app = Vue.createApp({ // Try with admin key first to check settings const response = await LNbits.api.request( 'GET', - '/castle/api/v1/settings', + '/libra/api/v1/settings', this.g.user.wallets[0].inkey ) this.settings = response.data - this.castleWalletConfigured = !!(this.settings && this.settings.castle_wallet_id) + this.libraWalletConfigured = !!(this.settings && this.settings.libra_wallet_id) // Check if user is super user by seeing if they can access admin features this.isSuperUser = this.g.user.super_user || false this.isAdmin = this.g.user.admin || this.isSuperUser } catch (error) { // Settings not available - this.castleWalletConfigured = false + this.libraWalletConfigured = false } finally { // Mark settings as loaded to enable toolbar buttons this.settingsLoaded = true @@ -531,7 +531,7 @@ window.app = Vue.createApp({ try { const response = await LNbits.api.request( 'GET', - '/castle/api/v1/user/wallet', + '/libra/api/v1/user/wallet', this.g.user.wallets[0].inkey ) this.userWalletSettings = response.data @@ -545,7 +545,7 @@ window.app = Vue.createApp({ try { const {data} = await LNbits.api.request( 'POST', - '/castle/api/v1/admin/accounts/sync', + '/libra/api/v1/admin/accounts/sync', this.g.user.wallets[0].adminkey ) const errors = (data?.errors || []).length @@ -567,9 +567,9 @@ window.app = Vue.createApp({ } }, showSettingsDialog() { - this.settingsDialog.castleWalletId = this.settings?.castle_wallet_id || '' + this.settingsDialog.libraWalletId = this.settings?.libra_wallet_id || '' this.settingsDialog.favaUrl = this.settings?.fava_url || 'http://localhost:3333' - this.settingsDialog.favaLedgerSlug = this.settings?.fava_ledger_slug || 'castle-ledger' + this.settingsDialog.favaLedgerSlug = this.settings?.fava_ledger_slug || 'libra-ledger' this.settingsDialog.favaTimeout = this.settings?.fava_timeout || 10.0 this.settingsDialog.show = true }, @@ -578,10 +578,10 @@ window.app = Vue.createApp({ this.userWalletDialog.show = true }, async submitSettings() { - if (!this.settingsDialog.castleWalletId) { + if (!this.settingsDialog.libraWalletId) { this.$q.notify({ type: 'warning', - message: 'Castle Wallet ID is required' + message: 'Libra Wallet ID is required' }) return } @@ -598,12 +598,12 @@ window.app = Vue.createApp({ try { await LNbits.api.request( 'PUT', - '/castle/api/v1/settings', + '/libra/api/v1/settings', this.g.user.wallets[0].adminkey, { - castle_wallet_id: this.settingsDialog.castleWalletId, + libra_wallet_id: this.settingsDialog.libraWalletId, fava_url: this.settingsDialog.favaUrl, - fava_ledger_slug: this.settingsDialog.favaLedgerSlug || 'castle-ledger', + fava_ledger_slug: this.settingsDialog.favaLedgerSlug || 'libra-ledger', fava_timeout: parseFloat(this.settingsDialog.favaTimeout) || 10.0 } ) @@ -613,7 +613,7 @@ window.app = Vue.createApp({ }) this.settingsDialog.show = false await this.loadSettings() - // Reload user wallet to reflect castle wallet for super user + // Reload user wallet to reflect libra wallet for super user if (this.isSuperUser) { await this.loadUserWallet() } @@ -636,7 +636,7 @@ window.app = Vue.createApp({ try { await LNbits.api.request( 'PUT', - '/castle/api/v1/user/wallet', + '/libra/api/v1/user/wallet', this.g.user.wallets[0].inkey, { user_wallet_id: this.userWalletDialog.userWalletId @@ -659,7 +659,7 @@ window.app = Vue.createApp({ try { await LNbits.api.request( 'POST', - '/castle/api/v1/entries/expense', + '/libra/api/v1/entries/expense', this.g.user.wallets[0].inkey, { description: this.expenseDialog.description, @@ -696,10 +696,10 @@ window.app = Vue.createApp({ } try { - // Generate an invoice on the Castle wallet + // Generate an invoice on the Libra wallet const response = await LNbits.api.request( 'POST', - '/castle/api/v1/generate-payment-invoice', + '/libra/api/v1/generate-payment-invoice', this.g.user.wallets[0].inkey, { amount: this.payDialog.amount @@ -745,7 +745,7 @@ window.app = Vue.createApp({ try { await LNbits.api.request( 'POST', - '/castle/api/v1/record-payment', + '/libra/api/v1/record-payment', this.g.user.wallets[0].inkey, { payment_hash: paymentHash @@ -788,15 +788,15 @@ window.app = Vue.createApp({ }, showManualPaymentOption() { // This is for when user wants to pay their debt manually - // For now, just notify them to contact castle + // For now, just notify them to contact libra this.$q.notify({ type: 'info', - message: 'Please contact Castle directly to arrange manual payment.', + message: 'Please contact Libra directly to arrange manual payment.', timeout: 3000 }) }, showManualPaymentDialog() { - // This is for when Castle owes user and they want to request manual payment + // This is for when Libra owes user and they want to request manual payment this.manualPaymentDialog.amount = Math.abs(this.balance.balance) this.manualPaymentDialog.description = '' this.manualPaymentDialog.show = true @@ -806,7 +806,7 @@ window.app = Vue.createApp({ try { await LNbits.api.request( 'POST', - '/castle/api/v1/manual-payment-request', + '/libra/api/v1/manual-payment-request', this.g.user.wallets[0].inkey, { amount: this.manualPaymentDialog.amount, @@ -831,8 +831,8 @@ window.app = Vue.createApp({ try { // If super user, load all requests; otherwise load user's own requests const endpoint = this.isSuperUser - ? '/castle/api/v1/manual-payment-requests/all' - : '/castle/api/v1/manual-payment-requests' + ? '/libra/api/v1/manual-payment-requests/all' + : '/libra/api/v1/manual-payment-requests' const key = this.isSuperUser ? this.g.user.wallets[0].adminkey : this.g.user.wallets[0].inkey @@ -855,7 +855,7 @@ window.app = Vue.createApp({ const response = await LNbits.api.request( 'GET', - '/castle/api/v1/entries/pending', + '/libra/api/v1/entries/pending', this.g.user.wallets[0].adminkey ) this.pendingExpenses = response.data @@ -867,7 +867,7 @@ window.app = Vue.createApp({ try { await LNbits.api.request( 'POST', - `/castle/api/v1/manual-payment-requests/${requestId}/approve`, + `/libra/api/v1/manual-payment-requests/${requestId}/approve`, this.g.user.wallets[0].adminkey ) this.$q.notify({ @@ -885,7 +885,7 @@ window.app = Vue.createApp({ try { await LNbits.api.request( 'POST', - `/castle/api/v1/manual-payment-requests/${requestId}/reject`, + `/libra/api/v1/manual-payment-requests/${requestId}/reject`, this.g.user.wallets[0].adminkey ) this.$q.notify({ @@ -901,7 +901,7 @@ window.app = Vue.createApp({ try { await LNbits.api.request( 'POST', - `/castle/api/v1/entries/${entryId}/approve`, + `/libra/api/v1/entries/${entryId}/approve`, this.g.user.wallets[0].adminkey ) this.$q.notify({ @@ -920,7 +920,7 @@ window.app = Vue.createApp({ try { await LNbits.api.request( 'POST', - `/castle/api/v1/entries/${entryId}/reject`, + `/libra/api/v1/entries/${entryId}/reject`, this.g.user.wallets[0].adminkey ) this.$q.notify({ @@ -939,7 +939,7 @@ window.app = Vue.createApp({ try { const response = await LNbits.api.request( 'GET', - '/castle/api/v1/assertions', + '/libra/api/v1/assertions', this.g.user.wallets[0].adminkey ) this.balanceAssertions = response.data @@ -965,7 +965,7 @@ window.app = Vue.createApp({ await LNbits.api.request( 'POST', - '/castle/api/v1/assertions', + '/libra/api/v1/assertions', this.g.user.wallets[0].adminkey, payload ) @@ -1014,7 +1014,7 @@ window.app = Vue.createApp({ try { await LNbits.api.request( 'POST', - `/castle/api/v1/assertions/${assertionId}/check`, + `/libra/api/v1/assertions/${assertionId}/check`, this.g.user.wallets[0].adminkey ) @@ -1033,7 +1033,7 @@ window.app = Vue.createApp({ try { await LNbits.api.request( 'DELETE', - `/castle/api/v1/assertions/${assertionId}`, + `/libra/api/v1/assertions/${assertionId}`, this.g.user.wallets[0].adminkey ) @@ -1062,7 +1062,7 @@ window.app = Vue.createApp({ try { const response = await LNbits.api.request( 'GET', - '/castle/api/v1/reconciliation/summary', + '/libra/api/v1/reconciliation/summary', this.g.user.wallets[0].adminkey ) this.reconciliation.summary = response.data @@ -1076,7 +1076,7 @@ window.app = Vue.createApp({ try { const response = await LNbits.api.request( 'GET', - '/castle/api/v1/reconciliation/discrepancies', + '/libra/api/v1/reconciliation/discrepancies', this.g.user.wallets[0].adminkey ) this.reconciliation.discrepancies = response.data @@ -1089,7 +1089,7 @@ window.app = Vue.createApp({ try { const response = await LNbits.api.request( 'POST', - '/castle/api/v1/reconciliation/check-all', + '/libra/api/v1/reconciliation/check-all', this.g.user.wallets[0].adminkey ) @@ -1143,7 +1143,7 @@ window.app = Vue.createApp({ try { await LNbits.api.request( 'POST', - '/castle/api/v1/entries/receivable', + '/libra/api/v1/entries/receivable', this.g.user.wallets[0].adminkey, { description: this.receivableDialog.description, @@ -1186,7 +1186,7 @@ window.app = Vue.createApp({ this.receivableDialog.currency = null }, async showSettleReceivableDialog(userBalance) { - // Only show for users who owe castle (positive balance = receivable) + // Only show for users who owe libra (positive balance = receivable) if (userBalance.balance <= 0) return // Clear any existing polling @@ -1202,19 +1202,19 @@ window.app = Vue.createApp({ // Fetch unsettled entries for this user (BOTH receivables AND expenses for net settlement) let allEntryLinks = [] try { - // Fetch receivable entries (user owes castle) + // Fetch receivable entries (user owes libra) const receivableResponse = await LNbits.api.request( 'GET', - `/castle/api/v1/users/${userBalance.user_id}/unsettled-entries?entry_type=receivable`, + `/libra/api/v1/users/${userBalance.user_id}/unsettled-entries?entry_type=receivable`, this.g.user.wallets[0].adminkey ) const receivableEntries = receivableResponse.data.unsettled_entries || [] allEntryLinks.push(...receivableEntries.map(e => e.link).filter(l => l)) - // Also fetch expense entries (castle owes user) - these are netted in the settlement + // Also fetch expense entries (libra owes user) - these are netted in the settlement const expenseResponse = await LNbits.api.request( 'GET', - `/castle/api/v1/users/${userBalance.user_id}/unsettled-entries?entry_type=expense`, + `/libra/api/v1/users/${userBalance.user_id}/unsettled-entries?entry_type=expense`, this.g.user.wallets[0].adminkey ) const expenseEntries = expenseResponse.data.unsettled_entries || [] @@ -1254,10 +1254,10 @@ window.app = Vue.createApp({ } try { - // Generate an invoice on the Castle wallet for the user to pay + // Generate an invoice on the Libra wallet for the user to pay const response = await LNbits.api.request( 'POST', - '/castle/api/v1/generate-payment-invoice', + '/libra/api/v1/generate-payment-invoice', this.g.user.wallets[0].adminkey, { amount: this.settleReceivableDialog.amount, @@ -1384,7 +1384,7 @@ window.app = Vue.createApp({ const response = await LNbits.api.request( 'POST', - '/castle/api/v1/receivables/settle', + '/libra/api/v1/receivables/settle', this.g.user.wallets[0].adminkey, payload ) @@ -1408,7 +1408,7 @@ window.app = Vue.createApp({ } }, async showPayUserDialog(userBalance) { - // Only show for users castle owes (negative balance = payable) + // Only show for users libra owes (negative balance = payable) if (userBalance.balance >= 0) return // Extract fiat balances (e.g., EUR) @@ -1416,26 +1416,26 @@ window.app = Vue.createApp({ const fiatCurrency = Object.keys(fiatBalances)[0] || null const fiatAmount = fiatCurrency ? fiatBalances[fiatCurrency] : 0 - // Use absolute values since balance is negative (liability = castle owes user) + // Use absolute values since balance is negative (liability = libra owes user) const maxAmountSats = Math.abs(userBalance.balance) const maxAmountFiat = Math.abs(fiatAmount) // Fetch unsettled entries for this user (BOTH expenses AND receivables for net settlement) let allEntryLinks = [] try { - // Fetch expense entries (castle owes user) + // Fetch expense entries (libra owes user) const expenseResponse = await LNbits.api.request( 'GET', - `/castle/api/v1/users/${userBalance.user_id}/unsettled-entries?entry_type=expense`, + `/libra/api/v1/users/${userBalance.user_id}/unsettled-entries?entry_type=expense`, this.g.user.wallets[0].adminkey ) const expenseEntries = expenseResponse.data.unsettled_entries || [] allEntryLinks.push(...expenseEntries.map(e => e.link).filter(l => l)) - // Also fetch receivable entries (user owes castle) - these are netted in the settlement + // Also fetch receivable entries (user owes libra) - these are netted in the settlement const receivableResponse = await LNbits.api.request( 'GET', - `/castle/api/v1/users/${userBalance.user_id}/unsettled-entries?entry_type=receivable`, + `/libra/api/v1/users/${userBalance.user_id}/unsettled-entries?entry_type=receivable`, this.g.user.wallets[0].adminkey ) const receivableEntries = receivableResponse.data.unsettled_entries || [] @@ -1448,7 +1448,7 @@ window.app = Vue.createApp({ show: true, user_id: userBalance.user_id, username: userBalance.username, - maxAmount: maxAmountSats, // Positive sats amount castle owes + maxAmount: maxAmountSats, // Positive sats amount libra owes maxAmountFiat: maxAmountFiat, // EUR or other fiat amount (positive) fiatCurrency: fiatCurrency, amount: maxAmountSats, // Default to sats since lightning is the default payment method @@ -1480,14 +1480,14 @@ window.app = Vue.createApp({ { out: false, amount: this.payUserDialog.amount, - memo: `Payment from Castle to ${this.payUserDialog.username}` + memo: `Payment from Libra to ${this.payUserDialog.username}` } ) console.log(invoiceResponse) const paymentRequest = invoiceResponse.data.bolt11 - // Pay the invoice from Castle's wallet + // Pay the invoice from Libra's wallet const paymentResponse = await LNbits.api.request( 'POST', `/api/v1/payments`, @@ -1498,7 +1498,7 @@ window.app = Vue.createApp({ } ) - // Record the payment in Castle accounting + // Record the payment in Libra accounting const payPayload = { user_id: this.payUserDialog.user_id, amount: this.payUserDialog.amount, @@ -1513,7 +1513,7 @@ window.app = Vue.createApp({ await LNbits.api.request( 'POST', - '/castle/api/v1/payables/pay', + '/libra/api/v1/payables/pay', this.g.user.wallets[0].adminkey, payPayload ) @@ -1579,7 +1579,7 @@ window.app = Vue.createApp({ const response = await LNbits.api.request( 'POST', - '/castle/api/v1/payables/pay', + '/libra/api/v1/payables/pay', this.g.user.wallets[0].adminkey, payload ) @@ -1606,7 +1606,7 @@ window.app = Vue.createApp({ try { const response = await LNbits.api.request( 'GET', - `/castle/api/v1/user-wallet/${userId}`, + `/libra/api/v1/user-wallet/${userId}`, this.g.user.wallets[0].adminkey ) return response.data @@ -1663,13 +1663,13 @@ window.app = Vue.createApp({ return null }, isReceivable(entry) { - // Check if this is a receivable entry (user owes castle) + // Check if this is a receivable entry (user owes libra) if (entry.tags && entry.tags.includes('receivable-entry')) return true if (entry.account && entry.account.includes('Receivable')) return true return false }, isPayable(entry) { - // Check if this is a payable entry (castle owes user) + // Check if this is a payable entry (libra owes user) if (entry.tags && entry.tags.includes('expense-entry')) return true if (entry.account && entry.account.includes('Payable')) return true return false diff --git a/static/js/permissions.js b/static/js/permissions.js index 4cc54f0..948ac3a 100644 --- a/static/js/permissions.js +++ b/static/js/permissions.js @@ -206,7 +206,7 @@ window.app = Vue.createApp({ try { const response = await LNbits.api.request( 'GET', - '/castle/api/v1/admin/permissions', + '/libra/api/v1/admin/permissions', this.g.user.wallets[0].adminkey ) this.permissions = response.data @@ -228,7 +228,7 @@ window.app = Vue.createApp({ // Admin permissions UI needs to see virtual accounts to grant permissions on them const response = await LNbits.api.request( 'GET', - '/castle/api/v1/accounts?exclude_virtual=false', + '/libra/api/v1/accounts?exclude_virtual=false', this.g.user.wallets[0].inkey ) this.accounts = response.data @@ -251,7 +251,7 @@ window.app = Vue.createApp({ try { const response = await LNbits.api.request( 'GET', - '/castle/api/v1/admin/castle-users', + '/libra/api/v1/admin/libra-users', this.g.user.wallets[0].adminkey ) this.users = response.data || [] @@ -318,7 +318,7 @@ window.app = Vue.createApp({ await LNbits.api.request( 'POST', - '/castle/api/v1/admin/permissions', + '/libra/api/v1/admin/permissions', this.g.user.wallets[0].adminkey, payload ) @@ -357,7 +357,7 @@ window.app = Vue.createApp({ try { await LNbits.api.request( 'DELETE', - `/castle/api/v1/admin/permissions/${this.permissionToRevoke.id}`, + `/libra/api/v1/admin/permissions/${this.permissionToRevoke.id}`, this.g.user.wallets[0].adminkey ) @@ -428,7 +428,7 @@ window.app = Vue.createApp({ const response = await LNbits.api.request( 'POST', - '/castle/api/v1/admin/permissions/bulk-grant', + '/libra/api/v1/admin/permissions/bulk-grant', this.g.user.wallets[0].adminkey, payload ) @@ -535,7 +535,7 @@ window.app = Vue.createApp({ try { const response = await LNbits.api.request( 'GET', - '/castle/api/v1/admin/equity-eligibility', + '/libra/api/v1/admin/equity-eligibility', this.g.user.wallets[0].adminkey ) this.equityEligibleUsers = response.data || [] @@ -573,7 +573,7 @@ window.app = Vue.createApp({ await LNbits.api.request( 'POST', - '/castle/api/v1/admin/equity-eligibility', + '/libra/api/v1/admin/equity-eligibility', this.g.user.wallets[0].adminkey, payload ) @@ -612,7 +612,7 @@ window.app = Vue.createApp({ try { await LNbits.api.request( 'DELETE', - `/castle/api/v1/admin/equity-eligibility/${this.equityToRevoke.user_id}`, + `/libra/api/v1/admin/equity-eligibility/${this.equityToRevoke.user_id}`, this.g.user.wallets[0].adminkey ) @@ -655,7 +655,7 @@ window.app = Vue.createApp({ try { const response = await LNbits.api.request( 'GET', - '/castle/api/v1/admin/roles', + '/libra/api/v1/admin/roles', this.g.user.wallets[0].adminkey ) this.roles = response.data || [] @@ -678,7 +678,7 @@ window.app = Vue.createApp({ try { const response = await LNbits.api.request( 'GET', - `/castle/api/v1/admin/roles/${role.id}`, + `/libra/api/v1/admin/roles/${role.id}`, this.g.user.wallets[0].adminkey ) @@ -733,7 +733,7 @@ window.app = Vue.createApp({ // Update existing role await LNbits.api.request( 'PUT', - `/castle/api/v1/admin/roles/${this.selectedRole.id}`, + `/libra/api/v1/admin/roles/${this.selectedRole.id}`, this.g.user.wallets[0].adminkey, payload ) @@ -747,7 +747,7 @@ window.app = Vue.createApp({ // Create new role await LNbits.api.request( 'POST', - '/castle/api/v1/admin/roles', + '/libra/api/v1/admin/roles', this.g.user.wallets[0].adminkey, payload ) @@ -786,7 +786,7 @@ window.app = Vue.createApp({ try { await LNbits.api.request( 'DELETE', - `/castle/api/v1/admin/roles/${this.roleToDelete.id}`, + `/libra/api/v1/admin/roles/${this.roleToDelete.id}`, this.g.user.wallets[0].adminkey ) @@ -862,7 +862,7 @@ window.app = Vue.createApp({ await LNbits.api.request( 'POST', - '/castle/api/v1/admin/user-roles', + '/libra/api/v1/admin/user-roles', this.g.user.wallets[0].adminkey, payload ) @@ -920,7 +920,7 @@ window.app = Vue.createApp({ try { const response = await LNbits.api.request( 'GET', - '/castle/api/v1/admin/users/roles', + '/libra/api/v1/admin/users/roles', this.g.user.wallets[0].adminkey ) @@ -984,7 +984,7 @@ window.app = Vue.createApp({ try { await LNbits.api.request( 'DELETE', - `/castle/api/v1/admin/user-roles/${this.userRoleToRevoke.id}`, + `/libra/api/v1/admin/user-roles/${this.userRoleToRevoke.id}`, this.g.user.wallets[0].adminkey ) @@ -1033,7 +1033,7 @@ window.app = Vue.createApp({ } await LNbits.api.request( 'POST', - `/castle/api/v1/admin/roles/${this.selectedRole.id}/permissions`, + `/libra/api/v1/admin/roles/${this.selectedRole.id}/permissions`, this.g.user.wallets[0].adminkey, payload ) @@ -1067,7 +1067,7 @@ window.app = Vue.createApp({ try { await LNbits.api.request( 'DELETE', - `/castle/api/v1/admin/roles/${this.selectedRole.id}/permissions/${permissionId}`, + `/libra/api/v1/admin/roles/${this.selectedRole.id}/permissions/${permissionId}`, this.g.user.wallets[0].adminkey ) // Reload role permissions diff --git a/tasks.py b/tasks.py index 8ec83b9..f6f84cb 100644 --- a/tasks.py +++ b/tasks.py @@ -1,5 +1,5 @@ """ -Background tasks for Castle accounting extension. +Background tasks for Libra accounting extension. These tasks handle automated reconciliation checks and maintenance. """ @@ -62,11 +62,11 @@ async def check_all_balance_assertions() -> dict: # Log results if results["failed"] > 0: - print(f"[CASTLE] Daily reconciliation check: {results['failed']} FAILED assertions!") + print(f"[LIBRA] Daily reconciliation check: {results['failed']} FAILED assertions!") for failed in results["failed_assertions"]: print(f" - Account {failed['account_id']}: expected {failed['expected_sats']}, got {failed['actual_sats']}") else: - print(f"[CASTLE] Daily reconciliation check: All {results['passed']} assertions passed ✓") + print(f"[LIBRA] Daily reconciliation check: All {results['passed']} assertions passed ✓") return results @@ -78,7 +78,7 @@ async def scheduled_daily_reconciliation(): This function is meant to be called by a scheduler (cron, systemd timer, etc.) or by LNbits background task system. """ - print(f"[CASTLE] Running scheduled daily reconciliation check at {datetime.now()}") + print(f"[LIBRA] Running scheduled daily reconciliation check at {datetime.now()}") try: results = await check_all_balance_assertions() @@ -86,38 +86,38 @@ async def scheduled_daily_reconciliation(): # TODO: Send notifications if there are failures # This could send email, webhook, or in-app notification if results["failed"] > 0: - print(f"[CASTLE] WARNING: {results['failed']} balance assertions failed!") + print(f"[LIBRA] WARNING: {results['failed']} balance assertions failed!") # Future: Send alert notification return results except Exception as e: - print(f"[CASTLE] Error in scheduled reconciliation: {e}") + print(f"[LIBRA] Error in scheduled reconciliation: {e}") raise async def scheduled_account_sync(): """ - Scheduled task that runs hourly to sync accounts from Beancount to Castle DB. + Scheduled task that runs hourly to sync accounts from Beancount to Libra DB. - This ensures Castle DB stays in sync with Beancount (source of truth) by - automatically adding any new accounts created in Beancount to Castle's + This ensures Libra DB stays in sync with Beancount (source of truth) by + automatically adding any new accounts created in Beancount to Libra's metadata database for permission tracking. """ from .account_sync import sync_accounts_from_beancount - logger.info(f"[CASTLE] Running scheduled account sync at {datetime.now()}") + logger.info(f"[LIBRA] Running scheduled account sync at {datetime.now()}") try: stats = await sync_accounts_from_beancount(force_full_sync=False) if stats["accounts_added"] > 0: logger.info( - f"[CASTLE] Account sync: Added {stats['accounts_added']} new accounts" + f"[LIBRA] Account sync: Added {stats['accounts_added']} new accounts" ) if stats["errors"]: logger.warning( - f"[CASTLE] Account sync: {len(stats['errors'])} errors encountered" + f"[LIBRA] Account sync: {len(stats['errors'])} errors encountered" ) for error in stats["errors"][:5]: # Log first 5 errors logger.error(f" - {error}") @@ -125,24 +125,24 @@ async def scheduled_account_sync(): return stats except Exception as e: - logger.error(f"[CASTLE] Error in scheduled account sync: {e}") + logger.error(f"[LIBRA] Error in scheduled account sync: {e}") raise async def wait_for_account_sync(): """ - Background task that periodically syncs accounts from Beancount to Castle DB. + Background task that periodically syncs accounts from Beancount to Libra DB. - Runs hourly to ensure Castle DB stays in sync with Beancount. + Runs hourly to ensure Libra DB stays in sync with Beancount. """ - logger.info("[CASTLE] Account sync background task started") + logger.info("[LIBRA] Account sync background task started") while True: try: # Run sync await scheduled_account_sync() except Exception as e: - logger.error(f"[CASTLE] Account sync error: {e}") + logger.error(f"[LIBRA] Account sync error: {e}") # Wait 1 hour before next sync await asyncio.sleep(3600) # 3600 seconds = 1 hour @@ -157,9 +157,9 @@ def start_daily_reconciliation_task(): For cron setup: # Run daily at 2 AM - 0 2 * * * curl -X POST http://localhost:5000/castle/api/v1/tasks/daily-reconciliation -H "X-Api-Key: YOUR_ADMIN_KEY" + 0 2 * * * curl -X POST http://localhost:5000/libra/api/v1/tasks/daily-reconciliation -H "X-Api-Key: YOUR_ADMIN_KEY" """ - print("[CASTLE] Daily reconciliation task registered") + print("[LIBRA] Daily reconciliation task registered") # In a production system, you would register this with LNbits task scheduler # For now, it can be triggered manually via API endpoint @@ -173,7 +173,7 @@ async def wait_for_paid_invoices(): before the payment is detected by client-side polling. """ invoice_queue = Queue() - register_invoice_listener(invoice_queue, "ext_castle") + register_invoice_listener(invoice_queue, "ext_libra") while True: payment = await invoice_queue.get() @@ -182,10 +182,10 @@ async def wait_for_paid_invoices(): async def on_invoice_paid(payment: Payment) -> None: """ - Handle a paid Castle invoice by automatically submitting to Fava. + Handle a paid Libra invoice by automatically submitting to Fava. - This function is called automatically when any invoice on the Castle wallet - is paid. It checks if the invoice is a Castle payment and records it in + This function is called automatically when any invoice on the Libra wallet + is paid. It checks if the invoice is a Libra payment and records it in Beancount via Fava. Concurrency Protection: @@ -194,13 +194,13 @@ async def on_invoice_paid(payment: Payment) -> None: - Uses idempotent entry creation to prevent duplicate entries even if the same payment is processed multiple times """ - # Only process Castle-specific payments - if not payment.extra or payment.extra.get("tag") != "castle": + # Only process Libra-specific payments + if not payment.extra or payment.extra.get("tag") != "libra": return user_id = payment.extra.get("user_id") if not user_id: - logger.warning(f"Castle invoice {payment.payment_hash} missing user_id in metadata") + logger.warning(f"Libra invoice {payment.payment_hash} missing user_id in metadata") return from .fava_client import get_fava_client @@ -216,7 +216,7 @@ async def on_invoice_paid(payment: Payment) -> None: user_lock = fava.get_user_lock(user_id) async with user_lock: - logger.info(f"Recording Castle payment {payment.payment_hash} for user {user_id[:8]} to Fava") + logger.info(f"Recording Libra payment {payment.payment_hash} for user {user_id[:8]} to Fava") try: from decimal import Decimal @@ -246,14 +246,14 @@ async def on_invoice_paid(payment: Payment) -> None: total_fiat_balance = fiat_balances.get(fiat_currency, Decimal(0)) # Determine receivables and payables based on balance - # Positive balance = user owes castle (receivable) - # Negative balance = castle owes user (payable) + # Positive balance = user owes libra (receivable) + # Negative balance = libra owes user (payable) if total_fiat_balance > 0: - # User owes castle + # User owes libra total_receivable = total_fiat_balance total_payable = Decimal(0) else: - # Castle owes user + # Libra owes user total_receivable = Decimal(0) total_payable = abs(total_fiat_balance) @@ -318,5 +318,5 @@ async def on_invoice_paid(payment: Payment) -> None: ) except Exception as e: - logger.error(f"Error recording Castle payment {payment.payment_hash}: {e}") + logger.error(f"Error recording Libra payment {payment.payment_hash}: {e}") raise diff --git a/templates/castle/index.html b/templates/libra/index.html similarity index 95% rename from templates/castle/index.html rename to templates/libra/index.html index 98b1625..0286927 100644 --- a/templates/castle/index.html +++ b/templates/libra/index.html @@ -3,7 +3,7 @@ {% block scripts %} {{ window_vars(user) }} - + {% endblock %} {% block page %} @@ -13,7 +13,7 @@
    -
    🏰 Castle Accounting
    +
    Libra

    Track expenses, receivables, and balances for the collective

    @@ -21,14 +21,14 @@ Configure Your Wallet - + Manage Permissions (Admin) Sync Accounts from Beancount - Castle Settings (Super User Only) + Libra Settings (Super User Only)
    @@ -36,19 +36,19 @@ - +
    - Setup Required: Castle Wallet ID must be configured before the extension can function. + Setup Required: Libra Wallet ID must be configured before the extension can function.
    - + @@ -57,7 +57,7 @@ - + @@ -131,13 +131,13 @@ Add Expense - - Castle wallet must be configured first + + Libra wallet must be configured first - + You must configure your wallet first @@ -145,14 +145,14 @@ v-if="isSuperUser" color="orange" @click="showReceivableDialog" - :disable="!castleWalletConfigured" + :disable="!libraWalletConfigured" > Add Receivable - - Castle wallet must be configured first + + Libra wallet must be configured first - Record when a user owes the Castle + Record when a user owes the Libra
    @@ -201,7 +201,7 @@ @@ -257,7 +257,7 @@ {% raw %}{{ balance.balance > 0 ? 'Total owed to you' : balance.balance < 0 ? 'Total you owe' : 'No outstanding balances' }}{% endraw %}
    - {% raw %}{{ balance.balance >= 0 ? 'You owe Castle' : 'Castle owes you' }}{% endraw %} + {% raw %}{{ balance.balance >= 0 ? 'You owe Libra' : 'Libra owes you' }}{% endraw %}
    @@ -941,9 +941,9 @@ filled dense readonly - :model-value="'Liability (Castle owes me)'" + :model-value="'Liability (Libra owes me)'" label="Type" - hint="This expense will be recorded as a liability (Castle owes you)" + hint="This expense will be recorded as a liability (Libra owes you)" >