PHASE 2: Implements balance assertions for reconciliation
Adds balance assertion functionality to enable admins to verify accounting accuracy. This includes: - A new `balance_assertions` table in the database - CRUD operations for balance assertions (create, get, list, check, delete) - API endpoints for managing balance assertions (admin only) - UI elements for creating, viewing, and re-checking assertions Also, reorders the implementation roadmap in the documentation to reflect better the dependencies between phases.
This commit is contained in:
parent
1a9c91d042
commit
0257b7807c
7 changed files with 890 additions and 17 deletions
177
views_api.py
177
views_api.py
|
|
@ -13,10 +13,13 @@ from lnbits.utils.exchange_rates import allowed_currencies, fiat_amount_as_satos
|
|||
|
||||
from .crud import (
|
||||
approve_manual_payment_request,
|
||||
check_balance_assertion,
|
||||
create_account,
|
||||
create_balance_assertion,
|
||||
create_journal_entry,
|
||||
create_manual_payment_request,
|
||||
db,
|
||||
delete_balance_assertion,
|
||||
get_account,
|
||||
get_account_balance,
|
||||
get_account_by_name,
|
||||
|
|
@ -26,6 +29,8 @@ from .crud import (
|
|||
get_all_manual_payment_requests,
|
||||
get_all_user_balances,
|
||||
get_all_user_wallet_settings,
|
||||
get_balance_assertion,
|
||||
get_balance_assertions,
|
||||
get_journal_entries_by_user,
|
||||
get_journal_entry,
|
||||
get_manual_payment_request,
|
||||
|
|
@ -37,8 +42,11 @@ from .crud import (
|
|||
from .models import (
|
||||
Account,
|
||||
AccountType,
|
||||
AssertionStatus,
|
||||
BalanceAssertion,
|
||||
CastleSettings,
|
||||
CreateAccount,
|
||||
CreateBalanceAssertion,
|
||||
CreateEntryLine,
|
||||
CreateJournalEntry,
|
||||
CreateManualPaymentRequest,
|
||||
|
|
@ -1051,3 +1059,172 @@ async def api_reject_expense_entry(
|
|||
|
||||
# Return updated entry
|
||||
return await get_journal_entry(entry_id)
|
||||
|
||||
|
||||
# ===== BALANCE ASSERTION ENDPOINTS =====
|
||||
|
||||
|
||||
@castle_api_router.post("/api/v1/assertions")
|
||||
async def api_create_balance_assertion(
|
||||
data: CreateBalanceAssertion,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
) -> BalanceAssertion:
|
||||
"""
|
||||
Create a balance assertion for reconciliation (admin only).
|
||||
The assertion will be checked immediately upon creation.
|
||||
"""
|
||||
from lnbits.settings import settings as lnbits_settings
|
||||
|
||||
if wallet.wallet.user != lnbits_settings.super_user:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN,
|
||||
detail="Only super user can create balance assertions",
|
||||
)
|
||||
|
||||
# Verify account exists
|
||||
account = await get_account(data.account_id)
|
||||
if not account:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail=f"Account {data.account_id} not found",
|
||||
)
|
||||
|
||||
# Create the assertion
|
||||
assertion = await create_balance_assertion(data, wallet.wallet.user)
|
||||
|
||||
# Check it immediately
|
||||
try:
|
||||
assertion = await check_balance_assertion(assertion.id)
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=str(e),
|
||||
)
|
||||
|
||||
# If assertion failed, return 409 Conflict with details
|
||||
if assertion.status == AssertionStatus.FAILED:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.CONFLICT,
|
||||
detail={
|
||||
"message": "Balance assertion failed",
|
||||
"expected_sats": assertion.expected_balance_sats,
|
||||
"actual_sats": assertion.checked_balance_sats,
|
||||
"difference_sats": assertion.difference_sats,
|
||||
"expected_fiat": float(assertion.expected_balance_fiat) if assertion.expected_balance_fiat else None,
|
||||
"actual_fiat": float(assertion.checked_balance_fiat) if assertion.checked_balance_fiat else None,
|
||||
"difference_fiat": float(assertion.difference_fiat) if assertion.difference_fiat else None,
|
||||
"fiat_currency": assertion.fiat_currency,
|
||||
},
|
||||
)
|
||||
|
||||
return assertion
|
||||
|
||||
|
||||
@castle_api_router.get("/api/v1/assertions")
|
||||
async def api_get_balance_assertions(
|
||||
account_id: str = None,
|
||||
status: str = None,
|
||||
limit: int = 100,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
) -> list[BalanceAssertion]:
|
||||
"""Get balance assertions with optional filters (admin only)"""
|
||||
from lnbits.settings import settings as lnbits_settings
|
||||
|
||||
if wallet.wallet.user != lnbits_settings.super_user:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN,
|
||||
detail="Only super user can view balance assertions",
|
||||
)
|
||||
|
||||
# Parse status enum if provided
|
||||
status_enum = None
|
||||
if status:
|
||||
try:
|
||||
status_enum = AssertionStatus(status)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=f"Invalid status: {status}. Must be one of: pending, passed, failed",
|
||||
)
|
||||
|
||||
return await get_balance_assertions(
|
||||
account_id=account_id,
|
||||
status=status_enum,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@castle_api_router.get("/api/v1/assertions/{assertion_id}")
|
||||
async def api_get_balance_assertion(
|
||||
assertion_id: str,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
) -> BalanceAssertion:
|
||||
"""Get a specific balance assertion (admin only)"""
|
||||
from lnbits.settings import settings as lnbits_settings
|
||||
|
||||
if wallet.wallet.user != lnbits_settings.super_user:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN,
|
||||
detail="Only super user can view balance assertions",
|
||||
)
|
||||
|
||||
assertion = await get_balance_assertion(assertion_id)
|
||||
if not assertion:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail="Balance assertion not found",
|
||||
)
|
||||
|
||||
return assertion
|
||||
|
||||
|
||||
@castle_api_router.post("/api/v1/assertions/{assertion_id}/check")
|
||||
async def api_check_balance_assertion(
|
||||
assertion_id: str,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
) -> BalanceAssertion:
|
||||
"""Re-check a balance assertion (admin only)"""
|
||||
from lnbits.settings import settings as lnbits_settings
|
||||
|
||||
if wallet.wallet.user != lnbits_settings.super_user:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN,
|
||||
detail="Only super user can check balance assertions",
|
||||
)
|
||||
|
||||
try:
|
||||
assertion = await check_balance_assertion(assertion_id)
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail=str(e),
|
||||
)
|
||||
|
||||
return assertion
|
||||
|
||||
|
||||
@castle_api_router.delete("/api/v1/assertions/{assertion_id}")
|
||||
async def api_delete_balance_assertion(
|
||||
assertion_id: str,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
) -> dict:
|
||||
"""Delete a balance assertion (admin only)"""
|
||||
from lnbits.settings import settings as lnbits_settings
|
||||
|
||||
if wallet.wallet.user != lnbits_settings.super_user:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN,
|
||||
detail="Only super user can delete balance assertions",
|
||||
)
|
||||
|
||||
# Verify it exists
|
||||
assertion = await get_balance_assertion(assertion_id)
|
||||
if not assertion:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail="Balance assertion not found",
|
||||
)
|
||||
|
||||
await delete_balance_assertion(assertion_id)
|
||||
|
||||
return {"success": True, "message": "Balance assertion deleted"}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue