Implement BAT-009 manual asset CRUD and snapshot close
This commit is contained in:
@@ -64,6 +64,38 @@
|
||||
"success": "bool",
|
||||
"account": {"type": "dict", "required": {"id": "str", "name": "str", "type": "str", "active": "bool"}}
|
||||
}
|
||||
},
|
||||
"manual_asset_mutation": {
|
||||
"required": {
|
||||
"success": "bool",
|
||||
"manual_asset": {"type": "dict", "required": {"id": "str", "name": "str", "active": "bool", "valuation_date": "str", "liquidity_bucket": "str", "source_note": "str"}}
|
||||
}
|
||||
},
|
||||
"asset_snapshot_preview": {
|
||||
"required": {
|
||||
"success": "bool",
|
||||
"basis_date": "str",
|
||||
"source": "str",
|
||||
"is_closed_snapshot": "bool",
|
||||
"total_assets": "int",
|
||||
"total_liabilities": "int",
|
||||
"net_worth": "int",
|
||||
"warnings": "list"
|
||||
}
|
||||
},
|
||||
"asset_snapshot_close": {
|
||||
"required": {
|
||||
"success": "bool",
|
||||
"basis_date": "str",
|
||||
"source": "str",
|
||||
"is_closed_snapshot": "bool",
|
||||
"snapshot_run_id": "str",
|
||||
"is_correction": "bool",
|
||||
"inserted_count": "int",
|
||||
"total_assets": "int",
|
||||
"total_liabilities": "int",
|
||||
"net_worth": "int"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ def test_asset_snapshot_schema_is_additive_and_idempotent(tmp_path):
|
||||
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
|
||||
assert {"id", "basis_date", "source_type", "source_id", "name", "amount", "liquidity_bucket", "valuation_source", "source_note", "memo", "snapshot_run_id", "correction_of_run_id", "correction_reason", "is_correction", "closed_at"} <= snapshot_cols
|
||||
|
||||
|
||||
def test_manual_assets_snapshot_report_and_net_worth_formula(tmp_path):
|
||||
@@ -83,3 +83,63 @@ def test_asset_liquidity_report_empty_new_tables_preserves_req005_fallback(tmp_p
|
||||
assert report["manual_asset_count"] == 0
|
||||
assert report["source"] == "accounts.last_balance"
|
||||
assert [item["source_type"] for item in report["accounts"]] == ["account", "account", "account"]
|
||||
|
||||
|
||||
def test_manual_asset_update_deactivate_preview_and_close_correction_policy(tmp_path):
|
||||
db_path = str(tmp_path / "ledger.sqlite3")
|
||||
account = service.create_ledger_account("월말 현금", last_balance=1_000_000, last_synced_at="2026-06-30", liquidity_bucket="cash", db_path=db_path)
|
||||
asset = service.create_manual_asset("전세보증금", 300_000_000, "2026-06-30", "real_estate", source_note="synthetic", db_path=db_path)
|
||||
loan = service.create_manual_asset("대출", 120_000_000, "2026-06-30", "loan", source_note="synthetic", db_path=db_path)
|
||||
dormant = service.create_manual_asset("해지 예금", 5_000_000, "2026-06-30", "deposit", source_note="synthetic", db_path=db_path)
|
||||
assert account["success"] and asset["success"] and loan["success"] and dormant["success"]
|
||||
|
||||
updated = service.update_manual_asset(asset["manual_asset"]["id"], valuation_amount=310_000_000, memo="updated synthetic", db_path=db_path)
|
||||
assert updated["success"] is True
|
||||
assert updated["manual_asset"]["valuation_amount"] == 310_000_000
|
||||
deactivated = service.deactivate_manual_asset(dormant["manual_asset"]["id"], db_path=db_path)
|
||||
assert deactivated["success"] is True
|
||||
assert deactivated["manual_asset"]["active"] is False
|
||||
|
||||
before = service.get_ledger_db_status(db_path, include_hash=False, include_integrity=False)["table_counts"].get("asset_snapshots", 0)
|
||||
preview = service.preview_asset_snapshot_close("2026-06-30", db_path=db_path)
|
||||
after = service.get_ledger_db_status(db_path, include_hash=False, include_integrity=False)["table_counts"].get("asset_snapshots", 0)
|
||||
assert preview["success"] is True
|
||||
assert preview["source"] == "live_authority"
|
||||
assert preview["is_closed_snapshot"] is False
|
||||
assert before == after
|
||||
assert preview["total_assets"] == 311_000_000
|
||||
assert preview["total_liabilities"] == 120_000_000
|
||||
assert preview["net_worth"] == 191_000_000
|
||||
|
||||
closed = service.close_asset_snapshot("2026-06-30", source_note="synthetic close", db_path=db_path)
|
||||
assert closed["success"] is True
|
||||
assert closed["source"] == "snapshot"
|
||||
assert closed["is_closed_snapshot"] is True
|
||||
assert closed["is_correction"] is False
|
||||
assert closed["inserted_count"] == 3
|
||||
first_run = closed["snapshot_run_id"]
|
||||
|
||||
report = service.get_asset_snapshot_report("2026-06-30", db_path=db_path)
|
||||
assert report["success"] is True
|
||||
assert report["is_closed_snapshot"] is True
|
||||
assert report["net_worth"] == closed["net_worth"]
|
||||
assert report["close_metadata"]["snapshot_run_id"] == first_run
|
||||
|
||||
blocked = service.close_asset_snapshot("2026-06-30", db_path=db_path)
|
||||
assert blocked["success"] is False
|
||||
assert blocked["error_code"] == "validation_error"
|
||||
|
||||
service.update_manual_asset(loan["manual_asset"]["id"], valuation_amount=110_000_000, db_path=db_path)
|
||||
corrected = service.close_asset_snapshot("2026-06-30", source_note="synthetic correction", correction_reason="synthetic correction test", db_path=db_path)
|
||||
assert corrected["success"] is True
|
||||
assert corrected["is_correction"] is True
|
||||
assert corrected["correction_of_run_id"] == first_run
|
||||
assert corrected["net_worth"] == 201_000_000
|
||||
|
||||
corrected_report = service.get_asset_snapshot_report("2026-06-30", db_path=db_path)
|
||||
assert corrected_report["net_worth"] == 201_000_000
|
||||
assert corrected_report["close_metadata"]["snapshot_run_id"] == corrected["snapshot_run_id"]
|
||||
|
||||
with sqlite3.connect(db_path) as conn:
|
||||
row_count = conn.execute("SELECT COUNT(*) FROM asset_snapshots WHERE basis_date='2026-06-30'").fetchone()[0]
|
||||
assert row_count == 6
|
||||
|
||||
@@ -118,6 +118,18 @@ def test_list_report_and_mutation_success_envelope_contract(tmp_path: Path) -> N
|
||||
assert mutation["success"] is True
|
||||
_assert_subset_schema(mutation, contract["mutation_success_envelope"], "mutation_success_envelope")
|
||||
|
||||
manual_asset = service.create_manual_asset("Synthetic Deposit", 2_000_000, "2026-06-30", "deposit", source_note="synthetic contract", db_path=db_path)
|
||||
assert manual_asset["success"] is True
|
||||
_assert_subset_schema(manual_asset, contract["manual_asset_mutation"], "manual_asset_mutation")
|
||||
|
||||
preview = service.preview_asset_snapshot_close("2026-06-30", db_path=db_path)
|
||||
assert preview["success"] is True
|
||||
_assert_subset_schema(preview, contract["asset_snapshot_preview"], "asset_snapshot_preview")
|
||||
|
||||
close = service.close_asset_snapshot("2026-06-30", source_note="synthetic contract close", db_path=db_path)
|
||||
assert close["success"] is True
|
||||
_assert_subset_schema(close, contract["asset_snapshot_close"], "asset_snapshot_close")
|
||||
|
||||
accounts = service.list_ledger_accounts(db_path=db_path)
|
||||
assert accounts["success"] is True
|
||||
assert accounts["count"] >= 2
|
||||
|
||||
@@ -47,6 +47,7 @@ def test_streamlit_apptest_core_pages_use_synthetic_fixture_db(tmp_path, monkeyp
|
||||
app_path = Path(__file__).resolve().parents[1] / "dashboard" / "app.py"
|
||||
page_expectations = {
|
||||
"요약": "가계부 운영 대시보드",
|
||||
"자산/마감": "월말 스냅샷 Preview / Close",
|
||||
"분류 규칙": "분류 규칙",
|
||||
"확인필요 큐": "KB↔토스뱅크 확인필요 큐",
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ from jarca_ledger_mcp import service
|
||||
def test_ledger_category_create_list_delete_reactivate(tmp_path):
|
||||
db_path = str(tmp_path / "ledger.sqlite3")
|
||||
health = service.health_check(db_path)
|
||||
assert health["schema_version"] == "8"
|
||||
assert health["schema_version"] == "10"
|
||||
|
||||
created = service.create_ledger_category("반려동물", "사료", sort_order=250, memo="test", db_path=db_path)
|
||||
assert created["success"] is True
|
||||
|
||||
@@ -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"] == "9"
|
||||
assert result["schema_version"] == "10"
|
||||
assert db_path.exists()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user