session->set( 'bnb_pending_booking', array( 'room_id' => $room_id, 'check_in' => $check_in, 'check_out' => $check_out, 'guests' => $guests, 'services' => $services, ) ); // Add to cart. $cart_item_key = WC()->cart->add_to_cart( $product_id, 1 ); // Clean up session. WC()->session->set( 'bnb_pending_booking', null ); return $cart_item_key; } /** * Add booking data to cart item when adding to cart. * * @param array $cart_item_data Cart item data. * @param int $product_id Product ID. * @param int $variation_id Variation ID. * @return array Modified cart item data. */ public static function add_cart_item_data( array $cart_item_data, int $product_id, int $variation_id ): array { // Check if this is a room product. $room_id = ProductSync::get_room_for_product( $product_id ); if ( ! $room_id ) { return $cart_item_data; } // Get booking data from session or POST. $booking_data = WC()->session->get( 'bnb_pending_booking' ); if ( ! $booking_data ) { // Try to get from POST (for direct form submissions). // phpcs:disable WordPress.Security.NonceVerification.Missing $booking_data = array( 'room_id' => isset( $_POST['bnb_room_id'] ) ? absint( $_POST['bnb_room_id'] ) : $room_id, 'check_in' => isset( $_POST['bnb_check_in'] ) ? sanitize_text_field( wp_unslash( $_POST['bnb_check_in'] ) ) : '', 'check_out' => isset( $_POST['bnb_check_out'] ) ? sanitize_text_field( wp_unslash( $_POST['bnb_check_out'] ) ) : '', 'guests' => isset( $_POST['bnb_guests'] ) ? absint( $_POST['bnb_guests'] ) : 1, 'services' => isset( $_POST['bnb_services'] ) ? self::sanitize_services( $_POST['bnb_services'] ) : array(), ); // phpcs:enable WordPress.Security.NonceVerification.Missing } // Validate required fields. if ( empty( $booking_data['check_in'] ) || empty( $booking_data['check_out'] ) ) { return $cart_item_data; } // Calculate nights. $check_in = new \DateTime( $booking_data['check_in'] ); $check_out = new \DateTime( $booking_data['check_out'] ); $nights = (int) $check_in->diff( $check_out )->days; if ( $nights < 1 ) { return $cart_item_data; } // Calculate price breakdown. $calculator = new Calculator( $room_id, $booking_data['check_in'], $booking_data['check_out'] ); $price_breakdown = $calculator->calculate(); $room_total = $price_breakdown['total']; // Calculate services total. $services_total = 0; $services_data = array(); foreach ( $booking_data['services'] as $service_selection ) { $service_id = $service_selection['service_id'] ?? 0; $quantity = $service_selection['quantity'] ?? 1; if ( ! $service_id ) { continue; } $service_price = Service::calculate_service_price( $service_id, $quantity, $nights ); $service_data = Service::get_service_data( $service_id ); $services_data[] = array( 'service_id' => $service_id, 'quantity' => $quantity, 'price' => $service_price, 'pricing_type' => $service_data['pricing_type'] ?? 'per_booking', 'name' => $service_data['name'] ?? '', ); $services_total += $service_price; } // Store booking data. $cart_item_data[ Manager::CART_ITEM_KEY ] = array( 'room_id' => $room_id, 'check_in' => $booking_data['check_in'], 'check_out' => $booking_data['check_out'], 'guests' => $booking_data['guests'], 'nights' => $nights, 'services' => $services_data, 'price_breakdown' => array( 'room_total' => $room_total, 'services_total' => $services_total, 'grand_total' => $room_total + $services_total, 'full_breakdown' => $price_breakdown, ), ); // Generate unique key based on booking data to allow multiple bookings. $cart_item_data['unique_key'] = md5( $room_id . $booking_data['check_in'] . $booking_data['check_out'] . microtime() ); return $cart_item_data; } /** * Restore booking data from session. * * @param array $cart_item Cart item data. * @param array $values Session values. * @return array Modified cart item data. */ public static function get_cart_item_from_session( array $cart_item, array $values ): array { if ( isset( $values[ Manager::CART_ITEM_KEY ] ) ) { $cart_item[ Manager::CART_ITEM_KEY ] = $values[ Manager::CART_ITEM_KEY ]; } return $cart_item; } /** * Validate room availability before adding to cart. * * @param bool $passed Whether validation passed. * @param int $product_id Product ID. * @param int $quantity Quantity. * @param int $variation_id Variation ID. * @param array $variations Variations. * @return bool */ public static function validate_add_to_cart( bool $passed, int $product_id, int $quantity, int $variation_id = 0, array $variations = array() ): bool { // Check if this is a room product. $room_id = ProductSync::get_room_for_product( $product_id ); if ( ! $room_id ) { return $passed; } // Get booking data. $booking_data = WC()->session->get( 'bnb_pending_booking' ); if ( ! $booking_data ) { // phpcs:disable WordPress.Security.NonceVerification.Missing $booking_data = array( 'check_in' => isset( $_POST['bnb_check_in'] ) ? sanitize_text_field( wp_unslash( $_POST['bnb_check_in'] ) ) : '', 'check_out' => isset( $_POST['bnb_check_out'] ) ? sanitize_text_field( wp_unslash( $_POST['bnb_check_out'] ) ) : '', 'guests' => isset( $_POST['bnb_guests'] ) ? absint( $_POST['bnb_guests'] ) : 1, ); // phpcs:enable WordPress.Security.NonceVerification.Missing } // Validate dates provided. if ( empty( $booking_data['check_in'] ) || empty( $booking_data['check_out'] ) ) { wc_add_notice( __( 'Please select check-in and check-out dates.', 'wp-bnb' ), 'error' ); return false; } // Validate date format. $check_in = \DateTime::createFromFormat( 'Y-m-d', $booking_data['check_in'] ); $check_out = \DateTime::createFromFormat( 'Y-m-d', $booking_data['check_out'] ); if ( ! $check_in || ! $check_out ) { wc_add_notice( __( 'Invalid date format. Please use the date picker.', 'wp-bnb' ), 'error' ); return false; } // Validate check-out after check-in. if ( $check_out <= $check_in ) { wc_add_notice( __( 'Check-out date must be after check-in date.', 'wp-bnb' ), 'error' ); return false; } // Validate not in past. $today = new \DateTime( 'today' ); if ( $check_in < $today ) { wc_add_notice( __( 'Check-in date cannot be in the past.', 'wp-bnb' ), 'error' ); return false; } // Check availability. $is_available = Availability::check_availability( $room_id, $booking_data['check_in'], $booking_data['check_out'] ); if ( ! $is_available ) { wc_add_notice( __( 'Sorry, this room is not available for the selected dates.', 'wp-bnb' ), 'error' ); return false; } // Check capacity. $capacity = get_post_meta( $room_id, '_bnb_room_capacity', true ); if ( $capacity && $booking_data['guests'] > (int) $capacity ) { wc_add_notice( sprintf( /* translators: %d: Room capacity */ __( 'This room has a maximum capacity of %d guests.', 'wp-bnb' ), $capacity ), 'error' ); return false; } // Check if same room with same dates already in cart. foreach ( WC()->cart->get_cart() as $cart_item ) { if ( isset( $cart_item[ Manager::CART_ITEM_KEY ] ) ) { $existing = $cart_item[ Manager::CART_ITEM_KEY ]; if ( $existing['room_id'] === $room_id && $existing['check_in'] === $booking_data['check_in'] && $existing['check_out'] === $booking_data['check_out'] ) { wc_add_notice( __( 'This room with the same dates is already in your cart.', 'wp-bnb' ), 'error' ); return false; } } } return $passed; } /** * Validate cart items on cart load. * * @param \WC_Cart $cart Cart object. * @return void */ public static function validate_cart_items( \WC_Cart $cart ): void { $items_to_remove = array(); foreach ( $cart->get_cart() as $cart_item_key => $cart_item ) { if ( ! isset( $cart_item[ Manager::CART_ITEM_KEY ] ) ) { continue; } $booking_data = $cart_item[ Manager::CART_ITEM_KEY ]; // Check if dates are still valid. $check_in = \DateTime::createFromFormat( 'Y-m-d', $booking_data['check_in'] ); $today = new \DateTime( 'today' ); if ( $check_in < $today ) { $items_to_remove[] = array( 'key' => $cart_item_key, 'message' => __( 'A room booking was removed because the check-in date has passed.', 'wp-bnb' ), ); continue; } // Check if room still available. $is_available = Availability::check_availability( $booking_data['room_id'], $booking_data['check_in'], $booking_data['check_out'] ); if ( ! $is_available ) { $items_to_remove[] = array( 'key' => $cart_item_key, 'message' => __( 'A room booking was removed because the room is no longer available for those dates.', 'wp-bnb' ), ); } } // Remove invalid items. foreach ( $items_to_remove as $item ) { $cart->remove_cart_item( $item['key'] ); wc_add_notice( $item['message'], 'error' ); } } /** * Display booking info in cart. * * @param array $item_data Item data for display. * @param array $cart_item Cart item. * @return array Modified item data. */ public static function display_cart_item_data( array $item_data, array $cart_item ): array { if ( ! isset( $cart_item[ Manager::CART_ITEM_KEY ] ) ) { return $item_data; } $booking_data = $cart_item[ Manager::CART_ITEM_KEY ]; // Format dates. $date_format = get_option( 'date_format' ); $check_in = \DateTime::createFromFormat( 'Y-m-d', $booking_data['check_in'] ); $check_out = \DateTime::createFromFormat( 'Y-m-d', $booking_data['check_out'] ); $item_data[] = array( 'key' => __( 'Check-in', 'wp-bnb' ), 'value' => $check_in ? $check_in->format( $date_format ) : $booking_data['check_in'], ); $item_data[] = array( 'key' => __( 'Check-out', 'wp-bnb' ), 'value' => $check_out ? $check_out->format( $date_format ) : $booking_data['check_out'], ); $item_data[] = array( 'key' => __( 'Nights', 'wp-bnb' ), 'value' => $booking_data['nights'], ); $item_data[] = array( 'key' => __( 'Guests', 'wp-bnb' ), 'value' => $booking_data['guests'], ); // Display services. if ( ! empty( $booking_data['services'] ) ) { $services_list = array(); foreach ( $booking_data['services'] as $service ) { $services_list[] = $service['name'] . ' × ' . $service['quantity']; } $item_data[] = array( 'key' => __( 'Services', 'wp-bnb' ), 'value' => implode( ', ', $services_list ), ); } return $item_data; } /** * Calculate dynamic prices for cart items. * * @param \WC_Cart $cart Cart object. * @return void */ public static function calculate_cart_item_prices( \WC_Cart $cart ): void { if ( is_admin() && ! defined( 'DOING_AJAX' ) ) { return; } foreach ( $cart->get_cart() as $cart_item ) { if ( ! isset( $cart_item[ Manager::CART_ITEM_KEY ] ) ) { continue; } $booking_data = $cart_item[ Manager::CART_ITEM_KEY ]; $grand_total = $booking_data['price_breakdown']['grand_total'] ?? 0; /** * Filter the cart item price for a booking. * * @param float $price The calculated price. * @param array $cart_item The cart item. */ $grand_total = apply_filters( 'wp_bnb_wc_cart_item_price', $grand_total, $cart_item ); // Set the price. $cart_item['data']->set_price( $grand_total ); } } /** * Lock quantity to 1 for room products. * * @param array $args Input arguments. * @param \WC_Product $product Product object. * @return array Modified arguments. */ public static function lock_quantity( array $args, \WC_Product $product ): array { if ( ProductSync::is_room_product( $product ) ) { $args['min_value'] = 1; $args['max_value'] = 1; $args['readonly'] = true; } return $args; } /** * Add booking data to order item meta. * * @param \WC_Order_Item_Product $item Order item. * @param string $cart_item_key Cart item key. * @param array $values Cart item values. * @param \WC_Order $order Order object. * @return void */ public static function add_order_item_meta( \WC_Order_Item_Product $item, string $cart_item_key, array $values, \WC_Order $order ): void { if ( ! isset( $values[ Manager::CART_ITEM_KEY ] ) ) { return; } $booking_data = $values[ Manager::CART_ITEM_KEY ]; // Store booking data in order item meta. $item->add_meta_data( '_bnb_room_id', $booking_data['room_id'] ); $item->add_meta_data( '_bnb_check_in', $booking_data['check_in'] ); $item->add_meta_data( '_bnb_check_out', $booking_data['check_out'] ); $item->add_meta_data( '_bnb_guests', $booking_data['guests'] ); $item->add_meta_data( '_bnb_nights', $booking_data['nights'] ); $item->add_meta_data( '_bnb_services', wp_json_encode( $booking_data['services'] ) ); $item->add_meta_data( '_bnb_price_breakdown', wp_json_encode( $booking_data['price_breakdown'] ) ); // Add visible meta for admin display. $date_format = get_option( 'date_format' ); $check_in = \DateTime::createFromFormat( 'Y-m-d', $booking_data['check_in'] ); $check_out = \DateTime::createFromFormat( 'Y-m-d', $booking_data['check_out'] ); $item->add_meta_data( __( 'Check-in', 'wp-bnb' ), $check_in ? $check_in->format( $date_format ) : $booking_data['check_in'] ); $item->add_meta_data( __( 'Check-out', 'wp-bnb' ), $check_out ? $check_out->format( $date_format ) : $booking_data['check_out'] ); $item->add_meta_data( __( 'Nights', 'wp-bnb' ), $booking_data['nights'] ); $item->add_meta_data( __( 'Guests', 'wp-bnb' ), $booking_data['guests'] ); } /** * Sanitize services array from POST data. * * @param mixed $services Raw services data. * @return array Sanitized services array. */ private static function sanitize_services( $services ): array { if ( ! is_array( $services ) ) { return array(); } $sanitized = array(); foreach ( $services as $service ) { if ( ! is_array( $service ) ) { continue; } $service_id = isset( $service['service_id'] ) ? absint( $service['service_id'] ) : 0; $quantity = isset( $service['quantity'] ) ? absint( $service['quantity'] ) : 1; if ( $service_id > 0 ) { $sanitized[] = array( 'service_id' => $service_id, 'quantity' => max( 1, $quantity ), ); } } return $sanitized; } }