Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f5adfb04e | |||
| b701d127f8 | |||
| 481495805b | |||
| 81c97c31d7 | |||
| 87aa89b1a6 | |||
| 13fd25f84c |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@ wp-plugins
|
|||||||
wp-core
|
wp-core
|
||||||
vendor/
|
vendor/
|
||||||
releases/*
|
releases/*
|
||||||
|
MARKETING.md
|
||||||
|
|||||||
109
CHANGELOG.md
109
CHANGELOG.md
@@ -5,6 +5,115 @@ 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.1] - 2026-02-03
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- API Settings subtabs for better organization:
|
||||||
|
- General subtab: Enable/disable REST API, rate limiting toggle, API information
|
||||||
|
- Rate Limits subtab: Configurable time window and endpoint-specific limits
|
||||||
|
- Endpoints subtab: Full endpoint documentation with HTTP method badges
|
||||||
|
- Configurable rate limiting:
|
||||||
|
- Time window setting (10-300 seconds, default 60)
|
||||||
|
- Per-endpoint-type limits (public, availability, booking, admin)
|
||||||
|
- Settings stored in WordPress options with fallback defaults
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- RateLimiter class now loads limits from WordPress options
|
||||||
|
- README updated with configurable rate limiting documentation
|
||||||
|
|
||||||
|
## [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
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Prometheus Metrics Integration:
|
||||||
|
- New `src/Integration/Prometheus.php` class for metrics collection
|
||||||
|
- Integration with wp-prometheus plugin via `wp_prometheus_collect_metrics` hook
|
||||||
|
- Inventory metrics: buildings total, rooms by status, services by status
|
||||||
|
- Booking metrics: bookings by status, check-ins/check-outs today, upcoming 7 days, avg duration
|
||||||
|
- Guest metrics: total guests, guests by status, repeat guests, new guests this month
|
||||||
|
- Occupancy metrics: current rate, monthly rate, occupied rooms, total bed capacity
|
||||||
|
- Revenue metrics: this month, YTD, average booking value, services revenue
|
||||||
|
- Grafana Dashboard:
|
||||||
|
- Pre-configured dashboard at `assets/grafana/wp-bnb-dashboard.json`
|
||||||
|
- Automatic registration with wp-prometheus dashboard provider
|
||||||
|
- Occupancy gauges with color-coded thresholds
|
||||||
|
- Pie charts for bookings, rooms, and guests by status
|
||||||
|
- Revenue and guest statistics panels
|
||||||
|
- Responsive grid layout with 24 panels
|
||||||
|
- Settings page Metrics tab:
|
||||||
|
- Enable/disable metrics collection toggle
|
||||||
|
- WP Prometheus detection with status indicator
|
||||||
|
- Complete metrics reference table
|
||||||
|
- Dashboard file location and export info
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Plugin.php updated to initialize Prometheus integration
|
||||||
|
- Settings page now has six tabs: General, Pricing, License, Updates, Metrics
|
||||||
|
|
||||||
## [0.8.0] - 2026-02-03
|
## [0.8.0] - 2026-02-03
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
57
CLAUDE.md
57
CLAUDE.md
@@ -935,3 +935,60 @@ Admin features always work; frontend requires valid license.
|
|||||||
- CSV export with BOM (`\xEF\xBB\xBF`) ensures Excel compatibility
|
- CSV export with BOM (`\xEF\xBB\xBF`) ensures Excel compatibility
|
||||||
- Guest data aggregation from bookings uses unique key pattern for anonymous guests
|
- Guest data aggregation from bookings uses unique key pattern for anonymous guests
|
||||||
- Occupancy calculation: (booked nights / total room nights) * 100
|
- Occupancy calculation: (booked nights / total room nights) * 100
|
||||||
|
|
||||||
|
### 2026-02-03 - Version 0.9.0 (Prometheus Metrics)
|
||||||
|
|
||||||
|
**Completed:**
|
||||||
|
|
||||||
|
- Created `src/Integration/Prometheus.php` class (~700 lines)
|
||||||
|
- Integration with wp-prometheus via `wp_prometheus_collect_metrics` hook
|
||||||
|
- Dashboard registration via `wp_prometheus_register_dashboards` hook
|
||||||
|
- Option to enable/disable metrics collection
|
||||||
|
- Inventory metrics: buildings total, rooms by status, services by status
|
||||||
|
- Booking metrics: by status, check-ins/outs today, upcoming 7 days, avg duration
|
||||||
|
- Guest metrics: total, by status, repeat guests, new this month
|
||||||
|
- Occupancy metrics: current rate, monthly rate, occupied rooms, bed capacity
|
||||||
|
- Revenue metrics: this month, YTD, avg booking value, services revenue
|
||||||
|
- Optimized SQL queries using `$wpdb->prepare()` throughout
|
||||||
|
- Created `assets/grafana/wp-bnb-dashboard.json` Grafana dashboard
|
||||||
|
- 24 panels with responsive grid layout
|
||||||
|
- Occupancy gauges with color-coded thresholds (red < 30%, orange < 50%, yellow < 70%, green ≥ 70%)
|
||||||
|
- Pie charts for bookings, rooms, and guests by status
|
||||||
|
- Revenue stat panels (this month, YTD, avg value, services)
|
||||||
|
- Guest stat panels (total, new, repeat, active services)
|
||||||
|
- Today's activity panels (check-ins, check-outs, upcoming)
|
||||||
|
- Prometheus datasource variable for flexibility
|
||||||
|
- Auto-refresh every 5 minutes
|
||||||
|
- Updated `src/Plugin.php`
|
||||||
|
- Added Prometheus class import
|
||||||
|
- Initialized Prometheus integration in `init_components()`
|
||||||
|
- Added "Metrics" tab to settings page (6 tabs total)
|
||||||
|
- Added `render_metrics_settings()` method with WP Prometheus detection
|
||||||
|
- Added `save_metrics_settings()` method
|
||||||
|
- Metrics reference table showing all available metrics
|
||||||
|
- Updated version to 0.9.0
|
||||||
|
|
||||||
|
**Files Created:**
|
||||||
|
|
||||||
|
- `src/Integration/Prometheus.php` - Prometheus metrics integration class
|
||||||
|
- `assets/grafana/wp-bnb-dashboard.json` - Pre-configured Grafana dashboard
|
||||||
|
|
||||||
|
**Files Changed:**
|
||||||
|
|
||||||
|
- `src/Plugin.php` - Prometheus initialization, metrics settings tab
|
||||||
|
- `wp-bnb.php` - Version bump to 0.9.0 (header and constant)
|
||||||
|
- `CHANGELOG.md` - Added v0.9.0 release notes
|
||||||
|
- `PLAN.md` - Marked Phase 9 as complete
|
||||||
|
- `README.md` - Added Prometheus metrics documentation
|
||||||
|
|
||||||
|
**Learnings:**
|
||||||
|
|
||||||
|
- wp-prometheus uses `wp_prometheus_collect_metrics` action with collector object
|
||||||
|
- Collector provides `register_gauge()` for fluctuating values
|
||||||
|
- Labels are passed as array to `register_gauge()`, values to `set()`
|
||||||
|
- Grafana dashboard JSON requires proper panel IDs and grid positions
|
||||||
|
- Occupancy queries need careful date range handling for month boundaries
|
||||||
|
- Revenue queries use `DECIMAL(10,2)` casting for accurate sums
|
||||||
|
- Metrics should be cached or computed efficiently as they're scraped frequently
|
||||||
|
- Dashboard registration requires file path, title, description, icon, and plugin name
|
||||||
|
- Settings tab detection uses `$prometheus_active` to show WP Prometheus status
|
||||||
|
|||||||
43
PLAN.md
43
PLAN.md
@@ -180,16 +180,35 @@ This document outlines the implementation plan for the WP BnB Management plugin.
|
|||||||
- [x] Guest statistics
|
- [x] Guest statistics
|
||||||
- [x] Export functionality (CSV, PDF)
|
- [x] Export functionality (CSV, PDF)
|
||||||
|
|
||||||
## Phase 9: Prometheus Metrics (v0.9.0)
|
## Phase 9: Prometheus Metrics (v0.9.0) - Complete
|
||||||
|
|
||||||
- [ ] Meanigful Metrics for this Plugin, see <https://src.bundespruefstelle.ch/magdev/wp-prometheus/raw/branch/main/README.md> for implementation details
|
- [x] Meaningful Metrics for this Plugin:
|
||||||
- [ ] Example Grafana-Dashboard, see <https://src.bundespruefstelle.ch/magdev/wp-prometheus/raw/branch/main/README.md> for implementation details
|
- Inventory: buildings, rooms by status, services by status
|
||||||
- [ ] Update settings page to enable/disable metrics
|
- Bookings: by status, check-ins/check-outs today, upcoming, avg duration
|
||||||
|
- Guests: total, by status, repeat guests, new this month
|
||||||
|
- Occupancy: current rate, monthly rate, occupied rooms, bed capacity
|
||||||
|
- Revenue: this month, YTD, average booking value, services revenue
|
||||||
|
- [x] Example Grafana Dashboard:
|
||||||
|
- Pre-configured dashboard JSON at `assets/grafana/wp-bnb-dashboard.json`
|
||||||
|
- Automatic registration with wp-prometheus
|
||||||
|
- 24 panels with gauges, pie charts, and stat displays
|
||||||
|
- [x] Update settings page to enable/disable metrics
|
||||||
|
|
||||||
## Phase 10: Security Audit (v0.10.0)
|
### Phase 10: API Endpoints (v0.10.0) - Complete
|
||||||
|
|
||||||
- [ ] Check for Wordpress best-practises
|
- [x] REST API for rooms (list, details, availability, calendar)
|
||||||
|
- [x] REST API for availability (search available rooms)
|
||||||
|
- [x] REST API for bookings (CRUD, status transitions)
|
||||||
|
- [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)
|
||||||
|
|
||||||
|
- [ ] 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+)
|
||||||
|
|
||||||
@@ -200,13 +219,6 @@ This document outlines the implementation plan for the WP BnB Management plugin.
|
|||||||
- [ ] Order management
|
- [ ] Order management
|
||||||
- [ ] Refund handling
|
- [ ] Refund handling
|
||||||
|
|
||||||
### API Endpoints
|
|
||||||
|
|
||||||
- [ ] REST API for rooms
|
|
||||||
- [ ] REST API for availability
|
|
||||||
- [ ] REST API for bookings
|
|
||||||
- [ ] Authentication and rate limiting
|
|
||||||
|
|
||||||
### Multi-language Support
|
### Multi-language Support
|
||||||
|
|
||||||
- [ ] Full translation support
|
- [ ] Full translation support
|
||||||
@@ -308,6 +320,7 @@ The plugin will provide extensive hooks for customization:
|
|||||||
| 0.6.0 | Frontend | Complete |
|
| 0.6.0 | Frontend | Complete |
|
||||||
| 0.7.0 | CF7 Integration | Complete |
|
| 0.7.0 | CF7 Integration | Complete |
|
||||||
| 0.8.0 | Dashboard | Complete |
|
| 0.8.0 | Dashboard | Complete |
|
||||||
| 0.9.0 | Prometheus Metrics | TBD |
|
| 0.9.0 | Prometheus Metrics | Complete |
|
||||||
| 0.10.0 | Security Audit | TBD |
|
| 0.10.0 | API Endpoints | TBD |
|
||||||
|
| 0.11.0 | Security Audit | TBD |
|
||||||
| 1.0.0 | Stable Release | TBD |
|
| 1.0.0 | Stable Release | TBD |
|
||||||
|
|||||||
269
README.md
269
README.md
@@ -21,6 +21,7 @@ WP BnB Management enables WordPress to act as a full management system for B&B h
|
|||||||
- **Contact Form 7 Integration**: Accept booking requests and inquiries through CF7 forms
|
- **Contact Form 7 Integration**: Accept booking requests and inquiries through CF7 forms
|
||||||
- **Dashboard**: Comprehensive admin dashboard with statistics and charts
|
- **Dashboard**: Comprehensive admin dashboard with statistics and charts
|
||||||
- **Reports**: Detailed reports with CSV and PDF export
|
- **Reports**: Detailed reports with CSV and PDF export
|
||||||
|
- **Prometheus Metrics**: Expose operational metrics for monitoring with Grafana
|
||||||
|
|
||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
@@ -383,6 +384,274 @@ add_action( 'wp_bnb_before_booking_create', function( $booking_data ) {
|
|||||||
} );
|
} );
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Prometheus Metrics
|
||||||
|
|
||||||
|
The plugin integrates with [WP Prometheus](https://src.bundespruefstelle.ch/magdev/wp-prometheus) to expose operational metrics for monitoring with Prometheus and Grafana.
|
||||||
|
|
||||||
|
### Enabling Metrics
|
||||||
|
|
||||||
|
1. Install and activate the WP Prometheus plugin
|
||||||
|
2. Navigate to **WP BnB → Settings → Metrics**
|
||||||
|
3. Enable "Expose BnB metrics via Prometheus"
|
||||||
|
4. Metrics will be available at your site's `/metrics/` endpoint
|
||||||
|
|
||||||
|
### Available Metrics
|
||||||
|
|
||||||
|
**Inventory Metrics:**
|
||||||
|
|
||||||
|
- `wp_bnb_buildings_total` - Total number of buildings
|
||||||
|
- `wp_bnb_rooms_total{status}` - Rooms by status (available, occupied, maintenance, inactive)
|
||||||
|
- `wp_bnb_services_total{status}` - Services by status (active, inactive)
|
||||||
|
- `wp_bnb_total_capacity_beds` - Total bed capacity across all rooms
|
||||||
|
|
||||||
|
**Booking Metrics:**
|
||||||
|
|
||||||
|
- `wp_bnb_bookings_total{status}` - Bookings by status (pending, confirmed, checked_in, checked_out, cancelled)
|
||||||
|
- `wp_bnb_checkins_today` - Check-ins scheduled for today
|
||||||
|
- `wp_bnb_checkouts_today` - Check-outs scheduled for today
|
||||||
|
- `wp_bnb_bookings_upcoming_7days` - Bookings starting in next 7 days
|
||||||
|
- `wp_bnb_booking_avg_duration_nights` - Average booking duration
|
||||||
|
|
||||||
|
**Occupancy Metrics:**
|
||||||
|
|
||||||
|
- `wp_bnb_occupancy_rate_current` - Current room occupancy rate (percentage)
|
||||||
|
- `wp_bnb_occupancy_rate_this_month` - Monthly occupancy rate (percentage)
|
||||||
|
- `wp_bnb_rooms_currently_occupied` - Rooms currently occupied
|
||||||
|
|
||||||
|
**Revenue Metrics:**
|
||||||
|
|
||||||
|
- `wp_bnb_revenue_this_month{currency}` - Revenue for current month
|
||||||
|
- `wp_bnb_revenue_ytd{currency}` - Revenue year to date
|
||||||
|
- `wp_bnb_booking_avg_value{currency}` - Average booking value
|
||||||
|
- `wp_bnb_services_revenue_this_month{currency}` - Services revenue this month
|
||||||
|
|
||||||
|
**Guest Metrics:**
|
||||||
|
|
||||||
|
- `wp_bnb_guests_total` - Total registered guests
|
||||||
|
- `wp_bnb_guests_by_status{status}` - Guests by status (active, blocked, vip)
|
||||||
|
- `wp_bnb_guests_repeat` - Guests with more than one booking
|
||||||
|
- `wp_bnb_guests_new_this_month` - New guests this month
|
||||||
|
|
||||||
|
### Grafana Dashboard
|
||||||
|
|
||||||
|
A pre-configured Grafana dashboard is included at `assets/grafana/wp-bnb-dashboard.json`. If WP Prometheus is installed, the dashboard is automatically registered and available for export.
|
||||||
|
|
||||||
|
The dashboard includes:
|
||||||
|
|
||||||
|
- Occupancy gauges with color-coded thresholds
|
||||||
|
- Bookings, rooms, and guests pie charts by status
|
||||||
|
- Revenue and guest statistics panels
|
||||||
|
- Today's check-ins/check-outs
|
||||||
|
- 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. In the **General** subtab, enable "Enable REST API"
|
||||||
|
3. Optionally enable rate limiting for protection against abuse
|
||||||
|
4. Configure rate limits in the **Rate Limits** subtab
|
||||||
|
5. View all available endpoints in the **Endpoints** subtab
|
||||||
|
|
||||||
|
### 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). Configure limits in **Settings → API → Rate Limits**.
|
||||||
|
|
||||||
|
**Default Limits:**
|
||||||
|
|
||||||
|
| Type | Default | 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 |
|
||||||
|
|
||||||
|
**Configuration Options:**
|
||||||
|
|
||||||
|
- **Time Window**: 10-300 seconds (default: 60 seconds)
|
||||||
|
- **Per-endpoint limits**: Customize for each endpoint type
|
||||||
|
- **Rate limiting toggle**: Enable/disable without losing settings
|
||||||
|
|
||||||
|
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?
|
||||||
|
|||||||
@@ -491,6 +491,37 @@
|
|||||||
height: 16px;
|
height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* API Method Badges */
|
||||||
|
.wp-bnb-method {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-method.get {
|
||||||
|
background: #e7f5e7;
|
||||||
|
color: #1e7e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-method.post {
|
||||||
|
background: #e7f0f5;
|
||||||
|
color: #1e5f7e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-method.patch {
|
||||||
|
background: #f5f0e7;
|
||||||
|
color: #7e5f1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-bnb-method.delete {
|
||||||
|
background: #f5e7e7;
|
||||||
|
color: #7e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
/* Form Tables */
|
/* Form Tables */
|
||||||
.form-table th {
|
.form-table th {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
|
|||||||
1580
assets/grafana/wp-bnb-dashboard.json
Normal file
1580
assets/grafana/wp-bnb-dashboard.json
Normal file
File diff suppressed because it is too large
Load Diff
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',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
232
src/Api/RateLimiter.php
Normal file
232
src/Api/RateLimiter.php
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
<?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_';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default rate limits per minute by endpoint type.
|
||||||
|
*
|
||||||
|
* @var array<string, int>
|
||||||
|
*/
|
||||||
|
private const DEFAULT_LIMITS = array(
|
||||||
|
'public' => 60, // Public read endpoints.
|
||||||
|
'availability' => 30, // Availability checks.
|
||||||
|
'booking' => 10, // Booking creation.
|
||||||
|
'admin' => 120, // Admin endpoints.
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rate limits per minute by endpoint type.
|
||||||
|
*
|
||||||
|
* @var array<string, int>
|
||||||
|
*/
|
||||||
|
private array $limits;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time window in seconds.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private int $window;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
$this->load_limits_from_options();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load rate limits from WordPress options.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function load_limits_from_options(): void {
|
||||||
|
$this->limits = array(
|
||||||
|
'public' => (int) get_option( 'wp_bnb_rate_limit_public', self::DEFAULT_LIMITS['public'] ),
|
||||||
|
'availability' => (int) get_option( 'wp_bnb_rate_limit_availability', self::DEFAULT_LIMITS['availability'] ),
|
||||||
|
'booking' => (int) get_option( 'wp_bnb_rate_limit_booking', self::DEFAULT_LIMITS['booking'] ),
|
||||||
|
'admin' => (int) get_option( 'wp_bnb_rate_limit_admin', self::DEFAULT_LIMITS['admin'] ),
|
||||||
|
);
|
||||||
|
$this->window = (int) get_option( 'wp_bnb_rate_limit_window', 60 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get default rate limits.
|
||||||
|
*
|
||||||
|
* @return array<string, int>
|
||||||
|
*/
|
||||||
|
public static function get_default_limits(): array {
|
||||||
|
return self::DEFAULT_LIMITS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
877
src/Integration/Prometheus.php
Normal file
877
src/Integration/Prometheus.php
Normal file
@@ -0,0 +1,877 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Prometheus Metrics Integration.
|
||||||
|
*
|
||||||
|
* Provides meaningful metrics for monitoring BnB operations.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Integration
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Integration;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\PostTypes\Booking;
|
||||||
|
use Magdev\WpBnb\PostTypes\Building;
|
||||||
|
use Magdev\WpBnb\PostTypes\Guest;
|
||||||
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
use Magdev\WpBnb\PostTypes\Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prometheus Metrics Integration class.
|
||||||
|
*
|
||||||
|
* Exposes BnB metrics via the wp-prometheus plugin.
|
||||||
|
*/
|
||||||
|
class Prometheus {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Option key for enabling metrics.
|
||||||
|
*/
|
||||||
|
public const OPTION_ENABLED = 'wp_bnb_metrics_enabled';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the Prometheus integration.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function init(): void {
|
||||||
|
// Only hook if metrics are enabled.
|
||||||
|
if ( ! self::is_enabled() ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hook into wp-prometheus collector.
|
||||||
|
add_action( 'wp_prometheus_collect_metrics', array( self::class, 'collect_metrics' ) );
|
||||||
|
|
||||||
|
// Register Grafana dashboard.
|
||||||
|
add_action( 'wp_prometheus_register_dashboards', array( self::class, 'register_dashboards' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if metrics collection is enabled.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function is_enabled(): bool {
|
||||||
|
return 'yes' === get_option( self::OPTION_ENABLED, 'yes' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable metrics collection.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function enable(): void {
|
||||||
|
update_option( self::OPTION_ENABLED, 'yes' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable metrics collection.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function disable(): void {
|
||||||
|
update_option( self::OPTION_ENABLED, 'no' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect and register all BnB metrics.
|
||||||
|
*
|
||||||
|
* @param object $collector The wp-prometheus collector instance.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function collect_metrics( $collector ): void {
|
||||||
|
self::collect_inventory_metrics( $collector );
|
||||||
|
self::collect_booking_metrics( $collector );
|
||||||
|
self::collect_guest_metrics( $collector );
|
||||||
|
self::collect_occupancy_metrics( $collector );
|
||||||
|
self::collect_revenue_metrics( $collector );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect inventory metrics (buildings, rooms, services).
|
||||||
|
*
|
||||||
|
* @param object $collector The wp-prometheus collector instance.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function collect_inventory_metrics( $collector ): void {
|
||||||
|
// Buildings total.
|
||||||
|
$buildings_total = wp_count_posts( Building::POST_TYPE );
|
||||||
|
$gauge = $collector->register_gauge(
|
||||||
|
'wp_bnb_buildings_total',
|
||||||
|
'Total number of buildings',
|
||||||
|
array()
|
||||||
|
);
|
||||||
|
$gauge->set( (int) $buildings_total->publish, array() );
|
||||||
|
|
||||||
|
// Rooms by status.
|
||||||
|
$rooms_gauge = $collector->register_gauge(
|
||||||
|
'wp_bnb_rooms_total',
|
||||||
|
'Total number of rooms by status',
|
||||||
|
array( 'status' )
|
||||||
|
);
|
||||||
|
|
||||||
|
$room_statuses = array( 'available', 'occupied', 'maintenance', 'inactive' );
|
||||||
|
foreach ( $room_statuses as $status ) {
|
||||||
|
$count = self::count_rooms_by_status( $status );
|
||||||
|
$rooms_gauge->set( $count, array( $status ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Services by status.
|
||||||
|
$services_gauge = $collector->register_gauge(
|
||||||
|
'wp_bnb_services_total',
|
||||||
|
'Total number of services by status',
|
||||||
|
array( 'status' )
|
||||||
|
);
|
||||||
|
|
||||||
|
$service_statuses = array( 'active', 'inactive' );
|
||||||
|
foreach ( $service_statuses as $status ) {
|
||||||
|
$count = self::count_services_by_status( $status );
|
||||||
|
$services_gauge->set( $count, array( $status ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect booking metrics.
|
||||||
|
*
|
||||||
|
* @param object $collector The wp-prometheus collector instance.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function collect_booking_metrics( $collector ): void {
|
||||||
|
// Bookings by status.
|
||||||
|
$bookings_gauge = $collector->register_gauge(
|
||||||
|
'wp_bnb_bookings_total',
|
||||||
|
'Total number of bookings by status',
|
||||||
|
array( 'status' )
|
||||||
|
);
|
||||||
|
|
||||||
|
$booking_statuses = array( 'pending', 'confirmed', 'checked_in', 'checked_out', 'cancelled' );
|
||||||
|
foreach ( $booking_statuses as $status ) {
|
||||||
|
$count = self::count_bookings_by_status( $status );
|
||||||
|
$bookings_gauge->set( $count, array( $status ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Today's check-ins.
|
||||||
|
$checkins_gauge = $collector->register_gauge(
|
||||||
|
'wp_bnb_checkins_today',
|
||||||
|
'Number of check-ins scheduled for today',
|
||||||
|
array()
|
||||||
|
);
|
||||||
|
$checkins_gauge->set( self::count_todays_checkins(), array() );
|
||||||
|
|
||||||
|
// Today's check-outs.
|
||||||
|
$checkouts_gauge = $collector->register_gauge(
|
||||||
|
'wp_bnb_checkouts_today',
|
||||||
|
'Number of check-outs scheduled for today',
|
||||||
|
array()
|
||||||
|
);
|
||||||
|
$checkouts_gauge->set( self::count_todays_checkouts(), array() );
|
||||||
|
|
||||||
|
// Upcoming bookings (next 7 days).
|
||||||
|
$upcoming_gauge = $collector->register_gauge(
|
||||||
|
'wp_bnb_bookings_upcoming_7days',
|
||||||
|
'Number of bookings starting in the next 7 days',
|
||||||
|
array()
|
||||||
|
);
|
||||||
|
$upcoming_gauge->set( self::count_upcoming_bookings( 7 ), array() );
|
||||||
|
|
||||||
|
// Average booking duration (nights).
|
||||||
|
$avg_duration = $collector->register_gauge(
|
||||||
|
'wp_bnb_booking_avg_duration_nights',
|
||||||
|
'Average booking duration in nights',
|
||||||
|
array()
|
||||||
|
);
|
||||||
|
$avg_duration->set( self::get_average_booking_duration(), array() );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect guest metrics.
|
||||||
|
*
|
||||||
|
* @param object $collector The wp-prometheus collector instance.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function collect_guest_metrics( $collector ): void {
|
||||||
|
// Total guests.
|
||||||
|
$guests_total = wp_count_posts( Guest::POST_TYPE );
|
||||||
|
$guests_gauge = $collector->register_gauge(
|
||||||
|
'wp_bnb_guests_total',
|
||||||
|
'Total number of registered guests',
|
||||||
|
array()
|
||||||
|
);
|
||||||
|
$guests_gauge->set( (int) $guests_total->publish, array() );
|
||||||
|
|
||||||
|
// Guests by status.
|
||||||
|
$guests_status_gauge = $collector->register_gauge(
|
||||||
|
'wp_bnb_guests_by_status',
|
||||||
|
'Number of guests by status',
|
||||||
|
array( 'status' )
|
||||||
|
);
|
||||||
|
|
||||||
|
$guest_statuses = array( 'active', 'blocked', 'vip' );
|
||||||
|
foreach ( $guest_statuses as $status ) {
|
||||||
|
$count = self::count_guests_by_status( $status );
|
||||||
|
$guests_status_gauge->set( $count, array( $status ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repeat guests (guests with more than one booking).
|
||||||
|
$repeat_gauge = $collector->register_gauge(
|
||||||
|
'wp_bnb_guests_repeat',
|
||||||
|
'Number of guests with more than one booking',
|
||||||
|
array()
|
||||||
|
);
|
||||||
|
$repeat_gauge->set( self::count_repeat_guests(), array() );
|
||||||
|
|
||||||
|
// New guests this month.
|
||||||
|
$new_guests_gauge = $collector->register_gauge(
|
||||||
|
'wp_bnb_guests_new_this_month',
|
||||||
|
'Number of new guests registered this month',
|
||||||
|
array()
|
||||||
|
);
|
||||||
|
$new_guests_gauge->set( self::count_new_guests_this_month(), array() );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect occupancy metrics.
|
||||||
|
*
|
||||||
|
* @param object $collector The wp-prometheus collector instance.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function collect_occupancy_metrics( $collector ): void {
|
||||||
|
// Current occupancy rate (percentage).
|
||||||
|
$occupancy_gauge = $collector->register_gauge(
|
||||||
|
'wp_bnb_occupancy_rate_current',
|
||||||
|
'Current room occupancy rate (percentage)',
|
||||||
|
array()
|
||||||
|
);
|
||||||
|
$occupancy_gauge->set( self::get_current_occupancy_rate(), array() );
|
||||||
|
|
||||||
|
// Occupancy rate this month.
|
||||||
|
$occupancy_month_gauge = $collector->register_gauge(
|
||||||
|
'wp_bnb_occupancy_rate_this_month',
|
||||||
|
'Room occupancy rate for the current month (percentage)',
|
||||||
|
array()
|
||||||
|
);
|
||||||
|
$occupancy_month_gauge->set( self::get_monthly_occupancy_rate(), array() );
|
||||||
|
|
||||||
|
// Rooms currently occupied.
|
||||||
|
$occupied_gauge = $collector->register_gauge(
|
||||||
|
'wp_bnb_rooms_currently_occupied',
|
||||||
|
'Number of rooms currently occupied',
|
||||||
|
array()
|
||||||
|
);
|
||||||
|
$occupied_gauge->set( self::count_currently_occupied_rooms(), array() );
|
||||||
|
|
||||||
|
// Total room capacity (beds).
|
||||||
|
$capacity_gauge = $collector->register_gauge(
|
||||||
|
'wp_bnb_total_capacity_beds',
|
||||||
|
'Total bed capacity across all rooms',
|
||||||
|
array()
|
||||||
|
);
|
||||||
|
$capacity_gauge->set( self::get_total_bed_capacity(), array() );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect revenue metrics.
|
||||||
|
*
|
||||||
|
* @param object $collector The wp-prometheus collector instance.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function collect_revenue_metrics( $collector ): void {
|
||||||
|
$currency = get_option( 'wp_bnb_currency', 'CHF' );
|
||||||
|
|
||||||
|
// Revenue this month.
|
||||||
|
$revenue_month_gauge = $collector->register_gauge(
|
||||||
|
'wp_bnb_revenue_this_month',
|
||||||
|
'Total revenue for the current month',
|
||||||
|
array( 'currency' )
|
||||||
|
);
|
||||||
|
$revenue_month_gauge->set( self::get_revenue_this_month(), array( $currency ) );
|
||||||
|
|
||||||
|
// Revenue year to date.
|
||||||
|
$revenue_ytd_gauge = $collector->register_gauge(
|
||||||
|
'wp_bnb_revenue_ytd',
|
||||||
|
'Total revenue year to date',
|
||||||
|
array( 'currency' )
|
||||||
|
);
|
||||||
|
$revenue_ytd_gauge->set( self::get_revenue_ytd(), array( $currency ) );
|
||||||
|
|
||||||
|
// Average booking value.
|
||||||
|
$avg_value_gauge = $collector->register_gauge(
|
||||||
|
'wp_bnb_booking_avg_value',
|
||||||
|
'Average booking value',
|
||||||
|
array( 'currency' )
|
||||||
|
);
|
||||||
|
$avg_value_gauge->set( self::get_average_booking_value(), array( $currency ) );
|
||||||
|
|
||||||
|
// Revenue from services this month.
|
||||||
|
$services_revenue_gauge = $collector->register_gauge(
|
||||||
|
'wp_bnb_services_revenue_this_month',
|
||||||
|
'Revenue from additional services this month',
|
||||||
|
array( 'currency' )
|
||||||
|
);
|
||||||
|
$services_revenue_gauge->set( self::get_services_revenue_this_month(), array( $currency ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register Grafana dashboards.
|
||||||
|
*
|
||||||
|
* @param object $provider The wp-prometheus dashboard provider instance.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function register_dashboards( $provider ): void {
|
||||||
|
$dashboard_file = WP_BNB_PATH . 'assets/grafana/wp-bnb-dashboard.json';
|
||||||
|
|
||||||
|
if ( file_exists( $dashboard_file ) ) {
|
||||||
|
$provider->register_dashboard(
|
||||||
|
'wp-bnb',
|
||||||
|
array(
|
||||||
|
'title' => __( 'WP BnB Dashboard', 'wp-bnb' ),
|
||||||
|
'description' => __( 'Monitor occupancy, bookings, revenue, and guest statistics for your B&B.', 'wp-bnb' ),
|
||||||
|
'icon' => 'dashicons-building',
|
||||||
|
'file' => $dashboard_file,
|
||||||
|
'plugin' => 'WP BnB Manager',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Helper Methods - Inventory
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count rooms by status.
|
||||||
|
*
|
||||||
|
* @param string $status Room status.
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private static function count_rooms_by_status( string $status ): int {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
return (int) $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT COUNT(DISTINCT p.ID)
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
|
||||||
|
WHERE p.post_type = %s
|
||||||
|
AND p.post_status = 'publish'
|
||||||
|
AND pm.meta_key = '_bnb_room_status'
|
||||||
|
AND pm.meta_value = %s",
|
||||||
|
Room::POST_TYPE,
|
||||||
|
$status
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count services by status.
|
||||||
|
*
|
||||||
|
* @param string $status Service status.
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private static function count_services_by_status( string $status ): int {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
return (int) $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT COUNT(DISTINCT p.ID)
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
|
||||||
|
WHERE p.post_type = %s
|
||||||
|
AND p.post_status = 'publish'
|
||||||
|
AND pm.meta_key = '_bnb_service_status'
|
||||||
|
AND pm.meta_value = %s",
|
||||||
|
Service::POST_TYPE,
|
||||||
|
$status
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Helper Methods - Bookings
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count bookings by status.
|
||||||
|
*
|
||||||
|
* @param string $status Booking status.
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private static function count_bookings_by_status( string $status ): int {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
return (int) $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT COUNT(DISTINCT p.ID)
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
|
||||||
|
WHERE p.post_type = %s
|
||||||
|
AND p.post_status = 'publish'
|
||||||
|
AND pm.meta_key = '_bnb_booking_status'
|
||||||
|
AND pm.meta_value = %s",
|
||||||
|
Booking::POST_TYPE,
|
||||||
|
$status
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count today's check-ins.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private static function count_todays_checkins(): int {
|
||||||
|
global $wpdb;
|
||||||
|
$today = gmdate( 'Y-m-d' );
|
||||||
|
|
||||||
|
return (int) $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT COUNT(DISTINCT p.ID)
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_date ON p.ID = pm_date.post_id
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
|
||||||
|
WHERE p.post_type = %s
|
||||||
|
AND p.post_status = 'publish'
|
||||||
|
AND pm_date.meta_key = '_bnb_booking_check_in'
|
||||||
|
AND pm_date.meta_value = %s
|
||||||
|
AND pm_status.meta_key = '_bnb_booking_status'
|
||||||
|
AND pm_status.meta_value IN ('confirmed', 'pending')",
|
||||||
|
Booking::POST_TYPE,
|
||||||
|
$today
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count today's check-outs.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private static function count_todays_checkouts(): int {
|
||||||
|
global $wpdb;
|
||||||
|
$today = gmdate( 'Y-m-d' );
|
||||||
|
|
||||||
|
return (int) $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT COUNT(DISTINCT p.ID)
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_date ON p.ID = pm_date.post_id
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
|
||||||
|
WHERE p.post_type = %s
|
||||||
|
AND p.post_status = 'publish'
|
||||||
|
AND pm_date.meta_key = '_bnb_booking_check_out'
|
||||||
|
AND pm_date.meta_value = %s
|
||||||
|
AND pm_status.meta_key = '_bnb_booking_status'
|
||||||
|
AND pm_status.meta_value = 'checked_in'",
|
||||||
|
Booking::POST_TYPE,
|
||||||
|
$today
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count upcoming bookings within given days.
|
||||||
|
*
|
||||||
|
* @param int $days Number of days to look ahead.
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private static function count_upcoming_bookings( int $days ): int {
|
||||||
|
global $wpdb;
|
||||||
|
$today = gmdate( 'Y-m-d' );
|
||||||
|
$end_date = gmdate( 'Y-m-d', strtotime( "+{$days} days" ) );
|
||||||
|
|
||||||
|
return (int) $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT COUNT(DISTINCT p.ID)
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_date ON p.ID = pm_date.post_id
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
|
||||||
|
WHERE p.post_type = %s
|
||||||
|
AND p.post_status = 'publish'
|
||||||
|
AND pm_date.meta_key = '_bnb_booking_check_in'
|
||||||
|
AND pm_date.meta_value >= %s
|
||||||
|
AND pm_date.meta_value <= %s
|
||||||
|
AND pm_status.meta_key = '_bnb_booking_status'
|
||||||
|
AND pm_status.meta_value IN ('confirmed', 'pending')",
|
||||||
|
Booking::POST_TYPE,
|
||||||
|
$today,
|
||||||
|
$end_date
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get average booking duration in nights.
|
||||||
|
*
|
||||||
|
* @return float
|
||||||
|
*/
|
||||||
|
private static function get_average_booking_duration(): float {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$result = $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT AVG(DATEDIFF(pm_out.meta_value, pm_in.meta_value))
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_in ON p.ID = pm_in.post_id
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_out ON p.ID = pm_out.post_id
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
|
||||||
|
WHERE p.post_type = %s
|
||||||
|
AND p.post_status = 'publish'
|
||||||
|
AND pm_in.meta_key = '_bnb_booking_check_in'
|
||||||
|
AND pm_out.meta_key = '_bnb_booking_check_out'
|
||||||
|
AND pm_status.meta_key = '_bnb_booking_status'
|
||||||
|
AND pm_status.meta_value NOT IN ('cancelled')",
|
||||||
|
Booking::POST_TYPE
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return round( (float) $result, 1 );
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Helper Methods - Guests
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count guests by status.
|
||||||
|
*
|
||||||
|
* @param string $status Guest status.
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private static function count_guests_by_status( string $status ): int {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
return (int) $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT COUNT(DISTINCT p.ID)
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
|
||||||
|
WHERE p.post_type = %s
|
||||||
|
AND p.post_status = 'publish'
|
||||||
|
AND pm.meta_key = '_bnb_guest_status'
|
||||||
|
AND pm.meta_value = %s",
|
||||||
|
Guest::POST_TYPE,
|
||||||
|
$status
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count repeat guests (more than one booking).
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private static function count_repeat_guests(): int {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
return (int) $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT COUNT(DISTINCT pm.meta_value)
|
||||||
|
FROM {$wpdb->postmeta} pm
|
||||||
|
INNER JOIN {$wpdb->posts} p ON pm.post_id = p.ID
|
||||||
|
WHERE p.post_type = %s
|
||||||
|
AND p.post_status = 'publish'
|
||||||
|
AND pm.meta_key = '_bnb_booking_guest_id'
|
||||||
|
AND pm.meta_value != ''
|
||||||
|
GROUP BY pm.meta_value
|
||||||
|
HAVING COUNT(*) > 1",
|
||||||
|
Booking::POST_TYPE
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count new guests registered this month.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private static function count_new_guests_this_month(): int {
|
||||||
|
global $wpdb;
|
||||||
|
$first_of_month = gmdate( 'Y-m-01 00:00:00' );
|
||||||
|
|
||||||
|
return (int) $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT COUNT(*)
|
||||||
|
FROM {$wpdb->posts}
|
||||||
|
WHERE post_type = %s
|
||||||
|
AND post_status = 'publish'
|
||||||
|
AND post_date >= %s",
|
||||||
|
Guest::POST_TYPE,
|
||||||
|
$first_of_month
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Helper Methods - Occupancy
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current occupancy rate (percentage of rooms occupied today).
|
||||||
|
*
|
||||||
|
* @return float
|
||||||
|
*/
|
||||||
|
private static function get_current_occupancy_rate(): float {
|
||||||
|
$total_rooms = self::count_available_rooms();
|
||||||
|
if ( $total_rooms <= 0 ) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$occupied = self::count_currently_occupied_rooms();
|
||||||
|
return round( ( $occupied / $total_rooms ) * 100, 1 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get monthly occupancy rate.
|
||||||
|
*
|
||||||
|
* @return float
|
||||||
|
*/
|
||||||
|
private static function get_monthly_occupancy_rate(): float {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$total_rooms = self::count_available_rooms();
|
||||||
|
if ( $total_rooms <= 0 ) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$first_of_month = gmdate( 'Y-m-01' );
|
||||||
|
$today = gmdate( 'Y-m-d' );
|
||||||
|
$days_so_far = (int) gmdate( 'd' );
|
||||||
|
$total_room_nights = $total_rooms * $days_so_far;
|
||||||
|
|
||||||
|
// Count booked nights this month (simplified: count bookings that overlap with this month).
|
||||||
|
$booked_nights = (int) $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT SUM(
|
||||||
|
DATEDIFF(
|
||||||
|
LEAST(pm_out.meta_value, %s),
|
||||||
|
GREATEST(pm_in.meta_value, %s)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_in ON p.ID = pm_in.post_id
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_out ON p.ID = pm_out.post_id
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
|
||||||
|
WHERE p.post_type = %s
|
||||||
|
AND p.post_status = 'publish'
|
||||||
|
AND pm_in.meta_key = '_bnb_booking_check_in'
|
||||||
|
AND pm_out.meta_key = '_bnb_booking_check_out'
|
||||||
|
AND pm_status.meta_key = '_bnb_booking_status'
|
||||||
|
AND pm_status.meta_value IN ('confirmed', 'checked_in', 'checked_out')
|
||||||
|
AND pm_in.meta_value <= %s
|
||||||
|
AND pm_out.meta_value >= %s",
|
||||||
|
$today,
|
||||||
|
$first_of_month,
|
||||||
|
Booking::POST_TYPE,
|
||||||
|
$today,
|
||||||
|
$first_of_month
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( $total_room_nights <= 0 ) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return round( ( $booked_nights / $total_room_nights ) * 100, 1 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count currently occupied rooms.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private static function count_currently_occupied_rooms(): int {
|
||||||
|
global $wpdb;
|
||||||
|
$today = gmdate( 'Y-m-d' );
|
||||||
|
|
||||||
|
return (int) $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT COUNT(DISTINCT pm_room.meta_value)
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_room ON p.ID = pm_room.post_id
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_in ON p.ID = pm_in.post_id
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_out ON p.ID = pm_out.post_id
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
|
||||||
|
WHERE p.post_type = %s
|
||||||
|
AND p.post_status = 'publish'
|
||||||
|
AND pm_room.meta_key = '_bnb_booking_room_id'
|
||||||
|
AND pm_in.meta_key = '_bnb_booking_check_in'
|
||||||
|
AND pm_out.meta_key = '_bnb_booking_check_out'
|
||||||
|
AND pm_status.meta_key = '_bnb_booking_status'
|
||||||
|
AND pm_status.meta_value IN ('confirmed', 'checked_in')
|
||||||
|
AND pm_in.meta_value <= %s
|
||||||
|
AND pm_out.meta_value > %s",
|
||||||
|
Booking::POST_TYPE,
|
||||||
|
$today,
|
||||||
|
$today
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count available rooms (not in maintenance/inactive).
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private static function count_available_rooms(): int {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
return (int) $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT COUNT(DISTINCT p.ID)
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = '_bnb_room_status'
|
||||||
|
WHERE p.post_type = %s
|
||||||
|
AND p.post_status = 'publish'
|
||||||
|
AND (pm.meta_value IS NULL OR pm.meta_value IN ('available', 'occupied'))",
|
||||||
|
Room::POST_TYPE
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get total bed capacity.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private static function get_total_bed_capacity(): int {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$result = $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT SUM(CAST(pm.meta_value AS UNSIGNED))
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
|
||||||
|
WHERE p.post_type = %s
|
||||||
|
AND p.post_status = 'publish'
|
||||||
|
AND pm.meta_key = '_bnb_room_beds'",
|
||||||
|
Room::POST_TYPE
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (int) $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Helper Methods - Revenue
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get revenue for the current month.
|
||||||
|
*
|
||||||
|
* @return float
|
||||||
|
*/
|
||||||
|
private static function get_revenue_this_month(): float {
|
||||||
|
global $wpdb;
|
||||||
|
$first_of_month = gmdate( 'Y-m-01' );
|
||||||
|
|
||||||
|
$result = $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT SUM(CAST(pm_price.meta_value AS DECIMAL(10,2)))
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_price ON p.ID = pm_price.post_id
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_in ON p.ID = pm_in.post_id
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
|
||||||
|
WHERE p.post_type = %s
|
||||||
|
AND p.post_status = 'publish'
|
||||||
|
AND pm_price.meta_key = '_bnb_booking_total_price'
|
||||||
|
AND pm_in.meta_key = '_bnb_booking_check_in'
|
||||||
|
AND pm_in.meta_value >= %s
|
||||||
|
AND pm_status.meta_key = '_bnb_booking_status'
|
||||||
|
AND pm_status.meta_value IN ('confirmed', 'checked_in', 'checked_out')",
|
||||||
|
Booking::POST_TYPE,
|
||||||
|
$first_of_month
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return round( (float) $result, 2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get revenue year to date.
|
||||||
|
*
|
||||||
|
* @return float
|
||||||
|
*/
|
||||||
|
private static function get_revenue_ytd(): float {
|
||||||
|
global $wpdb;
|
||||||
|
$first_of_year = gmdate( 'Y-01-01' );
|
||||||
|
|
||||||
|
$result = $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT SUM(CAST(pm_price.meta_value AS DECIMAL(10,2)))
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_price ON p.ID = pm_price.post_id
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_in ON p.ID = pm_in.post_id
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
|
||||||
|
WHERE p.post_type = %s
|
||||||
|
AND p.post_status = 'publish'
|
||||||
|
AND pm_price.meta_key = '_bnb_booking_total_price'
|
||||||
|
AND pm_in.meta_key = '_bnb_booking_check_in'
|
||||||
|
AND pm_in.meta_value >= %s
|
||||||
|
AND pm_status.meta_key = '_bnb_booking_status'
|
||||||
|
AND pm_status.meta_value IN ('confirmed', 'checked_in', 'checked_out')",
|
||||||
|
Booking::POST_TYPE,
|
||||||
|
$first_of_year
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return round( (float) $result, 2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get average booking value.
|
||||||
|
*
|
||||||
|
* @return float
|
||||||
|
*/
|
||||||
|
private static function get_average_booking_value(): float {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$result = $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT AVG(CAST(pm_price.meta_value AS DECIMAL(10,2)))
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_price ON p.ID = pm_price.post_id
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
|
||||||
|
WHERE p.post_type = %s
|
||||||
|
AND p.post_status = 'publish'
|
||||||
|
AND pm_price.meta_key = '_bnb_booking_total_price'
|
||||||
|
AND pm_status.meta_key = '_bnb_booking_status'
|
||||||
|
AND pm_status.meta_value IN ('confirmed', 'checked_in', 'checked_out')",
|
||||||
|
Booking::POST_TYPE
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return round( (float) $result, 2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get revenue from services this month.
|
||||||
|
*
|
||||||
|
* @return float
|
||||||
|
*/
|
||||||
|
private static function get_services_revenue_this_month(): float {
|
||||||
|
global $wpdb;
|
||||||
|
$first_of_month = gmdate( 'Y-m-01' );
|
||||||
|
|
||||||
|
$result = $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT SUM(CAST(pm_services.meta_value AS DECIMAL(10,2)))
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_services ON p.ID = pm_services.post_id
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_in ON p.ID = pm_in.post_id
|
||||||
|
INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
|
||||||
|
WHERE p.post_type = %s
|
||||||
|
AND p.post_status = 'publish'
|
||||||
|
AND pm_services.meta_key = '_bnb_booking_services_total'
|
||||||
|
AND pm_in.meta_key = '_bnb_booking_check_in'
|
||||||
|
AND pm_in.meta_value >= %s
|
||||||
|
AND pm_status.meta_key = '_bnb_booking_status'
|
||||||
|
AND pm_status.meta_value IN ('confirmed', 'checked_in', 'checked_out')",
|
||||||
|
Booking::POST_TYPE,
|
||||||
|
$first_of_month
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return round( (float) $result, 2 );
|
||||||
|
}
|
||||||
|
}
|
||||||
572
src/Plugin.php
572
src/Plugin.php
@@ -18,7 +18,9 @@ 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\Frontend\Widgets\AvailabilityCalendar;
|
use Magdev\WpBnb\Frontend\Widgets\AvailabilityCalendar;
|
||||||
use Magdev\WpBnb\Frontend\Widgets\BuildingRooms;
|
use Magdev\WpBnb\Frontend\Widgets\BuildingRooms;
|
||||||
use Magdev\WpBnb\Frontend\Widgets\SimilarRooms;
|
use Magdev\WpBnb\Frontend\Widgets\SimilarRooms;
|
||||||
@@ -142,6 +144,12 @@ final class Plugin {
|
|||||||
CF7::init();
|
CF7::init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize Prometheus metrics integration.
|
||||||
|
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();
|
||||||
@@ -166,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.
|
||||||
*
|
*
|
||||||
@@ -610,6 +628,14 @@ final class Plugin {
|
|||||||
class="nav-tab <?php echo 'updates' === $active_tab ? 'nav-tab-active' : ''; ?>">
|
class="nav-tab <?php echo 'updates' === $active_tab ? 'nav-tab-active' : ''; ?>">
|
||||||
<?php esc_html_e( 'Updates', 'wp-bnb' ); ?>
|
<?php esc_html_e( 'Updates', 'wp-bnb' ); ?>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wp-bnb-settings&tab=metrics' ) ); ?>"
|
||||||
|
class="nav-tab <?php echo 'metrics' === $active_tab ? 'nav-tab-active' : ''; ?>">
|
||||||
|
<?php esc_html_e( 'Metrics', 'wp-bnb' ); ?>
|
||||||
|
</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">
|
||||||
@@ -624,6 +650,12 @@ final class Plugin {
|
|||||||
case 'updates':
|
case 'updates':
|
||||||
$this->render_updates_settings();
|
$this->render_updates_settings();
|
||||||
break;
|
break;
|
||||||
|
case 'metrics':
|
||||||
|
$this->render_metrics_settings();
|
||||||
|
break;
|
||||||
|
case 'api':
|
||||||
|
$this->render_api_settings();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
$this->render_general_settings();
|
$this->render_general_settings();
|
||||||
break;
|
break;
|
||||||
@@ -1265,6 +1297,486 @@ final class Plugin {
|
|||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render metrics settings tab.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function render_metrics_settings(): void {
|
||||||
|
$metrics_enabled = Prometheus::is_enabled();
|
||||||
|
$prometheus_active = class_exists( '\Magdev\WpPrometheus\Plugin' ) || defined( 'WP_PROMETHEUS_VERSION' );
|
||||||
|
$dashboard_file = WP_BNB_PATH . 'assets/grafana/wp-bnb-dashboard.json';
|
||||||
|
$dashboard_available = file_exists( $dashboard_file );
|
||||||
|
?>
|
||||||
|
<form method="post" action="">
|
||||||
|
<?php wp_nonce_field( 'wp_bnb_save_settings', 'wp_bnb_settings_nonce' ); ?>
|
||||||
|
|
||||||
|
<h2><?php esc_html_e( 'Prometheus Metrics', 'wp-bnb' ); ?></h2>
|
||||||
|
|
||||||
|
<?php if ( ! $prometheus_active ) : ?>
|
||||||
|
<div class="notice notice-warning inline" style="margin: 0 0 20px 0;">
|
||||||
|
<p>
|
||||||
|
<span class="dashicons dashicons-warning" style="color: #dba617;"></span>
|
||||||
|
<strong><?php esc_html_e( 'WP Prometheus not detected', 'wp-bnb' ); ?></strong><br>
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: %s: Plugin URL */
|
||||||
|
esc_html__( 'The WP Prometheus plugin is required to expose metrics. Please install and activate it from %s.', 'wp-bnb' ),
|
||||||
|
'<a href="https://src.bundespruefstelle.ch/magdev/wp-prometheus" target="_blank">wp-prometheus</a>'
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<?php else : ?>
|
||||||
|
<div class="notice notice-success inline" style="margin: 0 0 20px 0;">
|
||||||
|
<p>
|
||||||
|
<span class="dashicons dashicons-yes-alt" style="color: #00a32a;"></span>
|
||||||
|
<strong><?php esc_html_e( 'WP Prometheus is active', 'wp-bnb' ); ?></strong>
|
||||||
|
<?php esc_html_e( 'Metrics will be exposed via the /metrics/ endpoint.', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<table class="form-table" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php esc_html_e( 'Enable Metrics', 'wp-bnb' ); ?></th>
|
||||||
|
<td>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="wp_bnb_metrics_enabled"
|
||||||
|
value="yes" <?php checked( $metrics_enabled ); ?>>
|
||||||
|
<?php esc_html_e( 'Expose BnB metrics via Prometheus', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'When enabled, occupancy, booking, revenue, and guest metrics will be available for Prometheus scraping.', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2><?php esc_html_e( 'Available Metrics', 'wp-bnb' ); ?></h2>
|
||||||
|
<p class="description"><?php esc_html_e( 'The following metrics are exposed when enabled:', 'wp-bnb' ); ?></p>
|
||||||
|
|
||||||
|
<table class="widefat striped" style="max-width: 800px; margin-top: 10px;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><?php esc_html_e( 'Metric', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Type', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Description', 'wp-bnb' ); ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>wp_bnb_buildings_total</code></td>
|
||||||
|
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
|
||||||
|
<td><?php esc_html_e( 'Total number of buildings', 'wp-bnb' ); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>wp_bnb_rooms_total</code></td>
|
||||||
|
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
|
||||||
|
<td><?php esc_html_e( 'Total rooms by status (available, occupied, maintenance, inactive)', 'wp-bnb' ); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>wp_bnb_bookings_total</code></td>
|
||||||
|
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
|
||||||
|
<td><?php esc_html_e( 'Total bookings by status (pending, confirmed, checked_in, checked_out, cancelled)', 'wp-bnb' ); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>wp_bnb_occupancy_rate_current</code></td>
|
||||||
|
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
|
||||||
|
<td><?php esc_html_e( 'Current room occupancy rate (percentage)', 'wp-bnb' ); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>wp_bnb_occupancy_rate_this_month</code></td>
|
||||||
|
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
|
||||||
|
<td><?php esc_html_e( 'Room occupancy rate for current month (percentage)', 'wp-bnb' ); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>wp_bnb_checkins_today</code></td>
|
||||||
|
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
|
||||||
|
<td><?php esc_html_e( 'Number of check-ins scheduled for today', 'wp-bnb' ); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>wp_bnb_checkouts_today</code></td>
|
||||||
|
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
|
||||||
|
<td><?php esc_html_e( 'Number of check-outs scheduled for today', 'wp-bnb' ); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>wp_bnb_revenue_this_month</code></td>
|
||||||
|
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
|
||||||
|
<td><?php esc_html_e( 'Total revenue for current month', 'wp-bnb' ); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>wp_bnb_revenue_ytd</code></td>
|
||||||
|
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
|
||||||
|
<td><?php esc_html_e( 'Total revenue year to date', 'wp-bnb' ); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>wp_bnb_booking_avg_value</code></td>
|
||||||
|
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
|
||||||
|
<td><?php esc_html_e( 'Average booking value', 'wp-bnb' ); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>wp_bnb_guests_total</code></td>
|
||||||
|
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
|
||||||
|
<td><?php esc_html_e( 'Total number of registered guests', 'wp-bnb' ); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>wp_bnb_guests_repeat</code></td>
|
||||||
|
<td><?php esc_html_e( 'Gauge', 'wp-bnb' ); ?></td>
|
||||||
|
<td><?php esc_html_e( 'Number of repeat guests (more than one booking)', 'wp-bnb' ); ?></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2 style="margin-top: 30px;"><?php esc_html_e( 'Grafana Dashboard', 'wp-bnb' ); ?></h2>
|
||||||
|
|
||||||
|
<?php if ( $dashboard_available ) : ?>
|
||||||
|
<p>
|
||||||
|
<?php esc_html_e( 'A pre-configured Grafana dashboard is available for visualizing WP BnB metrics.', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong><?php esc_html_e( 'Dashboard file:', 'wp-bnb' ); ?></strong>
|
||||||
|
<code>assets/grafana/wp-bnb-dashboard.json</code>
|
||||||
|
</p>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'If WP Prometheus is installed, this dashboard will be automatically registered and available for export in the Prometheus settings.', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
<?php else : ?>
|
||||||
|
<div class="notice notice-warning inline">
|
||||||
|
<p><?php esc_html_e( 'Grafana dashboard file not found.', 'wp-bnb' ); ?></p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<p class="submit">
|
||||||
|
<?php submit_button( __( 'Save Metrics Settings', 'wp-bnb' ), 'primary', 'submit', false ); ?>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render API settings tab.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function render_api_settings(): void {
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Subtab switching only.
|
||||||
|
$active_subtab = isset( $_GET['subtab'] ) ? sanitize_key( $_GET['subtab'] ) : 'general';
|
||||||
|
|
||||||
|
$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 );
|
||||||
|
|
||||||
|
// Rate limit values.
|
||||||
|
$defaults = \Magdev\WpBnb\Api\RateLimiter::get_default_limits();
|
||||||
|
$limit_public = get_option( 'wp_bnb_rate_limit_public', $defaults['public'] );
|
||||||
|
$limit_avail = get_option( 'wp_bnb_rate_limit_availability', $defaults['availability'] );
|
||||||
|
$limit_booking = get_option( 'wp_bnb_rate_limit_booking', $defaults['booking'] );
|
||||||
|
$limit_admin = get_option( 'wp_bnb_rate_limit_admin', $defaults['admin'] );
|
||||||
|
$limit_window = get_option( 'wp_bnb_rate_limit_window', 60 );
|
||||||
|
|
||||||
|
$base_url = admin_url( 'admin.php?page=wp-bnb-settings&tab=api' );
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- API Subtabs -->
|
||||||
|
<div class="wp-bnb-subtabs">
|
||||||
|
<a href="<?php echo esc_url( $base_url . '&subtab=general' ); ?>"
|
||||||
|
class="wp-bnb-subtab <?php echo 'general' === $active_subtab ? 'active' : ''; ?>">
|
||||||
|
<span class="dashicons dashicons-admin-generic"></span>
|
||||||
|
<?php esc_html_e( 'General', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo esc_url( $base_url . '&subtab=rate-limits' ); ?>"
|
||||||
|
class="wp-bnb-subtab <?php echo 'rate-limits' === $active_subtab ? 'active' : ''; ?>">
|
||||||
|
<span class="dashicons dashicons-dashboard"></span>
|
||||||
|
<?php esc_html_e( 'Rate Limits', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo esc_url( $base_url . '&subtab=endpoints' ); ?>"
|
||||||
|
class="wp-bnb-subtab <?php echo 'endpoints' === $active_subtab ? 'active' : ''; ?>">
|
||||||
|
<span class="dashicons dashicons-rest-api"></span>
|
||||||
|
<?php esc_html_e( 'Endpoints', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post" action="">
|
||||||
|
<?php wp_nonce_field( 'wp_bnb_save_settings', 'wp_bnb_settings_nonce' ); ?>
|
||||||
|
|
||||||
|
<?php if ( 'general' === $active_subtab ) : ?>
|
||||||
|
<!-- General Subtab -->
|
||||||
|
<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' ); ?>
|
||||||
|
<?php if ( 'yes' === $rate_limiting ) : ?>
|
||||||
|
<a href="<?php echo esc_url( $base_url . '&subtab=rate-limits' ); ?>"><?php esc_html_e( 'Configure limits', 'wp-bnb' ); ?> →</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<?php submit_button( __( 'Save Settings', 'wp-bnb' ) ); ?>
|
||||||
|
|
||||||
|
<?php elseif ( 'rate-limits' === $active_subtab ) : ?>
|
||||||
|
<!-- Rate Limits Subtab -->
|
||||||
|
<h2><?php esc_html_e( 'Rate Limit Configuration', 'wp-bnb' ); ?></h2>
|
||||||
|
|
||||||
|
<?php if ( 'yes' !== $rate_limiting ) : ?>
|
||||||
|
<div class="notice notice-warning inline" style="margin: 15px 0;">
|
||||||
|
<p>
|
||||||
|
<?php esc_html_e( 'Rate limiting is currently disabled.', 'wp-bnb' ); ?>
|
||||||
|
<a href="<?php echo esc_url( $base_url . '&subtab=general' ); ?>"><?php esc_html_e( 'Enable it in General settings', 'wp-bnb' ); ?></a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<p class="description"><?php esc_html_e( 'Configure the number of requests allowed per time window for each endpoint type.', 'wp-bnb' ); ?></p>
|
||||||
|
|
||||||
|
<table class="form-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php esc_html_e( 'Time Window', 'wp-bnb' ); ?></th>
|
||||||
|
<td>
|
||||||
|
<input type="number" name="wp_bnb_rate_limit_window" value="<?php echo esc_attr( $limit_window ); ?>" min="10" max="300" step="10" class="small-text">
|
||||||
|
<?php esc_html_e( 'seconds', 'wp-bnb' ); ?>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'The time window for rate limit counting. Default: 60 seconds.', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php esc_html_e( 'Public Endpoints', 'wp-bnb' ); ?></th>
|
||||||
|
<td>
|
||||||
|
<input type="number" name="wp_bnb_rate_limit_public" value="<?php echo esc_attr( $limit_public ); ?>" min="1" max="1000" class="small-text">
|
||||||
|
<?php esc_html_e( 'requests per window', 'wp-bnb' ); ?>
|
||||||
|
<p class="description">
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: %d: default limit */
|
||||||
|
esc_html__( 'Limit for public read endpoints (rooms, buildings, services). Default: %d', 'wp-bnb' ),
|
||||||
|
$defaults['public']
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php esc_html_e( 'Availability Endpoints', 'wp-bnb' ); ?></th>
|
||||||
|
<td>
|
||||||
|
<input type="number" name="wp_bnb_rate_limit_availability" value="<?php echo esc_attr( $limit_avail ); ?>" min="1" max="1000" class="small-text">
|
||||||
|
<?php esc_html_e( 'requests per window', 'wp-bnb' ); ?>
|
||||||
|
<p class="description">
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: %d: default limit */
|
||||||
|
esc_html__( 'Limit for availability checks and calendar requests. Default: %d', 'wp-bnb' ),
|
||||||
|
$defaults['availability']
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php esc_html_e( 'Booking Endpoints', 'wp-bnb' ); ?></th>
|
||||||
|
<td>
|
||||||
|
<input type="number" name="wp_bnb_rate_limit_booking" value="<?php echo esc_attr( $limit_booking ); ?>" min="1" max="100" class="small-text">
|
||||||
|
<?php esc_html_e( 'requests per window', 'wp-bnb' ); ?>
|
||||||
|
<p class="description">
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: %d: default limit */
|
||||||
|
esc_html__( 'Limit for booking creation. Keep low to prevent abuse. Default: %d', 'wp-bnb' ),
|
||||||
|
$defaults['booking']
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php esc_html_e( 'Admin Endpoints', 'wp-bnb' ); ?></th>
|
||||||
|
<td>
|
||||||
|
<input type="number" name="wp_bnb_rate_limit_admin" value="<?php echo esc_attr( $limit_admin ); ?>" min="1" max="1000" class="small-text">
|
||||||
|
<?php esc_html_e( 'requests per window', 'wp-bnb' ); ?>
|
||||||
|
<p class="description">
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: %d: default limit */
|
||||||
|
esc_html__( 'Limit for authenticated admin endpoints. Default: %d', 'wp-bnb' ),
|
||||||
|
$defaults['admin']
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2 style="margin-top: 30px;"><?php esc_html_e( 'Current Rate Limits Summary', 'wp-bnb' ); ?></h2>
|
||||||
|
<table class="widefat striped" style="max-width: 600px;">
|
||||||
|
<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><strong><?php echo esc_html( $limit_public . '/' . $limit_window . 's' ); ?></strong></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><strong><?php echo esc_html( $limit_avail . '/' . $limit_window . 's' ); ?></strong></td>
|
||||||
|
<td><?php esc_html_e( 'Availability checks, calendar', 'wp-bnb' ); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><?php esc_html_e( 'Booking', 'wp-bnb' ); ?></td>
|
||||||
|
<td><strong><?php echo esc_html( $limit_booking . '/' . $limit_window . 's' ); ?></strong></td>
|
||||||
|
<td><?php esc_html_e( 'Booking creation', 'wp-bnb' ); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><?php esc_html_e( 'Admin', 'wp-bnb' ); ?></td>
|
||||||
|
<td><strong><?php echo esc_html( $limit_admin . '/' . $limit_window . 's' ); ?></strong></td>
|
||||||
|
<td><?php esc_html_e( 'All admin endpoints', 'wp-bnb' ); ?></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<?php submit_button( __( 'Save Rate Limits', 'wp-bnb' ) ); ?>
|
||||||
|
|
||||||
|
<?php else : ?>
|
||||||
|
<!-- Endpoints Subtab -->
|
||||||
|
<h2><?php esc_html_e( 'Available Endpoints', 'wp-bnb' ); ?></h2>
|
||||||
|
|
||||||
|
<h3><?php esc_html_e( 'Public Endpoints', 'wp-bnb' ); ?></h3>
|
||||||
|
<p class="description"><?php esc_html_e( 'These endpoints are accessible without authentication.', 'wp-bnb' ); ?></p>
|
||||||
|
<table class="widefat striped" style="margin-bottom: 20px;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 80px;"><?php esc_html_e( 'Method', 'wp-bnb' ); ?></th>
|
||||||
|
<th style="width: 250px;"><?php esc_html_e( 'Endpoint', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Description', 'wp-bnb' ); ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td><span class="wp-bnb-method get">GET</span></td><td><code>/buildings</code></td><td><?php esc_html_e( 'List all buildings', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td><span class="wp-bnb-method get">GET</span></td><td><code>/buildings/{id}</code></td><td><?php esc_html_e( 'Get building details', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td><span class="wp-bnb-method get">GET</span></td><td><code>/buildings/{id}/rooms</code></td><td><?php esc_html_e( 'List rooms in building', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td><span class="wp-bnb-method get">GET</span></td><td><code>/rooms</code></td><td><?php esc_html_e( 'List/search rooms', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td><span class="wp-bnb-method get">GET</span></td><td><code>/rooms/{id}</code></td><td><?php esc_html_e( 'Get room details', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td><span class="wp-bnb-method get">GET</span></td><td><code>/rooms/{id}/availability</code></td><td><?php esc_html_e( 'Check room availability', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td><span class="wp-bnb-method get">GET</span></td><td><code>/rooms/{id}/calendar</code></td><td><?php esc_html_e( 'Get room calendar', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td><span class="wp-bnb-method post">POST</span></td><td><code>/availability/search</code></td><td><?php esc_html_e( 'Search available rooms', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td><span class="wp-bnb-method get">GET</span></td><td><code>/services</code></td><td><?php esc_html_e( 'List services', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td><span class="wp-bnb-method post">POST</span></td><td><code>/pricing/calculate</code></td><td><?php esc_html_e( 'Calculate booking price', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td><span class="wp-bnb-method post">POST</span></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', 'wp-bnb' ); ?></h3>
|
||||||
|
<p class="description"><?php esc_html_e( 'These endpoints require authentication (Application Password or Cookie + Nonce).', 'wp-bnb' ); ?></p>
|
||||||
|
<table class="widefat striped" style="margin-bottom: 20px;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 80px;"><?php esc_html_e( 'Method', 'wp-bnb' ); ?></th>
|
||||||
|
<th style="width: 250px;"><?php esc_html_e( 'Endpoint', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Description', 'wp-bnb' ); ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td><span class="wp-bnb-method get">GET</span></td><td><code>/bookings</code></td><td><?php esc_html_e( 'List all bookings', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td><span class="wp-bnb-method get">GET</span></td><td><code>/bookings/{id}</code></td><td><?php esc_html_e( 'Get booking details', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td><span class="wp-bnb-method patch">PATCH</span></td><td><code>/bookings/{id}</code></td><td><?php esc_html_e( 'Update booking', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td><span class="wp-bnb-method delete">DELETE</span></td><td><code>/bookings/{id}</code></td><td><?php esc_html_e( 'Cancel booking', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td><span class="wp-bnb-method post">POST</span></td><td><code>/bookings/{id}/confirm</code></td><td><?php esc_html_e( 'Confirm booking', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td><span class="wp-bnb-method post">POST</span></td><td><code>/bookings/{id}/check-in</code></td><td><?php esc_html_e( 'Check in guest', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td><span class="wp-bnb-method post">POST</span></td><td><code>/bookings/{id}/check-out</code></td><td><?php esc_html_e( 'Check out guest', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td><span class="wp-bnb-method get">GET</span></td><td><code>/guests</code></td><td><?php esc_html_e( 'List guests', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td><span class="wp-bnb-method get">GET</span></td><td><code>/guests/{id}</code></td><td><?php esc_html_e( 'Get guest details', 'wp-bnb' ); ?></td></tr>
|
||||||
|
<tr><td><span class="wp-bnb-method get">GET</span></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>
|
||||||
|
|
||||||
|
<table class="form-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<span class="dashicons dashicons-admin-network" style="color: #2271b1;"></span>
|
||||||
|
<?php esc_html_e( 'Application Passwords', 'wp-bnb' ); ?>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<p><?php esc_html_e( 'Create an Application Password in Users > Your Profile.', 'wp-bnb' ); ?></p>
|
||||||
|
<p><code>Authorization: Basic base64(username:app-password)</code></p>
|
||||||
|
<p class="description"><?php esc_html_e( 'Recommended for external applications and integrations.', 'wp-bnb' ); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<span class="dashicons dashicons-lock" style="color: #2271b1;"></span>
|
||||||
|
<?php esc_html_e( 'Cookie + Nonce', 'wp-bnb' ); ?>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<p><?php esc_html_e( 'For same-domain JavaScript requests when user is logged in.', 'wp-bnb' ); ?></p>
|
||||||
|
<p><code>X-WP-Nonce: <?php echo esc_html( wp_create_nonce( 'wp_rest' ) ); ?></code></p>
|
||||||
|
<p class="description"><?php esc_html_e( 'Best for frontend JavaScript that interacts with the API.', 'wp-bnb' ); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<?php endif; ?>
|
||||||
|
</form>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render license status badge.
|
* Render license status badge.
|
||||||
*
|
*
|
||||||
@@ -1336,6 +1848,12 @@ final class Plugin {
|
|||||||
case 'updates':
|
case 'updates':
|
||||||
$this->save_updates_settings();
|
$this->save_updates_settings();
|
||||||
break;
|
break;
|
||||||
|
case 'metrics':
|
||||||
|
$this->save_metrics_settings();
|
||||||
|
break;
|
||||||
|
case 'api':
|
||||||
|
$this->save_api_settings();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
$this->save_general_settings();
|
$this->save_general_settings();
|
||||||
break;
|
break;
|
||||||
@@ -1465,6 +1983,60 @@ final class Plugin {
|
|||||||
settings_errors( 'wp_bnb_settings' );
|
settings_errors( 'wp_bnb_settings' );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save metrics settings.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function save_metrics_settings(): void {
|
||||||
|
$metrics_enabled = isset( $_POST['wp_bnb_metrics_enabled'] ) ? 'yes' : 'no';
|
||||||
|
update_option( Prometheus::OPTION_ENABLED, $metrics_enabled );
|
||||||
|
|
||||||
|
add_settings_error( 'wp_bnb_settings', 'settings_saved', __( 'Metrics settings saved.', 'wp-bnb' ), 'success' );
|
||||||
|
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 );
|
||||||
|
|
||||||
|
// Save rate limit configuration.
|
||||||
|
$defaults = \Magdev\WpBnb\Api\RateLimiter::get_default_limits();
|
||||||
|
|
||||||
|
$limit_window = isset( $_POST['wp_bnb_rate_limit_window'] )
|
||||||
|
? max( 10, min( 300, absint( $_POST['wp_bnb_rate_limit_window'] ) ) )
|
||||||
|
: 60;
|
||||||
|
$limit_public = isset( $_POST['wp_bnb_rate_limit_public'] )
|
||||||
|
? max( 1, min( 1000, absint( $_POST['wp_bnb_rate_limit_public'] ) ) )
|
||||||
|
: $defaults['public'];
|
||||||
|
$limit_avail = isset( $_POST['wp_bnb_rate_limit_availability'] )
|
||||||
|
? max( 1, min( 1000, absint( $_POST['wp_bnb_rate_limit_availability'] ) ) )
|
||||||
|
: $defaults['availability'];
|
||||||
|
$limit_booking = isset( $_POST['wp_bnb_rate_limit_booking'] )
|
||||||
|
? max( 1, min( 100, absint( $_POST['wp_bnb_rate_limit_booking'] ) ) )
|
||||||
|
: $defaults['booking'];
|
||||||
|
$limit_admin = isset( $_POST['wp_bnb_rate_limit_admin'] )
|
||||||
|
? max( 1, min( 1000, absint( $_POST['wp_bnb_rate_limit_admin'] ) ) )
|
||||||
|
: $defaults['admin'];
|
||||||
|
|
||||||
|
update_option( 'wp_bnb_rate_limit_window', $limit_window );
|
||||||
|
update_option( 'wp_bnb_rate_limit_public', $limit_public );
|
||||||
|
update_option( 'wp_bnb_rate_limit_availability', $limit_avail );
|
||||||
|
update_option( 'wp_bnb_rate_limit_booking', $limit_booking );
|
||||||
|
update_option( 'wp_bnb_rate_limit_admin', $limit_admin );
|
||||||
|
|
||||||
|
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.8.0
|
* Version: 0.10.1
|
||||||
* 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.8.0' );
|
define( 'WP_BNB_VERSION', '0.10.1' );
|
||||||
|
|
||||||
// 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