Skip to content
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
18 changes: 18 additions & 0 deletions includes/API.php
Original file line number Diff line number Diff line change
Expand Up @@ -754,4 +754,22 @@ public function update_commerce_integration(
$this->set_response_handler( API\CommerceIntegration\Configuration\Update\Response::class );
return $this->perform_request( $request );
}

public function get_batch_status( string $catalog_id, string $batch_handle ) {
$endpoint = sprintf( '/%s/check_batch_request_status', $catalog_id );

$query_params = array( 'handle' => $batch_handle );

$url = $endpoint . '?' . http_build_query( $query_params );

$request = new Request(
$url,
'GET'
);

$this->set_response_handler( API\ProductCatalog\ProductGroups\Read\Response::class );
$response = $this->perform_request( $request );

return $response;
}
}
9 changes: 9 additions & 0 deletions includes/API/ProductCatalog/ProductGroups/Read/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,13 @@ public function get_ids(): array {
}
return $product_item_ids;
}

/**
* Returns the full response data, including warnings, errors, and other metadata.
*
* @return array
*/
public function get_full_response(): array {
return $this->response_data;
}
}
8 changes: 8 additions & 0 deletions includes/Admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -613,10 +613,18 @@ private function add_query_vars_to_find_products_with_sync_enabled( array $query
*/
private function add_query_vars_to_find_products_with_sync_disabled( array $query_vars ) {
$meta_query = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
'relation' => 'OR',

array(
'key' => Products::get_product_sync_meta_key(),
'value' => 'no',
),

array(
'key' => '_fb_sync_issues',
'value' => '"warnings";a:0',
'compare' => 'NOT LIKE',
),
);

if ( empty( $query_vars['meta_query'] ) ) {
Expand Down
19 changes: 19 additions & 0 deletions includes/ProductSync/ProductValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ public function validate() {
$this->validate_product_status();
$this->validate_product_visibility();
$this->validate_product_terms();
$this->validate_product_sync_issues();
}

/**
Expand Down Expand Up @@ -398,4 +399,22 @@ protected function validate_variation_structure() {
throw new ProductInvalidException( __( 'Too many attributes selected for product. Use 4 or less.', 'facebook-for-woocommerce' ) );
}
}

/**
* Validates the product for any sync issues before attempting to synchronize with Facebook.
*
* This function checks the product for various conditions that may prevent it from being properly synced,
* such as missing required fields or invalid data. If any issues are found, they are collected and can be
* used to prevent the sync or notify the user.
*
* @throws ProductExcludedException If the product is excluded from synchronization.
*/
protected function validate_product_sync_issues() {
$issues = get_post_meta( $this->product->get_id(), '_fb_sync_issues', true );
if ( ! empty( $issues['warnings'] ) && is_array( $issues['warnings'] ) ) {
$messages = implode( '; ', $issues['warnings'] );
/* translators: %s: List of sync issue messages. */
throw new ProductExcludedException( sprintf( __( 'Sync issues: %s', 'facebook-for-woocommerce' ), $messages ) );
}
}
}
172 changes: 171 additions & 1 deletion includes/Products/Sync/Background.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ class Background extends BackgroundJobHandler {
/** @var string data key */
protected $data_key = 'requests';

public function __construct() {
parent::__construct();

add_action( 'facebook_sync_poll_handle', array( $this, 'handle_poll_action' ), 10, 1 );
}

/**
* Processes a job.
*
Expand Down Expand Up @@ -251,7 +257,171 @@ 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 );
$response_handles = $response->handles;
$handles = ( isset( $response_handles ) && is_array( $response_handles ) ) ? $response_handles : array();
$handles = ( isset( $response_handles ) && is_array( $response_handles ) ) ? $response_handles : array();

foreach ( $handles as $handle ) {
set_transient( 'facebook_batch_handle_' . $handle, $requests, 12 * HOUR_IN_SECONDS );

if ( function_exists( 'as_schedule_single_action' ) ) {
as_schedule_single_action( time() + 60, 'facebook_sync_poll_handle', array( $handle ) );
} else {
wp_schedule_single_event( time() + 60, 'facebook_sync_poll_handle', array( $handle ) );
}
}

return $handles;
}


/**
* Poll Facebook API for the batch job status using the handle.
*
* @param string $handle The batch handle returned by Facebook.
* @return array|null Returns status data array on success, or null on failure.
*/
public function poll_batch_status( string $handle ): ?array {
try {
$catalog_id = facebook_for_woocommerce()->get_integration()->get_product_catalog_id();
$response = facebook_for_woocommerce()->get_api()->get_batch_status( $catalog_id, $handle );

return $response->get_full_response();

} catch ( \Exception $e ) {
error_log( 'Exception in poll_batch_status: ' . $e->getMessage() );
facebook_for_woocommerce()->log( 'Error polling batch status for handle ' . $handle . ': ' . $e->getMessage() );
return null;
}
}


/**
* Callback triggered by scheduled action to poll batch status.
*
* @param string $handle The batch handle to poll.
*/
public function handle_poll_action( string $handle ) {
$statuses = $this->poll_batch_status( $handle );

if ( ! is_array( $statuses ) || empty( $statuses ) ) {
return;
}

foreach ( $statuses as $status ) {

if ( isset( $status['status'] ) && 'IN_PROGRESS' === $status['status'] ) {
if ( function_exists( 'as_schedule_single_action' ) ) {
as_schedule_single_action( time() + 60, 'facebook_sync_poll_handle', array( $handle ) );
} else {
wp_schedule_single_event( time() + 60, 'facebook_sync_poll_handle', array( $handle ) );
}
return;
}

$this->process_batch_status_results( $status, $handle );
}

delete_transient( 'facebook_batch_handle_' . $handle );
}


/**
* Processes the results of a batch status update for product synchronization.
*
* This method handles the status array returned from a batch operation,
* performing any necessary actions based on the results and the provided handle.
*
* @param array $status The status results from the batch operation.
* @param string $handle The unique identifier for the batch process.
*/
protected function process_batch_status_results( array $status, string $handle ) {
if ( isset( $status[0] ) && is_array( $status[0] ) && isset( $status[0]['status'] ) ) {
foreach ( $status as $single_status ) {
$this->process_batch_status_results( $single_status, $handle );
}
return;
}

$warnings = $status['warnings'] ?? [];
$errors = $status['errors'] ?? [];
$issues_by_id = [];

foreach ( array_merge( $warnings, $errors ) as $entry ) {
$product_id_str = $entry['id'] ?? '';
$post_id = null;

if ( preg_match( '/wc_post_id_(\d+)/', $product_id_str, $matches ) ) {
$post_id = (int) $matches[1];
} elseif ( is_numeric( $product_id_str ) ) {
$post_id = (int) $product_id_str;
} else {
$sku_parts = explode( '_', $product_id_str );
$last_part = end( $sku_parts );
if ( count( $sku_parts ) > 1 && is_numeric( $last_part ) ) {
$post_id = (int) $last_part;
} else {
$post_id = wc_get_product_id_by_sku( $product_id_str );
}
}

if ( $post_id ) {
$issues_by_id[ $post_id ]['warnings'][] = $entry['message'];
}
}

foreach ( $issues_by_id as $post_id => $issues ) {
update_post_meta(
$post_id,
'_fb_sync_issues',
array(
'status' => $status['status'] ?? 'UNKNOWN',
'warnings' => $issues['warnings'] ?? [],
'handle' => $handle,
)
);
}

$posts_with_issues = get_posts(
array(
'post_type' => 'product',
'posts_per_page' => -1,
'post_status' => 'any',
'meta_key' => '_fb_sync_issues',
'fields' => 'ids',
)
);

$batch_requests = get_transient( 'facebook_batch_handle_' . $handle );
$batched_product_ids = [];

if ( is_array( $batch_requests ) ) {
foreach ( $batch_requests as $req ) {
if ( isset( $req['data']['id'] ) ) {
$id = $req['data']['id'];

if ( preg_match( '/wc_post_id_(\d+)/', $id, $matches ) ) {
$batched_product_ids[] = (int) $matches[1];
} elseif ( is_numeric( $id ) ) {
$batched_product_ids[] = (int) $id;
} else {
$sku_parts = explode( '_', $id );
$last_part = end( $sku_parts );
if ( count( $sku_parts ) > 1 && is_numeric( $last_part ) ) {
$batched_product_ids[] = (int) $last_part;
} else {
$product_id = wc_get_product_id_by_sku( $id );
if ( $product_id ) {
$batched_product_ids[] = (int) $product_id;
}
}
}
}
}
}

foreach ( $posts_with_issues as $post_id ) {
if ( in_array( $post_id, $batched_product_ids, true ) && ! isset( $issues_by_id[ $post_id ] ) ) {
delete_post_meta( $post_id, '_fb_sync_issues' );
}
}
}
}
Loading