diff --git a/CHANGELOG.md b/CHANGELOG.md
index f2c4ebd..c1d72d4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,76 @@ 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.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
@@ -634,6 +704,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
diff --git a/PLAN.md b/PLAN.md
index 58bf494..07424dd 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -204,12 +204,12 @@ 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: WooCommerce Integration (v0.11.0)
+### Phase 11: WooCommerce Integration (v0.11.0) - Complete
-- [ ] Payment processing
-- [ ] Invoice generation
-- [ ] Order management
-- [ ] Refund handling
+- [x] Payment processing
+- [x] Invoice generation
+- [x] Order management
+- [x] Refund handling
## Phase 12: Security Audit (v0.12.0)
@@ -359,6 +359,6 @@ The plugin will provide extensive hooks for customization:
| 0.8.0 | Dashboard | Complete |
| 0.9.0 | Prometheus Metrics | Complete |
| 0.10.0 | API Endpoints | Complete |
-| 0.11.0 | WooCommerce Integration | TBD |
+| 0.11.0 | WooCommerce Integration | Complete |
| 0.12.0 | Security Audit | TBD |
| 1.0.0 | Stable Release | TBD |
diff --git a/assets/css/admin.css b/assets/css/admin.css
index 49c9829..a2593f5 100644
--- a/assets/css/admin.css
+++ b/assets/css/admin.css
@@ -598,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;
diff --git a/assets/css/wc-integration.css b/assets/css/wc-integration.css
new file mode 100644
index 0000000..0cbcac7
--- /dev/null
+++ b/assets/css/wc-integration.css
@@ -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;
+ }
+}
diff --git a/assets/js/wc-integration.js b/assets/js/wc-integration.js
new file mode 100644
index 0000000..783dcfe
--- /dev/null
+++ b/assets/js/wc-integration.js
@@ -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('' + response.data.message + '');
+ } else {
+ $status.html('' + (response.data?.message || 'Error') + '');
+ }
+ },
+ error: function () {
+ $status.html('' + (WpBnbWC.settings.i18n?.error || 'Error occurred') + '');
+ },
+ 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 = '';
+ break;
+ case 'available':
+ icon = '';
+ break;
+ case 'unavailable':
+ icon = '';
+ 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 = '
' + (WpBnbWC.settings.i18n?.roomTotal || 'Room') + '' + WpBnbWC.formatPrice(roomPrice) + '
';
+
+ if (servicesTotal > 0) {
+ html += '' + (WpBnbWC.settings.i18n?.services || 'Services') + '' + WpBnbWC.formatPrice(servicesTotal) + '
';
+ }
+
+ html += '' + (WpBnbWC.settings.i18n?.total || 'Total') + '' + WpBnbWC.formatPrice(grandTotal) + '
';
+
+ $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);
diff --git a/src/Integration/WooCommerce/AdminColumns.php b/src/Integration/WooCommerce/AdminColumns.php
new file mode 100644
index 0000000..e758bec
--- /dev/null
+++ b/src/Integration/WooCommerce/AdminColumns.php
@@ -0,0 +1,282 @@
+ $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 '–';
+ return;
+ }
+
+ $order_number = $order->get_order_number();
+ $order_status = $order->get_status();
+ $edit_url = $order->get_edit_order_url();
+
+ printf(
+ '#%s',
+ 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(
+ '
%s',
+ 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 '–';
+ 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 '–';
+ 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 '–';
+ 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(
+ '%s',
+ 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(
+ '
%s - %s',
+ esc_html( $check_in_date->format( 'd.m' ) ),
+ esc_html( $check_out_date->format( 'd.m.y' ) )
+ );
+ }
+ }
+
+ // Status badge.
+ if ( $status ) {
+ printf(
+ '
%s',
+ 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(
+ '%s',
+ 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;
+ }
+}
diff --git a/src/Integration/WooCommerce/CartHandler.php b/src/Integration/WooCommerce/CartHandler.php
new file mode 100644
index 0000000..eef62e3
--- /dev/null
+++ b/src/Integration/WooCommerce/CartHandler.php
@@ -0,0 +1,545 @@
+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;
+ }
+}
diff --git a/src/Integration/WooCommerce/CheckoutHandler.php b/src/Integration/WooCommerce/CheckoutHandler.php
new file mode 100644
index 0000000..b8fce0b
--- /dev/null
+++ b/src/Integration/WooCommerce/CheckoutHandler.php
@@ -0,0 +1,347 @@
+ '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;
+ }
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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;
+ }
+}
diff --git a/src/Integration/WooCommerce/InvoiceGenerator.php b/src/Integration/WooCommerce/InvoiceGenerator.php
new file mode 100644
index 0000000..ad839c5
--- /dev/null
+++ b/src/Integration/WooCommerce/InvoiceGenerator.php
@@ -0,0 +1,633 @@
+ '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, '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' )
+ );
+
+ ?>
+
+
+
+ __( '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 = '
';
+ }
+ }
+
+ // Get footer.
+ $footer_text = Manager::get_invoice_footer();
+
+ // Build HTML.
+ $html = '';
+
+ // Header.
+ $html .= '';
+
+ // Addresses.
+ $html .= '';
+
+ // From.
+ $html .= '
';
+ $html .= '
' . esc_html__( 'From', 'wp-bnb' ) . '
';
+ $html .= '
' . esc_html( $business['name'] ) . '
';
+ if ( $business['street'] ) {
+ $html .= '
' . esc_html( $business['street'] ) . '
';
+ }
+ if ( $business['city'] || $business['postal'] ) {
+ $html .= '
' . esc_html( $business['postal'] . ' ' . $business['city'] ) . '
';
+ }
+ if ( $business['country'] ) {
+ $html .= '
' . esc_html( $business['country'] ) . '
';
+ }
+ if ( $business['email'] ) {
+ $html .= '
' . esc_html( $business['email'] ) . '
';
+ }
+ if ( $business['phone'] ) {
+ $html .= '
' . esc_html( $business['phone'] ) . '
';
+ }
+ $html .= '
';
+
+ // To.
+ $html .= '
';
+ $html .= '
' . esc_html__( 'Bill To', 'wp-bnb' ) . '
';
+ $html .= '
' . esc_html( $order->get_billing_first_name() . ' ' . $order->get_billing_last_name() ) . '
';
+ if ( $order->get_billing_address_1() ) {
+ $html .= '
' . esc_html( $order->get_billing_address_1() ) . '
';
+ }
+ if ( $order->get_billing_city() || $order->get_billing_postcode() ) {
+ $html .= '
' . esc_html( $order->get_billing_postcode() . ' ' . $order->get_billing_city() ) . '
';
+ }
+ if ( $order->get_billing_country() ) {
+ $html .= '
' . esc_html( WC()->countries->countries[ $order->get_billing_country() ] ?? $order->get_billing_country() ) . '
';
+ }
+ if ( $order->get_billing_email() ) {
+ $html .= '
' . esc_html( $order->get_billing_email() ) . '
';
+ }
+ $html .= '
';
+ $html .= '
';
+
+ // Booking details.
+ if ( $booking_id && $room_name ) {
+ $html .= '';
+ $html .= '
' . esc_html__( 'Booking Details', 'wp-bnb' ) . '
';
+ $html .= '
';
+ $html .= '| ' . esc_html__( 'Room', 'wp-bnb' ) . ' | ' . esc_html( $room_name );
+ if ( $building_name ) {
+ $html .= ' (' . esc_html( $building_name ) . ')';
+ }
+ $html .= ' |
';
+ $html .= '| ' . esc_html__( 'Check-in', 'wp-bnb' ) . ' | ' . esc_html( $check_in_display ) . ' |
';
+ $html .= '| ' . esc_html__( 'Check-out', 'wp-bnb' ) . ' | ' . esc_html( $check_out_display ) . ' |
';
+ $html .= '| ' . esc_html__( 'Nights', 'wp-bnb' ) . ' | ' . esc_html( $nights ) . ' |
';
+ $html .= '| ' . esc_html__( 'Guests', 'wp-bnb' ) . ' | ' . esc_html( $guests ) . ' |
';
+ $html .= '
';
+ $html .= '
';
+ }
+
+ // Line items.
+ $html .= '';
+ $html .= '
';
+ $html .= '';
+ $html .= '| ' . esc_html__( 'Description', 'wp-bnb' ) . ' | ';
+ $html .= '' . esc_html__( 'Qty', 'wp-bnb' ) . ' | ';
+ $html .= '' . esc_html__( 'Price', 'wp-bnb' ) . ' | ';
+ $html .= '' . esc_html__( 'Total', 'wp-bnb' ) . ' | ';
+ $html .= '
';
+ $html .= '';
+
+ foreach ( $order->get_items() as $item ) {
+ $qty = $item->get_quantity();
+ $total = $item->get_total();
+ $price = $qty > 0 ? $total / $qty : $total;
+
+ $html .= '';
+ $html .= '| ' . esc_html( $item->get_name() ) . ' | ';
+ $html .= '' . esc_html( $qty ) . ' | ';
+ $html .= '' . wc_price( $price ) . ' | ';
+ $html .= '' . wc_price( $total ) . ' | ';
+ $html .= '
';
+ }
+
+ $html .= '';
+ $html .= '
';
+ $html .= '
';
+
+ // Totals.
+ $html .= '';
+ $html .= '
';
+ $html .= '| ' . esc_html__( 'Subtotal', 'wp-bnb' ) . ' | ' . wc_price( $order->get_subtotal() ) . ' |
';
+
+ if ( $order->get_total_tax() > 0 ) {
+ $html .= '| ' . esc_html__( 'Tax', 'wp-bnb' ) . ' | ' . wc_price( $order->get_total_tax() ) . ' |
';
+ }
+
+ $html .= '| ' . esc_html__( 'Total', 'wp-bnb' ) . ' | ' . wc_price( $order->get_total() ) . ' |
';
+ $html .= '
';
+ $html .= '
';
+
+ // Payment info.
+ $html .= '';
+ $html .= '
' . esc_html__( 'Payment Status:', 'wp-bnb' ) . ' ';
+
+ if ( $order->is_paid() ) {
+ $html .= '' . esc_html__( 'PAID', 'wp-bnb' ) . '';
+ } else {
+ $html .= '' . esc_html__( 'PENDING', 'wp-bnb' ) . '';
+ }
+
+ $html .= '
';
+ $html .= '
' . esc_html__( 'Payment Method:', 'wp-bnb' ) . ' ' . esc_html( $order->get_payment_method_title() ) . '
';
+ $html .= '
';
+
+ // Footer.
+ $html .= '';
+
+ $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', '' ),
+ );
+ }
+}
diff --git a/src/Integration/WooCommerce/Manager.php b/src/Integration/WooCommerce/Manager.php
new file mode 100644
index 0000000..e68fc2d
--- /dev/null
+++ b/src/Integration/WooCommerce/Manager.php
@@ -0,0 +1,435 @@
+ 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() ) {
+ ?>
+
+ '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();
+ }
+}
diff --git a/src/Integration/WooCommerce/OrderHandler.php b/src/Integration/WooCommerce/OrderHandler.php
new file mode 100644
index 0000000..39ab9b3
--- /dev/null
+++ b/src/Integration/WooCommerce/OrderHandler.php
@@ -0,0 +1,584 @@
+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;
+ ?>
+
+
+
+
+
+
+
+ |
+ |
+
+
+
+ |
+
+ post_title ); ?>
+
+ post_title ); ?>
+
+ |
+
+
+
+ |
+ format( $date_format ) : $check_in ); ?> |
+
+
+ |
+ format( $date_format ) : $check_out ); ?> |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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}
+ */
+ 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;
+ }
+
+ ?>
+
+ get_meta( Manager::PRODUCT_ROOM_META, true );
+
+ return ! empty( $room_id );
+ }
+}
diff --git a/src/Integration/WooCommerce/RefundHandler.php b/src/Integration/WooCommerce/RefundHandler.php
new file mode 100644
index 0000000..9541f66
--- /dev/null
+++ b/src/Integration/WooCommerce/RefundHandler.php
@@ -0,0 +1,394 @@
+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 );
+ ?>
+
+ | : |
+ |
+
+
+
+
+ |
+
+
+
+ |
+
+ ' . esc_html__( 'View booking', 'wp-bnb' ) . ''
+ );
+ ?>
+
+ |
+
+
+ init_rest_api();
@@ -636,6 +642,10 @@ final class Plugin {
class="nav-tab ">
+
+
+
@@ -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;
@@ -1854,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;
@@ -2037,6 +2053,379 @@ final class Plugin {
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' );
+ ?>
+
+
+
+
+
+
+ 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 {
+ ?>
+
+ 'product_cat',
+ 'hide_empty' => false,
+ )
+ );
+ }
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+ |
+ |
+
+
+
+
+ |
+ |
+
+
+ |
+ |
+
+
+ |
+
+
+
+
+
+
+ |
+
+
+ |
+ |
+
+
+ |
+ |
+
+
+
+
+
+
+
+
+