namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'admin_permission' ), 'args' => $this->get_collection_params(), ), ) ); // GET /guests/search - Search guests (admin). register_rest_route( $this->namespace, '/' . $this->rest_base . '/search', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'search_guests' ), 'permission_callback' => array( $this, 'admin_permission' ), 'args' => array( 'q' => array( 'description' => __( 'Search query (name, email, phone).', 'wp-bnb' ), 'type' => 'string', 'required' => true, 'sanitize_callback' => 'sanitize_text_field', ), 'limit' => array( 'description' => __( 'Maximum results.', 'wp-bnb' ), 'type' => 'integer', 'default' => 10, 'minimum' => 1, 'maximum' => 50, 'sanitize_callback' => 'absint', ), ), ), ) ); // GET /guests/{id} - Get single guest (admin). register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'admin_permission' ), 'args' => array( 'id' => array( 'description' => __( 'Guest ID.', 'wp-bnb' ), 'type' => 'integer', 'required' => true, 'sanitize_callback' => 'absint', ), ), ), ) ); // GET /guests/{id}/bookings - Get guest's bookings (admin). register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)/bookings', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_guest_bookings' ), 'permission_callback' => array( $this, 'admin_permission' ), 'args' => array( 'id' => array( 'description' => __( 'Guest ID.', 'wp-bnb' ), 'type' => 'integer', 'required' => true, 'sanitize_callback' => 'absint', ), ), ), ) ); } /** * Get collection of guests. * * @param WP_REST_Request $request Current request. * @return WP_REST_Response|WP_Error Response object or error. */ public function get_items( $request ) { $rate_limit_error = $this->check_rate_limit( $request ); if ( $rate_limit_error ) { return $rate_limit_error; } $pagination = $this->get_pagination_params( $request ); $sorting = $this->get_sorting_params( $request, array( 'title', 'date' ), 'title' ); $args = array( 'post_type' => Guest::POST_TYPE, 'post_status' => 'publish', 'posts_per_page' => $pagination['per_page'], 'offset' => $pagination['offset'], 'orderby' => $sorting['orderby'], 'order' => $sorting['order'], ); // Search filter. $search = $request->get_param( 'search' ); if ( $search ) { $args['s'] = $search; } // Status filter. $status = $request->get_param( 'status' ); if ( $status ) { $args['meta_query'] = array( array( 'key' => '_bnb_guest_status', 'value' => $status, ), ); } $query = new \WP_Query( $args ); $items = array(); foreach ( $query->posts as $post ) { $items[] = $this->prepare_guest_response( $post ); } $response = $this->formatter->collection( $items, $query->found_posts, $pagination['page'], $pagination['per_page'] ); return $this->add_rate_limit_headers( $response, $request ); } /** * Search guests by name, email, or phone. * * @param WP_REST_Request $request Current request. * @return WP_REST_Response|WP_Error Response object or error. */ public function search_guests( $request ) { $rate_limit_error = $this->check_rate_limit( $request ); if ( $rate_limit_error ) { return $rate_limit_error; } $query = $request->get_param( 'q' ); $limit = $request->get_param( 'limit' ); // Search by title (name) first. $args = array( 'post_type' => Guest::POST_TYPE, 'post_status' => 'publish', 'posts_per_page' => $limit, 's' => $query, ); $results = get_posts( $args ); // Also search by email and phone if we have room for more results. if ( count( $results ) < $limit ) { $existing_ids = wp_list_pluck( $results, 'ID' ); $remaining = $limit - count( $results ); // Search by email. $email_args = array( 'post_type' => Guest::POST_TYPE, 'post_status' => 'publish', 'posts_per_page' => $remaining, 'post__not_in' => $existing_ids, 'meta_query' => array( array( 'key' => '_bnb_guest_email', 'value' => $query, 'compare' => 'LIKE', ), ), ); $email_results = get_posts( $email_args ); $results = array_merge( $results, $email_results ); // Search by phone if still room. if ( count( $results ) < $limit ) { $existing_ids = wp_list_pluck( $results, 'ID' ); $remaining = $limit - count( $results ); $phone_args = array( 'post_type' => Guest::POST_TYPE, 'post_status' => 'publish', 'posts_per_page' => $remaining, 'post__not_in' => $existing_ids, 'meta_query' => array( array( 'key' => '_bnb_guest_phone', 'value' => $query, 'compare' => 'LIKE', ), ), ); $phone_results = get_posts( $phone_args ); $results = array_merge( $results, $phone_results ); } } $items = array(); foreach ( $results as $post ) { $items[] = $this->prepare_guest_summary( $post ); } $response = $this->formatter->success( $items ); return $this->add_rate_limit_headers( $response, $request ); } /** * Get single guest. * * @param WP_REST_Request $request Current request. * @return WP_REST_Response|WP_Error Response object or error. */ public function get_item( $request ) { $rate_limit_error = $this->check_rate_limit( $request ); if ( $rate_limit_error ) { return $rate_limit_error; } $id = $request->get_param( 'id' ); $post = get_post( $id ); if ( ! $post || Guest::POST_TYPE !== $post->post_type ) { return $this->formatter->not_found( __( 'Guest', 'wp-bnb' ) ); } $data = $this->prepare_guest_response( $post, true ); $response = $this->formatter->success( $data ); return $this->add_rate_limit_headers( $response, $request ); } /** * Get guest's booking history. * * @param WP_REST_Request $request Current request. * @return WP_REST_Response|WP_Error Response object or error. */ public function get_guest_bookings( $request ) { $rate_limit_error = $this->check_rate_limit( $request ); if ( $rate_limit_error ) { return $rate_limit_error; } $guest_id = $request->get_param( 'id' ); $guest = get_post( $guest_id ); if ( ! $guest || Guest::POST_TYPE !== $guest->post_type ) { return $this->formatter->not_found( __( 'Guest', 'wp-bnb' ) ); } $bookings = Guest::get_bookings( $guest_id ); $items = array(); foreach ( $bookings as $booking ) { $room_id = get_post_meta( $booking->ID, '_bnb_booking_room_id', true ); $room = $room_id ? get_post( $room_id ) : null; $check_in = get_post_meta( $booking->ID, '_bnb_booking_check_in', true ); $check_out = get_post_meta( $booking->ID, '_bnb_booking_check_out', true ); $items[] = array( 'id' => $booking->ID, 'reference' => get_post_meta( $booking->ID, '_bnb_booking_reference', true ) ?: $booking->post_title, 'room' => $room ? array( 'id' => $room->ID, 'title' => get_the_title( $room ), ) : null, 'check_in' => $check_in, 'check_out' => $check_out, 'status' => get_post_meta( $booking->ID, '_bnb_booking_status', true ), 'total' => (float) get_post_meta( $booking->ID, '_bnb_booking_total_price', true ), 'created_at' => $booking->post_date_gmt, '_links' => array( 'self' => array( array( 'href' => rest_url( $this->namespace . '/bookings/' . $booking->ID ) ), ), ), ); } $response = $this->formatter->success( $items ); return $this->add_rate_limit_headers( $response, $request ); } /** * Prepare guest data for response. * * @param \WP_Post $post Guest post object. * @param bool $full Include full details. * @return array Guest data. */ private function prepare_guest_response( \WP_Post $post, bool $full = false ): array { $data = array( 'id' => $post->ID, 'first_name' => get_post_meta( $post->ID, '_bnb_guest_first_name', true ), 'last_name' => get_post_meta( $post->ID, '_bnb_guest_last_name', true ), 'email' => get_post_meta( $post->ID, '_bnb_guest_email', true ), 'phone' => get_post_meta( $post->ID, '_bnb_guest_phone', true ), 'status' => get_post_meta( $post->ID, '_bnb_guest_status', true ) ?: 'active', 'created_at' => $post->post_date_gmt, ); // Address. $data['address'] = array( 'street' => get_post_meta( $post->ID, '_bnb_guest_street', true ), 'city' => get_post_meta( $post->ID, '_bnb_guest_city', true ), 'postal_code' => get_post_meta( $post->ID, '_bnb_guest_postal_code', true ), 'country' => get_post_meta( $post->ID, '_bnb_guest_country', true ), ); // Statistics. $booking_count = Guest::get_booking_count( $post->ID ); $total_spent = Guest::get_total_spent( $post->ID ); $data['statistics'] = array( 'total_bookings' => $booking_count, 'total_spent' => $total_spent, ); if ( $full ) { $data['nationality'] = get_post_meta( $post->ID, '_bnb_guest_nationality', true ); $data['date_of_birth'] = get_post_meta( $post->ID, '_bnb_guest_date_of_birth', true ); $data['notes'] = get_post_meta( $post->ID, '_bnb_guest_notes', true ); $data['preferences'] = get_post_meta( $post->ID, '_bnb_guest_preferences', true ); // Get last stay date. $bookings = Guest::get_bookings( $post->ID ); if ( ! empty( $bookings ) ) { $last_booking = $bookings[0]; $data['statistics']['last_stay'] = get_post_meta( $last_booking->ID, '_bnb_booking_check_out', true ); } // Formatted address. $data['address']['formatted'] = Guest::get_formatted_address( $post->ID ); // GDPR consent info. $data['consent'] = array( 'data_processing' => (bool) get_post_meta( $post->ID, '_bnb_guest_consent_data', true ), 'marketing' => (bool) get_post_meta( $post->ID, '_bnb_guest_consent_marketing', true ), 'date' => get_post_meta( $post->ID, '_bnb_guest_consent_date', true ), ); } // Note: ID/passport numbers are NOT exposed via API for security. $data['_links'] = array( 'self' => array( array( 'href' => rest_url( $this->namespace . '/guests/' . $post->ID ) ), ), 'bookings' => array( array( 'href' => rest_url( $this->namespace . '/guests/' . $post->ID . '/bookings' ) ), ), ); return $data; } /** * Prepare guest summary for search results. * * @param \WP_Post $post Guest post object. * @return array Guest summary. */ private function prepare_guest_summary( \WP_Post $post ): array { return array( 'id' => $post->ID, 'first_name' => get_post_meta( $post->ID, '_bnb_guest_first_name', true ), 'last_name' => get_post_meta( $post->ID, '_bnb_guest_last_name', true ), 'email' => get_post_meta( $post->ID, '_bnb_guest_email', true ), 'phone' => get_post_meta( $post->ID, '_bnb_guest_phone', true ), '_links' => array( 'self' => array( array( 'href' => rest_url( $this->namespace . '/guests/' . $post->ID ) ), ), ), ); } /** * Get collection parameters with status filter. * * @return array Collection parameters. */ public function get_collection_params(): array { $params = parent::get_collection_params(); $params['status'] = array( 'description' => __( 'Filter by guest status.', 'wp-bnb' ), 'type' => 'string', 'enum' => array( 'active', 'inactive', 'blocked' ), 'sanitize_callback' => 'sanitize_text_field', ); return $params; } }