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:
Claude Agent
2026-03-04 14:28:07 +00:00
parent 1a5b2f0e5e
commit 7d649a1e0f

View File

@@ -1066,14 +1066,16 @@ def admin_update_booking(
) )
# Update fields (only if provided) # 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: if data.title is not None:
booking.title = data.title # type: ignore[assignment] booking.title = data.title # type: ignore[assignment]
if data.description is not None: if data.description is not None:
booking.description = data.description # type: ignore[assignment] booking.description = data.description # type: ignore[assignment]
if data.start_datetime is not None: 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: 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 # Re-validate booking rules
# Use booking owner's timezone for validation # Use booking owner's timezone for validation
@@ -1240,15 +1242,18 @@ def reschedule_booking(
old_start = booking.start_datetime old_start = booking.start_datetime
old_end = booking.end_datetime old_end = booking.end_datetime
# Validate new time slot # Convert input times from admin timezone to UTC
# Use booking owner's timezone for validation user_timezone = current_admin.timezone or "UTC" # type: ignore[attr-defined]
user_timezone = (booking.user.timezone or "UTC") if booking.user else "UTC" 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 booking_user_id = int(booking.user_id) if booking.user_id else 0
errors = validate_booking_rules( errors = validate_booking_rules(
db=db, db=db,
space_id=int(booking.space_id), # type: ignore[arg-type] space_id=int(booking.space_id), # type: ignore[arg-type]
start_datetime=data.start_datetime, start_datetime=start_datetime_utc,
end_datetime=data.end_datetime, end_datetime=end_datetime_utc,
user_id=booking_user_id, user_id=booking_user_id,
exclude_booking_id=booking.id, # Exclude self from overlap check exclude_booking_id=booking.id, # Exclude self from overlap check
user_timezone=user_timezone, user_timezone=user_timezone,
@@ -1260,9 +1265,9 @@ def reschedule_booking(
detail=", ".join(errors), detail=", ".join(errors),
) )
# Update times # Update times (already UTC)
booking.start_datetime = data.start_datetime # type: ignore[assignment] booking.start_datetime = start_datetime_utc # type: ignore[assignment]
booking.end_datetime = data.end_datetime # type: ignore[assignment] booking.end_datetime = end_datetime_utc # type: ignore[assignment]
# Sync with Google Calendar if event exists # Sync with Google Calendar if event exists
if booking.google_calendar_event_id and booking.user_id: if booking.google_calendar_event_id and booking.user_id:
@@ -1351,65 +1356,20 @@ def admin_create_booking(
detail="User not found", detail="User not found",
) )
# Validate booking rules (we need to check overlap with approved bookings only) # Convert input times from admin timezone to UTC
# For admin direct booking, we need custom validation: user_timezone = current_admin.timezone or "UTC" # type: ignore[attr-defined]
# 1. Duration limits start_datetime_utc = convert_to_utc(booking_data.start_datetime, user_timezone)
# 2. Working hours end_datetime_utc = convert_to_utc(booking_data.end_datetime, user_timezone)
# 3. Overlap with approved bookings only (not pending)
from app.models.settings import Settings
from sqlalchemy import and_
errors = [] # Validate booking rules (using UTC times, same as create_booking)
errors = validate_booking_rules(
# Fetch settings db=db,
settings = db.query(Settings).filter(Settings.id == 1).first() space_id=booking_data.space_id,
if not settings: user_id=target_user_id,
settings = Settings( start_datetime=start_datetime_utc,
id=1, end_datetime=end_datetime_utc,
min_duration_minutes=30, user_timezone=user_timezone,
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")
if errors: if errors:
raise HTTPException( raise HTTPException(
@@ -1417,12 +1377,12 @@ def admin_create_booking(
detail=errors[0], # Return first error detail=errors[0], # Return first error
) )
# Create booking with approved status # Create booking with approved status (UTC times)
booking = Booking( booking = Booking(
user_id=target_user_id, user_id=target_user_id,
space_id=booking_data.space_id, space_id=booking_data.space_id,
start_datetime=booking_data.start_datetime, start_datetime=start_datetime_utc,
end_datetime=booking_data.end_datetime, end_datetime=end_datetime_utc,
title=booking_data.title, title=booking_data.title,
description=booking_data.description, description=booking_data.description,
status="approved", # Direct approval, bypass pending state status="approved", # Direct approval, bypass pending state