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:
+23
-10
@@ -208,12 +208,11 @@ class FakeRepo:
|
||||
|
||||
def create_portfolio(
|
||||
self,
|
||||
portfolio_id: str,
|
||||
name: str,
|
||||
*,
|
||||
owner_user_id: str,
|
||||
broker: str,
|
||||
status: str,
|
||||
name: str,
|
||||
symbol: str,
|
||||
cash_balance: float,
|
||||
asset_quantity: float,
|
||||
@@ -222,6 +221,7 @@ class FakeRepo:
|
||||
active_strategy_id: str | None,
|
||||
avabile_strategies: list[str] | None,
|
||||
) -> Portfolio:
|
||||
portfolio_id = str(uuid.uuid4())
|
||||
self._portfolio = Portfolio(
|
||||
portfolio_id=portfolio_id,
|
||||
broker=broker,
|
||||
@@ -238,6 +238,13 @@ class FakeRepo:
|
||||
)
|
||||
return self._portfolio
|
||||
|
||||
def get_portfolio_by_name_and_user(self, owner_user_id: str, name: str) -> Portfolio | None:
|
||||
if self._portfolio is None:
|
||||
return None
|
||||
if self._portfolio.owner_user_id == owner_user_id and self._portfolio.name == name:
|
||||
return self._portfolio
|
||||
return None
|
||||
|
||||
def update_portfolio(self, portfolio_id: str, **fields: object) -> Portfolio:
|
||||
portfolio = self.get(portfolio_id)
|
||||
for key, value in fields.items():
|
||||
@@ -484,7 +491,7 @@ def _portfolio() -> Portfolio:
|
||||
portfolio_id="005930:tv",
|
||||
broker="kis",
|
||||
status="enabled",
|
||||
name="Samsung Electronics",
|
||||
name="005930:tv",
|
||||
symbol="005930",
|
||||
cash_balance=1_000_000.0,
|
||||
asset_quantity=1.0,
|
||||
@@ -511,10 +518,14 @@ def test_cli_list_renders_table() -> None:
|
||||
|
||||
|
||||
def test_cli_status_supports_json_output() -> None:
|
||||
repo = FakeRepo(_portfolio())
|
||||
repo = FakeRepo()
|
||||
user = repo.create_user("alice", permission="read")
|
||||
p = _portfolio()
|
||||
p.owner_user_id = user.id
|
||||
repo._portfolio = p
|
||||
runner = build_runner(repo)
|
||||
|
||||
result = runner.invoke(runner.app, ["--format", "json", "portfolio", "status", "005930:tv"])
|
||||
result = runner.invoke(runner.app, ["--format", "json", "portfolio", "status", "005930:tv", "--user", "alice"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
payload = json.loads(result.stdout)
|
||||
@@ -549,6 +560,8 @@ def test_cli_create_buy_and_delete_flow() -> None:
|
||||
"portfolio",
|
||||
"buy",
|
||||
"005930:tv",
|
||||
"--user",
|
||||
"alice",
|
||||
"--quantity",
|
||||
"2",
|
||||
"--price",
|
||||
@@ -560,12 +573,12 @@ def test_cli_create_buy_and_delete_flow() -> None:
|
||||
)
|
||||
assert repo.entries[0].action == "BUY"
|
||||
assert repo._portfolio.owner_user_id == user.id
|
||||
delete_result = runner.invoke(runner.app, ["portfolio", "delete", "005930:tv", "--keep-ledger", "--force"])
|
||||
delete_result = runner.invoke(runner.app, ["portfolio", "delete", "005930:tv", "--user", "alice", "--keep-ledger", "--force"])
|
||||
|
||||
assert create_result.exit_code == 0
|
||||
assert buy_result.exit_code == 0
|
||||
assert delete_result.exit_code == 0
|
||||
assert "Deleted portfolio: 005930:tv" in delete_result.stdout
|
||||
assert "Deleted portfolio: 005930:tv" in delete_result.stdout # name used as identifier
|
||||
|
||||
|
||||
def test_cli_write_command_prompts_before_create() -> None:
|
||||
@@ -590,7 +603,7 @@ def test_cli_write_command_prompts_before_create() -> None:
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Create portfolio 005930:tv for symbol 005930?" in result.stdout
|
||||
assert "Create portfolio '005930:tv' for symbol 005930 (user: alice)?" in result.stdout
|
||||
|
||||
|
||||
def test_cli_write_command_can_abort() -> None:
|
||||
@@ -627,8 +640,8 @@ def test_cli_portfolio_create_without_user_exits_error() -> None:
|
||||
["portfolio", "create", "005930:tv", "--symbol", "005930", "--force"],
|
||||
)
|
||||
|
||||
assert result.exit_code == 1
|
||||
assert "--user" in result.stdout
|
||||
assert result.exit_code != 0
|
||||
assert "--user" in result.output
|
||||
|
||||
|
||||
def test_cli_portfolio_create_with_valid_user_sets_owner() -> None:
|
||||
|
||||
Reference in New Issue
Block a user