Add WooCommerce integration for payments, invoices, and order management (v0.11.0)
All checks were successful
Create Release Package / build-release (push) Successful in 1m11s
All checks were successful
Create Release Package / build-release (push) Successful in 1m11s
- Product sync: Virtual WC products for rooms with bidirectional linking - Cart/Checkout: Booking data in cart items, availability validation, dynamic pricing - Orders: Automatic booking creation on payment, status mapping, guest record creation - Invoices: PDF generation via mPDF, auto-attach to emails, configurable numbering - Refunds: Full refund cancels booking, partial refund records amount only - Admin: Cross-linked columns and row actions between bookings and orders - Settings: WooCommerce tab with subtabs (General, Products, Orders, Invoices) - HPOS compatibility declared for High-Performance Order Storage Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
78
CHANGELOG.md
78
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/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.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
|
## [0.10.1] - 2026-02-03
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -634,6 +704,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Input sanitization and output escaping
|
- Input sanitization and output escaping
|
||||||
- Server secret masking in license settings
|
- 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.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.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
|
[0.5.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.5.0
|
||||||
|
|||||||
12
PLAN.md
12
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] Transient-based rate limiting with tiered limits
|
||||||
- [x] API settings tab with enable/disable toggles
|
- [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
|
- [x] Payment processing
|
||||||
- [ ] Invoice generation
|
- [x] Invoice generation
|
||||||
- [ ] Order management
|
- [x] Order management
|
||||||
- [ ] Refund handling
|
- [x] Refund handling
|
||||||
|
|
||||||
## Phase 12: Security Audit (v0.12.0)
|
## 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.8.0 | Dashboard | Complete |
|
||||||
| 0.9.0 | Prometheus Metrics | Complete |
|
| 0.9.0 | Prometheus Metrics | Complete |
|
||||||
| 0.10.0 | API Endpoints | 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 |
|
| 0.12.0 | Security Audit | TBD |
|
||||||
| 1.0.0 | Stable Release | TBD |
|
| 1.0.0 | Stable Release | TBD |
|
||||||
|
|||||||
@@ -598,12 +598,36 @@
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 3px 8px;
|
padding: 3px 8px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
color: #fff;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
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 */
|
/* Room Details Meta Box */
|
||||||
#bnb_room_details .form-table td label {
|
#bnb_room_details .form-table td label {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|||||||
443
assets/css/wc-integration.css
Normal file
443
assets/css/wc-integration.css
Normal file
@@ -0,0 +1,443 @@
|
|||||||
|
/**
|
||||||
|
* WooCommerce Integration Styles
|
||||||
|
*
|
||||||
|
* Styles for WP BnB - WooCommerce integration
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Cart Item - Booking Data Display
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.woocommerce-cart .cart_item .bnb-booking-info {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-cart .cart_item .bnb-booking-info dt {
|
||||||
|
display: inline-block;
|
||||||
|
font-weight: 600;
|
||||||
|
min-width: 80px;
|
||||||
|
color: #50575e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-cart .cart_item .bnb-booking-info dd {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Checkout - Booking Summary
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.bnb-checkout-booking-summary {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 15px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #e1e4e8;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-checkout-booking-summary h3 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
padding: 0 0 10px 0;
|
||||||
|
border-bottom: 1px solid #e1e4e8;
|
||||||
|
font-size: 1.1em;
|
||||||
|
color: #2271b1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-booking-item {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px dashed #e1e4e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-booking-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-booking-item strong {
|
||||||
|
display: block;
|
||||||
|
font-size: 1em;
|
||||||
|
color: #1d2327;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-building-name {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: #787c82;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-booking-details {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #50575e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-booking-details span {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-booking-details span::before {
|
||||||
|
content: "•";
|
||||||
|
margin-right: 5px;
|
||||||
|
color: #c3c4c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-booking-details span:first-child::before {
|
||||||
|
content: "";
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Thank You Page - Booking Confirmation
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.woocommerce-booking-confirmation {
|
||||||
|
margin: 30px 0;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f0f8f1;
|
||||||
|
border: 1px solid #d1e7d7;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-booking-confirmation h2 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
color: #00a32a;
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-table--booking-details {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-table--booking-details th {
|
||||||
|
text-align: left;
|
||||||
|
width: 40%;
|
||||||
|
padding: 8px 12px 8px 0;
|
||||||
|
color: #50575e;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-table--booking-details td {
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-table--booking-details small {
|
||||||
|
display: block;
|
||||||
|
color: #787c82;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-booking-reference {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 10px 15px;
|
||||||
|
background: #f6f7f7;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-booking-reference .bnb-status-badge {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Status Badges
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.bnb-status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-status-pending {
|
||||||
|
background: #fff8e5;
|
||||||
|
color: #9d6a00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-status-confirmed {
|
||||||
|
background: #e6f4ea;
|
||||||
|
color: #0a6e31;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-status-checked_in {
|
||||||
|
background: #e3f2fd;
|
||||||
|
color: #1565c0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-status-checked_out {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #616161;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-status-cancelled {
|
||||||
|
background: #ffeaea;
|
||||||
|
color: #d63638;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Booking Form (Frontend)
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.bnb-wc-booking-form {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 20px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e1e4e8;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-wc-booking-form h3 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
padding: 0 0 15px 0;
|
||||||
|
border-bottom: 1px solid #e1e4e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-wc-booking-form .form-row {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-wc-booking-form label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-wc-booking-form input[type="date"],
|
||||||
|
.bnb-wc-booking-form input[type="number"],
|
||||||
|
.bnb-wc-booking-form select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-wc-booking-form input[type="date"]:focus,
|
||||||
|
.bnb-wc-booking-form input[type="number"]:focus,
|
||||||
|
.bnb-wc-booking-form select:focus {
|
||||||
|
border-color: #2271b1;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 1px #2271b1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-wc-booking-form .form-row-inline {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-wc-booking-form .form-row-inline > div {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Availability status */
|
||||||
|
.bnb-availability-status {
|
||||||
|
margin: 15px 0;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-availability-status.checking {
|
||||||
|
background: #f6f7f7;
|
||||||
|
color: #787c82;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-availability-status.available {
|
||||||
|
background: #e6f4ea;
|
||||||
|
color: #0a6e31;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-availability-status.unavailable {
|
||||||
|
background: #ffeaea;
|
||||||
|
color: #d63638;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-availability-status .dashicons {
|
||||||
|
font-size: 18px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Price display */
|
||||||
|
.bnb-wc-price-display {
|
||||||
|
margin: 15px 0;
|
||||||
|
padding: 15px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-wc-price-display .price-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-wc-price-display .price-row.total {
|
||||||
|
border-top: 1px solid #e1e4e8;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Services selection */
|
||||||
|
.bnb-wc-services {
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-wc-services h4 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-wc-service-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-wc-service-item:hover {
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-wc-service-item.selected {
|
||||||
|
background: #e6f4ea;
|
||||||
|
border: 1px solid #d1e7d7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-wc-service-item input[type="checkbox"] {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-wc-service-item .service-name {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-wc-service-item .service-price {
|
||||||
|
color: #2271b1;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-wc-service-item .service-qty {
|
||||||
|
margin-left: 10px;
|
||||||
|
width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add to cart button */
|
||||||
|
.bnb-wc-booking-form .add-to-cart-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 20px;
|
||||||
|
background: #2271b1;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-wc-booking-form .add-to-cart-btn:hover {
|
||||||
|
background: #135e96;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-wc-booking-form .add-to-cart-btn:disabled {
|
||||||
|
background: #c3c4c7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Admin Order - Booking Info
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.bnb-order-booking-info {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: #f6f7f7;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-order-booking-info h3 {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #1d2327;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-order-booking-info p {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-order-booking-info a {
|
||||||
|
color: #2271b1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-order-booking-info a:hover {
|
||||||
|
color: #135e96;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Admin List Table - Order Columns
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.column-wc_order {
|
||||||
|
width: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-bnb_booking {
|
||||||
|
width: 15%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-bnb_booking small {
|
||||||
|
display: block;
|
||||||
|
color: #787c82;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Order action buttons */
|
||||||
|
.wc-action-button-view_booking::after {
|
||||||
|
font-family: dashicons;
|
||||||
|
content: "\f513";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Responsive
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.bnb-wc-booking-form .form-row-inline {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-booking-details span {
|
||||||
|
display: block;
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bnb-booking-details span::before {
|
||||||
|
content: "";
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
358
assets/js/wc-integration.js
Normal file
358
assets/js/wc-integration.js
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
/**
|
||||||
|
* WooCommerce Integration Scripts
|
||||||
|
*
|
||||||
|
* Handles booking form interactions for WooCommerce integration.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function ($) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WP BnB WooCommerce Integration
|
||||||
|
*/
|
||||||
|
const WpBnbWC = {
|
||||||
|
/**
|
||||||
|
* Settings from localization
|
||||||
|
*/
|
||||||
|
settings: {},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize
|
||||||
|
*/
|
||||||
|
init: function () {
|
||||||
|
this.settings = window.wpBnbWC || {};
|
||||||
|
this.bindEvents();
|
||||||
|
this.initBookingForms();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind global events
|
||||||
|
*/
|
||||||
|
bindEvents: function () {
|
||||||
|
// Admin: Sync all rooms button
|
||||||
|
$(document).on('click', '.bnb-sync-rooms-btn', this.handleSyncRooms.bind(this));
|
||||||
|
|
||||||
|
// Admin: Generate invoice button
|
||||||
|
$(document).on('click', '.bnb-generate-invoice-btn', this.handleGenerateInvoice.bind(this));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize booking forms
|
||||||
|
*/
|
||||||
|
initBookingForms: function () {
|
||||||
|
$('.bnb-wc-booking-form').each(function () {
|
||||||
|
new BookingForm($(this));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle sync all rooms button
|
||||||
|
*/
|
||||||
|
handleSyncRooms: function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const $btn = $(e.currentTarget);
|
||||||
|
const $status = $btn.siblings('.sync-status');
|
||||||
|
|
||||||
|
$btn.prop('disabled', true).addClass('updating');
|
||||||
|
$status.text(this.settings.i18n?.syncing || 'Syncing...');
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: this.settings.ajaxUrl,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'wp_bnb_sync_all_rooms',
|
||||||
|
nonce: this.settings.nonce
|
||||||
|
},
|
||||||
|
success: function (response) {
|
||||||
|
if (response.success) {
|
||||||
|
$status.html('<span class="success">' + response.data.message + '</span>');
|
||||||
|
} else {
|
||||||
|
$status.html('<span class="error">' + (response.data?.message || 'Error') + '</span>');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function () {
|
||||||
|
$status.html('<span class="error">' + (WpBnbWC.settings.i18n?.error || 'Error occurred') + '</span>');
|
||||||
|
},
|
||||||
|
complete: function () {
|
||||||
|
$btn.prop('disabled', false).removeClass('updating');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle generate invoice button
|
||||||
|
*/
|
||||||
|
handleGenerateInvoice: function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const $btn = $(e.currentTarget);
|
||||||
|
const orderId = $btn.data('order-id');
|
||||||
|
|
||||||
|
$btn.prop('disabled', true).addClass('updating');
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: this.settings.ajaxUrl,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'wp_bnb_generate_invoice',
|
||||||
|
nonce: this.settings.nonce,
|
||||||
|
order_id: orderId
|
||||||
|
},
|
||||||
|
success: function (response) {
|
||||||
|
if (response.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert(response.data?.message || 'Error generating invoice');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function () {
|
||||||
|
alert(WpBnbWC.settings.i18n?.error || 'Error occurred');
|
||||||
|
},
|
||||||
|
complete: function () {
|
||||||
|
$btn.prop('disabled', false).removeClass('updating');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Booking Form Handler
|
||||||
|
*/
|
||||||
|
class BookingForm {
|
||||||
|
constructor($form) {
|
||||||
|
this.$form = $form;
|
||||||
|
this.roomId = $form.data('room-id');
|
||||||
|
this.productId = $form.data('product-id');
|
||||||
|
this.checkAvailabilityTimeout = null;
|
||||||
|
|
||||||
|
this.bindEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents() {
|
||||||
|
this.$form.on('change', '.bnb-date-input', this.onDateChange.bind(this));
|
||||||
|
this.$form.on('change', '.bnb-guests-input', this.onGuestsChange.bind(this));
|
||||||
|
this.$form.on('change', '.bnb-service-checkbox', this.onServiceChange.bind(this));
|
||||||
|
this.$form.on('change', '.bnb-service-qty', this.onServiceQtyChange.bind(this));
|
||||||
|
this.$form.on('submit', this.onSubmit.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
onDateChange() {
|
||||||
|
const checkIn = this.$form.find('[name="bnb_check_in"]').val();
|
||||||
|
const checkOut = this.$form.find('[name="bnb_check_out"]').val();
|
||||||
|
|
||||||
|
// Validate dates
|
||||||
|
if (!checkIn || !checkOut) {
|
||||||
|
this.updateAvailabilityStatus('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkInDate = new Date(checkIn);
|
||||||
|
const checkOutDate = new Date(checkOut);
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
if (checkInDate < today) {
|
||||||
|
this.updateAvailabilityStatus('unavailable', WpBnbWC.settings.i18n?.pastDate || 'Check-in cannot be in the past');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkOutDate <= checkInDate) {
|
||||||
|
this.updateAvailabilityStatus('unavailable', WpBnbWC.settings.i18n?.invalidDates || 'Check-out must be after check-in');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check availability
|
||||||
|
this.checkAvailability(checkIn, checkOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
onGuestsChange() {
|
||||||
|
// Re-validate if needed
|
||||||
|
const checkIn = this.$form.find('[name="bnb_check_in"]').val();
|
||||||
|
const checkOut = this.$form.find('[name="bnb_check_out"]').val();
|
||||||
|
|
||||||
|
if (checkIn && checkOut) {
|
||||||
|
this.checkAvailability(checkIn, checkOut);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onServiceChange(e) {
|
||||||
|
const $checkbox = $(e.currentTarget);
|
||||||
|
const $item = $checkbox.closest('.bnb-wc-service-item');
|
||||||
|
const $qtyInput = $item.find('.bnb-service-qty');
|
||||||
|
|
||||||
|
if ($checkbox.is(':checked')) {
|
||||||
|
$item.addClass('selected');
|
||||||
|
$qtyInput.prop('disabled', false);
|
||||||
|
} else {
|
||||||
|
$item.removeClass('selected');
|
||||||
|
$qtyInput.prop('disabled', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updatePriceDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
onServiceQtyChange() {
|
||||||
|
this.updatePriceDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
checkAvailability(checkIn, checkOut) {
|
||||||
|
// Debounce
|
||||||
|
clearTimeout(this.checkAvailabilityTimeout);
|
||||||
|
|
||||||
|
this.updateAvailabilityStatus('checking', WpBnbWC.settings.i18n?.checking || 'Checking availability...');
|
||||||
|
|
||||||
|
this.checkAvailabilityTimeout = setTimeout(() => {
|
||||||
|
const guests = this.$form.find('[name="bnb_guests"]').val() || 1;
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: WpBnbWC.settings.ajaxUrl,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'wp_bnb_get_availability',
|
||||||
|
nonce: WpBnbWC.settings.nonce,
|
||||||
|
room_id: this.roomId,
|
||||||
|
check_in: checkIn,
|
||||||
|
check_out: checkOut,
|
||||||
|
guests: guests
|
||||||
|
},
|
||||||
|
success: (response) => {
|
||||||
|
if (response.success) {
|
||||||
|
const data = response.data;
|
||||||
|
|
||||||
|
if (data.available) {
|
||||||
|
this.updateAvailabilityStatus('available', WpBnbWC.settings.i18n?.available || 'Available');
|
||||||
|
this.updatePriceDisplay(data.price, data.breakdown);
|
||||||
|
this.enableSubmit();
|
||||||
|
} else {
|
||||||
|
this.updateAvailabilityStatus('unavailable', data.message || WpBnbWC.settings.i18n?.unavailable || 'Not available');
|
||||||
|
this.disableSubmit();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.updateAvailabilityStatus('unavailable', response.data?.message || 'Error checking availability');
|
||||||
|
this.disableSubmit();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.updateAvailabilityStatus('unavailable', WpBnbWC.settings.i18n?.error || 'Error checking availability');
|
||||||
|
this.disableSubmit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAvailabilityStatus(status, message) {
|
||||||
|
const $statusEl = this.$form.find('.bnb-availability-status');
|
||||||
|
|
||||||
|
if (!status) {
|
||||||
|
$statusEl.hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$statusEl.removeClass('checking available unavailable').addClass(status);
|
||||||
|
|
||||||
|
let icon = '';
|
||||||
|
switch (status) {
|
||||||
|
case 'checking':
|
||||||
|
icon = '<span class="dashicons dashicons-update"></span>';
|
||||||
|
break;
|
||||||
|
case 'available':
|
||||||
|
icon = '<span class="dashicons dashicons-yes-alt"></span>';
|
||||||
|
break;
|
||||||
|
case 'unavailable':
|
||||||
|
icon = '<span class="dashicons dashicons-no-alt"></span>';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$statusEl.html(icon + ' ' + message).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePriceDisplay(roomPrice, breakdown) {
|
||||||
|
const $priceDisplay = this.$form.find('.bnb-wc-price-display');
|
||||||
|
|
||||||
|
if (!roomPrice) {
|
||||||
|
$priceDisplay.hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate services total
|
||||||
|
let servicesTotal = 0;
|
||||||
|
const nights = breakdown?.nights || 1;
|
||||||
|
|
||||||
|
this.$form.find('.bnb-wc-service-item').each(function () {
|
||||||
|
const $item = $(this);
|
||||||
|
const $checkbox = $item.find('.bnb-service-checkbox');
|
||||||
|
|
||||||
|
if ($checkbox.is(':checked')) {
|
||||||
|
const price = parseFloat($item.data('price')) || 0;
|
||||||
|
const pricingType = $item.data('pricing-type');
|
||||||
|
const qty = parseInt($item.find('.bnb-service-qty').val()) || 1;
|
||||||
|
|
||||||
|
if (pricingType === 'per_night') {
|
||||||
|
servicesTotal += price * qty * nights;
|
||||||
|
} else if (pricingType === 'per_booking') {
|
||||||
|
servicesTotal += price * qty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const grandTotal = roomPrice + servicesTotal;
|
||||||
|
|
||||||
|
// Update display
|
||||||
|
let html = '<div class="price-row"><span>' + (WpBnbWC.settings.i18n?.roomTotal || 'Room') + '</span><span>' + WpBnbWC.formatPrice(roomPrice) + '</span></div>';
|
||||||
|
|
||||||
|
if (servicesTotal > 0) {
|
||||||
|
html += '<div class="price-row"><span>' + (WpBnbWC.settings.i18n?.services || 'Services') + '</span><span>' + WpBnbWC.formatPrice(servicesTotal) + '</span></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '<div class="price-row total"><span>' + (WpBnbWC.settings.i18n?.total || 'Total') + '</span><span>' + WpBnbWC.formatPrice(grandTotal) + '</span></div>';
|
||||||
|
|
||||||
|
$priceDisplay.html(html).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
enableSubmit() {
|
||||||
|
this.$form.find('.add-to-cart-btn').prop('disabled', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
disableSubmit() {
|
||||||
|
this.$form.find('.add-to-cart-btn').prop('disabled', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit(e) {
|
||||||
|
// Form will submit normally - WooCommerce handles the add to cart
|
||||||
|
// Just validate one more time
|
||||||
|
const checkIn = this.$form.find('[name="bnb_check_in"]').val();
|
||||||
|
const checkOut = this.$form.find('[name="bnb_check_out"]').val();
|
||||||
|
|
||||||
|
if (!checkIn || !checkOut) {
|
||||||
|
e.preventDefault();
|
||||||
|
alert(WpBnbWC.settings.i18n?.selectDates || 'Please select check-in and check-out dates');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format price
|
||||||
|
*/
|
||||||
|
WpBnbWC.formatPrice = function (price) {
|
||||||
|
const currency = this.settings.currency || 'CHF';
|
||||||
|
const symbol = this.settings.currencySymbol || currency;
|
||||||
|
const decimals = this.settings.priceDecimals || 2;
|
||||||
|
const decimalSep = this.settings.decimalSeparator || '.';
|
||||||
|
const thousandSep = this.settings.thousandSeparator || "'";
|
||||||
|
|
||||||
|
const formatted = price.toFixed(decimals).replace(/\B(?=(\d{3})+(?!\d))/g, thousandSep).replace('.', decimalSep);
|
||||||
|
|
||||||
|
return symbol + ' ' + formatted;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize on DOM ready
|
||||||
|
$(document).ready(function () {
|
||||||
|
WpBnbWC.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Export for external use
|
||||||
|
window.WpBnbWC = WpBnbWC;
|
||||||
|
|
||||||
|
})(jQuery);
|
||||||
282
src/Integration/WooCommerce/AdminColumns.php
Normal file
282
src/Integration/WooCommerce/AdminColumns.php
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* WooCommerce Admin Columns.
|
||||||
|
*
|
||||||
|
* Adds cross-reference columns and links between bookings and orders.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Integration\WooCommerce
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Integration\WooCommerce;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\PostTypes\Booking;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Admin Columns class.
|
||||||
|
*
|
||||||
|
* Enhances admin list tables with booking-order cross-references.
|
||||||
|
*/
|
||||||
|
final class AdminColumns {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize admin columns.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function init(): void {
|
||||||
|
// Add WC Order column to bookings list.
|
||||||
|
add_filter( 'manage_' . Booking::POST_TYPE . '_posts_columns', array( self::class, 'add_booking_columns' ) );
|
||||||
|
add_action( 'manage_' . Booking::POST_TYPE . '_posts_custom_column', array( self::class, 'render_booking_column' ), 10, 2 );
|
||||||
|
|
||||||
|
// Add Booking column to WC orders list.
|
||||||
|
add_filter( 'manage_edit-shop_order_columns', array( self::class, 'add_order_columns' ) );
|
||||||
|
add_action( 'manage_shop_order_posts_custom_column', array( self::class, 'render_order_column' ), 10, 2 );
|
||||||
|
|
||||||
|
// Also support HPOS orders list.
|
||||||
|
add_filter( 'manage_woocommerce_page_wc-orders_columns', array( self::class, 'add_order_columns' ) );
|
||||||
|
add_action( 'manage_woocommerce_page_wc-orders_custom_column', array( self::class, 'render_order_column_hpos' ), 10, 2 );
|
||||||
|
|
||||||
|
// Add row actions.
|
||||||
|
add_filter( 'post_row_actions', array( self::class, 'add_booking_row_actions' ), 10, 2 );
|
||||||
|
|
||||||
|
// Add order row actions.
|
||||||
|
add_filter( 'woocommerce_admin_order_actions', array( self::class, 'add_order_actions' ), 10, 2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add columns to booking admin list.
|
||||||
|
*
|
||||||
|
* @param array $columns Existing columns.
|
||||||
|
* @return array Modified columns.
|
||||||
|
*/
|
||||||
|
public static function add_booking_columns( array $columns ): array {
|
||||||
|
// Insert WC Order column after status.
|
||||||
|
$new_columns = array();
|
||||||
|
|
||||||
|
foreach ( $columns as $key => $value ) {
|
||||||
|
$new_columns[ $key ] = $value;
|
||||||
|
|
||||||
|
if ( 'status' === $key ) {
|
||||||
|
$new_columns['wc_order'] = __( 'WC Order', 'wp-bnb' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If status column doesn't exist, add at the end.
|
||||||
|
if ( ! isset( $new_columns['wc_order'] ) ) {
|
||||||
|
$new_columns['wc_order'] = __( 'WC Order', 'wp-bnb' );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $new_columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render booking admin column.
|
||||||
|
*
|
||||||
|
* @param string $column Column name.
|
||||||
|
* @param int $post_id Post ID.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function render_booking_column( string $column, int $post_id ): void {
|
||||||
|
if ( 'wc_order' !== $column ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$order = Manager::get_order_for_booking( $post_id );
|
||||||
|
|
||||||
|
if ( ! $order ) {
|
||||||
|
echo '<span class="na">–</span>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$order_number = $order->get_order_number();
|
||||||
|
$order_status = $order->get_status();
|
||||||
|
$edit_url = $order->get_edit_order_url();
|
||||||
|
|
||||||
|
printf(
|
||||||
|
'<a href="%s" class="order-view" title="%s">#%s</a>',
|
||||||
|
esc_url( $edit_url ),
|
||||||
|
esc_attr__( 'View order', 'wp-bnb' ),
|
||||||
|
esc_html( $order_number )
|
||||||
|
);
|
||||||
|
|
||||||
|
// Status badge.
|
||||||
|
$status_name = wc_get_order_status_name( $order_status );
|
||||||
|
printf(
|
||||||
|
'<br><mark class="order-status status-%s"><span>%s</span></mark>',
|
||||||
|
esc_attr( $order_status ),
|
||||||
|
esc_html( $status_name )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add columns to WooCommerce orders list.
|
||||||
|
*
|
||||||
|
* @param array $columns Existing columns.
|
||||||
|
* @return array Modified columns.
|
||||||
|
*/
|
||||||
|
public static function add_order_columns( array $columns ): array {
|
||||||
|
// Insert Booking column after order_status.
|
||||||
|
$new_columns = array();
|
||||||
|
|
||||||
|
foreach ( $columns as $key => $value ) {
|
||||||
|
$new_columns[ $key ] = $value;
|
||||||
|
|
||||||
|
if ( 'order_status' === $key ) {
|
||||||
|
$new_columns['bnb_booking'] = __( 'Booking', 'wp-bnb' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If status column doesn't exist, add before actions.
|
||||||
|
if ( ! isset( $new_columns['bnb_booking'] ) ) {
|
||||||
|
$columns_before = array_slice( $columns, 0, -1, true );
|
||||||
|
$columns_after = array_slice( $columns, -1, null, true );
|
||||||
|
|
||||||
|
$new_columns = $columns_before + array( 'bnb_booking' => __( 'Booking', 'wp-bnb' ) ) + $columns_after;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $new_columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render order admin column (legacy post-based orders).
|
||||||
|
*
|
||||||
|
* @param string $column Column name.
|
||||||
|
* @param int $post_id Post ID (Order ID).
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function render_order_column( string $column, int $post_id ): void {
|
||||||
|
if ( 'bnb_booking' !== $column ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$order = wc_get_order( $post_id );
|
||||||
|
|
||||||
|
if ( ! $order ) {
|
||||||
|
echo '<span class="na">–</span>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self::render_booking_info_for_order( $order );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render order admin column (HPOS).
|
||||||
|
*
|
||||||
|
* @param string $column Column name.
|
||||||
|
* @param \WC_Order $order Order object.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function render_order_column_hpos( string $column, $order ): void {
|
||||||
|
if ( 'bnb_booking' !== $column ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! $order instanceof \WC_Order ) {
|
||||||
|
echo '<span class="na">–</span>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self::render_booking_info_for_order( $order );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render booking info for an order.
|
||||||
|
*
|
||||||
|
* @param \WC_Order $order WooCommerce order.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function render_booking_info_for_order( \WC_Order $order ): void {
|
||||||
|
$booking_id = Manager::get_booking_for_order( $order );
|
||||||
|
|
||||||
|
if ( ! $booking_id ) {
|
||||||
|
echo '<span class="na">–</span>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$booking = get_post( $booking_id );
|
||||||
|
$check_in = get_post_meta( $booking_id, '_bnb_booking_check_in', true );
|
||||||
|
$check_out = get_post_meta( $booking_id, '_bnb_booking_check_out', true );
|
||||||
|
$status = get_post_meta( $booking_id, '_bnb_booking_status', true );
|
||||||
|
|
||||||
|
// Booking link.
|
||||||
|
printf(
|
||||||
|
'<a href="%s" title="%s">%s</a>',
|
||||||
|
esc_url( get_edit_post_link( $booking_id ) ),
|
||||||
|
esc_attr__( 'View booking', 'wp-bnb' ),
|
||||||
|
$booking ? esc_html( wp_trim_words( $booking->post_title, 3 ) ) : '#' . esc_html( $booking_id )
|
||||||
|
);
|
||||||
|
|
||||||
|
// Dates.
|
||||||
|
if ( $check_in && $check_out ) {
|
||||||
|
$check_in_date = \DateTime::createFromFormat( 'Y-m-d', $check_in );
|
||||||
|
$check_out_date = \DateTime::createFromFormat( 'Y-m-d', $check_out );
|
||||||
|
|
||||||
|
if ( $check_in_date && $check_out_date ) {
|
||||||
|
printf(
|
||||||
|
'<br><small>%s - %s</small>',
|
||||||
|
esc_html( $check_in_date->format( 'd.m' ) ),
|
||||||
|
esc_html( $check_out_date->format( 'd.m.y' ) )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status badge.
|
||||||
|
if ( $status ) {
|
||||||
|
printf(
|
||||||
|
'<br><span class="bnb-status-badge bnb-status-%s" style="font-size: 10px;">%s</span>',
|
||||||
|
esc_attr( $status ),
|
||||||
|
esc_html( ucfirst( str_replace( '_', ' ', $status ) ) )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add row actions to booking list.
|
||||||
|
*
|
||||||
|
* @param array $actions Existing actions.
|
||||||
|
* @param \WP_Post $post Post object.
|
||||||
|
* @return array Modified actions.
|
||||||
|
*/
|
||||||
|
public static function add_booking_row_actions( array $actions, \WP_Post $post ): array {
|
||||||
|
if ( Booking::POST_TYPE !== $post->post_type ) {
|
||||||
|
return $actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
$order = Manager::get_order_for_booking( $post->ID );
|
||||||
|
|
||||||
|
if ( $order ) {
|
||||||
|
$actions['view_order'] = sprintf(
|
||||||
|
'<a href="%s" aria-label="%s">%s</a>',
|
||||||
|
esc_url( $order->get_edit_order_url() ),
|
||||||
|
/* translators: %s: Order number */
|
||||||
|
esc_attr( sprintf( __( 'View order #%s', 'wp-bnb' ), $order->get_order_number() ) ),
|
||||||
|
__( 'View Order', 'wp-bnb' )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add actions to WooCommerce order row.
|
||||||
|
*
|
||||||
|
* @param array $actions Existing actions.
|
||||||
|
* @param \WC_Order $order Order object.
|
||||||
|
* @return array Modified actions.
|
||||||
|
*/
|
||||||
|
public static function add_order_actions( array $actions, \WC_Order $order ): array {
|
||||||
|
$booking_id = Manager::get_booking_for_order( $order );
|
||||||
|
|
||||||
|
if ( $booking_id ) {
|
||||||
|
$actions['view_booking'] = array(
|
||||||
|
'url' => get_edit_post_link( $booking_id ),
|
||||||
|
'name' => __( 'View Booking', 'wp-bnb' ),
|
||||||
|
'action' => 'view_booking',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $actions;
|
||||||
|
}
|
||||||
|
}
|
||||||
545
src/Integration/WooCommerce/CartHandler.php
Normal file
545
src/Integration/WooCommerce/CartHandler.php
Normal file
@@ -0,0 +1,545 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* WooCommerce Cart Handler.
|
||||||
|
*
|
||||||
|
* Handles cart item data, availability validation, and dynamic pricing.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Integration\WooCommerce
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Integration\WooCommerce;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\Booking\Availability;
|
||||||
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
use Magdev\WpBnb\PostTypes\Service;
|
||||||
|
use Magdev\WpBnb\Pricing\Calculator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cart Handler class.
|
||||||
|
*
|
||||||
|
* Manages booking data in the WooCommerce cart, validates availability,
|
||||||
|
* and calculates dynamic pricing based on dates and services.
|
||||||
|
*/
|
||||||
|
final class CartHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize cart handler.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function init(): void {
|
||||||
|
// Add booking data to cart item.
|
||||||
|
add_filter( 'woocommerce_add_cart_item_data', array( self::class, 'add_cart_item_data' ), 10, 3 );
|
||||||
|
|
||||||
|
// Restore booking data from session.
|
||||||
|
add_filter( 'woocommerce_get_cart_item_from_session', array( self::class, 'get_cart_item_from_session' ), 10, 2 );
|
||||||
|
|
||||||
|
// Validate before adding to cart.
|
||||||
|
add_filter( 'woocommerce_add_to_cart_validation', array( self::class, 'validate_add_to_cart' ), 10, 5 );
|
||||||
|
|
||||||
|
// Re-validate availability on cart load.
|
||||||
|
add_action( 'woocommerce_cart_loaded_from_session', array( self::class, 'validate_cart_items' ) );
|
||||||
|
|
||||||
|
// Display booking info in cart.
|
||||||
|
add_filter( 'woocommerce_get_item_data', array( self::class, 'display_cart_item_data' ), 10, 2 );
|
||||||
|
|
||||||
|
// Calculate dynamic price.
|
||||||
|
add_action( 'woocommerce_before_calculate_totals', array( self::class, 'calculate_cart_item_prices' ) );
|
||||||
|
|
||||||
|
// Prevent quantity changes for room products.
|
||||||
|
add_filter( 'woocommerce_quantity_input_args', array( self::class, 'lock_quantity' ), 10, 2 );
|
||||||
|
|
||||||
|
// Add booking data to order item meta.
|
||||||
|
add_action( 'woocommerce_checkout_create_order_line_item', array( self::class, 'add_order_item_meta' ), 10, 4 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a room to cart with booking data.
|
||||||
|
*
|
||||||
|
* @param int $room_id Room post ID.
|
||||||
|
* @param string $check_in Check-in date (Y-m-d).
|
||||||
|
* @param string $check_out Check-out date (Y-m-d).
|
||||||
|
* @param int $guests Number of guests.
|
||||||
|
* @param array $services Array of service selections.
|
||||||
|
* @return string|bool Cart item key or false on failure.
|
||||||
|
*/
|
||||||
|
public static function add_room_to_cart(
|
||||||
|
int $room_id,
|
||||||
|
string $check_in,
|
||||||
|
string $check_out,
|
||||||
|
int $guests = 1,
|
||||||
|
array $services = array()
|
||||||
|
) {
|
||||||
|
// Get product ID for room.
|
||||||
|
$product_id = ProductSync::get_product_for_room( $room_id );
|
||||||
|
|
||||||
|
if ( ! $product_id ) {
|
||||||
|
// Try to sync the room first.
|
||||||
|
$product_id = ProductSync::sync_room_to_product( $room_id );
|
||||||
|
|
||||||
|
if ( ! $product_id ) {
|
||||||
|
wc_add_notice( __( 'Unable to add room to cart. Product not found.', 'wp-bnb' ), 'error' );
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store booking data in session temporarily for the filter.
|
||||||
|
WC()->session->set(
|
||||||
|
'bnb_pending_booking',
|
||||||
|
array(
|
||||||
|
'room_id' => $room_id,
|
||||||
|
'check_in' => $check_in,
|
||||||
|
'check_out' => $check_out,
|
||||||
|
'guests' => $guests,
|
||||||
|
'services' => $services,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add to cart.
|
||||||
|
$cart_item_key = WC()->cart->add_to_cart( $product_id, 1 );
|
||||||
|
|
||||||
|
// Clean up session.
|
||||||
|
WC()->session->set( 'bnb_pending_booking', null );
|
||||||
|
|
||||||
|
return $cart_item_key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add booking data to cart item when adding to cart.
|
||||||
|
*
|
||||||
|
* @param array $cart_item_data Cart item data.
|
||||||
|
* @param int $product_id Product ID.
|
||||||
|
* @param int $variation_id Variation ID.
|
||||||
|
* @return array Modified cart item data.
|
||||||
|
*/
|
||||||
|
public static function add_cart_item_data( array $cart_item_data, int $product_id, int $variation_id ): array {
|
||||||
|
// Check if this is a room product.
|
||||||
|
$room_id = ProductSync::get_room_for_product( $product_id );
|
||||||
|
|
||||||
|
if ( ! $room_id ) {
|
||||||
|
return $cart_item_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get booking data from session or POST.
|
||||||
|
$booking_data = WC()->session->get( 'bnb_pending_booking' );
|
||||||
|
|
||||||
|
if ( ! $booking_data ) {
|
||||||
|
// Try to get from POST (for direct form submissions).
|
||||||
|
// phpcs:disable WordPress.Security.NonceVerification.Missing
|
||||||
|
$booking_data = array(
|
||||||
|
'room_id' => isset( $_POST['bnb_room_id'] ) ? absint( $_POST['bnb_room_id'] ) : $room_id,
|
||||||
|
'check_in' => isset( $_POST['bnb_check_in'] ) ? sanitize_text_field( wp_unslash( $_POST['bnb_check_in'] ) ) : '',
|
||||||
|
'check_out' => isset( $_POST['bnb_check_out'] ) ? sanitize_text_field( wp_unslash( $_POST['bnb_check_out'] ) ) : '',
|
||||||
|
'guests' => isset( $_POST['bnb_guests'] ) ? absint( $_POST['bnb_guests'] ) : 1,
|
||||||
|
'services' => isset( $_POST['bnb_services'] ) ? self::sanitize_services( $_POST['bnb_services'] ) : array(),
|
||||||
|
);
|
||||||
|
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required fields.
|
||||||
|
if ( empty( $booking_data['check_in'] ) || empty( $booking_data['check_out'] ) ) {
|
||||||
|
return $cart_item_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate nights.
|
||||||
|
$check_in = new \DateTime( $booking_data['check_in'] );
|
||||||
|
$check_out = new \DateTime( $booking_data['check_out'] );
|
||||||
|
$nights = (int) $check_in->diff( $check_out )->days;
|
||||||
|
|
||||||
|
if ( $nights < 1 ) {
|
||||||
|
return $cart_item_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate price breakdown.
|
||||||
|
$calculator = new Calculator( $room_id, $booking_data['check_in'], $booking_data['check_out'] );
|
||||||
|
$price_breakdown = $calculator->calculate();
|
||||||
|
$room_total = $price_breakdown['total'];
|
||||||
|
|
||||||
|
// Calculate services total.
|
||||||
|
$services_total = 0;
|
||||||
|
$services_data = array();
|
||||||
|
|
||||||
|
foreach ( $booking_data['services'] as $service_selection ) {
|
||||||
|
$service_id = $service_selection['service_id'] ?? 0;
|
||||||
|
$quantity = $service_selection['quantity'] ?? 1;
|
||||||
|
|
||||||
|
if ( ! $service_id ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$service_price = Service::calculate_service_price( $service_id, $quantity, $nights );
|
||||||
|
$service_data = Service::get_service_data( $service_id );
|
||||||
|
|
||||||
|
$services_data[] = array(
|
||||||
|
'service_id' => $service_id,
|
||||||
|
'quantity' => $quantity,
|
||||||
|
'price' => $service_price,
|
||||||
|
'pricing_type' => $service_data['pricing_type'] ?? 'per_booking',
|
||||||
|
'name' => $service_data['name'] ?? '',
|
||||||
|
);
|
||||||
|
|
||||||
|
$services_total += $service_price;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store booking data.
|
||||||
|
$cart_item_data[ Manager::CART_ITEM_KEY ] = array(
|
||||||
|
'room_id' => $room_id,
|
||||||
|
'check_in' => $booking_data['check_in'],
|
||||||
|
'check_out' => $booking_data['check_out'],
|
||||||
|
'guests' => $booking_data['guests'],
|
||||||
|
'nights' => $nights,
|
||||||
|
'services' => $services_data,
|
||||||
|
'price_breakdown' => array(
|
||||||
|
'room_total' => $room_total,
|
||||||
|
'services_total' => $services_total,
|
||||||
|
'grand_total' => $room_total + $services_total,
|
||||||
|
'full_breakdown' => $price_breakdown,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Generate unique key based on booking data to allow multiple bookings.
|
||||||
|
$cart_item_data['unique_key'] = md5(
|
||||||
|
$room_id . $booking_data['check_in'] . $booking_data['check_out'] . microtime()
|
||||||
|
);
|
||||||
|
|
||||||
|
return $cart_item_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore booking data from session.
|
||||||
|
*
|
||||||
|
* @param array $cart_item Cart item data.
|
||||||
|
* @param array $values Session values.
|
||||||
|
* @return array Modified cart item data.
|
||||||
|
*/
|
||||||
|
public static function get_cart_item_from_session( array $cart_item, array $values ): array {
|
||||||
|
if ( isset( $values[ Manager::CART_ITEM_KEY ] ) ) {
|
||||||
|
$cart_item[ Manager::CART_ITEM_KEY ] = $values[ Manager::CART_ITEM_KEY ];
|
||||||
|
}
|
||||||
|
return $cart_item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate room availability before adding to cart.
|
||||||
|
*
|
||||||
|
* @param bool $passed Whether validation passed.
|
||||||
|
* @param int $product_id Product ID.
|
||||||
|
* @param int $quantity Quantity.
|
||||||
|
* @param int $variation_id Variation ID.
|
||||||
|
* @param array $variations Variations.
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function validate_add_to_cart( bool $passed, int $product_id, int $quantity, int $variation_id = 0, array $variations = array() ): bool {
|
||||||
|
// Check if this is a room product.
|
||||||
|
$room_id = ProductSync::get_room_for_product( $product_id );
|
||||||
|
|
||||||
|
if ( ! $room_id ) {
|
||||||
|
return $passed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get booking data.
|
||||||
|
$booking_data = WC()->session->get( 'bnb_pending_booking' );
|
||||||
|
|
||||||
|
if ( ! $booking_data ) {
|
||||||
|
// phpcs:disable WordPress.Security.NonceVerification.Missing
|
||||||
|
$booking_data = array(
|
||||||
|
'check_in' => isset( $_POST['bnb_check_in'] ) ? sanitize_text_field( wp_unslash( $_POST['bnb_check_in'] ) ) : '',
|
||||||
|
'check_out' => isset( $_POST['bnb_check_out'] ) ? sanitize_text_field( wp_unslash( $_POST['bnb_check_out'] ) ) : '',
|
||||||
|
'guests' => isset( $_POST['bnb_guests'] ) ? absint( $_POST['bnb_guests'] ) : 1,
|
||||||
|
);
|
||||||
|
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate dates provided.
|
||||||
|
if ( empty( $booking_data['check_in'] ) || empty( $booking_data['check_out'] ) ) {
|
||||||
|
wc_add_notice( __( 'Please select check-in and check-out dates.', 'wp-bnb' ), 'error' );
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate date format.
|
||||||
|
$check_in = \DateTime::createFromFormat( 'Y-m-d', $booking_data['check_in'] );
|
||||||
|
$check_out = \DateTime::createFromFormat( 'Y-m-d', $booking_data['check_out'] );
|
||||||
|
|
||||||
|
if ( ! $check_in || ! $check_out ) {
|
||||||
|
wc_add_notice( __( 'Invalid date format. Please use the date picker.', 'wp-bnb' ), 'error' );
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate check-out after check-in.
|
||||||
|
if ( $check_out <= $check_in ) {
|
||||||
|
wc_add_notice( __( 'Check-out date must be after check-in date.', 'wp-bnb' ), 'error' );
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate not in past.
|
||||||
|
$today = new \DateTime( 'today' );
|
||||||
|
if ( $check_in < $today ) {
|
||||||
|
wc_add_notice( __( 'Check-in date cannot be in the past.', 'wp-bnb' ), 'error' );
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check availability.
|
||||||
|
$is_available = Availability::check_availability(
|
||||||
|
$room_id,
|
||||||
|
$booking_data['check_in'],
|
||||||
|
$booking_data['check_out']
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( ! $is_available ) {
|
||||||
|
wc_add_notice( __( 'Sorry, this room is not available for the selected dates.', 'wp-bnb' ), 'error' );
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check capacity.
|
||||||
|
$capacity = get_post_meta( $room_id, '_bnb_room_capacity', true );
|
||||||
|
if ( $capacity && $booking_data['guests'] > (int) $capacity ) {
|
||||||
|
wc_add_notice(
|
||||||
|
sprintf(
|
||||||
|
/* translators: %d: Room capacity */
|
||||||
|
__( 'This room has a maximum capacity of %d guests.', 'wp-bnb' ),
|
||||||
|
$capacity
|
||||||
|
),
|
||||||
|
'error'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if same room with same dates already in cart.
|
||||||
|
foreach ( WC()->cart->get_cart() as $cart_item ) {
|
||||||
|
if ( isset( $cart_item[ Manager::CART_ITEM_KEY ] ) ) {
|
||||||
|
$existing = $cart_item[ Manager::CART_ITEM_KEY ];
|
||||||
|
if ( $existing['room_id'] === $room_id
|
||||||
|
&& $existing['check_in'] === $booking_data['check_in']
|
||||||
|
&& $existing['check_out'] === $booking_data['check_out']
|
||||||
|
) {
|
||||||
|
wc_add_notice( __( 'This room with the same dates is already in your cart.', 'wp-bnb' ), 'error' );
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $passed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate cart items on cart load.
|
||||||
|
*
|
||||||
|
* @param \WC_Cart $cart Cart object.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function validate_cart_items( \WC_Cart $cart ): void {
|
||||||
|
$items_to_remove = array();
|
||||||
|
|
||||||
|
foreach ( $cart->get_cart() as $cart_item_key => $cart_item ) {
|
||||||
|
if ( ! isset( $cart_item[ Manager::CART_ITEM_KEY ] ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$booking_data = $cart_item[ Manager::CART_ITEM_KEY ];
|
||||||
|
|
||||||
|
// Check if dates are still valid.
|
||||||
|
$check_in = \DateTime::createFromFormat( 'Y-m-d', $booking_data['check_in'] );
|
||||||
|
$today = new \DateTime( 'today' );
|
||||||
|
|
||||||
|
if ( $check_in < $today ) {
|
||||||
|
$items_to_remove[] = array(
|
||||||
|
'key' => $cart_item_key,
|
||||||
|
'message' => __( 'A room booking was removed because the check-in date has passed.', 'wp-bnb' ),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if room still available.
|
||||||
|
$is_available = Availability::check_availability(
|
||||||
|
$booking_data['room_id'],
|
||||||
|
$booking_data['check_in'],
|
||||||
|
$booking_data['check_out']
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( ! $is_available ) {
|
||||||
|
$items_to_remove[] = array(
|
||||||
|
'key' => $cart_item_key,
|
||||||
|
'message' => __( 'A room booking was removed because the room is no longer available for those dates.', 'wp-bnb' ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove invalid items.
|
||||||
|
foreach ( $items_to_remove as $item ) {
|
||||||
|
$cart->remove_cart_item( $item['key'] );
|
||||||
|
wc_add_notice( $item['message'], 'error' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display booking info in cart.
|
||||||
|
*
|
||||||
|
* @param array $item_data Item data for display.
|
||||||
|
* @param array $cart_item Cart item.
|
||||||
|
* @return array Modified item data.
|
||||||
|
*/
|
||||||
|
public static function display_cart_item_data( array $item_data, array $cart_item ): array {
|
||||||
|
if ( ! isset( $cart_item[ Manager::CART_ITEM_KEY ] ) ) {
|
||||||
|
return $item_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
$booking_data = $cart_item[ Manager::CART_ITEM_KEY ];
|
||||||
|
|
||||||
|
// Format dates.
|
||||||
|
$date_format = get_option( 'date_format' );
|
||||||
|
$check_in = \DateTime::createFromFormat( 'Y-m-d', $booking_data['check_in'] );
|
||||||
|
$check_out = \DateTime::createFromFormat( 'Y-m-d', $booking_data['check_out'] );
|
||||||
|
|
||||||
|
$item_data[] = array(
|
||||||
|
'key' => __( 'Check-in', 'wp-bnb' ),
|
||||||
|
'value' => $check_in ? $check_in->format( $date_format ) : $booking_data['check_in'],
|
||||||
|
);
|
||||||
|
|
||||||
|
$item_data[] = array(
|
||||||
|
'key' => __( 'Check-out', 'wp-bnb' ),
|
||||||
|
'value' => $check_out ? $check_out->format( $date_format ) : $booking_data['check_out'],
|
||||||
|
);
|
||||||
|
|
||||||
|
$item_data[] = array(
|
||||||
|
'key' => __( 'Nights', 'wp-bnb' ),
|
||||||
|
'value' => $booking_data['nights'],
|
||||||
|
);
|
||||||
|
|
||||||
|
$item_data[] = array(
|
||||||
|
'key' => __( 'Guests', 'wp-bnb' ),
|
||||||
|
'value' => $booking_data['guests'],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Display services.
|
||||||
|
if ( ! empty( $booking_data['services'] ) ) {
|
||||||
|
$services_list = array();
|
||||||
|
foreach ( $booking_data['services'] as $service ) {
|
||||||
|
$services_list[] = $service['name'] . ' × ' . $service['quantity'];
|
||||||
|
}
|
||||||
|
$item_data[] = array(
|
||||||
|
'key' => __( 'Services', 'wp-bnb' ),
|
||||||
|
'value' => implode( ', ', $services_list ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $item_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate dynamic prices for cart items.
|
||||||
|
*
|
||||||
|
* @param \WC_Cart $cart Cart object.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function calculate_cart_item_prices( \WC_Cart $cart ): void {
|
||||||
|
if ( is_admin() && ! defined( 'DOING_AJAX' ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ( $cart->get_cart() as $cart_item ) {
|
||||||
|
if ( ! isset( $cart_item[ Manager::CART_ITEM_KEY ] ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$booking_data = $cart_item[ Manager::CART_ITEM_KEY ];
|
||||||
|
$grand_total = $booking_data['price_breakdown']['grand_total'] ?? 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter the cart item price for a booking.
|
||||||
|
*
|
||||||
|
* @param float $price The calculated price.
|
||||||
|
* @param array $cart_item The cart item.
|
||||||
|
*/
|
||||||
|
$grand_total = apply_filters( 'wp_bnb_wc_cart_item_price', $grand_total, $cart_item );
|
||||||
|
|
||||||
|
// Set the price.
|
||||||
|
$cart_item['data']->set_price( $grand_total );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lock quantity to 1 for room products.
|
||||||
|
*
|
||||||
|
* @param array $args Input arguments.
|
||||||
|
* @param \WC_Product $product Product object.
|
||||||
|
* @return array Modified arguments.
|
||||||
|
*/
|
||||||
|
public static function lock_quantity( array $args, \WC_Product $product ): array {
|
||||||
|
if ( ProductSync::is_room_product( $product ) ) {
|
||||||
|
$args['min_value'] = 1;
|
||||||
|
$args['max_value'] = 1;
|
||||||
|
$args['readonly'] = true;
|
||||||
|
}
|
||||||
|
return $args;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add booking data to order item meta.
|
||||||
|
*
|
||||||
|
* @param \WC_Order_Item_Product $item Order item.
|
||||||
|
* @param string $cart_item_key Cart item key.
|
||||||
|
* @param array $values Cart item values.
|
||||||
|
* @param \WC_Order $order Order object.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function add_order_item_meta( \WC_Order_Item_Product $item, string $cart_item_key, array $values, \WC_Order $order ): void {
|
||||||
|
if ( ! isset( $values[ Manager::CART_ITEM_KEY ] ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$booking_data = $values[ Manager::CART_ITEM_KEY ];
|
||||||
|
|
||||||
|
// Store booking data in order item meta.
|
||||||
|
$item->add_meta_data( '_bnb_room_id', $booking_data['room_id'] );
|
||||||
|
$item->add_meta_data( '_bnb_check_in', $booking_data['check_in'] );
|
||||||
|
$item->add_meta_data( '_bnb_check_out', $booking_data['check_out'] );
|
||||||
|
$item->add_meta_data( '_bnb_guests', $booking_data['guests'] );
|
||||||
|
$item->add_meta_data( '_bnb_nights', $booking_data['nights'] );
|
||||||
|
$item->add_meta_data( '_bnb_services', wp_json_encode( $booking_data['services'] ) );
|
||||||
|
$item->add_meta_data( '_bnb_price_breakdown', wp_json_encode( $booking_data['price_breakdown'] ) );
|
||||||
|
|
||||||
|
// Add visible meta for admin display.
|
||||||
|
$date_format = get_option( 'date_format' );
|
||||||
|
$check_in = \DateTime::createFromFormat( 'Y-m-d', $booking_data['check_in'] );
|
||||||
|
$check_out = \DateTime::createFromFormat( 'Y-m-d', $booking_data['check_out'] );
|
||||||
|
|
||||||
|
$item->add_meta_data( __( 'Check-in', 'wp-bnb' ), $check_in ? $check_in->format( $date_format ) : $booking_data['check_in'] );
|
||||||
|
$item->add_meta_data( __( 'Check-out', 'wp-bnb' ), $check_out ? $check_out->format( $date_format ) : $booking_data['check_out'] );
|
||||||
|
$item->add_meta_data( __( 'Nights', 'wp-bnb' ), $booking_data['nights'] );
|
||||||
|
$item->add_meta_data( __( 'Guests', 'wp-bnb' ), $booking_data['guests'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize services array from POST data.
|
||||||
|
*
|
||||||
|
* @param mixed $services Raw services data.
|
||||||
|
* @return array Sanitized services array.
|
||||||
|
*/
|
||||||
|
private static function sanitize_services( $services ): array {
|
||||||
|
if ( ! is_array( $services ) ) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
$sanitized = array();
|
||||||
|
|
||||||
|
foreach ( $services as $service ) {
|
||||||
|
if ( ! is_array( $service ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$service_id = isset( $service['service_id'] ) ? absint( $service['service_id'] ) : 0;
|
||||||
|
$quantity = isset( $service['quantity'] ) ? absint( $service['quantity'] ) : 1;
|
||||||
|
|
||||||
|
if ( $service_id > 0 ) {
|
||||||
|
$sanitized[] = array(
|
||||||
|
'service_id' => $service_id,
|
||||||
|
'quantity' => max( 1, $quantity ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sanitized;
|
||||||
|
}
|
||||||
|
}
|
||||||
347
src/Integration/WooCommerce/CheckoutHandler.php
Normal file
347
src/Integration/WooCommerce/CheckoutHandler.php
Normal file
@@ -0,0 +1,347 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* WooCommerce Checkout Handler.
|
||||||
|
*
|
||||||
|
* Handles checkout field customization and validation.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Integration\WooCommerce
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Integration\WooCommerce;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\Booking\Availability;
|
||||||
|
use Magdev\WpBnb\PostTypes\Guest;
|
||||||
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checkout Handler class.
|
||||||
|
*
|
||||||
|
* Manages checkout field customization, pre-filling, and validation.
|
||||||
|
*/
|
||||||
|
final class CheckoutHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize checkout handler.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function init(): void {
|
||||||
|
// Add custom checkout fields.
|
||||||
|
add_filter( 'woocommerce_checkout_fields', array( self::class, 'add_checkout_fields' ) );
|
||||||
|
|
||||||
|
// Pre-fill checkout fields.
|
||||||
|
add_filter( 'woocommerce_checkout_get_value', array( self::class, 'prefill_checkout_fields' ), 10, 2 );
|
||||||
|
|
||||||
|
// Validate checkout.
|
||||||
|
add_action( 'woocommerce_after_checkout_validation', array( self::class, 'validate_checkout' ), 10, 2 );
|
||||||
|
|
||||||
|
// Display booking summary in order review.
|
||||||
|
add_action( 'woocommerce_review_order_before_submit', array( self::class, 'display_booking_summary' ) );
|
||||||
|
|
||||||
|
// Save guest notes to order meta.
|
||||||
|
add_action( 'woocommerce_checkout_create_order', array( self::class, 'save_guest_notes' ), 10, 2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add custom checkout fields.
|
||||||
|
*
|
||||||
|
* @param array $fields Checkout fields.
|
||||||
|
* @return array Modified checkout fields.
|
||||||
|
*/
|
||||||
|
public static function add_checkout_fields( array $fields ): array {
|
||||||
|
// Only add fields if cart contains BnB bookings.
|
||||||
|
if ( ! self::cart_has_bookings() ) {
|
||||||
|
return $fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add special requests field.
|
||||||
|
$fields['order']['bnb_guest_notes'] = array(
|
||||||
|
'type' => 'textarea',
|
||||||
|
'label' => __( 'Special Requests', 'wp-bnb' ),
|
||||||
|
'placeholder' => __( 'Any special requests, dietary requirements, or preferences...', 'wp-bnb' ),
|
||||||
|
'class' => array( 'form-row-wide' ),
|
||||||
|
'required' => false,
|
||||||
|
'priority' => 90,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add expected arrival time.
|
||||||
|
$fields['order']['bnb_arrival_time'] = array(
|
||||||
|
'type' => 'select',
|
||||||
|
'label' => __( 'Expected Arrival Time', 'wp-bnb' ),
|
||||||
|
'class' => array( 'form-row-wide' ),
|
||||||
|
'required' => false,
|
||||||
|
'priority' => 85,
|
||||||
|
'options' => self::get_arrival_time_options(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return $fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pre-fill checkout fields from guest data.
|
||||||
|
*
|
||||||
|
* @param mixed $value Current value.
|
||||||
|
* @param string $input Input field name.
|
||||||
|
* @return mixed Pre-filled value.
|
||||||
|
*/
|
||||||
|
public static function prefill_checkout_fields( $value, string $input ) {
|
||||||
|
// Only for logged-in users.
|
||||||
|
if ( ! is_user_logged_in() ) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find an existing guest record by email.
|
||||||
|
$current_user = wp_get_current_user();
|
||||||
|
$guest = self::find_guest_by_email( $current_user->user_email );
|
||||||
|
|
||||||
|
if ( ! $guest ) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mappings = array(
|
||||||
|
'billing_first_name' => '_bnb_guest_first_name',
|
||||||
|
'billing_last_name' => '_bnb_guest_last_name',
|
||||||
|
'billing_email' => '_bnb_guest_email',
|
||||||
|
'billing_phone' => '_bnb_guest_phone',
|
||||||
|
'billing_address_1' => '_bnb_guest_address',
|
||||||
|
'billing_city' => '_bnb_guest_city',
|
||||||
|
'billing_postcode' => '_bnb_guest_postal_code',
|
||||||
|
'billing_country' => '_bnb_guest_country',
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( isset( $mappings[ $input ] ) ) {
|
||||||
|
$meta_value = get_post_meta( $guest->ID, $mappings[ $input ], true );
|
||||||
|
if ( $meta_value ) {
|
||||||
|
return $meta_value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate checkout for BnB bookings.
|
||||||
|
*
|
||||||
|
* @param array $data Checkout data.
|
||||||
|
* @param \WP_Error $errors Error object.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function validate_checkout( array $data, \WP_Error $errors ): void {
|
||||||
|
// Re-validate availability for all bookings.
|
||||||
|
foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
|
||||||
|
if ( ! isset( $cart_item[ Manager::CART_ITEM_KEY ] ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$booking_data = $cart_item[ Manager::CART_ITEM_KEY ];
|
||||||
|
|
||||||
|
// Check availability one more time.
|
||||||
|
$is_available = Availability::check_availability(
|
||||||
|
$booking_data['room_id'],
|
||||||
|
$booking_data['check_in'],
|
||||||
|
$booking_data['check_out']
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( ! $is_available ) {
|
||||||
|
$room = get_post( $booking_data['room_id'] );
|
||||||
|
$errors->add(
|
||||||
|
'bnb_room_unavailable',
|
||||||
|
sprintf(
|
||||||
|
/* translators: %s: Room name */
|
||||||
|
__( 'Sorry, %s is no longer available for the selected dates. Please update your cart.', 'wp-bnb' ),
|
||||||
|
$room ? $room->post_title : __( 'the room', 'wp-bnb' )
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate check-in date not in past.
|
||||||
|
$check_in = \DateTime::createFromFormat( 'Y-m-d', $booking_data['check_in'] );
|
||||||
|
$today = new \DateTime( 'today' );
|
||||||
|
|
||||||
|
if ( $check_in < $today ) {
|
||||||
|
$errors->add(
|
||||||
|
'bnb_date_passed',
|
||||||
|
__( 'A booking in your cart has a check-in date in the past. Please update your cart.', 'wp-bnb' )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display booking summary in order review section.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function display_booking_summary(): void {
|
||||||
|
if ( ! self::cart_has_bookings() ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bookings = array();
|
||||||
|
|
||||||
|
foreach ( WC()->cart->get_cart() as $cart_item ) {
|
||||||
|
if ( ! isset( $cart_item[ Manager::CART_ITEM_KEY ] ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$booking_data = $cart_item[ Manager::CART_ITEM_KEY ];
|
||||||
|
$room = get_post( $booking_data['room_id'] );
|
||||||
|
|
||||||
|
if ( ! $room ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$building = Room::get_building( $booking_data['room_id'] );
|
||||||
|
|
||||||
|
$date_format = get_option( 'date_format' );
|
||||||
|
$check_in = \DateTime::createFromFormat( 'Y-m-d', $booking_data['check_in'] );
|
||||||
|
$check_out = \DateTime::createFromFormat( 'Y-m-d', $booking_data['check_out'] );
|
||||||
|
|
||||||
|
$bookings[] = array(
|
||||||
|
'room_name' => $room->post_title,
|
||||||
|
'building_name' => $building ? $building->post_title : '',
|
||||||
|
'check_in' => $check_in ? $check_in->format( $date_format ) : '',
|
||||||
|
'check_out' => $check_out ? $check_out->format( $date_format ) : '',
|
||||||
|
'nights' => $booking_data['nights'],
|
||||||
|
'guests' => $booking_data['guests'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( empty( $bookings ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="bnb-checkout-booking-summary">
|
||||||
|
<h3><?php esc_html_e( 'Booking Summary', 'wp-bnb' ); ?></h3>
|
||||||
|
<?php foreach ( $bookings as $booking ) : ?>
|
||||||
|
<div class="bnb-booking-item">
|
||||||
|
<strong><?php echo esc_html( $booking['room_name'] ); ?></strong>
|
||||||
|
<?php if ( $booking['building_name'] ) : ?>
|
||||||
|
<span class="bnb-building-name"><?php echo esc_html( $booking['building_name'] ); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div class="bnb-booking-details">
|
||||||
|
<span>
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: 1: Check-in date, 2: Check-out date */
|
||||||
|
esc_html__( '%1$s to %2$s', 'wp-bnb' ),
|
||||||
|
esc_html( $booking['check_in'] ),
|
||||||
|
esc_html( $booking['check_out'] )
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: %d: Number of nights */
|
||||||
|
esc_html( _n( '%d night', '%d nights', $booking['nights'], 'wp-bnb' ) ),
|
||||||
|
$booking['nights']
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: %d: Number of guests */
|
||||||
|
esc_html( _n( '%d guest', '%d guests', $booking['guests'], 'wp-bnb' ) ),
|
||||||
|
$booking['guests']
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save guest notes to order meta.
|
||||||
|
*
|
||||||
|
* @param \WC_Order $order Order object.
|
||||||
|
* @param array $data Checkout data.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function save_guest_notes( \WC_Order $order, array $data ): void {
|
||||||
|
// phpcs:disable WordPress.Security.NonceVerification.Missing
|
||||||
|
if ( isset( $_POST['bnb_guest_notes'] ) && ! empty( $_POST['bnb_guest_notes'] ) ) {
|
||||||
|
$order->update_meta_data(
|
||||||
|
'_bnb_guest_notes',
|
||||||
|
sanitize_textarea_field( wp_unslash( $_POST['bnb_guest_notes'] ) )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $_POST['bnb_arrival_time'] ) && ! empty( $_POST['bnb_arrival_time'] ) ) {
|
||||||
|
$order->update_meta_data(
|
||||||
|
'_bnb_arrival_time',
|
||||||
|
sanitize_text_field( wp_unslash( $_POST['bnb_arrival_time'] ) )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if cart contains BnB bookings.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function cart_has_bookings(): bool {
|
||||||
|
if ( ! WC()->cart ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ( WC()->cart->get_cart() as $cart_item ) {
|
||||||
|
if ( isset( $cart_item[ Manager::CART_ITEM_KEY ] ) ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get arrival time options.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private static function get_arrival_time_options(): array {
|
||||||
|
$options = array(
|
||||||
|
'' => __( 'Select arrival time (optional)', 'wp-bnb' ),
|
||||||
|
'0-2' => __( 'Early morning (00:00 - 02:00)', 'wp-bnb' ),
|
||||||
|
'2-6' => __( 'Night (02:00 - 06:00)', 'wp-bnb' ),
|
||||||
|
'6-10' => __( 'Morning (06:00 - 10:00)', 'wp-bnb' ),
|
||||||
|
'10-14' => __( 'Late morning (10:00 - 14:00)', 'wp-bnb' ),
|
||||||
|
'14-18' => __( 'Afternoon (14:00 - 18:00)', 'wp-bnb' ),
|
||||||
|
'18-22' => __( 'Evening (18:00 - 22:00)', 'wp-bnb' ),
|
||||||
|
'22-24' => __( 'Late evening (22:00 - 00:00)', 'wp-bnb' ),
|
||||||
|
);
|
||||||
|
|
||||||
|
return $options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find guest by email address.
|
||||||
|
*
|
||||||
|
* @param string $email Email address.
|
||||||
|
* @return \WP_Post|null Guest post or null.
|
||||||
|
*/
|
||||||
|
private static function find_guest_by_email( string $email ): ?\WP_Post {
|
||||||
|
$guests = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Guest::POST_TYPE,
|
||||||
|
'posts_per_page' => 1,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'meta_query' => array(
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_guest_email',
|
||||||
|
'value' => $email,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return $guests[0] ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
633
src/Integration/WooCommerce/InvoiceGenerator.php
Normal file
633
src/Integration/WooCommerce/InvoiceGenerator.php
Normal file
@@ -0,0 +1,633 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* WooCommerce Invoice Generator.
|
||||||
|
*
|
||||||
|
* Generates PDF invoices for WooCommerce orders.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Integration\WooCommerce
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Integration\WooCommerce;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
use Magdev\WpBnb\Pricing\Calculator;
|
||||||
|
use Mpdf\Mpdf;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoice Generator class.
|
||||||
|
*
|
||||||
|
* Creates PDF invoices for bookings using mPDF.
|
||||||
|
*/
|
||||||
|
final class InvoiceGenerator {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoice storage directory relative to uploads.
|
||||||
|
*/
|
||||||
|
private const INVOICE_DIR = 'wp-bnb-invoices';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize invoice generator.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function init(): void {
|
||||||
|
// Attach invoice to order emails.
|
||||||
|
add_filter( 'woocommerce_email_attachments', array( self::class, 'attach_invoice_to_email' ), 10, 4 );
|
||||||
|
|
||||||
|
// Add admin order action button.
|
||||||
|
add_action( 'woocommerce_admin_order_actions_end', array( self::class, 'add_order_action_button' ) );
|
||||||
|
|
||||||
|
// AJAX handler for invoice generation.
|
||||||
|
add_action( 'wp_ajax_wp_bnb_generate_invoice', array( self::class, 'ajax_generate_invoice' ) );
|
||||||
|
|
||||||
|
// Handle invoice download.
|
||||||
|
add_action( 'init', array( self::class, 'handle_invoice_download' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate invoice for an order.
|
||||||
|
*
|
||||||
|
* @param \WC_Order $order WooCommerce order.
|
||||||
|
* @return string|null Invoice file path or null on failure.
|
||||||
|
*/
|
||||||
|
public static function generate_invoice( \WC_Order $order ): ?string {
|
||||||
|
/**
|
||||||
|
* Fires before generating an invoice.
|
||||||
|
*
|
||||||
|
* @param \WC_Order $order WooCommerce order.
|
||||||
|
*/
|
||||||
|
do_action( 'wp_bnb_wc_before_invoice_generate', $order );
|
||||||
|
|
||||||
|
// Get or generate invoice number.
|
||||||
|
$invoice_number = self::get_invoice_number( $order );
|
||||||
|
|
||||||
|
// Get booking data.
|
||||||
|
$booking_id = Manager::get_booking_for_order( $order );
|
||||||
|
|
||||||
|
// Generate HTML content.
|
||||||
|
$html = self::get_invoice_html( $order, $invoice_number, $booking_id );
|
||||||
|
|
||||||
|
// Generate PDF.
|
||||||
|
try {
|
||||||
|
$temp_dir = get_temp_dir() . 'mpdf';
|
||||||
|
if ( ! file_exists( $temp_dir ) ) {
|
||||||
|
wp_mkdir_p( $temp_dir );
|
||||||
|
}
|
||||||
|
|
||||||
|
$mpdf = new Mpdf(
|
||||||
|
array(
|
||||||
|
'mode' => 'utf-8',
|
||||||
|
'format' => 'A4',
|
||||||
|
'margin_left' => 15,
|
||||||
|
'margin_right' => 15,
|
||||||
|
'margin_top' => 15,
|
||||||
|
'margin_bottom' => 20,
|
||||||
|
'tempDir' => $temp_dir,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$mpdf->SetTitle( sprintf( 'Invoice %s', $invoice_number ) );
|
||||||
|
$mpdf->SetAuthor( get_option( 'wp_bnb_business_name', get_bloginfo( 'name' ) ) );
|
||||||
|
$mpdf->SetCreator( 'WP BnB' );
|
||||||
|
|
||||||
|
$mpdf->WriteHTML( $html );
|
||||||
|
|
||||||
|
// Save to file.
|
||||||
|
$file_path = self::get_invoice_path( $order, $invoice_number );
|
||||||
|
$mpdf->Output( $file_path, 'F' );
|
||||||
|
|
||||||
|
// Store invoice number and path in order meta.
|
||||||
|
$order->update_meta_data( '_bnb_invoice_number', $invoice_number );
|
||||||
|
$order->update_meta_data( '_bnb_invoice_path', $file_path );
|
||||||
|
$order->update_meta_data( '_bnb_invoice_date', current_time( 'mysql' ) );
|
||||||
|
$order->save();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires after generating an invoice.
|
||||||
|
*
|
||||||
|
* @param \WC_Order $order WooCommerce order.
|
||||||
|
* @param string $file_path Invoice file path.
|
||||||
|
*/
|
||||||
|
do_action( 'wp_bnb_wc_after_invoice_generate', $order, $file_path );
|
||||||
|
|
||||||
|
return $file_path;
|
||||||
|
|
||||||
|
} catch ( \Exception $e ) {
|
||||||
|
error_log( 'WP BnB Invoice generation failed: ' . $e->getMessage() );
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get invoice number for an order.
|
||||||
|
*
|
||||||
|
* @param \WC_Order $order WooCommerce order.
|
||||||
|
* @return string Invoice number.
|
||||||
|
*/
|
||||||
|
public static function get_invoice_number( \WC_Order $order ): string {
|
||||||
|
// Check if already has invoice number.
|
||||||
|
$existing = $order->get_meta( '_bnb_invoice_number', true );
|
||||||
|
|
||||||
|
if ( $existing ) {
|
||||||
|
return $existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new invoice number.
|
||||||
|
return Manager::get_next_invoice_number();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get invoice file path.
|
||||||
|
*
|
||||||
|
* @param \WC_Order $order WooCommerce order.
|
||||||
|
* @param string $invoice_number Invoice number.
|
||||||
|
* @return string File path.
|
||||||
|
*/
|
||||||
|
private static function get_invoice_path( \WC_Order $order, string $invoice_number ): string {
|
||||||
|
$upload_dir = wp_upload_dir();
|
||||||
|
$invoice_dir = $upload_dir['basedir'] . '/' . self::INVOICE_DIR;
|
||||||
|
|
||||||
|
// Create directory if needed.
|
||||||
|
if ( ! file_exists( $invoice_dir ) ) {
|
||||||
|
wp_mkdir_p( $invoice_dir );
|
||||||
|
|
||||||
|
// Add .htaccess to protect invoices.
|
||||||
|
$htaccess = $invoice_dir . '/.htaccess';
|
||||||
|
if ( ! file_exists( $htaccess ) ) {
|
||||||
|
file_put_contents( $htaccess, 'Deny from all' );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add index.php for extra protection.
|
||||||
|
$index = $invoice_dir . '/index.php';
|
||||||
|
if ( ! file_exists( $index ) ) {
|
||||||
|
file_put_contents( $index, '<?php // Silence is golden.' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize invoice number for filename.
|
||||||
|
$safe_number = sanitize_file_name( $invoice_number );
|
||||||
|
|
||||||
|
return $invoice_dir . '/invoice-' . $safe_number . '-' . $order->get_id() . '.pdf';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if invoice exists for an order.
|
||||||
|
*
|
||||||
|
* @param \WC_Order $order WooCommerce order.
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function invoice_exists( \WC_Order $order ): bool {
|
||||||
|
$path = $order->get_meta( '_bnb_invoice_path', true );
|
||||||
|
|
||||||
|
return $path && file_exists( $path );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach invoice to email.
|
||||||
|
*
|
||||||
|
* @param array $attachments Attachments array.
|
||||||
|
* @param string $email_id Email ID.
|
||||||
|
* @param \WC_Order $order WooCommerce order.
|
||||||
|
* @param \WC_Email $email Email object.
|
||||||
|
* @return array Modified attachments.
|
||||||
|
*/
|
||||||
|
public static function attach_invoice_to_email( array $attachments, string $email_id, $order, $email ): array {
|
||||||
|
// Only attach to specific emails.
|
||||||
|
$allowed_emails = array(
|
||||||
|
'customer_completed_order',
|
||||||
|
'customer_processing_order',
|
||||||
|
'customer_invoice',
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( ! in_array( $email_id, $allowed_emails, true ) ) {
|
||||||
|
return $attachments;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if order is a WC_Order.
|
||||||
|
if ( ! $order instanceof \WC_Order ) {
|
||||||
|
return $attachments;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if invoice attachment is enabled.
|
||||||
|
if ( ! Manager::is_invoice_attach_enabled() ) {
|
||||||
|
return $attachments;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this order has a booking.
|
||||||
|
$booking_id = Manager::get_booking_for_order( $order );
|
||||||
|
|
||||||
|
if ( ! $booking_id ) {
|
||||||
|
return $attachments;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate invoice if it doesn't exist.
|
||||||
|
if ( ! self::invoice_exists( $order ) ) {
|
||||||
|
self::generate_invoice( $order );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get invoice path.
|
||||||
|
$invoice_path = $order->get_meta( '_bnb_invoice_path', true );
|
||||||
|
|
||||||
|
if ( $invoice_path && file_exists( $invoice_path ) ) {
|
||||||
|
$attachments[] = $invoice_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $attachments;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add order action button for invoice.
|
||||||
|
*
|
||||||
|
* @param \WC_Order $order WooCommerce order.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function add_order_action_button( \WC_Order $order ): void {
|
||||||
|
// Check if this order has a booking.
|
||||||
|
$booking_id = Manager::get_booking_for_order( $order );
|
||||||
|
|
||||||
|
if ( ! $booking_id ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate download URL.
|
||||||
|
$download_url = add_query_arg(
|
||||||
|
array(
|
||||||
|
'bnb_download_invoice' => $order->get_id(),
|
||||||
|
'_wpnonce' => wp_create_nonce( 'bnb_download_invoice_' . $order->get_id() ),
|
||||||
|
),
|
||||||
|
admin_url( 'admin.php' )
|
||||||
|
);
|
||||||
|
|
||||||
|
?>
|
||||||
|
<a class="button tips" href="<?php echo esc_url( $download_url ); ?>"
|
||||||
|
data-tip="<?php esc_attr_e( 'Download Invoice', 'wp-bnb' ); ?>">
|
||||||
|
<span class="dashicons dashicons-pdf" style="vertical-align: middle;"></span>
|
||||||
|
</a>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler for generating invoice.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function ajax_generate_invoice(): void {
|
||||||
|
check_ajax_referer( 'wp_bnb_admin_nonce', 'nonce' );
|
||||||
|
|
||||||
|
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||||
|
wp_send_json_error( array( 'message' => __( 'Permission denied.', 'wp-bnb' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$order_id = isset( $_POST['order_id'] ) ? absint( $_POST['order_id'] ) : 0;
|
||||||
|
|
||||||
|
if ( ! $order_id ) {
|
||||||
|
wp_send_json_error( array( 'message' => __( 'Invalid order ID.', 'wp-bnb' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$order = wc_get_order( $order_id );
|
||||||
|
|
||||||
|
if ( ! $order ) {
|
||||||
|
wp_send_json_error( array( 'message' => __( 'Order not found.', 'wp-bnb' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$file_path = self::generate_invoice( $order );
|
||||||
|
|
||||||
|
if ( $file_path ) {
|
||||||
|
wp_send_json_success( array( 'message' => __( 'Invoice generated successfully.', 'wp-bnb' ) ) );
|
||||||
|
} else {
|
||||||
|
wp_send_json_error( array( 'message' => __( 'Failed to generate invoice.', 'wp-bnb' ) ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle invoice download request.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function handle_invoice_download(): void {
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||||
|
if ( ! isset( $_GET['bnb_download_invoice'] ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$order_id = absint( $_GET['bnb_download_invoice'] );
|
||||||
|
|
||||||
|
// Verify nonce.
|
||||||
|
if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'bnb_download_invoice_' . $order_id ) ) {
|
||||||
|
wp_die( esc_html__( 'Security check failed.', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check capabilities.
|
||||||
|
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||||
|
wp_die( esc_html__( 'You do not have permission to download invoices.', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$order = wc_get_order( $order_id );
|
||||||
|
|
||||||
|
if ( ! $order ) {
|
||||||
|
wp_die( esc_html__( 'Order not found.', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate invoice if needed.
|
||||||
|
if ( ! self::invoice_exists( $order ) ) {
|
||||||
|
self::generate_invoice( $order );
|
||||||
|
}
|
||||||
|
|
||||||
|
$invoice_path = $order->get_meta( '_bnb_invoice_path', true );
|
||||||
|
|
||||||
|
if ( ! $invoice_path || ! file_exists( $invoice_path ) ) {
|
||||||
|
wp_die( esc_html__( 'Invoice not found.', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$invoice_number = $order->get_meta( '_bnb_invoice_number', true );
|
||||||
|
$filename = 'invoice-' . sanitize_file_name( $invoice_number ) . '.pdf';
|
||||||
|
|
||||||
|
// Output file.
|
||||||
|
header( 'Content-Type: application/pdf' );
|
||||||
|
header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
|
||||||
|
header( 'Content-Length: ' . filesize( $invoice_path ) );
|
||||||
|
header( 'Cache-Control: no-cache, no-store, must-revalidate' );
|
||||||
|
|
||||||
|
readfile( $invoice_path );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get invoice HTML content.
|
||||||
|
*
|
||||||
|
* @param \WC_Order $order WooCommerce order.
|
||||||
|
* @param string $invoice_number Invoice number.
|
||||||
|
* @param int|null $booking_id Booking ID.
|
||||||
|
* @return string HTML content.
|
||||||
|
*/
|
||||||
|
private static function get_invoice_html( \WC_Order $order, string $invoice_number, ?int $booking_id ): string {
|
||||||
|
// Get business info.
|
||||||
|
$business = self::get_business_info();
|
||||||
|
|
||||||
|
// Get booking details.
|
||||||
|
$check_in = '';
|
||||||
|
$check_out = '';
|
||||||
|
$room_name = '';
|
||||||
|
$building_name = '';
|
||||||
|
$nights = 0;
|
||||||
|
$guests = 0;
|
||||||
|
|
||||||
|
if ( $booking_id ) {
|
||||||
|
$check_in = get_post_meta( $booking_id, '_bnb_booking_check_in', true );
|
||||||
|
$check_out = get_post_meta( $booking_id, '_bnb_booking_check_out', true );
|
||||||
|
$guests = get_post_meta( $booking_id, '_bnb_booking_adults', true );
|
||||||
|
$room_id = get_post_meta( $booking_id, '_bnb_booking_room_id', true );
|
||||||
|
|
||||||
|
if ( $room_id ) {
|
||||||
|
$room = get_post( $room_id );
|
||||||
|
$room_name = $room ? $room->post_title : '';
|
||||||
|
$building = Room::get_building( $room_id );
|
||||||
|
$building_name = $building ? $building->post_title : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $check_in && $check_out ) {
|
||||||
|
$check_in_date = \DateTime::createFromFormat( 'Y-m-d', $check_in );
|
||||||
|
$check_out_date = \DateTime::createFromFormat( 'Y-m-d', $check_out );
|
||||||
|
if ( $check_in_date && $check_out_date ) {
|
||||||
|
$nights = $check_in_date->diff( $check_out_date )->days;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format dates.
|
||||||
|
$date_format = get_option( 'date_format' );
|
||||||
|
$check_in_display = $check_in ? date_i18n( $date_format, strtotime( $check_in ) ) : '';
|
||||||
|
$check_out_display = $check_out ? date_i18n( $date_format, strtotime( $check_out ) ) : '';
|
||||||
|
$order_date = $order->get_date_created() ? $order->get_date_created()->date_i18n( $date_format ) : '';
|
||||||
|
|
||||||
|
// Get logo.
|
||||||
|
$logo_html = '';
|
||||||
|
$logo_id = Manager::get_invoice_logo();
|
||||||
|
if ( $logo_id ) {
|
||||||
|
$logo_url = wp_get_attachment_url( $logo_id );
|
||||||
|
if ( $logo_url ) {
|
||||||
|
$logo_html = '<img src="' . esc_url( $logo_url ) . '" style="max-height: 60px; max-width: 200px;">';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get footer.
|
||||||
|
$footer_text = Manager::get_invoice_footer();
|
||||||
|
|
||||||
|
// Build HTML.
|
||||||
|
$html = '<html><head><style>' . self::get_invoice_css() . '</style></head><body>';
|
||||||
|
|
||||||
|
// Header.
|
||||||
|
$html .= '<div class="invoice-header">';
|
||||||
|
$html .= '<div class="logo">' . $logo_html . '</div>';
|
||||||
|
$html .= '<div class="invoice-title">';
|
||||||
|
$html .= '<h1>' . esc_html__( 'INVOICE', 'wp-bnb' ) . '</h1>';
|
||||||
|
$html .= '<p class="invoice-number">' . esc_html( $invoice_number ) . '</p>';
|
||||||
|
$html .= '<p class="invoice-date">' . esc_html( $order_date ) . '</p>';
|
||||||
|
$html .= '</div>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
// Addresses.
|
||||||
|
$html .= '<div class="addresses">';
|
||||||
|
|
||||||
|
// From.
|
||||||
|
$html .= '<div class="address from">';
|
||||||
|
$html .= '<h3>' . esc_html__( 'From', 'wp-bnb' ) . '</h3>';
|
||||||
|
$html .= '<p><strong>' . esc_html( $business['name'] ) . '</strong></p>';
|
||||||
|
if ( $business['street'] ) {
|
||||||
|
$html .= '<p>' . esc_html( $business['street'] ) . '</p>';
|
||||||
|
}
|
||||||
|
if ( $business['city'] || $business['postal'] ) {
|
||||||
|
$html .= '<p>' . esc_html( $business['postal'] . ' ' . $business['city'] ) . '</p>';
|
||||||
|
}
|
||||||
|
if ( $business['country'] ) {
|
||||||
|
$html .= '<p>' . esc_html( $business['country'] ) . '</p>';
|
||||||
|
}
|
||||||
|
if ( $business['email'] ) {
|
||||||
|
$html .= '<p>' . esc_html( $business['email'] ) . '</p>';
|
||||||
|
}
|
||||||
|
if ( $business['phone'] ) {
|
||||||
|
$html .= '<p>' . esc_html( $business['phone'] ) . '</p>';
|
||||||
|
}
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
// To.
|
||||||
|
$html .= '<div class="address to">';
|
||||||
|
$html .= '<h3>' . esc_html__( 'Bill To', 'wp-bnb' ) . '</h3>';
|
||||||
|
$html .= '<p><strong>' . esc_html( $order->get_billing_first_name() . ' ' . $order->get_billing_last_name() ) . '</strong></p>';
|
||||||
|
if ( $order->get_billing_address_1() ) {
|
||||||
|
$html .= '<p>' . esc_html( $order->get_billing_address_1() ) . '</p>';
|
||||||
|
}
|
||||||
|
if ( $order->get_billing_city() || $order->get_billing_postcode() ) {
|
||||||
|
$html .= '<p>' . esc_html( $order->get_billing_postcode() . ' ' . $order->get_billing_city() ) . '</p>';
|
||||||
|
}
|
||||||
|
if ( $order->get_billing_country() ) {
|
||||||
|
$html .= '<p>' . esc_html( WC()->countries->countries[ $order->get_billing_country() ] ?? $order->get_billing_country() ) . '</p>';
|
||||||
|
}
|
||||||
|
if ( $order->get_billing_email() ) {
|
||||||
|
$html .= '<p>' . esc_html( $order->get_billing_email() ) . '</p>';
|
||||||
|
}
|
||||||
|
$html .= '</div>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
// Booking details.
|
||||||
|
if ( $booking_id && $room_name ) {
|
||||||
|
$html .= '<div class="booking-details">';
|
||||||
|
$html .= '<h3>' . esc_html__( 'Booking Details', 'wp-bnb' ) . '</h3>';
|
||||||
|
$html .= '<table class="details-table">';
|
||||||
|
$html .= '<tr><td><strong>' . esc_html__( 'Room', 'wp-bnb' ) . '</strong></td><td>' . esc_html( $room_name );
|
||||||
|
if ( $building_name ) {
|
||||||
|
$html .= ' <small>(' . esc_html( $building_name ) . ')</small>';
|
||||||
|
}
|
||||||
|
$html .= '</td></tr>';
|
||||||
|
$html .= '<tr><td><strong>' . esc_html__( 'Check-in', 'wp-bnb' ) . '</strong></td><td>' . esc_html( $check_in_display ) . '</td></tr>';
|
||||||
|
$html .= '<tr><td><strong>' . esc_html__( 'Check-out', 'wp-bnb' ) . '</strong></td><td>' . esc_html( $check_out_display ) . '</td></tr>';
|
||||||
|
$html .= '<tr><td><strong>' . esc_html__( 'Nights', 'wp-bnb' ) . '</strong></td><td>' . esc_html( $nights ) . '</td></tr>';
|
||||||
|
$html .= '<tr><td><strong>' . esc_html__( 'Guests', 'wp-bnb' ) . '</strong></td><td>' . esc_html( $guests ) . '</td></tr>';
|
||||||
|
$html .= '</table>';
|
||||||
|
$html .= '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Line items.
|
||||||
|
$html .= '<div class="line-items">';
|
||||||
|
$html .= '<table class="items-table">';
|
||||||
|
$html .= '<thead><tr>';
|
||||||
|
$html .= '<th class="description">' . esc_html__( 'Description', 'wp-bnb' ) . '</th>';
|
||||||
|
$html .= '<th class="qty">' . esc_html__( 'Qty', 'wp-bnb' ) . '</th>';
|
||||||
|
$html .= '<th class="price">' . esc_html__( 'Price', 'wp-bnb' ) . '</th>';
|
||||||
|
$html .= '<th class="total">' . esc_html__( 'Total', 'wp-bnb' ) . '</th>';
|
||||||
|
$html .= '</tr></thead>';
|
||||||
|
$html .= '<tbody>';
|
||||||
|
|
||||||
|
foreach ( $order->get_items() as $item ) {
|
||||||
|
$qty = $item->get_quantity();
|
||||||
|
$total = $item->get_total();
|
||||||
|
$price = $qty > 0 ? $total / $qty : $total;
|
||||||
|
|
||||||
|
$html .= '<tr>';
|
||||||
|
$html .= '<td class="description">' . esc_html( $item->get_name() ) . '</td>';
|
||||||
|
$html .= '<td class="qty">' . esc_html( $qty ) . '</td>';
|
||||||
|
$html .= '<td class="price">' . wc_price( $price ) . '</td>';
|
||||||
|
$html .= '<td class="total">' . wc_price( $total ) . '</td>';
|
||||||
|
$html .= '</tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$html .= '</tbody>';
|
||||||
|
$html .= '</table>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
// Totals.
|
||||||
|
$html .= '<div class="totals">';
|
||||||
|
$html .= '<table class="totals-table">';
|
||||||
|
$html .= '<tr><td>' . esc_html__( 'Subtotal', 'wp-bnb' ) . '</td><td>' . wc_price( $order->get_subtotal() ) . '</td></tr>';
|
||||||
|
|
||||||
|
if ( $order->get_total_tax() > 0 ) {
|
||||||
|
$html .= '<tr><td>' . esc_html__( 'Tax', 'wp-bnb' ) . '</td><td>' . wc_price( $order->get_total_tax() ) . '</td></tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$html .= '<tr class="grand-total"><td><strong>' . esc_html__( 'Total', 'wp-bnb' ) . '</strong></td><td><strong>' . wc_price( $order->get_total() ) . '</strong></td></tr>';
|
||||||
|
$html .= '</table>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
// Payment info.
|
||||||
|
$html .= '<div class="payment-info">';
|
||||||
|
$html .= '<p><strong>' . esc_html__( 'Payment Status:', 'wp-bnb' ) . '</strong> ';
|
||||||
|
|
||||||
|
if ( $order->is_paid() ) {
|
||||||
|
$html .= '<span class="paid">' . esc_html__( 'PAID', 'wp-bnb' ) . '</span>';
|
||||||
|
} else {
|
||||||
|
$html .= '<span class="unpaid">' . esc_html__( 'PENDING', 'wp-bnb' ) . '</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$html .= '</p>';
|
||||||
|
$html .= '<p><strong>' . esc_html__( 'Payment Method:', 'wp-bnb' ) . '</strong> ' . esc_html( $order->get_payment_method_title() ) . '</p>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
// Footer.
|
||||||
|
$html .= '<div class="footer">';
|
||||||
|
$html .= '<p>' . esc_html__( 'Thank you for your stay!', 'wp-bnb' ) . '</p>';
|
||||||
|
if ( $footer_text ) {
|
||||||
|
$html .= '<p class="custom-footer">' . esc_html( $footer_text ) . '</p>';
|
||||||
|
}
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
$html .= '</body></html>';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter the invoice HTML.
|
||||||
|
*
|
||||||
|
* @param string $html Invoice HTML.
|
||||||
|
* @param \WC_Order $order WooCommerce order.
|
||||||
|
*/
|
||||||
|
return apply_filters( 'wp_bnb_wc_invoice_html', $html, $order );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get invoice CSS styles.
|
||||||
|
*
|
||||||
|
* @return string CSS content.
|
||||||
|
*/
|
||||||
|
private static function get_invoice_css(): string {
|
||||||
|
return '
|
||||||
|
body { font-family: DejaVu Sans, sans-serif; font-size: 10pt; color: #333; line-height: 1.4; }
|
||||||
|
h1 { font-size: 24pt; color: #2271b1; margin: 0; text-align: right; }
|
||||||
|
h3 { font-size: 11pt; color: #50575e; margin: 15pt 0 5pt 0; }
|
||||||
|
|
||||||
|
.invoice-header { margin-bottom: 30pt; overflow: hidden; }
|
||||||
|
.logo { float: left; width: 50%; }
|
||||||
|
.invoice-title { float: right; width: 50%; text-align: right; }
|
||||||
|
.invoice-number { font-size: 14pt; font-weight: bold; color: #333; margin: 5pt 0; }
|
||||||
|
.invoice-date { font-size: 10pt; color: #787c82; margin: 0; }
|
||||||
|
|
||||||
|
.addresses { margin-bottom: 20pt; overflow: hidden; }
|
||||||
|
.address { width: 48%; }
|
||||||
|
.address.from { float: left; }
|
||||||
|
.address.to { float: right; }
|
||||||
|
.address p { margin: 2pt 0; font-size: 9pt; }
|
||||||
|
|
||||||
|
.booking-details { margin-bottom: 20pt; background: #f9f9f9; padding: 10pt; border-radius: 4pt; }
|
||||||
|
.details-table { width: 100%; border-collapse: collapse; }
|
||||||
|
.details-table td { padding: 4pt 8pt; font-size: 9pt; border-bottom: 1px solid #e1e4e8; }
|
||||||
|
.details-table td:first-child { width: 120pt; }
|
||||||
|
|
||||||
|
.line-items { margin-bottom: 20pt; }
|
||||||
|
.items-table { width: 100%; border-collapse: collapse; }
|
||||||
|
.items-table th { background: #f6f7f7; text-align: left; padding: 8pt; font-size: 9pt; border-bottom: 2px solid #c3c4c7; }
|
||||||
|
.items-table td { padding: 8pt; font-size: 9pt; border-bottom: 1px solid #e1e4e8; }
|
||||||
|
.items-table .qty, .items-table .price, .items-table .total { text-align: right; width: 70pt; }
|
||||||
|
|
||||||
|
.totals { margin-bottom: 20pt; }
|
||||||
|
.totals-table { width: 250pt; margin-left: auto; border-collapse: collapse; }
|
||||||
|
.totals-table td { padding: 6pt 8pt; font-size: 10pt; }
|
||||||
|
.totals-table td:last-child { text-align: right; }
|
||||||
|
.totals-table .grand-total td { border-top: 2px solid #333; font-size: 12pt; }
|
||||||
|
|
||||||
|
.payment-info { margin-bottom: 30pt; padding: 10pt; background: #f9f9f9; border-radius: 4pt; }
|
||||||
|
.payment-info p { margin: 4pt 0; font-size: 9pt; }
|
||||||
|
.payment-info .paid { color: #00a32a; font-weight: bold; }
|
||||||
|
.payment-info .unpaid { color: #dba617; font-weight: bold; }
|
||||||
|
|
||||||
|
.footer { text-align: center; margin-top: 40pt; padding-top: 15pt; border-top: 1px solid #c3c4c7; }
|
||||||
|
.footer p { font-size: 9pt; color: #787c82; margin: 3pt 0; }
|
||||||
|
.custom-footer { font-size: 8pt; }
|
||||||
|
';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get business information from settings.
|
||||||
|
*
|
||||||
|
* @return array Business info.
|
||||||
|
*/
|
||||||
|
private static function get_business_info(): array {
|
||||||
|
return array(
|
||||||
|
'name' => get_option( 'wp_bnb_business_name', get_bloginfo( 'name' ) ),
|
||||||
|
'street' => get_option( 'wp_bnb_address_street', '' ),
|
||||||
|
'city' => get_option( 'wp_bnb_address_city', '' ),
|
||||||
|
'postal' => get_option( 'wp_bnb_address_postal', '' ),
|
||||||
|
'country' => get_option( 'wp_bnb_address_country', '' ),
|
||||||
|
'email' => get_option( 'wp_bnb_contact_email', get_option( 'admin_email' ) ),
|
||||||
|
'phone' => get_option( 'wp_bnb_contact_phone', '' ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
435
src/Integration/WooCommerce/Manager.php
Normal file
435
src/Integration/WooCommerce/Manager.php
Normal file
@@ -0,0 +1,435 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* WooCommerce Integration Manager.
|
||||||
|
*
|
||||||
|
* Main integration class that initializes WooCommerce features.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Integration\WooCommerce
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Integration\WooCommerce;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WooCommerce Integration Manager class.
|
||||||
|
*
|
||||||
|
* Manages the integration with WooCommerce for payment processing,
|
||||||
|
* invoice generation, order management, and refund handling.
|
||||||
|
*/
|
||||||
|
final class Manager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Option key for enabling WooCommerce integration.
|
||||||
|
*/
|
||||||
|
public const OPTION_ENABLED = 'wp_bnb_wc_enabled';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Option key for auto-syncing products.
|
||||||
|
*/
|
||||||
|
public const OPTION_AUTO_SYNC = 'wp_bnb_wc_auto_sync_products';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Option key for auto-confirming bookings on payment.
|
||||||
|
*/
|
||||||
|
public const OPTION_AUTO_CONFIRM = 'wp_bnb_wc_auto_confirm_booking';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Option key for attaching invoices to emails.
|
||||||
|
*/
|
||||||
|
public const OPTION_INVOICE_ATTACH = 'wp_bnb_wc_invoice_attach';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Option key for product category.
|
||||||
|
*/
|
||||||
|
public const OPTION_PRODUCT_CATEGORY = 'wp_bnb_wc_product_category';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Option key for services as products.
|
||||||
|
*/
|
||||||
|
public const OPTION_SERVICES_AS_PRODUCTS = 'wp_bnb_wc_services_as_products';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Option key for invoice number prefix.
|
||||||
|
*/
|
||||||
|
public const OPTION_INVOICE_PREFIX = 'wp_bnb_invoice_prefix';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Option key for invoice starting number.
|
||||||
|
*/
|
||||||
|
public const OPTION_INVOICE_START_NUMBER = 'wp_bnb_invoice_start_number';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Option key for last invoice number.
|
||||||
|
*/
|
||||||
|
public const OPTION_INVOICE_LAST_NUMBER = 'wp_bnb_invoice_last_number';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Option key for invoice logo.
|
||||||
|
*/
|
||||||
|
public const OPTION_INVOICE_LOGO = 'wp_bnb_invoice_logo';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Option key for invoice footer text.
|
||||||
|
*/
|
||||||
|
public const OPTION_INVOICE_FOOTER = 'wp_bnb_invoice_footer';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Order meta key for linked booking ID.
|
||||||
|
*/
|
||||||
|
public const ORDER_BOOKING_META = '_bnb_booking_id';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Order meta key for linked room ID.
|
||||||
|
*/
|
||||||
|
public const ORDER_ROOM_META = '_bnb_room_id';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Booking meta key for linked WC order ID.
|
||||||
|
*/
|
||||||
|
public const BOOKING_ORDER_META = '_bnb_booking_wc_order_id';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Room meta key for linked WC product ID.
|
||||||
|
*/
|
||||||
|
public const ROOM_PRODUCT_META = '_bnb_wc_product_id';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Product meta key for linked room ID.
|
||||||
|
*/
|
||||||
|
public const PRODUCT_ROOM_META = '_bnb_room_id';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cart item data key for booking data.
|
||||||
|
*/
|
||||||
|
public const CART_ITEM_KEY = 'bnb_booking_data';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether components have been initialized.
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
private static bool $initialized = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the WooCommerce integration.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function init(): void {
|
||||||
|
// Prevent double initialization.
|
||||||
|
if ( self::$initialized ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only proceed if WooCommerce is active.
|
||||||
|
if ( ! self::is_wc_active() ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Declare HPOS compatibility.
|
||||||
|
add_action( 'before_woocommerce_init', array( self::class, 'declare_hpos_compatibility' ) );
|
||||||
|
|
||||||
|
// Only initialize components if integration is enabled.
|
||||||
|
if ( ! self::is_enabled() ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize sub-components after WooCommerce is fully loaded.
|
||||||
|
add_action( 'woocommerce_loaded', array( self::class, 'init_components' ) );
|
||||||
|
|
||||||
|
// Add admin notice if WooCommerce deactivated while integration enabled.
|
||||||
|
add_action( 'admin_notices', array( self::class, 'admin_notices' ) );
|
||||||
|
|
||||||
|
self::$initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Declare High-Performance Order Storage (HPOS) compatibility.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function declare_hpos_compatibility(): void {
|
||||||
|
if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) {
|
||||||
|
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility(
|
||||||
|
'custom_order_tables',
|
||||||
|
WP_BNB_PATH . 'wp-bnb.php',
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize all WooCommerce integration components.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function init_components(): void {
|
||||||
|
// Product synchronization (room <-> WC product).
|
||||||
|
ProductSync::init();
|
||||||
|
|
||||||
|
// Cart handling (booking data, availability, pricing).
|
||||||
|
CartHandler::init();
|
||||||
|
|
||||||
|
// Checkout customization.
|
||||||
|
CheckoutHandler::init();
|
||||||
|
|
||||||
|
// Order handling (booking creation on payment).
|
||||||
|
OrderHandler::init();
|
||||||
|
|
||||||
|
// Invoice generation.
|
||||||
|
InvoiceGenerator::init();
|
||||||
|
|
||||||
|
// Refund handling.
|
||||||
|
RefundHandler::init();
|
||||||
|
|
||||||
|
// Admin column enhancements.
|
||||||
|
if ( is_admin() ) {
|
||||||
|
AdminColumns::init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if WooCommerce is active.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function is_wc_active(): bool {
|
||||||
|
return class_exists( 'WooCommerce' ) || class_exists( 'WC_Product' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if WooCommerce integration is enabled.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function is_enabled(): bool {
|
||||||
|
return 'yes' === get_option( self::OPTION_ENABLED, 'no' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable WooCommerce integration.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function enable(): void {
|
||||||
|
update_option( self::OPTION_ENABLED, 'yes' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable WooCommerce integration.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function disable(): void {
|
||||||
|
update_option( self::OPTION_ENABLED, 'no' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if auto-sync products is enabled.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function is_auto_sync_enabled(): bool {
|
||||||
|
return 'yes' === get_option( self::OPTION_AUTO_SYNC, 'yes' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if auto-confirm booking is enabled.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function is_auto_confirm_enabled(): bool {
|
||||||
|
return 'yes' === get_option( self::OPTION_AUTO_CONFIRM, 'yes' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if invoice attachment is enabled.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function is_invoice_attach_enabled(): bool {
|
||||||
|
return 'yes' === get_option( self::OPTION_INVOICE_ATTACH, 'yes' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the WooCommerce product category for rooms.
|
||||||
|
*
|
||||||
|
* @return int Product category term ID, or 0 if not set.
|
||||||
|
*/
|
||||||
|
public static function get_product_category(): int {
|
||||||
|
return absint( get_option( self::OPTION_PRODUCT_CATEGORY, 0 ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get invoice number prefix.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function get_invoice_prefix(): string {
|
||||||
|
return get_option( self::OPTION_INVOICE_PREFIX, 'INV-' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get invoice starting number.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public static function get_invoice_start_number(): int {
|
||||||
|
return absint( get_option( self::OPTION_INVOICE_START_NUMBER, 1000 ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get and increment the next invoice number.
|
||||||
|
*
|
||||||
|
* @return string The next invoice number with prefix.
|
||||||
|
*/
|
||||||
|
public static function get_next_invoice_number(): string {
|
||||||
|
$last_number = absint( get_option( self::OPTION_INVOICE_LAST_NUMBER, 0 ) );
|
||||||
|
$start_number = self::get_invoice_start_number();
|
||||||
|
|
||||||
|
// Use start number if no invoices generated yet.
|
||||||
|
$next_number = ( 0 === $last_number ) ? $start_number : $last_number + 1;
|
||||||
|
|
||||||
|
// Update the last number.
|
||||||
|
update_option( self::OPTION_INVOICE_LAST_NUMBER, $next_number );
|
||||||
|
|
||||||
|
return self::get_invoice_prefix() . str_pad( (string) $next_number, 5, '0', STR_PAD_LEFT );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get invoice logo attachment ID.
|
||||||
|
*
|
||||||
|
* @return int Attachment ID or 0.
|
||||||
|
*/
|
||||||
|
public static function get_invoice_logo(): int {
|
||||||
|
return absint( get_option( self::OPTION_INVOICE_LOGO, 0 ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get invoice footer text.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function get_invoice_footer(): string {
|
||||||
|
return get_option( self::OPTION_INVOICE_FOOTER, '' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display admin notices.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function admin_notices(): void {
|
||||||
|
// Show notice if integration enabled but WC not active.
|
||||||
|
if ( self::is_enabled() && ! self::is_wc_active() ) {
|
||||||
|
?>
|
||||||
|
<div class="notice notice-warning is-dismissible">
|
||||||
|
<p>
|
||||||
|
<strong><?php esc_html_e( 'WP BnB:', 'wp-bnb' ); ?></strong>
|
||||||
|
<?php esc_html_e( 'WooCommerce integration is enabled but WooCommerce is not active. Please install and activate WooCommerce or disable the integration in WP BnB settings.', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map WooCommerce order status to booking status.
|
||||||
|
*
|
||||||
|
* @param string $wc_status WooCommerce order status (without 'wc-' prefix).
|
||||||
|
* @return string Booking status.
|
||||||
|
*/
|
||||||
|
public static function map_wc_status_to_booking( string $wc_status ): string {
|
||||||
|
$map = array(
|
||||||
|
'pending' => 'pending',
|
||||||
|
'on-hold' => 'pending',
|
||||||
|
'processing' => self::is_auto_confirm_enabled() ? 'confirmed' : 'pending',
|
||||||
|
'completed' => 'confirmed',
|
||||||
|
'cancelled' => 'cancelled',
|
||||||
|
'refunded' => 'cancelled',
|
||||||
|
'failed' => 'cancelled',
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter the WooCommerce to booking status mapping.
|
||||||
|
*
|
||||||
|
* @param array $map Status mapping array.
|
||||||
|
*/
|
||||||
|
$map = apply_filters( 'wp_bnb_wc_status_map', $map );
|
||||||
|
|
||||||
|
return $map[ $wc_status ] ?? 'pending';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map booking status to WooCommerce order status.
|
||||||
|
*
|
||||||
|
* @param string $booking_status Booking status.
|
||||||
|
* @return string WooCommerce order status (without 'wc-' prefix).
|
||||||
|
*/
|
||||||
|
public static function map_booking_status_to_wc( string $booking_status ): string {
|
||||||
|
$map = array(
|
||||||
|
'pending' => 'on-hold',
|
||||||
|
'confirmed' => 'processing',
|
||||||
|
'checked_in' => 'processing',
|
||||||
|
'checked_out' => 'completed',
|
||||||
|
'cancelled' => 'cancelled',
|
||||||
|
);
|
||||||
|
|
||||||
|
return $map[ $booking_status ] ?? 'on-hold';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get WooCommerce order for a booking.
|
||||||
|
*
|
||||||
|
* @param int $booking_id Booking post ID.
|
||||||
|
* @return \WC_Order|null WooCommerce order or null.
|
||||||
|
*/
|
||||||
|
public static function get_order_for_booking( int $booking_id ): ?\WC_Order {
|
||||||
|
$order_id = get_post_meta( $booking_id, self::BOOKING_ORDER_META, true );
|
||||||
|
|
||||||
|
if ( ! $order_id ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$order = wc_get_order( $order_id );
|
||||||
|
|
||||||
|
return $order instanceof \WC_Order ? $order : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get booking ID for a WooCommerce order.
|
||||||
|
*
|
||||||
|
* @param \WC_Order|int $order WooCommerce order or order ID.
|
||||||
|
* @return int|null Booking post ID or null.
|
||||||
|
*/
|
||||||
|
public static function get_booking_for_order( $order ): ?int {
|
||||||
|
if ( is_int( $order ) ) {
|
||||||
|
$order = wc_get_order( $order );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! $order instanceof \WC_Order ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$booking_id = $order->get_meta( self::ORDER_BOOKING_META, true );
|
||||||
|
|
||||||
|
return $booking_id ? absint( $booking_id ) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Link a booking to a WooCommerce order (bidirectional).
|
||||||
|
*
|
||||||
|
* @param int $booking_id Booking post ID.
|
||||||
|
* @param \WC_Order $order WooCommerce order.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function link_booking_to_order( int $booking_id, \WC_Order $order ): void {
|
||||||
|
// Store order ID in booking meta.
|
||||||
|
update_post_meta( $booking_id, self::BOOKING_ORDER_META, $order->get_id() );
|
||||||
|
|
||||||
|
// Store booking ID in order meta (HPOS compatible).
|
||||||
|
$order->update_meta_data( self::ORDER_BOOKING_META, $booking_id );
|
||||||
|
$order->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
584
src/Integration/WooCommerce/OrderHandler.php
Normal file
584
src/Integration/WooCommerce/OrderHandler.php
Normal file
@@ -0,0 +1,584 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* WooCommerce Order Handler.
|
||||||
|
*
|
||||||
|
* Handles order-to-booking creation and status synchronization.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Integration\WooCommerce
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Integration\WooCommerce;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\Booking\EmailNotifier;
|
||||||
|
use Magdev\WpBnb\PostTypes\Booking;
|
||||||
|
use Magdev\WpBnb\PostTypes\Guest;
|
||||||
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
use Magdev\WpBnb\Pricing\Calculator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Order Handler class.
|
||||||
|
*
|
||||||
|
* Creates bookings from WooCommerce orders on payment completion
|
||||||
|
* and synchronizes order/booking statuses.
|
||||||
|
*/
|
||||||
|
final class OrderHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag to prevent recursive status updates.
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
private static bool $updating_status = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize order handler.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function init(): void {
|
||||||
|
// Create booking on payment completion.
|
||||||
|
add_action( 'woocommerce_payment_complete', array( self::class, 'on_payment_complete' ) );
|
||||||
|
|
||||||
|
// Also handle manual order status changes to processing/completed.
|
||||||
|
add_action( 'woocommerce_order_status_processing', array( self::class, 'on_order_processing' ) );
|
||||||
|
add_action( 'woocommerce_order_status_completed', array( self::class, 'on_order_completed' ) );
|
||||||
|
|
||||||
|
// Sync order status changes to booking.
|
||||||
|
add_action( 'woocommerce_order_status_changed', array( self::class, 'sync_order_status_to_booking' ), 10, 4 );
|
||||||
|
|
||||||
|
// Display booking info in admin order page.
|
||||||
|
add_action( 'woocommerce_admin_order_data_after_billing_address', array( self::class, 'display_booking_info_admin' ) );
|
||||||
|
|
||||||
|
// Display booking info on thank you page.
|
||||||
|
add_action( 'woocommerce_thankyou', array( self::class, 'display_booking_info_thankyou' ) );
|
||||||
|
|
||||||
|
// Display booking info in order details.
|
||||||
|
add_action( 'woocommerce_order_details_after_order_table', array( self::class, 'display_booking_info_order_details' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle payment completion.
|
||||||
|
*
|
||||||
|
* @param int $order_id Order ID.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function on_payment_complete( int $order_id ): void {
|
||||||
|
self::maybe_create_booking( $order_id );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle order status change to processing.
|
||||||
|
*
|
||||||
|
* @param int $order_id Order ID.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function on_order_processing( int $order_id ): void {
|
||||||
|
self::maybe_create_booking( $order_id );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle order status change to completed.
|
||||||
|
*
|
||||||
|
* @param int $order_id Order ID.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function on_order_completed( int $order_id ): void {
|
||||||
|
self::maybe_create_booking( $order_id );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create booking from order if not already created.
|
||||||
|
*
|
||||||
|
* @param int $order_id Order ID.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function maybe_create_booking( int $order_id ): void {
|
||||||
|
$order = wc_get_order( $order_id );
|
||||||
|
|
||||||
|
if ( ! $order instanceof \WC_Order ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if booking already created.
|
||||||
|
$existing_booking = Manager::get_booking_for_order( $order );
|
||||||
|
|
||||||
|
if ( $existing_booking ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if order has room bookings.
|
||||||
|
$has_bookings = false;
|
||||||
|
foreach ( $order->get_items() as $item ) {
|
||||||
|
$room_id = $item->get_meta( '_bnb_room_id', true );
|
||||||
|
if ( $room_id ) {
|
||||||
|
$has_bookings = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! $has_bookings ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create booking.
|
||||||
|
self::create_booking_from_order( $order );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create booking(s) from WooCommerce order.
|
||||||
|
*
|
||||||
|
* @param \WC_Order $order WooCommerce order.
|
||||||
|
* @return int|null Booking ID or null on failure.
|
||||||
|
*/
|
||||||
|
public static function create_booking_from_order( \WC_Order $order ): ?int {
|
||||||
|
/**
|
||||||
|
* Fires before creating a booking from an order.
|
||||||
|
*
|
||||||
|
* @param \WC_Order $order WooCommerce order.
|
||||||
|
*/
|
||||||
|
do_action( 'wp_bnb_wc_before_booking_from_order', $order );
|
||||||
|
|
||||||
|
// Find or create guest.
|
||||||
|
$guest_id = self::find_or_create_guest( $order );
|
||||||
|
|
||||||
|
// Get booking data from order items.
|
||||||
|
$booking_id = null;
|
||||||
|
|
||||||
|
foreach ( $order->get_items() as $item ) {
|
||||||
|
$room_id = $item->get_meta( '_bnb_room_id', true );
|
||||||
|
|
||||||
|
if ( ! $room_id ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$check_in = $item->get_meta( '_bnb_check_in', true );
|
||||||
|
$check_out = $item->get_meta( '_bnb_check_out', true );
|
||||||
|
$guests = $item->get_meta( '_bnb_guests', true );
|
||||||
|
$nights = $item->get_meta( '_bnb_nights', true );
|
||||||
|
$services = $item->get_meta( '_bnb_services', true );
|
||||||
|
$breakdown = $item->get_meta( '_bnb_price_breakdown', true );
|
||||||
|
|
||||||
|
// Decode JSON if necessary.
|
||||||
|
if ( is_string( $services ) ) {
|
||||||
|
$services = json_decode( $services, true ) ?: array();
|
||||||
|
}
|
||||||
|
if ( is_string( $breakdown ) ) {
|
||||||
|
$breakdown = json_decode( $breakdown, true ) ?: array();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine initial status.
|
||||||
|
$status = Manager::is_auto_confirm_enabled() ? 'confirmed' : 'pending';
|
||||||
|
|
||||||
|
// Get guest notes.
|
||||||
|
$guest_notes = $order->get_meta( '_bnb_guest_notes', true );
|
||||||
|
|
||||||
|
// Create booking post.
|
||||||
|
$booking_data = array(
|
||||||
|
'post_type' => Booking::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'post_title' => self::generate_booking_title( $guest_id, $check_in, $check_out ),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter the booking data before creation.
|
||||||
|
*
|
||||||
|
* @param array $booking_data Booking post data.
|
||||||
|
* @param \WC_Order $order WooCommerce order.
|
||||||
|
*/
|
||||||
|
$booking_data = apply_filters( 'wp_bnb_wc_booking_from_order_data', $booking_data, $order );
|
||||||
|
|
||||||
|
$booking_id = wp_insert_post( $booking_data );
|
||||||
|
|
||||||
|
if ( ! $booking_id || is_wp_error( $booking_id ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store booking meta.
|
||||||
|
update_post_meta( $booking_id, '_bnb_booking_room_id', $room_id );
|
||||||
|
update_post_meta( $booking_id, '_bnb_booking_check_in', $check_in );
|
||||||
|
update_post_meta( $booking_id, '_bnb_booking_check_out', $check_out );
|
||||||
|
update_post_meta( $booking_id, '_bnb_booking_status', $status );
|
||||||
|
update_post_meta( $booking_id, '_bnb_booking_adults', max( 1, (int) $guests ) );
|
||||||
|
update_post_meta( $booking_id, '_bnb_booking_children', 0 );
|
||||||
|
update_post_meta( $booking_id, '_bnb_booking_guest_notes', $guest_notes );
|
||||||
|
update_post_meta( $booking_id, '_bnb_booking_source', 'woocommerce_order_' . $order->get_id() );
|
||||||
|
|
||||||
|
// Store guest info.
|
||||||
|
if ( $guest_id ) {
|
||||||
|
update_post_meta( $booking_id, '_bnb_booking_guest_id', $guest_id );
|
||||||
|
update_post_meta( $booking_id, '_bnb_booking_guest_name', Guest::get_full_name( $guest_id ) );
|
||||||
|
update_post_meta( $booking_id, '_bnb_booking_guest_email', get_post_meta( $guest_id, '_bnb_guest_email', true ) );
|
||||||
|
update_post_meta( $booking_id, '_bnb_booking_guest_phone', get_post_meta( $guest_id, '_bnb_guest_phone', true ) );
|
||||||
|
} else {
|
||||||
|
// Use order billing info.
|
||||||
|
$guest_name = $order->get_billing_first_name() . ' ' . $order->get_billing_last_name();
|
||||||
|
update_post_meta( $booking_id, '_bnb_booking_guest_name', $guest_name );
|
||||||
|
update_post_meta( $booking_id, '_bnb_booking_guest_email', $order->get_billing_email() );
|
||||||
|
update_post_meta( $booking_id, '_bnb_booking_guest_phone', $order->get_billing_phone() );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store pricing.
|
||||||
|
$total = $item->get_total();
|
||||||
|
update_post_meta( $booking_id, '_bnb_booking_calculated_price', $total );
|
||||||
|
|
||||||
|
if ( ! empty( $breakdown ) ) {
|
||||||
|
update_post_meta( $booking_id, '_bnb_booking_price_breakdown', $breakdown );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store services.
|
||||||
|
if ( ! empty( $services ) ) {
|
||||||
|
update_post_meta( $booking_id, Booking::SERVICES_META_KEY, $services );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate booking reference.
|
||||||
|
$reference = self::generate_booking_reference( $booking_id );
|
||||||
|
update_post_meta( $booking_id, '_bnb_booking_reference', $reference );
|
||||||
|
|
||||||
|
// Store confirmed timestamp if auto-confirmed.
|
||||||
|
if ( 'confirmed' === $status ) {
|
||||||
|
update_post_meta( $booking_id, '_bnb_booking_confirmed_at', current_time( 'mysql' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Link booking to order.
|
||||||
|
Manager::link_booking_to_order( $booking_id, $order );
|
||||||
|
|
||||||
|
// Also store room ID in order meta for quick access.
|
||||||
|
$order->update_meta_data( Manager::ORDER_ROOM_META, $room_id );
|
||||||
|
|
||||||
|
// Store check-in/out in order meta.
|
||||||
|
$order->update_meta_data( '_bnb_check_in', $check_in );
|
||||||
|
$order->update_meta_data( '_bnb_check_out', $check_out );
|
||||||
|
$order->save();
|
||||||
|
|
||||||
|
// Trigger status change action for email notifications.
|
||||||
|
if ( 'confirmed' === $status ) {
|
||||||
|
/**
|
||||||
|
* Fires when booking status changes (for email notifications).
|
||||||
|
*
|
||||||
|
* @param int $booking_id Booking post ID.
|
||||||
|
* @param string $old_status Old status.
|
||||||
|
* @param string $new_status New status.
|
||||||
|
*/
|
||||||
|
do_action( 'wp_bnb_booking_status_changed', $booking_id, 'pending', 'confirmed' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires after creating a booking from an order.
|
||||||
|
*
|
||||||
|
* @param int|null $booking_id Last booking ID created.
|
||||||
|
* @param \WC_Order $order WooCommerce order.
|
||||||
|
*/
|
||||||
|
do_action( 'wp_bnb_wc_after_booking_from_order', $booking_id, $order );
|
||||||
|
|
||||||
|
return $booking_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync order status changes to booking status.
|
||||||
|
*
|
||||||
|
* @param int $order_id Order ID.
|
||||||
|
* @param string $old_status Old status.
|
||||||
|
* @param string $new_status New status.
|
||||||
|
* @param \WC_Order $order Order object.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function sync_order_status_to_booking( int $order_id, string $old_status, string $new_status, \WC_Order $order ): void {
|
||||||
|
// Prevent recursive updates.
|
||||||
|
if ( self::$updating_status ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$booking_id = Manager::get_booking_for_order( $order );
|
||||||
|
|
||||||
|
if ( ! $booking_id ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map WC status to booking status.
|
||||||
|
$booking_status = Manager::map_wc_status_to_booking( $new_status );
|
||||||
|
|
||||||
|
// Get current booking status.
|
||||||
|
$current_booking_status = get_post_meta( $booking_id, '_bnb_booking_status', true );
|
||||||
|
|
||||||
|
// Don't update if status is the same.
|
||||||
|
if ( $current_booking_status === $booking_status ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't downgrade from checked_in/checked_out.
|
||||||
|
if ( in_array( $current_booking_status, array( 'checked_in', 'checked_out' ), true ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self::$updating_status = true;
|
||||||
|
|
||||||
|
// Update booking status.
|
||||||
|
update_post_meta( $booking_id, '_bnb_booking_status', $booking_status );
|
||||||
|
|
||||||
|
// Update confirmed timestamp if confirming.
|
||||||
|
if ( 'confirmed' === $booking_status && 'pending' === $current_booking_status ) {
|
||||||
|
update_post_meta( $booking_id, '_bnb_booking_confirmed_at', current_time( 'mysql' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger booking status changed action.
|
||||||
|
do_action( 'wp_bnb_booking_status_changed', $booking_id, $current_booking_status, $booking_status );
|
||||||
|
|
||||||
|
self::$updating_status = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find or create guest from order data.
|
||||||
|
*
|
||||||
|
* @param \WC_Order $order WooCommerce order.
|
||||||
|
* @return int|null Guest ID or null.
|
||||||
|
*/
|
||||||
|
private static function find_or_create_guest( \WC_Order $order ): ?int {
|
||||||
|
$email = $order->get_billing_email();
|
||||||
|
|
||||||
|
if ( ! $email ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find existing guest by email.
|
||||||
|
$existing_guests = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Guest::POST_TYPE,
|
||||||
|
'posts_per_page' => 1,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'meta_query' => array(
|
||||||
|
array(
|
||||||
|
'key' => '_bnb_guest_email',
|
||||||
|
'value' => $email,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( ! empty( $existing_guests ) ) {
|
||||||
|
return $existing_guests[0]->ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new guest.
|
||||||
|
$first_name = $order->get_billing_first_name();
|
||||||
|
$last_name = $order->get_billing_last_name();
|
||||||
|
$full_name = trim( $first_name . ' ' . $last_name );
|
||||||
|
|
||||||
|
$guest_data = array(
|
||||||
|
'post_type' => Guest::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'post_title' => $full_name ?: $email,
|
||||||
|
);
|
||||||
|
|
||||||
|
$guest_id = wp_insert_post( $guest_data );
|
||||||
|
|
||||||
|
if ( ! $guest_id || is_wp_error( $guest_id ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store guest meta.
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_first_name', $first_name );
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_last_name', $last_name );
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_email', $email );
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_phone', $order->get_billing_phone() );
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_address', $order->get_billing_address_1() );
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_city', $order->get_billing_city() );
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_postal_code', $order->get_billing_postcode() );
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_country', $order->get_billing_country() );
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_status', 'active' );
|
||||||
|
update_post_meta( $guest_id, '_bnb_guest_source', 'woocommerce' );
|
||||||
|
|
||||||
|
return $guest_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate booking title.
|
||||||
|
*
|
||||||
|
* @param int|null $guest_id Guest ID.
|
||||||
|
* @param string $check_in Check-in date.
|
||||||
|
* @param string $check_out Check-out date.
|
||||||
|
* @return string Booking title.
|
||||||
|
*/
|
||||||
|
private static function generate_booking_title( ?int $guest_id, string $check_in, string $check_out ): string {
|
||||||
|
$guest_name = $guest_id ? Guest::get_full_name( $guest_id ) : __( 'Guest', 'wp-bnb' );
|
||||||
|
|
||||||
|
$check_in_date = \DateTime::createFromFormat( 'Y-m-d', $check_in );
|
||||||
|
$check_out_date = \DateTime::createFromFormat( 'Y-m-d', $check_out );
|
||||||
|
|
||||||
|
if ( ! $check_in_date || ! $check_out_date ) {
|
||||||
|
return $guest_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format: "Guest Name (DD.MM - DD.MM.YYYY)" or span years.
|
||||||
|
if ( $check_in_date->format( 'Y' ) === $check_out_date->format( 'Y' ) ) {
|
||||||
|
return sprintf(
|
||||||
|
'%s (%s - %s)',
|
||||||
|
$guest_name,
|
||||||
|
$check_in_date->format( 'd.m' ),
|
||||||
|
$check_out_date->format( 'd.m.Y' )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sprintf(
|
||||||
|
'%s (%s - %s)',
|
||||||
|
$guest_name,
|
||||||
|
$check_in_date->format( 'd.m.Y' ),
|
||||||
|
$check_out_date->format( 'd.m.Y' )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate booking reference.
|
||||||
|
*
|
||||||
|
* @param int $booking_id Booking ID.
|
||||||
|
* @return string Booking reference.
|
||||||
|
*/
|
||||||
|
private static function generate_booking_reference( int $booking_id ): string {
|
||||||
|
return sprintf(
|
||||||
|
'BNB-%s-%05d',
|
||||||
|
gmdate( 'Y' ),
|
||||||
|
$booking_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display booking info in admin order page.
|
||||||
|
*
|
||||||
|
* @param \WC_Order $order WooCommerce order.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function display_booking_info_admin( \WC_Order $order ): void {
|
||||||
|
$booking_id = Manager::get_booking_for_order( $order );
|
||||||
|
|
||||||
|
if ( ! $booking_id ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$booking = get_post( $booking_id );
|
||||||
|
$status = get_post_meta( $booking_id, '_bnb_booking_status', true );
|
||||||
|
$room_id = get_post_meta( $booking_id, '_bnb_booking_room_id', true );
|
||||||
|
$room = $room_id ? get_post( $room_id ) : null;
|
||||||
|
?>
|
||||||
|
<div class="order_data_column bnb-order-booking-info">
|
||||||
|
<h3><?php esc_html_e( 'Booking Information', 'wp-bnb' ); ?></h3>
|
||||||
|
<p>
|
||||||
|
<strong><?php esc_html_e( 'Booking:', 'wp-bnb' ); ?></strong>
|
||||||
|
<a href="<?php echo esc_url( get_edit_post_link( $booking_id ) ); ?>">
|
||||||
|
<?php echo esc_html( $booking ? $booking->post_title : "#{$booking_id}" ); ?>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<?php if ( $room ) : ?>
|
||||||
|
<p>
|
||||||
|
<strong><?php esc_html_e( 'Room:', 'wp-bnb' ); ?></strong>
|
||||||
|
<a href="<?php echo esc_url( get_edit_post_link( $room_id ) ); ?>">
|
||||||
|
<?php echo esc_html( $room->post_title ); ?>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
<p>
|
||||||
|
<strong><?php esc_html_e( 'Status:', 'wp-bnb' ); ?></strong>
|
||||||
|
<span class="bnb-status-badge bnb-status-<?php echo esc_attr( $status ); ?>">
|
||||||
|
<?php echo esc_html( ucfirst( str_replace( '_', ' ', $status ) ) ); ?>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display booking info on thank you page.
|
||||||
|
*
|
||||||
|
* @param int $order_id Order ID.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function display_booking_info_thankyou( int $order_id ): void {
|
||||||
|
$order = wc_get_order( $order_id );
|
||||||
|
|
||||||
|
if ( ! $order ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$booking_id = Manager::get_booking_for_order( $order );
|
||||||
|
|
||||||
|
if ( ! $booking_id ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$reference = get_post_meta( $booking_id, '_bnb_booking_reference', true );
|
||||||
|
$check_in = get_post_meta( $booking_id, '_bnb_booking_check_in', true );
|
||||||
|
$check_out = get_post_meta( $booking_id, '_bnb_booking_check_out', true );
|
||||||
|
$room_id = get_post_meta( $booking_id, '_bnb_booking_room_id', true );
|
||||||
|
$room = $room_id ? get_post( $room_id ) : null;
|
||||||
|
$building = $room_id ? Room::get_building( $room_id ) : null;
|
||||||
|
|
||||||
|
$date_format = get_option( 'date_format' );
|
||||||
|
$check_in_date = \DateTime::createFromFormat( 'Y-m-d', $check_in );
|
||||||
|
$check_out_date = \DateTime::createFromFormat( 'Y-m-d', $check_out );
|
||||||
|
?>
|
||||||
|
<section class="woocommerce-booking-confirmation">
|
||||||
|
<h2><?php esc_html_e( 'Booking Confirmed', 'wp-bnb' ); ?></h2>
|
||||||
|
<table class="woocommerce-table woocommerce-table--booking-details">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th><?php esc_html_e( 'Booking Reference:', 'wp-bnb' ); ?></th>
|
||||||
|
<td><strong><?php echo esc_html( $reference ); ?></strong></td>
|
||||||
|
</tr>
|
||||||
|
<?php if ( $room ) : ?>
|
||||||
|
<tr>
|
||||||
|
<th><?php esc_html_e( 'Room:', 'wp-bnb' ); ?></th>
|
||||||
|
<td>
|
||||||
|
<?php echo esc_html( $room->post_title ); ?>
|
||||||
|
<?php if ( $building ) : ?>
|
||||||
|
<br><small><?php echo esc_html( $building->post_title ); ?></small>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
<tr>
|
||||||
|
<th><?php esc_html_e( 'Check-in:', 'wp-bnb' ); ?></th>
|
||||||
|
<td><?php echo esc_html( $check_in_date ? $check_in_date->format( $date_format ) : $check_in ); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><?php esc_html_e( 'Check-out:', 'wp-bnb' ); ?></th>
|
||||||
|
<td><?php echo esc_html( $check_out_date ? $check_out_date->format( $date_format ) : $check_out ); ?></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p class="woocommerce-notice woocommerce-notice--success">
|
||||||
|
<?php esc_html_e( 'Your booking has been confirmed. A confirmation email has been sent to your email address.', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display booking info in order details (customer account).
|
||||||
|
*
|
||||||
|
* @param \WC_Order $order WooCommerce order.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function display_booking_info_order_details( \WC_Order $order ): void {
|
||||||
|
$booking_id = Manager::get_booking_for_order( $order );
|
||||||
|
|
||||||
|
if ( ! $booking_id ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$reference = get_post_meta( $booking_id, '_bnb_booking_reference', true );
|
||||||
|
$status = get_post_meta( $booking_id, '_bnb_booking_status', true );
|
||||||
|
|
||||||
|
if ( $reference ) {
|
||||||
|
?>
|
||||||
|
<p class="woocommerce-booking-reference">
|
||||||
|
<strong><?php esc_html_e( 'Booking Reference:', 'wp-bnb' ); ?></strong>
|
||||||
|
<?php echo esc_html( $reference ); ?>
|
||||||
|
<span class="bnb-status-badge bnb-status-<?php echo esc_attr( $status ); ?>">
|
||||||
|
<?php echo esc_html( ucfirst( str_replace( '_', ' ', $status ) ) ); ?>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
515
src/Integration/WooCommerce/ProductSync.php
Normal file
515
src/Integration/WooCommerce/ProductSync.php
Normal file
@@ -0,0 +1,515 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* WooCommerce Product Synchronization.
|
||||||
|
*
|
||||||
|
* Synchronizes rooms with WooCommerce products.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Integration\WooCommerce
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Integration\WooCommerce;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\PostTypes\Building;
|
||||||
|
use Magdev\WpBnb\PostTypes\Room;
|
||||||
|
use Magdev\WpBnb\Pricing\Calculator;
|
||||||
|
use Magdev\WpBnb\Pricing\PricingTier;
|
||||||
|
use Magdev\WpBnb\Taxonomies\Amenity;
|
||||||
|
use Magdev\WpBnb\Taxonomies\RoomType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Product Synchronization class.
|
||||||
|
*
|
||||||
|
* Creates and maintains WooCommerce products that correspond to BnB rooms.
|
||||||
|
*/
|
||||||
|
final class ProductSync {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize product synchronization.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function init(): void {
|
||||||
|
// Sync product when room is saved.
|
||||||
|
add_action( 'save_post_' . Room::POST_TYPE, array( self::class, 'on_room_save' ), 20, 2 );
|
||||||
|
|
||||||
|
// Delete product when room is deleted.
|
||||||
|
add_action( 'before_delete_post', array( self::class, 'on_room_delete' ) );
|
||||||
|
|
||||||
|
// Add linked room info to product edit screen.
|
||||||
|
add_action( 'woocommerce_product_options_general_product_data', array( self::class, 'add_product_room_info' ) );
|
||||||
|
|
||||||
|
// AJAX handler for syncing all rooms.
|
||||||
|
add_action( 'wp_ajax_wp_bnb_sync_all_rooms', array( self::class, 'ajax_sync_all_rooms' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle room save - create or update product.
|
||||||
|
*
|
||||||
|
* @param int $post_id Post ID.
|
||||||
|
* @param \WP_Post $post Post object.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function on_room_save( int $post_id, \WP_Post $post ): void {
|
||||||
|
// Skip autosaves, revisions, and other non-published posts.
|
||||||
|
if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( 'publish' !== $post->post_status ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if auto-sync is enabled.
|
||||||
|
if ( ! Manager::is_auto_sync_enabled() ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync the product.
|
||||||
|
self::sync_room_to_product( $post_id );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle room deletion - delete linked product.
|
||||||
|
*
|
||||||
|
* @param int $post_id Post ID.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function on_room_delete( int $post_id ): void {
|
||||||
|
$post = get_post( $post_id );
|
||||||
|
|
||||||
|
if ( ! $post || Room::POST_TYPE !== $post->post_type ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self::delete_product_for_room( $post_id );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync a room to a WooCommerce product.
|
||||||
|
*
|
||||||
|
* Creates a new product if one doesn't exist, or updates existing one.
|
||||||
|
*
|
||||||
|
* @param int $room_id Room post ID.
|
||||||
|
* @return int|null Product ID or null on failure.
|
||||||
|
*/
|
||||||
|
public static function sync_room_to_product( int $room_id ): ?int {
|
||||||
|
$room = get_post( $room_id );
|
||||||
|
|
||||||
|
if ( ! $room || Room::POST_TYPE !== $room->post_type ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for existing product.
|
||||||
|
$product_id = self::get_product_for_room( $room_id );
|
||||||
|
|
||||||
|
if ( $product_id ) {
|
||||||
|
// Update existing product.
|
||||||
|
return self::update_product( $product_id, $room );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new product.
|
||||||
|
return self::create_product_for_room( $room );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a WooCommerce product for a room.
|
||||||
|
*
|
||||||
|
* @param \WP_Post $room Room post object.
|
||||||
|
* @return int|null Product ID or null on failure.
|
||||||
|
*/
|
||||||
|
public static function create_product_for_room( \WP_Post $room ): ?int {
|
||||||
|
/**
|
||||||
|
* Fires before creating a WC product for a room.
|
||||||
|
*
|
||||||
|
* @param int $room_id Room post ID.
|
||||||
|
*/
|
||||||
|
do_action( 'wp_bnb_wc_before_product_sync', $room->ID );
|
||||||
|
|
||||||
|
// Create a simple virtual product.
|
||||||
|
$product = new \WC_Product_Simple();
|
||||||
|
|
||||||
|
// Configure the product.
|
||||||
|
self::configure_product( $product, $room );
|
||||||
|
|
||||||
|
// Save the product.
|
||||||
|
$product_id = $product->save();
|
||||||
|
|
||||||
|
if ( ! $product_id ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store bidirectional links.
|
||||||
|
update_post_meta( $room->ID, Manager::ROOM_PRODUCT_META, $product_id );
|
||||||
|
update_post_meta( $product_id, Manager::PRODUCT_ROOM_META, $room->ID );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires after creating a WC product for a room.
|
||||||
|
*
|
||||||
|
* @param int $room_id Room post ID.
|
||||||
|
* @param int $product_id WC product ID.
|
||||||
|
*/
|
||||||
|
do_action( 'wp_bnb_wc_after_product_sync', $room->ID, $product_id );
|
||||||
|
|
||||||
|
return $product_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing WooCommerce product.
|
||||||
|
*
|
||||||
|
* @param int $product_id Product ID.
|
||||||
|
* @param \WP_Post $room Room post object.
|
||||||
|
* @return int|null Product ID or null on failure.
|
||||||
|
*/
|
||||||
|
private static function update_product( int $product_id, \WP_Post $room ): ?int {
|
||||||
|
$product = wc_get_product( $product_id );
|
||||||
|
|
||||||
|
if ( ! $product ) {
|
||||||
|
// Product was deleted, create a new one.
|
||||||
|
return self::create_product_for_room( $room );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires before updating a WC product for a room.
|
||||||
|
*
|
||||||
|
* @param int $room_id Room post ID.
|
||||||
|
*/
|
||||||
|
do_action( 'wp_bnb_wc_before_product_sync', $room->ID );
|
||||||
|
|
||||||
|
// Configure the product.
|
||||||
|
self::configure_product( $product, $room );
|
||||||
|
|
||||||
|
// Save the product.
|
||||||
|
$product->save();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires after updating a WC product for a room.
|
||||||
|
*
|
||||||
|
* @param int $room_id Room post ID.
|
||||||
|
* @param int $product_id WC product ID.
|
||||||
|
*/
|
||||||
|
do_action( 'wp_bnb_wc_after_product_sync', $room->ID, $product_id );
|
||||||
|
|
||||||
|
return $product_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure a WooCommerce product from room data.
|
||||||
|
*
|
||||||
|
* @param \WC_Product $product Product object.
|
||||||
|
* @param \WP_Post $room Room post object.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function configure_product( \WC_Product $product, \WP_Post $room ): void {
|
||||||
|
// Get room data.
|
||||||
|
$building = Room::get_building( $room->ID );
|
||||||
|
$building_name = $building ? $building->post_title : '';
|
||||||
|
$pricing = Calculator::getRoomPricing( $room->ID );
|
||||||
|
|
||||||
|
// Basic info.
|
||||||
|
$product->set_name( $room->post_title );
|
||||||
|
$product->set_slug( 'bnb-room-' . $room->ID );
|
||||||
|
$product->set_status( 'publish' );
|
||||||
|
|
||||||
|
// SKU.
|
||||||
|
$product->set_sku( 'bnb-room-' . $room->ID );
|
||||||
|
|
||||||
|
// Virtual product (no shipping).
|
||||||
|
$product->set_virtual( true );
|
||||||
|
|
||||||
|
// Description.
|
||||||
|
$description = $room->post_content;
|
||||||
|
if ( $building_name ) {
|
||||||
|
$description = sprintf(
|
||||||
|
/* translators: %s: Building name */
|
||||||
|
__( 'Room at %s', 'wp-bnb' ),
|
||||||
|
$building_name
|
||||||
|
) . "\n\n" . $description;
|
||||||
|
}
|
||||||
|
$product->set_description( $description );
|
||||||
|
$product->set_short_description( $room->post_excerpt ?: wp_trim_words( $room->post_content, 30 ) );
|
||||||
|
|
||||||
|
// Price (use short-term/nightly rate as base).
|
||||||
|
$base_price = $pricing[ PricingTier::SHORT_TERM->value ]['price'] ?? 0;
|
||||||
|
if ( $base_price > 0 ) {
|
||||||
|
$product->set_regular_price( (string) $base_price );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Featured image.
|
||||||
|
$thumbnail_id = get_post_thumbnail_id( $room->ID );
|
||||||
|
if ( $thumbnail_id ) {
|
||||||
|
$product->set_image_id( $thumbnail_id );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gallery images.
|
||||||
|
$gallery_ids = get_post_meta( $room->ID, '_bnb_room_gallery', true );
|
||||||
|
if ( $gallery_ids ) {
|
||||||
|
$ids = array_filter( explode( ',', $gallery_ids ), 'is_numeric' );
|
||||||
|
$product->set_gallery_image_ids( array_map( 'absint', $ids ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stock management (disabled - availability handled by booking system).
|
||||||
|
$product->set_manage_stock( false );
|
||||||
|
$product->set_stock_status( 'instock' );
|
||||||
|
|
||||||
|
// Catalog visibility - visible.
|
||||||
|
$product->set_catalog_visibility( 'visible' );
|
||||||
|
|
||||||
|
// Product category.
|
||||||
|
$category_id = Manager::get_product_category();
|
||||||
|
if ( $category_id ) {
|
||||||
|
$product->set_category_ids( array( $category_id ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store room metadata.
|
||||||
|
$capacity = get_post_meta( $room->ID, '_bnb_room_capacity', true );
|
||||||
|
$beds = get_post_meta( $room->ID, '_bnb_room_beds', true );
|
||||||
|
|
||||||
|
$product->update_meta_data( '_bnb_room_capacity', $capacity );
|
||||||
|
$product->update_meta_data( '_bnb_room_beds', $beds );
|
||||||
|
$product->update_meta_data( '_bnb_building_id', $building ? $building->ID : 0 );
|
||||||
|
$product->update_meta_data( '_bnb_building_name', $building_name );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter the product data before save.
|
||||||
|
*
|
||||||
|
* @param array $data Product data array.
|
||||||
|
* @param int $room_id Room post ID.
|
||||||
|
* @param \WP_Post $room Room post object.
|
||||||
|
*/
|
||||||
|
$data = apply_filters(
|
||||||
|
'wp_bnb_wc_product_data',
|
||||||
|
array(
|
||||||
|
'name' => $product->get_name(),
|
||||||
|
'price' => $product->get_regular_price(),
|
||||||
|
'description' => $product->get_description(),
|
||||||
|
),
|
||||||
|
$room->ID,
|
||||||
|
$room
|
||||||
|
);
|
||||||
|
|
||||||
|
// Apply filtered data.
|
||||||
|
if ( isset( $data['name'] ) ) {
|
||||||
|
$product->set_name( $data['name'] );
|
||||||
|
}
|
||||||
|
if ( isset( $data['price'] ) ) {
|
||||||
|
$product->set_regular_price( (string) $data['price'] );
|
||||||
|
}
|
||||||
|
if ( isset( $data['description'] ) ) {
|
||||||
|
$product->set_description( $data['description'] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the WooCommerce product for a room.
|
||||||
|
*
|
||||||
|
* @param int $room_id Room post ID.
|
||||||
|
* @return bool True if deleted, false otherwise.
|
||||||
|
*/
|
||||||
|
public static function delete_product_for_room( int $room_id ): bool {
|
||||||
|
$product_id = self::get_product_for_room( $room_id );
|
||||||
|
|
||||||
|
if ( ! $product_id ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$product = wc_get_product( $product_id );
|
||||||
|
|
||||||
|
if ( ! $product ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the product (force delete, not trash).
|
||||||
|
$product->delete( true );
|
||||||
|
|
||||||
|
// Clean up room meta.
|
||||||
|
delete_post_meta( $room_id, Manager::ROOM_PRODUCT_META );
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the WooCommerce product ID for a room.
|
||||||
|
*
|
||||||
|
* @param int $room_id Room post ID.
|
||||||
|
* @return int|null Product ID or null.
|
||||||
|
*/
|
||||||
|
public static function get_product_for_room( int $room_id ): ?int {
|
||||||
|
$product_id = get_post_meta( $room_id, Manager::ROOM_PRODUCT_META, true );
|
||||||
|
|
||||||
|
if ( ! $product_id ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify product still exists.
|
||||||
|
$product = wc_get_product( $product_id );
|
||||||
|
|
||||||
|
if ( ! $product ) {
|
||||||
|
// Clean up stale meta.
|
||||||
|
delete_post_meta( $room_id, Manager::ROOM_PRODUCT_META );
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return absint( $product_id );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the room ID for a WooCommerce product.
|
||||||
|
*
|
||||||
|
* @param int $product_id Product ID.
|
||||||
|
* @return int|null Room ID or null.
|
||||||
|
*/
|
||||||
|
public static function get_room_for_product( int $product_id ): ?int {
|
||||||
|
$room_id = get_post_meta( $product_id, Manager::PRODUCT_ROOM_META, true );
|
||||||
|
|
||||||
|
if ( ! $room_id ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify room still exists.
|
||||||
|
$room = get_post( $room_id );
|
||||||
|
|
||||||
|
if ( ! $room || Room::POST_TYPE !== $room->post_type ) {
|
||||||
|
// Clean up stale meta.
|
||||||
|
delete_post_meta( $product_id, Manager::PRODUCT_ROOM_META );
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return absint( $room_id );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync all published rooms to WooCommerce products.
|
||||||
|
*
|
||||||
|
* @return array{created: int, updated: int, errors: array<string>}
|
||||||
|
*/
|
||||||
|
public static function sync_all_rooms(): array {
|
||||||
|
$result = array(
|
||||||
|
'created' => 0,
|
||||||
|
'updated' => 0,
|
||||||
|
'errors' => array(),
|
||||||
|
);
|
||||||
|
|
||||||
|
$rooms = get_posts(
|
||||||
|
array(
|
||||||
|
'post_type' => Room::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'fields' => 'ids',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ( $rooms as $room_id ) {
|
||||||
|
$existing_product = self::get_product_for_room( $room_id );
|
||||||
|
|
||||||
|
$product_id = self::sync_room_to_product( $room_id );
|
||||||
|
|
||||||
|
if ( $product_id ) {
|
||||||
|
if ( $existing_product ) {
|
||||||
|
++$result['updated'];
|
||||||
|
} else {
|
||||||
|
++$result['created'];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$room = get_post( $room_id );
|
||||||
|
$result['errors'][] = sprintf(
|
||||||
|
/* translators: %s: Room title */
|
||||||
|
__( 'Failed to sync room: %s', 'wp-bnb' ),
|
||||||
|
$room ? $room->post_title : "#{$room_id}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler for syncing all rooms.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function ajax_sync_all_rooms(): void {
|
||||||
|
check_ajax_referer( 'wp_bnb_admin_nonce', 'nonce' );
|
||||||
|
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
|
wp_send_json_error( array( 'message' => __( 'Permission denied.', 'wp-bnb' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = self::sync_all_rooms();
|
||||||
|
|
||||||
|
wp_send_json_success(
|
||||||
|
array(
|
||||||
|
'message' => sprintf(
|
||||||
|
/* translators: 1: Created count, 2: Updated count */
|
||||||
|
__( 'Sync complete. Created: %1$d, Updated: %2$d', 'wp-bnb' ),
|
||||||
|
$result['created'],
|
||||||
|
$result['updated']
|
||||||
|
),
|
||||||
|
'created' => $result['created'],
|
||||||
|
'updated' => $result['updated'],
|
||||||
|
'errors' => $result['errors'],
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add linked room info to WooCommerce product edit screen.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function add_product_room_info(): void {
|
||||||
|
global $post;
|
||||||
|
|
||||||
|
if ( ! $post ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$room_id = self::get_room_for_product( $post->ID );
|
||||||
|
|
||||||
|
if ( ! $room_id ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$room = get_post( $room_id );
|
||||||
|
|
||||||
|
if ( ! $room ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
<div class="options_group show_if_simple">
|
||||||
|
<p class="form-field">
|
||||||
|
<label><?php esc_html_e( 'Linked BnB Room', 'wp-bnb' ); ?></label>
|
||||||
|
<span class="description">
|
||||||
|
<a href="<?php echo esc_url( get_edit_post_link( $room_id ) ); ?>">
|
||||||
|
<?php echo esc_html( $room->post_title ); ?>
|
||||||
|
</a>
|
||||||
|
<br>
|
||||||
|
<small><?php esc_html_e( 'This product is automatically synced from the linked room. Changes should be made in the room settings.', 'wp-bnb' ); ?></small>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a product is a BnB room product.
|
||||||
|
*
|
||||||
|
* @param int|\WC_Product $product Product ID or object.
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function is_room_product( $product ): bool {
|
||||||
|
if ( is_int( $product ) ) {
|
||||||
|
$product = wc_get_product( $product );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! $product instanceof \WC_Product ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$room_id = $product->get_meta( Manager::PRODUCT_ROOM_META, true );
|
||||||
|
|
||||||
|
return ! empty( $room_id );
|
||||||
|
}
|
||||||
|
}
|
||||||
394
src/Integration/WooCommerce/RefundHandler.php
Normal file
394
src/Integration/WooCommerce/RefundHandler.php
Normal file
@@ -0,0 +1,394 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* WooCommerce Refund Handler.
|
||||||
|
*
|
||||||
|
* Handles refund processing and booking cancellation.
|
||||||
|
*
|
||||||
|
* @package Magdev\WpBnb\Integration\WooCommerce
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types=1 );
|
||||||
|
|
||||||
|
namespace Magdev\WpBnb\Integration\WooCommerce;
|
||||||
|
|
||||||
|
use Magdev\WpBnb\Booking\EmailNotifier;
|
||||||
|
use Magdev\WpBnb\PostTypes\Booking;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refund Handler class.
|
||||||
|
*
|
||||||
|
* Processes refunds and updates booking status accordingly.
|
||||||
|
*/
|
||||||
|
final class RefundHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Booking meta key for refund amount.
|
||||||
|
*/
|
||||||
|
public const REFUND_AMOUNT_META = '_bnb_booking_refund_amount';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Booking meta key for refund reason.
|
||||||
|
*/
|
||||||
|
public const REFUND_REASON_META = '_bnb_booking_refund_reason';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Booking meta key for refund date.
|
||||||
|
*/
|
||||||
|
public const REFUND_DATE_META = '_bnb_booking_refund_date';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize refund handler.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function init(): void {
|
||||||
|
// Handle refund creation.
|
||||||
|
add_action( 'woocommerce_refund_created', array( self::class, 'on_refund_created' ), 10, 2 );
|
||||||
|
|
||||||
|
// Handle order fully refunded.
|
||||||
|
add_action( 'woocommerce_order_fully_refunded', array( self::class, 'on_order_fully_refunded' ), 10, 2 );
|
||||||
|
|
||||||
|
// Add refund notice in admin order page.
|
||||||
|
add_action( 'woocommerce_admin_order_totals_after_refunded', array( self::class, 'add_booking_refund_notice' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle refund creation.
|
||||||
|
*
|
||||||
|
* @param int $refund_id Refund ID.
|
||||||
|
* @param array $args Refund arguments.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function on_refund_created( int $refund_id, array $args ): void {
|
||||||
|
$order_id = $args['order_id'] ?? 0;
|
||||||
|
|
||||||
|
if ( ! $order_id ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$order = wc_get_order( $order_id );
|
||||||
|
|
||||||
|
if ( ! $order instanceof \WC_Order ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if order has a linked booking.
|
||||||
|
$booking_id = Manager::get_booking_for_order( $order );
|
||||||
|
|
||||||
|
if ( ! $booking_id ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$refund_amount = abs( floatval( $args['amount'] ?? 0 ) );
|
||||||
|
$refund_reason = $args['reason'] ?? '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires before processing a refund for a booking.
|
||||||
|
*
|
||||||
|
* @param \WC_Order $order WooCommerce order.
|
||||||
|
* @param float $refund_amount Refund amount.
|
||||||
|
*/
|
||||||
|
do_action( 'wp_bnb_wc_before_refund_process', $order, $refund_amount );
|
||||||
|
|
||||||
|
// Check if this is a full or partial refund.
|
||||||
|
$is_full_refund = self::is_full_refund( $order );
|
||||||
|
|
||||||
|
if ( $is_full_refund ) {
|
||||||
|
// Full refund - cancel the booking.
|
||||||
|
self::cancel_booking_on_refund( $booking_id, $refund_amount, $refund_reason );
|
||||||
|
} else {
|
||||||
|
// Partial refund - store refund info but don't cancel.
|
||||||
|
self::record_partial_refund( $booking_id, $refund_amount, $refund_reason );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires after processing a refund for a booking.
|
||||||
|
*
|
||||||
|
* @param \WC_Order $order WooCommerce order.
|
||||||
|
* @param int $booking_id Booking ID.
|
||||||
|
* @param bool $cancelled Whether booking was cancelled.
|
||||||
|
*/
|
||||||
|
do_action( 'wp_bnb_wc_after_refund_process', $order, $booking_id, $is_full_refund );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle order fully refunded.
|
||||||
|
*
|
||||||
|
* @param int $order_id Order ID.
|
||||||
|
* @param int $refund_id Refund ID.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function on_order_fully_refunded( int $order_id, int $refund_id ): void {
|
||||||
|
$order = wc_get_order( $order_id );
|
||||||
|
|
||||||
|
if ( ! $order instanceof \WC_Order ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$booking_id = Manager::get_booking_for_order( $order );
|
||||||
|
|
||||||
|
if ( ! $booking_id ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current booking status.
|
||||||
|
$current_status = get_post_meta( $booking_id, '_bnb_booking_status', true );
|
||||||
|
|
||||||
|
// Don't cancel if already cancelled.
|
||||||
|
if ( 'cancelled' === $current_status ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel the booking.
|
||||||
|
$total_refunded = $order->get_total_refunded();
|
||||||
|
self::cancel_booking_on_refund( $booking_id, $total_refunded, __( 'Order fully refunded', 'wp-bnb' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the order is fully refunded.
|
||||||
|
*
|
||||||
|
* @param \WC_Order $order WooCommerce order.
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function is_full_refund( \WC_Order $order ): bool {
|
||||||
|
$order_total = floatval( $order->get_total() );
|
||||||
|
$total_refunded = floatval( $order->get_total_refunded() );
|
||||||
|
|
||||||
|
// Consider it full refund if refunded amount >= order total.
|
||||||
|
return $total_refunded >= $order_total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel booking on full refund.
|
||||||
|
*
|
||||||
|
* @param int $booking_id Booking ID.
|
||||||
|
* @param float $refund_amount Refund amount.
|
||||||
|
* @param string $reason Refund reason.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function cancel_booking_on_refund( int $booking_id, float $refund_amount, string $reason ): void {
|
||||||
|
// Get current status.
|
||||||
|
$old_status = get_post_meta( $booking_id, '_bnb_booking_status', true );
|
||||||
|
|
||||||
|
// Don't cancel if already cancelled.
|
||||||
|
if ( 'cancelled' === $old_status ) {
|
||||||
|
// Just update refund info.
|
||||||
|
self::record_refund_meta( $booking_id, $refund_amount, $reason );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter whether to cancel booking on refund.
|
||||||
|
*
|
||||||
|
* @param bool $cancel Whether to cancel.
|
||||||
|
* @param \WC_Order $order WooCommerce order.
|
||||||
|
* @param float $refund_amount Refund amount.
|
||||||
|
*/
|
||||||
|
$should_cancel = apply_filters( 'wp_bnb_wc_should_cancel_on_refund', true, $booking_id, $refund_amount );
|
||||||
|
|
||||||
|
if ( ! $should_cancel ) {
|
||||||
|
self::record_partial_refund( $booking_id, $refund_amount, $reason );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update booking status to cancelled.
|
||||||
|
update_post_meta( $booking_id, '_bnb_booking_status', 'cancelled' );
|
||||||
|
|
||||||
|
// Store refund information.
|
||||||
|
self::record_refund_meta( $booking_id, $refund_amount, $reason );
|
||||||
|
|
||||||
|
// Add cancellation note.
|
||||||
|
$note = sprintf(
|
||||||
|
/* translators: %s: Refund amount */
|
||||||
|
__( 'Booking cancelled due to WooCommerce refund (%s)', 'wp-bnb' ),
|
||||||
|
wc_price( $refund_amount )
|
||||||
|
);
|
||||||
|
|
||||||
|
$existing_notes = get_post_meta( $booking_id, '_bnb_booking_notes', true );
|
||||||
|
$new_notes = $existing_notes ? $existing_notes . "\n\n" . $note : $note;
|
||||||
|
update_post_meta( $booking_id, '_bnb_booking_notes', $new_notes );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires when booking status changes (triggers email notifications).
|
||||||
|
*
|
||||||
|
* @param int $booking_id Booking ID.
|
||||||
|
* @param string $old_status Old status.
|
||||||
|
* @param string $new_status New status.
|
||||||
|
*/
|
||||||
|
do_action( 'wp_bnb_booking_status_changed', $booking_id, $old_status, 'cancelled' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record partial refund without cancelling.
|
||||||
|
*
|
||||||
|
* @param int $booking_id Booking ID.
|
||||||
|
* @param float $refund_amount Refund amount.
|
||||||
|
* @param string $reason Refund reason.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function record_partial_refund( int $booking_id, float $refund_amount, string $reason ): void {
|
||||||
|
// Get existing refund amount and add to it.
|
||||||
|
$existing_refund = floatval( get_post_meta( $booking_id, self::REFUND_AMOUNT_META, true ) );
|
||||||
|
$total_refund = $existing_refund + $refund_amount;
|
||||||
|
|
||||||
|
// Update refund meta.
|
||||||
|
self::record_refund_meta( $booking_id, $total_refund, $reason );
|
||||||
|
|
||||||
|
// Add note about partial refund.
|
||||||
|
$note = sprintf(
|
||||||
|
/* translators: %s: Refund amount */
|
||||||
|
__( 'Partial refund processed: %s', 'wp-bnb' ),
|
||||||
|
wc_price( $refund_amount )
|
||||||
|
);
|
||||||
|
|
||||||
|
$existing_notes = get_post_meta( $booking_id, '_bnb_booking_notes', true );
|
||||||
|
$new_notes = $existing_notes ? $existing_notes . "\n\n" . $note : $note;
|
||||||
|
update_post_meta( $booking_id, '_bnb_booking_notes', $new_notes );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record refund metadata.
|
||||||
|
*
|
||||||
|
* @param int $booking_id Booking ID.
|
||||||
|
* @param float $refund_amount Total refund amount.
|
||||||
|
* @param string $reason Refund reason.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function record_refund_meta( int $booking_id, float $refund_amount, string $reason ): void {
|
||||||
|
update_post_meta( $booking_id, self::REFUND_AMOUNT_META, $refund_amount );
|
||||||
|
update_post_meta( $booking_id, self::REFUND_DATE_META, current_time( 'mysql' ) );
|
||||||
|
|
||||||
|
if ( $reason ) {
|
||||||
|
update_post_meta( $booking_id, self::REFUND_REASON_META, $reason );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate refund amount for a booking.
|
||||||
|
*
|
||||||
|
* @param int $booking_id Booking ID.
|
||||||
|
* @param string $type Refund type: 'full' or 'nights_remaining'.
|
||||||
|
* @return float Refund amount.
|
||||||
|
*/
|
||||||
|
public static function calculate_refund_amount( int $booking_id, string $type = 'full' ): float {
|
||||||
|
$calculated_price = floatval( get_post_meta( $booking_id, '_bnb_booking_calculated_price', true ) );
|
||||||
|
|
||||||
|
if ( 'full' === $type ) {
|
||||||
|
return $calculated_price;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate pro-rata based on nights remaining.
|
||||||
|
$check_in = get_post_meta( $booking_id, '_bnb_booking_check_in', true );
|
||||||
|
$check_out = get_post_meta( $booking_id, '_bnb_booking_check_out', true );
|
||||||
|
|
||||||
|
if ( ! $check_in || ! $check_out ) {
|
||||||
|
return $calculated_price;
|
||||||
|
}
|
||||||
|
|
||||||
|
$today = new \DateTime( 'today' );
|
||||||
|
$check_in_date = \DateTime::createFromFormat( 'Y-m-d', $check_in );
|
||||||
|
$check_out_date = \DateTime::createFromFormat( 'Y-m-d', $check_out );
|
||||||
|
|
||||||
|
if ( ! $check_in_date || ! $check_out_date ) {
|
||||||
|
return $calculated_price;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If check-in hasn't happened, full refund.
|
||||||
|
if ( $today < $check_in_date ) {
|
||||||
|
return $calculated_price;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If check-out has passed, no refund.
|
||||||
|
if ( $today >= $check_out_date ) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate remaining nights.
|
||||||
|
$total_nights = $check_in_date->diff( $check_out_date )->days;
|
||||||
|
$nights_used = $check_in_date->diff( $today )->days;
|
||||||
|
$nights_remaining = $total_nights - $nights_used;
|
||||||
|
|
||||||
|
if ( $total_nights <= 0 ) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pro-rata refund.
|
||||||
|
$nightly_rate = $calculated_price / $total_nights;
|
||||||
|
|
||||||
|
return $nightly_rate * $nights_remaining;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get refund info for a booking.
|
||||||
|
*
|
||||||
|
* @param int $booking_id Booking ID.
|
||||||
|
* @return array|null Refund info or null.
|
||||||
|
*/
|
||||||
|
public static function get_booking_refund_info( int $booking_id ): ?array {
|
||||||
|
$amount = get_post_meta( $booking_id, self::REFUND_AMOUNT_META, true );
|
||||||
|
|
||||||
|
if ( ! $amount ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'amount' => floatval( $amount ),
|
||||||
|
'reason' => get_post_meta( $booking_id, self::REFUND_REASON_META, true ),
|
||||||
|
'date' => get_post_meta( $booking_id, self::REFUND_DATE_META, true ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add booking refund notice in admin order page.
|
||||||
|
*
|
||||||
|
* @param int $order_id Order ID.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function add_booking_refund_notice( int $order_id ): void {
|
||||||
|
$order = wc_get_order( $order_id );
|
||||||
|
|
||||||
|
if ( ! $order instanceof \WC_Order ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$booking_id = Manager::get_booking_for_order( $order );
|
||||||
|
|
||||||
|
if ( ! $booking_id ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$refund_info = self::get_booking_refund_info( $booking_id );
|
||||||
|
|
||||||
|
if ( ! $refund_info ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$booking_status = get_post_meta( $booking_id, '_bnb_booking_status', true );
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td class="label"><?php esc_html_e( 'Booking Status', 'wp-bnb' ); ?>:</td>
|
||||||
|
<td width="1%"></td>
|
||||||
|
<td class="total">
|
||||||
|
<span class="bnb-status-badge bnb-status-<?php echo esc_attr( $booking_status ); ?>">
|
||||||
|
<?php echo esc_html( ucfirst( str_replace( '_', ' ', $booking_status ) ) ); ?>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php if ( 'cancelled' === $booking_status ) : ?>
|
||||||
|
<tr>
|
||||||
|
<td class="label" colspan="2">
|
||||||
|
<small class="description">
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: %s: Booking edit link */
|
||||||
|
esc_html__( 'Booking was cancelled due to refund. %s', 'wp-bnb' ),
|
||||||
|
'<a href="' . esc_url( get_edit_post_link( $booking_id ) ) . '">' . esc_html__( 'View booking', 'wp-bnb' ) . '</a>'
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</small>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
}
|
||||||
389
src/Plugin.php
389
src/Plugin.php
@@ -21,6 +21,7 @@ use Magdev\WpBnb\Frontend\Shortcodes;
|
|||||||
use Magdev\WpBnb\Api\RestApi;
|
use Magdev\WpBnb\Api\RestApi;
|
||||||
use Magdev\WpBnb\Integration\CF7;
|
use Magdev\WpBnb\Integration\CF7;
|
||||||
use Magdev\WpBnb\Integration\Prometheus;
|
use Magdev\WpBnb\Integration\Prometheus;
|
||||||
|
use Magdev\WpBnb\Integration\WooCommerce\Manager as WooCommerceManager;
|
||||||
use Magdev\WpBnb\Frontend\Widgets\AvailabilityCalendar;
|
use Magdev\WpBnb\Frontend\Widgets\AvailabilityCalendar;
|
||||||
use Magdev\WpBnb\Frontend\Widgets\BuildingRooms;
|
use Magdev\WpBnb\Frontend\Widgets\BuildingRooms;
|
||||||
use Magdev\WpBnb\Frontend\Widgets\SimilarRooms;
|
use Magdev\WpBnb\Frontend\Widgets\SimilarRooms;
|
||||||
@@ -147,6 +148,11 @@ final class Plugin {
|
|||||||
// Initialize Prometheus metrics integration.
|
// Initialize Prometheus metrics integration.
|
||||||
Prometheus::init();
|
Prometheus::init();
|
||||||
|
|
||||||
|
// Initialize WooCommerce integration if WooCommerce is active.
|
||||||
|
if ( class_exists( 'WooCommerce' ) ) {
|
||||||
|
WooCommerceManager::init();
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize REST API.
|
// Initialize REST API.
|
||||||
$this->init_rest_api();
|
$this->init_rest_api();
|
||||||
|
|
||||||
@@ -636,6 +642,10 @@ final class Plugin {
|
|||||||
class="nav-tab <?php echo 'api' === $active_tab ? 'nav-tab-active' : ''; ?>">
|
class="nav-tab <?php echo 'api' === $active_tab ? 'nav-tab-active' : ''; ?>">
|
||||||
<?php esc_html_e( 'API', 'wp-bnb' ); ?>
|
<?php esc_html_e( 'API', 'wp-bnb' ); ?>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wp-bnb-settings&tab=woocommerce' ) ); ?>"
|
||||||
|
class="nav-tab <?php echo 'woocommerce' === $active_tab ? 'nav-tab-active' : ''; ?>">
|
||||||
|
<?php esc_html_e( 'WooCommerce', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
@@ -656,6 +666,9 @@ final class Plugin {
|
|||||||
case 'api':
|
case 'api':
|
||||||
$this->render_api_settings();
|
$this->render_api_settings();
|
||||||
break;
|
break;
|
||||||
|
case 'woocommerce':
|
||||||
|
$this->render_woocommerce_settings();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
$this->render_general_settings();
|
$this->render_general_settings();
|
||||||
break;
|
break;
|
||||||
@@ -1854,6 +1867,9 @@ final class Plugin {
|
|||||||
case 'api':
|
case 'api':
|
||||||
$this->save_api_settings();
|
$this->save_api_settings();
|
||||||
break;
|
break;
|
||||||
|
case 'woocommerce':
|
||||||
|
$this->save_woocommerce_settings();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
$this->save_general_settings();
|
$this->save_general_settings();
|
||||||
break;
|
break;
|
||||||
@@ -2037,6 +2053,379 @@ final class Plugin {
|
|||||||
settings_errors( 'wp_bnb_settings' );
|
settings_errors( 'wp_bnb_settings' );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render WooCommerce settings tab.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function render_woocommerce_settings(): void {
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Subtab switching only.
|
||||||
|
$active_subtab = isset( $_GET['subtab'] ) ? sanitize_key( $_GET['subtab'] ) : 'general';
|
||||||
|
$base_url = admin_url( 'admin.php?page=wp-bnb-settings&tab=woocommerce' );
|
||||||
|
|
||||||
|
$wc_active = class_exists( 'WooCommerce' );
|
||||||
|
?>
|
||||||
|
<div class="wp-bnb-subtabs">
|
||||||
|
<a href="<?php echo esc_url( $base_url . '&subtab=general' ); ?>"
|
||||||
|
class="wp-bnb-subtab <?php echo 'general' === $active_subtab ? 'active' : ''; ?>">
|
||||||
|
<span class="dashicons dashicons-admin-generic"></span>
|
||||||
|
<?php esc_html_e( 'General', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo esc_url( $base_url . '&subtab=products' ); ?>"
|
||||||
|
class="wp-bnb-subtab <?php echo 'products' === $active_subtab ? 'active' : ''; ?>">
|
||||||
|
<span class="dashicons dashicons-products"></span>
|
||||||
|
<?php esc_html_e( 'Products', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo esc_url( $base_url . '&subtab=orders' ); ?>"
|
||||||
|
class="wp-bnb-subtab <?php echo 'orders' === $active_subtab ? 'active' : ''; ?>">
|
||||||
|
<span class="dashicons dashicons-cart"></span>
|
||||||
|
<?php esc_html_e( 'Orders', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo esc_url( $base_url . '&subtab=invoices' ); ?>"
|
||||||
|
class="wp-bnb-subtab <?php echo 'invoices' === $active_subtab ? 'active' : ''; ?>">
|
||||||
|
<span class="dashicons dashicons-media-document"></span>
|
||||||
|
<?php esc_html_e( 'Invoices', 'wp-bnb' ); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ( ! $wc_active ) : ?>
|
||||||
|
<div class="notice notice-warning inline">
|
||||||
|
<p>
|
||||||
|
<strong><?php esc_html_e( 'WooCommerce is not active.', 'wp-bnb' ); ?></strong>
|
||||||
|
<?php esc_html_e( 'Please install and activate WooCommerce to use these features.', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
switch ( $active_subtab ) {
|
||||||
|
case 'products':
|
||||||
|
$this->render_wc_products_subtab( $wc_active );
|
||||||
|
break;
|
||||||
|
case 'orders':
|
||||||
|
$this->render_wc_orders_subtab( $wc_active );
|
||||||
|
break;
|
||||||
|
case 'invoices':
|
||||||
|
$this->render_wc_invoices_subtab( $wc_active );
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$this->render_wc_general_subtab( $wc_active );
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render WooCommerce General subtab.
|
||||||
|
*
|
||||||
|
* @param bool $wc_active Whether WooCommerce is active.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function render_wc_general_subtab( bool $wc_active ): void {
|
||||||
|
?>
|
||||||
|
<form method="post" action="">
|
||||||
|
<?php wp_nonce_field( 'wp_bnb_save_settings', 'wp_bnb_settings_nonce' ); ?>
|
||||||
|
<input type="hidden" name="wc_subtab" value="general">
|
||||||
|
|
||||||
|
<h2><?php esc_html_e( 'WooCommerce Integration', 'wp-bnb' ); ?></h2>
|
||||||
|
|
||||||
|
<table class="form-table" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php esc_html_e( 'Enable Integration', 'wp-bnb' ); ?></th>
|
||||||
|
<td>
|
||||||
|
<label for="wp_bnb_wc_enabled">
|
||||||
|
<input type="checkbox" name="wp_bnb_wc_enabled" id="wp_bnb_wc_enabled" value="yes"
|
||||||
|
<?php checked( 'yes', get_option( WooCommerceManager::OPTION_ENABLED, 'no' ) ); ?>
|
||||||
|
<?php disabled( ! $wc_active ); ?>>
|
||||||
|
<?php esc_html_e( 'Enable WooCommerce integration for bookings', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'Allow guests to book and pay through WooCommerce checkout.', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php esc_html_e( 'Auto-Confirm Bookings', 'wp-bnb' ); ?></th>
|
||||||
|
<td>
|
||||||
|
<label for="wp_bnb_wc_auto_confirm_booking">
|
||||||
|
<input type="checkbox" name="wp_bnb_wc_auto_confirm_booking" id="wp_bnb_wc_auto_confirm_booking" value="yes"
|
||||||
|
<?php checked( 'yes', get_option( WooCommerceManager::OPTION_AUTO_CONFIRM, 'yes' ) ); ?>
|
||||||
|
<?php disabled( ! $wc_active ); ?>>
|
||||||
|
<?php esc_html_e( 'Automatically confirm booking when payment is completed', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'If disabled, bookings will remain pending until manually confirmed.', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<?php if ( $wc_active ) : ?>
|
||||||
|
<p class="submit">
|
||||||
|
<input type="submit" name="submit" class="button button-primary"
|
||||||
|
value="<?php esc_attr_e( 'Save Settings', 'wp-bnb' ); ?>">
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</form>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render WooCommerce Products subtab.
|
||||||
|
*
|
||||||
|
* @param bool $wc_active Whether WooCommerce is active.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function render_wc_products_subtab( bool $wc_active ): void {
|
||||||
|
$categories = array();
|
||||||
|
if ( $wc_active ) {
|
||||||
|
$categories = get_terms(
|
||||||
|
array(
|
||||||
|
'taxonomy' => 'product_cat',
|
||||||
|
'hide_empty' => false,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<form method="post" action="">
|
||||||
|
<?php wp_nonce_field( 'wp_bnb_save_settings', 'wp_bnb_settings_nonce' ); ?>
|
||||||
|
<input type="hidden" name="wc_subtab" value="products">
|
||||||
|
|
||||||
|
<h2><?php esc_html_e( 'Product Synchronization', 'wp-bnb' ); ?></h2>
|
||||||
|
|
||||||
|
<table class="form-table" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php esc_html_e( 'Auto-Sync Products', 'wp-bnb' ); ?></th>
|
||||||
|
<td>
|
||||||
|
<label for="wp_bnb_wc_auto_sync_products">
|
||||||
|
<input type="checkbox" name="wp_bnb_wc_auto_sync_products" id="wp_bnb_wc_auto_sync_products" value="yes"
|
||||||
|
<?php checked( 'yes', get_option( WooCommerceManager::OPTION_AUTO_SYNC, 'yes' ) ); ?>
|
||||||
|
<?php disabled( ! $wc_active ); ?>>
|
||||||
|
<?php esc_html_e( 'Automatically create/update WooCommerce products when rooms are saved', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="wp_bnb_wc_product_category"><?php esc_html_e( 'Product Category', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<select name="wp_bnb_wc_product_category" id="wp_bnb_wc_product_category" <?php disabled( ! $wc_active ); ?>>
|
||||||
|
<option value=""><?php esc_html_e( '— No category —', 'wp-bnb' ); ?></option>
|
||||||
|
<?php foreach ( $categories as $cat ) : ?>
|
||||||
|
<option value="<?php echo esc_attr( $cat->term_id ); ?>"
|
||||||
|
<?php selected( get_option( WooCommerceManager::OPTION_PRODUCT_CATEGORY, 0 ), $cat->term_id ); ?>>
|
||||||
|
<?php echo esc_html( $cat->name ); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'Assign synced room products to this WooCommerce category.', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<?php if ( $wc_active ) : ?>
|
||||||
|
<h3><?php esc_html_e( 'Manual Sync', 'wp-bnb' ); ?></h3>
|
||||||
|
<p>
|
||||||
|
<button type="button" class="button bnb-sync-rooms-btn">
|
||||||
|
<span class="dashicons dashicons-update"></span>
|
||||||
|
<?php esc_html_e( 'Sync All Rooms Now', 'wp-bnb' ); ?>
|
||||||
|
</button>
|
||||||
|
<span class="sync-status"></span>
|
||||||
|
</p>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'Creates or updates WooCommerce products for all published rooms.', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="submit">
|
||||||
|
<input type="submit" name="submit" class="button button-primary"
|
||||||
|
value="<?php esc_attr_e( 'Save Settings', 'wp-bnb' ); ?>">
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</form>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render WooCommerce Orders subtab.
|
||||||
|
*
|
||||||
|
* @param bool $wc_active Whether WooCommerce is active.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function render_wc_orders_subtab( bool $wc_active ): void {
|
||||||
|
?>
|
||||||
|
<h2><?php esc_html_e( 'Order-Booking Status Mapping', 'wp-bnb' ); ?></h2>
|
||||||
|
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'The following table shows how WooCommerce order statuses map to booking statuses.', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table class="widefat fixed striped" style="max-width: 600px; margin-top: 15px;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><?php esc_html_e( 'WooCommerce Status', 'wp-bnb' ); ?></th>
|
||||||
|
<th><?php esc_html_e( 'Booking Status', 'wp-bnb' ); ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><?php esc_html_e( 'Pending Payment', 'wp-bnb' ); ?></td>
|
||||||
|
<td><span class="bnb-status-badge bnb-status-pending"><?php esc_html_e( 'Pending', 'wp-bnb' ); ?></span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><?php esc_html_e( 'On Hold', 'wp-bnb' ); ?></td>
|
||||||
|
<td><span class="bnb-status-badge bnb-status-pending"><?php esc_html_e( 'Pending', 'wp-bnb' ); ?></span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><?php esc_html_e( 'Processing', 'wp-bnb' ); ?></td>
|
||||||
|
<td>
|
||||||
|
<?php if ( get_option( WooCommerceManager::OPTION_AUTO_CONFIRM, 'yes' ) === 'yes' ) : ?>
|
||||||
|
<span class="bnb-status-badge bnb-status-confirmed"><?php esc_html_e( 'Confirmed', 'wp-bnb' ); ?></span>
|
||||||
|
<?php else : ?>
|
||||||
|
<span class="bnb-status-badge bnb-status-pending"><?php esc_html_e( 'Pending', 'wp-bnb' ); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><?php esc_html_e( 'Completed', 'wp-bnb' ); ?></td>
|
||||||
|
<td><span class="bnb-status-badge bnb-status-confirmed"><?php esc_html_e( 'Confirmed', 'wp-bnb' ); ?></span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><?php esc_html_e( 'Cancelled / Refunded / Failed', 'wp-bnb' ); ?></td>
|
||||||
|
<td><span class="bnb-status-badge bnb-status-cancelled"><?php esc_html_e( 'Cancelled', 'wp-bnb' ); ?></span></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3><?php esc_html_e( 'Refund Handling', 'wp-bnb' ); ?></h3>
|
||||||
|
<ul style="list-style: disc; margin-left: 20px;">
|
||||||
|
<li><?php esc_html_e( 'Full refunds automatically cancel the linked booking.', 'wp-bnb' ); ?></li>
|
||||||
|
<li><?php esc_html_e( 'Partial refunds are recorded but do not cancel the booking.', 'wp-bnb' ); ?></li>
|
||||||
|
<li><?php esc_html_e( 'Cancellation emails are sent when a booking is cancelled due to refund.', 'wp-bnb' ); ?></li>
|
||||||
|
</ul>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render WooCommerce Invoices subtab.
|
||||||
|
*
|
||||||
|
* @param bool $wc_active Whether WooCommerce is active.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function render_wc_invoices_subtab( bool $wc_active ): void {
|
||||||
|
?>
|
||||||
|
<form method="post" action="">
|
||||||
|
<?php wp_nonce_field( 'wp_bnb_save_settings', 'wp_bnb_settings_nonce' ); ?>
|
||||||
|
<input type="hidden" name="wc_subtab" value="invoices">
|
||||||
|
|
||||||
|
<h2><?php esc_html_e( 'Invoice Settings', 'wp-bnb' ); ?></h2>
|
||||||
|
|
||||||
|
<table class="form-table" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php esc_html_e( 'Attach to Emails', 'wp-bnb' ); ?></th>
|
||||||
|
<td>
|
||||||
|
<label for="wp_bnb_wc_invoice_attach">
|
||||||
|
<input type="checkbox" name="wp_bnb_wc_invoice_attach" id="wp_bnb_wc_invoice_attach" value="yes"
|
||||||
|
<?php checked( 'yes', get_option( WooCommerceManager::OPTION_INVOICE_ATTACH, 'yes' ) ); ?>
|
||||||
|
<?php disabled( ! $wc_active ); ?>>
|
||||||
|
<?php esc_html_e( 'Attach PDF invoice to order confirmation emails', 'wp-bnb' ); ?>
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="wp_bnb_invoice_prefix"><?php esc_html_e( 'Invoice Number Prefix', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="wp_bnb_invoice_prefix" id="wp_bnb_invoice_prefix"
|
||||||
|
value="<?php echo esc_attr( get_option( WooCommerceManager::OPTION_INVOICE_PREFIX, 'INV-' ) ); ?>"
|
||||||
|
class="regular-text" <?php disabled( ! $wc_active ); ?>>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'Prefix for invoice numbers (e.g., INV- for INV-00001).', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="wp_bnb_invoice_start_number"><?php esc_html_e( 'Starting Number', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="number" name="wp_bnb_invoice_start_number" id="wp_bnb_invoice_start_number"
|
||||||
|
value="<?php echo esc_attr( get_option( WooCommerceManager::OPTION_INVOICE_START_NUMBER, 1000 ) ); ?>"
|
||||||
|
class="small-text" min="1" <?php disabled( ! $wc_active ); ?>>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'Starting number for new invoices (only applies if no invoices generated yet).', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="wp_bnb_invoice_footer"><?php esc_html_e( 'Footer Text', 'wp-bnb' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<textarea name="wp_bnb_invoice_footer" id="wp_bnb_invoice_footer" rows="3" class="large-text"
|
||||||
|
<?php disabled( ! $wc_active ); ?>><?php echo esc_textarea( get_option( WooCommerceManager::OPTION_INVOICE_FOOTER, '' ) ); ?></textarea>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'Custom footer text for invoices (e.g., terms and conditions).', 'wp-bnb' ); ?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<?php if ( $wc_active ) : ?>
|
||||||
|
<p class="submit">
|
||||||
|
<input type="submit" name="submit" class="button button-primary"
|
||||||
|
value="<?php esc_attr_e( 'Save Settings', 'wp-bnb' ); ?>">
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</form>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save WooCommerce settings.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function save_woocommerce_settings(): void {
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in render_settings_page.
|
||||||
|
$subtab = isset( $_POST['wc_subtab'] ) ? sanitize_key( $_POST['wc_subtab'] ) : 'general';
|
||||||
|
|
||||||
|
switch ( $subtab ) {
|
||||||
|
case 'products':
|
||||||
|
$auto_sync = isset( $_POST['wp_bnb_wc_auto_sync_products'] ) ? 'yes' : 'no';
|
||||||
|
$category = isset( $_POST['wp_bnb_wc_product_category'] ) ? absint( $_POST['wp_bnb_wc_product_category'] ) : 0;
|
||||||
|
|
||||||
|
update_option( WooCommerceManager::OPTION_AUTO_SYNC, $auto_sync );
|
||||||
|
update_option( WooCommerceManager::OPTION_PRODUCT_CATEGORY, $category );
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'invoices':
|
||||||
|
$attach = isset( $_POST['wp_bnb_wc_invoice_attach'] ) ? 'yes' : 'no';
|
||||||
|
$prefix = isset( $_POST['wp_bnb_invoice_prefix'] ) ? sanitize_text_field( wp_unslash( $_POST['wp_bnb_invoice_prefix'] ) ) : 'INV-';
|
||||||
|
$start_number = isset( $_POST['wp_bnb_invoice_start_number'] ) ? absint( $_POST['wp_bnb_invoice_start_number'] ) : 1000;
|
||||||
|
$footer = isset( $_POST['wp_bnb_invoice_footer'] ) ? sanitize_textarea_field( wp_unslash( $_POST['wp_bnb_invoice_footer'] ) ) : '';
|
||||||
|
|
||||||
|
update_option( WooCommerceManager::OPTION_INVOICE_ATTACH, $attach );
|
||||||
|
update_option( WooCommerceManager::OPTION_INVOICE_PREFIX, $prefix );
|
||||||
|
update_option( WooCommerceManager::OPTION_INVOICE_START_NUMBER, $start_number );
|
||||||
|
update_option( WooCommerceManager::OPTION_INVOICE_FOOTER, $footer );
|
||||||
|
break;
|
||||||
|
|
||||||
|
default: // general
|
||||||
|
$enabled = isset( $_POST['wp_bnb_wc_enabled'] ) ? 'yes' : 'no';
|
||||||
|
$auto_confirm = isset( $_POST['wp_bnb_wc_auto_confirm_booking'] ) ? 'yes' : 'no';
|
||||||
|
|
||||||
|
update_option( WooCommerceManager::OPTION_ENABLED, $enabled );
|
||||||
|
update_option( WooCommerceManager::OPTION_AUTO_CONFIRM, $auto_confirm );
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
add_settings_error( 'wp_bnb_settings', 'settings_saved', __( 'WooCommerce settings saved.', 'wp-bnb' ), 'success' );
|
||||||
|
settings_errors( 'wp_bnb_settings' );
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AJAX handler for checking room availability.
|
* AJAX handler for checking room availability.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Plugin Name: WP BnB Management
|
* Plugin Name: WP BnB Management
|
||||||
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wp-bnb
|
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wp-bnb
|
||||||
* Description: A comprehensive Bed & Breakfast management system for WordPress. Manage buildings, rooms, bookings, and guests.
|
* Description: A comprehensive Bed & Breakfast management system for WordPress. Manage buildings, rooms, bookings, and guests.
|
||||||
* Version: 0.10.1
|
* Version: 0.11.0
|
||||||
* Requires at least: 6.0
|
* Requires at least: 6.0
|
||||||
* Requires PHP: 8.3
|
* Requires PHP: 8.3
|
||||||
* Author: Marco Graetsch
|
* Author: Marco Graetsch
|
||||||
@@ -24,7 +24,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Plugin version constant - MUST match Version in header above.
|
// Plugin version constant - MUST match Version in header above.
|
||||||
define( 'WP_BNB_VERSION', '0.10.1' );
|
define( 'WP_BNB_VERSION', '0.11.0' );
|
||||||
|
|
||||||
// Plugin path constants.
|
// Plugin path constants.
|
||||||
define( 'WP_BNB_PATH', plugin_dir_path( __FILE__ ) );
|
define( 'WP_BNB_PATH', plugin_dir_path( __FILE__ ) );
|
||||||
|
|||||||
Reference in New Issue
Block a user