feat: extend comprehensive monthly settlement snapshots (#18)
This commit is contained in:
@@ -3,13 +3,13 @@
|
||||
"schema": "gitea-agent.requirement/v1",
|
||||
"id": "REQ-003",
|
||||
"title": "계좌 기준일 평가 이력과 자산 마감 연동",
|
||||
"revision": "1.1",
|
||||
"revision": "1.2",
|
||||
"status": "approved",
|
||||
"depends_on": [],
|
||||
"applies_to": ["ledger", "dashboard", "mcp"],
|
||||
"language": "ko",
|
||||
"issue": "#15",
|
||||
"implementation_pr": "https://gitea.jsfamily.quest/jsfamily/jarca-ledger-mcp/pulls/16"
|
||||
"issue": "#18",
|
||||
"implementation_pr": "미정"
|
||||
}
|
||||
---
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
## 2. 배경
|
||||
|
||||
구현은 SQLite schema 13를 schema 11에 additive하게 적용한다. 계좌 잔액·import 결과는 실제 증거가 없는 한 `unverified` 후보이고, 기존 거래·인증·메뉴 계약을 바꾸지 않는다.
|
||||
기존 schema 11→13의 additive 이력을 보존하고 이번 종합 정산 확장은 schema 14를 additive하게 적용한다. 계좌 잔액·import 결과는 실제 증거가 없는 한 `unverified` 후보이고, 기존 거래·인증·메뉴 계약을 바꾸지 않는다.
|
||||
|
||||
## 3. 범위
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
### REQ-003.01 — additive 이력과 legacy 보존
|
||||
|
||||
schema 11의 테이블·필드·거래를 삭제하거나 추측 이주하지 않고 schema 13의 `account_valuations`, `asset_snapshot_runs`를 추가하며 `asset_snapshots`에 필요한 필드를 additive하게 확장한다. migration은 idempotent다.
|
||||
schema 11의 테이블·필드·거래를 삭제하거나 추측 이주하지 않고 schema 13의 `account_valuations`, `asset_snapshot_runs`를 추가하며 `asset_snapshots`에 필요한 필드를 additive하게 확장한다. migration은 idempotent다. Issue #18의 유일한 schema 확장은 additive/idempotent 13→14이며 기존 자산 전용 run의 미저장 종합 총계는 NULL로 유지하고 backfill하지 않는다.
|
||||
|
||||
**영향 범위**
|
||||
- 인터페이스: 기존 service/MCP 응답 envelope 유지, 신규 valuation/snapshot fields additive
|
||||
@@ -172,7 +172,7 @@ 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)
|
||||
### REQ-003.11 — 월정산 초안·승인·원자적 종합 마감 (Issue #15, #18)
|
||||
|
||||
월말 기준일의 durable draft는 자료가 없어도 생성되며 생성 자체로 run/rows를 만들지 않는다. 월별 활성 초안은 일반·정정 공통으로 하나다. 대상 구성은 명시적 refresh만 반영하고 검토는 frozen evidence의 version/fingerprint에 묶인다. Finalize는 drift를 거절하고 `BEGIN IMMEDIATE`에서 run, rows, completion/link를 함께 기록하며 동일 요청은 persisted response로 replay한다.
|
||||
|
||||
@@ -418,7 +418,7 @@ SQLite write 실패 시 run, rows, completed 상태가 모두 rollback된다.
|
||||
|
||||
부분 마감 결과는 남지 않는다.
|
||||
|
||||
### REQ-003.11-AC12 — additive schema 13 migration
|
||||
### REQ-003.11-AC12 — additive schema 14 migration
|
||||
|
||||
**판정 기준**
|
||||
|
||||
@@ -540,11 +540,11 @@ New draft operations are additive and legacy entrypoints remain available.
|
||||
|
||||
### REQ-003.01-AC01 — migration and preservation
|
||||
|
||||
**Cases (정상/경계/실패)**: 정상 schema 13 init twice; 경계 schema 11 legacy DB; 실패 malformed legacy DB.
|
||||
**Cases (정상/경계/실패)**: 정상 schema 14 init twice; 경계 schema 11/13 legacy DB; 실패 malformed legacy DB.
|
||||
|
||||
**State Invariants**: legacy tables/rows remain; migration repeats without duplicate columns.
|
||||
|
||||
**판정 기준**: `schema_version=13`, required tables/columns/indexes exist and old rows remain.
|
||||
**판정 기준**: `schema_version=14`, 기존 및 종합 정산 tables/columns/indexes가 존재하고 old rows와 구버전 미보존 NULL을 유지한다.
|
||||
|
||||
**검증**: `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
|
||||
@@ -648,6 +648,206 @@ New draft operations are additive and legacy entrypoints remain available.
|
||||
**검증**: `tests/test_requirement_docs.py`, auth/menu evidence tests, repository redaction/static search.
|
||||
**검증 분류**: level=static; backend=dashboard
|
||||
|
||||
### REQ-003.11-AC18 — 종합 저장 구조와 여섯 총계
|
||||
|
||||
**판정 기준**
|
||||
|
||||
같은 run의 헤더에 여섯 총계·월·통화·정책·버전·근거가 저장되고 양 상세는 같은 ID에 결속한다. 현금흐름 상세의 signed INTEGER, positive count, FK RESTRICT, unique5, NOT NULL 빈 분류를 검증한다.
|
||||
|
||||
**검증**
|
||||
|
||||
`tests/test_comprehensive_settlement.py::test_closed_header_persists_six_totals_policy_and_versions`; `tests/test_settlement_cashflow_schema.py::test_cashflow_detail_constraints_preserve_blank_categories` (실제 실행 기록과 최종 head에 결속하며 문서 기재 자체는 PASS가 아니다.)
|
||||
|
||||
**검증 분류**
|
||||
|
||||
- 수준: integration
|
||||
- 백엔드: sqlite
|
||||
- 케이스: 정상·경계·실패 synthetic 자료
|
||||
|
||||
**상태 불변조건**
|
||||
|
||||
기존 REQ-003 권위·승인·호환성 의미를 유지하며 미검증 또는 실패 상태는 완료로 판정하지 않는다.
|
||||
|
||||
### REQ-003.11-AC19 — 환급·저축·이동·대출상환 정책
|
||||
|
||||
**판정 기준**
|
||||
|
||||
환급월의 원래 지출 분류에 음수 지출을 반영한다. 명시적 category/subcategory 저축 목록만 expense에 포함하고 debt_payment는 expense로 매핑한다. 일반 이체·초기잔액 조정·명시적 상품권 충전 제외를 우선하고 investment 전체를 포함하지 않는다.
|
||||
|
||||
**검증**
|
||||
|
||||
`tests/test_settlement_cashflow_policy.py::test_explicit_savings_refund_and_movement_precedence`; `tests/test_settlement_cashflow_policy.py::test_refund_reduces_debt_payment_and_selected_savings`; `tests/test_settlement_cashflow_policy.py::test_month_boundaries_refund_month_negative_total_and_fx_blocker` (실제 실행 기록과 최종 head에 결속하며 문서 기재 자체는 PASS가 아니다.)
|
||||
|
||||
**검증 분류**
|
||||
|
||||
- 수준: integration
|
||||
- 백엔드: sqlite
|
||||
- 케이스: 정상·경계·실패 synthetic 자료
|
||||
|
||||
**상태 불변조건**
|
||||
|
||||
기존 REQ-003 권위·승인·호환성 의미를 유지하며 미검증 또는 실패 상태는 완료로 판정하지 않는다.
|
||||
|
||||
### REQ-003.11-AC20 — 미결정 분류 승인 차단
|
||||
|
||||
**판정 기준**
|
||||
|
||||
대응 분류 없는 환급과 미결정 unknown은 종합 승인·마감을 차단한다. 자산의 세 조건부 승인 플래그는 이 차단을 우회하지 못한다. 재분류 또는 명시적 포함/제외 판단 뒤 재검토하며 확정 제외는 차단하지 않는다.
|
||||
|
||||
**검증**
|
||||
|
||||
`tests/test_settlement_cashflow_policy.py::test_unknown_classification_blocks_and_explicit_exclusion_unblocks`; `tests/test_settlement_cashflow_policy.py::test_unknown_cashflow_source_is_not_recorded_as_excluded` (실제 실행 기록과 최종 head에 결속하며 문서 기재 자체는 PASS가 아니다.)
|
||||
|
||||
**검증 분류**
|
||||
|
||||
- 수준: integration
|
||||
- 백엔드: sqlite
|
||||
- 케이스: 정상·경계·실패 synthetic 자료
|
||||
|
||||
**상태 불변조건**
|
||||
|
||||
기존 REQ-003 권위·승인·호환성 의미를 유지하며 미검증 또는 실패 상태는 완료로 판정하지 않는다.
|
||||
|
||||
### REQ-003.11-AC21 — 당시 계산 근거와 부채 부호
|
||||
|
||||
**판정 기준**
|
||||
|
||||
거래 원본 ID/날짜/type/금액/통화/분류와 적용 판단·금액, 선택 자산 평가 ID/원본 날짜/출처/적용 방식/포함·제외를 고정한다. 부채 총계는 기존 abs(sum(raw loans))이며 상세 applied_amount 기여도 합이 이에 일치하고 원본을 덮어쓰지 않는다.
|
||||
|
||||
**검증**
|
||||
|
||||
`tests/test_comprehensive_settlement.py::test_asset_detail_preserves_raw_evidence_and_signed_liability_contributions`; `tests/test_comprehensive_settlement.py::test_source_snapshot_preserves_reproducible_values_without_raw_secrets` (실제 실행 기록과 최종 head에 결속하며 문서 기재 자체는 PASS가 아니다.)
|
||||
|
||||
**검증 분류**
|
||||
|
||||
- 수준: integration
|
||||
- 백엔드: sqlite
|
||||
- 케이스: 정상·경계·실패 synthetic 자료
|
||||
|
||||
**상태 불변조건**
|
||||
|
||||
기존 REQ-003 권위·승인·호환성 의미를 유지하며 미검증 또는 실패 상태는 완료로 판정하지 않는다.
|
||||
|
||||
### REQ-003.11-AC22 — 승인 지문의 모든 변경 입력
|
||||
|
||||
**판정 기준**
|
||||
|
||||
거래 추가·삭제·금액/분류 변경, 정책·집계 버전 변경, 선택 평가자료·포함/제외/미확정 변경을 지문에 결속한다. stale 승인으로는 마감하지 않고 정책 편집은 version 증가와 승인 무효화를 수행한다.
|
||||
|
||||
**검증**
|
||||
|
||||
`tests/test_comprehensive_settlement.py::test_source_policy_and_category_drift_invalidate_approval`; `tests/test_settlement_cashflow_policy.py::test_policy_edit_persists_versions_and_invalidates_approval` (실제 실행 기록과 최종 head에 결속하며 문서 기재 자체는 PASS가 아니다.)
|
||||
|
||||
**검증 분류**
|
||||
|
||||
- 수준: integration
|
||||
- 백엔드: sqlite
|
||||
- 케이스: 정상·경계·실패 synthetic 자료
|
||||
|
||||
**상태 불변조건**
|
||||
|
||||
기존 REQ-003 권위·승인·호환성 의미를 유지하며 미검증 또는 실패 상태는 완료로 판정하지 않는다.
|
||||
|
||||
### REQ-003.11-AC23 — 원자적 저장과 실제 저장 일치
|
||||
|
||||
**판정 기준**
|
||||
|
||||
income/expense 상세 합, net_cashflow 차, 반영 자산/부채 상세 합, net_worth 차의 여섯 식과 승인 결과를 실제 저장된 헤더·상세·근거와 대조한다. 불일치·중간 실패는 run/양 상세/완료 상태/응답 전체를 롤백한다.
|
||||
|
||||
**검증**
|
||||
|
||||
`tests/test_comprehensive_settlement.py::test_mid_close_failure_rolls_back_all_settlement_state`; `tests/test_comprehensive_settlement.py::test_close_rejects_stored_detail_sum_tampering` (실제 실행 기록과 최종 head에 결속하며 문서 기재 자체는 PASS가 아니다.)
|
||||
|
||||
**검증 분류**
|
||||
|
||||
- 수준: integration
|
||||
- 백엔드: sqlite
|
||||
- 케이스: 정상·경계·실패 synthetic 자료
|
||||
|
||||
**상태 불변조건**
|
||||
|
||||
기존 REQ-003 권위·승인·호환성 의미를 유지하며 미검증 또는 실패 상태는 완료로 판정하지 않는다.
|
||||
|
||||
### REQ-003.11-AC24 — 멱등 재시도와 동시 마감
|
||||
|
||||
**판정 기준**
|
||||
|
||||
동일 승인 요청·멱등 키는 하나의 run과 동일 저장 응답을 반환한다. 변경된 요청은 충돌하며 동시 요청에서도 중복 run이나 부분 완료가 없다.
|
||||
|
||||
**검증**
|
||||
|
||||
`tests/test_comprehensive_settlement.py::test_concurrent_close_retries_return_one_saved_result`; `tests/test_comprehensive_settlement.py::test_replay_and_correction_keep_each_run_immutable` (실제 실행 기록과 최종 head에 결속하며 문서 기재 자체는 PASS가 아니다.)
|
||||
|
||||
**검증 분류**
|
||||
|
||||
- 수준: integration
|
||||
- 백엔드: sqlite
|
||||
- 케이스: 정상·경계·실패 synthetic 자료
|
||||
|
||||
**상태 불변조건**
|
||||
|
||||
기존 REQ-003 권위·승인·호환성 의미를 유지하며 미검증 또는 실패 상태는 완료로 판정하지 않는다.
|
||||
|
||||
### REQ-003.11-AC25 — 불변 이력과 정정 분리
|
||||
|
||||
**판정 기준**
|
||||
|
||||
완료 이후 원장 변경으로 기존 헤더·양 상세·근거·저장 응답을 바꾸지 않는다. 최신 parent·단일 child 계약에 따라 정정 run을 append하며 최초와 정정을 exact run으로 각각 조회한다.
|
||||
|
||||
**검증**
|
||||
|
||||
`tests/test_comprehensive_settlement.py::test_completed_get_and_saved_snapshot_ignore_later_live_ledger_changes`; `tests/test_comprehensive_settlement.py::test_replay_and_correction_keep_each_run_immutable` (실제 실행 기록과 최종 head에 결속하며 문서 기재 자체는 PASS가 아니다.)
|
||||
|
||||
**검증 분류**
|
||||
|
||||
- 수준: integration
|
||||
- 백엔드: sqlite
|
||||
- 케이스: 정상·경계·실패 synthetic 자료
|
||||
|
||||
**상태 불변조건**
|
||||
|
||||
기존 REQ-003 권위·승인·호환성 의미를 유지하며 미검증 또는 실패 상태는 완료로 판정하지 않는다.
|
||||
|
||||
### REQ-003.11-AC26 — legacy NULL과 기존 초안 재승인
|
||||
|
||||
**판정 기준**
|
||||
|
||||
schema14 additive/idempotent 이관은 legacy 미보존 값을 NULL로 두며 현재 원장 소급 backfill이나 fake0을 하지 않는다. 기존 초안과 이력을 유지하지만 자산-only 과거 승인으로 종합 마감할 수 없고 새 검토/승인이 필요하다.
|
||||
|
||||
**검증**
|
||||
|
||||
`tests/test_settlement_cashflow_schema.py::test_legacy_migration_is_idempotent_and_keeps_unstored_totals_null`; `tests/test_settlement_cashflow_schema.py::test_cashflow_detail_constraints_preserve_blank_categories`; `tests/test_comprehensive_settlement.py::test_legacy_asset_approval_requires_comprehensive_review_before_close`; `tests/test_comprehensive_settlement.py::test_legacy_reader_accepts_original_header_without_migration` (실제 실행 기록과 최종 head에 결속하며 문서 기재 자체는 PASS가 아니다.)
|
||||
|
||||
**검증 분류**
|
||||
|
||||
- 수준: integration
|
||||
- 백엔드: sqlite
|
||||
- 케이스: 정상·경계·실패 synthetic 자료
|
||||
|
||||
**상태 불변조건**
|
||||
|
||||
기존 REQ-003 권위·승인·호환성 의미를 유지하며 미검증 또는 실패 상태는 완료로 판정하지 않는다.
|
||||
|
||||
### REQ-003.11-AC27 — 공개 계약·출처 구분·읽기 보안
|
||||
|
||||
**판정 기준**
|
||||
|
||||
shared service→MCP/gateway→인증된 UI에서 여섯 총계·분류/경고와 저장 run/버전을 표시한다. 기존 자산-only 직접 API와 live 보고는 그대로이며 저장 조회는 readonly/no-create다. ROOT review/finalize, active get.review, completed get.final_response를 유지하고 인증정보/원본 민감 payload를 공개하지 않는다.
|
||||
|
||||
**검증**
|
||||
|
||||
`tests/test_comprehensive_settlement_adapters.py::test_shared_adapters_expose_policy_and_saved_snapshot_contracts`; `tests/test_comprehensive_settlement.py::test_stored_reader_allowlists_source_root_and_nested_decision`; `tests/test_comprehensive_settlement.py::test_stored_reader_keeps_required_transaction_date`; `tests/test_comprehensive_settlement.py::test_saved_reader_rejects_unsupported_comprehensive_versions_without_writes`; `tests/test_comprehensive_settlement.py::test_saved_reader_does_not_mislabel_partial_version_metadata_as_legacy`; `tests/test_comprehensive_settlement_adapters.py::test_real_comprehensive_stdio_policy_close_and_saved_snapshot`; `tests/test_comprehensive_settlement_adapters.py::test_authenticated_dashboard_renders_saved_comprehensive_snapshot` (실제 실행 기록과 최종 head에 결속하며 문서 기재 자체는 PASS가 아니다.)
|
||||
|
||||
**검증 분류**
|
||||
|
||||
- 수준: integration
|
||||
- 백엔드: sqlite
|
||||
- 케이스: 정상·경계·실패 synthetic 자료
|
||||
|
||||
**상태 불변조건**
|
||||
|
||||
기존 REQ-003 권위·승인·호환성 의미를 유지하며 미검증 또는 실패 상태는 완료로 판정하지 않는다.
|
||||
|
||||
## 8. 오류 및 충돌 계약
|
||||
|
||||
공통 envelope는 성공 시 `success: true`, 실패 시 `success: false, error_code, error`를 유지한다. 주요 codes는 `validation_error`, `not_found`, `idempotency_conflict`, `correction_conflict`, `ambiguous_valuation`, `conditional_approval_required`, `db_error`다. SQLite unique/transaction conflict는 추가 write 없이 실패한다.
|
||||
@@ -662,9 +862,19 @@ REQ-001의 auth gate·cookie/cleanup/logout, REQ-002의 메뉴 10개와 order(`
|
||||
|
||||
## 11. 추적성
|
||||
|
||||
- 근거 이슈: `#12`
|
||||
- 근거 이슈: `#12`, `#15`, `#18`
|
||||
- 구현 PR: `#13`
|
||||
- Issue #15 구현 PR: https://gitea.jsfamily.quest/jsfamily/jarca-ledger-mcp/pulls/16
|
||||
- 관련 설계 문서: `docs/data-model.md`, `docs/interface-contract.md`, `docs/project-structure.md`, `docs/runtime-flow.md`
|
||||
- 관련 요구사항: `REQ-001`, `REQ-002`
|
||||
|
||||
### Issue #18 종합 정산 확장 (revision 1.2; 구현 PR 미정)
|
||||
|
||||
기존 요구사항·AC 식별자 및 공통 섹션은 유지하고 REQ-003.11만 확장한다. 월 수입·지출은 활성 거래의 [월초, 다음 월초) 범위, 자산·부채는 기존 월말 기준이다. 초안 정책은 `draft.cashflow_policy`에 저장하며 savings는 사용자가 명시한 category/subcategory pair만 인정한다. 생활비·선택 savings·`debt_payment`은 expense; 일반 transfer와 voucher top-up 제외가 우선하고 `investment` 전체 포함은 금지한다. Refund는 환급월 원래 지출 분류에 signed negative expense로 기록하며 과거 run을 수정하지 않는다. Unresolved unknown은 승인 blocker이며 asset conditional flag로 우회할 수 없고, 확정 제외는 blocker가 아니다. KRW only, no FX.
|
||||
|
||||
Run header는 settlement month/currency, 여섯 총계, policy JSON, aggregation/schema version, 당시 source JSON을 보존한다. `settlement_cashflow_snapshots`는 `id, settlement_run_id, cashflow_kind, category, subcategory, amount, transaction_count, currency` 8개 필드, signed INTEGER amount, positive count, FK RESTRICT, 5-field unique(run, cashflow_kind, category, subcategory, currency), run index를 갖는다. category/subcategory는 NOT NULL이며 빈 문자열은 그대로 구별한다. 기존 자산 상세와 authority/verified·exact/carry/substitute·missing/exclusion 근거를 유지한다.
|
||||
|
||||
동일 run의 income/expense grouped 합이 각 총계와 일치하고 `net_cashflow=income_total-expense_total`; 자산·부채는 포함 상세 합이며 `net_worth=total_assets-total_liabilities`다. Review fingerprint는 거래 당시 값·분류·정책/버전·grouped 결과/여섯 총계·선택 평가자료·포함/제외/미결정 상태를 포함하고 drift 시 finalize를 거부한다. Header, 두 상세 영역, evidence 및 draft completion/final response는 한 transaction이며 오류 시 모두 rollback. Equal idempotent request replays persisted response; changed request conflicts. Correction validates latest single parent and appends run/details; no historical overwrite. Legacy totals stay NULL, incomplete draft needs fresh review/approval.
|
||||
|
||||
Public service additions are `set_settlement_cashflow_policy(draft_id,expected_version,updated_by,cashflow_policy,db_path=None)` and `get_monthly_settlement_snapshot(month='',run_id='',db_path=None)`; MCP omits `db_path`. Review/finalize fields are at response ROOT; active get nests review; completed get exposes ROOT `final_response`. Group key is `cashflow_kind`. Stored run snapshot differs from live report; existing asset-only API and direct-close meaning remain unchanged. Reads are readonly/no-create; Dashboard authenticates before DB. Synthetic tests cover all Issue inclusion cases; no product test result is asserted here.
|
||||
- DB lifecycle: writer/bootstrap은 명시적 `init_db()` 호출로만 생성·마이그레이션하고, `read_db()`는 SQLite `mode=ro`로 연다. missing DB는 `db_not_initialized`이며 read path는 no mkdir, no migrate, no commit이다. first-use write만 init_db를 수행하고 auth-before-DB 순서를 보존한다.
|
||||
|
||||
Reference in New Issue
Block a user