HEX
Server: LiteSpeed
System: Linux eko108.isimtescil.net 4.18.0-477.21.1.lve.1.el8.x86_64 #1 SMP Tue Sep 5 23:08:35 UTC 2023 x86_64
User: uyarreklamcomtr (11202)
PHP: 7.4.33
Disabled: opcache_get_status
Upload Files
File: /var/www/vhosts/uyarreklam.com.tr/httpdocs/Jobs.tar
AbstractActionSchedulerJob.php000064400000007104151542752540012464 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs;

use Automattic\WooCommerce\GoogleListingsAndAds\ActionScheduler\ActionSchedulerInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\PluginHelper;
use Exception;

defined( 'ABSPATH' ) || exit;

/**
 * Class AbstractActionSchedulerJob
 *
 * Abstract class for jobs that use ActionScheduler.
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Jobs
 */
abstract class AbstractActionSchedulerJob implements ActionSchedulerJobInterface {

	use PluginHelper;

	/**
	 * @var ActionSchedulerInterface
	 */
	protected $action_scheduler;

	/**
	 * @var ActionSchedulerJobMonitor
	 */
	protected $monitor;

	/**
	 * Whether the job should be rescheduled on timeout.
	 *
	 * @var bool
	 */
	protected $retry_on_timeout = true;

	/**
	 * AbstractActionSchedulerJob constructor.
	 *
	 * @param ActionSchedulerInterface  $action_scheduler
	 * @param ActionSchedulerJobMonitor $monitor
	 */
	public function __construct( ActionSchedulerInterface $action_scheduler, ActionSchedulerJobMonitor $monitor ) {
		$this->action_scheduler = $action_scheduler;
		$this->monitor          = $monitor;
	}

	/**
	 * Init the batch schedule for the job.
	 *
	 * The job name is used to generate the schedule event name.
	 */
	public function init(): void {
		add_action( $this->get_process_item_hook(), [ $this, 'handle_process_items_action' ] );
	}

	/**
	 * Can the job be scheduled.
	 *
	 * @param array|null $args
	 *
	 * @return bool Returns true if the job can be scheduled.
	 */
	public function can_schedule( $args = [] ): bool {
		return ! $this->is_running( $args );
	}

	/**
	 * Handles processing single item action hook.
	 *
	 * @hooked gla/jobs/{$job_name}/process_item
	 *
	 * @param array $items The job items from the current batch.
	 *
	 * @throws Exception If an error occurs.
	 */
	public function handle_process_items_action( array $items = [] ) {
		$process_hook = $this->get_process_item_hook();
		$process_args = [ $items ];

		$this->monitor->validate_failure_rate( $this, $process_hook, $process_args );
		if ( $this->retry_on_timeout ) {
			$this->monitor->attach_timeout_monitor( $process_hook, $process_args );
		}

		try {
			$this->process_items( $items );
		} catch ( Exception $exception ) {
			// reschedule on failure
			$this->action_scheduler->schedule_immediate( $process_hook, $process_args );

			// throw the exception again so that it can be logged
			throw $exception;
		}

		$this->monitor->detach_timeout_monitor( $process_hook, $process_args );
	}

	/**
	 * Check if this job is running.
	 *
	 * The job is considered to be running if the "process_item" action is currently pending or in-progress.
	 *
	 * @param array|null $args
	 *
	 * @return bool
	 */
	protected function is_running( ?array $args = [] ): bool {
		return $this->action_scheduler->has_scheduled_action( $this->get_process_item_hook(), $args );
	}

	/**
	 * Get the base name for the job's scheduled actions.
	 *
	 * @return string
	 */
	protected function get_hook_base_name(): string {
		return "{$this->get_slug()}/jobs/{$this->get_name()}/";
	}

	/**
	 * Get the hook name for the "process item" action.
	 *
	 * This method is required by the job monitor.
	 *
	 * @return string
	 */
	public function get_process_item_hook(): string {
		return "{$this->get_hook_base_name()}process_item";
	}

	/**
	 * Process batch items.
	 *
	 * @param array $items A single batch from the get_batch() method.
	 *
	 * @throws Exception If an error occurs. The exception will be logged by ActionScheduler.
	 */
	abstract protected function process_items( array $items );
}
AbstractBatchedActionSchedulerJob.php000064400000013025151542752550013737 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs;

use Exception;

defined( 'ABSPATH' ) || exit;

/**
 * AbstractBatchedActionSchedulerJob class.
 *
 * Enables a job to be processed in recurring scheduled batches with queued events.
 *
 * Notes:
 * - Uses ActionScheduler's very scalable async actions feature which will run async batches in loop back requests until all batches are done
 * - Items may be processed concurrently by AS, but batches will be created one after the other, not concurrently
 * - The job will not start if it is already running
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Jobs
 */
abstract class AbstractBatchedActionSchedulerJob extends AbstractActionSchedulerJob implements BatchedActionSchedulerJobInterface {

	/**
	 * Init the batch schedule for the job.
	 *
	 * The job name is used to generate the schedule event name.
	 */
	public function init(): void {
		add_action( $this->get_create_batch_hook(), [ $this, 'handle_create_batch_action' ] );
		parent::init();
	}

	/**
	 * Get the hook name for the "create batch" action.
	 *
	 * @return string
	 */
	protected function get_create_batch_hook(): string {
		return "{$this->get_hook_base_name()}create_batch";
	}

	/**
	 * Enqueue the "create_batch" action provided it doesn't already exist.
	 *
	 * To minimize the resource use of starting the job the batch creation is handled async.
	 *
	 * @param array $args
	 */
	public function schedule( array $args = [] ) {
		$this->schedule_create_batch_action( 1 );
	}

	/**
	 * Handles batch creation action hook.
	 *
	 * @hooked gla/jobs/{$job_name}/create_batch
	 *
	 * Schedules an action to run immediately for the items in the batch.
	 *
	 * @param int $batch_number The batch number increments for each new batch in the job cycle.
	 *
	 * @throws Exception If an error occurs.
	 * @throws JobException If the job failure rate is too high.
	 */
	public function handle_create_batch_action( int $batch_number ) {
		$create_batch_hook = $this->get_create_batch_hook();
		$create_batch_args = [ $batch_number ];

		$this->monitor->validate_failure_rate( $this, $create_batch_hook, $create_batch_args );
		if ( $this->retry_on_timeout ) {
			$this->monitor->attach_timeout_monitor( $create_batch_hook, $create_batch_args );
		}

		$items = $this->get_batch( $batch_number );

		if ( empty( $items ) ) {
			// if no more items the job is complete
			$this->handle_complete( $batch_number );
		} else {
			// if items, schedule the process action
			$this->schedule_process_action( $items );

			// Add another "create_batch" action to handle unfiltered items.
			// The last batch created here will be an empty batch, it
			// will call "handle_complete" to finish the job.
			$this->schedule_create_batch_action( $batch_number + 1 );
		}

		$this->monitor->detach_timeout_monitor( $create_batch_hook, $create_batch_args );
	}

	/**
	 * Get job batch size.
	 *
	 * @return int
	 */
	protected function get_batch_size(): int {
		/**
		 * Filters the batch size for the job.
		 *
		 * @param string Job's name
		 */
		return apply_filters( 'woocommerce_gla_batched_job_size', 100, $this->get_name() );
	}

	/**
	 * Get the query offset based on a given batch number and the specified batch size.
	 *
	 * @param int $batch_number
	 *
	 * @return int
	 */
	protected function get_query_offset( int $batch_number ): int {
		return $this->get_batch_size() * ( $batch_number - 1 );
	}

	/**
	 * Schedule a new "create batch" action to run immediately.
	 *
	 * @param int $batch_number The batch number for the new batch.
	 */
	protected function schedule_create_batch_action( int $batch_number ) {
		if ( $this->can_schedule( [ $batch_number ] ) ) {
			$this->action_scheduler->schedule_immediate( $this->get_create_batch_hook(), [ $batch_number ] );
		}
	}

	/**
	 * Schedule a new "process" action to run immediately.
	 *
	 * @param int[] $items Array of item ids.
	 */
	protected function schedule_process_action( array $items ) {
		if ( ! $this->is_processing( $items ) ) {
			$this->action_scheduler->schedule_immediate( $this->get_process_item_hook(), [ $items ] );
		}
	}

	/**
	 * Check if this job is running.
	 *
	 * The job is considered to be running if a "create_batch" action is currently pending or in-progress.
	 *
	 * @param array|null $args
	 *
	 * @return bool
	 */
	protected function is_running( ?array $args = [] ): bool {
		return $this->action_scheduler->has_scheduled_action( $this->get_create_batch_hook(), $args );
	}

	/**
	 * Check if this job is processing the given items.
	 *
	 * The job is considered to be processing if a "process_item" action is currently pending or in-progress.
	 *
	 * @param array $items
	 *
	 * @return bool
	 */
	protected function is_processing( array $items = [] ): bool {
		return $this->action_scheduler->has_scheduled_action( $this->get_process_item_hook(), [ $items ] );
	}

	/**
	 * Called when the job is completed.
	 *
	 * @param int $final_batch_number The final batch number when the job was completed.
	 *                                  If equal to 1 then no items were processed by the job.
	 */
	protected function handle_complete( int $final_batch_number ) {
		// Optionally over-ride this method in child class.
	}

	/**
	 * Get a single batch of items.
	 *
	 * If no items are returned the job will stop.
	 *
	 * @param int $batch_number The batch number increments for each new batch in the job cycle.
	 *
	 * @return array
	 *
	 * @throws Exception If an error occurs. The exception will be logged by ActionScheduler.
	 */
	abstract protected function get_batch( int $batch_number ): array;
}
AbstractCouponSyncerJob.php000064400000003712151542752550012041 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs;

use Automattic\WooCommerce\GoogleListingsAndAds\ActionScheduler\ActionSchedulerInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantCenterService;
use Automattic\WooCommerce\GoogleListingsAndAds\Coupon\CouponHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Coupon\CouponSyncer;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WC;

defined( 'ABSPATH' ) || exit;

/**
 * Class AbstractCouponSyncerJob
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Jobs
 */
abstract class AbstractCouponSyncerJob extends AbstractActionSchedulerJob {

	/**
	 * @var CouponHelper
	 */
	protected $coupon_helper;

	/**
	 * @var CouponSyncer
	 */
	protected $coupon_syncer;

	/**
	 * @var WC
	 */
	protected $wc;

	/**
	 * @var MerchantCenterService
	 */
	protected $merchant_center;

	/**
	 * AbstractCouponSyncerJob constructor.
	 *
	 * @param ActionSchedulerInterface  $action_scheduler
	 * @param ActionSchedulerJobMonitor $monitor
	 * @param CouponHelper              $coupon_helper
	 * @param CouponSyncer              $coupon_syncer
	 * @param WC                        $wc
	 * @param MerchantCenterService     $merchant_center
	 */
	public function __construct(
		ActionSchedulerInterface $action_scheduler,
		ActionSchedulerJobMonitor $monitor,
		CouponHelper $coupon_helper,
		CouponSyncer $coupon_syncer,
		WC $wc,
		MerchantCenterService $merchant_center
	) {
		$this->coupon_helper   = $coupon_helper;
		$this->coupon_syncer   = $coupon_syncer;
		$this->wc              = $wc;
		$this->merchant_center = $merchant_center;
		parent::__construct( $action_scheduler, $monitor );
	}


