read_request( $product_id, $variation_id ); if ( ! is_wp_error( $booking ) ) { /** * Whether dates the rules allow can still be booked: true, or the reason they cannot. * * @since 1.1.0 * @param true|\WP_Error $allowed True so far. * @param array $booking Dates asked for. * @param int $product_id Product being added. * @param int $variation_id Variation being added, when there is one. * @param int $quantity How many are being added. */ $allowed = apply_filters( 'abfw_check_booking', true, $booking, $product_id, $variation_id, $quantity ); $booking = is_wp_error( $allowed ) ? $allowed : $booking; } if ( is_wp_error( $booking ) ) { wc_add_notice( $booking->get_error_message(), 'error' ); return false; } return $passed; } /** * Attach the dates to the cart line. * * @since 1.0.0 * @param array $item Cart line data. * @param int $product_id Product being added. * @param int $variation_id Variation being added, when there is one. * @return array */ public function add_cart_item_data( $item, $product_id, $variation_id = 0 ) { if ( ! Settings::is_bookable( $product_id ) ) { return $item; } $booking = $this->read_request( $product_id, $variation_id ); if ( ! is_wp_error( $booking ) ) { $item['abfw'] = $booking; } return $item; } /** * Price the stay: the product price is charged for every night or day. * * The price always comes from the catalogue rather than from the line being * priced, because the totals are computed several times in one request. * * @since 1.0.0 * @param \WC_Cart $cart Cart being totalled. */ public function set_prices( $cart ) { if ( is_admin() && ! wp_doing_ajax() ) { return; } foreach ( $cart->get_cart() as $item ) { if ( empty( $item['abfw']['units'] ) || ! isset( $item['data'] ) ) { continue; } $sold = wc_get_product( $item['variation_id'] ? $item['variation_id'] : $item['product_id'] ); if ( ! $sold ) { continue; } $unit = self::unit_price( $item, $sold ); /** * The price of the whole stay, once the price of a night or day is multiplied. * * @since 1.1.0 * @param float $price Price of the stay. * @param float $unit Price of a single night or day. * @param array $item Cart line being priced. * @param \WC_Product $sold Product being sold. */ $price = (float) apply_filters( 'abfw_stay_price', $unit * (int) $item['abfw']['units'], $unit, $item, $sold ); $item['data']->set_price( $price ); } } /** * The price of one night or one day of a cart line, before it is multiplied. * * @since 1.1.0 * @param array $item Cart line being priced, or one made up to price a stay. * @param \WC_Product $sold Product being sold. * @return float */ public static function unit_price( array $item, $sold ) { /** * The price of one night or one day, before it is multiplied. * * @since 1.0.0 * @param float $unit Price of a single night or day. * @param array $item Cart line being priced. * @param \WC_Product $sold Product being sold. */ return (float) apply_filters( 'abfw_unit_price', (float) $sold->get_price( 'edit' ), $item, $sold ); } /** * Refuse a bookable product added outside the product page. * * The block shop and the store API carry no dates, so a booking taken there * would reach the order with nothing to book. * * @since 1.0.0 * @param \WC_Product $product Product being added. * @throws \Automattic\WooCommerce\StoreApi\Exceptions\RouteException When the product needs dates. */ public function validate_store_api( $product ) { if ( ! $product instanceof \WC_Product || ! Settings::is_bookable( $product->get_id() ) ) { return; } throw new \Automattic\WooCommerce\StoreApi\Exceptions\RouteException( 'abfw_dates_required', esc_html__( 'Please choose your dates on the product page.', 'abogo-booking' ), 400 ); } /** * Show the dates under the product name in the cart and at checkout. * * @since 1.0.0 * @param array $data Lines already displayed. * @param array $item Cart line. * @return array */ public function display( $data, $item ) { if ( empty( $item['abfw'] ) ) { return $data; } $data[] = array( 'name' => __( 'Dates', 'abogo-booking' ), 'value' => $this->format( $item['abfw'] ), ); return $data; } /** * Keep the dates on the order line, so the order reads on its own. * * @since 1.0.0 * @param \WC_Order_Item_Product $order_item Line being written. * @param string $cart_key Key of the cart line. * @param array $item Cart line. */ public function save_order_item( $order_item, $cart_key, $item ) { if ( empty( $item['abfw'] ) ) { return; } $order_item->add_meta_data( '_abfw_start', $item['abfw']['start'], true ); $order_item->add_meta_data( '_abfw_end', $item['abfw']['end'], true ); $order_item->add_meta_data( '_abfw_units', $item['abfw']['units'], true ); $order_item->add_meta_data( __( 'Dates', 'abogo-booking' ), $this->format( $item['abfw'] ), true ); } /** * The dates as the customer reads them. * * @since 1.0.0 * @param array $booking Booking data held on the line. * @return string */ public function format( array $booking ) { $format = get_option( 'date_format' ); $start = date_i18n( $format, strtotime( $booking['start'] ) ); $end = date_i18n( $format, strtotime( $booking['end'] ) ); $length = 'days' === $booking['mode'] /* translators: %s: number of days. */ ? sprintf( _n( '%s day', '%s days', $booking['units'], 'abogo-booking' ), number_format_i18n( $booking['units'] ) ) /* translators: %s: number of nights. */ : sprintf( _n( '%s night', '%s nights', $booking['units'], 'abogo-booking' ), number_format_i18n( $booking['units'] ) ); if ( $booking['start'] === $booking['end'] ) { return $start; } /* translators: 1: start date, 2: end date, 3: length of the stay. */ return sprintf( __( '%1$s to %2$s (%3$s)', 'abogo-booking' ), $start, $end, $length ); } /** * Read and check the dates posted with the add to cart form. * * @since 1.0.0 * @param int $product_id Product being added. * @param int $variation_id Variation being added, when there is one. * @return array|\WP_Error */ private function read_request( $product_id, $variation_id = 0 ) { $start = $this->posted( 'abfw_start', $product_id ); $end = $this->posted( 'abfw_end', $product_id ); return Dates::validate( $start, $end, $product_id, $variation_id ); } /** * One posted date, from the field of that product when a group posted several. * * @since 1.0.0 * @param string $name Field name. * @param int $product_id Product being added. * @return string */ private function posted( $name, $product_id ) { // phpcs:disable WordPress.Security.NonceVerification.Missing foreach ( array( $name . '_' . $product_id, $name ) as $key ) { if ( isset( $_POST[ $key ] ) ) { return sanitize_text_field( wp_unslash( $_POST[ $key ] ) ); } } // phpcs:enable WordPress.Security.NonceVerification.Missing return ''; } }