| 1 |
<?php |
| 2 |
/** |
| 3 |
* WooCommerce Square |
| 4 |
* |
| 5 |
* This source file is subject to the GNU General Public License v3.0 |
| 6 |
* that is bundled with this package in the file license.txt. |
| 7 |
* It is also available through the world-wide-web at this URL: |
| 8 |
* http://www.gnu.org/licenses/gpl-3.0.html GNU General Public License v3.0 or later |
| 9 |
* If you did not receive a copy of the license and are unable to |
| 10 |
* obtain it through the world-wide-web, please send an email |
| 11 |
* to license@woocommerce.com so we can send you a copy immediately. |
| 12 |
* |
| 13 |
* DISCLAIMER |
| 14 |
* |
| 15 |
* Do not edit or add to this file if you wish to upgrade WooCommerce Square to newer |
| 16 |
* versions in the future. If you wish to customize WooCommerce Square for your |
| 17 |
* needs please refer to https://docs.woocommerce.com/document/woocommerce-square/ |
| 18 |
* |
| 19 |
* @author WooCommerce |
| 20 |
* @copyright Copyright: (c) 2019, Automattic, Inc. |
| 21 |
* @license http://www.gnu.org/licenses/gpl-3.0.html GNU General Public License v3.0 or later |
| 22 |
*/ |
| 23 |
|
| 24 |
namespace WooCommerce\Square\Handlers; |
| 25 |
|
| 26 |
use WooCommerce\Square\Plugin; |
| 27 |
use WooCommerce\Square\Sync\Records; |
| 28 |
|
| 29 |
defined( 'ABSPATH' ) || exit; |
| 30 |
|
| 31 |
/** |
| 32 |
* Synchronization handler class |
| 33 |
* |
| 34 |
* @since 2.0.0 |
| 35 |
*/ |
| 36 |
class Sync { |
| 37 |
|
| 38 |
|
| 39 |
/** @var string key of the option that stores a timestamp when the last sync job completed */ |
| 40 |
private $last_synced_at_option_key = 'wc_square_last_synced_at'; |
| 41 |
|
| 42 |
/** @var string name of the Action Scheduler event name for syncing with Square */ |
| 43 |
private $sync_scheduled_event_name; |
| 44 |
|
| 45 |
/** @var Plugin plugin instance */ |
| 46 |
private $plugin; |
| 47 |
|
| 48 |
|
| 49 |
/** |
| 50 |
* Constructs the class. |
| 51 |
* |
| 52 |
* @since 2.0.0 |
| 53 |
* |
| 54 |
* @param Plugin $plugin |
| 55 |
*/ |
| 56 |
public function __construct( Plugin $plugin ) { |
| 57 |
|
| 58 |
$this->plugin = $plugin; |
| 59 |
|
| 60 |
$this->sync_scheduled_event_name = 'wc_square_sync'; |
| 61 |
|
| 62 |
$this->add_hooks(); |
| 63 |
} |
| 64 |
|
| 65 |
|
| 66 |
/** |
| 67 |
* Adds the action & filter hooks. |
| 68 |
* |
| 69 |
* @since 2.0.0 |
| 70 |
*/ |
| 71 |
private function add_hooks() { |
| 72 |
|
| 73 |
// schedule the interval sync |
| 74 |
add_action( 'init', array( $this, 'schedule_sync' ) ); |
| 75 |
|
| 76 |
// run the interval sync when fired by Action Scheduler |
| 77 |
add_action( $this->sync_scheduled_event_name, array( $this, 'start_interval_sync' ) ); |
| 78 |
|
| 79 |
add_action( 'admin_notices', array( $this, 'render_import_no_navigation_warning' ) ); |
| 80 |
} |
| 81 |
|
| 82 |
/** |
| 83 |
* Returns array of post types supported for sync. |
| 84 |
* |
| 85 |
* Since 3.8.3 |
| 86 |
* |
| 87 |
* @return array |
| 88 |
*/ |
| 89 |
public function supported_product_types() { |
| 90 |
return array( |
| 91 |
'simple', |
| 92 |
'variable', |
| 93 |
); |
| 94 |
} |
| 95 |
|
| 96 |
/** |
| 97 |
* Schedules the interval sync. |
| 98 |
* |
| 99 |
* @param bool $change_interval (optional) whether to change the interval |
| 100 |
* @since 2.0.0 |
| 101 |
*/ |
| 102 |
public function schedule_sync( $change_interval = false ) { |
| 103 |
|
| 104 |
// bail if product sync is not enabled or there hasn't been a previous sync |
| 105 |
if ( $this->is_sync_in_progress() || ! $this->get_last_synced_at() || ! $this->get_plugin()->get_settings_handler()->is_connected() || ! $this->get_plugin()->get_settings_handler()->is_product_sync_enabled() ) { |
| 106 |
return; |
| 107 |
} |
| 108 |
|
| 109 |
$plugin_id = $this->get_plugin()->get_id(); |
| 110 |
$interval = wc_square()->get_settings_handler()->get_sync_interval(); |
| 111 |
|
| 112 |
if ( false === as_next_scheduled_action( $this->sync_scheduled_event_name, array(), $plugin_id ) || $change_interval ) { |
| 113 |
as_unschedule_all_actions( $this->sync_scheduled_event_name, array(), $plugin_id ); |
| 114 |
as_schedule_recurring_action( time() + $interval, $interval, $this->sync_scheduled_event_name, array(), $plugin_id ); |
| 115 |
} |
| 116 |
} |
| 117 |
|
| 118 |
|
| 119 |
/** |
| 120 |
* Unschedules the interval sync. |
| 121 |
* |
| 122 |
* @since 2.0.0 |
| 123 |
*/ |
| 124 |
public function unschedule_sync() { |
| 125 |
|
| 126 |
as_unschedule_action( $this->sync_scheduled_event_name, array(), 'square' ); |
| 127 |
} |
| 128 |
|
| 129 |
/** |
| 130 |
* Performs a product import from Square. |
| 131 |
* |
| 132 |
* @since 2.0.0 |
| 133 |
* |
| 134 |
* @param bool $update_during_import whether the store manager has ticked to update products during an import |
| 135 |
* @return \stdClass|null |
| 136 |
*/ |
| 137 |
public function start_product_import( $update_during_import = false ) { |
| 138 |
|
| 139 |
$job = $this->get_plugin()->get_background_job_handler()->create_job( |
| 140 |
array( |
| 141 |
'action' => 'product_import', |
| 142 |
'update_products_during_import' => $update_during_import, |
| 143 |
) |
| 144 |
); |
| 145 |
|
| 146 |
if ( $job ) { |
| 147 |
as_enqueue_async_action( 'wc_square_job_runner' ); |
| 148 |
} |
| 149 |
|
| 150 |
return $job; |
| 151 |
} |
| 152 |
|
| 153 |
/** |
| 154 |
* Performs a manual sync. |
| 155 |
* |
| 156 |
* @since 2.0.0 |
| 157 |
* |
| 158 |
* @param int[] $product_ids (optional) array of product IDs to sync |
| 159 |
* @return \stdClass|null |
| 160 |
*/ |
| 161 |
public function start_manual_sync( array $product_ids = array() ) { |
| 162 |
|
| 163 |
$product_ids = empty( $product_ids ) ? Product::get_products_synced_with_square() : $product_ids; |
| 164 |
|
| 165 |
$job = $this->get_plugin()->get_background_job_handler()->create_job( |
| 166 |
array( |
| 167 |
'action' => 'sync', |
| 168 |
'manual' => true, |
| 169 |
'product_ids' => $product_ids, |
| 170 |
) |
| 171 |
); |
| 172 |
|
| 173 |
if ( $job ) { |
| 174 |
as_enqueue_async_action( 'wc_square_job_runner' ); |
| 175 |
} |
| 176 |
|
| 177 |
return $job; |
| 178 |
} |
| 179 |
|
| 180 |
/** |
| 181 |
* Performs a manual product deletion. |
| 182 |
* |
| 183 |
* @since 2.0.0 |
| 184 |
* |
| 185 |
* @param int[] $product_ids array of product IDs to delete |
| 186 |
* @return \stdClass|null |
| 187 |
*/ |
| 188 |
public function start_manual_deletion( array $product_ids ) { |
| 189 |
|
| 190 |
$job = $this->get_plugin()->get_background_job_handler()->create_job( |
| 191 |
array( |
| 192 |
'action' => 'delete', |
| 193 |
'manual' => true, |
| 194 |
'product_ids' => $product_ids, |
| 195 |
) |
| 196 |
); |
| 197 |
|
| 198 |
if ( $job ) { |
| 199 |
as_enqueue_async_action( 'wc_square_job_runner' ); |
| 200 |
} |
| 201 |
|
| 202 |
return $job; |
| 203 |
} |
| 204 |
|
| 205 |
/** |
| 206 |
* Performs an interval sync with Square. |
| 207 |
* |
| 208 |
* @since 2.0.0 |
| 209 |
*/ |
| 210 |
public function start_interval_sync() { |
| 211 |
|
| 212 |
// bail if there is already a sync in progress |
| 213 |
if ( ! $this->is_sync_enabled() || $this->is_sync_in_progress() ) { |
| 214 |
return; |
| 215 |
} |
| 216 |
|
| 217 |
// use this opportunity to clear old background jobs |
| 218 |
$this->get_plugin()->get_background_job_handler()->clear_all_jobs(); |
| 219 |
|
| 220 |
$job = $this->get_plugin()->get_background_job_handler()->create_job( |
| 221 |
array( |
| 222 |
'action' => 'poll', |
| 223 |
'manual' => false, |
| 224 |
'catalog_last_synced_at' => $this->get_last_synced_at(), |
| 225 |
'inventory_last_synced_at' => $this->get_inventory_last_synced_at(), |
| 226 |
) |
| 227 |
); |
| 228 |
|
| 229 |
if ( $job ) { |
| 230 |
as_enqueue_async_action( 'wc_square_job_runner' ); |
| 231 |
} |
| 232 |
} |
| 233 |
|
| 234 |
|
| 235 |
/** Conditional methods *******************************************************************************************/ |
| 236 |
|
| 237 |
|
| 238 |
/** |
| 239 |
* Determines whether a sync, scheduled or manual, is in progress. |
| 240 |
* |
| 241 |
* @since 2.0.0 |
| 242 |
* |
| 243 |
* @return bool |
| 244 |
*/ |
| 245 |
public function is_sync_in_progress() { |
| 246 |
|
| 247 |
return ( defined( 'DOING_SQUARE_SYNC' ) && true === DOING_SQUARE_SYNC ) |
| 248 |
|| null !== $this->get_job_in_progress(); |
| 249 |
} |
| 250 |
|
| 251 |
|
| 252 |
/** |
| 253 |
* Determines if sync is enabled. |
| 254 |
* |
| 255 |
* @since 2.0.0 |
| 256 |
* |
| 257 |
* @return bool |
| 258 |
*/ |
| 259 |
public function is_sync_enabled() { |
| 260 |
|
| 261 |
return $this->get_plugin()->get_settings_handler()->is_product_sync_enabled(); |
| 262 |
} |
| 263 |
|
| 264 |
|
| 265 |
/** Setter methods ************************************************************************************************/ |
| 266 |
|
| 267 |
|
| 268 |
/** |
| 269 |
* Records a successful sync. |
| 270 |
* |
| 271 |
* @since 2.0.0 |
| 272 |
* |
| 273 |
* @param int[] $product_ids IDs of products synced |
| 274 |
* @param null|\stdClass $job optional sync job, may be used to set the job ID to prevent duplicates |
| 275 |
*/ |
| 276 |
public function record_sync( array $product_ids, $job = null ) { |
| 277 |
|
| 278 |
$products = count( $product_ids ); |
| 279 |
|
| 280 |
// only add a record of some products were synced |
| 281 |
if ( $products ) { |
| 282 |
|
| 283 |
Records::set_record( |
| 284 |
array( |
| 285 |
'id' => $job && isset( $job->id ) ? $job->id : null, |
| 286 |
'message' => sprintf( |
| 287 |
/* translators: Placeholder: %d number of products processed */ |
| 288 |
_n( 'Updated data for %d product.', 'Updated data for %d products.', $products, 'woocommerce-square' ), |
| 289 |
$products |
| 290 |
), |
| 291 |
) |
| 292 |
); |
| 293 |
} |
| 294 |
|
| 295 |
/** |
| 296 |
* Fires after a set of products are synced with square. |
| 297 |
* |
| 298 |
* @since 2.0.0 |
| 299 |
* |
| 300 |
* @param int[] $product_ids IDs for products that were synced |
| 301 |
*/ |
| 302 |
do_action( 'wc_square_products_synced', $product_ids ); |
| 303 |
} |
| 304 |
|
| 305 |
|
| 306 |
/** |
| 307 |
* Updates the time when the last sync job occurred. |
| 308 |
* |
| 309 |
* @since 2.0.0 |
| 310 |
* |
| 311 |
* @param int|string|null $timestamp a valid timestamp in UTC (optional, will default to now) |
| 312 |
* @return bool success |
| 313 |
*/ |
| 314 |
public function set_last_synced_at( $timestamp = null ) { |
| 315 |
|
| 316 |
if ( null === $timestamp ) { |
| 317 |
$timestamp = time(); |
| 318 |
} |
| 319 |
|
| 320 |
return is_numeric( $timestamp ) && update_option( $this->last_synced_at_option_key, (int) $timestamp ); |
| 321 |
} |
| 322 |
|
| 323 |
|
| 324 |
/** Getter methods ************************************************************************************************/ |
| 325 |
|
| 326 |
|
| 327 |
/** |
| 328 |
* Gets a job that is currently in progress. |
| 329 |
* |
| 330 |
* @since 2.0.0 |
| 331 |
* |
| 332 |
* @return null|\stdClass background job object or null if not found |
| 333 |
*/ |
| 334 |
public function get_job_in_progress() { |
| 335 |
|
| 336 |
$handler = $this->get_plugin()->get_background_job_handler(); |
| 337 |
|
| 338 |
try { |
| 339 |
$job = $handler->get_job(); |
| 340 |
} catch ( \Exception $e ) { |
| 341 |
$job = null; |
| 342 |
} |
| 343 |
|
| 344 |
return $job && isset( $job->status ) && in_array( $job->status, array( 'created', 'queued', 'processing' ), true ) ? $job : null; |
| 345 |
} |
| 346 |
|
| 347 |
/** |
| 348 |
* Gets the timestamp when the next sync job should start. |
| 349 |
* |
| 350 |
* @since 2.0.0 |
| 351 |
* |
| 352 |
* @return int |
| 353 |
*/ |
| 354 |
public function get_next_sync_at() { |
| 355 |
|
| 356 |
$timestamp = null; |
| 357 |
$scheduled = as_next_scheduled_action( $this->sync_scheduled_event_name ); |
| 358 |
|
| 359 |
if ( $scheduled ) { |
| 360 |
$timestamp = $scheduled; |
| 361 |
} |
| 362 |
|
| 363 |
return (int) $timestamp > 1 ? $timestamp : null; |
| 364 |
} |
| 365 |
|
| 366 |
|
| 367 |
/** |
| 368 |
* Gets the timestamp for when the last sync job completed. |
| 369 |
* |
| 370 |
* @since 2.0.0 |
| 371 |
* |
| 372 |
* @return int |
| 373 |
*/ |
| 374 |
public function get_last_synced_at() { |
| 375 |
|
| 376 |
$timestamp = get_option( $this->last_synced_at_option_key, null ); |
| 377 |
|
| 378 |
return (int) $timestamp > 1 ? $timestamp : null; |
| 379 |
} |
| 380 |
|
| 381 |
|
| 382 |
/** |
| 383 |
* Sets the timestamp for when the last inventory sync job started. |
| 384 |
* |
| 385 |
* @since 2.0.0 |
| 386 |
* |
| 387 |
* @param int|string|null $timestamp a valid timestamp in UTC (optional, will default to now) |
| 388 |
* @return bool success |
| 389 |
*/ |
| 390 |
public function set_inventory_last_synced_at( $timestamp = null ) { |
| 391 |
|
| 392 |
if ( null === $timestamp ) { |
| 393 |
$timestamp = time(); |
| 394 |
} |
| 395 |
|
| 396 |
return is_numeric( $timestamp ) && update_option( $this->last_synced_at_option_key . '_inventory', $timestamp ); |
| 397 |
} |
| 398 |
|
| 399 |
|
| 400 |
/** |
| 401 |
* Gets the timestamp for when the last inventory sync job completed. |
| 402 |
* |
| 403 |
* @since 2.0.0 |
| 404 |
* |
| 405 |
* @return int |
| 406 |
*/ |
| 407 |
public function get_inventory_last_synced_at() { |
| 408 |
|
| 409 |
$timestamp = get_option( $this->last_synced_at_option_key . '_inventory', null ); |
| 410 |
|
| 411 |
return (int) $timestamp > 1 ? $timestamp : null; |
| 412 |
} |
| 413 |
|
| 414 |
|
| 415 |
/** |
| 416 |
* Gets the plugin instance. |
| 417 |
* |
| 418 |
* @since 2.0.0 |
| 419 |
* |
| 420 |
* @return Plugin |
| 421 |
*/ |
| 422 |
private function get_plugin() { |
| 423 |
|
| 424 |
return $this->plugin; |
| 425 |
} |
| 426 |
|
| 427 |
/** |
| 428 |
* Show warning not to close or navigate away from the current |
| 429 |
* page when product import is in progress. |
| 430 |
*/ |
| 431 |
public function render_import_no_navigation_warning() { |
| 432 |
$job_in_progress = wc_square()->get_sync_handler()->get_job_in_progress(); |
| 433 |
|
| 434 |
if ( $job_in_progress && 'product_import' === $job_in_progress->action ) { |
| 435 |
wc_square()->get_admin_notice_handler()->add_admin_notice( |
| 436 |
__( 'Please do not close or navigate away from this page as the product import job is in progress. This page may load several times during the course of sync.', 'woocommerce-square' ), |
| 437 |
'wc-square-sync-in-progress-message', |
| 438 |
array( |
| 439 |
'notice_class' => 'notice-warning', |
| 440 |
) |
| 441 |
); |
| 442 |
} |
| 443 |
} |
| 444 |
} |
| 445 |
|