Implement hybrid approach for balance assertions
Balance assertions now use a hybrid architecture where Beancount is the source of truth for validation, while Castle stores metadata for UI convenience. Backend changes: - Add format_balance() function to beancount_format.py for formatting balance directives - Update POST /api/v1/assertions to write balance directive to Beancount first (via Fava) - Store metadata in Castle DB (created_by, tolerance, notes) for UI features - Validate assertions immediately by querying Fava for actual balance Frontend changes: - Update dialog description to explain Beancount validation - Update button tooltip to clarify balance assertions are written to Beancount - Update empty state message to mention Beancount checkpoints Benefits: - Single source of truth (Beancount ledger file) - Automatic validation by Beancount - Best of both worlds: robust validation + friendly UI See misc-docs/BALANCE-ASSERTIONS-HYBRID-APPROACH.md for full documentation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
28832d6bfe
commit
a3c3e44e5f
3 changed files with 69 additions and 6 deletions
36
views_api.py
36
views_api.py
|
|
@ -2455,9 +2455,17 @@ async def api_create_balance_assertion(
|
|||
) -> BalanceAssertion:
|
||||
"""
|
||||
Create a balance assertion for reconciliation (admin only).
|
||||
|
||||
Uses hybrid approach:
|
||||
1. Writes balance assertion to Beancount (via Fava) - source of truth
|
||||
2. Stores metadata in Castle DB for UI convenience (created_by, notes, tolerance)
|
||||
3. Lets Beancount validate the assertion automatically
|
||||
|
||||
The assertion will be checked immediately upon creation.
|
||||
"""
|
||||
from lnbits.settings import settings as lnbits_settings
|
||||
from .fava_client import get_fava_client
|
||||
from .beancount_format import format_balance
|
||||
|
||||
if wallet.wallet.user != lnbits_settings.super_user:
|
||||
raise HTTPException(
|
||||
|
|
@ -2473,10 +2481,32 @@ async def api_create_balance_assertion(
|
|||
detail=f"Account {data.account_id} not found",
|
||||
)
|
||||
|
||||
# Create the assertion
|
||||
assertion_date = data.date or datetime.now()
|
||||
|
||||
# HYBRID APPROACH: Write to Beancount first (source of truth)
|
||||
balance_directive = format_balance(
|
||||
date_val=assertion_date.date() if isinstance(assertion_date, datetime) else assertion_date,
|
||||
account=account.name,
|
||||
amount=data.expected_balance_sats,
|
||||
currency="SATS"
|
||||
)
|
||||
|
||||
# Submit to Fava/Beancount
|
||||
try:
|
||||
fava = get_fava_client()
|
||||
result = await fava.add_entry(balance_directive)
|
||||
logger.info(f"Balance assertion submitted to Fava: {result}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to write balance assertion to Fava: {e}")
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to write balance assertion to Beancount: {str(e)}"
|
||||
)
|
||||
|
||||
# Store metadata in Castle DB for UI convenience
|
||||
assertion = await create_balance_assertion(data, wallet.wallet.user)
|
||||
|
||||
# Check it immediately
|
||||
# Check it immediately (queries Fava for actual balance)
|
||||
try:
|
||||
assertion = await check_balance_assertion(assertion.id)
|
||||
except ValueError as e:
|
||||
|
|
@ -2490,7 +2520,7 @@ async def api_create_balance_assertion(
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.CONFLICT,
|
||||
detail={
|
||||
"message": "Balance assertion failed",
|
||||
"message": "Balance assertion failed (validated by Beancount)",
|
||||
"expected_sats": assertion.expected_balance_sats,
|
||||
"actual_sats": assertion.checked_balance_sats,
|
||||
"difference_sats": assertion.difference_sats,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue