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,4 +1,5 @@
from datetime import datetime
from decimal import Decimal
from enum import Enum
from typing import Optional
@ -13,6 +14,14 @@ class AccountType(str, Enum):
EXPENSE = "expense"
class JournalEntryFlag(str, Enum):
"""Transaction status flags (Beancount-style)"""
CLEARED = "*" # Fully reconciled/confirmed
PENDING = "!" # Not yet confirmed/awaiting approval
FLAGGED = "#" # Needs review/attention
VOID = "x" # Voided/cancelled entry
class Account(BaseModel):
id: str
name: str
@ -55,6 +64,8 @@ class JournalEntry(BaseModel):
created_at: datetime
reference: Optional[str] = None # Invoice ID or reference number
lines: list[EntryLine] = []
flag: JournalEntryFlag = JournalEntryFlag.CLEARED # Transaction status
meta: dict = {} # Metadata: source, tags, links, notes, etc.
class CreateJournalEntry(BaseModel):
@ -62,20 +73,22 @@ class CreateJournalEntry(BaseModel):
entry_date: Optional[datetime] = None
reference: Optional[str] = None
lines: list[CreateEntryLine]
flag: JournalEntryFlag = JournalEntryFlag.CLEARED
meta: dict = {}
class UserBalance(BaseModel):
user_id: str
balance: int # positive = castle owes user, negative = user owes castle
accounts: list[Account] = []
fiat_balances: dict[str, float] = {} # e.g. {"EUR": 250.0, "USD": 100.0}
fiat_balances: dict[str, Decimal] = {} # e.g. {"EUR": Decimal("250.0"), "USD": Decimal("100.0")}
class ExpenseEntry(BaseModel):
"""Helper model for creating expense entries"""
description: str
amount: float # Amount in the specified currency (or satoshis if currency is None)
amount: Decimal # Amount in the specified currency (or satoshis if currency is None)
expense_account: str # account name or ID
is_equity: bool = False # True = equity contribution, False = liability (castle owes user)
user_wallet: str
@ -87,7 +100,7 @@ class ReceivableEntry(BaseModel):
"""Helper model for creating accounts receivable entries"""
description: str
amount: float # Amount in the specified currency (or satoshis if currency is None)
amount: Decimal # Amount in the specified currency (or satoshis if currency is None)
revenue_account: str # account name or ID
user_id: str # The user_id (not wallet_id) of the user who owes the castle
reference: Optional[str] = None
@ -98,7 +111,7 @@ class RevenueEntry(BaseModel):
"""Helper model for creating revenue entries"""
description: str
amount: float # Amount in the specified currency (or satoshis if currency is None)
amount: Decimal # Amount in the specified currency (or satoshis if currency is None)
revenue_account: str
payment_method_account: str # e.g., "Cash", "Bank", "Lightning"
reference: Optional[str] = None