feat: add persistent monthly settlement drafts and approved valuation selection

This commit is contained in:
2026-10-10 02:49:34 +00:00
parent 68212e773b
commit 92f44661c3
37 changed files with 2902 additions and 29 deletions
@@ -3,13 +3,13 @@
"schema": "gitea-agent.requirement/v1",
"id": "REQ-003",
"title": "계좌 기준일 평가 이력과 자산 마감 연동",
"revision": "1.0",
"revision": "1.1",
"status": "approved",
"depends_on": [],
"applies_to": ["ledger", "dashboard", "mcp"],
"language": "ko",
"issue": "#12",
"implementation_pr": "#13"
"issue": "#15",
"implementation_pr": "미정"
}
---
@@ -21,7 +21,7 @@
## 2. 배경
구현은 SQLite schema 12를 schema 11에 additive하게 적용한다. 계좌 잔액·import 결과는 실제 증거가 없는 한 `unverified` 후보이고, 기존 거래·인증·메뉴 계약을 바꾸지 않는다.
구현은 SQLite schema 13를 schema 11에 additive하게 적용한다. 계좌 잔액·import 결과는 실제 증거가 없는 한 `unverified` 후보이고, 기존 거래·인증·메뉴 계약을 바꾸지 않는다.
## 3. 범위
@@ -52,7 +52,7 @@
### REQ-003.01 — additive 이력과 legacy 보존
schema 11의 테이블·필드·거래를 삭제하거나 추측 이주하지 않고 schema 12의 `account_valuations`, `asset_snapshot_runs`를 추가하며 `asset_snapshots`에 필요한 필드를 additive하게 확장한다. migration은 idempotent다.
schema 11의 테이블·필드·거래를 삭제하거나 추측 이주하지 않고 schema 13의 `account_valuations`, `asset_snapshot_runs`를 추가하며 `asset_snapshots`에 필요한 필드를 additive하게 확장한다. migration은 idempotent다.
**영향 범위**
- 인터페이스: 기존 service/MCP 응답 envelope 유지, 신규 valuation/snapshot fields additive
@@ -172,9 +172,21 @@ MCP와 Dashboard는 shared service/gateway를 사용한다. 기존 success/error
- 호환성: 기존 auth/menu/query/shell/browser evidence semantic inventory 0 missing
- 배포 전제 조건: secret redaction과 fixture DB
### REQ-003.11 — 월정산 초안·승인·원자적 마감 (Issue #15)
월말 기준일의 durable draft는 자료가 없어도 생성되며 생성 자체로 run/rows를 만들지 않는다. 월별 활성 초안은 일반·정정 공통으로 하나다. 대상 구성은 명시적 refresh만 반영하고 검토는 frozen evidence의 version/fingerprint에 묶인다. Finalize는 drift를 거절하고 `BEGIN IMMEDIATE`에서 run, rows, completion/link를 함께 기록하며 동일 요청은 persisted response로 replay한다.
**영향 범위**
- 인터페이스: shared settlement draft service, MCP tools, authenticated Dashboard gateway; monthly summary read-only
- 영속성: schema 12→13 additive `settlement_drafts`, `settlement_draft_items`; legacy 11→12 보존
- 후속 사용 주체: Dashboard operator, MCP caller, close reports/timeseries/monthly read model
- 마이그레이션: additive/idempotent; historical drafts backfill 없음
- 호환성: existing valuation/auth/menu/query/shell/MCP contracts 유지
- 배포 전제 조건: explicit writer init, SQLite transaction, synthetic tests
## 6. 제약과 불변조건
- verified leaf만 authority이며 future/unverified/rejected/unknown은 합산하지 않는다.
- Automatic `exact`/`as_of` selection and legacy direct close exclude future, unverified, rejected, and unknown valuations from authority. The sole future-valuation exception is an explicitly selected, verified `manual_substitute` in a settlement draft, with reason, review, approval, and original valuation/basis dates preserved; a future valuation is never chosen automatically. Unverified, rejected, or unknown amounts are never authority on any path.
- 동일 idempotency payload는 같은 ID를 반환하고 다른 payload는 conflict다.
- correction과 re-close는 append-only이고 parent당 child/run은 하나다.
- NULL과 0을 구별하고 모든 금액은 integer precision이다.
@@ -186,13 +198,353 @@ MCP와 Dashboard는 shared service/gateway를 사용한다. 기존 success/error
## 7. 인수 기준
### REQ-003.11-AC01 — 빈 월 초안 영속 생성
**판정 기준**
자료가 없어도 월말 기준일의 초안이 저장되고 생성만으로 snapshot run/rows가 생기지 않는다.
**검증**
`tests/test_monthly_settlement_drafts.py::test_empty_draft_persists_without_closing_and_has_consistent_public_shape`
**검증 분류**
- 수준: integration
- 백엔드: sqlite
- 케이스: 정상 empty draft; 경계 no targets; 실패 invalid month
**상태 불변조건**
Create는 close side effect가 없다.
### REQ-003.11-AC02 — 월말 기준일 및 create replay
**판정 기준**
월말만 허용하고 같은 key/payload는 idempotent, 다른 payload는 충돌한다.
**검증**
`tests/test_monthly_settlement_drafts.py::test_basis_must_be_calendar_month_end_and_create_key_is_idempotent`
**검증 분류**
- 수준: integration
- 백엔드: sqlite
- 케이스: normal month-end; boundary leap month; failure non-month-end
**상태 불변조건**
거절된 create는 초안을 추가하지 않는다.
### REQ-003.11-AC03 — 월별 단일 active 초안과 correction link
**판정 기준**
Normal/correction 초안을 합쳐 월별 active는 하나이며 취소 후 월 재개가 가능하다.
**검증**
`tests/test_monthly_settlement_drafts.py::test_one_active_draft_across_normal_and_correction_and_cancellation_reopens_month`
**검증 분류**
- 수준: integration
- 백엔드: sqlite
- 케이스: normal competing create; boundary correction lineage; failure second active
**상태 불변조건**
Correction lineage는 명시적이고 유일하다.
### REQ-003.11-AC04 — 입력·버전·행위자 검증
**판정 기준**
잘못된 값과 stale version을 거절하고 include/exclude 사유 및 actor를 기록한다.
**검증**
`tests/test_monthly_settlement_drafts.py::test_input_validation_versions_actors_and_include_exclude_reason`
**검증 분류**
- 수준: integration
- 백엔드: sqlite
- 케이스: normal valid mutation; boundary version; failure invalid type
**상태 불변조건**
거절된 mutation은 상태와 version을 변경하지 않는다.
### REQ-003.11-AC05 — 명시적 target refresh와 read-only review
**판정 기준**
대상 membership은 refresh 전 고정되고 review는 읽기 전용이다.
**검증**
`tests/test_monthly_settlement_drafts.py::test_targets_are_frozen_until_explicit_refresh_and_review_is_read_only`
**검증 분류**
- 수준: integration
- 백엔드: sqlite
- 케이스: normal refresh; boundary changed sources; failure implicit refresh
**상태 불변조건**
Review는 DB 상태를 쓰지 않는다.
### REQ-003.11-AC06 — exact, carry, manual substitute 날짜 구분
**판정 기준**
선택 모드는 valuation date와 basis date 차이를 보존하며 future substitute는 명시적이다.
**검증**
`tests/test_monthly_settlement_drafts.py::test_explicit_future_substitute_and_late_recorded_exact_are_distinguished`
**검증 분류**
- 수준: integration
- 백엔드: sqlite
- 케이스: normal exact; boundary future substitute; failure implicit future choice
**상태 불변조건**
`date_difference_days = valuation_date - basis_date`를 보존한다.
### REQ-003.11-AC07 — unverified 후보는 authority 아님
**판정 기준**
Unverified 후보 금액은 표시 가능하나 authority amount는 NULL이며 verified child는 재선택을 요구한다.
**검증**
`tests/test_monthly_settlement_drafts.py::test_unverified_candidate_has_no_authority_and_verified_child_requires_reselection`
**검증 분류**
- 수준: integration
- 백엔드: sqlite
- 케이스: normal verified selection; boundary candidate; failure candidate authority
**상태 불변조건**
NULL은 0이나 verified 금액으로 대체되지 않는다.
### REQ-003.11-AC08 — 수동 evidence drift와 future 차단
**판정 기준**
수동 source 변경은 명시적 refresh가 필요하고 future evidence가 자동 선택되지 않는다.
**검증**
`tests/test_monthly_settlement_drafts.py::test_manual_sources_drift_requires_explicit_refresh_and_future_not_auto_selected`
**검증 분류**
- 수준: integration
- 백엔드: sqlite
- 케이스: normal unchanged source; boundary drift; failure auto-select future
**상태 불변조건**
Frozen source hash는 refresh 전까지 불변이다.
### REQ-003.11-AC09 — 승인 fingerprint 및 drift gate
**판정 기준**
Mutation/source drift는 approval을 무효화하고 finalize를 막는다.
**검증**
`tests/test_monthly_settlement_drafts.py::test_approval_fingerprint_mutation_and_external_drift_block_finalize`
**검증 분류**
- 수준: integration
- 백엔드: sqlite
- 케이스: normal unchanged approval; boundary drift; failure stale finalize
**상태 불변조건**
Finalize는 승인된 version/fingerprint만 받는다.
### REQ-003.11-AC10 — 원자적 finalize 및 persisted replay
**판정 기준**
Finalize는 immutable provenance를 저장하고 동일 요청에 persisted response를 반환한다.
**검증**
`tests/test_monthly_settlement_drafts.py::test_finalize_persists_immutable_provenance_replays_exact_response_and_conflicts_key`
**검증 분류**
- 수준: integration
- 백엔드: sqlite
- 케이스: normal finalize; boundary replay; failure conflicting key
**상태 불변조건**
Replay는 저장된 결과를 재구성하며 중복 run을 만들지 않는다.
### REQ-003.11-AC11 — DB 실패 rollback
**판정 기준**
SQLite write 실패 시 run, rows, completed 상태가 모두 rollback된다.
**검증**
`tests/test_monthly_settlement_drafts.py::test_sqlite_trigger_failure_rolls_back_run_rows_and_completion`
**검증 분류**
- 수준: integration
- 백엔드: sqlite
- 케이스: normal successful commit; boundary trigger failure; failure mid-transaction
**상태 불변조건**
부분 마감 결과는 남지 않는다.
### REQ-003.11-AC12 — additive schema 13 migration
**판정 기준**
Migration은 legacy rows를 보존하고 반복 실행에 안전하다.
**검증**
`tests/test_monthly_settlement_drafts.py::test_schema13_migration_is_additive_and_idempotent`
**검증 분류**
- 수준: integration
- 백엔드: sqlite
- 케이스: normal new DB; boundary schema 12; failure repeated migration
**상태 불변조건**
Schema 11→12 의미와 데이터는 보존된다.
### REQ-003.11-AC13 — 동시 create와 stale finalize
**판정 기준**
동시 create는 active uniqueness를 지키고 stale version finalize는 거절한다.
**검증**
`tests/test_monthly_settlement_drafts.py::test_concurrent_create_and_stale_version_finalization_are_safe`
**검증 분류**
- 수준: integration
- 백엔드: sqlite
- 케이스: normal sequential mutation; boundary concurrent create; failure stale finalize
**상태 불변조건**
한 달에 active 초안은 최대 하나다.
### REQ-003.11-AC14 — draft adapter public contract
**판정 기준**
Gateway/MCP는 실제 service signature와 결과를 그대로 노출한다.
**검증**
`tests/test_settlement_draft_adapters.py::test_gateway_exports_exact_contract_signature_and_delegates`
**검증 분류**
- 수준: integration
- 백엔드: application
- 케이스: normal delegation; boundary signature; failure service error
**상태 불변조건**
Adapter는 authority 판단이나 응답 변형을 하지 않는다.
### REQ-003.11-AC15 — Dashboard auth 및 render 경계
**판정 기준**
인증은 DB 접근 전 수행되고 render는 lifecycle mutation을 하지 않는다.
**검증**
`tests/test_settlement_draft_adapters.py::test_dashboard_authentication_entrypoint_precedes_database_resolution`
**검증 분류**
- 수준: integration
- 백엔드: dashboard
- 케이스: normal authenticated read; boundary unauthenticated; failure DB access before auth
**상태 불변조건**
인증 없는 요청은 DB에 접근하지 않는다.
### REQ-003.11-AC16 — missing DB read no-create
**판정 기준**
Read-only gateway missing database를 생성하거나 migrate하지 않는다.
**검증**
`tests/test_settlement_draft_adapters.py::test_read_only_gateway_calls_do_not_create_missing_database`
**검증 분류**
- 수준: integration
- 백엔드: sqlite
- 케이스: normal initialized read; boundary missing DB; failure implicit create
**상태 불변조건**
Read path는 no mkdir, no migrate, no commit이다.
### REQ-003.11-AC17 — legacy contract regression
**판정 기준**
Valuation correction, monthly summary read-only, menu/auth compatibility regressions remain green.
**검증**
`tests/test_settlement_draft_adapters.py::test_service_reexports_every_lifecycle_operation`
**검증 분류**
- 수준: integration
- 백엔드: application
- 케이스: normal legacy call; boundary old schema; failure incompatible export
**상태 불변조건**
New draft operations are additive and legacy entrypoints remain available.
### REQ-003.01-AC01 — migration and preservation
**Cases (정상/경계/실패)**: 정상 schema 12 init twice; 경계 schema 11 legacy DB; 실패 malformed legacy DB.
**Cases (정상/경계/실패)**: 정상 schema 13 init twice; 경계 schema 11 legacy DB; 실패 malformed legacy DB.
**State Invariants**: legacy tables/rows remain; migration repeats without duplicate columns.
**판정 기준**: `schema_version=12`, required tables/columns/indexes exist and old rows remain.
**판정 기준**: `schema_version=13`, required tables/columns/indexes exist and old rows remain.
**검증**: `tests/test_account_valuations.py::test_account_valuation_schema_is_v12_and_idempotent`, `tests/test_issue_12_regressions.py::test_schema12_migration_preserves_legacy_rows_indexes_and_is_idempotent`.
**검증 분류**: level=integration; backend=sqlite