Files

277 lines
26 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 계좌 평가 이력과 자산 마감 데이터 모델
## 목적과 범위
계좌 평가 증거, 수동 자산, append-only 자산 마감과 기존 ledger entities의 실제 SQLite 정본을 기록한다.
- 포함 범위: `accounts`, `transactions`, `manual_assets`, `account_valuations`, `asset_snapshots`, `asset_snapshot_runs`, `reconciliation_sessions`, `review_decisions`, `import_batches`, `schema_meta`
- 제외 범위: auth cookie/session, external price/FX store, inferred transactions, actual credentials/data
## 정본 엔터티
### accounts — 기존 계좌
- 책임: 계좌 identity/current display balance와 account type/liquidity metadata
- 식별자: `id`
- 생명 주기: 기존 CRUD; valuation history는 별도 append-only
#### 필드
| 필드 | 타입 | 필수 여부 | 기본값 | 허용 값·형식 | 단위·정밀도 | 민감정보 |
|---|---|---|---|---|---|---|
| `id` | TEXT | 필수 | 없음 | generated account id | 식별자 | 아니오 |
| `name` | TEXT | 필수 | 없음 | non-empty | 없음 | 아니오 |
| `type` | TEXT | 필수 | `bank` | bank/card/cash/investment/loan/real_estate/other | enum | 아니오 |
| `currency` | TEXT | 필수 | `KRW` | supported currency | ISO code | 아니오 |
| `last_balance` | INTEGER | 선택 | NULL | exact integer | currency unit | 아니오 |
| `last_balance_basis_date` | TEXT | 선택 | `''` | YYYY-MM-DD or unknown | date of the evidence represented by `last_balance`; empty/unknown is explicit | 아니오 |
| `last_synced_at` | TEXT | 선택 | `''` | ISO date/time | timestamp | 아니오 |
| `statement_status` | TEXT | 필수 | `unknown` | existing status enum | 없음 | 아니오 |
| `liquidity_bucket` | TEXT | 필수 | `other` | cash/deposit/investment/real_estate/other/loan | enum | 아니오 |
| `account_number_masked` | TEXT | 필수 | `''` | masked only | redacted | 민감 가능 |
### account-valuations — 기준일 계좌 평가
- 책임: 날짜가 지정된 평가 evidence와 선택/correction provenance. investment `principal`은 reported principal amount이고 loan `principal`은 outstanding principal이다.
- 식별자: `id`
- 생명 주기: insert-only; correction은 `supersedes_id` child
#### 필드
| 필드 | 타입 | 필수 여부 | 기본값 | 허용 값·형식 | 단위·정밀도 | 민감정보 |
|---|---|---|---|---|---|---|
| `id` | TEXT | 필수 | 없음 | generated valuation id | 식별자 | 아니오 |
| `account_id` | TEXT FK | 필수 | 없음 | accounts.id | 관계 | 아니오 |
| `basis_date` | TEXT | 필수 | 없음 | YYYY-MM-DD | date | 아니오 |
| `amount` | INTEGER | 필수 | 없음 | exact signed integer | currency unit | 아니오 |
| `currency` | TEXT | 필수 | KRW | supported currency | ISO code | 아니오 |
| `valuation_kind` | TEXT | 필수 | balance | balance/cash_balance/total_value/loan/loan_balance | purpose | 아니오 |
| `source_type` | TEXT | 필수 | manual | manual/import/account_write/reconciliation/snapshot | enum | 아니오 |
| `source_ref` | TEXT | 필수 | `''` | source/hash/reference | opaque | 민감 가능 |
| `verification_status` | TEXT | 필수 | unverified | verified/unverified/rejected | enum | 아니오 |
| `recorded_at` | TEXT | 필수 | 없음 | ISO timestamp | timestamp | 아니오 |
| `memo` | TEXT | 필수 | `''` | operator note | 없음 | synthetic only |
| `idempotency_key` | TEXT | 필수 | `''` | caller key | opaque | 아니오 |
| `supersedes_id` | TEXT FK | 선택 | NULL | parent valuation | one child | 아니오 |
| `correction_reason` | TEXT | 필수 | `''` | required for correction | 없음 | 아니오 |
| `as_of_at`/`timezone` | TEXT | 함께 선택 | NULL | timezone-aware ISO + IANA | timestamp/zone | 아니오 |
| `cost_basis`,`net_contributed`,`profit`,`principal`,`interest`,`fee` | INTEGER | 선택 | NULL | exact integer; NULL distinct from 0 | currency unit | 아니오 |
| `reported_principal_kind`,`reported_principal_label`,`return_basis` | TEXT | 선택 | NULL | source-declared | 없음 | 아니오 |
| `return_rate` | REAL | 선택 | NULL | finite number | ratio | 아니오 |
| `representative_liability_definition` | TEXT | loan 필수 | NULL | principal_only/principal_interest_fees | enum | 아니오 |
#### `select_account_valuation` 선택 규칙과 기존 인덱스 매핑
- `select_account_valuation`은 `account_id is mandatory`인 단일 평가 선택 query다. `currency and valuation_kind/purpose are optional filters`이며, `basis_date exact match or <= upper bound` 조건을 지원한다.
- 선택 결과는 `verified leaf valuations only`다. 즉 superseded parent가 아닌 leaf만 권위로 사용하며, `future-dated and unverified valuations are excluded`한다.
- `idx_account_valuations_account_date supports account/date selection`; `idx_account_valuations_status_date supports status/date selection`. `neither index covers account + currency + purpose`이므로 해당 조합의 전체 선택 조건을 기존 인덱스가 대신 보장한다고 해석하지 않는다.
- `idempotency and single_correction indexes enforce write integrity, not selection`이다. 이 인덱스들은 재기록 방지와 correction 단일 child 불변조건을 위한 것이며 `select_account_valuation`의 후보 검색 인덱스가 아니다.
### manual-assets — 수동 자산/부채
- 책임: transaction이 아닌 평가 source
- 식별자: `id`
- 생명 주기: update/deactivate; closed snapshot은 별도 immutable copy
#### 필드
| 필드 | 타입 | 필수 여부 | 기본값 | 허용 값·형식 | 단위·정밀도 | 민감정보 |
|---|---|---|---|---|---|---|
| `id` | TEXT | 필수 | 없음 | generated asset id | 식별자 | 아니오 |
| `name` | TEXT | 필수 | 없음 | non-empty | 없음 | 아니오 |
| `liquidity_bucket` | TEXT | 필수 | other | normalized bucket | enum | 아니오 |
| `valuation_amount` | INTEGER | 선택 | NULL | exact integer; NULL unvalued | currency unit | 아니오 |
| `valuation_date` | TEXT | 필수 | `''` | YYYY-MM-DD | date | 아니오 |
| `currency` | TEXT | 필수 | KRW | supported currency | ISO code | 아니오 |
| `source_note`,`memo` | TEXT | 필수 | `''` | synthetic/redacted note | 없음 | 민감 가능 |
| `active` | INTEGER | 필수 | 1 | 0/1 | boolean | 아니오 |
### asset-snapshots — closed snapshot rows
- 책임: basis-date historical immutable item copy
- 식별자: `id`
- 생명 주기: insert-only; correction run creates new rows
#### 필드
| 필드 | 타입 | 필수 여부 | 기본값 | 허용 값·형식 | 단위·정밀도 | 민감정보 |
|---|---|---|---|---|---|---|
| `id`,`basis_date`,`source_type`,`name`,`created_at` | TEXT | 필수 | 없음 | generated/date/account/manual_asset | identity/date | 아니오 |
| `source_id`,`currency`,`liquidity_bucket`,`valuation_source`,`source_note`,`memo` | TEXT | 필수 | `''`/KRW/other | source metadata | 없음 | 가능 |
| `amount` | INTEGER | 선택 | NULL | exact integer | currency unit | 아니오 |
| `snapshot_run_id` | TEXT | 필수 | `''` | asset_snapshot_runs.id | relation | 아니오 |
| `correction_of_run_id`,`correction_reason` | TEXT | 필수 | `''` | prior run/reason | provenance | 아니오 |
| `is_correction` | INTEGER | 필수 | 0 | 0/1 | boolean | 아니오 |
| `closed_at` | TEXT | 필수 | `''` | ISO timestamp | timestamp | 아니오 |
| `valuation_id`,`selection_policy`,`verification_status`,`evidence_kind`,`exclusion_reason` | TEXT | additive | empty/exact/unverified/applied/empty | provenance enum | 없음 | 아니오 |
### asset-snapshot-runs — close 실행 증거
- 책임: approval, selection, applied/excluded/carried evidence와 run identity
- 식별자: `id`
- 생명 주기: insert-only; correction points to prior run
#### 필드
| 필드 | 타입 | 필수 여부 | 기본값 | 허용 값·형식 | 단위·정밀도 | 민감정보 |
|---|---|---|---|---|---|---|
| `id`,`basis_date`,`close_status` | TEXT | 필수 | 없음 | complete_exact/incomplete_draft/conditional | identity/date/status | 아니오 |
| `selection_policy` | TEXT | 필수 | exact | exact/as_of | enum | 아니오 |
| `idempotency_key` | TEXT | 필수 | `''` | unique when non-empty | opaque | 아니오 |
| `approved_by`,`approved_at`,`approval_reason`,`approval_reference` | TEXT | 필수 | empty | explicit approval metadata | provenance | 아니오 |
| `correction_of_run_id`,`correction_reason` | TEXT | 필수 | empty | prior run/reason | provenance | 아니오 |
| `applied_evidence`,`excluded_evidence`,`carried_evidence` | TEXT JSON | 필수 | `[]` | redacted evidence arrays | JSON | 가능 |
| `created_at` | TEXT | 필수 | 없음 | ISO timestamp | timestamp | 아니오 |
### transactions, import-batches, reconciliation-sessions, review-decisions, schema-meta — 기존 entities
기존 필드와 관계를 보존한다. import는 거래를 기록할 수 있지만 balance valuation candidate는 dated `unverified` row만 추가한다. loan cache writes without an explicit liability definition do not append an `account_valuations` candidate; current cache와 transaction import는 그대로 완료하며 정의를 추정하지 않는다. reconciliation은 calculated balance provenance와 `VALUATION_MANAGED` 상태를 보존하며 investment valuation 때문에 transaction을 생성하지 않는다. `schema_meta.schema_version`은 12다.
### 현재 계좌 잔액 캐시와 기준일
- `last_balance` is the current cache value: it is the latest current balance observed by an import or account write, not a historical valuation and not opening evidence.
- `last_balance_basis_date` is the basis field for the evidence represented by that cache. It is a date when known; legacy or date-less writes store unknown/NULL and must not be inferred from `last_synced_at` or a valuation row.
- `last_synced_at` is the execution/sync timestamp: when the import or account write advanced the cache, not the evidence basis date.
- imports and account writes advance the account cache; non-loan writes may also append a dated `account_valuations` candidate. loan cache writes without an explicit liability definition do not append historical evidence, and updating the cache does not rewrite historical rows.
- `account_valuations` is the historical authority. A dated valuation remains append-only evidence even when it is older than the current cache; older evidence never rolls back the current cache.
- Every valuation attached to a `loan` account has an explicit `principal_only` or `principal_interest_fees` `representative_liability_definition`. A non-loan to loan conversion with any undefined or invalid attached valuation is rejected without changing the account or history; conversion with no valuations is allowed.
## 관계와 불변조건
### 관계
| 출발 엔터티 | 관계 | 대상 엔터티 | 카디널리티 | 필수 여부 | 소유권 | 삭제 규칙 |
|---|---|---|---|---|---|---|
| `accounts` | has valuations | `account-valuations` | 1:N | required | account service | no cascade/history preserved |
| `account-valuations` | supersedes | `account-valuations` | 0..1 child | optional | valuation service | append-only |
| `asset-snapshot-runs` | contains | `asset-snapshots` | 1:N/possibly zero | required | snapshot service | no delete |
| `account-valuations` | evidence for | `asset-snapshots` | 0..N | optional | snapshot run | copy only |
| `import-batches` | produces candidate | `account-valuations` | 0..N | optional | import service | candidate preserved |
| `accounts` | has reconciliation | `reconciliation-sessions` | 1:N | optional | reconciliation service | no valuation side effect |
### 불변조건
- `idx_account_valuations_single_correction` ensures one direct child; correction parent scope is same account/date/currency/purpose. Cyclic or self parent, second child, missing reason, and conflict are no-write failures.
- `source_ref` is account-scoped evidence identity even when an explicit key is supplied; key/source mismatch conflicts. This is evidence idempotency. There is no generic label; only source-declared `reported_principal_kind` and `reported_principal_label` are physical fields.
- only verified leaves are authority; unknown/future/unverified are excluded or warning evidence. `last_balance` is external current comparison, not opening evidence; missing balance_after/opening evidence is `INCOMPLETE_CALCULATION`.
- preview does not insert; close inserts run and rows atomically; latest run is report authority. Reports expose actual valuation_date, age, carry, verification, run, approval, correction, warnings, and exclusions across close response, live, closed, timeseries, and monthly.
- `monthly_source_snapshot_id` is a read-model alias of `snapshot_run_id`, not a persistence field. Investment unavailable and unavailable investment, loan purpose/definition/ambiguity, and reconciliation completeness remain explicit; no fake writes.
- `asset_buckets - loan` is the net-worth formula; representative loan only, no fake transactions.
## 저장과 마이그레이션
- 저장 위치: resolved SQLite path from `jarca_ledger_mcp.infrastructure.sqlite.connection`
- 직렬화 형식: SQLite tables; evidence arrays JSON UTF-8
- 스키마 버전: schema 11 → schema 12 additive
- 기존 데이터 처리: `migrate_db()` creates missing tables/columns/indexes; this is an additive migration with no guessed migration or deletion. legacy rows with unknown basis remain unknown; no inference is permitted.
- 하위 호환성: existing ledger/auth/menu/MCP contracts and legacy rows remain readable
- 롤백 조건: pre-migration backup restore boundary; do not down-migrate or delete valuation history in application code
## 검증
- 정상 데이터: `tests/test_account_valuations.py`, `tests/test_asset_snapshots.py`
- 경계 데이터: NULL/0, stale carry-forward, correction, empty snapshot tables
- 오류 데이터: 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.
# Issue #18 — schema 15 comprehensive settlement additions
### asset_snapshot_runs cashflow field contract
All twelve comprehensive-run columns are nullable with no SQL default. This preserves NULL on historical asset-only runs; a new comprehensive write supplies all twelve fields. SQLite reports `id` (TEXT PRIMARY KEY) nullable because DDL omits explicit NOT NULL, although a run logically requires its identifier.
| Column | Type | Nullability | Default |
|---|---|---|---|
| `settlement_month` | TEXT | NULL | none |
| `currency` | TEXT | NULL | none |
| `income_total` | INTEGER | NULL | none |
| `expense_total` | INTEGER | NULL | none |
| `net_cashflow` | INTEGER | NULL | none |
| `total_assets` | INTEGER | NULL | none |
| `total_liabilities` | INTEGER | NULL | none |
| `net_worth` | INTEGER | NULL | none |
| `cashflow_policy_json` | TEXT | NULL | none |
| `aggregation_version` | TEXT | NULL | none |
| `snapshot_schema_version` | INTEGER | NULL | none |
| `source_snapshot_json` | TEXT | NULL | none |
### settlement_drafts cashflow field contract
| Column | Type | Nullability | Default |
|---|---|---|---|
| `cashflow_policy_json` | TEXT | NOT NULL | `'{}'` |
| `aggregation_version` | TEXT | NOT NULL | `'comprehensive-v1'` |
### settlement_cashflow_snapshots cashflow field contract
| Column | Type | Nullability | Default |
|---|---|---|---|
| `id` | TEXT | NULL | none |
| `monthly_settlement_id` | TEXT | NULL | none |
| `settlement_run_id` | TEXT | NULL | none |
| `cashflow_kind` | TEXT | NOT NULL | none |
| `category` | TEXT | NOT NULL | `''` |
| `subcategory` | TEXT | NOT NULL | `''` |
| `amount` | INTEGER | NOT NULL | none |
| `transaction_count` | INTEGER | NOT NULL | none |
| `currency` | TEXT | NOT NULL | none |
`id` is logically required although SQLite reports no explicit NOT NULL flag for this TEXT primary key. Both parent columns are individually nullable: `monthly_settlement_id` references `monthly_settlement(id)` ON DELETE RESTRICT, and `settlement_run_id` references `asset_snapshot_runs(id)` ON DELETE RESTRICT. An exclusive-parent CHECK requires exactly one parent to be non-NULL. Separate UNIQUE constraints apply to `(monthly_settlement_id,cashflow_kind,category,subcategory,currency)` and `(settlement_run_id,cashflow_kind,category,subcategory,currency)`. `transaction_count > 0` and amount/count are integers. Fresh schema uses `idx_settlement_cashflow_snapshots_month` for canonical-month lookup; upgraded run-parent storage retains `idx_settlement_cashflow_snapshots_run` for legacy-run lookup.
### Comprehensive snapshot evidence inputs
`source_snapshot_json` stores an object with `transactions` and `assets` arrays. Transaction inputs preserve `id`, `transaction_date`, `type`, original signed `amount`, `currency`, `category`, `subcategory`, `originalcashflow_kind`, applied `cashflow_kind`, `applied_amount`, `decision`, `sourceprovenance`, `category_cashflow_kind` and `category_active`. Refunds keep their original positive amount while the expense contribution in `applied_amount` is negative; unresolved classifications keep `applied_amount=NULL` and block approval. The actual stored alias is `originalcashflow_kind`, not a renamed persistence field.
Asset inputs preserve `source_id`, `source_type`, `valuation_id`, `valuation_date`, `basis_date`, `currency`, `authority_source`, `liquidity_bucket`, `is_liability`, `selection_mode`, `exclusion_reason`, `status`, `verification_status`, original `amount` and `applied_amount`. Missing/excluded evidence retains NULL applied amounts and its recorded status/reason; fixed included rows remain linked to the same run. The run header's `cashflow_policy_json`, `aggregation_version`, `snapshot_schema_version` and these source inputs together reproduce the original selection/aggregation decision without rereading today's ledger. A legacy asset-only run retains NULL comprehensive fields and is reported as legacy: no historical backfill or invented zero. The existing approval, draft, correction and idempotency links remain unchanged.
`asset_snapshot_runs` remains the run header. Schema 13→14 adds settlement month, currency, six nullable legacy-compatible INTEGER totals (`income_total`, `expense_total`, `net_cashflow`, `total_assets`, `total_liabilities`, `net_worth`), `cashflow_policy_json`, `aggregation_version`, `snapshot_schema_version`, and `source_snapshot_json`; preserve existing basis/approval/draft/correction/idempotency fields. Old asset-only run totals remain NULL; no current-ledger backfill. Migration is additive/idempotent; old unfinished drafts require fresh comprehensive review/approval.
`settlement_cashflow_snapshots`: nine columns `id`, `monthly_settlement_id`, `settlement_run_id`, `cashflow_kind`, `category`, `subcategory`, `amount`, `transaction_count`, `currency`; `cashflow_kind` is income/expense/excluded, `amount` signed INTEGER, count positive, and category fields are NOT NULL (empty string is explicit). The nullable parent columns reference `monthly_settlement.id` and `asset_snapshot_runs.id` with RESTRICT, while the exclusive-parent CHECK requires exactly one parent. The table has separate UNIQUE constraints on `(monthly_settlement_id,cashflow_kind,category,subcategory,currency)` and `(settlement_run_id,cashflow_kind,category,subcategory,currency)`, so rows remain grouped under exactly one canonical month or legacy run without merging the two parent domains.
Policy is stored on draft and header. Savings means only explicit category/subcategory pairs; transfer/voucher exclusion has priority; debt_payment is expense; refund is negative expense in refund month/original category. Unknown unresolved blocks approval; asset conditional flags cannot override it. KRW only/no FX. Source JSON freezes transaction values/classification/policy/decision and selected asset evidence/provenance. Six equations are validated against same-run details; excluded/duplicate/unknown amount do not sum. Header, both detail areas, evidence, completed draft linkage/response commit atomically; rollback all on failure. Runs are immutable; corrections append after latest-parent/single-child validation.
### Additive schema 15 monthly lifecycle
`monthly_settlement` owns canonical month status, version, approvals and nullable
`approved_cashflow_fingerprint`. `account_snapshot.monthly_settlement_id` is a
nullable FK for canonical account evaluations. Legacy `asset_snapshots`,
`asset_snapshot_runs`, `manual_assets`, and `account_valuations` retain their
rows and contracts. `settlement_cashflow_snapshots` has nullable
`settlement_run_id` and `monthly_settlement_id` foreign keys, with exactly one
parent required and separate uniqueness constraints. Run corrections retain
separate frozen groups; migration never guesses a monthly parent from dates.
Schema 11–14 upgrades and schema 15 initialization are atomic and idempotent;
failed copies or foreign-key checks roll back all DDL and metadata.
### Immutable monthly settlement history read model
`asset_snapshot_runs` is the header source for append-only immutable settlement history.
Each public history row identifies `run_id`, `settlement_month`, frozen draft
`version`, `closed_at`, `close_status`, and optional correction lineage through
`correction_of_run_id` (published as `correction_target`) plus
`correction_reason` (published as `reason`). The exact-record read additionally
returns the frozen basis date and persisted cashflow/net-worth totals. Reads are
ordered newest-first and never synthesize totals from current transactions,
account values, or mutable draft state. A correction appends a new run and
retains the referenced run unchanged.