""" Updated tasks.py for Fava integration. This shows how on_invoice_paid() should be modified to submit to Fava instead of storing in Castle DB. """ from decimal import Decimal from datetime import datetime from lnbits.core.models import Payment from loguru import logger from .fava_client import get_fava_client from .beancount_format import format_payment_entry from .crud import get_account_by_name, get_or_create_user_account from .models import AccountType async def on_invoice_paid_fava(payment: Payment) -> None: """ Handle a paid Castle invoice by automatically submitting to Fava. This function is called automatically when any invoice on the Castle wallet is paid. It checks if the invoice is a Castle payment and records it in Beancount via Fava. Key differences from original: - NO database storage in Castle - Formats as Beancount transaction - Submits to Fava API - Fava writes to Beancount file """ # Only process Castle-specific payments if not payment.extra or payment.extra.get("tag") != "castle": return user_id = payment.extra.get("user_id") if not user_id: logger.warning(f"Castle invoice {payment.payment_hash} missing user_id in metadata") return # Check if payment already recorded (idempotency) # NOTE: With Fava, we need to query Fava instead of Castle DB! fava = get_fava_client() try: # Query Fava for existing entry with this payment hash query = f"SELECT * WHERE links ~ 'ln-{payment.payment_hash[:16]}'" async with httpx.AsyncClient(timeout=5.0) as client: response = await client.get( f"{fava.base_url}/query", params={"query_string": query} ) result = response.json() if result.get('data', {}).get('rows'): logger.info(f"Payment {payment.payment_hash} already recorded, skipping") return except Exception as e: logger.warning(f"Could not check for duplicate payment: {e}") # Continue anyway - Fava/Beancount will catch duplicate if it exists logger.info(f"Recording Castle payment {payment.payment_hash} for user {user_id[:8]}") try: # Convert amount from millisatoshis to satoshis amount_sats = payment.amount // 1000 # Extract fiat metadata from invoice (if present) fiat_currency = None fiat_amount = None if payment.extra: fiat_currency = payment.extra.get("fiat_currency") fiat_amount_str = payment.extra.get("fiat_amount") if fiat_amount_str: fiat_amount = Decimal(str(fiat_amount_str)) # Get user's receivable account (what user owes) user_receivable = await get_or_create_user_account( user_id, AccountType.ASSET, "Accounts Receivable" ) # Get lightning account lightning_account = await get_account_by_name("Assets:Bitcoin:Lightning") if not lightning_account: logger.error("Lightning account 'Assets:Bitcoin:Lightning' not found") return # Format as Beancount transaction entry = format_payment_entry( user_id=user_id, payment_account=lightning_account.name, # "Assets:Bitcoin:Lightning" payable_or_receivable_account=user_receivable.name, # "Assets:Receivable:User-{id}" amount_sats=amount_sats, description=f"Lightning payment from user {user_id[:8]}", entry_date=datetime.now().date(), is_payable=False, # User paying castle (receivable settlement) fiat_currency=fiat_currency, fiat_amount=fiat_amount, payment_hash=payment.payment_hash, reference=payment.payment_hash # For linking ) # Submit to Fava result = await fava.add_entry(entry) logger.info( f"Successfully recorded payment {payment.payment_hash} to Fava: " f"{result.get('data', 'Unknown')}" ) except Exception as e: logger.error(f"Error recording Castle payment {payment.payment_hash}: {e}") raise # ALSO UPDATE: on_invoice_paid() in tasks.py # Replace the entire function body (lines 130-228) with the code above!