Route account writes for the split Fava ledger layout #32

Merged
padreug merged 5 commits from feat/split-ledger into main 2026-06-06 18:03:26 +00:00
2 changed files with 63 additions and 0 deletions
Showing only changes of commit 34ecb3f249 - Show all commits

Add POST /api/v1/admin/accounts for chart-of-accounts entries

Companion to the fava ledger split (aiolabs/server-deploy#4). Super-user
endpoint that adds a new Open directive to accounts/chart.beancount via
fava_client.add_account (explicit target_file), then mirrors the account
into Libra's DB via sync_single_account_from_beancount so permissions can
be granted on it.

Validates the account name against the five Beancount top-level prefixes
(Assets:/Liabilities:/Equity:/Income:/Expenses:) and returns 400 on a bad
prefix.

Per-user accounts (matching :User-xxxxxxxx) keep their existing code path
via crud.get_or_create_user_account, which inherits the inferred target_file
(accounts/users.beancount) from the add_account default.

Backend only -- the LNbits admin UI on top is tracked separately as
aiolabs/libra#30.

Refs: aiolabs/libra#29
Padreug 2026-06-06 15:30:23 +02:00

View file

@ -48,6 +48,13 @@ class CreateAccount(BaseModel):
is_virtual: bool = False # Set to True to create virtual parent account
class CreateChartAccount(BaseModel):
"""Admin-created chart-of-accounts entry written to accounts/chart.beancount."""
name: str # Full hierarchical account name, e.g. "Expenses:Services:Domain"
currencies: list[str] = ["EUR", "SATS", "USD"]
description: Optional[str] = None
class EntryLine(BaseModel):
id: str
journal_entry_id: str

View file

@ -52,6 +52,7 @@ from .models import (
LibraSettings,
CreateAccount,
CreateAccountPermission,
CreateChartAccount,
CreateBalanceAssertion,
CreateEntryLine,
CreateJournalEntry,
@ -3565,6 +3566,61 @@ async def api_get_account_hierarchy(
# ===== ACCOUNT SYNC ENDPOINTS =====
_VALID_ACCOUNT_PREFIXES = ("Assets:", "Liabilities:", "Equity:", "Income:", "Expenses:")
@libra_api_router.post("/api/v1/admin/accounts", status_code=HTTPStatus.CREATED)
async def api_admin_add_chart_account(
payload: CreateChartAccount,
auth: AuthContext = Depends(require_super_user),
) -> dict:
"""
Add a chart-of-accounts entry (super-user only).
Writes an Open directive to accounts/chart.beancount via Fava's /api/source,
then syncs the account into Libra's DB so permissions can be granted on it.
Per-user accounts (matching :User-xxxxxxxx) take a different code path via
crud.get_or_create_user_account and are not created through this endpoint.
"""
from .fava_client import get_fava_client
if not payload.name.startswith(_VALID_ACCOUNT_PREFIXES):
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=(
f"Account name must start with one of "
f"{', '.join(_VALID_ACCOUNT_PREFIXES)} (got {payload.name!r})"
),
)
logger.info(
f"Admin {auth.user_id[:8]} adding chart account {payload.name} "
f"with currencies {payload.currencies}"
)
fava = get_fava_client()
metadata: dict = {"added_by": auth.user_id[:8], "source": "admin-ui"}
if payload.description:
metadata["description"] = payload.description
await fava.add_account(
account_name=payload.name,
currencies=payload.currencies,
target_file="accounts/chart.beancount",
metadata=metadata,
)
# 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)
return {
"success": True,
"account_name": payload.name,
"synced_to_libra_db": synced,
}
@libra_api_router.post("/api/v1/admin/accounts/sync")
async def api_sync_all_accounts(
force_full_sync: bool = False,