228 lines
16 KiB
Markdown
228 lines
16 KiB
Markdown
---
|
|
id: "dcy-0005"
|
|
status: confirmed
|
|
---
|
|
|
|
# Portfolio ledger source-independent return aggregates
|
|
|
|
## Problem
|
|
|
|
`GET /v1/portfolios`와 `GET /v1/portfolios/{portfolio_id}/returns/summary`의 투자금 집계가 `portfolio_ledger.source = 'manual'`인 row만 포함해 broker 등 다른 producer가 기록한 유효한 원금·수익 event를 누락했다. 이 때문에 실제 ledger provenance와 API 집계값이 달라질 수 있으며, original source 제한만 제거하고 reversal source 제한을 남기면 cross-source reversal까지 잘못 집계할 수 있었다.
|
|
|
|
이 Discovery는 요청자가 OQ-1~OQ-5를 모두 결정하고 승인한 GitHub 이슈 #28을 canonical ProofLine 문서로 포착한다. 아래 `Scope`의 승인 이슈 본문은 canonical H2 충돌을 피하기 위해 heading만 한 단계 낮추고 나머지 내용을 최대한 원문 순서대로 보존한다.
|
|
|
|
## Evidence
|
|
|
|
### 승인 원문과 provenance
|
|
|
|
- 승인 원문: https://github.com/fourblocks2020/portfolio-hub/issues/28
|
|
- 제목: `Fix investment totals: remove source='manual' filter from ledger aggregates`
|
|
- 상태: `closed`
|
|
- 최초 작성: `2026-08-18T01:53:37Z`
|
|
- 승인 본문 최종 갱신: `2026-08-19T08:39:49Z`
|
|
- 승인 본문 SHA-256: `d5e5f65500d163bd5a9c9a97f47222e96373b3b6d0e09b021f1de3a2380a6014`
|
|
- Exact 승인 본문 snapshot: `evidence/issue-28-approved-discovery.md`
|
|
- 승인 결과: `APPROVED`; OQ-1~OQ-5는 각각 A로 결정됐고 unresolved blocking Open Question은 없다.
|
|
|
|
### 구현·검증 evidence
|
|
|
|
- 구현 기준 commit: `02bfa602995b8e1c2f410c9bd5762f062666f2d6`
|
|
- 구현 commit: `5eb8c042668b1c383ac59ca9be00aa1a5dfcb37a` (`fix: aggregate investment totals across ledger sources`)
|
|
- 구현 진행 comment: https://github.com/fourblocks2020/portfolio-hub/issues/28#issuecomment-5339488232; exact snapshot `evidence/implementation-progress.md`; SHA-256 `ada9617f9fb106e890ab4b232f23cde3b457b429971b664ed9fcc34689d03c52`
|
|
- 구현 완료 comment: https://github.com/fourblocks2020/portfolio-hub/issues/28#issuecomment-5339596866; exact snapshot `evidence/implementation-completion.md`; SHA-256 `3458db05561d843a7ecebab816f6aecc6120a6a2d62a4be151efd2876f0d3007`
|
|
- Focused repository/API/docs contract tests: `39 passed`.
|
|
- Disposable PostgreSQL 16 semantic integration: `1 passed`; broker-only, mixed-source, cross-source reversal 양방향과 `buffer_out` 제외를 실제 production repository SQL로 검증했다.
|
|
- 기존 `temp/deleted_20260813/.worktrees/` artifact를 제외한 clean candidate full suite: `661 passed, 16 skipped`.
|
|
- 변경 test Ruff와 `git diff --check`: PASS.
|
|
- 최초 독립 QC의 stale manual-only interface contract Major를 보완한 뒤 독립 재QC: PASS, Blocker 0 / Major 0 / Minor 0.
|
|
|
|
### Lifecycle 경계
|
|
|
|
- 현재 local `main`에 구현·검증·독립 재QC·commit까지 완료됐다.
|
|
- Push, PR, 원격 main 반영, release, production deployment/restart, production read-only pre/post check와 production ledger mutation은 수행되지 않았다.
|
|
- 이 confirmed Discovery와 사후 approved REQ/active AC는 normative scope와 현재 구현 평가를 기록하며 terminal lifecycle closure나 미수행 외부 반영을 주장하지 않는다.
|
|
|
|
## Scope
|
|
|
|
### 승인된 이슈 본문 전체
|
|
|
|
### Discovery: 투자 원금 집계의 source-independent 의미 복원
|
|
|
|
- **상태:** `APPROVED`
|
|
- **대상 저장소:** `fourblocks2020/portfolio-hub`
|
|
- **대상 API:** `GET /v1/portfolios`, `GET /v1/portfolios/{portfolio_id}/returns/summary`
|
|
- **관련 이슈:** #23의 manual-only cash-flow 집계 가정 중 Invest-total field 의미를 이 이슈가 대체한다.
|
|
|
|
#### Problem
|
|
|
|
`get_return_summaries()`는 현재 투자 원금 집계를 manual ledger row로 제한한다.
|
|
|
|
```sql
|
|
WHERE ledger.source = 'manual'
|
|
AND ledger.side IN ('invest', 'withdraw', 'profit')
|
|
```
|
|
|
|
그 결과 `total_invested`와 `net_contributed_capital`은 portfolio ledger 전체의 Invest/net Invest가 아니라 manual source의 cash flow만 반영한다. Broker 등 다른 source가 기록한 유효한 `INVEST`가 제외되므로 사용자에게 표시되는 투자 원금 의미와 실제 집계가 불일치한다.
|
|
|
|
이 동작은 #23의 manual-cash-flow contract에서 유래했지만, Invest-total field가 모든 유효한 투자 원금을 나타내야 한다면 적합하지 않다.
|
|
|
|
#### Evidence
|
|
|
|
- Production 관찰 사례인 portfolio `353200`에는 broker-origin `invest`, `buffer_out`, `buy` ledger entry가 있고 manual `INVEST`/`WITHDRAW`가 없다.
|
|
- 현재 portfolio API는 모든 `source='broker'` row를 집계에서 제외하므로 `net_contributed_capital: 0.0`을 반환한다.
|
|
- 같은 사례에서 broker `invest` amount는 `0`, `buffer_out`은 `800,000`이다. 이 이슈의 source-filter 수정만으로 `buffer_out`을 `INVEST`로 간주하거나 zero-amount event를 교정하지는 않는다.
|
|
|
|
#### Target Semantics
|
|
|
|
승인된 목표 semantics는 다음과 같다.
|
|
|
|
```text
|
|
total_invested = sum(valid INVEST entries from every source)
|
|
total_withdrawn = sum(valid WITHDRAW entries from every source)
|
|
net_contributed_capital = total_invested - total_withdrawn
|
|
cumulative_profit_withdrawn = sum(valid PROFIT entries from every source)
|
|
```
|
|
|
|
`valid`는 원본 ledger entry가 `REVERSAL.target_ledger_entry_id`로 연결된 matching reversal에 의해 취소되지 않았음을 뜻한다.
|
|
|
|
`total_invested`는 gross value인 `Σ INVEST`로 유지한다. `WITHDRAW`는 `total_withdrawn`과 `net_contributed_capital`에 반영하고, `PROFIT`는 투자 원금 계산에서 제외하여 `cumulative_profit_withdrawn`으로만 별도 집계한다.
|
|
|
|
#### Scope
|
|
|
|
- 투자 원금 aggregate에서 `ledger.source = 'manual'` predicate를 제거하고 모든 source의 유효한 `INVEST`·`WITHDRAW`·`PROFIT`을 side별로 집계한다.
|
|
- `INVEST`, `WITHDRAW`, `PROFIT` accounting side 의미는 유지한다.
|
|
- Original entry와 동일 portfolio에서 `target_ledger_entry_id`가 일치하는 `REVERSAL`이 있으면 reversal 자체의 source와 무관하게 original entry를 제외한다.
|
|
- Portfolio list와 single-portfolio returns summary가 동일한 aggregate semantics를 사용하게 한다.
|
|
- Non-manual `broker` INVEST와 mixed manual/broker INVEST·WITHDRAW·reversal 회귀를 검증한다.
|
|
- API/interface 문서에서 `total_invested`와 `net_contributed_capital`의 manual-only 표현을 승인된 의미로 갱신한다.
|
|
- 기존 owner/admin/service visibility와 authorization 경계를 변경하지 않는다.
|
|
|
|
#### Out of Scope
|
|
|
|
- `buffer_out`, `buy` 등 non-INVEST event를 INVEST로 재분류하는 작업
|
|
- Zero-amount broker `invest`를 non-zero event로 정규화하는 ledger-event modelling
|
|
- Existing ledger row의 mutation, backfill 또는 rewrite
|
|
- Ledger producer의 event emission 변경
|
|
- Reversal model 또는 `REVERSAL.target_ledger_entry_id` 의미 변경
|
|
- Owner/admin/service visibility 및 authorization 변경
|
|
- 이 Discovery 승인만으로 배포·릴리스·production mutation을 수행하는 것
|
|
|
|
#### Risks and Constraints
|
|
|
|
- Source predicate 제거는 과거 모든 source row에 즉시 적용되는 query semantic correction이므로 기존 응답 값이 증가하거나 달라질 수 있다.
|
|
- Side label이 같다는 이유만으로 `buffer_out`·`buy` 같은 다른 accounting event를 합산하면 안 된다.
|
|
- Reversal exclusion을 source-independent 집계와 함께 유지하지 않으면 reversed principal이 중복 반영될 수 있다.
|
|
- List와 detail summary가 다른 query path를 사용한다면 동일 fixture로 semantic parity를 검증해야 한다.
|
|
- `total_invested`의 의미 변경은 API field rename 없이 기존 field contract를 교정하는 선택인지 명시해야 한다.
|
|
- Production 사례 `353200`은 source-filter defect와 separate ledger-event modelling gap을 동시에 포함하므로, source-filter 수정만으로 `800,000`이 투자 원금으로 표시된다고 보장해서는 안 된다.
|
|
|
|
#### Open Questions
|
|
|
|
##### OQ-1 — Invest-total 집계의 source 범위
|
|
|
|
- [x] **A. 모든 source의 유효한 원금 event를 포함하되 `total_invested`는 gross `Σ INVEST`로 유지한다. — 결정**
|
|
- `total_invested = Σ valid INVEST`, `total_withdrawn = Σ valid WITHDRAW`, `net_contributed_capital = Σ valid INVEST - Σ valid WITHDRAW`로 계산한다.
|
|
- `PROFIT`는 원금 계산에서 제외하고 `cumulative_profit_withdrawn = Σ valid PROFIT`으로만 별도 집계한다.
|
|
- `ledger.source = 'manual'` predicate를 제거하고 source와 무관하게 accounting side와 reversal을 적용한다.
|
|
- [ ] **B. `manual`과 `broker`만 포함하는 allowlist를 사용한다.**
|
|
- 현재 알려진 두 source만 포함하지만 새 source가 추가되면 다시 누락될 수 있다.
|
|
- [ ] **C. Manual-only 의미를 유지하고 별도 all-source field를 추가한다.**
|
|
- 기존 값은 보존하지만 API schema와 UI 소비 계약이 확장되며 본 이슈의 단순 semantic correction 범위를 넘어간다.
|
|
|
|
##### OQ-2 — `buffer_out` 및 zero-amount broker `invest` 처리
|
|
|
|
- [x] **A. 이 이슈에서는 현행 accounting side를 그대로 사용하고 별도 ledger-event modelling 이슈로 분리한다. — 결정**
|
|
- `buffer_out`을 INVEST로 추론하지 않으며 source-filter defect만 교정한다.
|
|
- Portfolio `353200`의 `800,000` 표시 문제는 별도 결정 없이는 해결됐다고 주장하지 않는다.
|
|
- [ ] **B. `buffer_out`을 이 집계에서 INVEST로 간주한다.**
|
|
- 사용자 표시 값은 달라질 수 있지만 accounting event 의미를 query layer에서 재해석한다.
|
|
- [ ] **C. Broker producer가 non-zero `INVEST`를 기록하도록 이 이슈 범위를 확장한다.**
|
|
- Producer contract, historical data, deduplication과 migration 결정이 추가로 필요하다.
|
|
|
|
##### OQ-3 — 기존 ledger data 처리 방식
|
|
|
|
- [x] **A. Query semantic만 교정하고 existing ledger row를 mutation/backfill하지 않는다. — 결정**
|
|
- 모든 기존 유효 row가 새 query에 자동 반영되며 원장 provenance를 보존한다.
|
|
- [ ] **B. Source 또는 side를 migration으로 backfill한다.**
|
|
- Historical ledger mutation 범위, 대상 식별과 rollback/audit 계약이 별도로 필요하다.
|
|
- [ ] **C. 별도 aggregate projection/table을 만들어 재계산한다.**
|
|
- 지속 materialization, replay, ordering 및 consistency ownership이 추가된다.
|
|
|
|
##### OQ-4 — API compatibility 방식
|
|
|
|
- [x] **A. 기존 field 이름을 유지하고 의미를 all-source Invest-total로 교정한다. — 결정**
|
|
- `total_invested`, `total_withdrawn`, `net_contributed_capital`, `cumulative_profit_withdrawn`의 문서를 함께 갱신한다.
|
|
- [ ] **B. 새 field를 추가하고 기존 field는 manual-only로 유지한다.**
|
|
- 하위 호환은 보존하지만 response schema와 consumer 선택이 복잡해진다.
|
|
- [ ] **C. Versioned endpoint에서만 새 의미를 제공한다.**
|
|
- 가장 강한 contract 분리를 제공하지만 새 API version과 migration 계획이 필요하다.
|
|
|
|
##### OQ-5 — 배포 후 검증 경계
|
|
|
|
- [x] **A. Repository tests에 더해 승인된 read-only production query/API pre/post check를 별도 배포 절차에 포함한다. — 결정**
|
|
- Source-independent 합계, reversal 제외와 list/detail parity를 확인하되 production row는 변경하지 않는다.
|
|
- `353200`은 source-filter 제거 여부를 확인하는 관찰 사례로만 사용하고 `buffer_out=INVEST`를 기대하지 않는다.
|
|
- [ ] **B. Repository tests만 수행하고 production 검증은 하지 않는다.**
|
|
- 구현 검증은 단순하지만 실제 source 분포와 응답 변화 확인이 남는다.
|
|
- [ ] **C. Synthetic staging data에서만 post-deploy 검증한다.**
|
|
- Production read를 피하지만 실제 historical source/reversal 조합을 완전히 대표하지 못할 수 있다.
|
|
|
|
#### Acceptance Criteria
|
|
|
|
OQ가 확정되면 아래 criterion을 선택 결과에 맞춰 canonical requirement로 전환한다.
|
|
|
|
- [ ] Investment aggregate에서 original ledger row의 source predicate가 제거되고 모든 source의 유효 row가 side별로 집계된다.
|
|
- [ ] `total_invested = Σ valid INVEST`, `total_withdrawn = Σ valid WITHDRAW`, `net_contributed_capital = total_invested - total_withdrawn`이며 `PROFIT`는 원금 계산에서 제외되고 `cumulative_profit_withdrawn`으로만 집계된다.
|
|
- [ ] 동일 portfolio에서 matching `target_ledger_entry_id`를 가진 `REVERSAL`이 존재하면 reversal source와 무관하게 original entry가 제외된다.
|
|
- [ ] `GET /v1/portfolios`와 single-portfolio returns summary가 동일한 aggregate semantics를 사용한다.
|
|
- [ ] Non-manual `broker` INVEST의 non-zero amount 회귀가 검증된다.
|
|
- [ ] Mixed manual/broker INVEST·WITHDRAW, reversal과 source-independent total이 검증된다.
|
|
- [ ] API/interface 문서가 승인된 field 의미를 정확히 설명한다.
|
|
- [ ] Existing owner/admin/service visibility 경계가 유지된다.
|
|
- [ ] Out-of-scope인 `buffer_out` 재분류나 historical ledger mutation이 암묵적으로 포함되지 않는다.
|
|
|
|
#### Approval Record
|
|
|
|
- **승인 상태:** `APPROVED`
|
|
- **승인 결정:** OQ-1-A, OQ-2-A, OQ-3-A, OQ-4-A, OQ-5-A
|
|
- **OQ-1:** 모든 source의 유효한 row를 side별로 집계한다. `total_invested`는 gross `Σ INVEST`, `net_contributed_capital`은 `Σ INVEST - Σ WITHDRAW`이며 `PROFIT`는 원금 계산에서 제외한다.
|
|
- **OQ-2:** `buffer_out` 재분류와 zero-amount broker `invest` 교정은 별도 ledger-event modelling으로 분리한다.
|
|
- **OQ-3:** Query semantic만 교정하고 existing ledger row를 mutation/backfill하지 않는다.
|
|
- **OQ-4:** 기존 field 이름을 유지하고 all-source Invest-total 의미로 문서와 contract를 교정한다.
|
|
- **OQ-5:** Repository 검증에 더해 별도 배포 절차에서 승인된 read-only production query/API pre/post check를 수행한다. Production row mutation 권한은 포함하지 않는다.
|
|
- **Reversal 적용:** all-source original 집계에서 matching reversal도 source와 무관하게 적용한다. `reversal.source = 'manual'` predicate를 보존하면 broker original reversal이 누락되므로 허용하지 않는다.
|
|
- **추가 결정 검토:** 현재 구현·data-flow 검토에서 승인을 중지할 추가 material OQ는 발견되지 않았다.
|
|
- **권한 경계:** 이 승인은 이 Discovery와 bounded implementation/verification scope를 승인한다. Push, PR, release, deployment, production mutation 또는 issue closure 권한을 자동으로 부여하지 않는다.
|
|
|
|
|
|
### AC admission 분류
|
|
|
|
- `update`: 기존 active `ac-0001`, `ac-0002`, `ac-0003`, `ac-0005`의 manual-only 의미를 source-independent 계약과 현재 검증 상태로 교정한다.
|
|
- `satisfy`: 기존 active `ac-0004`의 목록 가시성·인증·필터 격리는 의미 변경 없이 이번 구현이 충족한다.
|
|
- `create`: 없음. 동일 behavior를 새 AC로 중복하지 않는다.
|
|
- `retire`: 없음.
|
|
- Release evidence와 terminal closure: 없음.
|
|
- Housekeeping: GitHub comment exact snapshot 두 건을 Line evidence로 보존한다.
|
|
|
|
## Out of Scope
|
|
|
|
- Existing ledger row mutation 또는 historical migration/backfill
|
|
- `buffer_out`을 `INVEST`로 재분류하거나 zero-amount broker event를 교정하는 ledger-event modelling
|
|
- 별도 aggregate projection/table 및 성능 근거 없는 schema 확장
|
|
- API field rename 또는 breaking response change
|
|
- Push, PR, 원격 main 반영, release, production deployment/restart
|
|
- Production write/mutation과 credential 기록
|
|
- 배포 절차의 승인된 read-only production pre/post check를 이미 완료했다고 주장하는 것
|
|
|
|
## Risks and Unknowns
|
|
|
|
### Risks
|
|
|
|
- 모든 source를 집계하므로 producer가 accounting side를 잘못 기록하면 그 modelling 오류가 aggregate에 노출될 수 있다. 이번 범위는 side 재분류가 아니라 유효한 기존 side의 source filter 교정이다.
|
|
- Cross-source reversal을 놓치면 original row가 잘못 남으므로 original과 reversal 양쪽의 source 제한을 제거하고 실제 PostgreSQL 테스트로 검증했다.
|
|
- 배포 전 실제 production data parity는 아직 미확인이다. 승인된 read-only pre/post check는 별도 배포 절차에서 수행해야 하며 mutation 권한을 포함하지 않는다.
|
|
|
|
### Open Questions
|
|
|
|
없음. OQ-1~OQ-5는 승인 이슈 본문에서 모두 결정됐으며 추가 blocking decision은 발견되지 않았다.
|