Fetches account balances from Fava/Beancount

Refactors account balance retrieval to fetch data from Fava/Beancount
for improved accounting accuracy.

Updates user balance retrieval to use Fava/Beancount data source.

Updates Castle settings ledger slug name.
This commit is contained in:
padreug 2025-11-09 22:47:04 +01:00
parent ff27f7ba01
commit a88d7b4ea0
2 changed files with 57 additions and 21 deletions

View file

@ -124,7 +124,7 @@ class CastleSettings(BaseModel):
# Fava/Beancount integration - ALL accounting is done via Fava # Fava/Beancount integration - ALL accounting is done via Fava
fava_url: str = "http://localhost:3333" # Base URL of Fava server fava_url: str = "http://localhost:3333" # Base URL of Fava server
fava_ledger_slug: str = "castle-accounting" # Ledger identifier in Fava URL fava_ledger_slug: str = "castle-ledger" # Ledger identifier in Fava URL
fava_timeout: float = 10.0 # Request timeout in seconds fava_timeout: float = 10.0 # Request timeout in seconds
updated_at: datetime = Field(default_factory=lambda: datetime.now()) updated_at: datetime = Field(default_factory=lambda: datetime.now())

View file

@ -235,9 +235,23 @@ async def api_get_account(account_id: str) -> Account:
@castle_api_router.get("/api/v1/accounts/{account_id}/balance") @castle_api_router.get("/api/v1/accounts/{account_id}/balance")
async def api_get_account_balance(account_id: str) -> dict: async def api_get_account_balance(account_id: str) -> dict:
"""Get account balance""" """Get account balance from Fava/Beancount"""
balance = await get_account_balance(account_id) from .fava_client import get_fava_client
return {"account_id": account_id, "balance": balance}
# Get account to retrieve its name
account = await get_account(account_id)
if not account:
raise HTTPException(status_code=404, detail="Account not found")
# Query Fava for balance
fava = get_fava_client()
balance_data = await fava.get_account_balance(account.name)
return {
"account_id": account_id,
"balance": balance_data["sats"], # Balance in satoshis
"positions": balance_data["positions"] # Full Beancount positions with cost basis
}
@castle_api_router.get("/api/v1/accounts/{account_id}/transactions") @castle_api_router.get("/api/v1/accounts/{account_id}/transactions")
@ -683,25 +697,28 @@ async def api_create_revenue_entry(
async def api_get_my_balance( async def api_get_my_balance(
wallet: WalletTypeInfo = Depends(require_invoice_key), wallet: WalletTypeInfo = Depends(require_invoice_key),
) -> UserBalance: ) -> UserBalance:
"""Get current user's balance with the Castle""" """Get current user's balance with the Castle (from Fava/Beancount)"""
from lnbits.settings import settings as lnbits_settings from lnbits.settings import settings as lnbits_settings
from .fava_client import get_fava_client
fava = get_fava_client()
# If super user, show total castle position # If super user, show total castle position
if wallet.wallet.user == lnbits_settings.super_user: if wallet.wallet.user == lnbits_settings.super_user:
all_balances = await get_all_user_balances() all_balances = await fava.get_all_user_balances()
# Calculate total: # Calculate total:
# Positive balances = Castle owes users (liabilities) # Positive balances = Castle owes users (liabilities)
# Negative balances = Users owe Castle (receivables) # Negative balances = Users owe Castle (receivables)
# Net: positive means castle owes, negative means castle is owed # Net: positive means castle owes, negative means castle is owed
total_liabilities = sum(b.balance for b in all_balances if b.balance > 0) total_liabilities = sum(b["balance"] for b in all_balances if b["balance"] > 0)
total_receivables = sum(abs(b.balance) for b in all_balances if b.balance < 0) total_receivables = sum(abs(b["balance"]) for b in all_balances if b["balance"] < 0)
net_balance = total_liabilities - total_receivables net_balance = total_liabilities - total_receivables
# Aggregate fiat balances from all users # Aggregate fiat balances from all users
total_fiat_balances = {} total_fiat_balances = {}
for user_balance in all_balances: for user_balance in all_balances:
for currency, amount in user_balance.fiat_balances.items(): for currency, amount in user_balance["fiat_balances"].items():
if currency not in total_fiat_balances: if currency not in total_fiat_balances:
total_fiat_balances[currency] = Decimal("0") total_fiat_balances[currency] = Decimal("0")
# Add all balances (positive and negative) # Add all balances (positive and negative)
@ -715,37 +732,56 @@ async def api_get_my_balance(
fiat_balances=total_fiat_balances, fiat_balances=total_fiat_balances,
) )
# For regular users, show their individual balance # For regular users, show their individual balance from Fava
return await get_user_balance(wallet.wallet.user) balance_data = await fava.get_user_balance(wallet.wallet.user)
return UserBalance(
user_id=wallet.wallet.user,
balance=balance_data["balance"],
accounts=[], # Could populate from balance_data["accounts"] if needed
fiat_balances=balance_data["fiat_balances"],
)
@castle_api_router.get("/api/v1/balance/{user_id}") @castle_api_router.get("/api/v1/balance/{user_id}")
async def api_get_user_balance(user_id: str) -> UserBalance: async def api_get_user_balance(user_id: str) -> UserBalance:
"""Get a specific user's balance with the Castle""" """Get a specific user's balance with the Castle (from Fava/Beancount)"""
return await get_user_balance(user_id) from .fava_client import get_fava_client
fava = get_fava_client()
balance_data = await fava.get_user_balance(user_id)
return UserBalance(
user_id=user_id,
balance=balance_data["balance"],
accounts=[],
fiat_balances=balance_data["fiat_balances"],
)
@castle_api_router.get("/api/v1/balances/all") @castle_api_router.get("/api/v1/balances/all")
async def api_get_all_balances( async def api_get_all_balances(
wallet: WalletTypeInfo = Depends(require_admin_key), wallet: WalletTypeInfo = Depends(require_admin_key),
) -> list[dict]: ) -> list[dict]:
"""Get all user balances (admin/super user only)""" """Get all user balances (admin/super user only) from Fava/Beancount"""
from lnbits.core.crud.users import get_user from lnbits.core.crud.users import get_user
from .fava_client import get_fava_client
balances = await get_all_user_balances() fava = get_fava_client()
balances = await fava.get_all_user_balances()
# Enrich with username information # Enrich with username information
result = [] result = []
for balance in balances: for balance in balances:
user = await get_user(balance.user_id) user = await get_user(balance["user_id"])
username = user.username if user and user.username else balance.user_id[:16] + "..." username = user.username if user and user.username else balance["user_id"][:16] + "..."
result.append({ result.append({
"user_id": balance.user_id, "user_id": balance["user_id"],
"username": username, "username": username,
"balance": balance.balance, "balance": balance["balance"],
"fiat_balances": balance.fiat_balances, "fiat_balances": balance["fiat_balances"],
"accounts": [acc.dict() for acc in balance.accounts], "accounts": balance["accounts"],
}) })
return result return result