Extends account lookup for user accounts

Implements account lookup logic for user-specific accounts,
specifically Liabilities:Payable and Assets:Receivable.

This allows the system to automatically map Beancount accounts
to corresponding accounts in the Castle system based on user ID.

Improves error messages when user accounts are not properly configured.
This commit is contained in:
padreug 2025-11-08 23:51:07 +01:00
parent 992a8fe554
commit 4b327a0aab

View file

@ -150,32 +150,58 @@ class AccountLookup:
"""
Get Castle account ID for a Beancount account name.
Special handling for Equity accounts:
- "Equity:Pat" -> looks up Pat's user_id and finds their equity account
Special handling for user-specific accounts:
- "Liabilities:Payable:Pat" -> looks up Pat's user_id and finds their Castle payable account
- "Assets:Receivable:Pat" -> looks up Pat's user_id and finds their Castle receivable account
Args:
account_name: Beancount account name (e.g., "Expenses:Food:Supplies" or "Equity:Pat")
account_name: Beancount account name (e.g., "Expenses:Food:Supplies", "Liabilities:Payable:Pat", "Assets:Receivable:Pat")
Returns:
Castle account UUID or None if not found
"""
# Check if this is an Equity:<name> account
if account_name.startswith("Equity:"):
user_name = extract_user_from_equity_account(account_name)
# Check if this is a Liabilities:Payable:<name> account
# Map Beancount Liabilities:Payable:Pat to Castle Liabilities:Payable:User-<id>
if account_name.startswith("Liabilities:Payable:"):
user_name = extract_user_from_user_account(account_name)
if user_name:
# Look up user's actual user_id
user_id = USER_MAPPINGS.get(user_name)
if user_id:
# Find this user's equity account
# Find this user's liability (payable) account
# This is the Liabilities:Payable:User-<id> account in Castle
if user_id in self.accounts_by_user:
equity_account_id = self.accounts_by_user[user_id].get('equity')
if equity_account_id:
return equity_account_id
liability_account_id = self.accounts_by_user[user_id].get('liability')
if liability_account_id:
return liability_account_id
# If not found, provide helpful error
raise ValueError(
f"User '{user_name}' (ID: {user_id}) does not have an equity account.\n"
f"Please enable equity eligibility for this user in Castle first."
f"User '{user_name}' (ID: {user_id}) does not have a payable account.\n"
f"This should have been created when they configured their wallet.\n"
f"Please configure the wallet for user ID: {user_id}"
)
# Check if this is an Assets:Receivable:<name> account
# Map Beancount Assets:Receivable:Pat to Castle Assets:Receivable:User-<id>
elif account_name.startswith("Assets:Receivable:"):
user_name = extract_user_from_user_account(account_name)
if user_name:
# Look up user's actual user_id
user_id = USER_MAPPINGS.get(user_name)
if user_id:
# Find this user's asset (receivable) account
# This is the Assets:Receivable:User-<id> account in Castle
if user_id in self.accounts_by_user:
asset_account_id = self.accounts_by_user[user_id].get('asset')
if asset_account_id:
return asset_account_id
# If not found, provide helpful error
raise ValueError(
f"User '{user_name}' (ID: {user_id}) does not have a receivable account.\n"
f"This should have been created when they configured their wallet.\n"
f"Please configure the wallet for user ID: {user_id}"
)
# Normal account lookup by name
@ -304,36 +330,40 @@ def parse_beancount_transaction(txn_text: str) -> Optional[Dict]:
# ===== HELPER FUNCTIONS =====
def extract_user_from_equity_account(account_name: str) -> Optional[str]:
def extract_user_from_user_account(account_name: str) -> Optional[str]:
"""
Extract user name from Equity account.
Extract user name from user-specific accounts (Payable or Receivable).
Examples:
"Equity:Pat" -> "Pat"
"Equity:Alice" -> "Alice"
"Liabilities:Payable:Pat" -> "Pat"
"Assets:Receivable:Alice" -> "Alice"
"Expenses:Food" -> None
Returns:
User name or None if not an Equity account
User name or None if not a user-specific account
"""
if account_name.startswith("Equity:"):
if account_name.startswith("Liabilities:Payable:"):
parts = account_name.split(":")
if len(parts) >= 2:
return parts[1]
if len(parts) >= 3:
return parts[2]
elif account_name.startswith("Assets:Receivable:"):
parts = account_name.split(":")
if len(parts) >= 3:
return parts[2]
return None
def determine_user_id(postings: list) -> Optional[str]:
"""
Determine which user ID to use for this transaction based on Equity accounts.
Determine which user ID to use for this transaction based on user-specific accounts.
Args:
postings: List of posting dicts with 'account' key
Returns:
User ID (wallet ID) from USER_MAPPINGS, or None if no Equity account found
User ID (wallet ID) from USER_MAPPINGS, or None if no user account found
"""
for posting in postings:
user_name = extract_user_from_equity_account(posting['account'])
user_name = extract_user_from_user_account(posting['account'])
if user_name:
user_id = USER_MAPPINGS.get(user_name)
if not user_id:
@ -343,7 +373,7 @@ def determine_user_id(postings: list) -> Optional[str]:
)
return user_id
# No Equity account found - this shouldn't happen for typical transactions
# No user-specific account found - this shouldn't happen for typical transactions
return None
# ===== CASTLE CONVERTER =====
@ -351,12 +381,13 @@ def determine_user_id(postings: list) -> Optional[str]:
def convert_to_castle_entry(parsed: dict, btc_eur_rate: float, account_lookup: AccountLookup) -> dict:
"""Convert parsed Beancount transaction to Castle format"""
# Determine which user this transaction is for (based on Equity accounts)
# Determine which user this transaction is for (based on user-specific accounts)
user_id = determine_user_id(parsed['postings'])
if not user_id:
raise ValueError(
f"Could not determine user ID for transaction.\n"
f"Transactions must have an Equity:<name> account (e.g., Equity:Pat)."
f"Transactions must have a Liabilities:Payable:<name> or Assets:Receivable:<name> account.\n"
f"Examples: Liabilities:Payable:Pat, Assets:Receivable:Pat"
)
# Build entry lines
@ -502,7 +533,7 @@ def import_beancount_file(beancount_file: str, dry_run: bool = False):
# Get user name for display
user_name = None
for posting in parsed['postings']:
user_name = extract_user_from_equity_account(posting['account'])
user_name = extract_user_from_user_account(posting['account'])
if user_name:
break