1
0
Fork 0
forked from aiolabs/libra

Rename Castle Accounting extension to Libra

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

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

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

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

View file

@ -1,8 +1,8 @@
# 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)