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)
# 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