PluginProbe ʕ •ᴥ•ʔ
WooCommerce Square / 5.4.2
WooCommerce Square v5.4.2
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 / Products.php
woocommerce-square / includes / Handlers Last commit date
Product 1 month ago Async_Request.php 2 years ago Background_Job.php 2 months ago Category.php 1 month ago Connection.php 4 months ago Email.php 1 year ago Order.php 2 days ago Order_Sync.php 10 months ago Product.php 1 month ago Products.php 2 days ago Sync.php 3 years ago
Products.php
1591 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;
25
26 defined( 'ABSPATH' ) || exit;
27
28 use Exception;
29 use WC_Product;
30 use WooCommerce\Square\Handlers\Product;
31 use WooCommerce\Square\Sync\Records;
32 use WooCommerce\Square;
33 use WooCommerce\Square\Gateway\Gift_Card;
34
35 /**
36 * Products admin handler.
37 *
38 * @since 2.0.0
39 */
40 class Products {
41
42
43 /** @var array associative array of product error codes and messages */
44 private $product_errors;
45
46 /** @var array associative array of memoized errors being output for a product at one time */
47 private $output_errors = array();
48
49 /** @var int[] array of product IDs that have been scheduled for sync in this request */
50 private $products_to_sync = array();
51
52 /** @var int[] array of product IDs that have been scheduled for deletion in this request */
53 private $products_to_delete = array();
54
55 /** @var bool whether gift card features are enabled */
56 private $gift_card_enabled = 'no';
57
58 /** @var int[] array of product IDs that have been scheduled for inventory sync in this request */
59 private $products_to_inventory_sync = array();
60
61 /** @var Square\Plugin plugin instance */
62 private $plugin;
63
64 /**
65 * Sets up the products admin handler.
66 *
67 * @since 2.0.0
68 */
69 public function __construct( Square\Plugin $plugin ) {
70
71 $this->plugin = $plugin;
72
73 // Get gift card features status.
74 $gift_card_settings = get_option( Gift_Card::SQUARE_PAYMENT_SETTINGS_OPTION_NAME, array() );
75 $this->gift_card_enabled = $gift_card_settings['enabled'] ?? 'no';
76
77 add_action( 'init', array( $this, 'register_common_errors' ) );
78 add_action( 'current_screen', array( $this, 'add_tabs' ), 99 );
79
80 // add hooks
81 $this->add_products_edit_screen_hooks();
82 $this->add_product_edit_screen_hooks();
83 $this->add_product_sync_hooks();
84 }
85
86 /**
87 * Loads register common errors.
88 *
89 * @since 4.8.6
90 */
91 public function register_common_errors() {
92 // Add common errors.
93 $this->product_errors = array(
94 /* translators: Placeholder: %s - product name */
95 'missing_sku' => __( "Please add an SKU to sync %s with Square. The SKU must match the item's SKU in your Square account.", 'woocommerce-square' ),
96 /* translators: Placeholder: %s - product name */
97 'missing_variation_sku' => __( "Please add an SKU to every variation of %s for syncing with Square. Each SKU must be unique and match the corresponding item's SKU in your Square account.", 'woocommerce-square' ),
98 );
99 }
100
101 /**
102 * Adds hooks to the admin products edit screen.
103 *
104 * Products filtering, bulk actions, etc.
105 *
106 * @since 2.0.0
107 */
108 private function add_products_edit_screen_hooks() {
109
110 // adds an option to the "Filter by product type" dropdown
111 add_action( 'restrict_manage_posts', array( $this, 'add_filter_products_synced_with_square_option' ) );
112 // allow filtering products by sync status by altering results
113 add_filter( 'request', array( $this, 'filter_products_synced_with_square' ) );
114
115 // prevent copying Square data when duplicating a product automatically
116 add_action( 'woocommerce_product_duplicate', array( $this, 'handle_product_duplication' ), 20, 2 );
117
118 // handle quick/bulk edit actions in the products edit screen for setting sync status
119 add_action( 'woocommerce_product_quick_edit_end', array( $this, 'add_quick_edit_inputs' ) );
120 add_action( 'woocommerce_product_bulk_edit_end', array( $this, 'add_bulk_edit_inputs' ) );
121 add_action( 'woocommerce_product_quick_edit_save', array( $this, 'set_synced_with_square' ) );
122 add_action( 'woocommerce_product_bulk_edit_save', array( $this, 'set_synced_with_square' ) );
123 add_action( 'woocommerce_admin_process_product_object', array( __CLASS__, 'process_post_data' ) );
124
125 // export product sync status.
126 add_filter( 'woocommerce_product_export_column_names', array( $this, 'add_sync_status_to_column' ) );
127 add_filter( 'woocommerce_product_export_product_default_columns', array( $this, 'add_sync_status_to_column' ) );
128 add_filter( 'woocommerce_product_export_product_column_wc_square_synced', array( $this, 'export_sync_status_taxonomy' ), 10, 2 );
129 add_filter( 'woocommerce_csv_product_import_mapping_options', array( $this, 'register_sync_status_for_importer' ) );
130 add_filter( 'woocommerce_csv_product_import_mapping_default_columns', array( $this, 'map_sync_status_column' ) );
131 add_filter( 'woocommerce_product_import_pre_insert_product_object', array( $this, 'import_sync_status' ), 10, 2 );
132
133 if ( 'yes' === $this->gift_card_enabled ) {
134 add_action( 'woocommerce_before_add_to_cart_button', array( $this, 'add_input_fields_to_gift_card_product' ) );
135 add_filter( 'woocommerce_product_is_taxable', array( $this, 'disable_taxes_for_gift_card_product' ), 10, 2 );
136 add_filter( 'woocommerce_product_needs_shipping', array( $this, 'disable_shipping_for_gift_card_product' ), 10, 2 );
137 add_filter( 'woocommerce_coupon_is_valid_for_product', array( $this, 'disable_coupons_for_gift_card_product' ), 10, 4 );
138 add_filter( 'woocommerce_coupon_is_valid', array( $this, 'coupon_is_valid' ), 10, 2 );
139 add_filter( 'woocommerce_add_cart_item_data', array( $this, 'add_cart_item_data' ), 10, 4 );
140 add_filter( 'woocommerce_get_item_data', array( $this, 'add_sent_to_email_to_cart_item' ), 10, 2 );
141 add_filter( 'woocommerce_add_to_cart_sold_individually_found_in_cart', array( $this, 'limit_gift_card_quantity_in_cart' ), 10, 2 );
142 add_filter( 'woocommerce_order_item_needs_processing', array( $this, 'filter_needs_processing' ), 10, 2 );
143 add_filter( 'woocommerce_single_product_image_thumbnail_html', array( $this, 'filter_single_product_featured_image_placeholder' ) );
144 add_filter( 'woocommerce_product_get_image', array( $this, 'filter_gift_card_product_featured_image_placeholder' ), 10, 3 );
145
146 // Product blocks support.
147 add_filter( 'woocommerce_product_add_to_cart_text', array( $this, 'gift_card_add_to_cart_text' ), 10, 2 );
148 add_filter( 'woocommerce_product_add_to_cart_url', array( $this, 'gift_card_add_to_cart_url' ), 10, 2 );
149 add_filter( 'woocommerce_product_has_options', array( $this, 'gift_card_product_has_options' ), 10, 2 );
150 add_filter( 'woocommerce_product_supports', array( $this, 'gift_card_product_supports' ), 10, 3 );
151 add_filter( 'woocommerce_product_get_image_id', array( $this, 'gift_card_product_image_id' ), 10, 2 );
152 }
153 }
154
155 /**
156 * Adds hooks to individual products edit screens.
157 *
158 * Product data input fields, variations, etc.
159 *
160 * @since 2.0.0
161 */
162 private function add_product_edit_screen_hooks() {
163
164 // handle individual products input fields for setting sync status
165 add_action( 'woocommerce_product_options_general_product_data', array( $this, 'add_product_data_fields' ) );
166 add_action( 'woocommerce_admin_process_product_object', array( $this, 'process_product_data' ), 20 );
167 add_action( 'woocommerce_before_product_object_save', array( $this, 'maybe_adjust_square_stock' ) );
168
169 add_action( 'admin_notices', array( $this, 'add_notice_product_hidden_from_catalog' ) );
170
171 if ( 'yes' === $this->gift_card_enabled ) {
172 add_filter( 'product_type_options', array( __CLASS__, 'add_gift_card_checkbox' ) );
173 add_filter( 'woocommerce_product_data_tabs', array( $this, 'filter_product_tabs' ), 50 );
174 }
175 }
176
177 /**
178 * Adds hooks to sync products that have been updated.
179 *
180 * @since 2.0.0
181 */
182 private function add_product_sync_hooks() {
183
184 add_action( 'woocommerce_update_product', array( $this, 'validate_product_update_and_sync' ) );
185 add_action( 'trashed_post', array( $this, 'maybe_stage_products_for_deletion' ) );
186 add_action( 'shutdown', array( $this, 'maybe_sync_staged_products' ) );
187 add_action( 'shutdown', array( $this, 'maybe_delete_staged_products' ) );
188
189 // Sync product inventory when a product is added to the cart.
190 add_action( 'woocommerce_add_to_cart', array( $this, 'maybe_stage_products_for_sync_inventory' ), 10, 4 );
191 add_action( 'shutdown', array( $this, 'maybe_sync_product_inventory' ) );
192 }
193
194 /**
195 * Add help tabs.
196 *
197 * @since 4.7.0
198 */
199 public function add_tabs() {
200 if ( ! function_exists( 'wc_get_screen_ids' ) ) {
201 return;
202 }
203
204 $screen = get_current_screen();
205
206 if ( ! $screen || ! in_array( $screen->id, wc_get_screen_ids(), true ) ) {
207 return;
208 }
209
210 $help_tabs = $screen->get_help_tabs();
211 if ( ! isset( $help_tabs['woocommerce_onboard_tab'] ) ) {
212 return;
213 }
214
215 $updated_help_tab = $help_tabs['woocommerce_onboard_tab'];
216
217 $square_text = '<h2>' . esc_html__( 'Square Onboarding Setup Wizard', 'woocommerce-square' ) . '</h2>';
218 $square_text .= '<p>' . esc_html__( 'If you need to access the Square onboarding setup wizard again, please click on the button below.', 'woocommerce-square' ) . '</p>' .
219 '<p><a href="' . esc_url( admin_url( 'admin.php?page=woocommerce-square-onboarding' ) ) . '" class="button button-primary">' . esc_html__( 'Setup wizard', 'woocommerce-square' ) . '</a></p>';
220
221 $updated_help_tab['content'] .= $square_text;
222
223 // Remove the old help tab and add the new one.
224 $screen->remove_help_tab( 'woocommerce_onboard_tab' );
225 $screen->add_help_tab( $updated_help_tab );
226 }
227
228 /**
229 * Adds an option to filter products by sync status.
230 *
231 * @internal
232 *
233 * @since 2.0.0
234 *
235 * @param string $post_type the post type context
236 */
237 public function add_filter_products_synced_with_square_option( $post_type ) {
238
239 if ( 'product' !== $post_type ) {
240 return;
241 }
242
243 $label = esc_html__( 'Synced with Square', 'woocommerce-square' );
244
245 // Nonce check not required, checked against known string, read-only action.
246 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
247 $selected = isset( $_GET['product_type'] ) && 'synced-with-square' === $_GET['product_type'] ? 'selected="selected"' : '';
248
249 ob_start();
250 ?>
251 jQuery( document ).ready( function( $ ) {
252 <?php // Not escaping as we know the value is not user-controlled and avoids issue with outputting `selected=""selected""` ?>
253 <?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
254 $( 'select#dropdown_product_type' ).append( '<option value="synced-with-square" <?php echo $selected; ?>><?php echo esc_js( $label ); ?></option>' );
255 } );
256 <?php
257 $javascript = ob_get_clean();
258 \WooCommerce\Square\Utilities\Helper::enqueue_inline_script( 'wc-square-products-sync-filter', $javascript );
259 }
260
261
262 /**
263 * Filters products in admin edit screen by sync status with Square.
264 *
265 * @internal
266 *
267 * @since 2.0.0
268 *
269 * @param array $query_vars query variables
270 * @return array
271 */
272 public function filter_products_synced_with_square( $query_vars ) {
273 global $typenow;
274
275 // Nonce check not required, just filtering products, read-only action.
276 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
277 if ( 'product' === $typenow && isset( $_GET['product_type'] ) && 'synced-with-square' === $_GET['product_type'] ) {
278
279 // not really a product type, otherwise WooCommerce will handle it as such
280 unset( $query_vars['product_type'] );
281
282 if ( ! isset( $query_vars['tax_query'] ) ) {
283 $query_vars['tax_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
284 } else {
285 $query_vars['tax_query']['relation'] = 'AND';
286 }
287
288 $query_vars['tax_query'][] = array(
289 'taxonomy' => Product::SYNCED_WITH_SQUARE_TAXONOMY,
290 'field' => 'slug',
291 'terms' => array( 'yes' ),
292 );
293 }
294
295 return $query_vars;
296 }
297
298
299 /**
300 * Adds general product data options to a product metabox.
301 *
302 * @internal
303 *
304 * @since 2.0.0
305 */
306 public function add_product_data_fields() {
307 global $product_object;
308
309 if ( ! $product_object instanceof \WC_Product ) {
310 return;
311 }
312
313 // don't show fields if product sync is disabled
314 if ( ! wc_square()->get_settings_handler()->is_product_sync_enabled() ) {
315 return;
316 }
317
318 ?>
319 <div class="wc-square-sync-with-square options_group show_if_simple show_if_variable">
320 <?php
321
322 $selector = '_' . Product::SYNCED_WITH_SQUARE_TAXONOMY;
323 $value = Product::is_synced_with_square( $product_object ) ? 'yes' : 'no';
324 $errors = $this->check_product_sync_errors( $product_object );
325
326 $setting_label = wc_square()->get_settings_handler()->is_system_of_record_square() ? __( 'Update product data with Square data', 'woocommerce-square' ) : __( 'Send product data to Square', 'woocommerce-square' );
327
328 woocommerce_wp_checkbox(
329 array(
330 'id' => $selector,
331 'label' => __( 'Sync with Square', 'woocommerce-square' ),
332 'value' => $value,
333 'cbvalue' => 'yes',
334 'default' => 'no',
335 'description' => $setting_label,
336 'custom_attributes' => ! empty( $errors ) ? array( 'disabled' => 'disabled' ) : array(),
337 )
338 );
339
340 ?>
341 <p class="form-field wc-square-sync-with-square-errors">
342 <?php foreach ( $this->product_errors as $error_code => $error_message ) : ?>
343 <?php $styles = ! in_array( $error_code, array_keys( $errors ), true ) ? 'display:none; color:#A00;' : 'display:block; color:#A00;'; ?>
344 <span class="wc-square-sync-with-square-error <?php echo sanitize_html_class( $error_code ); ?>" style="<?php echo esc_attr( $styles ); ?>"><?php echo wp_kses_post( $this->format_product_error( $error_code, $product_object ) ); ?></span>
345 <?php endforeach; ?>
346 </p>
347
348 <input type="hidden" id="<?php echo esc_attr( Product::SQUARE_VARIATION_ID_META_KEY ); ?>" value="<?php echo esc_attr( $product_object->get_meta( Product::SQUARE_VARIATION_ID_META_KEY ) ); ?>" />
349
350 </div>
351 <?php
352 }
353
354
355 /**
356 * Outputs HTML with a dropdown field to mark a product to be synced with Square.
357 *
358 * @since 2.0.0
359 *
360 * @param bool $bulk whether the field is meant for bulk edit
361 */
362 private function output_synced_with_square_edit_field( $bulk = false ) {
363
364 ?>
365 <div class="inline-edit-group wc-square-sync-with-square">
366 <label>
367 <span class="title"><?php esc_html_e( 'Sync with Square?', 'woocommerce-square' ); ?></span>
368 <span class="input-text-wrap">
369 <select class="square-synced" name="<?php echo esc_attr( Product::SYNCED_WITH_SQUARE_TAXONOMY ); ?>">
370 <?php if ( true === $bulk ) : // in bulk actions there's the option to leave the value unchanged (or unset) ?>
371 <option value="">&mdash; <?php esc_html_e( 'No change', 'woocommerce-square' ); ?> &mdash;</option>
372 <?php endif; ?>
373 <option value="no"><?php esc_html_e( 'No', 'woocommerce-square' ); ?></option>
374 <option value="yes"><?php esc_html_e( 'Yes', 'woocommerce-square' ); ?></option>
375 </select>
376 </span>
377 </label>
378 <p class="form-field wc-square-sync-with-square-errors">
379 <?php foreach ( $this->product_errors as $error_code => $error_message ) : ?>
380 <?php $product_name_placeholder = __( 'This product', 'woocommerce-square' ); ?>
381 <?php $product_name = strtolower( $product_name_placeholder ); ?>
382 <span class="wc-square-sync-with-square-error <?php echo esc_attr( $error_code ); ?>" style="display:none; color:#A00;"><?php echo esc_html( sprintf( $error_message, $product_name ) ); ?></span>
383 <?php endforeach; ?>
384 </p>
385 </div>
386 <?php
387 }
388
389
390 /**
391 * Adds quick edit fields to the products screen.
392 *
393 * @internal
394 *
395 * @since 2.0.0
396 */
397 public function add_quick_edit_inputs() {
398
399 $this->output_synced_with_square_edit_field();
400 }
401
402
403 /**
404 * Adds bulk edit fields to the products screen.
405 *
406 * @internal
407 *
408 * @since 2.0.0
409 */
410 public function add_bulk_edit_inputs() {
411
412 $this->output_synced_with_square_edit_field( true );
413 }
414
415
416 /**
417 * In case Woo is the SOR, validates whether a product can be synced with Square and disable sync if not
418 *
419 * @since 2.0.8
420 *
421 * @param int $product_id the product ID
422 */
423 public function validate_product_update_and_sync( $product_id ) {
424 if ( ! wc_square()->get_settings_handler()->is_system_of_record_woocommerce() ) {
425 return;
426 }
427
428 $product = wc_get_product( $product_id );
429
430 if ( ! Product::is_synced_with_square( $product ) ) {
431 return;
432 }
433
434 $errors = $this->check_product_sync_errors( $product );
435
436 if ( ! empty( $errors ) ) {
437 // if there are errors, remove the link and display them
438 Product::unset_synced_with_square( $product );
439
440 foreach ( $errors as $error ) {
441 wc_square()->get_message_handler()->add_error( $error );
442 Records::set_record(
443 array(
444 'type' => 'alert',
445 'product_id' => $product_id,
446 'message' => $error,
447 )
448 );
449 }
450 } else {
451 $this->maybe_stage_product_for_sync( $product );
452 }
453 }
454
455 /**
456 * Stages a product for sync with Square on product save if Woo is the SOR and the product is set to 'synced with square'.
457 *
458 * @internal
459 *
460 * @since 2.0.0
461 *
462 * @param WC_Product $product the product object
463 */
464 public function maybe_stage_product_for_sync( $product ) {
465
466 if ( ! $product || ! Product::is_synced_with_square( $product ) || in_array( $product->get_id(), $this->products_to_sync, true ) ) {
467 return;
468 }
469
470 $in_progress = wc_square()->get_sync_handler()->get_job_in_progress();
471
472 if ( $in_progress ) {
473 // return early if an import that is updating existing products is in progress.
474 if ( isset( $in_progress->update_products_during_import ) && $in_progress->update_products_during_import ) {
475 return;
476 }
477
478 if ( in_array( $product->get_id(), $in_progress->product_ids, true ) ) {
479 return;
480 }
481 }
482
483 // the triggering action for this method can be called multiple times in a single request - keep track
484 // of product IDs that have been scheduled for sync here to avoid multiple syncs on the same request
485 $this->products_to_sync[] = $product->get_id();
486 }
487
488
489 /**
490 * Initializes a synchronization event for any staged products in this request.
491 *
492 * @internal
493 *
494 * @since 2.0.0
495 */
496 public function maybe_sync_staged_products() {
497
498 if ( ! defined( 'DOING_SQUARE_SYNC' ) && ! empty( $this->products_to_sync ) && wc_square()->get_settings_handler()->is_system_of_record_woocommerce() ) {
499
500 wc_square()->get_sync_handler()->start_manual_sync( $this->products_to_sync );
501 }
502 }
503
504
505 /**
506 * Removes a product from Square if it is deleted locally and Woo is the SOR.
507 *
508 * @since 2.0.0
509 *
510 * @param int $product_id the product ID
511 */
512 public function maybe_stage_products_for_deletion( $product_id ) {
513
514 if ( wc_square()->get_settings_handler()->is_system_of_record_woocommerce() ) {
515
516 $product = wc_get_product( $product_id );
517
518 if ( $product && Product::is_synced_with_square( $product ) ) {
519
520 // the triggering action for this method can be called multiple times in a single request - keep track
521 // of product IDs that have been scheduled for sync here to avoid multiple syncs on the same request
522 $this->products_to_delete[] = $product_id;
523 }
524 }
525 }
526
527
528 /**
529 * Deletes any products staged for remote deletion.
530 *
531 * @since 2.0.0
532 */
533 public function maybe_delete_staged_products() {
534
535 if ( ! empty( $this->products_to_delete ) && wc_square()->get_settings_handler()->is_system_of_record_woocommerce() ) {
536
537 wc_square()->get_sync_handler()->start_manual_deletion( $this->products_to_delete );
538 }
539 }
540
541
542 /**
543 * Sets a product's synced with Square status for quick/bulk edit action.
544 *
545 * @internal
546 *
547 * @since 2.0.0
548 *
549 * @param \WC_Product $product a product object
550 */
551 public function set_synced_with_square( $product ) {
552 // phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended
553 $posted_data_key = Product::SYNCED_WITH_SQUARE_TAXONOMY;
554
555 if ( 'woocommerce_product_bulk_edit_save' === current_action() ) {
556 $default_value = null; // in bulk actions this will preserve the existing setting if nothing is specified
557 } else {
558 $default_value = 'no'; // in individual products context, the value should be always an explicit yes or no
559 }
560
561 $square_synced = isset( $_REQUEST[ $posted_data_key ] ) && in_array( $_REQUEST[ $posted_data_key ], array( 'yes', 'no' ), true ) ? sanitize_key( $_REQUEST[ $posted_data_key ] ) : $default_value; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
562
563 if ( is_string( $square_synced ) ) {
564 $errors = $this->check_product_sync_errors( $product );
565 if ( 'no' === $square_synced || empty( $errors ) ) {
566 if ( 'yes' === $square_synced && $product->is_type( 'variable' ) && wc_square()->get_settings_handler()->is_inventory_sync_enabled() ) {
567 // if syncing inventory with Square, parent variable products don't manage stock
568 $product->set_manage_stock( false );
569 }
570
571 Product::set_synced_with_square( $product, $square_synced );
572 } elseif ( ! empty( $errors ) ) {
573 foreach ( $errors as $error ) {
574 wc_square()->get_message_handler()->add_error( $error );
575 }
576 }
577 }
578
579 // phpcs:enable WordPress.Security.NonceVerification.Missing
580 }
581
582
583 /**
584 * Updates Square sync status for a product upon saving.
585 *
586 * @internal
587 *
588 * @since 2.0.0
589 *
590 * @param \WC_Product $product product object
591 */
592 public function process_product_data( $product ) {
593
594 // don't process fields if product sync is disabled
595 if ( ! wc_square()->get_settings_handler()->is_product_sync_enabled() ) {
596 return;
597 }
598
599 // bail if no valid product found, if it's a variation, errors have already been output
600 if ( ! $product || ( $product instanceof \WC_Product_Variation || $product->is_type( 'product_variation' ) ) || ! empty( $this->output_errors[ $product->get_id() ] ) ) {
601 return;
602 }
603
604 $errors = array();
605 $posted_key = '_' . Product::SYNCED_WITH_SQUARE_TAXONOMY;
606 $set_synced = isset( $_POST[ $posted_key ] ) && 'yes' === sanitize_key( $_POST[ $posted_key ] ); // phpcs:ignore
607 $was_synced = Product::is_synced_with_square( $product );
608
609 // condition has unchanged
610 if ( ! $set_synced && ! $was_synced ) {
611 return;
612 }
613
614 if ( $set_synced || $was_synced ) {
615 if ( $set_synced && $product->is_type( 'variable' ) && wc_square()->get_settings_handler()->is_inventory_sync_enabled() ) {
616 // if syncing inventory with Square, parent variable products don't manage stock
617 $product->set_manage_stock( false );
618 }
619
620 // finally, set the product sync with Square flag
621 Product::set_synced_with_square( $product, $set_synced ? 'yes' : 'no' );
622 }
623 }
624
625
626 /**
627 * Adjusts a product's Square stock.
628 *
629 * @since 2.0.0
630 *
631 * @param \WC_Product $product product object
632 */
633 public function maybe_adjust_square_stock( $product ) {
634 // this is hooked in to general product object save, so scope to specifically saving products via the admin
635 if ( ! doing_action( 'wp_ajax_woocommerce_save_variations' ) && ! doing_action( 'woocommerce_admin_process_product_object' ) ) {
636 return;
637 }
638
639 // only send stock updates for Woo SOR
640 if ( ! wc_square()->get_settings_handler()->is_system_of_record_woocommerce() || ! wc_square()->get_settings_handler()->is_inventory_sync_enabled() ) {
641 return;
642 }
643
644 if ( ! $product instanceof \WC_Product || ! Product::is_synced_with_square( $product ) ) {
645 return;
646 }
647
648 $square_id = $product->get_meta( Product::SQUARE_VARIATION_ID_META_KEY );
649
650 // only send when the product has an associated Square ID
651 if ( ! $square_id ) {
652 return;
653 }
654
655 $data = $product->get_data();
656 $changes = $product->get_changes();
657 $change = 0;
658
659 if ( isset( $data['stock_quantity'], $changes['stock_quantity'] ) ) {
660 $change = (int) $changes['stock_quantity'] - $data['stock_quantity'];
661 }
662
663 if ( 0 !== $change ) {
664
665 try {
666
667 if ( $change > 0 ) {
668 wc_square()->get_api()->add_inventory( $square_id, $change );
669 } else {
670 wc_square()->get_api()->remove_inventory( $square_id, $change );
671 }
672 } catch ( \Exception $exception ) {
673
674 wc_square()->log( 'Could not adjust Square inventory for ' . $product->get_formatted_name() . '. ' . $exception->getMessage() );
675
676 $quantity = (float) $data['stock_quantity'];
677
678 // if the API request fails, set the product quantity back from whence it came
679 $product->set_stock_quantity( $quantity );
680 }
681 }
682 }
683
684
685 /**
686 * Prevents copying Square data when duplicating a product in admin.
687 *
688 * @internal
689 *
690 * @since 2.0.0
691 *
692 * @param \WC_Product $duplicated_product product duplicate
693 * @param \WC_Product $original_product product duplicated
694 */
695 public function handle_product_duplication( $duplicated_product, $original_product ) {
696
697 if ( Product::is_synced_with_square( $original_product ) ) {
698 Product::unset_synced_with_square( $duplicated_product );
699 }
700
701 $duplicated_product->delete_meta_data( Product::SQUARE_ID_META_KEY );
702 $duplicated_product->delete_meta_data( Product::SQUARE_VARIATION_ID_META_KEY );
703
704 if ( $duplicated_product->is_type( 'variable' ) ) {
705
706 foreach ( $duplicated_product->get_children() as $duplicated_variation_id ) {
707 $duplicated_product_variation = wc_get_product( $duplicated_variation_id );
708 if ( $duplicated_product_variation ) {
709
710 $duplicated_product_variation->delete_meta_data( Product::SQUARE_VARIATION_ID_META_KEY );
711 $duplicated_product_variation->save_meta_data();
712 }
713 }
714 }
715
716 $duplicated_product->save_meta_data();
717 }
718
719
720 /**
721 * Outputs an admin notice when a product was hidden from catalog upon a sync error.
722 *
723 * @internal
724 *
725 * @since 2.0.0
726 */
727 public function add_notice_product_hidden_from_catalog() {
728 global $current_screen, $post;
729
730 if ( $post && $current_screen && 'product' === $current_screen->id ) {
731
732 $product = wc_get_product( $post );
733
734 if ( $product && 'hidden' === $product->get_catalog_visibility() ) {
735
736 $product_id = $product->get_id();
737 $records = Records::get_records( array( 'product' => $product_id ) );
738
739 foreach ( $records as $record ) {
740
741 if ( $record->was_product_hidden() && $product_id === $record->get_product_id() ) {
742
743 wc_square()->get_message_handler()->add_warning(
744 sprintf(
745 /* translators: Placeholder: %1$s - date (localized), %2$s - time (localized), %3$s - opening <a> HTML link tag, %4$s closing </a> HTML link tag */
746 esc_html__( 'The product catalog visibility has been set to "hidden", as a matching product could not be found in Square on %1$s at %2$s. %3$sCheck sync records%4$s.', 'woocommerce-square' ),
747 date_i18n( wc_date_format(), $record->get_timestamp() ),
748 date_i18n( wc_time_format(), $record->get_timestamp() ),
749 '<a href="' . esc_url( add_query_arg( array( 'section' => 'update' ), wc_square()->get_settings_url() ) ) . '">',
750 '</a>'
751 )
752 );
753
754 break;
755 }
756 }
757 }
758 }
759 }
760
761
762 /**
763 * Check whether this product can be synced with Square
764 *
765 * @param \WC_Product $product product object
766 * @return array errors
767 */
768 private function check_product_sync_errors( \WC_Product $product ) {
769
770 $errors = array();
771 if ( ! Product::has_sku( $product ) ) {
772 if ( $product->is_type( 'variable' ) ) {
773 $errors['missing_variation_sku'] = $this->format_product_error( 'missing_variation_sku', $product );
774 } else {
775 $errors['missing_sku'] = $this->format_product_error( 'missing_sku', $product );
776 }
777 }
778
779 return $errors;
780 }
781
782
783 /**
784 * Formats product error message with product information
785 *
786 * @param string $error error identifier (e.g. 'missing_variation_sku' or 'missing_sku')
787 * @param \WC_Product $product product object
788 * @return string formatted error message
789 */
790 private function format_product_error( string $error, \WC_Product $product ) {
791 return sprintf(
792 $this->product_errors[ $error ],
793 Product::get_product_edit_link( $product )
794 );
795 }
796
797
798 /**
799 * Gets the plugin instance.
800 *
801 * @since 2.0.8
802 *
803 * @return Plugin
804 */
805 protected function get_plugin() {
806 return $this->plugin;
807 }
808
809 /**
810 * Adds the sync status column to the export columns.
811 *
812 * @param array $columns Array of columns
813 * @return array
814 */
815 public function add_sync_status_to_column( $columns ) {
816 $columns['wc_square_synced'] = __( 'Sync with Square', 'woocommerce-square' );
817 return $columns;
818 }
819
820 /**
821 * Sets column data.
822 *
823 * @param mixed $value Value of a column
824 * @param \WC_Product $product WooCommerce product object.
825 */
826 public function export_sync_status_taxonomy( $value, $product ) {
827 $terms = get_terms(
828 array(
829 'object_ids' => $product->get_ID(),
830 'taxonomy' => 'wc_square_synced',
831 )
832 );
833
834 if ( ! is_wp_error( $terms ) && is_array( $terms ) && count( $terms ) > 0 ) {
835 $term = $terms[0];
836
837 return $term->name;
838 }
839
840 return 'no';
841 }
842
843 /**
844 * Registers the sync status to the importer.
845 *
846 * @param array $columns Array of columns
847 * @return array
848 */
849 public function register_sync_status_for_importer( $columns ) {
850 $columns['wc_square_synced'] = __( 'Sync with Square', 'woocommerce-square' );
851 return $columns;
852 }
853
854 /**
855 * Add automatic mapping support for wc_square_synced column.
856 *
857 * @param array $columns Array of columns
858 * @return array
859 */
860 public function map_sync_status_column( $columns ) {
861 $columns[ __( 'Sync with Square', 'woocommerce-square' ) ] = 'wc_square_synced';
862 return $columns;
863 }
864
865 /**
866 * Imports square sync status.
867 *
868 * @param \WC_Product $product WooCommerce product.
869 * @param array Import data.
870 *
871 * @return array
872 */
873 public function import_sync_status( $product, $data ) {
874 if ( is_a( $product, 'WC_Product' ) ) {
875
876 if ( ! empty( $data['wc_square_synced'] ) ) {
877 switch ( $data['wc_square_synced'] ) {
878 case 'yes':
879 wp_set_object_terms( $product->get_id(), array( 'yes' ), 'wc_square_synced' );
880 break;
881
882 case 'no':
883 wp_set_object_terms( $product->get_id(), array( 'no' ), 'wc_square_synced' );
884 break;
885 }
886 }
887 }
888
889 return $product;
890 }
891
892 /**
893 * Returns array of product types that support a Square Gift Card.
894 *
895 * @since 4.2.0
896 * @return array
897 */
898 public static function get_gift_card_compatible_product_types() {
899 return array(
900 'simple',
901 'variable',
902 );
903 }
904
905 /**
906 * Add checkbox in product type options.
907 *
908 * @since 4.2.0
909 *
910 * @param array $actions Array of actions.
911 * @return array
912 */
913 public static function add_gift_card_checkbox( $actions ) {
914 global $product_object;
915
916 $wrapper_classes = array();
917
918 foreach ( self::get_gift_card_compatible_product_types() as $type ) {
919 $wrapper_classes[] = 'show_if_' . $type;
920 }
921
922 $wrapper_classes[] = 'hide_if_bundle';
923 $wrapper_classes[] = 'hide_if_composite';
924
925 $actions['square_gift_card'] = array(
926 'id' => Product::SQUARE_GIFT_CARD_KEY,
927 'wrapper_class' => implode( ' ', $wrapper_classes ),
928 'label' => __( 'Square Gift Card', 'woocommerce-square' ),
929 'description' => __( 'Square Gift cards are virtual products that can be purchased by customers and gifted to one or more recipients. Gift card code holders can redeem and use them as store credit.', 'woocommerce-square' ),
930 'default' => Product::is_gift_card( $product_object ) ? 'yes' : 'no',
931 );
932
933 return $actions;
934 }
935
936 /**
937 * Removes product settings tabs that are irrelevant to the Gift Card product type.
938 *
939 * @since 4.2.0
940 *
941 * @param array $tabs Array of tabs
942 * @return array
943 */
944 public static function filter_product_tabs( $tabs ) {
945 $tabs['shipping']['class'][] = 'hide_if_square_gift_card';
946
947 return $tabs;
948 }
949
950 /**
951 * Handles a gift card product on publish/update.
952 *
953 * @since 4.2.0
954 *
955 * @param \WC_Product $product WooCommerce product.
956 * @return void
957 */
958 public static function process_post_data( $product ) {
959 if ( ! $product->is_type( self::get_gift_card_compatible_product_types() ) ) {
960 return;
961 }
962
963 // phpcs:disable WordPress.Security.NonceVerification.Missing
964
965 // Is gift card.
966 if ( isset( $_POST[ Product::SQUARE_GIFT_CARD_KEY ] ) ) {
967 $product->update_meta_data( Product::SQUARE_GIFT_CARD_KEY, 'yes' );
968 $product->set_virtual( true );
969 $product->set_sold_individually( 'yes' );
970 $product->set_tax_status( 'none' );
971 } elseif ( 'yes' === $product->get_meta( Product::SQUARE_GIFT_CARD_KEY ) ) {
972 $product->delete_meta_data( Product::SQUARE_GIFT_CARD_KEY );
973 $product->set_virtual( false );
974 $product->set_sold_individually( 'no' );
975 $product->set_tax_status( 'taxable' );
976 }
977
978 // phpcs:enable WordPress.Security.NonceVerification.Missing
979 }
980
981 /**
982 * Adds email address input fields to sent gift cards.
983 *
984 * @since 4.2.0
985 */
986 public static function add_input_fields_to_gift_card_product() {
987 global $product;
988
989 if ( ! Product::is_gift_card( $product ) ) {
990 return;
991 }
992
993 $buying_option = isset( $_POST['square-gift-card-buying-option'] ) ? sanitize_text_field( wp_unslash( $_POST['square-gift-card-buying-option'] ) ) : false; // phpcs:ignore
994 $gan = isset( $_POST['square-gift-card-gan'] ) ? sanitize_text_field( wp_unslash( $_POST['square-gift-card-gan'] ) ) : false; // phpcs:ignore
995 $is_load_checked = 'load' === $buying_option && ! empty( $gan );
996
997 ?>
998
999 <div id="square-gift-card-buying-options">
1000 <label for="square-gift-card-buying-option__new">
1001 <input type="radio" name="square-gift-card-buying-option" id="square-gift-card-buying-option__new" <?php echo ! $is_load_checked ? 'checked' : ''; ?> value="new" />
1002 <?php esc_html_e( 'Buy a new gift card', 'woocommerce-square' ); ?>
1003 </label>
1004
1005 <label for="square-gift-card-buying-option__reload">
1006 <input type="radio" name="square-gift-card-buying-option" id="square-gift-card-buying-option__reload" <?php echo $is_load_checked ? 'checked' : ''; ?> value="load" />
1007 <?php esc_html_e( 'Add value to an existing gift card', 'woocommerce-square' ); ?>
1008 </label>
1009
1010 <div id="square-gift-card-email-to-wrapper" data-square-gift-card-activity="new" <?php echo $is_load_checked ? 'style="display: none;"' : ''; ?>>
1011 <div class="square-gift-card-field-wrapper">
1012 <label><?php esc_html_e( "Sender's name", 'woocommerce-square' ); ?><span class="wc-square-required-indicator">&nbsp;*</span></label>
1013 <input type="text" name="square-gift-card-sender-name" <?php echo $is_load_checked ? '' : 'required'; ?>/>
1014 </div>
1015
1016 <div class="square-gift-card-field-wrapper">
1017 <label><?php esc_html_e( "Recipient's email", 'woocommerce-square' ); ?><span class="wc-square-required-indicator">&nbsp;*</span></label>
1018 <input type="email" name="square-gift-card-send-to-email" <?php echo $is_load_checked ? '' : 'required'; ?>/>
1019 </div>
1020
1021 <div class="square-gift-card-field-wrapper">
1022 <label><?php esc_html_e( "Recipient's name", 'woocommerce-square' ); ?><span class="wc-square-required-indicator">&nbsp;*</span></label>
1023 <input type="text" name="square-gift-card-sent-to-first-name" id="square-gift-card-sent-to-first-name" <?php echo $is_load_checked ? '' : 'required'; ?>/>
1024 </div>
1025
1026 <div class="square-gift-card-field-wrapper">
1027 <label><?php esc_html_e( 'Message', 'woocommerce-square' ); ?></label>
1028 <textarea name="square-gift-card-sent-to-message" id="square-gift-card-sent-to-message" cols="30" rows="5"></textarea>
1029 </div>
1030 </div>
1031
1032 <div id="square-gift-card-gan-wrapper" data-square-gift-card-activity="load" <?php echo ! $is_load_checked ? 'style="display: none;"' : ''; ?>>
1033 <label><?php esc_html_e( 'Enter the Gift Card number', 'woocommerce-square' ); ?><span class="wc-square-required-indicator">&nbsp;*</span></label>
1034 <input type="text" pattern="[0-9]+" name="square-gift-card-gan" <?php echo $is_load_checked ? 'required' : 'disabled'; ?> placeholder="<?php esc_attr_e( 'Gift Card number', 'woocommerce-square' ); ?>" value="<?php echo $is_load_checked ? esc_attr( $gan ) : ''; ?>" />
1035 </div>
1036 </div>
1037 <?php
1038 }
1039
1040 /**
1041 * Disables taxes for a Square Gift Card.
1042 *
1043 * @since 4.2.0
1044 *
1045 * @param boolean $tax_status Indicates whether taxes should be applied to a WooCommerce product.
1046 * @param \WC_Product $product The WooCommerce product object.
1047 *
1048 * @return boolean
1049 */
1050 public function disable_taxes_for_gift_card_product( $tax_status, $product ) {
1051 if ( Product::is_gift_card( $product ) ) {
1052 return false;
1053 }
1054
1055 return $tax_status;
1056 }
1057
1058 /**
1059 * Disables shipping for a Square Gift Card.
1060 *
1061 * @since 4.2.0
1062 *
1063 * @param boolean $needs_shipping Indicates whether shipping is required for a WooCommerce product.
1064 * @param \WC_Product $product The WooCommerce product object.
1065 *
1066 * @return boolean
1067 */
1068 public function disable_shipping_for_gift_card_product( $needs_shipping, $product ) {
1069 if ( $product->is_type( 'variation' ) ) {
1070 $parent_id = $product->get_parent_id();
1071 $product = wc_get_product( $parent_id );
1072 }
1073
1074 if ( Product::is_gift_card( $product ) ) {
1075 return false;
1076 }
1077
1078 return $needs_shipping;
1079 }
1080
1081 /**
1082 * Disables coupons for a Square Gift Card.
1083 *
1084 * @since 4.2.0
1085 *
1086 * @param boolean $is_valid Indicates whether coupons are applicable to a WooCommerce product.
1087 * @param \WC_Product $product The WooCommerce product object.
1088 * @param \WC_Coupon $coupon The WooCommerce coupon object.
1089 * @param array $values Cart item values.
1090 *
1091 * @return boolean
1092 */
1093 public function disable_coupons_for_gift_card_product( $is_valid, $product, $coupon, $values ) {
1094 if ( Product::is_gift_card( $product ) ) {
1095 return false;
1096 }
1097
1098 return $is_valid;
1099 }
1100
1101 /**
1102 * Invalidate coupons when used with gift card products.
1103 *
1104 * @since 4.2.0
1105 *
1106 * @param bool $is_valid Whether a coupon is valid.
1107 * @param WC_Coupon $coupon The coupon being applied.
1108 * @return bool
1109 */
1110 public function coupon_is_valid( $is_valid, $coupon ) {
1111 if ( $is_valid ) {
1112 switch ( $coupon->get_discount_type() ) {
1113 case 'fixed_cart':
1114 if ( Gift_Card::cart_contains_gift_card() ) {
1115 throw new Exception( esc_html__( 'Sorry, this coupon is not applicable to gift card products.', 'woocommerce-square' ) );
1116 }
1117 break;
1118 }
1119 }
1120
1121 return $is_valid;
1122 }
1123
1124 /**
1125 * Adds Gift Card send-to email address to cart item.
1126 *
1127 * @since 4.2.0
1128 *
1129 * @param array $cart_item_data Array of cart items.
1130 * @param int $product_id Woo product ID.
1131 * @param int $variation_id Woo product variation ID.
1132 * @param int $quantity Quantity of a product added to cart.
1133 */
1134 public function add_cart_item_data( $cart_item_data, $product_id, $variation_id, $quantity ) {
1135 // phpcs:disable WordPress.Security.NonceVerification.Missing
1136 $product = wc_get_product( $product_id );
1137
1138 // Return if product is not a gift card product.
1139 if ( ! Product::is_gift_card( $product ) ) {
1140 return $cart_item_data;
1141 }
1142
1143 // Add email data.
1144 if ( Gift_Card::is_new() && isset( $_POST['square-gift-card-send-to-email'] ) && ! empty( $_POST['square-gift-card-send-to-email'] ) ) {
1145 $sender_name = isset( $_POST['square-gift-card-sender-name'] ) ? wc_clean( wp_unslash( $_POST['square-gift-card-sender-name'] ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1146 $email = isset( $_POST['square-gift-card-send-to-email'] ) ? is_email( wp_unslash( $_POST['square-gift-card-send-to-email'] ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1147 $first_name = isset( $_POST['square-gift-card-sent-to-first-name'] ) ? wc_clean( wp_unslash( $_POST['square-gift-card-sent-to-first-name'] ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1148 $message = isset( $_POST['square-gift-card-sent-to-message'] ) ? wc_clean( wp_unslash( $_POST['square-gift-card-sent-to-message'] ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1149
1150 if ( $sender_name ) {
1151 $cart_item_data['square-gift-card-sender-name'] = $sender_name;
1152 }
1153
1154 if ( $email ) {
1155 $cart_item_data['square-gift-card-send-to-email'] = $email;
1156 }
1157
1158 if ( $first_name ) {
1159 $cart_item_data['square-gift-card-sent-to-first-name'] = $first_name;
1160 }
1161
1162 if ( $message ) {
1163 $cart_item_data['square-gift-card-sent-to-message'] = $message;
1164 }
1165 }
1166
1167 // Add gift card number.
1168 if ( Gift_Card::is_load() && isset( $_POST['square-gift-card-gan'] ) ) {
1169 if ( empty( $_POST['square-gift-card-gan'] ) ) {
1170 throw new Exception( esc_html__( 'The gift card number field is empty.', 'woocommerce-square' ) );
1171 }
1172
1173 $cart_item_data['square-gift-card-gan'] = wc_clean( wp_unslash( $_POST['square-gift-card-gan'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1174
1175 $response = $this->get_plugin()->get_gateway()->get_api()->retrieve_gift_card_by_gan( $cart_item_data['square-gift-card-gan'] );
1176
1177 if ( ! $response->get_data() instanceof \Square\Models\RetrieveGiftCardFromGANResponse ) {
1178 throw new Exception( esc_html__( 'The gift card number is either invalid or does not exist.', 'woocommerce-square' ) );
1179 }
1180 }
1181
1182 // phpcs:enable WordPress.Security.NonceVerification.Missing
1183 return $cart_item_data;
1184 }
1185
1186 /**
1187 * Adds gift card meta to cart item.
1188 *
1189 * @since 4.2.0
1190 *
1191 * @param array $item_data Cart item data. Empty by default.
1192 * @param array $cart_item Cart item array.
1193 */
1194 public function add_sent_to_email_to_cart_item( $item_data, $cart_item ) {
1195 if ( ! empty( $cart_item['square-gift-card-sender-name'] ) ) {
1196 $item_data[] = array(
1197 'key' => esc_html__( "Sender's name", 'woocommerce-square' ),
1198 'value' => wc_clean( $cart_item['square-gift-card-sender-name'] ),
1199 );
1200 }
1201
1202 if ( ! empty( $cart_item['square-gift-card-send-to-email'] ) ) {
1203 $item_data[] = array(
1204 'key' => esc_html__( "Recipient's email", 'woocommerce-square' ),
1205 'value' => wc_clean( $cart_item['square-gift-card-send-to-email'] ),
1206 );
1207 }
1208
1209 if ( ! empty( $cart_item['square-gift-card-gan'] ) ) {
1210 $item_data[] = array(
1211 'key' => esc_html__( 'Gift card number', 'woocommerce-square' ),
1212 'value' => wc_clean( $cart_item['square-gift-card-gan'] ),
1213 );
1214 }
1215
1216 if ( ! empty( $cart_item['square-gift-card-sent-to-first-name'] ) ) {
1217 $item_data[] = array(
1218 'key' => esc_html__( "Recipient's name", 'woocommerce-square' ),
1219 'value' => wc_clean( $cart_item['square-gift-card-sent-to-first-name'] ),
1220 );
1221 }
1222
1223 if ( ! empty( $cart_item['square-gift-card-sent-to-message'] ) ) {
1224 $item_data[] = array(
1225 'key' => esc_html__( 'Message', 'woocommerce-square' ),
1226 'value' => wc_clean( $cart_item['square-gift-card-sent-to-message'] ),
1227 );
1228 }
1229
1230 return $item_data;
1231 }
1232
1233 /**
1234 * Adds a custom gift card placeholder image to products that are marked
1235 * as a gift card.
1236 *
1237 * @since 4.2.0
1238 *
1239 * @param string $image HTML string for image.
1240 * @param \WC_Product $product WooCommerce product.
1241 * @param string $size (default: 'woocommerce_thumbnail').
1242 *
1243 * @return string
1244 */
1245 public function filter_gift_card_product_featured_image_placeholder( $image, $product, $size ) {
1246 if ( ! self::should_use_default_gift_card_placeholder_image() ) {
1247 return $image;
1248 }
1249
1250 if ( has_post_thumbnail( $product->get_id() ) ) {
1251 return $image;
1252 }
1253
1254 if ( ! Product::is_gift_card( $product ) ) {
1255 return $image;
1256 }
1257
1258 $placeholder_image_id = self::get_gift_card_default_placeholder_id();
1259
1260 $default_attr = array(
1261 'class' => 'woocommerce-placeholder wp-post-image',
1262 'alt' => __( 'Placeholder', 'woocommerce-square' ),
1263 );
1264
1265 if ( wp_attachment_is_image( $placeholder_image_id ) ) {
1266 $image = wp_get_attachment_image(
1267 $placeholder_image_id,
1268 $size,
1269 false,
1270 $default_attr
1271 );
1272 }
1273
1274 return $image;
1275 }
1276
1277 /**
1278 * Adds a custom gift card placeholder image to product that are marked
1279 * as a gift card on the single product page.
1280 *
1281 * @since 4.2.0
1282 *
1283 * @param string $html HTML string for image.
1284 *
1285 * @return string
1286 */
1287 public function filter_single_product_featured_image_placeholder( $html ) {
1288 if ( ! self::should_use_default_gift_card_placeholder_image() ) {
1289 return $html;
1290 }
1291
1292 $product_id = get_the_ID();
1293
1294 if ( ! $product_id ) {
1295 return $html;
1296 }
1297
1298 $product = wc_get_product( $product_id );
1299
1300 if ( ! $product instanceof \WC_Product ) {
1301 return $html;
1302 }
1303
1304 if ( is_product() && has_post_thumbnail( $product->get_id() ) ) {
1305 return $html;
1306 }
1307
1308 if ( ! Product::is_gift_card( $product ) ) {
1309 return $html;
1310 }
1311
1312 $placeholder_image_id = self::get_gift_card_default_placeholder_id();
1313
1314 if ( wp_attachment_is_image( $placeholder_image_id ) ) {
1315 $html = wc_get_gallery_image_html( $placeholder_image_id, true );
1316 }
1317
1318 return $html;
1319 }
1320
1321 /**
1322 * Limits adding a single gift card product to cart per order.
1323 *
1324 * @since 4.2.0
1325 *
1326 * @param boolean $found_in_cart Indicates if the product is found in cart.
1327 * @param int $product_id The ID of the product being added to the cart.
1328 *
1329 * @return boolean
1330 */
1331 public function limit_gift_card_quantity_in_cart( $found_in_cart, $product_id ) {
1332 $product = wc_get_product( $product_id );
1333
1334 if ( Product::is_gift_card( $product ) && Gift_Card::cart_contains_gift_card() ) {
1335 $message = esc_html__( 'You can only add 1 gift card product to your cart per order.', 'woocommerce-square' );
1336 $wp_button_class = wc_wp_theme_get_element_class_name( 'button' ) ? ' ' . wc_wp_theme_get_element_class_name( 'button' ) : '';
1337 throw new Exception( sprintf( '<a href="%s" class="button wc-forward%s">%s</a> %s', esc_url( wc_get_cart_url() ), esc_attr( $wp_button_class ), esc_html__( 'View cart', 'woocommerce-square' ), esc_html( $message ) ) );
1338 }
1339
1340 return $found_in_cart;
1341 }
1342
1343 /**
1344 * Disables processing for a gift card product so that the order goes to
1345 * the `completed` state.
1346 *
1347 * @since 4.2.0
1348 *
1349 * @param boolean $virtual_downloadable_item Is a product virtual & downloadable item.
1350 * @param \WC_Product $product Current product being processed.
1351 */
1352 public function filter_needs_processing( $virtual_downloadable_item, $product ) {
1353 if ( Product::is_gift_card( $product ) ) {
1354 return false;
1355 }
1356
1357 return $virtual_downloadable_item;
1358 }
1359
1360 /**
1361 * Stage products for inventory sync when product is added to cart.
1362 *
1363 * @param string $cart_item_key Cart item key.
1364 * @param int $product_id Product ID.
1365 * @param int $quantity Quantity.
1366 * @param int $variation_id Variation ID.
1367 *
1368 * @since 4.1.0
1369 */
1370 public function maybe_stage_products_for_sync_inventory( $cart_item_key, $product_id, $quantity, $variation_id ) {
1371 if ( ! $product_id && ! $variation_id ) {
1372 return;
1373 }
1374
1375 // Bail if inventory sync is not enabled.
1376 if ( ! wc_square()->get_settings_handler()->is_inventory_sync_enabled() ) {
1377 return;
1378 }
1379
1380 if ( $variation_id ) {
1381 $product_id = $variation_id;
1382 }
1383
1384 // Bail if the product is not synced with Square.
1385 $product = wc_get_product( $product_id );
1386 if ( ! $product instanceof \WC_Product || ! Product::is_synced_with_square( $product ) ) {
1387 return;
1388 }
1389
1390 // Stage product for inventory sync.
1391 $this->products_to_inventory_sync[] = $product_id;
1392 }
1393
1394 /**
1395 * Sync product inventory of staged products.
1396 *
1397 * @since 4.1.0
1398 */
1399 public function maybe_sync_product_inventory() {
1400 if ( ! empty( $this->products_to_inventory_sync ) && wc_square()->get_settings_handler()->is_inventory_sync_enabled() ) {
1401 // Sync product inventory asynchronously.
1402 $async_request = $this->plugin->get_async_request_handler();
1403 if ( $async_request instanceof Async_Request ) {
1404 $async_request->data( array( 'product_ids' => $this->products_to_inventory_sync ) )->dispatch();
1405 }
1406 }
1407 }
1408
1409 /**
1410 * Add to cart text.
1411 *
1412 * @param string $text Add to cart text.
1413 * @param \WC_Product $product Product object.
1414 * @return string
1415 */
1416 public function gift_card_add_to_cart_text( $text, $product ) {
1417 if ( ! is_a( $product, 'WC_Product' ) ) {
1418 return $text;
1419 }
1420
1421 if ( is_single( $product->get_id() ) ) {
1422 return $text;
1423 }
1424
1425 if ( Product::is_gift_card( $product ) ) {
1426 return esc_html__( 'Buy Gift Card', 'woocommerce-square' );
1427 }
1428
1429 return $text;
1430 }
1431
1432 /**
1433 * Add to cart URL.
1434 *
1435 * @param string $url Add to cart URL.
1436 * @param \WC_Product $product Product object.
1437 * @return string
1438 */
1439 public function gift_card_add_to_cart_url( $url, $product ) {
1440 if ( ! is_a( $product, 'WC_Product' ) ) {
1441 return $url;
1442 }
1443
1444 if ( is_single( $product->get_id() ) ) {
1445 return $url;
1446 }
1447
1448 if ( Product::is_gift_card( $product ) ) {
1449 return get_permalink( $product->get_id() );
1450 }
1451
1452 return $url;
1453 }
1454
1455 /**
1456 * Determine if the Product has options.
1457 *
1458 * This will change the add to card button link to product page.
1459 *
1460 * @param boolean $has_options Whether the product has options.
1461 * @param \WC_Product $product Product object.
1462 */
1463 public function gift_card_product_has_options( $has_options, $product ) {
1464 if ( ! is_a( $product, 'WC_Product' ) ) {
1465 return $has_options;
1466 }
1467
1468 if ( Product::is_gift_card( $product ) ) {
1469 return true;
1470 }
1471
1472 return $has_options;
1473 }
1474
1475 /**
1476 * Determine if the Product supports a feature.
1477 *
1478 * Disable AJAX add to cart for gift card products.
1479 *
1480 * @param boolean $supports Whether the product supports a feature.
1481 * @param string $feature Feature.
1482 * @param \WC_Product $product Product object.
1483 * @return boolean
1484 */
1485 public function gift_card_product_supports( $supports, $feature, $product ) {
1486 if ( ! is_a( $product, 'WC_Product' ) ) {
1487 return $supports;
1488 }
1489
1490 if ( 'ajax_add_to_cart' === $feature && Product::is_gift_card( $product ) ) {
1491 return false;
1492 }
1493
1494 return $supports;
1495 }
1496
1497 /**
1498 * Adds a custom gift card placeholder image to products that are marked
1499 * as a gift card.
1500 *
1501 * @param string $image_id Image ID.
1502 * @param \WC_Product $product WooCommerce product.
1503 *
1504 * @return string
1505 */
1506 public function gift_card_product_image_id( $image_id, $product ) {
1507 if ( ! self::should_use_default_gift_card_placeholder_image() ) {
1508 return $image_id;
1509 }
1510
1511 if ( ! Product::is_gift_card( $product ) ) {
1512 return $image_id;
1513 }
1514
1515 if ( has_post_thumbnail( $product->get_id() ) ) {
1516 return $image_id;
1517 }
1518
1519 if ( empty( $image_id ) ) {
1520 $placeholder_image_id = self::get_gift_card_default_placeholder_id();
1521
1522 if ( $placeholder_image_id ) {
1523 $image_id = $placeholder_image_id;
1524 }
1525 }
1526
1527 return $image_id;
1528 }
1529
1530 /**
1531 * Returns true if a gift card product should use the provided
1532 * default placeholder image.
1533 *
1534 * @since 4.8.1
1535 *
1536 * @return bool
1537 */
1538 public static function should_use_default_gift_card_placeholder_image() {
1539 $settings = get_option( Gift_Card::SQUARE_PAYMENT_SETTINGS_OPTION_NAME, array() );
1540 $is_enabled = isset( $settings['enabled'] ) && 'yes' === $settings['enabled'];
1541
1542 if ( ! $is_enabled ) {
1543 return false;
1544 }
1545
1546 $should_use_placeholder = isset( $settings['is_default_placeholder'] ) && 'yes' === $settings['is_default_placeholder'];
1547
1548 if ( ! $should_use_placeholder ) {
1549 return false;
1550 }
1551
1552 return true;
1553 }
1554
1555 /**
1556 * Returns the default placeholder image ID for gift card products.
1557 *
1558 * @since 4.8.1
1559 *
1560 * @return int
1561 */
1562 public static function get_gift_card_default_placeholder_id() {
1563 $settings = get_option( Gift_Card::SQUARE_PAYMENT_SETTINGS_OPTION_NAME, array() );
1564
1565 return (int) ( $settings['placeholder_id'] ?? 0 );
1566 }
1567
1568 /**
1569 * Returns the default placeholder image URL for gift card products.
1570 *
1571 * @since 4.8.1
1572 *
1573 * @return string|bool
1574 */
1575 public static function get_gift_card_default_placeholder_url() {
1576 $placeholder_id = self::get_gift_card_default_placeholder_id();
1577
1578 if ( ! $placeholder_id ) {
1579 return '';
1580 }
1581
1582 $attachment = get_post( $placeholder_id );
1583
1584 if ( ! $attachment ) {
1585 return '';
1586 }
1587
1588 return wp_get_attachment_url( $attachment->ID );
1589 }
1590 }
1591