Adds support for on-chain Bitcoin payments by: - Introducing a new `Assets:Bitcoin:OnChain` account. - Updating the `SettleReceivable` and `PayUser` models to include `txid` for storing transaction IDs. - Modifying the API endpoints to handle `btc_onchain` as a valid payment method and associate it with the new account. This allows tracking on-chain Bitcoin transactions separately from Lightning Network payments.
249 lines
8.5 KiB
Python
249 lines
8.5 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", "check", "lightning", "btc_onchain", "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)
|
|
payment_hash: Optional[str] = None # For lightning payments
|
|
txid: Optional[str] = None # For on-chain Bitcoin transactions
|
|
|
|
|
|
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", "check", "lightning", "btc_onchain", "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
|
|
txid: Optional[str] = None # For on-chain Bitcoin transactions
|
|
|
|
|
|
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")
|