Fixes user account creation in Fava/Beancount

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 <noreply@anthropic.com>
This commit is contained in:
padreug 2025-11-10 21:22:02 +01:00
parent a3c3e44e5f
commit 538751f21a
3 changed files with 45 additions and 13 deletions

27
crud.py
View file

@ -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

View file

@ -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)

View file

@ -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