From 78e347703e4f95e65e5b7aa0d45990cdc17fe382 Mon Sep 17 00:00:00 2001 From: Justin Ko Date: Wed, 9 Sep 2026 20:19:39 +0900 Subject: [PATCH] docs(requirements): approve manual cash dividend contract --- docs/requirements/README.md | 48 ++ docs/requirements/TEMPLATE.md | 212 +++++++ .../req-001_manual-cash-dividend.md | 591 ++++++++++++++++++ 3 files changed, 851 insertions(+) create mode 100644 docs/requirements/README.md create mode 100644 docs/requirements/TEMPLATE.md create mode 100644 docs/requirements/req-001_manual-cash-dividend.md diff --git a/docs/requirements/README.md b/docs/requirements/README.md new file mode 100644 index 0000000..505539c --- /dev/null +++ b/docs/requirements/README.md @@ -0,0 +1,48 @@ +# Requirements + +각 requirement 문서는 Markdown body 앞에 JSON front matter를 둔다. + +## File naming + +```text +req-NNN_.md +``` + +- `NNN`: requirement ID의 세 자리 decimal number +- ``: 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다. diff --git a/docs/requirements/TEMPLATE.md b/docs/requirements/TEMPLATE.md new file mode 100644 index 0000000..0914054 --- /dev/null +++ b/docs/requirements/TEMPLATE.md @@ -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: +Automated: true | false +``` + +Execution evidence 예시: + +```text +Test ID: TC-NNN-001 +Requirement revision: REQ-NNN@major.minor +Commit: +Environment: +Result: PASS | FAIL | BLOCKED +Executed at: +``` + +## 12. 추적성 + +- 근거 이슈: `` +- 구현 PR: `` +- 관련 설계 문서: `` +- 관련 requirement: `` + +## 13. 확정된 결정 + +- 사용자와 확정한 제품·정책 결정을 기록한다. +- 확정된 결정은 Open Question으로 다시 남기지 않는다. + +## 14. Open Questions + +`approved` 전 반드시 결정해야 하며, 각 항목에는 선택지, 권장안, 결정 주체와 종료 조건을 둔다. 미결정 항목이 없으면 `없음`으로 기록한다. + +- 없음 + +## 15. 승인 기록 + +- 상태: `draft` +- 승인 주체: 미승인 +- 승인 근거: 없음 diff --git a/docs/requirements/req-001_manual-cash-dividend.md b/docs/requirements/req-001_manual-cash-dividend.md new file mode 100644 index 0000000..1730fa7 --- /dev/null +++ b/docs/requirements/req-001_manual-cash-dividend.md @@ -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를 명시적으로 확정함