# 계좌 평가 이력과 자산 마감 데이터 모델 ## 목적과 범위 계좌 평가 증거, 수동 자산, 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/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 14 comprehensive settlement additions `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`: eight columns `id`, `settlement_run_id`, `cashflow_kind`, `category`, `subcategory`, `amount`, `transaction_count`, `currency`; `cashflow_kind` is income/expense/excluded, `amount` signed INTEGER, count positive, category fields NOT NULL (empty string is explicit), FK to `asset_snapshot_runs.id` with RESTRICT, unique `(settlement_run_id,cashflow_kind,category,subcategory,currency)`, and run lookup index. Rows are grouped details for exactly one run. 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.