Mix calculator
This commit is contained in:
@@ -11,11 +11,13 @@ from app.db.session import Base
|
||||
from app.main import app
|
||||
from app.models.assumption import FreightCostRule, PackagingCostRule, ProcessCostRule
|
||||
from app.models.client_access import ClientAccessAuditEvent, ClientAccount, ClientFeatureAccess, ClientUser
|
||||
from app.schemas.mix_calculator import MixCalculatorSessionCreate
|
||||
from app.models.mix import Mix, MixIngredient
|
||||
from app.models.product import Product
|
||||
from app.models.raw_material import RawMaterial, RawMaterialPriceVersion
|
||||
from app.services.client_access_service import build_client_access_export, ensure_user_module_permissions, serialize_client_account
|
||||
from app.services.costing_engine import calculate_mix_cost, calculate_product_cost, calculate_raw_material_cost
|
||||
from app.services.mix_calculator_service import calculate_mix_calculator_preview
|
||||
|
||||
|
||||
def build_session() -> Session:
|
||||
@@ -86,30 +88,83 @@ def test_mix_and_product_cost_breakdown():
|
||||
assert product_result["wholesale_price"] == 17.3268
|
||||
|
||||
|
||||
def test_mix_calculator_preview_scales_saved_mix_and_warns_on_fractional_bags():
|
||||
db = build_session()
|
||||
|
||||
maize = RawMaterial(name="Maize", unit_of_measure="tonne", kg_per_unit=1000, status="active")
|
||||
barley = RawMaterial(name="Barley", unit_of_measure="tonne", kg_per_unit=1000, status="active")
|
||||
db.add_all([maize, barley])
|
||||
db.flush()
|
||||
|
||||
mix = Mix(tenant_id="specialty-feeds", client_name="Specialty Feeds", name="Pigeon Mix", status="active", version=1)
|
||||
db.add(mix)
|
||||
db.flush()
|
||||
db.add_all(
|
||||
[
|
||||
MixIngredient(tenant_id="specialty-feeds", mix_id=mix.id, raw_material_id=maize.id, quantity_kg=180),
|
||||
MixIngredient(tenant_id="specialty-feeds", mix_id=mix.id, raw_material_id=barley.id, quantity_kg=100),
|
||||
]
|
||||
)
|
||||
db.flush()
|
||||
|
||||
product = Product(
|
||||
tenant_id="specialty-feeds",
|
||||
client_name="Specialty Feeds",
|
||||
name="Specialty Pigeon Breeder 20kg",
|
||||
mix_id=mix.id,
|
||||
sale_type="standard",
|
||||
own_bag=False,
|
||||
unit_of_measure="20kg bag",
|
||||
items_per_pallet=50,
|
||||
bagging_process="standard_bagging",
|
||||
)
|
||||
db.add(product)
|
||||
db.commit()
|
||||
|
||||
preview = calculate_mix_calculator_preview(
|
||||
db,
|
||||
tenant_id="specialty-feeds",
|
||||
payload=MixCalculatorSessionCreate(
|
||||
mix_date=date(2026, 4, 29),
|
||||
client_name="Specialty Feeds",
|
||||
product_id=product.id,
|
||||
batch_size_kg=550,
|
||||
prepared_by_name="Shift A",
|
||||
notes="Mid-morning run",
|
||||
),
|
||||
)
|
||||
|
||||
assert preview["batch_size_kg"] == 550
|
||||
assert preview["total_bags"] == 27.5
|
||||
assert preview["lines"][0]["required_kg"] == 353.5714
|
||||
assert preview["lines"][1]["required_kg"] == 196.4286
|
||||
assert len(preview["warnings"]) == 1
|
||||
assert "not a whole-bag quantity" in preview["warnings"][0]
|
||||
|
||||
|
||||
def test_root_and_login_endpoints():
|
||||
client = TestClient(app)
|
||||
with TestClient(app) as client:
|
||||
root_response = client.get("/")
|
||||
assert root_response.status_code == 200
|
||||
assert root_response.json()["endpoints"]["client_login"] == "/api/auth/client/login"
|
||||
assert root_response.json()["endpoints"]["admin_login"] == "/api/auth/admin/login"
|
||||
|
||||
root_response = client.get("/")
|
||||
assert root_response.status_code == 200
|
||||
assert root_response.json()["endpoints"]["client_login"] == "/api/auth/client/login"
|
||||
assert root_response.json()["endpoints"]["admin_login"] == "/api/auth/admin/login"
|
||||
client_login_response = client.post(
|
||||
"/api/auth/client/login",
|
||||
json={"email": settings.client_email, "password": settings.client_password},
|
||||
)
|
||||
assert client_login_response.status_code == 200
|
||||
assert client_login_response.json()["email"] == settings.client_email
|
||||
assert client_login_response.json()["tenant_id"] == settings.client_tenant_id
|
||||
assert client_login_response.json()["client_role"] == "superadmin"
|
||||
assert client_login_response.json()["module_permissions"]["client_access"] == "manage"
|
||||
|
||||
client_login_response = client.post(
|
||||
"/api/auth/client/login",
|
||||
json={"email": settings.client_email, "password": settings.client_password},
|
||||
)
|
||||
assert client_login_response.status_code == 200
|
||||
assert client_login_response.json()["email"] == settings.client_email
|
||||
assert client_login_response.json()["tenant_id"] == settings.client_tenant_id
|
||||
assert client_login_response.json()["client_role"] == "superadmin"
|
||||
assert client_login_response.json()["module_permissions"]["client_access"] == "manage"
|
||||
|
||||
admin_login_response = client.post(
|
||||
"/api/auth/admin/login",
|
||||
json={"email": settings.admin_email, "password": settings.admin_password},
|
||||
)
|
||||
assert admin_login_response.status_code == 200
|
||||
assert admin_login_response.json()["email"] == settings.admin_email
|
||||
admin_login_response = client.post(
|
||||
"/api/auth/admin/login",
|
||||
json={"email": settings.admin_email, "password": settings.admin_password},
|
||||
)
|
||||
assert admin_login_response.status_code == 200
|
||||
assert admin_login_response.json()["email"] == settings.admin_email
|
||||
|
||||
|
||||
def test_client_access_export_helpers():
|
||||
@@ -220,6 +275,61 @@ def test_client_access_endpoints():
|
||||
assert len(superadmin_access_response.json()) == 1
|
||||
|
||||
|
||||
def test_mix_calculator_endpoints_respect_owner_visibility():
|
||||
with TestClient(app) as client:
|
||||
superadmin_login = client.post(
|
||||
"/api/auth/client/login",
|
||||
json={"email": settings.client_email, "password": settings.client_password},
|
||||
)
|
||||
assert superadmin_login.status_code == 200
|
||||
superadmin_headers = {"Authorization": f"Bearer {superadmin_login.json()['token']}"}
|
||||
|
||||
options_response = client.get("/api/mix-calculator/options", headers=superadmin_headers)
|
||||
assert options_response.status_code == 200
|
||||
assert options_response.json()["products"][0]["product_name"] == "Hunter Orchard Blend 20kg"
|
||||
|
||||
create_response = client.post(
|
||||
"/api/mix-calculator",
|
||||
json={
|
||||
"mix_date": "2026-04-29",
|
||||
"client_name": "Hunter Premium Produce",
|
||||
"product_id": 1,
|
||||
"batch_size_kg": 560,
|
||||
"prepared_by_name": "Amelia Hart",
|
||||
"notes": "Morning production run",
|
||||
},
|
||||
headers=superadmin_headers,
|
||||
)
|
||||
assert create_response.status_code == 201
|
||||
created = create_response.json()
|
||||
assert created["session_number"].startswith("HPP-20260429-")
|
||||
assert created["total_bags"] == 28
|
||||
assert created["lines"][0]["required_kg"] == 360
|
||||
|
||||
patch_response = client.patch(
|
||||
f"/api/mix-calculator/{created['id']}",
|
||||
json={"batch_size_kg": 550},
|
||||
headers=superadmin_headers,
|
||||
)
|
||||
assert patch_response.status_code == 200
|
||||
assert patch_response.json()["total_bags"] == 27.5
|
||||
assert len(patch_response.json()["warnings"]) == 1
|
||||
|
||||
operator_login = client.post(
|
||||
"/api/auth/client/login",
|
||||
json={"email": "ethan.cole@hunterpremiumproduce.example", "password": settings.client_password},
|
||||
)
|
||||
assert operator_login.status_code == 200
|
||||
operator_headers = {"Authorization": f"Bearer {operator_login.json()['token']}"}
|
||||
|
||||
operator_list_response = client.get("/api/mix-calculator", headers=operator_headers)
|
||||
assert operator_list_response.status_code == 200
|
||||
assert operator_list_response.json() == []
|
||||
|
||||
operator_detail_response = client.get(f"/api/mix-calculator/{created['id']}", headers=operator_headers)
|
||||
assert operator_detail_response.status_code == 404
|
||||
|
||||
|
||||
def test_module_permission_blocks_client_module_access():
|
||||
with TestClient(app) as client:
|
||||
admin_login_response = client.post(
|
||||
@@ -229,7 +339,7 @@ def test_module_permission_blocks_client_module_access():
|
||||
admin_headers = {"Authorization": f"Bearer {admin_login_response.json()['token']}"}
|
||||
access_response = client.get("/api/client-access", headers=admin_headers)
|
||||
first_client = access_response.json()[0]
|
||||
first_user = first_client["users"][0]
|
||||
first_user = next(user for user in first_client["users"] if user["email"] == settings.client_email)
|
||||
|
||||
permission = next(
|
||||
permission for permission in first_user["module_permissions"] if permission["module_key"] == "raw_materials"
|
||||
|
||||
Reference in New Issue
Block a user