From b7e4e05469d7fb1b8bea2f9ce34d2d5936d60cae Mon Sep 17 00:00:00 2001 From: padreug Date: Wed, 22 Oct 2025 15:24:50 +0200 Subject: [PATCH] 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. --- crud.py | 37 +++++++++++++++++++++++++++++++ static/js/index.js | 18 ++++++++++++++++ templates/castle/index.html | 43 ++++++++++++++++++++++++++++++++++--- views_api.py | 23 +++++++++++++++++++- 4 files changed, 117 insertions(+), 4 deletions(-) diff --git a/crud.py b/crud.py index 85ba9cc..9097421 100644 --- a/crud.py +++ b/crud.py @@ -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]]: diff --git a/static/js/index.js b/static/js/index.js index b80dd16..816d846 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -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( diff --git a/templates/castle/index.html b/templates/castle/index.html index 9fadea4..a798486 100644 --- a/templates/castle/index.html +++ b/templates/castle/index.html @@ -76,14 +76,17 @@
-
+
{% raw %}{{ formatSats(Math.abs(balance.balance)) }} sats{% endraw %}
-
+
+ {% raw %}{{ balance.balance > 0 ? 'Total you owe' : balance.balance < 0 ? 'Total owed to you' : 'No outstanding balances' }}{% endraw %} +
+
{% raw %}{{ balance.balance >= 0 ? 'Castle owes you' : 'You owe Castle' }}{% endraw %}
+ + + +
Outstanding Balances by User
+ + + + +
+
+ diff --git a/views_api.py b/views_api.py index 466730e..b8ace5b 100644 --- a/views_api.py +++ b/views_api.py @@ -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 =====