feat: add persistent monthly settlement drafts and approved valuation selection
This commit is contained in:
@@ -174,3 +174,22 @@
|
||||
- 오류 데이터: invalid currency/kind, missing loan definition, idempotency/correction conflict
|
||||
- 저장 후 재조회: service list/select/report and contract regression tests
|
||||
- 마이그레이션: `test_*schema*` tests plus `python -m pytest tests/test_account_valuations.py tests/test_asset_snapshots.py`
|
||||
|
||||
## settlement-drafts — 월정산 초안
|
||||
|
||||
Schema 13 adds two additive tables; `init_db()`/`migrate_db()` remain the only schema writers and schema 11→12 history is unchanged. No draft backfill is inferred. Exact migrated columns (from `infrastructure/sqlite/migrations.py`) are listed below; `SCHEMA_SQL` and migration DDL are the schema authority.
|
||||
|
||||
| Table | Columns (SQLite types; `?` means nullable) | Indexes / constraints |
|
||||
|---|---|---|
|
||||
| `settlement_drafts` | `id TEXT PK`, `month TEXT NOT NULL`, `basis_date TEXT NOT NULL`, `status TEXT NOT NULL`, `version INTEGER NOT NULL`, `created_by TEXT NOT NULL`, `created_at TEXT NOT NULL`, `updated_at TEXT NOT NULL`, `memo TEXT DEFAULT ''`, `create_key TEXT UNIQUE NOT NULL`, `correction_of_run_id TEXT DEFAULT ''`, `correction_reason TEXT DEFAULT ''`, `final_run_id TEXT DEFAULT ''`, `final_response TEXT DEFAULT ''`, `cancelled_by TEXT DEFAULT ''`, `cancellation_reason TEXT DEFAULT ''`, `cancelled_at TEXT DEFAULT ''`, `approval_fingerprint TEXT DEFAULT ''`, `approval_version INTEGER DEFAULT 0`, `approved_by TEXT DEFAULT ''`, `approved_at TEXT DEFAULT ''`, `approval_reason TEXT DEFAULT ''`, `approval_reference TEXT DEFAULT ''`, `approval_flags TEXT DEFAULT '{}'`, additive `original_request_hash TEXT DEFAULT ''` | `idx_settlement_drafts_active_month(month) WHERE status='active'` unique; `idx_settlement_drafts_month(month,created_at)` |
|
||||
| `settlement_draft_items` | `id TEXT PK`, `draft_id TEXT NOT NULL FK`, `source_type TEXT NOT NULL`, `source_id TEXT NOT NULL`, `valuation_id TEXT DEFAULT ''`, `selection_mode TEXT DEFAULT 'exact'`, `selection_reason TEXT DEFAULT ''`, `included INTEGER DEFAULT 1`, `exclusion_reason TEXT DEFAULT ''`, `version INTEGER DEFAULT 1`, `selected_by TEXT DEFAULT ''`, `selected_at TEXT DEFAULT ''`, `evidence_json TEXT DEFAULT '{}'` | `UNIQUE(draft_id,source_type,source_id)` |
|
||||
|
||||
`settlement_drafts` therefore owns generated draft ID, YYYY-MM month, fixed month-end `basis_date`, active/completed/cancelled status, positive version, creator/timestamps/memo, create idempotency key+request fingerprint, correction parent/reason, approval actor/time/reason/reference/flags/fingerprint/version, final run ID and persisted response. The partial unique active-month constraint is shared by normal/correction drafts. `settlement_draft_items` owns stable target type/ID, inclusion/reason, selected valuation and selection mode/reason, item version/audit, frozen source evidence/hash and correction-child identity. Monetary authority is derived from eligible frozen evidence, not editable amount columns.
|
||||
|
||||
Modes are `exact`, `carried_before`, and `manual_substitute`. `exact` requires confirmed same-day evidence; `carried_before` requires a verified prior valuation and reason; `manual_substitute` requires explicit valuation ID, reason and review/approval even when valuation date is future. Preserve `date_difference_days = valuation_date - basis_date`; `age_days` retains its historical basis-minus-valuation meaning. Unverified candidates remain visible with `candidate_amount` but authority amount NULL. Missing is distinct from substitution/exclusion. Manual evidence freezes semantic fields and hash; only explicit refresh/reselection changes it.
|
||||
|
||||
Frozen/public evidence retains `valuation_date`, original `recorded_at`, derived `date_difference_direction` (`after`/`before`/`same`/`unknown`) and `age_days`; loan evidence retains `representative_liability_definition`. `recorded_at` may be later than month end while `valuation_date` is exactly month end. These names are preserved through finalized response, reports, timeseries and monthly coverage; date differences and semantic hashes are derived from frozen JSON rather than separate draft-item columns. Actor/version/timestamps of approval are cleared together on draft mutation.
|
||||
|
||||
Review fingerprint binds draft version, target/inclusion state, full valuation/manual semantic evidence, correction child, warnings, amounts/classification/date and prior run. Approved fingerprint/version become invalid on mutation or external drift. Finalize checks idempotency replay before active status, then validates version, approval, fingerprint, current evidence and correction parent, and inserts run+rows+draft completion/link in one `BEGIN IMMEDIATE`. Rollback on any SQLite failure; replay returns persisted response.
|
||||
|
||||
KRW only aggregation; non-KRW is warned/excluded, never converted. NULL is not zero; no targets yields provisional zero plus completeness and unregistered-liability warning, not a complete close. Existing snapshot rows preserve full frozen evidence, approval, date/mode/difference and actors; substitution, carried and approved-missing flags are orthogonal. `monthly_source_snapshot_id` remains a read-model alias, never a persistence field.
|
||||
|
||||
@@ -424,7 +424,7 @@ Writer/bootstrap explicitly calls `init_db()`. `read_db()` uses SQLite `mode=ro`
|
||||
|
||||
## D04 snapshot close output contract
|
||||
|
||||
Every successful snapshot close output has these exact top-level fields (the transport envelope may additionally include `success`): `snapshot_run_id`, `close_status`, `approval`, `correction`, `warnings`, `excluded_items`, `carried_items`, `provenance`, `valuation_date`, `age_days`, `verification_status`, `evidence_kind`, and `monthly_source_snapshot_id`. `monthly_source_snapshot_id` is an alias of `snapshot_run_id`, not a persistence field.
|
||||
Every successful **legacy direct close** output has these exact top-level fields (the transport envelope may additionally include `success`); this field list does not define the additive settlement-draft finalize response: `snapshot_run_id`, `close_status`, `approval`, `correction`, `warnings`, `excluded_items`, `carried_items`, `provenance`, `valuation_date`, `age_days`, `verification_status`, `evidence_kind`, and `monthly_source_snapshot_id`. `monthly_source_snapshot_id` is an alias of `snapshot_run_id`, not a persistence field.
|
||||
|
||||
The top-level `provenance` object has these exact nested `provenance` fields: `valuation_date`, `age_days`, `carried_forward`, `verification_status`, `evidence_kind`, `approval`, `correction`, `warnings`, `excluded_items`, and `carried_items`. `approval` and `correction` are objects or explicit empty values when not applicable; warnings and item collections are arrays. `valuation_date` is the actual selected evidence date, `age_days` is computed from the requested basis date, and verification/evidence status describe selected evidence. Live, closed, timeseries, and monthly read models preserve these fields and alias semantics.
|
||||
|
||||
@@ -433,3 +433,32 @@ The preview input includes basis_date, selection_policy, included/excluded evide
|
||||
Replay uses replay payload equality: an existing idempotency key replays only when the persisted request fingerprint is equal; a changed payload returns `idempotency_conflict`. The replay response is reconstructed from the persisted run and its evidence, with no replay-only reused fields.
|
||||
|
||||
`close_metadata` is a recursive close metadata allowlist: only run, approval, correction, warnings, exclusions, carried evidence, and allowlisted evidence fields (`source_type`, `source_id`, `authority_source`, `valuation_id`, dates/statuses and `state_hash`) cross the boundary. `applied_evidence`, `excluded_evidence`, and `carried_evidence` are redacted recursively; unknown nested keys are dropped, and credentials, tokens, cookies, raw account data, and arbitrary source-note payloads are never returned. This is the recursive close_metadata allowlist and redaction contract.
|
||||
|
||||
## settlement-draft-service — 월정산 초안 API (Issue #15)
|
||||
|
||||
Common application owner: `jarca_ledger_mcp/application/settlement_draft_service.py`; re-export through `jarca_ledger_mcp.service`; service and gateway signatures below include internal `db_path`, which is omitted from MCP tool inputSchema. All other MCP-exposed parameters retain their documented names, defaults, and requiredness. MCP exposes the same operation names, not the same signatures; Dashboard uses existing gateway layering. Draft finalize adds draft-specific response fields while preserving common aliases; it is not a claim that its response has the legacy direct-close shape. Each returns the existing `success`/`error_code`/`error` envelope. Dashboard auth gate precedes DB access; this adds no MCP authentication claim.
|
||||
|
||||
```python
|
||||
create_settlement_draft(month: str, created_by: str, idempotency_key: str, basis_date: str = "", memo: str = "", correction_of_run_id: str = "", correction_reason: str = "", db_path: str | None = None)
|
||||
list_settlement_drafts(month: str = "", db_path: str | None = None)
|
||||
get_settlement_draft(draft_id: str, db_path: str | None = None)
|
||||
update_settlement_draft(draft_id: str, expected_version: int, updated_by: str, memo: str, db_path: str | None = None)
|
||||
refresh_settlement_draft_targets(draft_id: str, expected_version: int, updated_by: str, db_path: str | None = None)
|
||||
set_settlement_draft_item(draft_id: str, source_type: str, source_id: str, expected_version: int, selected_by: str, valuation_id: str = "", selection_mode: str = "exact", selection_reason: str = "", included: bool = True, exclusion_reason: str = "", db_path: str | None = None)
|
||||
review_settlement_draft(draft_id: str, db_path: str | None = None)
|
||||
approve_settlement_draft(draft_id: str, expected_version: int, review_fingerprint: str, approved_by: str, approval_reason: str, approval_reference: str, allow_missing_exclusions: bool = False, allow_carried_before: bool = False, allow_manual_substitute: bool = False, db_path: str | None = None)
|
||||
finalize_settlement_draft(draft_id: str, expected_version: int, review_fingerprint: str, idempotency_key: str, db_path: str | None = None)
|
||||
cancel_settlement_draft(draft_id: str, expected_version: int, cancelled_by: str, reason: str, db_path: str | None = None)
|
||||
```
|
||||
|
||||
Create validates strict YYYY-MM and month-end date, nonempty actor/key and payload replay equality; same key+payload returns same draft, changed payload or active duplicate conflicts. Versions are positive strict integers (bool/fraction/None rejected); inclusion flags actual bool. `review` is read-only and returns draft/version/fingerprint, items and included/excluded/missing/warnings, approval_required, provisional totals, completeness, substitution and unregistered-liability flags. `candidate_amount` may be shown for unverified evidence but authority amount NULL. Approval binds fingerprint+version and explicit flags. Finalize requires approval even for exact complete data; same request replays the persisted response, conflicting key/payload errors, and failed transaction leaves no partial state.
|
||||
|
||||
`read_db()` remains SQLite `mode=ro`: missing DB returns `db_not_initialized`, no-create/no mkdir/no migrate/no commit; initialized read failures return `db_error`. Existing auth cookie/logout, MCP transport/scope, menu, query, shell and browser behavior are unchanged. Existing D04 close aliases and recursive metadata allowlist/redaction remain authoritative.
|
||||
|
||||
### Draft evidence public fields
|
||||
|
||||
`review.included_items`, finalized `items`, asset report `close_metadata.applied_evidence`, timeseries point `applied_evidence`, and monthly `source_coverage.assets.applied_evidence` preserve the same selected-evidence field names: `valuation_date` (actual valuation date), `recorded_at` (original recording timestamp, not the basis date), `date_difference_days = valuation_date - basis_date`, `date_difference_direction` (`after`, `before`, `same`, or `unknown`), and `age_days` (basis-minus-valuation days; negative for a future substitute). A loan additionally preserves `representative_liability_definition`; selection audit includes `selection_mode`, `selection_reason`, `selected_by`, `selected_at`, `item_version`, and draft/fingerprint lineage. These are derived or frozen response fields, not additional database columns.
|
||||
|
||||
Nested draft `approval` retains `allow_missing_exclusions`, `allow_carried_before`, and `allow_manual_substitute` alongside actor/time/reason/reference. The three approval flags and the three result flags (`contains_substitutions`, `contains_carried`, `has_approved_missing`) are independent. Unverified evidence can be visible as `candidate_amount` but cannot become applied monetary authority; only an explicitly approved missing/excluded row may persist with NULL amount. No lineage default keys are injected into legacy persisted evidence arrays. Recursive allowlisting excludes raw source notes, memos, and unknown nested fields.
|
||||
|
||||
Executable field-contract regression: `tests/test_settlement_draft_contract_scenarios.py::test_date_metadata_public_contract_names_survive_all_closed_consumers`. Related clause tests cover representative integer precision, transaction readiness independent of asset close, approved unresolved candidates, and the synthetic September pension/loan dates with stale, rollback, and concurrent idempotent retry.
|
||||
|
||||
@@ -210,3 +210,17 @@ The owner is the canonical decision or write boundary; consumers render or read
|
||||
The Dashboard gateway is `jarca_ledger_mcp/adapters/streamlit/overview_gateway.py` (including `get_asset_snapshot_report` and reconciliation/monthly read delegates). It calls shared application read services after `auth-before-DB`; page rendering is not an alternate policy or persistence boundary. A missing DB has stable unavailable semantics (`db_not_initialized`) and no-create behavior. The missing DB stable unavailable contract is no-create. An initialized DB read failure is `db_error`; initialized read has no write, migration, mkdir, or commit. The initialized DB read failure is db_error and initialized read has no write.
|
||||
|
||||
Reconciliation ownership remains in `reconciliation_service.py`: opening evidence and transaction coverage are required inputs. Missing opening evidence or incomplete coverage produces `INCOMPLETE_CALCULATION`/`reconciliation unavailable` with provenance, never an inferred balance or fake transaction.
|
||||
|
||||
## Issue #15 settlement draft ownership
|
||||
|
||||
| Path | Ownership / boundary |
|
||||
|---|---|
|
||||
| `jarca_ledger_mcp/application/settlement_draft_service.py` | Shared business validation, frozen selection/review/approval/finalize; owns no UI or transport behavior. |
|
||||
| `jarca_ledger_mcp/application/monthly_settlement_service.py` | Read-only monthly aggregate; never draft mutation or transaction readiness write. |
|
||||
| `jarca_ledger_mcp/application/assets_service.py` | Existing close/report authority; draft finalization does not call live-select close as shortcut. |
|
||||
| `jarca_ledger_mcp/infrastructure/sqlite/migrations.py` | Additive schema 12→13 migration; schema initializer and migration constraints agree. |
|
||||
| `jarca_ledger_mcp/adapters/mcp/server.py` | Thin same-name/same-signature MCP tools; no duplicate business logic, no new auth claim. |
|
||||
| `jarca_ledger_mcp/adapters/streamlit/` | Existing Dashboard gateway boundary; auth-before-DB, explicit user actions only. |
|
||||
| `tests/test_monthly_settlement_drafts.py` | Synthetic DB lifecycle, failure injection, replay and no-write regressions. |
|
||||
|
||||
Never document invented `jarca_ledger_mcp.mcp` or `monthly.py` modules. `transaction readiness` is separate from asset completeness and monthly reads remain read-only.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -239,3 +239,21 @@ The fingerprint input is exactly basis date, selection policy, included and excl
|
||||
An idempotent close is replayed only for equal request fingerprints. Replay reconstructs the response from the persisted run/evidence (no replay-only reused payload), while a different payload returns `idempotency_conflict`. Recursive close metadata is allowlisted and redacted before it reaches Dashboard/MCP.
|
||||
|
||||
Dashboard missing-DB reads return stable unavailable `db_not_initialized` without create/migrate/commit. The missing DB stable unavailable contract is no-create. Initialized read failures return `db_error`; successful initialized reads are read-only. The initialized DB read failure is db_error and initialized read has no write. Reconciliation requires opening evidence and coverage through the requested basis; otherwise it reports `INCOMPLETE_CALCULATION`/`reconciliation unavailable` with provenance, no inferred balance, and no fake transaction.
|
||||
|
||||
## settlement-draft-create — 월정산 초안 lifecycle (Issue #15)
|
||||
|
||||
### create → edit → review → approve → finalize
|
||||
|
||||
| 단계 ID | 실행 주체 | 처리 / guard | 상태 경계 | 실패 전이 |
|
||||
|---|---|---|---|---|
|
||||
| `settlement-draft-create` | adapter/service | Strict month/date/actor/key validation; explicit correction prior run/reason; acquire `BEGIN IMMEDIATE`; idempotent equal replay; enforce one active draft/month. | Persist draft and initial target items only; no snapshot run/rows/close. | validation/conflict; rollback |
|
||||
| `settlement-draft-edit` | shared service | Expected version; targets change only through explicit refresh; item include/exclude and valuation selection audited; mutation clears approval. | Increment draft/item versions; retain history/reasons. | stale version or invalid scope, no write |
|
||||
| `settlement-draft-review` | shared service | Read frozen source/valuation evidence, warning and correction-child drift; compute review fingerprint/totals/missing; no auto future selection. | Read-only; unverified candidate_amount with authority amount NULL. | drift/missing warnings; no authority mutation |
|
||||
| `settlement-draft-approve` | authorized caller | Require matching current fingerprint/version and explicit actor/reason/reference/flags. | Persist approval binding and flags. | mismatch/unsafe unknown/rejected evidence rejected |
|
||||
| `settlement-draft-finalize` | shared service/SQLite | Idempotency persisted-response lookup first; then active/version/approval/fingerprint/drift/correction-parent checks; insert run, rows and completed/link within one `BEGIN IMMEDIATE`. | Atomic immutable close and draft completion. | conflict or injected DB failure rolls back all writes |
|
||||
|
||||
Exact requires same-date verified evidence; carried_before uses verified prior with reason; manual_substitute requires explicit ID and reason, with signed `date_difference_days = valuation_date - basis_date`. `age_days` keeps its existing direction/meaning. Corrected valuation children are never followed automatically: explicit reselection then fresh review/approval. Missing and substitution remain orthogonal; manual evidence drift requires explicit refresh. Existing row-only legacy close uses established latest row-group ordering and available run identity; never infer a registry row or fall back to live authority.
|
||||
|
||||
### failure/recovery and read-only guards
|
||||
|
||||
Draft create/update/finalize use transaction-scoped writes; any DB failure rolls back run, rows and completion. Same finalize request returns persisted response, not a replay-only `reused` field; different request conflicts. Monthly summary and review do not mutate; transaction readiness remains independent. `read_db()` missing path is `db_not_initialized`, with no-create/no mkdir/no migrate/no commit. Dashboard gate precedes every protected DB access; existing auth cookie lifecycle and exact menu/query/shell/browser semantics remain unchanged.
|
||||
|
||||
Reference in New Issue
Block a user