File "fbproduct.php"

Full Path: /home/vantageo/public_html/cache/.wp-cli/wp-content/plugins/facebook-for-woocommerce/includes/fbproduct.php
File size: 32.93 KB
MIME-type: text/x-php
Charset: utf-8

<?php
// phpcs:ignoreFile
/**
 * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @package FacebookCommerce
 */

require_once __DIR__ . '/fbutils.php';

use WooCommerce\Facebook\Framework\Helper;
use WooCommerce\Facebook\Products;

defined( 'ABSPATH' ) || exit;

/**
 * Custom FB Product proxy class
 */
class WC_Facebook_Product {
	// Used for the background sync
	const PRODUCT_PREP_TYPE_ITEMS_BATCH = 'items_batch';
	// Used for the background feed upload
	const PRODUCT_PREP_TYPE_FEED = 'feed';
	// Used for direct update and create calls
	const PRODUCT_PREP_TYPE_NORMAL = 'normal';

	// Should match facebook-commerce.php while we migrate that code over
	// to this object.
	const FB_PRODUCT_DESCRIPTION = 'fb_product_description';
	const FB_PRODUCT_PRICE       = 'fb_product_price';
	const FB_PRODUCT_IMAGE       = 'fb_product_image';
	const FB_VARIANT_IMAGE       = 'fb_image';
	const FB_VISIBILITY          = 'fb_visibility';
	const FB_REMOVE_FROM_SYNC    = 'fb_remove_from_sync';

	const MIN_DATE_1 = '1970-01-29';
	const MIN_DATE_2 = '1970-01-30';
	const MAX_DATE   = '2038-01-17';
	const MAX_TIME   = 'T23:59+00:00';
	const MIN_TIME   = 'T00:00+00:00';

	static $use_checkout_url = array(
		'simple'    => 1,
		'variable'  => 1,
		'variation' => 1,
	);

	/**
	 * @var int WC_Product ID.
	 */
	public $id;

	/**
	 * @var WC_Product
	 */
	public $woo_product;

	/**
	 * @var string Facebook Product Description.
	 */
	private $fb_description;

	/**
	 * @var array Gallery URLs.
	 */
	private $gallery_urls;

	/**
	 * @var bool Use parent image for variable products.
	 */
	private $fb_use_parent_image;

	/**
	 * @var string Product Description.
	 */
	private $main_description;

	/**
	 * @var bool  Sync short description.
	 */
	private $sync_short_description;

	/**
	 * @var bool Product visibility on Facebook.
	 */
	public $fb_visibility;

	public function __construct( $wpid, $parent_product = null ) {

		if ( $wpid instanceof WC_Product ) {
			$this->id          = $wpid->get_id();
			$this->woo_product = $wpid;
		} else {
			$this->id                     = $wpid;
			$this->woo_product            = wc_get_product( $wpid );
		}

		$this->fb_description         = '';
		$this->gallery_urls           = null;
		$this->fb_use_parent_image    = null;
		$this->main_description       = '';
		$this->sync_short_description = \WC_Facebookcommerce_Integration::PRODUCT_DESCRIPTION_MODE_SHORT === facebook_for_woocommerce()->get_integration()->get_product_description_mode();

		if ( $meta = get_post_meta( $this->id, self::FB_VISIBILITY, true ) ) {
			$this->fb_visibility = wc_string_to_bool( $meta );
		} else {
			$this->fb_visibility = '';
			// for products that haven't synced yet
		}

		// Variable products should use some data from the parent_product
		// For performance reasons, that data shouldn't be regenerated every time.
		if ( $parent_product ) {
			$this->gallery_urls        = $parent_product->get_gallery_urls();
			$this->fb_use_parent_image = $parent_product->get_use_parent_image();
			$this->main_description    = $parent_product->get_fb_description();
		}
	}

	/**
	 * __get method for backward compatibility.
	 *
	 * @param string $key property name
	 * @return mixed
	 * @since 3.0.32
	 */
	public function __get( $key ) {
		// Add warning for private properties.
		if ( in_array( $key, array( 'fb_description', 'gallery_urls', 'fb_use_parent_image', 'main_description', 'sync_short_description' ), true ) ) {
			/* translators: %s property name. */
			_doing_it_wrong( __FUNCTION__, sprintf( esc_html__( 'The %s property is private and should not be accessed outside its class.', 'facebook-for-woocommerce' ), esc_html( $key ) ), '3.0.32' );
			return $this->$key;
		}

		return null;
	}

