Adds a date field to expense entries for better tracking and reporting. This allows users to specify the date of the expense transaction, providing more accurate financial records.
246 lines
8.2 KiB
Python
246 lines
8.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.)
|
|
entry_date: Optional[datetime] = None # Date of the expense transaction
|
|
|
|
|
|
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
|
|
user_id: Optional[str] = None # For admin-generated settlement invoices
|
|
|
|
|
|
class RecordPayment(BaseModel):
|
|
"""Record a payment"""
|
|
|
|
payment_hash: str
|
|
|
|
|
|
class SettleReceivable(BaseModel):
|
|
"""Manually settle a receivable (user pays castle in person)"""
|
|
|
|
user_id: str
|
|
amount: Decimal # Amount in the specified currency (or satoshis if currency is None)
|
|
payment_method: str # "cash", "bank_transfer", "lightning", "other"
|
|
description: str # Description of the payment
|
|
reference: Optional[str] = None # Optional reference (receipt number, transaction ID, etc.)
|
|
currency: Optional[str] = None # If None, amount is in satoshis. Otherwise, fiat currency code (EUR, USD, etc.)
|
|
amount_sats: Optional[int] = None # Equivalent amount in sats (for reference/conversion tracking)
|
|
|
|
|
|
class PayUser(BaseModel):
|
|
"""Pay a user (castle pays user for expense/liability)"""
|
|
|
|
user_id: str
|
|
amount: Decimal # Amount in the specified currency (or satoshis if currency is None)
|
|
payment_method: str # "cash", "bank_transfer", "lightning", "check", "other"
|
|
description: Optional[str] = None # Description of the payment
|
|
reference: Optional[str] = None # Optional reference (receipt number, transaction ID, etc.)
|
|
currency: Optional[str] = None # If None, amount is in satoshis. Otherwise, fiat currency code (EUR, USD, etc.)
|
|
amount_sats: Optional[int] = None # Equivalent amount in sats (for reference/conversion tracking)
|
|
payment_hash: Optional[str] = None # For lightning payments
|
|
|
|
|
|
class AssertionStatus(str, Enum):
|
|
"""Status of a balance assertion"""
|
|
PENDING = "pending" # Not yet checked
|
|
PASSED = "passed" # Assertion passed (balance matches)
|
|
FAILED = "failed" # Assertion failed (balance mismatch)
|
|
|
|
|
|
class BalanceAssertion(BaseModel):
|
|
"""Assert expected balance at a specific date for reconciliation"""
|
|
id: str
|
|
date: datetime
|
|
account_id: str
|
|
expected_balance_sats: int # Expected balance in satoshis
|
|
expected_balance_fiat: Optional[Decimal] = None # Optional fiat balance
|
|
fiat_currency: Optional[str] = None # Currency for fiat balance (EUR, USD, etc.)
|
|
tolerance_sats: int = 0 # Allow +/- this much difference in sats
|
|
tolerance_fiat: Decimal = Decimal("0") # Allow +/- this much difference in fiat
|
|
checked_balance_sats: Optional[int] = None # Actual balance found
|
|
checked_balance_fiat: Optional[Decimal] = None # Actual fiat balance found
|
|
difference_sats: Optional[int] = None # Difference in sats
|
|
difference_fiat: Optional[Decimal] = None # Difference in fiat
|
|
status: AssertionStatus = AssertionStatus.PENDING
|
|
created_by: str
|
|
created_at: datetime
|
|
checked_at: Optional[datetime] = None
|
|
|
|
|
|
class CreateBalanceAssertion(BaseModel):
|
|
"""Create a balance assertion"""
|
|
account_id: str
|
|
date: Optional[datetime] = None # If None, use current time
|
|
expected_balance_sats: int
|
|
expected_balance_fiat: Optional[Decimal] = None
|
|
fiat_currency: Optional[str] = None
|
|
tolerance_sats: int = 0
|
|
tolerance_fiat: Decimal = Decimal("0")
|