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
+14 -1
View File
@@ -5,8 +5,12 @@ from __future__ import annotations
from datetime import datetime
from typing import Any, Literal
import re as _re
from pydantic import BaseModel, ConfigDict, Field, field_validator
_PORTFOLIO_NAME_RE = _re.compile(r"^[a-zA-Z0-9._:\-]{1,100}$")
class StrategySetResponse(BaseModel):
id: str
@@ -70,7 +74,7 @@ class PortfolioDetailResponse(BaseModel):
class CreatePortfolioRequest(PortfolioBase):
portfolio_id: str
name: str # required, overrides Optional from PortfolioBase; used as (owner_user_id, name) identifier
strategy_name: str = "target_value"
strategy_params: dict[str, Any] = Field(default_factory=dict)
active_strategy_id: str | None = None
@@ -80,6 +84,15 @@ class CreatePortfolioRequest(PortfolioBase):
asset_price: float = Field(default=0.0, ge=0.0)
profit_buffer: float = Field(default=0.0, ge=0.0)
@field_validator("name")
@classmethod
def validate_name(cls, v: str) -> str:
if not _PORTFOLIO_NAME_RE.match(v):
raise ValueError(
f"이름은 [a-zA-Z0-9._:-] 문자만 허용하며 1~100자여야 합니다: {v!r}"
)
return v
class UpdatePortfolioRequest(BaseModel):
broker: str | None = None