Fix loan valuation definition bypasses
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
import sqlite3
|
||||
|
||||
from jarca_ledger_mcp import service
|
||||
from jarca_ledger_mcp.application.account_valuation_service import record_account_valuation_candidate
|
||||
from jarca_ledger_mcp.application.imports_service import _record_import_valuation_candidate
|
||||
|
||||
|
||||
def test_loan_balance_requires_representative_liability_definition(tmp_path):
|
||||
@@ -91,3 +95,104 @@ def test_correction_replays_by_evidence_and_conflicts_on_changed_payload(tmp_pat
|
||||
assert changed["success"] is False
|
||||
assert changed["error_code"] == "correction_conflict"
|
||||
assert service.list_account_valuations(account, db_path=path)["count"] == 2
|
||||
|
||||
|
||||
def test_direct_loan_candidate_requires_liability_definition_and_writes_nothing(tmp_path):
|
||||
path = str(tmp_path / "ledger.sqlite3")
|
||||
loan = service.create_ledger_account("대출", type="loan", db_path=path)["account"]["id"]
|
||||
|
||||
with sqlite3.connect(path) as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
rejected = record_account_valuation_candidate(
|
||||
conn, loan, "2026-06-30", 100, "KRW", "import", "source", "candidate", "memo",
|
||||
)
|
||||
|
||||
assert rejected["success"] is False
|
||||
assert rejected["error_code"] == "validation_error"
|
||||
assert service.list_account_valuations(loan, db_path=path)["count"] == 0
|
||||
|
||||
|
||||
def test_import_loan_candidate_requires_liability_definition_and_writes_nothing(tmp_path):
|
||||
path = str(tmp_path / "ledger.sqlite3")
|
||||
loan = service.create_ledger_account("대출", type="loan", db_path=path)["account"]["id"]
|
||||
|
||||
with sqlite3.connect(path) as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
assert _record_import_valuation_candidate(conn, loan, 100, "2026-06-30", "source-hash") is None
|
||||
|
||||
assert service.list_account_valuations(loan, db_path=path)["count"] == 0
|
||||
|
||||
|
||||
def test_dated_loan_create_and_historical_update_preserve_current_cache_without_valuations(tmp_path):
|
||||
path = str(tmp_path / "ledger.sqlite3")
|
||||
created = service.create_ledger_account(
|
||||
"대출", type="loan", last_balance=900, last_synced_at="2026-06-30",
|
||||
last_balance_basis_date="2026-06-30", db_path=path,
|
||||
)
|
||||
loan = created["account"]["id"]
|
||||
assert created["account"]["last_balance"] == 900
|
||||
assert created["account"]["last_synced_at"] == "2026-06-30"
|
||||
assert service.list_account_valuations(loan, db_path=path)["count"] == 0
|
||||
|
||||
updated = service.update_ledger_account(
|
||||
loan, last_balance=700, last_synced_at="2026-06-01",
|
||||
last_balance_basis_date="2026-06-01", db_path=path,
|
||||
)
|
||||
|
||||
assert updated["account"]["last_balance"] == 900
|
||||
assert updated["account"]["last_synced_at"] == "2026-06-30"
|
||||
assert service.list_account_valuations(loan, db_path=path)["count"] == 0
|
||||
|
||||
|
||||
def test_loan_cash_balance_and_total_value_are_rejected_without_writes(tmp_path):
|
||||
path = str(tmp_path / "ledger.sqlite3")
|
||||
loan = service.create_ledger_account("대출", type="loan", db_path=path)["account"]["id"]
|
||||
|
||||
for valuation_kind in ("cash_balance", "total_value"):
|
||||
rejected = service.register_account_valuation(
|
||||
loan, "2026-06-30", 100, valuation_kind=valuation_kind, db_path=path,
|
||||
)
|
||||
assert rejected["success"] is False
|
||||
assert rejected["error_code"] == "validation_error"
|
||||
|
||||
assert service.list_account_valuations(loan, db_path=path)["count"] == 0
|
||||
|
||||
|
||||
def test_non_loan_candidate_still_succeeds_and_reuses_idempotently(tmp_path):
|
||||
path = str(tmp_path / "ledger.sqlite3")
|
||||
account = service.create_ledger_account("계좌", db_path=path)["account"]["id"]
|
||||
|
||||
with sqlite3.connect(path) as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
first = record_account_valuation_candidate(
|
||||
conn, account, "2026-06-30", 100, "KRW", "import", "source", "candidate", "memo",
|
||||
)
|
||||
replay = record_account_valuation_candidate(
|
||||
conn, account, "2026-06-30", 100, "KRW", "import", "source", "candidate", "memo",
|
||||
)
|
||||
|
||||
assert first["success"] is True
|
||||
assert first["reused"] is False
|
||||
assert replay["success"] is True
|
||||
assert replay["reused"] is True
|
||||
assert replay["valuation"]["id"] == first["valuation"]["id"]
|
||||
assert service.list_account_valuations(account, db_path=path)["count"] == 1
|
||||
|
||||
|
||||
def test_candidate_invalid_amount_returns_controlled_error_with_existing_row(tmp_path):
|
||||
path = str(tmp_path / "ledger.sqlite3")
|
||||
account = service.create_ledger_account("계좌", db_path=path)["account"]["id"]
|
||||
|
||||
with sqlite3.connect(path) as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
first = record_account_valuation_candidate(
|
||||
conn, account, "2026-06-30", 100, "KRW", "import", "source", "candidate", "memo",
|
||||
)
|
||||
rejected = record_account_valuation_candidate(
|
||||
conn, account, "2026-06-30", "not-an-integer", "KRW", "import", "source", "candidate", "memo",
|
||||
)
|
||||
|
||||
assert first["success"] is True
|
||||
assert rejected["success"] is False
|
||||
assert rejected["error_code"] == "validation_error"
|
||||
assert service.list_account_valuations(account, db_path=path)["count"] == 1
|
||||
|
||||
@@ -104,6 +104,30 @@ def test_import_historical_kb_does_not_replace_newer_current_balance(monkeypatch
|
||||
assert service.list_account_valuations(account, db_path=path)["count"] == 2
|
||||
|
||||
|
||||
def test_loan_import_preserves_transactions_and_current_cache_without_valuation_candidate(monkeypatch, tmp_path):
|
||||
path = str(tmp_path / "ledger.sqlite3")
|
||||
account = service.create_ledger_account("대출", type="loan", db_path=path)["account"]["id"]
|
||||
monkeypatch.setattr(
|
||||
imports_service,
|
||||
"parse_kb_bank_pdf",
|
||||
lambda *_: _parsed(
|
||||
_fake_tx("2026-06-30", 700, kind="kb"),
|
||||
"loan-kb",
|
||||
{"final_balance": 700, "account_number_masked": ""},
|
||||
),
|
||||
)
|
||||
|
||||
result = imports_service.import_kb_bank_pdf("ignored", "pw", account_id=account, db_path=path)
|
||||
current = service.list_ledger_accounts(active_only=False, db_path=path)["accounts"][0]
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["inserted_count"] == 1
|
||||
assert current["last_balance"] == 700
|
||||
assert current["last_balance_basis_date"] == "2026-06-30"
|
||||
assert service.list_ledger_transactions(account_id=account, db_path=path)["count"] == 1
|
||||
assert service.list_account_valuations(account, db_path=path)["count"] == 0
|
||||
|
||||
|
||||
def test_import_historical_toss_bank_does_not_replace_newer_current_balance(monkeypatch, tmp_path):
|
||||
path = str(tmp_path / "ledger.sqlite3")
|
||||
account = service.create_ledger_account("Toss", institution="토스뱅크", last_balance=900, last_synced_at="2026-06-30", db_path=path, last_balance_basis_date="2026-06-30")["account"]["id"]
|
||||
|
||||
@@ -41,3 +41,13 @@ def test_interface_contract_documents_public_valuation_enums_validation_and_mapp
|
||||
)
|
||||
for marker in required_contract:
|
||||
assert marker in interface, marker
|
||||
|
||||
|
||||
def test_loan_cache_writers_document_no_inferred_valuation_history() -> None:
|
||||
data_model = _text("docs/data-model.md")
|
||||
interface = _text("docs/interface-contract.md")
|
||||
requirement = _text("docs/requirements/req-003_account-valuation-history.md")
|
||||
|
||||
assert "loan cache writes without an explicit liability definition do not append" in data_model
|
||||
assert "loan cache writes without an explicit liability definition do not append" in interface
|
||||
assert "대출 cache write는 대표 부채 정의가 없으면 이력 후보를 추가하지 않는다" in requirement
|
||||
|
||||
Reference in New Issue
Block a user