Adds fiat currency metadata to payments

Adds fiat currency information to payment invoices and ledger entries.

This allows for tracking the fiat value of transactions and provides a more complete financial picture. Calculates the fiat amount proportionally based on the user's balance and includes the fiat currency, amount, and exchange rates in the invoice's extra data. This data is then extracted and added to the ledger entry's metadata when recording the payment.
This commit is contained in:
padreug 2025-11-02 02:52:41 +01:00
parent 8f35788e1a
commit 3add13075c
2 changed files with 77 additions and 1 deletions

View file

@ -161,6 +161,23 @@ async def on_invoice_paid(payment: Payment) -> None:
# Convert amount from millisatoshis to satoshis
amount_sats = payment.amount // 1000
# Extract fiat metadata from invoice (if present)
from decimal import Decimal
line_metadata = {}
if payment.extra:
fiat_currency = payment.extra.get("fiat_currency")
fiat_amount = payment.extra.get("fiat_amount")
fiat_rate = payment.extra.get("fiat_rate")
btc_rate = payment.extra.get("btc_rate")
if fiat_currency and fiat_amount:
line_metadata = {
"fiat_currency": fiat_currency,
"fiat_amount": str(fiat_amount),
"fiat_rate": fiat_rate,
"btc_rate": btc_rate,
}
# Get user's receivable account (what user owes)
user_receivable = await get_or_create_user_account(
user_id, AccountType.ASSET, "Accounts Receivable"
@ -193,12 +210,14 @@ async def on_invoice_paid(payment: Payment) -> None:
debit=amount_sats,
credit=0,
description="Lightning payment received",
metadata=line_metadata,
),
CreateEntryLine(
account_id=user_receivable.id,
debit=0,
credit=amount_sats,
description="Payment applied to balance",
metadata=line_metadata,
),
],
)

View file

@ -585,13 +585,52 @@ async def api_generate_payment_invoice(
# Get castle wallet ID
castle_wallet_id = await check_castle_wallet_configured()
# Get user's balance to calculate fiat metadata
user_balance = await get_user_balance(target_user_id)
# Calculate proportional fiat amount for this invoice
invoice_extra = {"tag": "castle", "user_id": target_user_id}
if user_balance.fiat_balances:
# Simple single-currency solution: use the first (and should be only) currency
currencies = list(user_balance.fiat_balances.keys())
if len(currencies) > 1:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"User has multiple currencies ({', '.join(currencies)}). Please settle to a single currency first.",
)
if len(currencies) == 1:
fiat_currency = currencies[0]
total_fiat_balance = user_balance.fiat_balances[fiat_currency]
total_sat_balance = abs(user_balance.balance) # Use absolute value
if total_sat_balance > 0:
# Calculate proportional fiat amount for this invoice
# fiat_amount = (invoice_amount / total_sats) * total_fiat
from decimal import Decimal
proportion = Decimal(data.amount) / Decimal(total_sat_balance)
invoice_fiat_amount = abs(total_fiat_balance) * proportion
# Calculate fiat rate (sats per fiat unit)
fiat_rate = float(data.amount) / float(invoice_fiat_amount) if invoice_fiat_amount > 0 else 0
btc_rate = float(invoice_fiat_amount) / float(data.amount) * 100_000_000 if data.amount > 0 else 0
invoice_extra.update({
"fiat_currency": fiat_currency,
"fiat_amount": str(invoice_fiat_amount.quantize(Decimal("0.001"))),
"fiat_rate": fiat_rate,
"btc_rate": btc_rate,
})
# Create invoice on castle wallet
invoice_data = CreateInvoice(
out=False,
amount=data.amount,
memo=f"Payment from user {target_user_id[:8]} to Castle",
unit="sat",
extra={"tag": "castle", "user_id": target_user_id},
extra=invoice_extra,
)
payment = await create_payment_request(castle_wallet_id, invoice_data)
@ -663,6 +702,22 @@ async def api_record_payment(
# Convert amount from millisatoshis to satoshis
amount_sats = payment.amount // 1000
# Extract fiat metadata from invoice (if present)
line_metadata = {}
if payment.extra and isinstance(payment.extra, dict):
fiat_currency = payment.extra.get("fiat_currency")
fiat_amount = payment.extra.get("fiat_amount")
fiat_rate = payment.extra.get("fiat_rate")
btc_rate = payment.extra.get("btc_rate")
if fiat_currency and fiat_amount:
line_metadata = {
"fiat_currency": fiat_currency,
"fiat_amount": str(fiat_amount),
"fiat_rate": fiat_rate,
"btc_rate": btc_rate,
}
# Get user's receivable account (what user owes)
user_receivable = await get_or_create_user_account(
target_user_id, AccountType.ASSET, "Accounts Receivable"
@ -698,12 +753,14 @@ async def api_record_payment(
debit=amount_sats,
credit=0,
description="Lightning payment received",
metadata=line_metadata,
),
CreateEntryLine(
account_id=user_receivable.id,
debit=0,
credit=amount_sats,
description="Payment applied to balance",
metadata=line_metadata,
),
],
)