PluginProbe
Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant / 2.3.1
Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant v2.3.1
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.3.1, at inc/modules/product-labels/class-product-labels.php

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