feat: add persistent monthly settlement drafts and approved valuation selection
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user