Rename Castle Accounting extension to Libra

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

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

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

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

View file

@ -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

View file

@ -195,13 +195,13 @@ id="toc-professional-assessment">Professional Assessment</a></li>
<h1 id="accounting-analysis-net-settlement-entry-pattern">Accounting
Analysis: Net Settlement Entry Pattern</h1>
<p><strong>Date</strong>: 2025-01-12 <strong>Prepared By</strong>:
Senior Accounting Review <strong>Subject</strong>: Castle Extension -
Senior Accounting Review <strong>Subject</strong>: Libra Extension -
Lightning Payment Settlement Entries <strong>Status</strong>: Technical
Review</p>
<hr />
<h2 id="executive-summary">Executive Summary</h2>
<p>This document provides a professional accounting assessment of
Castles net settlement entry pattern used for recording Lightning
Libras 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</p>
<hr />
<h2 id="background-the-technical-challenge">Background: The Technical
Challenge</h2>
<p>Castle operates as a Lightning Network-integrated accounting system
<p>Libra operates as a Lightning Network-integrated accounting system
for collectives (co-living spaces, makerspaces). It faces a unique
accounting challenge:</p>
<p><strong>Scenario</strong>: User creates a receivable in EUR (e.g.,
@ -223,7 +223,7 @@ accounting challenge:</p>
<p><strong>Challenge</strong>: 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</p>
<hr />
<h2 id="current-implementation">Current Implementation</h2>
@ -231,7 +231,7 @@ double-entry balance</p>
<pre class="beancount"><code>; Step 1: Receivable Created
2025-11-12 * &quot;room (200.00 EUR)&quot; #receivable-entry
user-id: &quot;375ec158&quot;
source: &quot;castle-api&quot;
source: &quot;libra-api&quot;
sats-amount: &quot;225033&quot;
Assets:Receivable:User-375ec158 200.00 EUR
sats-equivalent: &quot;225033&quot;
@ -344,7 +344,7 @@ class="sourceCode sql"><code class="sourceCode sql"><span id="cb8-1"><a href="#c
payment-hash: &quot;8d080ec4...&quot;
Assets:Receivable:User-375ec158 -200.00 EUR
; No sats-equivalent needed here</code></pre>
<p><strong>Option B - Use EUR positions with metadata</strong> (Castles
<p><strong>Option B - Use EUR positions with metadata</strong> (Libras
current approach):</p>
<pre class="beancount"><code>Assets:Bitcoin:Lightning 200.00 EUR
sats-received: &quot;225033&quot;
@ -452,8 +452,8 @@ OR payable)</li>
(receivable AND payable)</li>
</ul>
<p><strong>When Net Settlement is Appropriate</strong>:</p>
<pre><code>User owes Castle: 555.00 EUR (receivable)
Castle owes User: 38.00 EUR (payable)
<pre><code>User owes Libra: 555.00 EUR (receivable)
Libra owes User: 38.00 EUR (payable)
Net amount due: 517.00 EUR (true settlement)</code></pre>
<p>Proper three-posting entry:</p>
<pre class="beancount"><code>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 ✓</code></pre>
<p><strong>When Two Postings Suffice</strong>:</p>
<pre><code>User owes Castle: 200.00 EUR (receivable)
Castle owes User: 0.00 EUR (no payable)
<pre><code>User owes Libra: 200.00 EUR (receivable)
Libra owes User: 0.00 EUR (no payable)
Amount due: 200.00 EUR (simple payment)</code></pre>
<p>Simpler two-posting entry:</p>
<pre class="beancount"><code>Assets:Bitcoin:Lightning 225033 SATS @@ 200.00 EUR
@ -515,7 +515,7 @@ positions - ❌ Requires metadata parsing for SATS balances</p>
id="approach-3-true-net-settlement-when-both-obligations-exist">Approach
3: True Net Settlement (When Both Obligations Exist)</h3>
<pre class="beancount"><code>2025-11-12 * &quot;Net settlement via Lightning&quot;
; 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: &quot;565251&quot;
Assets:Receivable:User-375ec158 -555.00 EUR
@ -570,7 +570,7 @@ Method</h4>
<p><strong>Decision Required</strong>: Select either position-based OR
metadata-based satoshi tracking.</p>
<p><strong>Option A - Keep Metadata Approach</strong> (recommended for
Castle):</p>
Libra):</p>
<div class="sourceCode" id="cb25"><pre
class="sourceCode python"><code class="sourceCode python"><span id="cb25-1"><a href="#cb25-1" aria-hidden="true" tabindex="-1"></a><span class="co"># In format_net_settlement_entry()</span></span>
<span id="cb25-2"><a href="#cb25-2" aria-hidden="true" tabindex="-1"></a>postings <span class="op">=</span> [</span>
@ -604,7 +604,7 @@ class="sourceCode python"><code class="sourceCode python"><span id="cb26-1"><a h
<span id="cb26-12"><a href="#cb26-12" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb26-13"><a href="#cb26-13" aria-hidden="true" tabindex="-1"></a>]</span></code></pre></div>
<p><strong>Recommendation</strong>: Choose Option A (metadata) for
consistency with Castles architecture.</p>
consistency with Libras architecture.</p>
<hr />
<h4 id="rename-function-for-clarity">1.3 Rename Function for
Clarity</h4>
@ -713,7 +713,7 @@ class="sourceCode python"><code class="sourceCode python"><span id="cb28-1"><a h
<span id="cb28-45"><a href="#cb28-45" aria-hidden="true" tabindex="-1"></a> payment_hash<span class="op">=</span>payment_hash</span>
<span id="cb28-46"><a href="#cb28-46" aria-hidden="true" tabindex="-1"></a> )</span>
<span id="cb28-47"><a href="#cb28-47" aria-hidden="true" tabindex="-1"></a> <span class="cf">else</span>:</span>
<span id="cb28-48"><a href="#cb28-48" aria-hidden="true" tabindex="-1"></a> <span class="co"># PAYABLE PAYMENT: Castle paying user (different flow)</span></span>
<span id="cb28-48"><a href="#cb28-48" aria-hidden="true" tabindex="-1"></a> <span class="co"># PAYABLE PAYMENT: Libra paying user (different flow)</span></span>
<span id="cb28-49"><a href="#cb28-49" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="cf">await</span> format_payable_payment_entry(...)</span></code></pre></div>
<hr />
<h3 id="priority-3-long-term-architectural-decisions">Priority 3:
@ -742,7 +742,7 @@ architectures:</p>
<p><strong>Recommendation</strong>: 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</p>
Aligns with current Libra metadata approach</p>
<hr />
<h4 id="consider-separate-ledger-for-cryptocurrency-holdings">3.2
Consider Separate Ledger for Cryptocurrency Holdings</h4>
@ -754,7 +754,7 @@ from fiat accounting</p>
Assets:Receivable:User-375ec158 -200.00 EUR</code></pre>
<p><strong>Cryptocurrency Sub-Ledger</strong> (SATS-denominated):</p>
<pre class="beancount"><code>2025-11-12 * &quot;Lightning payment received&quot;
Assets:Bitcoin:Lightning:Castle 225033 SATS
Assets:Bitcoin:Lightning:Libra 225033 SATS
Assets:Bitcoin:Custody:User-375ec 225033 SATS</code></pre>
<p><strong>Benefits</strong>: - ✅ Clean separation of concerns - ✅
Cryptocurrency movements tracked independently - ✅ Fiat accounting
@ -902,7 +902,7 @@ Entry balances</p>
<p><strong>Is this “best practice” accounting?</strong>
<strong>No</strong>, this implementation deviates from traditional
accounting standards in several ways.</p>
<p><strong>Is it acceptable for Castles use case?</strong> <strong>Yes,
<p><strong>Is it acceptable for Libras use case?</strong> <strong>Yes,
with modifications</strong>, its a reasonable pragmatic solution for a
novel problem (cryptocurrency payments of fiat debts).</p>
<p><strong>Critical improvements needed</strong>: 1. ✅ Remove
@ -912,7 +912,7 @@ Separate payment vs. settlement logic (accuracy and clarity)</p>
<p><strong>The fundamental challenge</strong>: Traditional accounting
wasnt designed for this scenario. There is no established “standard”
for recording cryptocurrency payments of fiat-denominated receivables.
Castles approach is functional, but should be refined to align better
Libras approach is functional, but should be refined to align better
with accounting principles where possible.</p>
<h3 id="next-steps">Next Steps</h3>
<ol type="1">
@ -935,7 +935,7 @@ Characteristics of Accounting Information</li>
<li><strong>ASC 105-10-05</strong>: Substance Over Form</li>
<li><strong>Beancount Documentation</strong>:
http://furius.ca/beancount/doc/index</li>
<li><strong>Castle Extension</strong>:
<li><strong>Libra Extension</strong>:
<code>docs/SATS-EQUIVALENT-METADATA.md</code></li>
<li><strong>BQL Analysis</strong>:
<code>docs/BQL-BALANCE-QUERIES.md</code></li>
@ -948,6 +948,6 @@ implemented</p>
<p><em>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
Castles payment recording system.</em></p>
Libras payment recording system.</em></p>
</body>
</html>

View file

@ -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.*

View file

@ -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)

View file

@ -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):

View file

@ -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 '.'

View file

@ -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
```

View file

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

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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'
}
}