feat: 태그 HTTP API(BAT-004) 및 pfhub tag CLI + baseline 문서 갱신(BAT-005)
BAT-004 (REQ-004, REQ-005):
- app/api/v1_tags.py: POST /v1/tags, GET /v1/tags, PATCH /v1/tags/{id}/deactivate
- app/api/v1_portfolios.py: POST/DELETE /v1/portfolios/{id}/tags
- app/api/v1_assets.py: POST/DELETE /v1/assets/{id}/tags 엔드포인트 추가
- app/schemas/v1.py: TagItem, TagCreateRequest, TagListResponse, TagApplyRequest/Response 추가
- app/repositories/portfolio_repository.py, postgres.py: append/remove_portfolio_tag_id 추가
- app/main.py: v1_portfolios_router, v1_tags_router 등록
- tests/test_tags_http_api.py: 22개 HTTP API 테스트
BAT-005 (REQ-007, REQ-008):
- app/cli/commands/tag.py: pfhub tag create/list/deactivate/apply/remove 7개 커맨드 신규
- app/cli/main.py: tag_app 등록
- app/cli/commands/portfolio.py: pfhub portfolio tag add/list/remove 완전 제거(breaking change)
- app/cli/core/utils.py: print_tag_table + list[Tag] render 분기 추가
- tests/test_tags_cli.py: REQ-007 AC-1~9 커버 24개 CLI 테스트 신규
- tests/test_cli.py: 제거된 portfolio tag 테스트 2개 삭제
- docs/data-model.md, interface-contract.md, project-structure.md, runtime-flows.md: 태그 모델 baseline 갱신(REQ-008)
- docs/batches/: BAT-004, BAT-005 전체 산출물(release-candidate)
247 passed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
# BAT-004 Design
|
||||
|
||||
- Status: confirmed
|
||||
- Batch: BAT-004
|
||||
|
||||
## Architecture Summary
|
||||
- REQ-004: `app/api/v1_tags.py` 신규 라우터 파일(prefix="/v1"), POST /v1/tags / GET /v1/tags / PATCH /v1/tags/{tag_id}/deactivate 엔드포인트.
|
||||
- REQ-005(asset side): 기존 `app/api/v1_assets.py`에 POST/DELETE /v1/assets/{asset_id}/tags 라우트 추가. v1 API에서 asset_id = portfolio_id이므로 두 엔드포인트 모두 `portfolios.tag_ids`를 조작.
|
||||
- REQ-005(portfolio side): `app/api/v1_portfolios.py` 신규 라우터 파일(prefix="/v1/portfolios"), POST/DELETE /v1/portfolios/{portfolio_id}/tags 엔드포인트. 기존 `portfolios.py`(prefix="/portfolios")와 path prefix가 달라 충돌 없음.
|
||||
- 정책 검증 로직은 서비스 레이어 없이 라우터에서 직접 처리(현재 아키텍처 패턴 유지).
|
||||
- tag_ids 배열 조작: `append_portfolio_tag_id` / `remove_portfolio_tag_id` 신규 메서드를 postgres.py에 추가.
|
||||
|
||||
## Changed Areas
|
||||
- Docs: docs/interface-contract.md (엔드포인트 명세 — REQ-008 배치에서 일괄 갱신)
|
||||
- Modules:
|
||||
- `app/api/v1_tags.py` — 신규 파일 (POST/GET/PATCH /v1/tags)
|
||||
- `app/api/v1_portfolios.py` — 신규 파일 (POST/DELETE /v1/portfolios/{portfolio_id}/tags)
|
||||
- `app/api/v1_assets.py` — POST/DELETE /v1/assets/{asset_id}/tags 라우트 추가
|
||||
- `app/schemas/v1.py` — TagItem, TagCreateRequest, TagListResponse, TagApplyRequest, TagApplyResponse 추가
|
||||
- `app/repositories/portfolio_repository.py` — append_portfolio_tag_id, remove_portfolio_tag_id 프로토콜 추가
|
||||
- `app/repositories/postgres.py` — append_portfolio_tag_id, remove_portfolio_tag_id 구현 추가
|
||||
- `app/main.py` — v1_tags, v1_portfolios 라우터 등록
|
||||
- `tests/test_tags_http_api.py` — 신규 테스트 파일
|
||||
- Interfaces:
|
||||
- POST /v1/tags — 태그 생성 (trade-write 이상, DB token 필수)
|
||||
- GET /v1/tags — 태그 목록 (read 이상, ?is_active 필터, DB token 필수)
|
||||
- PATCH /v1/tags/{tag_id}/deactivate — 태그 비활성화 (admin 또는 생성자 본인, DB token 필수)
|
||||
- POST /v1/assets/{asset_id}/tags — asset(portfolio)에 태그 적용 (admin, DB token 필수)
|
||||
- DELETE /v1/assets/{asset_id}/tags/{tag_id} — asset에서 태그 해제 (admin, DB token 필수)
|
||||
- POST /v1/portfolios/{portfolio_id}/tags — portfolio에 태그 적용 (trade-write, 소유자/admin, DB token 필수)
|
||||
- DELETE /v1/portfolios/{portfolio_id}/tags/{tag_id} — portfolio에서 태그 해제 (trade-write, 소유자/admin, DB token 필수)
|
||||
|
||||
## Key Decisions
|
||||
1. **v1_tags.py 신규 파일 분리**: tags 도메인 전용 라우터를 별도 파일로 분리해 v1_assets.py(asset 카탈로그 CRUD)와 관심사 분리. 이후 REQ-007 CLI 추가 시 충돌 없음.
|
||||
2. **v1_portfolios.py 신규 파일**: REQ-005 portfolio tag 엔드포인트는 `/v1/portfolios` prefix. 기존 `portfolios.py`는 `/portfolios` prefix(v1 아님)이므로 새 라우터 파일 생성이 필요. 두 파일의 path가 완전히 달라 충돌 없음.
|
||||
3. **asset_id = portfolio_id (v1 API 규약)**: `/v1/assets/{asset_id}/tags`는 catalog asset 테이블이 아니라 portfolios 테이블의 tag_ids를 조작. v1 API에서 portfolio_id를 asset_id로 노출하는 기존 규약 준수. `repo.get(asset_id)` 로 portfolio 존재 확인.
|
||||
4. **소문자 정규화 위치**: POST /v1/tags 라우터에서 `name = body.name.strip().lower()`. DB는 TEXT UNIQUE 그대로(BAT-003 설계 일관성).
|
||||
5. **정책 검증 위치**: 라우터에서 직접 `repo.get_tag_by_id()` 호출 후 조건 검사 → 서비스 레이어 없이 라우터 직접 처리(현재 아키텍처 패턴 유지).
|
||||
6. **array_append 멱등성**: `UPDATE portfolios SET tag_ids = array_append(tag_ids, %s::uuid) WHERE portfolio_id = %s AND NOT (%s::uuid = ANY(tag_ids))` — tag_id가 이미 배열에 있으면 UPDATE 행 0건 → rowcount=0인 경우 portfolio 존재를 별도 확인, 존재하면 멱등 처리(200 반환).
|
||||
7. **deactivate 멱등성**: `repo.get_tag_by_id(tag_id)` 먼저 호출 → is_active=FALSE이면 바로 현재 상태 반환(200). is_active=TRUE이면 `repo.deactivate_tag(tag_id)` 실행. tag 존재하지 않으면 404.
|
||||
8. **auth permission 분기(v1 패턴)**: 모든 v1 엔드포인트는 `auth.token_source != "db"` 이면 403 반환(기존 v1_assets.py 패턴 일관성 유지). permission_dependency(Depends) 사용해 최소 권한 확인 후 라우터에서 세부 조건 추가 확인.
|
||||
|
||||
## Edge Cases
|
||||
- 존재하지 않는 tag_id → PATCH /deactivate → 404 (get_tag_by_id 결과 None)
|
||||
- 존재하지 않는 portfolio_id → POST/DELETE /assets|portfolios/{id}/tags → 404
|
||||
- non-admin 비생성자 → PATCH /deactivate → 403
|
||||
- is_admin_tag=FALSE tag_id → POST /v1/assets/{asset_id}/tags → 403
|
||||
- is_active=FALSE tag_id → POST /v1/assets/{asset_id}/tags 또는 /portfolios → 403
|
||||
- 다른 user 생성 non-admin tag → POST /v1/portfolios/{id}/tags (소유자 토큰) → 403
|
||||
- 이미 tag_ids 배열에 있는 tag_id 재적용 → 200 멱등 반환 (array_append 조건부 + rowcount 분기)
|
||||
- 이미 is_active=FALSE → PATCH /deactivate 재요청 → 200 멱등 반환
|
||||
- env token (token_source="env") → 403 DB token required (v1 패턴 일관성)
|
||||
|
||||
## Architecture Impact
|
||||
- both (project-structure: 신규 라우터 파일 2개, runtime-flows: 태그 HTTP 흐름 신규)
|
||||
|
||||
## Reference Doc Update Plan
|
||||
- project-structure.md: REQ-008 배치에서 갱신 (v1_tags.py, v1_portfolios.py 추가, pfhub tag 그룹 추가)
|
||||
- runtime-flows.md: REQ-008 배치에서 갱신 (POST/GET/PATCH /v1/tags 흐름, POST/DELETE /v1/assets|portfolios/{id}/tags 흐름)
|
||||
|
||||
## Baseline Comparison (vs project-structure.md / runtime-flows.md)
|
||||
- project-structure.md 현재: app/api/ — v1_assets.py, v1_user_info.py, v1_ledger.py만 v1 라우터로 기록됨. v1_tags.py, v1_portfolios.py 추가 필요.
|
||||
- runtime-flows.md 현재: GET /v1/assets, GET /v1/user_info, POST ledger/trades, POST /auth/service-tokens 흐름만 기록됨. tags CRUD HTTP 흐름, 태그 적용/해제 HTTP 흐름 추가 필요.
|
||||
- 두 baseline 문서 갱신은 REQ-008 배치에서 수행.
|
||||
@@ -0,0 +1,39 @@
|
||||
# BAT-004 Implementation
|
||||
|
||||
- Status: completed
|
||||
- Batch: BAT-004
|
||||
|
||||
## Plan Summary
|
||||
- REQ-004: v1_tags.py 신규 라우터 + TagItem/TagCreateRequest/TagListResponse 스키마 + main.py 등록 + 소문자 정규화 + 권한 분기 + 비활성화 멱등성
|
||||
- REQ-005: v1_assets.py asset 태그 라우트 + v1_portfolios.py portfolio 태그 라우트 + TagApplyRequest/TagApplyResponse 스키마 + postgres.py array_append/remove + 정책 검증
|
||||
- 전체 pytest suite 228 passed, regression 없음
|
||||
|
||||
## Changed Files
|
||||
- `app/api/v1_tags.py` (신규) — POST/GET/PATCH /v1/tags
|
||||
- `app/api/v1_portfolios.py` (신규) — POST/DELETE /v1/portfolios/{portfolio_id}/tags
|
||||
- `app/api/v1_assets.py` — POST/DELETE /v1/assets/{asset_id}/tags 라우트 추가, TagApplyRequest/TagApplyResponse 임포트 추가
|
||||
- `app/schemas/v1.py` — TagItem, TagCreateRequest, TagListResponse, TagApplyRequest, TagApplyResponse 추가
|
||||
- `app/repositories/portfolio_repository.py` — append_portfolio_tag_id, remove_portfolio_tag_id 프로토콜 추가
|
||||
- `app/repositories/postgres.py` — append_portfolio_tag_id, remove_portfolio_tag_id 구현 추가
|
||||
- `app/main.py` — v1_tags, v1_portfolios 라우터 등록
|
||||
- `tests/test_tags_http_api.py` (신규) — 22개 테스트
|
||||
|
||||
## Execution Log
|
||||
1. app/schemas/v1.py: TagItem, TagCreateRequest, TagListResponse, TagApplyRequest, TagApplyResponse 추가
|
||||
2. app/repositories/portfolio_repository.py: append_portfolio_tag_id / remove_portfolio_tag_id 프로토콜 추가
|
||||
3. app/repositories/postgres.py: append_portfolio_tag_id (array_append + idempotent rowcount 분기) / remove_portfolio_tag_id 구현 추가
|
||||
4. app/api/v1_tags.py 신규: create_tag(소문자 정규화, is_admin_tag 분기), list_tags(is_active 필터), deactivate_tag(admin/creator 검증, 멱등성)
|
||||
5. app/api/v1_portfolios.py 신규: apply_portfolio_tag(소유자/admin 확인, 태그 정책 검증), remove_portfolio_tag(소유자/admin 확인)
|
||||
6. app/api/v1_assets.py: apply_asset_tag(admin only, is_admin_tag=TRUE AND is_active=TRUE), remove_asset_tag(admin only) 추가
|
||||
7. app/main.py: v1_tags, v1_portfolios 라우터 등록
|
||||
8. tests/test_tags_http_api.py: 22개 AC 테스트 작성 (REQ-004 AC-1~11, REQ-005 AC-1~11)
|
||||
9. pytest 전체 suite 실행 → 228 passed, 0 failed
|
||||
|
||||
## Validation
|
||||
- pytest 실행 결과: **228 passed, 0 failed** (2026-05-20)
|
||||
- 신규 테스트: test_tags_http_api.py 22개 (전체 통과)
|
||||
- 기존 regression: test_cli.py, test_v1.py, test_asset_catalog.py, test_portfolio_tags.py, test_portfolio_uuid.py, test_daemon_assignment.py, test_tags_master.py 전체 통과
|
||||
|
||||
## Remaining Risks
|
||||
- PATCH /v1/tags/{tag_id}/deactivate: 비생성자 non-admin 시 403 확인은 unit test로 커버. live DB에서 auth.user_id가 올바르게 채워지는지는 운영 환경 연동 테스트 필요.
|
||||
- append_portfolio_tag_id의 idempotent 처리(rowcount=0 분기): 단위 테스트로 커버되지 않았으며 live DB에서 정상 동작 확인 필요.
|
||||
@@ -0,0 +1,36 @@
|
||||
# BAT-004: tags HTTP API
|
||||
|
||||
- Status: release-candidate
|
||||
- Profile: standard
|
||||
- Source Discovery: dcy-002
|
||||
- Included REQ: REQ-004, REQ-005
|
||||
- Excluded REQ: (없음 — dcy-002 HTTP API 2개 전부 포함)
|
||||
|
||||
## Summary
|
||||
- tags 마스터에 대한 HTTP API(POST/GET/PATCH /v1/tags)를 신규 라우터로 추가하고(REQ-004), asset 및 portfolio에 태그를 적용·해제하는 HTTP API(POST/DELETE /v1/assets/{id}/tags, POST/DELETE /v1/portfolios/{id}/tags)를 구현한다(REQ-005).
|
||||
- 이 배치 이후 CLI 계층(REQ-007)과 baseline 문서 갱신(REQ-008)은 별도 배치에서 수행한다.
|
||||
|
||||
## Scope
|
||||
- In Scope:
|
||||
- `app/api/v1_tags.py` 신규 라우터 (POST/GET/PATCH /v1/tags) — REQ-004
|
||||
- `app/schemas/v1.py` — TagItem, TagCreateRequest, TagListResponse, TagApplyRequest 스키마 추가
|
||||
- `app/repositories/portfolio_repository.py` — insert_tag, get_tags, deactivate_tag 등 프로토콜 보완
|
||||
- `app/repositories/postgres.py` — 태그 CRUD SQL 구현 보완, tag_ids array_append/array_remove SQL
|
||||
- `app/api/v1_assets.py` — POST/DELETE /v1/assets/{asset_id}/tags 라우트 추가 — REQ-005
|
||||
- `app/api/portfolios.py` — POST/DELETE /v1/portfolios/{portfolio_id}/tags 라우트 추가 — REQ-005
|
||||
- `app/main.py` — v1_tags 라우터 등록
|
||||
- Out of Scope:
|
||||
- pfhub tag CLI (→ REQ-007, 다음 배치)
|
||||
- baseline 문서 갱신 (→ REQ-008, 다음 배치)
|
||||
- DB 스키마 변경 (→ BAT-003 완료)
|
||||
|
||||
## 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): 권한 분기(trade-write/admin/owner) 조건이 복잡 — 정책 오류 시 AC 위반 위험
|
||||
- REQ-004와 REQ-005는 같은 tags 도메인 HTTP 레이어이며 정책 검증 로직을 공유 → 함께 구현·검증
|
||||
- 선행 의존: BAT-003(release-candidate) — tags 테이블, portfolios.tag_ids, assets.tag_ids 완료 상태
|
||||
@@ -0,0 +1,46 @@
|
||||
# BAT-004 Planning
|
||||
|
||||
- Status: confirmed
|
||||
- Batch: BAT-004
|
||||
- Profile: standard
|
||||
|
||||
## Included REQ
|
||||
- REQ-004: tags 마스터 HTTP API (POST /v1/tags, GET /v1/tags, PATCH /v1/tags/{tag_id}/deactivate)
|
||||
- REQ-005: 태그 적용·해제 HTTP API (POST/DELETE /v1/assets/{asset_id}/tags, POST/DELETE /v1/portfolios/{portfolio_id}/tags)
|
||||
|
||||
## Delivery Plan
|
||||
1. **REQ-004 — v1_tags.py 신규 라우터**
|
||||
- `app/api/v1_tags.py` 파일 생성 — POST/GET/PATCH /v1/tags 엔드포인트
|
||||
- `app/schemas/v1.py` — TagItem, TagCreateRequest, TagListResponse 추가
|
||||
- `app/main.py` — v1_tags 라우터 등록
|
||||
- 이름 소문자 정규화(`str.lower()`)는 라우터 레이어에서 수행
|
||||
- 권한: POST는 trade-write 이상, GET은 read 이상, PATCH는 admin 또는 생성자 본인
|
||||
- 비활성화 멱등 처리(이미 is_active=FALSE → 200 반환)
|
||||
2. **REQ-005 — 태그 적용·해제 라우트 추가**
|
||||
- `app/api/v1_assets.py` — POST /v1/assets/{asset_id}/tags, DELETE /v1/assets/{asset_id}/tags/{tag_id}
|
||||
- `app/api/portfolios.py` — POST /v1/portfolios/{portfolio_id}/tags, DELETE /v1/portfolios/{portfolio_id}/tags/{tag_id}
|
||||
- `app/schemas/v1.py` — TagApplyRequest 추가
|
||||
- `app/repositories/postgres.py` — tag_ids 배열 append/remove SQL (array_append, array_remove)
|
||||
- 정책 검증: asset → is_admin_tag=TRUE AND is_active=TRUE, portfolio → is_active=TRUE AND (is_admin_tag=TRUE OR created_by_user_id=소유자)
|
||||
3. **전체 pytest 실행 — regression 확인**
|
||||
|
||||
## Dependencies
|
||||
- Internal:
|
||||
- BAT-003(release-candidate): tags 테이블, portfolios.tag_ids UUID[], assets.tag_ids UUID[], create_tag/get_tag_by_id/get_tags_by_ids/list_tags_master/deactivate_tag 프로토콜 및 postgres 구현 완료
|
||||
- app/core/auth.py: require_read, require_trade_write, require_admin 기존 permission dependency 재사용
|
||||
- External:
|
||||
- 없음
|
||||
|
||||
## Design Gate
|
||||
- Design Required Before Implementation: yes
|
||||
- Reason: 신규 라우터 파일(v1_tags.py) 추가, 권한 분기(trade-write/admin/owner) 설계, 정책 검증 로직 위치 결정(라우터 직접 vs 서비스 레이어), array_append/remove 멱등성 처리 방식 결정이 필요하다.
|
||||
|
||||
## Milestones
|
||||
1. REQ-004 라우터 구현 완료 + 스키마 추가 + tests
|
||||
2. REQ-005 apply/remove 라우트 구현 완료 + policy 검증 + tests
|
||||
3. 전체 pytest suite 통과 (regression 없음)
|
||||
|
||||
## Risks
|
||||
- R-1 (Medium): 권한 분기(trade-write/admin/owner) 조건 복잡 — PATCH /deactivate에서 admin vs 생성자 본인 구분, portfolio 태그 적용에서 owner_user_id 확인 오류 가능성
|
||||
- R-2 (Low): array_append 중복 허용 여부 — 동일 tag_id를 이미 포함한 배열에 재적용 시 멱등 처리 설계 필요
|
||||
- R-3 (Low): assets.tag_ids 컬럼이 BAT-003 ensure_schema()에 추가됨 — REQ-002 live DB 마이그레이션 완료 여부 확인 필요 (테스트 환경은 ensure_schema로 자동 생성)
|
||||
@@ -0,0 +1,70 @@
|
||||
# BAT-004 Verification
|
||||
|
||||
- Status: release-candidate
|
||||
- Batch: BAT-004
|
||||
|
||||
## Acceptance Mapping
|
||||
|
||||
- REQ-004:
|
||||
- AC-1: POST /v1/tags {"name": "TestTag"} → HTTP 201, name="testtag"(소문자 정규화) → `test_create_tag_lowercase_normalization`
|
||||
- AC-2: admin 토큰 → is_admin_tag=TRUE → `test_create_tag_admin_is_admin_tag_true`
|
||||
- AC-3: non-admin trade-write 토큰 → is_admin_tag=FALSE → `test_create_tag_non_admin_is_admin_tag_false`
|
||||
- AC-4: 동일 이름(대소문자 변형) 중복 → HTTP 409 → `test_create_tag_duplicate_name_returns_409`
|
||||
- AC-5: GET /v1/tags → HTTP 200, 활성 태그 목록 → `test_list_tags_active_only`
|
||||
- AC-6: GET /v1/tags?is_active=false → 비활성 태그만 반환 → `test_list_tags_inactive_filter`
|
||||
- AC-7: admin 토큰 PATCH /deactivate → HTTP 200, is_active=FALSE → `test_deactivate_tag_admin`
|
||||
- AC-8: 생성자(non-admin) 토큰 본인 태그 비활성화 → HTTP 200 → `test_deactivate_tag_creator`
|
||||
- AC-9: non-admin 비생성자 PATCH /deactivate → HTTP 403 → `test_deactivate_tag_non_creator_returns_403`
|
||||
- AC-10: 존재하지 않는 tag_id PATCH /deactivate → HTTP 404 → `test_deactivate_nonexistent_tag_returns_404`
|
||||
- AC-11: 이미 is_active=FALSE 태그 재요청 → HTTP 200 멱등 → `test_deactivate_already_inactive_tag_idempotent`
|
||||
|
||||
- REQ-005:
|
||||
- AC-1: admin + admin 생성 활성 tag → POST /v1/assets/{id}/tags → HTTP 200, tag_ids 추가 → `test_apply_asset_tag_admin_success`
|
||||
- AC-2: admin + is_admin_tag=FALSE tag → HTTP 403 → `test_apply_asset_tag_non_admin_tag_returns_403`
|
||||
- AC-3: admin + is_active=FALSE tag → HTTP 403 → `test_apply_asset_tag_inactive_returns_403`
|
||||
- AC-4: non-admin 토큰 → POST /v1/assets/{id}/tags → HTTP 403 → `test_apply_asset_tag_non_admin_token_returns_403`
|
||||
- AC-5: admin → DELETE /v1/assets/{id}/tags/{tag_id} → HTTP 200, tag_id 제거 → `test_remove_asset_tag_admin_success`
|
||||
- AC-6: 소유자, 본인 생성 non-admin 활성 tag → POST /v1/portfolios/{id}/tags → HTTP 200 → `test_apply_portfolio_tag_owner_own_tag`
|
||||
- AC-7: 소유자, admin 생성 활성 tag → POST /v1/portfolios/{id}/tags → HTTP 200 → `test_apply_portfolio_tag_owner_admin_tag`
|
||||
- AC-8: 소유자, 타 user 생성 non-admin tag → HTTP 403 → `test_apply_portfolio_tag_other_user_non_admin_tag_returns_403`
|
||||
- AC-9: 소유자/admin → DELETE /v1/portfolios/{id}/tags/{tag_id} → HTTP 200 → `test_remove_portfolio_tag_owner_success`
|
||||
- AC-10: 존재하지 않는 tag_id → HTTP 404 → `test_apply_portfolio_tag_nonexistent_tag_returns_404`
|
||||
- AC-11: 존재하지 않는 portfolio_id → HTTP 404 → `test_apply_portfolio_tag_nonexistent_portfolio_returns_404`
|
||||
|
||||
## Evidence
|
||||
|
||||
- Test:
|
||||
- pytest 실행 결과: **228 passed, 0 failed** (2026-05-20)
|
||||
- 신규 테스트: `tests/test_tags_http_api.py` 22개
|
||||
- `test_create_tag_lowercase_normalization`
|
||||
- `test_create_tag_admin_is_admin_tag_true`
|
||||
- `test_create_tag_non_admin_is_admin_tag_false`
|
||||
- `test_create_tag_duplicate_name_returns_409`
|
||||
- `test_list_tags_active_only`
|
||||
- `test_list_tags_inactive_filter`
|
||||
- `test_deactivate_tag_admin`
|
||||
- `test_deactivate_tag_creator`
|
||||
- `test_deactivate_tag_non_creator_returns_403`
|
||||
- `test_deactivate_nonexistent_tag_returns_404`
|
||||
- `test_deactivate_already_inactive_tag_idempotent`
|
||||
- `test_apply_asset_tag_admin_success`
|
||||
- `test_apply_asset_tag_non_admin_tag_returns_403`
|
||||
- `test_apply_asset_tag_inactive_returns_403`
|
||||
- `test_apply_asset_tag_non_admin_token_returns_403`
|
||||
- `test_remove_asset_tag_admin_success`
|
||||
- `test_apply_portfolio_tag_owner_own_tag`
|
||||
- `test_apply_portfolio_tag_owner_admin_tag`
|
||||
- `test_apply_portfolio_tag_other_user_non_admin_tag_returns_403`
|
||||
- `test_remove_portfolio_tag_owner_success`
|
||||
- `test_apply_portfolio_tag_nonexistent_tag_returns_404`
|
||||
- `test_apply_portfolio_tag_nonexistent_portfolio_returns_404`
|
||||
- 기존 regression 전체 통과: test_cli.py, test_v1.py, test_asset_catalog.py, test_portfolio_tags.py, test_portfolio_uuid.py, test_daemon_assignment.py, test_tags_master.py
|
||||
- Manual (pre-release 수동 검증 항목 — live DB 연동 시):
|
||||
- POST /v1/tags 실제 DB token으로 소문자 정규화 확인
|
||||
- append_portfolio_tag_id idempotent 처리(tag_id 중복 적용 시 200 반환) 확인
|
||||
- Logs: N/A
|
||||
|
||||
## Result
|
||||
- Ready for Release: true
|
||||
- Blocking Issues: 없음
|
||||
- Pre-release 수동 검증: live DB 연동 환경에서 auth.user_id 정상 채움 확인, append idempotent 처리 확인
|
||||
@@ -0,0 +1,44 @@
|
||||
# BAT-005 Design
|
||||
|
||||
- Status: confirmed
|
||||
- Batch: BAT-005
|
||||
|
||||
## Architecture Summary
|
||||
- REQ-007: `app/cli/commands/tag.py` 신규 파일에 `tag_app = typer.Typer()` 생성 후 `pfhub tag` 7개 커맨드 구현. CLI는 HTTP API를 거치지 않고 기존 repo 메서드를 직접 호출(현재 CLI 아키텍처 패턴 유지). apply/remove는 HTTP 오류를 repo ValueError로 전환해 표시.
|
||||
- `pfhub portfolio tag add/list/remove`는 `portfolio.py`에서 완전 삭제(deprecated 래퍼 없음) — 기존 테스트(test_portfolio_tags.py) 조정 필요.
|
||||
- REQ-008: 4개 baseline 문서를 코드 현황에 맞게 갱신. 모듈/테스트 변경 없음.
|
||||
|
||||
## Changed Areas
|
||||
- Docs:
|
||||
- `docs/data-model.md` — tags 테이블 스키마, portfolios.tag_ids, portfolio_tags DROP 반영
|
||||
- `docs/interface-contract.md` — /v1/tags, /v1/assets/{id}/tags, /v1/portfolios/{id}/tags 명세 추가
|
||||
- `docs/project-structure.md` — tag.py 신규, pfhub tag 커맨드 그룹 추가, pfhub portfolio tag 제거
|
||||
- `docs/runtime-flows.md` — pfhub tag CLI 흐름 신규, /v1/tags HTTP 흐름 신규
|
||||
- Modules:
|
||||
- `app/cli/commands/tag.py` — 신규 파일
|
||||
- `app/cli/main.py` — tag_app 등록
|
||||
- `app/cli/commands/portfolio.py` — tag_app, tag_add, tag_list, tag_remove 완전 삭제
|
||||
- `tests/test_portfolio_tags.py` — pfhub portfolio tag CLI 테스트 제거 또는 조정
|
||||
- Interfaces:
|
||||
- 신규: pfhub tag create/list/deactivate/apply/remove 7개 커맨드
|
||||
- 제거: pfhub portfolio tag add/list/remove 3개 커맨드
|
||||
|
||||
## Key Decisions
|
||||
1. **HTTP API 직접 호출 vs repo 메서드**: 기존 CLI 아키텍처는 AppContext를 통해 repo를 주입받아 직접 repo 메서드를 호출. pfhub tag CLI도 동일 패턴 유지 — HTTP 클라이언트 별도 없음. apply/remove는 `repo.append_portfolio_tag_id` / `repo.remove_portfolio_tag_id` 직접 호출, 정책 검증은 repo가 아닌 HTTP API 레이어에서 하는 것이 원칙이나 CLI는 repo 직접 호출이므로 정책 검증 로직을 CLI 레이어에서도 수행.
|
||||
2. **pfhub portfolio tag 완전 삭제**: deprecated 래퍼 없이 완전 제거. REQ-007 AC-9 "명령어가 존재하지 않는다(Typer error)" 충족.
|
||||
3. **apply/remove sub-group**: `pfhub tag apply`와 `pfhub tag remove`는 각각 `asset`/`portfolio` 두 sub-command를 갖는 nested Typer group으로 구현.
|
||||
4. **--force 확인**: `pfhub tag deactivate`, `pfhub tag remove` 두 커맨드에서 `--force` 플래그가 없으면 실행 거부(기존 CLI 패턴 일관성).
|
||||
5. **test_portfolio_tags.py 조정**: 기존 `pfhub portfolio tag add/list/remove` 테스트를 제거. 이 파일의 나머지 테스트(존재하면)는 유지.
|
||||
|
||||
## Edge Cases
|
||||
- `pfhub tag create <name>` 중복 이름 → repo ValueError → CLI 오류 메시지 출력 후 non-zero exit
|
||||
- `pfhub tag deactivate <tag_id>` --force 없이 실행 → "확인이 필요합니다" 오류
|
||||
- `pfhub tag apply asset/portfolio` 정책 위반 → repo/HTTP 403 오류 → CLI 오류 메시지 출력
|
||||
- `pfhub portfolio tag add/list/remove` 실행 → Typer "No such command" 오류 (제거 후)
|
||||
|
||||
## Architecture Impact
|
||||
- both (project-structure: tag.py 신규, runtime-flows: pfhub tag 흐름 신규)
|
||||
|
||||
## Reference Doc Update Plan
|
||||
- project-structure.md: REQ-008에서 직접 갱신 (이 배치 내부에서 처리)
|
||||
- runtime-flows.md: REQ-008에서 직접 갱신 (이 배치 내부에서 처리)
|
||||
@@ -0,0 +1,37 @@
|
||||
# BAT-005 Implementation
|
||||
|
||||
- Status: completed
|
||||
- Batch: BAT-005
|
||||
|
||||
## Plan Summary
|
||||
- REQ-007: tag.py 신규 CLI 커맨드 그룹 + main.py 등록 + portfolio.py tag 커맨드 제거
|
||||
- REQ-008: baseline 문서 4개 갱신 (data-model, interface-contract, project-structure, runtime-flows)
|
||||
|
||||
## Changed Files
|
||||
- `app/cli/commands/tag.py` — 신규: tag_app (create/list/deactivate), apply_app (asset/portfolio), remove_app (asset/portfolio) 7개 커맨드
|
||||
- `app/cli/main.py` — tag_app import 및 `app.add_typer(tag_app, name="tag")` 추가
|
||||
- `app/cli/commands/portfolio.py` — tag_app Typer, tag_add, tag_list, tag_remove 완전 삭제; PortfolioTagMutationRequest import 제거
|
||||
- `app/cli/core/utils.py` — print_tag_table() 추가; render_output()에 list[Tag] 분기 추가
|
||||
- `tests/test_cli.py` — test_cli_tag_add_and_list, test_cli_tag_remove_flow 제거
|
||||
- `tests/test_tags_cli.py` — 신규: FakeTagRepo 기반 24개 테스트 (REQ-007 AC-1~9 커버)
|
||||
- `docs/data-model.md` — REQ-008 AC-1: Entity Tag 신규, portfolios.tag_ids, portfolio_tags 제거 반영
|
||||
- `docs/interface-contract.md` — REQ-008 AC-2: Tag Management API 섹션 신규 (7개 엔드포인트)
|
||||
- `docs/project-structure.md` — REQ-008 AC-3: tag.py/v1_tags.py/v1_portfolios.py/tag schemas 반영
|
||||
- `docs/runtime-flows.md` — REQ-008 AC-4: pfhub tag CLI 흐름, /v1/tags, /v1/assets|portfolios/{id}/tags 흐름 신규
|
||||
|
||||
## Execution Log
|
||||
1. planning.md, design.md status → confirmed; index.md, docs/batches/index.md → in-delivery
|
||||
2. app/cli/commands/tag.py 신규 생성 (태그 7개 커맨드, _with_repo 패턴)
|
||||
3. app/cli/main.py에 tag_app 등록
|
||||
4. app/cli/commands/portfolio.py에서 tag_app 관련 코드 완전 삭제
|
||||
5. app/cli/core/utils.py에 print_tag_table + list[Tag] render 분기 추가
|
||||
6. tests/test_cli.py에서 test_cli_tag_add_and_list, test_cli_tag_remove_flow 제거
|
||||
7. tests/test_tags_cli.py 신규 생성 (FakeTagRepo, 24개 테스트)
|
||||
8. pytest 247 passed 확인
|
||||
9. docs/data-model.md, interface-contract.md, project-structure.md, runtime-flows.md 갱신
|
||||
|
||||
## Validation
|
||||
- pytest 247 passed (8.47s) — regression 없음, 신규 24개 테스트 전체 통과
|
||||
|
||||
## Remaining Risks
|
||||
- 없음 — REQ-007, REQ-008 구현 및 검증 완료
|
||||
@@ -0,0 +1,35 @@
|
||||
# BAT-005: pfhub tag CLI 및 baseline docs 갱신
|
||||
|
||||
- Status: release-candidate
|
||||
- Profile: standard
|
||||
- Source Discovery: dcy-002
|
||||
- Included REQ: REQ-007, REQ-008
|
||||
- Excluded REQ: (없음 — dcy-002 잔여 2개 전부 포함)
|
||||
|
||||
## Summary
|
||||
- tags 마스터 기반의 `pfhub tag` 최상위 CLI 커맨드 그룹을 신규 추가하고(REQ-007), 기존 `pfhub portfolio tag add/list/remove` 커맨드를 제거한다. 이후 dcy-002 전체 구현 완료를 반영해 baseline 문서 4개를 갱신한다(REQ-008).
|
||||
- 이 배치가 완료되면 dcy-002(태그 소유권 및 적용 대상 정책) 관련 REQ-001~008 전체 구현이 종료된다.
|
||||
|
||||
## Scope
|
||||
- In Scope:
|
||||
- `app/cli/commands/tag.py` 신규 파일 — `pfhub tag create/list/deactivate/apply/remove` 커맨드
|
||||
- `app/cli/main.py` — `tag_app` 등록 추가 (REQ-007)
|
||||
- `app/cli/commands/portfolio.py` — `tag_app`, `tag_add`, `tag_list`, `tag_remove` 제거 (REQ-007)
|
||||
- `docs/data-model.md` — tags 테이블 스키마, portfolios.tag_ids, portfolio_tags 제거 반영 (REQ-008)
|
||||
- `docs/interface-contract.md` — /v1/tags, /v1/assets/{id}/tags, /v1/portfolios/{id}/tags 명세 추가 (REQ-008)
|
||||
- `docs/project-structure.md` — pfhub tag 커맨드 그룹 추가, pfhub portfolio tag 제거 반영 (REQ-008)
|
||||
- `docs/runtime-flows.md` — pfhub tag CLI 흐름, /v1/tags HTTP 흐름 추가 (REQ-008)
|
||||
- Out of Scope:
|
||||
- HTTP API 변경 (→ BAT-004 완료)
|
||||
- DB 스키마 변경 (→ BAT-003 완료)
|
||||
|
||||
## 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): `pfhub portfolio tag add/list/remove` 제거는 기존 운영 스크립트에 breaking change — 운영자 사전 고지 필요
|
||||
- REQ-007 선행 의존: REQ-004(tags HTTP API), REQ-005(태그 적용/해제 HTTP API) 모두 BAT-004(release-candidate) 완료 상태
|
||||
- REQ-008은 REQ-001~007 전체 구현 완료 후 일괄 반영 → 이 배치가 마지막 실행 단계
|
||||
@@ -0,0 +1,50 @@
|
||||
# BAT-005 Planning
|
||||
|
||||
- Status: confirmed
|
||||
- Batch: BAT-005
|
||||
- Profile: standard
|
||||
|
||||
## Included REQ
|
||||
- REQ-007: pfhub tag CLI 명령어 (pfhub tag create/list/deactivate/apply/remove + pfhub portfolio tag 제거)
|
||||
- REQ-008: 태그 모델 도입 baseline 문서 갱신 (data-model, interface-contract, project-structure, runtime-flows)
|
||||
|
||||
## Delivery Plan
|
||||
1. **REQ-007 — pfhub tag CLI 신규 구현**
|
||||
- `app/cli/commands/tag.py` 신규 파일 생성
|
||||
- `pfhub tag create <name>` — POST /v1/tags 호출 (trade-write)
|
||||
- `pfhub tag list [--inactive]` — GET /v1/tags 호출 (is_active 필터)
|
||||
- `pfhub tag deactivate <tag_id> --force` — PATCH /v1/tags/{tag_id}/deactivate 호출
|
||||
- `pfhub tag apply asset <asset_id> <tag_id>` — POST /v1/assets/{asset_id}/tags 호출 (admin)
|
||||
- `pfhub tag apply portfolio <portfolio_id> <tag_id>` — POST /v1/portfolios/{portfolio_id}/tags 호출
|
||||
- `pfhub tag remove asset <asset_id> <tag_id> --force` — DELETE /v1/assets/{asset_id}/tags/{tag_id} 호출
|
||||
- `pfhub tag remove portfolio <portfolio_id> <tag_id> --force` — DELETE /v1/portfolios/{portfolio_id}/tags/{tag_id} 호출
|
||||
- `app/cli/main.py` — tag_app 등록 (`app.add_typer(tag_app, name="tag")`)
|
||||
- `app/cli/commands/portfolio.py` — tag_app Typer, tag_add, tag_list, tag_remove 제거
|
||||
2. **REQ-008 — baseline 문서 갱신**
|
||||
- `docs/data-model.md` — tags 테이블 스키마, portfolios.tag_ids UUID[], portfolio_tags 제거 반영
|
||||
- `docs/interface-contract.md` — /v1/tags (POST/GET/PATCH), /v1/assets/{id}/tags (POST/DELETE), /v1/portfolios/{id}/tags (POST/DELETE) 명세 추가
|
||||
- `docs/project-structure.md` — app/cli/commands/tag.py 추가, pfhub tag 커맨드 그룹 추가, portfolio tag 제거 반영
|
||||
- `docs/runtime-flows.md` — pfhub tag CLI 흐름, /v1/tags HTTP 흐름 추가
|
||||
3. **전체 pytest 실행 — regression 확인**
|
||||
|
||||
## Dependencies
|
||||
- Internal:
|
||||
- BAT-003(release-candidate): tags 테이블, portfolios.tag_ids, Tag 도메인 모델 완료
|
||||
- BAT-004(release-candidate): POST/GET/PATCH /v1/tags, POST/DELETE /v1/assets|portfolios/{id}/tags HTTP API 완료
|
||||
- 기존 CLI 패턴(AppContext 통해 settings/repository 주입, --force 확인) 준수
|
||||
- External:
|
||||
- 없음
|
||||
|
||||
## Design Gate
|
||||
- Design Required Before Implementation: yes
|
||||
- Reason: 기존 `pfhub portfolio tag add/list/remove` 커맨드 제거 방식(완전 삭제 vs deprecated 표시), tag.py 커맨드 구조(apply/remove sub-group 설계), HTTP API 호출 방식(직접 repo 메서드 vs HTTP client) 결정 필요.
|
||||
|
||||
## Milestones
|
||||
1. REQ-007 CLI 구현 완료 + pfhub portfolio tag 제거 + tests
|
||||
2. REQ-008 baseline 문서 4개 갱신 완료
|
||||
3. 전체 pytest suite 통과 (regression 없음)
|
||||
|
||||
## Risks
|
||||
- R-1 (Medium): `pfhub portfolio tag add/list/remove` 제거는 운영 환경 breaking change — 기존 스크립트/자동화 사용 여부 사전 확인 권고
|
||||
- R-2 (Low): pfhub tag CLI는 HTTP API를 직접 호출하지 않고 repo 메서드를 통해 구현 — repo 메서드가 이미 BAT-003/004에서 완성되어 있으므로 위험 낮음
|
||||
- R-3 (Low): REQ-007 AC-9(pfhub portfolio tag 제거 확인) 테스트 패턴 — CLI 테스트 파일에서 명령어 부재 확인 방식 설계 필요
|
||||
@@ -0,0 +1,40 @@
|
||||
# BAT-005 Verification
|
||||
|
||||
- Status: release-candidate
|
||||
- Batch: BAT-005
|
||||
|
||||
## Acceptance Mapping
|
||||
|
||||
- REQ-007:
|
||||
- AC-1: `pfhub tag create "코스피200"` → 태그 생성 성공 → `test_tag_create_success`, `test_tag_create_with_user`
|
||||
- AC-2: `pfhub tag list` → 활성 태그 목록만 출력 → `test_tag_list_shows_active_tags`
|
||||
- AC-3: `pfhub tag list --inactive` → 비활성 포함 전체 목록 출력 → `test_tag_list_inactive_shows_all`
|
||||
- AC-4: `pfhub tag deactivate <tag_id> --force` → 비활성화; 재실행 멱등 → `test_tag_deactivate_with_force`, `test_tag_deactivate_already_inactive_is_idempotent`, `test_tag_deactivate_without_force_aborts`, `test_tag_deactivate_nonexistent_fails`
|
||||
- AC-5: `pfhub tag apply asset <asset_id> <tag_id>` → admin+active 태그만 허용 → `test_tag_apply_asset_success`, `test_tag_apply_asset_non_admin_tag_fails`, `test_tag_apply_asset_inactive_tag_fails`
|
||||
- AC-6: `pfhub tag apply portfolio <portfolio_id> <tag_id>` → admin 태그 또는 소유자 태그만 허용 → `test_tag_apply_portfolio_admin_tag_success`, `test_tag_apply_portfolio_owner_tag_success`, `test_tag_apply_portfolio_policy_violation_fails`
|
||||
- AC-7: `pfhub tag remove asset <asset_id> <tag_id> --force` → 태그 제거 → `test_tag_remove_asset_with_force`, `test_tag_remove_asset_without_force_aborts`
|
||||
- AC-8: `pfhub tag remove portfolio <portfolio_id> <tag_id> --force` → 태그 제거 → `test_tag_remove_portfolio_with_force`
|
||||
- AC-9: `pfhub portfolio tag add/list/remove` → 명령어 없음(Typer error) → `test_portfolio_tag_subcommand_does_not_exist`, `test_portfolio_tag_list_does_not_exist`, `test_portfolio_tag_remove_does_not_exist`
|
||||
|
||||
- REQ-008:
|
||||
- AC-1: docs/data-model.md에 tags 테이블 스키마, portfolios.tag_ids UUID[], portfolio_tags 제거 반영 → 파일 내용 검증 완료
|
||||
- AC-2: docs/interface-contract.md에 /v1/tags (POST/GET/PATCH), /v1/assets/{id}/tags, /v1/portfolios/{id}/tags 명세 포함 → 파일 내용 검증 완료
|
||||
- AC-3: docs/project-structure.md에 pfhub tag 커맨드 그룹, app/cli/commands/tag.py 반영 → 파일 내용 검증 완료
|
||||
- AC-4: docs/runtime-flows.md에 pfhub tag CLI 흐름, /v1/tags HTTP 흐름, /v1/assets|portfolios/{id}/tags 흐름 포함 → 파일 내용 검증 완료
|
||||
|
||||
## Evidence
|
||||
- Test:
|
||||
- pytest 실행 결과: 247 passed (tests/test_tags_cli.py 24개 신규 포함, tests/test_cli.py 기존 32개 유지)
|
||||
- 신규 테스트 파일: tests/test_tags_cli.py (REQ-007 AC-1~9 커버, FakeTagRepo 기반)
|
||||
- regression: test_cli_tag_add_and_list, test_cli_tag_remove_flow 제거 (pfhub portfolio tag 커맨드 제거와 함께)
|
||||
- Manual:
|
||||
- docs/data-model.md: Entity Tag 섹션 신규, portfolios.tag_ids UUID[] 추가, portfolio_tags 제거 반영
|
||||
- docs/interface-contract.md: Tag Management API 섹션 신규 (7개 엔드포인트 명세)
|
||||
- docs/project-structure.md: app/cli/commands/tag.py, app/api/v1_tags.py/v1_portfolios.py, v1.py tag schemas 반영
|
||||
- docs/runtime-flows.md: pfhub tag CLI 흐름, v1 tag management 흐름, v1 tag apply/remove 흐름 신규
|
||||
- Logs:
|
||||
- `.venv/bin/python -m pytest -q` → 247 passed in 8.47s
|
||||
|
||||
## Result
|
||||
- Ready for Release: true
|
||||
- Blocking Issues: 없음
|
||||
@@ -11,3 +11,5 @@
|
||||
| BAT-001 | standard | verified | REQ-009, REQ-011, REQ-014 | dcy-001 | [bat-001](./bat-001_20260519_asset-catalog-db-foundation/) |
|
||||
| BAT-002 | standard | release-candidate | REQ-010, REQ-012, REQ-013, REQ-015 | dcy-001 | [bat-002](./bat-002_20260519_asset-catalog-api-completion/) |
|
||||
| BAT-003 | standard | release-candidate | REQ-001, REQ-002, REQ-003, REQ-006 | dcy-002 | [bat-003](./bat-003_20260519_tags-db-foundation/) |
|
||||
| BAT-004 | standard | release-candidate | REQ-004, REQ-005 | dcy-002 | [bat-004](./bat-004_20260520_tags-http-api/) |
|
||||
| BAT-005 | standard | release-candidate | REQ-007, REQ-008 | dcy-002 | [bat-005](./bat-005_20260520_pfhub-tag-cli-and-docs/) |
|
||||
|
||||
+44
-5
@@ -4,8 +4,8 @@
|
||||
- Baseline Seed: .stagepilot/bootstrap/baseline.yaml
|
||||
- Status: confirmed
|
||||
- Owner: repository-maintainers
|
||||
- Last Updated (KST): 2026-05-13 00:00
|
||||
- Source Discovery / Batch: bootstrap-baseline, bat-001, bat-006, bat-008, bat-010
|
||||
- Last Updated (KST): 2026-05-20 00:00
|
||||
- Source Discovery / Batch: bootstrap-baseline, bat-001, bat-006, bat-008, bat-010, bat-003, bat-004, bat-005
|
||||
|
||||
## Project Summary
|
||||
- PortfolioHub exposes portfolio management and trading ledger workflows through a FastAPI service and a companion CLI.
|
||||
@@ -27,13 +27,49 @@
|
||||
|
||||
## Model Summary
|
||||
|
||||
- Portfolio :: owned investment portfolio with balances, strategy references, tags, comments, and ledger history
|
||||
- Portfolio :: owned investment portfolio with balances, strategy references, tag_ids, comments, and ledger history
|
||||
- User :: authenticated principal that owns portfolios and receives client or service tokens
|
||||
- Daemon :: stable logical identity for a trading daemon process; registered per user; bound to service tokens and assigned to assets
|
||||
- AuthToken :: issued client or service token with permission, runtime_id, and optional daemon_id binding
|
||||
- BrokerCredential :: encrypted broker API credentials per user-broker pair
|
||||
- LedgerTrade :: idempotent trade execution record keyed by (asset_id, order_id, execution_id)
|
||||
- Asset :: catalog entry representing a tradeable asset; portfolios reference assets via FK
|
||||
- Tag :: master tag record with ownership (created_by_user_id), admin flag, and active/inactive lifecycle
|
||||
|
||||
## Entity: `Tag`
|
||||
|
||||
- Purpose: represent a named master tag with ownership and admin classification; applied to portfolios via tag_ids UUID[] column
|
||||
- Source of Truth: tags table (added by BAT-003 migration 20260519_0017)
|
||||
- Lifecycle: created (is_active=true) → optionally deactivated (is_active=false); no hard delete
|
||||
|
||||
### Key Fields
|
||||
|
||||
- tag_id :: uuid :: server-generated UUID primary key (gen_random_uuid())
|
||||
- name :: text UNIQUE :: tag label; normalized to lowercase and stripped; unique across all tags
|
||||
- created_by_user_id :: text :: user_id of the creator; used for portfolio tag apply policy
|
||||
- is_admin_tag :: boolean :: if true, tag was created with admin permission; can be applied to any portfolio regardless of ownership
|
||||
- is_active :: boolean :: inactive tags cannot be applied; existing tag_ids arrays are not modified on deactivation
|
||||
- created_at :: timestamptz :: creation timestamp (server_default=NOW())
|
||||
|
||||
### Uniqueness Constraints
|
||||
|
||||
- tags_name_key :: UNIQUE (name) :: one tag entry per unique lowercase name
|
||||
|
||||
### Relationships
|
||||
|
||||
- Portfolio :: portfolios.tag_ids UUID[] soft-references tags.tag_id (no FK constraint); array_append/array_remove manage membership
|
||||
|
||||
### State Rules
|
||||
|
||||
- Only is_active=true tags can be applied via POST /v1/assets/{id}/tags or POST /v1/portfolios/{id}/tags.
|
||||
- Deactivating a tag does not remove it from portfolios.tag_ids arrays (lazy cleanup).
|
||||
- Admin tags (is_admin_tag=true) can be applied to any portfolio; non-admin tags can only be applied to portfolios owned by the tag creator.
|
||||
- Asset tag application requires is_admin_tag=true (asset tags are always admin-controlled).
|
||||
|
||||
### Removed Entity: `portfolio_tags` (BAT-003)
|
||||
|
||||
- The old `portfolio_tags` table (string-keyed free-form tags per portfolio) was replaced by `portfolios.tag_ids UUID[]` in BAT-003.
|
||||
- `portfolio_tags` DROP was included in migration 20260519_0017.
|
||||
|
||||
## Entity: `Asset`
|
||||
|
||||
@@ -83,6 +119,7 @@
|
||||
- cash_balance :: double precision :: current cash snapshot after the latest ledger event
|
||||
- asset_quantity :: double precision :: current holdings snapshot after the latest ledger event
|
||||
- managed_by_daemon_id :: text|null :: soft reference to daemons.id; controls daemon-scoped API access; NULL = unassigned (fail-closed for ledger writes and asset listing); added by migration 20260513_0016
|
||||
- tag_ids :: uuid[] :: array of tag UUIDs from the tags master table; replaces the old string-based portfolio_tags table (BAT-003 migration 20260519_0017); default empty array
|
||||
|
||||
### Uniqueness Constraints
|
||||
|
||||
@@ -94,14 +131,15 @@
|
||||
- User :: owner_user_id links a portfolio to its owning user when ownership is assigned
|
||||
- Daemon :: managed_by_daemon_id links a portfolio to exactly one daemon; NULL means unassigned
|
||||
- StrategySet :: active_strategy_id and avabile_strategies connect reusable strategy definitions
|
||||
- LedgerEntry, PortfolioComment, PortfolioTag :: child records are keyed by portfolio_id (UUID)
|
||||
- LedgerEntry, PortfolioComment :: child records are keyed by portfolio_id (UUID)
|
||||
- Tag :: tag_ids UUID[] references tags.tag_id (no FK — soft reference for flexibility)
|
||||
|
||||
### State Rules
|
||||
|
||||
- Portfolio names are required, must be unique per owner_user_id, and are validated at the schema layer before persistence.
|
||||
- portfolio_id is server-generated at creation; clients must never supply or assume a specific portfolio_id value.
|
||||
- Balance-changing commands must update snapshot fields and append a ledger row together.
|
||||
- Tags are unique per portfolio and comments are retrieved in created_at descending order.
|
||||
- tag_ids stores UUID references to active tags; duplicates are prevented by conditional array_append; comments are retrieved in created_at descending order.
|
||||
|
||||
## Entity: `User`
|
||||
|
||||
@@ -275,6 +313,7 @@
|
||||
|
||||
## Change Log
|
||||
|
||||
- 2026-05-20 KST: bat-005 — REQ-008 AC-1: added Entity: Tag (tags master table, BAT-003 migration 20260519_0017); updated Portfolio.tag_ids UUID[] field; removed portfolio_tags table note; updated Portfolio Relationships and State Rules; updated Model Summary.
|
||||
- 2026-05-19 KST: bat-002 — REQ-015: added Entity: Asset (asset catalog with symbol UNIQUE, markets[], status); updated Portfolio.asset_id FK and symbol/market field notes.
|
||||
- 2026-05-13 KST: bat-010 — REQ-009/023/028: added Entity: Daemon; added portfolios.managed_by_daemon_id; added auth_tokens.daemon_id; updated AuthToken State Rules (runtime_id vs daemon_id distinction, fail-closed policy); updated Portfolio Relationships; updated Model Summary.
|
||||
- 2026-05-11 KST: bat-008 — REQ-011 AC-2: added portfolios.market column; added Entity: AuthToken (runtime_id field); added Entity: BrokerCredential; added Entity: LedgerTrade; updated Model Summary.
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
- Baseline Seed: .stagepilot/bootstrap/baseline.yaml
|
||||
- Status: confirmed
|
||||
- Owner: repository-maintainers
|
||||
- Last Updated (KST): 2026-05-13 00:00
|
||||
- Source Discovery / Batch: bootstrap-baseline, bat-001, bat-006, bat-008, bat-010
|
||||
- Last Updated (KST): 2026-05-20 00:00
|
||||
- Source Discovery / Batch: bootstrap-baseline, bat-001, bat-006, bat-008, bat-010, bat-003, bat-004, bat-005
|
||||
- Primary Runtime: mixed
|
||||
|
||||
## Project Summary
|
||||
@@ -69,10 +69,11 @@
|
||||
|
||||
### Inputs
|
||||
|
||||
- pfhub subcommands :: include user, portfolio, strategy, service-token, daemon, credential, health-check, init-db, and migrate-db command families
|
||||
- pfhub subcommands :: include user, portfolio, strategy, tag, service-token, daemon, credential, health-check, init-db, and migrate-db command families
|
||||
- Flags and options :: include --format table|json, --force confirmations, and operation-specific mutation arguments
|
||||
- Environment configuration :: app/core/config.py resolves PORTFOLIOHUB_* and TIMESCALEDB_* values before command execution
|
||||
- Portfolio identification (bat-006) :: all portfolio mutation commands accept `<name>` (positional) + `--user <username>` instead of raw portfolio_id; `comment` and `tag` sub-commands still accept portfolio_id (UUID) directly
|
||||
- Portfolio identification (bat-006) :: all portfolio mutation commands accept `<name>` (positional) + `--user <username>` instead of raw portfolio_id; `comment` sub-commands accept portfolio_id (UUID) directly
|
||||
- Tag commands (bat-005) :: `pfhub tag create/list/deactivate/apply/remove` operate on the tags master table; `pfhub portfolio tag add/list/remove` has been removed (breaking change)
|
||||
|
||||
### Outputs
|
||||
|
||||
@@ -274,6 +275,72 @@
|
||||
- After BAT-006, all `asset_id` values in GET /v1/assets responses change from TEXT to UUID format.
|
||||
- trex-basic consumers must handle UUID-format asset_id before the coordinated deployment is completed.
|
||||
|
||||
## Interface: `Tag Management API (BAT-004)`
|
||||
|
||||
- Type: http-api
|
||||
- Consumers / Producers: authenticated operators and admin services
|
||||
- Purpose: manage tag master records and apply/remove tags on assets and portfolios
|
||||
- Stability: internal (versioned under /v1)
|
||||
|
||||
### Authentication
|
||||
|
||||
- All /v1/tags, /v1/assets/{id}/tags, /v1/portfolios/{id}/tags endpoints require `Authorization: Bearer <token>`.
|
||||
- Token must be DB-issued (`token_source="db"`); env tokens rejected with HTTP 403.
|
||||
|
||||
### Endpoint: `POST /v1/tags`
|
||||
|
||||
- Auth: DB token required; trade-write or admin permission
|
||||
- Body: `{"name": "<str>"}` (min_length=1; normalized to lowercase + stripped)
|
||||
- Response: HTTP 201 `TagItem` `{"tag_id": "<uuid>", "name": "<str>", "is_admin_tag": <bool>, "is_active": true, "created_at": "<ISO8601|null>"}`
|
||||
- is_admin_tag is set to true when caller has admin permission; false for trade-write
|
||||
- Error: HTTP 409 if tag name already exists; HTTP 403 if env token
|
||||
|
||||
### Endpoint: `GET /v1/tags`
|
||||
|
||||
- Auth: DB token required; read permission
|
||||
- Query Params: `?include_inactive=true` (default false — returns only active tags)
|
||||
- Response: HTTP 200 `TagListResponse` `{"tags": [TagItem], "total": <int>}`
|
||||
- Error: HTTP 403 if env token
|
||||
|
||||
### Endpoint: `PATCH /v1/tags/{tag_id}/deactivate`
|
||||
|
||||
- Auth: DB token required; read permission (admin or tag creator)
|
||||
- Path Param: `tag_id` — must be a valid tag UUID
|
||||
- Body: none
|
||||
- Response: HTTP 200 `TagItem` with `is_active=false`; idempotent (already-inactive returns 200 immediately)
|
||||
- Error: HTTP 404 if tag not found; HTTP 403 if caller is not admin and not tag creator
|
||||
|
||||
### Endpoint: `POST /v1/assets/{asset_id}/tags`
|
||||
|
||||
- Auth: DB token required; admin permission
|
||||
- Path Param: `asset_id` — maps to portfolio_id (v1 convention: asset_id = portfolio_id)
|
||||
- Body: `{"tag_id": "<uuid>"}`
|
||||
- Response: HTTP 200 `TagApplyResponse` `{"tag_id": "<uuid>", "status": "applied"}`
|
||||
- Policy: tag must have is_admin_tag=true AND is_active=true; non-admin-tag → HTTP 403; inactive → HTTP 403
|
||||
- Error: HTTP 404 if asset or tag not found; HTTP 403 if policy violation or env token
|
||||
|
||||
### Endpoint: `DELETE /v1/assets/{asset_id}/tags/{tag_id}`
|
||||
|
||||
- Auth: DB token required; admin permission
|
||||
- Body: none
|
||||
- Response: HTTP 200 `TagApplyResponse` `{"tag_id": "<uuid>", "status": "removed"}`
|
||||
- Error: HTTP 404 if asset not found; HTTP 403 if not admin or env token
|
||||
|
||||
### Endpoint: `POST /v1/portfolios/{portfolio_id}/tags`
|
||||
|
||||
- Auth: DB token required; trade-write or admin permission
|
||||
- Body: `{"tag_id": "<uuid>"}`
|
||||
- Response: HTTP 200 `TagApplyResponse` `{"tag_id": "<uuid>", "status": "applied"}`
|
||||
- Policy: tag must have is_active=true; caller must be portfolio owner or admin; non-admin non-owner-created tag → HTTP 403
|
||||
- Error: HTTP 404 if portfolio or tag not found; HTTP 403 if policy violation or env token
|
||||
|
||||
### Endpoint: `DELETE /v1/portfolios/{portfolio_id}/tags/{tag_id}`
|
||||
|
||||
- Auth: DB token required; trade-write or admin permission
|
||||
- Body: none
|
||||
- Response: HTTP 200 `TagApplyResponse` `{"tag_id": "<uuid>", "status": "removed"}`
|
||||
- Error: HTTP 404 if portfolio not found; HTTP 403 if caller is not owner or admin
|
||||
|
||||
## Shared Constraints
|
||||
|
||||
- Token permission levels gate both HTTP and CLI mutations consistently.
|
||||
@@ -294,6 +361,7 @@
|
||||
|
||||
## Change Log
|
||||
|
||||
- 2026-05-20 KST: bat-005 — REQ-008 AC-2: added Tag Management API section (POST/GET/PATCH /v1/tags, POST/DELETE /v1/assets/{id}/tags, POST/DELETE /v1/portfolios/{id}/tags); updated CLI surface (pfhub tag, removed pfhub portfolio tag); updated CLI inputs.
|
||||
- 2026-05-19 KST: bat-002 — REQ-010/012/013/015: added admin catalog endpoints (POST/PATCH /v1/assets, PATCH /v1/assets/{id}/deactivate); rewrote GET /v1/assets authentication to support service/user/admin token branching; added market filter param; removed market field from AssetItem; added AssetCatalogResponse schema.
|
||||
- 2026-05-13 KST: bat-010 — REQ-006/007/008/011/023: added daemon authorization model section; updated GET /v1/assets (daemon-scoped filter, NULL 403); updated GET /v1/user_info (daemon_id field, NULL runtime_id → 200+null diagnostic); updated POST /v1/assets/{asset_id}/ledger/trades (daemon assignment authorization, fail-closed policy); added daemon CLI subcommand to CLI surface; updated v1 purpose and source discovery.
|
||||
- 2026-05-11 KST: bat-008 — REQ-011 AC-1: expanded v1 Consumer API section with authentication contract, detailed request/response schemas for GET /v1/assets, GET /v1/user_info, POST /v1/assets/{asset_id}/ledger/trades; updated GET /health response schema (REQ-004); added idempotency contract.
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
- Baseline Seed: .stagepilot/bootstrap/baseline.yaml
|
||||
- Status: confirmed
|
||||
- Owner: repository-maintainers
|
||||
- Last Updated (KST): 2026-05-11 00:00
|
||||
- Source Discovery / Batch: bootstrap-baseline, bat-001, bat-002, bat-006, bat-007
|
||||
- Last Updated (KST): 2026-05-20 00:00
|
||||
- Source Discovery / Batch: bootstrap-baseline, bat-001, bat-002, bat-006, bat-007, bat-003, bat-004, bat-005
|
||||
|
||||
## Project Summary
|
||||
- PortfolioHub exposes portfolio management and trading ledger workflows through a FastAPI service and a companion CLI.
|
||||
@@ -27,7 +27,7 @@
|
||||
- Type: mixed
|
||||
- Planned Entry Points:
|
||||
- uvicorn app.main:app :: serve the authenticated HTTP API, OpenAPI docs, and startup bootstrap hooks
|
||||
- pfhub :: run operational CLI commands for health, users, portfolios, strategies, service tokens, and broker credentials
|
||||
- pfhub :: run operational CLI commands for health, users, portfolios, strategies, tags, service tokens, and broker credentials
|
||||
- alembic upgrade head :: apply ordered database schema migrations for persistent environments
|
||||
|
||||
## Purpose
|
||||
@@ -82,12 +82,13 @@
|
||||
- .github/
|
||||
- Hosts StagePilot skills, document templates, and helper scripts such as stagepilot-doctor and stagepilot-bootstrap-seed.
|
||||
- app/api/
|
||||
- Defines FastAPI route surfaces for health, authentication, users, strategy sets, and portfolios.
|
||||
- v1 route surfaces: `v1_assets.py` (GET /v1/assets token-branched, POST /v1/assets admin catalog create, PATCH /v1/assets/{id} admin update, PATCH /v1/assets/{id}/deactivate admin deactivate), `v1_user_info.py` (GET /v1/user_info), `v1_ledger.py` (POST /v1/assets/{asset_id}/ledger/trades).
|
||||
- GET /v1/assets supports service/user/admin token branching; admin catalog CRUD requires admin token.
|
||||
- Defines FastAPI route surfaces for health, authentication, users, strategy sets, portfolios, and tags.
|
||||
- v1 route surfaces: `v1_assets.py` (GET /v1/assets token-branched, POST /v1/assets admin catalog, PATCH /v1/assets/{id}, PATCH /v1/assets/{id}/deactivate, POST/DELETE /v1/assets/{id}/tags), `v1_tags.py` (POST/GET /v1/tags, PATCH /v1/tags/{id}/deactivate), `v1_portfolios.py` (POST/DELETE /v1/portfolios/{id}/tags), `v1_user_info.py` (GET /v1/user_info), `v1_ledger.py` (POST /v1/assets/{asset_id}/ledger/trades).
|
||||
- GET /v1/assets supports service/user/admin token branching; admin catalog CRUD requires admin token; tag endpoints added by BAT-004.
|
||||
- app/cli/
|
||||
- Defines the Typer entrypoint, command groups, output helpers, and operator-facing command flows.
|
||||
- Command groups: `asset` (catalog CRUD via admin), `portfolio`, `strategy`, `user`, `service-token`, `credential` (broker credential management), and health commands (`health-check`, `init-db`, `migrate-db`).
|
||||
- Command groups: `asset` (catalog CRUD via admin), `portfolio`, `strategy`, `tag` (tag master CRUD and apply/remove; BAT-005), `user`, `service-token`, `credential` (broker credential management), and health commands (`health-check`, `init-db`, `migrate-db`).
|
||||
- `app/cli/commands/tag.py` (BAT-005): `pfhub tag create/list/deactivate/apply asset|portfolio/remove asset|portfolio`; direct repo method calls following existing AppContext pattern. `pfhub portfolio tag add/list/remove` removed (breaking change, BAT-005).
|
||||
- app/core/
|
||||
- Owns configuration loading, auth guards, security helpers, and shared error handling.
|
||||
- `encryption.py`: Fernet-based credential payload encrypt/decrypt helpers. Reads `CREDENTIAL_ENCRYPTION_KEY` env var via `Settings.require_credential_encryption_key()`.
|
||||
@@ -98,7 +99,7 @@
|
||||
- Key methods added in bat-006: `get_portfolio_by_name_and_user(owner_user_id, name) -> Portfolio | None` for (owner_user_id, name) → portfolio_id (UUID) resolution used by CLI commands.
|
||||
- app/schemas/
|
||||
- Defines request and response schemas for HTTP contracts.
|
||||
- `v1.py`: Contract schemas for v1 API (AssetItem, AssetsResponse, UserInfoResponse, BrokerItem, LedgerTradeRequest/Response, AssetCatalogCreateRequest, AssetCatalogUpdateRequest, AssetCatalogResponse).
|
||||
- `v1.py`: Contract schemas for v1 API (AssetItem, AssetsResponse, UserInfoResponse, BrokerItem, LedgerTradeRequest/Response, AssetCatalogCreateRequest, AssetCatalogUpdateRequest, AssetCatalogResponse, TagItem, TagCreateRequest, TagListResponse, TagApplyRequest, TagApplyResponse; tag schemas added by BAT-004).
|
||||
- app/services/
|
||||
- Implements business logic for auth, portfolios, strategy sets, and users.
|
||||
- migrations/
|
||||
@@ -161,6 +162,7 @@
|
||||
|
||||
## Change Log
|
||||
|
||||
- 2026-05-20 KST: bat-005 — REQ-008 AC-3: added app/cli/commands/tag.py (pfhub tag command group); updated app/api/ to reflect v1_tags.py, v1_portfolios.py, v1_assets.py tag endpoints; updated app/schemas/v1.py tag schemas; updated pfhub entry point (tags); noted pfhub portfolio tag removal.
|
||||
- 2026-05-11 KST: bat-007 — Added "Schema Ownership Boundary" section documenting ensure_schema() vs Alembic ownership zones, allowed/forbidden execution sequences, and legacy scope exclusion.
|
||||
- 2026-05-11 KST: bat-006 — Added get_portfolio_by_name_and_user() to repositories section; noted that portfolio_id is now a server-generated UUID.
|
||||
- 2026-04-30 KST: Bootstrap baseline created from repository observation for initial StagePilot adoption.
|
||||
+76
-3
@@ -4,8 +4,8 @@
|
||||
- Baseline Seed: .stagepilot/bootstrap/baseline.yaml
|
||||
- Status: confirmed
|
||||
- Owner: repository-maintainers
|
||||
- Last Updated (KST): 2026-05-13 00:00
|
||||
- Source Discovery / Batch: bootstrap-baseline, bat-001, bat-006, bat-007, bat-008, bat-010
|
||||
- Last Updated (KST): 2026-05-20 00:00
|
||||
- Source Discovery / Batch: bootstrap-baseline, bat-001, bat-006, bat-007, bat-008, bat-010, bat-003, bat-004, bat-005
|
||||
- Primary Runtime: mixed
|
||||
|
||||
## Project Summary
|
||||
@@ -22,7 +22,7 @@
|
||||
## Covered Entry Points
|
||||
|
||||
- uvicorn app.main:app :: serve the authenticated HTTP API, OpenAPI docs, and startup bootstrap hooks
|
||||
- pfhub :: run operational CLI commands for health, users, portfolios, strategies, service tokens, and daemon registry
|
||||
- pfhub :: run operational CLI commands for health, users, portfolios, strategies, tags, service tokens, and daemon registry
|
||||
- alembic upgrade head :: apply ordered database schema migrations for persistent environments
|
||||
|
||||
## Shared Components
|
||||
@@ -50,6 +50,33 @@
|
||||
4. Read commands print table or JSON output, while write commands require explicit confirmation unless --force is supplied.
|
||||
5. Command failures surface as validation errors, feature-not-ready errors, or persistence and auth exceptions.
|
||||
|
||||
## Flow: `pfhub tag CLI Command Execution`
|
||||
|
||||
1. `pfhub tag create <name> [--user <username>] [--admin]`
|
||||
- Resolves `--user` to `user_id` via `repo.get_user_by_username()`; raises ValueError if not found.
|
||||
- Calls `repo.create_tag(name=name.strip(), is_admin_tag=--admin, created_by_user_id=resolved_id)`.
|
||||
- Duplicate name → repo ValueError → console error, exit 1.
|
||||
2. `pfhub tag list [--inactive]`
|
||||
- Calls `repo.list_tags_master(include_inactive=--inactive)`.
|
||||
- Renders Tag table (table format) or JSON array (--format json).
|
||||
3. `pfhub tag deactivate <tag_id> --force`
|
||||
- Without `--force`: prompts for confirmation; abort on refusal (exit 0).
|
||||
- Calls `repo.get_tag_by_id(tag_id)`; if not found → ValueError → exit 1.
|
||||
- If already inactive → prints "already inactive", returns (idempotent, exit 0).
|
||||
- Calls `repo.deactivate_tag(tag_id)`.
|
||||
4. `pfhub tag apply asset <asset_id> <tag_id>`
|
||||
- Validates tag: get_tag_by_id → not found → ValueError; not is_admin_tag → ValueError; not is_active → ValueError.
|
||||
- Calls `repo.append_portfolio_tag_id(asset_id, tag_id)`.
|
||||
5. `pfhub tag apply portfolio <portfolio_id> <tag_id>`
|
||||
- Validates tag: get_tag_by_id → not found → ValueError; not is_active → ValueError.
|
||||
- Resolves portfolio: repo.get(portfolio_id) → not found → ValueError.
|
||||
- Policy check: tag.is_admin_tag OR tag.created_by_user_id == portfolio.owner_user_id; else → ValueError.
|
||||
- Calls `repo.append_portfolio_tag_id(portfolio_id, tag_id)`.
|
||||
6. `pfhub tag remove asset <asset_id> <tag_id> --force` / `pfhub tag remove portfolio <portfolio_id> <tag_id> --force`
|
||||
- Without `--force`: prompts for confirmation; abort on refusal (exit 0).
|
||||
- Calls `repo.remove_portfolio_tag_id(...)`.
|
||||
7. `pfhub portfolio tag add/list/remove` — **removed in BAT-005**; Typer returns exit code 2 (no such command).
|
||||
|
||||
## Flow: `CLI Portfolio Command — Name+User Resolution`
|
||||
|
||||
1. All portfolio mutation commands (`status`, `create`, `update`, `buy`, `sell`, `invest`, `transfer`, `reset`, `delete`) accept a `<name>` positional argument and `--user <username>` option.
|
||||
@@ -68,6 +95,51 @@
|
||||
- Request validation and auth resolution should happen before persistence mutation or response rendering.
|
||||
- The health endpoint remains unauthenticated so infrastructure can probe readiness without a token.
|
||||
|
||||
## Flow: `v1 API — Tag Management (POST/GET/PATCH /v1/tags)`
|
||||
|
||||
1. **POST /v1/tags** (create tag):
|
||||
- DB token required; trade-write or admin permission.
|
||||
- Env token → HTTP 403.
|
||||
- `name` is stripped and lowercased; `is_admin_tag` set if caller's permission is admin.
|
||||
- `repo.create_tag(name, is_admin_tag, created_by_user_id=auth.user_id)` → UniqueViolation → HTTP 409.
|
||||
- Returns HTTP 201 `TagItem`.
|
||||
2. **GET /v1/tags** (list tags):
|
||||
- DB token required; read permission.
|
||||
- `?include_inactive=true` → `repo.list_tags_master(include_inactive=True)`; default returns active only.
|
||||
- Returns HTTP 200 `TagListResponse {"tags": [...], "total": N}`.
|
||||
3. **PATCH /v1/tags/{tag_id}/deactivate**:
|
||||
- DB token required; read permission.
|
||||
- `repo.get_tag_by_id(tag_id)` → None → HTTP 404.
|
||||
- Caller must be admin or tag creator (`auth.user_id == tag.created_by_user_id`); else → HTTP 403.
|
||||
- If already inactive → return HTTP 200 immediately (idempotent).
|
||||
- `repo.deactivate_tag(tag_id)` → HTTP 200 `TagItem`.
|
||||
|
||||
## Flow: `v1 API — Tag Apply/Remove (POST/DELETE /v1/assets/{id}/tags and /v1/portfolios/{id}/tags)`
|
||||
|
||||
1. **POST /v1/assets/{asset_id}/tags** (apply tag to asset — admin only):
|
||||
- DB token required; admin permission (`require_admin`).
|
||||
- `repo.get(asset_id)` → ValueError → HTTP 404.
|
||||
- `repo.get_tag_by_id(tag_id)` → None → HTTP 404.
|
||||
- `not tag.is_admin_tag` → HTTP 403; `not tag.is_active` → HTTP 403.
|
||||
- `repo.append_portfolio_tag_id(asset_id, tag_id)` → HTTP 200 `{"tag_id": ..., "status": "applied"}`.
|
||||
2. **DELETE /v1/assets/{asset_id}/tags/{tag_id}** (remove tag from asset — admin only):
|
||||
- DB token required; admin permission.
|
||||
- `repo.get(asset_id)` → ValueError → HTTP 404.
|
||||
- `repo.remove_portfolio_tag_id(asset_id, tag_id)` → HTTP 200 `{"tag_id": ..., "status": "removed"}`.
|
||||
3. **POST /v1/portfolios/{portfolio_id}/tags** (apply tag to portfolio — owner or admin):
|
||||
- DB token required; trade-write or admin permission.
|
||||
- `repo.get(portfolio_id)` → ValueError → HTTP 404.
|
||||
- Caller must be portfolio owner or admin; else → HTTP 403.
|
||||
- `repo.get_tag_by_id(tag_id)` → None → HTTP 404.
|
||||
- `not tag.is_active` → HTTP 403.
|
||||
- Policy: `tag.is_admin_tag OR tag.created_by_user_id == portfolio.owner_user_id`; else → HTTP 403.
|
||||
- `repo.append_portfolio_tag_id(portfolio_id, tag_id)` → HTTP 200 `{"tag_id": ..., "status": "applied"}`.
|
||||
4. **DELETE /v1/portfolios/{portfolio_id}/tags/{tag_id}** (remove tag from portfolio — owner or admin):
|
||||
- DB token required; trade-write or admin permission.
|
||||
- `repo.get(portfolio_id)` → ValueError → HTTP 404.
|
||||
- Caller must be portfolio owner or admin; else → HTTP 403.
|
||||
- `repo.remove_portfolio_tag_id(portfolio_id, tag_id)` → HTTP 200 `{"tag_id": ..., "status": "removed"}`.
|
||||
|
||||
## Flow: `v1 API — DB Token Authentication`
|
||||
|
||||
1. v1 endpoint receives Bearer token from Authorization header.
|
||||
@@ -209,6 +281,7 @@
|
||||
|
||||
## Change Log
|
||||
|
||||
- 2026-05-20 KST: bat-005 — REQ-008 AC-4: added pfhub tag CLI flow; added v1 tag management flow (POST/GET/PATCH /v1/tags); added v1 tag apply/remove flow (/v1/assets/{id}/tags, /v1/portfolios/{id}/tags); updated Covered Entry Points (tags).
|
||||
- 2026-05-13 KST: bat-010 — REQ-006/007/008/009/023/005/012: updated GET /v1/assets flow (daemon-id guard, daemon-scoped filter); updated GET /v1/user_info flow (daemon_id diagnostic, NULL → 200+null); updated POST ledger/trades flow (3-step daemon assignment auth, fail-closed); added "Daemon Registry — Create and Bind Service Token" flow; added "Service-Token Rotate — daemon_id Invariant" flow; updated Alembic/ensure_schema boundary (BAT-010 migration 20260513_0016); updated Covered Entry Points (daemon subcommand).
|
||||
- 2026-05-11 KST: bat-008 — REQ-011 AC-3 confirmed: v1 API flows and runtime_id issuance flow already present from bat-001; Source Discovery/Batch header updated.
|
||||
- 2026-05-11 KST: bat-007 — Added "Local DB Initialization and Validation" flow (Scenario A/B, repeatable run criteria); added "Alembic vs ensure_schema() Schema Ownership Boundary" flow.
|
||||
|
||||
Reference in New Issue
Block a user