From 538751f21a12087925746c7f7c275ee74e2e8e50 Mon Sep 17 00:00:00 2001 From: padreug Date: Mon, 10 Nov 2025 21:22:02 +0100 Subject: [PATCH] Fixes user account creation in Fava/Beancount MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit fixes two critical bugs in the user account creation flow: 1. **Always check/create in Fava regardless of Castle DB status** - Previously, if an account existed in Castle DB, the function would return early without checking if the Open directive existed in Fava - This caused accounts to exist in Castle DB but not in Beancount - Now we always check Fava and create Open directives if needed 2. **Fix Open directive insertion to preserve metadata** - The insertion logic now skips over metadata lines when finding the insertion point - Prevents new Open directives from being inserted between existing directives and their metadata, which was causing orphaned metadata 3. **Add comprehensive logging** - Added detailed logging with [ACCOUNT CHECK], [FAVA CHECK], [FAVA CREATE], [CASTLE DB], and [WALLET UPDATE] prefixes - Makes it easier to trace account creation flow and debug issues 4. **Fix Fava filename handling** - Now queries /api/options to get the Beancount file path dynamically - Fixes "Parameter 'filename' is missing" errors with /api/source 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- crud.py | 27 ++++++++++++++++++--------- fava_client.py | 22 ++++++++++++++++++---- services.py | 9 +++++++++ 3 files changed, 45 insertions(+), 13 deletions(-) diff --git a/crud.py b/crud.py index 66d65b9..941a5d9 100644 --- a/crud.py +++ b/crud.py @@ -125,7 +125,12 @@ async def get_or_create_user_account( Account, ) - if not account: + logger.info(f"[ACCOUNT CHECK] User {user_id[:8]}, Account: {account_name}, In Castle DB: {account is not None}") + + # Always check/create in Fava, even if account exists in Castle DB + # This ensures Beancount has the Open directive + fava_account_exists = False + if True: # Always check Fava # Check if account exists in Fava/Beancount fava = get_fava_client() try: @@ -140,11 +145,12 @@ async def get_or_create_user_account( result = response.json() # Check if account exists in Fava - fava_has_account = len(result.get("data", {}).get("rows", [])) > 0 + fava_account_exists = len(result.get("data", {}).get("rows", [])) > 0 + logger.info(f"[FAVA CHECK] Account {account_name} exists in Fava: {fava_account_exists}") - if not fava_has_account: + if not fava_account_exists: # Create account in Fava/Beancount via Open directive - logger.info(f"Creating account in Fava: {account_name}") + logger.info(f"[FAVA CREATE] Creating account in Fava: {account_name}") await fava.add_account( account_name=account_name, currencies=["EUR", "SATS", "USD"], # Support common currencies @@ -153,15 +159,15 @@ async def get_or_create_user_account( "description": f"User-specific {account_type.value} account" } ) - logger.info(f"Created account in Fava: {account_name}") - else: - logger.info(f"Account already exists in Fava: {account_name}") + logger.info(f"[FAVA CREATE] Successfully created account in Fava: {account_name}") except Exception as e: - logger.warning(f"Could not check/create account in Fava: {e}") + logger.error(f"[FAVA ERROR] Could not check/create account in Fava: {e}", exc_info=True) # Continue anyway - account creation in Castle DB is still useful for metadata - # Create account in Castle DB for metadata tracking + # Create account in Castle DB for metadata tracking (only if it doesn't exist) + if not account: + logger.info(f"[CASTLE DB] Creating account in Castle DB: {account_name}") account = await create_account( CreateAccount( name=account_name, @@ -170,6 +176,9 @@ async def get_or_create_user_account( user_id=user_id, ) ) + logger.info(f"[CASTLE DB] Created account in Castle DB: {account_name}") + else: + logger.info(f"[CASTLE DB] Account already exists in Castle DB: {account_name}") return account diff --git a/fava_client.py b/fava_client.py index c5f88bc..2eb3e00 100644 --- a/fava_client.py +++ b/fava_client.py @@ -724,12 +724,22 @@ class FavaClient: try: async with httpx.AsyncClient(timeout=self.timeout) as client: - # Step 1: Get current source file - response = await client.get(f"{self.base_url}/source") + # Step 1: Get the main Beancount file path from Fava + options_response = await client.get(f"{self.base_url}/options") + options_response.raise_for_status() + options_data = options_response.json()["data"] + file_path = options_data["beancount_options"]["filename"] + + logger.debug(f"Fava main file: {file_path}") + + # Step 2: Get current source file + response = await client.get( + f"{self.base_url}/source", + params={"filename": file_path} + ) response.raise_for_status() source_data = response.json()["data"] - file_path = source_data["file_path"] sha256sum = source_data["sha256sum"] source = source_data["source"] @@ -738,12 +748,16 @@ class FavaClient: logger.info(f"Account {account_name} already exists in Beancount file") return {"data": sha256sum, "mtime": source_data.get("mtime", "")} - # Step 3: Find insertion point (after last Open directive) + # Step 3: Find insertion point (after last Open directive AND its metadata) lines = source.split('\n') insert_index = 0 for i, line in enumerate(lines): if line.strip().startswith(('open ', f'{opening_date.year}-')) and 'open' in line: + # Found an Open directive, now skip over any metadata lines insert_index = i + 1 + # Skip metadata lines (lines starting with whitespace) + while insert_index < len(lines) and lines[insert_index].startswith((' ', '\t')) and lines[insert_index].strip(): + insert_index += 1 # Step 4: Format Open directive as Beancount text currencies_str = ", ".join(currencies) diff --git a/services.py b/services.py index 648c86b..1f9d826 100644 --- a/services.py +++ b/services.py @@ -37,19 +37,28 @@ async def get_user_wallet(user_id: str) -> UserWalletSettings: async def update_user_wallet( user_id: str, data: UserWalletSettings ) -> UserWalletSettings: + from loguru import logger + + logger.info(f"[WALLET UPDATE] Starting update_user_wallet for user {user_id[:8]}") + settings = await get_user_wallet_settings(user_id) if not settings: + logger.info(f"[WALLET UPDATE] Creating new wallet settings for user {user_id[:8]}") settings = await create_user_wallet_settings(user_id, data) else: + logger.info(f"[WALLET UPDATE] Updating existing wallet settings for user {user_id[:8]}") settings = await update_user_wallet_settings(user_id, data) # Proactively create core user accounts when wallet is configured # This ensures all users have a consistent account structure from the start + logger.info(f"[WALLET UPDATE] Creating LIABILITY account for user {user_id[:8]}") await get_or_create_user_account( user_id, AccountType.LIABILITY, "Accounts Payable" ) + logger.info(f"[WALLET UPDATE] Creating ASSET account for user {user_id[:8]}") await get_or_create_user_account( user_id, AccountType.ASSET, "Accounts Receivable" ) + logger.info(f"[WALLET UPDATE] Completed update_user_wallet for user {user_id[:8]}") return settings