213 lines
8.7 KiB
Python
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}
|