diff --git a/beancount_format.py b/beancount_format.py index 2ba5b96..74ba53c 100644 --- a/beancount_format.py +++ b/beancount_format.py @@ -497,7 +497,8 @@ def format_payment_entry( fiat_currency: Optional[str] = None, fiat_amount: Optional[Decimal] = None, payment_hash: Optional[str] = None, - reference: Optional[str] = None + reference: Optional[str] = None, + settled_entry_links: Optional[List[str]] = None ) -> Dict[str, Any]: """ Format a payment entry (Lightning payment recorded). @@ -516,6 +517,7 @@ def format_payment_entry( fiat_amount: Optional fiat amount (unsigned) payment_hash: Lightning payment hash reference: Optional reference + settled_entry_links: List of expense/receivable links being settled (e.g., ["exp-abc123"]) Returns: Fava API entry dict @@ -584,6 +586,8 @@ def format_payment_entry( entry_meta["payment-hash"] = payment_hash links = [] + if settled_entry_links: + links.extend(settled_entry_links) if reference: links.append(reference) if payment_hash: @@ -594,7 +598,7 @@ def format_payment_entry( flag="*", # Cleared (payment already happened) narration=description, postings=postings, - tags=["lightning-payment"], + tags=["lightning-payment", "settlement"], links=links, meta=entry_meta ) @@ -713,7 +717,8 @@ def format_net_settlement_entry( description: str, entry_date: date, payment_hash: Optional[str] = None, - reference: Optional[str] = None + reference: Optional[str] = None, + settled_entry_links: Optional[List[str]] = None ) -> Dict[str, Any]: """ Format a net settlement payment entry (user paying net balance). @@ -743,6 +748,7 @@ def format_net_settlement_entry( entry_date: Date of payment payment_hash: Lightning payment hash reference: Optional reference + settled_entry_links: List of expense/receivable links being settled (e.g., ["exp-abc123", "rcv-def456"]) Returns: Fava API entry dict @@ -780,6 +786,8 @@ def format_net_settlement_entry( entry_meta["payment-hash"] = payment_hash links = [] + if settled_entry_links: + links.extend(settled_entry_links) if reference: links.append(reference) if payment_hash: diff --git a/tasks.py b/tasks.py index 1a8327d..b44c883 100644 --- a/tasks.py +++ b/tasks.py @@ -287,6 +287,18 @@ async def on_invoice_paid(payment: Payment) -> None: logger.error("Lightning account 'Assets:Bitcoin:Lightning' not found") return + # Query for unsettled entries to link this settlement back to them + # Net settlement can settle both expenses and receivables + settled_links = [] + try: + unsettled_expenses = await fava.get_unsettled_entries_bql(user_id, "expense") + settled_links.extend([e["link"] for e in unsettled_expenses if e.get("link")]) + unsettled_receivables = await fava.get_unsettled_entries_bql(user_id, "receivable") + settled_links.extend([e["link"] for e in unsettled_receivables if e.get("link")]) + except Exception as e: + logger.warning(f"Could not query unsettled entries for settlement links: {e}") + # Continue without links - settlement will still be recorded + # Format as net settlement transaction entry = format_net_settlement_entry( user_id=user_id, @@ -301,7 +313,8 @@ async def on_invoice_paid(payment: Payment) -> None: description=f"Lightning payment settlement from user {user_id[:8]}", entry_date=datetime.now().date(), payment_hash=payment.payment_hash, - reference=payment.payment_hash + reference=payment.payment_hash, + settled_entry_links=settled_links if settled_links else None ) # Submit to Fava diff --git a/views_api.py b/views_api.py index ffa02f4..a3e206d 100644 --- a/views_api.py +++ b/views_api.py @@ -1704,6 +1704,10 @@ async def api_record_payment( status_code=HTTPStatus.NOT_FOUND, detail="Lightning account not found" ) + # Get unsettled receivable entries to link to this settlement + unsettled = await fava.get_unsettled_entries_bql(target_user_id, "receivable") + settled_links = [e["link"] for e in unsettled if e.get("link")] + # Format payment entry and submit to Fava entry = format_payment_entry( user_id=target_user_id, @@ -1716,7 +1720,8 @@ async def api_record_payment( fiat_currency=fiat_currency, fiat_amount=fiat_amount, payment_hash=data.payment_hash, - reference=data.payment_hash + reference=data.payment_hash, + settled_entry_links=settled_links ) logger.info(f"Formatted payment entry: {entry}") @@ -1764,6 +1769,10 @@ async def api_pay_user( fava = get_fava_client() + # Get unsettled expense entries to link to this settlement + unsettled = await fava.get_unsettled_entries_bql(user_id, "expense") + settled_links = [e["link"] for e in unsettled if e.get("link")] + entry = format_payment_entry( user_id=user_id, payment_account=lightning_account.name, @@ -1772,7 +1781,8 @@ async def api_pay_user( description=f"Payment to user {user_id[:8]}", entry_date=datetime.now().date(), is_payable=True, # Castle paying user - reference=f"PAY-{user_id[:8]}" + reference=f"PAY-{user_id[:8]}", + settled_entry_links=settled_links ) # Submit to Fava @@ -1897,6 +1907,12 @@ async def api_settle_receivable( fiat_currency = data.currency.upper() if data.currency else None fiat_amount = Decimal(str(data.amount)) if data.currency else None + # Get settled entry links (use provided or auto-query unsettled) + settled_links = data.settled_entry_links + if not settled_links: + unsettled = await fava.get_unsettled_entries_bql(data.user_id, "receivable") + settled_links = [e["link"] for e in unsettled if e.get("link")] + entry = format_payment_entry( user_id=data.user_id, payment_account=payment_account.name, @@ -1908,7 +1924,8 @@ async def api_settle_receivable( fiat_currency=fiat_currency, fiat_amount=fiat_amount, payment_hash=data.payment_hash, - reference=data.reference or f"MANUAL-{data.user_id[:8]}" + reference=data.reference or f"MANUAL-{data.user_id[:8]}", + settled_entry_links=settled_links ) # Add additional metadata to entry @@ -2051,6 +2068,12 @@ async def api_pay_user( fiat_currency = None fiat_amount = None + # Get settled entry links (use provided or auto-query unsettled) + settled_links = data.settled_entry_links + if not settled_links: + unsettled = await fava.get_unsettled_entries_bql(data.user_id, "expense") + settled_links = [e["link"] for e in unsettled if e.get("link")] + entry = format_payment_entry( user_id=data.user_id, payment_account=payment_account.name, @@ -2062,7 +2085,8 @@ async def api_pay_user( fiat_currency=fiat_currency, fiat_amount=fiat_amount, payment_hash=data.payment_hash, - reference=data.reference or f"PAY-{data.user_id[:8]}" + reference=data.reference or f"PAY-{data.user_id[:8]}", + settled_entry_links=settled_links ) # Add additional metadata to entry @@ -2550,6 +2574,10 @@ async def api_approve_manual_payment_request( fava = get_fava_client() + # Get unsettled expense entries to link to this settlement + unsettled = await fava.get_unsettled_entries_bql(request.user_id, "expense") + settled_links = [e["link"] for e in unsettled if e.get("link")] + entry = format_payment_entry( user_id=request.user_id, payment_account=lightning_account.name, @@ -2558,7 +2586,8 @@ async def api_approve_manual_payment_request( description=f"Manual payment to user: {request.description}", entry_date=datetime.now().date(), is_payable=True, # Castle paying user - reference=f"MPR-{request.id}" + reference=f"MPR-{request.id}", + settled_entry_links=settled_links ) # Submit to Fava