11 KiB
11 KiB
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로 제한한다.
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-origininvest,buffer_out,buyledger entry가 있고 manualINVEST/WITHDRAW가 없다. - 현재 portfolio API는 모든
source='broker'row를 집계에서 제외하므로net_contributed_capital: 0.0을 반환한다. - 같은 사례에서 broker
investamount는0,buffer_out은800,000이다. 이 이슈의 source-filter 수정만으로buffer_out을INVEST로 간주하거나 zero-amount event를 교정하지는 않는다.
Target Semantics
승인된 목표 semantics는 다음과 같다.
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,PROFITaccounting side 의미는 유지한다.- Original entry와 동일 portfolio에서
target_ledger_entry_id가 일치하는REVERSAL이 있으면 reversal 자체의 source와 무관하게 original entry를 제외한다. - Portfolio list와 single-portfolio returns summary가 동일한 aggregate semantics를 사용하게 한다.
- Non-manual
brokerINVEST와 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 범위
- 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 처리
- 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 처리 방식
- 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 방식
- 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 — 배포 후 검증 경계
- 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
brokerINVEST의 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 brokerinvest교정은 별도 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 권한을 자동으로 부여하지 않는다.