diff( $end )->format( '%a' ); return 'days' === $mode ? $nights + 1 : $nights; } /** * Check a pair of dates against the rules of a product. * * @since 1.0.0 * @param string $raw_start Start date as posted. * @param string $raw_end End date as posted, empty when one date is asked for. * @param int $product_id Product being booked. * @param int $variation_id Variation being booked, when there is one. * @return array|\WP_Error Booking data on success. */ public static function validate( $raw_start, $raw_end, $product_id, $variation_id = 0 ) { $mode = Settings::get( 'mode', $product_id, $variation_id ); $two = 'two' === Settings::get( 'dates', $product_id, $variation_id ); $duration = max( 1, (int) Settings::get( 'duration', $product_id, $variation_id ) ); $start = self::parse( $raw_start ); if ( ! $start ) { return new \WP_Error( 'abfw_no_start', __( 'Please choose a date.', 'abogo-booking' ) ); } $end = $two ? self::parse( $raw_end ) : self::end_of_single_date( $start, $mode ); if ( ! $end ) { return new \WP_Error( 'abfw_no_end', __( 'Please choose an end date.', 'abogo-booking' ) ); } if ( $end < $start ) { return new \WP_Error( 'abfw_reversed', __( 'The end date cannot come before the start date.', 'abogo-booking' ) ); } $window = self::check_window( $start, $product_id, $variation_id ); if ( is_wp_error( $window ) ) { return $window; } $units = self::units( $start, $end, $mode ); if ( $units < 1 ) { return new \WP_Error( 'abfw_too_short', __( 'Please choose at least one night.', 'abogo-booking' ) ); } $length = self::check_length( $units, $duration, $product_id, $variation_id ); if ( is_wp_error( $length ) ) { return $length; } return array( 'start' => $start->format( 'Y-m-d' ), 'end' => $end->format( 'Y-m-d' ), 'units' => $units, 'mode' => $mode, ); } /** * Read dates the shop sets by hand: only dates that cannot exist, or a stay * longer than any can be, are refused, never the product's own rules. * * @since 1.0.1 * @param string $raw_start Start date as posted. * @param string $raw_end End date as posted, empty when one date is asked for. * @param int $product_id Product booked. * @param int $variation_id Variation booked, when there is one. * @return array|null Booking data, null when the dates cannot exist. */ public static function possible( $raw_start, $raw_end, $product_id, $variation_id = 0 ) { $mode = Settings::get( 'mode', $product_id, $variation_id ); $start = self::parse( $raw_start ); if ( ! $start ) { return null; } $end = 'two' === Settings::get( 'dates', $product_id, $variation_id ) ? self::parse( $raw_end ) : self::end_of_single_date( $start, $mode ); $units = $end ? self::units( $start, $end, $mode ) : 0; if ( ! $end || $end < $start || $units < 1 || $units > self::LONGEST ) { return null; } return array( 'start' => $start->format( 'Y-m-d' ), 'end' => $end->format( 'Y-m-d' ), 'units' => $units, 'mode' => $mode, ); } /** * Whether the start date falls inside the period open to booking. * * @since 1.0.0 * @param DateTimeImmutable $start Chosen start. * @param int $product_id Product being booked. * @param int $variation_id Variation being booked, when there is one. * @return true|\WP_Error */ private static function check_window( DateTimeImmutable $start, $product_id, $variation_id = 0 ) { $first = (int) Settings::get( 'first', $product_id, $variation_id ); $last = (int) Settings::get( 'last', $product_id, $variation_id ); $today = self::today(); if ( $start < $today->modify( '+' . $first . ' days' ) ) { return new \WP_Error( 'abfw_too_soon', sprintf( /* translators: %s: number of days. */ _n( 'Bookings open %s day from today.', 'Bookings open %s days from today.', max( 1, $first ), 'abogo-booking' ), number_format_i18n( $first ) ) ); } if ( $last > 0 && $start > $today->modify( '+' . $last . ' days' ) ) { return new \WP_Error( 'abfw_too_far', __( 'This date is too far ahead to be booked.', 'abogo-booking' ) ); } return true; } /** * Whether the length fits the block size, the allowed minimum and maximum, and * the longest stay any product can take. * * @since 1.0.0 * @param int $units Nights or days chosen. * @param int $duration Size of one block. * @param int $product_id Product being booked. * @param int $variation_id Variation being booked, when there is one. * @return true|\WP_Error */ private static function check_length( $units, $duration, $product_id, $variation_id = 0 ) { if ( $units > self::LONGEST ) { return new \WP_Error( 'abfw_too_long', sprintf( /* translators: %s: number of blocks. */ __( 'Please book no more than %s.', 'abogo-booking' ), number_format_i18n( self::LONGEST ) ) ); } if ( $units % $duration !== 0 ) { return new \WP_Error( 'abfw_bad_block', sprintf( /* translators: %s: number of days or nights. */ __( 'Bookings are taken in blocks of %s.', 'abogo-booking' ), number_format_i18n( $duration ) ) ); } $blocks = (int) ( $units / $duration ); $min = (int) Settings::get( 'min', $product_id, $variation_id ); $max = (int) Settings::get( 'max', $product_id, $variation_id ); if ( $min > 0 && $blocks < $min ) { return new \WP_Error( 'abfw_too_short_block', sprintf( /* translators: %s: number of blocks. */ __( 'Please book at least %s.', 'abogo-booking' ), number_format_i18n( $min * $duration ) ) ); } if ( $max > 0 && $blocks > $max ) { return new \WP_Error( 'abfw_too_long', sprintf( /* translators: %s: number of blocks. */ __( 'Please book no more than %s.', 'abogo-booking' ), number_format_i18n( $max * $duration ) ) ); } return true; } /** * Where a booking of a single date ends: a night runs into the next day. * * @since 1.0.1 * @param DateTimeImmutable $start Chosen date. * @param string $mode Either days or nights. * @return DateTimeImmutable */ private static function end_of_single_date( DateTimeImmutable $start, $mode ) { return $start->modify( 'nights' === $mode ? '+1 day' : '+0 day' ); } /** * Time zone of the shop, so a date never shifts by one day. * * @since 1.0.0 * @return DateTimeZone */ private static function timezone() { return function_exists( 'wp_timezone' ) ? wp_timezone() : new DateTimeZone( 'UTC' ); } }