PluginProbe
Easy Hotel – Powerful Hotel Booking / trunk
Easy Hotel – Powerful Hotel Booking vtrunk
2.0.8 2.0.7 2.0.6 2.0.5 2.0.4 2.0.3 2.0.2 2.0.1 2.0.0 1.9.9 1.9.8 1.9.7 1.9.6 1.9.5 1.9.4 1.9.3 1.9.2 1.8.1 1.8.2 1.8.3 1.8.4 1.8.5 1.8.6 1.8.7 1.8.8 All 110 releases
easy-hotel / admin / includes / classes / class.booking.php

class.booking.php in Easy Hotel – Powerful Hotel Booking trunk, at admin/includes/classes/class.booking.php

2,973 lines 112.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 use SureCart\Support\Currency;
3 use SureCart\Models\Product;
4 use SureCart\Models\Price;
5 use SureCart\Models\LineItem;
6 use SureCart\Models\ProductMedia;
7 use SureCart\Models\Order;
8 use SureCart\Models\Checkout;
9 use SureCart\Models\Customer;
10 use SureCart\Models\Coupon;
11
12 if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
13
14 class ESHB_Booking {
15
16 private static $_instance = null;
17 private $eshb_notice_printed = false;
18
19 public static function instance() {
20
21 if ( is_null( self::$_instance ) ) {
22 self::$_instance = new self();
23 }
24 return self::$_instance;
25
26 }
27
28 public function __construct() {
29
30 add_filter('litespeed_cacheable', [$this, 'disable_eshb_cache_from_cacheplugin'], 10, 1);
31
32 if(!is_admin()){
33 add_action( 'woocommerce_before_calculate_totals', [ $this, 'apply_custom_price_to_cart_item' ], 20, 1 );
34 add_action( 'woocommerce_checkout_create_order_line_item', [ $this, 'save_custom_meta_to_order' ], 10, 4 );
35 add_filter( 'woocommerce_get_item_data', [ $this, 'display_custom_meta_in_cart_and_order' ], 10, 2 );
36
37 add_action( 'woocommerce_thankyou', [$this, 'create_woocommerce_booking_on_checkout'], 10, 1 );
38 add_action( 'woocommerce_thankyou', [$this, 'capture_payment_after_checkout'], 20, 1 );
39 add_filter( 'woocommerce_hidden_order_itemmeta', [$this, 'hide_meta_from_display'] );
40 add_filter( 'woocommerce_order_item_get_formatted_meta_data', [$this, 'unset_specific_order_item_meta_data'], 10, 2 );
41 add_action( 'woocommerce_cart_item_removed', [ $this, 'eshb_clear_cart_block_on_removal' ], 10, 2 );
42 add_action( 'woocommerce_thankyou', [ $this, 'eshb_clear_session_cart_blocks_on_order' ], 5, 1 );
43 }
44
45 add_action( 'woocommerce_order_status_changed', [$this, 'update_booking_status_on_woocommerce_order_status_change'], 10, 4 );
46 add_action( 'save_post_eshb_booking', [$this, 'update_woocommerce_order_status_on_booking_status_change'], 20, 3 );
47 add_action( 'save_post_eshb_booking', [$this, 'send_booking_email_notification_for_booking_status'], 20, 3 );
48 add_filter('woocommerce_payment_complete_order_status', [$this, 'filter_status_based_on_booking_meta'], 10, 2);
49
50 add_action( "wp_ajax_nopriv_eshb_get_extra_services_charge", [$this, 'eshb_get_extra_services_charge'] );
51 add_action( "wp_ajax_eshb_get_extra_services_charge", [$this, 'eshb_get_extra_services_charge'] );
52 add_action( "wp_ajax_nopriv_eshb_get_booking_prices", [$this, 'eshb_get_booking_prices'] );
53 add_action( "wp_ajax_eshb_get_booking_prices", [$this, 'eshb_get_booking_prices'] );
54 add_action( "wp_ajax_nopriv_eshb_add_to_cart_reservation", [$this, 'eshb_add_to_cart_reservation'] );
55 add_action( "wp_ajax_eshb_add_to_cart_reservation", [$this, 'eshb_add_to_cart_reservation'] );
56 add_action( "wp_ajax_nopriv_eshb_send_reservation_request", [$this, 'eshb_send_reservation_request'] );
57 add_action( "wp_ajax_eshb_send_reservation_request", [$this, 'eshb_send_reservation_request'] );
58 add_action( 'wp_ajax_nopriv_eshb_get_accomodation_available_capacity_counts', [ $this, 'eshb_get_accomodation_available_capacity_counts' ] );
59 add_action( 'wp_ajax_eshb_get_accomodation_available_capacity_counts', [ $this, 'eshb_get_accomodation_available_capacity_counts' ] );
60 add_action( 'wp_ajax_nopriv_eshb_get_available_rooms_counts_data', [ $this, 'eshb_get_available_rooms_counts_data' ] );
61 add_action( 'wp_ajax_eshb_get_available_rooms_counts_data', [ $this, 'eshb_get_available_rooms_counts_data' ] );
62 add_action( 'wp_ajax_nopriv_eshb_get_cart_blocks', [ $this, 'eshb_get_cart_blocks_ajax' ] );
63 add_action( 'wp_ajax_eshb_get_cart_blocks', [ $this, 'eshb_get_cart_blocks_ajax' ] );
64 add_action( 'wp_ajax_nopriv_eshb_clear_cart_on_block_expiry', [ $this, 'eshb_clear_cart_on_block_expiry' ] );
65 add_action( 'wp_ajax_eshb_clear_cart_on_block_expiry', [ $this, 'eshb_clear_cart_on_block_expiry' ] );
66 add_action( 'wp_ajax_nopriv_eshb_check_my_session_blocks', [ $this, 'eshb_check_my_session_blocks' ] );
67 add_action( 'wp_ajax_eshb_check_my_session_blocks', [ $this, 'eshb_check_my_session_blocks' ] );
68 add_filter( 'eshb_get_disabled_dates_in_search', [ $this, 'eshb_add_blocked_dates_to_calendar' ], 10, 3 );
69 // Inline notice: shortcode-based cart/checkout (block themes use JS injection instead)
70 add_action( 'woocommerce_before_cart', [ $this, 'eshb_render_cart_block_notice_inline' ] );
71 add_action( 'woocommerce_before_checkout_form', [ $this, 'eshb_render_cart_block_notice_inline' ] );
72 // Cart item remove via AJAX on checkout (NOTE: woocommerce_checkout_cart_item_quantity filter
73 // is registered once at file load, not in the constructor — see bottom of file. Constructor
74 // runs each time `new ESHB_Booking()` is called, which would duplicate the filter.)
75 add_action( 'wp_ajax_nopriv_eshb_remove_cart_item', [ $this, 'eshb_remove_cart_item' ] );
76 add_action( 'wp_ajax_eshb_remove_cart_item', [ $this, 'eshb_remove_cart_item' ] );
77 add_action( 'eshb_before_availability_calendar', [ $this, 'eshb_render_calendar_legend' ], 10, 1 );
78
79 add_action('rest_api_init', function() {
80 register_rest_route('eshb/v1', '/booking-prices', [
81 'methods' => 'POST',
82 'callback' => [ $this, 'eshb_get_booking_prices_rest' ], // class method
83 'permission_callback' => function($request) {
84 return true;
85 },
86 'args' => [
87 'accomodationId' => [
88 'required' => true,
89 'sanitize_callback' => 'absint',
90 ],
91 'startDate' => [
92 'sanitize_callback' => 'sanitize_text_field',
93 ],
94 'endDate' => [
95 'sanitize_callback' => 'sanitize_text_field',
96 ],
97 'startTime' => [
98 'sanitize_callback' => 'sanitize_text_field',
99 ],
100 'endTime' => [
101 'sanitize_callback' => 'sanitize_text_field',
102 ],
103 'roomQuantity' => [
104 'sanitize_callback' => 'absint',
105 ],
106 'extraBedQuantity' => [
107 'sanitize_callback' => 'absint',
108 ],
109 'adultQuantity' => [
110 'sanitize_callback' => 'absint',
111 ],
112 'childrenQuantity' => [
113 'sanitize_callback' => 'absint',
114 ],
115 'selectedServices' => [
116 'sanitize_callback' => function($param) {
117 // Cap each line at the service's Max Quantity so the
118 // quoted price can never exceed the configured limit.
119 return ESHB_Helper::eshb_clamp_selected_services(
120 json_decode(sanitize_text_field($param), true)
121 );
122 }
123 ],
124 'nonce' => [
125 'required' => true,
126 'sanitize_callback' => 'sanitize_text_field',
127 ],
128 ],
129 ]);
130 });
131
132
133 }
134
135 function disable_eshb_cache_from_cacheplugin($cacheable){
136 // Disable LSCache for accommodation single pages
137 if( is_singular('eshb_accomodation') ){
138 return false; // don't cache
139 }
140
141 // Never cache the Native Checkout / customer account pages: they are
142 // per-visitor (reservation token + a freshly generated nonce baked
143 // into the page). A shared cache would serve a stale nonce to other
144 // visitors — which makes checkout fail for logged-out users (logged-in
145 // users bypass the cache, so it "only works when logged in").
146 if ( is_singular() ) {
147 global $post;
148 if ( $post instanceof WP_Post ) {
149 $eshb_content = (string) $post->post_content;
150 if ( has_shortcode( $eshb_content, 'eshb_native_checkout' ) || has_shortcode( $eshb_content, 'eshb_account' ) ) {
151 return false; // don't cache
152 }
153 }
154 }
155
156 return $cacheable;
157 }
158
159 public function update_product_price_by_id($product_id, $new_price) {
160 // Update the regular price
161 update_post_meta($product_id, '_regular_price', $new_price);
162
163 // Update the price (current price)
164 update_post_meta($product_id, '_price', $new_price);
165
166 // Optional: if the product is on sale, update the sale price to match the new price
167 //update_post_meta($product_id, '_sale_price', $new_price);
168
169 // Clear WooCommerce product cache
170 wc_delete_product_transients($product_id);
171 }
172
173 public function get_booked_dates($accomodation_id, $start_date = '', $end_date = '') {
174
175 $single_day_booking = false;
176 if($start_date == $end_date){
177 $single_day_booking = true;
178 }
179
180 $booked_dates = [];
181 $total_room_bookings_per_date = []; // Track room bookings per date
182 $eshb_settings = get_option('eshb_settings', []);
183
184 // Get total rooms allowed for this accommodation
185 $accomodation_metas = get_post_meta($accomodation_id, 'eshb_accomodation_metaboxes', true);
186 $allowed_total_rooms = isset($accomodation_metas['total_rooms']) ? intval($accomodation_metas['total_rooms']) : 0;
187
188 // Find all bookings for this accommodation
189 $bookings_args = [
190 'post_type' => 'eshb_booking',
191 'posts_per_page' => -1,
192 'post_status' => ['publish', 'deposit-payment', 'pending', 'processing', 'on-hold', 'completed'],
193 'fields' => 'ids',
194 ];
195
196 $bookings = new WP_Query($bookings_args);
197
198 if ($bookings->have_posts()) {
199 while ($bookings->have_posts()) {
200 $bookings->the_post();
201 $meta_values = get_post_meta(get_the_ID(), 'eshb_booking_metaboxes', true);
202 $booking_status = isset($meta_values['booking_status']) ? $meta_values['booking_status'] : '';
203
204 $completed_booking_status_maps = array(
205 'pending',
206 'deposit-payment',
207 'processing',
208 'on-hold',
209 'completed'
210 );
211
212 // Ensure required keys exist
213 if (is_array($meta_values) && in_array($booking_status, $completed_booking_status_maps) && isset($meta_values['booking_accomodation_id'], $meta_values['booking_start_date'], $meta_values['booking_end_date'])) {
214 $booking_accomodation_id = intval($meta_values['booking_accomodation_id']);
215 $booking_start_date = $meta_values['booking_start_date'];
216 $booking_end_date = $meta_values['booking_end_date'];
217
218 $booked_accomodation_ids = [];
219 $booking_accomodation_id = ESHB_Helper::get_main_post_id_for_translated($booking_accomodation_id);
220
221 if ($booking_accomodation_id == $accomodation_id) {
222 // Convert dates to DateTime objects
223 $booking_start_date = new DateTime($booking_start_date);
224 $booking_end_date = new DateTime($booking_end_date);
225
226 // if($booking_end_date == $booking_end_date) {
227 // $booking_end_date->modify('+1 day');
228 // }
229
230 while ($booking_start_date <= $booking_end_date) {
231
232 $formatted_date = $booking_start_date->format('Y-m-d');
233
234 if($meta_values['booking_end_date'] != $formatted_date){
235 $booked_id = get_the_ID();
236 $booking_metas = get_post_meta($booked_id, 'eshb_booking_metaboxes', true);
237 $booked_room_quantity = isset($booking_metas['room_quantity']) ? intval($booking_metas['room_quantity']) : 0;
238
239
240 // Track total booked rooms per date
241 if (!isset($total_room_bookings_per_date[$formatted_date])) {
242 $total_room_bookings_per_date[$formatted_date] = 0;
243 }
244 $total_room_bookings_per_date[$formatted_date] = $booked_room_quantity;
245 }
246 // Move to next day
247 $booking_start_date->modify('+1 day');
248 }
249
250 }
251 }
252 }
253 }
254
255 if(!$single_day_booking){
256 unset($total_room_bookings_per_date[$end_date]);
257 }
258
259 // Determine which dates should be disabled (fully booked)
260 foreach ($total_room_bookings_per_date as $date => $total_booked_rooms) {
261
262 // skip this booking if slots hours available
263 // allow external plugins to modify available slots
264 $available_slots = apply_filters(
265 'eshb_available_slots_in_booked_dates_loop',
266 false,
267 $date,
268 $total_booked_rooms,
269 $accomodation_id
270 );
271
272 if($available_slots) {
273 $total_booked_rooms = 0;
274 }
275
276
277 if ($allowed_total_rooms <= $total_booked_rooms) {
278 $date_obj = new DateTime($date);
279 $booked_dates[] = $date_obj->format('Y, m, d');
280 }
281 }
282
283
284 // Remove duplicate dates
285 $booked_dates = array_unique($booked_dates);
286
287 // Apply filter hook to allow external modifications
288 $booked_dates = apply_filters('eshb_modify_booked_dates', $booked_dates, $accomodation_id);
289
290
291 return $booked_dates;
292 }
293
294 public function get_holiday_dates($accomodation_id, $start_date = '', $end_date = '') {
295
296 $eshb_settings = get_option('eshb_settings', []);
297
298 $holidays = isset($eshb_settings['holidays']) ? $eshb_settings['holidays'] : [];
299
300 $finally_holidays = [];
301
302 if ( ! empty( $holidays ) ) {
303 foreach ( $holidays as $holiday ) {
304 $accommodation_ids = $holiday['accomodation-ids'] ?? [];
305 $holiday_date = $holiday['holiday-date'] ?? null;
306
307
308 // Check if holiday is applicable for all or specific accommodation
309 $is_applicable = empty($accommodation_ids) || (is_array($accommodation_ids) && in_array($accomodation_id, $accommodation_ids));
310
311 if ( $is_applicable && $holiday_date ) {
312 if ( is_array($holiday_date) && isset($holiday_date['from'], $holiday_date['to']) ) {
313 $start = DateTime::createFromFormat('Y-m-d', $holiday_date['from']);
314 $end = DateTime::createFromFormat('Y-m-d', $holiday_date['to']);
315 if ( $start && $end ) {
316 $end->modify('+1 day'); // Include end date
317 $interval = new DateInterval('P1D');
318 $dateRange = new DatePeriod($start, $interval, $end);
319
320 foreach ( $dateRange as $date ) {
321 $finally_holidays[] = $date->format('Y-m-d');
322 }
323 }
324 } else {
325 $finally_holidays[] = $holiday_date;
326 }
327 }
328 }
329 }
330
331 return $finally_holidays;
332 }
333
334 public function get_holiday_dates_in_date_ranges($accomodation_id, $start_date, $end_date){
335
336 $overlap = [];
337 if(!empty($accomodation_id)){
338 $holiday_dates = $this->get_holiday_dates($accomodation_id, $start_date, $end_date);
339 $overlap = [];
340 // Generate all dates in the range
341 $range_dates = [];
342 $current_date = new DateTime($start_date);
343 $end_date_obj = new DateTime($end_date);
344
345 while ($current_date <= $end_date_obj) {
346 $range_dates[] = $current_date->format('Y-m-d');
347 $current_date->modify('+1 day');
348 }
349
350 // Check for overlap
351 $overlap = array_intersect($range_dates, $holiday_dates);
352 }
353 return $overlap;
354 }
355
356 public function get_actual_booked_dates_in_date_ranges($accomodation_id, $start_date, $end_date){
357
358 $booked_dates = $this->get_booked_dates($accomodation_id, $start_date, $end_date); // retun arary like ['2025, 01, 27', '2025, 01, 28', '2025, 01, 13']
359
360 // Format booked dates to 'Y-m-d' for comparison
361 $booked_dates = array_map(function($date) {
362 return DateTime::createFromFormat('Y, m, d', $date)->format('Y-m-d');
363 }, $booked_dates);
364
365 // Generate all dates in the range
366 $range_dates = [];
367 $current_date = new DateTime($start_date);
368 $end_date_obj = new DateTime($end_date);
369
370 while ($current_date <= $end_date_obj) {
371 $range_dates[] = $current_date->format('Y-m-d');
372 $current_date->modify('+1 day');
373 }
374
375 $single_day_booking = false;
376 if($start_date == $end_date){
377 $single_day_booking = true;
378 }
379
380 if(!$single_day_booking){
381 array_pop($range_dates); // Remove the last date if it's a range booking
382 }
383
384 // Check for overlap
385 $overlap = array_intersect($range_dates, $booked_dates);
386
387 return $overlap;
388
389 }
390
391 public function save_booking_request($booking_args = array(), $customer_args = array()) {
392 $defaults = array(
393 'booking_accomodation_id' => '',
394 'booking_start_date' => '',
395 'booking_end_date' => '',
396 'room_quantity' => 1,
397 'extra_bed_quantity'=> 0,
398 'adult_quantity' => 1,
399 'children_quantity' => 0,
400 'details_html' => '',
401 'extra_services' => array(),
402 'extra_services_html' => '',
403 'booking_status' => 'pending'
404 );
405
406 // Merge with default values
407 $booking_args = wp_parse_args($booking_args, $defaults);
408
409 $accomodation_id = $booking_args['booking_accomodation_id'];
410
411 if(!empty($accomodation_id)){
412
413 $start_date = $booking_args['booking_start_date'];
414 $end_date = $booking_args['booking_end_date'];
415
416 // Insert the post
417 $post_id = wp_insert_post(array(
418 'post_type' => 'eshb_booking_request',
419 'post_title' => 'Booking request for ' . $start_date . ' - ' . $end_date,
420 'post_status' => 'publish',
421 'meta_input' => array(
422 'eshb_booking_request_metaboxes' => $booking_args,
423 'eshb_booking_request_customer_metaboxes' => $customer_args,
424 )
425 ));
426
427 // Check if the post was inserted successfully
428 if (!is_wp_error($post_id)) {
429 // Update the post title to include the post ID
430
431 $accomodation_title = get_the_title($accomodation_id);
432
433 wp_update_post(array(
434 'ID' => $post_id,
435 'post_title' => 'Booking request #' . $post_id . ' for: ' . $accomodation_title,
436 'post_status' => 'publish' // Update status to 'publish'
437 ));
438 }
439
440 }
441
442 }
443
444 public function eshb_send_reservation_request(){
445
446 // Verify nonce for security
447 if (!isset($_POST['nonce']) || !wp_verify_nonce( sanitize_text_field(wp_unslash($_POST['nonce'])), ESHB_Helper::generate_secure_nonce_action('eshb_global_nonce_action'))) {
448
449 $error = array(
450 'code' => 'invalid_nonce',
451 'message' => __('invalid_nonce', 'easy-hotel')
452 );
453
454 wp_send_json_error([
455 'error' => $error,
456 ]);
457
458 die();
459 }
460
461
462 if(isset($_POST['accomodationId']) && !empty($_POST['accomodationId'])){
463
464 $hotel_core = new ESHB_Core();
465 $customer = !empty($_POST['customerInfo']) ? map_deep( sanitize_text_field( wp_unslash($_POST['customerInfo'])), 'sanitize_text_field' ) : [];
466 $accomodation_id = isset( $_POST['accomodationId'] ) ? (int) sanitize_text_field( wp_unslash($_POST['accomodationId']) ) : '';
467 $today_date = esc_html(gmdate('Y-m-d')); // Get today's date
468 $date = new DateTime($today_date); // Create a DateTime object from today's date
469 $date->modify('+1 day'); // Add one day
470 $tomorrow = $date->format('Y-m-d'); // Get the new date in 'Y-m-d' format
471 $start_date = isset( $_POST['startDate'] ) ? sanitize_text_field( wp_unslash($_POST['startDate']) ) : $today_date;
472 $end_date = isset( $_POST['endDate'] ) ? sanitize_text_field( wp_unslash($_POST['endDate']) ) : $tomorrow;
473 $start_time = isset( $_POST['startTime'] ) ? sanitize_text_field( wp_unslash($_POST['startTime']) ) : '';
474 $end_time = isset( $_POST['endTime'] ) ? sanitize_text_field( wp_unslash($_POST['endTime']) ) : '';
475 $start_day_name = $start_date ? strtolower(gmdate('l', strtotime($start_date))) : '';
476 $accomodation_metaboxes = get_post_meta($accomodation_id, 'eshb_accomodation_metaboxes', true);
477 $eshb_settings = get_option( 'eshb_settings', [] );
478 $string_booking_success_msg = isset($eshb_settings['string_booking_request_success_msg']) && !empty($eshb_settings['string_booking_request_success_msg']) ? $eshb_settings['string_booking_request_success_msg'] : 'Booking request has been sent successfully.';
479 $string_booking_failed_msg = isset($eshb_settings['string_booking_request_failed_msg']) && !empty($eshb_settings['string_booking_request_failed_msg']) ? $eshb_settings['string_booking_request_failed_msg'] : 'Ops! Booking request can\'t be sent.';
480 $string_already_booked_msg = isset($eshb_settings['string_already_booked_msg']) && !empty($eshb_settings['string_already_booked_msg']) ? $eshb_settings['string_already_booked_msg'] : 'Ops! The date range is already booked:';
481 $room_visibility = in_array('rooms', $eshb_settings['booking-form-fields']) ? true : false;
482 $adult_visibility = in_array('adults', $eshb_settings['booking-form-fields']) ? true : false;
483 $childrens_visibility = in_array('childrens', $eshb_settings['booking-form-fields']) ? true : false;
484 $extra_beds_visibility = in_array('extra_beds', $eshb_settings['booking-form-fields']) ? true : false;
485
486 $eshb_week_settings = [
487 'string_check_in_day_error_msg' => 'Only allowed check in day is',
488 'eshb_booking_allowed_check_in_day' => 'all',
489 ];
490 $eshb_week_settings = apply_filters( 'eshb_week_settings', $eshb_week_settings );
491 $allowed_check_in_day = apply_filters( 'eshb_booking_allowed_check_in_day', 'all', $accomodation_id, $accomodation_metaboxes );
492 $string_check_in_day_error_msg = !empty($eshb_week_settings['string_check_in_day_error_msg']) ? $eshb_week_settings['string_check_in_day_error_msg'] : 'Only allowed check in day is : ';
493
494
495 // Min Max Booking Configurations
496 $min_max_settings = [
497 'required_min_nights' => '',
498 'required_max_nights' => '',
499 'is_global_source_for_min_max' => true,
500 ];
501 $min_max_settings = apply_filters( 'eshb_min_max_settings', $min_max_settings, $accomodation_id, $accomodation_metaboxes );
502 $required_min_nights = isset($min_max_settings['required_min_nights']) ? $min_max_settings['required_min_nights'] : '';
503 $required_max_nights = isset($min_max_settings['required_max_nights']) ? $min_max_settings['required_max_nights'] : '';
504 $string_required_minimum_nights_msg = esc_html__( 'Oops! Your reservation couldn\'t be completed. A minimum stay of', 'easy-hotel' );
505 $string_required_maximum_nights_msg = esc_html__( 'Oops! Your reservation couldn\'t be completed. A maximum stay of', 'easy-hotel' );
506
507 $min_stay_night_by_session = ESHB_Helper::get_eshb_min_stay_night_by_session($accomodation_id, $start_date, $end_date);
508
509 // get booked dates in the range
510 $actual_booked_dates = $this->get_actual_booked_dates_in_date_ranges($accomodation_id, $start_date, $end_date);
511
512 if(!empty($actual_booked_dates)){
513
514 $booked_dates_str = implode( ', ', array_map( 'esc_html', $actual_booked_dates ) );
515
516 $error_message = sprintf(
517 /* translators: 1: already booked message text, 2: list of booked dates */
518 esc_html__( '%1$s %2$s', 'easy-hotel' ),
519 esc_html( $string_already_booked_msg ),
520 $booked_dates_str
521 );
522
523 $error = array(
524 'code' => 'already_booked',
525 'message' => $error_message,
526 );
527
528 wp_send_json_error([
529 'error' => $error,
530 'booked_dates' => implode(', ', $actual_booked_dates),
531 ]);
532
533 wp_die();
534
535 }
536
537 // check rooms capacity
538 if(isset($_POST['roomQuantity']) && !empty($_POST['roomQuantity'])){
539 $roomQuantity = isset( $_POST['roomQuantity'] ) ? (int) sanitize_text_field( wp_unslash($_POST['roomQuantity'] ) ) : 1;
540
541 $availableRoom = $this->get_available_room_count_by_date_range($accomodation_id, $start_date, $end_date);
542
543 if($roomQuantity > $availableRoom){
544 $error = array(
545 'code' => 'room_capacity_not_enough',
546 'message' => sprintf(
547 /* translators: %s: number of available rooms */
548 esc_html__( 'Selected room is not available. Available room: %s', 'easy-hotel' ),
549 esc_html( $availableRoom )
550 ),
551 );
552
553 wp_send_json_error([
554 'error' => $error,
555 ]);
556 }
557
558 }
559
560
561 // Validate check-in day
562 if($allowed_check_in_day != 'all' && $start_day_name != '' && !empty($allowed_check_in_day) && strpos($allowed_check_in_day, $start_day_name) === false){
563 $error = array(
564 'code' => 'check_in_day_error',
565 'message' => sprintf(
566 /* translators: %s: allowed check-in day */
567 esc_html__( '%1$s %1$s', 'easy-hotel' ),
568 esc_html( $string_check_in_day_error_msg ),
569 esc_html( $allowed_check_in_day )
570 ),
571 );
572
573 wp_send_json_error([
574 'error' => $error,
575 ]);
576 }
577
578 $start = new DateTime($start_date);
579 $end = new DateTime($end_date);
580 $interval = $start->diff($end);
581 $days_count = $interval->days;
582 $room_quantity = isset( $_POST['roomQuantity'] ) ? (int) sanitize_text_field( wp_unslash($_POST['roomQuantity'] ) ) : 1;
583 $extra_bed_quantity = isset( $_POST['extraBedQuantity'] ) ? (int) sanitize_text_field( wp_unslash($_POST['extraBedQuantity'] ) ) : 0;
584 $adult_quantity = isset( $_POST['adultQuantity'] ) ? (int) sanitize_text_field( wp_unslash($_POST['adultQuantity'] ) ) : 1;
585 $children_quantity = isset( $_POST['childrenQuantity'] ) ? (int) sanitize_text_field( wp_unslash($_POST['childrenQuantity'] ) ) : 0;
586 $total_guest_quantity = $adult_quantity + $children_quantity;
587
588 $selected_services = [];
589 if(isset($_POST['selectedServices']) && !empty($_POST['selectedServices'])){
590
591 $selected_services = sanitize_text_field(wp_unslash($_POST['selectedServices']));
592 $selected_services = json_decode($selected_services, true);
593
594 // Cap each line at the service's Max Quantity before it is
595 // priced, described or stored on the booking.
596 $selected_services = ESHB_Helper::eshb_clamp_selected_services($selected_services);
597
598 }
599
600
601 if(!empty($allowed_check_in_day) && $allowed_check_in_day != 'all'){
602 $days_count = 1;
603 }
604
605 // validate min stay night by session
606 if(class_exists('ESHB_ADVANCED_PRICING') && !empty($min_stay_night_by_session) && $days_count < $min_stay_night_by_session){
607 $error = array(
608 'code' => 'min_stay_night_by_session',
609 'message' => sprintf(
610 /* translators: 1: minimum nights message text, 2: required minimum nights */
611 esc_html__( '%1$s %2$s nights!', 'easy-hotel' ),
612 esc_html( $string_required_minimum_nights_msg ),
613 esc_html( $min_stay_night_by_session )
614 ),
615 );
616 wp_send_json_error([
617 'error' => $error,
618 ]);
619 }
620
621 // validate minimum nights
622 if(!empty($required_min_nights) && $days_count < $required_min_nights){
623 $error = array(
624 'code' => 'required_min_nights',
625 'message' => sprintf(
626 /* translators: 1: minimum nights message text, 2: required minimum nights */
627 esc_html__( '%1$s %2$s nights!', 'easy-hotel' ),
628 esc_html( $string_required_minimum_nights_msg ),
629 esc_html( $required_min_nights )
630 ),
631 );
632
633 wp_send_json_error([
634 'error' => $error,
635 ]);
636 }
637 if(!empty($required_max_nights) && $days_count > $required_max_nights){
638 $error = array(
639 'code' => 'required_max_nights',
640 'message' => sprintf(
641 /* translators: 1: maximum nights message text, 2: required maximum nights */
642 esc_html__( '%1$s %2$s nights allowed!', 'easy-hotel' ),
643 esc_html( $string_required_maximum_nights_msg ),
644 esc_html( $required_max_nights )
645 ),
646 );
647
648 wp_send_json_error([
649 'error' => $error,
650 ]);
651 }
652
653
654
655
656 if (get_post_type($accomodation_id) === 'eshb_accomodation') {
657
658 $details = [];
659
660 if ($room_visibility && $room_quantity > 0) {
661 $details[] = 'Room: ' . $room_quantity;
662 }
663 if ($extra_beds_visibility && $extra_bed_quantity > 0) {
664 $details[] = 'Extra Bed: ' . $extra_bed_quantity;
665 }
666 if ($adult_visibility && $adult_quantity > 0) {
667 $details[] = 'Adult: ' . $adult_quantity;
668 }
669 if ($childrens_visibility && $children_quantity > 0) {
670 $details[] = 'Children: ' . $children_quantity;
671 }
672
673 $details_html = implode(', ', $details);
674
675 $extra_service_titles = [];
676 $extra_services_html = '';
677
678 if(!empty($selected_services) && is_array($selected_services) && count($selected_services) > 0 ){
679
680 foreach ($selected_services as $service) {
681
682 $service_id = $service['id'];
683
684 $service_quantity = $service['quantity'];
685
686 if(!empty($service_quantity)){
687 $eshb_service_metaboxes = get_post_meta($service_id, 'eshb_service_metaboxes', true);
688
689 $charge_type = $eshb_service_metaboxes['service_charge_type'];
690
691 $service_title = get_the_title($service_id) . ' For ' . $service_quantity . ' ' . ucfirst(strtolower($charge_type));
692
693 if(!$room_visibility && $charge_type == 'room'){
694 $service_title = get_the_title($service_id);
695 }
696 }else{
697 $service_title = get_the_title($service_id);
698 }
699
700 array_push($extra_service_titles, $service_title);
701
702 }
703 $extra_services_html = implode(", ",$extra_service_titles);
704 }
705
706
707 $pricing = $this->calculate_booking_pricing($accomodation_id, $start_date, $end_date, $room_quantity, $extra_bed_quantity, $adult_quantity, $children_quantity, $selected_services, true, $start_time, $end_time);
708
709 $base_price = $pricing['basePrice'];
710 $extra_services_charge = $pricing['extraServicesPrice'];
711 $extra_bed_price= $pricing['extraBedPrice'];
712 $subtotal_price = $pricing['subtotalPrice'];
713 $total_price = $pricing['totalPrice'];
714
715 $dates = ESHB_Helper::eshb_format_date( $start_date ) .' - '. ESHB_Helper::eshb_format_date( $end_date );
716 if($start_date == $end_date){
717 $dates = ESHB_Helper::eshb_format_date( $start_date );
718 }
719
720
721 $times = '';
722 if(!empty($start_time) && !empty($end_time)) {
723 // Get WP formatted time (according to Settings > General > Time Format)
724 $formated_start_timestamp = strtotime( $start_time );
725 $formated_end_timestamp = strtotime( $end_time );
726 $wp_time_format = get_option( 'time_format' ); // WP settings
727 $formatted_start_time = wp_date( $wp_time_format, $formated_start_timestamp );
728 $formatted_end_time = wp_date( $wp_time_format, $formated_end_timestamp );
729 $times = $formatted_start_time . ' - ' . $formatted_end_time;
730 }
731
732
733 $accomodation_title = get_the_title($accomodation_id);
734
735 $customer_name = $customer['name'];
736 $customer_email = $customer['email'];
737 $customer_phone = $customer['phone'];
738 $customer_message = $customer['message'];
739
740 $admin_email = get_option('admin_email');
741 $recipent_email = $eshb_settings['recipent_email'];
742 $recipent_email = empty($recipent_email) ? $admin_email : $recipent_email;
743 $from_name = wp_parse_url(get_site_url(), PHP_URL_HOST);
744
745 $subject = __('Booking Request!', 'easy-hotel');
746
747 $message = '';
748 $message .= '<div>';
749 $message .= '<h2>Great news!</h2><p>You have a new booking request.</p>';
750 $message .= '<h3>Customer Details</h3>';
751 $message .= '<table style="text-align:left;">';
752 $message .= '<tr><th>Name: ' . $customer_name . '</th></tr>';
753 $message .= '<tr><th>Email: ' . $customer_email . '</th></tr>';
754 $message .= '<tr><th>Phone: ' . $customer_phone . '</th></tr>';
755 $message .= '</table>';
756 $message .= '<h3>Booking Details</h3>';
757 $message .= '<table style="text-align:left;">';
758 $message .= '<tr><th>Accomodation: ' . $accomodation_title . '</th></tr>';
759 $message .= '<tr><th>Date: ' . ESHB_Helper::eshb_format_date( $start_date ) .' - '. ESHB_Helper::eshb_format_date( $end_date ) . '</th></tr>';
760
761 if(!empty($times)) {
762 $message .= '<tr><th>Times: ' . $times . '</th></tr>';
763 }
764
765 $message .= '<tr><th>Details: ' . $details_html . '</th></tr>';
766
767 if(!empty($extra_services_html)){
768 $message .= '<tr><th>Exra Services: ' . $extra_services_html . '</th></tr>';
769 }
770
771 $message .= '</table>';
772 $message .= '<h5>Message:</h5><p>'.$customer_message.'</p>';
773 $message .= '</div>';
774
775 // Prepare the data array to store in post meta
776 $form_data = [
777 'booking_status' => 'processing',
778 'booking_accomodation_id' => $accomodation_id,
779 'total_price' => $total_price,
780 'subtotal_price' => $subtotal_price,
781 'base_price' => $base_price,
782 'extra_service_price' => $extra_services_charge,
783 'extra_bed_price' => $extra_bed_price,
784 'booking_start_date' => $start_date,
785 'booking_end_date' => $end_date,
786 'booking_start_time' => $start_time,
787 'booking_end_time' => $end_time,
788 'dates' => $dates,
789 'room_quantity' => $room_quantity,
790 'extra_bed_quantity' => $extra_bed_quantity,
791 'adult_quantity' => $adult_quantity,
792 'children_quantity' => $children_quantity,
793 'extra_services' => $selected_services,
794 'details_html' => $details_html,
795 'extra_services_html' => $extra_services_html,
796 'customer' => $customer
797 ];
798
799 if(!empty($start_time)) {
800 $form_data['start_time'] = $start_time;
801 }
802
803 if(!empty($end_time)) {
804 $form_data['end_time'] = $end_time;
805 }
806
807 if(!empty($times)) {
808 $form_data['times'] = $times;
809 }
810
811 // Insert the post
812 $post_id = wp_insert_post(array(
813 'post_type' => 'eshb_booking',
814 'post_title' => 'Booking for ' . $start_date . ' - ' . $end_date,
815 'post_status' => 'publish',
816 'meta_input' => array(
817 'eshb_booking_metaboxes' => $form_data
818 )
819 ));
820
821 // Check if the post was inserted successfully
822
823 if (!is_wp_error($post_id)) {
824
825 // Update the post title to include the post ID
826 wp_update_post(array(
827 'ID' => $post_id,
828 'post_title' => 'Booking #' . $post_id . ' for: ' . $accomodation_title,
829 'post_status' => 'publish' // Update status to 'publish'
830 ));
831
832 // Update Available Rooms Count For This Accomodation
833 $accomodation_metaboxes = get_post_meta( $accomodation_id, 'eshb_accomodation_metaboxes', true );
834 $total_rooms = !empty($accomodation_metaboxes['total_rooms']) ? floatval($accomodation_metaboxes['total_rooms']) : 1;
835 $current_available_rooms = !empty($accomodation_metaboxes['available_rooms']) ? floatval($accomodation_metaboxes['available_rooms']) : 0;
836
837 if(!empty($current_available_rooms)){
838 $available_rooms = $current_available_rooms - floatval($room_quantity);
839 }else{
840 $available_rooms = $total_rooms - floatval($room_quantity);
841 }
842
843 $accomodation_metaboxes['available_rooms'] = $available_rooms;
844 update_post_meta($accomodation_id, 'eshb_accomodation_metaboxes', $accomodation_metaboxes);
845
846 }
847
848 $save_booking = $this->save_booking_request($form_data, $customer);
849 $send_email = $hotel_core->eshb_send_html_email($recipent_email, $subject, $message, $from_name, $customer_email);
850
851 if ( $send_email ) {
852
853 // Return success response
854 wp_send_json_success([
855 'message' => esc_html( $string_booking_success_msg ),
856 'admin_email' => esc_html( $admin_email ),
857 ]);
858
859 } else {
860 $error = array(
861 'code' => 'email_failed',
862 'message' => esc_html( $string_booking_failed_msg ),
863 );
864
865 wp_send_json_error([
866 'error' => $error,
867 ]);
868 }
869
870 }else{
871 $error = array(
872 'code' => 'accomodation_post_type_not_found',
873 'message' => esc_html( $string_booking_failed_msg ),
874 );
875
876 wp_send_json_error([
877 'error' => $error,
878 ]);
879 }
880 wp_die();
881 }
882
883 }
884
885 public function apply_custom_price_to_cart_item($cart) {
886
887 // Early return for admin requests (except AJAX)
888 if (is_admin() && !defined('DOING_AJAX')) {
889 return;
890 }
891
892 // Use the passed cart object if valid, otherwise fallback
893 if (!$cart || !is_object($cart) || !method_exists($cart, 'get_cart')) {
894 $cart = WC()->cart;
895 if (!$cart || !is_object($cart) || !method_exists($cart, 'get_cart')) {
896 return;
897 }
898 }
899
900 $redirect_added = false;
901
902 foreach ($cart->get_cart() as $cart_item_key => $cart_item) {
903 if (empty($cart_item['total_price']) || $cart_item['total_price'] <= 0) {
904 continue;
905 }
906 if (isset($cart_item['data']) && is_object($cart_item['data'])) {
907 $cart_item['data']->set_price($cart_item['total_price']);
908 }
909 }
910 }
911
912 private function add_currency_redirect() {
913 static $redirect_added = false;
914 if ($redirect_added) {
915 return;
916 }
917 add_action('wp_footer', function() {
918 echo '<script>if(typeof window.eshb_currency_redirect==="undefined"){window.eshb_currency_redirect=true;window.location.href="' . esc_url(get_permalink(get_the_ID())) . '";}</script>';
919 });
920 $redirect_added = true;
921 }
922
923 public function save_custom_meta_to_order($item, $cart_item_key, $values, $order) {
924
925 if (isset($values['accomodation_id'])) {
926 $item->add_meta_data('Accomodation ID', $values['accomodation_id'], true);
927 }
928
929 if (isset($values['total_price'])) {
930 $item->add_meta_data('Total Price', $values['total_price'], true);
931 }
932
933 if (isset($values['subtotal_price'])) {
934 $item->add_meta_data('Subtotal Price', $values['subtotal_price'], true);
935 }
936
937 if (isset($values['base_price'])) {
938 $item->add_meta_data('Base Price', $values['base_price'], true);
939 }
940
941 if (isset($values['extra_services_charge'])) {
942 $item->add_meta_data('Extra Services Charge', $values['extra_services_charge'], true);
943 }
944
945 if (isset($values['extra_bed_price'])) {
946 $item->add_meta_data('Extra Bed Price', $values['extra_bed_price'], true);
947 }
948
949 if(class_exists('ESHB_CURRENCY_WOO_FUNCTIONS')){
950 $currency_functions = new ESHB_CURRENCY_WOO_FUNCTIONS();
951
952 if (isset($values['total_price'])) {
953 $item->add_meta_data('Total Price', $currency_functions->apply_custom_currency_rate($values['total_price']), true);
954 }
955
956 if (isset($values['subtotal_price'])) {
957 $item->add_meta_data('Subtotal Price', $currency_functions->apply_custom_currency_rate($values['subtotal_price']), true);
958 }
959
960 if (isset($values['base_price'])) {
961 $item->add_meta_data('Base Price', $currency_functions->apply_custom_currency_rate($values['base_price']), true);
962 }
963
964 if (isset($values['extra_services_charge'])) {
965 $item->add_meta_data('Extra Services Charge', $currency_functions->apply_custom_currency_rate($values['extra_services_charge']), true);
966 }
967
968 if (isset($values['extra_bed_price'])) {
969 $item->add_meta_data('Extra Bed Price', $currency_functions->apply_custom_currency_rate($values['extra_bed_price']), true);
970 }
971 }
972
973 if (isset($values['room_quantity'])) {
974 $item->add_meta_data('Rooms', $values['room_quantity'], true);
975 }
976
977 if (isset($values['extra_bed_quantity'])) {
978 $item->add_meta_data('Extra Bed', $values['extra_bed_quantity'], true);
979 }
980
981 if (isset($values['adult_quantity'])) {
982 $item->add_meta_data('Adults', $values['adult_quantity'], true);
983 }
984
985 if (isset($values['children_quantity'])) {
986 $item->add_meta_data('Children', $values['children_quantity'], true);
987 }
988
989 if (isset($values['start_date'])) {
990 $item->add_meta_data('Start Date', $values['start_date'], true);
991 }
992
993 if (isset($values['end_date'])) {
994 $item->add_meta_data('End Date', $values['end_date'], true);
995 }
996
997 if (isset($values['dates'])) {
998 $item->add_meta_data('Date', $values['dates'], true);
999 }
1000
1001 if (isset($values['start_time'])) {
1002 $item->add_meta_data('Start Time', $values['start_time'], true);
1003 }
1004
1005 if (isset($values['end_time'])) {
1006 $item->add_meta_data('End Time', $values['end_time'], true);
1007 }
1008
1009 if (!empty($values['times'])) {
1010 $item->add_meta_data('Time', $values['times'], true);
1011 }
1012
1013 if (isset($values['details_html'])) {
1014 $item->add_meta_data('Details', $values['details_html'], true);
1015 }
1016
1017 if (isset($values['extra_services'])) {
1018 $item->add_meta_data('Extra Services IDs', $values['extra_services'], true);
1019 }
1020
1021 if (isset($values['extra_services_html'])) {
1022 $item->add_meta_data('Extra Services', $values['extra_services_html'], true);
1023 }
1024
1025 }
1026
1027 public function capture_payment_after_checkout($order_id){
1028 // capture payment
1029 ESHB_Helper::eshb_capture_payment($order_id);
1030 }
1031 public function create_woocommerce_booking_on_checkout($order_id){
1032
1033 if (!$order_id) {
1034 return;
1035 }
1036
1037 // Check if the booking post has already been created
1038 if (get_post_meta($order_id, '_booking_post_created', true)) {
1039 return; // Exit if the booking post has already been created
1040 }
1041
1042 // Get the order object
1043 $order = wc_get_order($order_id);
1044
1045 if (!$order) {
1046 return;
1047 }
1048
1049 $eshb_settings = get_option('eshb_settings', []);
1050
1051 // Get the order object
1052 $order = wc_get_order($order_id);
1053 $order_status = $order->get_status();
1054 $booking_status = $order_status;
1055
1056 $is_deposit_order = ( (float) get_post_meta( $order_id, 'initial_deposit', true ) > 0 )
1057 || ( 'deposit-payment' === $order->get_status() );
1058
1059 if ( $order && $order->is_paid() && $order->get_status() !== 'completed' && ! $is_deposit_order && isset($eshb_settings['booking-auto-approval']) && $eshb_settings['booking-auto-approval'] == true) {
1060 $order->update_status('completed', 'Payment successful, status updated via code.');
1061 $booking_status = 'completed';
1062 }
1063
1064 $first_name = $order->get_billing_first_name();
1065 $last_name = $order->get_billing_last_name();
1066 $customer_name = trim($first_name . ' ' . $last_name); // Concatenate first and last name
1067 $customer_email = $order->get_billing_email(); // Get customer email
1068 $customer_phone = $order->get_billing_phone(); // Get customer phone number
1069
1070
1071
1072 // Loop through order items
1073 foreach ($order->get_items() as $item_id => $item) {
1074
1075 // Retrieve meta data saved in the order item
1076 $accomodation_id = $item->get_meta('Accomodation ID');
1077 $accomodation_title = get_the_title( $accomodation_id );
1078 $room_quantity = $item->get_meta('Rooms');
1079 $extra_bed_quantity = $item->get_meta('Extra Bed');
1080 $adult_quantity = $item->get_meta('Adults');
1081 $children_quantity = $item->get_meta('Children');
1082 $start_date = $item->get_meta('Start Date');
1083 $end_date = $item->get_meta('End Date');
1084 $dates = $item->get_meta('Date');
1085 $times = $item->get_meta('Time');
1086 $start_time = $item->get_meta('Start Time');
1087 $end_time = $item->get_meta('End Time');
1088 $details_html = $item->get_meta('Details');
1089 $extra_services = $item->get_meta('Extra Services IDs');
1090 $extra_services_html = $item->get_meta('Extra Services');
1091 $base_price = $item->get_meta('Base Price');
1092 $extra_services_charge = $item->get_meta('Extra Service Price');
1093 $extra_bed_price = $item->get_meta('Extra Bed Price');
1094 $total_without_discount = $item->get_meta('Subtotal Price');
1095 $total_price = $item->get_meta('Total Price');
1096
1097 // Prepare the data array to store in post meta
1098 $cart_item_data = [
1099 'booking_status' => $booking_status,
1100 'order_id' => $order_id,
1101 'booking_accomodation_id' => $accomodation_id,
1102 'subtotal_price' => $order->get_subtotal(),
1103 'total_price' => $total_price,
1104 'total_paid' => 0,
1105 'base_price' => $base_price,
1106 'extra_service_price' => $extra_services_charge,
1107 'extra_bed_price' => $extra_bed_price,
1108 'booking_start_date' => $start_date,
1109 'booking_end_date' => $end_date,
1110 'booking_start_time' => !empty($start_time) ? $start_time : '10:00',
1111 'booking_end_time' => !empty($end_time) ? $end_time : '22:00',
1112 'dates' => $dates,
1113 'room_quantity' => $room_quantity,
1114 'extra_bed_quantity' => $extra_bed_quantity,
1115 'adult_quantity' => $adult_quantity,
1116 'children_quantity' => $children_quantity,
1117 'extra_services' => $extra_services,
1118 'start_time' => !empty($start_time) ? $start_time : '10:00',
1119 'end_time' => !empty($end_time) ? $end_time : '22:00',
1120 'times' => $times,
1121 'details_html' => $details_html,
1122 'extra_services_html' => $extra_services_html,
1123 ];
1124
1125 // filter for $cart_item_data;
1126 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Public hook name kept for backward compatibility with existing integrations.
1127 $cart_item_data = apply_filters( 'on_booking_cart_item_data', $cart_item_data );
1128
1129 // Insert the booking
1130 if($accomodation_id && !empty($accomodation_id)) {
1131 ESHB_Helper::eshb_insert_booking($order_id, $booking_status, $cart_item_data);
1132 }
1133
1134
1135 }
1136
1137 }
1138
1139 public function display_custom_meta_in_cart_and_order($item_data, $cart_item) {
1140
1141 if (!empty($cart_item['discount']) && is_cart()) {
1142 $discount = $cart_item['discount'];
1143 $currency_symbol = $cart_item['currency_symbol'];
1144
1145 $item_data[] = array(
1146 'key' => __('Save', 'easy-hotel'),
1147 'value' => wc_price($discount)
1148 );
1149 }
1150
1151 if (isset($cart_item['dates'])) {
1152
1153 $dates = $cart_item['dates'];
1154
1155 $item_data[] = array(
1156 'key' => __('Date', 'easy-hotel'),
1157 'value' => esc_html( $dates )
1158 );
1159 }
1160
1161 if (!empty($cart_item['times'])) {
1162
1163 $times = $cart_item['times'];
1164
1165 $item_data[] = array(
1166 'key' => __('Time', 'easy-hotel'),
1167 'value' => esc_html( $times )
1168 );
1169 }
1170
1171
1172 if (!empty($cart_item['night'])) {
1173
1174 $night = $cart_item['night'];
1175 $night_label = esc_html__('Night', 'easy-hotel');
1176 if ($night > 1) {
1177 $night_label = esc_html__('Nights', 'easy-hotel');
1178 }
1179
1180 $item_data[] = array(
1181 'key' => $night_label,
1182 'value' => esc_html( $night )
1183 );
1184 }
1185
1186
1187
1188 if (isset($cart_item['details_html'])) {
1189
1190 $details_html = $cart_item['details_html'];
1191
1192 $item_data[] = array(
1193 'key' => __('Details', 'easy-hotel'),
1194 'value' => esc_html( $details_html )
1195 );
1196 }
1197
1198 $extra_services_ids = isset($cart_item['extra_services']) ? $cart_item['extra_services'] : '';
1199
1200 if(!empty($extra_services_ids)){
1201 if (isset($cart_item['extra_services_html'])) {
1202
1203 $extra_services_html = $cart_item['extra_services_html'];
1204
1205 $item_data[] = array(
1206 'key' => __('Extra Services', 'easy-hotel'),
1207 'value' => esc_html( $extra_services_html )
1208 );
1209 }
1210 }
1211
1212 // make unique item data
1213 $item_data = array_unique($item_data, SORT_REGULAR);
1214
1215 return $item_data;
1216
1217 }
1218
1219 public function hide_meta_from_display($hidden_meta_keys) {
1220 // Add meta keys to hide
1221 $keys_to_hide = array(
1222 'Accomodation ID',
1223 'Subtotal Price',
1224 'Total Price',
1225 'Base Price',
1226 'Extra Services Charge',
1227 'Extra Bed Price',
1228 'Rooms',
1229 'Extra Bed',
1230 'Adults',
1231 'Children',
1232 'Start Date',
1233 'End Date',
1234 'Start Time',
1235 'End Time',
1236 'Save'
1237 );
1238
1239 return array_merge($hidden_meta_keys, $keys_to_hide);
1240 }
1241
1242 public function unset_specific_order_item_meta_data($formatted_meta, $item){
1243
1244 $keys_to_hide = array(
1245 'Accomodation ID',
1246 'Subtotal Price',
1247 'Total Price',
1248 'Base Price',
1249 'Extra Services Charge',
1250 'Extra Bed Price',
1251 'Rooms',
1252 'Extra Bed',
1253 'Adults',
1254 'Children',
1255 'Start Date',
1256 'End Date',
1257 'Start Time',
1258 'End Time',
1259 'Save'
1260 );
1261
1262 foreach($formatted_meta as $key => $meta){
1263 if(in_array($meta->key, $keys_to_hide)) {
1264 unset($formatted_meta[$key]);
1265 }
1266 }
1267 return $formatted_meta;
1268 }
1269
1270 public function calculate_booking_pricing($accomodation_id, $start_date, $end_date, $room_quantity, $extra_bed_quantity, $adult_quantity, $children_quantity, $selected_services, $currency_converter = false, $start_time = '', $end_time = '') {
1271 if (empty($accomodation_id)) return;
1272
1273 $accomodation_id = (int) $accomodation_id;
1274 $total_guest_quantity = (int) $adult_quantity + (int) $children_quantity;
1275 $start = new DateTime($start_date);
1276 $end = new DateTime($end_date);
1277 $days_count = max(1, $start->diff($end)->days);
1278 $hours_count = ESHB_Helper::eshb_calculate_time_diff($start_date, $end_date, $start_time, $end_time);
1279 $today = new DateTime('now', new DateTimeZone('GMT'));
1280 $today_date = $today->format('Y-m-d');
1281 $tomorrow = $today->modify('+1 day')->format('Y-m-d');
1282 $hotel_core = new ESHB_Core();
1283 $metaboxes = get_post_meta($accomodation_id, 'eshb_accomodation_metaboxes', true);
1284 $allowed_check_in_day = apply_filters( 'eshb_booking_allowed_check_in_day', 'all', $accomodation_id, $metaboxes );
1285 $single_day_price = 0;
1286
1287 // Handle week-based or same-day bookings
1288 if (!empty($allowed_check_in_day) && $allowed_check_in_day !== 'all') {
1289 $days_count = 1;
1290 }
1291
1292 $pricing_periodicity = false;
1293
1294 if ($start_date === $end_date) {
1295 $days_count = 1;
1296 $pricing_periodicity = apply_filters( 'eshb_pricing_periodicity', false, $accomodation_id, $metaboxes );
1297 $single_day_price = apply_filters( 'eshb_single_day_price', 0, $accomodation_id);
1298 }
1299
1300
1301 // Base price
1302 $base_price = 0;
1303 $base_price = $hotel_core->get_eshb_day_wise_price($start_date, $end_date, $accomodation_id, true, $days_count, $adult_quantity, $children_quantity);
1304 $base_price = !empty($single_day_price) ? $single_day_price : $base_price;
1305 $has_session_price = $hotel_core->has_upcoming_or_current_session_price($accomodation_id, $start_date, $end_date, $days_count, $adult_quantity, $children_quantity);
1306
1307 if($has_session_price){
1308 $session_price = $hotel_core->get_eshb_price_by_session($accomodation_id, $start_date, $end_date, $days_count, $adult_quantity, $children_quantity);
1309 }
1310
1311 $base_price = !empty($session_price) && $session_price > 0 ? $session_price : $base_price;
1312 $regular_base_price = $hotel_core->get_eshb_day_wise_price($start_date, $end_date, $accomodation_id, true, $days_count, $adult_quantity, $children_quantity);
1313
1314 // Pricing logic
1315 $bed_price = (int) ($metaboxes['extra_bed_price'] ?? 0);
1316 $pricing_type = $metaboxes['pricing_type'] ?? 'room_wise';
1317
1318
1319 $pricing_base = $pricing_type === 'room_wise'
1320 ? $base_price * $room_quantity
1321 : $base_price * $total_guest_quantity;
1322
1323
1324 $regular_pricing_base = $pricing_type === 'room_wise'
1325 ? $regular_base_price * $room_quantity
1326 : $regular_base_price * $total_guest_quantity;
1327
1328
1329 $extra_bed_price = $extra_bed_quantity > 0
1330 ? $extra_bed_quantity * $bed_price * $days_count
1331 : 0;
1332
1333
1334
1335 $subtotal_price = $pricing_base + $extra_bed_price;
1336 $regular_subtotal_price = $regular_pricing_base + $extra_bed_price;
1337
1338 if($pricing_periodicity && !empty($hours_count)){
1339 $subtotal_price = $pricing_periodicity == 'per_hour' ? $subtotal_price * (int) $hours_count : $subtotal_price;
1340 $regular_subtotal_price = $pricing_periodicity == 'per_hour' ? $regular_subtotal_price * (int) $hours_count : $regular_subtotal_price;
1341 }
1342
1343
1344 // Extra services
1345 $extra_services_charge = 0;
1346
1347 if (!empty($selected_services) && is_array($selected_services)) {
1348 $extra_services_charge = array_reduce($selected_services, function ($carry, $service) use ($days_count, $room_quantity, $total_guest_quantity) {
1349 $service_id = !empty($service['id']) ? $service['id'] : 0;
1350 $service_quantity = !empty($service['quantity']) ? $service['quantity'] : 0;
1351
1352 if(empty($service_id) || empty($service_quantity)) {
1353 return $carry;
1354 }
1355
1356 $meta = get_post_meta($service_id, 'eshb_service_metaboxes', true);
1357 $price = $meta['service_price'] ?? 0;
1358 $periodicity = $meta['service_periodicity'] ?? 'once';
1359 $charge_type = $meta['service_charge_type'] ?? 'room';
1360
1361 // Never price more than the service's configured Max Quantity,
1362 // whatever the request asked for.
1363 $service_quantity = ESHB_Helper::eshb_clamp_service_quantity( $service_id, $service_quantity, $meta );
1364
1365 //$single_price = $price * $service_quantity;
1366 $single_price = $price ? $price : 0;
1367
1368 if ($periodicity == 'per_day') {
1369 $single_price *= $days_count;
1370 }
1371
1372 if($charge_type == 'room'){
1373 $single_price *= $room_quantity;
1374 }else{
1375 $single_price *= $service_quantity;
1376 }
1377
1378 return $carry + floatval($single_price);
1379 }, 0);
1380 }
1381
1382
1383 $eshb_settings = get_option('eshb_settings');
1384 $booking_type = isset($eshb_settings['booking-type']) ? $eshb_settings['booking-type'] : '';
1385
1386 $regular_subtotal_price = $regular_subtotal_price + $extra_services_charge;
1387 $regular_total_price = $regular_subtotal_price;
1388
1389 $subtotal_price = $subtotal_price + $extra_services_charge;
1390 $total_price = $subtotal_price;
1391
1392 $discount = $total_price < $regular_total_price ? abs( (float) $regular_total_price - (float) $total_price) : 0;
1393
1394 $currency_symbol = $hotel_core->get_eshb_currency_symbol();
1395 $currency_symbol = html_entity_decode($currency_symbol);
1396 $currency_position = $hotel_core->get_eshb_currency_position();
1397
1398
1399 $extra_bed_price = apply_filters( 'eshb_apply_currency_converter_on_pricing_calculation', $extra_bed_price, $currency_converter);
1400 $extra_services_charge = apply_filters( 'eshb_apply_currency_converter_on_pricing_calculation', $extra_services_charge, $currency_converter);
1401 $base_price = apply_filters( 'eshb_apply_currency_converter_on_pricing_calculation', $base_price, $currency_converter);
1402 $regular_base_price = apply_filters( 'eshb_apply_currency_converter_on_pricing_calculation', $regular_base_price, $currency_converter);
1403 $subtotal_price = apply_filters( 'eshb_apply_currency_converter_on_pricing_calculation', $subtotal_price, $currency_converter);
1404 $regular_subtotal_price = apply_filters( 'eshb_apply_currency_converter_on_pricing_calculation', $regular_subtotal_price, $currency_converter);
1405 $total_price = apply_filters( 'eshb_apply_currency_converter_on_pricing_calculation', $total_price, $currency_converter);
1406 $regular_total_price = apply_filters( 'eshb_apply_currency_converter_on_pricing_calculation', $regular_total_price, $currency_converter);
1407 $single_day_price = apply_filters( 'eshb_apply_currency_converter_on_pricing_calculation', $single_day_price, $currency_converter);
1408 $discount = apply_filters( 'eshb_apply_currency_converter_on_pricing_calculation', $discount, $currency_converter);
1409
1410
1411 $single_day_price_html = $hotel_core->eshb_price( $single_day_price );
1412 $regular_base_price_html = $hotel_core->eshb_price( $regular_base_price );
1413 $base_price_html = $hotel_core->eshb_price( $base_price );
1414 $extra_bed_price_html = $hotel_core->eshb_price( $extra_bed_price );
1415 $extra_services_charge_html = $hotel_core->eshb_price( $extra_services_charge );
1416 $subtotal_price_html = $hotel_core->eshb_price( $subtotal_price );
1417 $regular_subtotal_price_html = $hotel_core->eshb_price( $regular_subtotal_price );
1418 $total_price_html = $hotel_core->eshb_price( $total_price );
1419 $regular_total_price_html = $hotel_core->eshb_price( $regular_total_price );
1420 $discount_html = $hotel_core->eshb_price( $discount );
1421
1422 $is_wc_price = ($booking_type === 'woocommerce' && class_exists('WooCommerce')) ? true : false;
1423
1424 return [
1425 'startDate' => $start_date,
1426 'endDate' => $end_date,
1427 'today' => $today_date,
1428 'tomorrow' => $tomorrow,
1429 'daysCount' => $days_count,
1430 'singleDayPrice' => $single_day_price,
1431 'extraServicesPrice' => $extra_services_charge,
1432 'extraBedPrice' => $extra_bed_price,
1433 'basePrice' => $base_price,
1434 'regularBasePrice' => $regular_base_price,
1435 'subtotalPrice' => $subtotal_price,
1436 'regularSubtotalPrice'=> $regular_subtotal_price,
1437 'totalPrice' => $total_price,
1438 'regularTotalPrice' => $regular_total_price,
1439 'discount' => $discount,
1440 'currencySymbol' => $currency_symbol,
1441 'currencyPosition' => $currency_position,
1442
1443 'singleDayPriceHtml' => $single_day_price_html,
1444 'extraServicesPriceHtml' => $extra_services_charge_html,
1445 'extraBedPriceHtml' => $extra_bed_price_html,
1446 'basePriceHtml' => $base_price_html,
1447 'regularBasePriceHtml' => $regular_base_price_html,
1448 'subtotalPriceHtml' => $subtotal_price_html,
1449 'regularSubtotalPriceHtml'=> $regular_subtotal_price_html,
1450 'totalPriceHtml' => $total_price_html,
1451 'regularTotalPriceHtml' => $regular_total_price_html,
1452 'discountHtml' => $discount_html,
1453
1454 'isWcPrice' => $is_wc_price
1455 ];
1456 }
1457
1458 public function eshb_add_to_cart_reservation(){
1459
1460 // Verify nonce for security
1461 if (!isset($_POST['nonce']) || !wp_verify_nonce( sanitize_text_field(wp_unslash($_POST['nonce'])), ESHB_Helper::generate_secure_nonce_action('eshb_global_nonce_action'))) {
1462
1463 $error = array(
1464 'code' => 'invalid_nonce',
1465 'message' => __('invalid_nonce', 'easy-hotel')
1466 );
1467
1468 wp_send_json_error([
1469 'error' => $error,
1470 ]);
1471
1472 die();
1473 }
1474
1475
1476 if(isset($_POST['accomodationId']) && !empty($_POST['accomodationId'])){
1477
1478 $hotel_core = new ESHB_Core();
1479 $accomodation_id = isset( $_POST['accomodationId'] ) ? (int) sanitize_text_field( wp_unslash($_POST['accomodationId']) ) : '';
1480 $today_date = esc_html(gmdate('Y-m-d')); // Get today's date
1481 $date = new DateTime($today_date); // Create a DateTime object from today's date
1482 $date->modify('+1 day'); // Add one day
1483 $tomorrow = $date->format('Y-m-d'); // Get the new date in 'Y-m-d' format
1484 $start_date = isset( $_POST['startDate'] ) ? sanitize_text_field( wp_unslash($_POST['startDate']) ) : $today_date;
1485 $end_date = isset( $_POST['endDate'] ) ? sanitize_text_field( wp_unslash($_POST['endDate']) ) : $tomorrow;
1486 $start_day_name = $start_date ? strtolower(gmdate('l', strtotime($start_date))) : '';
1487 $start_time = isset( $_POST['startTime'] ) ? sanitize_text_field( wp_unslash($_POST['startTime']) ) : '';
1488 $end_time = isset( $_POST['endTime'] ) ? sanitize_text_field( wp_unslash($_POST['endTime']) ) : '';
1489 $accomodation_metaboxes = get_post_meta($accomodation_id, 'eshb_accomodation_metaboxes', true);
1490
1491 $start = new DateTime($start_date);
1492 $end = new DateTime($end_date);
1493 $interval = $start->diff($end);
1494 $days_count = $interval->days;
1495
1496 $room_quantity = isset( $_POST['roomQuantity'] ) ? (int) sanitize_text_field( wp_unslash($_POST['roomQuantity'] ) ) : 1;
1497 $extra_bed_quantity = isset( $_POST['extraBedQuantity'] ) ? (int) sanitize_text_field( wp_unslash($_POST['extraBedQuantity'] ) ) : 0;
1498 $adult_quantity = isset( $_POST['adultQuantity'] ) ? (int) sanitize_text_field( wp_unslash($_POST['adultQuantity'] ) ) : 1;
1499 $children_quantity = isset( $_POST['childrenQuantity'] ) ? (int) sanitize_text_field( wp_unslash($_POST['childrenQuantity'] ) ) : 0;
1500 $total_guest_quantity = $adult_quantity + $children_quantity;
1501
1502 $selected_services = [];
1503 if(isset($_POST['selectedServices']) && !empty($_POST['selectedServices'])){
1504
1505 $selected_services = sanitize_text_field(wp_unslash($_POST['selectedServices']));
1506 $selected_services = json_decode($selected_services, true);
1507
1508 // Cap each line at the service's Max Quantity before it is
1509 // priced, described or stored on the booking.
1510 $selected_services = ESHB_Helper::eshb_clamp_selected_services($selected_services);
1511
1512 }
1513
1514 $eshb_settings = get_option( 'eshb_settings', [] );
1515 $booking_type = isset($eshb_settings['booking-type']) && !empty($eshb_settings['booking-type']) ? $eshb_settings['booking-type'] : 'woocommerce';
1516 $string_booking_success_msg = isset($eshb_settings['string_book_success_msg']) && !empty($eshb_settings['string_booking_success_msg']) ? $eshb_settings['string_booking_success_msg'] : 'Reservation Successfully added to your cart.';
1517 $string_booking_failed_msg = isset($eshb_settings['string_booking_failed_msg']) && !empty($eshb_settings['string_booking_failed_msg']) ? $eshb_settings['string_booking_failed_msg'] : 'Ops! This Reservation has been failed.';
1518 $string_already_booked_msg = isset($eshb_settings['string_already_booked_msg']) && !empty($eshb_settings['string_already_booked_msg']) ? $eshb_settings['string_already_booked_msg'] : 'Ops! The date range is already booked:';
1519 $string_minimum_week_nights_msg = isset($eshb_settings['string_minimum_week_nights_msg']) && !empty($eshb_settings['string_minimum_week_nights_msg']) ? $eshb_settings['string_minimum_week_nights_msg'] : 'Please select more than 1 night!';
1520 $string_time_error_msg = isset($eshb_settings['string_time_error_msg']) && !empty($eshb_settings['string_time_error_msg']) ? $eshb_settings['string_time_error_msg'] : 'Selected time slot is not available!';
1521 $room_visibility = in_array('rooms', $eshb_settings['booking-form-fields']) ? true : false;
1522 $adult_visibility = in_array('adults', $eshb_settings['booking-form-fields']) ? true : false;
1523 $childrens_visibility = in_array('childrens', $eshb_settings['booking-form-fields']) ? true : false;
1524 $extra_beds_visibility = in_array('extra_beds', $eshb_settings['booking-form-fields']) ? true : false;
1525
1526 $eshb_week_settings = [
1527 'string_check_in_day_error_msg' => 'Only allowed check in day is',
1528 'eshb_booking_allowed_check_in_day' => 'all',
1529 ];
1530 $eshb_week_settings = apply_filters( 'eshb_week_settings', $eshb_week_settings );
1531 $allowed_check_in_day = apply_filters( 'eshb_booking_allowed_check_in_day', 'all', $accomodation_id, $accomodation_metaboxes );
1532 $string_check_in_day_error_msg = !empty($eshb_week_settings['string_check_in_day_error_msg']) ? $eshb_week_settings['string_check_in_day_error_msg'] : 'Only allowed check in day is : ';
1533
1534 // Min Max Booking Configurations
1535 $min_max_settings = [
1536 'required_min_nights' => '',
1537 'required_max_nights' => '',
1538 'is_global_source_for_min_max' => true,
1539 ];
1540 $min_max_settings = apply_filters( 'eshb_min_max_settings', $min_max_settings, $accomodation_id, $accomodation_metaboxes );
1541 $required_min_nights = $start_date !== $end_date && isset($min_max_settings['required_min_nights']) ? $min_max_settings['required_min_nights'] : '';
1542 $required_max_nights = $start_date !== $end_date && isset($min_max_settings['required_max_nights']) ? $min_max_settings['required_max_nights'] : '';
1543 $string_required_minimum_nights_msg = esc_html__( 'Oops! Your reservation couldn\'t be completed. A minimum stay of', 'easy-hotel' );
1544 $string_required_maximum_nights_msg = esc_html__( 'Oops! Your reservation couldn\'t be completed. A maximum stay of', 'easy-hotel' );
1545 $pricing_periodicity = apply_filters( 'eshb_pricing_periodicity', false, $accomodation_id, $accomodation_metaboxes );
1546
1547 $min_stay_night_by_session = ESHB_Helper::get_eshb_min_stay_night_by_session($accomodation_id, $start_date, $end_date);
1548
1549 // Validate booking type
1550 if ( $booking_type == 'woocommerce' && ! class_exists( 'WooCommerce' ) ) {
1551 $error = array(
1552 'code' => 'woocommerce_not_activated',
1553 'message' => esc_html__( 'Ops! Woocommerce not activated. Activate Woocommerce.', 'easy-hotel'),
1554 );
1555 wp_send_json_error( [ 'error' => $error ] );
1556
1557 } elseif ( $booking_type == 'surecart' && ! class_exists( 'SureCart' ) ) {
1558 $error = array(
1559 'code' => 'surecart_not_activated',
1560 'message' => esc_html__( 'Ops! Surecart not activated. Activate Surecart.', 'easy-hotel' ),
1561 );
1562 wp_send_json_error( [ 'error' => $error ] );
1563
1564 } elseif ( $booking_type == 'surecart' && ! class_exists( 'ESHB_SURECART' ) ) {
1565 $error = array(
1566 'code' => 'surecart_addon_not_activated',
1567 'message' => esc_html__( 'Ops! EHB Surecart addons not activated. Activate EHB Surecart.', 'easy-hotel' ),
1568 );
1569 wp_send_json_error( [ 'error' => $error ] );
1570 }
1571
1572
1573 $available_times = ESHB_Helper::get_available_times_by_date($accomodation_id, $start_date);
1574 $is_valid_time = false;
1575
1576 if($pricing_periodicity == 'per_hour' && (!empty($accomodation_metaboxes['single_day_price']) || !empty($accomodation_metaboxes['single_day_sale_price'])) && !empty($available_times)){
1577 $available_slots = isset($available_times['available_slots']) ? $available_times['available_slots'] : [];
1578
1579 if(count($available_slots) > 0) {
1580 foreach ($available_slots as $slot) {
1581 if ($start_time >= $slot[0] && $end_time <= $slot[1]) {
1582 $is_valid_time = true;
1583 break;
1584 }
1585 }
1586 }else{
1587 $is_valid_time = false;
1588 }
1589
1590 if(!$is_valid_time){
1591 $error = array('code' => 'invalid_time', 'message' => esc_html($string_time_error_msg));
1592 wp_send_json_error([
1593 'error' => $error,
1594 ]);
1595 }
1596 }
1597
1598 $actual_booked_dates = $this->get_actual_booked_dates_in_date_ranges($accomodation_id, $start_date, $end_date);
1599 $booked_dates_str = '';
1600 if(!empty($actual_booked_dates)){
1601
1602 $error_message = sprintf(
1603 /* translators: 1: already booked message text, 2: list of booked dates */
1604 esc_html__( '%1$s %2$s', 'easy-hotel' ),
1605 esc_html( $string_already_booked_msg ),
1606 $booked_dates_str
1607 );
1608
1609 $error = array(
1610 'code' => 'already_booked',
1611 'message' => $error_message,
1612 );
1613 wp_send_json_error([
1614 'error' => $error,
1615 'booked_dates' => implode(', ', $actual_booked_dates),
1616 ]);
1617
1618 }
1619
1620 $holiday_dates = $this->get_holiday_dates_in_date_ranges($accomodation_id, $start_date, $end_date);
1621
1622 if(!empty($holiday_dates)){
1623 $error = array('code' => 'holiday_dates', 'message' => esc_html__('Selected date range is not available. These dates are in holidays.', 'easy-hotel'));
1624 wp_send_json_error([
1625 'error' => $error,
1626 'holiday_dates' => implode(', ', $holiday_dates),
1627 ]);
1628 }
1629
1630 // Check cart blocking (temporary holds by other users)
1631 $cart_block_msg = $this->eshb_check_cart_block($accomodation_id, $start_date, $end_date);
1632 if (!empty($cart_block_msg)) {
1633 $error = array('code' => 'cart_blocked', 'message' => esc_html($cart_block_msg));
1634 wp_send_json_error(['error' => $error]);
1635 }
1636
1637 // check rooms capacity
1638 if(!$is_valid_time && isset($_POST['roomQuantity']) && !empty($_POST['roomQuantity'])){
1639 $roomQuantity = isset( $_POST['roomQuantity'] ) ? (int) sanitize_text_field( wp_unslash($_POST['roomQuantity'] ) ) : 1;
1640
1641 $availableRoom = $this->get_available_room_count_by_date_range($accomodation_id, $start_date, $end_date);
1642
1643 if($roomQuantity > $availableRoom){
1644 $error = array(
1645 'code' => 'room_capacity_not_enough',
1646 'message' => sprintf(
1647 /* translators: %s: number of available rooms */
1648 esc_html__( 'Selected room is not available. Available room: %s', 'easy-hotel' ),
1649 esc_html( $availableRoom )
1650 ),
1651 );
1652 wp_send_json_error([
1653 'error' => $error,
1654 ]);
1655 }
1656 }
1657
1658
1659 // Validate check-in day
1660 // Same check as the other booking path above: a rule can now allow several
1661 // days, so the comparison is a lookup in the comma separated list rather than
1662 // an equality test — which was also the wrong way round here, rejecting the
1663 // allowed day and letting every other one through.
1664 if($allowed_check_in_day != 'all' && $start_day_name != '' && !empty($allowed_check_in_day) && strpos($allowed_check_in_day, $start_day_name) === false) {
1665 $error = array(
1666 'code' => 'check_in_day_error',
1667 'message' => sprintf(
1668 /* translators: %s: allowed check-in day */
1669 esc_html__( '%1$s %1$s', 'easy-hotel' ),
1670 esc_html( $string_check_in_day_error_msg ),
1671 esc_html( $allowed_check_in_day )
1672 ),
1673 );
1674 wp_send_json_error([
1675 'error' => $error,
1676 ]);
1677 }
1678
1679
1680
1681 if(!empty($allowed_check_in_day) && $allowed_check_in_day != 'all'){
1682 $days_count = 1;
1683 }
1684
1685 // validate min stay night by session
1686 if(class_exists('ESHB_ADVANCED_PRICING') && !empty($min_stay_night_by_session) && $days_count < $min_stay_night_by_session){
1687 $error = array(
1688 'code' => 'min_stay_night_by_session',
1689 'message' => sprintf(
1690 /* translators: 1: minimum nights message text, 2: required minimum nights */
1691 esc_html__( '%1$s %2$s nights!', 'easy-hotel' ),
1692 esc_html( $string_required_minimum_nights_msg ),
1693 esc_html( $min_stay_night_by_session )
1694 ),
1695 );
1696 wp_send_json_error([
1697 'error' => $error,
1698 ]);
1699 }
1700
1701 if(!empty($required_min_nights) && $days_count < $required_min_nights){
1702 $error = array(
1703 'code' => 'required_min_nights',
1704 'message' => sprintf(
1705 /* translators: 1: minimum nights message text, 2: required minimum nights */
1706 esc_html__( '%1$s %2$s nights!', 'easy-hotel' ),
1707 esc_html( $string_required_minimum_nights_msg ),
1708 esc_html( $required_min_nights )
1709 ),
1710 );
1711 wp_send_json_error([
1712 'error' => $error,
1713 ]);
1714 }
1715 if(!empty($required_max_nights) && $days_count > $required_max_nights){
1716 $error = array(
1717 'code' => 'required_max_nights',
1718 'message' => sprintf(
1719 /* translators: 1: maximum nights message text, 2: required maximum nights */
1720 esc_html__( '%1$s %2$s nights allowed!', 'easy-hotel' ),
1721 esc_html( $string_required_maximum_nights_msg ),
1722 esc_html( $required_max_nights )
1723 ),
1724 );
1725 wp_send_json_error([
1726 'error' => $error,
1727 ]);
1728 }
1729
1730
1731
1732 // validate capacities
1733 $adult_capacity = !empty($accomodation_metaboxes['adult_capacity']) ? $accomodation_metaboxes['adult_capacity'] : 0;
1734 $children_capacity = !empty($accomodation_metaboxes['children_capacity']) ? $accomodation_metaboxes['children_capacity'] : 0;
1735 $total_capacity = !empty($accomodation_metaboxes['total_capacity']) ? $accomodation_metaboxes['total_capacity'] : 0;
1736 $extra_bed_capacity = !empty($accomodation_metaboxes['total_extra_beds']) ? $accomodation_metaboxes['total_extra_beds'] : 0;
1737
1738 // multiply total capacity by per rooms
1739 $total_capacity = $total_capacity * $room_quantity;
1740
1741 if(!empty($total_capacity)){
1742 if($adult_capacity < 1 && $children_capacity > 0){
1743 $adult_capacity = $total_capacity - $children_quantity;
1744 }elseif($adult_capacity < 1 && $children_capacity < 1){
1745 $adult_capacity = $total_capacity;
1746 }
1747
1748 if($children_capacity < 1 && $adult_capacity > 0){
1749 $children_capacity = $total_capacity - $adult_quantity;
1750 }elseif($children_capacity < 1 && $adult_capacity < 1){
1751 $children_capacity = $total_capacity;
1752 }
1753 }
1754
1755 // multiply capacity by per rooms
1756 $adult_capacity = $adult_capacity * $room_quantity;
1757 $children_capacity = $children_capacity * $room_quantity;
1758 $extra_bed_capacity = $extra_bed_capacity * $room_quantity;
1759
1760 if(!empty($adult_capacity) && $adult_quantity > 0 && $adult_capacity < $adult_quantity){
1761 $error = array('code' => 'adult_capacity_not_enough', 'message' => esc_html__('Maximum Adult Capacity', 'easy-hotel') . ' ' . $adult_capacity);
1762 wp_send_json_error([
1763 'error' => $error,
1764 ]);
1765 }
1766
1767 if(!empty($children_capacity) && $children_quantity > 0 && $children_capacity < $children_quantity){
1768 $error = array('code' => 'children_capacity_not_enough', 'message' => esc_html__('Maximum Children Capacity', 'easy-hotel') . ' ' . $children_capacity);
1769 wp_send_json_error([
1770 'error' => $error,
1771 ]);
1772 }
1773
1774 if($extra_bed_quantity > 0 && $extra_bed_capacity < $extra_bed_quantity){
1775 $error = array('code' => 'extra_bed_capacity_not_enough', 'message' => esc_html__('Maximum Extra Bed Capacity', 'easy-hotel') . ' ' . $extra_bed_capacity );
1776 wp_send_json_error([
1777 'error' => $error,
1778 ]);
1779 }
1780
1781
1782
1783 $pricing = $this->calculate_booking_pricing($accomodation_id, $start_date, $end_date, $room_quantity, $extra_bed_quantity, $adult_quantity, $children_quantity, $selected_services, false, $start_time, $end_time );
1784
1785 $base_price = $pricing['basePrice'];
1786 $extra_services_charge = $pricing['extraServicesPrice'];
1787 $extra_bed_price= $pricing['extraBedPrice'];
1788 $subtotal_price = $pricing['subtotalPrice'];
1789 $total_price = $pricing['totalPrice'];
1790 $discount = $pricing['discount'];
1791 $currency_symbol = $pricing['currencySymbol'];
1792 $quantity = 1;
1793
1794 $details = [];
1795
1796 if ($room_visibility && $room_quantity > 0) {
1797 $details[] = 'Room: ' . $room_quantity;
1798 }
1799 if ($extra_beds_visibility && $extra_bed_quantity > 0) {
1800 $details[] = 'Extra Bed: ' . $extra_bed_quantity;
1801 }
1802 if ($adult_visibility && $adult_quantity > 0) {
1803 $details[] = 'Adult: ' . $adult_quantity;
1804 }
1805 if ($childrens_visibility && $children_quantity > 0) {
1806 $details[] = 'Children: ' . $children_quantity;
1807 }
1808
1809 $details_html = implode(', ', $details);
1810
1811
1812 $extra_service_titles = [];
1813 $extra_services_html = '';
1814
1815 if(!empty($selected_services) && is_array($selected_services) && count($selected_services) > 0 ){
1816
1817 foreach ($selected_services as $service) {
1818
1819 $service_id = $service['id'];
1820
1821 $service_quantity = $service['quantity'];
1822
1823 if(!empty($service_quantity)){
1824 $eshb_service_metaboxes = get_post_meta($service_id, 'eshb_service_metaboxes', true);
1825
1826 $charge_type = $eshb_service_metaboxes['service_charge_type'];
1827
1828 $service_title = get_the_title($service_id) . ' For ' . $service_quantity . ' ' . ucfirst(strtolower($charge_type));
1829
1830 if(!$room_visibility && $charge_type == 'room'){
1831 $service_title = get_the_title($service_id);
1832 }
1833 }else{
1834 $service_title = get_the_title($service_id);
1835 }
1836
1837
1838
1839 array_push($extra_service_titles, $service_title);
1840
1841 }
1842 $extra_services_html = implode(", ",$extra_service_titles);
1843 }
1844
1845 $dates = ESHB_Helper::eshb_format_date( $start_date ) .' - '. ESHB_Helper::eshb_format_date( $end_date );
1846 if($start_date == $end_date){
1847 $dates = ESHB_Helper::eshb_format_date( $start_date );
1848 }
1849
1850 $formated_start_timestamp = strtotime( $start_time );
1851 $formated_end_timestamp = strtotime( $end_time );
1852
1853 $cart_item_data = [
1854 'accomodation_id' => $accomodation_id,
1855 'subtotal_price' => $subtotal_price,
1856 'total_price' => $total_price,
1857 'base_price' => $base_price,
1858 'discount' => $discount,
1859 'extra_services_charge' => $extra_services_charge,
1860 'extra_bed_price' => $extra_bed_price,
1861 'currency_symbol' => $currency_symbol,
1862 'start_date' => $start_date,
1863 'end_date' => $end_date,
1864 'night' => $days_count,
1865 'dates' => $dates,
1866 'room_quantity' => $room_quantity,
1867 'adult_quantity' => $adult_quantity,
1868 'details_html' => $details_html,
1869 'extra_services_html' => $extra_services_html
1870 ];
1871
1872 if(!empty($extra_bed_quantity)) {
1873 $cart_item_data['extra_bed_quantity'] = $extra_bed_quantity;
1874 }
1875 if(!empty($start_time)) {
1876 $cart_item_data['start_time'] = $start_time;
1877 }
1878 if(!empty($end_time)) {
1879 $cart_item_data['end_time'] = $end_time;
1880 }
1881
1882 if(!empty($start_time) && !empty($end_time)) {
1883 // Get WP formatted time (according to Settings > General > Time Format)
1884 $wp_time_format = get_option( 'time_format' ); // WP settings
1885 $formatted_start_time = wp_date( $wp_time_format, $formated_start_timestamp );
1886 $formatted_end_time = wp_date( $wp_time_format, $formated_end_timestamp );
1887 $times = $formatted_start_time . ' - ' . $formatted_end_time;
1888 $cart_item_data['times'] = $times;
1889 }
1890
1891 if(!empty($children_quantity)) {
1892 $cart_item_data['children_quantity'] = $children_quantity;
1893 }
1894
1895 if(!empty($selected_services)) {
1896 $cart_item_data['extra_services'] = $selected_services;
1897 }
1898
1899 if(!empty($extra_services_charge)) {
1900 $cart_item_data['extra_services_html'] = $extra_services_html;
1901 }
1902
1903 if (get_post_type($accomodation_id) === 'eshb_accomodation') {
1904
1905 $thumbnail_id = get_post_thumbnail_id($accomodation_id);
1906 $price_id = 0;
1907 $product_id = ESHB_Helper::get_product_id_for_cart($accomodation_id, $booking_type);
1908 $product_id = apply_filters( 'eshb_product_id_for_cart', $product_id, $accomodation_id, $thumbnail_id );
1909
1910 $cart_item_data['product_id'] = $product_id;
1911 $cart_item_data['price_id'] = $price_id;
1912
1913 do_action( 'eshb_before_add_to_cart', $booking_type, $cart_item_data, $string_booking_success_msg );
1914
1915 if($booking_type == 'woocommerce'){
1916
1917 update_post_meta($product_id, '_accomodation_id', $accomodation_id);
1918 $this->update_product_price_by_id($product_id, $subtotal_price);
1919
1920 $cart_item_data['unique_key'] = uniqid( '', true );
1921 $cart_item_key = WC()->cart->add_to_cart($product_id, $quantity, 0, [], $cart_item_data);
1922
1923 if ( $cart_item_key ) {
1924
1925 $blocked_until = $this->eshb_set_cart_block($accomodation_id, $start_date, $end_date, $room_quantity);
1926
1927 wp_send_json_success([
1928 'booking-type' => $booking_type,
1929 'message' => esc_html( $string_booking_success_msg ),
1930 'cart_item_key' => $cart_item_key,
1931 'cart_count' => WC()->cart->get_cart_contents_count(),
1932 'blocked_until' => $blocked_until,
1933 'accomodation_id' => $accomodation_id,
1934 'start_date' => $start_date,
1935 'end_date' => $end_date,
1936 ]);
1937
1938 } else {
1939
1940 $error = array(
1941 'code' => 'cart_item_key_not_found',
1942 'message' => esc_html( $string_booking_failed_msg ),
1943 );
1944
1945 wp_send_json_error([
1946 'error' => $error,
1947 'cart_item_data' => $cart_item_data,
1948 'product_id' => $product_id,
1949 ]);
1950 }
1951
1952 }
1953
1954 do_action( 'eshb_after_add_to_cart', $booking_type, $cart_item_data, $string_booking_success_msg );
1955
1956 }else{
1957 $error = array('code' => 'accomodation_post_type_not_found', 'message' => esc_html($string_booking_failed_msg));
1958 wp_send_json_error([
1959 'error' => $error,
1960 ]);
1961 }
1962 die();
1963 }
1964
1965 }
1966
1967 public function eshb_get_booking_prices() {
1968 // Ensure external filters are loaded during AJAX requests
1969 do_action('eshb_before_calculate_pricing');
1970
1971 if (isset($_POST['accomodationId']) && !empty($_POST['accomodationId'])) {
1972
1973 // Verify nonce for security
1974 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), ESHB_Helper::generate_secure_nonce_action('eshb_global_nonce_action'))) {
1975 wp_send_json_error(['message' => 'Invalid nonce']);
1976 die();
1977 }
1978
1979 $accomodation_id = (int) sanitize_text_field(wp_unslash($_POST['accomodationId']));
1980
1981 $start_date = sanitize_text_field(wp_unslash($_POST['startDate'] ?? $today_date));
1982 $end_date = sanitize_text_field(wp_unslash($_POST['endDate'] ?? $tomorrow));
1983
1984 $start_time = sanitize_text_field(wp_unslash($_POST['startTime'] ?? ''));
1985 $end_time = sanitize_text_field(wp_unslash($_POST['endTime'] ?? ''));
1986
1987 $room_quantity = (int) sanitize_text_field(wp_unslash($_POST['roomQuantity'] ?? 1));
1988 $extra_bed_quantity = (int) sanitize_text_field(wp_unslash($_POST['extraBedQuantity'] ?? 0));
1989 $adult_quantity = (int) sanitize_text_field(wp_unslash($_POST['adultQuantity'] ?? 1));
1990 $children_quantity = (int) sanitize_text_field(wp_unslash($_POST['childrenQuantity'] ?? 0));
1991 $selected_services = [];
1992 if(isset($_POST['selectedServices']) && !empty($_POST['selectedServices'])){
1993
1994 $selected_services = sanitize_text_field(wp_unslash($_POST['selectedServices']));
1995 $selected_services = json_decode($selected_services, true);
1996
1997 // Cap each line at the service's Max Quantity before it is
1998 // priced, described or stored on the booking.
1999 $selected_services = ESHB_Helper::eshb_clamp_selected_services($selected_services);
2000
2001 }
2002
2003
2004 $pricing = $this->calculate_booking_pricing($accomodation_id, $start_date, $end_date, $room_quantity, $extra_bed_quantity, $adult_quantity, $children_quantity, $selected_services, true, $start_time, $end_time);
2005
2006 wp_send_json_success($pricing);
2007 }
2008
2009 wp_die();
2010 }
2011
2012 public function eshb_get_booking_prices_rest( $request ) {
2013 do_action('eshb_before_calculate_pricing');
2014
2015 $params = $request->get_params();
2016
2017 // // Nonce check
2018 if ( ! isset($params['nonce']) || ! wp_verify_nonce($params['nonce'], ESHB_Helper::generate_secure_nonce_action('eshb_global_nonce_action')) ) {
2019 return new WP_Error('invalid_nonce', 'Invalid nonce', ['status' => 403]);
2020 }
2021
2022 $accomodation_id = (int) ($params['accomodationId'] ?? 0);
2023 $start_date = $params['startDate'] ?? gmdate('Y-m-d');
2024 $end_date = $params['endDate'] ?? gmdate('Y-m-d', strtotime('+1 day'));
2025 $start_time = $params['startTime'] ?? '';
2026 $end_time = $params['endTime'] ?? '';
2027 $room_quantity = (int) ($params['roomQuantity'] ?? 1);
2028 $extra_bed_quantity = (int) ($params['extraBedQuantity'] ?? 0);
2029 $adult_quantity = (int) ($params['adultQuantity'] ?? 1);
2030 $children_quantity = (int) ($params['childrenQuantity'] ?? 0);
2031 $selected_services = $params['selectedServices'] ?? [];
2032
2033 $pricing = $this->calculate_booking_pricing(
2034 $accomodation_id,
2035 $start_date,
2036 $end_date,
2037 $room_quantity,
2038 $extra_bed_quantity,
2039 $adult_quantity,
2040 $children_quantity,
2041 $selected_services,
2042 true,
2043 $start_time,
2044 $end_time
2045 );
2046
2047 return rest_ensure_response($pricing);
2048 }
2049
2050 public function eshb_get_extra_services_charge(){
2051
2052 // Verify nonce for security
2053 if (!isset($_POST['nonce']) || !wp_verify_nonce( sanitize_text_field(wp_unslash($_POST['nonce'])), ESHB_Helper::generate_secure_nonce_action('eshb_global_nonce_action'))) {
2054 wp_send_json_error(['message' => 'Invalid nonce']);
2055 die();
2056 }
2057
2058 if(isset($_POST['selectedServices']) && !empty($_POST['selectedServices'])){
2059
2060 $selected_services = array_map('sanitize_text_field', wp_unslash($_POST['selectedServices']));
2061 $days_count = isset($_POST['dayCount']) ? sanitize_text_field(wp_unslash($_POST['dayCount'])) : 1;
2062
2063 $price = 0;
2064
2065 if(is_array($selected_services)){
2066 foreach ($selected_services as $service) {
2067
2068 $service_id = $service['id'];
2069 $quantity = $service['quantity'];
2070
2071 $eshb_service_metaboxes = get_post_meta($service_id, 'eshb_service_metaboxes', true);
2072
2073 $service_periodicity = $eshb_service_metaboxes['service_periodicity'];
2074
2075 if (isset($eshb_service_metaboxes['service_price'])) { // Ensure 'service_price' is set
2076 $single_price = $eshb_service_metaboxes['service_price'] * $quantity;
2077 if($service_periodicity == 'perday' && !empty($days_count)){
2078 $single_price = $days_count * $single_price;
2079 }
2080 $price += floatval($single_price); // Add the single price to the total price
2081 }
2082
2083 }
2084 }
2085
2086 wp_send_json_success( ['price' => $price] );
2087
2088 wp_die();
2089
2090 }
2091
2092 }
2093
2094 public function get_available_room_count_by_date_range($accomodation_id, $start_date, $end_date, $start_time = '', $end_time = '') {
2095 $accomodation_id = intval($accomodation_id);
2096 $start_date_obj = new DateTime($start_date);
2097 $end_date_obj = new DateTime($end_date);
2098
2099 if ($end_date_obj < $start_date_obj) {
2100 return new WP_Error('invalid_date_range', 'End date cannot be earlier than start date.');
2101 }
2102
2103 // Fetch total rooms
2104 $accomodation_metas = get_post_meta($accomodation_id, 'eshb_accomodation_metaboxes', true);
2105 $total_rooms = isset($accomodation_metas['total_rooms']) ? intval($accomodation_metas['total_rooms']) : 1;
2106
2107 // Initialize bookings count array
2108 $bookings_per_date = [];
2109
2110 // Query all bookings
2111 $bookings_args = [
2112 'post_type' => 'eshb_booking',
2113 'posts_per_page' => -1,
2114 //'post_status' => 'publish',
2115 'post_status' => ['publish', 'completed', 'processing', 'deposit-payment', 'pending'],
2116 'fields' => 'ids',
2117 ];
2118 $bookings = new WP_Query($bookings_args);
2119
2120 if ($bookings->have_posts()) {
2121 while ($bookings->have_posts()) {
2122 $bookings->the_post();
2123 $booking_id = get_the_ID();
2124 $booking_metas = get_post_meta($booking_id, 'eshb_booking_metaboxes', true);
2125 $booking_accomodation_id = $booking_metas['booking_accomodation_id'];
2126 $booking_status = isset($booking_metas['booking_status']) ? $booking_metas['booking_status'] : '';
2127 $valid_statuses = array('pending', 'processing', 'on-hold', 'completed', 'deposit-payment');
2128
2129 if (
2130 is_array($booking_metas) &&
2131 intval($booking_accomodation_id) === $accomodation_id &&
2132 isset($booking_metas['booking_start_date'], $booking_metas['booking_end_date']) &&
2133 in_array($booking_status, $valid_statuses)
2134 ) {
2135 $booking_start = new DateTime($booking_metas['booking_start_date']);
2136 $booking_end = new DateTime($booking_metas['booking_end_date']);
2137 $booked_rooms = !empty($booking_metas['room_quantity']) ? intval($booking_metas['room_quantity']) : 1;
2138 $accomodation_metaboxes = get_post_meta( $booking_accomodation_id, 'eshb_accomodation_metaboxes', true );
2139 $total_rooms = !empty($accomodation_metaboxes['total_rooms']) ? $accomodation_metaboxes['total_rooms'] : 1;
2140
2141 if(!empty($start_time) && !empty($end_time)) {
2142 $booking_start_time = !empty($booking_metas['booking_start_time']) ? $booking_metas['booking_start_time'] : '';
2143 $booking_end_time = !empty($booking_metas['booking_end_time']) ? $booking_metas['booking_end_time'] : '';
2144
2145 if($booking_start_time !== $start_time) {
2146 $booked_rooms = 0;
2147 }
2148 if($booking_start_time === $start_time && $booking_end_time === $end_time) {
2149
2150 $booked_rooms = 0;
2151 }
2152 }
2153
2154 // Loop through each date in this booking
2155 $period = new DatePeriod($booking_start, new DateInterval('P1D'), (clone $booking_end)->modify('+0 day'));
2156 if($booking_start == $booking_end) {
2157 $period = new DatePeriod($booking_start, new DateInterval('P1D'), (clone $booking_end)->modify('+1 day'));
2158 }
2159
2160 foreach ($period as $date) {
2161 $formatted = $date->format('Y-m-d');
2162 if (!isset($bookings_per_date[$formatted])) {
2163 $bookings_per_date[$formatted] = 0;
2164 }
2165 $bookings_per_date[$formatted] += $booked_rooms;
2166 }
2167
2168 }
2169 }
2170 wp_reset_postdata();
2171 }
2172
2173
2174
2175 // Now calculate the max number of rooms booked in the given date range
2176 $range_period = new DatePeriod($start_date_obj, new DateInterval('P1D'), (clone $end_date_obj)->modify('+0 day'));
2177 $max_booked_in_range = 0;
2178
2179 if($start_date_obj == $end_date_obj){
2180 $formatted = $start_date_obj->format('Y-m-d');
2181 $available_rooms = isset($bookings_per_date[$formatted]) ? $total_rooms - $bookings_per_date[$formatted] : $total_rooms;
2182 return $available_rooms;
2183 }
2184
2185 foreach ($range_period as $date) {
2186 $formatted = $date->format('Y-m-d');
2187 $booked = isset($bookings_per_date[$formatted]) ? $bookings_per_date[$formatted] : 0;
2188 if ($booked > $max_booked_in_range) {
2189 $max_booked_in_range = $booked;
2190 }
2191 }
2192
2193 // Final available room count
2194 $available_rooms = max(0, $total_rooms - $max_booked_in_range);
2195
2196 if($available_rooms < 0) {
2197 $available_rooms = 0;
2198 }
2199
2200 return $available_rooms;
2201 }
2202
2203 public function eshb_get_available_rooms_counts_data(){
2204
2205 // Verify nonce for security
2206 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), ESHB_Helper::generate_secure_nonce_action('eshb_global_nonce_action'))) {
2207 wp_send_json_error(['message' => 'Invalid nonce']);
2208 die();
2209 }
2210
2211
2212 $accomodation_id = !empty($_POST['accomodationId']) ? sanitize_text_field( wp_unslash($_POST['accomodationId']) ) : '';
2213 $start_date = !empty($_POST['startDate']) ? sanitize_text_field( wp_unslash($_POST['startDate']) ) : '';
2214 $end_date = !empty($_POST['endDate']) ? sanitize_text_field( wp_unslash($_POST['endDate']) ) : '';
2215
2216 $start_time = !empty($_POST['startTime']) ? sanitize_text_field( wp_unslash($_POST['startTime']) ) : '';
2217 $end_time = !empty($_POST['endTime']) ? sanitize_text_field( wp_unslash($_POST['endTime']) ) : '';
2218
2219
2220 $start_date = new DateTime($start_date);
2221 $end_date = new DateTime($end_date);
2222
2223 $eshb_accomodation_metaboxes = get_post_meta($accomodation_id, 'eshb_accomodation_metaboxes', true);
2224 $total_rooms = !empty($eshb_accomodation_metaboxes['total_rooms']) ? $eshb_accomodation_metaboxes['total_rooms'] : 0;
2225
2226 $available = 1;
2227 $available = $this->get_available_room_count_by_date_range($accomodation_id, $start_date->format('Y-m-d'), $end_date->format('Y-m-d'), $start_time, $end_time);
2228 if($available < 0 ) {
2229 $available = 0;
2230 }
2231
2232 wp_send_json_success(
2233 array(
2234 'available' => $available,
2235 'total' => $total_rooms,
2236 )
2237 );
2238
2239 die();
2240
2241 }
2242
2243 public function eshb_get_accomodation_available_capacity_counts(){
2244
2245 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), ESHB_Helper::generate_secure_nonce_action('eshb_global_nonce_action'))) {
2246 wp_send_json_error(['message' => 'Invalid nonce']);
2247 die();
2248 }
2249
2250
2251 if(isset($_POST['accomodationId']) && !empty($_POST['accomodationId']) && isset($_POST['type']) && !empty($_POST['type'])){
2252 $type = sanitize_text_field( wp_unslash($_POST['type']) );
2253 $accomodation_id = sanitize_text_field( wp_unslash($_POST['accomodationId']) );
2254 $accomodation_metaboxes = get_post_meta($accomodation_id, 'eshb_accomodation_metaboxes', true);
2255 $total_guest_quantity = $accomodation_metaboxes['total_capacity'];
2256 $selected_rooms_count = !empty($_POST['rooms']) ? sanitize_text_field( wp_unslash($_POST['rooms']) ) : 1;
2257 $start_date = !empty($_POST['startDate']) ? sanitize_text_field( wp_unslash($_POST['startDate']) ) : '';
2258 $end_date = !empty($_POST['endDate']) ? sanitize_text_field( wp_unslash($_POST['endDate']) ) : '';
2259 $start_date = new DateTime($start_date);
2260 $end_date = new DateTime($end_date);
2261
2262 $available = 1;
2263 $msg = __('Maximum Capacity', 'easy-hotel');
2264
2265 if($type == 'room_quantity'){
2266 $total = $accomodation_metaboxes['total_rooms'];
2267
2268 $available = $this->get_available_room_count_by_date_range($accomodation_id, $start_date->format('Y-m-d'), $end_date->format('Y-m-d'));
2269
2270 if($available < 1){
2271 $msg = __('Available Room', 'easy-hotel');
2272 }
2273
2274 }
2275
2276 if($type == 'adult_quantity'){
2277
2278 if(!empty($accomodation_metaboxes['adult_capacity'])){
2279 $available = $accomodation_metaboxes['adult_capacity'];
2280 $available = $available * $selected_rooms_count;
2281 }else{
2282 $childrenQuantity = !empty($_POST['childrenQuantity']) ? sanitize_text_field( wp_unslash( $_POST['childrenQuantity'] ) ) : 0;
2283
2284 $total_capacity = $accomodation_metaboxes['total_capacity'];
2285
2286 $available = $total_capacity - $childrenQuantity;
2287
2288 $available = $available * $selected_rooms_count;
2289
2290 $msg = __('Maximum Adult and Children Capacity', 'easy-hotel');
2291 }
2292
2293 }
2294
2295 if($type == 'children_quantity'){
2296
2297 $total_capacity = $accomodation_metaboxes['total_capacity'];
2298
2299 if(!empty($accomodation_metaboxes['children_capacity'])){
2300 $available = $accomodation_metaboxes['children_capacity'];
2301 $available = $available * $selected_rooms_count;
2302 }else{
2303 $childrenQuantity = !empty($_POST['adultQuantity']) ? sanitize_text_field( wp_unslash( $_POST['adultQuantity'] ) ) : 0;
2304
2305 $available = $total_capacity - $childrenQuantity;
2306
2307 $available = $available * $selected_rooms_count;
2308
2309 $msg = __('Maximum Adult and Children Capacity', 'easy-hotel');
2310 }
2311
2312 }
2313
2314 if($type == 'extra_bed_quantity'){
2315 $available = $accomodation_metaboxes['total_extra_beds'];
2316 $available = $available * $selected_rooms_count;
2317 }
2318
2319 if($type == 'service-quantity'){
2320 $available = $total_guest_quantity;
2321 }
2322
2323
2324 if(($type != 'room_quantity') && (empty($available) || $available < 1)){
2325 $available = $total;
2326 }
2327
2328 if($available < 0) {
2329 $available = 0;
2330 }
2331
2332 $msg = $type != 'adult_quantity' || $type != 'children_quantity' ? $msg . ' ' . $available : $msg;
2333
2334 wp_send_json_success(
2335 array(
2336 'message' => $msg,
2337 'available' => $available,
2338 'type' => $type,
2339 )
2340 );
2341 }
2342 }
2343
2344 public function get_accomodation_available_extra_bed_counts(){
2345
2346 // Verify nonce for security
2347 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), ESHB_Helper::generate_secure_nonce_action('eshb_global_nonce_action'))) {
2348 wp_send_json_error(['message' => 'Invalid nonce']);
2349 die();
2350 }
2351
2352 if(isset($_POST['accomodationId']) && !empty($_POST['accomodationId']) && !empty($_POST['accomodationId'])){
2353
2354 $total_extra_beds = 0;
2355 $accomodation_id = (int) sanitize_text_field( wp_unslash($_POST['accomodationId']) );
2356 $accomodation_metaboxes = get_post_meta($accomodation_id, 'eshb_accomodation_metaboxes', true);
2357
2358 $total_extra_beds = $accomodation_metaboxes['total_extra_beds'];
2359
2360 wp_send_json_success( $total_extra_beds );
2361
2362 }
2363 }
2364
2365 public function send_email_woocommerce_style($email, $subject, $heading, $message) {
2366 if ( ! function_exists( 'WC' ) ) {
2367 return;
2368 }
2369
2370 // Get WooCommerce mailer
2371 $mailer = WC()->mailer();
2372
2373 // Wrap message with WooCommerce email template
2374 $wrapped_message = $mailer->wrap_message($heading, $message);
2375
2376 // Create new WC_Email instance
2377 $wc_email = new WC_Email();
2378
2379 // Style the wrapped message with WooCommerce inline styles
2380 $html_message = $wc_email->style_inline($wrapped_message);
2381
2382 // Headers
2383 $headers = array('Content-Type: text/html; charset=UTF-8');
2384
2385 // Send the email
2386 $mailer->send($email, $subject, $html_message, $headers);
2387 }
2388
2389 public function filter_status_based_on_booking_meta($status, $order_id) {
2390 // Check if custom meta _booking_post_created exists
2391 $booking_post_id = get_post_meta($order_id, '_booking_post_created', true);
2392
2393 $eshb_settings = get_option('eshb_settings', []);
2394
2395 // Don't auto-complete an order that still owes a deposit balance —
2396 // it must remain 'deposit-payment' until the rest is paid. A full
2397 // payment never has a captured deposit recorded, so it auto-completes.
2398 $is_deposit_order = ( (float) get_post_meta( $order_id, 'initial_deposit', true ) > 0 );
2399
2400 if ( ! empty($booking_post_id) && ! $is_deposit_order ) {
2401 if(isset($eshb_settings['booking-auto-approval']) && $eshb_settings['booking-auto-approval'] == true){
2402 return 'completed'; // Mark order as completed
2403 }
2404 }
2405
2406 // Otherwise, keep default status (usually 'processing')
2407 return $status;
2408 }
2409
2410 public function update_booking_status_on_woocommerce_order_status_change($order_id, $old_status, $new_status, $order) {
2411
2412 if($old_status == $new_status) {
2413 return;
2414 }
2415
2416 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
2417
2418 //if ( $screen && $screen->id !== 'woocommerce_page_wc-orders' ) return;
2419 if ( $screen && $screen->id === 'edit-eshb_booking' ) return;
2420
2421 if ( $screen && 'woocommerce_page_wc-orders' === $screen->id ) {
2422 //return;
2423 }
2424
2425 // Retrieve the order object
2426 $order = wc_get_order($order_id);
2427
2428 // Get the booking post ID from order meta
2429 $booking_post_id = get_post_meta($order_id, '_booking_post_created', true); // Adjusted to your meta key
2430
2431 if ($booking_post_id) {
2432
2433 if ( 'processing' === $new_status && $order ) {
2434 $eshb_settings = get_option( 'eshb_settings', [] );
2435 $auto_approve = ! empty( $eshb_settings['booking-auto-approval'] ) && $eshb_settings['booking-auto-approval'] == true;
2436 $is_deposit = ( (float) get_post_meta( $order_id, 'initial_deposit', true ) > 0 )
2437 || ( (float) get_post_meta( $order_id, 'eshb_booking_due_amount', true ) > 0 );
2438
2439 if ( $auto_approve && ! $is_deposit ) {
2440 $new_status = 'completed';
2441 if ( $order->get_status() !== 'completed' ) {
2442 // Re-enters this method with new_status='completed', which
2443 // then falls through to the normal sync below.
2444 $order->update_status( 'completed', 'Auto-approved: full payment received.' );
2445 }
2446 }
2447 }
2448
2449 // update booking status
2450 $updated_booking = array(
2451 'ID' => $booking_post_id,
2452 'post_status' => $new_status,
2453 );
2454
2455 wp_update_post( $updated_booking );
2456
2457 // Get the existing meta for the custom post
2458 $booking_meta = get_post_meta($booking_post_id, 'eshb_booking_metaboxes', true);
2459
2460 // Ensure the meta is an array before modifying it
2461 if (is_array($booking_meta)) {
2462 // Update the `booking_status` field
2463 $booking_meta['booking_status'] = $new_status;
2464
2465 // Save the updated meta back to the post
2466 update_post_meta($booking_post_id, 'eshb_booking_metaboxes', $booking_meta);
2467 }
2468 }
2469 }
2470
2471 public function update_woocommerce_order_status_on_booking_status_change($post_id, $post, $update) {
2472
2473 if(class_exists('ESHB_MANUAL_BOOKING') && is_admin()){
2474 return;
2475 }
2476
2477 if ( ! function_exists( 'WC' ) ) {
2478 return;
2479 }
2480
2481 // Ensure this runs only during post update
2482 if (!$update || wp_is_post_autosave($post_id) || wp_is_post_revision($post_id)) {
2483 return;
2484 }
2485
2486 // Check if the post type matches
2487 if ($post->post_type !== 'eshb_booking') {
2488 return;
2489 }
2490
2491
2492
2493 // Retrieve the metabox data
2494 $eshb_booking_metaboxes = get_post_meta($post_id, 'eshb_booking_metaboxes', true) ?? [];
2495
2496 // Validate the required fields
2497 if (empty($eshb_booking_metaboxes) || empty($eshb_booking_metaboxes['booking_status']) || empty($eshb_booking_metaboxes['order_id'])) {
2498 return;
2499 }
2500
2501 $booking_status = sanitize_text_field($eshb_booking_metaboxes['booking_status']);
2502 $order_id = intval($eshb_booking_metaboxes['order_id']);
2503
2504 // Get the WooCommerce order object
2505 $order = wc_get_order($order_id);
2506 if (!$order) {
2507 return; // Invalid order
2508 }
2509
2510 // Check if the current order status matches the booking status to avoid redundant updates
2511 if ($order->get_status() === $booking_status) {
2512 return; // Status is already up-to-date
2513 }
2514
2515 try {
2516 $order->update_status($booking_status, 'Booking status updated to: ' . $booking_status);
2517 } catch (Exception $e) {
2518
2519 }
2520 }
2521
2522 public function send_booking_email_notification_for_booking_status($post_id, $post, $update){
2523
2524 // Ensure this runs only during post update
2525 if (!$update || wp_is_post_autosave($post_id) || wp_is_post_revision($post_id)) {
2526 return;
2527 }
2528
2529 // Check if the post type matches
2530 if ($post->post_type !== 'eshb_booking') {
2531 return;
2532 }
2533
2534 // Retrieve the metabox data
2535 $eshb_booking_metaboxes = get_post_meta($post_id, 'eshb_booking_metaboxes', true) ?? [];
2536
2537
2538 // Validate the required fields
2539 if (empty($eshb_booking_metaboxes) || empty($eshb_booking_metaboxes['booking_status']) || empty($eshb_booking_metaboxes['order_id'])) {
2540 return;
2541 }
2542
2543 $booking_status = $post->post_status;
2544 $order_id = $eshb_booking_metaboxes['order_id'];
2545
2546 $eshb_settings = get_option('eshb_settings', []);
2547 $booking_type = isset($eshb_settings['booking-type']) && !empty($eshb_settings['booking-type']) ? $eshb_settings['booking-type'] : 'woocommerce';
2548
2549
2550 do_action('eshb_booking_status_changed', $post_id, $booking_status, $booking_type, $order_id);
2551
2552 if(class_exists('ESHB_Booking_Templates')) {
2553 return;
2554 }
2555 // send email for woocommerce
2556 if ( $booking_type == 'woocommerce' && function_exists( 'WC' ) ) {
2557
2558 $order = wc_get_order($order_id);
2559 if (!$order) {
2560 return; // Invalid order
2561 }
2562 // Trigger email manually
2563 $mailer = WC()->mailer();
2564
2565 switch ( $booking_status ) {
2566
2567 case 'cancelled':
2568 $to = $order->get_billing_email();
2569 $subject = sprintf(
2570 /* translators: %s: booking/post ID */
2571 __( 'Your Booking #%s has been Cancelled', 'easy-hotel' ),
2572 esc_html( $post_id )
2573 );
2574 $heading = __( 'Booking Cancelled', 'easy-hotel' );
2575
2576 $first_name = esc_html( $order->get_billing_first_name() );
2577 $post_title = esc_html( get_the_title( $post_id ) );
2578
2579 $message = '<p>' . sprintf(
2580 /* translators: %s: customer first name */
2581 __( 'Hi %s,', 'easy-hotel' ),
2582 $first_name
2583 ) . '</p>';
2584
2585 $message .= '<p>' . sprintf(
2586 /* translators: %s: booking title */
2587 __( 'We regret to inform you that your <strong>%s</strong> has been <strong>cancelled</strong>.', 'easy-hotel' ),
2588 $post_title
2589 ) . '</p>';
2590
2591 $message .= '<p>' . __( 'If you have any questions, please feel free to reply to this email.', 'easy-hotel' ) . '</p>';
2592 $message .= '<p>' . __( 'Thank you for shopping with us.', 'easy-hotel' ) . '</p>';
2593
2594 $this->send_email_woocommerce_style( $to, $subject, $heading, $message );
2595 break;
2596 }
2597
2598 }
2599 }
2600
2601 // -------------------------------------------------------------------------
2602 // Cart Blocking Methods
2603 // -------------------------------------------------------------------------
2604
2605 private function eshb_get_session_token() {
2606
2607 if ( function_exists( 'eshb_native_checkout_is_enabled' ) && eshb_native_checkout_is_enabled()
2608 && function_exists( 'eshb_native_checkout_get_token' ) ) {
2609 $native_token = eshb_native_checkout_get_token( false );
2610 if ( ! empty( $native_token ) ) {
2611 return 'native_' . $native_token;
2612 }
2613 }
2614 if ( class_exists( 'WooCommerce' ) && WC()->session ) {
2615 return (string) WC()->session->get_customer_id();
2616 }
2617 return '';
2618 }
2619
2620 private function eshb_dates_overlap( $s1, $e1, $s2, $e2 ) {
2621 return $s1 < $e2 && $s2 < $e1;
2622 }
2623
2624 private function eshb_get_blocking_settings() {
2625 $eshb_settings = get_option( 'eshb_settings', [] );
2626 $enabled = ! empty( $eshb_settings['cart-blocking-switcher'] );
2627 $blocking_time = ! empty( $eshb_settings['cart-blocking-time'] ) ? (int) $eshb_settings['cart-blocking-time'] : 5;
2628 return [ 'enabled' => $enabled, 'minutes' => $blocking_time ];
2629 }
2630
2631 private function eshb_set_cart_block( $accom_id, $start_date, $end_date, $room_qty ) {
2632 $cfg = $this->eshb_get_blocking_settings();
2633 if ( ! $cfg['enabled'] ) return 0;
2634
2635 $session = $this->eshb_get_session_token();
2636 if ( empty( $session ) ) return 0;
2637
2638 $key = 'eshb_cart_holds_' . (int) $accom_id;
2639 $holds = get_transient( $key );
2640 if ( ! is_array( $holds ) ) $holds = [];
2641
2642 $now = time();
2643 $until = $now + ( $cfg['minutes'] * 60 );
2644
2645 // Clean expired entries
2646 foreach ( $holds as $token => $hold ) {
2647 if ( $hold['until'] < $now ) unset( $holds[ $token ] );
2648 }
2649
2650 $holds[ $session ] = [
2651 'start_date' => $start_date,
2652 'end_date' => $end_date,
2653 'qty' => (int) $room_qty,
2654 'until' => $until,
2655 ];
2656
2657 set_transient( $key, $holds, $cfg['minutes'] * 60 + 60 );
2658 return $until;
2659 }
2660
2661 private function eshb_check_cart_block( $accom_id, $start_date, $end_date ) {
2662 $cfg = $this->eshb_get_blocking_settings();
2663 if ( ! $cfg['enabled'] ) return '';
2664
2665 $key = 'eshb_cart_holds_' . (int) $accom_id;
2666 $holds = get_transient( $key );
2667 if ( ! is_array( $holds ) ) return '';
2668
2669 $session = $this->eshb_get_session_token();
2670 $now = time();
2671
2672 foreach ( $holds as $token => $hold ) {
2673 if ( $token === $session ) continue;
2674 if ( $hold['until'] < $now ) continue;
2675 if ( $this->eshb_dates_overlap( $start_date, $end_date, $hold['start_date'], $hold['end_date'] ) ) {
2676 $remaining = (int) ceil( ( $hold['until'] - $now ) / 60 );
2677 /* translators: %d: remaining minutes */
2678 return sprintf( __( 'These dates are temporarily reserved by another user. Please try again in %d minute(s).', 'easy-hotel' ), $remaining );
2679 }
2680 }
2681 return '';
2682 }
2683
2684 /**
2685 * Remove the current visitor's hold for one accommodation, deleting the
2686 * transient entirely when no holds remain. Shared by the cart-removal,
2687 * order-completion and Native Checkout release paths.
2688 */
2689 private function eshb_remove_session_hold( $accom_id ) {
2690 $accom_id = (int) $accom_id;
2691 if ( ! $accom_id ) return;
2692
2693 $session = $this->eshb_get_session_token();
2694 if ( empty( $session ) ) return;
2695
2696 $key = 'eshb_cart_holds_' . $accom_id;
2697 $holds = get_transient( $key );
2698 if ( ! is_array( $holds ) ) return;
2699
2700 unset( $holds[ $session ] );
2701 if ( empty( $holds ) ) {
2702 delete_transient( $key );
2703 } else {
2704 $cfg = $this->eshb_get_blocking_settings();
2705 set_transient( $key, $holds, $cfg['minutes'] * 60 + 60 );
2706 }
2707 }
2708
2709 // -------------------------------------------------------------------------
2710 // Public cart-blocking API (used by the Native Checkout controller, which
2711 // is a separate class and short-circuits the WooCommerce add-to-cart flow)
2712 // -------------------------------------------------------------------------
2713
2714 /**
2715 * Place a temporary hold for a native reservation. Returns the unix
2716 * timestamp the hold expires at, or 0 when blocking is disabled.
2717 */
2718 public function eshb_block_dates_for_reservation( $accom_id, $start_date, $end_date, $room_qty ) {
2719 return $this->eshb_set_cart_block( $accom_id, $start_date, $end_date, $room_qty );
2720 }
2721
2722 /**
2723 * Whether the given dates collide with another visitor's active hold.
2724 * Returns a translated message on conflict, or '' when clear.
2725 */
2726 public function eshb_get_cart_block_conflict( $accom_id, $start_date, $end_date ) {
2727 return $this->eshb_check_cart_block( $accom_id, $start_date, $end_date );
2728 }
2729
2730 /**
2731 * Release the current visitor's hold for an accommodation (on
2732 * completed booking or expired/abandoned native reservation).
2733 */
2734 public function eshb_release_cart_block( $accom_id ) {
2735 $this->eshb_remove_session_hold( $accom_id );
2736 }
2737
2738 /**
2739 * Expiry timestamp (unix) of the current visitor's own hold for an
2740 * accommodation, or 0 when there is none. Lets the Native Checkout
2741 * page render an accurate countdown.
2742 */
2743 public function eshb_get_my_block_until( $accom_id ) {
2744 $accom_id = (int) $accom_id;
2745 if ( ! $accom_id ) return 0;
2746
2747 $holds = get_transient( 'eshb_cart_holds_' . $accom_id );
2748 if ( ! is_array( $holds ) ) return 0;
2749
2750 $session = $this->eshb_get_session_token();
2751 if ( empty( $session ) || empty( $holds[ $session ]['until'] ) ) return 0;
2752
2753 return (int) $holds[ $session ]['until'];
2754 }
2755
2756 /**
2757 * Expose the hold-countdown notice markup so the Native Checkout
2758 * template can render the same banner used by the WooCommerce flow.
2759 */
2760 public function eshb_cart_block_notice_html( $mode = 'inline' ) {
2761 return $this->eshb_get_blocking_notice_html( $mode );
2762 }
2763
2764 public function eshb_add_blocked_dates_to_calendar( $all_dates, $accom_id, $accom_metas ) {
2765 $key = 'eshb_cart_holds_' . (int) $accom_id;
2766 $holds = get_transient( $key );
2767
2768 $blocked_ranges = [];
2769 if ( is_array( $holds ) ) {
2770 $now = time();
2771
2772 foreach ( $holds as $token => $hold ) {
2773 if ( $hold['until'] < $now ) continue;
2774 $blocked_ranges[] = [
2775 'start_date' => $hold['start_date'],
2776 'end_date' => $hold['end_date'],
2777 'until' => $hold['until'],
2778 ];
2779 }
2780 }
2781
2782 $all_dates['blocked_dates'] = $blocked_ranges;
2783 return $all_dates;
2784 }
2785
2786 public function eshb_get_cart_blocks_ajax() {
2787 check_ajax_referer( ESHB_Helper::generate_secure_nonce_action( 'eshb_global_nonce_action' ), 'nonce' );
2788
2789 $accom_id = isset( $_POST['accomodation_id'] ) ? (int) sanitize_text_field( wp_unslash( $_POST['accomodation_id'] ) ) : 0;
2790 if ( ! $accom_id ) {
2791 wp_send_json_error( [ 'message' => 'Invalid accommodation ID' ] );
2792 return;
2793 }
2794
2795 $key = 'eshb_cart_holds_' . $accom_id;
2796 $holds = get_transient( $key );
2797
2798 $blocked_ranges = [];
2799 if ( is_array( $holds ) ) {
2800 $now = time();
2801
2802 foreach ( $holds as $token => $hold ) {
2803 if ( $hold['until'] < $now ) continue;
2804 $blocked_ranges[] = [
2805 'start_date' => $hold['start_date'],
2806 'end_date' => $hold['end_date'],
2807 'until' => $hold['until'],
2808 ];
2809 }
2810 }
2811
2812 wp_send_json_success( [ 'blocked_ranges' => $blocked_ranges ] );
2813 }
2814
2815 public function eshb_clear_cart_block_on_removal( $cart_item_key, $cart ) {
2816 $removed = isset( $cart->removed_cart_contents[ $cart_item_key ] ) ? $cart->removed_cart_contents[ $cart_item_key ] : null;
2817 if ( ! $removed ) return;
2818
2819 $accom_id = isset( $removed['accomodation_id'] ) ? (int) $removed['accomodation_id'] : 0;
2820 if ( ! $accom_id ) return;
2821
2822 $this->eshb_remove_session_hold( $accom_id );
2823 }
2824
2825 public function eshb_render_calendar_legend( $accomodation_id = 0 ) {
2826 static $rendered = false;
2827 if ( $rendered || is_admin() ) return;
2828 $rendered = true;
2829
2830 $eshb_settings = get_option( 'eshb_settings', [] );
2831 $cart_blocking_on = ! empty( $eshb_settings['cart-blocking-switcher'] );
2832
2833 $items = [
2834 'available' => [ 'class' => 'eshb-legend-available', 'label' => __( 'Available', 'easy-hotel' ) ],
2835 'selected' => [ 'class' => 'eshb-legend-selected', 'label' => __( 'Selected', 'easy-hotel' ) ],
2836 'booked' => [ 'class' => 'eshb-legend-booked', 'label' => __( 'Booked', 'easy-hotel' ) ],
2837 'holiday' => [ 'class' => 'eshb-legend-holiday', 'label' => __( 'Holiday', 'easy-hotel' ) ],
2838 ];
2839
2840 if ( $cart_blocking_on ) {
2841 $items['reserved'] = [ 'class' => 'eshb-legend-reserved', 'label' => __( 'Reserved', 'easy-hotel' ) ];
2842 }
2843
2844 $items = apply_filters( 'eshb_calendar_legend_items', $items, $accomodation_id );
2845 ?>
2846 <div class="eshb-calendar-legend">
2847 <?php foreach ( $items as $item ) : ?>
2848 <div class="eshb-legend-item">
2849 <span class="eshb-legend-swatch <?php echo esc_attr( $item['class'] ); ?>"></span>
2850 <span class="eshb-legend-label"><?php echo esc_html( $item['label'] ); ?></span>
2851 </div>
2852 <?php endforeach; ?>
2853 </div>
2854 <?php
2855 }
2856
2857 private function eshb_get_blocking_notice_html( $mode = 'fixed' ) {
2858 $eshb_settings = get_option( 'eshb_settings', [] );
2859 if ( empty( $eshb_settings['cart-blocking-switcher'] ) ) return '';
2860
2861 $msg = ! empty( $eshb_settings['cart-blocking-notice-msg'] ) ? esc_html( $eshb_settings['cart-blocking-notice-msg'] ) : esc_html__( 'Your reservation is held for', 'easy-hotel' );
2862
2863 $class = $mode === 'inline'
2864 ? 'woocommerce-info eshb-cart-block-notice eshb-cart-block-notice--inline'
2865 : 'eshb-cart-block-notice eshb-cart-block-notice--fixed';
2866
2867 return '<div id="eshb-cart-block-notice" class="' . $class . '" data-mode="' . esc_attr( $mode ) . '" style="display:none;" aria-live="polite">'
2868 . '&#x23F3; <span class="eshb-block-msg">' . $msg . '</span> <span class="eshb-block-timer">0:00</span>'
2869 . '</div>';
2870 }
2871
2872 public function eshb_render_cart_block_notice_inline() {
2873 if ( $this->eshb_notice_printed || is_admin() ) return;
2874 $this->eshb_notice_printed = true;
2875 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
2876 echo $this->eshb_get_blocking_notice_html( 'inline' );
2877 }
2878
2879 public function eshb_prepend_notice_to_woo_block( $block_content, $block ) {
2880 if ( $this->eshb_notice_printed || is_admin() ) return $block_content;
2881 if ( ! in_array( $block['blockName'], [ 'woocommerce/cart', 'woocommerce/checkout' ], true ) ) return $block_content;
2882 $html = $this->eshb_get_blocking_notice_html( 'inline' );
2883 if ( ! $html ) return $block_content;
2884 $this->eshb_notice_printed = true;
2885 // Append for checkout to avoid breaking React hydration; prepend for cart
2886 return $block['blockName'] === 'woocommerce/checkout'
2887 ? $block_content . $html
2888 : $html . $block_content;
2889 }
2890
2891 public function eshb_footer_notice_fallback() {
2892 if ( $this->eshb_notice_printed || is_admin() ) return;
2893 if ( ! is_cart() && ! is_checkout() ) return;
2894 $html = $this->eshb_get_blocking_notice_html( 'inline' );
2895 if ( ! $html ) return;
2896 $this->eshb_notice_printed = true;
2897 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
2898 echo $html;
2899 }
2900
2901 public function eshb_clear_cart_on_block_expiry() {
2902 check_ajax_referer( ESHB_Helper::generate_secure_nonce_action( 'eshb_global_nonce_action' ), 'nonce' );
2903 if ( class_exists( 'WooCommerce' ) && WC()->cart ) {
2904 WC()->cart->empty_cart();
2905 }
2906 wp_send_json_success( [ 'cleared' => true ] );
2907 }
2908
2909 public function eshb_check_my_session_blocks() {
2910 check_ajax_referer( ESHB_Helper::generate_secure_nonce_action( 'eshb_global_nonce_action' ), 'nonce' );
2911
2912 $accom_ids = isset( $_POST['accom_ids'] ) ? array_map( 'intval', (array) $_POST['accom_ids'] ) : [];
2913 if ( empty( $accom_ids ) ) {
2914 wp_send_json_success( [ 'active_ids' => [] ] );
2915 return;
2916 }
2917
2918 $session = $this->eshb_get_session_token();
2919 $now = time();
2920 $active_ids = [];
2921
2922 foreach ( $accom_ids as $accom_id ) {
2923 if ( ! $accom_id ) continue;
2924 $holds = get_transient( 'eshb_cart_holds_' . $accom_id );
2925 if ( is_array( $holds ) && isset( $holds[ $session ] ) && $holds[ $session ]['until'] > $now ) {
2926 $active_ids[] = $accom_id;
2927 }
2928 }
2929
2930 wp_send_json_success( [ 'active_ids' => $active_ids ] );
2931 }
2932
2933 public function eshb_clear_session_cart_blocks_on_order( $order_id ) {
2934 if ( ! $order_id ) return;
2935
2936 $session = $this->eshb_get_session_token();
2937 if ( empty( $session ) ) return;
2938
2939 $order = wc_get_order( $order_id );
2940 if ( ! $order ) return;
2941
2942 foreach ( $order->get_items() as $item ) {
2943 $accom_id = (int) $item->get_meta( 'accomodation_id' );
2944 if ( ! $accom_id ) continue;
2945
2946 $this->eshb_remove_session_hold( $accom_id );
2947 }
2948 }
2949
2950 public function eshb_checkout_order_review_qty_input( $qty_html, $cart_item, $cart_item_key ) {
2951 if ( ! is_checkout() ) return $qty_html;
2952 $eshb_settings = get_option( 'eshb_settings', [] );
2953 if ( empty( $eshb_settings['direct-booking'] ) ) return $qty_html;
2954 $nonce = wp_create_nonce( ESHB_Helper::generate_secure_nonce_action( 'eshb_global_nonce_action' ) );
2955 $ajaxurl = esc_url( admin_url( 'admin-ajax.php' ) );
2956 return $qty_html
2957 . ' <button type="button" class="eshb-cart-remove-btn" aria-label="' . esc_attr__( 'Remove', 'easy-hotel' ) . '" data-cart-key="' . esc_attr( $cart_item_key ) . '" data-nonce="' . esc_attr( $nonce ) . '" data-ajaxurl="' . esc_attr( $ajaxurl ) . '"><i class="fas fa-times" aria-hidden="true"></i></button>';
2958 }
2959
2960 public function eshb_remove_cart_item() {
2961 check_ajax_referer( ESHB_Helper::generate_secure_nonce_action( 'eshb_global_nonce_action' ), 'nonce' );
2962 $cart_item_key = sanitize_text_field( wp_unslash( $_POST['cart_item_key'] ?? '' ) );
2963 if ( $cart_item_key && class_exists( 'WooCommerce' ) && WC()->cart ) {
2964 WC()->cart->remove_cart_item( $cart_item_key );
2965 }
2966 wp_send_json_success();
2967 }
2968
2969 }
2970 ESHB_Booking::instance();
2971
2972 // Registered once on the singleton (constructor runs multiple times due to `new ESHB_Booking()` calls elsewhere)
2973 add_filter( 'woocommerce_checkout_cart_item_quantity', [ ESHB_Booking::instance(), 'eshb_checkout_order_review_qty_input' ], 10, 3 );