diff --git a/beancount_format.py b/beancount_format.py index 4cbc5c7..fbdfba9 100644 --- a/beancount_format.py +++ b/beancount_format.py @@ -115,13 +115,18 @@ def format_balance( account: str, amount: int, currency: str = "SATS" -) -> str: +) -> Dict[str, Any]: """ - Format a balance assertion directive for Beancount. + Format a balance assertion directive for Fava's JSON API. Balance assertions verify that an account has an expected balance on a specific date. They are checked automatically by Beancount when the file is loaded. + Fava's `deserialise` (fava/serialisation.py) expects + `{"t": "Balance", "amount": {"number", "currency"}, ...}` — the + previous source-string return 500'd on every assertion create + (libra-#39). + Args: date_val: Date of the balance assertion account: Account name (e.g., "Assets:Bitcoin:Lightning") @@ -129,15 +134,15 @@ def format_balance( currency: Currency code (default: "SATS") Returns: - Beancount balance directive as a string - - Example: - >>> format_balance(date(2025, 11, 10), "Assets:Bitcoin:Lightning", 1500000, "SATS") - '2025-11-10 balance Assets:Bitcoin:Lightning 1500000 SATS' + Fava API Balance entry dict ready for `fava.add_entry`. """ - date_str = date_val.strftime('%Y-%m-%d') - # Two spaces between account and amount (Beancount convention) - return f"{date_str} balance {account} {amount} {currency}" + return { + "t": "Balance", + "date": date_val.strftime('%Y-%m-%d'), + "account": account, + "amount": {"number": str(amount), "currency": currency}, + "meta": {}, + } def format_posting_with_cost( diff --git a/tests/test_reconciliation_api.py b/tests/test_reconciliation_api.py index 66757be..a168ff8 100644 --- a/tests/test_reconciliation_api.py +++ b/tests/test_reconciliation_api.py @@ -18,19 +18,6 @@ from uuid import uuid4 import pytest -# Tests that try to actually create + check an assertion all hit issue #39: -# `format_balance` returns a Beancount source string but `fava.add_entry` -# expects a dict, so Fava 500s on every assertion-create call. The contract -# violation is on libra's side; mark these strict-xfail so they go green -# automatically once #39 lands and the format_balance return shape is fixed. -ASSERTION_CREATE_BROKEN = pytest.mark.xfail( - reason="libra/issues/39 — POST /assertions submits a Beancount source string " - "to Fava's JSON API and 500s. Drop this marker when the format_balance " - "return type is changed to a dict.", - strict=True, -) - - # --------------------------------------------------------------------------- # helpers (local — assertion endpoints don't have wrapper helpers yet) # --------------------------------------------------------------------------- @@ -58,7 +45,6 @@ async def _create_assertion( # --------------------------------------------------------------------------- -@ASSERTION_CREATE_BROKEN @pytest.mark.anyio async def test_assertion_against_empty_account_passes( client, super_user_headers, standard_accounts, @@ -79,7 +65,6 @@ async def test_assertion_against_empty_account_passes( assert body.get("difference_sats", 0) == 0 -@ASSERTION_CREATE_BROKEN @pytest.mark.anyio async def test_assertion_with_wrong_balance_returns_409( client, super_user_headers, standard_accounts, @@ -102,7 +87,6 @@ async def test_assertion_with_wrong_balance_returns_409( assert detail.get("difference_sats") == 999_999 or detail.get("difference_sats") == -999_999 -@ASSERTION_CREATE_BROKEN @pytest.mark.anyio async def test_assertion_with_tolerance_accepts_small_diff( client, super_user_headers, standard_accounts, @@ -119,7 +103,6 @@ async def test_assertion_with_tolerance_accepts_small_diff( assert r.json().get("status") == "passed" -@ASSERTION_CREATE_BROKEN @pytest.mark.anyio async def test_list_assertions_returns_created( client, super_user_headers, standard_accounts, @@ -145,7 +128,6 @@ async def test_list_assertions_returns_created( assert assertion_id in ids, f"created assertion {assertion_id} missing from list {ids}" -@ASSERTION_CREATE_BROKEN @pytest.mark.anyio async def test_get_assertion_by_id( client, super_user_headers, standard_accounts, @@ -167,7 +149,6 @@ async def test_get_assertion_by_id( assert r.json().get("id") == assertion_id -@ASSERTION_CREATE_BROKEN @pytest.mark.anyio async def test_recheck_assertion_via_check_endpoint( client, super_user_headers, standard_accounts, @@ -190,7 +171,6 @@ async def test_recheck_assertion_via_check_endpoint( assert r.json().get("status") == "passed" -@ASSERTION_CREATE_BROKEN @pytest.mark.anyio async def test_delete_assertion_removes_it( client, super_user_headers, standard_accounts,