	public function exists() {
		return ( $this->woo_product !== null && $this->woo_product !== false );
	}

	// Fall back to calling method on $woo_product
	public function __call( $function, $args ) {
		if ( $this->woo_product ) {
			return call_user_func_array( array( $this->woo_product, $function ), $args );
		} else {
			$e         = new Exception();
			$backtrace = var_export( $e->getTraceAsString(), true );
			WC_Facebookcommerce_Utils::fblog(
				"Calling $function on Null Woo Object. Trace:\n" . $backtrace,
				array(),
				true
			);
			return null;
		}
	}

	public function get_gallery_urls() {
		if ( $this->gallery_urls === null ) {
			if ( is_callable( array( $this, 'get_gallery_image_ids' ) ) ) {
				$image_ids = $this->get_gallery_image_ids();
			} else {
				$image_ids = $this->get_gallery_attachment_ids();
			}
			$gallery_urls = array();
			foreach ( $image_ids as $image_id ) {
				$image_url = wp_get_attachment_url( $image_id );
				if ( ! empty( $image_url ) ) {
					array_push(
						$gallery_urls,
						WC_Facebookcommerce_Utils::make_url( $image_url )
					);
				}
			}
			$this->gallery_urls = array_filter( $gallery_urls );
		}

		return $this->gallery_urls;
	}

	public function get_post_data() {
		if ( is_callable( 'get_post' ) ) {
			return get_post( $this->id );
		} else {
			return $this->get_post_data();
		}
	}

	public function get_fb_price( $for_items_batch = false ) {
		$product_price = Products::get_product_price( $this->woo_product );

		return $for_items_batch ? self::format_price_for_fb_items_batch( $product_price ) : $product_price;
	}

	private static function format_price_for_fb_items_batch( $price ) {
		// items_batch endpoint requires a string and a currency code
		$formatted = ( $price / 100.0 ) . ' ' . get_woocommerce_currency();
		return $formatted;
	}


	/**
	 * Determines whether the current product is a WooCommerce Bookings product.
	 *
	 * TODO: add an integration that filters the Facebook price instead {WV 2020-07-22}
	 *
	 * @since 2.0.0
	 *
	 * @return bool
	 */
	private function is_bookable_product() {

		return facebook_for_woocommerce()->is_plugin_active( 'woocommerce-bookings.php' ) && class_exists( 'WC_Product_Booking' ) && is_callable( 'is_wc_booking_product' ) && is_wc_booking_product( $this );
	}


	/**
	 * Gets a list of image URLs to use for this product in Facebook sync.
	 *
	 * @return array
	 */
	public function get_all_image_urls() {

		$image_urls = array();

		/**
		 * Filters the FB product image size.
		 *
		 * @since 3.0.34
		 *
		 * @param string $size The image size. e.g. 'full', 'medium', 'thumbnail'.
		 */
		$image_size               = apply_filters( 'facebook_for_woocommerce_fb_product_image_size', 'full' );
		$product_image_url        = wp_get_attachment_image_url( $this->woo_product->get_image_id(), $image_size ); ;
		$parent_product_image_url = null;
		$custom_image_url         = $this->woo_product->get_meta( self::FB_PRODUCT_IMAGE );

		if ( $this->woo_product->is_type( 'variation' ) ) {

			if ( $parent_product = wc_get_product( $this->woo_product->get_parent_id() ) ) {

				$parent_product_image_url = wp_get_attachment_image_url( $parent_product->get_image_id(), $image_size );
			}
		}

		switch ( $this->woo_product->get_meta( Products::PRODUCT_IMAGE_SOURCE_META_KEY ) ) {

			case Products::PRODUCT_IMAGE_SOURCE_CUSTOM:
				$image_urls = array( $custom_image_url, $product_image_url, $parent_product_image_url );
				break;

			case Products::PRODUCT_IMAGE_SOURCE_PARENT_PRODUCT:
				$image_urls = array( $parent_product_image_url, $product_image_url );
				break;

			case Products::PRODUCT_IMAGE_SOURCE_PRODUCT:
			default:
				$image_urls = array( $product_image_url, $parent_product_image_url );
				break;
		}

		$image_urls = array_merge( $image_urls, $this->get_gallery_urls() );
		$image_urls = array_filter( array_unique( $image_urls ) );

		// Regenerate $image_url PHP array indexes after filtering.
		// The array_filter does not touches indexes so if something gets removed we may end up with gaps.
		// Later parts of the code expect something to exist under the 0 index.
		$image_urls = array_values( $image_urls );

		if ( empty( $image_urls ) ) {
			$image_urls[] = wc_placeholder_img_src();
		}

		return $image_urls;
	}

