diff --git a/facebook-commerce.php b/facebook-commerce.php index 23f4af4c..38b359fd 100644 --- a/facebook-commerce.php +++ b/facebook-commerce.php @@ -793,6 +793,7 @@ public function on_product_save( int $wp_id ) { $products_to_delete_from_facebook = $this->get_removed_from_sync_products_to_delete(); if ( $product->is_type( 'variable' ) ) { + $this->save_variable_product_settings( $product ); // check variations for deletion foreach ( $products_to_delete_from_facebook as $delete_product_id ) { $delete_product = wc_get_product( $delete_product_id ); @@ -841,6 +842,19 @@ public function on_product_save( int $wp_id ) { } } + /** + * Saves the submitted Facebook settings for a variable product. + * + * + * @param \WC_Product $product The variable product object. + */ + private function save_variable_product_settings( WC_Product $product ) { + $woo_product = new WC_Facebook_Product( $product->get_id() ); + if ( isset( $_POST[ WC_Facebook_Product::FB_VARIABLE_BRAND ] ) ) { + $woo_product->set_fb_brand( sanitize_text_field( wp_unslash( $_POST[ WC_Facebook_Product::FB_VARIABLE_BRAND ] ) ) ); + } + } + /** * Saves the submitted Facebook settings for a product. * @@ -868,6 +882,14 @@ private function save_product_settings( WC_Product $product ) { if ( isset( $_POST[ WC_Facebook_Product::FB_PRODUCT_IMAGE ] ) ) { $woo_product->set_product_image( sanitize_text_field( wp_unslash( $_POST[ WC_Facebook_Product::FB_PRODUCT_IMAGE ] ) ) ); } + + if ( isset( $_POST[ WC_Facebook_Product::FB_BRAND ] ) ) { + $woo_product->set_fb_brand( sanitize_text_field( wp_unslash( $_POST[ WC_Facebook_Product::FB_BRAND ] ) ) ); + } + + if ( isset( $_POST[ WC_Facebook_Product::FB_MPN ] ) ) { + $woo_product->set_fb_mpn( sanitize_text_field( wp_unslash( $_POST[ WC_Facebook_Product::FB_MPN ] ) ) ); + } // phpcs:enable WordPress.Security.NonceVerification.Missing } diff --git a/includes/Admin.php b/includes/Admin.php index 2e006857..33f7bafe 100644 --- a/includes/Admin.php +++ b/includes/Admin.php @@ -1185,6 +1185,8 @@ public function add_product_settings_tab_content() { $price = get_post_meta( $post->ID, \WC_Facebook_Product::FB_PRODUCT_PRICE, true ); $image_source = get_post_meta( $post->ID, Products::PRODUCT_IMAGE_SOURCE_META_KEY, true ); $image = get_post_meta( $post->ID, \WC_Facebook_Product::FB_PRODUCT_IMAGE, true ); + $fb_brand = get_post_meta( $post->ID, \WC_Facebook_Product::FB_BRAND, true ) ? get_post_meta( $post->ID, \WC_Facebook_Product::FB_BRAND, true ) : get_post_meta( $post->ID, '_wc_facebook_enhanced_catalog_attributes_brand', true ); + $fb_mpn = get_post_meta( $post->ID, \WC_Facebook_Product::FB_MPN, true ); if ( $sync_enabled ) { $sync_mode = $is_visible ? self::SYNC_MODE_SYNC_AND_SHOW : self::SYNC_MODE_SYNC_AND_HIDE; @@ -1270,6 +1272,24 @@ public function add_product_settings_tab_content() { ) ); + woocommerce_wp_text_input( + array( + 'id' => \WC_Facebook_Product::FB_BRAND, + 'label' => __( 'Brand', 'facebook-for-woocommerce' ), + 'value' => $fb_brand, + 'class' => 'enable-if-sync-enabled', + ) + ); + + woocommerce_wp_text_input( + array( + 'id' => \WC_Facebook_Product::FB_MPN, + 'label' => __( 'Manufacturer Parts Number (MPN)', 'facebook-for-woocommerce' ), + 'value' => $fb_mpn, + 'class' => 'enable-if-sync-enabled', + ) + ); + woocommerce_wp_hidden_input( array( 'id' => \WC_Facebook_Product::FB_REMOVE_FROM_SYNC, @@ -1278,6 +1298,21 @@ public function add_product_settings_tab_content() { ); ?> + +
+ + @@ -1318,6 +1353,7 @@ public function add_product_variation_edit_fields( $index, $variation_data, $pos $price = $this->get_product_variation_meta( $variation, \WC_Facebook_Product::FB_PRODUCT_PRICE, $parent ); $image_url = $this->get_product_variation_meta( $variation, \WC_Facebook_Product::FB_PRODUCT_IMAGE, $parent ); $image_source = $variation->get_meta( Products::PRODUCT_IMAGE_SOURCE_META_KEY ); + $fb_mpn = $this->get_product_variation_meta( $variation, \WC_Facebook_Product::FB_MPN, $parent ); if ( $sync_enabled ) { $sync_mode = $is_visible ? self::SYNC_MODE_SYNC_AND_SHOW : self::SYNC_MODE_SYNC_AND_HIDE; @@ -1405,6 +1441,17 @@ public function add_product_variation_edit_fields( $index, $variation_data, $pos 'wrapper_class' => 'form-row form-full', ) ); + + woocommerce_wp_text_input( + array( + 'id' => sprintf( 'variable_%s%s', \WC_Facebook_Product::FB_MPN, $index ), + 'name' => sprintf( "variable_%s[$index]", \WC_Facebook_Product::FB_MPN ), + 'label' => __( 'Manufacturer Parts Number (MPN)', 'facebook-for-woocommerce' ), + 'value' => $fb_mpn, + 'class' => 'enable-if-sync-enabled', + ) + ); + } @@ -1456,6 +1503,8 @@ public function save_product_variation_edit_fields( $variation_id, $index ) { Products::set_product_visibility( $variation, self::SYNC_MODE_SYNC_AND_HIDE !== $sync_mode ); $posted_param = 'variable_' . \WC_Facebookcommerce_Integration::FB_PRODUCT_DESCRIPTION; $description = isset( $_POST[ $posted_param ][ $index ] ) ? sanitize_text_field( wp_unslash( $_POST[ $posted_param ][ $index ] ) ) : null; + $posted_param = 'variable_' . \WC_Facebook_Product::FB_MPN; + $fb_mpn = isset( $_POST[ $posted_param ][ $index ] ) ? sanitize_text_field( wp_unslash( $_POST[ $posted_param ][ $index ] ) ) : null; $posted_param = 'variable_fb_product_image_source'; $image_source = isset( $_POST[ $posted_param ][ $index ] ) ? sanitize_key( wp_unslash( $_POST[ $posted_param ][ $index ] ) ) : ''; $posted_param = 'variable_' . \WC_Facebook_Product::FB_PRODUCT_IMAGE; @@ -1464,6 +1513,7 @@ public function save_product_variation_edit_fields( $variation_id, $index ) { $price = isset( $_POST[ $posted_param ][ $index ] ) ? wc_format_decimal( wc_clean( wp_unslash( $_POST[ $posted_param ][ $index ] ) ) ) : ''; $variation->update_meta_data( \WC_Facebookcommerce_Integration::FB_PRODUCT_DESCRIPTION, $description ); $variation->update_meta_data( Products::PRODUCT_IMAGE_SOURCE_META_KEY, $image_source ); + $variation->update_meta_data( \WC_Facebook_Product::FB_MPN, $fb_mpn ); $variation->update_meta_data( \WC_Facebook_Product::FB_PRODUCT_IMAGE, $image_url ); $variation->update_meta_data( \WC_Facebook_Product::FB_PRODUCT_PRICE, $price ); $variation->save_meta_data(); diff --git a/includes/Admin/Enhanced_Catalog_Attribute_Fields.php b/includes/Admin/Enhanced_Catalog_Attribute_Fields.php index d5044521..d1662638 100644 --- a/includes/Admin/Enhanced_Catalog_Attribute_Fields.php +++ b/includes/Admin/Enhanced_Catalog_Attribute_Fields.php @@ -47,7 +47,7 @@ class Enhanced_Catalog_Attribute_Fields { */ private $category_handler; - public function __construct( $page_type, \WP_Term $term = null, \WC_Product $product = null ) { + public function __construct( $page_type, ?\WP_Term $term = null, ?\WC_Product $product = null ) { $this->page_type = $page_type; $this->term = $term; $this->product = $product; diff --git a/includes/Products/FBCategories.php b/includes/Products/FBCategories.php index 33d7d223..91270ccd 100644 --- a/includes/Products/FBCategories.php +++ b/includes/Products/FBCategories.php @@ -22,6 +22,8 @@ */ class FBCategories { + private $keys_to_exclude = ['brand' => true]; + /** * Fetches the attribute from a category using attribute key. * @@ -129,7 +131,14 @@ public function get_attributes( $category_id ) { $return_attributes = array(); foreach ( $category['attributes'] as $attribute_hash ) { // Get attribute array from the stored hash version - $return_attributes[] = $this->get_attribute_field_by_hash( $attribute_hash ); + $attribute = $this->get_attribute_field_by_hash( $attribute_hash ); + + // Skip if attribute is invalid or its key is in the exclude list + if ( ! is_array( $attribute ) || empty( $attribute['key'] ) || isset( $this->keys_to_exclude[ $attribute['key'] ] ) ) { + continue; + } + + $return_attributes[] = $attribute; } return $return_attributes; diff --git a/includes/fbproduct.php b/includes/fbproduct.php index c083b06c..b42ff8a1 100644 --- a/includes/fbproduct.php +++ b/includes/fbproduct.php @@ -36,6 +36,9 @@ class WC_Facebook_Product { const FB_VARIANT_IMAGE = 'fb_image'; const FB_VISIBILITY = 'fb_visibility'; const FB_REMOVE_FROM_SYNC = 'fb_remove_from_sync'; + const FB_BRAND = 'fb_brand'; + const FB_VARIABLE_BRAND = 'fb_variable_brand'; + const FB_MPN = 'fb_mpn'; const MIN_DATE_1 = '1970-01-29'; const MIN_DATE_2 = '1970-01-30'; @@ -358,6 +361,28 @@ public function set_product_image( $image ) { } } + public function set_fb_brand( $fb_brand ) { + $fb_brand = stripslashes( + WC_Facebookcommerce_Utils::clean_string( $fb_brand ) + ); + update_post_meta( + $this->id, + self::FB_BRAND, + $fb_brand + ); + } + + public function set_fb_mpn( $fb_mpn ) { + $fb_mpn = stripslashes( + WC_Facebookcommerce_Utils::clean_string( $fb_mpn ) + ); + update_post_meta( + $this->id, + self::FB_MPN, + $fb_mpn + ); + } + public function set_price( $price ) { if ( is_numeric( $price ) ) { update_post_meta( @@ -391,6 +416,38 @@ public function set_use_parent_image( $setting ) { ); } + public function get_fb_brand() { + // Get brand directly from post meta + $fb_brand = get_post_meta( + $this->id, + self::FB_BRAND, + true + ); + + // If empty and this is a variation, get the parent brand + if ( empty( $fb_brand ) && $this->is_type('variation') ) { + $parent_id = $this->get_parent_id(); + if ( $parent_id ) { + $fb_brand = get_post_meta($parent_id, self::FB_BRAND, true); + } + } + + // Fallback to brand attribute or store name if no brand found + if ( empty( $fb_brand ) ) { + $brand = get_post_meta( $this->id, Products::ENHANCED_CATALOG_ATTRIBUTES_META_KEY_PREFIX . 'brand', true ); + $brand_taxonomy = get_the_term_list( $this->id, 'product_brand', '', ', ' ); + if ( $brand ) { + $fb_brand = $brand; + } elseif ( !is_wp_error( $brand_taxonomy ) && $brand_taxonomy ) { + $fb_brand = $brand_taxonomy; + } else { + $fb_brand = wp_strip_all_tags( WC_Facebookcommerce_Utils::get_store_name() ); + } + } + + return WC_Facebookcommerce_Utils::clean_string( $fb_brand ); + } + public function get_fb_description() { $description = ''; @@ -498,6 +555,23 @@ public function add_sale_price( $product_data, $for_items_batch = false ) { return $product_data; } + public function get_fb_mpn() { + $fb_mpn = get_post_meta( + $this->id, + self::FB_MPN, + true + ); + + // If empty and this is a variation, get the parent MPN + if ( empty( $fb_mpn ) && $this->is_type('variation') ) { + $parent_id = $this->get_parent_id(); + if ( $parent_id ) { + $fb_mpn = get_post_meta($parent_id, self::FB_MPN, true); + } + } + + return WC_Facebookcommerce_Utils::clean_string( $fb_mpn ); + } public function get_price_plus_tax( $price ) { $woo_product = $this->woo_product; @@ -650,18 +724,6 @@ public function prepare_product( $retailer_id = null, $type_to_prepare_for = sel $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() ); - } - $custom_fields = $this->get_facebook_specific_fields(); if ( self::PRODUCT_PREP_TYPE_ITEMS_BATCH === $type_to_prepare_for ) { @@ -672,7 +734,8 @@ public function prepare_product( $retailer_id = null, $type_to_prepare_for = sel 'additional_image_link' => $this->get_additional_image_urls( $image_urls ), 'link' => $product_url, 'product_type' => $categories['categories'], - 'brand' => Helper::str_truncate( $brand, 100 ), + 'brand' => Helper::str_truncate( $this->get_fb_brand(), 100 ), + 'mpn' => Helper::str_truncate( $this->get_fb_mpn(), 100 ), 'retailer_id' => $retailer_id, 'price' => $this->get_fb_price( true ), 'availability' => $this->is_in_stock() ? 'in stock' : 'out of stock', @@ -703,7 +766,8 @@ public function prepare_product( $retailer_id = null, $type_to_prepare_for = sel */ 'category' => $categories['categories'], 'product_type' => $categories['categories'], - 'brand' => Helper::str_truncate( $brand, 100 ), + 'brand' => Helper::str_truncate( $this->get_fb_brand(), 100 ), + 'mpn' => Helper::str_truncate( $this->get_fb_mpn(), 100 ), 'retailer_id' => $retailer_id, 'price' => $this->get_fb_price(), 'currency' => get_woocommerce_currency(), @@ -829,6 +893,36 @@ private function apply_enhanced_catalog_fields_from_attributes( $product_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 ) { + if ( is_array( $attribute ) && isset( $attribute['key'] ) ) { + return in_array( $attribute['key'], $sanitized_keys ); + } + return false; // Return false if $attribute is not valid + } + ); + + return $matched_attributes; + } + + /** * Normalizes variant data for Facebook. * @@ -857,6 +951,11 @@ public function prepare_variants_for_item( &$product_data ) { // For each product field type, pull the single variant foreach ( $variant_names as $original_variant_name ) { + // Ensure that the attribute exists before accessing it + if ( !isset( $attributes[ $original_variant_name ] ) ) { + continue; // Skip if the attribute is not set + } + // 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; diff --git a/tests/Unit/fbproductTest.php b/tests/Unit/fbproductTest.php index e4b89807..f236d035 100644 --- a/tests/Unit/fbproductTest.php +++ b/tests/Unit/fbproductTest.php @@ -588,4 +588,83 @@ public function test_prepare_product_items_batch() { $this->assertArrayHasKey('description', $product_data); $this->assertArrayHasKey('image_link', $product_data); } + + /** + * Test Brand is added for simple product + * @return void + */ + public function test_brand_for_simple_product_set() { + $woo_product = WC_Helper_Product::create_simple_product(); + $facebook_product = new \WC_Facebook_Product( $woo_product ); + $facebook_product->set_fb_brand('Nike'); + $facebook_product->save(); + + $fb_product = new \WC_Facebook_Product( $woo_product ); + $data = $fb_product->prepare_product(); + + $this->assertEquals( $data['brand'], 'Nike' ); + } + + /** + * Test MPN is added for simple product + * @return void + */ + public function test_mpn_for_simple_product_set() { + $woo_product = WC_Helper_Product::create_simple_product(); + $facebook_product = new \WC_Facebook_Product( $woo_product ); + $facebook_product->set_fb_mpn('123456789'); + $facebook_product->save(); + + $fb_product = new \WC_Facebook_Product( $woo_product ); + $data = $fb_product->prepare_product(); + + $this->assertEquals( $data['mpn'], '123456789' ); + } + + /** + * Test MPN is added for variable product + * @return void + */ + public function test_mpn_for_variable_product_set() { + $woo_product = WC_Helper_Product::create_variation_product(); + $woo_variation = wc_get_product($woo_product->get_children()[0]); + $facebook_product = new \WC_Facebook_Product( $woo_variation, new \WC_Facebook_Product( $woo_product ) ); + $facebook_product->set_fb_mpn('987654321'); + $facebook_product->save(); + + $fb_product = new \WC_Facebook_Product( $woo_variation, new \WC_Facebook_Product( $woo_product ) ); + $data = $fb_product->prepare_product(); + + $this->assertEquals( $data['mpn'], '987654321' ); + } + + /** + * Test it gets brand from parent product if it is a variation. + * @return void + */ + public function test_get_fb_brand_variable_products() { + // Create a variable product and set the brand for the parent + $variable_product = WC_Helper_Product::create_variation_product(); + $facebook_product_parent = new \WC_Facebook_Product($variable_product); + $facebook_product_parent->set_fb_brand('Nike'); + $facebook_product_parent->save(); + + // Get the variation product + $variation = wc_get_product($variable_product->get_children()[0]); + + // Create a Facebook product instance for the variation + $facebook_product_variation = new \WC_Facebook_Product($variation); + + // Retrieve the brand from the variation + $brand = $facebook_product_variation->get_fb_brand(); + $this->assertEquals($brand, 'Nike'); + + // Set a different brand for the variation + $facebook_product_variation->set_fb_brand('Adidas'); + $facebook_product_variation->save(); + + // Retrieve the brand again and check if it reflects the new value + $brand = $facebook_product_variation->get_fb_brand(); + $this->assertEquals($brand, 'Adidas'); + } }