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.
181 lines
5.2 KiB
Python
181 lines
5.2 KiB
Python
from datetime import datetime
|
|
from decimal import Decimal
|
|
from enum import Enum
|
|
from typing import Optional
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
class AccountType(str, Enum):
|
|
ASSET = "asset"
|
|
LIABILITY = "liability"
|
|
EQUITY = "equity"
|
|
REVENUE = "revenue"
|
|
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
|
|
account_type: AccountType
|
|
description: Optional[str] = None
|
|
user_id: Optional[str] = None # For user-specific accounts
|
|
created_at: datetime
|
|
|
|
|
|
class CreateAccount(BaseModel):
|
|
name: str
|
|
account_type: AccountType
|
|
description: Optional[str] = None
|
|
user_id: Optional[str] = None
|
|
|
|
|
|
class EntryLine(BaseModel):
|
|
id: str
|
|
journal_entry_id: str
|
|
account_id: str
|
|
debit: int = 0 # in satoshis
|
|
credit: int = 0 # in satoshis
|
|
description: Optional[str] = None
|
|
metadata: dict = {} # Stores currency info: fiat_currency, fiat_amount, fiat_rate, etc.
|
|
|
|
|
|
class CreateEntryLine(BaseModel):
|
|
account_id: str
|
|
debit: int = 0
|
|
credit: int = 0
|
|
description: Optional[str] = None
|
|
metadata: dict = {} # Stores currency info
|
|
|
|
|
|
class JournalEntry(BaseModel):
|
|
id: str
|
|
description: str
|
|
entry_date: datetime
|
|
created_by: str # wallet ID of user who created it
|
|
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):
|
|
description: str
|
|
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, Decimal] = {} # e.g. {"EUR": Decimal("250.0"), "USD": Decimal("100.0")}
|
|
|
|
|
|
class ExpenseEntry(BaseModel):
|
|
"""Helper model for creating expense entries"""
|
|
|
|
description: str
|
|
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
|
|
reference: Optional[str] = None
|
|
currency: Optional[str] = None # If None, amount is in satoshis. Otherwise, fiat currency code (EUR, USD, etc.)
|
|
|
|
|
|
class ReceivableEntry(BaseModel):
|
|
"""Helper model for creating accounts receivable entries"""
|
|
|
|
description: str
|
|
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
|
|
currency: Optional[str] = None # If None, amount is in satoshis. Otherwise, fiat currency code
|
|
|
|
|
|
class RevenueEntry(BaseModel):
|
|
"""Helper model for creating revenue entries"""
|
|
|
|
description: str
|
|
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
|
|
currency: Optional[str] = None # If None, amount is in satoshis. Otherwise, fiat currency code
|
|
|
|
|
|
class CastleSettings(BaseModel):
|
|
"""Settings for the Castle extension"""
|
|
|
|
castle_wallet_id: Optional[str] = None # The wallet ID that represents the Castle
|
|
updated_at: datetime = Field(default_factory=lambda: datetime.now())
|
|
|
|
@classmethod
|
|
def is_admin_only(cls) -> bool:
|
|
return True
|
|
|
|
|
|
class UserCastleSettings(CastleSettings):
|
|
"""User-specific settings (stored with user_id)"""
|
|
|
|
id: str
|
|
|
|
|
|
class UserWalletSettings(BaseModel):
|
|
"""Per-user wallet settings"""
|
|
|
|
user_wallet_id: Optional[str] = None # The wallet ID for this specific user
|
|
updated_at: datetime = Field(default_factory=lambda: datetime.now())
|
|
|
|
|
|
class StoredUserWalletSettings(UserWalletSettings):
|
|
"""Stored user wallet settings with user ID"""
|
|
|
|
id: str # user_id
|
|
|
|
|
|
class ManualPaymentRequest(BaseModel):
|
|
"""Manual payment request from user to castle"""
|
|
|
|
id: str
|
|
user_id: str
|
|
amount: int # in satoshis
|
|
description: str
|
|
status: str = "pending" # pending, approved, rejected
|
|
created_at: datetime
|
|
reviewed_at: Optional[datetime] = None
|
|
reviewed_by: Optional[str] = None # user_id of castle admin who reviewed
|
|
journal_entry_id: Optional[str] = None # set when approved
|
|
|
|
|
|
class CreateManualPaymentRequest(BaseModel):
|
|
"""Create a manual payment request"""
|
|
|
|
amount: int
|
|
description: str
|
|
|
|
|
|
class GeneratePaymentInvoice(BaseModel):
|
|
"""Generate payment invoice request"""
|
|
|
|
amount: int
|
|
|
|
|
|
class RecordPayment(BaseModel):
|
|
"""Record a payment"""
|
|
|
|
payment_hash: str
|