import ulid from datetime import datetime, timedelta from sqlalchemy.orm import Session from sqlalchemy import select from typing import Optional, List from fastapi import HTTPException, status from app.models.ServiceModel import ServiceCatalog, ServiceJob, ServiceAppointment, ServiceJobEvent, ServiceJobAssignment from app.models.CustomerDeviceModel import CustomerDevice from app.schemas.ServiceSchema import ServiceJobCreate, ServiceJobRescheduleRequest from app.services.SlotAllocationService import SlotAllocationService class ServiceJobService: def __init__(self): self.allocation_service = SlotAllocationService() def create_online_booking(self, db: Session, customer_id: str, data: ServiceJobCreate) -> dict: # Register device if new device_id = data.device_id if data.new_device: device_id = str(ulid.ULID()) device = CustomerDevice( device_id=device_id, customer_id=customer_id, brand=data.new_device.brand, model=data.new_device.model, model_number=data.new_device.model_number, imei_primary=data.new_device.imei_primary, imei_secondary=data.new_device.imei_secondary, color=data.new_device.color, device_condition=data.new_device.device_condition, device_type=data.new_device.device_type, notes=data.new_device.notes, storage_capacity=data.new_device.storage_capacity ) db.add(device) db.flush() elif not device_id: raise HTTPException(status_code=400, detail="Device details required") # 2. Granular Cashify-style Validation & Snapshots service_name_snapshot = None variant_name_snapshot = None base_price_snapshot = None duration_snapshot = 60 warranty_snapshot = None inspection_fee_snapshot = 300.00 queue_number = None queue_date = None if data.repair_variant_id: from app.models.DeviceCatalogModel import RepairVariant, DeviceModel variant = db.execute(select(RepairVariant).where(RepairVariant.variant_id == data.repair_variant_id)).scalar_one_or_none() if not variant: raise HTTPException(status_code=404, detail="Selected repair variant not found") # Cross-database model validation if data.model_id: model = db.execute(select(DeviceModel).where(DeviceModel.model_id == data.model_id)).scalar_one_or_none() if not model: raise HTTPException(status_code=404, detail="Selected device model not found") if variant.repair_service.model_id != data.model_id: raise HTTPException(status_code=400, detail="Selected repair variant does not belong to the chosen device model") service_name_snapshot = variant.repair_service.service_type.name variant_name_snapshot = variant.name base_price_snapshot = float(variant.price) duration_snapshot = variant.duration_minutes warranty_snapshot = variant.warranty_days elif data.service_id: service = db.get(ServiceCatalog, data.service_id) if not service: raise HTTPException(status_code=404, detail="Requested catalog service not found") service_name_snapshot = service.name base_price_snapshot = float(service.base_price) duration_snapshot = service.estimated_duration_minutes # Restrict customer priority defaults to NORMAL priority = "NORMAL" if data.priority and data.priority in ["NORMAL", "HIGH", "URGENT"]: priority = data.priority # Handle Walk-in Queueing if data.source == "WALK_IN": today = datetime.utcnow().date() queue_date = today # Fetch daily count count_stmt = select(func.count(ServiceJob.job_id)).where(ServiceJob.queue_date == today) daily_count = db.execute(count_stmt).scalar() or 0 queue_number = f"W-{today.year}-{daily_count + 1:03d}" # Generate unique Job number timestamp = int(datetime.utcnow().timestamp()) job_no = f"SRV-{timestamp}" # Create ServiceJob in BOOKING_PENDING status job = ServiceJob( job_id=str(ulid.ULID()), job_no=job_no, customer_id=customer_id, device_id=device_id, source=data.source, service_id=data.service_id, status="BOOKING_PENDING", custom_service_name=data.custom_service_name, device_type=data.device_type, brand_id=data.brand_id, series_id=data.series_id, model_id=data.model_id, service_type_id=data.service_type_id, repair_service_id=data.repair_service_id, repair_variant_id=data.repair_variant_id, currency="INR", service_name_snapshot=service_name_snapshot, variant_name_snapshot=variant_name_snapshot, base_price_snapshot=base_price_snapshot, duration_snapshot=duration_snapshot, warranty_snapshot=warranty_snapshot, inspection_fee_snapshot=inspection_fee_snapshot, queue_number=queue_number, queue_date=queue_date, priority=priority ) db.add(job) db.flush() # Handle Slot Hold appointment = None if data.appointment: # Create Appointment Hold cleanly for customer requested slot (10 min hold window) appointment = ServiceAppointment( appointment_id=str(ulid.ULID()), service_job_id=job.job_id, scheduled_start=data.appointment.scheduled_start, scheduled_end=data.appointment.scheduled_end, status="HELD", hold_expires_at=datetime.utcnow() + timedelta(minutes=10) ) db.add(appointment) # Log timeline event event = ServiceJobEvent( event_id=str(ulid.ULID()), job_id=job.job_id, event_type="BOOKING_INITIATED", performed_by=customer_id, notes="Customer initiated service booking online." ) db.add(event) db.commit() db.refresh(job) return { "job_id": job.job_id, "job_no": job.job_no, "appointment_id": appointment.appointment_id if appointment else None, "hold_expires_at": appointment.hold_expires_at.isoformat() if appointment else None } def reschedule_appointment( self, db: Session, job_id: str, requester_id: str, data: ServiceJobRescheduleRequest ) -> dict: job = db.get(ServiceJob, job_id) if not job: raise HTTPException(status_code=404, detail="Service job not found") # 1. Fetch current active appointment app_stmt = select(ServiceAppointment).where( ServiceAppointment.service_job_id == job_id, ServiceAppointment.status == "CONFIRMED" ) old_appt = db.execute(app_stmt).scalar_one_or_none() # 2. Check if new slot time is available dynamically slots = self.allocation_service.get_available_slots(db, data.scheduled_start.date(), job.service_id) matching_slot = None for slot in slots: if slot["start_time"] == data.scheduled_start.isoformat(): matching_slot = slot break if not matching_slot: raise HTTPException(status_code=400, detail="New requested appointment slot is not available") # 3. Create new CONFIRMED appointment new_appt = ServiceAppointment( appointment_id=str(ulid.ULID()), service_job_id=job_id, scheduled_start=data.scheduled_start, scheduled_end=data.scheduled_end, status="CONFIRMED", confirmed_at=datetime.utcnow() ) db.add(new_appt) # 4. Cancel old appointment if old_appt: old_appt.status = "CANCELLED" old_appt.cancelled_at = datetime.utcnow() # 5. Log Timeline Event event = ServiceJobEvent( event_id=str(ulid.ULID()), job_id=job_id, event_type="RESCHEDULED", performed_by=requester_id, notes=f"Appointment rescheduled from {old_appt.scheduled_start if old_appt else 'N/A'} to {data.scheduled_start}. Reason: {data.reason or 'Not specified'}" ) db.add(event) db.commit() return {"message": "Rescheduling complete", "new_appointment_id": new_appt.appointment_id}