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:
parent
be386f60ef
commit
b0705fc24a
5 changed files with 116 additions and 4 deletions
76
crud.py
76
crud.py
|
|
@ -298,10 +298,43 @@ async def get_user_balance(user_id: str) -> UserBalance:
|
||||||
)
|
)
|
||||||
|
|
||||||
total_balance = 0
|
total_balance = 0
|
||||||
|
fiat_balances = {} # Track fiat balances by currency
|
||||||
|
|
||||||
for account in user_accounts:
|
for account in user_accounts:
|
||||||
balance = await get_account_balance(account.id)
|
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 a liability account (castle owes user), it's positive
|
||||||
# If it's an asset account (user owes castle), it's negative
|
# If it's an asset account (user owes castle), it's negative
|
||||||
if account.account_type == AccountType.LIABILITY:
|
if account.account_type == AccountType.LIABILITY:
|
||||||
|
|
@ -314,6 +347,7 @@ async def get_user_balance(user_id: str) -> UserBalance:
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
balance=total_balance,
|
balance=total_balance,
|
||||||
accounts=user_accounts,
|
accounts=user_accounts,
|
||||||
|
fiat_balances=fiat_balances,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -337,17 +371,55 @@ async def get_all_user_balances() -> list[UserBalance]:
|
||||||
user_balances = []
|
user_balances = []
|
||||||
for user_id, accounts in users_dict.items():
|
for user_id, accounts in users_dict.items():
|
||||||
total_balance = 0
|
total_balance = 0
|
||||||
|
fiat_balances = {}
|
||||||
|
|
||||||
for account in accounts:
|
for account in accounts:
|
||||||
balance = await get_account_balance(account.id)
|
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:
|
if account.account_type == AccountType.LIABILITY:
|
||||||
total_balance += balance
|
total_balance += balance
|
||||||
elif account.account_type == AccountType.ASSET:
|
elif account.account_type == AccountType.ASSET:
|
||||||
total_balance -= balance
|
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(
|
user_balances.append(
|
||||||
UserBalance(
|
UserBalance(
|
||||||
user_id=user_id, balance=total_balance, accounts=accounts
|
user_id=user_id,
|
||||||
|
balance=total_balance,
|
||||||
|
accounts=accounts,
|
||||||
|
fiat_balances=fiat_balances,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ class UserBalance(BaseModel):
|
||||||
user_id: str
|
user_id: str
|
||||||
balance: int # positive = castle owes user, negative = user owes castle
|
balance: int # positive = castle owes user, negative = user owes castle
|
||||||
accounts: list[Account] = []
|
accounts: list[Account] = []
|
||||||
|
fiat_balances: dict[str, float] = {} # e.g. {"EUR": 250.0, "USD": 100.0}
|
||||||
|
|
||||||
|
|
||||||
class ExpenseEntry(BaseModel):
|
class ExpenseEntry(BaseModel):
|
||||||
|
|
|
||||||
|
|
@ -506,6 +506,14 @@ window.app = Vue.createApp({
|
||||||
formatSats(amount) {
|
formatSats(amount) {
|
||||||
return new Intl.NumberFormat().format(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) {
|
formatDate(dateString) {
|
||||||
return new Date(dateString).toLocaleDateString()
|
return new Date(dateString).toLocaleDateString()
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,11 @@
|
||||||
<div class="text-h4" :class="balance.balance >= 0 ? 'text-negative' : 'text-positive'">
|
<div class="text-h4" :class="balance.balance >= 0 ? 'text-negative' : 'text-positive'">
|
||||||
{% raw %}{{ formatSats(Math.abs(balance.balance)) }} sats{% endraw %}
|
{% raw %}{{ formatSats(Math.abs(balance.balance)) }} sats{% endraw %}
|
||||||
</div>
|
</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">
|
<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 %}
|
{% raw %}{{ balance.balance > 0 ? 'Total you owe' : balance.balance < 0 ? 'Total owed to you' : 'No outstanding balances' }}{% endraw %}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -126,6 +131,11 @@
|
||||||
<div :class="props.row.balance > 0 ? 'text-negative' : 'text-positive'">
|
<div :class="props.row.balance > 0 ? 'text-negative' : 'text-positive'">
|
||||||
{% raw %}{{ formatSats(Math.abs(props.row.balance)) }} sats{% endraw %}
|
{% raw %}{{ formatSats(Math.abs(props.row.balance)) }} sats{% endraw %}
|
||||||
</div>
|
</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">
|
<div class="text-caption text-grey">
|
||||||
{% raw %}{{ props.row.balance > 0 ? 'You owe' : 'Owes you' }}{% endraw %}
|
{% raw %}{{ props.row.balance > 0 ? 'You owe' : 'Owes you' }}{% endraw %}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -316,7 +326,14 @@
|
||||||
<div class="text-h6 q-mb-md">Pay Balance</div>
|
<div class="text-h6 q-mb-md">Pay Balance</div>
|
||||||
|
|
||||||
<div v-if="balance" class="q-mb-md">
|
<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>
|
||||||
|
|
||||||
<div v-if="!payDialog.paymentRequest">
|
<div v-if="!payDialog.paymentRequest">
|
||||||
|
|
|
||||||
16
views_api.py
16
views_api.py
|
|
@ -420,9 +420,23 @@ async def api_get_my_balance(
|
||||||
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 get_all_user_balances()
|
||||||
total_owed = sum(b.balance for b in all_balances if b.balance > 0)
|
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 as castle's "balance" - positive means castle owes money
|
||||||
return UserBalance(
|
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
|
# For regular users, show their individual balance
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue