Completes Phase 1: Beancount patterns adoption

Implements core improvements from Phase 1 of the Beancount patterns adoption:

- Uses Decimal for fiat amounts to prevent floating point errors
- Adds a meta field to journal entries for a full audit trail
- Adds a flag field to journal entries for transaction status
- Migrates existing account names to a hierarchical format

This commit introduces a database migration to add the `flag` and `meta` columns to the `journal_entries` table. It also includes updates to the models, CRUD operations, and API endpoints to handle the new fields.
This commit is contained in:
padreug 2025-10-23 00:03:32 +02:00
parent 35d2057694
commit 1a28ec59eb
7 changed files with 616 additions and 31 deletions

View file

@ -1,3 +1,4 @@
from decimal import Decimal
from http import HTTPStatus
from fastapi import APIRouter, Depends, HTTPException
@ -43,6 +44,7 @@ from .models import (
ExpenseEntry,
GeneratePaymentInvoice,
JournalEntry,
JournalEntryFlag,
ManualPaymentRequest,
ReceivableEntry,
RecordPayment,
@ -231,14 +233,14 @@ async def api_create_expense_entry(
)
# Convert fiat to satoshis
amount_sats = await fiat_amount_as_satoshis(data.amount, data.currency)
amount_sats = await fiat_amount_as_satoshis(float(data.amount), data.currency)
# Store currency metadata
# Store currency metadata (store fiat_amount as string to preserve Decimal precision)
metadata = {
"fiat_currency": data.currency.upper(),
"fiat_amount": round(data.amount, ndigits=3),
"fiat_rate": amount_sats / data.amount if data.amount > 0 else 0,
"btc_rate": (data.amount / amount_sats * 100_000_000) if amount_sats > 0 else 0,
"fiat_amount": str(data.amount.quantize(Decimal("0.001"))), # Store as string with 3 decimal places
"fiat_rate": float(amount_sats) / float(data.amount) if data.amount > 0 else 0,
"btc_rate": float(data.amount) / float(amount_sats) * 100_000_000 if amount_sats > 0 else 0,
}
# Get or create expense account
@ -267,9 +269,19 @@ async def api_create_expense_entry(
# Create journal entry
# DR Expense, CR User Account (Liability or Equity)
description_suffix = f" ({metadata['fiat_amount']} {metadata['fiat_currency']})" if metadata else ""
# Add meta information for audit trail
entry_meta = {
"source": "api",
"created_via": "expense_entry",
"user_id": wallet.wallet.user,
"is_equity": data.is_equity,
}
entry_data = CreateJournalEntry(
description=data.description + description_suffix,
reference=data.reference,
meta=entry_meta,
lines=[
CreateEntryLine(
account_id=expense_account.id,
@ -315,14 +327,14 @@ async def api_create_receivable_entry(
)
# Convert fiat to satoshis
amount_sats = await fiat_amount_as_satoshis(data.amount, data.currency)
amount_sats = await fiat_amount_as_satoshis(float(data.amount), data.currency)
# Store currency metadata
# Store currency metadata (store fiat_amount as string to preserve Decimal precision)
metadata = {
"fiat_currency": data.currency.upper(),
"fiat_amount": round(data.amount, ndigits=3),
"fiat_rate": amount_sats / data.amount if data.amount > 0 else 0,
"btc_rate": (data.amount / amount_sats * 100_000_000) if amount_sats > 0 else 0,
"fiat_amount": str(data.amount.quantize(Decimal("0.001"))), # Store as string with 3 decimal places
"fiat_rate": float(amount_sats) / float(data.amount) if data.amount > 0 else 0,
"btc_rate": float(data.amount) / float(amount_sats) * 100_000_000 if amount_sats > 0 else 0,
}
# Get or create revenue account
@ -343,9 +355,19 @@ async def api_create_receivable_entry(
# Create journal entry
# DR Accounts Receivable (User), CR Revenue
description_suffix = f" ({metadata['fiat_amount']} {metadata['fiat_currency']})" if metadata else ""
# Add meta information for audit trail
entry_meta = {
"source": "api",
"created_via": "receivable_entry",
"debtor_user_id": data.user_id,
}
entry_data = CreateJournalEntry(
description=data.description + description_suffix,
reference=data.reference,
flag=JournalEntryFlag.PENDING, # Receivables start as pending until paid
meta=entry_meta,
lines=[
CreateEntryLine(
account_id=user_receivable.id,
@ -447,7 +469,7 @@ async def api_get_my_balance(
for user_balance in all_balances:
for currency, amount in user_balance.fiat_balances.items():
if currency not in total_fiat_balances:
total_fiat_balances[currency] = 0.0
total_fiat_balances[currency] = Decimal("0")
# Add all balances (positive and negative)
total_fiat_balances[currency] += amount
@ -579,9 +601,20 @@ async def api_record_payment(
# Create journal entry to record payment
# DR Lightning Balance, CR Accounts Receivable (User)
# This reduces what the user owes
# Add meta information for audit trail
entry_meta = {
"source": "lightning_payment",
"created_via": "record_payment",
"payment_hash": data.payment_hash,
"payer_user_id": wallet.wallet.user,
}
entry_data = CreateJournalEntry(
description=f"Lightning payment from user {wallet.wallet.user[:8]}",
reference=data.payment_hash,
flag=JournalEntryFlag.CLEARED, # Payment is immediately cleared
meta=entry_meta,
lines=[
CreateEntryLine(
account_id=lightning_account.id,