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 / Handlers / Product / Woo_SOR.php
woocommerce-square / includes / Handlers / Product Last commit date
Woo_SOR.php 2 months ago
Woo_SOR.php
523 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\Handlers\Product;
25
26 use Square\Models\CatalogObject;
27
28 class Woo_SOR extends \WooCommerce\Square\Handlers\Product {
29
30 /**
31 * Updates a Square catalog item with a WooCommerce product's data.
32 *
33 * @since 2.0.0
34 *
35 * @param CatalogObject $catalog_object Square SDK catalog object
36 * @param \WC_Product $product WooCommerce product
37 * @return CatalogObject
38 * @throws \Exception
39 */
40 public static function update_catalog_item( CatalogObject $catalog_object, \WC_Product $product ) {
41
42 if ( 'ITEM' !== $catalog_object->getType() || ! $catalog_object->getItemData() ) {
43 throw new \Exception( 'Type of $catalog_object must be an ITEM' );
44 }
45
46 // ensure the product meta is persisted
47 self::update_product( $product, $catalog_object );
48
49 if ( ! $catalog_object->getId() ) {
50 $catalog_object->setId( self::get_square_item_id( $product ) );
51 }
52
53 $is_delete = 'trash' === $product->get_status();
54
55 $catalog_object = self::set_catalog_object_location_ids( $catalog_object, $is_delete );
56
57 $item_data = $catalog_object->getItemData();
58
59 $item_data->setName( $product->get_name() );
60 $item_data->setDescriptionHtml( $product->get_description() );
61
62 $square_categories = array();
63 $reporting_category = null;
64
65 foreach ( $product->get_category_ids() as $category_id ) {
66
67 $map = \WooCommerce\Square\Handlers\Category::get_mapping( $category_id );
68
69 if ( ! empty( $map['square_id'] ) ) {
70
71 $square_category = new \Square\Models\CatalogObjectCategory();
72 $square_category->setId( $map['square_id'] );
73 $square_categories[] = $square_category;
74
75 if ( ! $reporting_category ) {
76 $reporting_category = $square_category;
77 }
78 }
79 }
80
81 // if categories with Square IDs were found
82 if ( ! empty( $square_categories ) ) {
83 $item_data->setCategories( $square_categories );
84
85 if ( $reporting_category ) {
86 $item_data->setReportingCategory( $reporting_category );
87 }
88 }
89
90 // Only use attributes that are used for variations.
91 $attributes = self::get_used_variation_attributes( $product );
92
93 $product_variation_ids = $product->get_children();
94 $catalog_variations = $item_data->getVariations() ?? array();
95
96 // if dealing with a variable product, try and match the variations
97 if ( $product->is_type( 'variable' ) ) {
98
99 $options_ids = array();
100
101 /**
102 * If there are multiple variations, it must be a considered as Dynamic Options supported product.
103 * Create/Update and Assign Dynamic Options only if a product
104 * has multiple attributes OR options already exists in Square.
105 */
106 if (
107 count( $attributes ) > 1
108 ) {
109 $result = wc_square()->get_api()->retrieve_options_data();
110 $options_data = $result[1] ?? array();
111
112 // Set the product as a dynamic options product.
113 update_post_meta( $product->get_id(), '_dynamic_options', true );
114
115 // Loop through the attributes to create options and values at Square.
116 foreach ( $attributes as $attribute_id => $attribute ) {
117
118 $attribute_name = $attribute->get_name();
119 // Check if its a taxonomy-based attribute.
120 $attribute_option_values = array();
121 if ( taxonomy_exists( $attribute_id ) ) {
122 $terms = get_terms( $attribute_id );
123 $attribute_option_values = wp_list_pluck( $terms, 'name' );
124 } else {
125 $attribute_option_values = $attribute->get_options();
126 }
127
128 // Check if Square already has the option created with the same name.
129 // To do so, we can check if we already have the name in options/transient,
130 // if yes, use the relative Square ID.
131 $option_id = false;
132 foreach ( $options_data as $transient_option_id => $option_data_transient ) {
133 if ( $option_data_transient['name'] === $attribute_name ) {
134 $option_id = $transient_option_id;
135 break;
136 }
137 }
138
139 // If name does not exist, create a new option in Square.
140 // If name exists, check if all values are present in Square.
141 // If not, create the missing values.
142 $option = wc_square()->get_api()->create_options_and_values( $option_id, $attribute_name, $attribute_option_values );
143 $options_ids[] = $option->getId();
144 }
145
146 // Set the item_option_id for each option to the product.
147 $product_options = array();
148
149 foreach ( $options_ids as $option_id ) {
150 $item_option = new \Square\Models\CatalogItemOptionForItem();
151 $item_option->setItemOptionId( $option_id );
152 $product_options[] = $item_option;
153 }
154
155 $catalog_object->getItemData()->setItemOptions( $product_options );
156 } else {
157 // If the product has only one attribute, it's not a dynamic options product.
158 // So, remove the dynamic options meta.
159 delete_post_meta( $product->get_id(), '_dynamic_options' );
160 $catalog_object->getItemData()->setItemOptions( null );
161 }
162
163 if ( is_array( $catalog_variations ) ) {
164
165 foreach ( $catalog_variations as $object_key => $variation_object ) {
166
167 $product_variation_id = self::get_product_id_by_square_variation_id( $variation_object->getId() );
168
169 // ID might not be set, so try the SKU
170 if ( ! $product_variation_id ) {
171 $product_variation_id = wc_get_product_id_by_sku( $variation_object->getItemVariationData()->getSku() );
172 }
173
174 // if a product was found and belongs to the parent, use it
175 if ( false !== ( $key = array_search( $product_variation_id, $product_variation_ids, false ) ) ) {
176
177 $product_variation = wc_get_product( $product_variation_id );
178
179 if ( $product_variation instanceof \WC_Product ) {
180
181 $catalog_variations[ $object_key ] = self::update_catalog_variation( $variation_object, $product_variation, $options_ids );
182
183 // consider this variation taken care of
184 unset( $product_variation_ids[ $key ] );
185 }
186 } else {
187
188 unset( $catalog_variations[ $object_key ] );
189 }
190 }
191 }
192
193 // all that's left are variations that didn't have a match, so create new variations
194 foreach ( $product_variation_ids as $product_variation_id ) {
195
196 $product_variation = wc_get_product( $product_variation_id );
197
198 if ( ! $product_variation instanceof \WC_Product ) {
199 continue;
200 }
201
202 $variation_object = new CatalogObject(
203 'ITEM_VARIATION',
204 ''
205 );
206
207 $catalog_item_variation = new \Square\Models\CatalogItemVariation();
208 $catalog_item_variation->setItemId( $catalog_object->getId() );
209 $variation_object->setItemVariationData( $catalog_item_variation );
210
211 $catalog_variations[] = self::update_catalog_variation( $variation_object, $product_variation, $options_ids );
212 }
213 } else { // otherwise, we have a simple product
214
215 if ( ! empty( $catalog_variations ) ) {
216
217 $variation_object = $catalog_variations[0];
218
219 } else {
220
221 $variation_object = new CatalogObject(
222 'ITEM_VARIATION',
223 ''
224 );
225
226 $catalog_item_variation = new \Square\Models\CatalogItemVariation();
227 $catalog_item_variation->setItemId( $catalog_object->getId() );
228 $variation_object->setItemVariationData( $catalog_item_variation );
229 }
230
231 $catalog_variations = array( self::update_catalog_variation( $variation_object, $product ) );
232
233 $catalog_object->getItemData()->setItemOptions( null );
234 }
235
236 $item_data->setVariations( array_values( $catalog_variations ) );
237
238 $catalog_object->setItemData( $item_data );
239
240 /**
241 * Fires when updating a Square catalog item with WooCommerce product data.
242 *
243 * @since 2.0.0
244 *
245 * @param CatalogObject $catalog_object Square SDK catalog object
246 * @param \WC_Product $product WooCommerce product
247 */
248 $catalog_object = apply_filters( 'wc_square_update_catalog_item', $catalog_object, $product );
249
250 return $catalog_object;
251 }
252
253
254 /**
255 * Updates a Square catalog item variation with a WooCommerce product's data.
256 *
257 * @since 2.0.0
258 *
259 * @param CatalogObject $catalog_object Square SDK catalog object
260 * @param \WC_Product $product WooCommerce product
261 * @param array $options_ids Array of options IDs
262 *
263 * @return CatalogObject
264 * @throws \Exception
265 */
266 public static function update_catalog_variation( CatalogObject $catalog_object, \WC_Product $product, $options_ids = array() ) {
267
268 if ( 'ITEM_VARIATION' !== $catalog_object->getType() || ! $catalog_object->getItemVariationData() ) {
269 throw new \Exception( 'Type of $catalog_object must be an ITEM_VARIATION' );
270 }
271
272 // ensure the variation meta is persisted
273 self::update_variation( $product, $catalog_object );
274
275 if ( ! $catalog_object->getId() ) {
276 // Use a temp ID so stale stored variation IDs do not cause Square to reject the batch.
277 $catalog_object->setId( '#item_variation_' . $product->get_id() );
278 }
279
280 if ( ! $catalog_object->getVersion() ) {
281 $catalog_object->setVersion( self::get_square_variation_version( $product ) );
282 }
283
284 $catalog_object = self::set_catalog_object_location_ids( $catalog_object, 'trash' === $product->get_status() );
285
286 $variation_data = $catalog_object->getItemVariationData();
287
288 if ( $product->get_regular_price() || $product->get_sale_price() ) {
289 $variation_data->setPriceMoney( self::price_to_money( $product->get_sale_price() ?: $product->get_regular_price() ) );
290 } else {
291 $variation_data->setPriceMoney( self::price_to_money( 0 ) );
292 }
293
294 $variation_data->setPricingType( 'FIXED_PRICING' );
295
296 /**
297 * Simple products have only 1 variation and the name of the variation
298 * is derived from CatalogItem::name. For variable products, each variation
299 * can have its own name, so we put a condition to only set the name for
300 * variation.
301 *
302 * @see https://github.com/woocommerce/woocommerce-square/issues/570
303 */
304 if ( 'variation' === $product->get_type() ) {
305 $result = wc_square()->get_api()->retrieve_options_data();
306 $options_data = isset( $result[1] ) ? $result[1] : array();
307 $parent_product = wc_get_product( $product->get_parent_id() );
308 $attributes = self::get_used_variation_attributes( $parent_product );
309 $variation_items = $product->get_attributes();
310 $variation_item_values = array();
311
312 if ( 1 === count( $attributes ) ) {
313 // Set the name of the variation if it's a single variation.
314 $variation_data->setName( reset( $variation_items ) );
315 $variation_data->setItemOptionValues( null );
316 } else {
317 // If there are multiple attributes, the name of the variation is the combination of all attribute values.
318 $variation_name = array();
319 $variation_index = 0;
320
321 /**
322 * Set the `item_option_values` for the variation.
323 *
324 * Retrieve the options data from the transient. At this point, the options data
325 * should already be available, as we have already created the necessary options
326 * and values in the parent product above.
327 */
328 foreach ( $variation_items as $attribute_id => $attribute_value ) {
329 // If the attribute value is empty, set it to 'Any'.
330 $attribute_value = empty( $attribute_value ) ? WC_SQUARE_OPTION_ANY : $attribute_value;
331
332 // Check if it's a global attribute (taxonomy-based, e.g., "pa_color")
333 $taxonomy_exists = false;
334 if ( taxonomy_exists( $attribute_id ) ) {
335 // Use wc_attribute_label for global attributes
336 $attribute_name = $attribute_id;
337 $variation_name[] = $attribute_value = WC_SQUARE_OPTION_ANY === $attribute_value ? WC_SQUARE_OPTION_ANY : get_term_by( 'slug', $attribute_value, $attribute_id )->name;
338 $taxonomy_exists = true;
339 } else {
340 // For custom attributes, simply use the cleaned-up attribute ID
341 $attribute_name = str_replace( '-', ' ', $attribute_id );
342 $attribute_id = $attribute_name;
343 $variation_name[] = $attribute_value;
344 }
345
346 $option_id = '';
347 $option_value_id = '';
348 if ( isset( $options_data[ $options_ids[ $variation_index ] ] ) ) {
349 $option_id = $options_ids[ $variation_index ];
350
351 foreach ( $options_data[ $options_ids[ $variation_index ] ]['value_ids'] as $value_id => $value_name ) {
352 if ( $value_name === $attribute_value ) {
353 $option_value_id = $value_id;
354 break;
355 }
356 }
357 }
358
359 if ( $option_id && $option_value_id ) {
360 $option_value_object = new \Square\Models\CatalogItemOptionValueForItemVariation();
361 $option_value_object->setItemOptionId( $option_id );
362 $option_value_object->setItemOptionValueId( $option_value_id );
363
364 $variation_item_values[] = $option_value_object;
365 } else {
366
367 if ( $taxonomy_exists ) {
368 // Get all attribute terms from Woo taxonomy.
369 $attribute_option_values = get_terms( $attribute_id );
370 $attribute_option_values = wp_list_pluck( $attribute_option_values, 'name' );
371 } else {
372 // Get all attribute values from the parent product.
373 $attribute_option_values = $parent_product->get_attribute( $attribute_id );
374 $attribute_option_values = array_map( 'trim', explode( '|', $attribute_option_values ) );
375 }
376
377 // If the attribute value is 'Any', add it to the attribute option values.
378 if ( WC_SQUARE_OPTION_ANY === $attribute_value && ! in_array( WC_SQUARE_OPTION_ANY, $attribute_option_values, true ) ) {
379 $attribute_option_values[] = WC_SQUARE_OPTION_ANY;
380 }
381
382 $option = wc_square()->get_api()->create_options_and_values( $option_id, $attribute_name, $attribute_option_values );
383 $option_id = $option->getId();
384
385 // Get the Square ID of the attribute value.
386 $updated_option_values = $option->getItemOptionData() ? $option->getItemOptionData()->getValues() : array();
387 foreach ( $updated_option_values as $option_value ) {
388 if ( $option_value->getItemOptionValueData()->getName() === $attribute_value ) {
389 $option_value_id = $option_value->getId();
390 break;
391 }
392 }
393
394 $option_value_object = new \Square\Models\CatalogItemOptionValueForItemVariation();
395 $option_value_object->setItemOptionId( $option_id );
396 $option_value_object->setItemOptionValueId( $option_value_id );
397
398 $variation_item_values[] = $option_value_object;
399 }
400
401 ++$variation_index;
402 }
403
404 // Set the name of the variation as the combination of all attribute values.
405 $variation_data->setName( implode( ', ', $variation_name ) );
406 $variation_data->setItemOptionValues( $variation_item_values );
407 }
408 }
409
410 if ( wc_square()->get_settings_handler()->is_inventory_sync_enabled() ) {
411 $track_inventory = $variation_data->getTrackInventory();
412 $location_overrides = $variation_data->getLocationOverrides();
413
414 /*
415 * Only update track_inventory if it's not set.
416 * This will only update inventory tracking on new variations.
417 * inventory tracking will remain the same for existing variations.
418 */
419 if ( is_null( $track_inventory ) && is_null( $location_overrides ) ) {
420 $variation_data->setTrackInventory( $product->get_manage_stock() );
421
422 // If the product is not managing stock and is out of stock, set it as sold out.
423 if ( ! $product->get_manage_stock() && 'outofstock' === $product->get_stock_status() ) {
424 $configured_location = wc_square()->get_settings_handler()->get_location_id();
425 $location_override = new \Square\Models\ItemVariationLocationOverrides();
426 $location_override->setLocationId( $configured_location );
427 // We need to set track_inventory to true to be able to set sold_out to true, without it will be ignored.
428 $location_override->setTrackInventory( true );
429 $location_override->setSoldOut( true );
430 $location_overrides = array( $location_override );
431
432 // Set the location overrides.
433 $variation_data->setLocationOverrides( $location_overrides );
434 }
435 }
436 }
437
438 $variation_data->setSku( $product->get_sku() );
439
440 if ( ! $variation_data->getItemId() ) {
441
442 $parent_product = $product->get_parent_id() ? wc_get_product( $product->get_parent_id() ) : $product;
443
444 if ( ! $parent_product instanceof \WC_Product ) {
445 $variation_data->setItemId( self::get_square_item_id( $parent_product ) );
446 }
447 }
448
449 $catalog_object->setItemVariationData( $variation_data );
450
451 /**
452 * Fires when updating a Square catalog item variation with WooCommerce product data.
453 *
454 * @since 2.0.0
455 *
456 * @param CatalogObject $catalog_object Square SDK catalog object
457 * @param \WC_Product $product WooCommerce product
458 */
459 $catalog_object = apply_filters( 'wc_square_update_catalog_item_variation', $catalog_object, $product );
460
461 return $catalog_object;
462 }
463
464
465 /**
466 * Sets the present/absent location IDs to a catalog object.
467 *
468 * @since 2.0.0
469 *
470 * @param CatalogObject $catalog_object Square SDK catalog object
471 * @param bool $is_delete whether the product is being deleted
472 * @return CatalogObject
473 */
474 public static function set_catalog_object_location_ids( CatalogObject $catalog_object, $is_delete = false ) {
475
476 $location_id = wc_square()->get_settings_handler()->get_location_id();
477
478 $present_location_ids = $catalog_object->getPresentAtLocationIds() ?: array();
479 $absent_location_ids = $catalog_object->getAbsentAtLocationIds() ?: array();
480
481 // if trashed, set as absent at our location
482 if ( $is_delete ) {
483
484 $absent_location_ids[] = $location_id;
485
486 if ( false !== ( $key = array_search( $location_id, $present_location_ids, true ) ) ) {
487 unset( $present_location_ids[ $key ] );
488 }
489 } else { // otherwise, it's present
490
491 $present_location_ids[] = $location_id;
492
493 if ( false !== ( $key = array_search( $location_id, $absent_location_ids, true ) ) ) {
494 unset( $absent_location_ids[ $key ] );
495 }
496 }
497
498 $catalog_object->setAbsentAtLocationIds( array_unique( array_values( $absent_location_ids ) ) );
499 $catalog_object->setPresentAtLocationIds( array_unique( array_values( $present_location_ids ) ) );
500
501 $catalog_object->setPresentAtAllLocations( false );
502
503 return $catalog_object;
504 }
505
506 /**
507 * Helper to get only attributes used for variations.
508 *
509 * @since 4.9.3
510 *
511 * @param WC_Product $product
512 * @return array
513 */
514 public static function get_used_variation_attributes( $product ) {
515 return array_filter(
516 $product->get_attributes(),
517 function ( $attribute ) {
518 return $attribute->get_variation();
519 }
520 );
521 }
522 }
523