feat: add persistent monthly settlement drafts and approved valuation selection
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
"""Real AppTest controls use synthetic SQLite, not mocked gateway results.
|
||||
|
||||
These are lower-level panel tests; existing dashboard authentication suites own
|
||||
entrypoint authentication acceptance.
|
||||
"""
|
||||
import pytest
|
||||
from streamlit.testing.v1 import AppTest
|
||||
|
||||
from jarca_ledger_mcp import service as s
|
||||
from jarca_ledger_mcp.db import init_db
|
||||
|
||||
|
||||
def control(elements, label):
|
||||
return next(e for e in elements if e.label == label)
|
||||
|
||||
|
||||
def app_for(tmp_path, monkeypatch, path):
|
||||
monkeypatch.setenv('JARCA_LEDGER_DB_PATH', str(path))
|
||||
appfile = tmp_path / 'panel.py'
|
||||
appfile.write_text('from dashboard.page_views.settlement_drafts import render_settlement_drafts\nrender_settlement_drafts()\n')
|
||||
app = AppTest.from_file(str(appfile)).run(timeout=20)
|
||||
assert not app.exception
|
||||
return app
|
||||
|
||||
|
||||
def test_panel_selects_after_date_evidence_approves_and_finalizes_actual_database(tmp_path, monkeypatch):
|
||||
path = str(tmp_path / 'panel.sqlite3')
|
||||
init_db(path)
|
||||
account = s.create_ledger_account('Synthetic retirement', type='investment', liquidity_bucket='investment', db_path=path)['account']['id']
|
||||
value = s.register_account_valuation(account, '2026-10-05', 100, valuation_kind='total_value', verification_status='verified', idempotency_key='future', db_path=path)['valuation']['id']
|
||||
app = app_for(tmp_path, monkeypatch, path)
|
||||
control(app.button, '초안 생성 / 정정 초안').click().run(timeout=20)
|
||||
control(app.text_input, '대상 ID').set_value(account)
|
||||
control(app.text_input, '평가자료 ID (비우면 해제)').set_value(value)
|
||||
control(app.selectbox, '선택 방식').select('manual_substitute')
|
||||
control(app.text_input, '선택 사유').set_value('Synthetic late statement')
|
||||
control(app.button, '저장 / 지정 / 포함·제외').click().run(timeout=20)
|
||||
assert not app.exception and not app.error
|
||||
control(app.button, '검토 / 새로고침 대상').click().run(timeout=20)
|
||||
assert any('대체 평가자료를 포함한 월정산' in w.value for w in app.warning)
|
||||
control(app.text_input, '승인 사유').set_value('Synthetic reviewer accepted date')
|
||||
control(app.text_input, '승인 근거').set_value('Synthetic reference')
|
||||
control(app.checkbox, '대체자료 승인').check()
|
||||
control(app.button, '검토 결과 승인').click().run(timeout=20)
|
||||
assert not app.exception and not app.error
|
||||
control(app.button, '최종 마감').click().run(timeout=20)
|
||||
assert not app.exception and not app.error
|
||||
# AppTest 1.58 retains pre-rerun controls in the click-run delta tree,
|
||||
# including in a flat synthetic app. Check immediate history here;
|
||||
# control absence below uses a fresh instance, not a claim about live DOM.
|
||||
assert any('불변 이력' in e.value for e in app.markdown)
|
||||
drafts = s.list_settlement_drafts('2026-09', db_path=path)['drafts']
|
||||
assert len(drafts) == 1 and drafts[0]['status'] == 'completed'
|
||||
final = s.get_settlement_draft(drafts[0]['id'], db_path=path)['final_response']
|
||||
assert final['totals']['net_worth'] == 100 and final['contains_substitutions']
|
||||
assert final['items'][0]['selection_reason'] == 'Synthetic late statement'
|
||||
# Reusing the stale tree also raises KeyError for removed selectbox state.
|
||||
# Reopen the real panel against the same completed SQLite draft instead.
|
||||
app = app_for(tmp_path, monkeypatch, path)
|
||||
assert not app.exception and '불변 이력' in ' '.join(e.value for e in app.markdown)
|
||||
assert not any(e.label == '최종 마감' for e in app.button)
|
||||
assert not any(e.label == '검토 결과 승인' for e in app.button)
|
||||
assert not any('현재 승인 대상 fingerprint:' in e.value for e in app.caption)
|
||||
|
||||
|
||||
def test_panel_valuation_listing_shows_real_selected_evidence_ids(tmp_path, monkeypatch):
|
||||
path = str(tmp_path / 'list.sqlite3')
|
||||
init_db(path)
|
||||
account = s.create_ledger_account('Synthetic cash', db_path=path)['account']['id']
|
||||
vid = s.register_account_valuation(account, '2026-09-30', 10, verification_status='verified', idempotency_key='value', db_path=path)['valuation']['id']
|
||||
app = app_for(tmp_path, monkeypatch, path)
|
||||
control(app.button, '초안 생성 / 정정 초안').click().run(timeout=20)
|
||||
control(app.text_input, '대상 ID').set_value(account)
|
||||
control(app.button, '평가자료 조회').click().run(timeout=20)
|
||||
assert not app.exception
|
||||
assert vid in ' '.join(e.value for e in app.json)
|
||||
|
||||
|
||||
def test_panel_supports_investment_total_and_explicit_loan_definition(tmp_path, monkeypatch):
|
||||
path = str(tmp_path / 'register.sqlite3')
|
||||
init_db(path)
|
||||
loan = s.create_ledger_account('Synthetic loan', type='loan', liquidity_bucket='loan', db_path=path)['account']['id']
|
||||
app = app_for(tmp_path, monkeypatch, path)
|
||||
control(app.button, '초안 생성 / 정정 초안').click().run(timeout=20)
|
||||
assert {'total_value', 'cash_balance'} <= set(control(app.selectbox, '평가 종류').options)
|
||||
control(app.text_input, '평가 등록 계좌 ID').set_value(loan)
|
||||
control(app.text_input, '평가일 YYYY-MM-DD').set_value('2026-09-30')
|
||||
control(app.number_input, '금액').set_value(40)
|
||||
control(app.selectbox, '평가 종류').select('loan_balance')
|
||||
control(app.selectbox, '대표 부채 정의').select('principal_only')
|
||||
control(app.selectbox, '확인 상태').select('verified')
|
||||
control(app.text_input, '등록 idempotency key').set_value('synthetic-loan-value')
|
||||
control(app.button, '평가자료 등록').click().run(timeout=20)
|
||||
assert not app.exception and not app.error
|
||||
values = s.list_account_valuations(loan, db_path=path)['valuations']
|
||||
assert len(values) == 1 and values[0]['representative_liability_definition'] == 'principal_only'
|
||||
assert values[0]['amount'] == 40
|
||||
|
||||
|
||||
def test_panel_edit_invalidates_cached_review_before_approval(tmp_path, monkeypatch):
|
||||
path = str(tmp_path / 'edit.sqlite3')
|
||||
init_db(path)
|
||||
account = s.create_ledger_account('Synthetic cash', db_path=path)['account']['id']
|
||||
s.register_account_valuation(account, '2026-09-30', 10, verification_status='verified', idempotency_key='value', db_path=path)
|
||||
app = app_for(tmp_path, monkeypatch, path)
|
||||
control(app.button, '초안 생성 / 정정 초안').click().run(timeout=20)
|
||||
control(app.button, '검토 / 새로고침 대상').click().run(timeout=20)
|
||||
draft = s.list_settlement_drafts('2026-09', db_path=path)['drafts'][0]
|
||||
key = f"review_result_{draft['id']}"
|
||||
old = app.session_state[key]['review_fingerprint']
|
||||
assert any(old in e.value and '현재 승인 대상' in e.value for e in app.caption)
|
||||
control(app.text_input, '초안 메모 저장').set_value('Synthetic edited memo')
|
||||
control(app.button, '저장 / 지정 / 포함·제외').click().run(timeout=20)
|
||||
assert not app.exception and not app.error
|
||||
assert not any(old in e.value and '현재 승인 대상' in e.value for e in app.caption)
|
||||
assert not any(e.label == '검토 결과 승인' for e in app.button)
|
||||
assert not any(e.label == '최종 마감' for e in app.button)
|
||||
with pytest.raises(KeyError):
|
||||
app.session_state[key]
|
||||
current = s.get_settlement_draft(draft['id'], db_path=path)['draft']
|
||||
assert current['version'] == draft['version'] + 1 and current['memo'] == 'Synthetic edited memo'
|
||||
control(app.button, '검토 / 새로고침 대상').click().run(timeout=20)
|
||||
control(app.text_input, '승인 사유').set_value('Synthetic fresh review')
|
||||
control(app.text_input, '승인 근거').set_value('Synthetic reference')
|
||||
control(app.button, '검토 결과 승인').click().run(timeout=20)
|
||||
assert not app.exception and not app.error
|
||||
control(app.button, '최종 마감').click().run(timeout=20)
|
||||
assert not app.exception and not app.error
|
||||
assert s.get_settlement_draft(draft['id'], db_path=path)['draft']['status'] == 'completed'
|
||||
|
||||
|
||||
def test_panel_correction_invalidates_review_without_registration_account_input(tmp_path, monkeypatch):
|
||||
path = str(tmp_path / 'correction.sqlite3')
|
||||
init_db(path)
|
||||
account = s.create_ledger_account('Synthetic cash', db_path=path)['account']['id']
|
||||
vid = s.register_account_valuation(account, '2026-09-30', 10, verification_status='verified', idempotency_key='value', db_path=path)['valuation']['id']
|
||||
app = app_for(tmp_path, monkeypatch, path)
|
||||
control(app.button, '초안 생성 / 정정 초안').click().run(timeout=20)
|
||||
control(app.button, '검토 / 새로고침 대상').click().run(timeout=20)
|
||||
draft = s.list_settlement_drafts('2026-09', db_path=path)['drafts'][0]
|
||||
old = app.session_state[f"review_result_{draft['id']}"]['review_fingerprint']
|
||||
assert control(app.text_input, '평가 등록 계좌 ID').value == ''
|
||||
control(app.text_input, '정정할 valuation ID').set_value(vid)
|
||||
control(app.number_input, '정정 금액').set_value(20)
|
||||
control(app.text_input, '평가 정정 사유').set_value('Synthetic correction')
|
||||
control(app.text_input, '평가 정정 키').set_value('synthetic-child')
|
||||
control(app.selectbox, '정정 확인 상태').select('verified')
|
||||
control(app.button, '평가자료 정정').click().run(timeout=20)
|
||||
assert not app.exception and not app.error
|
||||
assert not any(old in e.value and '현재 승인 대상' in e.value for e in app.caption)
|
||||
assert not any(e.label == '검토 결과 승인' for e in app.button)
|
||||
detail = s.get_settlement_draft(draft['id'], db_path=path)
|
||||
assert detail['items'][0]['review_needed'] and detail['items'][0]['valuation_id'] == vid
|
||||
assert len(s.list_account_valuations(account, db_path=path)['valuations']) == 2
|
||||
Reference in New Issue
Block a user