	/**
	 * Gets a list of video URLs to use for this product in Facebook sync.
	 *
	 * @return array
	 */
	public function get_all_video_urls() {

		$video_urls = array();

		$attached_videos = get_attached_media( 'video', $this->id );
		if ( empty( $attached_videos ) ) {
			return $video_urls;
		}
		foreach ( $attached_videos as $video ) {
			$url = wp_get_attachment_url( $video->ID );
			if ( $url ) {
				array_push(
					$video_urls,
					array(
						'url' => $url,
					)
				);
			}
        }

		return $video_urls;
	}



	/**
	 * Gets the list of additional image URLs for the product from the complete list of image URLs.
	 *
	 * It assumes the first URL will be used as the product image.
	 * It returns 20 or less image URLs because Facebook doesn't allow more items on the additional_image_urls field.
	 *
	 * @since 2.0.2
	 *
	 * @param array $image_urls all image URLs for the product.
	 * @return array
	 */
	private function get_additional_image_urls( $image_urls ) {

		return array_slice( $image_urls, 1, 20 );
	}


	// Returns the parent image id for variable products only.
	public function get_parent_image_id() {
		if ( WC_Facebookcommerce_Utils::is_variation_type( $this->woo_product->get_type() ) ) {
			$parent_data = $this->get_parent_data();
			return $parent_data['image_id'];
		}
		return null;
	}

	public function set_description( $description ) {
		$description          = stripslashes(
			WC_Facebookcommerce_Utils::clean_string( $description )
		);
		$this->fb_description = $description;
		update_post_meta(
			$this->id,
			self::FB_PRODUCT_DESCRIPTION,
			$description
		);
	}

	public function set_product_image( $image ) {
		if ( $image !== null && strlen( $image ) !== 0 ) {
			$image = WC_Facebookcommerce_Utils::clean_string( $image );
			$image = WC_Facebookcommerce_Utils::make_url( $image );
			update_post_meta(
				$this->id,
				self::FB_PRODUCT_IMAGE,
				$image
			);
		}
	}

	public function set_price( $price ) {
		if ( is_numeric( $price ) ) {
			update_post_meta(
				$this->id,
				self::FB_PRODUCT_PRICE,
				$price
			);
		} else {
			delete_post_meta(
				$this->id,
				self::FB_PRODUCT_PRICE
			);
		}
	}

	public function get_use_parent_image() {
		if ( $this->fb_use_parent_image === null ) {
			$variant_image_setting     =
			get_post_meta( $this->id, self::FB_VARIANT_IMAGE, true );
			$this->fb_use_parent_image = ( $variant_image_setting ) ? true : false;
		}
		return $this->fb_use_parent_image;
	}

	public function set_use_parent_image( $setting ) {
		$this->fb_use_parent_image = ( $setting == 'yes' );
		update_post_meta(
			$this->id,
			self::FB_VARIANT_IMAGE,
			$this->fb_use_parent_image
		);
	}

	public function get_fb_description() {
		$description = '';

		if ( $this->fb_description ) {
			$description = $this->fb_description;
		}

		if ( empty( $description ) ) {
			// Try to get description from post meta
			$description = get_post_meta(
				$this->id,
				self::FB_PRODUCT_DESCRIPTION,
				true
			);
		}

		// Check if the product type is a variation and no description is found yet
		if ( empty( $description ) && WC_Facebookcommerce_Utils::is_variation_type( $this->woo_product->get_type() ) ) {
			$description = WC_Facebookcommerce_Utils::clean_string( $this->woo_product->get_description() );

			// Fallback to main description
			if ( empty( $description ) && $this->main_description ) {
				$description = $this->main_description;
			}
		}

		// If no description is found from meta or variation, get from post
		if ( empty( $description ) ) {
			$post         = $this->get_post_data();
			$post_content = WC_Facebookcommerce_Utils::clean_string( $post->post_content );
			$post_excerpt = WC_Facebookcommerce_Utils::clean_string( $post->post_excerpt );
			$post_title   = WC_Facebookcommerce_Utils::clean_string( $post->post_title );

			// Prioritize content, then excerpt, then title
			if ( ! empty( $post_content ) ) {
				$description = $post_content;
			}

			if ( $this->sync_short_description || ( empty( $description ) && ! empty( $post_excerpt ) ) ) {
				$description = $post_excerpt;
			}

			if ( empty( $description ) ) {
				$description = $post_title;
			}
		}
		/**
		 * Filters the FB product description.
		 *
		 * @since 3.2.6
		 *
		 * @param string  $description Facebook product description.
		 * @param int     $id          WooCommerce Product ID.
		 */
		return apply_filters( 'facebook_for_woocommerce_fb_product_description', $description, $this->id );
	}

