From 136ae168857bde807f929108013b35fad3a46ba2 Mon Sep 17 00:00:00 2001 From: Justin Ko Date: Sun, 24 May 2026 13:09:37 +0900 Subject: [PATCH] feat: add portfolio item symbol to v1 responses --- app/api/v1_portfolios.py | 1 + app/schemas/v1.py | 1 + .../implementation.md | 46 +++++++ .../index.md | 33 +++++ .../planning.md | 50 ++++++++ .../verification.md | 79 ++++++++++++ docs/batches/index.md | 1 + docs/interface-contract.md | 12 +- ...rtfolio-hub-ticket-symbol-in-portfolios.md | 119 ++++++++++++++++++ ...q-032_portfolio-v1-baseline-docs-update.md | 60 +++++++++ .../Interface/req-025_v1-portfolios-create.md | 62 ++++++++- .../Interface/req-026_v1-portfolios-update.md | 60 +++++++++ .../req-027_v1-portfolios-soft-delete.md | 60 +++++++++ .../Interface/req-028_v1-portfolios-list.md | 62 ++++++++- docs/srs/index.md | 4 +- tests/test_v1_portfolios_crud.py | 14 ++- tests/test_v1_portfolios_list.py | 2 + 17 files changed, 655 insertions(+), 11 deletions(-) create mode 100644 docs/batches/bat-014_20260524_portfolioitem-symbol-response-contract/implementation.md create mode 100644 docs/batches/bat-014_20260524_portfolioitem-symbol-response-contract/index.md create mode 100644 docs/batches/bat-014_20260524_portfolioitem-symbol-response-contract/planning.md create mode 100644 docs/batches/bat-014_20260524_portfolioitem-symbol-response-contract/verification.md create mode 100644 docs/issues/20260524_portfolio-hub-ticket-symbol-in-portfolios.md diff --git a/app/api/v1_portfolios.py b/app/api/v1_portfolios.py index 63b50b8..2ea571d 100644 --- a/app/api/v1_portfolios.py +++ b/app/api/v1_portfolios.py @@ -54,6 +54,7 @@ def _portfolio_to_item(portfolio: Portfolio) -> PortfolioItem: return PortfolioItem( portfolio_id=portfolio.portfolio_id, asset_id=portfolio.asset_id, + symbol=portfolio.symbol, owner_user_id=portfolio.owner_user_id, broker=portfolio.broker, status=portfolio.status, diff --git a/app/schemas/v1.py b/app/schemas/v1.py index 10440c1..20b5224 100644 --- a/app/schemas/v1.py +++ b/app/schemas/v1.py @@ -165,6 +165,7 @@ class PortfolioUpdateRequest(BaseModel): class PortfolioItem(BaseModel): portfolio_id: str asset_id: str | None = None + symbol: str | None = None owner_user_id: str | None = None broker: str status: str diff --git a/docs/batches/bat-014_20260524_portfolioitem-symbol-response-contract/implementation.md b/docs/batches/bat-014_20260524_portfolioitem-symbol-response-contract/implementation.md new file mode 100644 index 0000000..c7149c7 --- /dev/null +++ b/docs/batches/bat-014_20260524_portfolioitem-symbol-response-contract/implementation.md @@ -0,0 +1,46 @@ +# BAT-014 Implementation + +- Status: completed +- Batch: BAT-014 + +## Plan Summary +- planning.md Delivery Plan에 따라 (1) `PortfolioItem` schema에 `symbol` 추가 → (2) `_portfolio_to_item()` serialization 보강 → (3) CRUD/list 테스트 expectation 갱신 → (4) `docs/interface-contract.md` 계약 반영 → (5) pytest 검증 순서로 구현 완료. +- 사용자가 명시한 핵심 요구대로 `GET /v1/portfolios` 는 **asset join 결과를 응답**해야 하므로, repository shape(`_SELECT_PORTFOLIO` + `_row_to_portfolio`)를 유지한 채 응답 schema/문서/테스트를 정합화했다. + +## Changed Files +- `app/schemas/v1.py` + - `PortfolioItem` 에 `symbol: str | None = None` 필드 추가 +- `app/api/v1_portfolios.py` + - `_portfolio_to_item()` 에 `symbol=portfolio.symbol` 추가 +- `tests/test_v1_portfolios_list.py` + - GET `/v1/portfolios` 응답의 `symbol == "005930"` 검증 추가 + - 기존 `asset_id` 검증 케이스에도 `symbol` assertion 추가 +- `tests/test_v1_portfolios_crud.py` + - POST/PATCH/DELETE 성공 응답에서 `symbol == "005930"` 검증 추가 +- `docs/interface-contract.md` + - POST/PATCH/DELETE/GET `/v1/portfolios` 응답 설명에 `symbol` 필드 반영 + - GET `/v1/portfolios` 가 server-side `portfolios JOIN assets` 결과를 직렬화한다는 설명 추가 + +## Execution Log +1. BAT-014 planning/index와 REQ-025/026/027/028/032를 읽고 구현 범위를 확정했다. +2. `app/schemas/v1.py` 에 `PortfolioItem.symbol` 필드를 추가했다. +3. `app/api/v1_portfolios.py` `_portfolio_to_item()` 에 `symbol=portfolio.symbol` 매핑을 추가했다. +4. `tests/test_v1_portfolios_crud.py` 의 POST/PATCH/DELETE 성공 케이스에 `symbol == "005930"` assertion을 추가했다. +5. `tests/test_v1_portfolios_list.py` 의 service-token list 케이스와 `asset_id` 검증 케이스에 `symbol == "005930"` assertion을 추가했다. +6. `docs/interface-contract.md` 의 POST/PATCH/DELETE/GET `/v1/portfolios` 명세를 갱신해 `symbol` 과 join 기반 응답 계약을 명문화했다. +7. `app/repositories/postgres.py` 를 재검토해 `_row_to_portfolio()` 가 `symbol=row[4]` 를 매핑하고, `get()` / `create_portfolio()` / `update_portfolio()` / `soft_delete_portfolio()` / `list_portfolios()` 가 모두 `_SELECT_PORTFOLIO` 기반 join 결과를 사용함을 확인했다. +8. `./.venv/bin/python -m pytest tests/test_v1_portfolios_crud.py tests/test_v1_portfolios_list.py -q` 실행 → **32 passed in 2.13s**. +9. `./.venv/bin/python -m pytest -q` 전체 회귀 실행 → **371 passed in 11.00s**. + +## Validation +- Targeted regression: + - `./.venv/bin/python -m pytest tests/test_v1_portfolios_crud.py tests/test_v1_portfolios_list.py -q` → **32 passed in 2.13s** +- Full regression: + - `./.venv/bin/python -m pytest -q` → **371 passed in 11.00s** +- Repository join evidence: + - `_row_to_portfolio()` 가 `symbol=row[4]` 를 `Portfolio.symbol` 로 매핑 + - `get()` / `create_portfolio()` / `update_portfolio()` / `soft_delete_portfolio()` / `list_portfolios()` 가 `_SELECT_PORTFOLIO` join 결과를 재사용 + +## Remaining Risks +- R1 (Low): 이번 batch의 symbol 회귀는 FastAPI/TestClient + fake repo 중심이다. 다만 repository 코드 inspection으로 실제 Postgres 경로도 `assets.symbol` join 결과를 응답까지 유지함을 확인했다. +- R2 (Negligible): pytest 출력에 기존 `HTTP_422_UNPROCESSABLE_ENTITY` deprecation warning 2~3건이 남아 있으나 BAT-014 범위와 직접 관련은 없고 기능 실패는 없다. diff --git a/docs/batches/bat-014_20260524_portfolioitem-symbol-response-contract/index.md b/docs/batches/bat-014_20260524_portfolioitem-symbol-response-contract/index.md new file mode 100644 index 0000000..7579b2b --- /dev/null +++ b/docs/batches/bat-014_20260524_portfolioitem-symbol-response-contract/index.md @@ -0,0 +1,33 @@ +# BAT-014: PortfolioItem symbol 응답 계약 보강 (REQ-025, REQ-026, REQ-027, REQ-028, REQ-032) + +- Status: release-candidate +- Profile: batch-lite +- Source Discovery: dcy-003 +- Included REQ: REQ-025, REQ-026, REQ-027, REQ-028, REQ-032 +- Excluded REQ: REQ-029, REQ-030, REQ-031 + +## Summary +- trex-orbit 팀 이슈(`docs/issues/20260524_portfolio-hub-ticket-symbol-in-portfolios.md`) 후속으로, `PortfolioItem` 기반 v1 portfolio 응답 계약에 `symbol` 필드를 추가한다. +- 특히 `GET /v1/portfolios` 는 클라이언트 캐시 복원이 아니라 **`portfolios JOIN assets` 결과를 그대로 응답**해야 하며, `symbol` 은 linked asset row의 `assets.symbol` 이어야 한다. +- POST/PATCH/DELETE `/v1/portfolios` 응답도 동일한 `PortfolioItem` contract를 따르도록 정합성을 맞추고, baseline 문서(`docs/interface-contract.md`)에 이를 반영한다. + +## Scope +- In Scope: + - `app/schemas/v1.py` — `PortfolioItem.symbol: str | None = None` 추가 + - `app/api/v1_portfolios.py` — `_portfolio_to_item()` 에 `symbol=portfolio.symbol` 반영 + - `app/repositories/postgres.py` — GET/list/create/update/delete 응답이 asset join 결과를 유지하는지 검증 + - `tests/test_v1_portfolios_list.py` — GET `/v1/portfolios` 응답 `symbol` 검증 추가 + - `tests/test_v1_portfolios_crud.py` — POST/PATCH/DELETE 응답 `symbol` 검증 추가 + - `docs/interface-contract.md` — `PortfolioItem` 응답 예시/설명에 `symbol` 및 GET join 기반 응답 설명 추가 +- Out of Scope: + - portfolio write semantics 변경 (ownership, soft delete, disabled 보호 정책 자체는 불변) + - `/v1/portfolios/{id}/ledger/trades`, `/tags`, `/comments` 계약 변경 + - assets catalog schema 또는 DB migration + +## Documents +- Planning: [planning.md](./planning.md) + +## Notes +- profile 선정 근거: follow-up change는 기존 Implemented REQ의 응답 contract augmentation이며 범위가 좁고 additive 성격이 강해 `batch-lite` 로 시작한다. +- 다만 `docs/interface-contract.md` 갱신은 필수이므로 문서 정합성까지 한 배치에서 닫는다. +- BAT-009 / BAT-010 verification evidence는 본 batch 완료 후 superseded 표기가 필요할 수 있다. diff --git a/docs/batches/bat-014_20260524_portfolioitem-symbol-response-contract/planning.md b/docs/batches/bat-014_20260524_portfolioitem-symbol-response-contract/planning.md new file mode 100644 index 0000000..3fe5e36 --- /dev/null +++ b/docs/batches/bat-014_20260524_portfolioitem-symbol-response-contract/planning.md @@ -0,0 +1,50 @@ +# BAT-014 Planning + +- Status: draft +- Batch: BAT-014 +- Profile: batch-lite + +## Included REQ +- REQ-025: POST /v1/portfolios 응답에 `PortfolioItem.symbol` 포함 +- REQ-026: PATCH /v1/portfolios/{portfolio_id} 응답에 `PortfolioItem.symbol` 포함 +- REQ-027: DELETE /v1/portfolios/{portfolio_id} (HTTP 200 응답) 에 `PortfolioItem.symbol` 포함 +- REQ-028: GET /v1/portfolios 응답이 `portfolios JOIN assets` 결과를 기반으로 `symbol` 을 직접 반환 +- REQ-032: baseline 문서에 위 계약 반영 + +## Delivery Plan +1. `app/schemas/v1.py` 의 `PortfolioItem` 에 `symbol: str | None = None` 추가 +2. `app/api/v1_portfolios.py` 의 `_portfolio_to_item()` 에 `symbol=portfolio.symbol` 추가 +3. `app/repositories/postgres.py` 의 `_SELECT_PORTFOLIO` / `_row_to_portfolio` / `get()` / `list_portfolios()` 흐름을 재확인해 GET `/v1/portfolios` 가 asset join 결과를 응답까지 유지하는지 검증 +4. `tests/test_v1_portfolios_list.py` 에서 GET 응답 item의 `symbol` 존재 및 기대값(`005930`) 검증 추가 +5. `tests/test_v1_portfolios_crud.py` 에서 POST/PATCH/DELETE 응답의 `symbol` 존재 및 기대값 검증 추가 +6. `docs/interface-contract.md` 에 POST/PATCH/DELETE/GET `/v1/portfolios` 응답 설명의 `symbol` 반영 및 GET의 asset join 기반 응답 설명 추가 +7. 회귀 테스트 실행 및 결과를 후속 verification 단계에 기록 + +## Dependencies +- Internal: + - BAT-009 (v1 portfolio CRUD core) — 기존 구현과 테스트 기반 + - BAT-010 (portfolio v1 baseline docs) — interface-contract baseline 업데이트 trace +- External: + - 없음 + +## Design Gate +- Design Required Before Implementation: **no** +- Reason: + - 변경은 기존 CRUD/list 응답 contract에 additive field 하나를 추가하고 문서/테스트를 보강하는 수준이다. + - `app/repositories/postgres.py` 는 이미 `_SELECT_PORTFOLIO` 에서 `JOIN assets a ON p.asset_id = a.asset_id` 및 `a.symbol` 선택을 수행하고 있어, 새 쿼리 전략이나 구조 재설계가 필요하지 않다. + - 사용자가 명시한 핵심 제약(`GET /v1/portfolios` 는 asset join 결과를 응답해야 함)도 현재 repository shape와 정합한다. 따라서 design 산출물 없이 구현/검증으로 바로 진행 가능하다. + +## Milestones +1. Schema/API serialization 반영 +2. CRUD/list 테스트 보강 +3. baseline 문서 갱신 +4. 회귀 테스트 실행 +5. verification 근거 정리 후 REQ 재Implemented 검토 + +## Risks +- **R1 (Low)**: 테스트 fixture가 in-memory fake repo를 사용하므로 repository-level join 보장을 직접 증명하지 못할 수 있음. + - **대응**: GET contract 문서와 repository 코드 확인 결과를 verification에 함께 기록하고, 필요 시 repo integration test 추가를 검토. +- **R2 (Low)**: `PortfolioItem` 에 `symbol` 추가 시 기존 스냅샷/응답 dict equality 테스트가 깨질 수 있음. + - **대응**: 테스트 expectation을 additive field 기준으로 갱신. +- **R3 (Low)**: baseline 문서가 POST/PATCH/DELETE/GET 중 일부 응답만 갱신되어 source of truth 불일치가 남을 수 있음. + - **대응**: `docs/interface-contract.md` 네 경로를 한 턴에 모두 갱신. diff --git a/docs/batches/bat-014_20260524_portfolioitem-symbol-response-contract/verification.md b/docs/batches/bat-014_20260524_portfolioitem-symbol-response-contract/verification.md new file mode 100644 index 0000000..4386aca --- /dev/null +++ b/docs/batches/bat-014_20260524_portfolioitem-symbol-response-contract/verification.md @@ -0,0 +1,79 @@ +# BAT-014 Verification + +- Status: confirmed +- Batch: BAT-014 + +## Acceptance Mapping + +### REQ-025: POST /v1/portfolios +- AC-1 (admin + user_id → target owner): `tests/test_v1_portfolios_crud.py::test_post_admin_creates_for_target_user` PASSED — `owner_user_id == _OTHER_USER_ID`. +- AC-2 (session/env token → own user/fallback): `tests/test_v1_portfolios_crud.py::test_post_user_creates_for_self` PASSED. +- AC-3 (service token → 403): `tests/test_v1_portfolios_crud.py::test_post_service_token_returns_403` PASSED. +- AC-5 (inactive/missing asset → 422): `tests/test_v1_portfolios_crud.py::test_post_inactive_asset_returns_422`, `tests/test_v1_portfolios_crud.py::test_post_missing_asset_returns_422` PASSED. +- AC-6 (no token → 401): `tests/test_v1_portfolios_crud.py::test_post_no_bearer_returns_401` PASSED. +- AC-7 (non-admin body user_id ignored): `tests/test_v1_portfolios_crud.py::test_post_non_admin_user_id_ignored` PASSED. +- AC-8 (POST 응답 `symbol` == joined `assets.symbol`): `tests/test_v1_portfolios_crud.py::test_post_admin_creates_for_target_user` PASSED — response body `symbol == "005930"`. + +### REQ-026: PATCH /v1/portfolios/{portfolio_id} +- AC-1 (admin → any portfolio): `tests/test_v1_portfolios_crud.py::test_patch_admin_updates_any_portfolio` PASSED — `broker == "ebest"`. +- AC-2 (session token + own portfolio → 200): `tests/test_v1_portfolios_crud.py::test_patch_user_updates_own_portfolio` PASSED. +- AC-3 (session token + other portfolio → 403): `tests/test_v1_portfolios_crud.py::test_patch_user_other_portfolio_returns_403` PASSED. +- AC-4 (service token → 403): `tests/test_v1_portfolios_crud.py::test_patch_service_token_returns_403` PASSED. +- AC-6 (no token → 401): `tests/test_v1_portfolios_crud.py::test_patch_no_bearer_returns_401` PASSED. +- AC-7 (non-existent → 404): `tests/test_v1_portfolios_crud.py::test_patch_nonexistent_portfolio_returns_404` PASSED. +- AC-8 (disabled + non-status field → 409): `tests/test_v1_portfolios_crud.py::test_patch_disabled_portfolio_non_status_returns_409` PASSED. +- AC-9 (disabled + status=enabled → 200): `tests/test_v1_portfolios_crud.py::test_patch_disabled_portfolio_status_restore_returns_200` PASSED. +- AC-10 (PATCH 응답 `symbol` == joined `assets.symbol`): `tests/test_v1_portfolios_crud.py::test_patch_admin_updates_any_portfolio`, `tests/test_v1_portfolios_crud.py::test_patch_disabled_portfolio_status_restore_returns_200` PASSED — both assert `symbol == "005930"`. + +### REQ-027: DELETE /v1/portfolios/{portfolio_id} +- AC-1 (session + own → 200, disabled, daemon_id NULL): `tests/test_v1_portfolios_crud.py::test_delete_user_own_active_portfolio_soft_deletes` PASSED. +- AC-3 (admin + other user → 200): `tests/test_v1_portfolios_crud.py::test_delete_admin_other_user_portfolio` PASSED. +- AC-4 (session + other → 403): `tests/test_v1_portfolios_crud.py::test_delete_user_other_portfolio_returns_403` PASSED. +- AC-5 (service token → 403): `tests/test_v1_portfolios_crud.py::test_delete_service_token_returns_403` PASSED. +- AC-7 (no token → 401): `tests/test_v1_portfolios_crud.py::test_delete_no_bearer_returns_401` PASSED. +- AC-8 (non-existent → 404): `tests/test_v1_portfolios_crud.py::test_delete_nonexistent_portfolio_returns_404` PASSED. +- AC-11 (DELETE 200 응답 `symbol` == joined `assets.symbol`): `tests/test_v1_portfolios_crud.py::test_delete_user_own_active_portfolio_soft_deletes`, `tests/test_v1_portfolios_crud.py::test_delete_admin_other_user_portfolio` PASSED — both assert `symbol == "005930"`. + +### REQ-028: GET /v1/portfolios +- AC-1 (service + daemon_id → own daemon portfolios): `tests/test_v1_portfolios_list.py::test_get_service_token_daemon_bound_returns_own_portfolios` PASSED. +- AC-2 (service no daemon → 403): `tests/test_v1_portfolios_list.py::test_get_service_token_no_daemon_returns_403` PASSED. +- AC-3 (session → own portfolios): `tests/test_v1_portfolios_list.py::test_get_user_token_returns_own_portfolios` PASSED. +- AC-4 (admin no ?user_id → default branch): `tests/test_v1_portfolios_list.py::test_get_admin_default_returns_own_portfolios` PASSED. +- AC-5 (admin + ?user_id → target): `tests/test_v1_portfolios_list.py::test_get_admin_with_user_id_returns_target_portfolios` PASSED. +- AC-7 (disabled excluded by default): `tests/test_v1_portfolios_list.py::test_get_excludes_disabled_by_default` PASSED. +- AC-8 (`include_disabled=true` → all): `tests/test_v1_portfolios_list.py::test_get_include_disabled_returns_all` PASSED. +- AC-9 (client token → 403): `tests/test_v1_portfolios_list.py::test_get_client_token_returns_403` PASSED. +- AC-10 (no token → 401): `tests/test_v1_portfolios_list.py::test_get_no_bearer_returns_401` PASSED. +- AC-11 (`asset_id` response field): `tests/test_v1_portfolios_list.py::test_get_response_has_asset_id_field` PASSED. +- AC-12 (`symbol` == joined `assets.symbol`): `tests/test_v1_portfolios_list.py::test_get_service_token_daemon_bound_returns_own_portfolios`, `tests/test_v1_portfolios_list.py::test_get_response_has_asset_id_field` PASSED — both assert `symbol == "005930"`. + +### REQ-032: portfolio v1 baseline 문서 갱신 +- AC-1 (`docs/interface-contract.md` 에 v1 portfolio route entries 존재): 문서 유지 + POST/PATCH/DELETE/GET `/v1/portfolios` 섹션 갱신 완료. +- AC-6 (사람 리뷰 기준 baseline 충돌 없음): 이번 batch 범위에서 `docs/interface-contract.md` 의 portfolio response contract를 코드와 정합화했다. +- AC-7 (`symbol` 필드 + GET join 설명): `docs/interface-contract.md` 에서 POST/PATCH/DELETE/GET `/v1/portfolios` 응답 설명에 `symbol` 추가, GET 섹션에 `portfolios JOIN assets` 기반 직렬화 설명 추가 완료. + +## Evidence +- Test: + - `./.venv/bin/python -m pytest tests/test_v1_portfolios_crud.py tests/test_v1_portfolios_list.py -q` → **32 passed in 2.13s** + - `./.venv/bin/python -m pytest -q` → **371 passed in 11.00s** +- Manual / code inspection: + - `app/repositories/postgres.py::_row_to_portfolio()` 는 `symbol=row[4]` 를 `Portfolio.symbol` 로 매핑한다. + - `app/repositories/postgres.py::get()` / `create_portfolio()` / `update_portfolio()` / `soft_delete_portfolio()` / `list_portfolios()` 는 `_SELECT_PORTFOLIO` 기반 join 결과를 재사용한다. + - 따라서 GET 뿐 아니라 POST/PATCH/DELETE 응답도 linked asset row의 `assets.symbol` 을 유지한다. +- Logs: + - pytest 출력은 `implementation.md` Execution Log 8~9 항목 참조. + +## Architecture Impact Sync +- Profile이 `batch-lite` 이고 planning.md 의 Design Gate는 `no` 다. +- 실제 구현은 schema/API/test/docs contract augmentation에 한정되며 신규 쿼리 전략/모듈/마이그레이션이 없다. +- `app/repositories/postgres.py` 의 기존 join shape를 재사용했으므로 **no architecture impact confirmed**. +- Baseline docs sync는 REQ-032 범위에 맞춰 `docs/interface-contract.md` 갱신으로 충족했다. `docs/project-structure.md` / `docs/runtime-flows.md` / `docs/data-model.md` 는 이번 변경으로 추가 수정이 필요한 구조 변화가 없다. + +## Result +- Ready for Release: true +- Blocking Issues: + - 없음 (본 batch 범위 내 AC evidence 확보) +- Follow-up: + - BAT-009 verification의 REQ-025/026/027/028 response-contract evidence는 BAT-014 기준으로 superseded 처리. + - BAT-010 verification의 REQ-032 interface-contract evidence는 BAT-014 기준으로 superseded 처리. + - 기존 422 deprecation warning(`HTTP_422_UNPROCESSABLE_ENTITY`) 정리는 별도 housekeeping 성격의 후속 change로 분리 가능. diff --git a/docs/batches/index.md b/docs/batches/index.md index fd08635..a7964c7 100644 --- a/docs/batches/index.md +++ b/docs/batches/index.md @@ -21,3 +21,4 @@ | BAT-011 | standard | release-candidate | REQ-029 | dcy-003 | [bat-011](./bat-011_20260521_v1-portfolios-ledger-path-migration/) | | BAT-012 | standard | release-candidate | REQ-002, REQ-010 | dcy-001, dcy-002, dcy-003 | [bat-012](./bat-012_20260521_assets-tagging-and-admin-get-redefinition/) | | BAT-013 | batch-lite | release-candidate | REQ-005 | dcy-002 | [bat-013](./bat-013_20260521_asset-tag-apply-remove-fix/) | +| BAT-014 | batch-lite | release-candidate | REQ-025, REQ-026, REQ-027, REQ-028, REQ-032 | dcy-003 | [bat-014](./bat-014_20260524_portfolioitem-symbol-response-contract/) | diff --git a/docs/interface-contract.md b/docs/interface-contract.md index fc8aed1..ecd09a1 100644 --- a/docs/interface-contract.md +++ b/docs/interface-contract.md @@ -300,21 +300,24 @@ - Auth: any valid token; service/client → HTTP 403 - Body: `{"asset_id": "", "broker": "", "name": "", "cash_balance": , "asset_quantity": , "asset_price": , "profit_buffer": , "strategy_name": "", "strategy_params": {}, "managed_by_daemon_id": "", "user_id": " (admin only)"}` -- Response: HTTP 201 `PortfolioItem` +- Response: HTTP 201 `PortfolioItem {"portfolio_id": "", "asset_id": "", "symbol": "", "owner_user_id": "", "broker": "", "status": "", "name": "", ...}` +- Note: `symbol` is populated from the server-side joined asset row (`portfolios.asset_id -> assets.asset_id -> assets.symbol`), not from client-side catalog reconstruction. - Error: HTTP 422 if asset not found or inactive; HTTP 403 if service/client token ### Endpoint: `PATCH /v1/portfolios/{portfolio_id}` - Auth: any valid token; service/client → HTTP 403; ownership required - Body: `{"broker": "", "status": "", "name": "", "strategy_name": "", "strategy_params": {}, "asset_price": , "managed_by_daemon_id": ""}` -- Response: HTTP 200 `PortfolioItem` +- Response: HTTP 200 `PortfolioItem {"portfolio_id": "", "asset_id": "", "symbol": "", ...}` +- Note: response `symbol` is emitted from the joined asset row referenced by `asset_id`. - Error: HTTP 403 if service/client or ownership mismatch; HTTP 404 if portfolio not found; HTTP 409 if portfolio is disabled and non-status fields are provided ### Endpoint: `DELETE /v1/portfolios/{portfolio_id}` (Soft Delete) - Auth: any valid token; service/client → HTTP 403; ownership required - Body: none -- Response: HTTP 200 `PortfolioItem` with `status="disabled"`, `managed_by_daemon_id=null` +- Response: HTTP 200 `PortfolioItem` with `status="disabled"`, `managed_by_daemon_id=null`, `symbol=""` +- Note: soft delete does not break the asset join; `symbol` remains the linked `assets.symbol` value in the HTTP 200 response. - Dependent data: portfolio_comments and tag_ids are preserved on soft delete; only status and daemon assignment change - Error: HTTP 403 if service/client or ownership mismatch; HTTP 404 if not found @@ -325,7 +328,8 @@ - Branching by token type: - `service`: returns portfolios where `managed_by_daemon_id = token.daemon_id`; NULL daemon_id → HTTP 403 - `user/admin`: returns owner's own portfolios; admin may override with `?user_id=` -- Response: HTTP 200 `PortfolioListResponse {"portfolios": [...], "total": }` +- Response: HTTP 200 `PortfolioListResponse {"portfolios": [{"portfolio_id": "", "asset_id": "", "symbol": "", ...}], "total": }` +- Note: list items are serialized from the server-side `portfolios JOIN assets` result; `symbol` must equal the joined `assets.symbol` for each returned `asset_id`. ### Endpoint: `GET /v1/portfolios/{portfolio_id}/tags` diff --git a/docs/issues/20260524_portfolio-hub-ticket-symbol-in-portfolios.md b/docs/issues/20260524_portfolio-hub-ticket-symbol-in-portfolios.md new file mode 100644 index 0000000..bfd5355 --- /dev/null +++ b/docs/issues/20260524_portfolio-hub-ticket-symbol-in-portfolios.md @@ -0,0 +1,119 @@ +# [REQ] `GET /v1/portfolios` 응답에 종목 코드(`symbol`) 포함 + +## 요약 + +`GET /v1/portfolios` 응답의 `PortfolioItem` 에 종목 코드(`symbol`) 필드를 추가해주세요. 현재는 `asset_id` (UUID) 만 반환되어, 종목 코드가 필요한 클라이언트가 별도 카탈로그 조회로 매핑해야 합니다. + +## 배경 + +trex-orbit 자동매매 데몬은 매 사이클마다 `GET /v1/portfolios` 로 daemon-scoped portfolio 리스트를 받아 운용 대상 종목을 결정합니다. 이후 외부 데이터 서비스 (`kis-data-hub`) 에 시세 / 분봉을 요청하려면 종목 코드 (예: `"005930"`) 가 필요한데, 현재 `PortfolioItem` 에는 `asset_id` (UUID) 만 있고 `symbol` 이 빠져 있어 다음 우회가 필요합니다. + +1. trex-orbit 부팅 시 asset 카탈로그를 별도로 조회해 `asset_id → symbol` 캐시 구성 +2. 사이클 중 `asset_id` 마다 캐시 lookup +3. 캐시 미스 시 portfolio-hub 에 추가 조회 + +이 과정에서 캐시 stale / 신규 asset 등록 후 동기화 지연 / 클라이언트마다 캐시 로직 중복 같은 운영 부담이 발생합니다. + +## 현재 상태 + +**Endpoint**: `GET /v1/portfolios` ([`app/api/v1_portfolios.py:224`](app/api/v1_portfolios.py#L224)) + +**응답 스키마**: [`PortfolioItem`](app/schemas/v1.py#L165) + +```python +class PortfolioItem(BaseModel): + portfolio_id: str + asset_id: str | None = None # UUID + owner_user_id: str | None = None + broker: str + status: str + name: str + cash_balance: float = 0.0 + asset_quantity: float = 0.0 + price: float = 0.0 + profit_buffer: float = 0.0 + tag_ids: list[str] = Field(default_factory=list) + managed_by_daemon_id: str | None = None + created_at: datetime | None = None + updated_at: datetime | None = None + # symbol 없음 +``` + +종목 코드는 별도 `AssetItem` 에만 존재 ([`app/schemas/v1.py:55`](app/schemas/v1.py#L55)): + +```python +class AssetItem(BaseModel): + asset_id: str + symbol: str # ← 여기 있음 + status: str + ... +``` + +## 요청 변경 + +### 1. 스키마 + +`PortfolioItem` 에 `symbol` 필드 추가: + +```python +class PortfolioItem(BaseModel): + portfolio_id: str + asset_id: str | None = None + symbol: str | None = None # ← 추가 요청 + owner_user_id: str | None = None + ... +``` + +### 2. 응답 채움 로직 + +`GET /v1/portfolios` 의 `_portfolio_to_item` 빌더가 `portfolios.asset_id → assets.symbol` join 결과를 활용해 `symbol` 을 채워주세요. 이미 repository 단계에서 join 되어 있다면 변환 로직만 추가하면 됩니다. + +### 3. 단건 / 관련 엔드포인트도 동일 적용 + +다음 엔드포인트의 응답에도 `symbol` 포함: + +- `POST /v1/portfolios` (생성 후 반환) +- `PATCH /v1/portfolios/{portfolio_id}` +- `DELETE /v1/portfolios/{portfolio_id}` (soft delete 응답) +- 기타 `PortfolioItem` 을 반환하는 모든 경로 + +## 정당성 + +1. **클라이언트 단순화**: daemon 클라이언트 (trex-orbit 등) 가 portfolio 정보만 받으면 즉시 외부 시세 API 호출 가능. 추가 카탈로그 조회 / 캐시 관리 불필요. +2. **일관성**: `cash_balance`, `asset_quantity`, `price` 등 자산 관련 필드는 이미 PortfolioItem 에 포함됨. `symbol` 도 자산 식별의 핵심이므로 같이 포함되는 것이 자연스러움. +3. **stale cache 위험 제거**: 클라이언트 캐시 + miss fallback 패턴은 카탈로그 변경 시 일시적 불일치 가능. server 측 join 으로 항상 최신 매핑 제공. +4. **운영 부담 감소**: 캐시 무효화 / 갱신 / 재로드 로직이 클라이언트마다 중복 구현되는 것을 회피. + +## 호환성 + +`symbol` 은 신규 필드이며 `Optional[str]` 로 추가하면 기존 클라이언트에 영향 없음 (additive change). + +## 영향 범위 + +- [`app/api/v1_portfolios.py`](app/api/v1_portfolios.py) — `_portfolio_to_item` 에 symbol 채움 +- [`app/schemas/v1.py`](app/schemas/v1.py#L165) — `PortfolioItem` 에 `symbol` 필드 추가 +- [`app/repositories/`](app/repositories/) — `list_portfolios` / `get_portfolio` 가 asset join 결과를 들고 오도록 보강 (이미 그렇다면 변경 없음) +- [`docs/interface-contract.md`](docs/interface-contract.md#L321) — `GET /v1/portfolios` 응답 예시 갱신 +- 테스트 — `PortfolioItem` 응답 형식 검증 케이스 추가 + +## 검수 기준 + +1. `GET /v1/portfolios` 호출 시 각 item 에 `symbol` 필드가 포함됨 +2. `symbol` 값은 해당 `asset_id` 의 `AssetItem.symbol` 과 일치 (server side join) +3. 단건 조회 / 생성 / 수정 / 삭제 응답도 동일하게 `symbol` 포함 +4. `interface-contract.md` 의 `GET /v1/portfolios` 응답 예시에 `symbol` 명시 +5. 기존 테스트 회귀 없음 + 신규 케이스 추가 + +## 우선순위 + +**Medium** — trex-orbit 사이드에서 캐시 기반 우회가 가능해 즉시 차단 이슈는 아니지만, 운영 본격 시작 전 정리하면 lifecycle 비용을 크게 줄임. + +## 관련 문서 + +- trex-orbit `temp/plan.md` 8.4.1 step 3 (asset_id → symbol resolve) — 본 이슈의 우회 구현 단계 +- trex-orbit `temp/plan.md` 8.6.4.1 전제 시점 — universe 결정 과정에서 symbol resolve 가 필요한 이유 + +## 작성자 / 일자 + +- 요청자: trex-orbit 팀 (genichin@gmail.com) +- 작성일: 2026-05-24 diff --git a/docs/srs/Documentation/req-032_portfolio-v1-baseline-docs-update.md b/docs/srs/Documentation/req-032_portfolio-v1-baseline-docs-update.md index 18c0316..6efc9b6 100644 --- a/docs/srs/Documentation/req-032_portfolio-v1-baseline-docs-update.md +++ b/docs/srs/Documentation/req-032_portfolio-v1-baseline-docs-update.md @@ -13,6 +13,7 @@ - `docs/interface-contract.md` 갱신: - `/v1/portfolios*` 라우트 명세 추가: POST(REQ-025), PATCH(REQ-026), DELETE(REQ-027), GET(REQ-028), `/ledger/trades`(REQ-029), `/tags`(REQ-030), `/comments`(REQ-031) - 각 라우트의 토큰 분기 규칙(`token_type ∈ {service, client}` 거부 + `permission` 기준 분기) 일관 명시 + - `PortfolioItem` 응답 예시에 `symbol` 필드를 포함하고, 특히 `GET /v1/portfolios` 는 `portfolios JOIN assets` 기반 응답으로 `assets.symbol` 을 노출한다는 점을 명시 - 구 path `POST /v1/assets/{asset_id}/ledger/trades` 제거 + 동일 batch release 정책 명시 - `GET /v1/assets`는 dcy-001 FR-2 admin catalog 조회 전용으로 의미 복원 (REQ-013 Deprecated 정합) - daemon authorization model 갱신 (3단계 fail-closed) @@ -36,6 +37,7 @@ 4. Given `grep -n "soft delete\|status='disabled'\|disabled" docs/data-model.md`, When 본 REQ 구현 후, Then Portfolio.status='disabled' 의미와 의존 데이터 보존 정책이 반영된다. 5. Given `grep -n "v1_portfolios.py" docs/project-structure.md`, When 본 REQ 구현 후, Then 신규 라우터 파일이 명시된다. 6. Given 사람 리뷰, When baseline 4종 문서를 점검, Then REQ-025~031의 v1 portfolio mutation 정책과 충돌하는 잔재가 없다. +7. Given `grep -n "symbol" docs/interface-contract.md`, When 본 REQ 구현 후, Then `PortfolioItem` 기반 POST/PATCH/DELETE/GET `/v1/portfolios` 응답 설명에 `symbol` 필드가 명시되고 GET 항목은 joined asset result 설명을 포함한다. ## Impacted Area - Docs: @@ -57,6 +59,64 @@ ## Change Log +### CHG-20260524-02 +- Date: 2026-05-24 +- Author: Hermes Agent + +#### Change Summary +- Status: Approved → Implemented + +#### Intent +- BAT-014 verification 완료. `docs/interface-contract.md` 에 portfolio CRUD/list 응답의 `symbol` 필드와 GET join 기반 응답 설명이 반영되어 REQ-032 augmentation 범위가 충족되었다. + +#### Acceptance Criteria Delta +- No change + +#### Impacted Area Delta +- No change + +#### Delivery Trace Delta +- Batches: BAT-014 (release-candidate); BAT-010 interface-contract evidence superseded by BAT-014 +- Releases: pending + +#### Revalidation Impact +- Reverification Needed: no +- Existing Implementation Invalidated: no +- Status Recommendation: promote-to-implemented +- Follow-up Action: release 단계 전 추가 baseline drift 점검 + +### CHG-20260524-01 +- Date: 2026-05-24 +- Author: Hermes Agent + +#### Change Summary +- portfolio v1 baseline 문서에 `PortfolioItem.symbol` 및 GET `/v1/portfolios` asset-join 응답 계약 반영이 필요해 REQ-032를 augment 하고 상태를 Implemented → Approved로 되돌림. + +#### Intent +- trex-orbit 팀 요청으로 CRUD/list portfolio 응답 계약이 `symbol` 포함 형태로 확장되었다. baseline 문서가 이를 반영하지 않으면 구현 후에도 API source of truth가 stale 해지므로, interface-contract/runtime 설명의 후속 갱신을 별도 delivery trace로 강제한다. + +#### Acceptance Criteria Delta +- Added: + - AC-7: baseline 문서에 `symbol` 필드 및 GET join 기반 응답 설명이 존재해야 함 + +#### Impacted Area Delta +- Docs: `docs/interface-contract.md` (필수), 필요 시 `docs/runtime-flows.md` 의 GET `/v1/portfolios` 설명 보강 +- Modules: 없음 +- Tests: 없음 (문서 grep + 사람 리뷰) + +#### Delivery Trace Delta +- Batches: BAT-010 evidence partially superseded; follow-up BAT-014 예정 +- Releases: REL-001 baseline evidence 일부 재검토 필요 + +#### Revalidation Impact +- Reverification Needed: yes +- Existing Implementation Invalidated: yes (baseline documentation evidence partial) +- Status Recommendation: revert-to-approved +- Follow-up Action: BAT-014에서 interface-contract 예시와 설명 갱신 후 재검증 + +#### Validation Plan +- `docs/interface-contract.md` 에서 POST/PATCH/DELETE/GET `/v1/portfolios` 응답 설명과 예시에 `symbol` 이 추가됐는지, GET 섹션에 joined asset result 설명이 있는지 grep + 사람 리뷰로 확인한다. + ### CHG-20260521-01 - Date: 2026-05-21 - Author: Justin Ko diff --git a/docs/srs/Interface/req-025_v1-portfolios-create.md b/docs/srs/Interface/req-025_v1-portfolios-create.md index 2f33473..5d99610 100644 --- a/docs/srs/Interface/req-025_v1-portfolios-create.md +++ b/docs/srs/Interface/req-025_v1-portfolios-create.md @@ -20,7 +20,8 @@ - **body 스키마** (`PortfolioCreateRequest` 신규): - 필수: `asset_id`, `broker` - 선택: `name`, `cash_balance`, `asset_quantity`, `asset_price`, `profit_buffer`, `strategy_name`, `strategy_params`, `managed_by_daemon_id`, `user_id`(admin 분기 전용) -- 응답: HTTP 201 + 생성된 portfolio의 `PortfolioItem`(`portfolio_id`, `asset_id`, `owner_user_id`, ...). +- 응답: HTTP 201 + 생성된 portfolio의 `PortfolioItem`(`portfolio_id`, `asset_id`, `symbol`, `owner_user_id`, ...). +- 응답의 `symbol` 값은 생성된 `portfolios.asset_id`가 가리키는 `assets.symbol`을 기준으로 server-side join 결과에서 채운다. 클라이언트가 별도 catalog lookup으로 복원하는 방식은 본 REQ 범위의 정식 계약이 아니다. - catalog 검증 위치(서비스 계층 공통 진입점 vs v1 라우터 레이어 한정)는 본 REQ 구현 시점에 결정. dcy-003 R-5 트레이드오프 reference 참조. ## Acceptance Criteria @@ -31,6 +32,7 @@ 5. Given body `asset_id`가 catalog에 미존재 또는 `status='inactive'`, When `POST /v1/portfolios`, Then HTTP 422 또는 403로 거부되고 portfolio row가 생성되지 않는다. 6. Given Bearer token 없음, When `POST /v1/portfolios`, Then HTTP 401. 7. Given non-admin token + body `user_id` 지정, When `POST /v1/portfolios`, Then `user_id`는 무시되어 `owner_user_id == auth.user_id`로 생성되거나 HTTP 422. +8. Given active catalog asset(`asset_id=`, `symbol="005930"`), When `POST /v1/portfolios`, Then HTTP 201 응답의 `PortfolioItem.symbol == "005930"` 이고 값은 해당 asset row join 결과와 일치한다. ## Impacted Area - Docs: @@ -54,6 +56,64 @@ ## Change Log +### CHG-20260524-02 +- Date: 2026-05-24 +- Author: Hermes Agent + +#### Change Summary +- Status: Approved → Implemented + +#### Intent +- BAT-014 verification 완료. `PortfolioItem.symbol` 응답 계약이 코드, 테스트, baseline 문서에 반영되었고 REQ-025 acceptance criteria 전부가 trace 가능해졌다. + +#### Acceptance Criteria Delta +- No change + +#### Impacted Area Delta +- No change + +#### Delivery Trace Delta +- Batches: BAT-014 (release-candidate); BAT-009 response-contract evidence superseded by BAT-014 +- Releases: pending + +#### Revalidation Impact +- Reverification Needed: no +- Existing Implementation Invalidated: no +- Status Recommendation: promote-to-implemented +- Follow-up Action: release 단계에서 smoke 확인 + +### CHG-20260524-01 +- Date: 2026-05-24 +- Author: Hermes Agent + +#### Change Summary +- `PortfolioItem.symbol` 응답 계약 추가에 따라 REQ-025 생성 응답 contract를 보강하고 상태를 Implemented → Approved로 되돌림. + +#### Intent +- trex-orbit 팀 이슈(`docs/issues/20260524_portfolio-hub-ticket-symbol-in-portfolios.md`) 후속. 생성 응답만 `asset_id`를 반환하고 `symbol`을 생략하면 클라이언트가 별도 asset catalog 조회/캐시를 유지해야 하므로, 생성 직후 응답에서도 joined asset의 `symbol`을 즉시 반환하도록 계약을 확장한다. + +#### Acceptance Criteria Delta +- Added: + - AC-8: POST 응답의 `PortfolioItem.symbol` 이 `assets.symbol` join 결과와 일치해야 함 + +#### Impacted Area Delta +- Docs: `docs/interface-contract.md` 의 POST `/v1/portfolios` 응답 예시에 `symbol` 추가 필요 (REQ-032 후속) +- Modules: `app/schemas/v1.py`, `app/api/v1_portfolios.py` (`_portfolio_to_item`), `app/repositories/postgres.py:get()` join 응답 확인 +- Tests: `tests/test_v1_portfolios_crud.py` 에 POST 응답 `symbol` 검증 추가 필요 + +#### Delivery Trace Delta +- Batches: BAT-009 evidence partially superseded; follow-up BAT-014 예정 +- Releases: REL-001 response-contract evidence 일부 재검토 필요 + +#### Revalidation Impact +- Reverification Needed: yes +- Existing Implementation Invalidated: yes (response schema evidence partial) +- Status Recommendation: revert-to-approved +- Follow-up Action: BAT-014에서 POST/PATCH/DELETE/GET portfolio 응답 `symbol` 회귀 검증 추가 + +#### Validation Plan +- `tests/test_v1_portfolios_crud.py` 에서 POST 201 응답 body의 `symbol` 존재 및 `asset_id`가 가리키는 asset row의 `symbol` 일치 여부를 검증한다. + ### CHG-20260521-01 - Date: 2026-05-21 - Author: Justin Ko diff --git a/docs/srs/Interface/req-026_v1-portfolios-update.md b/docs/srs/Interface/req-026_v1-portfolios-update.md index c87e017..6d8bea7 100644 --- a/docs/srs/Interface/req-026_v1-portfolios-update.md +++ b/docs/srs/Interface/req-026_v1-portfolios-update.md @@ -20,6 +20,7 @@ - 수정 가능 필드는 기존 `UpdatePortfolioRequest` 범위: `broker`, `status`, `name`, `symbol`, `strategy_name`, `strategy_params`, `asset_price`, `managed_by_daemon_id` 등. - **disabled portfolio 보호** (REQ-027 soft delete 정책 정합): `portfolios.status == 'disabled'`인 portfolio에 대해 status 전이(`'enabled'`로 복구) 외 다른 필드 변경 시 HTTP 409 또는 422로 거부. - 응답: HTTP 200 + 갱신된 `PortfolioItem`. +- 응답의 `symbol` 값은 수정 대상 portfolio가 참조하는 `assets.symbol`을 기준으로 server-side join 결과에서 채운다. ## Acceptance Criteria 1. Given admin token + 본인 또는 타 user portfolio, When `PATCH /v1/portfolios/{id}` body `{"strategy_name": "..."}`, Then HTTP 200 + 갱신된 portfolio 반환. @@ -31,6 +32,7 @@ 7. Given 비존재 `portfolio_id`, When `PATCH /v1/portfolios/{id}`, Then HTTP 404. 8. Given `status='disabled'` portfolio + body `{"strategy_name": "..."}`(status 외 필드), When `PATCH`, Then HTTP 409 또는 422 (disabled write 차단). 9. Given `status='disabled'` portfolio + body `{"status": "enabled"}` (admin), When `PATCH`, Then HTTP 200 + status 복구. +10. Given portfolio가 active asset(`symbol="005930"`)을 참조, When `PATCH /v1/portfolios/{id}` 성공, Then HTTP 200 응답의 `PortfolioItem.symbol == "005930"` 이고 값은 joined asset row와 일치한다. ## Impacted Area - Docs: @@ -51,6 +53,64 @@ ## Change Log +### CHG-20260524-02 +- Date: 2026-05-24 +- Author: Hermes Agent + +#### Change Summary +- Status: Approved → Implemented + +#### Intent +- BAT-014 verification 완료. PATCH 성공 응답의 `PortfolioItem.symbol` 계약이 코드/테스트/문서에 반영되어 REQ-026 acceptance criteria 전부가 다시 충족되었다. + +#### Acceptance Criteria Delta +- No change + +#### Impacted Area Delta +- No change + +#### Delivery Trace Delta +- Batches: BAT-014 (release-candidate); BAT-009 response-contract evidence superseded by BAT-014 +- Releases: pending + +#### Revalidation Impact +- Reverification Needed: no +- Existing Implementation Invalidated: no +- Status Recommendation: promote-to-implemented +- Follow-up Action: release 단계에서 smoke 확인 + +### CHG-20260524-01 +- Date: 2026-05-24 +- Author: Hermes Agent + +#### Change Summary +- `PortfolioItem.symbol` 응답 계약 추가에 따라 REQ-026 수정 응답 contract를 보강하고 상태를 Implemented → Approved로 되돌림. + +#### Intent +- trex-orbit 팀이 portfolio mutation 후에도 추가 catalog lookup 없이 즉시 symbol을 사용해야 하는 요구를 제기했다. PATCH 응답이 기존 portfolio state만 되돌리고 joined asset 식별자(`symbol`)를 생략하면 CRUD 응답 contract가 비대칭이므로, 수정 응답에도 동일 필드를 포함하도록 정의를 확장한다. + +#### Acceptance Criteria Delta +- Added: + - AC-10: PATCH 성공 응답의 `PortfolioItem.symbol` 이 joined `assets.symbol` 과 일치해야 함 + +#### Impacted Area Delta +- Docs: `docs/interface-contract.md` 의 PATCH `/v1/portfolios/{id}` 응답 예시에 `symbol` 추가 필요 (REQ-032 후속) +- Modules: `app/schemas/v1.py`, `app/api/v1_portfolios.py` (`_portfolio_to_item`) +- Tests: `tests/test_v1_portfolios_crud.py` 에 PATCH 응답 `symbol` 검증 추가 필요 + +#### Delivery Trace Delta +- Batches: BAT-009 evidence partially superseded; follow-up BAT-014 예정 +- Releases: REL-001 response-contract evidence 일부 재검토 필요 + +#### Revalidation Impact +- Reverification Needed: yes +- Existing Implementation Invalidated: yes (response schema evidence partial) +- Status Recommendation: revert-to-approved +- Follow-up Action: BAT-014에서 PATCH 응답 contract 회귀 검증 추가 + +#### Validation Plan +- `tests/test_v1_portfolios_crud.py` 에서 PATCH 200 응답 body의 `symbol` 존재 및 joined asset symbol 일치 여부를 검증한다. + ### CHG-20260521-01 - Date: 2026-05-21 - Author: Justin Ko diff --git a/docs/srs/Interface/req-027_v1-portfolios-soft-delete.md b/docs/srs/Interface/req-027_v1-portfolios-soft-delete.md index 3b627b3..3529a4b 100644 --- a/docs/srs/Interface/req-027_v1-portfolios-soft-delete.md +++ b/docs/srs/Interface/req-027_v1-portfolios-soft-delete.md @@ -29,6 +29,7 @@ - 비존재 `portfolio_id`는 HTTP 404. ownership 불일치는 HTTP 403. - 이미 `disabled` 상태인 portfolio에 재호출 시 HTTP 200 + 멱등 응답(또는 HTTP 409 — 본 REQ 구현 시 확정). - 응답: HTTP 200 또는 204 + (HTTP 200 시) disabled 상태 portfolio 또는 확정 응답. +- HTTP 200 응답을 반환하는 구현에서는 `PortfolioItem.symbol` 도 함께 포함해야 하며, 값은 soft delete 이후에도 linked asset row join 결과와 동일해야 한다. ## Acceptance Criteria 1. Given session token + 본인 active portfolio, When `DELETE /v1/portfolios/{id}`, Then HTTP 200/204 + `portfolios.status='disabled'` + `managed_by_daemon_id IS NULL` 사후 상태. @@ -41,6 +42,7 @@ 8. Given 비존재 `portfolio_id`, When `DELETE /v1/portfolios/{id}`, Then HTTP 404. 9. Given disabled portfolio + admin `PATCH status='enabled'`, When `PATCH`, Then HTTP 200 + 복구. (REQ-026 AC-9와 연계) 10. Given AC-1 실행 후 disabled portfolio에 ledger trade/tag/comment write 시도, When 시도, Then 모두 차단(HTTP 409 또는 422). (REQ-029/030/031과 연계 검증) +11. Given active asset(`symbol="005930"`)을 참조하던 portfolio, When `DELETE /v1/portfolios/{id}` 가 HTTP 200 응답을 반환, Then 응답의 `PortfolioItem.symbol == "005930"` 이고 값은 joined asset row와 일치한다. ## Impacted Area - Docs: @@ -62,6 +64,64 @@ ## Change Log +### CHG-20260524-02 +- Date: 2026-05-24 +- Author: Hermes Agent + +#### Change Summary +- Status: Approved → Implemented + +#### Intent +- BAT-014 verification 완료. DELETE(soft delete) HTTP 200 응답의 `PortfolioItem.symbol` 계약이 코드/테스트/문서에 반영되어 REQ-027 acceptance criteria 전부가 다시 충족되었다. + +#### Acceptance Criteria Delta +- No change + +#### Impacted Area Delta +- No change + +#### Delivery Trace Delta +- Batches: BAT-014 (release-candidate); BAT-009 response-contract evidence superseded by BAT-014 +- Releases: pending + +#### Revalidation Impact +- Reverification Needed: no +- Existing Implementation Invalidated: no +- Status Recommendation: promote-to-implemented +- Follow-up Action: release 단계에서 smoke 확인 + +### CHG-20260524-01 +- Date: 2026-05-24 +- Author: Hermes Agent + +#### Change Summary +- `PortfolioItem.symbol` 응답 계약 추가에 따라 REQ-027 soft delete 응답 contract를 보강하고 상태를 Implemented → Approved로 되돌림. + +#### Intent +- trex-orbit 팀 이슈 후속으로, delete/disable 응답에서도 client가 별도 asset lookup 없이 포트폴리오 식별 symbol을 유지할 수 있어야 한다. soft delete는 asset reference를 제거하지 않으므로 HTTP 200 응답이 있는 경우 `symbol`을 함께 반환하는 계약이 자연스럽다. + +#### Acceptance Criteria Delta +- Added: + - AC-11: DELETE(HTTP 200) 응답의 `PortfolioItem.symbol` 이 joined `assets.symbol` 과 일치해야 함 + +#### Impacted Area Delta +- Docs: `docs/interface-contract.md` 의 DELETE `/v1/portfolios/{id}` 응답 예시에 `symbol` 추가 필요 (REQ-032 후속) +- Modules: `app/schemas/v1.py`, `app/api/v1_portfolios.py` (`_portfolio_to_item`) +- Tests: `tests/test_v1_portfolios_crud.py` 에 DELETE 200 응답 `symbol` 검증 추가 필요 + +#### Delivery Trace Delta +- Batches: BAT-009 evidence partially superseded; follow-up BAT-014 예정 +- Releases: REL-001 response-contract evidence 일부 재검토 필요 + +#### Revalidation Impact +- Reverification Needed: yes +- Existing Implementation Invalidated: yes (response schema evidence partial) +- Status Recommendation: revert-to-approved +- Follow-up Action: BAT-014에서 DELETE 응답 contract 회귀 검증 추가 + +#### Validation Plan +- `tests/test_v1_portfolios_crud.py` 에서 DELETE 200 응답 body의 `symbol` 존재 및 joined asset symbol 일치 여부를 검증한다. + ### CHG-20260521-01 - Date: 2026-05-21 - Author: Justin Ko diff --git a/docs/srs/Interface/req-028_v1-portfolios-list.md b/docs/srs/Interface/req-028_v1-portfolios-list.md index 2181a52..1eb5608 100644 --- a/docs/srs/Interface/req-028_v1-portfolios-list.md +++ b/docs/srs/Interface/req-028_v1-portfolios-list.md @@ -19,7 +19,8 @@ - `permission ∈ {read, trade-write}` → `owner_user_id = auth.user_id` 필터, 본인 portfolio만 반환. - **`?market=` 필터**: 모든 토큰 타입에서 지원. ` = ANY(assets.markets)`인 portfolio만 반환(catalog와 join). - **`?include_disabled=true` 옵션** (dcy-003 R-2 완화): 기본 응답은 `status='disabled'`인 portfolio 제외. opt-in 시에만 포함. -- 응답 항목 schema: 신규 `PortfolioItem` (`portfolio_id`, `asset_id`, `owner_user_id`, `broker`, `status`, `cash_balance`, ..., `tag_ids`, `managed_by_daemon_id`, ...). `asset_id`는 catalog의 `assets.asset_id`(UUID) 참조. +- 응답 항목 schema: 신규 `PortfolioItem` (`portfolio_id`, `asset_id`, `symbol`, `owner_user_id`, `broker`, `status`, `cash_balance`, ..., `tag_ids`, `managed_by_daemon_id`, ...). `asset_id`는 catalog의 `assets.asset_id`(UUID) 참조. +- `GET /v1/portfolios` 의 각 item은 반드시 `portfolios JOIN assets` 결과를 기반으로 직렬화되어야 한다. 특히 `symbol` 은 linked asset row의 `assets.symbol` 을 그대로 노출해야 하며, 클라이언트 캐시나 후속 catalog lookup으로 복원하는 모델은 정식 응답 계약이 아니다. - 응답: HTTP 200 + `PortfolioListResponse` (`portfolios: list[PortfolioItem]`, `total: int`). ## Acceptance Criteria @@ -34,6 +35,7 @@ 9. Given client token, When `GET /v1/portfolios`, Then HTTP 403 fail-closed. 10. Given Bearer token 없음, When `GET /v1/portfolios`, Then HTTP 401. 11. Given 응답 portfolio 항목, When `asset_id` 필드 확인, Then `assets.asset_id`(catalog UUID)를 참조한다. +12. Given `GET /v1/portfolios` 응답 item, When `symbol` 필드를 확인, Then 값은 같은 row의 `asset_id`가 가리키는 joined `assets.symbol` 과 일치한다. ## Impacted Area - Docs: @@ -57,6 +59,64 @@ ## Change Log +### CHG-20260524-02 +- Date: 2026-05-24 +- Author: Hermes Agent + +#### Change Summary +- Status: Approved → Implemented + +#### Intent +- BAT-014 verification 완료. GET `/v1/portfolios` 응답이 `portfolios JOIN assets` 결과에서 `symbol` 을 직접 반환함이 코드/테스트/문서로 재검증되었다. + +#### Acceptance Criteria Delta +- No change + +#### Impacted Area Delta +- No change + +#### Delivery Trace Delta +- Batches: BAT-014 (release-candidate); BAT-009 response-contract evidence superseded by BAT-014 +- Releases: pending + +#### Revalidation Impact +- Reverification Needed: no +- Existing Implementation Invalidated: no +- Status Recommendation: promote-to-implemented +- Follow-up Action: release 단계에서 smoke 확인 + +### CHG-20260524-01 +- Date: 2026-05-24 +- Author: Hermes Agent + +#### Change Summary +- `GET /v1/portfolios` 응답을 joined asset result 기반으로 명확히 고정하고 `PortfolioItem.symbol` 계약을 추가하기 위해 REQ-028을 augment. 상태를 Implemented → Approved로 되돌림. + +#### Intent +- trex-orbit 자동매매 데몬은 `GET /v1/portfolios` 응답만으로 외부 시세 API 호출 대상 symbol을 바로 얻어야 한다. 기존 계약은 `asset_id`만 제공해 클라이언트 측 catalog cache를 강제했으므로, server-side asset join 결과(`assets.symbol`)를 직접 응답에 포함하도록 명시한다. + +#### Acceptance Criteria Delta +- Added: + - AC-12: GET 응답 item의 `symbol` 은 joined `assets.symbol` 과 일치해야 함 + +#### Impacted Area Delta +- Docs: `docs/interface-contract.md` GET `/v1/portfolios` 예시/필드 정의에 `symbol` 및 join 기반 응답 설명 추가 필요 (REQ-032 후속) +- Modules: `app/schemas/v1.py`, `app/api/v1_portfolios.py` (`_portfolio_to_item`), `app/repositories/postgres.py` (`_SELECT_PORTFOLIO` join 결과를 응답까지 전달) +- Tests: `tests/test_v1_portfolios_list.py` 에 list 응답 `symbol` 검증 추가 필요 + +#### Delivery Trace Delta +- Batches: BAT-009 evidence partially superseded; follow-up BAT-014 예정 +- Releases: REL-001 response-contract evidence 일부 재검토 필요 + +#### Revalidation Impact +- Reverification Needed: yes +- Existing Implementation Invalidated: yes (response schema evidence partial) +- Status Recommendation: revert-to-approved +- Follow-up Action: BAT-014에서 GET `/v1/portfolios` 응답이 asset join 결과를 직접 반환하는지 회귀 검증 + +#### Validation Plan +- `tests/test_v1_portfolios_list.py` 에서 각 item의 `symbol` 필드 존재와 `asset_id`가 가리키는 joined asset row의 `symbol` 일치 여부를 검증한다. + ### CHG-20260521-01 - Date: 2026-05-21 - Author: Justin Ko diff --git a/docs/srs/index.md b/docs/srs/index.md index bf1c7dd..94539ff 100644 --- a/docs/srs/index.md +++ b/docs/srs/index.md @@ -116,8 +116,8 @@ ## Recent Change Log Summary +- 2026-05-24: REQ-025 / REQ-026 / REQ-027 / REQ-028 / REQ-032 Approved → Implemented (CHG-20260524-02 각각) — BAT-014 verification 완료. `PortfolioItem.symbol` 응답 계약이 코드(`app/schemas/v1.py`, `app/api/v1_portfolios.py`), 테스트(`tests/test_v1_portfolios_crud.py`, `tests/test_v1_portfolios_list.py`), baseline 문서(`docs/interface-contract.md`)에 반영됨. BAT-014는 release-candidate로 승격되었고 BAT-009/BAT-010의 관련 response-contract evidence는 superseded. +- 2026-05-24: REQ-025 / REQ-026 / REQ-027 / REQ-028 / REQ-032 Implemented → Approved (CHG-20260524-01 각각) — trex-orbit 팀 이슈(`docs/issues/20260524_portfolio-hub-ticket-symbol-in-portfolios.md`) 후속. `PortfolioItem.symbol` 응답 계약 추가 및 `GET /v1/portfolios` 의 asset join 기반 응답(`portfolios JOIN assets` → `assets.symbol`)을 명문화. BAT-009 / BAT-010 및 REL-001의 기존 response-contract evidence는 부분 무효화되어 follow-up BAT-014에서 재검증 필요. - 2026-05-21: REQ-005 Approved → Implemented (CHG-20260521-03) — BAT-013 confirm-batch-verification 완료. `append_asset_tag_id`/`remove_asset_tag_id` 메서드 신규 + 라우트 호출 교체 + 회귀 테스트 7건 추가. assets 테이블 mutation 정합화 evidence 확보. 전체 369 passed (BAT-012 baseline 362 → +7). BAT-004의 AC-1/AC-5 evidence는 BAT-013에서 superseded. CLI 동일 mismatch는 follow-up REQ 후보. - 2026-05-21: REQ-005 Implemented → Approved (CHG-20260521-02) — BAT-012 implementation 단계에서 `apply_asset_tag`/`remove_asset_tag` 라우트([app/api/v1_assets.py:175,200](../../app/api/v1_assets.py))가 `append_portfolio_tag_id`/`remove_portfolio_tag_id`를 호출하여 **portfolios 테이블을 대상화**하는 불일치 발견. AC-1/AC-5 의도("assets 테이블의 tag_ids")가 BAT-012 신설 `assets.tag_ids` 컬럼에 반영되지 않음. AC-1/AC-5 명문화, Impacted Area에 `append_asset_tag_id`/`remove_asset_tag_id` 메서드 추가. BAT-004 REQ-005 evidence는 부분 무효, 신규 batch 편입 필요. - 2026-05-21: REQ-002 / REQ-010 Approved → Implemented (CHG-20260521-03 각각) — BAT-012 confirm-batch-verification 완료. 362 PASSED (32 신규: REQ-002 AC-1~4 8건 + REQ-010 AC-9~12 9건 + AC-1~8 회귀 9건 + 단위 6건). assets.tag_ids 마이그레이션·도메인·repository·schema·GET 라우트 archived 합성 모두 evidence 확보. REQ-010 BAT-002 trace는 superseded by BAT-012로 표기. -- 2026-05-21: REQ-002 Implemented → Approved (CHG-20260521-02) — 파일명(`req-002_assets-tag-ids-column.md`)·dcy-002 FR-2와 본문 정합성 회복. 기존 본문(portfolios.tag_ids)은 dcy-001 분리 이전 portfolios==asset 시절 흔적으로 REQ-003과 중복이었음. 새 범위(assets.tag_ids + `idx_assets_tag_ids_gin`)는 미구현 — BAT-003 verification은 REQ-002 범위 무효화. REQ-009 Impacted Area에 cross-reference 추가(CHG-20260521-02, Implemented 유지). 신규 batch 편입 필요. -- 2026-05-21: REQ-010 Implemented → Approved (CHG-20260521-02) — REL-001 smoke 검증에서 GET /v1/assets가 dcy-001 시점 portfolio 조회 코드를 유지 중인 불일치 확인. dcy-003 옵션 B(REQ-013 Deprecation) 후속의 GET admin catalog 재정의를 본 REQ에 augmentation. AC-9~12 신설, app/api/v1_assets.py GET 라우트 재정의 + AssetCatalogListResponse 추가. BAT-002 verification 재실행 필요. diff --git a/tests/test_v1_portfolios_crud.py b/tests/test_v1_portfolios_crud.py index 747f56c..b742697 100644 --- a/tests/test_v1_portfolios_crud.py +++ b/tests/test_v1_portfolios_crud.py @@ -198,6 +198,7 @@ def test_post_admin_creates_for_target_user() -> None: assert data["owner_user_id"] == _OTHER_USER_ID assert data["broker"] == "kiwoom" assert data["status"] == "enabled" + assert data["symbol"] == "005930" def test_post_user_creates_for_self() -> None: @@ -302,7 +303,9 @@ def test_patch_admin_updates_any_portfolio() -> None: headers=_auth("admin-tok"), ) assert resp.status_code == 200 - assert resp.json()["broker"] == "ebest" + data = resp.json() + assert data["broker"] == "ebest" + assert data["symbol"] == "005930" def test_patch_user_updates_own_portfolio() -> None: @@ -402,7 +405,9 @@ def test_patch_disabled_portfolio_status_restore_returns_200() -> None: headers=_auth("admin-tok"), ) assert resp.status_code == 200 - assert resp.json()["status"] == "enabled" + data = resp.json() + assert data["status"] == "enabled" + assert data["symbol"] == "005930" # --------------------------------------------------------------------------- @@ -424,6 +429,7 @@ def test_delete_user_own_active_portfolio_soft_deletes() -> None: data = resp.json() assert data["status"] == "disabled" assert data["managed_by_daemon_id"] is None + assert data["symbol"] == "005930" def test_delete_admin_other_user_portfolio() -> None: @@ -434,7 +440,9 @@ def test_delete_admin_other_user_portfolio() -> None: with _with_repo(repo): resp = client.delete(f"/v1/portfolios/{_PORTFOLIO_UUID}", headers=_auth("admin-tok")) assert resp.status_code == 200 - assert resp.json()["status"] == "disabled" + data = resp.json() + assert data["status"] == "disabled" + assert data["symbol"] == "005930" def test_delete_user_other_portfolio_returns_403() -> None: diff --git a/tests/test_v1_portfolios_list.py b/tests/test_v1_portfolios_list.py index 39830c3..2b0ca20 100644 --- a/tests/test_v1_portfolios_list.py +++ b/tests/test_v1_portfolios_list.py @@ -142,6 +142,7 @@ def test_get_service_token_daemon_bound_returns_own_portfolios() -> None: data = resp.json() assert data["total"] == 1 assert data["portfolios"][0]["portfolio_id"] == "p-001" + assert data["portfolios"][0]["symbol"] == "005930" def test_get_service_token_no_daemon_returns_403() -> None: @@ -272,3 +273,4 @@ def test_get_response_has_asset_id_field() -> None: assert resp.status_code == 200 item = resp.json()["portfolios"][0] assert item["asset_id"] == _ASSET_UUID + assert item["symbol"] == "005930"