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:
parent
35d2057694
commit
1a28ec59eb
7 changed files with 616 additions and 31 deletions
55
views_api.py
55
views_api.py
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue