fix(bookings): add UTC conversion to admin and reschedule endpoints
admin_create_booking, admin_update_booking, and reschedule_booking stored naive datetimes without converting to UTC, causing +2h offset on calendar after the frontend started treating all DB times as UTC. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1066,14 +1066,16 @@ def admin_update_booking(
|
||||
)
|
||||
|
||||
# Update fields (only if provided)
|
||||
# Convert datetimes from admin timezone to UTC for storage
|
||||
user_timezone = current_admin.timezone or "UTC" # type: ignore[attr-defined]
|
||||
if data.title is not None:
|
||||
booking.title = data.title # type: ignore[assignment]
|
||||
if data.description is not None:
|
||||
booking.description = data.description # type: ignore[assignment]
|
||||
if data.start_datetime is not None:
|
||||
booking.start_datetime = data.start_datetime # type: ignore[assignment]
|
||||
booking.start_datetime = convert_to_utc(data.start_datetime, user_timezone) # type: ignore[assignment]
|
||||
if data.end_datetime is not None:
|
||||
booking.end_datetime = data.end_datetime # type: ignore[assignment]
|
||||
booking.end_datetime = convert_to_utc(data.end_datetime, user_timezone) # type: ignore[assignment]
|
||||
|
||||
# Re-validate booking rules
|
||||
# Use booking owner's timezone for validation
|
||||
@@ -1240,15 +1242,18 @@ def reschedule_booking(
|
||||
old_start = booking.start_datetime
|
||||
old_end = booking.end_datetime
|
||||
|
||||
# Validate new time slot
|
||||
# Use booking owner's timezone for validation
|
||||
user_timezone = (booking.user.timezone or "UTC") if booking.user else "UTC"
|
||||
# Convert input times from admin timezone to UTC
|
||||
user_timezone = current_admin.timezone or "UTC" # type: ignore[attr-defined]
|
||||
start_datetime_utc = convert_to_utc(data.start_datetime, user_timezone)
|
||||
end_datetime_utc = convert_to_utc(data.end_datetime, user_timezone)
|
||||
|
||||
# Validate new time slot (using UTC times)
|
||||
booking_user_id = int(booking.user_id) if booking.user_id else 0
|
||||
errors = validate_booking_rules(
|
||||
db=db,
|
||||
space_id=int(booking.space_id), # type: ignore[arg-type]
|
||||
start_datetime=data.start_datetime,
|
||||
end_datetime=data.end_datetime,
|
||||
start_datetime=start_datetime_utc,
|
||||
end_datetime=end_datetime_utc,
|
||||
user_id=booking_user_id,
|
||||
exclude_booking_id=booking.id, # Exclude self from overlap check
|
||||
user_timezone=user_timezone,
|
||||
@@ -1260,9 +1265,9 @@ def reschedule_booking(
|
||||
detail=", ".join(errors),
|
||||
)
|
||||
|
||||
# Update times
|
||||
booking.start_datetime = data.start_datetime # type: ignore[assignment]
|
||||
booking.end_datetime = data.end_datetime # type: ignore[assignment]
|
||||
# Update times (already UTC)
|
||||
booking.start_datetime = start_datetime_utc # type: ignore[assignment]
|
||||
booking.end_datetime = end_datetime_utc # type: ignore[assignment]
|
||||
|
||||
# Sync with Google Calendar if event exists
|
||||
if booking.google_calendar_event_id and booking.user_id:
|
||||
@@ -1351,65 +1356,20 @@ def admin_create_booking(
|
||||
detail="User not found",
|
||||
)
|
||||
|
||||
# Validate booking rules (we need to check overlap with approved bookings only)
|
||||
# For admin direct booking, we need custom validation:
|
||||
# 1. Duration limits
|
||||
# 2. Working hours
|
||||
# 3. Overlap with approved bookings only (not pending)
|
||||
from app.models.settings import Settings
|
||||
from sqlalchemy import and_
|
||||
# Convert input times from admin timezone to UTC
|
||||
user_timezone = current_admin.timezone or "UTC" # type: ignore[attr-defined]
|
||||
start_datetime_utc = convert_to_utc(booking_data.start_datetime, user_timezone)
|
||||
end_datetime_utc = convert_to_utc(booking_data.end_datetime, user_timezone)
|
||||
|
||||
errors = []
|
||||
|
||||
# Fetch settings
|
||||
settings = db.query(Settings).filter(Settings.id == 1).first()
|
||||
if not settings:
|
||||
settings = Settings(
|
||||
id=1,
|
||||
min_duration_minutes=30,
|
||||
max_duration_minutes=480,
|
||||
working_hours_start=8,
|
||||
working_hours_end=20,
|
||||
max_bookings_per_day_per_user=3,
|
||||
min_hours_before_cancel=2,
|
||||
)
|
||||
db.add(settings)
|
||||
db.commit()
|
||||
db.refresh(settings)
|
||||
|
||||
# a) Validate duration in range
|
||||
duration_minutes = (booking_data.end_datetime - booking_data.start_datetime).total_seconds() / 60
|
||||
if (
|
||||
duration_minutes < settings.min_duration_minutes
|
||||
or duration_minutes > settings.max_duration_minutes
|
||||
):
|
||||
errors.append(
|
||||
f"Durata rezervării trebuie să fie între {settings.min_duration_minutes} "
|
||||
f"și {settings.max_duration_minutes} minute"
|
||||
)
|
||||
|
||||
# b) Validate working hours
|
||||
if (
|
||||
booking_data.start_datetime.hour < settings.working_hours_start
|
||||
or booking_data.end_datetime.hour > settings.working_hours_end
|
||||
):
|
||||
errors.append(
|
||||
f"Rezervările sunt permise doar între {settings.working_hours_start}:00 "
|
||||
f"și {settings.working_hours_end}:00"
|
||||
)
|
||||
|
||||
# c) Check for overlapping approved bookings only
|
||||
overlapping_bookings = db.query(Booking).filter(
|
||||
Booking.space_id == booking_data.space_id,
|
||||
Booking.status == "approved", # Only check approved bookings
|
||||
and_(
|
||||
Booking.start_datetime < booking_data.end_datetime,
|
||||
Booking.end_datetime > booking_data.start_datetime,
|
||||
),
|
||||
).first()
|
||||
|
||||
if overlapping_bookings:
|
||||
errors.append("Spațiul este deja rezervat în acest interval")
|
||||
# Validate booking rules (using UTC times, same as create_booking)
|
||||
errors = validate_booking_rules(
|
||||
db=db,
|
||||
space_id=booking_data.space_id,
|
||||
user_id=target_user_id,
|
||||
start_datetime=start_datetime_utc,
|
||||
end_datetime=end_datetime_utc,
|
||||
user_timezone=user_timezone,
|
||||
)
|
||||
|
||||
if errors:
|
||||
raise HTTPException(
|
||||
@@ -1417,12 +1377,12 @@ def admin_create_booking(
|
||||
detail=errors[0], # Return first error
|
||||
)
|
||||
|
||||
# Create booking with approved status
|
||||
# Create booking with approved status (UTC times)
|
||||
booking = Booking(
|
||||
user_id=target_user_id,
|
||||
space_id=booking_data.space_id,
|
||||
start_datetime=booking_data.start_datetime,
|
||||
end_datetime=booking_data.end_datetime,
|
||||
start_datetime=start_datetime_utc,
|
||||
end_datetime=end_datetime_utc,
|
||||
title=booking_data.title,
|
||||
description=booking_data.description,
|
||||
status="approved", # Direct approval, bypass pending state
|
||||
|
||||
Reference in New Issue
Block a user