fix: address account valuation review findings
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from dashboard.page_views import accounts as accounts_page
|
||||
from jarca_ledger_mcp.adapters.mcp import server
|
||||
|
||||
|
||||
def test_dashboard_valuation_basis_never_uses_sync_timestamp():
|
||||
account = {
|
||||
"last_balance_basis_date": "2026-06-30",
|
||||
"last_synced_at": "2026-07-09T23:59:00+09:00",
|
||||
}
|
||||
assert accounts_page._account_balance_basis_date(account) == "2026-06-30"
|
||||
assert accounts_page._account_balance_basis_date({"last_synced_at": account["last_synced_at"]}) == ""
|
||||
assert "last_synced_at" not in inspect.getsource(accounts_page.page_accounts)
|
||||
|
||||
|
||||
def test_mcp_account_write_tools_forward_explicit_balance_basis(monkeypatch):
|
||||
calls: list[tuple[str, tuple, dict]] = []
|
||||
|
||||
def fake_create(*args, **kwargs):
|
||||
calls.append(("create", args, kwargs))
|
||||
return {"success": True}
|
||||
|
||||
def fake_update(*args, **kwargs):
|
||||
calls.append(("update", args, kwargs))
|
||||
return {"success": True}
|
||||
|
||||
monkeypatch.setattr(server.contracts, "create_ledger_account", fake_create)
|
||||
monkeypatch.setattr(server.contracts, "update_ledger_account", fake_update)
|
||||
|
||||
assert server.create_ledger_account(
|
||||
"dated", last_balance=100, last_synced_at="2026-07-09T00:00:00Z",
|
||||
last_balance_basis_date="2026-06-30",
|
||||
)["success"]
|
||||
assert server.update_ledger_account(
|
||||
"account-1", last_balance=200, last_synced_at="2026-07-10T00:00:00Z",
|
||||
last_balance_basis_date="2026-07-01",
|
||||
)["success"]
|
||||
|
||||
assert calls[0][2]["last_balance_basis_date"] == "2026-06-30"
|
||||
assert calls[1][2]["last_balance_basis_date"] == "2026-07-01"
|
||||
@@ -0,0 +1,93 @@
|
||||
from jarca_ledger_mcp import service
|
||||
|
||||
|
||||
def test_loan_balance_requires_representative_liability_definition(tmp_path):
|
||||
path = str(tmp_path / "ledger.sqlite3")
|
||||
bank = service.create_ledger_account("은행", type="bank", db_path=path)["account"]["id"]
|
||||
loan = service.create_ledger_account("대출", type="loan", db_path=path)["account"]["id"]
|
||||
|
||||
missing_definition = service.register_account_valuation(
|
||||
loan,
|
||||
"2026-06-30",
|
||||
100,
|
||||
valuation_kind="balance",
|
||||
verification_status="verified",
|
||||
db_path=path,
|
||||
)
|
||||
assert missing_definition["success"] is False
|
||||
assert missing_definition["error_code"] == "validation_error"
|
||||
assert service.list_account_valuations(loan, db_path=path)["count"] == 0
|
||||
|
||||
ordinary_balance = service.register_account_valuation(
|
||||
bank,
|
||||
"2026-06-30",
|
||||
100,
|
||||
valuation_kind="balance",
|
||||
verification_status="verified",
|
||||
db_path=path,
|
||||
)
|
||||
defined_loan_balance = service.register_account_valuation(
|
||||
loan,
|
||||
"2026-06-30",
|
||||
100,
|
||||
valuation_kind="balance",
|
||||
representative_liability_definition="principal_only",
|
||||
verification_status="verified",
|
||||
db_path=path,
|
||||
)
|
||||
assert ordinary_balance["success"] is True
|
||||
assert defined_loan_balance["success"] is True
|
||||
|
||||
|
||||
def test_correction_replays_by_evidence_and_conflicts_on_changed_payload(tmp_path):
|
||||
path = str(tmp_path / "ledger.sqlite3")
|
||||
account = service.create_ledger_account("계좌", db_path=path)["account"]["id"]
|
||||
parent = service.register_account_valuation(
|
||||
account,
|
||||
"2026-06-30",
|
||||
100,
|
||||
source_ref="statement-1",
|
||||
verification_status="verified",
|
||||
db_path=path,
|
||||
)["valuation"]
|
||||
|
||||
first = service.correct_account_valuation(
|
||||
parent["id"],
|
||||
110,
|
||||
"statement correction",
|
||||
db_path=path,
|
||||
)
|
||||
replay = service.correct_account_valuation(
|
||||
parent["id"],
|
||||
110,
|
||||
"statement correction",
|
||||
db_path=path,
|
||||
)
|
||||
changed_amount = service.correct_account_valuation(
|
||||
parent["id"],
|
||||
111,
|
||||
"statement correction",
|
||||
db_path=path,
|
||||
)
|
||||
changed_reason = service.correct_account_valuation(
|
||||
parent["id"],
|
||||
110,
|
||||
"different correction",
|
||||
db_path=path,
|
||||
)
|
||||
changed_source = service.correct_account_valuation(
|
||||
parent["id"],
|
||||
110,
|
||||
"statement correction",
|
||||
source_ref="statement-2",
|
||||
db_path=path,
|
||||
)
|
||||
|
||||
assert first["success"] is True
|
||||
assert replay["success"] is True
|
||||
assert replay["reused"] is True
|
||||
assert replay["valuation"]["id"] == first["valuation"]["id"]
|
||||
for changed in (changed_amount, changed_reason, changed_source):
|
||||
assert changed["success"] is False
|
||||
assert changed["error_code"] == "correction_conflict"
|
||||
assert service.list_account_valuations(account, db_path=path)["count"] == 2
|
||||
@@ -61,7 +61,7 @@ def test_correction_is_append_only_reasoned_and_single_child(tmp_path):
|
||||
|
||||
def test_account_balance_write_records_unverified_candidate_only_with_real_basis_date(tmp_path):
|
||||
path = str(tmp_path / "ledger.sqlite3")
|
||||
created = service.create_ledger_account("동기화 계좌", last_balance=500, last_synced_at="2026-06-30", db_path=path)
|
||||
created = service.create_ledger_account("동기화 계좌", last_balance=500, last_synced_at="2026-06-30", db_path=path, last_balance_basis_date="2026-06-30")
|
||||
service.create_ledger_account("날짜 없는 계좌", last_balance=700, db_path=path)
|
||||
values = service.list_account_valuations(created["account"]["id"], db_path=path)
|
||||
assert values["count"] == 1
|
||||
@@ -95,7 +95,7 @@ def test_correction_preserves_optional_fields_unless_overridden(tmp_path):
|
||||
|
||||
def test_account_name_only_update_does_not_create_candidate(tmp_path):
|
||||
path = str(tmp_path / "ledger.sqlite3")
|
||||
created = service.create_ledger_account("계좌", last_balance=500, last_synced_at="2026-06-30", db_path=path)
|
||||
created = service.create_ledger_account("계좌", last_balance=500, last_synced_at="2026-06-30", db_path=path, last_balance_basis_date="2026-06-30")
|
||||
account_id = created["account"]["id"]
|
||||
updated = service.update_ledger_account(account_id, name="새 이름", db_path=path)
|
||||
values = service.list_account_valuations(account_id, db_path=path)
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
|
||||
from jarca_ledger_mcp import service
|
||||
|
||||
|
||||
def test_exact_run_replay_after_correction_reconstructs_original_persisted_run(tmp_path):
|
||||
db_path = str(tmp_path / "ledger.sqlite3")
|
||||
service.create_manual_asset("현금", 100, "2026-06-30", "cash", db_path=db_path)
|
||||
service.create_manual_asset("평가 대기", None, "2026-06-30", "other", db_path=db_path)
|
||||
|
||||
first = service.close_asset_snapshot(
|
||||
"2026-06-30",
|
||||
source_note="synthetic initial close",
|
||||
memo="synthetic initial memo",
|
||||
idempotency_key="snapshot-close-a",
|
||||
allow_missing_exclusions=True,
|
||||
approved_by="reviewer-a",
|
||||
approval_reason="synthetic initial approval",
|
||||
approval_reference="approval-a",
|
||||
db_path=db_path,
|
||||
)
|
||||
assert first["success"] is True
|
||||
assert first["close_status"] == "conditional"
|
||||
assert first["is_correction"] is False
|
||||
assert first["warnings"]
|
||||
assert first["approval"]["approval_reference"] == "approval-a"
|
||||
assert first["provenance"]["snapshot_run_id"] == first["snapshot_run_id"]
|
||||
|
||||
corrected = service.close_asset_snapshot(
|
||||
"2026-06-30",
|
||||
source_note="synthetic correction close",
|
||||
memo="synthetic correction memo",
|
||||
correction_reason="synthetic correction",
|
||||
idempotency_key="snapshot-close-b",
|
||||
allow_missing_exclusions=True,
|
||||
approved_by="reviewer-b",
|
||||
approval_reason="synthetic correction approval",
|
||||
approval_reference="approval-b",
|
||||
db_path=db_path,
|
||||
)
|
||||
assert corrected["success"] is True
|
||||
assert corrected["is_correction"] is True
|
||||
assert corrected["snapshot_run_id"] != first["snapshot_run_id"]
|
||||
assert corrected["correction"]["correction_of_run_id"] == first["snapshot_run_id"]
|
||||
assert corrected["approval"]["approval_reference"] == "approval-b"
|
||||
assert corrected["snapshot_run_id"] not in first["inserted_snapshot_ids"]
|
||||
|
||||
with sqlite3.connect(db_path) as conn:
|
||||
before_replay = conn.execute("SELECT COUNT(*) FROM asset_snapshots").fetchone()[0]
|
||||
|
||||
replay = service.close_asset_snapshot(
|
||||
"2026-06-30",
|
||||
source_note="synthetic initial close",
|
||||
memo="synthetic initial memo",
|
||||
idempotency_key="snapshot-close-a",
|
||||
allow_missing_exclusions=True,
|
||||
approved_by="reviewer-a",
|
||||
approval_reason="synthetic initial approval",
|
||||
approval_reference="approval-a",
|
||||
db_path=db_path,
|
||||
)
|
||||
|
||||
with sqlite3.connect(db_path) as conn:
|
||||
after_replay = conn.execute("SELECT COUNT(*) FROM asset_snapshots").fetchone()[0]
|
||||
|
||||
assert replay == first
|
||||
assert replay["snapshot_run_id"] == first["snapshot_run_id"]
|
||||
assert replay["inserted_snapshot_ids"] == first["inserted_snapshot_ids"]
|
||||
assert replay["correction"] == first["correction"]
|
||||
assert replay["approval"] == first["approval"]
|
||||
assert replay["warnings"] == first["warnings"]
|
||||
assert replay["provenance"] == first["provenance"]
|
||||
assert replay["included_items"] == first["included_items"]
|
||||
assert replay["excluded_items"] == first["excluded_items"]
|
||||
assert replay["total_assets"] == first["total_assets"]
|
||||
assert replay["total_liabilities"] == first["total_liabilities"]
|
||||
assert replay["net_worth"] == first["net_worth"]
|
||||
assert replay["snapshot_run_id"] != corrected["snapshot_run_id"]
|
||||
assert corrected["snapshot_run_id"] not in replay["inserted_snapshot_ids"]
|
||||
assert after_replay == before_replay
|
||||
|
||||
|
||||
def test_empty_run_replay_after_nonempty_correction_preserves_original_empty_payload(tmp_path):
|
||||
db_path = str(tmp_path / "ledger.sqlite3")
|
||||
service.create_ledger_account("누락 계좌", db_path=db_path)
|
||||
first = service.close_asset_snapshot(
|
||||
"2026-06-30",
|
||||
idempotency_key="empty-close-a",
|
||||
allow_missing_exclusions=True,
|
||||
approved_by="reviewer-a",
|
||||
approval_reason="approve empty",
|
||||
approval_reference="approval-a",
|
||||
db_path=db_path,
|
||||
)
|
||||
assert first["success"] is True
|
||||
assert first["inserted_count"] == 0
|
||||
assert first["included_items"] == []
|
||||
|
||||
service.create_manual_asset("후속 자산", 100, "2026-06-30", "cash", db_path=db_path)
|
||||
corrected = service.close_asset_snapshot(
|
||||
"2026-06-30",
|
||||
correction_reason="add verified asset",
|
||||
idempotency_key="nonempty-close-b",
|
||||
allow_missing_exclusions=True,
|
||||
approved_by="reviewer-b",
|
||||
approval_reason="approve correction",
|
||||
approval_reference="approval-b",
|
||||
db_path=db_path,
|
||||
)
|
||||
assert corrected["success"] is True
|
||||
assert corrected["inserted_count"] == 1
|
||||
|
||||
# Restore the original live authority while retaining the later immutable
|
||||
# correction run, so replay reaches the original-key reconstruction path.
|
||||
with sqlite3.connect(db_path) as conn:
|
||||
conn.execute("DELETE FROM manual_assets WHERE name=?", ("후속 자산",))
|
||||
|
||||
replay = service.close_asset_snapshot(
|
||||
"2026-06-30",
|
||||
idempotency_key="empty-close-a",
|
||||
allow_missing_exclusions=True,
|
||||
approved_by="reviewer-a",
|
||||
approval_reason="approve empty",
|
||||
approval_reference="approval-a",
|
||||
db_path=db_path,
|
||||
)
|
||||
assert replay == first
|
||||
assert replay["inserted_count"] == 0
|
||||
assert replay["inserted_snapshot_ids"] == []
|
||||
assert replay["included_items"] == []
|
||||
@@ -0,0 +1,105 @@
|
||||
from types import SimpleNamespace
|
||||
import sqlite3
|
||||
|
||||
from jarca_ledger_mcp import service
|
||||
from jarca_ledger_mcp.application import imports_service
|
||||
|
||||
|
||||
def _parsed(transaction, source_hash):
|
||||
return {
|
||||
"transactions": [transaction],
|
||||
"validation": {"ok": True},
|
||||
"source_hash": source_hash,
|
||||
"metadata": {"final_balance": transaction.balance_after, "account_number_masked": ""},
|
||||
"file_path": "/tmp/synthetic.pdf",
|
||||
"page_count": 1,
|
||||
}
|
||||
|
||||
|
||||
def _transaction(date, balance, row):
|
||||
return SimpleNamespace(
|
||||
transaction_date=date,
|
||||
transaction_datetime=f"{date} 10:00:00",
|
||||
amount=100,
|
||||
counterparty="상대",
|
||||
raw_summary="이체",
|
||||
branch="",
|
||||
remittance_memo="",
|
||||
balance_after=balance,
|
||||
type="income",
|
||||
page=1,
|
||||
action="입금",
|
||||
external_id=lambda source_hash: f"kb:{source_hash}:{row}",
|
||||
raw_payload=lambda: "{}",
|
||||
)
|
||||
|
||||
|
||||
def test_late_import_current_balance_is_authoritative_by_evidence_basis_date(monkeypatch, tmp_path):
|
||||
path = str(tmp_path / "ledger.sqlite3")
|
||||
account = service.create_ledger_account(
|
||||
"KB", institution="KB국민은행", db_path=path,
|
||||
)["account"]["id"]
|
||||
imports = iter(
|
||||
(
|
||||
(_transaction("2026-06-01", 700, 1), "hash-old"),
|
||||
(_transaction("2026-06-30", 900, 2), "hash-new"),
|
||||
)
|
||||
)
|
||||
monkeypatch.setattr(imports_service, "parse_kb_bank_pdf", lambda *_: _parsed(*next(imports)))
|
||||
sync_times = iter(("2026-07-01T09:00:00", "2026-07-02T09:00:00"))
|
||||
monkeypatch.setattr(imports_service, "now_iso", lambda: next(sync_times))
|
||||
|
||||
older = imports_service.import_kb_bank_pdf("older", "pw", account_id=account, db_path=path)
|
||||
newer = imports_service.import_kb_bank_pdf("newer", "pw", account_id=account, db_path=path)
|
||||
|
||||
current = service.list_ledger_accounts(active_only=False, db_path=path)["accounts"][0]
|
||||
valuations = service.list_account_valuations(account, db_path=path)["valuations"]
|
||||
|
||||
assert older["success"] and newer["success"]
|
||||
assert current["last_balance"] == 900
|
||||
assert current["last_synced_at"] == "2026-07-02T09:00:00"
|
||||
assert current["last_balance_basis_date"] == "2026-06-30"
|
||||
assert {(item["basis_date"], item["amount"]) for item in valuations} >= {
|
||||
("2026-06-01", 700),
|
||||
("2026-06-30", 900),
|
||||
}
|
||||
|
||||
|
||||
def test_account_write_sync_timestamp_does_not_infer_balance_basis_or_candidate(tmp_path):
|
||||
path = str(tmp_path / "ledger.sqlite3")
|
||||
unknown = service.create_ledger_account(
|
||||
"기준일 미상",
|
||||
last_balance=500,
|
||||
last_synced_at="2026-07-01T09:00:00+09:00",
|
||||
db_path=path,
|
||||
)
|
||||
|
||||
assert unknown["success"] is True
|
||||
assert unknown["account"]["last_balance_basis_date"] == ""
|
||||
assert service.list_account_valuations(unknown["account"]["id"], db_path=path)["count"] == 0
|
||||
|
||||
dated = service.create_ledger_account(
|
||||
"기준일 확인",
|
||||
last_balance=600,
|
||||
last_synced_at="2026-07-01T09:00:00+09:00",
|
||||
db_path=path,
|
||||
last_balance_basis_date="2026-06-30",
|
||||
)
|
||||
valuations = service.list_account_valuations(dated["account"]["id"], db_path=path)["valuations"]
|
||||
assert dated["account"]["last_balance_basis_date"] == "2026-06-30"
|
||||
assert [(item["basis_date"], item["amount"]) for item in valuations] == [("2026-06-30", 600)]
|
||||
|
||||
|
||||
def test_unknown_legacy_balance_basis_is_not_reported_as_valuation_date(tmp_path):
|
||||
path = str(tmp_path / "ledger.sqlite3")
|
||||
account = service.create_ledger_account("legacy", db_path=path)["account"]["id"]
|
||||
with sqlite3.connect(path) as conn:
|
||||
conn.execute(
|
||||
"UPDATE accounts SET last_balance=?, last_synced_at=?, last_balance_basis_date='' WHERE id=?",
|
||||
(500, "2026-07-01T09:00:00+09:00", account),
|
||||
)
|
||||
|
||||
report = service.get_asset_snapshot_report("2026-06-30", db_path=path)
|
||||
excluded = next(item for item in report["excluded_items"] if item["source_id"] == account)
|
||||
assert excluded["valuation_date"] == ""
|
||||
assert excluded["last_synced_at"] == "2026-07-01T09:00:00+09:00"
|
||||
@@ -75,9 +75,9 @@ def test_reconciliation_prefers_investment_total_value_over_balance(tmp_path):
|
||||
|
||||
def test_historical_account_update_preserves_current_and_adds_one_candidate(tmp_path):
|
||||
path = str(tmp_path / "ledger.sqlite3")
|
||||
created = service.create_ledger_account("계좌", last_balance=900, last_synced_at="2026-06-30", db_path=path)
|
||||
created = service.create_ledger_account("계좌", last_balance=900, last_synced_at="2026-06-30", db_path=path, last_balance_basis_date="2026-06-30")
|
||||
account = created["account"]["id"]
|
||||
updated = service.update_ledger_account(account, last_balance=700, last_synced_at="2026-06-01", db_path=path)
|
||||
updated = service.update_ledger_account(account, 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 and updated["account"]["last_synced_at"] == "2026-06-30"
|
||||
values = service.list_account_valuations(account, db_path=path)["valuations"]
|
||||
assert len(values) == 2 and sum(v["basis_date"] == "2026-06-01" for v in values) == 1
|
||||
@@ -294,20 +294,23 @@ def test_loan_representative_is_always_aggregated_as_liability(tmp_path):
|
||||
assert report["included_items"][0]["liquidity_bucket"] == "loan"
|
||||
|
||||
|
||||
def test_loan_balance_fallback_without_representative_definition_is_excluded(tmp_path):
|
||||
def test_loan_balance_without_representative_definition_is_rejected_without_write(tmp_path):
|
||||
path = str(tmp_path / "ledger.sqlite3")
|
||||
loan = service.create_ledger_account(
|
||||
"대출", type="loan", last_balance=100,
|
||||
last_synced_at="2026-06-30", db_path=path,
|
||||
)["account"]["id"]
|
||||
service.register_account_valuation(
|
||||
rejected = service.register_account_valuation(
|
||||
loan, "2026-06-30", 100, valuation_kind="balance",
|
||||
verification_status="verified", 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
|
||||
report = service.get_asset_snapshot_report("2026-06-30", db_path=path)
|
||||
assert report["total_liabilities"] == 0
|
||||
assert report["net_worth"] == 0
|
||||
assert report["excluded_items"][0]["reason"] == "missing_representative_liability_definition"
|
||||
assert report["excluded_items"][0]["reason"] == "missing_verified_exact_valuation"
|
||||
|
||||
|
||||
def test_warning_candidate_public_payload_is_minimal():
|
||||
|
||||
@@ -96,7 +96,7 @@ def _parsed(tx, source, metadata):
|
||||
|
||||
def test_import_historical_kb_does_not_replace_newer_current_balance(monkeypatch, tmp_path):
|
||||
path = str(tmp_path / "ledger.sqlite3")
|
||||
account = service.create_ledger_account("KB", institution="KB국민은행", last_balance=900, last_synced_at="2026-06-30", db_path=path)["account"]["id"]
|
||||
account = service.create_ledger_account("KB", institution="KB국민은행", last_balance=900, last_synced_at="2026-06-30", db_path=path, last_balance_basis_date="2026-06-30")["account"]["id"]
|
||||
monkeypatch.setattr(imports_service, "parse_kb_bank_pdf", lambda *_: _parsed(_fake_tx("2026-06-01", 700, kind="kb"), "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]
|
||||
@@ -106,7 +106,7 @@ def test_import_historical_kb_does_not_replace_newer_current_balance(monkeypatch
|
||||
|
||||
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)["account"]["id"]
|
||||
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"]
|
||||
monkeypatch.setattr(imports_service, "parse_toss_bank_pdf", lambda *_: _parsed(_fake_tx("2026-06-01", 700), "bank", {"account_number_masked": ""}))
|
||||
result = imports_service.import_toss_bank_pdf("ignored", account_id=account, db_path=path)
|
||||
current = service.list_ledger_accounts(active_only=False, db_path=path)["accounts"][0]
|
||||
@@ -116,7 +116,7 @@ def test_import_historical_toss_bank_does_not_replace_newer_current_balance(monk
|
||||
|
||||
def test_import_historical_toss_securities_does_not_replace_newer_current_balance(monkeypatch, tmp_path):
|
||||
path = str(tmp_path / "ledger.sqlite3")
|
||||
account = service.create_ledger_account("Securities", type="investment", institution="토스증권", last_balance=900, last_synced_at="2026-06-30", db_path=path)["account"]["id"]
|
||||
account = service.create_ledger_account("Securities", type="investment", institution="토스증권", last_balance=900, last_synced_at="2026-06-30", db_path=path, last_balance_basis_date="2026-06-30")["account"]["id"]
|
||||
monkeypatch.setattr(imports_service, "parse_toss_securities_pdf", lambda *_: _parsed(_fake_tx("2026-06-01", 700, kind="tosssec"), "sec", {"account_number_masked": ""}))
|
||||
result = imports_service.import_toss_securities_pdf("ignored", account_id=account, db_path=path)
|
||||
current = service.list_ledger_accounts(active_only=False, db_path=path)["accounts"][0]
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).parents[1]
|
||||
|
||||
|
||||
def _text(relative_path: str) -> str:
|
||||
return (ROOT / relative_path).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_data_model_documents_cache_authority_and_legacy_basis_migration() -> None:
|
||||
data_model = _text("docs/data-model.md")
|
||||
|
||||
required_contract = (
|
||||
"`last_balance` is the current cache value",
|
||||
"`last_balance_basis_date`",
|
||||
"`last_synced_at` is the execution/sync timestamp",
|
||||
"imports and account writes advance the account cache",
|
||||
"`account_valuations` is the historical authority",
|
||||
"older evidence never rolls back the current cache",
|
||||
"additive migration",
|
||||
"legacy rows with unknown basis remain unknown; no inference",
|
||||
)
|
||||
for marker in required_contract:
|
||||
assert marker in data_model, marker
|
||||
|
||||
|
||||
def test_interface_contract_documents_public_valuation_enums_validation_and_mapping() -> None:
|
||||
interface = _text("docs/interface-contract.md")
|
||||
|
||||
assert "actual enums in source" not in interface
|
||||
required_contract = (
|
||||
"source_type: `manual`, `import`, `account_write`, `reconciliation`, `snapshot`",
|
||||
"verification_status: `verified`, `unverified`, `rejected`",
|
||||
"valuation_kind: `balance`, `cash_balance`, `total_value`, `loan`, `loan_balance`",
|
||||
"representative_liability_definition: `principal_only`, `principal_interest_fees`",
|
||||
"loan accounts require representative_liability_definition for `loan`, `loan_balance`, and `balance` aliases",
|
||||
"legacy valuation-kind aliases remain accepted and map to the canonical purpose",
|
||||
"validation failure returns `validation_error` and performs no write",
|
||||
"loan valuations map to liabilities and non-loan valuations map to assets",
|
||||
)
|
||||
for marker in required_contract:
|
||||
assert marker in interface, marker
|
||||
Reference in New Issue
Block a user