@@ -0,0 +1,133 @@
|
||||
"""Хелперы: мастер-рацион vs план на день vs экран оператора загрузки."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
from typing import Any
|
||||
|
||||
from flask.testing import FlaskClient
|
||||
|
||||
DAILY_PLAN_SYNC_TABLES: tuple[str, ...] = (
|
||||
"daily_trip_skip",
|
||||
"daily_ingredient_skip",
|
||||
"daily_unloading_group_skip",
|
||||
"daily_ingredient_replacement",
|
||||
"daily_component_norm_adjustment",
|
||||
)
|
||||
|
||||
KIOSK_HEADERS = {"X-Wesp-Kiosk": "1"}
|
||||
|
||||
|
||||
def plan_date_today() -> str:
|
||||
return date.today().isoformat()
|
||||
|
||||
|
||||
def master_recipe_json(client: FlaskClient, recipe_id: str) -> dict[str, Any]:
|
||||
resp = client.get(f"/api/recipes/{recipe_id}")
|
||||
assert resp.status_code == 200, resp.get_data(as_text=True)
|
||||
return resp.get_json()
|
||||
|
||||
|
||||
def plan_recipe_json(
|
||||
client: FlaskClient,
|
||||
recipe_id: str,
|
||||
*,
|
||||
plan_date: str | None = None,
|
||||
kiosk: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
iso = (plan_date or plan_date_today())[:10]
|
||||
headers = KIOSK_HEADERS if kiosk else {}
|
||||
resp = client.get(
|
||||
f"/api/recipes/{recipe_id}?date={iso}",
|
||||
headers=headers,
|
||||
)
|
||||
assert resp.status_code == 200, resp.get_data(as_text=True)
|
||||
return resp.get_json()
|
||||
|
||||
|
||||
def first_ingredient_amount(payload: dict[str, Any]) -> float:
|
||||
ingredients = payload.get("ingredients") or []
|
||||
assert ingredients, "expected at least one ingredient"
|
||||
return float(ingredients[0].get("amount") or 0)
|
||||
|
||||
|
||||
def ingredient_amounts(payload: dict[str, Any]) -> list[float]:
|
||||
return [float(i.get("amount") or 0) for i in (payload.get("ingredients") or [])]
|
||||
|
||||
|
||||
def operator_weight_display(client: FlaskClient, recipe_id: str) -> dict[str, Any]:
|
||||
set_resp = client.post("/api/set_current_recipe", json={"recipe_id": recipe_id})
|
||||
assert set_resp.status_code == 200, set_resp.get_data(as_text=True)
|
||||
display = client.get("/api/weight_display_data")
|
||||
assert display.status_code == 200, display.get_data(as_text=True)
|
||||
return display.get_json()
|
||||
|
||||
|
||||
def assert_operator_matches_plan_not_master(
|
||||
client: FlaskClient,
|
||||
recipe_id: str,
|
||||
*,
|
||||
plan_date: str | None = None,
|
||||
expect_plan_differs_from_master: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
"""Оператор (weight_display) = план; мастер — базовый рецепт без date=."""
|
||||
master = master_recipe_json(client, recipe_id)
|
||||
plan = plan_recipe_json(client, recipe_id, plan_date=plan_date)
|
||||
operator = operator_weight_display(client, recipe_id)
|
||||
|
||||
assert operator.get("status") == "active", operator
|
||||
plan_amounts = ingredient_amounts(plan)
|
||||
master_amounts = ingredient_amounts(master)
|
||||
assert plan_amounts, "plan must expose ingredients"
|
||||
|
||||
if expect_plan_differs_from_master:
|
||||
assert plan_amounts != master_amounts or plan.get("ingredients") != master.get(
|
||||
"ingredients"
|
||||
), "plan should differ from master for this scenario"
|
||||
|
||||
assert operator["total_component"] == int(round(plan_amounts[0]))
|
||||
assert operator["total_mixture"] == int(round(sum(plan_amounts)))
|
||||
assert operator["total_component"] != int(round(master_amounts[0])) or len(
|
||||
plan_amounts
|
||||
) != len(master_amounts)
|
||||
|
||||
return {
|
||||
"master": master,
|
||||
"plan": plan,
|
||||
"operator": operator,
|
||||
"master_amounts": master_amounts,
|
||||
"plan_amounts": plan_amounts,
|
||||
}
|
||||
|
||||
|
||||
def assert_daily_plan_tables_registered() -> None:
|
||||
from app.models import (
|
||||
DailyComponentNormAdjustment,
|
||||
DailyIngredientReplacement,
|
||||
DailyIngredientSkip,
|
||||
DailyTripSkip,
|
||||
DailyUnloadingGroupSkip,
|
||||
_sync_models,
|
||||
)
|
||||
from app.services.sync_manager import SERVER_MASTER_TABLES, SNAPSHOT_MODELS
|
||||
from app.services.sync_record_data import get_record_data_for_sync
|
||||
|
||||
model_by_table = {
|
||||
DailyTripSkip.__tablename__: DailyTripSkip,
|
||||
DailyIngredientSkip.__tablename__: DailyIngredientSkip,
|
||||
DailyUnloadingGroupSkip.__tablename__: DailyUnloadingGroupSkip,
|
||||
DailyIngredientReplacement.__tablename__: DailyIngredientReplacement,
|
||||
DailyComponentNormAdjustment.__tablename__: DailyComponentNormAdjustment,
|
||||
}
|
||||
assert set(DAILY_PLAN_SYNC_TABLES) == set(model_by_table)
|
||||
|
||||
sync_model_tables = {m.__tablename__ for m in _sync_models if m.__tablename__.startswith("daily_")}
|
||||
assert sync_model_tables == set(DAILY_PLAN_SYNC_TABLES)
|
||||
|
||||
snapshot_tables = {m.__tablename__ for m in SNAPSHOT_MODELS}
|
||||
for table in DAILY_PLAN_SYNC_TABLES:
|
||||
assert table in snapshot_tables, f"{table} missing from SNAPSHOT_MODELS"
|
||||
assert table in SERVER_MASTER_TABLES, f"{table} missing from SERVER_MASTER_TABLES"
|
||||
|
||||
# smoke: get_record_data_for_sync resolves model (None без строки — ок)
|
||||
assert get_record_data_for_sync("daily_trip_skip", "nonexistent-id") is None
|
||||
Reference in New Issue
Block a user