Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Product Syncing via Feeds #2841

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions includes/API.php
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,18 @@ public function read_feed( string $product_feed_id ) {
return $this->perform_request( $request );
}

/**
* @param string $product_catalog_id Facebook Product Catalog ID.
* @return Response
* @throws ApiException
* @throws API\Exceptions\Request_Limit_Reached
*/
public function create_feed( string $product_catalog_id, array $data ) {
$request = new API\ProductCatalog\ProductFeeds\Create\Request( $product_catalog_id, $data );
$this->set_response_handler( API\ProductCatalog\ProductFeeds\Create\Response::class );
return $this->perform_request( $request );
}


/**
* @param string $product_feed_upload_id
Expand All @@ -529,6 +541,18 @@ public function read_upload( string $product_feed_upload_id ) {
return $this->perform_request( $request );
}

/**
* @param string $product_feed_id Facebook Product Feed ID.
* @return Response
* @throws ApiException
* @throws API\Exceptions\Request_Limit_Reached
*/
public function create_upload( string $product_feed_id, array $data ) {
$request = new API\ProductCatalog\ProductFeedUploads\Create\Request( $product_feed_id, $data );
$this->set_response_handler( API\ProductCatalog\ProductFeedUploads\Create\Response::class );
return $this->perform_request( $request );
}


/**
* @param string $external_merchant_settings_id
Expand Down
25 changes: 25 additions & 0 deletions includes/API/ProductCatalog/ProductFeedUploads/Create/Request.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php
declare( strict_types=1 );

namespace WooCommerce\Facebook\API\ProductCatalog\ProductFeedUploads\Create;

use WooCommerce\Facebook\API\Request as ApiRequest;

defined( 'ABSPATH' ) || exit;

/**
* Request object for Product Catalog > Product Feed Upload > Create Graph Api.
*
* @link https://developers.facebook.com/docs/marketing-api/reference/product-feed/uploads/#Creating
*/
class Request extends ApiRequest {

/**
* @param string $product_feed_id Facebook Product Feed ID.
* @param array $data Facebook Product Feed Data.
*/
public function __construct( string $product_feed_id, array $data ) {
parent::__construct( "/{$product_feed_id}/uploads", 'POST' );
parent::set_data( $data );
}
}
16 changes: 16 additions & 0 deletions includes/API/ProductCatalog/ProductFeedUploads/Create/Response.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php
declare( strict_types=1 );

namespace WooCommerce\Facebook\API\ProductCatalog\ProductFeedUploads\Create;

use WooCommerce\Facebook\API\Response as ApiResponse;

defined( 'ABSPATH' ) || exit;

/**
* Response object for Product Catalog > Product Feed Upload > Create Graph Api.
*
* @link https://developers.facebook.com/docs/marketing-api/reference/product-feed/uploads/#Creating
* @property-read array $data Facebook Product Feeds Upload.
*/
class Response extends ApiResponse {}
2 changes: 1 addition & 1 deletion includes/API/ProductCatalog/ProductFeeds/Read/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ class Request extends ApiRequest {
* @param string $product_feed_id Facebook Product Feed ID.
*/
public function __construct( string $product_feed_id ) {
parent::__construct( "/{$product_feed_id}/?fields=created_time,latest_upload,product_count,schedule,update_schedule", 'GET' );
parent::__construct( "/{$product_feed_id}/?fields=created_time,latest_upload,product_count,schedule,update_schedule,name", 'GET' );
}
}
2 changes: 2 additions & 0 deletions includes/Jobs/GenerateProductFeed.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ protected function handle_end() {
$feed_handler = new \WC_Facebook_Product_Feed();
$feed_handler->rename_temporary_feed_file_to_final_feed_file();
facebook_for_woocommerce()->get_tracker()->save_batch_generation_time();

do_action('wc_facebook_feed_generation_completed');
}

/**
Expand Down
194 changes: 194 additions & 0 deletions includes/Products/Feed.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@

defined( 'ABSPATH' ) || exit;

use Error;
use Exception;
use WC_Facebookcommerce_Utils;
use WooCommerce\Facebook\Framework\Helper;
use WooCommerce\Facebook\Utilities\Heartbeat;
use WooCommerce\Facebook\Framework\Plugin\Exception as PluginException;
Expand All @@ -36,6 +39,8 @@ class Feed {
/** @var string the WordPress option name where the secret included in the feed URL is stored */
const OPTION_FEED_URL_SECRET = 'wc_facebook_feed_url_secret';

/** @var string the feed name for creating a new feed by this plugin */
const FEED_NAME = 'Product Feed by Facebook for WooCommerce plugin. DO NOT DELETE.';

