WooCommerce Square - Version 1.0.37

Version Description

  • 2019-04-16 =
  • Fix - Use correct assets loading scheme.
Download this release

Release Info

Developer ferdev
Plugin Icon 128x128 WooCommerce Square
Version 1.0.37
Comparing to
See all releases

Code changes from version 1.0.36 to 1.0.37

Files changed (34) hide show
  1. changelog.txt +3 -0
  2. languages/woocommerce-square.pot +2 -2
  3. readme.txt +8 -2
  4. trunk/assets/css/wc-square-admin-styles.css +46 -0
  5. trunk/assets/css/wc-square-frontend-styles.css +14 -0
  6. trunk/assets/js/wc-square-admin-scripts.js +124 -0
  7. trunk/assets/js/wc-square-admin-scripts.min.js +1 -0
  8. trunk/assets/js/wc-square-payments.js +155 -0
  9. trunk/assets/js/wc-square-payments.min.js +1 -0
  10. trunk/changelog.txt +163 -0
  11. trunk/includes/admin/class-wc-square-admin-integration.php +333 -0
  12. trunk/includes/admin/class-wc-square-admin-product-meta-box.php +155 -0
  13. trunk/includes/admin/class-wc-square-bulk-sync-handler.php +456 -0
  14. trunk/includes/admin/class-wc-square-privacy.php +110 -0
  15. trunk/includes/class-wc-square-client.php +388 -0
  16. trunk/includes/class-wc-square-connect.php +653 -0
  17. trunk/includes/class-wc-square-deactivation.php +31 -0
  18. trunk/includes/class-wc-square-install.php +78 -0
  19. trunk/includes/class-wc-square-inventory-poll.php +50 -0
  20. trunk/includes/class-wc-square-sync-from-square.php +411 -0
  21. trunk/includes/class-wc-square-sync-logger.php +52 -0
  22. trunk/includes/class-wc-square-sync-to-square-wp-hooks.php +304 -0
  23. trunk/includes/class-wc-square-sync-to-square.php +479 -0
  24. trunk/includes/class-wc-square-utils.php +974 -0
  25. trunk/includes/class-wc-square-wc-products.php +2461 -0
  26. trunk/includes/payment/class-wc-square-gateway.php +563 -0
  27. trunk/includes/payment/class-wc-square-payment-logger.php +29 -0
  28. trunk/includes/payment/class-wc-square-payments-connect.php +211 -0
  29. trunk/includes/payment/class-wc-square-payments.php +205 -0
  30. trunk/languages/woocommerce-square.pot +698 -0
  31. trunk/readme.txt +83 -0
  32. trunk/uninstall.php +24 -0
  33. trunk/woocommerce-square.php +421 -0
  34. woocommerce-square.php +3 -3
changelog.txt CHANGED
@@ -1,5 +1,8 @@
1
  *** WooCommerce Square Changelog ***
2
 
 
 
 
3
  = 1.0.36 - 2019-04-15 =
4
  * Tweak - WC 3.6 compatibility.
5
 
1
  *** WooCommerce Square Changelog ***
2
 
3
+ = 1.0.37 - 2019-04-16 =
4
+ * Fix - Use correct assets loading scheme.
5
+
6
  = 1.0.36 - 2019-04-15 =
7
  * Tweak - WC 3.6 compatibility.
8
 
languages/woocommerce-square.pot CHANGED
@@ -2,10 +2,10 @@
2
  # This file is distributed under the same license as the WooCommerce Square package.
3
  msgid ""
4
  msgstr ""
5
- "Project-Id-Version: WooCommerce Square 1.0.36\n"
6
  "Report-Msgid-Bugs-To: "
7
  "https://github.com/woocommerce/woocommerce-square/issues\n"
8
- "POT-Creation-Date: 2019-04-15 20:27:52+00:00\n"
9
  "MIME-Version: 1.0\n"
10
  "Content-Type: text/plain; charset=utf-8\n"
11
  "Content-Transfer-Encoding: 8bit\n"
2
  # This file is distributed under the same license as the WooCommerce Square package.
3
  msgid ""
4
  msgstr ""
5
+ "Project-Id-Version: WooCommerce Square 1.0.37\n"
6
  "Report-Msgid-Bugs-To: "
7
  "https://github.com/woocommerce/woocommerce-square/issues\n"
8
+ "POT-Creation-Date: 2019-04-16 14:35:33+00:00\n"
9
  "MIME-Version: 1.0\n"
10
  "Content-Type: text/plain; charset=utf-8\n"
11
  "Content-Transfer-Encoding: 8bit\n"
readme.txt CHANGED
@@ -2,9 +2,9 @@
2
  Contributors: automattic, royho, woothemes, bor0
3
  Tags: credit card, square, woocommerce, inventory sync
4
  Requires at least: 4.4
5
- Tested up to: 5.0
6
  Requires PHP: 5.6
7
- Stable tag: 1.0.35
8
  License: GPLv3
9
  License URI: https://www.gnu.org/licenses/gpl-3.0.html
10
 
@@ -57,6 +57,12 @@ If you get stuck, you can ask for help in the Plugin Forum.
57
 
58
  == Changelog ==
59
 
 
 
 
 
 
 
60
  = 1.0.35 - 2019-02-01 =
61
  * Fix - Idempotency key reuse issue when checking out.
62
 
2
  Contributors: automattic, royho, woothemes, bor0
3
  Tags: credit card, square, woocommerce, inventory sync
4
  Requires at least: 4.4
5
+ Tested up to: 5.1
6
  Requires PHP: 5.6
7
+ Stable tag: 1.0.37
8
  License: GPLv3
9
  License URI: https://www.gnu.org/licenses/gpl-3.0.html
10
 
57
 
58
  == Changelog ==
59
 
60
+ = 1.0.37 - 2019-04-16 =
61
+ * Fix - Use correct assets loading scheme.
62
+
63
+ = 1.0.36 - 2019-04-15 =
64
+ * Tweak - WC 3.6 compatibility.
65
+
66
  = 1.0.35 - 2019-02-01 =
67
  * Fix - Idempotency key reuse issue when checking out.
68
 
trunk/assets/css/wc-square-admin-styles.css ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .wc-square-progress-bar {
2
+ background-color: #23282d;
3
+ height: 15px;
4
+ padding: 5px;
5
+ width: 100%;
6
+ margin: 50px 0;
7
+ border-radius: 3px;
8
+ box-shadow: 0 1px 3px #000 inset, 0 1px 0 #444;
9
+ }
10
+
11
+ .wc-square-progress-bar span {
12
+ display: inline-block;
13
+ height: 100%;
14
+ width:1%;
15
+ border-radius: 3px;
16
+ box-shadow: 0 1px 0 rgba(255, 255, 255, .3) inset;
17
+ transition: width .4s ease-in-out;
18
+ background-color:#00a0d2;
19
+ }
20
+
21
+ .sq-input {
22
+ height: 60px;
23
+ }
24
+
25
+ .wc-square-connect-button, .wc-square-connect-button:active, .wc-square-connect-button:visited {
26
+ text-decoration: none;
27
+ display: inline-block;
28
+ border-radius: 3px;
29
+ background-color: #333333;
30
+ padding: 7px 10px;
31
+ height: 30px;
32
+ color: #ffffff;
33
+ box-shadow: 1px 1px 1px 0px #999999;
34
+ }
35
+
36
+ .wc-square-connect-button:hover, .wc-square-connect-button:focus {
37
+ color: #ffffff;
38
+ background-color: #444444;
39
+ }
40
+
41
+ .wc-square-connect-button span {
42
+ vertical-align: top;
43
+ display: inline-block;
44
+ padding-top: 7px;
45
+ padding-left: 10px;
46
+ }
trunk/assets/css/wc-square-frontend-styles.css ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .sq-input {
2
+ margin: 0 !important;
3
+ font-size: 1.387em;
4
+ background-color: #f2f2f2;
5
+ box-shadow: inset 0 1px 1px rgba(0,0,0,.125);
6
+ box-sizing: border-box;
7
+ }
8
+
9
+ @media only screen and (max-width: 500px) {
10
+ div.payment_box.payment_method_square .form-row.form-row-first, div.payment_box.payment_method_square .form-row.form-row-last {
11
+ float:none;
12
+ width:100%;
13
+ }
14
+ }
trunk/assets/js/wc-square-admin-scripts.js ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ jQuery( document ).ready( function( $ ) {
2
+ 'use strict';
3
+
4
+ // create namespace to avoid any possible conflicts
5
+ $.wc_square_admin = {
6
+ /**
7
+ * Loops through the sync process
8
+ *
9
+ * @since 1.0.0
10
+ * @version 1.0.14
11
+ * @param int process the current step in the loop
12
+ * @param string type the type of the AJAX call
13
+ */
14
+ sync: function( process, type ) {
15
+ var data = {
16
+ security: wc_square_local.ajaxSyncNonce,
17
+ process: parseInt( process, 10 ),
18
+ action: 'wc-to-square' === type ? 'wc_to_square' : 'square_to_wc'
19
+ };
20
+
21
+ $.ajax({
22
+ type: 'POST',
23
+ data: data,
24
+ url: wc_square_local.admin_ajax_url
25
+ }).done( function( response ) {
26
+ if ( 'done' === response.process ) {
27
+ // Triggers when all processing is done.
28
+ $( 'body' ).trigger( 'woocommerce_square_wc_to_square_sync_complete', [ response ] );
29
+
30
+ $( 'table.form-table' ).unblock();
31
+
32
+ $( '.wc-square-progress-bar span' ).css( { width: response.percentage + '%' } ).parent( '.wc-square-progress-bar' ).fadeOut( 'slow', function() {
33
+ alert( response.message );
34
+ });
35
+
36
+ } else {
37
+ $( '.wc-square-progress-bar span' ).css( { width: response.percentage + '%' } );
38
+
39
+ $.wc_square_admin.sync( parseInt( response.process, 10 ), response.type );
40
+ }
41
+ }).fail( function( jqXHR, textStatus, errorThrown ) {
42
+ $( 'table.form-table' ).unblock();
43
+ console.log( errorThrown );
44
+ alert( errorThrown );
45
+ });
46
+ },
47
+
48
+ init_sync_buttons: function() {
49
+ if ( ! wc_square_local.country_currency ) {
50
+ $( '#wc-to-square, #square-to-wc' ).attr( 'disabled', 'disabled' );
51
+ return;
52
+ }
53
+
54
+ $( '.woocommerce_page_wc-settings' ).on( 'click', '#wc-to-square, #square-to-wc', function( e ) {
55
+ e.preventDefault();
56
+
57
+ var confirmed = confirm( wc_square_local.i18n.confirm_sync );
58
+
59
+ if ( ! confirmed ) {
60
+ return;
61
+ }
62
+
63
+ var page = $( this ).parents( 'table.form-table' ),
64
+ progress_bar = $( '<div class="wc-square-progress-bar wc-square-stripes"><span class="step"></span></div>' );
65
+
66
+ // remove the progress bar on each trigger
67
+ $( '.wc-square-progress-bar' ).remove();
68
+
69
+ page.block({
70
+ message: null,
71
+ overlayCSS: {
72
+ background: '#fff',
73
+ opacity: 0.6
74
+ }
75
+ });
76
+
77
+ // add the progress bar
78
+ page.after( progress_bar );
79
+
80
+ $.wc_square_admin.sync( 0, $( this ).attr( 'id' ) );
81
+ });
82
+
83
+ },
84
+
85
+ init: function() {
86
+ this.init_sync_buttons();
87
+
88
+ $( document.body ).on( 'change', '#woocommerce_square_testmode', function() {
89
+ if ( $( this ).is( ':checked' ) ) {
90
+ $( '#woocommerce_square_application_id' ).parents( 'tr' ).eq(0).hide();
91
+ $( '#woocommerce_square_token' ).parents( 'tr' ).eq(0).hide();
92
+
93
+ $( '#woocommerce_square_sandbox_application_id' ).parents( 'tr' ).eq(0).show();
94
+ $( '#woocommerce_square_sandbox_token' ).parents( 'tr' ).eq(0).show();
95
+ } else {
96
+ $( '#woocommerce_square_application_id' ).parents( 'tr' ).eq(0).show();
97
+ $( '#woocommerce_square_token' ).parents( 'tr' ).eq(0).show();
98
+
99
+ $( '#woocommerce_square_sandbox_application_id' ).parents( 'tr' ).eq(0).hide();
100
+ $( '#woocommerce_square_sandbox_token' ).parents( 'tr' ).eq(0).hide();
101
+ }
102
+ });
103
+
104
+ $( '#woocommerce_square_testmode' ).trigger( 'change' );
105
+
106
+ $( document.body ).on( 'change', '#woocommerce_squareconnect_sync_products', function() {
107
+ if ( $( this ).is( ':checked' ) ) {
108
+ $( '#woocommerce_squareconnect_sync_categories' ).parents( 'tr' ).eq(0).show();
109
+ $( '#woocommerce_squareconnect_sync_inventory' ).parents( 'tr' ).eq(0).show();
110
+ $( '#woocommerce_squareconnect_sync_images' ).parents( 'tr' ).eq(0).show();
111
+ } else {
112
+ $( '#woocommerce_squareconnect_sync_categories' ).parents( 'tr' ).eq(0).hide();
113
+ $( '#woocommerce_squareconnect_sync_inventory' ).parents( 'tr' ).eq(0).hide();
114
+ $( '#woocommerce_squareconnect_sync_images' ).parents( 'tr' ).eq(0).hide();
115
+ }
116
+ });
117
+
118
+ $( '#woocommerce_squareconnect_sync_products' ).trigger( 'change' );
119
+ }
120
+ }; // close namespace
121
+
122
+ $.wc_square_admin.init();
123
+ // end document ready
124
+ });
trunk/assets/js/wc-square-admin-scripts.min.js ADDED
@@ -0,0 +1 @@
 
1
+ jQuery(document).ready(function(e){"use strict";e.wc_square_admin={sync:function(o,c){var r={security:wc_square_local.ajaxSyncNonce,process:parseInt(o,10),action:"wc-to-square"===c?"wc_to_square":"square_to_wc"};e.ajax({type:"POST",data:r,url:wc_square_local.admin_ajax_url}).done(function(o){"done"===o.process?(e("body").trigger("woocommerce_square_wc_to_square_sync_complete",[o]),e("table.form-table").unblock(),e(".wc-square-progress-bar span").css({width:o.percentage+"%"}).parent(".wc-square-progress-bar").fadeOut("slow",function(){alert(o.message)})):(e(".wc-square-progress-bar span").css({width:o.percentage+"%"}),e.wc_square_admin.sync(parseInt(o.process,10),o.type))}).fail(function(o,c,r){e("table.form-table").unblock(),console.log(r),alert(r)})},init_sync_buttons:function(){wc_square_local.country_currency?e(".woocommerce_page_wc-settings").on("click","#wc-to-square, #square-to-wc",function(o){if(o.preventDefault(),confirm(wc_square_local.i18n.confirm_sync)){var c=e(this).parents("table.form-table"),r=e('<div class="wc-square-progress-bar wc-square-stripes"><span class="step"></span></div>');e(".wc-square-progress-bar").remove(),c.block({message:null,overlayCSS:{background:"#fff",opacity:.6}}),c.after(r),e.wc_square_admin.sync(0,e(this).attr("id"))}}):e("#wc-to-square, #square-to-wc").attr("disabled","disabled")},init:function(){this.init_sync_buttons(),e(document.body).on("change","#woocommerce_square_testmode",function(){e(this).is(":checked")?(e("#woocommerce_square_application_id").parents("tr").eq(0).hide(),e("#woocommerce_square_token").parents("tr").eq(0).hide(),e("#woocommerce_square_sandbox_application_id").parents("tr").eq(0).show(),e("#woocommerce_square_sandbox_token").parents("tr").eq(0).show()):(e("#woocommerce_square_application_id").parents("tr").eq(0).show(),e("#woocommerce_square_token").parents("tr").eq(0).show(),e("#woocommerce_square_sandbox_application_id").parents("tr").eq(0).hide(),e("#woocommerce_square_sandbox_token").parents("tr").eq(0).hide())}),e("#woocommerce_square_testmode").trigger("change"),e(document.body).on("change","#woocommerce_squareconnect_sync_products",function(){e(this).is(":checked")?(e("#woocommerce_squareconnect_sync_categories").parents("tr").eq(0).show(),e("#woocommerce_squareconnect_sync_inventory").parents("tr").eq(0).show(),e("#woocommerce_squareconnect_sync_images").parents("tr").eq(0).show()):(e("#woocommerce_squareconnect_sync_categories").parents("tr").eq(0).hide(),e("#woocommerce_squareconnect_sync_inventory").parents("tr").eq(0).hide(),e("#woocommerce_squareconnect_sync_images").parents("tr").eq(0).hide())}),e("#woocommerce_squareconnect_sync_products").trigger("change")}},e.wc_square_admin.init()});
trunk/assets/js/wc-square-payments.js ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ (function ( $ ) {
2
+ 'use strict';
3
+
4
+ var wcSquarePaymentForm;
5
+
6
+ // create namespace to avoid any possible conflicts
7
+ $.wc_square_payments = {
8
+ init: function() {
9
+ // Checkout page
10
+ $( document.body ).on( 'updated_checkout', function() {
11
+ $.wc_square_payments.loadForm();
12
+ });
13
+
14
+ // Pay order form page
15
+ if ( $( 'form#order_review' ).length ) {
16
+ $.wc_square_payments.loadForm();
17
+ }
18
+
19
+ var custom_element = square_params.custom_form_trigger_element;
20
+
21
+ // custom click trigger for 3rd party forms that initially hides the payment form
22
+ // such as multistep checkout plugins
23
+ if ( custom_element.length ) {
24
+ $( document.body ).on( 'click', custom_element, function() {
25
+ $.wc_square_payments.loadForm();
26
+ });
27
+ }
28
+
29
+ // work around for iFrame not loading if elements being replaced is hidden
30
+ $( document.body ).on( 'click', '#payment_method_square', function() {
31
+ $( '.payment_box.payment_method_square' ).css( { 'display': 'block', 'visibility': 'visible', 'height': 'auto' } );
32
+ });
33
+ },
34
+ loadForm: function() {
35
+ if ( $( '#payment_method_square' ).length ) {
36
+ // work around for iFrame not loading if elements being replaced is hidden
37
+ if ( ! $( '#payment_method_square' ).is( ':checked' ) ) {
38
+ $( '.payment_box.payment_method_square' ).css( { 'display': 'block', 'visibility': 'hidden', 'height': '0' } );
39
+ }
40
+
41
+ // destroy the form and rebuild on each init
42
+ if ( 'object' === $.type( wcSquarePaymentForm ) ) {
43
+ wcSquarePaymentForm.destroy();
44
+ }
45
+
46
+ wcSquarePaymentForm = new SqPaymentForm({
47
+ env: square_params.environment,
48
+ applicationId: square_params.application_id,
49
+ inputClass: 'sq-input',
50
+ cardNumber: {
51
+ elementId: 'sq-card-number',
52
+ placeholder: square_params.placeholder_card_number
53
+ },
54
+ cvv: {
55
+ elementId: 'sq-cvv',
56
+ placeholder: square_params.placeholder_card_cvv
57
+ },
58
+ expirationDate: {
59
+ elementId: 'sq-expiration-date',
60
+ placeholder: square_params.placeholder_card_expiration
61
+ },
62
+ postalCode: {
63
+ elementId: 'sq-postal-code',
64
+ placeholder: square_params.placeholder_card_postal_code
65
+ },
66
+ callbacks: {
67
+ cardNonceResponseReceived: function( errors, nonce, cardData ) {
68
+ if ( errors ) {
69
+ var html = '';
70
+
71
+ html += '<ul class="woocommerce_error woocommerce-error">';
72
+
73
+ // handle errors
74
+ $( errors ).each( function( index, error ) {
75
+ html += '<li>' + error.message + '</li>';
76
+ });
77
+
78
+ html += '</ul>';
79
+
80
+ // append it to DOM
81
+ $( '.payment_method_square .sq-fieldset' ).eq(0).prepend( html );
82
+ } else {
83
+ var $form = $( 'form.woocommerce-checkout, form#order_review' );
84
+
85
+ // inject nonce to a hidden field to be submitted
86
+ $form.append( '<input type="hidden" class="square-nonce" name="square_nonce" value="' + nonce + '" />' );
87
+
88
+ $form.submit();
89
+ }
90
+ },
91
+
92
+ paymentFormLoaded: function() {
93
+ wcSquarePaymentForm.setPostalCode( $( '#billing_postcode' ).val() );
94
+ },
95
+
96
+ unsupportedBrowserDetected: function() {
97
+ var html = '';
98
+
99
+ html += '<ul class="woocommerce_error woocommerce-error">';
100
+ html += '<li>' + square_params.unsupported_browser + '</li>';
101
+ html += '</ul>';
102
+
103
+ // append it to DOM
104
+ $( '.payment_method_square .sq-fieldset' ).eq(0).prepend( html );
105
+ }
106
+ },
107
+ inputStyles: $.parseJSON( square_params.payment_form_input_styles )
108
+ });
109
+
110
+ wcSquarePaymentForm.build();
111
+
112
+ // when checkout form is submitted on checkout page
113
+ $( 'form.woocommerce-checkout' ).on( 'checkout_place_order_square', function( event ) {
114
+ // remove any error messages first
115
+ $( '.payment_method_square .woocommerce-error' ).remove();
116
+
117
+ if ( $( '#payment_method_square' ).is( ':checked' ) && $( 'input.square-nonce' ).length === 0 ) {
118
+ wcSquarePaymentForm.requestCardNonce();
119
+
120
+ return false;
121
+ }
122
+
123
+ return true;
124
+ });
125
+
126
+ // when checkout form is submitted on pay order page
127
+ $( 'form#order_review' ).on( 'submit', function( event ) {
128
+ // remove any error messages first
129
+ $( '.payment_method_square .woocommerce-error' ).remove();
130
+
131
+ if ( $( '#payment_method_square' ).is( ':checked' ) && $( 'input.square-nonce' ).length === 0 ) {
132
+ wcSquarePaymentForm.requestCardNonce();
133
+
134
+ return false;
135
+ }
136
+
137
+ return true;
138
+ });
139
+
140
+ $( document.body ).on( 'checkout_error', function() {
141
+ $( 'input.square-nonce' ).remove();
142
+ });
143
+
144
+ // work around for iFrame not loading if elements being replaced is hidden
145
+ setTimeout( function() {
146
+ if ( ! $( '#payment_method_square' ).is( ':checked' ) ) {
147
+ $( '.payment_box.payment_method_square' ).css( { 'display': 'none', 'visibility': 'visible', 'height': 'auto' } );
148
+ }
149
+ }, 1000 );
150
+ }
151
+ }
152
+ }; // close namespace
153
+
154
+ $.wc_square_payments.init();
155
+ }( jQuery ) );
trunk/assets/js/wc-square-payments.min.js ADDED
@@ -0,0 +1 @@
 
1
+ !function(e){"use strict";var o;e.wc_square_payments={init:function(){e(document.body).on("updated_checkout",function(){e.wc_square_payments.loadForm()}),e("form#order_review").length&&e.wc_square_payments.loadForm();var o=square_params.custom_form_trigger_element;o.length&&e(document.body).on("click",o,function(){e.wc_square_payments.loadForm()}),e(document.body).on("click","#payment_method_square",function(){e(".payment_box.payment_method_square").css({display:"block",visibility:"visible",height:"auto"})})},loadForm:function(){e("#payment_method_square").length&&(e("#payment_method_square").is(":checked")||e(".payment_box.payment_method_square").css({display:"block",visibility:"hidden",height:"0"}),"object"===e.type(o)&&o.destroy(),(o=new SqPaymentForm({env:square_params.environment,applicationId:square_params.application_id,inputClass:"sq-input",cardNumber:{elementId:"sq-card-number",placeholder:square_params.placeholder_card_number},cvv:{elementId:"sq-cvv",placeholder:square_params.placeholder_card_cvv},expirationDate:{elementId:"sq-expiration-date",placeholder:square_params.placeholder_card_expiration},postalCode:{elementId:"sq-postal-code",placeholder:square_params.placeholder_card_postal_code},callbacks:{cardNonceResponseReceived:function(o,r,a){if(o){var t="";t+='<ul class="woocommerce_error woocommerce-error">',e(o).each(function(e,o){t+="<li>"+o.message+"</li>"}),t+="</ul>",e(".payment_method_square .sq-fieldset").eq(0).prepend(t)}else{var n=e("form.woocommerce-checkout, form#order_review");n.append('<input type="hidden" class="square-nonce" name="square_nonce" value="'+r+'" />'),n.submit()}},paymentFormLoaded:function(){o.setPostalCode(e("#billing_postcode").val())},unsupportedBrowserDetected:function(){var o="";o+='<ul class="woocommerce_error woocommerce-error">',o+="<li>"+square_params.unsupported_browser+"</li>",o+="</ul>",e(".payment_method_square .sq-fieldset").eq(0).prepend(o)}},inputStyles:e.parseJSON(square_params.payment_form_input_styles)})).build(),e("form.woocommerce-checkout").on("checkout_place_order_square",function(r){return e(".payment_method_square .woocommerce-error").remove(),!e("#payment_method_square").is(":checked")||0!==e("input.square-nonce").length||(o.requestCardNonce(),!1)}),e("form#order_review").on("submit",function(r){return e(".payment_method_square .woocommerce-error").remove(),!e("#payment_method_square").is(":checked")||0!==e("input.square-nonce").length||(o.requestCardNonce(),!1)}),e(document.body).on("checkout_error",function(){e("input.square-nonce").remove()}),setTimeout(function(){e("#payment_method_square").is(":checked")||e(".payment_box.payment_method_square").css({display:"none",visibility:"visible",height:"auto"})},1e3))}},e.wc_square_payments.init()}(jQuery);
trunk/changelog.txt ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *** WooCommerce Square Changelog ***
2
+
3
+ = 1.0.37 - 2019-04-16 =
4
+ * Fix - Use correct assets loading scheme.
5
+
6
+ = 1.0.36 - 2019-04-15 =
7
+ * Tweak - WC 3.6 compatibility.
8
+
9
+ = 1.0.35 - 2019-02-01 =
10
+ * Fix - Idempotency key reuse issue when checking out.
11
+
12
+ = 1.0.34 - 2018-11-07 =
13
+ * Update - Fieldset tag to div tag in payment box to prevent unwanted styling.
14
+ * Fix - Provide unique idempotency ID to the order instead of random unique number.
15
+ * Update - WP tested up to version 5.0
16
+
17
+ = 1.0.33 - 2018-09-27 =
18
+ * Update - WC tested up to version 3.5
19
+
20
+ = 1.0.32 - 2018-08-23 =
21
+ * Fix - UK/GB localed does not support Diners/Discover, so do not show these brands on checkout.
22
+
23
+ = 1.0.31 - 2018-06-05 =
24
+ * Fix - SSL notice in admin shows even though SSL is provided.
25
+ * Update - Send WC active prices to Square instead of tax exclusive.
26
+ * Add - Hook `wc_square_sync_to_square_price` to filter prices sent to Square.
27
+
28
+ = 1.0.30 - 2018-05-23 =
29
+ * Update - WC tested up to version 3.4
30
+ * Update - Privacy policy notification.
31
+ * Update - Export/erasure hooks added.
32
+
33
+ = 1.0.29 - 2018-04-17 =
34
+ * Fix - If more than 1000 items, next batch is ignored.
35
+ * Fix - Wrong setting used when syncing product.
36
+ * Fix - Japan locale does not support Diners/Discover, so do not show these brands on checkout.
37
+ * Update - WC tested up to version 3.4
38
+
39
+ 2018-04-04 - version 1.0.28
40
+ * Fix - Double inventory sync for newly created products.
41
+ * Fix - Infinite loop with pagination, where Square keeps sending the same link.
42
+ * Fix - Remove usage of deprecated jQuery method.
43
+ * Fix - Sync job should not be sending anything if there are no stock changes.
44
+ * Fix - Trigger to decrease stock upon purchase not working
45
+ * Tweak - Add logging and skip sync in case store's currency/country is not allowed.
46
+ * Tweak - Improve debug logging when fetching inventory
47
+ * Tweak - Inventory reduction in Square will be treated as stock sale.
48
+
49
+ 2018-02-19 - version 1.0.27
50
+ * Fix - In some cases request timeouts are not set long enough causing timeout errors.
51
+ * Fix - Square to WC sync sets products with 0 quantity to In Stock.
52
+ * Fix - Potential issue with Square to WC sync when response is not an array.
53
+ * Fix - When description is more than 4095 characters, sync fails to Square.
54
+ * Fix - Add checks to product object before scheduling an event to prevent errors in cron job.
55
+ * Fix - Add check to prevent auto inventory poll from running when a manual sync is in progress.
56
+
57
+ 2018-01-29 - version 1.0.26
58
+ * Fix - Call to undefined method causes error.
59
+
60
+ 2018-01-29 - version 1.0.25
61
+ * Tweaks - Error handling.
62
+ * Add - Public Release on .org
63
+
64
+ 2017-12-13 - version 1.0.24
65
+ * Fix - In some cases rounding issues occur causing payment unable to process.
66
+ * Update - WC tested up to version.
67
+ * Add - Readme.txt.
68
+
69
+ 2017-11-10 - version 1.0.23
70
+ * Add - WC minimum requirements in header.
71
+ * Fix - WC Product sometimes is not a proper object preventing sync.
72
+ * Fix - PHP 7 notice with amount not cast an int.
73
+
74
+ 2017-09-06 - version 1.0.22
75
+ * Tweak - Increase inventory polling cache and add timeout limit.
76
+ * Fix - CRON scheduling time not using UTC causes delay in CRON job.
77
+
78
+ 2017-07-26 - version 1.0.21
79
+ * Fix - Non decimal place price such as Japanese Yen is not formatted properly when syncing Square to WC.
80
+
81
+ 2017-06-29 - version 1.0.20
82
+ * Fix - Schedule sale price not updating on Square when sale is over.
83
+ * Fix - Pass price to Square tax exclusive instead of inclusive.
84
+ * Fix - Create customer error on payments when phone number is not provided.
85
+ * Update - Settings location description to be more clear.
86
+
87
+ 2017-06-06 - version 1.0.19
88
+ * Fix - Sync currency amounts that do not contain decimals correctly.
89
+
90
+ 2017-05-19 - version 1.0.18
91
+ * Fix - Better logic of sale price on WC when syncing so it doesn't override regular price unintented.
92
+ * Fix - If image exists for a product, don't sync the image to prevent duplicated images being created.
93
+ * Fix - Enable status not showing for Square payments in gateway list.
94
+
95
+ 2017-05-08 - version 1.0.17
96
+ * Fix - WC 3.0 debug tools compatibility.
97
+ * Add - Support for Japan market.
98
+
99
+ 2017-03-31 - version 1.0.16
100
+ * Add - Confirmation message before bulk sync.
101
+ * Add - Cron schedules for when product stock changes instead of firing off immediately.
102
+
103
+ 2017-03-27 - version 1.0.15
104
+ * Update - Additional updates for WooCommerce 3.0.0 compatibility.
105
+ * Add - Support for UK market.
106
+
107
+ 2017-03-13 - version 1.0.14
108
+ * Fix - When multiple notify emails are set, causes 500 server error.
109
+ * Fix - Sync issues when syncing large datasets.
110
+ * Fix - Sync item with no sku sometimes gets corrupted with other item data.
111
+ * Update - WooCommerce 3.0.0 compatibility.
112
+
113
+ 2017-01-17 - version 1.0.13
114
+ * Add - Support for Australian markets.
115
+
116
+ 2017-01-09 - version 1.0.12
117
+ * Fix - When syncing inventory WC to Square, stock level becomes zero.
118
+
119
+ 2016-10-26 - version 1.0.11
120
+ * Fix - When WC API is used, it can cause duplicate WC API Exception.
121
+
122
+ 2016-10-14 - version 1.0.10
123
+ * Update - Make sure Square only works for US and CA merchants.
124
+
125
+ 2016-09-12 - version 1.0.9
126
+ * Fix - Normalize price when syncing Square to WC to prevent errors.
127
+ * Add - Option to disable sync per product.
128
+
129
+ 2016-09-08 - version 1.0.8
130
+ * Add - WC Products CRUD to replace REST API for creating products to prevent interference with wc-api.
131
+ * Add - Filter to add custom DOM elements for payments to render payment form "woocommerce_square_payment_form_trigger_element".
132
+ * Fix - Payments token expiration was not handled correctly due to using API v2.
133
+
134
+ 2016-08-30 - version 1.0.7
135
+ * Tweak - Replace all wp_remote_requests with raw cURL.
136
+ * Fix - In IE, the browser won't replace iFrame form fields when it is hidden.
137
+
138
+ 2016-08-27 - version 1.0.6
139
+ * Add - Clear cache/transient tool in system status->tool.
140
+ * Fix - Prevent infinite loop when polling Square inventory.
141
+
142
+ 2016-08-25 - version 1.0.5
143
+ * Fix - Image not syncing when using wp_remote_request. Changed to raw cURL.
144
+
145
+ 2016-08-19 - version 1.0.4
146
+ * Tweak - Payment form field styles.
147
+ * Add - woocommerce_square_payment_input_styles filter to allow manipulation of the form styles.
148
+ * Fix - When duplicating product in WC, it replaces the original product on Square.
149
+ * Fix - In FireFox, the browser won't replace iFrame form fields when it is hidden.
150
+
151
+ 2016-08-16 - version 1.0.3
152
+ * Fix - When duplicating product in WC, it replaces the original product on Square.
153
+ * Fix - oAuth tokens were not renewing properly.
154
+
155
+ 2016-08-04 - version 1.0.2
156
+ * Update - HTTP protocol to version 1.1 to prevent timeouts.
157
+
158
+ 2016-08-01 - version 1.0.1
159
+ * Tweak - Make credit card form fields more responsive in mobile devices.
160
+ * Fix - Square list endpoints may return duplicates, so put in place a check for that.
161
+
162
+ 2016-07-26 - version 1.0.0
163
+ * First Release
trunk/includes/admin/class-wc-square-admin-integration.php ADDED
@@ -0,0 +1,333 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) {
3
+ exit; // Exit if accessed directly
4
+ }
5
+
6
+ /**
7
+ * Class WC_Square_Integration
8
+ *
9
+ * Settings CRUD for the extension.
10
+ */
11
+ class WC_Square_Integration extends WC_Integration {
12
+ private $oauth_connect_url;
13
+ private $merchant_access_token;
14
+
15
+ /**
16
+ * Constructor
17
+ */
18
+ public function __construct() {
19
+ $this->id = 'squareconnect';
20
+ $this->method_title = __( 'Square', 'woocommerce-square' );
21
+ $this->method_description = __( 'Connect with Square to start syncing your products and inventory and also accept credit card and debit card payments in your checkout.', 'woocommerce-square' );
22
+ $this->merchant_access_token = get_option( 'woocommerce_square_merchant_access_token' );
23
+
24
+ $this->maybe_save_token();
25
+ $this->maybe_delete_token();
26
+
27
+ $this->init_form_fields();
28
+ $this->init_settings();
29
+
30
+ $this->oauth_connect_url = 'https://connect.woocommerce.com/login/square';
31
+
32
+ if ( WC_SQUARE_ENABLE_STAGING ) {
33
+ $this->oauth_connect_url = 'https://connect.woocommerce.com/login/squaresandbox';
34
+ }
35
+
36
+ add_action( 'woocommerce_update_options_integration_' . $this->id, array( $this, 'process_admin_options' ) );
37
+ add_filter( 'woocommerce_settings_api_form_fields_' . $this->id, array( $this, 'maybe_render_locations' ) );
38
+ }
39
+
40
+ /**
41
+ * Adds the form fields for the settings
42
+ *
43
+ * @access public
44
+ * @since 1.0.0
45
+ * @version 1.0.0
46
+ * @return bool
47
+ */
48
+ public function init_form_fields() {
49
+ $application_dashboard_url = 'https://connect.squareup.com/apps';
50
+
51
+ $form_fields = array(
52
+ 'location' => array(
53
+ 'title' => __( 'Business Location', 'woocommerce-square' ),
54
+ 'type' => 'select',
55
+ 'description' => __( 'Select the location you wish to link to this site. You must have <a href="https://squareup.com/dashboard/locations" target="_blank">locations</a> set in Square and approved.', 'woocommerce-square' ),
56
+ 'desc_tip' => false,
57
+ 'options' => array( '' => __( 'Select a Location', 'woocommerce-square' ) ),
58
+ 'disabled' => true,
59
+ ),
60
+ 'sync_email' => array(
61
+ 'title' => __( 'Notification Email', 'woocommerce-square' ),
62
+ 'type' => 'text',
63
+ 'description' => __( 'Enter the email(s) to be notified when a sync has ended. Separate each email with a comma.', 'woocommerce-square' ),
64
+ 'desc_tip' => false,
65
+ 'default' => '',
66
+ 'placeholder' => get_option( 'admin_email' ),
67
+ ),
68
+ 'logging' => array(
69
+ 'title' => __( 'Logging', 'woocommerce-square' ),
70
+ 'label' => __( 'Log debug messages', 'woocommerce-square' ),
71
+ 'type' => 'checkbox',
72
+ 'description' => __( 'Save debug messages to the WooCommerce System Status log.', 'woocommerce-square' ),
73
+ 'default' => 'no',
74
+ ),
75
+ 'sync_title' => array(
76
+ 'title' => __( 'Synchronization', 'woocommerce-square' ),
77
+ 'type' => 'title',
78
+ 'description' => __( 'Determine which aspects of your product catalog to synchronize between WooCommerce and Square. Products need to have SKUs set for each variation.', 'woocommerce-square' ),
79
+ ),
80
+ 'sync_products' => array(
81
+ 'title' => __( 'Enabled', 'woocommerce-square' ),
82
+ 'type' => 'checkbox',
83
+ 'label' => __( 'Products', 'woocommerce-square' ),
84
+ 'description' => __( 'Basic Product information will be synced, excluding Categories and Inventory.', 'woocommerce-square' ),
85
+ 'default' => 'yes',
86
+ ),
87
+ 'sync_categories' => array(
88
+ 'title' => __( 'Include Categories', 'woocommerce-square' ),
89
+ 'type' => 'checkbox',
90
+ 'label' => __( 'Sync Categories', 'woocommerce-square' ),
91
+ 'description' => __( 'Categories will sync on creation or update, and Products will have their Categories synced.', 'woocommerce-square' ),
92
+ 'default' => 'yes',
93
+ ),
94
+ 'sync_inventory' => array(
95
+ 'title' => __( 'Include Inventory', 'woocommerce-square' ),
96
+ 'type' => 'checkbox',
97
+ 'label' => __( 'Sync Inventory', 'woocommerce-square' ),
98
+ 'description' => __( 'Inventory will sync on manual update or after a Product is ordered.', 'woocommerce-square' ),
99
+ 'default' => 'yes',
100
+ ),
101
+ 'sync_images' => array(
102
+ 'title' => __( 'Include Images', 'woocommerce-square' ),
103
+ 'type' => 'checkbox',
104
+ 'label' => __( 'Sync Images', 'woocommerce-square' ),
105
+ 'description' => __( 'Product Image will be synced.', 'woocommerce-square' ),
106
+ 'default' => 'yes',
107
+ ),
108
+ 'inventory_polling' => array(
109
+ 'title' => __( 'Square Inventory Sync', 'woocommerce-square' ),
110
+ 'type' => 'checkbox',
111
+ 'label' => __( 'Enable', 'woocommerce-square' ),
112
+ 'description' => __( 'For automatic inventory syncing from Square to WooCommerce, this needs to be enabled. It will poll the inventory from Square on an hourly basis.', 'woocommerce-square' ),
113
+ 'default' => 'no',
114
+ ),
115
+ );
116
+
117
+ $this->form_fields = apply_filters( 'woocommerce_square_integration_settings_args', $form_fields );
118
+
119
+ return true;
120
+ }
121
+
122
+ /**
123
+ * Add in our own custom options
124
+ *
125
+ * @access public
126
+ * @since 1.0.0
127
+ * @version 1.0.0
128
+ * @return bool
129
+ */
130
+ public function admin_options() {
131
+ $current_user = wp_get_current_user();
132
+
133
+ $redirect_url = add_query_arg(
134
+ array(
135
+ 'page' => 'wc-settings',
136
+ 'tab' => 'integration',
137
+ 'section' => $this->id,
138
+ ),
139
+ admin_url( 'admin.php' )
140
+ );
141
+
142
+ $redirect_url = wp_nonce_url( $redirect_url, 'connect_square', 'wc_square_token_nonce' );
143
+
144
+ $query_args = array(
145
+ 'redirect' => urlencode( urlencode( $redirect_url ) ),
146
+ 'scopes' => 'MERCHANT_PROFILE_READ,PAYMENTS_READ,PAYMENTS_WRITE,CUSTOMERS_READ,CUSTOMERS_WRITE,SETTLEMENTS_READ,ITEMS_READ,ITEMS_WRITE',
147
+ );
148
+
149
+ $production_connect_url = add_query_arg( $query_args, $this->oauth_connect_url );
150
+
151
+ $disconnect_url = add_query_arg(
152
+ array(
153
+ 'page' => 'wc-settings',
154
+ 'tab' => 'integration',
155
+ 'section' => $this->id,
156
+ 'disconnect_square' => 1,
157
+ ),
158
+ admin_url( 'admin.php' )
159
+ );
160
+
161
+ $disconnect_url = wp_nonce_url( $disconnect_url, 'disconnect_square', 'wc_square_token_nonce' );
162
+
163
+ echo '<h2>' . esc_html( $this->get_method_title() ) . '</h2>';
164
+ echo wp_kses_post( wpautop( $this->get_method_description() ) );
165
+ echo '<div><input type="hidden" name="section" value="' . esc_attr( $this->id ) . '" /></div>';
166
+ ?>
167
+
168
+ <table class="form-table">
169
+ <tbody>
170
+ <tr>
171
+ <th>
172
+ <?php esc_html_e( 'Connect/Disconnect', 'woocommerce-square' ); ?>
173
+ </th>
174
+ <td>
175
+ <?php if ( ! empty( $this->merchant_access_token ) ) { ?>
176
+ <a href="<?php echo esc_attr( $disconnect_url ); ?>" class='button-primary'>
177
+ <?php echo esc_html__( 'Disconnect from Square', 'woocommerce-square' ); ?>
178
+ </a>
179
+ <?php } else { ?>
180
+ <a href="<?php echo esc_attr( $production_connect_url ); ?>" class="wc-square-connect-button">
181
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 44 44" width="30" height="30">
182
+ <path fill="#FFFFFF" d="M36.65 0h-29.296c-4.061 0-7.354 3.292-7.354 7.354v29.296c0 4.062 3.293 7.354 7.354 7.354h29.296c4.062 0 7.354-3.292 7.354-7.354v-29.296c.001-4.062-3.291-7.354-7.354-7.354zm-.646 33.685c0 1.282-1.039 2.32-2.32 2.32h-23.359c-1.282 0-2.321-1.038-2.321-2.32v-23.36c0-1.282 1.039-2.321 2.321-2.321h23.359c1.281 0 2.32 1.039 2.32 2.321v23.36z" />
183
+ <path fill="#FFFFFF" d="M17.333 28.003c-.736 0-1.332-.6-1.332-1.339v-9.324c0-.739.596-1.339 1.332-1.339h9.338c.738 0 1.332.6 1.332 1.339v9.324c0 .739-.594 1.339-1.332 1.339h-9.338z" />
184
+ </svg>
185
+ <span><?php esc_html_e( 'Connect with Square', 'woocommerce-square' ); ?></span>
186
+ </a>
187
+ <?php } ?>
188
+ </td>
189
+ </tr>
190
+ </tbody>
191
+ </table>
192
+
193
+ <?php
194
+ if ( ! empty( $this->merchant_access_token ) ) { ?>
195
+ <?php echo '<table class="form-table">' . $this->generate_settings_html( $this->get_form_fields(), false ) . '</table>'; ?>
196
+ <?php
197
+
198
+ // only show rest of the settings if a location is selected
199
+ if ( $this->get_option( 'location' ) ) :
200
+ ?>
201
+ <h3 class="wc-settings-sub-title"><?php esc_html_e( 'Initiate a Manual Sync', 'woocommerce-square' ); ?></h3>
202
+ <table class="form-table">
203
+ <tr valign="top">
204
+ <th scope="row" class="titledesc">
205
+ <label for="wc-to-square"><?php esc_html_e( 'WC &rarr; Square Sync', 'woocommerce-square' ); ?></label>
206
+ <?php echo wc_help_tip( __( 'Click button to perform a one time sync from WooCommerce to Square.', 'woocommerce-square' ) ); ?>
207
+ </th>
208
+ <td class="forminp">
209
+ <a href="#" id="wc-to-square" class="button button-secondary"><?php esc_html_e( 'WC &rarr; Square', 'woocommerce-square' ); ?></a>
210
+ </td>
211
+ </tr>
212
+ <tr valign="top">
213
+ <th scope="row" class="titledesc">
214
+ <label for="square-to-wc"><?php esc_html_e( 'Square &rarr; WC Sync', 'woocommerce-square' ); ?></label>
215
+ <?php echo wc_help_tip( __( 'Click button to perform a one time sync from Square to WooCommerce.', 'woocommerce-square' ) ); ?>
216
+ </th>
217
+ <td class="forminp">
218
+ <a href="#" id="square-to-wc" class="button button-secondary"><?php esc_html_e( 'Square &rarr; WC', 'woocommerce-square' ); ?></a>
219
+ </td>
220
+ </tr>
221
+ </table>
222
+ <?php endif;
223
+
224
+ do_action( 'woocommerce_square_integration_custom_settings' );
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Maybe save the token from authentication.
230
+ *
231
+ * @access public
232
+ * @since 1.0.0
233
+ * @version 1.0.0
234
+ * @return bool
235
+ */
236
+ public function maybe_save_token() {
237
+ if ( empty( $_GET['square_access_token'] ) ) {
238
+ return false;
239
+ }
240
+
241
+ if ( function_exists( 'wp_verify_nonce' ) && ! wp_verify_nonce( $_GET['wc_square_token_nonce'], 'connect_square' ) ) {
242
+ wp_die( __( 'Cheatin&#8217; huh?', 'woocommerce-square' ) );
243
+ }
244
+
245
+ $existing_token = get_option( 'woocommerce_square_merchant_access_token' );
246
+
247
+ // if token already exists, don't continue
248
+ if ( ! empty( $existing_token ) ) {
249
+ return false;
250
+ }
251
+
252
+ update_option( 'woocommerce_square_merchant_access_token', sanitize_text_field( urldecode( $_GET['square_access_token'] ) ) );
253
+
254
+ // let's set the token instance again so settings option is refreshed
255
+ $this->merchant_access_token = get_option( 'woocommerce_square_merchant_access_token' );
256
+
257
+ delete_transient( WC_Square_Connect::LOCATIONS_CACHE_KEY );
258
+
259
+ return true;
260
+ }
261
+
262
+ /**
263
+ * Maybe delete the token for merchant.
264
+ *
265
+ * @todo perhaps we should also revoke the token fromm connect.woocommerce.com??
266
+ * @access public
267
+ * @since 1.0.0
268
+ * @version 1.0.0
269
+ * @return bool
270
+ */
271
+ public function maybe_delete_token() {
272
+ if ( empty( $_GET['disconnect_square'] ) ) {
273
+ return false;
274
+ }
275
+
276
+ if ( function_exists( 'wp_verify_nonce' ) && ! wp_verify_nonce( $_GET['wc_square_token_nonce'], 'disconnect_square' ) ) {
277
+ wp_die( __( 'Cheatin&#8217; huh?', 'woocommerce-square' ) );
278
+ }
279
+
280
+ $existing_token = get_option( 'woocommerce_square_merchant_access_token' );
281
+
282
+ // if token does not exist, don't continue
283
+ if ( empty( $existing_token ) ) {
284
+ return false;
285
+ }
286
+
287
+ delete_option( 'woocommerce_square_merchant_access_token' );
288
+
289
+ // let's set the token instance again so settings option is refreshed
290
+ $this->merchant_access_token = get_option( 'woocommerce_square_merchant_access_token' );
291
+
292
+ delete_transient( WC_Square_Connect::LOCATIONS_CACHE_KEY );
293
+
294
+ return true;
295
+ }
296
+
297
+ /**
298
+ * Validates location field.
299
+ *
300
+ * @access public
301
+ * @since 1.0.0
302
+ * @version 1.0.0
303
+ * @return bool
304
+ */
305
+ public function validate_location_field( $key ) {
306
+ $field = $this->get_field_key( $key );
307
+ $value = ! empty( $_POST[ $field ] ) ? $_POST[ $field ] : '';
308
+
309
+ $token = $this->get_option( 'token' );
310
+ if ( empty( $token ) && empty( $value ) ) {
311
+ delete_transient( WC_Square_Connect::LOCATIONS_CACHE_KEY );
312
+ }
313
+
314
+ return $this->validate_select_field( $key, $value );
315
+ }
316
+
317
+ /**
318
+ * Maybe render list of locations.
319
+ *
320
+ * @param array $form_fields Form fields
321
+ *
322
+ * @return array Form fields
323
+ */
324
+ public function maybe_render_locations( $form_fields ) {
325
+ $locations = Woocommerce_Square::instance()->square_connect->get_square_business_locations();
326
+ if ( ! empty( $locations ) && ! empty( $form_fields ) ) {
327
+ $form_fields['location']['options'] = array_merge( $form_fields['location']['options'], $locations );
328
+ $form_fields['location']['disabled'] = false;
329
+ }
330
+
331
+ return $form_fields;
332
+ }
333
+ }
trunk/includes/admin/class-wc-square-admin-product-meta-box.php ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) {
3
+ exit;
4
+ }
5
+
6
+ /**
7
+ * Class WC_Square_Admin_Product_Meta_Box
8
+ *
9
+ * Adds product specific sync options via meta box.
10
+ *
11
+ */
12
+ class WC_Square_Admin_Product_Meta_Box {
13
+ /**
14
+ * Constructor
15
+ *
16
+ * @version 1.0.9
17
+ * @since 1.0.9
18
+ */
19
+ public function __construct() {
20
+ // add a sync field to the product general tab
21
+ add_action( 'woocommerce_product_options_general_product_data', array( $this, 'add_product_sync_checkbox_general' ) );
22
+
23
+ // save sync field for the product general tab
24
+ add_action( 'woocommerce_process_product_meta_simple', array( $this, 'save_product_sync_checkbox_general' ) );
25
+ add_action( 'woocommerce_process_product_meta_booking', array( $this, 'save_product_sync_checkbox_general' ) );
26
+
27
+ // save sync field for variable product general tab
28
+ add_action( 'woocommerce_process_product_meta_variable', array( $this, 'save_product_sync_checkbox_general' ) );
29
+
30
+ // add sync to product bulk edit menu
31
+ add_action( 'woocommerce_product_bulk_edit_end', array( $this, 'add_product_bulk_edit_sync' ) );
32
+
33
+ // save sync to product bulk edit
34
+ add_action( 'woocommerce_product_bulk_edit_save', array( $this, 'save_product_bulk_edit_sync' ) );
35
+ }
36
+
37
+ /**
38
+ * Add a sync field to the product general tab
39
+ *
40
+ * @access public
41
+ * @since 1.0.9
42
+ * @version 1.0.9
43
+ * @return bool
44
+ */
45
+ public function add_product_sync_checkbox_general() {
46
+ global $post;
47
+
48
+ $sync = get_post_meta( $post->ID, '_wcsquare_disable_sync', true );
49
+
50
+ // set default to no if nothing is set
51
+ if ( empty( $sync ) ) {
52
+ $sync = 'no';
53
+ }
54
+
55
+ $output = '';
56
+
57
+ $output .= '<div class="options_group show_if_simple show_if_variable show_if_booking">' . PHP_EOL;
58
+
59
+ $output .= '<p class="form-field wcsquare_product_default_sync_field"><label for="wcsquare_product_default_sync">' . wp_kses_post( __( '(Square) Disable Sync', 'woocommerce-square' ) ) . '</label><input type="checkbox" name="_wcsquare_disable_sync" id="wcsquare_product_default_sync" value="yes" ' . checked( 'yes', $sync, false ) . '/>' . PHP_EOL;
60
+
61
+ $output .= '<span class="description">' . wp_kses_post( __( 'Check box to disable this product from syncing.', 'woocommerce-square' ) ) . '</span>' . PHP_EOL;
62
+
63
+ $output .= '</p>' . PHP_EOL;
64
+
65
+ $output .= '</div>';
66
+
67
+ echo $output;
68
+
69
+ return true;
70
+ }
71
+
72
+ /**
73
+ * Save the sync field for the product general tab
74
+ *
75
+ * @access public
76
+ * @since 1.0.9
77
+ * @version 1.0.9
78
+ * @param int $post_id
79
+ * @return bool
80
+ */
81
+ public function save_product_sync_checkbox_general( $post_id ) {
82
+ if ( empty( $post_id ) ) {
83
+ return;
84
+ }
85
+
86
+ if ( ! empty( $_POST['_wcsquare_disable_sync'] ) ) {
87
+ update_post_meta( $post_id, '_wcsquare_disable_sync', 'yes' );
88
+
89
+ } else {
90
+
91
+ update_post_meta( $post_id, '_wcsquare_disable_sync', 'no' );
92
+ }
93
+
94
+ return true;
95
+ }
96
+
97
+ /**
98
+ * Add sync setting to product bulk edit menu
99
+ *
100
+ * @access public
101
+ * @since 1.0.9
102
+ * @version 1.0.9
103
+ * @return bool
104
+ */
105
+ public function add_product_bulk_edit_sync() {
106
+ ?>
107
+ <label>
108
+ <span class="title"><?php esc_html_e( 'Disable Sync', 'woocommerce-square' ); ?></span>
109
+ <span class="input-text-wrap">
110
+ <select class="square-sync-product" name="_wcsquare_disable_sync">
111
+ <?php
112
+ $options = array(
113
+ '' => __( '— No Change —', 'woocommerce-square' ),
114
+ 'yes' => __( 'Yes', 'woocommerce-square' ),
115
+ 'no' => __( 'No', 'woocommerce-square' )
116
+ );
117
+
118
+ foreach ( $options as $key => $value ) {
119
+ echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>';
120
+ }
121
+ ?>
122
+ </select>
123
+ </span>
124
+ </label>
125
+ <?php
126
+ }
127
+
128
+ /**
129
+ * Save sync setting to product bulk edit
130
+ *
131
+ * @access public
132
+ * @since 1.0.9
133
+ * @version 1.0.9
134
+ * @param object $product
135
+ * @return bool
136
+ */
137
+ public function save_product_bulk_edit_sync( $product ) {
138
+ if ( empty( $product ) ) {
139
+ return;
140
+ }
141
+
142
+ if ( ! empty( $_GET['_wcsquare_disable_sync'] ) && 'yes' === $_GET['_wcsquare_disable_sync'] ) {
143
+ update_post_meta( version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->id : $product->get_id(), '_wcsquare_disable_sync', 'yes' );
144
+
145
+ } else {
146
+ update_post_meta( version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->id : $product->get_id(), '_wcsquare_disable_sync', 'no' );
147
+ }
148
+
149
+ return true;
150
+ }
151
+
152
+ }
153
+
154
+ new WC_Square_Admin_Product_Meta_Box();
155
+
trunk/includes/admin/class-wc-square-bulk-sync-handler.php ADDED
@@ -0,0 +1,456 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) {
3
+ exit;
4
+ }
5
+
6
+ /**
7
+ * Class WC_Square_Bulk_Sync_Handler
8
+ *
9
+ * Facilitates Bulk syncing to/from Square/WC. Handles AJAX initiation of
10
+ * sync, progress updates, actual sync method calls, and sync completion emails.
11
+ */
12
+ class WC_Square_Bulk_Sync_Handler {
13
+
14
+ public $connect;
15
+ public $wc_to_square;
16
+ public $square_to_wc;
17
+
18
+ public function __construct( WC_Square_Connect $connect, WC_Square_Sync_To_Square $to_square, WC_Square_Sync_From_Square $from_square ) {
19
+
20
+ $this->connect = $connect;
21
+ $this->wc_to_square = $to_square;
22
+ $this->square_to_wc = $from_square;
23
+
24
+ add_action( 'wp_ajax_square_to_wc', array( $this, 'square_to_wc_ajax' ) );
25
+ add_action( 'wp_ajax_wc_to_square', array( $this, 'wc_to_square_ajax' ) );
26
+ }
27
+
28
+ /**
29
+ * Sets manual sync processing flag to prevent
30
+ * other processes from running such as inventory
31
+ * polling.
32
+ *
33
+ * @since 1.0.27
34
+ */
35
+ public function sync_processing() {
36
+ $cache_age = apply_filters( 'woocommerce_square_manual_sync_processing_cache', 2 * HOUR_IN_SECONDS );
37
+ set_transient( 'wc_square_manual_sync_processing', 'yes', $cache_age );
38
+ }
39
+
40
+ /**
41
+ * Process Square to WC ajax
42
+ *
43
+ * @since 1.0.0
44
+ * @version 1.0.14
45
+ * @return bool
46
+ */
47
+ public function square_to_wc_ajax() {
48
+ check_ajax_referer( 'square-sync', 'security' );
49
+
50
+ $this->sync_processing();
51
+
52
+ /**
53
+ * Fires if a valid bulk Square to WC sync is being processed.
54
+ *
55
+ * @since 1.0.0
56
+ */
57
+ do_action( 'woocommerce_square_bulk_syncing_square_to_wc' );
58
+
59
+ $settings = get_option( 'woocommerce_squareconnect_settings' );
60
+ $emails = ! empty( $settings['sync_email'] ) ? explode( ',', str_replace( ' ', '', $settings['sync_email'] ) ) : '';
61
+
62
+ $sync_products = ( 'yes' === $settings['sync_products'] );
63
+ $sync_categories = ( 'yes' === $settings['sync_categories'] );
64
+ $sync_inventory = ( 'yes' === $settings['sync_inventory'] );
65
+ $sync_images = ( 'yes' === $settings['sync_images'] );
66
+ $cache_age = apply_filters( 'woocommerce_square_syncing_square_ids_cache', DAY_IN_SECONDS );
67
+ $message = '';
68
+
69
+ if ( ! $sync_products ) {
70
+ wp_send_json( array( 'process' => 'done', 'percentage' => 100, 'type' => 'square-to-wc', 'message' => __( 'Product Sync is disabled. Sync aborted.', 'woocommerce-square' ) ) );
71
+ }
72
+
73
+ // we need to check for cURL
74
+ if ( ! function_exists( 'curl_init' ) ) {
75
+ wp_send_json( array( 'process' => 'done', 'percentage' => 100, 'type' => 'square-to-wc', 'message' => __( 'cURL is not available. Sync aborted. Please contact your host to install cURL.', 'woocommerce-square' ) ) );
76
+ }
77
+
78
+ // if a WC to Square process still needs to be completed reset the caches
79
+ // as the two processes ( WC -> Square and Square -> WC ) use the same cache
80
+ if ( 'wc_to_square' === get_transient( 'sq_wc_sync_current_process' ) ) {
81
+ WC_Square_Utils::delete_transients();
82
+ }
83
+
84
+ // set Square->WC as the current active process
85
+ set_transient( 'sq_wc_sync_current_process', 'square_to_wc', $cache_age );
86
+
87
+ // index for the current item in the process
88
+ $process = $this->get_process_index();
89
+
90
+ // only sync categories on the first pass
91
+ if ( ( 0 === $process ) && $sync_categories ) {
92
+
93
+ $this->square_to_wc->sync_categories();
94
+
95
+ }
96
+
97
+ if ( ( 0 === $process ) && $sync_inventory ) {
98
+ // ensure this manual update gets the freshest item counts
99
+ delete_transient( 'wc_square_inventory' );
100
+
101
+ $this->connect->get_square_inventory();
102
+
103
+ }
104
+
105
+ // products
106
+ // get all product ids
107
+ $square_item_ids = $this->get_processing_ids();
108
+
109
+ // run this only on first process
110
+ if ( $process === 0 ) {
111
+ $square_items = $this->connect->get_square_products();
112
+ $square_item_ids = ! empty( $square_items ) ? array_unique( wp_list_pluck( (array) $square_items, 'id' ) ) : array();
113
+
114
+ // cache it
115
+ set_transient( 'wc_square_processing_total_count', count( $square_item_ids ), $cache_age );
116
+ set_transient( 'wc_square_processing_ids', $square_item_ids, $cache_age );
117
+
118
+ }
119
+
120
+ if ( $square_item_ids && $sync_products ) {
121
+
122
+ $square_item_id = array_pop( $square_item_ids );
123
+ $square_item = $this->connect->get_square_product( $square_item_id );
124
+
125
+ if ( $square_item ) {
126
+
127
+ $this->square_to_wc->sync_product( $square_item, $sync_categories, $sync_inventory, $sync_images );
128
+
129
+ } else {
130
+
131
+ WC_Square_Sync_Logger::log( sprintf( '[Square -> WC] Bulk Sync: Error retrieving Square Item with ID %s.', $square_item_id ) );
132
+
133
+ }
134
+
135
+ $process++;
136
+
137
+ $percentage = $this->get_process_percentage( $process );
138
+
139
+ $this->delete_processed_id( $square_item_id );
140
+
141
+ $remaining_ids = $this->get_processing_ids();
142
+
143
+ // run this only on last process
144
+ if ( empty( $remaining_ids ) ) {
145
+ $process = 'done';
146
+
147
+ $percentage = 100;
148
+
149
+ // send sync email
150
+ $this->send_sync_email( $emails, __( 'Sync Completed', 'woocommerce-square' ) );
151
+
152
+ // reset the processed ids
153
+ WC_Square_Utils::delete_transients();
154
+
155
+ $message = __( 'Sync completed', 'woocommerce-square' );
156
+ }
157
+
158
+ wp_send_json( array( 'process' => $process, 'percentage' => $percentage, 'type' => 'square-to-wc', 'message' => $message ) );
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Process WC to Square ajax
164
+ *
165
+ * @since 1.0.0
166
+ * @version 1.0.0
167
+ * @return bool
168
+ */
169
+ public function wc_to_square_ajax() {
170
+ check_ajax_referer( 'square-sync', 'security' );
171
+
172
+ $this->sync_processing();
173
+
174
+ $settings = get_option( 'woocommerce_squareconnect_settings' );
175
+ $emails = ! empty( $settings['sync_email'] ) ? $settings['sync_email'] : '';
176
+
177
+ $sync_products = ( 'yes' === $settings['sync_products'] );
178
+ $sync_categories = ( 'yes' === $settings['sync_categories'] );
179
+ $sync_inventory = ( 'yes' === $settings['sync_inventory'] );
180
+ $sync_images = ( 'yes' === $settings['sync_images'] );
181
+ $cache_age = apply_filters( 'woocommerce_square_syncing_wc_product_ids_cache', DAY_IN_SECONDS );
182
+ $message = '';
183
+
184
+ if ( ! $sync_products ) {
185
+ wp_send_json( array( 'process' => 'done', 'percentage' => 100, 'type' => 'wc-to-square', 'message' => __( 'Product Sync is disabled. Sync aborted.', 'woocommerce-square' ) ) );
186
+ }
187
+
188
+ // we need to check for cURL
189
+ if ( ! function_exists( 'curl_init' ) ) {
190
+ wp_send_json( array( 'process' => 'done', 'percentage' => 100, 'type' => 'wc-to-square', 'message' => __( 'cURL is not available. Sync aborted. Please contact your host to install cURL.', 'woocommerce-square' ) ) );
191
+ }
192
+
193
+ // if a Square to WC process still needs to be completed reset the caches
194
+ // as the two processes ( WC -> Square and Square -> WC ) use the same cache
195
+ if ( 'square_to_wc' === get_transient( 'sq_wc_sync_current_process' ) ) {
196
+ WC_Square_Utils::delete_transients();
197
+ }
198
+
199
+ // set WC->Square as the current active process
200
+ set_transient( 'sq_wc_sync_current_process', 'wc_to_square', $cache_age );
201
+
202
+ $process = $this->get_process_index();
203
+
204
+ // only sync categories on the first pass
205
+ if ( ( 0 === $process ) && $sync_categories ) {
206
+
207
+ $this->wc_to_square->sync_categories();
208
+
209
+ }
210
+
211
+ // products
212
+ // get all product ids
213
+ $wc_product_ids = $this->get_processing_ids();
214
+
215
+ // run the following only on first process and cache it
216
+ if ( ( 0 === $process ) && $sync_products ) {
217
+
218
+ $wc_product_ids = $this->get_all_product_ids();
219
+
220
+ // cache it
221
+ set_transient( 'wc_square_processing_total_count', count( $wc_product_ids ), $cache_age );
222
+ set_transient( 'wc_square_processing_ids', $wc_product_ids, $cache_age );
223
+ }
224
+
225
+ if ( $sync_products && ! empty( $wc_product_ids ) ) {
226
+
227
+ $wc_product_id = array_pop( $wc_product_ids );
228
+
229
+ $wc_product = wc_get_product( $wc_product_id );
230
+
231
+ if ( is_object( $wc_product ) && is_a( $wc_product, 'WC_Product' ) ) {
232
+ $this->wc_to_square->sync_product( $wc_product, $sync_categories, $sync_inventory, $sync_images );
233
+ }
234
+
235
+ $process++;
236
+
237
+ $percentage = $this->get_process_percentage( $process );
238
+
239
+ if ( is_object( $wc_product ) ) {
240
+ $this->delete_processed_id( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->id : $wc_product->get_id() );
241
+ } else {
242
+ $this->delete_processed_id( $wc_product_id );
243
+ }
244
+
245
+ $remaining_ids = $this->get_processing_ids();
246
+
247
+ // run this only on last process
248
+ if ( empty( $remaining_ids ) ) {
249
+ $process = 'done';
250
+
251
+ $percentage = 100;
252
+
253
+ // send sync email
254
+ $this->send_sync_email( $emails, __( 'Sync Completed', 'woocommerce-square' ) );
255
+
256
+ // reset the processed ids
257
+ WC_Square_Utils::delete_transients();
258
+
259
+ $message = __( 'Sync completed', 'woocommerce-square' );
260
+ }
261
+
262
+ wp_send_json( array( 'process' => $process, 'percentage' => $percentage, 'type' => 'wc-to-square', 'message' => $message ) );
263
+
264
+ }
265
+
266
+ wp_send_json( array( 'process' => 'done', 'percentage' => 100, 'type' => 'wc-to-square', 'message' => __( 'No Products to Sync.', 'woocommerce-square' ) ) );
267
+ }
268
+
269
+ /**
270
+ * Figure out at which product index we are
271
+ * at using the total count and the remaining item.
272
+ * The index stats at 0 .
273
+ *
274
+ * @since 1.0.0
275
+ *
276
+ * @return int $process_index
277
+ */
278
+ public function get_process_index() {
279
+
280
+ $total_items = (int) get_transient( 'wc_square_processing_total_count' );
281
+ $remaining_ids_count = count( $this->get_processing_ids() );
282
+ $process_index = $total_items - $remaining_ids_count;
283
+
284
+ if ( empty( $process_index ) ) {
285
+ $process_index = 0;
286
+ }
287
+
288
+ return $process_index;
289
+ }
290
+
291
+ /**
292
+ * Gets all product ids that are sync-eligible (they have SKUs).
293
+ *
294
+ * This looks for products as well as variations, if a variant has a SKU, the
295
+ * parent product will be included in the result set.
296
+ *
297
+ * @access public
298
+ * @since 1.0.0
299
+ * @version 1.0.14
300
+ * @return array $ids
301
+ */
302
+ public function get_all_product_ids() {
303
+
304
+ $args = apply_filters( 'woocommerce_square_get_all_product_ids_args', array(
305
+ 'posts_per_page' => -1,
306
+ 'post_type' => array( 'product', 'product_variation' ),
307
+ 'post_status' => 'publish',
308
+ 'fields' => 'id=>parent',
309
+ 'meta_query' => array(
310
+ array(
311
+ 'key' => '_sku',
312
+ 'compare' => '!=',
313
+ 'value' => ''
314
+ )
315
+ )
316
+ ) );
317
+
318
+ $products_with_skus = get_posts( $args );
319
+ $product_ids = array();
320
+
321
+ /*
322
+ * Our result set contains products and variations. We're only concerned with
323
+ * returning top-level products, so favor the parent ID if present (denotes a variation)
324
+ */
325
+ foreach ( $products_with_skus as $product_id => $parent_id ) {
326
+ $post_id = 0 == $parent_id ? $product_id : $parent_id;
327
+
328
+ // check if product sync is disable, if so skip
329
+ if ( WC_Square_Utils::skip_product_sync( $post_id ) ) {
330
+ WC_Square_Sync_Logger::log( sprintf( '[WC -> Square] Syncing disabled for this WC Product %d', $post_id ) );
331
+
332
+ continue;
333
+ }
334
+
335
+ // when it is a variation, we need to check its parent for publish
336
+ // post status.
337
+ if ( 0 == $parent_id ) {
338
+ $product_ids[] = $product_id;
339
+ } else {
340
+ $post = get_post( $parent_id );
341
+
342
+ if ( is_object( $post ) && 'publish' === $post->post_status ) {
343
+ $product_ids[] = $parent_id;
344
+ }
345
+ }
346
+ }
347
+
348
+ /*
349
+ * Products can have multiple variants, so we might end up with
350
+ * duplicate parent product IDs in our list.
351
+ */
352
+ $unique_product_ids = array_unique( $product_ids );
353
+
354
+ return $unique_product_ids;
355
+ }
356
+
357
+ /**
358
+ * Deletes the product ID from the list so we can continue if sync is terminated early.
359
+ * This function can take both the WC product id or Square product ID
360
+ *
361
+ * @access public
362
+ * @since 1.0.0
363
+ * @version 1.0.0
364
+ * @param string $product_id
365
+ * @return bool
366
+ */
367
+ public function delete_processed_id( $product_id = null ) {
368
+
369
+ if ( null === $product_id ) {
370
+ return false;
371
+ }
372
+
373
+ $ids = $this->get_processing_ids();
374
+
375
+ if ( ( $key = array_search( $product_id, $ids ) ) !== false ) {
376
+ unset( $ids[ $key ] );
377
+ }
378
+
379
+ set_transient( 'wc_square_processing_ids', $ids, apply_filters( 'woocommerce_square_sync_processing_ids_cache', DAY_IN_SECONDS ) );
380
+
381
+ return true;
382
+ }
383
+
384
+ /**
385
+ * Gets the already processed product IDs ( both Square and WC )
386
+ *
387
+ * @access public
388
+ * @since 1.0.0
389
+ * @version 1.0.0
390
+ * @return array $ids
391
+ */
392
+ public function get_processing_ids() {
393
+ if ( $ids = get_transient( 'wc_square_processing_ids' ) ) {
394
+ return $ids;
395
+ }
396
+
397
+ return array();
398
+ }
399
+
400
+ /**
401
+ * Get process percentage
402
+ *
403
+ * @access public
404
+ * @since 1.0.0
405
+ * @version 1.0.0
406
+ * @param int $process the current process step
407
+ * @return string $percentage
408
+ */
409
+ public function get_process_percentage( $process ) {
410
+
411
+ $total_count = (int) get_transient( 'wc_square_processing_total_count' );
412
+ $percentage = 0;
413
+
414
+ if ( $total_count > 0 ) {
415
+ $percentage = ( $process / $total_count );
416
+ }
417
+
418
+ if ( 0 === $process ) {
419
+ // 10% is added to offset the category process
420
+ $percentage = $percentage + 0.10;
421
+ }
422
+
423
+ return round( $percentage, 2 ) * 100;
424
+ }
425
+
426
+ /**
427
+ * Sends the sync notification email when operation ends
428
+ *
429
+ * @access public
430
+ * @since 1.0.0
431
+ * @version 1.0.14
432
+ * @param string $emails
433
+ * @param string $message
434
+ * @return bool
435
+ */
436
+ public function send_sync_email( $emails, $message = '' ) {
437
+ // default to admin's email
438
+ if ( empty( $emails ) ) {
439
+ $emails = array();
440
+ $emails[] = get_option( 'admin_email' );
441
+ }
442
+
443
+ $subject = sprintf( __( '%s - WooCommerce Square Sync Operation', 'woocommerce-square' ), wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ) );
444
+
445
+ $headers = array();
446
+
447
+ foreach ( $emails as $email ) {
448
+ $headers[] = sprintf( __( '%s', 'woocommerce-square' ) . ' ' . wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ) . ' <' . $email . '>', 'From:' ) . PHP_EOL;
449
+
450
+ wp_mail( $email, $subject, $message, $headers );
451
+ }
452
+
453
+ return true;
454
+ }
455
+
456
+ }
trunk/includes/admin/class-wc-square-privacy.php ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! class_exists( 'WC_Abstract_Privacy' ) ) {
3
+ return;
4
+ }
5
+
6
+ class WC_Square_Privacy extends WC_Abstract_Privacy {
7
+ /**
8
+ * Constructor
9
+ *
10
+ */
11
+ public function __construct() {
12
+ parent::__construct( __( 'Square', 'woocommerce-square' ) );
13
+
14
+ $this->add_exporter( 'woocommerce-square-customer-data', __( 'WooCommerce Square Customer Data', 'woocommerce-square' ), array( $this, 'customer_data_exporter' ) );
15
+ $this->add_eraser( 'woocommerce-square-customer-data', __( 'WooCommerce Square Customer Data', 'woocommerce-square' ), array( $this, 'customer_data_eraser' ) );
16
+ }
17
+
18
+ /**
19
+ * Gets the message of the privacy to display.
20
+ *
21
+ */
22
+ public function get_message() {
23
+ return wpautop( sprintf( __( 'By using this extension, you may be storing personal data or sharing data with an external service. <a href="%s" target="_blank">Learn more about how this works, including what you may want to include in your privacy policy.</a>', 'woocommerce-square' ), 'https://docs.woocommerce.com/document/privacy-payments/#woocommerce-square' ) );
24
+ }
25
+
26
+ /**
27
+ * Handle exporting data for Orders.
28
+ *
29
+ * @param string $email_address E-mail address to export.
30
+ * @param int $page Pagination of data.
31
+ *
32
+ * @return array
33
+ */
34
+ public function order_data_exporter( $email_address, $page = 1 ) {
35
+ $done = false;
36
+ $data_to_export = array();
37
+
38
+ $done = true;
39
+
40
+ return array(
41
+ 'data' => $data_to_export,
42
+ 'done' => $done,
43
+ );
44
+ }
45
+
46
+ /**
47
+ * Finds and exports customer data by email address.
48
+ *
49
+ * @since 3.4.0
50
+ * @param string $email_address The user email address.
51
+ * @param int $page Page.
52
+ * @return array An array of personal data in name value pairs
53
+ */
54
+ public function customer_data_exporter( $email_address, $page ) {
55
+ $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data.
56
+ $data_to_export = array();
57
+
58
+ if ( $user instanceof WP_User ) {
59
+ $data_to_export[] = array(
60
+ 'group_id' => 'woocommerce_customer',
61
+ 'group_label' => __( 'Customer Data', 'woocommerce-square' ),
62
+ 'item_id' => 'user',
63
+ 'data' => array(
64
+ array(
65
+ 'name' => __( 'Square customer id', 'woocommerce-square' ),
66
+ 'value' => get_user_meta( $user->ID, '_square_customer_id', true ),
67
+ ),
68
+ ),
69
+ );
70
+ }
71
+
72
+ return array(
73
+ 'data' => $data_to_export,
74
+ 'done' => true,
75
+ );
76
+ }
77
+
78
+ /**
79
+ * Finds and erases customer data by email address.
80
+ *
81
+ * @since 3.4.0
82
+ * @param string $email_address The user email address.
83
+ * @param int $page Page.
84
+ * @return array An array of personal data in name value pairs
85
+ */
86
+ public function customer_data_eraser( $email_address, $page ) {
87
+ $page = (int) $page;
88
+ $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data.
89
+
90
+ $square_customer_id = get_user_meta( $user->ID, '_square_customer_id', true );
91
+
92
+ $items_removed = false;
93
+ $messages = array();
94
+
95
+ if ( ! empty( $square_customer_id ) ) {
96
+ $items_removed = true;
97
+ delete_user_meta( $user->ID, '_square_customer_id' );
98
+ $messages[] = __( 'Square User Data Erased.', 'woocommerce-square' );
99
+ }
100
+
101
+ return array(
102
+ 'items_removed' => $items_removed,
103
+ 'items_retained' => false,
104
+ 'messages' => $messages,
105
+ 'done' => true,
106
+ );
107
+ }
108
+ }
109
+
110
+ new WC_Square_Privacy();
trunk/includes/class-wc-square-client.php ADDED
@@ -0,0 +1,388 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) {
3
+ exit; // Exit if accessed directly
4
+ }
5
+
6
+ /**
7
+ * Class WC_Square_Client
8
+ *
9
+ * Makes actual HTTP requests to the Square API.
10
+ * Handles:
11
+ * - Authentication
12
+ * - Endpoint selection (API version, Merchant ID in path)
13
+ * - Request retries
14
+ * - Paginated results
15
+ * - Content-Type negotiation (JSON)
16
+ */
17
+ class WC_Square_Client {
18
+
19
+ /**
20
+ * @var
21
+ */
22
+ protected $access_token;
23
+
24
+ /**
25
+ * @var
26
+ */
27
+ protected $merchant_id;
28
+
29
+ /**
30
+ * @var string
31
+ */
32
+ protected $api_version = 'v1';
33
+
34
+ /**
35
+ * @return mixed
36
+ */
37
+ public function get_access_token() {
38
+
39
+ return $this->access_token;
40
+
41
+ }
42
+
43
+ /**
44
+ * @param $token
45
+ */
46
+ public function set_access_token( $token ) {
47
+
48
+ $this->access_token = $token;
49
+
50
+ }
51
+
52
+ /**
53
+ * @return mixed
54
+ */
55
+ public function get_merchant_id() {
56
+
57
+ return $this->merchant_id;
58
+
59
+ }
60
+
61
+ /**
62
+ * @param $merchant_id
63
+ */
64
+ public function set_merchant_id( $merchant_id ) {
65
+
66
+ $this->merchant_id = $merchant_id;
67
+
68
+ }
69
+
70
+ /**
71
+ * @return string
72
+ */
73
+ public function get_api_version() {
74
+
75
+ return $this->api_version;
76
+
77
+ }
78
+
79
+ /**
80
+ * @param $version
81
+ */
82
+ public function set_api_version( $version ) {
83
+
84
+ $this->api_version = $version;
85
+
86
+ }
87
+
88
+ /**
89
+ * @return int|mixed|void
90
+ */
91
+ public function get_api_url_base() {
92
+ if ( WC_SQUARE_ENABLE_STAGING ) {
93
+ return apply_filters( 'woocommerce_square_api_url', 'https://connect.squareupstaging.com/' );
94
+ }
95
+
96
+ return apply_filters( 'woocommerce_square_api_url', 'https://connect.squareup.com/' );
97
+ }
98
+
99
+ /**
100
+ * @return string
101
+ */
102
+ public function get_api_url() {
103
+
104
+ $url = trailingslashit( $this->get_api_url_base() );
105
+ $url .= trailingslashit( $this->get_api_version() );
106
+
107
+ return $url;
108
+
109
+ }
110
+
111
+ /**
112
+ * Initializes the header arguments.
113
+ *
114
+ * @since 1.0.0
115
+ * @version 1.0.14
116
+ * @return int|mixed|void
117
+ */
118
+ public function get_request_args() {
119
+
120
+ $args = array(
121
+ 'headers' => array(
122
+ 'Authorization' => 'Bearer ' . sanitize_text_field( $this->get_access_token() ),
123
+ 'Accept' => 'application/json',
124
+ 'Content-Type' => 'application/json',
125
+ ),
126
+ 'user-agent' => 'WooCommerceSquare/' . WC_SQUARE_VERSION . '; ' . get_bloginfo( 'url' ),
127
+ 'timeout' => 45,
128
+ 'httpversion' => '1.1',
129
+ );
130
+
131
+ return apply_filters( 'woocommerce_square_request_args', $args );
132
+ }
133
+
134
+ /**
135
+ * @param $path
136
+ *
137
+ * @return string
138
+ */
139
+ protected function get_request_url( $path ) {
140
+
141
+ $api_url_base = trailingslashit( $this->get_api_url() );
142
+ $merchant_id = '';
143
+
144
+ // Add merchant ID to the request URL if we aren't hitting /me/*
145
+ if ( strpos( trim( $path, '/' ), 'me' ) !== 0 ) {
146
+
147
+ $merchant_id = trailingslashit( $this->get_merchant_id() );
148
+
149
+ }
150
+
151
+ $request_path = ltrim( $path, '/' );
152
+ $request_url = trailingslashit( $api_url_base . $merchant_id . $request_path );
153
+
154
+ return $request_url;
155
+
156
+ }
157
+
158
+ /**
159
+ * Gets the number of retries per request
160
+ *
161
+ * @access public
162
+ * @since 1.0.0
163
+ * @version 1.0.0
164
+ * @param int $count
165
+ * @return int
166
+ */
167
+ public function request_retries( $count = 5 ) {
168
+
169
+ return apply_filters( 'woocommerce_square_request_retries', $count );
170
+
171
+ }
172
+
173
+ /**
174
+ * Wrapper around http_request() that handles pagination for List endpoints.
175
+ *
176
+ * @since 1.0.25 Switch to use WP native remote requests.
177
+ * @param string $debug_label Description of the request, for logging.
178
+ * @param string $path API endpoint path to hit. E.g. /items/
179
+ * @param string $method HTTP method to use. Defaults to 'GET'.
180
+ * @param mixed $body Optional. Request payload - will be JSON encoded if non-scalar.
181
+ *
182
+ * @return bool|object|WP_Error
183
+ */
184
+ public function request( $debug_label, $path, $method = 'GET', $body = null ) {
185
+ // we need to check for cURL
186
+ if ( ! function_exists( 'curl_init' ) ) {
187
+ WC_Square_Sync_Logger::log( 'cURL is not available. Sync aborted. Please contact your host to install cURL.' );
188
+
189
+ return false;
190
+ }
191
+
192
+ // The access token is required for all requests
193
+ $access_token = $this->get_access_token();
194
+
195
+ if ( empty( $access_token ) ) {
196
+
197
+ return false;
198
+
199
+ }
200
+
201
+ $request_url = $this->get_request_url( $path );
202
+ $return_data = array();
203
+ $page_label = '';
204
+ $page_count = 1;
205
+
206
+ while ( true ) {
207
+
208
+ $response = $this->http_request( $debug_label . $page_label, $request_url, $method, $body );
209
+
210
+ if ( ! $response ) {
211
+
212
+ return $response;
213
+
214
+ }
215
+
216
+ $response_data = json_decode( wp_remote_retrieve_body( $response ) );
217
+
218
+ // A paged list result will be an array, so let's merge if we're already returning an array
219
+ if ( ( 'GET' === $method ) && is_array( $return_data ) && is_array( $response_data ) ) {
220
+
221
+ $return_data = array_merge( $return_data, $response_data );
222
+
223
+ } else {
224
+
225
+ $return_data = $response_data;
226
+
227
+ }
228
+
229
+ $link_header = wp_remote_retrieve_header( $response, 'Link' );
230
+
231
+ // Look for the next page, if specified
232
+ if ( ! preg_match( '/<(.+)>;rel=("|\')next("|\')/i', $link_header ) ) {
233
+ return $return_data;
234
+ }
235
+
236
+ $rel_link_matches = array();
237
+
238
+ // Set up the next page URL for the following loop
239
+ if ( ( 'GET' === $method ) && preg_match( '/<(.+)>;rel=("|\')next("|\')/i', $link_header, $rel_link_matches ) ) {
240
+
241
+ // Check if we're at the end of pagination.
242
+ if ( $request_url === $rel_link_matches[1] ) {
243
+ return $return_data;
244
+ }
245
+
246
+ $request_url = $rel_link_matches[1];
247
+ $body = null;
248
+ $page_count++;
249
+ $page_label = sprintf( ' - Fetching page %d', $page_count );
250
+
251
+ } else {
252
+
253
+ return $return_data;
254
+
255
+ }
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Helper method to make HTTP requests to the Square API, with retries.
261
+ *
262
+ * @since 1.0.25 Switch to use WP native remote requests.
263
+ * @param string $debug_label Description of the request, for logging.
264
+ * @param string $request_url URL to request.
265
+ * @param string $method HTTP method to use. Defaults to 'GET'.
266
+ * @param mixed $body Optional. Request payload - will be JSON encoded if non-scalar.
267
+ *
268
+ * @return bool|object|WP_Error
269
+ */
270
+ private function http_request( $debug_label, $request_url, $method = 'GET', $body = null ) {
271
+ $request_args = $this->get_request_args();
272
+
273
+ if ( ! is_null( $body ) ) {
274
+ if ( ! empty( $request_args['headers']['Content-Type'] ) && ( 'application/json' === $request_args['headers']['Content-Type'] ) ) {
275
+ $request_args['body'] = wp_json_encode( $body );
276
+ } else {
277
+ $request_args['body'] = $body;
278
+ }
279
+ }
280
+
281
+ $request_args['method'] = $method;
282
+
283
+ // Make actual request in a retry loop
284
+ $try_count = 1;
285
+ $max_retries = $this->request_retries();
286
+
287
+ while ( true ) {
288
+ $start_time = current_time( 'timestamp' );
289
+ $response = wp_remote_request( untrailingslashit( $request_url ), $request_args );
290
+ $end_time = current_time( 'timestamp' );
291
+
292
+ WC_Square_Sync_Logger::log( sprintf( '%s', $debug_label ), $start_time, $end_time );
293
+
294
+ $decoded_response = json_decode( wp_remote_retrieve_body( $response ) );
295
+
296
+ if ( is_object( $decoded_response ) && ! empty( $decoded_response->type ) ) {
297
+ if ( preg_match( '/bad_request/', $decoded_response->type ) || preg_match( '/not_found/', $decoded_response->type ) ) {
298
+ WC_Square_Sync_Logger::log( sprintf( '%s - %s', $decoded_response->type, $decoded_response->message ), $start_time, $end_time );
299
+
300
+ return false;
301
+ }
302
+ }
303
+
304
+ // handle expired tokens
305
+ if ( is_object( $decoded_response ) &&
306
+ ( ! empty( $decoded_response->type ) && 'oauth.expired' === $decoded_response->type ) ||
307
+ ( ! empty( $decoded_response->errors ) && 'ACCESS_TOKEN_EXPIRED' === $decoded_response->errors[0]->code ) ) {
308
+
309
+ $oauth_connect_url = 'https://connect.woocommerce.com/renew/square';
310
+
311
+ if ( WC_SQUARE_ENABLE_STAGING ) {
312
+ $oauth_connect_url = 'https://connect.woocommerce.com/renew/squaresandbox';
313
+ }
314
+
315
+ $args = array(
316
+ 'body' => array(
317
+ 'token' => $this->access_token,
318
+ ),
319
+ 'timeout' => 45,
320
+ );
321
+
322
+ $start_time = current_time( 'timestamp' );
323
+ $oauth_response = wp_remote_post( $oauth_connect_url, $args );
324
+ $end_time = current_time( 'timestamp' );
325
+
326
+ $decoded_oauth_response = json_decode( wp_remote_retrieve_body( $oauth_response ) );
327
+
328
+ if ( is_wp_error( $oauth_response ) ) {
329
+
330
+ WC_Square_Sync_Logger::log( sprintf( 'Renewing expired token error - %s', $parsed_oauth_response['curl_error'] ), $start_time, $end_time );
331
+
332
+ return false;
333
+
334
+ } elseif ( is_object( $decoded_oauth_response ) && ! empty( $decoded_oauth_response->error ) ) {
335
+
336
+ WC_Square_Sync_Logger::log( sprintf( 'Renewing expired token error - %s', $decoded_oauth_response->type ), $start_time, $end_time );
337
+
338
+ return false;
339
+
340
+ } elseif ( is_object( $decoded_oauth_response ) && ! empty( $decoded_oauth_response->access_token ) ) {
341
+ update_option( 'woocommerce_square_merchant_access_token', sanitize_text_field( urldecode( $decoded_oauth_response->access_token ) ) );
342
+
343
+ // let's set the token instance again so settings option is refreshed
344
+ $this->set_access_token( sanitize_text_field( urldecode( $decoded_oauth_response->access_token ) ) );
345
+ $request_args['headers']['Authorization'] = 'Bearer ' . sanitize_text_field( $this->get_access_token() );
346
+
347
+ WC_Square_Sync_Logger::log( sprintf( 'Retrying with new refreshed token' ), $start_time, $end_time );
348
+
349
+ // start at the beginning again
350
+ continue;
351
+ } else {
352
+ WC_Square_Sync_Logger::log( sprintf( 'Renewing expired token error - Unknown Error' ), $start_time, $end_time );
353
+
354
+ return false;
355
+ }
356
+ }
357
+
358
+ // handle revoked tokens
359
+ if ( is_object( $decoded_response ) && ! empty( $decoded_response->type ) && 'oauth.revoked' === $decoded_response->type ) {
360
+ WC_Square_Sync_Logger::log( sprintf( 'Token is revoked!' ), $start_time, $end_time );
361
+
362
+ return false;
363
+ }
364
+
365
+ if ( is_wp_error( $response ) ) {
366
+
367
+ WC_Square_Sync_Logger::log( sprintf( '(%s) Try #%d - %s', $debug_label, $try_count, $response->get_error_message() ), $start_time, $end_time );
368
+
369
+ } else {
370
+
371
+ return $response;
372
+
373
+ }
374
+
375
+ $try_count++;
376
+
377
+ if ( $try_count > $max_retries ) {
378
+ break;
379
+ }
380
+
381
+ sleep( 1 );
382
+
383
+ }
384
+
385
+ return false;
386
+
387
+ }
388
+ }
trunk/includes/class-wc-square-connect.php ADDED
@@ -0,0 +1,653 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) {
3
+ exit;
4
+ }
5
+
6
+ /**
7
+ * Class WC_Square_Connect
8
+ *
9
+ * A layer on top of WC_Square_Client, providing convenient wrapper methods
10
+ * to work with the Square API in terms of WC Products and Square Items.
11
+ */
12
+ class WC_Square_Connect {
13
+
14
+ protected $_client;
15
+ public $wc;
16
+ public $log;
17
+
18
+ const MULTIPLE_LOCATION_ACCOUNT_TYPE = 'BUSINESS';
19
+ const SINGLE_LOCATION_ACCOUNT_TYPE = 'LOCATION';
20
+ const ITEM_IMAGE_MULTIPART_BOUNDARY = 'SQUARE-ITEM-IMAGE';
21
+ const MERCHANT_ACCOUNT_TYPE_CACHE_KEY = 'wc_square_merchant_account_type';
22
+ const LOCATIONS_CACHE_KEY = 'wc_square_locations';
23
+ const INVENTORY_CACHE_KEY = 'wc_square_inventory';
24
+ const ITEM_SKU_MAP_CACHE_KEY = 'square_item_sku_map';
25
+
26
+ /**
27
+ * WC_Square_Connect constructor.
28
+ */
29
+ public function __construct( WC_Square_Client $client ) {
30
+
31
+ $this->_client = $client;
32
+
33
+ add_action( 'wp_loaded', array( $this, 'init' ) );
34
+
35
+ }
36
+
37
+ public function init() {
38
+
39
+ add_action( 'woocommerce_square_bulk_syncing_square_to_wc', array( $this, 'clear_item_sku_map_cache' ) );
40
+
41
+ $this->wc = new WC_Square_WC_Products();
42
+
43
+ // add clear transients button in WC system tools
44
+ add_filter( 'woocommerce_debug_tools', array( $this, 'add_debug_tool' ) );
45
+ }
46
+
47
+ /**
48
+ * Add debug tool button
49
+ *
50
+ * @access public
51
+ * @since 1.0.5
52
+ * @version 1.0.17
53
+ * @return array $tools
54
+ */
55
+ public function add_debug_tool( $tools ) {
56
+ if ( ! empty( $_GET['action'] ) && 'wcsquare_clear_transients' === $_GET['action'] && version_compare( WC_VERSION, '3.0', '<' ) ) {
57
+ WC_Square_Utils::delete_transients();
58
+
59
+ echo '<div class="updated"><p>' . esc_html__( 'Square Sync Transients Cleared', 'woocommerce-square' ) . '</p></div>';
60
+ }
61
+
62
+ $tools['wcsquare_clear_transients'] = array(
63
+ 'name' => __( 'Square Sync Transients', 'woocommerce-square' ),
64
+ 'button' => __( 'Clear all transients/cache', 'woocommerce-square' ),
65
+ 'desc' => __( 'This will clear all Square Sync related transients/caches to start fresh. Useful when sync failed halfway through.', 'woocommerce-square' ),
66
+ );
67
+
68
+ if ( version_compare( WC_VERSION, '3.0', '>=' ) ) {
69
+ $tools['wcsquare_clear_transients']['callback'] = 'WC_Square_Utils::delete_transients';
70
+ }
71
+
72
+ return $tools;
73
+ }
74
+
75
+ /**
76
+ * Checks to see if token is valid.
77
+ *
78
+ * There is no formal way to check this other than to
79
+ * retrieve the merchant account details and if it comes back
80
+ * with a code 200, we assume it is valid.
81
+ *
82
+ * @access public
83
+ * @since 1.0.0
84
+ * @version 1.0.0
85
+ * @return bool
86
+ */
87
+ public function is_valid_token() {
88
+
89
+ $merchant_account_type = $this->get_square_merchant_account_type();
90
+
91
+ return ( false !== $merchant_account_type );
92
+
93
+ }
94
+
95
+ /**
96
+ * Retrieve merchant's account information, such as business name and email address.
97
+ *
98
+ * Endpoint doc: https://docs.connect.squareup.com/api/connect/v1/#navsection-merchant
99
+ * Return value doc: https://docs.connect.squareup.com/api/connect/v1/#datatype-merchant
100
+ *
101
+ * @access public
102
+ * @since 1.0.0
103
+ * @version 1.0.0
104
+ * @return false|null|object False if an HTTP error or non-200 status is encountered. Null on JSON decode error. Object on success.
105
+ */
106
+ public function get_square_merchant() {
107
+
108
+ return $this->_client->request( 'Retrieving Merchant', 'me' );
109
+
110
+ }
111
+
112
+ /**
113
+ * Get the account type for the merchant.
114
+ *
115
+ * See: https://docs.connect.squareup.com/api/connect/v1/#enum-merchantaccounttype
116
+ *
117
+ * @access public
118
+ * @since 1.0.0
119
+ * @version 1.0.0
120
+ * @return bool|string Boolean false on failure, string account type (LOCATION or BUSINESS) on success.
121
+ */
122
+ public function get_square_merchant_account_type() {
123
+
124
+ $account_type = get_transient( self::MERCHANT_ACCOUNT_TYPE_CACHE_KEY );
125
+
126
+ if ( false === $account_type ) {
127
+
128
+ $merchant = $this->get_square_merchant();
129
+
130
+ if ( is_null( $merchant ) || false === $merchant ) {
131
+
132
+ return false;
133
+
134
+ }
135
+
136
+ if ( isset( $merchant->account_type ) ) {
137
+
138
+ $account_type = $merchant->account_type;
139
+
140
+ set_transient( self::MERCHANT_ACCOUNT_TYPE_CACHE_KEY, $account_type, DAY_IN_SECONDS );
141
+
142
+ }
143
+
144
+ }
145
+
146
+ return $account_type;
147
+
148
+ }
149
+
150
+ /**
151
+ * Gets the locations of the business.
152
+ *
153
+ * @access public
154
+ * @since 1.0.0
155
+ * @version 1.0.0
156
+ * @return array $locations
157
+ */
158
+ public function get_square_business_locations() {
159
+
160
+ if ( false !== ( $locations = get_transient( self::LOCATIONS_CACHE_KEY ) ) ) {
161
+
162
+ if ( ! empty( $locations ) ) {
163
+ return $locations;
164
+ }
165
+
166
+ }
167
+
168
+ $locations = array();
169
+ $account_type = $this->get_square_merchant_account_type();
170
+
171
+ /*
172
+ * Only "BUSINESS" accounts have multiple locations that need to be
173
+ * retrieved from a separate endpoint
174
+ * See: https://docs.connect.squareup.com/api/connect/v1/#get-locations
175
+ */
176
+ if ( self::MULTIPLE_LOCATION_ACCOUNT_TYPE === $account_type ) {
177
+
178
+ $items = $this->_client->request( 'Retrieve Business Locations', 'me/locations' );
179
+
180
+ if ( ! empty( $items ) ) {
181
+
182
+ foreach( $items as $item ) {
183
+ if ( is_object( $item ) ) {
184
+ $locations[ $item->id ] = $item->name;
185
+ }
186
+ }
187
+ }
188
+ }
189
+
190
+ /*
191
+ * Single location accounts have all the details under the Merchant object
192
+ */
193
+ if ( self::SINGLE_LOCATION_ACCOUNT_TYPE === $account_type ) {
194
+
195
+ $merchant = $this->get_square_merchant();
196
+
197
+ if ( isset( $merchant->id ) ) {
198
+
199
+ $locations[ $merchant->id ] = $merchant->name;
200
+
201
+ }
202
+
203
+ }
204
+
205
+ set_transient( self::LOCATIONS_CACHE_KEY, $locations, apply_filters( 'woocommerce_square_business_location_cache', DAY_IN_SECONDS ) );
206
+
207
+ return $locations;
208
+
209
+ }
210
+
211
+ /**
212
+ * Create a Square Item for a WC Product.
213
+ *
214
+ * See: https://docs.connect.squareup.com/api/connect/v1/#post-items
215
+ *
216
+ * @param WC_Product $wc_product
217
+ * @param bool $include_category
218
+ * @param bool $include_inventory
219
+ * @return object|bool Created Square Item object on success, boolean False on failure.
220
+ */
221
+ public function create_square_product( $wc_product, $include_category = false, $include_inventory = false ) {
222
+
223
+ // We can only handle simple products or ones with variations
224
+ if ( ! in_array( ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->product_type : $wc_product->get_type() ), array( 'simple', 'variable' ) ) ) {
225
+
226
+ return false;
227
+
228
+ }
229
+
230
+ // TODO: Consider making this method "dumber" - remove this formatting call.
231
+ $product = WC_Square_Utils::format_wc_product_create_for_square_api(
232
+ $wc_product,
233
+ $include_category,
234
+ $include_inventory
235
+ );
236
+
237
+ return $this->_client->request( 'Creating Square Base Product from: ' . ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->id : $wc_product->get_id() ), 'items', 'POST', $product );
238
+
239
+ }
240
+
241
+ /**
242
+ * Update the corresponding Square Item for a WC Product.
243
+ *
244
+ * See: https://docs.connect.squareup.com/api/connect/v1/#put-itemid
245
+ *
246
+ * @param WC_Product $wc_product
247
+ * @param string $square_item_id
248
+ * @param bool $include_category
249
+ * @param bool $include_inventory
250
+ * @return object|bool Updated Square Item object on success, boolean False on failure.
251
+ */
252
+ public function update_square_product( $wc_product, $square_item_id, $include_category = false, $include_inventory = false ) {
253
+
254
+ // We can only handle simple products or ones with variations
255
+ if ( ! in_array( ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->product_type : $wc_product->get_type() ), array( 'simple', 'variable' ) ) ) {
256
+
257
+ return false;
258
+
259
+ }
260
+
261
+ // TODO: Consider making this method "dumber" - remove this formatting call.
262
+ $product = WC_Square_Utils::format_wc_product_update_for_square_api( $wc_product, $include_category );
263
+
264
+ $endpoint = 'items/' . $square_item_id;
265
+
266
+ return $this->_client->request( 'Updating a Square Base Product for: ' . $square_item_id, $endpoint, 'PUT', $product );
267
+
268
+ }
269
+
270
+ /**
271
+ * Set the HTTP request Content-Type header to multipart/form-data for uploading Item images.
272
+ *
273
+ * @param array $http_args
274
+ * @return array
275
+ */
276
+ public function square_product_image_update_filter_http_args( $http_args ) {
277
+
278
+ if ( empty( $http_args['headers'] ) ) {
279
+
280
+ $http_args['headers'] = array();
281
+
282
+ }
283
+
284
+ $http_args['headers']['Content-Type'] = 'multipart/form-data; boundary=' . self::ITEM_IMAGE_MULTIPART_BOUNDARY;
285
+
286
+ return $http_args;
287
+
288
+ }
289
+
290
+ /**
291
+ * Updates the master image for an Item
292
+ * See: https://docs.connect.squareup.com/api/connect/v1/#post-image
293
+ *
294
+ * @param string $square_item_id Square Item ID to upload image for.
295
+ * @param string $mime_type Mime type of the image.
296
+ * @param string $path_to_image Full path to image, accessible using file_get_contents().
297
+ * @return bool|object Response object on success, boolean false on failure.
298
+ */
299
+ public function update_square_product_image( $square_item_id, $mime_type, $path_to_image ) {
300
+
301
+ // The WP HTTP API doesn't natively support multipart form data, so we must build the body ourselves
302
+ // See: http://lists.automattic.com/pipermail/wp-hackers/2013-January/045105.html
303
+ $request_body = '--' . self::ITEM_IMAGE_MULTIPART_BOUNDARY . "\r\n";
304
+ $request_body .= 'Content-Disposition: form-data; name="image_data"; filename="' . basename( $path_to_image ) . '"' . "\r\n";
305
+ $request_body .= 'Content-Type: ' . $mime_type . "\r\n\r\n"; // requires two CRLFs
306
+ $request_body .= file_get_contents( $path_to_image );
307
+ $request_body .= "\r\n--" . self::ITEM_IMAGE_MULTIPART_BOUNDARY . "--\r\n\r\n"; // requires two CRLFs
308
+
309
+ $api_path = '/items/' . $square_item_id . '/image';
310
+
311
+ add_filter( 'woocommerce_square_request_args', array( $this, 'square_product_image_update_filter_http_args' ) );
312
+
313
+ $result = $this->_client->request( 'Updating Square Item Image for: ' . $square_item_id, $api_path, 'POST', $request_body );
314
+
315
+ remove_filter( 'woocommerce_square_request_args', array( $this, 'square_product_image_update_filter_http_args' ) );
316
+
317
+ return $result;
318
+
319
+ }
320
+
321
+ /**
322
+ * Updates a product for a particular location
323
+ * Square API does not allow updates to a product along with variations (sucks)
324
+ * So a separate requests have to be made to update the variations see - update_square_variation()
325
+ *
326
+ * @access public
327
+ * @since 1.0.0
328
+ * @version 1.0.0
329
+ * @param int $product_id id from Square
330
+ * @param object $wc_product
331
+ * @return mixed
332
+ */
333
+ public function update_square_base_product( $s_product_id, $wc_product ) {
334
+
335
+ $product = array(
336
+ 'name' => $wc_product->get_title(),
337
+ 'description' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->post->post_content : $wc_product->get_description(),
338
+ 'visibility' => 'PUBLIC',
339
+ );
340
+
341
+ $category = wp_get_post_terms( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->id : $wc_product->get_id(), 'product_cat', array( 'parent' => 0 ) );
342
+
343
+ if ( ! empty( $category ) ) {
344
+
345
+ $square_cat_id = WC_Square_Utils::wc_compat_get_term_meta( $category[0]->term_id, 'square_cat_id', true );
346
+
347
+ $product['category_id'] = $square_cat_id;
348
+
349
+ }
350
+
351
+ $endpoint = 'items/' . $s_product_id;
352
+
353
+ return $this->_client->request( 'Updating a Square Base Product for: ' . $s_product_id, $endpoint, 'PUT', $product );
354
+
355
+ }
356
+
357
+ /**
358
+ * Updates a single product variation
359
+ * Note that each product has at least one variation in Square
360
+ * Square does not allow multiple variation to be updated at the same time
361
+ *
362
+ * @param string $square_item_id id from Square
363
+ * @param object|array $variation_data Data to create ItemVariation with
364
+ * @return mixed
365
+ */
366
+ public function create_square_variation( $square_item_id, $variation_data ) {
367
+ if ( empty( $square_item_id ) ) {
368
+ return false;
369
+ }
370
+
371
+ $endpoint = '/items/' . $square_item_id . '/variations';
372
+
373
+ return $this->_client->request( 'Creating a Square Product Variation for: ' . $square_item_id, $endpoint, 'POST', $variation_data );
374
+
375
+ }
376
+
377
+ /**
378
+ * Updates a single product variation
379
+ * Note that each product has at least one variation in Square
380
+ * Square does not allow multiple variation to be updated at the same time
381
+ *
382
+ * @param string $square_item_id id from Square
383
+ * @param string $square_variation_id id from Square
384
+ * @param object|array $variation_data Data to update ItemVariation with
385
+ * @return mixed
386
+ */
387
+ public function update_square_variation( $square_item_id, $square_variation_id, $variation_data ) {
388
+ if ( empty( $square_item_id ) ) {
389
+ return false;
390
+ }
391
+
392
+ $endpoint = '/items/' . $square_item_id . '/variations/' . $square_variation_id;
393
+
394
+ return $this->_client->request( 'Updating a Square Product Variation for: ' . $square_variation_id, $endpoint, 'PUT', $variation_data );
395
+
396
+ }
397
+
398
+ /**
399
+ * Deletes a single product variation
400
+ *
401
+ * @access public
402
+ * @since 1.0.0
403
+ * @version 1.0.0
404
+ * @param int $s_product_id id from Square
405
+ * @param int $s_variation_id id from Square
406
+ * @return mixed
407
+ */
408
+ public function delete_square_variation( $s_product_id, $s_variation_id ) {
409
+
410
+ $endpoint = '/items/' . $s_product_id . '/variations/' . $s_variation_id;
411
+
412
+ return $this->_client->request( 'Deleting a Square Product Variation for: ' . $s_variation_id, $endpoint, 'DELETE' );
413
+
414
+ }
415
+
416
+ /**
417
+ * Gets the products for a particular location
418
+ *
419
+ * @access public
420
+ * @since 1.0.0
421
+ * @version 1.0.0
422
+ * @return mixed
423
+ */
424
+ public function get_square_products() {
425
+
426
+ $response = $this->_client->request( 'Retrieving Products', 'items' );
427
+
428
+ return $response ? $response : array();
429
+
430
+ }
431
+
432
+ /**
433
+ * Gets a product for a particular location
434
+ *
435
+ * @access public
436
+ * @since 1.0.0
437
+ * @version 1.0.0
438
+ * @param string $s_product_id
439
+ * @return mixed
440
+ */
441
+ public function get_square_product( $s_product_id ) {
442
+
443
+ if ( empty( $s_product_id ) ) {
444
+
445
+ return array();
446
+
447
+ }
448
+
449
+ $endpoint = 'items/' . $s_product_id;
450
+ $response = $this->_client->request( 'Retrieving a Square Product for: ' . $s_product_id, $endpoint );
451
+
452
+ return $response ? $response : array();
453
+
454
+ }
455
+
456
+ /**
457
+ * Clear the Square Item SKU Map Cache
458
+ */
459
+ public function clear_item_sku_map_cache() {
460
+
461
+ delete_transient( self::ITEM_SKU_MAP_CACHE_KEY );
462
+
463
+ }
464
+
465
+ /**
466
+ * Generate a mapping of Square Item IDs to their associated SKUs.
467
+ *
468
+ * @return array List of Square Item IDs and their variation SKUs.
469
+ */
470
+ public function get_square_product_sku_map() {
471
+
472
+ if ( $cached = get_transient( self::ITEM_SKU_MAP_CACHE_KEY ) ) {
473
+
474
+ return $cached;
475
+
476
+ }
477
+
478
+ $square_products = $this->get_square_products();
479
+ $square_product_sku_map = array();
480
+ $processed_ids = array();
481
+
482
+ foreach ( $square_products as $s_product ) {
483
+ // Square may return dups so make sure we check for that
484
+ if ( in_array( $s_product->id, $processed_ids ) ) {
485
+ continue;
486
+ }
487
+
488
+ $square_product_sku_map[ $s_product->id ] = array();
489
+
490
+ foreach ( $s_product->variations as $s_variation ) {
491
+
492
+ if ( ! empty( $s_variation->sku ) ) {
493
+
494
+ $square_product_sku_map[ $s_product->id ][] = $s_variation->sku;
495
+
496
+ }
497
+ }
498
+
499
+ $processed_ids[] = $s_product;
500
+ }
501
+
502
+ set_transient( self::ITEM_SKU_MAP_CACHE_KEY, $square_product_sku_map, apply_filters( 'woocommerce_square_item_sku_cache', DAY_IN_SECONDS ) );
503
+
504
+ return $square_product_sku_map;
505
+
506
+ }
507
+
508
+ /**
509
+ * Checks if product exists on square
510
+ *
511
+ * @access public
512
+ * @since 1.0.0
513
+ * @version 1.0.0
514
+ * @param string|array $sku_list One or more SKUs to get a product by
515
+ * @return false if not exists or product object
516
+ */
517
+ public function square_product_exists( $sku_list ) {
518
+
519
+ $square_item_sku_map = $this->get_square_product_sku_map();
520
+
521
+ $sku_list = (array) $sku_list;
522
+
523
+ foreach ( $square_item_sku_map as $square_item_id => $square_item_skus ) {
524
+
525
+ if ( array_intersect( $sku_list, $square_item_skus ) ) {
526
+
527
+ return $this->get_square_product( $square_item_id );
528
+
529
+ }
530
+
531
+ }
532
+
533
+ return false;
534
+
535
+ }
536
+
537
+ /**
538
+ * Gets the categories for a particular location
539
+ *
540
+ * @access public
541
+ * @since 1.0.0
542
+ * @version 1.0.0
543
+ * @return mixed
544
+ */
545
+ public function get_square_categories() {
546
+
547
+ $response = $this->_client->request( 'Retrieving Square Categories', 'categories' );
548
+
549
+ return $response ? $response : array();
550
+
551
+ }
552
+
553
+ /**
554
+ * Create a square category
555
+ *
556
+ * @access public
557
+ * @since 1.0.0
558
+ * @version 1.0.0
559
+ * @param string $name
560
+ * @return mixed
561
+ */
562
+ public function create_square_category( $name ) {
563
+
564
+ $category = array(
565
+ 'name' => sanitize_text_field( $name ),
566
+ );
567
+
568
+ return $this->_client->request( 'Creating Square Category for: ' . sanitize_text_field( $name ), 'categories', 'POST', $category );
569
+
570
+ }
571
+
572
+ /**
573
+ * Update a Square Category
574
+ *
575
+ * @param string $square_category_id
576
+ * @param string $name
577
+ *
578
+ * @return bool|object|WP_Error
579
+ */
580
+ public function update_square_category( $square_category_id, $name ) {
581
+
582
+ $endpoint = 'categories/' . $square_category_id;
583
+ $category = array(
584
+ 'name' => sanitize_text_field( $name ),
585
+ );
586
+
587
+ return $this->_client->request( 'Updating Category for: ' . sanitize_text_field( $name ), $endpoint, 'PUT', $category );
588
+
589
+ }
590
+
591
+ /**
592
+ * Get square variation inventory.
593
+ *
594
+ * Note: There is no current way to get a specific product variation's inventory.
595
+ *
596
+ * @return array
597
+ */
598
+ public function get_square_inventory() {
599
+ if ( $cached = get_transient( self::INVENTORY_CACHE_KEY ) ) {
600
+
601
+ return $cached;
602
+
603
+ }
604
+
605
+ $response = $this->_client->request( 'Getting All Square Inventory', 'inventory' ); // default 1000 max limit
606
+
607
+ $square_inventory = array();
608
+
609
+ if ( is_array( $response ) ) {
610
+
611
+ $square_inventory_ids = wp_list_pluck( $response, 'variation_id' );
612
+ $square_inventory_quantities = wp_list_pluck( $response, 'quantity_on_hand' );
613
+ $square_inventory = array_combine( $square_inventory_ids, $square_inventory_quantities );
614
+
615
+ }
616
+
617
+ set_transient( self::INVENTORY_CACHE_KEY, $square_inventory, apply_filters( 'woocommerce_square_inventory_cache', DAY_IN_SECONDS ) );
618
+
619
+ return $square_inventory;
620
+
621
+ }
622
+
623
+ /**
624
+ * Updates square variation inventory
625
+ *
626
+ * @access public
627
+ * @since 1.0.0
628
+ * @version 1.0.0
629
+ * @param string $square_variation_id
630
+ * @param int $quantity_delta
631
+ * @param string $type the type of adjustment MANUAL_ADJUST, RECEIVE_STOCK, SALE
632
+ * @return bool
633
+ */
634
+ public function update_square_inventory( $square_variation_id, $quantity_delta, $type = 'MANUAL_ADJUST' ) {
635
+
636
+ $endpoint = 'inventory/' . $square_variation_id;
637
+ $inventory = array(
638
+ 'quantity_delta' => $quantity_delta,
639
+ 'adjustment_type' => $type,
640
+ );
641
+ $response = $this->_client->request( 'Updating Square Inventory for: ' . $square_variation_id, $endpoint, 'POST', $inventory );
642
+
643
+ return ( false !== $response );
644
+
645
+ }
646
+
647
+ /**
648
+ * Refresh the Square Inventory cache.
649
+ */
650
+ public function refresh_inventory_cache() {
651
+ delete_transient( self::INVENTORY_CACHE_KEY );
652
+ }
653
+ }
trunk/includes/class-wc-square-deactivation.php ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) {
3
+ exit; // Exit if accessed directly
4
+ }
5
+
6
+ class WC_Square_Deactivation {
7
+
8
+ /**
9
+ * Constructor not to be instantiated
10
+ *
11
+ * @access private
12
+ * @since 1.0.0
13
+ * @version 1.0.0
14
+ * @return bool
15
+ */
16
+ private function __construct() {}
17
+
18
+ /**
19
+ * Perform deactivation tasks
20
+ *
21
+ * @access public
22
+ * @since 1.0.0
23
+ * @version 1.0.0
24
+ * @return bool
25
+ */
26
+ public static function deactivate() {
27
+ wp_clear_scheduled_hook( 'woocommerce_square_inventory_poll' );
28
+
29
+ return true;
30
+ }
31
+ }
trunk/includes/class-wc-square-install.php ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) {
3
+ exit; // Exit if accessed directly
4
+ }
5
+
6
+ /**
7
+ * Installation/Activation Class.
8
+ *
9
+ * Handles the activation/installation of the plugin.
10
+ *
11
+ * @category Installation
12
+ * @package WooCommerce Square/Install
13
+ * @version 1.0.0
14
+ */
15
+ class WC_Square_Install {
16
+ /**
17
+ * Intialize
18
+ *
19
+ * @access public
20
+ * @version 1.0.0
21
+ * @since 1.0.0
22
+ * @return bool
23
+ */
24
+ public static function init() {
25
+ add_action( 'admin_init', array( __CLASS__, 'check_version' ), 5 );
26
+
27
+ return true;
28
+ }
29
+
30
+ /**
31
+ * Checks the plugin version
32
+ *
33
+ * @access public
34
+ * @since 1.0.0
35
+ * @version 1.0.0
36
+ * @return bool
37
+ */
38
+ public static function check_version() {
39
+ if ( ! defined( 'IFRAME_REQUEST' ) && ( get_option( 'wc_square_version' ) != WC_SQUARE_VERSION ) ) {
40
+ self::install();
41
+
42
+ do_action( 'wc_square_updated' );
43
+ }
44
+
45
+ return true;
46
+ }
47
+
48
+ /**
49
+ * Do installs.
50
+ *
51
+ * @access public
52
+ * @since 1.0.0
53
+ * @version 1.0.0
54
+ * @return bool
55
+ */
56
+ public static function install() {
57
+ self::update_plugin_version();
58
+
59
+ return true;
60
+ }
61
+
62
+ /**
63
+ * Updates the plugin version in db
64
+ *
65
+ * @access public
66
+ * @since 1.0.0
67
+ * @version 1.0.0
68
+ * @return bool
69
+ */
70
+ private static function update_plugin_version() {
71
+ delete_option( 'wc_square_version' );
72
+ add_option( 'wc_square_version', WC_SQUARE_VERSION );
73
+
74
+ return true;
75
+ }
76
+ }
77
+
78
+ WC_Square_Install::init();
trunk/includes/class-wc-square-inventory-poll.php ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) {
3
+ exit;
4
+ }
5
+
6
+ /**
7
+ * Class WC_Square_Inventory_Poll
8
+ *
9
+ * Cron driven methods to poll Square's inventory at intervals.
10
+ * This is to replace the webhook method as it is not recommended by Square.
11
+ */
12
+ class WC_Square_Inventory_Poll {
13
+ protected $integration;
14
+ protected $to_wc;
15
+
16
+ /**
17
+ * Constructor
18
+ *
19
+ * @since 1.0.0
20
+ * @version 1.0.0
21
+ * @param object $integration
22
+ * @param object $to_wc
23
+ */
24
+ public function __construct( WC_Square_Integration $integration, WC_Square_Sync_From_Square $to_wc ) {
25
+ $this->integration = $integration;
26
+ $this->to_wc = $to_wc;
27
+
28
+ add_action( 'init', array( $this, 'run_schedule' ) );
29
+
30
+ // the scheduled cron will trigger this event
31
+ add_action( 'woocommerce_square_inventory_poll', array( $this, 'sync' ) );
32
+ }
33
+
34
+ public function run_schedule() {
35
+ $frequency = apply_filters( 'woocommerce_square_inventory_poll_frequency', 'hourly' );
36
+
37
+ if ( ! wp_next_scheduled( 'woocommerce_square_inventory_poll' ) ) {
38
+ wp_schedule_event( time(), $frequency, 'woocommerce_square_inventory_poll' );
39
+ }
40
+ }
41
+
42
+ public function sync() {
43
+ $sync = ( 'yes' === $this->integration->get_option( 'inventory_polling' ) );
44
+ $manual_sync_processing = get_transient( 'wc_square_manual_sync_processing' );
45
+
46
+ if ( $sync && ! $manual_sync_processing ) {
47
+ $this->to_wc->sync_all_inventory();
48
+ }
49
+ }
50
+ }
trunk/includes/class-wc-square-sync-from-square.php ADDED
@@ -0,0 +1,411 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) {
3
+ exit; // Exit if accessed directly
4
+ }
5
+
6
+ /**
7
+ * Class WC_Square_Sync_From_Square
8
+ *
9
+ * Methods to Sync from Square to WC. Organized as "sync" methods that
10
+ * determine if "create" or "update" actions should be taken on the entities
11
+ * involved.
12
+ *
13
+ * NOTE: Existing WC Products are *not* updated with data from their
14
+ * corresponding Square Item due to a lack of modified timestamp
15
+ * on the Square API side.
16
+ */
17
+ class WC_Square_Sync_From_Square {
18
+
19
+ /**
20
+ * @var WC_Square_Connect
21
+ */
22
+ protected $connect;
23
+
24
+ /**
25
+ * WC_Square_Sync_From_Square constructor.
26
+ */
27
+ public function __construct( WC_Square_Connect $connect ) {
28
+
29
+ $this->connect = $connect;
30
+
31
+ }
32
+
33
+ /**
34
+ * Sync Square categories to WooCommerce.
35
+ *
36
+ * Looks for category names that don't exist in WooCommerce, and creates them.
37
+ */
38
+ public function sync_categories() {
39
+
40
+ $square_category_objects = $this->connect->get_square_categories();
41
+ $square_categories = array();
42
+ $processed_categories = array();
43
+
44
+ foreach ( $square_category_objects as $square_category ) {
45
+ // Square list endpoints may return dups so we must check for that
46
+ if ( in_array( $square_category->id, $processed_categories ) ) {
47
+ continue;
48
+ }
49
+
50
+ if ( is_object( $square_category ) && ! empty( $square_category->name ) && ! empty( $square_category->id ) ) {
51
+ $square_categories[ $square_category->name ] = $square_category->id;
52
+ $processed_categories[] = $square_category->id;
53
+ }
54
+ }
55
+
56
+ if ( empty( $square_categories ) ) {
57
+
58
+ WC_Square_Sync_Logger::log( '[Square -> WC] No categories found to sync.' );
59
+ return;
60
+
61
+ }
62
+
63
+ $wc_category_objects = $this->connect->wc->get_product_categories();
64
+ $wc_categories = array();
65
+
66
+ if ( is_wp_error( $wc_category_objects ) ) {
67
+
68
+ WC_Square_Sync_Logger::log( '[Square -> WC] Error encountered retrieving WC Product Categories: ' . $wc_category_objects->get_error_message() );
69
+ return;
70
+
71
+ }
72
+
73
+ if ( ! empty( $wc_category_objects['product_categories'] ) ) {
74
+
75
+ foreach ( $wc_category_objects['product_categories'] as $wc_category ) {
76
+
77
+ if ( empty( $wc_category['name'] ) || empty( $wc_category['id'] ) || ( 0 !== $wc_category['parent'] ) ) {
78
+ continue;
79
+ }
80
+
81
+ $wc_categories[ $wc_category['name'] ] = $wc_category['id'];
82
+
83
+ }
84
+ }
85
+
86
+ // Look for previously synced categories and update them with data from Square
87
+ foreach ( $wc_categories as $wc_cat_name => $wc_cat_id ) {
88
+
89
+ $wc_square_cat_id = WC_Square_Utils::get_wc_term_square_id( $wc_cat_id );
90
+
91
+ // Make sure the associated Square ID still exists on the Square side
92
+ if ( $wc_square_cat_id && ( $square_cat_name = array_search( $wc_square_cat_id, $square_categories ) ) ) {
93
+
94
+ $result = $this->connect->wc->edit_product_category( $wc_cat_id, array(
95
+ 'product_category' => array(
96
+ 'name' => $square_cat_name,
97
+ ),
98
+ ) );
99
+
100
+ if ( is_wp_error( $result ) ) {
101
+
102
+ WC_Square_Sync_Logger::log( sprintf( '[Square -> WC] Error updating WC Product Category %d for Square ID %s: %s', $wc_cat_id, $wc_square_cat_id, $result->get_error_message() ) );
103
+ continue;
104
+
105
+ } elseif ( empty( $result['product_category'] ) ) {
106
+
107
+ WC_Square_Sync_Logger::log( sprintf( '[Square -> WC] Unexpected empty result updating WC Product Category %d for Square ID %s.', $wc_cat_id, $wc_square_cat_id ) );
108
+ continue;
109
+
110
+ }
111
+
112
+ // We no longer need to process this Square Category, so remove from list
113
+ unset( $square_categories[ $square_cat_name ] );
114
+
115
+ } else {
116
+
117
+ WC_Square_Sync_Logger::log( sprintf( '[Square -> WC] Cannot sync Square Category ID %s, it no longer exists.', $wc_square_cat_id ) );
118
+
119
+ }
120
+ }
121
+
122
+ /*
123
+ * Go through the remaining Square Categories and either:
124
+ * 1) Match them to an existing WC Category
125
+ * 2) Create a new WC Category
126
+ */
127
+ foreach ( $square_categories as $name => $square_id ) {
128
+
129
+ if ( empty( $wc_categories[ $name ] ) ) {
130
+
131
+ $result = $this->connect->wc->create_product_category( array(
132
+ 'product_category' => array(
133
+ 'name' => $name,
134
+ ),
135
+ ) );
136
+
137
+ if ( is_wp_error( $result ) ) {
138
+
139
+ WC_Square_Sync_Logger::log( sprintf( '[Square -> WC] Error creating WC Product Category for Square ID %s: %s', $wc_square_cat_id, $result->get_error_message() ) );
140
+ continue;
141
+
142
+ } elseif ( empty( $result['product_category'] ) ) {
143
+
144
+ WC_Square_Sync_Logger::log( sprintf( '[Square -> WC] Unexpected empty result creating WC Product Category for Square ID %s.', $wc_square_cat_id ) );
145
+ continue;
146
+
147
+ }
148
+
149
+ $wc_term_id = $result['product_category']['id'];
150
+
151
+ } else {
152
+
153
+ $wc_term_id = $wc_categories[ $name ];
154
+
155
+ }
156
+
157
+ WC_Square_Utils::update_wc_term_square_id( $wc_term_id, $square_id );
158
+
159
+ }
160
+
161
+ }
162
+
163
+ /**
164
+ * Sync a Square Item to WC, optionally including Categories and Inventory.
165
+ *
166
+ * @param object $square_item
167
+ * @param bool $include_category
168
+ * @param bool $include_inventory
169
+ * @param bool $include_image
170
+ */
171
+ public function sync_product( $square_item, $include_category = false, $include_inventory = false, $include_image = false ) {
172
+ $wc_product = WC_Square_Utils::get_wc_product_for_square_item( $square_item );
173
+ $is_new_product = ( false === $wc_product );
174
+
175
+ // If none of the Square item variations have sku we must skip the sync.
176
+ if ( ! WC_Square_Utils::is_square_item_skus_set( $square_item ) ) {
177
+ WC_Square_Sync_Logger::log( sprintf( '[Square -> WC] Skipping sync for Square item ID %s missing one or more SKU.', $square_item->id ) );
178
+
179
+ return;
180
+ }
181
+
182
+ // Only create items that don't yet exist in WC
183
+ if ( $is_new_product ) {
184
+
185
+ $wc_product = $this->create_product( $square_item, $include_category, $include_inventory, $include_image );
186
+
187
+ WC_Square_Sync_Logger::log( sprintf( '[Square -> WC] Creating WC product for Square Item ID %s.', $square_item->id ) );
188
+ }
189
+
190
+ if ( $wc_product ) {
191
+
192
+ WC_Square_Utils::set_square_ids_on_wc_product_by_sku( $wc_product, $square_item );
193
+
194
+ $wc_product_id = version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->id : $wc_product->get_id();
195
+
196
+ if ( ! $is_new_product ) {
197
+
198
+ $this->update_product( $wc_product, $square_item, $include_category, $include_inventory, $include_image );
199
+
200
+ WC_Square_Sync_Logger::log( sprintf( '[Square -> WC] Updating WC product for Square Item ID %s.', $square_item->id ) );
201
+
202
+ }
203
+
204
+ if ( $include_inventory ) {
205
+ if ( WC_Square_Utils::skip_product_sync( $wc_product_id ) ) {
206
+ WC_Square_Sync_Logger::log( sprintf( '[Square -> WC] Syncing disabled for this WC Product %d for Square ID %s', $wc_product_id, $square_item->id ) );
207
+
208
+ return;
209
+ }
210
+
211
+ $this->sync_inventory( $wc_product, $square_item );
212
+
213
+ WC_Square_Sync_Logger::log( sprintf( '[Square -> WC] Syncing WC product inventory for Square Item ID %s.', $square_item->id ) );
214
+
215
+ }
216
+ } else {
217
+
218
+ WC_Square_Sync_Logger::log( sprintf( '[Square -> WC] Error creating WC Product found for Square Item ID %s.', $square_item->id ) );
219
+ return;
220
+
221
+ }
222
+
223
+ }
224
+
225
+ /**
226
+ * Create a new WC Product using data from Square.
227
+ *
228
+ * @param object $square_item
229
+ * @param bool $include_category
230
+ * @param bool $include_inventory
231
+ * @param bool $include_image
232
+ *
233
+ * @return bool|WC_Product Created WC_Product on success, boolean false on failure.
234
+ */
235
+ public function create_product( $square_item, $include_category = false, $include_inventory = false, $include_image = false ) {
236
+
237
+ $product_update = WC_Square_Utils::format_square_item_for_wc_api_create( $square_item, $include_category, $include_inventory, $include_image );
238
+
239
+ // note here that when creating variations via WC API, if the parent product
240
+ // is in the trash and the SKU matches the variation of the parent, the variations
241
+ // won't be created. This is because the WC API is not checking if variations are
242
+ // published.
243
+ $result = $this->connect->wc->create_product( array( 'product' => $product_update ) );
244
+
245
+ if ( is_wp_error( $result ) ) {
246
+
247
+ WC_Square_Sync_Logger::log( sprintf( '[Square -> WC] Error creating WC Product for Square ID %s: %s', $square_item->id, $result->get_error_message() ) );
248
+
249
+ } elseif ( isset( $result['product']['id'] ) ) {
250
+
251
+ return wc_get_product( $result['product']['id'] );
252
+
253
+ }
254
+
255
+ return false;
256
+
257
+ }
258
+
259
+ /**
260
+ * Update an existing WC Product using data from Square.
261
+ *
262
+ * @param WC_Product $wc_product
263
+ * @param object $square_item
264
+ * @param bool $include_category
265
+ * @param bool $include_inventory
266
+ * @param bool $include_image
267
+ *
268
+ * @return bool|WC_Product Updated WC_Product on success, boolean false on failure.
269
+ */
270
+ public function update_product( WC_Product $wc_product, $square_item, $include_category = false, $include_inventory = false, $include_image = false ) {
271
+
272
+ $wc_product_id = version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->id : $wc_product->get_id();
273
+
274
+ if ( WC_Square_Utils::skip_product_sync( $wc_product_id ) ) {
275
+ WC_Square_Sync_Logger::log( sprintf( '[Square -> WC] Syncing disabled for this WC Product %d for Square ID %s', $wc_product_id, $square_item->id ) );
276
+
277
+ return false;
278
+ }
279
+
280
+ $product_update = WC_Square_Utils::format_square_item_for_wc_api_update( $square_item, $wc_product, $include_category, $include_inventory, $include_image );
281
+
282
+ $result = $this->connect->wc->edit_product( $wc_product_id, array( 'product' => $product_update ) );
283
+
284
+ if ( is_wp_error( $result ) ) {
285
+
286
+ WC_Square_Sync_Logger::log( sprintf( '[Square -> WC] Error updating WC Product %d for Square ID %s: %s', $wc_product_id, $square_item->id, $result->get_error_message() ) );
287
+
288
+ } elseif ( isset( $result['product']['id'] ) ) {
289
+
290
+ return wc_get_product( $result['product']['id'] );
291
+
292
+ }
293
+
294
+ return false;
295
+
296
+ }
297
+
298
+ /**
299
+ * Sync a WC Product's inventory with data from Square
300
+ *
301
+ * @param WC_Product $wc_product
302
+ * @param stdClass $square_item
303
+ */
304
+ public function sync_inventory( WC_Product $wc_product, $square_item ) {
305
+ if ( ! Woocommerce_Square::instance()->is_allowed_countries()
306
+ || ! Woocommerce_Square::instance()->is_allowed_currencies() ) {
307
+ WC_Square_Sync_Logger::log( '[Square -> WC] Error syncing inventory for WC Product - Country or Currency mismatch' );
308
+ return;
309
+ }
310
+
311
+ $wc_variation_ids = WC_Square_Utils::get_stock_managed_wc_variation_ids( $wc_product );
312
+ $square_inventory = $this->connect->get_square_inventory();
313
+
314
+ foreach ( $wc_variation_ids as $wc_variation_id ) {
315
+
316
+ $square_variation_id = WC_Square_Utils::get_wc_variation_square_id( $wc_variation_id );
317
+
318
+ if ( ! $square_variation_id || ! isset( $square_inventory[ $square_variation_id ] ) ) {
319
+
320
+ continue;
321
+
322
+ }
323
+
324
+ // check each variation stock_tracking setting and set stock if tracking is enabled
325
+ foreach ( $square_item->variations as $variation_item ) {
326
+
327
+ if ( $variation_item->id == $square_variation_id && $variation_item->track_inventory ) {
328
+
329
+ $square_stock = (int) $square_inventory[ $square_variation_id ];
330
+ $wc_variation = wc_get_product( $wc_variation_id );
331
+
332
+ $current_stock = $wc_variation->get_stock_quantity();
333
+
334
+ // Do not trigger sync if setting same stock quantity
335
+ if ( $square_stock === $current_stock ) {
336
+ continue;
337
+ }
338
+
339
+ $result = version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_variation->set_stock( $square_stock ) : wc_update_product_stock( $wc_variation, $square_stock );
340
+
341
+ }
342
+ }
343
+ }
344
+ }
345
+
346
+ /**
347
+ * Sync all inventory from Square (expensive)
348
+ * @todo if searching for square id fails, check for SKU
349
+ */
350
+ public function sync_all_inventory() {
351
+ if ( ! Woocommerce_Square::instance()->is_allowed_countries()
352
+ || ! Woocommerce_Square::instance()->is_allowed_currencies() ) {
353
+ WC_Square_Sync_Logger::log( '[Square -> WC] Error syncing all inventory - Country or Currency mismatch' );
354
+ return;
355
+ }
356
+
357
+ try {
358
+ set_time_limit( apply_filters( 'woocommerce_square_inventory_sync_timeout_limit', 200 ) );
359
+
360
+ // refresh cache first to get the latest inventory
361
+ $this->connect->refresh_inventory_cache();
362
+
363
+ $square_inventory = $this->connect->get_square_inventory();
364
+
365
+ // To prevent infinite loop when stock is updated in WC.
366
+ set_transient( 'wc_square_polling', 'yes', 60 * MINUTE_IN_SECONDS );
367
+
368
+ // hopefully there has been a manual sync prior so that square item id
369
+ // has already been saved in the product/variation metas to prevent
370
+ // unnecessary round trip requests to Square to find the SKU
371
+ foreach ( $square_inventory as $variation_id => $stock ) {
372
+ WC_Square_Sync_Logger::log( sprintf( '[Square -> WC] Syncing WC product inventory for Square Item ID %s.', $variation_id ) );
373
+
374
+ $wc_variation_product = WC_Square_Utils::get_wc_product_for_square_item_variation_id( $variation_id );
375
+
376
+ if ( ! is_object( $wc_variation_product ) ) {
377
+ WC_Square_Sync_Logger::log( sprintf( '[Square -> WC] Syncing WC product inventory for Square Item ID %s - WC product/variation not found skipping.', $variation_id ) );
378
+ continue;
379
+ }
380
+
381
+ $product_id = 0;
382
+
383
+ // check if we need to skip
384
+ if ( 'simple' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_variation_product->product_type : $wc_variation_product->get_type() ) ) {
385
+ $product_id = version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_variation_product->id : $wc_variation_product->get_id();
386
+ } elseif ( 'variation' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_variation_product->product_type : $wc_variation_product->get_type() ) ) {
387
+ $product_id = version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_variation_product->parent->id : $wc_variation_product->get_parent_id();
388
+ }
389
+
390
+ $current_stock = $wc_variation_product->get_stock_quantity();
391
+
392
+ // Do not trigger sync if setting same stock quantity
393
+ if ( $stock === $current_stock ) {
394
+ WC_Square_Sync_Logger::log( sprintf( '[Square -> WC] Syncing WC product inventory for Square Item ID %s - No change in stock quantity for product/variation, skipping.', $variation_id ) );
395
+ continue;
396
+ }
397
+
398
+ if ( is_object( $wc_variation_product ) && ! WC_Square_Utils::skip_product_sync( $product_id ) ) {
399
+ version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_variation_product->set_stock( (int) $stock ) : wc_update_product_stock( $wc_variation_product, (int) $stock );
400
+ }
401
+ }
402
+
403
+ delete_transient( 'wc_square_polling' );
404
+
405
+ return true;
406
+ } catch ( Exception $e ) {
407
+ delete_transient( 'wc_square_polling' );
408
+ WC_Square_Sync_Logger::log( sprintf( '[Square -> WC] Inventory Poll: ', $e->getMessage() ) );
409
+ }
410
+ }
411
+ }
trunk/includes/class-wc-square-sync-logger.php ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) {
3
+ exit; // Exit if accessed directly
4
+ }
5
+
6
+ /**
7
+ * Square sync logging class which saves important data to the log
8
+ *
9
+ * @since 1.0.0
10
+ * @version 1.0.0
11
+ */
12
+ class WC_Square_Sync_Logger {
13
+
14
+ public static $logger;
15
+ const WC_LOG_FILENAME = 'woocommerce-square-sync';
16
+
17
+ /**
18
+ * Utilize WC logger class
19
+ *
20
+ * @since 1.0.0
21
+ * @version 1.0.0
22
+ */
23
+ public static function log( $message, $start_time = null, $end_time = null ) {
24
+ if ( empty( self::$logger ) ) {
25
+ self::$logger = new WC_Logger();
26
+ }
27
+
28
+ $settings = get_option( 'woocommerce_squareconnect_settings', '' );
29
+
30
+ if ( ! empty( $settings['logging'] ) && 'yes' !== $settings['logging'] ) {
31
+ return;
32
+ }
33
+
34
+ if ( ! is_null( $start_time ) ) {
35
+
36
+ $formatted_start_time = date_i18n( get_option( 'date_format' ) . ' g:ia', $start_time );
37
+ $end_time = is_null( $end_time ) ? current_time( 'timestamp' ) : $end_time;
38
+ $formatted_end_time = date_i18n( get_option( 'date_format' ) . ' g:ia', $end_time );
39
+ $elapsed_time = round( abs( $end_time - $start_time ) / 60, 2 );
40
+
41
+ $log_entry = '====Start Log ' . $formatted_start_time . '====' . "\n" . $message . "\n";
42
+ $log_entry .= '====End Log ' . $formatted_end_time . ' (' . $elapsed_time . ')====' . "\n\n";
43
+
44
+ } else {
45
+
46
+ $log_entry = '====Start Log====' . "\n" . $message . "\n" . '====End Log====' . "\n\n";
47
+
48
+ }
49
+
50
+ self::$logger->add( self::WC_LOG_FILENAME, $log_entry );
51
+ }
52
+ }
trunk/includes/class-wc-square-sync-to-square-wp-hooks.php ADDED
@@ -0,0 +1,304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) {
3
+ exit; // Exit if accessed directly
4
+ }
5
+
6
+ /**
7
+ * Class WC_Square_Sync_To_Square_WordPress_Hooks
8
+ *
9
+ * Attach WC_Square_Sync_To_Square methods to WordPress and WooCommerce core hooks.
10
+ */
11
+ class WC_Square_Sync_To_Square_WordPress_Hooks {
12
+
13
+ /**
14
+ * @var WC_Integration
15
+ */
16
+ protected $integration;
17
+
18
+ /**
19
+ * @var WC_Square_Sync_To_Square
20
+ */
21
+ protected $square;
22
+
23
+ /**
24
+ * Whether or not to sync product data to Square.
25
+ *
26
+ * @var bool
27
+ */
28
+ protected $sync_products;
29
+
30
+ /**
31
+ * @var bool
32
+ */
33
+ protected $sync_categories;
34
+
35
+ /**
36
+ * @var bool
37
+ */
38
+ protected $sync_images;
39
+
40
+ /**
41
+ * @var bool
42
+ */
43
+ protected $sync_inventory;
44
+
45
+ /**
46
+ * Whether or not hooks should fire.
47
+ *
48
+ * @var bool
49
+ */
50
+ protected $enabled = true;
51
+
52
+ /**
53
+ * Keep track of which products have already assigned a sync job
54
+ *
55
+ * @var array
56
+ */
57
+ protected $products_synced = array(
58
+ );
59
+
60
+ /**
61
+ * WC_Square_Sync_To_Square_WordPress_Hooks constructor.
62
+ *
63
+ * @param WC_Integration $integration
64
+ * @param WC_Square_Sync_To_Square $square
65
+ */
66
+ public function __construct( WC_Integration $integration, WC_Square_Sync_To_Square $square ) {
67
+
68
+ $this->integration = $integration;
69
+ $this->square = $square;
70
+
71
+ $this->sync_products = ( 'yes' === $integration->get_option( 'sync_products' ) );
72
+ $this->sync_categories = ( 'yes' === $integration->get_option( 'sync_categories' ) );
73
+ $this->sync_images = ( 'yes' === $integration->get_option( 'sync_images' ) );
74
+ $this->sync_inventory = ( 'yes' === $integration->get_option( 'sync_inventory' ) );
75
+
76
+ add_action( 'wc_square_loaded', array( $this, 'attach_hooks' ) );
77
+ add_action( 'wc_square_save_post_event', array( $this, 'process_save_post_event' ), 10, 2 );
78
+ add_action( 'wc_square_on_product_set_stock_event', array( $this, 'on_product_set_stock' ) );
79
+ add_action( 'wc_square_on_variation_set_stock_event', array( $this, 'on_variation_set_stock' ) );
80
+ }
81
+
82
+ /**
83
+ * Dynamically enable WP/WC hook callbacks.
84
+ */
85
+ public function enable() {
86
+
87
+ $this->enabled = true;
88
+
89
+ }
90
+
91
+ /**
92
+ * Dynamically disable WP/WC hook callbacks.
93
+ */
94
+ public function disable() {
95
+
96
+ $this->enabled = false;
97
+
98
+ }
99
+
100
+ /**
101
+ * Hook into WordPress and WooCommerce core.
102
+ */
103
+ public function attach_hooks() {
104
+
105
+ if ( $this->sync_products ) {
106
+ if ( version_compare( WC_VERSION, '3.0.0', '<' ) ) {
107
+ add_action( 'save_post', array( $this, 'pre_wc_30_on_save_post' ), 10, 2 );
108
+ } else {
109
+ add_action( 'woocommerce_before_product_object_save', array( $this, 'on_save_post' ), 10, 2 );
110
+ }
111
+ }
112
+
113
+ if ( $this->sync_categories ) {
114
+
115
+ add_action( 'created_product_cat', array( $this, 'on_category_modified' ) );
116
+
117
+ add_action( 'edited_product_cat', array( $this, 'on_category_modified' ) );
118
+
119
+ }
120
+
121
+ if ( $this->sync_inventory ) {
122
+ $param = isset( $_GET['wc-api'] ) ? $_GET['wc-api'] : '';
123
+
124
+ if ( 'WC_Square_Integration' !== $param ) {
125
+
126
+ // Only add the stock hooks for versions below 3.0. In versions
127
+ // >= 3.0 the save hook will take care of stock inventory as well.
128
+ if ( version_compare( WC_VERSION, '3.0.0', '<' ) ) {
129
+ add_action( 'woocommerce_product_set_stock', array( $this, 'schedule_on_product_set_stock' ) );
130
+
131
+ add_action( 'woocommerce_variation_set_stock', array( $this, 'schedule_on_variation_set_stock' ) );
132
+ }
133
+ }
134
+ }
135
+
136
+ }
137
+
138
+ /**
139
+ * Sync a WC Product to Square when it is saved.
140
+ *
141
+ * @param int $post_id
142
+ * @param bool $sync_categories
143
+ * @param bool $sync_inventory
144
+ * @param bool $sync_images
145
+ */
146
+ public function process_save_post_event( $post_id ) {
147
+ // clear inventory cache
148
+ delete_transient( 'wc_square_inventory' );
149
+
150
+ $wc_product = wc_get_product( $post_id );
151
+
152
+ if ( WC_Square_Utils::skip_product_sync( $post_id ) ) {
153
+ return;
154
+ }
155
+
156
+ if ( is_object( $wc_product ) && ! empty( $wc_product ) ) {
157
+ $this->square->sync_product( $wc_product, $this->sync_categories, $this->sync_inventory, $this->sync_images );
158
+ }
159
+
160
+ WC_Square_Utils::delete_transients();
161
+ }
162
+
163
+ /**
164
+ * Trigger the save post event.
165
+ *
166
+ * @since 1.0.0
167
+ * @version 1.0.20
168
+ * @param object $product
169
+ * @param object $data_store
170
+ */
171
+ public function on_save_post( $product, $data_store ) {
172
+ $post = get_post( $product->get_id() );
173
+
174
+ if ( ! $this->enabled
175
+ || in_array( $product->get_id(), $this->products_synced )
176
+ || ( defined( 'WP_LOAD_IMPORTERS' ) && WP_LOAD_IMPORTERS )
177
+ || wp_is_post_revision( $post )
178
+ || wp_is_post_autosave( $post )
179
+ || ( 'publish' !== get_post_status( $post ) )
180
+ || 'product' !== $post->post_type
181
+ ) {
182
+ return;
183
+ }
184
+
185
+ // Because of metaboxes, core may fire save on the product a couple of times.
186
+ // TODO: This workaround should be resolved once we re-architecture Square to
187
+ // use Action Scheduler (or something similar, a queue) to manage jobs.
188
+ $this->products_synced[] = $product->get_id();
189
+
190
+ $args = array(
191
+ $product->get_id(),
192
+ uniqid(), // this is needed due to WP not scheduling new events with same name and args
193
+ );
194
+
195
+ wp_schedule_single_event( time() + 60, 'wc_square_save_post_event', $args );
196
+ }
197
+
198
+ /**
199
+ * Trigger the save post event.
200
+ *
201
+ * @see 'save_post'
202
+ * @since 1.0.0
203
+ * @version 1.0.20
204
+ * @param $post_id
205
+ * @param $post
206
+ */
207
+ public function pre_wc_30_on_save_post( $post_id, $post ) {
208
+ if ( ! $this->enabled
209
+ || ( defined( 'DOING_AJAX' ) && DOING_AJAX ) // TODO: Look into removing this check.
210
+ || ( defined( 'WP_LOAD_IMPORTERS' ) && WP_LOAD_IMPORTERS )
211
+ || wp_is_post_revision( $post )
212
+ || wp_is_post_autosave( $post )
213
+ || ( 'publish' !== get_post_status( $post ) )
214
+ || 'product' !== $post->post_type
215
+ ) {
216
+ return;
217
+ }
218
+
219
+ $args = array(
220
+ $post_id,
221
+ uniqid(), // this is needed due to WP not scheduling new events with same name and args
222
+ );
223
+
224
+ wp_schedule_single_event( time() + 60, 'wc_square_save_post_event', $args );
225
+ }
226
+
227
+ /**
228
+ * Sync categories to Square when a category is created or altered.
229
+ */
230
+ public function on_category_modified() {
231
+
232
+ if ( $this->enabled ) {
233
+
234
+ $this->square->sync_categories();
235
+
236
+ }
237
+
238
+ }
239
+
240
+ /**
241
+ * Schedule cron job when product stock is changed.
242
+ *
243
+ * @since 1.0.16
244
+ * @version 1.0.16
245
+ */
246
+ public function schedule_on_product_set_stock( WC_Product $wc_product ) {
247
+ if ( ! is_object( $wc_product ) || empty( $wc_product ) ) {
248
+ return;
249
+ }
250
+
251
+ $args = array(
252
+ $wc_product,
253
+ uniqid(), // this is needed due to WP not scheduling new events with same name and args
254
+ );
255
+
256
+ $polling = get_transient( 'wc_square_polling' );
257
+
258
+ if ( $this->enabled && ! $polling ) {
259
+ wp_schedule_single_event( time() + 60, 'wc_square_on_product_set_stock_event', $args );
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Schedule cron job when product variation stock is changed.
265
+ *
266
+ * @since 1.0.16
267
+ * @version 1.0.16
268
+ */
269
+ public function schedule_on_variation_set_stock( WC_Product_Variation $wc_variation ) {
270
+ if ( ! is_object( $wc_variation ) || empty( $wc_variation ) ) {
271
+ return;
272
+ }
273
+
274
+ $args = array(
275
+ $wc_variation,
276
+ uniqid(), // this is needed due to WP not scheduling new events with same name and args
277
+ );
278
+
279
+ $polling = get_transient( 'wc_square_polling' );
280
+
281
+ if ( $this->enabled && ! $polling ) {
282
+ wp_schedule_single_event( time() + 60, 'wc_square_on_variation_set_stock_event', $args );
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Sync inventory to Square when a product's stock is altered.
288
+ *
289
+ * @param array $wc_product
290
+ */
291
+ public function on_product_set_stock( $wc_product ) {
292
+ $this->square->sync_inventory( $wc_product );
293
+ }
294
+
295
+ /**
296
+ * Sync inventory to Square when a variation's stock is altered.
297
+ *
298
+ * @param array $wc_variation
299
+ */
300
+ public function on_variation_set_stock( $wc_variation ) {
301
+ $product = version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_variation->parent : wc_get_product( $wc_variation->get_parent_id() );
302
+ $this->square->sync_inventory( $product );
303
+ }
304
+ }
trunk/includes/class-wc-square-sync-to-square.php ADDED
@@ -0,0 +1,479 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) {
3
+ exit; // Exit if accessed directly
4
+ }
5
+
6
+ /**
7
+ * Class WC_Square_Sync_To_Square
8
+ *
9
+ * Methods to Sync from WC to Square. Organized as "sync" methods that
10
+ * determine if "create" or "update" actions should be taken on the entities
11
+ * involved.
12
+ */
13
+ class WC_Square_Sync_To_Square {
14
+
15
+ /**
16
+ * @var WC_Square_Connect
17
+ */
18
+ protected $connect;
19
+
20
+ /**
21
+ * WC_Square_Sync_To_Square constructor.
22
+ */
23
+ public function __construct( WC_Square_Connect $connect ) {
24
+ add_filter( 'woocommerce_duplicate_product_exclude_meta', array( $this, 'duplicate_product_remove_meta' ) );
25
+
26
+ $this->connect = $connect;
27
+
28
+ }
29
+
30
+ /**
31
+ * Removes certain product meta when product is duplicated in WC to
32
+ * prevent overwriting the original item on Square.
33
+ *
34
+ * @access public
35
+ * @since 1.0.4
36
+ * @version 1.0.4
37
+ * @return array $metas;
38
+ */
39
+ public function duplicate_product_remove_meta( $metas ) {
40
+ $metas[] = '_square_item_id';
41
+ $metas[] = '_square_item_variation_id';
42
+
43
+ return $metas;
44
+ }
45
+
46
+ /**
47
+ * Sync WooCommerce categories to Square.
48
+ *
49
+ * Looks for category names that don't exist in Square, and creates them.
50
+ */
51
+ public function sync_categories() {
52
+
53
+ $wc_category_objects = $this->connect->wc->get_product_categories();
54
+ $wc_categories = array();
55
+
56
+ if ( is_wp_error( $wc_category_objects ) || empty( $wc_category_objects['product_categories'] ) ) {
57
+ return;
58
+ }
59
+
60
+ foreach ( $wc_category_objects['product_categories'] as $wc_category ) {
61
+
62
+ if ( empty( $wc_category['name'] ) || empty( $wc_category['id'] ) || ( $wc_category['parent'] !== 0 ) ) {
63
+ continue;
64
+ }
65
+
66
+ $wc_categories[ $wc_category['name'] ] = $wc_category['id'];
67
+
68
+ }
69
+
70
+ $square_category_objects = $this->connect->get_square_categories();
71
+ $square_categories = array();
72
+ $processed_categories = array();
73
+
74
+ foreach ( $square_category_objects as $square_category ) {
75
+ // Square list endpoints may return dups so we need to check for that
76
+ if ( in_array( $square_category->id, $processed_categories ) ) {
77
+ continue;
78
+ }
79
+
80
+ if ( is_object( $square_category ) && ! empty( $square_category->name ) && ! empty( $square_category->id ) ) {
81
+ $square_categories[ $square_category->name ] = $square_category->id;
82
+ $processed_categories[] = $square_category->id;
83
+ }
84
+ }
85
+
86
+ foreach ( $wc_categories as $wc_cat_name => $wc_cat_id ) {
87
+
88
+ $square_cat_id = WC_Square_Utils::get_wc_term_square_id( $wc_cat_id );
89
+
90
+ if ( $square_cat_id && ( $square_cat_name = array_search( $square_cat_id, $square_categories ) ) ) {
91
+
92
+ // Update a known Square Category whose name has changed in WC.
93
+ if ( $wc_cat_name !== $square_cat_name ) {
94
+
95
+ $this->connect->update_square_category( $square_cat_id, $wc_cat_name );
96
+
97
+ }
98
+
99
+ } elseif ( isset( $square_categories[ $wc_cat_name ] ) ) {
100
+
101
+ // Store the Square Category ID on a WC term that matches.
102
+ $square_category_id = $square_categories[ $wc_cat_name ];
103
+
104
+ WC_Square_Utils::update_wc_term_square_id( $wc_cat_id, $square_category_id );
105
+
106
+ } else {
107
+
108
+ // Create a new Square Category for a WC term that doesn't yet exist.
109
+ $response = $this->connect->create_square_category( $wc_cat_name );
110
+
111
+ if ( ! empty( $response->id ) ) {
112
+
113
+ $square_category_id = $response->id;
114
+
115
+ WC_Square_Utils::update_wc_term_square_id( $wc_cat_id, $square_category_id );
116
+
117
+ }
118
+
119
+ }
120
+
121
+ }
122
+
123
+ }
124
+
125
+ /**
126
+ * Find the Square Item that corresponds to the given WC Product.
127
+ *
128
+ * First searches for a Square Item ID in the WC Product metadata,
129
+ * then compares all WC Product SKUs against all Square Items.
130
+ *
131
+ * @param WC_Product $wc_product
132
+ * @return object|bool Square Item object on success, boolean false if no Square Item found.
133
+ */
134
+ public function get_square_item_for_wc_product( WC_Product $wc_product ) {
135
+
136
+ $product_id = version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->id : $wc_product->get_id();
137
+
138
+ if ( 'variation' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->product_type : $wc_product->get_type() ) ) {
139
+
140
+ $product_id = version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->variation_id : $wc_product->get_id();
141
+
142
+ }
143
+
144
+ if ( $square_item_id = WC_Square_Utils::get_wc_product_square_id( $product_id ) ) {
145
+
146
+ return $this->connect->get_square_product( $square_item_id );
147
+
148
+ }
149
+
150
+ $wc_product_skus = WC_Square_Utils::get_wc_product_skus( $wc_product );
151
+
152
+ return $this->connect->square_product_exists( $wc_product_skus );
153
+
154
+ }
155
+
156
+ /**
157
+ * Sync a WC Product to Square, optionally including Categories and Inventory.
158
+ *
159
+ * @param WC_Product $wc_product
160
+ * @param bool $include_category
161
+ * @param bool $include_inventory
162
+ * @param bool $include_image
163
+ */
164
+ public function sync_product( WC_Product $wc_product, $include_category = false, $include_inventory = false, $include_image = false ) {
165
+ $create = false;
166
+
167
+ // Only sync products with a SKU
168
+ $wc_product_skus = WC_Square_Utils::get_wc_product_skus( $wc_product );
169
+
170
+ if ( empty( $wc_product_skus ) ) {
171
+
172
+ WC_Square_Sync_Logger::log( sprintf( '[WC -> Square] Skipping WC Product %d since it has no SKUs.', version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->id : $wc_product->get_id() ) );
173
+ return;
174
+
175
+ }
176
+
177
+ if ( ! Woocommerce_Square::instance()->is_allowed_countries()
178
+ || ! Woocommerce_Square::instance()->is_allowed_currencies() ) {
179
+ WC_Square_Sync_Logger::log( '[WC -> Square] Error syncing inventory for WC Product - Country or Currency mismatch' );
180
+ return;
181
+ }
182
+
183
+ // Look for a Square Item with a matching SKU
184
+ $square_item = $this->get_square_item_for_wc_product( $wc_product );
185
+
186
+ // SKU found, update Item
187
+ if ( WC_Square_Utils::is_square_item_found( $square_item ) ) {
188
+
189
+ $result = $this->update_product( $wc_product, $square_item, $include_category, $include_inventory );
190
+
191
+ // No matching SKU found, create new Item
192
+ } else {
193
+ $create = true;
194
+ $result = $this->create_product( $wc_product, $include_category, $include_inventory );
195
+ }
196
+
197
+ // Sync inventory if create/update was successful
198
+ // TODO: consider whether or not this should be part of sync_product()..
199
+ if ( $result ) {
200
+ if ( $include_inventory ) {
201
+ $this->sync_inventory( $wc_product, $create );
202
+
203
+ }
204
+
205
+ if ( $include_image ) {
206
+
207
+ $this->sync_product_image( $wc_product, $result );
208
+
209
+ }
210
+
211
+ } else {
212
+
213
+ WC_Square_Sync_Logger::log( sprintf( '[WC -> Square] Error syncing WC Product %d.', version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->id : $wc_product->get_id() ) );
214
+
215
+ }
216
+
217
+ }
218
+
219
+ /**
220
+ * Sync a WC Product's inventory to Square
221
+ *
222
+ * @since 1.0.0
223
+ * @version 1.0.14
224
+ * @param WC_Product $wc_product
225
+ * @param bool $create
226
+ */
227
+ public function sync_inventory( WC_Product $wc_product, $create = false ) {
228
+ if ( ! Woocommerce_Square::instance()->is_allowed_countries()
229
+ || ! Woocommerce_Square::instance()->is_allowed_currencies() ) {
230
+ WC_Square_Sync_Logger::log( '[WC -> Square] Error syncing inventory for WC Product - Country or Currency mismatch' );
231
+ return;
232
+ }
233
+
234
+ // refresh cache first to get the latest inventory
235
+ $this->connect->refresh_inventory_cache();
236
+
237
+ // If not creating new product we need to first retrieve inventory.
238
+ if ( ! $create ) {
239
+ $square_inventory = $this->connect->get_square_inventory();
240
+ }
241
+
242
+ $wc_variation_ids = WC_Square_Utils::get_stock_managed_wc_variation_ids( $wc_product );
243
+
244
+ foreach ( $wc_variation_ids as $wc_variation_id ) {
245
+
246
+ $square_variation_id = WC_Square_Utils::get_wc_variation_square_id( $wc_variation_id );
247
+
248
+ if ( $square_variation_id || ( ! $create && isset( $square_inventory[ $square_variation_id ] ) ) ) {
249
+
250
+ $wc_stock = (int) get_post_meta( $wc_variation_id, '_stock', true );
251
+
252
+ $square_stock = 0;
253
+
254
+ if ( ! $create && isset( $square_inventory[ $square_variation_id ] ) ) {
255
+ $square_stock = (int) $square_inventory[ $square_variation_id ];
256
+ }
257
+
258
+ $delta = $wc_stock - $square_stock;
259
+
260
+ // Do not trigger inventory update if there is no stock changes.
261
+ if ( $delta == 0 ) {
262
+ WC_Square_Sync_Logger::log( sprintf( '[WC -> Square] Syncing WC product inventory for WC Product %d - No change in stock quantity for product/variation, skipping.', $wc_variation_id ) );
263
+ continue;
264
+ }
265
+
266
+ // Assume delta is gt 0, i.e. we received stock
267
+ $type = 'RECEIVE_STOCK';
268
+
269
+ if ( $delta < 0 ) {
270
+ // Delta is lt 0, so it is treated as sales
271
+ $type = 'SALE';
272
+ }
273
+
274
+ $result = $this->connect->update_square_inventory(
275
+ $square_variation_id,
276
+ $delta,
277
+ apply_filters( 'woocommerce_square_inventory_type', $type, $delta )
278
+ );
279
+
280
+ if ( ! $result ) {
281
+
282
+ WC_Square_Sync_Logger::log( sprintf( '[WC -> Square] Error syncing inventory for WC Product %d.', version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->id : $wc_product->get_id() ) );
283
+
284
+ }
285
+
286
+ }
287
+
288
+ }
289
+
290
+ }
291
+
292
+ /**
293
+ * Create a Square Item for a WC Product
294
+ *
295
+ * @param WC_Product $wc_product
296
+ * @param bool $include_category
297
+ * @param bool $include_inventory
298
+ *
299
+ * @return object|bool Created Square Item object on success, boolean False on failure.
300
+ */
301
+ public function create_product( WC_Product $wc_product, $include_category = false, $include_inventory = false ) {
302
+
303
+ $square_item = $this->connect->create_square_product( $wc_product, $include_category, $include_inventory );
304
+
305
+ if ( $square_item ) {
306
+
307
+ WC_Square_Utils::set_square_ids_on_wc_product_by_sku( $wc_product, $square_item );
308
+
309
+ return $square_item;
310
+
311
+ }
312
+
313
+ WC_Square_Sync_Logger::log( sprintf( '[WC -> Square] Error creating Square Item for WC Product %d.', version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->id : $wc_product->get_id() ) );
314
+
315
+ return false;
316
+
317
+ }
318
+
319
+ /**
320
+ * Update a Square Item for a WC Product
321
+ *
322
+ * @param WC_Product $wc_product
323
+ * @param object $square_item
324
+ * @param bool $include_category
325
+ * @param bool $include_inventory
326
+ *
327
+ * @return object|bool Updated Square Item object on success, boolean False on failure.
328
+ */
329
+ public function update_product( WC_Product $wc_product, $square_item, $include_category = false, $include_inventory = false ) {
330
+ if ( empty( $square_item ) || empty( $square_item->id ) ) {
331
+ WC_Square_Sync_Logger::log( sprintf( '[WC -> Square] Error updating Square Item ID N/A (WC Product %d).', version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->id : $wc_product->get_id() ) );
332
+ return false;
333
+ }
334
+
335
+ if ( empty( $wc_product ) ) {
336
+ WC_Square_Sync_Logger::log( sprintf( '[WC -> Square] Error updating Square Item ID %s (WC Product N/A).', $square_item->id ) );
337
+ return false;
338
+ }
339
+
340
+ $square_item = $this->connect->update_square_product( $wc_product, $square_item->id, $include_category, $include_inventory );
341
+
342
+ if ( ! $square_item ) {
343
+
344
+ WC_Square_Sync_Logger::log( sprintf( '[WC -> Square] Error updating Square Item ID %s (WC Product %d).', $square_item->id, version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->id : $wc_product->get_id() ) );
345
+ return false;
346
+
347
+ }
348
+
349
+ WC_Square_Utils::update_wc_product_square_id( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->id : $wc_product->get_id(), $square_item->id );
350
+
351
+ if ( 'simple' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->product_type : $wc_product->get_type() ) ) {
352
+
353
+ $wc_variations = array( $wc_product );
354
+
355
+ } elseif ( 'variable' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->product_type : $wc_product->get_type() ) ) {
356
+
357
+ $wc_variations = WC_Square_Utils::get_wc_product_variations( $wc_product );
358
+
359
+ }
360
+
361
+ foreach ( $wc_variations as $wc_variation ) {
362
+ $product_type = version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_variation->product_type : $wc_variation->get_type();
363
+ $variation_data = WC_Square_Utils::format_wc_variation_for_square_api( $wc_variation, $include_inventory );
364
+
365
+ if ( 'variation' === $product_type ) {
366
+ $wc_variation_id = version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_variation->variation_id : $wc_variation->get_id();
367
+ } else {
368
+ $wc_variation_id = version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_variation->id : $wc_variation->get_id();
369
+ }
370
+
371
+ if ( $square_variation_id = WC_Square_Utils::get_wc_variation_square_id( $wc_variation_id ) ) {
372
+
373
+ $result = $this->connect->update_square_variation( $square_item->id, $square_variation_id, $variation_data );
374
+
375
+ } else {
376
+
377
+ $result = $this->connect->create_square_variation( $square_item->id, $variation_data );
378
+
379
+ if ( $result && isset( $result->id ) ) {
380
+
381
+ WC_Square_Utils::update_wc_variation_square_id( $wc_variation_id, $result->id );
382
+
383
+ }
384
+
385
+ }
386
+
387
+ if ( ! $result ) {
388
+
389
+ if ( $square_variation_id ) {
390
+
391
+ WC_Square_Sync_Logger::log( sprintf( '[WC -> Square] Error updating Square ItemVariation %s for WC Variation %d.', $square_variation_id, $wc_variation_id ) );
392
+
393
+ } else {
394
+
395
+ WC_Square_Sync_Logger::log( sprintf( '[WC -> Square] Error creating Square ItemVariation for WC Variation %d.', $wc_variation_id ) );
396
+
397
+ }
398
+
399
+ }
400
+
401
+ }
402
+
403
+ return $square_item;
404
+
405
+ }
406
+
407
+ /**
408
+ * Sync a WC Product's Image to Square
409
+ *
410
+ * @param WC_Product $wc_product WC Product to sync Item Image for.
411
+ * @param object $square_item Optional. Corresponding Square Item object for $wc_product.
412
+ *
413
+ * @return bool Success.
414
+ */
415
+ public function sync_product_image( WC_Product $wc_product, $square_item = null ) {
416
+
417
+ if ( is_null( $square_item ) ) {
418
+
419
+ $square_item = $this->get_square_item_for_wc_product( $wc_product );
420
+
421
+ }
422
+
423
+ if ( ! $square_item ) {
424
+
425
+ WC_Square_Sync_Logger::log( sprintf( '[WC -> Square] Image Sync: No Square Item found for WC Product %d.', version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->id : $wc_product->get_id() ) );
426
+ return false;
427
+
428
+ }
429
+
430
+ if ( ! has_post_thumbnail( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->id : $wc_product->get_id() ) ) {
431
+
432
+ WC_Square_Sync_Logger::log( sprintf( '[WC -> Square] Image Sync: Skipping WC Product %d (no post thumbnail set).', version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->id : $wc_product->get_id() ) );
433
+ return true;
434
+
435
+ }
436
+
437
+ return $this->update_product_image( $wc_product, $square_item->id );
438
+
439
+ }
440
+
441
+ /**
442
+ * Update a Square Item Image for a WC Product
443
+ *
444
+ * @param WC_Product $wc_product
445
+ * @param string $square_item_id
446
+ * @return bool Success.
447
+ */
448
+ public function update_product_image( WC_Product $wc_product, $square_item_id ) {
449
+
450
+ $image_id = get_post_thumbnail_id( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->id : $wc_product->get_id() );
451
+
452
+ if ( empty( $image_id ) ) {
453
+
454
+ WC_Square_Sync_Logger::log( sprintf( '[WC -> Square] Update Product Image: No thumbnail ID for WC Product %d.', version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->id : $wc_product->get_id() ) );
455
+ return true;
456
+
457
+ }
458
+
459
+ $mime_type = get_post_field( 'post_mime_type', $image_id, 'raw' );
460
+ $image_path = get_attached_file( $image_id );
461
+
462
+ $result = $this->connect->update_square_product_image( $square_item_id, $mime_type, $image_path );
463
+
464
+ if ( $result && isset( $result->id ) ) {
465
+
466
+ WC_Square_Utils::update_wc_product_image_square_id( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->id : $wc_product->get_id(), $result->id );
467
+
468
+ return true;
469
+
470
+ } else {
471
+
472
+ WC_Square_Sync_Logger::log( sprintf( '[WC -> Square] Error updating Product Image for WC Product %d.', version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->id : $wc_product->get_id() ) );
473
+ return false;
474
+
475
+ }
476
+
477
+ }
478
+
479
+ }
trunk/includes/class-wc-square-utils.php ADDED
@@ -0,0 +1,974 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) {
3
+ exit; // Exit if accessed directly
4
+ }
5
+
6
+ /**
7
+ * Class WC_Square_Utils
8
+ *
9
+ * Static helper methods for the WC <-> Square integration, used in multiple
10
+ * places throughout the extension, with no dependencies of their own.
11
+ *
12
+ * Mostly data formatting and entity retrieval methods.
13
+ */
14
+ class WC_Square_Utils {
15
+
16
+ const WC_TERM_SQUARE_ID = 'square_cat_id';
17
+ const WC_PRODUCT_SQUARE_ID = '_square_item_id';
18
+ const WC_VARIATION_SQUARE_ID = '_square_item_variation_id';
19
+ const WC_PRODUCT_IMAGE_SQUARE_ID = '_square_item_image_id';
20
+
21
+ /**
22
+ * Convert a WC Product or Variation into a Square ItemVariation
23
+ * See: https://docs.connect.squareup.com/api/connect/v1/#datatype-itemvariation
24
+ *
25
+ * @param WC_Product|WC_Product_Variation $variation
26
+ * @param bool $include_inventory
27
+ * @return array Formatted as a Square ItemVariation
28
+ */
29
+ public static function format_wc_variation_for_square_api( $variation, $include_inventory = false ) {
30
+
31
+ $formatted = array(
32
+ 'name' => null,
33
+ 'pricing_type' => null,
34
+ 'price_money' => null,
35
+ 'sku' => null,
36
+ 'track_inventory' => null,
37
+ 'inventory_alert_type' => null,
38
+ 'inventory_alert_threshold' => null,
39
+ 'user_data' => null,
40
+ );
41
+
42
+ if ( $variation instanceof WC_Product ) {
43
+ if ( version_compare( WC_VERSION, '3.0.0', '<' ) ) {
44
+ $price = $variation->get_display_price();
45
+ } else {
46
+ // Will send the active price as is.
47
+ $price = $variation->get_price();
48
+ }
49
+
50
+ $formatted['name'] = __( 'Regular', 'woocommerce-square' );
51
+ $formatted['price_money'] = array(
52
+ 'currency_code' => apply_filters( 'woocommerce_square_currency', get_woocommerce_currency() ),
53
+ 'amount' => (int) WC_Square_Utils::format_amount_to_square( apply_filters( 'wc_square_sync_to_square_price', $price, $variation ) ),
54
+ );
55
+ $formatted['sku'] = $variation->get_sku();
56
+
57
+ if ( $include_inventory && $variation->managing_stock() ) {
58
+ $formatted['track_inventory'] = true;
59
+ }
60
+ }
61
+
62
+ if ( $variation instanceof WC_Product_Variation ) {
63
+
64
+ $formatted['name'] = implode( ', ', $variation->get_variation_attributes() );
65
+
66
+ }
67
+
68
+ return array_filter( $formatted );
69
+
70
+ }
71
+
72
+ /**
73
+ * Normalize description text to Square
74
+ * Square expects no HTML to be in the description and
75
+ * the limit of characters is 4095.
76
+ *
77
+ * @since 1.0.27
78
+ * @param mixed $description
79
+ * @return string
80
+ */
81
+ public static function normalize_description( $description = '' ) {
82
+ return substr( wp_strip_all_tags( $description ), 0, 4095 );
83
+ }
84
+
85
+ /**
86
+ * Convert a WC Product into a Square Item for Update
87
+ *
88
+ * Updates (PUT) don't accept the same parameters (namely variations) as Creation
89
+ * See: https://docs.connect.squareup.com/api/connect/v1/index.html#put-itemid
90
+ *
91
+ * @param WC_Product $wc_product
92
+ * @param bool $include_category
93
+ * @return array
94
+ */
95
+ public static function format_wc_product_update_for_square_api( WC_Product $wc_product, $include_category = false ) {
96
+
97
+ $formatted = array(
98
+ 'name' => $wc_product->get_title(),
99
+ 'description' => self::normalize_description( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->post->post_content : $wc_product->get_description() ),
100
+ 'visibility' => 'PUBLIC',
101
+ );
102
+
103
+ if ( $include_category ) {
104
+ $square_cat_id = self::get_square_category_id_for_wc_product( $wc_product );
105
+
106
+ if ( $square_cat_id ) {
107
+ $formatted['category_id'] = $square_cat_id;
108
+ }
109
+ }
110
+
111
+ return array_filter( $formatted );
112
+ }
113
+
114
+ /**
115
+ * Convert a WC Product into a Square Item for Create
116
+ *
117
+ * Creation (POST) allows more parameters than Updating, namely variations
118
+ * See: https://docs.connect.squareup.com/api/connect/v1/index.html#post-items
119
+ *
120
+ * @param WC_Product $wc_product
121
+ * @param bool $include_category
122
+ * @param bool $include_inventory
123
+ * @return array
124
+ */
125
+ public static function format_wc_product_create_for_square_api( WC_Product $wc_product, $include_category = false, $include_inventory = false ) {
126
+
127
+ $formatted = self::format_wc_product_update_for_square_api( $wc_product, $include_category );
128
+
129
+ // check product type
130
+ if ( 'simple' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->product_type : $wc_product->get_type() ) ) {
131
+
132
+ $formatted['variations'] = array(
133
+ WC_Square_Utils::format_wc_variation_for_square_api( $wc_product, $include_inventory ),
134
+ );
135
+
136
+ } elseif ( 'variable' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->product_type : $wc_product->get_type() ) ) {
137
+
138
+ $wc_variations = self::get_wc_product_variations( $wc_product );
139
+
140
+ foreach ( (array) $wc_variations as $wc_variation ) {
141
+
142
+ $formatted['variations'][] = WC_Square_Utils::format_wc_variation_for_square_api( $wc_variation, $include_inventory );
143
+ }
144
+ }
145
+
146
+ return array_filter( $formatted );
147
+ }
148
+
149
+ /**
150
+ * Map existing WC Variation IDs to a formatted product update array.
151
+ *
152
+ * Square ItemVariations are matched to their WC Variation equivalents via SKU.
153
+ *
154
+ * @param WC_Product $wc_product
155
+ * @param array $product_update Formatted product update. @see WC_Square_Utils::format_square_item_for_wc_api
156
+ *
157
+ * @return array WC Product formatted for update, with Variation IDs mapped.
158
+ */
159
+ public static function set_wc_variation_ids_for_update( WC_Product $wc_product, $product_update ) {
160
+
161
+ if ( ( 'variable' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->product_type : $wc_product->get_type() ) ) && isset( $product_update['variations'] ) ) {
162
+
163
+ $wc_variations = self::get_wc_product_variations( $wc_product );
164
+
165
+ $wc_variation_sku_id_map = array();
166
+
167
+ foreach ( $wc_variations as $wc_variation ) {
168
+ $wc_variation_sku = $wc_variation->get_sku();
169
+
170
+ $wc_variation_id = version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_variation->variation_id : $wc_variation->get_id();
171
+
172
+ if ( ! empty( $wc_variation_sku ) && ! empty( $wc_variation_id ) ) {
173
+
174
+ $wc_variation_sku_id_map[ $wc_variation_sku ] = $wc_variation_id;
175
+ }
176
+ }
177
+
178
+ foreach ( (array) $product_update['variations'] as $idx => $variation ) {
179
+ if ( ! empty( $variation['sku'] ) && isset( $wc_variation_sku_id_map[ $variation['sku'] ] ) ) {
180
+ $product_update['variations'][ $idx ]['id'] = $wc_variation_sku_id_map[ $variation['sku'] ];
181
+ }
182
+ }
183
+ }
184
+
185
+ return $product_update;
186
+ }
187
+
188
+ /**
189
+ * Format a Square Item for an UPDATE through the WC Product API
190
+ *
191
+ * See: https://docs.connect.squareup.com/api/connect/v1/#datatype-item
192
+ * See: https://woothemes.github.io/woocommerce-rest-api-docs/#products-properties
193
+ *
194
+ * @param object $square_item
195
+ * @param WC_Product $wc_product
196
+ * @param bool $include_category
197
+ * @param bool $include_inventory
198
+ * @param bool $include_image
199
+ *
200
+ * @return array
201
+ */
202
+ public static function format_square_item_for_wc_api_update( $square_item, WC_Product $wc_product, $include_category = false, $include_inventory = false, $include_image = false ) {
203
+
204
+ $formatted = self::format_square_item_for_wc_api_create( $square_item, $include_category, $include_inventory, $include_image );
205
+
206
+ return self::set_wc_variation_ids_for_update( $wc_product, $formatted );
207
+ }
208
+
209
+ /**
210
+ * Format a Square Item for a CREATE through the WC Product API
211
+ *
212
+ * See: https://docs.connect.squareup.com/api/connect/v1/#datatype-item
213
+ * See: https://woothemes.github.io/woocommerce-rest-api-docs/#products-properties
214
+ *
215
+ * @param object $square_item
216
+ * @param bool $include_inventory
217
+ * @param bool $include_image
218
+ *
219
+ * @return array
220
+ */
221
+ public static function format_square_item_for_wc_api_create( $square_item, $include_category = false, $include_inventory = false, $include_image = false ) {
222
+
223
+ $formatted = array(
224
+ 'title' => $square_item->name,
225
+ );
226
+
227
+ if ( apply_filters( 'woocommerce_square_sync_from_square_description', false ) ) {
228
+ $description = ! empty( $square_item->description ) ? $square_item->description : '';
229
+ $formatted['description'] = $description;
230
+ }
231
+
232
+ if ( $include_image && isset( $square_item->master_image->url ) ) {
233
+
234
+ $formatted['images'] = array(
235
+ array(
236
+ 'position' => 0,
237
+ 'src' => $square_item->master_image->url,
238
+ ),
239
+ );
240
+ }
241
+
242
+ if ( $include_category && isset( $square_item->category->id ) ) {
243
+ $wc_cat_id = self::get_wc_category_id_for_square_category_id( $square_item->category->id );
244
+
245
+ if ( $wc_cat_id ) {
246
+ $formatted['categories'] = array( $wc_cat_id );
247
+ }
248
+ }
249
+
250
+ if ( count( $square_item->variations ) > 1 ) {
251
+
252
+ $formatted['type'] = 'variable';
253
+ $formatted['variations'] = array();
254
+
255
+ foreach ( $square_item->variations as $square_item_variation ) {
256
+
257
+ $formatted['variations'][] = self::format_square_item_variation_for_wc_api( $square_item_variation, $include_inventory );
258
+
259
+ }
260
+
261
+ $formatted['attributes'] = array(
262
+ array(
263
+ 'name' => 'Attribute',
264
+ 'slug' => 'attribute',
265
+ 'position' => 0,
266
+ 'visible' => true,
267
+ 'variation' => true,
268
+ 'options' => wp_list_pluck( $square_item->variations, 'name' ),
269
+ ),
270
+ );
271
+
272
+ } else {
273
+
274
+ $variation = self::format_square_item_variation_for_wc_api( $square_item->variations[0], $include_inventory );
275
+
276
+ $formatted['type'] = 'simple';
277
+ $formatted['sku'] = isset( $variation['sku'] ) ? $variation['sku'] : null;
278
+ $formatted['regular_price'] = isset( $variation['regular_price'] ) ? $variation['regular_price'] : null;
279
+ $formatted['stock_quantity'] = isset( $variation['stock_quantity'] ) ? $variation['stock_quantity'] : null;
280
+ $formatted['managing_stock'] = isset( $variation['managing_stock'] ) ? $variation['managing_stock'] : null;
281
+
282
+ }
283
+
284
+ return array_filter( $formatted );
285
+ }
286
+
287
+ /**
288
+ * Convert a Square ItemVariation for the WC Product API
289
+ *
290
+ * See: https://docs.connect.squareup.com/api/connect/v1/#datatype-itemvariation
291
+ * See: https://woothemes.github.io/woocommerce-rest-api-docs/#products-properties
292
+ *
293
+ * @param object $square_item_variation
294
+ * @return array
295
+ */
296
+ public static function format_square_item_variation_for_wc_api( $square_item_variation, $include_inventory = false ) {
297
+
298
+ $formatted = array(
299
+ 'sku' => ! empty( $square_item_variation->sku ) ? $square_item_variation->sku : '',
300
+ 'regular_price' => self::format_square_price_for_wc( $square_item_variation->price_money->amount ),
301
+ 'stock_quantity' => null,
302
+ 'attributes' => array(
303
+ array(
304
+ 'name' => 'Attribute',
305
+ 'option' => ! empty( $square_item_variation->name ) ? $square_item_variation->name : '',
306
+ ),
307
+ ),
308
+ );
309
+
310
+ if ( $include_inventory ) {
311
+ $formatted['managing_stock'] = $square_item_variation->track_inventory ? true : null;
312
+ }
313
+
314
+ return array_filter( $formatted );
315
+ }
316
+
317
+ /**
318
+ * Formats the price coming from Square as they use the lowest denominator ex. cents
319
+ *
320
+ * See: https://docs.connect.squareup.com/api/connect/v1/#workingwithmonetaryamounts
321
+ *
322
+ * @param int $price
323
+ * @return int
324
+ */
325
+ public static function format_square_price_for_wc( $price = 0 ) {
326
+ return apply_filters( 'woocommerce_square_format_price', self::format_amount_from_square( $price ) );
327
+ }
328
+
329
+ /**
330
+ * Retrieve the Square ID for a WC Term
331
+ *
332
+ * @param int $wc_term_id
333
+ * @return mixed See get_woocommerce_term_meta()
334
+ */
335
+ public static function get_wc_term_square_id( $wc_term_id ) {
336
+ return WC_Square_Utils::wc_compat_get_term_meta( $wc_term_id, self::WC_TERM_SQUARE_ID );
337
+ }
338
+
339
+ /**
340
+ * Update the Square ID for a WC Term
341
+ *
342
+ * @param int $wc_term_id
343
+ * @param string $square_id
344
+ * @return bool See update_woocommerce_term_meta()
345
+ */
346
+ public static function update_wc_term_square_id( $wc_term_id, $square_id ) {
347
+ return WC_Square_Utils::wc_compat_update_term_meta( $wc_term_id, self::WC_TERM_SQUARE_ID, $square_id );
348
+ }
349
+
350
+ /**
351
+ * Retrieve the Square ID for a WC Product
352
+ *
353
+ * @param int $wc_product_id
354
+ * @return array|mixed See get_post_meta()
355
+ */
356
+ public static function get_wc_product_square_id( $wc_product_id ) {
357
+ return get_post_meta( $wc_product_id, self::WC_PRODUCT_SQUARE_ID, true );
358
+ }
359
+
360
+ /**
361
+ * Update the Square ID for a WC Product
362
+ *
363
+ * @param int $wc_product_id
364
+ * @param string $square_id
365
+ * @return bool|int See update_post_meta()
366
+ */
367
+ public static function update_wc_product_square_id( $wc_product_id, $square_id ) {
368
+ return update_post_meta( $wc_product_id, self::WC_PRODUCT_SQUARE_ID, $square_id );
369
+ }
370
+
371
+ /**
372
+ * Retrieve the Square ID for a WC Product Variation
373
+ *
374
+ * @param int $wc_variation_id
375
+ * @return array|mixed See get_post_meta()
376
+ */
377
+ public static function get_wc_variation_square_id( $wc_variation_id ) {
378
+ return get_post_meta( $wc_variation_id, self::WC_VARIATION_SQUARE_ID, true );
379
+ }
380
+
381
+ /**
382
+ * Update the Square ID for a WC Product Variation
383
+ *
384
+ * @param int $wc_variation_id
385
+ * @param string $square_id
386
+ * @return bool|int See update_post_meta()
387
+ */
388
+ public static function update_wc_variation_square_id( $wc_variation_id, $square_id ) {
389
+ return update_post_meta( $wc_variation_id, self::WC_VARIATION_SQUARE_ID, $square_id );
390
+ }
391
+
392
+ /**
393
+ * Get all SKUs associated with a WC Product (could be many, if variable).
394
+ *
395
+ * @param WC_Product $wc_product
396
+ * @return array
397
+ */
398
+ public static function get_wc_product_skus( WC_Product $wc_product ) {
399
+ $wc_product_skus = array();
400
+
401
+ if ( 'simple' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->product_type : $wc_product->get_type() ) ) {
402
+
403
+ $wc_product_skus[] = $wc_product->get_sku();
404
+
405
+ } elseif ( 'variable' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->product_type : $wc_product->get_type() ) ) {
406
+ $wc_variations = self::get_wc_product_variations( $wc_product );
407
+
408
+ if ( version_compare( WC_VERSION, '3.0.0', '<' ) ) {
409
+ $wc_product_skus = wp_list_pluck( $wc_variations, 'sku' );
410
+ } else {
411
+ foreach ( $wc_variations as $wc_variation ) {
412
+ $wc_product_skus[] = $wc_variation->get_sku();
413
+ }
414
+ }
415
+ }
416
+
417
+ // SKUs are optional, so let's only return ones that have values
418
+ return array_filter( $wc_product_skus );
419
+ }
420
+
421
+ /**
422
+ * Determine which WC Product Category to send to Square.
423
+ *
424
+ * Returns the first top-level Category that has an associated Square ID.
425
+ *
426
+ * @param WC_Product $wc_product
427
+ * @return bool|mixed
428
+ */
429
+ public static function get_square_category_id_for_wc_product( WC_Product $wc_product ) {
430
+ $wc_categories = wp_get_post_terms( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->id : $wc_product->get_id(), 'product_cat' );
431
+
432
+ if ( is_wp_error( $wc_categories ) && empty( $wc_categories ) ) {
433
+
434
+ return false;
435
+
436
+ }
437
+
438
+ foreach ( $wc_categories as $category ) {
439
+
440
+ if ( $category->parent ) {
441
+
442
+ $ancestors = get_ancestors( $category->term_id, 'product_cat', 'taxonomy' );
443
+ $top_level_id = end( $ancestors );
444
+
445
+ } else {
446
+
447
+ $top_level_id = $category->term_id;
448
+
449
+ }
450
+
451
+ $square_cat_id = self::get_wc_term_square_id( $top_level_id );
452
+
453
+ if ( $square_cat_id ) {
454
+ return $square_cat_id;
455
+ }
456
+ }
457
+
458
+ return false;
459
+ }
460
+
461
+ /**
462
+ * Retrieve the Square Item Image ID for a WC Product
463
+ *
464
+ * @param int $wc_product_id
465
+ * @return array|mixed See get_post_meta()
466
+ */
467
+ public static function get_wc_product_image_square_id( $wc_product_id ) {
468
+ return get_post_meta( $wc_product_id, self::WC_PRODUCT_IMAGE_SQUARE_ID, true );
469
+ }
470
+
471
+ /**
472
+ * Update the Square Item Image ID for a WC Product
473
+ *
474
+ * @param int $wc_product_id
475
+ * @param string $square_id
476
+ * @return bool|int See update_post_meta()
477
+ */
478
+ public static function update_wc_product_image_square_id( $wc_product_id, $square_image_id ) {
479
+ return update_post_meta( $wc_product_id, self::WC_PRODUCT_IMAGE_SQUARE_ID, $square_image_id );
480
+ }
481
+
482
+ /**
483
+ * Retrieve the WC Category ID that corresponds to a given Square Category ID.
484
+ *
485
+ * @param string $square_cat_id
486
+ * @return bool|int WC Category ID on successful match, boolean false otherwise.
487
+ */
488
+ public static function get_wc_category_id_for_square_category_id( $square_cat_id ) {
489
+ $categories = get_terms( 'product_cat', array(
490
+ 'parent' => 0,
491
+ 'hide_empty' => false,
492
+ 'fields' => 'ids',
493
+ ) );
494
+
495
+ if ( is_wp_error( $categories ) ) {
496
+ WC_Square_Sync_Logger::log( sprintf( '%s::%s - Taxonomy "product_cat" not found. Make sure WooCommerce is enabled.', __CLASS__, __FUNCTION__ ) );
497
+ return false;
498
+ }
499
+
500
+ foreach ( $categories as $wc_category ) {
501
+ $wc_square_cat_id = self::get_wc_term_square_id( $wc_category );
502
+
503
+ if ( $wc_square_cat_id && ( $square_cat_id === $wc_square_cat_id ) ) {
504
+ return $wc_category;
505
+ }
506
+ }
507
+
508
+ return false;
509
+ }
510
+
511
+ /**
512
+ * Attempt to find a WC Product that corresponds to a given Square Item.
513
+ *
514
+ * This function first queries for a WC Product already associated to the
515
+ * Square Item's ID. If none found, all WC Products (and variations) are
516
+ * queried using the SKUs present in the Square Item's Variations. If a
517
+ * match is found, the parent (non-variant) WC Product is returned.
518
+ *
519
+ * @param object $square_item
520
+ * @return bool|WC_Product Corresponding WC_Product on successful match, boolean false otherwise.
521
+ */
522
+ public static function get_wc_product_for_square_item( $square_item ) {
523
+ if ( ! is_object( $square_item ) || ! property_exists( $square_item, 'id' ) ) {
524
+ return false;
525
+ }
526
+
527
+ $wc_product_ids = get_posts( array(
528
+ 'post_type' => 'product',
529
+ 'post_status' => 'publish', // this is ignored
530
+ 'meta_query' => array(
531
+ array(
532
+ 'key' => self::WC_PRODUCT_SQUARE_ID,
533
+ 'compare' => '=',
534
+ 'value' => $square_item->id,
535
+ ),
536
+ ),
537
+ 'posts_per_page' => 1,
538
+ 'fields' => 'ids',
539
+ ) );
540
+
541
+ if ( ! empty( $wc_product_ids ) ) {
542
+ $wc_product = wc_get_product( $wc_product_ids[0] );
543
+
544
+ // only return publish products
545
+ if ( 'publish' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->post->post_status : $wc_product->get_status() ) ) {
546
+ return $wc_product;
547
+ }
548
+ }
549
+
550
+ $square_item_skus = self::get_square_item_skus( $square_item );
551
+
552
+ if ( empty( $square_item_skus ) ) {
553
+ return false;
554
+ }
555
+
556
+ $wc_product_ids = get_posts( array(
557
+ 'post_type' => array( 'product', 'product_variation' ),
558
+ 'post_status' => 'publish', // this is ignored
559
+ 'meta_query' => array(
560
+ array(
561
+ 'key' => '_sku',
562
+ 'compare' => 'IN',
563
+ 'value' => $square_item_skus,
564
+ ),
565
+ ),
566
+ 'posts_per_page' => 1,
567
+ 'fields' => 'ids',
568
+ ) );
569
+
570
+ if ( ! empty( $wc_product_ids ) ) {
571
+ $wc_product = wc_get_product( $wc_product_ids[0] );
572
+
573
+ if ( ! is_object( $wc_product ) ) {
574
+ return false;
575
+ }
576
+
577
+ if ( 'publish' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->post->post_status : $wc_product->get_status() ) ) {
578
+ if ( 'simple' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->product_type : $wc_product->get_type() ) ) {
579
+
580
+ return $wc_product;
581
+
582
+ }
583
+
584
+ return wc_get_product( ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->parent : $wc_product->get_parent_id() ) );
585
+ }
586
+ }
587
+
588
+ return false;
589
+ }
590
+
591
+ /**
592
+ * Attempt to find a WC Product that corresponds to a given Square Item.
593
+ *
594
+ * @param string $square_variation_id
595
+ * @return bool|WC_Product Corresponding WC_Product on successful match, boolean false otherwise.
596
+ */
597
+ public static function get_wc_product_for_square_item_variation_id( $square_variation_id ) {
598
+ $wc_product_ids = get_posts( array(
599
+ 'post_type' => array( 'product', 'product_variation' ),
600
+ 'post_status' => 'publish', // this is ignored
601
+ 'meta_query' => array(
602
+ array(
603
+ 'key' => self::WC_VARIATION_SQUARE_ID,
604
+ 'compare' => '=',
605
+ 'value' => $square_variation_id,
606
+ ),
607
+ ),
608
+ 'posts_per_page' => 1,
609
+ 'fields' => 'ids',
610
+ ) );
611
+
612
+ if ( ! empty( $wc_product_ids ) ) {
613
+ $product = wc_get_product( $wc_product_ids[0] );
614
+
615
+ // only return publish products
616
+ if ( 'publish' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->post->post_status : $product->get_status() ) ) {
617
+ return $product;
618
+ }
619
+ }
620
+
621
+ return false;
622
+ }
623
+
624
+ /**
625
+ * Checks if all SKUs have been set for the Square Item.
626
+ * We do not want to sync the item if not all SKU is set.
627
+ *
628
+ * @since 1.0.14
629
+ * @version 1.0.14
630
+ * @param object $square_item
631
+ * @return bool
632
+ */
633
+ public static function is_square_item_skus_set( $square_item ) {
634
+ if ( empty( $square_item ) || empty( $square_item->variations ) ) {
635
+
636
+ return false;
637
+ }
638
+
639
+ foreach ( $square_item->variations as $item_variation ) {
640
+ // If even one sku is missing we don't want to sync.
641
+ if ( empty( $item_variation->sku ) ) {
642
+
643
+ return false;
644
+ }
645
+ }
646
+
647
+ return true;
648
+ }
649
+
650
+ /**
651
+ * Return array of SKUs from all variations of a Square Item
652
+ *
653
+ * @param object $square_item
654
+ * @return array
655
+ */
656
+ public static function get_square_item_skus( $square_item ) {
657
+
658
+ $item_skus = array();
659
+
660
+ if ( empty( $square_item->variations ) ) {
661
+
662
+ return $item_skus;
663
+ }
664
+
665
+ foreach ( $square_item->variations as $item_variation ) {
666
+ if ( ! empty( $item_variation->sku ) ) {
667
+
668
+ $item_skus[] = $item_variation->sku;
669
+ }
670
+ }
671
+
672
+ return $item_skus;
673
+ }
674
+
675
+ /**
676
+ * Store Square Item ID and ItemVariation IDs on a WC Product and it's variations,
677
+ * matching using the SKU value.
678
+ *
679
+ * This is most useful in WC->Square creation, and Square->WC operations.
680
+ *
681
+ * @param WC_Product $wc_product
682
+ * @param object $square_item
683
+ */
684
+ public static function set_square_ids_on_wc_product_by_sku( WC_Product $wc_product, $square_item ) {
685
+ if ( ! is_object( $square_item ) || ! property_exists( $square_item, 'id' ) ) {
686
+ return false;
687
+ }
688
+
689
+ self::update_wc_product_square_id( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->id : $wc_product->get_id(), $square_item->id );
690
+
691
+ if ( 'simple' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->product_type : $wc_product->get_type() ) ) {
692
+
693
+ self::update_wc_variation_square_id( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->id : $wc_product->get_id(), $square_item->variations[0]->id );
694
+
695
+ } elseif ( 'variable' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->product_type : $wc_product->get_type() ) ) {
696
+
697
+ // Create mapping of Square ItemVariation SKU => ID
698
+ $square_variations = array();
699
+
700
+ foreach ( $square_item->variations as $square_variation ) {
701
+
702
+ if ( ! empty( $square_variation->sku ) ) {
703
+
704
+ $square_variations[ $square_variation->sku ] = $square_variation->id;
705
+
706
+ }
707
+ }
708
+
709
+ // Create mapping of WC Variation SKU => ID
710
+ $wc_item_variations = self::get_wc_product_variations( $wc_product );
711
+ $wc_variations = array();
712
+
713
+ foreach ( $wc_item_variations as $wc_item_variation ) {
714
+ $wc_variation_sku = $wc_item_variation->get_sku();
715
+
716
+ if ( ! empty( $wc_variation_sku ) ) {
717
+ $wc_variations[ $wc_variation_sku ] = version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_item_variation->variation_id : $wc_item_variation->get_id();
718
+ }
719
+ }
720
+
721
+ // Map the WC Variations to their Square ItemVariation counterparts
722
+ foreach ( $wc_variations as $sku => $wc_variation_id ) {
723
+ if ( array_key_exists( $sku, $square_variations ) ) {
724
+
725
+ self::update_wc_variation_square_id( $wc_variation_id, $square_variations[ $sku ] );
726
+ }
727
+ }
728
+ }
729
+ }
730
+
731
+ /**
732
+ * Retrieve WC Variation IDs for a given WC Product, that we're managing stock for.
733
+ *
734
+ * @param WC_Product $wc_product
735
+ * @return array
736
+ */
737
+ public static function get_stock_managed_wc_variation_ids( WC_Product $wc_product ) {
738
+ $wc_product = wc_get_product( $wc_product->get_id() );
739
+
740
+ $wc_variation_ids = array();
741
+
742
+ if ( ! is_object( $wc_product ) ) {
743
+ return $wc_variation_ids;
744
+ }
745
+
746
+ if ( 'simple' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->product_type : $wc_product->get_type() ) ) {
747
+
748
+ if ( $wc_product->managing_stock() ) {
749
+
750
+ $wc_variation_ids = array( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->id : $wc_product->get_id() );
751
+
752
+ }
753
+ } elseif ( 'variable' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->product_type : $wc_product->get_type() ) ) {
754
+
755
+ $variations = self::get_wc_product_variations( $wc_product );
756
+
757
+ foreach ( (array) $variations as $variation ) {
758
+
759
+ if ( $variation->managing_stock() ) {
760
+
761
+ $wc_variation_ids[] = version_compare( WC_VERSION, '3.0.0', '<' ) ? $variation->variation_id : $variation->get_id();
762
+
763
+ }
764
+ }
765
+ }
766
+
767
+ return $wc_variation_ids;
768
+ }
769
+
770
+ /**
771
+ * Get all variations of a given WC_Product_Variable.
772
+ *
773
+ * @param WC_Product_Variable $wc_variable_product
774
+ * @return array Array of WC_Product_Variation objects.
775
+ */
776
+ public static function get_wc_product_variations( WC_Product_Variable $wc_variable_product ) {
777
+ $variations = array();
778
+
779
+ foreach ( $wc_variable_product->get_children() as $child_id ) {
780
+
781
+ $variation = version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_variable_product->get_child( $child_id ) : wc_get_product( $child_id );
782
+
783
+ $variation_id = version_compare( WC_VERSION, '3.0.0', '<' ) ? $variation->variation_id : $variation->get_id();
784
+
785
+ if ( empty( $variation_id ) ) {
786
+ continue;
787
+ }
788
+
789
+ $variations[] = $variation;
790
+ }
791
+
792
+ return $variations;
793
+ }
794
+
795
+ /**
796
+ * Check if the square item is found
797
+ *
798
+ * @param object $square_item
799
+ * @return bool
800
+ */
801
+ public static function is_square_item_found( $square_item ) {
802
+ if ( is_object( $square_item ) && 'not_found' !== $square_item->type ) {
803
+ return true;
804
+ }
805
+
806
+ return false;
807
+ }
808
+
809
+ /**
810
+ * Checks to see if product disable sync is enabled
811
+ *
812
+ * @param int $product_id parent product id
813
+ * @return bool
814
+ */
815
+ public static function skip_product_sync( $product_id = null ) {
816
+ if ( null === $product_id ) {
817
+ return false;
818
+ }
819
+
820
+ $skip_sync = get_post_meta( $product_id, '_wcsquare_disable_sync', true );
821
+
822
+ if ( 'yes' === $skip_sync ) {
823
+ return true;
824
+ }
825
+
826
+ return false;
827
+ }
828
+
829
+ /**
830
+ * Process amount to be passed to Square.
831
+ * @return float
832
+ */
833
+ public static function format_amount_to_square( $total, $currency = '' ) {
834
+ if ( ! $currency ) {
835
+ $currency = get_woocommerce_currency();
836
+ }
837
+
838
+ switch ( strtoupper( $currency ) ) {
839
+ // Zero decimal currencies
840
+ case 'BIF':
841
+ case 'CLP':
842
+ case 'DJF':
843
+ case 'GNF':
844
+ case 'JPY':
845
+ case 'KMF':
846
+ case 'KRW':
847
+ case 'MGA':
848
+ case 'PYG':
849
+ case 'RWF':
850
+ case 'VND':
851
+ case 'VUV':
852
+ case 'XAF':
853
+ case 'XOF':
854
+ case 'XPF':
855
+ $total = absint( $total );
856
+ break;
857
+ default:
858
+ $total = absint( wc_format_decimal( ( (float) $total * 100 ), wc_get_price_decimals() ) ); // In cents.
859
+ break;
860
+ }
861
+
862
+ return $total;
863
+ }
864
+
865
+ /**
866
+ * Process amount to be passed from Square.
867
+ * @return float
868
+ */
869
+ public static function format_amount_from_square( $total, $currency = '' ) {
870
+ if ( ! $currency ) {
871
+ $currency = get_woocommerce_currency();
872
+ }
873
+
874
+ switch ( strtoupper( $currency ) ) {
875
+ // Zero decimal currencies
876
+ case 'BIF':
877
+ case 'CLP':
878
+ case 'DJF':
879
+ case 'GNF':
880
+ case 'JPY':
881
+ case 'KMF':
882
+ case 'KRW':
883
+ case 'MGA':
884
+ case 'PYG':
885
+ case 'RWF':
886
+ case 'VND':
887
+ case 'VUV':
888
+ case 'XAF':
889
+ case 'XOF':
890
+ case 'XPF':
891
+ $total = absint( $total );
892
+ break;
893
+ default:
894
+ $total = wc_format_decimal( absint( $total ) / 100 );
895
+ break;
896
+ }
897
+
898
+ return $total;
899
+ }
900
+
901
+ /**
902
+ * This is for developers to test with their own staging access with Square.
903
+ * This is usually not accessible by regular merchants.
904
+ *
905
+ * @param string $environment
906
+ * @return string
907
+ */
908
+ public static function is_staging( $environment = null ) {
909
+ if ( ! empty( $environment ) && 'staging' === $environment && defined( 'WP_DEBUG' ) && WP_DEBUG ) {
910
+ return true;
911
+ }
912
+
913
+ return false;
914
+ }
915
+
916
+ /**
917
+ * Deletes all transients.
918
+ *
919
+ * @since 1.0.17
920
+ * @version 1.0.17
921
+ */
922
+ public static function delete_transients() {
923
+ delete_transient( 'wc_square_processing_total_count' );
924
+
925
+ delete_transient( 'wc_square_processing_ids' );
926
+
927
+ delete_transient( 'wc_square_syncing_square_inventory' );
928
+
929
+ delete_transient( 'sq_wc_sync_current_process' );
930
+
931
+ delete_transient( 'wc_square_inventory' );
932
+
933
+ delete_transient( 'wc_square_polling' );
934
+
935
+ delete_transient( 'wc_square_manual_sync_processing' );
936
+
937
+ return true;
938
+ }
939
+
940
+ /**
941
+ *
942
+ * Updates a term meta. Compatibility function for WC 3.6.
943
+ *
944
+ * @since 1.0.36
945
+ * @param int $term_id Term ID.
946
+ * @param string $meta_key Meta key.
947
+ * @param mixed $meta_value Meta value.
948
+ * @return bool
949
+ */
950
+ public static function wc_compat_update_term_meta( $term_id, $meta_key, $meta_value ) {
951
+ if ( version_compare( WC_VERSION, '3.6', 'ge' ) ) {
952
+ return update_term_meta( $term_id, $meta_key, $meta_value );
953
+ } else {
954
+ return update_woocommerce_term_meta( $term_id, $meta_key, $meta_value );
955
+ }
956
+ }
957
+
958
+ /**
959
+ * Gets a term meta. Compatibility function for WC 3.6.
960
+ *
961
+ * @since 1.0.36
962
+ * @param int $term_id Term ID.
963
+ * @param string $key Meta key.
964
+ * @param bool $single Whether to return a single value. (default: true).
965
+ * @return mixed
966
+ */
967
+ public static function wc_compat_get_term_meta( $term_id, $key, $single = true ) {
968
+ if ( version_compare( WC_VERSION, '3.6', 'ge' ) ) {
969
+ return get_term_meta( $term_id, $key, $single );
970
+ } else {
971
+ return get_woocommerce_term_meta( $term_id, $key, $single );
972
+ }
973
+ }
974
+ }
trunk/includes/class-wc-square-wc-products.php ADDED
@@ -0,0 +1,2461 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) {
3
+ exit;
4
+ }
5
+
6
+ class WC_Square_API_Exception extends Exception {
7
+
8
+ /** @var string sanitized error code */
9
+ protected $error_code;
10
+
11
+ /**
12
+ * Setup exception, requires 3 params:
13
+ *
14
+ * error code - machine-readable, e.g. `woocommerce_invalid_product_id`
15
+ * error message - friendly message, e.g. 'Product ID is invalid'
16
+ * http status code - proper HTTP status code to respond with, e.g. 400
17
+ *
18
+ * @since 2.2
19
+ * @param string $error_code
20
+ * @param string $error_message user-friendly translated error message
21
+ * @param int $http_status_code HTTP status code to respond with
22
+ */
23
+ public function __construct( $error_code, $error_message, $http_status_code ) {
24
+ $this->error_code = $error_code;
25
+ parent::__construct( $error_message, $http_status_code );
26
+ }
27
+
28
+ /**
29
+ * Returns the error code
30
+ *
31
+ * @since 2.2
32
+ * @return string
33
+ */
34
+ public function getErrorCode() {
35
+ return $this->error_code;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Class WC_Square_WC_Products.
41
+ *
42
+ * A WC CRUD class for products.
43
+ *
44
+ * @since 1.0.8
45
+ * @version 1.0.8
46
+ */
47
+ class WC_Square_WC_Products {
48
+ /**
49
+ * Validate the request by checking:
50
+ *
51
+ * 1) the ID is a valid integer
52
+ * 2) the ID returns a valid post object and matches the provided post type
53
+ * 3) the current user has the proper permissions to read/edit/delete the post
54
+ *
55
+ * @since 2.1
56
+ * @param string|int $id the post ID
57
+ * @param string $type the post type, either `shop_order`, `shop_coupon`, or `product`
58
+ * @param string $context the context of the request, either `read`, `edit` or `delete`
59
+ * @return int|WP_Error valid post ID or WP_Error if any of the checks fails
60
+ */
61
+ protected function validate_request( $id, $type, $context ) {
62
+
63
+ if ( 'shop_order' === $type || 'shop_coupon' === $type || 'shop_webhook' === $type ) {
64
+ $resource_name = str_replace( 'shop_', '', $type );
65
+ } else {
66
+ $resource_name = $type;
67
+ }
68
+
69
+ $id = absint( $id );
70
+
71
+ // Validate ID
72
+ if ( empty( $id ) ) {
73
+ return new WP_Error( "woocommerce_api_invalid_{$resource_name}_id", sprintf( __( 'Invalid %s ID', 'woocommerce-square' ), $type ), array( 'status' => 404 ) );
74
+ }
75
+
76
+ // Only custom post types have per-post type/permission checks
77
+ if ( 'customer' !== $type ) {
78
+
79
+ $post = get_post( $id );
80
+
81
+ if ( null === $post ) {
82
+ return new WP_Error( "woocommerce_api_no_{$resource_name}_found", sprintf( __( 'No %1$s found with the ID equal to %2$s', 'woocommerce-square' ), $resource_name, $id ), array( 'status' => 404 ) );
83
+ }
84
+
85
+ // For checking permissions, product variations are the same as the product post type
86
+ $post_type = ( 'product_variation' === $post->post_type ) ? 'product' : $post->post_type;
87
+
88
+ // Validate post type
89
+ if ( $type !== $post_type ) {
90
+ return new WP_Error( "woocommerce_api_invalid_{$resource_name}", sprintf( __( 'Invalid %s', 'woocommerce-square' ), $resource_name ), array( 'status' => 404 ) );
91
+ }
92
+
93
+ // Validate permissions
94
+ switch ( $context ) {
95
+
96
+ case 'read':
97
+ if ( ! $this->is_readable( $post ) ) {
98
+ return new WP_Error( "woocommerce_api_user_cannot_read_{$resource_name}", sprintf( __( 'You do not have permission to read this %s', 'woocommerce-square' ), $resource_name ), array( 'status' => 401 ) );
99
+ }
100
+ break;
101
+
102
+ case 'edit':
103
+ if ( ! $this->is_editable( $post ) ) {
104
+ return new WP_Error( "woocommerce_api_user_cannot_edit_{$resource_name}", sprintf( __( 'You do not have permission to edit this %s', 'woocommerce-square' ), $resource_name ), array( 'status' => 401 ) );
105
+ }
106
+ break;
107
+
108
+ case 'delete':
109
+ if ( ! $this->is_deletable( $post ) ) {
110
+ return new WP_Error( "woocommerce_api_user_cannot_delete_{$resource_name}", sprintf( __( 'You do not have permission to delete this %s', 'woocommerce-square' ), $resource_name ), array( 'status' => 401 ) );
111
+ }
112
+ break;
113
+ }
114
+ }
115
+
116
+ return $id;
117
+ }
118
+
119
+ /**
120
+ * Checks if the given post is readable by the current user
121
+ *
122
+ * @since 2.1
123
+ * @see WC_API_Resource::check_permission()
124
+ * @param WP_Post|int $post
125
+ * @return bool
126
+ */
127
+ protected function is_readable( $post ) {
128
+
129
+ return $this->check_permission( $post, 'read' );
130
+ }
131
+
132
+ /**
133
+ * Checks if the given post is editable by the current user
134
+ *
135
+ * @since 2.1
136
+ * @see WC_API_Resource::check_permission()
137
+ * @param WP_Post|int $post
138
+ * @return bool
139
+ */
140
+ protected function is_editable( $post ) {
141
+
142
+ return $this->check_permission( $post, 'edit' );
143
+
144
+ }
145
+
146
+ /**
147
+ * Checks if the given post is deletable by the current user
148
+ *
149
+ * @since 2.1
150
+ * @see WC_API_Resource::check_permission()
151
+ * @param WP_Post|int $post
152
+ * @return bool
153
+ */
154
+ protected function is_deletable( $post ) {
155
+
156
+ return $this->check_permission( $post, 'delete' );
157
+ }
158
+
159
+ /**
160
+ * Checks the permissions for the current user given a post and context
161
+ *
162
+ * @since 2.1
163
+ * @param WP_Post|int $post
164
+ * @param string $context the type of permission to check, either `read`, `write`, or `delete`
165
+ * @return bool true if the current user has the permissions to perform the context on the post
166
+ */
167
+ private function check_permission( $post, $context ) {
168
+ $permission = false;
169
+
170
+ if ( ! is_a( $post, 'WP_Post' ) ) {
171
+ $post = get_post( $post );
172
+ }
173
+
174
+ if ( is_null( $post ) ) {
175
+ return $permission;
176
+ }
177
+
178
+ $post_type = get_post_type_object( $post->post_type );
179
+
180
+ if ( 'read' === $context ) {
181
+ $permission = 'revision' !== $post->post_type && current_user_can( $post_type->cap->read_private_posts, $post->ID );
182
+ } elseif ( 'edit' === $context ) {
183
+ $permission = current_user_can( $post_type->cap->edit_post, $post->ID );
184
+ } elseif ( 'delete' === $context ) {
185
+ $permission = current_user_can( $post_type->cap->delete_post, $post->ID );
186
+ }
187
+
188
+ return apply_filters( 'woocommerce_api_check_permission', $permission, $context, $post, $post_type );
189
+ }
190
+
191
+ /**
192
+ * Get the product for the given ID
193
+ *
194
+ * @since 2.1
195
+ * @param int $id the product ID
196
+ * @param string $fields
197
+ * @return array
198
+ */
199
+ public function get_product( $id, $fields = null ) {
200
+
201
+ $id = $this->validate_request( $id, 'product', 'read' );
202
+
203
+ if ( is_wp_error( $id ) ) {
204
+ return $id;
205
+ }
206
+
207
+ $product = wc_get_product( $id );
208
+
209
+ // add data that applies to every product type
210
+ $product_data = $this->get_product_data( $product );
211
+
212
+ // add variations to variable products
213
+ if ( $product->is_type( 'variable' ) && $product->has_child() ) {
214
+ $product_data['variations'] = $this->get_variation_data( $product );
215
+ }
216
+
217
+ // add the parent product data to an individual variation
218
+ if ( $product->is_type( 'variation' ) && $product->parent ) {
219
+ $product_data['parent'] = $this->get_product_data( $product->parent );
220
+ }
221
+
222
+ // Add grouped products data
223
+ if ( $product->is_type( 'grouped' ) && $product->has_child() ) {
224
+ $product_data['grouped_products'] = $this->get_grouped_products_data( $product );
225
+ }
226
+
227
+ $product_post_parent = version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->post->post_parent : $product->get_parent_id();
228
+
229
+ if ( $product->is_type( 'simple' ) && ! empty( $product_post_parent ) ) {
230
+ $_product = wc_get_product( $product_post_parent );
231
+ $product_data['parent'] = $this->get_product_data( $_product );
232
+ }
233
+
234
+ return array( 'product' => apply_filters( 'woocommerce_api_product_response', $product_data, $product, $fields ) );
235
+ }
236
+
237
+ /**
238
+ * Create a new product.
239
+ *
240
+ * @since 2.2
241
+ * @param array $data posted data
242
+ * @return array
243
+ */
244
+ public function create_product( $data ) {
245
+ $id = 0;
246
+
247
+ try {
248
+ if ( ! isset( $data['product'] ) ) {
249
+ throw new WC_Square_API_Exception( 'woocommerce_api_missing_product_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce-square' ), 'product' ), 400 );
250
+ }
251
+
252
+ $data = $data['product'];
253
+
254
+ // Check permissions.
255
+ if ( ! current_user_can( 'publish_products' ) ) {
256
+ throw new WC_Square_API_Exception( 'woocommerce_api_user_cannot_create_product', __( 'You do not have permission to create products', 'woocommerce-square' ), 401 );
257
+ }
258
+
259
+ $data = apply_filters( 'woocommerce_api_create_product_data', $data, $this );
260
+
261
+ // Check if product title is specified.
262
+ if ( ! isset( $data['title'] ) ) {
263
+ throw new WC_Square_API_Exception( 'woocommerce_api_missing_product_title', sprintf( __( 'Missing parameter %s', 'woocommerce-square' ), 'title' ), 400 );
264
+ }
265
+
266
+ // Check product type.
267
+ if ( ! isset( $data['type'] ) ) {
268
+ $data['type'] = 'simple';
269
+ }
270
+
271
+ // Set visible visibility when not sent.
272
+ if ( ! isset( $data['catalog_visibility'] ) ) {
273
+ $data['catalog_visibility'] = 'visible';
274
+ }
275
+
276
+ // Validate the product type.
277
+ if ( ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) {
278
+ throw new WC_Square_API_Exception( 'woocommerce_api_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce-square' ), implode( ', ', array_keys( wc_get_product_types() ) ) ), 400 );
279
+ }
280
+
281
+ // Enable description html tags.
282
+ $post_content = isset( $data['description'] ) ? wc_clean( $data['description'] ) : '';
283
+ if ( $post_content && isset( $data['enable_html_description'] ) && true === $data['enable_html_description'] ) {
284
+
285
+ $post_content = $data['description'];
286
+ }
287
+
288
+ // Enable short description html tags.
289
+ $post_excerpt = isset( $data['short_description'] ) ? wc_clean( $data['short_description'] ) : '';
290
+ if ( $post_excerpt && isset( $data['enable_html_short_description'] ) && true === $data['enable_html_short_description'] ) {
291
+ $post_excerpt = $data['short_description'];
292
+ }
293
+
294
+ $new_product = array(
295
+ 'post_title' => wc_clean( $data['title'] ),
296
+ 'post_status' => isset( $data['status'] ) ? wc_clean( $data['status'] ) : 'publish',
297
+ 'post_type' => 'product',
298
+ 'post_excerpt' => isset( $data['short_description'] ) ? $post_excerpt : '',
299
+ 'post_content' => isset( $data['description'] ) ? $post_content : '',
300
+ 'post_author' => get_current_user_id(),
301
+ 'menu_order' => isset( $data['menu_order'] ) ? intval( $data['menu_order'] ) : 0,
302
+ );
303
+
304
+ if ( ! empty( $data['name'] ) ) {
305
+ $new_product = array_merge( $new_product, array( 'post_name' => sanitize_title( $data['name'] ) ) );
306
+ }
307
+
308
+ // Attempts to create the new product.
309
+ $id = wp_insert_post( $new_product, true );
310
+
311
+ // Checks for an error in the product creation.
312
+ if ( is_wp_error( $id ) ) {
313
+ throw new WC_Square_API_Exception( 'woocommerce_api_cannot_create_product', $id->get_error_message(), 400 );
314
+ }
315
+
316
+ // Check for featured/gallery images, upload it and set it.
317
+ if ( isset( $data['images'] ) ) {
318
+ $this->save_product_images( $id, $data['images'] );
319
+ }
320
+
321
+ // Save product meta fields.
322
+ $this->save_product_meta( $id, $data );
323
+
324
+ // Save variations.
325
+ if ( isset( $data['type'] ) && 'variable' == $data['type'] && isset( $data['variations'] ) && is_array( $data['variations'] ) ) {
326
+ $this->save_variations( $id, $data );
327
+ }
328
+
329
+ do_action( 'woocommerce_api_create_product', $id, $data );
330
+
331
+ // Clear cache/transients.
332
+ wc_delete_product_transients( $id );
333
+
334
+ return $this->get_product( $id );
335
+ } catch ( WC_Square_API_Exception $e ) {
336
+ // Remove the product when fails.
337
+ $this->clear_product( $id );
338
+
339
+ return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Edit a product
345
+ *
346
+ * @since 2.2
347
+ * @param int $id the product ID
348
+ * @param array $data
349
+ * @return array
350
+ */
351
+ public function edit_product( $id, $data ) {
352
+ try {
353
+ if ( ! isset( $data['product'] ) ) {
354
+ throw new WC_Square_API_Exception( 'woocommerce_api_missing_product_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce-square' ), 'product' ), 400 );
355
+ }
356
+
357
+ $data = $data['product'];
358
+
359
+ $id = $this->validate_request( $id, 'product', 'edit' );
360
+
361
+ if ( is_wp_error( $id ) ) {
362
+ return $id;
363
+ }
364
+
365
+ $data = apply_filters( 'woocommerce_api_edit_product_data', $data, $this );
366
+
367
+ // Product title.
368
+ if ( isset( $data['title'] ) ) {
369
+ wp_update_post( array( 'ID' => $id, 'post_title' => wc_clean( $data['title'] ) ) );
370
+ }
371
+
372
+ // Product name (slug).
373
+ if ( isset( $data['name'] ) ) {
374
+ wp_update_post( array( 'ID' => $id, 'post_name' => sanitize_title( $data['name'] ) ) );
375
+ }
376
+
377
+ // Product status.
378
+ if ( isset( $data['status'] ) ) {
379
+ wp_update_post( array( 'ID' => $id, 'post_status' => wc_clean( $data['status'] ) ) );
380
+ }
381
+
382
+ // Product short description.
383
+ if ( isset( $data['short_description'] ) ) {
384
+ // Enable short description html tags.
385
+ $post_excerpt = ( isset( $data['enable_html_short_description'] ) && true === $data['enable_html_short_description'] ) ? $data['short_description'] : wc_clean( $data['short_description'] );
386
+
387
+ wp_update_post( array( 'ID' => $id, 'post_excerpt' => $post_excerpt ) );
388
+ }
389
+
390
+ // Product description.
391
+ if ( isset( $data['description'] ) ) {
392
+ // Enable description html tags.
393
+ $post_content = ( isset( $data['enable_html_description'] ) && true === $data['enable_html_description'] ) ? $data['description'] : wc_clean( $data['description'] );
394
+
395
+ wp_update_post( array( 'ID' => $id, 'post_content' => $post_content ) );
396
+ }
397
+
398
+ // Menu order.
399
+ if ( isset( $data['menu_order'] ) ) {
400
+ wp_update_post( array( 'ID' => $id, 'menu_order' => intval( $data['menu_order'] ) ) );
401
+ }
402
+
403
+ // Validate the product type.
404
+ if ( isset( $data['type'] ) && ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) {
405
+ throw new WC_Square_API_Exception( 'woocommerce_api_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce-square' ), implode( ', ', array_keys( wc_get_product_types() ) ) ), 400 );
406
+ }
407
+
408
+ // Check for featured/gallery images, upload it and set it.
409
+ if ( isset( $data['images'] ) ) {
410
+ $this->save_product_images( $id, $data['images'] );
411
+ }
412
+
413
+ // Save product meta fields.
414
+ $this->save_product_meta( $id, $data );
415
+
416
+ // Save variations.
417
+ $product = wc_get_product( $id );
418
+ if ( $product->is_type( 'variable' ) ) {
419
+ if ( isset( $data['variations'] ) && is_array( $data['variations'] ) ) {
420
+ $this->save_variations( $id, $data );
421
+ } else {
422
+ // Just sync variations
423
+ WC_Product_Variable::sync( $id );
424
+ WC_Product_Variable::sync_stock_status( $id );
425
+ }
426
+ }
427
+
428
+ do_action( 'woocommerce_api_edit_product', $id, $data );
429
+
430
+ // Clear cache/transients.
431
+ wc_delete_product_transients( $id );
432
+
433
+ return $this->get_product( $id );
434
+ } catch ( WC_Square_API_Exception $e ) {
435
+ return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
436
+ }
437
+ }
438
+
439
+ /**
440
+ * Get a listing of product categories
441
+ *
442
+ * @since 2.2
443
+ * @param string|null $fields fields to limit response to
444
+ * @return array
445
+ */
446
+ public function get_product_categories( $fields = null ) {
447
+ try {
448
+ // Permissions check
449
+ if ( ! current_user_can( 'manage_product_terms' ) ) {
450
+ throw new WC_Square_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product categories', 'woocommerce-square' ), 401 );
451
+ }
452
+
453
+ $product_categories = array();
454
+
455
+ $terms = get_terms( 'product_cat', array( 'hide_empty' => false, 'fields' => 'ids' ) );
456
+
457
+ foreach ( $terms as $term_id ) {
458
+ $product_categories[] = current( $this->get_product_category( $term_id, $fields ) );
459
+ }
460
+
461
+ return array( 'product_categories' => apply_filters( 'woocommerce_api_product_categories_response', $product_categories, $terms, $fields, $this ) );
462
+ } catch ( WC_Square_API_Exception $e ) {
463
+ return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
464
+ }
465
+ }
466
+
467
+ /**
468
+ * Get the product category for the given ID
469
+ *
470
+ * @since 2.2
471
+ * @param string $id product category term ID
472
+ * @param string|null $fields fields to limit response to
473
+ * @return array
474
+ */
475
+ public function get_product_category( $id, $fields = null ) {
476
+ try {
477
+ $id = absint( $id );
478
+
479
+ // Validate ID
480
+ if ( empty( $id ) ) {
481
+ throw new WC_Square_API_Exception( 'woocommerce_api_invalid_product_category_id', __( 'Invalid product category ID', 'woocommerce-square' ), 400 );
482
+ }
483
+
484
+ // Permissions check
485
+ if ( ! current_user_can( 'manage_product_terms' ) ) {
486
+ throw new WC_Square_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product categories', 'woocommerce-square' ), 401 );
487
+ }
488
+
489
+ $term = get_term( $id, 'product_cat' );
490
+
491
+ if ( is_wp_error( $term ) || is_null( $term ) ) {
492
+ throw new WC_Square_API_Exception( 'woocommerce_api_invalid_product_category_id', __( 'A product category with the provided ID could not be found', 'woocommerce-square' ), 404 );
493
+ }
494
+
495
+ $term_id = intval( $term->term_id );
496
+
497
+ // Get category display type
498
+ $display_type = WC_Square_Utils::wc_compat_get_term_meta( $term_id, 'display_type' );
499
+
500
+ // Get category image
501
+ $image = '';
502
+ if ( $image_id = WC_Square_Utils::wc_compat_get_term_meta( $term_id, 'thumbnail_id' ) ) {
503
+ $image = wp_get_attachment_url( $image_id );
504
+ }
505
+
506
+ $product_category = array(
507
+ 'id' => $term_id,
508
+ 'name' => $term->name,
509
+ 'slug' => $term->slug,
510
+ 'parent' => $term->parent,
511
+ 'description' => $term->description,
512
+ 'display' => $display_type ? $display_type : 'default',
513
+ 'image' => $image ? esc_url( $image ) : '',
514
+ 'count' => intval( $term->count ),
515
+ );
516
+
517
+ return array( 'product_category' => apply_filters( 'woocommerce_api_product_category_response', $product_category, $id, $fields, $term, $this ) );
518
+ } catch ( WC_Square_API_Exception $e ) {
519
+ return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
520
+ }
521
+ }
522
+
523
+ /**
524
+ * Create a new product category.
525
+ *
526
+ * @since 2.5.0
527
+ * @param array $data Posted data
528
+ * @return array|WP_Error Product category if succeed, otherwise WP_Error
529
+ * will be returned
530
+ */
531
+ public function create_product_category( $data ) {
532
+ global $wpdb;
533
+
534
+ try {
535
+ if ( ! isset( $data['product_category'] ) ) {
536
+ throw new WC_Square_API_Exception( 'woocommerce_api_missing_product_category_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce-square' ), 'product_category' ), 400 );
537
+ }
538
+
539
+ // Check permissions
540
+ if ( ! current_user_can( 'manage_product_terms' ) ) {
541
+ throw new WC_Square_API_Exception( 'woocommerce_api_user_cannot_create_product_category', __( 'You do not have permission to create product categories', 'woocommerce-square' ), 401 );
542
+ }
543
+
544
+ $defaults = array(
545
+ 'name' => '',
546
+ 'slug' => '',
547
+ 'description' => '',
548
+ 'parent' => 0,
549
+ 'display' => 'default',
550
+ 'image' => '',
551
+ );
552
+
553
+ $data = wp_parse_args( $data['product_category'], $defaults );
554
+ $data = apply_filters( 'woocommerce_api_create_product_category_data', $data, $this );
555
+
556
+ // Check parent.
557
+ $data['parent'] = absint( $data['parent'] );
558
+ if ( $data['parent'] ) {
559
+ $parent = get_term_by( 'id', $data['parent'], 'product_cat' );
560
+ if ( ! $parent ) {
561
+ throw new WC_Square_API_Exception( 'woocommerce_api_invalid_product_category_parent', __( 'Product category parent is invalid', 'woocommerce-square' ), 400 );
562
+ }
563
+ }
564
+
565
+ // If value of image is numeric, assume value as image_id.
566
+ $image = $data['image'];
567
+ $image_id = 0;
568
+ if ( is_numeric( $image ) ) {
569
+ $image_id = absint( $image );
570
+ } elseif ( ! empty( $image ) ) {
571
+ $upload = $this->upload_product_category_image( esc_url_raw( $image ) );
572
+ $image_id = $this->set_product_category_image_as_attachment( $upload );
573
+ }
574
+
575
+ $insert = wp_insert_term( $data['name'], 'product_cat', $data );
576
+ if ( is_wp_error( $insert ) ) {
577
+ throw new WC_Square_API_Exception( 'woocommerce_api_cannot_create_product_category', $insert->get_error_message(), 400 );
578
+ }
579
+
580
+ $id = $insert['term_id'];
581
+
582
+ WC_Square_Utils::wc_compat_update_term_meta( $id, 'display_type', 'default' === $data['display'] ? '' : sanitize_text_field( $data['display'] ) );
583
+
584
+ // Check if image_id is a valid image attachment before updating the term meta.
585
+ if ( $image_id && wp_attachment_is_image( $image_id ) ) {
586
+ WC_Square_Utils::wc_compat_update_term_meta( $id, 'thumbnail_id', $image_id );
587
+ }
588
+
589
+ do_action( 'woocommerce_api_create_product_category', $id, $data );
590
+
591
+ return $this->get_product_category( $id );
592
+ } catch ( WC_Square_API_Exception $e ) {
593
+ return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
594
+ }
595
+ }
596
+
597
+ /**
598
+ * Edit a product category.
599
+ *
600
+ * @since 2.5.0
601
+ * @param int $id Product category term ID
602
+ * @param array $data Posted data
603
+ * @return array|WP_Error Product category if succeed, otherwise WP_Error
604
+ * will be returned
605
+ */
606
+ public function edit_product_category( $id, $data ) {
607
+ global $wpdb;
608
+
609
+ try {
610
+ if ( ! isset( $data['product_category'] ) ) {
611
+ throw new WC_Square_API_Exception( 'woocommerce_api_missing_product_category', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce-square' ), 'product_category' ), 400 );
612
+ }
613
+
614
+ $id = absint( $id );
615
+ $data = $data['product_category'];
616
+
617
+ // Check permissions.
618
+ if ( ! current_user_can( 'manage_product_terms' ) ) {
619
+ throw new WC_Square_API_Exception( 'woocommerce_api_user_cannot_edit_product_category', __( 'You do not have permission to edit product categories', 'woocommerce-square' ), 401 );
620
+ }
621
+
622
+ $data = apply_filters( 'woocommerce_api_edit_product_category_data', $data, $this );
623
+ $category = $this->get_product_category( $id );
624
+
625
+ if ( is_wp_error( $category ) ) {
626
+ return $category;
627
+ }
628
+
629
+ if ( isset( $data['image'] ) ) {
630
+ $image_id = 0;
631
+
632
+ // If value of image is numeric, assume value as image_id.
633
+ $image = $data['image'];
634
+ if ( is_numeric( $image ) ) {
635
+ $image_id = absint( $image );
636
+ } elseif ( ! empty( $image ) ) {
637
+ $upload = $this->upload_product_category_image( esc_url_raw( $image ) );
638
+ $image_id = $this->set_product_category_image_as_attachment( $upload );
639
+ }
640
+
641
+ // In case client supplies invalid image or wants to unset category image.
642
+ if ( ! wp_attachment_is_image( $image_id ) ) {
643
+ $image_id = '';
644
+ }
645
+ }
646
+
647
+ $update = wp_update_term( $id, 'product_cat', $data );
648
+ if ( is_wp_error( $update ) ) {
649
+ throw new WC_Square_API_Exception( 'woocommerce_api_cannot_edit_product_catgory', __( 'Could not edit the category', 'woocommerce-square' ), 400 );
650
+ }
651
+
652
+ if ( ! empty( $data['display'] ) ) {
653
+ WC_Square_Utils::wc_compat_update_term_meta( $id, 'display_type', 'default' === $data['display'] ? '' : sanitize_text_field( $data['display'] ) );
654
+ }
655
+
656
+ if ( isset( $image_id ) ) {
657
+ WC_Square_Utils::wc_compat_update_term_meta( $id, 'thumbnail_id', $image_id );
658
+ }
659
+
660
+ do_action( 'woocommerce_api_edit_product_category', $id, $data );
661
+
662
+ return $this->get_product_category( $id );
663
+ } catch ( WC_Square_API_Exception $e ) {
664
+ return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
665
+ }
666
+ }
667
+
668
+ /**
669
+ * Get standard product data that applies to every product type
670
+ *
671
+ * @since 2.1
672
+ * @param WC_Product $product
673
+ * @return WC_Product
674
+ */
675
+ private function get_product_data( $product ) {
676
+ $product_post = version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->get_post_data() : get_post( version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->id : $product->get_id() );
677
+
678
+ return array(
679
+ 'title' => $product->get_title(),
680
+ 'id' => (int) $product->is_type( 'variation' ) ? ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->get_variation_id() : $product->get_id() ) : ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->id : $product->get_id() ),
681
+ 'created_at' => $this->format_datetime( $product_post->post_date_gmt ),
682
+ 'updated_at' => $this->format_datetime( $product_post->post_modified_gmt ),
683
+ 'type' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->product_type : $product->get_type(),
684
+ 'status' => $product_post->post_status,
685
+ 'downloadable' => $product->is_downloadable(),
686
+ 'virtual' => $product->is_virtual(),
687
+ 'permalink' => $product->get_permalink(),
688
+ 'sku' => $product->get_sku(),
689
+ 'price' => $product->get_price(),
690
+ 'regular_price' => $product->get_regular_price(),
691
+ 'sale_price' => $product->get_sale_price() ? $product->get_sale_price() : null,
692
+ 'price_html' => $product->get_price_html(),
693
+ 'taxable' => $product->is_taxable(),
694
+ 'tax_status' => $product->get_tax_status(),
695
+ 'tax_class' => $product->get_tax_class(),
696
+ 'managing_stock' => $product->managing_stock(),
697
+ 'stock_quantity' => $product->get_stock_quantity(),
698
+ 'in_stock' => $product->is_in_stock(),
699
+ 'backorders_allowed' => $product->backorders_allowed(),
700
+ 'backordered' => $product->is_on_backorder(),
701
+ 'sold_individually' => $product->is_sold_individually(),
702
+ 'purchaseable' => $product->is_purchasable(),
703
+ 'featured' => $product->is_featured(),
704
+ 'visible' => $product->is_visible(),
705
+ 'catalog_visibility' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->visibility : $product->get_catalog_visibility(),
706
+ 'on_sale' => $product->is_on_sale(),
707
+ 'product_url' => $product->is_type( 'external' ) ? $product->get_product_url() : '',
708
+ 'button_text' => $product->is_type( 'external' ) ? $product->get_button_text() : '',
709
+ 'weight' => $product->get_weight() ? $product->get_weight() : null,
710
+ 'dimensions' => array(
711
+ 'length' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->length : $product->get_length(),
712
+ 'width' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->width : $product->get_width(),
713
+ 'height' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->height : $product->get_height(),
714
+ 'unit' => get_option( 'woocommerce_dimension_unit' ),
715
+ ),
716
+ 'shipping_required' => $product->needs_shipping(),
717
+ 'shipping_taxable' => $product->is_shipping_taxable(),
718
+ 'shipping_class' => $product->get_shipping_class(),
719
+ 'shipping_class_id' => ( 0 !== $product->get_shipping_class_id() ) ? $product->get_shipping_class_id() : null,
720
+ 'description' => wpautop( do_shortcode( $product_post->post_content ) ),
721
+ 'short_description' => apply_filters( 'woocommerce_short_description', $product_post->post_excerpt ),
722
+ 'reviews_allowed' => ( 'open' === $product_post->comment_status ),
723
+ 'average_rating' => wc_format_decimal( $product->get_average_rating(), 2 ),
724
+ 'rating_count' => (int) $product->get_rating_count(),
725
+ 'related_ids' => array_map( 'absint', array_values( ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->get_related() : wc_get_related_products( $product->get_id() ) ) ) ),
726
+ 'upsell_ids' => array_map( 'absint', ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->get_upsells() : $product->get_upsell_ids() ) ),
727
+ 'cross_sell_ids' => array_map( 'absint', ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->get_cross_sells() : $product->get_cross_sell_ids() ) ),
728
+ 'parent_id' => $product->is_type( 'variation' ) ? ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->parent->id : $product->get_parent_id() ) : ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->post->post_parent : $product->get_id() ),
729
+ 'categories' => wp_get_post_terms( version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->id : $product->get_id(), 'product_cat', array( 'fields' => 'names' ) ),
730
+ 'tags' => wp_get_post_terms( version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->id : $product->get_id(), 'product_tag', array( 'fields' => 'names' ) ),
731
+ 'images' => $this->get_images( $product ),
732
+ 'featured_src' => (string) wp_get_attachment_url( get_post_thumbnail_id( $product->is_type( 'variation' ) ? ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->variation_id : $product->get_id() ) : ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->id : $product->get_id() ) ) ),
733
+ 'attributes' => $this->get_attributes( $product ),
734
+ 'downloads' => $this->get_downloads( $product ),
735
+ 'download_limit' => (int) version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->download_limit : $product->get_download_limit(),
736
+ 'download_expiry' => (int) version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->download_expiry : $product->get_download_expiry(),
737
+ 'download_type' => 'standard',
738
+ 'purchase_note' => wpautop( do_shortcode( wp_kses_post( version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->purchase_note : $product->get_purchase_note() ) ) ),
739
+ 'total_sales' => metadata_exists( 'post', version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->id : $product->get_id(), 'total_sales' ) ? (int) get_post_meta( version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->id : $product->get_id(), 'total_sales', true ) : 0,
740
+ 'variations' => array(),
741
+ 'parent' => array(),
742
+ 'grouped_products' => array(),
743
+ 'menu_order' => $this->get_product_menu_order( $product ),
744
+ );
745
+ }
746
+
747
+ /**
748
+ * Get product menu order.
749
+ *
750
+ * @since 2.5.3
751
+ * @param WC_Product $product
752
+ * @return int
753
+ */
754
+ private function get_product_menu_order( $product ) {
755
+ $product_post = version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->get_post_data() : get_post( version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->id : $product->get_id() );
756
+
757
+ $menu_order = $product_post->menu_order;
758
+
759
+ if ( $product->is_type( 'variation' ) ) {
760
+ $_product = get_post( version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->get_variation_id() : $product->get_id() );
761
+ $menu_order = $_product->menu_order;
762
+ }
763
+
764
+ return apply_filters( 'woocommerce_api_product_menu_order', $menu_order, $product );
765
+ }
766
+
767
+ /**
768
+ * Get an individual variation's data
769
+ *
770
+ * @since 2.1
771
+ * @param WC_Product $product
772
+ * @return array
773
+ */
774
+ private function get_variation_data( $product ) {
775
+ $variations = array();
776
+
777
+ foreach ( $product->get_children() as $child_id ) {
778
+
779
+ $variation = version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->get_child( $child_id ) : wc_get_product( $child_id );
780
+
781
+ if ( ! $variation->exists() ) {
782
+ continue;
783
+ }
784
+
785
+ $post_data = get_post( version_compare( WC_VERSION, '3.0.0', '<' ) ? $variation->get_variation_id() : $variation->get_id() );
786
+
787
+ $variations[] = array(
788
+ 'id' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $variation->get_variation_id() : $variation->get_id(),
789
+ 'created_at' => $this->format_datetime( $post_data->post_date_gmt ),
790
+ 'updated_at' => $this->format_datetime( $post_data->post_modified_gmt ),
791
+ 'downloadable' => $variation->is_downloadable(),
792
+ 'virtual' => $variation->is_virtual(),
793
+ 'permalink' => $variation->get_permalink(),
794
+ 'sku' => $variation->get_sku(),
795
+ 'price' => $variation->get_price(),
796
+ 'regular_price' => $variation->get_regular_price(),
797
+ 'sale_price' => $variation->get_sale_price() ? $variation->get_sale_price() : null,
798
+ 'taxable' => $variation->is_taxable(),
799
+ 'tax_status' => $variation->get_tax_status(),
800
+ 'tax_class' => $variation->get_tax_class(),
801
+ 'managing_stock' => $variation->managing_stock(),
802
+ 'stock_quantity' => $variation->get_stock_quantity(),
803
+ 'in_stock' => $variation->is_in_stock(),
804
+ 'backorders_allowed' => $variation->backorders_allowed(),
805
+ 'backordered' => $variation->is_on_backorder(),
806
+ 'purchaseable' => $variation->is_purchasable(),
807
+ 'visible' => $variation->variation_is_visible(),
808
+ 'on_sale' => $variation->is_on_sale(),
809
+ 'weight' => $variation->get_weight() ? $variation->get_weight() : null,
810
+ 'dimensions' => array(
811
+ 'length' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $variation->length : $variation->get_length(),
812
+ 'width' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $variation->width : $variation->get_width(),
813
+ 'height' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $variation->height : $variation->get_height(),
814
+ 'unit' => get_option( 'woocommerce_dimension_unit' ),
815
+ ),
816
+ 'shipping_class' => $variation->get_shipping_class(),
817
+ 'shipping_class_id' => ( 0 !== $variation->get_shipping_class_id() ) ? $variation->get_shipping_class_id() : null,
818
+ 'image' => $this->get_images( $variation ),
819
+ 'attributes' => $this->get_attributes( $variation ),
820
+ 'downloads' => $this->get_downloads( $variation ),
821
+ 'download_limit' => (int) version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->download_limit : $product->get_download_limit(),
822
+ 'download_expiry' => (int) version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->download_expiry : $product->get_download_expiry(),
823
+ );
824
+ }
825
+
826
+ return $variations;
827
+ }
828
+
829
+ /**
830
+ * Get grouped products data
831
+ *
832
+ * @since 2.5.0
833
+ * @param WC_Product $product
834
+ *
835
+ * @return array
836
+ */
837
+ private function get_grouped_products_data( $product ) {
838
+ $products = array();
839
+
840
+ foreach ( $product->get_children() as $child_id ) {
841
+ $_product = version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->get_child( $child_id ) : wc_get_product( $child_id );
842
+
843
+ if ( ! $_product->exists() ) {
844
+ continue;
845
+ }
846
+
847
+ $products[] = $this->get_product_data( $_product );
848
+
849
+ }
850
+
851
+ return $products;
852
+ }
853
+
854
+ /**
855
+ * Save product meta.
856
+ *
857
+ * @since 2.2
858
+ * @param int $product_id
859
+ * @param array $data
860
+ * @return bool
861
+ * @throws WC_Square_API_Exception
862
+ */
863
+ protected function save_product_meta( $product_id, $data ) {
864
+ global $wpdb;
865
+
866
+ // Product Type.
867
+ $product_type = null;
868
+ if ( isset( $data['type'] ) ) {
869
+ $product_type = wc_clean( $data['type'] );
870
+ wp_set_object_terms( $product_id, $product_type, 'product_type' );
871
+ } else {
872
+ $_product_type = get_the_terms( $product_id, 'product_type' );
873
+ if ( is_array( $_product_type ) ) {
874
+ $_product_type = current( $_product_type );
875
+ $product_type = $_product_type->slug;
876
+ }
877
+ }
878
+
879
+ // Default total sales.
880
+ add_post_meta( $product_id, 'total_sales', '0', true );
881
+
882
+ // Virtual.
883
+ if ( isset( $data['virtual'] ) ) {
884
+ update_post_meta( $product_id, '_virtual', ( true === $data['virtual'] ) ? 'yes' : 'no' );
885
+ }
886
+
887
+ // Tax status.
888
+ if ( isset( $data['tax_status'] ) ) {
889
+ update_post_meta( $product_id, '_tax_status', wc_clean( $data['tax_status'] ) );
890
+ }
891
+
892
+ // Tax Class.
893
+ if ( isset( $data['tax_class'] ) ) {
894
+ update_post_meta( $product_id, '_tax_class', wc_clean( $data['tax_class'] ) );
895
+ }
896
+
897
+ // Catalog Visibility.
898
+ if ( isset( $data['catalog_visibility'] ) ) {
899
+ update_post_meta( $product_id, '_visibility', wc_clean( $data['catalog_visibility'] ) );
900
+ }
901
+
902
+ // Purchase Note.
903
+ if ( isset( $data['purchase_note'] ) ) {
904
+ update_post_meta( $product_id, '_purchase_note', wc_clean( $data['purchase_note'] ) );
905
+ }
906
+
907
+ // Featured Product.
908
+ if ( isset( $data['featured'] ) ) {
909
+ update_post_meta( $product_id, '_featured', ( true === $data['featured'] ) ? 'yes' : 'no' );
910
+ }
911
+
912
+ // Shipping data.
913
+ $this->save_product_shipping_data( $product_id, $data );
914
+
915
+ // SKU.
916
+ if ( isset( $data['sku'] ) ) {
917
+ $sku = get_post_meta( $product_id, '_sku', true );
918
+ $new_sku = wc_clean( $data['sku'] );
919
+
920
+ if ( '' == $new_sku ) {
921
+ update_post_meta( $product_id, '_sku', '' );
922
+ } elseif ( $new_sku !== $sku ) {
923
+ if ( ! empty( $new_sku ) ) {
924
+ $unique_sku = wc_product_has_unique_sku( $product_id, $new_sku );
925
+ if ( ! $unique_sku ) {
926
+ throw new WC_Square_API_Exception( 'woocommerce_api_product_sku_already_exists', __( 'The SKU already exists on another product', 'woocommerce-square' ), 400 );
927
+ } else {
928
+ update_post_meta( $product_id, '_sku', $new_sku );
929
+ }
930
+ } else {
931
+ update_post_meta( $product_id, '_sku', '' );
932
+ }
933
+ }
934
+ }
935
+
936
+ // Attributes.
937
+ if ( isset( $data['attributes'] ) ) {
938
+ $attributes = array();
939
+
940
+ foreach ( $data['attributes'] as $attribute ) {
941
+ $is_taxonomy = 0;
942
+ $taxonomy = 0;
943
+
944
+ if ( ! isset( $attribute['name'] ) ) {
945
+ continue;
946
+ }
947
+
948
+ $attribute_slug = sanitize_title( $attribute['name'] );
949
+
950
+ if ( isset( $attribute['slug'] ) ) {
951
+ $taxonomy = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] );
952
+ $attribute_slug = sanitize_title( $attribute['slug'] );
953
+ }
954
+
955
+ if ( $taxonomy ) {
956
+ $is_taxonomy = 1;
957
+ }
958
+
959
+ if ( $is_taxonomy ) {
960
+
961
+ if ( isset( $attribute['options'] ) ) {
962
+ $options = $attribute['options'];
963
+
964
+ if ( ! is_array( $attribute['options'] ) ) {
965
+ // Text based attributes - Posted values are term names.
966
+ $options = explode( WC_DELIMITER, $options );
967
+ }
968
+
969
+ $values = array_map( 'wc_sanitize_term_text_based', $options );
970
+ $values = array_filter( $values, 'strlen' );
971
+ } else {
972
+ $values = array();
973
+ }
974
+
975
+ // Update post terms.
976
+ if ( taxonomy_exists( $taxonomy ) ) {
977
+ wp_set_object_terms( $product_id, $values, $taxonomy );
978
+ }
979
+
980
+ if ( ! empty( $values ) ) {
981
+ // Add attribute to array, but don't set values.
982
+ $attributes[ $taxonomy ] = array(
983
+ 'name' => $taxonomy,
984
+ 'value' => '',
985
+ 'position' => isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0',
986
+ 'is_visible' => ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0,
987
+ 'is_variation' => ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0,
988
+ 'is_taxonomy' => $is_taxonomy,
989
+ );
990
+ }
991
+ } elseif ( isset( $attribute['options'] ) ) {
992
+ // Array based.
993
+ if ( is_array( $attribute['options'] ) ) {
994
+ $values = implode( ' ' . WC_DELIMITER . ' ', array_map( 'wc_clean', $attribute['options'] ) );
995
+
996
+ // Text based, separate by pipe.
997
+ } else {
998
+ $values = implode( ' ' . WC_DELIMITER . ' ', array_map( 'wc_clean', explode( WC_DELIMITER, $attribute['options'] ) ) );
999
+ }
1000
+
1001
+ // Custom attribute - Add attribute to array and set the values.
1002
+ $attributes[ $attribute_slug ] = array(
1003
+ 'name' => wc_clean( $attribute['name'] ),
1004
+ 'value' => $values,
1005
+ 'position' => isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0',
1006
+ 'is_visible' => ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0,
1007
+ 'is_variation' => ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0,
1008
+ 'is_taxonomy' => $is_taxonomy,
1009
+ );
1010
+ }
1011
+ }
1012
+
1013
+ uasort( $attributes, 'wc_product_attribute_uasort_comparison' );
1014
+
1015
+ update_post_meta( $product_id, '_product_attributes', $attributes );
1016
+ }
1017
+
1018
+ // Sales and prices.
1019
+ if ( in_array( $product_type, array( 'variable', 'grouped' ) ) ) {
1020
+
1021
+ // Variable and grouped products have no prices.
1022
+ update_post_meta( $product_id, '_regular_price', '' );
1023
+ update_post_meta( $product_id, '_sale_price', '' );
1024
+ update_post_meta( $product_id, '_sale_price_dates_from', '' );
1025
+ update_post_meta( $product_id, '_sale_price_dates_to', '' );
1026
+ update_post_meta( $product_id, '_price', '' );
1027
+
1028
+ } else {
1029
+ $current_regular_price = get_post_meta( $product_id, '_regular_price', true );
1030
+ $current_sale_price = get_post_meta( $product_id, '_sale_price', true );
1031
+
1032
+ // Regular Price passed from Square ( They only have one price ).
1033
+ if ( isset( $data['regular_price'] ) ) {
1034
+ // Check if current product is on sale.
1035
+ if ( $current_regular_price > $current_sale_price && $data['regular_price'] < $current_regular_price ) {
1036
+ $regular_price = get_post_meta( $product_id, '_regular_price', true );
1037
+ $sale_price = $data['regular_price'];
1038
+ } else {
1039
+ $regular_price = ( '' === $data['regular_price'] ) ? '' : $data['regular_price'];
1040
+ $sale_price = '';
1041
+ }
1042
+ } else {
1043
+ $regular_price = get_post_meta( $product_id, '_regular_price', true );
1044
+ $sale_price = get_post_meta( $product_id, '_sale_price', true );
1045
+ }
1046
+
1047
+ if ( isset( $data['sale_price_dates_from'] ) ) {
1048
+ $date_from = $data['sale_price_dates_from'];
1049
+ } else {
1050
+ $date_from = get_post_meta( $product_id, '_sale_price_dates_from', true );
1051
+ $date_from = ( '' === $date_from ) ? '' : date( 'Y-m-d', $date_from );
1052
+ }
1053
+
1054
+ if ( isset( $data['sale_price_dates_to'] ) ) {
1055
+ $date_to = $data['sale_price_dates_to'];
1056
+ } else {
1057
+ $date_to = get_post_meta( $product_id, '_sale_price_dates_to', true );
1058
+ $date_to = ( '' === $date_to ) ? '' : date( 'Y-m-d', $date_to );
1059
+ }
1060
+
1061
+ $this->wc_save_product_price( $product_id, $regular_price, $sale_price, $date_from, $date_to );
1062
+ }
1063
+
1064
+ // Product parent ID for groups.
1065
+ if ( isset( $data['parent_id'] ) ) {
1066
+ wp_update_post( array( 'ID' => $product_id, 'post_parent' => absint( $data['parent_id'] ) ) );
1067
+ }
1068
+
1069
+ // Update parent if grouped so price sorting works and stays in sync with the cheapest child.
1070
+ $_product = wc_get_product( $product_id );
1071
+
1072
+ if ( ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $_product->post->post_parent : $_product->get_parent_id() ) > 0 || 'grouped' === $product_type ) {
1073
+
1074
+ $clear_parent_ids = array();
1075
+
1076
+ if ( ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $_product->post->post_parent : $_product->get_parent_id() ) > 0 ) {
1077
+ $clear_parent_ids[] = ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $_product->post->post_parent : $_product->get_parent_id() );
1078
+ }
1079
+
1080
+ if ( 'grouped' === $product_type ) {
1081
+ $clear_parent_ids[] = $product_id;
1082
+ }
1083
+
1084
+ if ( ! empty( $clear_parent_ids ) ) {
1085
+ foreach ( $clear_parent_ids as $clear_id ) {
1086
+
1087
+ $children_by_price = get_posts( array(
1088
+ 'post_parent' => $clear_id,
1089
+ 'orderby' => 'meta_value_num',
1090
+ 'order' => 'asc',
1091
+ 'meta_key' => '_price',
1092
+ 'posts_per_page' => 1,
1093
+ 'post_type' => 'product',
1094
+ 'fields' => 'ids',
1095
+ ) );
1096
+
1097
+ if ( $children_by_price ) {
1098
+ foreach ( $children_by_price as $child ) {
1099
+ $child_price = get_post_meta( $child, '_price', true );
1100
+ update_post_meta( $clear_id, '_price', $child_price );
1101
+ }
1102
+ }
1103
+ }
1104
+ }
1105
+ }
1106
+
1107
+ // Sold Individually.
1108
+ if ( isset( $data['sold_individually'] ) ) {
1109
+ update_post_meta( $product_id, '_sold_individually', ( true === $data['sold_individually'] ) ? 'yes' : '' );
1110
+ }
1111
+
1112
+ // Stock status.
1113
+ if ( isset( $data['in_stock'] ) ) {
1114
+ $stock_status = ( true === $data['in_stock'] ) ? 'instock' : 'outofstock';
1115
+ } else {
1116
+ $stock_status = get_post_meta( $product_id, '_stock_status', true );
1117
+
1118
+ if ( '' === $stock_status ) {
1119
+ $stock_status = 'instock';
1120
+ }
1121
+ }
1122
+
1123
+ // Stock Data.
1124
+ if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) {
1125
+ // Manage stock.
1126
+ if ( isset( $data['managing_stock'] ) ) {
1127
+ $managing_stock = ( true === $data['managing_stock'] ) ? 'yes' : 'no';
1128
+ update_post_meta( $product_id, '_manage_stock', $managing_stock );
1129
+ } else {
1130
+ $managing_stock = get_post_meta( $product_id, '_manage_stock', true );
1131
+ }
1132
+
1133
+ // Backorders.
1134
+ if ( isset( $data['backorders'] ) ) {
1135
+ if ( 'notify' === $data['backorders'] ) {
1136
+ $backorders = 'notify';
1137
+ } else {
1138
+ $backorders = ( true === $data['backorders'] ) ? 'yes' : 'no';
1139
+ }
1140
+
1141
+ update_post_meta( $product_id, '_backorders', $backorders );
1142
+ } else {
1143
+ $backorders = get_post_meta( $product_id, '_backorders', true );
1144
+ if ( '' === $backorders ) {
1145
+ $backorders = 'no';
1146
+ }
1147
+ }
1148
+
1149
+ if ( 'grouped' === $product_type ) {
1150
+
1151
+ update_post_meta( $product_id, '_manage_stock', 'no' );
1152
+ update_post_meta( $product_id, '_backorders', 'no' );
1153
+ update_post_meta( $product_id, '_stock', '' );
1154
+
1155
+ wc_update_product_stock_status( $product_id, $stock_status );
1156
+
1157
+ } elseif ( 'external' === $product_type ) {
1158
+
1159
+ update_post_meta( $product_id, '_manage_stock', 'no' );
1160
+ update_post_meta( $product_id, '_backorders', 'no' );
1161
+ update_post_meta( $product_id, '_stock', '' );
1162
+
1163
+ wc_update_product_stock_status( $product_id, 'instock' );
1164
+ } elseif ( 'yes' === $managing_stock ) {
1165
+ update_post_meta( $product_id, '_backorders', $backorders );
1166
+
1167
+ // Stock status is always determined by children so sync later.
1168
+ if ( 'variable' !== $product_type ) {
1169
+ wc_update_product_stock_status( $product_id, $stock_status );
1170
+ }
1171
+
1172
+ // Stock quantity.
1173
+ if ( isset( $data['stock_quantity'] ) ) {
1174
+ wc_update_product_stock( $product_id, wc_stock_amount( $data['stock_quantity'] ) );
1175
+ } elseif ( isset( $data['inventory_delta'] ) ) {
1176
+ $stock_quantity = wc_stock_amount( get_post_meta( $product_id, '_stock', true ) );
1177
+ $stock_quantity += wc_stock_amount( $data['inventory_delta'] );
1178
+
1179
+ wc_update_product_stock( $product_id, wc_stock_amount( $stock_quantity ) );
1180
+ }
1181
+ } else {
1182
+
1183
+ // Don't manage stock.
1184
+ update_post_meta( $product_id, '_manage_stock', 'no' );
1185
+ update_post_meta( $product_id, '_backorders', $backorders );
1186
+ update_post_meta( $product_id, '_stock', '' );
1187
+
1188
+ wc_update_product_stock_status( $product_id, $stock_status );
1189
+ }
1190
+ } elseif ( 'variable' !== $product_type ) {
1191
+ wc_update_product_stock_status( $product_id, $stock_status );
1192
+ }
1193
+
1194
+ // Upsells.
1195
+ if ( isset( $data['upsell_ids'] ) ) {
1196
+ $upsells = array();
1197
+ $ids = $data['upsell_ids'];
1198
+
1199
+ if ( ! empty( $ids ) ) {
1200
+ foreach ( $ids as $id ) {
1201
+ if ( $id && $id > 0 ) {
1202
+ $upsells[] = $id;
1203
+ }
1204
+ }
1205
+
1206
+ update_post_meta( $product_id, '_upsell_ids', $upsells );
1207
+ } else {
1208
+ delete_post_meta( $product_id, '_upsell_ids' );
1209
+ }
1210
+ }
1211
+
1212
+ // Cross sells.
1213
+ if ( isset( $data['cross_sell_ids'] ) ) {
1214
+ $crosssells = array();
1215
+ $ids = $data['cross_sell_ids'];
1216
+
1217
+ if ( ! empty( $ids ) ) {
1218
+ foreach ( $ids as $id ) {
1219
+ if ( $id && $id > 0 ) {
1220
+ $crosssells[] = $id;
1221
+ }
1222
+ }
1223
+
1224
+ update_post_meta( $product_id, '_crosssell_ids', $crosssells );
1225
+ } else {
1226
+ delete_post_meta( $product_id, '_crosssell_ids' );
1227
+ }
1228
+ }
1229
+
1230
+ // Product categories.
1231
+ if ( isset( $data['categories'] ) && is_array( $data['categories'] ) ) {
1232
+ $term_ids = array_unique( array_map( 'intval', $data['categories'] ) );
1233
+ wp_set_object_terms( $product_id, $term_ids, 'product_cat' );
1234
+ }
1235
+
1236
+ // Product tags.
1237
+ if ( isset( $data['tags'] ) && is_array( $data['tags'] ) ) {
1238
+ $term_ids = array_unique( array_map( 'intval', $data['tags'] ) );
1239
+ wp_set_object_terms( $product_id, $term_ids, 'product_tag' );
1240
+ }
1241
+
1242
+ // Downloadable.
1243
+ if ( isset( $data['downloadable'] ) ) {
1244
+ $is_downloadable = ( true === $data['downloadable'] ) ? 'yes' : 'no';
1245
+ update_post_meta( $product_id, '_downloadable', $is_downloadable );
1246
+ } else {
1247
+ $is_downloadable = get_post_meta( $product_id, '_downloadable', true );
1248
+ }
1249
+
1250
+ // Downloadable options.
1251
+ if ( 'yes' == $is_downloadable ) {
1252
+
1253
+ // Downloadable files.
1254
+ if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) {
1255
+ $this->save_downloadable_files( $product_id, $data['downloads'] );
1256
+ }
1257
+
1258
+ // Download limit.
1259
+ if ( isset( $data['download_limit'] ) ) {
1260
+ update_post_meta( $product_id, '_download_limit', ( '' === $data['download_limit'] ) ? '' : absint( $data['download_limit'] ) );
1261
+ }
1262
+
1263
+ // Download expiry.
1264
+ if ( isset( $data['download_expiry'] ) ) {
1265
+ update_post_meta( $product_id, '_download_expiry', ( '' === $data['download_expiry'] ) ? '' : absint( $data['download_expiry'] ) );
1266
+ }
1267
+
1268
+ // Download type.
1269
+ if ( isset( $data['download_type'] ) ) {
1270
+ update_post_meta( $product_id, '_download_type', wc_clean( $data['download_type'] ) );
1271
+ }
1272
+ }
1273
+
1274
+ // Product url.
1275
+ if ( 'external' === $product_type ) {
1276
+ if ( isset( $data['product_url'] ) ) {
1277
+ update_post_meta( $product_id, '_product_url', wc_clean( $data['product_url'] ) );
1278
+ }
1279
+
1280
+ if ( isset( $data['button_text'] ) ) {
1281
+ update_post_meta( $product_id, '_button_text', wc_clean( $data['button_text'] ) );
1282
+ }
1283
+ }
1284
+
1285
+ // Reviews allowed.
1286
+ if ( isset( $data['reviews_allowed'] ) ) {
1287
+ $reviews_allowed = ( true === $data['reviews_allowed'] ) ? 'open' : 'closed';
1288
+
1289
+ $wpdb->update( $wpdb->posts, array( 'comment_status' => $reviews_allowed ), array( 'ID' => $product_id ) );
1290
+ }
1291
+
1292
+ // Do action for product type
1293
+ do_action( 'woocommerce_api_process_product_meta_' . $product_type, $product_id, $data );
1294
+
1295
+ return true;
1296
+ }
1297
+
1298
+ /**
1299
+ * Save variations
1300
+ *
1301
+ * @since 2.2
1302
+ * @param int $id
1303
+ * @param array $data
1304
+ * @return bool
1305
+ * @throws WC_Square_API_Exception
1306
+ */
1307
+ protected function save_variations( $id, $data ) {
1308
+ global $wpdb;
1309
+
1310
+ $variations = $data['variations'];
1311
+ $attributes = (array) maybe_unserialize( get_post_meta( $id, '_product_attributes', true ) );
1312
+
1313
+ foreach ( $variations as $menu_order => $variation ) {
1314
+ $variation_id = isset( $variation['id'] ) ? absint( $variation['id'] ) : 0;
1315
+
1316
+ if ( ! $variation_id && isset( $variation['sku'] ) ) {
1317
+ $variation_sku = wc_clean( $variation['sku'] );
1318
+ $variation_id = wc_get_product_id_by_sku( $variation_sku );
1319
+ }
1320
+
1321
+ // Generate a useful post title
1322
+ $variation_post_title = sprintf( __( 'Variation #%1$s of %2$s', 'woocommerce-square' ), $variation_id, esc_html( get_the_title( $id ) ) );
1323
+
1324
+ // Update or Add post
1325
+ if ( ! $variation_id ) {
1326
+ $post_status = ( isset( $variation['visible'] ) && false === $variation['visible'] ) ? 'private' : 'publish';
1327
+
1328
+ $new_variation = array(
1329
+ 'post_title' => $variation_post_title,
1330
+ 'post_content' => '',
1331
+ 'post_status' => $post_status,
1332
+ 'post_author' => get_current_user_id(),
1333
+ 'post_parent' => $id,
1334
+ 'post_type' => 'product_variation',
1335
+ 'menu_order' => $menu_order,
1336
+ );
1337
+
1338
+ $variation_id = wp_insert_post( $new_variation );
1339
+
1340
+ do_action( 'woocommerce_create_product_variation', $variation_id );
1341
+ } else {
1342
+ $update_variation = array( 'post_title' => $variation_post_title, 'menu_order' => $menu_order );
1343
+ if ( isset( $variation['visible'] ) ) {
1344
+ $post_status = ( false === $variation['visible'] ) ? 'private' : 'publish';
1345
+ $update_variation['post_status'] = $post_status;
1346
+ }
1347
+
1348
+ $wpdb->update( $wpdb->posts, $update_variation, array( 'ID' => $variation_id ) );
1349
+
1350
+ do_action( 'woocommerce_update_product_variation', $variation_id );
1351
+ }
1352
+
1353
+ // Stop with we don't have a variation ID
1354
+ if ( is_wp_error( $variation_id ) ) {
1355
+ throw new WC_Square_API_Exception( 'woocommerce_api_cannot_save_product_variation', $variation_id->get_error_message(), 400 );
1356
+ }
1357
+
1358
+ // SKU
1359
+ if ( isset( $variation['sku'] ) ) {
1360
+ $sku = get_post_meta( $variation_id, '_sku', true );
1361
+ $new_sku = wc_clean( $variation['sku'] );
1362
+
1363
+ if ( '' == $new_sku ) {
1364
+ update_post_meta( $variation_id, '_sku', '' );
1365
+ } elseif ( $new_sku !== $sku ) {
1366
+ if ( ! empty( $new_sku ) ) {
1367
+ $unique_sku = wc_product_has_unique_sku( $variation_id, $new_sku );
1368
+ if ( ! $unique_sku ) {
1369
+ throw new WC_Square_API_Exception( 'woocommerce_api_product_sku_already_exists', __( 'The SKU already exists on another product', 'woocommerce-square' ), 400 );
1370
+ } else {
1371
+ update_post_meta( $variation_id, '_sku', $new_sku );
1372
+ }
1373
+ } else {
1374
+ update_post_meta( $variation_id, '_sku', '' );
1375
+ }
1376
+ }
1377
+ }
1378
+
1379
+ // Thumbnail.
1380
+ if ( isset( $variation['image'] ) && is_array( $variation['image'] ) ) {
1381
+ $image = current( $variation['image'] );
1382
+ if ( $image && is_array( $image ) ) {
1383
+ if ( isset( $image['position'] ) && 0 == $image['position'] ) {
1384
+ if ( isset( $image['src'] ) ) {
1385
+ $upload = $this->upload_product_image( wc_clean( $image['src'] ) );
1386
+
1387
+ if ( is_wp_error( $upload ) ) {
1388
+ throw new WC_Square_API_Exception( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), 400 );
1389
+ }
1390
+
1391
+ $attachment_id = $this->set_product_image_as_attachment( $upload, $id );
1392
+ } elseif ( isset( $image['id'] ) ) {
1393
+ $attachment_id = $image['id'];
1394
+ }
1395
+
1396
+ // Set the image alt if present.
1397
+ if ( ! empty( $image['alt'] ) ) {
1398
+ update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) );
1399
+ }
1400
+
1401
+ // Set the image title if present.
1402
+ if ( ! empty( $image['title'] ) ) {
1403
+ wp_update_post( array( 'ID' => $attachment_id, 'post_title' => $image['title'] ) );
1404
+ }
1405
+
1406
+ update_post_meta( $variation_id, '_thumbnail_id', $attachment_id );
1407
+ }
1408
+ } else {
1409
+ delete_post_meta( $variation_id, '_thumbnail_id' );
1410
+ }
1411
+ }
1412
+
1413
+ // Virtual variation
1414
+ if ( isset( $variation['virtual'] ) ) {
1415
+ $is_virtual = ( true === $variation['virtual'] ) ? 'yes' : 'no';
1416
+ update_post_meta( $variation_id, '_virtual', $is_virtual );
1417
+ }
1418
+
1419
+ // Downloadable variation
1420
+ if ( isset( $variation['downloadable'] ) ) {
1421
+ $is_downloadable = ( true === $variation['downloadable'] ) ? 'yes' : 'no';
1422
+ update_post_meta( $variation_id, '_downloadable', $is_downloadable );
1423
+ } else {
1424
+ $is_downloadable = get_post_meta( $variation_id, '_downloadable', true );
1425
+ }
1426
+
1427
+ // Shipping data
1428
+ $this->save_product_shipping_data( $variation_id, $variation );
1429
+
1430
+ // Stock handling
1431
+ if ( isset( $variation['managing_stock'] ) ) {
1432
+ $managing_stock = ( true === $variation['managing_stock'] ) ? 'yes' : 'no';
1433
+ } else {
1434
+ $managing_stock = get_post_meta( $variation_id, '_manage_stock', true );
1435
+ }
1436
+
1437
+ update_post_meta( $variation_id, '_manage_stock', '' === $managing_stock ? 'no' : $managing_stock );
1438
+
1439
+ if ( isset( $variation['in_stock'] ) ) {
1440
+ $stock_status = ( true === $variation['in_stock'] ) ? 'instock' : 'outofstock';
1441
+ } else {
1442
+ $stock_status = get_post_meta( $variation_id, '_stock_status', true );
1443
+ }
1444
+
1445
+ wc_update_product_stock_status( $variation_id, '' === $stock_status ? 'instock' : $stock_status );
1446
+
1447
+ if ( 'yes' === $managing_stock ) {
1448
+ $backorders = get_post_meta( $variation_id, '_backorders', true );
1449
+
1450
+ if ( isset( $variation['backorders'] ) ) {
1451
+ if ( 'notify' === $variation['backorders'] ) {
1452
+ $backorders = 'notify';
1453
+ } else {
1454
+ $backorders = ( true === $variation['backorders'] ) ? 'yes' : 'no';
1455
+ }
1456
+ }
1457
+
1458
+ update_post_meta( $variation_id, '_backorders', '' === $backorders ? 'no' : $backorders );
1459
+
1460
+ if ( isset( $variation['stock_quantity'] ) ) {
1461
+ wc_update_product_stock( $variation_id, wc_stock_amount( $variation['stock_quantity'] ) );
1462
+ } elseif ( isset( $data['inventory_delta'] ) ) {
1463
+ $stock_quantity = wc_stock_amount( get_post_meta( $variation_id, '_stock', true ) );
1464
+ $stock_quantity += wc_stock_amount( $data['inventory_delta'] );
1465
+
1466
+ wc_update_product_stock( $variation_id, wc_stock_amount( $stock_quantity ) );
1467
+ }
1468
+ } else {
1469
+ delete_post_meta( $variation_id, '_backorders' );
1470
+ delete_post_meta( $variation_id, '_stock' );
1471
+ }
1472
+
1473
+ $current_regular_price = get_post_meta( $variation_id, '_regular_price', true );
1474
+ $current_sale_price = get_post_meta( $variation_id, '_sale_price', true );
1475
+
1476
+ // Regular Price passed from Square ( They only have one price ).
1477
+ if ( isset( $variation['regular_price'] ) ) {
1478
+ // Check if current product is on sale.
1479
+ if ( $current_regular_price > $current_sale_price && $variation['regular_price'] < $current_regular_price ) {
1480
+ $regular_price = get_post_meta( $variation_id, '_regular_price', true );
1481
+ $sale_price = $variation['regular_price'];
1482
+ } else {
1483
+ $regular_price = ( '' === $variation['regular_price'] ) ? '' : $variation['regular_price'];
1484
+ $sale_price = '';
1485
+ }
1486
+ } else {
1487
+ $regular_price = get_post_meta( $variation_id, '_regular_price', true );
1488
+ $sale_price = get_post_meta( $variation_id, '_sale_price', true );
1489
+ }
1490
+
1491
+ if ( isset( $variation['sale_price_dates_from'] ) ) {
1492
+ $date_from = $variation['sale_price_dates_from'];
1493
+ } else {
1494
+ $date_from = get_post_meta( $variation_id, '_sale_price_dates_from', true );
1495
+ $date_from = ( '' === $date_from ) ? '' : date( 'Y-m-d', $date_from );
1496
+ }
1497
+
1498
+ if ( isset( $variation['sale_price_dates_to'] ) ) {
1499
+ $date_to = $variation['sale_price_dates_to'];
1500
+ } else {
1501
+ $date_to = get_post_meta( $variation_id, '_sale_price_dates_to', true );
1502
+ $date_to = ( '' === $date_to ) ? '' : date( 'Y-m-d', $date_to );
1503
+ }
1504
+
1505
+ $this->wc_save_product_price( $variation_id, $regular_price, $sale_price, $date_from, $date_to );
1506
+
1507
+ // Tax class
1508
+ if ( isset( $variation['tax_class'] ) ) {
1509
+ if ( 'parent' !== $variation['tax_class'] ) {
1510
+ update_post_meta( $variation_id, '_tax_class', wc_clean( $variation['tax_class'] ) );
1511
+ } else {
1512
+ delete_post_meta( $variation_id, '_tax_class' );
1513
+ }
1514
+ }
1515
+
1516
+ // Downloads
1517
+ if ( 'yes' == $is_downloadable ) {
1518
+ // Downloadable files
1519
+ if ( isset( $variation['downloads'] ) && is_array( $variation['downloads'] ) ) {
1520
+ $this->save_downloadable_files( $id, $variation['downloads'], $variation_id );
1521
+ }
1522
+
1523
+ // Download limit
1524
+ if ( isset( $variation['download_limit'] ) ) {
1525
+ $download_limit = absint( $variation['download_limit'] );
1526
+ update_post_meta( $variation_id, '_download_limit', ( ! $download_limit ) ? '' : $download_limit );
1527
+ }
1528
+
1529
+ // Download expiry
1530
+ if ( isset( $variation['download_expiry'] ) ) {
1531
+ $download_expiry = absint( $variation['download_expiry'] );
1532
+ update_post_meta( $variation_id, '_download_expiry', ( ! $download_expiry ) ? '' : $download_expiry );
1533
+ }
1534
+ } else {
1535
+ update_post_meta( $variation_id, '_download_limit', '' );
1536
+ update_post_meta( $variation_id, '_download_expiry', '' );
1537
+ update_post_meta( $variation_id, '_downloadable_files', '' );
1538
+ }
1539
+
1540
+ // Description.
1541
+ if ( isset( $variation['description'] ) ) {
1542
+ update_post_meta( $variation_id, '_variation_description', wp_kses_post( $variation['description'] ) );
1543
+ }
1544
+
1545
+ // Update taxonomies
1546
+ if ( isset( $variation['attributes'] ) ) {
1547
+ $updated_attribute_keys = array();
1548
+
1549
+ foreach ( $variation['attributes'] as $attribute_key => $attribute ) {
1550
+ if ( ! isset( $attribute['name'] ) ) {
1551
+ continue;
1552
+ }
1553
+
1554
+ $taxonomy = 0;
1555
+ $_attribute = array();
1556
+
1557
+ if ( isset( $attribute['slug'] ) ) {
1558
+ $taxonomy = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] );
1559
+ }
1560
+
1561
+ if ( ! $taxonomy ) {
1562
+ $taxonomy = sanitize_title( $attribute['name'] );
1563
+ }
1564
+
1565
+ if ( isset( $attributes[ $taxonomy ] ) ) {
1566
+ $_attribute = $attributes[ $taxonomy ];
1567
+ }
1568
+
1569
+ if ( isset( $_attribute['is_variation'] ) && $_attribute['is_variation'] ) {
1570
+ $_attribute_key = 'attribute_' . sanitize_title( $_attribute['name'] );
1571
+ $updated_attribute_keys[] = $_attribute_key;
1572
+
1573
+ if ( isset( $_attribute['is_taxonomy'] ) && $_attribute['is_taxonomy'] ) {
1574
+ // Don't use wc_clean as it destroys sanitized characters
1575
+ $_attribute_value = isset( $attribute['option'] ) ? sanitize_title( stripslashes( $attribute['option'] ) ) : '';
1576
+ } else {
1577
+ $_attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : '';
1578
+ }
1579
+
1580
+ update_post_meta( $variation_id, $_attribute_key, $_attribute_value );
1581
+ }
1582
+ }
1583
+
1584
+ // Remove old taxonomies attributes so data is kept up to date - first get attribute key names
1585
+ $delete_attribute_keys = $wpdb->get_col( $wpdb->prepare( "SELECT meta_key FROM {$wpdb->postmeta} WHERE meta_key LIKE 'attribute_%%' AND meta_key NOT IN ( '" . implode( "','", $updated_attribute_keys ) . "' ) AND post_id = %d;", $variation_id ) );
1586
+
1587
+ foreach ( $delete_attribute_keys as $key ) {
1588
+ delete_post_meta( $variation_id, $key );
1589
+ }
1590
+ }
1591
+
1592
+ do_action( 'woocommerce_api_save_product_variation', $variation_id, $menu_order, $variation );
1593
+ }
1594
+
1595
+ // Update parent if variable so price sorting works and stays in sync with the cheapest child
1596
+ WC_Product_Variable::sync( $id );
1597
+
1598
+ // Update default attributes options setting
1599
+ if ( isset( $data['default_attribute'] ) ) {
1600
+ $data['default_attributes'] = $data['default_attribute'];
1601
+ }
1602
+
1603
+ if ( isset( $data['default_attributes'] ) && is_array( $data['default_attributes'] ) ) {
1604
+ $default_attributes = array();
1605
+
1606
+ foreach ( $data['default_attributes'] as $default_attr_key => $default_attr ) {
1607
+ if ( ! isset( $default_attr['name'] ) ) {
1608
+ continue;
1609
+ }
1610
+
1611
+ $taxonomy = sanitize_title( $default_attr['name'] );
1612
+
1613
+ if ( isset( $default_attr['slug'] ) ) {
1614
+ $taxonomy = $this->get_attribute_taxonomy_by_slug( $default_attr['slug'] );
1615
+ }
1616
+
1617
+ if ( isset( $attributes[ $taxonomy ] ) ) {
1618
+ $_attribute = $attributes[ $taxonomy ];
1619
+
1620
+ if ( $_attribute['is_variation'] ) {
1621
+ $value = '';
1622
+
1623
+ if ( isset( $default_attr['option'] ) ) {
1624
+ if ( $_attribute['is_taxonomy'] ) {
1625
+ // Don't use wc_clean as it destroys sanitized characters
1626
+ $value = sanitize_title( trim( stripslashes( $default_attr['option'] ) ) );
1627
+ } else {
1628
+ $value = wc_clean( trim( stripslashes( $default_attr['option'] ) ) );
1629
+ }
1630
+ }
1631
+
1632
+ if ( $value ) {
1633
+ $default_attributes[ $taxonomy ] = $value;
1634
+ }
1635
+ }
1636
+ }
1637
+ }
1638
+
1639
+ update_post_meta( $id, '_default_attributes', $default_attributes );
1640
+ }
1641
+
1642
+ return true;
1643
+ }
1644
+
1645
+ /**
1646
+ * Save product shipping data
1647
+ *
1648
+ * @since 2.2
1649
+ * @param int $id
1650
+ * @param array $data
1651
+ */
1652
+ private function save_product_shipping_data( $id, $data ) {
1653
+ if ( isset( $data['weight'] ) ) {
1654
+ update_post_meta( $id, '_weight', ( '' === $data['weight'] ) ? '' : wc_format_decimal( $data['weight'] ) );
1655
+ }
1656
+
1657
+ // Product dimensions
1658
+ if ( isset( $data['dimensions'] ) ) {
1659
+ // Height
1660
+ if ( isset( $data['dimensions']['height'] ) ) {
1661
+ update_post_meta( $id, '_height', ( '' === $data['dimensions']['height'] ) ? '' : wc_format_decimal( $data['dimensions']['height'] ) );
1662
+ }
1663
+
1664
+ // Width
1665
+ if ( isset( $data['dimensions']['width'] ) ) {
1666
+ update_post_meta( $id, '_width', ( '' === $data['dimensions']['width'] ) ? '' : wc_format_decimal( $data['dimensions']['width'] ) );
1667
+ }
1668
+
1669
+ // Length
1670
+ if ( isset( $data['dimensions']['length'] ) ) {
1671
+ update_post_meta( $id, '_length', ( '' === $data['dimensions']['length'] ) ? '' : wc_format_decimal( $data['dimensions']['length'] ) );
1672
+ }
1673
+ }
1674
+
1675
+ // Virtual
1676
+ if ( isset( $data['virtual'] ) ) {
1677
+ $virtual = ( true === $data['virtual'] ) ? 'yes' : 'no';
1678
+
1679
+ if ( 'yes' == $virtual ) {
1680
+ update_post_meta( $id, '_weight', '' );
1681
+ update_post_meta( $id, '_length', '' );
1682
+ update_post_meta( $id, '_width', '' );
1683
+ update_post_meta( $id, '_height', '' );
1684
+ }
1685
+ }
1686
+
1687
+ // Shipping class
1688
+ if ( isset( $data['shipping_class'] ) ) {
1689
+ wp_set_object_terms( $id, wc_clean( $data['shipping_class'] ), 'product_shipping_class' );
1690
+ }
1691
+ }
1692
+
1693
+ /**
1694
+ * Save downloadable files
1695
+ *
1696
+ * @since 2.2
1697
+ * @param int $product_id
1698
+ * @param array $downloads
1699
+ * @param int $variation_id
1700
+ */
1701
+ private function save_downloadable_files( $product_id, $downloads, $variation_id = 0 ) {
1702
+ $files = array();
1703
+
1704
+ // File paths will be stored in an array keyed off md5(file path)
1705
+ foreach ( $downloads as $key => $file ) {
1706
+ if ( isset( $file['url'] ) ) {
1707
+ $file['file'] = $file['url'];
1708
+ }
1709
+
1710
+ if ( ! isset( $file['file'] ) ) {
1711
+ continue;
1712
+ }
1713
+
1714
+ $file_name = isset( $file['name'] ) ? wc_clean( $file['name'] ) : '';
1715
+
1716
+ if ( 0 === strpos( $file['file'], 'http' ) ) {
1717
+ $file_url = esc_url_raw( $file['file'] );
1718
+ } else {
1719
+ $file_url = wc_clean( $file['file'] );
1720
+ }
1721
+
1722
+ $files[ md5( $file_url ) ] = array(
1723
+ 'name' => $file_name,
1724
+ 'file' => $file_url,
1725
+ );
1726
+ }
1727
+
1728
+ // Grant permission to any newly added files on any existing orders for this product prior to saving
1729
+ do_action( 'woocommerce_process_product_file_download_paths', $product_id, $variation_id, $files );
1730
+
1731
+ $id = ( 0 === $variation_id ) ? $product_id : $variation_id;
1732
+ update_post_meta( $id, '_downloadable_files', $files );
1733
+ }
1734
+
1735
+ /**
1736
+ * Get attribute taxonomy by slug.
1737
+ *
1738
+ * @since 2.2
1739
+ * @param string $slug
1740
+ * @return string|null
1741
+ */
1742
+ private function get_attribute_taxonomy_by_slug( $slug ) {
1743
+ $taxonomy = null;
1744
+ $attribute_taxonomies = wc_get_attribute_taxonomies();
1745
+
1746
+ foreach ( $attribute_taxonomies as $key => $tax ) {
1747
+ if ( $slug == $tax->attribute_name ) {
1748
+ $taxonomy = 'pa_' . $tax->attribute_name;
1749
+
1750
+ break;
1751
+ }
1752
+ }
1753
+
1754
+ return $taxonomy;
1755
+ }
1756
+
1757
+ /**
1758
+ * Get the images for a product or product variation
1759
+ *
1760
+ * @since 2.1
1761
+ * @param WC_Product|WC_Product_Variation $product
1762
+ * @return array
1763
+ */
1764
+ private function get_images( $product ) {
1765
+
1766
+ $images = $attachment_ids = array();
1767
+
1768
+ if ( $product->is_type( 'variation' ) ) {
1769
+
1770
+ if ( has_post_thumbnail( version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->get_variation_id() : $product->get_id() ) ) {
1771
+
1772
+ // Add variation image if set
1773
+ $attachment_ids[] = get_post_thumbnail_id( version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->get_variation_id() : $product->get_id() );
1774
+
1775
+ } elseif ( has_post_thumbnail( version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->id : $product->get_id() ) ) {
1776
+
1777
+ // Otherwise use the parent product featured image if set
1778
+ $attachment_ids[] = get_post_thumbnail_id( version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->id : $product->get_id() );
1779
+ }
1780
+ } else {
1781
+
1782
+ // Add featured image
1783
+ if ( has_post_thumbnail( version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->id : $product->get_id() ) ) {
1784
+ $attachment_ids[] = get_post_thumbnail_id( version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->id : $product->get_id() );
1785
+ }
1786
+
1787
+ // Add gallery images
1788
+ $attachment_ids = array_merge( $attachment_ids, ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->get_gallery_attachment_ids() : $product->get_gallery_image_ids() ) );
1789
+ }
1790
+
1791
+ // Build image data
1792
+ foreach ( $attachment_ids as $position => $attachment_id ) {
1793
+
1794
+ $attachment_post = get_post( $attachment_id );
1795
+
1796
+ if ( is_null( $attachment_post ) ) {
1797
+ continue;
1798
+ }
1799
+
1800
+ $attachment = wp_get_attachment_image_src( $attachment_id, 'full' );
1801
+
1802
+ if ( ! is_array( $attachment ) ) {
1803
+ continue;
1804
+ }
1805
+
1806
+ $images[] = array(
1807
+ 'id' => (int) $attachment_id,
1808
+ 'created_at' => $this->format_datetime( $attachment_post->post_date_gmt ),
1809
+ 'updated_at' => $this->format_datetime( $attachment_post->post_modified_gmt ),
1810
+ 'src' => current( $attachment ),
1811
+ 'title' => get_the_title( $attachment_id ),
1812
+ 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ),
1813
+ 'position' => (int) $position,
1814
+ );
1815
+ }
1816
+
1817
+ // Set a placeholder image if the product has no images set
1818
+ if ( empty( $images ) ) {
1819
+
1820
+ $images[] = array(
1821
+ 'id' => 0,
1822
+ 'created_at' => $this->format_datetime( time() ), // Default to now
1823
+ 'updated_at' => $this->format_datetime( time() ),
1824
+ 'src' => wc_placeholder_img_src(),
1825
+ 'title' => __( 'Placeholder', 'woocommerce-square' ),
1826
+ 'alt' => __( 'Placeholder', 'woocommerce-square' ),
1827
+ 'position' => 0,
1828
+ );
1829
+ }
1830
+
1831
+ return $images;
1832
+ }
1833
+
1834
+ /**
1835
+ * Save product images.
1836
+ *
1837
+ * @since 2.2
1838
+ * @param array $images
1839
+ * @param int $id
1840
+ * @throws WC_Square_API_Exception
1841
+ */
1842
+ protected function save_product_images( $id, $images ) {
1843
+ if ( is_array( $images ) ) {
1844
+ $gallery = array();
1845
+
1846
+ $product_image = get_the_post_thumbnail_url( $id, 'full' );
1847
+
1848
+ foreach ( $images as $image ) {
1849
+ /**
1850
+ * Because Square saves all images as original.jpeg when passed back,
1851
+ * we can't really check against filename. So we have no choice here but
1852
+ * to not sync the image if one already exists to prevent WP from creating
1853
+ * duplicate images all over the place. See https://github.com/woocommerce/woocommerce-square/issues/131
1854
+ */
1855
+ if ( $product_image ) {
1856
+ continue;
1857
+ }
1858
+
1859
+ if ( isset( $image['position'] ) && 0 == $image['position'] ) {
1860
+ $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0;
1861
+
1862
+ if ( 0 === $attachment_id && isset( $image['src'] ) ) {
1863
+ $upload = $this->upload_product_image( esc_url_raw( $image['src'] ) );
1864
+
1865
+ if ( is_wp_error( $upload ) ) {
1866
+ throw new WC_Square_API_Exception( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), 400 );
1867
+ }
1868
+
1869
+ $attachment_id = $this->set_product_image_as_attachment( $upload, $id );
1870
+ }
1871
+
1872
+ set_post_thumbnail( $id, $attachment_id );
1873
+ } else {
1874
+ $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0;
1875
+
1876
+ if ( 0 === $attachment_id && isset( $image['src'] ) ) {
1877
+ $upload = $this->upload_product_image( esc_url_raw( $image['src'] ) );
1878
+
1879
+ if ( is_wp_error( $upload ) ) {
1880
+ throw new WC_Square_API_Exception( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), 400 );
1881
+ }
1882
+
1883
+ $attachment_id = $this->set_product_image_as_attachment( $upload, $id );
1884
+ }
1885
+
1886
+ $gallery[] = $attachment_id;
1887
+ }
1888
+
1889
+ // Set the image alt if present.
1890
+ if ( ! empty( $image['alt'] ) && $attachment_id ) {
1891
+ update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) );
1892
+ }
1893
+
1894
+ // Set the image title if present.
1895
+ if ( ! empty( $image['title'] ) && $attachment_id ) {
1896
+ wp_update_post( array( 'ID' => $attachment_id, 'post_title' => $image['title'] ) );
1897
+ }
1898
+ }
1899
+
1900
+ if ( ! empty( $gallery ) ) {
1901
+ update_post_meta( $id, '_product_image_gallery', implode( ',', $gallery ) );
1902
+ }
1903
+ } else {
1904
+ delete_post_thumbnail( $id );
1905
+ update_post_meta( $id, '_product_image_gallery', '' );
1906
+ }
1907
+ }
1908
+
1909
+ /**
1910
+ * Upload image from URL
1911
+ *
1912
+ * @since 2.2
1913
+ * @param string $image_url
1914
+ * @return int|WP_Error attachment id
1915
+ */
1916
+ public function upload_product_image( $image_url ) {
1917
+ return $this->upload_image_from_url( $image_url, 'product_image' );
1918
+ }
1919
+
1920
+ /**
1921
+ * Upload product category image from URL.
1922
+ *
1923
+ * @since 2.5.0
1924
+ * @param string $image_url
1925
+ * @return int|WP_Error attachment id
1926
+ */
1927
+ public function upload_product_category_image( $image_url ) {
1928
+ return $this->upload_image_from_url( $image_url, 'product_category_image' );
1929
+ }
1930
+
1931
+ /**
1932
+ * Upload image from URL.
1933
+ *
1934
+ * @throws WC_Square_API_Exception
1935
+ *
1936
+ * @since 2.5.0
1937
+ * @param string $image_url
1938
+ * @param string $upload_for
1939
+ * @return int|WP_Error Attachment id
1940
+ */
1941
+ protected function upload_image_from_url( $image_url, $upload_for = 'product_image' ) {
1942
+ $file_name = basename( current( explode( '?', $image_url ) ) );
1943
+ $parsed_url = @parse_url( $image_url );
1944
+
1945
+ // Check parsed URL.
1946
+ if ( ! $parsed_url || ! is_array( $parsed_url ) ) {
1947
+ throw new WC_Square_API_Exception( 'woocommerce_api_invalid_' . $upload_for, sprintf( __( 'Invalid URL %s', 'woocommerce-square' ), $image_url ), 400 );
1948
+ }
1949
+
1950
+ // Ensure url is valid.
1951
+ $image_url = str_replace( ' ', '%20', $image_url );
1952
+
1953
+ // Get the file.
1954
+ $response = wp_safe_remote_get( $image_url, array(
1955
+ 'timeout' => 45,
1956
+ ) );
1957
+
1958
+ if ( is_wp_error( $response ) ) {
1959
+ throw new WC_Square_API_Exception( 'woocommerce_api_invalid_remote_' . $upload_for, sprintf( __( 'Error getting remote image %s.', 'woocommerce-square' ), $image_url ) . ' ' . sprintf( __( 'Error: %s.', 'woocommerce-square' ), $response->get_error_message() ), 400 );
1960
+ } elseif ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
1961
+ throw new WC_Square_API_Exception( 'woocommerce_api_invalid_remote_' . $upload_for, sprintf( __( 'Error getting remote image %s.', 'woocommerce-square' ), $image_url ), 400 );
1962
+ }
1963
+
1964
+ // Ensure we have a file name and type.
1965
+ $wp_filetype = wp_check_filetype( $file_name, wc_rest_allowed_image_mime_types() );
1966
+
1967
+ if ( ! $wp_filetype['type'] ) {
1968
+ $headers = wp_remote_retrieve_headers( $response );
1969
+ if ( isset( $headers['content-disposition'] ) && strstr( $headers['content-disposition'], 'filename=' ) ) {
1970
+ $disposition = end( explode( 'filename=', $headers['content-disposition'] ) );
1971
+ $disposition = sanitize_file_name( $disposition );
1972
+ $file_name = $disposition;
1973
+ } elseif ( isset( $headers['content-type'] ) && strstr( $headers['content-type'], 'image/' ) ) {
1974
+ $file_name = 'image.' . str_replace( 'image/', '', $headers['content-type'] );
1975
+ }
1976
+ unset( $headers );
1977
+
1978
+ // Recheck filetype
1979
+ $wp_filetype = wp_check_filetype( $file_name, wc_rest_allowed_image_mime_types() );
1980
+
1981
+ if ( ! $wp_filetype['type'] ) {
1982
+ throw new WC_Square_API_Exception( 'woocommerce_api_invalid_' . $upload_for, __( 'Invalid image type.', 'woocommerce-square' ), 400 );
1983
+ }
1984
+ }
1985
+
1986
+ // Upload the file.
1987
+ $upload = wp_upload_bits( $file_name, '', wp_remote_retrieve_body( $response ) );
1988
+
1989
+ if ( $upload['error'] ) {
1990
+ throw new WC_Square_API_Exception( 'woocommerce_api_' . $upload_for . '_upload_error', $upload['error'], 400 );
1991
+ }
1992
+
1993
+ // Get filesize.
1994
+ $filesize = filesize( $upload['file'] );
1995
+
1996
+ if ( 0 == $filesize ) {
1997
+ @unlink( $upload['file'] );
1998
+ unset( $upload );
1999
+ throw new WC_Square_API_Exception( 'woocommerce_api_' . $upload_for . '_upload_file_error', __( 'Zero size file downloaded', 'woocommerce-square' ), 400 );
2000
+ }
2001
+
2002
+ unset( $response );
2003
+
2004
+ do_action( 'woocommerce_api_uploaded_image_from_url', $upload, $image_url, $upload_for );
2005
+
2006
+ return $upload;
2007
+ }
2008
+
2009
+ /**
2010
+ * Sets product image as attachment and returns the attachment ID.
2011
+ *
2012
+ * @since 2.2
2013
+ * @param array $upload
2014
+ * @param int $id
2015
+ * @return int
2016
+ */
2017
+ protected function set_product_image_as_attachment( $upload, $id ) {
2018
+ return $this->set_uploaded_image_as_attachment( $upload, $id );
2019
+ }
2020
+
2021
+ /**
2022
+ * Sets uploaded category image as attachment and returns the attachment ID.
2023
+ *
2024
+ * @since 2.5.0
2025
+ * @param integer $upload Upload information from wp_upload_bits
2026
+ * @return int Attachment ID
2027
+ */
2028
+ protected function set_product_category_image_as_attachment( $upload ) {
2029
+ return $this->set_uploaded_image_as_attachment( $upload );
2030
+ }
2031
+
2032
+ /**
2033
+ * Set uploaded image as attachment.
2034
+ *
2035
+ * @since 2.5.0
2036
+ * @param array $upload Upload information from wp_upload_bits
2037
+ * @param int $id Post ID. Default to 0.
2038
+ * @return int Attachment ID
2039
+ */
2040
+ protected function set_uploaded_image_as_attachment( $upload, $id = 0 ) {
2041
+ $info = wp_check_filetype( $upload['file'] );
2042
+ $title = '';
2043
+ $content = '';
2044
+
2045
+ if ( $image_meta = @wp_read_image_metadata( $upload['file'] ) ) {
2046
+ if ( trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) {
2047
+ $title = wc_clean( $image_meta['title'] );
2048
+ }
2049
+ if ( trim( $image_meta['caption'] ) ) {
2050
+ $content = wc_clean( $image_meta['caption'] );
2051
+ }
2052
+ }
2053
+
2054
+ $attachment = array(
2055
+ 'post_mime_type' => $info['type'],
2056
+ 'guid' => $upload['url'],
2057
+ 'post_parent' => $id,
2058
+ 'post_title' => $title,
2059
+ 'post_content' => $content,
2060
+ );
2061
+
2062
+ $attachment_id = wp_insert_attachment( $attachment, $upload['file'], $id );
2063
+ if ( ! is_wp_error( $attachment_id ) ) {
2064
+ wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $upload['file'] ) );
2065
+ }
2066
+
2067
+ return $attachment_id;
2068
+ }
2069
+
2070
+ /**
2071
+ * Get attribute options.
2072
+ *
2073
+ * @param int $product_id
2074
+ * @param array $attribute
2075
+ * @return array
2076
+ */
2077
+ protected function get_attribute_options( $product_id, $attribute ) {
2078
+ if ( isset( $attribute['is_taxonomy'] ) && $attribute['is_taxonomy'] ) {
2079
+ return wc_get_product_terms( $product_id, $attribute['name'], array( 'fields' => 'names' ) );
2080
+ } elseif ( isset( $attribute['value'] ) ) {
2081
+ return array_map( 'trim', explode( '|', $attribute['value'] ) );
2082
+ }
2083
+
2084
+ return array();
2085
+ }
2086
+
2087
+ /**
2088
+ * Get the attributes for a product or product variation
2089
+ *
2090
+ * @since 2.1
2091
+ * @param WC_Product|WC_Product_Variation $product
2092
+ * @return array
2093
+ */
2094
+ private function get_attributes( $product ) {
2095
+
2096
+ $attributes = array();
2097
+
2098
+ if ( $product->is_type( 'variation' ) ) {
2099
+
2100
+ // variation attributes
2101
+ foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) {
2102
+
2103
+ // taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_`
2104
+ $attributes[] = array(
2105
+ 'name' => wc_attribute_label( str_replace( 'attribute_', '', $attribute_name ), $product ),
2106
+ 'slug' => str_replace( 'attribute_', '', str_replace( 'pa_', '', $attribute_name ) ),
2107
+ 'option' => $attribute,
2108
+ );
2109
+ }
2110
+ } else {
2111
+ foreach ( $product->get_attributes() as $attribute ) {
2112
+ $attributes[] = array(
2113
+ 'name' => wc_attribute_label( $attribute['name'], $product ),
2114
+ 'slug' => str_replace( 'pa_', '', $attribute['name'] ),
2115
+ 'position' => (int) $attribute['position'],
2116
+ 'visible' => (bool) $attribute['is_visible'],
2117
+ 'variation' => (bool) $attribute['is_variation'],
2118
+ 'options' => $this->get_attribute_options( version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->id : $product->get_id(), $attribute ),
2119
+ );
2120
+ }
2121
+ }
2122
+
2123
+ return $attributes;
2124
+ }
2125
+
2126
+ /**
2127
+ * Get the downloads for a product or product variation
2128
+ *
2129
+ * @since 2.1
2130
+ * @param WC_Product|WC_Product_Variation $product
2131
+ * @return array
2132
+ */
2133
+ private function get_downloads( $product ) {
2134
+
2135
+ $downloads = array();
2136
+
2137
+ if ( $product->is_downloadable() ) {
2138
+
2139
+ $files = version_compare( WC_VERSION, '3.0', '<' ) ? $product->get_files() : $product->get_downloads();
2140
+
2141
+ foreach ( $files as $file_id => $file ) {
2142
+
2143
+ $downloads[] = array(
2144
+ 'id' => $file_id, // do not cast as int as this is a hash
2145
+ 'name' => $file['name'],
2146
+ 'file' => $file['file'],
2147
+ );
2148
+ }
2149
+ }
2150
+
2151
+ return $downloads;
2152
+ }
2153
+
2154
+ /**
2155
+ * Get a listing of product attributes
2156
+ *
2157
+ * @since 2.5.0
2158
+ * @param string|null $fields fields to limit response to
2159
+ * @return array
2160
+ */
2161
+ public function get_product_attributes( $fields = null ) {
2162
+ try {
2163
+ // Permissions check.
2164
+ if ( ! current_user_can( 'manage_product_terms' ) ) {
2165
+ throw new WC_Square_API_Exception( 'woocommerce_api_user_cannot_read_product_attributes', __( 'You do not have permission to read product attributes', 'woocommerce-square' ), 401 );
2166
+ }
2167
+
2168
+ $product_attributes = array();
2169
+ $attribute_taxonomies = wc_get_attribute_taxonomies();
2170
+
2171
+ foreach ( $attribute_taxonomies as $attribute ) {
2172
+ $product_attributes[] = array(
2173
+ 'id' => intval( $attribute->attribute_id ),
2174
+ 'name' => $attribute->attribute_label,
2175
+ 'slug' => wc_attribute_taxonomy_name( $attribute->attribute_name ),
2176
+ 'type' => $attribute->attribute_type,
2177
+ 'order_by' => $attribute->attribute_orderby,
2178
+ 'has_archives' => (bool) $attribute->attribute_public,
2179
+ );
2180
+ }
2181
+
2182
+ return array( 'product_attributes' => apply_filters( 'woocommerce_api_product_attributes_response', $product_attributes, $attribute_taxonomies, $fields, $this ) );
2183
+ } catch ( WC_Square_API_Exception $e ) {
2184
+ return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
2185
+ }
2186
+ }
2187
+
2188
+ /**
2189
+ * Get the product attribute for the given ID
2190
+ *
2191
+ * @since 2.5.0
2192
+ * @param string $id product attribute term ID
2193
+ * @param string|null $fields fields to limit response to
2194
+ * @return array
2195
+ */
2196
+ public function get_product_attribute( $id, $fields = null ) {
2197
+ global $wpdb;
2198
+
2199
+ try {
2200
+ $id = absint( $id );
2201
+
2202
+ // Validate ID
2203
+ if ( empty( $id ) ) {
2204
+ throw new WC_Square_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'Invalid product attribute ID', 'woocommerce-square' ), 400 );
2205
+ }
2206
+
2207
+ // Permissions check
2208
+ if ( ! current_user_can( 'manage_product_terms' ) ) {
2209
+ throw new WC_Square_API_Exception( 'woocommerce_api_user_cannot_read_product_attributes', __( 'You do not have permission to read product attributes', 'woocommerce-square' ), 401 );
2210
+ }
2211
+
2212
+ $attribute = $wpdb->get_row( $wpdb->prepare( "
2213
+ SELECT *
2214
+ FROM {$wpdb->prefix}woocommerce_attribute_taxonomies
2215
+ WHERE attribute_id = %d
2216
+ ", $id ) );
2217
+
2218
+ if ( is_wp_error( $attribute ) || is_null( $attribute ) ) {
2219
+ throw new WC_Square_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce-square' ), 404 );
2220
+ }
2221
+
2222
+ $product_attribute = array(
2223
+ 'id' => intval( $attribute->attribute_id ),
2224
+ 'name' => $attribute->attribute_label,
2225
+ 'slug' => wc_attribute_taxonomy_name( $attribute->attribute_name ),
2226
+ 'type' => $attribute->attribute_type,
2227
+ 'order_by' => $attribute->attribute_orderby,
2228
+ 'has_archives' => (bool) $attribute->attribute_public,
2229
+ );
2230
+
2231
+ return array( 'product_attribute' => apply_filters( 'woocommerce_api_product_attribute_response', $product_attribute, $id, $fields, $attribute, $this ) );
2232
+ } catch ( WC_Square_API_Exception $e ) {
2233
+ return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
2234
+ }
2235
+ }
2236
+
2237
+ /**
2238
+ * Validate attribute data.
2239
+ *
2240
+ * @since 2.5.0
2241
+ * @param string $name
2242
+ * @param string $slug
2243
+ * @param string $type
2244
+ * @param string $order_by
2245
+ * @param bool $new_data
2246
+ * @return bool
2247
+ * @throws WC_Square_API_Exception
2248
+ */
2249
+ protected function validate_attribute_data( $name, $slug, $type, $order_by, $new_data = true ) {
2250
+ if ( empty( $name ) ) {
2251
+ throw new WC_Square_API_Exception( 'woocommerce_api_missing_product_attribute_name', sprintf( __( 'Missing parameter %s', 'woocommerce-square' ), 'name' ), 400 );
2252
+ }
2253
+
2254
+ if ( strlen( $slug ) >= 28 ) {
2255
+ throw new WC_Square_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_too_long', sprintf( __( 'Slug "%s" is too long (28 characters max). Shorten it, please.', 'woocommerce-square' ), $slug ), 400 );
2256
+ } elseif ( wc_check_if_attribute_name_is_reserved( $slug ) ) {
2257
+ throw new WC_Square_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_reserved_name', sprintf( __( 'Slug "%s" is not allowed because it is a reserved term. Change it, please.', 'woocommerce-square' ), $slug ), 400 );
2258
+ } elseif ( $new_data && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) {
2259
+ throw new WC_Square_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_already_exists', sprintf( __( 'Slug "%s" is already in use. Change it, please.', 'woocommerce-square' ), $slug ), 400 );
2260
+ }
2261
+
2262
+ // Validate the attribute type
2263
+ if ( ! in_array( wc_clean( $type ), array_keys( wc_get_attribute_types() ) ) ) {
2264
+ throw new WC_Square_API_Exception( 'woocommerce_api_invalid_product_attribute_type', sprintf( __( 'Invalid product attribute type - the product attribute type must be any of these: %s', 'woocommerce-square' ), implode( ', ', array_keys( wc_get_attribute_types() ) ) ), 400 );
2265
+ }
2266
+
2267
+ // Validate the attribute order by
2268
+ if ( ! in_array( wc_clean( $order_by ), array( 'menu_order', 'name', 'name_num', 'id' ) ) ) {
2269
+ throw new WC_Square_API_Exception( 'woocommerce_api_invalid_product_attribute_order_by', sprintf( __( 'Invalid product attribute order_by type - the product attribute order_by type must be any of these: %s', 'woocommerce-square' ), implode( ', ', array( 'menu_order', 'name', 'name_num', 'id' ) ) ), 400 );
2270
+ }
2271
+
2272
+ return true;
2273
+ }
2274
+
2275
+ /**
2276
+ * Create a new product attribute.
2277
+ *
2278
+ * @since 2.5.0
2279
+ * @param array $data Posted data.
2280
+ * @return array
2281
+ */
2282
+ public function create_product_attribute( $data ) {
2283
+ global $wpdb;
2284
+
2285
+ try {
2286
+ if ( ! isset( $data['product_attribute'] ) ) {
2287
+ throw new WC_Square_API_Exception( 'woocommerce_api_missing_product_attribute_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce-square' ), 'product_attribute' ), 400 );
2288
+ }
2289
+
2290
+ $data = $data['product_attribute'];
2291
+
2292
+ // Check permissions.
2293
+ if ( ! current_user_can( 'manage_product_terms' ) ) {
2294
+ throw new WC_Square_API_Exception( 'woocommerce_api_user_cannot_create_product_attribute', __( 'You do not have permission to create product attributes', 'woocommerce-square' ), 401 );
2295
+ }
2296
+
2297
+ $data = apply_filters( 'woocommerce_api_create_product_attribute_data', $data, $this );
2298
+
2299
+ if ( ! isset( $data['name'] ) ) {
2300
+ $data['name'] = '';
2301
+ }
2302
+
2303
+ // Set the attribute slug.
2304
+ if ( ! isset( $data['slug'] ) ) {
2305
+ $data['slug'] = wc_sanitize_taxonomy_name( stripslashes( $data['name'] ) );
2306
+ } else {
2307
+ $data['slug'] = preg_replace( '/^pa\_/', '', wc_sanitize_taxonomy_name( stripslashes( $data['slug'] ) ) );
2308
+ }
2309
+
2310
+ // Set attribute type when not sent.
2311
+ if ( ! isset( $data['type'] ) ) {
2312
+ $data['type'] = 'select';
2313
+ }
2314
+
2315
+ // Set order by when not sent.
2316
+ if ( ! isset( $data['order_by'] ) ) {
2317
+ $data['order_by'] = 'menu_order';
2318
+ }
2319
+
2320
+ // Validate the attribute data.
2321
+ $this->validate_attribute_data( $data['name'], $data['slug'], $data['type'], $data['order_by'], true );
2322
+
2323
+ $insert = $wpdb->insert(
2324
+ $wpdb->prefix . 'woocommerce_attribute_taxonomies',
2325
+ array(
2326
+ 'attribute_label' => $data['name'],
2327
+ 'attribute_name' => $data['slug'],
2328
+ 'attribute_type' => $data['type'],
2329
+ 'attribute_orderby' => $data['order_by'],
2330
+ 'attribute_public' => isset( $data['has_archives'] ) && true === $data['has_archives'] ? 1 : 0,
2331
+ ),
2332
+ array( '%s', '%s', '%s', '%s', '%d' )
2333
+ );
2334
+
2335
+ // Checks for an error in the product creation.
2336
+ if ( is_wp_error( $insert ) ) {
2337
+ throw new WC_Square_API_Exception( 'woocommerce_api_cannot_create_product_attribute', $insert->get_error_message(), 400 );
2338
+ }
2339
+
2340
+ $id = $wpdb->insert_id;
2341
+
2342
+ do_action( 'woocommerce_api_create_product_attribute', $id, $data );
2343
+
2344
+ // Clear transients.
2345
+ flush_rewrite_rules();
2346
+ delete_transient( 'wc_attribute_taxonomies' );
2347
+
2348
+ return $this->get_product_attribute( $id );
2349
+ } catch ( WC_Square_API_Exception $e ) {
2350
+ return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
2351
+ }
2352
+ }
2353
+
2354
+ /**
2355
+ * Clear product
2356
+ */
2357
+ protected function clear_product( $product_id ) {
2358
+ if ( ! is_numeric( $product_id ) || 0 >= $product_id ) {
2359
+ return;
2360
+ }
2361
+
2362
+ // Delete product attachments
2363
+ $attachments = get_children( array(
2364
+ 'post_parent' => $product_id,
2365
+ 'post_status' => 'any',
2366
+ 'post_type' => 'attachment',
2367
+ ) );
2368
+
2369
+ foreach ( (array) $attachments as $attachment ) {
2370
+ wp_delete_attachment( $attachment->ID, true );
2371
+ }
2372
+
2373
+ // Delete product
2374
+ wp_delete_post( $product_id, true );
2375
+ }
2376
+
2377
+ /**
2378
+ * Format a unix timestamp or MySQL datetime into an RFC3339 datetime
2379
+ *
2380
+ * @since 2.1
2381
+ * @param int|string $timestamp unix timestamp or MySQL datetime
2382
+ * @param bool $convert_to_utc
2383
+ * @return string RFC3339 datetime
2384
+ */
2385
+ public function format_datetime( $timestamp, $convert_to_utc = false ) {
2386
+
2387
+ if ( $convert_to_utc ) {
2388
+ $timezone = new DateTimeZone( wc_timezone_string() );
2389
+ } else {
2390
+ $timezone = new DateTimeZone( 'UTC' );
2391
+ }
2392
+
2393
+ try {
2394
+
2395
+ if ( is_numeric( $timestamp ) ) {
2396
+ $date = new DateTime( "@{$timestamp}" );
2397
+ } else {
2398
+ $date = new DateTime( $timestamp, $timezone );
2399
+ }
2400
+
2401
+ // convert to UTC by adjusting the time based on the offset of the site's timezone
2402
+ if ( $convert_to_utc ) {
2403
+ $date->modify( -1 * $date->getOffset() . ' seconds' );
2404
+ }
2405
+ } catch ( Exception $e ) {
2406
+
2407
+ $date = new DateTime( '@0' );
2408
+ }
2409
+
2410
+ return $date->format( 'Y-m-d\TH:i:s\Z' );
2411
+ }
2412
+
2413
+ /**
2414
+ * Save product price.
2415
+ *
2416
+ * This is a private function (internal use ONLY) used until a data manipulation api is built.
2417
+ *
2418
+ * @deprecated 2.7.0
2419
+ * @param int $product_id
2420
+ * @param float $regular_price
2421
+ * @param float $sale_price
2422
+ * @param string $date_from
2423
+ * @param string $date_to
2424
+ */
2425
+ public function wc_save_product_price( $product_id, $regular_price, $sale_price = '', $date_from = '', $date_to = '' ) {
2426
+ $product_id = absint( $product_id );
2427
+ $regular_price = wc_format_decimal( $regular_price );
2428
+ $sale_price = '' === $sale_price ? '' : wc_format_decimal( $sale_price );
2429
+ $date_from = wc_clean( $date_from );
2430
+ $date_to = wc_clean( $date_to );
2431
+
2432
+ update_post_meta( $product_id, '_regular_price', $regular_price );
2433
+ update_post_meta( $product_id, '_sale_price', $sale_price );
2434
+
2435
+ // Save Dates
2436
+ update_post_meta( $product_id, '_sale_price_dates_from', $date_from ? strtotime( $date_from ) : '' );
2437
+ update_post_meta( $product_id, '_sale_price_dates_to', $date_to ? strtotime( $date_to ) : '' );
2438
+
2439
+ if ( $date_to && ! $date_from ) {
2440
+ $date_from = strtotime( 'NOW', current_time( 'timestamp' ) );
2441
+ update_post_meta( $product_id, '_sale_price_dates_from', $date_from );
2442
+ }
2443
+
2444
+ // Update price if on sale
2445
+ if ( '' !== $sale_price && '' === $date_to && '' === $date_from ) {
2446
+ update_post_meta( $product_id, '_price', $sale_price );
2447
+ } else {
2448
+ update_post_meta( $product_id, '_price', $regular_price );
2449
+ }
2450
+
2451
+ if ( '' !== $sale_price && $date_from && strtotime( $date_from ) < strtotime( 'NOW', current_time( 'timestamp' ) ) ) {
2452
+ update_post_meta( $product_id, '_price', $sale_price );
2453
+ }
2454
+
2455
+ if ( $date_to && strtotime( $date_to ) < strtotime( 'NOW', current_time( 'timestamp' ) ) ) {
2456
+ update_post_meta( $product_id, '_price', $regular_price );
2457
+ update_post_meta( $product_id, '_sale_price_dates_from', '' );
2458
+ update_post_meta( $product_id, '_sale_price_dates_to', '' );
2459
+ }
2460
+ }
2461
+ }
trunk/includes/payment/class-wc-square-gateway.php ADDED
@@ -0,0 +1,563 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) {
3
+ exit; // Exit if accessed directly
4
+ }
5
+
6
+ class WC_Square_Gateway extends WC_Payment_Gateway {
7
+ protected $connect;
8
+ protected $token;
9
+ public $log;
10
+
11
+ /**
12
+ * Constructor
13
+ */
14
+ public function __construct() {
15
+ $this->id = 'square';
16
+ $this->method_title = __( 'Square', 'woocommerce-square' );
17
+ $this->method_description = __( 'Square works by adding payments fields in an iframe and then sending the details to Square for verification and processing.', 'woocommerce-square' );
18
+ $this->has_fields = true;
19
+ $this->supports = array(
20
+ 'products',
21
+ 'refunds',
22
+ );
23
+
24
+ // Load the form fields
25
+ $this->init_form_fields();
26
+
27
+ // Load the settings.
28
+ $this->init_settings();
29
+
30
+ // Get setting values
31
+ $this->title = $this->get_option( 'title' );
32
+ $this->description = $this->get_option( 'description' );
33
+ $this->enabled = $this->get_option( 'enabled' );
34
+ $this->capture = $this->get_option( 'capture' ) === 'yes' ? true : false;
35
+ $this->create_customer = $this->get_option( 'create_customer' ) === 'yes' ? true : false;
36
+ $this->logging = $this->get_option( 'logging' ) === 'yes' ? true : false;
37
+ $this->connect = new WC_Square_Payments_Connect(); // decouple in future when v2 is ready
38
+ $this->token = get_option( 'woocommerce_square_merchant_access_token' );
39
+
40
+ $this->connect->set_access_token( $this->token );
41
+
42
+ if ( WC_SQUARE_ENABLE_STAGING ) {
43
+ $this->description .= ' ' . __( 'STAGING MODE ENABLED. In staging mode, you can use the card number 4111111111111111 with any CVC and a valid expiration date.', 'woocommerce-square' );
44
+
45
+ $this->description = trim( $this->description );
46
+ }
47
+
48
+ // Hooks
49
+ add_action( 'wp_enqueue_scripts', array( $this, 'payment_scripts' ) );
50
+ add_action( 'admin_notices', array( $this, 'admin_notices' ) );
51
+ add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) );
52
+ }
53
+
54
+ /**
55
+ * get_icon function.
56
+ *
57
+ * @access public
58
+ * @return string
59
+ */
60
+ public function get_icon() {
61
+ $icon = '<img src="' . WC_HTTPS::force_https_url( WC()->plugin_url() . '/assets/images/icons/credit-cards/visa.svg' ) . '" alt="Visa" width="32" style="margin-left: 0.3em" />';
62
+ $icon .= '<img src="' . WC_HTTPS::force_https_url( WC()->plugin_url() . '/assets/images/icons/credit-cards/mastercard.svg' ) . '" alt="Mastercard" width="32" style="margin-left: 0.3em" />';
63
+ $icon .= '<img src="' . WC_HTTPS::force_https_url( WC()->plugin_url() . '/assets/images/icons/credit-cards/amex.svg' ) . '" alt="Amex" width="32" style="margin-left: 0.3em" />';
64
+
65
+ // Do not show Discover/Diners for Japan and UK since it's not available
66
+ if ( 'JP' !== WC()->countries->get_base_country() && 'GB' !== WC()->countries->get_base_country() ) {
67
+ $icon .= '<img src="' . WC_HTTPS::force_https_url( WC()->plugin_url() . '/assets/images/icons/credit-cards/discover.svg' ) . '" alt="Discover" width="32" style="margin-left: 0.3em" />';
68
+ $icon .= '<img src="' . WC_HTTPS::force_https_url( WC()->plugin_url() . '/assets/images/icons/credit-cards/diners.svg' ) . '" alt="Diners" width="32" style="margin-left: 0.3em" />';
69
+ }
70
+ return apply_filters( 'woocommerce_gateway_icon', $icon, $this->id );
71
+ }
72
+
73
+ /**
74
+ * Check if required fields are set
75
+ */
76
+ public function admin_notices() {
77
+ if ( 'yes' !== $this->enabled ) {
78
+ return;
79
+ }
80
+
81
+ // Show message if SSL is not detected in checkout page.
82
+ if ( ! WC_SQUARE_ENABLE_STAGING && ! wc_checkout_is_https() ) {
83
+ /* translators: %1$s - transport layer securtiy wiki URL */
84
+ echo '<div class="error"><p>' . sprintf( __( 'Square is enabled, but a SSL certificate is not detected. Your checkout may not be secure! Please ensure your server has a valid <a href="%1$s" target="_blank">SSL certificate</a>', 'woocommerce-square' ), 'https://en.wikipedia.org/wiki/Transport_Layer_Security' ) . '</p></div>';
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Check if this gateway is enabled
90
+ */
91
+ public function is_available() {
92
+ $is_available = true;
93
+
94
+ if ( 'yes' === $this->enabled ) {
95
+ if ( ! WC_SQUARE_ENABLE_STAGING && ! wc_checkout_is_https() ) {
96
+ $is_available = false;
97
+ }
98
+
99
+ if ( ! WC_SQUARE_ENABLE_STAGING && empty( $this->token ) ) {
100
+ $is_available = false;
101
+ }
102
+
103
+ // Square only supports Australia, Canada, Japan, UK, and US for now.
104
+ if ( ( 'US' !== WC()->countries->get_base_country() && 'CA' !== WC()->countries->get_base_country() && 'AU' !== WC()->countries->get_base_country() && 'GB' !== WC()->countries->get_base_country() && 'JP' !== WC()->countries->get_base_country() ) || ( 'USD' !== get_woocommerce_currency() && 'CAD' !== get_woocommerce_currency() && 'AUD' !== get_woocommerce_currency() && 'GBP' !== get_woocommerce_currency() && 'JPY' !== get_woocommerce_currency() ) ) {
105
+ $is_available = false;
106
+ }
107
+ } else {
108
+ $is_available = false;
109
+ }
110
+
111
+ return apply_filters( 'woocommerce_square_payment_gateway_is_available', $is_available );
112
+ }
113
+
114
+ /**
115
+ * Initialize Gateway Settings Form Fields
116
+ */
117
+ public function init_form_fields() {
118
+ $this->form_fields = apply_filters(
119
+ 'woocommerce_square_gateway_settings',
120
+ array(
121
+ 'enabled' => array(
122
+ 'title' => __( 'Enable/Disable', 'woocommerce-square' ),
123
+ 'label' => __( 'Enable Square', 'woocommerce-square' ),
124
+ 'type' => 'checkbox',
125
+ 'description' => '',
126
+ 'default' => 'no',
127
+ ),
128
+ 'title' => array(
129
+ 'title' => __( 'Title', 'woocommerce-square' ),
130
+ 'type' => 'text',
131
+ 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce-square' ),
132
+ 'default' => __( 'Credit card (Square)', 'woocommerce-square' ),
133
+ ),
134
+ 'description' => array(
135
+ 'title' => __( 'Description', 'woocommerce-square' ),
136
+ 'type' => 'textarea',
137
+ 'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce-square' ),
138
+ 'default' => __( 'Pay with your credit card via Square.', 'woocommerce-square' ),
139
+ ),
140
+ 'capture' => array(
141
+ 'title' => __( 'Delay Capture', 'woocommerce-square' ),
142
+ 'label' => __( 'Enable Delay Capture', 'woocommerce-square' ),
143
+ 'type' => 'checkbox',
144
+ 'description' => __( 'When enabled, the request will only perform an Auth on the provided card. You can then later perform either a Capture or Void.', 'woocommerce-square' ),
145
+ 'default' => 'no',
146
+ ),
147
+ 'create_customer' => array(
148
+ 'title' => __( 'Create Customer', 'woocommerce-square' ),
149
+ 'label' => __( 'Enable Create Customer', 'woocommerce-square' ),
150
+ 'type' => 'checkbox',
151
+ 'description' => __( 'When enabled, processing a payment will create a customer profile on Square.', 'woocommerce-square' ),
152
+ 'default' => 'no',
153
+ ),
154
+ 'logging' => array(
155
+ 'title' => __( 'Logging', 'woocommerce-square' ),
156
+ 'label' => __( 'Log debug messages', 'woocommerce-square' ),
157
+ 'type' => 'checkbox',
158
+ 'description' => __( 'Save debug messages to the WooCommerce System Status log.', 'woocommerce-square' ),
159
+ 'default' => 'no',
160
+ ),
161
+ )
162
+ );
163
+ }
164
+
165
+ /**
166
+ * Payment form on checkout page
167
+ */
168
+ public function payment_fields() {
169
+ ?>
170
+ <div class="sq-fieldset">
171
+ <?php
172
+ $allowed = array(
173
+ 'a' => array(
174
+ 'href' => array(),
175
+ 'title' => array(),
176
+ ),
177
+ 'br' => array(),
178
+ 'em' => array(),
179
+ 'strong' => array(),
180
+ 'span' => array(
181
+ 'class' => array(),
182
+ ),
183
+ );
184
+ if ( $this->description ) {
185
+ echo apply_filters( 'woocommerce_square_description', wpautop( wp_kses( $this->description, $allowed ) ) );
186
+ }
187
+ ?>
188
+ <p class="form-row form-row-wide">
189
+ <label for="sq-card-number"><?php esc_html_e( 'Card Number', 'woocommerce-square' ); ?> <span class="required">*</span></label>
190
+ <input id="sq-card-number" type="text" maxlength="20" autocomplete="off" placeholder="•••• •••• •••• ••••" name="<?php echo esc_attr( $this->id ); ?>-card-number" />
191
+ </p>
192
+
193
+ <p class="form-row form-row-first">
194
+ <label for="sq-expiration-date"><?php esc_html_e( 'Expiry (MM/YY)', 'woocommerce-square' ); ?> <span class="required">*</span></label>
195
+ <input id="sq-expiration-date" type="text" autocomplete="off" placeholder="<?php esc_attr_e( 'MM / YY', 'woocommerce-square' ); ?>" name="<?php echo esc_attr( $this->id ); ?>-card-expiry" />
196
+ </p>
197
+
198
+ <p class="form-row form-row-last">
199
+ <label for="sq-cvv"><?php esc_html_e( 'Card Code', 'woocommerce-square' ); ?> <span class="required">*</span></label>
200
+ <input id="sq-cvv" type="text" autocomplete="off" placeholder="<?php esc_attr_e( 'CVV', 'woocommerce-square' ); ?>" name="<?php echo esc_attr( $this->id ); ?>-card-cvv" />
201
+ </p>
202
+
203
+ <p class="form-row form-row-wide">
204
+ <label for="sq-postal-code"><?php esc_html_e( 'Card Postal Code', 'woocommerce-square' ); ?> <span class="required">*</span></label>
205
+ <input id="sq-postal-code" type="text" autocomplete="off" placeholder="<?php esc_attr_e( 'Card Postal Code', 'woocommerce-square' ); ?>" name="<?php echo esc_attr( $this->id ); ?>-card-postal-code" />
206
+ </p>
207
+ </div>
208
+ <?php
209
+ }
210
+
211
+ /**
212
+ * Get payment form input styles.
213
+ * This function is pass to the JS script in order to style the
214
+ * input fields within the iFrame.
215
+ *
216
+ * Possible styles are: mediaMinWidth, mediaMaxWidth, backgroundColor, boxShadow,
217
+ * color, fontFamily, fontSize, fontWeight, lineHeight and padding.
218
+ *
219
+ * @since 1.0.4
220
+ * @version 1.0.4
221
+ * @access public
222
+ * @return json $styles
223
+ */
224
+ public function get_input_styles() {
225
+ $styles = array(
226
+ array(
227
+ 'fontSize' => '1.2em',
228
+ 'padding' => '.618em',
229
+ 'fontWeight' => 400,
230
+ 'backgroundColor' => 'transparent',
231
+ 'lineHeight' => 1.7,
232
+ ),
233
+ array(
234
+ 'mediaMaxWidth' => '1200px',
235
+ 'fontSize' => '1em',
236
+ ),
237
+ );
238
+
239
+ return apply_filters( 'woocommerce_square_payment_input_styles', wp_json_encode( $styles ) );
240
+ }
241
+
242
+ /**
243
+ * payment_scripts function.
244
+ *
245
+ *
246
+ * @access public
247
+ */
248
+ public function payment_scripts() {
249
+ if ( ! is_checkout() ) {
250
+ return;
251
+ }
252
+
253
+ $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
254
+
255
+ wp_register_script( 'square', 'https://js.squareup.com/v2/paymentform', '', '0.0.2', true );
256
+ wp_register_script( 'woocommerce-square', WC_SQUARE_PLUGIN_URL . '/assets/js/wc-square-payments' . $suffix . '.js', array( 'jquery', 'square' ), WC_SQUARE_VERSION, true );
257
+
258
+ wp_localize_script(
259
+ 'woocommerce-square',
260
+ 'square_params',
261
+ array(
262
+ 'application_id' => SQUARE_APPLICATION_ID,
263
+ 'environment' => WC_SQUARE_ENABLE_STAGING ? 'staging' : 'production',
264
+ 'placeholder_card_number' => __( '•••• •••• •••• ••••', 'woocommerce-square' ),
265
+ 'placeholder_card_expiration' => __( 'MM / YY', 'woocommerce-square' ),
266
+ 'placeholder_card_cvv' => __( 'CVV', 'woocommerce-square' ),
267
+ 'placeholder_card_postal_code' => __( 'Card Postal Code', 'woocommerce-square' ),
268
+ 'payment_form_input_styles' => esc_js( $this->get_input_styles() ),
269
+ 'custom_form_trigger_element' => apply_filters( 'woocommerce_square_payment_form_trigger_element', esc_js( '' ) ),
270
+ )
271
+ );
272
+
273
+ wp_enqueue_script( 'woocommerce-square' );
274
+
275
+ wp_enqueue_style( 'woocommerce-square-styles', WC_SQUARE_PLUGIN_URL . '/assets/css/wc-square-frontend-styles.css' );
276
+
277
+ return true;
278
+ }
279
+
280
+ /**
281
+ * Process the payment
282
+ */
283
+ public function process_payment( $order_id, $retry = true ) {
284
+ $order = wc_get_order( $order_id );
285
+ $nonce = isset( $_POST['square_nonce'] ) ? wc_clean( $_POST['square_nonce'] ) : '';
286
+ $currency = version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->get_order_currency() : $order->get_currency();
287
+
288
+ $this->log( "Info: Begin processing payment for order {$order_id} for the amount of {$order->get_total()}" );
289
+
290
+ try {
291
+ $data = array(
292
+ 'idempotency_key' => apply_filters( 'woocommerce_square_idempotency_key', uniqid(), $order ),
293
+ 'amount_money' => array(
294
+ 'amount' => (int) WC_Square_Utils::format_amount_to_square( $order->get_total(), $currency ),
295
+ 'currency' => $currency,
296
+ ),
297
+ 'reference_id' => (string) $order->get_order_number(),
298
+ 'delay_capture' => $this->capture ? true : false,
299
+ 'card_nonce' => $nonce,
300
+ 'buyer_email_address' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->billing_email : $order->get_billing_email(),
301
+ 'billing_address' => array(
302
+ 'address_line_1' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->billing_address_1 : $order->get_billing_address_1(),
303
+ 'address_line_2' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->billing_address_2 : $order->get_billing_address_2(),
304
+ 'locality' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->billing_city : $order->get_billing_city(),
305
+ 'administrative_district_level_1' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->billing_state : $order->get_billing_state(),
306
+ 'postal_code' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->billing_postcode : $order->get_billing_postcode(),
307
+ 'country' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->billing_country : $order->get_billing_country(),
308
+ ),
309
+ 'note' => apply_filters( 'woocommerce_square_payment_order_note', 'WooCommerce: Order #' . (string) $order->get_order_number(), $order ),
310
+ );
311
+
312
+ if ( $order->needs_shipping_address() ) {
313
+ $data['shipping_address'] = array(
314
+ 'address_line_1' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->shipping_address_1 : $order->get_shipping_address_1(),
315
+ 'address_line_2' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->shipping_address_2 : $order->get_shipping_address_2(),
316
+ 'locality' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->shipping_city : $order->get_shipping_city(),
317
+ 'administrative_district_level_1' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->shipping_state : $order->get_shipping_state(),
318
+ 'postal_code' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->shipping_postcode : $order->get_shipping_postcode(),
319
+ 'country' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->shipping_country : $order->get_shipping_country(),
320
+ );
321
+ }
322
+
323
+ $result = $this->connect->charge_card_nonce( Woocommerce_Square::instance()->integration->get_option( 'location' ), $data );
324
+
325
+ if ( is_wp_error( $result ) ) {
326
+ wc_add_notice( __( 'Error: Square was unable to complete the transaction. Please try again later or use another means of payment.', 'woocommerce-square' ), 'error' );
327
+
328
+ throw new Exception( $result->get_error_message() );
329
+ }
330
+
331
+ if ( ! empty( $result->errors ) ) {
332
+ if ( 'INVALID_REQUEST_ERROR' === $result->errors[0]->category ) {
333
+ wc_add_notice( __( 'Error: Square was unable to complete the transaction. Please try again later or use another means of payment.', 'woocommerce-square' ), 'error' );
334
+ }
335
+
336
+ if ( 'PAYMENT_METHOD_ERROR' === $result->errors[0]->category || 'VALIDATION_ERROR' === $result->errors[0]->category ) {
337
+ // format errors for display
338
+ $error_html = __( 'Payment Error: ', 'woocommerce-square' );
339
+ $error_html .= '<br />';
340
+ $error_html .= '<ul>';
341
+
342
+ foreach ( $result->errors as $error ) {
343
+ $error_html .= '<li>' . $error->detail . '</li>';
344
+ }
345
+
346
+ $error_html .= '</ul>';
347
+
348
+ wc_add_notice( $error_html, 'error' );
349
+ }
350
+
351
+ $errors = print_r( $result->errors, true );
352
+
353
+ throw new Exception( $errors );
354
+ }
355
+
356
+ if ( empty( $result ) ) {
357
+ wc_add_notice( __( 'Error: Square was unable to complete the transaction. Please try again later or use another means of payment.', 'woocommerce-square' ), 'error' );
358
+
359
+ throw new Exception( 'Unknown Error' );
360
+ }
361
+
362
+ if ( 'CAPTURED' === $result->transaction->tenders[0]->card_details->status ) {
363
+ // Store captured value
364
+ update_post_meta( $order_id, '_square_charge_captured', 'yes' );
365
+
366
+ // Payment complete
367
+ $order->payment_complete( $result->transaction->id );
368
+
369
+ // Add order note
370
+ /* translators: %s - transaction id */
371
+ $complete_message = sprintf( __( 'Square charge complete (Charge ID: %s)', 'woocommerce-square' ), $result->transaction->id );
372
+ $order->add_order_note( $complete_message );
373
+ $this->log( "Success: $complete_message" );
374
+ } else {
375
+ // Store captured value
376
+ update_post_meta( $order_id, '_square_charge_captured', 'no' );
377
+ update_post_meta( $order_id, '_transaction_id', $result->transaction->id );
378
+
379
+ // Mark as on-hold
380
+ /* translators: %s - transaction id */
381
+ $authorized_message = sprintf( __( 'Square charge authorized (Authorized ID: %s). Process order to take payment, or cancel to remove the pre-authorization.', 'woocommerce-square' ), $result->transaction->id );
382
+ $order->update_status( 'on-hold', $authorized_message );
383
+ $this->log( "Success: $authorized_message" );
384
+
385
+ // Reduce stock levels
386
+ version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->reduce_order_stock() : wc_reduce_stock_levels( $order_id );
387
+ }
388
+
389
+ // we got this far which means the transaction went through
390
+ if ( $this->create_customer ) {
391
+ $this->maybe_create_customer( $order );
392
+ }
393
+
394
+ // Remove cart
395
+ WC()->cart->empty_cart();
396
+
397
+ // Return thank you page redirect
398
+ return array(
399
+ 'result' => 'success',
400
+ 'redirect' => $this->get_return_url( $order ),
401
+ );
402
+ } catch ( Exception $e ) {
403
+ /* translators: %s - error message */
404
+ $this->log( sprintf( __( 'Error: %s', 'woocommerce-square' ), $e->getMessage() ) );
405
+
406
+ $order->update_status( 'failed', $e->getMessage() );
407
+
408
+ return;
409
+ }
410
+ }
411
+
412
+ /**
413
+ * Tries to create the customer on Square
414
+ *
415
+ * @param object $order
416
+ */
417
+ public function maybe_create_customer( $order ) {
418
+ $user = get_current_user_id();
419
+ $square_customer_id = get_user_meta( $user, '_square_customer_id', true );
420
+ $create_customer = true;
421
+ $phone_number = (string) version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->billing_phone : $order->get_billing_phone();
422
+
423
+ $customer = array(
424
+ 'given_name' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->billing_first_name : $order->get_billing_first_name(),
425
+ 'family_name' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->billing_last_name : $order->get_billing_last_name(),
426
+ 'email_address' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->billing_email : $order->get_billing_email(),
427
+ 'address' => array(
428
+ 'address_line_1' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->billing_address_1 : $order->get_billing_address_1(),
429
+ 'address_line_2' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->billing_address_2 : $order->get_billing_address_2(),
430
+ 'locality' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->billing_city : $order->get_billing_city(),
431
+ 'administrative_district_level_1' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->billing_state : $order->get_billing_state(),
432
+ 'postal_code' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->billing_postcode : $order->get_billing_postcode(),
433
+ 'country' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->billing_country : $order->get_billing_country(),
434
+ ),
435
+ 'phone_number' => ! empty( $phone_number ) ? $phone_number : null,
436
+ 'reference_id' => ! empty( $user ) ? (string) $user : __( 'Guest', 'woocommerce-square' ),
437
+ );
438
+
439
+ // to prevent creating duplicate customer
440
+ // check to make sure this customer does not exist on Square
441
+ if ( ! empty( $square_customer_id ) ) {
442
+ $square_customer = $this->connect->get_customer( $square_customer_id );
443
+
444
+ if ( empty( $square_customer->errors ) ) {
445
+ // customer already exist on Square
446
+ $create_customer = false;
447
+ }
448
+ }
449
+
450
+ if ( $create_customer ) {
451
+ $result = $this->connect->create_customer( $customer );
452
+
453
+ // we don't want to halt any processes here just log it
454
+ if ( is_wp_error( $result ) ) {
455
+ /* translators: %s - error message */
456
+ $this->log( sprintf( __( 'Error creating customer: %s', 'woocommerce-square' ), $result->get_error_message() ) );
457
+ /* translators: %s - error message */
458
+ $order->add_order_note( sprintf( __( 'Error creating customer: %s', 'woocommerce-square' ), $result->get_error_message() ) );
459
+ }
460
+
461
+ // we don't want to halt any processes here just log it
462
+ if ( ! empty( $result->errors ) ) {
463
+ /* translators: %s - errors */
464
+ $this->log( sprintf( __( 'Error creating customer: %s', 'woocommerce-square' ), print_r( $result->errors, true ) ) );
465
+ /* translators: %s - errors */
466
+ $order->add_order_note( sprintf( __( 'Error creating customer: %s', 'woocommerce-square' ), print_r( $result->errors, true ) ) );
467
+ }
468
+
469
+ // if no errors save Square customer ID to user meta
470
+ if ( ! is_wp_error( $result ) && empty( $result->errors ) && ! empty( $user ) ) {
471
+ update_user_meta( $user, '_square_customer_id', $result->customer->id );
472
+ /* translators: %s - customer id */
473
+ $order->add_order_note( sprintf( __( 'Customer created on Square: %s', 'woocommerce-square' ), $result->customer->id ) );
474
+ }
475
+ }
476
+ }
477
+
478
+ /**
479
+ * Refund a charge
480
+ * @param int $order_id
481
+ * @param float $amount
482
+ * @return bool
483
+ */
484
+ public function process_refund( $order_id, $amount = null, $reason = '' ) {
485
+ $order = wc_get_order( $order_id );
486
+
487
+ if ( ! $order || ! $order->get_transaction_id() ) {
488
+ return false;
489
+ }
490
+
491
+ if ( 'square' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->payment_method : $order->get_payment_method() ) ) {
492
+ try {
493
+ $this->log( "Info: Begin refund for order {$order_id} for the amount of {$amount}" );
494
+
495
+ $trans_id = get_post_meta( $order_id, '_transaction_id', true );
496
+ $captured = get_post_meta( $order_id, '_square_charge_captured', true );
497
+ $location = Woocommerce_Square::instance()->integration->get_option( 'location' );
498
+
499
+ $transaction_status = $this->connect->get_transaction_status( $location, $trans_id );
500
+
501
+ if ( 'CAPTURED' === $transaction_status ) {
502
+ $tender_id = $this->connect->get_tender_id( $location, $trans_id );
503
+
504
+ $body = array();
505
+
506
+ $body['idempotency_key'] = apply_filters( 'woocommerce_square_idempotency_key', $order_id . '-' . $order->get_order_number(), $order );
507
+ $body['tender_id'] = $tender_id;
508
+
509
+ if ( ! is_null( $amount ) ) {
510
+ $body['amount_money'] = array(
511
+ 'amount' => (int) WC_Square_Utils::format_amount_to_square( $amount ),
512
+ 'currency' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->get_order_currency() : $order->get_currency(),
513
+ );
514
+ }
515
+
516
+ if ( $reason ) {
517
+ $body['reason'] = $reason;
518
+ }
519
+
520
+ $result = $this->connect->refund_transaction( $location, $trans_id, $body );
521
+
522
+ if ( is_wp_error( $result ) ) {
523
+ throw new Exception( $result->get_error_message() );
524
+
525
+ } elseif ( ! empty( $result->errors ) ) {
526
+ throw new Exception( 'Error: ' . print_r( $result->errors, true ) );
527
+
528
+ } else {
529
+ if ( 'APPROVED' === $result->refund->status || 'PENDING' === $result->refund->status ) {
530
+ /* translators: %1$s - refund amount, %2$s - refund id, %3$s - refund reason */
531
+ $refund_message = sprintf( __( 'Refunded %1$s - Refund ID: %2$s - Reason: %3$s', 'woocommerce-square' ), wc_price( $result->refund->amount_money->amount / 100 ), $result->refund->id, $reason );
532
+
533
+ $order->add_order_note( $refund_message );
534
+
535
+ $this->log( 'Success: ' . html_entity_decode( strip_tags( $refund_message ) ) );
536
+
537
+ return true;
538
+ }
539
+ }
540
+ }
541
+ } catch ( Exception $e ) {
542
+ /* translators: %s - error message */
543
+ $this->log( sprintf( __( 'Error: %s', 'woocommerce-square' ), $e->getMessage() ) );
544
+
545
+ return false;
546
+ }
547
+ }
548
+ }
549
+
550
+ /**
551
+ * Logs
552
+ *
553
+ * @since 1.0.0
554
+ * @version 1.0.0
555
+ *
556
+ * @param string $message
557
+ */
558
+ public function log( $message ) {
559
+ if ( $this->logging ) {
560
+ WC_Square_Payment_Logger::log( $message );
561
+ }
562
+ }
563
+ }
trunk/includes/payment/class-wc-square-payment-logger.php ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) {
3
+ exit; // Exit if accessed directly
4
+ }
5
+
6
+ /**
7
+ * Square payment logging class which saves important data to the log
8
+ *
9
+ * @since 1.0.0
10
+ * @version 1.0.0
11
+ */
12
+ class WC_Square_Payment_Logger {
13
+
14
+ public static $logger;
15
+
16
+ /**
17
+ * Utilize WC logger class
18
+ *
19
+ * @since 1.0.0
20
+ * @version 1.0.0
21
+ */
22
+ public static function log( $message ) {
23
+ if ( empty( self::$logger ) ) {
24
+ self::$logger = new WC_Logger();
25
+ }
26
+
27
+ self::$logger->add( 'woocommerce-gateway-square', $message );
28
+ }
29
+ }
trunk/includes/payment/class-wc-square-payments-connect.php ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) {
3
+ exit; // Exit if accessed directly
4
+ }
5
+
6
+ /**
7
+ * Provides some overloaded methods from Square Client Class.
8
+ * This is needed because Square needs v2 of the API to work with payment endpoints.
9
+ * Once they have ported over all endpoints from v1 to v2, we can merge this back
10
+ * into the main Square Client Class.
11
+ */
12
+ class WC_Square_Payments_Connect extends WC_Square_Client {
13
+ const LOCATIONS_CACHE_KEY = 'wc_square_payments_locations';
14
+
15
+ /**
16
+ * @var string
17
+ */
18
+ protected $api_version = 'v2';
19
+
20
+ /**
21
+ * Checks to see if token is valid.
22
+ *
23
+ * There is no formal way to check this other than to
24
+ * retrieve the merchant account details and if it comes back
25
+ * with a code 200, we assume it is valid.
26
+ *
27
+ * @access public
28
+ * @since 1.0.0
29
+ * @version 1.0.0
30
+ * @return bool
31
+ */
32
+ public function is_valid_token() {
33
+
34
+ $merchant = $this->request( 'Retrieving Merchant', 'locations' );
35
+
36
+ if ( is_wp_error( $merchant ) ) {
37
+ return false;
38
+ }
39
+
40
+ return true;
41
+ }
42
+
43
+ /**
44
+ * Charges the card nonce.
45
+ *
46
+ * @access public
47
+ * @since 1.0.0
48
+ * @version 1.0.0
49
+ * @param string $location_id
50
+ * @param array $data
51
+ * @return object
52
+ */
53
+ public function charge_card_nonce( $location_id, $data ) {
54
+ $path = '/locations/' . $location_id . '/transactions';
55
+
56
+ return $this->request( 'Charge Card Nonce', $path, 'POST', $data );
57
+ }
58
+
59
+ /**
60
+ * Retrieves a transaction from Square
61
+ *
62
+ * @access public
63
+ * @since 1.0.0
64
+ * @version 1.0.0
65
+ * @param string $location_id
66
+ * @param string $transaction_id
67
+ * @return object
68
+ */
69
+ public function get_transaction( $location_id, $transaction_id ) {
70
+ $path = '/locations/' . $location_id . '/transactions/' . $transaction_id;
71
+
72
+ return $this->request( 'Get Transaction', $path );
73
+ }
74
+
75
+ /**
76
+ * Gets the transaction status
77
+ *
78
+ * @access public
79
+ * @since 1.0.0
80
+ * @version 1.0.0
81
+ * @param string $location_id
82
+ * @param string $transaction_id
83
+ * @return object
84
+ */
85
+ public function get_transaction_status( $location_id, $transaction_id ) {
86
+ $result = $this->get_transaction( $location_id, $transaction_id );
87
+
88
+ if ( is_wp_error( $result ) ) {
89
+ return null;
90
+ }
91
+
92
+ return $result->transaction->tenders[0]->card_details->status;
93
+ }
94
+
95
+ /**
96
+ * Gets the tender id of the transaction
97
+ *
98
+ * @access public
99
+ * @since 1.0.0
100
+ * @version 1.0.0
101
+ * @param string $location_id
102
+ * @param string $transaction_id
103
+ * @return object
104
+ */
105
+ public function get_tender_id( $location_id, $transaction_id ) {
106
+ $result = $this->get_transaction( $location_id, $transaction_id );
107
+
108
+ if ( is_wp_error( $result ) ) {
109
+ return null;
110
+ }
111
+
112
+ return $result->transaction->tenders[0]->id;
113
+ }
114
+
115
+ /**
116
+ * Capture a previously authorized transaction ( delay/capture )
117
+ *
118
+ * @access public
119
+ * @since 1.0.0
120
+ * @version 1.0.0
121
+ * @param string $location_id
122
+ * @param string $transaction_id
123
+ * @return object
124
+ */
125
+ public function capture_transaction( $location_id, $transaction_id ) {
126
+ $path = '/locations/' . $location_id . '/transactions/' . $transaction_id . '/capture';
127
+
128
+ return $this->request( 'Capture Transaction', $path, 'POST' );
129
+ }
130
+
131
+ /**
132
+ * Voids a previously authorized transaction ( delay/capture )
133
+ *
134
+ * @access public
135
+ * @since 1.0.0
136
+ * @version 1.0.0
137
+ * @param string $location_id
138
+ * @param string $transaction_id
139
+ * @return object
140
+ */
141
+ public function void_transaction( $location_id, $transaction_id ) {
142
+ $path = '/locations/' . $location_id . '/transactions/' . $transaction_id . '/void';
143
+
144
+ return $this->request( 'Void Authorized Transaction', $path, 'POST' );
145
+ }
146
+
147
+ /**
148
+ * Refunds a transaction
149
+ *
150
+ * @access public
151
+ * @since 1.0.0
152
+ * @version 1.0.0
153
+ * @param string $location_id
154
+ * @param string $transaction_id
155
+ * @param array $data
156
+ * @return object
157
+ */
158
+ public function refund_transaction( $location_id, $transaction_id, $data ) {
159
+ $path = '/locations/' . $location_id . '/transactions/' . $transaction_id . '/refund';
160
+
161
+ return $this->request( 'Refund Transaction', $path, 'POST', $data );
162
+ }
163
+
164
+ /**
165
+ * Create a customer
166
+ *
167
+ * @access public
168
+ * @since 1.0.0
169
+ * @version 1.0.0
170
+ * @param array $data
171
+ * @return object
172
+ */
173
+ public function create_customer( $data ) {
174
+ $path = '/customers';
175
+
176
+ return $this->request( 'Create Customer', $path, 'POST', $data );
177
+ }
178
+
179
+ /**
180
+ * Get a customer
181
+ *
182
+ * @access public
183
+ * @since 1.0.0
184
+ * @version 1.0.0
185
+ * @param string $customer_id
186
+ * @return object
187
+ */
188
+ public function get_customer( $customer_id = null ) {
189
+ if ( null === $customer_id ) {
190
+ return false;
191
+ }
192
+
193
+ $path = '/customers/' . $customer_id;
194
+
195
+ return $this->request( 'Get Customer', $path, 'GET' );
196
+ }
197
+
198
+ /**
199
+ * @param $path
200
+ *
201
+ * @return string
202
+ */
203
+ protected function get_request_url( $path ) {
204
+ $api_url_base = trailingslashit( $this->get_api_url() );
205
+
206
+ $request_path = ltrim( $path, '/' );
207
+ $request_url = untrailingslashit( $api_url_base . $request_path );
208
+
209
+ return $request_url;
210
+ }
211
+ }
trunk/includes/payment/class-wc-square-payments.php ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) {
3
+ exit; // Exit if accessed directly
4
+ }
5
+
6
+ include_once( dirname( __FILE__ ) . '/class-wc-square-payments-connect.php' );
7
+
8
+ class WC_Square_Payments {
9
+ protected $connect;
10
+ public $logging;
11
+
12
+ /**
13
+ * Constructor
14
+ */
15
+ public function __construct( WC_Square_Payments_Connect $connect ) {
16
+ $this->init();
17
+ $this->connect = $connect;
18
+
19
+ add_filter( 'woocommerce_payment_gateways', array( $this, 'register_gateway' ) );
20
+
21
+ add_action( 'woocommerce_order_status_on-hold_to_processing', array( $this, 'capture_payment' ) );
22
+ add_action( 'woocommerce_order_status_on-hold_to_completed', array( $this, 'capture_payment' ) );
23
+ add_action( 'woocommerce_order_status_on-hold_to_cancelled', array( $this, 'cancel_payment' ) );
24
+ add_action( 'woocommerce_order_status_on-hold_to_refunded', array( $this, 'cancel_payment' ) );
25
+
26
+ if ( is_admin() ) {
27
+ add_filter( 'woocommerce_order_actions', array( $this, 'add_capture_charge_order_action' ) );
28
+ add_action( 'woocommerce_order_action_square_capture_charge', array( $this, 'maybe_capture_charge' ) );
29
+ }
30
+
31
+ $gateway_settings = get_option( 'woocommerce_square_settings' );
32
+
33
+ $this->logging = ! empty( $gateway_settings['logging'] ) ? true : false;
34
+
35
+ return true;
36
+ }
37
+
38
+ /**
39
+ * Init
40
+ */
41
+ public function init() {
42
+ if ( ! class_exists( 'WC_Payment_Gateway' ) ) {
43
+ return;
44
+ }
45
+
46
+ // live/production app id from Square account
47
+ if ( ! defined( 'SQUARE_APPLICATION_ID' ) ) {
48
+ define( 'SQUARE_APPLICATION_ID', 'sq0idp-wGVapF8sNt9PLrdj5znuKA' );
49
+ }
50
+
51
+ // Includes
52
+ include_once( dirname( __FILE__ ) . '/class-wc-square-gateway.php' );
53
+
54
+ return true;
55
+ }
56
+
57
+ /**
58
+ * Register the gateway for use
59
+ */
60
+ public function register_gateway( $methods ) {
61
+ $methods[] = 'WC_Square_Gateway';
62
+
63
+ return $methods;
64
+ }
65
+
66
+ public function add_capture_charge_order_action( $actions ) {
67
+ if ( ! isset( $_REQUEST['post'] ) ) {
68
+ return $actions;
69
+ }
70
+
71
+ $order = wc_get_order( absint( $_REQUEST['post'] ) );
72
+
73
+ // Bail if order is not found.
74
+ if ( empty( $order ) ) {
75
+ return $actions;
76
+ }
77
+
78
+ // bail if the order wasn't paid for with this gateway
79
+ if ( 'square' !== ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->payment_method : $order->get_payment_method() ) ) {
80
+ return $actions;
81
+ }
82
+
83
+ // bail if charge was already captured
84
+ if ( 'yes' === get_post_meta( version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->id : $order->get_id(), '_square_charge_captured', true ) ) {
85
+ return $actions;
86
+ }
87
+
88
+ $actions['square_capture_charge'] = esc_html__( 'Capture Charge', 'woocommerce-square' );
89
+
90
+ return $actions;
91
+ }
92
+
93
+ public function maybe_capture_charge( $order ) {
94
+ if ( ! is_object( $order ) ) {
95
+ $order = wc_get_order( $order );
96
+ }
97
+
98
+ $this->capture_payment( version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->id : $order->get_id() );
99
+
100
+ return true;
101
+ }
102
+
103
+ /**
104
+ * Capture payment when the order is changed from on-hold to complete or processing
105
+ *
106
+ * @param int $order_id
107
+ */
108
+ public function capture_payment( $order_id ) {
109
+ $order = wc_get_order( $order_id );
110
+
111
+ if ( 'square' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->payment_method : $order->get_payment_method() ) ) {
112
+ try {
113
+ $this->log( "Info: Begin capture for order {$order_id}" );
114
+
115
+ $trans_id = get_post_meta( $order_id, '_transaction_id', true );
116
+ $captured = get_post_meta( $order_id, '_square_charge_captured', true );
117
+ $location = Woocommerce_Square::instance()->integration->get_option( 'location' );
118
+ $token = get_option( 'woocommerce_square_merchant_access_token' );
119
+
120
+ $this->connect->set_access_token( $token );
121
+
122
+ $transaction_status = $this->connect->get_transaction_status( $location, $trans_id );
123
+
124
+ if ( 'AUTHORIZED' === $transaction_status ) {
125
+ $result = $this->connect->capture_transaction( $location, $trans_id ); // returns empty object
126
+
127
+ if ( is_wp_error( $result ) ) {
128
+ $order->add_order_note( __( 'Unable to capture charge!', 'woocommerce-square' ) . ' ' . $result->get_error_message() );
129
+
130
+ throw new Exception( $result->get_error_message() );
131
+ } elseif ( ! empty( $result->errors ) ) {
132
+ $order->add_order_note( __( 'Unable to capture charge!', 'woocommerce-square' ) . ' ' . print_r( $result->errors, true ) );
133
+
134
+ throw new Exception( print_r( $result->errors, true ) );
135
+ } else {
136
+ $order->add_order_note( sprintf( __( 'Square charge complete (Charge ID: %s)', 'woocommerce-square' ), $trans_id ) );
137
+ update_post_meta( $order_id, '_square_charge_captured', 'yes' );
138
+ $this->log( "Info: Capture successful for {$order_id}" );
139
+ }
140
+ }
141
+ } catch ( Exception $e ) {
142
+ $this->log( sprintf( __( 'Error unable to capture charge: %s', 'woocommerce-square' ), $e->getMessage() ) );
143
+ }
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Cancel authorization
149
+ *
150
+ * @param int $order_id
151
+ */
152
+ public function cancel_payment( $order_id ) {
153
+ $order = wc_get_order( $order_id );
154
+
155
+ if ( 'square' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->payment_method : $order->get_payment_method() ) ) {
156
+ try {
157
+ $this->log( "Info: Cancel payment for order {$order_id}" );
158
+
159
+ $trans_id = get_post_meta( $order_id, '_transaction_id', true );
160
+ $captured = get_post_meta( $order_id, '_square_charge_captured', true );
161
+ $location = Woocommerce_Square::instance()->integration->get_option( 'location' );
162
+ $token = get_option( 'woocommerce_square_merchant_access_token' );
163
+
164
+ $this->connect->set_access_token( $token );
165
+
166
+ $transaction_status = $this->connect->get_transaction_status( $location, $trans_id );
167
+
168
+ if ( 'AUTHORIZED' === $transaction_status ) {
169
+ $result = $this->connect->void_transaction( $location, $trans_id ); // returns empty object
170
+
171
+ if ( is_wp_error( $result ) ) {
172
+ $order->add_order_note( __( 'Unable to void charge!', 'woocommerce-square' ) . ' ' . $result->get_error_message() );
173
+ throw new Exception( $result->get_error_message() );
174
+ } elseif ( ! empty( $result->errors ) ) {
175
+ $order->add_order_note( __( 'Unable to void charge!', 'woocommerce-square' ) . ' ' . print_r( $result->errors, true ) );
176
+
177
+ throw new Exception( print_r( $result->errors, true ) );
178
+ } else {
179
+ $order->add_order_note( sprintf( __( 'Square charge voided! (Charge ID: %s)', 'woocommerce-square' ), $trans_id ) );
180
+ delete_post_meta( $order_id, '_square_charge_captured' );
181
+ delete_post_meta( $order_id, '_transaction_id' );
182
+ }
183
+ }
184
+ } catch ( Exception $e ) {
185
+ $this->log( sprintf( __( 'Unable to void charge!: %s', 'woocommerce-square' ), $e->getMessage() ) );
186
+ }
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Logs
192
+ *
193
+ * @since 1.0.0
194
+ * @version 1.0.0
195
+ *
196
+ * @param string $message
197
+ */
198
+ public function log( $message ) {
199
+ if ( $this->logging ) {
200
+ WC_Square_Payment_Logger::log( $message );
201
+ }
202
+ }
203
+ }
204
+
205
+ new WC_Square_Payments( new WC_Square_Payments_Connect() );
trunk/languages/woocommerce-square.pot ADDED
@@ -0,0 +1,698 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (C) 2019 WooCommerce
2
+ # This file is distributed under the same license as the WooCommerce Square package.
3
+ msgid ""
4
+ msgstr ""
5
+ "Project-Id-Version: WooCommerce Square 1.0.37\n"
6
+ "Report-Msgid-Bugs-To: "
7
+ "https://github.com/woocommerce/woocommerce-square/issues\n"
8
+ "POT-Creation-Date: 2019-04-16 14:35:33+00:00\n"
9
+ "MIME-Version: 1.0\n"
10
+ "Content-Type: text/plain; charset=utf-8\n"
11
+ "Content-Transfer-Encoding: 8bit\n"
12
+ "PO-Revision-Date: 2019-MO-DA HO:MI+ZONE\n"
13
+ "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
14
+ "Language-Team: LANGUAGE <EMAIL@ADDRESS>\n"
15
+ "X-Generator: grunt-wp-i18n1.0.1\n"
16
+
17
+ #: includes/admin/class-wc-square-admin-integration.php:20
18
+ #: includes/admin/class-wc-square-privacy.php:12
19
+ #: includes/payment/class-wc-square-gateway.php:16
20
+ msgid "Square"
21
+ msgstr ""
22
+
23
+ #: includes/admin/class-wc-square-admin-integration.php:21
24
+ msgid ""
25
+ "Connect with Square to start syncing your products and inventory and also "
26
+ "accept credit card and debit card payments in your checkout."
27
+ msgstr ""
28
+
29
+ #: includes/admin/class-wc-square-admin-integration.php:53
30
+ msgid "Business Location"
31
+ msgstr ""
32
+
33
+ #: includes/admin/class-wc-square-admin-integration.php:55
34
+ msgid ""
35
+ "Select the location you wish to link to this site. You must have <a "
36
+ "href=\"https://squareup.com/dashboard/locations\" "
37
+ "target=\"_blank\">locations</a> set in Square and approved."
38
+ msgstr ""
39
+
40
+ #: includes/admin/class-wc-square-admin-integration.php:57
41
+ msgid "Select a Location"
42
+ msgstr ""
43
+
44
+ #: includes/admin/class-wc-square-admin-integration.php:61
45
+ msgid "Notification Email"
46
+ msgstr ""
47
+
48
+ #: includes/admin/class-wc-square-admin-integration.php:63
49
+ msgid ""
50
+ "Enter the email(s) to be notified when a sync has ended. Separate each "
51
+ "email with a comma."
52
+ msgstr ""
53
+
54
+ #: includes/admin/class-wc-square-admin-integration.php:69
55
+ #: includes/payment/class-wc-square-gateway.php:155
56
+ msgid "Logging"
57
+ msgstr ""
58
+
59
+ #: includes/admin/class-wc-square-admin-integration.php:70
60
+ #: includes/payment/class-wc-square-gateway.php:156
61
+ msgid "Log debug messages"
62
+ msgstr ""
63
+
64
+ #: includes/admin/class-wc-square-admin-integration.php:72
65
+ #: includes/payment/class-wc-square-gateway.php:158
66
+ msgid "Save debug messages to the WooCommerce System Status log."
67
+ msgstr ""
68
+
69
+ #: includes/admin/class-wc-square-admin-integration.php:76
70
+ msgid "Synchronization"
71
+ msgstr ""
72
+
73
+ #: includes/admin/class-wc-square-admin-integration.php:78
74
+ msgid ""
75
+ "Determine which aspects of your product catalog to synchronize between "
76
+ "WooCommerce and Square. Products need to have SKUs set for each variation."
77
+ msgstr ""
78
+
79
+ #: includes/admin/class-wc-square-admin-integration.php:81
80
+ msgid "Enabled"
81
+ msgstr ""
82
+
83
+ #: includes/admin/class-wc-square-admin-integration.php:83
84
+ msgid "Products"
85
+ msgstr ""
86
+
87
+ #: includes/admin/class-wc-square-admin-integration.php:84
88
+ msgid ""
89
+ "Basic Product information will be synced, excluding Categories and "
90
+ "Inventory."
91
+ msgstr ""
92
+
93
+ #: includes/admin/class-wc-square-admin-integration.php:88
94
+ msgid "Include Categories"
95
+ msgstr ""
96
+
97
+ #: includes/admin/class-wc-square-admin-integration.php:90
98
+ msgid "Sync Categories"
99
+ msgstr ""
100
+
101
+ #: includes/admin/class-wc-square-admin-integration.php:91
102
+ msgid ""
103
+ "Categories will sync on creation or update, and Products will have their "
104
+ "Categories synced."
105
+ msgstr ""
106
+
107
+ #: includes/admin/class-wc-square-admin-integration.php:95
108
+ msgid "Include Inventory"
109
+ msgstr ""
110
+
111
+ #: includes/admin/class-wc-square-admin-integration.php:97
112
+ msgid "Sync Inventory"
113
+ msgstr ""
114
+
115
+ #: includes/admin/class-wc-square-admin-integration.php:98
116
+ msgid "Inventory will sync on manual update or after a Product is ordered."
117
+ msgstr ""
118
+
119
+ #: includes/admin/class-wc-square-admin-integration.php:102
120
+ msgid "Include Images"
121
+ msgstr ""
122
+
123
+ #: includes/admin/class-wc-square-admin-integration.php:104
124
+ msgid "Sync Images"
125
+ msgstr ""
126
+
127
+ #: includes/admin/class-wc-square-admin-integration.php:105
128
+ msgid "Product Image will be synced."
129
+ msgstr ""
130
+
131
+ #: includes/admin/class-wc-square-admin-integration.php:109
132
+ msgid "Square Inventory Sync"
133
+ msgstr ""
134
+
135
+ #: includes/admin/class-wc-square-admin-integration.php:111
136
+ msgid "Enable"
137
+ msgstr ""
138
+
139
+ #: includes/admin/class-wc-square-admin-integration.php:112
140
+ msgid ""
141
+ "For automatic inventory syncing from Square to WooCommerce, this needs to "
142
+ "be enabled. It will poll the inventory from Square on an hourly basis."
143
+ msgstr ""
144
+
145
+ #: includes/admin/class-wc-square-admin-integration.php:172
146
+ msgid "Connect/Disconnect"
147
+ msgstr ""
148
+
149
+ #: includes/admin/class-wc-square-admin-integration.php:177
150
+ msgid "Disconnect from Square"
151
+ msgstr ""
152
+
153
+ #: includes/admin/class-wc-square-admin-integration.php:185
154
+ msgid "Connect with Square"
155
+ msgstr ""
156
+
157
+ #: includes/admin/class-wc-square-admin-integration.php:201
158
+ msgid "Initiate a Manual Sync"
159
+ msgstr ""
160
+
161
+ #: includes/admin/class-wc-square-admin-integration.php:205
162
+ msgid "WC &rarr; Square Sync"
163
+ msgstr ""
164
+
165
+ #: includes/admin/class-wc-square-admin-integration.php:206
166
+ msgid "Click button to perform a one time sync from WooCommerce to Square."
167
+ msgstr ""
168
+
169
+ #: includes/admin/class-wc-square-admin-integration.php:209
170
+ msgid "WC &rarr; Square"
171
+ msgstr ""
172
+
173
+ #: includes/admin/class-wc-square-admin-integration.php:214
174
+ msgid "Square &rarr; WC Sync"
175
+ msgstr ""
176
+
177
+ #: includes/admin/class-wc-square-admin-integration.php:215
178
+ msgid "Click button to perform a one time sync from Square to WooCommerce."
179
+ msgstr ""
180
+
181
+ #: includes/admin/class-wc-square-admin-integration.php:218
182
+ msgid "Square &rarr; WC"
183
+ msgstr ""
184
+
185
+ #: includes/admin/class-wc-square-admin-integration.php:242
186
+ #: includes/admin/class-wc-square-admin-integration.php:277
187
+ #: woocommerce-square.php:84 woocommerce-square.php:96
188
+ msgid "Cheatin&#8217; huh?"
189
+ msgstr ""
190
+
191
+ #: includes/admin/class-wc-square-admin-product-meta-box.php:59
192
+ msgid "(Square) Disable Sync"
193
+ msgstr ""
194
+
195
+ #: includes/admin/class-wc-square-admin-product-meta-box.php:61
196
+ msgid "Check box to disable this product from syncing."
197
+ msgstr ""
198
+
199
+ #: includes/admin/class-wc-square-admin-product-meta-box.php:108
200
+ msgid "Disable Sync"
201
+ msgstr ""
202
+
203
+ #: includes/admin/class-wc-square-admin-product-meta-box.php:113
204
+ msgid "— No Change —"
205
+ msgstr ""
206
+
207
+ #: includes/admin/class-wc-square-admin-product-meta-box.php:114
208
+ msgid "Yes"
209
+ msgstr ""
210
+
211
+ #: includes/admin/class-wc-square-admin-product-meta-box.php:115
212
+ msgid "No"
213
+ msgstr ""
214
+
215
+ #: includes/admin/class-wc-square-bulk-sync-handler.php:70
216
+ #: includes/admin/class-wc-square-bulk-sync-handler.php:185
217
+ msgid "Product Sync is disabled. Sync aborted."
218
+ msgstr ""
219
+
220
+ #: includes/admin/class-wc-square-bulk-sync-handler.php:75
221
+ #: includes/admin/class-wc-square-bulk-sync-handler.php:190
222
+ msgid ""
223
+ "cURL is not available. Sync aborted. Please contact your host to install "
224
+ "cURL."
225
+ msgstr ""
226
+
227
+ #: includes/admin/class-wc-square-bulk-sync-handler.php:150
228
+ #: includes/admin/class-wc-square-bulk-sync-handler.php:254
229
+ msgid "Sync Completed"
230
+ msgstr ""
231
+
232
+ #: includes/admin/class-wc-square-bulk-sync-handler.php:155
233
+ #: includes/admin/class-wc-square-bulk-sync-handler.php:259
234
+ msgid "Sync completed"
235
+ msgstr ""
236
+
237
+ #: includes/admin/class-wc-square-bulk-sync-handler.php:266
238
+ msgid "No Products to Sync."
239
+ msgstr ""
240
+
241
+ #: includes/admin/class-wc-square-bulk-sync-handler.php:443
242
+ msgid "%s - WooCommerce Square Sync Operation"
243
+ msgstr ""
244
+
245
+ #: includes/admin/class-wc-square-bulk-sync-handler.php:448
246
+ msgid "%s"
247
+ msgstr ""
248
+
249
+ #: includes/admin/class-wc-square-privacy.php:14
250
+ #: includes/admin/class-wc-square-privacy.php:15
251
+ msgid "WooCommerce Square Customer Data"
252
+ msgstr ""
253
+
254
+ #: includes/admin/class-wc-square-privacy.php:23
255
+ msgid ""
256
+ "By using this extension, you may be storing personal data or sharing data "
257
+ "with an external service. <a href=\"%s\" target=\"_blank\">Learn more about "
258
+ "how this works, including what you may want to include in your privacy "
259
+ "policy.</a>"
260
+ msgstr ""
261
+
262
+ #: includes/admin/class-wc-square-privacy.php:61
263
+ msgid "Customer Data"
264
+ msgstr ""
265
+
266
+ #: includes/admin/class-wc-square-privacy.php:65
267
+ msgid "Square customer id"
268
+ msgstr ""
269
+
270
+ #: includes/admin/class-wc-square-privacy.php:98
271
+ msgid "Square User Data Erased."
272
+ msgstr ""
273
+
274
+ #: includes/class-wc-square-connect.php:59
275
+ msgid "Square Sync Transients Cleared"
276
+ msgstr ""
277
+
278
+ #: includes/class-wc-square-connect.php:63
279
+ msgid "Square Sync Transients"
280
+ msgstr ""
281
+
282
+ #: includes/class-wc-square-connect.php:64
283
+ msgid "Clear all transients/cache"
284
+ msgstr ""
285
+
286
+ #: includes/class-wc-square-connect.php:65
287
+ msgid ""
288
+ "This will clear all Square Sync related transients/caches to start fresh. "
289
+ "Useful when sync failed halfway through."
290
+ msgstr ""
291
+
292
+ #: includes/class-wc-square-utils.php:50
293
+ msgid "Regular"
294
+ msgstr ""
295
+
296
+ #: includes/class-wc-square-wc-products.php:73
297
+ msgid "Invalid %s ID"
298
+ msgstr ""
299
+
300
+ #: includes/class-wc-square-wc-products.php:82
301
+ msgid "No %1$s found with the ID equal to %2$s"
302
+ msgstr ""
303
+
304
+ #: includes/class-wc-square-wc-products.php:90
305
+ msgid "Invalid %s"
306
+ msgstr ""
307
+
308
+ #: includes/class-wc-square-wc-products.php:98
309
+ msgid "You do not have permission to read this %s"
310
+ msgstr ""
311
+
312
+ #: includes/class-wc-square-wc-products.php:104
313
+ msgid "You do not have permission to edit this %s"
314
+ msgstr ""
315
+
316
+ #: includes/class-wc-square-wc-products.php:110
317
+ msgid "You do not have permission to delete this %s"
318
+ msgstr ""
319
+
320
+ #: includes/class-wc-square-wc-products.php:249
321
+ #: includes/class-wc-square-wc-products.php:536
322
+ #: includes/class-wc-square-wc-products.php:2287
323
+ msgid "No %1$s data specified to create %1$s"
324
+ msgstr ""
325
+
326
+ #: includes/class-wc-square-wc-products.php:256
327
+ msgid "You do not have permission to create products"
328
+ msgstr ""
329
+
330
+ #: includes/class-wc-square-wc-products.php:263
331
+ #: includes/class-wc-square-wc-products.php:2251
332
+ msgid "Missing parameter %s"
333
+ msgstr ""
334
+
335
+ #: includes/class-wc-square-wc-products.php:278
336
+ #: includes/class-wc-square-wc-products.php:405
337
+ msgid "Invalid product type - the product type must be any of these: %s"
338
+ msgstr ""
339
+
340
+ #: includes/class-wc-square-wc-products.php:354
341
+ #: includes/class-wc-square-wc-products.php:611
342
+ msgid "No %1$s data specified to edit %1$s"
343
+ msgstr ""
344
+
345
+ #: includes/class-wc-square-wc-products.php:450
346
+ #: includes/class-wc-square-wc-products.php:486
347
+ msgid "You do not have permission to read product categories"
348
+ msgstr ""
349
+
350
+ #: includes/class-wc-square-wc-products.php:481
351
+ msgid "Invalid product category ID"
352
+ msgstr ""
353
+
354
+ #: includes/class-wc-square-wc-products.php:492
355
+ msgid "A product category with the provided ID could not be found"
356
+ msgstr ""
357
+
358
+ #: includes/class-wc-square-wc-products.php:541
359
+ msgid "You do not have permission to create product categories"
360
+ msgstr ""
361
+
362
+ #: includes/class-wc-square-wc-products.php:561
363
+ msgid "Product category parent is invalid"
364
+ msgstr ""
365
+
366
+ #: includes/class-wc-square-wc-products.php:619
367
+ msgid "You do not have permission to edit product categories"
368
+ msgstr ""
369
+
370
+ #: includes/class-wc-square-wc-products.php:649
371
+ msgid "Could not edit the category"
372
+ msgstr ""
373
+
374
+ #: includes/class-wc-square-wc-products.php:926
375
+ #: includes/class-wc-square-wc-products.php:1369
376
+ msgid "The SKU already exists on another product"
377
+ msgstr ""
378
+
379
+ #: includes/class-wc-square-wc-products.php:1322
380
+ msgid "Variation #%1$s of %2$s"
381
+ msgstr ""
382
+
383
+ #: includes/class-wc-square-wc-products.php:1825
384
+ #: includes/class-wc-square-wc-products.php:1826
385
+ msgid "Placeholder"
386
+ msgstr ""
387
+
388
+ #: includes/class-wc-square-wc-products.php:1947
389
+ msgid "Invalid URL %s"
390
+ msgstr ""
391
+
392
+ #: includes/class-wc-square-wc-products.php:1959
393
+ #: includes/class-wc-square-wc-products.php:1961
394
+ msgid "Error getting remote image %s."
395
+ msgstr ""
396
+
397
+ #: includes/class-wc-square-wc-products.php:1959
398
+ msgid "Error: %s."
399
+ msgstr ""
400
+
401
+ #: includes/class-wc-square-wc-products.php:1982
402
+ msgid "Invalid image type."
403
+ msgstr ""
404
+
405
+ #: includes/class-wc-square-wc-products.php:1999
406
+ msgid "Zero size file downloaded"
407
+ msgstr ""
408
+
409
+ #: includes/class-wc-square-wc-products.php:2165
410
+ #: includes/class-wc-square-wc-products.php:2209
411
+ msgid "You do not have permission to read product attributes"
412
+ msgstr ""
413
+
414
+ #: includes/class-wc-square-wc-products.php:2204
415
+ msgid "Invalid product attribute ID"
416
+ msgstr ""
417
+
418
+ #: includes/class-wc-square-wc-products.php:2219
419
+ msgid "A product attribute with the provided ID could not be found"
420
+ msgstr ""
421
+
422
+ #: includes/class-wc-square-wc-products.php:2255
423
+ msgid "Slug \"%s\" is too long (28 characters max). Shorten it, please."
424
+ msgstr ""
425
+
426
+ #: includes/class-wc-square-wc-products.php:2257
427
+ msgid "Slug \"%s\" is not allowed because it is a reserved term. Change it, please."
428
+ msgstr ""
429
+
430
+ #: includes/class-wc-square-wc-products.php:2259
431
+ msgid "Slug \"%s\" is already in use. Change it, please."
432
+ msgstr ""
433
+
434
+ #: includes/class-wc-square-wc-products.php:2264
435
+ msgid ""
436
+ "Invalid product attribute type - the product attribute type must be any of "
437
+ "these: %s"
438
+ msgstr ""
439
+
440
+ #: includes/class-wc-square-wc-products.php:2269
441
+ msgid ""
442
+ "Invalid product attribute order_by type - the product attribute order_by "
443
+ "type must be any of these: %s"
444
+ msgstr ""
445
+
446
+ #: includes/class-wc-square-wc-products.php:2294
447
+ msgid "You do not have permission to create product attributes"
448
+ msgstr ""
449
+
450
+ #: includes/payment/class-wc-square-gateway.php:17
451
+ msgid ""
452
+ "Square works by adding payments fields in an iframe and then sending the "
453
+ "details to Square for verification and processing."
454
+ msgstr ""
455
+
456
+ #: includes/payment/class-wc-square-gateway.php:43
457
+ msgid ""
458
+ "STAGING MODE ENABLED. In staging mode, you can use the card number "
459
+ "4111111111111111 with any CVC and a valid expiration date."
460
+ msgstr ""
461
+
462
+ #: includes/payment/class-wc-square-gateway.php:84
463
+ #. translators: %1$s - transport layer securtiy wiki URL
464
+ msgid ""
465
+ "Square is enabled, but a SSL certificate is not detected. Your checkout may "
466
+ "not be secure! Please ensure your server has a valid <a href=\"%1$s\" "
467
+ "target=\"_blank\">SSL certificate</a>"
468
+ msgstr ""
469
+
470
+ #: includes/payment/class-wc-square-gateway.php:122
471
+ msgid "Enable/Disable"
472
+ msgstr ""
473
+
474
+ #: includes/payment/class-wc-square-gateway.php:123
475
+ msgid "Enable Square"
476
+ msgstr ""
477
+
478
+ #: includes/payment/class-wc-square-gateway.php:129
479
+ msgid "Title"
480
+ msgstr ""
481
+
482
+ #: includes/payment/class-wc-square-gateway.php:131
483
+ msgid "This controls the title which the user sees during checkout."
484
+ msgstr ""
485
+
486
+ #: includes/payment/class-wc-square-gateway.php:132
487
+ msgid "Credit card (Square)"
488
+ msgstr ""
489
+
490
+ #: includes/payment/class-wc-square-gateway.php:135
491
+ msgid "Description"
492
+ msgstr ""
493
+
494
+ #: includes/payment/class-wc-square-gateway.php:137
495
+ msgid "This controls the description which the user sees during checkout."
496
+ msgstr ""
497
+
498
+ #: includes/payment/class-wc-square-gateway.php:138
499
+ msgid "Pay with your credit card via Square."
500
+ msgstr ""
501
+
502
+ #: includes/payment/class-wc-square-gateway.php:141
503
+ msgid "Delay Capture"
504
+ msgstr ""
505
+
506
+ #: includes/payment/class-wc-square-gateway.php:142
507
+ msgid "Enable Delay Capture"
508
+ msgstr ""
509
+
510
+ #: includes/payment/class-wc-square-gateway.php:144
511
+ msgid ""
512
+ "When enabled, the request will only perform an Auth on the provided card. "
513
+ "You can then later perform either a Capture or Void."
514
+ msgstr ""
515
+
516
+ #: includes/payment/class-wc-square-gateway.php:148
517
+ msgid "Create Customer"
518
+ msgstr ""
519
+
520
+ #: includes/payment/class-wc-square-gateway.php:149
521
+ msgid "Enable Create Customer"
522
+ msgstr ""
523
+
524
+ #: includes/payment/class-wc-square-gateway.php:151
525
+ msgid "When enabled, processing a payment will create a customer profile on Square."
526
+ msgstr ""
527
+
528
+ #: includes/payment/class-wc-square-gateway.php:189
529
+ msgid "Card Number"
530
+ msgstr ""
531
+
532
+ #: includes/payment/class-wc-square-gateway.php:194
533
+ msgid "Expiry (MM/YY)"
534
+ msgstr ""
535
+
536
+ #: includes/payment/class-wc-square-gateway.php:195
537
+ #: includes/payment/class-wc-square-gateway.php:265
538
+ msgid "MM / YY"
539
+ msgstr ""
540
+
541
+ #: includes/payment/class-wc-square-gateway.php:199
542
+ msgid "Card Code"
543
+ msgstr ""
544
+
545
+ #: includes/payment/class-wc-square-gateway.php:200
546
+ #: includes/payment/class-wc-square-gateway.php:266
547
+ msgid "CVV"
548
+ msgstr ""
549
+
550
+ #: includes/payment/class-wc-square-gateway.php:204
551
+ #: includes/payment/class-wc-square-gateway.php:205
552
+ #: includes/payment/class-wc-square-gateway.php:267
553
+ msgid "Card Postal Code"
554
+ msgstr ""
555
+
556
+ #: includes/payment/class-wc-square-gateway.php:264
557
+ msgid "•••• •••• •••• ••••"
558
+ msgstr ""
559
+
560
+ #: includes/payment/class-wc-square-gateway.php:326
561
+ #: includes/payment/class-wc-square-gateway.php:333
562
+ #: includes/payment/class-wc-square-gateway.php:357
563
+ msgid ""
564
+ "Error: Square was unable to complete the transaction. Please try again "
565
+ "later or use another means of payment."
566
+ msgstr ""
567
+
568
+ #: includes/payment/class-wc-square-gateway.php:338
569
+ msgid "Payment Error: "
570
+ msgstr ""
571
+
572
+ #: includes/payment/class-wc-square-gateway.php:371
573
+ #: includes/payment/class-wc-square-payments.php:136
574
+ #. translators: %s - transaction id
575
+ msgid "Square charge complete (Charge ID: %s)"
576
+ msgstr ""
577
+
578
+ #: includes/payment/class-wc-square-gateway.php:381
579
+ #. translators: %s - transaction id
580
+ msgid ""
581
+ "Square charge authorized (Authorized ID: %s). Process order to take "
582
+ "payment, or cancel to remove the pre-authorization."
583
+ msgstr ""
584
+
585
+ #: includes/payment/class-wc-square-gateway.php:404
586
+ #: includes/payment/class-wc-square-gateway.php:543
587
+ #. translators: %s - error message
588
+ msgid "Error: %s"
589
+ msgstr ""
590
+
591
+ #: includes/payment/class-wc-square-gateway.php:436
592
+ msgid "Guest"
593
+ msgstr ""
594
+
595
+ #: includes/payment/class-wc-square-gateway.php:456
596
+ #: includes/payment/class-wc-square-gateway.php:458
597
+ #: includes/payment/class-wc-square-gateway.php:464
598
+ #: includes/payment/class-wc-square-gateway.php:466
599
+ #. translators: %s - error message
600
+ #. translators: %s - errors
601
+ #. translators: %s - errors
602
+ msgid "Error creating customer: %s"
603
+ msgstr ""
604
+
605
+ #: includes/payment/class-wc-square-gateway.php:473
606
+ #. translators: %s - customer id
607
+ msgid "Customer created on Square: %s"
608
+ msgstr ""
609
+
610
+ #: includes/payment/class-wc-square-gateway.php:531
611
+ #. translators: %1$s - refund amount, %2$s - refund id, %3$s - refund reason
612
+ msgid "Refunded %1$s - Refund ID: %2$s - Reason: %3$s"
613
+ msgstr ""
614
+
615
+ #: includes/payment/class-wc-square-payments.php:88
616
+ msgid "Capture Charge"
617
+ msgstr ""
618
+
619
+ #: includes/payment/class-wc-square-payments.php:128
620
+ #: includes/payment/class-wc-square-payments.php:132
621
+ msgid "Unable to capture charge!"
622
+ msgstr ""
623
+
624
+ #: includes/payment/class-wc-square-payments.php:142
625
+ msgid "Error unable to capture charge: %s"
626
+ msgstr ""
627
+
628
+ #: includes/payment/class-wc-square-payments.php:172
629
+ #: includes/payment/class-wc-square-payments.php:175
630
+ msgid "Unable to void charge!"
631
+ msgstr ""
632
+
633
+ #: includes/payment/class-wc-square-payments.php:179
634
+ msgid "Square charge voided! (Charge ID: %s)"
635
+ msgstr ""
636
+
637
+ #: includes/payment/class-wc-square-payments.php:185
638
+ msgid "Unable to void charge!: %s"
639
+ msgstr ""
640
+
641
+ #: woocommerce-square.php:211
642
+ msgid ""
643
+ "Square requires that the <a href=\"%s\">base country/region</a> is "
644
+ "Australia, Canada, Japan, United Kingdom, or United States."
645
+ msgstr ""
646
+
647
+ #: woocommerce-square.php:219
648
+ msgid ""
649
+ "Square requires that the <a href=\"%s\">currency</a> is set to AUD, CAD, "
650
+ "GBP, JPY, or USD."
651
+ msgstr ""
652
+
653
+ #: woocommerce-square.php:332
654
+ msgid ""
655
+ "This process may take awhile depending on the amount of items that need to "
656
+ "be synced. Please do not close this tab/window or else the sync will "
657
+ "terminate. Click OK to continue to sync."
658
+ msgstr ""
659
+
660
+ #: woocommerce-square.php:390
661
+ msgid ""
662
+ "WooCommerce Square Plugin requires WooCommerce to be installed and active. "
663
+ "You can download %s here."
664
+ msgstr ""
665
+
666
+ #: woocommerce-square.php:408
667
+ msgid ""
668
+ "WooCommerce Square is almost ready. To get started, %1$sconnect your Square "
669
+ "Account.%2$s"
670
+ msgstr ""
671
+
672
+ #: woocommerce-square.php:412
673
+ msgid ""
674
+ "WooCommerce Square is almost ready. Please %1$sset your business "
675
+ "location.%2$s"
676
+ msgstr ""
677
+
678
+ #. Plugin Name of the plugin/theme
679
+ msgid "WooCommerce Square"
680
+ msgstr ""
681
+
682
+ #. Plugin URI of the plugin/theme
683
+ msgid "https://woocommerce.com/products/square/"
684
+ msgstr ""
685
+
686
+ #. Description of the plugin/theme
687
+ msgid ""
688
+ "Adds ability to sync inventory between WooCommerce and Square POS. In "
689
+ "addition, you can also make purchases through the Square payment gateway."
690
+ msgstr ""
691
+
692
+ #. Author of the plugin/theme
693
+ msgid "WooCommerce"
694
+ msgstr ""
695
+
696
+ #. Author URI of the plugin/theme
697
+ msgid "https://www.woocommerce.com/"
698
+ msgstr ""
trunk/readme.txt ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ === WooCommerce Square ===
2
+ Contributors: automattic, royho, woothemes, bor0
3
+ Tags: credit card, square, woocommerce, inventory sync
4
+ Requires at least: 4.4
5
+ Tested up to: 5.1
6
+ Requires PHP: 5.6
7
+ Stable tag: 1.0.37
8
+ License: GPLv3
9
+ License URI: https://www.gnu.org/licenses/gpl-3.0.html
10
+
11
+ Adds ability to sync inventory between WooCommerce and Square POS. In addition, you can also make purchases through the Square payment gateway.
12
+
13
+ == Description ==
14
+
15
+ Adds ability to sync inventory between WooCommerce and Square POS. In addition, you can also make purchases through the Square payment gateway.
16
+
17
+ = Take Credit card payments easily and directly on your store =
18
+
19
+ The Square plugin extends WooCommerce allowing you to take payments directly on your store via Square’s API.
20
+
21
+ == Installation ==
22
+
23
+ You can download an [older version of this gateway for older versions of WooCommerce from here](https://wordpress.org/plugins/woocommerce-square/developers/).
24
+
25
+ = Automatic installation =
26
+
27
+ Automatic installation is the easiest option as WordPress handles the file transfers itself and you don’t need to leave your web browser. To
28
+ do an automatic install of, log in to your WordPress dashboard, navigate to the Plugins menu and click Add New.
29
+
30
+ In the search field type “WooCommerce Square” and click Search Plugins. Once you’ve found our plugin you can view details about it such as the point release, rating and description. Most importantly of course, you can install it by simply clicking "Install Now".
31
+
32
+ = Manual installation =
33
+
34
+ The manual installation method involves downloading our plugin and uploading it to your web server via your favorite FTP application. The WordPress codex contains [instructions on how to do this here](http://codex.wordpress.org/Managing_Plugins#Manual_Plugin_Installation).
35
+
36
+ = Updating =
37
+
38
+ Automatic updates should work like a charm; as always though, ensure you backup your site just in case.
39
+
40
+ == Frequently Asked Questions ==
41
+
42
+ = Does this require an SSL certificate? =
43
+
44
+ Yes! SSL certificate must be installed on your site to use Square.
45
+
46
+ = Where can I find documentation? =
47
+
48
+ For help setting up and configuring, please refer to our [user guide](https://docs.woocommerce.com/document/woocommerce-square/)
49
+
50
+ = Where can I get support or talk to other users? =
51
+
52
+ If you get stuck, you can ask for help in the Plugin Forum.
53
+
54
+ == Screenshots ==
55
+
56
+ 1. The settings panel.
57
+
58
+ == Changelog ==
59
+
60
+ = 1.0.37 - 2019-04-16 =
61
+ * Fix - Use correct assets loading scheme.
62
+
63
+ = 1.0.36 - 2019-04-15 =
64
+ * Tweak - WC 3.6 compatibility.
65
+
66
+ = 1.0.35 - 2019-02-01 =
67
+ * Fix - Idempotency key reuse issue when checking out.
68
+
69
+ = 1.0.34 - 2018-11-07 =
70
+ * Update - Fieldset tag to div tag in payment box to prevent unwanted styling.
71
+ * Fix - Provide unique idempotency ID to the order instead of random unique number.
72
+ * Update - WP tested up to version 5.0
73
+
74
+ = 1.0.33 - 2018-09-27 =
75
+ * Update - WC tested up to version 3.5
76
+
77
+ = 1.0.32 - 2018-08-23 =
78
+ * Fix - UK/GB localed does not support Diners/Discover, so do not show these brands on checkout.
79
+
80
+ == Upgrade Notice ==
81
+
82
+ = 1.0.25 =
83
+ * Public Release!
trunk/uninstall.php ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) {
3
+ exit; // Exit if accessed directly
4
+ }
5
+
6
+ // if uninstall not called from WordPress exit
7
+ if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
8
+ exit;
9
+ }
10
+
11
+ /*
12
+ * Only remove ALL product and page data if WC_REMOVE_ALL_DATA constant is set to true in user's
13
+ * wp-config.php. This is to prevent data loss when deleting the plugin from the backend
14
+ * and to ensure only the site owner can perform this action.
15
+ */
16
+ if ( defined( 'WC_REMOVE_ALL_DATA' ) && true === WC_REMOVE_ALL_DATA ) {
17
+ delete_option( 'wc_square_endpoint_set' );
18
+ delete_option( 'woocommerce_squareconnect_settings' );
19
+ delete_option( 'wc_square_polling' );
20
+ delete_transient( 'wc_square_polling' );
21
+ delete_transient( 'wc_square_locations' );
22
+ delete_transient( 'wc_square_manual_sync_processing' );
23
+ delete_option( 'woocommerce_square_merchant_access_token' );
24
+ }
trunk/woocommerce-square.php ADDED
@@ -0,0 +1,421 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Plugin Name: WooCommerce Square
4
+ * Version: 1.0.37
5
+ * Plugin URI: https://woocommerce.com/products/square/
6
+ * Description: Adds ability to sync inventory between WooCommerce and Square POS. In addition, you can also make purchases through the Square payment gateway.
7
+ * Author: WooCommerce
8
+ * Author URI: https://www.woocommerce.com/
9
+ * Requires at least: 4.5.0
10
+ * Tested up to: 5.0
11
+ * WC requires at least: 2.6
12
+ * WC tested up to: 3.6
13
+ * Text Domain: woocommerce-square
14
+ * Domain Path: /languages
15
+ *
16
+ * @package WordPress
17
+ * @author WooCommerce
18
+ */
19
+
20
+ if ( ! defined( 'ABSPATH' ) ) {
21
+ exit;
22
+ }
23
+
24
+ if ( ! class_exists( 'Woocommerce_Square' ) ) :
25
+
26
+ define( 'WC_SQUARE_VERSION', '1.0.37' );
27
+
28
+ /**
29
+ * Main class.
30
+ *
31
+ * @package Woocommerce_Square
32
+ * @since 1.0.0
33
+ * @version 1.0.0
34
+ */
35
+ class Woocommerce_Square {
36
+
37
+ private static $_instance = null;
38
+
39
+ /**
40
+ * @var WC_Integration
41
+ */
42
+ public $integration;
43
+
44
+ /**
45
+ * @var WC_Square_Client
46
+ */
47
+ public $square_client;
48
+
49
+ /**
50
+ * @var WC_Square_Connect
51
+ */
52
+ public $square_connect;
53
+
54
+ /**
55
+ * @var WC_Square_Sync_To_Square_WordPress_Hooks
56
+ */
57
+ protected $wc_to_square_wp_hooks;
58
+
59
+ /**
60
+ * Get the single instance aka Singleton
61
+ *
62
+ * @access public
63
+ * @since 1.0.0
64
+ * @version 1.0.0
65
+ * @return bool
66
+ */
67
+ public static function instance() {
68
+ if ( is_null( self::$_instance ) ) {
69
+ self::$_instance = new self();
70
+ }
71
+
72
+ return self::$_instance;
73
+ }
74
+
75
+ /**
76
+ * Prevent cloning
77
+ *
78
+ * @access public
79
+ * @since 1.0.0
80
+ * @version 1.0.0
81
+ * @return bool
82
+ */
83
+ public function __clone() {
84
+ _doing_it_wrong( __FUNCTION__, __( 'Cheatin&#8217; huh?', 'woocommerce-square' ), WC_SQUARE_VERSION );
85
+ }
86
+
87
+ /**
88
+ * Prevent unserializing instances
89
+ *
90
+ * @access public
91
+ * @since 1.0.0
92
+ * @version 1.0.0
93
+ * @return bool
94
+ */
95
+ public function __wakeup() {
96
+ _doing_it_wrong( __FUNCTION__, __( 'Cheatin&#8217; huh?', 'woocommerce-square' ), WC_SQUARE_VERSION );
97
+ }
98
+
99
+ /**
100
+ * Woocommerce_Square constructor.
101
+ */
102
+ private function __construct() {
103
+
104
+ add_action( 'woocommerce_loaded', array( $this, 'bootstrap' ) );
105
+
106
+ }
107
+
108
+ public function bootstrap() {
109
+ add_action( 'admin_notices', array( $this, 'check_environment' ) );
110
+
111
+ $this->define_constants();
112
+ $this->includes();
113
+ $this->init();
114
+ $this->init_hooks();
115
+
116
+ do_action( 'wc_square_loaded' );
117
+
118
+ }
119
+
120
+ public function init() {
121
+ $this->integration = new WC_Square_Integration();
122
+
123
+ $square_client = new WC_Square_Client();
124
+
125
+ $access_token = get_option( 'woocommerce_square_merchant_access_token' );
126
+ $square_client->set_access_token( $access_token );
127
+ $square_client->set_merchant_id( $this->integration->get_option( 'location' ) );
128
+ $this->square_client = $square_client;
129
+
130
+ $this->square_connect = new WC_Square_Connect( $square_client );
131
+
132
+ $wc_to_square_sync = new WC_Square_Sync_To_Square( $this->square_connect );
133
+ $square_to_wc_sync = new WC_Square_Sync_From_Square( $this->square_connect );
134
+
135
+ $inventory_poll = new WC_Square_Inventory_Poll( $this->integration, $square_to_wc_sync );
136
+
137
+ if ( is_admin() ) {
138
+
139
+ $bulk_handler = new WC_Square_Bulk_Sync_Handler( $this->square_connect, $wc_to_square_sync, $square_to_wc_sync );
140
+
141
+ }
142
+
143
+ $this->wc_to_square_wp_hooks = new WC_Square_Sync_To_Square_WordPress_Hooks( $this->integration, $wc_to_square_sync );
144
+ }
145
+
146
+ /**
147
+ * Define constants
148
+ *
149
+ * @access public
150
+ * @since 1.0.0
151
+ * @version 1.0.0
152
+ * @return bool
153
+ */
154
+ public function define_constants() {
155
+ define( 'WC_SQUARE_PATH', untrailingslashit( plugin_dir_path( __FILE__ ) ) );
156
+ define( 'WC_SQUARE_PLUGIN_URL', untrailingslashit( plugin_dir_url( __FILE__ ) ) );
157
+
158
+ // if using staging, define this in wp-config.php
159
+ if ( ! defined( 'WC_SQUARE_ENABLE_STAGING' ) ) {
160
+ define( 'WC_SQUARE_ENABLE_STAGING', false );
161
+ }
162
+
163
+ return true;
164
+ }
165
+
166
+ /**
167
+ * Check if country is set to allowed country.
168
+ *
169
+ * @since 1.0.10
170
+ * @version 1.0.10
171
+ */
172
+ public function is_allowed_countries() {
173
+ if ( 'US' !== WC()->countries->get_base_country() && 'CA' !== WC()->countries->get_base_country() && 'AU' !== WC()->countries->get_base_country() && 'GB' !== WC()->countries->get_base_country() && 'JP' !== WC()->countries->get_base_country() ) {
174
+ return false;
175
+ }
176
+
177
+ return true;
178
+ }
179
+
180
+ /**
181
+ * Check if currency is set to allowed currency.
182
+ *
183
+ * @since 1.0.10
184
+ * @version 1.0.10
185
+ */
186
+ public function is_allowed_currencies() {
187
+ if ( 'USD' !== get_woocommerce_currency() && 'CAD' !== get_woocommerce_currency() && 'AUD' !== get_woocommerce_currency() && 'GBP' !== get_woocommerce_currency() && 'JPY' !== get_woocommerce_currency() ) {
188
+ return false;
189
+ }
190
+
191
+ return true;
192
+ }
193
+
194
+ /**
195
+ * Check required environment
196
+ *
197
+ * @access public
198
+ * @since 1.0.10
199
+ * @version 1.0.10
200
+ * @return null
201
+ */
202
+ public function check_environment() {
203
+ if ( ! current_user_can( 'manage_woocommerce' ) ) {
204
+ return;
205
+ }
206
+
207
+ if ( ! $this->is_allowed_countries() ) {
208
+ $admin_page = 'wc-settings';
209
+
210
+ echo '<div class="error">
211
+ <p>' . sprintf( __( 'Square requires that the <a href="%s">base country/region</a> is Australia, Canada, Japan, United Kingdom, or United States.', 'woocommerce-square' ), admin_url( 'admin.php?page=' . $admin_page . '&tab=general' ) ) . '</p>
212
+ </div>';
213
+ }
214
+
215
+ if ( ! $this->is_allowed_currencies() ) {
216
+ $admin_page = 'wc-settings';
217
+
218
+ echo '<div class="error">
219
+ <p>' . sprintf( __( 'Square requires that the <a href="%s">currency</a> is set to AUD, CAD, GBP, JPY, or USD.', 'woocommerce-square' ), admin_url( 'admin.php?page=' . $admin_page . '&tab=general' ) ) . '</p>
220
+ </div>';
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Include all files needed
226
+ *
227
+ * @access public
228
+ * @since 1.0.0
229
+ * @version 1.0.0
230
+ * @return bool
231
+ */
232
+ public function includes() {
233
+ require_once( dirname( __FILE__ ) . '/includes/admin/class-wc-square-privacy.php' );
234
+ require_once( dirname( __FILE__ ) . '/includes/class-wc-square-install.php' );
235
+ require_once( dirname( __FILE__ ) . '/includes/class-wc-square-deactivation.php' );
236
+ require_once( dirname( __FILE__ ) . '/includes/class-wc-square-sync-logger.php' );
237
+ require_once( dirname( __FILE__ ) . '/includes/class-wc-square-wc-products.php' );
238
+ require_once( dirname( __FILE__ ) . '/includes/class-wc-square-connect.php' );
239
+ require_once( dirname( __FILE__ ) . '/includes/class-wc-square-sync-to-square.php' );
240
+ require_once( dirname( __FILE__ ) . '/includes/class-wc-square-sync-from-square.php' );
241
+ require_once( dirname( __FILE__ ) . '/includes/admin/class-wc-square-admin-integration.php' );
242
+ require_once( dirname( __FILE__ ) . '/includes/class-wc-square-sync-to-square-wp-hooks.php' );
243
+ require_once( dirname( __FILE__ ) . '/includes/class-wc-square-client.php' );
244
+ require_once( dirname( __FILE__ ) . '/includes/class-wc-square-utils.php' );
245
+ require_once( dirname( __FILE__ ) . '/includes/class-wc-square-inventory-poll.php' );
246
+ require_once( dirname( __FILE__ ) . '/includes/payment/class-wc-square-payment-logger.php' );
247
+ require_once( dirname( __FILE__ ) . '/includes/payment/class-wc-square-payments.php' );
248
+
249
+ if ( is_admin() ) {
250
+ require_once( dirname( __FILE__ ) . '/includes/admin/class-wc-square-bulk-sync-handler.php' );
251
+ require_once( dirname( __FILE__ ) . '/includes/admin/class-wc-square-admin-product-meta-box.php' );
252
+ }
253
+
254
+ }
255
+
256
+ /**
257
+ * Add integration settings page
258
+ *
259
+ * @access public
260
+ * @since 1.0.0
261
+ * @version 1.0.0
262
+ * @return bool
263
+ */
264
+ public function include_integration( $integrations ) {
265
+ // Square only supports US and Canada for now.
266
+ if ( $this->is_allowed_currencies() && $this->is_allowed_countries() ) {
267
+ $integrations[] = $this->integration;
268
+ }
269
+
270
+ return $integrations;
271
+
272
+ }
273
+
274
+ /**
275
+ * Initializes hooks
276
+ *
277
+ * @access public
278
+ * @since 1.0.0
279
+ * @version 1.0.0
280
+ * @return bool
281
+ */
282
+ public function init_hooks() {
283
+
284
+ register_deactivation_hook( __FILE__, array( 'WC_Square_Deactivation', 'deactivate' ) );
285
+
286
+ if ( class_exists( 'WooCommerce' ) ) {
287
+
288
+ add_filter( 'woocommerce_integrations', array( $this, 'include_integration' ) );
289
+
290
+ add_action( 'plugins_loaded', array( $this, 'load_plugin_textdomain' ) );
291
+
292
+ add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) );
293
+
294
+ add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_styles' ) );
295
+
296
+ add_action( 'woocommerce_square_bulk_syncing_square_to_wc', array( $this->wc_to_square_wp_hooks, 'disable' ) );
297
+
298
+ add_action( 'admin_notices', array( $this, 'is_connected_to_square' ) );
299
+
300
+ } else {
301
+
302
+ add_action( 'admin_notices', array( $this, 'woocommerce_missing_notice' ) );
303
+
304
+ }
305
+
306
+ }
307
+
308
+ /**
309
+ * Loads the admin JS scripts
310
+ *
311
+ * @access public
312
+ * @since 1.0.0
313
+ * @version 1.0.0
314
+ * @return bool
315
+ */
316
+ public function enqueue_admin_scripts() {
317
+ $current_screen = get_current_screen();
318
+
319
+ $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
320
+
321
+ wp_register_script( 'wc-square-admin-scripts', WC_SQUARE_PLUGIN_URL . '/assets/js/wc-square-admin-scripts' . $suffix . '.js', array( 'jquery' ), WC_SQUARE_VERSION, true );
322
+
323
+ if ( 'woocommerce_page_wc-settings' === $current_screen->id ) {
324
+
325
+ wp_enqueue_script( 'wc-square-admin-scripts' );
326
+
327
+ $localized_vars = array(
328
+ 'admin_ajax_url' => admin_url( 'admin-ajax.php' ),
329
+ 'ajaxSyncNonce' => wp_create_nonce( 'square-sync' ),
330
+ 'country_currency' => $this->is_allowed_countries() && $this->is_allowed_currencies(),
331
+ 'i18n' => array(
332
+ 'confirm_sync' => __( 'This process may take awhile depending on the amount of items that need to be synced. Please do not close this tab/window or else the sync will terminate. Click OK to continue to sync.', 'woocommerce-square' ),
333
+ ),
334
+ );
335
+
336
+ wp_localize_script( 'wc-square-admin-scripts', 'wc_square_local', $localized_vars );
337
+ }
338
+
339
+ return true;
340
+ }
341
+
342
+ /**
343
+ * Loads the admin CSS styles
344
+ *
345
+ * @access public
346
+ * @since 1.0.0
347
+ * @version 1.0.0
348
+ * @return bool
349
+ */
350
+ public function enqueue_admin_styles() {
351
+ $current_screen = get_current_screen();
352
+
353
+ wp_register_style( 'wc-square-admin-styles', WC_SQUARE_PLUGIN_URL . '/assets/css/wc-square-admin-styles.css', null, WC_SQUARE_VERSION );
354
+
355
+ if ( 'woocommerce_page_wc-settings' === $current_screen->id ) {
356
+
357
+ wp_enqueue_style( 'wc-square-admin-styles' );
358
+ }
359
+
360
+ return true;
361
+ }
362
+
363
+ /**
364
+ * Load the plugin text domain for translation.
365
+ *
366
+ * @access public
367
+ * @since 1.0.0
368
+ * @version 1.0.0
369
+ * @return bool
370
+ */
371
+ public function load_plugin_textdomain() {
372
+ $locale = apply_filters( 'woocommerce_square_plugin_locale', get_locale(), 'woocommerce-square' );
373
+
374
+ load_textdomain( 'woocommerce-square', trailingslashit( WP_LANG_DIR ) . 'woocommerce-square/woocommerce-square' . '-' . $locale . '.mo' );
375
+
376
+ load_plugin_textdomain( 'woocommerce-square', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
377
+
378
+ return true;
379
+ }
380
+
381
+ /**
382
+ * WooCommerce fallback notice.
383
+ *
384
+ * @access public
385
+ * @since 1.0.0
386
+ * @version 1.0.0
387
+ * @return string
388
+ */
389
+ public function woocommerce_missing_notice() {
390
+ echo '<div class="error"><p>' . sprintf( __( 'WooCommerce Square Plugin requires WooCommerce to be installed and active. You can download %s here.', 'woocommerce-square' ), '<a href="https://woocommerce.com/woocommerce/" target="_blank">WooCommerce</a>' ) . '</p></div>';
391
+
392
+ return true;
393
+ }
394
+
395
+ /**
396
+ * Shows a notice when the site is not yet connected to square.
397
+ *
398
+ * @access public
399
+ * @since 1.0.0
400
+ * @version 1.0.0
401
+ * @return string
402
+ */
403
+ public function is_connected_to_square() {
404
+ $settings = get_option( 'woocommerce_squareconnect_settings', '' );
405
+ $existing_token = get_option( 'woocommerce_square_merchant_access_token' );
406
+
407
+ if ( empty( $existing_token ) ) {
408
+ echo '<div class="error"><p>' . sprintf( __( 'WooCommerce Square is almost ready. To get started, %1$sconnect your Square Account.%2$s', 'woocommerce-square' ), '<a href="' . admin_url( 'admin.php?page=wc-settings&tab=integration&section=squareconnect' ) . '">', '</a>' ) . '</p></div>';
409
+ }
410
+
411
+ if ( empty( $settings ) || empty( $settings['location'] ) ) {
412
+ echo '<div class="error"><p>' . sprintf( __( 'WooCommerce Square is almost ready. Please %1$sset your business location.%2$s', 'woocommerce-square' ), '<a href="' . admin_url( 'admin.php?page=wc-settings&tab=integration&section=squareconnect' ) . '">', '</a>' ) . '</p></div>';
413
+ }
414
+
415
+ return true;
416
+ }
417
+ }
418
+
419
+ Woocommerce_Square::instance();
420
+
421
+ endif;
woocommerce-square.php CHANGED
@@ -1,7 +1,7 @@
1
  <?php
2
  /**
3
  * Plugin Name: WooCommerce Square
4
- * Version: 1.0.36
5
  * Plugin URI: https://woocommerce.com/products/square/
6
  * Description: Adds ability to sync inventory between WooCommerce and Square POS. In addition, you can also make purchases through the Square payment gateway.
7
  * Author: WooCommerce
@@ -23,7 +23,7 @@ if ( ! defined( 'ABSPATH' ) ) {
23
 
24
  if ( ! class_exists( 'Woocommerce_Square' ) ) :
25
 
26
- define( 'WC_SQUARE_VERSION', '1.0.36' );
27
 
28
  /**
29
  * Main class.
@@ -153,7 +153,7 @@ if ( ! class_exists( 'Woocommerce_Square' ) ) :
153
  */
154
  public function define_constants() {
155
  define( 'WC_SQUARE_PATH', untrailingslashit( plugin_dir_path( __FILE__ ) ) );
156
- define( 'WC_SQUARE_PLUGIN_URL', untrailingslashit( plugins_url( basename( plugin_dir_path( __FILE__ ) ), basename( __FILE__ ) ) ) );
157
 
158
  // if using staging, define this in wp-config.php
159
  if ( ! defined( 'WC_SQUARE_ENABLE_STAGING' ) ) {
1
  <?php
2
  /**
3
  * Plugin Name: WooCommerce Square
4
+ * Version: 1.0.37
5
  * Plugin URI: https://woocommerce.com/products/square/
6
  * Description: Adds ability to sync inventory between WooCommerce and Square POS. In addition, you can also make purchases through the Square payment gateway.
7
  * Author: WooCommerce
23
 
24
  if ( ! class_exists( 'Woocommerce_Square' ) ) :
25
 
26
+ define( 'WC_SQUARE_VERSION', '1.0.37' );
27
 
28
  /**
29
  * Main class.
153
  */
154
  public function define_constants() {
155
  define( 'WC_SQUARE_PATH', untrailingslashit( plugin_dir_path( __FILE__ ) ) );
156
+ define( 'WC_SQUARE_PLUGIN_URL', untrailingslashit( plugin_dir_url( __FILE__ ) ) );
157
 
158
  // if using staging, define this in wp-config.php
159
  if ( ! defined( 'WC_SQUARE_ENABLE_STAGING' ) ) {