Manual_Synchronization.php
| 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\Sync; |
| 25 | |
| 26 | use Square\Models\BatchRetrieveInventoryCountsResponse; |
| 27 | use Square\Models\BatchUpsertCatalogObjectsResponse; |
| 28 | use Square\Models\BatchRetrieveCatalogObjectsResponse; |
| 29 | use Square\Models\CatalogObject; |
| 30 | use Square\Models\SearchCatalogObjectsResponse; |
| 31 | use Square\Models\CatalogInfoResponse; |
| 32 | use \Square\ApiHelper; |
| 33 | use WooCommerce\Square\Handlers\Category; |
| 34 | use WooCommerce\Square\Handlers\Product; |
| 35 | |
| 36 | defined( 'ABSPATH' ) || exit; |
| 37 | |
| 38 | /** |
| 39 | * Class to represent a single synchronization job triggered manually. |
| 40 | * |
| 41 | * @since 2.0.0 |
| 42 | */ |
| 43 | class Manual_Synchronization extends Stepped_Job { |
| 44 | |
| 45 | |
| 46 | /** @var int the limit for how many objects can be upserted in a batch upsert request */ |
| 47 | const BATCH_UPSERT_OBJECT_LIMIT = 600; |
| 48 | |
| 49 | /** @var int the limit for how many inventory changes can be made in a single request */ |
| 50 | const BATCH_CHANGE_INVENTORY_LIMIT = 100; |
| 51 | |
| 52 | /** @var int the limit for how many inventory counts can be requested per batch |
| 53 | * Square paginates responses in page size of 100. |
| 54 | * Consider some items can have more than one object returned with different states. */ |
| 55 | const BATCH_INVENTORY_COUNTS_LIMIT = 125; |
| 56 | |
| 57 | /** |
| 58 | * Validates the products attached to this job. |
| 59 | * |
| 60 | * @since 2.0.0 |
| 61 | */ |
| 62 | protected function validate_products() { |
| 63 | $product_ids = $this->get_attr( 'product_ids' ); |
| 64 | $unsupported_product_ids = array(); |
| 65 | |
| 66 | if ( is_array( $product_ids ) ) { |
| 67 | $matched_product_ids = wc_get_products( |
| 68 | array( |
| 69 | 'include' => $product_ids, |
| 70 | 'return' => 'ids', |
| 71 | 'type' => wc_square()->get_sync_handler()->supported_product_types(), |
| 72 | 'limit' => -1, |
| 73 | ) |
| 74 | ); |
| 75 | |
| 76 | $matched_product_ids = is_array( $matched_product_ids ) ? $matched_product_ids : array(); |
| 77 | $unsupported_product_ids = array_diff( $product_ids, $matched_product_ids ); |
| 78 | |
| 79 | foreach ( $unsupported_product_ids as $matched_product_id ) { |
| 80 | $product = wc_get_product( $matched_product_id ); |
| 81 | $type = $product->get_type(); |
| 82 | |
| 83 | Records::set_record( |
| 84 | array( |
| 85 | 'type' => 'alert', |
| 86 | 'message' => sprintf( |
| 87 | /* translators: %1$s - product edit page URL, %2$s - Product ID, %3$s - Product type. */ |
| 88 | __( 'Product <a href="%1$s">#%2$s</a> is excluded from sync as the product type "%3$s" is unsupported.', 'woocommerce-square' ), |
| 89 | get_edit_post_link( $matched_product_id ), |
| 90 | $matched_product_id, |
| 91 | $type |
| 92 | ), |
| 93 | ) |
| 94 | ); |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | $products_query = array( |
| 99 | 'include' => $product_ids, |
| 100 | 'limit' => -1, |
| 101 | 'status' => array( 'private', 'publish' ), |
| 102 | 'return' => 'ids', |
| 103 | ); |
| 104 | |
| 105 | if ( 'delete' === $this->get_attr( 'action' ) ) { |
| 106 | |
| 107 | $products_query['status'] = array( 'trash', 'draft', 'pending', 'private', 'publish' ); |
| 108 | } |
| 109 | |
| 110 | $validated_products = wc_get_products( $products_query ); |
| 111 | |
| 112 | $this->set_attr( 'validated_product_ids', $validated_products ); |
| 113 | |
| 114 | $this->complete_step( 'validate_products' ); |
| 115 | } |
| 116 | |
| 117 | |
| 118 | /** |
| 119 | * Updates the catalog API limits. |
| 120 | * |
| 121 | * @since 2.0.0 |
| 122 | */ |
| 123 | protected function update_limits() { |
| 124 | |
| 125 | try { |
| 126 | |
| 127 | $catalog_info = wc_square()->get_api()->catalog_info(); |
| 128 | |
| 129 | if ( $catalog_info->get_data() instanceof CatalogInfoResponse && $catalog_info->get_data()->getLimits() ) { |
| 130 | |
| 131 | $limits = $catalog_info->get_data()->getLimits(); |
| 132 | |
| 133 | $this->set_attr( 'max_objects_to_retrieve', $limits->getBatchRetrieveMaxObjectIds() ); |
| 134 | $this->set_attr( 'max_objects_per_batch', $limits->getBatchUpsertMaxObjectsPerBatch() ); |
| 135 | $this->set_attr( 'max_objects_total', $limits->getBatchUpsertMaxTotalObjects() ); |
| 136 | } |
| 137 | } catch ( \Exception $exception ) { // no need to handle errors here |
| 138 | } |
| 139 | |
| 140 | $this->complete_step( 'update_limits' ); |
| 141 | } |
| 142 | |
| 143 | |
| 144 | /** |
| 145 | * Extracts the category IDs from the list of product IDs in this job, and saves them. |
| 146 | * |
| 147 | * @since 2.0.0 |
| 148 | */ |
| 149 | protected function extract_category_ids() { |
| 150 | |
| 151 | $category_ids = $this->get_shared_category_ids( $this->get_attr( 'validated_product_ids' ) ); |
| 152 | |
| 153 | $this->set_attr( 'category_ids', $category_ids ); |
| 154 | |
| 155 | $this->complete_step( 'extract_category_ids' ); |
| 156 | } |
| 157 | |
| 158 | |
| 159 | /** |
| 160 | * Refreshes mappings for categories with known Square IDs. |
| 161 | * |
| 162 | * @since 2.0.0 |
| 163 | * |
| 164 | * @throws \Exception |
| 165 | */ |
| 166 | protected function refresh_category_mappings() { |
| 167 | |
| 168 | $map = Category::get_map(); |
| 169 | $category_ids = $this->get_attr( 'refresh_mappings_category_ids', $this->get_attr( 'category_ids' ) ); |
| 170 | $mapped_categories = array(); |
| 171 | $unmapped_categories = $this->get_attr( 'unmapped_categories', array() ); |
| 172 | $unmapped_category_ids = array(); |
| 173 | |
| 174 | if ( empty( $category_ids ) ) { |
| 175 | $this->complete_step( 'refresh_category_mappings' ); |
| 176 | return; |
| 177 | } |
| 178 | |
| 179 | if ( count( $category_ids ) > $this->get_max_objects_to_retrieve() ) { |
| 180 | |
| 181 | $category_ids_batch = array_slice( $category_ids, 0, $this->get_max_objects_to_retrieve() ); |
| 182 | |
| 183 | $this->set_attr( 'refresh_mappings_category_ids', array_diff( $category_ids, $category_ids_batch ) ); |
| 184 | |
| 185 | $category_ids = $category_ids_batch; |
| 186 | |
| 187 | } else { |
| 188 | |
| 189 | $this->set_attr( 'refresh_mappings_category_ids', array() ); |
| 190 | } |
| 191 | |
| 192 | foreach ( $category_ids as $category_id ) { |
| 193 | |
| 194 | if ( isset( $map[ $category_id ] ) ) { |
| 195 | |
| 196 | $mapped_categories[ $category_id ] = $map[ $category_id ]; |
| 197 | |
| 198 | } else { |
| 199 | |
| 200 | $unmapped_category_ids[] = $category_id; |
| 201 | } |
| 202 | } |
| 203 | |
| 204 | if ( ! empty( $mapped_categories ) ) { |
| 205 | |
| 206 | $square_ids = array_values( |
| 207 | array_filter( |
| 208 | array_map( |
| 209 | function ( $mapped_category ) { |
| 210 | return isset( $mapped_category['square_id'] ) ? $mapped_category['square_id'] : null; |
| 211 | }, |
| 212 | $mapped_categories |
| 213 | ) |
| 214 | ) |
| 215 | ); |
| 216 | |
| 217 | if ( ! empty( $square_ids ) ) { |
| 218 | |
| 219 | $response = wc_square()->get_api()->batch_retrieve_catalog_objects( $square_ids ); |
| 220 | |
| 221 | // swap the square ID into the array key for quick lookup |
| 222 | $mapped_category_audit = array(); |
| 223 | |
| 224 | foreach ( $mapped_categories as $mapped_category_id => $mapped_category ) { |
| 225 | $mapped_category_audit[ $mapped_category['square_id'] ] = $mapped_category_id; |
| 226 | } |
| 227 | |
| 228 | if ( ! $response->get_data() instanceof BatchRetrieveCatalogObjectsResponse ) { |
| 229 | throw new \Exception( 'Could not fetch category data from Square. Response data is missing' ); |
| 230 | } |
| 231 | |
| 232 | // handle response |
| 233 | if ( is_array( $response->get_data()->getObjects() ) ) { |
| 234 | foreach ( $response->get_data()->getObjects() as $category ) { |
| 235 | |
| 236 | // don't check for the name, it will get overwritten by the Woo value anyway |
| 237 | if ( isset( $mapped_category_audit[ $category->getId() ] ) ) { |
| 238 | |
| 239 | $category_id = $mapped_category_audit[ $category->getId() ]; |
| 240 | |
| 241 | $map[ $category_id ]['square_version'] = $category->getVersion(); |
| 242 | unset( $mapped_category_audit[ $category->getId() ] ); |
| 243 | } |
| 244 | } |
| 245 | } |
| 246 | |
| 247 | // any remaining categories were not found in Square and should have their local mapping data removed |
| 248 | if ( ! empty( $mapped_category_audit ) ) { |
| 249 | |
| 250 | $outdated_category_ids = array_values( $mapped_category_audit ); |
| 251 | |
| 252 | foreach ( $outdated_category_ids as $outdated_category_id ) { |
| 253 | |
| 254 | unset( $map[ $outdated_category_id ], $mapped_categories[ $outdated_category_id ] ); |
| 255 | |
| 256 | $unmapped_category_ids[] = $outdated_category_id; |
| 257 | } |
| 258 | |
| 259 | $unmapped_category_ids = array_unique( $unmapped_category_ids ); |
| 260 | } |
| 261 | } |
| 262 | // update unmapped list |
| 263 | } |
| 264 | |
| 265 | if ( ! empty( $unmapped_category_ids ) ) { |
| 266 | |
| 267 | $unmapped_category_terms = get_terms( |
| 268 | array( |
| 269 | 'taxonomy' => 'product_cat', |
| 270 | 'include' => $unmapped_category_ids, |
| 271 | ) |
| 272 | ); |
| 273 | |
| 274 | // make the 'name' attribute the array key, for more efficient searching later. |
| 275 | foreach ( $unmapped_category_terms as $unmapped_category_term ) { |
| 276 | $unmapped_categories[ strtolower( wp_specialchars_decode( $unmapped_category_term->name ) ) ] = $unmapped_category_term; |
| 277 | } |
| 278 | } |
| 279 | |
| 280 | // save category lists |
| 281 | $this->set_attr( 'mapped_categories', $mapped_categories ); |
| 282 | $this->set_attr( 'unmapped_categories', $unmapped_categories ); |
| 283 | |
| 284 | Category::update_map( $map ); |
| 285 | } |
| 286 | |
| 287 | |
| 288 | /** |
| 289 | * Checks the Square API for any unmapped categories we may have. |
| 290 | * |
| 291 | * @since 2.0.0 |
| 292 | * |
| 293 | * @throws \Exception |
| 294 | */ |
| 295 | protected function query_unmapped_categories() { |
| 296 | |
| 297 | $unmapped_categories = $this->get_attr( 'unmapped_categories', array() ); |
| 298 | $mapped_categories = $this->get_attr( 'mapped_categories', array() ); |
| 299 | |
| 300 | if ( empty( $unmapped_categories ) ) { |
| 301 | |
| 302 | $this->complete_step( 'query_unmapped_categories' ); |
| 303 | |
| 304 | } else { |
| 305 | |
| 306 | $response = wc_square()->get_api()->search_catalog_objects( |
| 307 | array( |
| 308 | 'object_types' => array( 'CATEGORY' ), |
| 309 | 'cursor' => $this->get_attr( 'unmapped_categories_cursor' ), |
| 310 | ) |
| 311 | ); |
| 312 | |
| 313 | $category_map = Category::get_map(); |
| 314 | $categories = $response->get_data() instanceof SearchCatalogObjectsResponse ? $response->get_data()->getObjects() : null; |
| 315 | |
| 316 | if ( is_array( $categories ) ) { |
| 317 | |
| 318 | foreach ( $categories as $category_object ) { |
| 319 | |
| 320 | $unmapped_category_key = strtolower( $category_object->getCategoryData()->getName() ); |
| 321 | |
| 322 | if ( isset( $unmapped_categories[ $unmapped_category_key ] ) ) { |
| 323 | |
| 324 | $category_id = $unmapped_categories[ $unmapped_category_key ]['term_id']; |
| 325 | |
| 326 | $category_map[ $category_id ] = array( |
| 327 | 'square_id' => $category_object->getId(), |
| 328 | 'square_version' => $category_object->getVersion(), |
| 329 | ); |
| 330 | |
| 331 | $mapped_categories[] = $category_id; |
| 332 | unset( $unmapped_categories[ $unmapped_category_key ] ); |
| 333 | } |
| 334 | } |
| 335 | } |
| 336 | |
| 337 | Category::update_map( $category_map ); |
| 338 | $this->set_attr( 'mapped_categories', $mapped_categories ); |
| 339 | $this->set_attr( 'unmapped_categories', $unmapped_categories ); |
| 340 | |
| 341 | $cursor = $response->get_data() instanceof SearchCatalogObjectsResponse ? $response->get_data()->getCursor() : null; |
| 342 | $this->set_attr( 'unmapped_categories_cursor', $cursor ); |
| 343 | |
| 344 | if ( empty( $cursor ) ) { |
| 345 | |
| 346 | $this->complete_step( 'query_unmapped_categories' ); |
| 347 | } |
| 348 | } |
| 349 | } |
| 350 | |
| 351 | |
| 352 | /** |
| 353 | * Upserts the categories for the selected products to Square. |
| 354 | * |
| 355 | * @since 2.0.0 |
| 356 | * |
| 357 | * @throws \Exception |
| 358 | */ |
| 359 | protected function upsert_categories() { |
| 360 | |
| 361 | $category_ids = $this->get_attr( 'category_ids' ); |
| 362 | $categories = get_terms( |
| 363 | array( |
| 364 | 'taxonomy' => 'product_cat', |
| 365 | 'include' => $category_ids, |
| 366 | ) |
| 367 | ); |
| 368 | |
| 369 | $batches = array(); |
| 370 | $reverse_map = array(); |
| 371 | |
| 372 | // For now, keep it to one category per batch. Since we can still send 1000 batches per request, it's efficient, |
| 373 | // and insulates errors per category rather than a single category error breaking the entire batch it is in. |
| 374 | // TODO: Performance - Consider sending larger-sized batches to reduce total requests for shops with thousands of categories. |
| 375 | // This will require the ability to handle a failed batch, pulling out the error-causing category, and retrying the batch. |
| 376 | foreach ( $categories as $category ) { |
| 377 | |
| 378 | $category_id = $category->term_id; |
| 379 | $square_id = Category::get_square_id( $category_id ); |
| 380 | $square_version = Category::get_square_version( $category_id ); |
| 381 | |
| 382 | $reverse_map[ $square_id ] = $category_id; |
| 383 | |
| 384 | $catalog_category = new \Square\Models\CatalogCategory(); |
| 385 | $catalog_category->setName( wp_specialchars_decode( $category->name ) ); |
| 386 | |
| 387 | $catalog_object = new \Square\Models\CatalogObject( 'CATEGORY', $square_id ); |
| 388 | $catalog_object->setCategoryData( $catalog_category ); |
| 389 | |
| 390 | if ( 0 < $square_version ) { |
| 391 | $catalog_object->setVersion( $square_version ); |
| 392 | } |
| 393 | |
| 394 | $batches[] = new \Square\Models\CatalogObjectBatch( array( $catalog_object ) ); |
| 395 | } |
| 396 | |
| 397 | foreach ( array_chunk( $batches, $this->get_max_objects_per_upsert() ) as $batch ) { |
| 398 | $idempotency_key = wc_square()->get_idempotency_key( md5( serialize( $batch ) . $this->get_attr( 'id' ) ) . '_upsert_categories' ); |
| 399 | $result = wc_square()->get_api()->batch_upsert_catalog_objects( $idempotency_key, $batch ); |
| 400 | |
| 401 | if ( ! $result->get_data() instanceof BatchUpsertCatalogObjectsResponse ) { |
| 402 | throw new \Exception( 'Response data is invalid' ); |
| 403 | } |
| 404 | |
| 405 | $id_mappings = $result->get_data()->getIdMappings(); // new entries to Square will return in the ID Mapping. |
| 406 | |
| 407 | if ( ! empty( $id_mappings ) ) { |
| 408 | foreach ( $id_mappings as $id_mapping ) { |
| 409 | $client_object_id = $id_mapping->getClientObjectId(); |
| 410 | $remote_object_id = $id_mapping->getObjectId(); |
| 411 | |
| 412 | if ( isset( $reverse_map[ $client_object_id ] ) ) { |
| 413 | $reverse_map[ $remote_object_id ] = $reverse_map[ $client_object_id ]; |
| 414 | unset( $reverse_map[ $client_object_id ] ); |
| 415 | } |
| 416 | } |
| 417 | } |
| 418 | |
| 419 | foreach ( $result->get_data()->getObjects() as $upserted_category ) { |
| 420 | $id = $upserted_category->getId(); |
| 421 | $version = $upserted_category->getVersion(); |
| 422 | |
| 423 | if ( isset( $reverse_map[ $id ] ) ) { |
| 424 | Category::update_mapping( $reverse_map[ $id ], $id, $version ); |
| 425 | unset( $reverse_map[ $id ] ); |
| 426 | } |
| 427 | } |
| 428 | } |
| 429 | |
| 430 | $this->complete_step( 'upsert_categories' ); |
| 431 | } |
| 432 | |
| 433 | /** |
| 434 | * Updates a set of products that already have a Square ID set and are found in the catalog. |
| 435 | * |
| 436 | * @since 2.0.0 |
| 437 | * |
| 438 | * @throws \Exception |
| 439 | */ |
| 440 | protected function update_matched_products() { |
| 441 | |
| 442 | $product_ids = $this->get_attr( 'matched_product_ids', $this->get_attr( 'validated_product_ids', array() ) ); |
| 443 | $processed_product_ids = $this->get_attr( 'processed_product_ids', array() ); |
| 444 | |
| 445 | // remove IDs that have already been processed |
| 446 | $product_ids = array_diff( $product_ids, $processed_product_ids ); |
| 447 | |
| 448 | if ( empty( $product_ids ) ) { |
| 449 | |
| 450 | $this->complete_step( 'update_matched_products' ); |
| 451 | return; |
| 452 | } |
| 453 | |
| 454 | if ( count( $product_ids ) > $this->get_max_objects_to_retrieve() ) { |
| 455 | |
| 456 | $product_ids_batch = array_slice( $product_ids, 0, $this->get_max_objects_to_retrieve() ); |
| 457 | |
| 458 | $this->set_attr( 'matched_product_ids', array_diff( $product_ids, $product_ids_batch ) ); |
| 459 | |
| 460 | $product_ids = $product_ids_batch; |
| 461 | |
| 462 | } else { |
| 463 | |
| 464 | $this->set_attr( 'matched_product_ids', array() ); |
| 465 | } |
| 466 | |
| 467 | $products_map = Product::get_square_meta( $product_ids, 'square_item_id' ); |
| 468 | $square_ids = array_keys( $products_map ); |
| 469 | |
| 470 | if ( empty( $square_ids ) ) { |
| 471 | return; |
| 472 | } |
| 473 | |
| 474 | $response = wc_square()->get_api()->batch_retrieve_catalog_objects( $square_ids ); |
| 475 | |
| 476 | if ( ! $response->get_data() instanceof BatchRetrieveCatalogObjectsResponse ) { |
| 477 | throw new \Exception( 'Response data is missing' ); |
| 478 | } |
| 479 | |
| 480 | $catalog_objects = array(); |
| 481 | |
| 482 | if ( $response->get_data()->getObjects() ) { |
| 483 | |
| 484 | foreach ( $response->get_data()->getObjects() as $catalog_object ) { |
| 485 | |
| 486 | if ( ! empty( $products_map[ $catalog_object->getId() ]['product_id'] ) ) { |
| 487 | |
| 488 | $product_id = $products_map[ $catalog_object->getId() ]['product_id']; |
| 489 | |
| 490 | $catalog_objects[ $product_id ] = $catalog_object; |
| 491 | } |
| 492 | } |
| 493 | } |
| 494 | |
| 495 | if ( ! empty( $catalog_objects ) ) { |
| 496 | |
| 497 | $result = $this->upsert_catalog_objects( $catalog_objects ); |
| 498 | |
| 499 | $this->set_attr( 'processed_product_ids', array_merge( $result['processed'], $processed_product_ids ) ); |
| 500 | |
| 501 | // any products that were staged but not processed, push to the matched array to try next time |
| 502 | $matched_product_ids = $this->get_attr( 'matched_product_ids', array() ); |
| 503 | $this->set_attr( 'matched_product_ids', array_merge( $result['unprocessed'], $matched_product_ids ) ); |
| 504 | } |
| 505 | } |
| 506 | |
| 507 | |
| 508 | /** |
| 509 | * Searches the full Square catalog to find matches and updates them. |
| 510 | * |
| 511 | * @since 2.0.0 |
| 512 | * |
| 513 | * @throws \Exception |
| 514 | */ |
| 515 | protected function search_matched_products() { |
| 516 | |
| 517 | $product_ids = $this->get_attr( 'search_product_ids', $this->get_attr( 'validated_product_ids', array() ) ); |
| 518 | $processed_product_ids = $this->get_attr( 'processed_product_ids', array() ); |
| 519 | $in_progress = $this->get_attr( |
| 520 | 'in_progress_search_matched_products', |
| 521 | array( |
| 522 | 'unprocessed_search_response' => null, |
| 523 | 'processed_remote_object_ids' => array(), |
| 524 | 'catalog_objects_to_update' => array(), |
| 525 | 'upserting' => false, |
| 526 | ) |
| 527 | ); |
| 528 | |
| 529 | // remove IDs that have already been processed |
| 530 | $product_ids = array_diff( $product_ids, $processed_product_ids ); |
| 531 | |
| 532 | if ( empty( $product_ids ) ) { |
| 533 | |
| 534 | $this->complete_step( 'search_matched_products' ); |
| 535 | return; |
| 536 | } |
| 537 | |
| 538 | $products_map = Product::get_square_meta( $product_ids, 'square_item_id' ); |
| 539 | |
| 540 | if ( ! empty( $in_progress['unprocessed_search_response'] ) ) { |
| 541 | $search_response = ApiHelper::deserialize( $in_progress['unprocessed_search_response'], new SearchCatalogObjectsResponse() ); |
| 542 | } else { |
| 543 | |
| 544 | $response = wc_square()->get_api()->search_catalog_objects( |
| 545 | array( |
| 546 | 'cursor' => $this->get_attr( 'search_products_cursor' ), |
| 547 | 'object_types' => array( 'ITEM' ), |
| 548 | 'limit' => $this->get_max_objects_to_retrieve(), |
| 549 | ) |
| 550 | ); |
| 551 | |
| 552 | $search_response = $response->get_data(); |
| 553 | |
| 554 | $in_progress['unprocessed_search_response'] = wp_json_encode( $search_response, JSON_PRETTY_PRINT ); |
| 555 | } |
| 556 | |
| 557 | if ( ! $search_response instanceof SearchCatalogObjectsResponse ) { |
| 558 | throw new \Exception( 'Response data is missing' ); |
| 559 | } |
| 560 | |
| 561 | $catalog_objects = $search_response->getObjects() ? $search_response->getObjects() : array(); |
| 562 | $cursor = $search_response->getCursor(); |
| 563 | $catalog_objects_to_update = $in_progress['catalog_objects_to_update']; |
| 564 | |
| 565 | if ( true !== $in_progress['upserting'] ) { |
| 566 | |
| 567 | wc_square()->log( 'Searching through ' . count( $catalog_objects ) . ' catalog objects' ); |
| 568 | |
| 569 | foreach ( $catalog_objects as $catalog_object ) { |
| 570 | |
| 571 | $remote_object_id = $catalog_object->getId(); |
| 572 | |
| 573 | if ( in_array( $remote_object_id, $in_progress['processed_remote_object_ids'], true ) ) { |
| 574 | continue; |
| 575 | } |
| 576 | |
| 577 | if ( isset( $products_map[ $remote_object_id ]['product_id'] ) ) { |
| 578 | |
| 579 | $product_id = $products_map[ $remote_object_id ]['product_id']; |
| 580 | |
| 581 | $product = wc_get_product( $product_id ); |
| 582 | |
| 583 | // update the product's meta |
| 584 | if ( $product ) { |
| 585 | Product\Woo_SOR::update_product( $product, $catalog_object ); |
| 586 | } |
| 587 | |
| 588 | foreach ( $catalog_object->getItemData()->getVariations() as $catalog_variation ) { |
| 589 | |
| 590 | $variation_product_id = Product::get_product_id_by_square_variation_id( $catalog_variation->getId() ); |
| 591 | |
| 592 | if ( $variation_product_id ) { |
| 593 | |
| 594 | $variation = wc_get_product( $variation_product_id ); |
| 595 | |
| 596 | if ( $variation ) { |
| 597 | Product\Woo_SOR::update_variation( $variation, $catalog_variation ); |
| 598 | } |
| 599 | } |
| 600 | } |
| 601 | |
| 602 | $catalog_objects_to_update[ $product_id ] = $catalog_object; |
| 603 | |
| 604 | } else { |
| 605 | |
| 606 | // no variations? no sku |
| 607 | if ( ! is_array( $catalog_object->getItemData()->getVariations() ) ) { |
| 608 | continue; |
| 609 | } |
| 610 | |
| 611 | $product_id = 0; |
| 612 | $matched_object = null; |
| 613 | |
| 614 | foreach ( $catalog_object->getItemData()->getVariations() as $catalog_variation ) { |
| 615 | |
| 616 | $product_id = wc_get_product_id_by_sku( $catalog_variation->getItemVariationData()->getSku() ); |
| 617 | |
| 618 | $product = wc_get_product( $product_id ); |
| 619 | |
| 620 | if ( ! $product ) { |
| 621 | continue; |
| 622 | } |
| 623 | |
| 624 | $parent_product = wc_get_product( $product->get_parent_id() ); |
| 625 | |
| 626 | if ( $product->get_parent_id() && $parent_product ) { |
| 627 | $product = $parent_product; |
| 628 | } |
| 629 | |
| 630 | if ( ! in_array( $product->get_id(), $product_ids, false ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.FoundNonStrictFalse |
| 631 | continue; |
| 632 | } |
| 633 | |
| 634 | $product_id = $product->get_id(); |
| 635 | $matched_object = $catalog_object; |
| 636 | |
| 637 | break; |
| 638 | } |
| 639 | |
| 640 | if ( $product_id && $matched_object ) { |
| 641 | $catalog_objects_to_update[ $product_id ] = $matched_object; |
| 642 | } |
| 643 | } |
| 644 | |
| 645 | $in_progress['processed_remote_object_ids'][] = $remote_object_id; |
| 646 | $in_progress['catalog_objects_to_update'] = $catalog_objects_to_update; |
| 647 | } |
| 648 | } |
| 649 | |
| 650 | $in_progress['upserting'] = true; |
| 651 | |
| 652 | $catalog_processed = ! $cursor; |
| 653 | |
| 654 | $remaining_product_ids = array_diff( $product_ids, array_keys( $catalog_objects_to_update ) ); |
| 655 | |
| 656 | if ( ! empty( $catalog_objects_to_update ) ) { |
| 657 | |
| 658 | $result = $this->upsert_catalog_objects( $catalog_objects_to_update ); |
| 659 | |
| 660 | $processed_product_ids = array_merge( $result['processed'], $processed_product_ids ); |
| 661 | $this->set_attr( 'processed_product_ids', $processed_product_ids ); |
| 662 | |
| 663 | if ( ! empty( $result['unprocessed'] ) ) { |
| 664 | |
| 665 | $catalog_processed = false; |
| 666 | $remaining_product_ids = array_merge( $result['unprocessed'], $remaining_product_ids ); |
| 667 | $in_progress['catalog_objects_to_update'] = array_diff_key( $catalog_objects_to_update, array_flip( $processed_product_ids ) ); |
| 668 | |
| 669 | } else { |
| 670 | |
| 671 | $in_progress = null; |
| 672 | } |
| 673 | |
| 674 | $this->set_attr( 'in_progress_search_matched_products', $in_progress ); |
| 675 | } |
| 676 | |
| 677 | if ( ! $catalog_processed && ! empty( $remaining_product_ids ) ) { |
| 678 | |
| 679 | $this->set_attr( 'search_products_cursor', $cursor ); |
| 680 | $this->set_attr( 'search_product_ids', $remaining_product_ids ); |
| 681 | |
| 682 | } else { |
| 683 | |
| 684 | Product::clear_square_meta( $remaining_product_ids ); |
| 685 | $this->complete_step( 'search_matched_products' ); |
| 686 | } |
| 687 | } |
| 688 | |
| 689 | |
| 690 | /** |
| 691 | * @throws \Exception |
| 692 | */ |
| 693 | protected function upsert_new_products() { |
| 694 | |
| 695 | $product_ids = $this->get_attr( 'upsert_new_product_ids', $this->get_attr( 'validated_product_ids', array() ) ); |
| 696 | $processed_product_ids = $this->get_attr( 'processed_product_ids', array() ); |
| 697 | |
| 698 | // remove IDs that have already been processed |
| 699 | $product_ids = array_diff( $product_ids, $processed_product_ids ); |
| 700 | |
| 701 | if ( empty( $product_ids ) ) { |
| 702 | |
| 703 | $this->complete_step( 'upsert_new_products' ); |
| 704 | return; |
| 705 | } |
| 706 | |
| 707 | $catalog_objects = array(); |
| 708 | |
| 709 | foreach ( $product_ids as $product_id ) { |
| 710 | |
| 711 | $catalog_item = new \Square\Models\CatalogItem(); |
| 712 | $catalog_object = new CatalogObject( 'ITEM', Product::get_square_item_id( $product_id ) ); |
| 713 | $catalog_object->setItemData( $catalog_item ); |
| 714 | $catalog_objects[ $product_id ] = $catalog_object; |
| 715 | } |
| 716 | |
| 717 | $result = $this->upsert_catalog_objects( $catalog_objects ); |
| 718 | |
| 719 | // newly upserted IDs should get their inventory pushed |
| 720 | $this->set_attr( 'inventory_push_product_ids', $result['processed'] ); |
| 721 | |
| 722 | $processed_product_ids = array_merge( $result['processed'], $processed_product_ids ); |
| 723 | |
| 724 | $this->set_attr( 'processed_product_ids', $processed_product_ids ); |
| 725 | |
| 726 | if ( ! empty( $result['unprocessed'] ) ) { |
| 727 | |
| 728 | $this->set_attr( 'upsert_new_product_ids', $result['unprocessed'] ); |
| 729 | |
| 730 | } else { |
| 731 | |
| 732 | // at this point, log a failure for any products that weren't processed |
| 733 | foreach ( array_diff( $product_ids, $processed_product_ids ) as $product_id ) { |
| 734 | Records::set_record( |
| 735 | array( |
| 736 | 'type' => 'info', |
| 737 | 'product_id' => $product_id, |
| 738 | 'message' => sprintf( |
| 739 | /* translators: Placeholder: %s - product ID */ |
| 740 | esc_html__( 'Product #%s could not be updated.', 'woocommerce-square' ), |
| 741 | '<a href="' . esc_url( get_edit_post_link( $product_id ) ) . '">' . $product_id . '</a>' |
| 742 | ), |
| 743 | ) |
| 744 | ); |
| 745 | } |
| 746 | |
| 747 | $this->complete_step( 'upsert_new_products' ); |
| 748 | } |
| 749 | } |
| 750 | |
| 751 | |
| 752 | /** |
| 753 | * Upserts a list of catalog objects and updates their cooresponding products. |
| 754 | * |
| 755 | * @since 2.0.0 |
| 756 | * |
| 757 | * @param array $objects list of catalog objects to update, as $product_id => CatalogItem |
| 758 | * @return array |
| 759 | * @throws \Exception |
| 760 | */ |
| 761 | protected function upsert_catalog_objects( array $objects ) { |
| 762 | |
| 763 | wc_square()->log( 'Upserting ' . count( $objects ) . ' catalog objects' ); |
| 764 | |
| 765 | $is_delete_action = 'delete' === $this->get_attr( 'action' ); |
| 766 | $product_ids = array_keys( $objects ); |
| 767 | $original_square_image_ids = array(); |
| 768 | $staged_product_ids = array(); |
| 769 | $successful_product_ids = array(); |
| 770 | $total_object_count = 0; |
| 771 | $batches = array(); |
| 772 | $result = array( |
| 773 | 'processed' => array(), |
| 774 | 'unprocessed' => $product_ids, |
| 775 | ); |
| 776 | |
| 777 | $in_progress = $this->get_attr( |
| 778 | 'in_progress_upsert_catalog_objects', |
| 779 | array( |
| 780 | 'batches' => array(), |
| 781 | 'staged_product_ids' => array(), |
| 782 | 'total_object_count' => null, |
| 783 | 'unprocessed_upsert_response' => null, |
| 784 | 'mapped_client_item_ids' => array(), |
| 785 | 'processed_remote_catalog_item_ids' => array(), |
| 786 | ) |
| 787 | ); |
| 788 | |
| 789 | if ( null === $in_progress['unprocessed_upsert_response'] ) { |
| 790 | |
| 791 | // need all three items to restore from in-progress |
| 792 | if ( ! empty( $in_progress['batches'] ) && ! empty( $in_progress['staged_product_ids'] ) && ! empty( $in_progress['total_object_count'] ) ) { |
| 793 | |
| 794 | $staged_product_ids = $in_progress['staged_product_ids']; |
| 795 | $total_object_count = $in_progress['total_object_count']; |
| 796 | $batches = array_map( |
| 797 | static function ( $batch_data ) { |
| 798 | return ApiHelper::deserialize( $batch_data ); |
| 799 | }, |
| 800 | $in_progress['batches'] |
| 801 | ); |
| 802 | } |
| 803 | |
| 804 | foreach ( $objects as $product_id => $object ) { |
| 805 | |
| 806 | if ( in_array( $product_id, $staged_product_ids, true ) ) { |
| 807 | continue; |
| 808 | } |
| 809 | |
| 810 | if ( ! $object instanceof CatalogObject ) { |
| 811 | $object = $this->convert_to_catalog_object( $object ); |
| 812 | } |
| 813 | |
| 814 | $product = wc_get_product( $product_id ); |
| 815 | $original_square_image_ids[ $product_id ] = $product->get_meta( '_square_item_image_id' ); |
| 816 | |
| 817 | $catalog_item = new Catalog_Item( $product, $is_delete_action ); |
| 818 | $batch = $catalog_item->get_batch( $object ); |
| 819 | $object_count = $catalog_item->get_batch_object_count(); |
| 820 | |
| 821 | if ( $this->get_max_objects_total() >= $object_count + $total_object_count ) { |
| 822 | $batches[] = $batch; |
| 823 | $total_object_count += $object_count; |
| 824 | $staged_product_ids[] = $product_id; |
| 825 | } else { |
| 826 | break; |
| 827 | } |
| 828 | } |
| 829 | } |
| 830 | |
| 831 | $upsert_response = null; |
| 832 | |
| 833 | if ( null !== $in_progress['unprocessed_upsert_response'] ) { |
| 834 | $upsert_response = ApiHelper::deserialize( $in_progress['unprocessed_upsert_response'], new BatchUpsertCatalogObjectsResponse() ); |
| 835 | } |
| 836 | |
| 837 | if ( ! $upsert_response instanceof BatchUpsertCatalogObjectsResponse ) { |
| 838 | |
| 839 | $start = microtime( true ); |
| 840 | |
| 841 | $idempotency_key = wc_square()->get_idempotency_key( md5( serialize( $batches ) ) . time() . '_upsert_products' ); |
| 842 | $response = wc_square()->get_api()->batch_upsert_catalog_objects( $idempotency_key, $batches ); |
| 843 | $upsert_response = $response->get_data(); |
| 844 | |
| 845 | if ( ! $upsert_response instanceof BatchUpsertCatalogObjectsResponse ) { |
| 846 | throw new \Exception( 'API response data is missing' ); |
| 847 | } |
| 848 | |
| 849 | $duration = number_format( microtime( true ) - $start, 2 ); |
| 850 | |
| 851 | wc_square()->log( 'Upserted ' . count( $upsert_response->getObjects() ) . ' objects in ' . $duration . 's' ); |
| 852 | |
| 853 | $in_progress['unprocessed_upsert_response'] = wp_json_encode( $response, JSON_PRETTY_PRINT ); |
| 854 | } |
| 855 | |
| 856 | // update local square meta for newly upserted objects |
| 857 | if ( ! $is_delete_action && $upsert_response instanceof BatchUpsertCatalogObjectsResponse && is_array( $upsert_response->getIdMappings() ) ) { |
| 858 | |
| 859 | wc_square()->log( 'Mapping new Square item IDs to WooCommerce product IDs' ); |
| 860 | |
| 861 | $start = microtime( true ); |
| 862 | |
| 863 | foreach ( $upsert_response->getIdMappings() as $id_mapping ) { |
| 864 | |
| 865 | $client_item_id = $id_mapping->getClientObjectId(); |
| 866 | $remote_item_id = $id_mapping->getObjectId(); |
| 867 | |
| 868 | if ( in_array( $client_item_id, $in_progress['mapped_client_item_ids'], true ) ) { |
| 869 | continue; |
| 870 | } |
| 871 | |
| 872 | if ( 0 === strpos( $client_item_id, '#item_variation_' ) ) { |
| 873 | |
| 874 | $product_id = substr( $client_item_id, strlen( '#item_variation_' ) ); |
| 875 | Product::set_square_item_variation_id( $product_id, $remote_item_id ); |
| 876 | |
| 877 | } elseif ( 0 === strpos( $client_item_id, '#item_' ) ) { |
| 878 | |
| 879 | $product_id = substr( $client_item_id, strlen( '#item_' ) ); |
| 880 | Product::set_square_item_id( $product_id, $remote_item_id ); |
| 881 | } |
| 882 | |
| 883 | $in_progress['mapped_client_item_ids'][] = $client_item_id; |
| 884 | } |
| 885 | |
| 886 | $duration = number_format( microtime( true ) - $start, 2 ); |
| 887 | |
| 888 | wc_square()->log( 'Mapped ' . count( $in_progress['mapped_client_item_ids'] ) . ' Square IDs in ' . $duration . 's' ); |
| 889 | } |
| 890 | |
| 891 | $pull_inventory_variation_ids = $this->get_attr( 'pull_inventory_variation_ids', array() ); |
| 892 | |
| 893 | wc_square()->log( 'Storing Square item data to WooCommerce products' ); |
| 894 | |
| 895 | $start = microtime( true ); |
| 896 | |
| 897 | // loop through all returned objects and store their IDs to Woo products |
| 898 | foreach ( $upsert_response->getObjects() as $remote_catalog_item ) { |
| 899 | |
| 900 | $remote_item_id = $remote_catalog_item->getId(); |
| 901 | |
| 902 | if ( in_array( $remote_item_id, $in_progress['processed_remote_catalog_item_ids'], true ) ) { |
| 903 | continue; |
| 904 | } |
| 905 | |
| 906 | $product = Product::get_product_by_square_id( $remote_item_id ); |
| 907 | |
| 908 | if ( ! $product ) { |
| 909 | $in_progress['processed_remote_catalog_item_ids'][] = $remote_item_id; |
| 910 | continue; |
| 911 | } |
| 912 | |
| 913 | Product::update_square_meta( |
| 914 | $product, |
| 915 | array( |
| 916 | 'item_id' => $remote_item_id, |
| 917 | 'item_version' => $remote_catalog_item->getVersion(), |
| 918 | 'item_image_id' => Product::get_catalog_item_thumbnail_id( $remote_catalog_item ), |
| 919 | ) |
| 920 | ); |
| 921 | |
| 922 | $successful_product_ids[] = $product->get_id(); |
| 923 | |
| 924 | if ( is_array( $remote_catalog_item->getItemData()->getVariations() ) ) { |
| 925 | |
| 926 | foreach ( $remote_catalog_item->getItemData()->getVariations() as $catalog_item_variation ) { |
| 927 | |
| 928 | $product_variation = Product::get_product_by_square_variation_id( $catalog_item_variation->getId() ); |
| 929 | |
| 930 | if ( $product_variation ) { |
| 931 | |
| 932 | $pull_inventory_variation_ids[] = $catalog_item_variation->getId(); |
| 933 | |
| 934 | Product::update_square_meta( |
| 935 | $product_variation, |
| 936 | array( |
| 937 | 'item_variation_id' => $catalog_item_variation->getId(), |
| 938 | 'item_variation_version' => $catalog_item_variation->getVersion(), |
| 939 | ) |
| 940 | ); |
| 941 | } |
| 942 | } |
| 943 | } |
| 944 | |
| 945 | $local_image_id = $product->get_image_id(); |
| 946 | $product_id = $product->get_id(); |
| 947 | |
| 948 | // If there is a local image which is different from the last uploaded image |
| 949 | // Or if the remote square image id has changed |
| 950 | if ( ( $local_image_id && $local_image_id !== $product->get_meta( '_square_uploaded_image_id' ) ) || |
| 951 | ( ! ( $original_square_image_ids[ $product_id ] && $original_square_image_ids[ $product_id ] === $product->get_meta( '_square_item_image_id' ) ) ) ) { |
| 952 | // there is no batch image endpoint |
| 953 | $this->push_product_image( $product ); |
| 954 | |
| 955 | } |
| 956 | |
| 957 | $in_progress['processed_remote_catalog_item_ids'][] = $remote_item_id; |
| 958 | |
| 959 | $result['processed'][] = $product->get_id(); |
| 960 | $result['unprocessed'] = array_diff( $product_ids, $result['processed'] ); |
| 961 | } |
| 962 | |
| 963 | $this->set_attr( 'pull_inventory_variation_ids', $pull_inventory_variation_ids ); |
| 964 | |
| 965 | $duration = number_format( microtime( true ) - $start, 2 ); |
| 966 | |
| 967 | wc_square()->log( 'Stored Square data to ' . count( $result['processed'] ) . ' products in ' . $duration . 's' ); |
| 968 | |
| 969 | // log any failed products |
| 970 | foreach ( array_diff( $staged_product_ids, $successful_product_ids ) as $product_id ) { |
| 971 | |
| 972 | Records::set_record( |
| 973 | array( |
| 974 | 'type' => 'alert', |
| 975 | 'product_id' => $product_id, |
| 976 | 'message' => sprintf( |
| 977 | /* translators: Placeholder: %s - product ID */ |
| 978 | esc_html__( 'Product %s could not be updated in Square.', 'woocommerce-square' ), |
| 979 | '<a href="' . esc_url( get_edit_post_link( $product_id ) ) . '">' . $product_id . '</a>' |
| 980 | ), |
| 981 | ) |
| 982 | ); |
| 983 | } |
| 984 | |
| 985 | $this->set_attr( 'in_progress_upsert_catalog_objects', null ); |
| 986 | |
| 987 | $result['processed'] = $staged_product_ids; |
| 988 | $result['unprocessed'] = array_diff( $product_ids, $staged_product_ids ); |
| 989 | |
| 990 | return $result; |
| 991 | } |
| 992 | |
| 993 | |
| 994 | /** |
| 995 | * Converts object data to an instance of CatalogObject. |
| 996 | * |
| 997 | * @since 2.0.0 |
| 998 | * |
| 999 | * @param array|string $object_data json string or array of object data |
| 1000 | * @return CatalogObject |
| 1001 | */ |
| 1002 | protected function convert_to_catalog_object( $object_data ) { |
| 1003 | $object_data_string = is_string( $object_data ) ? $object_data : wp_json_encode( $object_data ); |
| 1004 | $object_data_obj = is_string( $object_data ) ? json_decode( $object_data ) : $object_data; |
| 1005 | |
| 1006 | $catalog_object = new CatalogObject( $object_data_obj->type, $object_data_obj->id ); |
| 1007 | $object = ApiHelper::deserialize( $object_data_string, $catalog_object ); |
| 1008 | |
| 1009 | return $object instanceof CatalogObject ? $object : null; |
| 1010 | } |
| 1011 | |
| 1012 | |
| 1013 | /** |
| 1014 | * Pushes a product's image to Square. |
| 1015 | * |
| 1016 | * @since 2.0.0 |
| 1017 | * |
| 1018 | * @param \WC_Product|int $product product object or ID |
| 1019 | */ |
| 1020 | protected function push_product_image( $product ) { |
| 1021 | |
| 1022 | $product = wc_get_product( $product ); |
| 1023 | |
| 1024 | if ( ! $product instanceof \WC_Product || ! $product->get_image_id() ) { |
| 1025 | return; |
| 1026 | } |
| 1027 | |
| 1028 | $local_image_id = $product->get_image_id(); |
| 1029 | $image_path = get_attached_file( $local_image_id ); |
| 1030 | |
| 1031 | if ( $image_path ) { |
| 1032 | |
| 1033 | try { |
| 1034 | |
| 1035 | $image_id = wc_square()->get_api()->create_image( $image_path, Product::get_square_item_id( $product ), $product->get_name() ); |
| 1036 | |
| 1037 | Product::set_square_image_id( $product, $image_id ); |
| 1038 | |
| 1039 | // record the WC image ID that was uploaded |
| 1040 | $product->update_meta_data( '_square_uploaded_image_id', $local_image_id ); |
| 1041 | $product->save_meta_data(); |
| 1042 | |
| 1043 | } catch ( \Exception $exception ) { |
| 1044 | |
| 1045 | if ( wc_square()->get_settings_handler()->is_debug_enabled() ) { |
| 1046 | wc_square()->log( 'Could not upload image for product #' . $product->get_id() . ': ' . $exception->getMessage() ); |
| 1047 | } |
| 1048 | } |
| 1049 | } |
| 1050 | } |
| 1051 | |
| 1052 | |
| 1053 | /** |
| 1054 | * Pushes WooCommerce inventory to Square for synced items. |
| 1055 | * |
| 1056 | * @since 2.0.0 |
| 1057 | * |
| 1058 | * @throws \Exception |
| 1059 | */ |
| 1060 | protected function push_inventory() { |
| 1061 | |
| 1062 | $product_ids = $this->get_attr( 'inventory_push_product_ids', array() ); |
| 1063 | $count = $this->get_attr( 'push_inventory_count', 0 ); |
| 1064 | $inventory_changes = array(); |
| 1065 | $inventory_change_count = 0; |
| 1066 | |
| 1067 | foreach ( $product_ids as $key => $product_id ) { |
| 1068 | |
| 1069 | $product = wc_get_product( $product_id ); |
| 1070 | $square_variation_id = Product::get_square_item_variation_id( $product_id, false ); |
| 1071 | |
| 1072 | if ( $product instanceof \WC_Product ) { |
| 1073 | |
| 1074 | $product_inventory_changes = array(); |
| 1075 | |
| 1076 | if ( $product->is_type( 'variable' ) && $product->has_child() ) { |
| 1077 | |
| 1078 | foreach ( $product->get_children() as $child_id ) { |
| 1079 | |
| 1080 | $child = wc_get_product( $child_id ); |
| 1081 | $inventory_change = Product::get_inventory_change_physical_count_type( $child ); |
| 1082 | |
| 1083 | if ( $child instanceof \WC_Product && $child->get_manage_stock() && $inventory_change ) { |
| 1084 | |
| 1085 | $product_inventory_changes[] = $inventory_change; |
| 1086 | } |
| 1087 | } |
| 1088 | } elseif ( $square_variation_id ) { |
| 1089 | |
| 1090 | $inventory_change = Product::get_inventory_change_physical_count_type( $product ); |
| 1091 | |
| 1092 | if ( $inventory_change && $product->get_manage_stock() ) { |
| 1093 | |
| 1094 | $product_inventory_changes[] = $inventory_change; |
| 1095 | } |
| 1096 | } |
| 1097 | |
| 1098 | if ( self::BATCH_CHANGE_INVENTORY_LIMIT >= $inventory_change_count + count( $product_inventory_changes ) ) { |
| 1099 | if ( ! empty( $product_inventory_changes ) ) { |
| 1100 | $inventory_changes[] = $product_inventory_changes; |
| 1101 | $inventory_change_count += count( $product_inventory_changes ); |
| 1102 | } |
| 1103 | unset( $product_ids[ $key ] ); |
| 1104 | |
| 1105 | } else { |
| 1106 | |
| 1107 | break; |
| 1108 | } |
| 1109 | } else { |
| 1110 | |
| 1111 | unset( $product_ids[ $key ] ); |
| 1112 | } |
| 1113 | } |
| 1114 | |
| 1115 | if ( ! empty( $inventory_changes ) ) { |
| 1116 | |
| 1117 | $inventory_changes = array_merge( ...$inventory_changes ); |
| 1118 | $idempotency_key = wc_square()->get_idempotency_key( md5( serialize( $inventory_changes ) ) . '_change_inventory' ); |
| 1119 | $result = wc_square()->get_api()->batch_change_inventory( $idempotency_key, $inventory_changes ); |
| 1120 | } |
| 1121 | |
| 1122 | $this->set_attr( 'inventory_push_product_ids', $product_ids ); |
| 1123 | $this->set_attr( 'push_inventory_count', $count + count( $inventory_changes ) ); |
| 1124 | |
| 1125 | if ( empty( $product_ids ) ) { |
| 1126 | |
| 1127 | $this->complete_step( 'push_inventory' ); |
| 1128 | } |
| 1129 | } |
| 1130 | |
| 1131 | |
| 1132 | /** |
| 1133 | * Performs a sync when Square is the Sync setting. |
| 1134 | * |
| 1135 | * @since 2.0.0 |
| 1136 | */ |
| 1137 | protected function square_sor_sync() { |
| 1138 | |
| 1139 | $synced_product_ids = $this->get_attr( 'validated_product_ids', array() ); |
| 1140 | $processed_product_ids = $this->get_attr( 'processed_product_ids', array() ); |
| 1141 | $deleted_square_variations = $this->get_attr( 'deleted_square_variations', array() ); |
| 1142 | $unprocessed_product_ids = array_diff( array_merge( $synced_product_ids, $deleted_square_variations ), $processed_product_ids ); |
| 1143 | $catalog_processed = $this->get_attr( 'catalog_processed', false ); |
| 1144 | |
| 1145 | if ( $catalog_processed ) { |
| 1146 | |
| 1147 | wc_square()->log( 'Square catalog fully processed' ); |
| 1148 | |
| 1149 | if ( ! empty( $unprocessed_product_ids ) ) { |
| 1150 | $this->mark_failed_products( $unprocessed_product_ids ); |
| 1151 | } |
| 1152 | |
| 1153 | $this->complete_step( 'square_sor_sync' ); |
| 1154 | return; |
| 1155 | } |
| 1156 | |
| 1157 | try { |
| 1158 | |
| 1159 | $response_data = $this->get_attr( 'catalog_objects_search_response_data', null ); |
| 1160 | |
| 1161 | if ( ! $response_data ) { |
| 1162 | |
| 1163 | wc_square()->log( 'Generating a new catalog search request' ); |
| 1164 | |
| 1165 | $cursor = $this->get_attr( 'square_sor_cursor' ); |
| 1166 | |
| 1167 | $response = wc_square()->get_api()->search_catalog_objects( |
| 1168 | array( |
| 1169 | 'cursor' => $cursor, |
| 1170 | 'object_types' => array( 'ITEM' ), |
| 1171 | 'include_related_objects' => true, |
| 1172 | 'limit' => $this->get_max_objects_to_retrieve(), |
| 1173 | ) |
| 1174 | ); |
| 1175 | |
| 1176 | $response_data = $response->get_data(); |
| 1177 | |
| 1178 | $this->set_attr( 'catalog_objects_search_response_data', wp_json_encode( $response_data ) ); |
| 1179 | |
| 1180 | } else { |
| 1181 | |
| 1182 | $response_data = ApiHelper::deserialize( $response_data, new SearchCatalogObjectsResponse() ); |
| 1183 | } |
| 1184 | |
| 1185 | if ( ! $response_data instanceof SearchCatalogObjectsResponse ) { |
| 1186 | throw new \Exception( 'API response data is missing' ); |
| 1187 | } |
| 1188 | |
| 1189 | $cursor = $response_data->getCursor(); |
| 1190 | $this->set_attr( 'square_sor_cursor', $cursor ); |
| 1191 | |
| 1192 | $catalog_processed = ! $cursor; |
| 1193 | $this->set_attr( 'catalog_processed', $catalog_processed ); |
| 1194 | |
| 1195 | } catch ( \Exception $exception ) { // bail early and fail for any API and plugin errors |
| 1196 | |
| 1197 | $this->fail( 'Product sync failed. ' . $exception->getMessage() ); |
| 1198 | return; |
| 1199 | } |
| 1200 | |
| 1201 | $related_objects = $response_data->getRelatedObjects(); |
| 1202 | |
| 1203 | if ( $related_objects && is_array( $related_objects ) ) { |
| 1204 | // first import any related categories |
| 1205 | foreach ( $related_objects as $related_object ) { |
| 1206 | if ( 'CATEGORY' === $related_object->getType() ) { |
| 1207 | Category::import_or_update( $related_object ); |
| 1208 | } |
| 1209 | } |
| 1210 | } |
| 1211 | |
| 1212 | $pull_inventory_variation_ids = $this->get_attr( 'pull_inventory_variation_ids', array() ); |
| 1213 | |
| 1214 | /** @var \Square\Models\CatalogObject[] */ |
| 1215 | $catalog_objects = $products_to_update = array(); |
| 1216 | |
| 1217 | $catalog_objects = $response_data->getObjects() ? $response_data->getObjects() : array(); |
| 1218 | |
| 1219 | wc_square()->log( 'Searching for products in ' . count( $catalog_objects ) . ' Square objects' ); |
| 1220 | |
| 1221 | foreach ( $catalog_objects as $object ) { |
| 1222 | |
| 1223 | $found_product = null; |
| 1224 | |
| 1225 | if ( ! $object instanceof CatalogObject ) { |
| 1226 | continue; |
| 1227 | } |
| 1228 | |
| 1229 | // filter out objects that aren't at our configured location |
| 1230 | if ( ! $object->getPresentAtAllLocations() && ( ! is_array( $object->getPresentAtLocationIds() ) || ! in_array( wc_square()->get_settings_handler()->get_location_id(), $object->getPresentAtLocationIds(), true ) ) ) { |
| 1231 | continue; |
| 1232 | } |
| 1233 | |
| 1234 | // even simple items have a single variation |
| 1235 | if ( ! is_array( $object->getItemData()->getVariations() ) ) { |
| 1236 | continue; |
| 1237 | } |
| 1238 | |
| 1239 | $maybe_parent_product = Product::get_product_by_square_id( $object->getId() ); |
| 1240 | |
| 1241 | if ( $maybe_parent_product instanceof \WC_Product && $maybe_parent_product->is_type( 'variable' ) ) { |
| 1242 | $missing_variations = array(); |
| 1243 | $woo_product_variations = $maybe_parent_product->get_children(); |
| 1244 | $square_product_variations = $object->getItemData()->getVariations(); |
| 1245 | $square_variation_ids = array_map( |
| 1246 | function( $square_product_variation ) { |
| 1247 | return wc_get_product_id_by_sku( $square_product_variation->getItemVariationData()->getSku() ); |
| 1248 | }, |
| 1249 | $square_product_variations |
| 1250 | ); |
| 1251 | |
| 1252 | foreach ( $woo_product_variations as $woo_product_variation_id ) { |
| 1253 | if ( ! in_array( (int) $woo_product_variation_id, $square_variation_ids, true ) ) { |
| 1254 | $woo_product_variation = wc_get_product( $woo_product_variation_id ); |
| 1255 | $woo_product_variation->set_status( 'private' ); |
| 1256 | $woo_product_variation->save(); |
| 1257 | $missing_variations[] = $woo_product_variation_id; |
| 1258 | } |
| 1259 | } |
| 1260 | |
| 1261 | $missing_variations = array_diff( $woo_product_variations, $square_variation_ids ); |
| 1262 | $this->set_attr( 'deleted_square_variations', $missing_variations ); |
| 1263 | } |
| 1264 | |
| 1265 | foreach ( $object->getItemData()->getVariations() as $variation ) { |
| 1266 | |
| 1267 | $found_product_id = wc_get_product_id_by_sku( $variation->getItemVariationData()->getSku() ); |
| 1268 | |
| 1269 | // bail if this product has already been processed |
| 1270 | if ( in_array( $found_product_id, $processed_product_ids, false ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.FoundNonStrictFalse |
| 1271 | break; |
| 1272 | } |
| 1273 | |
| 1274 | $found_product = wc_get_product( $found_product_id ); |
| 1275 | |
| 1276 | if ( ! $found_product ) { |
| 1277 | continue; |
| 1278 | } |
| 1279 | |
| 1280 | if ( $found_product instanceof \WC_Product_Variation ) { |
| 1281 | |
| 1282 | $found_variation = $found_product; |
| 1283 | $found_parent_id = $found_product->get_parent_id() ? $found_product->get_parent_id() : 0; |
| 1284 | $found_product = null; |
| 1285 | |
| 1286 | // bail if this parent product has already been processed |
| 1287 | if ( in_array( $found_parent_id, $processed_product_ids, false ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.FoundNonStrictFalse |
| 1288 | break; |
| 1289 | } |
| 1290 | |
| 1291 | $found_parent = wc_get_product( $found_parent_id ); |
| 1292 | |
| 1293 | if ( $found_parent ) { |
| 1294 | |
| 1295 | Product::set_square_item_variation_id( $found_variation, $variation->getId() ); |
| 1296 | |
| 1297 | $found_product = $found_parent; |
| 1298 | } |
| 1299 | |
| 1300 | break; |
| 1301 | |
| 1302 | } else { |
| 1303 | |
| 1304 | Product::set_square_item_variation_id( $found_product, $variation->getId() ); |
| 1305 | } |
| 1306 | } |
| 1307 | |
| 1308 | if ( $found_product && in_array( $found_product->get_id(), $synced_product_ids, false ) ) { // phpcs:disable WordPress.PHP.StrictInArray.FoundNonStrictFalse |
| 1309 | |
| 1310 | Product::set_square_item_id( $found_product, $object->getId() ); |
| 1311 | |
| 1312 | $products_to_update[] = $found_product; |
| 1313 | |
| 1314 | $catalog_objects[ $found_product->get_id() ] = $object; |
| 1315 | } |
| 1316 | } |
| 1317 | |
| 1318 | wc_square()->log( 'Found ' . count( $products_to_update ) . ' products with matching SKUs' ); |
| 1319 | |
| 1320 | // Square SOR always gets the latest inventory |
| 1321 | // set this before processing so nothing is missed during processing |
| 1322 | wc_square()->get_sync_handler()->set_inventory_last_synced_at(); |
| 1323 | |
| 1324 | foreach ( $products_to_update as $product ) { |
| 1325 | |
| 1326 | try { |
| 1327 | |
| 1328 | $square_object = ! empty( $catalog_objects[ $product->get_id() ] ) ? $catalog_objects[ $product->get_id() ] : null; |
| 1329 | |
| 1330 | // if no Square object was found |
| 1331 | if ( ! $square_object ) { |
| 1332 | $record = array( |
| 1333 | 'type' => 'alert', |
| 1334 | 'product_id' => $product->get_id(), |
| 1335 | /* translators: Placeholder %s Product ID */ |
| 1336 | 'message' => sprintf( esc_html__( '%s does not exist in the Square catalog.', 'woocommerce-square' ), '<a href="' . esc_url( get_edit_post_link( $product->get_id() ) ) . '">' . $product->get_formatted_name() . '</a>' ), |
| 1337 | ); |
| 1338 | |
| 1339 | // if enabled, hide the product from the catalog |
| 1340 | if ( wc_square()->get_settings_handler()->is_system_of_record_square() && wc_square()->get_settings_handler()->hide_missing_square_products() ) { |
| 1341 | try { |
| 1342 | $product->set_catalog_visibility( 'hidden' ); |
| 1343 | $product->save(); |
| 1344 | |
| 1345 | $record['product_hidden'] = true; |
| 1346 | } catch ( \Exception $e ) { |
| 1347 | $record['message'] .= esc_html__( 'This product failed to be hidden.', 'woocommerce-square' ); |
| 1348 | } |
| 1349 | } |
| 1350 | |
| 1351 | Records::set_record( $record ); |
| 1352 | continue; |
| 1353 | } |
| 1354 | |
| 1355 | foreach ( $square_object->getItemData()->getVariations() as $variation ) { |
| 1356 | $pull_inventory_variation_ids[] = $variation->getId(); |
| 1357 | } |
| 1358 | |
| 1359 | Product::update_from_square( $product, $square_object->getItemData(), false ); |
| 1360 | |
| 1361 | $image_id = Product::get_catalog_item_thumbnail_id( $square_object ); |
| 1362 | Product::update_image_from_square( $product, $image_id ); |
| 1363 | |
| 1364 | } catch ( \Exception $exception ) { |
| 1365 | |
| 1366 | Records::set_record( |
| 1367 | array( |
| 1368 | 'type' => 'alert', |
| 1369 | 'product_id' => $product->get_id(), |
| 1370 | /* translators: Placeholder %1$s Product Name, %2$s Exception message */ |
| 1371 | 'message' => sprintf( esc_html__( 'Could not sync %1$s data from Square. %2$s.', 'woocommerce-square' ), '<a href="' . esc_url( get_edit_post_link( $product->get_id() ) ) . '">' . $product->get_formatted_name() . '</a>', $exception->getMessage() ), |
| 1372 | ) |
| 1373 | ); |
| 1374 | |
| 1375 | } |
| 1376 | |
| 1377 | $processed_product_ids[] = $product->get_id(); |
| 1378 | } |
| 1379 | |
| 1380 | $this->set_attr( 'catalog_objects_search_response_data', null ); |
| 1381 | |
| 1382 | $this->set_attr( 'pull_inventory_variation_ids', $pull_inventory_variation_ids ); |
| 1383 | |
| 1384 | $this->set_attr( 'processed_product_ids', $processed_product_ids ); |
| 1385 | } |
| 1386 | |
| 1387 | |
| 1388 | /** |
| 1389 | * Pulls the latest inventory counts for the variation IDs in `pull_inventory_variation_ids`. |
| 1390 | * |
| 1391 | * @since 2.0.2 |
| 1392 | * |
| 1393 | * @throws \Exception |
| 1394 | */ |
| 1395 | protected function pull_inventory() { |
| 1396 | |
| 1397 | $processed_ids = $this->get_attr( 'processed_square_variation_ids', array() ); |
| 1398 | |
| 1399 | $in_progress = wp_parse_args( |
| 1400 | $this->get_attr( |
| 1401 | 'in_progress_pull_inventory', |
| 1402 | array() |
| 1403 | ), |
| 1404 | array( |
| 1405 | 'response_data' => null, |
| 1406 | 'processed_variation_ids' => array(), |
| 1407 | ) |
| 1408 | ); |
| 1409 | |
| 1410 | $response_data = null; |
| 1411 | |
| 1412 | // if a response was never cleared, we likely had a timeout |
| 1413 | if ( null !== $in_progress['response_data'] ) { |
| 1414 | $response_data = ApiHelper::deserialize( $in_progress['response_data'], new BatchRetrieveInventoryCountsResponse() ); |
| 1415 | } |
| 1416 | |
| 1417 | // if the saved response was somehow corrupted, start over |
| 1418 | if ( ! $response_data instanceof BatchRetrieveInventoryCountsResponse ) { |
| 1419 | |
| 1420 | $square_variation_ids = $this->get_attr( 'pull_inventory_variation_ids', array() ); |
| 1421 | |
| 1422 | // remove IDs that have already been processed |
| 1423 | $square_variation_ids = array_diff( $square_variation_ids, $processed_ids ); |
| 1424 | |
| 1425 | if ( empty( $square_variation_ids ) ) { |
| 1426 | |
| 1427 | $this->complete_step( 'pull_inventory' ); |
| 1428 | return; |
| 1429 | } |
| 1430 | |
| 1431 | if ( count( $square_variation_ids ) > self::BATCH_INVENTORY_COUNTS_LIMIT ) { |
| 1432 | |
| 1433 | $variation_ids_batch = array_slice( $square_variation_ids, 0, self::BATCH_INVENTORY_COUNTS_LIMIT ); |
| 1434 | |
| 1435 | $this->set_attr( 'pull_inventory_variation_ids', array_diff( $square_variation_ids, $variation_ids_batch ) ); |
| 1436 | |
| 1437 | $square_variation_ids = $variation_ids_batch; |
| 1438 | } |
| 1439 | |
| 1440 | $cursor = ''; |
| 1441 | $response_counts = array(); |
| 1442 | $location_ids = array( wc_square()->get_settings_handler()->get_location_id() ); |
| 1443 | $catalog_object_ids = array_values( $square_variation_ids ); |
| 1444 | |
| 1445 | // Repeat fetching objects using the cursor when the results are paginated. |
| 1446 | do { |
| 1447 | $response = wc_square()->get_api()->batch_retrieve_inventory_counts( |
| 1448 | array( |
| 1449 | 'catalog_object_ids' => $catalog_object_ids, |
| 1450 | 'location_ids' => $location_ids, |
| 1451 | 'cursor' => $cursor, |
| 1452 | ) |
| 1453 | ); |
| 1454 | |
| 1455 | if ( ! $response->get_data() instanceof BatchRetrieveInventoryCountsResponse ) { |
| 1456 | throw new \Exception( 'Response data missing or invalid' ); |
| 1457 | } |
| 1458 | |
| 1459 | $response_data = $response->get_data(); |
| 1460 | |
| 1461 | // if no counts were returned, there's nothing to process |
| 1462 | if ( ! is_array( $response_data->getCounts() ) ) { |
| 1463 | |
| 1464 | $this->set_attr( 'processed_square_variation_ids', array_merge( $processed_ids, $square_variation_ids ) ); |
| 1465 | return; |
| 1466 | } |
| 1467 | |
| 1468 | $in_progress['response_data'] = wp_json_encode( $response_data, JSON_PRETTY_PRINT ); |
| 1469 | |
| 1470 | // Store the response counts to be processed later. |
| 1471 | $response_counts = array_merge( $response_counts, $response_data->getCounts() ); |
| 1472 | $cursor = $response->get_data()->getCursor(); |
| 1473 | |
| 1474 | } while ( ! empty( $cursor ) ); |
| 1475 | } |
| 1476 | |
| 1477 | $catalog_objects_inventory_stats = array(); |
| 1478 | |
| 1479 | foreach ( $response_counts as $count ) { |
| 1480 | // If catalog stats array already contains the catalog object marked as IN_STOCK, then continue. |
| 1481 | if ( isset( $catalog_objects_inventory_stats[ $count->getCatalogObjectId() ] ) && $catalog_objects_inventory_stats[ $count->getCatalogObjectId() ]['IN_STOCK'] ) { |
| 1482 | continue; |
| 1483 | // Else if the catalog object is IN_STOCK, then mark IN_STOCK as true and set the quantity for later use. |
| 1484 | } elseif ( 'IN_STOCK' === $count->getState() ) { |
| 1485 | $catalog_objects_inventory_stats[ $count->getCatalogObjectId() ] = array( |
| 1486 | 'IN_STOCK' => true, |
| 1487 | 'quantity' => $count->getQuantity(), |
| 1488 | ); |
| 1489 | // Else if the catalog object doesn't have an IN_STOCK status, then mark IN_STOCK as false and set the quantity as 0 for later use. |
| 1490 | } else { |
| 1491 | $catalog_objects_inventory_stats[ $count->getCatalogObjectId() ] = array( |
| 1492 | 'IN_STOCK' => false, |
| 1493 | 'quantity' => 0, |
| 1494 | ); |
| 1495 | } |
| 1496 | } |
| 1497 | |
| 1498 | $catalog_objects_tracking_stats = Helper::get_catalog_objects_tracking_stats( $catalog_object_ids ); |
| 1499 | |
| 1500 | foreach ( $catalog_objects_tracking_stats as $catalog_object_id => $is_tracking_inventory ) { |
| 1501 | |
| 1502 | if ( in_array( $catalog_object_id, $in_progress['processed_variation_ids'], false ) ) { // phpcs:disable WordPress.PHP.StrictInArray.FoundNonStrictFalse |
| 1503 | continue; |
| 1504 | } |
| 1505 | |
| 1506 | $product = Product::get_product_by_square_variation_id( $catalog_object_id ); |
| 1507 | |
| 1508 | if ( $product instanceof \WC_Product ) { |
| 1509 | |
| 1510 | /* If catalog object is tracked and has a quantity > 0 set in Square. */ |
| 1511 | if ( $is_tracking_inventory && isset( $catalog_objects_inventory_stats[ $catalog_object_id ] ) ) { |
| 1512 | $product->set_stock_quantity( (float) $catalog_objects_inventory_stats[ $catalog_object_id ]['quantity'] ); |
| 1513 | $product->set_manage_stock( true ); |
| 1514 | |
| 1515 | /* If the catalog object is tracked but the quantity in Square is set to 0. */ |
| 1516 | } elseif ( $is_tracking_inventory ) { |
| 1517 | $product->set_stock_quantity( 0 ); |
| 1518 | $product->set_manage_stock( true ); |
| 1519 | |
| 1520 | /* If the catalog object is not tracked in Square at all. */ |
| 1521 | } else { |
| 1522 | $product->set_stock_status( 'instock' ); |
| 1523 | $product->set_manage_stock( false ); |
| 1524 | } |
| 1525 | |
| 1526 | $product->save(); |
| 1527 | |
| 1528 | $in_progress['processed_variation_ids'][] = $catalog_object_id; |
| 1529 | } |
| 1530 | |
| 1531 | $this->set_attr( 'in_progress_pull_inventory', $in_progress ); |
| 1532 | } |
| 1533 | |
| 1534 | $this->set_attr( 'processed_square_variation_ids', array_merge( $processed_ids, $in_progress['processed_variation_ids'] ) ); |
| 1535 | |
| 1536 | // clear any in-progress data |
| 1537 | $this->set_attr( 'in_progress_pull_inventory', array() ); |
| 1538 | } |
| 1539 | |
| 1540 | /** |
| 1541 | * Marks a set of products as failed to sync. |
| 1542 | * |
| 1543 | * @since 2.0.0 |
| 1544 | * |
| 1545 | * @param \WC_Product[]|int[] $products products to mark as failed |
| 1546 | */ |
| 1547 | protected function mark_failed_products( $products = array() ) { |
| 1548 | |
| 1549 | foreach ( $products as $product ) { |
| 1550 | |
| 1551 | $product = wc_get_product( $product ); |
| 1552 | |
| 1553 | if ( ! $product instanceof \WC_Product ) { |
| 1554 | continue; |
| 1555 | } |
| 1556 | |
| 1557 | $record_data = array( |
| 1558 | 'type' => 'alert', |
| 1559 | 'product_id' => $product->get_id(), |
| 1560 | ); |
| 1561 | |
| 1562 | // optionally hide unmatched products from catalog |
| 1563 | if ( wc_square()->get_settings_handler()->is_system_of_record_square() && wc_square()->get_settings_handler()->hide_missing_square_products() ) { |
| 1564 | |
| 1565 | try { |
| 1566 | |
| 1567 | $product->set_catalog_visibility( 'hidden' ); |
| 1568 | $product->save(); |
| 1569 | |
| 1570 | $record_data['product_hidden'] = true; |
| 1571 | |
| 1572 | } catch ( \Exception $e ) { |
| 1573 | /* translators: Placeholder %1$s Product Name, %2$s Exception message */ |
| 1574 | $record['message'] = sprintf( esc_html__( '%1$s was deleted in Square but could not be hidden in WooCommerce. %2$s.', 'woocommerce-square' ), '<a href="' . esc_url( get_edit_post_link( $product->get_id() ) ) . '">' . $product->get_formatted_name() . '</a>', $e->getMessage() ); |
| 1575 | } |
| 1576 | } |
| 1577 | |
| 1578 | Records::set_record( $record_data ); |
| 1579 | } |
| 1580 | } |
| 1581 | |
| 1582 | |
| 1583 | /** |
| 1584 | * Gets a list of unique category IDs used by a group of product IDs. |
| 1585 | * |
| 1586 | * @since 2.0.0 |
| 1587 | * |
| 1588 | * @param int[] $product_ids array of product IDs. |
| 1589 | * @return int[] |
| 1590 | */ |
| 1591 | protected function get_shared_category_ids( $product_ids ) { |
| 1592 | |
| 1593 | if ( ! empty( $product_ids ) ) { |
| 1594 | $category_ids = get_terms( |
| 1595 | array( |
| 1596 | 'taxonomy' => 'product_cat', |
| 1597 | 'fields' => 'ids', |
| 1598 | 'object_ids' => $product_ids, |
| 1599 | ) |
| 1600 | ); |
| 1601 | } |
| 1602 | |
| 1603 | return ! empty( $category_ids ) && ! is_wp_error( $category_ids ) ? $category_ids : array(); |
| 1604 | } |
| 1605 | |
| 1606 | |
| 1607 | /** |
| 1608 | * Assigns the next steps needed for this sync job. |
| 1609 | * |
| 1610 | * @since 2.0.0 |
| 1611 | */ |
| 1612 | protected function assign_next_steps() { |
| 1613 | |
| 1614 | $next_steps = array(); |
| 1615 | |
| 1616 | if ( $this->is_system_of_record_woocommerce() ) { |
| 1617 | |
| 1618 | if ( 'delete' === $this->get_attr( 'action' ) ) { |
| 1619 | |
| 1620 | $next_steps = array( |
| 1621 | 'validate_products', |
| 1622 | 'update_matched_products', |
| 1623 | 'search_matched_products', |
| 1624 | ); |
| 1625 | |
| 1626 | } else { |
| 1627 | |
| 1628 | $next_steps = array( |
| 1629 | 'validate_products', |
| 1630 | 'extract_category_ids', |
| 1631 | 'refresh_category_mappings', |
| 1632 | 'query_unmapped_categories', |
| 1633 | 'upsert_categories', |
| 1634 | 'update_matched_products', |
| 1635 | 'search_matched_products', |
| 1636 | 'upsert_new_products', |
| 1637 | ); |
| 1638 | |
| 1639 | // only handle product inventory if enabled |
| 1640 | if ( wc_square()->get_settings_handler()->is_inventory_sync_enabled() ) { |
| 1641 | $next_steps[] = 'push_inventory'; |
| 1642 | $next_steps[] = 'pull_inventory'; |
| 1643 | } |
| 1644 | } |
| 1645 | } elseif ( $this->is_system_of_record_square() ) { |
| 1646 | |
| 1647 | $next_steps = array( |
| 1648 | 'validate_products', |
| 1649 | 'square_sor_sync', |
| 1650 | ); |
| 1651 | |
| 1652 | // only pull product inventory if enabled |
| 1653 | if ( wc_square()->get_settings_handler()->is_inventory_sync_enabled() ) { |
| 1654 | $next_steps[] = 'pull_inventory'; |
| 1655 | } |
| 1656 | } |
| 1657 | |
| 1658 | $this->set_attr( 'next_steps', $next_steps ); |
| 1659 | } |
| 1660 | |
| 1661 | |
| 1662 | /** |
| 1663 | * Gets the maximum number of objects to retrieve in a single sync job. |
| 1664 | * |
| 1665 | * @since 2.0.0 |
| 1666 | * |
| 1667 | * @return int |
| 1668 | */ |
| 1669 | protected function get_max_objects_to_retrieve() { |
| 1670 | $max = $this->get_attr( 'max_objects_to_retrieve', 100 ); |
| 1671 | |
| 1672 | /** |
| 1673 | * Filters the maximum number of objects to retrieve in a single sync job. |
| 1674 | * |
| 1675 | * @since 2.0.0 |
| 1676 | * |
| 1677 | * $param int $max |
| 1678 | */ |
| 1679 | return max( 1, (int) apply_filters( 'wc_square_sync_max_objects_to_retrieve', $max ) ); |
| 1680 | } |
| 1681 | |
| 1682 | |
| 1683 | /** |
| 1684 | * Gets the maximum number of objects per batch in a single sync job. |
| 1685 | * |
| 1686 | * @deprecated 3.2 |
| 1687 | * @since 2.0.0 |
| 1688 | * |
| 1689 | * @return int |
| 1690 | */ |
| 1691 | protected function get_max_objects_per_batch() { |
| 1692 | |
| 1693 | wc_deprecated_function( __METHOD__, '3.2' ); |
| 1694 | |
| 1695 | $max = $this->get_attr( 'max_objects_per_batch', 1000 ); |
| 1696 | |
| 1697 | /** |
| 1698 | * Filters the maximum number of objects per batch in a single sync job. |
| 1699 | * |
| 1700 | * @since 2.0.0 |
| 1701 | * |
| 1702 | * $param int $max |
| 1703 | */ |
| 1704 | return max( 10, (int) apply_filters( 'wc_square_sync_max_objects_per_batch', $max ) ); |
| 1705 | } |
| 1706 | |
| 1707 | |
| 1708 | /** |
| 1709 | * Gets the maximum number of objects per batch upsert in a single request. |
| 1710 | * |
| 1711 | * @since 2.0.0 |
| 1712 | * |
| 1713 | * @return int |
| 1714 | */ |
| 1715 | protected function get_max_objects_per_upsert() { |
| 1716 | |
| 1717 | $max = $this->get_attr( 'max_objects_per_upsert', 500 ); |
| 1718 | |
| 1719 | /** |
| 1720 | * Filters the maximum number of objects per upsert in a single request. |
| 1721 | * |
| 1722 | * @since 2.0.0 |
| 1723 | * |
| 1724 | * $param int $max |
| 1725 | */ |
| 1726 | return max( 1, (int) apply_filters( 'wc_square_sync_max_objects_per_upsert', $max ) ); |
| 1727 | } |
| 1728 | |
| 1729 | |
| 1730 | /** |
| 1731 | * Gets the maximum number of objects allowed in a single sync job. |
| 1732 | * |
| 1733 | * @since 2.0.0 |
| 1734 | * |
| 1735 | * @return int |
| 1736 | */ |
| 1737 | protected function get_max_objects_total() { |
| 1738 | |
| 1739 | $max = $this->get_attr( 'max_objects_total', self::BATCH_UPSERT_OBJECT_LIMIT ); |
| 1740 | |
| 1741 | /** |
| 1742 | * Filters the maximum number of objects allowed in a single sync job. |
| 1743 | * |
| 1744 | * @since 2.0.0 |
| 1745 | * |
| 1746 | * $param int $max |
| 1747 | */ |
| 1748 | return max( 1, (int) apply_filters( 'wc_square_sync_max_objects_total', $max ) ); |
| 1749 | } |
| 1750 | } |
| 1751 |