# 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로 분리 가능.