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
This commit is contained in:
2026-05-11 09:27:57 +00:00
parent 9ed307555a
commit 829629fa8f
46 changed files with 3260 additions and 220 deletions
+136 -3
View File
@@ -4,8 +4,8 @@
- Baseline Seed: .stagepilot/bootstrap/baseline.yaml
- Status: confirmed
- Owner: repository-maintainers
- Last Updated (KST): 2026-04-30 22:13
- Source Discovery / Batch: bootstrap-baseline
- Last Updated (KST): 2026-05-11 00:00
- Source Discovery / Batch: bootstrap-baseline, bat-001, bat-006, bat-008
- Primary Runtime: mixed
## Project Summary
@@ -45,7 +45,7 @@
- JSON response models :: responses are shaped by app/schemas/auth.py, app/schemas/user.py, and app/schemas/portfolio.py
- HTTP 201, 200, and 204 status codes :: create, read or update, and delete semantics respectively
- GET /health payload :: returns {"status": "ok"} for readiness checks
- GET /health payload :: returns `{"status": "ok", "service": "portfolio-hub", "version": "<semver>", "checked_at": "<ISO8601 KST>"}` for readiness checks
### Error Contract
@@ -72,6 +72,7 @@
- pfhub subcommands :: include user, portfolio, strategy, service-token, 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
### Outputs
@@ -91,6 +92,136 @@
- Write commands require explicit confirmation unless --force is present.
- JSON output should remain machine-readable across additive command evolution.
## Interface: `v1 Consumer API (trex-basic)`
- Type: http-api
- Consumers / Producers: trex-basic service (Consumer-Driven Contract, dcy-002)
- Purpose: provide asset listing, user info, and trade ledger recording for the trex-basic trading daemon
- Stability: internal (versioned under /v1)
### Authentication
- All /v1 endpoints require `Authorization: Bearer <service-token>` header.
- Token must be a DB-issued service token (`token_source="db"`, `token_type="service"`); env tokens are rejected with HTTP 403.
- POST /v1/assets/{asset_id}/ledger/trades additionally requires `trade-write` permission.
### Endpoint: `GET /v1/assets`
- Auth: service-token bearer (read)
- Query Params: `?status=<value>` (optional, filters by portfolio status)
- Response: HTTP 200
```
{
"total": <int>,
"assets": [
{
"asset_id": "<UUID>", // portfolio.portfolio_id (UUID after BAT-006)
"symbol": "<str>",
"market": "<str|null>",
"status": "<str>",
"cash": <float>,
"quantity": <float>,
"broker_type": "<str>",
"strategy_id": "<str|null>",
"strategy_type": "<str|null>",
"strategy_params": {<dict>},
"updated_at": "<ISO8601|null>",
"name": "<str|null>",
"price": <float|null>,
"profit_buffer": <float|null>,
"tags": [<str>]|null,
"owner_user_id": "<str|null>",
"created_at": "<ISO8601|null>",
"last_trade_at": "<ISO8601|null>"
}
]
}
```
- Error: HTTP 401 if no token; HTTP 403 if not a service token
### Endpoint: `GET /v1/user_info`
- Auth: service-token bearer (read); token must have `runtime_id` assigned
- Response: HTTP 200
```
{
"runtime_id": "<str>",
"trading_enabled": <bool>, // mapped from users.is_active
"disabled_reason": "<str|null>", // "account_inactive" when trading_enabled=false
"brokers": [
{
"broker": "<str>",
"account_ref": "<str>",
"credential_payload": {<dict>}|null, // Fernet-decrypted {app_key, app_secret}
"credential_version": <int>,
"last_rotated_at": "<ISO8601|null>"
}
],
"user_id": "<str>",
"username": "<str>",
"email": "<str|null>",
"permission": "<str>"
}
```
- Error: HTTP 401 if no token; HTTP 403 if not a service token; HTTP 404 if token has no runtime_id
### Endpoint: `POST /v1/assets/{asset_id}/ledger/trades`
- Auth: service-token bearer (trade-write)
- Path Param: `asset_id` — must be a valid UUID portfolio_id
- Request Body:
```
{
"order_id": "<str>", // required, min_length=1
"execution_id": "<str>", // required, min_length=1
"side": "buy"|"sell",
"quantity": <decimal>, // > 0
"price": <decimal>, // > 0
"fee_amount": <decimal>, // >= 0, default 0
"tax_amount": <decimal>, // >= 0, default 0
"executed_at": "<ISO8601>"
}
```
- Response HTTP 201 (new record):
```
{
"ledger_entry_id": <int>,
"asset_id": "<str>",
"order_id": "<str>",
"execution_id": "<str>",
"side": "<str>",
"quantity": <float>,
"price": <float>,
"fee_amount": <float>,
"tax_amount": <float>,
"executed_at": "<ISO8601>",
"recorded_at": "<ISO8601|null>",
"status": "new"
}
```
- Response HTTP 200 (idempotent duplicate with identical payload): same body with `"status": "duplicate"`
- Error: HTTP 401 if no token; HTTP 403 if not service token or insufficient permission; HTTP 404 if asset_id not found; HTTP 409 if duplicate key with different payload
### Idempotency Contract
- Deduplication key: `(asset_id, order_id, execution_id)` — enforced by `uq_ledger_trades_idempotency` DB unique constraint
- Identical payload re-send → HTTP 200 + `status="duplicate"`, no new row created
- Same key, different payload → HTTP 409 Conflict
### Deployment Constraint (bat-006)
- trex-basic and portfolio-hub must be deployed simultaneously when BAT-006 is released.
- 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.
## Shared Constraints
- Token permission levels gate both HTTP and CLI mutations consistently.
@@ -111,4 +242,6 @@
## Change Log
- 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.
- 2026-05-11 KST: bat-006 — Added v1 consumer API interface section (trex-basic); noted asset_id UUID change and simultaneous deployment constraint; updated CLI portfolio inputs to reflect name+user identification.
- 2026-04-30 KST: Bootstrap baseline created from repository observation for initial StagePilot adoption.