docs(requirements): approve manual cash dividend contract
This commit is contained in:
@@ -0,0 +1,48 @@
|
|||||||
|
# Requirements
|
||||||
|
|
||||||
|
각 requirement 문서는 Markdown body 앞에 JSON front matter를 둔다.
|
||||||
|
|
||||||
|
## File naming
|
||||||
|
|
||||||
|
```text
|
||||||
|
req-NNN_<slug>.md
|
||||||
|
```
|
||||||
|
|
||||||
|
- `NNN`: requirement ID의 세 자리 decimal number
|
||||||
|
- `<slug>`: requirement title을 나타내는 lowercase kebab-case ASCII slug
|
||||||
|
|
||||||
|
## Metadata schema
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"schema": "proofline.requirement/v1",
|
||||||
|
"id": "REQ-NNN",
|
||||||
|
"title": "사람이 읽는 제목",
|
||||||
|
"revision": "major.minor",
|
||||||
|
"status": "draft | approved | retired",
|
||||||
|
"depends_on": ["REQ-NNN"],
|
||||||
|
"applies_to": ["algorithm-name"],
|
||||||
|
"language": "ko"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `id`: repository 안에서 고유한 requirement identifier
|
||||||
|
- `revision`: 사람이 읽는 requirement revision. `draft` 상태에서 본문을 수정해도 올리지 않으며, `approved` 상태로 전환할 때 또는 승인된 requirement의 의미를 변경하기 위해 재승인할 때만 올린다.
|
||||||
|
- `status`: requirement lifecycle 상태. `approved`는 명시적인 사용자 승인을 의미한다.
|
||||||
|
- `depends_on`: 선행 requirement identifier 목록
|
||||||
|
- `applies_to`: requirement가 적용되는 algorithm 또는 product 영역
|
||||||
|
|
||||||
|
## Python parsing
|
||||||
|
|
||||||
|
front matter body는 JSON이므로 별도 dependency 없이 Python 표준 라이브러리로 읽을 수 있다.
|
||||||
|
|
||||||
|
```python
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
text = Path("docs/requirements/req-001_algorithm-input-contract.md").read_text(encoding="utf-8")
|
||||||
|
_, metadata_json, _ = text.split("---", 2)
|
||||||
|
metadata = json.loads(metadata_json)
|
||||||
|
```
|
||||||
|
|
||||||
|
JSON front matter의 `revision`과 `status`가 문서의 단일 source of truth다.
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
---
|
||||||
|
{
|
||||||
|
"schema": "proofline.requirement/v1",
|
||||||
|
"id": "REQ-NNN",
|
||||||
|
"title": "요구사항 제목",
|
||||||
|
"revision": "0.1",
|
||||||
|
"status": "draft",
|
||||||
|
"depends_on": [],
|
||||||
|
"applies_to": ["product-area"],
|
||||||
|
"language": "ko"
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
# REQ-NNN — 요구사항 제목
|
||||||
|
|
||||||
|
## 1. 목적
|
||||||
|
|
||||||
|
이 요구사항으로 달성해야 하는 사용자 또는 시스템의 결과를 기술한다.
|
||||||
|
|
||||||
|
## 2. 배경
|
||||||
|
|
||||||
|
현재 문제, 요구사항이 필요한 이유, 관련된 운영·도메인 맥락을 기술한다. 구현 방법보다 해결해야 할 문제에 집중한다.
|
||||||
|
|
||||||
|
## 3. 범위
|
||||||
|
|
||||||
|
### 포함
|
||||||
|
|
||||||
|
- 이 요구사항이 제공하거나 변경해야 하는 동작
|
||||||
|
- 영향을 받는 API, 데이터, 제품 영역 또는 운영 흐름
|
||||||
|
|
||||||
|
### 제외
|
||||||
|
|
||||||
|
- 이번 요구사항에서 다루지 않는 기능
|
||||||
|
- 별도 requirement 또는 후속 작업으로 분리할 항목
|
||||||
|
|
||||||
|
## 4. 용어와 기준
|
||||||
|
|
||||||
|
- **용어**: 이 문서에서 사용하는 의미
|
||||||
|
- **정본 기준**: 값이나 상태의 canonical source
|
||||||
|
|
||||||
|
## 5. 요구사항
|
||||||
|
|
||||||
|
`REQ-NNN`은 문서와 기능 묶음의 identifier다. 고객에게 전달할 요구사항명세리스트와 구현 추적에서는 독립적으로 구현·검증할 수 있는 각 요구사항을 `REQ-NNN.01`, `REQ-NNN.02` 형식으로 식별한다.
|
||||||
|
|
||||||
|
각 요구사항에는 하네스가 변경 영향과 필요한 회귀 범위를 판정할 수 있도록 `Impact`를 기록한다.
|
||||||
|
|
||||||
|
### REQ-NNN.01 — 요구사항 이름
|
||||||
|
|
||||||
|
시스템은 관찰 가능한 결과를 명확하게 기술한 조건을 충족해야 한다.
|
||||||
|
|
||||||
|
**Impact**
|
||||||
|
|
||||||
|
- Interfaces: `<영향받는 API, event 또는 없음>`
|
||||||
|
- Persistence: `<영향받는 table, state 또는 없음>`
|
||||||
|
- Consumers: `<영향받는 downstream consumer 또는 없음>`
|
||||||
|
- Migration: `<필요 여부와 범위>`
|
||||||
|
- Compatibility: `<유지해야 하는 기존 동작>`
|
||||||
|
- Deployment prerequisites: `<배포 전제조건 또는 없음>`
|
||||||
|
|
||||||
|
### REQ-NNN.02 — 요구사항 이름
|
||||||
|
|
||||||
|
시스템은 정상 동작뿐 아니라 실패·경계·변경 없음 조건도 명확히 충족해야 한다.
|
||||||
|
|
||||||
|
**Impact**
|
||||||
|
|
||||||
|
- Interfaces: `<영향받는 API, event 또는 없음>`
|
||||||
|
- Persistence: `<영향받는 table, state 또는 없음>`
|
||||||
|
- Consumers: `<영향받는 downstream consumer 또는 없음>`
|
||||||
|
- Migration: `<필요 여부와 범위>`
|
||||||
|
- Compatibility: `<유지해야 하는 기존 동작>`
|
||||||
|
- Deployment prerequisites: `<배포 전제조건 또는 없음>`
|
||||||
|
|
||||||
|
## 6. 제약과 불변조건
|
||||||
|
|
||||||
|
- 구현 방식과 무관하게 항상 유지해야 하는 데이터·상태 불변조건
|
||||||
|
- 기존 호출자와 저장 데이터에 대해 유지해야 하는 하위호환 조건
|
||||||
|
- 인증·권한·민감정보 처리 조건
|
||||||
|
- 원자성, 정밀도, 동시성 또는 순서 보장 조건
|
||||||
|
|
||||||
|
## 7. Acceptance Criteria
|
||||||
|
|
||||||
|
Acceptance Criterion은 소속 요구사항을 포함한 `REQ-NNN.01-AC01` 형식으로 식별한다. 문서 전체에서만 고유한 `AC-001` 형식은 사용하지 않는다.
|
||||||
|
|
||||||
|
### REQ-NNN.01-AC01 — 판정 가능한 결과
|
||||||
|
|
||||||
|
**Criterion**
|
||||||
|
|
||||||
|
- 하나의 지속적인 요구사항을 PASS 또는 FAIL로 판정할 수 있게 기술한다.
|
||||||
|
- 특정 함수명이나 내부 구현보다 외부에서 관찰 가능한 결과를 명시한다.
|
||||||
|
|
||||||
|
**Verification**
|
||||||
|
|
||||||
|
- 정상 경로 검증 방법
|
||||||
|
- 실패 및 경계 조건 검증 방법
|
||||||
|
- 변경되면 안 되는 상태의 검증 방법
|
||||||
|
- 필요한 경우 실제 persistence backend 또는 production entry point 검증 방법
|
||||||
|
|
||||||
|
**Verification Classification**
|
||||||
|
|
||||||
|
- Level: `unit | API | integration | end-to-end`
|
||||||
|
- Backend: `<검증 대상 backend 또는 없음>`
|
||||||
|
- Cases: `positive | negative | boundary | unchanged-state | concurrency`
|
||||||
|
|
||||||
|
**State Invariants**
|
||||||
|
|
||||||
|
- 성공 시 반드시 변경되어야 하는 상태
|
||||||
|
- 성공하더라도 변경되면 안 되는 상태
|
||||||
|
- 실패 시 부분 기록 없이 유지되어야 하는 상태
|
||||||
|
|
||||||
|
### REQ-NNN.02-AC01 — 판정 가능한 결과
|
||||||
|
|
||||||
|
**Criterion**
|
||||||
|
|
||||||
|
- 두 번째 독립 요구사항을 기술한다.
|
||||||
|
|
||||||
|
**Verification**
|
||||||
|
|
||||||
|
- Criterion 전체를 입증할 검증 절차와 기대 결과를 기술한다.
|
||||||
|
|
||||||
|
**Verification Classification**
|
||||||
|
|
||||||
|
- Level: `unit | API | integration | end-to-end`
|
||||||
|
- Backend: `<검증 대상 backend 또는 없음>`
|
||||||
|
- Cases: `positive | negative | boundary | unchanged-state | concurrency`
|
||||||
|
|
||||||
|
**State Invariants**
|
||||||
|
|
||||||
|
- 성공 시 반드시 변경되어야 하는 상태
|
||||||
|
- 성공하더라도 변경되면 안 되는 상태
|
||||||
|
- 실패 시 부분 기록 없이 유지되어야 하는 상태
|
||||||
|
|
||||||
|
## 8. 오류 및 충돌 계약
|
||||||
|
|
||||||
|
- 입력 오류: 상태 코드 또는 오류 분류와 무변경 조건
|
||||||
|
- 상태 충돌: 재시도·확인·중복 처리 정책
|
||||||
|
- 저장 실패: transaction rollback과 부분 기록 방지 조건
|
||||||
|
|
||||||
|
## 9. 데이터 및 인터페이스 계약
|
||||||
|
|
||||||
|
- 입력 field와 필수·선택 여부
|
||||||
|
- 출력 field와 의미
|
||||||
|
- 저장해야 하는 canonical 값과 metadata
|
||||||
|
- 시간대, 단위, 정밀도 및 식별자 규칙
|
||||||
|
|
||||||
|
## 10. 하위호환성
|
||||||
|
|
||||||
|
- 유지해야 하는 기존 동작과 호출 방식
|
||||||
|
- 기존 데이터에 적용할 규칙
|
||||||
|
- 의도적으로 변경되는 동작이 있다면 그 경계
|
||||||
|
|
||||||
|
## 11. 검증 계획
|
||||||
|
|
||||||
|
- Unit/API contract 검증
|
||||||
|
- Persistence migration 및 round-trip 검증
|
||||||
|
- 동시성·중복·재시도 검증
|
||||||
|
- 기존 기능 회귀 검증
|
||||||
|
- OpenAPI 또는 사용자-facing 문서 정합성 검증
|
||||||
|
|
||||||
|
### Test Traceability Contract
|
||||||
|
|
||||||
|
- Requirement 문서에는 검증 의도와 판정 조건만 기록한다.
|
||||||
|
- 실행 가능한 테스트 정의는 별도 test catalog artifact에서 관리한다.
|
||||||
|
- 각 테스트 케이스는 안정적인 identifier를 사용하고 `verifies`로 Acceptance Criterion을 참조한다.
|
||||||
|
- 하나의 테스트가 여러 Criterion을 검증할 수 있으며, 각 연결을 빠짐없이 기록한다.
|
||||||
|
- 실행 결과는 requirement 또는 test definition에 덮어쓰지 않고 별도 evidence artifact에 기록한다.
|
||||||
|
- Evidence는 requirement revision, 검증한 commit, 환경과 실행 시각에 결합한다.
|
||||||
|
- 테스트 정의의 존재, 자동화 여부, 특정 commit에서의 실행 여부와 PASS 결과를 서로 다른 상태로 취급한다.
|
||||||
|
|
||||||
|
Test definition 예시:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Test ID: TC-NNN-001
|
||||||
|
Verifies:
|
||||||
|
- REQ-NNN.01-AC01
|
||||||
|
Test reference: <test file, case 또는 harness selector>
|
||||||
|
Automated: true | false
|
||||||
|
```
|
||||||
|
|
||||||
|
Execution evidence 예시:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Test ID: TC-NNN-001
|
||||||
|
Requirement revision: REQ-NNN@major.minor
|
||||||
|
Commit: <commit SHA>
|
||||||
|
Environment: <runtime 및 backend>
|
||||||
|
Result: PASS | FAIL | BLOCKED
|
||||||
|
Executed at: <timestamp>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 12. 추적성
|
||||||
|
|
||||||
|
- 근거 이슈: `<issue URL 또는 ID>`
|
||||||
|
- 구현 PR: `<PR URL 또는 ID>`
|
||||||
|
- 관련 설계 문서: `<repository-relative path>`
|
||||||
|
- 관련 requirement: `<REQ-NNN 또는 없음>`
|
||||||
|
|
||||||
|
## 13. 확정된 결정
|
||||||
|
|
||||||
|
- 사용자와 확정한 제품·정책 결정을 기록한다.
|
||||||
|
- 확정된 결정은 Open Question으로 다시 남기지 않는다.
|
||||||
|
|
||||||
|
## 14. Open Questions
|
||||||
|
|
||||||
|
`approved` 전 반드시 결정해야 하며, 각 항목에는 선택지, 권장안, 결정 주체와 종료 조건을 둔다. 미결정 항목이 없으면 `없음`으로 기록한다.
|
||||||
|
|
||||||
|
- 없음
|
||||||
|
|
||||||
|
## 15. 승인 기록
|
||||||
|
|
||||||
|
- 상태: `draft`
|
||||||
|
- 승인 주체: 미승인
|
||||||
|
- 승인 근거: 없음
|
||||||
@@ -0,0 +1,591 @@
|
|||||||
|
---
|
||||||
|
{
|
||||||
|
"schema": "proofline.requirement/v1",
|
||||||
|
"id": "REQ-001",
|
||||||
|
"title": "사용자 입력 현금 배당 원장 기록",
|
||||||
|
"revision": "1.0",
|
||||||
|
"status": "approved",
|
||||||
|
"depends_on": [],
|
||||||
|
"applies_to": ["portfolio-hub", "manual-ledger"],
|
||||||
|
"language": "ko"
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
# REQ-001 — 사용자 입력 현금 배당 원장 기록
|
||||||
|
|
||||||
|
## 1. 목적
|
||||||
|
|
||||||
|
사용자는 Broker에서 자동으로 수집하기 어려운 현금 배당의 실제 실수령액을 portfolio-hub의 manual ledger에 직접 기록할 수 있어야 한다. 기록된 배당은 전략 주문 가능 현금과 분리하여 `profit_buffer`에 반영하고, 입력 사실과 변경 후 portfolio 상태를 원장에서 조회하고 정정할 수 있어야 한다.
|
||||||
|
|
||||||
|
## 2. 배경
|
||||||
|
|
||||||
|
초기 범위에서는 Broker별 배당 조회 기능의 가용성과 형식 차이에 의존하지 않고 사용자가 실제 입금 내역을 직접 기록한다. 배당 실수령액은 투자 원금이나 주문 가능 현금이 아니므로 `cash_balance`에 합산하지 않는다. 잘못 입력한 기록은 기존 append-only `REVERSAL` 흐름으로 상쇄하며, 원본 원장을 수정하거나 삭제하지 않는다.
|
||||||
|
|
||||||
|
이 문서는 GitHub 이슈 #31의 확정 계약과 PR #33의 구현 증거를 요구사항으로 정리한 초안이다. 구현 코드와 테스트는 의도의 근거이지만, 이 문서의 `approved` 전환은 별도의 명시적인 사용자 승인을 필요로 한다.
|
||||||
|
|
||||||
|
## 3. 범위
|
||||||
|
|
||||||
|
### 포함
|
||||||
|
|
||||||
|
- manual ledger action `DIVIDEND`
|
||||||
|
- 배당 실수령액, 선택적 세금 metadata, 입금일과 메모 입력
|
||||||
|
- 입력값 검증과 OpenAPI 계약
|
||||||
|
- PostgreSQL 원장 저장과 portfolio snapshot 갱신
|
||||||
|
- 중복 후보 경고와 사용자 확인 후 재제출
|
||||||
|
- manual ledger 및 unified ledger 조회
|
||||||
|
- 기존 `REVERSAL`을 이용한 배당 정정
|
||||||
|
- Decimal 정밀도와 transaction 원자성
|
||||||
|
- 기존 manual ledger action 하위호환
|
||||||
|
- API·저장소·실제 PostgreSQL 검증
|
||||||
|
|
||||||
|
### 제외
|
||||||
|
|
||||||
|
- Broker 배당 자동 수집 또는 동기화
|
||||||
|
- 외화 환산과 다통화 손익
|
||||||
|
- 배당 총액(gross amount) 입력
|
||||||
|
- 세금 자동 계산·역산과 세율 정책
|
||||||
|
- 배당수익 별도 집계
|
||||||
|
- 실현손익 또는 기간 수익률 breakdown 변경
|
||||||
|
- 배당금을 `cash_balance`나 전략 주문 가능 현금으로 이동하는 기능
|
||||||
|
- portfolio-hub CLI의 배당 입력·표시 UX
|
||||||
|
- trex-web의 `현금 배당` label, 입력 form과 중복 확인 UX
|
||||||
|
- reversal UI
|
||||||
|
|
||||||
|
## 4. 용어와 기준
|
||||||
|
|
||||||
|
- **실수령액(net cash dividend)**: 세금 등을 제외하고 사용자 계좌에 실제 입금된 배당금. API에서는 `amount`로 표현한다.
|
||||||
|
- **세금 metadata**: 사용자가 참고 목적으로 입력하는 `tax`. 자동 계산하지 않으며 portfolio 상태에 영향을 주지 않는다.
|
||||||
|
- **입금일**: 사용자가 date-only 형식으로 입력하는 배당 입금 날짜. 기존 `executed_at`에 UTC 자정으로 저장한다.
|
||||||
|
- **기록 시각**: 서버가 원장 항목을 append한 시각인 `recorded_at`.
|
||||||
|
- **중복 후보**: 동일 portfolio, 동일 UTC 입금일, 동일 Decimal `amount`, 동일 `note`를 가진 reversal되지 않은 기존 `DIVIDEND`.
|
||||||
|
- **정본 기준**: 배당 사실과 변경 후 상태는 PostgreSQL `portfolio_ledger`의 append-only 기록 및 그 기록과 같은 transaction에서 갱신된 portfolio snapshot을 기준으로 한다.
|
||||||
|
|
||||||
|
## 5. 요구사항
|
||||||
|
|
||||||
|
`REQ-001`은 사용자 입력 현금 배당 기능 묶음의 identifier다. 고객 요구사항명세리스트와 구현 추적에서는 독립적으로 구현·검증할 수 있는 다음 항목을 사용한다.
|
||||||
|
|
||||||
|
### REQ-001.01 — 현금 배당 입력 계약
|
||||||
|
|
||||||
|
시스템은 portfolio 소유 사용자가 manual ledger API를 통해 실제 배당 실수령액, 입금일, 선택적 세금 metadata와 메모를 입력할 수 있게 해야 한다.
|
||||||
|
|
||||||
|
**Impact**
|
||||||
|
|
||||||
|
- Interfaces: `POST /v1/portfolios/{portfolio_id}/ledger/manual`, OpenAPI
|
||||||
|
- Persistence: `portfolio_ledger`
|
||||||
|
- Consumers: portfolio-hub API client, 후속 trex-web 연계
|
||||||
|
- Migration: 신규 입금일 column 없이 기존 원장 metadata 구조 사용
|
||||||
|
- Compatibility: 기존 manual action의 유효 입력 계약 유지
|
||||||
|
- Deployment prerequisites: 기존 portfolio-hub PostgreSQL migration head 적용
|
||||||
|
|
||||||
|
### REQ-001.02 — 배당에 따른 portfolio 상태 변경
|
||||||
|
|
||||||
|
시스템은 유효한 `DIVIDEND`의 `amount`만큼 `profit_buffer`를 증가시키고, `cash_balance`와 `asset_quantity`는 변경하지 않아야 한다. `total_value_after`는 증가한 buffer를 반영해야 하며 `tax`는 모든 상태 계산에서 제외해야 한다.
|
||||||
|
|
||||||
|
**Impact**
|
||||||
|
|
||||||
|
- Interfaces: manual ledger 생성 응답, ledger 조회 응답
|
||||||
|
- Persistence: portfolio snapshot, `portfolio_ledger` after-snapshot
|
||||||
|
- Consumers: portfolio valuation과 unified ledger 소비자
|
||||||
|
- Migration: 없음
|
||||||
|
- Compatibility: 기존 action의 상태 계산식 유지
|
||||||
|
- Deployment prerequisites: 없음
|
||||||
|
|
||||||
|
### REQ-001.03 — 입금일과 기록 시각 분리
|
||||||
|
|
||||||
|
시스템은 date-only 입금일을 기존 `executed_at`에 UTC 자정(`00:00:00Z`)으로 저장하고, `recorded_at`은 서버 append 시각으로 독립적으로 유지해야 한다.
|
||||||
|
|
||||||
|
**Impact**
|
||||||
|
|
||||||
|
- Interfaces: manual ledger 요청·응답, unified ledger 응답
|
||||||
|
- Persistence: 기존 `portfolio_ledger.executed_at`, `recorded_at`
|
||||||
|
- Consumers: 입금일 표시와 중복 판정 소비자
|
||||||
|
- Migration: 입금일 전용 DB column 추가 없음
|
||||||
|
- Compatibility: 기존 원장의 `recorded_at` 의미 유지
|
||||||
|
- Deployment prerequisites: 없음
|
||||||
|
|
||||||
|
### REQ-001.04 — 배당 원장 저장과 조회
|
||||||
|
|
||||||
|
시스템은 배당을 `source=manual`, canonical `action=DIVIDEND`로 append하고, unified ledger에서 배당 금액과 metadata 및 변경 후 snapshot을 직접 조회할 수 있게 해야 한다.
|
||||||
|
|
||||||
|
**Impact**
|
||||||
|
|
||||||
|
- Interfaces: manual ledger 응답, `GET /v1/portfolios/{portfolio_id}/ledger`, OpenAPI
|
||||||
|
- Persistence: `portfolio_ledger`
|
||||||
|
- Consumers: portfolio-hub API client, trex-web 원장 화면
|
||||||
|
- Migration: 기존 manual ledger metadata column 사용
|
||||||
|
- Compatibility: broker 원장과 기존 manual action의 unified ledger 표현 유지
|
||||||
|
- Deployment prerequisites: 없음
|
||||||
|
|
||||||
|
### REQ-001.05 — 중복 후보 경고와 확인 재제출
|
||||||
|
|
||||||
|
시스템은 중복 후보를 자동 기록하지 않고 HTTP 409 경고와 일치한 원장 ID를 반환해야 한다. 사용자가 `confirm_duplicate=true`로 확인한 재요청은 실제 예외 기록으로 허용해야 한다.
|
||||||
|
|
||||||
|
**Impact**
|
||||||
|
|
||||||
|
- Interfaces: manual ledger 요청의 `confirm_duplicate`, HTTP 409 오류 응답
|
||||||
|
- Persistence: 중복 후보 조회와 조건부 append
|
||||||
|
- Consumers: 중복 확인 UX를 제공하는 후속 trex-web
|
||||||
|
- Migration: 없음
|
||||||
|
- Compatibility: 기존 호출자가 새 배당 전용 field를 생략한 경우의 기존 계약 유지
|
||||||
|
- Deployment prerequisites: 없음
|
||||||
|
|
||||||
|
### REQ-001.06 — DIVIDEND reversal
|
||||||
|
|
||||||
|
시스템은 기존 append-only `REVERSAL`로 원본 배당의 실수령액만큼 `profit_buffer`를 감소시켜 배당 효과를 상쇄해야 한다. 원본 원장을 수정하거나 삭제해서는 안 된다.
|
||||||
|
|
||||||
|
**Impact**
|
||||||
|
|
||||||
|
- Interfaces: 기존 manual `REVERSAL` 요청·응답
|
||||||
|
- Persistence: 원본 `DIVIDEND`, 새 `REVERSAL`, portfolio snapshot
|
||||||
|
- Consumers: manual ledger와 unified ledger 소비자
|
||||||
|
- Migration: 없음
|
||||||
|
- Compatibility: 기존 잔액 부족·대상 없음·중복 reversal 정책 유지
|
||||||
|
- Deployment prerequisites: 없음
|
||||||
|
|
||||||
|
### REQ-001.07 — Decimal 정밀도와 원자적 정합성
|
||||||
|
|
||||||
|
시스템은 수용한 Decimal 금액을 입력, 중복 비교, 원장 저장, snapshot 계산과 reversal에서 같은 의미로 유지해야 한다. 원장과 portfolio snapshot이 같은 값을 표현할 수 없는 요청은 어떤 상태도 기록하기 전에 원자적으로 거부해야 한다.
|
||||||
|
|
||||||
|
**Impact**
|
||||||
|
|
||||||
|
- Interfaces: Decimal 입력 검증과 충돌 응답
|
||||||
|
- Persistence: Decimal 원장 값과 portfolio snapshot
|
||||||
|
- Consumers: 잔액·원장·reversal 정합성에 의존하는 모든 소비자
|
||||||
|
- Migration: column type 변경 없음
|
||||||
|
- Compatibility: 수용된 값의 정합성을 보장하면서 기존 manual action 계약 유지
|
||||||
|
- Deployment prerequisites: 실제 PostgreSQL 경계값 검증
|
||||||
|
|
||||||
|
### REQ-001.08 — 기존 manual ledger 하위호환
|
||||||
|
|
||||||
|
시스템은 `DIVIDEND` 도입으로 기존 manual ledger action의 입력, 상태 변경, 충돌과 reversal 계약을 변경해서는 안 된다.
|
||||||
|
|
||||||
|
**Impact**
|
||||||
|
|
||||||
|
- Interfaces: 기존 manual ledger API 전체
|
||||||
|
- Persistence: 기존 action의 원장 및 snapshot 갱신
|
||||||
|
- Consumers: 기존 portfolio-hub API client
|
||||||
|
- Migration: 없음
|
||||||
|
- Compatibility: `INVEST`, `BUFFER_OUT`, `BUFFER_IN`, `BUY`, `SELL`, `WITHDRAW`, `PROFIT`, `REVERSAL` 유지
|
||||||
|
- Deployment prerequisites: 기존 action 회귀 검증
|
||||||
|
|
||||||
|
### REQ-001.09 — 권한과 민감정보 비노출
|
||||||
|
|
||||||
|
시스템은 기존 portfolio 소유권과 manual-write 권한 정책을 유지하고, 배당 기능으로 인해 API·로그·오류 응답에 secret 또는 account 식별정보를 추가로 노출해서는 안 된다.
|
||||||
|
|
||||||
|
**Impact**
|
||||||
|
|
||||||
|
- Interfaces: 인증된 manual ledger API와 오류 응답
|
||||||
|
- Persistence: actor와 기존 감사 metadata
|
||||||
|
- Consumers: 사용자 및 운영 로그 소비자
|
||||||
|
- Migration: 없음
|
||||||
|
- Compatibility: 기존 인증·권한·daemon-managed portfolio 확인 정책 유지
|
||||||
|
- Deployment prerequisites: 없음
|
||||||
|
|
||||||
|
## 6. 제약과 불변조건
|
||||||
|
|
||||||
|
- `amount`는 0보다 큰 Decimal이어야 한다.
|
||||||
|
- `tax`는 선택 입력이며 기본값은 `0`이고 음수일 수 없다.
|
||||||
|
- `amount`, `tax`와 관련 수치는 원장의 `NUMERIC(20,8)` 범위와 scale을 준수해야 한다.
|
||||||
|
- `tax`는 metadata이며 `profit_buffer`, `cash_balance`, `asset_quantity`, `total_value_after` 계산에 영향을 주지 않는다.
|
||||||
|
- `quantity`, 거래 `price`, `fee`, 주문·체결 ID와 reversal 대상 ID는 `DIVIDEND` 입력 대상이 아니다.
|
||||||
|
- date-only 입금일은 서버 timezone과 무관하게 UTC 자정으로 변환한다.
|
||||||
|
- 원장 append와 portfolio snapshot 갱신은 원자적으로 함께 성공하거나 실패해야 한다.
|
||||||
|
- 실패하거나 거부된 요청은 원장과 portfolio snapshot을 모두 변경하지 않는다.
|
||||||
|
- 원장은 append-only이며 원본 배당을 직접 수정하거나 삭제하지 않는다.
|
||||||
|
- 중복 비교는 binary float 근사가 아니라 저장 계약과 동일한 Decimal 의미를 사용한다.
|
||||||
|
- `profit_buffer`가 부족한 `DIVIDEND` reversal은 기존 잔액 부족 정책에 따라 거부한다.
|
||||||
|
|
||||||
|
|
||||||
|
## 7. Acceptance Criteria
|
||||||
|
|
||||||
|
### REQ-001.01-AC01 — 유효한 배당 입력 수용
|
||||||
|
|
||||||
|
**Criterion**
|
||||||
|
|
||||||
|
portfolio 소유 사용자가 `action=DIVIDEND`, 유효한 `amount`, date-only `executed_at`, 선택적 `tax`와 `note`를 제출하면 시스템은 manual ledger 항목을 생성하고 HTTP 201을 반환한다. `tax` 생략 시 저장 의미는 `0`이다.
|
||||||
|
|
||||||
|
**Verification**
|
||||||
|
|
||||||
|
- `amount>0`, `tax>=0`, `executed_at=YYYY-MM-DD`인 정상 요청이 HTTP 201인지 확인한다.
|
||||||
|
- `tax`를 생략한 요청의 저장·응답 값이 `0`인지 확인한다.
|
||||||
|
- 생성 응답의 `action`, `source`, `amount`, `tax`, `executed_at`과 after-snapshot을 확인한다.
|
||||||
|
|
||||||
|
**Verification Classification**
|
||||||
|
|
||||||
|
- Level: `API | integration`
|
||||||
|
- Backend: PostgreSQL
|
||||||
|
- Cases: `positive | boundary`
|
||||||
|
|
||||||
|
**State Invariants**
|
||||||
|
|
||||||
|
- 성공 시 원장 항목이 정확히 하나 추가되어야 한다.
|
||||||
|
- 성공 시 `tax`는 portfolio 상태를 변경하지 않아야 한다.
|
||||||
|
- 실패한 입력은 원장과 snapshot을 변경하지 않아야 한다.
|
||||||
|
|
||||||
|
### REQ-001.01-AC02 — 잘못된 배당 입력 거부
|
||||||
|
|
||||||
|
**Criterion**
|
||||||
|
|
||||||
|
시스템은 `amount` 또는 입금일이 없거나, 허용 범위를 벗어난 수치 또는 `DIVIDEND`와 무관한 field가 포함된 요청을 원장 기록 전에 거부한다.
|
||||||
|
|
||||||
|
**Verification**
|
||||||
|
|
||||||
|
- 누락·0·음수·8자리 초과 소수·저장 범위 초과 `amount`를 검증한다.
|
||||||
|
- 누락되거나 잘못된 date-only `executed_at`을 검증한다.
|
||||||
|
- 음수·8자리 초과 소수·저장 범위를 벗어난 `tax`를 검증한다.
|
||||||
|
- `quantity`, `price`, `fee`, `target_ledger_entry_id` 입력을 검증한다.
|
||||||
|
- 각 거부가 HTTP 422이며 상태가 변하지 않는지 확인한다.
|
||||||
|
|
||||||
|
**Verification Classification**
|
||||||
|
|
||||||
|
- Level: `unit | API`
|
||||||
|
- Backend: 없음
|
||||||
|
- Cases: `negative | boundary | unchanged-state`
|
||||||
|
|
||||||
|
**State Invariants**
|
||||||
|
|
||||||
|
- 거부된 요청은 원장 항목을 만들지 않아야 한다.
|
||||||
|
- 거부된 요청은 모든 portfolio snapshot을 유지해야 한다.
|
||||||
|
|
||||||
|
### REQ-001.02-AC01 — 배당 상태 효과
|
||||||
|
|
||||||
|
**Criterion**
|
||||||
|
|
||||||
|
유효한 배당 기록 후 `profit_buffer_after`는 직전 값에 `amount`를 더한 값이어야 한다. `cash_balance_after`와 `asset_quantity_after`는 직전 값과 같아야 하며, `total_value_after`는 증가한 buffer를 반영해야 한다. `tax` 값은 이 계산에 영향을 주지 않는다.
|
||||||
|
|
||||||
|
**Verification**
|
||||||
|
|
||||||
|
서로 다른 `amount`와 `tax` 조합으로 기록 전후 portfolio와 ledger after-snapshot을 비교하고 각 계산식을 확인한다.
|
||||||
|
|
||||||
|
**Verification Classification**
|
||||||
|
|
||||||
|
- Level: `API | integration`
|
||||||
|
- Backend: PostgreSQL
|
||||||
|
- Cases: `positive | boundary | unchanged-state`
|
||||||
|
|
||||||
|
**State Invariants**
|
||||||
|
|
||||||
|
- `profit_buffer` 외의 현금·수량 상태는 변경되지 않아야 한다.
|
||||||
|
- 동일 `amount`에 대해 `tax` 값이 달라도 상태 효과는 같아야 한다.
|
||||||
|
- ledger after-snapshot과 portfolio snapshot은 같아야 한다.
|
||||||
|
|
||||||
|
### REQ-001.03-AC01 — UTC 입금일 저장
|
||||||
|
|
||||||
|
**Criterion**
|
||||||
|
|
||||||
|
`YYYY-MM-DD` 입금일은 `executed_at`의 해당 UTC 날짜 `00:00:00Z`로 저장·반환되어야 한다. `recorded_at`은 서버 append 시각이어야 한다.
|
||||||
|
|
||||||
|
**Verification**
|
||||||
|
|
||||||
|
- 서로 다른 날짜를 입력해 UTC 자정 저장과 조회를 확인한다.
|
||||||
|
- 서버 환경 timezone과 무관한 결과를 확인한다.
|
||||||
|
- `recorded_at`이 입력한 입금일로 대체되지 않는지 확인한다.
|
||||||
|
|
||||||
|
**Verification Classification**
|
||||||
|
|
||||||
|
- Level: `API | integration`
|
||||||
|
- Backend: PostgreSQL
|
||||||
|
- Cases: `positive | boundary`
|
||||||
|
|
||||||
|
**State Invariants**
|
||||||
|
|
||||||
|
- date-only 입력은 시간대 변환으로 전날 또는 다음 날이 되어서는 안 된다.
|
||||||
|
- `recorded_at`의 감사 의미는 유지되어야 한다.
|
||||||
|
|
||||||
|
### REQ-001.04-AC01 — Unified ledger 배당 조회
|
||||||
|
|
||||||
|
**Criterion**
|
||||||
|
|
||||||
|
기록된 배당은 manual ledger와 unified ledger에서 `source=manual`, `action=DIVIDEND`, `executed_at`, canonical `amount`, `tax_amount`, `note` 및 변경 후 snapshot으로 조회되어야 한다.
|
||||||
|
|
||||||
|
**Verification**
|
||||||
|
|
||||||
|
- 배당 생성 후 `source=manual` filter를 사용해 unified ledger를 조회한다.
|
||||||
|
- canonical `amount`가 별도 field로 존재하며 거래 `price` 해석에만 의존하지 않는지 확인한다.
|
||||||
|
- OpenAPI의 request·response schema에 같은 field와 nullability가 나타나는지 확인한다.
|
||||||
|
|
||||||
|
**Verification Classification**
|
||||||
|
|
||||||
|
- Level: `API | integration`
|
||||||
|
- Backend: PostgreSQL
|
||||||
|
- Cases: `positive | unchanged-state`
|
||||||
|
|
||||||
|
**State Invariants**
|
||||||
|
|
||||||
|
- 조회는 원장과 portfolio 상태를 변경하지 않아야 한다.
|
||||||
|
- broker entry와 기존 manual entry의 기존 표현을 변경하지 않아야 한다.
|
||||||
|
|
||||||
|
### REQ-001.05-AC01 — 최초 중복 후보 경고
|
||||||
|
|
||||||
|
**Criterion**
|
||||||
|
|
||||||
|
동일 portfolio, UTC 입금일, Decimal `amount`, `note`의 reversal되지 않은 기존 배당이 있고 `confirm_duplicate`가 없거나 `false`이면 시스템은 새 항목을 기록하지 않고 HTTP 409 `POTENTIAL_DUPLICATE_DIVIDEND`와 `matched_ledger_entry_id`를 반환한다.
|
||||||
|
|
||||||
|
**Verification**
|
||||||
|
|
||||||
|
- 첫 요청을 기록한 뒤 동일 요청을 재제출한다.
|
||||||
|
- 409 code와 원본 entry ID를 확인한다.
|
||||||
|
- 원장 수와 portfolio snapshot이 변하지 않았는지 확인한다.
|
||||||
|
- Decimal 값이 다른 요청을 같은 값으로 오판하지 않는지 확인한다.
|
||||||
|
|
||||||
|
**Verification Classification**
|
||||||
|
|
||||||
|
- Level: `API | integration`
|
||||||
|
- Backend: PostgreSQL
|
||||||
|
- Cases: `negative | boundary | unchanged-state | concurrency`
|
||||||
|
|
||||||
|
**State Invariants**
|
||||||
|
|
||||||
|
- 경고 응답은 새 원장을 만들지 않아야 한다.
|
||||||
|
- 경고 응답은 `profit_buffer`를 변경하지 않아야 한다.
|
||||||
|
- 일치 ID는 실제 중복 후보를 정확히 가리켜야 한다.
|
||||||
|
|
||||||
|
### REQ-001.05-AC02 — 확인된 중복과 reversal된 원본 처리
|
||||||
|
|
||||||
|
**Criterion**
|
||||||
|
|
||||||
|
중복 후보 요청에 `confirm_duplicate=true`가 있으면 시스템은 별도 배당 항목을 기록해야 한다. 이미 reversal된 배당 원본은 새로운 중복 후보에서 제외해야 한다.
|
||||||
|
|
||||||
|
**Verification**
|
||||||
|
|
||||||
|
- 409 이후 동일 요청에 `confirm_duplicate=true`를 보내 HTTP 201과 별도 entry ID를 확인한다.
|
||||||
|
- 원본을 reversal한 뒤 동일 배당을 다시 제출해 경고 없이 기록되는지 확인한다.
|
||||||
|
|
||||||
|
**Verification Classification**
|
||||||
|
|
||||||
|
- Level: `API | integration`
|
||||||
|
- Backend: PostgreSQL
|
||||||
|
- Cases: `positive | boundary`
|
||||||
|
|
||||||
|
**State Invariants**
|
||||||
|
|
||||||
|
- 확인된 중복은 각각 독립적인 append-only 원장이어야 한다.
|
||||||
|
- reversal된 원본은 중복 억제 상태를 영구히 남기지 않아야 한다.
|
||||||
|
|
||||||
|
### REQ-001.05-AC03 — 동시 중복 요청의 일관성
|
||||||
|
|
||||||
|
**Criterion**
|
||||||
|
|
||||||
|
동일 portfolio에 확인되지 않은 같은 배당 요청이 동시에 도착하더라도 새 배당 항목은 최대 하나만 기록되어야 하며 최종 원장과 portfolio snapshot은 일치해야 한다.
|
||||||
|
|
||||||
|
**Verification**
|
||||||
|
|
||||||
|
실제 PostgreSQL에서 같은 요청을 동시에 제출해 확인되지 않은 요청 둘이 모두 자동 기록되지 않으며, 거부된 요청은 상태를 변경하지 않고 최종 원장 수와 `profit_buffer`가 허용된 기록 수와 일치하는지 확인한다.
|
||||||
|
|
||||||
|
**Verification Classification**
|
||||||
|
|
||||||
|
- Level: `integration`
|
||||||
|
- Backend: PostgreSQL
|
||||||
|
- Cases: `concurrency | unchanged-state`
|
||||||
|
|
||||||
|
**State Invariants**
|
||||||
|
|
||||||
|
- lost update가 발생하지 않아야 한다.
|
||||||
|
- 원장 수와 최종 snapshot 사이에 불일치가 없어야 한다.
|
||||||
|
- 경고로 거부된 transaction은 부분 상태를 남기지 않아야 한다.
|
||||||
|
|
||||||
|
### REQ-001.06-AC01 — 배당 reversal
|
||||||
|
|
||||||
|
**Criterion**
|
||||||
|
|
||||||
|
유효한 `DIVIDEND`를 reversal하면 원본 `amount`만큼 `profit_buffer`가 감소하고 원본 이전 값으로 복원되어야 한다. 원본과 reversal 원장은 모두 남아야 하며 현금과 수량은 변경되지 않아야 한다.
|
||||||
|
|
||||||
|
**Verification**
|
||||||
|
|
||||||
|
- 정상 금액과 최소 단위 `0.00000001`을 각각 기록하고 reversal한다.
|
||||||
|
- 원본 이전 buffer로 정확히 복원되는지 확인한다.
|
||||||
|
- 원본 보존, reversal 대상 ID, 중복 reversal 거부와 buffer 부족 거부를 확인한다.
|
||||||
|
|
||||||
|
**Verification Classification**
|
||||||
|
|
||||||
|
- Level: `API | integration`
|
||||||
|
- Backend: PostgreSQL
|
||||||
|
- Cases: `positive | negative | boundary | unchanged-state`
|
||||||
|
|
||||||
|
**State Invariants**
|
||||||
|
|
||||||
|
- 원본 원장은 수정·삭제되지 않아야 한다.
|
||||||
|
- reversal은 `cash_balance`와 `asset_quantity`를 변경하지 않아야 한다.
|
||||||
|
- 실패한 reversal은 새 원장이나 부분 snapshot을 남기지 않아야 한다.
|
||||||
|
|
||||||
|
### REQ-001.07-AC01 — Decimal round-trip
|
||||||
|
|
||||||
|
**Criterion**
|
||||||
|
|
||||||
|
API가 수용한 최대 8자리 소수의 `amount`와 `tax`는 PostgreSQL 저장, 조회, 중복 비교와 reversal에서 같은 Decimal 의미를 유지해야 한다.
|
||||||
|
|
||||||
|
**Verification**
|
||||||
|
|
||||||
|
- `0.00000001` 등 경계값을 기록하고 DB text 표현·API 응답·reversal 결과를 비교한다.
|
||||||
|
- float로 변환하면 같아질 수 있는 서로 다른 Decimal 입력이 동일한 중복으로 판정되지 않는지 확인한다.
|
||||||
|
|
||||||
|
**Verification Classification**
|
||||||
|
|
||||||
|
- Level: `API | integration`
|
||||||
|
- Backend: PostgreSQL
|
||||||
|
- Cases: `positive | boundary`
|
||||||
|
|
||||||
|
**State Invariants**
|
||||||
|
|
||||||
|
- 수용된 원장 값과 snapshot 효과가 달라져서는 안 된다.
|
||||||
|
- reversal 기준은 원본의 정확한 Decimal 금액이어야 한다.
|
||||||
|
|
||||||
|
### REQ-001.08-AC01 — 기존 action 회귀 없음
|
||||||
|
|
||||||
|
**Criterion**
|
||||||
|
|
||||||
|
기존 manual action은 `DIVIDEND` 도입 전과 같은 유효 입력, 상태 효과와 충돌 정책을 유지해야 한다. 새 배당 전용 field를 사용하지 않는 기존 호출자는 변경 없이 동작해야 한다. 비-`DIVIDEND` action에 `confirm_duplicate=false`를 명시한 요청은 허용하고, `confirm_duplicate=true`를 명시한 요청은 원장 기록 전에 HTTP 422로 거부해야 한다.
|
||||||
|
|
||||||
|
**Verification**
|
||||||
|
|
||||||
|
- 기존 action 전체의 정상·잔액 부족·reversal 경로를 실행한다.
|
||||||
|
- 새 배당 전용 field를 생략한 기존 요청과 저장 데이터의 회귀를 확인한다.
|
||||||
|
- 비-`DIVIDEND` action에 `confirm_duplicate=false`를 명시한 요청이 기존 action과 동일하게 처리되는지 확인한다.
|
||||||
|
- 비-`DIVIDEND` action에 `confirm_duplicate=true`를 명시한 요청이 HTTP 422이며 상태가 변하지 않는지 확인한다.
|
||||||
|
|
||||||
|
**Verification Classification**
|
||||||
|
|
||||||
|
- Level: `API | integration`
|
||||||
|
- Backend: PostgreSQL
|
||||||
|
- Cases: `positive | negative | boundary | unchanged-state`
|
||||||
|
|
||||||
|
**State Invariants**
|
||||||
|
|
||||||
|
- 기존 action의 계산식과 원장 표현이 변경되지 않아야 한다.
|
||||||
|
- 새 배당 전용 field의 기본값은 기존 호출자를 깨뜨리지 않아야 한다.
|
||||||
|
- 비-`DIVIDEND` action의 `confirm_duplicate=true` 거부는 원장과 portfolio snapshot을 변경하지 않아야 한다.
|
||||||
|
|
||||||
|
### REQ-001.09-AC01 — 권한 유지와 민감정보 비노출
|
||||||
|
|
||||||
|
**Criterion**
|
||||||
|
|
||||||
|
배당 기록은 기존 portfolio 소유권과 manual-write 확인 정책을 통과한 사용자에게만 허용되어야 한다. 응답·오류·로그는 이 기능으로 인해 secret, credential 또는 불필요한 account 식별정보를 추가로 노출하지 않아야 한다.
|
||||||
|
|
||||||
|
**Verification**
|
||||||
|
|
||||||
|
- 기존 인증·소유권·daemon-managed portfolio 확인 경로를 실행한다.
|
||||||
|
- 정상 응답, 422, 409와 persistence 오류의 body 및 rendered log를 민감정보 sentinel로 검사한다.
|
||||||
|
|
||||||
|
**Verification Classification**
|
||||||
|
|
||||||
|
- Level: `API | integration`
|
||||||
|
- Backend: PostgreSQL, application log
|
||||||
|
- Cases: `positive | negative | unchanged-state`
|
||||||
|
|
||||||
|
**State Invariants**
|
||||||
|
|
||||||
|
- 권한이 없는 요청은 원장과 snapshot을 변경하지 않아야 한다.
|
||||||
|
- 오류 경로에서도 credential과 connection 정보가 노출되지 않아야 한다.
|
||||||
|
|
||||||
|
## 8. 오류 및 충돌 계약
|
||||||
|
|
||||||
|
- 유효하지 않은 schema 입력은 HTTP 422로 거부하고 상태를 변경하지 않는다.
|
||||||
|
- 중복 후보의 최초 요청은 HTTP 409 `POTENTIAL_DUPLICATE_DIVIDEND`와 `matched_ledger_entry_id`를 반환한다.
|
||||||
|
- 사용자 확인이 필요한 daemon-managed portfolio 정책과 disabled portfolio 정책은 기존 HTTP 409 계약을 유지한다.
|
||||||
|
- reversal 대상 없음, 중복 reversal과 잔액 부족은 기존 manual ledger 충돌 계약을 유지한다.
|
||||||
|
- 입력 검증 또는 저장 실패로 요청을 기록할 수 없으면 부분 기록 없이 거부한다.
|
||||||
|
- DB 오류 또는 transaction 실패 시 원장 append와 portfolio snapshot update를 모두 rollback한다.
|
||||||
|
|
||||||
|
## 9. 데이터 및 인터페이스 계약
|
||||||
|
|
||||||
|
### 요청
|
||||||
|
|
||||||
|
- `action`: `DIVIDEND`
|
||||||
|
- `amount`: 필수, `>0`, Decimal, 최대 20자리·소수점 이하 최대 8자리
|
||||||
|
- `tax`: 선택, 기본값 `0`, `>=0`, Decimal, 최대 20자리·소수점 이하 최대 8자리
|
||||||
|
- `executed_at`: 필수 date-only `YYYY-MM-DD`
|
||||||
|
- `note`: 선택 문자열
|
||||||
|
- `confirm_duplicate`: 선택 boolean, 기본값 `false`
|
||||||
|
|
||||||
|
`confirm_duplicate`는 `DIVIDEND`의 중복 확인 계약에 사용한다. 비-`DIVIDEND` action에서는 명시적 `false`를 허용하고 명시적 `true`를 HTTP 422로 거부한다.
|
||||||
|
|
||||||
|
### 허용하지 않는 DIVIDEND 입력
|
||||||
|
|
||||||
|
- `quantity`
|
||||||
|
- 거래 `price`
|
||||||
|
- `fee`
|
||||||
|
- `target_ledger_entry_id`
|
||||||
|
- 주문 또는 체결 식별자
|
||||||
|
|
||||||
|
### 생성 응답과 unified ledger
|
||||||
|
|
||||||
|
- `ledger_entry_id`
|
||||||
|
- `portfolio_id`
|
||||||
|
- `source=manual`
|
||||||
|
- `action=DIVIDEND`
|
||||||
|
- canonical `amount`
|
||||||
|
- `tax` 또는 unified ledger의 `tax_amount`
|
||||||
|
- UTC 자정의 `executed_at`
|
||||||
|
- 서버 append 시각 `recorded_at`
|
||||||
|
- `note`
|
||||||
|
- `cash_balance_after`
|
||||||
|
- `asset_quantity_after`
|
||||||
|
- `profit_buffer_after`
|
||||||
|
- `total_value_after`
|
||||||
|
|
||||||
|
### 저장 계약
|
||||||
|
|
||||||
|
- 입금일 전용 DB column을 추가하지 않고 기존 `executed_at`을 사용한다.
|
||||||
|
- `tax`는 참고 metadata로 저장한다.
|
||||||
|
- 배당 사실과 after-snapshot은 `source=manual`인 append-only 원장으로 보존한다.
|
||||||
|
|
||||||
|
## 10. 하위호환성
|
||||||
|
|
||||||
|
- 기존 manual ledger action의 필수·선택 field와 상태 계산을 유지한다.
|
||||||
|
- 기존 broker ledger write와 unified ledger 조회 계약을 유지한다.
|
||||||
|
- 새 배당 전용 field를 사용하지 않는 기존 action 요청을 변경 없이 허용한다.
|
||||||
|
- `REVERSAL`의 대상 없음, 잔액 부족과 중복 reversal 정책을 변경하지 않는다.
|
||||||
|
|
||||||
|
## 11. 검증 계획
|
||||||
|
|
||||||
|
- API schema와 OpenAPI에서 `DIVIDEND` action 및 입력·응답 field 검증
|
||||||
|
- 배당 기록 전후 portfolio와 ledger after-snapshot 비교
|
||||||
|
- UTC 입금일과 서버 `recorded_at` 분리 검증
|
||||||
|
- 최초 중복 409, 확인 재제출, reversal된 원본 제외 검증
|
||||||
|
- 동시 중복 요청의 최대 1건 기록과 최종 원장·snapshot 일치 검증
|
||||||
|
- 최소 단위와 저장 범위 경계값의 실제 PostgreSQL Decimal round-trip 검증
|
||||||
|
- 입력 또는 저장 실패의 pre-write rejection과 rollback 검증
|
||||||
|
- 기존 action과 기존 저장 데이터의 하위호환 회귀
|
||||||
|
- 응답·오류·로그의 민감정보 비노출 검증
|
||||||
|
- 문서와 OpenAPI 계약 정합성 검증
|
||||||
|
|
||||||
|
### Test Traceability Contract
|
||||||
|
|
||||||
|
- 이 requirement 문서에는 검증 의도와 판정 조건만 기록한다.
|
||||||
|
- 실행 가능한 테스트 정의는 별도 test catalog artifact에서 관리한다.
|
||||||
|
- 각 테스트는 안정적인 identifier와 `verifies` 목록으로 하나 이상의 `REQ-001.XX-ACYY`를 참조한다.
|
||||||
|
- 테스트 정의의 존재, 자동화 여부, 특정 commit에서의 실행 여부와 PASS 결과를 별도 상태로 관리한다.
|
||||||
|
- 실행 결과는 requirement나 test definition을 수정하지 않고 별도 evidence에 기록한다.
|
||||||
|
- Evidence는 최소한 `REQ-001` revision, 검증한 commit SHA, runtime/backend, 결과와 실행 시각을 포함한다.
|
||||||
|
|
||||||
|
## 12. 추적성
|
||||||
|
|
||||||
|
- 근거 이슈: `https://github.com/fourblocks2020/portfolio-hub/issues/31`
|
||||||
|
- 구현 PR: `https://github.com/fourblocks2020/portfolio-hub/pull/33`
|
||||||
|
- 검토한 PR head: `6df3e4ba029cc465bf28030767190d6af55c5402`
|
||||||
|
- 관련 설계 문서:
|
||||||
|
- `docs/interface-contract.md`
|
||||||
|
- `docs/data-model.md`
|
||||||
|
- `docs/runtime-flows.md`
|
||||||
|
- 관련 requirement: 없음
|
||||||
|
|
||||||
|
## 13. 확정된 결정
|
||||||
|
|
||||||
|
- 사용자는 실수령액 `amount`를 입력한다.
|
||||||
|
- `tax`는 기본값 `0`인 선택적 non-negative metadata이며 자동 계산하지 않는다.
|
||||||
|
- 입금일은 새 DB column 없이 기존 `executed_at`에 저장한다.
|
||||||
|
- date-only 입금일은 UTC 자정으로 변환한다.
|
||||||
|
- 최초 중복 후보 요청은 409로 거부하고 `confirm_duplicate=true`인 재요청만 기록한다.
|
||||||
|
- 구현 범위는 portfolio-hub API·DB·OpenAPI·테스트이며 UI는 별도 연계 범위다.
|
||||||
|
- 잘못 입력한 배당은 기존 append-only `REVERSAL`로 정정한다.
|
||||||
|
- 비-`DIVIDEND` action에서는 `confirm_duplicate=false`를 허용하고 `confirm_duplicate=true`를 HTTP 422로 거부한다.
|
||||||
|
|
||||||
|
## 14. Open Questions
|
||||||
|
|
||||||
|
- 없음
|
||||||
|
|
||||||
|
## 15. 승인 기록
|
||||||
|
|
||||||
|
- 상태: `approved`
|
||||||
|
- 승인 주체: `genichin`
|
||||||
|
- 승인 근거: 이 대화에서 `REQ-001` 승인과 OQ-001 선택지 A를 명시적으로 확정함
|
||||||
Reference in New Issue
Block a user