from fastapi import FastAPI, Depends, HTTPException, APIRouter from fastapi.middleware.cors import CORSMiddleware from sqlalchemy.orm import Session from typing import List, Optional from datetime import datetime from .database import engine, get_db, Base from .models import Drug, DrugVariant, Dispensing from pydantic import BaseModel # Create tables Base.metadata.create_all(bind=engine) app = FastAPI(title="Drug Inventory API") # CORS middleware for frontend app.add_middleware( CORSMiddleware, allow_origins=["*"], # In production, restrict this allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Create a router with /api prefix router = APIRouter(prefix="/api") # Pydantic schemas class DrugCreate(BaseModel): name: str description: str = None class DrugUpdate(BaseModel): name: str = None description: str = None class DrugResponse(BaseModel): id: int name: str description: str = None class Config: from_attributes = True class DrugVariantCreate(BaseModel): strength: str quantity: float unit: str = "units" low_stock_threshold: float = 10 class DrugVariantUpdate(BaseModel): strength: str = None quantity: float = None unit: str = None low_stock_threshold: float = None class DrugVariantResponse(BaseModel): id: int drug_id: int strength: str quantity: float unit: str low_stock_threshold: float class Config: from_attributes = True class DrugWithVariantsResponse(BaseModel): id: int name: str description: str = None variants: List[DrugVariantResponse] = [] class Config: from_attributes = True class DispensingCreate(BaseModel): drug_variant_id: int quantity: float animal_name: str user_name: str notes: Optional[str] = None class DispensingResponse(BaseModel): id: int drug_variant_id: int quantity: float animal_name: str user_name: str notes: Optional[str] = None dispensed_at: datetime class Config: from_attributes = True # Routes @router.get("/") def read_root(): return {"message": "Drug Inventory API"} @router.get("/drugs", response_model=List[DrugWithVariantsResponse]) def list_drugs(db: Session = Depends(get_db)): """Get all drugs with their variants""" drugs = db.query(Drug).all() result = [] for drug in drugs: variants = db.query(DrugVariant).filter(DrugVariant.drug_id == drug.id).all() drug_dict = drug.__dict__.copy() drug_dict['variants'] = variants result.append(drug_dict) return result @router.get("/drugs/low-stock", response_model=List[DrugWithVariantsResponse]) def low_stock_drugs(db: Session = Depends(get_db)): """Get drugs with low stock variants""" # Get variants that are low on stock low_stock_variants = db.query(DrugVariant).filter( DrugVariant.quantity <= DrugVariant.low_stock_threshold ).all() # Get unique drug IDs drug_ids = list(set(v.drug_id for v in low_stock_variants)) drugs = db.query(Drug).filter(Drug.id.in_(drug_ids)).all() result = [] for drug in drugs: variants = [v for v in low_stock_variants if v.drug_id == drug.id] drug_dict = drug.__dict__.copy() drug_dict['variants'] = variants result.append(drug_dict) return result @router.get("/drugs/{drug_id}", response_model=DrugWithVariantsResponse) def get_drug(drug_id: int, db: Session = Depends(get_db)): """Get a specific drug with its variants""" drug = db.query(Drug).filter(Drug.id == drug_id).first() if not drug: raise HTTPException(status_code=404, detail="Drug not found") variants = db.query(DrugVariant).filter(DrugVariant.drug_id == drug.id).all() drug_dict = drug.__dict__.copy() drug_dict['variants'] = variants return drug_dict @router.post("/drugs", response_model=DrugWithVariantsResponse) def create_drug(drug: DrugCreate, db: Session = Depends(get_db)): """Create a new drug""" # Check if drug name already exists existing = db.query(Drug).filter(Drug.name == drug.name).first() if existing: raise HTTPException(status_code=400, detail="Drug with this name already exists") db_drug = Drug(name=drug.name, description=drug.description) db.add(db_drug) db.commit() db.refresh(db_drug) # Return drug with empty variants list drug_dict = db_drug.__dict__.copy() drug_dict['variants'] = [] return drug_dict @router.put("/drugs/{drug_id}", response_model=DrugWithVariantsResponse) def update_drug(drug_id: int, drug_update: DrugUpdate, db: Session = Depends(get_db)): """Update a drug""" drug = db.query(Drug).filter(Drug.id == drug_id).first() if not drug: raise HTTPException(status_code=404, detail="Drug not found") for field, value in drug_update.dict(exclude_unset=True).items(): setattr(drug, field, value) db.commit() db.refresh(drug) variants = db.query(DrugVariant).filter(DrugVariant.drug_id == drug.id).all() drug_dict = drug.__dict__.copy() drug_dict['variants'] = variants return drug_dict @router.delete("/drugs/{drug_id}") def delete_drug(drug_id: int, db: Session = Depends(get_db)): """Delete a drug and all its variants""" drug = db.query(Drug).filter(Drug.id == drug_id).first() if not drug: raise HTTPException(status_code=404, detail="Drug not found") # Delete all variants first db.query(DrugVariant).filter(DrugVariant.drug_id == drug_id).delete() # Delete the drug db.delete(drug) db.commit() return {"message": "Drug and all variants deleted successfully"} # Drug Variant endpoints @router.post("/drugs/{drug_id}/variants", response_model=DrugVariantResponse) def create_drug_variant(drug_id: int, variant: DrugVariantCreate, db: Session = Depends(get_db)): """Create a new variant for a drug""" # Check if drug exists drug = db.query(Drug).filter(Drug.id == drug_id).first() if not drug: raise HTTPException(status_code=404, detail="Drug not found") # Check if variant with same strength already exists for this drug existing = db.query(DrugVariant).filter( DrugVariant.drug_id == drug_id, DrugVariant.strength == variant.strength ).first() if existing: raise HTTPException(status_code=400, detail="Variant with this strength already exists for this drug") db_variant = DrugVariant( drug_id=drug_id, strength=variant.strength, quantity=variant.quantity, unit=variant.unit, low_stock_threshold=variant.low_stock_threshold ) db.add(db_variant) db.commit() db.refresh(db_variant) return db_variant @router.get("/variants/{variant_id}", response_model=DrugVariantResponse) def get_drug_variant(variant_id: int, db: Session = Depends(get_db)): """Get a specific drug variant""" variant = db.query(DrugVariant).filter(DrugVariant.id == variant_id).first() if not variant: raise HTTPException(status_code=404, detail="Drug variant not found") return variant @router.put("/variants/{variant_id}", response_model=DrugVariantResponse) def update_drug_variant(variant_id: int, variant_update: DrugVariantUpdate, db: Session = Depends(get_db)): """Update a drug variant""" variant = db.query(DrugVariant).filter(DrugVariant.id == variant_id).first() if not variant: raise HTTPException(status_code=404, detail="Drug variant not found") for field, value in variant_update.dict(exclude_unset=True).items(): setattr(variant, field, value) db.commit() db.refresh(variant) return variant @router.delete("/variants/{variant_id}") def delete_drug_variant(variant_id: int, db: Session = Depends(get_db)): """Delete a drug variant""" variant = db.query(DrugVariant).filter(DrugVariant.id == variant_id).first() if not variant: raise HTTPException(status_code=404, detail="Drug variant not found") db.delete(variant) db.commit() return {"message": "Drug variant deleted successfully"} # Dispensing endpoints @router.post("/dispense", response_model=DispensingResponse) def dispense_drug(dispensing: DispensingCreate, db: Session = Depends(get_db)): """Record a drug dispensing and reduce inventory""" # Check if drug variant exists variant = db.query(DrugVariant).filter(DrugVariant.id == dispensing.drug_variant_id).first() if not variant: raise HTTPException(status_code=404, detail="Drug variant not found") # Check if enough quantity available if variant.quantity < dispensing.quantity: raise HTTPException( status_code=400, detail=f"Insufficient quantity. Available: {variant.quantity}, Requested: {dispensing.quantity}" ) # Reduce variant quantity variant.quantity -= dispensing.quantity db.commit() # Create dispensing record db_dispensing = Dispensing(**dispensing.dict()) db.add(db_dispensing) db.commit() db.refresh(db_dispensing) return db_dispensing @router.get("/dispense/history", response_model=List[DispensingResponse]) def list_dispensings(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): """Get dispensing records (audit log)""" return db.query(Dispensing).order_by(Dispensing.dispensed_at.desc()).offset(skip).limit(limit).all() @router.get("/drugs/{drug_id}/dispense/history", response_model=List[DispensingResponse]) def get_drug_dispensings(drug_id: int, db: Session = Depends(get_db)): """Get dispensing history for a specific drug (all variants)""" # Verify drug exists drug = db.query(Drug).filter(Drug.id == drug_id).first() if not drug: raise HTTPException(status_code=404, detail="Drug not found") # Get all variant IDs for this drug variant_ids = db.query(DrugVariant.id).filter(DrugVariant.drug_id == drug_id).subquery() return db.query(Dispensing).filter(Dispensing.drug_variant_id.in_(variant_ids)).order_by(Dispensing.dispensed_at.desc()).all() @router.get("/variants/{variant_id}/dispense/history", response_model=List[DispensingResponse]) def get_variant_dispensings(variant_id: int, db: Session = Depends(get_db)): """Get dispensing history for a specific drug variant""" # Verify variant exists variant = db.query(DrugVariant).filter(DrugVariant.id == variant_id).first() if not variant: raise HTTPException(status_code=404, detail="Drug variant not found") return db.query(Dispensing).filter(Dispensing.drug_variant_id == variant_id).order_by(Dispensing.dispensed_at.desc()).all() # Include router with /api prefix app.include_router(router)