251 lines
12 KiB
Python
251 lines
12 KiB
Python
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)
|