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:
61
CHANGELOG.md
61
CHANGELOG.md
@@ -5,6 +5,67 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.10.0] - 2026-02-03
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- REST API Infrastructure:
|
||||||
|
- New `src/Api/` directory with complete REST API implementation
|
||||||
|
- `ResponseFormatter.php` - Standardized response formatting (success, collection, error responses)
|
||||||
|
- `RateLimiter.php` - Transient-based rate limiting with tiered limits
|
||||||
|
- `Controllers/AbstractController.php` - Base controller with common functionality
|
||||||
|
- `RestApi.php` - Main registration class with namespace `wp-bnb/v1`
|
||||||
|
- Buildings API:
|
||||||
|
- `GET /wp-bnb/v1/buildings` - List buildings with pagination and search
|
||||||
|
- `GET /wp-bnb/v1/buildings/{id}` - Get single building with address, contact, details
|
||||||
|
- `GET /wp-bnb/v1/buildings/{id}/rooms` - Get rooms in a building with status filter
|
||||||
|
- Rooms API:
|
||||||
|
- `GET /wp-bnb/v1/rooms` - List rooms with filters (building, room_type, amenities, capacity, status)
|
||||||
|
- `GET /wp-bnb/v1/rooms/{id}` - Get room details with gallery, pricing, amenities
|
||||||
|
- `GET /wp-bnb/v1/rooms/{id}/availability` - Check availability with price calculation
|
||||||
|
- `GET /wp-bnb/v1/rooms/{id}/calendar` - Get monthly calendar data
|
||||||
|
- `POST /wp-bnb/v1/availability/search` - Search available rooms by date range and criteria
|
||||||
|
- Bookings API:
|
||||||
|
- `POST /wp-bnb/v1/bookings` - Create booking (public, creates pending status)
|
||||||
|
- `GET /wp-bnb/v1/bookings` - List bookings with filters (admin)
|
||||||
|
- `GET /wp-bnb/v1/bookings/{id}` - Get booking details (admin)
|
||||||
|
- `PATCH /wp-bnb/v1/bookings/{id}` - Update booking (admin)
|
||||||
|
- `DELETE /wp-bnb/v1/bookings/{id}` - Cancel booking (admin)
|
||||||
|
- `POST /wp-bnb/v1/bookings/{id}/confirm` - Confirm pending booking (admin)
|
||||||
|
- `POST /wp-bnb/v1/bookings/{id}/check-in` - Check in guest (admin)
|
||||||
|
- `POST /wp-bnb/v1/bookings/{id}/check-out` - Check out guest (admin)
|
||||||
|
- Guests API (admin only):
|
||||||
|
- `GET /wp-bnb/v1/guests` - List guests with pagination
|
||||||
|
- `GET /wp-bnb/v1/guests/{id}` - Get guest details (excludes encrypted ID numbers)
|
||||||
|
- `GET /wp-bnb/v1/guests/search` - Search guests by name/email
|
||||||
|
- `GET /wp-bnb/v1/guests/{id}/bookings` - Get guest's booking history
|
||||||
|
- Services API:
|
||||||
|
- `GET /wp-bnb/v1/services` - List active services with categories
|
||||||
|
- `GET /wp-bnb/v1/services/{id}` - Get service details with pricing info
|
||||||
|
- `POST /wp-bnb/v1/services/{id}/calculate` - Calculate service price for booking
|
||||||
|
- Pricing API:
|
||||||
|
- `POST /wp-bnb/v1/pricing/calculate` - Full price calculation with services
|
||||||
|
- `GET /wp-bnb/v1/pricing/seasons` - Get configured seasons and pricing modifiers
|
||||||
|
- API Settings tab in plugin settings:
|
||||||
|
- Enable/disable REST API toggle
|
||||||
|
- Enable/disable rate limiting toggle
|
||||||
|
- Endpoint documentation table
|
||||||
|
- Authentication instructions
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Plugin.php updated to initialize REST API on `rest_api_init` hook
|
||||||
|
- Settings page now has seven tabs: General, Pricing, License, Updates, Metrics, API
|
||||||
|
- README.md updated with comprehensive REST API documentation
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Rate limiting: public (60/min), availability (30/min), booking (10/min), admin (120/min)
|
||||||
|
- Admin endpoints require `edit_posts` capability
|
||||||
|
- Supports WordPress Application Passwords for external API access
|
||||||
|
- Client identification by user ID (authenticated) or IP address (anonymous)
|
||||||
|
- Proxy/Cloudflare IP detection via X-Forwarded-For and CF-Connecting-IP headers
|
||||||
|
|
||||||
## [0.9.0] - 2026-02-03
|
## [0.9.0] - 2026-02-03
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
14
PLAN.md
14
PLAN.md
@@ -194,17 +194,21 @@ This document outlines the implementation plan for the WP BnB Management plugin.
|
|||||||
- 24 panels with gauges, pie charts, and stat displays
|
- 24 panels with gauges, pie charts, and stat displays
|
||||||
- [x] Update settings page to enable/disable metrics
|
- [x] Update settings page to enable/disable metrics
|
||||||
|
|
||||||
### Phase 10: API Endpoints (v0.10.0)
|
### Phase 10: API Endpoints (v0.10.0) - Complete
|
||||||
|
|
||||||
- [ ] REST API for rooms
|
- [x] REST API for rooms (list, details, availability, calendar)
|
||||||
- [ ] REST API for availability
|
- [x] REST API for availability (search available rooms)
|
||||||
- [ ] REST API for bookings
|
- [x] REST API for bookings (CRUD, status transitions)
|
||||||
- [ ] Authentication and rate limiting
|
- [x] REST API for buildings, guests, services, pricing
|
||||||
|
- [x] Authentication (Application Passwords, edit_posts capability)
|
||||||
|
- [x] Transient-based rate limiting with tiered limits
|
||||||
|
- [x] API settings tab with enable/disable toggles
|
||||||
|
|
||||||
## Phase 11: Security Audit (v0.11.0)
|
## Phase 11: Security Audit (v0.11.0)
|
||||||
|
|
||||||
- [ ] Check for Wordpress best-practices
|
- [ ] Check for Wordpress best-practices
|
||||||
- [ ] Review the code for OWASP Top 10, including XSS, XSRF, SQLi and other critical threads
|
- [ ] Review the code for OWASP Top 10, including XSS, XSRF, SQLi and other critical threads
|
||||||
|
- [ ] Test the API-Endpoints against a local live system under <http://localhost:9080/> for common vulnerabilities
|
||||||
|
|
||||||
## Future Considerations (v1.0.0+)
|
## Future Considerations (v1.0.0+)
|
||||||
|
|
||||||
|
|||||||
198
README.md
198
README.md
@@ -444,6 +444,204 @@ The dashboard includes:
|
|||||||
- Today's check-ins/check-outs
|
- Today's check-ins/check-outs
|
||||||
- Trend indicators
|
- Trend indicators
|
||||||
|
|
||||||
|
## REST API
|
||||||
|
|
||||||
|
The plugin provides a comprehensive REST API for integration with external applications, mobile apps, and third-party services.
|
||||||
|
|
||||||
|
### Enabling the API
|
||||||
|
|
||||||
|
1. Navigate to **WP BnB → Settings → API**
|
||||||
|
2. Enable "Enable REST API"
|
||||||
|
3. Optionally enable rate limiting for protection against abuse
|
||||||
|
|
||||||
|
### Base URL
|
||||||
|
|
||||||
|
All API endpoints are prefixed with:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
https://your-site.com/wp-json/wp-bnb/v1/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
**Public endpoints** (room listings, availability checks) require no authentication.
|
||||||
|
|
||||||
|
**Admin endpoints** (booking management, guest data) require authentication via:
|
||||||
|
|
||||||
|
- **Cookie + Nonce**: For same-domain JavaScript requests
|
||||||
|
- **Application Passwords**: For external applications (WordPress 5.6+, recommended)
|
||||||
|
|
||||||
|
To create an Application Password:
|
||||||
|
|
||||||
|
1. Go to **Users → Profile**
|
||||||
|
2. Scroll to "Application Passwords"
|
||||||
|
3. Enter a name and click "Add New Application Password"
|
||||||
|
4. Use the generated password with HTTP Basic Auth
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -u "username:app-password" https://site.com/wp-json/wp-bnb/v1/bookings
|
||||||
|
```
|
||||||
|
|
||||||
|
### Public Endpoints
|
||||||
|
|
||||||
|
| Method | Endpoint | Description |
|
||||||
|
| ------ | -------- | ----------- |
|
||||||
|
| GET | `/buildings` | List all buildings |
|
||||||
|
| GET | `/buildings/{id}` | Get building details |
|
||||||
|
| GET | `/buildings/{id}/rooms` | Get rooms in a building |
|
||||||
|
| GET | `/rooms` | List/search rooms |
|
||||||
|
| GET | `/rooms/{id}` | Get room details |
|
||||||
|
| GET | `/rooms/{id}/availability` | Check room availability |
|
||||||
|
| GET | `/rooms/{id}/calendar` | Get monthly calendar data |
|
||||||
|
| POST | `/availability/search` | Search available rooms |
|
||||||
|
| GET | `/services` | List all services |
|
||||||
|
| GET | `/services/{id}` | Get service details |
|
||||||
|
| POST | `/pricing/calculate` | Calculate booking price |
|
||||||
|
| POST | `/bookings` | Create a new booking (pending status) |
|
||||||
|
|
||||||
|
### Admin Endpoints
|
||||||
|
|
||||||
|
| Method | Endpoint | Description |
|
||||||
|
| ------ | -------- | ----------- |
|
||||||
|
| GET | `/bookings` | List all bookings |
|
||||||
|
| GET | `/bookings/{id}` | Get booking details |
|
||||||
|
| PATCH | `/bookings/{id}` | Update a booking |
|
||||||
|
| DELETE | `/bookings/{id}` | Cancel a booking |
|
||||||
|
| POST | `/bookings/{id}/confirm` | Confirm a pending booking |
|
||||||
|
| POST | `/bookings/{id}/check-in` | Check in a guest |
|
||||||
|
| POST | `/bookings/{id}/check-out` | Check out a guest |
|
||||||
|
| GET | `/guests` | List all guests |
|
||||||
|
| GET | `/guests/{id}` | Get guest details |
|
||||||
|
| GET | `/guests/search` | Search guests |
|
||||||
|
| GET | `/guests/{id}/bookings` | Get guest's booking history |
|
||||||
|
|
||||||
|
### Rate Limiting
|
||||||
|
|
||||||
|
When enabled, rate limits are applied per client (by user ID or IP address):
|
||||||
|
|
||||||
|
| Type | Limit | Applies To |
|
||||||
|
| ---- | ----- | ---------- |
|
||||||
|
| Public | 60/min | Room/building listings |
|
||||||
|
| Availability | 30/min | Availability and calendar endpoints |
|
||||||
|
| Booking | 10/min | Booking creation |
|
||||||
|
| Admin | 120/min | All admin endpoints |
|
||||||
|
|
||||||
|
Rate limit headers are included in responses:
|
||||||
|
|
||||||
|
- `X-RateLimit-Limit`: Maximum requests allowed
|
||||||
|
- `X-RateLimit-Remaining`: Requests remaining in window
|
||||||
|
- `X-RateLimit-Reset`: Unix timestamp when limit resets
|
||||||
|
|
||||||
|
### Example: Check Room Availability
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl "https://site.com/wp-json/wp-bnb/v1/rooms/42/availability?check_in=2026-03-15&check_out=2026-03-20"
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"available": true,
|
||||||
|
"room_id": 42,
|
||||||
|
"check_in": "2026-03-15",
|
||||||
|
"check_out": "2026-03-20",
|
||||||
|
"nights": 5,
|
||||||
|
"pricing": {
|
||||||
|
"base_price": 500.00,
|
||||||
|
"seasonal_modifier": 1.0,
|
||||||
|
"weekend_surcharge": 40.00,
|
||||||
|
"total": 540.00,
|
||||||
|
"currency": "CHF"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example: Create a Booking
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST https://site.com/wp-json/wp-bnb/v1/bookings \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"room_id": 42,
|
||||||
|
"check_in": "2026-03-15",
|
||||||
|
"check_out": "2026-03-20",
|
||||||
|
"guests": 2,
|
||||||
|
"guest_info": {
|
||||||
|
"first_name": "John",
|
||||||
|
"last_name": "Doe",
|
||||||
|
"email": "john@example.com",
|
||||||
|
"phone": "+41 79 123 4567"
|
||||||
|
},
|
||||||
|
"services": [
|
||||||
|
{"service_id": 5, "quantity": 1}
|
||||||
|
],
|
||||||
|
"notes": "Late arrival expected"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 123,
|
||||||
|
"reference": "BNB-2026-00042",
|
||||||
|
"status": "pending",
|
||||||
|
"room": {
|
||||||
|
"id": 42,
|
||||||
|
"title": "Deluxe Suite"
|
||||||
|
},
|
||||||
|
"check_in": "2026-03-15",
|
||||||
|
"check_out": "2026-03-20",
|
||||||
|
"nights": 5,
|
||||||
|
"guests": 2,
|
||||||
|
"pricing": {
|
||||||
|
"room_total": 540.00,
|
||||||
|
"services_total": 50.00,
|
||||||
|
"grand_total": 590.00,
|
||||||
|
"currency": "CHF"
|
||||||
|
},
|
||||||
|
"_links": {
|
||||||
|
"self": [{"href": "https://site.com/wp-json/wp-bnb/v1/bookings/123"}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example: Search Available Rooms
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST https://site.com/wp-json/wp-bnb/v1/availability/search \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"check_in": "2026-03-15",
|
||||||
|
"check_out": "2026-03-20",
|
||||||
|
"guests": 2,
|
||||||
|
"amenities": ["wifi", "parking"]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Responses
|
||||||
|
|
||||||
|
Errors follow WordPress REST API conventions:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "rest_not_found",
|
||||||
|
"message": "Room not found.",
|
||||||
|
"data": {
|
||||||
|
"status": 404
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Common error codes:
|
||||||
|
|
||||||
|
- `rest_invalid_param` (400): Invalid request parameters
|
||||||
|
- `rest_forbidden` (403): Insufficient permissions
|
||||||
|
- `rest_not_found` (404): Resource not found
|
||||||
|
- `rest_conflict` (409): Booking conflict
|
||||||
|
- `rest_rate_limit_exceeded` (429): Rate limit exceeded
|
||||||
|
|
||||||
## Frequently Asked Questions
|
## Frequently Asked Questions
|
||||||
|
|
||||||
### Do I need a license to use this plugin?
|
### Do I need a license to use this plugin?
|
||||||
|
|||||||
382
src/Api/Controllers/AbstractController.php
Normal file
382
src/Api/Controllers/AbstractController.php
Normal file
@@ -0,0 +1,382 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Abstract REST Controller
|
||||||
|
*
|
||||||
|
* Base class for all REST API controllers with common functionality.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Api\Controllers
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Api\Controllers;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\Api\RestApi;
|
||||||
|
use Magdev\WpBnb\Api\RateLimiter;
|
||||||
|
use Magdev\WpBnb\Api\ResponseFormatter;
|
||||||
|
use WP_REST_Controller;
|
||||||
|
use WP_REST_Request;
|
||||||
|
use WP_REST_Response;
|
||||||
|
use WP_Error;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract Controller class.
|
||||||
|
*/
|
||||||
|
abstract class AbstractController extends WP_REST_Controller {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API namespace.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $namespace = RestApi::NAMESPACE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rate limiter instance.
|
||||||
|
*
|
||||||
|
* @var RateLimiter
|
||||||
|
*/
|
||||||
|
protected RateLimiter $rate_limiter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response formatter instance.
|
||||||
|
*
|
||||||
|
* @var ResponseFormatter
|
||||||
|
*/
|
||||||
|
protected ResponseFormatter $formatter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
$this->rate_limiter = new RateLimiter();
|
||||||
|
$this->formatter = new ResponseFormatter();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check rate limit before processing request.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_Error|null Error if rate limited, null otherwise.
|
||||||
|
*/
|
||||||
|
protected function check_rate_limit( WP_REST_Request $request ): ?WP_Error {
|
||||||
|
// Skip rate limiting if disabled.
|
||||||
|
if ( 'yes' !== get_option( 'wp_bnb_api_rate_limiting', 'yes' ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$identifier = $this->get_client_identifier( $request );
|
||||||
|
$endpoint = $request->get_route();
|
||||||
|
|
||||||
|
if ( ! $this->rate_limiter->check( $identifier, $endpoint ) ) {
|
||||||
|
return $this->formatter->rate_limit_error(
|
||||||
|
$this->rate_limiter->get_retry_after( $identifier, $endpoint )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add rate limit headers to response.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Response $response Current response.
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response Response with headers.
|
||||||
|
*/
|
||||||
|
protected function add_rate_limit_headers( WP_REST_Response $response, WP_REST_Request $request ): WP_REST_Response {
|
||||||
|
if ( 'yes' !== get_option( 'wp_bnb_api_rate_limiting', 'yes' ) ) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
$identifier = $this->get_client_identifier( $request );
|
||||||
|
$endpoint = $request->get_route();
|
||||||
|
$info = $this->rate_limiter->get_rate_limit_info( $identifier, $endpoint );
|
||||||
|
|
||||||
|
$response->header( 'X-RateLimit-Limit', (string) $info['limit'] );
|
||||||
|
$response->header( 'X-RateLimit-Remaining', (string) $info['remaining'] );
|
||||||
|
$response->header( 'X-RateLimit-Reset', (string) $info['reset'] );
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get client identifier for rate limiting.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return string Client identifier.
|
||||||
|
*/
|
||||||
|
protected function get_client_identifier( WP_REST_Request $request ): string {
|
||||||
|
// Use user ID if authenticated.
|
||||||
|
$user_id = get_current_user_id();
|
||||||
|
if ( $user_id > 0 ) {
|
||||||
|
return 'user_' . $user_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'ip_' . $this->get_client_ip();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get client IP address.
|
||||||
|
*
|
||||||
|
* Supports proxies and Cloudflare.
|
||||||
|
*
|
||||||
|
* @return string Client IP address.
|
||||||
|
*/
|
||||||
|
protected function get_client_ip(): string {
|
||||||
|
$headers = array(
|
||||||
|
'HTTP_CF_CONNECTING_IP', // Cloudflare.
|
||||||
|
'HTTP_X_FORWARDED_FOR', // Proxy.
|
||||||
|
'HTTP_X_REAL_IP', // Nginx.
|
||||||
|
'REMOTE_ADDR',
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ( $headers as $header ) {
|
||||||
|
if ( ! empty( $_SERVER[ $header ] ) ) {
|
||||||
|
$ip = sanitize_text_field( wp_unslash( $_SERVER[ $header ] ) );
|
||||||
|
// Handle comma-separated list (X-Forwarded-For).
|
||||||
|
if ( str_contains( $ip, ',' ) ) {
|
||||||
|
$ip = trim( explode( ',', $ip )[0] );
|
||||||
|
}
|
||||||
|
return $ip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '127.0.0.1';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate date format (Y-m-d).
|
||||||
|
*
|
||||||
|
* @param string $date Date string.
|
||||||
|
* @return bool True if valid.
|
||||||
|
*/
|
||||||
|
protected function validate_date( string $date ): bool {
|
||||||
|
$d = \DateTimeImmutable::createFromFormat( 'Y-m-d', $date );
|
||||||
|
return $d && $d->format( 'Y-m-d' ) === $date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate date is not in the past.
|
||||||
|
*
|
||||||
|
* @param string $date Date string (Y-m-d).
|
||||||
|
* @return bool True if date is today or future.
|
||||||
|
*/
|
||||||
|
protected function validate_future_date( string $date ): bool {
|
||||||
|
if ( ! $this->validate_date( $date ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$date_obj = \DateTimeImmutable::createFromFormat( 'Y-m-d', $date );
|
||||||
|
$today = new \DateTimeImmutable( 'today' );
|
||||||
|
return $date_obj >= $today;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Permission callback for public endpoints.
|
||||||
|
*
|
||||||
|
* @return bool Always true.
|
||||||
|
*/
|
||||||
|
public function public_permission(): bool {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Permission callback for authenticated endpoints.
|
||||||
|
*
|
||||||
|
* @return bool True if logged in.
|
||||||
|
*/
|
||||||
|
public function authenticated_permission(): bool {
|
||||||
|
return is_user_logged_in();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Permission callback for admin endpoints.
|
||||||
|
*
|
||||||
|
* @return bool True if user can edit posts.
|
||||||
|
*/
|
||||||
|
public function admin_permission(): bool {
|
||||||
|
return current_user_can( 'edit_posts' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Permission callback for managing bookings.
|
||||||
|
*
|
||||||
|
* @return bool True if user can edit posts.
|
||||||
|
*/
|
||||||
|
public function manage_bookings_permission(): bool {
|
||||||
|
return current_user_can( 'edit_posts' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get pagination parameters from request.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return array{page: int, per_page: int, offset: int}
|
||||||
|
*/
|
||||||
|
protected function get_pagination_params( WP_REST_Request $request ): array {
|
||||||
|
$page = max( 1, (int) $request->get_param( 'page' ) ?: 1 );
|
||||||
|
$per_page = min( 100, max( 1, (int) $request->get_param( 'per_page' ) ?: 10 ) );
|
||||||
|
$offset = ( $page - 1 ) * $per_page;
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'page' => $page,
|
||||||
|
'per_page' => $per_page,
|
||||||
|
'offset' => $offset,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get sorting parameters from request.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @param array $allowed_orderby Allowed orderby values.
|
||||||
|
* @param string $default_orderby Default orderby value.
|
||||||
|
* @return array{orderby: string, order: string}
|
||||||
|
*/
|
||||||
|
protected function get_sorting_params( WP_REST_Request $request, array $allowed_orderby = array( 'title', 'date' ), string $default_orderby = 'title' ): array {
|
||||||
|
$orderby = $request->get_param( 'orderby' ) ?: $default_orderby;
|
||||||
|
$order = strtoupper( $request->get_param( 'order' ) ?: 'ASC' );
|
||||||
|
|
||||||
|
// Validate orderby.
|
||||||
|
if ( ! in_array( $orderby, $allowed_orderby, true ) ) {
|
||||||
|
$orderby = $default_orderby;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate order.
|
||||||
|
if ( ! in_array( $order, array( 'ASC', 'DESC' ), true ) ) {
|
||||||
|
$order = 'ASC';
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'orderby' => $orderby,
|
||||||
|
'order' => $order,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format post for API response.
|
||||||
|
*
|
||||||
|
* @param \WP_Post $post Post object.
|
||||||
|
* @return array Basic post data.
|
||||||
|
*/
|
||||||
|
protected function format_post_base( \WP_Post $post ): array {
|
||||||
|
return array(
|
||||||
|
'id' => $post->ID,
|
||||||
|
'title' => get_the_title( $post ),
|
||||||
|
'slug' => $post->post_name,
|
||||||
|
'excerpt' => get_the_excerpt( $post ),
|
||||||
|
'content' => apply_filters( 'the_content', $post->post_content ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format featured image for API response.
|
||||||
|
*
|
||||||
|
* @param int $post_id Post ID.
|
||||||
|
* @return array|null Image data or null.
|
||||||
|
*/
|
||||||
|
protected function format_featured_image( int $post_id ): ?array {
|
||||||
|
$thumbnail_id = get_post_thumbnail_id( $post_id );
|
||||||
|
if ( ! $thumbnail_id ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->format_image( $thumbnail_id );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format image attachment for API response.
|
||||||
|
*
|
||||||
|
* @param int $attachment_id Attachment ID.
|
||||||
|
* @return array|null Image data or null.
|
||||||
|
*/
|
||||||
|
protected function format_image( int $attachment_id ): ?array {
|
||||||
|
$full = wp_get_attachment_image_src( $attachment_id, 'full' );
|
||||||
|
if ( ! $full ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sizes = array();
|
||||||
|
foreach ( array( 'thumbnail', 'medium', 'large' ) as $size ) {
|
||||||
|
$src = wp_get_attachment_image_src( $attachment_id, $size );
|
||||||
|
if ( $src ) {
|
||||||
|
$sizes[ $size ] = array(
|
||||||
|
'url' => $src[0],
|
||||||
|
'width' => $src[1],
|
||||||
|
'height' => $src[2],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'id' => $attachment_id,
|
||||||
|
'url' => $full[0],
|
||||||
|
'width' => $full[1],
|
||||||
|
'height' => $full[2],
|
||||||
|
'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ),
|
||||||
|
'sizes' => $sizes,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add HATEOAS links to response item.
|
||||||
|
*
|
||||||
|
* @param array $item Response item.
|
||||||
|
* @param string $route Base route for self link.
|
||||||
|
* @param int $id Item ID.
|
||||||
|
* @return array Item with _links.
|
||||||
|
*/
|
||||||
|
protected function add_links( array $item, string $route, int $id ): array {
|
||||||
|
$item['_links'] = array(
|
||||||
|
'self' => array(
|
||||||
|
array(
|
||||||
|
'href' => rest_url( $this->namespace . '/' . $route . '/' . $id ),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return $item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get common collection parameters for schema.
|
||||||
|
*
|
||||||
|
* @return array Collection parameters.
|
||||||
|
*/
|
||||||
|
public function get_collection_params(): array {
|
||||||
|
return array(
|
||||||
|
'page' => array(
|
||||||
|
'description' => __( 'Current page of the collection.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'default' => 1,
|
||||||
|
'minimum' => 1,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
'per_page' => array(
|
||||||
|
'description' => __( 'Maximum number of items to be returned per page.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'default' => 10,
|
||||||
|
'minimum' => 1,
|
||||||
|
'maximum' => 100,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
'search' => array(
|
||||||
|
'description' => __( 'Limit results to those matching a string.', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
),
|
||||||
|
'orderby' => array(
|
||||||
|
'description' => __( 'Sort collection by attribute.', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'default' => 'title',
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
),
|
||||||
|
'order' => array(
|
||||||
|
'description' => __( 'Order sort attribute ascending or descending.', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'default' => 'asc',
|
||||||
|
'enum' => array( 'asc', 'desc' ),
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
930
src/Api/Controllers/BookingsController.php
Normal file
930
src/Api/Controllers/BookingsController.php
Normal file
@@ -0,0 +1,930 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Bookings REST Controller
|
||||||
|
*
|
||||||
|
* Handles REST API endpoints for bookings.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Api\Controllers
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Api\Controllers;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\PostTypes\Booking;
|
||||||
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
use Magdev\WpBnb\PostTypes\Guest;
|
||||||
|
use Magdev\WpBnb\PostTypes\Service;
|
||||||
|
use Magdev\WpBnb\Booking\Availability;
|
||||||
|
use Magdev\WpBnb\Booking\EmailNotifier;
|
||||||
|
use Magdev\WpBnb\Pricing\Calculator;
|
||||||
|
use WP_REST_Request;
|
||||||
|
use WP_REST_Response;
|
||||||
|
use WP_REST_Server;
|
||||||
|
use WP_Error;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bookings Controller class.
|
||||||
|
*/
|
||||||
|
final class BookingsController extends AbstractController {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route base.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $rest_base = 'bookings';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register routes.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function register_routes(): void {
|
||||||
|
// GET /bookings - List bookings (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_bookings_collection_params(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// POST /bookings - Create booking (public).
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base,
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::CREATABLE,
|
||||||
|
'callback' => array( $this, 'create_item' ),
|
||||||
|
'permission_callback' => array( $this, 'public_permission' ),
|
||||||
|
'args' => $this->get_create_booking_params(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// GET /bookings/{id} - Get single booking.
|
||||||
|
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' => __( 'Booking ID.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// PATCH /bookings/{id} - Update booking (admin).
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base . '/(?P<id>[\d]+)',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::EDITABLE,
|
||||||
|
'callback' => array( $this, 'update_item' ),
|
||||||
|
'permission_callback' => array( $this, 'admin_permission' ),
|
||||||
|
'args' => $this->get_update_booking_params(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// DELETE /bookings/{id} - Cancel booking (admin).
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base . '/(?P<id>[\d]+)',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::DELETABLE,
|
||||||
|
'callback' => array( $this, 'delete_item' ),
|
||||||
|
'permission_callback' => array( $this, 'admin_permission' ),
|
||||||
|
'args' => array(
|
||||||
|
'id' => array(
|
||||||
|
'description' => __( 'Booking ID.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// POST /bookings/{id}/confirm - Confirm booking.
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base . '/(?P<id>[\d]+)/confirm',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::CREATABLE,
|
||||||
|
'callback' => array( $this, 'confirm_booking' ),
|
||||||
|
'permission_callback' => array( $this, 'admin_permission' ),
|
||||||
|
'args' => array(
|
||||||
|
'id' => array(
|
||||||
|
'description' => __( 'Booking ID.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// POST /bookings/{id}/check-in - Check in guest.
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base . '/(?P<id>[\d]+)/check-in',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::CREATABLE,
|
||||||
|
'callback' => array( $this, 'check_in_booking' ),
|
||||||
|
'permission_callback' => array( $this, 'admin_permission' ),
|
||||||
|
'args' => array(
|
||||||
|
'id' => array(
|
||||||
|
'description' => __( 'Booking ID.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// POST /bookings/{id}/check-out - Check out guest.
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base . '/(?P<id>[\d]+)/check-out',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::CREATABLE,
|
||||||
|
'callback' => array( $this, 'check_out_booking' ),
|
||||||
|
'permission_callback' => array( $this, 'admin_permission' ),
|
||||||
|
'args' => array(
|
||||||
|
'id' => array(
|
||||||
|
'description' => __( 'Booking ID.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get collection of bookings.
|
||||||
|
*
|
||||||
|
* @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 );
|
||||||
|
|
||||||
|
$args = array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => $pagination['per_page'],
|
||||||
|
'offset' => $pagination['offset'],
|
||||||
|
'orderby' => 'date',
|
||||||
|
'order' => 'DESC',
|
||||||
|
);
|
||||||
|
|
||||||
|
$meta_query = array();
|
||||||
|
|
||||||
|
// Status filter.
|
||||||
|
$status = $request->get_param( 'status' );
|
||||||
|
if ( $status ) {
|
||||||
|
$meta_query[] = array(
|
||||||
|
'key' => '_bnb_booking_status',
|
||||||
|
'value' => $status,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Room filter.
|
||||||
|
$room_id = $request->get_param( 'room_id' );
|
||||||
|
if ( $room_id ) {
|
||||||
|
$meta_query[] = array(
|
||||||
|
'key' => '_bnb_booking_room_id',
|
||||||
|
'value' => $room_id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guest filter.
|
||||||
|
$guest_id = $request->get_param( 'guest_id' );
|
||||||
|
if ( $guest_id ) {
|
||||||
|
$meta_query[] = array(
|
||||||
|
'key' => '_bnb_booking_guest_id',
|
||||||
|
'value' => $guest_id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date range filter.
|
||||||
|
$date_from = $request->get_param( 'date_from' );
|
||||||
|
if ( $date_from && $this->validate_date( $date_from ) ) {
|
||||||
|
$meta_query[] = array(
|
||||||
|
'key' => '_bnb_booking_check_in',
|
||||||
|
'value' => $date_from,
|
||||||
|
'compare' => '>=',
|
||||||
|
'type' => 'DATE',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$date_to = $request->get_param( 'date_to' );
|
||||||
|
if ( $date_to && $this->validate_date( $date_to ) ) {
|
||||||
|
$meta_query[] = array(
|
||||||
|
'key' => '_bnb_booking_check_in',
|
||||||
|
'value' => $date_to,
|
||||||
|
'compare' => '<=',
|
||||||
|
'type' => 'DATE',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $meta_query ) ) {
|
||||||
|
$meta_query['relation'] = 'AND';
|
||||||
|
$args['meta_query'] = $meta_query;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = new \WP_Query( $args );
|
||||||
|
$items = array();
|
||||||
|
|
||||||
|
foreach ( $query->posts as $post ) {
|
||||||
|
$items[] = $this->prepare_booking_response( $post );
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $this->formatter->collection(
|
||||||
|
$items,
|
||||||
|
$query->found_posts,
|
||||||
|
$pagination['page'],
|
||||||
|
$pagination['per_page']
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->add_rate_limit_headers( $response, $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a booking.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function create_item( $request ) {
|
||||||
|
$rate_limit_error = $this->check_rate_limit( $request );
|
||||||
|
if ( $rate_limit_error ) {
|
||||||
|
return $rate_limit_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
$room_id = $request->get_param( 'room_id' );
|
||||||
|
$check_in = $request->get_param( 'check_in' );
|
||||||
|
$check_out = $request->get_param( 'check_out' );
|
||||||
|
$guest = $request->get_param( 'guest' );
|
||||||
|
|
||||||
|
// Validate room.
|
||||||
|
$room = get_post( $room_id );
|
||||||
|
if ( ! $room || Room::POST_TYPE !== $room->post_type || 'publish' !== $room->post_status ) {
|
||||||
|
return $this->formatter->not_found( __( 'Room', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate dates.
|
||||||
|
if ( ! $this->validate_date( $check_in ) ) {
|
||||||
|
return $this->formatter->validation_error( 'check_in', __( 'Invalid check-in date format. Use Y-m-d.', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
if ( ! $this->validate_date( $check_out ) ) {
|
||||||
|
return $this->formatter->validation_error( 'check_out', __( 'Invalid check-out date format. Use Y-m-d.', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
if ( $check_in >= $check_out ) {
|
||||||
|
return $this->formatter->validation_error( 'check_out', __( 'Check-out must be after check-in.', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check availability.
|
||||||
|
if ( ! Availability::is_available( $room_id, $check_in, $check_out ) ) {
|
||||||
|
return $this->formatter->conflict(
|
||||||
|
__( 'Room is not available for the selected dates.', 'wp-bnb' )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate guest info.
|
||||||
|
if ( empty( $guest['first_name'] ) || empty( $guest['last_name'] ) || empty( $guest['email'] ) ) {
|
||||||
|
return $this->formatter->validation_error( 'guest', __( 'Guest first name, last name, and email are required.', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! is_email( $guest['email'] ) ) {
|
||||||
|
return $this->formatter->validation_error( 'guest.email', __( 'Invalid email address.', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find or create guest.
|
||||||
|
$guest_id = $this->find_or_create_guest( $guest );
|
||||||
|
|
||||||
|
// Calculate price.
|
||||||
|
$price = Calculator::calculate( $room_id, $check_in, $check_out );
|
||||||
|
$services = $request->get_param( 'services' ) ?? array();
|
||||||
|
$room_price = $price['price'] ?? 0;
|
||||||
|
|
||||||
|
// Calculate services total.
|
||||||
|
$services_total = 0;
|
||||||
|
$services_data = array();
|
||||||
|
$check_in_date = new \DateTimeImmutable( $check_in );
|
||||||
|
$check_out_date = new \DateTimeImmutable( $check_out );
|
||||||
|
$nights = (int) $check_in_date->diff( $check_out_date )->days;
|
||||||
|
|
||||||
|
foreach ( $services as $service_item ) {
|
||||||
|
$service_id = $service_item['service_id'] ?? 0;
|
||||||
|
$quantity = $service_item['quantity'] ?? 1;
|
||||||
|
|
||||||
|
$service_data = Service::get_service_data( $service_id );
|
||||||
|
if ( $service_data ) {
|
||||||
|
$service_price = Service::calculate_service_price( $service_id, $quantity, $nights );
|
||||||
|
$services_total += $service_price;
|
||||||
|
$services_data[] = array(
|
||||||
|
'service_id' => $service_id,
|
||||||
|
'quantity' => $quantity,
|
||||||
|
'price' => $service_price,
|
||||||
|
'pricing_type' => $service_data['pricing_type'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$total_price = $room_price + $services_total;
|
||||||
|
|
||||||
|
// Generate reference.
|
||||||
|
$reference = Booking::generate_reference();
|
||||||
|
|
||||||
|
// Create booking post.
|
||||||
|
$guest_name = trim( $guest['first_name'] . ' ' . $guest['last_name'] );
|
||||||
|
$post_id = wp_insert_post(
|
||||||
|
array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'post_title' => $guest_name . ' (' . $check_in . ' - ' . $check_out . ')',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( is_wp_error( $post_id ) ) {
|
||||||
|
return $this->formatter->server_error( __( 'Failed to create booking.', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save meta.
|
||||||
|
update_post_meta( $post_id, '_bnb_booking_room_id', $room_id );
|
||||||
|
update_post_meta( $post_id, '_bnb_booking_guest_id', $guest_id );
|
||||||
|
update_post_meta( $post_id, '_bnb_booking_guest_name', $guest_name );
|
||||||
|
update_post_meta( $post_id, '_bnb_booking_guest_email', sanitize_email( $guest['email'] ) );
|
||||||
|
update_post_meta( $post_id, '_bnb_booking_guest_phone', sanitize_text_field( $guest['phone'] ?? '' ) );
|
||||||
|
update_post_meta( $post_id, '_bnb_booking_check_in', $check_in );
|
||||||
|
update_post_meta( $post_id, '_bnb_booking_check_out', $check_out );
|
||||||
|
update_post_meta( $post_id, '_bnb_booking_status', 'pending' );
|
||||||
|
update_post_meta( $post_id, '_bnb_booking_adults', absint( $request->get_param( 'guests_count' ) ?? 1 ) );
|
||||||
|
update_post_meta( $post_id, '_bnb_booking_calculated_price', $room_price );
|
||||||
|
update_post_meta( $post_id, '_bnb_booking_total_price', $total_price );
|
||||||
|
update_post_meta( $post_id, '_bnb_booking_reference', $reference );
|
||||||
|
|
||||||
|
if ( ! empty( $services_data ) ) {
|
||||||
|
update_post_meta( $post_id, '_bnb_booking_services', wp_json_encode( $services_data ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$notes = $request->get_param( 'notes' );
|
||||||
|
if ( $notes ) {
|
||||||
|
update_post_meta( $post_id, '_bnb_booking_guest_notes', sanitize_textarea_field( $notes ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send notification email.
|
||||||
|
if ( class_exists( EmailNotifier::class ) ) {
|
||||||
|
EmailNotifier::send_admin_notification( $post_id );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare response.
|
||||||
|
$booking = get_post( $post_id );
|
||||||
|
$data = $this->prepare_booking_response( $booking, true );
|
||||||
|
$location = rest_url( $this->namespace . '/bookings/' . $post_id );
|
||||||
|
|
||||||
|
$response = $this->formatter->created( $data, $location );
|
||||||
|
|
||||||
|
return $this->add_rate_limit_headers( $response, $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get single booking.
|
||||||
|
*
|
||||||
|
* @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 || Booking::POST_TYPE !== $post->post_type ) {
|
||||||
|
return $this->formatter->not_found( __( 'Booking', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $this->prepare_booking_response( $post, true );
|
||||||
|
$response = $this->formatter->success( $data );
|
||||||
|
|
||||||
|
return $this->add_rate_limit_headers( $response, $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a booking.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function update_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 || Booking::POST_TYPE !== $post->post_type ) {
|
||||||
|
return $this->formatter->not_found( __( 'Booking', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update status if provided.
|
||||||
|
$status = $request->get_param( 'status' );
|
||||||
|
if ( $status ) {
|
||||||
|
$current_status = get_post_meta( $id, '_bnb_booking_status', true );
|
||||||
|
if ( ! Booking::can_transition_to( $current_status, $status ) ) {
|
||||||
|
return $this->formatter->validation_error(
|
||||||
|
'status',
|
||||||
|
sprintf(
|
||||||
|
/* translators: %1$s: current status, %2$s: target status */
|
||||||
|
__( 'Cannot transition from %1$s to %2$s.', 'wp-bnb' ),
|
||||||
|
$current_status,
|
||||||
|
$status
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
update_post_meta( $id, '_bnb_booking_status', $status );
|
||||||
|
|
||||||
|
if ( 'confirmed' === $status ) {
|
||||||
|
update_post_meta( $id, '_bnb_booking_confirmed_at', current_time( 'mysql' ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update notes if provided.
|
||||||
|
$notes = $request->get_param( 'notes' );
|
||||||
|
if ( null !== $notes ) {
|
||||||
|
update_post_meta( $id, '_bnb_booking_notes', sanitize_textarea_field( $notes ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update guest notes if provided.
|
||||||
|
$guest_notes = $request->get_param( 'guest_notes' );
|
||||||
|
if ( null !== $guest_notes ) {
|
||||||
|
update_post_meta( $id, '_bnb_booking_guest_notes', sanitize_textarea_field( $guest_notes ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$booking = get_post( $id );
|
||||||
|
$data = $this->prepare_booking_response( $booking, true );
|
||||||
|
$response = $this->formatter->success( $data );
|
||||||
|
|
||||||
|
return $this->add_rate_limit_headers( $response, $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel a booking.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function delete_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 || Booking::POST_TYPE !== $post->post_type ) {
|
||||||
|
return $this->formatter->not_found( __( 'Booking', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel the booking (don't delete).
|
||||||
|
update_post_meta( $id, '_bnb_booking_status', 'cancelled' );
|
||||||
|
|
||||||
|
// Send cancellation email.
|
||||||
|
if ( class_exists( EmailNotifier::class ) ) {
|
||||||
|
EmailNotifier::send_cancellation_email( $id );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->formatter->no_content();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirm a booking.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function confirm_booking( $request ) {
|
||||||
|
return $this->transition_status( $request, 'confirmed' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check in a booking.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function check_in_booking( $request ) {
|
||||||
|
return $this->transition_status( $request, 'checked_in' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check out a booking.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function check_out_booking( $request ) {
|
||||||
|
return $this->transition_status( $request, 'checked_out' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transition booking status.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @param string $new_status Target status.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
private function transition_status( WP_REST_Request $request, string $new_status ) {
|
||||||
|
$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 || Booking::POST_TYPE !== $post->post_type ) {
|
||||||
|
return $this->formatter->not_found( __( 'Booking', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$current_status = get_post_meta( $id, '_bnb_booking_status', true );
|
||||||
|
|
||||||
|
if ( ! Booking::can_transition_to( $current_status, $new_status ) ) {
|
||||||
|
return $this->formatter->validation_error(
|
||||||
|
'status',
|
||||||
|
sprintf(
|
||||||
|
/* translators: %1$s: current status, %2$s: target status */
|
||||||
|
__( 'Cannot transition from %1$s to %2$s.', 'wp-bnb' ),
|
||||||
|
$current_status,
|
||||||
|
$new_status
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
update_post_meta( $id, '_bnb_booking_status', $new_status );
|
||||||
|
|
||||||
|
if ( 'confirmed' === $new_status ) {
|
||||||
|
update_post_meta( $id, '_bnb_booking_confirmed_at', current_time( 'mysql' ) );
|
||||||
|
if ( class_exists( EmailNotifier::class ) ) {
|
||||||
|
EmailNotifier::send_confirmation_email( $id );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$booking = get_post( $id );
|
||||||
|
$data = $this->prepare_booking_response( $booking, true );
|
||||||
|
$response = $this->formatter->success( $data );
|
||||||
|
|
||||||
|
return $this->add_rate_limit_headers( $response, $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find or create a guest from booking data.
|
||||||
|
*
|
||||||
|
* @param array $guest_data Guest data.
|
||||||
|
* @return int Guest post ID.
|
||||||
|
*/
|
||||||
|
private function find_or_create_guest( array $guest_data ): int {
|
||||||
|
$email = sanitize_email( $guest_data['email'] );
|
||||||
|
|
||||||
|
// Check if guest exists.
|
||||||
|
$existing = Guest::get_by_email( $email );
|
||||||
|
if ( $existing ) {
|
||||||
|
return $existing->ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new guest.
|
||||||
|
$guest_name = trim( $guest_data['first_name'] . ' ' . $guest_data['last_name'] );
|
||||||
|
$guest_id = wp_insert_post(
|
||||||
|
array(
|
||||||
|
'post_type' => Guest::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'post_title' => $guest_name,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( is_wp_error( $guest_id ) ) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save guest meta.
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_first_name', sanitize_text_field( $guest_data['first_name'] ) );
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_last_name', sanitize_text_field( $guest_data['last_name'] ) );
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_email', $email );
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_status', 'active' );
|
||||||
|
|
||||||
|
if ( ! empty( $guest_data['phone'] ) ) {
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_phone', sanitize_text_field( $guest_data['phone'] ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $guest_data['address'] ) ) {
|
||||||
|
$address = $guest_data['address'];
|
||||||
|
if ( ! empty( $address['street'] ) ) {
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_street', sanitize_text_field( $address['street'] ) );
|
||||||
|
}
|
||||||
|
if ( ! empty( $address['city'] ) ) {
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_city', sanitize_text_field( $address['city'] ) );
|
||||||
|
}
|
||||||
|
if ( ! empty( $address['postal_code'] ) ) {
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_postal_code', sanitize_text_field( $address['postal_code'] ) );
|
||||||
|
}
|
||||||
|
if ( ! empty( $address['country'] ) ) {
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_country', sanitize_text_field( $address['country'] ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $guest_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare booking data for response.
|
||||||
|
*
|
||||||
|
* @param \WP_Post $post Booking post object.
|
||||||
|
* @param bool $full Include full details.
|
||||||
|
* @return array Booking data.
|
||||||
|
*/
|
||||||
|
private function prepare_booking_response( \WP_Post $post, bool $full = false ): array {
|
||||||
|
$room_id = get_post_meta( $post->ID, '_bnb_booking_room_id', true );
|
||||||
|
$guest_id = get_post_meta( $post->ID, '_bnb_booking_guest_id', true );
|
||||||
|
$check_in = get_post_meta( $post->ID, '_bnb_booking_check_in', true );
|
||||||
|
$check_out = get_post_meta( $post->ID, '_bnb_booking_check_out', true );
|
||||||
|
$status = get_post_meta( $post->ID, '_bnb_booking_status', true );
|
||||||
|
|
||||||
|
// Calculate nights.
|
||||||
|
$nights = 0;
|
||||||
|
if ( $check_in && $check_out ) {
|
||||||
|
$check_in_date = new \DateTimeImmutable( $check_in );
|
||||||
|
$check_out_date = new \DateTimeImmutable( $check_out );
|
||||||
|
$nights = (int) $check_in_date->diff( $check_out_date )->days;
|
||||||
|
}
|
||||||
|
|
||||||
|
$room = $room_id ? get_post( $room_id ) : null;
|
||||||
|
|
||||||
|
$data = array(
|
||||||
|
'id' => $post->ID,
|
||||||
|
'reference' => get_post_meta( $post->ID, '_bnb_booking_reference', true ) ?: $post->post_title,
|
||||||
|
'status' => $status,
|
||||||
|
'room' => $room ? array(
|
||||||
|
'id' => $room->ID,
|
||||||
|
'title' => get_the_title( $room ),
|
||||||
|
'room_number' => get_post_meta( $room->ID, '_bnb_room_room_number', true ),
|
||||||
|
) : null,
|
||||||
|
'guest' => array(
|
||||||
|
'id' => (int) $guest_id,
|
||||||
|
'name' => get_post_meta( $post->ID, '_bnb_booking_guest_name', true ),
|
||||||
|
'email' => get_post_meta( $post->ID, '_bnb_booking_guest_email', true ),
|
||||||
|
'phone' => get_post_meta( $post->ID, '_bnb_booking_guest_phone', true ),
|
||||||
|
),
|
||||||
|
'dates' => array(
|
||||||
|
'check_in' => $check_in,
|
||||||
|
'check_out' => $check_out,
|
||||||
|
'nights' => $nights,
|
||||||
|
),
|
||||||
|
'pricing' => array(
|
||||||
|
'room_total' => (float) get_post_meta( $post->ID, '_bnb_booking_calculated_price', true ),
|
||||||
|
'services_total' => 0,
|
||||||
|
'grand_total' => (float) get_post_meta( $post->ID, '_bnb_booking_total_price', true ),
|
||||||
|
'currency' => get_option( 'wp_bnb_currency', 'CHF' ),
|
||||||
|
),
|
||||||
|
'created_at' => $post->post_date_gmt,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get building info.
|
||||||
|
if ( $room ) {
|
||||||
|
$building_id = get_post_meta( $room->ID, '_bnb_room_building_id', true );
|
||||||
|
$building = $building_id ? get_post( $building_id ) : null;
|
||||||
|
if ( $building ) {
|
||||||
|
$data['building'] = array(
|
||||||
|
'id' => $building->ID,
|
||||||
|
'title' => get_the_title( $building ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate services total.
|
||||||
|
$services_json = get_post_meta( $post->ID, '_bnb_booking_services', true );
|
||||||
|
if ( $services_json ) {
|
||||||
|
$services = json_decode( $services_json, true );
|
||||||
|
if ( is_array( $services ) ) {
|
||||||
|
$services_total = 0;
|
||||||
|
$services_list = array();
|
||||||
|
foreach ( $services as $service ) {
|
||||||
|
$services_total += (float) ( $service['price'] ?? 0 );
|
||||||
|
$service_post = get_post( $service['service_id'] );
|
||||||
|
$services_list[] = array(
|
||||||
|
'id' => $service['service_id'],
|
||||||
|
'name' => $service_post ? get_the_title( $service_post ) : '',
|
||||||
|
'quantity' => $service['quantity'] ?? 1,
|
||||||
|
'price' => (float) ( $service['price'] ?? 0 ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$data['pricing']['services_total'] = $services_total;
|
||||||
|
$data['services'] = $services_list;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $full ) {
|
||||||
|
$data['notes'] = get_post_meta( $post->ID, '_bnb_booking_notes', true );
|
||||||
|
$data['guest_notes'] = get_post_meta( $post->ID, '_bnb_booking_guest_notes', true );
|
||||||
|
$data['adults'] = (int) get_post_meta( $post->ID, '_bnb_booking_adults', true );
|
||||||
|
$data['children'] = (int) get_post_meta( $post->ID, '_bnb_booking_children', true );
|
||||||
|
$data['confirmed_at'] = get_post_meta( $post->ID, '_bnb_booking_confirmed_at', true );
|
||||||
|
}
|
||||||
|
|
||||||
|
$data['_links'] = array(
|
||||||
|
'self' => array(
|
||||||
|
array( 'href' => rest_url( $this->namespace . '/bookings/' . $post->ID ) ),
|
||||||
|
),
|
||||||
|
'room' => $room ? array(
|
||||||
|
array( 'href' => rest_url( $this->namespace . '/rooms/' . $room->ID ) ),
|
||||||
|
) : array(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get bookings collection parameters.
|
||||||
|
*
|
||||||
|
* @return array Collection parameters.
|
||||||
|
*/
|
||||||
|
private function get_bookings_collection_params(): array {
|
||||||
|
$params = $this->get_collection_params();
|
||||||
|
|
||||||
|
$params['status'] = array(
|
||||||
|
'description' => __( 'Filter by booking status.', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'enum' => array( 'pending', 'confirmed', 'checked_in', 'checked_out', 'cancelled' ),
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
);
|
||||||
|
|
||||||
|
$params['room_id'] = array(
|
||||||
|
'description' => __( 'Filter by room ID.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
);
|
||||||
|
|
||||||
|
$params['guest_id'] = array(
|
||||||
|
'description' => __( 'Filter by guest ID.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
);
|
||||||
|
|
||||||
|
$params['date_from'] = array(
|
||||||
|
'description' => __( 'Filter bookings with check-in from this date (Y-m-d).', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
);
|
||||||
|
|
||||||
|
$params['date_to'] = array(
|
||||||
|
'description' => __( 'Filter bookings with check-in until this date (Y-m-d).', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
);
|
||||||
|
|
||||||
|
return $params;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get create booking parameters.
|
||||||
|
*
|
||||||
|
* @return array Create parameters.
|
||||||
|
*/
|
||||||
|
private function get_create_booking_params(): array {
|
||||||
|
return array(
|
||||||
|
'room_id' => array(
|
||||||
|
'description' => __( 'Room ID.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
'check_in' => array(
|
||||||
|
'description' => __( 'Check-in date (Y-m-d).', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
),
|
||||||
|
'check_out' => array(
|
||||||
|
'description' => __( 'Check-out date (Y-m-d).', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
),
|
||||||
|
'guest' => array(
|
||||||
|
'description' => __( 'Guest information.', 'wp-bnb' ),
|
||||||
|
'type' => 'object',
|
||||||
|
'required' => true,
|
||||||
|
'properties' => array(
|
||||||
|
'first_name' => array( 'type' => 'string', 'required' => true ),
|
||||||
|
'last_name' => array( 'type' => 'string', 'required' => true ),
|
||||||
|
'email' => array( 'type' => 'string', 'required' => true, 'format' => 'email' ),
|
||||||
|
'phone' => array( 'type' => 'string' ),
|
||||||
|
'address' => array(
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => array(
|
||||||
|
'street' => array( 'type' => 'string' ),
|
||||||
|
'city' => array( 'type' => 'string' ),
|
||||||
|
'postal_code' => array( 'type' => 'string' ),
|
||||||
|
'country' => array( 'type' => 'string' ),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'guests_count' => array(
|
||||||
|
'description' => __( 'Number of guests.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'default' => 1,
|
||||||
|
'minimum' => 1,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
'services' => array(
|
||||||
|
'description' => __( 'Additional services.', 'wp-bnb' ),
|
||||||
|
'type' => 'array',
|
||||||
|
'items' => array(
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => array(
|
||||||
|
'service_id' => array( 'type' => 'integer', 'required' => true ),
|
||||||
|
'quantity' => array( 'type' => 'integer', 'default' => 1 ),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'notes' => array(
|
||||||
|
'description' => __( 'Guest notes or special requests.', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_textarea_field',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get update booking parameters.
|
||||||
|
*
|
||||||
|
* @return array Update parameters.
|
||||||
|
*/
|
||||||
|
private function get_update_booking_params(): array {
|
||||||
|
return array(
|
||||||
|
'id' => array(
|
||||||
|
'description' => __( 'Booking ID.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
'status' => array(
|
||||||
|
'description' => __( 'Booking status.', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'enum' => array( 'pending', 'confirmed', 'checked_in', 'checked_out', 'cancelled' ),
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
),
|
||||||
|
'notes' => array(
|
||||||
|
'description' => __( 'Internal staff notes.', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_textarea_field',
|
||||||
|
),
|
||||||
|
'guest_notes' => array(
|
||||||
|
'description' => __( 'Guest notes or special requests.', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_textarea_field',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
323
src/Api/Controllers/BuildingsController.php
Normal file
323
src/Api/Controllers/BuildingsController.php
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Buildings REST Controller
|
||||||
|
*
|
||||||
|
* Handles REST API endpoints for buildings.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Api\Controllers
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Api\Controllers;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\PostTypes\Building;
|
||||||
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
use WP_REST_Request;
|
||||||
|
use WP_REST_Response;
|
||||||
|
use WP_REST_Server;
|
||||||
|
use WP_Error;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Buildings Controller class.
|
||||||
|
*/
|
||||||
|
final class BuildingsController extends AbstractController {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route base.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $rest_base = 'buildings';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register routes.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function register_routes(): void {
|
||||||
|
// GET /buildings - List all buildings.
|
||||||
|
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_collection_params(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// GET /buildings/{id} - Get single building.
|
||||||
|
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' => __( 'Building ID.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// GET /buildings/{id}/rooms - Get rooms in building.
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base . '/(?P<id>[\d]+)/rooms',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::READABLE,
|
||||||
|
'callback' => array( $this, 'get_building_rooms' ),
|
||||||
|
'permission_callback' => array( $this, 'public_permission' ),
|
||||||
|
'args' => array(
|
||||||
|
'id' => array(
|
||||||
|
'description' => __( 'Building ID.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
'status' => array(
|
||||||
|
'description' => __( 'Filter by room status.', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'enum' => array( 'available', 'occupied', 'maintenance', 'blocked' ),
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get collection of buildings.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function get_items( $request ) {
|
||||||
|
// Check rate limit.
|
||||||
|
$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' => Building::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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = new \WP_Query( $args );
|
||||||
|
$items = array();
|
||||||
|
|
||||||
|
foreach ( $query->posts as $post ) {
|
||||||
|
$items[] = $this->prepare_building_response( $post );
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $this->formatter->collection(
|
||||||
|
$items,
|
||||||
|
$query->found_posts,
|
||||||
|
$pagination['page'],
|
||||||
|
$pagination['per_page']
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->add_rate_limit_headers( $response, $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get single building.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function get_item( $request ) {
|
||||||
|
// Check rate limit.
|
||||||
|
$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 || Building::POST_TYPE !== $post->post_type || 'publish' !== $post->post_status ) {
|
||||||
|
return $this->formatter->not_found( __( 'Building', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $this->prepare_building_response( $post, true );
|
||||||
|
$response = $this->formatter->success( $data );
|
||||||
|
|
||||||
|
return $this->add_rate_limit_headers( $response, $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get rooms in a building.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function get_building_rooms( $request ) {
|
||||||
|
// Check rate limit.
|
||||||
|
$rate_limit_error = $this->check_rate_limit( $request );
|
||||||
|
if ( $rate_limit_error ) {
|
||||||
|
return $rate_limit_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
$building_id = $request->get_param( 'id' );
|
||||||
|
$building = get_post( $building_id );
|
||||||
|
|
||||||
|
if ( ! $building || Building::POST_TYPE !== $building->post_type || 'publish' !== $building->post_status ) {
|
||||||
|
return $this->formatter->not_found( __( 'Building', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$args = array(
|
||||||
|
'post_type' => Room::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'meta_query' => array(
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_room_building_id',
|
||||||
|
'value' => $building_id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'orderby' => 'meta_value',
|
||||||
|
'meta_key' => '_bnb_room_room_number',
|
||||||
|
'order' => 'ASC',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter by status.
|
||||||
|
$status = $request->get_param( 'status' );
|
||||||
|
if ( $status ) {
|
||||||
|
$args['meta_query'][] = array(
|
||||||
|
'key' => '_bnb_room_status',
|
||||||
|
'value' => $status,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$rooms = get_posts( $args );
|
||||||
|
$items = array();
|
||||||
|
|
||||||
|
foreach ( $rooms as $room ) {
|
||||||
|
$items[] = $this->prepare_room_summary( $room );
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $this->formatter->success( $items );
|
||||||
|
|
||||||
|
return $this->add_rate_limit_headers( $response, $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare building data for response.
|
||||||
|
*
|
||||||
|
* @param \WP_Post $post Building post object.
|
||||||
|
* @param bool $full Include full details.
|
||||||
|
* @return array Building data.
|
||||||
|
*/
|
||||||
|
private function prepare_building_response( \WP_Post $post, bool $full = false ): array {
|
||||||
|
$data = $this->format_post_base( $post );
|
||||||
|
|
||||||
|
// Featured image.
|
||||||
|
$data['featured_image'] = $this->format_featured_image( $post->ID );
|
||||||
|
$data['permalink'] = get_permalink( $post->ID );
|
||||||
|
|
||||||
|
// Address.
|
||||||
|
$data['address'] = array(
|
||||||
|
'street' => get_post_meta( $post->ID, '_bnb_building_street', true ),
|
||||||
|
'street2' => get_post_meta( $post->ID, '_bnb_building_street2', true ),
|
||||||
|
'city' => get_post_meta( $post->ID, '_bnb_building_city', true ),
|
||||||
|
'state' => get_post_meta( $post->ID, '_bnb_building_state', true ),
|
||||||
|
'postal_code' => get_post_meta( $post->ID, '_bnb_building_zip', true ),
|
||||||
|
'country' => get_post_meta( $post->ID, '_bnb_building_country', true ),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Contact.
|
||||||
|
$data['contact'] = array(
|
||||||
|
'phone' => get_post_meta( $post->ID, '_bnb_building_phone', true ),
|
||||||
|
'email' => get_post_meta( $post->ID, '_bnb_building_email', true ),
|
||||||
|
'website' => get_post_meta( $post->ID, '_bnb_building_website', true ),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Details.
|
||||||
|
$data['details'] = array(
|
||||||
|
'rooms_count' => (int) get_post_meta( $post->ID, '_bnb_building_total_rooms', true ),
|
||||||
|
'floors' => (int) get_post_meta( $post->ID, '_bnb_building_floors', true ),
|
||||||
|
'year_built' => (int) get_post_meta( $post->ID, '_bnb_building_year_built', true ),
|
||||||
|
'check_in_time' => get_post_meta( $post->ID, '_bnb_building_check_in_time', true ) ?: '14:00',
|
||||||
|
'check_out_time' => get_post_meta( $post->ID, '_bnb_building_check_out_time', true ) ?: '11:00',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Count actual rooms.
|
||||||
|
$actual_rooms = Room::get_rooms_for_building( $post->ID );
|
||||||
|
$data['details']['actual_rooms_count'] = count( $actual_rooms );
|
||||||
|
|
||||||
|
// Full address formatted.
|
||||||
|
if ( $full ) {
|
||||||
|
$data['address']['formatted'] = Building::get_formatted_address( $post->ID );
|
||||||
|
|
||||||
|
// Country name.
|
||||||
|
$countries = Building::get_countries();
|
||||||
|
$country_code = $data['address']['country'];
|
||||||
|
$data['address']['country_name'] = $countries[ $country_code ] ?? $country_code;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add HATEOAS links.
|
||||||
|
$data['_links'] = array(
|
||||||
|
'self' => array(
|
||||||
|
array( 'href' => rest_url( $this->namespace . '/buildings/' . $post->ID ) ),
|
||||||
|
),
|
||||||
|
'rooms' => array(
|
||||||
|
array( 'href' => rest_url( $this->namespace . '/buildings/' . $post->ID . '/rooms' ) ),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare room summary for building rooms list.
|
||||||
|
*
|
||||||
|
* @param \WP_Post $room Room post object.
|
||||||
|
* @return array Room summary data.
|
||||||
|
*/
|
||||||
|
private function prepare_room_summary( \WP_Post $room ): array {
|
||||||
|
return array(
|
||||||
|
'id' => $room->ID,
|
||||||
|
'title' => get_the_title( $room ),
|
||||||
|
'slug' => $room->post_name,
|
||||||
|
'permalink' => get_permalink( $room->ID ),
|
||||||
|
'room_number' => get_post_meta( $room->ID, '_bnb_room_room_number', true ),
|
||||||
|
'floor' => (int) get_post_meta( $room->ID, '_bnb_room_floor', true ),
|
||||||
|
'capacity' => (int) get_post_meta( $room->ID, '_bnb_room_capacity', true ),
|
||||||
|
'status' => get_post_meta( $room->ID, '_bnb_room_status', true ) ?: 'available',
|
||||||
|
'thumbnail' => get_the_post_thumbnail_url( $room->ID, 'thumbnail' ) ?: null,
|
||||||
|
'_links' => array(
|
||||||
|
'self' => array(
|
||||||
|
array( 'href' => rest_url( $this->namespace . '/rooms/' . $room->ID ) ),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
278
src/Api/Controllers/PricingController.php
Normal file
278
src/Api/Controllers/PricingController.php
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Pricing REST Controller
|
||||||
|
*
|
||||||
|
* Handles REST API endpoints for price calculations.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Api\Controllers
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Api\Controllers;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
use Magdev\WpBnb\PostTypes\Service;
|
||||||
|
use Magdev\WpBnb\Pricing\Calculator;
|
||||||
|
use Magdev\WpBnb\Pricing\Season;
|
||||||
|
use WP_REST_Request;
|
||||||
|
use WP_REST_Response;
|
||||||
|
use WP_REST_Server;
|
||||||
|
use WP_Error;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pricing Controller class.
|
||||||
|
*/
|
||||||
|
final class PricingController extends AbstractController {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route base.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $rest_base = 'pricing';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register routes.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function register_routes(): void {
|
||||||
|
// POST /pricing/calculate - Calculate full booking price.
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base . '/calculate',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::CREATABLE,
|
||||||
|
'callback' => array( $this, 'calculate_price' ),
|
||||||
|
'permission_callback' => array( $this, 'public_permission' ),
|
||||||
|
'args' => array(
|
||||||
|
'room_id' => array(
|
||||||
|
'description' => __( 'Room ID.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
'check_in' => array(
|
||||||
|
'description' => __( 'Check-in date (Y-m-d).', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
),
|
||||||
|
'check_out' => array(
|
||||||
|
'description' => __( 'Check-out date (Y-m-d).', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
),
|
||||||
|
'services' => array(
|
||||||
|
'description' => __( 'Additional services.', 'wp-bnb' ),
|
||||||
|
'type' => 'array',
|
||||||
|
'items' => array(
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => array(
|
||||||
|
'service_id' => array( 'type' => 'integer', 'required' => true ),
|
||||||
|
'quantity' => array( 'type' => 'integer', 'default' => 1 ),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// GET /pricing/seasons - Get active seasons.
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base . '/seasons',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::READABLE,
|
||||||
|
'callback' => array( $this, 'get_seasons' ),
|
||||||
|
'permission_callback' => array( $this, 'public_permission' ),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate full booking 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$room_id = $request->get_param( 'room_id' );
|
||||||
|
$check_in = $request->get_param( 'check_in' );
|
||||||
|
$check_out = $request->get_param( 'check_out' );
|
||||||
|
$services = $request->get_param( 'services' ) ?? array();
|
||||||
|
|
||||||
|
// Validate room.
|
||||||
|
$room = get_post( $room_id );
|
||||||
|
if ( ! $room || Room::POST_TYPE !== $room->post_type || 'publish' !== $room->post_status ) {
|
||||||
|
return $this->formatter->not_found( __( 'Room', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate dates.
|
||||||
|
if ( ! $this->validate_date( $check_in ) ) {
|
||||||
|
return $this->formatter->validation_error( 'check_in', __( 'Invalid check-in date format. Use Y-m-d.', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
if ( ! $this->validate_date( $check_out ) ) {
|
||||||
|
return $this->formatter->validation_error( 'check_out', __( 'Invalid check-out date format. Use Y-m-d.', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
if ( $check_in >= $check_out ) {
|
||||||
|
return $this->formatter->validation_error( 'check_out', __( 'Check-out must be after check-in.', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate nights.
|
||||||
|
$check_in_date = new \DateTimeImmutable( $check_in );
|
||||||
|
$check_out_date = new \DateTimeImmutable( $check_out );
|
||||||
|
$nights = (int) $check_in_date->diff( $check_out_date )->days;
|
||||||
|
|
||||||
|
// Calculate room price.
|
||||||
|
$price = Calculator::calculate( $room_id, $check_in, $check_out );
|
||||||
|
$room_total = $price['price'] ?? 0;
|
||||||
|
$breakdown = $price['breakdown'] ?? array();
|
||||||
|
$currency = get_option( 'wp_bnb_currency', 'CHF' );
|
||||||
|
|
||||||
|
// Build night-by-night breakdown.
|
||||||
|
$night_breakdown = array();
|
||||||
|
$current_date = $check_in_date;
|
||||||
|
$base_rate = $breakdown['base_price_per_night'] ?? 0;
|
||||||
|
|
||||||
|
while ( $current_date < $check_out_date ) {
|
||||||
|
$date_str = $current_date->format( 'Y-m-d' );
|
||||||
|
$day_of_week = (int) $current_date->format( 'w' );
|
||||||
|
$modifiers = array();
|
||||||
|
$rate = $base_rate;
|
||||||
|
|
||||||
|
// Check for weekend surcharge.
|
||||||
|
$weekend_days = explode( ',', get_option( 'wp_bnb_weekend_days', '5,6' ) );
|
||||||
|
if ( in_array( (string) $day_of_week, $weekend_days, true ) ) {
|
||||||
|
$weekend_surcharge = $breakdown['weekend_surcharge'] ?? 0;
|
||||||
|
if ( $weekend_surcharge > 0 ) {
|
||||||
|
$rate += $weekend_surcharge / max( 1, $breakdown['weekend_nights'] ?? 1 );
|
||||||
|
$modifiers[] = 'weekend_surcharge';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for seasonal modifier.
|
||||||
|
$season = Season::get_active_season( $date_str );
|
||||||
|
if ( $season && $season['modifier'] != 1.0 ) {
|
||||||
|
$modifiers[] = 'season:' . $season['name'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$night_breakdown[] = array(
|
||||||
|
'date' => $date_str,
|
||||||
|
'rate' => round( $rate, 2 ),
|
||||||
|
'modifiers' => $modifiers,
|
||||||
|
);
|
||||||
|
|
||||||
|
$current_date = $current_date->modify( '+1 day' );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate services.
|
||||||
|
$services_items = array();
|
||||||
|
$services_total = 0;
|
||||||
|
|
||||||
|
foreach ( $services as $service_item ) {
|
||||||
|
$service_id = $service_item['service_id'] ?? 0;
|
||||||
|
$quantity = $service_item['quantity'] ?? 1;
|
||||||
|
|
||||||
|
$service_data = Service::get_service_data( $service_id );
|
||||||
|
if ( $service_data && 'active' === $service_data['status'] ) {
|
||||||
|
$service_price = Service::calculate_service_price( $service_id, $quantity, $nights );
|
||||||
|
$services_total += $service_price;
|
||||||
|
|
||||||
|
$services_items[] = array(
|
||||||
|
'id' => $service_id,
|
||||||
|
'name' => $service_data['title'],
|
||||||
|
'quantity' => $quantity,
|
||||||
|
'nights' => 'per_night' === $service_data['pricing_type'] ? $nights : null,
|
||||||
|
'subtotal' => $service_price,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$grand_total = $room_total + $services_total;
|
||||||
|
|
||||||
|
// Build response.
|
||||||
|
$data = array(
|
||||||
|
'room' => array(
|
||||||
|
'id' => $room->ID,
|
||||||
|
'title' => get_the_title( $room ),
|
||||||
|
'pricing_tier' => $breakdown['tier']->value ?? 'short_term',
|
||||||
|
),
|
||||||
|
'dates' => array(
|
||||||
|
'check_in' => $check_in,
|
||||||
|
'check_out' => $check_out,
|
||||||
|
'nights' => $nights,
|
||||||
|
),
|
||||||
|
'room_pricing' => array(
|
||||||
|
'base_rate' => $breakdown['base_price_per_night'] ?? 0,
|
||||||
|
'subtotal' => $room_total,
|
||||||
|
'breakdown' => $night_breakdown,
|
||||||
|
),
|
||||||
|
'services_pricing' => array(
|
||||||
|
'items' => $services_items,
|
||||||
|
'subtotal' => $services_total,
|
||||||
|
),
|
||||||
|
'totals' => array(
|
||||||
|
'room' => $room_total,
|
||||||
|
'services' => $services_total,
|
||||||
|
'grand_total' => $grand_total,
|
||||||
|
'currency' => $currency,
|
||||||
|
'formatted' => Calculator::formatPrice( $grand_total ),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
$response = $this->formatter->success( $data );
|
||||||
|
|
||||||
|
return $this->add_rate_limit_headers( $response, $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get active seasons.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function get_seasons( $request ) {
|
||||||
|
$rate_limit_error = $this->check_rate_limit( $request );
|
||||||
|
if ( $rate_limit_error ) {
|
||||||
|
return $rate_limit_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
$seasons = Season::get_all();
|
||||||
|
$items = array();
|
||||||
|
|
||||||
|
foreach ( $seasons as $season ) {
|
||||||
|
$items[] = array(
|
||||||
|
'id' => $season['id'],
|
||||||
|
'name' => $season['name'],
|
||||||
|
'start_date' => $season['start_date'],
|
||||||
|
'end_date' => $season['end_date'],
|
||||||
|
'modifier' => (float) $season['modifier'],
|
||||||
|
'priority' => (int) $season['priority'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by priority (highest first).
|
||||||
|
usort(
|
||||||
|
$items,
|
||||||
|
function ( $a, $b ) {
|
||||||
|
return $b['priority'] - $a['priority'];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
$response = $this->formatter->success( $items );
|
||||||
|
|
||||||
|
return $this->add_rate_limit_headers( $response, $request );
|
||||||
|
}
|
||||||
|
}
|
||||||
768
src/Api/Controllers/RoomsController.php
Normal file
768
src/Api/Controllers/RoomsController.php
Normal file
@@ -0,0 +1,768 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Rooms REST Controller
|
||||||
|
*
|
||||||
|
* Handles REST API endpoints for rooms, availability, and calendar.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Api\Controllers
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Api\Controllers;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
use Magdev\WpBnb\PostTypes\Building;
|
||||||
|
use Magdev\WpBnb\PostTypes\Booking;
|
||||||
|
use Magdev\WpBnb\Booking\Availability;
|
||||||
|
use Magdev\WpBnb\Pricing\Calculator;
|
||||||
|
use Magdev\WpBnb\Pricing\PricingTier;
|
||||||
|
use Magdev\WpBnb\Taxonomies\RoomType;
|
||||||
|
use Magdev\WpBnb\Taxonomies\Amenity;
|
||||||
|
use Magdev\WpBnb\Frontend\Search;
|
||||||
|
use WP_REST_Request;
|
||||||
|
use WP_REST_Response;
|
||||||
|
use WP_REST_Server;
|
||||||
|
use WP_Error;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rooms Controller class.
|
||||||
|
*/
|
||||||
|
final class RoomsController extends AbstractController {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route base.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $rest_base = 'rooms';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register routes.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function register_routes(): void {
|
||||||
|
// GET /rooms - List all rooms.
|
||||||
|
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_rooms_collection_params(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// GET /rooms/{id} - Get single room.
|
||||||
|
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' => __( 'Room ID.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// GET /rooms/{id}/availability - Check room availability.
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base . '/(?P<id>[\d]+)/availability',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::READABLE,
|
||||||
|
'callback' => array( $this, 'get_availability' ),
|
||||||
|
'permission_callback' => array( $this, 'public_permission' ),
|
||||||
|
'args' => array(
|
||||||
|
'id' => array(
|
||||||
|
'description' => __( 'Room ID.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
'check_in' => array(
|
||||||
|
'description' => __( 'Check-in date (Y-m-d).', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
),
|
||||||
|
'check_out' => array(
|
||||||
|
'description' => __( 'Check-out date (Y-m-d).', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// GET /rooms/{id}/calendar - Get room calendar.
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base . '/(?P<id>[\d]+)/calendar',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::READABLE,
|
||||||
|
'callback' => array( $this, 'get_calendar' ),
|
||||||
|
'permission_callback' => array( $this, 'public_permission' ),
|
||||||
|
'args' => array(
|
||||||
|
'id' => array(
|
||||||
|
'description' => __( 'Room ID.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
'year' => array(
|
||||||
|
'description' => __( 'Year.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'default' => (int) gmdate( 'Y' ),
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
'month' => array(
|
||||||
|
'description' => __( 'Month (1-12).', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'default' => (int) gmdate( 'n' ),
|
||||||
|
'minimum' => 1,
|
||||||
|
'maximum' => 12,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// POST /availability/search - Search available rooms.
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/availability/search',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::CREATABLE,
|
||||||
|
'callback' => array( $this, 'search_availability' ),
|
||||||
|
'permission_callback' => array( $this, 'public_permission' ),
|
||||||
|
'args' => array(
|
||||||
|
'check_in' => array(
|
||||||
|
'description' => __( 'Check-in date (Y-m-d).', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
),
|
||||||
|
'check_out' => array(
|
||||||
|
'description' => __( 'Check-out date (Y-m-d).', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
),
|
||||||
|
'guests' => array(
|
||||||
|
'description' => __( 'Number of guests.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'minimum' => 1,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
'building_id' => array(
|
||||||
|
'description' => __( 'Building ID.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
),
|
||||||
|
'room_type' => array(
|
||||||
|
'description' => __( 'Room type term ID or slug.', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
),
|
||||||
|
'amenities' => array(
|
||||||
|
'description' => __( 'Comma-separated amenity slugs.', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
),
|
||||||
|
'price_min' => array(
|
||||||
|
'description' => __( 'Minimum price per night.', 'wp-bnb' ),
|
||||||
|
'type' => 'number',
|
||||||
|
),
|
||||||
|
'price_max' => array(
|
||||||
|
'description' => __( 'Maximum price per night.', 'wp-bnb' ),
|
||||||
|
'type' => 'number',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get collection of rooms.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function get_items( $request ) {
|
||||||
|
// Check rate limit.
|
||||||
|
$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', 'capacity', 'price' ), 'title' );
|
||||||
|
|
||||||
|
$args = array(
|
||||||
|
'post_type' => Room::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => $pagination['per_page'],
|
||||||
|
'offset' => $pagination['offset'],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle special orderby values.
|
||||||
|
switch ( $sorting['orderby'] ) {
|
||||||
|
case 'capacity':
|
||||||
|
$args['meta_key'] = '_bnb_room_capacity';
|
||||||
|
$args['orderby'] = 'meta_value_num';
|
||||||
|
break;
|
||||||
|
case 'price':
|
||||||
|
$args['meta_key'] = '_bnb_room_price_short_term';
|
||||||
|
$args['orderby'] = 'meta_value_num';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$args['orderby'] = $sorting['orderby'];
|
||||||
|
}
|
||||||
|
$args['order'] = $sorting['order'];
|
||||||
|
|
||||||
|
// Search filter.
|
||||||
|
$search = $request->get_param( 'search' );
|
||||||
|
if ( $search ) {
|
||||||
|
$args['s'] = $search;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Building filter.
|
||||||
|
$building = $request->get_param( 'building' );
|
||||||
|
if ( $building ) {
|
||||||
|
$args['meta_query'][] = array(
|
||||||
|
'key' => '_bnb_room_building_id',
|
||||||
|
'value' => $building,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status filter.
|
||||||
|
$status = $request->get_param( 'status' );
|
||||||
|
if ( $status ) {
|
||||||
|
$args['meta_query'][] = array(
|
||||||
|
'key' => '_bnb_room_status',
|
||||||
|
'value' => $status,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capacity filter.
|
||||||
|
$capacity_min = $request->get_param( 'capacity_min' );
|
||||||
|
if ( $capacity_min ) {
|
||||||
|
$args['meta_query'][] = array(
|
||||||
|
'key' => '_bnb_room_capacity',
|
||||||
|
'value' => $capacity_min,
|
||||||
|
'compare' => '>=',
|
||||||
|
'type' => 'NUMERIC',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Room type filter.
|
||||||
|
$room_type = $request->get_param( 'room_type' );
|
||||||
|
if ( $room_type ) {
|
||||||
|
$args['tax_query'][] = array(
|
||||||
|
'taxonomy' => RoomType::TAXONOMY,
|
||||||
|
'field' => is_numeric( $room_type ) ? 'term_id' : 'slug',
|
||||||
|
'terms' => $room_type,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Amenities filter.
|
||||||
|
$amenities = $request->get_param( 'amenities' );
|
||||||
|
if ( $amenities ) {
|
||||||
|
$amenity_slugs = array_map( 'trim', explode( ',', $amenities ) );
|
||||||
|
$args['tax_query'][] = array(
|
||||||
|
'taxonomy' => Amenity::TAXONOMY,
|
||||||
|
'field' => 'slug',
|
||||||
|
'terms' => $amenity_slugs,
|
||||||
|
'operator' => 'AND',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $args['meta_query'] ) && count( $args['meta_query'] ) > 1 ) {
|
||||||
|
$args['meta_query']['relation'] = 'AND';
|
||||||
|
}
|
||||||
|
if ( isset( $args['tax_query'] ) && count( $args['tax_query'] ) > 1 ) {
|
||||||
|
$args['tax_query']['relation'] = 'AND';
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = new \WP_Query( $args );
|
||||||
|
$items = array();
|
||||||
|
|
||||||
|
foreach ( $query->posts as $post ) {
|
||||||
|
$items[] = $this->prepare_room_response( $post );
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $this->formatter->collection(
|
||||||
|
$items,
|
||||||
|
$query->found_posts,
|
||||||
|
$pagination['page'],
|
||||||
|
$pagination['per_page']
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->add_rate_limit_headers( $response, $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get single room.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function get_item( $request ) {
|
||||||
|
// Check rate limit.
|
||||||
|
$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 || Room::POST_TYPE !== $post->post_type || 'publish' !== $post->post_status ) {
|
||||||
|
return $this->formatter->not_found( __( 'Room', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $this->prepare_room_response( $post, true );
|
||||||
|
$response = $this->formatter->success( $data );
|
||||||
|
|
||||||
|
return $this->add_rate_limit_headers( $response, $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get room availability.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function get_availability( $request ) {
|
||||||
|
// Check rate limit.
|
||||||
|
$rate_limit_error = $this->check_rate_limit( $request );
|
||||||
|
if ( $rate_limit_error ) {
|
||||||
|
return $rate_limit_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
$room_id = $request->get_param( 'id' );
|
||||||
|
$check_in = $request->get_param( 'check_in' );
|
||||||
|
$check_out = $request->get_param( 'check_out' );
|
||||||
|
|
||||||
|
// Validate room exists.
|
||||||
|
$room = get_post( $room_id );
|
||||||
|
if ( ! $room || Room::POST_TYPE !== $room->post_type || 'publish' !== $room->post_status ) {
|
||||||
|
return $this->formatter->not_found( __( 'Room', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate dates.
|
||||||
|
if ( ! $this->validate_date( $check_in ) ) {
|
||||||
|
return $this->formatter->validation_error( 'check_in', __( 'Invalid check-in date format. Use Y-m-d.', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
if ( ! $this->validate_date( $check_out ) ) {
|
||||||
|
return $this->formatter->validation_error( 'check_out', __( 'Invalid check-out date format. Use Y-m-d.', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
if ( $check_in >= $check_out ) {
|
||||||
|
return $this->formatter->validation_error( 'check_out', __( 'Check-out must be after check-in.', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check availability.
|
||||||
|
$is_available = Availability::is_available( $room_id, $check_in, $check_out );
|
||||||
|
|
||||||
|
// Calculate nights.
|
||||||
|
$check_in_date = new \DateTimeImmutable( $check_in );
|
||||||
|
$check_out_date = new \DateTimeImmutable( $check_out );
|
||||||
|
$nights = (int) $check_in_date->diff( $check_out_date )->days;
|
||||||
|
|
||||||
|
$data = array(
|
||||||
|
'available' => $is_available,
|
||||||
|
'room_id' => $room_id,
|
||||||
|
'check_in' => $check_in,
|
||||||
|
'check_out' => $check_out,
|
||||||
|
'nights' => $nights,
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( $is_available ) {
|
||||||
|
// Calculate pricing.
|
||||||
|
$price = Calculator::calculate( $room_id, $check_in, $check_out );
|
||||||
|
$data['pricing'] = array(
|
||||||
|
'tier' => $price['breakdown']['tier']->value ?? 'short_term',
|
||||||
|
'base_rate' => $price['breakdown']['base_price_per_night'] ?? 0,
|
||||||
|
'total' => $price['price'] ?? 0,
|
||||||
|
'formatted' => $price['price_formatted'] ?? '',
|
||||||
|
'currency' => get_option( 'wp_bnb_currency', 'CHF' ),
|
||||||
|
'breakdown' => $price['breakdown'] ?? array(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Get conflicts.
|
||||||
|
$conflicts = $this->get_conflicts( $room_id, $check_in, $check_out );
|
||||||
|
$data['conflicts'] = $conflicts;
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $this->formatter->success( $data );
|
||||||
|
|
||||||
|
return $this->add_rate_limit_headers( $response, $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get room calendar.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function get_calendar( $request ) {
|
||||||
|
// Check rate limit.
|
||||||
|
$rate_limit_error = $this->check_rate_limit( $request );
|
||||||
|
if ( $rate_limit_error ) {
|
||||||
|
return $rate_limit_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
$room_id = $request->get_param( 'id' );
|
||||||
|
$year = $request->get_param( 'year' );
|
||||||
|
$month = $request->get_param( 'month' );
|
||||||
|
|
||||||
|
// Validate room exists.
|
||||||
|
$room = get_post( $room_id );
|
||||||
|
if ( ! $room || Room::POST_TYPE !== $room->post_type || 'publish' !== $room->post_status ) {
|
||||||
|
return $this->formatter->not_found( __( 'Room', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate month.
|
||||||
|
if ( $month < 1 || $month > 12 ) {
|
||||||
|
return $this->formatter->validation_error( 'month', __( 'Month must be between 1 and 12.', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = Availability::get_calendar_data( $room_id, $year, $month );
|
||||||
|
$response = $this->formatter->success( $data );
|
||||||
|
|
||||||
|
return $this->add_rate_limit_headers( $response, $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search available rooms.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Current request.
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error.
|
||||||
|
*/
|
||||||
|
public function search_availability( $request ) {
|
||||||
|
// Check rate limit.
|
||||||
|
$rate_limit_error = $this->check_rate_limit( $request );
|
||||||
|
if ( $rate_limit_error ) {
|
||||||
|
return $rate_limit_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
$check_in = $request->get_param( 'check_in' );
|
||||||
|
$check_out = $request->get_param( 'check_out' );
|
||||||
|
|
||||||
|
// Validate dates.
|
||||||
|
if ( ! $this->validate_date( $check_in ) ) {
|
||||||
|
return $this->formatter->validation_error( 'check_in', __( 'Invalid check-in date format. Use Y-m-d.', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
if ( ! $this->validate_date( $check_out ) ) {
|
||||||
|
return $this->formatter->validation_error( 'check_out', __( 'Invalid check-out date format. Use Y-m-d.', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
if ( $check_in >= $check_out ) {
|
||||||
|
return $this->formatter->validation_error( 'check_out', __( 'Check-out must be after check-in.', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build search args.
|
||||||
|
$search_args = array(
|
||||||
|
'check_in' => $check_in,
|
||||||
|
'check_out' => $check_out,
|
||||||
|
);
|
||||||
|
|
||||||
|
$guests = $request->get_param( 'guests' );
|
||||||
|
if ( $guests ) {
|
||||||
|
$search_args['guests'] = $guests;
|
||||||
|
}
|
||||||
|
|
||||||
|
$building_id = $request->get_param( 'building_id' );
|
||||||
|
if ( $building_id ) {
|
||||||
|
$search_args['building_id'] = $building_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$room_type = $request->get_param( 'room_type' );
|
||||||
|
if ( $room_type ) {
|
||||||
|
$search_args['room_type'] = $room_type;
|
||||||
|
}
|
||||||
|
|
||||||
|
$amenities = $request->get_param( 'amenities' );
|
||||||
|
if ( $amenities ) {
|
||||||
|
$search_args['amenities'] = array_map( 'trim', explode( ',', $amenities ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$price_min = $request->get_param( 'price_min' );
|
||||||
|
if ( $price_min ) {
|
||||||
|
$search_args['price_min'] = $price_min;
|
||||||
|
}
|
||||||
|
|
||||||
|
$price_max = $request->get_param( 'price_max' );
|
||||||
|
if ( $price_max ) {
|
||||||
|
$search_args['price_max'] = $price_max;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use existing Search class.
|
||||||
|
$results = Search::search( $search_args );
|
||||||
|
|
||||||
|
// Format response.
|
||||||
|
$items = array();
|
||||||
|
foreach ( $results['rooms'] as $room_data ) {
|
||||||
|
$items[] = array(
|
||||||
|
'room' => $room_data,
|
||||||
|
'availability' => array(
|
||||||
|
'available' => true,
|
||||||
|
'nights' => $room_data['nights'] ?? 0,
|
||||||
|
'total_price' => $room_data['stay_price'] ?? 0,
|
||||||
|
'formatted_price' => $room_data['stay_price_formatted'] ?? '',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = array(
|
||||||
|
'results' => $items,
|
||||||
|
'total' => $results['count'],
|
||||||
|
'filters_applied' => $search_args,
|
||||||
|
);
|
||||||
|
|
||||||
|
$response = $this->formatter->success( $data );
|
||||||
|
|
||||||
|
return $this->add_rate_limit_headers( $response, $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get conflicting bookings for a date range.
|
||||||
|
*
|
||||||
|
* @param int $room_id Room ID.
|
||||||
|
* @param string $check_in Check-in date.
|
||||||
|
* @param string $check_out Check-out date.
|
||||||
|
* @return array Conflicting bookings.
|
||||||
|
*/
|
||||||
|
private function get_conflicts( int $room_id, string $check_in, string $check_out ): array {
|
||||||
|
$bookings = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'meta_query' => array(
|
||||||
|
'relation' => 'AND',
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_room_id',
|
||||||
|
'value' => $room_id,
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_status',
|
||||||
|
'value' => 'cancelled',
|
||||||
|
'compare' => '!=',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_check_in',
|
||||||
|
'value' => $check_out,
|
||||||
|
'compare' => '<',
|
||||||
|
'type' => 'DATE',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_booking_check_out',
|
||||||
|
'value' => $check_in,
|
||||||
|
'compare' => '>',
|
||||||
|
'type' => 'DATE',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$conflicts = array();
|
||||||
|
foreach ( $bookings as $booking ) {
|
||||||
|
$conflicts[] = array(
|
||||||
|
'booking_id' => $booking->ID,
|
||||||
|
'reference' => $booking->post_title,
|
||||||
|
'check_in' => get_post_meta( $booking->ID, '_bnb_booking_check_in', true ),
|
||||||
|
'check_out' => get_post_meta( $booking->ID, '_bnb_booking_check_out', true ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $conflicts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare room data for response.
|
||||||
|
*
|
||||||
|
* @param \WP_Post $post Room post object.
|
||||||
|
* @param bool $full Include full details.
|
||||||
|
* @return array Room data.
|
||||||
|
*/
|
||||||
|
private function prepare_room_response( \WP_Post $post, bool $full = false ): array {
|
||||||
|
$data = $this->format_post_base( $post );
|
||||||
|
|
||||||
|
$data['permalink'] = get_permalink( $post->ID );
|
||||||
|
$data['featured_image'] = $this->format_featured_image( $post->ID );
|
||||||
|
|
||||||
|
// Gallery.
|
||||||
|
$gallery_ids = get_post_meta( $post->ID, '_bnb_room_gallery', true );
|
||||||
|
$gallery = array();
|
||||||
|
if ( $gallery_ids ) {
|
||||||
|
foreach ( explode( ',', $gallery_ids ) as $image_id ) {
|
||||||
|
$image = $this->format_image( (int) $image_id );
|
||||||
|
if ( $image ) {
|
||||||
|
$gallery[] = $image;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$data['gallery'] = $gallery;
|
||||||
|
|
||||||
|
// Building reference.
|
||||||
|
$building_id = get_post_meta( $post->ID, '_bnb_room_building_id', true );
|
||||||
|
$building = $building_id ? get_post( $building_id ) : null;
|
||||||
|
$data['building'] = $building ? array(
|
||||||
|
'id' => $building->ID,
|
||||||
|
'title' => get_the_title( $building ),
|
||||||
|
'slug' => $building->post_name,
|
||||||
|
'permalink' => get_permalink( $building->ID ),
|
||||||
|
'city' => get_post_meta( $building->ID, '_bnb_building_city', true ),
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
// Room details.
|
||||||
|
$data['room_number'] = get_post_meta( $post->ID, '_bnb_room_room_number', true );
|
||||||
|
$data['floor'] = (int) get_post_meta( $post->ID, '_bnb_room_floor', true );
|
||||||
|
$data['size_sqm'] = (float) get_post_meta( $post->ID, '_bnb_room_size', true );
|
||||||
|
|
||||||
|
// Capacity.
|
||||||
|
$data['capacity'] = array(
|
||||||
|
'max_guests' => (int) get_post_meta( $post->ID, '_bnb_room_capacity', true ),
|
||||||
|
'adults' => (int) get_post_meta( $post->ID, '_bnb_room_max_adults', true ),
|
||||||
|
'children' => (int) get_post_meta( $post->ID, '_bnb_room_max_children', true ),
|
||||||
|
);
|
||||||
|
|
||||||
|
$data['beds'] = get_post_meta( $post->ID, '_bnb_room_beds', true );
|
||||||
|
$data['bathrooms'] = (float) get_post_meta( $post->ID, '_bnb_room_bathrooms', true );
|
||||||
|
$data['status'] = get_post_meta( $post->ID, '_bnb_room_status', true ) ?: 'available';
|
||||||
|
|
||||||
|
// Room types.
|
||||||
|
$room_types = wp_get_post_terms( $post->ID, RoomType::TAXONOMY );
|
||||||
|
$data['room_types'] = array_map(
|
||||||
|
function ( $term ) {
|
||||||
|
return array(
|
||||||
|
'id' => $term->term_id,
|
||||||
|
'name' => $term->name,
|
||||||
|
'slug' => $term->slug,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
is_array( $room_types ) ? $room_types : array()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Amenities.
|
||||||
|
$amenities = wp_get_post_terms( $post->ID, Amenity::TAXONOMY );
|
||||||
|
$data['amenities'] = array_map(
|
||||||
|
function ( $term ) {
|
||||||
|
return array(
|
||||||
|
'id' => $term->term_id,
|
||||||
|
'name' => $term->name,
|
||||||
|
'slug' => $term->slug,
|
||||||
|
'icon' => get_term_meta( $term->term_id, '_bnb_amenity_icon', true ),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
is_array( $amenities ) ? $amenities : array()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Pricing.
|
||||||
|
$pricing = Calculator::getRoomPricing( $post->ID );
|
||||||
|
$currency = get_option( 'wp_bnb_currency', 'CHF' );
|
||||||
|
|
||||||
|
$data['pricing'] = array(
|
||||||
|
'currency' => $currency,
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ( PricingTier::cases() as $tier ) {
|
||||||
|
$price = $pricing[ $tier->value ]['price'] ?? null;
|
||||||
|
$data['pricing'][ $tier->value ] = array(
|
||||||
|
'price' => $price,
|
||||||
|
'formatted' => $price ? Calculator::formatPrice( $price ) : null,
|
||||||
|
'unit' => $tier->unit(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$weekend_surcharge = $pricing['weekend_surcharge']['price'] ?? null;
|
||||||
|
$data['pricing']['weekend_surcharge'] = array(
|
||||||
|
'price' => $weekend_surcharge,
|
||||||
|
'formatted' => $weekend_surcharge ? Calculator::formatPrice( $weekend_surcharge ) : null,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add HATEOAS links.
|
||||||
|
$data['_links'] = array(
|
||||||
|
'self' => array(
|
||||||
|
array( 'href' => rest_url( $this->namespace . '/rooms/' . $post->ID ) ),
|
||||||
|
),
|
||||||
|
'building' => $building ? array(
|
||||||
|
array( 'href' => rest_url( $this->namespace . '/buildings/' . $building->ID ) ),
|
||||||
|
) : array(),
|
||||||
|
'availability' => array(
|
||||||
|
array( 'href' => rest_url( $this->namespace . '/rooms/' . $post->ID . '/availability' ) ),
|
||||||
|
),
|
||||||
|
'calendar' => array(
|
||||||
|
array( 'href' => rest_url( $this->namespace . '/rooms/' . $post->ID . '/calendar' ) ),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get rooms collection parameters.
|
||||||
|
*
|
||||||
|
* @return array Collection parameters.
|
||||||
|
*/
|
||||||
|
private function get_rooms_collection_params(): array {
|
||||||
|
$params = $this->get_collection_params();
|
||||||
|
|
||||||
|
$params['building'] = array(
|
||||||
|
'description' => __( 'Filter by building ID.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
);
|
||||||
|
|
||||||
|
$params['room_type'] = array(
|
||||||
|
'description' => __( 'Filter by room type (term ID or slug).', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
);
|
||||||
|
|
||||||
|
$params['amenities'] = array(
|
||||||
|
'description' => __( 'Filter by amenities (comma-separated slugs).', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
);
|
||||||
|
|
||||||
|
$params['capacity_min'] = array(
|
||||||
|
'description' => __( 'Minimum guest capacity.', 'wp-bnb' ),
|
||||||
|
'type' => 'integer',
|
||||||
|
'minimum' => 1,
|
||||||
|
'sanitize_callback' => 'absint',
|
||||||
|
);
|
||||||
|
|
||||||
|
$params['status'] = array(
|
||||||
|
'description' => __( 'Filter by room status.', 'wp-bnb' ),
|
||||||
|
'type' => 'string',
|
||||||
|
'enum' => array( 'available', 'occupied', 'maintenance', 'blocked' ),
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
);
|
||||||
|
|
||||||
|
$params['orderby']['enum'] = array( 'title', 'date', 'capacity', 'price' );
|
||||||
|
|
||||||
|
return $params;
|
||||||
|
}
|
||||||
|
}
|
||||||
375
src/Api/Controllers/ServicesController.php
Normal file
375
src/Api/Controllers/ServicesController.php
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
<?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',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
194
src/Api/RateLimiter.php
Normal file
194
src/Api/RateLimiter.php
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* REST API Rate Limiter
|
||||||
|
*
|
||||||
|
* Provides transient-based rate limiting for API endpoints.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Api
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rate Limiter class.
|
||||||
|
*/
|
||||||
|
final class RateLimiter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transient prefix for rate limit data.
|
||||||
|
*/
|
||||||
|
private const TRANSIENT_PREFIX = 'wp_bnb_rate_';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rate limits per minute by endpoint type.
|
||||||
|
*
|
||||||
|
* @var array<string, int>
|
||||||
|
*/
|
||||||
|
private array $limits = array(
|
||||||
|
'public' => 60, // Public read endpoints.
|
||||||
|
'availability' => 30, // Availability checks.
|
||||||
|
'booking' => 10, // Booking creation.
|
||||||
|
'admin' => 120, // Admin endpoints.
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time window in seconds.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private int $window = 60;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if request is within rate limit.
|
||||||
|
*
|
||||||
|
* @param string $identifier Client identifier (user ID or IP).
|
||||||
|
* @param string $endpoint Request endpoint.
|
||||||
|
* @return bool True if within limit, false if exceeded.
|
||||||
|
*/
|
||||||
|
public function check( string $identifier, string $endpoint ): bool {
|
||||||
|
$type = $this->get_endpoint_type( $endpoint );
|
||||||
|
$limit = $this->limits[ $type ] ?? $this->limits['public'];
|
||||||
|
$key = $this->get_transient_key( $identifier, $type );
|
||||||
|
$data = get_transient( $key );
|
||||||
|
|
||||||
|
if ( false === $data ) {
|
||||||
|
// First request in window.
|
||||||
|
set_transient(
|
||||||
|
$key,
|
||||||
|
array(
|
||||||
|
'count' => 1,
|
||||||
|
'start' => time(),
|
||||||
|
),
|
||||||
|
$this->window
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if window expired.
|
||||||
|
if ( time() - $data['start'] >= $this->window ) {
|
||||||
|
set_transient(
|
||||||
|
$key,
|
||||||
|
array(
|
||||||
|
'count' => 1,
|
||||||
|
'start' => time(),
|
||||||
|
),
|
||||||
|
$this->window
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if limit exceeded.
|
||||||
|
if ( $data['count'] >= $limit ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment counter.
|
||||||
|
++$data['count'];
|
||||||
|
$remaining_window = $this->window - ( time() - $data['start'] );
|
||||||
|
set_transient( $key, $data, $remaining_window );
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get seconds until rate limit resets.
|
||||||
|
*
|
||||||
|
* @param string $identifier Client identifier.
|
||||||
|
* @param string $endpoint Request endpoint.
|
||||||
|
* @return int Seconds until reset.
|
||||||
|
*/
|
||||||
|
public function get_retry_after( string $identifier, string $endpoint ): int {
|
||||||
|
$type = $this->get_endpoint_type( $endpoint );
|
||||||
|
$key = $this->get_transient_key( $identifier, $type );
|
||||||
|
$data = get_transient( $key );
|
||||||
|
|
||||||
|
if ( false === $data ) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return max( 0, $this->window - ( time() - $data['start'] ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current rate limit info for headers.
|
||||||
|
*
|
||||||
|
* @param string $identifier Client identifier.
|
||||||
|
* @param string $endpoint Request endpoint.
|
||||||
|
* @return array{limit: int, remaining: int, reset: int}
|
||||||
|
*/
|
||||||
|
public function get_rate_limit_info( string $identifier, string $endpoint ): array {
|
||||||
|
$type = $this->get_endpoint_type( $endpoint );
|
||||||
|
$limit = $this->limits[ $type ] ?? $this->limits['public'];
|
||||||
|
$key = $this->get_transient_key( $identifier, $type );
|
||||||
|
$data = get_transient( $key );
|
||||||
|
|
||||||
|
if ( false === $data ) {
|
||||||
|
return array(
|
||||||
|
'limit' => $limit,
|
||||||
|
'remaining' => $limit,
|
||||||
|
'reset' => time() + $this->window,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$remaining = max( 0, $limit - $data['count'] );
|
||||||
|
$reset = $data['start'] + $this->window;
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'limit' => $limit,
|
||||||
|
'remaining' => $remaining,
|
||||||
|
'reset' => $reset,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine endpoint type from route.
|
||||||
|
*
|
||||||
|
* @param string $endpoint Request endpoint.
|
||||||
|
* @return string Endpoint type.
|
||||||
|
*/
|
||||||
|
private function get_endpoint_type( string $endpoint ): string {
|
||||||
|
if ( str_contains( $endpoint, '/availability' ) || str_contains( $endpoint, '/calendar' ) ) {
|
||||||
|
return 'availability';
|
||||||
|
}
|
||||||
|
if ( str_contains( $endpoint, '/bookings' ) ) {
|
||||||
|
return 'booking';
|
||||||
|
}
|
||||||
|
if ( str_contains( $endpoint, '/guests' ) ) {
|
||||||
|
return 'admin';
|
||||||
|
}
|
||||||
|
return 'public';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get transient key for rate limit data.
|
||||||
|
*
|
||||||
|
* @param string $identifier Client identifier.
|
||||||
|
* @param string $type Endpoint type.
|
||||||
|
* @return string Transient key.
|
||||||
|
*/
|
||||||
|
private function get_transient_key( string $identifier, string $type ): string {
|
||||||
|
return self::TRANSIENT_PREFIX . md5( $identifier . '_' . $type );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set custom rate limits.
|
||||||
|
*
|
||||||
|
* @param array<string, int> $limits Rate limits by type.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function set_limits( array $limits ): void {
|
||||||
|
$this->limits = array_merge( $this->limits, $limits );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set custom time window.
|
||||||
|
*
|
||||||
|
* @param int $window Window in seconds.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function set_window( int $window ): void {
|
||||||
|
$this->window = $window;
|
||||||
|
}
|
||||||
|
}
|
||||||
171
src/Api/ResponseFormatter.php
Normal file
171
src/Api/ResponseFormatter.php
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* REST API Response Formatter
|
||||||
|
*
|
||||||
|
* Provides standardized response formatting for all API endpoints.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Api
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Api;
|
||||||
|
|
||||||
|
use WP_REST_Response;
|
||||||
|
use WP_Error;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response Formatter class.
|
||||||
|
*/
|
||||||
|
final class ResponseFormatter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format successful response.
|
||||||
|
*
|
||||||
|
* @param mixed $data Response data.
|
||||||
|
* @param int $status HTTP status code.
|
||||||
|
* @return WP_REST_Response
|
||||||
|
*/
|
||||||
|
public function success( mixed $data, int $status = 200 ): WP_REST_Response {
|
||||||
|
return new WP_REST_Response( $data, $status );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format collection response with pagination headers.
|
||||||
|
*
|
||||||
|
* @param array $items Collection items.
|
||||||
|
* @param int $total Total number of items.
|
||||||
|
* @param int $page Current page number.
|
||||||
|
* @param int $per_page Items per page.
|
||||||
|
* @return WP_REST_Response
|
||||||
|
*/
|
||||||
|
public function collection( array $items, int $total, int $page, int $per_page ): WP_REST_Response {
|
||||||
|
$response = new WP_REST_Response( $items, 200 );
|
||||||
|
$max_pages = (int) ceil( $total / $per_page );
|
||||||
|
|
||||||
|
$response->header( 'X-WP-Total', (string) $total );
|
||||||
|
$response->header( 'X-WP-TotalPages', (string) $max_pages );
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format created response (201).
|
||||||
|
*
|
||||||
|
* @param mixed $data Response data.
|
||||||
|
* @param string $location Location header URL.
|
||||||
|
* @return WP_REST_Response
|
||||||
|
*/
|
||||||
|
public function created( mixed $data, string $location = '' ): WP_REST_Response {
|
||||||
|
$response = new WP_REST_Response( $data, 201 );
|
||||||
|
if ( $location ) {
|
||||||
|
$response->header( 'Location', $location );
|
||||||
|
}
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format no content response (204).
|
||||||
|
*
|
||||||
|
* @return WP_REST_Response
|
||||||
|
*/
|
||||||
|
public function no_content(): WP_REST_Response {
|
||||||
|
return new WP_REST_Response( null, 204 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create validation error.
|
||||||
|
*
|
||||||
|
* @param string $param Parameter name.
|
||||||
|
* @param string $message Error message.
|
||||||
|
* @return WP_Error
|
||||||
|
*/
|
||||||
|
public function validation_error( string $param, string $message ): WP_Error {
|
||||||
|
return new WP_Error(
|
||||||
|
'rest_invalid_param',
|
||||||
|
$message,
|
||||||
|
array(
|
||||||
|
'status' => 400,
|
||||||
|
'param' => $param,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create not found error.
|
||||||
|
*
|
||||||
|
* @param string $resource Resource name.
|
||||||
|
* @return WP_Error
|
||||||
|
*/
|
||||||
|
public function not_found( string $resource = 'Resource' ): WP_Error {
|
||||||
|
return new WP_Error(
|
||||||
|
'rest_not_found',
|
||||||
|
/* translators: %s: Resource name */
|
||||||
|
sprintf( __( '%s not found.', 'wp-bnb' ), $resource ),
|
||||||
|
array( 'status' => 404 )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create forbidden error.
|
||||||
|
*
|
||||||
|
* @param string $message Error message.
|
||||||
|
* @return WP_Error
|
||||||
|
*/
|
||||||
|
public function forbidden( string $message = '' ): WP_Error {
|
||||||
|
return new WP_Error(
|
||||||
|
'rest_forbidden',
|
||||||
|
$message ?: __( 'You do not have permission to access this resource.', 'wp-bnb' ),
|
||||||
|
array( 'status' => 403 )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create rate limit error.
|
||||||
|
*
|
||||||
|
* @param int $retry_after Seconds until rate limit resets.
|
||||||
|
* @return WP_Error
|
||||||
|
*/
|
||||||
|
public function rate_limit_error( int $retry_after = 60 ): WP_Error {
|
||||||
|
return new WP_Error(
|
||||||
|
'rest_rate_limit_exceeded',
|
||||||
|
__( 'Rate limit exceeded. Please try again later.', 'wp-bnb' ),
|
||||||
|
array(
|
||||||
|
'status' => 429,
|
||||||
|
'retry_after' => $retry_after,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create conflict error (e.g., booking conflict).
|
||||||
|
*
|
||||||
|
* @param string $message Error message.
|
||||||
|
* @param array $conflicts Conflicting resources.
|
||||||
|
* @return WP_Error
|
||||||
|
*/
|
||||||
|
public function conflict( string $message, array $conflicts = array() ): WP_Error {
|
||||||
|
return new WP_Error(
|
||||||
|
'rest_conflict',
|
||||||
|
$message,
|
||||||
|
array(
|
||||||
|
'status' => 409,
|
||||||
|
'conflicts' => $conflicts,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create internal server error.
|
||||||
|
*
|
||||||
|
* @param string $message Error message.
|
||||||
|
* @return WP_Error
|
||||||
|
*/
|
||||||
|
public function server_error( string $message = '' ): WP_Error {
|
||||||
|
return new WP_Error(
|
||||||
|
'rest_server_error',
|
||||||
|
$message ?: __( 'An internal server error occurred.', 'wp-bnb' ),
|
||||||
|
array( 'status' => 500 )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
113
src/Api/RestApi.php
Normal file
113
src/Api/RestApi.php
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* REST API Main Class
|
||||||
|
*
|
||||||
|
* Registers all REST API controllers and routes.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Api
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Api;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\Api\Controllers\BuildingsController;
|
||||||
|
use Magdev\WpBnb\Api\Controllers\RoomsController;
|
||||||
|
use Magdev\WpBnb\Api\Controllers\BookingsController;
|
||||||
|
use Magdev\WpBnb\Api\Controllers\GuestsController;
|
||||||
|
use Magdev\WpBnb\Api\Controllers\ServicesController;
|
||||||
|
use Magdev\WpBnb\Api\Controllers\PricingController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REST API class.
|
||||||
|
*/
|
||||||
|
final class RestApi {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API namespace.
|
||||||
|
*/
|
||||||
|
public const NAMESPACE = 'wp-bnb/v1';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API version.
|
||||||
|
*/
|
||||||
|
public const VERSION = '1.0.0';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller instances.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private array $controllers = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the REST API.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function init(): void {
|
||||||
|
// Check if API is enabled.
|
||||||
|
if ( 'yes' !== get_option( 'wp_bnb_api_enabled', 'yes' ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register all API routes.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function register_routes(): void {
|
||||||
|
$this->controllers = array(
|
||||||
|
new BuildingsController(),
|
||||||
|
new RoomsController(),
|
||||||
|
new BookingsController(),
|
||||||
|
new GuestsController(),
|
||||||
|
new ServicesController(),
|
||||||
|
new PricingController(),
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ( $this->controllers as $controller ) {
|
||||||
|
$controller->register_routes();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register API info endpoint.
|
||||||
|
register_rest_route(
|
||||||
|
self::NAMESPACE,
|
||||||
|
'/info',
|
||||||
|
array(
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => array( $this, 'get_api_info' ),
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get API information.
|
||||||
|
*
|
||||||
|
* @return \WP_REST_Response API info response.
|
||||||
|
*/
|
||||||
|
public function get_api_info(): \WP_REST_Response {
|
||||||
|
return new \WP_REST_Response(
|
||||||
|
array(
|
||||||
|
'name' => 'WP BnB REST API',
|
||||||
|
'version' => self::VERSION,
|
||||||
|
'namespace' => self::NAMESPACE,
|
||||||
|
'description' => __( 'REST API for WP BnB booking management.', 'wp-bnb' ),
|
||||||
|
'endpoints' => array(
|
||||||
|
'buildings' => rest_url( self::NAMESPACE . '/buildings' ),
|
||||||
|
'rooms' => rest_url( self::NAMESPACE . '/rooms' ),
|
||||||
|
'bookings' => rest_url( self::NAMESPACE . '/bookings' ),
|
||||||
|
'guests' => rest_url( self::NAMESPACE . '/guests' ),
|
||||||
|
'services' => rest_url( self::NAMESPACE . '/services' ),
|
||||||
|
'pricing' => rest_url( self::NAMESPACE . '/pricing' ),
|
||||||
|
'availability' => rest_url( self::NAMESPACE . '/availability' ),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
200
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
191
src/Plugin.php
191
src/Plugin.php
@@ -18,6 +18,7 @@ use Magdev\WpBnb\Booking\Availability;
|
|||||||
use Magdev\WpBnb\Booking\EmailNotifier;
|
use Magdev\WpBnb\Booking\EmailNotifier;
|
||||||
use Magdev\WpBnb\Frontend\Search;
|
use Magdev\WpBnb\Frontend\Search;
|
||||||
use Magdev\WpBnb\Frontend\Shortcodes;
|
use Magdev\WpBnb\Frontend\Shortcodes;
|
||||||
|
use Magdev\WpBnb\Api\RestApi;
|
||||||
use Magdev\WpBnb\Integration\CF7;
|
use Magdev\WpBnb\Integration\CF7;
|
||||||
use Magdev\WpBnb\Integration\Prometheus;
|
use Magdev\WpBnb\Integration\Prometheus;
|
||||||
use Magdev\WpBnb\Frontend\Widgets\AvailabilityCalendar;
|
use Magdev\WpBnb\Frontend\Widgets\AvailabilityCalendar;
|
||||||
@@ -146,6 +147,9 @@ final class Plugin {
|
|||||||
// Initialize Prometheus metrics integration.
|
// Initialize Prometheus metrics integration.
|
||||||
Prometheus::init();
|
Prometheus::init();
|
||||||
|
|
||||||
|
// Initialize REST API.
|
||||||
|
$this->init_rest_api();
|
||||||
|
|
||||||
// Initialize admin components.
|
// Initialize admin components.
|
||||||
if ( is_admin() ) {
|
if ( is_admin() ) {
|
||||||
$this->init_admin();
|
$this->init_admin();
|
||||||
@@ -170,6 +174,16 @@ final class Plugin {
|
|||||||
$updater->init();
|
$updater->init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the REST API.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function init_rest_api(): void {
|
||||||
|
$api = new RestApi();
|
||||||
|
$api->init();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize admin components.
|
* Initialize admin components.
|
||||||
*
|
*
|
||||||
@@ -618,6 +632,10 @@ final class Plugin {
|
|||||||
class="nav-tab <?php echo 'metrics' === $active_tab ? 'nav-tab-active' : ''; ?>">
|
class="nav-tab <?php echo 'metrics' === $active_tab ? 'nav-tab-active' : ''; ?>">
|
||||||
<?php esc_html_e( 'Metrics', 'wp-bnb' ); ?>
|
<?php esc_html_e( 'Metrics', 'wp-bnb' ); ?>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wp-bnb-settings&tab=api' ) ); ?>"
|
||||||
|
class="nav-tab <?php echo 'api' === $active_tab ? 'nav-tab-active' : ''; ?>">
|
||||||
|
<?php esc_html_e( 'API', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
@@ -635,6 +653,9 @@ final class Plugin {
|
|||||||
case 'metrics':
|
case 'metrics':
|
||||||
$this->render_metrics_settings();
|
$this->render_metrics_settings();
|
||||||
break;
|
break;
|
||||||
|
case 'api':
|
||||||
|
$this->render_api_settings();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
$this->render_general_settings();
|
$this->render_general_settings();
|
||||||
break;
|
break;
|
||||||
@@ -1433,6 +1454,157 @@ final class Plugin {
|
|||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render API settings tab.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function render_api_settings(): void {
|
||||||
|
$api_enabled = get_option( 'wp_bnb_api_enabled', 'yes' );
|
||||||
|
$rate_limiting = get_option( 'wp_bnb_api_rate_limiting', 'yes' );
|
||||||
|
$api_base_url = rest_url( RestApi::NAMESPACE );
|
||||||
|
?>
|
||||||
|
<form method="post" action="">
|
||||||
|
<?php wp_nonce_field( 'wp_bnb_save_settings', 'wp_bnb_settings_nonce' ); ?>
|
||||||
|
|
||||||
|
<h2><?php esc_html_e( 'REST API Settings', 'wp-bnb' ); ?></h2>
|
||||||
|
|
||||||
|
<table class="form-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php esc_html_e( 'Enable API', 'wp-bnb' ); ?></th>
|
||||||
|
<td>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="wp_bnb_api_enabled" value="yes" <?php checked( $api_enabled, 'yes' ); ?>>
|
||||||
|
<?php esc_html_e( 'Enable the REST API endpoints', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'When enabled, external applications can access room, availability, and booking data via the API.', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php esc_html_e( 'Rate Limiting', 'wp-bnb' ); ?></th>
|
||||||
|
<td>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="wp_bnb_api_rate_limiting" value="yes" <?php checked( $rate_limiting, 'yes' ); ?>>
|
||||||
|
<?php esc_html_e( 'Enable rate limiting', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'Limits API requests to prevent abuse. Recommended for production sites.', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2 style="margin-top: 30px;"><?php esc_html_e( 'API Information', 'wp-bnb' ); ?></h2>
|
||||||
|
|
||||||
|
<table class="form-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php esc_html_e( 'Base URL', 'wp-bnb' ); ?></th>
|
||||||
|
<td>
|
||||||
|
<code><?php echo esc_html( $api_base_url ); ?></code>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'All API endpoints are prefixed with this URL.', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php esc_html_e( 'API Version', 'wp-bnb' ); ?></th>
|
||||||
|
<td>
|
||||||
|
<code><?php echo esc_html( RestApi::VERSION ); ?></code>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php esc_html_e( 'Info Endpoint', 'wp-bnb' ); ?></th>
|
||||||
|
<td>
|
||||||
|
<code><?php echo esc_html( $api_base_url . '/info' ); ?></code>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'Returns API information and available endpoints.', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2 style="margin-top: 30px;"><?php esc_html_e( 'Available Endpoints', 'wp-bnb' ); ?></h2>
|
||||||
|
|
||||||
|
<h3><?php esc_html_e( 'Public Endpoints', 'wp-bnb' ); ?></h3>
|
||||||
|
<table class="widefat" style="margin-bottom: 20px;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><?php esc_html_e( 'Method', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Endpoint', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Description', 'wp-bnb' ); ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>GET</td><td><code>/buildings</code></td><td><?php esc_html_e( 'List all buildings', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td>GET</td><td><code>/buildings/{id}</code></td><td><?php esc_html_e( 'Get building details', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td>GET</td><td><code>/buildings/{id}/rooms</code></td><td><?php esc_html_e( 'List rooms in building', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td>GET</td><td><code>/rooms</code></td><td><?php esc_html_e( 'List/search rooms', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td>GET</td><td><code>/rooms/{id}</code></td><td><?php esc_html_e( 'Get room details', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td>GET</td><td><code>/rooms/{id}/availability</code></td><td><?php esc_html_e( 'Check room availability', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td>GET</td><td><code>/rooms/{id}/calendar</code></td><td><?php esc_html_e( 'Get room calendar', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td>POST</td><td><code>/availability/search</code></td><td><?php esc_html_e( 'Search available rooms', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td>GET</td><td><code>/services</code></td><td><?php esc_html_e( 'List services', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td>POST</td><td><code>/pricing/calculate</code></td><td><?php esc_html_e( 'Calculate booking price', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td>POST</td><td><code>/bookings</code></td><td><?php esc_html_e( 'Create booking (pending status)', 'wp-bnb' ); ?></td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3><?php esc_html_e( 'Admin Endpoints (Requires Authentication)', 'wp-bnb' ); ?></h3>
|
||||||
|
<table class="widefat" style="margin-bottom: 20px;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><?php esc_html_e( 'Method', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Endpoint', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Description', 'wp-bnb' ); ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>GET</td><td><code>/bookings</code></td><td><?php esc_html_e( 'List all bookings', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td>GET</td><td><code>/bookings/{id}</code></td><td><?php esc_html_e( 'Get booking details', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td>PATCH</td><td><code>/bookings/{id}</code></td><td><?php esc_html_e( 'Update booking', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td>DELETE</td><td><code>/bookings/{id}</code></td><td><?php esc_html_e( 'Cancel booking', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td>POST</td><td><code>/bookings/{id}/confirm</code></td><td><?php esc_html_e( 'Confirm booking', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td>POST</td><td><code>/bookings/{id}/check-in</code></td><td><?php esc_html_e( 'Check in guest', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td>POST</td><td><code>/bookings/{id}/check-out</code></td><td><?php esc_html_e( 'Check out guest', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td>GET</td><td><code>/guests</code></td><td><?php esc_html_e( 'List guests', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td>GET</td><td><code>/guests/{id}</code></td><td><?php esc_html_e( 'Get guest details', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td>GET</td><td><code>/guests/search</code></td><td><?php esc_html_e( 'Search guests', 'wp-bnb' ); ?></td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2 style="margin-top: 30px;"><?php esc_html_e( 'Authentication', 'wp-bnb' ); ?></h2>
|
||||||
|
<p><?php esc_html_e( 'Admin endpoints require authentication. Use one of the following methods:', 'wp-bnb' ); ?></p>
|
||||||
|
<ul style="list-style: disc; margin-left: 20px;">
|
||||||
|
<li><strong><?php esc_html_e( 'Application Passwords:', 'wp-bnb' ); ?></strong> <?php esc_html_e( 'Create one in Users > Your Profile. Use Basic Auth with username and app password.', 'wp-bnb' ); ?></li>
|
||||||
|
<li><strong><?php esc_html_e( 'Cookie + Nonce:', 'wp-bnb' ); ?></strong> <?php esc_html_e( 'For same-domain requests. Pass nonce in X-WP-Nonce header.', 'wp-bnb' ); ?></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2 style="margin-top: 30px;"><?php esc_html_e( 'Rate Limits', 'wp-bnb' ); ?></h2>
|
||||||
|
<table class="widefat" style="margin-bottom: 20px;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><?php esc_html_e( 'Type', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Limit', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Applies To', 'wp-bnb' ); ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td><?php esc_html_e( 'Public', 'wp-bnb' ); ?></td><td>60/min</td><td><?php esc_html_e( 'GET rooms, buildings, services', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td><?php esc_html_e( 'Availability', 'wp-bnb' ); ?></td><td>30/min</td><td><?php esc_html_e( 'Availability checks, calendar', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td><?php esc_html_e( 'Booking', 'wp-bnb' ); ?></td><td>10/min</td><td><?php esc_html_e( 'Booking creation', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td><?php esc_html_e( 'Admin', 'wp-bnb' ); ?></td><td>120/min</td><td><?php esc_html_e( 'All admin endpoints', 'wp-bnb' ); ?></td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p class="submit">
|
||||||
|
<?php submit_button( __( 'Save API Settings', 'wp-bnb' ), 'primary', 'submit', false ); ?>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render license status badge.
|
* Render license status badge.
|
||||||
*
|
*
|
||||||
@@ -1507,6 +1679,9 @@ final class Plugin {
|
|||||||
case 'metrics':
|
case 'metrics':
|
||||||
$this->save_metrics_settings();
|
$this->save_metrics_settings();
|
||||||
break;
|
break;
|
||||||
|
case 'api':
|
||||||
|
$this->save_api_settings();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
$this->save_general_settings();
|
$this->save_general_settings();
|
||||||
break;
|
break;
|
||||||
@@ -1649,6 +1824,22 @@ final class Plugin {
|
|||||||
settings_errors( 'wp_bnb_settings' );
|
settings_errors( 'wp_bnb_settings' );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save API settings.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function save_api_settings(): void {
|
||||||
|
$api_enabled = isset( $_POST['wp_bnb_api_enabled'] ) ? 'yes' : 'no';
|
||||||
|
$rate_limiting = isset( $_POST['wp_bnb_api_rate_limiting'] ) ? 'yes' : 'no';
|
||||||
|
|
||||||
|
update_option( 'wp_bnb_api_enabled', $api_enabled );
|
||||||
|
update_option( 'wp_bnb_api_rate_limiting', $rate_limiting );
|
||||||
|
|
||||||
|
add_settings_error( 'wp_bnb_settings', 'settings_saved', __( 'API settings saved.', 'wp-bnb' ), 'success' );
|
||||||
|
settings_errors( 'wp_bnb_settings' );
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AJAX handler for checking room availability.
|
* AJAX handler for checking room availability.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Plugin Name: WP BnB Management
|
* Plugin Name: WP BnB Management
|
||||||
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wp-bnb
|
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wp-bnb
|
||||||
* Description: A comprehensive Bed & Breakfast management system for WordPress. Manage buildings, rooms, bookings, and guests.
|
* Description: A comprehensive Bed & Breakfast management system for WordPress. Manage buildings, rooms, bookings, and guests.
|
||||||
* Version: 0.9.0
|
* Version: 0.10.0
|
||||||
* Requires at least: 6.0
|
* Requires at least: 6.0
|
||||||
* Requires PHP: 8.3
|
* Requires PHP: 8.3
|
||||||
* Author: Marco Graetsch
|
* Author: Marco Graetsch
|
||||||
@@ -24,7 +24,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Plugin version constant - MUST match Version in header above.
|
// Plugin version constant - MUST match Version in header above.
|
||||||
define( 'WP_BNB_VERSION', '0.9.0' );
|
define( 'WP_BNB_VERSION', '0.10.0' );
|
||||||
|
|
||||||
// Plugin path constants.
|
// Plugin path constants.
|
||||||
define( 'WP_BNB_PATH', plugin_dir_path( __FILE__ ) );
|
define( 'WP_BNB_PATH', plugin_dir_path( __FILE__ ) );
|
||||||
|
|||||||
Reference in New Issue
Block a user