Wire admin add-account endpoint into the UI #46
2 changed files with 48 additions and 5 deletions
fix(accounts): recover ledger-only account instead of blanket 409 (libra-#50)
When add_account reported the Open already existed, the endpoint raised 409 before the DB-mirror step — so an account present in the ledger but missing from libra's DB (a prior sync failure with no cross-DB atomicity, or an out-of-band open) was stranded: invisible to permissions with no recovery path. Now 409 only when the account is already in the DB too; otherwise sync it and return success. Adds a recovery test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
commit
39440b75a7
|
|
@ -93,6 +93,32 @@ async def test_add_chart_account_duplicate_returns_409(
|
||||||
assert "already exists" in second.json().get("detail", "").lower()
|
assert "already exists" in second.json().get("detail", "").lower()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_add_chart_account_recovers_ledger_only_account(
|
||||||
|
client, super_user_headers,
|
||||||
|
):
|
||||||
|
"""An account present in the ledger but absent from libra's DB (prior sync
|
||||||
|
failure / out-of-band edit) is recovered (synced), not 409'd — otherwise it
|
||||||
|
would be permanently un-grantable with no path back.
|
||||||
|
|
||||||
|
Reproduce the ledger-only state by creating normally (so Fava parses the
|
||||||
|
Open) then deleting only the libra-DB row — appending to the ledger file
|
||||||
|
directly would race Fava's parse cache."""
|
||||||
|
from ..crud import db # the same singleton the app uses
|
||||||
|
|
||||||
|
name = _unique("Expenses:Recover")
|
||||||
|
first = await add_chart_account(client, super_user_headers=super_user_headers, name=name)
|
||||||
|
assert first.status_code == 201, f"setup create failed: {first.status_code} {first.text}"
|
||||||
|
|
||||||
|
await db.execute("DELETE FROM accounts WHERE name = :name", {"name": name})
|
||||||
|
|
||||||
|
r = await add_chart_account(client, super_user_headers=super_user_headers, name=name)
|
||||||
|
assert r.status_code == 201, f"expected 201 recovery, got {r.status_code}: {r.text}"
|
||||||
|
body = r.json()
|
||||||
|
assert body.get("already_existed") is True, body
|
||||||
|
assert body["synced_to_libra_db"] is True, body
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_add_chart_account_invalid_prefix_returns_400(
|
async def test_add_chart_account_invalid_prefix_returns_400(
|
||||||
client, super_user_headers, fava_ledger_path,
|
client, super_user_headers, fava_ledger_path,
|
||||||
|
|
|
||||||
19
views_api.py
19
views_api.py
|
|
@ -3750,14 +3750,31 @@ async def api_admin_add_chart_account(
|
||||||
metadata=metadata,
|
metadata=metadata,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from .account_sync import sync_single_account_from_beancount
|
||||||
|
|
||||||
if result.get("already_existed"):
|
if result.get("already_existed"):
|
||||||
|
# The Open directive is already in the ledger. If it's also already
|
||||||
|
# mirrored into libra's DB, it's a true duplicate → 409. If not (a prior
|
||||||
|
# sync failed — there's no cross-DB atomicity — or it was opened out of
|
||||||
|
# band), mirror it now so it becomes grantable instead of being stranded
|
||||||
|
# with no recovery path.
|
||||||
|
from .crud import get_account_by_name
|
||||||
|
|
||||||
|
if await get_account_by_name(payload.name) is not None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.CONFLICT,
|
status_code=HTTPStatus.CONFLICT,
|
||||||
detail=f"Account {payload.name} already exists",
|
detail=f"Account {payload.name} already exists",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
synced = await sync_single_account_from_beancount(payload.name)
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"account_name": payload.name,
|
||||||
|
"synced_to_libra_db": synced,
|
||||||
|
"already_existed": True,
|
||||||
|
}
|
||||||
|
|
||||||
# Mirror into libra DB so permissions / metadata layer sees it.
|
# Mirror into libra DB so permissions / metadata layer sees it.
|
||||||
from .account_sync import sync_single_account_from_beancount
|
|
||||||
synced = await sync_single_account_from_beancount(payload.name)
|
synced = await sync_single_account_from_beancount(payload.name)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue