"""제비율 데이터의 **밑수·처리 규칙**이 코드와 같은가 — 거울 시험 (2026-09-09). 요율 숫자만 데이터에 두면 **그 데이터만 보고는 금액을 되짚을 수 없다.** 밑수를 무엇으로 잡고 어떻게 자르는지가 코드에만 있으면, 데이터를 갈아 끼운 사람이 규칙이 바뀐 줄 모른다. 그래서 `rates_2026.json` 에 밑수(`base`)와 `processing_rules` 를 함께 싣고, **여기서 코드와 맞대 본다.** 한쪽만 바뀌면 이 시험이 멈춘다. ⚠ 겨누는 것 넷 ① 비목마다 데이터가 적은 밑수와 **코드가 실제로 쓰는 밑수**가 같은가 ② 안전관리비 두 식(A·B)의 배수 1.2 · 관급 부가세 제수 1.1 이 데이터와 같은가 ③ 자르는 법 — 줄은 원 단위 버림, 관급자재대만 천원 올림 ④ 밑수 칸이 **빠진 요율이 없는가** (빠지면 그 비목만 데이터로 못 되짚음) """ from __future__ import annotations import json import sys from decimal import Decimal from pathlib import Path ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) from B09_Estimation.B09_Estimation_Engine_Cost import ( # noqa: E402 CostInput, ceil_thousand, floor_won, ) from B09_Estimation.B09_Estimation_Rates import load_rate_dataset # noqa: E402 from B09_Estimation.B09_Estimation_Statutory import ( # noqa: E402 _SAFETY_B_MULTIPLIER, _VAT_DIVISOR, STATUTORY_ITEMS, ExpenseContext, _base_and_rate, ) RATES = json.loads( (ROOT / "resources" / "data_cost_input_value" / "rates_2026.json").read_text(encoding="utf-8") ) #: 밑수 이름 → 그 밑수를 만드는 식. 값은 서로 겹치지 않는 소수를 써서 **합이 유일**하게 한다. MATERIAL = Decimal(1_000_003) DIRECT_LABOR = Decimal(10_000_019) TOTAL_LABOR = Decimal(100_000_007) DIRECT_CONSTRUCTION = Decimal(1_000_000_007) _CONTEXT = ExpenseContext( material_cost=MATERIAL, direct_labor_cost=DIRECT_LABOR, total_labor_cost=TOTAL_LABOR, direct_construction_cost=DIRECT_CONSTRUCTION, scale_reference=DIRECT_CONSTRUCTION, ) _BASE_VALUE = { "total_labor_cost": TOTAL_LABOR, "direct_labor_cost": DIRECT_LABOR, "direct_construction_cost": DIRECT_CONSTRUCTION, "material_cost_plus_total_labor_cost": MATERIAL + TOTAL_LABOR, } def _dataset(): return load_rate_dataset() def test_비목마다_데이터와_코드의_밑수가_같다() -> None: """① 데이터가 적은 밑수로 실제 금액이 서는가 — 이름만 맞고 계산이 다르면 헛것이다.""" dataset = _dataset() data = CostInput( direct_material_krw=MATERIAL, direct_labor_krw=DIRECT_LABOR, direct_expense_krw=Decimal(0), ) checked = 0 for item in STATUTORY_ITEMS: if item.key == "performance_guarantee_fee": continue # 밑수에 공사기간(년)을 곱하는 자리 — 아래 시험에서 따로 본다 declared = RATES["variables"][item.rate_variable].get("base") if declared not in _BASE_VALUE: continue # 밑수가 식으로 갈리는 비목(안전관리비·보증수수료)은 아래 시험에서 본다 base, _percent, _note = _base_and_rate(dataset, data, _CONTEXT, item) assert base == _BASE_VALUE[declared], f"{item.key}: 데이터 {declared} 와 코드가 다름" checked += 1 assert checked >= 8 def test_공사이행보증은_기간을_곱한다() -> None: """데이터가 `multiplier: 공사기간(년)` 이라 적은 자리 — 곱이 실제로 붙는가.""" dataset = _dataset() rules = RATES["processing_rules"]["performance_guarantee_fee"] assert rules["base"] == "direct_construction_cost" and "공사기간" in rules["multiplier"] item = next(i for i in STATUTORY_ITEMS if i.key == "performance_guarantee_fee") one_year = CostInput( direct_material_krw=MATERIAL, direct_labor_krw=DIRECT_LABOR, direct_expense_krw=Decimal(0), duration_days=365, ) two_years = CostInput( direct_material_krw=MATERIAL, direct_labor_krw=DIRECT_LABOR, direct_expense_krw=Decimal(0), duration_days=730, ) base_one, _, _ = _base_and_rate(dataset, one_year, _CONTEXT, item) base_two, _, _ = _base_and_rate(dataset, two_years, _CONTEXT, item) assert base_one is not None and base_two is not None assert base_two == base_one * 2 def test_안전관리비_두_수가_데이터와_같다() -> None: """② 1.2(B 배수)·1.1(관급 부가세 제수)이 코드에만 있으면 데이터로 못 되짚는다.""" safety = RATES["processing_rules"]["safety_management_cost"] assert Decimal(str(safety["b_multiplier"])) == _SAFETY_B_MULTIPLIER assert Decimal(str(safety["owner_supplied_vat_divisor"])) == _VAT_DIVISOR assert safety["adopt"] == "min_of_A_and_B" assert RATES["variables"]["rate_safety_pct"]["base_rule"] == "min_of_two_formulas" def test_자르는_법이_데이터와_같다() -> None: """③ 줄은 버림, 관급자재대만 천원 올림.""" rounding = RATES["processing_rules"]["rounding"] assert rounding["line_amount"] == "floor_won" assert rounding["owner_supplied_material_total"] == "ceil_thousand" assert floor_won(Decimal("1999.9")) == Decimal(1999) assert ceil_thousand(Decimal(1)) == Decimal(1000) def test_밑수_칸이_빠진_요율이_없다() -> None: """④ 하나라도 비면 그 비목만 데이터로 못 되짚는다.""" without_base = [ name for name, value in RATES["variables"].items() if isinstance(value, dict) and "base" not in value and not {"base_without_owner_supplied_material", "embedded_in"} & set(value) ] assert without_base == [], without_base def test_조달수수료를_어디서도_빼지_않는다() -> None: """★법대로 — 차감 처리는 없다. 데이터에도 그 사실을 못 박는다.""" owner = RATES["processing_rules"]["owner_supplied_material"] assert owner["procurement_fee_deduction"] is False assert "순자재대" in owner["safety_base_uses"]