feat: add portfolio item symbol to v1 responses
This commit is contained in:
+46
@@ -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 범위와 직접 관련은 없고 기능 실패는 없다.
|
||||
@@ -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 표기가 필요할 수 있다.
|
||||
@@ -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` 네 경로를 한 턴에 모두 갱신.
|
||||
@@ -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로 분리 가능.
|
||||
@@ -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/) |
|
||||
|
||||
Reference in New Issue
Block a user