Reverts balance perspective to castle's view

Changes the displayed balance perspective to reflect the castle's point of view instead of the user's.

This involves:
- Displaying balances as positive when the user owes the castle
- Displaying balances as negative when the castle owes the user.

This change affects how balances are calculated and displayed in both the backend logic and the frontend templates.
This commit is contained in:
padreug 2025-11-10 01:40:09 +01:00
parent 0f24833e02
commit 5c1c7b1b05
2 changed files with 39 additions and 41 deletions

View file

@ -172,18 +172,18 @@ class FavaClient:
async def get_user_balance(self, user_id: str) -> Dict[str, Any]:
"""
Get user's total balance (what castle owes user).
Get user's balance from castle's perspective.
Aggregates:
- Liabilities:Payable:User-{user_id} (negative balance = castle owes)
- Assets:Receivable:User-{user_id} (positive balance = user owes)
- Liabilities:Payable:User-{user_id} (negative = castle owes user)
- Assets:Receivable:User-{user_id} (positive = user owes castle)
Args:
user_id: User ID
Returns:
{
"balance": int (sats, positive = castle owes user),
"balance": int (sats, positive = user owes castle, negative = castle owes user),
"fiat_balances": {"EUR": Decimal("100.50")},
"accounts": [list of account dicts with balances]
}
@ -228,12 +228,11 @@ class FavaClient:
for cost_str, amount in sats_positions.items():
amount_int = int(amount)
# For user balance perspective, negate Beancount balance
# - Payable (Liability): negative in Beancount → positive (castle owes user)
# - Receivable (Asset): positive in Beancount → negative (user owes castle)
adjusted_amount = -amount_int
total_sats += adjusted_amount
account_balance["sats"] += adjusted_amount
# Use Beancount balance as-is (castle's perspective)
# - Receivable (Asset): positive = user owes castle
# - Payable (Liability): negative = castle owes user
total_sats += amount_int
account_balance["sats"] += amount_int
# Extract fiat amount from cost basis
# Format: "100.00 EUR" or "{100.00 EUR}"
@ -248,22 +247,19 @@ class FavaClient:
if fiat_currency not in fiat_balances:
fiat_balances[fiat_currency] = Decimal(0)
# Apply same sign adjustment to fiat
# Cost basis is always positive, derive sign from amount
# Apply same sign as sats amount
if amount_int < 0:
fiat_amount = -fiat_amount
adjusted_fiat = -fiat_amount
fiat_balances[fiat_currency] += adjusted_fiat
fiat_balances[fiat_currency] += fiat_amount
except (ValueError, IndexError):
logger.warning(f"Could not parse cost basis: {cost_str}")
elif isinstance(sats_positions, (int, float)):
# Simple number (no cost basis)
amount_int = int(sats_positions)
# Negate Beancount balance for user perspective
adjusted_amount = -amount_int
total_sats += adjusted_amount
account_balance["sats"] += adjusted_amount
# Use Beancount balance as-is
total_sats += amount_int
account_balance["sats"] += amount_int
accounts.append(account_balance)
@ -341,31 +337,33 @@ class FavaClient:
continue
import re
# Extract SATS amount
# Extract SATS amount (with sign)
sats_match = re.match(r'^(-?\d+)\s+SATS', amount_str)
if sats_match:
sats_amount = int(sats_match.group(1))
# Negate Beancount balance for user perspective
# Payable (liability): negative in Beancount = castle owes user (positive for user)
# Receivable (asset): positive in Beancount = user owes castle (negative for user)
adjusted_amount = -sats_amount
user_data[user_id]["balance"] += adjusted_amount
# For admin/castle view, use Beancount amounts as-is:
# Receivable (asset): positive in Beancount = user owes castle (positive)
# Payable (liability): negative in Beancount = castle owes user (negative)
user_data[user_id]["balance"] += sats_amount
# Extract fiat from cost syntax: {33.33 EUR, ...}
cost_match = re.search(r'\{([\d.]+)\s+([A-Z]+)', amount_str)
if cost_match:
fiat_amount_raw = Decimal(cost_match.group(1))
fiat_amount_unsigned = Decimal(cost_match.group(1))
fiat_currency = cost_match.group(2)
if fiat_currency not in user_data[user_id]["fiat_balances"]:
user_data[user_id]["fiat_balances"][fiat_currency] = Decimal(0)
# Apply same sign logic as sats
if "-" in amount_str:
fiat_amount_raw = -fiat_amount_raw
adjusted_fiat = -fiat_amount_raw
user_data[user_id]["fiat_balances"][fiat_currency] += adjusted_fiat
# Apply the same sign as the SATS amount
# If SATS is negative, fiat should be negative too
if sats_match:
sats_amount_for_sign = int(sats_match.group(1))
if sats_amount_for_sign < 0:
fiat_amount_unsigned = -fiat_amount_unsigned
user_data[user_id]["fiat_balances"][fiat_currency] += fiat_amount_unsigned
return list(user_data.values())

View file

@ -182,7 +182,7 @@
</template>
<template v-slot:body-cell-balance="props">
<q-td :props="props">
<div :class="props.row.balance > 0 ? 'text-negative' : 'text-positive'">
<div :class="props.row.balance > 0 ? 'text-positive' : 'text-negative'">
{% 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">
@ -191,15 +191,15 @@
</span>
</div>
<div class="text-caption text-grey">
{% raw %}{{ props.row.balance > 0 ? 'You owe' : 'Owes you' }}{% endraw %}
{% raw %}{{ props.row.balance > 0 ? 'Owes you' : 'You owe' }}{% endraw %}
</div>
</q-td>
</template>
<template v-slot:body-cell-actions="props">
<q-td :props="props">
<!-- User owes Castle (negative balance) - Castle receives payment -->
<!-- User owes Castle (positive balance) - Castle receives payment -->
<q-btn
v-if="props.row.balance < 0"
v-if="props.row.balance > 0"
flat
dense
size="sm"
@ -209,9 +209,9 @@
>
<q-tooltip>Settle receivable (user pays castle)</q-tooltip>
</q-btn>
<!-- Castle owes User (positive balance) - Castle pays user -->
<!-- Castle owes User (negative balance) - Castle pays user -->
<q-btn
v-if="props.row.balance > 0"
v-if="props.row.balance < 0"
flat
dense
size="sm"
@ -241,7 +241,7 @@
</div>
</div>
<div v-if="balance !== null">
<div class="text-h4" :class="isSuperUser ? (balance.balance >= 0 ? 'text-negative' : 'text-positive') : (balance.balance >= 0 ? 'text-positive' : 'text-negative')">
<div class="text-h4" :class="isSuperUser ? (balance.balance >= 0 ? 'text-positive' : 'text-negative') : (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">
@ -250,21 +250,21 @@
</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 %}
{% raw %}{{ balance.balance > 0 ? 'Total owed to you' : balance.balance < 0 ? 'Total you owe' : 'No outstanding balances' }}{% endraw %}
</div>
<div class="text-subtitle2" v-else>
{% raw %}{{ balance.balance >= 0 ? 'Castle owes you' : 'You owe Castle' }}{% endraw %}
{% raw %}{{ balance.balance >= 0 ? 'You owe Castle' : 'Castle owes you' }}{% endraw %}
</div>
<div class="q-mt-md q-gutter-sm">
<q-btn
v-if="balance.balance < 0 && !isSuperUser"
v-if="balance.balance > 0 && !isSuperUser"
color="primary"
@click="showPayBalanceDialog"
>
Pay Balance
</q-btn>
<q-btn
v-if="balance.balance > 0 && !isSuperUser"
v-if="balance.balance < 0 && !isSuperUser"
color="secondary"
@click="showManualPaymentDialog"
>