PluginProbe ʕ •ᴥ•ʔ
WooCommerce Square / 5.4.3
WooCommerce Square v5.4.3
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 2.0.5 2.0.6 2.0.7 2.0.8 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.3.0 2.3.1 2.3.2 2.3.3 2.3.4 2.4.0 2.4.1 2.5.0 2.5.1 2.5.2 2.5.3 2.6.0 2.7.0 2.8.0 2.9.0 2.9.1 3.0.0 3.0.1 3.0.2 3.0.3 3.1.0 3.2.0 3.3.0 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.6.1 3.7.0 3.7.1 3.8.0 3.8.1 3.8.2 3.8.3 3.9.0 4.0.0 4.1.0 4.2.0 4.2.1 4.2.2 4.2.3 4.3.0 4.3.1 4.3.2 4.4.0 4.4.1 4.4.2 4.5.0 4.5.1 4.5.2 4.6.0 4.6.1 4.6.2 4.6.3 4.6.4 4.7.0 4.7.1 4.7.2 4.7.3 4.7.4 4.8.0 4.8.1 4.8.2 4.8.3 4.8.4 4.8.5 4.8.6 4.8.7 4.8.8 4.9.0 4.9.1 4.9.2 4.9.3 4.9.4 4.9.5 4.9.6 4.9.7 4.9.8 4.9.9 5.0.0 5.0.1 5.1.0 5.1.1 5.1.2 5.2.0 5.3.0 5.3.1 5.3.2 5.3.3
woocommerce-square / includes / Sync / Interval_Polling.php
woocommerce-square / includes / Sync Last commit date
Records 3 months ago Catalog_Item.php 9 months ago Helper.php 1 year ago Interval_Polling.php 6 months ago Job.php 2 years ago Manual_Synchronization.php 1 month ago Order_Importer.php 11 months ago Order_Mapper.php 11 months ago Order_Polling.php 11 months ago Product_Import.php 2 months ago Records.php 2 years ago Stepped_Job.php 2 years ago
Interval_Polling.php
452 lines
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 Square\Models\BatchRetrieveInventoryCountsResponse;
28 use WooCommerce\Square\Handlers\Product;
29 use WooCommerce\Square\Handlers\Category;
30
31 defined( 'ABSPATH' ) || exit;
32
33 /**
34 * Class to represent a synchronization job to poll latest product updates at intervals.
35 *
36 * @since 2.0.0
37 */
38 class Interval_Polling extends Stepped_Job {
39
40 /**
41 * Assigns the next steps needed for this sync job.
42 *
43 * Adds the next steps to the 'next_steps' attribute.
44 *
45 * @since 2.0.0
46 */
47 protected function assign_next_steps() {
48
49 $next_steps = array();
50
51 if ( $this->is_system_of_record_square() ) {
52
53 $next_steps = array(
54 'update_category_data',
55 'update_product_data',
56 );
57 }
58
59 // only pull latest inventory if enabled
60 if ( wc_square()->get_settings_handler()->is_inventory_sync_enabled() ) {
61 $next_steps[] = 'update_inventory_tracking';
62 $next_steps[] = 'update_inventory_counts';
63 }
64
65 $this->set_attr( 'next_steps', $next_steps );
66 }
67
68 /**
69 * Updates categories from Square.
70 *
71 * @since 2.0.8
72 *
73 * @throws \Exception
74 */
75 protected function update_category_data() {
76 $date = new \DateTime();
77 $date->setTimestamp( $this->get_attr( 'catalog_last_synced_at', (int) wc_square()->get_sync_handler()->get_last_synced_at() ) );
78 $date->setTimezone( new \DateTimeZone( 'UTC' ) );
79
80 $count = 0;
81 $response = wc_square()->get_api()->search_catalog_objects(
82 array(
83 'object_types' => array( 'CATEGORY' ),
84 'begin_time' => $date->format( DATE_ATOM ),
85 )
86 );
87
88 if ( $response->get_data() instanceof SearchCatalogObjectsResponse ) {
89 $categories = $response->get_data()->getObjects();
90
91 if ( $categories && is_array( $categories ) ) {
92 foreach ( $categories as $category ) {
93 Category::import_or_update( $category );
94 }
95 $count = count( $categories );
96
97 Records::set_record(
98 array(
99 'type' => 'info',
100 'message' => sprintf(
101 /* translators: Placeholder %d number of categories. */
102 _n( 'Updated data for %d category.', 'Updated data for %d categories.', count( $categories ), 'woocommerce-square' ),
103 count( $categories )
104 ),
105 )
106 );
107 }
108 } else {
109 Records::set_record(
110 array(
111 'type' => 'alert',
112 'message' => esc_html__( 'Product category data could not be updated from Square. Invalid API response.', 'woocommerce-square' ),
113 )
114 );
115 }
116
117 $this->set_attr( 'update_category_data_count', $count );
118 $this->complete_step( 'update_category_data' );
119 }
120
121 /**
122 * Updates products from Square.
123 *
124 * @since 2.0.0
125 *
126 * @throws \Exception
127 */
128 protected function update_product_data() {
129 $date = new \DateTime();
130 $date->setTimestamp( $this->get_attr( 'catalog_last_synced_at', (int) wc_square()->get_sync_handler()->get_last_synced_at() ) );
131 $date->setTimezone( new \DateTimeZone( 'UTC' ) );
132
133 $products_updated = $this->get_attr( 'processed_product_ids', array() );
134 $cursor = $this->get_attr( 'update_product_data_cursor' );
135
136 $response = wc_square()->get_api()->search_catalog_objects(
137 array(
138 'object_types' => array( 'ITEM' ),
139 'include_deleted_objects' => true,
140 'begin_time' => $date->format( DATE_ATOM ),
141 'cursor' => $cursor,
142 )
143 );
144
145 // store the timestamp after this API request was completed
146 // we don't want to set it at the end, as counts may have changed in the time it takes to process the data
147 if ( ! $cursor ) {
148 wc_square()->get_sync_handler()->set_last_synced_at();
149 }
150
151 if ( $response->get_data() instanceof SearchCatalogObjectsResponse && is_array( $response->get_data()->getObjects() ) ) {
152
153 $product_import = new Product_Import();
154
155 foreach ( $response->get_data()->getObjects() as $object ) {
156
157 // filter out objects that aren't at our configured location
158 if ( ! $object->getPresentAtAllLocations() && ( ! is_array( $object->getPresentAtLocationIds() ) || ! in_array( wc_square()->get_settings_handler()->get_location_id(), $object->getPresentAtLocationIds(), true ) ) ) {
159 continue;
160 }
161
162 $product = Product::get_product_by_square_id( $object->getId() );
163
164 if ( $product instanceof \WC_Product ) {
165 if ( ! in_array( $product->get_type(), wc_square()->get_sync_handler()->supported_product_types(), true ) ) {
166 Records::set_record(
167 array(
168 'type' => 'alert',
169 'message' => sprintf(
170 /* translators: %1$s - product edit page URL, %2$s - Product ID, %3$s - Product type. */
171 __( 'Product <a href="%1$s">#%2$s</a> is excluded from sync as the product type "%3$s" is unsupported.', 'woocommerce-square' ),
172 get_edit_post_link( $product->get_id() ),
173 $product->get_id(),
174 $product->get_type()
175 ),
176 )
177 );
178
179 continue;
180 }
181
182 // deleted items won't have any data to set, so don't try and update the product
183 if ( $object->getIsDeleted() ) {
184
185 $record = array(
186 'type' => 'alert',
187 'product_id' => $product->get_id(),
188 );
189
190 // if enabled, hide the product from the catalog
191 if ( wc_square()->get_settings_handler()->hide_missing_square_products() ) {
192
193 try {
194
195 $product->set_catalog_visibility( 'hidden' );
196 $product->save();
197
198 $record['product_hidden'] = true;
199
200 } catch ( \Exception $e ) {
201 /* translators: Placeholder %1$s Product Name, %2$s Exception message */
202 $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() );
203 }
204 }
205
206 Records::set_record( $record );
207
208 } else {
209
210 try {
211 $data = $product_import->extract_product_data( $object, $product );
212
213 /**
214 * Filters the data that is used to create update a WooCommerce product during import.
215 *
216 * @since 2.0.0
217 *
218 * @param array $data product data
219 * @param \Square\Models\CatalogObject $object the catalog object from the Square API
220 * @param Interval_Polling $this current class instance
221 */
222 $data = apply_filters( 'woocommerce_square_create_product_data', $data, $object, $this );
223
224 // Update the product, this will update/create the variations as well.
225 $product_import->update_product( $product, $data );
226 Product::update_from_square( $product, $object->getItemData(), false );
227
228 $products_updated[] = $product->get_id();
229
230 } catch ( \Exception $exception ) {
231
232 Records::set_record(
233 array(
234 'type' => 'alert',
235 'product_id' => $product->get_id(),
236 /* translators: Placeholder %1$s Product Name, %2$s Exception message */
237 '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() ),
238 )
239 );
240 }
241 }
242 }
243 }
244 }
245
246 $cursor = $response->get_data() instanceof SearchCatalogObjectsResponse ? $response->get_data()->getCursor() : null;
247
248 $this->set_attr( 'update_product_data_cursor', $cursor );
249 $this->set_attr( 'processed_product_ids', array_unique( $products_updated ) );
250 $this->set_attr( 'update_product_data_count', count( array_unique( $products_updated ) ) );
251
252 if ( ! $cursor ) {
253 $this->complete_step( 'update_product_data' );
254 }
255 }
256
257 /**
258 * Updates the inventory tracking value from the latest in Square.
259 *
260 * Helper method, do not open to public.
261 *
262 * @since 3.8.2
263 *
264 * @throws \Exception
265 */
266 protected function update_inventory_tracking() {
267 $products_updated = $this->get_attr( 'processed_product_ids' );
268 $cursor = $this->get_attr( 'update_inventory_tracking_cursor', null );
269 $last_synced_at = $this->get_attr( 'inventory_last_synced_at' );
270 $args = array(
271 'object_types' => array( 'ITEM_VARIATION' ),
272 'limit' => 100,
273 'cursor' => $cursor,
274 );
275
276 if ( $last_synced_at ) {
277 $date = new \DateTime();
278 $date->setTimestamp( $last_synced_at );
279 $date->setTimezone( new \DateTimeZone( 'UTC' ) );
280 $args['begin_time'] = $date->format( DATE_ATOM );
281 }
282
283 $search_result = wc_square()->get_api()->search_catalog_objects( $args );
284
285 if ( ! $search_result->get_data() instanceof SearchCatalogObjectsResponse ) {
286 throw new \Exception( 'API response data is invalid' );
287 }
288
289 $objects = $search_result->get_data()->getObjects() ? $search_result->get_data()->getObjects() : array();
290 $cursor = $search_result->get_data() instanceof SearchCatalogObjectsResponse ? $search_result->get_data()->getCursor() : null;
291
292 $catalog_objects_tracking_stats = Helper::get_catalog_inventory_tracking( $objects );
293 $catalog_objects_to_update = array();
294
295 foreach ( $catalog_objects_tracking_stats as $catalog_object_id => $inventory_data ) {
296 $is_tracking_inventory = $inventory_data['track_inventory'] ?? false;
297 $sold_out = $inventory_data['sold_out'] ?? false;
298 $product = Product::get_product_by_square_variation_id( $catalog_object_id );
299 if ( $product instanceof \WC_Product ) {
300 $manage_stock = $product->get_manage_stock();
301 $stock_status = $product->get_stock_status();
302 $out_of_stock = 'outofstock' === $stock_status;
303
304 // If Inventory tracking is the same as the product's manage stock setting and sold_old value same, skip.
305 if ( (bool) $is_tracking_inventory === (bool) $manage_stock && (bool) $sold_out === (bool) $out_of_stock ) {
306 continue;
307 }
308 $catalog_objects_to_update[] = $catalog_object_id;
309 }
310 }
311
312 if ( ! empty( $catalog_objects_to_update ) ) {
313 // Catalog Inventory data.
314 $inventory_hash = Helper::get_catalog_objects_inventory_stats( $catalog_objects_to_update );
315
316 foreach ( $catalog_objects_to_update as $catalog_object_id ) {
317 $product = Product::get_product_by_square_variation_id( $catalog_object_id );
318 if ( $product instanceof \WC_Product ) {
319 $inventory_data = $catalog_objects_tracking_stats[ $catalog_object_id ] ?? array();
320 $is_tracking_inventory = $inventory_data['track_inventory'] ?? false;
321 $sold_out = $inventory_data['sold_out'] ?? false;
322
323 /* If catalog object is tracked and has a quantity > 0 set in Square. */
324 if ( $is_tracking_inventory && isset( $inventory_hash[ $catalog_object_id ] ) ) {
325 $product->set_stock_quantity( (float) $inventory_hash[ $catalog_object_id ] );
326 $product->set_manage_stock( true );
327
328 /* If the catalog object is tracked but the quantity in Square is set to 0. */
329 } elseif ( $is_tracking_inventory ) {
330 $product->set_stock_quantity( 0 );
331 $product->set_manage_stock( true );
332
333 /* If the catalog object is not tracked in Square at all. */
334 } else {
335 $product->set_stock_status( $sold_out ? 'outofstock' : 'instock' );
336 $product->set_manage_stock( false );
337 }
338
339 $product->save();
340 $products_updated[] = $product->get_id();
341 }
342 }
343 }
344
345 $this->set_attr( 'update_inventory_tracking_cursor', $cursor );
346 $this->set_attr( 'processed_product_ids', array_unique( $products_updated ) );
347
348 if ( ! $cursor ) {
349 $this->complete_step( 'update_inventory_tracking' );
350 }
351 }
352
353 /**
354 * Updates the inventory counts from the latest in Square.
355 *
356 * Helper method, do not open to public.
357 *
358 * @since 2.0.0
359 *
360 * @throws \Exception
361 */
362 protected function update_inventory_counts() {
363 $products_updated = $this->get_attr( 'processed_product_ids' );
364 $cursor = $this->get_attr( 'update_inventory_counts_cursor' );
365 $update_count = $this->get_attr( 'update_inventory_counts_count', 0 );
366
367 $args = array(
368 'location_ids' => array( wc_square()->get_settings_handler()->get_location_id() ),
369 'cursor' => $cursor,
370 );
371
372 $last_synced_at = $this->get_attr( 'inventory_last_synced_at' );
373
374 if ( $last_synced_at ) {
375
376 $date = new \DateTime();
377 $date->setTimestamp( $last_synced_at );
378 $date->setTimezone( new \DateTimeZone( 'UTC' ) );
379
380 $args['updated_after'] = $date->format( DATE_ATOM );
381 }
382
383 $response = wc_square()->get_api()->batch_retrieve_inventory_counts( $args );
384 $cursor = $response->get_data() instanceof BatchRetrieveInventoryCountsResponse ? $response->get_data()->getCursor() : null;
385
386 // store the start timestamp after the first API request was completed but do not save it now
387 // if cursor is present, then it is not the last page. So, use the inventory_last_synced_at time
388 // else use the current time
389 $last_sync_timestamp = $cursor ? $last_synced_at : current_time( 'timestamp', true ); // phpcs:disable WordPress.DateTime.CurrentTimeTimestamp.RequestedUTC
390
391 $catalog_objects_inventory_stats = array();
392
393 foreach ( $response->get_counts() as $count ) {
394 // If catalog stats array already contains the catalog object marked as IN_STOCK, then continue.
395 if ( isset( $catalog_objects_inventory_stats[ $count->getCatalogObjectId() ] ) && $catalog_objects_inventory_stats[ $count->getCatalogObjectId() ]['IN_STOCK'] ) {
396 continue;
397 // Else if the catalog object is IN_STOCK, then mark IN_STOCK as true and set the quantity for later use.
398 } elseif ( 'IN_STOCK' === $count->getState() ) {
399 $catalog_objects_inventory_stats[ $count->getCatalogObjectId() ] = array(
400 'IN_STOCK' => true,
401 'quantity' => $count->getQuantity(),
402 );
403 // 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.
404 } else {
405 $catalog_objects_inventory_stats[ $count->getCatalogObjectId() ] = array(
406 'IN_STOCK' => false,
407 'quantity' => 0,
408 );
409 }
410 }
411
412 // Get the inventory tracking for catalog objects.
413 $catalog_objects_tracking_stats = Helper::get_catalog_objects_tracking_stats(
414 array_keys( $catalog_objects_inventory_stats )
415 );
416
417 foreach ( $catalog_objects_inventory_stats as $catalog_object_id => $stats ) {
418
419 $product = Product::get_product_by_square_variation_id( $catalog_object_id );
420
421 // Square can return multiple "types" of counts, WooCommerce only distinguishes whether a product is in stock or not
422 if ( $product instanceof \WC_Product ) {
423 $inventory_data = $catalog_objects_tracking_stats[ $catalog_object_id ] ?? array();
424 $is_tracking_inventory = $inventory_data['track_inventory'] ?? false;
425 $sold_out = $inventory_data['sold_out'] ?? false;
426
427 if ( $is_tracking_inventory ) {
428 $product->set_manage_stock( true );
429 $product->set_stock_quantity( $stats['quantity'] );
430 } else {
431 $product->set_stock_status( $sold_out ? 'outofstock' : 'instock' );
432 $product->set_manage_stock( false );
433 }
434
435 $product->save();
436
437 $products_updated[] = $product->get_id();
438 }
439 }
440
441 $this->set_attr( 'update_inventory_counts_cursor', $cursor );
442 $this->set_attr( 'processed_product_ids', array_unique( $products_updated ) );
443 $this->set_attr( 'update_inventory_counts_count', $update_count + count( $catalog_objects_inventory_stats ) );
444
445 if ( ! $cursor ) {
446 // When all the inventory counts are synced then set the last sync time to the start time that was stored
447 wc_square()->get_sync_handler()->set_inventory_last_synced_at( $last_sync_timestamp );
448 $this->complete_step( 'update_inventory_counts' );
449 }
450 }
451 }
452