PluginProbe
Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant / 2.2.8
Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant v2.2.8
2.3.2 2.3.1 2.3.0 2.2.8 2.2.7 trunk 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.10.5 1.11.0 1.11.1 1.11.2 1.6 1.7 1.8 1.8.1 1.8.2 1.8.3 1.9.0 1.9.1 1.9.10 1.9.11 All 60 releases
merchant / inc / modules / product-labels / class-product-labels.php

class-product-labels.php in Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant 2.2.8, at inc/modules/product-labels/class-product-labels.php

1,645 lines 48.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Product Labels.
5 *
6 * @package Merchant
7 */
8
9 if ( ! defined( 'ABSPATH' ) ) {
10 exit; // Exit if accessed directly
11 }
12
13 /**
14 * Product Labels Class.
15 *
16 */
17 class Merchant_Product_Labels extends Merchant_Add_Module {
18
19 /**
20 * Module ID.
21 *
22 */
23 const MODULE_ID = 'product-labels';
24
25 /**
26 * Is module preview.
27 *
28 */
29 public static $is_module_preview = false;
30
31 /**
32 * Whether the module has a shortcode or not.
33 *
34 * @var bool
35 */
36 public $has_shortcode = true;
37
38 /**
39 * Constructor.
40 *
41 */
42 public function __construct() {
43 // Module id.
44 $this->module_id = self::MODULE_ID;
45
46 // WooCommerce only.
47 $this->wc_only = true;
48
49 // Parent construct.
50 parent::__construct();
51
52 // Module section.
53 $this->module_section = 'convert-more';
54
55 // Module default settings.
56 $this->module_default_settings = array(
57 'layout' => 'single-label',
58 'labels' => array(
59 array(
60 'label_type' => 'text',
61 'label' => esc_html__( 'SALE', 'merchant' ),
62 'label_text_shape' => 'text-shape-1',
63 ),
64 ),
65 );
66
67 // Module data.
68 $this->module_data = Merchant_Admin_Modules::$modules_data[ self::MODULE_ID ];
69
70 // Module options path.
71 $this->module_options_path = MERCHANT_DIR . 'inc/modules/' . self::MODULE_ID . '/admin/options.php';
72
73 // Is module preview page.
74 if ( is_admin() && parent::is_module_settings_page() ) {
75 self::$is_module_preview = true;
76
77 // Enqueue admin styles.
78 add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_css' ) );
79 add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_js' ) );
80
81 // Admin preview box.
82 add_filter( 'merchant_module_preview', array( $this, 'render_admin_preview' ), 10, 2 );
83
84 // Custom CSS.
85 // The custom CSS should be added here as well due to ensure preview box works properly.
86 add_filter( 'merchant_custom_css', array( $this, 'admin_custom_css' ) );
87 }
88
89 if ( ! Merchant_Modules::is_module_active( self::MODULE_ID ) ) {
90 return;
91 }
92
93 if ( is_admin() ) {
94 // Init translations.
95 $this->init_translations();
96 }
97
98 // Required for block editor
99 add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_css' ) );
100
101 // Return early if it's on admin but not in the respective module settings page.
102 if ( is_admin() && ! wp_doing_ajax() && ! parent::is_module_settings_page() ) {
103 return;
104 }
105
106 add_filter( 'merchant_product_labels_display_rules', array( $this, 'add_pre_order_option' ) );
107
108 // Enqueue styles.
109 add_action( 'merchant_enqueue_before_main_css_js', array( $this, 'enqueue_css' ) );
110
111 // Enqueue scripts
112 add_action( 'merchant_enqueue_before_main_css_js', array( $this, 'enqueue_js' ) );
113
114 // Product Loop/Archives
115 if ( merchant_is_botiga_active() ) {
116 add_action( 'woocommerce_before_shop_loop_item_title', array( $this, 'loop_product_output' ) );
117 } else {
118 add_action( 'woocommerce_before_shop_loop_item', array( $this, 'loop_product_output' ) );
119 }
120
121 // Ultimate Addon Elementor
122 if ( defined( 'UAEL_FILE' ) ) {
123 add_action( 'uael_woo_products_before_summary_wrap', array( $this, 'loop_product_output' ) );
124 }
125
126 // Woo Block
127 add_filter( 'woocommerce_blocks_product_grid_item_html', array( $this, 'products_block' ), 9999, 3 );
128
129 // Product Single
130 if ( merchant_is_flatsome_active() ) {
131 add_action( 'flatsome_sale_flash', array( $this, 'single_product_output' ) );
132 } elseif ( class_exists( 'Woostify_WooCommerce' ) ) {
133 add_action( 'woostify_product_images_box_end', array( $this, 'single_product_output' ) );
134 } else {
135 add_action( 'woocommerce_product_thumbnails', array( $this, 'single_product_output' ) );
136 }
137
138 // XStore Theme - Archive/Single/Elementor Widget
139 if ( defined( 'ETHEME_FW' ) ) {
140 // Product Single: Default
141 add_action( 'woocommerce_before_single_product_summary', array( $this, 'single_product_output' ) );
142
143 // Product Single: Elementor Product Image widget
144 add_action( 'etheme_before_single_product_image', array( $this, 'single_product_output' ) );
145
146 // Archive: Elementor Products Widgets by XStore
147 add_filter( 'single_product_archive_thumbnail_size', array( $this, 'loop_product_output' ) );
148 }
149
150 add_action( 'woocommerce_single_product_image_gallery_classes', array( $this, 'single_product_image_gallery_classes' ) );
151
152 add_filter( 'woocommerce_sale_flash', array( $this, 'remove_on_sale' ), 9999 );
153
154 // Custom CSS.
155 add_filter( 'merchant_custom_css', array( $this, 'frontend_custom_css' ) );
156
157 $this->migrate_exclusion_list();
158
159 // Migrate exclusion fields for backward compatibility
160 $this->migrate_exclusion_fields();
161 }
162
163 /**
164 * Init translations.
165 *
166 * @return void
167 */
168 public function init_translations() {
169 $settings = $this->get_module_settings();
170 if ( isset( $settings['labels'] ) ) {
171 foreach ( $settings['labels'] as $label ) {
172 if ( isset( $label['label'] ) ) {
173 Merchant_Translator::register_string( $label['label'], esc_html__( 'Product Labels', 'merchant' ) );
174 }
175 if ( isset( $label['percentage_text'] ) ) {
176 Merchant_Translator::register_string( $label['percentage_text'], esc_html__( 'Percentage text in product labels', 'merchant' ) );
177 }
178 }
179 }
180 }
181
182 /**
183 * Add option
184 *
185 * @param $options
186 *
187 * @return mixed
188 */
189 public function add_pre_order_option( $options ) {
190 if ( class_exists( 'Merchant_Pre_Orders' ) && Merchant_Modules::is_module_active( Merchant_Pre_Orders::MODULE_ID ) ) {
191 $options['pre-order'] = esc_html__( 'Pre-Order Products', 'merchant' );
192 }
193
194 return $options;
195 }
196
197 /**
198 * Function for `woocommerce_blocks_product_grid_item_html` filter-hook.
199 *
200 * @param string $html Product grid item HTML.
201 * @param array $data Product data passed to the template.
202 * @param \WC_Product $product Product object.
203 *
204 * @return string
205 */
206 public function products_block( $html, $data, $product ) {
207
208 /**
209 * Filters the HTML for products in the grid.
210 *
211 * @param string $html Product grid item HTML.
212 * @param array $data Product data passed to the template.
213 * @param \WC_Product $product Product object.
214 * @return string Updated product grid item HTML.
215 *
216 * @since 1.9.12
217 */
218 return apply_filters(
219 'merchant_blocks_product_grid_item_html',
220 "<li class=\"wc-block-grid__product merchant_product-labels-grid_item_html\">
221 <a href=\"{$data->permalink}\" class=\"wc-block-grid__product-link\">
222 {$data->image}
223 {$data->title}
224 {$this->get_labels( $product, 'archive' )}
225 </a>
226 {$data->price}
227 {$data->rating}
228 {$data->button}
229 </li>",
230 $data,
231 $product
232 );
233 }
234
235 /**
236 * Remove default sale message
237 *
238 * @param $html
239 *
240 * @return string
241 */
242 public function remove_on_sale( $html ) {
243 return '';
244 }
245
246 /**
247 * Admin enqueue CSS.
248 *
249 * @return void
250 */
251 public function admin_enqueue_css() {
252 $page = ( ! empty( $_GET['page'] ) ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
253 $module = ( ! empty( $_GET['module'] ) ) ? sanitize_text_field( wp_unslash( $_GET['module'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
254
255 if ( 'merchant' === $page && self::MODULE_ID === $module ) {
256 wp_enqueue_style( 'merchant-' . self::MODULE_ID, MERCHANT_URI . 'assets/css/modules/' . self::MODULE_ID . '/product-labels.min.css', array(), MERCHANT_VERSION );
257 wp_enqueue_style( 'merchant-admin-' . self::MODULE_ID, MERCHANT_URI . 'assets/css/modules/' . self::MODULE_ID . '/admin/preview.min.css', array(), MERCHANT_VERSION );
258 }
259 }
260
261 /**
262 * Admin enqueue CSS.
263 *
264 * @return void
265 */
266 public function admin_enqueue_js() {
267 $page = ( ! empty( $_GET['page'] ) ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
268 $module = ( ! empty( $_GET['module'] ) ) ? sanitize_text_field( wp_unslash( $_GET['module'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
269
270 if ( 'merchant' === $page && self::MODULE_ID === $module ) {
271 wp_enqueue_script( 'merchant-' . self::MODULE_ID, MERCHANT_URI . 'assets/js/modules/product-labels/admin/preview.min.js', array( 'jquery', 'merchant-admin' ), '4.0.13',
272 true );
273 }
274 }
275
276 /**
277 * Enqueue CSS.
278 *
279 * @return void
280 */
281 public function enqueue_css() {
282 // Specific module styles.
283 wp_enqueue_style( 'merchant-' . self::MODULE_ID, MERCHANT_URI . 'assets/css/modules/' . self::MODULE_ID . '/product-labels.min.css', array(), MERCHANT_VERSION );
284 }
285
286 /**
287 * Enqueue scripts.
288 *
289 * @return void
290 */
291 public function enqueue_js() {
292 wp_enqueue_script( 'merchant-' . self::MODULE_ID, MERCHANT_URI . 'assets/js/modules/' . self::MODULE_ID . '/product-labels.min.js', array(), MERCHANT_VERSION, true );
293 }
294
295 /**
296 * Render admin preview
297 *
298 * @param Merchant_Admin_Preview $preview
299 * @param string $module
300 *
301 * @return Merchant_Admin_Preview
302 */
303 public function render_admin_preview( $preview, $module ) {
304 if ( self::MODULE_ID === $module ) {
305 ob_start();
306 self::admin_preview_content();
307 $content = ob_get_clean();
308
309 // HTML.
310 $preview->set_html( $content );
311
312 // Label Text.
313 $preview->set_text( 'label_text', '.merchant-onsale' );
314
315 // Position.
316 $preview->set_class( 'label_position', '.merchant-onsale', array( 'top-left', 'top-right' ) );
317 }
318
319 return $preview;
320 }
321
322 /**
323 * Admin preview content.
324 *
325 * @return void
326 */
327 public function admin_preview_content() {
328 ?>
329 <div class="merchant-product-labels-preview">
330 <div class="image-wrapper">
331 <div class="merchant-product-labels merchant-product-labels__regular" data-currency="<?php echo esc_attr( get_woocommerce_currency_symbol() ); ?>">
332 <span class="merchant-label merchant-label-top-left"></span>
333 </div>
334 </div>
335 <h3><?php
336 echo esc_html__( 'Product Title', 'merchant' ); ?></h3>
337 <p><?php
338 echo esc_html__( 'The product description normally goes here.', 'merchant' ); ?></p>
339 </div>
340
341 <?php
342 }
343
344 /**
345 * Get sale percentage & amount
346 *
347 * @return string label
348 */
349 public function get_product_sale_data( $product, $label ) {
350 $sale_data = array(
351 'amount' => 0,
352 'percentage' => 0,
353 );
354
355 if ( $product->is_type( 'variable' ) ) {
356 $regular_price = (float) wc_get_price_to_display( $product, array( 'price' => $product->get_variation_regular_price( 'min' ) ) );
357 $sale_price = (float) wc_get_price_to_display( $product );
358
359 /**
360 * `merchant_product_labels_sale_data_sale_price`
361 *
362 * @since 1.10.5
363 */
364 $sale_price = apply_filters( 'merchant_product_labels_sale_data_sale_price', $sale_price, $product, $label );
365
366 if ( 0 !== $sale_price || ! empty( $sale_price ) ) {
367 $sale_data['amount'] = $regular_price - $sale_price;
368 $sale_data['percentage'] = $regular_price ? round( 100 - ( $sale_price / $regular_price * 100 ) ) : 0;
369 }
370 } elseif ( $product->is_type( 'grouped' ) ) {
371 $children_ids = $product->get_children();
372
373 $total_regular_price = 0;
374 $total_sale_price = 0;
375
376 foreach ( $children_ids as $child_id ) {
377 $child_product = wc_get_product( $child_id );
378
379 $regular_price = (float) $child_product->get_regular_price();
380 $sale_price = (float) $child_product->get_sale_price();
381
382 if ( $child_product->is_type( 'variable' ) ) {
383 $regular_price = (float) $child_product->get_variation_regular_price( 'min' );
384 $sale_price = (float) $child_product->get_variation_sale_price( 'min' );
385 }
386
387 $total_regular_price += $regular_price;
388 $total_sale_price += ! empty( $sale_price ) ? $sale_price : $regular_price;
389 }
390
391 if ( 0 !== $total_sale_price || ! empty( $total_sale_price ) ) {
392 $sale_data['amount'] = $total_regular_price - $total_sale_price;
393 $sale_data['percentage'] = $total_regular_price ? round( 100 - ( ( $total_sale_price / $total_regular_price ) * 100 ) ) : 0;
394 }
395 } else {
396 $regular_price = (float) wc_get_price_to_display( $product, array( 'price' => $product->get_regular_price() ) );
397 $sale_price = (float) wc_get_price_to_display( $product, array( 'price' => $product->get_sale_price() ) );
398
399 /**
400 * `merchant_product_labels_sale_data_sale_price`
401 *
402 * @since 1.10.5
403 */
404 $sale_price = apply_filters( 'merchant_product_labels_sale_data_sale_price', $sale_price, $product, $label );
405
406 if ( 0 !== $sale_price || ! empty( $sale_price ) ) {
407 $sale_data['amount'] = $regular_price - $sale_price;
408 $sale_data['percentage'] = $regular_price ? round( 100 - ( ( $sale_price / $regular_price ) * 100 ),0 ) : 0;
409 }
410 }
411
412 /**
413 * Filter the product sale dat .
414 *
415 * @param int $percentage The product discount percentage.
416 * @param WC_Product $product The product object.
417 * @param array $label The label data.
418 *
419 * @since 1.10.5
420 */
421 return apply_filters( 'merchant_product_labels_sale_data', $sale_data, $product, $label );
422 }
423
424 /**
425 * Custom CSS.
426 *
427 * @return string
428 */
429 public function get_module_custom_css() {
430 $css = '';
431
432 return $css;
433 }
434
435 /**
436 * Admin custom CSS.
437 *
438 * @param string $css The custom CSS.
439 *
440 * @return string $css The custom CSS.
441 */
442 public function admin_custom_css( $css ) {
443 $css .= $this->get_module_custom_css();
444
445 return $css;
446 }
447
448 /**
449 * Frontend custom CSS.
450 *
451 * @param string $css The custom CSS.
452 *
453 * @return string $css The custom CSS.
454 */
455 public function frontend_custom_css( $css ) {
456 // Append module custom CSS.
457 $css .= $this->get_module_custom_css();
458
459 // Append module custom CSS based on themes (ensure themes compatibility).
460 // Detect the current theme.
461 $theme = wp_get_theme();
462 $theme_name = $theme->get( 'Name' );
463
464 // All themes.
465 $css .= '
466 .woocommerce .onsale,
467 .wc-block-grid__product-onsale,
468 .wc-block-grid__product .onsale {
469 display: none !important;
470 }
471 ';
472
473 // Kadence
474 if ( 'Kadence' === $theme_name ) {
475 $css .= '
476 .merchant_product-labels-grid_item_html a,
477 .merchant_product-labels-grid_item_html .wc-block-grid__product-image,
478 .merchant_product-labels-grid_item_html .wc-block-grid__product-image img {
479 width: 100% !important;
480 }
481 ';
482 }
483
484 if ( 'Flatsome' === $theme_name || 'Flatsome Child' === $theme_name ) {
485 $css .= '
486 .type-product .col-inner {
487 overflow: hidden;
488 }
489 ';
490 }
491
492 if ( 'OceanWP' === $theme_name ) {
493 $css .= '
494 .type-product .product-inner {
495 overflow: hidden;
496 }
497 ';
498 }
499
500 if ( 'Astra' === $theme_name ) {
501 $css .= '
502 .woocommerce-js div.product div.images.woocommerce-product-gallery .flex-viewport {
503 z-index: 999;
504 }
505
506 .ast-onsale-card {
507 display: none;
508 }
509
510 .merchant-product-labels__image img {
511 box-shadow: none;
512 }
513 ';
514 }
515
516 // Ultimate addon Elementor
517 if ( defined( 'UAEL_FILE' ) ) {
518 $css .= '
519 .uael-woo-product-wrapper {
520 position: relative;
521 }
522 ';
523 }
524
525 // XStore Theme
526 if ( defined( 'ETHEME_FW' ) ) {
527 $css .= '
528 .product-content .merchant-product-labels.position-top-left {
529 margin-left: 15px !important;
530 }
531 .product-content .merchant-product-labels.position-top-right {
532 margin-right: 15px !important;
533 }
534 ';
535 }
536
537 return $css;
538 }
539
540 /**
541 * Print shortcode content.
542 *
543 * @return string
544 */
545 public function shortcode_handler() {
546 if ( ! Merchant_Modules::is_module_active( $this->module_id ) ) {
547 return '';
548 }
549
550 if ( ! $this->is_shortcode_enabled() ) {
551 return '';
552 }
553
554 if ( ! is_singular( 'product' ) ) {
555 // If user is admin, show error message.
556 if ( current_user_can( 'manage_options' ) ) {
557 return $this->shortcode_placement_error();
558 }
559
560 return '';
561 }
562
563 global $product;
564
565 ob_start();
566
567 echo wp_kses( $this->get_labels( $product, 'single' ), array(
568 'div' => array(
569 'class' => array(),
570 'style' => array(),
571 ),
572 'strong' => array(),
573 'span' => array(
574 'class' => array(),
575 'style' => array(),
576 ),
577 'img' => array(
578 'src' => true,
579 'width' => true,
580 'height' => true,
581 'class' => array(),
582 'style' => array(),
583 ),
584 ) );
585
586 $shortcode_content = ob_get_clean();
587
588 /**
589 * Filter the shortcode html content.
590 *
591 * @param string $shortcode_content shortcode html content
592 * @param string $module_id module id
593 * @param int $post_id product id
594 *
595 * @since 1.8
596 */
597 return apply_filters( 'merchant_module_shortcode_content_html', $shortcode_content, $this->module_id, get_the_ID() );
598 }
599
600 /**
601 * Single product output.
602 *
603 * @return void
604 */
605 public function single_product_output() {
606 if ( $this->is_shortcode_enabled() ) {
607 return;
608 }
609
610 global $product;
611
612 echo wp_kses( $this->get_labels( $product, 'single' ), array(
613 'div' => array(
614 'class' => array(),
615 'style' => array(),
616 ),
617 'strong' => array(),
618 'span' => array(
619 'class' => array(),
620 'style' => array(),
621 ),
622 'img' => array(
623 'src' => true,
624 'width' => true,
625 'height' => true,
626 'class' => array(),
627 'style' => array(),
628 ),
629 ) );
630 }
631
632 /**
633 * Add class to image wrapper in Product page.
634 *
635 * @param $classes
636 *
637 * @return mixed
638 */
639 public function single_product_image_gallery_classes( $classes ) {
640 $classes[] = 'merchant-product-labels-image-wrap';
641
642 return $classes;
643 }
644
645 /**
646 * Loop product output.
647 *
648 * @return void
649 */
650 public function loop_product_output() {
651 global $product;
652
653 echo wp_kses( $this->get_labels( $product, 'archive' ), array(
654 'div' => array(
655 'class' => array(),
656 'style' => array(),
657 ),
658 'strong' => array(),
659 'span' => array(
660 'class' => array(),
661 'style' => array(),
662 ),
663 'img' => array(
664 'src' => true,
665 'width' => true,
666 'height' => true,
667 'class' => array(),
668 'style' => array(),
669 ),
670 ) );
671 }
672
673 /**
674 * Get module settings with defaults
675 *
676 * @return array Module settings
677 */
678 private function get_normalized_settings() {
679 $settings = $this->get_module_settings();
680
681 return array(
682 'use_shortcode' => $settings['use_shortcode'] ?? false,
683 'multiple_labels' => $settings['multiple_labels'] ?? false,
684 'labels' => $settings['labels'] ?? array(),
685 );
686 }
687
688 /**
689 * Check if label should be displayed based on status
690 *
691 * @param array $label Label configuration.
692 *
693 * @return bool True if label is active
694 */
695 private function is_label_active( $label ) {
696 return ! isset( $label['campaign_status'] ) || $label['campaign_status'] !== 'inactive';
697 }
698
699 /**
700 * Normalize label display settings with defaults
701 *
702 * @param array $label Label configuration.
703 *
704 * @return array Normalized label configuration
705 */
706 private function normalize_label_display_settings( $label ) {
707 if ( ! isset( $label['show_pages'] ) ) {
708 $label['show_pages'] = array( 'homepage', 'single', 'archive' );
709 }
710
711 if ( ! isset( $label['show_devices'] ) ) {
712 $label['show_devices'] = array( 'desktop', 'mobile' );
713 }
714
715 return $label;
716 }
717
718 /**
719 * Get taxonomy mapping for display rules
720 *
721 * @return array Taxonomy map with taxonomy and slug keys
722 */
723 private function get_taxonomy_map() {
724 return array(
725 'by_category' => array(
726 'taxonomy' => 'product_cat',
727 'key' => 'product_cats',
728 ),
729 'by_tags' => array(
730 'taxonomy' => 'product_tag',
731 'key' => 'product_tags',
732 ),
733 'by_brands' => array(
734 'taxonomy' => 'product_brand',
735 'key' => 'product_brands',
736 ),
737 );
738 }
739
740 /**
741 * Check if product has taxonomy term
742 *
743 * @param WC_Product $product Product object.
744 * @param array $label Label configuration.
745 * @param string $display_rule Display rule type.
746 *
747 * @return bool True if product has matching term
748 */
749 private function product_has_taxonomy_term( $product, $label, $display_rule ) {
750 $taxonomy_map = $this->get_taxonomy_map();
751
752 if ( ! isset( $taxonomy_map[ $display_rule ] ) ) {
753 return false;
754 }
755
756 $taxonomy = $taxonomy_map[ $display_rule ]['taxonomy'];
757 $key = $taxonomy_map[ $display_rule ]['key'];
758 $slugs = $label[ $key ] ?? array();
759
760 foreach ( $slugs as $slug ) {
761 if ( has_term( $slug, $taxonomy, $product->get_id() ) ) {
762 return true;
763 }
764 }
765
766 return false;
767 }
768
769 /**
770 * Check if product matches specific products rule
771 *
772 * @param WC_Product $product Product object.
773 * @param array $label Label configuration.
774 *
775 * @return bool True if product ID matches
776 */
777 private function product_matches_specific_products( $product, $label ) {
778 $product_ids = $label['product_ids'] ?? array();
779 $product_ids = merchant_parse_product_ids( $product_ids );
780
781 return in_array( $product->get_id(), $product_ids, true );
782 }
783
784 /**
785 * Check if product should display label based on display rule
786 *
787 * @param WC_Product $product Product object.
788 * @param array $label Label configuration.
789 *
790 * @return bool True if product matches display rule
791 */
792 private function product_matches_display_rule( $product, $label ) {
793 $display_rule = $label['display_rules'] ?? 'products_on_sale';
794
795 switch ( $display_rule ) {
796 case 'featured_products':
797 return $this->is_featured( $product );
798
799 case 'products_on_sale':
800 return $this->is_on_sale( $product );
801
802 case 'by_category':
803 case 'by_tags':
804 case 'by_brands':
805 return $this->product_has_taxonomy_term( $product, $label, $display_rule );
806
807 case 'out_of_stock':
808 return $this->is_out_of_stock( $product );
809
810 case 'pre-order':
811 return $this->is_pre_order( $product );
812
813 case 'new_products':
814 if ( isset( $label['new_products_days'] ) ) {
815 return $this->is_new( $product, $label['new_products_days'] );
816 }
817 return false;
818
819 case 'specific_products':
820 return $this->product_matches_specific_products( $product, $label );
821
822 case 'all_products':
823 return true;
824
825 default:
826 return false;
827 }
828 }
829
830 /**
831 * Get product sale data for label replacements
832 *
833 * @param WC_Product $product Product object.
834 * @param array $label Label configuration.
835 *
836 * @return array Sale data with amount and percentage
837 */
838 private function get_sale_replacement_data( $product, $label ) {
839 if ( ! $this->is_on_sale( $product ) ) {
840 return array(
841 'amount' => '',
842 'percentage' => '',
843 );
844 }
845
846 $sale_data = $this->get_product_sale_data( $product, $label );
847 if ( ! is_array( $sale_data ) ) {
848 $sale_data = array( 'amount' => 0, 'percentage' => 0 );
849 }
850
851 $amount = isset( $sale_data['amount'] ) && $sale_data['amount'] > 0
852 ? wc_price( $sale_data['amount'] )
853 : '';
854
855 $percentage = isset( $sale_data['percentage'] ) && $sale_data['percentage'] > 0
856 ? $sale_data['percentage'] . '%'
857 : '';
858
859 return array(
860 'amount' => $amount,
861 'percentage' => $percentage,
862 );
863 }
864
865 /**
866 * Get product inventory data for label replacements
867 *
868 * @param WC_Product $product Product object.
869 *
870 * @return array Inventory data with status and quantity
871 */
872 private function get_inventory_replacement_data( $product ) {
873 $status = $product->is_in_stock() ? esc_html__( 'In stock', 'merchant' ) : esc_html__( 'Sold out', 'merchant' );
874 $quantity = $product->get_stock_quantity();
875
876 return array(
877 'status' => $status,
878 'quantity' => $quantity,
879 );
880 }
881
882 /**
883 * Replace shortcodes in label HTML with product data
884 *
885 * @param string $label_html Label HTML with shortcodes.
886 * @param WC_Product $product Product object.
887 * @param array $label Label configuration.
888 *
889 * @return string Label HTML with replaced values
890 */
891 private function replace_label_shortcodes( $label_html, $product, $label ) {
892 $sale_data = $this->get_sale_replacement_data( $product, $label );
893 $inventory_data = $this->get_inventory_replacement_data( $product );
894
895 return str_replace(
896 array(
897 '{sale}',
898 '{sale_amount}',
899 '{inventory}',
900 '{inventory_quantity}',
901 ),
902 array(
903 $sale_data['percentage'],
904 $sale_data['amount'],
905 $inventory_data['status'],
906 $inventory_data['quantity'],
907 ),
908 $label_html
909 );
910 }
911
912 /**
913 * Process single label and return label data
914 *
915 * @param array $label Label configuration.
916 * @param WC_Product $product Product object.
917 * @param string $context Display context.
918 *
919 * @return array|null Label data array or null if not displayed
920 */
921 private function process_single_label( $label, $product, $context ) {
922 // Check if label is active
923 if ( ! $this->is_label_active( $label ) ) {
924 return null;
925 }
926
927 // Normalize display settings
928 $label = $this->normalize_label_display_settings( $label );
929
930 // Check if label should show in this context
931 if ( ! $this->show_label( $label, $context ) ) {
932 return null;
933 }
934
935 // Check if product is excluded
936 if ( $this->is_product_excluded( $product->get_id(), $label ) ) {
937 return null;
938 }
939
940 // Check if product matches display rule
941 if ( ! $this->product_matches_display_rule( $product, $label ) ) {
942 return null;
943 }
944
945 // Get and process label HTML
946 $label_html = $this->label( $label );
947 $label_html = $this->replace_label_shortcodes( $label_html, $product, $label );
948
949 return array(
950 'label' => $label,
951 'html' => $label_html,
952 );
953 }
954
955 /**
956 * Collect all matching labels for product
957 *
958 * @param array $labels All labels configuration.
959 * @param WC_Product $product Product object.
960 * @param string $context Display context.
961 * @param bool $multiple_labels Whether to collect multiple labels.
962 *
963 * @return array Array of matching label data
964 */
965 private function collect_matching_labels( $labels, $product, $context, $multiple_labels ) {
966 $matching_labels = array();
967
968 foreach ( $labels as $label ) {
969 $label_data = $this->process_single_label( $label, $product, $context );
970
971 if ( $label_data ) {
972 $matching_labels[] = $label_data;
973
974 // Stop after first match if multiple labels disabled
975 if ( ! $multiple_labels ) {
976 break;
977 }
978 }
979 }
980
981 return $matching_labels;
982 }
983
984 /**
985 * Get device visibility CSS classes
986 *
987 * @param array $label Label configuration.
988 *
989 * @return string CSS classes for device visibility
990 */
991 private function get_device_visibility_classes( $label ) {
992 if ( ! isset( $label['show_devices'] ) ) {
993 return '';
994 }
995
996 $classes = '';
997 foreach ( $label['show_devices'] as $device ) {
998 $classes .= ' show-on-' . $device;
999 }
1000
1001 return $classes;
1002 }
1003
1004 /**
1005 * Get label shape CSS class
1006 *
1007 * @param array $label Label configuration.
1008 *
1009 * @return string CSS class for label shape
1010 */
1011 private function get_label_shape_class( $label ) {
1012 $label_type = $label['label_type'] ?? 'text';
1013
1014 if ( $label_type !== 'text' ) {
1015 return '';
1016 }
1017
1018 $shape = $label['label_text_shape'] ?? 'text-shape-1';
1019 return ' merchant-product-labels__' . $shape;
1020 }
1021
1022 /**
1023 * Get label wrapper CSS classes
1024 *
1025 * @param array $label Label configuration.
1026 *
1027 * @return string CSS classes for label wrapper
1028 */
1029 private function get_label_wrapper_classes( $label ) {
1030 $label_type = $label['label_type'] ?? 'text';
1031 $position = $label['position_anchor'] ?? 'top-left';
1032
1033 $classes = 'merchant-product-labels__label-wrapper';
1034 $classes .= ' merchant-product-labels__' . $label_type;
1035 $classes .= $this->get_label_shape_class( $label );
1036 $classes .= $position === 'top-right' ? ' position-top-right' : ' position-top-left';
1037
1038 return $classes;
1039 }
1040
1041 /**
1042 * Render single label HTML
1043 *
1044 * @param array $label_data Label data with configuration and HTML.
1045 *
1046 * @return string Rendered label HTML
1047 */
1048 private function render_single_label( $label_data ) {
1049 $label = $label_data['label'];
1050 $label_html = $label_data['html'];
1051 $label_styles = $this->get_shape_based_styles( $label );
1052 $label_classes = $this->get_label_wrapper_classes( $label );
1053
1054 return sprintf(
1055 '<div class="%s" style="%s">%s</div>',
1056 esc_attr( $label_classes ),
1057 merchant_array_to_css( $label_styles ),
1058 $label_html
1059 );
1060 }
1061
1062 /**
1063 * Get main container CSS classes
1064 *
1065 * @param bool $is_shortcode Whether using shortcode.
1066 * @param string $context Display context.
1067 * @param string $device_classes Device visibility classes.
1068 *
1069 * @return string CSS classes for main container
1070 */
1071 private function get_main_container_classes( $is_shortcode, $context, $device_classes ) {
1072 $classes = 'merchant-product-labels';
1073 $classes .= ' merchant-product-labels__' . ( ( $is_shortcode && $context === 'single' ) ? 'shortcode' : 'regular' );
1074 $classes .= ' merchant-product-labels--multiple';
1075 $classes .= $device_classes;
1076
1077 return $classes;
1078 }
1079
1080 /**
1081 * Render all matching labels
1082 *
1083 * @param array $matching_labels Array of matching label data.
1084 * @param bool $is_shortcode Whether using shortcode.
1085 * @param string $context Display context.
1086 *
1087 * @return string Rendered labels HTML
1088 */
1089 private function render_labels_output( $matching_labels, $is_shortcode, $context ) {
1090 if ( empty( $matching_labels ) ) {
1091 return '';
1092 }
1093
1094 $output = '';
1095 $device_classes = '';
1096
1097 foreach ( $matching_labels as $label_data ) {
1098 // Get device visibility classes from first label
1099 if ( empty( $device_classes ) ) {
1100 $device_classes = $this->get_device_visibility_classes( $label_data['label'] );
1101 }
1102
1103 $output .= $this->render_single_label( $label_data );
1104 }
1105
1106 $main_classes = $this->get_main_container_classes( $is_shortcode, $context, $device_classes );
1107
1108 return sprintf(
1109 '<div class="%s">%s</div>',
1110 esc_attr( $main_classes ),
1111 $output
1112 );
1113 }
1114
1115 /**
1116 * Get labels group.
1117 *
1118 * @param WC_Product $product Product object.
1119 * @param string $context Context archive or single.
1120 *
1121 * @return string Product labels HTML.
1122 */
1123 public function get_labels( $product, $context = 'both' ) {
1124 $settings = $this->get_normalized_settings();
1125
1126 // Handle legacy mode
1127 if ( empty( $settings['labels'] ) ) {
1128 return $this->legacy_product_label( $product );
1129 }
1130
1131 // Collect matching labels
1132 $matching_labels = $this->collect_matching_labels(
1133 $settings['labels'],
1134 $product,
1135 $context,
1136 $settings['multiple_labels']
1137 );
1138
1139 // Render and return output
1140 return $this->render_labels_output(
1141 $matching_labels,
1142 $settings['use_shortcode'],
1143 $context
1144 );
1145 }
1146
1147 /**
1148 * Get all styles for the current label
1149 *
1150 * @param $label
1151 *
1152 * @return array
1153 */
1154 public function get_shape_based_styles( $label ) {
1155 $styles = array();
1156
1157 if ( ! is_array( $label ) || empty( $label ) ) {
1158 return $styles;
1159 }
1160
1161 $label_type = $label['label_type'] ?? 'text';
1162 $label_shape = $label['label_text_shape'] ?? 'text-shape-1';
1163
1164 $styles['margin'] = 0;
1165 $styles['padding'] = 0;
1166
1167 if ( $label_type === 'text' ) {
1168 $styles['width'] = ( $label['label_width'] ?? 100 ) . 'px';
1169 $styles['height'] = ( $label['label_height'] ?? 32 ) . 'px';
1170 $styles['font-size'] = ( $label['font_size'] ?? 14 ) . 'px';
1171
1172 $font_style = $label['font_style'] ?? 'normal';
1173
1174 if ( strpos( $font_style, 'bold' ) !== false ) {
1175 $styles['font-weight'] = 'bold';
1176 }
1177
1178 if ( strpos( $font_style, 'italic' ) !== false ) {
1179 $styles['font-style'] = 'italic';
1180 }
1181
1182 $styles['background-color'] = $label['background_color'] ?? '#212121';
1183 $styles['color'] = $label['text_color'] ?? '#ffffff';
1184 $styles['border-radius'] = ( $label['shape_radius'] ?? 5 ) . 'px';
1185 }
1186
1187 // Always use absolute positioning
1188 $x = $label['position_x'] ?? 10;
1189 $y = $label['position_y'] ?? 10;
1190 $x_unit = $label['position_x_unit'] ?? 'px';
1191 $anchor = $label['position_anchor'] ?? 'top-left';
1192
1193 $styles['top'] = $y . 'px';
1194
1195 // Handle anchor point - use left or right based on anchor
1196 if ( $anchor === 'top-right' ) {
1197 $styles['right'] = $x . $x_unit;
1198 } else {
1199 // top-left (default)
1200 $styles['left'] = $x . $x_unit;
1201 }
1202
1203 // Apply rotation for shapes 5 and 6 based on anchor point
1204 if ( $label_type === 'text' && in_array( $label_shape, array( 'text-shape-5', 'text-shape-6' ), true ) ) {
1205 $is_top_right = ( $anchor === 'top-right' );
1206
1207 if ( $label_shape === 'text-shape-5' ) {
1208 $rotation = $is_top_right ? '45deg' : '-45deg';
1209 $translateX = $is_top_right ? '50%' : '-50%';
1210 $translateY = '50%';
1211 $origin = $is_top_right ? 'right' : 'left';
1212 } else { // text-shape-6
1213 $rotation = $is_top_right ? '45deg' : '-45deg';
1214 $translateX = $is_top_right ? '50%' : '-50%';
1215 $translateY = '25%';
1216 $origin = $is_top_right ? 'right' : 'left';
1217 }
1218
1219 $styles['transform'] = "rotate({$rotation}) translate({$translateX}, {$translateY})";
1220 $styles['transform-origin'] = $origin;
1221 }
1222
1223
1224 return $styles;
1225 }
1226
1227 /**
1228 * Should show the label for current page.
1229 *
1230 * @param $label
1231 *
1232 * @return bool
1233 */
1234 public function show_label( $label, $context = '' ) {
1235 $show = false;
1236 $show_pages = $label['show_pages' ] ?? array();
1237
1238 if ( empty( $show_pages ) ) {
1239 return $show;
1240 }
1241
1242 if ( is_product() && in_array( 'single', $show_pages, true ) ) {
1243 $show = true;
1244 }
1245
1246 if ( in_array( 'archive', $show_pages, true ) && ( is_product_taxonomy() || is_shop() ) ) {
1247 $show = true;
1248 }
1249
1250 // Block
1251 if ( in_array( 'archive', $show_pages, true ) && $context === 'archive' ) {
1252 $show = true;
1253
1254 // Don't show for blocks on homepage if `homepage` is unchecked
1255 if ( ! in_array( 'homepage', $show_pages, true ) && is_front_page() ) {
1256 $show = false;
1257 }
1258 }
1259
1260 if ( in_array( 'homepage', $show_pages, true ) && is_front_page() ) {
1261 $show = true;
1262 }
1263
1264 return $show;
1265 }
1266
1267 /**
1268 * Product label output.
1269 *
1270 * @return string legacy product label html.
1271 */
1272 private function legacy_product_label( $product ) {
1273 if ( $this->is_shortcode_enabled() ) {
1274 return '';
1275 }
1276
1277 global $product;
1278 $settings = $this->get_module_settings();
1279 $styles = array();
1280
1281 if ( ! empty( $product ) && $this->is_on_sale( $product ) ) {
1282 $label_text = $settings['label_text'];
1283 if ( ! empty( $settings['display_percentage'] ) ) {
1284 if ( $product->is_type( 'variable' ) ) {
1285 $percentages = array();
1286 $prices = $product->get_variation_prices();
1287
1288 foreach ( $prices['price'] as $key => $price ) {
1289 if ( $prices['regular_price'][ $key ] !== $price ) {
1290 $percentages[] = round( 100 - ( floatval( $prices['sale_price'][ $key ] ) / floatval( $prices['regular_price'][ $key ] ) * 100 ) );
1291 }
1292 }
1293
1294 $percentage = max( $percentages );
1295 } elseif ( $product->is_type( 'grouped' ) ) {
1296 $percentages = array();
1297 $children_ids = $product->get_children();
1298
1299 foreach ( $children_ids as $child_id ) {
1300 $child_product = wc_get_product( $child_id );
1301 $regular_price = (float) $child_product->get_regular_price();
1302 $sale_price = (float) $child_product->get_sale_price();
1303
1304 if ( 0 !== $sale_price || ! empty( $sale_price ) ) {
1305 $percentages[] = round( 100 - ( ( $sale_price / $regular_price ) * 100 ) );
1306 }
1307 }
1308 $percentage = max( $percentages );
1309 } else {
1310 $regular_price = (float) $product->get_regular_price();
1311 $sale_price = (float) $product->get_sale_price();
1312
1313 if ( 0 !== $sale_price || ! empty( $sale_price ) ) {
1314 $percentage = round( 100 - ( ( $sale_price / $regular_price ) * 100 ) );
1315 }
1316 }
1317
1318 $label_text = str_replace( '{value}', $percentage, Merchant_Translator::translate( $settings['percentage_text'] ) );
1319 }
1320
1321 $styles['background-color'] = isset( $settings['background_color'] ) ? $settings['background_color'] : '#212121';
1322 $styles['color'] = isset( $settings['text_color'] ) ? $settings['text_color'] : '#ffffff';
1323 $styles['border-radius'] = isset( $settings['label_shape'] ) ? $settings['label_shape'] . 'px' : 8 . 'px';
1324
1325 return '<div class="merchant-product-labels merchant-product-labels__regular position-' . esc_attr( $settings['label_position'] ) . '"><span class="merchant-label merchant-label-'
1326 . esc_attr( $settings['label_position'] ) . ' merchant-onsale-shape-' . esc_attr( $settings['label_shape'] ) . '" style="' . merchant_array_to_css( $styles )
1327 . '">' . esc_html( Merchant_Translator::translate( $label_text ) ) . '</span></div>';
1328 }
1329
1330 return '';
1331 }
1332
1333 /**
1334 * Get label HTML.
1335 *
1336 * @return string
1337 */
1338 public function label( $label_data ) {
1339 $html = '';
1340 if ( empty( $label_data['label'] ) ) {
1341 return $html;
1342 }
1343
1344 $label_position = $label_data['label_position'] ?? 'top-left';
1345
1346 $label_type = $label_data['label_type'] ?? 'text';
1347
1348 $styles = array();
1349
1350 if ( $label_type === 'text' ) {
1351 $html = trim( esc_html( Merchant_Translator::translate( $label_data['label'] ) ) );
1352 } else {
1353 $styles['width'] = ( $label_data['label_width'] ?? 45 ) . 'px';
1354 $styles['height'] = ( $label_data['label_height'] ?? 45 ) . 'px';
1355
1356 $custom_shape_id = $label_data['label_image_shape_custom'] ?? false;
1357 $shape_url = $custom_shape_id ? wp_get_attachment_url( $custom_shape_id ) : MERCHANT_URI . 'assets/images/icons/' . self::MODULE_ID . '/' . ( $label_data['label_image_shape'] ?? 'image-shape-1' ) .'.svg';
1358
1359 $html = '<img src="' . esc_url( $shape_url ) . '"/>';
1360 }
1361
1362
1363 $label = '<span class="merchant-label merchant-label-' . esc_attr( $label_position ) . '" style="' . merchant_array_to_css( $styles ) . '">'
1364 . $html . '</span>';
1365
1366 /**
1367 * Filter the single product label.
1368 *
1369 * @param string $label The label HTML.
1370 * @param array $label_data The label data.
1371 *
1372 * @since 1.6
1373 */
1374 return apply_filters( 'merchant_product_label', $label, $label_data );
1375 }
1376
1377 /**
1378 * Get label data.
1379 *
1380 * @param $product WC_Product product object.
1381 *
1382 * @return bool
1383 */
1384 private function is_on_sale( $product ) {
1385 return $product !== null && $product->is_on_sale();
1386 }
1387
1388 /**
1389 * Check if product is in categories.
1390 *
1391 * @param $product WC_Product product object.
1392 * @param $slugs array category slugs.
1393 *
1394 * @return bool
1395 */
1396 private function is_in_category( $product, $slugs ) {
1397 if ( ! is_array( $slugs ) ) {
1398 $slugs = array( $slugs );
1399 }
1400
1401 $terms = get_the_terms( $product->get_id(), 'product_cat' );
1402 if ( $terms && ! is_wp_error( $terms ) ) {
1403 foreach ( $terms as $term ) {
1404 if ( in_array( $term->slug, $slugs, true ) ) {
1405 return true;
1406 }
1407 }
1408 }
1409
1410 return false;
1411 }
1412
1413 /**
1414 * Check if product is out of stock.
1415 *
1416 * @param $product WC_Product product object.
1417 *
1418 * @return bool
1419 */
1420 private function is_out_of_stock( $product ) {
1421 return $product !== null && ! $product->is_in_stock();
1422 }
1423
1424 /**
1425 * Check if product is featured.
1426 *
1427 * @param $product WC_Product product object.
1428 *
1429 * @return bool
1430 */
1431 private function is_featured( $product ) {
1432 return $product !== null && $product->is_featured();
1433 }
1434
1435 /**
1436 * Check if the product is new.
1437 *
1438 * @param $product WC_Product product object.
1439 * @param $days number of days to check.
1440 *
1441 * @return bool
1442 */
1443 private function is_new( $product, $days ) {
1444 $product_creation_date = get_the_date( 'Y-m-d', $product->get_id() );
1445
1446 $current_date = gmdate( 'Y-m-d' );
1447 $days_difference = abs( strtotime( $current_date ) - strtotime( $product_creation_date ) ) / DAY_IN_SECONDS;
1448
1449 return $days_difference <= $days;
1450 }
1451
1452 /**
1453 * Check if product is a pre-order item.
1454 *
1455 * @param $product
1456 *
1457 * @return bool
1458 */
1459 public function is_pre_order( $product ) {
1460 if ( ! is_object( $product ) ) {
1461 return false;
1462 }
1463
1464 if ( class_exists( 'Merchant_Pre_Orders' ) && Merchant_Modules::is_module_active( Merchant_Pre_Orders::MODULE_ID ) && class_exists( 'Merchant_Pre_Orders_Main_Functionality' ) ) {
1465 $available_pre_order = Merchant_Pre_Orders_Main_Functionality::available_product_rule( $product->get_id() );
1466
1467 if ( ! empty( $available_pre_order ) ) {
1468 return true; // Pre-order available
1469 }
1470
1471 // If Pre-order Not available for variable, check if available for any of its variation
1472 if ( $product->is_type( 'variable' ) ) {
1473 $variations = $product->get_available_variations();
1474
1475 foreach ( $variations as $variation ) {
1476 $variation_id = $variation['variation_id'];
1477
1478 // Check if the variation is available for pre-order
1479 $available_pre_order = Merchant_Pre_Orders_Main_Functionality::available_product_rule( $variation_id );
1480
1481 if ( ! empty( $available_pre_order ) ) {
1482 return true; // Pre-order available for variation
1483 }
1484 }
1485 }
1486 }
1487
1488 return false;
1489 }
1490
1491 /**
1492 * Migrate Exclusion list which was introduced later for the Excluded products.
1493 *
1494 * By default, it's off. So turn it off if condition matches.
1495 *
1496 * @return void
1497 */
1498 private function migrate_exclusion_list() {
1499 $option = 'merchant_' . $this->module_id .'_exclusion_list';
1500
1501 if ( get_option( $option, false ) || ! method_exists( 'Merchant_Admin_Options', 'set' ) ) {
1502 return;
1503 }
1504
1505 $labels = Merchant_Admin_Options::get( $this->module_id, 'labels', array() );
1506 if ( ! empty( $labels ) ) {
1507 $update = false;
1508 foreach ( $labels as $key => $offer ) {
1509 $excluded_products = $offer['excluded_products'] ?? '';
1510 $excluded_categories = $offer['excluded_categories'] ?? array();
1511 $excluded_tags = $offer['excluded_tags'] ?? array();
1512 $excluded_brands = $offer['excluded_brands'] ?? array();
1513
1514 if ( ! empty( $excluded_products ) || ! empty( $excluded_categories ) || ! empty( $excluded_tags ) || ! empty( $excluded_brands ) ) {
1515 $labels[ $key ]['exclusion_enabled'] = true;
1516 $update = true;
1517 }
1518 }
1519
1520 // Update only if necessary.
1521 if ( $update ) {
1522 Merchant_Admin_Options::set( $this->module_id, 'labels', $labels );
1523 }
1524
1525 update_option( $option, true, false );
1526 }
1527 }
1528
1529 /**
1530 * Check if product is excluded based on label exclusion settings
1531 *
1532 * @param int $product_id Product ID
1533 * @param array $label Label settings
1534 * @return bool
1535 * @since 2.1.8
1536 */
1537 private function is_product_excluded( $product_id, $label ) {
1538 $display_rule = $label['display_rules'] ?? 'products_on_sale';
1539 $product = wc_get_product( $product_id );
1540 $_product_id = $product && $product->is_type( 'variation' ) ? $product->get_parent_id() : $product_id;
1541
1542 // Exclude products
1543 $exclude_products = ! empty( $label['exclude_products_toggle'] );
1544 if ( $exclude_products ) {
1545 $excluded_product_ids = $label['excluded_products'] ?? array();
1546 $excluded_product_ids = merchant_parse_product_ids( $excluded_product_ids );
1547
1548 if ( in_array( (int) $product_id, $excluded_product_ids, true ) || in_array( (int) $_product_id, $excluded_product_ids, true ) ) {
1549 return true;
1550 }
1551 }
1552
1553 // Exclude categories (only when not targeting specific categories)
1554 $exclude_categories = ! empty( $label['exclude_categories_toggle'] );
1555 if ( $exclude_categories && $display_rule !== 'by_category' ) {
1556 $excluded_categories_slugs = $label['excluded_categories'] ?? array();
1557
1558 if ( ! empty( $excluded_categories_slugs ) && has_term( $excluded_categories_slugs, 'product_cat', $_product_id ) ) {
1559 return true;
1560 }
1561 }
1562
1563 // Exclude tags (only when not targeting specific tags)
1564 $exclude_tags = ! empty( $label['exclude_tags_toggle'] );
1565 if ( $exclude_tags && $display_rule !== 'by_tags' ) {
1566 $excluded_tags_slugs = $label['excluded_tags'] ?? array();
1567
1568 if ( ! empty( $excluded_tags_slugs ) && has_term( $excluded_tags_slugs, 'product_tag', $_product_id ) ) {
1569 return true;
1570 }
1571 }
1572
1573 // Exclude brands (only when not targeting specific brands)
1574 $exclude_brands = ! empty( $label['exclude_brands_toggle'] );
1575 if ( $exclude_brands && $display_rule !== 'by_brands' ) {
1576 $excluded_brands_slugs = $label['excluded_brands'] ?? array();
1577
1578 if ( ! empty( $excluded_brands_slugs ) && has_term( $excluded_brands_slugs, 'product_brand', $_product_id ) ) {
1579 return true;
1580 }
1581 }
1582
1583 return false;
1584 }
1585
1586 /**
1587 * Migrate exclusion fields for backward compatibility
1588 *
1589 * @since 2.1.8
1590 */
1591 private function migrate_exclusion_fields() {
1592 $option_name = 'merchant_' . $this->module_id . '_ex_fields';
1593 if ( ! get_option( $option_name, false ) ) {
1594 $labels = Merchant_Admin_Options::get( self::MODULE_ID, 'labels', array() );
1595 if ( empty( $labels ) ) {
1596 // No labels to migrate, so we can skip this.
1597 update_option( $option_name, true, false );
1598
1599 return;
1600 }
1601 $needs_migration = false;
1602 $updated_labels = array();
1603 foreach ( $labels as $label ) {
1604 $n = 0;
1605 // Check if individual toggles don't exist yet
1606 if ( ! isset( $label['exclude_products_toggle'] ) ) {
1607 $label['exclude_products_toggle'] = ! empty( $label['excluded_products'] );
1608 ++ $n;
1609 }
1610
1611 if ( ! isset( $label['exclude_categories_toggle'] ) ) {
1612 $label['exclude_categories_toggle'] = ! empty( $label['excluded_categories'] );
1613 ++ $n;
1614 }
1615
1616 if ( ! isset( $label['exclude_tags_toggle'] ) ) {
1617 $label['exclude_tags_toggle'] = ! empty( $label['excluded_tags'] );
1618 ++ $n;
1619 }
1620
1621 if ( ! isset( $label['exclude_brands_toggle'] ) ) {
1622 $label['exclude_brands_toggle'] = ! empty( $label['excluded_brands'] );
1623 ++ $n;
1624 }
1625 if ( $n > 0 ) {
1626 $needs_migration = true; // Set to true if any iteration needs migration
1627 }
1628
1629 $updated_labels[] = $label;
1630 }
1631
1632 if ( $needs_migration ) {
1633 Merchant_Admin_Options::set( self::MODULE_ID, 'labels', $updated_labels );
1634 }
1635 // We set autoload to false because this is a flag option only and needed once.
1636 update_option( $option_name, true, false );
1637 }
1638 }
1639 }
1640
1641 // Initialize the module.
1642 add_action( 'init', function () {
1643 Merchant_Modules::create_module( new Merchant_Product_Labels() );
1644 } );
1645