	/**
	 * Can the job be scheduled.
	 *
	 * @param array|null $args
	 *
	 * @return bool Returns true if the job can be scheduled.
	 */
	public function can_schedule( $args = [] ): bool {
		return ! $this->is_running( $args ) && $this->merchant_center->should_push();
	}
}
AbstractProductSyncerBatchedJob.php000064400000004701151542752550013470 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs;

use Automattic\WooCommerce\GoogleListingsAndAds\ActionScheduler\ActionSchedulerInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantCenterService;
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantStatuses;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\BatchProductHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\ProductRepository;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\ProductSyncer;

defined( 'ABSPATH' ) || exit;

/**
 * Class AbstractGoogleBatchedJob
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Jobs
 */
abstract class AbstractProductSyncerBatchedJob extends AbstractBatchedActionSchedulerJob {

	/**
	 * @var ProductSyncer
	 */
	protected $product_syncer;

	/**
	 * @var ProductRepository
	 */
	protected $product_repository;

	/**
	 * @var BatchProductHelper
	 */
	protected $batch_product_helper;

	/**
	 * @var MerchantCenterService
	 */
	protected $merchant_center;

	/**
	 * @var MerchantStatuses
	 */
	protected $merchant_statuses;

	/**
	 * AbstractProductSyncerBatchedJob constructor.
	 *
	 * @param ActionSchedulerInterface  $action_scheduler
	 * @param ActionSchedulerJobMonitor $monitor
	 * @param ProductSyncer             $product_syncer
	 * @param ProductRepository         $product_repository
	 * @param BatchProductHelper        $batch_product_helper
	 * @param MerchantCenterService     $merchant_center
	 * @param MerchantStatuses          $merchant_statuses
	 */
	public function __construct(
		ActionSchedulerInterface $action_scheduler,
		ActionSchedulerJobMonitor $monitor,
		ProductSyncer $product_syncer,
		ProductRepository $product_repository,
		BatchProductHelper $batch_product_helper,
		MerchantCenterService $merchant_center,
		MerchantStatuses $merchant_statuses
	) {
		$this->batch_product_helper = $batch_product_helper;
		$this->product_syncer       = $product_syncer;
		$this->product_repository   = $product_repository;
		$this->merchant_center      = $merchant_center;
		$this->merchant_statuses    = $merchant_statuses;
		parent::__construct( $action_scheduler, $monitor );
	}

	/**
	 * Can the job be scheduled.
	 *
	 * @param array|null $args
	 *
	 * @return bool Returns true if the job can be scheduled.
	 */
	public function can_schedule( $args = [] ): bool {
		return ! $this->is_running( $args ) && $this->merchant_center->should_push();
	}
}
AbstractProductSyncerJob.php000064400000003513151542752550012215 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs;

use Automattic\WooCommerce\GoogleListingsAndAds\ActionScheduler\ActionSchedulerInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantCenterService;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\ProductRepository;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\ProductSyncer;

defined( 'ABSPATH' ) || exit;

/**
 * Class AbstractProductSyncerJob
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Jobs
 */
abstract class AbstractProductSyncerJob extends AbstractActionSchedulerJob {

	/**
	 * @var ProductSyncer
	 */
	protected $product_syncer;

	/**
	 * @var ProductRepository
	 */
	protected $product_repository;

	/**
	 * @var MerchantCenterService
	 */
	protected $merchant_center;

	/**
	 * AbstractProductSyncerJob constructor.
	 *
	 * @param ActionSchedulerInterface  $action_scheduler
	 * @param ActionSchedulerJobMonitor $monitor
	 * @param ProductSyncer             $product_syncer
	 * @param ProductRepository         $product_repository
	 * @param MerchantCenterService     $merchant_center
	 */
	public function __construct(
		ActionSchedulerInterface $action_scheduler,
		ActionSchedulerJobMonitor $monitor,
		ProductSyncer $product_syncer,
		ProductRepository $product_repository,
		MerchantCenterService $merchant_center
	) {
		$this->product_syncer     = $product_syncer;
		$this->product_repository = $product_repository;
		$this->merchant_center    = $merchant_center;
		parent::__construct( $action_scheduler, $monitor );
	}

	/**
	 * Can the job be scheduled.
	 *
	 * @param array|null $args
	 *
	 * @return bool Returns true if the job can be scheduled.
	 */
	public function can_schedule( $args = [] ): bool {
		return ! $this->is_running( $args ) && $this->merchant_center->should_push();
	}
}
ActionSchedulerJobInterface.php000064400000001421151542752550012616 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs;

defined( 'ABSPATH' ) || exit;

/**
 * Interface ActionSchedulerJobInterface
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Jobs
 */
interface ActionSchedulerJobInterface extends JobInterface {

	/**
	 * Get the hook name for the "process item" action.
	 *
	 * This method is required by the job monitor.
	 *
	 * @return string
	 */
	public function get_process_item_hook(): string;

	/**
	 * Can the job be scheduled.
	 *
	 * @param array|null $args
	 *
	 * @return bool Returns true if the job can be scheduled.
	 */
	public function can_schedule( $args = [] ): bool;

	/**
	 * Schedule the job.
	 *
	 * @param array $args
	 */
	public function schedule( array $args = [] );
}
ActionSchedulerJobMonitor.php000064400000015610151542752550012352 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs;

use Automattic\WooCommerce\GoogleListingsAndAds\ActionScheduler\ActionSchedulerInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\PluginHelper;

defined( 'ABSPATH' ) || exit;

/**
 * Class ActionSchedulerJobMonitor
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Jobs
 */
class ActionSchedulerJobMonitor implements Service {

	use PluginHelper;

	/**
	 * @var ActionSchedulerInterface
	 */
	protected $action_scheduler;

	/**
	 * @var bool[] Array of `true` values for each job that is monitored. A hash string generated by `self::get_job_hash`
	 *      is used as keys.
	 */
	protected $monitored_hooks = [];

	/**
	 * ActionSchedulerJobMonitor constructor.
	 *
	 * @param ActionSchedulerInterface $action_scheduler
	 */
	public function __construct( ActionSchedulerInterface $action_scheduler ) {
		$this->action_scheduler = $action_scheduler;
	}

	/**
	 * Check whether the failure rate is above the specified threshold within the timeframe.
	 *
	 * To protect against failing jobs running forever the job's failure rate is checked before creating a new batch.
	 * By default, a job is stopped if it has 5 failures in the last hour.
	 *
	 * @param ActionSchedulerJobInterface $job
	 * @param string                      $hook The job action hook.
	 * @param array|null                  $args The job arguments.
	 *
	 * @throws JobException If the job's error rate is above the threshold.
	 */
	public function validate_failure_rate( ActionSchedulerJobInterface $job, string $hook, ?array $args = null ) {
		if ( $this->is_failure_rate_above_threshold( $hook, $args ) ) {
			throw JobException::stopped_due_to_high_failure_rate( $job->get_name() );
		}
	}

	/**
	 * Reschedules the job if it has failed due to timeout.
	 *
	 * @param string     $hook The job action hook.
	 * @param array|null $args The job arguments.
	 *
	 * @since 1.7.0
	 */
	public function attach_timeout_monitor( string $hook, ?array $args = null ) {
		$this->monitored_hooks[ self::get_job_hash( $hook, $args ) ] = true;
		add_action(
			'action_scheduler_unexpected_shutdown',
			[ $this, 'reschedule_if_timeout' ],
			10,
			2
		);
	}

	/**
	 * Detaches the timeout monitor that handles rescheduling jobs on timeout.
	 *
	 * @param string     $hook The job action hook.
	 * @param array|null $args The job arguments.
	 *
	 * @since 1.7.0
	 */
	public function detach_timeout_monitor( string $hook, ?array $args = null ) {
		unset( $this->monitored_hooks[ self::get_job_hash( $hook, $args ) ] );
		remove_action( 'action_scheduler_unexpected_shutdown', [ $this, 'reschedule_if_timeout' ] );
	}

	/**
	 * Reschedules an action if it has failed due to a timeout error.
	 *
	 * The number of previous failures will be checked before rescheduling the action, and it must be below the
	 * specified threshold in `self::get_failure_rate_threshold` within the timeframe specified in
	 * `self::get_failure_timeframe` for the action to be rescheduled.
	 *
	 * @param int   $action_id
	 * @param array $error
	 *
	 * @since 1.7.0
	 */
	public function reschedule_if_timeout( $action_id, $error ) {
		if ( ! empty( $error ) && $this->is_timeout_error( $error ) ) {
			$action = $this->action_scheduler->fetch_action( $action_id );
			$hook   = $action->get_hook();
			$args   = $action->get_args();

			// Confirm that the job is initiated by GLA and monitored by this instance.
			// The `self::attach_timeout_monitor` method will register the job's hook and arguments hash into the $monitored_hooks variable.
			if ( $this->get_slug() !== $action->get_group() || ! $this->is_monitored_for_timeout( $hook, $args ) ) {
				return;
			}

			// Check if the job has not failed more than the allowed threshold.
			if ( $this->is_failure_rate_above_threshold( $hook, $args ) ) {
				do_action(
					'woocommerce_gla_debug_message',
					sprintf( 'The %s job failed too many times, not rescheduling.', $hook ),
					__METHOD__
				);

				return;
			}

			do_action(
				'woocommerce_gla_debug_message',
				sprintf( 'The %s job has failed due to a timeout error, rescheduling...', $hook ),
				__METHOD__
			);

			$this->action_scheduler->schedule_immediate( $hook, $args );
		}
	}

	/**
	 * Determines whether the given error is an execution "timeout" error.
	 *
	 * @param array $error An associative array describing the error with keys "type", "message", "file" and "line".
	 *
	 * @return bool
	 *
	 * @link https://www.php.net/manual/en/function.error-get-last.php
	 *
	 * @since 1.7.0
	 */
	protected function is_timeout_error( array $error ): bool {
		return isset( $error['type'] ) && $error['type'] === E_ERROR &&
			isset( $error['message'] ) && strpos( $error ['message'], 'Maximum execution time' ) !== false;
	}

	/**
	 * Check whether the job's failure rate is above the specified threshold within the timeframe.
	 *
	 * @param string     $hook The job action hook.
	 * @param array|null $args The job arguments.
	 *
	 * @return bool True if the job's error rate is above the threshold, and false otherwise.
	 *
	 * @see ActionSchedulerJobMonitor::get_failure_rate_threshold()
	 * @see ActionSchedulerJobMonitor::get_failure_timeframe()
	 *
	 * @since 1.7.0
	 */
	protected function is_failure_rate_above_threshold( string $hook, ?array $args = null ): bool {
		$failed_actions = $this->action_scheduler->search(
			[
				'hook'         => $hook,
				'args'         => $args,
				'status'       => $this->action_scheduler::STATUS_FAILED,
				'per_page'     => $this->get_failure_rate_threshold(),
				'date'         => gmdate( 'U' ) - $this->get_failure_timeframe(),
				'date_compare' => '>',
			],
			'ids'
		);

		return count( $failed_actions ) >= $this->get_failure_rate_threshold();
	}

