feat(v2): rewrite models.py for v2 schema
Replaces the Lamassu-era data models (LamassuConfig, StoredLamassuTransaction, single-config CRUD carriers) with the v2 Pydantic surface matching m005: - Machine / CreateMachineData / UpdateMachineData: per-operator multi-machine registry keyed by Nostr npub. Replaces single-row LamassuConfig + SSH fields. - DcaClient now scoped per (machine_id, user_id). Includes autoforward fields for satmachineadmin#8 (best-effort LN-address forwarding for LPs). - DcaDeposit gains machine_id + creator_user_id (audit trail finding from v1). - DcaSettlement: idempotency carrier for bitSpire kind-21000 events. Carries platform_fee_sats + operator_fee_sats as absolute ints (v1 hook for the v2 customer-discount engine — see plan). - CommissionSplitLeg / CommissionSplit / SetCommissionSplitsData: operator's remainder-distribution rules. SetCommissionSplitsData validates legs sum to 1.0 at the boundary so crud.py only sees valid sets. - DcaPayment dropped transaction_type in favor of leg_type discriminator (dca | super_fee | operator_split | settlement | autoforward | refund). Gains settlement_id + machine_id + operator_user_id + destination_*. - TelemetrySnapshot: sparse beacon + fleet snapshot fields, all nullable so we degrade gracefully against today's minimal kind-30078 payload. - SuperConfig / UpdateSuperConfigData: super-only platform-fee carrier. - PartialDispenseData (satmachineadmin#3), SettleBalanceData (satmachineadmin#4): operator UX action carriers. Stays on pydantic v1 @validator pattern + Optional[X] hints to match the rest of the codebase. The UP045 / N805 lint noise is pre-existing tech debt across the repo, not introduced here. Refs: plan at ~/.claude/plans/snug-gliding-shamir.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ae4e241d1c
commit
013e3d5f6b
1 changed files with 330 additions and 159 deletions
481
models.py
481
models.py
|
|
@ -1,27 +1,107 @@
|
||||||
# Description: Pydantic data models dictate what is passed between frontend and backend.
|
# Satoshi Machine v2 — Pydantic data models.
|
||||||
|
#
|
||||||
|
# The v2 schema replaces the Lamassu-era single-config, super-only data model
|
||||||
|
# with a per-operator multi-machine layout that ingests bitSpire settlements
|
||||||
|
# over Nostr kind-21000. See migrations.py::m005_satmachine_v2_overhaul and
|
||||||
|
# the plan at ~/.claude/plans/snug-gliding-shamir.md.
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from pydantic import BaseModel, validator
|
from pydantic import BaseModel, validator
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Machines — one row per bitSpire ATM, owned by exactly one operator.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class CreateMachineData(BaseModel):
|
||||||
|
"""Operator adds a machine to their fleet by Nostr npub.
|
||||||
|
|
||||||
|
`wallet_id` is the LNbits wallet that will receive bitSpire settlements
|
||||||
|
for this machine. The same operator can own multiple machines; each
|
||||||
|
machine gets its own wallet so per-machine accounting via Payment.tag
|
||||||
|
(set to "satmachine:{machine_npub}") works natively.
|
||||||
|
"""
|
||||||
|
|
||||||
|
machine_npub: str
|
||||||
|
wallet_id: str
|
||||||
|
name: Optional[str] = None
|
||||||
|
location: Optional[str] = None
|
||||||
|
fiat_code: str = "GTQ"
|
||||||
|
# Used only when bitSpire's settlement event omits net_sats/fee_sats
|
||||||
|
# in Payment.extra (older bitSpire or edge cases). See plan's
|
||||||
|
# lamassu-next informational issue #1.
|
||||||
|
fallback_commission_pct: float = 0.05
|
||||||
|
|
||||||
|
@validator("fallback_commission_pct")
|
||||||
|
def commission_in_unit_range(cls, v):
|
||||||
|
if v is None:
|
||||||
|
return v
|
||||||
|
if v < 0 or v > 1:
|
||||||
|
raise ValueError("fallback_commission_pct must be between 0 and 1")
|
||||||
|
return round(float(v), 4)
|
||||||
|
|
||||||
|
|
||||||
|
class Machine(BaseModel):
|
||||||
|
id: str
|
||||||
|
operator_user_id: str
|
||||||
|
machine_npub: str
|
||||||
|
wallet_id: str
|
||||||
|
name: Optional[str]
|
||||||
|
location: Optional[str]
|
||||||
|
fiat_code: str
|
||||||
|
is_active: bool
|
||||||
|
fallback_commission_pct: float
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateMachineData(BaseModel):
|
||||||
|
name: Optional[str] = None
|
||||||
|
location: Optional[str] = None
|
||||||
|
fiat_code: Optional[str] = None
|
||||||
|
is_active: Optional[bool] = None
|
||||||
|
wallet_id: Optional[str] = None
|
||||||
|
fallback_commission_pct: Optional[float] = None
|
||||||
|
|
||||||
|
@validator("fallback_commission_pct")
|
||||||
|
def commission_in_unit_range(cls, v):
|
||||||
|
if v is None:
|
||||||
|
return v
|
||||||
|
if v < 0 or v > 1:
|
||||||
|
raise ValueError("fallback_commission_pct must be between 0 and 1")
|
||||||
|
return round(float(v), 4)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# DCA Clients — LP registrations, scoped per (machine, user).
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
# DCA Client Models
|
|
||||||
class CreateDcaClientData(BaseModel):
|
class CreateDcaClientData(BaseModel):
|
||||||
|
machine_id: str
|
||||||
user_id: str
|
user_id: str
|
||||||
wallet_id: str
|
wallet_id: str
|
||||||
username: str
|
username: Optional[str] = None
|
||||||
dca_mode: str = "flow" # 'flow' or 'fixed'
|
dca_mode: str = "flow" # 'flow' | 'fixed'
|
||||||
fixed_mode_daily_limit: Optional[float] = None
|
fixed_mode_daily_limit: Optional[float] = None
|
||||||
|
# Auto-forward DCA distributions to an external LN address (best-effort;
|
||||||
|
# sats stay in LNbits wallet on forward failure — see satmachineadmin#8).
|
||||||
|
autoforward_ln_address: Optional[str] = None
|
||||||
|
autoforward_enabled: bool = False
|
||||||
|
|
||||||
|
|
||||||
class DcaClient(BaseModel):
|
class DcaClient(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
|
machine_id: str
|
||||||
user_id: str
|
user_id: str
|
||||||
wallet_id: str
|
wallet_id: str
|
||||||
username: Optional[str]
|
username: Optional[str]
|
||||||
dca_mode: str
|
dca_mode: str
|
||||||
fixed_mode_daily_limit: Optional[int]
|
fixed_mode_daily_limit: Optional[float]
|
||||||
|
autoforward_ln_address: Optional[str]
|
||||||
|
autoforward_enabled: bool
|
||||||
status: str
|
status: str
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
|
|
@ -31,19 +111,34 @@ class UpdateDcaClientData(BaseModel):
|
||||||
username: Optional[str] = None
|
username: Optional[str] = None
|
||||||
dca_mode: Optional[str] = None
|
dca_mode: Optional[str] = None
|
||||||
fixed_mode_daily_limit: Optional[float] = None
|
fixed_mode_daily_limit: Optional[float] = None
|
||||||
|
autoforward_ln_address: Optional[str] = None
|
||||||
|
autoforward_enabled: Optional[bool] = None
|
||||||
status: Optional[str] = None
|
status: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
# Deposit Models (Now storing GTQ directly)
|
class ClientBalanceSummary(BaseModel):
|
||||||
|
client_id: str
|
||||||
|
machine_id: str
|
||||||
|
total_deposits: float # confirmed deposits in fiat
|
||||||
|
total_payments: float # DCA fiat-equivalent distributed
|
||||||
|
remaining_balance: float # deposits - payments
|
||||||
|
currency: str
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Deposits — fiat the operator (or super) records against an LP at a machine.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
class CreateDepositData(BaseModel):
|
class CreateDepositData(BaseModel):
|
||||||
client_id: str
|
client_id: str
|
||||||
amount: float # Amount in GTQ (e.g., 150.75)
|
machine_id: str
|
||||||
|
amount: float
|
||||||
currency: str = "GTQ"
|
currency: str = "GTQ"
|
||||||
notes: Optional[str] = None
|
notes: Optional[str] = None
|
||||||
|
|
||||||
@validator('amount')
|
@validator("amount")
|
||||||
def round_amount_to_cents(cls, v):
|
def round_amount(cls, v):
|
||||||
"""Ensure amount is rounded to 2 decimal places for DECIMAL(10,2) storage"""
|
|
||||||
if v is not None:
|
if v is not None:
|
||||||
return round(float(v), 2)
|
return round(float(v), 2)
|
||||||
return v
|
return v
|
||||||
|
|
@ -52,9 +147,11 @@ class CreateDepositData(BaseModel):
|
||||||
class DcaDeposit(BaseModel):
|
class DcaDeposit(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
client_id: str
|
client_id: str
|
||||||
amount: float # Amount in GTQ (e.g., 150.75)
|
machine_id: str
|
||||||
|
creator_user_id: str
|
||||||
|
amount: float
|
||||||
currency: str
|
currency: str
|
||||||
status: str # 'pending' or 'confirmed'
|
status: str # 'pending' | 'confirmed' | 'rejected'
|
||||||
notes: Optional[str]
|
notes: Optional[str]
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
confirmed_at: Optional[datetime]
|
confirmed_at: Optional[datetime]
|
||||||
|
|
@ -65,179 +162,253 @@ class UpdateDepositData(BaseModel):
|
||||||
currency: Optional[str] = None
|
currency: Optional[str] = None
|
||||||
notes: Optional[str] = None
|
notes: Optional[str] = None
|
||||||
|
|
||||||
@validator('amount')
|
@validator("amount")
|
||||||
def round_amount_to_cents(cls, v):
|
def round_amount(cls, v):
|
||||||
if v is not None:
|
if v is not None:
|
||||||
return round(float(v), 2)
|
return round(float(v), 2)
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
class UpdateDepositStatusData(BaseModel):
|
class UpdateDepositStatusData(BaseModel):
|
||||||
status: str
|
status: str # 'pending' | 'confirmed' | 'rejected'
|
||||||
notes: Optional[str] = None
|
notes: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
# Payment Models
|
# =============================================================================
|
||||||
class CreateDcaPaymentData(BaseModel):
|
# Settlements — one per bitSpire kind-21000 event.
|
||||||
client_id: str
|
# =============================================================================
|
||||||
amount_sats: int
|
# platform_fee_sats and operator_fee_sats are absolute audit-grade values.
|
||||||
amount_fiat: float # Amount in GTQ (e.g., 150.75)
|
# Today they equal the contractual split; tomorrow (post-v1 promo engine)
|
||||||
|
# they record who-forgave-what. DO NOT collapse them into a single pct.
|
||||||
|
# See plan section "Customer discounts & promotions (post-v1)".
|
||||||
|
|
||||||
|
|
||||||
|
class CreateDcaSettlementData(BaseModel):
|
||||||
|
machine_id: str
|
||||||
|
bitspire_event_id: str # nostr event id — the idempotency key
|
||||||
|
bitspire_txid: Optional[str] = None
|
||||||
|
payment_hash: str
|
||||||
|
gross_sats: int
|
||||||
|
fiat_amount: float
|
||||||
|
fiat_code: str = "GTQ"
|
||||||
exchange_rate: float
|
exchange_rate: float
|
||||||
transaction_type: str # 'flow', 'fixed', 'manual', 'commission'
|
net_sats: int
|
||||||
lamassu_transaction_id: Optional[str] = None
|
commission_sats: int
|
||||||
payment_hash: Optional[str] = None
|
platform_fee_sats: int
|
||||||
transaction_time: Optional[datetime] = None # Original ATM transaction time
|
operator_fee_sats: int
|
||||||
|
used_fallback_split: bool = False
|
||||||
|
tx_type: str # 'cash_out' | 'cash_in'
|
||||||
|
bills_json: Optional[str] = None
|
||||||
|
cassettes_json: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class DcaSettlement(BaseModel):
|
||||||
|
id: str
|
||||||
|
machine_id: str
|
||||||
|
bitspire_event_id: str
|
||||||
|
bitspire_txid: Optional[str]
|
||||||
|
payment_hash: str
|
||||||
|
gross_sats: int
|
||||||
|
fiat_amount: float
|
||||||
|
fiat_code: str
|
||||||
|
exchange_rate: float
|
||||||
|
net_sats: int
|
||||||
|
commission_sats: int
|
||||||
|
platform_fee_sats: int
|
||||||
|
operator_fee_sats: int
|
||||||
|
used_fallback_split: bool
|
||||||
|
tx_type: str
|
||||||
|
bills_json: Optional[str]
|
||||||
|
cassettes_json: Optional[str]
|
||||||
|
status: str # 'pending' | 'processed' | 'partial' | 'refunded' | 'errored'
|
||||||
|
error_message: Optional[str]
|
||||||
|
processed_at: Optional[datetime]
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Commission splits — operator-defined remainder allocation per machine.
|
||||||
|
# =============================================================================
|
||||||
|
# machine_id=NULL means operator's default; non-null means per-machine override.
|
||||||
|
# Sum of pct across rows for a (operator_user_id, machine_id) scope must be 1.0,
|
||||||
|
# enforced at write-time in crud.py.
|
||||||
|
|
||||||
|
|
||||||
|
class CommissionSplitLeg(BaseModel):
|
||||||
|
"""Single leg of an operator's commission-split rule set."""
|
||||||
|
|
||||||
|
wallet_id: str
|
||||||
|
label: Optional[str] = None
|
||||||
|
pct: float
|
||||||
|
sort_order: int = 0
|
||||||
|
|
||||||
|
@validator("pct")
|
||||||
|
def pct_in_unit_range(cls, v):
|
||||||
|
if v < 0 or v > 1:
|
||||||
|
raise ValueError("pct must be between 0 and 1")
|
||||||
|
return round(float(v), 4)
|
||||||
|
|
||||||
|
|
||||||
|
class CommissionSplit(BaseModel):
|
||||||
|
id: str
|
||||||
|
machine_id: Optional[str] # None = operator's default ruleset
|
||||||
|
operator_user_id: str
|
||||||
|
wallet_id: str
|
||||||
|
label: Optional[str]
|
||||||
|
pct: float
|
||||||
|
sort_order: int
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class SetCommissionSplitsData(BaseModel):
|
||||||
|
"""Replaces the entire ruleset for a given scope.
|
||||||
|
|
||||||
|
`machine_id=None` writes the operator's default ruleset (applies to any
|
||||||
|
machine without an explicit override). Otherwise scoped per machine.
|
||||||
|
"""
|
||||||
|
|
||||||
|
machine_id: Optional[str] = None
|
||||||
|
legs: list[CommissionSplitLeg]
|
||||||
|
|
||||||
|
@validator("legs")
|
||||||
|
def legs_sum_to_one(cls, v):
|
||||||
|
total = round(sum(leg.pct for leg in v), 4)
|
||||||
|
if abs(total - 1.0) > 0.0001:
|
||||||
|
raise ValueError(f"split percentages must sum to 1.0 (got {total})")
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Payments — every distribution leg (DCA / super_fee / split / settle / etc.)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class CreateDcaPaymentData(BaseModel):
|
||||||
|
settlement_id: Optional[str] = None
|
||||||
|
client_id: Optional[str] = None
|
||||||
|
machine_id: str
|
||||||
|
operator_user_id: str
|
||||||
|
leg_type: str
|
||||||
|
# 'dca' | 'super_fee' | 'operator_split' | 'settlement' | 'autoforward' | 'refund'
|
||||||
|
destination_wallet_id: Optional[str] = None
|
||||||
|
destination_ln_address: Optional[str] = None
|
||||||
|
amount_sats: int
|
||||||
|
amount_fiat: Optional[float] = None
|
||||||
|
exchange_rate: Optional[float] = None
|
||||||
|
transaction_time: datetime
|
||||||
|
external_payment_hash: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class DcaPayment(BaseModel):
|
class DcaPayment(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
client_id: str
|
settlement_id: Optional[str]
|
||||||
|
client_id: Optional[str]
|
||||||
|
machine_id: str
|
||||||
|
operator_user_id: str
|
||||||
|
leg_type: str
|
||||||
|
destination_wallet_id: Optional[str]
|
||||||
|
destination_ln_address: Optional[str]
|
||||||
amount_sats: int
|
amount_sats: int
|
||||||
amount_fiat: float # Amount in GTQ (e.g., 150.75)
|
amount_fiat: Optional[float]
|
||||||
exchange_rate: float
|
exchange_rate: Optional[float]
|
||||||
transaction_type: str
|
transaction_time: datetime
|
||||||
lamassu_transaction_id: Optional[str]
|
external_payment_hash: Optional[str]
|
||||||
payment_hash: Optional[str]
|
status: str # 'pending' | 'completed' | 'failed' | 'refunded'
|
||||||
status: str # 'pending', 'confirmed', 'failed'
|
error_message: Optional[str]
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
transaction_time: Optional[datetime] = None # Original ATM transaction time
|
|
||||||
|
|
||||||
|
|
||||||
# Client Balance Summary (Now storing GTQ directly)
|
# =============================================================================
|
||||||
class ClientBalanceSummary(BaseModel):
|
# Telemetry — sparse beacon (kind-30078) + fleet snapshot (kind-30079) state.
|
||||||
client_id: str
|
# =============================================================================
|
||||||
total_deposits: float # Total confirmed deposits in GTQ
|
|
||||||
total_payments: float # Total payments made in GTQ
|
|
||||||
remaining_balance: float # Available balance for DCA in GTQ
|
|
||||||
currency: str
|
|
||||||
|
|
||||||
|
|
||||||
# Transaction Processing Models
|
class TelemetrySnapshot(BaseModel):
|
||||||
class LamassuTransaction(BaseModel):
|
machine_id: str
|
||||||
transaction_id: str
|
# Beacon (kind-30078) — all fields are nullable because the upstream payload
|
||||||
amount_fiat: float # Amount in GTQ (e.g., 150.75)
|
# is sparse today. As lamassu-next#43 lands, the post-#43 columns fill in.
|
||||||
amount_crypto: int
|
beacon_cash_in: Optional[bool] = None
|
||||||
exchange_rate: float
|
beacon_cash_out: Optional[bool] = None
|
||||||
transaction_type: str # 'cash_in' or 'cash_out'
|
beacon_cash_level: Optional[str] = None
|
||||||
status: str
|
beacon_fiat: Optional[str] = None
|
||||||
timestamp: datetime
|
beacon_model: Optional[str] = None
|
||||||
|
beacon_name: Optional[str] = None
|
||||||
|
beacon_location: Optional[str] = None
|
||||||
|
beacon_geo: Optional[str] = None
|
||||||
|
beacon_fees_json: Optional[str] = None
|
||||||
|
beacon_limits_json: Optional[str] = None
|
||||||
|
beacon_denominations_json: Optional[str] = None
|
||||||
|
beacon_version: Optional[str] = None
|
||||||
|
beacon_received_at: Optional[datetime] = None
|
||||||
|
# Fleet telemetry (kind-30079) — operator-only, awaits lamassu-next#42.
|
||||||
|
telemetry_json: Optional[str] = None
|
||||||
|
telemetry_received_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
|
||||||
# Lamassu Transaction Storage Models
|
# =============================================================================
|
||||||
class CreateLamassuTransactionData(BaseModel):
|
# Super config — singleton row with the platform fee.
|
||||||
lamassu_transaction_id: str
|
# =============================================================================
|
||||||
fiat_amount: float # Amount in GTQ (e.g., 150.75)
|
|
||||||
crypto_amount: int
|
|
||||||
commission_percentage: float
|
|
||||||
discount: float = 0.0
|
|
||||||
effective_commission: float
|
|
||||||
commission_amount_sats: int
|
|
||||||
base_amount_sats: int
|
|
||||||
exchange_rate: float
|
|
||||||
crypto_code: str = "BTC"
|
|
||||||
fiat_code: str = "GTQ"
|
|
||||||
device_id: Optional[str] = None
|
|
||||||
transaction_time: datetime
|
|
||||||
|
|
||||||
|
|
||||||
class StoredLamassuTransaction(BaseModel):
|
class SuperConfig(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
lamassu_transaction_id: str
|
super_fee_pct: float
|
||||||
fiat_amount: float # Amount in GTQ (e.g., 150.75)
|
super_fee_wallet_id: Optional[str]
|
||||||
crypto_amount: int
|
updated_at: datetime
|
||||||
commission_percentage: float
|
|
||||||
discount: float
|
|
||||||
effective_commission: float
|
|
||||||
commission_amount_sats: int
|
|
||||||
base_amount_sats: int
|
|
||||||
exchange_rate: float
|
|
||||||
crypto_code: str
|
|
||||||
fiat_code: str
|
|
||||||
device_id: Optional[str]
|
|
||||||
transaction_time: datetime
|
|
||||||
processed_at: datetime
|
|
||||||
clients_count: int # Number of clients who received distributions
|
|
||||||
distributions_total_sats: int # Total sats distributed to clients
|
|
||||||
|
|
||||||
|
|
||||||
# Lamassu Configuration Models
|
class UpdateSuperConfigData(BaseModel):
|
||||||
class CreateLamassuConfigData(BaseModel):
|
super_fee_pct: Optional[float] = None
|
||||||
host: str
|
super_fee_wallet_id: Optional[str] = None
|
||||||
port: int = 5432
|
|
||||||
database_name: str
|
|
||||||
username: str
|
|
||||||
password: str
|
|
||||||
# Source wallet for DCA distributions
|
|
||||||
source_wallet_id: Optional[str] = None
|
|
||||||
# Commission wallet for storing commission earnings
|
|
||||||
commission_wallet_id: Optional[str] = None
|
|
||||||
# SSH Tunnel settings
|
|
||||||
use_ssh_tunnel: bool = False
|
|
||||||
ssh_host: Optional[str] = None
|
|
||||||
ssh_port: int = 22
|
|
||||||
ssh_username: Optional[str] = None
|
|
||||||
ssh_password: Optional[str] = None
|
|
||||||
ssh_private_key: Optional[str] = None # Path to private key file or key content
|
|
||||||
# DCA Client Limits
|
|
||||||
max_daily_limit_gtq: float = 2000.0 # Maximum daily limit for Fixed mode clients
|
|
||||||
|
|
||||||
@validator('max_daily_limit_gtq')
|
@validator("super_fee_pct")
|
||||||
def round_max_daily_limit(cls, v):
|
def fee_in_unit_range(cls, v):
|
||||||
"""Ensure max daily limit is rounded to 2 decimal places"""
|
if v is None:
|
||||||
if v is not None:
|
return v
|
||||||
return round(float(v), 2)
|
if v < 0 or v > 1:
|
||||||
|
raise ValueError("super_fee_pct must be between 0 and 1")
|
||||||
|
return round(float(v), 4)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Operator UX action carriers — partial-tx and balance-settlement features.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class PartialDispenseData(BaseModel):
|
||||||
|
"""Resolves satmachineadmin#3 — operator confirms actual bills dispensed
|
||||||
|
when bitSpire reports an error mid-dispense.
|
||||||
|
|
||||||
|
Either `dispensed_fraction` (0..1) for ratio-based recompute, or
|
||||||
|
`dispensed_sats` for explicit recompute. Exactly one must be set.
|
||||||
|
"""
|
||||||
|
|
||||||
|
settlement_id: str
|
||||||
|
dispensed_fraction: Optional[float] = None
|
||||||
|
dispensed_sats: Optional[int] = None
|
||||||
|
notes: Optional[str] = None
|
||||||
|
|
||||||
|
@validator("dispensed_fraction")
|
||||||
|
def fraction_in_unit_range(cls, v):
|
||||||
|
if v is None:
|
||||||
|
return v
|
||||||
|
if v < 0 or v > 1:
|
||||||
|
raise ValueError("dispensed_fraction must be between 0 and 1")
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
class LamassuConfig(BaseModel):
|
class SettleBalanceData(BaseModel):
|
||||||
id: str
|
"""Resolves satmachineadmin#4 — operator settles small remaining LP balance
|
||||||
host: str
|
from their own wallet at the current exchange rate."""
|
||||||
port: int
|
|
||||||
database_name: str
|
|
||||||
username: str
|
|
||||||
password: str
|
|
||||||
is_active: bool
|
|
||||||
test_connection_last: Optional[datetime]
|
|
||||||
test_connection_success: Optional[bool]
|
|
||||||
created_at: datetime
|
|
||||||
updated_at: datetime
|
|
||||||
# Source wallet for DCA distributions
|
|
||||||
source_wallet_id: Optional[str] = None
|
|
||||||
# Commission wallet for storing commission earnings
|
|
||||||
commission_wallet_id: Optional[str] = None
|
|
||||||
# SSH Tunnel settings
|
|
||||||
use_ssh_tunnel: bool = False
|
|
||||||
ssh_host: Optional[str] = None
|
|
||||||
ssh_port: int = 22
|
|
||||||
ssh_username: Optional[str] = None
|
|
||||||
ssh_password: Optional[str] = None
|
|
||||||
ssh_private_key: Optional[str] = None
|
|
||||||
# Poll tracking
|
|
||||||
last_poll_time: Optional[datetime] = None
|
|
||||||
last_successful_poll: Optional[datetime] = None
|
|
||||||
# DCA Client Limits
|
|
||||||
max_daily_limit_gtq: float = 2000.0 # Maximum daily limit for Fixed mode clients
|
|
||||||
|
|
||||||
|
|
||||||
class UpdateLamassuConfigData(BaseModel):
|
|
||||||
host: Optional[str] = None
|
|
||||||
port: Optional[int] = None
|
|
||||||
database_name: Optional[str] = None
|
|
||||||
username: Optional[str] = None
|
|
||||||
password: Optional[str] = None
|
|
||||||
is_active: Optional[bool] = None
|
|
||||||
# Source wallet for DCA distributions
|
|
||||||
source_wallet_id: Optional[str] = None
|
|
||||||
# Commission wallet for storing commission earnings
|
|
||||||
commission_wallet_id: Optional[str] = None
|
|
||||||
# SSH Tunnel settings
|
|
||||||
use_ssh_tunnel: Optional[bool] = None
|
|
||||||
ssh_host: Optional[str] = None
|
|
||||||
ssh_port: Optional[int] = None
|
|
||||||
ssh_username: Optional[str] = None
|
|
||||||
ssh_password: Optional[str] = None
|
|
||||||
ssh_private_key: Optional[str] = None
|
|
||||||
# DCA Client Limits
|
|
||||||
max_daily_limit_gtq: Optional[int] = None
|
|
||||||
|
|
||||||
|
client_id: str
|
||||||
|
funding_wallet_id: str
|
||||||
|
# If None, settle the full remaining balance.
|
||||||
|
amount_fiat: Optional[float] = None
|
||||||
|
notes: Optional[str] = None
|
||||||
|
|
||||||
|
@validator("amount_fiat")
|
||||||
|
def round_amount(cls, v):
|
||||||
|
if v is not None:
|
||||||
|
return round(float(v), 2)
|
||||||
|
return v
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue