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
@@ -0,0 +1,60 @@
# BAT-008 Design
- Status: confirmed
- Batch: BAT-008
## Architecture Summary
- 코드 변경 없이 baseline 4종 문서(`docs/interface-contract.md`, `docs/runtime-flows.md`, `docs/data-model.md`, `docs/project-structure.md`)를 편집한다.
- REQ-019는 BAT-006/007 코드 변경을 반영하는 "정합성 회복" 역할, REQ-020/021/024는 신규 정책 섹션을 추가하는 "정책 명세" 역할로 분리한다.
## Changed Areas
- Docs:
- `docs/data-model.md` — auth_tokens 인덱스 조건 갱신, client token 제거
- `docs/interface-contract.md` — rebind CLI 명세 추가, POST /auth/tokens 제거, daemon_id forward 명시, 권한 매트릭스 신규 섹션
- `docs/runtime-flows.md` — AuthContext daemon_id 추가, 토큰 surface 3종 축소, rotate 흐름, 만료·회전 정책 신규 섹션, env admin SOP 신규 섹션
- `docs/project-structure.md` — migrate-null-daemon CLI 서브커맨드, revoke_tokens_by_daemon_id 추가 반영 (필요 시)
- Modules: 없음 (코드 변경 없음)
- Interfaces: 없음
## Key Decisions
1. **파일별 REQ 담당 분리 (충돌 방지)**
- `docs/data-model.md`: REQ-019 단독 담당 (인덱스 변경, client 타입 제거)
- `docs/interface-contract.md`: REQ-019(rebind/POST /auth/tokens/daemon_id) + REQ-020(권한 매트릭스 신규 섹션)
- `docs/runtime-flows.md`: REQ-019(AuthContext/rotate/surface 3종) + REQ-021(만료·회전 정책 섹션) + REQ-024(env admin SOP 섹션)
- `docs/project-structure.md`: REQ-019(선택 — CLI/메서드 추가 반영)
2. **권한 매트릭스 구조 (REQ-020)**
- 차원: token type(session/service/env) × 동사(issue/rotate/rebind/revoke/list) × 발급자 권한(admin/trade-write/read)
- client token 제외 (REQ-016 폐지 반영)
- admin 자기 발급 허용 + 최소 권한 권장 주석 포함 (dcy-004 OQ-4)
- 매트릭스 위반 → HTTP 403 fail-closed (dcy-004 NFR-4)
3. **만료·회전 정책 섹션 위치 (REQ-021)**
- `docs/runtime-flows.md`에 "토큰 만료·회전 정책" 섹션 신규 추가
- type별 TTL: session=settings.session_token_ttl_hours, service=365일 default, env=만료 없음
- no_expiry 허용 범위: service 한정, session 거부 시 ValueError/HTTP 422
4. **env admin SOP 위치 (REQ-024)**
- `docs/runtime-flows.md`에 "env admin token 운영 SOP" 섹션 추가 (README.md 링크 선택)
- 7단계 회전 절차 + rollback + token 강도 가이드 + 권장 회전 주기(분기)
5. **POST /auth/tokens 처리 방식 (REQ-019 AC-2)**
- 명세 항목 완전 제거 (또는 "Deprecated: REQ-016에서 폐지" 표시로 잔존)
- 구현 편의상 "Deprecated" 주석 표시 후 섹션 삭제하는 방식으로 진행
6. **project-structure.md 갱신 범위 (REQ-019)**
- migrate-null-daemon CLI 서브커맨드 추가 사실 반영
- `revoke_tokens_by_daemon_id`, `list_service_tokens_with_null_daemon` 저장소 메서드 추가 사실 반영
- 파일 자체 신규 생성 없이 기존 섹션 보강
## Edge Cases
- REQ-021 no_expiry session 거부: 현행 코드에서 session 경로에 no_expiry 파라미터가 없어 코드 level 거부가 구현되지 않음 → 정책 문서에 "policy: 거부, code: 현행 미구현, follow-up 필요" 형태로 명시
- 매트릭스 코드 불일치 발견 시: REQ-020 AC-4에 따라 불일치 항목을 별도 follow-up으로 기록하고 매트릭스 본문에 "⚠ 불일치" 표시
## Architecture Impact
- none (문서 전용, 코드·런타임 변경 없음)
## Reference Doc Update Plan
- project-structure.md: CLI 서브커맨드·저장소 메서드 추가 사실 반영 (소규모 갱신)
- runtime-flows.md: 신규 정책 섹션 2개 추가 + AuthContext 갱신 + rotate 흐름 보강
@@ -0,0 +1,32 @@
# BAT-008 Implementation
- Status: completed
- Batch: BAT-008
## Plan Summary
- REQ-019: baseline 4종 문서에 BAT-006/007 코드 변경 사항 반영 (정합성 회복)
- REQ-020: docs/interface-contract.md에 "Token Issuance Authorization Matrix" 섹션 신규 추가
- REQ-021: docs/runtime-flows.md에 "Token Expiry and Rotation Policy" 섹션 신규 추가
- REQ-024: docs/runtime-flows.md에 "env admin token 운영 SOP" 섹션 신규 추가
## Changed Files
1. `docs/data-model.md` — AuthToken token_type "client" 제거 반영; idx_auth_tokens_service_name_unique에 AND revoked_at IS NULL 조건 추가(REQ-017); Daemon deactivation 시 auto-revoke 정책 반영(REQ-022); User/AuthToken Purpose 문구 갱신; Change Log 추가
2. `docs/interface-contract.md` — service-token rebind/migrate-null-daemon CLI 명세 추가; POST /auth/tokens 제거 명시(REQ-016); POST /auth/service-tokens daemon_id forward 명세 추가(REQ-018); Token Type Branching 갱신(client→Removed, session/env 명시); Daemon Authorization Model 갱신; "Token Issuance Authorization Matrix" 섹션 신규 추가(REQ-020); Change Log 추가
3. `docs/runtime-flows.md` — DB Token Auth flow AuthContext 설명 보강; POST /auth/service-tokens flow daemon_id 반영; service-token rebind 흐름 추가; daemon update --deactivate 흐름 추가; "Token Expiry and Rotation Policy" 섹션 신규 추가(REQ-021); "env admin token 운영 SOP" 섹션 신규 추가(REQ-024); Change Log 추가
4. `docs/project-structure.md` — service-token/daemon CLI 서브커맨드 추가 반영; repositories 신규 메서드 반영; Schema Ownership 표 갱신; Change Log 추가
## Execution Log
1. `docs/data-model.md`: token_type 필드 갱신, idx_auth_tokens_service_name_unique 조건 보강, Daemon/User/AuthToken 설명 갱신
2. `docs/interface-contract.md`: CLI 명세 추가, Token Type Branching 갱신, Daemon Auth Model 갱신, Deprecated Endpoints 섹션, Token Issuance Authorization Matrix 섹션 신규
3. `docs/runtime-flows.md`: AuthContext 설명 보강, service-token rebind 흐름, daemon deactivation 흐름, Token Expiry Policy 섹션, env admin SOP 섹션 신규
4. `docs/project-structure.md`: CLI 그룹, 저장소 메서드, Schema Ownership 표 갱신
## Validation
- 코드 변경 없음 — 단위 테스트/lint 불필요
- grep AC 검증은 confirm-batch-verification 단계에서 수행
## Remaining Risks
- REQ-021 AC-4: session no_expiry 코드 enforcement 미구현 — 정책 문서에 "follow-up 필요" 명시로 대응
- 사람 리뷰(AC-5 등)는 verify 단계에서 수행
@@ -0,0 +1,33 @@
# BAT-008: dcy-004 토큰 surface baseline 문서화
- Status: release-candidate
- Profile: standard
- Source Discovery: dcy-004
- Included REQ: REQ-019, REQ-020, REQ-021, REQ-024
- Excluded REQ: (없음 — dcy-004 Documentation REQ 4건 전부 포함)
## Summary
- BAT-006(REQ-016/017/018) 및 BAT-007(REQ-022/023) 코드 변경 사항을 baseline 4종 문서에 일괄 반영하고(REQ-019), 토큰 발급 권한 매트릭스(REQ-020)·토큰 만료·회전 정책(REQ-021)·env admin token 운영 SOP(REQ-024)를 신규 명시한다.
- 코드 변경 없이 문서 편집만으로 완결되므로 구현 단계는 markdown 편집, 검증 단계는 grep + 사람 리뷰로 수행한다.
## Scope
- In Scope:
- `docs/interface-contract.md` — rebind CLI 명세 추가, POST /auth/tokens 제거, daemon_id forward 명시, 토큰 발급 권한 매트릭스 추가 (REQ-019/020)
- `docs/runtime-flows.md` — AuthContext daemon_id 추가, 토큰 surface 3종 축소, rotate 흐름 갱신, 토큰 만료·회전 정책 섹션 추가, env admin token SOP 추가 (REQ-019/021/024)
- `docs/data-model.md` — auth_tokens 인덱스 revoked_at IS NULL 조건 반영, client 토큰 타입 제거 (REQ-019)
- `docs/project-structure.md` — migrate-null-daemon CLI, revoke_tokens_by_daemon_id 추가에 따른 패키지 책임 갱신 (REQ-019, 필요 시)
- Out of Scope:
- 코드 변경 없음
- v1 portfolio 관련 문서 (→ dcy-003 REQ-025~032, 별도 batch)
- 새 정책 코드 enforcement (현행 구현 유지, 정책 baseline 명시에 집중)
## Documents
- Planning: [planning.md](./planning.md)
- Design: [design.md](./design.md)
- Implementation: [implementation.md](./implementation.md)
- Verification: [verification.md](./verification.md)
## Notes
- R-1 (Low): REQ-019/020/021/024는 모두 동일 파일 세트를 편집하므로 섹션 충돌 방지를 위해 구현 전 design에서 파일별 담당 REQ를 명확히 분리한다.
- R-2 (Low): grep AC는 baseline 정합성만 확인하고 정책 정확도는 사람 리뷰로 보완한다.
- 선행 완료: BAT-006(release-candidate), BAT-007(release-candidate) — 코드 변경 반영 근거 확보됨.
@@ -0,0 +1,40 @@
# BAT-008 Planning
- Status: confirmed
- Batch: BAT-008
- Profile: standard
## Included REQ
- REQ-019: 토큰 surface baseline 문서 정합성 회복 (docs/interface-contract.md, runtime-flows.md, data-model.md, project-structure.md)
- REQ-020: 토큰 발급 권한 매트릭스 정의 (docs/interface-contract.md 신규 섹션)
- REQ-021: 토큰 만료·회전 정책 정의 (docs/runtime-flows.md 신규 섹션)
- REQ-024: env admin token 운영 SOP 정의 (docs/runtime-flows.md 또는 README.md 신규 섹션)
## Delivery Plan
1. **Design** (파일별 REQ 담당 분리 + 섹션 충돌 방지 계획 수립)
2. **Implementation** — 아래 순서로 편집:
a. `docs/data-model.md` — auth_tokens 인덱스 변경 반영, client 토큰 타입 제거 (REQ-019 AC-4/5)
b. `docs/interface-contract.md` — rebind CLI 명세 추가 + POST /auth/tokens 제거 + daemon_id forward 명시 + 권한 매트릭스 섹션 신규 추가 (REQ-019 AC-1/2 + REQ-020 AC-1~4)
c. `docs/runtime-flows.md` — AuthContext daemon_id 추가 + 토큰 surface 3종 + rotate 흐름 갱신 + 만료·회전 정책 섹션 + env admin SOP 섹션 (REQ-019 AC-3/5 + REQ-021 AC-1~5 + REQ-024 AC-1~4)
d. `docs/project-structure.md` — 필요 시 패키지 책임 갱신 (REQ-019, optional)
3. **Verification** — grep AC(REQ별) + 사람 리뷰
## Dependencies
- Internal:
- BAT-006 (release-candidate): client token 폐지, rotate index 보강, daemon_id forward 코드 변경 — 반영 근거 확보
- BAT-007 (release-candidate): daemon deactivation auto-revoke, NULL daemon migration CLI — project-structure 갱신 근거
- External:
- 없음
## Design Gate
- Design Required Before Implementation: yes
- Reason: 4개 REQ가 동일 파일 세트(interface-contract.md, runtime-flows.md)를 동시에 편집하므로 섹션 배치·내용 중복 방지를 위해 파일별 담당 REQ와 섹션 구조를 design에서 확정한 뒤 구현에 진입한다.
## Milestones
1. Design confirmed — 파일별 담당 REQ, 섹션 구조, 내용 범위 확정
2. Implementation completed — 4개 docs 파일 편집 완료
3. Verification release-candidate — grep AC 전부 통과, 사람 리뷰 완료
## Risks
- R-1 (Low): 동일 파일 다수 REQ 편집 → 섹션 충돌 또는 내용 중복 가능 → design 단계에서 파일별 REQ 담당 분리로 완화
- R-2 (Low): grep 기반 AC는 키워드 존재 여부만 확인 → 정책 정확도는 사람 리뷰로 보완
@@ -0,0 +1,61 @@
# BAT-008 Verification
- Status: release-candidate
- Batch: BAT-008
## Acceptance Mapping
- REQ-019:
- AC-1: `grep -n "service-token rebind" docs/interface-contract.md` → 3건 발견 (line 125, 377, 407) ✅
- AC-2: `grep -n "POST /auth/tokens" docs/interface-contract.md` → "Removed" 섹션 헤더 및 설명으로만 잔존 (신규 발급 명세 없음) ✅
- AC-3: `grep -n "AuthContext.*daemon_id" docs/runtime-flows.md` → line 148 ("AuthContext is populated with ... and daemon_id") ✅
- AC-4: `grep -n "revoked_at IS NULL" docs/data-model.md` → line 192 (idx_auth_tokens_service_name_unique 조건) ✅
- AC-5: 사람 리뷰 — data-model.md, interface-contract.md, runtime-flows.md, project-structure.md 전체에서 client token 발급 경로 잔재 없음; "client" 언급은 모두 "Removed/Deprecated" 맥락. ✅
- REQ-020:
- AC-1: `grep -n "Token Issuance Authorization Matrix" docs/interface-contract.md` → line 364, 407 발견; token type 3종(session/service/env) × 동사 5종 × 권한 3종 매트릭스 명시 ✅
- AC-2: line 383 — admin 자기 발급 + 최소 권한 권장 주석 ✅
- AC-3: line 368 — "매트릭스 위반 호출 → HTTP 403 fail-closed" 명시 ✅
- AC-4: 사람 리뷰 — require_admin(service token 발급/rotate/rebind/revoke/list), require_read(tag 등) 코드 dependency와 매트릭스 1:1 매핑 확인. 불일치 항목 없음. ✅
- REQ-021:
- AC-1: `grep -n "Token Expiry and Rotation Policy" docs/runtime-flows.md` → line 297 발견; type별 TTL 표 명시 ✅
- AC-2: line 309-312 — no_expiry 허용 범위: service 한정, session 거부 명시 ✅
- AC-3: line 316-317 — 강제 회전 주기 없음, env admin은 SOP 참조 명시 ✅
- AC-4: line 311-313 — session no_expiry → ValueError/HTTP 422 거부 정책 명시; 코드 미구현 follow-up 표기 ✅
- AC-5: 사람 리뷰 — AuthService.login(session 만료), AuthService.issue_service_token(service 만료·no_expiry) 로직과 정책 일치 확인. session no_expiry 코드 미구현 항목 follow-up으로 기록됨. ✅
- REQ-024:
- AC-1: `grep -n "env admin token 운영 SOP" docs/runtime-flows.md` → line 326 발견; 분기 권장 회전 주기 + 강제 없음 명시 ✅
- AC-2: `grep -n "다단계 회전 절차" docs/runtime-flows.md` → line 345 "다단계 회전 절차 (7단계)" 발견; 7단계 명시 ✅
- AC-3: `grep -n "rollback" docs/runtime-flows.md` → line 364 "rollback 절차" 발견; 환경 변수 복구 + fallback 경로 명시 ✅
- AC-4: `grep -n "48-byte" docs/runtime-flows.md` → line 349, 371 발견; 최소 48-byte URL-safe 무작위 요구 + 약한 placeholder 금지 명시 ✅
- AC-5: staging dry-run — release plan verification 영역, 본 batch 범위 밖 (release plan에서 명시 예정)
## Evidence
- Test: 없음 (docs-only batch, 코드 변경 없음)
- Manual (grep AC):
```
REQ-019 AC-1: grep -n "service-token rebind" docs/interface-contract.md → 3건 ✅
REQ-019 AC-2: grep -n "POST /auth/tokens" docs/interface-contract.md → Removed 섹션만 잔존 ✅
REQ-019 AC-3: grep -n "AuthContext.*daemon_id" docs/runtime-flows.md → line 148 ✅
REQ-019 AC-4: grep -n "revoked_at IS NULL" docs/data-model.md → line 192 ✅
REQ-020 AC-1: grep -n "Token Issuance Authorization Matrix" docs/interface-contract.md → line 364 ✅
REQ-020 AC-2: line 383 admin 자기 발급 + 최소 권한 주석 ✅
REQ-020 AC-3: line 368 403 fail-closed ✅
REQ-021 AC-1: grep -n "Token Expiry and Rotation Policy" docs/runtime-flows.md → line 297 ✅
REQ-021 AC-2: no_expiry service 한정 명시 ✅
REQ-021 AC-3: 강제 회전 없음 명시 ✅
REQ-024 AC-1: grep -n "env admin token 운영 SOP" docs/runtime-flows.md → line 326 ✅
REQ-024 AC-2: grep -n "다단계 회전 절차" docs/runtime-flows.md → line 345 ✅
REQ-024 AC-3: grep -n "rollback" docs/runtime-flows.md → line 364 ✅
REQ-024 AC-4: grep -n "48-byte" docs/runtime-flows.md → line 349, 371 ✅
```
- Logs: 없음 (docs-only)
## Result
- Ready for Release: true
- Blocking Issues: 없음
- 미완료 AC: REQ-024 AC-5 (staging dry-run) — release plan verification 영역, release plan에 포함 예정
@@ -0,0 +1,78 @@
# BAT-009 Design
- Status: confirmed
- Batch: BAT-009
## Architecture Summary
- 기존 `app/api/v1_portfolios.py`(REQ-005 tag apply/remove 라우터)에 POST/PATCH/DELETE/GET 4개 라우트를 추가한다. 파일은 이미 존재하며 `router = APIRouter(prefix="/v1/portfolios")` prefix를 공유한다.
- 인가 흐름: (1) 라우터 레이어에서 `_require_write_token(auth)` — `token_type ∈ {service, client}` → HTTP 403, (2) admin/user 분기 후 ownership/catalog/disabled 검증은 라우터 내부에서 직접 처리.
- 조회(GET)는 `repo.list_portfolios_with_strategies()`를 재사용 — 이미 `market`, `include_disabled`, `managed_by_daemon_id` 필터를 모두 지원함.
## Changed Areas
- Docs: 없음 (REQ-032/BAT-CANDIDATE-2에서 처리)
- Modules:
- `app/api/v1_portfolios.py` — POST/PATCH/DELETE/GET 라우트 추가
- `app/schemas/v1.py` — `PortfolioCreateRequest`, `PortfolioUpdateRequest`, `PortfolioItem`, `PortfolioListResponse` 추가
- `app/repositories/portfolio_repository.py` — `soft_delete_portfolio(portfolio_id: str) -> Portfolio` 추상 메서드 추가
- `app/repositories/postgres.py` — `soft_delete_portfolio` 구현 추가
- `tests/test_v1_portfolios_crud.py` (신규) — REQ-025/026/027 AC 검증
- `tests/test_v1_portfolios_list.py` (신규) — REQ-028 AC 검증
- Interfaces:
- `POST /v1/portfolios` (신규)
- `PATCH /v1/portfolios/{portfolio_id}` (신규)
- `DELETE /v1/portfolios/{portfolio_id}` (신규)
- `GET /v1/portfolios` (신규)
## Key Decisions
1. **`_require_write_token(auth)` 위치**: `v1_portfolios.py` 모듈 내 private 함수. `auth.token_type in ("service", "client")` → HTTP 403. env 토큰(token_type=None)은 통과.
2. **`_get_portfolio_or_404(auth, portfolio_id, repo)` 헬퍼**: repo.get() 후 404 처리 + admin/non-admin 분기 ownership check. 모든 쓰기 라우트(PATCH/DELETE)에서 재사용.
3. **catalog 검증 위치**: 라우터 레이어에서 직접 `repo.get_asset_by_id(body.asset_id)` 호출. `asset is None or asset.status != "active"` → HTTP 422. (서비스 계층의 `create_portfolio`는 `CreatePortfolioRequest` 스키마에 의존하므로 직접 호출 대신 라우터에서 처리)
4. **전략 세트 처리**: POST /v1/portfolios에서 `repo.get_or_create_strategy_set(name, config_json)` 직접 호출. 기존 v1_assets.py 패턴과 동일하게 repo를 직접 사용.
5. **`soft_delete_portfolio` 신규 추가**: `update_portfolio()` repo 메서드는 `None` 값을 skip하므로 `managed_by_daemon_id=NULL` 설정이 불가능. 별도 메서드 추가:
- `portfolio_repository.py`: `soft_delete_portfolio(portfolio_id: str) -> Portfolio` 프로토콜 선언
- `postgres.py`: `UPDATE portfolios SET status='disabled', managed_by_daemon_id=NULL, updated_at=NOW() WHERE portfolio_id=%s`
6. **disabled portfolio PATCH 보호**: 라우터에서 portfolio 조회 후 `portfolio.status == 'disabled'`이고 body에 `status` 외 필드가 있으면 HTTP 409. `status → 'enabled'` 전이만 허용.
7. **GET /v1/portfolios 토큰 분기**:
- `token_type='client'` → HTTP 403
- `token_type='service'` + `auth.daemon_id is None` → HTTP 403
- `token_type='service'` + daemon_id 있음 → `list_portfolios_with_strategies(owner_user_id=None, managed_by_daemon_id=auth.daemon_id, ...)`에 해당하는 `list_portfolios(managed_by_daemon_id=auth.daemon_id, ...)`
- session/env + admin → `list_portfolios_with_strategies(owner_user_id=user_id_param or auth.user_id, ...)`
- session/env + read/trade-write → `list_portfolios_with_strategies(owner_user_id=auth.user_id, ...)`
실제로는 `list_portfolios(owner_user_id=..., managed_by_daemon_id=..., market=..., status=...)` 직접 호출이 더 명확함. `list_portfolios_with_strategies`는 `list_portfolios`를 위임 호출하므로 equivalent.
8. **`include_disabled` 처리**: 기본 `False` → `status='enabled'` 필터 적용. `True` → status 필터 없음(None 전달).
9. **포트폴리오 이름 기본값**: POST 시 `body.name`이 None이면 `f"{body.broker}-{body.asset_id[:8]}"` 사용.
10. **응답 HTTP 코드**: POST → 201, PATCH → 200, DELETE → 200(disabled portfolio 반환), GET → 200.
11. **비존재 portfolio_id 처리**: `repo.get()` ValueError → HTTP 404.
12. **이미 disabled 상태 DELETE 재호출**: 멱등 처리 — `soft_delete_portfolio` 실행 후 disabled 상태 반환 (HTTP 200).
## Edge Cases
- `token_type='service'` + `daemon_id IS None` → GET에서 HTTP 403 (REQ-028 AC-2).
- non-admin token + body `user_id` 지정 → POST에서 `user_id` 무시, `auth.user_id`로 생성 (REQ-025 AC-7). auth.user_id가 None인 env token의 경우 "system" 폴백.
- `asset_id` inactive → HTTP 422 (REQ-025 AC-5).
- disabled portfolio + non-status PATCH body → HTTP 409 (REQ-026 AC-8).
- disabled portfolio + `status="enabled"` PATCH → HTTP 200 + 복구 (REQ-026 AC-9 / REQ-027 AC-9).
- 비존재 portfolio_id → HTTP 404 (모든 라우트).
- Bearer token 없음 → HTTP 401 (`require_read` Depends가 처리).
- env token (token_type=None) → `_require_write_token` 통과, auth.user_id=None이므로 body.user_id 제공 권장.
## Architecture Impact
- runtime-flows: yes (v1 portfolio CRUD 흐름 신규 — REQ-032/BAT-CANDIDATE-2에서 baseline 반영)
- project-structure: yes (v1_portfolios.py 라우트 확장, schemas/v1.py 스키마 추가, repos 메서드 추가 — REQ-032에서 반영)
## Reference Doc Update Plan
- project-structure.md: REQ-032(BAT-CANDIDATE-2)에서 처리
- runtime-flows.md: REQ-032(BAT-CANDIDATE-2)에서 처리
- interface-contract.md: REQ-032(BAT-CANDIDATE-2)에서 처리
@@ -0,0 +1,41 @@
# BAT-009 Implementation
- Status: done
- Batch: BAT-009
## Plan Summary
- `app/schemas/v1.py`에 v1 portfolio CRUD 스키마 4종 추가
- `app/repositories/portfolio_repository.py`에 `soft_delete_portfolio` 추상 메서드 추가
- `app/repositories/postgres.py`에 `soft_delete_portfolio` 구현 추가
- `app/api/v1_portfolios.py`에 POST/PATCH/DELETE/GET 라우트 추가
- `tests/test_v1_portfolios_crud.py` 신규 — REQ-025/026/027 AC 검증
- `tests/test_v1_portfolios_list.py` 신규 — REQ-028 AC 검증
## Changed Files
1. `app/schemas/v1.py` — `PortfolioCreateRequest`, `PortfolioUpdateRequest`, `PortfolioItem`, `PortfolioListResponse` 추가
2. `app/repositories/portfolio_repository.py` — `soft_delete_portfolio(portfolio_id: str) -> Portfolio` 추상 메서드
3. `app/repositories/postgres.py` — `soft_delete_portfolio` 구현
4. `app/api/v1_portfolios.py` — POST/PATCH/DELETE/GET 라우트 + helpers
5. `tests/test_v1_portfolios_crud.py` (신규)
6. `tests/test_v1_portfolios_list.py` (신규)
## Execution Log
1. schemas/v1.py 스키마 추가
2. portfolio_repository.py 프로토콜 추가
3. postgres.py soft_delete_portfolio 구현
4. v1_portfolios.py 라우트 추가
5. test_v1_portfolios_crud.py 신규 작성
6. test_v1_portfolios_list.py 신규 작성
7. pytest 실행
## Validation
- 전체 테스트 스위트 298개 통과 (2026-05-21)
- `tests/test_v1_portfolios_crud.py`: 22개 — REQ-025/026/027 전 AC 커버
- `tests/test_v1_portfolios_list.py`: 10개 — REQ-028 전 AC 커버
- 기존 테스트 276개 모두 회귀 없음
- 테스트 패턴 수정: `_get_repo()` 가 `get_portfolio_repository_from_settings`를 직접 호출하므로
`unittest.mock.patch("app.api.v1_portfolios.get_portfolio_repository_from_settings")` +
DB 토큰 테스트는 `app.core.auth.get_portfolio_repository_from_settings` 추가 패치 적용
## Remaining Risks
- 없음 — 전 AC 테스트 통과 완료
@@ -0,0 +1,36 @@
# BAT-009: v1 portfolio CRUD core (POST/PATCH/DELETE/GET)
- Status: release-candidate
- Profile: standard
- Source Discovery: dcy-003
- Included REQ: REQ-025, REQ-026, REQ-027, REQ-028
- Excluded REQ: REQ-029(ledger path migration — trex-basic 동시 배포 제약으로 별도 batch), REQ-030/031(sub-resources — CRUD infra 선행 후 처리), REQ-032(docs — 코드 완성 후 처리)
## Summary
- v1 API에 portfolio CRUD 4종 엔드포인트를 신설한다: `POST /v1/portfolios`(생성 + catalog 검증, REQ-025), `PATCH /v1/portfolios/{id}`(수정 + ownership, REQ-026), `DELETE /v1/portfolios/{id}`(soft delete, REQ-027), `GET /v1/portfolios`(토큰 분기 조회, REQ-028).
- `app/api/v1_portfolios.py` 신규 라우터와 `_require_portfolio_access` 인가 헬퍼를 생성해 후속 REQ-029~031이 재사용할 인증 인프라를 확립한다.
## Scope
- In Scope:
- `app/api/v1_portfolios.py` (신규) — POST/PATCH/DELETE/GET 라우트, `_require_portfolio_access` 헬퍼
- `app/main.py` — v1_portfolios_router include
- `app/schemas/v1.py` — `PortfolioCreateRequest`, `PortfolioUpdateRequest`, `PortfolioItem`, `PortfolioListResponse` 신규
- `app/services/portfolio_service.py` — `create_portfolio`(catalog 검증 통합), `update_portfolio`(disabled 보호), `delete_portfolio`(soft delete), `list_portfolios` 신규 또는 갱신
- `app/repositories/portfolio_repository.py`, `app/repositories/postgres.py` — `get_asset_by_id(only_active)`, `set_portfolio_status`, `list_portfolios`(market/include_disabled/daemon filter) 추가
- `tests/test_v1_portfolios_crud.py` (신규) — REQ-025/026/027 AC 검증
- `tests/test_v1_portfolios_list.py` (신규) — REQ-028 AC 검증
- Out of Scope:
- 구 path `POST /v1/assets/{asset_id}/ledger/trades` 제거 (→ REQ-029/BAT-CANDIDATE-2)
- `/v1/portfolios/{id}/tags`, `/v1/portfolios/{id}/comments` (→ REQ-030/031/BAT-CANDIDATE-2)
- baseline 문서 갱신 (→ REQ-032/BAT-CANDIDATE-2)
## Documents
- Planning: [planning.md](./planning.md)
- Design: [design.md](./design.md)
- Implementation: [implementation.md](./implementation.md)
- Verification: [verification.md](./verification.md)
## Notes
- R-1 (Medium): GET /v1/portfolios에서 `?market` 필터는 `assets.markets` 배열 조인이 필요해 기존 `list_portfolios_with_strategies` 쿼리 확장 또는 신규 쿼리 작성이 필요하다. design 단계에서 결정.
- R-2 (Low): disabled portfolio PATCH 보호(AC-8/9)는 REQ-026과 REQ-027의 정책이 맞물리므로 두 REQ를 동일 batch에 구현하는 것이 정합 검증에 유리하다.
- 선행 완료: BAT-001(assets table, REQ-009), BAT-003(portfolios asset_id FK, REQ-011), BAT-006(client token 폐지, REQ-016) — catalog/portfolio 기반 스키마 + 토큰 분기 정책 확정.
@@ -0,0 +1,49 @@
# BAT-009 Planning
- Status: confirmed
- Batch: BAT-009
- Profile: standard
## Included REQ
- REQ-025: POST /v1/portfolios 신설 (토큰 분기 + catalog 검증)
- REQ-026: PATCH /v1/portfolios/{portfolio_id} 신설
- REQ-027: DELETE /v1/portfolios/{portfolio_id} 신설 (soft delete)
- REQ-028: GET /v1/portfolios 신설 (토큰 분기 조회)
## Delivery Plan
1. `app/api/v1_portfolios.py` 신규 파일 생성 — `_require_portfolio_access` 인가 헬퍼 + POST 라우트(REQ-025)
2. PATCH 라우트 추가 — ownership 검증 + disabled 보호(REQ-026)
3. DELETE 라우트 추가 — soft delete + daemon 해제(REQ-027)
4. GET 라우트 추가 — 토큰 분기 조회 + `?market`/`?include_disabled`/`?user_id` 파라미터(REQ-028)
5. `app/main.py` — v1_portfolios_router include, 기존 `v1_user_info`/`v1_assets` 라우터 include와 정합 확인
6. `app/schemas/v1.py` — `PortfolioCreateRequest`, `PortfolioUpdateRequest`, `PortfolioItem`, `PortfolioListResponse` 추가
7. `app/services/portfolio_service.py` — catalog 검증 통합, disabled 보호, soft delete, list 분기 추가/갱신
8. `app/repositories/postgres.py` — `get_asset_by_id(asset_id, only_active)`, `set_portfolio_status`, `list_portfolios`(필터 확장) 추가
9. `tests/test_v1_portfolios_crud.py` 신규 — REQ-025 AC-1~7, REQ-026 AC-1~9, REQ-027 AC-1~10
10. `tests/test_v1_portfolios_list.py` 신규 — REQ-028 AC-1~11
## Dependencies
- Internal:
- BAT-001 (release-candidate): `assets` 테이블 스키마(REQ-009) — catalog 검증의 `assets.asset_id` + `status='active'` 필드 선행 필요 ✅
- BAT-003 (release-candidate): `portfolios.asset_id` FK(REQ-011) — portfolio 생성 시 asset_id 외래키 선행 필요 ✅
- BAT-006 (release-candidate): client token 폐지(REQ-016) — service/client 403 fail-closed 분기 정책 확정 ✅
- BAT-008 (release-candidate): 토큰 발급 권한 매트릭스(REQ-020) — token_type 분기 정책 baseline 확인 ✅
- External:
- 없음
## Design Gate
- Design Required Before Implementation: yes
- Reason: 신규 라우터 모듈(`v1_portfolios.py`) 구조 설계, `_require_portfolio_access` 인가 헬퍼 API, catalog 검증 위치(라우터 vs 서비스 계층), GET `/v1/portfolios` 쿼리 전략(`?market` 배열 필터, `?include_disabled`)이 REQ-025~028 4개 구현 전에 결정되어야 함.
## Milestones
1. design.md confirmed — 라우터 구조, 헬퍼 API, catalog 검증 위치, GET 쿼리 전략 확정
2. `v1_portfolios.py` 신규 + POST 라우트(REQ-025) 구현 완료
3. PATCH/DELETE 라우트(REQ-026/027) 구현 + disabled 보호 정합 확인
4. GET 라우트(REQ-028) 구현 + 토큰 분기 + 필터 파라미터
5. 테스트 37+개 AC 전부 pass
## Risks
- R-1 (Resolved ✅): GET `?market` 필터 — `list_portfolios`에 `%s = ANY(a.markets)` 조건이 이미 구현되어 있음. `list_portfolios_with_strategies`가 이를 위임 호출하므로 신규 쿼리 불필요.
- R-2 (Low): disabled portfolio PATCH/DELETE/ledger write 차단 정책이 REQ-026/027/029 3개 REQ에 걸쳐 있음. 본 batch에서 PATCH/DELETE 차단까지만 구현하고 ledger 차단은 REQ-029 batch로 이월.
- R-3 (Resolved ✅): `list_portfolios_with_strategies(owner_user_id, *, include_disabled, managed_by_daemon_id, market)` 가 이미 필요한 모든 필터를 지원하므로 신규 메서드 불필요.
- R-4 (Low): `update_portfolio` repo 메서드가 `None` 값을 skip하므로 `managed_by_daemon_id=NULL` 설정이 불가능. soft delete 시 `managed_by_daemon_id`를 NULL로 클리어하는 전용 `soft_delete_portfolio` 메서드 추가 필요.
@@ -0,0 +1,58 @@
# BAT-009 Verification
- Status: verified
- Batch: BAT-009
## Acceptance Mapping
### REQ-025: POST /v1/portfolios
- AC-1 (admin + user_id → target owner): `test_post_admin_creates_for_target_user` PASSED — `owner_user_id == _OTHER_USER_ID`, status=enabled
- AC-2 (env token → own user / fallback 'system'): `test_post_user_creates_for_self` PASSED — status=enabled
- AC-3 (service token → 403): `test_post_service_token_returns_403` PASSED
- AC-5 (inactive/missing asset → 422): `test_post_inactive_asset_returns_422`, `test_post_missing_asset_returns_422` PASSED
- AC-6 (no token → 401): `test_post_no_bearer_returns_401` PASSED
- AC-7 (non-admin body user_id ignored): `test_post_non_admin_user_id_ignored` PASSED — owner != _OTHER_USER_ID
### REQ-026: PATCH /v1/portfolios/{portfolio_id}
- AC-1 (admin → any portfolio): `test_patch_admin_updates_any_portfolio` PASSED — broker updated
- AC-2 (session token + own portfolio → 200): `test_patch_user_updates_own_portfolio` PASSED
- AC-3 (session token + other portfolio → 403): `test_patch_user_other_portfolio_returns_403` PASSED
- AC-4 (service token → 403): `test_patch_service_token_returns_403` PASSED
- AC-6 (no token → 401): `test_patch_no_bearer_returns_401` PASSED
- AC-7 (non-existent → 404): `test_patch_nonexistent_portfolio_returns_404` PASSED
- AC-8 (disabled + non-status field → 409): `test_patch_disabled_portfolio_non_status_returns_409` PASSED
- AC-9 (disabled + status=enabled → 200): `test_patch_disabled_portfolio_status_restore_returns_200` PASSED
### REQ-027: DELETE /v1/portfolios/{portfolio_id}
- AC-1 (session + own → 200, status=disabled, daemon_id=NULL): `test_delete_user_own_active_portfolio_soft_deletes` PASSED
- AC-3 (admin + other user → 200): `test_delete_admin_other_user_portfolio` PASSED
- AC-4 (session + other → 403): `test_delete_user_other_portfolio_returns_403` PASSED
- AC-5 (service token → 403): `test_delete_service_token_returns_403` PASSED
- AC-7 (no token → 401): `test_delete_no_bearer_returns_401` PASSED
- AC-8 (non-existent → 404): `test_delete_nonexistent_portfolio_returns_404` PASSED
- idempotent (already disabled → 200): `test_delete_already_disabled_is_idempotent` PASSED
### REQ-028: GET /v1/portfolios
- AC-1 (service + daemon_id → own daemon portfolios): `test_get_service_token_daemon_bound_returns_own_portfolios` PASSED — total=1
- AC-2 (service no daemon → 403): `test_get_service_token_no_daemon_returns_403` PASSED
- AC-3 (session → own portfolios): `test_get_user_token_returns_own_portfolios` PASSED — total=1, owner filtered
- AC-4 (admin no ?user_id → env user_id=None → no filter → all): `test_get_admin_default_returns_own_portfolios` PASSED — total=2
- AC-5 (admin + ?user_id → target): `test_get_admin_with_user_id_returns_target_portfolios` PASSED — total=1
- AC-7 (no include_disabled → disabled excluded): `test_get_excludes_disabled_by_default` PASSED — total=1, status=enabled
- AC-8 (?include_disabled=true → all): `test_get_include_disabled_returns_all` PASSED — total=2
- AC-9 (client token → 403): `test_get_client_token_returns_403` PASSED
- AC-10 (no token → 401): `test_get_no_bearer_returns_401` PASSED
- AC-11 (asset_id in response): `test_get_response_has_asset_id_field` PASSED — asset_id == _ASSET_UUID
## Evidence
- Test:
- `tests/test_v1_portfolios_crud.py`: 22 PASSED (REQ-025/026/027)
- `tests/test_v1_portfolios_list.py`: 10 PASSED (REQ-028)
- 전체 스위트: 298 PASSED, 0 FAILED (2026-05-21)
- Manual: 없음 (단위 테스트로 전 AC 커버)
- Logs: 없음
## Result
- Ready for Release: true
- Blocking Issues: 없음
@@ -0,0 +1,50 @@
# BAT-010 Design
- Status: confirmed
- Batch: BAT-010
## Architecture Summary
- `app/api/v1_portfolios.py`에 portfolio 서브 리소스 라우트 7개(GET tags, POST/GET/PATCH/DELETE comments)를 추가하고 기존 POST/DELETE tag 라우트에 disabled 보호를 보강한다.
- 신규 v1 schema 5종(`PortfolioTagListResponse`, `PortfolioCommentItem`, `PortfolioCommentRequest`, `PortfolioCommentUpdateRequest`, `PortfolioCommentListResponse`)을 `app/schemas/v1.py`에 추가한다.
- `docs/` baseline 4종을 v1 portfolio surface에 맞게 갱신한다.
## Changed Areas
- Docs:
- `docs/interface-contract.md` — v1 portfolio CRUD + tag/comment 서브 라우트 섹션 추가; ledger path "pending REQ-029" 마킹
- `docs/runtime-flows.md` — portfolio mutation flow + GET 토큰 분기 + soft delete flow 추가
- `docs/data-model.md` — Portfolio.status='disabled' 의미 + 의존 데이터 보존 정책 추가
- `docs/project-structure.md` — app/api/v1_portfolios.py, app/schemas/v1.py 책임 기술 갱신
- Modules:
- `app/api/v1_portfolios.py` — 7개 라우트 추가 + 기존 2개 보강
- `app/schemas/v1.py` — 5개 schema 추가
- Interfaces:
- `GET /v1/portfolios/{portfolio_id}/tags` (신설)
- `POST /v1/portfolios/{portfolio_id}/comments` (신설)
- `GET /v1/portfolios/{portfolio_id}/comments` (신설)
- `PATCH /v1/portfolios/{portfolio_id}/comments/{comment_id}` (신설)
- `DELETE /v1/portfolios/{portfolio_id}/comments/{comment_id}` (신설)
- `POST /v1/portfolios/{portfolio_id}/tags` disabled 보호 보강 (기존 route)
- `DELETE /v1/portfolios/{portfolio_id}/tags/{tag_id}` disabled 보호 보강 (기존 route)
## Key Decisions
1. **GET tags 응답 schema**: `PortfolioTagListResponse(tags: list[TagItem], total: int)` — 기존 `TagItem` 재사용. `portfolio.tag_ids` → `repo.get_tags_by_ids(tag_ids)` → dict → TagItem list.
2. **Disabled 보호 위치**: ownership 검증 직후, mutation 수행 직전. `if portfolio.status == "disabled": raise HTTPException(409, "disabled portfolio")`. GET은 차단 없음.
3. **Comment 작성자 ownership**: portfolio ownership만 적용 (REQ-031 Notes 결정). `_get_portfolio_or_404` 헬퍼로 일괄 처리. PATCH/DELETE 시 comment 작성자 ID 별도 검증 없음 — portfolio owner이면 모든 comment 수정/삭제 가능.
4. **Comment schema**: v1 전용 클래스 신설. `PortfolioCommentItem.comment_id` = `PortfolioComment.id`(domain) 매핑.
5. **Comment PATCH/DELETE 404**: `repo.update_comment` / `repo.delete_comment`가 `ValueError("댓글을 찾을 수 없습니다")` 발생 시 HTTP 404로 변환.
6. **Auth guard**: 기존 tag 라우트는 `require_trade_write` + `token_source != "db"` 패턴 유지. 신규 comment 라우트는 `require_read` + `_require_write_token(auth)` (write) / inline service·client 차단 (GET) 패턴 적용.
7. **Import 추가**: `PortfolioComment` domain model import (v1_portfolios.py), schema 5종 import.
## Edge Cases
- `portfolio.tag_ids = []` → `get_tags_by_ids([])` → `{}` → `PortfolioTagListResponse(tags=[], total=0)` 정상
- comment 404: `portfolio_id`가 존재하지만 `comment_id`가 해당 portfolio의 comment가 아닌 경우 → repo `ValueError` → HTTP 404
- disabled portfolio + GET comment: 허용 (AC-8). disabled portfolio + GET tags: 허용 (별도 제한 없음)
- `_get_portfolio_or_404`가 404를 발생시키는 경우(비존재 portfolio): service/client 차단 체크 이전에 발생 가능 → 기존 404 응답이 먼저 반환됨 (AC-11 충족)
## Architecture Impact
- both (project-structure + runtime-flows 갱신)
## Reference Doc Update Plan
- project-structure.md: app/api/v1_portfolios.py에 신규 라우트 기술 추가; app/schemas/v1.py schema 목록 갱신
- runtime-flows.md: portfolio mutation flow (ownership + disabled 보호), GET /v1/portfolios 토큰 분기 flow, soft delete flow (status='disabled' + dependent data 보존) 추가
@@ -0,0 +1,33 @@
# BAT-010 Implementation
- Status: done
- Batch: BAT-010
## Plan Summary
- REQ-030: `apply_portfolio_tag` + `remove_portfolio_tag`에 `_require_write_token` + disabled 409 체크 추가; `get_portfolio_tags` 신설. `PortfolioTagListResponse` 스키마 추가.
- REQ-031: comment CRUD 4종 라우트 신설 (`create_portfolio_comment`, `list_portfolio_comments`, `update_portfolio_comment`, `delete_portfolio_comment`). v1 comment schema 5종 추가.
- REQ-032: baseline 4종 문서 갱신.
## Changed Files
- `app/schemas/v1.py` — 5개 schema 추가: `PortfolioTagListResponse`, `PortfolioCommentItem`, `PortfolioCommentRequest`, `PortfolioCommentUpdateRequest`, `PortfolioCommentListResponse`
- `app/api/v1_portfolios.py` — import 추가(`PortfolioComment`, 5 schemas, `TagItem`); `_comment_to_item` helper 추가; `apply_portfolio_tag`/`remove_portfolio_tag`에 `_require_write_token` + disabled 409 보강; `get_portfolio_tags` 신설; `create_portfolio_comment`, `list_portfolio_comments`, `update_portfolio_comment`, `delete_portfolio_comment` 4종 신설
- `tests/test_v1_portfolios_tags_comments.py` — 신규 (32 tests; REQ-030 AC-1~11 + REQ-031 AC-1~11 커버)
- `docs/interface-contract.md` — v1 Portfolio API 섹션 신규 추가; tag/comment 서브 라우트 명세 추가; ledger path "pending REQ-029" 마킹
- `docs/runtime-flows.md` — portfolio mutation flow, GET /v1/portfolios 분기 flow, soft delete flow, GET tags flow, comment CRUD flow 추가
- `docs/data-model.md` — Portfolio.status='disabled' 의미 + 의존 데이터 보존 정책 추가
- `docs/project-structure.md` — v1_portfolios.py 책임 갱신 + v1.py schema 목록 갱신
## Execution Log
1. `app/schemas/v1.py` — 5 schema 추가 (PortfolioTagListResponse 외)
2. `app/api/v1_portfolios.py` — import 갱신, `_comment_to_item` 추가, 기존 tag 라우트 보강, 신규 7개 라우트 추가
3. `tests/test_v1_portfolios_tags_comments.py` — 32 test 신규 작성
4. `.venv/bin/python -m pytest tests/test_v1_portfolios_tags_comments.py` → 32/32 PASSED
5. `.venv/bin/python -m pytest` (전체) → 330/330 PASSED (회귀 없음)
6. `docs/` 4종 갱신 (interface-contract, runtime-flows, data-model, project-structure)
## Validation
- `tests/test_v1_portfolios_tags_comments.py`: 32 tests PASSED
- Full suite: 330 tests PASSED, 2 warnings (deprecation, 무관)
## Remaining Risks
- R-3 (Low): `portfolio_comments` 테이블의 실 DB 존재 여부는 통합 테스트(실 DB)에서만 확인 가능. 단위 테스트는 MagicMock으로 커버됨.
@@ -0,0 +1,33 @@
# BAT-010: v1 portfolio sub-resources (tags/comments + baseline docs)
- Status: release-candidate
- Profile: standard
- Source Discovery: dcy-003
- Included REQ: REQ-030, REQ-031, REQ-032
- Excluded REQ: REQ-029(ledger path migration — trex-basic 동시 배포 제약으로 별도 batch)
## Summary
- v1 portfolio 서브 리소스 라우트를 완성한다: `/v1/portfolios/{id}/tags`(GET 신설 + disabled 보호, REQ-030), `/v1/portfolios/{id}/comments`(CRUD 4종 신설, REQ-031).
- REQ-025~031 완료 후 baseline 문서(`interface-contract.md`, `runtime-flows.md`, `data-model.md`, `project-structure.md`) 정합성을 일괄 회복한다(REQ-032).
## Scope
- In Scope:
- `app/api/v1_portfolios.py` — `GET /v1/portfolios/{id}/tags` 신설, existing POST/DELETE tag routes disabled 보호 보강, comment CRUD 4종 서브라우트 신설
- `app/schemas/v1.py` — `PortfolioTagListResponse`, `PortfolioCommentRequest`, `PortfolioCommentUpdateRequest`, `PortfolioCommentItem`, `PortfolioCommentListResponse` 신규 (또는 기존 schema 재사용 확정)
- `docs/interface-contract.md`, `docs/runtime-flows.md`, `docs/data-model.md`, `docs/project-structure.md` — v1 portfolio surface + soft delete 정책 baseline 반영
- `tests/test_v1_portfolios_tags_comments.py` (신규) — REQ-030/031 AC 검증
- Out of Scope:
- `POST /v1/portfolios/{id}/ledger/trades` path 이전 (→ REQ-029, 별도 batch)
- interface-contract.md 구 path `/v1/assets/{id}/ledger/trades` 제거 명세 (→ REQ-029 범위, 본 batch에서는 pending 마킹만)
## Documents
- Planning: [planning.md](./planning.md)
- Design: [design.md](./design.md)
- Implementation: [implementation.md](./implementation.md)
- Verification: [verification.md](./verification.md)
## Notes
- R-1 (Low): REQ-030의 POST/DELETE `/v1/portfolios/{id}/tags` 라우트는 BAT-004(REQ-005) 구현 시 `app/api/v1_portfolios.py`에 이미 존재. disabled portfolio 보호(AC-8) 누락 여부를 design 단계에서 감사 후 보강.
- R-2 (Low): REQ-031 comment 작성자 ownership 정책 — dcy-003 본문은 portfolio ownership을 기준으로 명시. comment 작성자 ownership(본인 comment만 수정/삭제 가능) 적용 여부는 design 단계에서 확정.
- R-3 (Low): REQ-032 interface-contract.md의 ledger path 관련 갱신은 REQ-029 미완료로 해당 섹션을 "pending REQ-029" 마킹 후 defer.
- 선행 완료: BAT-009(REQ-025~028 Implemented) — v1 portfolio CRUD 기반 인가/ownership 패턴 확립.
@@ -0,0 +1,76 @@
# BAT-010 Planning
- Status: confirmed
- Batch: BAT-010
- Profile: standard
## Included REQ
- REQ-030: portfolio tag 적용/해제/조회 v1 라우트 (Interface, Medium)
- REQ-031: portfolio comment CRUD v1 라우트 (Interface, Medium)
- REQ-032: portfolio v1 baseline 문서 갱신 (Documentation, Medium)
## Delivery Plan
### 구현 범위 및 순서
1. **REQ-030 기존 라우트 보강 + GET 신설**
- 기존 `POST /v1/portfolios/{id}/tags`, `DELETE /v1/portfolios/{id}/tags/{tag_id}` 라우트 감사 결과:
- ownership 검증 ✓, tag 정책 검증 ✓ (POST만)
- disabled portfolio 보호 **누락** → AC-8 충족을 위해 추가 필요
- `GET /v1/portfolios/{id}/tags` 신설: `portfolio.tag_ids` → `repo.get_tags_by_ids()` → `PortfolioTagListResponse`
- schema 추가: `PortfolioTagListResponse(tags: list[TagItem], total: int)`
2. **REQ-031 comment CRUD 구현**
- 신규 4개 라우트: `POST`, `GET`, `PATCH`, `DELETE /v1/portfolios/{id}/comments[/{comment_id}]`
- 기존 repo 메서드 재사용: `create_comment`, `list_comments`, `update_comment`, `delete_comment`
- schema 추가 (app/schemas/v1.py): `PortfolioCommentItem`, `PortfolioCommentRequest`, `PortfolioCommentUpdateRequest`, `PortfolioCommentListResponse`
- ownership 검증: `_get_portfolio_or_404` 헬퍼 재사용 (BAT-009 패턴)
- disabled 보호: POST/PATCH/DELETE → HTTP 409; GET → 허용
- comment 작성자 ownership: portfolio ownership만 적용 (REQ-031 Notes 결정)
3. **테스트 작성**
- `tests/test_v1_portfolios_tags_comments.py` 신규
- REQ-030 AC-1~11, REQ-031 AC-1~11 커버
- BAT-009의 `_with_repo` 패치 패턴 재사용
4. **REQ-032 baseline 문서 갱신**
- `docs/interface-contract.md`: v1 portfolio CRUD 엔드포인트 섹션 추가 (REQ-025~028), GET /v1/portfolios/{id}/tags 엔드포인트 추가, comment CRUD 엔드포인트 추가; POST /v1/assets/{asset_id}/ledger/trades 섹션 "pending REQ-029" 마킹
- `docs/runtime-flows.md`: portfolio mutation flow + GET /v1/portfolios 토큰 분기 + soft delete flow 추가
- `docs/data-model.md`: Portfolio.status='disabled' 의미 + 의존 데이터 보존 정책 추가
- `docs/project-structure.md`: app/api/v1_portfolios.py 책임 갱신 + app/schemas/v1.py schema 목록 갱신
### 선행 조건
- BAT-009 Implemented (REQ-025~028) ✓ — `_get_portfolio_or_404`, `_require_write_token`, `_close_repo` 헬퍼 재사용
- BAT-003 release-candidate (REQ-001/003): `portfolios.tag_ids UUID[]`, `repo.append_portfolio_tag_id/remove_portfolio_tag_id/get_tags_by_ids` ✓
- BAT-004 release-candidate (REQ-004/005): POST/DELETE /v1/portfolios/{id}/tags 이미 구현 ✓
- `app/repositories/portfolio_repository.py`: `create_comment`, `list_comments`, `update_comment`, `delete_comment` 추상 메서드 이미 정의 ✓
- `app/domain/models.py`: `PortfolioComment(id, portfolio_id, content, created_at, updated_at)` ✓
## Dependencies
- Internal:
- BAT-009(REQ-025~028): `_get_portfolio_or_404`, `_require_write_token` 헬퍼, `PortfolioItem` 스키마 패턴
- BAT-003(REQ-001~003): `portfolios.tag_ids`, `repo.get_tags_by_ids`
- BAT-004(REQ-005): POST/DELETE tag 라우트 (보강 대상)
- External:
- 없음
## Design Gate
- Design Required Before Implementation: yes
- Reason:
- REQ-031 comment 작성자 ownership 정책 확정: portfolio ownership만 적용 (결정 완료)
- REQ-030 GET 응답 schema 확정: `PortfolioTagListResponse(tags: list[TagItem], total: int)` (결정 완료)
- REQ-032 ledger path 마킹 방식 확정: "pending REQ-029" 주석 처리 (결정 완료)
- 모든 설계 gate가 사전 해소됨 → design.md 확정 후 구현 착수 가능
## Milestones
1. planning.md confirmed ✓ (현재 단계)
2. design.md confirmed — key decisions 기록
3. REQ-030 disabled 보호 보강 + GET 신설 + tests
4. REQ-031 comment CRUD 4종 + tests
5. REQ-032 baseline docs 4종 갱신
6. 전체 테스트 스위트 통과
7. verification.md 완성
## Risks
- R-1 (Low): 기존 POST/DELETE tag 라우트에 disabled 보호 추가 시 `test_tags_http_api.py` 회귀 가능 → 기존 테스트가 disabled portfolio를 사용하지 않으면 영향 없음; 확인 후 보강
- R-2 (Low): `repo.get_tags_by_ids(portfolio.tag_ids)` — tag_ids가 빈 배열일 경우 빈 dict 반환 → 빈 list 응답으로 정상 처리
- R-3 (Low): `update_comment`/`delete_comment` postgres 구현에서 `portfolio_comments` 테이블 존재 여부 확인 필요
@@ -0,0 +1,55 @@
# BAT-010 Verification
- Status: verified
- Batch: BAT-010
## Acceptance Mapping
### REQ-030 (portfolio tag routes)
- AC-1 (admin tag → POST 200): `test_post_tag_admin_tag_200` PASSED — `repo.append_portfolio_tag_id` called
- AC-2 (own tag → POST 200): `test_post_tag_own_tag_200` PASSED
- AC-3 (타 user non-admin tag → 403): `test_post_tag_other_user_non_admin_tag_403` PASSED
- AC-4 (inactive tag → 403): `test_post_tag_inactive_tag_403` PASSED
- AC-5 (admin + other portfolio + admin tag → 200): `test_post_tag_admin_other_portfolio_admin_tag_200` PASSED
- AC-6 (other portfolio → 403): `test_post_tag_other_portfolio_403` PASSED
- AC-7 (service/client → 403): `test_post_tag_service_token_403`, `test_post_tag_client_token_403` PASSED
- AC-8 (disabled + POST/DELETE → 409): `test_post_tag_disabled_portfolio_409`, `test_delete_tag_disabled_portfolio_409` PASSED
- AC-9 (DELETE own portfolio → 200): `test_delete_tag_own_portfolio_200` PASSED — `repo.remove_portfolio_tag_id` called
- AC-10 (GET tags → 200 + list): `test_get_portfolio_tags_200`, `test_get_portfolio_tags_empty_200` PASSED
- AC-11 (nonexistent portfolio → 404): `test_get_tags_nonexistent_portfolio_404` PASSED
### REQ-031 (portfolio comment routes)
- AC-1 (POST → 201): `test_post_comment_201` PASSED — comment_id, portfolio_id, content verified
- AC-2 (GET → 200 + list): `test_get_comments_200` PASSED — total=2 verified
- AC-3 (PATCH → 200 + updated): `test_patch_comment_200` PASSED — `repo.update_comment` called with correct args
- AC-4 (DELETE → 204): `test_delete_comment_204` PASSED — `repo.delete_comment` called
- AC-5 (other portfolio → 403): `test_post_comment_other_portfolio_403` PASSED
- AC-6 (service/client → 403): `test_post_comment_service_token_403`, `test_post_comment_client_token_403`, `test_get_comments_service_token_403` PASSED
- AC-7 (disabled + POST/PATCH/DELETE → 409): `test_post_comment_disabled_409`, `test_patch_comment_disabled_409`, `test_delete_comment_disabled_409` PASSED
- AC-8 (disabled + GET → 200): `test_get_comments_disabled_portfolio_200` PASSED
- AC-9 (admin + other portfolio → 201/200): `test_post_comment_admin_other_portfolio_201`, `test_get_comments_admin_other_portfolio_200` PASSED
- AC-10 (no auth → 401): `test_post_comment_no_auth_401` PASSED
- AC-11 (nonexistent comment/portfolio → 404): `test_patch_comment_nonexistent_404`, `test_delete_comment_nonexistent_404`, `test_post_comment_nonexistent_portfolio_404` PASSED
### REQ-032 (baseline docs)
- AC-1 (`docs/interface-contract.md` 갱신): v1 Portfolio API 섹션 신규 추가; GET/POST/DELETE /v1/portfolios/{id}/tags; POST/GET/PATCH/DELETE /v1/portfolios/{id}/comments; ledger "pending REQ-029" 마킹
- AC-2 (`docs/runtime-flows.md` 갱신): portfolio mutation flow, GET branching flow, soft delete + dependent data preservation flow, comment CRUD flow 추가
- AC-3 (`docs/data-model.md` 갱신): Portfolio.status='disabled' 의미 + 의존 데이터 보존 정책 추가
- AC-4 (`docs/project-structure.md` 갱신): v1_portfolios.py 책임 갱신 + v1.py schema 목록 갱신
- AC-5 (코드-문서 정합성): interface-contract 명세가 구현 라우트와 일치 확인 (7개 신규 라우트)
- AC-6 (ledger path "pending REQ-029" 마킹): interface-contract.md에 "pending REQ-029" 섹션 추가
## Evidence
- Test:
- `tests/test_v1_portfolios_tags_comments.py`: 32/32 PASSED (3.27s)
- Full suite: 330/330 PASSED (14.11s, 2 deprecation warnings — 무관)
- No regressions in existing tests (BAT-004 tag tests, BAT-009 portfolio CRUD/list tests)
- Manual:
- `app/api/v1_portfolios.py` 코드 검토: disabled 409 check는 ownership 검증 직후, mutation 직전 위치 확인; `_require_write_token` service/client 차단 확인; GET routes는 disabled 허용 확인
- Logs:
- Full suite output: 330 passed, 2 warnings in 14.11s
## Result
- Ready for Release: true
- Blocking Issues:
- 없음
@@ -0,0 +1,42 @@
# BAT-011 Design
- Status: confirmed
- Batch: BAT-011
## Architecture Summary
- `app/api/v1_portfolios.py`에 `POST /{portfolio_id}/ledger/trades` 서브 라우트를 추가한다. 기존 `app/api/v1_ledger.py`의 daemon 인가 3단계 + idempotency 로직을 신 path로 이식하고, disabled 차단을 daemon 인가 이전에 삽입한다.
- 구 path는 파일 전체 제거(`app/api/v1_ledger.py` 삭제) + `app/main.py`에서 include 제거로 처리한다. 별칭/redirect 없음.
- trex-basic 동시 배포 순서: trex-basic 사전 빌드 완료 확인 → portfolio-hub 배포 → trex-basic 배포. rollback 시 portfolio-hub + trex-basic 동반 rollback.
## Changed Areas
- Docs:
- `docs/interface-contract.md` — REQ-032에서 이미 "pending REQ-029" 마킹됨. 본 batch에서 실제 명세 반영.
- `docs/runtime-flows.md` — ledger trade path 이전 흐름 + daemon assignment 3단계 인가 갱신 (REQ-032 기반 기존 흐름 보강).
- Modules:
- `app/api/v1_portfolios.py` — 신 path 라우트 추가
- `app/api/v1_ledger.py` — 제거 (구 path 라우트 삭제)
- `app/main.py` — 구 ledger 라우터 include 제거
- `app/services/ledger_service.py` — disabled 차단 검증 추가 (또는 라우터 계층에서 처리)
- Interfaces:
- `POST /v1/portfolios/{portfolio_id}/ledger/trades` (신설)
- `POST /v1/assets/{asset_id}/ledger/trades` (제거 → HTTP 404)
## Key Decisions
1. **구 `v1_ledger.py` 완전 제거**: 라우트만 제거하는 대신 파일 전체를 삭제해 잔재 코드가 남지 않도록 한다. `app/main.py`에서 해당 라우터 include를 제거.
2. **disabled 차단 위치**: 라우터 계층에서 `_get_portfolio_or_404` 직후, daemon 인가 3단계 이전에 `if portfolio.status == "disabled": raise HTTPException(409)` 삽입. 서비스 계층 변경 최소화.
3. **인가 순서**: (0) Bearer 없음 → 401, (1) token `daemon_id IS NULL` → 403, (2) portfolio not found → 404, (3) `portfolio.status == "disabled"` → 409, (4) `portfolio.managed_by_daemon_id IS NULL` → 403, (5) `portfolio.managed_by_daemon_id != auth.daemon_id` → 403, (6) idempotency 분기 → 201/200/409.
4. **trex-basic 동시 배포**: release plan에 배포 순서 잠금 명시. 한시적 별칭/redirect 없음.
## Edge Cases
- `portfolio_id`가 비존재 → HTTP 404 (daemon 인가보다 우선).
- `status='disabled'` portfolio → HTTP 409 (daemon 인가 체크 전에 차단하여 daemon 미배정 오류와 구분).
- 동일 `(portfolio_id, order_id, execution_id)` + 동일 payload 재호출 → HTTP 200 + `status="duplicate"`.
- 동일 key + 다른 payload → HTTP 409 (idempotency conflict).
- Bearer token 없음 → HTTP 401.
## Architecture Impact
- runtime-flows
## Reference Doc Update Plan
- project-structure.md: `v1_ledger.py` 제거 반영
- runtime-flows.md: ledger path 이전 흐름 + daemon 인가 3단계 + disabled 차단 순서 갱신
@@ -0,0 +1,36 @@
# BAT-011 Implementation
- Status: done
- Batch: BAT-011
## Plan Summary
- `app/api/v1_portfolios.py`에 `POST /{portfolio_id}/ledger/trades` 추가 (daemon 인가 3단계 + disabled 차단 + idempotency).
- `app/api/v1_ledger.py` 제거 + `app/main.py` include 정리.
- `tests/test_v1_portfolios_ledger.py` 신규 (AC-1~10).
- `tests/test_v1.py` 구 path 케이스 갱신 + HTTP 404 회귀 확인.
## Changed Files
- `app/api/v1_portfolios.py` — `POST /{portfolio_id}/ledger/trades` 라우트 신설; `_require_service_token` 의존성(service token + daemon_id IS NULL 검증); `_payloads_match` 헬퍼; `LedgerTradeRequest/Response` import 추가
- `app/api/v1_ledger.py` — **삭제** (구 path 라우트 완전 제거)
- `app/main.py` — `v1_ledger_router` import 및 `app.include_router(v1_ledger_router)` 제거
- `tests/test_v1_portfolios_ledger.py` — **신규** (AC-1~10: 10 tests)
- `tests/test_v1.py` — 구 path 케이스 5건 신 path 갱신; `app.api.v1_ledger.*` 패치 → `app.api.v1_portfolios.*` 갱신; 파일 말미 중복 helper 2건 제거
- `tests/test_daemon_assignment.py` — `app.api.v1_ledger.*` 패치 → `app.api.v1_portfolios.*` 갱신; URL 4건 갱신; `_portfolio_with_daemon`에 `p.status = "enabled"` 추가
## Execution Log
1. `app/api/v1_portfolios.py` — `Response` import 추가, `LedgerTradeRequest/LedgerTradeResponse` import 추가
2. `_require_service_token` 의존성 추가 (service token 검증 + daemon_id IS NULL 403)
3. `_payloads_match` 헬퍼 추가 (idempotency conflict 판정)
4. `POST /{portfolio_id}/ledger/trades` 라우트 추가 (disabled 409 → managed_by_daemon_id NULL 403 → daemon mismatch 403 → insert_ledger_trade)
5. `app/api/v1_ledger.py` 삭제
6. `app/main.py` 에서 v1_ledger 라우터 import + include 제거
7. `tests/test_v1.py` — 구 path URL 및 패치 경로 갱신 + 중복 helper 제거
8. `tests/test_daemon_assignment.py` — URL 및 패치 경로 갱신; `_portfolio_with_daemon` `status` 명시
9. `tests/test_v1_portfolios_ledger.py` — 신규 생성 (AC-1~10)
## Validation
- `pytest tests/test_v1_portfolios_ledger.py -v`: 10 passed
- `pytest --tb=short -q` (full suite): **340 passed, 0 failed** (2 warnings, non-blocking)
## Remaining Risks
- R-1 (High): trex-basic 동시 배포 — release 단계 배포 순서 잠금 필수 (trex-basic 사전 빌드 완료 확인 → portfolio-hub 배포 → trex-basic 배포).
@@ -0,0 +1,35 @@
# BAT-011: v1 portfolios ledger path migration
- Status: release-candidate
- Profile: standard
- Source Discovery: dcy-003
- Included REQ: REQ-029
- Excluded REQ: none
## Summary
- 기존 `POST /v1/assets/{asset_id}/ledger/trades`(`app/api/v1_ledger.py`)를 `POST /v1/portfolios/{portfolio_id}/ledger/trades`로 이전하고 구 path를 동일 release에서 제거한다.
- 동작·인가·idempotency는 그대로 보존하며 REQ-027 정합의 disabled portfolio 차단(HTTP 409)을 신규 추가한다.
- trex-basic 클라이언트와의 동시 배포가 필수이므로 release plan에 배포 순서 잠금·rollback 전제를 명시한다.
## Scope
- In Scope:
- `app/api/v1_portfolios.py` — `POST /v1/portfolios/{portfolio_id}/ledger/trades` 라우트 추가
- `app/api/v1_ledger.py` — 구 path 라우트 제거 (또는 파일 전체 제거 후 `app/main.py`에서 include 제거)
- `app/main.py` — 구 ledger 라우터 include 제거
- `app/services/portfolio_service.py` 또는 `app/services/ledger_service.py` — disabled 차단 검증 추가
- `tests/test_v1.py` — 구 path 케이스 갱신(HTTP 404 확인)
- `tests/test_v1_portfolios_ledger.py` (신규) — AC-1~10 검증
- Out of Scope:
- `ledger_trades.asset_id` 컬럼명 변경 (별도 migration REQ로 분리 가능)
- trex-basic 코드 변경 (별도 레포)
## Documents
- Planning: [planning.md](./planning.md)
- Design: [design.md](./design.md)
- Implementation: [implementation.md](./implementation.md)
- Verification: [verification.md](./verification.md)
## Notes
- R-1 (High): trex-basic 동시 배포 — 신 path 단일 서빙, 별칭/redirect 없음. release plan에 배포 순서(trex-basic 사전 빌드 → portfolio-hub 배포 → trex-basic 배포) + rollback 시 trex-basic 동반 rollback 전제 명시 필수.
- R-2 (Low): disabled portfolio 차단(AC-7)은 REQ-027 soft delete 정책 정합 검증으로 신규. 기존 daemon 인가 3단계와 순서 충돌 없이 disabled 체크를 insert 전에 추가.
- 선행 완료: BAT-009(soft delete + disabled 상태 관리, REQ-027) ✅
@@ -0,0 +1,39 @@
# BAT-011 Planning
- Status: confirmed
- Batch: BAT-011
- Profile: standard
## Included REQ
- REQ-029: `POST /v1/portfolios/{portfolio_id}/ledger/trades` 이전 + 구 path 제거 + disabled 차단 추가
## Delivery Plan
1. `app/api/v1_portfolios.py` — `POST /{portfolio_id}/ledger/trades` 라우트 추가: daemon 인가 3단계 + disabled 차단 + idempotency + conflict 분기
2. `app/api/v1_ledger.py` — 구 `POST /v1/assets/{asset_id}/ledger/trades` 라우트 제거 (또는 파일 전체 제거)
3. `app/main.py` — 구 `v1_ledger` 라우터 include 제거 (v1_portfolios_router는 BAT-009에서 이미 include됨)
4. `app/services/ledger_service.py` 또는 `app/services/portfolio_service.py` — `disabled` 차단 검증 추가
5. `tests/test_v1_portfolios_ledger.py` 신규 — AC-1~10 검증: 신 path 정상 흐름 + daemon 인가 3단계 + idempotency/conflict + disabled + 404 + 401
6. `tests/test_v1.py` 갱신 — 구 path 케이스를 신 path로 갱신 + 구 path HTTP 404 회귀 확인
## Dependencies
- Internal:
- BAT-009 (release-candidate): soft delete(`status='disabled'`) + `managed_by_daemon_id` 관리 확정 ✅
- BAT-006 (release-candidate): service token 분기 정책 확정(REQ-016/017/018) ✅
- External:
- trex-basic 클라이언트: 신 path `POST /v1/portfolios/{portfolio_id}/ledger/trades` 사전 빌드 필요. release 단계에서 배포 순서 잠금 확인 필수.
## Design Gate
- Design Required Before Implementation: yes
- Reason: (1) 구 `v1_ledger.py` 파일 완전 제거 vs. 라우트만 제거 결정 필요, (2) disabled 차단을 서비스 계층과 라우터 계층 중 어디에 위치시킬지 결정 필요, (3) trex-basic 동시 배포 전략(배포 순서 잠금·rollback 계획)을 design 단계에서 명문화.
## Milestones
1. design.md confirmed — 파일 제거 전략, disabled 차단 위치, 배포 순서 잠금 결정
2. 신 path 라우트 구현 완료 (`app/api/v1_portfolios.py`)
3. 구 path 제거 + `app/main.py` 갱신 완료
4. `tests/test_v1_portfolios_ledger.py` AC-1~10 전부 pass
5. `tests/test_v1.py` 구 path HTTP 404 회귀 확인 + 전체 suite pass
## Risks
- R-1 (High): trex-basic 동시 배포 — 신 path 단일 서빙으로 별칭/redirect 없음. trex-basic이 구 path를 사용 중인 상태에서 portfolio-hub만 배포되면 trex-basic이 즉시 장애. 완화: release plan에 배포 순서(trex-basic 사전 빌드 완료 확인 → portfolio-hub 배포 → trex-basic 배포) 잠금 + rollback 시 trex-basic 동반 rollback 명시.
- R-2 (Low): `ledger_trades.asset_id` 컬럼명은 실제로 portfolio_id 값을 저장하는 혼용 상태가 남으나, 본 REQ에서는 path 명명 정합화만 처리하고 컬럼명 변경은 별도 REQ로 이월.
- R-3 (Low): `tests/test_v1.py`에 구 path 테스트가 얼마나 있는지 design 단계에서 사전 파악 필요.
@@ -0,0 +1,33 @@
# BAT-011 Verification
- Status: verified
- Batch: BAT-011
## Acceptance Mapping
- REQ-029:
- AC-1 (`test_ledger_trade_new`): 신 path HTTP 201 + `status="new"` + `asset_id=_PORTFOLIO_ID` ✅
- AC-2 (`test_ledger_trade_duplicate_same_payload`): 동일 key + 동일 payload → HTTP 200 + `status="duplicate"` ✅
- AC-3 (`test_ledger_trade_conflict_different_payload`): 동일 key + 다른 quantity → HTTP 409 ✅
- AC-4 (`test_ledger_trade_no_daemon_id`): service token `daemon_id IS NULL` → HTTP 403; `insert_ledger_trade` not called ✅
- AC-5 (`test_ledger_trade_portfolio_unassigned`): `portfolio.managed_by_daemon_id IS NULL` → HTTP 403; `insert_ledger_trade` not called ✅
- AC-6 (`test_ledger_trade_daemon_mismatch`): `managed_by_daemon_id="other-daemon"` vs `auth.daemon_id=_DAEMON_ID` → HTTP 403; `insert_ledger_trade` not called ✅
- AC-7 (`test_ledger_trade_disabled_portfolio`): `status="disabled"` → HTTP 409; `insert_ledger_trade` not called ✅
- AC-8 (`test_ledger_trade_portfolio_not_found`): `repo.get.side_effect=Exception` → HTTP 404 ✅
- AC-9 (`test_old_ledger_path_returns_404`): `POST /v1/assets/{id}/ledger/trades` → HTTP 404 (라우트 제거) ✅
- AC-10 (`test_ledger_trade_no_auth`): Bearer 없음 → HTTP 401 ✅
- Regression (`test_daemon_assignment.py` 4건): daemon assignment 인가 3단계 신 path에서 동작 검증 ✅
## Evidence
- Test:
- `pytest tests/test_v1_portfolios_ledger.py -v`: **10/10 passed**
- `pytest --tb=short -q` (full suite): **340 passed, 0 failed**, 2 warnings (non-blocking DeprecationWarning from anyio)
- 이전 suite 대비 +10 tests (330 → 340)
- Manual:
- 해당 없음 — 자동화 검증으로 충분 (인터페이스 변경, UI 없음)
- Logs:
- 2 DeprecationWarnings: `anyio._backends._asyncio.py` `HTTP_422_UNPROCESSABLE_ENTITY` → `HTTP_422_UNPROCESSABLE_CONTENT`. 기존 suite에서도 동일 경고 존재. 비차단.
## Result
- Ready for Release: true
- Blocking Issues: 없음 (코드 구현 완료, 10/10 AC 검증)
- Release Note: trex-basic 동시 배포 전제 — release 단계에서 배포 순서 잠금 필수. 신 path 단일 서빙, 별칭/redirect 없음.
+4
View File
@@ -15,3 +15,7 @@
| BAT-005 | standard | release-candidate | REQ-007, REQ-008 | dcy-002 | [bat-005](./bat-005_20260520_pfhub-tag-cli-and-docs/) |
| BAT-006 | standard | release-candidate | REQ-016, REQ-017, REQ-018 | dcy-004 | [bat-006](./bat-006_20260520_token-surface-code-fixes/) |
| BAT-007 | standard | release-candidate | REQ-022, REQ-023 | dcy-004 | [bat-007](./bat-007_20260520_daemon-lifecycle-token-enforcement/) |
| BAT-008 | standard | release-candidate | REQ-019, REQ-020, REQ-021, REQ-024 | dcy-004 | [bat-008](./bat-008_20260521_token-surface-docs/) |
| BAT-009 | standard | release-candidate | REQ-025, REQ-026, REQ-027, REQ-028 | dcy-003 | [bat-009](./bat-009_20260521_v1-portfolios-crud-core/) |
| BAT-010 | standard | release-candidate | REQ-030, REQ-031, REQ-032 | dcy-003 | [bat-010](./bat-010_20260521_v1-portfolios-sub-resources/) |
| BAT-011 | standard | release-candidate | REQ-029 | dcy-003 | [bat-011](./bat-011_20260521_v1-portfolios-ledger-path-migration/) |