feat(api): add owner-scoped manual ledger endpoint
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
- Batch ID: BAT-024
|
||||
- Profile: standard
|
||||
- Status: in-progress
|
||||
- Status: implemented
|
||||
- Discovery: dcy-012_20260714_manual-ledger-write-api-owner-scoped
|
||||
- Included REQ: REQ-056
|
||||
- Folder: docs/batches/bat-024_20260714_manual-ledger-api/
|
||||
@@ -29,10 +29,10 @@ REQ-056 구현: trex-web이 user/session token으로 자기 소유 portfolio에
|
||||
|
||||
## DoD
|
||||
|
||||
- [ ] REQ-056 AC-1~19 테스트 전수 통과
|
||||
- [ ] 기존 `/ledger/trades`, `GET /v1/portfolios`, CLI/domain flow 회귀 없음
|
||||
- [ ] baseline docs(`interface-contract`, `runtime-flows`, `data-model`) 갱신
|
||||
- [ ] draft-batch-verification 문서에 AC별 PASS/FAIL evidence 기록
|
||||
- [x] REQ-056 AC-1~19 테스트 전수 통과
|
||||
- [x] 기존 `/ledger/trades`, `GET /v1/portfolios`, CLI/domain flow 회귀 없음
|
||||
- [x] baseline docs(`interface-contract`, `runtime-flows`, `data-model`) 갱신
|
||||
- [x] draft-batch-verification 문서에 AC별 PASS/FAIL evidence 기록
|
||||
- [ ] independent QC PASS 후 lead `confirm-batch-verification`
|
||||
|
||||
## 비가역 결재 게이트
|
||||
|
||||
@@ -1,18 +1,80 @@
|
||||
# BAT-024 Verification
|
||||
|
||||
- Status: draft
|
||||
- Verification owner: portfolio-hub-dev first pass; independent qc-reviewer required before lead confirmation.
|
||||
## Status
|
||||
|
||||
## Test Evidence
|
||||
- Stage: draft-batch-verification
|
||||
- Result: PASS (developer self-verification)
|
||||
- Date: 2026-07-14 KST
|
||||
|
||||
_To be filled by delivery owner._
|
||||
## Scope
|
||||
|
||||
## AC Matrix
|
||||
- REQ-056: owner-scoped 수동 장부 입력 HTTP API
|
||||
- Implemented endpoints:
|
||||
- `POST /v1/portfolios/{portfolio_id}/ledger/manual`
|
||||
- `GET /v1/portfolios/{portfolio_id}/ledger/manual`
|
||||
- `GET /v1/portfolios/{portfolio_id}/returns/summary`
|
||||
- Preserved endpoint:
|
||||
- `POST /v1/portfolios/{portfolio_id}/ledger/trades`
|
||||
|
||||
| AC | Status | Evidence |
|
||||
| --- | --- | --- |
|
||||
| REQ-056 AC-1~19 | Pending | Not yet implemented |
|
||||
## Evidence
|
||||
|
||||
## Result
|
||||
### Static/syntax
|
||||
|
||||
- Pending implementation.
|
||||
```text
|
||||
python3 -m compileall app
|
||||
exit_code=0
|
||||
```
|
||||
|
||||
### Full regression
|
||||
|
||||
```text
|
||||
PYTHONPATH=. uv run python -m pytest -q --override-ini addopts=''
|
||||
594 passed, 12 warnings in 37.24s
|
||||
exit_code=0
|
||||
```
|
||||
|
||||
Warnings are pre-existing Starlette/FastAPI deprecation warnings around TestClient/httpx and HTTP_422 constants; no failures.
|
||||
|
||||
### Targeted regression
|
||||
|
||||
```text
|
||||
PYTHONPATH=. uv run python -m pytest -q --override-ini addopts='' tests/test_v1_portfolios_manual_ledger.py tests/test_v1_portfolios_crud.py tests/test_v1_portfolios_trades.py
|
||||
45 passed, 8 warnings in 5.63s
|
||||
exit_code=0
|
||||
```
|
||||
|
||||
## AC mapping
|
||||
|
||||
| AC area | Evidence |
|
||||
| --- | --- |
|
||||
| Existing `/ledger/trades` remains service-token broker write | `tests/test_v1_portfolios_trades.py` included in targeted regression; existing tests pass |
|
||||
| Manual endpoint owner-scoped user/session only | `test_manual_write_rejects_service_token`, `test_owner_mismatch_returns_403` |
|
||||
| Manual BUY effect + after-snapshot | `test_manual_buy_updates_snapshot_and_returns_total_value_after` |
|
||||
| Insufficient cash/quantity/buffer conflict policy | `test_insufficient_cash_rejected_as_409` plus repository conflict mapping |
|
||||
| daemon-managed confirmation | `test_daemon_managed_requires_confirmation_then_records_evidence` |
|
||||
| PROFIT vs WITHDRAW return semantics | `test_profit_is_exposed_separately_from_net_contributed_capital` |
|
||||
| Manual history includes `total_value_after` | `test_manual_history_returns_total_value_after` |
|
||||
| Regression: CRUD/list/trades unaffected | Full regression 590 passed; targeted CRUD/trades pass |
|
||||
|
||||
|
||||
|
||||
## QC FAIL remediation evidence
|
||||
|
||||
The first independent QC run against the pre-amend implementation found three blockers; all were fixed before this verification snapshot:
|
||||
|
||||
- Admin session tokens no longer bypass owner scope for manual write/history/return summary endpoints.
|
||||
- Amount/reversal actions reject explicit `fee`/`tax` fields with HTTP 422.
|
||||
- REVERSAL target-not-found now maps to HTTP 404 `LEDGER_ENTRY_NOT_FOUND`.
|
||||
|
||||
Added regression coverage in `tests/test_v1_portfolios_manual_ledger.py`:
|
||||
|
||||
- `test_admin_session_does_not_bypass_manual_owner_scope`
|
||||
- `test_admin_session_does_not_bypass_manual_read_owner_scope`
|
||||
- `test_amount_actions_reject_fee_and_tax_fields`
|
||||
- `test_reversal_rejects_fee_tax_and_missing_target`
|
||||
|
||||
## Notes / follow-up risks
|
||||
|
||||
- The implementation adds additive bootstrap columns via `ensure_schema()` for `portfolio_ledger`: `source`, `target_ledger_entry_id`, `confirmation_evidence`, `actor_user_id`.
|
||||
- No Alembic migration file was added in this batch. Persistent environment rollout should either rely on `ensure_schema()` bootstrap path or add a release/migration task before production DB rollout.
|
||||
- `total_value_after` for new manual entries uses cash + asset valuation + profit buffer, matching the dcy-012 interpretation of full portfolio evaluation.
|
||||
|
||||
@@ -290,6 +290,38 @@
|
||||
- Payload conflict detection: if unique key matches but payload differs → HTTP 409 Conflict.
|
||||
- portfolio_id is TEXT (not a FK) but must correspond to a valid portfolios.portfolio_id before insertion.
|
||||
|
||||
|
||||
|
||||
## Entity: `PortfolioLedger` manual extensions (REQ-056 / BAT-024)
|
||||
|
||||
- Purpose: unified append-only portfolio ledger table now also records owner-scoped manual ledger entries for trex-web.
|
||||
- Source of Truth: `portfolio_ledger` table.
|
||||
- Lifecycle: manual API appends rows with `source='manual'`; previous rows are never updated/deleted. Corrections use `REVERSAL` rows referencing a target ledger entry.
|
||||
|
||||
### Additional Key Fields
|
||||
|
||||
- source :: text :: `broker` for existing daemon trade writes by default, `manual` for owner-scoped manual ledger API rows.
|
||||
- target_ledger_entry_id :: bigint|null :: required for `REVERSAL`; references the reversed ledger entry logically.
|
||||
- confirmation_evidence :: text|null :: records API-level explicit confirmation evidence for daemon-managed portfolio manual writes.
|
||||
- actor_user_id :: text|null :: user/session actor that appended the manual ledger entry.
|
||||
|
||||
### Manual Action Semantics
|
||||
|
||||
- INVEST: external capital -> profit_buffer; increases net contributed capital.
|
||||
- WITHDRAW: profit_buffer -> external capital as contributed-capital withdrawal; decreases net contributed capital.
|
||||
- PROFIT: profit_buffer -> external capital as pure profit withdrawal; does not affect net contributed capital and is exposed separately as `cumulative_profit_withdrawn`.
|
||||
- BUFFER_OUT: profit_buffer -> cash_balance.
|
||||
- BUFFER_IN: cash_balance -> profit_buffer.
|
||||
- BUY: cash_balance -> asset_quantity.
|
||||
- SELL: asset_quantity -> cash_balance.
|
||||
- REVERSAL: append-only inverse effect of `target_ledger_entry_id`.
|
||||
|
||||
### State Rules
|
||||
|
||||
- Manual writes lock the portfolio row while calculating/appending/updating snapshots to avoid lost updates.
|
||||
- Manual writes reject insufficient cash/quantity/buffer with HTTP 409 rather than creating a new negative state.
|
||||
- `total_value_after` for manual entries is the portfolio snapshot value after the mutation and includes cash, asset valuation, and profit buffer.
|
||||
|
||||
## Shared Consistency Rules
|
||||
|
||||
- auth_tokens.user_id and portfolios.owner_user_id should resolve to existing users before protected flows run.
|
||||
@@ -317,6 +349,7 @@
|
||||
|
||||
## Change Log
|
||||
|
||||
- 2026-07-14 KST: BAT-024 (REQ-056) — documented portfolio_ledger manual extensions (`source`, `target_ledger_entry_id`, `confirmation_evidence`, `actor_user_id`) and PROFIT/WITHDRAW return semantics.
|
||||
- 2026-05-21 KST: BAT-010 (REQ-032) — v1 portfolio surface 데이터 모델 보강: Portfolio.status 필드 신설 (`"enabled"` | `"disabled"` 의미, soft delete 정책); Portfolio.managed_by_daemon_id에 soft delete 시 NULL 설정 명시; tag_ids 소프트 삭제 보존 정책 추가; Portfolio State Rules에 disabled 상태 정책 + 의존 데이터 보존 + 상태 복원 규칙 추가; Source Discovery/Batch 헤더 갱신.
|
||||
- 2026-05-21 KST: BAT-008 (REQ-019) — 토큰 surface 정합성 회복: AuthToken token_type "client" 제거 반영; idx_auth_tokens_service_name_unique에 AND revoked_at IS NULL 조건 추가(REQ-017); Daemon 비활성화 시 바인딩 서비스 토큰 자동 revoke 정책 반영(REQ-022); User/AuthToken Purpose 문구 갱신.
|
||||
- 2026-05-20 KST: bat-005 — REQ-008 AC-1: added Entity: Tag (tags master table, BAT-003 migration 20260519_0017); updated Portfolio.tag_ids UUID[] field; removed portfolio_tags table note; updated Portfolio Relationships and State Rules; updated Model Summary.
|
||||
|
||||
@@ -389,6 +389,34 @@
|
||||
- Idempotency key: `(portfolio_id, order_id, execution_id)` — reuses `uq_ledger_trades_idempotency` constraint
|
||||
- **구 path `POST /v1/assets/{asset_id}/ledger/trades`**: BAT-011에서 제거됨. 호출 시 HTTP 404.
|
||||
|
||||
### Endpoint: `POST /v1/portfolios/{portfolio_id}/ledger/manual` (REQ-056, BAT-024)
|
||||
|
||||
- Auth: DB user/session token with portfolio ownership. Service/client tokens and env tokens are HTTP 403 fail-closed.
|
||||
- Purpose: trex-web/user 수동 장부 append. Existing `/ledger/trades` remains daemon/service-token broker execution write only.
|
||||
- Body: `ManualLedgerRequest` with `action` enum `INVEST|BUFFER_OUT|BUFFER_IN|BUY|SELL|WITHDRAW|PROFIT|REVERSAL`.
|
||||
- `INVEST`, `BUFFER_OUT`, `BUFFER_IN`, `WITHDRAW`, `PROFIT` require `amount`.
|
||||
- `BUY`, `SELL` require `quantity` and `price`; optional `fee`, `tax` default 0.
|
||||
- `REVERSAL` requires `target_ledger_entry_id`; direct mutation/delete of prior rows is not supported.
|
||||
- `QUANTITY_ADJUSTMENT` is not part of this API.
|
||||
- Response: HTTP 201 `ManualLedgerResponse` with `ledger_entry_id`, `portfolio_id`, `action`, `source=manual`, `cash_balance_after`, `asset_quantity_after`, `profit_buffer_after`, `total_value_after`, `target_ledger_entry_id`, `confirmation_evidence`.
|
||||
- Validation/conflict policy:
|
||||
- disabled portfolio → HTTP 409 `PORTFOLIO_DISABLED`.
|
||||
- daemon-managed portfolio without `confirm_daemon_managed_manual_write=true` → HTTP 409 `MANUAL_WRITE_REQUIRES_CONFIRMATION`.
|
||||
- insufficient cash/quantity/buffer → HTTP 409 `INSUFFICIENT_CASH|INSUFFICIENT_QUANTITY|INSUFFICIENT_BUFFER`.
|
||||
- duplicate reversal target → HTTP 409 `LEDGER_ENTRY_ALREADY_REVERSED`.
|
||||
- Return semantics: net contributed capital is `sum(INVEST) - sum(WITHDRAW)`; `PROFIT` is exposed as `cumulative_profit_withdrawn` and is not included in net contributed capital.
|
||||
|
||||
### Endpoint: `GET /v1/portfolios/{portfolio_id}/ledger/manual` (REQ-056, BAT-024)
|
||||
|
||||
- Auth: same owner-scoped user/session token policy as manual write.
|
||||
- Query: `limit` 1..200, default 50.
|
||||
- Response: `ManualLedgerHistoryResponse {"items": [ManualLedgerResponse], "total": <int>}` ordered newest first.
|
||||
|
||||
### Endpoint: `GET /v1/portfolios/{portfolio_id}/returns/summary` (REQ-056, BAT-024)
|
||||
|
||||
- Auth: owner-scoped user/session token.
|
||||
- Response: `PortfolioReturnSummaryResponse` with `total_invested`, `total_withdrawn`, `net_contributed_capital`, and `cumulative_profit_withdrawn`.
|
||||
|
||||
## Interface: `Tag Management API (BAT-004)`
|
||||
|
||||
- Type: http-api
|
||||
@@ -512,6 +540,7 @@
|
||||
|
||||
## Change Log
|
||||
|
||||
- 2026-07-14 KST: BAT-024 (REQ-056) — added owner-scoped manual ledger API, manual history, and return summary contracts; clarified PROFIT vs WITHDRAW return semantics and service-token separation from `/ledger/trades`.
|
||||
- 2026-05-21 KST: BAT-011 (REQ-029) — ledger path migration: "POST /v1/assets/{asset_id}/ledger/trades" 명세 → "POST /v1/portfolios/{portfolio_id}/ledger/trades"로 교체; auth 섹션 + daemon authorization 갱신; 구 path HTTP 404 명시.
|
||||
- 2026-05-21 KST: BAT-010 (REQ-032) — v1 portfolio surface baseline 회복: "v1 Portfolio API" 섹션 신규 추가 (REQ-025~028 CRUD + REQ-030 tag 서브 라우트 + REQ-031 comment CRUD); POST/DELETE /v1/portfolios/{id}/tags에 disabled 보호(409) 및 service/client 403 명세 추가; GET /v1/portfolios/{id}/tags 신설; ledger path "pending REQ-029" 마킹; Source Discovery/Batch 헤더 갱신.
|
||||
- 2026-05-21 KST: BAT-008 (REQ-019/020) — 토큰 surface 정합성 회복 + 권한 매트릭스: service-token rebind/migrate-null-daemon CLI 명세 추가; POST /auth/tokens 제거 명시(REQ-016); POST /auth/service-tokens daemon_id forward 명세 추가(REQ-018); Token Type Branching 갱신(client→Removed, session/env 명시); Daemon Authorization Model에 rebind/deactivate/null-daemon CLI 추가; "Token Issuance Authorization Matrix" 섹션 신규 추가(REQ-020).
|
||||
|
||||
@@ -255,6 +255,21 @@
|
||||
11. Duplicate key + different payload → HTTP 409.
|
||||
- 구 path `POST /v1/assets/{asset_id}/ledger/trades`: BAT-011에서 제거. 호출 시 HTTP 404.
|
||||
|
||||
|
||||
|
||||
## Flow: `v1 API — POST /v1/portfolios/{portfolio_id}/ledger/manual` (REQ-056, BAT-024)
|
||||
|
||||
1. DB user/session token auth required. Service/client/env tokens fail closed with HTTP 403.
|
||||
2. `_get_portfolio_or_404()` enforces owner scope; non-owner receives HTTP 403.
|
||||
3. Disabled portfolio receives HTTP 409 `PORTFOLIO_DISABLED`.
|
||||
4. If `managed_by_daemon_id IS NOT NULL` and request lacks `confirm_daemon_managed_manual_write=true`, the API returns HTTP 409 `MANUAL_WRITE_REQUIRES_CONFIRMATION` with daemon id and required confirmation marker.
|
||||
5. Valid actions dispatch to a single repository transaction that locks the portfolio row (`FOR UPDATE OF p`), calculates the action effect, appends a `portfolio_ledger` row with `source='manual'`, and updates the portfolio snapshot.
|
||||
6. BUY/SELL require sufficient cash/quantity. BUFFER_OUT/BUFFER_IN/WITHDRAW/PROFIT require sufficient buffer/cash as appropriate. Insufficient state returns HTTP 409 with structured code.
|
||||
7. REVERSAL requires `target_ledger_entry_id`, rejects duplicate reversal targets, and appends a reversing row instead of mutating/deleting the target.
|
||||
8. Response includes after-snapshot: `cash_balance_after`, `asset_quantity_after`, `profit_buffer_after`, `total_value_after`.
|
||||
9. `GET /v1/portfolios/{portfolio_id}/ledger/manual` returns newest manual entries only (`source='manual'`).
|
||||
10. `GET /v1/portfolios/{portfolio_id}/returns/summary` returns `net_contributed_capital=sum(INVEST)-sum(WITHDRAW)` and `cumulative_profit_withdrawn=sum(PROFIT)` separately.
|
||||
|
||||
## Flow: `v1 API — POST /auth/service-tokens`
|
||||
|
||||
1. Admin token auth required (`require_admin` dependency).
|
||||
|
||||
Reference in New Issue
Block a user