ifixkart-backend/Backend/app/services/ServiceJobService.py

213 lines
8.7 KiB
Python

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}