Add BQL-based report endpoints for expenses and contributions

New endpoints:
- GET /api/v1/reports/expenses - Expense summary by account or month
- GET /api/v1/reports/contributions - User contribution totals

New FavaClient methods:
- get_expense_summary_bql() - Aggregates expenses with date filtering
- get_user_contributions_bql() - Aggregates user expense submissions

Both use sum(weight) for efficient SATS aggregation from price notation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
padreug 2025-12-15 01:15:29 +01:00
parent addf4cd05f
commit 7dabe8700d
2 changed files with 291 additions and 0 deletions

View file

@ -2215,6 +2215,135 @@ async def api_get_castle_users(
return users
@castle_api_router.get("/api/v1/reports/expenses")
async def api_expense_report(
start_date: Optional[str] = None,
end_date: Optional[str] = None,
group_by: str = "account",
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> dict:
"""
Get expense summary report using BQL.
Args:
start_date: Filter from this date (YYYY-MM-DD), optional
end_date: Filter to this date (YYYY-MM-DD), optional
group_by: "account" (by expense category) or "month" (by month)
Returns:
{
"summary": [
{"account": "Expenses:Supplies:Food", "fiat": 500.00, "sats": 550000},
...
],
"total_fiat": 1500.00,
"total_sats": 1650000,
"fiat_currency": "EUR",
"group_by": "account",
"start_date": "2025-01-01",
"end_date": "2025-12-31"
}
Admin only.
"""
from .fava_client import get_fava_client
if group_by not in ["account", "month"]:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="group_by must be 'account' or 'month'"
)
fava = get_fava_client()
summaries = await fava.get_expense_summary_bql(
start_date=start_date,
end_date=end_date,
group_by=group_by
)
# Calculate totals
total_fiat = sum(s.get("fiat", 0) for s in summaries)
total_sats = sum(s.get("sats", 0) for s in summaries)
return {
"summary": summaries,
"total_fiat": total_fiat,
"total_sats": total_sats,
"fiat_currency": "EUR",
"group_by": group_by,
"start_date": start_date,
"end_date": end_date,
"count": len(summaries)
}
@castle_api_router.get("/api/v1/reports/contributions")
async def api_contributions_report(
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> dict:
"""
Get user contribution report using BQL.
Shows total expenses submitted by each user (creating payables).
Returns:
{
"contributions": [
{
"user_id": "cfe378b3",
"username": "alice",
"total_fiat": 1500.00,
"total_sats": 1650000,
"entry_count": 25
},
...
],
"total_fiat": 5000.00,
"total_sats": 5500000,
"fiat_currency": "EUR",
"user_count": 5
}
Admin only.
"""
from lnbits.core.crud.users import get_user
from .fava_client import get_fava_client
fava = get_fava_client()
contributions = await fava.get_user_contributions_bql()
# Enrich with usernames
for contrib in contributions:
user_id = contrib["user_id"]
# Try to find full user_id from wallet settings
settings = await get_all_user_wallet_settings()
full_user_id = None
for s in settings:
if s.id.startswith(user_id):
full_user_id = s.id
break
if full_user_id:
user = await get_user(full_user_id)
contrib["username"] = user.username if user and user.username else None
contrib["full_user_id"] = full_user_id
else:
contrib["username"] = None
contrib["full_user_id"] = None
# Calculate totals
total_fiat = sum(c.get("total_fiat", 0) for c in contributions)
total_sats = sum(c.get("total_sats", 0) for c in contributions)
return {
"contributions": contributions,
"total_fiat": total_fiat,
"total_sats": total_sats,
"fiat_currency": "EUR",
"user_count": len(contributions)
}
@castle_api_router.get("/api/v1/users/{user_id}/unsettled-entries")
async def api_get_unsettled_entries(
user_id: str,