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 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 years ago Catalog_Item.php 3 years ago Helper.php 3 years ago Interval_Polling.php 3 years ago Job.php 3 years ago Manual_Synchronization.php 2 years ago Product_Import.php 2 years ago Records.php 2 years ago Stepped_Job.php 2 years ago
Interval_Polling.php
433 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 foreach ( $response->get_data()->getObjects() as $object ) {
154
155 // filter out objects that aren't at our configured location
156 if ( ! $object->getPresentAtAllLocations() && ( ! is_array( $object->getPresentAtLocationIds() ) || ! in_array( wc_square()->get_settings_handler()->get_location_id(), $object->getPresentAtLocationIds(), true ) ) ) {
157 continue;
158 }
159
160 $product = Product::get_product_by_square_id( $object->getId() );
161
162 if ( $product instanceof \WC_Product ) {
163 if ( ! in_array( $product->get_type(), wc_square()->get_sync_handler()->supported_product_types(), true ) ) {
164 Records::set_record(
165 array(
166 'type' => 'alert',
167 'message' => sprintf(
168 /* translators: %1$s - product edit page URL, %2$s - Product ID, %3$s - Product type. */
169 __( 'Product <a href="%1$s">#%2$s</a> is excluded from sync as the product type "%3$s" is unsupported.', 'woocommerce-square' ),
170 get_edit_post_link( $product->get_id() ),
171 $product->get_id(),
172 $product->get_type()
173 ),
174 )
175 );
176
177 continue;
178 }
179
180 // deleted items won't have any data to set, so don't try and update the product
181 if ( $object->getIsDeleted() ) {
182
183 $record = array(
184 'type' => 'alert',
185 'product_id' => $product->get_id(),
186 );
187
188 // if enabled, hide the product from the catalog
189 if ( wc_square()->get_settings_handler()->hide_missing_square_products() ) {
190
191 try {
192
193 $product->set_catalog_visibility( 'hidden' );
194 $product->save();
195
196 $record['product_hidden'] = true;
197
198 } catch ( \Exception $e ) {
199 /* translators: Placeholder %1$s Product Name, %2$s Exception message */
200 $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() );
201 }
202 }
203
204 Records::set_record( $record );
205
206 } else {
207
208 try {
209 $thumbnail_image_id = Product::get_catalog_item_thumbnail_id( $object );
210 Product::update_from_square( $product, $object->getItemData(), false );
211
212 Product::update_image_from_square( $product, $thumbnail_image_id );
213
214 $products_updated[] = $product->get_id();
215
216 } catch ( \Exception $exception ) {
217
218 Records::set_record(
219 array(
220 'type' => 'alert',
221 'product_id' => $product->get_id(),
222 /* translators: Placeholder %1$s Product Name, %2$s Exception message */
223 '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() ),
224 )
225 );
226 }
227 }
228 }
229 }
230 }
231
232 $cursor = $response->get_data() instanceof SearchCatalogObjectsResponse ? $response->get_data()->getCursor() : null;
233
234 $this->set_attr( 'update_product_data_cursor', $cursor );
235 $this->set_attr( 'processed_product_ids', array_unique( $products_updated ) );
236 $this->set_attr( 'update_product_data_count', count( array_unique( $products_updated ) ) );
237
238 if ( ! $cursor ) {
239 $this->complete_step( 'update_product_data' );
240 }
241 }
242
243 /**
244 * Updates the inventory tracking value from the latest in Square.
245 *
246 * Helper method, do not open to public.
247 *
248 * @since 3.8.2
249 *
250 * @throws \Exception
251 */
252 protected function update_inventory_tracking() {
253 $products_updated = $this->get_attr( 'processed_product_ids' );
254 $cursor = $this->get_attr( 'update_inventory_tracking_cursor', null );
255 $last_synced_at = $this->get_attr( 'inventory_last_synced_at' );
256 $args = array(
257 'object_types' => array( 'ITEM_VARIATION' ),
258 'limit' => 100,
259 'cursor' => $cursor,
260 );
261
262 if ( $last_synced_at ) {
263 $date = new \DateTime();
264 $date->setTimestamp( $last_synced_at );
265 $date->setTimezone( new \DateTimeZone( 'UTC' ) );
266 $args['begin_time'] = $date->format( DATE_ATOM );
267 }
268
269 $search_result = wc_square()->get_api()->search_catalog_objects( $args );
270
271 if ( ! $search_result->get_data() instanceof SearchCatalogObjectsResponse ) {
272 throw new \Exception( 'API response data is invalid' );
273 }
274
275 $objects = $search_result->get_data()->getObjects() ? $search_result->get_data()->getObjects() : array();
276 $cursor = $search_result->get_data() instanceof SearchCatalogObjectsResponse ? $search_result->get_data()->getCursor() : null;
277
278 $catalog_objects_tracking_stats = Helper::get_catalog_inventory_tracking( $objects );
279 $catalog_objects_to_update = array();
280
281 foreach ( $catalog_objects_tracking_stats as $catalog_object_id => $is_tracking_inventory ) {
282 $product = Product::get_product_by_square_variation_id( $catalog_object_id );
283 if ( $product instanceof \WC_Product ) {
284 $manage_stock = $product->get_manage_stock();
285 // If Inventory tracking is the same as the product's manage stock setting, skip.
286 if ( (bool) $is_tracking_inventory === (bool) $manage_stock ) {
287 continue;
288 }
289 $catalog_objects_to_update[] = $catalog_object_id;
290 }
291 }
292
293 if ( ! empty( $catalog_objects_to_update ) ) {
294 // Catalog Inventory data.
295 $inventory_hash = Helper::get_catalog_objects_inventory_stats( $catalog_objects_to_update );
296
297 foreach ( $catalog_objects_to_update as $catalog_object_id ) {
298 $product = Product::get_product_by_square_variation_id( $catalog_object_id );
299 if ( $product instanceof \WC_Product ) {
300 $is_tracking_inventory = isset( $catalog_objects_tracking_stats[ $catalog_object_id ] ) ?
301 $catalog_objects_tracking_stats[ $catalog_object_id ] :
302 true;
303
304 /* If catalog object is tracked and has a quantity > 0 set in Square. */
305 if ( $is_tracking_inventory && isset( $inventory_hash[ $catalog_object_id ] ) ) {
306 $product->set_stock_quantity( (float) $inventory_hash[ $catalog_object_id ] );
307 $product->set_manage_stock( true );
308
309 /* If the catalog object is tracked but the quantity in Square is set to 0. */
310 } elseif ( $is_tracking_inventory ) {
311 $product->set_stock_quantity( 0 );
312 $product->set_manage_stock( true );
313
314 /* If the catalog object is not tracked in Square at all. */
315 } else {
316 $product->set_stock_status( 'instock' );
317 $product->set_manage_stock( false );
318 }
319
320 $product->save();
321 $products_updated[] = $product->get_id();
322 }
323 }
324 }
325
326 $this->set_attr( 'update_inventory_tracking_cursor', $cursor );
327 $this->set_attr( 'processed_product_ids', array_unique( $products_updated ) );
328
329 if ( ! $cursor ) {
330 $this->complete_step( 'update_inventory_tracking' );
331 }
332 }
333
334 /**
335 * Updates the inventory counts from the latest in Square.
336 *
337 * Helper method, do not open to public.
338 *
339 * @since 2.0.0
340 *
341 * @throws \Exception
342 */
343 protected function update_inventory_counts() {
344 $products_updated = $this->get_attr( 'processed_product_ids' );
345 $cursor = $this->get_attr( 'update_inventory_counts_cursor' );
346 $update_count = $this->get_attr( 'update_inventory_counts_count', 0 );
347
348 $args = array(
349 'location_ids' => array( wc_square()->get_settings_handler()->get_location_id() ),
350 'cursor' => $cursor,
351 );
352
353 $last_synced_at = $this->get_attr( 'inventory_last_synced_at' );
354
355 if ( $last_synced_at ) {
356
357 $date = new \DateTime();
358 $date->setTimestamp( $last_synced_at );
359 $date->setTimezone( new \DateTimeZone( 'UTC' ) );
360
361 $args['updated_after'] = $date->format( DATE_ATOM );
362 }
363
364 $response = wc_square()->get_api()->batch_retrieve_inventory_counts( $args );
365 $cursor = $response->get_data() instanceof BatchRetrieveInventoryCountsResponse ? $response->get_data()->getCursor() : null;
366
367 // store the start timestamp after the first API request was completed but do not save it now
368 // if cursor is present, then it is not the last page. So, use the inventory_last_synced_at time
369 // else use the current time
370 $last_sync_timestamp = $cursor ? $last_synced_at : current_time( 'timestamp', true ); // phpcs:disable WordPress.DateTime.CurrentTimeTimestamp.RequestedUTC
371
372 $catalog_objects_inventory_stats = array();
373
374 foreach ( $response->get_counts() as $count ) {
375 // If catalog stats array already contains the catalog object marked as IN_STOCK, then continue.
376 if ( isset( $catalog_objects_inventory_stats[ $count->getCatalogObjectId() ] ) && $catalog_objects_inventory_stats[ $count->getCatalogObjectId() ]['IN_STOCK'] ) {
377 continue;
378 // Else if the catalog object is IN_STOCK, then mark IN_STOCK as true and set the quantity for later use.
379 } elseif ( 'IN_STOCK' === $count->getState() ) {
380 $catalog_objects_inventory_stats[ $count->getCatalogObjectId() ] = array(
381 'IN_STOCK' => true,
382 'quantity' => $count->getQuantity(),
383 );
384 // 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.
385 } else {
386 $catalog_objects_inventory_stats[ $count->getCatalogObjectId() ] = array(
387 'IN_STOCK' => false,
388 'quantity' => 0,
389 );
390 }
391 }
392
393 // Get the inventory tracking for catalog objects.
394 $catalog_objects_tracking_stats = Helper::get_catalog_objects_tracking_stats(
395 array_keys( $catalog_objects_inventory_stats )
396 );
397
398 foreach ( $catalog_objects_inventory_stats as $catalog_object_id => $stats ) {
399
400 $product = Product::get_product_by_square_variation_id( $catalog_object_id );
401
402 // Square can return multiple "types" of counts, WooCommerce only distinguishes whether a product is in stock or not
403 if ( $product instanceof \WC_Product ) {
404 $is_tracking_inventory = isset( $catalog_objects_tracking_stats[ $catalog_object_id ] ) ?
405 $catalog_objects_tracking_stats[ $catalog_object_id ] :
406 true;
407
408 if ( $is_tracking_inventory ) {
409 $product->set_manage_stock( true );
410 $product->set_stock_quantity( $stats['quantity'] );
411 } else {
412 $product->set_stock_status( 'instock' );
413 $product->set_manage_stock( false );
414 }
415
416 $product->save();
417
418 $products_updated[] = $product->get_id();
419 }
420 }
421
422 $this->set_attr( 'update_inventory_counts_cursor', $cursor );
423 $this->set_attr( 'processed_product_ids', array_unique( $products_updated ) );
424 $this->set_attr( 'update_inventory_counts_count', $update_count + count( $catalog_objects_inventory_stats ) );
425
426 if ( ! $cursor ) {
427 // When all the inventory counts are synced then set the last sync time to the start time that was stored
428 wc_square()->get_sync_handler()->set_inventory_last_synced_at( $last_sync_timestamp );
429 $this->complete_step( 'update_inventory_counts' );
430 }
431 }
432 }
433