feat: consolidate ledger trade writes into portfolio_ledger
- add ledger consolidation migration and remove ledger_trades usage - return portfolio state after v1 ledger trade writes - deprecate legacy buy/sell endpoints with deprecation headers - add BAT-016, DCY-006, and REQ-036~038 documentation - update repository models and tests for unified ledger entries
This commit is contained in:
@@ -60,6 +60,23 @@ def test_trade_route_rejects_read_token(monkeypatch) -> None:
|
||||
assert response.json()["detail"] == "Insufficient permission"
|
||||
|
||||
|
||||
def test_legacy_buy_sell_routes_emit_deprecation_header(monkeypatch) -> None:
|
||||
monkeypatch.setenv("PORTFOLIOHUB_DB_ENABLED", "false")
|
||||
monkeypatch.setenv("PORTFOLIOHUB_TRADE_WRITE_TOKENS", "trade-token")
|
||||
get_settings.cache_clear()
|
||||
client = _client_with_tokens()
|
||||
|
||||
for path in ("/portfolios/p1/buy", "/portfolios/p1/sell"):
|
||||
response = client.post(
|
||||
path,
|
||||
headers={"Authorization": "Bearer trade-token"},
|
||||
json={"quantity": 1, "price": 1000, "fee": 0, "tax": 0, "note": ""},
|
||||
)
|
||||
|
||||
assert response.status_code == 501
|
||||
assert response.headers["Deprecation"] == "true"
|
||||
|
||||
|
||||
def test_admin_route_accepts_admin_token(monkeypatch) -> None:
|
||||
monkeypatch.setenv("PORTFOLIOHUB_DB_ENABLED", "false")
|
||||
monkeypatch.setenv("PORTFOLIOHUB_ADMIN_TOKENS", "admin-token")
|
||||
|
||||
@@ -151,10 +151,10 @@ def _setup_ledger_mocks(monkeypatch, principal: TokenPrincipal, portfolio: Magic
|
||||
|
||||
|
||||
def test_ledger_trade_403_when_token_has_no_daemon_id(monkeypatch) -> None:
|
||||
from app.domain.models import LedgerTrade
|
||||
from app.domain.models import LedgerEntry
|
||||
principal = _principal(daemon_id=None)
|
||||
portfolio = _portfolio_with_daemon(daemon_id="d-1")
|
||||
trade = MagicMock(spec=LedgerTrade)
|
||||
trade = MagicMock(spec=LedgerEntry)
|
||||
mock_repo, client = _setup_ledger_mocks(monkeypatch, principal, portfolio, trade)
|
||||
|
||||
with patch("app.core.auth.get_portfolio_repository_from_settings", return_value=mock_repo), \
|
||||
@@ -170,10 +170,10 @@ def test_ledger_trade_403_when_token_has_no_daemon_id(monkeypatch) -> None:
|
||||
|
||||
|
||||
def test_ledger_trade_403_when_asset_has_no_daemon_assignment(monkeypatch) -> None:
|
||||
from app.domain.models import LedgerTrade
|
||||
from app.domain.models import LedgerEntry
|
||||
principal = _principal(daemon_id="d-1")
|
||||
portfolio = _portfolio_with_daemon(daemon_id=None)
|
||||
trade = MagicMock(spec=LedgerTrade)
|
||||
trade = MagicMock(spec=LedgerEntry)
|
||||
mock_repo, client = _setup_ledger_mocks(monkeypatch, principal, portfolio, trade)
|
||||
|
||||
with patch("app.core.auth.get_portfolio_repository_from_settings", return_value=mock_repo), \
|
||||
@@ -189,10 +189,10 @@ def test_ledger_trade_403_when_asset_has_no_daemon_assignment(monkeypatch) -> No
|
||||
|
||||
|
||||
def test_ledger_trade_403_when_daemon_mismatch(monkeypatch) -> None:
|
||||
from app.domain.models import LedgerTrade
|
||||
from app.domain.models import LedgerEntry
|
||||
principal = _principal(daemon_id="d-1")
|
||||
portfolio = _portfolio_with_daemon(daemon_id="d-2")
|
||||
trade = MagicMock(spec=LedgerTrade)
|
||||
trade = MagicMock(spec=LedgerEntry)
|
||||
mock_repo, client = _setup_ledger_mocks(monkeypatch, principal, portfolio, trade)
|
||||
|
||||
with patch("app.core.auth.get_portfolio_repository_from_settings", return_value=mock_repo), \
|
||||
@@ -208,7 +208,7 @@ def test_ledger_trade_403_when_daemon_mismatch(monkeypatch) -> None:
|
||||
|
||||
|
||||
def test_ledger_trade_201_when_daemon_matches(monkeypatch) -> None:
|
||||
from app.domain.models import LedgerTrade
|
||||
from app.domain.models import LedgerEntry
|
||||
principal = _principal(daemon_id="d-1")
|
||||
portfolio = _portfolio_with_daemon(daemon_id="d-1")
|
||||
trade = MagicMock()
|
||||
|
||||
+4
-4
@@ -11,7 +11,7 @@ from fastapi.testclient import TestClient
|
||||
|
||||
from app.core.auth import AuthContext, Permission
|
||||
from app.core.config import Settings, get_settings
|
||||
from app.domain.models import AuthToken, BrokerCredential, Portfolio, TokenPrincipal, User, LedgerTrade
|
||||
from app.domain.models import AuthToken, BrokerCredential, Portfolio, TokenPrincipal, User, LedgerEntry
|
||||
from app.main import create_app
|
||||
|
||||
|
||||
@@ -356,7 +356,7 @@ def _trade_payload(**overrides) -> dict:
|
||||
return base
|
||||
|
||||
|
||||
def _make_ledger_trade(**kwargs) -> LedgerTrade:
|
||||
def _make_ledger_trade(**kwargs) -> LedgerEntry:
|
||||
defaults = dict(
|
||||
ledger_entry_id=1,
|
||||
portfolio_id="005930",
|
||||
@@ -371,7 +371,7 @@ def _make_ledger_trade(**kwargs) -> LedgerTrade:
|
||||
recorded_at=datetime(2026, 5, 1, 1, 0, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
defaults.update(kwargs)
|
||||
return LedgerTrade(**defaults)
|
||||
return LedgerEntry(**defaults)
|
||||
|
||||
|
||||
def _make_ledger_principal() -> TokenPrincipal:
|
||||
@@ -381,7 +381,7 @@ def _make_ledger_principal() -> TokenPrincipal:
|
||||
)
|
||||
|
||||
|
||||
def _make_mock_repo_for_ledger(monkeypatch, trade: LedgerTrade, created: bool, found: bool = True) -> MagicMock:
|
||||
def _make_mock_repo_for_ledger(monkeypatch, trade: LedgerEntry, created: bool, found: bool = True) -> MagicMock:
|
||||
settings = _make_app_db(monkeypatch)
|
||||
mock_repo = MagicMock()
|
||||
mock_repo.get_token_principal.return_value = _make_ledger_principal()
|
||||
|
||||
@@ -9,7 +9,7 @@ from unittest.mock import MagicMock, patch
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.core.config import Settings, get_settings
|
||||
from app.domain.models import LedgerTrade, Portfolio, TokenPrincipal
|
||||
from app.domain.models import LedgerEntry, Portfolio, TokenPrincipal
|
||||
from app.main import create_app
|
||||
|
||||
_ENC_KEY = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
|
||||
@@ -55,7 +55,7 @@ def _make_portfolio(
|
||||
)
|
||||
|
||||
|
||||
def _make_trade(**kwargs) -> LedgerTrade:
|
||||
def _make_trade(**kwargs) -> LedgerEntry:
|
||||
defaults = dict(
|
||||
ledger_entry_id=1,
|
||||
portfolio_id=_PORTFOLIO_ID,
|
||||
@@ -70,14 +70,14 @@ def _make_trade(**kwargs) -> LedgerTrade:
|
||||
recorded_at=datetime(2026, 5, 1, 1, 0, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
defaults.update(kwargs)
|
||||
return LedgerTrade(**defaults)
|
||||
return LedgerEntry(**defaults)
|
||||
|
||||
|
||||
def _make_service_repo(
|
||||
*,
|
||||
daemon_id: str | None = _DAEMON_ID,
|
||||
portfolio: Portfolio | None = None,
|
||||
trade: LedgerTrade | None = None,
|
||||
trade: LedgerEntry | None = None,
|
||||
created: bool = True,
|
||||
) -> MagicMock:
|
||||
repo = MagicMock()
|
||||
|
||||
Reference in New Issue
Block a user