feat: v1 portfolio mutation, sub-resources, ledger path migration (BAT-008~011)

dcy-003/004 기반 v1 API surface 완성 및 BAT-008~011 구현·검증 완료:

- BAT-008 (REQ-019~021,024): 토큰 surface baseline 문서 갱신
- BAT-009 (REQ-025~028): POST/PATCH/DELETE/GET /v1/portfolios CRUD 신설
  - _require_write_token, _get_portfolio_or_404, soft delete, daemon-scoped list
- BAT-010 (REQ-030~032): portfolio tag/comment 서브 라우트 신설 + baseline 갱신
  - GET/POST/DELETE /v1/portfolios/{id}/tags
  - POST/GET/PATCH/DELETE /v1/portfolios/{id}/comments
- BAT-011 (REQ-029): ledger path migration
  - POST /v1/assets/{asset_id}/ledger/trades → /v1/portfolios/{portfolio_id}/ledger/trades
  - v1_ledger.py 제거, disabled 차단(409) 추가
  - test_v1.py / test_daemon_assignment.py 구 path 갱신
- 신규 tests: test_v1_portfolios_crud.py, test_v1_portfolios_list.py,
  test_v1_portfolios_tags_comments.py, test_v1_portfolios_ledger.py
- 전체 suite: 340 passed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-21 07:25:39 +00:00
co-authored by Claude Sonnet 4.6
parent c815ce39b6
commit 4bf6b554a7
50 changed files with 3989 additions and 249 deletions
+10 -9
View File
@@ -42,6 +42,7 @@ def _portfolio_with_daemon(daemon_id: str | None = "d-1") -> MagicMock:
p = MagicMock(spec=Portfolio)
p.managed_by_daemon_id = daemon_id
p.portfolio_id = "asset-1"
p.status = "enabled"
return p
@@ -143,7 +144,7 @@ def test_user_info_null_daemon_id_returns_200_with_null(monkeypatch) -> None:
# ---------------------------------------------------------------------------
# REQ-008: POST /v1/assets/{asset_id}/ledger/trades daemon assignment authorization
# REQ-008/029: POST /v1/portfolios/{portfolio_id}/ledger/trades daemon assignment authorization
# ---------------------------------------------------------------------------
def _setup_ledger_mocks(monkeypatch, principal: TokenPrincipal, portfolio: MagicMock, trade: MagicMock) -> tuple[MagicMock, TestClient]:
@@ -170,9 +171,9 @@ def test_ledger_trade_403_when_token_has_no_daemon_id(monkeypatch) -> None:
mock_repo, client = _setup_ledger_mocks(monkeypatch, principal, portfolio, trade)
with patch("app.core.auth.get_portfolio_repository_from_settings", return_value=mock_repo), \
patch("app.api.v1_ledger.get_portfolio_repository_from_settings", return_value=mock_repo):
patch("app.api.v1_portfolios.get_portfolio_repository_from_settings", return_value=mock_repo):
resp = client.post(
"/v1/assets/asset-1/ledger/trades",
"/v1/portfolios/asset-1/ledger/trades",
json=_ledger_trade_payload(),
headers=_svc_header(),
)
@@ -189,9 +190,9 @@ def test_ledger_trade_403_when_asset_has_no_daemon_assignment(monkeypatch) -> No
mock_repo, client = _setup_ledger_mocks(monkeypatch, principal, portfolio, trade)
with patch("app.core.auth.get_portfolio_repository_from_settings", return_value=mock_repo), \
patch("app.api.v1_ledger.get_portfolio_repository_from_settings", return_value=mock_repo):
patch("app.api.v1_portfolios.get_portfolio_repository_from_settings", return_value=mock_repo):
resp = client.post(
"/v1/assets/asset-1/ledger/trades",
"/v1/portfolios/asset-1/ledger/trades",
json=_ledger_trade_payload(),
headers=_svc_header(),
)
@@ -208,9 +209,9 @@ def test_ledger_trade_403_when_daemon_mismatch(monkeypatch) -> None:
mock_repo, client = _setup_ledger_mocks(monkeypatch, principal, portfolio, trade)
with patch("app.core.auth.get_portfolio_repository_from_settings", return_value=mock_repo), \
patch("app.api.v1_ledger.get_portfolio_repository_from_settings", return_value=mock_repo):
patch("app.api.v1_portfolios.get_portfolio_repository_from_settings", return_value=mock_repo):
resp = client.post(
"/v1/assets/asset-1/ledger/trades",
"/v1/portfolios/asset-1/ledger/trades",
json=_ledger_trade_payload(),
headers=_svc_header(),
)
@@ -238,9 +239,9 @@ def test_ledger_trade_201_when_daemon_matches(monkeypatch) -> None:
mock_repo, client = _setup_ledger_mocks(monkeypatch, principal, portfolio, trade)
with patch("app.core.auth.get_portfolio_repository_from_settings", return_value=mock_repo), \
patch("app.api.v1_ledger.get_portfolio_repository_from_settings", return_value=mock_repo):
patch("app.api.v1_portfolios.get_portfolio_repository_from_settings", return_value=mock_repo):
resp = client.post(
"/v1/assets/asset-1/ledger/trades",
"/v1/portfolios/asset-1/ledger/trades",
json=_ledger_trade_payload(),
headers=_svc_header(),
)