@@ -115,15 +115,74 @@ def _get_soft_closed_time_spans_for_reservation_unit(reservation_unit: "Reservat
115115def _find_first_reservable_time_span_for_reservation_unit (
116116 reservation_unit : "ReservationUnit" ,
117117 normalised_reservable_time_spans : list [TimeSpanElement ],
118+ reservations : list [TimeSpanElement ],
118119 minimum_duration_minutes : int ,
119120) -> datetime | None :
120121 """
121- Find the first reservable time span for the ReservationUnit that is long enough to fit the minimum duration
122-
123- Loop through the normalised time spans to find the first one that is long enough to fit the minimum duration
122+ Find the first reservable time span for the ReservationUnit.
123+
124+ In this function we know that normalised_reservable_time_spans (without buffers) and reservations (with buffers)
125+ never overlap due to earlier normalisation.
126+ However, normalised_reservable_time_spans (with buffers from reservation_unit) and
127+ reservations (without buffers) can overlap, so we need to process the normalised_reservable_time_spans one-by-one
128+ and check if they overlap with reservations.
129+ If they do, we need to shorten the reservable_time_span so that its buffer doesn't overlap with the reservation,
130+ while leaving the buffer size unchanged.
131+ After processing, we can check if the reservable_time_span is still long enough to fit the minimum duration.
124132 """
125133 for reservable_time_span in normalised_reservable_time_spans :
126- # Move time span start time to the next valid start time
134+ if reservable_time_span is None :
135+ continue
136+
137+ reservable_time_span .buffer_time_before = reservation_unit .buffer_time_before
138+ reservable_time_span .buffer_time_after = reservation_unit .buffer_time_after
139+
140+ for reservation in reservations :
141+ # Reservation (with buffers) can not overlap with the reservable time span (without buffers).
142+ if reservable_time_span .overlaps_with (reservation ):
143+ raise ValueError ("Reservable Time Span overlaps with Reservations buffer, this should never happen." )
144+
145+ # Only continue forward if a buffered time span overlaps with a reservation (without buffers)
146+ if not reservation .overlaps_with (reservable_time_span ):
147+ # No overlapping, skip
148+ continue
149+
150+ # Reservation is inside the Before-buffer, shorten the reservable time span from the start
151+ # ┌──────────────────────────┬─────────────────────────────────────┐
152+ # │ ----oooooo -> ----oo │ Reservation starts in Before-buffer │
153+ # │ xxxx -> xxxx │ │
154+ # ├──────────────────────────┼─────────────────────────────────────┤
155+ # │ --oooo -> --oo │ Reservation ends in Before-buffer │
156+ # │ xxxx -> xxxx │ │
157+ # └──────────────────────────┴─────────────────────────────────────┘
158+ elif (
159+ reservable_time_span .buffered_start_datetime
160+ <= reservation .end_datetime
161+ <= reservable_time_span .start_datetime
162+ ):
163+ overlap = reservation .end_datetime - reservable_time_span .buffered_start_datetime
164+ reservable_time_span .start_datetime += overlap
165+
166+ # Reservation is inside the After-buffer, shorten the reservable time span from the end
167+ # ┌──────────────────────────┬─────────────────────────────────────┐
168+ # │ oooo---- -> oo---- │ Reservation starts in After-buffer │
169+ # │ xxxx -> xxxx │ │
170+ # ├──────────────────────────┼─────────────────────────────────────┤
171+ # │ oooooo---- -> oo---- │ Reservation ends in After-buffer │
172+ # │ xxxx -> xxxx │ │
173+ # └──────────────────────────┴─────────────────────────────────────┘
174+ elif (
175+ reservable_time_span .end_datetime
176+ <= reservation .start_datetime
177+ <= reservable_time_span .buffered_end_datetime
178+ ):
179+ overlap = reservable_time_span .buffered_end_datetime - reservation .start_datetime
180+ reservable_time_span .end_datetime -= overlap
181+
182+ if reservable_time_span .duration_minutes <= 0 :
183+ break
184+
185+ # In case of an invalid start time due to normalisation, move to the next valid start time
127186 reservable_time_span .move_to_next_valid_start_time (reservation_unit )
128187
129188 if reservable_time_span .can_fit_reservation_for_reservation_unit (
@@ -305,15 +364,15 @@ def with_first_reservable_time(
305364 # Affect closed status
306365 # Can overlap with buffers
307366 reservation_unit_hard_closed_time_spans : list [TimeSpanElement ] = (
308- _get_hard_closed_time_spans_for_reservation_unit (reservation_unit ) #
367+ _get_hard_closed_time_spans_for_reservation_unit (reservation_unit ) # Formatting :)
309368 + shared_hard_closed_time_spans
310369 )
311370
312371 # Reservation Unit Soft Closed Time Spans
313372 # Don't affect closed status
314373 # Can overlap with buffers
315374 reservation_unit_soft_closed_time_spans : list [TimeSpanElement ] = (
316- _get_soft_closed_time_spans_for_reservation_unit (reservation_unit ) #
375+ _get_soft_closed_time_spans_for_reservation_unit (reservation_unit ) # Formatting :)
317376 )
318377
319378 # Reservation Closed Time Spans
@@ -326,7 +385,7 @@ def with_first_reservable_time(
326385 # These time spans have no effect on the closed status of the ReservationUnit,
327386 # meaning that the ReservationUnit can be shown as open, even if there are no reservable time spans.
328387 soft_closed_time_spans = (
329- reservation_unit_soft_closed_time_spans #
388+ reservation_unit_soft_closed_time_spans # Formatting :)
330389 + reservation_unit_reservation_closed_time_spans
331390 )
332391
@@ -339,10 +398,7 @@ def with_first_reservable_time(
339398
340399 # Go through each ReservableTimeSpan individually one-by-one
341400 for reservable_time_span in reservation_unit .origin_hauki_resource .reservable_time_spans .all ():
342- current_time_span = reservable_time_span .as_time_span_element (
343- buffer_time_before = reservation_unit .buffer_time_before ,
344- buffer_time_after = reservation_unit .buffer_time_after ,
345- )
401+ current_time_span = reservable_time_span .as_time_span_element ()
346402
347403 # Combine all Hard-Closed time spans into one list
348404 hard_closed_time_spans : list [TimeSpanElement ] = (
@@ -377,6 +433,7 @@ def with_first_reservable_time(
377433 first_reservable_times [reservation_unit .pk ] = _find_first_reservable_time_span_for_reservation_unit (
378434 reservation_unit = reservation_unit ,
379435 normalised_reservable_time_spans = soft_normalised_reservable_time_spans ,
436+ reservations = reservation_unit_reservation_closed_time_spans ,
380437 minimum_duration_minutes = minimum_duration_minutes ,
381438 )
382439 # Suitable timespan was found, select and continue to next reservation unit
0 commit comments