dialogflowAPI / app /services /trip_service.py
OnlyBiggg
refactor code
df37f6e
from typing import Literal
from fastapi.params import Depends
from typing_extensions import Annotated
from loguru import logger
from bisect import bisect_left
from fastapi import HTTPException
from app.client.futabus_client import FutabusClient
from app.repositories.trip_repository import TripRepository
from app.resolvers.location_resolver import LocationResolver
from app.dto.request import (
TicketRequest,
TripQueryRequest,
)
from app.dto.trip import TripDTO, TripOptionDTO
from app.services.helper import (
extra_time_dialogflow,
get_time_range,
get_weekday_name,
to_datetime_from_Dialogflow,
)
from app.dto.response import DialogServiceResult
class TripService:
"""Domain service xử lý logic tìm kiếm chuyến xe cho Dialogflow chatbot."""
def __init__(
self,
location_resolver: LocationResolver,
trip_repository: TripRepository,
futabus_client: FutabusClient,
):
self.location_resolver = location_resolver
self.futabus_client = futabus_client
self.trip_repository = trip_repository
@staticmethod
def find_trip_by_time_offices(
trips: list[dict],
departure_time: str,
origin_office_id: int,
dest_office_id: int,
) -> dict:
"""
Tìm chuyến xe phù hợp với thời gian khởi hành và ID văn phòng đón/trả.
Args:
trips (list[dict]): Danh sách chuyến xe từ API
departure_time (str): Thời gian khởi hành (định dạng raw từ API)
origin_office_id (int): ID văn phòng điểm đi
dest_office_id (int): ID văn phòng điểm đến
Returns:
dict: Thông tin chuyến xe phù hợp, hoặc {} nếu không tìm thấy.
"""
if not departure_time or not origin_office_id or not dest_office_id:
return {}
for trip in trips:
route = trip.get("route", {})
if (
trip.get("raw_departure_time") == departure_time
and route.get("origin_hub_office_id") == origin_office_id
and route.get("dest_hub_office_id") == dest_office_id
):
return trip
return {}
@staticmethod
def confirm_trip_search_input(data: TripQueryRequest):
"""
Xác nhận thông tin tìm kiếm chuyến xe từ người dùng.
Args:
data (TripQueryRequest): Thông tin tìm kiếm chuyến xe từ người dùng.
Returns:
DialogServiceResult: Kết quả xác nhận bao gồm thông tin chuyến xe đã tìm kiếm.
"""
if not data:
logger.error("Invalid trip query data")
return DialogServiceResult(
text=["Thông tin tìm kiếm chuyến xe không hợp lệ."]
)
time = extra_time_dialogflow(data.time_select)
date = to_datetime_from_Dialogflow(data.date)
date, week_day = get_weekday_name(date)
text = f"**Điểm đi:** {data.origin_office or data.departure_city}\n"
text += f"**Điểm đến:** {data.dest_office or data.destination_city}\n"
text += f"**Thời gian:** {time} {date} {week_day}\n"
text += f"**Số vé:** {int(data.ticket_number) or 1}\n"
options = [
"Tìm chuyến xe",
"Không, cảm ơn",
]
return DialogServiceResult(text=[text], options=options)
@staticmethod
def find_surrounding_trips(
trips: list[dict], target_time: str, limit: int = 4
) -> list[dict]:
"""
Trả về danh sách chuyến xe gần nhất xung quanh thời gian chỉ định.
Args:
trips (list[dict]): Danh sách chuyến xe (đã được sort theo thời gian)
target_time (str): Thời gian muốn so sánh (định dạng giống raw_departure_time)
limit (int): Số lượng chuyến cần lấy xung quanh (mặc định: 4)
Returns:
list[dict]: Các chuyến gần nhất xung quanh thời gian
"""
if not target_time or not trips or limit <= 0:
return []
time_list = [trip.get("raw_departure_time", "") for trip in trips]
index = bisect_left(time_list, target_time)
half = limit // 2
start = max(0, index - half)
end = min(len(trips), start + limit)
if end - start < limit:
start = max(0, end - limit)
return trips[start:end]
async def get_all_trip_data(self, data: TripQueryRequest) -> list:
"""
Lấy tất cả dữ liệu chuyến xe dựa trên thông tin truy vấn từ người dùng.
Args:
data (TripQueryRequest): Thông tin truy vấn chuyến đi bao gồm văn phòng, thành phố, ngày, số vé, thời gian dự kiến.
Returns:
DialogServiceResult: Kết quả chứa danh sách chuyến xe hoặc thông báo lỗi.
"""
ticket_count = data.ticket_number or 1
# Lấy mã và ID các điểm đi/đến
origin_id, origin_code, origin_ids = await self.location_resolver.resolve_info(
office=data.origin_office, city=data.departure_city, role="origin"
)
dest_id, dest_code, dest_ids = await self.location_resolver.resolve_info(
office=data.dest_office, city=data.destination_city, role="dest"
)
route_ids = await self.trip_repository.get_all_route_ids(
origin_code=origin_code,
from_id=origin_id,
origin_ids=origin_ids,
dest_code=dest_code,
to_id=dest_id,
dest_ids=dest_ids,
)
date = to_datetime_from_Dialogflow(data.date)
from_time, to_time = get_time_range(date)
trips = await self.trip_repository.get_trip_list(
from_time, to_time, route_ids, ticket_count
)
return trips
async def get_trips_option(self, data: TripQueryRequest) -> DialogServiceResult:
"""
Xử lý truy vấn tìm kiếm chuyến xe theo thông tin người dùng từ Dialogflow.
Args:
data (TripRequest): Thông tin truy vấn chuyến đi bao gồm văn phòng, thành phố, ngày, số vé, thời gian dự kiến.
Returns:
DialogflowResult: Dữ liệu trả về cho chatbot Dialogflow bao gồm gợi ý chuyến xe hoặc thông báo lỗi.
"""
time = extra_time_dialogflow(data.time_select)
# ❗ Nếu thời gian không rõ ràng hoặc chưa được xác định
if isinstance(time, list) or time is None:
return DialogServiceResult(parameters={"is_time_ambiguous": True})
ticket_count = data.ticket_number or 1
# Lấy mã và ID các điểm đi/đến
origin_id, origin_code, origin_ids = await self.location_resolver.resolve_info(
office=data.origin_office, city=data.departure_city, role="origin"
)
dest_id, dest_code, dest_ids = await self.location_resolver.resolve_info(
office=data.dest_office, city=data.destination_city, role="dest"
)
# route_ids = await self.trip_repository.get_all_route_ids(
# origin_code=origin_code,
# from_id=origin_id,
# origin_ids=origin_ids,
# dest_code=dest_code,
# to_id=dest_id,
# dest_ids=dest_ids,
# )
# date = to_datetime_from_Dialogflow(
# data.date
# ) # Chuyển đổi ngày từ Dialogflow về datetime
# # Tính khoảng thời gian tìm kiếm
# from_time, to_time = get_time_range(date)
trips = await self.get_all_trip_data(data=data)
if not trips:
text = (
f"Hệ thống không tìm thấy tuyến xe **{data.departure_city}** - "
f"**{data.destination_city}**.\nQuý khách vui lòng thử lại hoặc gọi 1900 6067 để được hỗ trợ."
)
options = ["Xem tuyến xe khác", "Không, cảm ơn"]
return DialogServiceResult(
text=[text],
payload=options,
)
# Nếu có chuyến đúng văn phòng đúng giờ
if data.origin_office and data.dest_office:
matched = self.find_trip_by_time_offices(trips, origin_ids, dest_ids)
if matched:
parameters = {"is_valid_trip": True, "trip_data": matched}
return DialogServiceResult(parameters=parameters)
surrounding_trip = self.find_surrounding_trips(
trips=trips, target_time=time, limit=4
)
trip_options: list[TripOptionDTO] = [
TripOptionDTO(
trip_id=trip["id"],
route=f' {trip["id"]}| {trip["raw_departure_time"]} | {trip["route"]["origin_hub_name"]} => {trip["route"]["dest_hub_name"]}',
)
for trip in surrounding_trip
if trip["empty_seat_quantity"] >= ticket_count
]
prompt = "Quý khách vui lòng lựa chọn chuyến xe\n" + "\n".join(
f"{i+1}. {t.route}" for i, t in enumerate(trip_options)
)
parameters = {"trip_options": trip_options}
return DialogServiceResult(
text=[prompt], payload=trip_options, parameters=parameters
)
@staticmethod
def find_trip_by_route_name(
route_name: str, options: list[TripOptionDTO]
) -> TripOptionDTO | None:
"""
Tìm trip tương ứng với route name (case-insensitive).
"""
route_name = route_name.strip().lower()
return next(
(opt for opt in options if opt.route.strip().lower() == route_name), None
)
async def validate_trip_selection(
self,
id_selected: int,
data: TripQueryRequest,
) -> DialogServiceResult:
"""
Xác thực lựa chọn chuyến xe từ người dùng.
Args:
data (TripValidateRequest): Thông tin lựa chọn chuyến xe từ người dùng.
Returns:
DialogServiceResult: Kết quả xác thực bao gồm thông tin chuyến xe đã chọn.
"""
if not id_selected:
logger.error("Invalid trip selection data")
# raise HTTPException(status_code=400, detail="Invalid trip selection data")
# text = "Không có chuyến xe nào được chọn"
parameters = {"is_valid_trip": False}
return DialogServiceResult(parameters=parameters)
matched_trip = await self.find_trip_by_id(trip_id=id_selected, data=data)
if not matched_trip:
parameters = {"is_valid_trip": False}
logger.error("Not found match trip")
return DialogServiceResult(parameters=parameters)
return DialogServiceResult(
parameters={"is_valid_trip": True, "trip_data": matched_trip}
)
async def find_trip_by_id(self, trip_id: str, data: TripQueryRequest):
if not trip_id:
logger.error("Invalid trip selected ID for fetching trip data")
return None
trips = await self.get_all_trip_data(data)
if not trips:
logger.error(f"No trips found for trip ID {trip_id}")
return None
return next((trip for trip in trips if trip.get("id") == trip_id), None)
async def get_available_seat(self, trip: TripDTO) -> DialogServiceResult:
"""
Lấy danh sách ghế trống của chuyến xe.
Args:
trip (TripSelectRequest): Thông tin chuyến xe cần lấy ghế.
Returns:
DialogServiceResult: Danh sách ghế trống của chuyến xe.
"""
if not trip:
logger.error("Invalid trip data for fetching seats")
raise HTTPException(
status_code=400, detail="Invalid trip data for fetching seats"
)
seats = await self.futabus_client.get_seat_trip(trip)
if not seats:
logger.error(f"No seats found for trip {trip.id}")
text = "Không có ghế trống cho chuyến xe này"
return DialogServiceResult(text=[text])
logger.debug(f"Found {len(seats)} seats for trip {trip.id}")
available_seats = [s for s in seats if s.get("bookStatus") == 0]
if not available_seats:
logger.error(f"No available seats for trip {trip.id}")
text = "Không còn ghế trống cho chuyến xe này"
return DialogServiceResult(text=[text])
available_seats.sort(key=lambda x: x.get("chair", ""))
text = f"Chuyến xe {trip.route.name}{len(available_seats)} ghế trống"
opts = available_seats
return DialogServiceResult(text=[text], options=opts)
async def validate_seat_selection(
self, selected: str, data: TripDTO
) -> DialogServiceResult:
"""
Xác thực lựa chọn ghế từ người dùng.
Args:
selected (str): Ghế được người dùng chọn.
data (TripDTO): Thông tin chuyến xe.
Returns:
DialogServiceResult: Kết quả xác thực bao gồm thông tin ghế đã chọn.
"""
if not selected or not data or not data.id:
logger.error("Invalid seat selection data")
return DialogServiceResult(parameters={"is_valid_seat": False})
seats = await self.futabus_client.get_seat_trip(data)
available_seats = [s for s in seats if s.get("bookStatus") == 0]
# Kiểm tra xem ghế có trong danh sách ghế của chuyến xe không
if selected.lower() in [s.get("chair").lower() for s in available_seats]:
text = f"Bạn đã chọn ghế {selected.upper()} cho chuyến xe."
params = {"seat": selected.upper(), "is_valid_seat": True}
return DialogServiceResult(
text=[text],
parameters=params,
)
logger.debug(f"Seat {selected} is not available for trip {data.id}")
text = (
f"Ghế {selected.upper()} không có sẵn cho chuyến xe. "
"Vui lòng chọn ghế khác."
)
params = {"is_valid_seat": False}
return DialogServiceResult(text=[text], parameters=params)
async def get_pick_up_points(
self, route_id: int, way_id: int
) -> DialogServiceResult:
"""
Lấy danh sách các điểm dừng của chuyến xe theo route_id và way_id.
Args:
route_id (int): ID của tuyến xe.
way_id (int): ID của lộ trình.
Returns:
list[dict]: Danh sách các điểm dừng.
"""
pickups = await self.trip_repository.get_stop(route_id, way_id, role="pick-up")
logger.info(
f"Retrieved {len(pickups)} pick-up stops for route {route_id} and way {way_id} : {pickups}"
)
if not pickups:
logger.error(f"No stops found for route {route_id} and way {way_id}")
raise HTTPException(
status_code=404,
detail=f"No pick-up stops found for route {route_id} and way {way_id}",
)
text = "Quý khách vui lòng chọn điểm đón cho chuyến xe của mình"
text += "\n" + " | ".join(f"{s['id']}" for i, s in enumerate(pickups))
params = {"pickup_points": pickups}
return DialogServiceResult(text=[text], options=pickups, parameters=params)
async def validate_stop_selection(
self, id_selected: int, data: TripDTO, role: Literal["pick-up", "drop-off"]
) -> DialogServiceResult:
"""
Xác thực lựa chọn điểm đón của người dùng.
Args:
selected (str): Điểm đón được người dùng chọn.
data (TripDTO): Thông tin chuyến xe.
Returns:
DialogServiceResult: Kết quả xác thực bao gồm thông tin điểm đón đã chọn.
"""
logger.info(f"ID: {id_selected}")
logger.info(f"Role: {role}")
if role == "pick-up":
if not id_selected:
logger.error("Invalid pick-up selection data")
return DialogServiceResult(parameters={"is_valid_pickup": False})
pickups = await self.trip_repository.get_stop(
data.route_id, data.way_id, role="pick-up"
)
matched_pickup = next(
(s for s in pickups if s.get("id") == id_selected), None
)
if matched_pickup:
pickup_name = matched_pickup.get("name")
text = f"Bạn đã chọn điểm đón **{pickup_name}** cho chuyến xe."
params = {"pickup": pickup_name, "is_valid_pickup": True}
return DialogServiceResult(
text=[text],
parameters=params,
)
logger.info(
f"Pick-up point {id_selected} is not available for trip {data.id}"
)
text = (
f"Điểm đón không có sẵn cho chuyến xe. " "Vui lòng chọn điểm đón khác."
)
params = {"is_valid_pickup": False}
return DialogServiceResult(text=[text], parameters=params)
if role == "drop-off":
if not id_selected:
logger.error("Invalid drop-off selection data")
return DialogServiceResult(parameters={"is_valid_dropoff": False})
drop_offs = await self.trip_repository.get_stop(
data.route_id, data.way_id, role="drop-off"
)
matched_drop_off = next(
(s for s in drop_offs if s.get("id") == id_selected), None
)
# Kiểm tra xem điểm đón có trong danh sách điểm dừng của chuyến xe không
if matched_drop_off:
drop_off_name = matched_drop_off.get("name")
text = f"Bạn đã chọn điểm trả **{drop_off_name}** cho chuyến xe."
params = {"dropoff": drop_off_name, "is_valid_dropoff": True}
return DialogServiceResult(
text=[text],
parameters=params,
)
logger.info(
f"Drop-off point {id_selected} is not available for trip {data.id}"
)
text = (
f"Điểm trả không có sẵn cho chuyến xe. " "Vui lòng chọn điểm trả khác."
)
params = {"is_valid_dropoff": False}
return DialogServiceResult(text=[text], parameters=params)
else:
logger.error(f"Invalid role for stop selection: {role}")
raise HTTPException(
status_code=400, detail="Invalid role for stop selection"
)
async def get_drop_off_points(self, route_id: int, way_id: int) -> list:
"""
Lấy danh sách các điểm dừng trả khách của chuyến xe theo route_id và way_id.
Args:
route_id (int): ID của tuyến xe.
way_id (int): ID của lộ trình.
Returns:
list[dict]: Danh sách các điểm dừng trả khách.
"""
drop_offs = await self.trip_repository.get_stop(
route_id, way_id, role="drop-off"
)
if not drop_offs:
logger.error(
f"No drop-off stops found for route {route_id} and way {way_id}"
)
text = "Quý khách vui lòng chọn điểm trả cho chuyến xe của mình"
text += "\n" + " | ".join(f"{s['id']}" for i, s in enumerate(drop_offs))
return DialogServiceResult(text=[text], options=drop_offs)
@staticmethod
async def make_ticket_summary(ticket: TicketRequest) -> DialogServiceResult:
"""
Tạo tóm tắt vé xe dựa trên thông tin người dùng và chuyến xe.
Args:
ticket (TicketRequest): Thông tin vé xe bao gồm người dùng, chuyến xe, ghế, điểm đón/trả.
Returns:
DialogServiceResult: Kết quả tóm tắt vé xe.
"""
if not ticket or not ticket.trip_data:
logger.error("Invalid ticket data for summary")
return DialogServiceResult(text=["Thông tin vé không hợp lệ."])
text = f" \
**Thông tin hành khách**\n\
**Họ và tên** {ticket.user_name} \n\
**Số điện thoại** {ticket.phone_number}\n\
**Email** {ticket.email} \n\
**Thông tin lượt đi**\n\
**Tuyến xe** {ticket.trip_data.route.name} \n\
**Thời gian xuất bến** {ticket.trip_data.raw_departure_time} {ticket.trip_data.raw_departure_date} \n\
**Số ghế** {ticket.seat.upper()} \n\
**Điểm lên xe** {ticket.pickup} \n\
**Điểm trả khách** {ticket.dropoff} \n\
**Tổng tiền lượt đi** {ticket.trip_data.price} VND \
"
options = [
"Đặt vé",
"Không, cảm ơn",
]
return DialogServiceResult(text=[text], options=options)