From e3fbd6c1e13106bbe57871aabb98cd723c88773b Mon Sep 17 00:00:00 2001 From: Carter Buce Date: Fri, 11 Apr 2025 18:18:31 -0700 Subject: [PATCH 1/4] add undefined_attributes field to product syncing --- includes/fbproduct.php | 66 +++++++++++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/includes/fbproduct.php b/includes/fbproduct.php index b2b5c8d94..d09c2a204 100644 --- a/includes/fbproduct.php +++ b/includes/fbproduct.php @@ -182,7 +182,7 @@ class WC_Facebook_Product { */ public function check_attribute_mapping($attribute_name) { $sanitized_name = \WC_Facebookcommerce_Utils::sanitize_variant_name($attribute_name, false); - + foreach (self::$standard_facebook_fields as $fb_field => $possible_matches) { foreach ($possible_matches as $match) { if (stripos($sanitized_name, $match) !== false) { @@ -190,7 +190,7 @@ public function check_attribute_mapping($attribute_name) { } } } - + return false; } @@ -205,10 +205,10 @@ public function get_unmapped_attributes() { foreach ($attributes as $attribute_name => $_) { $value = $this->woo_product->get_attribute($attribute_name); - + if (!empty($value)) { $mapped_field = $this->check_attribute_mapping($attribute_name); - + if ($mapped_field === false) { $unmapped_attributes[] = array( 'name' => $attribute_name, @@ -221,6 +221,25 @@ public function get_unmapped_attributes() { return $unmapped_attributes; } + /** + * Get all attributes on the product that do not have a defined value. This can happen for variant products + * that use the "Any" attribute selection + * + * @return array Array of undefined attribute names + */ + public function get_undefined_attributes() { + $undefined_attributes = array(); + $attributes = $this->woo_product->get_attributes(); + + foreach ($attributes as $attribute_name => $attribute_value) { + if ( empty( $attribute_value ) ) { + $undefined_attributes[] = $attribute_name; + } + } + + return $undefined_attributes; + } + public function __construct( $wpid, $parent_product = null ) { if ( $wpid instanceof WC_Product ) { @@ -542,7 +561,7 @@ public function set_fb_brand( $fb_brand ) { /** * Utility method to set basic Facebook product attributes - * + * * @param string $key The meta key to store the value under * @param string $value The value to store * @return void @@ -557,11 +576,11 @@ private function set_fb_attribute($key, $value) { $value ); } - + public function set_fb_material( $fb_material ) { $this->set_fb_attribute(self::FB_MATERIAL, $fb_material); } - + public function set_fb_pattern( $fb_pattern ) { $this->set_fb_attribute(self::FB_PATTERN, $fb_pattern); } @@ -577,7 +596,7 @@ public function set_fb_condition( $fb_condition ) { public function set_fb_age_group( $fb_age_group ) { $this->set_fb_attribute(self::FB_AGE_GROUP, $fb_age_group); } - + public function set_fb_gender( $fb_gender ) { $this->set_fb_attribute(self::FB_GENDER, $fb_gender); } @@ -653,7 +672,7 @@ public function get_fb_brand() { 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) { @@ -744,7 +763,7 @@ public function get_fb_short_description() { $short_description = WC_Facebookcommerce_Utils::clean_string($parent_post->post_excerpt); } } - + // If no parent description found, try getting the variation's own excerpt if (empty($short_description)) { $post = $this->get_post_data(); @@ -752,14 +771,14 @@ public function get_fb_short_description() { $short_description = WC_Facebookcommerce_Utils::clean_string($post->post_excerpt); } } - + return apply_filters('facebook_for_woocommerce_fb_product_short_description', $short_description, $this->id); } // Use the product's short description (excerpt) from WooCommerce $post = $this->get_post_data(); $post_excerpt = WC_Facebookcommerce_Utils::clean_string($post->post_excerpt); - + if (!empty($post_excerpt)) { $short_description = $post_excerpt; } @@ -941,7 +960,7 @@ public function get_fb_age_group() { // If this is a variation, get its specific age group value if ($this->is_type('variation')) { $attributes = $this->woo_product->get_attributes(); - + foreach ($attributes as $key => $value) { $attr_key = strtolower($key); if ($attr_key === 'age_group') { @@ -972,7 +991,7 @@ public function get_fb_gender() { // If this is a variation, get its specific gender value if ($this->is_type('variation')) { $attributes = $this->woo_product->get_attributes(); - + foreach ($attributes as $key => $value) { $attr_key = strtolower($key); if ($attr_key === 'gender') { @@ -1009,7 +1028,7 @@ public function get_fb_size() { // If this is a variation, get its specific size value if ($this->is_type('variation')) { $attributes = $this->woo_product->get_attributes(); - + foreach ($attributes as $key => $value) { $attr_key = strtolower($key); if ($attr_key === 'size') { @@ -1046,7 +1065,7 @@ public function get_fb_color() { // If this is a variation, get its specific color value if ($this->is_type('variation')) { $attributes = $this->woo_product->get_attributes(); - + foreach ($attributes as $key => $value) { $attr_key = strtolower($key); if ($attr_key === 'color' || $attr_key === 'colour') { @@ -1082,7 +1101,7 @@ public function get_fb_material() { // If this is a variation, get its specific material value if ($this->is_type('variation')) { $attributes = $this->woo_product->get_attributes(); - + // Check for material attribute foreach ($attributes as $key => $value) { $attr_key = strtolower($key); @@ -1114,7 +1133,7 @@ public function get_fb_mpn() { // If this is a variation, get its specific mpn value if ($this->is_type('variation')) { $attributes = $this->woo_product->get_attributes(); - + // Check for mpn attribute foreach ($attributes as $key => $value) { $attr_key = strtolower($key); @@ -1151,7 +1170,7 @@ public function get_fb_pattern() { // If this is a variation, get its specific material value if ($this->is_type('variation')) { $attributes = $this->woo_product->get_attributes(); - + // Check for material attribute foreach ($attributes as $key => $value) { $attr_key = strtolower($key); @@ -1278,7 +1297,7 @@ public function prepare_product( $retailer_id = null, $type_to_prepare_for = sel } $image_urls = $this->get_all_image_urls(); - + // Replace WordPress sanitization's ampersand with a real ampersand. $product_url = str_replace( '&%3B', @@ -1292,7 +1311,7 @@ public function prepare_product( $retailer_id = null, $type_to_prepare_for = sel } $categories = WC_Facebookcommerce_Utils::get_product_categories( $id ); - + $product_data = array(); $product_data[ 'description' ] = Helper::str_truncate( $this->get_fb_description(), self::MAX_DESCRIPTION_LENGTH ); $product_data[ 'short_description' ] = $this->get_fb_short_description(); @@ -1312,6 +1331,7 @@ public function prepare_product( $retailer_id = null, $type_to_prepare_for = sel $product_data[ 'gender' ] = $this->get_fb_gender(); $product_data[ 'material' ] = Helper::str_truncate( $this->get_fb_material(), 100 ); $product_data[ 'woo_product_type' ] = $this->get_type(); + $product_data[ 'undefined_attributes'] = $this->get_undefined_attributes(); $product_data[ 'unmapped_attributes' ] = $this->get_unmapped_attributes(); if ( self::PRODUCT_PREP_TYPE_ITEMS_BATCH === $type_to_prepare_for ) { @@ -1329,7 +1349,7 @@ public function prepare_product( $retailer_id = null, $type_to_prepare_for = sel $product_data['url'] = $product_url; $product_data['price'] = $this->get_fb_price(); $product_data['currency'] = get_woocommerce_currency(); - + /** * '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 @@ -1341,7 +1361,7 @@ public function prepare_product( $retailer_id = null, $type_to_prepare_for = sel * @see https://github.com/woocommerce/facebook-for-woocommerce/issues/2593 */ $product_data['category'] = $categories['categories']; - + $product_data = $this->add_sale_price( $product_data ); }//end if From 4b83fd07b4d5164f9a5a18f39eccdab21e0fe827 Mon Sep 17 00:00:00 2001 From: Carter Buce Date: Wed, 16 Apr 2025 12:47:29 -0700 Subject: [PATCH 2/4] send entire attributes array --- includes/fbproduct.php | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/includes/fbproduct.php b/includes/fbproduct.php index 35ed848e5..d012266ab 100644 --- a/includes/fbproduct.php +++ b/includes/fbproduct.php @@ -221,25 +221,6 @@ public function get_unmapped_attributes() { return $unmapped_attributes; } - /** - * Get all attributes on the product that do not have a defined value. This can happen for variant products - * that use the "Any" attribute selection - * - * @return array Array of undefined attribute names - */ - public function get_undefined_attributes() { - $undefined_attributes = array(); - $attributes = $this->woo_product->get_attributes(); - - foreach ($attributes as $attribute_name => $attribute_value) { - if ( empty( $attribute_value ) ) { - $undefined_attributes[] = $attribute_name; - } - } - - return $undefined_attributes; - } - public function __construct( $wpid, $parent_product = null ) { if ( $wpid instanceof WC_Product ) { @@ -1347,7 +1328,7 @@ public function prepare_product( $retailer_id = null, $type_to_prepare_for = sel $product_data[ 'gender' ] = $this->get_fb_gender(); $product_data[ 'material' ] = Helper::str_truncate( $this->get_fb_material(), 100 ); $product_data[ 'woo_product_type' ] = $this->get_type(); - $product_data[ 'undefined_attributes'] = $this->get_undefined_attributes(); + $product_data[ 'attributes'] = $this->woo_product->get_attributes(); $product_data[ 'unmapped_attributes' ] = $this->get_unmapped_attributes(); if ( self::PRODUCT_PREP_TYPE_ITEMS_BATCH === $type_to_prepare_for ) { From 5c27a751ecb24c6cdcad2e57eb7dbf7ec9ec1a05 Mon Sep 17 00:00:00 2001 From: Carter Buce Date: Tue, 22 Apr 2025 17:22:40 -0700 Subject: [PATCH 3/4] updating to use 'custom_fields' --- includes/fbproduct.php | 28 ++++- tests/Unit/fbproductTest.php | 206 ++++++++++++++++++++++------------- 2 files changed, 155 insertions(+), 79 deletions(-) diff --git a/includes/fbproduct.php b/includes/fbproduct.php index d012266ab..a4a6a57aa 100644 --- a/includes/fbproduct.php +++ b/includes/fbproduct.php @@ -1327,9 +1327,7 @@ public function prepare_product( $retailer_id = null, $type_to_prepare_for = sel $product_data[ 'age_group' ] = $this->get_fb_age_group(); $product_data[ 'gender' ] = $this->get_fb_gender(); $product_data[ 'material' ] = Helper::str_truncate( $this->get_fb_material(), 100 ); - $product_data[ 'woo_product_type' ] = $this->get_type(); - $product_data[ 'attributes'] = $this->woo_product->get_attributes(); - $product_data[ 'unmapped_attributes' ] = $this->get_unmapped_attributes(); + $product_data[ 'custom_fields' ] = $this->get_custom_fields(); if ( self::PRODUCT_PREP_TYPE_ITEMS_BATCH === $type_to_prepare_for ) { $product_data['title'] = Helper::str_truncate( WC_Facebookcommerce_Utils::clean_string( $this->get_title() ), self::MAX_TITLE_LENGTH ); @@ -1731,4 +1729,28 @@ public function prepare_variants_for_group( $feed_data = false ) { return $final_variants; } + /** + * Get a set of custom fields not present in the facebook product data type + * + * @return array + */ + private function get_custom_fields(): array { + $custom_fields = array(); + $product_type = $this->woo_product->get_type(); + + $custom_fields[ 'woo_product_type' ] = $product_type; + + // All attributes set on the item. For variable and simple products, this is a map of + // [string => WC_Product_Attribute]. For product variations, this is a map of [string => ?string]. For now, + // sending only for product variations as those can lead to issues with the checkout page. + if ($product_type == 'variation') { + $custom_fields['product_variation_attributes'] = $this->woo_product->get_attributes(); + } + + // attributes that are not mapped to standard Facebook fields + $custom_fields[ 'unmapped_attributes' ] = $this->get_unmapped_attributes(); + + return $custom_fields; + } + } diff --git a/tests/Unit/fbproductTest.php b/tests/Unit/fbproductTest.php index 122e20856..62c3bad91 100644 --- a/tests/Unit/fbproductTest.php +++ b/tests/Unit/fbproductTest.php @@ -525,7 +525,7 @@ public function test_prepare_product_with_video_field() { update_post_meta($this->product->get_id(), WC_Facebook_Product::FB_PRODUCT_VIDEO, $video_urls); $product_data = $this->fb_product->prepare_product(null, WC_Facebook_Product::PRODUCT_PREP_TYPE_ITEMS_BATCH); - + $this->assertArrayHasKey('video', $product_data); $this->assertEquals($expected_video_urls, $product_data['video']); } @@ -533,13 +533,13 @@ public function test_prepare_product_with_video_field() { public function test_set_product_video_urls() { // Prepare attachment IDs $attachment_ids = '123,456'; - + // Mock get_video_urls_from_attachment_ids function $this->fb_product = $this->getMockBuilder(WC_Facebook_Product::class) ->setConstructorArgs([$this->product]) ->setMethods(['get_video_urls_from_attachment_ids']) ->getMock(); - + $this->fb_product->method('get_video_urls_from_attachment_ids') ->willReturnCallback(function($id) { switch ($id) { @@ -551,14 +551,14 @@ public function test_set_product_video_urls() { return ''; } }); - + // Set the video URLs in post meta $video_urls = array_filter(array_map([$this->fb_product, 'get_video_urls_from_attachment_ids'], explode(',', $attachment_ids))); update_post_meta( $this->fb_product->get_id(), WC_Facebook_Product::FB_PRODUCT_VIDEO, $video_urls ); - + // Get the saved video URLs from post meta $saved_video_urls = get_post_meta( $this->product->get_id(), WC_Facebook_Product::FB_PRODUCT_VIDEO, true ); - + // Assert that the saved video URLs match the expected values $this->assertEquals( $saved_video_urls, $video_urls); @@ -572,7 +572,61 @@ public function test_set_product_video_urls() { $this->assertNotContains('', $saved_video_urls); } - public function test_prepare_product_items_batch() { + public function test_custom_fields_simple_product() { + $product_data = $this->fb_product->prepare_product(null, WC_Facebook_Product::PRODUCT_PREP_TYPE_ITEMS_BATCH); + $this->assertEquals('simple', $product_data['custom_fields']['woo_product_type']); + $this->assertEquals(array(), $product_data['custom_fields']['unmapped_attributes']); + $this->assertArrayNotHasKey('product_variation_attributes', $product_data['custom_fields']); + } + + public function test_custom_fields_variable_product() { + $product = WC_Helper_Product::create_simple_product(); + $woo_attributes = array('test_unmapped_attribute' => 1, 'pa_size' => 'medium'); + + // Set attributes on the product + $attributes = array(); + $position = 0; + foreach ($woo_attributes as $key => $value) { + $attribute = new WC_Product_Attribute(); + $attribute->set_id(0); + $attribute->set_name($key); + $attribute->set_options(array($value)); + $attribute->set_position($position++); + $attribute->set_visible(1); + $attribute->set_variation(0); + $attributes[] = $attribute; + } + $product->set_attributes($attributes); + $product->save_meta_data(); + + // Prepare Product and validate assertions + $facebook_product = new \WC_Facebook_Product($product); + $product_data = $facebook_product->prepare_product(null, WC_Facebook_Product::PRODUCT_PREP_TYPE_ITEMS_BATCH); + + $this->assertEquals('simple', $product_data['custom_fields']['woo_product_type']); + $this->assertArrayNotHasKey('product_variation_attributes', $product_data['custom_fields']); + $this->assertNotEmpty($product_data['custom_fields']['unmapped_attributes']); + } + + public function test_custom_fields_product_variation() { + // Create variable product with variation + $variable_product = WC_Helper_Product::create_variation_product(); + $variation = wc_get_product( $variable_product->get_children()[0] ); + + // Prepare Product and validate assertions + $facebook_product = new \WC_Facebook_Product( $variation ); + $product_data = $facebook_product->prepare_product( null, WC_Facebook_Product::PRODUCT_PREP_TYPE_ITEMS_BATCH ); + + $this->assertEquals( 'variation', $product_data['custom_fields']['woo_product_type'] ); + $this->assertEquals( array(), $product_data['custom_fields']['unmapped_attributes'] ); + $this->assertEquals( $product_data['custom_fields']['product_variation_attributes'], array( + 'pa_size' => 'small', + 'pa_colour' => '', + 'pa_number' => '' + ) ); + } + + public function test_prepare_product_items_batch() { // Test the PRODUCT_PREP_TYPE_ITEMS_BATCH preparation type $fb_description = 'Facebook specific description'; @@ -586,7 +640,7 @@ public function test_prepare_product_items_batch() { $this->assertArrayHasKey('image_link', $product_data); } - + /** * Test it gets rich text description from post meta. * @return void @@ -599,8 +653,8 @@ public function test_get_rich_text_description_from_post_meta() { $rich_text_description = $facebook_product->get_rich_text_description(); $this->assertEquals( $rich_text_description, 'rich text description' ); - } - + } + /** * Tests for get_rich_text_description() method */ @@ -609,7 +663,7 @@ public function test_get_rich_text_description() { $product = WC_Helper_Product::create_simple_product(); $facebook_product = new \WC_Facebook_Product($product); $facebook_product->set_description('fb description test'); - + $description = $facebook_product->get_rich_text_description(); $this->assertEquals('fb description test', $description); @@ -629,7 +683,7 @@ public function test_get_rich_text_description() { $variation = wc_get_product($variable_product->get_children()[0]); $variation->set_description('

variation description

'); $variation->save(); - + $parent_fb_product = new \WC_Facebook_Product($variable_product); $facebook_product = new \WC_Facebook_Product($variation, $parent_fb_product); $description = $facebook_product->get_rich_text_description(); @@ -639,7 +693,7 @@ public function test_get_rich_text_description() { $product = WC_Helper_Product::create_simple_product(); $product->set_description('

product content description

'); $product->save(); - + $facebook_product = new \WC_Facebook_Product($product); $description = $facebook_product->get_rich_text_description(); $this->assertEquals('

product content description

', $description); @@ -647,7 +701,7 @@ public function test_get_rich_text_description() { $product->set_description(''); $product->set_short_description('

short description test

'); $product->save(); - + $facebook_product = new \WC_Facebook_Product($product); $description = $facebook_product->get_rich_text_description(); $this->assertEquals('

short description test

', $description); @@ -656,13 +710,13 @@ public function test_get_rich_text_description() { $filter = $this->add_filter_with_safe_teardown('facebook_for_woocommerce_fb_rich_text_description', function($description) { return '

filtered description

'; }); - + $description = $facebook_product->get_rich_text_description(); $this->assertEquals('

filtered description

', $description); - + // Remove the filter early $filter->teardown_safely_immediately(); - + delete_option(WC_Facebookcommerce_Integration::SETTING_PRODUCT_DESCRIPTION_MODE); } @@ -696,7 +750,7 @@ public function test_rich_text_description_html_preservation() { $facebook_product->set_rich_text_description($html_content); $description = $facebook_product->get_rich_text_description(); - + // Test HTML structure is preserved $this->assertStringContainsString('
', $description); $this->assertStringContainsString('

', $description); @@ -715,15 +769,15 @@ public function test_rich_text_description_html_preservation() { public function test_empty_rich_text_description_fallback() { $product = WC_Helper_Product::create_simple_product(); $facebook_product = new \WC_Facebook_Product($product); - + // Ensure rich_text_description is empty $facebook_product->set_rich_text_description(''); - + // Test fallback to post meta update_post_meta($product->get_id(), \WC_Facebook_Product::FB_RICH_TEXT_DESCRIPTION, '

fallback description

'); $description = $facebook_product->get_rich_text_description(); $this->assertEquals('

fallback description

', $description); - + // Test behavior when both rich_text_description and post meta are empty delete_post_meta($product->get_id(), \WC_Facebook_Product::FB_RICH_TEXT_DESCRIPTION); $description = $facebook_product->get_rich_text_description(); @@ -737,36 +791,36 @@ public function test_rich_text_description_variants() { // Create variable product with variation $variable_product = WC_Helper_Product::create_variation_product(); $variation = wc_get_product($variable_product->get_children()[0]); - + // Set up parent product $parent_fb_product = new \WC_Facebook_Product($variable_product); - + // Set the rich text description using post meta for the parent update_post_meta($variable_product->get_id(), \WC_Facebook_Product::FB_RICH_TEXT_DESCRIPTION, '

parent rich text

'); - + // Test 1: Variation inherits parent's rich text description when empty $facebook_product = new \WC_Facebook_Product($variation, $parent_fb_product); $description = $facebook_product->get_rich_text_description(); $this->assertEquals('

parent rich text

', $description); - + // Test 2: Variation uses its own rich text description when set $variation_fb_product = new \WC_Facebook_Product($variation, $parent_fb_product); $variation_fb_product->set_rich_text_description('

variation rich text

'); $description = $variation_fb_product->get_rich_text_description(); $this->assertEquals('

variation rich text

', $description); - + // // Test 3: Variation uses its post meta when set // update_post_meta($variation->get_id(), \WC_Facebook_Product::FB_RICH_TEXT_DESCRIPTION, '

variation meta rich text

'); // $new_variation_product = new \WC_Facebook_Product($variation, $parent_fb_product); // $description = $new_variation_product->get_rich_text_description(); // $this->assertEquals('

variation meta rich text

', $description); - + // Test 4: Fallback chain for variations delete_post_meta($variation->get_id(), \WC_Facebook_Product::FB_RICH_TEXT_DESCRIPTION); } /** - * Test Brand is added for simple product + * Test Brand is added for simple product * @return void */ public function test_brand_for_simple_product_set() { @@ -782,7 +836,7 @@ public function test_brand_for_simple_product_set() { } /** - * Test MPN is added for simple product + * Test MPN is added for simple product * @return void */ public function test_mpn_for_simple_product_set() { @@ -798,7 +852,7 @@ public function test_mpn_for_simple_product_set() { } /** - * Test MPN is added for variable product + * Test MPN is added for variable product * @return void */ public function test_mpn_for_variable_product_set() { @@ -822,10 +876,10 @@ 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); - + // Set brand for parent product update_post_meta($variable_product->get_id(), \WC_Facebook_Product::FB_BRAND, 'Nike'); - + // Get the variation product $variation = wc_get_product($variable_product->get_children()[0]); @@ -853,7 +907,7 @@ public function test_get_fb_brand_variable_products() { private function create_product_attribute($name, $value, $is_taxonomy) { $attribute = new \WC_Product_Attribute(); $attribute->set_id(0); - + // Handle attribute names with spaces if ($is_taxonomy) { $name = strtolower(str_replace(' ', '-', $name)); @@ -861,15 +915,15 @@ private function create_product_attribute($name, $value, $is_taxonomy) { } else { $attribute->set_name($name); } - + if ($is_taxonomy) { // For taxonomy attributes $values = is_array($value) ? $value : [$value]; $term_ids = []; - + foreach ($values as $term_value) { $taxonomy = $attribute->get_name(); - + // Create the taxonomy if it doesn't exist if (!taxonomy_exists($taxonomy)) { register_taxonomy( @@ -883,7 +937,7 @@ private function create_product_attribute($name, $value, $is_taxonomy) { ] ); } - + // Create and get the term $term = wp_insert_term($term_value, $taxonomy); if (!is_wp_error($term)) { @@ -898,11 +952,11 @@ private function create_product_attribute($name, $value, $is_taxonomy) { $attribute->set_options($values); $attribute->is_taxonomy(false); } - + $attribute->set_position(0); $attribute->set_visible(1); $attribute->set_variation(0); - + return $attribute; } @@ -920,7 +974,7 @@ private function process_attributes_and_verify($product, $input_attributes, $exp ); $attributes[] = $attribute; } - + $product->set_attributes($attributes); $product->save(); @@ -954,14 +1008,14 @@ private function verify_saved_meta_values($product_id, $expected_output) { foreach ($meta_key_map as $field => $meta_key) { $saved_value = get_post_meta($product_id, $meta_key, true); - + if (!empty($expected_output[$field])) { // Get term name if it's a taxonomy term ID if (is_numeric($saved_value)) { $term = get_term($saved_value); $saved_value = $term ? $term->name : $saved_value; } - + $this->assertEquals( $expected_output[$field], $saved_value, @@ -1042,7 +1096,7 @@ public function test_external_update_time_unset() { $this->assertEquals(isset($data['external_update_time']), false); } - + /** * Tests for get_fb_short_description() method @@ -1051,48 +1105,48 @@ public function test_get_fb_short_description() { // Test 1: Variation products should inherit parent's short description $variable_product = WC_Helper_Product::create_variation_product(); $variation = wc_get_product($variable_product->get_children()[0]); - + // Set the parent product's short description $variable_product->set_short_description('parent short description'); $variable_product->save(); - + // Even if we try to set a short description on the variation (which we dont have functionality for in WooCommerce UI) $variation->set_short_description('variation short description - should be ignored'); $variation->save(); - + $parent_fb_product = new \WC_Facebook_Product($variable_product); $facebook_product = new \WC_Facebook_Product($variation, $parent_fb_product); $description = $facebook_product->get_fb_short_description(); - + // Variations should inherit the parent product's short description $this->assertEquals('parent short description', $description, 'Variations should inherit parent short description'); - + // Test 2: Gets short description from post excerpt for simple products $product = WC_Helper_Product::create_simple_product(); $product->set_short_description('product short description'); $product->save(); - + $facebook_product = new \WC_Facebook_Product($product); $description = $facebook_product->get_fb_short_description(); $this->assertEquals('product short description', $description); - + // Test 3: Returns empty string when no short description exists $product = WC_Helper_Product::create_simple_product(); $product->set_short_description(''); $product->save(); - + $facebook_product = new \WC_Facebook_Product($product); $description = $facebook_product->get_fb_short_description(); $this->assertEquals('', $description); - + // Test 4: Applies filters $filter = $this->add_filter_with_safe_teardown('facebook_for_woocommerce_fb_product_short_description', function($description, $id) { return 'filtered short description for product ' . $id; }, 10, 2); - + $description = $facebook_product->get_fb_short_description(); $this->assertEquals('filtered short description for product ' . $product->get_id(), $description); - + // Remove the filter early $filter->teardown_safely_immediately(); } @@ -1104,7 +1158,7 @@ public function test_get_fb_short_description_fallback_to_short_main_description // Arrange $product = WC_Helper_Product::create_simple_product(); $short_description = 'Short main description'; - + // Set up the test conditions $product->set_description($short_description); $product->set_short_description(''); // Ensure short description is empty @@ -1116,7 +1170,7 @@ public function test_get_fb_short_description_fallback_to_short_main_description // Assert $this->assertEquals( - $short_description, + $short_description, $result_description, 'Short main description should be used when excerpt is empty' ); @@ -1129,7 +1183,7 @@ public function test_get_fb_short_description_fallback_to_exact_length_main_desc // Arrange $product = WC_Helper_Product::create_simple_product(); $exact_length_description = str_repeat('a', 1000); - + // Set up the test conditions $product->set_description($exact_length_description); $product->set_short_description(''); // Ensure short description is empty @@ -1141,7 +1195,7 @@ public function test_get_fb_short_description_fallback_to_exact_length_main_desc // Assert $this->assertEquals( - $exact_length_description, + $exact_length_description, $result_description, 'Main description of exactly 1000 characters should be used when excerpt is empty' ); @@ -1159,7 +1213,7 @@ public function test_get_fb_short_description_no_fallback_to_long_main_descripti // Arrange $product = WC_Helper_Product::create_simple_product(); $too_long_description = str_repeat('a', 1001); - + // Set up the test conditions $product->set_description($too_long_description); $product->set_short_description(''); // Ensure short description is empty @@ -1171,7 +1225,7 @@ public function test_get_fb_short_description_no_fallback_to_long_main_descripti // Assert $this->assertEquals( - '', + '', $result_description, 'Should not fallback to main description if it exceeds 1000 characters' ); @@ -1196,7 +1250,7 @@ public function test_get_fb_short_description_prefers_short_over_main() { $product = WC_Helper_Product::create_simple_product(); $short_description = 'Short product description'; $main_description = 'Main product description that is also short'; - + // Set up both descriptions $product->set_description($main_description); $product->set_short_description($short_description); @@ -1208,11 +1262,11 @@ public function test_get_fb_short_description_prefers_short_over_main() { // Assert $this->assertEquals( - $short_description, + $short_description, $result_description, 'Short description should be used when available, regardless of main description length' ); - + // Verify we're not using the main description $this->assertNotEquals( $main_description, @@ -1227,7 +1281,7 @@ public function test_get_fb_short_description_prefers_short_over_main() { public function test_get_unmapped_attributes_no_attributes() { $product = WC_Helper_Product::create_simple_product(); $facebook_product = new \WC_Facebook_Product($product); - + $unmapped_attributes = $facebook_product->get_unmapped_attributes(); $this->assertIsArray($unmapped_attributes); $this->assertEmpty($unmapped_attributes); @@ -1238,7 +1292,7 @@ public function test_get_unmapped_attributes_no_attributes() { */ public function test_get_unmapped_attributes_only_mapped() { $product = WC_Helper_Product::create_simple_product(); - + // Add mapped attributes (size, color) $attributes = array(); $attributes[] = $this->create_product_attribute('size', 'Large', false); @@ -1248,7 +1302,7 @@ public function test_get_unmapped_attributes_only_mapped() { $facebook_product = new \WC_Facebook_Product($product); $unmapped_attributes = $facebook_product->get_unmapped_attributes(); - + $this->assertIsArray($unmapped_attributes); $this->assertEmpty($unmapped_attributes); } @@ -1258,7 +1312,7 @@ public function test_get_unmapped_attributes_only_mapped() { */ public function test_get_unmapped_attributes_only_unmapped() { $product = WC_Helper_Product::create_simple_product(); - + // Add unmapped attributes $attributes = array(); $attributes[] = $this->create_product_attribute('weight', '2kg', false); @@ -1268,14 +1322,14 @@ public function test_get_unmapped_attributes_only_unmapped() { $facebook_product = new \WC_Facebook_Product($product); $unmapped_attributes = $facebook_product->get_unmapped_attributes(); - + $this->assertIsArray($unmapped_attributes); $this->assertCount(2, $unmapped_attributes); - + // Verify first unmapped attribute $this->assertEquals('weight', $unmapped_attributes[0]['name']); $this->assertEquals('2kg', $unmapped_attributes[0]['value']); - + // Verify second unmapped attribute $this->assertEquals('style', $unmapped_attributes[1]['name']); $this->assertEquals('Modern', $unmapped_attributes[1]['value']); @@ -1286,7 +1340,7 @@ public function test_get_unmapped_attributes_only_unmapped() { */ public function test_get_unmapped_attributes_mixed() { $product = WC_Helper_Product::create_simple_product(); - + // Add both mapped and unmapped attributes $attributes = array(); $attributes[] = $this->create_product_attribute('size', 'Medium', false); // mapped @@ -1298,10 +1352,10 @@ public function test_get_unmapped_attributes_mixed() { $facebook_product = new \WC_Facebook_Product($product); $unmapped_attributes = $facebook_product->get_unmapped_attributes(); - + $this->assertIsArray($unmapped_attributes); $this->assertCount(2, $unmapped_attributes); - + // Verify only unmapped attributes are returned $this->assertEquals('weight', $unmapped_attributes[0]['name']); $this->assertEquals('3kg', $unmapped_attributes[0]['value']); @@ -1314,7 +1368,7 @@ public function test_get_unmapped_attributes_mixed() { */ public function test_get_unmapped_attributes_empty_values() { $product = WC_Helper_Product::create_simple_product(); - + // Add attributes with empty values $attributes = array(); $attributes[] = $this->create_product_attribute('weight', '', false); // empty unmapped @@ -1325,10 +1379,10 @@ public function test_get_unmapped_attributes_empty_values() { $facebook_product = new \WC_Facebook_Product($product); $unmapped_attributes = $facebook_product->get_unmapped_attributes(); - + $this->assertIsArray($unmapped_attributes); $this->assertCount(1, $unmapped_attributes); - + // Verify only non-empty unmapped attribute is returned $this->assertEquals('style', $unmapped_attributes[0]['name']); $this->assertEquals('Modern', $unmapped_attributes[0]['value']); From e18f6333d6a79bd9de064c00a8e183e8ebae8e81 Mon Sep 17 00:00:00 2001 From: Carter Buce Date: Tue, 22 Apr 2025 19:01:40 -0700 Subject: [PATCH 4/4] include 'custom_fields' column in product feed --- includes/fbproductfeed.php | 69 +++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/includes/fbproductfeed.php b/includes/fbproductfeed.php index 2c7d9bb8b..65feeb072 100644 --- a/includes/fbproductfeed.php +++ b/includes/fbproductfeed.php @@ -387,7 +387,7 @@ public function get_product_feed_header_row() { 'additional_image_link,sale_price_effective_date,sale_price,condition,' . 'visibility,gender,color,size,pattern,google_product_category,default_product,'. 'variant,gtin,quantity_to_sell_on_facebook,rich_text_description,external_update_time,'. - 'external_variant_id'. PHP_EOL; + 'external_variant_id,custom_fields'. PHP_EOL; } @@ -478,16 +478,16 @@ private function prepare_product_for_feed( $woo_product, &$attribute_variants ) // If this group has default variant value, log this product item if ( isset( $parent_attribute_values['default_variant_id'] ) && ! empty( $parent_attribute_values['default_variant_id'] ) ) { - $this->has_default_product_count++; + $this->has_default_product_count ++; } else { - $this->no_default_product_count++; + $this->no_default_product_count ++; } } // log simple product if ( ! isset( $product_data['default_product'] ) ) { - $this->no_default_product_count++; + $this->no_default_product_count ++; $product_data['default_product'] = ''; } @@ -509,36 +509,37 @@ private function prepare_product_for_feed( $woo_product, &$attribute_variants ) } return $product_data['retailer_id'] . ',' . - static::format_string_for_feed( static::get_value_from_product_data( $product_data, 'name' ) ) . ',' . - static::format_string_for_feed( static::get_value_from_product_data( $product_data, 'description' ) ) . ',' . - static::get_value_from_product_data( $product_data, 'image_url' ) . ',' . - static::get_value_from_product_data( $product_data, 'url' ) . ',' . - static::format_string_for_feed( static::get_value_from_product_data( $product_data, 'product_type' ) ) . ',' . - static::format_string_for_feed( static::get_value_from_product_data( $product_data, 'brand' ) ) . ',' . - static::format_string_for_feed( static::format_price_for_feed( - static::get_value_from_product_data( $product_data, 'price', 0 ), - static::get_value_from_product_data( $product_data, 'currency' ) - )) . ',' . - static::format_string_for_feed( static::get_value_from_product_data( $product_data, 'availability' ) ) . ',' . - $item_group_id . ',' . - static::get_value_from_product_data( $product_data, 'checkout_url' ) . ',' . - static::format_additional_image_url( static::get_value_from_product_data( $product_data, 'additional_image_urls' ) ) . ',' . - static::format_string_for_feed( $sale_price_effective_date ) . ',' . - static::format_string_for_feed( $sale_price ) . ',' . - 'new' . ',' . - static::format_string_for_feed( static::get_value_from_product_data( $product_data, 'visibility' )) . ',' . - static::format_string_for_feed( static::get_value_from_product_data( $product_data, 'gender' )) . ',' . - static::format_string_for_feed( static::get_value_from_product_data( $product_data, 'color' )) . ',' . - static::format_string_for_feed( static::get_value_from_product_data( $product_data, 'size' )) . ',' . - static::format_string_for_feed( static::get_value_from_product_data( $product_data, 'pattern' )) . ',' . - static::format_string_for_feed( static::get_value_from_product_data( $product_data, 'google_product_category' )) . ',' . - static::format_string_for_feed( static::get_value_from_product_data( $product_data, 'default_product' )) . ',' . - static::format_string_for_feed( static::get_value_from_product_data( $product_data, 'variant' )) . ',' . - static::format_string_for_feed( static::get_value_from_product_data( $product_data, 'gtin' )) . ',' . - static::format_string_for_feed( static::get_value_from_product_data( $product_data, 'quantity_to_sell_on_facebook' )) . ',' . - static::format_string_for_feed( static::get_value_from_product_data( $product_data, 'rich_text_description' ) ) . ',' . - static::get_value_from_product_data( $product_data, 'external_update_time' ) . ',' . - static::get_value_from_product_data( $product_data, 'external_variant_id' ) . PHP_EOL; + static::format_string_for_feed( static::get_value_from_product_data( $product_data, 'name' ) ) . ',' . + static::format_string_for_feed( static::get_value_from_product_data( $product_data, 'description' ) ) . ',' . + static::get_value_from_product_data( $product_data, 'image_url' ) . ',' . + static::get_value_from_product_data( $product_data, 'url' ) . ',' . + static::format_string_for_feed( static::get_value_from_product_data( $product_data, 'product_type' ) ) . ',' . + static::format_string_for_feed( static::get_value_from_product_data( $product_data, 'brand' ) ) . ',' . + static::format_string_for_feed( static::format_price_for_feed( + static::get_value_from_product_data( $product_data, 'price', 0 ), + static::get_value_from_product_data( $product_data, 'currency' ) + ) ) . ',' . + static::format_string_for_feed( static::get_value_from_product_data( $product_data, 'availability' ) ) . ',' . + $item_group_id . ',' . + static::get_value_from_product_data( $product_data, 'checkout_url' ) . ',' . + static::format_additional_image_url( static::get_value_from_product_data( $product_data, 'additional_image_urls' ) ) . ',' . + static::format_string_for_feed( $sale_price_effective_date ) . ',' . + static::format_string_for_feed( $sale_price ) . ',' . + 'new' . ',' . + static::format_string_for_feed( static::get_value_from_product_data( $product_data, 'visibility' ) ) . ',' . + static::format_string_for_feed( static::get_value_from_product_data( $product_data, 'gender' ) ) . ',' . + static::format_string_for_feed( static::get_value_from_product_data( $product_data, 'color' ) ) . ',' . + static::format_string_for_feed( static::get_value_from_product_data( $product_data, 'size' ) ) . ',' . + static::format_string_for_feed( static::get_value_from_product_data( $product_data, 'pattern' ) ) . ',' . + static::format_string_for_feed( static::get_value_from_product_data( $product_data, 'google_product_category' ) ) . ',' . + static::format_string_for_feed( static::get_value_from_product_data( $product_data, 'default_product' ) ) . ',' . + static::format_string_for_feed( static::get_value_from_product_data( $product_data, 'variant' ) ) . ',' . + static::format_string_for_feed( static::get_value_from_product_data( $product_data, 'gtin' ) ) . ',' . + static::format_string_for_feed( static::get_value_from_product_data( $product_data, 'quantity_to_sell_on_facebook' ) ) . ',' . + static::format_string_for_feed( static::get_value_from_product_data( $product_data, 'rich_text_description' ) ) . ',' . + static::get_value_from_product_data( $product_data, 'external_update_time' ) . ',' . + static::get_value_from_product_data( $product_data, 'external_variant_id' ) . ',' . + static::get_value_from_product_data( $product_data, 'custom_fields' ) . PHP_EOL; } private static function format_additional_image_url( $product_image_urls ) {