/**
* Feed constructor.
Expand All @@ -62,6 +67,9 @@ private function add_hooks() {

// handle the feed data request
add_action( 'woocommerce_api_' . self::REQUEST_FEED_ACTION, array( $this, 'handle_feed_data_request' ) );

// Send request fir feed one time upload after feed file generated
add_action( 'wc_facebook_feed_generation_completed', array( $this, 'send_request_to_upload_feed' ) );
}


Expand Down Expand Up @@ -179,6 +187,192 @@ public function schedule_feed_generation() {
}


/**
* Sends request to Meta to start a one-time feed file upload session.
*
* @internal
*/
public function send_request_to_upload_feed() {
$feed_id = self::retrieve_or_create_integration_feed_id();
if ($feed_id === null || $feed_id === '') {
WC_Facebookcommerce_Utils::log( 'Feed: integration feed ID is null or empty, feed will not be uploaded.' );
return;
}

$data = [
'url' => Feed::get_feed_data_url(),
];

try {
facebook_for_woocommerce()->get_api()->create_upload($feed_id, $data );
} catch ( Exception $exception ) {
facebook_for_woocommerce()->log( 'Could not send create feed upload via request: ' . $exception->getMessage() );
}
}

/**
* Retrieves or creates an integration feed ID
*
* @return string the integration feed ID
*
* @internal
*/
public function retrieve_or_create_integration_feed_id() {
// Step 1 - Get feed ID if it is already available in local cache
$feed_id = facebook_for_woocommerce()->get_integration()->get_feed_id();
if ($feed_id !== null && $feed_id !== '') {
if ( self::validate_feed_exists($feed_id) ) {
WC_Facebookcommerce_Utils::log( 'Feed: retrieve_integration_feed_id(): feed_id = '.$feed_id.', from local cache.');
return $feed_id;
} else {
WC_Facebookcommerce_Utils::log( 'Feed: retrieve_integration_feed_id(): feed_id = '.$feed_id.', from local cache was invalidated.');
}
}

// Step 2 - Query feeds data from Meta and filter the right one
$feed_id = self::query_and_filter_integration_feed_id();
if ($feed_id !== null && $feed_id !== '') {
facebook_for_woocommerce()->get_integration()->update_feed_id($feed_id);
WC_Facebookcommerce_Utils::log( 'Feed: retrieve_integration_feed_id(): feed_id = '.$feed_id.', queried and filtered from Meta API.');
return $feed_id;
}

// Step 3 - Create a new feed
$feed_id = self::create_feed_id();
if ($feed_id !== null && $feed_id !== '') {
facebook_for_woocommerce()->get_integration()->update_feed_id($feed_id);
WC_Facebookcommerce_Utils::log( 'Feed: retrieve_integration_feed_id(): feed_id = '.$feed_id.', created a new feed via Meta API.');
return $feed_id;
}

return '';
}

/**
* Validates that provided feed ID still exists on the Meta side
*
* @param string $feed_id the feed ID
*
* @return bool true if the feed ID is valid
*
* @internal
*/
private function validate_feed_exists($feed_id) {
$catalog_id = facebook_for_woocommerce()->get_integration()->get_product_catalog_id();
if ( '' === $catalog_id ) {
throw new Error( 'No catalog ID' );
}

try {
$feed_nodes = facebook_for_woocommerce()->get_api()->read_feeds( $catalog_id )->data;
} catch ( Exception $e ) {
$message = sprintf( 'There was an error trying to get feed nodes for catalog: %s', $e->getMessage() );
WC_Facebookcommerce_Utils::log( $message );
return '';
}

foreach ( $feed_nodes as $feed ) {
if ($feed['id'] == $feed_id) {
return true;
}
}

return false;
}

