2026-04-25 20:43:37 +12:00
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
|
|
|
from sqlalchemy import select
|
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
|
|
2026-04-25 22:51:36 +12:00
|
|
|
from app.api.deps import AuthSession, require_client_session
|
2026-04-25 20:43:37 +12:00
|
|
|
from app.db.session import get_db
|
|
|
|
|
from app.models.mix import Mix, MixIngredient
|
|
|
|
|
from app.models.raw_material import RawMaterial
|
|
|
|
|
from app.schemas.mix import MixCreate, MixIngredientCreate, MixIngredientUpdate, MixRead, MixUpdate
|
|
|
|
|
from app.services.costing_engine import calculate_mix_cost
|
|
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/api/mixes", tags=["mixes"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("", response_model=list[MixRead])
|
2026-04-25 22:51:36 +12:00
|
|
|
def list_mixes(session: AuthSession = Depends(require_client_session), db: Session = Depends(get_db)):
|
|
|
|
|
mixes = db.scalars(select(Mix).where(Mix.tenant_id == session.tenant_id).order_by(Mix.name)).all()
|
2026-04-25 20:43:37 +12:00
|
|
|
return [calculate_mix_cost(db, mix.id) for mix in mixes]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("", response_model=MixRead, status_code=status.HTTP_201_CREATED)
|
2026-04-25 22:51:36 +12:00
|
|
|
def create_mix(payload: MixCreate, session: AuthSession = Depends(require_client_session), db: Session = Depends(get_db)):
|
2026-04-25 20:43:37 +12:00
|
|
|
mix = Mix(
|
2026-04-25 22:51:36 +12:00
|
|
|
tenant_id=session.tenant_id,
|
2026-04-25 20:43:37 +12:00
|
|
|
client_name=payload.client_name,
|
|
|
|
|
name=payload.name,
|
|
|
|
|
status=payload.status,
|
|
|
|
|
version=payload.version,
|
|
|
|
|
notes=payload.notes,
|
|
|
|
|
)
|
|
|
|
|
db.add(mix)
|
|
|
|
|
db.flush()
|
|
|
|
|
for ingredient in payload.ingredients:
|
2026-04-25 22:51:36 +12:00
|
|
|
if db.scalar(
|
|
|
|
|
select(RawMaterial).where(
|
|
|
|
|
RawMaterial.id == ingredient.raw_material_id,
|
|
|
|
|
RawMaterial.tenant_id == session.tenant_id,
|
|
|
|
|
)
|
|
|
|
|
) is None:
|
2026-04-25 20:43:37 +12:00
|
|
|
raise HTTPException(status_code=404, detail=f"Raw material {ingredient.raw_material_id} not found")
|
|
|
|
|
db.add(
|
|
|
|
|
MixIngredient(
|
2026-04-25 22:51:36 +12:00
|
|
|
tenant_id=session.tenant_id,
|
2026-04-25 20:43:37 +12:00
|
|
|
mix_id=mix.id,
|
|
|
|
|
raw_material_id=ingredient.raw_material_id,
|
|
|
|
|
quantity_kg=ingredient.quantity_kg,
|
|
|
|
|
notes=ingredient.notes,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
db.commit()
|
|
|
|
|
return calculate_mix_cost(db, mix.id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/{mix_id}", response_model=MixRead)
|
2026-04-25 22:51:36 +12:00
|
|
|
def get_mix(mix_id: int, session: AuthSession = Depends(require_client_session), db: Session = Depends(get_db)):
|
|
|
|
|
if db.scalar(select(Mix.id).where(Mix.id == mix_id, Mix.tenant_id == session.tenant_id)) is None:
|
2026-04-25 20:43:37 +12:00
|
|
|
raise HTTPException(status_code=404, detail="Mix not found")
|
|
|
|
|
return calculate_mix_cost(db, mix_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.patch("/{mix_id}", response_model=MixRead)
|
2026-04-25 22:51:36 +12:00
|
|
|
def update_mix(mix_id: int, payload: MixUpdate, session: AuthSession = Depends(require_client_session), db: Session = Depends(get_db)):
|
|
|
|
|
mix = db.scalar(select(Mix).where(Mix.id == mix_id, Mix.tenant_id == session.tenant_id))
|
2026-04-25 20:43:37 +12:00
|
|
|
if mix is None:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Mix not found")
|
|
|
|
|
for field, value in payload.model_dump(exclude_unset=True).items():
|
|
|
|
|
setattr(mix, field, value)
|
|
|
|
|
db.commit()
|
|
|
|
|
return calculate_mix_cost(db, mix_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/{mix_id}/ingredients", response_model=MixRead, status_code=status.HTTP_201_CREATED)
|
2026-04-25 22:51:36 +12:00
|
|
|
def add_mix_ingredient(mix_id: int, payload: MixIngredientCreate, session: AuthSession = Depends(require_client_session), db: Session = Depends(get_db)):
|
|
|
|
|
if db.scalar(select(Mix.id).where(Mix.id == mix_id, Mix.tenant_id == session.tenant_id)) is None:
|
2026-04-25 20:43:37 +12:00
|
|
|
raise HTTPException(status_code=404, detail="Mix not found")
|
2026-04-25 22:51:36 +12:00
|
|
|
if db.scalar(select(RawMaterial.id).where(RawMaterial.id == payload.raw_material_id, RawMaterial.tenant_id == session.tenant_id)) is None:
|
2026-04-25 20:43:37 +12:00
|
|
|
raise HTTPException(status_code=404, detail="Raw material not found")
|
2026-04-25 22:51:36 +12:00
|
|
|
db.add(
|
|
|
|
|
MixIngredient(
|
|
|
|
|
tenant_id=session.tenant_id,
|
|
|
|
|
mix_id=mix_id,
|
|
|
|
|
raw_material_id=payload.raw_material_id,
|
|
|
|
|
quantity_kg=payload.quantity_kg,
|
|
|
|
|
notes=payload.notes,
|
|
|
|
|
)
|
|
|
|
|
)
|
2026-04-25 20:43:37 +12:00
|
|
|
db.commit()
|
|
|
|
|
return calculate_mix_cost(db, mix_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.patch("/{mix_id}/ingredients/{ingredient_id}", response_model=MixRead)
|
2026-04-25 22:51:36 +12:00
|
|
|
def update_mix_ingredient(
|
|
|
|
|
mix_id: int,
|
|
|
|
|
ingredient_id: int,
|
|
|
|
|
payload: MixIngredientUpdate,
|
|
|
|
|
session: AuthSession = Depends(require_client_session),
|
|
|
|
|
db: Session = Depends(get_db),
|
|
|
|
|
):
|
|
|
|
|
ingredient = db.scalar(
|
|
|
|
|
select(MixIngredient).where(
|
|
|
|
|
MixIngredient.id == ingredient_id,
|
|
|
|
|
MixIngredient.mix_id == mix_id,
|
|
|
|
|
MixIngredient.tenant_id == session.tenant_id,
|
|
|
|
|
)
|
|
|
|
|
)
|
2026-04-25 20:43:37 +12:00
|
|
|
if ingredient is None:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Ingredient not found")
|
|
|
|
|
for field, value in payload.model_dump(exclude_unset=True).items():
|
|
|
|
|
setattr(ingredient, field, value)
|
|
|
|
|
db.commit()
|
|
|
|
|
return calculate_mix_cost(db, mix_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/{mix_id}/ingredients/{ingredient_id}", response_model=MixRead)
|
2026-04-25 22:51:36 +12:00
|
|
|
def delete_mix_ingredient(mix_id: int, ingredient_id: int, session: AuthSession = Depends(require_client_session), db: Session = Depends(get_db)):
|
|
|
|
|
ingredient = db.scalar(
|
|
|
|
|
select(MixIngredient).where(
|
|
|
|
|
MixIngredient.id == ingredient_id,
|
|
|
|
|
MixIngredient.mix_id == mix_id,
|
|
|
|
|
MixIngredient.tenant_id == session.tenant_id,
|
|
|
|
|
)
|
|
|
|
|
)
|
2026-04-25 20:43:37 +12:00
|
|
|
if ingredient is None:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Ingredient not found")
|
|
|
|
|
db.delete(ingredient)
|
|
|
|
|
db.commit()
|
|
|
|
|
return calculate_mix_cost(db, mix_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/{mix_id}/cost-breakdown", response_model=MixRead)
|
2026-04-25 22:51:36 +12:00
|
|
|
def get_mix_cost_breakdown(mix_id: int, session: AuthSession = Depends(require_client_session), db: Session = Depends(get_db)):
|
|
|
|
|
if db.scalar(select(Mix.id).where(Mix.id == mix_id, Mix.tenant_id == session.tenant_id)) is None:
|
2026-04-25 20:43:37 +12:00
|
|
|
raise HTTPException(status_code=404, detail="Mix not found")
|
|
|
|
|
return calculate_mix_cost(db, mix_id)
|