	/**
	 * @param array $product_data
	 * @param bool $for_items_batch
	 *
	 * @return array
	 */
	public function add_sale_price( $product_data, $for_items_batch = false ) {

		$sale_price = $this->woo_product->get_sale_price();
		$sale_price_effective_date = '';

		$sale_start =
			( $date     = $this->woo_product->get_date_on_sale_from() )
				? date_i18n( WC_DateTime::ATOM, $date->getOffsetTimestamp() )
				: self::MIN_DATE_1 . self::MIN_TIME;

		$sale_end =
			( $date   = $this->woo_product->get_date_on_sale_to() )
				? date_i18n( WC_DateTime::ATOM, $date->getOffsetTimestamp() )
				: self::MAX_DATE . self::MAX_TIME;

		// check if sale exist
		if ( is_numeric( $sale_price ) && $sale_price > 0 ) {
			$sale_price_effective_date = $sale_start . '/' . $sale_end;
			$sale_price =
				intval( round( $this->get_price_plus_tax( $sale_price ) * 100 ) );
		}

		// check if sale is expired and sale time range is valid
		if ( $for_items_batch ) {
			$product_data['sale_price_effective_date'] = $sale_price_effective_date;
			$product_data['sale_price']                = is_numeric( $sale_price ) ? self::format_price_for_fb_items_batch( $sale_price ) : "";
		} else {
			$product_data['sale_price_start_date'] = $sale_start;
			$product_data['sale_price_end_date']   = $sale_end;
			$product_data['sale_price']            = is_numeric( $sale_price ) ? $sale_price : 0;
		}

		return $product_data;
	}


	public function get_price_plus_tax( $price ) {
		$woo_product = $this->woo_product;
		// // wc_get_price_including_tax exist for Woo > 2.7
		if ( function_exists( 'wc_get_price_including_tax' ) ) {
			$args = array(
				'qty'   => 1,
				'price' => $price,
			);
			return get_option( 'woocommerce_tax_display_shop' ) === 'incl'
				  ? wc_get_price_including_tax( $woo_product, $args )
				  : wc_get_price_excluding_tax( $woo_product, $args );
		} else {
			return get_option( 'woocommerce_tax_display_shop' ) === 'incl'
				  ? $woo_product->get_price_including_tax( 1, $price )
				  : $woo_product->get_price_excluding_tax( 1, $price );
		}
	}

	public function get_grouped_product_option_names( $key, $option_values ) {
		// Convert all slug_names in $option_values into the visible names that
		// advertisers have set to be the display names for a given attribute value
		$terms = get_the_terms( $this->id, $key );
		return ! is_array( $terms ) ? array() : array_map(
			function ( $slug_name ) use ( $terms ) {
				foreach ( $terms as $term ) {
					if ( $term->slug === $slug_name ) {
						return $term->name;
					}
				}
				return $slug_name;
			},
			$option_values
		);
	}


	public function update_visibility( $is_product_page, $visible_box_checked ) {
		$visibility = get_post_meta( $this->id, self::FB_VISIBILITY, true );
		if ( $visibility && ! $is_product_page ) {
			// If the product was previously set to visible, keep it as visible
			// (unless we're on the product page)
			$this->fb_visibility = $visibility;
		} else {
			// If the product is not visible OR we're on the product page,
			// then update the visibility as needed.
			$this->fb_visibility = $visible_box_checked ? true : false;
			update_post_meta( $this->id, self::FB_VISIBILITY, $this->fb_visibility );
		}
	}

	// wrapper function to find item_id for default variation
	function find_matching_product_variation() {
		if ( is_callable( array( $this, 'get_default_attributes' ) ) ) {
			$default_attributes = $this->get_default_attributes();
		} else {
			$default_attributes = $this->get_variation_default_attributes();
		}

		if ( ! $default_attributes ) {
			return;
		}
		foreach ( $default_attributes as $key => $value ) {
			if ( strncmp( $key, 'attribute_', strlen( 'attribute_' ) ) === 0 ) {
				continue;
			}
			unset( $default_attributes[ $key ] );
			$default_attributes[ sprintf( 'attribute_%s', $key ) ] = $value;
		}
		if ( class_exists( 'WC_Data_Store' ) ) {
			// for >= woo 3.0.0
			$data_store = WC_Data_Store::load( 'product' );
			return $data_store->find_matching_product_variation(
				$this,
				$default_attributes
			);
		} else {
			return $this->get_matching_variation( $default_attributes );
		}
	}

