PluginProbe
WooCommerce Square / 4.5.0
WooCommerce Square v4.5.0
5.5.0 5.4.3 5.4.2 5.4.1 5.4.0 trunk 1.0.25 1.0.26 1.0.27 1.0.28 1.0.29 1.0.30 1.0.31 1.0.32 1.0.33 1.0.34 1.0.35 1.0.36 1.0.37 1.0.38 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 All 132 releases
woocommerce-square / includes / Sync / Product_Import.php
Product_Import.php
1,390 lines 43.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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\SearchCatalogObjectsResponse;
27 use WooCommerce\Square\Handlers\Category;
28 use WooCommerce\Square\Handlers\Product;
29 use WooCommerce\Square\Utilities\Money_Utility;
30
31 defined( 'ABSPATH' ) || exit;
32
33 /**
34 * Class to represent a synchronization job to import products from Square.
35 *
36 * @since 2.0.0
37 */
38 class Product_Import extends Stepped_Job {
39
40
41 protected function assign_next_steps() {
42
43 $this->set_attr(
44 'next_steps',
45 array(
46 'import_products',
47 'import_inventory',
48 )
49 );
50 }
51
52
53 /**
54 * Gets the limit for how many items to import per request.
55 *
56 * Square has a hard maximum for this at 1000, but 100 seems to be a sane
57 * default to allow for creating products without timing out.
58 *
59 * @since 2.0.0
60 *
61 * @return int
62 */
63 protected function get_import_api_limit() {
64
65 /**
66 * Filters the number of items to import from the Square API per request.
67 *
68 * @since 2.0.0
69 *
70 * @param int limit
71 */
72 $limit = (int) apply_filters( 'wc_square_import_api_limit', 100 );
73
74 return max( 1, min( 1000, $limit ) );
75 }
76
77
78 /**
79 * Performs a product import.
80 *
81 * @since 2.0.0
82 *
83 * @throws \Exception
84 */
85 protected function import_products() {
86
87 $cursor = $this->get_attr( 'fetch_products_cursor' );
88 $imported_product_ids = $this->get_attr( 'processed_product_ids', array() );
89 $updated_product_ids = $this->get_attr( 'updated_product_ids', array() );
90 $skipped_products = $this->get_attr( 'skipped_products', array() );
91 $update_products_during_import = $this->get_attr( 'update_products_during_import', false );
92
93 $response = wc_square()->get_api()->search_catalog_objects(
94 array(
95 'cursor' => $cursor,
96 'object_types' => array( 'ITEM' ),
97 'include_related_objects' => true,
98 'limit' => $this->get_import_api_limit(),
99 )
100 );
101
102 if ( ! $response->get_data() instanceof SearchCatalogObjectsResponse ) {
103 throw new \Exception( 'API response data is invalid' );
104 }
105
106 $related_objects = $response->get_data()->getRelatedObjects();
107 $categories = array();
108
109 if ( $related_objects && is_array( $related_objects ) ) {
110 foreach ( $related_objects as $related_object ) {
111
112 if ( 'CATEGORY' === $related_object->getType() ) {
113 $categories[ $related_object->getId() ] = $related_object;
114 }
115 }
116 }
117
118 $catalog_objects = $response->get_data()->getObjects() ? $response->get_data()->getObjects() : array();
119
120 foreach ( $catalog_objects as $catalog_object_index => $catalog_object ) {
121
122 // validate permissions
123 if ( ! current_user_can( 'publish_products' ) ) {
124 $this->record_error( 'You do not have permission to create products' );
125 break; // use a break so we don't continue trying to import products without permissions
126 }
127
128 // validate Square Catalog object (API data)
129 if ( ! $catalog_object instanceof \Square\Models\CatalogObject || ! $catalog_object->getItemData() instanceof \Square\Models\CatalogItem ) {
130 $this->record_error( 'Invalid data' );
131 continue;
132 }
133
134 $item_id = $catalog_object->getId();
135
136 // Ignore items that are available at all locations, but absent at ours.
137 if ( is_array( $catalog_object->getAbsentAtLocationIds() ) && in_array( wc_square()->get_settings_handler()->get_location_id(), $catalog_object->getAbsentAtLocationIds(), true ) ) {
138 $skipped_products[ $item_id ] = null;
139 continue;
140 }
141
142 // Ignore items that are not available at our location.
143 if ( ! $catalog_object->getPresentAtAllLocations() && ( ! is_array( $catalog_object->getPresentAtLocationIds() ) || ! in_array( wc_square()->get_settings_handler()->get_location_id(), $catalog_object->getPresentAtLocationIds(), true ) ) ) {
144 $skipped_products[ $item_id ] = null;
145 continue;
146 }
147
148 $product_id = (int) Product::get_product_id_by_square_id( $item_id );
149 $product = ! empty( $product_id ) ? wc_get_product( $product_id ) : null;
150
151 if ( in_array( $product_id, array_merge( $imported_product_ids, $updated_product_ids ), true ) ) {
152 continue; // don't import/update the same product twice.
153
154 } elseif ( $product_id && ! $update_products_during_import ) {
155 $skipped_products[ $item_id ] = null;
156 continue;
157
158 } elseif ( $product_id && ! $product ) {
159 $this->record_error( 'Product not found', $catalog_object, 'update' );
160 continue;
161 }
162
163 // import or update categories related to the products that are being imported
164 $catelog_category_id = $catalog_object->getItemData()->getCategoryId();
165
166 if ( $catelog_category_id && isset( $categories[ $catelog_category_id ] ) ) {
167 Category::import_or_update( $categories[ $catelog_category_id ] );
168 unset( $categories[ $catelog_category_id ] ); // don't import/update the same category multiple times per batch
169 }
170
171 $data = $this->extract_product_data( $catalog_object, $product );
172
173 if ( ! $data ) {
174 $skipped_products[ $item_id ] = null;
175 continue;
176 }
177
178 /**
179 * Filters the data that is used to create update a WooCommerce product during import.
180 *
181 * @since 2.0.0
182 *
183 * @param array $data product data
184 * @param \Square\Models\CatalogObject $catalog_object the catalog object from the Square API
185 * @param Product_Import $this import class instance
186 */
187 $data = apply_filters( 'woocommerce_square_create_product_data', $data, $catalog_object, $this );
188
189 // set default type
190 $data['type'] = ! empty( $data['type'] ) ? $data['type'] : 'simple';
191
192 // if an item matches an existing product, update the product using data from Square
193 if ( $product ) {
194 $product_id = $this->update_product( $product, $data );
195
196 if ( $product_id ) {
197 $updated_product_ids[] = $product_id;
198 }
199 } elseif ( $this->item_variation_has_matching_sku( $data ) ) {
200 // look in variation SKUs for a match - if so, skip the parent item, a normal sync should link it automatically
201 continue;
202 } else {
203 $product_id = $this->import_product( $data );
204
205 if ( $product_id ) {
206 Product::set_synced_with_square( $product_id );
207 $imported_product_ids[] = $product_id;
208 }
209 }
210 }
211
212 wc_square()->log( 'Imported New Products Count: ' . count( $imported_product_ids ) );
213 wc_square()->log( 'Updated Products Count: ' . count( $updated_product_ids ) );
214
215 $cursor = $response->get_data()->getCursor();
216 $this->set_attr( 'fetch_products_cursor', $cursor );
217
218 $this->set_attr( 'skipped_products', $skipped_products );
219 $this->set_attr( 'updated_product_ids', $updated_product_ids );
220 $this->set_attr( 'processed_product_ids', $imported_product_ids );
221
222 if ( ! $cursor ) {
223 $this->complete_step( 'import_products' );
224 }
225 }
226
227
228 /**
229 * Imports inventory counts for all the tracked Square products.
230 *
231 * @since 2.0.0
232 *
233 * @throws \Exception
234 */
235 protected function import_inventory() {
236
237 $search_result = wc_square()->get_api()->search_catalog_objects(
238 array(
239 'object_types' => array( 'ITEM_VARIATION' ),
240 'limit' => 100,
241 'cursor' => $this->get_attr( 'import_inventory_cursor', null ),
242 )
243 );
244
245 if ( ! $search_result->get_data() instanceof SearchCatalogObjectsResponse ) {
246 throw new \Exception( 'API response data is invalid' );
247 }
248
249 $count = $this->get_attr( 'import_inventory_count', 0 );
250 $cursor = $search_result->get_data()->getCursor();
251 $objects = $search_result->get_data()->getObjects() ? $search_result->get_data()->getObjects() : array();
252 $variation_ids = array_map(
253 static function( \Square\Models\CatalogObject $catalog_object ) {
254 return $catalog_object->getId();
255 },
256 $objects
257 );
258
259 $catalog_objects_hash = Helper::get_catalog_inventory_tracking( $objects );
260
261 $result = wc_square()->get_api()->batch_retrieve_inventory_counts(
262 array(
263 'catalog_object_ids' => $variation_ids,
264 'location_ids' => array( wc_square()->get_settings_handler()->get_location_id() ),
265 'states' => array( 'IN_STOCK' ),
266 )
267 );
268
269 /* We maintain this hash because batch_retrieve_inventory_counts doesn't return any catalog objects if they
270 * are not tracked.
271 *
272 * This is why, we instead iterate on $objects in the next steps and use $inventory_hash to set inventory.
273 */
274 $inventory_hash = array();
275
276 foreach ( $result->get_counts() as $inventory_count ) {
277 $inventory_hash[ $inventory_count->getCatalogObjectId() ] = $inventory_count->getQuantity();
278 }
279
280 foreach ( $objects as $catalog_object ) {
281
282 // all inventory should be tied to a variation, but check here just in case
283 if ( 'ITEM_VARIATION' === $catalog_object->getType() ) {
284
285 $product = Product::get_product_by_square_variation_id( $catalog_object->getId() );
286
287 if ( $product && $product instanceof \WC_Product ) {
288 $is_tracking_inventory = isset( $catalog_objects_hash[ $catalog_object->getId() ] ) ?
289 $catalog_objects_hash[ $catalog_object->getId() ] :
290 false;
291
292 /* If catalog object is tracked and has a quantity > 0 set in Square. */
293 if ( $is_tracking_inventory && isset( $inventory_hash[ $catalog_object->getId() ] ) ) {
294 $product->set_stock_quantity( (float) $inventory_hash[ $catalog_object->getId() ] );
295 $product->set_manage_stock( true );
296
297 /* If the catalog object is tracked but the quantity in Square is set to 0. */
298 } elseif ( $is_tracking_inventory ) {
299 $product->set_stock_quantity( 0 );
300 $product->set_manage_stock( true );
301
302 /* If the catalog object is not tracked in Square at all. */
303 } else {
304 $product->set_stock_status( 'instock' );
305 $product->set_manage_stock( false );
306 }
307
308 $product->save();
309 }
310 }
311 }
312
313 $this->set_attr( 'import_inventory_count', $count + count( $objects ) );
314 $this->set_attr( 'import_inventory_cursor', $cursor );
315
316 if ( ! $cursor ) {
317
318 $this->complete_step( 'import_inventory' );
319 }
320 }
321
322
323 /**
324 * Determines whether any catalog item variation is missing a SKU
325 *
326 * @since 2.2.0
327 *
328 * @param \Square\Models\CatalogObject $catalog_object the catalog object
329 * @return bool
330 */
331 private function item_variation_has_missing_sku( $catalog_object ) {
332 $missing_sku = true;
333
334 foreach ( $catalog_object->getItemData()->getVariations() as $variation ) {
335 if ( in_array( trim( $variation->getItemVariationData()->getSku() ), array( '', null ), true ) ) { // can't use empty() because a valid SKU could be '0' which returns true
336 $missing_sku = true;
337 break;
338 }
339
340 $missing_sku = false;
341 }
342
343 return $missing_sku;
344 }
345
346
347 /**
348 * Determines whether a SKU within a catalog item is found in WooCommerce.
349 *
350 * @since 2.0.0
351 *
352 * @param array $data the catalog object data
353 * @return bool
354 */
355 private function item_variation_has_matching_sku( $data ) {
356
357 if ( 'simple' === $data['type'] ) {
358 return (bool) wc_get_product_id_by_sku( $data['sku'] );
359 } else {
360 foreach ( $data['variations'] as $variation ) {
361 $variation_id = wc_get_product_id_by_sku( $variation['sku'] );
362
363 if ( $variation_id ) {
364 // found variation with matching SKU, check if parent still exists and return that result
365 return (bool) Product::get_parent_product_id_by_variation_id( $variation_id );
366 }
367 }
368 }
369 return false;
370 }
371
372
373 /**
374 * Creates a product from catalog object data.
375 *
376 * @since 2.0.0
377 *
378 * @param array $data the catalog object data
379 * @return int|null
380 */
381 private function import_product( $data ) {
382 try {
383 $product_id = $this->create_product_from_square_data( $data );
384
385 // save product meta fields
386 $this->save_product_meta( $product_id, $data );
387
388 // save the image, if included
389 if ( ! empty( $data['image_id'] ) ) {
390 Product::update_image_from_square( $product_id, $data['image_id'], true );
391 }
392
393 // save variations
394 if ( 'variable' === $data['type'] && is_array( $data['variations'] ) && isset( $data['type'], $data['variations'] ) ) {
395
396 $this->save_variations( $product_id, $data );
397 }
398
399 /**
400 * Fired when a product is created from a square product import.
401 *
402 * @since 2.0.0
403 *
404 * @param int $product_id the product ID that was created
405 * @param array $data the data used to create the product
406 */
407 do_action( 'woocommerce_square_create_product', $product_id, $data );
408
409 // clear cache/transients
410 wc_delete_product_transients( $product_id );
411 } catch ( \Exception $e ) {
412 // remove the product when creation fails
413 if ( ! empty( $product_id ) ) {
414 $this->clear_product( $product_id );
415 }
416 $product_id = 0;
417
418 $this->record_error( $e->getMessage(), $data );
419 }
420
421 return $product_id;
422 }
423
424 /**
425 * Updates a product from catalog object data
426 *
427 * @since 2.2.0
428 *
429 * @param \WC_Product $product existing product in Woo that is being updated
430 * @param array $data the Square catalog object data
431 * @return int|null
432 */
433 private function update_product( $product, $data ) {
434 global $wpdb;
435 $product_id = $product->get_id();
436
437 try {
438 $wpdb->query( 'START TRANSACTION' );
439
440 // update an existing product from a simple to a variable product if it has at least two variations
441 if ( ! $product instanceof \WC_Product_Variable && 'variable' === $data['type'] ) {
442 $product_id = $this->update_simple_product_to_variable( $product_id, $data );
443 }
444
445 $product->set_name( $data['title'] );
446 $product->set_description( $data['description'] );
447 $product->save();
448
449 // save product meta fields
450 $this->save_product_meta( $product_id, $data );
451
452 // save the image, if included
453 Product::update_image_from_square( $product_id, $data['image_id'], true );
454
455 // save/update variations
456 if ( isset( $data['type'], $data['variations'] ) && 'variable' === $data['type'] && is_array( $data['variations'] ) ) {
457 $this->save_variations( $product_id, $data );
458 }
459
460 $wpdb->query( 'COMMIT' );
461
462 wc_delete_product_transients( $product_id );
463 } catch ( \Exception $e ) {
464 // undo any updated data when updating fails
465 $wpdb->query( 'ROLLBACK' );
466 $product_id = 0;
467
468 $this->record_error( $e->getMessage(), $data, 'update' );
469 }
470
471 return $product_id;
472 }
473
474 /**
475 * Convert an existing simple product to a variable when new variations are found.
476 * This function
477 *
478 * @since 2.2.0
479 *
480 * @param int $variation_id simple product ID being updated to a variation with new parent product
481 * @param array $data
482 * @return int|null
483 */
484 protected function update_simple_product_to_variable( $variation_id, $data = array() ) {
485 // create a new parent product
486 $parent_product_id = $this->create_product_from_square_data( $data );
487
488 // convert the simple product to a variation
489 wp_set_object_terms( $variation_id, 'variation', 'product_type' );
490 wp_update_post(
491 array(
492 'ID' => $variation_id,
493 'post_content' => '',
494 'post_author' => get_current_user_id(),
495 'post_parent' => $parent_product_id,
496 'post_type' => 'product_variation',
497 )
498 );
499
500 // remove simple product meta that doesn't exist or needs be updated on the variation
501 delete_post_meta( $variation_id, Product::SQUARE_ID_META_KEY );
502 delete_post_meta( $variation_id, Product::SQUARE_VERSION_META_KEY );
503 delete_post_meta( $variation_id, Product::SQUARE_IMAGE_ID_META_KEY );
504 delete_post_meta( $variation_id, '_visibility' );
505
506 // copy total sales from previous simple product over to new parent variable product
507 $total_sales = get_post_meta( $variation_id, 'total_sales', true );
508
509 if ( $total_sales ) {
510 update_post_meta( $parent_product_id, 'total_sales', $total_sales );
511 }
512
513 return $parent_product_id;
514 }
515
516 /**
517 * Extracts product data from a CatalogObject to an array of data.
518 *
519 * @since 2.0.0
520 *
521 * @param \Square\Models\CatalogObject $catalog_object the catalog object
522 * @param WC_Product|null $product
523 * @return array|null
524 * @throws \Exception
525 */
526 protected function extract_product_data( $catalog_object, $product = null ) {
527
528 $variations = $catalog_object->getItemData()->getVariations() ? $catalog_object->getItemData()->getVariations() : array();
529
530 // if there are no variations, something is wrong - every catalog item has at least one
531 if ( 0 >= count( $variations ) ) {
532 return null;
533 }
534
535 $category_id = Category::get_category_id_by_square_id( $catalog_object->getItemData()->getCategoryId() );
536
537 $data = array(
538 'title' => $catalog_object->getItemData()->getName(),
539 'type' => ( 1 === count( $variations ) && ! ( $product && $product instanceof \WC_Product_Variable ) ) ? 'simple' : 'variable',
540 'sku' => '', // make sure to reset SKU when simple product is updated to variable.
541 'description' => Product::get_catalog_item_description( $catalog_object->getItemData() ),
542 'image_id' => Product::get_catalog_item_thumbnail_id( $catalog_object ),
543 'categories' => array( $category_id ),
544 'square_meta' => array(
545 'item_id' => $catalog_object->getId(),
546 'item_version' => $catalog_object->getVersion(),
547 ),
548 );
549
550 // variable product
551 if ( 'variable' === $data['type'] ) {
552 $data['variations'] = array();
553
554 foreach ( $variations as $variation ) {
555
556 // sanity check for valid API data
557 if ( ! $variation instanceof \Square\Models\CatalogObject || ! $variation->getItemVariationData() instanceof \Square\Models\CatalogItemVariation ) {
558 continue;
559 }
560
561 // Ignore variations that are available at all locations, but absent at ours.
562 if ( is_array( $variation->getAbsentAtLocationIds() ) && in_array( wc_square()->get_settings_handler()->get_location_id(), $variation->getAbsentAtLocationIds(), true ) ) {
563 continue;
564 }
565
566 // Ignore variations that are not available at our location.
567 if ( ! $variation->getPresentAtAllLocations() && ( ! is_array( $variation->getPresentAtLocationIds() ) || ! in_array( wc_square()->get_settings_handler()->get_location_id(), $variation->getPresentAtLocationIds(), true ) ) ) {
568 continue;
569 }
570
571 try {
572 $data['variations'][] = $this->extract_square_item_variation_data( $variation );
573
574 } catch ( \Exception $exception ) {
575
576 // alert for failed variation imports
577 Records::set_record(
578 array(
579 'type' => 'alert',
580 'message' => sprintf(
581 /* translators: Placeholders: %1$s - Square item name, %2$s - Square item variation name, %3$s - failure reason */
582 __( 'Could not import "%1$s - %2$s" from Square. %3$s', 'woocommerce-square' ),
583 $catalog_object->getItemData()->getName(),
584 $variation->getItemVariationData()->getName(),
585 $exception->getMessage()
586 ),
587 )
588 );
589 }
590 }
591
592 if ( ! count( $data['variations'] ) ) {
593 return null;
594 }
595
596 $data['attributes'] = array(
597 array(
598 'name' => 'Attribute',
599 'slug' => 'attribute',
600 'position' => 0,
601 'visible' => true,
602 'variation' => true,
603 'options' => str_replace( '|', ' - ', wp_list_pluck( $data['variations'], 'name' ) ),
604 ),
605 );
606 } else { // simple product
607 try {
608
609 $variation = $this->extract_square_item_variation_data( $variations[0] );
610
611 $data['type'] = 'simple';
612 $data['sku'] = ! empty( $variation['sku'] ) ? $variation['sku'] : null;
613 $data['regular_price'] = ! empty( $variation['regular_price'] ) ? $variation['regular_price'] : null;
614 $data['stock_quantity'] = ! empty( $variation['stock_quantity'] ) ? $variation['stock_quantity'] : null;
615 $data['managing_stock'] = ! empty( $variation['managing_stock'] ) ? $variation['managing_stock'] : null;
616
617 $data['square_meta']['item_variation_id'] = ! empty( $variation['square_meta']['item_variation_id'] ) ? $variation['square_meta']['item_variation_id'] : null;
618 $data['square_meta']['item_variation_version'] = ! empty( $variation['square_meta']['item_variation_version'] ) ? $variation['square_meta']['item_variation_version'] : null;
619
620 } catch ( \Exception $exception ) {
621
622 Records::set_record(
623 array(
624 'type' => 'alert',
625 'message' => sprintf(
626 /* translators: Placeholders: %1$s - Square item name, %2$s - failure reason */
627 __( 'Could not import "%1$s" from Square. %2$s', 'woocommerce-square' ),
628 $catalog_object->getItemData()->getName(),
629 $exception->getMessage()
630 ),
631 )
632 );
633
634 return null;
635 }
636 }
637
638 return $data;
639 }
640
641 /**
642 * Extracts data from a CatalogItemVariation for insertion into a WC product.
643 *
644 * @since 2.0.0
645 *
646 * @param \Square\Models\CatalogObject $variation the variation object
647 * @return array
648 * @throws \Exception
649 */
650 protected function extract_square_item_variation_data( $variation ) {
651
652 $variation_data = $variation->getItemVariationData();
653
654 if ( 'VARIABLE_PRICING' === $variation_data->getPricingType() ) {
655 throw new \Exception( __( 'Items with variable pricing cannot be imported.', 'woocommerce-square' ) );
656 }
657
658 if ( in_array( $variation_data->getSku(), array( '', null ), true ) ) {
659 throw new \Exception( __( 'Variations with missing SKUs cannot be imported.', 'woocommerce-square' ) );
660 }
661
662 $data = array(
663 'name' => $variation_data->getName(),
664 'sku' => $variation_data->getSku(),
665 'regular_price' => $variation_data->getPriceMoney() && $variation_data->getPriceMoney()->getAmount() ? Money_Utility::cents_to_float( $variation->getItemVariationData()->getPriceMoney()->getAmount() ) : null,
666 'stock_quantity' => null,
667 'managing_stock' => true,
668 'square_meta' => array(
669 'item_variation_id' => $variation->getId(),
670 'item_variation_version' => $variation->getVersion(),
671 ),
672 'attributes' => array(
673 array(
674 'name' => 'Attribute',
675 'is_variation' => true,
676 'option' => str_replace( '|', ' - ', $variation_data->getName() ),
677 ),
678 ),
679 );
680
681 return $data;
682 }
683
684
685 protected function save_product_images( $product_id, $images ) {}
686
687
688 protected function upload_product_image( $src ) {}
689
690
691 protected function set_product_image_as_attachment( $upload, $product_id ) {}
692
693
694 /**
695 * Saves product meta data for a given product.
696 *
697 * @since 2.0.0
698 *
699 * @param int $product_id the product ID
700 * @param array $data the product data
701 * @return bool
702 * @throws \Exception
703 */
704 protected function save_product_meta( $product_id, $data ) {
705
706 // product type
707 $product_type = null;
708
709 if ( isset( $data['type'] ) ) {
710
711 $product_type = wc_clean( $data['type'] );
712
713 wp_set_object_terms( $product_id, $product_type, 'product_type' );
714 } else {
715 $_product_type = get_the_terms( $product_id, 'product_type' );
716
717 if ( is_array( $_product_type ) ) {
718
719 $_product_type = current( $_product_type );
720 $product_type = $_product_type->slug;
721 }
722 }
723
724 // default total sales
725 add_post_meta( $product_id, 'total_sales', '0', true );
726
727 // catalog visibility
728 update_post_meta( $product_id, '_visibility', ! empty( $data['catalog_visibility'] ) ? wc_clean( $data['catalog_visibility'] ) : 'visible' );
729
730 // sku
731 if ( isset( $data['sku'] ) ) {
732
733 $sku = get_post_meta( $product_id, '_sku', true );
734 $new_sku = wc_clean( $data['sku'] );
735
736 if ( '' === $new_sku ) {
737
738 update_post_meta( $product_id, '_sku', '' );
739
740 } elseif ( $new_sku !== $sku ) {
741
742 if ( ! empty( $new_sku ) ) {
743
744 $unique_sku = wc_product_has_unique_sku( $product_id, $new_sku );
745
746 if ( $unique_sku ) {
747
748 update_post_meta( $product_id, '_sku', $new_sku );
749
750 } else {
751
752 throw new \Exception( __( 'The SKU already exists on another product', 'woocommerce-square' ) );
753 }
754 } else {
755 update_post_meta( $product_id, '_sku', '' );
756 }
757 }
758 }
759
760 // attributes
761 if ( isset( $data['attributes'] ) ) {
762
763 $attributes = array();
764
765 foreach ( $data['attributes'] as $attribute ) {
766
767 $is_taxonomy = 0;
768 $taxonomy = 0;
769
770 if ( ! isset( $attribute['name'] ) ) {
771 continue;
772 }
773
774 $attribute_slug = sanitize_title( $attribute['name'] );
775
776 if ( isset( $attribute['slug'] ) ) {
777
778 $taxonomy = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] );
779 $attribute_slug = sanitize_title( $attribute['slug'] );
780 }
781
782 if ( $taxonomy ) {
783
784 $is_taxonomy = 1;
785 }
786
787 if ( $is_taxonomy ) {
788
789 if ( isset( $attribute['options'] ) ) {
790
791 $options = $attribute['options'];
792
793 if ( ! is_array( $attribute['options'] ) ) {
794
795 // text based attributes - Posted values are term names
796 $options = explode( WC_DELIMITER, $options );
797 }
798
799 $values = array_map( 'wc_sanitize_term_text_based', $options );
800 $values = array_filter( $values, 'strlen' );
801
802 } else {
803
804 $values = array();
805 }
806
807 // update post terms
808 if ( taxonomy_exists( $taxonomy ) ) {
809
810 wp_set_object_terms( $product_id, $values, $taxonomy );
811 }
812
813 if ( ! empty( $values ) ) {
814
815 // add attribute to array, but don't set values
816 $attributes[ $taxonomy ] = array(
817 'name' => $taxonomy,
818 'value' => '',
819 'position' => isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0',
820 'is_visible' => ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0,
821 'is_variation' => ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0,
822 'is_taxonomy' => $is_taxonomy,
823 );
824 }
825 } elseif ( isset( $attribute['options'] ) ) {
826 // array based
827 if ( is_array( $attribute['options'] ) ) {
828
829 $values = implode( ' ' . WC_DELIMITER . ' ', array_map( 'wc_clean', $attribute['options'] ) );
830
831 } else {
832
833 $values = implode( ' ' . WC_DELIMITER . ' ', array_map( 'wc_clean', explode( WC_DELIMITER, $attribute['options'] ) ) );
834 }
835
836 // custom attribute - add attribute to array and set the values
837 $attributes[ $attribute_slug ] = array(
838 'name' => wc_clean( $attribute['name'] ),
839 'value' => $values,
840 'position' => isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0',
841 'is_visible' => ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0,
842 'is_variation' => ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0,
843 'is_taxonomy' => $is_taxonomy,
844 );
845 }
846 }
847
848 uasort( $attributes, 'wc_product_attribute_uasort_comparison' );
849
850 update_post_meta( $product_id, '_product_attributes', $attributes );
851 }
852
853 // sales and prices
854 if ( in_array( $product_type, array( 'variable', 'grouped' ), true ) ) {
855
856 // variable and grouped products have no prices
857 update_post_meta( $product_id, '_regular_price', '' );
858 update_post_meta( $product_id, '_sale_price', '' );
859 update_post_meta( $product_id, '_sale_price_dates_from', '' );
860 update_post_meta( $product_id, '_sale_price_dates_to', '' );
861 update_post_meta( $product_id, '_price', '' );
862
863 } else {
864
865 $this->wc_save_product_price( $product_id, $data['regular_price'] );
866 }
867
868 // product categories
869 if ( isset( $data['categories'] ) && is_array( $data['categories'] ) ) {
870
871 $term_ids = array_unique( array_map( 'intval', $data['categories'] ) );
872
873 wp_set_object_terms( $product_id, $term_ids, 'product_cat' );
874 }
875
876 // clear/invalidate cache before calling WooCommerce\Square\Handlers\Product functions (these functions call wc_get_product() and save(), overriding our changes)
877 if ( is_callable( '\WC_Cache_Helper::invalidate_cache_group' ) ) {
878 \WC_Cache_Helper::invalidate_cache_group( 'product_' . $product_id );
879 }
880
881 // square item id
882 if ( isset( $data['square_meta']['item_id'] ) ) {
883 Product::set_square_item_id( $product_id, $data['square_meta']['item_id'] );
884 }
885
886 // square item version
887 if ( isset( $data['square_meta']['item_version'] ) ) {
888 Product::set_square_version( $product_id, $data['square_meta']['item_version'] );
889 }
890
891 // square item variation id
892 if ( isset( $data['square_meta']['item_variation_id'] ) ) {
893 Product::set_square_item_variation_id( $product_id, $data['square_meta']['item_variation_id'] );
894 }
895
896 // square item variation version
897 if ( isset( $data['square_meta']['item_variation_version'] ) ) {
898 Product::set_square_variation_version( $product_id, $data['square_meta']['item_variation_version'] );
899 }
900
901 /**
902 * Fires after processing product meta for a product imported from Square.
903 *
904 * @since 2.0.0
905 *
906 * @param int $product_id the product ID
907 * @param array $data the product data
908 */
909 do_action( 'woocommerce_square_process_product_meta_' . $product_type, $product_id, $data );
910
911 return true;
912 }
913
914
915 /**
916 * Saves the variations for a given product.
917 *
918 * @since 2.0.0
919 *
920 * @param int $product_id the product ID
921 * @param array $data the product data, including a 'variations' key
922 * @return bool
923 * @throws \Exception
924 */
925 protected function save_variations( $product_id, $data ) {
926 global $wpdb;
927
928 $variations = $data['variations'];
929 $attributes = (array) maybe_unserialize( get_post_meta( $product_id, '_product_attributes', true ) );
930 $variable_product = wc_get_product( $product_id );
931 $variations_to_remove = $variable_product ? $variable_product->get_children() : array();
932
933 foreach ( $variations as $menu_order => $variation ) {
934
935 $variation_id = isset( $variation['id'] ) ? absint( $variation['id'] ) : 0;
936
937 if ( ! $variation_id && isset( $variation['sku'] ) ) {
938
939 $variation_sku = wc_clean( $variation['sku'] );
940 $variation_id = wc_get_product_id_by_sku( $variation_sku );
941 }
942
943 /* translators: Placeholders: %1$s - variation ID, %2$s - product name */
944 $variation_post_title = sprintf( __( 'Variation #%1$s of %2$s', 'woocommerce-square' ), $variation_id, esc_html( get_the_title( $product_id ) ) );
945
946 // update or add post
947 if ( ! $variation_id ) {
948
949 $post_status = ( isset( $variation['visible'] ) && false === $variation['visible'] ) ? 'private' : 'publish';
950 $new_variation = array(
951 'post_title' => $variation_post_title,
952 'post_content' => '',
953 'post_status' => $post_status,
954 'post_author' => get_current_user_id(),
955 'post_parent' => $product_id,
956 'post_type' => 'product_variation',
957 'menu_order' => $menu_order,
958 );
959
960 $variation_id = wp_insert_post( $new_variation );
961
962 /**
963 * Fired after creating a product variation during an import from Square.
964 *
965 * @since 2.0.0
966 *
967 * @param int $variation_id the new variation ID
968 */
969 do_action( 'woocommerce_square_create_product_variation', $variation_id );
970
971 } else {
972
973 $update_variation = array(
974 'post_title' => $variation_post_title,
975 'menu_order' => $menu_order,
976 'post_parent' => $product_id,
977 );
978
979 if ( isset( $variation['visible'] ) ) {
980
981 $post_status = ( false === $variation['visible'] ) ? 'private' : 'publish';
982
983 $update_variation['post_status'] = $post_status;
984 }
985
986 $wpdb->update( $wpdb->posts, $update_variation, array( 'ID' => $variation_id ) );
987
988 // remove any variation from $variations_to_remove that is found in Sqaure and also matches a variation in Woo
989 if ( ( $key = array_search( $variation_id, $variations_to_remove, true ) ) !== false ) { //phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.Found, Squiz.PHP.DisallowMultipleAssignments.FoundInControlStructure
990 unset( $variations_to_remove[ $key ] );
991 }
992
993 /**
994 * Fired after updating a product variation during an import from Square.
995 *
996 * @since 2.0.0
997 *
998 * @param int $variation_id the updated variation ID
999 */
1000 do_action( 'woocommerce_square_update_product_variation', $variation_id );
1001 }
1002
1003 // stop if we don't have a variation ID
1004 if ( is_wp_error( $variation_id ) ) {
1005
1006 throw new \Exception( $variation_id->get_error_message() );
1007 }
1008
1009 // SKU
1010 if ( isset( $variation['sku'] ) ) {
1011
1012 $sku = get_post_meta( $variation_id, '_sku', true );
1013 $new_sku = wc_clean( $variation['sku'] );
1014
1015 if ( '' === $new_sku ) {
1016
1017 update_post_meta( $variation_id, '_sku', '' );
1018
1019 } elseif ( $new_sku !== $sku ) {
1020
1021 if ( ! empty( $new_sku ) ) {
1022
1023 if ( wc_product_has_unique_sku( $variation_id, $new_sku ) ) {
1024
1025 update_post_meta( $variation_id, '_sku', $new_sku );
1026
1027 } else {
1028
1029 throw new \Exception( __( 'The SKU already exists on another product', 'woocommerce-square' ) );
1030 }
1031 } else {
1032
1033 update_post_meta( $variation_id, '_sku', '' );
1034 }
1035 }
1036 }
1037
1038 update_post_meta( $variation_id, '_manage_stock', 'yes' );
1039 update_post_meta( $variation_id, '_backorders', 'no' );
1040
1041 $this->wc_save_product_price( $variation_id, $variation['regular_price'] );
1042
1043 update_post_meta( $variation_id, '_download_limit', '' );
1044 update_post_meta( $variation_id, '_download_expiry', '' );
1045 update_post_meta( $variation_id, '_downloadable_files', '' );
1046
1047 // description
1048 if ( isset( $variation['description'] ) ) {
1049 update_post_meta( $variation_id, '_variation_description', wp_kses_post( $variation['description'] ) );
1050 }
1051
1052 // update taxonomies
1053 if ( isset( $variation['attributes'] ) ) {
1054
1055 $updated_attribute_keys = array();
1056
1057 foreach ( $variation['attributes'] as $attribute_key => $attribute ) {
1058
1059 if ( ! isset( $attribute['name'] ) ) {
1060 continue;
1061 }
1062
1063 $taxonomy = 0;
1064 $_attribute = array();
1065
1066 if ( isset( $attribute['slug'] ) ) {
1067
1068 $taxonomy = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] );
1069 }
1070
1071 if ( ! $taxonomy ) {
1072
1073 $taxonomy = sanitize_title( $attribute['name'] );
1074 }
1075
1076 if ( isset( $attributes[ $taxonomy ] ) ) {
1077
1078 $_attribute = $attributes[ $taxonomy ];
1079 }
1080
1081 if ( isset( $_attribute['is_variation'] ) && $_attribute['is_variation'] ) {
1082
1083 $_attribute_key = 'attribute_' . sanitize_title( $_attribute['name'] );
1084 $updated_attribute_keys[] = $_attribute_key;
1085
1086 if ( isset( $_attribute['is_taxonomy'] ) && $_attribute['is_taxonomy'] ) {
1087
1088 // Don't use wc_clean as it destroys sanitized characters
1089 $_attribute_value = isset( $attribute['option'] ) ? sanitize_title( stripslashes( $attribute['option'] ) ) : '';
1090
1091 } else {
1092
1093 $_attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : '';
1094 }
1095
1096 update_post_meta( $variation_id, $_attribute_key, $_attribute_value );
1097 }
1098 }
1099
1100 // remove old taxonomies attributes so data is kept up to date - first get attribute key names
1101 $delete_attribute_keys = $wpdb->get_col( $wpdb->prepare( "SELECT meta_key FROM {$wpdb->postmeta} WHERE meta_key LIKE 'attribute_%%' AND meta_key NOT IN ( '" . implode( "','", $updated_attribute_keys ) . "' ) AND post_id = %d;", $variation_id ) ); //phpcs:ignore WordPress.DB.PreparedSQLPlaceholders, WordPress.DB.PreparedSQL.NotPrepared
1102
1103 foreach ( $delete_attribute_keys as $key ) {
1104
1105 delete_post_meta( $variation_id, $key );
1106 }
1107 }
1108
1109 // square item variation id
1110 if ( isset( $variation['square_meta']['item_variation_id'] ) ) {
1111 Product::set_square_item_variation_id( $variation_id, $variation['square_meta']['item_variation_id'] );
1112 }
1113
1114 // square item variation version
1115 if ( isset( $variation['square_meta']['item_variation_version'] ) ) {
1116 Product::set_square_variation_version( $variation_id, $variation['square_meta']['item_variation_version'] );
1117 }
1118
1119 /**
1120 * Fired after saving a product variation during a Square product import.
1121 *
1122 * @since 2.0.0
1123 *
1124 * @param int $variation_id the variation ID
1125 * @param int $menu_order the menu order
1126 * @param array $variation the variation data
1127 */
1128 do_action( 'woocommerce_square_save_product_variation', $variation_id, $menu_order, $variation );
1129 }
1130
1131 // remove any existing variations on the product that no longer exist in Square
1132 foreach ( $variations_to_remove as $variation_id ) {
1133 wp_delete_post( $variation_id, true );
1134 }
1135
1136 // update parent if variable so price sorting works and stays in sync with the cheapest child
1137 \WC_Product_Variable::sync( $product_id );
1138
1139 // update default attributes options setting
1140 if ( isset( $data['default_attribute'] ) ) {
1141
1142 $data['default_attributes'] = $data['default_attribute'];
1143 }
1144
1145 if ( isset( $data['default_attributes'] ) && is_array( $data['default_attributes'] ) ) {
1146
1147 $default_attributes = array();
1148
1149 foreach ( $data['default_attributes'] as $default_attr_key => $default_attr ) {
1150
1151 if ( ! isset( $default_attr['name'] ) ) {
1152 continue;
1153 }
1154
1155 $taxonomy = sanitize_title( $default_attr['name'] );
1156
1157 if ( isset( $default_attr['slug'] ) ) {
1158 $taxonomy = $this->get_attribute_taxonomy_by_slug( $default_attr['slug'] );
1159 }
1160
1161 if ( isset( $attributes[ $taxonomy ] ) ) {
1162
1163 $_attribute = $attributes[ $taxonomy ];
1164
1165 if ( $_attribute['is_variation'] ) {
1166
1167 $value = '';
1168
1169 if ( isset( $default_attr['option'] ) ) {
1170
1171 if ( $_attribute['is_taxonomy'] ) {
1172
1173 // Don't use wc_clean as it destroys sanitized characters
1174 $value = sanitize_title( trim( stripslashes( $default_attr['option'] ) ) );
1175
1176 } else {
1177
1178 $value = wc_clean( trim( stripslashes( $default_attr['option'] ) ) );
1179 }
1180 }
1181
1182 if ( $value ) {
1183
1184 $default_attributes[ $taxonomy ] = $value;
1185 }
1186 }
1187 }
1188 }
1189
1190 update_post_meta( $product_id, '_default_attributes', $default_attributes );
1191 }
1192
1193 return true;
1194 }
1195
1196
1197 /**
1198 * Gets an attribute taxonomy by its slug.
1199 *
1200 * @since 2.0.0
1201 *
1202 * @param string $slug
1203 * @return string|null
1204 */
1205 protected function get_attribute_taxonomy_by_slug( $slug ) {
1206
1207 $taxonomy = null;
1208 $attribute_taxonomies = wc_get_attribute_taxonomies();
1209
1210 foreach ( $attribute_taxonomies as $key => $tax ) {
1211
1212 if ( $slug === $tax->attribute_name ) {
1213
1214 $taxonomy = 'pa_' . $tax->attribute_name;
1215 break;
1216 }
1217 }
1218
1219 return $taxonomy;
1220 }
1221
1222
1223 /**
1224 * Saves the product price.
1225 *
1226 * @since 2.0.0
1227 *
1228 * @param int $product_id
1229 * @param float $regular_price
1230 * @param float $sale_price
1231 * @param string $date_from
1232 * @param string $date_to
1233 */
1234 public function wc_save_product_price( $product_id, $regular_price, $sale_price = '', $date_from = '', $date_to = '' ) {
1235
1236 $product_id = absint( $product_id );
1237 $regular_price = wc_format_decimal( $regular_price );
1238 $sale_price = '' === $sale_price ? '' : wc_format_decimal( $sale_price );
1239 $date_from = wc_clean( $date_from );
1240 $date_to = wc_clean( $date_to );
1241
1242 update_post_meta( $product_id, '_regular_price', $regular_price );
1243 update_post_meta( $product_id, '_sale_price', $sale_price );
1244
1245 // Save Dates
1246 update_post_meta( $product_id, '_sale_price_dates_from', $date_from ? strtotime( $date_from ) : '' );
1247 update_post_meta( $product_id, '_sale_price_dates_to', $date_to ? strtotime( $date_to ) : '' );
1248
1249 if ( $date_to && ! $date_from ) {
1250
1251 $date_from = strtotime( 'NOW', current_time( 'timestamp' ) ); //phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp.Requested
1252
1253 update_post_meta( $product_id, '_sale_price_dates_from', $date_from );
1254 }
1255
1256 // Update price if on sale
1257 if ( '' !== $sale_price && '' === $date_to && '' === $date_from ) {
1258
1259 update_post_meta( $product_id, '_price', $sale_price );
1260
1261 } else {
1262
1263 update_post_meta( $product_id, '_price', $regular_price );
1264 }
1265
1266 if ( '' !== $sale_price && $date_from && strtotime( $date_from ) < strtotime( 'NOW', current_time( 'timestamp' ) ) ) { //phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp.Requested
1267
1268 update_post_meta( $product_id, '_price', $sale_price );
1269 }
1270
1271 if ( $date_to && strtotime( $date_to ) < strtotime( 'NOW', current_time( 'timestamp' ) ) ) { //phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp.Requested
1272
1273 update_post_meta( $product_id, '_price', $regular_price );
1274 update_post_meta( $product_id, '_sale_price_dates_from', '' );
1275 update_post_meta( $product_id, '_sale_price_dates_to', '' );
1276 }
1277 }
1278
1279
1280 /**
1281 * Clears a product from WooCommerce - used when product creation fails partially through the creation process.
1282 *
1283 * @since 2.0.0
1284 *
1285 * @param int $product_id the product ID
1286 */
1287 protected function clear_product( $product_id ) {
1288
1289 if ( ! is_numeric( $product_id ) || 0 >= $product_id ) {
1290 return;
1291 }
1292
1293 // Delete product attachments
1294 $attachments = get_children(
1295 array(
1296 'post_parent' => $product_id,
1297 'post_status' => 'any',
1298 'post_type' => 'attachment',
1299 )
1300 );
1301
1302 foreach ( $attachments as $attachment ) {
1303 wp_delete_attachment( $attachment->ID, true );
1304 }
1305
1306 // Delete product
1307 wp_delete_post( $product_id, true );
1308 }
1309
1310 /**
1311 * Creates a product in WooCommerce using the data from Square
1312 *
1313 * @since 2.2.0
1314 *
1315 * @param array $data New product data
1316 * @return int
1317 * @throws \Exception
1318 */
1319 protected function create_product_from_square_data( $data = array() ) {
1320 // validate title field
1321 if ( ! isset( $data['title'] ) ) {
1322 /* translators: Placeholders: %s - missing parameter name */
1323 throw new \Exception( sprintf( __( 'Missing parameter %s', 'woocommerce-square' ), 'title' ) );
1324 }
1325
1326 // validate type
1327 if ( ! array_key_exists( wc_clean( $data['type'] ), wc_get_product_types() ) ) {
1328 /* translators: Placeholders: %s - comma separated list of valid product types */
1329 throw new \Exception( sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce-square' ), implode( ', ', array_keys( wc_get_product_types() ) ) ) );
1330 }
1331
1332 $new_product = array(
1333 'post_title' => wc_clean( $data['title'] ),
1334 'post_status' => isset( $data['status'] ) ? wc_clean( $data['status'] ) : 'publish',
1335 'post_type' => 'product',
1336 'post_content' => isset( $data['description'] ) ? $data['description'] : '',
1337 'post_author' => get_current_user_id(),
1338 'menu_order' => isset( $data['menu_order'] ) ? (int) $data['menu_order'] : 0,
1339 );
1340
1341 if ( ! empty( $data['name'] ) ) {
1342 $new_product = array_merge( $new_product, array( 'post_name' => sanitize_title( $data['name'] ) ) );
1343 }
1344
1345 // attempt to create the new product
1346 $product_id = wp_insert_post( $new_product, true );
1347
1348 if ( is_wp_error( $product_id ) ) {
1349 throw new \Exception( $product_id->get_error_message() );
1350 }
1351
1352 return $product_id;
1353 }
1354
1355 /**
1356 * Record an error made during import or update by creating a new Sync Record and Square log
1357 *
1358 * @since 2.2.0
1359 *
1360 * @param string $error Error message to record
1361 * @param \Square\Models\CatalogObject|array|null $catalog_item the catalog object or data
1362 * @param string $context Context for whether the error occurred during import or update
1363 */
1364 protected function record_error( $error, $catalog_item = null, $context = 'import' ) {
1365 if ( $catalog_item && $catalog_item instanceof \Square\Models\CatalogObject && $catalog_item->getItemData() instanceof \Square\Models\CatalogItem ) {
1366 $item_name = $catalog_item->getItemData()->getName();
1367 } elseif ( is_array( $catalog_item ) && ! empty( $catalog_item['title'] ) ) {
1368 $item_name = $catalog_item['title'];
1369 }
1370
1371 if ( 'update' === $context ) {
1372 /* translators: Placeholders: %1$s - Square item name, %2$s - Failure reason */
1373 $message = sprintf( __( 'Could not update %1$s from Square. %2$s', 'woocommerce-square' ), ! empty( $item_name ) ? '"' . $item_name . '"' : 'item', $error );
1374 } else {
1375 /* translators: Placeholders: %1$s - Square item name, %2$s - Failure reason */
1376 $message = sprintf( __( 'Could not import %1$s from Square. %2$s', 'woocommerce-square' ), ! empty( $item_name ) ? '"' . $item_name . '"' : 'item', $error );
1377 }
1378
1379 Records::set_record(
1380 array(
1381 'type' => 'alert',
1382 'message' => $message,
1383 )
1384 );
1385
1386 wc_square()->log( sprintf( 'Error %s product during import: %s', 'import' === $context ? 'creating' : 'updating', $error ) );
1387 }
1388
1389 }
1390