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:
2026-08-15 12:56:53 +09:00
parent bb3bb5d3f0
commit e2102b6f2b
10 changed files with 861 additions and 7 deletions
@@ -0,0 +1,40 @@
## 구현 진행 상황
- **대상:** 포트폴리오 추가 시 연결 데몬을 설정해도 지정되지 않음 (GitHub Issue #27)
- **브랜치:** `line/line-0004-implementation`
- **기준:** `main` (`bb3bb5d3f0ce57358f7385084fba1343f7ff06e1`)
- **상태:** 로컬 구현·검증 완료 (미커밋)
- **최종 업데이트:** 2026-08-15 15:20 KST
### 구현 계획 및 작업 목록
- [x] 공유 daemon validation 도메인 오류 정의
- `DaemonNotFoundError`(HTTP 404)·`DaemonInactiveError`(HTTP 409)를 `app/domain/errors.py`에 추가해 전 계층에서 같은 오류 계약을 사용한다.
- [x] `create_portfolio` 저장소 계약·구현에 `managed_by_daemon_id` 전달
- 프로토콜 시그니처(`portfolio_repository.py`)와 PostgreSQL 구현(`postgres.py`)에 `managed_by_daemon_id` 파라미터를 추가하고 INSERT 컬럼까지 반영해 생성 persistence 경계에 누락을 제거한다.
- [x] 생성·수정 트랜잭션 안의 원자적 daemon 검증 경계
- 기존 `_lock_active_strategy` 패턴처럼 daemon row를 `FOR SHARE` lock하고 존재·소유권·active를 같은 트랜잭션에서 검증해, portfolio write와 하나의 원자적 판정으로 묶어 TOCTOU를 차단한다.
- [x] `POST /v1/portfolios` 생성 handler에서 비-null daemon 전달·오류 매핑
- UUID 형식이 아니면 422, 미존재·foreign(타인 소유)이면 404, inactive면 409로 매핑하고 admin의 `user_id` override 생성은 최종 target owner 기준으로 검증한다.
- [x] `PATCH /v1/portfolios/{portfolio_id}` 비-null 지정·재지정에도 동일 검증 적용
- 기존 비-null PATCH 지정·재지정 경로에 생성과 동일한 daemon 검증·오류 계약을 적용하되, 포트폴리오 소유권·disabled 보호·service/client 거부 등 기존 mutation 계약은 유지한다.
- [x] 단위/API 테스트 추가
- 성공 생성 응답의 `managed_by_daemon_id` 일치, 미선택 생성 시 null 유지, malformed 422, 미존재·foreign 404, inactive 409, admin target-owner 허용/거부, PATCH 지정·재지정을 검증한다.
- [x] 실제 PostgreSQL 통합 테스트 추가
- `POST → GET` persistence 보존, 검증 실패 시 portfolio 무변경, assignment와 daemon 비활성화가 경합해도 inactive daemon에 새 assignment가 확정되지 않는 동시성 race를 검증한다.
- [x] 문서 갱신
- `docs/interface-contract.md`, `docs/data-model.md`에 daemon 배정 검증 계약·오류 코드·원자적 판정 경계를 반영한다.
- [x] 회귀·정적 검사 및 진행 상황 업데이트
- 기존 portfolio 생성·수정의 인증·소유권·strategy·asset·disabled 계약 회귀, 전체 관련 테스트, compile·`git diff --check`·Ruff 검사를 실행하고 진행 상황 파일을 갱신한다.
### 진행 기록
- 2026-08-15 13:00 KST — Issue #27의 승인된 Discovery(`dcy-0004`) 범위를 기준으로 구현 계획을 확정했다. 근본 원인(`v1_portfolios.py` 생성 handler가 `repo.create_portfolio(...)`에 `managed_by_daemon_id`를 전달하지 않음)과 승인된 결정(존재·active·동일 소유자, 404/409/422 오류 계약, 원자적 판정 경계)을 반영해 작업 목록을 구성했다.
- 2026-08-15 15:10 KST — `app/domain/errors.py`에 `DaemonNotFoundError`·`DaemonInactiveError`를 추가하고, `create_portfolio` 프로토콜·구현과 INSERT 컬럼에 `managed_by_daemon_id`를 반영했다. `postgres.py`에 `_lock_active_daemon`(daemon row `FOR SHARE` + 존재·동일 소유자·active 검증)을 추가해 생성·수정 트랜잭션 안에서 원자적 판정 경계를 구성했다.
- 2026-08-15 15:10 KST — `POST /v1/portfolios`와 `PATCH /v1/portfolios/{id}` handler에 UUID 형식(422)·미존재/foreign(404)·inactive(409) 검증과 오류 매핑을 적용했다. admin target-user 생성은 최종 target owner 기준으로 daemon 소유권을 검증한다.
- 2026-08-15 15:12 KST — 기존 `test_v1_portfolios_crud.py`의 `_FakeRepo.create_portfolio` 시그니처를 새 파라미터에 맞춰 보완했다.
- 2026-08-15 15:15 KST — 전용 API 테스트 `tests/test_v1_portfolios_daemon_assignment.py`(13개)를 추가했다. POST 성공·null 유지·malformed 422·미존재/foreign 404·inactive 409·admin target-owner 허용/거부, PATCH 지정·재지정·malformed·foreign·inactive를 검증해 통과했다.
- 2026-08-15 15:18 KST — 실제 PostgreSQL 통합 테스트 `tests/test_v1_portfolios_daemon_postgres_integration.py`(4개)를 추가했다. HTTP `POST → GET` persistence, 검증 실패 시 portfolio 무변경, assignment vs deactivation race의 허용 outcome, PATCH 재배정 persistence를 실제 DB에서 검증했다.
- 2026-08-15 15:20 KST — 회귀 검증: 전체 단위 스위트 `625 passed, 11 skipped`, 실제 PostgreSQL 통합(daemon 4 + strategy 4) `8 passed`. Python compile, `git diff --check`, 변경 핵심 파일 Ruff 검사 통과(Ruff 이슈 44건은 기준 커밋과 동일한 기존 항목이며 신규 도입 없음).
> 각 작업이 실제로 완료되고 검증될 때마다 이 댓글의 체크박스와 진행 기록을 갱신한다. 체크 표시는 구현 완료 또는 해당 검증의 실제 통과를 의미하며, 배포·릴리스 완료를 의미하지 않는다. 현재 변경은 기준 HEAD 위 미커밋 상태로 보존되어 있으며 커밋·push·PR·main 반영·배포는 수행하지 않았다.