	/**
	 * Get the job failure rate threshold (per timeframe).
	 *
	 * @return int
	 */
	protected function get_failure_rate_threshold(): int {
		return absint( apply_filters( 'woocommerce_gla_job_failure_rate_threshold', 3 ) );
	}

	/**
	 * Get the job failure timeframe (in seconds).
	 *
	 * @return int
	 */
	protected function get_failure_timeframe(): int {
		return absint( apply_filters( 'woocommerce_gla_job_failure_timeframe', 2 * HOUR_IN_SECONDS ) );
	}

	/**
	 * Generates a unique hash (checksum) for each job using its hook name and arguments.
	 *
	 * @param string     $hook
	 * @param array|null $args
	 *
	 * @return string
	 *
	 * @since 1.7.0
	 */
	protected static function get_job_hash( string $hook, ?array $args = null ): string {
		return hash( 'crc32b', $hook . wp_json_encode( $args ) );
	}

	/**
	 * Determines whether the given set of job hook and arguments is monitored for timeout.
	 *
	 * @param string     $hook
	 * @param array|null $args
	 *
	 * @return bool
	 *
	 * @since 1.7.0
	 */
	protected function is_monitored_for_timeout( string $hook, ?array $args = null ): bool {
		return isset( $this->monitored_hooks[ self::get_job_hash( $hook, $args ) ] );
	}
}
BatchedActionSchedulerJobInterface.php000064400000001660151542752550014076 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs;

use Exception;

defined( 'ABSPATH' ) || exit;

/**
 * Interface BatchedActionSchedulerJobInterface
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Jobs
 */
interface BatchedActionSchedulerJobInterface extends ActionSchedulerJobInterface {

	/**
	 * Handles batch creation action hook.
	 *
	 * @hooked gla/jobs/{$job_name}/create_batch
	 *
	 * @param int $batch_number The batch number increments for each new batch in the job cycle.
	 *
	 * @throws Exception If an error occurs.
	 */
	public function handle_create_batch_action( int $batch_number );

	/**
	 * Handles processing a single batch action hook.
	 *
	 * @hooked gla/jobs/{$job_name}/process_item
	 *
	 * @param array $items The job items from the current batch.
	 *
	 * @throws Exception If an error occurs.
	 */
	public function handle_process_items_action( array $items );
}
CleanupProductsJob.php000064400000003133151542752550011036 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs;

use Automattic\WooCommerce\GoogleListingsAndAds\Product\ProductSyncerException;

defined( 'ABSPATH' ) || exit;

/**
 * Class CleanupProductsJob
 *
 * Deletes all stale Google products from Google Merchant Center.
 * Stale products are the ones that are no longer relevant due to a change in merchant settings.
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Jobs
 */
class CleanupProductsJob extends AbstractProductSyncerBatchedJob {

	/**
	 * Get the name of the job.
	 *
	 * @return string
	 */
	public function get_name(): string {
		return 'cleanup_products_job';
	}

	/**
	 * Get a single batch of items.
	 *
	 * If no items are returned the job will stop.
	 *
	 * @param int $batch_number The batch number increments for each new batch in the job cycle.
	 *
	 * @return array
	 */
	public function get_batch( int $batch_number ): array {
		return $this->product_repository->find_synced_product_ids( [], $this->get_batch_size(), $this->get_query_offset( $batch_number ) );
	}

	/**
	 * Process batch items.
	 *
	 * @param int[] $items A single batch of WooCommerce product IDs from the get_batch() method.
	 *
	 * @throws ProductSyncerException If an error occurs. The exception will be logged by ActionScheduler.
	 */
	protected function process_items( array $items ) {
		$products      = $this->product_repository->find_by_ids( $items );
		$stale_entries = $this->batch_product_helper->generate_stale_products_request_entries( $products );
		$this->product_syncer->delete_by_batch_requests( $stale_entries );
	}
}
CleanupSyncedProducts.php000064400000004254151542752550011556 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs;

defined( 'ABSPATH' ) || exit;

/**
 * Class CleanupSyncedProducts
 *
 * Marks products as unsynced when we disconnect the Merchant Account.
 * The Merchant Center must remain disconnected during the job. If it is
 * reconnected during the job it will stop processing, since the
 * ProductSyncer will take over and update all the products.
 *
 * @since 1.12.0
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Jobs
 */
class CleanupSyncedProducts extends AbstractProductSyncerBatchedJob {

	/**
	 * Get whether Merchant Center is connected.
	 *
	 * @return bool
	 */
	public function is_mc_connected(): bool {
		return $this->merchant_center->is_connected();
	}

	/**
	 * Get the name of the job.
	 *
	 * @return string
	 */
	public function get_name(): string {
		return 'cleanup_synced_products';
	}

	/**
	 * Can the job be scheduled.
	 * Only cleanup when the Merchant Center is disconnected.
	 *
	 * @param array|null $args
	 *
	 * @return bool Returns true if the job can be scheduled.
	 */
	public function can_schedule( $args = [] ): bool {
		return ! $this->is_running( $args ) && ! $this->is_mc_connected();
	}

	/**
	 * Get a single batch of items.
	 *
	 * If no items are returned the job will stop.
	 *
	 * @param int $batch_number The batch number increments for each new batch in the job cycle.
	 *
	 * @return int[]
	 */
	public function get_batch( int $batch_number ): array {
		return $this->product_repository->find_synced_product_ids( [], $this->get_batch_size(), $this->get_query_offset( $batch_number ) );
	}

	/**
	 * Process batch items.
	 * Skips processing if the Merchant Center has been connected again.
	 *
	 * @param int[] $items A single batch of WooCommerce product IDs from the get_batch() method.
	 */
	protected function process_items( array $items ) {
		if ( $this->is_mc_connected() ) {
			do_action(
				'woocommerce_gla_debug_message',
				sprintf(
					'Skipping cleanup of unsynced products because Merchant Center is connected: %s',
					implode( ',', $items )
				),
				__METHOD__
			);
			return;
		}

		$this->batch_product_helper->mark_batch_as_unsynced( $items );
	}
}
DeleteAllProducts.php000064400000003564151542752550010657 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs;

use Automattic\WooCommerce\GoogleListingsAndAds\Product\ProductSyncerException;

defined( 'ABSPATH' ) || exit;

/**
 * Class DeleteAllProducts
 *
 * Deletes all WooCommerce products from Google Merchant Center.
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Jobs
 */
class DeleteAllProducts extends AbstractProductSyncerBatchedJob {

	/**
	 * Get the name of the job.
	 *
	 * @return string
	 */
	public function get_name(): string {
		return 'delete_all_products';
	}

	/**
	 * Get a single batch of items.
	 *
	 * If no items are returned the job will stop.
	 *
	 * @param int $batch_number The batch number increments for each new batch in the job cycle.
	 *
	 * @return int[]
	 */
	protected function get_batch( int $batch_number ): array {
		return $this->product_repository->find_synced_product_ids( [], $this->get_batch_size(), $this->get_query_offset( $batch_number ) );
	}

	/**
	 * Process batch items.
	 *
	 * @param int[] $items A single batch of WooCommerce product IDs from the get_batch() method.
	 *
	 * @throws ProductSyncerException If an error occurs. The exception will be logged by ActionScheduler.
	 */
	protected function process_items( array $items ) {
		$products        = $this->product_repository->find_by_ids( $items );
		$product_entries = $this->batch_product_helper->generate_delete_request_entries( $products );
		$this->product_syncer->delete_by_batch_requests( $product_entries );
	}

	/**
	 * Called when the job is completed.
	 *
	 * @since 2.6.4
	 *
	 * @param int $final_batch_number The final batch number when the job was completed.
	 *                                If equal to 1 then no items were processed by the job.
	 */
	protected function handle_complete( int $final_batch_number ) {
		$this->merchant_statuses->maybe_refresh_status_data( true );
	}
}
DeleteCoupon.php000064400000005125151542752550007661 0ustar00<?php
declare(strict_types = 1);
namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs;

use Automattic\WooCommerce\GoogleListingsAndAds\Coupon\CouponSyncerException;
use Automattic\WooCommerce\GoogleListingsAndAds\Google\DeleteCouponEntry;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\Promotion as GooglePromotion;

defined( 'ABSPATH' ) || exit();

/**
 * Class DeleteCoupon
 *
 * Delete existing WooCommerce coupon from Google Merchant Center.
 *
 * Note: The job will not start if it is already running.
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Jobs
 */
class DeleteCoupon extends AbstractCouponSyncerJob implements
	StartOnHookInterface {

	/**
	 * Get the name of the job.
	 *
	 * @return string
	 */
	public function get_name(): string {
		return 'delete_coupon';
	}

	/**
	 * Process an item.
	 *
	 * @param array[] $coupon_entries
	 *
	 * @throws JobException If no valid coupon data is provided as argument. The exception will be logged by ActionScheduler.
	 * @throws CouponSyncerException If an error occurs. The exception will be logged by ActionScheduler.
	 */
	public function process_items( array $coupon_entries ) {
		$wc_coupon_id     = $coupon_entries[0] ?? null;
		$google_promotion = $coupon_entries[1] ?? null;
		$google_ids       = $coupon_entries[2] ?? null;
		if ( ( ! is_int( $wc_coupon_id ) ) || empty( $google_promotion ) || empty( $google_ids ) ) {
			throw JobException::item_not_provided(
				'Required data for the coupon to delete'
			);
		}

		$this->coupon_syncer->delete(
			new DeleteCouponEntry(
				$wc_coupon_id,
				new GooglePromotion( $google_promotion ),
				$google_ids
			)
		);
	}

	/**
	 * Schedule the job.
	 *
	 * @param array[] $args
	 *
	 * @throws JobException If no coupon is provided as argument. The exception will be logged by ActionScheduler.
	 */
	public function schedule( array $args = [] ) {
		$coupon_entry = $args[0] ?? null;

		if ( ! $coupon_entry instanceof DeleteCouponEntry ) {
			throw JobException::item_not_provided(
				'DeleteCouponEntry for the coupon to delete'
			);
		}

		if ( $this->can_schedule( [ $coupon_entry ] ) ) {
			$this->action_scheduler->schedule_immediate(
				$this->get_process_item_hook(),
				[
					[
						$coupon_entry->get_wc_coupon_id(),
						$coupon_entry->get_google_promotion(),
						$coupon_entry->get_synced_google_ids(),
					],
				]
			);
		}
	}

	/**
	 * Get the name of an action hook to attach the job's start method to.
	 *
	 * @return StartHook
	 */
	public function get_start_hook(): StartHook {
		return new StartHook( "{$this->get_hook_base_name()}start" );
	}
}
DeleteProducts.php000064400000004403151542752550010217 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs;

use Automattic\WooCommerce\GoogleListingsAndAds\Google\BatchProductIDRequestEntry;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\ProductSyncerException;
use Automattic\WooCommerce\GoogleListingsAndAds\Value\ProductIDMap;

defined( 'ABSPATH' ) || exit;

/**
 * Class DeleteProducts
 *
 * Deletes WooCommerce products from Google Merchant Center.
 *
 * Note: The job will not start if it is already running.
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Jobs
 */
class DeleteProducts extends AbstractProductSyncerJob implements StartOnHookInterface {