	private function build_checkout_url( $product_url ) {
		// Use product_url for external/bundle product setting.
		$product_type = $this->get_type();
		if ( ! $product_type || ! isset( self::$use_checkout_url[ $product_type ] ) ) {
				$checkout_url = $product_url;
		} elseif ( wc_get_cart_url() ) {
			$char = '?';
			// Some merchant cart pages are actually a querystring
			if ( strpos( wc_get_cart_url(), '?' ) !== false ) {
				$char = '&';
			}

			$checkout_url = WC_Facebookcommerce_Utils::make_url(
				wc_get_cart_url() . $char
			);

			if ( WC_Facebookcommerce_Utils::is_variation_type( $this->get_type() ) ) {
				$query_data = array(
					'add-to-cart'  => $this->get_parent_id(),
					'variation_id' => $this->get_id(),
				);

				$query_data = array_merge(
					$query_data,
					$this->get_variation_attributes()
				);

			} else {
				$query_data = array(
					'add-to-cart' => $this->get_id(),
				);
			}

			$checkout_url = $checkout_url . http_build_query( $query_data );

		} else {
			$checkout_url = null;
		}//end if
	}

	/**
	 * Gets product data to send to Facebook.
	 *
	 * @param string $retailer_id the retailer ID of the product
	 * @param string $type_to_prepare_for whether the data is going to be used in a feed upload, an items_batch update or a direct api call
	 * @return array
	 */
	public function prepare_product( $retailer_id = null, $type_to_prepare_for = self::PRODUCT_PREP_TYPE_NORMAL ) {

		if ( ! $retailer_id ) {
			$retailer_id =
			WC_Facebookcommerce_Utils::get_fb_retailer_id( $this );
		}
		$image_urls = $this->get_all_image_urls();

		$video_urls = $this->get_all_video_urls();

		// Replace WordPress sanitization's ampersand with a real ampersand.
		$product_url = str_replace(
			'&amp%3B',
			'&',
			html_entity_decode( $this->get_permalink() )
		);

		$id = $this->get_id();
		if ( WC_Facebookcommerce_Utils::is_variation_type( $this->get_type() ) ) {
			$id = $this->get_parent_id();
		}
		$categories =
		WC_Facebookcommerce_Utils::get_product_categories( $id );

		// Get brand attribute.
		$brand = get_post_meta( $id, Products::ENHANCED_CATALOG_ATTRIBUTES_META_KEY_PREFIX . 'brand', true );
		$brand_taxonomy = get_the_term_list( $id, 'product_brand', '', ', ' );

		if ( $brand ) {
			$brand = WC_Facebookcommerce_Utils::clean_string( $brand );
		} elseif ( !is_wp_error( $brand_taxonomy ) && $brand_taxonomy ) {
			$brand = WC_Facebookcommerce_Utils::clean_string( $brand_taxonomy );
		} else {
			$brand = wp_strip_all_tags( WC_Facebookcommerce_Utils::get_store_name() );
		}

		if ( self::PRODUCT_PREP_TYPE_ITEMS_BATCH === $type_to_prepare_for ) {
			$product_data = array(
				'title'                 => WC_Facebookcommerce_Utils::clean_string( $this->get_title() ),
				'description'           => $this->get_fb_description(),
				'image_link'            => $image_urls[0],
				'additional_image_link' => $this->get_additional_image_urls( $image_urls ),
				'link'                  => $product_url,
				'product_type'          => $categories['categories'],
				'brand'                 => Helper::str_truncate( $brand, 100 ),
				'retailer_id'           => $retailer_id,
				'price'                 => $this->get_fb_price( true ),
				'availability'          => $this->is_in_stock() ? 'in stock' : 'out of stock',
				'visibility'            => Products::is_product_visible( $this->woo_product ) ? \WC_Facebookcommerce_Integration::FB_SHOP_PRODUCT_VISIBLE : \WC_Facebookcommerce_Integration::FB_SHOP_PRODUCT_HIDDEN,
			);
			$product_data   = $this->add_sale_price( $product_data, true );
			$gpc_field_name = 'google_product_category';
			if ( ! empty( $video_urls ) ) {
				$product_data['video'] = $video_urls;
			}
		} else {
			$product_data = array(
				'name'                  => WC_Facebookcommerce_Utils::clean_string( $this->get_title() ),
				'description'           => $this->get_fb_description(),
				'image_url'             => $image_urls[0],
				'additional_image_urls' => $this->get_additional_image_urls( $image_urls ),
				'url'                   => $product_url,
				/**
				 * 'category' is a required field for creating a ProductItem object when posting to /{product_catalog_id}/products.
				 * This field should have the Google product category for the item. Google product category is not a required field
				 * in the WooCommerce product editor. Hence, we are setting 'category' to Woo product categories by default and overriding
				 * it when a Google product category is set.
				 *
				 * @see https://developers.facebook.com/docs/marketing-api/reference/product-catalog/products/#parameters-2
				 * @see https://github.com/woocommerce/facebook-for-woocommerce/pull/2575
				 * @see https://github.com/woocommerce/facebook-for-woocommerce/issues/2593
				 */
				'category'              => $categories['categories'],
				'product_type'          => $categories['categories'],
				'brand'                 => Helper::str_truncate( $brand, 100 ),
				'retailer_id'           => $retailer_id,
				'price'                 => $this->get_fb_price(),
				'currency'              => get_woocommerce_currency(),
				'availability'          => $this->is_in_stock() ? 'in stock' : 'out of stock',
				'visibility'            => Products::is_product_visible( $this->woo_product ) ? \WC_Facebookcommerce_Integration::FB_SHOP_PRODUCT_VISIBLE : \WC_Facebookcommerce_Integration::FB_SHOP_PRODUCT_HIDDEN,
			);

			if ( self::PRODUCT_PREP_TYPE_NORMAL !== $type_to_prepare_for && ! empty( $video_urls ) ) {
				$product_data['video'] = $video_urls;
			}
			$product_data   = $this->add_sale_price( $product_data );
			$gpc_field_name = 'category';
		}//end if

		$google_product_category = Products::get_google_product_category_id( $this->woo_product );
		if ( $google_product_category ) {
			$product_data[ $gpc_field_name ] = $google_product_category;
		}

		// Currently only items batch and feed support enhanced catalog fields
		if ( self::PRODUCT_PREP_TYPE_NORMAL !== $type_to_prepare_for && $google_product_category ) {
			$product_data = $this->apply_enhanced_catalog_fields_from_attributes( $product_data, $google_product_category );
		}

		// add the Commerce values (only stock quantity for the moment)
		if ( Products::is_product_ready_for_commerce( $this->woo_product ) ) {
			$product_data['quantity_to_sell_on_facebook'] = (int) max( 0, $this->woo_product->get_stock_quantity() );
		}

		// Only use checkout URLs if they exist.
		$checkout_url = $this->build_checkout_url( $product_url );
		if ( $checkout_url ) {
			$product_data['checkout_url'] = $checkout_url;
		}

		// IF using WPML, set the product to hidden unless it is in the
		// default language. WPML >= 3.2 Supported.
		if ( defined( 'ICL_LANGUAGE_CODE' ) ) {
			if ( class_exists( 'WC_Facebook_WPML_Injector' ) && WC_Facebook_WPML_Injector::should_hide( $id ) ) {
				$product_data['visibility'] = \WC_Facebookcommerce_Integration::FB_SHOP_PRODUCT_HIDDEN;
			}
		}

			// Exclude variations that are "virtual" products from export to Facebook &&
			// No Visibility Option for Variations
			// get_virtual() returns true for "unassembled bundles", so we exclude
			// bundles from this check.
			if ( true === $this->get_virtual() && 'bundle' !== $this->get_type() ) {
				$product_data['visibility'] = \WC_Facebookcommerce_Integration::FB_SHOP_PRODUCT_HIDDEN;
			}

		if ( self::PRODUCT_PREP_TYPE_FEED !== $type_to_prepare_for ) {
			$this->prepare_variants_for_item( $product_data );
		} elseif (
		WC_Facebookcommerce_Utils::is_all_caps( $product_data['description'] )
		) {
			$product_data['description'] =
			mb_strtolower( $product_data['description'] );
		}

		/**
		   * Filters the generated product data.
		   *
		   * @param int   $id           Woocommerce product id
		   * @param array $product_data An array of product data
		   */
		return apply_filters(
			'facebook_for_woocommerce_integration_prepare_product',
			$product_data,
			$id
		);
	}

