Enhances Beancount import and API entry creation
Improves the Beancount import process to send SATS amounts with fiat metadata to the API, enabling automatic conversion to EUR-based postings. Updates the API to store entries in Fava instead of the Castle DB, simplifying the JournalEntry creation process. Adds error handling to the upload entry function. Includes a note about imported transactions being stored in EUR with SATS in metadata.
This commit is contained in:
parent
1d605be021
commit
0e93fc5ffc
2 changed files with 41 additions and 50 deletions
|
|
@ -245,18 +245,19 @@ def eur_to_sats(eur_amount: Decimal, btc_eur_rate: float) -> int:
|
|||
return int(sats.quantize(Decimal('1')))
|
||||
|
||||
def build_metadata(eur_amount: Decimal, btc_eur_rate: float) -> dict:
|
||||
"""Build metadata dict for Castle entry line"""
|
||||
"""
|
||||
Build metadata dict for Castle entry line.
|
||||
|
||||
The API will extract fiat_currency and fiat_amount and use them
|
||||
to create proper EUR-based postings with SATS in metadata.
|
||||
"""
|
||||
abs_eur = abs(eur_amount)
|
||||
abs_sats = abs(eur_to_sats(abs_eur, btc_eur_rate))
|
||||
|
||||
# fiat_rate = sats per EUR
|
||||
fiat_rate = float(abs_sats) / float(abs_eur) if abs_eur > 0 else 0
|
||||
|
||||
return {
|
||||
"fiat_currency": "EUR",
|
||||
"fiat_amount": str(abs_eur.quantize(Decimal("0.001"))),
|
||||
"fiat_rate": fiat_rate,
|
||||
"btc_rate": btc_eur_rate
|
||||
"fiat_amount": str(abs_eur.quantize(Decimal("0.01"))), # Store as string for JSON
|
||||
"btc_rate": str(btc_eur_rate) # Store exchange rate for reference
|
||||
}
|
||||
|
||||
# ===== BEANCOUNT PARSER =====
|
||||
|
|
@ -407,7 +408,12 @@ def determine_user_id(postings: list) -> Optional[str]:
|
|||
# ===== CASTLE CONVERTER =====
|
||||
|
||||
def convert_to_castle_entry(parsed: dict, btc_eur_rate: float, account_lookup: AccountLookup) -> dict:
|
||||
"""Convert parsed Beancount transaction to Castle format"""
|
||||
"""
|
||||
Convert parsed Beancount transaction to Castle format.
|
||||
|
||||
Sends SATS amounts with fiat metadata. The Castle API will automatically
|
||||
convert to EUR-based postings with SATS stored in metadata.
|
||||
"""
|
||||
|
||||
# Determine which user this transaction is for (based on user-specific accounts)
|
||||
user_id = determine_user_id(parsed['postings'])
|
||||
|
|
@ -435,10 +441,10 @@ def convert_to_castle_entry(parsed: dict, btc_eur_rate: float, account_lookup: A
|
|||
if eur_amount is None:
|
||||
raise ValueError(f"Could not determine amount for {posting['account']}")
|
||||
|
||||
# Convert EUR to sats
|
||||
# Convert EUR to sats (amount sent to API)
|
||||
sats = eur_to_sats(eur_amount, btc_eur_rate)
|
||||
|
||||
# Build metadata
|
||||
# Build metadata (API will extract fiat_currency and fiat_amount)
|
||||
metadata = build_metadata(eur_amount, btc_eur_rate)
|
||||
|
||||
lines.append({
|
||||
|
|
@ -456,7 +462,7 @@ def convert_to_castle_entry(parsed: dict, btc_eur_rate: float, account_lookup: A
|
|||
"meta": {
|
||||
"source": "beancount_import",
|
||||
"imported_at": datetime.now().isoformat(),
|
||||
"btc_eur_rate": btc_eur_rate,
|
||||
"btc_eur_rate": str(btc_eur_rate),
|
||||
"user_id": user_id # Track which user this transaction is for
|
||||
},
|
||||
"lines": lines
|
||||
|
|
@ -486,9 +492,18 @@ def upload_entry(entry: dict, api_key: str, dry_run: bool = False) -> dict:
|
|||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(url, json=entry, headers=headers)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
print(f" ❌ HTTP Error: {e}")
|
||||
if response.text:
|
||||
print(f" Response: {response.text}")
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f" ❌ Error: {e}")
|
||||
raise
|
||||
|
||||
# ===== MAIN IMPORT FUNCTION =====
|
||||
|
||||
|
|
@ -590,6 +605,8 @@ def import_beancount_file(beancount_file: str, dry_run: bool = False):
|
|||
|
||||
if success_count > 0 and not dry_run:
|
||||
print(f"\n✅ Successfully imported {success_count} transactions to Castle!")
|
||||
print(f"\n💡 Note: Transactions are stored in EUR with SATS in metadata.")
|
||||
print(f" Check Fava to see the imported entries.")
|
||||
|
||||
# ===== MAIN =====
|
||||
|
||||
|
|
|
|||
46
views_api.py
46
views_api.py
|
|
@ -654,8 +654,8 @@ async def api_create_journal_entry(
|
|||
result = await fava.add_entry(entry)
|
||||
logger.info(f"Journal entry submitted to Fava: {result.get('data', 'Unknown')}")
|
||||
|
||||
# Return mock JournalEntry for API compatibility
|
||||
# TODO: Query Fava to get the actual entry back with its hash
|
||||
# Return simplified JournalEntry for API compatibility
|
||||
# Note: Castle no longer stores entries in DB, Fava is the source of truth
|
||||
timestamp = datetime.now().timestamp()
|
||||
return JournalEntry(
|
||||
id=f"fava-{timestamp}",
|
||||
|
|
@ -665,18 +665,8 @@ async def api_create_journal_entry(
|
|||
created_at=datetime.now(),
|
||||
reference=data.reference,
|
||||
flag=data.flag if data.flag else JournalEntryFlag.CLEARED,
|
||||
lines=[
|
||||
EntryLine(
|
||||
id=f"fava-{timestamp}-{i}",
|
||||
journal_entry_id=f"fava-{timestamp}",
|
||||
account_id=line.account_id,
|
||||
amount=line.amount,
|
||||
description=line.description,
|
||||
metadata=line.metadata
|
||||
)
|
||||
for i, line in enumerate(data.lines)
|
||||
],
|
||||
meta={**data.meta, "source": "fava", "fava_response": result.get('data', 'Unknown')}
|
||||
lines=[], # Empty - entry is stored in Fava, not Castle DB
|
||||
meta={"source": "fava", "fava_response": result.get('data', 'Unknown')}
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -1068,33 +1058,17 @@ async def api_create_revenue_entry(
|
|||
result = await fava.add_entry(entry)
|
||||
logger.info(f"Revenue entry submitted to Fava: {result.get('data', 'Unknown')}")
|
||||
|
||||
# Return JournalEntry for API compatibility
|
||||
# Return simplified JournalEntry for API compatibility
|
||||
# Note: Castle no longer stores entries in DB, Fava is the source of truth
|
||||
return JournalEntry(
|
||||
id=entry_id, # Use the generated castle entry ID
|
||||
id=entry_id,
|
||||
description=data.description,
|
||||
entry_date=datetime.now(),
|
||||
created_by=wallet.wallet.id,
|
||||
created_at=datetime.now(),
|
||||
reference=castle_reference, # Use castle reference with unique ID
|
||||
flag=JournalEntryFlag.CLEARED, # Revenue entries are cleared
|
||||
lines=[
|
||||
EntryLine(
|
||||
id=f"line-1-{entry_id}",
|
||||
journal_entry_id=entry_id,
|
||||
account_id=payment_account.id,
|
||||
amount=amount_sats,
|
||||
description="Payment received",
|
||||
metadata={"fiat_currency": fiat_currency, "fiat_amount": str(fiat_amount)} if fiat_currency else {}
|
||||
),
|
||||
EntryLine(
|
||||
id=f"line-2-{entry_id}",
|
||||
journal_entry_id=entry_id,
|
||||
account_id=revenue_account.id,
|
||||
amount=-amount_sats,
|
||||
description="Revenue earned",
|
||||
metadata={}
|
||||
)
|
||||
],
|
||||
reference=castle_reference,
|
||||
flag=JournalEntryFlag.CLEARED,
|
||||
lines=[], # Empty - entry is stored in Fava, not Castle DB
|
||||
meta={"source": "fava", "fava_response": result.get('data', 'Unknown')}
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue