feat(portfolios): persist and validate daemon assignment on create/update
POST /v1/portfolios now forwards a non-null managed_by_daemon_id into create persistence, and both create and PATCH apply the same daemon validation contract (UUID format -> 422, missing/foreign -> 404, inactive -> 409, admin target-owner basis). Daemon validation locks the daemon row FOR SHARE inside the portfolio write transaction so validation and write form one atomic decision boundary. Adds domain errors, repository protocol/signature updates, API/PostgreSQL tests (POST->GET persistence, no partial state on failure, assignment vs deactivation race), and contract/data-model documentation.
This commit is contained in:
@@ -304,7 +304,8 @@
|
||||
- Body: `{"asset_id": "<uuid>", "broker": "<str>", "name": "<str|null>", "cash_balance": <float>, "asset_quantity": <float>, "asset_price": <float>, "profit_buffer": <float>, "strategy_id": "<required strategy ID>", "managed_by_daemon_id": "<str|null>", "user_id": "<str|null> (admin only)"}`
|
||||
- Response: HTTP 201 `PortfolioItem {"portfolio_id": "<uuid>", ..., "active_strategy_id": "<str>", "strategy_name": "<str>", "strategy_params": {<dict>}, "strategy_revision": <int>, ...}`
|
||||
- Note: `symbol` is populated from the server-side joined asset row (`portfolios.asset_id -> assets.asset_id -> assets.symbol`), not from client-side catalog reconstruction.
|
||||
- Error: HTTP 403 if service/client token; HTTP 404 if `strategy_id` is unknown; HTTP 409 if the strategy is inactive; HTTP 422 for a missing/invalid `strategy_id` or an unknown/inactive asset
|
||||
- Note: a non-null `managed_by_daemon_id` must be a daemon server-generated UUID that exists, is active, and is owned by the target portfolio owner (admin `user_id` override uses the final target owner, not the calling admin). Daemon validation and the portfolio write share one atomic transaction (`FOR SHARE` daemon lock); a selected daemon that is deactivated concurrently is rejected rather than committed.
|
||||
- Error: HTTP 403 if service/client token; HTTP 404 if `strategy_id` is unknown or the daemon does not exist/is not owned by the portfolio owner; HTTP 409 if the strategy is inactive or the daemon is inactive; HTTP 422 for a missing/invalid `strategy_id`, an unknown/inactive asset, or a non-UUID `managed_by_daemon_id`
|
||||
|
||||
### Endpoint: `PATCH /v1/portfolios/{portfolio_id}`
|
||||
|
||||
@@ -312,7 +313,8 @@
|
||||
- Body: `{"broker": "<str|null>", "status": "<str|null>", "name": "<str|null>", "strategy_id": "<str|null>", "asset_price": <float|null>, "managed_by_daemon_id": "<str|null>"}`
|
||||
- Response: HTTP 200 `PortfolioItem {"portfolio_id": "<uuid>", ..., "active_strategy_id": "<str>", "strategy_name": "<str>", "strategy_params": {<dict>}, "strategy_revision": <int>, ...}`
|
||||
- Note: response `symbol` is emitted from the joined asset row referenced by `asset_id`.
|
||||
- Error: HTTP 403 if service/client or ownership mismatch; HTTP 404 if portfolio or strategy is not found; HTTP 409 if the portfolio is disabled for the mutation or the selected strategy is inactive; HTTP 422 for an invalid request body
|
||||
- Note: a non-null `managed_by_daemon_id` assignment/reassignment follows the same daemon validation contract as create: it must be a UUID that exists, is active, and is owned by the portfolio owner. The daemon `FOR SHARE` lock and the portfolio UPDATE share one atomic transaction, so a concurrently deactivated daemon is rejected with HTTP 409.
|
||||
- Error: HTTP 403 if service/client or ownership mismatch; HTTP 404 if portfolio, strategy, or daemon is not found (foreign daemon is hidden as 404); HTTP 409 if the portfolio is disabled for the mutation, the selected strategy is inactive, or the daemon is inactive; HTTP 422 for an invalid request body or a non-UUID `managed_by_daemon_id`
|
||||
|
||||
## Interface: `v1 Strategy Catalog API`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user