"""Dual sync: reorder ингредиентов + группы выгрузки (баготест #5–#6).""" from __future__ import annotations import unittest import uuid from sqlalchemy import select from app import db from app.models import Ingredient, Recipe, UnloadingGroup from app.services.recipe_update_service import update_recipe_from_payload from app.services.sync_manager import enqueue_sync_queue_task from tests.helpers.mill_recipe_fixtures import ( create_recipe_with_children, group_payload_from_row, ingredient_payload_from_row, recipe_update_payload, ) from tests.helpers.sync_dual_harness import SyncDualInstanceHarness SCENARIO_DUAL_REORDER_AND_NEW_GROUP = "DUAL_REORDER_AND_NEW_GROUP" SCENARIO_DUAL_ADD_GROUP_UPDATES_EXISTING = "DUAL_ADD_GROUP_UPDATES_EXISTING" class SyncRecipeChildrenDualTests(unittest.TestCase): def setUp(self) -> None: self.harness = SyncDualInstanceHarness() self.harness.start() def tearDown(self) -> None: self.harness.stop() def _seed_recipe_on_terminals(self, recipe_id: str) -> None: with self.harness.server_ctx(): enqueue_sync_queue_task("recipe", recipe_id, "create", priority=2, target_node_id=None) for ing in db.session.execute( select(Ingredient).where( Ingredient.recipe_id == recipe_id, Ingredient.is_deleted.is_(False), ) ).scalars().all(): enqueue_sync_queue_task("ingredient", str(ing.id), "create", priority=2) for grp in db.session.execute( select(UnloadingGroup).where( UnloadingGroup.recipe_id == recipe_id, UnloadingGroup.is_deleted.is_(False), ) ).scalars().all(): enqueue_sync_queue_task("unloading_group", str(grp.id), "create", priority=2) db.session.commit() self.harness.drain_sync() def _ingredient_orders(self, which: str, recipe_id: str) -> list[tuple[str, int]]: with self.harness.terminal_ctx(which): rows = db.session.execute( select(Ingredient.id, Ingredient.order) .where( Ingredient.recipe_id == recipe_id, Ingredient.is_deleted.is_(False), ) .order_by(Ingredient.order.asc()) ).all() return [(str(rid), int(ord_)) for rid, ord_ in rows] def _group_snapshot(self, which: str, recipe_id: str) -> list[tuple[str, str, float]]: with self.harness.terminal_ctx(which): rows = db.session.execute( select( UnloadingGroup.name, UnloadingGroup.distribution_type, UnloadingGroup.value, ) .where( UnloadingGroup.recipe_id == recipe_id, UnloadingGroup.is_deleted.is_(False), ) .order_by(UnloadingGroup.order.asc()) ).all() return [(n, t, float(v)) for n, t, v in rows] def test_reorder_ingredients_and_add_group_reach_both_terminals(self) -> None: SCENARIO_DUAL_REORDER_AND_NEW_GROUP # noqa: F841 with self.harness.server_ctx(): recipe_id, ing_ids, _ = create_recipe_with_children(with_groups=False) grp_id = str(uuid.uuid4()) db.session.add( UnloadingGroup( id=grp_id, name="1 заезд", distribution_type="percent", value=75.0, weight=1000.0, order=1, recipe_id=recipe_id, created_by="system", updated_by="system", ) ) db.session.commit() self._seed_recipe_on_terminals(recipe_id) with self.harness.server_ctx(): recipe = db.session.get(Recipe, recipe_id) ing_a = db.session.get(Ingredient, ing_ids[0]) ing_b = db.session.get(Ingredient, ing_ids[1]) grp1 = db.session.get(UnloadingGroup, grp_id) payload = recipe_update_payload( recipe, ingredients=[ ingredient_payload_from_row(ing_b, order=1), ingredient_payload_from_row(ing_a, order=2), ], groups=[ group_payload_from_row(grp1, order=1), { "name": "2 заезд", "distribution_type": "percent", "value": 25.0, "weight": 400.0, "order": 2, }, ], ) update_recipe_from_payload(recipe_id, payload) db.session.commit() self.harness.drain_sync() for term in ("a", "b"): self.assertEqual( self._ingredient_orders(term, recipe_id), [(ing_ids[1], 1), (ing_ids[0], 2)], msg=f"ingredient order terminal {term}", ) snap = self._group_snapshot(term, recipe_id) self.assertEqual(len(snap), 2, msg=f"groups count terminal {term}") self.assertEqual(snap[0][2], 75.0) self.assertEqual(snap[1], ("2 заезд", "percent", 25.0)) def test_split_groups_60_40_on_both_terminals_not_140_percent(self) -> None: SCENARIO_DUAL_ADD_GROUP_UPDATES_EXISTING # noqa: F841 with self.harness.server_ctx(): recipe_id, ing_ids, _ = create_recipe_with_children(with_groups=False) grp_id = str(uuid.uuid4()) db.session.add( UnloadingGroup( id=grp_id, name="4 гр 1 зам", distribution_type="percent", value=100.0, weight=1590.0, order=1, recipe_id=recipe_id, created_by="system", updated_by="system", ) ) db.session.commit() self._seed_recipe_on_terminals(recipe_id) with self.harness.server_ctx(): recipe = db.session.get(Recipe, recipe_id) grp1 = db.session.get(UnloadingGroup, grp_id) ings = [ ingredient_payload_from_row(db.session.get(Ingredient, iid)) for iid in ing_ids ] payload = recipe_update_payload( recipe, ingredients=ings, groups=[ { **group_payload_from_row(grp1, order=1), "value": 60.0, }, { "name": "еще одна", "distribution_type": "percent", "value": 40.0, "weight": 635.0, "order": 2, }, ], ) update_recipe_from_payload(recipe_id, payload) db.session.commit() self.harness.drain_sync() for term in ("a", "b"): snap = self._group_snapshot(term, recipe_id) self.assertEqual(len(snap), 2, msg=f"terminal {term}") self.assertEqual(snap[0][2], 60.0, msg=f"grp1 value terminal {term}") self.assertEqual(snap[1][2], 40.0, msg=f"grp2 value terminal {term}") total_percent = sum(v for _, t, v in snap if t == "percent") self.assertAlmostEqual(total_percent, 100.0, places=1) def test_dry_matter_locked_syncs_to_both_terminals(self) -> None: with self.harness.server_ctx(): recipe_id, ing_ids, _ = create_recipe_with_children(with_groups=False) recipe = db.session.get(Recipe, recipe_id) recipe.dry_matter_locked = True db.session.commit() self._seed_recipe_on_terminals(recipe_id) with self.harness.server_ctx(): recipe = db.session.get(Recipe, recipe_id) comp_id = db.session.get(Ingredient, ing_ids[0]).component_id update_recipe_from_payload( recipe_id, recipe_update_payload( recipe, ingredients=[ { "id": ing_ids[0], "component_id": comp_id, "dry_matter": 50.0, "dry_matter_per_head": 1.0, "order": 1, } ], groups=[], ) | {"dry_matter_locked": True, "heads_count": 10, "trip_percent": 100.0}, ) db.session.commit() self.harness.drain_sync() for term in ("a", "b"): row = self.harness.recipe_on_terminal(term, recipe_id) self.assertTrue(row.dry_matter_locked, msg=f"terminal {term}") if __name__ == "__main__": unittest.main()