Adds fiat settlement entry formatting

Introduces a function to format fiat settlement entries for Beancount, handling cash, bank transfers, and other non-lightning payments.

This allows for recording transactions in fiat currency with sats as metadata.

Updates the API endpoint to use the new function when settling receivables with fiat currencies.
This commit is contained in:
padreug 2025-11-10 10:51:55 +01:00
parent 472c4e2164
commit 490b361268
2 changed files with 148 additions and 27 deletions

View file

@ -528,6 +528,112 @@ def format_payment_entry(
) )
def format_fiat_settlement_entry(
user_id: str,
payment_account: str,
payable_or_receivable_account: str,
fiat_amount: Decimal,
fiat_currency: str,
amount_sats: int,
description: str,
entry_date: date,
is_payable: bool = True,
payment_method: str = "cash",
reference: Optional[str] = None
) -> Dict[str, Any]:
"""
Format a fiat (cash/bank) settlement entry.
Unlike Lightning payments, fiat settlements use fiat currency as the primary amount
with SATS stored as metadata for reference.
Args:
user_id: User ID
payment_account: Payment method account (e.g., "Assets:Cash", "Assets:Bank")
payable_or_receivable_account: User's account being settled
fiat_amount: Amount in fiat currency (unsigned)
fiat_currency: Fiat currency code (EUR, USD, etc.)
amount_sats: Equivalent amount in satoshis (for metadata only)
description: Payment description
entry_date: Date of settlement
is_payable: True if castle paying user (payable), False if user paying castle (receivable)
payment_method: Payment method (cash, bank_transfer, check, etc.)
reference: Optional reference
Returns:
Fava API entry dict
"""
fiat_amount_abs = abs(fiat_amount)
amount_sats_abs = abs(amount_sats)
if is_payable:
# Castle paying user: DR Payable, CR Cash/Bank
postings = [
{
"account": payable_or_receivable_account,
"amount": f"{fiat_amount_abs:.2f} {fiat_currency}",
"meta": {
"sats-equivalent": str(amount_sats_abs)
}
},
{
"account": payment_account,
"amount": f"-{fiat_amount_abs:.2f} {fiat_currency}",
"meta": {
"sats-equivalent": str(amount_sats_abs)
}
}
]
else:
# User paying castle: DR Cash/Bank, CR Receivable
postings = [
{
"account": payment_account,
"amount": f"{fiat_amount_abs:.2f} {fiat_currency}",
"meta": {
"sats-equivalent": str(amount_sats_abs)
}
},
{
"account": payable_or_receivable_account,
"amount": f"-{fiat_amount_abs:.2f} {fiat_currency}",
"meta": {
"sats-equivalent": str(amount_sats_abs)
}
}
]
# Map payment method to appropriate source and tag
payment_method_map = {
"cash": ("cash_settlement", "cash-payment"),
"bank_transfer": ("bank_settlement", "bank-transfer"),
"check": ("check_settlement", "check-payment"),
"btc_onchain": ("onchain_settlement", "onchain-payment"),
"other": ("manual_settlement", "manual-payment")
}
source, tag = payment_method_map.get(payment_method.lower(), ("manual_settlement", "manual-payment"))
entry_meta = {
"user-id": user_id,
"source": source
}
links = []
if reference:
links.append(reference)
return format_transaction(
date_val=entry_date,
flag="*", # Cleared (payment already happened)
narration=description,
postings=postings,
tags=[tag],
links=links,
meta=entry_meta
)
def format_net_settlement_entry( def format_net_settlement_entry(
user_id: str, user_id: str,
payment_account: str, payment_account: str,

View file

@ -1569,43 +1569,58 @@ async def api_settle_receivable(
# DR Cash/Bank (asset increased), CR Accounts Receivable (asset decreased) # DR Cash/Bank (asset increased), CR Accounts Receivable (asset decreased)
# This records that user paid their debt # This records that user paid their debt
from .fava_client import get_fava_client from .fava_client import get_fava_client
from .beancount_format import format_payment_entry from .beancount_format import format_payment_entry, format_fiat_settlement_entry
from decimal import Decimal from decimal import Decimal
fava = get_fava_client() fava = get_fava_client()
# Determine amount and currency # Determine if this is a fiat or lightning payment
if data.currency: is_fiat_payment = data.currency and data.payment_method.lower() in [
# Fiat currency payment (e.g., EUR, USD) "cash", "bank_transfer", "check", "other"
# Use the sats equivalent for the journal entry to match the receivable ]
if is_fiat_payment:
# Fiat currency payment (cash, bank transfer, etc.)
# Record in fiat currency with sats as metadata
if not data.amount_sats: if not data.amount_sats:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, status_code=HTTPStatus.BAD_REQUEST,
detail="amount_sats is required when settling with fiat currency" detail="amount_sats is required when settling with fiat currency"
) )
amount_in_sats = data.amount_sats
fiat_currency = data.currency.upper()
fiat_amount = data.amount
else:
# Satoshi payment
amount_in_sats = int(data.amount)
fiat_currency = None
fiat_amount = None
# Format payment entry entry = format_fiat_settlement_entry(
entry = format_payment_entry( user_id=data.user_id,
user_id=data.user_id, payment_account=payment_account.name,
payment_account=payment_account.name, payable_or_receivable_account=user_receivable.name,
payable_or_receivable_account=user_receivable.name, fiat_amount=Decimal(str(data.amount)),
amount_sats=amount_in_sats, fiat_currency=data.currency.upper(),
description=data.description, amount_sats=data.amount_sats,
entry_date=datetime.now().date(), description=data.description,
is_payable=False, # User paying castle (receivable settlement) entry_date=datetime.now().date(),
fiat_currency=fiat_currency, is_payable=False, # User paying castle (receivable settlement)
fiat_amount=fiat_amount, payment_method=data.payment_method,
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]}" )
) else:
# Lightning or BTC onchain payment
# Record in SATS with optional fiat metadata
amount_in_sats = data.amount_sats if data.amount_sats else int(data.amount)
fiat_currency = data.currency.upper() if data.currency else None
fiat_amount = Decimal(str(data.amount)) if data.currency else None
entry = format_payment_entry(
user_id=data.user_id,
payment_account=payment_account.name,
payable_or_receivable_account=user_receivable.name,
amount_sats=amount_in_sats,
description=data.description,
entry_date=datetime.now().date(),
is_payable=False, # User paying castle (receivable settlement)
fiat_currency=fiat_currency,
fiat_amount=fiat_amount,
payment_hash=data.payment_hash,
reference=data.reference or f"MANUAL-{data.user_id[:8]}"
)
# Add additional metadata to entry # Add additional metadata to entry
if "meta" not in entry: if "meta" not in entry: