feat(REQ-001): add portfolio comment history API, CLI, repository, migration, and tests

Agent-Logs-Url: https://github.com/genichin/portfolio-hub/sessions/64fc2cec-78f5-41d1-99a4-45f868781edd

Co-authored-by: genichin <4282227+genichin@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-04-10 13:54:18 +00:00
committed by GitHub
co-authored by genichin
parent 4684597353
commit e8f782bec6
14 changed files with 1173 additions and 10 deletions
+71 -1
View File
@@ -10,6 +10,8 @@ import typer
from app.cli.core.utils import execute_command, render_output
from app.schemas.portfolio import (
BuyRequest,
CommentCreateRequest,
CommentUpdateRequest,
CreatePortfolioRequest,
InvestRequest,
ResetPortfolioRequest,
@@ -21,6 +23,8 @@ from app.services.portfolio_service import PortfolioService
portfolio_app = typer.Typer(help="Portfolio lifecycle and trade commands.")
comment_app = typer.Typer(help="Portfolio comment history commands.")
portfolio_app.add_typer(comment_app, name="comment")
def _parse_strategy_params(value: str) -> dict[str, Any]:
@@ -263,4 +267,70 @@ def delete_portfolio(
if ctx.obj.output_format == "json":
render_output(ctx.obj.console, ctx.obj.output_format, {"deleted": portfolio_id, "keep_ledger": keep_ledger})
else:
ctx.obj.console.print(f"Deleted portfolio: {portfolio_id}")
ctx.obj.console.print(f"Deleted portfolio: {portfolio_id}")
@comment_app.command("add")
def comment_add(
ctx: typer.Context,
portfolio_id: str = typer.Argument(..., help="Portfolio identifier."),
content: str = typer.Option(..., "--content", help="Comment text (supports multiline)."),
) -> None:
detail = _with_service(
ctx,
lambda service, app_context: service.create_comment(
portfolio_id,
CommentCreateRequest(content=content),
),
)
render_output(ctx.obj.console, ctx.obj.output_format, detail)
@comment_app.command("list")
def comment_list(
ctx: typer.Context,
portfolio_id: str = typer.Argument(..., help="Portfolio identifier."),
) -> None:
detail = _with_service(
ctx,
lambda service, app_context: service.list_comments(portfolio_id),
)
render_output(ctx.obj.console, ctx.obj.output_format, detail, empty_message="No comments found.")
@comment_app.command("update")
def comment_update(
ctx: typer.Context,
portfolio_id: str = typer.Argument(..., help="Portfolio identifier."),
comment_id: str = typer.Argument(..., help="Comment identifier."),
content: str = typer.Option(..., "--content", help="Updated comment text."),
force: bool = typer.Option(False, "--force", help="Skip confirmation prompt."),
) -> None:
_confirm(force, f"Update comment {comment_id} for portfolio {portfolio_id}?")
detail = _with_service(
ctx,
lambda service, app_context: service.update_comment(
portfolio_id,
comment_id,
CommentUpdateRequest(content=content),
),
)
render_output(ctx.obj.console, ctx.obj.output_format, detail)
@comment_app.command("delete")
def comment_delete(
ctx: typer.Context,
portfolio_id: str = typer.Argument(..., help="Portfolio identifier."),
comment_id: str = typer.Argument(..., help="Comment identifier."),
force: bool = typer.Option(False, "--force", help="Skip confirmation prompt."),
) -> None:
_confirm(force, f"Delete comment {comment_id} for portfolio {portfolio_id}?")
_with_service(
ctx,
lambda service, app_context: service.delete_comment(portfolio_id, comment_id),
)
if ctx.obj.output_format == "json":
render_output(ctx.obj.console, ctx.obj.output_format, {"deleted": comment_id, "portfolio_id": portfolio_id})
else:
ctx.obj.console.print(f"Deleted comment: {comment_id}")