From dffec2c7ea610ac02507724c9c365ff4ce99fcad Mon Sep 17 00:00:00 2001 From: Rahul Raina Date: Thu, 28 Sep 2023 14:49:50 +0800 Subject: [PATCH 1/7] 1. Add support for video in catalogs sync 2. Use Batch API for single product sync instead of ProductItem API --- facebook-commerce.php | 48 ++++++++++++- includes/Products/Sync/Background.php | 100 ++------------------------ includes/fbproduct.php | 33 +++++++++ includes/fbutils.php | 93 ++++++++++++++++++++++++ 4 files changed, 178 insertions(+), 96 deletions(-) diff --git a/facebook-commerce.php b/facebook-commerce.php index 53d2cef57..adfb170be 100644 --- a/facebook-commerce.php +++ b/facebook-commerce.php @@ -16,6 +16,7 @@ use WooCommerce\Facebook\Framework\Plugin\Exception as PluginException; use WooCommerce\Facebook\Products; use WooCommerce\Facebook\Products\Feed; +use WooCommerce\Facebook\Products\Sync; defined( 'ABSPATH' ) || exit; @@ -1175,7 +1176,7 @@ public function on_simple_product_publish( $wp_id, $woo_product = null, &$parent if ( $fb_product_item_id ) { $woo_product->fb_visibility = Products::is_product_visible( $woo_product->woo_product ); - $this->update_product_item( $woo_product, $fb_product_item_id ); + $this->update_product_item_batch_api( $woo_product, $fb_product_item_id ); return $fb_product_item_id; } else { // Check if this is a new product item for an existing product group @@ -1465,6 +1466,51 @@ private function get_product_variation_attributes( array $variation ): array { return $final_attributes; } + /** + * Update existing product using batch API. + * + * @param WC_Facebook_Product $woo_product + * @param string $fb_product_item_id + * @return void + */ + public function update_product_item_batch_api( WC_Facebook_Product $woo_product, string $fb_product_item_id ): void { + $product = $woo_product-> prepare_product(null, \WC_Facebook_Product::PRODUCT_PREP_TYPE_ITEMS_BATCH ); + $product['item_group_id'] = $product['retailer_id']; + $product_data = WC_Facebookcommerce_Utils::normalize_product_data_for_items_batch( $product ); + + // extract the retailer_id + $retailer_id = $product_data['retailer_id']; + + // NB: Changing this to get items_batch to work + // retailer_id cannot be included in the data object + unset( $product_data['retailer_id'] ); + $product_data['id'] = $retailer_id; + + $requests = array([ + 'method' => Sync::ACTION_UPDATE, + 'data' => $product_data, + ]); + + try { + $facebook_catalog_id = facebook_for_woocommerce()->get_integration()->get_product_catalog_id(); + $response = facebook_for_woocommerce()->get_api()->send_item_updates( $facebook_catalog_id, $requests ); + if ( $response->handles ) { + $this->display_success_message( + 'Updated product ' . $fb_product_item_id . ' on Facebook.' + ); + } else { + $this->display_error_message( + 'Updated product ' . $fb_product_item_id . ' on Facebook has failed.' + ); + } + } catch ( ApiException $e ) { + $message = sprintf( 'There was an error trying to update a product item: %s', $e->getMessage() ); + WC_Facebookcommerce_Utils::log( $message ); + } + } + /** * Update existing product. * diff --git a/includes/Products/Sync/Background.php b/includes/Products/Sync/Background.php index 0858fe524..cd1432c14 100644 --- a/includes/Products/Sync/Background.php +++ b/includes/Products/Sync/Background.php @@ -185,9 +185,9 @@ private function process_item_update( $prefixed_product_id ) { if ( ! Products::product_should_be_deleted( $product ) && Products::product_should_be_synced( $product ) ) { if ( $product->is_type( 'variation' ) ) { - $product_data = $this->prepare_product_variation_data( $product ); + $product_data = \WC_Facebookcommerce_Utils::prepare_product_variation_data_items_batch( $product ); } else { - $product_data = $this->prepare_product_data( $product ); + $product_data = \WC_Facebookcommerce_Utils::prepare_product_data_items_batch( $product ); } // extract the retailer_id @@ -217,98 +217,6 @@ private function process_item_update( $prefixed_product_id ) { return $request; } - /** - * Prepares the data for a product variation to be included in a sync request. - * - * @since 2.0.0 - * - * @param \WC_Product $product product object - * @return array - * @throws PluginException In case no product found. - */ - private function prepare_product_variation_data( $product ) { - $parent_product = wc_get_product( $product->get_parent_id() ); - - if ( ! $parent_product instanceof \WC_Product ) { - throw new PluginException( "No parent product found with ID equal to {$product->get_parent_id()}." ); - } - - $fb_parent_product = new \WC_Facebook_Product( $parent_product->get_id() ); - $fb_product = new \WC_Facebook_Product( $product->get_id(), $fb_parent_product ); - - $data = $fb_product->prepare_product( null, \WC_Facebook_Product::PRODUCT_PREP_TYPE_ITEMS_BATCH ); - - // product variations use the parent product's retailer ID as the retailer product group ID - // $data['retailer_product_group_id'] = \WC_Facebookcommerce_Utils::get_fb_retailer_id( $parent_product ); - $data['item_group_id'] = \WC_Facebookcommerce_Utils::get_fb_retailer_id( $parent_product ); - - return $this->normalize_product_data( $data ); - } - - /** - * Normalizes product data to be included in a sync request. /items_batch - * rather than /batch this time. - * - * @since 2.0.0 - * - * @param array $data product data. - * @return array - */ - private function normalize_product_data( $data ) { - // Allowed values are 'refurbished', 'used', and 'new', but the plugin has always used the latter. - $data['condition'] = 'new'; - // Attributes other than size, color, pattern, or gender need to be included in the additional_variant_attributes field. - if ( isset( $data['custom_data'] ) && is_array( $data['custom_data'] ) ) { - $attributes = []; - foreach ( $data['custom_data'] as $key => $val ) { - - /** - * Filter: facebook_for_woocommerce_variant_attribute_comma_replacement - * - * The Facebook API expects a comma-separated list of attributes in `additional_variant_attribute` field. - * https://developers.facebook.com/docs/marketing-api/catalog/reference/ - * This means that WooCommerce product attributes included in this field should avoid the comma (`,`) character. - * Facebook for WooCommerce replaces any `,` with a space by default. - * This filter allows a site to provide a different replacement string. - * - * @since 2.5.0 - * - * @param string $replacement The default replacement string (`,`). - * @param string $value Attribute value. - * @return string Return the desired replacement string. - */ - $attribute_value = str_replace( - ',', - apply_filters( 'facebook_for_woocommerce_variant_attribute_comma_replacement', ' ', $val ), - $val - ); - /** Force replacing , and : characters if those were not cleaned up by filters */ - $attributes[] = str_replace( [ ',', ':' ], ' ', $key ) . ':' . str_replace( [ ',', ':' ], ' ', $attribute_value ); - } - - $data['additional_variant_attribute'] = implode( ',', $attributes ); - unset( $data['custom_data'] ); - } - - return $data; - } - - /** - * Prepares the product data to be included in a sync request. - * - * @since 2.0.0 - * - * @param \WC_Product $product product object - * @return array - */ - private function prepare_product_data( $product ) { - $fb_product = new \WC_Facebook_Product( $product->get_id() ); - $data = $fb_product->prepare_product( null, \WC_Facebook_Product::PRODUCT_PREP_TYPE_ITEMS_BATCH ); - // products that are not variations use their retailer retailer ID as the retailer product group ID - $data['item_group_id'] = $data['retailer_id']; - return $this->normalize_product_data( $data ); - } - /** * Processes a DELETE sync request for the given product. * @@ -342,7 +250,9 @@ private function process_item_delete( $prefixed_retailer_id ) { private function send_item_updates( array $requests ): array { $facebook_catalog_id = facebook_for_woocommerce()->get_integration()->get_product_catalog_id(); $response = facebook_for_woocommerce()->get_api()->send_item_updates( $facebook_catalog_id, $requests ); - $handles = ( isset( $response->handles ) && is_array( $response->handles ) ) ? $response->handles : []; + $response_handles = $response->handles; + // For some reason isset( $response->handles ) is false while isset( $response_handles is true + $handles = ( isset( $response_handles ) && is_array( $response_handles ) ) ? $response_handles : []; return $handles; } } diff --git a/includes/fbproduct.php b/includes/fbproduct.php index 1ad184bfa..ea84f9694 100644 --- a/includes/fbproduct.php +++ b/includes/fbproduct.php @@ -270,6 +270,29 @@ public function get_all_image_urls() { } + 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 = $video->guid; + 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. * @@ -583,6 +606,8 @@ public function prepare_product( $retailer_id = null, $type_to_prepare_for = sel } $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( '&%3B', @@ -625,6 +650,9 @@ public function prepare_product( $retailer_id = null, $type_to_prepare_for = sel ); $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() ), @@ -651,6 +679,11 @@ public function prepare_product( $retailer_id = null, $type_to_prepare_for = sel '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 diff --git a/includes/fbutils.php b/includes/fbutils.php index 64f81c57e..b07f09595 100644 --- a/includes/fbutils.php +++ b/includes/fbutils.php @@ -622,6 +622,99 @@ public static function get_cached_best_tip() { ); return $cached_best_tip; } + + /** + * Normalizes product data to be included in a sync request. /items_batch + * rather than /batch this time. + * + * @since 2.0.0 + * + * @param array $data product data. + * @return array + */ + public static function normalize_product_data_for_items_batch( $data ) { + // Allowed values are 'refurbished', 'used', and 'new', but the plugin has always used the latter. + $data['condition'] = 'new'; + // Attributes other than size, color, pattern, or gender need to be included in the additional_variant_attributes field. + if ( isset( $data['custom_data'] ) && is_array( $data['custom_data'] ) ) { + $attributes = []; + foreach ( $data['custom_data'] as $key => $val ) { + + /** + * Filter: facebook_for_woocommerce_variant_attribute_comma_replacement + * + * The Facebook API expects a comma-separated list of attributes in `additional_variant_attribute` field. + * https://developers.facebook.com/docs/marketing-api/catalog/reference/ + * This means that WooCommerce product attributes included in this field should avoid the comma (`,`) character. + * Facebook for WooCommerce replaces any `,` with a space by default. + * This filter allows a site to provide a different replacement string. + * + * @since 2.5.0 + * + * @param string $replacement The default replacement string (`,`). + * @param string $value Attribute value. + * @return string Return the desired replacement string. + */ + $attribute_value = str_replace( + ',', + apply_filters( 'facebook_for_woocommerce_variant_attribute_comma_replacement', ' ', $val ), + $val + ); + /** Force replacing , and : characters if those were not cleaned up by filters */ + $attributes[] = str_replace( [ ',', ':' ], ' ', $key ) . ':' . str_replace( [ ',', ':' ], ' ', $attribute_value ); + } + + $data['additional_variant_attribute'] = implode( ',', $attributes ); + unset( $data['custom_data'] ); + } + + return $data; + } + + /** + * Prepares the product data to be included in a sync request. + * + * @since 2.0.0 + * + * @param \WC_Product $product product object + * @return array + */ + public static function prepare_product_data_items_batch( $product ) { + $fb_product = new \WC_Facebook_Product( $product->get_id() ); + $data = $fb_product->prepare_product( null, \WC_Facebook_Product::PRODUCT_PREP_TYPE_ITEMS_BATCH ); + // products that are not variations use their retailer retailer ID as the retailer product group ID + $data['item_group_id'] = $data['retailer_id']; + return self::normalize_product_data_for_items_batch( $data ); + } + + /** + * Prepares the data for a product variation to be included in a sync request. + * + * @since 2.0.0 + * + * @param \WC_Product $product product object + * @return array + * @throws PluginException In case no product found. + */ + private function prepare_product_variation_data_items_batch( $product ) { + $parent_product = wc_get_product( $product->get_parent_id() ); + + if ( ! $parent_product instanceof \WC_Product ) { + throw new PluginException( "No parent product found with ID equal to {$product->get_parent_id()}." ); + } + + $fb_parent_product = new \WC_Facebook_Product( $parent_product->get_id() ); + $fb_product = new \WC_Facebook_Product( $product->get_id(), $fb_parent_product ); + + $data = $fb_product->prepare_product( null, \WC_Facebook_Product::PRODUCT_PREP_TYPE_ITEMS_BATCH ); + + // product variations use the parent product's retailer ID as the retailer product group ID + // $data['retailer_product_group_id'] = \WC_Facebookcommerce_Utils::get_fb_retailer_id( $parent_product ); + $data['item_group_id'] = \WC_Facebookcommerce_Utils::get_fb_retailer_id( $parent_product ); + + return self::normalize_product_data_for_items_batch( $data ); + } + } endif; From 7e68999ca5d1258a99c4a2b5374ca07479a5cc36 Mon Sep 17 00:00:00 2001 From: Rahul Raina Date: Tue, 17 Oct 2023 18:25:11 +0800 Subject: [PATCH 2/7] Change integration test --- includes/API.php | 2 +- tests/Unit/WCFacebookCommerceIntegrationTest.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/includes/API.php b/includes/API.php index 2efc2d455..ca807a4e7 100644 --- a/includes/API.php +++ b/includes/API.php @@ -589,7 +589,7 @@ public function update_plugin_version_configuration( string $external_business_i * @return API\Response|API\ProductCatalog\ItemsBatch\Create\Response * @throws ApiException */ - public function send_item_updates( string $facebook_product_catalog_id, array $requests ) { + public function send_item_updates( string $facebook_product_catalog_id, array $requests ): API\ProductCatalog\ItemsBatch\Create\Response { $request = new API\ProductCatalog\ItemsBatch\Create\Request( $facebook_product_catalog_id, $requests ); $this->set_response_handler( API\ProductCatalog\ItemsBatch\Create\Response::class ); return $this->perform_request( $request ); diff --git a/tests/Unit/WCFacebookCommerceIntegrationTest.php b/tests/Unit/WCFacebookCommerceIntegrationTest.php index 112fad037..6414b0663 100644 --- a/tests/Unit/WCFacebookCommerceIntegrationTest.php +++ b/tests/Unit/WCFacebookCommerceIntegrationTest.php @@ -823,9 +823,9 @@ public function test_on_product_publish_simple_product() { $facebook_product_data['additional_image_urls'] = ''; $this->api->expects( $this->once() ) - ->method( 'update_product_item' ) - ->with( 'facebook-product-item-id', $facebook_product_data ) - ->willReturn( new API\ProductCatalog\Products\Update\Response( '{"success":true}' ) ); + ->method( 'send_item_updates' ) + ->with( $this->anything() ) + ->willReturn( new API\ProductCatalog\ItemsBatch\Create\Response( '{"success":true}' ) ); $this->integration->on_product_publish( $product->get_id() ); } From d75272396f6ffd9921e676065e013463c62aba71 Mon Sep 17 00:00:00 2001 From: Bartosz Budzanowski Date: Tue, 17 Oct 2023 14:17:06 +0300 Subject: [PATCH 3/7] Remove type hint. --- includes/API.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/API.php b/includes/API.php index ca807a4e7..2efc2d455 100644 --- a/includes/API.php +++ b/includes/API.php @@ -589,7 +589,7 @@ public function update_plugin_version_configuration( string $external_business_i * @return API\Response|API\ProductCatalog\ItemsBatch\Create\Response * @throws ApiException */ - public function send_item_updates( string $facebook_product_catalog_id, array $requests ): API\ProductCatalog\ItemsBatch\Create\Response { + public function send_item_updates( string $facebook_product_catalog_id, array $requests ) { $request = new API\ProductCatalog\ItemsBatch\Create\Request( $facebook_product_catalog_id, $requests ); $this->set_response_handler( API\ProductCatalog\ItemsBatch\Create\Response::class ); return $this->perform_request( $request ); From 090697f6c85b186370473c4fab0ca944e4a115af Mon Sep 17 00:00:00 2001 From: Bartosz Budzanowski Date: Tue, 17 Oct 2023 14:32:29 +0300 Subject: [PATCH 4/7] Revert "Remove type hint." This reverts commit d75272396f6ffd9921e676065e013463c62aba71. --- includes/API.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/API.php b/includes/API.php index 2efc2d455..ca807a4e7 100644 --- a/includes/API.php +++ b/includes/API.php @@ -589,7 +589,7 @@ public function update_plugin_version_configuration( string $external_business_i * @return API\Response|API\ProductCatalog\ItemsBatch\Create\Response * @throws ApiException */ - public function send_item_updates( string $facebook_product_catalog_id, array $requests ) { + public function send_item_updates( string $facebook_product_catalog_id, array $requests ): API\ProductCatalog\ItemsBatch\Create\Response { $request = new API\ProductCatalog\ItemsBatch\Create\Request( $facebook_product_catalog_id, $requests ); $this->set_response_handler( API\ProductCatalog\ItemsBatch\Create\Response::class ); return $this->perform_request( $request ); From 298c4f11720966f8a6085f202e9d5163b304d70b Mon Sep 17 00:00:00 2001 From: Bartosz Budzanowski Date: Tue, 17 Oct 2023 14:33:46 +0300 Subject: [PATCH 5/7] Fix test_on_product_save_existing_simple_product_sync_enabled_updates_the_product --- tests/Unit/WCFacebookCommerceIntegrationTest.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/Unit/WCFacebookCommerceIntegrationTest.php b/tests/Unit/WCFacebookCommerceIntegrationTest.php index 6414b0663..7b7c5f50e 100644 --- a/tests/Unit/WCFacebookCommerceIntegrationTest.php +++ b/tests/Unit/WCFacebookCommerceIntegrationTest.php @@ -529,9 +529,8 @@ public function test_on_product_save_existing_simple_product_sync_enabled_update $facebook_product_data['category'] = 1718; $this->api->expects( $this->once() ) - ->method( 'update_product_item' ) - ->with( 'facebook-product-item-id', $facebook_product_data ) - ->willReturn( new API\ProductCatalog\Products\Update\Response( '{"success":true}' ) ); + ->method( 'update_product_item_batch_api' ) + ->with( 'facebook-product-item-id', $facebook_product_data ); $this->integration->on_product_save( $product_to_update->get_id() ); From 8506846accace070a398c13527bbeacef915b14e Mon Sep 17 00:00:00 2001 From: Bartosz Budzanowski Date: Tue, 17 Oct 2023 22:30:10 +0300 Subject: [PATCH 6/7] Revert "Fix test_on_product_save_existing_simple_product_sync_enabled_updates_the_product" This reverts commit 298c4f11720966f8a6085f202e9d5163b304d70b. --- tests/Unit/WCFacebookCommerceIntegrationTest.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/Unit/WCFacebookCommerceIntegrationTest.php b/tests/Unit/WCFacebookCommerceIntegrationTest.php index 7b7c5f50e..6414b0663 100644 --- a/tests/Unit/WCFacebookCommerceIntegrationTest.php +++ b/tests/Unit/WCFacebookCommerceIntegrationTest.php @@ -529,8 +529,9 @@ public function test_on_product_save_existing_simple_product_sync_enabled_update $facebook_product_data['category'] = 1718; $this->api->expects( $this->once() ) - ->method( 'update_product_item_batch_api' ) - ->with( 'facebook-product-item-id', $facebook_product_data ); + ->method( 'update_product_item' ) + ->with( 'facebook-product-item-id', $facebook_product_data ) + ->willReturn( new API\ProductCatalog\Products\Update\Response( '{"success":true}' ) ); $this->integration->on_product_save( $product_to_update->get_id() ); From 165e5a833935152bd4e029ebee46235472a5b47a Mon Sep 17 00:00:00 2001 From: Bartosz Budzanowski Date: Tue, 17 Oct 2023 22:30:39 +0300 Subject: [PATCH 7/7] use proper syntax --- facebook-commerce.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/facebook-commerce.php b/facebook-commerce.php index 7b6eae9d5..3f6aeff89 100644 --- a/facebook-commerce.php +++ b/facebook-commerce.php @@ -1498,8 +1498,8 @@ public function update_product_item_batch_api( WC_Facebook_Product $woo_product, ]); try { - $facebook_catalog_id = facebook_for_woocommerce()->get_integration()->get_product_catalog_id(); - $response = facebook_for_woocommerce()->get_api()->send_item_updates( $facebook_catalog_id, $requests ); + $facebook_catalog_id = $this->get_product_catalog_id(); + $response = $this->facebook_for_woocommerce->get_api()->send_item_updates( $facebook_catalog_id, $requests ); if ( $response->handles ) { $this->display_success_message( 'Updated product