Resolve entry identity via entry-id metadata; unfuse user references (libra-#42)

Approving a pending entry created with a reference (e.g. invoice
"42-144") 404'd with "Pending entry unknown not found": the list
endpoints recovered the entry id by parsing links for a libra- prefix,
but reference-bearing entries displace that link with the fused
"{reference}-{entry_id}" form, so the id surfaced as the literal
"unknown" and the approve call round-tripped it.

Make the entry-id transaction metadata the single canonical identity:

- _extract_entry_id() resolves metadata-first (libra- link parsing kept
  only for pre-dfdcc44 ledger history); used by /entries/user,
  /entries/pending, approve, and reject.
- Creation endpoints no longer fuse the reference with the entry id —
  the user reference becomes its own sanitized link and round-trips
  verbatim in API responses. Typed exp-/rcv-/inc- links stay as the
  settlement-tracking handles.
- format_revenue_entry now writes entry-id metadata like its siblings
  and sanitizes its reference link (was appended raw); generic
  POST /entries sanitizes its reference link too.
- User-journal reference extraction skips all system link prefixes
  (typed links used to leak into the reference field).

Contract documented in CLAUDE.md (Data Integrity → Entry Identity &
Links), pinned by tests/test_entry_identity_api.py and formatter
contract tests in test_unit.py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-06-12 20:39:06 +02:00
commit 15d9910073
6 changed files with 374 additions and 89 deletions

View file

@ -135,6 +135,96 @@ def test_generate_entry_ids_are_unique():
assert len(ids) == 100 # 16 hex chars = 64 bits; collisions in 100 are negligible
# ---------------------------------------------------------------------------
# Entry identity contract — every libra-authored entry formatter must write
# `entry-id` metadata (the canonical id) and keep the user reference as its
# own sanitized link, never fused with the id.
# ---------------------------------------------------------------------------
def test_format_expense_entry_identity_contract():
entry = bf.format_expense_entry(
user_id="abc12345",
expense_account="Expenses:Food",
user_account="Liabilities:Payable:User-abc12345",
amount_sats=50000,
description="Groceries",
entry_date=date(2026, 6, 12),
fiat_currency="EUR",
fiat_amount=Decimal("46.50"),
reference="Invoice #123",
entry_id="deadbeef00000001",
)
assert entry["meta"]["entry-id"] == "deadbeef00000001"
assert "exp-deadbeef00000001" in entry["links"]
assert "Invoice-123" in entry["links"] # sanitized, standalone
def test_format_receivable_entry_identity_contract():
entry = bf.format_receivable_entry(
user_id="abc12345",
revenue_account="Income:Accommodation",
receivable_account="Assets:Receivable:User-abc12345",
amount_sats=100000,
description="2-night stay",
entry_date=date(2026, 6, 12),
fiat_currency="EUR",
fiat_amount=Decimal("93.00"),
reference="BOOKING/42",
entry_id="deadbeef00000002",
)
assert entry["meta"]["entry-id"] == "deadbeef00000002"
assert "rcv-deadbeef00000002" in entry["links"]
assert "BOOKING/42" in entry["links"]
def test_format_income_entry_identity_contract():
"""The production-bug shape: income + reference like '42-144'."""
entry = bf.format_income_entry(
user_id="abc12345",
user_account="Assets:Receivable:User-abc12345",
revenue_account="Income:MemberDuesContributions",
amount_sats=1112490,
description="2 Memberships",
entry_date=date(2026, 6, 12),
fiat_currency="USD",
fiat_amount=Decimal("700.00"),
reference="42-144",
entry_id="deadbeef00000003",
)
assert entry["meta"]["entry-id"] == "deadbeef00000003"
assert "inc-deadbeef00000003" in entry["links"]
assert "42-144" in entry["links"]
def test_format_revenue_entry_identity_contract():
entry = bf.format_revenue_entry(
payment_account="Assets:Cash",
revenue_account="Income:Sales",
amount_sats=100000,
description="Product sale",
entry_date=date(2026, 6, 12),
fiat_currency="EUR",
fiat_amount=Decimal("50.00"),
reference="Till receipt 9",
entry_id="deadbeef00000004",
)
assert entry["meta"]["entry-id"] == "deadbeef00000004"
assert "Till-receipt-9" in entry["links"] # sanitized
def test_format_revenue_entry_generates_entry_id_when_absent():
entry = bf.format_revenue_entry(
payment_account="Assets:Cash",
revenue_account="Income:Sales",
amount_sats=100000,
description="Product sale",
entry_date=date(2026, 6, 12),
)
eid = entry["meta"]["entry-id"]
assert len(eid) == 16 and all(c in "0123456789abcdef" for c in eid)
# ---------------------------------------------------------------------------
# account_utils.format_hierarchical_account_name
# ---------------------------------------------------------------------------