76 lines
2.5 KiB
Python
76 lines
2.5 KiB
Python
"""Application ports (REQ-007). The intent dedup/audit store contract.
|
|
|
|
Kept in the application layer (not domain) so the pure slot domain stays
|
|
unaware of the intent concept. Concrete implementations live in adapters.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Optional, Protocol
|
|
from uuid import UUID
|
|
|
|
from trex_core.application.auto_reconcile import HoldingsCostBasisEvidence
|
|
|
|
|
|
class IntentStore(Protocol):
|
|
"""Durable idempotency + audit store for accepted intents (dcy-002 OQ-1).
|
|
|
|
`try_insert` is the dedup gate: it returns False if `intent_id` already
|
|
exists, which prevents a duplicate intent from re-driving a slot
|
|
transition (NFR-3). `finalize` records the decision after processing.
|
|
"""
|
|
|
|
def try_insert(self, intent_id: str, intent_type: str, payload: dict) -> bool:
|
|
"""Insert a new intent row in 'processing' state.
|
|
|
|
Returns True if newly inserted, False if `intent_id` already exists.
|
|
"""
|
|
...
|
|
|
|
def get(self, intent_id: str) -> Optional["IntentRecord"]:
|
|
"""Return the stored intent record, or None."""
|
|
...
|
|
|
|
def finalize(
|
|
self,
|
|
intent_id: str,
|
|
status: str,
|
|
slot_id: Optional[UUID],
|
|
reason: Optional[str],
|
|
) -> None:
|
|
"""Update an intent row with its final decision (admitted/rejected)."""
|
|
...
|
|
|
|
|
|
class IntentRecord:
|
|
"""Snapshot of an intent_log row (duck-typed; see adapters)."""
|
|
|
|
intent_id: str
|
|
intent_type: str
|
|
status: str
|
|
slot_id: Optional[UUID]
|
|
reason: Optional[str]
|
|
|
|
|
|
class PortfolioHubCostBasisPort(Protocol):
|
|
"""FR-7 (dcy-034 / REQ-105): portfolio-hub WAC (weighted average cost) read port.
|
|
|
|
Wraps `GET /v1/portfolios/{id}/holdings/cost-basis` from portfolio-hub REQ-043.
|
|
When unavailable or when the portfolio has no holding, implementations return None.
|
|
sync() degrades gracefully: None => skip avg_price seed + WARN log.
|
|
"""
|
|
|
|
def get_cost_basis(self, portfolio_id: str) -> Optional[float]:
|
|
"""Return the weighted average cost (WAC) for the portfolio's current holding.
|
|
|
|
Returns None when the portfolio has no holding, the endpoint is unavailable,
|
|
or any error occurs. The caller must handle None as a graceful-degrade signal.
|
|
"""
|
|
...
|
|
|
|
def get_cost_basis_evidence(self, portfolio_id: str) -> HoldingsCostBasisEvidence:
|
|
"""Return strict canonical evidence for automatic holding reconciliation.
|
|
|
|
Implementations must not fall back to legacy cost-basis fields.
|
|
"""
|
|
...
|