/**
* Queries existing feeds for the integration catalog and filters
* the plugin integration feed ID
*
* @return string the integration feed ID
*
* @internal
*/
private function query_and_filter_integration_feed_id() {
$catalog_id = facebook_for_woocommerce()->get_integration()->get_product_catalog_id();
if ( '' === $catalog_id ) {
throw new Error( 'No catalog ID' );
}

try {
$feed_nodes = facebook_for_woocommerce()->get_api()->read_feeds( $catalog_id )->data;
} catch ( Exception $e ) {
$message = sprintf( 'There was an error trying to get feed nodes for catalog: %s', $e->getMessage() );
WC_Facebookcommerce_Utils::log( $message );
return '';
}

if ( !empty( $feed_nodes ) ) {

try {
$catalog = facebook_for_woocommerce()->get_api()->get_catalog( $catalog_id );
} catch ( Exception $e ) {
$message = sprintf( 'There was an error trying to get a catalog: %s', $e->getMessage() );
WC_Facebookcommerce_Utils::log( $message );
}

/*
We need to detect which feed is the one that was created for Facebook for WooCommerce plugin usage.

We are detecting based on the name.
- Option 1. Plugin can create this feed name currently.
- Option 2 and 3. FBE creates a catalog with feed name '{catalog name} - Feed' or '{catalog name} – Feed' (short vs long dash)
- Option 4. Plugin used to create a feed name 'Initial product sync from WooCommerce. DO NOT DELETE.'
*/
foreach ( $feed_nodes as $feed ) {
try {
$feed_metadata = facebook_for_woocommerce()->get_api()->read_feed( $feed['id'] );
} catch ( Exception $e ) {
$message = sprintf( 'There was an error trying to get feed metadata: %s', $e->getMessage() );
WC_Facebookcommerce_Utils::log( $message );
continue;
}

$woo_feed_name_option_1 = self::FEED_NAME;
$woo_feed_name_option_2 = sprintf( '%s - Feed', $catalog['name'] );
$woo_feed_name_option_3 = sprintf( '%s – Feed', $catalog['name'] );
$woo_feed_name_option_4 = 'Initial product sync from WooCommerce. DO NOT DELETE.';

if ( $feed_metadata['name'] === $woo_feed_name_option_1 ||
$feed_metadata['name'] === $woo_feed_name_option_2 ||
$feed_metadata['name'] === $woo_feed_name_option_3 ||
$feed_metadata['name'] === $woo_feed_name_option_4 ) {
return $feed['id'];
}
}
}

return '';
}

/**
* Makes a request to Meta to create a new feed
*
* @return string the integration feed ID
*
* @internal
*/
private function create_feed_id() {
try {
$catalog_id = facebook_for_woocommerce()->get_integration()->get_product_catalog_id();
if ( '' === $catalog_id ) {
throw new Error( 'No catalog ID' );
}

$data = [
'name' => self::FEED_NAME,
];

$feed = facebook_for_woocommerce()->get_api()->create_feed( $catalog_id, $data );
return $feed['id'];
} catch ( Exception $exception ) {
facebook_for_woocommerce()->log( 'Could not create a feed: ' . $exception->getMessage() );
}

return '';
}


/**
* Checks whether fpassthru has been disabled in PHP.
*
Expand Down
2 changes: 1 addition & 1 deletion includes/fbproduct.php
Original file line number Diff line number Diff line change
Expand Up @@ -766,7 +766,7 @@ public function prepare_product( $retailer_id = null, $type_to_prepare_for = sel
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'] )
WC_Facebookcommerce_Utils::is_all_caps( $product_data['description'] )
) {
$product_data['description'] =
mb_strtolower( $product_data['description'] );
Expand Down
9 changes: 6 additions & 3 deletions includes/fbproductfeed.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ class WC_Facebook_Product_Feed {
const FILE_NAME = 'product_catalog_%s.csv';
const FACEBOOK_CATALOG_FEED_FILENAME = 'fae_product_catalog.csv';
const FB_ADDITIONAL_IMAGES_FOR_FEED = 5;
const FEED_NAME = 'Initial product sync from WooCommerce. DO NOT DELETE.';
const FB_PRODUCT_GROUP_ID = 'fb_product_group_id';
const FB_VISIBILITY = 'fb_visibility';

Expand Down Expand Up @@ -59,6 +58,8 @@ public function generate_feed() {

\WC_Facebookcommerce_Utils::log( 'Product feed file generated' );

do_action('wc_facebook_feed_generation_completed');

} catch ( \Exception $exception ) {

\WC_Facebookcommerce_Utils::log( $exception->getMessage() );
Expand Down Expand Up @@ -374,7 +375,7 @@ public function get_product_feed_header_row() {
return 'id,title,description,image_link,link,product_type,' .
'brand,price,availability,item_group_id,checkout_url,' .
'additional_image_link,sale_price_effective_date,sale_price,condition,' .
'visibility,gender,color,size,pattern,google_product_category,default_product,variant' . PHP_EOL;
'visibility,gender,color,size,pattern,google_product_category,default_product,variant,gtin,quantity_to_sell_on_facebook' . PHP_EOL;
}


Expand Down Expand Up @@ -520,7 +521,9 @@ private function prepare_product_for_feed( $woo_product, &$attribute_variants )
static::get_value_from_product_data( $product_data, 'pattern' ) . ',' .
static::get_value_from_product_data( $product_data, 'google_product_category' ) . ',' .
static::get_value_from_product_data( $product_data, 'default_product' ) . ',' .
static::get_value_from_product_data( $product_data, 'variant' ) . PHP_EOL;
static::get_value_from_product_data( $product_data, 'variant' ) . ',' .
static::get_value_from_product_data( $product_data, 'gtin' ) . ',' .
static::get_value_from_product_data( $product_data, 'quantity_to_sell_on_facebook' ) . PHP_EOL;
}

private static function format_additional_image_url( $product_image_urls ) {
Expand Down
Loading
Loading