resources->find( $resource_id ); if ( ! $resource ) { throw new CatalogException( 'abece_booking_not_found', __( 'The item was not found.', 'abece-booking' ), 404 ); } if ( ! MembershipModeService::enabled() ) { $resource['access_mode'] = 'public'; $member_override = false; } $now = $now ? DateTimeImmutable::createFromInterface( $now )->setTimezone( wp_timezone() ) : new DateTimeImmutable( 'now', wp_timezone() ); $booking_day = Time::local( $date, '12:00' ); $is_member = $member_override ?? ( $booking_day && $this->membership->isUserMember( $wp_user_id, $booking_day ) ); $result = [ 'resource_id' => $resource_id, 'date' => $date, 'timezone' => wp_timezone()->getName(), 'is_member' => $is_member, 'intervals' => [] ]; if ( ! $resource['is_active'] || ! $resource['is_visible'] || ! $resource['is_bookable'] ) { return $result; } if ( 'members_only' === $resource['access_mode'] && ! $is_member ) { return $result + ['booking_restriction_code'=>'abece_booking_members_only','booking_restriction_message'=>__('This resource is available to club members only.','abece-booking')]; } $day = Time::day( $date ); if ( ! $day || $date < $now->format( 'Y-m-d' ) ) { return $result; } $days = $resource[ $is_member ? 'member_booking_days' : 'guest_booking_days' ]; /* translators: %d: maximum number of days ahead. */ if ( ! $admin && null !== $days && $date > $now->modify( '+' . $days . ' days' )->format( 'Y-m-d' ) ) { return $result + ['booking_restriction_code'=>'abece_booking_booking_horizon','booking_restriction_message'=>sprintf(__('You can book this resource up to %d days ahead.','abece-booking'),$days)]; } $step = $resource['booking_step_minutes']; $minimum = $resource['min_booking_minutes']; $maximum = $admin ? 1500 : $resource['max_booking_minutes']; if ( $step <= 0 || $minimum <= 0 || $maximum < $minimum ) { throw new CatalogException( 'abece_booking_invalid_booking_grid', __( 'The resource booking time grid is invalid.', 'abece-booking' ), 400 ); } $minimum = (int) ceil( $minimum / $step ) * $step; $maximum = (int) floor( $maximum / $step ) * $step; if ( $minimum > $maximum ) { return $result; } $tomorrow = Time::nextDay( $date ); if ( ! $tomorrow ) { return $result; } $busy = array_map( static fn( $row ) => [ Time::fromSql( $row['starts_at'] ), Time::fromSql( $row['ends_at'] ) ], $this->occupied->occupied( $resource_id, Time::sql( $day ), Time::sql( $tomorrow ), $exclude ) ); $free = $this->subtract( $this->schedule->intervals( $resource_id, $date ), $busy ); // Walk actual instants: nonexistent DST wall times are skipped; repeated times have distinct offsets. $grid = []; for ( $timestamp = $day->getTimestamp(); $timestamp <= $tomorrow->getTimestamp(); $timestamp += 60 ) { $local = Time::timestamp( $timestamp ); if ( '00' === $local->format( 's' ) && ( (int) $local->format( 'H' ) * 60 + (int) $local->format( 'i' ) ) % $step === 0 ) { $grid[] = $timestamp; } } foreach ( $free as [ $open, $close ] ) { $aligned = array_values( array_filter( $grid, static fn( $time ) => $time >= $open && $time <= $close ) ); if ( ! $aligned ) { continue; } $last_end = $aligned[ count( $aligned ) - 1 ]; foreach ( $aligned as $start ) { if ( $start <= $now->getTimestamp() ) { continue; } if ( ! $admin && ! $is_member && 'members_priority' === $resource['access_mode'] && ($resource['public_release_hours'] === null || $resource['public_release_hours'] <= 0 || $start > $now->getTimestamp() + $resource['public_release_hours'] * 3600) ) { $result['restriction_starts_after']=$now->getTimestamp()+($resource['public_release_hours']??0)*3600; $result['booking_restriction_code']='abece_booking_member_priority'; $result['booking_restriction_message']=__('Members have priority for this time. Please try closer to the start.','abece-booking'); continue; } $duration = min( $maximum, (int) floor( ( $last_end - $start ) / ( $step * 60 ) ) * $step ); if ( $duration < $minimum ) { continue; } // Every duration min + n * step up to max fits the same uninterrupted free interval. $ends = []; for ( $minutes = $minimum; $minutes <= $duration; $minutes += $step ) { $ends[] = [ 'ends_at' => Time::iso( $start + $minutes * 60 ), 'duration_minutes' => $minutes ]; } $result['intervals'][] = [ 'end_times' => $ends, 'starts_at' => Time::iso( $start ), 'earliest_end_at' => Time::iso( $start + $minimum * 60 ), 'latest_end_at' => Time::iso( $start + $duration * 60 ), 'min_duration_minutes' => $minimum, 'max_duration_minutes' => $duration, 'booking_step_minutes' => $step ]; } } return $result; } private function subtract( array $intervals, array $busy ): array { foreach ( $busy as [ $start, $end ] ) { if ( $end <= $start ) { continue; } $remaining = []; foreach ( $intervals as [ $open, $close ] ) { if ( $end <= $open || $start >= $close ) { $remaining[] = [ $open, $close ]; continue; } if ( $start > $open ) { $remaining[] = [ $open, min( $start, $close ) ]; } if ( $end < $close ) { $remaining[] = [ max( $end, $open ), $close ]; } } $intervals = $remaining; } return $intervals; } }