	/**
	 * Adds enhanced catalog fields to product data array. Separated from
	 * the main function to make it easier to develop and debug, potentially
	 * worth refactoring into main prepare_product function when complete.
	 *
	 * @param array  $product_data       The preparted product data map.
	 * @param string $google_category_id The Google product category id string.
	 * @return array
	 */
	private function apply_enhanced_catalog_fields_from_attributes( $product_data, $google_category_id ) {
		$category_handler   = facebook_for_woocommerce()->get_facebook_category_handler();
		if ( empty( $google_category_id ) || ! $category_handler->is_category( $google_category_id ) ) {
			return $product_data;
		}
		$enhanced_data = array();

		$category_attrs = $category_handler->get_attributes_with_fallback_to_parent_category( $google_category_id );
		$all_attributes = $this->get_matched_attributes_for_product( $this->woo_product, $category_attrs );

		foreach ( $all_attributes as $attribute ) {
			$value            = Products::get_enhanced_catalog_attribute( $attribute['key'], $this->woo_product );
			$convert_to_array = (
				isset( $attribute['can_have_multiple_values'] ) &&
				true === $attribute['can_have_multiple_values'] &&
				'string' === $attribute['type']
			);

			if ( ! empty( $value ) &&
				$category_handler->is_valid_value_for_attribute( $google_category_id, $attribute['key'], $value )
			) {
				if ( $convert_to_array ) {
					$value = array_map( 'trim', explode( ',', $value ) );
				}
				$enhanced_data[ $attribute['key'] ] = $value;
			}
		}

		return array_merge( $product_data, $enhanced_data );
	}


