# REQ-056: owner-scoped 수동 장부 입력 HTTP API - Status: Implemented - Type: Interface - Priority: High - Owner: Justin Ko ## Intent - trex-web이 user/session token으로 자기 소유 portfolio에 수동 장부 entry를 append할 수 있는 portfolio-hub v1 API 계약을 신설한다. - 기존 `POST /v1/portfolios/{portfolio_id}/ledger/trades`는 trex-core/daemon/service-token broker 체결 보고 전용 계약으로 유지하고, 사용자 수동 입력은 신규 `POST /v1/portfolios/{portfolio_id}/ledger/manual`로 분리한다. - manual write는 portfolio snapshot(`cash_balance`, `asset_quantity`, `profit_buffer`)과 `portfolio_ledger` append를 단일 트랜잭션으로 처리하고, trex-web이 장부 이력/후속 상태/수익 실현액을 표시할 수 있는 응답 shape을 제공한다. ## Requirement - 신규 라우트 `POST /v1/portfolios/{portfolio_id}/ledger/manual`을 제공한다. - Bearer user/session token을 요구한다. - 요청자는 대상 portfolio의 `owner_user_id`와 일치해야 한다. owner mismatch는 HTTP 403 fail-closed로 거부한다. - service-token/daemon-token은 본 endpoint에서 HTTP 403 fail-closed로 거부한다. service-token broker trade write는 기존 `/ledger/trades`만 사용한다. - `status='disabled'` portfolio write는 HTTP 409로 거부한다. - request body는 단일 action-dispatch shape을 사용하며 `action` enum을 포함한다. | action | 필수 입력 | 선택 입력 | snapshot effect | |---|---|---|---| | `INVEST` | `amount` | `note` | `profit_buffer += amount` | | `BUFFER_OUT` | `amount` | `note` | `profit_buffer -= amount`, `cash_balance += amount` | | `BUFFER_IN` | `amount` | `note` | `cash_balance -= amount`, `profit_buffer += amount` | | `BUY` | `quantity`, `price` | `fee`, `tax`, `note` | `cash_balance -= quantity * price + fee + tax`, `asset_quantity += quantity` | | `SELL` | `quantity`, `price` | `fee`, `tax`, `note` | `asset_quantity -= quantity`, `cash_balance += quantity * price - fee - tax` | | `WITHDRAW` | `amount` | `note` | `profit_buffer -= amount`; 순입금액 계산에서 `sum(INVEST) - sum(WITHDRAW)`의 차감 항목 | | `PROFIT` | `amount` | `note` | `profit_buffer -= amount`; 순수 수익 실현 인출이며 순입금액 계산에는 미포함 | | `REVERSAL` | `target_ledger_entry_id` | `note` | target entry의 반대 effect를 서버가 계산해 append | - `QUANTITY_ADJUSTMENT`는 1차 범위에서 제외한다. manual endpoint enum/schema/test는 해당 action을 허용하지 않아야 한다. - 입력 값 검증: - `amount`, `quantity`, `price`는 0보다 커야 한다. - `fee`, `tax`는 생략 시 0으로 취급하며 음수를 허용하지 않는다. - action에 필요하지 않은 핵심 수치 필드가 포함되면 422로 거부하거나 schema에서 명시적으로 무시하지 말고 계약상 실패로 처리한다. - manual write는 새 음수 상태를 만드는 operation을 1차 범위에서 거부한다. - `BUY`: `quantity * price + fee + tax > cash_balance`이면 HTTP 409. - `SELL`: `quantity > asset_quantity`이면 HTTP 409. - `BUFFER_IN`: `amount > cash_balance`이면 HTTP 409. - `BUFFER_OUT`, `WITHDRAW`, `PROFIT`: `amount > profit_buffer`이면 HTTP 409. - `INVEST`: `amount > 0`이면 잔고 부족 검증 없이 허용한다. - daemon-managed portfolio(`managed_by_daemon_id IS NOT NULL`)에는 API-level explicit confirmation을 강제한다. - confirmation 없는 manual write는 HTTP 409 + error code `MANUAL_WRITE_REQUIRES_CONFIRMATION`으로 거부한다. - 사용자가 경고를 확인한 뒤 request body에 confirmation flag/evidence를 포함하면 write를 허용한다. - 허용된 write는 actor user, `managed_by_daemon_id`, confirmation evidence를 ledger/audit metadata 또는 동등한 추적 가능한 저장 위치에 남긴다. - `REVERSAL` 정책: - `target_ledger_entry_id`는 필수이며 같은 portfolio 소속 ledger entry여야 한다. - target entry를 직접 수정/삭제하지 않고 반대 effect의 신규 `REVERSAL` entry를 append한다. - target을 찾을 수 없거나 owner scope를 벗어나면 fail-closed(404 또는 403)한다. - 동일 target entry의 중복 reversal은 HTTP 409로 거부한다. - manual write 성공 응답은 HTTP 201을 기본으로 하며 다음 필드를 포함한다. - `ledger_entry_id`, `portfolio_id`, `action`, 입력 수치(`amount` 또는 `quantity`/`price`/`fee`/`tax`), `note`, `recorded_at` - `cash_balance_after`, `asset_quantity_after`, `profit_buffer_after`, `total_value_after` - `target_ledger_entry_id`(REVERSAL인 경우), `source="manual"` 또는 동등한 source 구분 필드 - daemon-managed confirmation이 적용된 경우 confirmation metadata 또는 audit reference - manual ledger history/read 계약은 trex-web 표시를 위해 portfolio별 최근 ledger entries를 반환해야 한다. - 기존 history endpoint를 확장하거나 신규 `GET /v1/portfolios/{portfolio_id}/ledger/manual`/동등 endpoint를 설계할 수 있으나, 응답 item에는 `action`, `amount`, `quantity`, `price`, `fee`, `tax`, `note`, `recorded_at`, `cash_balance_after`, `asset_quantity_after`, `profit_buffer_after`, `total_value_after`가 포함되어야 한다. - `total_value_after`는 ledger entry 기록 시점의 portfolio-hub snapshot 값이며 실시간 mark-to-market 값으로 해석하지 않는다. - 수익률/요약 응답은 `cumulative_profit_withdrawn`을 별도 지표로 노출해야 한다. - 값은 `PROFIT` action 누적액이다. - 순입금액은 `sum(INVEST) - sum(WITHDRAW)`로 계산하며 `PROFIT`은 이 계산에 포함하지 않는다. - ledger append와 portfolio snapshot update는 같은 DB 트랜잭션에서 수행하고, 같은 portfolio의 동시 manual write는 row lock 또는 동등한 concurrency control로 lost update 없이 직렬화한다. - 기존 `/ledger/trades` service-token broker write, `GET /v1/portfolios`, CLI portfolio invest/transfer/buy/sell 흐름은 본 endpoint 추가로 회귀하지 않아야 한다. ## Acceptance Criteria 1. Given owner user/session token + active portfolio, When `POST /v1/portfolios/{id}/ledger/manual` with `action="INVEST"`, `amount=100000`, Then HTTP 201 + `profit_buffer_after`가 100000 증가하고 `cash_balance_after`/`asset_quantity_after`는 기존 값이며 `portfolio_ledger` row가 append된다. 2. Given owner token + active portfolio(`profit_buffer=100000`, `cash_balance=0`), When `BUFFER_OUT amount=40000`, Then HTTP 201 + `profit_buffer_after=60000`, `cash_balance_after=40000`. 3. Given owner token + active portfolio(`cash_balance=40000`, `profit_buffer=60000`), When `BUFFER_IN amount=10000`, Then HTTP 201 + `cash_balance_after=30000`, `profit_buffer_after=70000`. 4. Given owner token + active portfolio(`cash_balance=1000000`, `asset_quantity=0`), When `BUY quantity=10 price=80000 fee=100 tax=0`, Then HTTP 201 + `cash_balance_after=199900`, `asset_quantity_after=10`. 5. Given owner token + active portfolio(`cash_balance=199900`, `asset_quantity=10`), When `SELL quantity=3 price=90000 fee=100 tax=0`, Then HTTP 201 + `asset_quantity_after=7`, `cash_balance_after=469800`. 6. Given owner token + active portfolio(`profit_buffer=70000`), When `WITHDRAW amount=20000`, Then HTTP 201 + `profit_buffer_after=50000` and the portfolio return/net-contribution calculation treats the amount as `sum(INVEST) - sum(WITHDRAW)` reduction. 7. Given owner token + active portfolio(`profit_buffer=50000`), When `PROFIT amount=10000`, Then HTTP 201 + `profit_buffer_after=40000`, `cumulative_profit_withdrawn` increases by 10000, and net contributed capital is unchanged. 8. Given owner token + existing ledger entry in the same portfolio, When `REVERSAL target_ledger_entry_id=` is posted, Then HTTP 201 + a new `REVERSAL` row is appended with the target's opposite snapshot effect and the original target row remains unchanged. 9. Given a target ledger entry was already reversed, When the same `target_ledger_entry_id` is reversed again, Then HTTP 409 and no additional ledger row/snapshot update occurs. 10. Given non-owner user/session token, When manual write or manual history/read is requested for another user's portfolio, Then HTTP 403 fail-closed and no ledger/snapshot mutation occurs. 11. Given service-token/daemon-token, When `POST /v1/portfolios/{id}/ledger/manual` is called, Then HTTP 403 fail-closed; When existing `POST /v1/portfolios/{id}/ledger/trades` is called with valid daemon assignment, Then existing broker trade contract remains unchanged. 12. Given disabled portfolio, When owner token posts any manual action, Then HTTP 409 and no ledger/snapshot mutation occurs. 13. Given daemon-managed portfolio and no confirmation flag/evidence, When owner token posts any manual action, Then HTTP 409 with error code `MANUAL_WRITE_REQUIRES_CONFIRMATION`; Given confirmation evidence is included, Then the write succeeds and actor/daemon/confirmation audit metadata is stored. 14. Given insufficient balances, When `BUY` exceeds cash, `SELL` exceeds quantity, `BUFFER_IN` exceeds cash, or `BUFFER_OUT`/`WITHDRAW`/`PROFIT` exceeds `profit_buffer`, Then HTTP 409 and ledger/snapshot remain unchanged for each case. 15. Given invalid action `QUANTITY_ADJUSTMENT` or missing/zero/negative required numeric fields, When manual write is posted, Then HTTP 422 and no ledger/snapshot mutation occurs. 16. Given successful manual writes, When manual ledger history/read is requested, Then each returned entry includes `action`, numeric inputs, `note`, `recorded_at`, `cash_balance_after`, `asset_quantity_after`, `profit_buffer_after`, `total_value_after`. 17. Given `PROFIT` entries exist, When return/summary response is requested, Then `cumulative_profit_withdrawn` is returned separately and net contribution uses `INVEST - WITHDRAW` without `PROFIT`. 18. Given two concurrent manual writes for the same portfolio, When both complete, Then row lock or equivalent control serializes updates without lost update; final snapshot equals sequential application of both accepted entries. 19. Given existing regression tests for `/ledger/trades`, `GET /v1/portfolios`, and CLI portfolio invest/transfer/buy/sell, When this REQ is implemented, Then those contracts still pass without semantic regression. ## Impacted Area - Docs: - `docs/interface-contract.md` — 신규 manual ledger write/read endpoint, request/response schema, error codes, `cumulative_profit_withdrawn` 지표 명세. - `docs/runtime-flows.md` — owner-scoped manual write flow, daemon-managed confirmation, transaction/row lock, reversal flow 추가. - `docs/data-model.md` — `portfolio_ledger` manual source/action metadata, reversal target reference, after-snapshot/cumulative profit semantics 보강. - Modules: - `app/api/v1_portfolios.py` 또는 신규 v1 router — `POST /v1/portfolios/{portfolio_id}/ledger/manual` 및 manual ledger history/read endpoint. - `app/schemas/v1.py` — manual request/response schemas, action enum, error response shape. - `app/domain/services.py` — manual action/effect helper, negative-state validation, reversal calculation, `PROFIT` vs `WITHDRAW` classification. - `app/repositories/portfolio_repository.py`, `app/repositories/postgres.py` — transaction/row lock, ledger append, reversal target lookup/dedup, history/read, `cumulative_profit_withdrawn` aggregate. - auth dependencies — user/session owner-scope write guard and service-token fail-closed guard. - Tests: - 신규/갱신 API tests for AC-1~17. - repository/concurrency tests for AC-18. - regression tests for existing `/ledger/trades`, portfolio list, CLI/domain flow compatibility (AC-19). ## Notes - Source: `docs/discovery/dcy-012_20260714_manual-ledger-write-api-owner-scoped.md` confirmed by Justin Ko, Ready for REQ Drafting true. - Project #7 item: `PVTI_lADOBCBKFM4BbUVHzgyvA9A` — Status Requirements. - 본 REQ는 Interface 중심이다. Data 변경(예: reversal target reference 저장 위치, confirmation metadata 저장 위치)이 필요할 수 있으나, 현재는 API 계약과 수락 기준에서 추적하고 batch design에서 기존 `portfolio_ledger` metadata로 충분한지 또는 migration REQ 분리가 필요한지 판정한다. - 기존 `/ledger/trades`는 daemon/service-token broker write 전용으로 보존한다. user/session token manual BUY/SELL은 `/ledger/trades`로 우회 허용하지 않는다. - `PROFIT`과 `WITHDRAW`는 snapshot effect가 모두 `profit_buffer` 감소일 수 있으나 성과 계산 의미가 다르다. `WITHDRAW`는 원금/입금액 회수로 순입금액을 감소시키고, `PROFIT`은 순수 수익 실현 인출로 `cumulative_profit_withdrawn`에만 누적한다. - `total_value_after`는 ledger 기록 시점의 snapshot 값이며, 실시간 가격 재평가 결과가 아니다. - `QUANTITY_ADJUSTMENT`는 후속 Discovery/REQ 후보이며 본 REQ 구현 범위가 아니다. ## Change Log ### CHG-20260714-01 - Date: 2026-07-14 - Author: portfolio-hub-dev (Hermes) #### Change Summary - 신규 생성 (Proposed) — dcy-012 confirmed Discovery 근거로 draft-req 실행. owner-scoped manual ledger write/read API 계약을 정의하고 기존 daemon/service-token `/ledger/trades` 계약 보존을 명시한다. #### Intent - trex-web manual ledger UI가 소비할 portfolio-hub API surface를 `POST /v1/portfolios/{portfolio_id}/ledger/manual` + action enum으로 고정하고, action/effect/권한/409 validation/daemon-managed confirmation/history response/PROFIT 계산 의미를 수락 가능 요구사항으로 정규화한다. #### Acceptance Criteria Delta - Added: - AC-1~19 최초 작성 - Updated: 없음 - Removed: 없음 #### Impacted Area Delta - Docs: `docs/interface-contract.md`, `docs/runtime-flows.md`, `docs/data-model.md` 갱신 예고 - Modules: v1 portfolio router, v1 schemas, domain services, portfolio repository/postgres repository, auth dependency 확장 - Tests: manual action/effect, owner/non-owner/service-token/disabled/daemon-confirmation, insufficient balance, reversal, history/read, cumulative profit, concurrency, existing ledger trade/CLI regression #### Delivery Trace Delta - Batches: 미정 (confirm-req 이후 suggest-batch-reqs) - Releases: 미정 #### Revalidation Impact - Reverification Needed: no - Existing Implementation Invalidated: no - Status Recommendation: keep-current - Follow-up Action: confirm-req(팀장 승인) → suggest-batch-reqs. Design 단계에서 reversal/confirmation metadata 저장 위치가 migration 필요 여부를 확정한다. #### Validation Plan - pytest TestClient/API tests로 AC-1~17 검증. repository/integration 또는 concurrency harness로 AC-18 검증. 기존 `/ledger/trades`, `GET /v1/portfolios`, CLI/domain 흐름 regression으로 AC-19 검증. ### CHG-20260714-02 - Date: 2026-07-14 - Author: TREX lead (Hermes) #### Change Summary - Status: Proposed → Approved (confirm-req). dcy-012 confirmed Discovery와 AC-1~19가 test 가능한 구현 기준으로 충분하다고 판정한다. #### Intent - owner-scoped manual ledger API를 batch delivery 기준으로 확정한다. #### Acceptance Criteria Delta - Added: 없음 - Updated: 없음 - Removed: 없음 #### Impacted Area Delta - 변경 없음. 구현 단계에서 docs/interface-contract.md, docs/runtime-flows.md, docs/data-model.md, API/schema/domain/repository/auth/tests를 갱신한다. #### Delivery Trace Delta - Batches: BAT-024 후보로 제안 - Releases: 미정 #### Revalidation Impact - Reverification Needed: no - Existing Implementation Invalidated: no - Status Recommendation: proceed-to-batch - Follow-up Action: suggest-batch-reqs → draft-batch/planning/design/implementation/verification. #### Validation Plan - AC-1~19를 batch verification에서 API/repository/concurrency/regression evidence로 검증한다. ## Implementation Confirmation - Confirmed: 2026-07-14 KST - Implementing Batch: BAT-024 — `docs/batches/bat-024_20260714_manual-ledger-api/` - Implementation commit: `b3447f6 feat(api): add owner-scoped manual ledger endpoint` - Developer verification: `594 passed, 12 warnings in 37.24s` - Independent QC: PASS (`deleg_f2337a39`) — `594 passed, 12 warnings in 37.29s`; focused remediation tests `4 passed`. - Lead gate: `confirm-req-implemented` accepted. - Release caveat: production DB rollout needs release-gate handling for additive `portfolio_ledger` columns (`source`, `target_ledger_entry_id`, `confirmation_evidence`, `actor_user_id`), preferably Alembic migration/runbook instead of relying only on `ensure_schema()`.