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
+17 -11
View File
@@ -4,8 +4,8 @@
- Baseline Seed: .stagepilot/bootstrap/baseline.yaml
- Status: confirmed
- Owner: repository-maintainers
- 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
- Last Updated (KST): 2026-05-21 00:00
- Source Discovery / Batch: bootstrap-baseline, bat-001, bat-006, bat-008, bat-010, bat-003, bat-004, bat-005, BAT-006(dcy-004), BAT-007(dcy-004), BAT-008(dcy-004)
## Project Summary
- PortfolioHub exposes portfolio management and trading ledger workflows through a FastAPI service and a companion CLI.
@@ -30,7 +30,7 @@
- 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
- AuthToken :: issued service token (or session token for login flows) with permission, runtime_id, and optional daemon_id binding; client token type removed (REQ-016/BAT-006)
- 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
@@ -114,12 +114,13 @@
- name :: text :: human-readable portfolio name; unique per owner_user_id; validated against [a-zA-Z0-9._:-]{1,100}
- asset_id :: uuid :: FK → assets.asset_id; catalog reference for the traded asset; BAT-001 migrated symbol/market columns to this FK
- symbol :: text :: display symbol populated via JOIN on assets (read-only, not stored in portfolios)
- status :: text :: `"enabled"` (active, default) | `"disabled"` (soft-deleted); set to `"disabled"` by `DELETE /v1/portfolios/{id}`; only `status` can be mutated on a disabled portfolio (restoration via PATCH); all other writes → HTTP 409
- owner_user_id :: text|null :: owning user reference for attribution and authorization
- active_strategy_id :: text|null :: currently selected strategy-set reference
- 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
- 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); set to NULL on soft delete; 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); preserved on soft delete; default empty array
### Uniqueness Constraints
@@ -140,10 +141,12 @@
- 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.
- tag_ids stores UUID references to active tags; duplicates are prevented by conditional array_append; comments are retrieved in created_at descending order.
- `status="disabled"` (soft delete): `managed_by_daemon_id` is set to NULL; portfolio is excluded from `GET /v1/portfolios` by default. Dependent data (`portfolio_comments`, `tag_ids`) is preserved — never deleted on soft delete. READ operations on sub-resources (GET tags, GET comments) remain allowed; write mutations raise HTTP 409.
- Status restoration: PATCH with `{"status": "enabled"}` re-activates a disabled portfolio; no other fields may be set in the same request.
## Entity: `User`
- Purpose: represent an authenticated principal that can own portfolios and receive client or service tokens
- Purpose: represent an authenticated principal that can own portfolios and receive service tokens
- Source of Truth: users table with dependent auth_tokens rows
- Lifecycle: bootstrapped or created -> updated -> deactivated or deleted
@@ -158,7 +161,7 @@
### Relationships
- AuthToken :: user_id links issued client and service tokens to the owning user
- AuthToken :: user_id links issued service tokens to the owning user
- Portfolio :: portfolios.owner_user_id ties portfolios to a managing user
- TokenPrincipal :: runtime auth resolution derives principals from persisted tokens and users
@@ -170,7 +173,7 @@
## Entity: `AuthToken`
- Purpose: represent an issued client or service token linked to a user with permission and optional runtime identifier
- Purpose: represent an issued service token (or session token for login flows) linked to a user with permission and optional runtime identifier; client token type (token_type="client") removed by REQ-016/BAT-006
- Source of Truth: auth_tokens table
- Lifecycle: issued -> optionally rotated -> revoked or expired
@@ -178,7 +181,7 @@
- id :: text :: primary key; raw token value (stored hashed in practice)
- user_id :: text :: owning user reference
- token_type :: text :: "client" or "service"
- token_type :: text :: "session" (login tokens) | "service" (daemon-bound operational tokens); "client" type removed by REQ-016/BAT-006
- token_source :: text :: "db" for persisted tokens; "env" for environment-injected tokens
- permission :: text :: read, trade-write, or admin
- service_name :: text|null :: logical name for service tokens (unique within token_type="service")
@@ -189,7 +192,7 @@
### Uniqueness Constraints
- idx_auth_tokens_service_name_unique :: UNIQUE (service_name) WHERE token_type='service'
- idx_auth_tokens_service_name_unique :: UNIQUE (name) WHERE token_type='service' AND revoked_at IS NULL (BAT-006/REQ-017: revoked_at IS NULL 조건 추가로 rotate 후 구 token과 신 token 이름 충돌 해소)
- idx_auth_tokens_runtime_id_unique :: UNIQUE (runtime_id) WHERE runtime_id IS NOT NULL
### State Rules
@@ -198,6 +201,7 @@
- runtime_id is per-token and changes on every rotate; daemon_id is stable across rotates (daemon_id invariant).
- daemon_id=NULL tokens are fail-closed on GET /v1/assets and POST /v1/assets/{asset_id}/ledger/trades (HTTP 403); GET /v1/user_info returns 200 + `daemon_id: null` as diagnostic.
- Token permission gates both HTTP API guards and CLI mutation capabilities.
- Deprecated: token_type="client" issuance path removed by REQ-016/BAT-006; existing "client" type rows in legacy DBs remain readable but new issuance is blocked.
## Entity: `Daemon`
@@ -228,7 +232,7 @@
### State Rules
- A daemon must exist and be active before a service token can be bound to it at issuance.
- Deactivating a daemon (is_active=false) blocks new token issuance but does not revoke existing tokens.
- Deactivating a daemon (is_active=false) immediately revokes all bound service tokens via `revoke_tokens_by_daemon_id` (REQ-022/BAT-007) and blocks new token issuance.
- daemon_slug is immutable after creation; only display_name and is_active can be updated.
## Entity: `BrokerCredential`
@@ -313,6 +317,8 @@
## Change Log
- 2026-05-21 KST: BAT-010 (REQ-032) — v1 portfolio surface 데이터 모델 보강: Portfolio.status 필드 신설 (`"enabled"` | `"disabled"` 의미, soft delete 정책); Portfolio.managed_by_daemon_id에 soft delete 시 NULL 설정 명시; tag_ids 소프트 삭제 보존 정책 추가; Portfolio State Rules에 disabled 상태 정책 + 의존 데이터 보존 + 상태 복원 규칙 추가; Source Discovery/Batch 헤더 갱신.
- 2026-05-21 KST: BAT-008 (REQ-019) — 토큰 surface 정합성 회복: AuthToken token_type "client" 제거 반영; idx_auth_tokens_service_name_unique에 AND revoked_at IS NULL 조건 추가(REQ-017); Daemon 비활성화 시 바인딩 서비스 토큰 자동 revoke 정책 반영(REQ-022); User/AuthToken Purpose 문구 갱신.
- 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.