	/**
	 * Get the name of the job.
	 *
	 * @return string
	 */
	public function get_name(): string {
		return 'delete_products';
	}

	/**
	 * Process an item.
	 *
	 * @param string[] $product_id_map An array of Google product IDs mapped to WooCommerce product IDs as their key.
	 *
	 * @throws ProductSyncerException If an error occurs. The exception will be logged by ActionScheduler.
	 */
	public function process_items( array $product_id_map ) {
		$product_entries = BatchProductIDRequestEntry::create_from_id_map( new ProductIDMap( $product_id_map ) );
		$this->product_syncer->delete_by_batch_requests( $product_entries );
	}

	/**
	 * Schedule the job.
	 *
	 * @param array $args
	 *
	 * @throws JobException If no product is provided as argument. The exception will be logged by ActionScheduler.
	 */
	public function schedule( array $args = [] ) {
		$args   = $args[0] ?? [];
		$id_map = ( new ProductIDMap( $args ) )->get();

		if ( empty( $id_map ) ) {
			throw JobException::item_not_provided( 'Array of WooCommerce product IDs' );
		}

		if ( did_action( 'woocommerce_gla_batch_retry_delete_products' ) ) {
			// Retry after one minute.
			$this->action_scheduler->schedule_single( gmdate( 'U' ) + 60, $this->get_process_item_hook(), [ $id_map ] );
		} elseif ( $this->can_schedule( [ $id_map ] ) ) {
			$this->action_scheduler->schedule_immediate( $this->get_process_item_hook(), [ $id_map ] );
		}
	}

	/**
	 * Get an action hook to attach the job's start method to.
	 *
	 * @return StartHook
	 */
	public function get_start_hook(): StartHook {
		return new StartHook( 'woocommerce_gla_batch_retry_delete_products', 1 );
	}
}
JobException.php000064400000003636151542752550007671 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs;

use Automattic\WooCommerce\GoogleListingsAndAds\Exception\GoogleListingsAndAdsException;
use RuntimeException;

defined( 'ABSPATH' ) || exit;

/**
 * Class JobException
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Jobs
 */
class JobException extends RuntimeException implements GoogleListingsAndAdsException {

	/**
	 * Create a new exception instance for when a job item is not found.
	 *
	 * @return static
	 */
	public static function item_not_found(): JobException {
		return new static( __( 'Job item not found.', 'google-listings-and-ads' ) );
	}

	/**
	 * Create a new exception instance for when a required job item is not provided.
	 *
	 * @param string $item The item name.
	 *
	 * @return static
	 */
	public static function item_not_provided( string $item ): JobException {
		return new static(
			sprintf(
			/* translators: %s: the job item name */
				__( 'Required job item "%s" not provided.', 'google-listings-and-ads' ),
				$item
			)
		);
	}

	/**
	 * Create a new exception instance for when a job is stopped due to a high failure rate.
	 *
	 * @param string $job_name
	 *
	 * @return static
	 */
	public static function stopped_due_to_high_failure_rate( string $job_name ): JobException {
		return new static(
			sprintf(
				/* translators: %s: the job name */
				__( 'The "%s" job was stopped because its failure rate is above the allowed threshold.', 'google-listings-and-ads' ),
				$job_name
			)
		);
	}

	/**
	 * Create a new exception instance for when a job class is not found.
	 *
	 * @param string $job_classname
	 *
	 * @return static
	 */
	public static function job_does_not_exist( string $job_classname ): JobException {
		return new static(
			sprintf(
				/* translators: %s: the job classname */
				__( 'The job "%s" does not exist.', 'google-listings-and-ads' ),
				$job_classname
			)
		);
	}
}
JobInitializer.php000064400000004360151542752550010211 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs;

use Automattic\WooCommerce\GoogleListingsAndAds\ActionScheduler\ActionSchedulerInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Conditional;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Registerable;
use DateTime;

defined( 'ABSPATH' ) || exit;

/**
 * Class JobInitializer
 *
 * Initializes all jobs when certain conditions are met (e.g. the request is async or initiated by CRON, CLI, etc.).
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Jobs
 */
class JobInitializer implements Registerable, Conditional {

	/**
	 * @var JobRepository
	 */
	protected $job_repository;

	/**
	 * @var ActionSchedulerInterface
	 */
	protected $action_scheduler;

	/**
	 * JobInitializer constructor.
	 *
	 * @param JobRepository            $job_repository
	 * @param ActionSchedulerInterface $action_scheduler
	 */
	public function __construct( JobRepository $job_repository, ActionSchedulerInterface $action_scheduler ) {
		$this->job_repository   = $job_repository;
		$this->action_scheduler = $action_scheduler;
	}

	/**
	 * Initialize all jobs.
	 */
	public function register(): void {
		foreach ( $this->job_repository->list() as $job ) {
			$job->init();

			if ( $job instanceof StartOnHookInterface ) {
				add_action(
					$job->get_start_hook()->get_hook(),
					function ( ...$args ) use ( $job ) {
						$job->schedule( $args );
					},
					10,
					$job->get_start_hook()->get_argument_count()
				);
			}

			if (
				$job instanceof RecurringJobInterface &&
				! $this->action_scheduler->has_scheduled_action( $job->get_start_hook()->get_hook() ) &&
				$job->can_schedule()
			) {

				$recurring_date_time = new DateTime( 'tomorrow 3am', wp_timezone() );
				$schedule            = '0 3 * * *'; // 3 am every day
				$this->action_scheduler->schedule_cron( $recurring_date_time->getTimestamp(), $schedule, $job->get_start_hook()->get_hook() );
			}
		}
	}

	/**
	 * Check whether this object is currently needed.
	 *
	 * @return bool Whether the object is needed.
	 */
	public static function is_needed(): bool {
		return ( defined( 'DOING_AJAX' ) || defined( 'DOING_CRON' ) || ( defined( 'WP_CLI' ) && WP_CLI ) || is_admin() );
	}
}
JobInterface.php000064400000001251151542752550007622 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs;

use Automattic\WooCommerce\GoogleListingsAndAds\Internal\DependencyManagement\JobServiceProvider;

defined( 'ABSPATH' ) || exit;

/**
 * Interface JobInterface
 *
 * Note: In order for the jobs to be initialized/registered, they need to be added to the container.
 *
 * @see JobServiceProvider to add job classes to the container.
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Jobs
 */
interface JobInterface {

	/**
	 * Get the name of the job.
	 *
	 * @return string
	 */
	public function get_name(): string;

	/**
	 * Init the job.
	 */
	public function init(): void;
}
JobRepository.php000064400000003020151542752550010075 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs;

use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\Internal\ContainerAwareTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\Internal\Interfaces\ContainerAwareInterface;
use Exception;

defined( 'ABSPATH' ) || exit;

/**
 * Class JobRepository
 *
 * ContainerAware used for:
 * - JobInterface
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Jobs
 */
class JobRepository implements ContainerAwareInterface, Service {

	use ContainerAwareTrait;

	/**
	 * @var JobInterface[] indexed by class name.
	 */
	protected $jobs = [];

	/**
	 * Fetch all jobs from Container.
	 *
	 * @return JobInterface[]
	 */
	public function list(): array {
		foreach ( $this->container->get( JobInterface::class ) as $job ) {
			$this->jobs[ get_class( $job ) ] = $job;
		}

		return $this->jobs;
	}

	/**
	 * Fetch job from Container (or cache if previously fetched).
	 *
	 * @param string $classname Job class name.
	 *
	 * @return JobInterface
	 *
	 * @throws JobException If the job is not found.
	 */
	public function get( string $classname ): JobInterface {
		if ( ! isset( $this->jobs[ $classname ] ) ) {
			try {
				$job = $this->container->get( $classname );
			} catch ( Exception $e ) {
				throw JobException::job_does_not_exist( $classname );
			}

			$classname                = get_class( $job );
			$this->jobs[ $classname ] = $job;
		}

		return $this->jobs[ $classname ];
	}
}
MigrateGTIN.php000064400000011402151542752550007340 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs;

use Automattic\WooCommerce\GoogleListingsAndAds\ActionScheduler\ActionSchedulerInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\HelperTraits\GTINMigrationUtilities;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes\AttributeManager;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\ProductRepository;
use Exception;

defined( 'ABSPATH' ) || exit;

/**
 * Class MigrateGTIN
 *
 * Schedules GTIN migration for all the products in the store.
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Jobs
 * @since 2.9.0
 */
class MigrateGTIN extends AbstractBatchedActionSchedulerJob implements OptionsAwareInterface {
	use OptionsAwareTrait;
	use GTINMigrationUtilities;

	public const GTIN_MIGRATION_COMPLETED   = 'completed';
	public const GTIN_MIGRATION_STARTED     = 'started';
	public const GTIN_MIGRATION_READY       = 'ready';
	public const GTIN_MIGRATION_UNAVAILABLE = 'unavailable';

	/**
	 * @var ProductRepository
	 */
	protected $product_repository;


	/**
	 * @var AttributeManager
	 */
	protected $attribute_manager;


	/**
	 * MigrateGTIN constructor.
	 *
	 * @param ActionSchedulerInterface  $action_scheduler
	 * @param ActionSchedulerJobMonitor $monitor
	 * @param ProductRepository         $product_repository
	 * @param AttributeManager          $attribute_manager
	 */
	public function __construct( ActionSchedulerInterface $action_scheduler, ActionSchedulerJobMonitor $monitor, ProductRepository $product_repository, AttributeManager $attribute_manager ) {
		parent::__construct( $action_scheduler, $monitor );
		$this->product_repository = $product_repository;
		$this->attribute_manager  = $attribute_manager;
	}

	/**
	 * Get the name of the job.
	 *
	 * @return string
	 */
	public function get_name(): string {
		return 'migrate_gtin';
	}

	/**
	 * Can the job be scheduled.
	 *
	 * @param array|null $args
	 *
	 * @return bool Returns true if the job can be scheduled.
	 */
	public function can_schedule( $args = [] ): bool {
		return ! parent::is_running( $args ) && $this->is_gtin_available_in_core();
	}

	/**
	 * Process batch items.
	 *
	 * @param int[] $items A single batch of WooCommerce product IDs from the get_batch() method.
	 */
	protected function process_items( array $items ) {
		// update the product core GTIN using G4W GTIN
		$products = $this->product_repository->find_by_ids( $items );
		foreach ( $products as $product ) {
			// process variations
			if ( $product instanceof \WC_Product_Variable ) {
				$variations = $product->get_children();
				$this->process_items( $variations );
				continue;
			}

			if ( $product->get_global_unique_id() ) {
				$this->debug( $this->error_gtin_already_set( $product ) );
				continue;
			}

			$gtin = $this->get_gtin( $product );

			if ( ! $gtin ) {
				$this->debug( $this->error_gtin_not_found( $product ) );
				continue;
			}

			$gtin = $this->prepare_gtin( $gtin );
			if ( ! is_numeric( $gtin ) ) {
				$this->debug( $this->error_gtin_invalid( $product, $gtin ) );
				continue;
			}

			try {
				$product->set_global_unique_id( $gtin );
				$product->save();
				$this->debug( $this->successful_migrated_gtin( $product, $gtin ) );
			} catch ( Exception $e ) {
				$this->debug( $this->error_gtin_not_saved( $product, $gtin, $e ) );
			}
		}
	}

