"""Регрессия маппинга нутриентов: NFC, fuzzy-match, derived pct/g per kg, EAV slugs.""" from __future__ import annotations import unittest from app import create_app, db from app.lab.calc.derived import apply_content_derived from app.lab.calc.engine import calculate_ration from app.lab.calc.ingredient_derive import derive_ingredient_nutrients, dict_to_cells from app.lab.calc.nutrients import get_nutrient_value from app.lab.indicators import indicator_by_key from app.lab.nutrient_keys import augment_with_indicator_keys, canonicalize_for_storage from app.lab.nutrient_schema import read_from_mapping from app.lab.services.component_nutrients import ( nutrients_calc_dict, nutrients_full_dict, pin_nutrient_values, upsert_from_api_dict, ) from app.models.component import Component from app.services.setup_state import mark_setup_complete from tests.helpers.zootech_test_helpers import ZootechTestConfig class IngredientDeriveKeyExactMatchTests(unittest.TestCase): def test_dict_to_cells_does_not_map_rup_per_kg_to_sv(self) -> None: """Нер. СП / кг СВ (11) не должен перезаписывать СВ (850) в derive.""" cells = dict_to_cells( { "СВ": 850.0, "Нер. СП / кг СВ": 11.0, "% уСП/кг СВ": 7.3, } ) self.assertEqual(cells.get("D"), 850.0) def test_derive_digestible_nfe_positive_on_seno_seed_shape(self) -> None: derived = derive_ingredient_nutrients( { "СВ": 850, "Сыр. Протеин": 78, "Сырая клетч": 328, "Сырой жир": 22, "Сырая зола": 65, "БЕР": 95, "уСП": 62, "Нер. СП / кг СВ": 11, "% уСП/кг СВ": 7.3, } ) self.assertGreater(derived.get("Переварим БЭВ", 0), 0) self.assertGreater(derived.get("Перевар Орг Вещ", 0), 0) self.assertGreater(derived.get("уСП/кг СВ формул", 0), 0) class NutrientKeyExactMatchTests(unittest.TestCase): def test_read_mapping_ca_not_dcab(self) -> None: data = {"Ca": 3.5, "DCAB Форм": 120.0, "DCAB": 99.0} self.assertEqual(read_from_mapping(data, ("DCAB Форм", "DCAB")), 120.0) self.assertEqual(read_from_mapping(data, ("Ca",)), 3.5) def test_fat_pct_key_does_not_read_fat_per_kg_dm_column(self) -> None: """Старый fuzzy-match подставлял СЖ/кг СВ (г/кг) в %-СЖ/кг СВ (%).""" nutrients = {"СЖ/кг СВ": 2260.0, "Сырой жир": 22.0} v = get_nutrient_value( 850, nutrients, ["%-СЖ/кг СВ ()"], indicator_key="fat_pct_per_kg_dm", ) self.assertIsNone(v) def test_nfc_key_not_in_nfc_pct_indicator_keys(self) -> None: defn = indicator_by_key("nfc_pct_dm") self.assertIsNotNone(defn) keys = [k.lower() for k in defn["nutrient_keys"]] self.assertNotIn("nfc", keys) self.assertEqual(defn.get("derived"), "pct_of_dm") self.assertEqual(defn.get("from_key"), "nfe") class DerivedContentTests(unittest.TestCase): def test_pct_of_dm(self) -> None: defn = {"derived": "pct_of_dm", "from_key": "crude_fat"} content = {"crude_fat": 220.0, "dry_matter": 8500.0} self.assertAlmostEqual(apply_content_derived(content, defn), 2.588235, places=4) def test_g_per_kg_dm_daily_totals(self) -> None: defn = {"derived": "g_per_kg_dm", "from_key": "crude_protein"} content = {"crude_protein": 1000.0, "dry_matter": 8500.0} self.assertAlmostEqual(apply_content_derived(content, defn), 117.647, places=2) def test_g_per_kg_dm_compound_mode_returns_source(self) -> None: defn = {"derived": "g_per_kg_dm", "from_key": "crude_fat"} content = {"crude_fat": 28.0, "dry_matter": 650.0} self.assertEqual( apply_content_derived(content, defn, compound_mode=True), 28.0, ) def test_nel_per_kg_dm_daily_vs_compound(self) -> None: defn = {"derived": "nel_per_kg_dm"} daily = {"nel": 32.0, "dry_matter": 8500.0} compound = {"nel": 7.8, "dry_matter": 880.0} self.assertAlmostEqual(apply_content_derived(daily, defn), 3.7647, places=3) self.assertAlmostEqual(apply_content_derived(compound, defn, compound_mode=True), 7.8) class AugmentIndicatorKeysTests(unittest.TestCase): def test_header_duplicated_as_indicator_slug(self) -> None: out = augment_with_indicator_keys({"Сырой жир": 22.0, "БЭВ": 95.0}) self.assertEqual(out["Сырой жир"], 22.0) self.assertEqual(out["crude_fat"], 22.0) self.assertEqual(out["nfe"], 95.0) def test_canonicalize_preserves_headers_and_slugs(self) -> None: stored = canonicalize_for_storage({"Сыр. Протеин": 170.0}) self.assertEqual(stored["Сыр. Протеин"], 170.0) self.assertEqual(stored["crude_protein"], 170.0) class NutrientMappingRationRegressionTests(unittest.TestCase): _LINE = { "daily_kg": 10, "in_ration": True, "in_compound": False, "dry_matter": 850, "nutrients": { "СВ": 850, "Сырой жир": 22, "БЕР": 95, "NFC": 520, "СЖ/кг СВ": 2260, }, } def _indicator(self, result: dict, key: str) -> float: return next(i["content"] for i in result["indicators"] if i["key"] == key) def test_nfc_g_per_kg_not_used_for_nfc_pct(self) -> None: result = calculate_ration("DAIRY", [self._LINE], {}, heads_per_trip=1) nfc_pct = self._indicator(result, "nfc_pct_dm") self.assertAlmostEqual(nfc_pct, 11.176, places=1) self.assertLess(nfc_pct, 50.0) def test_fat_pct_from_crude_fat_daily_total_not_per_kg_column(self) -> None: result = calculate_ration("DAIRY", [self._LINE], {}, heads_per_trip=1) fat_pct = self._indicator(result, "fat_pct_per_kg_dm") self.assertAlmostEqual(fat_pct, 2.588, places=1) self.assertLess(fat_pct, 10.0) def test_cp_per_kg_dm_from_daily_total(self) -> None: line = { **self._LINE, "nutrients": { **self._LINE["nutrients"], "Сыр. Протеин": 100, }, } result = calculate_ration("DAIRY", [line], {}, heads_per_trip=1) cp = self._indicator(result, "crude_protein") dm = self._indicator(result, "dry_matter") cp_kg = self._indicator(result, "cp_per_kg_dm") self.assertAlmostEqual(cp_kg, cp / (dm / 1000.0), places=2) class ComponentNutrientsEavTests(unittest.TestCase): def setUp(self) -> None: self.app = create_app(ZootechTestConfig) self.ctx = self.app.app_context() self.ctx.push() db.create_all() mark_setup_complete(self.app) def tearDown(self) -> None: db.session.remove() db.drop_all() self.ctx.pop() def test_upsert_adds_indicator_key_slugs_to_eav(self) -> None: comp = Component(name="LAB regression NFC", type="зерновые", dry_matter=88.0, price=1.0) db.session.add(comp) db.session.commit() upsert_from_api_dict( comp.id, { "СВ": 880, "Сыр. Протеин": 170, "Сырая клетч": 95, "Сырой жир": 38, "БЭВ": 520, "NFC": 999, }, ) full = nutrients_full_dict(comp.id) self.assertEqual(full.get("crude_fat"), 38.0) self.assertIn("crude_protein", full) self.assertEqual(full.get("NFC"), 999.0) def test_calc_dict_uses_ber_for_nfe_not_nfc_in_ration(self) -> None: comp = Component(name="LAB regression calc", type="зерновые", dry_matter=88.0, price=1.0) db.session.add(comp) db.session.commit() pin_nutrient_values( comp.id, {"СВ": 880, "БЕР": 95, "NFC": 520, "Сырой жир": 38}, ) calc = nutrients_calc_dict(comp.id) result = calculate_ration( "DAIRY", [ { "daily_kg": 5, "in_ration": True, "in_compound": False, "dry_matter": calc.get("СВ") or 880, "nutrients": calc, } ], {}, heads_per_trip=1, ) nfc_pct = next(i["content"] for i in result["indicators"] if i["key"] == "nfc_pct_dm") nfc_from_wrong_key = 520 / 880 * 100 self.assertAlmostEqual(nfc_pct, 95 / 880 * 100, places=1) self.assertNotAlmostEqual(nfc_pct, nfc_from_wrong_key, places=0) self.assertLess(nfc_pct, 50.0)