from __future__ import annotations from datetime import UTC, datetime from uuid import uuid4 import pytest from fastapi.testclient import TestClient from app.modules.zootech.catalog_apply import apply_catalog_change from tests.orchestrator_dual_harness import make_enterprise def test_wesp_compat_feed_dispensers(client: TestClient): enterprise_id = make_enterprise(client) login = client.post("/api/v1/auth/login", json={"email": "admin@compton.example", "password": "Admin1234"}) token = login.json()["access_token"] session = client.post( f"/api/v1/enterprise/enterprises/{enterprise_id}/session", headers={"Authorization": f"Bearer {token}"}, ) token = session.json()["access_token"] headers = {"Authorization": f"Bearer {token}"} resp = client.get(f"/api/feed_dispensers?enterprise_id={enterprise_id}", headers=headers) assert resp.status_code == 200 assert isinstance(resp.json(), list) def test_wesp_compat_feed_dispenser_crud(client: TestClient): enterprise_id = make_enterprise(client) login = client.post("/api/v1/auth/login", json={"email": "admin@compton.example", "password": "Admin1234"}) token = login.json()["access_token"] session = client.post( f"/api/v1/enterprise/enterprises/{enterprise_id}/session", headers={"Authorization": f"Bearer {token}"}, ) token = session.json()["access_token"] headers = {"Authorization": f"Bearer {token}"} params = {"enterprise_id": enterprise_id} create = client.post( "/api/feed_dispensers", headers=headers, params=params, json={"name": "Disp A", "farm": "Farm 1", "operator": "Op 1", "type": "dispenser"}, ) assert create.status_code == 200 dispenser_id = create.json()["id"] listed = client.get("/api/feed_dispensers", headers=headers, params=params) assert listed.status_code == 200 assert any(row["id"] == dispenser_id for row in listed.json()) update = client.put( f"/api/feed_dispensers/{dispenser_id}", headers=headers, params=params, json={"name": "Disp B", "farm": "Farm 2", "operator": "Op 2"}, ) assert update.status_code == 200 assert update.json().get("message") period = client.post( f"/api/feed_dispensers/{dispenser_id}/periods", headers=headers, params=params, json={"name": "Morning"}, ) assert period.status_code == 200 period_id = period.json()["id"] period_update = client.put( f"/api/feed_dispensers/{dispenser_id}/periods/{period_id}", headers=headers, params=params, json={"name": "Evening"}, ) assert period_update.status_code == 200 period_delete = client.delete( f"/api/feed_dispensers/{dispenser_id}/periods/{period_id}", headers=headers, params=params, ) assert period_delete.status_code == 200 delete = client.delete( f"/api/feed_dispensers/{dispenser_id}", headers=headers, params=params, ) assert delete.status_code == 200 def test_wesp_compat_components(client: TestClient): enterprise_id = make_enterprise(client) login = client.post("/api/v1/auth/login", json={"email": "admin@compton.example", "password": "Admin1234"}) token = login.json()["access_token"] session = client.post( f"/api/v1/enterprise/enterprises/{enterprise_id}/session", headers={"Authorization": f"Bearer {token}"}, ) token = session.json()["access_token"] headers = {"Authorization": f"Bearer {token}"} resp = client.get(f"/api/components?enterprise_id={enterprise_id}", headers=headers) assert resp.status_code == 200 assert isinstance(resp.json(), list) def test_wesp_compat_component_feed_types(client: TestClient): enterprise_id = make_enterprise(client) login = client.post("/api/v1/auth/login", json={"email": "admin@compton.example", "password": "Admin1234"}) token = login.json()["access_token"] session = client.post( f"/api/v1/enterprise/enterprises/{enterprise_id}/session", headers={"Authorization": f"Bearer {token}"}, ) token = session.json()["access_token"] headers = {"Authorization": f"Bearer {token}"} resp = client.get(f"/api/components/feed-types?enterprise_id={enterprise_id}", headers=headers) assert resp.status_code == 200 body = resp.json() assert isinstance(body.get("types"), list) assert len(body["types"]) >= 4 assert body["types"][0]["value"] def test_wesp_compat_recipe_includes_ingredients_and_unloading_groups(client: TestClient): enterprise_id = make_enterprise(client) login = client.post("/api/v1/auth/login", json={"email": "admin@compton.example", "password": "Admin1234"}) token = login.json()["access_token"] session = client.post( f"/api/v1/enterprise/enterprises/{enterprise_id}/session", headers={"Authorization": f"Bearer {token}"}, ) token = session.json()["access_token"] headers = {"Authorization": f"Bearer {token}"} recipe_id = "recipe-compat-1" ing_id = "ing-compat-1" grp_id = "grp-compat-1" apply_catalog_change( enterprise_id, "recipe", recipe_id, "upsert", { "id": recipe_id, "name": "Compat Mix", "heads_per_trip": 100, "mixing_time": 10, "trip_percent": 100.0, }, 1, "r1", ) apply_catalog_change( enterprise_id, "ingredient", ing_id, "upsert", { "id": ing_id, "recipe_id": recipe_id, "name": "Corn", "amount": 12.5, "order": 1, "dry_matter": 88.0, }, 1, "i1", ) apply_catalog_change( enterprise_id, "unloading_group", grp_id, "upsert", { "id": grp_id, "recipe_id": recipe_id, "name": "Barn A", "distribution_type": "percent", "value": 50.0, "weight": 500.0, "order": 1, }, 1, "g1", ) resp = client.get(f"/api/recipes/{recipe_id}?enterprise_id={enterprise_id}", headers=headers) assert resp.status_code == 200 body = resp.json() assert body["name"] == "Compat Mix" assert len(body.get("ingredients") or []) == 1 assert body["ingredients"][0]["name"] == "Corn" assert len(body.get("unloading_groups") or body.get("unloadingGroups") or []) == 1 assert body["unloading_groups"][0]["name"] == "Barn A" def test_wesp_compat_recipe_calculate(client: TestClient): resp = client.post( "/api/recipes/calculate", json={ "ingredients": [{"weightPerHead": 10, "dryMatter": 88, "component_id": "c1"}], "headsCount": 100, "tripPercent": 100, "unloadingGroups": [{"distributionType": "percent", "value": 50}], }, ) assert resp.status_code == 200 body = resp.json() assert len(body.get("ingredients") or []) == 1 assert len(body.get("unloadingGroups") or []) == 1 def test_wesp_compat_recipe_put_persists_unloading_group(client: TestClient): enterprise_id = make_enterprise(client) login = client.post("/api/v1/auth/login", json={"email": "admin@compton.example", "password": "Admin1234"}) token = login.json()["access_token"] session = client.post( f"/api/v1/enterprise/enterprises/{enterprise_id}/session", headers={"Authorization": f"Bearer {token}"}, ) token = session.json()["access_token"] headers = {"Authorization": f"Bearer {token}"} recipe_id = "recipe-put-1" apply_catalog_change( enterprise_id, "recipe", recipe_id, "upsert", {"id": recipe_id, "name": "Before", "heads_per_trip": 100, "mixing_time": 10, "trip_percent": 100.0}, 1, "r1", ) put = client.put( f"/api/recipes/{recipe_id}?enterprise_id={enterprise_id}", headers=headers, json={ "name": "After", "heads_count": 100, "mixing_time": 10, "trip_percent": 100, "dry_matter_locked": False, "unloading_link_broken": False, "ingredients": [], "unloading_groups": [ { "name": "Group A", "distribution_type": "percent", "value": 100, "weight": 500, "order": 1, } ], }, ) assert put.status_code == 200 got = client.get(f"/api/recipes/{recipe_id}?enterprise_id={enterprise_id}", headers=headers) assert got.status_code == 200 groups = got.json().get("unloading_groups") or [] assert len(groups) == 1 assert groups[0]["name"] == "Group A" def test_wesp_compat_analytics_endpoints(client: TestClient): enterprise_id = make_enterprise(client) login = client.post("/api/v1/auth/login", json={"email": "admin@compton.example", "password": "Admin1234"}) token = login.json()["access_token"] session = client.post( f"/api/v1/enterprise/enterprises/{enterprise_id}/session", headers={"Authorization": f"Bearer {token}"}, ) token = session.json()["access_token"] headers = {"Authorization": f"Bearer {token}"} params = f"enterprise_id={enterprise_id}&date_from=2026-07-01&date_to=2026-07-15" finance = client.get(f"/api/analytics/finance?{params}", headers=headers) assert finance.status_code == 200 assert "overloadRub" in finance.json() plan_fact = client.get(f"/api/analytics/plan-fact?{params}", headers=headers) assert plan_fact.status_code == 200 assert plan_fact.json()["items"] == [] summary = client.get(f"/api/feed-quality/alerts/summary?{params}", headers=headers) assert summary.status_code == 200 assert "total" in summary.json() alerts = client.get(f"/api/feed-quality/alerts?{params}", headers=headers) assert alerts.status_code == 200 assert "items" in alerts.json() fq_settings = client.get(f"/api/feed-quality/settings?enterprise_id={enterprise_id}", headers=headers) assert fq_settings.status_code == 200 fq_body = fq_settings.json() assert "settings" in fq_body assert "defaults" in fq_body assert fq_body["settings"]["loading"]["enabled"] is True fq_put = client.put( f"/api/feed-quality/settings?enterprise_id={enterprise_id}", headers=headers, json={"settings": {"loading": {"warning_pct": 12}}}, ) assert fq_put.status_code == 200 assert fq_put.json()["saved"] is True assert fq_put.json()["settings"]["loading"]["warning_pct"] == 12.0 def test_wesp_compat_reports_list(client: TestClient): from app.modules.zootech.report_apply import apply_report_change enterprise_id = make_enterprise(client) login = client.post("/api/v1/auth/login", json={"email": "admin@compton.example", "password": "Admin1234"}) token = login.json()["access_token"] session = client.post( f"/api/v1/enterprise/enterprises/{enterprise_id}/session", headers={"Authorization": f"Bearer {token}"}, ) token = session.json()["access_token"] headers = {"Authorization": f"Bearer {token}"} loading_id = "lr-test-1" apply_report_change( enterprise_id, "loading_report", loading_id, "upsert", { "recipe_id": "recipe-1", "recipe_name": "Test Mix", "start_time": datetime.now(UTC).isoformat(), "end_time": datetime.now(UTC).isoformat(), "total_weight": 1200.0, "dispenser_type": "dispenser", "components": [{"component_name": "Corn", "actual_weight": 100.0}], }, 1, "h1", ) apply_report_change( enterprise_id, "unloading_report", "ur-test-1", "upsert", { "loading_report_id": loading_id, "start_time": datetime.now(UTC).isoformat(), "total_weight": 1200.0, "total_unloaded_weight": 1100.0, "remaining_weight": 100.0, }, 1, "u1", ) resp = client.get(f"/api/reports?enterprise_id={enterprise_id}", headers=headers) assert resp.status_code == 200 body = resp.json() assert isinstance(body, list) assert len(body) == 1 assert body[0]["recipe_id"] == "recipe-1" assert body[0]["unloading_data"]["total_unloaded_weight"] == 1100.0 def test_apply_report_change_update_adds_components(client: TestClient): import json from app.core.database import session_scope from app.modules.zootech.report_apply import apply_report_change from app.modules.zootech.report_models import ZootechLoadingReport from sqlalchemy import select enterprise_id = make_enterprise(client) loading_id = "lr-test-components" apply_report_change( enterprise_id, "loading_report", loading_id, "upsert", { "recipe_id": "recipe-1", "recipe_name": "Test Mix", "start_time": datetime.now(UTC).isoformat(), "total_weight": 1200.0, }, 1, "h1", ) apply_report_change( enterprise_id, "loading_report", loading_id, "upsert", { "recipe_id": "recipe-1", "recipe_name": "Test Mix", "start_time": datetime.now(UTC).isoformat(), "total_weight": 1200.0, "components": [{"component_name": "Corn", "actual_weight": 100.0, "overload": 5.0}], }, 2, "h2", ) with session_scope() as db: row = db.scalar( select(ZootechLoadingReport).where( ZootechLoadingReport.enterprise_id == enterprise_id, ZootechLoadingReport.id == loading_id, ) ) assert row is not None payload = json.loads(row.payload_json or "{}") assert len(payload.get("components") or []) == 1 assert payload["components"][0]["component_name"] == "Corn" def test_apply_report_change_feed_alert(client: TestClient): from app.core.database import session_scope from app.modules.zootech.report_apply import apply_report_change from app.modules.zootech.report_models import ZootechFeedAlert from sqlalchemy import select enterprise_id = make_enterprise(client) alert_id = "fa-test-1" apply_report_change( enterprise_id, "feed_alert", alert_id, "upsert", { "event_type": "UNDERLOAD", "severity": "error", "loading_report_id": "lr-1", "recipe_id": "recipe-1", "recipe_name": "Mix", "detail": "Underload detected", "deviation_kg": -5.0, }, 1, "h1", ) with session_scope() as db: row = db.scalar( select(ZootechFeedAlert).where( ZootechFeedAlert.enterprise_id == enterprise_id, ZootechFeedAlert.id == alert_id, ) ) assert row is not None assert row.alert_type == "UNDERLOAD" assert row.message == "Underload detected" assert row.payload_json def test_wesp_compat_transfer_recipe_between_periods(client: TestClient): enterprise_id = make_enterprise(client) login = client.post("/api/v1/auth/login", json={"email": "admin@compton.example", "password": "Admin1234"}) token = login.json()["access_token"] session = client.post( f"/api/v1/enterprise/enterprises/{enterprise_id}/session", headers={"Authorization": f"Bearer {token}"}, ) token = session.json()["access_token"] headers = {"Authorization": f"Bearer {token}"} params = {"enterprise_id": enterprise_id} dispenser = client.post( "/api/feed_dispensers", headers=headers, params=params, json={"name": "Disp transfer", "farm": "Farm", "operator": "Op", "type": "dispenser"}, ) assert dispenser.status_code == 200 dispenser_id = dispenser.json()["id"] period_a = client.post( f"/api/feed_dispensers/{dispenser_id}/periods", headers=headers, params=params, json={"name": "Morning"}, ) period_b = client.post( f"/api/feed_dispensers/{dispenser_id}/periods", headers=headers, params=params, json={"name": "Evening"}, ) assert period_a.status_code == 200 assert period_b.status_code == 200 period_a_id = period_a.json()["id"] period_b_id = period_b.json()["id"] recipe = client.post( f"/api/periods/{period_a_id}/recipes", headers=headers, params=params, json={ "name": "Trip A", "heads_count": 100, "mixing_time": 10, "trip_percent": 100, "ingredients": [], "unloading_groups": [{"name": "G1", "distribution_type": "percent", "value": 100}], }, ) assert recipe.status_code == 200 recipe_id = recipe.json()["id"] transfer = client.post( f"/api/feed_dispensers/{dispenser_id}/periods/{period_b_id}/recipes/{recipe_id}/transfer", headers=headers, params=params, json={ "from_dispenser_id": dispenser_id, "from_period_id": period_a_id, "to_index": 0, }, ) assert transfer.status_code == 200 assert transfer.json().get("success") is True src = client.get(f"/api/periods/{period_a_id}/recipes", headers=headers, params=params) dst = client.get(f"/api/periods/{period_b_id}/recipes", headers=headers, params=params) assert src.status_code == 200 assert dst.status_code == 200 assert src.json() == [] assert len(dst.json()) == 1 assert dst.json()[0]["id"] == recipe_id def test_wesp_compat_move_recipe_within_period(client: TestClient): enterprise_id = make_enterprise(client) login = client.post("/api/v1/auth/login", json={"email": "admin@compton.example", "password": "Admin1234"}) token = login.json()["access_token"] session = client.post( f"/api/v1/enterprise/enterprises/{enterprise_id}/session", headers={"Authorization": f"Bearer {token}"}, ) token = session.json()["access_token"] headers = {"Authorization": f"Bearer {token}"} params = {"enterprise_id": enterprise_id} dispenser = client.post( "/api/feed_dispensers", headers=headers, params=params, json={"name": "Disp move", "farm": "Farm", "operator": "Op", "type": "dispenser"}, ) assert dispenser.status_code == 200 dispenser_id = dispenser.json()["id"] period = client.post( f"/api/feed_dispensers/{dispenser_id}/periods", headers=headers, params=params, json={"name": "Morning"}, ) assert period.status_code == 200 period_id = period.json()["id"] def _create_recipe(name: str) -> str: resp = client.post( f"/api/periods/{period_id}/recipes", headers=headers, params=params, json={ "name": name, "heads_count": 100, "mixing_time": 10, "trip_percent": 100, "ingredients": [], "unloading_groups": [{"name": "G1", "distribution_type": "percent", "value": 100}], }, ) assert resp.status_code == 200 return resp.json()["id"] first_id = _create_recipe("First") second_id = _create_recipe("Second") move = client.put( f"/api/recipes/{first_id}/move", headers=headers, params=params, json={"from_index": 0, "to_index": 1, "period_id": period_id}, ) assert move.status_code == 200 assert move.json().get("success") is True listed = client.get(f"/api/periods/{period_id}/recipes", headers=headers, params=params) assert listed.status_code == 200 ids = [row["id"] for row in listed.json()] assert ids == [second_id, first_id] def test_wesp_compat_sklad(client: TestClient): from datetime import date, timedelta from app.modules.zootech.report_apply import apply_report_change enterprise_id = make_enterprise(client) login = client.post("/api/v1/auth/login", json={"email": "admin@compton.example", "password": "Admin1234"}) token = login.json()["access_token"] session = client.post( f"/api/v1/enterprise/enterprises/{enterprise_id}/session", headers={"Authorization": f"Bearer {token}"}, ) token = session.json()["access_token"] headers = {"Authorization": f"Bearer {token}"} params = {"enterprise_id": enterprise_id} component = client.post( "/api/components", headers=headers, params=params, json={"name": "Corn", "type": "grain", "dryMatter": 88, "protein": 9, "energy": 13, "price": 15}, ) assert component.status_code == 200 component_id = component.json()["id"] today = date.today() date_from = (today - timedelta(days=30)).isoformat() date_to = today.isoformat() empty = client.get("/api/sklad", headers=headers, params={**params, "date_from": date_from, "date_to": date_to}) assert empty.status_code == 200 assert empty.json()["items"] == [] apply_report_change( enterprise_id, "loading_report", "lr-sklad-1", "upsert", { "recipe_id": "recipe-1", "recipe_name": "Mix", "start_time": datetime.now(UTC).isoformat(), "total_weight": 500.0, "components": [ {"component_id": component_id, "component_name": "Corn", "actual_weight": 42.5}, ], }, 1, "h1", ) report_only = client.get("/api/sklad", headers=headers, params={**params, "date_from": date_from, "date_to": date_to}) assert report_only.status_code == 200 items = report_only.json()["items"] assert len(items) == 1 assert items[0]["component_id"] == component_id assert items[0]["has_stock"] is False assert items[0]["consumed_total_kg"] == 42.5 added = client.post("/api/sklad/add", headers=headers, params=params, json={"component_id": component_id}) assert added.status_code == 200 assert added.json()["success"] is True updated = client.post( f"/api/sklad/{component_id}", headers=headers, params=params, json={"total_kg": 1000.0, "inflow_kg": 50.0}, ) assert updated.status_code == 200 assert updated.json()["success"] is True stocked = client.get("/api/sklad", headers=headers, params={**params, "date_from": date_from, "date_to": date_to}) assert stocked.status_code == 200 stocked_items = stocked.json()["items"] assert len(stocked_items) == 1 assert stocked_items[0]["has_stock"] is True assert stocked_items[0]["remaining_kg"] == 1007.5 chart = client.get( f"/api/sklad/{component_id}/chart", headers=headers, params={**params, "date_from": date_from, "date_to": date_to}, ) assert chart.status_code == 200 assert isinstance(chart.json().get("points"), list) assert len(chart.json()["points"]) > 0 removed = client.delete(f"/api/sklad/{component_id}", headers=headers, params=params) assert removed.status_code == 200 assert removed.json()["success"] is True def test_wesp_compat_feed_accounting(client: TestClient): from app.modules.zootech.report_apply import apply_report_change enterprise_id = make_enterprise(client) login = client.post("/api/v1/auth/login", json={"email": "admin@compton.example", "password": "Admin1234"}) token = login.json()["access_token"] session = client.post( f"/api/v1/enterprise/enterprises/{enterprise_id}/session", headers={"Authorization": f"Bearer {token}"}, ) token = session.json()["access_token"] headers = {"Authorization": f"Bearer {token}"} params = {"enterprise_id": enterprise_id} org = client.get("/api/feed-accounting/org-settings", headers=headers, params=params) assert org.status_code == 200 body = org.json() assert "organization_name" in body assert "available_farms" in body assert body["signatures"]["zootechnician"] is False saved = client.put( "/api/feed-accounting/org-settings", headers=headers, params=params, json={ "organization_name": "Test Farm LLC", "zootechnician": "Ivanov I.I.", "warehouse_keeper": "Petrov P.P.", }, ) assert saved.status_code == 200 assert saved.json().get("success") is True apply_report_change( enterprise_id, "loading_report", "lr-fa-1", "upsert", { "recipe_id": "recipe-1", "recipe_name": "Mix", "start_time": "2026-06-15T10:00:00+00:00", "total_weight": 500.0, "components": [{"component_name": "Corn", "actual_weight": 25.0}], }, 1, "h1", ) preview = client.get( "/api/feed-accounting/sp20-preview", headers=headers, params={**params, "month": "2026-06"}, ) assert preview.status_code == 200 preview_body = preview.json() assert preview_body["lines_count"] == 1 assert preview_body["total_kg"] == 25.0 assert preview_body["has_requisites"] is True xlsx = client.get( "/api/feed-accounting/consumption.xlsx", headers=headers, params={**params, "date_from": "2026-06-01", "date_to": "2026-06-30"}, ) assert xlsx.status_code == 200 assert "spreadsheetml" in (xlsx.headers.get("content-type") or "") assert len(xlsx.content) > 100 pdf = client.get( "/api/feed-accounting/sp20.pdf", headers=headers, params={**params, "month": "2026-06"}, ) assert pdf.status_code == 200 assert (pdf.headers.get("content-type") or "").startswith("application/pdf") assert pdf.content[:4] == b"%PDF" def test_wesp_compat_mill_recipes_list_after_create(client: TestClient): enterprise_id = make_enterprise(client) login = client.post("/api/v1/auth/login", json={"email": "admin@compton.example", "password": "Admin1234"}) token = login.json()["access_token"] session = client.post( f"/api/v1/enterprise/enterprises/{enterprise_id}/session", headers={"Authorization": f"Bearer {token}"}, ) token = session.json()["access_token"] headers = {"Authorization": f"Bearer {token}"} params = {"enterprise_id": enterprise_id} mill = client.post( "/api/feed_dispensers", headers=headers, params=params, json={"name": "Mill 1", "farm": "Farm 1", "operator": "Op 1", "type": "mill"}, ) assert mill.status_code == 200 mill_id = mill.json()["id"] empty = client.get(f"/api/feed_dispensers/{mill_id}/recipes", headers=headers, params=params) assert empty.status_code == 200 assert empty.json() == [] created = client.post( "/api/recipes", headers=headers, params=params, json={ "name": "Mill Mix", "heads_count": 50, "mixing_time": 5, "trip_percent": 100, "dry_matter_locked": False, "unloading_link_broken": False, "dispenser_id": mill_id, "ingredients": [], "unloading_groups": [], }, ) assert created.status_code == 200 recipe_id = created.json()["id"] listed = client.get(f"/api/feed_dispensers/{mill_id}/recipes", headers=headers, params=params) assert listed.status_code == 200 recipes = listed.json() assert len(recipes) == 1 assert recipes[0]["id"] == recipe_id assert recipes[0]["name"] == "Mill Mix"