Rework Issue 18 / PR 19, source review 76: C11-F01 and D02-F02/D03-F03/D04-F04. Preserve the approved contract and add named display, schema, policy, ownership and signature regressions.
24 KiB
계좌 평가 이력과 자산 마감 데이터 모델
목적과 범위
계좌 평가 증거, 수동 자산, 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이고 loanprincipal은 outstanding principal이다. - 식별자:
id - 생명 주기: insert-only; correction은
supersedes_idchild
필드
| 필드 | 타입 | 필수 여부 | 기본값 | 허용 값·형식 | 단위·정밀도 | 민감정보 |
|---|---|---|---|---|---|---|
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_balanceis 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_dateis 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 fromlast_synced_ator a valuation row.last_synced_atis 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_valuationscandidate. loan cache writes without an explicit liability definition do not append historical evidence, and updating the cache does not rewrite historical rows. account_valuationsis 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
loanaccount has an explicitprincipal_onlyorprincipal_interest_feesrepresentative_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_correctionensures 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_refis 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-declaredreported_principal_kindandreported_principal_labelare physical fields.- only verified leaves are authority; unknown/future/unverified are excluded or warning evidence.
last_balanceis external current comparison, not opening evidence; missing balance_after/opening evidence isINCOMPLETE_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_idis a read-model alias ofsnapshot_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 - loanis 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 pluspython -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 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 |
settlement_run_id |
TEXT | NOT 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. settlement_run_id references asset_snapshot_runs(id) ON DELETE RESTRICT. The unique key is (settlement_run_id, cashflow_kind, category, subcategory, currency). transaction_count > 0 and amount/count are integers; idx_settlement_cashflow_snapshots_run indexes the run.
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: 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.