Approving a pending income entry 404s: entry id resolves to "unknown" in list endpoints #42
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Symptom
Approving a pending income entry from the admin UI fails with a toast:
Reproduces for income entries created via
POST /api/v1/entries/income. Example ledger entry that fails to approve:Root cause
The two journal-list endpoints —
GET /api/v1/entries/userandGET /api/v1/entries/pending— extract the entry id by searching the links for the substringlibra-:But the entry formatters now emit typed link prefixes (
inc-{id},exp-{id},rcv-{id}) and only fall back to alibra-{id}link when noreferenceis set (beancount_format.format_income_entryetc.). The income entry above has linksinc-934c956acd364f3band42-144-934c956acd364f3b— neither containslibra-, soentry_idstaysNoneand is rendered as the literal string"unknown"(entry_id or "unknown").The UI then POSTs to
/api/v1/entries/unknown/approve, and the approve handler can't find a transaction whose link ends in-unknown, returning 404.The approve/reject handlers themselves are fine — they already match
link_clean.endswith(f"-{entry_id}"), so they resolve correctly once given the real id. The bug is purely in the list endpoints producing a bad id for the frontend.Note: this also affects expense and receivable entries that carry a custom
reference(their primary link isexp-/rcv-prefixed, notlibra-); income just surfaced it because it always usesinc-.Fix
Resolve the id from the canonical
entry-idtransaction metadata (written by every libra entry formatter) before falling back to link parsing. A shared_extract_entry_id()helper now does meta-first resolution with a legacy link fallback (libra-{id}and typedinc-/exp-/rcv-prefixes), used by both list endpoints.Fixed on
main(views_api.py). Surfaced on a recently-upgraded production instance where members couldn't approve income entries.Follow-up (not in this fix)
GET /api/v1/entries/pendingreturns the real id (not"unknown") for income/expense/receivable entries with areferenceset."unknown"sentinel entirely in favor of omitting non-approvable entries' ids, so a future regression fails loudly instead of round-tripping a bad id.Confirmed: exact trigger is a custom
reference, not the entry typeThe differentiator is whether the entry was created with a
reference— not income vs expense, and not a software version difference between servers. Two real entries, same code:Fails (still
!, id renderedunknown):reference
42-144set → links areinc-…+42-144-…→ nolibra-link → list extraction returns"unknown"→POST /entries/unknown/approve→ 404.Works (approved, now
*):no reference →
libra_reference = f"libra-{entry_id}"→libra-…link present → list extraction succeeds → approves fine.Source of the missing
libra-link (views_api.py, income/expense/receivable creation):So the "worked on another server" observation was not a version difference — that instance simply approved an entry that had no
reference. Any reference-bearing pending entry (income, expense, or receivable) is affected.The metadata-first
_extract_entry_id()fix resolves all cases sinceentry-idis written on every entry regardless of reference.Fixed in
15d9910(on main).Final design —
entry-idtransaction metadata is the single canonical identity:_extract_entry_id()resolves metadata-first; used by/entries/user,/entries/pending, approve, and reject (libra-link parsing kept only for pre-dfdcc44 ledger history).exp-/rcv-/inc-links remain the settlement-tracking handles (BQL matching unchanged).format_revenue_entrynow writesentry-idmetadata + sanitizes its reference link; genericPOST /entriessanitizes its reference link; user-journalreferenceextraction no longer leaks typed system links.Contract documented in CLAUDE.md (Data Integrity → Entry Identity & Links). Pinned by
tests/test_entry_identity_api.py(4 integration regression tests, including the exact production shape: income +42-144reference → real id in pending list → approve succeeds) and formatter contract tests intest_unit.py. Full suite: 117 passed; remaining failures pre-existing/environmental (filed separately).For the stuck production entry: it has
entry-idmetadata, so after deploying this it will list with its real id and approve normally — no ledger surgery needed.