Files
portfolio-hub/docs/runtime-flows.md
T
genichin 829629fa8f feat: implement portfolio UUID migration and v1 API finalization (BAT-006, BAT-001 re-verified)
Core Changes:
- Migrate portfolio_id from string to UUID in schema and repository layer
- Update portfolio CLI commands to use name+user combination for identification
- Update HTTP API to accept portfolio_id as UUID path parameter
- Implement portfolio ownership validation in repository queries

Requirements Completed (REQ-016 to REQ-019):
- REQ-016: portfolios table UUID schema migration with data migration script
- REQ-017: CLI portfolio commands with name+user identification
- REQ-018: HTTP API portfolio path parameters and v1 asset_id UUID returns
- REQ-019: Portfolio UUID baseline documentation updates

BAT-001 Re-verification:
- REQ-004~010 marked as Implemented after verification evidence validation
- All v1 API endpoints verified with comprehensive test coverage

Documentation Updates:
- Update data-model.md with portfolio UUID structure
- Update interface-contract.md with UUID path parameters
- Update runtime-flows.md with ownership-based resolution flows
- Update project-structure.md with database layer changes
- Add migration script for portfolio UUID transition

Testing:
- Add test_portfolio_uuid.py for migration validation
- Update test_cli.py and test_portfolio_ownership.py for ownership checks
2026-05-11 09:27:57 +00:00

173 lines
11 KiB
Markdown

