Preserve inactive-category fallback and saved settlement header evidence
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user