788 lines
29 KiB
PHP
788 lines
29 KiB
PHP
<?php
|
|
/**
|
|
* Copyright (c) Bytedance, Inc. and its affiliates. All Rights Reserved
|
|
*
|
|
* This source code is licensed under the license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @package TikTok
|
|
*/
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
die;
|
|
}
|
|
|
|
require_once __DIR__ . '/../utils/TBPApi.php';
|
|
require_once __DIR__ . '/../utils/utilities.php';
|
|
require_once __DIR__ . '/TikTokProductsController.php';
|
|
|
|
class Tt4b_Catalog_Class {
|
|
|
|
|
|
|
|
/**
|
|
* The TikTok Mapi Class used to make various requests to TikTok
|
|
*
|
|
* @var Tt4b_Mapi_Class
|
|
*/
|
|
protected $mapi;
|
|
|
|
/**
|
|
* The woocommerce logger
|
|
*
|
|
* @var Logger
|
|
*/
|
|
protected $logger;
|
|
|
|
/**
|
|
* Custom way to pull Woo products
|
|
*
|
|
* @var TikTokProductsController
|
|
*/
|
|
protected $tikTokProductsController;
|
|
|
|
/**
|
|
* Week in seconds - used to determine if it's been at least a week since the last full sync
|
|
*/
|
|
const WEEK = 604800;
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param Tt4b_Mapi_Class $mapi The Tt4b_Mapi_Class
|
|
* @param Logger $logger
|
|
*
|
|
* @return void
|
|
*/
|
|
public function __construct( Tt4b_Mapi_Class $mapi, Logger $logger ) {
|
|
$this->mapi = $mapi;
|
|
$this->logger = $logger;
|
|
$this->tikTokProductsController = new TikTokProductsController();
|
|
}
|
|
|
|
/**
|
|
* Initializes actions related to Tt4b_Catalog_Class such as catalog sync functionality used by action_scheduler
|
|
*
|
|
* @return void
|
|
*/
|
|
public function init() {
|
|
add_action( 'tt4b_catalog_sync_helper', array( $this, 'catalog_sync_helper' ), 2, 9 );
|
|
add_action( 'tt4b_catalog_sync', array( $this, 'catalog_sync' ), 1, 4 );
|
|
add_action( 'tt4b_delete_products_helper', array( $this, 'delete_products_helper' ), 1, 8 );
|
|
add_action( 'tt4b_variation_sync_helper', array( $this, 'variation_sync_helper' ), 2, 5 );
|
|
}
|
|
|
|
/**
|
|
* Returns the amount of catalog items are in approved/processing/rejected.
|
|
*
|
|
* @param string $access_token The MAPI issued access token
|
|
* @param string $bc_id The users business center ID
|
|
* @param string $catalog_id The users catalog ID
|
|
*
|
|
* @return array(processing, approved, rejected)
|
|
*/
|
|
public static function get_catalog_processing_status(
|
|
$access_token,
|
|
$bc_id,
|
|
$catalog_id
|
|
) {
|
|
// returns a counter of how many items are approved, processing, or rejected
|
|
// from the TikTok catalog/product/get/ endpoint
|
|
$logger = new Logger();
|
|
$mapi = new Tt4b_Mapi_Class( $logger );
|
|
|
|
$url = 'catalog/overview/';
|
|
$params = array(
|
|
'bc_id' => $bc_id,
|
|
'catalog_id' => $catalog_id,
|
|
);
|
|
$base = array(
|
|
'processing' => 0,
|
|
'approved' => 0,
|
|
'rejected' => 0,
|
|
);
|
|
|
|
$result = $mapi->mapi_get( $url, $access_token, $params, 'v1.2' );
|
|
$obj = json_decode( $result, true );
|
|
|
|
if ( ! isset( $obj['data'] ) ) {
|
|
$logger->log( __METHOD__, 'get_catalog_processing_status data not set' );
|
|
return $base;
|
|
}
|
|
|
|
if ( 'OK' !== $obj['message'] ) {
|
|
$logger->log( __METHOD__, 'get_catalog_processing_status not OK response' );
|
|
return $base;
|
|
}
|
|
|
|
$processing = $obj['data']['processing'];
|
|
$approved = $obj['data']['approved'];
|
|
$rejected = $obj['data']['rejected'];
|
|
|
|
return array(
|
|
'processing' => $processing,
|
|
'approved' => $approved,
|
|
'rejected' => $rejected,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Begins catalog sync, if there is not one currently enqueued. Schedules recurring catalog sync on an hourly basis.
|
|
*
|
|
* @param string $catalog_id The users catalog ID
|
|
* @param string $bc_id The users business center ID
|
|
* @param string $store_name The users store name
|
|
* @param string $access_token The MAPI issued access token
|
|
*
|
|
* @return void
|
|
*/
|
|
public function initiate_catalog_sync( $catalog_id, $bc_id, $store_name, $access_token ) {
|
|
// check for woo install
|
|
if ( ! did_action( 'woocommerce_loaded' ) > 0 ) {
|
|
return;
|
|
}
|
|
|
|
$tt4b_catalog_sync_payload = array(
|
|
'catalog_id' => $catalog_id,
|
|
'bc_id' => $bc_id,
|
|
'store_name' => $store_name,
|
|
'access_token' => $access_token,
|
|
);
|
|
|
|
if ( false === as_has_scheduled_action(
|
|
'tt4b_catalog_sync'
|
|
)
|
|
) {
|
|
as_schedule_cron_action(
|
|
'today',
|
|
$this->generate_cron_string(),
|
|
'tt4b_catalog_sync',
|
|
$tt4b_catalog_sync_payload,
|
|
'tt4b_daily_catalog_sync'
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sync merchant catalog from woocommerce store to TikTok catalog manager via creation of catalog_sync_helper functions for batches of products
|
|
*
|
|
* @param string $catalog_id The users catalog ID
|
|
* @param string $bc_id The users business center ID
|
|
* @param string $store_name The users store name
|
|
* @param string $access_token The MAPI issued access token
|
|
*
|
|
* @return void
|
|
*/
|
|
public function catalog_sync( $catalog_id, $bc_id, $store_name, $access_token ) {
|
|
// check for woo install
|
|
if ( ! did_action( 'woocommerce_loaded' ) > 0 ) {
|
|
return;
|
|
}
|
|
$this->logger->log( __METHOD__, "catalog_sync executing for $store_name" );
|
|
|
|
if ( '' === $catalog_id ) {
|
|
$this->logger->log( __METHOD__, 'missing catalog_id for full catalog sync' );
|
|
return;
|
|
}
|
|
if ( '' === $bc_id ) {
|
|
$this->logger->log( __METHOD__, 'missing bc_id for full catalog sync' );
|
|
return;
|
|
}
|
|
if ( '' === $access_token || false === $access_token ) {
|
|
$this->logger->log( __METHOD__, 'missing access token for full catalog sync' );
|
|
return;
|
|
}
|
|
// store_name just used for brand, can default it.
|
|
if ( '' === $store_name ) {
|
|
$store_name = 'WOO_COMMERCE';
|
|
}
|
|
$timeForSync = get_option( 'tt4b_last_product_sync_time' );
|
|
$lastFullSync = get_option( 'tt4b_last_full_sync_time', 1 );
|
|
$reconciliation_sync = false;
|
|
$reconciliation_sync_time = $timeForSync;
|
|
// if the time elapsed since the last full sync is over a week, initiate a full catalog sync
|
|
if ( time() - $lastFullSync >= self::WEEK ) {
|
|
$timeForSync = 1;
|
|
$reconciliation_sync = true;
|
|
$this->logger->log( __METHOD__, 'one week since last full sync, initiating full catalog sync' );
|
|
update_option( 'tt4b_last_full_sync_time', time() );
|
|
}
|
|
update_option( 'tt4b_last_product_sync_time', time() );
|
|
$args = array(
|
|
'date_modified' => '>=' . $timeForSync,
|
|
'paginate' => true,
|
|
'limit' => 100,
|
|
);
|
|
$result = wc_get_products( $args );
|
|
$pages = $result->max_num_pages;
|
|
$tt4b_catalog_sync_helper_payload = array(
|
|
'catalog_id' => $catalog_id,
|
|
'bc_id' => $bc_id,
|
|
'store_name' => $store_name,
|
|
'access_token' => $access_token,
|
|
'page_total' => $pages,
|
|
'last_catalog_sync' => $timeForSync,
|
|
'reconciliation_sync' => $reconciliation_sync,
|
|
'reconciliation_sync_time' => $reconciliation_sync_time,
|
|
);
|
|
$formatted_last_catalog_sync = wp_date( 'j F Y H:i:s', $timeForSync );
|
|
$this->logger->log( __METHOD__, "deleting, adding, and updating products from wc_get_products since $formatted_last_catalog_sync" );
|
|
self::check_and_start_async_action( 'tt4b_delete_products_helper', $tt4b_catalog_sync_helper_payload, '' );
|
|
}
|
|
|
|
/**
|
|
* Helper function used to delete products on tiktok catalog manager that have been removed (trashed or deleted) from the woocommerce store
|
|
* This function should run before any products sync to avoid issues when products are trashed and then untrashed
|
|
*
|
|
* @param string $catalog_id The users catalog ID
|
|
* @param string $bc_id The users business center ID
|
|
* @param string $store_name The users store name
|
|
* @param string $access_token The MAPI issued access token
|
|
* @param integer $page_total The maximum number of pages of 100 products to sync for this job
|
|
* @param integer $last_catalog_sync The unix timestamp of the last catalog sync, used to fetch products modified and created since that timestamp
|
|
* @param boolean $reconciliation_sync Control if the current sync is reconciliation for TikTok's product webhooks, which will sync all products to the webhooks and only deltas to the MAPI endpoint, defaults to false
|
|
* @param integer $reconciliation_sync_time The reference time for the reconciliation sync, which will be used if reconciliation sync is enabled, to make sure to only send recent modifications to the MAPI endpoint
|
|
*
|
|
* @return void
|
|
*/
|
|
public function delete_products_helper( $catalog_id, $bc_id, $store_name, $access_token, $page_total, $last_catalog_sync, $reconciliation_sync, $reconciliation_sync_time ) {
|
|
// since delete_products_helper is the first job to run after a scheduled catalog sync, shift to daily sync instead of hourly sync if
|
|
if ( true === as_has_scheduled_action( 'tt4b_catalog_sync', null, 'tt4b_scheduled_catalog_sync' ) ) {
|
|
as_unschedule_all_actions( 'tt4b_catalog_sync', null, 'tt4b_scheduled_catalog_sync' );
|
|
if ( false === as_has_scheduled_action( 'tt4b_catalog_sync', null, 'tt4b_daily_catalog_sync' ) ) {
|
|
as_schedule_cron_action(
|
|
'tomorrow',
|
|
$this->generate_cron_string(),
|
|
'tt4b_catalog_sync',
|
|
array(
|
|
'catalog_id' => $catalog_id,
|
|
'bc_id' => $bc_id,
|
|
'store_name' => $store_name,
|
|
'access_token' => $access_token,
|
|
),
|
|
'tt4b_daily_catalog_sync'
|
|
);
|
|
}
|
|
}
|
|
$products_to_delete = (array) get_option( 'tt4b_product_delete_queue', array() );
|
|
// check count of products to delete, only send payload if SKUs array is not empty
|
|
if ( 0 === count( $products_to_delete ) ) {
|
|
$this->logger->log( __METHOD__, 'no products retrieved from tt4b_product_delete_queue option, skipping product deletion' );
|
|
} else {
|
|
$raw_products_payload = array();
|
|
$raw_variations_payload = array();
|
|
$mapi_dpa_products_payload = array();
|
|
foreach ( $products_to_delete as $sku_key => $product ) {
|
|
// type check here is necessary for backwards compatibility after API change:
|
|
// allow remaining sku IDs to be posted to MAPI for deletion
|
|
// new associative array of product data to be posted to new raw API and MAPI
|
|
if ( is_int( $sku_key ) ) {
|
|
$mapi_dpa_products_payload[] = $product;
|
|
} elseif ( $product['topic'] === 'partner_gw_product_sync' ) {
|
|
$mapi_dpa_products_payload[] = $sku_key;
|
|
$raw_products_payload[] = array(
|
|
'id' => $product['id'],
|
|
'data' => $product['data'],
|
|
);
|
|
} elseif ( $product['topic'] === 'partner_gw_product_variation_sync' ) {
|
|
$mapi_dpa_products_payload[] = $sku_key;
|
|
$raw_variations_payload[] = array(
|
|
'id' => $product['id'],
|
|
'data' => $product['data'],
|
|
'parent_id' => $product['parent_id'],
|
|
);
|
|
}
|
|
}
|
|
if ( 0 < count( $raw_products_payload ) ) {
|
|
$raw_products_request = array(
|
|
'products' => $raw_products_payload,
|
|
'topic' => 'partner_gw_product_sync',
|
|
'tag' => 'delete',
|
|
);
|
|
$this->mapi->tbp_post( get_option( 'tt4b_external_data' ), 'woocommerce/php/product/batch/', 'v1.0', $raw_products_request, TBPApi::PLUGIN );
|
|
}
|
|
if ( 0 < count( $raw_variations_payload ) ) {
|
|
$raw_variations_request = array(
|
|
'products' => $raw_variations_payload,
|
|
'topic' => 'partner_gw_product_variation_sync',
|
|
'tag' => 'delete',
|
|
);
|
|
$this->mapi->tbp_post( get_option( 'tt4b_external_data' ), 'woocommerce/php/product/batch/', 'v1.0', $raw_variations_request, TBPApi::PLUGIN );
|
|
}
|
|
if ( 0 < count( $mapi_dpa_products_payload ) ) {
|
|
$mapi_dpa_request = array(
|
|
'sku_ids' => $mapi_dpa_products_payload,
|
|
'bc_id' => $bc_id,
|
|
'catalog_id' => $catalog_id,
|
|
);
|
|
$this->mapi->mapi_post( 'catalog/product/delete/', $access_token, $mapi_dpa_request, 'v1.3' );
|
|
}
|
|
update_option( 'tt4b_product_delete_queue', array() );
|
|
}
|
|
$tt4b_catalog_sync_helper_payload = array(
|
|
'catalog_id' => $catalog_id,
|
|
'bc_id' => $bc_id,
|
|
'store_name' => $store_name,
|
|
'access_token' => $access_token,
|
|
'page' => 1,
|
|
'page_total' => $page_total,
|
|
'last_catalog_sync' => $last_catalog_sync,
|
|
'reconciliation_sync' => $reconciliation_sync,
|
|
'reconciliation_sync_time' => $reconciliation_sync_time,
|
|
);
|
|
// after product deletion is skipped or completed, proceed to initiate catalog sync helpers if anything needs to be synced
|
|
if ( $page_total >= 1 ) {
|
|
self::check_and_start_async_action( 'tt4b_catalog_sync_helper', $tt4b_catalog_sync_helper_payload, '' );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper function used to post batches of products from woocommerce store to tiktok catalog manager
|
|
*
|
|
* @param string $catalog_id The users catalog ID
|
|
* @param string $bc_id The users business center ID
|
|
* @param string $store_name The users store name
|
|
* @param string $access_token The MAPI issued access token
|
|
* @param integer $page The page of products from the user catalog
|
|
* @param integer $page_total The maximum number of pages of 100 products to sync for this job
|
|
* @param integer $last_catalog_sync The unix timestamp of the last catalog sync, used to fetch products modified and created since that timestamp
|
|
* @param boolean $reconciliation_sync Control if the current sync is reconciliation for TikTok's product webhooks, which will sync all products to the webhooks and only deltas to the MAPI endpoint, defaults to false
|
|
* @param integer $reconciliation_sync_time The reference time for the reconciliation sync, which will be used if reconciliation sync is enabled, to make sure to only send recent modifications to the MAPI endpoint
|
|
*
|
|
* @return void
|
|
*/
|
|
public function catalog_sync_helper( string $catalog_id, string $bc_id, string $store_name, string $access_token, int $page, int $page_total, int $last_catalog_sync, bool $reconciliation_sync, int $reconciliation_sync_time ) {
|
|
$wc_get_products_args = array(
|
|
'date_modified' => '>=' . $last_catalog_sync,
|
|
'limit' => 100,
|
|
'page' => $page,
|
|
);
|
|
$parsed_time = date( 'Y-m-d\TH:i:s', $last_catalog_sync );
|
|
$request = new WP_REST_Request( 'GET', '/wc/v3/products' );
|
|
$request->set_query_params(
|
|
array(
|
|
'per_page' => 100,
|
|
'page' => $page,
|
|
'modified_after' => $parsed_time,
|
|
'dates_are_gmt' => true,
|
|
)
|
|
);
|
|
$products = wc_get_products( $wc_get_products_args );
|
|
if ( 0 === count( $products ) ) {
|
|
$this->logger->log( __METHOD__, 'no products retrieved from wc_get_products' );
|
|
}
|
|
$failed_products_count = 0;
|
|
$products_to_restore = (array) get_option( 'tt4b_product_restore_queue', array() );
|
|
$mapi_reference_time = $last_catalog_sync;
|
|
if ( $reconciliation_sync ) {
|
|
$mapi_reference_time = $reconciliation_sync_time;
|
|
}
|
|
|
|
$raw_products_payload = array();
|
|
$mapi_upload_payload = array();
|
|
// $mapi_update_payload = [];
|
|
foreach ( $products as $product ) {
|
|
if ( is_null( $product ) ) {
|
|
++$failed_products_count;
|
|
continue;
|
|
}
|
|
$product_id = $product->get_id();
|
|
$product_sku = $product->get_sku();
|
|
|
|
$dpa_product_info = $this->generate_product_info( $store_name, $product );
|
|
if ( array() === $dpa_product_info ) {
|
|
$this->logger->log( __METHOD__, 'unable to parse product: ' . $product_id . ' in to MAPI DPA schema' );
|
|
++$failed_products_count;
|
|
continue;
|
|
}
|
|
$restored_product = array_key_exists( $product_id, $products_to_restore );
|
|
if ( $restored_product ) {
|
|
unset( $products_to_restore[ $product_id ] );
|
|
}
|
|
|
|
$createTime = $product->get_date_created()->getTimestamp();
|
|
if ( $mapi_reference_time <= $createTime || $restored_product ) {
|
|
$mapi_upload_payload[] = $dpa_product_info;
|
|
}
|
|
// if ( $createTime < $mapi_reference_time && !$restored_product ) {
|
|
// $mapi_update_payload[] = $dpa_product_info;
|
|
// } else {
|
|
// $mapi_upload_payload[] = $dpa_product_info;
|
|
// }
|
|
|
|
$response = $this->tikTokProductsController->prepare_object( $product, $request );
|
|
if ( $response->is_error() ) {
|
|
$this->logger->log( __METHOD__, 'unable to parse product: ' . $product_id . ' in to REST API schema' );
|
|
++$failed_products_count;
|
|
continue;
|
|
}
|
|
|
|
$product_information = array(
|
|
'id' => (string) $product_id,
|
|
'data' => json_encode( $response->get_data() ),
|
|
);
|
|
$raw_products_payload[] = $product_information;
|
|
|
|
if ( $product->is_type( 'variable' ) ) {
|
|
$args = array(
|
|
'parent' => $product_id,
|
|
'type' => 'variation',
|
|
'paginate' => true,
|
|
'limit' => 100,
|
|
);
|
|
$result = wc_get_products( $args );
|
|
$variation_pages = $result->max_num_pages;
|
|
|
|
$tt4b_variation_sync_payload = array(
|
|
'parent_id' => $product_id,
|
|
'parent_sku' => $product_sku,
|
|
'access_token' => $access_token,
|
|
'page_total' => $variation_pages,
|
|
'page' => 1,
|
|
);
|
|
self::check_and_start_async_action( 'tt4b_variation_sync_helper', $tt4b_variation_sync_payload, '' );
|
|
|
|
$variations = $product->get_available_variations( 'objects' );
|
|
$dpa_variations = $this->fetch_mapi_product_variations( $dpa_product_info['sku_id'], $dpa_product_info['description'] !== $dpa_product_info['title'] ? $dpa_product_info['description'] : '', $store_name, $variations, $mapi_reference_time );
|
|
// $update_variations = $dpa_variations[0];
|
|
$upload_variations = $dpa_variations[1];
|
|
$mapi_upload_payload = array_merge( $mapi_upload_payload, $upload_variations );
|
|
// $mapi_update_payload = array_merge( $mapi_update_payload, $update_variations );
|
|
}
|
|
}
|
|
|
|
if ( 0 < count( $raw_products_payload ) ) {
|
|
$raw_products_request = array(
|
|
'topic' => 'partner_gw_product_sync',
|
|
'tag' => 'update',
|
|
'products' => $raw_products_payload,
|
|
);
|
|
$this->mapi->tbp_post( get_option( 'tt4b_external_data' ), 'woocommerce/php/product/batch/', 'v1.0', $raw_products_request, TBPApi::PLUGIN );
|
|
}
|
|
if ( 0 < count( $mapi_upload_payload ) ) {
|
|
$mapi_upload_request = array(
|
|
'bc_id' => $bc_id,
|
|
'catalog_id' => $catalog_id,
|
|
'products' => $mapi_upload_payload,
|
|
);
|
|
$this->mapi->mapi_post( 'catalog/product/upload/', $access_token, $mapi_upload_request, 'v1.3' );
|
|
}
|
|
// if ( 0 < count( $mapi_update_payload ) ) {
|
|
// $mapi_update_request = [
|
|
// 'bc_id' => $bc_id,
|
|
// 'catalog_id' => $catalog_id,
|
|
// 'products' => $mapi_update_payload
|
|
// ];
|
|
// $this->mapi->mapi_post( 'catalog/product/update/', $access_token, $mapi_update_request, 'v1.3' );
|
|
// }
|
|
update_option( 'tt4b_product_restore_queue', $products_to_restore );
|
|
|
|
++$page;
|
|
$tt4b_catalog_sync_helper_payload = array(
|
|
'catalog_id' => $catalog_id,
|
|
'bc_id' => $bc_id,
|
|
'store_name' => $store_name,
|
|
'access_token' => $access_token,
|
|
'page' => $page,
|
|
'page_total' => $page_total,
|
|
'last_catalog_sync' => $last_catalog_sync,
|
|
'reconciliation_sync' => $reconciliation_sync,
|
|
'reconciliation_sync_time' => $reconciliation_sync_time,
|
|
);
|
|
if ( $page <= $page_total ) {
|
|
self::check_and_start_async_action( 'tt4b_catalog_sync_helper', $tt4b_catalog_sync_helper_payload, '' );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sync a product's variants to the TikTok catalog
|
|
*
|
|
* @param int $parent_id
|
|
* @param string $parent_sku
|
|
* @param string $access_token
|
|
* @param int $page_total
|
|
* @param int $page
|
|
*
|
|
* @return void
|
|
*/
|
|
public function variation_sync_helper( int $parent_id, string $parent_sku, string $access_token, int $page_total, int $page ) {
|
|
$wc_get_product_variation_args = array(
|
|
'parent' => $parent_id,
|
|
'type' => 'variation',
|
|
'limit' => 100,
|
|
'page' => $page,
|
|
);
|
|
$products = wc_get_products( $wc_get_product_variation_args );
|
|
if ( 0 === count( $products ) ) {
|
|
$this->logger->log( __METHOD__, 'no variations retrieved from wc_get_products for parent product: ' . $parent_id );
|
|
}
|
|
$failed_products_count = 0;
|
|
$raw_products_payload = array();
|
|
foreach ( $products as $product ) {
|
|
if ( is_null( $product ) ) {
|
|
++$failed_products_count;
|
|
continue;
|
|
}
|
|
$product_id = $product->get_id();
|
|
|
|
$request = new WP_REST_Request( 'GET', "wc/v3/products/$parent_id" );
|
|
$request->set_query_params(
|
|
array(
|
|
'dates_are_gmt' => true,
|
|
)
|
|
);
|
|
|
|
$response = $this->tikTokProductsController->prepare_object( $product, $request );
|
|
if ( $response->is_error() ) {
|
|
$this->logger->log( __METHOD__, 'unable to parse product: ' . $product_id . ' in to REST API schema' );
|
|
++$failed_products_count;
|
|
continue;
|
|
}
|
|
|
|
$product_information = array(
|
|
'id' => (string) $product_id,
|
|
'data' => json_encode( $response->get_data() ),
|
|
'parent_id' => (string) $parent_id,
|
|
);
|
|
$raw_products_payload[] = $product_information;
|
|
}
|
|
|
|
if ( 0 < count( $raw_products_payload ) ) {
|
|
$raw_products_request = array(
|
|
'topic' => 'partner_gw_product_variation_sync',
|
|
'tag' => 'update',
|
|
'products' => $raw_products_payload,
|
|
);
|
|
$this->mapi->tbp_post( get_option( 'tt4b_external_data' ), 'woocommerce/php/product/batch/', 'v1.0', $raw_products_request, TBPApi::PLUGIN );
|
|
}
|
|
|
|
++$page;
|
|
$tt4b_variation_sync_payload = array(
|
|
'parent_id' => $parent_id,
|
|
'parent_sku' => $parent_sku,
|
|
'access_token' => $access_token,
|
|
'page_total' => $page_total,
|
|
'page' => $page,
|
|
);
|
|
if ( $page <= $page_total ) {
|
|
self::check_and_start_async_action( 'tt4b_variation_sync_helper', $tt4b_variation_sync_payload, '' );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Prepare child product_variations associated with parent variable product to be synced to TikTok MAPI catalog
|
|
*
|
|
* @param string $parent_sku
|
|
* @param string $parent_description
|
|
* @param string $store_name
|
|
* @param WC_Product_Variation[] $product_variations
|
|
* @param integer $last_catalog_sync The unix timestamp of the last catalog sync, used to fetch products modified and created since that timestamp
|
|
*
|
|
* @return array
|
|
*/
|
|
public function fetch_mapi_product_variations( $parent_sku, $parent_description, $store_name, $product_variations, $last_catalog_sync ) {
|
|
$dpa_variation_update_products = array();
|
|
$dpa_variation_upload_products = array();
|
|
$products_to_restore = (array) get_option( 'tt4b_product_restore_queue', array() );
|
|
$failed_variations_count = 0;
|
|
if ( 0 === count( $product_variations ) ) {
|
|
$this->logger->log( __METHOD__, 'empty array of variable products provided' );
|
|
}
|
|
foreach ( $product_variations as $variation ) {
|
|
if ( is_null( $variation ) ) {
|
|
++$failed_variations_count;
|
|
continue;
|
|
}
|
|
$dpa_variation_product = $this->generate_product_info( $store_name, $variation, $parent_sku, $parent_description );
|
|
$variation_id = $variation->get_id();
|
|
|
|
$restored_product = array_key_exists( $variation_id, $products_to_restore );
|
|
if ( $restored_product ) {
|
|
unset( $products_to_restore[ $variation_id ] );
|
|
}
|
|
|
|
$createTime = $variation->get_date_created()->getTimestamp();
|
|
if ( $last_catalog_sync <= $createTime || $restored_product ) {
|
|
$dpa_variation_upload_products[] = $dpa_variation_product;
|
|
}
|
|
// if ( $createTime < $last_catalog_sync && !$restored_product ) {
|
|
// MAPI currently doesn't support updating item_group_id with /product/update/ endpoint
|
|
// unset( $dpa_variation_product['item_group_id'] );
|
|
// unset( $dpa_variation_product['price_info']['sale_price'] );
|
|
// $dpa_variation_update_products[] = $dpa_variation_product;
|
|
// } else {
|
|
// $dpa_variation_upload_products[] = $dpa_variation_product;
|
|
// }
|
|
}
|
|
update_option( 'tt4b_product_restore_queue', $products_to_restore );
|
|
return array( $dpa_variation_update_products, $dpa_variation_upload_products );
|
|
}
|
|
|
|
/**
|
|
* Check if async action should be added according to name, payload, and group
|
|
*
|
|
* @param string $action_name name of the action to run
|
|
* @param array $payload array payload for the action
|
|
*
|
|
* @param string $group action group, pass empty string if no group
|
|
*/
|
|
private function check_and_start_async_action( string $action_name, array $payload, string $group ) {
|
|
if ( '' == $group ) {
|
|
if ( false === as_has_scheduled_action(
|
|
$action_name,
|
|
$payload
|
|
)
|
|
) {
|
|
as_enqueue_async_action(
|
|
$action_name,
|
|
$payload
|
|
);
|
|
}
|
|
} elseif ( false === as_has_scheduled_action(
|
|
$action_name,
|
|
$payload,
|
|
$group
|
|
)
|
|
) {
|
|
|
|
as_enqueue_async_action(
|
|
$action_name,
|
|
$payload,
|
|
$group
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate the needed product_data in array format for products, and product variants accordingly
|
|
*
|
|
* @param string $store_name
|
|
* @param WC_Product $product
|
|
* @param string $parent_sku optional - provided for child products (variants) to associate child product to parent product
|
|
* @param string $parent_description optional - provided for child products in case the child doesn't have a unique description
|
|
*
|
|
* @return array
|
|
*/
|
|
public function generate_product_info( $store_name, $product, $parent_sku = '', $parent_description = '' ) {
|
|
$title = $product->get_name();
|
|
$description = $product->get_short_description();
|
|
if ( '' === $description && '' === $parent_description ) {
|
|
$description = $title;
|
|
} elseif ( '' === $description && '' !== $parent_description ) {
|
|
$description = $parent_description;
|
|
}
|
|
$condition = 'NEW';
|
|
|
|
$availability = 'IN_STOCK';
|
|
$stock_status = $product->is_in_stock();
|
|
if ( false === $stock_status ) {
|
|
$availability = 'OUT_OF_STOCK';
|
|
}
|
|
$sku_id = (string) $product->get_sku();
|
|
$raw_sku = $sku_id;
|
|
if ( '' === $sku_id ) {
|
|
$sku_id = (string) $product->get_id();
|
|
}
|
|
// if parent_id is not provided then the item_group_id is equal to the current product's sku_id
|
|
// if parent_id is provided meaning product is child of parent_id, the item_group_id should be the parent_sku
|
|
$item_group_id = $sku_id;
|
|
if ( '' !== $parent_sku ) {
|
|
$item_group_id = $parent_sku;
|
|
// if the current product SKU is the same as it's parent SKU, concatenate $parent_sku with the child post ID
|
|
// otherwise use the SKU of the variation
|
|
$sku_id = variation_content_id_helper( Method::CATALOG, $parent_sku, $sku_id, $product->get_id() );
|
|
// if there is a variation description only for this variant, use that instead of either
|
|
// the parent description or the title for the description field in the TikTok Catalog
|
|
$variantDescription = $product->get_description();
|
|
if ( '' !== $variantDescription ) {
|
|
$description = $variantDescription;
|
|
}
|
|
}
|
|
$link = get_permalink( $product->get_id() );
|
|
$image_id = $product->get_image_id();
|
|
$image_url = wp_get_attachment_image_url( $image_id, 'full' );
|
|
$price = $product->get_price();
|
|
// if regular price is not empty, false, or string use that instead
|
|
$regularPrice = $product->get_regular_price();
|
|
if ( '' !== $regularPrice && false !== $regularPrice && '0' !== $regularPrice ) {
|
|
$price = $regularPrice;
|
|
}
|
|
$sale_price = $product->get_sale_price();
|
|
if ( '0' === $sale_price || '' === $sale_price ) {
|
|
$sale_price = $price;
|
|
}
|
|
// Get product gallery images - max 10
|
|
$gallery_image_ids = array_slice( $product->get_gallery_image_ids(), 0, 10, true );
|
|
$gallery_image_urls = array();
|
|
foreach ( $gallery_image_ids as $gallery_image_id ) {
|
|
$gallery_image_urls[] = wp_get_attachment_image_url( $gallery_image_id, 'full' );
|
|
}
|
|
|
|
// if any of the values are empty, the whole request will fail, so skip the product.
|
|
$missing_fields = array();
|
|
if ( '' === $sku_id || false === $sku_id ) {
|
|
$missing_fields[] = 'sku_id';
|
|
}
|
|
if ( '' === $title || false === $title ) {
|
|
$missing_fields[] = 'title';
|
|
}
|
|
if ( '' === $image_url || false === $image_url ) {
|
|
$missing_fields[] = 'image_url';
|
|
}
|
|
if ( '' === $price || false === $price || '0' === $price ) {
|
|
$missing_fields[] = 'price';
|
|
}
|
|
if ( count( $missing_fields ) > 0 ) {
|
|
$debug_message = sprintf(
|
|
'sku_id: %s title: %s is missing the following fields for product sync: %s',
|
|
$sku_id,
|
|
$title,
|
|
join( ',', $missing_fields )
|
|
);
|
|
$this->logger->log( __METHOD__, $debug_message );
|
|
return array();
|
|
}
|
|
|
|
$dpa_product = array(
|
|
'sku_id' => $sku_id,
|
|
'item_group_id' => $item_group_id,
|
|
'title' => $title,
|
|
'availability' => $availability,
|
|
'description' => $description,
|
|
'image_url' => $image_url,
|
|
'brand' => $store_name,
|
|
'product_detail' => array(
|
|
'condition' => $condition,
|
|
),
|
|
'price_info' => array(
|
|
'price' => $price,
|
|
'sale_price' => $sale_price,
|
|
),
|
|
'landing_page' => array(
|
|
'landing_page_url' => $link,
|
|
),
|
|
'extra_info' => array(
|
|
'custom_label_0' => $raw_sku,
|
|
),
|
|
|
|
);
|
|
|
|
// add additional product images if available
|
|
if ( count( $gallery_image_urls ) > 0 ) {
|
|
$dpa_product['additional_image_link'] = $gallery_image_urls;
|
|
}
|
|
|
|
return $dpa_product;
|
|
}
|
|
|
|
/**
|
|
* Returns a cron string with randomized hour and minute values for scheduling recurring eligibility collection
|
|
*
|
|
* @return string
|
|
*/
|
|
private function generate_cron_string() {
|
|
$minute = rand( 0, 59 );
|
|
$hour = rand( 0, 23 );
|
|
return '' . $minute . ' ' . $hour . ' * * 0-6';
|
|
}
|
|
}
|