	/**
	 * Tweak schedule function for adding a start flag.
	 *
	 * @param array $args
	 */
	public function schedule( array $args = [] ) {
		$this->options->update( OptionsInterface::GTIN_MIGRATION_STATUS, self::GTIN_MIGRATION_STARTED );
		parent::schedule( $args );
	}

	/**
	 *
	 * To run when the job is completed.
	 *
	 * @param int $final_batch_number
	 */
	public function handle_complete( int $final_batch_number ) {
		$this->options->update( OptionsInterface::GTIN_MIGRATION_STATUS, self::GTIN_MIGRATION_COMPLETED );
	}


	/**
	 * Get a single batch of items.
	 *
	 * If no items are returned the job will stop.
	 *
	 * @param int $batch_number The batch number increments for each new batch in the job cycle.
	 *
	 * @return array
	 *
	 * @throws Exception If an error occurs. The exception will be logged by ActionScheduler.
	 */
	protected function get_batch( int $batch_number ): array {
		return $this->product_repository->find_all_product_ids( $this->get_batch_size(), $this->get_query_offset( $batch_number ) );
	}

	/**
	 * Debug info in the logs.
	 *
	 * @param string $message
	 *
	 * @return void
	 */
	protected function debug( string $message ): void {
		do_action(
			'woocommerce_gla_debug_message',
			$message,
			__METHOD__
		);
	}
}
Notifications/AbstractItemNotificationJob.php000064400000010505151542752550015466 0ustar00<?php
declare(strict_types=1);

namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs\Notifications;

use Automattic\WooCommerce\GoogleListingsAndAds\Exception\InvalidValue;
use Automattic\WooCommerce\GoogleListingsAndAds\Value\NotificationStatus;

defined( 'ABSPATH' ) || exit;

/**
 * Class AbstractItemNotificationJob
 * Generic class for the Notification Jobs containing items
 *
 * @since 2.8.0
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Jobs\Notifications
 */
abstract class AbstractItemNotificationJob extends AbstractNotificationJob {

	/**
	 * Logic when processing the items
	 *
	 * @param array $args Arguments with the item id and the topic
	 */
	protected function process_items( array $args ): void {
		if ( ! isset( $args['item_id'] ) || ! isset( $args['topic'] ) ) {
			do_action(
				'woocommerce_gla_error',
				'Error sending the Notification. Topic and Item ID are mandatory',
				__METHOD__
			);
			return;
		}

		$item  = $args['item_id'];
		$topic = $args['topic'];
		$data  = $args['data'] ?? [];

		try {
			if ( $this->can_process( $item, $topic ) && $this->notifications_service->notify( $topic, $item, $data ) ) {
				$this->set_status( $item, $this->get_after_notification_status( $topic ) );
				$this->handle_notified( $topic, $item );
			}
		} catch ( InvalidValue $e ) {
			do_action(
				'woocommerce_gla_error',
				sprintf( 'Error sending Notification for - Item ID: %s - Topic: %s - Data %s. Product was deleted from the database before the notification was sent.', $item, $topic, wp_json_encode( $data ) ),
				__METHOD__
			);
		}
	}

	/**
	 * Set the notification status for the item.
	 *
	 * @param int    $item_id
	 * @param string $status
	 * @throws InvalidValue If the given ID doesn't reference a valid product.
	 */
	protected function set_status( int $item_id, string $status ): void {
		$item = $this->get_item( $item_id );
		$this->get_helper()->set_notification_status( $item, $status );
	}

	/**
	 * Get the Notification Status after the notification happens
	 *
	 * @param string $topic
	 * @return string
	 */
	protected function get_after_notification_status( string $topic ): string {
		if ( $this->is_create_topic( $topic ) ) {
			return NotificationStatus::NOTIFICATION_CREATED;
		} elseif ( $this->is_delete_topic( $topic ) ) {
			return NotificationStatus::NOTIFICATION_DELETED;
		} else {
			return NotificationStatus::NOTIFICATION_UPDATED;
		}
	}

	/**
	 * Checks if the item can be processed based on the topic.
	 * This is needed because the item can change the Notification Status before
	 * the Job process the item.
	 *
	 * @param int    $item_id
	 * @param string $topic
	 * @throws InvalidValue If the given ID doesn't reference a valid product.
	 * @return bool
	 */
	protected function can_process( int $item_id, string $topic ): bool {
		$item = $this->get_item( $item_id );

		if ( $this->is_create_topic( $topic ) ) {
			return $this->get_helper()->should_trigger_create_notification( $item );
		} elseif ( $this->is_delete_topic( $topic ) ) {
			return $this->get_helper()->should_trigger_delete_notification( $item );
		} else {
			return $this->get_helper()->should_trigger_update_notification( $item );
		}
	}

	/**
	 * Handle the item after the notification.
	 *
	 * @param string $topic
	 * @param int    $item
	 * @throws InvalidValue If the given ID doesn't reference a valid product.
	 */
	protected function handle_notified( string $topic, int $item ): void {
		if ( $this->is_delete_topic( $topic ) ) {
			$this->get_helper()->mark_as_unsynced( $this->get_item( $item ) );
		}

		if ( $this->is_create_topic( $topic ) ) {
			$this->get_helper()->mark_as_notified( $this->get_item( $item ) );
		}
	}

	/**
	 * If a topic is a delete topic
	 *
	 * @param string $topic The topic to check
	 *
	 * @return bool
	 */
	protected function is_delete_topic( $topic ): bool {
		return str_contains( $topic, '.delete' );
	}

	/**
	 * If a topic is a create topic
	 *
	 * @param string $topic The topic to check
	 *
	 * @return bool
	 */
	protected function is_create_topic( $topic ): bool {
		return str_contains( $topic, '.create' );
	}
	/**
	 * Get the item
	 *
	 * @param int $item_id
	 * @return \WC_Product|\WC_Coupon
	 */
	abstract protected function get_item( int $item_id );

	/**
	 * Get the helper
	 *
	 * @return HelperNotificationInterface
	 */
	abstract public function get_helper(): HelperNotificationInterface;
}
Notifications/AbstractNotificationJob.php000064400000005372151542752560014656 0ustar00<?php
declare(strict_types=1);

namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs\Notifications;

use Automattic\WooCommerce\GoogleListingsAndAds\ActionScheduler\ActionSchedulerInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\API\WP\NotificationsService;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\AbstractActionSchedulerJob;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\ActionSchedulerJobMonitor;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\JobInterface;

defined( 'ABSPATH' ) || exit;

/**
 * Class AbstractNotificationJob
 * Generic class for the Notifications Jobs
 *
 * @since 2.8.0
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Jobs\Notifications
 */
abstract class AbstractNotificationJob extends AbstractActionSchedulerJob implements JobInterface {

	/**
	 * @var NotificationsService $notifications_service
	 */
	protected $notifications_service;

	/**
	 * Notifications Jobs constructor.
	 *
	 * @param ActionSchedulerInterface  $action_scheduler
	 * @param ActionSchedulerJobMonitor $monitor
	 * @param NotificationsService      $notifications_service
	 */
	public function __construct(
		ActionSchedulerInterface $action_scheduler,
		ActionSchedulerJobMonitor $monitor,
		NotificationsService $notifications_service
	) {
		$this->notifications_service = $notifications_service;
		parent::__construct( $action_scheduler, $monitor );
	}

	/**
	 * Get the parent job name
	 *
	 * @return string
	 */
	public function get_name(): string {
		return 'notifications/' . $this->get_job_name();
	}


	/**
	 * Schedule the Job
	 *
	 * @param array $args
	 */
	public function schedule( array $args = [] ): void {
		if ( $this->can_schedule( [ $args ] ) ) {
			$this->action_scheduler->schedule_immediate(
				$this->get_process_item_hook(),
				[ $args ]
			);
		}
	}


	/**
	 * Can the job be scheduled.
	 *
	 * @param array|null $args
	 *
	 * @return bool Returns true if the job can be scheduled.
	 */
	public function can_schedule( $args = [] ): bool {
		/**
		 * Allow users to disable the notification job schedule.
		 *
		 * @since 2.8.0
		 *
		 * @param bool $value The current filter value. By default, it is the result of `$this->can_schedule` function.
		 * @param string $job_name The current Job name.
		 * @param array $args The arguments for the schedule function with the item id and the topic.
		 */
		return apply_filters( 'woocommerce_gla_notification_job_can_schedule', $this->notifications_service->is_ready() && parent::can_schedule( $args ), $this->get_job_name(), $args );
	}

	/**
	 * Get the child job name
	 *
	 * @return string
	 */
	abstract public function get_job_name(): string;

	/**
	 * Logic when processing the items
	 *
	 * @param array $args
	 */
	abstract protected function process_items( array $args ): void;
}
Notifications/CouponNotificationJob.php000064400000003606151542752560014354 0ustar00<?php
declare(strict_types=1);

namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs\Notifications;

use Automattic\WooCommerce\GoogleListingsAndAds\ActionScheduler\ActionSchedulerInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\API\WP\NotificationsService;
use Automattic\WooCommerce\GoogleListingsAndAds\Coupon\CouponHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\ActionSchedulerJobMonitor;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\Notifications\HelperNotificationInterface;

defined( 'ABSPATH' ) || exit;

/**
 * Class CouponNotificationJob
 * Class for the Coupons Notifications Jobs
 *
 * @since 2.8.0
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Jobs\Notifications
 */
class CouponNotificationJob extends AbstractItemNotificationJob {

	/**
	 * @var CouponHelper $helper
	 */
	protected $helper;

	/**
	 * Notifications Jobs constructor.
	 *
	 * @param ActionSchedulerInterface    $action_scheduler
	 * @param ActionSchedulerJobMonitor   $monitor
	 * @param NotificationsService        $notifications_service
	 * @param HelperNotificationInterface $coupon_helper
	 */
	public function __construct(
		ActionSchedulerInterface $action_scheduler,
		ActionSchedulerJobMonitor $monitor,
		NotificationsService $notifications_service,
		HelperNotificationInterface $coupon_helper
	) {
		$this->helper = $coupon_helper;
		parent::__construct( $action_scheduler, $monitor, $notifications_service );
	}

	/**
	 * Get the coupon
	 *
	 * @param int $item_id
	 * @return \WC_Coupon
	 */
	protected function get_item( int $item_id ) {
		return $this->helper->get_wc_coupon( $item_id );
	}

	/**
	 * Get the Coupon Helper
	 *
	 * @return HelperNotificationInterface
	 */
	public function get_helper(): HelperNotificationInterface {
		return $this->helper;
	}

	/**
	 * Get the job name
	 *
	 * @return string
	 */
	public function get_job_name(): string {
		return 'coupons';
	}
}
Notifications/HelperNotificationInterface.php000064400000002714151542752560015515 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs\Notifications;

defined( 'ABSPATH' ) || exit;

/**
 * Interface HelperNotificationInterface
 *
 * @since 2.8.0
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Jobs\Notifications
 */
interface HelperNotificationInterface {

	/**
	 * Checks if the item can be processed based on the topic.
	 *
	 * @param WC_Product|WC_Coupon $item
	 *
	 * @return bool
	 */
	public function should_trigger_create_notification( $item ): bool;


