docs: record ProofLine specification for issue 28
This commit is contained in:
@@ -3,14 +3,15 @@ id: "ac-0001"
|
||||
status: active
|
||||
---
|
||||
|
||||
# 포트폴리오 목록의 ledger 투자금 집계 필드
|
||||
# 포트폴리오 목록의 source-independent 투자금 집계 필드
|
||||
|
||||
## Criterion
|
||||
|
||||
`GET /v1/portfolios`의 각 `PortfolioItem`은 기존 응답 필드를 유지하면서 `net_contributed_capital`과 `cumulative_profit_withdrawn`을 float 값으로 포함해야 한다. 전자는 순투자현금, 후자는 누적 수익 인출액을 뜻하며, 해당 portfolio에 유효한 manual ledger가 없거나 집계 결과가 없으면 두 값은 각각 `0.0`이어야 한다.
|
||||
`GET /v1/portfolios`의 각 `PortfolioItem`은 기존 응답 필드를 유지하면서 `net_contributed_capital`과 `cumulative_profit_withdrawn`을 float 값으로 포함해야 한다. 전자는 모든 source의 유효한 `INVEST` gross 합계에서 유효한 `WITHDRAW` 합계를 뺀 순투자원금이고, 후자는 원금과 분리한 모든 source의 유효한 `PROFIT` 합계다. 해당 portfolio에 유효한 대상 ledger event가 없으면 두 값은 각각 `0.0`이어야 한다.
|
||||
|
||||
## Verification
|
||||
|
||||
- 성공 조건: ledger 집계가 있는 portfolio는 두 필드에 해당 집계값을 반환하고, ledger 집계가 없는 portfolio는 두 필드를 `0.0`으로 반환한다.
|
||||
- 실패·경계 조건: 빈 목록은 `portfolios: []`, `total: 0`을 반환하며 신규 필드 추가 때문에 기존 응답 필드가 제거되거나 값의 의미가 바뀌지 않는다.
|
||||
- 검사 방법: `tests/test_v1_portfolios_list.py`의 목록 집계값 결합·기본값·빈 목록·기존 필드 회귀 테스트와 OpenAPI schema의 `PortfolioItem`을 검사한다.
|
||||
- 성공 조건: broker-only와 manual/broker mixed-source portfolio가 source와 무관한 side별 합계로 두 필드를 반환하고, ledger 집계가 없는 portfolio는 두 필드를 `0.0`으로 반환한다.
|
||||
- 실패·경계 조건: `PROFIT`이 원금에 포함되거나 `buffer_out` 등 다른 side가 `INVEST`로 재해석되거나, 기존 응답 필드·token/filter 동작이 변경되면 실패한다.
|
||||
- 검사 방법: `tests/test_portfolio_ledger_aggregates.py`, `tests/test_v1_portfolios_list.py`, `tests/test_v1_portfolios_manual_ledger.py`와 disposable PostgreSQL integration을 실행한다.
|
||||
- 현재 구현 평가: focused `39 passed`, actual PostgreSQL semantic integration `1 passed`, clean full regression `661 passed, 16 skipped`이며 독립 재QC가 PASS했다.
|
||||
|
||||
@@ -3,14 +3,15 @@ id: "ac-0002"
|
||||
status: active
|
||||
---
|
||||
|
||||
# REVERSAL-aware manual ledger 산식과 목록·단건 일치
|
||||
# Source-independent REVERSAL 산식과 목록·단건 일치
|
||||
|
||||
## Criterion
|
||||
|
||||
`net_contributed_capital`은 `REVERSAL`되지 않은 manual `INVEST` 합계에서 `REVERSAL`되지 않은 manual `WITHDRAW` 합계를 뺀 값이어야 하고, `cumulative_profit_withdrawn`은 `REVERSAL`되지 않은 manual `PROFIT` 합계여야 한다. `REVERSAL`이 참조한 `INVEST`, `WITHDRAW`, `PROFIT` 원거래는 모든 return aggregate에서 제외하고 `REVERSAL` 자체는 금액으로 합산하지 않으며, 목록과 단건 `returns/summary`는 같은 유효 원장 판정과 네 집계값 의미를 사용해야 한다.
|
||||
`total_invested`는 모든 source의 유효한 `INVEST` gross 합계, `total_withdrawn`은 유효한 `WITHDRAW` 합계, `net_contributed_capital`은 두 값의 차, `cumulative_profit_withdrawn`은 원금에서 제외한 유효한 `PROFIT` 합계여야 한다. 동일 portfolio에서 matching `REVERSAL.target_ledger_entry_id`로 참조된 original `INVEST`, `WITHDRAW`, `PROFIT`은 original과 reversal의 source와 무관하게 모든 return aggregate에서 제외하고 `REVERSAL` 자체는 금액으로 합산하지 않는다. 목록과 단건 `returns/summary`는 같은 repository aggregate와 네 집계값 의미를 사용해야 한다.
|
||||
|
||||
## Verification
|
||||
|
||||
- 성공 조건: 미취소 `INVEST`, `WITHDRAW`, `PROFIT` 조합에서 `PROFIT`은 `WITHDRAW`와 분리되고 목록과 단건 summary의 `total_invested`, `total_withdrawn`, `net_contributed_capital`, `cumulative_profit_withdrawn`이 일치한다.
|
||||
- 실패·경계 조건: `INVEST`, `WITHDRAW`, `PROFIT`을 각각 reversal하면 해당 원거래 효과가 제거되며 reversal row 자체가 합계에 더해지지 않는다.
|
||||
- 검사 방법: `tests/test_portfolio_ledger_aggregates.py`의 shared batch semantics와 `tests/test_v1_portfolios_manual_ledger.py`의 profit 분리 및 세 action reversal 회귀를 실행한다.
|
||||
- 성공 조건: broker-only와 mixed-source `INVEST`·`WITHDRAW`·`PROFIT` 조합에서 gross·withdrawn·net·profit 값이 승인 산식과 일치하고 목록과 단건 summary가 일치한다.
|
||||
- 실패·경계 조건: manual reversal→broker original 또는 broker reversal→manual original에서 original 효과가 남거나, `PROFIT`이 원금에 섞이거나 reversal row 자체가 합산되면 실패한다.
|
||||
- 검사 방법: SQL contract unit test와 실제 PostgreSQL 16 integration에서 production repository SQL, 양방향 cross-source reversal, `buffer_out` 제외를 검사한다.
|
||||
- 현재 구현 평가: source predicate가 original과 reversal 양쪽에서 제거됐고 actual PostgreSQL semantic integration 및 독립 재QC가 PASS했다.
|
||||
|
||||
@@ -3,14 +3,15 @@ id: "ac-0003"
|
||||
status: active
|
||||
---
|
||||
|
||||
# 다중 포트폴리오 grouped aggregate와 N+1 방지
|
||||
# 다중 포트폴리오 source-independent grouped aggregate와 N+1 방지
|
||||
|
||||
## Criterion
|
||||
|
||||
목록용 repository 계약은 반환 대상인 여러 portfolio ID의 유효 manual ledger 집계를 하나의 grouped aggregate query로 계산해 portfolio ID별 결과를 반환해야 하며, 목록 크기에 비례하는 portfolio별 ledger query를 수행해서는 안 된다. 입력 ID 목록이 비어 있으면 schema 확인이나 DB 연결·query 없이 빈 mapping을 반환해야 한다.
|
||||
목록용 repository 계약은 반환 대상인 여러 portfolio ID의 source-independent valid ledger aggregate를 하나의 grouped query로 계산해 portfolio ID별 결과를 반환해야 하며, 목록 크기에 비례하는 portfolio별 ledger query를 수행해서는 안 된다. 입력 ID 목록이 비어 있으면 schema 확인이나 DB 연결·query 없이 빈 mapping을 반환해야 한다.
|
||||
|
||||
## Verification
|
||||
|
||||
- 성공 조건: 둘 이상의 portfolio ID를 한 번에 전달했을 때 단일 query가 실행되고 각 ID의 합계가 독립된 mapping으로 반환된다.
|
||||
- 실패·경계 조건: 빈 ID 입력은 `{}`를 반환하고 DB connection 및 schema/query 호출이 발생하지 않는다.
|
||||
- 검사 방법: `tests/test_portfolio_ledger_aggregates.py`에서 `GROUP BY ledger.portfolio_id`, portfolio ID 배열 parameter, 단일 `execute`, 빈 입력 no-call을 검사하고 `tests/test_v1_portfolios_list.py`에서 route가 summary batch를 한 번 요청하는지 확인한다.
|
||||
- 성공 조건: 둘 이상의 portfolio ID를 한 번에 전달했을 때 단일 query가 실행되고 broker/manual source가 섞여도 각 ID의 side별 합계가 독립된 mapping으로 반환된다.
|
||||
- 실패·경계 조건: source predicate가 다시 도입되거나 portfolio별 N+1 query가 발생하거나, 빈 ID 입력에서 DB 작업이 발생하거나, portfolio ID 사이 금액이 섞이면 실패한다.
|
||||
- 검사 방법: `tests/test_portfolio_ledger_aggregates.py`에서 `GROUP BY ledger.portfolio_id`, ID 배열 parameter, 단일 execute와 빈 입력 no-call을 검사하고 list route의 summary batch 요청을 검증한다.
|
||||
- 현재 구현 평가: grouped query 구조와 empty-input fast path를 유지한 채 source predicate만 제거했으며 focused·full regression과 독립 재QC가 PASS했다.
|
||||
|
||||
@@ -3,14 +3,15 @@ id: "ac-0005"
|
||||
status: active
|
||||
---
|
||||
|
||||
# additive API·문서 호환성과 reversal 회귀 보존
|
||||
# API·문서 호환성과 query-only source-independent 교정
|
||||
|
||||
## Criterion
|
||||
|
||||
ledger 투자금 집계 확장은 기존 `GET /v1/portfolios` 및 단건 returns summary 계약에 대한 additive 변경이어야 한다. OpenAPI와 repository-owned interface/runtime 문서는 `net_contributed_capital`을 순투자현금, `cumulative_profit_withdrawn`을 누적 수익 인출액으로 정의하고 `portfolio-hub`가 ledger 권위 값을 제공함을 명시해야 하며, 기존 이중 reversal 거부 동작을 유지해야 한다.
|
||||
Ledger 투자금 집계 교정은 기존 `GET /v1/portfolios` 및 단건 returns summary의 field 이름과 응답 구조를 유지해야 한다. OpenAPI schema와 repository-owned interface/data-model/runtime 문서는 source-independent valid `INVEST`·`WITHDRAW`·`PROFIT`, gross `total_invested`, `INVEST-WITHDRAW` net, 별도 `PROFIT`, source-independent matching reversal과 non-capital side 제외를 일관되게 설명해야 한다. 교정은 existing ledger row mutation/backfill 또는 `buffer_out` 재분류 없이 query semantic에 한정돼야 한다.
|
||||
|
||||
## Verification
|
||||
|
||||
- 성공 조건: 기존 목록 필드와 token/filter 동작이 보존되고 `docs/interface-contract.md`, `docs/runtime-flows.md`, OpenAPI schema가 두 필드의 산식·REVERSAL·권위 경계를 일관되게 설명한다.
|
||||
- 실패·경계 조건: `PROFIT`을 `WITHDRAW` 또는 전체 실현손익으로 설명하지 않고, 동일 원거래를 두 번 reversal하려는 기존 요청은 계속 거부된다.
|
||||
- 검사 방법: 목록·manual ledger 회귀 테스트와 문서·OpenAPI 검사를 실행하고 focused suite 및 전체 프로젝트 suite를 사용해 기존 동작 회귀 여부를 판정한다.
|
||||
- 성공 조건: `app/schemas/v1.py`, `docs/interface-contract.md`, `docs/data-model.md`, `docs/runtime-flows.md`가 같은 산식·reversal·권위 경계를 설명하고 기존 field·auth·filter 계약을 보존한다.
|
||||
- 실패·경계 조건: stale manual-only 문구가 남거나 `PROFIT`을 원금/전체 실현손익으로 설명하거나, API rename, ledger mutation/backfill, `buffer_out=INVEST` 재분류 또는 권한 우회가 발생하면 실패한다.
|
||||
- 검사 방법: docs contract regression, API/list/manual ledger focused suite, 변경 diff와 migration/ledger write 부재를 검사한다.
|
||||
- 현재 구현 평가: 최초 독립 QC가 발견한 stale interface contract를 보완하고 manual-only 문구 부재 회귀 테스트를 추가했으며 재QC는 PASS(Blocker 0 / Major 0 / Minor 0)했다.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"next_line_number": 5,
|
||||
"next_line_number": 6,
|
||||
"next_ac_number": 32
|
||||
}
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
---
|
||||
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은 발견되지 않았다.
|
||||
@@ -0,0 +1,50 @@
|
||||
## 구현 완료
|
||||
|
||||
### 구현 내용
|
||||
|
||||
- Portfolio return aggregate에서 original ledger의 `source='manual'` 제한을 제거해 모든 source의 유효한 `INVEST`·`WITHDRAW`·`PROFIT`을 side별로 집계한다.
|
||||
- `total_invested`는 gross `Σ INVEST`, `total_withdrawn`은 `Σ WITHDRAW`, `net_contributed_capital`은 두 값의 차로 유지한다.
|
||||
- `PROFIT`는 투자 원금 계산에서 제외하고 `cumulative_profit_withdrawn`으로만 별도 집계한다.
|
||||
- Matching `REVERSAL.target_ledger_entry_id`는 original과 reversal의 source와 무관하게 original entry를 제외한다.
|
||||
- `buffer_out` 등 non-capital side는 `INVEST`로 재해석하거나 집계하지 않는다.
|
||||
- Existing ledger row mutation/backfill 없이 query semantic만 교정했다.
|
||||
- List와 단건 returns summary가 동일한 repository aggregate를 사용하도록 유지했다.
|
||||
- API schema, data model, runtime flow와 외부 interface contract를 all-source 의미로 동기화했다.
|
||||
|
||||
### 실제 검증 결과
|
||||
|
||||
- Focused repository/API/docs contract tests: **39 passed**
|
||||
- Disposable PostgreSQL 16 semantic integration: **1 passed**
|
||||
- Non-zero broker-only INVEST
|
||||
- Manual/broker mixed INVEST·WITHDRAW·PROFIT
|
||||
- Manual reversal → broker original
|
||||
- Broker reversal → manual original
|
||||
- `buffer_out` 집계 제외
|
||||
- Clean candidate full suite: **661 passed, 16 skipped**
|
||||
- 변경 테스트 범위 Ruff: **PASS**
|
||||
- `git diff --check`: **PASS**
|
||||
- 최초 독립 QC: Blocker 0 / Major 1 / Minor 0 — stale manual-only interface contract 발견
|
||||
- 보완 후 독립 재QC: **PASS — Blocker 0 / Major 0 / Minor 0**
|
||||
|
||||
원본 checkout의 full suite에서는 기존 `temp/deleted_20260813/.worktrees/` 복제본을 grep하는 auth-surface tests 3건만 실패했다. 해당 pre-existing artifact를 수정·삭제하지 않고 제외한 clean candidate copy에서 전체 회귀가 통과했다.
|
||||
|
||||
### 비차단 후속 권고
|
||||
|
||||
- 배포 절차에서 승인된 read-only production pre/post check로 실제 portfolio의 source-independent 합계와 list/detail parity를 확인한다.
|
||||
- Portfolio `353200`은 source-filter 교정 관찰에만 사용하며 `buffer_out=INVEST` 또는 zero-amount broker `invest` 교정 완료를 주장하지 않는다.
|
||||
- `buffer_out` 재분류나 producer의 ledger-event modelling 변경이 필요하면 별도 Discovery로 결정한다.
|
||||
|
||||
### 완료 및 외부 반영 경계
|
||||
|
||||
- 로컬 `main` 구현·검증·독립 재QC·커밋 완료:
|
||||
- `5eb8c042668b1c383ac59ca9be00aa1a5dfcb37a`
|
||||
- `fix: aggregate investment totals across ledger sources`
|
||||
- Tracked worktree/index: clean
|
||||
- 원격 push: **수행하지 않음**
|
||||
- PR: **생성하지 않음**
|
||||
- Release: **수행하지 않음**
|
||||
- Production deployment/restart: **수행하지 않음**
|
||||
- Production read-only pre/post check: **배포 절차에서 수행 예정이며 현재 미수행**
|
||||
- Production ledger mutation: **수행하지 않음**
|
||||
|
||||
따라서 이 완료는 로컬 제품 구현·검증·커밋 완료를 의미하며 원격 통합, release 또는 production deployment 완료를 의미하지 않는다.
|
||||
@@ -0,0 +1,46 @@
|
||||
## 구현 진행 상황
|
||||
|
||||
- **대상:** Fix investment totals: remove source='manual' filter from ledger aggregates
|
||||
- **브랜치:** `main`
|
||||
- **기준:** `main` (`02bfa602995b8e1c2f410c9bd5762f062666f2d6`)
|
||||
- **상태:** 로컬 `main` 구현·검증·독립 재QC·커밋 완료 — 미푸시, 미배포
|
||||
- **최종 업데이트:** 2026-08-19 17:34:13 KST
|
||||
|
||||
### 구현 계획 및 작업 목록
|
||||
|
||||
- [x] 투자 원금 집계를 모든 source로 확장
|
||||
- `total_invested`는 유효한 모든 `INVEST`의 gross 합계로 유지하고, `net_contributed_capital`은 `INVEST - WITHDRAW`로 계산한다.
|
||||
- [x] 수익 인출을 투자 원금과 분리
|
||||
- `PROFIT`는 원금 계산에서 제외하고 `cumulative_profit_withdrawn`으로만 집계한다.
|
||||
- [x] Source와 무관한 reversal 제외 보장
|
||||
- 동일 portfolio의 matching `REVERSAL.target_ledger_entry_id`가 있으면 original row와 reversal의 source와 관계없이 원본을 제외한다.
|
||||
- [x] 기존 ledger data와 accounting side 경계 보존
|
||||
- Existing ledger row를 mutation/backfill하지 않고 query semantic만 교정하며 `buffer_out` 등 다른 side를 `INVEST`로 재해석하지 않는다.
|
||||
- [x] List와 단건 returns summary의 동일한 의미 유지
|
||||
- `GET /v1/portfolios`와 `GET /v1/portfolios/{portfolio_id}/returns/summary`가 같은 repository aggregate를 사용함을 검증한다.
|
||||
- [x] Non-manual 및 mixed-source 회귀 테스트 추가
|
||||
- Non-zero broker `INVEST`, manual/broker 혼합 `INVEST`·`WITHDRAW`·`PROFIT`, source-independent reversal을 검증한다.
|
||||
- [x] API 및 interface 문서 갱신
|
||||
- `total_invested`, `total_withdrawn`, `net_contributed_capital`, `cumulative_profit_withdrawn`의 all-source 의미와 `PROFIT` 분리 경계를 명시한다.
|
||||
- [x] 전체 회귀와 변경 범위 정적 검증
|
||||
- Focused tests, full repository tests, 변경 범위 lint와 diff 검사를 실행한다.
|
||||
- [x] 독립 QC 및 완료 경계 기록
|
||||
- Blocker/Major/Minor를 독립 검토하고 local 구현·검증과 push·release·deployment를 구분해 기록한다.
|
||||
|
||||
### 진행 기록
|
||||
|
||||
- 2026-08-19 17:23:19 KST — Issue #28의 승인된 OQ-1-A~OQ-5-A와 구현 경계를 확인했다.
|
||||
- 2026-08-19 17:23:19 KST — 별도 worktree 없이 clean local `main`의 기준 SHA를 고정하고 구현·검증을 시작했다.
|
||||
- 2026-08-19 17:25:55 KST — Original과 reversal의 `source='manual'` predicate를 제거하고 side별 gross INVEST, WITHDRAW, PROFIT 분리 계산을 구현했다.
|
||||
- 2026-08-19 17:25:55 KST — API schema와 data-model/runtime-flow 문서를 all-source semantics, PROFIT 분리, source-independent reversal 경계로 갱신했다.
|
||||
- 2026-08-19 17:25:55 KST — Focused API/repository tests `38 passed`; disposable PostgreSQL 16에서 broker-only, mixed source, 양방향 cross-source reversal, `buffer_out` 제외 semantic test `1 passed`.
|
||||
- 2026-08-19 17:25:55 KST — 전체 파일 Ruff에는 candidate 이전부터 존재한 오류가 있어 변경 범위와 구분해 기록했고, 신규 파일 lint 및 diff 검사는 PASS했다.
|
||||
- 2026-08-19 17:28:28 KST — 원본 checkout full suite는 기존 `temp/deleted_20260813/.worktrees/` 복제본을 grep하는 auth surface tests 3건만 실패하고 `657 passed, 16 skipped`였다. 해당 pre-existing artifact를 제외한 clean candidate copy에서 full suite `660 passed, 16 skipped`로 PASS했다.
|
||||
- 2026-08-19 17:28:28 KST — 구현 범위의 독립 read-only QC를 시작했다. QC 결과 반영 전에는 최종 완료로 표시하지 않는다.
|
||||
- 2026-08-19 17:32:35 KST — 최초 독립 QC는 Blocker 0 / Major 1 / Minor 0으로, `docs/interface-contract.md`의 manual-only 외부 계약이 구현과 충돌한다고 판정했다.
|
||||
- 2026-08-19 17:32:35 KST — 외부 interface contract의 list/summary 의미, source-independent reversal, non-capital side 제외와 change log를 갱신하고 문서 계약 회귀 테스트를 추가했다.
|
||||
- 2026-08-19 17:32:35 KST — 보완 후 focused `39 passed`, test-file Ruff·diff check PASS, clean candidate full suite `661 passed, 16 skipped`로 재검증하고 독립 재QC를 시작했다.
|
||||
- 2026-08-19 17:34:13 KST — 독립 재QC가 PASS(Blocker 0 / Major 0 / Minor 0)했다.
|
||||
- 2026-08-19 17:34:13 KST — 로컬 `main`에 `5eb8c042668b1c383ac59ca9be00aa1a5dfcb37a` (`fix: aggregate investment totals across ledger sources`)로 커밋했다. Push, PR, release, deployment와 production read-only pre/post check는 수행하지 않았다.
|
||||
|
||||
> 각 작업이 실제로 완료되고 검증될 때마다 이 댓글의 체크박스와 진행 기록을 갱신한다. 체크 표시는 구현 완료 또는 해당 검증의 실제 통과를 의미하며, 배포·릴리스 완료를 의미하지 않는다.
|
||||
@@ -0,0 +1,147 @@
|
||||
# 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 권한을 자동으로 부여하지 않는다.
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
id: "line-0005"
|
||||
---
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
id: "req-0005"
|
||||
status: approved
|
||||
discovery: "dcy-0005"
|
||||
criteria:
|
||||
create: []
|
||||
update:
|
||||
- "ac-0001"
|
||||
- "ac-0002"
|
||||
- "ac-0003"
|
||||
- "ac-0005"
|
||||
retire: []
|
||||
satisfy:
|
||||
- "ac-0004"
|
||||
---
|
||||
|
||||
# Portfolio ledger source-independent return aggregates
|
||||
|
||||
## Objective
|
||||
|
||||
Portfolio return aggregate가 producer source와 무관하게 유효한 accounting event를 반영하도록 교정하면서 gross 투자원금, 회수원금, 순투자원금과 수익 인출을 분리하고, source-independent reversal·목록/단건 일치·기존 권한 경계를 보존한다.
|
||||
|
||||
## Scope
|
||||
|
||||
- 모든 source의 유효한 `INVEST`, `WITHDRAW`, `PROFIT` row를 side별로 집계한다.
|
||||
- `total_invested = Σ valid INVEST`, `total_withdrawn = Σ valid WITHDRAW`, `net_contributed_capital = total_invested - total_withdrawn`, `cumulative_profit_withdrawn = Σ valid PROFIT` 의미를 사용한다.
|
||||
- `PROFIT`을 원금에서 제외하고 별도 수익 인출 합계로 유지한다.
|
||||
- 동일 portfolio의 matching `REVERSAL.target_ledger_entry_id`가 존재하면 original과 reversal의 source와 무관하게 original `INVEST`·`WITHDRAW`·`PROFIT`을 제외한다.
|
||||
- `buffer_out` 등 다른 side를 원금·수익 aggregate에 포함하거나 `INVEST`로 재해석하지 않는다.
|
||||
- 목록은 반환 대상 ID만 하나의 grouped query로 집계하고 단건 summary는 같은 repository aggregate에 위임한다.
|
||||
- Existing API field 이름과 owner/admin/service authorization 및 filter 경계를 유지한다.
|
||||
- API schema, data model, runtime flow와 external interface contract를 source-independent 의미로 동기화한다.
|
||||
|
||||
## Constraints
|
||||
|
||||
- Existing ledger row mutation, migration 또는 backfill 없이 SELECT query semantic만 교정한다.
|
||||
- 별도 aggregate projection/table 또는 schema migration을 추가하지 않는다.
|
||||
- Credential, token, password 또는 connection string을 문서와 evidence에 기록하지 않는다.
|
||||
- Production 확인은 승인된 read-only query/API pre/post check로 제한하며 별도 배포 절차가 소유한다.
|
||||
|
||||
### Current Implementation Assessment
|
||||
|
||||
- 승인 범위는 local `main` commit `5eb8c042668b1c383ac59ca9be00aa1a5dfcb37a`에 구현됐다.
|
||||
- Focused repository/API/docs contract tests `39 passed`, disposable PostgreSQL 16 semantic integration `1 passed`, clean candidate full suite `661 passed, 16 skipped`, 변경 test Ruff와 `git diff --check`가 통과했다.
|
||||
- 최초 독립 QC의 stale manual-only interface contract Major를 보완한 후 독립 재QC는 PASS(Blocker 0 / Major 0 / Minor 0)했다.
|
||||
- exact GitHub 진행·완료 comment는 `line-0005/evidence/implementation-progress.md`와 `implementation-completion.md`에 보존한다.
|
||||
- 현재 평가는 local 구현·검증·commit 완료이며 push, PR, 원격 main 반영, release, production deployment/restart와 read-only production pre/post check 완료를 주장하지 않는다.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- `buffer_out` 재분류 또는 zero-amount broker event producer 교정
|
||||
- Historical ledger mutation/backfill
|
||||
- API field rename 또는 breaking response change
|
||||
- 별도 projection/table, migration 또는 성능 redesign
|
||||
- Push, PR, 원격 main 반영, release, production deployment/restart
|
||||
- Production ledger mutation 또는 배포 lifecycle closure
|
||||
Reference in New Issue
Block a user