from fastapi import APIRouter, HTTPException, Depends from pydantic import ValidationError from sqlalchemy.orm import Session, joinedload from typing import List, Optional from ..models import ( Dewar as DewarModel, Slot as SlotModel, LogisticsEvent as LogisticsEventModel, ) from ..schemas import LogisticsEventCreate, SlotSchema, Dewar as DewarSchema from ..database import get_db import logging from datetime import datetime, timedelta router = APIRouter() logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) def calculate_time_until_refill( last_refill: Optional[datetime], refill_interval_hours: int = 48 ) -> int: refill_interval = timedelta(hours=refill_interval_hours) now = datetime.now() if not last_refill: return -1 # Sentinel value indicating no refill event recorded time_until_next_refill = last_refill + refill_interval - now return max(0, int(time_until_next_refill.total_seconds())) @router.post("/dewars/return", response_model=DewarSchema) async def return_to_storage(data: LogisticsEventCreate, db: Session = Depends(get_db)): logger.info( f"Returning dewar to storage: {data.dewar_qr_code}" f"at location {data.location_qr_code}" ) try: # Log the incoming payload logger.info("Received payload: %s", data.json()) dewar = ( db.query(DewarModel) .filter(DewarModel.unique_id == data.dewar_qr_code) .first() ) if not dewar: logger.error(f"Dewar not found for unique ID: {data.dewar_qr_code}") raise HTTPException(status_code=404, detail="Dewar not found") original_slot = ( db.query(SlotModel) .filter(SlotModel.dewar_unique_id == data.dewar_qr_code) .first() ) if original_slot and original_slot.qr_code != data.location_qr_code: logger.error( f"Dewar {data.dewar_qr_code} is" f"associated with slot {original_slot.qr_code}" ) raise HTTPException( status_code=400, detail=f"Dewar {data.dewar_qr_code} is associated" f"with a different slot {original_slot.qr_code}.", ) slot = ( db.query(SlotModel) .filter(SlotModel.qr_code == data.location_qr_code) .first() ) if not slot: logger.error(f"Slot not found for QR code: {data.location_qr_code}") raise HTTPException(status_code=404, detail="Slot not found") if slot.occupied and slot.dewar_unique_id != data.dewar_qr_code: logger.error( f"Slot {data.location_qr_code} is already occupied by another dewar" ) raise HTTPException( status_code=400, detail="Selected slot is already occupied by another dewar", ) # Update slot with dewar information slot.dewar_unique_id = dewar.unique_id slot.occupied = True dewar.last_retrieved_timestamp = None # Set the `at_beamline` attribute to False dewar.at_beamline = False # Log the event log_event(db, dewar.id, slot.id, "returned") db.commit() logger.info( f"Dewar {data.dewar_qr_code} successfully" f"returned to storage slot {slot.qr_code}." ) db.refresh(dewar) return dewar except ValidationError as e: logger.error(f"Validation error: {e.json()}") raise HTTPException(status_code=400, detail="Invalid payload") except Exception as e: logger.error(f"Unexpected error: {str(e)}") raise HTTPException(status_code=500, detail="Internal server error") @router.post("/dewar/scan", response_model=dict) async def scan_dewar(event_data: LogisticsEventCreate, db: Session = Depends(get_db)): logger.info(f"Received event data: {event_data}") dewar_qr_code = event_data.dewar_qr_code location_qr_code = event_data.location_qr_code transaction_type = event_data.transaction_type # Validate Dewar QR Code if not dewar_qr_code or not dewar_qr_code.strip(): logger.error("Dewar QR Code is null or empty") raise HTTPException( status_code=422, detail="Dewar QR Code cannot be null or empty" ) # Retrieve the Dewar dewar = db.query(DewarModel).filter(DewarModel.unique_id == dewar_qr_code).first() if not dewar: logger.error("Dewar not found") raise HTTPException(status_code=404, detail="Dewar not found") # Check for Outgoing QR Codes and set transaction type if location_qr_code in ["Outgoing X10-SA", "Outgoing X06-SA"]: transaction_type = "outgoing" # Retrieve the Slot associated with the Dewar (for outgoing) slot = None if transaction_type == "outgoing": slot = ( db.query(SlotModel) .filter(SlotModel.dewar_unique_id == dewar.unique_id) .first() ) if not slot: logger.error(f"No slot associated with dewar for outgoing: {dewar_qr_code}") raise HTTPException( status_code=404, detail="No slot associated with dewar for outgoing" ) # Incoming Logic if transaction_type == "incoming": slot = db.query(SlotModel).filter(SlotModel.qr_code == location_qr_code).first() if not slot or slot.occupied: logger.error(f"Slot not found or already occupied: {location_qr_code}") raise HTTPException( status_code=400, detail="Slot not found or already occupied" ) slot.dewar_unique_id = dewar.unique_id slot.occupied = True elif transaction_type == "outgoing": if not slot.occupied or slot.dewar_unique_id != dewar.unique_id: logger.error(f"Slot not valid for outgoing: {location_qr_code}") raise HTTPException( status_code=400, detail="Dewar not associated with the slot for outgoing", ) slot.dewar_unique_id = None slot.occupied = False elif transaction_type == "beamline": slot = db.query(SlotModel).filter(SlotModel.qr_code == location_qr_code).first() if not slot: logger.error(f"Beamline location not found: {location_qr_code}") raise HTTPException(status_code=400, detail="Beamline location not found") dewar.beamline_location = location_qr_code logger.info(f"Dewar {dewar_qr_code} assigned to beamline {location_qr_code}") # Log the event log_event(db, dewar.id, slot.id if slot else None, transaction_type) db.commit() logger.info( f"Transaction completed: {transaction_type}" f"for dewar {dewar_qr_code} in slot {slot.qr_code if slot else 'N/A'}" ) return {"message": "Status updated successfully"} @router.get("/slots", response_model=List[SlotSchema]) async def get_all_slots(db: Session = Depends(get_db)): slots = db.query(SlotModel).options(joinedload(SlotModel.dewar)).all() slots_with_refill_time = [] for slot in slots: # Initialize variables for slot-related data time_until_refill = None retrievedTimestamp = None beamlineLocation = None at_beamline = False if slot.dewar_unique_id: # Calculate time until refill last_refill_event = ( db.query(LogisticsEventModel) .join(DewarModel, DewarModel.id == LogisticsEventModel.dewar_id) .filter( DewarModel.unique_id == slot.dewar.unique_id, LogisticsEventModel.event_type == "refill", ) .order_by(LogisticsEventModel.timestamp.desc()) .first() ) if last_refill_event: last_refill = last_refill_event.timestamp time_until_refill = calculate_time_until_refill(last_refill) else: time_until_refill = -1 # Fetch the latest event for the dewar last_event = ( db.query(LogisticsEventModel) .join(DewarModel, DewarModel.id == LogisticsEventModel.dewar_id) .filter(DewarModel.unique_id == slot.dewar.unique_id) .order_by(LogisticsEventModel.timestamp.desc()) .first() ) # Determine if the dewar is at the beamline if last_event: if last_event.event_type == "beamline": at_beamline = True # Optionally set retrievedTimestamp and beamlineLocation for # beamline events retrievedTimestamp = last_event.timestamp.isoformat() associated_slot = ( db.query(SlotModel) .filter(SlotModel.id == last_event.slot_id) .first() ) beamlineLocation = ( associated_slot.label if associated_slot else None ) elif last_event.event_type == "returned": at_beamline = False # Correct the contact_person assignment contact_person = None if slot.dewar and slot.dewar.contact_person: first_name = slot.dewar.contact_person.firstname last_name = slot.dewar.contact_person.lastname contact_person = f"{first_name} {last_name}" # Prepare the slot data for the response slot_data = SlotSchema( id=slot.id, qr_code=slot.qr_code, label=slot.label, qr_base=slot.qr_base, occupied=slot.occupied, needs_refill=slot.needs_refill, dewar_unique_id=slot.dewar_unique_id, dewar_name=slot.dewar.dewar_name if slot.dewar else None, time_until_refill=time_until_refill, at_beamline=at_beamline, retrievedTimestamp=retrievedTimestamp, beamlineLocation=beamlineLocation, shipment_name=( slot.dewar.shipment.shipment_name if slot.dewar and slot.dewar.shipment else None ), contact_person=contact_person, local_contact="local contact placeholder", ) # Add updated slot data to the response list slots_with_refill_time.append(slot_data) return slots_with_refill_time @router.post("/dewar/refill", response_model=dict) async def refill_dewar(qr_code: str, db: Session = Depends(get_db)): logger.info(f"Refilling dewar with QR code: {qr_code}") dewar = db.query(DewarModel).filter(DewarModel.unique_id == qr_code.strip()).first() if not dewar: logger.error("Dewar not found") raise HTTPException(status_code=404, detail="Dewar not found") now = datetime.now() new_event = LogisticsEventModel( dewar_id=dewar.id, slot_id=None, event_type="refill", timestamp=now, ) db.add(new_event) db.commit() time_until_refill_seconds = calculate_time_until_refill(now) logger.info( f"Dewar refilled successfully" f"with time_until_refill: {time_until_refill_seconds}" ) return { "message": "Dewar refilled successfully", "time_until_refill": time_until_refill_seconds, } @router.get("/dewars", response_model=List[DewarSchema]) async def get_all_dewars(db: Session = Depends(get_db)): dewars = db.query(DewarModel).all() return dewars @router.get("/dewar/{unique_id}", response_model=DewarSchema) async def get_dewar_by_unique_id(unique_id: str, db: Session = Depends(get_db)): logger.info(f"Received request for dewar with unique_id: {unique_id}") dewar = ( db.query(DewarModel).filter(DewarModel.unique_id == unique_id.strip()).first() ) if not dewar: logger.warning(f"Dewar with unique_id '{unique_id}' not found.") raise HTTPException(status_code=404, detail="Dewar not found") logger.info(f"Returning dewar: {dewar}") return dewar def log_event(db: Session, dewar_id: int, slot_id: Optional[int], event_type: str): new_event = LogisticsEventModel( dewar_id=dewar_id, slot_id=slot_id, event_type=event_type, timestamp=datetime.now(), ) db.add(new_event) db.commit() logger.info( f"Logged event: {event_type} for dewar: {dewar_id} " f"in slot: {slot_id if slot_id else 'N/A'}" )