Adds fiat currency balances to user balances

Extends user balance information to include fiat currency balances,
calculated based on entry line metadata and account types.

This allows for a more comprehensive view of user balances,
including both satoshi and fiat currency holdings.

Updates the castle index template and API to display fiat balances.
This commit is contained in:
padreug 2025-10-22 16:56:13 +02:00
parent be386f60ef
commit b0705fc24a
5 changed files with 116 additions and 4 deletions

76
crud.py
View file

@ -298,10 +298,43 @@ async def get_user_balance(user_id: str) -> UserBalance:
)
total_balance = 0
fiat_balances = {} # Track fiat balances by currency
for account in user_accounts:
balance = await get_account_balance(account.id)
# Get all entry lines for this account to calculate fiat balances
entry_lines = await db.fetchall(
"SELECT * FROM entry_lines WHERE account_id = :account_id",
{"account_id": account.id},
)
for line in entry_lines:
# Parse metadata to get fiat amounts
metadata = json.loads(line["metadata"]) if line.get("metadata") else {}
fiat_currency = metadata.get("fiat_currency")
fiat_amount = metadata.get("fiat_amount")
if fiat_currency and fiat_amount:
# Initialize currency if not exists
if fiat_currency not in fiat_balances:
fiat_balances[fiat_currency] = 0.0
# Calculate fiat balance based on account type
if account.account_type == AccountType.LIABILITY:
# Liability: credit increases (castle owes more), debit decreases
if line["credit"] > 0:
fiat_balances[fiat_currency] += fiat_amount
elif line["debit"] > 0:
fiat_balances[fiat_currency] -= fiat_amount
elif account.account_type == AccountType.ASSET:
# Asset (receivable): debit increases (user owes more), credit decreases
if line["debit"] > 0:
fiat_balances[fiat_currency] -= fiat_amount
elif line["credit"] > 0:
fiat_balances[fiat_currency] += fiat_amount
# Calculate satoshi balance
# If it's a liability account (castle owes user), it's positive
# If it's an asset account (user owes castle), it's negative
if account.account_type == AccountType.LIABILITY:
@ -314,6 +347,7 @@ async def get_user_balance(user_id: str) -> UserBalance:
user_id=user_id,
balance=total_balance,
accounts=user_accounts,
fiat_balances=fiat_balances,
)
@ -337,17 +371,55 @@ async def get_all_user_balances() -> list[UserBalance]:
user_balances = []
for user_id, accounts in users_dict.items():
total_balance = 0
fiat_balances = {}
for account in accounts:
balance = await get_account_balance(account.id)
# Get all entry lines for this account to calculate fiat balances
entry_lines = await db.fetchall(
"SELECT * FROM entry_lines WHERE account_id = :account_id",
{"account_id": account.id},
)
for line in entry_lines:
# Parse metadata to get fiat amounts
metadata = json.loads(line["metadata"]) if line.get("metadata") else {}
fiat_currency = metadata.get("fiat_currency")
fiat_amount = metadata.get("fiat_amount")
if fiat_currency and fiat_amount:
# Initialize currency if not exists
if fiat_currency not in fiat_balances:
fiat_balances[fiat_currency] = 0.0
# Calculate fiat balance based on account type
if account.account_type == AccountType.LIABILITY:
# Liability: credit increases (castle owes more), debit decreases
if line["credit"] > 0:
fiat_balances[fiat_currency] += fiat_amount
elif line["debit"] > 0:
fiat_balances[fiat_currency] -= fiat_amount
elif account.account_type == AccountType.ASSET:
# Asset (receivable): debit increases (user owes more), credit decreases
if line["debit"] > 0:
fiat_balances[fiat_currency] -= fiat_amount
elif line["credit"] > 0:
fiat_balances[fiat_currency] += fiat_amount
# Calculate satoshi balance
if account.account_type == AccountType.LIABILITY:
total_balance += balance
elif account.account_type == AccountType.ASSET:
total_balance -= balance
if total_balance != 0: # Only include users with non-zero balance
if total_balance != 0 or fiat_balances: # Include users with non-zero balance or fiat balances
user_balances.append(
UserBalance(
user_id=user_id, balance=total_balance, accounts=accounts
user_id=user_id,
balance=total_balance,
accounts=accounts,
fiat_balances=fiat_balances,
)
)

