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>
376 lines
11 KiB
PHP
376 lines
11 KiB
PHP
<?php
|
|
/**
|
|
* Services REST Controller
|
|
*
|
|
* Handles REST API endpoints for services.
|
|
*
|
|
* @package Magdev\WpBnb\Api\Controllers
|
|
*/
|
|
|
|
declare( strict_types=1 );
|
|
|
|
namespace Magdev\WpBnb\Api\Controllers;
|
|
|
|
use Magdev\WpBnb\PostTypes\Service;
|
|
use Magdev\WpBnb\Taxonomies\ServiceCategory;
|
|
use Magdev\WpBnb\Pricing\Calculator;
|
|
use WP_REST_Request;
|
|
use WP_REST_Response;
|
|
use WP_REST_Server;
|
|
use WP_Error;
|
|
|
|
/**
|
|
* Services Controller class.
|
|
*/
|
|
final class ServicesController extends AbstractController {
|
|
|
|
/**
|
|
* Route base.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $rest_base = 'services';
|
|
|
|
/**
|
|
* Register routes.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function register_routes(): void {
|
|
// GET /services - List active services (public).
|
|
register_rest_route(
|
|
$this->namespace,
|
|
'/' . $this->rest_base,
|
|
array(
|
|
array(
|
|
'methods' => WP_REST_Server::READABLE,
|
|
'callback' => array( $this, 'get_items' ),
|
|
'permission_callback' => array( $this, 'public_permission' ),
|
|
'args' => $this->get_services_collection_params(),
|
|
),
|
|
)
|
|
);
|
|
|
|
// GET /services/{id} - Get single service (public).
|
|
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, 'public_permission' ),
|
|
'args' => array(
|
|
'id' => array(
|
|
'description' => __( 'Service ID.', 'wp-bnb' ),
|
|
'type' => 'integer',
|
|
'required' => true,
|
|
'sanitize_callback' => 'absint',
|
|
),
|
|
),
|
|
),
|
|
)
|
|
);
|
|
|
|
// POST /services/{id}/calculate - Calculate service price.
|
|
register_rest_route(
|
|
$this->namespace,
|
|
'/' . $this->rest_base . '/(?P<id>[\d]+)/calculate',
|
|
array(
|
|
array(
|
|
'methods' => WP_REST_Server::CREATABLE,
|
|
'callback' => array( $this, 'calculate_price' ),
|
|
'permission_callback' => array( $this, 'public_permission' ),
|
|
'args' => array(
|
|
'id' => array(
|
|
'description' => __( 'Service ID.', 'wp-bnb' ),
|
|
'type' => 'integer',
|
|
'required' => true,
|
|
'sanitize_callback' => 'absint',
|
|
),
|
|
'quantity' => array(
|
|
'description' => __( 'Quantity.', 'wp-bnb' ),
|
|
'type' => 'integer',
|
|
'default' => 1,
|
|
'minimum' => 1,
|
|
'sanitize_callback' => 'absint',
|
|
),
|
|
'nights' => array(
|
|
'description' => __( 'Number of nights (for per-night services).', 'wp-bnb' ),
|
|
'type' => 'integer',
|
|
'default' => 1,
|
|
'minimum' => 1,
|
|
'sanitize_callback' => 'absint',
|
|
),
|
|
),
|
|
),
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get collection of services.
|
|
*
|
|
* @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;
|
|
}
|
|
|
|
$args = array(
|
|
'post_type' => Service::POST_TYPE,
|
|
'post_status' => 'publish',
|
|
'posts_per_page' => 100, // Services typically don't need pagination.
|
|
'orderby' => 'meta_value_num',
|
|
'meta_key' => '_bnb_service_sort_order',
|
|
'order' => 'ASC',
|
|
);
|
|
|
|
$meta_query = array();
|
|
|
|
// Status filter (default: active only).
|
|
$status = $request->get_param( 'status' ) ?: 'active';
|
|
if ( 'all' !== $status ) {
|
|
$meta_query[] = array(
|
|
'key' => '_bnb_service_status',
|
|
'value' => $status,
|
|
);
|
|
}
|
|
|
|
// Pricing type filter.
|
|
$pricing_type = $request->get_param( 'pricing_type' );
|
|
if ( $pricing_type ) {
|
|
$meta_query[] = array(
|
|
'key' => '_bnb_service_pricing_type',
|
|
'value' => $pricing_type,
|
|
);
|
|
}
|
|
|
|
if ( ! empty( $meta_query ) ) {
|
|
$meta_query['relation'] = 'AND';
|
|
$args['meta_query'] = $meta_query;
|
|
}
|
|
|
|
// Category filter.
|
|
$category = $request->get_param( 'category' );
|
|
if ( $category ) {
|
|
$args['tax_query'] = array(
|
|
array(
|
|
'taxonomy' => 'bnb_service_category',
|
|
'field' => is_numeric( $category ) ? 'term_id' : 'slug',
|
|
'terms' => $category,
|
|
),
|
|
);
|
|
}
|
|
|
|
$services = get_posts( $args );
|
|
$items = array();
|
|
|
|
foreach ( $services as $service ) {
|
|
$items[] = $this->prepare_service_response( $service );
|
|
}
|
|
|
|
$response = $this->formatter->success( $items );
|
|
|
|
return $this->add_rate_limit_headers( $response, $request );
|
|
}
|
|
|
|
/**
|
|
* Get single service.
|
|
*
|
|
* @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 || Service::POST_TYPE !== $post->post_type || 'publish' !== $post->post_status ) {
|
|
return $this->formatter->not_found( __( 'Service', 'wp-bnb' ) );
|
|
}
|
|
|
|
$data = $this->prepare_service_response( $post, true );
|
|
$response = $this->formatter->success( $data );
|
|
|
|
return $this->add_rate_limit_headers( $response, $request );
|
|
}
|
|
|
|
/**
|
|
* Calculate service price.
|
|
*
|
|
* @param WP_REST_Request $request Current request.
|
|
* @return WP_REST_Response|WP_Error Response object or error.
|
|
*/
|
|
public function calculate_price( $request ) {
|
|
$rate_limit_error = $this->check_rate_limit( $request );
|
|
if ( $rate_limit_error ) {
|
|
return $rate_limit_error;
|
|
}
|
|
|
|
$service_id = $request->get_param( 'id' );
|
|
$quantity = $request->get_param( 'quantity' );
|
|
$nights = $request->get_param( 'nights' );
|
|
|
|
// Validate service.
|
|
$service = get_post( $service_id );
|
|
if ( ! $service || Service::POST_TYPE !== $service->post_type || 'publish' !== $service->post_status ) {
|
|
return $this->formatter->not_found( __( 'Service', 'wp-bnb' ) );
|
|
}
|
|
|
|
// Check if service is active.
|
|
$status = get_post_meta( $service_id, '_bnb_service_status', true );
|
|
if ( 'active' !== $status ) {
|
|
return $this->formatter->validation_error( 'id', __( 'Service is not available.', 'wp-bnb' ) );
|
|
}
|
|
|
|
// Check max quantity.
|
|
$max_quantity = (int) get_post_meta( $service_id, '_bnb_service_max_quantity', true ) ?: 1;
|
|
if ( $quantity > $max_quantity ) {
|
|
return $this->formatter->validation_error(
|
|
'quantity',
|
|
sprintf(
|
|
/* translators: %d: maximum quantity */
|
|
__( 'Maximum quantity is %d.', 'wp-bnb' ),
|
|
$max_quantity
|
|
)
|
|
);
|
|
}
|
|
|
|
// Calculate price.
|
|
$total = Service::calculate_service_price( $service_id, $quantity, $nights );
|
|
$pricing_type = get_post_meta( $service_id, '_bnb_service_pricing_type', true );
|
|
$unit_price = (float) get_post_meta( $service_id, '_bnb_service_price', true );
|
|
$currency = get_option( 'wp_bnb_currency', 'CHF' );
|
|
|
|
// Build calculation string.
|
|
$calculation = '';
|
|
switch ( $pricing_type ) {
|
|
case 'included':
|
|
$calculation = __( 'Included', 'wp-bnb' );
|
|
break;
|
|
case 'per_booking':
|
|
$calculation = sprintf(
|
|
'%s x %d',
|
|
Calculator::formatPrice( $unit_price ),
|
|
$quantity
|
|
);
|
|
break;
|
|
case 'per_night':
|
|
$calculation = sprintf(
|
|
'%s x %d x %d %s',
|
|
Calculator::formatPrice( $unit_price ),
|
|
$quantity,
|
|
$nights,
|
|
_n( 'night', 'nights', $nights, 'wp-bnb' )
|
|
);
|
|
break;
|
|
}
|
|
|
|
$data = array(
|
|
'service_id' => $service_id,
|
|
'quantity' => $quantity,
|
|
'nights' => $nights,
|
|
'unit_price' => $unit_price,
|
|
'total' => $total,
|
|
'formatted' => Calculator::formatPrice( $total ),
|
|
'currency' => $currency,
|
|
'calculation' => $calculation,
|
|
);
|
|
|
|
$response = $this->formatter->success( $data );
|
|
|
|
return $this->add_rate_limit_headers( $response, $request );
|
|
}
|
|
|
|
/**
|
|
* Prepare service data for response.
|
|
*
|
|
* @param \WP_Post $post Service post object.
|
|
* @param bool $full Include full details.
|
|
* @return array Service data.
|
|
*/
|
|
private function prepare_service_response( \WP_Post $post, bool $full = false ): array {
|
|
$pricing_type = get_post_meta( $post->ID, '_bnb_service_pricing_type', true );
|
|
$price = (float) get_post_meta( $post->ID, '_bnb_service_price', true );
|
|
$status = get_post_meta( $post->ID, '_bnb_service_status', true ) ?: 'active';
|
|
$max_quantity = (int) get_post_meta( $post->ID, '_bnb_service_max_quantity', true ) ?: 1;
|
|
$currency = get_option( 'wp_bnb_currency', 'CHF' );
|
|
|
|
$data = array(
|
|
'id' => $post->ID,
|
|
'title' => get_the_title( $post ),
|
|
'slug' => $post->post_name,
|
|
'description' => get_the_excerpt( $post ),
|
|
'pricing' => array(
|
|
'type' => $pricing_type,
|
|
'price' => $price,
|
|
'formatted' => Service::format_service_price( Service::get_service_data( $post->ID ) ),
|
|
'currency' => $currency,
|
|
),
|
|
'max_quantity' => $max_quantity,
|
|
'status' => $status,
|
|
);
|
|
|
|
// Category.
|
|
$categories = wp_get_post_terms( $post->ID, 'bnb_service_category' );
|
|
if ( ! empty( $categories ) && ! is_wp_error( $categories ) ) {
|
|
$category = $categories[0];
|
|
$data['category'] = array(
|
|
'id' => $category->term_id,
|
|
'name' => $category->name,
|
|
'slug' => $category->slug,
|
|
'icon' => get_term_meta( $category->term_id, '_bnb_service_category_icon', true ),
|
|
);
|
|
}
|
|
|
|
if ( $full ) {
|
|
$data['content'] = apply_filters( 'the_content', $post->post_content );
|
|
$data['sort_order'] = (int) get_post_meta( $post->ID, '_bnb_service_sort_order', true );
|
|
}
|
|
|
|
$data['_links'] = array(
|
|
'self' => array(
|
|
array( 'href' => rest_url( $this->namespace . '/services/' . $post->ID ) ),
|
|
),
|
|
);
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Get services collection parameters.
|
|
*
|
|
* @return array Collection parameters.
|
|
*/
|
|
private function get_services_collection_params(): array {
|
|
return array(
|
|
'status' => array(
|
|
'description' => __( 'Filter by status (default: active).', 'wp-bnb' ),
|
|
'type' => 'string',
|
|
'enum' => array( 'active', 'inactive', 'all' ),
|
|
'default' => 'active',
|
|
'sanitize_callback' => 'sanitize_text_field',
|
|
),
|
|
'pricing_type' => array(
|
|
'description' => __( 'Filter by pricing type.', 'wp-bnb' ),
|
|
'type' => 'string',
|
|
'enum' => array( 'included', 'per_booking', 'per_night' ),
|
|
'sanitize_callback' => 'sanitize_text_field',
|
|
),
|
|
'category' => array(
|
|
'description' => __( 'Filter by category (term ID or slug).', 'wp-bnb' ),
|
|
'type' => 'string',
|
|
'sanitize_callback' => 'sanitize_text_field',
|
|
),
|
|
);
|
|
}
|
|
}
|