Preserve inactive-category fallback and saved settlement header evidence

This commit is contained in:
2026-10-13 23:17:12 +00:00
parent ae4383ca2c
commit 10201cdb75
7 changed files with 258 additions and 2 deletions
+64
View File
@@ -244,3 +244,67 @@ def test_nonstring_cashflow_decision_kind_has_validation_error():
except Exception as caught:
error = caught
assert isinstance(error, ValueError), "malformed kind must have the validator's ValueError contract, not an unhandled TypeError"
def test_inactive_category_uses_raw_fallback_through_public_close(tmp_path):
path = str(tmp_path / "inactive-category-fallback.sqlite3")
init_db(path)
# A close run requires a nonempty asset valuation; cashflow-only drafts cannot finalize.
_ok(service.create_manual_asset("Synthetic close cash", 100, "2026-09-30", "cash", db_path=path))
cases = [
# SQL fallback is category-based: ordinary legacy categories become expense,
# while canonical legacy categories retain their established classifications.
("Old exclude", "A", "exclude", "expense", 11, "expense"),
("Old income", "B", "income", "expense", 13, "expense"),
("Old transfer", "C", "transfer", "expense", 17, "expense"),
("Old unknown", "D", "unknown", "income", 19, "expense"),
("Old refund", "E", "exclude", "refund", 5, "expense"),
("Old movement", "F", "exclude", "transfer", 7, "excluded"),
("수입", "Legacy", "exclude", "income", 29, "income"),
("Old expense", "H", "expense", "expense", 37, "expense"),
("Old investment", "G", "investment", "expense", 31, "expense"),
]
ids = {}
for index, (category, subcategory, kind, raw_type, amount, expected) in enumerate(cases):
_add_category(category, subcategory, kind, path)
txid = _add_tx(f"2026-09-{index + 1:02d}", raw_type, amount, category, subcategory, path)
assert service.delete_ledger_category(category, subcategory, force=True, db_path=path)["success"]
ids[txid] = (expected, kind, raw_type, amount)
draft = _draft(path)
reviewed = _ok(service.review_settlement_draft(draft["id"], db_path=path))
by_id = {row["id"]: row for row in reviewed["source_snapshot"]["transactions"]}
for txid, (expected, _kind, raw_type, amount) in ids.items():
row = by_id[txid]
applied = -amount if raw_type == "refund" else amount
assert row["cashflow_kind"] == expected
assert row["originalcashflow_kind"] == ("income" if row["category"] == "수입" else ("unknown" if row["category"] == "미분류" else "expense"))
assert row["applied_amount"] == applied # excluded movement retains its amount as evidence
assert row["sourceprovenance"] == "fallback"
assert row["category_cashflow_kind"] is None and row["category_active"] is None
assert reviewed["expense_total"] == 123 # 11 + 13 + 17 + 19 - 5 + 31 + 37
assert reviewed["income_total"] == 29
approved = _ok(service.approve_settlement_draft(draft["id"], draft["version"], reviewed["review_fingerprint"], "reviewer", "synthetic", "ref", db_path=path))
closed = _ok(service.finalize_settlement_draft(draft["id"], approved["draft"]["version"], reviewed["review_fingerprint"], "inactive-category-close", db_path=path))
snapshot = _ok(service.get_monthly_settlement_snapshot(run_id=closed["snapshot_run_id"], db_path=path))
persisted = {row["id"]: row for row in snapshot["source_snapshot"]["transactions"]}
for txid in ids:
assert persisted[txid]["sourceprovenance"] == "fallback"
assert persisted[txid]["category_active"] is None
assert persisted[txid]["category_cashflow_kind"] is None
assert snapshot["expense_total"] == 123 and snapshot["income_total"] == 29
def test_explicit_decision_classifies_only_unresolved_inactive_fallback(tmp_path):
path = str(tmp_path / "inactive-explicit-decision.sqlite3")
init_db(path)
_add_category("미분류", "Retired", "unknown", path)
unresolved = _add_tx("2026-09-10", "expense", 23, "미분류", "Retired", path)
assert service.delete_ledger_category("미분류", "Retired", force=True, db_path=path)["success"]
draft = _draft(path)
policy = _policy([{"category": "미분류", "subcategory": "Retired", "cashflow_kind": "income", "reason": "synthetic explicit decision"}])
_ok(_capability("set_settlement_cashflow_policy")(draft["id"], draft["version"], "operator", policy, db_path=path))
reviewed = _ok(service.review_settlement_draft(draft["id"], db_path=path))
row = next(x for x in reviewed["source_snapshot"]["transactions"] if x["id"] == unresolved)
assert row["cashflow_kind"] == "income" and row["decision"]["cashflow_kind"] == "income"
assert row["originalcashflow_kind"] == "unknown" and row["sourceprovenance"] == "fallback"
assert reviewed["income_total"] == 23