M36: deepen operational and mobile UX
This commit is contained in:
@@ -26,6 +26,7 @@ from app.schemas import (
|
||||
NextBookingRisk,
|
||||
RegisterReturnRequest,
|
||||
RegisterReturnResult,
|
||||
RescheduleBookingRequest,
|
||||
ReturnPreviewResult,
|
||||
)
|
||||
from app.services.audit import record_audit_event
|
||||
@@ -112,8 +113,16 @@ def list_bookings(
|
||||
bookings = db.scalars(
|
||||
stmt if page is None else stmt.offset((page_number - 1) * page_size).limit(page_size)
|
||||
).all()
|
||||
customers = {c.id: c for c in db.scalars(select(Customer)).all()}
|
||||
vehicles = {v.id: v for v in db.scalars(select(Vehicle)).all()}
|
||||
customer_ids = {booking.customer_id for booking in bookings}
|
||||
vehicle_ids = {booking.vehicle_id for booking in bookings}
|
||||
customers = {
|
||||
customer.id: customer
|
||||
for customer in db.scalars(select(Customer).where(Customer.id.in_(customer_ids))).all()
|
||||
}
|
||||
vehicles = {
|
||||
vehicle.id: vehicle
|
||||
for vehicle in db.scalars(select(Vehicle).where(Vehicle.id.in_(vehicle_ids))).all()
|
||||
}
|
||||
items = [_to_out(b, customers[b.customer_id], vehicles[b.vehicle_id]) for b in bookings]
|
||||
if page is None:
|
||||
return items
|
||||
@@ -379,6 +388,53 @@ def complete_booking_requirements(
|
||||
return _to_out(booking, customer, vehicle)
|
||||
|
||||
|
||||
@router.patch("/{public_ref}/schedule", response_model=BookingOut)
|
||||
def reschedule_booking(
|
||||
public_ref: str,
|
||||
body: RescheduleBookingRequest,
|
||||
db: Session = Depends(get_db),
|
||||
user: CurrentUser = Depends(get_current_user),
|
||||
) -> BookingOut:
|
||||
if body.ends_at <= body.starts_at:
|
||||
raise HTTPException(status_code=422, detail="Booking end must be after its start")
|
||||
booking = db.scalar(select(Booking).where(Booking.public_ref == public_ref).with_for_update())
|
||||
if booking is None:
|
||||
raise HTTPException(status_code=404, detail="Booking not found")
|
||||
if booking.status != "reserved":
|
||||
raise HTTPException(status_code=409, detail="Only a reserved booking can be rescheduled")
|
||||
vehicle = db.scalar(select(Vehicle).where(Vehicle.id == booking.vehicle_id).with_for_update())
|
||||
customer = db.get(Customer, booking.customer_id)
|
||||
if customer is None or vehicle is None:
|
||||
raise HTTPException(status_code=500, detail="Booking references a missing record")
|
||||
overlap = db.scalar(
|
||||
select(Booking.id).where(
|
||||
Booking.vehicle_id == booking.vehicle_id,
|
||||
Booking.id != booking.id,
|
||||
Booking.status.in_(("reserved", "active")),
|
||||
Booking.starts_at < body.ends_at,
|
||||
Booking.ends_at > body.starts_at,
|
||||
)
|
||||
)
|
||||
if overlap is not None:
|
||||
raise HTTPException(status_code=409, detail="Vehicle already has an overlapping booking")
|
||||
before = {"starts_at": booking.starts_at.isoformat(), "ends_at": booking.ends_at.isoformat()}
|
||||
booking.starts_at = body.starts_at
|
||||
booking.ends_at = body.ends_at
|
||||
record_audit_event(
|
||||
db,
|
||||
actor_type="user",
|
||||
actor_label=user.display_name,
|
||||
action="booking_rescheduled",
|
||||
entity_type="booking",
|
||||
entity_id=booking.id,
|
||||
before=before,
|
||||
after={"starts_at": booking.starts_at.isoformat(), "ends_at": booking.ends_at.isoformat()},
|
||||
metadata={"reason": body.reason.strip()},
|
||||
)
|
||||
db.commit()
|
||||
return _to_out(booking, customer, vehicle)
|
||||
|
||||
|
||||
@router.post("/{public_ref}/cancel", response_model=BookingOut)
|
||||
def cancel_booking(
|
||||
public_ref: str,
|
||||
|
||||
Reference in New Issue
Block a user