	/**
	 * Filters list of attributes to only those available for a given product
	 *
	 * @param \WC_Product $product WooCommerce Product
	 * @param array       $all_attributes List of Enhanced Catalog attributes to match
	 * @return array
	 */
	public function get_matched_attributes_for_product( $product, $all_attributes ) {
		$matched_attributes = array();
		$sanitized_keys     = array_map(
			function( $key ) {
					return \WC_Facebookcommerce_Utils::sanitize_variant_name( $key, false );
			},
			array_keys( $product->get_attributes() )
		);

		$matched_attributes = array_filter(
			$all_attributes,
			function( $attribute ) use ( $sanitized_keys ) {
				return in_array( $attribute['key'], $sanitized_keys );
			}
		);

		return $matched_attributes;
	}


	/**
	 * Normalizes variant data for Facebook.
	 *
	 * @param array $product_data variation product data
	 * @return array
	 */
	public function prepare_variants_for_item( &$product_data ) {

		/** @var \WC_Product_Variation $product */
		$product = $this;

		if ( ! $product->is_type( 'variation' ) ) {
			return array();
		}

		$attributes = $product->get_variation_attributes();

		if ( ! $attributes ) {
			return array();
		}

		$variant_names = array_keys( $attributes );
		$variant_data  = array();

		// Loop through variants (size, color, etc) if they exist
		// For each product field type, pull the single variant
		foreach ( $variant_names as $original_variant_name ) {

			// don't handle any attributes that are designated as Commerce attributes
			if ( in_array( str_replace( 'attribute_', '', strtolower( $original_variant_name ) ), Products::get_distinct_product_attributes( $this->woo_product ), true ) ) {
				continue;
			}

			// Retrieve label name for attribute
			$label = wc_attribute_label( $original_variant_name, $product );

			// Clean up variant name (e.g. pa_color should be color)
			$new_name = \WC_Facebookcommerce_Utils::sanitize_variant_name( $original_variant_name, false );

			// Sometimes WC returns an array, sometimes it's an assoc array, depending
			// on what type of taxonomy it's using.  array_values will guarantee we
			// only get a flat array of values.
			if ( $options = \WC_Facebookcommerce_Utils::get_variant_option_name( $this->id, $label, $attributes[ $original_variant_name ] ) ) {

				if ( is_array( $options ) ) {

					$option_values = array_values( $options );

				} else {

					$option_values = array( $options );

					// If this attribute has value 'any', options will be empty strings
					// Redirect to product page to select variants.
					// Reset checkout url since checkout_url (build from query data will
					// be invalid in this case.
					if ( count( $option_values ) === 1 && empty( $option_values[0] ) ) {
							$option_values[0]             = 'any';
							$product_data['checkout_url'] = $product_data['url'];
					}
				}

				if ( \WC_Facebookcommerce_Utils::FB_VARIANT_GENDER === $new_name && ! isset( $product_data[ \WC_Facebookcommerce_Utils::FB_VARIANT_GENDER ] ) ) {

					// If we can't validate the gender, this will be null.
					$product_data[ $new_name ] = \WC_Facebookcommerce_Utils::validateGender( $option_values[0] );
				}

				switch ( $new_name ) {

					case \WC_Facebookcommerce_Utils::FB_VARIANT_GENDER:
						// If we can't validate the GENDER field, we'll fall through to the
						// default case and set the gender into custom data.
						if ( $product_data[ $new_name ] ) {

							$variant_data[] = array(
								'product_field' => $new_name,
								'label'         => $label,
								'options'       => $option_values,
							);
						}

						break;

					default:
						// This is for any custom_data.
						if ( ! isset( $product_data['custom_data'] ) ) {
							$product_data['custom_data'] = array();
						}
						$new_name                                 = wc_attribute_label( $new_name, $product );
						$product_data['custom_data'][ $new_name ] = urldecode( $option_values[0] );
						break;
				}//end switch
			} else {

				\WC_Facebookcommerce_Utils::log( $product->get_id() . ': No options for ' . $original_variant_name );
				continue;
			}//end if
		}//end foreach

		return $variant_data;
	}


