Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 70d588808e | |||
| 0bf7f19ac5 | |||
| 7c0016c244 | |||
| 997541ab48 | |||
| 36a69b0de4 | |||
| 5d24cfa6f9 | |||
| 2865956c56 | |||
| 965060cc03 | |||
| 0e55fae7f2 | |||
| 1b6a5a4897 | |||
| 3f5adfb04e | |||
| b701d127f8 | |||
| 481495805b |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@ wp-plugins
|
||||
wp-core
|
||||
vendor/
|
||||
releases/*
|
||||
MARKETING.md
|
||||
|
||||
125
CHANGELOG.md
125
CHANGELOG.md
@@ -5,6 +5,123 @@ 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/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.11.3] - 2026-02-03
|
||||
|
||||
### Changed
|
||||
|
||||
- Calendar filters now display side by side instead of stacked rows
|
||||
|
||||
## [0.11.2] - 2026-02-03
|
||||
|
||||
### Changed
|
||||
|
||||
- Calendar page room column now wider (200px) with proper left alignment
|
||||
- Room column displays building name on second row for better identification
|
||||
- Changed calendar table layout from fixed to auto for flexible column widths
|
||||
|
||||
## [0.11.1] - 2026-02-03
|
||||
|
||||
### Added
|
||||
|
||||
- Internationalization (i18n) support:
|
||||
- Translation template file `languages/wp-bnb.pot` with 1,140 translatable strings
|
||||
- German (Switzerland) translation `languages/wp-bnb-de_CH.po` with 77% coverage (875 strings)
|
||||
- Compiled binary `languages/wp-bnb-de_CH.mo` for WordPress use
|
||||
- Coverage includes: admin UI, post types, taxonomies, settings, dashboard, reports, REST API, WooCommerce, CF7, widgets, blocks, shortcodes
|
||||
|
||||
### Changed
|
||||
|
||||
- README.md updated with comprehensive WooCommerce integration documentation
|
||||
- Added REST API to key features list in README
|
||||
|
||||
## [0.11.0] - 2026-02-03
|
||||
|
||||
### Added
|
||||
|
||||
- WooCommerce Integration System:
|
||||
- New `src/Integration/WooCommerce/` directory with complete integration
|
||||
- `Manager.php` - Core integration manager with HPOS compatibility declaration
|
||||
- `ProductSync.php` - Room-to-WooCommerce-product synchronization
|
||||
- `CartHandler.php` - Cart item data, availability validation, dynamic pricing
|
||||
- `CheckoutHandler.php` - Checkout field customization and pre-fill
|
||||
- `OrderHandler.php` - Booking creation on payment completion
|
||||
- `InvoiceGenerator.php` - PDF invoice generation using mPDF
|
||||
- `RefundHandler.php` - Booking cancellation on full refund
|
||||
- `AdminColumns.php` - Admin list cross-links between bookings and orders
|
||||
- Product Synchronization:
|
||||
- Virtual WooCommerce products created for rooms (SKU: `bnb-room-{id}`)
|
||||
- Auto-sync on room save, delete on room deletion
|
||||
- Manual "Sync All Rooms" button in settings
|
||||
- Bidirectional meta linking (room ↔ product)
|
||||
- Cart & Checkout:
|
||||
- Booking data stored in cart items (room, dates, guests, services)
|
||||
- Availability validation before add-to-cart and at checkout
|
||||
- Dynamic price calculation based on dates and services
|
||||
- Cart item display shows booking details (dates, guests, nights)
|
||||
- Special requests and arrival time fields at checkout
|
||||
- Booking summary display in checkout and order received page
|
||||
- Order & Booking Integration:
|
||||
- Automatic booking creation on `woocommerce_payment_complete`
|
||||
- Guest record creation from order billing info
|
||||
- Bidirectional order-booking links via meta keys
|
||||
- Status synchronization (WC status → Booking status mapping)
|
||||
- Booking reference generation (BNB-YYYY-NNNNN)
|
||||
- Invoice Generation:
|
||||
- PDF invoices using existing mPDF dependency
|
||||
- Invoice numbering with configurable prefix and start number
|
||||
- Auto-attach invoices to WooCommerce order emails
|
||||
- Download invoice button in admin order actions
|
||||
- Secure storage in `wp-content/uploads/wp-bnb-invoices/`
|
||||
- Refund Handling:
|
||||
- Full refund triggers booking cancellation
|
||||
- Partial refund stores amount in booking meta without cancellation
|
||||
- Refund info displayed in booking admin
|
||||
- `wp_bnb_wc_should_cancel_on_refund` filter for customization
|
||||
- Admin Enhancements:
|
||||
- "WC Order" column in bookings list with order link and status
|
||||
- "Booking" column in WC orders list with dates and status
|
||||
- Row actions for cross-navigation between bookings and orders
|
||||
- HPOS (High-Performance Order Storage) support
|
||||
- WooCommerce Settings Tab with Subtabs:
|
||||
- General: Enable integration, auto-confirm on payment, WC status indicator
|
||||
- Products: Auto-sync toggle, product category selection, sync button
|
||||
- Orders: Status mapping reference table
|
||||
- Invoices: Auto-attach, prefix, starting number, logo, footer text
|
||||
- Frontend Assets:
|
||||
- `assets/css/wc-integration.css` - Cart, checkout, and booking form styles
|
||||
- `assets/js/wc-integration.js` - Booking form handler, AJAX operations
|
||||
|
||||
### Changed
|
||||
|
||||
- Plugin.php updated to initialize WooCommerce integration when WC is active
|
||||
- Settings page now has eight tabs: General, Pricing, License, Updates, Metrics, API, WooCommerce
|
||||
- HPOS compatibility declared via `FeaturesUtil::declare_compatibility()`
|
||||
|
||||
### Security
|
||||
|
||||
- Invoice storage protected with .htaccess (deny all)
|
||||
- Nonce verification on all AJAX operations
|
||||
- Capability checks for admin actions
|
||||
- HPOS-compatible meta access using `$order->get_meta()` / `$order->update_meta_data()`
|
||||
|
||||
## [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
|
||||
@@ -616,6 +733,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Input sanitization and output escaping
|
||||
- Server secret masking in license settings
|
||||
|
||||
[0.11.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.11.0
|
||||
[0.10.1]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.10.1
|
||||
[0.10.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.10.0
|
||||
[0.9.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.9.0
|
||||
[0.8.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.8.0
|
||||
[0.7.2]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.7.2
|
||||
[0.7.1]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.7.1
|
||||
[0.7.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.7.0
|
||||
[0.6.1]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.6.1
|
||||
[0.6.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.6.0
|
||||
[0.5.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.5.0
|
||||
|
||||
272
CLAUDE.md
272
CLAUDE.md
@@ -237,7 +237,21 @@ wp-bnb/
|
||||
│ ├── Plugin.php # Main plugin singleton
|
||||
│ ├── Admin/ # Admin pages
|
||||
│ │ ├── Calendar.php # Availability calendar page
|
||||
│ │ ├── Dashboard.php # Dashboard page with statistics
|
||||
│ │ ├── Reports.php # Reports page with exports
|
||||
│ │ └── Seasons.php # Seasons management page
|
||||
│ ├── Api/ # REST API (v0.10.0+)
|
||||
│ │ ├── RestApi.php # Main API registration
|
||||
│ │ ├── RateLimiter.php # Transient-based rate limiting
|
||||
│ │ ├── ResponseFormatter.php # Standardized responses
|
||||
│ │ └── Controllers/ # API endpoint controllers
|
||||
│ │ ├── AbstractController.php
|
||||
│ │ ├── BookingsController.php
|
||||
│ │ ├── BuildingsController.php
|
||||
│ │ ├── GuestsController.php
|
||||
│ │ ├── PricingController.php
|
||||
│ │ ├── RoomsController.php
|
||||
│ │ └── ServicesController.php
|
||||
│ ├── Blocks/ # Gutenberg blocks
|
||||
│ │ └── BlockRegistrar.php # Block registration and rendering
|
||||
│ ├── Booking/ # Booking system
|
||||
@@ -250,38 +264,47 @@ wp-bnb/
|
||||
│ │ ├── AvailabilityCalendar.php
|
||||
│ │ ├── BuildingRooms.php
|
||||
│ │ └── SimilarRooms.php
|
||||
│ ├── License/
|
||||
│ │ └── Manager.php # License management
|
||||
│ ├── Integration/ # Third-party integrations
|
||||
│ │ ├── CF7.php # Contact Form 7 integration
|
||||
│ │ └── Prometheus.php # Prometheus metrics
|
||||
│ ├── License/ # License management
|
||||
│ │ ├── Manager.php # License validation and activation
|
||||
│ │ └── Updater.php # Auto-update system
|
||||
│ ├── PostTypes/ # Custom post types
|
||||
│ │ ├── Booking.php # Booking post type
|
||||
│ │ ├── Building.php # Building post type
|
||||
│ │ └── Room.php # Room post type
|
||||
│ │ ├── Guest.php # Guest post type
|
||||
│ │ ├── Room.php # Room post type
|
||||
│ │ └── Service.php # Service post type
|
||||
│ ├── Pricing/ # Pricing system
|
||||
│ │ ├── Calculator.php # Price calculation
|
||||
│ │ ├── PricingTier.php # Pricing tier enum
|
||||
│ │ └── Season.php # Seasonal pricing
|
||||
│ ├── Integration/ # Third-party integrations
|
||||
│ │ └── CF7.php # Contact Form 7 integration
|
||||
│ ├── Privacy/ # Privacy & GDPR
|
||||
│ │ └── Manager.php # Data export/deletion
|
||||
│ └── Taxonomies/ # Custom taxonomies
|
||||
│ ├── Amenity.php # Amenity taxonomy (tags)
|
||||
│ └── RoomType.php # Room type taxonomy (categories)
|
||||
├── lib/ # Git submodules
|
||||
│ └── wc-licensed-product-client/ # License client library
|
||||
├── vendor/ # Composer dependencies (auto-generated)
|
||||
├── assets/
|
||||
│ ├── RoomType.php # Room type taxonomy (categories)
|
||||
│ └── ServiceCategory.php # Service categories
|
||||
├── assets/ # CSS, JS, images
|
||||
│ ├── css/
|
||||
│ │ ├── admin.css # Admin styles
|
||||
│ │ ├── blocks-editor.css # Gutenberg editor styles
|
||||
│ │ ├── cf7-integration.css # CF7 form styles
|
||||
│ │ └── frontend.css # Frontend styles (~1250 lines)
|
||||
│ │ └── frontend.css # Frontend styles
|
||||
│ ├── grafana/
|
||||
│ │ └── wp-bnb-dashboard.json # Pre-configured Grafana dashboard
|
||||
│ └── js/
|
||||
│ ├── admin.js # Admin scripts
|
||||
│ ├── blocks-editor.js # Gutenberg editor scripts
|
||||
│ ├── cf7-integration.js # CF7 form scripts
|
||||
│ └── frontend.js # Frontend scripts (~825 lines)
|
||||
├── templates/ # Twig templates (future)
|
||||
├── languages/ # Translation files (future)
|
||||
└── releases/ # Release packages (git-ignored)
|
||||
│ └── frontend.js # Frontend scripts
|
||||
├── languages/ # Translation files (.pot/.po/.mo)
|
||||
├── lib/ # Git submodules
|
||||
│ └── wc-licensed-product-client/ # License client library
|
||||
├── releases/ # Release packages (git-ignored)
|
||||
├── templates/ # Twig templates (reserved for future)
|
||||
└── vendor/ # Composer dependencies (auto-generated)
|
||||
```
|
||||
|
||||
### Implementation Details
|
||||
@@ -992,3 +1015,222 @@ Admin features always work; frontend requires valid license.
|
||||
- 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
|
||||
|
||||
### 2026-02-03 - Version 0.10.0/0.10.1 (REST API Endpoints)
|
||||
|
||||
**Completed:**
|
||||
|
||||
- Created `src/Api/RestApi.php` main registration class
|
||||
- Namespace constant: `wp-bnb/v1`
|
||||
- Controller initialization and route registration
|
||||
- Integration with Plugin class via `rest_api_init` hook
|
||||
- Created `src/Api/RateLimiter.php`
|
||||
- Transient-based rate limiting per client (user ID or IP)
|
||||
- Tiered limits: public (60/min), availability (30/min), booking (10/min), admin (120/min)
|
||||
- Configurable via WordPress options with fallback defaults
|
||||
- `check()`, `get_retry_after()`, `get_rate_limit_info()` methods
|
||||
- Created `src/Api/ResponseFormatter.php`
|
||||
- Standardized success/error responses
|
||||
- `success()`, `collection()`, `created()` methods
|
||||
- Error helpers: `validation_error()`, `not_found()`, `forbidden()`, `conflict()`, `rate_limit_error()`
|
||||
- Created `src/Api/Controllers/AbstractController.php`
|
||||
- Base class extending `WP_REST_Controller`
|
||||
- Rate limit checking and header injection
|
||||
- Client IP detection (supports Cloudflare, proxies)
|
||||
- Common permission callbacks: `public_permission()`, `admin_permission()`, `manage_bookings_permission()`
|
||||
- Helper methods: `validate_date()`, `validate_future_date()`, `get_pagination_params()`, `get_sorting_params()`
|
||||
- Image formatting: `format_featured_image()`, `format_image()`
|
||||
- HATEOAS links via `add_links()`
|
||||
- Created `src/Api/Controllers/BuildingsController.php`
|
||||
- GET /buildings - List with pagination, search, orderby
|
||||
- GET /buildings/{id} - Single building with address, contact, rooms count
|
||||
- GET /buildings/{id}/rooms - Rooms in building
|
||||
- Created `src/Api/Controllers/RoomsController.php`
|
||||
- GET /rooms - List with filters (building, room_type, amenities, capacity, status)
|
||||
- GET /rooms/{id} - Full room data with gallery, pricing, amenities
|
||||
- GET /rooms/{id}/availability - Check availability using `Availability::check_availability_with_price()`
|
||||
- GET /rooms/{id}/calendar - Monthly calendar using `Availability::get_calendar_data()`
|
||||
- Created `src/Api/Controllers/AvailabilityController.php`
|
||||
- POST /availability/search - Search available rooms with date range, capacity, filters
|
||||
- Created `src/Api/Controllers/BookingsController.php`
|
||||
- POST /bookings - Create booking with guest auto-creation, conflict check
|
||||
- GET /bookings - Admin list with filters (status, room, date range)
|
||||
- GET /bookings/{id} - Full booking with room, guest, services
|
||||
- PATCH /bookings/{id} - Update booking details
|
||||
- DELETE /bookings/{id} - Cancel booking (sets status to cancelled)
|
||||
- POST /bookings/{id}/confirm - Status transition
|
||||
- POST /bookings/{id}/check-in - Status transition
|
||||
- POST /bookings/{id}/check-out - Status transition
|
||||
- Created `src/Api/Controllers/GuestsController.php`
|
||||
- GET /guests - Admin list with search, status filter
|
||||
- GET /guests/{id} - Guest data (excludes encrypted ID numbers)
|
||||
- GET /guests/search - Quick search by name/email
|
||||
- GET /guests/{id}/bookings - Guest's booking history
|
||||
- Created `src/Api/Controllers/ServicesController.php`
|
||||
- GET /services - List active services with categories
|
||||
- GET /services/{id} - Service details with pricing info
|
||||
- Created `src/Api/Controllers/PricingController.php`
|
||||
- POST /pricing/calculate - Full price breakdown with room, dates, services
|
||||
- Updated `src/Plugin.php`
|
||||
- Added API tab to settings page with subtabs (General, Rate Limits, Endpoints)
|
||||
- Enable/disable API toggle
|
||||
- Configurable rate limiting with per-endpoint-type limits
|
||||
- Time window configuration (10-300 seconds)
|
||||
- Full endpoint documentation with HTTP method badges
|
||||
- Updated `README.md` with comprehensive REST API documentation
|
||||
- Endpoint reference tables (public and admin)
|
||||
- Authentication examples (Application Passwords)
|
||||
- Rate limiting configuration and response headers
|
||||
- Code examples for common operations
|
||||
|
||||
**Files Created:**
|
||||
|
||||
- `src/Api/RestApi.php` - Main API registration
|
||||
- `src/Api/RateLimiter.php` - Rate limiting
|
||||
- `src/Api/ResponseFormatter.php` - Response formatting
|
||||
- `src/Api/Controllers/AbstractController.php` - Base controller
|
||||
- `src/Api/Controllers/BuildingsController.php` - Buildings endpoints
|
||||
- `src/Api/Controllers/RoomsController.php` - Rooms endpoints
|
||||
- `src/Api/Controllers/AvailabilityController.php` - Availability search
|
||||
- `src/Api/Controllers/BookingsController.php` - Bookings CRUD
|
||||
- `src/Api/Controllers/GuestsController.php` - Guests endpoints
|
||||
- `src/Api/Controllers/ServicesController.php` - Services endpoints
|
||||
- `src/Api/Controllers/PricingController.php` - Pricing calculation
|
||||
- `MARKETING.md` - Marketing texts for shops (gitignored)
|
||||
|
||||
**Files Changed:**
|
||||
|
||||
- `src/Plugin.php` - API settings tab with subtabs, RestApi initialization
|
||||
- `wp-bnb.php` - Version bump to 0.10.0, then 0.10.1
|
||||
- `CHANGELOG.md` - Added v0.10.0 and v0.10.1 release notes
|
||||
- `PLAN.md` - Marked Phase 10 as complete, reorganized roadmap
|
||||
- `README.md` - Added REST API documentation section
|
||||
- `.gitignore` - Added MARKETING.md to exclusions
|
||||
|
||||
**Learnings:**
|
||||
|
||||
- WordPress REST API uses `WP_REST_Controller` as base class with `register_routes()` method
|
||||
- Route registration via `register_rest_route()` with namespace, route pattern, and args
|
||||
- Permission callbacks return bool; use `current_user_can('edit_posts')` for admin endpoints
|
||||
- Rate limiting with transients: store count and start time, check against limits
|
||||
- Transient key should include client identifier and endpoint type hash
|
||||
- X-RateLimit headers (Limit, Remaining, Reset) provide rate limit info to clients
|
||||
- Application Passwords (WordPress 5.6+) recommended for external API access
|
||||
- HATEOAS links added via `_links` key in response
|
||||
- Conflict detection reuses existing `Availability::check_availability()` method
|
||||
- Settings subtabs use query parameters (`subtab=general`) with conditional rendering
|
||||
- Configurable options should have sensible defaults via `get_option($key, $default)`
|
||||
- Marketing content (MARKETING.md) should be gitignored to keep repo focused on code
|
||||
|
||||
**Released:**
|
||||
|
||||
- v0.10.0: Committed `81c97c3` - Base REST API implementation
|
||||
- v0.10.1: Committed `3f5adfb` - Configurable rate limiting with settings subtabs
|
||||
- Tags: `v0.10.0`, `v0.10.1`
|
||||
- Pushed to origin: dev, main, both tags
|
||||
|
||||
### 2026-02-03 - Version 0.11.0 (WooCommerce Integration)
|
||||
|
||||
**Completed:**
|
||||
|
||||
- Created `src/Integration/WooCommerce/Manager.php` (~435 lines)
|
||||
- Core integration manager with option constants
|
||||
- HPOS (High-Performance Order Storage) compatibility declaration
|
||||
- `is_wc_active()`, `is_enabled()` checks
|
||||
- `map_wc_status_to_booking()` for status synchronization
|
||||
- `get_order_for_booking()`, `get_booking_for_order()` bidirectional lookups
|
||||
- `link_booking_to_order()` for creating relationships
|
||||
- Created `src/Integration/WooCommerce/ProductSync.php` (~515 lines)
|
||||
- Virtual WooCommerce products for rooms (SKU: `bnb-room-{id}`)
|
||||
- Auto-sync on `save_post_bnb_room` hook
|
||||
- Product deletion on room deletion
|
||||
- `sync_all_rooms()` for bulk synchronization
|
||||
- Bidirectional meta linking (room ↔ product)
|
||||
- Created `src/Integration/WooCommerce/CartHandler.php` (~545 lines)
|
||||
- Cart item data structure with booking details
|
||||
- Availability validation on add-to-cart
|
||||
- Dynamic price calculation using `Calculator` class
|
||||
- Cart item display with dates, guests, nights
|
||||
- Services support in cart items
|
||||
- Created `src/Integration/WooCommerce/CheckoutHandler.php` (~347 lines)
|
||||
- Special requests textarea field
|
||||
- Arrival time dropdown field
|
||||
- Guest data pre-fill from user profile
|
||||
- Final availability validation before payment
|
||||
- Booking summary display
|
||||
- Created `src/Integration/WooCommerce/OrderHandler.php` (~584 lines)
|
||||
- Booking creation on `woocommerce_payment_complete`
|
||||
- Guest record creation from billing info
|
||||
- Booking reference generation (BNB-YYYY-NNNNN)
|
||||
- Status synchronization on order status changes
|
||||
- Order-booking bidirectional linking
|
||||
- Created `src/Integration/WooCommerce/InvoiceGenerator.php` (~633 lines)
|
||||
- PDF generation using existing mPDF dependency
|
||||
- Configurable invoice numbering (prefix + start number)
|
||||
- Auto-attach to WooCommerce order emails
|
||||
- Secure storage in `wp-content/uploads/wp-bnb-invoices/`
|
||||
- .htaccess protection for invoice directory
|
||||
- Created `src/Integration/WooCommerce/RefundHandler.php` (~394 lines)
|
||||
- Full refund triggers booking cancellation
|
||||
- Partial refund records amount without cancellation
|
||||
- Refund meta storage on booking
|
||||
- `wp_bnb_wc_should_cancel_on_refund` filter for customization
|
||||
- Created `src/Integration/WooCommerce/AdminColumns.php` (~282 lines)
|
||||
- "WC Order" column in bookings list
|
||||
- "Booking" column in WC orders list (supports HPOS)
|
||||
- Row actions for cross-navigation
|
||||
- Created `assets/css/wc-integration.css` (~443 lines)
|
||||
- Cart booking display styles
|
||||
- Checkout summary styles
|
||||
- Booking form styles
|
||||
- Status badge colors
|
||||
- Created `assets/js/wc-integration.js` (~358 lines)
|
||||
- `BookingForm` class for frontend forms
|
||||
- AJAX sync rooms handler
|
||||
- AJAX generate invoice handler
|
||||
- Availability checking with debounce
|
||||
- Updated `src/Plugin.php`
|
||||
- WooCommerce initialization when WC is active
|
||||
- WooCommerce settings tab with 4 subtabs (General, Products, Orders, Invoices)
|
||||
- Asset enqueuing for WooCommerce integration
|
||||
|
||||
**Files Created:**
|
||||
|
||||
- `src/Integration/WooCommerce/Manager.php`
|
||||
- `src/Integration/WooCommerce/ProductSync.php`
|
||||
- `src/Integration/WooCommerce/CartHandler.php`
|
||||
- `src/Integration/WooCommerce/CheckoutHandler.php`
|
||||
- `src/Integration/WooCommerce/OrderHandler.php`
|
||||
- `src/Integration/WooCommerce/InvoiceGenerator.php`
|
||||
- `src/Integration/WooCommerce/RefundHandler.php`
|
||||
- `src/Integration/WooCommerce/AdminColumns.php`
|
||||
- `assets/css/wc-integration.css`
|
||||
- `assets/js/wc-integration.js`
|
||||
|
||||
**Files Changed:**
|
||||
|
||||
- `src/Plugin.php` - WooCommerce initialization and settings tab
|
||||
- `assets/css/admin.css` - Status badge colors for booking statuses
|
||||
- `wp-bnb.php` - Version bump to 0.11.0
|
||||
- `CHANGELOG.md` - Added v0.11.0 release notes
|
||||
- `PLAN.md` - Marked Phase 11 as complete
|
||||
|
||||
**Learnings:**
|
||||
|
||||
- WooCommerce HPOS compatibility requires `FeaturesUtil::declare_compatibility()` in `before_woocommerce_init` hook
|
||||
- Use `$order->get_meta()` / `$order->update_meta_data()` instead of `get_post_meta()` for HPOS compatibility
|
||||
- Virtual products (`virtual => true`, `downloadable => false`) don't require shipping
|
||||
- Cart item data stored via `woocommerce_add_cart_item_data` filter persists through session
|
||||
- Dynamic pricing via `woocommerce_before_calculate_totals` hook with priority 20
|
||||
- `woocommerce_payment_complete` fires after successful payment, ideal for booking creation
|
||||
- Invoice attachment via `woocommerce_email_attachments` filter with order and email type detection
|
||||
- Refund detection: `woocommerce_refund_created` for partial, `woocommerce_order_fully_refunded` for full
|
||||
- Status badge CSS must be in admin.css for settings page (wc-integration.css is frontend only)
|
||||
- HPOS orders list uses `manage_woocommerce_page_wc-orders_columns` filter (different from legacy)
|
||||
|
||||
**Released:**
|
||||
|
||||
- Committed: `2865956` on dev branch
|
||||
- Merged to main (fast-forward)
|
||||
- Tagged: `v0.11.0`
|
||||
- Pushed to origin: dev, main, v0.11.0
|
||||
|
||||
166
PLAN.md
166
PLAN.md
@@ -204,7 +204,14 @@ This document outlines the implementation plan for the WP BnB Management plugin.
|
||||
- [x] Transient-based rate limiting with tiered limits
|
||||
- [x] API settings tab with enable/disable toggles
|
||||
|
||||
## Phase 11: Security Audit (v0.11.0)
|
||||
### Phase 11: WooCommerce Integration (v0.11.0) - Complete
|
||||
|
||||
- [x] Payment processing
|
||||
- [x] Invoice generation
|
||||
- [x] Order management
|
||||
- [x] Refund handling
|
||||
|
||||
## Phase 12: Security Audit (v0.12.0)
|
||||
|
||||
- [ ] Check for Wordpress best-practices
|
||||
- [ ] Review the code for OWASP Top 10, including XSS, XSRF, SQLi and other critical threads
|
||||
@@ -212,13 +219,6 @@ This document outlines the implementation plan for the WP BnB Management plugin.
|
||||
|
||||
## Future Considerations (v1.0.0+)
|
||||
|
||||
### WooCommerce Integration (Optional)
|
||||
|
||||
- [ ] Payment processing
|
||||
- [ ] Invoice generation
|
||||
- [ ] Order management
|
||||
- [ ] Refund handling
|
||||
|
||||
### Multi-language Support
|
||||
|
||||
- [ ] Full translation support
|
||||
@@ -238,53 +238,90 @@ This document outlines the implementation plan for the WP BnB Management plugin.
|
||||
|
||||
```text
|
||||
wp-bnb/
|
||||
├── wp-bnb.php # Main plugin file
|
||||
├── composer.json # Dependencies
|
||||
├── src/ # PHP source (PSR-4)
|
||||
│ ├── Plugin.php # Main plugin class
|
||||
│ ├── License/ # License management
|
||||
│ │ └── Manager.php
|
||||
│ ├── PostTypes/ # Custom post types
|
||||
│ │ ├── Building.php
|
||||
│ │ ├── Room.php
|
||||
│ │ ├── Booking.php
|
||||
│ │ ├── Guest.php
|
||||
│ │ └── Service.php
|
||||
│ ├── Taxonomies/ # Custom taxonomies
|
||||
│ │ ├── RoomType.php
|
||||
│ │ └── Amenity.php
|
||||
│ ├── Admin/ # Admin functionality
|
||||
│ │ ├── Dashboard.php
|
||||
│ │ ├── Settings.php
|
||||
│ │ └── MetaBoxes.php
|
||||
│ ├── Frontend/ # Frontend functionality
|
||||
│ │ ├── Shortcodes.php
|
||||
│ │ ├── Widgets.php
|
||||
│ │ └── Search.php
|
||||
├── wp-bnb.php # Main plugin file (entry point)
|
||||
├── composer.json # Composer configuration
|
||||
├── composer.lock # Dependency lock file
|
||||
├── CHANGELOG.md # Version history
|
||||
├── CLAUDE.md # AI assistant documentation
|
||||
├── PLAN.md # Implementation roadmap
|
||||
├── README.md # User documentation
|
||||
├── .editorconfig # Editor configuration
|
||||
├── .gitignore # Git ignore patterns
|
||||
├── .gitmodules # Git submodule configuration
|
||||
├── .gitea/
|
||||
│ └── workflows/
|
||||
│ └── release.yml # CI/CD release pipeline
|
||||
├── src/ # PHP source (PSR-4: Magdev\WpBnb)
|
||||
│ ├── Plugin.php # Main plugin singleton
|
||||
│ ├── Admin/ # Admin pages
|
||||
│ │ ├── Calendar.php # Availability calendar page
|
||||
│ │ ├── Dashboard.php # Dashboard page with statistics
|
||||
│ │ ├── Reports.php # Reports page with exports
|
||||
│ │ └── Seasons.php # Seasons management page
|
||||
│ ├── Api/ # REST API (v0.10.0+)
|
||||
│ │ ├── RestApi.php # Main API registration
|
||||
│ │ ├── RateLimiter.php # Transient-based rate limiting
|
||||
│ │ ├── ResponseFormatter.php # Standardized responses
|
||||
│ │ └── Controllers/ # API endpoint controllers
|
||||
│ │ ├── AbstractController.php
|
||||
│ │ ├── BookingsController.php
|
||||
│ │ ├── BuildingsController.php
|
||||
│ │ ├── GuestsController.php
|
||||
│ │ ├── PricingController.php
|
||||
│ │ ├── RoomsController.php
|
||||
│ │ └── ServicesController.php
|
||||
│ ├── Blocks/ # Gutenberg blocks
|
||||
│ │ ├── Building.php
|
||||
│ │ ├── Room.php
|
||||
│ │ └── Search.php
|
||||
│ ├── Pricing/ # Pricing logic
|
||||
│ │ ├── Calculator.php
|
||||
│ │ └── PricingTier.php
|
||||
│ │ └── BlockRegistrar.php # Block registration and rendering
|
||||
│ ├── Booking/ # Booking logic
|
||||
│ │ ├── Manager.php
|
||||
│ │ ├── Calendar.php
|
||||
│ │ └── Workflow.php
|
||||
│ └── Integration/ # Third-party integrations
|
||||
│ └── CF7.php
|
||||
├── templates/ # Twig templates
|
||||
│ ├── admin/
|
||||
│ ├── frontend/
|
||||
│ └── email/
|
||||
│ │ ├── Availability.php # Availability checking
|
||||
│ │ └── EmailNotifier.php # Email notifications
|
||||
│ ├── Frontend/ # Frontend components
|
||||
│ │ ├── Search.php # Room search and AJAX handlers
|
||||
│ │ ├── Shortcodes.php # All shortcode handlers
|
||||
│ │ └── Widgets/ # WordPress widgets
|
||||
│ │ ├── AvailabilityCalendar.php
|
||||
│ │ ├── BuildingRooms.php
|
||||
│ │ └── SimilarRooms.php
|
||||
│ ├── Integration/ # Third-party integrations
|
||||
│ │ ├── CF7.php # Contact Form 7 integration
|
||||
│ │ └── Prometheus.php # Prometheus metrics
|
||||
│ ├── License/ # License management
|
||||
│ │ ├── Manager.php # License validation and activation
|
||||
│ │ └── Updater.php # Auto-update system
|
||||
│ ├── PostTypes/ # Custom post types
|
||||
│ │ ├── Booking.php
|
||||
│ │ ├── Building.php
|
||||
│ │ ├── Guest.php
|
||||
│ │ ├── Room.php
|
||||
│ │ └── Service.php
|
||||
│ ├── Pricing/ # Pricing logic
|
||||
│ │ ├── Calculator.php # Price calculation
|
||||
│ │ ├── PricingTier.php # Pricing tier enum
|
||||
│ │ └── Season.php # Seasonal pricing
|
||||
│ ├── Privacy/ # Privacy & GDPR
|
||||
│ │ └── Manager.php # Data export/deletion
|
||||
│ └── Taxonomies/ # Custom taxonomies
|
||||
│ ├── Amenity.php # Amenities (tags)
|
||||
│ ├── RoomType.php # Room types (categories)
|
||||
│ └── ServiceCategory.php # Service categories
|
||||
├── assets/ # CSS, JS, images
|
||||
│ ├── css/
|
||||
│ ├── js/
|
||||
│ └── images/
|
||||
├── languages/ # Translation files
|
||||
│ │ ├── admin.css # Admin styles
|
||||
│ │ ├── blocks-editor.css # Gutenberg editor styles
|
||||
│ │ ├── cf7-integration.css # CF7 form styles
|
||||
│ │ └── frontend.css # Frontend styles
|
||||
│ ├── grafana/
|
||||
│ │ └── wp-bnb-dashboard.json # Pre-configured Grafana dashboard
|
||||
│ └── js/
|
||||
│ ├── admin.js # Admin scripts
|
||||
│ ├── blocks-editor.js # Gutenberg editor scripts
|
||||
│ ├── cf7-integration.js # CF7 form scripts
|
||||
│ └── frontend.js # Frontend scripts
|
||||
├── languages/ # Translation files (.pot/.po/.mo)
|
||||
├── lib/ # Git submodules
|
||||
│ └── wc-licensed-product-client/
|
||||
├── releases/ # Release packages (git-ignored)
|
||||
├── templates/ # Twig templates (reserved for future)
|
||||
└── vendor/ # Composer dependencies
|
||||
```
|
||||
|
||||
@@ -309,18 +346,19 @@ The plugin will provide extensive hooks for customization:
|
||||
|
||||
## Version Milestones
|
||||
|
||||
| Version | Focus | Target |
|
||||
| ------- | ------------------ | -------- |
|
||||
| 0.0.1 | Initial setup | Complete |
|
||||
| 0.1.0 | Data structures | Complete |
|
||||
| 0.2.0 | Pricing | Complete |
|
||||
| 0.3.0 | Bookings | Complete |
|
||||
| 0.4.0 | Guests | Complete |
|
||||
| 0.5.0 | Services | Complete |
|
||||
| 0.6.0 | Frontend | Complete |
|
||||
| 0.7.0 | CF7 Integration | Complete |
|
||||
| 0.8.0 | Dashboard | Complete |
|
||||
| 0.9.0 | Prometheus Metrics | Complete |
|
||||
| 0.10.0 | API Endpoints | TBD |
|
||||
| 0.11.0 | Security Audit | TBD |
|
||||
| 1.0.0 | Stable Release | TBD |
|
||||
| Version | Focus | Target |
|
||||
| ------- | ----------------------- | -------- |
|
||||
| 0.0.1 | Initial setup | Complete |
|
||||
| 0.1.0 | Data structures | Complete |
|
||||
| 0.2.0 | Pricing | Complete |
|
||||
| 0.3.0 | Bookings | Complete |
|
||||
| 0.4.0 | Guests | Complete |
|
||||
| 0.5.0 | Services | Complete |
|
||||
| 0.6.0 | Frontend | Complete |
|
||||
| 0.7.0 | CF7 Integration | Complete |
|
||||
| 0.8.0 | Dashboard | Complete |
|
||||
| 0.9.0 | Prometheus Metrics | Complete |
|
||||
| 0.10.0 | API Endpoints | Complete |
|
||||
| 0.11.0 | WooCommerce Integration | Complete |
|
||||
| 0.12.0 | Security Audit | TBD |
|
||||
| 1.0.0 | Stable Release | TBD |
|
||||
|
||||
112
README.md
112
README.md
@@ -19,9 +19,11 @@ WP BnB Management enables WordPress to act as a full management system for B&B h
|
||||
- **Auto-Updates**: Automatic update checks and installation from license server
|
||||
- **Development Mode**: License bypass for local development environments
|
||||
- **Contact Form 7 Integration**: Accept booking requests and inquiries through CF7 forms
|
||||
- **WooCommerce Integration**: Accept payments, auto-sync rooms as products, generate invoices
|
||||
- **Dashboard**: Comprehensive admin dashboard with statistics and charts
|
||||
- **Reports**: Detailed reports with CSV and PDF export
|
||||
- **Prometheus Metrics**: Expose operational metrics for monitoring with Grafana
|
||||
- **REST API**: Comprehensive API for external integrations
|
||||
|
||||
### Requirements
|
||||
|
||||
@@ -29,6 +31,7 @@ WP BnB Management enables WordPress to act as a full management system for B&B h
|
||||
- PHP 8.3 or higher
|
||||
- Valid license key
|
||||
- Contact Form 7 (optional, for booking forms)
|
||||
- WooCommerce 8.0+ (optional, for payments and invoicing)
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -444,6 +447,95 @@ The dashboard includes:
|
||||
- Today's check-ins/check-outs
|
||||
- Trend indicators
|
||||
|
||||
## WooCommerce Integration
|
||||
|
||||
The plugin integrates with WooCommerce to enable payment processing, automatic invoicing, and seamless order management.
|
||||
|
||||
### Enabling WooCommerce Integration
|
||||
|
||||
1. Install and activate WooCommerce 8.0 or higher
|
||||
2. Navigate to **WP BnB → Settings → WooCommerce**
|
||||
3. Enable "Enable WooCommerce Integration"
|
||||
4. Configure product sync and invoice settings
|
||||
|
||||
### Features
|
||||
|
||||
**Product Synchronization:**
|
||||
|
||||
- Rooms are automatically synced as virtual WooCommerce products
|
||||
- Products use SKU format `bnb-room-{id}` for tracking
|
||||
- Price, description, and images are kept in sync
|
||||
- Products created/updated on room save, deleted on room deletion
|
||||
- Manual "Sync All Rooms" button in settings
|
||||
|
||||
**Cart & Checkout:**
|
||||
|
||||
- Add room bookings to WooCommerce cart with dates and guest count
|
||||
- Real-time availability validation prevents double-booking
|
||||
- Dynamic pricing calculated from room rates and services
|
||||
- Special checkout fields for arrival time and special requests
|
||||
- Guest information pre-filled from user profile
|
||||
|
||||
**Booking Creation:**
|
||||
|
||||
- Bookings automatically created on successful payment
|
||||
- Guest records created from billing information
|
||||
- Booking linked to WooCommerce order for reference
|
||||
- Booking reference displayed on order confirmation
|
||||
|
||||
**Order-Booking Synchronization:**
|
||||
|
||||
- Order status changes sync to booking status:
|
||||
- Order completed → Booking confirmed
|
||||
- Order cancelled → Booking cancelled
|
||||
- Order refunded → Booking cancelled (full refund)
|
||||
- Partial refunds recorded without cancellation
|
||||
- Bidirectional linking between orders and bookings
|
||||
|
||||
**PDF Invoices:**
|
||||
|
||||
- Automatic PDF invoice generation
|
||||
- Configurable invoice number prefix (default: `INV-`)
|
||||
- Sequential invoice numbering with configurable start number
|
||||
- Auto-attach to WooCommerce order emails
|
||||
- Secure storage in `wp-content/uploads/wp-bnb-invoices/`
|
||||
- Manual generation from order admin
|
||||
|
||||
### WooCommerce Settings
|
||||
|
||||
**General Subtab:**
|
||||
|
||||
- Enable/disable WooCommerce integration
|
||||
- Enable automatic product sync
|
||||
- Enable auto-attach invoices to emails
|
||||
|
||||
**Products Subtab:**
|
||||
|
||||
- View sync status and product count
|
||||
- Manual "Sync All Rooms Now" button
|
||||
- Product category assignment
|
||||
|
||||
**Orders Subtab:**
|
||||
|
||||
- Order-booking status mapping
|
||||
- View linked orders and bookings
|
||||
|
||||
**Invoices Subtab:**
|
||||
|
||||
- Invoice number prefix
|
||||
- Starting invoice number
|
||||
- Company details for invoice header
|
||||
- PDF styling options
|
||||
|
||||
### HPOS Compatibility
|
||||
|
||||
The integration is fully compatible with WooCommerce High-Performance Order Storage (HPOS). Order meta is accessed using the modern `$order->get_meta()` and `$order->update_meta_data()` methods.
|
||||
|
||||
### Admin Columns
|
||||
|
||||
- **Bookings list**: "WC Order" column with link to order
|
||||
- **WooCommerce Orders list**: "Booking" column with link to booking
|
||||
|
||||
## REST API
|
||||
|
||||
The plugin provides a comprehensive REST API for integration with external applications, mobile apps, and third-party services.
|
||||
@@ -451,8 +543,10 @@ The plugin provides a comprehensive REST API for integration with external appli
|
||||
### Enabling the API
|
||||
|
||||
1. Navigate to **WP BnB → Settings → API**
|
||||
2. Enable "Enable REST 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
|
||||
|
||||
@@ -517,15 +611,23 @@ curl -u "username:app-password" https://site.com/wp-json/wp-bnb/v1/bookings
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
When enabled, rate limits are applied per client (by user ID or IP address):
|
||||
When enabled, rate limits are applied per client (by user ID or IP address). Configure limits in **Settings → API → Rate Limits**.
|
||||
|
||||
| Type | Limit | Applies To |
|
||||
| ---- | ----- | ---------- |
|
||||
**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
|
||||
@@ -658,7 +760,7 @@ Yes, guest data can be exported and deleted on request, and consent is tracked a
|
||||
|
||||
### Does it integrate with WooCommerce?
|
||||
|
||||
WooCommerce integration for payments is planned for a future release.
|
||||
Yes! WooCommerce integration is available for payment processing and invoicing. Rooms are synced as virtual products, bookings are created on successful payment, and PDF invoices are auto-generated and attached to order emails. Navigate to **WP BnB → Settings → WooCommerce** to enable and configure the integration.
|
||||
|
||||
### How is guest data secured?
|
||||
|
||||
|
||||
@@ -491,6 +491,37 @@
|
||||
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-table th {
|
||||
width: 200px;
|
||||
@@ -567,12 +598,36 @@
|
||||
display: inline-block;
|
||||
padding: 3px 8px;
|
||||
border-radius: 3px;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.bnb-status-pending {
|
||||
background: #fff8e5;
|
||||
color: #9d6a00;
|
||||
}
|
||||
|
||||
.bnb-status-confirmed {
|
||||
background: #e6f4ea;
|
||||
color: #0a6e31;
|
||||
}
|
||||
|
||||
.bnb-status-checked_in {
|
||||
background: #e3f2fd;
|
||||
color: #1565c0;
|
||||
}
|
||||
|
||||
.bnb-status-checked_out {
|
||||
background: #f5f5f5;
|
||||
color: #616161;
|
||||
}
|
||||
|
||||
.bnb-status-cancelled {
|
||||
background: #ffeaea;
|
||||
color: #d63638;
|
||||
}
|
||||
|
||||
/* Room Details Meta Box */
|
||||
#bnb_room_details .form-table td label {
|
||||
display: inline-block;
|
||||
@@ -895,13 +950,17 @@
|
||||
|
||||
/* Calendar Filters */
|
||||
.bnb-calendar-filters {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #c3c4c7;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.bnb-calendar-filters form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.bnb-calendar-filters label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -921,7 +980,7 @@
|
||||
.bnb-calendar-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
table-layout: auto;
|
||||
}
|
||||
|
||||
.bnb-calendar-table th,
|
||||
@@ -942,7 +1001,8 @@
|
||||
.bnb-calendar-table th.room-header {
|
||||
text-align: left;
|
||||
padding-left: 10px;
|
||||
min-width: 150px;
|
||||
width: 200px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
/* Calendar Day Cell */
|
||||
@@ -1013,16 +1073,31 @@
|
||||
}
|
||||
|
||||
/* Room Row in Multi-Room Calendar */
|
||||
.bnb-calendar-room {
|
||||
.bnb-calendar-table .bnb-calendar-room {
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
padding: 8px 10px;
|
||||
background: #f6f7f7;
|
||||
min-width: 200px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bnb-calendar-room small {
|
||||
.bnb-calendar-room a {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.bnb-calendar-room .room-number {
|
||||
font-weight: normal;
|
||||
color: #646970;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.bnb-calendar-room .building-name {
|
||||
display: block;
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
color: #646970;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* Calendar Legend */
|
||||
@@ -1102,8 +1177,9 @@
|
||||
|
||||
/* Responsive */
|
||||
@media screen and (max-width: 782px) {
|
||||
.bnb-calendar-filters {
|
||||
.bnb-calendar-filters form {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.bnb-calendar-filters select {
|
||||
|
||||
443
assets/css/wc-integration.css
Normal file
443
assets/css/wc-integration.css
Normal file
@@ -0,0 +1,443 @@
|
||||
/**
|
||||
* WooCommerce Integration Styles
|
||||
*
|
||||
* Styles for WP BnB - WooCommerce integration
|
||||
*
|
||||
* @package Magdev\WpBnb
|
||||
*/
|
||||
|
||||
/* ==========================================================================
|
||||
Cart Item - Booking Data Display
|
||||
========================================================================== */
|
||||
|
||||
.woocommerce-cart .cart_item .bnb-booking-info {
|
||||
margin-top: 8px;
|
||||
padding: 10px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.woocommerce-cart .cart_item .bnb-booking-info dt {
|
||||
display: inline-block;
|
||||
font-weight: 600;
|
||||
min-width: 80px;
|
||||
color: #50575e;
|
||||
}
|
||||
|
||||
.woocommerce-cart .cart_item .bnb-booking-info dd {
|
||||
display: inline-block;
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Checkout - Booking Summary
|
||||
========================================================================== */
|
||||
|
||||
.bnb-checkout-booking-summary {
|
||||
margin: 20px 0;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e1e4e8;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.bnb-checkout-booking-summary h3 {
|
||||
margin: 0 0 15px 0;
|
||||
padding: 0 0 10px 0;
|
||||
border-bottom: 1px solid #e1e4e8;
|
||||
font-size: 1.1em;
|
||||
color: #2271b1;
|
||||
}
|
||||
|
||||
.bnb-booking-item {
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px dashed #e1e4e8;
|
||||
}
|
||||
|
||||
.bnb-booking-item:last-child {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.bnb-booking-item strong {
|
||||
display: block;
|
||||
font-size: 1em;
|
||||
color: #1d2327;
|
||||
}
|
||||
|
||||
.bnb-building-name {
|
||||
display: block;
|
||||
font-size: 0.85em;
|
||||
color: #787c82;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.bnb-booking-details {
|
||||
margin-top: 8px;
|
||||
font-size: 0.9em;
|
||||
color: #50575e;
|
||||
}
|
||||
|
||||
.bnb-booking-details span {
|
||||
display: inline-block;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.bnb-booking-details span::before {
|
||||
content: "•";
|
||||
margin-right: 5px;
|
||||
color: #c3c4c7;
|
||||
}
|
||||
|
||||
.bnb-booking-details span:first-child::before {
|
||||
content: "";
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Thank You Page - Booking Confirmation
|
||||
========================================================================== */
|
||||
|
||||
.woocommerce-booking-confirmation {
|
||||
margin: 30px 0;
|
||||
padding: 20px;
|
||||
background: #f0f8f1;
|
||||
border: 1px solid #d1e7d7;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.woocommerce-booking-confirmation h2 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #00a32a;
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.woocommerce-table--booking-details {
|
||||
width: 100%;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.woocommerce-table--booking-details th {
|
||||
text-align: left;
|
||||
width: 40%;
|
||||
padding: 8px 12px 8px 0;
|
||||
color: #50575e;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.woocommerce-table--booking-details td {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.woocommerce-table--booking-details small {
|
||||
display: block;
|
||||
color: #787c82;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.woocommerce-booking-reference {
|
||||
margin: 20px 0;
|
||||
padding: 10px 15px;
|
||||
background: #f6f7f7;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.woocommerce-booking-reference .bnb-status-badge {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Status Badges
|
||||
========================================================================== */
|
||||
|
||||
.bnb-status-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.bnb-status-pending {
|
||||
background: #fff8e5;
|
||||
color: #9d6a00;
|
||||
}
|
||||
|
||||
.bnb-status-confirmed {
|
||||
background: #e6f4ea;
|
||||
color: #0a6e31;
|
||||
}
|
||||
|
||||
.bnb-status-checked_in {
|
||||
background: #e3f2fd;
|
||||
color: #1565c0;
|
||||
}
|
||||
|
||||
.bnb-status-checked_out {
|
||||
background: #f5f5f5;
|
||||
color: #616161;
|
||||
}
|
||||
|
||||
.bnb-status-cancelled {
|
||||
background: #ffeaea;
|
||||
color: #d63638;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Booking Form (Frontend)
|
||||
========================================================================== */
|
||||
|
||||
.bnb-wc-booking-form {
|
||||
margin: 20px 0;
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
border: 1px solid #e1e4e8;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.bnb-wc-booking-form h3 {
|
||||
margin: 0 0 20px 0;
|
||||
padding: 0 0 15px 0;
|
||||
border-bottom: 1px solid #e1e4e8;
|
||||
}
|
||||
|
||||
.bnb-wc-booking-form .form-row {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.bnb-wc-booking-form label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bnb-wc-booking-form input[type="date"],
|
||||
.bnb-wc-booking-form input[type="number"],
|
||||
.bnb-wc-booking-form select {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #c3c4c7;
|
||||
border-radius: 4px;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.bnb-wc-booking-form input[type="date"]:focus,
|
||||
.bnb-wc-booking-form input[type="number"]:focus,
|
||||
.bnb-wc-booking-form select:focus {
|
||||
border-color: #2271b1;
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 1px #2271b1;
|
||||
}
|
||||
|
||||
.bnb-wc-booking-form .form-row-inline {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.bnb-wc-booking-form .form-row-inline > div {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Availability status */
|
||||
.bnb-availability-status {
|
||||
margin: 15px 0;
|
||||
padding: 10px 15px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.bnb-availability-status.checking {
|
||||
background: #f6f7f7;
|
||||
color: #787c82;
|
||||
}
|
||||
|
||||
.bnb-availability-status.available {
|
||||
background: #e6f4ea;
|
||||
color: #0a6e31;
|
||||
}
|
||||
|
||||
.bnb-availability-status.unavailable {
|
||||
background: #ffeaea;
|
||||
color: #d63638;
|
||||
}
|
||||
|
||||
.bnb-availability-status .dashicons {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
/* Price display */
|
||||
.bnb-wc-price-display {
|
||||
margin: 15px 0;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.bnb-wc-price-display .price-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.bnb-wc-price-display .price-row.total {
|
||||
border-top: 1px solid #e1e4e8;
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
font-weight: 700;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
/* Services selection */
|
||||
.bnb-wc-services {
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.bnb-wc-services h4 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.bnb-wc-service-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
margin-bottom: 8px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.bnb-wc-service-item:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.bnb-wc-service-item.selected {
|
||||
background: #e6f4ea;
|
||||
border: 1px solid #d1e7d7;
|
||||
}
|
||||
|
||||
.bnb-wc-service-item input[type="checkbox"] {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.bnb-wc-service-item .service-name {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.bnb-wc-service-item .service-price {
|
||||
color: #2271b1;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bnb-wc-service-item .service-qty {
|
||||
margin-left: 10px;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
/* Add to cart button */
|
||||
.bnb-wc-booking-form .add-to-cart-btn {
|
||||
width: 100%;
|
||||
padding: 12px 20px;
|
||||
background: #2271b1;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 1.1em;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.bnb-wc-booking-form .add-to-cart-btn:hover {
|
||||
background: #135e96;
|
||||
}
|
||||
|
||||
.bnb-wc-booking-form .add-to-cart-btn:disabled {
|
||||
background: #c3c4c7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Admin Order - Booking Info
|
||||
========================================================================== */
|
||||
|
||||
.bnb-order-booking-info {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: #f6f7f7;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.bnb-order-booking-info h3 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 13px;
|
||||
color: #1d2327;
|
||||
}
|
||||
|
||||
.bnb-order-booking-info p {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.bnb-order-booking-info a {
|
||||
color: #2271b1;
|
||||
}
|
||||
|
||||
.bnb-order-booking-info a:hover {
|
||||
color: #135e96;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Admin List Table - Order Columns
|
||||
========================================================================== */
|
||||
|
||||
.column-wc_order {
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
.column-bnb_booking {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
.column-bnb_booking small {
|
||||
display: block;
|
||||
color: #787c82;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* Order action buttons */
|
||||
.wc-action-button-view_booking::after {
|
||||
font-family: dashicons;
|
||||
content: "\f513";
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Responsive
|
||||
========================================================================== */
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.bnb-wc-booking-form .form-row-inline {
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.bnb-booking-details span {
|
||||
display: block;
|
||||
margin-right: 0;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.bnb-booking-details span::before {
|
||||
content: "";
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
358
assets/js/wc-integration.js
Normal file
358
assets/js/wc-integration.js
Normal file
@@ -0,0 +1,358 @@
|
||||
/**
|
||||
* WooCommerce Integration Scripts
|
||||
*
|
||||
* Handles booking form interactions for WooCommerce integration.
|
||||
*
|
||||
* @package Magdev\WpBnb
|
||||
*/
|
||||
|
||||
(function ($) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* WP BnB WooCommerce Integration
|
||||
*/
|
||||
const WpBnbWC = {
|
||||
/**
|
||||
* Settings from localization
|
||||
*/
|
||||
settings: {},
|
||||
|
||||
/**
|
||||
* Initialize
|
||||
*/
|
||||
init: function () {
|
||||
this.settings = window.wpBnbWC || {};
|
||||
this.bindEvents();
|
||||
this.initBookingForms();
|
||||
},
|
||||
|
||||
/**
|
||||
* Bind global events
|
||||
*/
|
||||
bindEvents: function () {
|
||||
// Admin: Sync all rooms button
|
||||
$(document).on('click', '.bnb-sync-rooms-btn', this.handleSyncRooms.bind(this));
|
||||
|
||||
// Admin: Generate invoice button
|
||||
$(document).on('click', '.bnb-generate-invoice-btn', this.handleGenerateInvoice.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize booking forms
|
||||
*/
|
||||
initBookingForms: function () {
|
||||
$('.bnb-wc-booking-form').each(function () {
|
||||
new BookingForm($(this));
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle sync all rooms button
|
||||
*/
|
||||
handleSyncRooms: function (e) {
|
||||
e.preventDefault();
|
||||
const $btn = $(e.currentTarget);
|
||||
const $status = $btn.siblings('.sync-status');
|
||||
|
||||
$btn.prop('disabled', true).addClass('updating');
|
||||
$status.text(this.settings.i18n?.syncing || 'Syncing...');
|
||||
|
||||
$.ajax({
|
||||
url: this.settings.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'wp_bnb_sync_all_rooms',
|
||||
nonce: this.settings.nonce
|
||||
},
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
$status.html('<span class="success">' + response.data.message + '</span>');
|
||||
} else {
|
||||
$status.html('<span class="error">' + (response.data?.message || 'Error') + '</span>');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
$status.html('<span class="error">' + (WpBnbWC.settings.i18n?.error || 'Error occurred') + '</span>');
|
||||
},
|
||||
complete: function () {
|
||||
$btn.prop('disabled', false).removeClass('updating');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle generate invoice button
|
||||
*/
|
||||
handleGenerateInvoice: function (e) {
|
||||
e.preventDefault();
|
||||
const $btn = $(e.currentTarget);
|
||||
const orderId = $btn.data('order-id');
|
||||
|
||||
$btn.prop('disabled', true).addClass('updating');
|
||||
|
||||
$.ajax({
|
||||
url: this.settings.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'wp_bnb_generate_invoice',
|
||||
nonce: this.settings.nonce,
|
||||
order_id: orderId
|
||||
},
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert(response.data?.message || 'Error generating invoice');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
alert(WpBnbWC.settings.i18n?.error || 'Error occurred');
|
||||
},
|
||||
complete: function () {
|
||||
$btn.prop('disabled', false).removeClass('updating');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Booking Form Handler
|
||||
*/
|
||||
class BookingForm {
|
||||
constructor($form) {
|
||||
this.$form = $form;
|
||||
this.roomId = $form.data('room-id');
|
||||
this.productId = $form.data('product-id');
|
||||
this.checkAvailabilityTimeout = null;
|
||||
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
this.$form.on('change', '.bnb-date-input', this.onDateChange.bind(this));
|
||||
this.$form.on('change', '.bnb-guests-input', this.onGuestsChange.bind(this));
|
||||
this.$form.on('change', '.bnb-service-checkbox', this.onServiceChange.bind(this));
|
||||
this.$form.on('change', '.bnb-service-qty', this.onServiceQtyChange.bind(this));
|
||||
this.$form.on('submit', this.onSubmit.bind(this));
|
||||
}
|
||||
|
||||
onDateChange() {
|
||||
const checkIn = this.$form.find('[name="bnb_check_in"]').val();
|
||||
const checkOut = this.$form.find('[name="bnb_check_out"]').val();
|
||||
|
||||
// Validate dates
|
||||
if (!checkIn || !checkOut) {
|
||||
this.updateAvailabilityStatus('');
|
||||
return;
|
||||
}
|
||||
|
||||
const checkInDate = new Date(checkIn);
|
||||
const checkOutDate = new Date(checkOut);
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
if (checkInDate < today) {
|
||||
this.updateAvailabilityStatus('unavailable', WpBnbWC.settings.i18n?.pastDate || 'Check-in cannot be in the past');
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkOutDate <= checkInDate) {
|
||||
this.updateAvailabilityStatus('unavailable', WpBnbWC.settings.i18n?.invalidDates || 'Check-out must be after check-in');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check availability
|
||||
this.checkAvailability(checkIn, checkOut);
|
||||
}
|
||||
|
||||
onGuestsChange() {
|
||||
// Re-validate if needed
|
||||
const checkIn = this.$form.find('[name="bnb_check_in"]').val();
|
||||
const checkOut = this.$form.find('[name="bnb_check_out"]').val();
|
||||
|
||||
if (checkIn && checkOut) {
|
||||
this.checkAvailability(checkIn, checkOut);
|
||||
}
|
||||
}
|
||||
|
||||
onServiceChange(e) {
|
||||
const $checkbox = $(e.currentTarget);
|
||||
const $item = $checkbox.closest('.bnb-wc-service-item');
|
||||
const $qtyInput = $item.find('.bnb-service-qty');
|
||||
|
||||
if ($checkbox.is(':checked')) {
|
||||
$item.addClass('selected');
|
||||
$qtyInput.prop('disabled', false);
|
||||
} else {
|
||||
$item.removeClass('selected');
|
||||
$qtyInput.prop('disabled', true);
|
||||
}
|
||||
|
||||
this.updatePriceDisplay();
|
||||
}
|
||||
|
||||
onServiceQtyChange() {
|
||||
this.updatePriceDisplay();
|
||||
}
|
||||
|
||||
checkAvailability(checkIn, checkOut) {
|
||||
// Debounce
|
||||
clearTimeout(this.checkAvailabilityTimeout);
|
||||
|
||||
this.updateAvailabilityStatus('checking', WpBnbWC.settings.i18n?.checking || 'Checking availability...');
|
||||
|
||||
this.checkAvailabilityTimeout = setTimeout(() => {
|
||||
const guests = this.$form.find('[name="bnb_guests"]').val() || 1;
|
||||
|
||||
$.ajax({
|
||||
url: WpBnbWC.settings.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'wp_bnb_get_availability',
|
||||
nonce: WpBnbWC.settings.nonce,
|
||||
room_id: this.roomId,
|
||||
check_in: checkIn,
|
||||
check_out: checkOut,
|
||||
guests: guests
|
||||
},
|
||||
success: (response) => {
|
||||
if (response.success) {
|
||||
const data = response.data;
|
||||
|
||||
if (data.available) {
|
||||
this.updateAvailabilityStatus('available', WpBnbWC.settings.i18n?.available || 'Available');
|
||||
this.updatePriceDisplay(data.price, data.breakdown);
|
||||
this.enableSubmit();
|
||||
} else {
|
||||
this.updateAvailabilityStatus('unavailable', data.message || WpBnbWC.settings.i18n?.unavailable || 'Not available');
|
||||
this.disableSubmit();
|
||||
}
|
||||
} else {
|
||||
this.updateAvailabilityStatus('unavailable', response.data?.message || 'Error checking availability');
|
||||
this.disableSubmit();
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
this.updateAvailabilityStatus('unavailable', WpBnbWC.settings.i18n?.error || 'Error checking availability');
|
||||
this.disableSubmit();
|
||||
}
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
|
||||
updateAvailabilityStatus(status, message) {
|
||||
const $statusEl = this.$form.find('.bnb-availability-status');
|
||||
|
||||
if (!status) {
|
||||
$statusEl.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
$statusEl.removeClass('checking available unavailable').addClass(status);
|
||||
|
||||
let icon = '';
|
||||
switch (status) {
|
||||
case 'checking':
|
||||
icon = '<span class="dashicons dashicons-update"></span>';
|
||||
break;
|
||||
case 'available':
|
||||
icon = '<span class="dashicons dashicons-yes-alt"></span>';
|
||||
break;
|
||||
case 'unavailable':
|
||||
icon = '<span class="dashicons dashicons-no-alt"></span>';
|
||||
break;
|
||||
}
|
||||
|
||||
$statusEl.html(icon + ' ' + message).show();
|
||||
}
|
||||
|
||||
updatePriceDisplay(roomPrice, breakdown) {
|
||||
const $priceDisplay = this.$form.find('.bnb-wc-price-display');
|
||||
|
||||
if (!roomPrice) {
|
||||
$priceDisplay.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate services total
|
||||
let servicesTotal = 0;
|
||||
const nights = breakdown?.nights || 1;
|
||||
|
||||
this.$form.find('.bnb-wc-service-item').each(function () {
|
||||
const $item = $(this);
|
||||
const $checkbox = $item.find('.bnb-service-checkbox');
|
||||
|
||||
if ($checkbox.is(':checked')) {
|
||||
const price = parseFloat($item.data('price')) || 0;
|
||||
const pricingType = $item.data('pricing-type');
|
||||
const qty = parseInt($item.find('.bnb-service-qty').val()) || 1;
|
||||
|
||||
if (pricingType === 'per_night') {
|
||||
servicesTotal += price * qty * nights;
|
||||
} else if (pricingType === 'per_booking') {
|
||||
servicesTotal += price * qty;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const grandTotal = roomPrice + servicesTotal;
|
||||
|
||||
// Update display
|
||||
let html = '<div class="price-row"><span>' + (WpBnbWC.settings.i18n?.roomTotal || 'Room') + '</span><span>' + WpBnbWC.formatPrice(roomPrice) + '</span></div>';
|
||||
|
||||
if (servicesTotal > 0) {
|
||||
html += '<div class="price-row"><span>' + (WpBnbWC.settings.i18n?.services || 'Services') + '</span><span>' + WpBnbWC.formatPrice(servicesTotal) + '</span></div>';
|
||||
}
|
||||
|
||||
html += '<div class="price-row total"><span>' + (WpBnbWC.settings.i18n?.total || 'Total') + '</span><span>' + WpBnbWC.formatPrice(grandTotal) + '</span></div>';
|
||||
|
||||
$priceDisplay.html(html).show();
|
||||
}
|
||||
|
||||
enableSubmit() {
|
||||
this.$form.find('.add-to-cart-btn').prop('disabled', false);
|
||||
}
|
||||
|
||||
disableSubmit() {
|
||||
this.$form.find('.add-to-cart-btn').prop('disabled', true);
|
||||
}
|
||||
|
||||
onSubmit(e) {
|
||||
// Form will submit normally - WooCommerce handles the add to cart
|
||||
// Just validate one more time
|
||||
const checkIn = this.$form.find('[name="bnb_check_in"]').val();
|
||||
const checkOut = this.$form.find('[name="bnb_check_out"]').val();
|
||||
|
||||
if (!checkIn || !checkOut) {
|
||||
e.preventDefault();
|
||||
alert(WpBnbWC.settings.i18n?.selectDates || 'Please select check-in and check-out dates');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format price
|
||||
*/
|
||||
WpBnbWC.formatPrice = function (price) {
|
||||
const currency = this.settings.currency || 'CHF';
|
||||
const symbol = this.settings.currencySymbol || currency;
|
||||
const decimals = this.settings.priceDecimals || 2;
|
||||
const decimalSep = this.settings.decimalSeparator || '.';
|
||||
const thousandSep = this.settings.thousandSeparator || "'";
|
||||
|
||||
const formatted = price.toFixed(decimals).replace(/\B(?=(\d{3})+(?!\d))/g, thousandSep).replace('.', decimalSep);
|
||||
|
||||
return symbol + ' ' + formatted;
|
||||
};
|
||||
|
||||
// Initialize on DOM ready
|
||||
$(document).ready(function () {
|
||||
WpBnbWC.init();
|
||||
});
|
||||
|
||||
// Export for external use
|
||||
window.WpBnbWC = WpBnbWC;
|
||||
|
||||
})(jQuery);
|
||||
BIN
languages/wp-bnb-de_CH.mo
Normal file
BIN
languages/wp-bnb-de_CH.mo
Normal file
Binary file not shown.
5162
languages/wp-bnb-de_CH.po
Normal file
5162
languages/wp-bnb-de_CH.po
Normal file
File diff suppressed because it is too large
Load Diff
5163
languages/wp-bnb.pot
Normal file
5163
languages/wp-bnb.pot
Normal file
File diff suppressed because it is too large
Load Diff
@@ -256,16 +256,20 @@ final class Calendar {
|
||||
<tbody>
|
||||
<?php foreach ( $rooms as $room ) : ?>
|
||||
<?php
|
||||
$room_number = get_post_meta( $room->ID, '_bnb_room_room_number', true );
|
||||
$booked_dates = Availability::get_booked_dates( $room->ID, $year, $month );
|
||||
$room_number = get_post_meta( $room->ID, '_bnb_room_room_number', true );
|
||||
$room_building = Room::get_building( $room->ID );
|
||||
$booked_dates = Availability::get_booked_dates( $room->ID, $year, $month );
|
||||
?>
|
||||
<tr>
|
||||
<td class="bnb-calendar-room">
|
||||
<a href="<?php echo esc_url( get_edit_post_link( $room->ID ) ); ?>">
|
||||
<?php echo esc_html( $room->post_title ); ?>
|
||||
<?php if ( $room_number ) : ?>
|
||||
<span class="room-number">#<?php echo esc_html( $room_number ); ?></span>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
<?php if ( $room_number ) : ?>
|
||||
<br><small>#<?php echo esc_html( $room_number ); ?></small>
|
||||
<?php if ( $room_building ) : ?>
|
||||
<span class="building-name"><?php echo esc_html( $room_building->post_title ); ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<?php for ( $day = 1; $day <= $days_in_month; $day++ ) : ?>
|
||||
|
||||
@@ -22,23 +22,61 @@ final class RateLimiter {
|
||||
private const TRANSIENT_PREFIX = 'wp_bnb_rate_';
|
||||
|
||||
/**
|
||||
* Rate limits per minute by endpoint type.
|
||||
* Default rate limits per minute by endpoint type.
|
||||
*
|
||||
* @var array<string, int>
|
||||
*/
|
||||
private array $limits = array(
|
||||
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 = 60;
|
||||
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.
|
||||
|
||||
282
src/Integration/WooCommerce/AdminColumns.php
Normal file
282
src/Integration/WooCommerce/AdminColumns.php
Normal file
@@ -0,0 +1,282 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Admin Columns.
|
||||
*
|
||||
* Adds cross-reference columns and links between bookings and orders.
|
||||
*
|
||||
* @package Magdev\WpBnb\Integration\WooCommerce
|
||||
*/
|
||||
|
||||
declare( strict_types=1 );
|
||||
|
||||
namespace Magdev\WpBnb\Integration\WooCommerce;
|
||||
|
||||
use Magdev\WpBnb\PostTypes\Booking;
|
||||
|
||||
/**
|
||||
* Admin Columns class.
|
||||
*
|
||||
* Enhances admin list tables with booking-order cross-references.
|
||||
*/
|
||||
final class AdminColumns {
|
||||
|
||||
/**
|
||||
* Initialize admin columns.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init(): void {
|
||||
// Add WC Order column to bookings list.
|
||||
add_filter( 'manage_' . Booking::POST_TYPE . '_posts_columns', array( self::class, 'add_booking_columns' ) );
|
||||
add_action( 'manage_' . Booking::POST_TYPE . '_posts_custom_column', array( self::class, 'render_booking_column' ), 10, 2 );
|
||||
|
||||
// Add Booking column to WC orders list.
|
||||
add_filter( 'manage_edit-shop_order_columns', array( self::class, 'add_order_columns' ) );
|
||||
add_action( 'manage_shop_order_posts_custom_column', array( self::class, 'render_order_column' ), 10, 2 );
|
||||
|
||||
// Also support HPOS orders list.
|
||||
add_filter( 'manage_woocommerce_page_wc-orders_columns', array( self::class, 'add_order_columns' ) );
|
||||
add_action( 'manage_woocommerce_page_wc-orders_custom_column', array( self::class, 'render_order_column_hpos' ), 10, 2 );
|
||||
|
||||
// Add row actions.
|
||||
add_filter( 'post_row_actions', array( self::class, 'add_booking_row_actions' ), 10, 2 );
|
||||
|
||||
// Add order row actions.
|
||||
add_filter( 'woocommerce_admin_order_actions', array( self::class, 'add_order_actions' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add columns to booking admin list.
|
||||
*
|
||||
* @param array $columns Existing columns.
|
||||
* @return array Modified columns.
|
||||
*/
|
||||
public static function add_booking_columns( array $columns ): array {
|
||||
// Insert WC Order column after status.
|
||||
$new_columns = array();
|
||||
|
||||
foreach ( $columns as $key => $value ) {
|
||||
$new_columns[ $key ] = $value;
|
||||
|
||||
if ( 'status' === $key ) {
|
||||
$new_columns['wc_order'] = __( 'WC Order', 'wp-bnb' );
|
||||
}
|
||||
}
|
||||
|
||||
// If status column doesn't exist, add at the end.
|
||||
if ( ! isset( $new_columns['wc_order'] ) ) {
|
||||
$new_columns['wc_order'] = __( 'WC Order', 'wp-bnb' );
|
||||
}
|
||||
|
||||
return $new_columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render booking admin column.
|
||||
*
|
||||
* @param string $column Column name.
|
||||
* @param int $post_id Post ID.
|
||||
* @return void
|
||||
*/
|
||||
public static function render_booking_column( string $column, int $post_id ): void {
|
||||
if ( 'wc_order' !== $column ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$order = Manager::get_order_for_booking( $post_id );
|
||||
|
||||
if ( ! $order ) {
|
||||
echo '<span class="na">–</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
$order_number = $order->get_order_number();
|
||||
$order_status = $order->get_status();
|
||||
$edit_url = $order->get_edit_order_url();
|
||||
|
||||
printf(
|
||||
'<a href="%s" class="order-view" title="%s">#%s</a>',
|
||||
esc_url( $edit_url ),
|
||||
esc_attr__( 'View order', 'wp-bnb' ),
|
||||
esc_html( $order_number )
|
||||
);
|
||||
|
||||
// Status badge.
|
||||
$status_name = wc_get_order_status_name( $order_status );
|
||||
printf(
|
||||
'<br><mark class="order-status status-%s"><span>%s</span></mark>',
|
||||
esc_attr( $order_status ),
|
||||
esc_html( $status_name )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add columns to WooCommerce orders list.
|
||||
*
|
||||
* @param array $columns Existing columns.
|
||||
* @return array Modified columns.
|
||||
*/
|
||||
public static function add_order_columns( array $columns ): array {
|
||||
// Insert Booking column after order_status.
|
||||
$new_columns = array();
|
||||
|
||||
foreach ( $columns as $key => $value ) {
|
||||
$new_columns[ $key ] = $value;
|
||||
|
||||
if ( 'order_status' === $key ) {
|
||||
$new_columns['bnb_booking'] = __( 'Booking', 'wp-bnb' );
|
||||
}
|
||||
}
|
||||
|
||||
// If status column doesn't exist, add before actions.
|
||||
if ( ! isset( $new_columns['bnb_booking'] ) ) {
|
||||
$columns_before = array_slice( $columns, 0, -1, true );
|
||||
$columns_after = array_slice( $columns, -1, null, true );
|
||||
|
||||
$new_columns = $columns_before + array( 'bnb_booking' => __( 'Booking', 'wp-bnb' ) ) + $columns_after;
|
||||
}
|
||||
|
||||
return $new_columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render order admin column (legacy post-based orders).
|
||||
*
|
||||
* @param string $column Column name.
|
||||
* @param int $post_id Post ID (Order ID).
|
||||
* @return void
|
||||
*/
|
||||
public static function render_order_column( string $column, int $post_id ): void {
|
||||
if ( 'bnb_booking' !== $column ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$order = wc_get_order( $post_id );
|
||||
|
||||
if ( ! $order ) {
|
||||
echo '<span class="na">–</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
self::render_booking_info_for_order( $order );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render order admin column (HPOS).
|
||||
*
|
||||
* @param string $column Column name.
|
||||
* @param \WC_Order $order Order object.
|
||||
* @return void
|
||||
*/
|
||||
public static function render_order_column_hpos( string $column, $order ): void {
|
||||
if ( 'bnb_booking' !== $column ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! $order instanceof \WC_Order ) {
|
||||
echo '<span class="na">–</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
self::render_booking_info_for_order( $order );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render booking info for an order.
|
||||
*
|
||||
* @param \WC_Order $order WooCommerce order.
|
||||
* @return void
|
||||
*/
|
||||
private static function render_booking_info_for_order( \WC_Order $order ): void {
|
||||
$booking_id = Manager::get_booking_for_order( $order );
|
||||
|
||||
if ( ! $booking_id ) {
|
||||
echo '<span class="na">–</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
$booking = get_post( $booking_id );
|
||||
$check_in = get_post_meta( $booking_id, '_bnb_booking_check_in', true );
|
||||
$check_out = get_post_meta( $booking_id, '_bnb_booking_check_out', true );
|
||||
$status = get_post_meta( $booking_id, '_bnb_booking_status', true );
|
||||
|
||||
// Booking link.
|
||||
printf(
|
||||
'<a href="%s" title="%s">%s</a>',
|
||||
esc_url( get_edit_post_link( $booking_id ) ),
|
||||
esc_attr__( 'View booking', 'wp-bnb' ),
|
||||
$booking ? esc_html( wp_trim_words( $booking->post_title, 3 ) ) : '#' . esc_html( $booking_id )
|
||||
);
|
||||
|
||||
// Dates.
|
||||
if ( $check_in && $check_out ) {
|
||||
$check_in_date = \DateTime::createFromFormat( 'Y-m-d', $check_in );
|
||||
$check_out_date = \DateTime::createFromFormat( 'Y-m-d', $check_out );
|
||||
|
||||
if ( $check_in_date && $check_out_date ) {
|
||||
printf(
|
||||
'<br><small>%s - %s</small>',
|
||||
esc_html( $check_in_date->format( 'd.m' ) ),
|
||||
esc_html( $check_out_date->format( 'd.m.y' ) )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Status badge.
|
||||
if ( $status ) {
|
||||
printf(
|
||||
'<br><span class="bnb-status-badge bnb-status-%s" style="font-size: 10px;">%s</span>',
|
||||
esc_attr( $status ),
|
||||
esc_html( ucfirst( str_replace( '_', ' ', $status ) ) )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add row actions to booking list.
|
||||
*
|
||||
* @param array $actions Existing actions.
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return array Modified actions.
|
||||
*/
|
||||
public static function add_booking_row_actions( array $actions, \WP_Post $post ): array {
|
||||
if ( Booking::POST_TYPE !== $post->post_type ) {
|
||||
return $actions;
|
||||
}
|
||||
|
||||
$order = Manager::get_order_for_booking( $post->ID );
|
||||
|
||||
if ( $order ) {
|
||||
$actions['view_order'] = sprintf(
|
||||
'<a href="%s" aria-label="%s">%s</a>',
|
||||
esc_url( $order->get_edit_order_url() ),
|
||||
/* translators: %s: Order number */
|
||||
esc_attr( sprintf( __( 'View order #%s', 'wp-bnb' ), $order->get_order_number() ) ),
|
||||
__( 'View Order', 'wp-bnb' )
|
||||
);
|
||||
}
|
||||
|
||||
return $actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add actions to WooCommerce order row.
|
||||
*
|
||||
* @param array $actions Existing actions.
|
||||
* @param \WC_Order $order Order object.
|
||||
* @return array Modified actions.
|
||||
*/
|
||||
public static function add_order_actions( array $actions, \WC_Order $order ): array {
|
||||
$booking_id = Manager::get_booking_for_order( $order );
|
||||
|
||||
if ( $booking_id ) {
|
||||
$actions['view_booking'] = array(
|
||||
'url' => get_edit_post_link( $booking_id ),
|
||||
'name' => __( 'View Booking', 'wp-bnb' ),
|
||||
'action' => 'view_booking',
|
||||
);
|
||||
}
|
||||
|
||||
return $actions;
|
||||
}
|
||||
}
|
||||
545
src/Integration/WooCommerce/CartHandler.php
Normal file
545
src/Integration/WooCommerce/CartHandler.php
Normal file
@@ -0,0 +1,545 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Cart Handler.
|
||||
*
|
||||
* Handles cart item data, availability validation, and dynamic pricing.
|
||||
*
|
||||
* @package Magdev\WpBnb\Integration\WooCommerce
|
||||
*/
|
||||
|
||||
declare( strict_types=1 );
|
||||
|
||||
namespace Magdev\WpBnb\Integration\WooCommerce;
|
||||
|
||||
use Magdev\WpBnb\Booking\Availability;
|
||||
use Magdev\WpBnb\PostTypes\Room;
|
||||
use Magdev\WpBnb\PostTypes\Service;
|
||||
use Magdev\WpBnb\Pricing\Calculator;
|
||||
|
||||
/**
|
||||
* Cart Handler class.
|
||||
*
|
||||
* Manages booking data in the WooCommerce cart, validates availability,
|
||||
* and calculates dynamic pricing based on dates and services.
|
||||
*/
|
||||
final class CartHandler {
|
||||
|
||||
/**
|
||||
* Initialize cart handler.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init(): void {
|
||||
// Add booking data to cart item.
|
||||
add_filter( 'woocommerce_add_cart_item_data', array( self::class, 'add_cart_item_data' ), 10, 3 );
|
||||
|
||||
// Restore booking data from session.
|
||||
add_filter( 'woocommerce_get_cart_item_from_session', array( self::class, 'get_cart_item_from_session' ), 10, 2 );
|
||||
|
||||
// Validate before adding to cart.
|
||||
add_filter( 'woocommerce_add_to_cart_validation', array( self::class, 'validate_add_to_cart' ), 10, 5 );
|
||||
|
||||
// Re-validate availability on cart load.
|
||||
add_action( 'woocommerce_cart_loaded_from_session', array( self::class, 'validate_cart_items' ) );
|
||||
|
||||
// Display booking info in cart.
|
||||
add_filter( 'woocommerce_get_item_data', array( self::class, 'display_cart_item_data' ), 10, 2 );
|
||||
|
||||
// Calculate dynamic price.
|
||||
add_action( 'woocommerce_before_calculate_totals', array( self::class, 'calculate_cart_item_prices' ) );
|
||||
|
||||
// Prevent quantity changes for room products.
|
||||
add_filter( 'woocommerce_quantity_input_args', array( self::class, 'lock_quantity' ), 10, 2 );
|
||||
|
||||
// Add booking data to order item meta.
|
||||
add_action( 'woocommerce_checkout_create_order_line_item', array( self::class, 'add_order_item_meta' ), 10, 4 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a room to cart with booking data.
|
||||
*
|
||||
* @param int $room_id Room post ID.
|
||||
* @param string $check_in Check-in date (Y-m-d).
|
||||
* @param string $check_out Check-out date (Y-m-d).
|
||||
* @param int $guests Number of guests.
|
||||
* @param array $services Array of service selections.
|
||||
* @return string|bool Cart item key or false on failure.
|
||||
*/
|
||||
public static function add_room_to_cart(
|
||||
int $room_id,
|
||||
string $check_in,
|
||||
string $check_out,
|
||||
int $guests = 1,
|
||||
array $services = array()
|
||||
) {
|
||||
// Get product ID for room.
|
||||
$product_id = ProductSync::get_product_for_room( $room_id );
|
||||
|
||||
if ( ! $product_id ) {
|
||||
// Try to sync the room first.
|
||||
$product_id = ProductSync::sync_room_to_product( $room_id );
|
||||
|
||||
if ( ! $product_id ) {
|
||||
wc_add_notice( __( 'Unable to add room to cart. Product not found.', 'wp-bnb' ), 'error' );
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Store booking data in session temporarily for the filter.
|
||||
WC()->session->set(
|
||||
'bnb_pending_booking',
|
||||
array(
|
||||
'room_id' => $room_id,
|
||||
'check_in' => $check_in,
|
||||
'check_out' => $check_out,
|
||||
'guests' => $guests,
|
||||
'services' => $services,
|
||||
)
|
||||
);
|
||||
|
||||
// Add to cart.
|
||||
$cart_item_key = WC()->cart->add_to_cart( $product_id, 1 );
|
||||
|
||||
// Clean up session.
|
||||
WC()->session->set( 'bnb_pending_booking', null );
|
||||
|
||||
return $cart_item_key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add booking data to cart item when adding to cart.
|
||||
*
|
||||
* @param array $cart_item_data Cart item data.
|
||||
* @param int $product_id Product ID.
|
||||
* @param int $variation_id Variation ID.
|
||||
* @return array Modified cart item data.
|
||||
*/
|
||||
public static function add_cart_item_data( array $cart_item_data, int $product_id, int $variation_id ): array {
|
||||
// Check if this is a room product.
|
||||
$room_id = ProductSync::get_room_for_product( $product_id );
|
||||
|
||||
if ( ! $room_id ) {
|
||||
return $cart_item_data;
|
||||
}
|
||||
|
||||
// Get booking data from session or POST.
|
||||
$booking_data = WC()->session->get( 'bnb_pending_booking' );
|
||||
|
||||
if ( ! $booking_data ) {
|
||||
// Try to get from POST (for direct form submissions).
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Missing
|
||||
$booking_data = array(
|
||||
'room_id' => isset( $_POST['bnb_room_id'] ) ? absint( $_POST['bnb_room_id'] ) : $room_id,
|
||||
'check_in' => isset( $_POST['bnb_check_in'] ) ? sanitize_text_field( wp_unslash( $_POST['bnb_check_in'] ) ) : '',
|
||||
'check_out' => isset( $_POST['bnb_check_out'] ) ? sanitize_text_field( wp_unslash( $_POST['bnb_check_out'] ) ) : '',
|
||||
'guests' => isset( $_POST['bnb_guests'] ) ? absint( $_POST['bnb_guests'] ) : 1,
|
||||
'services' => isset( $_POST['bnb_services'] ) ? self::sanitize_services( $_POST['bnb_services'] ) : array(),
|
||||
);
|
||||
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
||||
}
|
||||
|
||||
// Validate required fields.
|
||||
if ( empty( $booking_data['check_in'] ) || empty( $booking_data['check_out'] ) ) {
|
||||
return $cart_item_data;
|
||||
}
|
||||
|
||||
// Calculate nights.
|
||||
$check_in = new \DateTime( $booking_data['check_in'] );
|
||||
$check_out = new \DateTime( $booking_data['check_out'] );
|
||||
$nights = (int) $check_in->diff( $check_out )->days;
|
||||
|
||||
if ( $nights < 1 ) {
|
||||
return $cart_item_data;
|
||||
}
|
||||
|
||||
// Calculate price breakdown.
|
||||
$calculator = new Calculator( $room_id, $booking_data['check_in'], $booking_data['check_out'] );
|
||||
$price_breakdown = $calculator->calculate();
|
||||
$room_total = $price_breakdown['total'];
|
||||
|
||||
// Calculate services total.
|
||||
$services_total = 0;
|
||||
$services_data = array();
|
||||
|
||||
foreach ( $booking_data['services'] as $service_selection ) {
|
||||
$service_id = $service_selection['service_id'] ?? 0;
|
||||
$quantity = $service_selection['quantity'] ?? 1;
|
||||
|
||||
if ( ! $service_id ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$service_price = Service::calculate_service_price( $service_id, $quantity, $nights );
|
||||
$service_data = Service::get_service_data( $service_id );
|
||||
|
||||
$services_data[] = array(
|
||||
'service_id' => $service_id,
|
||||
'quantity' => $quantity,
|
||||
'price' => $service_price,
|
||||
'pricing_type' => $service_data['pricing_type'] ?? 'per_booking',
|
||||
'name' => $service_data['name'] ?? '',
|
||||
);
|
||||
|
||||
$services_total += $service_price;
|
||||
}
|
||||
|
||||
// Store booking data.
|
||||
$cart_item_data[ Manager::CART_ITEM_KEY ] = array(
|
||||
'room_id' => $room_id,
|
||||
'check_in' => $booking_data['check_in'],
|
||||
'check_out' => $booking_data['check_out'],
|
||||
'guests' => $booking_data['guests'],
|
||||
'nights' => $nights,
|
||||
'services' => $services_data,
|
||||
'price_breakdown' => array(
|
||||
'room_total' => $room_total,
|
||||
'services_total' => $services_total,
|
||||
'grand_total' => $room_total + $services_total,
|
||||
'full_breakdown' => $price_breakdown,
|
||||
),
|
||||
);
|
||||
|
||||
// Generate unique key based on booking data to allow multiple bookings.
|
||||
$cart_item_data['unique_key'] = md5(
|
||||
$room_id . $booking_data['check_in'] . $booking_data['check_out'] . microtime()
|
||||
);
|
||||
|
||||
return $cart_item_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore booking data from session.
|
||||
*
|
||||
* @param array $cart_item Cart item data.
|
||||
* @param array $values Session values.
|
||||
* @return array Modified cart item data.
|
||||
*/
|
||||
public static function get_cart_item_from_session( array $cart_item, array $values ): array {
|
||||
if ( isset( $values[ Manager::CART_ITEM_KEY ] ) ) {
|
||||
$cart_item[ Manager::CART_ITEM_KEY ] = $values[ Manager::CART_ITEM_KEY ];
|
||||
}
|
||||
return $cart_item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate room availability before adding to cart.
|
||||
*
|
||||
* @param bool $passed Whether validation passed.
|
||||
* @param int $product_id Product ID.
|
||||
* @param int $quantity Quantity.
|
||||
* @param int $variation_id Variation ID.
|
||||
* @param array $variations Variations.
|
||||
* @return bool
|
||||
*/
|
||||
public static function validate_add_to_cart( bool $passed, int $product_id, int $quantity, int $variation_id = 0, array $variations = array() ): bool {
|
||||
// Check if this is a room product.
|
||||
$room_id = ProductSync::get_room_for_product( $product_id );
|
||||
|
||||
if ( ! $room_id ) {
|
||||
return $passed;
|
||||
}
|
||||
|
||||
// Get booking data.
|
||||
$booking_data = WC()->session->get( 'bnb_pending_booking' );
|
||||
|
||||
if ( ! $booking_data ) {
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Missing
|
||||
$booking_data = array(
|
||||
'check_in' => isset( $_POST['bnb_check_in'] ) ? sanitize_text_field( wp_unslash( $_POST['bnb_check_in'] ) ) : '',
|
||||
'check_out' => isset( $_POST['bnb_check_out'] ) ? sanitize_text_field( wp_unslash( $_POST['bnb_check_out'] ) ) : '',
|
||||
'guests' => isset( $_POST['bnb_guests'] ) ? absint( $_POST['bnb_guests'] ) : 1,
|
||||
);
|
||||
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
||||
}
|
||||
|
||||
// Validate dates provided.
|
||||
if ( empty( $booking_data['check_in'] ) || empty( $booking_data['check_out'] ) ) {
|
||||
wc_add_notice( __( 'Please select check-in and check-out dates.', 'wp-bnb' ), 'error' );
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate date format.
|
||||
$check_in = \DateTime::createFromFormat( 'Y-m-d', $booking_data['check_in'] );
|
||||
$check_out = \DateTime::createFromFormat( 'Y-m-d', $booking_data['check_out'] );
|
||||
|
||||
if ( ! $check_in || ! $check_out ) {
|
||||
wc_add_notice( __( 'Invalid date format. Please use the date picker.', 'wp-bnb' ), 'error' );
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate check-out after check-in.
|
||||
if ( $check_out <= $check_in ) {
|
||||
wc_add_notice( __( 'Check-out date must be after check-in date.', 'wp-bnb' ), 'error' );
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate not in past.
|
||||
$today = new \DateTime( 'today' );
|
||||
if ( $check_in < $today ) {
|
||||
wc_add_notice( __( 'Check-in date cannot be in the past.', 'wp-bnb' ), 'error' );
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check availability.
|
||||
$is_available = Availability::check_availability(
|
||||
$room_id,
|
||||
$booking_data['check_in'],
|
||||
$booking_data['check_out']
|
||||
);
|
||||
|
||||
if ( ! $is_available ) {
|
||||
wc_add_notice( __( 'Sorry, this room is not available for the selected dates.', 'wp-bnb' ), 'error' );
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check capacity.
|
||||
$capacity = get_post_meta( $room_id, '_bnb_room_capacity', true );
|
||||
if ( $capacity && $booking_data['guests'] > (int) $capacity ) {
|
||||
wc_add_notice(
|
||||
sprintf(
|
||||
/* translators: %d: Room capacity */
|
||||
__( 'This room has a maximum capacity of %d guests.', 'wp-bnb' ),
|
||||
$capacity
|
||||
),
|
||||
'error'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if same room with same dates already in cart.
|
||||
foreach ( WC()->cart->get_cart() as $cart_item ) {
|
||||
if ( isset( $cart_item[ Manager::CART_ITEM_KEY ] ) ) {
|
||||
$existing = $cart_item[ Manager::CART_ITEM_KEY ];
|
||||
if ( $existing['room_id'] === $room_id
|
||||
&& $existing['check_in'] === $booking_data['check_in']
|
||||
&& $existing['check_out'] === $booking_data['check_out']
|
||||
) {
|
||||
wc_add_notice( __( 'This room with the same dates is already in your cart.', 'wp-bnb' ), 'error' );
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $passed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate cart items on cart load.
|
||||
*
|
||||
* @param \WC_Cart $cart Cart object.
|
||||
* @return void
|
||||
*/
|
||||
public static function validate_cart_items( \WC_Cart $cart ): void {
|
||||
$items_to_remove = array();
|
||||
|
||||
foreach ( $cart->get_cart() as $cart_item_key => $cart_item ) {
|
||||
if ( ! isset( $cart_item[ Manager::CART_ITEM_KEY ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$booking_data = $cart_item[ Manager::CART_ITEM_KEY ];
|
||||
|
||||
// Check if dates are still valid.
|
||||
$check_in = \DateTime::createFromFormat( 'Y-m-d', $booking_data['check_in'] );
|
||||
$today = new \DateTime( 'today' );
|
||||
|
||||
if ( $check_in < $today ) {
|
||||
$items_to_remove[] = array(
|
||||
'key' => $cart_item_key,
|
||||
'message' => __( 'A room booking was removed because the check-in date has passed.', 'wp-bnb' ),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if room still available.
|
||||
$is_available = Availability::check_availability(
|
||||
$booking_data['room_id'],
|
||||
$booking_data['check_in'],
|
||||
$booking_data['check_out']
|
||||
);
|
||||
|
||||
if ( ! $is_available ) {
|
||||
$items_to_remove[] = array(
|
||||
'key' => $cart_item_key,
|
||||
'message' => __( 'A room booking was removed because the room is no longer available for those dates.', 'wp-bnb' ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove invalid items.
|
||||
foreach ( $items_to_remove as $item ) {
|
||||
$cart->remove_cart_item( $item['key'] );
|
||||
wc_add_notice( $item['message'], 'error' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display booking info in cart.
|
||||
*
|
||||
* @param array $item_data Item data for display.
|
||||
* @param array $cart_item Cart item.
|
||||
* @return array Modified item data.
|
||||
*/
|
||||
public static function display_cart_item_data( array $item_data, array $cart_item ): array {
|
||||
if ( ! isset( $cart_item[ Manager::CART_ITEM_KEY ] ) ) {
|
||||
return $item_data;
|
||||
}
|
||||
|
||||
$booking_data = $cart_item[ Manager::CART_ITEM_KEY ];
|
||||
|
||||
// Format dates.
|
||||
$date_format = get_option( 'date_format' );
|
||||
$check_in = \DateTime::createFromFormat( 'Y-m-d', $booking_data['check_in'] );
|
||||
$check_out = \DateTime::createFromFormat( 'Y-m-d', $booking_data['check_out'] );
|
||||
|
||||
$item_data[] = array(
|
||||
'key' => __( 'Check-in', 'wp-bnb' ),
|
||||
'value' => $check_in ? $check_in->format( $date_format ) : $booking_data['check_in'],
|
||||
);
|
||||
|
||||
$item_data[] = array(
|
||||
'key' => __( 'Check-out', 'wp-bnb' ),
|
||||
'value' => $check_out ? $check_out->format( $date_format ) : $booking_data['check_out'],
|
||||
);
|
||||
|
||||
$item_data[] = array(
|
||||
'key' => __( 'Nights', 'wp-bnb' ),
|
||||
'value' => $booking_data['nights'],
|
||||
);
|
||||
|
||||
$item_data[] = array(
|
||||
'key' => __( 'Guests', 'wp-bnb' ),
|
||||
'value' => $booking_data['guests'],
|
||||
);
|
||||
|
||||
// Display services.
|
||||
if ( ! empty( $booking_data['services'] ) ) {
|
||||
$services_list = array();
|
||||
foreach ( $booking_data['services'] as $service ) {
|
||||
$services_list[] = $service['name'] . ' × ' . $service['quantity'];
|
||||
}
|
||||
$item_data[] = array(
|
||||
'key' => __( 'Services', 'wp-bnb' ),
|
||||
'value' => implode( ', ', $services_list ),
|
||||
);
|
||||
}
|
||||
|
||||
return $item_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate dynamic prices for cart items.
|
||||
*
|
||||
* @param \WC_Cart $cart Cart object.
|
||||
* @return void
|
||||
*/
|
||||
public static function calculate_cart_item_prices( \WC_Cart $cart ): void {
|
||||
if ( is_admin() && ! defined( 'DOING_AJAX' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( $cart->get_cart() as $cart_item ) {
|
||||
if ( ! isset( $cart_item[ Manager::CART_ITEM_KEY ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$booking_data = $cart_item[ Manager::CART_ITEM_KEY ];
|
||||
$grand_total = $booking_data['price_breakdown']['grand_total'] ?? 0;
|
||||
|
||||
/**
|
||||
* Filter the cart item price for a booking.
|
||||
*
|
||||
* @param float $price The calculated price.
|
||||
* @param array $cart_item The cart item.
|
||||
*/
|
||||
$grand_total = apply_filters( 'wp_bnb_wc_cart_item_price', $grand_total, $cart_item );
|
||||
|
||||
// Set the price.
|
||||
$cart_item['data']->set_price( $grand_total );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock quantity to 1 for room products.
|
||||
*
|
||||
* @param array $args Input arguments.
|
||||
* @param \WC_Product $product Product object.
|
||||
* @return array Modified arguments.
|
||||
*/
|
||||
public static function lock_quantity( array $args, \WC_Product $product ): array {
|
||||
if ( ProductSync::is_room_product( $product ) ) {
|
||||
$args['min_value'] = 1;
|
||||
$args['max_value'] = 1;
|
||||
$args['readonly'] = true;
|
||||
}
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add booking data to order item meta.
|
||||
*
|
||||
* @param \WC_Order_Item_Product $item Order item.
|
||||
* @param string $cart_item_key Cart item key.
|
||||
* @param array $values Cart item values.
|
||||
* @param \WC_Order $order Order object.
|
||||
* @return void
|
||||
*/
|
||||
public static function add_order_item_meta( \WC_Order_Item_Product $item, string $cart_item_key, array $values, \WC_Order $order ): void {
|
||||
if ( ! isset( $values[ Manager::CART_ITEM_KEY ] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$booking_data = $values[ Manager::CART_ITEM_KEY ];
|
||||
|
||||
// Store booking data in order item meta.
|
||||
$item->add_meta_data( '_bnb_room_id', $booking_data['room_id'] );
|
||||
$item->add_meta_data( '_bnb_check_in', $booking_data['check_in'] );
|
||||
$item->add_meta_data( '_bnb_check_out', $booking_data['check_out'] );
|
||||
$item->add_meta_data( '_bnb_guests', $booking_data['guests'] );
|
||||
$item->add_meta_data( '_bnb_nights', $booking_data['nights'] );
|
||||
$item->add_meta_data( '_bnb_services', wp_json_encode( $booking_data['services'] ) );
|
||||
$item->add_meta_data( '_bnb_price_breakdown', wp_json_encode( $booking_data['price_breakdown'] ) );
|
||||
|
||||
// Add visible meta for admin display.
|
||||
$date_format = get_option( 'date_format' );
|
||||
$check_in = \DateTime::createFromFormat( 'Y-m-d', $booking_data['check_in'] );
|
||||
$check_out = \DateTime::createFromFormat( 'Y-m-d', $booking_data['check_out'] );
|
||||
|
||||
$item->add_meta_data( __( 'Check-in', 'wp-bnb' ), $check_in ? $check_in->format( $date_format ) : $booking_data['check_in'] );
|
||||
$item->add_meta_data( __( 'Check-out', 'wp-bnb' ), $check_out ? $check_out->format( $date_format ) : $booking_data['check_out'] );
|
||||
$item->add_meta_data( __( 'Nights', 'wp-bnb' ), $booking_data['nights'] );
|
||||
$item->add_meta_data( __( 'Guests', 'wp-bnb' ), $booking_data['guests'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize services array from POST data.
|
||||
*
|
||||
* @param mixed $services Raw services data.
|
||||
* @return array Sanitized services array.
|
||||
*/
|
||||
private static function sanitize_services( $services ): array {
|
||||
if ( ! is_array( $services ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$sanitized = array();
|
||||
|
||||
foreach ( $services as $service ) {
|
||||
if ( ! is_array( $service ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$service_id = isset( $service['service_id'] ) ? absint( $service['service_id'] ) : 0;
|
||||
$quantity = isset( $service['quantity'] ) ? absint( $service['quantity'] ) : 1;
|
||||
|
||||
if ( $service_id > 0 ) {
|
||||
$sanitized[] = array(
|
||||
'service_id' => $service_id,
|
||||
'quantity' => max( 1, $quantity ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $sanitized;
|
||||
}
|
||||
}
|
||||
347
src/Integration/WooCommerce/CheckoutHandler.php
Normal file
347
src/Integration/WooCommerce/CheckoutHandler.php
Normal file
@@ -0,0 +1,347 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Checkout Handler.
|
||||
*
|
||||
* Handles checkout field customization and validation.
|
||||
*
|
||||
* @package Magdev\WpBnb\Integration\WooCommerce
|
||||
*/
|
||||
|
||||
declare( strict_types=1 );
|
||||
|
||||
namespace Magdev\WpBnb\Integration\WooCommerce;
|
||||
|
||||
use Magdev\WpBnb\Booking\Availability;
|
||||
use Magdev\WpBnb\PostTypes\Guest;
|
||||
use Magdev\WpBnb\PostTypes\Room;
|
||||
|
||||
/**
|
||||
* Checkout Handler class.
|
||||
*
|
||||
* Manages checkout field customization, pre-filling, and validation.
|
||||
*/
|
||||
final class CheckoutHandler {
|
||||
|
||||
/**
|
||||
* Initialize checkout handler.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init(): void {
|
||||
// Add custom checkout fields.
|
||||
add_filter( 'woocommerce_checkout_fields', array( self::class, 'add_checkout_fields' ) );
|
||||
|
||||
// Pre-fill checkout fields.
|
||||
add_filter( 'woocommerce_checkout_get_value', array( self::class, 'prefill_checkout_fields' ), 10, 2 );
|
||||
|
||||
// Validate checkout.
|
||||
add_action( 'woocommerce_after_checkout_validation', array( self::class, 'validate_checkout' ), 10, 2 );
|
||||
|
||||
// Display booking summary in order review.
|
||||
add_action( 'woocommerce_review_order_before_submit', array( self::class, 'display_booking_summary' ) );
|
||||
|
||||
// Save guest notes to order meta.
|
||||
add_action( 'woocommerce_checkout_create_order', array( self::class, 'save_guest_notes' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add custom checkout fields.
|
||||
*
|
||||
* @param array $fields Checkout fields.
|
||||
* @return array Modified checkout fields.
|
||||
*/
|
||||
public static function add_checkout_fields( array $fields ): array {
|
||||
// Only add fields if cart contains BnB bookings.
|
||||
if ( ! self::cart_has_bookings() ) {
|
||||
return $fields;
|
||||
}
|
||||
|
||||
// Add special requests field.
|
||||
$fields['order']['bnb_guest_notes'] = array(
|
||||
'type' => 'textarea',
|
||||
'label' => __( 'Special Requests', 'wp-bnb' ),
|
||||
'placeholder' => __( 'Any special requests, dietary requirements, or preferences...', 'wp-bnb' ),
|
||||
'class' => array( 'form-row-wide' ),
|
||||
'required' => false,
|
||||
'priority' => 90,
|
||||
);
|
||||
|
||||
// Add expected arrival time.
|
||||
$fields['order']['bnb_arrival_time'] = array(
|
||||
'type' => 'select',
|
||||
'label' => __( 'Expected Arrival Time', 'wp-bnb' ),
|
||||
'class' => array( 'form-row-wide' ),
|
||||
'required' => false,
|
||||
'priority' => 85,
|
||||
'options' => self::get_arrival_time_options(),
|
||||
);
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-fill checkout fields from guest data.
|
||||
*
|
||||
* @param mixed $value Current value.
|
||||
* @param string $input Input field name.
|
||||
* @return mixed Pre-filled value.
|
||||
*/
|
||||
public static function prefill_checkout_fields( $value, string $input ) {
|
||||
// Only for logged-in users.
|
||||
if ( ! is_user_logged_in() ) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
// Try to find an existing guest record by email.
|
||||
$current_user = wp_get_current_user();
|
||||
$guest = self::find_guest_by_email( $current_user->user_email );
|
||||
|
||||
if ( ! $guest ) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$mappings = array(
|
||||
'billing_first_name' => '_bnb_guest_first_name',
|
||||
'billing_last_name' => '_bnb_guest_last_name',
|
||||
'billing_email' => '_bnb_guest_email',
|
||||
'billing_phone' => '_bnb_guest_phone',
|
||||
'billing_address_1' => '_bnb_guest_address',
|
||||
'billing_city' => '_bnb_guest_city',
|
||||
'billing_postcode' => '_bnb_guest_postal_code',
|
||||
'billing_country' => '_bnb_guest_country',
|
||||
);
|
||||
|
||||
if ( isset( $mappings[ $input ] ) ) {
|
||||
$meta_value = get_post_meta( $guest->ID, $mappings[ $input ], true );
|
||||
if ( $meta_value ) {
|
||||
return $meta_value;
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate checkout for BnB bookings.
|
||||
*
|
||||
* @param array $data Checkout data.
|
||||
* @param \WP_Error $errors Error object.
|
||||
* @return void
|
||||
*/
|
||||
public static function validate_checkout( array $data, \WP_Error $errors ): void {
|
||||
// Re-validate availability for all bookings.
|
||||
foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
|
||||
if ( ! isset( $cart_item[ Manager::CART_ITEM_KEY ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$booking_data = $cart_item[ Manager::CART_ITEM_KEY ];
|
||||
|
||||
// Check availability one more time.
|
||||
$is_available = Availability::check_availability(
|
||||
$booking_data['room_id'],
|
||||
$booking_data['check_in'],
|
||||
$booking_data['check_out']
|
||||
);
|
||||
|
||||
if ( ! $is_available ) {
|
||||
$room = get_post( $booking_data['room_id'] );
|
||||
$errors->add(
|
||||
'bnb_room_unavailable',
|
||||
sprintf(
|
||||
/* translators: %s: Room name */
|
||||
__( 'Sorry, %s is no longer available for the selected dates. Please update your cart.', 'wp-bnb' ),
|
||||
$room ? $room->post_title : __( 'the room', 'wp-bnb' )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Validate check-in date not in past.
|
||||
$check_in = \DateTime::createFromFormat( 'Y-m-d', $booking_data['check_in'] );
|
||||
$today = new \DateTime( 'today' );
|
||||
|
||||
if ( $check_in < $today ) {
|
||||
$errors->add(
|
||||
'bnb_date_passed',
|
||||
__( 'A booking in your cart has a check-in date in the past. Please update your cart.', 'wp-bnb' )
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display booking summary in order review section.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function display_booking_summary(): void {
|
||||
if ( ! self::cart_has_bookings() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$bookings = array();
|
||||
|
||||
foreach ( WC()->cart->get_cart() as $cart_item ) {
|
||||
if ( ! isset( $cart_item[ Manager::CART_ITEM_KEY ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$booking_data = $cart_item[ Manager::CART_ITEM_KEY ];
|
||||
$room = get_post( $booking_data['room_id'] );
|
||||
|
||||
if ( ! $room ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$building = Room::get_building( $booking_data['room_id'] );
|
||||
|
||||
$date_format = get_option( 'date_format' );
|
||||
$check_in = \DateTime::createFromFormat( 'Y-m-d', $booking_data['check_in'] );
|
||||
$check_out = \DateTime::createFromFormat( 'Y-m-d', $booking_data['check_out'] );
|
||||
|
||||
$bookings[] = array(
|
||||
'room_name' => $room->post_title,
|
||||
'building_name' => $building ? $building->post_title : '',
|
||||
'check_in' => $check_in ? $check_in->format( $date_format ) : '',
|
||||
'check_out' => $check_out ? $check_out->format( $date_format ) : '',
|
||||
'nights' => $booking_data['nights'],
|
||||
'guests' => $booking_data['guests'],
|
||||
);
|
||||
}
|
||||
|
||||
if ( empty( $bookings ) ) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<div class="bnb-checkout-booking-summary">
|
||||
<h3><?php esc_html_e( 'Booking Summary', 'wp-bnb' ); ?></h3>
|
||||
<?php foreach ( $bookings as $booking ) : ?>
|
||||
<div class="bnb-booking-item">
|
||||
<strong><?php echo esc_html( $booking['room_name'] ); ?></strong>
|
||||
<?php if ( $booking['building_name'] ) : ?>
|
||||
<span class="bnb-building-name"><?php echo esc_html( $booking['building_name'] ); ?></span>
|
||||
<?php endif; ?>
|
||||
<div class="bnb-booking-details">
|
||||
<span>
|
||||
<?php
|
||||
printf(
|
||||
/* translators: 1: Check-in date, 2: Check-out date */
|
||||
esc_html__( '%1$s to %2$s', 'wp-bnb' ),
|
||||
esc_html( $booking['check_in'] ),
|
||||
esc_html( $booking['check_out'] )
|
||||
);
|
||||
?>
|
||||
</span>
|
||||
<span>
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %d: Number of nights */
|
||||
esc_html( _n( '%d night', '%d nights', $booking['nights'], 'wp-bnb' ) ),
|
||||
$booking['nights']
|
||||
);
|
||||
?>
|
||||
</span>
|
||||
<span>
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %d: Number of guests */
|
||||
esc_html( _n( '%d guest', '%d guests', $booking['guests'], 'wp-bnb' ) ),
|
||||
$booking['guests']
|
||||
);
|
||||
?>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Save guest notes to order meta.
|
||||
*
|
||||
* @param \WC_Order $order Order object.
|
||||
* @param array $data Checkout data.
|
||||
* @return void
|
||||
*/
|
||||
public static function save_guest_notes( \WC_Order $order, array $data ): void {
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Missing
|
||||
if ( isset( $_POST['bnb_guest_notes'] ) && ! empty( $_POST['bnb_guest_notes'] ) ) {
|
||||
$order->update_meta_data(
|
||||
'_bnb_guest_notes',
|
||||
sanitize_textarea_field( wp_unslash( $_POST['bnb_guest_notes'] ) )
|
||||
);
|
||||
}
|
||||
|
||||
if ( isset( $_POST['bnb_arrival_time'] ) && ! empty( $_POST['bnb_arrival_time'] ) ) {
|
||||
$order->update_meta_data(
|
||||
'_bnb_arrival_time',
|
||||
sanitize_text_field( wp_unslash( $_POST['bnb_arrival_time'] ) )
|
||||
);
|
||||
}
|
||||
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if cart contains BnB bookings.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function cart_has_bookings(): bool {
|
||||
if ( ! WC()->cart ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ( WC()->cart->get_cart() as $cart_item ) {
|
||||
if ( isset( $cart_item[ Manager::CART_ITEM_KEY ] ) ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get arrival time options.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private static function get_arrival_time_options(): array {
|
||||
$options = array(
|
||||
'' => __( 'Select arrival time (optional)', 'wp-bnb' ),
|
||||
'0-2' => __( 'Early morning (00:00 - 02:00)', 'wp-bnb' ),
|
||||
'2-6' => __( 'Night (02:00 - 06:00)', 'wp-bnb' ),
|
||||
'6-10' => __( 'Morning (06:00 - 10:00)', 'wp-bnb' ),
|
||||
'10-14' => __( 'Late morning (10:00 - 14:00)', 'wp-bnb' ),
|
||||
'14-18' => __( 'Afternoon (14:00 - 18:00)', 'wp-bnb' ),
|
||||
'18-22' => __( 'Evening (18:00 - 22:00)', 'wp-bnb' ),
|
||||
'22-24' => __( 'Late evening (22:00 - 00:00)', 'wp-bnb' ),
|
||||
);
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find guest by email address.
|
||||
*
|
||||
* @param string $email Email address.
|
||||
* @return \WP_Post|null Guest post or null.
|
||||
*/
|
||||
private static function find_guest_by_email( string $email ): ?\WP_Post {
|
||||
$guests = get_posts(
|
||||
array(
|
||||
'post_type' => Guest::POST_TYPE,
|
||||
'posts_per_page' => 1,
|
||||
'post_status' => 'publish',
|
||||
'meta_query' => array(
|
||||
array(
|
||||
'key' => '_bnb_guest_email',
|
||||
'value' => $email,
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
return $guests[0] ?? null;
|
||||
}
|
||||
}
|
||||
633
src/Integration/WooCommerce/InvoiceGenerator.php
Normal file
633
src/Integration/WooCommerce/InvoiceGenerator.php
Normal file
@@ -0,0 +1,633 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Invoice Generator.
|
||||
*
|
||||
* Generates PDF invoices for WooCommerce orders.
|
||||
*
|
||||
* @package Magdev\WpBnb\Integration\WooCommerce
|
||||
*/
|
||||
|
||||
declare( strict_types=1 );
|
||||
|
||||
namespace Magdev\WpBnb\Integration\WooCommerce;
|
||||
|
||||
use Magdev\WpBnb\PostTypes\Room;
|
||||
use Magdev\WpBnb\Pricing\Calculator;
|
||||
use Mpdf\Mpdf;
|
||||
|
||||
/**
|
||||
* Invoice Generator class.
|
||||
*
|
||||
* Creates PDF invoices for bookings using mPDF.
|
||||
*/
|
||||
final class InvoiceGenerator {
|
||||
|
||||
/**
|
||||
* Invoice storage directory relative to uploads.
|
||||
*/
|
||||
private const INVOICE_DIR = 'wp-bnb-invoices';
|
||||
|
||||
/**
|
||||
* Initialize invoice generator.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init(): void {
|
||||
// Attach invoice to order emails.
|
||||
add_filter( 'woocommerce_email_attachments', array( self::class, 'attach_invoice_to_email' ), 10, 4 );
|
||||
|
||||
// Add admin order action button.
|
||||
add_action( 'woocommerce_admin_order_actions_end', array( self::class, 'add_order_action_button' ) );
|
||||
|
||||
// AJAX handler for invoice generation.
|
||||
add_action( 'wp_ajax_wp_bnb_generate_invoice', array( self::class, 'ajax_generate_invoice' ) );
|
||||
|
||||
// Handle invoice download.
|
||||
add_action( 'init', array( self::class, 'handle_invoice_download' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate invoice for an order.
|
||||
*
|
||||
* @param \WC_Order $order WooCommerce order.
|
||||
* @return string|null Invoice file path or null on failure.
|
||||
*/
|
||||
public static function generate_invoice( \WC_Order $order ): ?string {
|
||||
/**
|
||||
* Fires before generating an invoice.
|
||||
*
|
||||
* @param \WC_Order $order WooCommerce order.
|
||||
*/
|
||||
do_action( 'wp_bnb_wc_before_invoice_generate', $order );
|
||||
|
||||
// Get or generate invoice number.
|
||||
$invoice_number = self::get_invoice_number( $order );
|
||||
|
||||
// Get booking data.
|
||||
$booking_id = Manager::get_booking_for_order( $order );
|
||||
|
||||
// Generate HTML content.
|
||||
$html = self::get_invoice_html( $order, $invoice_number, $booking_id );
|
||||
|
||||
// Generate PDF.
|
||||
try {
|
||||
$temp_dir = get_temp_dir() . 'mpdf';
|
||||
if ( ! file_exists( $temp_dir ) ) {
|
||||
wp_mkdir_p( $temp_dir );
|
||||
}
|
||||
|
||||
$mpdf = new Mpdf(
|
||||
array(
|
||||
'mode' => 'utf-8',
|
||||
'format' => 'A4',
|
||||
'margin_left' => 15,
|
||||
'margin_right' => 15,
|
||||
'margin_top' => 15,
|
||||
'margin_bottom' => 20,
|
||||
'tempDir' => $temp_dir,
|
||||
)
|
||||
);
|
||||
|
||||
$mpdf->SetTitle( sprintf( 'Invoice %s', $invoice_number ) );
|
||||
$mpdf->SetAuthor( get_option( 'wp_bnb_business_name', get_bloginfo( 'name' ) ) );
|
||||
$mpdf->SetCreator( 'WP BnB' );
|
||||
|
||||
$mpdf->WriteHTML( $html );
|
||||
|
||||
// Save to file.
|
||||
$file_path = self::get_invoice_path( $order, $invoice_number );
|
||||
$mpdf->Output( $file_path, 'F' );
|
||||
|
||||
// Store invoice number and path in order meta.
|
||||
$order->update_meta_data( '_bnb_invoice_number', $invoice_number );
|
||||
$order->update_meta_data( '_bnb_invoice_path', $file_path );
|
||||
$order->update_meta_data( '_bnb_invoice_date', current_time( 'mysql' ) );
|
||||
$order->save();
|
||||
|
||||
/**
|
||||
* Fires after generating an invoice.
|
||||
*
|
||||
* @param \WC_Order $order WooCommerce order.
|
||||
* @param string $file_path Invoice file path.
|
||||
*/
|
||||
do_action( 'wp_bnb_wc_after_invoice_generate', $order, $file_path );
|
||||
|
||||
return $file_path;
|
||||
|
||||
} catch ( \Exception $e ) {
|
||||
error_log( 'WP BnB Invoice generation failed: ' . $e->getMessage() );
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get invoice number for an order.
|
||||
*
|
||||
* @param \WC_Order $order WooCommerce order.
|
||||
* @return string Invoice number.
|
||||
*/
|
||||
public static function get_invoice_number( \WC_Order $order ): string {
|
||||
// Check if already has invoice number.
|
||||
$existing = $order->get_meta( '_bnb_invoice_number', true );
|
||||
|
||||
if ( $existing ) {
|
||||
return $existing;
|
||||
}
|
||||
|
||||
// Generate new invoice number.
|
||||
return Manager::get_next_invoice_number();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get invoice file path.
|
||||
*
|
||||
* @param \WC_Order $order WooCommerce order.
|
||||
* @param string $invoice_number Invoice number.
|
||||
* @return string File path.
|
||||
*/
|
||||
private static function get_invoice_path( \WC_Order $order, string $invoice_number ): string {
|
||||
$upload_dir = wp_upload_dir();
|
||||
$invoice_dir = $upload_dir['basedir'] . '/' . self::INVOICE_DIR;
|
||||
|
||||
// Create directory if needed.
|
||||
if ( ! file_exists( $invoice_dir ) ) {
|
||||
wp_mkdir_p( $invoice_dir );
|
||||
|
||||
// Add .htaccess to protect invoices.
|
||||
$htaccess = $invoice_dir . '/.htaccess';
|
||||
if ( ! file_exists( $htaccess ) ) {
|
||||
file_put_contents( $htaccess, 'Deny from all' );
|
||||
}
|
||||
|
||||
// Add index.php for extra protection.
|
||||
$index = $invoice_dir . '/index.php';
|
||||
if ( ! file_exists( $index ) ) {
|
||||
file_put_contents( $index, '<?php // Silence is golden.' );
|
||||
}
|
||||
}
|
||||
|
||||
// Sanitize invoice number for filename.
|
||||
$safe_number = sanitize_file_name( $invoice_number );
|
||||
|
||||
return $invoice_dir . '/invoice-' . $safe_number . '-' . $order->get_id() . '.pdf';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if invoice exists for an order.
|
||||
*
|
||||
* @param \WC_Order $order WooCommerce order.
|
||||
* @return bool
|
||||
*/
|
||||
public static function invoice_exists( \WC_Order $order ): bool {
|
||||
$path = $order->get_meta( '_bnb_invoice_path', true );
|
||||
|
||||
return $path && file_exists( $path );
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach invoice to email.
|
||||
*
|
||||
* @param array $attachments Attachments array.
|
||||
* @param string $email_id Email ID.
|
||||
* @param \WC_Order $order WooCommerce order.
|
||||
* @param \WC_Email $email Email object.
|
||||
* @return array Modified attachments.
|
||||
*/
|
||||
public static function attach_invoice_to_email( array $attachments, string $email_id, $order, $email ): array {
|
||||
// Only attach to specific emails.
|
||||
$allowed_emails = array(
|
||||
'customer_completed_order',
|
||||
'customer_processing_order',
|
||||
'customer_invoice',
|
||||
);
|
||||
|
||||
if ( ! in_array( $email_id, $allowed_emails, true ) ) {
|
||||
return $attachments;
|
||||
}
|
||||
|
||||
// Check if order is a WC_Order.
|
||||
if ( ! $order instanceof \WC_Order ) {
|
||||
return $attachments;
|
||||
}
|
||||
|
||||
// Check if invoice attachment is enabled.
|
||||
if ( ! Manager::is_invoice_attach_enabled() ) {
|
||||
return $attachments;
|
||||
}
|
||||
|
||||
// Check if this order has a booking.
|
||||
$booking_id = Manager::get_booking_for_order( $order );
|
||||
|
||||
if ( ! $booking_id ) {
|
||||
return $attachments;
|
||||
}
|
||||
|
||||
// Generate invoice if it doesn't exist.
|
||||
if ( ! self::invoice_exists( $order ) ) {
|
||||
self::generate_invoice( $order );
|
||||
}
|
||||
|
||||
// Get invoice path.
|
||||
$invoice_path = $order->get_meta( '_bnb_invoice_path', true );
|
||||
|
||||
if ( $invoice_path && file_exists( $invoice_path ) ) {
|
||||
$attachments[] = $invoice_path;
|
||||
}
|
||||
|
||||
return $attachments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add order action button for invoice.
|
||||
*
|
||||
* @param \WC_Order $order WooCommerce order.
|
||||
* @return void
|
||||
*/
|
||||
public static function add_order_action_button( \WC_Order $order ): void {
|
||||
// Check if this order has a booking.
|
||||
$booking_id = Manager::get_booking_for_order( $order );
|
||||
|
||||
if ( ! $booking_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate download URL.
|
||||
$download_url = add_query_arg(
|
||||
array(
|
||||
'bnb_download_invoice' => $order->get_id(),
|
||||
'_wpnonce' => wp_create_nonce( 'bnb_download_invoice_' . $order->get_id() ),
|
||||
),
|
||||
admin_url( 'admin.php' )
|
||||
);
|
||||
|
||||
?>
|
||||
<a class="button tips" href="<?php echo esc_url( $download_url ); ?>"
|
||||
data-tip="<?php esc_attr_e( 'Download Invoice', 'wp-bnb' ); ?>">
|
||||
<span class="dashicons dashicons-pdf" style="vertical-align: middle;"></span>
|
||||
</a>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler for generating invoice.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function ajax_generate_invoice(): void {
|
||||
check_ajax_referer( 'wp_bnb_admin_nonce', 'nonce' );
|
||||
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Permission denied.', 'wp-bnb' ) ) );
|
||||
}
|
||||
|
||||
$order_id = isset( $_POST['order_id'] ) ? absint( $_POST['order_id'] ) : 0;
|
||||
|
||||
if ( ! $order_id ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Invalid order ID.', 'wp-bnb' ) ) );
|
||||
}
|
||||
|
||||
$order = wc_get_order( $order_id );
|
||||
|
||||
if ( ! $order ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Order not found.', 'wp-bnb' ) ) );
|
||||
}
|
||||
|
||||
$file_path = self::generate_invoice( $order );
|
||||
|
||||
if ( $file_path ) {
|
||||
wp_send_json_success( array( 'message' => __( 'Invoice generated successfully.', 'wp-bnb' ) ) );
|
||||
} else {
|
||||
wp_send_json_error( array( 'message' => __( 'Failed to generate invoice.', 'wp-bnb' ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle invoice download request.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function handle_invoice_download(): void {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ( ! isset( $_GET['bnb_download_invoice'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$order_id = absint( $_GET['bnb_download_invoice'] );
|
||||
|
||||
// Verify nonce.
|
||||
if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'bnb_download_invoice_' . $order_id ) ) {
|
||||
wp_die( esc_html__( 'Security check failed.', 'wp-bnb' ) );
|
||||
}
|
||||
|
||||
// Check capabilities.
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
wp_die( esc_html__( 'You do not have permission to download invoices.', 'wp-bnb' ) );
|
||||
}
|
||||
|
||||
$order = wc_get_order( $order_id );
|
||||
|
||||
if ( ! $order ) {
|
||||
wp_die( esc_html__( 'Order not found.', 'wp-bnb' ) );
|
||||
}
|
||||
|
||||
// Generate invoice if needed.
|
||||
if ( ! self::invoice_exists( $order ) ) {
|
||||
self::generate_invoice( $order );
|
||||
}
|
||||
|
||||
$invoice_path = $order->get_meta( '_bnb_invoice_path', true );
|
||||
|
||||
if ( ! $invoice_path || ! file_exists( $invoice_path ) ) {
|
||||
wp_die( esc_html__( 'Invoice not found.', 'wp-bnb' ) );
|
||||
}
|
||||
|
||||
$invoice_number = $order->get_meta( '_bnb_invoice_number', true );
|
||||
$filename = 'invoice-' . sanitize_file_name( $invoice_number ) . '.pdf';
|
||||
|
||||
// Output file.
|
||||
header( 'Content-Type: application/pdf' );
|
||||
header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
|
||||
header( 'Content-Length: ' . filesize( $invoice_path ) );
|
||||
header( 'Cache-Control: no-cache, no-store, must-revalidate' );
|
||||
|
||||
readfile( $invoice_path );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get invoice HTML content.
|
||||
*
|
||||
* @param \WC_Order $order WooCommerce order.
|
||||
* @param string $invoice_number Invoice number.
|
||||
* @param int|null $booking_id Booking ID.
|
||||
* @return string HTML content.
|
||||
*/
|
||||
private static function get_invoice_html( \WC_Order $order, string $invoice_number, ?int $booking_id ): string {
|
||||
// Get business info.
|
||||
$business = self::get_business_info();
|
||||
|
||||
// Get booking details.
|
||||
$check_in = '';
|
||||
$check_out = '';
|
||||
$room_name = '';
|
||||
$building_name = '';
|
||||
$nights = 0;
|
||||
$guests = 0;
|
||||
|
||||
if ( $booking_id ) {
|
||||
$check_in = get_post_meta( $booking_id, '_bnb_booking_check_in', true );
|
||||
$check_out = get_post_meta( $booking_id, '_bnb_booking_check_out', true );
|
||||
$guests = get_post_meta( $booking_id, '_bnb_booking_adults', true );
|
||||
$room_id = get_post_meta( $booking_id, '_bnb_booking_room_id', true );
|
||||
|
||||
if ( $room_id ) {
|
||||
$room = get_post( $room_id );
|
||||
$room_name = $room ? $room->post_title : '';
|
||||
$building = Room::get_building( $room_id );
|
||||
$building_name = $building ? $building->post_title : '';
|
||||
}
|
||||
|
||||
if ( $check_in && $check_out ) {
|
||||
$check_in_date = \DateTime::createFromFormat( 'Y-m-d', $check_in );
|
||||
$check_out_date = \DateTime::createFromFormat( 'Y-m-d', $check_out );
|
||||
if ( $check_in_date && $check_out_date ) {
|
||||
$nights = $check_in_date->diff( $check_out_date )->days;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Format dates.
|
||||
$date_format = get_option( 'date_format' );
|
||||
$check_in_display = $check_in ? date_i18n( $date_format, strtotime( $check_in ) ) : '';
|
||||
$check_out_display = $check_out ? date_i18n( $date_format, strtotime( $check_out ) ) : '';
|
||||
$order_date = $order->get_date_created() ? $order->get_date_created()->date_i18n( $date_format ) : '';
|
||||
|
||||
// Get logo.
|
||||
$logo_html = '';
|
||||
$logo_id = Manager::get_invoice_logo();
|
||||
if ( $logo_id ) {
|
||||
$logo_url = wp_get_attachment_url( $logo_id );
|
||||
if ( $logo_url ) {
|
||||
$logo_html = '<img src="' . esc_url( $logo_url ) . '" style="max-height: 60px; max-width: 200px;">';
|
||||
}
|
||||
}
|
||||
|
||||
// Get footer.
|
||||
$footer_text = Manager::get_invoice_footer();
|
||||
|
||||
// Build HTML.
|
||||
$html = '<html><head><style>' . self::get_invoice_css() . '</style></head><body>';
|
||||
|
||||
// Header.
|
||||
$html .= '<div class="invoice-header">';
|
||||
$html .= '<div class="logo">' . $logo_html . '</div>';
|
||||
$html .= '<div class="invoice-title">';
|
||||
$html .= '<h1>' . esc_html__( 'INVOICE', 'wp-bnb' ) . '</h1>';
|
||||
$html .= '<p class="invoice-number">' . esc_html( $invoice_number ) . '</p>';
|
||||
$html .= '<p class="invoice-date">' . esc_html( $order_date ) . '</p>';
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Addresses.
|
||||
$html .= '<div class="addresses">';
|
||||
|
||||
// From.
|
||||
$html .= '<div class="address from">';
|
||||
$html .= '<h3>' . esc_html__( 'From', 'wp-bnb' ) . '</h3>';
|
||||
$html .= '<p><strong>' . esc_html( $business['name'] ) . '</strong></p>';
|
||||
if ( $business['street'] ) {
|
||||
$html .= '<p>' . esc_html( $business['street'] ) . '</p>';
|
||||
}
|
||||
if ( $business['city'] || $business['postal'] ) {
|
||||
$html .= '<p>' . esc_html( $business['postal'] . ' ' . $business['city'] ) . '</p>';
|
||||
}
|
||||
if ( $business['country'] ) {
|
||||
$html .= '<p>' . esc_html( $business['country'] ) . '</p>';
|
||||
}
|
||||
if ( $business['email'] ) {
|
||||
$html .= '<p>' . esc_html( $business['email'] ) . '</p>';
|
||||
}
|
||||
if ( $business['phone'] ) {
|
||||
$html .= '<p>' . esc_html( $business['phone'] ) . '</p>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
|
||||
// To.
|
||||
$html .= '<div class="address to">';
|
||||
$html .= '<h3>' . esc_html__( 'Bill To', 'wp-bnb' ) . '</h3>';
|
||||
$html .= '<p><strong>' . esc_html( $order->get_billing_first_name() . ' ' . $order->get_billing_last_name() ) . '</strong></p>';
|
||||
if ( $order->get_billing_address_1() ) {
|
||||
$html .= '<p>' . esc_html( $order->get_billing_address_1() ) . '</p>';
|
||||
}
|
||||
if ( $order->get_billing_city() || $order->get_billing_postcode() ) {
|
||||
$html .= '<p>' . esc_html( $order->get_billing_postcode() . ' ' . $order->get_billing_city() ) . '</p>';
|
||||
}
|
||||
if ( $order->get_billing_country() ) {
|
||||
$html .= '<p>' . esc_html( WC()->countries->countries[ $order->get_billing_country() ] ?? $order->get_billing_country() ) . '</p>';
|
||||
}
|
||||
if ( $order->get_billing_email() ) {
|
||||
$html .= '<p>' . esc_html( $order->get_billing_email() ) . '</p>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Booking details.
|
||||
if ( $booking_id && $room_name ) {
|
||||
$html .= '<div class="booking-details">';
|
||||
$html .= '<h3>' . esc_html__( 'Booking Details', 'wp-bnb' ) . '</h3>';
|
||||
$html .= '<table class="details-table">';
|
||||
$html .= '<tr><td><strong>' . esc_html__( 'Room', 'wp-bnb' ) . '</strong></td><td>' . esc_html( $room_name );
|
||||
if ( $building_name ) {
|
||||
$html .= ' <small>(' . esc_html( $building_name ) . ')</small>';
|
||||
}
|
||||
$html .= '</td></tr>';
|
||||
$html .= '<tr><td><strong>' . esc_html__( 'Check-in', 'wp-bnb' ) . '</strong></td><td>' . esc_html( $check_in_display ) . '</td></tr>';
|
||||
$html .= '<tr><td><strong>' . esc_html__( 'Check-out', 'wp-bnb' ) . '</strong></td><td>' . esc_html( $check_out_display ) . '</td></tr>';
|
||||
$html .= '<tr><td><strong>' . esc_html__( 'Nights', 'wp-bnb' ) . '</strong></td><td>' . esc_html( $nights ) . '</td></tr>';
|
||||
$html .= '<tr><td><strong>' . esc_html__( 'Guests', 'wp-bnb' ) . '</strong></td><td>' . esc_html( $guests ) . '</td></tr>';
|
||||
$html .= '</table>';
|
||||
$html .= '</div>';
|
||||
}
|
||||
|
||||
// Line items.
|
||||
$html .= '<div class="line-items">';
|
||||
$html .= '<table class="items-table">';
|
||||
$html .= '<thead><tr>';
|
||||
$html .= '<th class="description">' . esc_html__( 'Description', 'wp-bnb' ) . '</th>';
|
||||
$html .= '<th class="qty">' . esc_html__( 'Qty', 'wp-bnb' ) . '</th>';
|
||||
$html .= '<th class="price">' . esc_html__( 'Price', 'wp-bnb' ) . '</th>';
|
||||
$html .= '<th class="total">' . esc_html__( 'Total', 'wp-bnb' ) . '</th>';
|
||||
$html .= '</tr></thead>';
|
||||
$html .= '<tbody>';
|
||||
|
||||
foreach ( $order->get_items() as $item ) {
|
||||
$qty = $item->get_quantity();
|
||||
$total = $item->get_total();
|
||||
$price = $qty > 0 ? $total / $qty : $total;
|
||||
|
||||
$html .= '<tr>';
|
||||
$html .= '<td class="description">' . esc_html( $item->get_name() ) . '</td>';
|
||||
$html .= '<td class="qty">' . esc_html( $qty ) . '</td>';
|
||||
$html .= '<td class="price">' . wc_price( $price ) . '</td>';
|
||||
$html .= '<td class="total">' . wc_price( $total ) . '</td>';
|
||||
$html .= '</tr>';
|
||||
}
|
||||
|
||||
$html .= '</tbody>';
|
||||
$html .= '</table>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Totals.
|
||||
$html .= '<div class="totals">';
|
||||
$html .= '<table class="totals-table">';
|
||||
$html .= '<tr><td>' . esc_html__( 'Subtotal', 'wp-bnb' ) . '</td><td>' . wc_price( $order->get_subtotal() ) . '</td></tr>';
|
||||
|
||||
if ( $order->get_total_tax() > 0 ) {
|
||||
$html .= '<tr><td>' . esc_html__( 'Tax', 'wp-bnb' ) . '</td><td>' . wc_price( $order->get_total_tax() ) . '</td></tr>';
|
||||
}
|
||||
|
||||
$html .= '<tr class="grand-total"><td><strong>' . esc_html__( 'Total', 'wp-bnb' ) . '</strong></td><td><strong>' . wc_price( $order->get_total() ) . '</strong></td></tr>';
|
||||
$html .= '</table>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Payment info.
|
||||
$html .= '<div class="payment-info">';
|
||||
$html .= '<p><strong>' . esc_html__( 'Payment Status:', 'wp-bnb' ) . '</strong> ';
|
||||
|
||||
if ( $order->is_paid() ) {
|
||||
$html .= '<span class="paid">' . esc_html__( 'PAID', 'wp-bnb' ) . '</span>';
|
||||
} else {
|
||||
$html .= '<span class="unpaid">' . esc_html__( 'PENDING', 'wp-bnb' ) . '</span>';
|
||||
}
|
||||
|
||||
$html .= '</p>';
|
||||
$html .= '<p><strong>' . esc_html__( 'Payment Method:', 'wp-bnb' ) . '</strong> ' . esc_html( $order->get_payment_method_title() ) . '</p>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Footer.
|
||||
$html .= '<div class="footer">';
|
||||
$html .= '<p>' . esc_html__( 'Thank you for your stay!', 'wp-bnb' ) . '</p>';
|
||||
if ( $footer_text ) {
|
||||
$html .= '<p class="custom-footer">' . esc_html( $footer_text ) . '</p>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '</body></html>';
|
||||
|
||||
/**
|
||||
* Filter the invoice HTML.
|
||||
*
|
||||
* @param string $html Invoice HTML.
|
||||
* @param \WC_Order $order WooCommerce order.
|
||||
*/
|
||||
return apply_filters( 'wp_bnb_wc_invoice_html', $html, $order );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get invoice CSS styles.
|
||||
*
|
||||
* @return string CSS content.
|
||||
*/
|
||||
private static function get_invoice_css(): string {
|
||||
return '
|
||||
body { font-family: DejaVu Sans, sans-serif; font-size: 10pt; color: #333; line-height: 1.4; }
|
||||
h1 { font-size: 24pt; color: #2271b1; margin: 0; text-align: right; }
|
||||
h3 { font-size: 11pt; color: #50575e; margin: 15pt 0 5pt 0; }
|
||||
|
||||
.invoice-header { margin-bottom: 30pt; overflow: hidden; }
|
||||
.logo { float: left; width: 50%; }
|
||||
.invoice-title { float: right; width: 50%; text-align: right; }
|
||||
.invoice-number { font-size: 14pt; font-weight: bold; color: #333; margin: 5pt 0; }
|
||||
.invoice-date { font-size: 10pt; color: #787c82; margin: 0; }
|
||||
|
||||
.addresses { margin-bottom: 20pt; overflow: hidden; }
|
||||
.address { width: 48%; }
|
||||
.address.from { float: left; }
|
||||
.address.to { float: right; }
|
||||
.address p { margin: 2pt 0; font-size: 9pt; }
|
||||
|
||||
.booking-details { margin-bottom: 20pt; background: #f9f9f9; padding: 10pt; border-radius: 4pt; }
|
||||
.details-table { width: 100%; border-collapse: collapse; }
|
||||
.details-table td { padding: 4pt 8pt; font-size: 9pt; border-bottom: 1px solid #e1e4e8; }
|
||||
.details-table td:first-child { width: 120pt; }
|
||||
|
||||
.line-items { margin-bottom: 20pt; }
|
||||
.items-table { width: 100%; border-collapse: collapse; }
|
||||
.items-table th { background: #f6f7f7; text-align: left; padding: 8pt; font-size: 9pt; border-bottom: 2px solid #c3c4c7; }
|
||||
.items-table td { padding: 8pt; font-size: 9pt; border-bottom: 1px solid #e1e4e8; }
|
||||
.items-table .qty, .items-table .price, .items-table .total { text-align: right; width: 70pt; }
|
||||
|
||||
.totals { margin-bottom: 20pt; }
|
||||
.totals-table { width: 250pt; margin-left: auto; border-collapse: collapse; }
|
||||
.totals-table td { padding: 6pt 8pt; font-size: 10pt; }
|
||||
.totals-table td:last-child { text-align: right; }
|
||||
.totals-table .grand-total td { border-top: 2px solid #333; font-size: 12pt; }
|
||||
|
||||
.payment-info { margin-bottom: 30pt; padding: 10pt; background: #f9f9f9; border-radius: 4pt; }
|
||||
.payment-info p { margin: 4pt 0; font-size: 9pt; }
|
||||
.payment-info .paid { color: #00a32a; font-weight: bold; }
|
||||
.payment-info .unpaid { color: #dba617; font-weight: bold; }
|
||||
|
||||
.footer { text-align: center; margin-top: 40pt; padding-top: 15pt; border-top: 1px solid #c3c4c7; }
|
||||
.footer p { font-size: 9pt; color: #787c82; margin: 3pt 0; }
|
||||
.custom-footer { font-size: 8pt; }
|
||||
';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get business information from settings.
|
||||
*
|
||||
* @return array Business info.
|
||||
*/
|
||||
private static function get_business_info(): array {
|
||||
return array(
|
||||
'name' => get_option( 'wp_bnb_business_name', get_bloginfo( 'name' ) ),
|
||||
'street' => get_option( 'wp_bnb_address_street', '' ),
|
||||
'city' => get_option( 'wp_bnb_address_city', '' ),
|
||||
'postal' => get_option( 'wp_bnb_address_postal', '' ),
|
||||
'country' => get_option( 'wp_bnb_address_country', '' ),
|
||||
'email' => get_option( 'wp_bnb_contact_email', get_option( 'admin_email' ) ),
|
||||
'phone' => get_option( 'wp_bnb_contact_phone', '' ),
|
||||
);
|
||||
}
|
||||
}
|
||||
435
src/Integration/WooCommerce/Manager.php
Normal file
435
src/Integration/WooCommerce/Manager.php
Normal file
@@ -0,0 +1,435 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Integration Manager.
|
||||
*
|
||||
* Main integration class that initializes WooCommerce features.
|
||||
*
|
||||
* @package Magdev\WpBnb\Integration\WooCommerce
|
||||
*/
|
||||
|
||||
declare( strict_types=1 );
|
||||
|
||||
namespace Magdev\WpBnb\Integration\WooCommerce;
|
||||
|
||||
/**
|
||||
* WooCommerce Integration Manager class.
|
||||
*
|
||||
* Manages the integration with WooCommerce for payment processing,
|
||||
* invoice generation, order management, and refund handling.
|
||||
*/
|
||||
final class Manager {
|
||||
|
||||
/**
|
||||
* Option key for enabling WooCommerce integration.
|
||||
*/
|
||||
public const OPTION_ENABLED = 'wp_bnb_wc_enabled';
|
||||
|
||||
/**
|
||||
* Option key for auto-syncing products.
|
||||
*/
|
||||
public const OPTION_AUTO_SYNC = 'wp_bnb_wc_auto_sync_products';
|
||||
|
||||
/**
|
||||
* Option key for auto-confirming bookings on payment.
|
||||
*/
|
||||
public const OPTION_AUTO_CONFIRM = 'wp_bnb_wc_auto_confirm_booking';
|
||||
|
||||
/**
|
||||
* Option key for attaching invoices to emails.
|
||||
*/
|
||||
public const OPTION_INVOICE_ATTACH = 'wp_bnb_wc_invoice_attach';
|
||||
|
||||
/**
|
||||
* Option key for product category.
|
||||
*/
|
||||
public const OPTION_PRODUCT_CATEGORY = 'wp_bnb_wc_product_category';
|
||||
|
||||
/**
|
||||
* Option key for services as products.
|
||||
*/
|
||||
public const OPTION_SERVICES_AS_PRODUCTS = 'wp_bnb_wc_services_as_products';
|
||||
|
||||
/**
|
||||
* Option key for invoice number prefix.
|
||||
*/
|
||||
public const OPTION_INVOICE_PREFIX = 'wp_bnb_invoice_prefix';
|
||||
|
||||
/**
|
||||
* Option key for invoice starting number.
|
||||
*/
|
||||
public const OPTION_INVOICE_START_NUMBER = 'wp_bnb_invoice_start_number';
|
||||
|
||||
/**
|
||||
* Option key for last invoice number.
|
||||
*/
|
||||
public const OPTION_INVOICE_LAST_NUMBER = 'wp_bnb_invoice_last_number';
|
||||
|
||||
/**
|
||||
* Option key for invoice logo.
|
||||
*/
|
||||
public const OPTION_INVOICE_LOGO = 'wp_bnb_invoice_logo';
|
||||
|
||||
/**
|
||||
* Option key for invoice footer text.
|
||||
*/
|
||||
public const OPTION_INVOICE_FOOTER = 'wp_bnb_invoice_footer';
|
||||
|
||||
/**
|
||||
* Order meta key for linked booking ID.
|
||||
*/
|
||||
public const ORDER_BOOKING_META = '_bnb_booking_id';
|
||||
|
||||
/**
|
||||
* Order meta key for linked room ID.
|
||||
*/
|
||||
public const ORDER_ROOM_META = '_bnb_room_id';
|
||||
|
||||
/**
|
||||
* Booking meta key for linked WC order ID.
|
||||
*/
|
||||
public const BOOKING_ORDER_META = '_bnb_booking_wc_order_id';
|
||||
|
||||
/**
|
||||
* Room meta key for linked WC product ID.
|
||||
*/
|
||||
public const ROOM_PRODUCT_META = '_bnb_wc_product_id';
|
||||
|
||||
/**
|
||||
* Product meta key for linked room ID.
|
||||
*/
|
||||
public const PRODUCT_ROOM_META = '_bnb_room_id';
|
||||
|
||||
/**
|
||||
* Cart item data key for booking data.
|
||||
*/
|
||||
public const CART_ITEM_KEY = 'bnb_booking_data';
|
||||
|
||||
/**
|
||||
* Whether components have been initialized.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private static bool $initialized = false;
|
||||
|
||||
/**
|
||||
* Initialize the WooCommerce integration.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init(): void {
|
||||
// Prevent double initialization.
|
||||
if ( self::$initialized ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only proceed if WooCommerce is active.
|
||||
if ( ! self::is_wc_active() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Declare HPOS compatibility.
|
||||
add_action( 'before_woocommerce_init', array( self::class, 'declare_hpos_compatibility' ) );
|
||||
|
||||
// Only initialize components if integration is enabled.
|
||||
if ( ! self::is_enabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize sub-components after WooCommerce is fully loaded.
|
||||
add_action( 'woocommerce_loaded', array( self::class, 'init_components' ) );
|
||||
|
||||
// Add admin notice if WooCommerce deactivated while integration enabled.
|
||||
add_action( 'admin_notices', array( self::class, 'admin_notices' ) );
|
||||
|
||||
self::$initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Declare High-Performance Order Storage (HPOS) compatibility.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function declare_hpos_compatibility(): void {
|
||||
if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) {
|
||||
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility(
|
||||
'custom_order_tables',
|
||||
WP_BNB_PATH . 'wp-bnb.php',
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all WooCommerce integration components.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init_components(): void {
|
||||
// Product synchronization (room <-> WC product).
|
||||
ProductSync::init();
|
||||
|
||||
// Cart handling (booking data, availability, pricing).
|
||||
CartHandler::init();
|
||||
|
||||
// Checkout customization.
|
||||
CheckoutHandler::init();
|
||||
|
||||
// Order handling (booking creation on payment).
|
||||
OrderHandler::init();
|
||||
|
||||
// Invoice generation.
|
||||
InvoiceGenerator::init();
|
||||
|
||||
// Refund handling.
|
||||
RefundHandler::init();
|
||||
|
||||
// Admin column enhancements.
|
||||
if ( is_admin() ) {
|
||||
AdminColumns::init();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if WooCommerce is active.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_wc_active(): bool {
|
||||
return class_exists( 'WooCommerce' ) || class_exists( 'WC_Product' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if WooCommerce integration is enabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_enabled(): bool {
|
||||
return 'yes' === get_option( self::OPTION_ENABLED, 'no' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable WooCommerce integration.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function enable(): void {
|
||||
update_option( self::OPTION_ENABLED, 'yes' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable WooCommerce integration.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function disable(): void {
|
||||
update_option( self::OPTION_ENABLED, 'no' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if auto-sync products is enabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_auto_sync_enabled(): bool {
|
||||
return 'yes' === get_option( self::OPTION_AUTO_SYNC, 'yes' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if auto-confirm booking is enabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_auto_confirm_enabled(): bool {
|
||||
return 'yes' === get_option( self::OPTION_AUTO_CONFIRM, 'yes' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if invoice attachment is enabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_invoice_attach_enabled(): bool {
|
||||
return 'yes' === get_option( self::OPTION_INVOICE_ATTACH, 'yes' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the WooCommerce product category for rooms.
|
||||
*
|
||||
* @return int Product category term ID, or 0 if not set.
|
||||
*/
|
||||
public static function get_product_category(): int {
|
||||
return absint( get_option( self::OPTION_PRODUCT_CATEGORY, 0 ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get invoice number prefix.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_invoice_prefix(): string {
|
||||
return get_option( self::OPTION_INVOICE_PREFIX, 'INV-' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get invoice starting number.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function get_invoice_start_number(): int {
|
||||
return absint( get_option( self::OPTION_INVOICE_START_NUMBER, 1000 ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get and increment the next invoice number.
|
||||
*
|
||||
* @return string The next invoice number with prefix.
|
||||
*/
|
||||
public static function get_next_invoice_number(): string {
|
||||
$last_number = absint( get_option( self::OPTION_INVOICE_LAST_NUMBER, 0 ) );
|
||||
$start_number = self::get_invoice_start_number();
|
||||
|
||||
// Use start number if no invoices generated yet.
|
||||
$next_number = ( 0 === $last_number ) ? $start_number : $last_number + 1;
|
||||
|
||||
// Update the last number.
|
||||
update_option( self::OPTION_INVOICE_LAST_NUMBER, $next_number );
|
||||
|
||||
return self::get_invoice_prefix() . str_pad( (string) $next_number, 5, '0', STR_PAD_LEFT );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get invoice logo attachment ID.
|
||||
*
|
||||
* @return int Attachment ID or 0.
|
||||
*/
|
||||
public static function get_invoice_logo(): int {
|
||||
return absint( get_option( self::OPTION_INVOICE_LOGO, 0 ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get invoice footer text.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_invoice_footer(): string {
|
||||
return get_option( self::OPTION_INVOICE_FOOTER, '' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Display admin notices.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function admin_notices(): void {
|
||||
// Show notice if integration enabled but WC not active.
|
||||
if ( self::is_enabled() && ! self::is_wc_active() ) {
|
||||
?>
|
||||
<div class="notice notice-warning is-dismissible">
|
||||
<p>
|
||||
<strong><?php esc_html_e( 'WP BnB:', 'wp-bnb' ); ?></strong>
|
||||
<?php esc_html_e( 'WooCommerce integration is enabled but WooCommerce is not active. Please install and activate WooCommerce or disable the integration in WP BnB settings.', 'wp-bnb' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map WooCommerce order status to booking status.
|
||||
*
|
||||
* @param string $wc_status WooCommerce order status (without 'wc-' prefix).
|
||||
* @return string Booking status.
|
||||
*/
|
||||
public static function map_wc_status_to_booking( string $wc_status ): string {
|
||||
$map = array(
|
||||
'pending' => 'pending',
|
||||
'on-hold' => 'pending',
|
||||
'processing' => self::is_auto_confirm_enabled() ? 'confirmed' : 'pending',
|
||||
'completed' => 'confirmed',
|
||||
'cancelled' => 'cancelled',
|
||||
'refunded' => 'cancelled',
|
||||
'failed' => 'cancelled',
|
||||
);
|
||||
|
||||
/**
|
||||
* Filter the WooCommerce to booking status mapping.
|
||||
*
|
||||
* @param array $map Status mapping array.
|
||||
*/
|
||||
$map = apply_filters( 'wp_bnb_wc_status_map', $map );
|
||||
|
||||
return $map[ $wc_status ] ?? 'pending';
|
||||
}
|
||||
|
||||
/**
|
||||
* Map booking status to WooCommerce order status.
|
||||
*
|
||||
* @param string $booking_status Booking status.
|
||||
* @return string WooCommerce order status (without 'wc-' prefix).
|
||||
*/
|
||||
public static function map_booking_status_to_wc( string $booking_status ): string {
|
||||
$map = array(
|
||||
'pending' => 'on-hold',
|
||||
'confirmed' => 'processing',
|
||||
'checked_in' => 'processing',
|
||||
'checked_out' => 'completed',
|
||||
'cancelled' => 'cancelled',
|
||||
);
|
||||
|
||||
return $map[ $booking_status ] ?? 'on-hold';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WooCommerce order for a booking.
|
||||
*
|
||||
* @param int $booking_id Booking post ID.
|
||||
* @return \WC_Order|null WooCommerce order or null.
|
||||
*/
|
||||
public static function get_order_for_booking( int $booking_id ): ?\WC_Order {
|
||||
$order_id = get_post_meta( $booking_id, self::BOOKING_ORDER_META, true );
|
||||
|
||||
if ( ! $order_id ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$order = wc_get_order( $order_id );
|
||||
|
||||
return $order instanceof \WC_Order ? $order : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get booking ID for a WooCommerce order.
|
||||
*
|
||||
* @param \WC_Order|int $order WooCommerce order or order ID.
|
||||
* @return int|null Booking post ID or null.
|
||||
*/
|
||||
public static function get_booking_for_order( $order ): ?int {
|
||||
if ( is_int( $order ) ) {
|
||||
$order = wc_get_order( $order );
|
||||
}
|
||||
|
||||
if ( ! $order instanceof \WC_Order ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$booking_id = $order->get_meta( self::ORDER_BOOKING_META, true );
|
||||
|
||||
return $booking_id ? absint( $booking_id ) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Link a booking to a WooCommerce order (bidirectional).
|
||||
*
|
||||
* @param int $booking_id Booking post ID.
|
||||
* @param \WC_Order $order WooCommerce order.
|
||||
* @return void
|
||||
*/
|
||||
public static function link_booking_to_order( int $booking_id, \WC_Order $order ): void {
|
||||
// Store order ID in booking meta.
|
||||
update_post_meta( $booking_id, self::BOOKING_ORDER_META, $order->get_id() );
|
||||
|
||||
// Store booking ID in order meta (HPOS compatible).
|
||||
$order->update_meta_data( self::ORDER_BOOKING_META, $booking_id );
|
||||
$order->save();
|
||||
}
|
||||
}
|
||||
584
src/Integration/WooCommerce/OrderHandler.php
Normal file
584
src/Integration/WooCommerce/OrderHandler.php
Normal file
@@ -0,0 +1,584 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Order Handler.
|
||||
*
|
||||
* Handles order-to-booking creation and status synchronization.
|
||||
*
|
||||
* @package Magdev\WpBnb\Integration\WooCommerce
|
||||
*/
|
||||
|
||||
declare( strict_types=1 );
|
||||
|
||||
namespace Magdev\WpBnb\Integration\WooCommerce;
|
||||
|
||||
use Magdev\WpBnb\Booking\EmailNotifier;
|
||||
use Magdev\WpBnb\PostTypes\Booking;
|
||||
use Magdev\WpBnb\PostTypes\Guest;
|
||||
use Magdev\WpBnb\PostTypes\Room;
|
||||
use Magdev\WpBnb\Pricing\Calculator;
|
||||
|
||||
/**
|
||||
* Order Handler class.
|
||||
*
|
||||
* Creates bookings from WooCommerce orders on payment completion
|
||||
* and synchronizes order/booking statuses.
|
||||
*/
|
||||
final class OrderHandler {
|
||||
|
||||
/**
|
||||
* Flag to prevent recursive status updates.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private static bool $updating_status = false;
|
||||
|
||||
/**
|
||||
* Initialize order handler.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init(): void {
|
||||
// Create booking on payment completion.
|
||||
add_action( 'woocommerce_payment_complete', array( self::class, 'on_payment_complete' ) );
|
||||
|
||||
// Also handle manual order status changes to processing/completed.
|
||||
add_action( 'woocommerce_order_status_processing', array( self::class, 'on_order_processing' ) );
|
||||
add_action( 'woocommerce_order_status_completed', array( self::class, 'on_order_completed' ) );
|
||||
|
||||
// Sync order status changes to booking.
|
||||
add_action( 'woocommerce_order_status_changed', array( self::class, 'sync_order_status_to_booking' ), 10, 4 );
|
||||
|
||||
// Display booking info in admin order page.
|
||||
add_action( 'woocommerce_admin_order_data_after_billing_address', array( self::class, 'display_booking_info_admin' ) );
|
||||
|
||||
// Display booking info on thank you page.
|
||||
add_action( 'woocommerce_thankyou', array( self::class, 'display_booking_info_thankyou' ) );
|
||||
|
||||
// Display booking info in order details.
|
||||
add_action( 'woocommerce_order_details_after_order_table', array( self::class, 'display_booking_info_order_details' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle payment completion.
|
||||
*
|
||||
* @param int $order_id Order ID.
|
||||
* @return void
|
||||
*/
|
||||
public static function on_payment_complete( int $order_id ): void {
|
||||
self::maybe_create_booking( $order_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle order status change to processing.
|
||||
*
|
||||
* @param int $order_id Order ID.
|
||||
* @return void
|
||||
*/
|
||||
public static function on_order_processing( int $order_id ): void {
|
||||
self::maybe_create_booking( $order_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle order status change to completed.
|
||||
*
|
||||
* @param int $order_id Order ID.
|
||||
* @return void
|
||||
*/
|
||||
public static function on_order_completed( int $order_id ): void {
|
||||
self::maybe_create_booking( $order_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create booking from order if not already created.
|
||||
*
|
||||
* @param int $order_id Order ID.
|
||||
* @return void
|
||||
*/
|
||||
private static function maybe_create_booking( int $order_id ): void {
|
||||
$order = wc_get_order( $order_id );
|
||||
|
||||
if ( ! $order instanceof \WC_Order ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if booking already created.
|
||||
$existing_booking = Manager::get_booking_for_order( $order );
|
||||
|
||||
if ( $existing_booking ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if order has room bookings.
|
||||
$has_bookings = false;
|
||||
foreach ( $order->get_items() as $item ) {
|
||||
$room_id = $item->get_meta( '_bnb_room_id', true );
|
||||
if ( $room_id ) {
|
||||
$has_bookings = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $has_bookings ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create booking.
|
||||
self::create_booking_from_order( $order );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create booking(s) from WooCommerce order.
|
||||
*
|
||||
* @param \WC_Order $order WooCommerce order.
|
||||
* @return int|null Booking ID or null on failure.
|
||||
*/
|
||||
public static function create_booking_from_order( \WC_Order $order ): ?int {
|
||||
/**
|
||||
* Fires before creating a booking from an order.
|
||||
*
|
||||
* @param \WC_Order $order WooCommerce order.
|
||||
*/
|
||||
do_action( 'wp_bnb_wc_before_booking_from_order', $order );
|
||||
|
||||
// Find or create guest.
|
||||
$guest_id = self::find_or_create_guest( $order );
|
||||
|
||||
// Get booking data from order items.
|
||||
$booking_id = null;
|
||||
|
||||
foreach ( $order->get_items() as $item ) {
|
||||
$room_id = $item->get_meta( '_bnb_room_id', true );
|
||||
|
||||
if ( ! $room_id ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$check_in = $item->get_meta( '_bnb_check_in', true );
|
||||
$check_out = $item->get_meta( '_bnb_check_out', true );
|
||||
$guests = $item->get_meta( '_bnb_guests', true );
|
||||
$nights = $item->get_meta( '_bnb_nights', true );
|
||||
$services = $item->get_meta( '_bnb_services', true );
|
||||
$breakdown = $item->get_meta( '_bnb_price_breakdown', true );
|
||||
|
||||
// Decode JSON if necessary.
|
||||
if ( is_string( $services ) ) {
|
||||
$services = json_decode( $services, true ) ?: array();
|
||||
}
|
||||
if ( is_string( $breakdown ) ) {
|
||||
$breakdown = json_decode( $breakdown, true ) ?: array();
|
||||
}
|
||||
|
||||
// Determine initial status.
|
||||
$status = Manager::is_auto_confirm_enabled() ? 'confirmed' : 'pending';
|
||||
|
||||
// Get guest notes.
|
||||
$guest_notes = $order->get_meta( '_bnb_guest_notes', true );
|
||||
|
||||
// Create booking post.
|
||||
$booking_data = array(
|
||||
'post_type' => Booking::POST_TYPE,
|
||||
'post_status' => 'publish',
|
||||
'post_title' => self::generate_booking_title( $guest_id, $check_in, $check_out ),
|
||||
);
|
||||
|
||||
/**
|
||||
* Filter the booking data before creation.
|
||||
*
|
||||
* @param array $booking_data Booking post data.
|
||||
* @param \WC_Order $order WooCommerce order.
|
||||
*/
|
||||
$booking_data = apply_filters( 'wp_bnb_wc_booking_from_order_data', $booking_data, $order );
|
||||
|
||||
$booking_id = wp_insert_post( $booking_data );
|
||||
|
||||
if ( ! $booking_id || is_wp_error( $booking_id ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Store booking meta.
|
||||
update_post_meta( $booking_id, '_bnb_booking_room_id', $room_id );
|
||||
update_post_meta( $booking_id, '_bnb_booking_check_in', $check_in );
|
||||
update_post_meta( $booking_id, '_bnb_booking_check_out', $check_out );
|
||||
update_post_meta( $booking_id, '_bnb_booking_status', $status );
|
||||
update_post_meta( $booking_id, '_bnb_booking_adults', max( 1, (int) $guests ) );
|
||||
update_post_meta( $booking_id, '_bnb_booking_children', 0 );
|
||||
update_post_meta( $booking_id, '_bnb_booking_guest_notes', $guest_notes );
|
||||
update_post_meta( $booking_id, '_bnb_booking_source', 'woocommerce_order_' . $order->get_id() );
|
||||
|
||||
// Store guest info.
|
||||
if ( $guest_id ) {
|
||||
update_post_meta( $booking_id, '_bnb_booking_guest_id', $guest_id );
|
||||
update_post_meta( $booking_id, '_bnb_booking_guest_name', Guest::get_full_name( $guest_id ) );
|
||||
update_post_meta( $booking_id, '_bnb_booking_guest_email', get_post_meta( $guest_id, '_bnb_guest_email', true ) );
|
||||
update_post_meta( $booking_id, '_bnb_booking_guest_phone', get_post_meta( $guest_id, '_bnb_guest_phone', true ) );
|
||||
} else {
|
||||
// Use order billing info.
|
||||
$guest_name = $order->get_billing_first_name() . ' ' . $order->get_billing_last_name();
|
||||
update_post_meta( $booking_id, '_bnb_booking_guest_name', $guest_name );
|
||||
update_post_meta( $booking_id, '_bnb_booking_guest_email', $order->get_billing_email() );
|
||||
update_post_meta( $booking_id, '_bnb_booking_guest_phone', $order->get_billing_phone() );
|
||||
}
|
||||
|
||||
// Store pricing.
|
||||
$total = $item->get_total();
|
||||
update_post_meta( $booking_id, '_bnb_booking_calculated_price', $total );
|
||||
|
||||
if ( ! empty( $breakdown ) ) {
|
||||
update_post_meta( $booking_id, '_bnb_booking_price_breakdown', $breakdown );
|
||||
}
|
||||
|
||||
// Store services.
|
||||
if ( ! empty( $services ) ) {
|
||||
update_post_meta( $booking_id, Booking::SERVICES_META_KEY, $services );
|
||||
}
|
||||
|
||||
// Generate booking reference.
|
||||
$reference = self::generate_booking_reference( $booking_id );
|
||||
update_post_meta( $booking_id, '_bnb_booking_reference', $reference );
|
||||
|
||||
// Store confirmed timestamp if auto-confirmed.
|
||||
if ( 'confirmed' === $status ) {
|
||||
update_post_meta( $booking_id, '_bnb_booking_confirmed_at', current_time( 'mysql' ) );
|
||||
}
|
||||
|
||||
// Link booking to order.
|
||||
Manager::link_booking_to_order( $booking_id, $order );
|
||||
|
||||
// Also store room ID in order meta for quick access.
|
||||
$order->update_meta_data( Manager::ORDER_ROOM_META, $room_id );
|
||||
|
||||
// Store check-in/out in order meta.
|
||||
$order->update_meta_data( '_bnb_check_in', $check_in );
|
||||
$order->update_meta_data( '_bnb_check_out', $check_out );
|
||||
$order->save();
|
||||
|
||||
// Trigger status change action for email notifications.
|
||||
if ( 'confirmed' === $status ) {
|
||||
/**
|
||||
* Fires when booking status changes (for email notifications).
|
||||
*
|
||||
* @param int $booking_id Booking post ID.
|
||||
* @param string $old_status Old status.
|
||||
* @param string $new_status New status.
|
||||
*/
|
||||
do_action( 'wp_bnb_booking_status_changed', $booking_id, 'pending', 'confirmed' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires after creating a booking from an order.
|
||||
*
|
||||
* @param int|null $booking_id Last booking ID created.
|
||||
* @param \WC_Order $order WooCommerce order.
|
||||
*/
|
||||
do_action( 'wp_bnb_wc_after_booking_from_order', $booking_id, $order );
|
||||
|
||||
return $booking_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync order status changes to booking status.
|
||||
*
|
||||
* @param int $order_id Order ID.
|
||||
* @param string $old_status Old status.
|
||||
* @param string $new_status New status.
|
||||
* @param \WC_Order $order Order object.
|
||||
* @return void
|
||||
*/
|
||||
public static function sync_order_status_to_booking( int $order_id, string $old_status, string $new_status, \WC_Order $order ): void {
|
||||
// Prevent recursive updates.
|
||||
if ( self::$updating_status ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$booking_id = Manager::get_booking_for_order( $order );
|
||||
|
||||
if ( ! $booking_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Map WC status to booking status.
|
||||
$booking_status = Manager::map_wc_status_to_booking( $new_status );
|
||||
|
||||
// Get current booking status.
|
||||
$current_booking_status = get_post_meta( $booking_id, '_bnb_booking_status', true );
|
||||
|
||||
// Don't update if status is the same.
|
||||
if ( $current_booking_status === $booking_status ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't downgrade from checked_in/checked_out.
|
||||
if ( in_array( $current_booking_status, array( 'checked_in', 'checked_out' ), true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::$updating_status = true;
|
||||
|
||||
// Update booking status.
|
||||
update_post_meta( $booking_id, '_bnb_booking_status', $booking_status );
|
||||
|
||||
// Update confirmed timestamp if confirming.
|
||||
if ( 'confirmed' === $booking_status && 'pending' === $current_booking_status ) {
|
||||
update_post_meta( $booking_id, '_bnb_booking_confirmed_at', current_time( 'mysql' ) );
|
||||
}
|
||||
|
||||
// Trigger booking status changed action.
|
||||
do_action( 'wp_bnb_booking_status_changed', $booking_id, $current_booking_status, $booking_status );
|
||||
|
||||
self::$updating_status = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find or create guest from order data.
|
||||
*
|
||||
* @param \WC_Order $order WooCommerce order.
|
||||
* @return int|null Guest ID or null.
|
||||
*/
|
||||
private static function find_or_create_guest( \WC_Order $order ): ?int {
|
||||
$email = $order->get_billing_email();
|
||||
|
||||
if ( ! $email ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Try to find existing guest by email.
|
||||
$existing_guests = get_posts(
|
||||
array(
|
||||
'post_type' => Guest::POST_TYPE,
|
||||
'posts_per_page' => 1,
|
||||
'post_status' => 'publish',
|
||||
'meta_query' => array(
|
||||
array(
|
||||
'key' => '_bnb_guest_email',
|
||||
'value' => $email,
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! empty( $existing_guests ) ) {
|
||||
return $existing_guests[0]->ID;
|
||||
}
|
||||
|
||||
// Create new guest.
|
||||
$first_name = $order->get_billing_first_name();
|
||||
$last_name = $order->get_billing_last_name();
|
||||
$full_name = trim( $first_name . ' ' . $last_name );
|
||||
|
||||
$guest_data = array(
|
||||
'post_type' => Guest::POST_TYPE,
|
||||
'post_status' => 'publish',
|
||||
'post_title' => $full_name ?: $email,
|
||||
);
|
||||
|
||||
$guest_id = wp_insert_post( $guest_data );
|
||||
|
||||
if ( ! $guest_id || is_wp_error( $guest_id ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Store guest meta.
|
||||
update_post_meta( $guest_id, '_bnb_guest_first_name', $first_name );
|
||||
update_post_meta( $guest_id, '_bnb_guest_last_name', $last_name );
|
||||
update_post_meta( $guest_id, '_bnb_guest_email', $email );
|
||||
update_post_meta( $guest_id, '_bnb_guest_phone', $order->get_billing_phone() );
|
||||
update_post_meta( $guest_id, '_bnb_guest_address', $order->get_billing_address_1() );
|
||||
update_post_meta( $guest_id, '_bnb_guest_city', $order->get_billing_city() );
|
||||
update_post_meta( $guest_id, '_bnb_guest_postal_code', $order->get_billing_postcode() );
|
||||
update_post_meta( $guest_id, '_bnb_guest_country', $order->get_billing_country() );
|
||||
update_post_meta( $guest_id, '_bnb_guest_status', 'active' );
|
||||
update_post_meta( $guest_id, '_bnb_guest_source', 'woocommerce' );
|
||||
|
||||
return $guest_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate booking title.
|
||||
*
|
||||
* @param int|null $guest_id Guest ID.
|
||||
* @param string $check_in Check-in date.
|
||||
* @param string $check_out Check-out date.
|
||||
* @return string Booking title.
|
||||
*/
|
||||
private static function generate_booking_title( ?int $guest_id, string $check_in, string $check_out ): string {
|
||||
$guest_name = $guest_id ? Guest::get_full_name( $guest_id ) : __( 'Guest', 'wp-bnb' );
|
||||
|
||||
$check_in_date = \DateTime::createFromFormat( 'Y-m-d', $check_in );
|
||||
$check_out_date = \DateTime::createFromFormat( 'Y-m-d', $check_out );
|
||||
|
||||
if ( ! $check_in_date || ! $check_out_date ) {
|
||||
return $guest_name;
|
||||
}
|
||||
|
||||
// Format: "Guest Name (DD.MM - DD.MM.YYYY)" or span years.
|
||||
if ( $check_in_date->format( 'Y' ) === $check_out_date->format( 'Y' ) ) {
|
||||
return sprintf(
|
||||
'%s (%s - %s)',
|
||||
$guest_name,
|
||||
$check_in_date->format( 'd.m' ),
|
||||
$check_out_date->format( 'd.m.Y' )
|
||||
);
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'%s (%s - %s)',
|
||||
$guest_name,
|
||||
$check_in_date->format( 'd.m.Y' ),
|
||||
$check_out_date->format( 'd.m.Y' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate booking reference.
|
||||
*
|
||||
* @param int $booking_id Booking ID.
|
||||
* @return string Booking reference.
|
||||
*/
|
||||
private static function generate_booking_reference( int $booking_id ): string {
|
||||
return sprintf(
|
||||
'BNB-%s-%05d',
|
||||
gmdate( 'Y' ),
|
||||
$booking_id
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display booking info in admin order page.
|
||||
*
|
||||
* @param \WC_Order $order WooCommerce order.
|
||||
* @return void
|
||||
*/
|
||||
public static function display_booking_info_admin( \WC_Order $order ): void {
|
||||
$booking_id = Manager::get_booking_for_order( $order );
|
||||
|
||||
if ( ! $booking_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$booking = get_post( $booking_id );
|
||||
$status = get_post_meta( $booking_id, '_bnb_booking_status', true );
|
||||
$room_id = get_post_meta( $booking_id, '_bnb_booking_room_id', true );
|
||||
$room = $room_id ? get_post( $room_id ) : null;
|
||||
?>
|
||||
<div class="order_data_column bnb-order-booking-info">
|
||||
<h3><?php esc_html_e( 'Booking Information', 'wp-bnb' ); ?></h3>
|
||||
<p>
|
||||
<strong><?php esc_html_e( 'Booking:', 'wp-bnb' ); ?></strong>
|
||||
<a href="<?php echo esc_url( get_edit_post_link( $booking_id ) ); ?>">
|
||||
<?php echo esc_html( $booking ? $booking->post_title : "#{$booking_id}" ); ?>
|
||||
</a>
|
||||
</p>
|
||||
<?php if ( $room ) : ?>
|
||||
<p>
|
||||
<strong><?php esc_html_e( 'Room:', 'wp-bnb' ); ?></strong>
|
||||
<a href="<?php echo esc_url( get_edit_post_link( $room_id ) ); ?>">
|
||||
<?php echo esc_html( $room->post_title ); ?>
|
||||
</a>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
<p>
|
||||
<strong><?php esc_html_e( 'Status:', 'wp-bnb' ); ?></strong>
|
||||
<span class="bnb-status-badge bnb-status-<?php echo esc_attr( $status ); ?>">
|
||||
<?php echo esc_html( ucfirst( str_replace( '_', ' ', $status ) ) ); ?>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Display booking info on thank you page.
|
||||
*
|
||||
* @param int $order_id Order ID.
|
||||
* @return void
|
||||
*/
|
||||
public static function display_booking_info_thankyou( int $order_id ): void {
|
||||
$order = wc_get_order( $order_id );
|
||||
|
||||
if ( ! $order ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$booking_id = Manager::get_booking_for_order( $order );
|
||||
|
||||
if ( ! $booking_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$reference = get_post_meta( $booking_id, '_bnb_booking_reference', true );
|
||||
$check_in = get_post_meta( $booking_id, '_bnb_booking_check_in', true );
|
||||
$check_out = get_post_meta( $booking_id, '_bnb_booking_check_out', true );
|
||||
$room_id = get_post_meta( $booking_id, '_bnb_booking_room_id', true );
|
||||
$room = $room_id ? get_post( $room_id ) : null;
|
||||
$building = $room_id ? Room::get_building( $room_id ) : null;
|
||||
|
||||
$date_format = get_option( 'date_format' );
|
||||
$check_in_date = \DateTime::createFromFormat( 'Y-m-d', $check_in );
|
||||
$check_out_date = \DateTime::createFromFormat( 'Y-m-d', $check_out );
|
||||
?>
|
||||
<section class="woocommerce-booking-confirmation">
|
||||
<h2><?php esc_html_e( 'Booking Confirmed', 'wp-bnb' ); ?></h2>
|
||||
<table class="woocommerce-table woocommerce-table--booking-details">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'Booking Reference:', 'wp-bnb' ); ?></th>
|
||||
<td><strong><?php echo esc_html( $reference ); ?></strong></td>
|
||||
</tr>
|
||||
<?php if ( $room ) : ?>
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'Room:', 'wp-bnb' ); ?></th>
|
||||
<td>
|
||||
<?php echo esc_html( $room->post_title ); ?>
|
||||
<?php if ( $building ) : ?>
|
||||
<br><small><?php echo esc_html( $building->post_title ); ?></small>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'Check-in:', 'wp-bnb' ); ?></th>
|
||||
<td><?php echo esc_html( $check_in_date ? $check_in_date->format( $date_format ) : $check_in ); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'Check-out:', 'wp-bnb' ); ?></th>
|
||||
<td><?php echo esc_html( $check_out_date ? $check_out_date->format( $date_format ) : $check_out ); ?></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="woocommerce-notice woocommerce-notice--success">
|
||||
<?php esc_html_e( 'Your booking has been confirmed. A confirmation email has been sent to your email address.', 'wp-bnb' ); ?>
|
||||
</p>
|
||||
</section>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Display booking info in order details (customer account).
|
||||
*
|
||||
* @param \WC_Order $order WooCommerce order.
|
||||
* @return void
|
||||
*/
|
||||
public static function display_booking_info_order_details( \WC_Order $order ): void {
|
||||
$booking_id = Manager::get_booking_for_order( $order );
|
||||
|
||||
if ( ! $booking_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$reference = get_post_meta( $booking_id, '_bnb_booking_reference', true );
|
||||
$status = get_post_meta( $booking_id, '_bnb_booking_status', true );
|
||||
|
||||
if ( $reference ) {
|
||||
?>
|
||||
<p class="woocommerce-booking-reference">
|
||||
<strong><?php esc_html_e( 'Booking Reference:', 'wp-bnb' ); ?></strong>
|
||||
<?php echo esc_html( $reference ); ?>
|
||||
<span class="bnb-status-badge bnb-status-<?php echo esc_attr( $status ); ?>">
|
||||
<?php echo esc_html( ucfirst( str_replace( '_', ' ', $status ) ) ); ?>
|
||||
</span>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
}
|
||||
515
src/Integration/WooCommerce/ProductSync.php
Normal file
515
src/Integration/WooCommerce/ProductSync.php
Normal file
@@ -0,0 +1,515 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Product Synchronization.
|
||||
*
|
||||
* Synchronizes rooms with WooCommerce products.
|
||||
*
|
||||
* @package Magdev\WpBnb\Integration\WooCommerce
|
||||
*/
|
||||
|
||||
declare( strict_types=1 );
|
||||
|
||||
namespace Magdev\WpBnb\Integration\WooCommerce;
|
||||
|
||||
use Magdev\WpBnb\PostTypes\Building;
|
||||
use Magdev\WpBnb\PostTypes\Room;
|
||||
use Magdev\WpBnb\Pricing\Calculator;
|
||||
use Magdev\WpBnb\Pricing\PricingTier;
|
||||
use Magdev\WpBnb\Taxonomies\Amenity;
|
||||
use Magdev\WpBnb\Taxonomies\RoomType;
|
||||
|
||||
/**
|
||||
* Product Synchronization class.
|
||||
*
|
||||
* Creates and maintains WooCommerce products that correspond to BnB rooms.
|
||||
*/
|
||||
final class ProductSync {
|
||||
|
||||
/**
|
||||
* Initialize product synchronization.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init(): void {
|
||||
// Sync product when room is saved.
|
||||
add_action( 'save_post_' . Room::POST_TYPE, array( self::class, 'on_room_save' ), 20, 2 );
|
||||
|
||||
// Delete product when room is deleted.
|
||||
add_action( 'before_delete_post', array( self::class, 'on_room_delete' ) );
|
||||
|
||||
// Add linked room info to product edit screen.
|
||||
add_action( 'woocommerce_product_options_general_product_data', array( self::class, 'add_product_room_info' ) );
|
||||
|
||||
// AJAX handler for syncing all rooms.
|
||||
add_action( 'wp_ajax_wp_bnb_sync_all_rooms', array( self::class, 'ajax_sync_all_rooms' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle room save - create or update product.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public static function on_room_save( int $post_id, \WP_Post $post ): void {
|
||||
// Skip autosaves, revisions, and other non-published posts.
|
||||
if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( 'publish' !== $post->post_status ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if auto-sync is enabled.
|
||||
if ( ! Manager::is_auto_sync_enabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sync the product.
|
||||
self::sync_room_to_product( $post_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle room deletion - delete linked product.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @return void
|
||||
*/
|
||||
public static function on_room_delete( int $post_id ): void {
|
||||
$post = get_post( $post_id );
|
||||
|
||||
if ( ! $post || Room::POST_TYPE !== $post->post_type ) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::delete_product_for_room( $post_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync a room to a WooCommerce product.
|
||||
*
|
||||
* Creates a new product if one doesn't exist, or updates existing one.
|
||||
*
|
||||
* @param int $room_id Room post ID.
|
||||
* @return int|null Product ID or null on failure.
|
||||
*/
|
||||
public static function sync_room_to_product( int $room_id ): ?int {
|
||||
$room = get_post( $room_id );
|
||||
|
||||
if ( ! $room || Room::POST_TYPE !== $room->post_type ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check for existing product.
|
||||
$product_id = self::get_product_for_room( $room_id );
|
||||
|
||||
if ( $product_id ) {
|
||||
// Update existing product.
|
||||
return self::update_product( $product_id, $room );
|
||||
}
|
||||
|
||||
// Create new product.
|
||||
return self::create_product_for_room( $room );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a WooCommerce product for a room.
|
||||
*
|
||||
* @param \WP_Post $room Room post object.
|
||||
* @return int|null Product ID or null on failure.
|
||||
*/
|
||||
public static function create_product_for_room( \WP_Post $room ): ?int {
|
||||
/**
|
||||
* Fires before creating a WC product for a room.
|
||||
*
|
||||
* @param int $room_id Room post ID.
|
||||
*/
|
||||
do_action( 'wp_bnb_wc_before_product_sync', $room->ID );
|
||||
|
||||
// Create a simple virtual product.
|
||||
$product = new \WC_Product_Simple();
|
||||
|
||||
// Configure the product.
|
||||
self::configure_product( $product, $room );
|
||||
|
||||
// Save the product.
|
||||
$product_id = $product->save();
|
||||
|
||||
if ( ! $product_id ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Store bidirectional links.
|
||||
update_post_meta( $room->ID, Manager::ROOM_PRODUCT_META, $product_id );
|
||||
update_post_meta( $product_id, Manager::PRODUCT_ROOM_META, $room->ID );
|
||||
|
||||
/**
|
||||
* Fires after creating a WC product for a room.
|
||||
*
|
||||
* @param int $room_id Room post ID.
|
||||
* @param int $product_id WC product ID.
|
||||
*/
|
||||
do_action( 'wp_bnb_wc_after_product_sync', $room->ID, $product_id );
|
||||
|
||||
return $product_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing WooCommerce product.
|
||||
*
|
||||
* @param int $product_id Product ID.
|
||||
* @param \WP_Post $room Room post object.
|
||||
* @return int|null Product ID or null on failure.
|
||||
*/
|
||||
private static function update_product( int $product_id, \WP_Post $room ): ?int {
|
||||
$product = wc_get_product( $product_id );
|
||||
|
||||
if ( ! $product ) {
|
||||
// Product was deleted, create a new one.
|
||||
return self::create_product_for_room( $room );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires before updating a WC product for a room.
|
||||
*
|
||||
* @param int $room_id Room post ID.
|
||||
*/
|
||||
do_action( 'wp_bnb_wc_before_product_sync', $room->ID );
|
||||
|
||||
// Configure the product.
|
||||
self::configure_product( $product, $room );
|
||||
|
||||
// Save the product.
|
||||
$product->save();
|
||||
|
||||
/**
|
||||
* Fires after updating a WC product for a room.
|
||||
*
|
||||
* @param int $room_id Room post ID.
|
||||
* @param int $product_id WC product ID.
|
||||
*/
|
||||
do_action( 'wp_bnb_wc_after_product_sync', $room->ID, $product_id );
|
||||
|
||||
return $product_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a WooCommerce product from room data.
|
||||
*
|
||||
* @param \WC_Product $product Product object.
|
||||
* @param \WP_Post $room Room post object.
|
||||
* @return void
|
||||
*/
|
||||
private static function configure_product( \WC_Product $product, \WP_Post $room ): void {
|
||||
// Get room data.
|
||||
$building = Room::get_building( $room->ID );
|
||||
$building_name = $building ? $building->post_title : '';
|
||||
$pricing = Calculator::getRoomPricing( $room->ID );
|
||||
|
||||
// Basic info.
|
||||
$product->set_name( $room->post_title );
|
||||
$product->set_slug( 'bnb-room-' . $room->ID );
|
||||
$product->set_status( 'publish' );
|
||||
|
||||
// SKU.
|
||||
$product->set_sku( 'bnb-room-' . $room->ID );
|
||||
|
||||
// Virtual product (no shipping).
|
||||
$product->set_virtual( true );
|
||||
|
||||
// Description.
|
||||
$description = $room->post_content;
|
||||
if ( $building_name ) {
|
||||
$description = sprintf(
|
||||
/* translators: %s: Building name */
|
||||
__( 'Room at %s', 'wp-bnb' ),
|
||||
$building_name
|
||||
) . "\n\n" . $description;
|
||||
}
|
||||
$product->set_description( $description );
|
||||
$product->set_short_description( $room->post_excerpt ?: wp_trim_words( $room->post_content, 30 ) );
|
||||
|
||||
// Price (use short-term/nightly rate as base).
|
||||
$base_price = $pricing[ PricingTier::SHORT_TERM->value ]['price'] ?? 0;
|
||||
if ( $base_price > 0 ) {
|
||||
$product->set_regular_price( (string) $base_price );
|
||||
}
|
||||
|
||||
// Featured image.
|
||||
$thumbnail_id = get_post_thumbnail_id( $room->ID );
|
||||
if ( $thumbnail_id ) {
|
||||
$product->set_image_id( $thumbnail_id );
|
||||
}
|
||||
|
||||
// Gallery images.
|
||||
$gallery_ids = get_post_meta( $room->ID, '_bnb_room_gallery', true );
|
||||
if ( $gallery_ids ) {
|
||||
$ids = array_filter( explode( ',', $gallery_ids ), 'is_numeric' );
|
||||
$product->set_gallery_image_ids( array_map( 'absint', $ids ) );
|
||||
}
|
||||
|
||||
// Stock management (disabled - availability handled by booking system).
|
||||
$product->set_manage_stock( false );
|
||||
$product->set_stock_status( 'instock' );
|
||||
|
||||
// Catalog visibility - visible.
|
||||
$product->set_catalog_visibility( 'visible' );
|
||||
|
||||
// Product category.
|
||||
$category_id = Manager::get_product_category();
|
||||
if ( $category_id ) {
|
||||
$product->set_category_ids( array( $category_id ) );
|
||||
}
|
||||
|
||||
// Store room metadata.
|
||||
$capacity = get_post_meta( $room->ID, '_bnb_room_capacity', true );
|
||||
$beds = get_post_meta( $room->ID, '_bnb_room_beds', true );
|
||||
|
||||
$product->update_meta_data( '_bnb_room_capacity', $capacity );
|
||||
$product->update_meta_data( '_bnb_room_beds', $beds );
|
||||
$product->update_meta_data( '_bnb_building_id', $building ? $building->ID : 0 );
|
||||
$product->update_meta_data( '_bnb_building_name', $building_name );
|
||||
|
||||
/**
|
||||
* Filter the product data before save.
|
||||
*
|
||||
* @param array $data Product data array.
|
||||
* @param int $room_id Room post ID.
|
||||
* @param \WP_Post $room Room post object.
|
||||
*/
|
||||
$data = apply_filters(
|
||||
'wp_bnb_wc_product_data',
|
||||
array(
|
||||
'name' => $product->get_name(),
|
||||
'price' => $product->get_regular_price(),
|
||||
'description' => $product->get_description(),
|
||||
),
|
||||
$room->ID,
|
||||
$room
|
||||
);
|
||||
|
||||
// Apply filtered data.
|
||||
if ( isset( $data['name'] ) ) {
|
||||
$product->set_name( $data['name'] );
|
||||
}
|
||||
if ( isset( $data['price'] ) ) {
|
||||
$product->set_regular_price( (string) $data['price'] );
|
||||
}
|
||||
if ( isset( $data['description'] ) ) {
|
||||
$product->set_description( $data['description'] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the WooCommerce product for a room.
|
||||
*
|
||||
* @param int $room_id Room post ID.
|
||||
* @return bool True if deleted, false otherwise.
|
||||
*/
|
||||
public static function delete_product_for_room( int $room_id ): bool {
|
||||
$product_id = self::get_product_for_room( $room_id );
|
||||
|
||||
if ( ! $product_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$product = wc_get_product( $product_id );
|
||||
|
||||
if ( ! $product ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Delete the product (force delete, not trash).
|
||||
$product->delete( true );
|
||||
|
||||
// Clean up room meta.
|
||||
delete_post_meta( $room_id, Manager::ROOM_PRODUCT_META );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the WooCommerce product ID for a room.
|
||||
*
|
||||
* @param int $room_id Room post ID.
|
||||
* @return int|null Product ID or null.
|
||||
*/
|
||||
public static function get_product_for_room( int $room_id ): ?int {
|
||||
$product_id = get_post_meta( $room_id, Manager::ROOM_PRODUCT_META, true );
|
||||
|
||||
if ( ! $product_id ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Verify product still exists.
|
||||
$product = wc_get_product( $product_id );
|
||||
|
||||
if ( ! $product ) {
|
||||
// Clean up stale meta.
|
||||
delete_post_meta( $room_id, Manager::ROOM_PRODUCT_META );
|
||||
return null;
|
||||
}
|
||||
|
||||
return absint( $product_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the room ID for a WooCommerce product.
|
||||
*
|
||||
* @param int $product_id Product ID.
|
||||
* @return int|null Room ID or null.
|
||||
*/
|
||||
public static function get_room_for_product( int $product_id ): ?int {
|
||||
$room_id = get_post_meta( $product_id, Manager::PRODUCT_ROOM_META, true );
|
||||
|
||||
if ( ! $room_id ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Verify room still exists.
|
||||
$room = get_post( $room_id );
|
||||
|
||||
if ( ! $room || Room::POST_TYPE !== $room->post_type ) {
|
||||
// Clean up stale meta.
|
||||
delete_post_meta( $product_id, Manager::PRODUCT_ROOM_META );
|
||||
return null;
|
||||
}
|
||||
|
||||
return absint( $room_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync all published rooms to WooCommerce products.
|
||||
*
|
||||
* @return array{created: int, updated: int, errors: array<string>}
|
||||
*/
|
||||
public static function sync_all_rooms(): array {
|
||||
$result = array(
|
||||
'created' => 0,
|
||||
'updated' => 0,
|
||||
'errors' => array(),
|
||||
);
|
||||
|
||||
$rooms = get_posts(
|
||||
array(
|
||||
'post_type' => Room::POST_TYPE,
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'fields' => 'ids',
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $rooms as $room_id ) {
|
||||
$existing_product = self::get_product_for_room( $room_id );
|
||||
|
||||
$product_id = self::sync_room_to_product( $room_id );
|
||||
|
||||
if ( $product_id ) {
|
||||
if ( $existing_product ) {
|
||||
++$result['updated'];
|
||||
} else {
|
||||
++$result['created'];
|
||||
}
|
||||
} else {
|
||||
$room = get_post( $room_id );
|
||||
$result['errors'][] = sprintf(
|
||||
/* translators: %s: Room title */
|
||||
__( 'Failed to sync room: %s', 'wp-bnb' ),
|
||||
$room ? $room->post_title : "#{$room_id}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler for syncing all rooms.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function ajax_sync_all_rooms(): void {
|
||||
check_ajax_referer( 'wp_bnb_admin_nonce', 'nonce' );
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Permission denied.', 'wp-bnb' ) ) );
|
||||
}
|
||||
|
||||
$result = self::sync_all_rooms();
|
||||
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'message' => sprintf(
|
||||
/* translators: 1: Created count, 2: Updated count */
|
||||
__( 'Sync complete. Created: %1$d, Updated: %2$d', 'wp-bnb' ),
|
||||
$result['created'],
|
||||
$result['updated']
|
||||
),
|
||||
'created' => $result['created'],
|
||||
'updated' => $result['updated'],
|
||||
'errors' => $result['errors'],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add linked room info to WooCommerce product edit screen.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function add_product_room_info(): void {
|
||||
global $post;
|
||||
|
||||
if ( ! $post ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$room_id = self::get_room_for_product( $post->ID );
|
||||
|
||||
if ( ! $room_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$room = get_post( $room_id );
|
||||
|
||||
if ( ! $room ) {
|
||||
return;
|
||||
}
|
||||
|
||||
?>
|
||||
<div class="options_group show_if_simple">
|
||||
<p class="form-field">
|
||||
<label><?php esc_html_e( 'Linked BnB Room', 'wp-bnb' ); ?></label>
|
||||
<span class="description">
|
||||
<a href="<?php echo esc_url( get_edit_post_link( $room_id ) ); ?>">
|
||||
<?php echo esc_html( $room->post_title ); ?>
|
||||
</a>
|
||||
<br>
|
||||
<small><?php esc_html_e( 'This product is automatically synced from the linked room. Changes should be made in the room settings.', 'wp-bnb' ); ?></small>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a product is a BnB room product.
|
||||
*
|
||||
* @param int|\WC_Product $product Product ID or object.
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_room_product( $product ): bool {
|
||||
if ( is_int( $product ) ) {
|
||||
$product = wc_get_product( $product );
|
||||
}
|
||||
|
||||
if ( ! $product instanceof \WC_Product ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$room_id = $product->get_meta( Manager::PRODUCT_ROOM_META, true );
|
||||
|
||||
return ! empty( $room_id );
|
||||
}
|
||||
}
|
||||
394
src/Integration/WooCommerce/RefundHandler.php
Normal file
394
src/Integration/WooCommerce/RefundHandler.php
Normal file
@@ -0,0 +1,394 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Refund Handler.
|
||||
*
|
||||
* Handles refund processing and booking cancellation.
|
||||
*
|
||||
* @package Magdev\WpBnb\Integration\WooCommerce
|
||||
*/
|
||||
|
||||
declare( strict_types=1 );
|
||||
|
||||
namespace Magdev\WpBnb\Integration\WooCommerce;
|
||||
|
||||
use Magdev\WpBnb\Booking\EmailNotifier;
|
||||
use Magdev\WpBnb\PostTypes\Booking;
|
||||
|
||||
/**
|
||||
* Refund Handler class.
|
||||
*
|
||||
* Processes refunds and updates booking status accordingly.
|
||||
*/
|
||||
final class RefundHandler {
|
||||
|
||||
/**
|
||||
* Booking meta key for refund amount.
|
||||
*/
|
||||
public const REFUND_AMOUNT_META = '_bnb_booking_refund_amount';
|
||||
|
||||
/**
|
||||
* Booking meta key for refund reason.
|
||||
*/
|
||||
public const REFUND_REASON_META = '_bnb_booking_refund_reason';
|
||||
|
||||
/**
|
||||
* Booking meta key for refund date.
|
||||
*/
|
||||
public const REFUND_DATE_META = '_bnb_booking_refund_date';
|
||||
|
||||
/**
|
||||
* Initialize refund handler.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init(): void {
|
||||
// Handle refund creation.
|
||||
add_action( 'woocommerce_refund_created', array( self::class, 'on_refund_created' ), 10, 2 );
|
||||
|
||||
// Handle order fully refunded.
|
||||
add_action( 'woocommerce_order_fully_refunded', array( self::class, 'on_order_fully_refunded' ), 10, 2 );
|
||||
|
||||
// Add refund notice in admin order page.
|
||||
add_action( 'woocommerce_admin_order_totals_after_refunded', array( self::class, 'add_booking_refund_notice' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle refund creation.
|
||||
*
|
||||
* @param int $refund_id Refund ID.
|
||||
* @param array $args Refund arguments.
|
||||
* @return void
|
||||
*/
|
||||
public static function on_refund_created( int $refund_id, array $args ): void {
|
||||
$order_id = $args['order_id'] ?? 0;
|
||||
|
||||
if ( ! $order_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$order = wc_get_order( $order_id );
|
||||
|
||||
if ( ! $order instanceof \WC_Order ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if order has a linked booking.
|
||||
$booking_id = Manager::get_booking_for_order( $order );
|
||||
|
||||
if ( ! $booking_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$refund_amount = abs( floatval( $args['amount'] ?? 0 ) );
|
||||
$refund_reason = $args['reason'] ?? '';
|
||||
|
||||
/**
|
||||
* Fires before processing a refund for a booking.
|
||||
*
|
||||
* @param \WC_Order $order WooCommerce order.
|
||||
* @param float $refund_amount Refund amount.
|
||||
*/
|
||||
do_action( 'wp_bnb_wc_before_refund_process', $order, $refund_amount );
|
||||
|
||||
// Check if this is a full or partial refund.
|
||||
$is_full_refund = self::is_full_refund( $order );
|
||||
|
||||
if ( $is_full_refund ) {
|
||||
// Full refund - cancel the booking.
|
||||
self::cancel_booking_on_refund( $booking_id, $refund_amount, $refund_reason );
|
||||
} else {
|
||||
// Partial refund - store refund info but don't cancel.
|
||||
self::record_partial_refund( $booking_id, $refund_amount, $refund_reason );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires after processing a refund for a booking.
|
||||
*
|
||||
* @param \WC_Order $order WooCommerce order.
|
||||
* @param int $booking_id Booking ID.
|
||||
* @param bool $cancelled Whether booking was cancelled.
|
||||
*/
|
||||
do_action( 'wp_bnb_wc_after_refund_process', $order, $booking_id, $is_full_refund );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle order fully refunded.
|
||||
*
|
||||
* @param int $order_id Order ID.
|
||||
* @param int $refund_id Refund ID.
|
||||
* @return void
|
||||
*/
|
||||
public static function on_order_fully_refunded( int $order_id, int $refund_id ): void {
|
||||
$order = wc_get_order( $order_id );
|
||||
|
||||
if ( ! $order instanceof \WC_Order ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$booking_id = Manager::get_booking_for_order( $order );
|
||||
|
||||
if ( ! $booking_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current booking status.
|
||||
$current_status = get_post_meta( $booking_id, '_bnb_booking_status', true );
|
||||
|
||||
// Don't cancel if already cancelled.
|
||||
if ( 'cancelled' === $current_status ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel the booking.
|
||||
$total_refunded = $order->get_total_refunded();
|
||||
self::cancel_booking_on_refund( $booking_id, $total_refunded, __( 'Order fully refunded', 'wp-bnb' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the order is fully refunded.
|
||||
*
|
||||
* @param \WC_Order $order WooCommerce order.
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_full_refund( \WC_Order $order ): bool {
|
||||
$order_total = floatval( $order->get_total() );
|
||||
$total_refunded = floatval( $order->get_total_refunded() );
|
||||
|
||||
// Consider it full refund if refunded amount >= order total.
|
||||
return $total_refunded >= $order_total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel booking on full refund.
|
||||
*
|
||||
* @param int $booking_id Booking ID.
|
||||
* @param float $refund_amount Refund amount.
|
||||
* @param string $reason Refund reason.
|
||||
* @return void
|
||||
*/
|
||||
public static function cancel_booking_on_refund( int $booking_id, float $refund_amount, string $reason ): void {
|
||||
// Get current status.
|
||||
$old_status = get_post_meta( $booking_id, '_bnb_booking_status', true );
|
||||
|
||||
// Don't cancel if already cancelled.
|
||||
if ( 'cancelled' === $old_status ) {
|
||||
// Just update refund info.
|
||||
self::record_refund_meta( $booking_id, $refund_amount, $reason );
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter whether to cancel booking on refund.
|
||||
*
|
||||
* @param bool $cancel Whether to cancel.
|
||||
* @param \WC_Order $order WooCommerce order.
|
||||
* @param float $refund_amount Refund amount.
|
||||
*/
|
||||
$should_cancel = apply_filters( 'wp_bnb_wc_should_cancel_on_refund', true, $booking_id, $refund_amount );
|
||||
|
||||
if ( ! $should_cancel ) {
|
||||
self::record_partial_refund( $booking_id, $refund_amount, $reason );
|
||||
return;
|
||||
}
|
||||
|
||||
// Update booking status to cancelled.
|
||||
update_post_meta( $booking_id, '_bnb_booking_status', 'cancelled' );
|
||||
|
||||
// Store refund information.
|
||||
self::record_refund_meta( $booking_id, $refund_amount, $reason );
|
||||
|
||||
// Add cancellation note.
|
||||
$note = sprintf(
|
||||
/* translators: %s: Refund amount */
|
||||
__( 'Booking cancelled due to WooCommerce refund (%s)', 'wp-bnb' ),
|
||||
wc_price( $refund_amount )
|
||||
);
|
||||
|
||||
$existing_notes = get_post_meta( $booking_id, '_bnb_booking_notes', true );
|
||||
$new_notes = $existing_notes ? $existing_notes . "\n\n" . $note : $note;
|
||||
update_post_meta( $booking_id, '_bnb_booking_notes', $new_notes );
|
||||
|
||||
/**
|
||||
* Fires when booking status changes (triggers email notifications).
|
||||
*
|
||||
* @param int $booking_id Booking ID.
|
||||
* @param string $old_status Old status.
|
||||
* @param string $new_status New status.
|
||||
*/
|
||||
do_action( 'wp_bnb_booking_status_changed', $booking_id, $old_status, 'cancelled' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Record partial refund without cancelling.
|
||||
*
|
||||
* @param int $booking_id Booking ID.
|
||||
* @param float $refund_amount Refund amount.
|
||||
* @param string $reason Refund reason.
|
||||
* @return void
|
||||
*/
|
||||
private static function record_partial_refund( int $booking_id, float $refund_amount, string $reason ): void {
|
||||
// Get existing refund amount and add to it.
|
||||
$existing_refund = floatval( get_post_meta( $booking_id, self::REFUND_AMOUNT_META, true ) );
|
||||
$total_refund = $existing_refund + $refund_amount;
|
||||
|
||||
// Update refund meta.
|
||||
self::record_refund_meta( $booking_id, $total_refund, $reason );
|
||||
|
||||
// Add note about partial refund.
|
||||
$note = sprintf(
|
||||
/* translators: %s: Refund amount */
|
||||
__( 'Partial refund processed: %s', 'wp-bnb' ),
|
||||
wc_price( $refund_amount )
|
||||
);
|
||||
|
||||
$existing_notes = get_post_meta( $booking_id, '_bnb_booking_notes', true );
|
||||
$new_notes = $existing_notes ? $existing_notes . "\n\n" . $note : $note;
|
||||
update_post_meta( $booking_id, '_bnb_booking_notes', $new_notes );
|
||||
}
|
||||
|
||||
/**
|
||||
* Record refund metadata.
|
||||
*
|
||||
* @param int $booking_id Booking ID.
|
||||
* @param float $refund_amount Total refund amount.
|
||||
* @param string $reason Refund reason.
|
||||
* @return void
|
||||
*/
|
||||
private static function record_refund_meta( int $booking_id, float $refund_amount, string $reason ): void {
|
||||
update_post_meta( $booking_id, self::REFUND_AMOUNT_META, $refund_amount );
|
||||
update_post_meta( $booking_id, self::REFUND_DATE_META, current_time( 'mysql' ) );
|
||||
|
||||
if ( $reason ) {
|
||||
update_post_meta( $booking_id, self::REFUND_REASON_META, $reason );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate refund amount for a booking.
|
||||
*
|
||||
* @param int $booking_id Booking ID.
|
||||
* @param string $type Refund type: 'full' or 'nights_remaining'.
|
||||
* @return float Refund amount.
|
||||
*/
|
||||
public static function calculate_refund_amount( int $booking_id, string $type = 'full' ): float {
|
||||
$calculated_price = floatval( get_post_meta( $booking_id, '_bnb_booking_calculated_price', true ) );
|
||||
|
||||
if ( 'full' === $type ) {
|
||||
return $calculated_price;
|
||||
}
|
||||
|
||||
// Calculate pro-rata based on nights remaining.
|
||||
$check_in = get_post_meta( $booking_id, '_bnb_booking_check_in', true );
|
||||
$check_out = get_post_meta( $booking_id, '_bnb_booking_check_out', true );
|
||||
|
||||
if ( ! $check_in || ! $check_out ) {
|
||||
return $calculated_price;
|
||||
}
|
||||
|
||||
$today = new \DateTime( 'today' );
|
||||
$check_in_date = \DateTime::createFromFormat( 'Y-m-d', $check_in );
|
||||
$check_out_date = \DateTime::createFromFormat( 'Y-m-d', $check_out );
|
||||
|
||||
if ( ! $check_in_date || ! $check_out_date ) {
|
||||
return $calculated_price;
|
||||
}
|
||||
|
||||
// If check-in hasn't happened, full refund.
|
||||
if ( $today < $check_in_date ) {
|
||||
return $calculated_price;
|
||||
}
|
||||
|
||||
// If check-out has passed, no refund.
|
||||
if ( $today >= $check_out_date ) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Calculate remaining nights.
|
||||
$total_nights = $check_in_date->diff( $check_out_date )->days;
|
||||
$nights_used = $check_in_date->diff( $today )->days;
|
||||
$nights_remaining = $total_nights - $nights_used;
|
||||
|
||||
if ( $total_nights <= 0 ) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Pro-rata refund.
|
||||
$nightly_rate = $calculated_price / $total_nights;
|
||||
|
||||
return $nightly_rate * $nights_remaining;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get refund info for a booking.
|
||||
*
|
||||
* @param int $booking_id Booking ID.
|
||||
* @return array|null Refund info or null.
|
||||
*/
|
||||
public static function get_booking_refund_info( int $booking_id ): ?array {
|
||||
$amount = get_post_meta( $booking_id, self::REFUND_AMOUNT_META, true );
|
||||
|
||||
if ( ! $amount ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array(
|
||||
'amount' => floatval( $amount ),
|
||||
'reason' => get_post_meta( $booking_id, self::REFUND_REASON_META, true ),
|
||||
'date' => get_post_meta( $booking_id, self::REFUND_DATE_META, true ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add booking refund notice in admin order page.
|
||||
*
|
||||
* @param int $order_id Order ID.
|
||||
* @return void
|
||||
*/
|
||||
public static function add_booking_refund_notice( int $order_id ): void {
|
||||
$order = wc_get_order( $order_id );
|
||||
|
||||
if ( ! $order instanceof \WC_Order ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$booking_id = Manager::get_booking_for_order( $order );
|
||||
|
||||
if ( ! $booking_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$refund_info = self::get_booking_refund_info( $booking_id );
|
||||
|
||||
if ( ! $refund_info ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$booking_status = get_post_meta( $booking_id, '_bnb_booking_status', true );
|
||||
?>
|
||||
<tr>
|
||||
<td class="label"><?php esc_html_e( 'Booking Status', 'wp-bnb' ); ?>:</td>
|
||||
<td width="1%"></td>
|
||||
<td class="total">
|
||||
<span class="bnb-status-badge bnb-status-<?php echo esc_attr( $booking_status ); ?>">
|
||||
<?php echo esc_html( ucfirst( str_replace( '_', ' ', $booking_status ) ) ); ?>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<?php if ( 'cancelled' === $booking_status ) : ?>
|
||||
<tr>
|
||||
<td class="label" colspan="2">
|
||||
<small class="description">
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %s: Booking edit link */
|
||||
esc_html__( 'Booking was cancelled due to refund. %s', 'wp-bnb' ),
|
||||
'<a href="' . esc_url( get_edit_post_link( $booking_id ) ) . '">' . esc_html__( 'View booking', 'wp-bnb' ) . '</a>'
|
||||
);
|
||||
?>
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
840
src/Plugin.php
840
src/Plugin.php
@@ -21,6 +21,7 @@ use Magdev\WpBnb\Frontend\Shortcodes;
|
||||
use Magdev\WpBnb\Api\RestApi;
|
||||
use Magdev\WpBnb\Integration\CF7;
|
||||
use Magdev\WpBnb\Integration\Prometheus;
|
||||
use Magdev\WpBnb\Integration\WooCommerce\Manager as WooCommerceManager;
|
||||
use Magdev\WpBnb\Frontend\Widgets\AvailabilityCalendar;
|
||||
use Magdev\WpBnb\Frontend\Widgets\BuildingRooms;
|
||||
use Magdev\WpBnb\Frontend\Widgets\SimilarRooms;
|
||||
@@ -147,6 +148,11 @@ final class Plugin {
|
||||
// Initialize Prometheus metrics integration.
|
||||
Prometheus::init();
|
||||
|
||||
// Initialize WooCommerce integration if WooCommerce is active.
|
||||
if ( class_exists( 'WooCommerce' ) ) {
|
||||
WooCommerceManager::init();
|
||||
}
|
||||
|
||||
// Initialize REST API.
|
||||
$this->init_rest_api();
|
||||
|
||||
@@ -636,6 +642,10 @@ final class Plugin {
|
||||
class="nav-tab <?php echo 'api' === $active_tab ? 'nav-tab-active' : ''; ?>">
|
||||
<?php esc_html_e( 'API', 'wp-bnb' ); ?>
|
||||
</a>
|
||||
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wp-bnb-settings&tab=woocommerce' ) ); ?>"
|
||||
class="nav-tab <?php echo 'woocommerce' === $active_tab ? 'nav-tab-active' : ''; ?>">
|
||||
<?php esc_html_e( 'WooCommerce', 'wp-bnb' ); ?>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div class="tab-content">
|
||||
@@ -656,6 +666,9 @@ final class Plugin {
|
||||
case 'api':
|
||||
$this->render_api_settings();
|
||||
break;
|
||||
case 'woocommerce':
|
||||
$this->render_woocommerce_settings();
|
||||
break;
|
||||
default:
|
||||
$this->render_general_settings();
|
||||
break;
|
||||
@@ -1460,147 +1473,319 @@ final class Plugin {
|
||||
* @return void
|
||||
*/
|
||||
private function render_api_settings(): void {
|
||||
$api_enabled = get_option( 'wp_bnb_api_enabled', 'yes' );
|
||||
$rate_limiting = get_option( 'wp_bnb_api_rate_limiting', 'yes' );
|
||||
$api_base_url = rest_url( RestApi::NAMESPACE );
|
||||
// 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' ); ?>
|
||||
|
||||
<h2><?php esc_html_e( 'REST API Settings', 'wp-bnb' ); ?></h2>
|
||||
<?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' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2 style="margin-top: 30px;"><?php esc_html_e( 'API Information', 'wp-bnb' ); ?></h2>
|
||||
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Base URL', 'wp-bnb' ); ?></th>
|
||||
<td>
|
||||
<code><?php echo esc_html( $api_base_url ); ?></code>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'All API endpoints are prefixed with this URL.', 'wp-bnb' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'API Version', 'wp-bnb' ); ?></th>
|
||||
<td>
|
||||
<code><?php echo esc_html( RestApi::VERSION ); ?></code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Info Endpoint', 'wp-bnb' ); ?></th>
|
||||
<td>
|
||||
<code><?php echo esc_html( $api_base_url . '/info' ); ?></code>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Returns API information and available endpoints.', 'wp-bnb' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2 style="margin-top: 30px;"><?php esc_html_e( 'Available Endpoints', 'wp-bnb' ); ?></h2>
|
||||
|
||||
<h3><?php esc_html_e( 'Public Endpoints', 'wp-bnb' ); ?></h3>
|
||||
<table class="widefat" style="margin-bottom: 20px;">
|
||||
<thead>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'Method', 'wp-bnb' ); ?></th>
|
||||
<th><?php esc_html_e( 'Endpoint', 'wp-bnb' ); ?></th>
|
||||
<th><?php esc_html_e( 'Description', 'wp-bnb' ); ?></th>
|
||||
<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>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>GET</td><td><code>/buildings</code></td><td><?php esc_html_e( 'List all buildings', 'wp-bnb' ); ?></td></tr>
|
||||
<tr><td>GET</td><td><code>/buildings/{id}</code></td><td><?php esc_html_e( 'Get building details', 'wp-bnb' ); ?></td></tr>
|
||||
<tr><td>GET</td><td><code>/buildings/{id}/rooms</code></td><td><?php esc_html_e( 'List rooms in building', 'wp-bnb' ); ?></td></tr>
|
||||
<tr><td>GET</td><td><code>/rooms</code></td><td><?php esc_html_e( 'List/search rooms', 'wp-bnb' ); ?></td></tr>
|
||||
<tr><td>GET</td><td><code>/rooms/{id}</code></td><td><?php esc_html_e( 'Get room details', 'wp-bnb' ); ?></td></tr>
|
||||
<tr><td>GET</td><td><code>/rooms/{id}/availability</code></td><td><?php esc_html_e( 'Check room availability', 'wp-bnb' ); ?></td></tr>
|
||||
<tr><td>GET</td><td><code>/rooms/{id}/calendar</code></td><td><?php esc_html_e( 'Get room calendar', 'wp-bnb' ); ?></td></tr>
|
||||
<tr><td>POST</td><td><code>/availability/search</code></td><td><?php esc_html_e( 'Search available rooms', 'wp-bnb' ); ?></td></tr>
|
||||
<tr><td>GET</td><td><code>/services</code></td><td><?php esc_html_e( 'List services', 'wp-bnb' ); ?></td></tr>
|
||||
<tr><td>POST</td><td><code>/pricing/calculate</code></td><td><?php esc_html_e( 'Calculate booking price', 'wp-bnb' ); ?></td></tr>
|
||||
<tr><td>POST</td><td><code>/bookings</code></td><td><?php esc_html_e( 'Create booking (pending status)', 'wp-bnb' ); ?></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3><?php esc_html_e( 'Admin Endpoints (Requires Authentication)', 'wp-bnb' ); ?></h3>
|
||||
<table class="widefat" style="margin-bottom: 20px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'Method', 'wp-bnb' ); ?></th>
|
||||
<th><?php esc_html_e( 'Endpoint', 'wp-bnb' ); ?></th>
|
||||
<th><?php esc_html_e( 'Description', 'wp-bnb' ); ?></th>
|
||||
<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>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>GET</td><td><code>/bookings</code></td><td><?php esc_html_e( 'List all bookings', 'wp-bnb' ); ?></td></tr>
|
||||
<tr><td>GET</td><td><code>/bookings/{id}</code></td><td><?php esc_html_e( 'Get booking details', 'wp-bnb' ); ?></td></tr>
|
||||
<tr><td>PATCH</td><td><code>/bookings/{id}</code></td><td><?php esc_html_e( 'Update booking', 'wp-bnb' ); ?></td></tr>
|
||||
<tr><td>DELETE</td><td><code>/bookings/{id}</code></td><td><?php esc_html_e( 'Cancel booking', 'wp-bnb' ); ?></td></tr>
|
||||
<tr><td>POST</td><td><code>/bookings/{id}/confirm</code></td><td><?php esc_html_e( 'Confirm booking', 'wp-bnb' ); ?></td></tr>
|
||||
<tr><td>POST</td><td><code>/bookings/{id}/check-in</code></td><td><?php esc_html_e( 'Check in guest', 'wp-bnb' ); ?></td></tr>
|
||||
<tr><td>POST</td><td><code>/bookings/{id}/check-out</code></td><td><?php esc_html_e( 'Check out guest', 'wp-bnb' ); ?></td></tr>
|
||||
<tr><td>GET</td><td><code>/guests</code></td><td><?php esc_html_e( 'List guests', 'wp-bnb' ); ?></td></tr>
|
||||
<tr><td>GET</td><td><code>/guests/{id}</code></td><td><?php esc_html_e( 'Get guest details', 'wp-bnb' ); ?></td></tr>
|
||||
<tr><td>GET</td><td><code>/guests/search</code></td><td><?php esc_html_e( 'Search guests', 'wp-bnb' ); ?></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</table>
|
||||
|
||||
<h2 style="margin-top: 30px;"><?php esc_html_e( 'Authentication', 'wp-bnb' ); ?></h2>
|
||||
<p><?php esc_html_e( 'Admin endpoints require authentication. Use one of the following methods:', 'wp-bnb' ); ?></p>
|
||||
<ul style="list-style: disc; margin-left: 20px;">
|
||||
<li><strong><?php esc_html_e( 'Application Passwords:', 'wp-bnb' ); ?></strong> <?php esc_html_e( 'Create one in Users > Your Profile. Use Basic Auth with username and app password.', 'wp-bnb' ); ?></li>
|
||||
<li><strong><?php esc_html_e( 'Cookie + Nonce:', 'wp-bnb' ); ?></strong> <?php esc_html_e( 'For same-domain requests. Pass nonce in X-WP-Nonce header.', 'wp-bnb' ); ?></li>
|
||||
</ul>
|
||||
<h2 style="margin-top: 30px;"><?php esc_html_e( 'API Information', 'wp-bnb' ); ?></h2>
|
||||
|
||||
<h2 style="margin-top: 30px;"><?php esc_html_e( 'Rate Limits', 'wp-bnb' ); ?></h2>
|
||||
<table class="widefat" style="margin-bottom: 20px;">
|
||||
<thead>
|
||||
<table class="form-table">
|
||||
<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>
|
||||
<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>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><?php esc_html_e( 'Public', 'wp-bnb' ); ?></td><td>60/min</td><td><?php esc_html_e( 'GET rooms, buildings, services', 'wp-bnb' ); ?></td></tr>
|
||||
<tr><td><?php esc_html_e( 'Availability', 'wp-bnb' ); ?></td><td>30/min</td><td><?php esc_html_e( 'Availability checks, calendar', 'wp-bnb' ); ?></td></tr>
|
||||
<tr><td><?php esc_html_e( 'Booking', 'wp-bnb' ); ?></td><td>10/min</td><td><?php esc_html_e( 'Booking creation', 'wp-bnb' ); ?></td></tr>
|
||||
<tr><td><?php esc_html_e( 'Admin', 'wp-bnb' ); ?></td><td>120/min</td><td><?php esc_html_e( 'All admin endpoints', 'wp-bnb' ); ?></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<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>
|
||||
|
||||
<p class="submit">
|
||||
<?php submit_button( __( 'Save API Settings', 'wp-bnb' ), 'primary', 'submit', false ); ?>
|
||||
</p>
|
||||
<?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
|
||||
}
|
||||
@@ -1682,6 +1867,9 @@ final class Plugin {
|
||||
case 'api':
|
||||
$this->save_api_settings();
|
||||
break;
|
||||
case 'woocommerce':
|
||||
$this->save_woocommerce_settings();
|
||||
break;
|
||||
default:
|
||||
$this->save_general_settings();
|
||||
break;
|
||||
@@ -1836,10 +2024,408 @@ final class Plugin {
|
||||
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' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render WooCommerce settings tab.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function render_woocommerce_settings(): void {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Subtab switching only.
|
||||
$active_subtab = isset( $_GET['subtab'] ) ? sanitize_key( $_GET['subtab'] ) : 'general';
|
||||
$base_url = admin_url( 'admin.php?page=wp-bnb-settings&tab=woocommerce' );
|
||||
|
||||
$wc_active = class_exists( 'WooCommerce' );
|
||||
?>
|
||||
<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=products' ); ?>"
|
||||
class="wp-bnb-subtab <?php echo 'products' === $active_subtab ? 'active' : ''; ?>">
|
||||
<span class="dashicons dashicons-products"></span>
|
||||
<?php esc_html_e( 'Products', 'wp-bnb' ); ?>
|
||||
</a>
|
||||
<a href="<?php echo esc_url( $base_url . '&subtab=orders' ); ?>"
|
||||
class="wp-bnb-subtab <?php echo 'orders' === $active_subtab ? 'active' : ''; ?>">
|
||||
<span class="dashicons dashicons-cart"></span>
|
||||
<?php esc_html_e( 'Orders', 'wp-bnb' ); ?>
|
||||
</a>
|
||||
<a href="<?php echo esc_url( $base_url . '&subtab=invoices' ); ?>"
|
||||
class="wp-bnb-subtab <?php echo 'invoices' === $active_subtab ? 'active' : ''; ?>">
|
||||
<span class="dashicons dashicons-media-document"></span>
|
||||
<?php esc_html_e( 'Invoices', 'wp-bnb' ); ?>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<?php if ( ! $wc_active ) : ?>
|
||||
<div class="notice notice-warning inline">
|
||||
<p>
|
||||
<strong><?php esc_html_e( 'WooCommerce is not active.', 'wp-bnb' ); ?></strong>
|
||||
<?php esc_html_e( 'Please install and activate WooCommerce to use these features.', 'wp-bnb' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
switch ( $active_subtab ) {
|
||||
case 'products':
|
||||
$this->render_wc_products_subtab( $wc_active );
|
||||
break;
|
||||
case 'orders':
|
||||
$this->render_wc_orders_subtab( $wc_active );
|
||||
break;
|
||||
case 'invoices':
|
||||
$this->render_wc_invoices_subtab( $wc_active );
|
||||
break;
|
||||
default:
|
||||
$this->render_wc_general_subtab( $wc_active );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render WooCommerce General subtab.
|
||||
*
|
||||
* @param bool $wc_active Whether WooCommerce is active.
|
||||
* @return void
|
||||
*/
|
||||
private function render_wc_general_subtab( bool $wc_active ): void {
|
||||
?>
|
||||
<form method="post" action="">
|
||||
<?php wp_nonce_field( 'wp_bnb_save_settings', 'wp_bnb_settings_nonce' ); ?>
|
||||
<input type="hidden" name="wc_subtab" value="general">
|
||||
|
||||
<h2><?php esc_html_e( 'WooCommerce Integration', 'wp-bnb' ); ?></h2>
|
||||
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Enable Integration', 'wp-bnb' ); ?></th>
|
||||
<td>
|
||||
<label for="wp_bnb_wc_enabled">
|
||||
<input type="checkbox" name="wp_bnb_wc_enabled" id="wp_bnb_wc_enabled" value="yes"
|
||||
<?php checked( 'yes', get_option( WooCommerceManager::OPTION_ENABLED, 'no' ) ); ?>
|
||||
<?php disabled( ! $wc_active ); ?>>
|
||||
<?php esc_html_e( 'Enable WooCommerce integration for bookings', 'wp-bnb' ); ?>
|
||||
</label>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Allow guests to book and pay through WooCommerce checkout.', 'wp-bnb' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Auto-Confirm Bookings', 'wp-bnb' ); ?></th>
|
||||
<td>
|
||||
<label for="wp_bnb_wc_auto_confirm_booking">
|
||||
<input type="checkbox" name="wp_bnb_wc_auto_confirm_booking" id="wp_bnb_wc_auto_confirm_booking" value="yes"
|
||||
<?php checked( 'yes', get_option( WooCommerceManager::OPTION_AUTO_CONFIRM, 'yes' ) ); ?>
|
||||
<?php disabled( ! $wc_active ); ?>>
|
||||
<?php esc_html_e( 'Automatically confirm booking when payment is completed', 'wp-bnb' ); ?>
|
||||
</label>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'If disabled, bookings will remain pending until manually confirmed.', 'wp-bnb' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<?php if ( $wc_active ) : ?>
|
||||
<p class="submit">
|
||||
<input type="submit" name="submit" class="button button-primary"
|
||||
value="<?php esc_attr_e( 'Save Settings', 'wp-bnb' ); ?>">
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render WooCommerce Products subtab.
|
||||
*
|
||||
* @param bool $wc_active Whether WooCommerce is active.
|
||||
* @return void
|
||||
*/
|
||||
private function render_wc_products_subtab( bool $wc_active ): void {
|
||||
$categories = array();
|
||||
if ( $wc_active ) {
|
||||
$categories = get_terms(
|
||||
array(
|
||||
'taxonomy' => 'product_cat',
|
||||
'hide_empty' => false,
|
||||
)
|
||||
);
|
||||
}
|
||||
?>
|
||||
<form method="post" action="">
|
||||
<?php wp_nonce_field( 'wp_bnb_save_settings', 'wp_bnb_settings_nonce' ); ?>
|
||||
<input type="hidden" name="wc_subtab" value="products">
|
||||
|
||||
<h2><?php esc_html_e( 'Product Synchronization', 'wp-bnb' ); ?></h2>
|
||||
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Auto-Sync Products', 'wp-bnb' ); ?></th>
|
||||
<td>
|
||||
<label for="wp_bnb_wc_auto_sync_products">
|
||||
<input type="checkbox" name="wp_bnb_wc_auto_sync_products" id="wp_bnb_wc_auto_sync_products" value="yes"
|
||||
<?php checked( 'yes', get_option( WooCommerceManager::OPTION_AUTO_SYNC, 'yes' ) ); ?>
|
||||
<?php disabled( ! $wc_active ); ?>>
|
||||
<?php esc_html_e( 'Automatically create/update WooCommerce products when rooms are saved', 'wp-bnb' ); ?>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wp_bnb_wc_product_category"><?php esc_html_e( 'Product Category', 'wp-bnb' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<select name="wp_bnb_wc_product_category" id="wp_bnb_wc_product_category" <?php disabled( ! $wc_active ); ?>>
|
||||
<option value=""><?php esc_html_e( '— No category —', 'wp-bnb' ); ?></option>
|
||||
<?php foreach ( $categories as $cat ) : ?>
|
||||
<option value="<?php echo esc_attr( $cat->term_id ); ?>"
|
||||
<?php selected( get_option( WooCommerceManager::OPTION_PRODUCT_CATEGORY, 0 ), $cat->term_id ); ?>>
|
||||
<?php echo esc_html( $cat->name ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Assign synced room products to this WooCommerce category.', 'wp-bnb' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<?php if ( $wc_active ) : ?>
|
||||
<h3><?php esc_html_e( 'Manual Sync', 'wp-bnb' ); ?></h3>
|
||||
<p>
|
||||
<button type="button" class="button bnb-sync-rooms-btn">
|
||||
<span class="dashicons dashicons-update"></span>
|
||||
<?php esc_html_e( 'Sync All Rooms Now', 'wp-bnb' ); ?>
|
||||
</button>
|
||||
<span class="sync-status"></span>
|
||||
</p>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Creates or updates WooCommerce products for all published rooms.', 'wp-bnb' ); ?>
|
||||
</p>
|
||||
|
||||
<p class="submit">
|
||||
<input type="submit" name="submit" class="button button-primary"
|
||||
value="<?php esc_attr_e( 'Save Settings', 'wp-bnb' ); ?>">
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render WooCommerce Orders subtab.
|
||||
*
|
||||
* @param bool $wc_active Whether WooCommerce is active.
|
||||
* @return void
|
||||
*/
|
||||
private function render_wc_orders_subtab( bool $wc_active ): void {
|
||||
?>
|
||||
<h2><?php esc_html_e( 'Order-Booking Status Mapping', 'wp-bnb' ); ?></h2>
|
||||
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'The following table shows how WooCommerce order statuses map to booking statuses.', 'wp-bnb' ); ?>
|
||||
</p>
|
||||
|
||||
<table class="widefat fixed striped" style="max-width: 600px; margin-top: 15px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'WooCommerce Status', 'wp-bnb' ); ?></th>
|
||||
<th><?php esc_html_e( 'Booking Status', 'wp-bnb' ); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><?php esc_html_e( 'Pending Payment', 'wp-bnb' ); ?></td>
|
||||
<td><span class="bnb-status-badge bnb-status-pending"><?php esc_html_e( 'Pending', 'wp-bnb' ); ?></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><?php esc_html_e( 'On Hold', 'wp-bnb' ); ?></td>
|
||||
<td><span class="bnb-status-badge bnb-status-pending"><?php esc_html_e( 'Pending', 'wp-bnb' ); ?></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><?php esc_html_e( 'Processing', 'wp-bnb' ); ?></td>
|
||||
<td>
|
||||
<?php if ( get_option( WooCommerceManager::OPTION_AUTO_CONFIRM, 'yes' ) === 'yes' ) : ?>
|
||||
<span class="bnb-status-badge bnb-status-confirmed"><?php esc_html_e( 'Confirmed', 'wp-bnb' ); ?></span>
|
||||
<?php else : ?>
|
||||
<span class="bnb-status-badge bnb-status-pending"><?php esc_html_e( 'Pending', 'wp-bnb' ); ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><?php esc_html_e( 'Completed', 'wp-bnb' ); ?></td>
|
||||
<td><span class="bnb-status-badge bnb-status-confirmed"><?php esc_html_e( 'Confirmed', 'wp-bnb' ); ?></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><?php esc_html_e( 'Cancelled / Refunded / Failed', 'wp-bnb' ); ?></td>
|
||||
<td><span class="bnb-status-badge bnb-status-cancelled"><?php esc_html_e( 'Cancelled', 'wp-bnb' ); ?></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3><?php esc_html_e( 'Refund Handling', 'wp-bnb' ); ?></h3>
|
||||
<ul style="list-style: disc; margin-left: 20px;">
|
||||
<li><?php esc_html_e( 'Full refunds automatically cancel the linked booking.', 'wp-bnb' ); ?></li>
|
||||
<li><?php esc_html_e( 'Partial refunds are recorded but do not cancel the booking.', 'wp-bnb' ); ?></li>
|
||||
<li><?php esc_html_e( 'Cancellation emails are sent when a booking is cancelled due to refund.', 'wp-bnb' ); ?></li>
|
||||
</ul>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render WooCommerce Invoices subtab.
|
||||
*
|
||||
* @param bool $wc_active Whether WooCommerce is active.
|
||||
* @return void
|
||||
*/
|
||||
private function render_wc_invoices_subtab( bool $wc_active ): void {
|
||||
?>
|
||||
<form method="post" action="">
|
||||
<?php wp_nonce_field( 'wp_bnb_save_settings', 'wp_bnb_settings_nonce' ); ?>
|
||||
<input type="hidden" name="wc_subtab" value="invoices">
|
||||
|
||||
<h2><?php esc_html_e( 'Invoice Settings', 'wp-bnb' ); ?></h2>
|
||||
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Attach to Emails', 'wp-bnb' ); ?></th>
|
||||
<td>
|
||||
<label for="wp_bnb_wc_invoice_attach">
|
||||
<input type="checkbox" name="wp_bnb_wc_invoice_attach" id="wp_bnb_wc_invoice_attach" value="yes"
|
||||
<?php checked( 'yes', get_option( WooCommerceManager::OPTION_INVOICE_ATTACH, 'yes' ) ); ?>
|
||||
<?php disabled( ! $wc_active ); ?>>
|
||||
<?php esc_html_e( 'Attach PDF invoice to order confirmation emails', 'wp-bnb' ); ?>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wp_bnb_invoice_prefix"><?php esc_html_e( 'Invoice Number Prefix', 'wp-bnb' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" name="wp_bnb_invoice_prefix" id="wp_bnb_invoice_prefix"
|
||||
value="<?php echo esc_attr( get_option( WooCommerceManager::OPTION_INVOICE_PREFIX, 'INV-' ) ); ?>"
|
||||
class="regular-text" <?php disabled( ! $wc_active ); ?>>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Prefix for invoice numbers (e.g., INV- for INV-00001).', 'wp-bnb' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wp_bnb_invoice_start_number"><?php esc_html_e( 'Starting Number', 'wp-bnb' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" name="wp_bnb_invoice_start_number" id="wp_bnb_invoice_start_number"
|
||||
value="<?php echo esc_attr( get_option( WooCommerceManager::OPTION_INVOICE_START_NUMBER, 1000 ) ); ?>"
|
||||
class="small-text" min="1" <?php disabled( ! $wc_active ); ?>>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Starting number for new invoices (only applies if no invoices generated yet).', 'wp-bnb' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wp_bnb_invoice_footer"><?php esc_html_e( 'Footer Text', 'wp-bnb' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<textarea name="wp_bnb_invoice_footer" id="wp_bnb_invoice_footer" rows="3" class="large-text"
|
||||
<?php disabled( ! $wc_active ); ?>><?php echo esc_textarea( get_option( WooCommerceManager::OPTION_INVOICE_FOOTER, '' ) ); ?></textarea>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Custom footer text for invoices (e.g., terms and conditions).', 'wp-bnb' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<?php if ( $wc_active ) : ?>
|
||||
<p class="submit">
|
||||
<input type="submit" name="submit" class="button button-primary"
|
||||
value="<?php esc_attr_e( 'Save Settings', 'wp-bnb' ); ?>">
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Save WooCommerce settings.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function save_woocommerce_settings(): void {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in render_settings_page.
|
||||
$subtab = isset( $_POST['wc_subtab'] ) ? sanitize_key( $_POST['wc_subtab'] ) : 'general';
|
||||
|
||||
switch ( $subtab ) {
|
||||
case 'products':
|
||||
$auto_sync = isset( $_POST['wp_bnb_wc_auto_sync_products'] ) ? 'yes' : 'no';
|
||||
$category = isset( $_POST['wp_bnb_wc_product_category'] ) ? absint( $_POST['wp_bnb_wc_product_category'] ) : 0;
|
||||
|
||||
update_option( WooCommerceManager::OPTION_AUTO_SYNC, $auto_sync );
|
||||
update_option( WooCommerceManager::OPTION_PRODUCT_CATEGORY, $category );
|
||||
break;
|
||||
|
||||
case 'invoices':
|
||||
$attach = isset( $_POST['wp_bnb_wc_invoice_attach'] ) ? 'yes' : 'no';
|
||||
$prefix = isset( $_POST['wp_bnb_invoice_prefix'] ) ? sanitize_text_field( wp_unslash( $_POST['wp_bnb_invoice_prefix'] ) ) : 'INV-';
|
||||
$start_number = isset( $_POST['wp_bnb_invoice_start_number'] ) ? absint( $_POST['wp_bnb_invoice_start_number'] ) : 1000;
|
||||
$footer = isset( $_POST['wp_bnb_invoice_footer'] ) ? sanitize_textarea_field( wp_unslash( $_POST['wp_bnb_invoice_footer'] ) ) : '';
|
||||
|
||||
update_option( WooCommerceManager::OPTION_INVOICE_ATTACH, $attach );
|
||||
update_option( WooCommerceManager::OPTION_INVOICE_PREFIX, $prefix );
|
||||
update_option( WooCommerceManager::OPTION_INVOICE_START_NUMBER, $start_number );
|
||||
update_option( WooCommerceManager::OPTION_INVOICE_FOOTER, $footer );
|
||||
break;
|
||||
|
||||
default: // general
|
||||
$enabled = isset( $_POST['wp_bnb_wc_enabled'] ) ? 'yes' : 'no';
|
||||
$auto_confirm = isset( $_POST['wp_bnb_wc_auto_confirm_booking'] ) ? 'yes' : 'no';
|
||||
|
||||
update_option( WooCommerceManager::OPTION_ENABLED, $enabled );
|
||||
update_option( WooCommerceManager::OPTION_AUTO_CONFIRM, $auto_confirm );
|
||||
break;
|
||||
}
|
||||
|
||||
add_settings_error( 'wp_bnb_settings', 'settings_saved', __( 'WooCommerce settings saved.', 'wp-bnb' ), 'success' );
|
||||
settings_errors( 'wp_bnb_settings' );
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler for checking room availability.
|
||||
*
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Plugin Name: WP BnB Management
|
||||
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wp-bnb
|
||||
* Description: A comprehensive Bed & Breakfast management system for WordPress. Manage buildings, rooms, bookings, and guests.
|
||||
* Version: 0.10.0
|
||||
* Version: 0.11.3
|
||||
* Requires at least: 6.0
|
||||
* Requires PHP: 8.3
|
||||
* Author: Marco Graetsch
|
||||
@@ -24,7 +24,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
||||
}
|
||||
|
||||
// Plugin version constant - MUST match Version in header above.
|
||||
define( 'WP_BNB_VERSION', '0.10.0' );
|
||||
define( 'WP_BNB_VERSION', '0.11.3' );
|
||||
|
||||
// Plugin path constants.
|
||||
define( 'WP_BNB_PATH', plugin_dir_path( __FILE__ ) );
|
||||
|
||||
Reference in New Issue
Block a user