+ No balance assertions yet. Create one to verify your accounting accuracy.
+
+
+
+
@@ -761,4 +891,110 @@
+
+
+
+
+
Create Balance Assertion
+
+
+ Balance assertions help you verify accounting accuracy by checking if an account's actual balance matches your expected balance. If the assertion fails, you'll be alerted to investigate the discrepancy.
+
+
+
+
+
+
+ {% raw %}{{ scope.opt.name }}{% endraw %}
+ {% raw %}{{ scope.opt.account_type }}{% endraw %}
+
+
+
+
+
+
+
+ The balance you expect this account to have in satoshis
+
+
+
+
+
+ Allow the actual balance to differ by ± this amount (default: 0)
+
+
+
+
+
+
Optional: Fiat Balance Check
+
+
+
+
+
+
+
+
+
+ Create & Check
+
+ Cancel
+
+
+
+
+
{% endblock %}
diff --git a/views_api.py b/views_api.py
index f709d4a..1ea10b3 100644
--- a/views_api.py
+++ b/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"}