	/**
	 * Normalizes variable product variations data for Facebook.
	 *
	 * @param bool $feed_data whether this is used for feed data
	 * @return array
	 */
	public function prepare_variants_for_group( $feed_data = false ) {

		/** @var \WC_Product_Variable $product */
		$product        = $this;
		$final_variants = array();

		try {

			if ( ! $product->is_type( 'variable' ) ) {
				throw new \Exception( 'prepare_variants_for_group called on non-variable product' );
			}

			$variation_attributes = $product->get_variation_attributes();

			if ( ! $variation_attributes ) {
				return array();
			}

			foreach ( array_keys( $product->get_attributes() ) as $name ) {

				$label = wc_attribute_label( $name, $product );

				if ( taxonomy_is_product_attribute( $name ) ) {
					$key = $name;
				} else {
					// variation_attributes keys are labels for custom attrs for some reason
					$key = $label;
				}

				if ( ! $key ) {
					throw new \Exception( "Critical error: can't get attribute name or label!" );
				}

				if ( isset( $variation_attributes[ $key ] ) ) {
					// array of the options (e.g. small, medium, large)
					$option_values = $variation_attributes[ $key ];
				} else {
					// skip variations without valid attribute options
					\WC_Facebookcommerce_Utils::log( $product->get_id() . ': No options for ' . $name );
					continue;
				}

				// If this is a variable product, check default attribute.
				// If it's being used, show it as the first option on Facebook.
				if ( $first_option = $product->get_variation_default_attribute( $key ) ) {

					$index = array_search( $first_option, $option_values, false );

					unset( $option_values[ $index ] );

					array_unshift( $option_values, $first_option );
				}

				if ( function_exists( 'taxonomy_is_product_attribute' ) && taxonomy_is_product_attribute( $name ) ) {
					$option_values = $this->get_grouped_product_option_names( $key, $option_values );
				}

				switch ( $name ) {

					case Products::get_product_color_attribute( $this->woo_product ):
						$name = WC_Facebookcommerce_Utils::FB_VARIANT_COLOR;
						break;

					case Products::get_product_size_attribute( $this->woo_product ):
						$name = WC_Facebookcommerce_Utils::FB_VARIANT_SIZE;
						break;

					case Products::get_product_pattern_attribute( $this->woo_product ):
						$name = WC_Facebookcommerce_Utils::FB_VARIANT_PATTERN;
						break;

					default:
						/**
						 * For API approach, product_field need to start with 'custom_data:'
						 *
						 * @link https://developers.facebook.com/docs/marketing-api/reference/product-variant/
						 */
						$name = \WC_Facebookcommerce_Utils::sanitize_variant_name( $name );
				}//end switch

				// for feed uploading, product field should remove prefix 'custom_data:'
				if ( $feed_data ) {
					$name = str_replace( 'custom_data:', '', $name );
				}

				$final_variants[] = array(
					'product_field' => $name,
					'label'         => $label,
					'options'       => $option_values,
				);
			}//end foreach
		} catch ( \Exception $e ) {

			\WC_Facebookcommerce_Utils::fblog( $e->getMessage() );

			return array();
		}//end try

		return $final_variants;
	}


}