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.helper.php

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

1,352 lines 52.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
3 class ESHB_Helper {
4
5 public static function eshb_nonce_field( $action = 'eshb_action', $field = 'eshb_nonce', $echo = true ) {
6 // Generate site-specific action using home_url()
7 $nonce_action = sanitize_key( $action ) . '_' . md5( home_url() );
8
9 // Create the nonce field HTML
10 $nonce_field = wp_nonce_field( $nonce_action, sanitize_key( $field ), true, false );
11
12 // Whitelist only the safe HTML tags and attributes
13 $allowed_tags = array(
14 'input' => array(
15 'type' => true,
16 'id' => true,
17 'name' => true,
18 'value' => true,
19 ),
20 );
21
22 // Sanitize output
23 $safe_nonce_field = wp_kses( $nonce_field, $allowed_tags );
24
25 if ( $echo ) {
26 // Escaping required by Plugin Check (escape before output)
27 echo wp_kses( $safe_nonce_field, $allowed_tags );
28 return null;
29 }
30
31 // Return sanitized version
32 return $safe_nonce_field;
33 }
34
35 public static function generate_secure_nonce_action($action) {
36 $nonce_action = $action . '_' . md5( home_url() );
37 return $nonce_action;
38 }
39
40 public static function eshb_insert_booking($order_id, $booking_status, $cart_item_data) {
41
42 if(empty($booking_status) || empty($cart_item_data) || $cart_item_data === null) return false;
43
44 $post_id = wp_insert_post(array(
45 'post_type' => 'eshb_booking',
46 'post_title' => 'Booking',
47 'post_status' => $booking_status,
48 'meta_input' => array(
49 'eshb_booking_metaboxes' => $cart_item_data
50 )
51 ));
52
53 // Check if the post was inserted successfully
54 $accomodation_id = !empty($cart_item_data['booking_accomodation_id']) ? $cart_item_data['booking_accomodation_id'] : 0;
55 $accomodation_title = get_the_title( $accomodation_id );
56
57 if (!is_wp_error($post_id)) {
58 wp_update_post(array(
59 'ID' => $post_id,
60 'post_title' => 'Booking #' . $post_id . ' for: ' . $accomodation_title,
61 'post_status' => $booking_status, // Update status to 'publish'
62 ));
63
64
65 // Update Available Rooms Count For This Accomodation
66 $accomodation_metaboxes = get_post_meta( $accomodation_id, 'eshb_accomodation_metaboxes', true );
67 $total_rooms = floatval($accomodation_metaboxes['total_rooms']);
68 $current_available_rooms = floatval($accomodation_metaboxes['available_rooms']);
69 $room_quantity = isset($cart_item_data['room_quantity']) ? $cart_item_data['room_quantity'] : 1;
70
71 if(!empty($current_available_rooms)){
72 $available_rooms = $current_available_rooms - floatval($room_quantity);
73 }else{
74 $available_rooms = $total_rooms - floatval($room_quantity);
75 }
76
77 $accomodation_metaboxes['available_rooms'] = $available_rooms;
78 update_post_meta($accomodation_id, 'eshb_accomodation_metaboxes', $accomodation_metaboxes);
79
80 // Update Order Status
81 $new_status = $booking_status;
82 update_post_meta($order_id, '_booking_post_created', $post_id);
83
84
85 do_action( 'eshb_after_booking_created', $post_id, $order_id );
86
87 return $post_id;
88
89 }else{
90 $error_message = $post_id->get_error_message();
91 return false;
92 }
93 }
94
95 public static function get_clean_number($num){
96 if (intval($num) == $num) {
97 return intval($num);
98 }
99 return $num;
100 }
101
102 public static function get_product_id_for_cart($accomodation_id, $booking_type) {
103 $product_id = 0;
104
105 if ($booking_type == 'woocommerce') {
106 $thumbnail_id = get_post_thumbnail_id($accomodation_id);
107 $product_id = self::get_or_create_woocommerce_product($accomodation_id, $thumbnail_id);
108 }
109 return $product_id;
110 }
111
112 public static function get_main_post_id_for_translated ($post_id) {
113 $main_post_id = $post_id;
114 if ( function_exists( 'pll_get_post' )) {
115 $default_lang = pll_default_language() ? pll_default_language() : 'en';
116 $main_post_id = pll_get_post( $post_id, $default_lang ) ? pll_get_post( $post_id, $default_lang ) : $post_id ;
117
118 }elseif ( function_exists( 'apply_filters' ) && function_exists( 'icl_object_id' ) ) {
119 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Third-party filter provided by WPML, name cannot be prefixed.
120 $main_post_id = apply_filters( 'wpml_original_element_id', NULL, $post_id, 'post_post' );
121 }
122 return $main_post_id;
123 }
124
125 public static function eshb_get_booking_statuses(){
126
127 $statuses = array(
128 'pending' => 'Pending payment',
129 'deposit-payment' => 'Deposit payment',
130 'processing' => 'Processing',
131 'on-hold' => 'On hold',
132 'completed' => 'Completed',
133 'cancelled' => 'Cancelled',
134 'refunded' => 'Refunded',
135 'failed' => 'Failed'
136 );
137 return apply_filters( 'eshb_booking_statuses', $statuses );
138
139 }
140
141 public static function eshb_get_payment_statuses(){
142
143 $order_status = array(
144 'pending' => 'Pending',
145 'processing' => 'Processing',
146 'on-hold' => 'On hold',
147 'completed' => 'Completed',
148 'cancelled' => 'Cancelled',
149 'refunded' => 'Refunded',
150 'failed' => 'Failed'
151 );
152 return $order_status;
153
154 }
155
156 public static function get_or_create_woocommerce_product($accomodation_id, $thumbnail_id) {
157
158 $product_id = get_post_meta($accomodation_id, '_woocommerce_product_id', true);
159 $product = (!empty($product_id) && get_post_status($product_id) === 'publish') ? wc_get_product($product_id) : false;
160
161 if (!$product) {
162 $product = new WC_Product();
163 $product->set_name(get_the_title($accomodation_id));
164 $product->set_price(1);
165 $product->set_regular_price(1);
166 $product->set_virtual(true);
167 $product->set_image_id($thumbnail_id);
168 $product->save();
169
170 $product_id = $product->get_id();
171
172 update_post_meta($accomodation_id, '_woocommerce_product_id', $product_id);
173 update_post_meta($accomodation_id, '_regular_price', 1);
174 }
175
176 return $product_id;
177 }
178
179 public static function update_product_price_by_id($product_id, $new_price) {
180 // Update the regular price
181 update_post_meta($product_id, '_regular_price', $new_price);
182
183 // Update the price (current price)
184 update_post_meta($product_id, '_price', $new_price);
185
186 // Clear WooCommerce product cache
187 wc_delete_product_transients($product_id);
188 }
189
190 public static function get_current_booking_metadata($booking_id, $key) {
191 if (empty($booking_id)) {
192 $booking_id = get_the_ID();
193 }
194 if (empty($booking_id)) {
195 return '';
196 }
197 $eshb_booking_metaboxes = get_post_meta($booking_id, 'eshb_booking_metaboxes', true);
198 return isset($eshb_booking_metaboxes[$key]) ? $eshb_booking_metaboxes[$key] : '';
199 }
200
201 public static function get_current_booking_customer_metadata($booking_id, $key = '', $default_value = 'default') {
202 if (empty($booking_id)) {
203 $booking_id = get_the_ID();
204 }
205
206 $address = get_post_meta($booking_id, 'eshb_booking_customer_details_metaboxes', true);
207 $value = $address ?? $default_value;
208
209 if(!empty($key)){
210
211 $value = ( isset($address[$key]) && '' !== $address[$key] ) ? $address[$key] : $default_value;
212 }
213 return $value;
214 }
215
216 public static function get_current_payment_customer_metadata($id, $key) {
217 if (empty($id)) {
218 $id = get_the_ID();
219 }
220 if (empty($id)) return '';
221
222 $address = get_post_meta($id, 'eshb_payment_customer_details_metaboxes', true);
223 return ( isset($address[$key]) && '' !== $address[$key] ) ? $address[$key] : 'default';
224 }
225
226 public static function eshb_get_payment_ids() {
227
228 $args = array(
229 'post_type' => 'eshb_payment',
230 'posts_per_page' => -1,
231 'fields' => 'ids',
232 'post_status' => array( 'completed' ),
233 'orderby' => 'ID',
234 'order' => 'DESC',
235 );
236
237 $payments = get_posts($args);
238 $payment_ids = [];
239
240 foreach ($payments as $payment_id) {
241 if(get_post_status( $payment_id ) == 'completed'){
242 $payment_ids[$payment_id] = get_the_title($payment_id);
243 }
244 }
245
246 return $payment_ids;
247 }
248
249 public static function eshb_get_booking_ids() {
250
251 $booking_statuses = self::eshb_get_booking_statuses();
252 $booking_status_keys = array_keys( $booking_statuses );
253
254 $args = array(
255 'post_type' => 'eshb_booking',
256 'posts_per_page' => -1,
257 'fields' => 'ids',
258 'post_status' => 'any'
259 );
260
261 $bookings = get_posts($args);
262 $booking_ids = [];
263
264 foreach ($bookings as $booking_id) {
265 $booking_ids[$booking_id] = get_the_title($booking_id);
266 }
267
268 return $booking_ids;
269 }
270
271 public static function eshb_get_payment_gateways() {
272 $gateways = [
273 'manual' => 'Manual Payment',
274 'cod' => 'Cash On Delivey'
275 ];
276 return $gateways;
277 }
278
279 public static function get_total_price_from_order_meta($order){
280 $total_price = 0;
281 foreach ( $order->get_items() as $item ) {
282 $item_total_price = $item->get_meta('Total Price');
283 if (!empty($item_total_price)) {
284 $total_price += floatval($item_total_price);
285 }
286 }
287 return $total_price;
288 }
289
290 public static function eshb_get_wc_state_city_name ($country_code, $code) {
291 $states_file = WP_PLUGIN_DIR . '/woocommerce/i18n/states.php';
292 $state_city_name = '';
293 if ( file_exists( $states_file ) ) {
294 $all_states = include $states_file; // returns array of all countries' states
295 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Third-party filter provided by WooCommerce, name cannot be prefixed.
296 $all_states = apply_filters( 'woocommerce_states', $all_states );
297 $state_city_name = isset( $all_states[$country_code][ $code ] ) ? $all_states[$country_code][ $code ] : '';
298 }
299 return $state_city_name;
300 }
301
302 /**
303 * Whether a country offers any state to pick.
304 *
305 * Reads the same countries.json the admin dropdown is built from, so PHP and
306 * JS never disagree about which countries are stateless. 52 of the 250 entries
307 * — American Samoa, Bermuda, Gibraltar and the like — ship an empty "states"
308 * list, and their State dropdown renders with nothing but its placeholder.
309 *
310 * @param string $country_code Two letter code, e.g. "AS".
311 * @return bool False only when the country is known and has no states.
312 */
313 public static function eshb_country_has_states ( $country_code ) {
314
315 static $countries = null;
316
317 $country_code = trim( (string) $country_code );
318
319 if ( '' === $country_code ) {
320 return false;
321 }
322
323 if ( null === $countries ) {
324
325 $countries = array();
326 $countries_file = ESHB_PL_PATH . 'public/assets/lib/countries.json';
327
328 if ( file_exists( $countries_file ) ) {
329
330 $decoded = json_decode( file_get_contents( $countries_file ), true ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- local plugin asset.
331
332 if ( is_array( $decoded ) ) {
333 foreach ( $decoded as $country ) {
334 if ( ! empty( $country['code2'] ) ) {
335 $countries[ trim( $country['code2'] ) ] = ! empty( $country['states'] );
336 }
337 }
338 }
339
340 }
341
342 }
343
344 if ( ! isset( $countries[ $country_code ] ) ) {
345 return true;
346 }
347
348 return $countries[ $country_code ];
349
350 }
351
352 public static function eshb_get_extra_services () {
353
354 $services = [];
355
356 $posts = get_posts([
357 'post_type' => 'eshb_service',
358 'posts_per_page' => -1
359 ]);
360
361 foreach ( $posts as $post ) {
362 $services[ $post->ID ] = $post->post_title;
363 }
364 return $services;
365 }
366
367 /**
368 * Quantity cap configured on a service (Service Options → Max Quantity).
369 *
370 * Only meaningful for services that expose a quantity stepper, i.e.
371 * charge type other than "Per Room". Returns 0 when no cap is set, in
372 * which case the caller keeps its own natural limit (the guest count).
373 *
374 * @param int $service_id Service post ID.
375 * @param array|null $meta Pre-fetched eshb_service_metaboxes, if available.
376 * @return int Cap, or 0 for "no limit".
377 */
378 public static function eshb_get_service_max_quantity( $service_id, $meta = null ) {
379
380 $service_id = (int) $service_id;
381
382 if ( ! is_array( $meta ) ) {
383 $meta = get_post_meta( $service_id, 'eshb_service_metaboxes', true );
384 }
385
386 $max = is_array( $meta ) && isset( $meta['service_max_quantity'] )
387 ? (int) $meta['service_max_quantity']
388 : 0;
389
390 if ( $max < 1 ) {
391 $max = 0;
392 }
393
394 return (int) apply_filters( 'eshb_service_max_quantity', $max, $service_id, $meta );
395 }
396
397 /**
398 * Clamp a requested service quantity to the configured Max Quantity.
399 *
400 * The browser already blocks the stepper, so this is the server-side
401 * guard for hand-crafted requests and for admin-created bookings.
402 *
403 * @param int $service_id Service post ID.
404 * @param int $quantity Requested quantity.
405 * @param array|null $meta Pre-fetched eshb_service_metaboxes, if available.
406 * @return int
407 */
408 public static function eshb_clamp_service_quantity( $service_id, $quantity, $meta = null ) {
409
410 $quantity = (int) $quantity;
411 $max = self::eshb_get_service_max_quantity( $service_id, $meta );
412
413 if ( $max > 0 && $quantity > $max ) {
414 $quantity = $max;
415 }
416
417 return $quantity;
418 }
419
420 /**
421 * Clamp every line of a selected-services list to its Max Quantity.
422 *
423 * @param mixed $selected_services Decoded [ ['id' => .., 'quantity' => ..], .. ].
424 * @return array
425 */
426 public static function eshb_clamp_selected_services( $selected_services ) {
427
428 if ( ! is_array( $selected_services ) ) {
429 return [];
430 }
431
432 foreach ( $selected_services as $index => $service ) {
433
434 if ( ! is_array( $service ) || empty( $service['id'] ) ) {
435 continue;
436 }
437
438 $selected_services[ $index ]['quantity'] = self::eshb_clamp_service_quantity(
439 $service['id'],
440 $service['quantity'] ?? 0
441 );
442 }
443
444 return $selected_services;
445 }
446
447 // Record a payment (deposit or subsequent) against an order.
448 public static function eshb_assign_payment_to_booking( $payment_id, $order_id, $booking_id, $amount_paid, $update = false, $args = [] ) {
449
450 if ( $amount_paid <= 0 ) {
451 return false;
452 }
453
454 $eshb_settings = get_option('eshb_settings');
455 $booking_type = isset($eshb_settings['booking-type']) ? $eshb_settings['booking-type'] : '';
456 $eshb_booking_metaboxes = get_post_meta( $booking_id, 'eshb_booking_metaboxes', true);
457 $eshb_payment_metaboxes = get_post_meta( $payment_id, 'eshb_payment_metaboxes', true);
458 $total_price = !empty($eshb_booking_metaboxes['total_price']) ? $eshb_booking_metaboxes['total_price'] : 0;
459 $total_paid = !empty($eshb_booking_metaboxes['total_paid']) ? $eshb_booking_metaboxes['total_paid'] : 0;
460 $due = (float) get_post_meta( $order_id, 'eshb_booking_due_amount', true );
461 $booking_new_status = 'deposit-payment';
462
463 $payment_ids = !empty($eshb_booking_metaboxes['payment_ids']) ? $eshb_booking_metaboxes['payment_ids'] : [];
464 if(!in_array($payment_id, $payment_ids)){
465 array_push($payment_ids, $payment_id);
466 }
467
468 if($booking_type == 'woocommerce' && class_exists( 'woocommerce' )){
469 $order = wc_get_order($order_id);
470 if(!$order) return;
471 $total_price = $order->get_subtotal();
472 $total_paid = $order->get_total();
473 }
474
475 $total_paid = 0;
476
477 foreach ( $payment_ids as $payment_id ) {
478 $metabox = get_post_meta( $payment_id, 'eshb_payment_metaboxes', true );
479
480 if ( ! empty( $metabox['amount'] ) ) {
481 $total_paid += floatval( $metabox['amount'] );
482 }
483 }
484
485 $new_due = (float) $total_price - (float)$total_paid;
486 $eshb_booking_metaboxes['payment_ids'] = $payment_ids;
487 $eshb_booking_metaboxes['total_paid'] = $total_paid;
488
489 if($new_due > 0){
490 $eshb_booking_metaboxes['due_amount'] = $new_due;
491 update_post_meta($order_id, 'eshb_booking_due_amount', $new_due);
492 }else{
493
494 if (!empty($eshb_settings['booking-auto-approval']) && $eshb_settings['booking-auto-approval'] == true) {
495 $booking_new_status = 'completed';
496 }else{
497 $booking_new_status = 'processing';
498 }
499 delete_post_meta( $order_id, 'eshb_booking_due_amount');
500 }
501
502 $eshb_booking_metaboxes['booking_status'] = $booking_new_status;
503
504
505 // update booking metabox
506 update_post_meta($booking_id, 'eshb_booking_metaboxes', $eshb_booking_metaboxes);
507
508
509 // update payment metabox
510 if(empty($eshb_payment_metaboxes['transaction_id'])){
511 $transaction_id = 'TXN-' . str_pad( $payment_id, 8, '0', STR_PAD_LEFT );
512 $eshb_payment_metaboxes['transaction_id'] = $transaction_id;
513 update_post_meta($payment_id, 'eshb_payment_metaboxes', $eshb_payment_metaboxes);
514 }
515
516 // update order
517 if($booking_type == 'woocommerce' && class_exists( 'woocommerce' )){
518 // update woocommerce order
519 $order = wc_get_order($order_id);
520 if($order){
521 $order->set_total($total_paid);
522 $order->update_status($booking_new_status);
523 if($new_due <= 0){
524 //$order->payment_complete();
525 $order->add_order_note('Payment received manually.');
526 }
527 $txn = $order->get_transaction_id();
528
529 $order->save();
530 }
531 }else{
532 // update booking status
533 wp_update_post( [
534 'ID' => $booking_id,
535 'post_status' => $booking_new_status
536 ]);
537 }
538
539 return $booking_id;
540 }
541
542 /**
543 * Splits a PHP date() format into its parts, honouring the backslash escape.
544 *
545 * Locales lean on that escape: Spanish stores "j \d\e F \d\e Y", where the
546 * "\d\e" pairs are the literal word "de" and not the day/timezone tokens.
547 * Anything that rewrites the format one character at a time (str_replace or
548 * strtr) turns that word back into tokens, which is how "4 de agosto de
549 * 2026" reached the booking calendar as "4 DDe agosto DDe 2026".
550 *
551 * @param string $format PHP date() format.
552 * @return array List of [ character, is_literal ] pairs.
553 */
554 protected static function eshb_split_date_format( $format ) {
555
556 $parts = array();
557 $format = (string) $format;
558 $length = strlen( $format );
559
560 for ( $i = 0; $i < $length; $i++ ) {
561
562 if ( '\\' === $format[ $i ] && $i + 1 < $length ) {
563 $i++;
564 $parts[] = array( $format[ $i ], true );
565 continue;
566 }
567
568 $parts[] = array( $format[ $i ], false );
569 }
570
571 return $parts;
572 }
573
574 /**
575 * Wraps a run of literal text for moment.js.
576 *
577 * Pure punctuation — "/", "-", ", " — means nothing to moment and reads
578 * better raw; anything else is bracketed so no word inside the format can
579 * ever be mistaken for a token.
580 *
581 * @param string $text
582 * @return string
583 */
584 protected static function eshb_moment_literal( $text ) {
585
586 if ( '' === $text ) {
587 return '';
588 }
589
590 // Brackets are moment's own literal delimiters, so they cannot sit
591 // inside one.
592 $text = str_replace( array( '[', ']' ), '', $text );
593
594 if ( '' === $text || ! preg_match( '/[^\s\/\-.,:;()]/u', $text ) ) {
595 return $text;
596 }
597
598 return '[' . $text . ']';
599 }
600
601 /**
602 * Converts a PHP date() format into a moment.js format.
603 *
604 * Escaped characters — and anything moment reads as a token but PHP does
605 * not define — come back as moment literals, so translated words inside the
606 * site date format survive the trip to JavaScript intact.
607 *
608 * @param string $format PHP date() format, defaults to Settings > General.
609 * @return string
610 */
611 public static function eshb_date_format_to_moment( $format = '' ) {
612
613 $format = '' !== (string) $format ? $format : get_option( 'date_format', 'F j, Y' );
614
615 $tokens = array(
616 'd' => 'DD', 'j' => 'D', 'D' => 'ddd', 'l' => 'dddd',
617 'N' => 'E', 'w' => 'd', 'z' => 'DDD', 'W' => 'W',
618 'F' => 'MMMM', 'm' => 'MM', 'M' => 'MMM', 'n' => 'M',
619 'o' => 'GGGG', 'Y' => 'YYYY', 'y' => 'YY',
620 'a' => 'a', 'A' => 'A', 'g' => 'h', 'G' => 'H',
621 'h' => 'hh', 'H' => 'HH', 'i' => 'mm', 's' => 'ss',
622 'v' => 'SSS', 'u' => 'SSSSSS',
623 'O' => 'ZZ', 'P' => 'Z', 'p' => 'Z', 'T' => 'z',
624 'U' => 'X',
625 );
626
627 $moment = '';
628 $literal = '';
629
630 foreach ( self::eshb_split_date_format( $format ) as $part ) {
631
632 list( $char, $is_literal ) = $part;
633
634 // "S" is the English ordinal suffix. moment folds it into the day
635 // token, so "jS" has to come out as "Do" rather than "D" plus junk.
636 if ( ! $is_literal && 'S' === $char ) {
637 if ( '' === $literal && 'D' === substr( $moment, -1 ) ) {
638 $moment = rtrim( $moment, 'D' ) . 'Do';
639 }
640 continue;
641 }
642
643 if ( ! $is_literal && isset( $tokens[ $char ] ) ) {
644 $moment .= self::eshb_moment_literal( $literal ) . $tokens[ $char ];
645 $literal = '';
646 continue;
647 }
648
649 $literal .= $char;
650 }
651
652 return $moment . self::eshb_moment_literal( $literal );
653 }
654
655 /**
656 * A shape hint for the site date format — "DD/MM/YYYY" — shown as the date
657 * field placeholder while the calendar is open.
658 *
659 * Literal text is left as written, so a Spanish site reads "DD de MM de
660 * YYYY" instead of the mangled "DD \DD\e MM \DD\e YYYY".
661 *
662 * @param string $format PHP date() format, defaults to Settings > General.
663 * @return string
664 */
665 public static function eshb_date_format_hint( $format = '' ) {
666
667 $format = '' !== (string) $format ? $format : get_option( 'date_format', 'm/d/Y' );
668
669 $tokens = array(
670 'd' => 'DD', 'j' => 'DD', 'l' => 'DD', 'D' => 'DD', 'N' => 'DD', 'w' => 'DD',
671 'm' => 'MM', 'n' => 'MM', 'F' => 'MM', 'M' => 'MM',
672 'Y' => 'YYYY', 'y' => 'YY', 'S' => '',
673 );
674
675 $hint = '';
676
677 foreach ( self::eshb_split_date_format( $format ) as $part ) {
678
679 list( $char, $is_literal ) = $part;
680
681 $hint .= ( ! $is_literal && isset( $tokens[ $char ] ) ) ? $tokens[ $char ] : $char;
682 }
683
684 return $hint;
685 }
686
687 /**
688 * Today's date in the site timezone.
689 *
690 * gmdate() answers in UTC, so between local midnight and UTC midnight the
691 * booking form offered yesterday as the default check-in: a Madrid site
692 * (UTC+2) shows the 3rd to anyone opening the form after 22:00 on the 3rd.
693 *
694 * @param string $format
695 * @return string
696 */
697 public static function eshb_today( $format = 'Y-m-d' ) {
698 return current_time( $format );
699 }
700
701 /**
702 * Reads a stored string that carries no timezone of its own — a Y-m-d
703 * booking date, a "14:00" check-in time, a current_time('mysql') stamp — as
704 * a moment in the site timezone.
705 *
706 * strtotime() reads such a string as UTC, because WordPress pins PHP's
707 * default timezone there, and date_i18n()/wp_date() then convert it into
708 * the site timezone. That round trip moves the value by the site offset:
709 * a booking date lands on the day before on any site behind UTC, a 2:00 pm
710 * check-in prints as 4:00 pm in Madrid, and a cancellation stamp written by
711 * current_time() is shifted twice.
712 *
713 * @param string $value
714 * @return int|false Timestamp, or false when the string is unreadable.
715 */
716 protected static function eshb_local_timestamp( $value ) {
717
718 $value = trim( (string) $value );
719
720 if ( '' === $value ) {
721 return false;
722 }
723
724 // A bare calendar date is anchored at midday, so no offset — and no DST
725 // jump — can push it onto a neighbouring day.
726 if ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $value ) ) {
727 $value .= ' 12:00:00';
728 }
729
730 try {
731 $moment = new DateTime( $value, wp_timezone() );
732 } catch ( Exception $e ) {
733 return false;
734 }
735
736 return $moment->getTimestamp();
737 }
738
739 /**
740 * Renders a stored booking date with the site date format.
741 *
742 * @param string $date Date in Y-m-d, or anything DateTime accepts.
743 * @param string $format PHP date() format, defaults to Settings > General.
744 * @return string
745 */
746 public static function eshb_format_date( $date, $format = '' ) {
747
748 $timestamp = self::eshb_local_timestamp( $date );
749
750 if ( false === $timestamp ) {
751 return '';
752 }
753
754 return wp_date( '' !== (string) $format ? $format : get_option( 'date_format' ), $timestamp );
755 }
756
757 /**
758 * Renders a wall clock time — a check-in or slot time such as "14:00" —
759 * with the site time format.
760 *
761 * The stored value is what the hotel typed, not an instant in time, so it
762 * has to come back out unchanged rather than shifted by the site offset.
763 *
764 * @param string $time
765 * @param string $format PHP date() format, defaults to Settings > General.
766 * @return string
767 */
768 public static function eshb_format_time( $time, $format = '' ) {
769
770 $timestamp = self::eshb_local_timestamp( $time );
771
772 if ( false === $timestamp ) {
773 return '';
774 }
775
776 return wp_date( '' !== (string) $format ? $format : get_option( 'time_format' ), $timestamp );
777 }
778
779 /**
780 * Renders a stamp written by current_time( 'mysql' ) — already site local —
781 * with the site date and time formats.
782 *
783 * @param string $value
784 * @param string $format PHP date() format, defaults to Settings > General.
785 * @return string
786 */
787 public static function eshb_format_datetime( $value, $format = '' ) {
788
789 $timestamp = self::eshb_local_timestamp( $value );
790
791 if ( false === $timestamp ) {
792 return '';
793 }
794
795 if ( '' === (string) $format ) {
796 $format = get_option( 'date_format' ) . ' ' . get_option( 'time_format' );
797 }
798
799 return wp_date( $format, $timestamp );
800 }
801
802 public static function get_eshb_default_start_end_date(){
803 $today_date = self::eshb_today(); // Today in the site timezone, not UTC
804
805 // Create a DateTime object from today's date
806 $date = new DateTime($today_date);
807
808 // Add one day
809 $date->modify('+1 day');
810
811 // Get the new date in 'Y-m-d' format
812 $tomorrow_date = $date->format('Y-m-d');
813
814 return array(
815 'start_date' => $today_date,
816 'end_date' => $tomorrow_date
817 );
818 }
819
820 public static function get_eshb_default_start_end_time(){
821 return array(
822 'start_time' => '10:00',
823 'end_time' => '11:00'
824 );
825 }
826
827 public static function eshb_capture_payment ($order_id){
828
829 if(!$order_id) return;
830
831 $order = wc_get_order( $order_id );
832 if ( !$order ) return;
833
834 $order_status = $order->get_status();
835 $due = (float) ( get_post_meta( $order_id, 'eshb_booking_due_amount', true ) ?: 0 );
836 $last_payment_type = get_post_meta( $order_id, 'last_payment_type', true );
837
838 if($due < 1 && $order_status == 'completed'){
839 $last_payment_type = 'full_payment';
840 }
841
842 if(!$last_payment_type) return;
843
844 $booking_id = get_post_meta($order_id, '_booking_post_created', true);
845 if(!$booking_id) return;
846
847 $payment_status = get_post_meta($booking_id, 'payment_status', true);
848
849 $eshb_booking_metaboxes = get_post_meta($booking_id, 'eshb_booking_metaboxes', true);
850 $total_price = ESHB_Helper::get_total_price_from_order_meta($order);
851 $total_paid = !empty($eshb_booking_metaboxes['total_paid']) ? $eshb_booking_metaboxes['total_paid'] : 0;
852
853 if($payment_status == 'completed' && $total_paid > 0) {
854 return;
855 }
856
857 if(in_array($last_payment_type, ['initial_deposit', 'remaining_payment']) && $total_paid >= $total_price) {
858 return;
859 };
860
861
862
863 $gateway = $order->get_payment_method();
864 $currency = $order->get_currency();
865 $fee = 0;
866 $amount = $total_paid;
867 $payment_type = 'Full Payment';
868 $initial_deposit = get_post_meta( $order_id, 'initial_deposit', true );
869 $payment_status = 'completed';
870 $new_due = 0;
871
872
873 if($last_payment_type == 'initial_deposit'){
874 $amount = $initial_deposit;
875 $payment_type = 'Initial Deposit';
876 $status = 'deposit-payment';
877 $new_due = $total_price - $initial_deposit;
878 $total_paid = $initial_deposit;
879 }elseif($last_payment_type == 'remaining_payment'){
880 $amount = $due;
881 $total_paid = $total_price;
882 $payment_type = 'Remaining Payment';
883 }else{
884 $amount = $total_price;
885 $total_paid = $total_price;
886 $payment_type = 'Full Payment';
887 }
888
889 // create payment options
890 $payment_options = [
891 'booking_id' => $booking_id,
892 'transaction_id' => '',
893 'gateway' => $gateway,
894 'gateway_mode' => 'live',
895 'amount' => $amount,
896 'fee' => $fee,
897 'currency' => $currency,
898 'payment_type' => $payment_type,
899 ];
900
901
902 $first_name = $order->get_billing_first_name();
903 $last_name = $order->get_billing_last_name();
904 $email = $order->get_billing_email();
905 $phone = $order->get_billing_phone();
906 $country = $order->get_billing_country();
907 $state = $order->get_billing_state();
908 $city = $order->get_billing_city();
909 $address_1 = $order->get_billing_address_1();
910 $address_2 = $order->get_billing_address_2();
911 $postcode = $order->get_billing_postcode();
912
913 // create customer details
914 $customer_details = [
915 'first_name' => $first_name,
916 'last_name' => $last_name,
917 'email' => $email,
918 'phone' => $phone,
919 'country' => $country,
920 'state' => $state,
921 'city' => $city,
922 'address_1' => $address_1,
923 'address_2' => $address_2,
924 'postcode' => $postcode,
925 ];
926
927 $post_title = 'Payment for Booking #' . $booking_id;
928
929 // insert payment to eshb_payment posttype
930 $payment_id = wp_insert_post(
931 [
932 'post_title' => $post_title,
933 'post_type' => 'eshb_payment',
934 'post_status' => $payment_status,
935 ]
936 );
937
938 // update payment metadata if payment success
939 if($payment_id){
940 $transaction_id = 'TXN-' . str_pad( $payment_id, 8, '0', STR_PAD_LEFT );
941 $payment_options['transaction_id'] = $transaction_id;
942 update_post_meta($payment_id, 'eshb_payment_metaboxes', $payment_options);
943 update_post_meta($payment_id, 'eshb_payment_customer_details_metaboxes', $customer_details);
944 update_post_meta($booking_id, 'eshb_booking_customer_details_metaboxes', $customer_details);
945
946 // update booking metaboxes
947 $payment_ids = !empty($eshb_booking_metaboxes['payment_ids']) ? $eshb_booking_metaboxes['payment_ids'] : [];
948 if(!in_array($payment_id, $payment_ids)){
949 array_push($payment_ids, $payment_id);
950 }
951
952
953 $eshb_booking_metaboxes['payment_ids'] = $payment_ids;
954 $eshb_booking_metaboxes['total_paid'] = $total_paid;
955
956
957 // update due meta
958 if($last_payment_type == 'initial_deposit'){
959 $eshb_booking_metaboxes['due_amount'] = $new_due;
960 update_post_meta($order_id, 'eshb_booking_due_amount', $new_due);
961 update_post_meta($order_id, 'last_payment_type', $last_payment_type);
962 }elseif($last_payment_type == 'remaining_payment'){
963 delete_post_meta( $order_id, 'eshb_booking_due_amount');
964 delete_post_meta( $order_id, 'last_payment_type');
965 }
966
967 if($new_due < 1 || !$new_due){
968 update_post_meta($booking_id, 'payment_status', 'completed');
969 }
970
971 // update booking
972 update_post_meta($booking_id, 'eshb_booking_metaboxes', $eshb_booking_metaboxes);
973
974
975 // allow other plugins to hook after capture payment
976 do_action('eshb_after_capture_payment', $order_id);
977
978
979 // delete last payment type
980 delete_post_meta( $order_id, 'last_payment_type');
981 delete_post_meta($order_id, 'last_requested_payment_amount');
982 }
983 return $payment_id;
984
985 }
986
987 public static function eshb_calculate_time_diff ($start_date, $end_date, $start_time, $end_time) {
988 // Require times; if missing return 0 hours
989 if (empty($start_time) || empty($end_time)) {
990 return 0;
991 }
992
993 // Default end date to start date when empty
994 $start_date = !empty($start_date) ? $start_date : gmdate('Y-m-d');
995 $end_date = !empty($end_date) ? $end_date : $start_date;
996
997 // Normalize special case where end time can be provided as 24:00
998 $is_end_24 = ($end_time === '24:00' || $end_time === '24:00:00');
999 $normalized_end_time = $is_end_24 ? '00:00' : $end_time;
1000
1001 $start_dt = DateTime::createFromFormat('Y-m-d H:i:s', $start_date . ' ' . $start_time);
1002 if(!$start_dt){
1003 $start_dt = DateTime::createFromFormat('Y-m-d H:i', $start_date . ' ' . $start_time);
1004 }
1005
1006 $end_dt = DateTime::createFromFormat('Y-m-d H:i:s', $end_date . ' ' . $normalized_end_time);
1007 if(!$end_dt){
1008 $end_dt = DateTime::createFromFormat('Y-m-d H:i', $end_date . ' ' . $normalized_end_time);
1009 }
1010
1011 if(!$start_dt || !$end_dt){
1012 return 0;
1013 }
1014
1015 if(!$is_end_24 && $start_dt->format('H:i') === $end_dt->format('H:i') && $end_date === $start_date){
1016 return 0;
1017 }
1018
1019 if($is_end_24 || $end_dt <= $start_dt){
1020 $end_dt->modify('+1 day');
1021 }
1022
1023 $duration_seconds = $end_dt->getTimestamp() - $start_dt->getTimestamp();
1024 $hours_count = (int) ceil($duration_seconds / 3600);
1025
1026 return max(0, (int) $hours_count);
1027 }
1028
1029 public static function get_available_times_by_date($accommodation_id, $date, $excludes = []) {
1030 // Get booking settings (working hours)
1031 $settings = get_option('eshb_single_day_settings', []);
1032 $start_time = !empty($settings['start_time']) ? $settings['start_time'] : '10:00';
1033 $end_time = !empty($settings['end_time']) ? $settings['end_time'] : '22:00';
1034
1035 $date_str = gmdate('Y-m-d', strtotime($date));
1036
1037 // Fetch all bookings
1038 $bookings = get_posts([
1039 'post_type' => 'eshb_booking',
1040 'posts_per_page' => -1,
1041 'post_status' => ['publish','completed','processing','deposit-payment','pending'],
1042 'fields' => 'ids',
1043 ]);
1044
1045 $booked_slots = [];
1046 $slot_keys = []; // Track unique slot keys
1047
1048 foreach ($bookings as $booking_id) {
1049 $meta = get_post_meta($booking_id, 'eshb_booking_metaboxes', true);
1050 if (!is_array($meta)) continue;
1051
1052
1053 if ((int)($meta['booking_accomodation_id'] ?? 0) !== (int)$accommodation_id) continue;
1054
1055 // Skip if booking date does not match
1056 if (($meta['booking_start_date'] ?? '') !== $date_str) continue;
1057
1058 $b_start = $meta['booking_start_time'] ?? '';
1059 $b_end = $meta['booking_end_time'] ?? '';
1060
1061 // skip if invalid
1062 if (!$b_start || !$b_end) continue;
1063
1064 $slot_key = $b_start.'-'.$b_end;
1065
1066 // Add unique slot only once
1067 if (!isset($slot_keys[$slot_key])) {
1068 $slot_keys[$slot_key] = true;
1069
1070 $booked_slots[] = [$b_start, $b_end];
1071 }
1072 }
1073
1074 // Build available slots based on working window
1075 $available_slots = [];
1076
1077 // skip for excludes
1078 if (count($excludes) > 0) {
1079 array_push($available_slots, $excludes);
1080 }
1081
1082 $window_start_ts = strtotime("$date_str $start_time");
1083 $window_end_ts = strtotime("$date_str $end_time");
1084
1085 if (empty($booked_slots)) {
1086 // If no bookings, full window is available
1087 $available_slots[] = [$start_time, $end_time];
1088 } else {
1089 // Sort booked slots by start time
1090 usort($booked_slots, function($a, $b) use ($date_str) {
1091 return strtotime("$date_str {$a[0]}") <=> strtotime("$date_str {$b[0]}");
1092 });
1093
1094 $pointer = $window_start_ts;
1095
1096 foreach ($booked_slots as $slot) {
1097 $slot_start_ts = strtotime("$date_str {$slot[0]}");
1098 $slot_end_ts = strtotime("$date_str {$slot[1]}");
1099
1100
1101 if ($slot_start_ts > $pointer) {
1102 $available_slots[] = [gmdate('H:i', $pointer), $slot[0]];
1103 }
1104
1105 $pointer = max($pointer, $slot_end_ts);
1106 }
1107
1108 if ($pointer < $window_end_ts) {
1109 $available_slots[] = [gmdate('H:i', $pointer), $end_time];
1110 }
1111 }
1112
1113 return [
1114 'booked_slots' => $booked_slots,
1115 'available_slots' => $available_slots,
1116 'next_start_time' => $available_slots[0][0] ?? '',
1117 'next_end_time' => $available_slots[0][1] ?? '',
1118 ];
1119 }
1120
1121 public static function format_to_wp_time($time_string){
1122 return self::eshb_format_time( $time_string );
1123 }
1124
1125 public static function eshb_set_accomodation_localize($accomodation_id = null) {
1126 $cart_url = site_url('/cart');
1127 $checkout_url = site_url('/checkout');
1128 if(class_exists('woocommerce')) {
1129 $cart_url = wc_get_cart_url();
1130 $checkout_url = wc_get_checkout_url();
1131 }
1132
1133 $min_max_settings = [
1134 'calendar_start_date_buffer' => 0,
1135 'required_min_nights' => 1,
1136 'required_max_nights' => 999,
1137 ];
1138
1139 $eshb_settings = get_option('eshb_settings');
1140 $search_capacity_settings = [];
1141 if(!empty($eshb_settings)){
1142 $search_capacity_settings = [
1143 'min_adult_quantity' => !empty($eshb_settings['min-adult-capacity']) ? $eshb_settings['min-adult-capacity'] : 1,
1144 'min_children_quantity' => !empty($eshb_settings['min-children-capacity']) ? $eshb_settings['min-children-capacity'] : 0,
1145 'max_adult_quantity' => !empty($eshb_settings['max-adult-capacity']) ? $eshb_settings['max-adult-capacity'] : 1000,
1146 'max_children_quantity' => !empty($eshb_settings['max-children-capacity']) ? $eshb_settings['max-children-capacity'] : 1000,
1147 ];
1148 }
1149 $booking_capacity_settings = [];
1150 if(!empty($eshb_settings)){
1151 $booking_capacity_settings = [
1152 'min_adult_quantity' => !empty($eshb_settings['booking-min-adult-capacity']) ? $eshb_settings['booking-min-adult-capacity'] : 1,
1153 'min_children_quantity' => !empty($eshb_settings['booking-min-children-capacity']) ? $eshb_settings['booking-min-children-capacity'] : 0,
1154 ];
1155 }
1156
1157 $eshb_min_max_settings = apply_filters( 'eshb_min_max_global_settings_localize', $min_max_settings);
1158 $calendar_start_date_buffer = !empty($eshb_min_max_settings['calendar_start_date_buffer']) ? $eshb_min_max_settings['calendar_start_date_buffer'] : 0;
1159 $required_min_nights = !empty($eshb_min_max_settings['required_min_nights']) ? $eshb_min_max_settings['required_min_nights'] : 1;
1160 $required_max_nights = !empty($eshb_min_max_settings['required_max_nights']) ? $eshb_min_max_settings['required_max_nights'] : 999;
1161
1162 $eshb_week_settings = apply_filters( 'eshb_week_settings', [] );
1163 $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'] : '';
1164
1165
1166 // accomodation details metadata
1167 $eshb_accomodation_metaboxes = false;
1168 $is_it_single_day_booking_accomodation = false;
1169
1170 if(is_singular( 'eshb_accomodation' ) && ($accomodation_id == null || empty($accomodation_id))){
1171 $accomodation_id = get_the_ID();
1172 }
1173
1174
1175 $eshb_booking = new ESHB_Booking();
1176 $start_date = '';
1177 $end_date = '';
1178
1179 if (isset($_GET['nonce']) && wp_verify_nonce( sanitize_text_field(wp_unslash($_GET['nonce'])), ESHB_Helper::generate_secure_nonce_action('eshb_global_nonce_action'))) {
1180 $start_date = !empty($_POST['start_date']) ? sanitize_text_field( wp_unslash($_POST['start_date']) ) : '';
1181 $end_date = !empty($_POST['end_date']) ? sanitize_text_field( wp_unslash($_POST['end_date']) ) : '';
1182 }
1183
1184 if($accomodation_id) {
1185 $eshb_accomodation_metaboxes = get_post_meta($accomodation_id, 'eshb_accomodation_metaboxes', true);
1186 $available_rooms = $eshb_booking->get_available_room_count_by_date_range($accomodation_id, $start_date, $end_date);
1187 $available_rooms = $available_rooms > 0 ? $available_rooms : 0;
1188 $eshb_accomodation_metaboxes['available_rooms'] = $available_rooms;
1189
1190
1191 $accomodation_min_max_settings = apply_filters( 'eshb_min_max_settings', [
1192 'required_min_nights' => $required_min_nights,
1193 'required_max_nights' => $required_max_nights,
1194 'is_global_source_for_min_max' => true,
1195 ], $accomodation_id, $eshb_accomodation_metaboxes );
1196
1197 $required_min_nights = !empty($accomodation_min_max_settings['required_min_nights']) ? $accomodation_min_max_settings['required_min_nights'] : 1;
1198 $required_max_nights = !empty($accomodation_min_max_settings['required_max_nights']) ? $accomodation_min_max_settings['required_max_nights'] : 999;
1199 }
1200
1201 $eshb_translations = [
1202 'maximumCapacity' => __('Maximum Capacity', 'easy-hotel'),
1203 'availableCapacity' => __('Available Capacity', 'easy-hotel'),
1204 'availableRoom' => __('Available Room', 'easy-hotel'),
1205 'maximumAdultAndChildrenCapacity' => __('Maximum Adult and Children Capacity', 'easy-hotel'),
1206 'maximumTimeSlot' => __('Allowed max time for this slot is', 'easy-hotel'),
1207 'minimumTimeSlot' => __('Allowed min time for this slot is', 'easy-hotel'),
1208 'minNightsErrorMsg' => __('Ops! This Reservation has been failed. Requried Minimum', 'easy-hotel'),
1209 'maxNightsErrorMsg' => __('Ops! This Reservation has been failed. Requried Maximum', 'easy-hotel'),
1210 'minNightsErrorMsgAvCal' => __('Requried Minimum Nights:', 'easy-hotel'),
1211 'maxNightsErrorMsgAvCal' => __('Requried Maximum Nights:', 'easy-hotel'),
1212 ];
1213
1214 $eshb_translations = apply_filters( 'eshb_booking_action_messages', $eshb_translations, [$accomodation_id, $eshb_settings] );
1215 $show_count = apply_filters( 'eshb_booking_capacity_count_show', true, [$accomodation_id, $eshb_settings] );
1216
1217 $nonce_action = ESHB_Helper::generate_secure_nonce_action('eshb_global_nonce_action');
1218 wp_localize_script(
1219 'eshb-public-script',
1220 'eshb_ajax',
1221 [
1222 'root' => esc_url(rest_url()),
1223 'ajaxurl' => admin_url( 'admin-ajax.php' ),
1224 'adminURL' => admin_url(),
1225 'wooCartUrl' => $cart_url,
1226 'wooCheckoutUrl' => $checkout_url,
1227 'direct_booking' => !empty($eshb_settings['direct-booking']),
1228 'is_admin' => is_admin(),
1229 'nonce' => wp_create_nonce($nonce_action),
1230 'eshb_add_to_cart_reservation_nonce' => wp_create_nonce('eshb_eshb_add_to_cart_reservation_nonce'),
1231 'reservation_request_nonce' => wp_create_nonce('eshb_reservation_request_nonce'),
1232 'version' => ESHB_VERSION,
1233 'pluginURL' => ESHB_DIR_URL,
1234 'dateFormat' => get_option( 'date_format' ),
1235 'requiredMinNights' => $required_min_nights,
1236 'requiredMaxNights' => $required_max_nights,
1237 'calendar_start_date_buffer' => $calendar_start_date_buffer,
1238 'checkInDayErrorMsg' => $string_check_in_day_error_msg,
1239 'currentAccomodationMeta' => $eshb_accomodation_metaboxes,
1240 'search_capacities' => $search_capacity_settings,
1241 'booking_capacities' => $booking_capacity_settings,
1242 'booking_capacity_count_show' => $show_count,
1243 'translations' => $eshb_translations,
1244 'cart_blocking_enabled' => ! empty( $eshb_settings['cart-blocking-switcher'] ),
1245 'cart_blocking_time' => ! empty( $eshb_settings['cart-blocking-time'] ) ? (int) $eshb_settings['cart-blocking-time'] : 5,
1246 'cart_blocking_color' => ! empty( $eshb_settings['cart-blocking-color'] ) ? esc_attr( $eshb_settings['cart-blocking-color'] ) : '#720eec',
1247 'cart_blocking_notice_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' ),
1248 ]
1249 );
1250 }
1251
1252 /**
1253 * Every session (season) that carries a "Minimum Nights" rule for this accomodation.
1254 *
1255 * Returned in a shape the booking calendar can evaluate client side, so the rule
1256 * is known the moment a date is clicked instead of only after the range has
1257 * already been applied.
1258 *
1259 * @param int|string $accomodation_id
1260 * @return array List of ['start_date','end_date','days','min_nights'].
1261 */
1262 public static function get_eshb_session_min_nights_rules($accomodation_id) {
1263
1264 $all_week = ['saturday', 'sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday'];
1265
1266 $query = new WP_Query( array(
1267 'post_type' => 'eshb_session',
1268 'post_status' => 'publish',
1269 'posts_per_page' => -1,
1270 ) );
1271
1272 $rules = [];
1273
1274 if ($query->have_posts()) {
1275 while ($query->have_posts()) {
1276 $query->the_post();
1277 $post_id = get_the_ID();
1278 $metaboxes = maybe_unserialize(get_post_meta($post_id, 'eshb_session_metaboxes', true));
1279
1280 if (empty($metaboxes['start_date']) || empty($metaboxes['end_date'])) continue;
1281
1282 $min_nights = !empty($metaboxes['min_nights']) ? (int) $metaboxes['min_nights'] : 0;
1283 if ($min_nights < 1) continue;
1284
1285 $accomodation_ids = $metaboxes['accomodation_ids'] ?? [];
1286 if (empty($accomodation_ids) || !in_array($accomodation_id, $accomodation_ids)) continue;
1287
1288 $session_days = !empty($metaboxes['days']) ? $metaboxes['days'] : $all_week;
1289 if (in_array('all', (array) $session_days)) {
1290 $session_days = $all_week;
1291 }
1292
1293 $rules[] = [
1294 'start_date' => $metaboxes['start_date'],
1295 'end_date' => $metaboxes['end_date'],
1296 'days' => array_values( (array) $session_days ),
1297 'min_nights' => $min_nights,
1298 ];
1299 }
1300 wp_reset_postdata();
1301 }
1302
1303 return $rules;
1304 }
1305
1306 /**
1307 * Highest "Minimum Nights" any session imposes on the given stay.
1308 *
1309 * Every night of the stay is checked (the check-out day is excluded, it is not a
1310 * night) and the LARGEST matching value wins — with overlapping seasons the
1311 * strictest rule has to be the one that applies.
1312 *
1313 * @return int 0 when no session covers the stay.
1314 */
1315 public static function get_eshb_min_stay_night_by_session($accomodation_id, $start_date, $end_date) {
1316
1317
1318 if (empty($start_date) || empty($end_date)) {
1319 return 0;
1320 }
1321
1322 $rules = self::get_eshb_session_min_nights_rules($accomodation_id);
1323
1324 if (empty($rules)) {
1325 return 0;
1326 }
1327
1328 // Create date range (exclude checkout date if multi-day)
1329 $period = new DatePeriod(
1330 new DateTime($start_date),
1331 new DateInterval('P1D'),
1332 ($start_date === $end_date)
1333 ? (new DateTime($end_date))->modify('+1 day') // same-day booking
1334 : new DateTime($end_date) // exclude checkout
1335 );
1336
1337 $min_stay_night = 0;
1338
1339 foreach ($period as $day) {
1340 $current_date = $day->format('Y-m-d');
1341 $current_day_name = strtolower($day->format('l'));
1342
1343 foreach ($rules as $rule) {
1344 if ($current_date >= $rule['start_date'] && $current_date <= $rule['end_date'] && in_array($current_day_name, $rule['days'])) {
1345 $min_stay_night = max($min_stay_night, $rule['min_nights']);
1346 }
1347 }
1348 }
1349
1350 return $min_stay_night;
1351 }
1352 }