# REQ-027: DELETE /v1/portfolios/{portfolio_id} 신설 (soft delete) - Status: Implemented - Type: Interface - Priority: High - Owner: Justin Ko ## Intent - v1 API에 portfolio 종료 경로(`DELETE /v1/portfolios/{portfolio_id}`)를 신설해 user/admin이 HTTP로 portfolio를 종료할 수 있도록 한다. - dcy-003 OQ-1 결정에 따라 soft delete(`status='disabled'`)로 구현해 데이터 보존성과 운영 복구 가능성을 확보한다. hard delete 경로는 v1에 노출하지 않는다. ## Requirement - 신규 라우트 `DELETE /v1/portfolios/{portfolio_id}` (Bearer 인증 필수). - **토큰 분기 규칙** (REQ-025/026과 동일 패턴): - `token_type ∈ {service, client}` → HTTP 403 fail-closed. - 그 외(`token_type=session` 또는 env token): - `permission='admin'` → 모든 portfolio 삭제 가능. - `permission ∈ {read, trade-write}` → 자기 소유(`owner_user_id = auth.user_id`) portfolio만, 불일치 시 HTTP 403. - **soft delete 동작**: - `portfolios.status = 'disabled'`로 전환 - `managed_by_daemon_id`는 `NULL`로 자동 해제 - `updated_at` 갱신 - 의존 데이터(`portfolio_ledger`, `ledger_trades`, `portfolio_comments`, `portfolios.tag_ids`) 모두 보존 - **disabled 상태 write 차단** (REQ-026/029/030/031에서도 검증되는 정책): - `PATCH`(status 전이 외 필드 변경) → HTTP 409 또는 422 - `POST /ledger/trades` → 차단(REQ-029에서 처리) - tag/comment 추가·수정·삭제 → 차단(REQ-030/031에서 처리) - **복구 경로**: admin이 `PATCH /v1/portfolios/{portfolio_id}` body `{"status": "enabled"}`(또는 이전 status)로 전환(REQ-026 AC-9). daemon 재배정은 별도 호출. - 비존재 `portfolio_id`는 HTTP 404. ownership 불일치는 HTTP 403. - 이미 `disabled` 상태인 portfolio에 재호출 시 HTTP 200 + 멱등 응답(또는 HTTP 409 — 본 REQ 구현 시 확정). - 응답: HTTP 200 또는 204 + (HTTP 200 시) disabled 상태 portfolio 또는 확정 응답. ## Acceptance Criteria 1. Given session token + 본인 active portfolio, When `DELETE /v1/portfolios/{id}`, Then HTTP 200/204 + `portfolios.status='disabled'` + `managed_by_daemon_id IS NULL` 사후 상태. 2. Given AC-1 실행 후, When `portfolio_ledger`/`ledger_trades`/`portfolio_comments`/`portfolios.tag_ids` 조회, Then 의존 데이터가 모두 보존되어 있다. 3. Given admin token + 타 user active portfolio, When `DELETE /v1/portfolios/{id}`, Then HTTP 200/204 + 동일 사후 상태. 4. Given session token + 타 user portfolio, When `DELETE /v1/portfolios/{id}`, Then HTTP 403 (ownership 불일치). 5. Given service token, When `DELETE /v1/portfolios/{id}`, Then HTTP 403 fail-closed. 6. Given client token(폐지 전), When `DELETE /v1/portfolios/{id}`, Then HTTP 403 fail-closed. 7. Given Bearer token 없음, When `DELETE /v1/portfolios/{id}`, Then HTTP 401. 8. Given 비존재 `portfolio_id`, When `DELETE /v1/portfolios/{id}`, Then HTTP 404. 9. Given disabled portfolio + admin `PATCH status='enabled'`, When `PATCH`, Then HTTP 200 + 복구. (REQ-026 AC-9와 연계) 10. Given AC-1 실행 후 disabled portfolio에 ledger trade/tag/comment write 시도, When 시도, Then 모두 차단(HTTP 409 또는 422). (REQ-029/030/031과 연계 검증) ## Impacted Area - Docs: - `docs/interface-contract.md` — DELETE /v1/portfolios/{id} 명세 (REQ-032) - `docs/runtime-flows.md` — soft delete flow + disabled write 차단 + 복구 (REQ-032) - `docs/data-model.md` — Portfolio.status='disabled' 의미 + 의존 데이터 보존 정책 (REQ-032) - Modules: - `app/api/v1_portfolios.py` — DELETE 라우트 - `app/services/portfolio_service.py:delete` — 기존 메서드 재사용 시 soft delete로 변경 또는 신규 `soft_delete_portfolio` 헬퍼 - `app/repositories/portfolio_repository.py`, `app/repositories/postgres.py` — `set_portfolio_status(portfolio_id, status, clear_daemon=True)` 메서드 신규 또는 기존 `update_portfolio` 활용 - Tests: - 신규/갱신 `tests/test_v1_portfolios_crud.py` — AC-1~10 검증 ## Notes - dcy-003 R-2(disabled 누적 가시성·write 차단 일관성)는 본 REQ + REQ-028 `?include_disabled` opt-in 정책 + REQ-026/029/030/031의 차단 검증으로 함께 완화. - 누적 disabled 정리(아카이브, 영구 삭제 운영 SOP)는 본 Discovery 범위 밖. 별도 운영 Discovery로 위임. - 본 REQ는 hard delete 경로를 v1에 노출하지 않으므로 hard 삭제 운영은 DB 직접 접근 또는 별도 운영 도구로만 가능. - Source: dcy-003 FR-4, OQ-1, OQ-2, OQ-5, NFR-1, NFR-3, NFR-4. ## Change Log ### CHG-20260521-01 - Date: 2026-05-21 - Author: Justin Ko #### Change Summary - Status: Approved → Implemented #### Intent - BAT-009 confirm-batch-verification 완료. tests/test_v1_portfolios_crud.py AC-1~10 전 케이스 pytest 통과 (2026-05-21, 298 PASSED). soft_delete_portfolio: status='disabled', managed_by_daemon_id=NULL, 의존 데이터 보존. #### Acceptance Criteria Delta - No change #### Impacted Area Delta - No change #### Delivery Trace Delta - Batches: BAT-009 (release-candidate) - Releases: TBD #### Revalidation Impact - Reverification Needed: no - Existing Implementation Invalidated: no - Status Recommendation: promote-to-implemented - Follow-up Action: 없음 ### CHG-20260520-02 - Date: 2026-05-20 - Author: Justin Ko #### Change Summary - Status: Proposed → Approved #### Intent - 승인 게이트 통과: 필수 필드(Status/Type/Priority/Owner/Intent/Requirement/AC/Impacted Area/Change Log) 모두 충족, AC 구체성 확인 완료, blocker 없음 #### Acceptance Criteria Delta - No change #### Impacted Area Delta - No change #### Delivery Trace Delta - Batches: none (미정) - Releases: none (미정) #### Revalidation Impact - Reverification Needed: no - Existing Implementation Invalidated: no - Status Recommendation: promote-to-approved - Follow-up Action: batch 계획 편입 ### CHG-20260520-01 - Date: 2026-05-20 - Author: Justin Ko #### Change Summary - 최초 생성 — dcy-003 confirm-discovery 후 draft-req 실행. dcy-003 FR-4 산출물. #### Intent - v1 portfolio 종료 경로 + soft delete 정책 명시. #### Acceptance Criteria Delta - Added: - AC-1~10 최초 작성 #### Impacted Area Delta - Docs: docs/interface-contract.md, docs/runtime-flows.md, docs/data-model.md (REQ-032) - Modules: app/api/v1_portfolios.py, app/services/portfolio_service.py, app/repositories/* — 최초 지정 - Tests: tests/test_v1_portfolios_crud.py — 최초 지정 #### Delivery Trace Delta - Batches: none (미정) - Releases: none (미정) #### Revalidation Impact - Reverification Needed: no - Existing Implementation Invalidated: no - Status Recommendation: keep-current - Follow-up Action: confirm-req 후 batch 계획 편입; REQ-025/026과 동일 batch 권장 #### Validation Plan - pytest TestClient로 AC-1~10 검증; soft delete 사후 상태 + 의존 데이터 보존 + 복구 경로 + write 차단 일관성