fix(accounts): 409 when admin-adding an account that already exists

add_account no-ops if the Open directive is already present but returned
a normal-looking dict, so the admin endpoint reported success ('created
(sync pending)') for a duplicate. Return an already_existed flag and
raise 409 from the endpoint. Also anchor the existence check on the Open
directive with a trailing-boundary match so a prefix (Expenses:Gas)
doesn't match a longer sibling (Expenses:GasStation). The flag is
additive, so the idempotent user-account path keeps no-opping silently.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-06-16 00:07:39 +02:00
commit caef3cf5e8
2 changed files with 23 additions and 5 deletions

View file

@ -1643,10 +1643,22 @@ class FavaClient:
sha256sum = source_data["sha256sum"] sha256sum = source_data["sha256sum"]
source = source_data["source"] source = source_data["source"]
# Step 2: Check if account already exists (may have been created by concurrent request) # Step 2: Check if account already exists (may have been
if f"open {account_name}" in source: # created by a concurrent request). Anchor on the Open
# directive and require the account to be followed by
# whitespace/end-of-line so a prefix (Expenses:Gas) does
# not match a longer sibling (Expenses:GasStation).
if re.search(
rf"open {re.escape(account_name)}(?:\s|$)",
source,
re.MULTILINE,
):
logger.info(f"Account {account_name} already exists in {target_file}") logger.info(f"Account {account_name} already exists in {target_file}")
return {"data": sha256sum, "mtime": source_data.get("mtime", "")} return {
"data": sha256sum,
"mtime": source_data.get("mtime", ""),
"already_existed": True,
}
# Step 3: Always append at end of file. # Step 3: Always append at end of file.
# Post-split layout, each include file has one mutation # Post-split layout, each include file has one mutation
@ -1700,7 +1712,7 @@ class FavaClient:
result = response.json() result = response.json()
logger.info(f"Added account {account_name} to {target_file} with currencies {currencies}") logger.info(f"Added account {account_name} to {target_file} with currencies {currencies}")
return result return {**result, "already_existed": False}
except httpx.HTTPStatusError as e: except httpx.HTTPStatusError as e:
# Check for checksum conflict (HTTP 412 Precondition Failed or similar) # Check for checksum conflict (HTTP 412 Precondition Failed or similar)

View file

@ -3695,13 +3695,19 @@ async def api_admin_add_chart_account(
if payload.description: if payload.description:
metadata["description"] = payload.description metadata["description"] = payload.description
await fava.add_account( result = await fava.add_account(
account_name=payload.name, account_name=payload.name,
currencies=payload.currencies, currencies=payload.currencies,
target_file="accounts/chart.beancount", target_file="accounts/chart.beancount",
metadata=metadata, metadata=metadata,
) )
if result.get("already_existed"):
raise HTTPException(
status_code=HTTPStatus.CONFLICT,
detail=f"Account {payload.name} already exists",
)
# 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 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)