Adds Fava integration for invoice payments
Implements a new handler to process Castle invoice payments by submitting them to Fava, which in turn writes them to a Beancount file. This approach avoids storing payment data directly in the Castle database. The handler formats the payment as a Beancount transaction, includes fiat currency if available, and queries Fava to prevent duplicate entries. The commit also updates documentation to reflect the changes to the invoice processing workflow.
This commit is contained in:
parent
750692a2f0
commit
3c925abe9e
1 changed files with 119 additions and 0 deletions
119
tasks_fava.py
Normal file
119
tasks_fava.py
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
"""
|
||||||
|
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!
|
||||||
Loading…
Add table
Add a link
Reference in a new issue