Removes core balance calculation logic

Migrates balance calculation and inventory tracking to
Fava/Beancount, leveraging Fava's query API for all
accounting calculations. This simplifies the core module
and centralizes accounting logic in Fava.
This commit is contained in:
padreug 2025-11-09 23:13:26 +01:00
parent efc09aa5ce
commit 88ff3821ce
4 changed files with 13 additions and 585 deletions

139
crud.py
View file

@ -29,8 +29,6 @@ from .models import (
)
# Import core accounting logic
from .core.balance import BalanceCalculator, AccountType as CoreAccountType
from .core.inventory import CastleInventory, CastlePosition
from .core.validation import (
ValidationError,
validate_journal_entry,
@ -484,128 +482,6 @@ async def count_journal_entries_by_user_and_account_type(user_id: str, account_t
# ===== BALANCE AND REPORTING =====
async def get_account_balance(account_id: str) -> int:
"""
Calculate account balance using single amount field (Beancount-style).
Only includes entries that are cleared (flag='*'), excludes pending/flagged/voided entries.
For each account type:
- Assets/Expenses: balance = sum of amounts (positive amounts increase, negative decrease)
- Liabilities/Equity/Revenue: balance = -sum of amounts (negative amounts increase, positive decrease)
This works because we store amounts consistently:
- Debit (asset/expense increase) = positive amount
- Credit (liability/equity/revenue increase) = negative amount
"""
result = await db.fetchone(
"""
SELECT COALESCE(SUM(el.amount), 0) as total_amount
FROM entry_lines el
JOIN journal_entries je ON el.journal_entry_id = je.id
WHERE el.account_id = :id
AND je.flag = '*'
""",
{"id": account_id},
)
if not result:
return 0
account = await get_account(account_id)
if not account:
return 0
total_amount = result["total_amount"]
# Use core BalanceCalculator for consistent logic
core_account_type = CoreAccountType(account.account_type.value)
return BalanceCalculator.calculate_account_balance_from_amount(
total_amount, core_account_type
)
async def get_user_balance(user_id: str) -> UserBalance:
"""Get user's balance with the Castle (positive = castle owes user, negative = user owes castle)"""
# Get all user-specific accounts
user_accounts = await db.fetchall(
"SELECT * FROM accounts WHERE user_id = :user_id",
{"user_id": user_id},
Account,
)
# Calculate balances for each account
account_balances = {}
account_inventories = {}
for account in user_accounts:
# Get satoshi balance
balance = await get_account_balance(account.id)
account_balances[account.id] = balance
# Get all entry lines for this account to build inventory
# Only include cleared entries (exclude pending/flagged/voided)
entry_lines = await db.fetchall(
"""
SELECT el.*
FROM entry_lines el
JOIN journal_entries je ON el.journal_entry_id = je.id
WHERE el.account_id = :account_id
AND je.flag = '*'
""",
{"account_id": account.id},
)
# Use BalanceCalculator to build inventory from entry lines
core_account_type = CoreAccountType(account.account_type.value)
inventory = BalanceCalculator.build_inventory_from_entry_lines(
[dict(line) for line in entry_lines],
core_account_type
)
account_inventories[account.id] = inventory
# Use BalanceCalculator to calculate total user balance
accounts_list = [
{"id": acc.id, "account_type": acc.account_type.value}
for acc in user_accounts
]
balance_result = BalanceCalculator.calculate_user_balance(
accounts_list,
account_balances,
account_inventories
)
return UserBalance(
user_id=user_id,
balance=balance_result["balance"],
accounts=user_accounts,
fiat_balances=balance_result["fiat_balances"],
)
async def get_all_user_balances() -> list[UserBalance]:
"""Get balances for all users (used by castle to see who they owe)"""
# Get all user-specific accounts
all_accounts = await db.fetchall(
"SELECT * FROM accounts WHERE user_id IS NOT NULL",
{},
Account,
)
# Get unique user IDs
user_ids = set(account.user_id for account in all_accounts if account.user_id)
# Calculate balance for each user using the refactored function
user_balances = []
for user_id in user_ids:
balance = await get_user_balance(user_id)
# Include users with non-zero balance or fiat balances
if balance.balance != 0 or balance.fiat_balances:
user_balances.append(balance)
return user_balances
async def get_account_transactions(
account_id: str, limit: int = 100
) -> list[tuple[JournalEntry, EntryLine]]:
@ -1013,26 +889,31 @@ async def check_balance_assertion(assertion_id: str) -> BalanceAssertion:
"""
Check a balance assertion by comparing expected vs actual balance.
Updates the assertion with the check results.
Uses Fava/Beancount for balance queries.
"""
from decimal import Decimal
from .fava_client import get_fava_client
assertion = await get_balance_assertion(assertion_id)
if not assertion:
raise ValueError(f"Balance assertion {assertion_id} not found")
# Get actual account balance
# Get actual account balance from Fava
account = await get_account(assertion.account_id)
if not account:
raise ValueError(f"Account {assertion.account_id} not found")
# Calculate balance at the assertion date
actual_balance = await get_account_balance(assertion.account_id)
fava = get_fava_client()
# Get balance from Fava
balance_data = await fava.get_account_balance(account.name)
actual_balance = balance_data["sats"]
# Get fiat balance if needed
actual_fiat_balance = None
if assertion.fiat_currency and account.user_id:
user_balance = await get_user_balance(account.user_id)
actual_fiat_balance = user_balance.fiat_balances.get(assertion.fiat_currency, Decimal("0"))
user_balance_data = await fava.get_user_balance(account.user_id)
actual_fiat_balance = user_balance_data["fiat_balances"].get(assertion.fiat_currency, Decimal("0"))
# Check sats balance
difference_sats = actual_balance - assertion.expected_balance_sats