Adds super user balance overview

Implements functionality for super users to view a breakdown of outstanding balances for all users.

This includes:
- Adding an API endpoint to fetch all user balances.
- Updating the frontend to display these balances in a table, accessible only to super users.
- Modifying the balance calculation for the current user to reflect the total owed by or to the castle for super users.

This provides super users with a comprehensive view of the castle's financial position.
This commit is contained in:
padreug 2025-10-22 15:24:50 +02:00
parent cb7e4ee555
commit b7e4e05469
4 changed files with 117 additions and 4 deletions

37
crud.py
View file

@ -317,6 +317,43 @@ async def get_user_balance(user_id: str) -> UserBalance:
)
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,
)
# Group by user_id
users_dict = {}
for account in all_accounts:
if account.user_id not in users_dict:
users_dict[account.user_id] = []
users_dict[account.user_id].append(account)
# Calculate balance for each user
user_balances = []
for user_id, accounts in users_dict.items():
total_balance = 0
for account in accounts:
balance = await get_account_balance(account.id)
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
user_balances.append(
UserBalance(
user_id=user_id, balance=total_balance, accounts=accounts
)
)
return user_balances
async def get_account_transactions(
account_id: str, limit: int = 100
) -> list[tuple[JournalEntry, EntryLine]]:

View file

@ -8,6 +8,7 @@ window.app = Vue.createApp({
data() {
return {
balance: null,
allUserBalances: [],
transactions: [],
accounts: [],
currencies: [],
@ -71,10 +72,27 @@ window.app = Vue.createApp({
this.g.user.wallets[0].inkey
)
this.balance = response.data
// If super user, also load all user balances
if (this.isSuperUser) {
await this.loadAllUserBalances()
}
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
async loadAllUserBalances() {
try {
const response = await LNbits.api.request(
'GET',
'/castle/api/v1/balances/all',
this.g.user.wallets[0].adminkey
)
this.allUserBalances = response.data
} catch (error) {
console.error('Error loading all user balances:', error)
}
},
async loadTransactions() {
try {
const response = await LNbits.api.request(

View file

@ -76,14 +76,17 @@
</div>
</div>
<div v-if="balance !== null">
<div class="text-h4" :class="balance.balance >= 0 ? 'text-positive' : 'text-negative'">
<div class="text-h4" :class="balance.balance >= 0 ? 'text-negative' : 'text-positive'">
{% raw %}{{ formatSats(Math.abs(balance.balance)) }} sats{% endraw %}
</div>
<div class="text-subtitle2">
<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>
<div class="text-subtitle2" v-else>
{% raw %}{{ balance.balance >= 0 ? 'Castle owes you' : 'You owe Castle' }}{% endraw %}
</div>
<q-btn
v-if="balance.balance < 0"
v-if="balance.balance < 0 && !isSuperUser"
color="primary"
class="q-mt-md"
@click="showPayBalanceDialog"
@ -98,6 +101,40 @@
</q-card-section>
</q-card>
<!-- User Balances Breakdown (Super User Only) -->
<q-card v-if="isSuperUser && allUserBalances.length > 0">
<q-card-section>
<h6 class="q-my-none q-mb-md">Outstanding Balances by User</h6>
<q-table
flat
:rows="allUserBalances"
:columns="[
{name: 'user', label: 'User ID', field: 'user_id', align: 'left'},
{name: 'balance', label: 'Amount Owed', field: 'balance', align: 'right'}
]"
row-key="user_id"
hide-pagination
:rows-per-page-options="[0]"
>
<template v-slot:body-cell-user="props">
<q-td :props="props">
<div class="text-caption">{% raw %}{{ props.row.user_id.substring(0, 16) }}...{% endraw %}</div>
</q-td>
</template>
<template v-slot:body-cell-balance="props">
<q-td :props="props">
<div :class="props.row.balance > 0 ? 'text-negative' : 'text-positive'">
{% raw %}{{ formatSats(Math.abs(props.row.balance)) }} sats{% endraw %}
</div>
<div class="text-caption text-grey">
{% raw %}{{ props.row.balance > 0 ? 'You owe' : 'Owes you' }}{% endraw %}
</div>
</q-td>
</template>
</q-table>
</q-card-section>
</q-card>
<!-- Quick Actions -->
<q-card>
<q-card-section>

View file

@ -19,6 +19,7 @@ from .crud import (
get_account_transactions,
get_all_accounts,
get_all_journal_entries,
get_all_user_balances,
get_journal_entries_by_user,
get_journal_entry,
get_or_create_user_account,
@ -382,7 +383,19 @@ async def api_get_my_balance(
wallet: WalletTypeInfo = Depends(require_invoice_key),
) -> UserBalance:
"""Get current user's balance with the Castle"""
return await get_user_balance(wallet.wallet.id)
from lnbits.settings import settings as lnbits_settings
# If super user, show total castle liabilities (what castle owes to all users)
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)
# Return as castle's "balance" - positive means castle owes money
return UserBalance(
user_id=wallet.wallet.user, balance=total_owed, accounts=[]
)
# For regular users, show their individual balance
return await get_user_balance(wallet.wallet.user)
@castle_api_router.get("/api/v1/balance/{user_id}")
@ -391,6 +404,14 @@ async def api_get_user_balance(user_id: str) -> UserBalance:
return await get_user_balance(user_id)
@castle_api_router.get("/api/v1/balances/all")
async def api_get_all_balances(
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> list[UserBalance]:
"""Get all user balances (admin/super user only)"""
return await get_all_user_balances()
# ===== PAYMENT ENDPOINTS =====