View file

@ -68,6 +68,7 @@ class UserBalance(BaseModel):
user_id: str
balance: int # positive = castle owes user, negative = user owes castle
accounts: list[Account] = []
fiat_balances: dict[str, float] = {} # e.g. {"EUR": 250.0, "USD": 100.0}
class ExpenseEntry(BaseModel):

View file

@ -506,6 +506,14 @@ window.app = Vue.createApp({
formatSats(amount) {
return new Intl.NumberFormat().format(amount)
},
formatFiat(amount, currency) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency,
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(amount)
},
formatDate(dateString) {
return new Date(dateString).toLocaleDateString()
},

View file

@ -79,6 +79,11 @@
<div class="text-h4" :class="balance.balance >= 0 ? 'text-negative' : 'text-positive'">
{% raw %}{{ formatSats(Math.abs(balance.balance)) }} sats{% endraw %}
</div>
<div v-if="balance.fiat_balances && Object.keys(balance.fiat_balances).length > 0" class="text-h6 q-mt-sm">
<span v-for="(amount, currency) in balance.fiat_balances" :key="currency" class="q-mr-md">
{% raw %}{{ formatFiat(Math.abs(amount), currency) }}{% endraw %}
</span>
</div>
<div class="text-subtitle2" v-if="isSuperUser">
{% raw %}{{ balance.balance > 0 ? 'Total you owe' : balance.balance < 0 ? 'Total owed to you' : 'No outstanding balances' }}{% endraw %}
</div>
@ -126,6 +131,11 @@
<div :class="props.row.balance > 0 ? 'text-negative' : 'text-positive'">
{% raw %}{{ formatSats(Math.abs(props.row.balance)) }} sats{% endraw %}
</div>
<div v-if="props.row.fiat_balances && Object.keys(props.row.fiat_balances).length > 0" class="text-caption">
<span v-for="(amount, currency) in props.row.fiat_balances" :key="currency" class="q-mr-sm">
{% raw %}{{ formatFiat(Math.abs(amount), currency) }}{% endraw %}
</span>
</div>
<div class="text-caption text-grey">
{% raw %}{{ props.row.balance > 0 ? 'You owe' : 'Owes you' }}{% endraw %}
</div>
@ -316,7 +326,14 @@
<div class="text-h6 q-mb-md">Pay Balance</div>
<div v-if="balance" class="q-mb-md">
Amount owed: <strong>{% raw %}{{ formatSats(Math.abs(balance.balance)) }}{% endraw %} sats</strong>
<div>
Amount owed: <strong>{% raw %}{{ formatSats(Math.abs(balance.balance)) }}{% endraw %} sats</strong>
</div>
<div v-if="balance.fiat_balances && Object.keys(balance.fiat_balances).length > 0" class="text-body2 q-mt-xs">
<span v-for="(amount, currency) in balance.fiat_balances" :key="currency" class="q-mr-md">
<strong>{% raw %}{{ formatFiat(Math.abs(amount), currency) }}{% endraw %}</strong>
</span>
</div>
</div>
<div v-if="!payDialog.paymentRequest">

View file

@ -420,9 +420,23 @@ async def api_get_my_balance(
if wallet.wallet.user == lnbits_settings.super_user:
all_balances = await get_all_user_balances()
total_owed = sum(b.balance for b in all_balances if b.balance > 0)
# Aggregate fiat balances from all users
total_fiat_balances = {}
for user_balance in all_balances:
for currency, amount in user_balance.fiat_balances.items():
if currency not in total_fiat_balances:
total_fiat_balances[currency] = 0.0
# Only add positive balances (what castle owes)
if amount > 0:
total_fiat_balances[currency] += amount
# Return as castle's "balance" - positive means castle owes money
return UserBalance(
user_id=wallet.wallet.user, balance=total_owed, accounts=[]
user_id=wallet.wallet.user,
balance=total_owed,
accounts=[],
fiat_balances=total_fiat_balances,
)
# For regular users, show their individual balance