PluginProbe ʕ •ᴥ•ʔ
WooCommerce Square / 3.8.1
WooCommerce Square v3.8.1
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 Interval_Polling.php 3 years ago Job.php 3 years ago Manual_Synchronization.php 3 years ago Product_Import.php 3 years ago Records.php 3 years ago Stepped_Job.php 3 years ago
Interval_Polling.php
308 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_counts';
62 }
63
64 $this->set_attr( 'next_steps', $next_steps );
65 }
66
67 /**
68 * Updates categories from Square.
69 *
70 * @since 2.0.8
71 *
72 * @throws \Exception
73 */
74 protected function update_category_data() {
75 $date = new \DateTime();
76 $date->setTimestamp( $this->get_attr( 'catalog_last_synced_at', (int) wc_square()->get_sync_handler()->get_last_synced_at() ) );
77 $date->setTimezone( new \DateTimeZone( 'UTC' ) );
78
79 $response = wc_square()->get_api()->search_catalog_objects(
80 array(
81 'object_types' => array( 'CATEGORY' ),
82 'begin_time' => $date->format( DATE_ATOM ),
83 )
84 );
85
86 if ( $response->get_data() instanceof SearchCatalogObjectsResponse ) {
87 $categories = $response->get_data()->getObjects();
88
89 if ( $categories && is_array( $categories ) ) {
90 foreach ( $categories as $category ) {
91 Category::import_or_update( $category );
92 }
93
94 Records::set_record(
95 array(
96 'type' => 'info',
97 'message' => sprintf(
98 /* translators: Placeholder %d number of categories. */
99 _n( 'Updated data for %d category.', 'Updated data for %d categories.', count( $categories ), 'woocommerce-square' ),
100 count( $categories )
101 ),
102 )
103 );
104 }
105 } else {
106 Records::set_record(
107 array(
108 'type' => 'alert',
109 'message' => esc_html__( 'Product category data could not be updated from Square. Invalid API response.', 'woocommerce-square' ),
110 )
111 );
112 }
113
114 $this->complete_step( 'update_category_data' );
115 }
116
117 /**
118 * Updates products from Square.
119 *
120 * @since 2.0.0
121 *
122 * @throws \Exception
123 */
124 protected function update_product_data() {
125 $date = new \DateTime();
126 $date->setTimestamp( $this->get_attr( 'catalog_last_synced_at', (int) wc_square()->get_sync_handler()->get_last_synced_at() ) );
127 $date->setTimezone( new \DateTimeZone( 'UTC' ) );
128
129 $products_updated = $this->get_attr( 'processed_product_ids', array() );
130 $cursor = $this->get_attr( 'update_product_data_cursor' );
131
132 $response = wc_square()->get_api()->search_catalog_objects(
133 array(
134 'object_types' => array( 'ITEM' ),
135 'include_deleted_objects' => true,
136 'begin_time' => $date->format( DATE_ATOM ),
137 'cursor' => $cursor,
138 )
139 );
140
141 // store the timestamp after this API request was completed
142 // we don't want to set it at the end, as counts may have changed in the time it takes to process the data
143 if ( ! $cursor ) {
144 wc_square()->get_sync_handler()->set_last_synced_at();
145 }
146
147 if ( $response->get_data() instanceof SearchCatalogObjectsResponse && is_array( $response->get_data()->getObjects() ) ) {
148
149 foreach ( $response->get_data()->getObjects() as $object ) {
150
151 // filter out objects that aren't at our configured location
152 if ( ! $object->getPresentAtAllLocations() && ( ! is_array( $object->getPresentAtLocationIds() ) || ! in_array( wc_square()->get_settings_handler()->get_location_id(), $object->getPresentAtLocationIds(), true ) ) ) {
153 continue;
154 }
155
156 $product = Product::get_product_by_square_id( $object->getId() );
157
158 if ( $product instanceof \WC_Product ) {
159
160 // deleted items won't have any data to set, so don't try and update the product
161 if ( $object->getIsDeleted() ) {
162
163 $record = array(
164 'type' => 'alert',
165 'product_id' => $product->get_id(),
166 );
167
168 // if enabled, hide the product from the catalog
169 if ( wc_square()->get_settings_handler()->hide_missing_square_products() ) {
170
171 try {
172
173 $product->set_catalog_visibility( 'hidden' );
174 $product->save();
175
176 $record['product_hidden'] = true;
177
178 } catch ( \Exception $e ) {
179 /* translators: Placeholder %1$s Product Name, %2$s Exception message */
180 $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() );
181 }
182 }
183
184 Records::set_record( $record );
185
186 } else {
187
188 try {
189 $thumbnail_image_id = Product::get_catalog_item_thumbnail_id( $object );
190 Product::update_from_square( $product, $object->getItemData(), false );
191
192 if ( ! $product->get_image_id() && $thumbnail_image_id ) {
193 Product::update_image_from_square( $product, $thumbnail_image_id );
194 }
195
196 $products_updated[] = $product->get_id();
197
198 } catch ( \Exception $exception ) {
199
200 Records::set_record(
201 array(
202 'type' => 'alert',
203 'product_id' => $product->get_id(),
204 /* translators: Placeholder %1$s Product Name, %2$s Exception message */
205 '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() ),
206 )
207 );
208 }
209 }
210 }
211 }
212 }
213
214 $cursor = $response->get_data() instanceof SearchCatalogObjectsResponse ? $response->get_data()->getCursor() : null;
215
216 $this->set_attr( 'update_product_data_cursor', $cursor );
217 $this->set_attr( 'processed_product_ids', array_unique( $products_updated ) );
218
219 if ( ! $cursor ) {
220 $this->complete_step( 'update_product_data' );
221 }
222 }
223
224
225 /**
226 * Updates the inventory counts from the latest in Square.
227 *
228 * Helper method, do not open to public.
229 *
230 * @since 2.0.0
231 *
232 * @throws \Exception
233 */
234 protected function update_inventory_counts() {
235
236 $products_updated = $this->get_attr( 'processed_product_ids' );
237 $cursor = $this->get_attr( 'update_inventory_counts_cursor' );
238
239 $args = array(
240 'location_ids' => array( wc_square()->get_settings_handler()->get_location_id() ),
241 'cursor' => $cursor,
242 );
243
244 $last_synced_at = $this->get_attr( 'inventory_last_synced_at' );
245
246 if ( $last_synced_at ) {
247
248 $date = new \DateTime();
249 $date->setTimestamp( $last_synced_at );
250 $date->setTimezone( new \DateTimeZone( 'UTC' ) );
251
252 $args['updated_after'] = $date->format( DATE_ATOM );
253 }
254
255 $response = wc_square()->get_api()->batch_retrieve_inventory_counts( $args );
256 $cursor = $response->get_data() instanceof BatchRetrieveInventoryCountsResponse ? $response->get_data()->getCursor() : null;
257
258 // store the start timestamp after the first API request was completed but do not save it now
259 // if cursor is present, then it is not the last page. So, use the inventory_last_synced_at time
260 // else use the current time
261 $last_sync_timestamp = $cursor ? $last_synced_at : current_time( 'timestamp', true ); // phpcs:disable WordPress.DateTime.CurrentTimeTimestamp.RequestedUTC
262
263 $catalog_objects_inventory_stats = array();
264
265 foreach ( $response->get_counts() as $count ) {
266 // If catalog stats array already contains the catalog object marked as IN_STOCK, then continue.
267 if ( isset( $catalog_objects_inventory_stats[ $count->getCatalogObjectId() ] ) && $catalog_objects_inventory_stats[ $count->getCatalogObjectId() ]['IN_STOCK'] ) {
268 continue;
269 // Else if the catalog object is IN_STOCK, then mark IN_STOCK as true and set the quantity for later use.
270 } elseif ( 'IN_STOCK' === $count->getState() ) {
271 $catalog_objects_inventory_stats[ $count->getCatalogObjectId() ] = array(
272 'IN_STOCK' => true,
273 'quantity' => $count->getQuantity(),
274 );
275 // 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.
276 } else {
277 $catalog_objects_inventory_stats[ $count->getCatalogObjectId() ] = array(
278 'IN_STOCK' => false,
279 'quantity' => 0,
280 );
281 }
282 }
283
284 foreach ( $catalog_objects_inventory_stats as $catalog_object_id => $stats ) {
285
286 $product = Product::get_product_by_square_variation_id( $catalog_object_id );
287
288 // Square can return multiple "types" of counts, WooCommerce only distinguishes whether a product is in stock or not
289 if ( $product instanceof \WC_Product ) {
290 $product->set_manage_stock( true );
291 $product->set_stock_quantity( $stats['quantity'] );
292 $product->save();
293
294 $products_updated[] = $product->get_id();
295 }
296 }
297
298 $this->set_attr( 'update_inventory_counts_cursor', $cursor );
299 $this->set_attr( 'processed_product_ids', array_unique( $products_updated ) );
300
301 if ( ! $cursor ) {
302 // When all the inventory counts are synced then set the last sync time to the start time that was stored
303 wc_square()->get_sync_handler()->set_inventory_last_synced_at( $last_sync_timestamp );
304 $this->complete_step( 'update_inventory_counts' );
305 }
306 }
307 }
308