# Portfolio Hub Runtime Flows
- Baseline Mode: observed
- 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-006, bat-007, bat-008
- Primary Runtime: mixed
## Project Summary
- PortfolioHub exposes portfolio management and trading ledger workflows through a FastAPI service and a companion CLI.
## Purpose
- This document is the current approved execution-flow baseline for the project.
- It is a cross-cutting reference document for Discovery, REQ, Batch Design, Batch Verification, and implementation updates.
## Scope
- Covers representative command and runtime flows.
- Does not duplicate full acceptance criteria or low-level implementation details.
## 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, and service tokens
- alembic upgrade head :: apply ordered database schema migrations for persistent environments
## Shared Components
- app/core/config.py Settings loading and environment normalization
- app/core/auth.py permission guards and auth context resolution
- app/services/* business logic for auth, users, portfolios, and strategy sets
- app/repositories/factory.py repository selection and Postgres config construction
- app/repositories/postgres.py Postgres persistence, schema bootstrap, and query execution
## Flow: `HTTP API Request Handling`
1. Uvicorn loads app.main:app, which creates the FastAPI application from create_app().
2. create_app() loads Settings, registers middleware and routers, and wires exception handlers.
3. During lifespan startup, the app optionally bootstraps an initial admin user when DB mode and bootstrap credentials are configured.
4. Incoming requests pass through auth and permission dependencies before services mutate state or query persistence.
5. Services call the repository factory, which returns PostgresPortfolioRepository only when PORTFOLIOHUB_DB_ENABLED=true.
6. Routes serialize domain results into app/schemas response models or return HTTP 204 for deletion flows.
## Flow: `CLI Operational Command Execution`
1. The pfhub entrypoint builds a Typer application and injects Settings, console output, and a repository factory into AppContext.
2. Command groups parse arguments, confirmations, and output format selection before invoking the relevant service or repository methods.
3. Database setup commands use the shared settings layer to target the configured PostgreSQL or TimescaleDB instance.
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: `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.
2. `_resolve_portfolio_id(repository, name, user)` is called before any mutation:
a. Calls `get_user_by_username(user)` → raises ValueError if user not found.
b. Calls `get_portfolio_by_name_and_user(owner_user_id, name)` → raises ValueError if portfolio not found.
c. Returns `portfolio.portfolio_id` (UUID) for downstream use.
3. `create` command takes `<name>` as the portfolio identifier; portfolio_id (UUID) is server-generated and returned in the response.
4. `update --new-name <new_name>` renames the portfolio; `--force` is required to confirm the rename.
5. `comment` and `tag` sub-commands continue to accept `<portfolio_id>` (UUID) directly; they are outside the name+user resolution flow.
## Flow Constraints
- Persistence-dependent HTTP and CLI flows require PORTFOLIOHUB_DB_ENABLED=true plus valid TimescaleDB connection settings.
- Permission levels read, trade-write, and admin must map consistently across API guards, issued tokens, and CLI mutations.
- 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 — Service-Token Authentication`
1. v1 endpoint receives Bearer token from Authorization header.
2. `require_read` resolves the token: checks env tokens first, then DB lookup via `get_token_principal()`.
3. If DB token: validates `token_source == "db"` and `token_type == "service"`, else returns HTTP 403.
4. `AuthContext` is populated with `user_id`, `token_type`, `permission`, and `runtime_id`.
## Flow: `v1 API — GET /v1/assets`
1. Service-token auth required (see above).
2. `list_portfolios_with_strategies()` fetches all portfolios from DB.
3. Optional `?status=<value>` query param filters portfolios by status field.
4. For portfolios with `active_strategy_id`, `get_strategy_set()` fetches config and JSON-parses `strategy_params`; failures fall back to `{}`.
5. Returns `AssetsResponse` with `total` and `assets[]` using new field names: `cash`, `quantity`, `broker_type`, `strategy_params`.
6. `asset_id` in each asset item equals `portfolio.portfolio_id` which is a server-generated UUID after BAT-006 migration (dcy-002 A-3 updated: text identity assumption replaced by UUID).
## Flow: `v1 API — GET /v1/user_info`
1. Service-token auth required. HTTP 404 if `runtime_id` is None.
2. `get_user()` fetches user record by `user_id`.
3. `list_broker_credentials()` fetches all broker credential rows for the user.
4. For each credential, Fernet-decrypts `encrypted_payload` using `CREDENTIAL_ENCRYPTION_KEY`; decryption failures log a warning and return `credential_payload: null`.
5. Returns `UserInfoResponse` with `trading_enabled` (from `is_active`), `disabled_reason`, and `brokers[]`.
## Flow: `v1 API — POST /v1/assets/{asset_id}/ledger/trades`
1. Service-token auth required with `trade-write` permission.
2. Validates asset existence via `repo.get(asset_id)`; HTTP 404 if missing.
3. Calls `insert_ledger_trade()` with idempotency key `(asset_id, order_id, execution_id)`.
4. If new record: returns HTTP 201 with `status="new"`.
5. If duplicate key with identical payload: returns HTTP 200 with `status="duplicate"`.
6. If duplicate key with different payload: returns HTTP 409 with conflict detail.
## Flow: `v1 API — POST /auth/service-tokens`
1. Admin token auth required (`require_admin` dependency).
2. Validates request body: `name`, `username`, `permission`, `expires_in_days` / `no_expiry`.
3. Calls `AuthService.issue_service_token()` which creates a new DB token with `runtime_id`.
4. Returns HTTP 201 with `token` (raw value), `runtime_id`, and `auth_token` metadata.
## Flow: `Application Startup Validation`
1. `lifespan` startup hook runs `_bootstrap_initial_user()` if DB enabled and bootstrap credentials configured.
2. If `db_enabled=True`, calls `settings.require_credential_encryption_key()`.
3. If `CREDENTIAL_ENCRYPTION_KEY` is not configured, raises `RuntimeError` and aborts startup.
## Flow: `Local DB Initialization and Validation`
### Scenario A — Fresh DB (DB 미존재 상태)
1. `docker compose -f docker-compose.local-test.yml up -d` — TimescaleDB 컨테이너 시작.
2. 컨테이너 준비 확인: `docker compose -f docker-compose.local-test.yml ps` → `portfolio-hub-timescaledb` 상태가 `healthy` 인지 확인.
3. `pfhub_DB_ENABLED=true pfhub init-db` — 대상 DB 생성 + Alembic `upgrade head` 실행.
- 기대 출력: `database: portfolio_hub`, `created: True`, `schema_initialized: True`, `migration_applied: head`
- pass 기준: exit code 0, 위 필드 모두 출력
- fail 기준: connection refused, permission denied, Alembic revision 오류
4. `pfhub_DB_ENABLED=true pfhub health-check` — DB ping 성공 확인.
- 기대 출력: `db_enabled: True`, `repository: PostgresPortfolioRepository`
- pass 기준: exit code 0, `db_enabled: True`
5. Smoke flow: 사용자 생성 → portfolio 생성 → `portfolio list --user <username>` 확인.
- pass 기준: 생성된 portfolio가 list 출력에 포함됨
### Scenario B — runtime-precreated schema (ensure_schema() 선행)
1. DB 컨테이너 시작 (Scenario A 1~2 동일).
2. `pfhub_DB_ENABLED=true uvicorn app.main:app` — App 기동. `ensure_schema()`가 핵심 테이블 생성.
3. `pfhub_DB_ENABLED=true pfhub migrate-db` — `bootstrap_or_upgrade_to_head()` 실행.
- 내부 동작: `alembic_version` 테이블 없음 → 기존 핵심 테이블(portfolios 등) 존재 → legacy stamp → head로 stamp + 미보유 객체(broker_credentials, ledger_trades, runtime_id 등) upgrade
- 기대 출력: `migration_applied: head`
- pass 기준: exit code 0, 이후 `pfhub health-check` exit code 0
4. `pfhub_DB_ENABLED=true pfhub health-check` 재확인.
### Repeatable Run Criteria
- 모든 시나리오는 컨테이너 볼륨 초기화(`docker compose down -v`) 후 재실행 가능.
- 출력 형식: `pfhub --output json <command>` 로 JSON 출력 수집 시 필드명이 고정됨.
- 로그 수집 위치: `docker compose logs portfolio-hub-timescaledb` — DB 측 오류 시 참조.
- Evidence 형식: exit code + 주요 필드 값 스크린샷 또는 텍스트 캡처 → batch verification.md에 기록.
## Flow: `Alembic vs ensure_schema() Schema Ownership Boundary`
`ensure_schema()` (postgres.py)와 Alembic revision 양쪽이 다루는 객체가 존재한다. 충돌은 `bootstrap_or_upgrade_to_head()`의 legacy stamp 경로 + 각 revision의 IF NOT EXISTS guard로 처리된다.
### 경계 규칙
1. **ensure_schema() 선행 가능 객체**: users, auth_tokens, strategy_sets, portfolios, portfolio_ledger, portfolio_comments, portfolio_tags + 관련 인덱스. 모두 IF NOT EXISTS로 보호됨.
2. **Alembic만 소유 (ensure_schema()에 없음)**: `portfolios.market`, `auth_tokens.runtime_id`, `idx_auth_tokens_runtime_id_unique`, `broker_credentials`, `ledger_trades`. ensure_schema()만 실행한 환경에서 v1 API 사용 불가.
3. **허용 실행 순서**: `init-db` 선행 권장. 불가피하게 ensure_schema() 선행 시, 반드시 `migrate-db` 후속 실행.
4. **금지**: `alembic_version` 테이블 수동 삭제 후 migrate-db 재실행 (legacy stamp 오판).
## Current Gaps / Planned Changes
- End-to-end validation against a real PostgreSQL or TimescaleDB deployment is still pending.
- Redis and Loki are configured as supporting infrastructure but are not yet represented as explicit runtime flows.
- Broker credential write path (upsert) is available only via CLI; no HTTP API endpoint for credential registration exists yet.
## Change Log
- 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.
- 2026-05-11 KST: bat-006 — Added CLI portfolio name+user resolution flow; updated GET /v1/assets flow to note asset_id is UUID after BAT-006 migration.
- 2026-05-01 KST: bat-001 — Added v1 API flows (service-token auth, GET /v1/assets, GET /v1/user_info, POST ledger/trades, POST /auth/service-tokens), startup validation flow.
- 2026-04-30 KST: Bootstrap baseline created from repository observation for initial StagePilot adoption.