	/**
	 * Indicates if the item ready for sending a delete Notification.
	 *
	 * @param WC_Product|WC_Coupon $item
	 *
	 * @return bool
	 */
	public function should_trigger_delete_notification( $item ): bool;

	/**
	 * Indicates if the item ready for sending an update Notification.
	 *
	 * @param WC_Product|WC_Coupon $item
	 *
	 * @return bool
	 */
	public function should_trigger_update_notification( $item ): bool;

	/**
	 * Marks the item as unsynced.
	 *
	 * @param WC_Product|WC_Coupon $item
	 *
	 * @return void
	 */
	public function mark_as_unsynced( $item ): void;

	/**
	 * Set the notification status for an item.
	 *
	 * @param WC_Product|WC_Coupon $item
	 * @param string               $status
	 *
	 * @return void
	 */
	public function set_notification_status( $item, $status ): void;

	/**
	 * Marks the item as notified.
	 *
	 * @param WC_Product|WC_Coupon $item
	 *
	 * @return void
	 */
	public function mark_as_notified( $item ): void;
}
Notifications/ProductNotificationJob.php000064400000005143151542752560014527 0ustar00<?php
declare(strict_types=1);

namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs\Notifications;

use Automattic\WooCommerce\GoogleListingsAndAds\ActionScheduler\ActionSchedulerInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\API\WP\NotificationsService;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\InvalidValue;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\ActionSchedulerJobMonitor;
use Automattic\WooCommerce\GoogleListingsAndAds\PluginHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\ProductHelper;

defined( 'ABSPATH' ) || exit;

/**
 * Class ProductNotificationJob
 * Class for the Product Notifications Jobs
 *
 * @since 2.8.0
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Jobs\Notifications
 */
class ProductNotificationJob extends AbstractItemNotificationJob {

	use PluginHelper;

	/**
	 * @var ProductHelper $helper
	 */
	protected $helper;

	/**
	 * Notifications Jobs constructor.
	 *
	 * @param ActionSchedulerInterface    $action_scheduler
	 * @param ActionSchedulerJobMonitor   $monitor
	 * @param NotificationsService        $notifications_service
	 * @param HelperNotificationInterface $helper
	 */
	public function __construct(
		ActionSchedulerInterface $action_scheduler,
		ActionSchedulerJobMonitor $monitor,
		NotificationsService $notifications_service,
		HelperNotificationInterface $helper
	) {
		$this->helper = $helper;
		parent::__construct( $action_scheduler, $monitor, $notifications_service );
	}

	/**
	 * Override Product Notification adding Offer ID for deletions.
	 * The offer_id might match the real offer ID or not, depending on whether the product has been synced by us or not.
	 * Google should check on their side if the product actually exists.
	 *
	 * @param array $args Arguments with the item id and the topic.
	 */
	protected function process_items( $args ): void {
		if ( isset( $args['topic'] ) && isset( $args['item_id'] ) && $this->is_delete_topic( $args['topic'] ) ) {
			$args['data'] = [ 'offer_id' => $this->helper->get_offer_id( $args['item_id'] ) ];
		}

		parent::process_items( $args );
	}

	/**
	 * Get the product
	 *
	 * @param int $item_id
	 * @throws InvalidValue If the given ID doesn't reference a valid product.
	 *
	 * @return \WC_Product
	 */
	protected function get_item( int $item_id ) {
		return $this->helper->get_wc_product( $item_id );
	}

	/**
	 * Get the Product Helper
	 *
	 * @return ProductHelper
	 */
	public function get_helper(): HelperNotificationInterface {
		return $this->helper;
	}

	/**
	 * Get the job name
	 *
	 * @return string
	 */
	public function get_job_name(): string {
		return 'products';
	}
}
Notifications/SettingsNotificationJob.php000064400000001372151542752560014707 0ustar00<?php
declare(strict_types=1);

namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs\Notifications;

defined( 'ABSPATH' ) || exit;

/**
 * Class SettingsNotificationJob
 * Class for the Settings Notifications
 *
 * @since 2.8.0
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Jobs\Notifications
 */
class SettingsNotificationJob extends AbstractNotificationJob {

	/**
	 * Logic when processing the items
	 *
	 * @param array $args Arguments for the notification
	 */
	protected function process_items( array $args ): void {
		$this->notifications_service->notify( $this->notifications_service::TOPIC_SETTINGS_UPDATED );
	}


	/**
	 * Get the job name
	 *
	 * @return string
	 */
	public function get_job_name(): string {
		return 'settings';
	}
}
Notifications/ShippingNotificationJob.php000064400000001407151542752560014667 0ustar00<?php
declare(strict_types=1);

namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs\Notifications;

defined( 'ABSPATH' ) || exit;

/**
 * Class ShippingNotificationJob
 * Class for the Shipping Notifications
 *
 * @since 2.8.0
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Jobs\Notifications
 */
class ShippingNotificationJob extends AbstractNotificationJob {

	/**
	 * Get the job name
	 *
	 * @return string
	 */
	public function get_job_name(): string {
		return 'shipping';
	}


	/**
	 * Logic when processing the items
	 *
	 * @param array $args Arguments for the notification
	 */
	protected function process_items( array $args ): void {
		$this->notifications_service->notify( $this->notifications_service::TOPIC_SHIPPING_UPDATED, null, $args );
	}
}
ProductSyncStats.php000064400000003117151542752560010567 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs;

use Automattic\WooCommerce\GoogleListingsAndAds\ActionScheduler\ActionScheduler;

defined( 'ABSPATH' ) || exit;

/**
 * Class ProductSyncStats
 *
 * Counts how many scheduled jobs we have for syncing products.
 * A scheduled job can either be a batch or an individual product.
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Jobs
 */
class ProductSyncStats {

	/**
	 * The ActionScheduler object.
	 *
	 * @var ActionScheduler
	 */
	protected $scheduler;

	/**
	 * Job names for syncing products.
	 */
	protected const MATCHES = [
		'refresh_synced_products',
		'update_all_products',
		'update_products',
		'delete_products',
	];

	/**
	 * ProductSyncStats constructor.
	 *
	 * @param ActionScheduler $scheduler
	 */
	public function __construct( ActionScheduler $scheduler ) {
		$this->scheduler = $scheduler;
	}

