from __future__ import annotations import sqlite3 from streamlit.testing.v1 import AppTest from jarca_ledger_mcp.application.monthly_settlement_service import ( create_monthly_settlement, list_account_snapshots_for_settlement, ) from jarca_ledger_mcp.infrastructure.sqlite.migrations import init_db from dashboard.page_views.monthly_settlement import _account_evaluation_totals, _format_evaluation_total def test_account_evaluation_totals_native_currency_null_and_zero(): result = _account_evaluation_totals([ {"account_id": "a1", "snapshot_id": "s1", "amount": 0, "currency": "KRW"}, {"account_id": "a2", "snapshot_id": "s2", "amount": -2500, "currency": "USD"}, {"account_id": "a3", "snapshot_id": "s3", "amount": None, "currency": "KRW"}, {"account_id": "a4", "snapshot_id": None, "amount": 999999, "currency": "KRW"}, ]) assert result["totals"] == {"KRW": 0, "USD": -2500} assert result["unknown_count"] == 1 assert result["registered_count"] == 3 assert result["unregistered_count"] == 1 assert _format_evaluation_total("KRW", 0) == "0원" assert _format_evaluation_total("USD", -2500) == "-2,500 USD" def test_account_evaluation_totals_missing_currency_is_separate(): result = _account_evaluation_totals([ {"account_id": "a1", "snapshot_id": "s1", "amount": 100, "currency": None}, {"account_id": "a2", "snapshot_id": None, "amount": None, "currency": "KRW"}, ]) assert result["totals"] == {"미지정": 100} def _account(conn, ident, name, currency="KRW"): conn.execute( "INSERT INTO accounts(id,name,currency,created_at,updated_at) VALUES(?,?,?,?,?)", (ident, name, currency, "2026-10-01T00:00:00Z", "2026-10-01T00:00:00Z"), ) def _snapshot(conn, ident, settlement, account, name, amount, currency="KRW"): conn.execute( """INSERT INTO account_snapshot (id,basis_date,source_type,source_id,name,amount,currency,monthly_settlement_id,created_at) VALUES(?,?,?,?,?,?,?,?,?)""", (ident, "2026-10-31", "account", account, name, amount, currency, settlement, "2026-10-31T00:00:00Z"), ) def test_draft_uses_all_accounts_and_exact_monthly_snapshot(tmp_path): path = tmp_path / "ledger.sqlite3" init_db(str(path)) settlement = create_monthly_settlement("2026-10", db_path=str(path))["settlement_id"] with sqlite3.connect(path) as conn: _account(conn, "a1", "계좌1") _account(conn, "a2", "계좌2") _snapshot(conn, "s1", settlement, "a1", "계좌1", 0) _snapshot(conn, "other", "other-month", "a2", "계좌2", 99) conn.commit() result = list_account_snapshots_for_settlement(settlement, db_path=str(path)) assert [(x["source_id"], x["amount"], x["display_status"]) for x in result["snapshots"]] == [("a1", 0, "확인"), ("a2", None, "미등록")] assert result["count"] == 2 and result["registered_count"] == 1 and result["unregistered_count"] == 1 def test_closed_uses_historical_rows_only_and_preserves_null_zero_and_duplicate(tmp_path): path = tmp_path / "ledger.sqlite3" init_db(str(path)) settlement = create_monthly_settlement("2026-10", db_path=str(path))["settlement_id"] with sqlite3.connect(path) as conn: _account(conn, "old", "과거계좌", "USD") _snapshot(conn, "s-null", settlement, "old", "보존이름", None, "USD") _snapshot(conn, "s-zero", settlement, "old", "보존이름", 0, "USD") conn.execute("UPDATE monthly_settlement SET status='closed', approved_by='test', approved_at='2026-10-31T00:00:00Z', approved_version=1, approved_fingerprint='fp', closed_by='test', closed_at='2026-10-31T00:00:00Z' WHERE id=?", (settlement,)) _account(conn, "new", "신규계좌") conn.commit() result = list_account_snapshots_for_settlement(settlement, db_path=str(path)) assert result["closed"] is True assert [x["amount"] for x in result["snapshots"]] == [None, 0] assert [x["display_status"] for x in result["snapshots"]] == ["미확인", "확인"] assert all(x["name"] == "보존이름" for x in result["snapshots"]) assert all(x["source_id"] != "new" for x in result["snapshots"]) def test_monthly_renderer_opens_real_registration_dialog_from_actual_button(monkeypatch): from jarca_ledger_mcp.adapters.streamlit import overview_gateway, settlement_gateway monkeypatch.setattr( overview_gateway, "get_monthly_settlement_summary", lambda *args, **kwargs: { "success": True, "month": "2026-10", "selected_month": "2026-10", "basis_date": "2026-10-31", "actual_income": 0, "actual_expense": 0, "net_cashflow": 0, "reports": {"cashflow": {}, "budget": {}, "assets": {}}, }, ) monkeypatch.setattr( settlement_gateway, "get_monthly_settlement_record", lambda *args, **kwargs: {"success": True, "settlement": {"id": "ms-1", "month": "2026-10", "status": "draft"}}, ) monkeypatch.setattr( settlement_gateway, "list_account_snapshots_for_settlement", lambda *args, **kwargs: { "success": True, "closed": False, "count": 3, "registered_count": 2, "unregistered_count": 1, "snapshots": [ {"account_id": "acct-1", "account_name": "주계좌", "amount": 1630800, "currency": "KRW", "basis_date": "2026-10-31", "display_status": "확인", "snapshot_id": "snap-1", "account_type": "bank", "account_liquidity_bucket": "bank"}, {"account_id": "acct-2", "account_name": "임시 주택담보대출", "amount": 1562300, "currency": "KRW", "basis_date": "2026-10-31", "display_status": "확인", "snapshot_id": "snap-2", "account_type": "loan", "account_liquidity_bucket": "loan"}, {"account_id": "acct-3", "account_name": "보조계좌", "amount": None, "currency": "USD", "basis_date": None, "display_status": "미등록", "snapshot_id": None}, ], }, ) app = AppTest.from_string( "import streamlit as st\n" "st.query_params.update(page='monthly_settlement', id='ms-1')\n" "from dashboard.page_views.monthly_settlement import page_monthly_settlement\n" "page_monthly_settlement(db_path='synthetic-unused.sqlite3')\n", default_timeout=30, ).run() assert not app.exception assert any(metric.label == "자산 합계 (KRW)" and metric.value == "1,630,800원" for metric in app.metric) assert any(metric.label == "부채 합계 (KRW)" and metric.value == "1,562,300원" for metric in app.metric) assert any(metric.label == "순자산 (KRW)" and metric.value == "68,500원" for metric in app.metric) metric_columns = [column for column in app.columns if column.metric] assert len(metric_columns) == 3 assert [column.metric[0].label for column in metric_columns] == [ "자산 합계 (KRW)", "부채 합계 (KRW)", "순자산 (KRW)", ] assert any("평가등록 2 / 전체 3" in caption.value and "미등록 1" in caption.value for caption in app.caption) assert not [expander for expander in app.expander if expander.label == "필요 메모/근거"] account_buttons = [button for button in app.button if button.label == "보조계좌"] assert len(account_buttons) == 1 assert not [button for button in app.button if button.label == "평가 등록"] dialog = account_buttons[0].click().run() assert not dialog.exception inputs = {widget.label: widget for widget in dialog.text_input} assert inputs["계정"].value == "보조계좌" assert inputs["계정"].disabled is True assert inputs["통화"].value == "USD" assert dialog.date_input[0].value.isoformat() == "2026-10-31" assert "평가금액 (정수)" in inputs assert dialog.text_area[0].label == "메모 (선택)" def test_monthly_renderer_preserves_ledger_projection_and_source_choices(monkeypatch): from jarca_ledger_mcp.adapters.streamlit import overview_gateway, settlement_gateway monkeypatch.setattr( overview_gateway, "get_monthly_settlement_summary", lambda *args, **kwargs: { "success": True, "month": "2026-10", "selected_month": "2026-10", "basis_date": "2026-10-31", "reports": {"cashflow": {}, "budget": {}, "assets": {}}, }, ) monkeypatch.setattr( settlement_gateway, "get_monthly_settlement_record", lambda *args, **kwargs: {"success": True, "settlement": {"id": "ms-10", "month": "2026-10", "status": "draft"}}, ) monkeypatch.setattr( settlement_gateway, "list_account_snapshots_for_settlement", lambda *args, **kwargs: { "success": True, "closed": False, "count": 1, "registered_count": 0, "unregistered_count": 1, "snapshots": [{ "account_id": "acct-10", "account_name": "실제계정10", "amount": None, "currency": "KRW", "basis_date": None, "display_status": "미등록", "snapshot_id": None, "previous_ledger": {"id": "prev-10", "transaction_date": "2026-10-30", "balance_after": 1234, "currency": "KRW"}, "next_ledger": {"id": "next-10", "transaction_date": "2026-11-01", "balance_after": 2345, "currency": "KRW"}, }], }, ) app = AppTest.from_string( "import streamlit as st\n" "st.query_params.update(page='monthly_settlement', id='ms-10')\n" "from dashboard.page_views.monthly_settlement import page_monthly_settlement\n" "page_monthly_settlement(db_path='synthetic-unused.sqlite3')\n", default_timeout=30, ).run() assert not app.exception dialog = [button for button in app.button if button.label == "실제계정10"][0].click().run() assert not dialog.exception source = dialog.selectbox[0] assert len(source.options) == 3 assert source.value.startswith("이전 기록") inputs = {widget.label: widget for widget in dialog.text_input} assert inputs["평가금액 (정수)"].value == "1234" assert inputs["평가금액 (정수)"].disabled is True assert any(option.startswith("이후 기록") for option in source.options) assert inputs["평가금액 (정수)"].disabled is True def test_monthly_renderer_mixed_rows_keeps_snapshot_id_for_unlink(monkeypatch): from jarca_ledger_mcp.adapters.streamlit import overview_gateway, settlement_gateway monkeypatch.setattr(overview_gateway, "get_monthly_settlement_summary", lambda *args, **kwargs: { "success": True, "month": "2026-10", "selected_month": "2026-10", "basis_date": "2026-10-31", "reports": {"cashflow": {}, "budget": {}, "assets": {}}, }) monkeypatch.setattr(settlement_gateway, "get_monthly_settlement_record", lambda *args, **kwargs: { "success": True, "settlement": {"id": "ms-mixed", "month": "2026-10", "status": "draft"}, }) monkeypatch.setattr(settlement_gateway, "list_account_snapshots_for_settlement", lambda *args, **kwargs: { "success": True, "closed": False, "status": "draft", "count": 2, "registered_count": 1, "unregistered_count": 1, "snapshots": [ {"account_id": "acct-1", "account_name": "등록계정", "amount": 100, "currency": "KRW", "basis_date": "2026-10-31", "display_status": "확인", "snapshot_id": "snap-1"}, {"account_id": "acct-2", "account_name": "미등록계정", "amount": None, "currency": "USD", "basis_date": None, "display_status": "미등록", "snapshot_id": None}, ], }) app = AppTest.from_string( "import streamlit as st\n" "st.query_params.update(page='monthly_settlement', id='ms-mixed')\n" "from dashboard.page_views.monthly_settlement import page_monthly_settlement\n" "page_monthly_settlement(db_path='synthetic-unused.sqlite3')\n", default_timeout=30, ).run() assert not app.exception unlink = [button for button in app.button if button.label == "×"] assert len(unlink) == 1 assert unlink[0].help == "연결 해제" dialog = unlink[0].click().run() assert not dialog.exception assert any("등록계정" in markdown.value for markdown in dialog.markdown)