feat: implement asset snapshot and manual asset read model
This commit is contained in:
+8
-2
@@ -949,7 +949,7 @@ def page_overview(db_path: str) -> None:
|
||||
c4.metric("저축률", format_percent(cashflow.get("savings_rate")))
|
||||
|
||||
render_section_label("🏦", "자산 유동성 현황")
|
||||
st.caption("기준: accounts.liquidity_bucket + accounts.last_balance. 거래 합계로 비거래 자산을 추정하지 않습니다.")
|
||||
st.caption("기준: service 공통 평가 헬퍼(accounts.last_balance + manual_assets 보강). 거래 합계로 비거래 자산을 추정하지 않습니다.")
|
||||
a1, a2, a3 = st.columns(3)
|
||||
a1.metric("자산 합계(부채 제외)", format_krw(asset_report.get("total_assets", 0)))
|
||||
a2.metric("총부채", format_krw(asset_report.get("total_liabilities", 0)))
|
||||
@@ -966,13 +966,19 @@ def page_overview(db_path: str) -> None:
|
||||
|
||||
render_section_label("🧭", "자산 구성")
|
||||
source_accounts = pd.DataFrame(asset_report.get("accounts", []))
|
||||
manual_assets = pd.DataFrame(asset_report.get("manual_assets", []))
|
||||
if not source_accounts.empty:
|
||||
source_accounts["bucket_label"] = source_accounts["liquidity_bucket"].map(LIQUIDITY_BUCKET_LABELS).fillna(source_accounts["liquidity_bucket"])
|
||||
source_accounts["last_balance_display"] = source_accounts["last_balance"].map(format_krw)
|
||||
show_cols = ["name", "type", "bucket_label", "last_balance_display", "last_synced_at", "memo"]
|
||||
st.dataframe(source_accounts[show_cols], width="stretch", height=260)
|
||||
st.dataframe(source_accounts[show_cols], width="stretch", height=220)
|
||||
else:
|
||||
st.info("last_balance가 입력된 활성 계좌/자산 항목이 없습니다.")
|
||||
if not manual_assets.empty:
|
||||
manual_assets["bucket_label"] = manual_assets["liquidity_bucket"].map(LIQUIDITY_BUCKET_LABELS).fillna(manual_assets["liquidity_bucket"])
|
||||
manual_assets["valuation_display"] = manual_assets["amount"].map(format_krw)
|
||||
st.caption("수기 자산/부채(manual_assets) — 비거래 자산 권위 소스")
|
||||
st.dataframe(manual_assets[["name", "bucket_label", "valuation_display", "valuation_date", "source_note", "memo"]], width="stretch", height=180)
|
||||
|
||||
render_section_label("📉", "지출 구조/예산")
|
||||
budgets = pd.DataFrame(budget_report.get("items", []))
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# BAT-008 Design
|
||||
|
||||
## Schema and persistence design
|
||||
|
||||
- Add `manual_assets` as a first-class table for non-transaction-derived assets/liabilities.
|
||||
- Add `asset_snapshots` as an append-only history table keyed by basis date plus evaluated item identity.
|
||||
- Migrations remain additive/idempotent; existing ledger/account flows must continue to operate when both tables are empty.
|
||||
|
||||
## Authority-source and valuation design
|
||||
|
||||
- Account-backed assets use `accounts.last_balance` as the live authority source.
|
||||
- Non-transaction assets/liabilities use `manual_assets` as the live authority source.
|
||||
- `asset_snapshots` is a historical record layer, not a competing primary input source.
|
||||
- Net worth formula remains: asset buckets (`cash`, `investment`, `deposit`, `real_estate`, `other`) minus `loan`; missing valuations are excluded, not inferred.
|
||||
|
||||
## Read model design
|
||||
|
||||
- Service layer should expose:
|
||||
- manual assets list/report helpers
|
||||
- basis-date asset snapshot read helpers
|
||||
- net-worth timeseries helpers
|
||||
- shared valuation/fallback helpers consumed by both MCP and dashboard
|
||||
- Snapshot read responses must expose basis date, bucket totals, liabilities, net worth, and inclusion/exclusion explanation fields.
|
||||
- Manual assets read responses must expose item name, value, basis date, `liquidity_bucket`, and memo/source metadata.
|
||||
|
||||
## Dashboard alignment design
|
||||
|
||||
- `dashboard/app.py` should consume the same valuation helpers as the MCP read tools.
|
||||
- If no manual assets/snapshots exist, dashboard behavior must safely fall back to REQ-005 account-based display.
|
||||
- Excluded/unvalued items should be explainable instead of silently counted.
|
||||
|
||||
## Verification design
|
||||
|
||||
- Add targeted regression tests for:
|
||||
- additive migration creation
|
||||
- manual asset persistence and retrieval
|
||||
- snapshot accumulation/read behavior
|
||||
- net-worth formula / exclusion policy
|
||||
- dashboard/MCP shared fallback behavior
|
||||
- Prefer synthetic SQLite fixtures and temp DBs only.
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
# BAT-008 Implementation
|
||||
|
||||
## Plan summary
|
||||
|
||||
BAT-008 implemented the new valuation authority model in `service.py`/`db.py`, exposed read-only MCP tools in `server.py`, aligned the Streamlit overview with the shared valuation helper, and updated baseline docs to match the confirmed Discovery/REQ scope.
|
||||
|
||||
## Changed files
|
||||
|
||||
- `jarca_ledger_mcp/db.py`
|
||||
- `SCHEMA_VERSION` → `9`
|
||||
- additive/idempotent tables: `manual_assets`, `asset_snapshots`
|
||||
- migration path updated so existing DBs create the new tables safely
|
||||
- `jarca_ledger_mcp/service.py`
|
||||
- shared valuation helpers for account/manual-asset/snapshot authority sources
|
||||
- new service APIs: `create_manual_asset`, `list_manual_assets`, `create_asset_snapshot`, `get_asset_snapshot_report`, `get_net_worth_timeseries`
|
||||
- `get_asset_liquidity_report` aligned to the same valuation policy while preserving REQ-005 fallback semantics
|
||||
- `jarca_ledger_mcp/server.py`
|
||||
- new read MCP tools: `list_manual_assets`, `get_asset_snapshot_report`, `get_net_worth_timeseries`
|
||||
- `get_asset_liquidity_report` contract text updated for shared valuation source behavior
|
||||
- `dashboard/app.py`
|
||||
- overview asset section now explains the valuation authority source and shows `manual_assets` rows when present
|
||||
- existing account-only fallback remains when new tables are empty
|
||||
- Documentation
|
||||
- `docs/data-model.md`
|
||||
- `docs/interface-contract.md`
|
||||
- `docs/runtime-flows.md`
|
||||
- `docs/project-structure.md`
|
||||
- Tests
|
||||
- new: `tests/test_asset_snapshots.py`
|
||||
- adjusted: `tests/test_service.py`
|
||||
|
||||
## Execution log
|
||||
|
||||
- 2026-06-30 21:26 KST: BAT-008 standard batch scaffold created from approved REQ-009/REQ-010.
|
||||
- 2026-06-30 21:3x~21:4x KST: implementation landed in `db.py`, `service.py`, `server.py`, `dashboard/app.py`, docs, and regression tests via `jarcaledgerdev` handoff + local verification orchestration.
|
||||
- 2026-06-30 21:40 KST: targeted compile/test validation passed.
|
||||
- 2026-06-30 21:41 KST: MCP stdio contract smoke check passed and discovered the three new read tools.
|
||||
- 2026-06-30 21:42 KST: synthetic end-to-end service probe confirmed live-authority fallback, snapshot read behavior, and net-worth timeseries output.
|
||||
|
||||
## Validation
|
||||
|
||||
### Command 1
|
||||
```bash
|
||||
cd /root/projects/jarca-ledger-mcp
|
||||
source .venv/bin/activate
|
||||
python -m py_compile jarca_ledger_mcp/*.py dashboard/app.py
|
||||
pytest tests/test_asset_snapshots.py tests/test_service.py tests/test_contract_regression.py -q
|
||||
```
|
||||
|
||||
Result:
|
||||
- `................. [100%]`
|
||||
- exit code `0`
|
||||
|
||||
### Command 2
|
||||
```bash
|
||||
cd /root/projects/jarca-ledger-mcp
|
||||
source .venv/bin/activate
|
||||
hermes mcp test jarca-ledger
|
||||
```
|
||||
|
||||
Result:
|
||||
- stdio connection success
|
||||
- tools discovered: `51`
|
||||
- new BAT-008 read tools discovered:
|
||||
- `list_manual_assets`
|
||||
- `get_asset_snapshot_report`
|
||||
- `get_net_worth_timeseries`
|
||||
|
||||
### Command 3
|
||||
```bash
|
||||
cd /root/projects/jarca-ledger-mcp
|
||||
source .venv/bin/activate
|
||||
python - <<'PY'
|
||||
from tempfile import TemporaryDirectory
|
||||
from pathlib import Path
|
||||
from jarca_ledger_mcp import service
|
||||
|
||||
with TemporaryDirectory() as td:
|
||||
db_path = str(Path(td) / 'ledger.sqlite3')
|
||||
service.create_ledger_account('현금', last_balance=1000000, last_synced_at='2026-06-30', liquidity_bucket='cash', db_path=db_path)
|
||||
service.create_ledger_account('투자', type='investment', last_balance=2000000, liquidity_bucket='investment', db_path=db_path)
|
||||
service.create_manual_asset('전세보증금', 300000000, '2026-06-30', 'real_estate', source_note='synthetic appraisal', db_path=db_path)
|
||||
service.create_manual_asset('주담대', 120000000, '2026-06-30', 'loan', source_note='synthetic loan balance', db_path=db_path)
|
||||
print(service.list_manual_assets(db_path=db_path))
|
||||
print(service.get_asset_snapshot_report('2026-06-30', db_path=db_path))
|
||||
service.create_asset_snapshot('2026-05-31', 'account', 'acct-a', '현금', 1000000, 'cash', source_note='synthetic', db_path=db_path)
|
||||
service.create_asset_snapshot('2026-05-31', 'manual_asset', 'manual-home', '부동산', 200000000, 'real_estate', source_note='synthetic', db_path=db_path)
|
||||
service.create_asset_snapshot('2026-05-31', 'manual_asset', 'manual-loan', '대출', 80000000, 'loan', source_note='synthetic', db_path=db_path)
|
||||
print(service.get_asset_snapshot_report('2026-05-31', db_path=db_path))
|
||||
print(service.get_net_worth_timeseries(start_date='2026-05-01', end_date='2026-06-30', db_path=db_path))
|
||||
PY
|
||||
```
|
||||
|
||||
Key result excerpts:
|
||||
- `list_manual_assets` returned `count=2` and `authority_source='manual_assets'`
|
||||
- live fallback snapshot report returned `source='live_authority'`, `total_assets=303000000`, `total_liabilities=120000000`, `net_worth=183000000`
|
||||
- stored snapshot report returned `source='asset_snapshots'`, `snapshot_count=3`, `net_worth=121000000`
|
||||
- timeseries returned `points[0].basis_date='2026-05-31'`, `points[0].net_worth=121000000`
|
||||
|
||||
## Remaining risks / follow-up notes
|
||||
|
||||
- Snapshot creation is intentionally service-only in BAT-008. MCP mutation exposure remains future scope by Discovery decision.
|
||||
- Manual-asset edit/delete workflows and month-end operator UX are still procedural/document-driven rather than exposed as first-class CRUD flows.
|
||||
- QC gate still needs to confirm REQ Implemented promotion before commit-stage handoff.
|
||||
@@ -0,0 +1,36 @@
|
||||
# BAT-008 Asset snapshots / manual assets / read MCP
|
||||
|
||||
- Profile: standard
|
||||
- Status: release-candidate
|
||||
- Included REQ: REQ-009, REQ-010
|
||||
- Discovery: dcy-008
|
||||
- Owner: 렛저장인
|
||||
- Date (KST): 2026-06-30
|
||||
|
||||
## 목표
|
||||
|
||||
REQ-009/REQ-010에 따라 `asset_snapshots`/`manual_assets` 저장 모델을 additive/idempotent하게 도입하고, 월말 수동 스냅샷 정책을 수용하는 집계 계층과 조회성 MCP(기준일 스냅샷/순자산 시계열/수기 자산 조회)를 구현한다. 동시에 대시보드 자산/순자산 표시가 동일 권위 소스와 동일 산식을 사용하도록 정렬한다.
|
||||
|
||||
## 산출물
|
||||
|
||||
- `asset_snapshots`, `manual_assets` additive/idempotent schema migration
|
||||
- 수기 자산 저장/조회, 스냅샷 누적, 순자산 시계열 집계 service 함수
|
||||
- 조회성 MCP tool/응답 계약: 기준일 자산 스냅샷, 순자산 시계열, 수기 자산 조회
|
||||
- `dashboard/app.py`의 자산/순자산 표시가 신규 모델과 안전 폴백을 함께 지원하도록 정렬
|
||||
- `docs/data-model.md`, `docs/interface-contract.md`, `docs/project-structure.md`, `docs/runtime-flows.md` baseline 갱신
|
||||
- BAT-008 planning/design/implementation/verification 문서와 synthetic-only 검증 evidence
|
||||
|
||||
## 범위 제한
|
||||
|
||||
- 외부 시세/부동산 자동 평가 연동
|
||||
- 스냅샷 생성 mutation MCP
|
||||
- 수기 자산 생성/수정/삭제 mutation MCP
|
||||
- 투자 조언/리밸런싱/세금/수익률 계산
|
||||
- 운영 DB 실데이터 기반 evidence
|
||||
|
||||
## 문서
|
||||
|
||||
- `planning.md`: delivery plan, dependency, risk, validation entry criteria
|
||||
- `design.md`: schema/authority-source/fallback/read-contract design
|
||||
- `implementation.md`: changed files, execution log, validation commands
|
||||
- `verification.md`: AC mapping, PASS/FAIL, REQ Implemented decision
|
||||
@@ -0,0 +1,37 @@
|
||||
# BAT-008 Planning
|
||||
|
||||
## Scope
|
||||
|
||||
- Implement REQ-009: `asset_snapshots`/`manual_assets` valuation model, authority-source rule, month-end manual snapshot policy support.
|
||||
- Implement REQ-010: asset snapshot read MCP, net-worth timeseries read MCP, manual assets read MCP, dashboard/MCP alignment.
|
||||
- Preserve REQ-005 safety fallback when new tables are empty.
|
||||
- Use synthetic/temp data only for tests and verification evidence.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- dcy-008 confirmed Discovery and approved REQ-009/REQ-010.
|
||||
- Existing REQ-005 dashboard liquidity/net-worth flow and `accounts.last_balance` authority-source baseline.
|
||||
- Existing MCP server/service pattern in `jarca_ledger_mcp/server.py`, `jarca_ledger_mcp/service.py`, `jarca_ledger_mcp/db.py`.
|
||||
- Additive/idempotent SQLite migration discipline and synthetic-only evidence policy.
|
||||
|
||||
## Work plan
|
||||
|
||||
1. Create BAT-008 standard batch docs and register entry.
|
||||
2. Add schema/model support for `manual_assets` and `asset_snapshots` with safe empty-table fallback.
|
||||
3. Add service-layer valuation helpers, snapshot accumulation/report functions, and dashboard alignment logic.
|
||||
4. Add read-only MCP tool endpoints/contracts for snapshot read, net-worth timeseries, and manual assets read.
|
||||
5. Update baseline docs (`docs/data-model.md`, `docs/interface-contract.md`, `docs/project-structure.md`, `docs/runtime-flows.md`).
|
||||
6. Run narrow validation first (targeted tests / lint / smoke as applicable), then broader regression if narrow gates pass.
|
||||
7. If evidence satisfies ACs, move BAT-008 to release-candidate and REQ-009/REQ-010 to Implemented.
|
||||
|
||||
## Design Gate
|
||||
|
||||
- Result: standard batch 유지.
|
||||
- Reason: schema, service, MCP contract, dashboard alignment, baseline docs를 함께 변경하며 interface/data/fallback 설계가 필요하다.
|
||||
|
||||
## Risks and mitigations
|
||||
|
||||
- Authority-source drift between dashboard and MCP: mitigate by centralizing valuation helpers and writing shared regression tests.
|
||||
- Empty new tables breaking existing REQ-005 flows: mitigate by explicit fallback tests and additive migrations only.
|
||||
- Double counting between account-backed assets and manual assets: mitigate by clearly separating source responsibility and documenting inclusion/exclusion rules.
|
||||
- Snapshot history ambiguity: mitigate by storing basis date, source type, and memo for every snapshot row.
|
||||
@@ -0,0 +1,64 @@
|
||||
# BAT-008 Verification
|
||||
|
||||
## Verification summary
|
||||
|
||||
Status: PASS
|
||||
|
||||
Evidence collected against synthetic/temp data only. No production ledger DB mutation or real financial files were used.
|
||||
|
||||
## Acceptance criteria mapping
|
||||
|
||||
### REQ-009
|
||||
|
||||
- AC-1: **PASS**
|
||||
- Evidence: `tests/test_asset_snapshots.py::test_asset_snapshot_schema_is_additive_and_idempotent`
|
||||
- `db.py` adds `manual_assets` and `asset_snapshots` via additive/idempotent schema + migration path.
|
||||
- AC-2: **PASS**
|
||||
- Evidence: `create_manual_asset`/`list_manual_assets` service path + synthetic probe output includes `name`, `valuation_amount`, `valuation_date`, `liquidity_bucket`, `source_note`.
|
||||
- AC-3: **PASS**
|
||||
- Evidence: `create_asset_snapshot` stores basis date/source target/amount/bucket/source note; `test_snapshot_rows_are_append_only_and_timeseries_uses_same_formula` proves append-only readback.
|
||||
- AC-4: **PASS**
|
||||
- Evidence: `valuation_policy` and `authority_source` fields distinguish `accounts.last_balance`, `manual_assets`, and `asset_snapshots`; docs updated in `docs/data-model.md` and `docs/runtime-flows.md`.
|
||||
- AC-5: **PASS**
|
||||
- Evidence: valuation helper formula is `asset_buckets - loan`; synthetic probe returned `303000000 - 120000000 = 183000000`; unvalued items are excluded rather than inferred.
|
||||
- AC-6: **PASS**
|
||||
- Evidence: `tests/test_asset_snapshots.py::test_asset_liquidity_report_empty_new_tables_preserves_req005_fallback` confirms empty new tables preserve REQ-005 path with `source='accounts.last_balance'`.
|
||||
- AC-7: **PASS**
|
||||
- Evidence: `docs/data-model.md`, `docs/runtime-flows.md`, and `docs/project-structure.md` now document the new tables, authority-source rule, and month-end snapshot flow.
|
||||
|
||||
### REQ-010
|
||||
|
||||
- AC-1: **PASS**
|
||||
- Evidence: `server.py` exposes `get_asset_snapshot_report`; synthetic probe returned `basis_date`, per-bucket totals, liabilities, net worth, included/excluded items.
|
||||
- AC-2: **PASS**
|
||||
- Evidence: `server.py` exposes `get_net_worth_timeseries`; service/test coverage proves identical formula per basis date.
|
||||
- AC-3: **PASS**
|
||||
- Evidence: `server.py` exposes `list_manual_assets`; synthetic probe returned name/value/date/bucket/source note payloads.
|
||||
- AC-4: **PASS**
|
||||
- Evidence: `dashboard/app.py` caption explicitly states shared valuation helper; `service.py` centralizes calculation for dashboard + MCP.
|
||||
- AC-5: **PASS**
|
||||
- Evidence: same fallback regression test as REQ-009 AC-6 plus `get_asset_liquidity_report` preserves existing keys while adding optional explanatory fields.
|
||||
- AC-6: **PASS**
|
||||
- Evidence: `docs/interface-contract.md`, `docs/runtime-flows.md`, and `docs/project-structure.md` updated for read MCP contract and dashboard/MCP alignment.
|
||||
|
||||
## Requirement state decision
|
||||
|
||||
- REQ-009: Implemented promotion confirmed.
|
||||
- REQ-010: Implemented promotion confirmed.
|
||||
|
||||
## Validation evidence
|
||||
|
||||
1. `python -m py_compile jarca_ledger_mcp/*.py dashboard/app.py` → pass
|
||||
2. `pytest tests/test_asset_snapshots.py tests/test_service.py tests/test_contract_regression.py -q` → pass (`................. [100%]`)
|
||||
3. `hermes mcp test jarca-ledger` → pass; stdio connected, 51 tools discovered, including the three new BAT-008 read tools
|
||||
4. synthetic service probe → pass; confirmed live-authority fallback, stored-snapshot read path, and net-worth timeseries output
|
||||
|
||||
## Remaining issues
|
||||
|
||||
### Hard blockers
|
||||
- None identified from the collected implementation evidence.
|
||||
|
||||
### Non-blocking follow-up
|
||||
- jarcaledgerqc final re-check returned: `PASS, REQ-009/REQ-010은 이제 Implemented로 전환해도 됩니다.`
|
||||
- fresh Streamlit browser smoke on `http://127.0.0.1:8511` rendered `schema v9`, `자산 유동성 현황`, and produced no browser console JS errors.
|
||||
- Snapshot creation/manual-asset CRUD MCP mutations remain intentionally out of scope for BAT-008.
|
||||
@@ -15,3 +15,4 @@
|
||||
| BAT-005 | standard | release-candidate | REQ-005 | dcy-005 | [bat-005_20260629_dashboard-operations-hub-liquidity-monthly-cashflow](bat-005_20260629_dashboard-operations-hub-liquidity-monthly-cashflow/) |
|
||||
| BAT-006 | standard | release-candidate | REQ-006 | dcy-006 | [bat-006_20260630_pytest-apptest-smoke-ci-automation](bat-006_20260630_pytest-apptest-smoke-ci-automation/) |
|
||||
| BAT-007 | standard | release-candidate | REQ-007, REQ-008 | dcy-007 | [bat-007_20260630_mcp-response-contract-ci-versioning](bat-007_20260630_mcp-response-contract-ci-versioning/) |
|
||||
| BAT-008 | standard | release-candidate | REQ-009, REQ-010 | dcy-008 | [bat-008_20260630_asset-snapshots-manual-assets-read-mcp](bat-008_20260630_asset-snapshots-manual-assets-read-mcp/) |
|
||||
|
||||
+49
-5
@@ -4,8 +4,8 @@
|
||||
- Baseline Seed: `.stage-pilot/bootstrap/baseline.yaml`
|
||||
- Status: draft
|
||||
- Owner: Jarca / project maintainer
|
||||
- Last Updated (KST): 2026-06-29
|
||||
- Source Discovery / Batch: bootstrap-baseline
|
||||
- Last Updated (KST): 2026-06-30
|
||||
- Source Discovery / Batch: bootstrap-baseline, BAT-008
|
||||
|
||||
## Project Summary
|
||||
- Jarca 가계부 전용 MCP 서버와 Streamlit 대시보드로 계좌/카드 거래, 예산, 분류 규칙, import, 현금흐름 분석을 SQLite에 저장하고 운영한다.
|
||||
@@ -40,9 +40,51 @@
|
||||
- `active=false` hides accounts from default lists without deleting history.
|
||||
- `manual_only` is the statement-unavailable class; `partial_import` remains eligible for manual supplementation and balance reconciliation.
|
||||
- Balance reconciliation is report/preview first: compare `baseline_balance`/`baseline_at` against the latest `balance_after` or net account transaction delta, with 1,000 KRW absolute tolerance.
|
||||
- 비거래 자산/부채는 별도 평가 테이블 없이 `last_balance`를 현재 평가액/잔액, `last_synced_at`을 기준일, `memo`를 출처 메모로 사용한다.
|
||||
- 계좌 기반 자산/부채는 `last_balance`를 현재 평가 권위 소스로 사용하고, `last_synced_at`을 기준일, `memo`를 출처 메모로 보존한다.
|
||||
- 거래로 산출되지 않는 비거래 자산/부채는 `ManualAsset`에 별도 저장한다. 계좌와 수기 자산을 같은 항목으로 중복 등록하지 않는다.
|
||||
- `liquidity_bucket='loan'`은 자산 구성 비율 분모에서 제외하고 총부채/순자산 산식에만 반영한다. `last_balance`가 없는 계좌/자산은 임의 거래 합계로 추정하지 않는다.
|
||||
|
||||
## Entity: ManualAsset
|
||||
- Source of Truth: `manual_assets`
|
||||
- Purpose: Stores non-transaction-derived assets/liabilities such as real estate, deposits, loans, and other manually valued holdings that should not be mixed into account rows.
|
||||
- Lifecycle: created -> revalued/updated -> inactive/archived
|
||||
- Key Fields:
|
||||
- `id` :: text :: stable manual asset identifier
|
||||
- `name` :: text :: operator-facing asset/liability name
|
||||
- `liquidity_bucket` :: text :: `cash`, `investment`, `deposit`, `real_estate`, `loan`, `other`
|
||||
- `valuation_amount` :: integer nullable :: latest manually entered valuation/balance; null means excluded from valuation totals
|
||||
- `valuation_date` :: date text :: valuation basis date
|
||||
- `currency` :: text :: default `KRW`
|
||||
- `source_note` :: text :: valuation evidence/source memo
|
||||
- `memo` :: text :: operator note
|
||||
- `active` :: integer/bool :: inactive rows are excluded from default read reports
|
||||
- Relationships:
|
||||
- Referenced by `asset_snapshots.source_type='manual_asset'` and `source_id` when a month-end valuation is snapshotted.
|
||||
- State Rules:
|
||||
- `manual_assets` is the live authority source for non-transaction assets/liabilities.
|
||||
- Missing `valuation_amount` rows are excluded; transaction sums are not used as a substitute.
|
||||
- `loan` bucket values are treated as liabilities and subtracted from asset-bucket totals.
|
||||
|
||||
## Entity: AssetSnapshot
|
||||
- Source of Truth: `asset_snapshots`
|
||||
- Purpose: Append-only item-level record of a basis-date asset/liability valuation produced from the live authority sources.
|
||||
- Lifecycle: appended at month-end/manual close -> read for point-in-time reports and timeseries
|
||||
- Key Fields:
|
||||
- `id` :: text :: snapshot row identifier
|
||||
- `basis_date` :: date text :: valuation date represented by the row
|
||||
- `source_type` :: text :: `account` or `manual_asset`
|
||||
- `source_id` :: text :: source entity identifier when available
|
||||
- `name` :: text :: snapshot display name copied from the source at capture time
|
||||
- `amount` :: integer nullable :: snapshotted valuation amount
|
||||
- `liquidity_bucket` :: text :: bucket copied from the source at capture time
|
||||
- `valuation_source`, `source_note`, `memo` :: text :: evidence and operator context
|
||||
- `created_at` :: text :: snapshot row creation timestamp
|
||||
- Relationships:
|
||||
- Represents a historical record of `LedgerAccount.last_balance` or `ManualAsset.valuation_amount` rather than a new primary input source.
|
||||
- State Rules:
|
||||
- Snapshot rows are append-only for BAT-008. Existing rows are not destructively overwritten.
|
||||
- Basis-date snapshot reports prefer stored `asset_snapshots` rows when rows exist for the date; otherwise read reports can fall back to live authority sources.
|
||||
|
||||
## Entity: Transaction
|
||||
- Source of Truth: `transactions`
|
||||
- Purpose: Represents imported or manually entered financial event evidence.
|
||||
@@ -148,7 +190,8 @@
|
||||
- External counterparty securities transfers are review items unless matched or explicitly confirmed.
|
||||
- Import evidence fields are retained for audit and future correction.
|
||||
- DB corrections should be narrow and memo-tagged.
|
||||
- Dashboard asset liquidity/net-worth reports use `accounts.liquidity_bucket` and `accounts.last_balance` only; transactions are not used to infer non-transaction asset values.
|
||||
- Dashboard/MCP asset and net-worth reports use a shared valuation helper: account-backed items from `accounts.last_balance` plus non-transaction items from `manual_assets`.
|
||||
- Net worth is asset buckets (`cash`, `investment`, `deposit`, `real_estate`, `other`) minus absolute `loan` liabilities; missing valuations are excluded and reported, never inferred from transactions.
|
||||
|
||||
## Persistence Notes
|
||||
- Schema changes should be additive/idempotent unless a destructive migration is explicitly approved and backed up.
|
||||
@@ -161,10 +204,11 @@
|
||||
- Formal schema migration documentation should be added before larger DB changes.
|
||||
- Some UI workflows need final E2E verification against real rows.
|
||||
- Statement-unavailable account handling is tracked by REQ-003/BAT-003; future work may add a dedicated reconciliation workbench and persistent review-decision queue.
|
||||
- REQ-005/BAT-005 adds dashboard operations hub semantics and account-level liquidity buckets; future work may add separate asset snapshots only after a new REQ.
|
||||
- BAT-008 adds `manual_assets` and `asset_snapshots` with read-only MCP/report support. Future work may add mutation MCP for snapshot creation/manual asset CRUD and automatic market/real-estate valuation.
|
||||
|
||||
## Change Log
|
||||
- 2026-06-26 22:24 KST: Created by `bootstrap-baseline` from repository observation.
|
||||
- 2026-06-27 10:17 KST: BAT-002/REQ-002 기준으로 LedgerBackupArtifact와 metadata-only evidence 정책 추가.
|
||||
- 2026-06-29 KST: REQ-003/BAT-003 기준으로 `accounts.statement_status`와 수동 잔액대조 preview 모델 추가.
|
||||
- 2026-06-29 KST: REQ-005/BAT-005 기준으로 `accounts.liquidity_bucket`, `last_balance` 기반 자산 유동성/부채/순자산 규칙 추가.
|
||||
- 2026-06-30 KST: REQ-009/REQ-010/BAT-008 기준으로 `manual_assets`, `asset_snapshots`, 권위 소스(`accounts.last_balance` + `manual_assets`), 순자산 산식/미평가 제외 정책 추가.
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
# 0. 문서 상태
|
||||
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
|
||||
|
||||
- 상태: confirmed
|
||||
- 문서 ID: dcy-008_20260630_asset-snapshot-manual-valuation-system
|
||||
- 이슈명: 자산 스냅샷/수기 자산 테이블/평가 체계 정립
|
||||
- 작성 시각(KST): 2026-06-30 20:38 KST
|
||||
- 마지막 갱신 시각(KST): 2026-06-30 21:17 KST
|
||||
- 대체됨: 없음
|
||||
- 후속 Discovery 참조: 없음
|
||||
- 생성된 REQ 참조: `REQ-009`, `REQ-010`
|
||||
|
||||
# 1. 계획 상태 요약
|
||||
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
|
||||
|
||||
- 연계 문서 현황:
|
||||
- 선행 Discovery: dcy-005(`docs/discovery/dcy-005_20260629_dashboard-operations-hub-liquidity-monthly-cashflow.md`) — 자산 유동성/순자산 표시를 확정했지만 별도 자산 스냅샷·수기 자산 테이블·평가 자동화는 Out of Scope로 남김
|
||||
- 관련 TODO/메모: `docs/data-model.md` Current Gaps와 `docs/runtime-flows.md` Current Gaps가 "별도 자산 스냅샷/수기 자산 테이블·자동 시세/부동산 평가"를 후속 REQ로 명시 보류
|
||||
- 해석:
|
||||
- 현재 자산/순자산은 `accounts.liquidity_bucket`과 가변 단일값 `accounts.last_balance`로만 산출돼, 시점별 평가 이력이 남지 않고 거래로 산출되지 않는 비거래 자산(부동산·예적금·대출·기타)의 평가 기준·갱신 책임이 계좌 한 줄에 묶여 있어 평가 체계가 부재하다.
|
||||
- 이번 Discovery는 dcy-005가 명시적으로 후속으로 미룬 "자산 스냅샷 이력 + 수기 자산 테이블 + 평가 체계"를 독립 주기로 정의하고, `asset_snapshots`/`manual_assets` 별도 테이블 도입, 계좌 `last_balance` + 수기 자산 보강 기반 권위 소스, 월말 수동 스냅샷 정책, 조회성 MCP 포함 범위를 확정한다.
|
||||
|
||||
# 2. 요구사항 판정 결과
|
||||
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
|
||||
|
||||
- 판정: 신규
|
||||
- 근거
|
||||
- 가장 근접한 dcy-005는 대시보드 운영 허브와 `accounts.liquidity_bucket`/`last_balance` 기반 유동성·순자산 "표시"를 confirmed 범위로 다루며, 별도 자산 스냅샷 테이블·수기 자산 테이블·평가 자동화를 Out of Scope로 명시 제외했다. 이번 요청은 그 제외된 평가 이력·자산 테이블·평가 체계를 신규로 정의하므로 dcy-005의 미세 조정이 아니다.
|
||||
- dcy-005는 confirmed 상태로 BAT-005(release-candidate)까지 진행돼 변경 전제가 고정돼 있어, 평가 이력·신규 엔티티 도입을 같은 문서에 누적하면 책임 경계가 흐려진다. 후속 반복을 독립 주기로 추적하는 편이 명확하다.
|
||||
|
||||
# 3. 문제점의 요약
|
||||
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. Discovery의 핵심 맥락을 가장 먼저 이해할 수 있어야 한다.
|
||||
|
||||
- 현재 상태
|
||||
- `dashboard/app.py`의 `get_asset_liquidity_report`는 활성 계좌를 `accounts.liquidity_bucket`으로 묶고 자산/부채 값으로 `accounts.last_balance`만 사용한다(`docs/runtime-flows.md` 51~53행). 즉 자산 평가값이 계좌별 단일 가변 컬럼에만 존재한다.
|
||||
- `docs/data-model.md`의 LedgerAccount는 `last_balance`(최신 잔액), `last_synced_at`(기준 스냅샷 시각), `liquidity_bucket`만 갖고 있고, 시점별 자산 평가 이력을 보존하는 별도 엔티티가 없다. 비거래 자산/부채는 "별도 평가 테이블 없이 `last_balance`/`last_synced_at`/`memo`로 처리"하도록 규정돼 있다(data-model 43행).
|
||||
- `docs/data-model.md` Current Gaps(164행)와 `docs/runtime-flows.md`(131행)는 "별도 자산 스냅샷/수기 자산 테이블과 자동 시세·부동산 평가는 REQ-005 이후로 보류, 신규 REQ 필요"라고 명시한다. 즉 이번 주제는 기존 baseline이 의도적으로 비워 둔 갭이다.
|
||||
- `liquidity_bucket`/`last_balance` 관련 구현은 `jarca_ledger_mcp/service.py`, `jarca_ledger_mcp/db.py`, `jarca_ledger_mcp/server.py`, `dashboard/app.py`에 존재하나, 스냅샷 이력 테이블·수기 자산 엔티티·평가 규칙 모듈은 존재하지 않는다.
|
||||
- `docs/project-structure.md`, `docs/runtime-flows.md` baseline 문서는 존재하나 자산 스냅샷 이력·수기 자산 테이블·평가 체계 데이터 흐름을 아직 반영하지 않는다.
|
||||
- 기대 상태
|
||||
- 자산 평가값이 시점별 스냅샷으로 보존돼 "특정 기준일 순자산"과 순자산 시계열(추이)을 재현·조회할 수 있다.
|
||||
- 거래로 산출되지 않는 비거래 자산(부동산·예적금·대출·기타)을 수기 자산 항목으로 명시 등록·갱신하고, 평가 출처·기준일·평가 방법이 일관된 평가 체계로 기록된다.
|
||||
- 자산 평가 규칙(자산성 항목 합계 − 부채(`loan`) 차감, 미입력 자산 제외 표기 등)이 단일 정의로 문서·집계·화면에서 동일하게 적용된다.
|
||||
- 이해관계자
|
||||
- 가계 재무를 운영하며 자산/순자산 추이를 관리하는 사용자(대시보드 1차 사용자)
|
||||
- 문서/요구사항 리뷰어 및 승인자(Hermes/사용자)
|
||||
- 데이터 모델·서비스·DB·대시보드를 구현/운영하는 개발 담당자
|
||||
- 제약사항
|
||||
- DB 스키마 변경은 additive/idempotent 원칙을 따르며 파괴적 마이그레이션은 명시 승인·백업 후에만 한다(`docs/data-model.md` Persistence Notes).
|
||||
- 기존 dcy-005/REQ-005 confirmed 범위(`accounts.liquidity_bucket`/`last_balance` 기반 표시)와 충돌하지 않고, 평가 권위 소스 이중화로 비율·순자산이 어긋나지 않아야 한다.
|
||||
- 본 Discovery 단계에서는 소스 코드/테스트/스키마를 변경하지 않고 사양만 정의한다.
|
||||
|
||||
## 주요 사용자 시나리오
|
||||
사용자 또는 운영자가 실제로 어떤 상황에서 이 변경을 필요로 하는지 대표 시나리오를 적는다.
|
||||
|
||||
- 사용자가 월말에 부동산·예적금·투자·대출 잔액을 수기 자산 항목으로 입력/갱신하면, 해당 시점의 자산 평가 스냅샷이 기준일과 함께 보존된다.
|
||||
- 사용자가 대시보드에서 "이번 달 말 순자산"과 "최근 N개월 순자산 추이"를 조회해 자산이 늘었는지 줄었는지 시점 비교로 확인한다.
|
||||
- 사용자가 특정 자산(예: 전세보증금, 차량)의 평가값을 갱신할 때, 과거 평가값을 덮어쓰지 않고 새 스냅샷으로 누적해 평가 이력을 추적한다.
|
||||
|
||||
# 4. 이번에 정의할 변경
|
||||
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다.
|
||||
|
||||
- 자산 평가 스냅샷 이력 모델 정의: 시점별 자산 평가값을 보존하는 스냅샷 엔티티(`asset_snapshots` 권장)와 기준일·평가 출처·스냅샷 단위(계좌/자산 항목)·생성 정책(월말 자동 vs 수동)을 사양으로 정의한다.
|
||||
- 수기 자산 테이블 정의: 거래로 산출되지 않는 비거래 자산/부채(부동산·예적금·대출·기타)를 계좌와 분리해 명시 등록하는 수기 자산 엔티티(`manual_assets` 권장)와 평가값·기준일·`liquidity_bucket`·출처 메모 필드를 정의한다.
|
||||
- 평가 체계 규칙 정의: 자산성 항목 합계에서 부채(`loan`)를 차감하는 순자산 산식, 평가 권위 소스(계좌 `last_balance` vs 수기 자산 vs 스냅샷)의 단일화/우선순위, 미입력·미평가 자산의 제외·표기 규칙을 단일 정의로 확정한다.
|
||||
- 조회성 MCP 범위 정의: 자산 스냅샷/순자산 시계열/수기 자산을 대시보드 내부 모델에만 가두지 않고, 동일한 평가 권위 소스와 순자산 산식을 사용하는 조회성 MCP 응답 계약까지 범위에 포함한다. 초기 범위는 조회(read) 중심으로 한정하고 스냅샷 생성·수기 자산 CRUD 같은 mutation MCP는 후속 범위로 둔다.
|
||||
- baseline 문서 반영: 위 자산 스냅샷·수기 자산·평가 체계 데이터 모델과 평가/스냅샷 데이터 흐름을 `docs/data-model.md`, `docs/project-structure.md`, `docs/runtime-flows.md` 갱신 요구로 등록한다.
|
||||
|
||||
## 영향 범위
|
||||
변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다.
|
||||
|
||||
- 코드 영향:
|
||||
- `jarca_ledger_mcp/db.py` 스키마(스냅샷·수기 자산 테이블 additive 추가), `jarca_ledger_mcp/service.py` 자산 평가/스냅샷 집계·순자산 시계열 함수, `jarca_ledger_mcp/server.py`의 조회성 MCP 응답 계약(기준일 스냅샷 조회, 순자산 시계열 조회, 수기 자산 조회).
|
||||
- `dashboard/app.py`의 `get_asset_liquidity_report`/순자산 표시부가 단일 권위 소스(스냅샷/수기 자산)를 참조하도록 조정.
|
||||
- 문서 영향:
|
||||
- `docs/data-model.md`(`asset_snapshots`/`manual_assets` 엔티티, 순자산 산식·평가 권위 소스 규칙), `docs/project-structure.md`, `docs/runtime-flows.md`(스냅샷 생성·순자산 시계열 데이터 흐름) 갱신.
|
||||
- dcy-005/REQ-005와의 평가 권위 소스 정합성 명시.
|
||||
- 운영 영향:
|
||||
- 비거래 자산 평가 입력/갱신과 월말 스냅샷 생성 절차가 신설돼 운영 입력 루틴·잔액대조 흐름에 영향을 준다.
|
||||
|
||||
# 5. 요구사항 목록
|
||||
실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다.
|
||||
|
||||
## 기능 요구사항
|
||||
- FR-1: 시점별 자산 평가값을 보존하는 자산 스냅샷 이력을 도입한다. 각 스냅샷은 최소한 기준일, 평가 대상(계좌 또는 수기 자산 항목), 평가 금액, `liquidity_bucket`, 평가 출처/메모를 보존하며, 과거 값은 덮어쓰지 않고 누적한다.
|
||||
- FR-2: 거래로 산출되지 않는 비거래 자산/부채를 계좌와 분리해 등록하는 수기 자산 테이블을 도입한다. 각 항목은 자산명, 평가 금액, 기준일, `liquidity_bucket`(`cash`/`investment`/`deposit`/`real_estate`/`loan`/`other`), 출처 메모를 갖는다.
|
||||
- FR-3: 순자산 평가 규칙을 단일 정의로 확정한다. 자산성 항목(`cash`/`investment`/`deposit`/`real_estate`/`other`) 평가값 합계에서 `loan` 부채를 차감하고, 평가값이 없는 항목은 임의 거래 합계로 추정하지 않고 제외하며 화면에 제외 사실을 표기한다.
|
||||
- FR-4: 대시보드/리포트와 조회성 MCP가 자산·순자산을 산출할 때 평가 권위 소스를 단일화한다. 계좌 `last_balance`, 수기 자산, 스냅샷이 동시에 존재할 때의 우선순위와 폴백을 명시하고, dcy-005/REQ-005의 `accounts.liquidity_bucket` 기반 표시와 충돌하지 않게 정합성 규칙을 정의한다.
|
||||
- FR-5: 조회성 MCP 범위에 기준일 자산 스냅샷 조회, 순자산 시계열 조회, 수기 자산 조회를 포함한다. 각 응답은 최소한 기준일/집계 구간, 평가 권위 소스, `liquidity_bucket`, 순자산 산식 적용 기준, 포함·제외 항목을 설명할 수 있어야 한다.
|
||||
- FR-6: `docs/data-model.md`, `docs/project-structure.md`, `docs/runtime-flows.md` baseline 문서에 `asset_snapshots`/`manual_assets` 모델과 스냅샷 생성·순자산 시계열·조회성 MCP 데이터 흐름 갱신을 포함한다.
|
||||
|
||||
## 비기능 요구사항
|
||||
성능, 운영성, 일관성, 자동화 안정성 등 기능 외 요구사항을 적는다.
|
||||
|
||||
- NFR-1 (일관성): 순자산·자산 구성 계산은 단일 평가 권위 소스와 단일 산식(자산 합계 − `loan` 차감)을 모든 섹션·집계·MCP 응답에서 동일하게 적용한다.
|
||||
- NFR-2 (이력 보존성): 자산 평가값 갱신은 기존 스냅샷을 파괴적으로 덮어쓰지 않고 신규 스냅샷으로 누적해 기준일별 재현 가능성을 보장한다.
|
||||
- NFR-3 (안정성): `asset_snapshots`/`manual_assets` 스키마 변경은 additive/idempotent로 적용하고, 신규 테이블이 비어 있어도 기존 대시보드·`get_asset_liquidity_report` 동작을 깨지 않는다.
|
||||
|
||||
## 범위 경계
|
||||
이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다.
|
||||
|
||||
- In Scope
|
||||
- 자산 스냅샷 이력 엔티티, 수기 자산 테이블 엔티티, 순자산 평가 규칙·평가 권위 소스 단일화 사양 정의 및 baseline 문서 갱신 요구.
|
||||
- 기준일 자산 스냅샷 조회, 순자산 시계열 조회, 수기 자산 조회를 포함한 조회성 MCP 범위 정의.
|
||||
- dcy-005/REQ-005 표시 범위와의 평가 정합성 규칙 정의.
|
||||
- Out of Scope
|
||||
- 외부 시세/부동산 평가 자동 연동, 실시간 잔액 연동.
|
||||
- 자산 평가 알림/리밸런싱 추천, 세금·수익률 자동 계산.
|
||||
- 스냅샷 생성, 수기 자산 생성/수정/삭제 같은 mutation MCP 범위.
|
||||
- 본 단계에서의 실제 스키마/코드/테스트 변경(사양 정의만 수행).
|
||||
|
||||
# 6. 리스크/가정 목록
|
||||
불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다.
|
||||
|
||||
## 리스크
|
||||
- R-1 (High): 자산 평가 권위 소스가 계좌 `last_balance`, 수기 자산, 스냅샷으로 삼중화되면 동일 자산이 중복 집계되거나 순자산이 어긋날 수 있다.
|
||||
- 완화 방안: FR-4에서 단일 권위 소스와 우선순위/폴백을 먼저 확정하고, dcy-005/REQ-005 표시 경로와 조회성 MCP 응답이 모두 같은 규칙을 따르도록 REQ 초안에 명시한다.
|
||||
- R-2 (Medium): 신규 테이블(`asset_snapshots`/`manual_assets`) 도입이 기존 `get_asset_liquidity_report`/대시보드 집계와 동작 충돌을 일으킬 수 있다.
|
||||
- 완화 방안: additive/idempotent 스키마와 빈 테이블 안전 폴백을 NFR-3로 고정하고, 단계적 전환(표시는 기존 유지 → 권위 소스 전환) 경로를 설계 단계에서 분리한다.
|
||||
- R-3 (Medium): 수기 자산 평가는 사용자 입력 의존도가 높아 입력 누락/기준일 불일치 시 순자산 추이가 왜곡된다.
|
||||
- 완화 방안: 기준일·출처 메모를 필수 필드로 두고, 미평가/오래된 평가 항목을 화면 경고로 노출하는 규칙을 요구사항에 포함한다.
|
||||
|
||||
## 가정
|
||||
아직 확정되지 않았지만 현재 문서 작성의 기반이 되는 전제를 적는다.
|
||||
|
||||
- A-1: `liquidity_bucket` 분류 축과 그 허용값(`cash`/`investment`/`deposit`/`real_estate`/`loan`/`other`)은 dcy-005/REQ-005 정의를 그대로 재사용한다.
|
||||
- A-2: 순자산 산식의 기본 골격(자산성 합계 − `loan` 차감, 미평가 제외)은 dcy-005에서 확정된 정의를 계승해 스냅샷/수기 자산으로 확장한다.
|
||||
- A-3: 비거래 자산 평가값은 초기에는 사용자 수기 입력을 권위 소스로 하며 외부 자동 연동은 후속 범위로 둔다.
|
||||
|
||||
## 오픈 질문
|
||||
답을 아직 모르는 항목을 적는다. 추정으로 덮지 말고 REQ 초안 작성 이전에 확인할 질문으로 남긴다.
|
||||
|
||||
- OQ-1: 자산 평가 권위 소스를 어떻게 단일화할지(① 계좌 `last_balance` 유지 + 수기 자산 보강, ② 모든 자산을 수기 자산/스냅샷으로 일원화) 결정한다.
|
||||
- 상태: Answered
|
||||
- 답: 계좌 `last_balance`를 기존 권위 소스로 유지하되, 거래로 산출되지 않는 비거래 자산/부채는 `manual_assets`로 보강한다. 스냅샷은 이 둘을 기준일별로 고정하는 기록층으로 사용한다.
|
||||
- 반영 위치: FR-4, In Scope, `docs/data-model.md`/조회성 MCP 응답 규칙
|
||||
- OQ-2: 자산 스냅샷 생성 정책(월말 자동 생성 vs 사용자 수동 생성 vs 둘 다)과 스냅샷 단위(계좌 단위 vs 자산 항목 단위)를 결정한다.
|
||||
- 상태: Answered
|
||||
- 답: 초기 운영 정책은 월말 수동 생성으로 확정한다. 스냅샷 구조는 계좌와 수기 자산 항목을 모두 담을 수 있는 항목 단위 집계를 수용하되, 운영 입력은 월말 마감 기준으로 단순화한다.
|
||||
- 반영 위치: FR-1, 운영 영향, runtime-flows 후속 갱신
|
||||
|
||||
- OQ-3: 조회성 MCP 범위를 어디까지 포함할지(① 기준일 자산 스냅샷 조회 + 순자산 시계열 조회 + 수기 자산 조회 포함, ② 일부만 포함) 결정한다.
|
||||
- 상태: Answered
|
||||
- 답: 기준일 자산 스냅샷 조회, 순자산 시계열 조회, 수기 자산 조회를 `dcy-008`의 조회성 MCP 범위에 포함한다. 스냅샷 생성과 수기 자산 CRUD 같은 mutation MCP는 후속 범위로 분리한다.
|
||||
- 반영 위치: FR-5, In Scope/Out of Scope, `docs/interface-contract.md`/`docs/runtime-flows.md` 후속 갱신
|
||||
|
||||
### 오픈 질문 처리 기준
|
||||
- `Open`: 아직 답이 없고 다음 단계 진행에 영향이 있는 상태다. 문서에 유지한다.
|
||||
- `Answered`: 답이 확정된 상태다. 관련 본문/요구사항에 반영한 뒤 오픈 질문 목록에서는 제거하거나 resolved 기록으로 이동한다.
|
||||
- `Deferred`: 지금 단계에서 결정하지 않고 다음 단계 또는 후속 이슈로 넘기는 상태다. 넘길 대상과 이유를 반드시 적는다.
|
||||
- `Dropped`: 더 이상 유효하지 않은 질문이다. 삭제 사유를 짧게 남기고 목록에서 제거한다.
|
||||
- 처리 원칙 1: 질문만 남기지 말고 반드시 `상태`, `처리 방안`, `종료 조건`을 함께 적는다.
|
||||
- 처리 원칙 2: 답이 나온 질문은 본문 반영 없이 단순 삭제하지 않는다. 반영 위치를 먼저 갱신한다.
|
||||
- 처리 원칙 3: High 영향 질문은 `Deferred` 처리 후 처리 방안에 REQ 인계 추적 방식을 명시한다.
|
||||
|
||||
# 7. 초기 성공 기준
|
||||
이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다. 가능하면 데이터 출처까지 연결한다.
|
||||
|
||||
- S-1: 기준일별 자산 평가 스냅샷이 보존돼 "특정 기준일 순자산"과 순자산 시계열을 재현할 수 있는 데이터 모델이 사양으로 정의된다.
|
||||
- S-2: 비거래 자산/부채를 수기 자산 테이블로 등록·갱신하고, 자산성 합계 − `loan` 차감 순자산 산식이 단일 평가 권위 소스 기준으로 일관되게 정의된다.
|
||||
- S-3: `asset_snapshots`/`manual_assets` 데이터 모델과 스냅샷 생성·순자산 시계열·조회성 MCP 데이터 흐름이 `docs/data-model.md`, `docs/project-structure.md`, `docs/runtime-flows.md`에 반영되고, dcy-005/REQ-005 표시 경로와의 정합성이 문서로 확인된다.
|
||||
|
||||
## 측정 방식 및 데이터 출처
|
||||
각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다.
|
||||
|
||||
- S-1 측정 방식: 표본 자산/기준일로 스냅샷 누적·기준일 순자산 재현을 손검증과 비교 / 데이터 출처: `asset_snapshots` 모델 사양, `accounts.last_balance`/`last_synced_at`
|
||||
- S-2 측정 방식: 표본 수기 자산·부채로 순자산 산식 결과를 손검증과 비교 / 데이터 출처: `manual_assets` 모델 사양, `liquidity_bucket` 분류 규칙, dcy-005 순자산 정의
|
||||
- S-3 측정 방식: baseline 문서 diff에서 `asset_snapshots`/`manual_assets`·스냅샷/시계열/조회성 MCP 흐름·dcy-005 정합성 반영 여부 확인 / 데이터 출처: `docs/data-model.md`, `docs/project-structure.md`, `docs/runtime-flows.md`
|
||||
|
||||
# 8. REQ로 넘기기 전 확인 체크
|
||||
REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다.
|
||||
|
||||
- [x] 범위(In Scope/Out of Scope) 확정
|
||||
- 기준: `asset_snapshots`/`manual_assets` 별도 테이블, 조회성 MCP 포함 범위, 외부 시세 연동 제외를 이번 confirm에서 확정했다.
|
||||
- [x] 성공 지표의 측정 방식/데이터 출처 확정
|
||||
- 기준: S-1~S-3 검수 방식과 데이터 출처가 REQ 초안으로 내릴 수 있을 정도로 정의됐다.
|
||||
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
|
||||
- 기준: R-1의 정책 답은 이번 confirm에서 확정했고, 구현/검수 책임자 지정은 REQ·batch planning 단계에서 배정한다.
|
||||
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
|
||||
- 기준: OQ-1~OQ-3을 모두 Answered 처리했다.
|
||||
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
|
||||
- 기준: 사용자가 권위 소스 단일화·스냅샷 정책·조회성 MCP 포함 권장안을 승인하고 `dcy-008 confirm`을 지시했다.
|
||||
|
||||
# 9. 파일 처리 결과
|
||||
이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다. 이후 추적과 리뷰 시 근거로 사용한다.
|
||||
|
||||
- 처리 결과: 생성
|
||||
- 생성 경로: docs/discovery/dcy-008_20260630_asset-snapshot-manual-valuation-system.md
|
||||
- 참조 문서:
|
||||
- docs/discovery/dcy-005_20260629_dashboard-operations-hub-liquidity-monthly-cashflow.md (자산 유동성·순자산 표시 범위 및 본 Discovery의 후속 보류 출처)
|
||||
- docs/data-model.md (LedgerAccount `last_balance`/`liquidity_bucket`, Current Gaps의 자산 스냅샷 보류 근거; `asset_snapshots`/`manual_assets` 도입 대상)
|
||||
- docs/runtime-flows.md, docs/project-structure.md, docs/interface-contract.md (스냅샷 생성·순자산 시계열·조회성 MCP 흐름/응답 계약 갱신 대상; stage-pilot/templates/runtime-flows.md, stage-pilot/templates/project-structure.md를 논리 템플릿 참조로 사용)
|
||||
- dashboard/app.py `get_asset_liquidity_report`, jarca_ledger_mcp/service.py, jarca_ledger_mcp/db.py (현재 자산/순자산 집계 구현 근거)
|
||||
|
||||
# 10. 사용자 결정 필요 항목 요약
|
||||
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다.
|
||||
|
||||
## DECIDE
|
||||
- DECIDE_AUTHORITY_SOURCE: ① 계좌 `last_balance` 유지 + 수기 자산 보강으로 확정.
|
||||
- DECIDE_NEW_TABLES: `asset_snapshots`, `manual_assets` 별도 테이블 도입으로 확정.
|
||||
|
||||
## CONFIRM
|
||||
- CONFIRM_SNAPSHOT_POLICY: 월말 수동 생성으로 확정. 스냅샷 구조는 계좌와 수기 자산 항목을 모두 담을 수 있는 항목 단위를 수용한다.
|
||||
- CONFIRM_NETWORTH_FORMULA: dcy-005 정의(자산성 합계 − `loan` 차감, 미평가 제외)를 그대로 계승해 스냅샷/수기 자산으로 확장하는 방향으로 확정.
|
||||
- CONFIRM_MCP_SNAPSHOT_READ_SCOPE: 자산 스냅샷 조회, 순자산 시계열 조회, 수기 자산 조회를 조회성 MCP 범위에 포함하고, 스냅샷 생성/수기 자산 CRUD 같은 mutation MCP는 후속 범위로 확정.
|
||||
|
||||
## DATA
|
||||
- DATA_MANUAL_ASSET_INVENTORY: 실제 초기 수기 등록 목록(부동산·예적금·대출·기타)과 평가값·기준일·출처는 REQ/delivery 단계 입력 데이터로 수집한다. Discovery confirm의 차단 항목으로 남기지 않는다.
|
||||
|
||||
# 11. Discovery Freeze
|
||||
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다.
|
||||
|
||||
## 섹션 참조
|
||||
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
|
||||
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
|
||||
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
|
||||
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
|
||||
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
|
||||
|
||||
## Freeze 플래그
|
||||
- Handoff Decision: 승인
|
||||
- Handoff Rationale: 권장안 기준으로 권위 소스, 신규 테이블, 월말 수동 스냅샷 정책, 조회성 MCP 포함 범위를 확정했다. 남은 수기 자산 실제 목록은 REQ/delivery 단계 입력 데이터이며 REQ 초안 작성을 막지 않는다.
|
||||
- Ready for REQ Drafting: true
|
||||
- Confirmed By: 진환 고
|
||||
- Confirmed At (KST): 2026-06-30 21:15 KST
|
||||
@@ -13,3 +13,4 @@
|
||||
| dcy-005_20260629_dashboard-operations-hub-liquidity-monthly-cashflow | 20260629 | Jarca Ledger 대시보드 운영 허브 사양 정립 (이번 달 현금흐름 + 자산 유동성 + 운영 미처리) | [Discovery](./dcy-005_20260629_dashboard-operations-hub-liquidity-monthly-cashflow.md) | confirmed |
|
||||
| dcy-006_20260630_pytest-apptest-smoke-ci-automation | 20260630 | pytest/AppTest/Smoke CI 자동화 | [Discovery](./dcy-006_20260630_pytest-apptest-smoke-ci-automation.md) | confirmed |
|
||||
| dcy-007_20260630_mcp-response-contract-ci-contract-versioning | 20260630 | MCP response contract / CI contract versioning 정립 | [Discovery](./dcy-007_20260630_mcp-response-contract-ci-contract-versioning.md) | confirmed |
|
||||
| dcy-008_20260630_asset-snapshot-manual-valuation-system | 20260630 | 자산 스냅샷/수기 자산 테이블/평가 체계 정립 | [Discovery](./dcy-008_20260630_asset-snapshot-manual-valuation-system.md) | confirmed |
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
- Status: governed
|
||||
- Owner: Jarca / project maintainer
|
||||
- Last Updated (KST): 2026-06-30
|
||||
- Source Discovery / Batch: bootstrap-baseline, dcy-007, BAT-007
|
||||
- Source Discovery / Batch: bootstrap-baseline, dcy-007, BAT-007, BAT-008
|
||||
|
||||
## Project Summary
|
||||
- Jarca 가계부 전용 MCP 서버와 Streamlit 대시보드로 계좌/카드 거래, 예산, 분류 규칙, import, 현금흐름 분석을 SQLite에 저장하고 운영한다.
|
||||
@@ -26,6 +26,7 @@
|
||||
- Accounts: `create_ledger_account`, `list_ledger_accounts`, `update_ledger_account`; account payloads include additive `liquidity_bucket` for asset-liquidity reporting.
|
||||
- Transactions: `create_ledger_transaction`, `list_ledger_transactions`, `update_ledger_transaction`, `delete_ledger_transaction`
|
||||
- Budgets/reports: `create_ledger_budget`, `list_ledger_budgets`, monthly/category/fixed/budget/investable reports, `get_asset_liquidity_report`
|
||||
- Asset valuation reads: `list_manual_assets`, `get_asset_snapshot_report`, `get_net_worth_timeseries`; these are read-only in BAT-008 and use the same valuation policy as the dashboard.
|
||||
- Imports: KB Bank PDF, Toss Bank PDF, Toss Securities PDF, SeoulPay XLSX, CSV preview/import where implemented
|
||||
- Categories/rules: category CRUD, rule CRUD, categorization preview/apply, uncategorized summaries
|
||||
- Reconciliation: cross-account preview/run/review queue, self-transfer candidates/linking, cancel-original candidates/linking
|
||||
@@ -72,6 +73,14 @@
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 1.0 | 2026-06-30 | Initial governed baseline | dcy-007 / REQ-007 / BAT-007 | Formalizes compatibility governance without exposing runtime version fields or changing success payload shapes. |
|
||||
|
||||
### Asset Valuation Read Tools
|
||||
- `list_manual_assets(active_only=true)` returns `success`, `manual_assets`, `count`, and `valuation_policy`. Each item exposes `name`, `valuation_amount`, `valuation_date`, `liquidity_bucket`, `source_note`, `memo`, and `authority_source='manual_assets'`.
|
||||
- `get_asset_snapshot_report(basis_date='')` returns `success`, `basis_date`, `source`, `snapshot_count`, `buckets`, `total_assets`, `total_liabilities`, `net_worth`, `included_items`, `excluded_items`, and `valuation_policy`.
|
||||
- If `asset_snapshots` rows exist for `basis_date`, `source='asset_snapshots'` and values come from stored snapshot rows.
|
||||
- If no snapshot rows exist, `source='live_authority'` and the report reads `accounts.last_balance` plus active `manual_assets`.
|
||||
- `get_net_worth_timeseries(start_date='', end_date='', months=12)` returns `success`, requested date bounds, `points`, `count`, and `valuation_policy`; each point contains `basis_date`, `total_assets`, `total_liabilities`, `net_worth`, `source`, and `item_count`.
|
||||
- Contract impact: additive minor. Existing success payloads are preserved; `get_asset_liquidity_report` gains optional explanatory fields (`manual_assets`, `manual_asset_count`, `included_items`, `excluded_items`, `valuation_policy`) while preserving existing keys.
|
||||
|
||||
## Interface: CI Required-Check Contract
|
||||
- Required-check source of truth in code: `.github/workflows/ci.yml` job names are stable and must be mirrored by repository branch protection settings.
|
||||
- Required-check default set for PR/push merge blocking: `pytest-regression`, `dashboard-app-test`, `dashboard-http-smoke`, `db-safety-smoke`, `contract-regression`, `lint`.
|
||||
@@ -111,7 +120,8 @@
|
||||
- Rule page should support create/edit/delete and condition fields `amount_min`, `amount_max`, `day_of_month`.
|
||||
- Dialog-based triage actions should avoid losing context where possible.
|
||||
- Account page should display/edit `statement_status` and show a minimal manual balance reconciliation report without adding an automatic schedule or dedicated workbench.
|
||||
- Account page should display/edit `liquidity_bucket`; overview uses it with `last_balance` for asset liquidity/composition/debt/net-worth sections.
|
||||
- Account page should display/edit `liquidity_bucket`; overview uses the shared service valuation report for asset liquidity/composition/debt/net-worth sections.
|
||||
- Overview asset/net-worth display must keep the REQ-005 account-only fallback when `manual_assets`/`asset_snapshots` are empty, and may additionally show manual asset rows when present.
|
||||
- Overview top priority is KST current-month actual income/expense/net cashflow/savings rate based on `transaction_date` and `cashflow_kind`.
|
||||
- UI/dashboard-impacting batches require browser screenshot/snapshot evidence from synthetic/validation DBs. Non-UI batches may omit browser evidence, and docs-only batches do not require it.
|
||||
|
||||
@@ -129,7 +139,7 @@
|
||||
- Broad mutation workflows should capture metadata-only DB status and backup evidence before/after risky operations.
|
||||
- Restore execution is limited to script/runbook workflows and explicit approval, not MCP tools.
|
||||
- Manual balance reconciliation is preview/report-first. `REVIEW_NEEDED` suggests manual decision values (`계좌이동`, `실지출`, `실수입`, `투자이동`, `분석제외`, `확인필요`) but does not auto-confirm mutations.
|
||||
- Asset liquidity reporting is account-based: `liquidity_bucket='loan'` is debt/net-worth only, and missing `last_balance` rows must not be inferred from transaction totals.
|
||||
- Asset liquidity reporting uses the shared valuation policy: account-backed values from `accounts.last_balance`, non-transaction assets/liabilities from `manual_assets`, `loan` as debt/net-worth only, and missing values excluded with explanation rather than inferred from transaction totals.
|
||||
- Browser evidence screenshots/snapshots must use synthetic or validation DBs only and must not include real account identifiers, source financial filenames, passwords, or raw payloads.
|
||||
- Financial data and passwords are sensitive runtime inputs and should not be committed.
|
||||
|
||||
@@ -137,6 +147,7 @@
|
||||
- Formal MCP response contract versioning is defined for version `1.0`; full tool-surface snapshot coverage remains a future expansion beyond the representative BAT-007 scope.
|
||||
- Browser-level UI evidence criteria are captured in REQ-004 and remain local evidence rather than a required CI gate unless a future REQ changes that policy.
|
||||
- CI tests/lint required-check contract is defined for the six stable jobs listed above; actual branch protection enforcement remains an operator-managed repository setting.
|
||||
- BAT-008 adds read-only asset valuation MCP tools. Mutation MCP for manual asset CRUD or snapshot creation remains out of scope/future work.
|
||||
|
||||
## Change Log
|
||||
- 2026-06-26 22:24 KST: Created by `bootstrap-baseline` from repository observation.
|
||||
@@ -145,3 +156,4 @@
|
||||
- 2026-06-29 KST: REQ-004/BAT-004 기준으로 Streamlit browser evidence 산출물과 UI evidence gate 규칙 추가.
|
||||
- 2026-06-29 KST: REQ-005/BAT-005 기준으로 account `liquidity_bucket`, `get_asset_liquidity_report`, dashboard operations hub contract 추가.
|
||||
- 2026-06-30 KST: REQ-007/REQ-008/BAT-007 기준으로 MCP response contract `1.0` governance, representative contract-regression scope, `ruff` lint, six-job required-check contract를 추가.
|
||||
- 2026-06-30 KST: REQ-009/REQ-010/BAT-008 기준으로 `list_manual_assets`, `get_asset_snapshot_report`, `get_net_worth_timeseries` read MCP contract와 dashboard valuation alignment 규칙을 추가.
|
||||
|
||||
+15
-5
@@ -5,7 +5,7 @@
|
||||
- Status: draft
|
||||
- Owner: Jarca / project maintainer
|
||||
- Last Updated (KST): 2026-06-30
|
||||
- Source Discovery / Batch: bootstrap-baseline, BAT-006, BAT-007
|
||||
- Source Discovery / Batch: bootstrap-baseline, BAT-006, BAT-007, BAT-008
|
||||
- Primary Runtime: mixed
|
||||
|
||||
## Project Summary
|
||||
@@ -48,9 +48,10 @@
|
||||
## Flow: `Dashboard operations hub overview`
|
||||
1. Operator opens the `요약` page.
|
||||
2. Dashboard resolves the KST current month and calls `get_monthly_cashflow_report(month)` using `transactions.transaction_date` and `ledger_categories.cashflow_kind` semantics.
|
||||
3. Dashboard calls `get_asset_liquidity_report`, which groups active accounts by `accounts.liquidity_bucket` and uses only `accounts.last_balance` for asset/liability values.
|
||||
4. Dashboard renders sections in REQ-005 priority order: this-month actual income/expense/net/savings rate, asset liquidity, asset composition, budget/spend, operational warnings.
|
||||
5. Missing `last_balance` account rows are excluded from asset totals and surfaced as warnings; transaction sums are not used to estimate non-transaction assets.
|
||||
3. Dashboard calls `get_asset_liquidity_report`, which uses the shared valuation helper: account-backed assets from `accounts.last_balance` plus active `manual_assets` rows when present.
|
||||
4. If `manual_assets` and `asset_snapshots` are empty, the report keeps the REQ-005 account-only fallback and preserves existing totals/keys.
|
||||
5. Dashboard renders sections in REQ-005 priority order: this-month actual income/expense/net/savings rate, asset liquidity, asset composition, budget/spend, operational warnings.
|
||||
6. Missing `last_balance` or `manual_assets.valuation_amount` values are excluded from asset totals and surfaced as warnings; transaction sums are not used to estimate non-transaction assets.
|
||||
|
||||
## Flow: `CI validation gates`
|
||||
1. GitHub Actions starts `.github/workflows/ci.yml` on `pull_request`, `push` to `main`, or `workflow_dispatch`.
|
||||
@@ -63,6 +64,14 @@
|
||||
8. Any required job failure fails the workflow. Stable required-check job names are `pytest-regression`, `dashboard-app-test`, `dashboard-http-smoke`, `db-safety-smoke`, `contract-regression`, and `lint`; actual branch protection toggle enforcement is configured outside the repository.
|
||||
9. CI logs are status-oriented and synthetic-data-only; they must not contain real account identifiers, raw payloads, statement filenames, balances from the operating DB, or passwords.
|
||||
|
||||
## Flow: `Asset snapshot and net-worth read model`
|
||||
1. Month-end manual close records item-level rows in `asset_snapshots` from the live authority sources: account `last_balance` rows and `manual_assets` valuations. BAT-008 stores the schema and service helper but does not expose snapshot creation as an MCP mutation.
|
||||
2. `get_asset_snapshot_report(basis_date)` first checks for snapshot rows on the requested basis date.
|
||||
3. If snapshot rows exist, the report computes bucket totals, total assets, total liabilities, and net worth from the stored rows.
|
||||
4. If no snapshot rows exist, the report falls back to live authority sources (`accounts.last_balance` + active `manual_assets`) and marks `source='live_authority'`.
|
||||
5. `get_net_worth_timeseries(start_date, end_date, months)` reads distinct stored snapshot basis dates and applies the same asset-buckets-minus-loan formula to every point.
|
||||
6. `list_manual_assets` exposes current manual asset/liability rows for read-only MCP consumers. Manual asset CRUD and snapshot creation MCP tools remain future scope.
|
||||
|
||||
## Flow: `Import preview and import`
|
||||
1. Operator chooses or supplies a supported source file: KB Bank PDF, Toss Bank PDF, Toss Securities PDF, SeoulPay XLSX, or CSV.
|
||||
2. Preview function parses the file and reports counts, sample rows, duplicate status, and classification preview.
|
||||
@@ -121,7 +130,8 @@
|
||||
- CI gates must use synthetic/temp DBs only. The default operating DB path (`~/.local/share/jarca/ledger.sqlite3`) is forbidden for AppTest, HTTP smoke, and DB safety smoke wrappers.
|
||||
- Contract regression must use synthetic/temp DBs only and must not expose runtime contract-version fields in MCP success payloads unless a future approved REQ changes the policy.
|
||||
- Branch protection required-check drift should be audited against the six stable workflow job names documented in `docs/interface-contract.md`; changing a job name requires an explicit REQ/BAT note because it can break external repository settings.
|
||||
- Dashboard asset liquidity calculations must use `accounts.liquidity_bucket` and `accounts.last_balance`; `loan` is excluded from the asset composition denominator and reflected as debt/net-worth only.
|
||||
- Dashboard and MCP asset valuation reports must use the shared service valuation policy: `accounts.last_balance` plus `manual_assets`, asset buckets minus loan, and missing valuation exclusion.
|
||||
- Snapshot read tools are read-only in BAT-008. Snapshot creation and manual asset CRUD MCP mutation tools require a future REQ.
|
||||
- Sensitive passwords are runtime inputs and must not be committed.
|
||||
|
||||
## Current Gaps / Planned Changes
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
# REQ-009 Asset snapshot/manual assets valuation model
|
||||
|
||||
- Status: Implemented
|
||||
- Type: Data
|
||||
- Priority: High
|
||||
- Owner: 렛저장인
|
||||
- Source Discovery: `docs/discovery/dcy-008_20260630_asset-snapshot-manual-valuation-system.md`
|
||||
- Batch: BAT-008
|
||||
- Created At (KST): 2026-06-30 21:17 KST
|
||||
- Approved At (KST): 2026-06-30 21:24 KST
|
||||
- Implemented At (KST): 2026-06-30 22:05 KST
|
||||
|
||||
## 1. 배경
|
||||
|
||||
REQ-005는 `accounts.liquidity_bucket` + `last_balance` 기반 자산 유동성/순자산 표시를 구현했지만, 시점별 자산 평가 이력과 비거래 자산/부채의 독립 모델은 의도적으로 제외했다. dcy-008 confirmed Discovery에 따라 자산 스냅샷 이력과 수기 자산 모델을 별도 테이블로 도입하고, 계좌 `last_balance` + 수기 자산 보강을 단일 평가 권위 소스로 정립해 기준일별 순자산 재현과 후속 MCP 조회 기능의 데이터 기반을 마련해야 한다.
|
||||
|
||||
## 2. Intent
|
||||
|
||||
자산 평가 모델을 계좌 한 줄의 최신 값에서 벗어나, 기준일별 스냅샷과 비거래 자산/부채의 명시적 엔티티로 분리한다. 이 REQ의 목적은 `asset_snapshots`/`manual_assets` 데이터 모델, 순자산 산식, 권위 소스 우선순위, 월말 수동 스냅샷 정책을 구현 가능한 저장 계층 요구사항으로 고정하는 것이다.
|
||||
|
||||
## 3. 범위
|
||||
|
||||
### In Scope
|
||||
- `asset_snapshots`와 `manual_assets`를 별도 additive/idempotent 테이블로 도입한다.
|
||||
- 비거래 자산/부채의 필수 속성(이름, 평가값, 기준일, `liquidity_bucket`, 출처 메모)을 정의한다.
|
||||
- 자산 스냅샷의 필수 속성(기준일, 평가 대상, 평가 금액, `liquidity_bucket`, 출처/메모)을 정의한다.
|
||||
- 자산성 합계 − `loan` 차감, 미평가 제외 규칙을 저장/집계 계층 기준으로 고정한다.
|
||||
- 월말 수동 스냅샷 생성 정책과 항목 단위 수용 구조를 반영한다.
|
||||
- `docs/data-model.md`에 위 모델과 권위 소스 규칙을 반영한다.
|
||||
|
||||
### Out of Scope
|
||||
- 외부 시세/부동산 자동 평가 연동.
|
||||
- 실시간 자동 스냅샷 생성 스케줄러.
|
||||
- 수익률/세금/리밸런싱 계산.
|
||||
- 조회성 MCP 응답 shape 정의 자체(별도 Interface REQ에서 다룸).
|
||||
|
||||
## 4. Requirement
|
||||
|
||||
### FR-1. Manual assets data model
|
||||
- `manual_assets`는 거래로 산출되지 않는 비거래 자산/부채를 저장해야 한다.
|
||||
- 각 항목은 최소한 이름, 자산/부채 구분 또는 `liquidity_bucket`, 평가값, 기준일, 출처 메모를 가져야 한다.
|
||||
- 허용 `liquidity_bucket` 값은 `cash`, `investment`, `deposit`, `real_estate`, `loan`, `other`다.
|
||||
- 사용자 입력 기반 평가값이 권위 소스가 되는 항목은 계좌 모델과 혼합 저장하지 않고 별도 엔티티로 유지해야 한다.
|
||||
|
||||
### FR-2. Asset snapshots history model
|
||||
- `asset_snapshots`는 기준일 시점의 자산 평가 결과를 누적 저장해야 한다.
|
||||
- 스냅샷은 계좌와 수기 자산 항목을 모두 담을 수 있는 항목 단위 구조를 수용해야 한다.
|
||||
- 기존 스냅샷은 파괴적으로 덮어쓰지 않고 신규 행으로 누적해야 한다.
|
||||
- 각 행은 최소한 기준일, 평가 대상 식별자, 평가 금액, `liquidity_bucket`, 평가 출처/메모를 보존해야 한다.
|
||||
|
||||
### FR-3. Valuation authority source rule
|
||||
- 계좌 기반 자산은 기존 `accounts.last_balance`를 기본 평가 권위 소스로 사용해야 한다.
|
||||
- 거래로 산출되지 않는 비거래 자산/부채는 `manual_assets`를 권위 소스로 사용해야 한다.
|
||||
- 스냅샷은 위 권위 소스를 기준일 시점에 고정한 기록층이어야 하며, 실시간 권위 소스를 대체하는 별도 1차 입력원이 되어서는 안 된다.
|
||||
- 동일 자산이 중복 집계되지 않도록 계좌 항목과 수기 자산 항목의 책임 경계를 정의해야 한다.
|
||||
|
||||
### FR-4. Net worth formula and exclusion policy
|
||||
- 순자산은 자산성 버킷(`cash`, `investment`, `deposit`, `real_estate`, `other`) 합계에서 `loan` 부채를 차감해 계산해야 한다.
|
||||
- 평가값이 없는 항목은 임의 거래 합계로 추정하지 않고 제외해야 한다.
|
||||
- 제외된 항목은 후속 화면/응답 계층에서 설명 가능하도록 상태 또는 근거를 남길 수 있어야 한다.
|
||||
|
||||
### FR-5. Manual month-end snapshot policy
|
||||
- 초기 운영 정책은 월말 수동 스냅샷 생성이어야 한다.
|
||||
- 스냅샷 생성 흐름은 월말 마감 시점에 계좌 `last_balance`와 수기 자산 평가값을 함께 고정할 수 있어야 한다.
|
||||
- 자동 생성/스케줄링은 후속 범위로 남긴다.
|
||||
|
||||
### FR-6. Baseline documentation alignment
|
||||
- `docs/data-model.md`는 `asset_snapshots`, `manual_assets`, 권위 소스 우선순위, 순자산 산식을 반영해야 한다.
|
||||
- 필요 시 `docs/project-structure.md`와 `docs/runtime-flows.md`에 데이터 모델 책임과 월말 스냅샷 흐름을 참조 수준으로 반영해야 한다.
|
||||
|
||||
## 5. Acceptance Criteria
|
||||
|
||||
- AC-1: 신규 DB/기존 DB 모두에서 `asset_snapshots`, `manual_assets`가 additive/idempotent migration으로 생성된다.
|
||||
- AC-2: `manual_assets`는 이름, 평가값, 기준일, `liquidity_bucket`, 출처 메모를 저장할 수 있다.
|
||||
- AC-3: `asset_snapshots`는 기준일, 평가 대상, 평가 금액, `liquidity_bucket`, 출처/메모를 누적 저장할 수 있다.
|
||||
- AC-4: 계좌 기반 자산은 `last_balance`, 비거래 자산/부채는 `manual_assets`를 권위 소스로 사용하는 규칙이 구현/문서에 반영된다.
|
||||
- AC-5: 순자산 계산이 자산성 합계 − `loan` 차감, 미평가 제외 규칙을 따른다.
|
||||
- AC-6: 빈 신규 테이블 상태에서도 기존 `REQ-005` 대시보드 흐름이 깨지지 않는다.
|
||||
- AC-7: `docs/data-model.md`와 필요 baseline 문서가 본 REQ 범위를 반영한다.
|
||||
|
||||
## 6. Impacted Area
|
||||
|
||||
- `jarca_ledger_mcp/db.py`
|
||||
- `jarca_ledger_mcp/service.py`
|
||||
- `docs/data-model.md`
|
||||
- 필요 시 `docs/project-structure.md`
|
||||
- 필요 시 `docs/runtime-flows.md`
|
||||
|
||||
## 7. Notes
|
||||
|
||||
- 이 REQ는 저장 구조/권위 소스/순자산 산식 같은 데이터 모델 책임을 다루므로 `Data` 타입으로 관리한다.
|
||||
- 조회성 MCP 응답 계약과 대시보드 노출 shape는 `REQ-010`에서 다룬다.
|
||||
- 실제 초기 수기 자산 목록은 구현 전 사용자가 입력해야 하며, 본 REQ는 그 입력 포맷과 저장 규칙을 다루는 수준으로 제한한다.
|
||||
|
||||
## 8. Change Log
|
||||
|
||||
- 2026-06-30 21:17 KST: dcy-008 confirmed 범위를 바탕으로 Proposed 초안을 생성.
|
||||
- 2026-06-30 21:24 KST: dcy-008 confirmed 결정사항과 Acceptance Criteria를 재검토해 Approved로 전환.
|
||||
- 2026-06-30 22:05 KST: BAT-008 구현·검증 evidence와 jarcaledgerqc PASS gate를 근거로 Implemented로 전환.
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
# REQ-010 Asset snapshot/net worth read MCP and dashboard alignment
|
||||
|
||||
- Status: Implemented
|
||||
- Type: Interface
|
||||
- Priority: High
|
||||
- Owner: 렛저장인
|
||||
- Source Discovery: `docs/discovery/dcy-008_20260630_asset-snapshot-manual-valuation-system.md`
|
||||
- Batch: BAT-008
|
||||
- Created At (KST): 2026-06-30 21:17 KST
|
||||
- Approved At (KST): 2026-06-30 21:24 KST
|
||||
- Implemented At (KST): 2026-06-30 22:05 KST
|
||||
|
||||
## 1. 배경
|
||||
|
||||
자산 스냅샷/수기 자산/평가 체계가 데이터 모델로만 존재하면 대시보드와 MCP 소비자가 서로 다른 규칙으로 자산·순자산을 해석할 위험이 있다. dcy-008 confirmed Discovery는 기준일 자산 스냅샷 조회, 순자산 시계열 조회, 수기 자산 조회를 조회성 MCP 범위에 포함하고, dcy-005 대시보드 표시 경로와 동일한 산식·권위 소스를 사용하도록 요구한다.
|
||||
|
||||
## 2. Intent
|
||||
|
||||
자산 스냅샷과 순자산 평가 결과를 대시보드 내부 계산에만 가두지 않고, 조회성 MCP와 화면 표시가 같은 규칙·같은 설명 필드·같은 제외 정책을 따르도록 인터페이스 계약을 정렬한다. 이 REQ의 목적은 기준일 조회, 시계열 조회, 수기 자산 조회의 최소 응답 계약과 대시보드 정합성 의무를 구현 가능한 인터페이스 요구사항으로 고정하는 것이다.
|
||||
|
||||
## 3. 범위
|
||||
|
||||
### In Scope
|
||||
- 기준일 자산 스냅샷 조회 MCP를 정의한다.
|
||||
- 순자산 시계열 조회 MCP를 정의한다.
|
||||
- 수기 자산 조회 MCP를 정의한다.
|
||||
- 응답에 기준일/집계 구간, 권위 소스, `liquidity_bucket`, 산식 적용 기준, 포함/제외 항목 설명을 포함한다.
|
||||
- 대시보드 자산/순자산 표시가 동일 권위 소스와 동일 산식을 사용하도록 정렬한다.
|
||||
- `docs/interface-contract.md`, 필요 시 `docs/runtime-flows.md`, `docs/project-structure.md`를 갱신한다.
|
||||
|
||||
### Out of Scope
|
||||
- 스냅샷 생성 mutation MCP.
|
||||
- 수기 자산 생성/수정/삭제 mutation MCP.
|
||||
- 외부 시세/부동산 자동 평가를 노출하는 인터페이스.
|
||||
- 투자 조언/리밸런싱 추천 응답.
|
||||
|
||||
## 4. Requirement
|
||||
|
||||
### FR-1. Asset snapshot read MCP
|
||||
- 기준일별 자산 스냅샷 조회 경로를 제공해야 한다.
|
||||
- 응답은 최소한 기준일, 자산 버킷별 금액, 총부채, 순자산, 포함/제외 항목 요약을 설명할 수 있어야 한다.
|
||||
- 응답은 어떤 권위 소스(`last_balance`, `manual_assets`, snapshot record)가 사용됐는지 추적 가능해야 한다.
|
||||
|
||||
### FR-2. Net worth timeseries read MCP
|
||||
- 최근 N개월 또는 지정 구간 순자산 시계열 조회 경로를 제공해야 한다.
|
||||
- 각 시점은 동일 산식(자산성 합계 − `loan`)과 동일 제외 정책을 적용해야 한다.
|
||||
- 시계열 응답은 기준일 또는 구간 정의를 명확히 포함해야 한다.
|
||||
|
||||
### FR-3. Manual assets read MCP
|
||||
- 수기 자산/부채 목록 또는 집계 조회 경로를 제공해야 한다.
|
||||
- 응답은 항목명, 평가값, 기준일, `liquidity_bucket`, 출처 메모를 확인할 수 있어야 한다.
|
||||
- 미평가 또는 오래된 평가 항목은 후속 UX/응답에서 경고 가능하도록 상태 정보를 제공할 수 있어야 한다.
|
||||
|
||||
### FR-4. Dashboard/MCP contract alignment
|
||||
- `dashboard/app.py`의 자산 유동성/순자산 표시와 조회성 MCP 응답은 동일 권위 소스 우선순위와 동일 순자산 산식을 사용해야 한다.
|
||||
- `REQ-005`의 기존 표시 경로와 충돌하지 않도록, 신규 모델이 비어 있는 경우 기존 안전 폴백을 유지해야 한다.
|
||||
- 동일 데이터에 대해 대시보드와 MCP가 숫자/분류 규칙이 어긋나지 않도록 검증 가능해야 한다.
|
||||
|
||||
### FR-5. Baseline documentation alignment
|
||||
- `docs/interface-contract.md`는 자산 스냅샷 조회, 순자산 시계열 조회, 수기 자산 조회의 최소 응답 계약을 반영해야 한다.
|
||||
- `docs/runtime-flows.md`는 월말 스냅샷 생성 이후 조회성 MCP와 대시보드가 동일 데이터를 소비하는 흐름을 설명해야 한다.
|
||||
- `docs/project-structure.md`는 `dashboard/app.py`, `jarca_ledger_mcp/service.py`, `jarca_ledger_mcp/server.py` 간 책임 경계를 반영해야 한다.
|
||||
|
||||
## 5. Acceptance Criteria
|
||||
|
||||
- AC-1: 기준일 자산 스냅샷 조회 MCP가 정의되고 기준일, 버킷별 금액, 총부채, 순자산, 포함/제외 항목을 반환할 수 있다.
|
||||
- AC-2: 순자산 시계열 조회 MCP가 정의되고 지정 구간의 기준일별 순자산을 동일 산식으로 반환할 수 있다.
|
||||
- AC-3: 수기 자산 조회 MCP가 정의되고 항목명, 평가값, 기준일, `liquidity_bucket`, 출처 메모를 반환할 수 있다.
|
||||
- AC-4: 대시보드와 MCP가 동일 권위 소스/동일 산식/동일 제외 정책을 사용한다.
|
||||
- AC-5: 신규 모델이 비어 있어도 기존 `REQ-005` 자산 표시 경로가 안전하게 폴백한다.
|
||||
- AC-6: `docs/interface-contract.md`와 필요 baseline 문서가 본 REQ 범위를 반영한다.
|
||||
|
||||
## 6. Impacted Area
|
||||
|
||||
- `jarca_ledger_mcp/server.py`
|
||||
- `jarca_ledger_mcp/service.py`
|
||||
- `dashboard/app.py`
|
||||
- `docs/interface-contract.md`
|
||||
- 필요 시 `docs/runtime-flows.md`
|
||||
- 필요 시 `docs/project-structure.md`
|
||||
|
||||
## 7. Notes
|
||||
|
||||
- 이 REQ는 조회성 MCP와 대시보드 표시 계약을 다루므로 `Interface` 타입으로 관리한다.
|
||||
- mutation MCP는 본 REQ에서 제외하며 후속 Discovery/REQ로 분리한다.
|
||||
- 데이터 저장 구조와 월말 수동 스냅샷 정책의 구현 책임은 `REQ-009`를 선행 입력으로 사용한다.
|
||||
|
||||
## 8. Change Log
|
||||
|
||||
- 2026-06-30 21:17 KST: dcy-008 confirmed 범위를 바탕으로 Proposed 초안을 생성.
|
||||
- 2026-06-30 21:24 KST: dcy-008 confirmed 결정사항과 Acceptance Criteria를 재검토해 Approved로 전환.
|
||||
- 2026-06-30 22:05 KST: BAT-008 구현·검증 evidence와 jarcaledgerqc PASS gate를 근거로 Implemented로 전환.
|
||||
+6
-1
@@ -33,7 +33,7 @@
|
||||
|
||||
## Next Requirement ID
|
||||
|
||||
- Current: `REQ-009`
|
||||
- Current: `REQ-011`
|
||||
- Rule: `Requirement Register`에 있는 가장 큰 REQ 번호보다 정확히 1 커야 한다.
|
||||
|
||||
## Requirement Register
|
||||
@@ -48,6 +48,8 @@
|
||||
| REQ-006 | pytest/AppTest/Smoke CI 자동화 | Testing | Implemented | High | 렛저장인 | [req-006_pytest-apptest-smoke-ci-automation.md](Testing/req-006_pytest-apptest-smoke-ci-automation.md) |
|
||||
| REQ-007 | MCP response contract versioning governance | Interface | Implemented | High | 렛저장인 | [req-007_mcp-response-contract-versioning-governance.md](Interface/req-007_mcp-response-contract-versioning-governance.md) |
|
||||
| REQ-008 | CI contract regression and lint required checks | Testing | Implemented | High | 렛저장인 | [req-008_ci-contract-regression-and-lint-required-checks.md](Testing/req-008_ci-contract-regression-and-lint-required-checks.md) |
|
||||
| REQ-009 | Asset snapshot/manual assets valuation model | Data | Implemented | High | 렛저장인 | [req-009_asset-snapshot-manual-assets-valuation-model.md](Data/req-009_asset-snapshot-manual-assets-valuation-model.md) |
|
||||
| REQ-010 | Asset snapshot/net worth read MCP and dashboard alignment | Interface | Implemented | High | 렛저장인 | [req-010_asset-snapshot-net-worth-read-mcp-and-dashboard-alignment.md](Interface/req-010_asset-snapshot-net-worth-read-mcp-and-dashboard-alignment.md) |
|
||||
|
||||
## Recent Change Log Summary
|
||||
|
||||
@@ -66,3 +68,6 @@
|
||||
- 2026-06-30 18:35 KST: dcy-007 confirmed 범위를 `REQ-007`(MCP response contract versioning governance)과 `REQ-008`(CI contract regression and lint required checks) Proposed 상태로 생성했다.
|
||||
- 2026-06-30 18:56 KST: `REQ-007`, `REQ-008`을 승인 게이트 재검토 후 Approved로 전환했다.
|
||||
- 2026-06-30 KST: BAT-007 구현과 contract regression/lint/smoke 검증 evidence를 근거로 `REQ-007`, `REQ-008`을 Implemented로 전환했다.
|
||||
- 2026-06-30 21:17 KST: dcy-008 confirmed 범위를 `REQ-009`(asset snapshot/manual assets valuation model)과 `REQ-010`(asset snapshot/net worth read MCP and dashboard alignment) Proposed 상태로 생성했다.
|
||||
- 2026-06-30 21:24 KST: `REQ-009`, `REQ-010`을 승인 게이트 재검토 후 Approved로 전환했다.
|
||||
- 2026-06-30 22:05 KST: BAT-008 구현·검증 evidence, fresh Streamlit browser smoke, jarcaledgerqc PASS gate를 근거로 `REQ-009`, `REQ-010`을 Implemented로 전환했다.
|
||||
|
||||
+66
-2
@@ -7,7 +7,7 @@ from pathlib import Path
|
||||
from typing import Iterator
|
||||
|
||||
DEFAULT_DB_PATH = Path.home() / ".local" / "share" / "jarca" / "ledger.sqlite3"
|
||||
SCHEMA_VERSION = "8"
|
||||
SCHEMA_VERSION = "9"
|
||||
|
||||
SCHEMA_SQL = """
|
||||
PRAGMA journal_mode=WAL;
|
||||
@@ -137,7 +137,40 @@ CREATE TABLE IF NOT EXISTS ledger_categories (
|
||||
CREATE INDEX IF NOT EXISTS idx_ledger_categories_active_sort ON ledger_categories(active, sort_order, category, subcategory);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_ledger_categories_unique ON ledger_categories(category, subcategory);
|
||||
|
||||
INSERT OR REPLACE INTO schema_meta(key, value) VALUES ('schema_version', '7');
|
||||
CREATE TABLE IF NOT EXISTS manual_assets (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
liquidity_bucket TEXT NOT NULL DEFAULT 'other',
|
||||
valuation_amount INTEGER,
|
||||
valuation_date TEXT NOT NULL DEFAULT '',
|
||||
currency TEXT NOT NULL DEFAULT 'KRW',
|
||||
source_note TEXT NOT NULL DEFAULT '',
|
||||
memo TEXT NOT NULL DEFAULT '',
|
||||
active INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_manual_assets_active_bucket ON manual_assets(active, liquidity_bucket, name);
|
||||
CREATE INDEX IF NOT EXISTS idx_manual_assets_valuation_date ON manual_assets(valuation_date);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS asset_snapshots (
|
||||
id TEXT PRIMARY KEY,
|
||||
basis_date TEXT NOT NULL,
|
||||
source_type TEXT NOT NULL,
|
||||
source_id TEXT NOT NULL DEFAULT '',
|
||||
name TEXT NOT NULL,
|
||||
amount INTEGER,
|
||||
currency TEXT NOT NULL DEFAULT 'KRW',
|
||||
liquidity_bucket TEXT NOT NULL DEFAULT 'other',
|
||||
valuation_source TEXT NOT NULL DEFAULT '',
|
||||
source_note TEXT NOT NULL DEFAULT '',
|
||||
memo TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_asset_snapshots_basis_date ON asset_snapshots(basis_date);
|
||||
CREATE INDEX IF NOT EXISTS idx_asset_snapshots_source ON asset_snapshots(source_type, source_id);
|
||||
|
||||
INSERT OR REPLACE INTO schema_meta(key, value) VALUES ('schema_version', '9');
|
||||
"""
|
||||
|
||||
ACCOUNT_ADDITIVE_COLUMNS: dict[str, str] = {
|
||||
@@ -226,6 +259,37 @@ def migrate_db(conn: sqlite3.Connection) -> None:
|
||||
_add_missing_columns(conn, "ledger_categories", {"cashflow_kind": "TEXT NOT NULL DEFAULT 'expense'"})
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_ledger_categories_active_sort ON ledger_categories(active, sort_order, category, subcategory)")
|
||||
conn.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_ledger_categories_unique ON ledger_categories(category, subcategory)")
|
||||
conn.execute("""CREATE TABLE IF NOT EXISTS manual_assets (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
liquidity_bucket TEXT NOT NULL DEFAULT 'other',
|
||||
valuation_amount INTEGER,
|
||||
valuation_date TEXT NOT NULL DEFAULT '',
|
||||
currency TEXT NOT NULL DEFAULT 'KRW',
|
||||
source_note TEXT NOT NULL DEFAULT '',
|
||||
memo TEXT NOT NULL DEFAULT '',
|
||||
active INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)""")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_manual_assets_active_bucket ON manual_assets(active, liquidity_bucket, name)")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_manual_assets_valuation_date ON manual_assets(valuation_date)")
|
||||
conn.execute("""CREATE TABLE IF NOT EXISTS asset_snapshots (
|
||||
id TEXT PRIMARY KEY,
|
||||
basis_date TEXT NOT NULL,
|
||||
source_type TEXT NOT NULL,
|
||||
source_id TEXT NOT NULL DEFAULT '',
|
||||
name TEXT NOT NULL,
|
||||
amount INTEGER,
|
||||
currency TEXT NOT NULL DEFAULT 'KRW',
|
||||
liquidity_bucket TEXT NOT NULL DEFAULT 'other',
|
||||
valuation_source TEXT NOT NULL DEFAULT '',
|
||||
source_note TEXT NOT NULL DEFAULT '',
|
||||
memo TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL
|
||||
)""")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_asset_snapshots_basis_date ON asset_snapshots(basis_date)")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_asset_snapshots_source ON asset_snapshots(source_type, source_id)")
|
||||
conn.execute("INSERT OR REPLACE INTO schema_meta(key, value) VALUES ('schema_version', ?)", (SCHEMA_VERSION,))
|
||||
|
||||
|
||||
|
||||
@@ -51,9 +51,24 @@ def update_ledger_account(account_id: str, name: str | None = None, type: str |
|
||||
|
||||
@mcp.tool()
|
||||
def get_asset_liquidity_report() -> dict:
|
||||
"""Return asset liquidity, debt, and net worth from account liquidity buckets and last_balance."""
|
||||
"""Return asset liquidity, debt, and net worth from shared account/manual valuation sources."""
|
||||
return service.get_asset_liquidity_report()
|
||||
|
||||
@mcp.tool()
|
||||
def list_manual_assets(active_only: bool = True) -> dict:
|
||||
"""List manual non-transaction assets/liabilities used for net-worth valuation."""
|
||||
return service.list_manual_assets(active_only=active_only)
|
||||
|
||||
@mcp.tool()
|
||||
def get_asset_snapshot_report(basis_date: str = "") -> dict:
|
||||
"""Return basis-date asset snapshot/net-worth report with live-authority fallback."""
|
||||
return service.get_asset_snapshot_report(basis_date=basis_date)
|
||||
|
||||
@mcp.tool()
|
||||
def get_net_worth_timeseries(start_date: str = "", end_date: str = "", months: int = 12) -> dict:
|
||||
"""Return net-worth timeseries from stored asset snapshot rows."""
|
||||
return service.get_net_worth_timeseries(start_date=start_date, end_date=end_date, months=months)
|
||||
|
||||
@mcp.tool()
|
||||
def get_balance_reconciliation_report(account_id: str = "", baseline_balance: int | None = None, baseline_at: str = "", source_note: str = "", memo: str = "", tolerance: int = 1000) -> dict:
|
||||
"""Preview balance reconciliation for manual_only/partial_import accounts without mutating the DB."""
|
||||
|
||||
+272
-41
@@ -21,6 +21,9 @@ VALID_TRANSACTION_TYPES = {"income", "expense", "transfer", "refund", "adjustmen
|
||||
VALID_ACCOUNT_TYPES = {"bank", "card", "cash", "investment", "loan", "other"}
|
||||
VALID_STATEMENT_STATUSES = {"unknown", "import_supported", "manual_only", "partial_import"}
|
||||
VALID_LIQUIDITY_BUCKETS = {"cash", "investment", "deposit", "real_estate", "loan", "other"}
|
||||
ASSET_BUCKETS = ["cash", "investment", "deposit", "real_estate", "other"]
|
||||
LIABILITY_BUCKETS = ["loan"]
|
||||
ASSET_BUCKET_ORDER = [*ASSET_BUCKETS, *LIABILITY_BUCKETS]
|
||||
RECONCILIATION_TARGET_STATUSES = {"manual_only", "partial_import"}
|
||||
BALANCE_RECONCILIATION_TOLERANCE_KRW = 1_000
|
||||
BALANCE_RECONCILIATION_REFERENCE_RATIO = 0.001
|
||||
@@ -223,7 +226,7 @@ def _readonly_sqlite_connection(path: Path) -> sqlite3.Connection:
|
||||
|
||||
def _safe_table_counts(conn: sqlite3.Connection) -> dict[str, int]:
|
||||
counts: dict[str, int] = {}
|
||||
for table in ("accounts", "transactions", "budgets", "import_batches", "category_rules", "ledger_categories"):
|
||||
for table in ("accounts", "transactions", "budgets", "import_batches", "category_rules", "ledger_categories", "manual_assets", "asset_snapshots"):
|
||||
exists = conn.execute("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", (table,)).fetchone()
|
||||
if exists:
|
||||
counts[table] = int(conn.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0])
|
||||
@@ -374,73 +377,301 @@ def list_ledger_accounts(active_only: bool = True, db_path: str | None = None) -
|
||||
return ok(accounts=[row_to_dict(r) for r in rows], count=len(rows))
|
||||
|
||||
|
||||
def get_asset_liquidity_report(db_path: str | None = None) -> dict[str, Any]:
|
||||
"""Summarize assets/liabilities from account-level liquidity_bucket and last_balance only."""
|
||||
with db(db_path) as conn:
|
||||
rows = conn.execute("SELECT * FROM accounts WHERE active=1 ORDER BY name").fetchall()
|
||||
accounts = [row_to_dict(r) for r in rows]
|
||||
bucket_order = ["cash", "investment", "deposit", "real_estate", "other", "loan"]
|
||||
bucket_totals = {bucket: 0 for bucket in bucket_order}
|
||||
bucket_counts = {bucket: 0 for bucket in bucket_order}
|
||||
missing_balance_accounts: list[dict[str, Any]] = []
|
||||
invalid_bucket_accounts: list[dict[str, Any]] = []
|
||||
source_accounts: list[dict[str, Any]] = []
|
||||
def _valuation_policy() -> dict[str, Any]:
|
||||
return {
|
||||
"authority_sources": ["accounts.last_balance", "manual_assets"],
|
||||
"snapshot_role": "historical_record_layer",
|
||||
"formula": "asset_buckets - loan",
|
||||
"asset_buckets": ASSET_BUCKETS,
|
||||
"liability_buckets": LIABILITY_BUCKETS,
|
||||
"missing_valuation_policy": "exclude_without_transaction_inference",
|
||||
}
|
||||
|
||||
for account in accounts:
|
||||
|
||||
def _account_valuation_items(conn: sqlite3.Connection) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]]]:
|
||||
rows = conn.execute("SELECT * FROM accounts WHERE active=1 ORDER BY name").fetchall()
|
||||
included: list[dict[str, Any]] = []
|
||||
excluded: list[dict[str, Any]] = []
|
||||
invalid: list[dict[str, Any]] = []
|
||||
for row in rows:
|
||||
account = row_to_dict(row)
|
||||
raw_bucket = str(account.get("liquidity_bucket") or "other")
|
||||
try:
|
||||
bucket = normalize_liquidity_bucket(raw_bucket)
|
||||
except ValueError:
|
||||
bucket = "other"
|
||||
invalid_bucket_accounts.append({"id": account.get("id", ""), "name": account.get("name", ""), "liquidity_bucket": raw_bucket})
|
||||
|
||||
invalid.append({"id": account.get("id", ""), "name": account.get("name", ""), "liquidity_bucket": raw_bucket})
|
||||
base = {
|
||||
"id": account.get("id", ""),
|
||||
"source_id": account.get("id", ""),
|
||||
"source_type": "account",
|
||||
"authority_source": "accounts.last_balance",
|
||||
"name": account.get("name", ""),
|
||||
"type": account.get("type", ""),
|
||||
"liquidity_bucket": bucket,
|
||||
"valuation_date": account.get("last_synced_at", ""),
|
||||
"last_synced_at": account.get("last_synced_at", ""),
|
||||
"source_note": account.get("memo", ""),
|
||||
"memo": account.get("memo", ""),
|
||||
}
|
||||
balance = account.get("last_balance")
|
||||
if balance is None:
|
||||
missing_balance_accounts.append({"id": account.get("id", ""), "name": account.get("name", ""), "liquidity_bucket": bucket})
|
||||
excluded.append({**base, "reason": "missing_last_balance"})
|
||||
continue
|
||||
|
||||
amount = int(balance)
|
||||
bucket_counts[bucket] += 1
|
||||
bucket_totals[bucket] += amount
|
||||
source_accounts.append(
|
||||
included.append({**base, "amount": amount, "last_balance": amount})
|
||||
return included, excluded, invalid
|
||||
|
||||
|
||||
def _manual_asset_valuation_items(conn: sqlite3.Connection, active_only: bool = True) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
||||
where = "WHERE active=1" if active_only else ""
|
||||
rows = conn.execute(f"SELECT * FROM manual_assets {where} ORDER BY name").fetchall()
|
||||
included: list[dict[str, Any]] = []
|
||||
excluded: list[dict[str, Any]] = []
|
||||
for row in rows:
|
||||
asset = row_to_dict(row)
|
||||
try:
|
||||
bucket = normalize_liquidity_bucket(asset.get("liquidity_bucket"))
|
||||
except ValueError:
|
||||
bucket = "other"
|
||||
base = {
|
||||
"id": asset.get("id", ""),
|
||||
"source_id": asset.get("id", ""),
|
||||
"source_type": "manual_asset",
|
||||
"authority_source": "manual_assets",
|
||||
"name": asset.get("name", ""),
|
||||
"liquidity_bucket": bucket,
|
||||
"valuation_date": asset.get("valuation_date", ""),
|
||||
"source_note": asset.get("source_note", ""),
|
||||
"memo": asset.get("memo", ""),
|
||||
"active": asset.get("active", True),
|
||||
}
|
||||
value = asset.get("valuation_amount")
|
||||
if value is None:
|
||||
excluded.append({**base, "reason": "missing_valuation_amount"})
|
||||
continue
|
||||
included.append({**base, "amount": int(value), "valuation_amount": int(value)})
|
||||
return included, excluded
|
||||
|
||||
|
||||
def _snapshot_items(conn: sqlite3.Connection, basis_date: str) -> list[dict[str, Any]]:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM asset_snapshots WHERE basis_date=? ORDER BY created_at ASC, name ASC",
|
||||
(basis_date,),
|
||||
).fetchall()
|
||||
items: list[dict[str, Any]] = []
|
||||
for row in rows:
|
||||
snap = row_to_dict(row)
|
||||
try:
|
||||
bucket = normalize_liquidity_bucket(snap.get("liquidity_bucket"))
|
||||
except ValueError:
|
||||
bucket = "other"
|
||||
items.append(
|
||||
{
|
||||
"id": account.get("id", ""),
|
||||
"name": account.get("name", ""),
|
||||
"type": account.get("type", ""),
|
||||
"id": snap.get("id", ""),
|
||||
"source_id": snap.get("source_id", ""),
|
||||
"source_type": snap.get("source_type", ""),
|
||||
"authority_source": "asset_snapshots",
|
||||
"valuation_source": snap.get("valuation_source", ""),
|
||||
"name": snap.get("name", ""),
|
||||
"liquidity_bucket": bucket,
|
||||
"last_balance": amount,
|
||||
"last_synced_at": account.get("last_synced_at", ""),
|
||||
"memo": account.get("memo", ""),
|
||||
"amount": int(snap.get("amount") or 0),
|
||||
"valuation_date": snap.get("basis_date", ""),
|
||||
"source_note": snap.get("source_note", ""),
|
||||
"memo": snap.get("memo", ""),
|
||||
}
|
||||
)
|
||||
return items
|
||||
|
||||
asset_buckets = ["cash", "investment", "deposit", "real_estate", "other"]
|
||||
total_assets = sum(bucket_totals[bucket] for bucket in asset_buckets)
|
||||
total_liabilities = abs(bucket_totals["loan"])
|
||||
|
||||
def _summarize_valuation_items(included_items: list[dict[str, Any]], excluded_items: list[dict[str, Any]] | None = None) -> dict[str, Any]:
|
||||
bucket_totals = {bucket: 0 for bucket in ASSET_BUCKET_ORDER}
|
||||
bucket_counts = {bucket: 0 for bucket in ASSET_BUCKET_ORDER}
|
||||
for item in included_items:
|
||||
bucket = normalize_liquidity_bucket(item.get("liquidity_bucket"))
|
||||
amount = int(item.get("amount") or 0)
|
||||
bucket_counts[bucket] += 1
|
||||
bucket_totals[bucket] += amount
|
||||
total_assets = sum(bucket_totals[bucket] for bucket in ASSET_BUCKETS)
|
||||
total_liabilities = sum(abs(bucket_totals[bucket]) for bucket in LIABILITY_BUCKETS)
|
||||
net_worth = total_assets - total_liabilities
|
||||
buckets = []
|
||||
for bucket in bucket_order:
|
||||
for bucket in ASSET_BUCKET_ORDER:
|
||||
amount = bucket_totals[bucket]
|
||||
denominator = total_liabilities if bucket == "loan" else total_assets
|
||||
denominator = total_liabilities if bucket in LIABILITY_BUCKETS else total_assets
|
||||
buckets.append(
|
||||
{
|
||||
"liquidity_bucket": bucket,
|
||||
"amount": amount,
|
||||
"account_count": bucket_counts[bucket],
|
||||
"asset_share": round(amount / total_assets, 4) if bucket != "loan" and total_assets else None,
|
||||
"liability_share": round(abs(amount) / total_liabilities, 4) if bucket == "loan" and total_liabilities else None,
|
||||
"account_count": sum(1 for item in included_items if item.get("source_type") == "account" and item.get("liquidity_bucket") == bucket),
|
||||
"manual_asset_count": sum(1 for item in included_items if item.get("source_type") == "manual_asset" and item.get("liquidity_bucket") == bucket),
|
||||
"item_count": bucket_counts[bucket],
|
||||
"asset_share": round(amount / total_assets, 4) if bucket not in LIABILITY_BUCKETS and total_assets else None,
|
||||
"liability_share": round(abs(amount) / total_liabilities, 4) if bucket in LIABILITY_BUCKETS and total_liabilities else None,
|
||||
"share": round(abs(amount) / denominator, 4) if denominator else None,
|
||||
}
|
||||
)
|
||||
return {
|
||||
"buckets": buckets,
|
||||
"bucket_totals": bucket_totals,
|
||||
"total_assets": total_assets,
|
||||
"total_liabilities": total_liabilities,
|
||||
"net_worth": net_worth,
|
||||
"included_items": included_items,
|
||||
"excluded_items": excluded_items or [],
|
||||
"valuation_policy": _valuation_policy(),
|
||||
}
|
||||
|
||||
|
||||
def create_manual_asset(name: str, valuation_amount: int | None, valuation_date: str, liquidity_bucket: str, source_note: str = "", memo: str = "", currency: str = "KRW", active: bool = True, db_path: str | None = None) -> dict[str, Any]:
|
||||
if not name.strip():
|
||||
return fail("validation_error", "manual asset name is required")
|
||||
try:
|
||||
bucket = normalize_liquidity_bucket(liquidity_bucket)
|
||||
amount = int(valuation_amount) if valuation_amount is not None else None
|
||||
vdate = normalize_date(valuation_date, default_today=True)
|
||||
except Exception as e:
|
||||
return fail("validation_error", str(e))
|
||||
ts = now_iso()
|
||||
asset_id = new_id("asset")
|
||||
with db(db_path) as conn:
|
||||
conn.execute(
|
||||
"""INSERT INTO manual_assets(id,name,liquidity_bucket,valuation_amount,valuation_date,currency,source_note,memo,active,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(asset_id, name.strip(), bucket, amount, vdate, currency, source_note, memo, int(active), ts, ts),
|
||||
)
|
||||
row = conn.execute("SELECT * FROM manual_assets WHERE id=?", (asset_id,)).fetchone()
|
||||
return ok(manual_asset={**row_to_dict(row), "authority_source": "manual_assets"})
|
||||
|
||||
|
||||
def list_manual_assets(active_only: bool = True, db_path: str | None = None) -> dict[str, Any]:
|
||||
where = "WHERE active=1" if active_only else ""
|
||||
with db(db_path) as conn:
|
||||
rows = conn.execute(f"SELECT * FROM manual_assets {where} ORDER BY name").fetchall()
|
||||
assets = [{**row_to_dict(row), "authority_source": "manual_assets"} for row in rows]
|
||||
return ok(manual_assets=assets, count=len(assets), valuation_policy=_valuation_policy())
|
||||
|
||||
|
||||
def create_asset_snapshot(basis_date: str, source_type: str, source_id: str, name: str, amount: int | None, liquidity_bucket: str, valuation_source: str = "", source_note: str = "", memo: str = "", currency: str = "KRW", db_path: str | None = None) -> dict[str, Any]:
|
||||
if source_type not in {"account", "manual_asset"}:
|
||||
return fail("validation_error", "source_type must be one of ['account', 'manual_asset']")
|
||||
if not name.strip():
|
||||
return fail("validation_error", "snapshot item name is required")
|
||||
try:
|
||||
basis = normalize_date(basis_date, default_today=True)
|
||||
bucket = normalize_liquidity_bucket(liquidity_bucket)
|
||||
amount_int = int(amount) if amount is not None else None
|
||||
except Exception as e:
|
||||
return fail("validation_error", str(e))
|
||||
ts = now_iso()
|
||||
snapshot_id = new_id("snap")
|
||||
with db(db_path) as conn:
|
||||
conn.execute(
|
||||
"""INSERT INTO asset_snapshots(id,basis_date,source_type,source_id,name,amount,currency,liquidity_bucket,valuation_source,source_note,memo,created_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(snapshot_id, basis, source_type, source_id, name.strip(), amount_int, currency, bucket, valuation_source or source_type, source_note, memo, ts),
|
||||
)
|
||||
row = conn.execute("SELECT * FROM asset_snapshots WHERE id=?", (snapshot_id,)).fetchone()
|
||||
return ok(asset_snapshot=row_to_dict(row))
|
||||
|
||||
|
||||
def get_asset_snapshot_report(basis_date: str = "", db_path: str | None = None) -> dict[str, Any]:
|
||||
basis = normalize_date(basis_date, default_today=True) if basis_date else current_kst_date().isoformat()
|
||||
with db(db_path) as conn:
|
||||
snapshots = _snapshot_items(conn, basis)
|
||||
if snapshots:
|
||||
summary = _summarize_valuation_items(snapshots, [])
|
||||
return ok(
|
||||
basis_date=basis,
|
||||
source="asset_snapshots",
|
||||
snapshot_count=len(snapshots),
|
||||
**summary,
|
||||
)
|
||||
account_items, account_excluded, invalid_bucket_accounts = _account_valuation_items(conn)
|
||||
manual_items, manual_excluded = _manual_asset_valuation_items(conn)
|
||||
included = account_items + manual_items
|
||||
excluded = account_excluded + manual_excluded
|
||||
summary = _summarize_valuation_items(included, excluded)
|
||||
return ok(
|
||||
buckets=buckets,
|
||||
accounts=source_accounts,
|
||||
total_assets=total_assets,
|
||||
total_liabilities=total_liabilities,
|
||||
net_worth=net_worth,
|
||||
missing_balance_accounts=missing_balance_accounts,
|
||||
basis_date=basis,
|
||||
source="live_authority",
|
||||
snapshot_count=0,
|
||||
invalid_bucket_accounts=invalid_bucket_accounts,
|
||||
source="accounts.last_balance",
|
||||
**summary,
|
||||
)
|
||||
|
||||
|
||||
def get_net_worth_timeseries(start_date: str = "", end_date: str = "", months: int = 12, db_path: str | None = None) -> dict[str, Any]:
|
||||
try:
|
||||
months_int = max(1, min(int(months), 120))
|
||||
end_norm = normalize_date(end_date, default_today=True) if end_date else current_kst_date().isoformat()
|
||||
if start_date:
|
||||
start_norm = normalize_date(start_date)
|
||||
else:
|
||||
end_dt = date.fromisoformat(end_norm)
|
||||
start_month = end_dt.month - months_int + 1
|
||||
year = end_dt.year + (start_month - 1) // 12
|
||||
month = (start_month - 1) % 12 + 1
|
||||
start_norm = date(year, month, 1).isoformat()
|
||||
except Exception as e:
|
||||
return fail("validation_error", str(e))
|
||||
with db(db_path) as conn:
|
||||
basis_rows = conn.execute(
|
||||
"""SELECT DISTINCT basis_date FROM asset_snapshots
|
||||
WHERE basis_date>=? AND basis_date<=?
|
||||
ORDER BY basis_date ASC""",
|
||||
(start_norm, end_norm),
|
||||
).fetchall()
|
||||
points = []
|
||||
for row in basis_rows:
|
||||
basis = row["basis_date"]
|
||||
items = _snapshot_items(conn, basis)
|
||||
summary = _summarize_valuation_items(items, [])
|
||||
points.append(
|
||||
{
|
||||
"basis_date": basis,
|
||||
"total_assets": summary["total_assets"],
|
||||
"total_liabilities": summary["total_liabilities"],
|
||||
"net_worth": summary["net_worth"],
|
||||
"source": "asset_snapshots",
|
||||
"item_count": len(items),
|
||||
}
|
||||
)
|
||||
return ok(start_date=start_norm, end_date=end_norm, months=months_int, points=points, count=len(points), valuation_policy=_valuation_policy())
|
||||
|
||||
|
||||
def get_asset_liquidity_report(db_path: str | None = None) -> dict[str, Any]:
|
||||
"""Summarize assets/liabilities from shared valuation authority sources.
|
||||
|
||||
Existing REQ-005 keys are preserved. When manual_assets is empty, totals and
|
||||
source remain equivalent to the original accounts.last_balance behavior.
|
||||
"""
|
||||
with db(db_path) as conn:
|
||||
account_items, account_excluded, invalid_bucket_accounts = _account_valuation_items(conn)
|
||||
manual_items, manual_excluded = _manual_asset_valuation_items(conn)
|
||||
included = account_items + manual_items
|
||||
excluded = account_excluded + manual_excluded
|
||||
summary = _summarize_valuation_items(included, excluded)
|
||||
source_accounts = [item for item in included if item.get("source_type") == "account"]
|
||||
source = "accounts.last_balance" if not manual_items and not manual_excluded else "accounts.last_balance+manual_assets"
|
||||
return ok(
|
||||
buckets=summary["buckets"],
|
||||
accounts=source_accounts,
|
||||
manual_assets=manual_items,
|
||||
total_assets=summary["total_assets"],
|
||||
total_liabilities=summary["total_liabilities"],
|
||||
net_worth=summary["net_worth"],
|
||||
missing_balance_accounts=[
|
||||
{"id": item.get("id", ""), "name": item.get("name", ""), "liquidity_bucket": item.get("liquidity_bucket", "")}
|
||||
for item in account_excluded
|
||||
],
|
||||
missing_valuation_items=excluded,
|
||||
invalid_bucket_accounts=invalid_bucket_accounts,
|
||||
manual_asset_count=len(manual_items),
|
||||
included_items=included,
|
||||
excluded_items=excluded,
|
||||
valuation_policy=summary["valuation_policy"],
|
||||
source=source,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
|
||||
from jarca_ledger_mcp import service
|
||||
from jarca_ledger_mcp.db import init_db
|
||||
|
||||
|
||||
def test_asset_snapshot_schema_is_additive_and_idempotent(tmp_path):
|
||||
db_path = str(tmp_path / "ledger.sqlite3")
|
||||
init_db(db_path)
|
||||
init_db(db_path)
|
||||
|
||||
with sqlite3.connect(db_path) as conn:
|
||||
manual_cols = {row[1] for row in conn.execute("PRAGMA table_info(manual_assets)")}
|
||||
snapshot_cols = {row[1] for row in conn.execute("PRAGMA table_info(asset_snapshots)")}
|
||||
|
||||
assert {"id", "name", "valuation_amount", "valuation_date", "liquidity_bucket", "source_note", "memo", "active"} <= manual_cols
|
||||
assert {"id", "basis_date", "source_type", "source_id", "name", "amount", "liquidity_bucket", "valuation_source", "source_note", "memo"} <= snapshot_cols
|
||||
|
||||
|
||||
def test_manual_assets_snapshot_report_and_net_worth_formula(tmp_path):
|
||||
db_path = str(tmp_path / "ledger.sqlite3")
|
||||
cash = service.create_ledger_account("현금", last_balance=1_000_000, last_synced_at="2026-06-30", liquidity_bucket="cash", db_path=db_path)
|
||||
stocks = service.create_ledger_account("투자", type="investment", last_balance=2_000_000, liquidity_bucket="investment", db_path=db_path)
|
||||
missing = service.create_ledger_account("미평가 계좌", liquidity_bucket="deposit", db_path=db_path)
|
||||
assert cash["success"] and stocks["success"] and missing["success"]
|
||||
|
||||
house = service.create_manual_asset("전세보증금", 300_000_000, "2026-06-30", "real_estate", source_note="synthetic appraisal", db_path=db_path)
|
||||
loan = service.create_manual_asset("주담대", 120_000_000, "2026-06-30", "loan", source_note="synthetic loan balance", db_path=db_path)
|
||||
unvalued = service.create_manual_asset("평가대기 차량", None, "2026-06-30", "other", source_note="pending", db_path=db_path)
|
||||
assert house["success"] and loan["success"] and unvalued["success"]
|
||||
|
||||
manual = service.list_manual_assets(db_path=db_path)
|
||||
assert manual["success"] is True
|
||||
assert manual["count"] == 3
|
||||
assert {item["authority_source"] for item in manual["manual_assets"]} == {"manual_assets"}
|
||||
|
||||
report = service.get_asset_snapshot_report("2026-06-30", db_path=db_path)
|
||||
assert report["success"] is True
|
||||
assert report["source"] == "live_authority"
|
||||
assert report["valuation_policy"]["formula"] == "asset_buckets - loan"
|
||||
assert report["total_assets"] == 303_000_000
|
||||
assert report["total_liabilities"] == 120_000_000
|
||||
assert report["net_worth"] == 183_000_000
|
||||
assert {item["source_type"] for item in report["included_items"]} == {"account", "manual_asset"}
|
||||
assert any(item["source_type"] == "manual_asset" and item["name"] == "평가대기 차량" for item in report["excluded_items"])
|
||||
assert any(item["source_type"] == "account" and item["name"] == "미평가 계좌" for item in report["excluded_items"])
|
||||
|
||||
|
||||
def test_snapshot_rows_are_append_only_and_timeseries_uses_same_formula(tmp_path):
|
||||
db_path = str(tmp_path / "ledger.sqlite3")
|
||||
service.create_asset_snapshot("2026-05-31", "account", "acct-a", "현금", 1_000_000, "cash", source_note="synthetic", db_path=db_path)
|
||||
service.create_asset_snapshot("2026-05-31", "manual_asset", "manual-home", "부동산", 200_000_000, "real_estate", source_note="synthetic", db_path=db_path)
|
||||
service.create_asset_snapshot("2026-05-31", "manual_asset", "manual-loan", "대출", 80_000_000, "loan", source_note="synthetic", db_path=db_path)
|
||||
service.create_asset_snapshot("2026-06-30", "account", "acct-a", "현금", 1_500_000, "cash", source_note="synthetic", db_path=db_path)
|
||||
service.create_asset_snapshot("2026-06-30", "manual_asset", "manual-home", "부동산", 205_000_000, "real_estate", source_note="synthetic", db_path=db_path)
|
||||
service.create_asset_snapshot("2026-06-30", "manual_asset", "manual-loan", "대출", 78_000_000, "loan", source_note="synthetic", db_path=db_path)
|
||||
|
||||
may = service.get_asset_snapshot_report("2026-05-31", db_path=db_path)
|
||||
assert may["success"] is True
|
||||
assert may["source"] == "asset_snapshots"
|
||||
assert may["snapshot_count"] == 3
|
||||
assert may["net_worth"] == 121_000_000
|
||||
|
||||
series = service.get_net_worth_timeseries(start_date="2026-05-01", end_date="2026-06-30", db_path=db_path)
|
||||
assert series["success"] is True
|
||||
assert [point["basis_date"] for point in series["points"]] == ["2026-05-31", "2026-06-30"]
|
||||
assert [point["net_worth"] for point in series["points"]] == [121_000_000, 128_500_000]
|
||||
|
||||
|
||||
def test_asset_liquidity_report_empty_new_tables_preserves_req005_fallback(tmp_path):
|
||||
db_path = str(tmp_path / "ledger.sqlite3")
|
||||
service.create_ledger_account("현금성 계좌", last_balance=1_000_000, liquidity_bucket="cash", db_path=db_path)
|
||||
service.create_ledger_account("투자 계좌", last_balance=3_000_000, liquidity_bucket="investment", db_path=db_path)
|
||||
service.create_ledger_account("대출", last_balance=500_000, liquidity_bucket="loan", db_path=db_path)
|
||||
|
||||
report = service.get_asset_liquidity_report(db_path=db_path)
|
||||
assert report["success"] is True
|
||||
assert report["total_assets"] == 4_000_000
|
||||
assert report["total_liabilities"] == 500_000
|
||||
assert report["net_worth"] == 3_500_000
|
||||
assert report["manual_asset_count"] == 0
|
||||
assert report["source"] == "accounts.last_balance"
|
||||
assert [item["source_type"] for item in report["accounts"]] == ["account", "account", "account"]
|
||||
@@ -10,7 +10,7 @@ def test_health_check_initializes_db(tmp_path):
|
||||
db_path = tmp_path / "ledger.sqlite3"
|
||||
result = service.health_check(str(db_path))
|
||||
assert result["success"] is True
|
||||
assert result["schema_version"] == "8"
|
||||
assert result["schema_version"] == "9"
|
||||
assert db_path.exists()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user