diff --git a/helper/import_beancount.py b/helper/import_beancount.py index 30b0236..eae4033 100755 --- a/helper/import_beancount.py +++ b/helper/import_beancount.py @@ -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" } - response = requests.post(url, json=entry, headers=headers) - response.raise_for_status() - return response.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 ===== diff --git a/views_api.py b/views_api.py index b375292..73ebf20 100644 --- a/views_api.py +++ b/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')} )