POST /api/v1/assertions broken — submits Beancount source string to Fava JSON API #39

Open
opened 2026-06-06 21:39:14 +00:00 by padreug · 0 comments
Owner

What

POST /libra/api/v1/assertions always returns 500 because the create handler hands a Beancount source-format string to fava.add_entry, which expects a dict and serialises it as JSON to Fava's /api/add_entries. Fava 500s on the malformed payload.

End user impact: the entire balance-assertion / reconciliation feature is currently non-functional.

Repro

curl -X POST http://<lnbits>/libra/api/v1/assertions \
  -H "X-Api-Key: <super_user_admin_key>" \
  -H "Content-type: application/json" \
  -d '{"account_id": "<existing_asset_account_id>", "expected_balance_sats": 0}'

Returns:

500 Internal Server Error
{"detail": "Failed to write balance assertion to Beancount: Server error '500 INTERNAL SERVER ERROR' for url 'http://<fava>/<slug>/api/add_entries'"}

Where

views_api.py:2993-3010:

balance_directive = format_balance(...)        # ← returns a STRING
result = await fava.add_entry(balance_directive)  # ← expects a DICT

beancount_format.py:113-141:

def format_balance(date_val, account, amount, currency="SATS") -> str:
    ...
    return f"{date_str} balance {account}  {amount} {currency}"

The fix has to land on whichever side owns the contract:

  1. Best: make format_balance return a dict with {"t": "Balance", "date": ..., "account": ..., "amount": f"{n} {currency}", "tolerance": ...} so it composes with fava.add_entry. Fava's add_entries does accept Balance directives this way (see fava/json_api.pyput_add_entries deserialises any directive type, not only Transactions).
  2. Alternative: skip fava.add_entry entirely for balance assertions and use Fava's /api/source endpoint to append the source line directly. More fragile (file-write race, the libra-#36 sibling), but matches format_balance's current return type.

Option 1 is the consistent thing to do — every other libra entry path goes through fava.add_entry with a dict.

Caught by

The reconciliation test file tests/test_reconciliation_api.py — 7 of 12 tests fail with this 500. The 5 that pass don't reach the broken Fava write (auth-gate, validation, listing, unknown-account, daily-task entry point all return before the bad path).

Test file marks the affected tests as xfail pointing at this issue until the fix lands.

Surfaced alongside

This was found together with libra-#38 (validation.py exception-handling gap) and likely shares a single PR cycle — both are small, contained, in the assertion/validation surface.

Scope

  • beancount_format.format_balance — change return type from str to dict[str, Any] matching Fava's Balance directive shape.
  • views_api.py: api_create_balance_assertion — no change if (1) is taken.
  • Audit other callers of format_balance (currently only the assertion endpoint).
  • Once the create path works, the crud.check_balance_assertion re-check side and /reconciliation/check-all likely already work since they read balances via the existing BQL path.

Not blocking other libra work but blocks the reconciliation feature entirely.

## What `POST /libra/api/v1/assertions` always returns 500 because the create handler hands a Beancount source-format string to `fava.add_entry`, which expects a dict and serialises it as JSON to Fava's `/api/add_entries`. Fava 500s on the malformed payload. End user impact: the entire balance-assertion / reconciliation feature is currently non-functional. ## Repro ```bash curl -X POST http://<lnbits>/libra/api/v1/assertions \ -H "X-Api-Key: <super_user_admin_key>" \ -H "Content-type: application/json" \ -d '{"account_id": "<existing_asset_account_id>", "expected_balance_sats": 0}' ``` Returns: ``` 500 Internal Server Error {"detail": "Failed to write balance assertion to Beancount: Server error '500 INTERNAL SERVER ERROR' for url 'http://<fava>/<slug>/api/add_entries'"} ``` ## Where `views_api.py:2993-3010`: ```python balance_directive = format_balance(...) # ← returns a STRING result = await fava.add_entry(balance_directive) # ← expects a DICT ``` `beancount_format.py:113-141`: ```python def format_balance(date_val, account, amount, currency="SATS") -> str: ... return f"{date_str} balance {account} {amount} {currency}" ``` The fix has to land on whichever side owns the contract: 1. **Best**: make `format_balance` return a dict with `{"t": "Balance", "date": ..., "account": ..., "amount": f"{n} {currency}", "tolerance": ...}` so it composes with `fava.add_entry`. Fava's `add_entries` does accept Balance directives this way (see `fava/json_api.py` — `put_add_entries` deserialises any directive type, not only Transactions). 2. **Alternative**: skip `fava.add_entry` entirely for balance assertions and use Fava's `/api/source` endpoint to append the source line directly. More fragile (file-write race, the libra-#36 sibling), but matches `format_balance`'s current return type. Option 1 is the consistent thing to do — every other libra entry path goes through `fava.add_entry` with a dict. ## Caught by The reconciliation test file `tests/test_reconciliation_api.py` — 7 of 12 tests fail with this 500. The 5 that pass don't reach the broken Fava write (auth-gate, validation, listing, unknown-account, daily-task entry point all return before the bad path). Test file marks the affected tests as `xfail` pointing at this issue until the fix lands. ## Surfaced alongside This was found together with libra-#38 (validation.py exception-handling gap) and likely shares a single PR cycle — both are small, contained, in the assertion/validation surface. ## Scope - `beancount_format.format_balance` — change return type from `str` to `dict[str, Any]` matching Fava's Balance directive shape. - `views_api.py: api_create_balance_assertion` — no change if (1) is taken. - Audit other callers of `format_balance` (currently only the assertion endpoint). - Once the create path works, the `crud.check_balance_assertion` re-check side and `/reconciliation/check-all` likely already work since they read balances via the existing BQL path. Not blocking other libra work but blocks the reconciliation feature entirely.
padreug referenced this issue from a commit 2026-06-07 13:39:52 +00:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/libra#39
No description provided.