	/**
	 * Check if a job name is used for product syncing.
	 *
	 * @param string $hook
	 *
	 * @return bool
	 */
	protected function job_matches( string $hook ): bool {
		foreach ( self::MATCHES as $match ) {
			if ( false !== stripos( $hook, $match ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Return the amount of product sync jobs which are pending.
	 *
	 * @return int
	 */
	public function get_count(): int {
		$count     = 0;
		$scheduled = $this->scheduler->search(
			[
				'status'   => 'pending',
				'per_page' => -1,
			]
		);

		foreach ( $scheduled as $action ) {
			if ( $this->job_matches( $action->get_hook() ) ) {
				++$count;
			}
		}

		return $count;
	}
}
RecurringJobInterface.php000064400000000643151542752560011510 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs;

defined( 'ABSPATH' ) || exit;

/**
 * Interface RecurringJobInterface
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Jobs
 */
interface RecurringJobInterface extends StartOnHookInterface {

	/**
	 * Return the recurring job's interval in seconds.
	 *
	 * @return int
	 */
	public function get_interval(): int;
}
ResubmitExpiringProducts.php000064400000003500151542752560012313 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs;

use Automattic\WooCommerce\GoogleListingsAndAds\Product\ProductSyncerException;

defined( 'ABSPATH' ) || exit;

/**
 * Class ResubmitExpiringProducts
 *
 * Resubmits all WooCommerce products that are nearly expired to Google Merchant Center.
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Jobs
 */
class ResubmitExpiringProducts extends AbstractProductSyncerBatchedJob implements RecurringJobInterface {

	/**
	 * Get the name of the job.
	 *
	 * @return string
	 */
	public function get_name(): string {
		return 'resubmit_expiring_products';
	}

	/**
	 * Get a single batch of items.
	 *
	 * If no items are returned the job will stop.
	 *
	 * @param int $batch_number The batch number increments for each new batch in the job cycle.
	 *
	 * @return array
	 */
	public function get_batch( int $batch_number ): array {
		return $this->product_repository->find_expiring_product_ids( $this->get_batch_size(), $this->get_query_offset( $batch_number ) );
	}

	/**
	 * Process batch items.
	 *
	 * @param int[] $items A single batch of WooCommerce product IDs from the get_batch() method.
	 *
	 * @throws ProductSyncerException If an error occurs. The exception will be logged by ActionScheduler.
	 */
	protected function process_items( array $items ) {
		$products = $this->product_repository->find_by_ids( $items );

		$this->product_syncer->update( $products );
	}

	/**
	 * Return the recurring job's interval in seconds.
	 *
	 * @return int
	 */
	public function get_interval(): int {
		return 24 * 60 * 60; // 24 hours
	}

	/**
	 * Get the name of an action hook to attach the job's start method to.
	 *
	 * @return StartHook
	 */
	public function get_start_hook(): StartHook {
		return new StartHook( "{$this->get_hook_base_name()}start" );
	}
}
StartHook.php000064400000001630151542752560007207 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs;

defined( 'ABSPATH' ) || exit;

/**
 * Class StartHook
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Jobs
 */
class StartHook {

	/**
	 * @var string
	 */
	protected $hook;

	/**
	 * @var int
	 */
	protected $argument_count;

	/**
	 * StartHook constructor.
	 *
	 * @param string $hook           The name of an action hook to attach the job's start method to
	 * @param int    $argument_count The number of arguments returned by the specified action hook
	 */
	public function __construct( string $hook, int $argument_count = 0 ) {
		$this->hook           = $hook;
		$this->argument_count = $argument_count;
	}

	/**
	 * @return string
	 */
	public function get_hook(): string {
		return $this->hook;
	}

	/**
	 * @return int
	 */
	public function get_argument_count(): int {
		return $this->argument_count;
	}
}
StartOnHookInterface.php000064400000001036151542752560011325 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs;

defined( 'ABSPATH' ) || exit;

/**
 * Interface StartOnHookInterface
 *
 * Action Scheduler jobs that implement this interface will start on a specific action hook.
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Jobs
 */
interface StartOnHookInterface extends ActionSchedulerJobInterface {

	/**
	 * Get an action hook to attach the job's start method to.
	 *
	 * @return StartHook
	 */
	public function get_start_hook(): StartHook;
}
SyncableProductsBatchedActionSchedulerJobTrait.php000064400000005323151542752560016467 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs;

use Automattic\WooCommerce\GoogleListingsAndAds\Product\FilteredProductList;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\JobException;
use Exception;
use WC_Product;

/*
 * Contains AbstractBatchedActionSchedulerJob methods.
 *
 * @since 2.2.0
 */
trait SyncableProductsBatchedActionSchedulerJobTrait {

	/**
	 * Get a single batch of items.
	 *
	 * If no items are returned the job will stop.
	 *
	 * @param int $batch_number The batch number increments for each new batch in the job cycle.
	 *
	 * @return WC_Product[]
	 */
	public function get_batch( int $batch_number ): array {
		return $this->get_filtered_batch( $batch_number )->get();
	}

	/**
	 * Get a single filtered batch of items.
	 *
	 * If no items are returned the job will stop.
	 *
	 * @since 1.4.0
	 *
	 * @param int $batch_number The batch number increments for each new batch in the job cycle.
	 *
	 * @return FilteredProductList
	 */
	protected function get_filtered_batch( int $batch_number ): FilteredProductList {
		return $this->product_repository->find_sync_ready_products( [], $this->get_batch_size(), $this->get_query_offset( $batch_number ) );
	}

	/**
	 * Handles batch creation action hook.
	 *
	 * @hooked gla/jobs/{$job_name}/create_batch
	 *
	 * Schedules an action to run immediately for the items in the batch.
	 * Uses the unfiltered count to check if there are additional batches.
	 *
	 * @since 1.4.0
	 *
	 * @param int $batch_number The batch number increments for each new batch in the job cycle.
	 *
	 * @throws Exception If an error occurs.
	 * @throws JobException If the job failure rate is too high.
	 */
	public function handle_create_batch_action( int $batch_number ) {
		$create_batch_hook = $this->get_create_batch_hook();
		$create_batch_args = [ $batch_number ];

		$this->monitor->validate_failure_rate( $this, $create_batch_hook, $create_batch_args );
		if ( $this->retry_on_timeout ) {
			$this->monitor->attach_timeout_monitor( $create_batch_hook, $create_batch_args );
		}

		$items = $this->get_filtered_batch( $batch_number );

		if ( 0 === $items->get_unfiltered_count() ) {
			// if no more items the job is complete
			$this->handle_complete( $batch_number );
		} else {
			// if items, schedule the process action
			if ( count( $items ) ) {
				$this->schedule_process_action( $items->get_product_ids() );
			}

			// Add another "create_batch" action to handle unfiltered items.
			// The last batch created here will be an empty batch, it
			// will call "handle_complete" to finish the job.
			$this->schedule_create_batch_action( $batch_number + 1 );
		}

		$this->monitor->detach_timeout_monitor( $create_batch_hook, $create_batch_args );
	}
}
Update/CleanupProductTargetCountriesJob.php000064400000003314151542752560015142 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs\Update;

use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\AbstractProductSyncerBatchedJob;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\ProductSyncerException;

defined( 'ABSPATH' ) || exit;

/**
 * Class CleanupProductTargetCountriesJob
 *
 * Deletes the previous list of target countries which was in use before the
 * Global Offers option became available.
 *
 * @since 1.1.0
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Jobs\Update
 */
class CleanupProductTargetCountriesJob extends AbstractProductSyncerBatchedJob {

	/**
	 * Get the name of the job.
	 *
	 * @return string
	 */
	public function get_name(): string {
		return 'cleanup_product_target_countries';
	}

	/**
	 * Get a single batch of items.
	 *
	 * If no items are returned the job will stop.
	 *
	 * @param int $batch_number The batch number increments for each new batch in the job cycle.
	 *
	 * @return array
	 */
	public function get_batch( int $batch_number ): array {
		return $this->product_repository->find_synced_product_ids( [], $this->get_batch_size(), $this->get_query_offset( $batch_number ) );
	}

	/**
	 * Process batch items.
	 *
	 * @param int[] $items A single batch of WooCommerce product IDs from the get_batch() method.
	 *
	 * @throws ProductSyncerException If an error occurs. The exception will be logged by ActionScheduler.
	 */
	protected function process_items( array $items ) {
		$products      = $this->product_repository->find_by_ids( $items );
		$stale_entries = $this->batch_product_helper->generate_stale_countries_request_entries( $products );
		$this->product_syncer->delete_by_batch_requests( $stale_entries );
	}
}
Update/PluginUpdate.php000064400000003666151542752560011127 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs\Update;

use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\Internal\Interfaces\InstallableInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\JobException;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\JobRepository;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\UpdateAllProducts;

defined( 'ABSPATH' ) || exit;

/**
 * Runs update jobs when the plugin is updated.
 *
 * @since 1.1.0
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Jobs\Update
 */
class PluginUpdate implements Service, InstallableInterface {

	/**
	 * @var JobRepository
	 */
	protected $job_repository;

	/**
	 * PluginUpdate constructor.
	 *
	 * @param JobRepository $job_repository
	 */
	public function __construct( JobRepository $job_repository ) {
		$this->job_repository = $job_repository;
	}

	/**
	 * Update Jobs that need to be run per version.
	 *
	 * @var array
	 */
	private $updates = [
		'1.0.1'  => [
			CleanupProductTargetCountriesJob::class,
			UpdateAllProducts::class,
		],
		'1.12.6' => [
			UpdateAllProducts::class,
		],
	];

	/**
	 * Run installation logic for this class.
	 *
	 * @param string $old_version Previous version before updating.
	 * @param string $new_version Current version after updating.
	 */
	public function install( string $old_version, string $new_version ): void {
		foreach ( $this->updates as $version => $jobs ) {
			if ( version_compare( $old_version, $version, '<' ) ) {
				$this->schedule_jobs( $jobs );
			}
		}
	}

	/**
	 * Schedules a list of jobs.
	 *
	 * @param array $jobs List of jobs
	 */
	protected function schedule_jobs( array $jobs ): void {
		foreach ( $jobs as $job ) {
			try {
				$this->job_repository->get( $job )->schedule();
			} catch ( JobException $e ) {
				do_action( 'woocommerce_gla_exception', $e, __METHOD__ );
			}
		}
	}
}
UpdateAllProducts.php000064400000004106151542752560010671 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs;

use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\ProductSyncerException;

defined( 'ABSPATH' ) || exit;

/**
 * Class UpdateAllProducts
 *
 * Submits all WooCommerce products to Google Merchant Center and/or updates the existing ones.
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Jobs
 */
class UpdateAllProducts extends AbstractProductSyncerBatchedJob implements OptionsAwareInterface {
	use OptionsAwareTrait;
	use SyncableProductsBatchedActionSchedulerJobTrait;

	/**
	 * Get the name of the job.
	 *
	 * @return string
	 */
	public function get_name(): string {
		return 'update_all_products';
	}

	/**
	 * Process batch items.
	 *
	 * @param int[] $items A single batch of WooCommerce product IDs from the get_batch() method.
	 *
	 * @throws ProductSyncerException If an error occurs. The exception will be logged by ActionScheduler.
	 */
	protected function process_items( array $items ) {
		$products = $this->product_repository->find_by_ids( $items );
		$this->product_syncer->update( $products );
	}

	/**
	 * Schedules a delayed batched job
	 *
	 * @param int $delay The delay time in seconds
	 */
	public function schedule_delayed( int $delay ) {
		if ( $this->can_schedule( [ 1 ] ) ) {
			$this->action_scheduler->schedule_single( gmdate( 'U' ) + $delay, $this->get_create_batch_hook(), [ 1 ] );
		}
	}

	/**
	 * Called when the job is completed.
	 *
	 * @param int $final_batch_number The final batch number when the job was completed.
	 *                                If equal to 1 then no items were processed by the job.
	 */
	protected function handle_complete( int $final_batch_number ) {
		$this->options->update( OptionsInterface::UPDATE_ALL_PRODUCTS_LAST_SYNC, strtotime( 'now' ) );
		$this->merchant_statuses->maybe_refresh_status_data( true );
	}
}
UpdateCoupon.php000064400000003657151542752560007712 0ustar00<?php
declare(strict_types = 1);
namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs;

use Automattic\WooCommerce\GoogleListingsAndAds\Coupon\CouponSyncerException;
use WC_Coupon;

defined( 'ABSPATH' ) || exit();

/**
 * Class UpdateCoupon
 *
 * Submits WooCommerce coupon to Google Merchant Center and/or updates the existing one.
 *
 * Note: The job will not start if it is already running.
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Jobs
 */
class UpdateCoupon extends AbstractCouponSyncerJob implements
	StartOnHookInterface {

	/**
	 * Get the name of the job.
	 *
	 * @return string
	 */
	public function get_name(): string {
		return 'update_coupon';
	}

	/**
	 * Process an item.
	 *
	 * @param int[] $coupon_ids
	 *
	 * @throws CouponSyncerException If an error occurs. The exception will be logged by ActionScheduler.
	 */
	public function process_items( $coupon_ids ) {
		foreach ( $coupon_ids as $coupon_id ) {
			$coupon = $this->wc->maybe_get_coupon( $coupon_id );
			if ( $coupon instanceof WC_Coupon &&
				$this->coupon_helper->is_sync_ready( $coupon ) ) {
				$this->coupon_syncer->update( $coupon );
			}
		}
	}

	/**
	 * Schedule the job.
	 *
	 * @param array[] $args
	 *
	 * @throws JobException If no coupon is provided as argument. The exception will be logged by ActionScheduler.
	 */
	public function schedule( array $args = [] ) {
		$args       = $args[0] ?? null;
		$coupon_ids = array_filter( $args, 'is_integer' );

		if ( empty( $coupon_ids ) ) {
			throw JobException::item_not_provided( 'WooCommerce Coupon IDs' );
		}

		if ( $this->can_schedule( [ $coupon_ids ] ) ) {
			$this->action_scheduler->schedule_immediate(
				$this->get_process_item_hook(),
				[ $coupon_ids ]
			);
		}
	}

	/**
	 * Get the name of an action hook to attach the job's start method to.
	 *
	 * @return StartHook
	 */
	public function get_start_hook(): StartHook {
		return new StartHook( "{$this->get_hook_base_name()}start" );
	}
}
UpdateMerchantProductStatuses.php000064400000010422151542752560013271 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs;

use Automattic\WooCommerce\GoogleListingsAndAds\ActionScheduler\ActionSchedulerInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantCenterService;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\MerchantReport;
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantStatuses;
use Throwable;

defined( 'ABSPATH' ) || exit;

/**
 * Class UpdateMerchantProductStatuses
 *
 * Update Product Stats
 *
 * Note: The job will not start if it is already running or if the Google Merchant Center account is not connected.
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Jobs
 *
 * @since 2.6.4
 */
class UpdateMerchantProductStatuses extends AbstractActionSchedulerJob {
	/**
	 * @var MerchantCenterService
	 */
	protected $merchant_center;

	/**
	 * @var MerchantReport
	 */
	protected $merchant_report;

	/**
	 * @var MerchantStatuses
	 */

	protected $merchant_statuses;

	/**
	 * UpdateMerchantProductStatuses constructor.
	 *
	 * @param ActionSchedulerInterface  $action_scheduler
	 * @param ActionSchedulerJobMonitor $monitor
	 * @param MerchantCenterService     $merchant_center
	 * @param MerchantReport            $merchant_report
	 * @param MerchantStatuses          $merchant_statuses
	 */
	public function __construct( ActionSchedulerInterface $action_scheduler, ActionSchedulerJobMonitor $monitor, MerchantCenterService $merchant_center, MerchantReport $merchant_report, MerchantStatuses $merchant_statuses ) {
		parent::__construct( $action_scheduler, $monitor );
		$this->merchant_center   = $merchant_center;
		$this->merchant_report   = $merchant_report;
		$this->merchant_statuses = $merchant_statuses;
	}

	/**
	 * Get the name of the job.
	 *
	 * @return string
	 */
	public function get_name(): string {
		return 'update_merchant_product_statuses';
	}

	/**
	 * Can the job be scheduled.
	 *
	 * @param array|null $args
	 *
	 * @return bool Returns true if the job can be scheduled.
	 */
	public function can_schedule( $args = [] ): bool {
		return parent::can_schedule( $args ) && $this->merchant_center->is_connected();
	}

	/**
	 * Process the job.
	 *
	 * @param int[] $items An array of job arguments.
	 *
	 * @throws JobException If the merchant product statuses cannot be retrieved..
	 */
	public function process_items( array $items ) {
		try {
			$next_page_token = $items['next_page_token'] ?? null;

			// Clear the cache if we're starting from the beginning.
			if ( ! $next_page_token ) {
				$this->merchant_statuses->clear_product_statuses_cache_and_issues();
				$this->merchant_statuses->refresh_account_and_presync_issues();
			}

			$results         = $this->merchant_report->get_product_view_report( $next_page_token );
			$next_page_token = $results['next_page_token'];

			$this->merchant_statuses->process_product_statuses( $results['statuses'] );

			if ( $next_page_token ) {
				$this->schedule( [ [ 'next_page_token' => $next_page_token ] ] );
			} else {
				$this->merchant_statuses->handle_complete_mc_statuses_fetching();
			}
		} catch ( Throwable $e ) {
			$this->merchant_statuses->handle_failed_mc_statuses_fetching( $e->getMessage() );
			throw new JobException( 'Error updating merchant product statuses: ' . $e->getMessage() );
		}
	}

	/**
	 * Schedule the job.
	 *
	 * @param array $args - arguments.
	 */
	public function schedule( array $args = [] ) {
		if ( $this->can_schedule( $args ) ) {
			$this->action_scheduler->schedule_immediate( $this->get_process_item_hook(), $args );
		}
	}

	/**
	 * The job is considered to be scheduled if the "process_item" action is currently pending or in-progress regardless of the arguments.
	 *
	 * @return bool
	 */
	public function is_scheduled(): bool {
		// We set 'args' to null so it matches any arguments. This is because it's possible to have multiple instances of the job running with different page tokens
		return $this->is_running( null );
	}

	/**
	 * Validate the failure rate of the job.
	 *
	 * @return string|void Returns an error message if the failure rate is too high, otherwise null.
	 */
	public function get_failure_rate_message() {
		try {
			$this->monitor->validate_failure_rate( $this, $this->get_process_item_hook() );
		} catch ( JobException $e ) {
			return $e->getMessage();
		}
	}
}
UpdateProducts.php000064400000004372151542752560010245 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs;

use Automattic\WooCommerce\GoogleListingsAndAds\Product\ProductSyncerException;

defined( 'ABSPATH' ) || exit;

/**
 * Class UpdateProducts
 *
 * Submits WooCommerce products to Google Merchant Center and/or updates the existing ones.
 *
 * Note: The job will not start if it is already running.
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Jobs
 */
class UpdateProducts extends AbstractProductSyncerJob implements StartOnHookInterface {

	/**
	 * Get the name of the job.
	 *
	 * @return string
	 */
	public function get_name(): string {
		return 'update_products';
	}

	/**
	 * Process an item.
	 *
	 * @param int[] $product_ids An array of WooCommerce product ids.
	 *
	 * @throws ProductSyncerException If an error occurs. The exception will be logged by ActionScheduler.
	 * @throws JobException If invalid or non-existing products are provided. The exception will be logged by ActionScheduler.
	 */
	public function process_items( array $product_ids ) {
		$args     = [ 'include' => $product_ids ];
		$products = $this->product_repository->find_sync_ready_products( $args )->get();

		if ( empty( $products ) ) {
			throw JobException::item_not_found();
		}

		$this->product_syncer->update( $products );
	}

	/**
	 * Schedule the job.
	 *
	 * @param array $args - arguments.
	 *
	 * @throws JobException If no product is provided as argument. The exception will be logged by ActionScheduler.
	 */
	public function schedule( array $args = [] ) {
		$args = $args[0] ?? [];
		$ids  = array_filter( $args, 'is_integer' );

		if ( empty( $ids ) ) {
			throw JobException::item_not_provided( 'Array of WooCommerce Product IDs' );
		}

		if ( did_action( 'woocommerce_gla_batch_retry_update_products' ) ) {
			$this->action_scheduler->schedule_single( gmdate( 'U' ) + 60, $this->get_process_item_hook(), [ $ids ] );
		} elseif ( $this->can_schedule( [ $ids ] ) ) {
			$this->action_scheduler->schedule_immediate( $this->get_process_item_hook(), [ $ids ] );
		}
	}

	/**
	 * Get an action hook to attach the job's start method to.
	 *
	 * @return StartHook
	 */
	public function get_start_hook(): StartHook {
		return new StartHook( 'woocommerce_gla_batch_retry_update_products', 1 );
	}
}
UpdateShippingSettings.php000064400000006041151542752560011737 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs;

use Automattic\WooCommerce\GoogleListingsAndAds\ActionScheduler\ActionSchedulerInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Settings as GoogleSettings;
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantCenterService;

defined( 'ABSPATH' ) || exit;

/**
 * Class UpdateShippingSettings
 *
 * Submits WooCommerce shipping settings to Google Merchant Center replacing the existing shipping settings.
 *
 * Note: The job will not start if it is already running or if the Google Merchant Center account is not connected.
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Jobs
 *
 * @since 2.1.0
 */
class UpdateShippingSettings extends AbstractActionSchedulerJob {
	/**
	 * @var MerchantCenterService
	 */
	protected $merchant_center;

	/**
	 * @var GoogleSettings
	 */
	protected $google_settings;

	/**
	 * UpdateShippingSettings constructor.
	 *
	 * @param ActionSchedulerInterface  $action_scheduler
	 * @param ActionSchedulerJobMonitor $monitor
	 * @param MerchantCenterService     $merchant_center
	 * @param GoogleSettings            $google_settings
	 */
	public function __construct( ActionSchedulerInterface $action_scheduler, ActionSchedulerJobMonitor $monitor, MerchantCenterService $merchant_center, GoogleSettings $google_settings ) {
		parent::__construct( $action_scheduler, $monitor );
		$this->merchant_center = $merchant_center;
		$this->google_settings = $google_settings;
	}

	/**
	 * Get the name of the job.
	 *
	 * @return string
	 */
	public function get_name(): string {
		return 'update_shipping_settings';
	}

	/**
	 * Can the job be scheduled.
	 *
	 * @param array|null $args
	 *
	 * @return bool Returns true if the job can be scheduled.
	 */
	public function can_schedule( $args = [] ): bool {
		return parent::can_schedule( $args ) && $this->can_sync_shipping();
	}

	/**
	 * Process the job.
	 *
	 * @param int[] $items An array of job arguments.
	 *
	 * @throws JobException If the shipping settings cannot be synced.
	 */
	public function process_items( array $items ) {
		if ( ! $this->can_sync_shipping() ) {
			throw new JobException( 'Cannot sync shipping settings. Confirm that the merchant center account is connected and the option to automatically sync the shipping settings is selected.' );
		}

		$this->google_settings->sync_shipping();
	}

	/**
	 * Schedule the job.
	 *
	 * @param array $args - arguments.
	 */
	public function schedule( array $args = [] ) {
		if ( $this->can_schedule() ) {
			$this->action_scheduler->schedule_immediate( $this->get_process_item_hook() );
		}
	}

	/**
	 * Can the WooCommerce shipping settings be synced to Google Merchant Center.
	 *
	 * @return bool
	 */
	protected function can_sync_shipping(): bool {
		// Confirm that the Merchant Center account is connected and the user has chosen for the shipping rates to be synced from WooCommerce settings.
		return $this->merchant_center->is_connected() && $this->google_settings->should_get_shipping_rates_from_woocommerce();
	}
}
UpdateSyncableProductsCount.php000064400000007145151542752560012740 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Jobs;

use Automattic\WooCommerce\GoogleListingsAndAds\ActionScheduler\ActionSchedulerInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\AbstractBatchedActionSchedulerJob;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\ActionSchedulerJobMonitor;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\SyncableProductsBatchedActionSchedulerJobTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\ProductHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\ProductRepository;

defined( 'ABSPATH' ) || exit;

/**
 * Class UpdateSyncableProductsCount
 *
 * Get the number of syncable products (i.e. product ready to be synced to Google Merchant Center) and update it in the DB.
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Jobs
 * @since 2.2.0
 */
class UpdateSyncableProductsCount extends AbstractBatchedActionSchedulerJob implements OptionsAwareInterface {
	use OptionsAwareTrait;
	use SyncableProductsBatchedActionSchedulerJobTrait;

	/**
	 * @var ProductRepository
	 */
	protected $product_repository;

	/**
	 * @var ProductHelper
	 */
	protected $product_helper;

	/**
	 * UpdateSyncableProductsCount constructor.
	 *
	 * @param ActionSchedulerInterface  $action_scheduler
	 * @param ActionSchedulerJobMonitor $monitor
	 * @param ProductRepository         $product_repository
	 * @param ProductHelper             $product_helper
	 */
	public function __construct( ActionSchedulerInterface $action_scheduler, ActionSchedulerJobMonitor $monitor, ProductRepository $product_repository, ProductHelper $product_helper ) {
		parent::__construct( $action_scheduler, $monitor );
		$this->product_repository = $product_repository;
		$this->product_helper     = $product_helper;
	}

	/**
	 * Get the name of the job.
	 *
	 * @return string
	 */
	public function get_name(): string {
		return 'update_syncable_products_count';
	}

	/**
	 * Get job batch size.
	 *
	 * @return int
	 */
	protected function get_batch_size(): int {
		/**
		 * Filters the batch size for the job.
		 *
		 * @param string Job's name
		 */
		return apply_filters( 'woocommerce_gla_batched_job_size', 500, $this->get_name() );
	}

	/**
	 * Process batch items.
	 *
	 * @param int[] $items A single batch of WooCommerce product IDs from the get_batch() method.
	 */
	protected function process_items( array $items ) {
		$product_ids = $this->options->get( OptionsInterface::SYNCABLE_PRODUCTS_COUNT_INTERMEDIATE_DATA );

		if ( ! is_array( $product_ids ) ) {
			$product_ids = [];
		}

		$grouped_items = $this->product_helper->maybe_swap_for_parent_ids( $items );

		$this->options->update( OptionsInterface::SYNCABLE_PRODUCTS_COUNT_INTERMEDIATE_DATA, array_unique( [ ...$product_ids, ...$grouped_items ] ) );
	}

	/**
	 * Called when the job is completed.
	 *
	 * @param int $final_batch_number The final batch number when the job was completed.
	 *                                If equal to 1 then no items were processed by the job.
	 */
	protected function handle_complete( int $final_batch_number ) {
		$product_ids = $this->options->get( OptionsInterface::SYNCABLE_PRODUCTS_COUNT_INTERMEDIATE_DATA );
		$count       = is_array( $product_ids ) ? count( $product_ids ) : 0;
		$this->options->update( OptionsInterface::SYNCABLE_PRODUCTS_COUNT, $count );
		$this->options->delete( OptionsInterface::SYNCABLE_PRODUCTS_COUNT_INTERMEDIATE_DATA );
	}
}