feat(api): add owner-scoped manual ledger endpoint

This commit is contained in:
2026-07-14 19:49:03 +09:00
parent 762cf11376
commit b3447f699e
11 changed files with 1022 additions and 16 deletions
+93 -1
View File
@@ -6,7 +6,7 @@ from datetime import date, datetime
from decimal import Decimal
from typing import Any, Literal
from pydantic import BaseModel, Field
from pydantic import BaseModel, ConfigDict, Field, model_validator
# ---------------------------------------------------------------------------
@@ -136,6 +136,98 @@ class LedgerTradeResponse(BaseModel):
status: Literal["new", "duplicate"] = "new"
# ---------------------------------------------------------------------------
# POST/GET /v1/portfolios/{id}/ledger/manual (REQ-056)
# ---------------------------------------------------------------------------
ManualLedgerAction = Literal[
"INVEST",
"BUFFER_OUT",
"BUFFER_IN",
"BUY",
"SELL",
"WITHDRAW",
"PROFIT",
"REVERSAL",
]
class ManualLedgerRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
action: ManualLedgerAction
amount: Decimal | None = Field(default=None, gt=0)
quantity: Decimal | None = Field(default=None, gt=0)
price: Decimal | None = Field(default=None, gt=0)
fee: Decimal = Field(default=Decimal("0"), ge=0)
tax: Decimal = Field(default=Decimal("0"), ge=0)
target_ledger_entry_id: int | None = None
note: str = ""
confirm_daemon_managed_manual_write: bool = False
confirmation_evidence: str | None = None
@model_validator(mode="after")
def validate_action_fields(self) -> "ManualLedgerRequest":
amount_actions = {"INVEST", "BUFFER_OUT", "BUFFER_IN", "WITHDRAW", "PROFIT"}
trade_actions = {"BUY", "SELL"}
provided_fields = self.model_fields_set
fee_or_tax_provided = "fee" in provided_fields or "tax" in provided_fields
if self.action in amount_actions:
if self.amount is None:
raise ValueError("amount is required for this action")
if (
self.quantity is not None
or self.price is not None
or self.target_ledger_entry_id is not None
or fee_or_tax_provided
):
raise ValueError("unexpected numeric/target field for amount action")
elif self.action in trade_actions:
if self.quantity is None or self.price is None:
raise ValueError("quantity and price are required for BUY/SELL")
if self.amount is not None or self.target_ledger_entry_id is not None:
raise ValueError("unexpected amount/target field for BUY/SELL")
elif self.action == "REVERSAL":
if self.target_ledger_entry_id is None:
raise ValueError("target_ledger_entry_id is required for REVERSAL")
if self.amount is not None or self.quantity is not None or self.price is not None or fee_or_tax_provided:
raise ValueError("unexpected numeric field for REVERSAL")
return self
class ManualLedgerResponse(BaseModel):
ledger_entry_id: int
portfolio_id: str
action: str
amount: float | None = None
quantity: float = 0.0
price: float = 0.0
fee: float = 0.0
tax: float = 0.0
note: str = ""
recorded_at: datetime | None = None
cash_balance_after: float = 0.0
asset_quantity_after: float = 0.0
profit_buffer_after: float = 0.0
total_value_after: float = 0.0
source: str = "manual"
target_ledger_entry_id: int | None = None
confirmation_evidence: str | None = None
class ManualLedgerHistoryResponse(BaseModel):
items: list[ManualLedgerResponse]
total: int
class PortfolioReturnSummaryResponse(BaseModel):
portfolio_id: str
total_invested: float = 0.0
total_withdrawn: float = 0.0
net_contributed_capital: float = 0.0
cumulative_profit_withdrawn: float = 0.0
# ---------------------------------------------------------------------------
# POST/PATCH/DELETE/GET /v1/portfolios (REQ-025, REQ-026, REQ-027, REQ-028)
# ---------------------------------------------------------------------------