from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import select from sqlalchemy.orm import Session, selectinload from app.api.deps import AuthSession, require_client_module_access from app.db.session import get_db from app.models.raw_material import RawMaterial, RawMaterialPriceVersion from app.schemas.raw_material import ( RawMaterialCreate, RawMaterialPriceVersionCreate, RawMaterialPriceVersionRead, RawMaterialRead, RawMaterialUpdate, ) from app.services.costing_engine import calculate_raw_material_cost, serialize_raw_material router = APIRouter(prefix="/api/raw-materials", tags=["raw-materials"]) def _serialize_price(material: RawMaterial, price: RawMaterialPriceVersion) -> dict: price_comp = calculate_raw_material_cost(material, price) return { "id": price.id, "market_value": price.market_value, "waste_percentage": price.waste_percentage, "effective_date": price.effective_date, "status": price.status, "notes": price.notes, "created_at": price.created_at, "loss_cost": price_comp.loss_cost, "cost_per_unit": price_comp.cost_per_unit, "cost_per_kg": price_comp.cost_per_kg, } @router.get("", response_model=list[RawMaterialRead]) def list_raw_materials(session: AuthSession = Depends(require_client_module_access("raw_materials")), db: Session = Depends(get_db)): materials = db.scalars( select(RawMaterial) .where(RawMaterial.tenant_id == session.tenant_id) .options(selectinload(RawMaterial.price_versions)) .order_by(RawMaterial.name) ).all() return [serialize_raw_material(material) for material in materials] @router.post("", response_model=RawMaterialRead, status_code=status.HTTP_201_CREATED) def create_raw_material(payload: RawMaterialCreate, session: AuthSession = Depends(require_client_module_access("raw_materials", "edit")), db: Session = Depends(get_db)): material = RawMaterial( tenant_id=session.tenant_id, name=payload.name, supplier=payload.supplier, unit_of_measure=payload.unit_of_measure, kg_per_unit=payload.kg_per_unit, status=payload.status, notes=payload.notes, ) material.price_versions.append( RawMaterialPriceVersion( tenant_id=session.tenant_id, market_value=payload.initial_price.market_value, waste_percentage=payload.initial_price.waste_percentage, effective_date=payload.initial_price.effective_date, status=payload.initial_price.status, notes=payload.initial_price.notes, ) ) db.add(material) db.commit() db.refresh(material) return serialize_raw_material(material) @router.get("/{raw_material_id}", response_model=RawMaterialRead) def get_raw_material(raw_material_id: int, session: AuthSession = Depends(require_client_module_access("raw_materials")), db: Session = Depends(get_db)): material = db.scalar( select(RawMaterial) .where(RawMaterial.id == raw_material_id, RawMaterial.tenant_id == session.tenant_id) .options(selectinload(RawMaterial.price_versions)) ) if material is None: raise HTTPException(status_code=404, detail="Raw material not found") return serialize_raw_material(material) @router.patch("/{raw_material_id}", response_model=RawMaterialRead) def update_raw_material( raw_material_id: int, payload: RawMaterialUpdate, session: AuthSession = Depends(require_client_module_access("raw_materials", "edit")), db: Session = Depends(get_db), ): material = db.scalar( select(RawMaterial) .where(RawMaterial.id == raw_material_id, RawMaterial.tenant_id == session.tenant_id) .options(selectinload(RawMaterial.price_versions)) ) if material is None: raise HTTPException(status_code=404, detail="Raw material not found") for field, value in payload.model_dump(exclude_unset=True).items(): setattr(material, field, value) db.commit() db.refresh(material) return serialize_raw_material(material) @router.post("/{raw_material_id}/prices", response_model=RawMaterialPriceVersionRead, status_code=status.HTTP_201_CREATED) def add_price_version( raw_material_id: int, payload: RawMaterialPriceVersionCreate, session: AuthSession = Depends(require_client_module_access("raw_materials", "edit")), db: Session = Depends(get_db), ): material = db.scalar(select(RawMaterial).where(RawMaterial.id == raw_material_id, RawMaterial.tenant_id == session.tenant_id)) if material is None: raise HTTPException(status_code=404, detail="Raw material not found") price = RawMaterialPriceVersion( tenant_id=session.tenant_id, raw_material_id=raw_material_id, market_value=payload.market_value, waste_percentage=payload.waste_percentage, effective_date=payload.effective_date, status=payload.status, notes=payload.notes, ) db.add(price) db.commit() db.refresh(price) return _serialize_price(material, price) @router.get("/{raw_material_id}/price-history", response_model=list[RawMaterialPriceVersionRead]) def get_price_history(raw_material_id: int, session: AuthSession = Depends(require_client_module_access("raw_materials")), db: Session = Depends(get_db)): material = db.scalar(select(RawMaterial).where(RawMaterial.id == raw_material_id, RawMaterial.tenant_id == session.tenant_id)) if material is None: raise HTTPException(status_code=404, detail="Raw material not found") prices = db.scalars( select(RawMaterialPriceVersion) .where( RawMaterialPriceVersion.raw_material_id == raw_material_id, RawMaterialPriceVersion.tenant_id == session.tenant_id, ) .order_by(RawMaterialPriceVersion.effective_date.desc()) ).all() items = [] for price in prices: items.append(_serialize_price(material, price)) return items