Implement Phase 10: REST API Endpoints (v0.10.0)
All checks were successful
Create Release Package / build-release (push) Successful in 1m10s
All checks were successful
Create Release Package / build-release (push) Successful in 1m10s
- Add complete REST API infrastructure under src/Api/ - ResponseFormatter for standardized responses - RateLimiter with tiered limits (public 60/min, availability 30/min, booking 10/min, admin 120/min) - AbstractController base class with common functionality - BuildingsController: list, get, rooms endpoints - RoomsController: list, get, availability, calendar, search endpoints - BookingsController: CRUD + confirm/check-in/check-out status transitions - GuestsController: list, get, search, booking history (admin only) - ServicesController: list, get, calculate endpoints - PricingController: calculate, seasons endpoints - API settings tab with enable/disable toggles - Comprehensive API documentation in README Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
452
src/Api/Controllers/GuestsController.php
Normal file
452
src/Api/Controllers/GuestsController.php
Normal file
@@ -0,0 +1,452 @@
|
||||
<?php
|
||||
/**
|
||||
* Guests REST Controller
|
||||
*
|
||||
* Handles REST API endpoints for guests (admin only).
|
||||
*
|
||||
* @package Magdev\WpBnb\Api\Controllers
|
||||
*/
|
||||
|
||||
declare( strict_types=1 );
|
||||
|
||||
namespace Magdev\WpBnb\Api\Controllers;
|
||||
|
||||
use Magdev\WpBnb\PostTypes\Guest;
|
||||
use Magdev\WpBnb\PostTypes\Booking;
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Response;
|
||||
use WP_REST_Server;
|
||||
use WP_Error;
|
||||
|
||||
/**
|
||||
* Guests Controller class.
|
||||
*/
|
||||
final class GuestsController extends AbstractController {
|
||||
|
||||
/**
|
||||
* Route base.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'guests';
|
||||
|
||||
/**
|
||||
* Register routes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_routes(): void {
|
||||
// GET /guests - List guests (admin).
|
||||
register_rest_route(
|
||||
$this->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<id>[\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<id>[\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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user