WooCommerce Square - Version 1.0.25

Version Description

  • 2018-01-29 =
  • Tweaks - Error handling.
  • Public release on .org
Download this release

Release Info

Developer royho
Plugin Icon 128x128 WooCommerce Square
Version 1.0.25
Comparing to
See all releases

Version 1.0.25

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
+ }
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
+ }
assets/js/wc-square-admin-scripts.js ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: function() {
49
+
50
+ $( '.woocommerce_page_wc-settings' ).on( 'click', '#wc-to-square, #square-to-wc', function( e ) {
51
+ e.preventDefault();
52
+
53
+ var confirmed = confirm( wc_square_local.i18n.confirm_sync );
54
+
55
+ if ( ! confirmed ) {
56
+ return;
57
+ }
58
+
59
+ var page = $( this ).parents( 'table.form-table' ),
60
+ progress_bar = $( '<div class="wc-square-progress-bar wc-square-stripes"><span class="step"></span></div>' );
61
+
62
+ // remove the progress bar on each trigger
63
+ $( '.wc-square-progress-bar' ).remove();
64
+
65
+ page.block({
66
+ message: null,
67
+ overlayCSS: {
68
+ background: '#fff',
69
+ opacity: 0.6
70
+ }
71
+ });
72
+
73
+ // add the progress bar
74
+ page.after( progress_bar );
75
+
76
+ $.wc_square_admin.sync( 0, $( this ).attr( 'id' ) );
77
+ });
78
+
79
+ $( document.body ).on( 'change', '#woocommerce_square_testmode', function() {
80
+ if ( $( this ).is( ':checked' ) ) {
81
+ $( '#woocommerce_square_application_id' ).parents( 'tr' ).eq(0).hide();
82
+ $( '#woocommerce_square_token' ).parents( 'tr' ).eq(0).hide();
83
+
84
+ $( '#woocommerce_square_sandbox_application_id' ).parents( 'tr' ).eq(0).show();
85
+ $( '#woocommerce_square_sandbox_token' ).parents( 'tr' ).eq(0).show();
86
+ } else {
87
+ $( '#woocommerce_square_application_id' ).parents( 'tr' ).eq(0).show();
88
+ $( '#woocommerce_square_token' ).parents( 'tr' ).eq(0).show();
89
+
90
+ $( '#woocommerce_square_sandbox_application_id' ).parents( 'tr' ).eq(0).hide();
91
+ $( '#woocommerce_square_sandbox_token' ).parents( 'tr' ).eq(0).hide();
92
+ }
93
+ });
94
+
95
+ $( '#woocommerce_square_testmode' ).trigger( 'change' );
96
+
97
+ $( document.body ).on( 'change', '#woocommerce_squareconnect_sync_products', function() {
98
+ if ( $( this ).is( ':checked' ) ) {
99
+ $( '#woocommerce_squareconnect_sync_categories' ).parents( 'tr' ).eq(0).show();
100
+ $( '#woocommerce_squareconnect_sync_inventory' ).parents( 'tr' ).eq(0).show();
101
+ $( '#woocommerce_squareconnect_sync_images' ).parents( 'tr' ).eq(0).show();
102
+ } else {
103
+ $( '#woocommerce_squareconnect_sync_categories' ).parents( 'tr' ).eq(0).hide();
104
+ $( '#woocommerce_squareconnect_sync_inventory' ).parents( 'tr' ).eq(0).hide();
105
+ $( '#woocommerce_squareconnect_sync_images' ).parents( 'tr' ).eq(0).hide();
106
+ }
107
+ });
108
+
109
+ $( '#woocommerce_squareconnect_sync_products' ).trigger( 'change' );
110
+ }
111
+ }; // close namespace
112
+
113
+ $.wc_square_admin.init();
114
+ // end document ready
115
+ });
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:function(){e(".woocommerce_page_wc-settings").on("click","#wc-to-square, #square-to-wc",function(o){o.preventDefault();if(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(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()});
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 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 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' ).size() === 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' ).size() === 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 ) );
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 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 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").size()||(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").size()||(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);
changelog.txt ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *** WooCommerce Square Changelog ***
2
+
3
+ 2018-01-29 - version 1.0.25
4
+ * Tweaks - Error handling.
5
+ * Add - Public Release on .org
6
+
7
+ 2017-12-13 - version 1.0.24
8
+ * Fix - In some cases rounding issues occur causing payment unable to process.
9
+ * Update - WC tested up to version.
10
+ * Add - Readme.txt.
11
+
12
+ 2017-11-10 - version 1.0.23
13
+ * Add - WC minimum requirements in header.
14
+ * Fix - WC Product sometimes is not a proper object preventing sync.
15
+ * Fix - PHP 7 notice with amount not cast an int.
16
+
17
+ 2017-09-06 - version 1.0.22
18
+ * Tweak - Increase inventory polling cache and add timeout limit.
19
+ * Fix - CRON scheduling time not using UTC causes delay in CRON job.
20
+
21
+ 2017-07-26 - version 1.0.21
22
+ * Fix - Non decimal place price such as Japanese Yen is not formatted properly when syncing Square to WC.
23
+
24
+ 2017-06-29 - version 1.0.20
25
+ * Fix - Schedule sale price not updating on Square when sale is over.
26
+ * Fix - Pass price to Square tax exclusive instead of inclusive.
27
+ * Fix - Create customer error on payments when phone number is not provided.
28
+ * Update - Settings location description to be more clear.
29
+
30
+ 2017-06-06 - version 1.0.19
31
+ * Fix - Sync currency amounts that do not contain decimals correctly.
32
+
33
+ 2017-05-19 - version 1.0.18
34
+ * Fix - Better logic of sale price on WC when syncing so it doesn't override regular price unintented.
35
+ * Fix - If image exists for a product, don't sync the image to prevent duplicated images being created.
36
+ * Fix - Enable status not showing for Square payments in gateway list.
37
+
38
+ 2017-05-08 - version 1.0.17
39
+ * Fix - WC 3.0 debug tools compatibility.
40
+ * Add - Support for Japan market.
41
+
42
+ 2017-03-31 - version 1.0.16
43
+ * Add - Confirmation message before bulk sync.
44
+ * Add - Cron schedules for when product stock changes instead of firing off immediately.
45
+
46
+ 2017-03-27 - version 1.0.15
47
+ * Update - Additional updates for WooCommerce 3.0.0 compatibility.
48
+ * Add - Support for UK market.
49
+
50
+ 2017-03-13 - version 1.0.14
51
+ * Fix - When multiple notify emails are set, causes 500 server error.
52
+ * Fix - Sync issues when syncing large datasets.
53
+ * Fix - Sync item with no sku sometimes gets corrupted with other item data.
54
+ * Update - WooCommerce 3.0.0 compatibility.
55
+
56
+ 2017-01-17 - version 1.0.13
57
+ * Add - Support for Australian markets.
58
+
59
+ 2017-01-09 - version 1.0.12
60
+ * Fix - When syncing inventory WC to Square, stock level becomes zero.
61
+
62
+ 2016-10-26 - version 1.0.11
63
+ * Fix - When WC API is used, it can cause duplicate WC API Exception.
64
+
65
+ 2016-10-14 - version 1.0.10
66
+ * Update - Make sure Square only works for US and CA merchants.
67
+
68
+ 2016-09-12 - version 1.0.9
69
+ * Fix - Normalize price when syncing Square to WC to prevent errors.
70
+ * Add - Option to disable sync per product.
71
+
72
+ 2016-09-08 - version 1.0.8
73
+ * Add - WC Products CRUD to replace REST API for creating products to prevent interference with wc-api.
74
+ * Add - Filter to add custom DOM elements for payments to render payment form "woocommerce_square_payment_form_trigger_element".
75
+ * Fix - Payments token expiration was not handled correctly due to using API v2.
76
+
77
+ 2016-08-30 - version 1.0.7
78
+ * Tweak - Replace all wp_remote_requests with raw cURL.
79
+ * Fix - In IE, the browser won't replace iFrame form fields when it is hidden.
80
+
81
+ 2016-08-27 - version 1.0.6
82
+ * Add - Clear cache/transient tool in system status->tool.
83
+ * Fix - Prevent infinite loop when polling Square inventory.
84
+
85
+ 2016-08-25 - version 1.0.5
86
+ * Fix - Image not syncing when using wp_remote_request. Changed to raw cURL.
87
+
88
+ 2016-08-19 - version 1.0.4
89
+ * Tweak - Payment form field styles.
90
+ * Add - woocommerce_square_payment_input_styles filter to allow manipulation of the form styles.
91
+ * Fix - When duplicating product in WC, it replaces the original product on Square.
92
+ * Fix - In FireFox, the browser won't replace iFrame form fields when it is hidden.
93
+
94
+ 2016-08-16 - version 1.0.3
95
+ * Fix - When duplicating product in WC, it replaces the original product on Square.
96
+ * Fix - oAuth tokens were not renewing properly.
97
+
98
+ 2016-08-04 - version 1.0.2
99
+ * Update - HTTP protocol to version 1.1 to prevent timeouts.
100
+
101
+ 2016-08-01 - version 1.0.1
102
+ * Tweak - Make credit card form fields more responsive in mobile devices.
103
+ * Fix - Square list endpoints may return duplicates, so put in place a check for that.
104
+
105
+ 2016-07-26 - version 1.0.0
106
+ * First Release
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
+ }
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
+
includes/admin/class-wc-square-bulk-sync-handler.php ADDED
@@ -0,0 +1,444 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ * Process Square to WC ajax
30
+ *
31
+ * @since 1.0.0
32
+ * @version 1.0.14
33
+ * @return bool
34
+ */
35
+ public function square_to_wc_ajax() {
36
+ check_ajax_referer( 'square-sync', 'security' );
37
+
38
+ /**
39
+ * Fires if a valid bulk Square to WC sync is being processed.
40
+ *
41
+ * @since 1.0.0
42
+ */
43
+ do_action( 'woocommerce_square_bulk_syncing_square_to_wc' );
44
+
45
+ $settings = get_option( 'woocommerce_squareconnect_settings' );
46
+ $emails = ! empty( $settings['sync_email'] ) ? explode( ',', str_replace( ' ', '', $settings['sync_email'] ) ) : '';
47
+
48
+ $sync_products = ( 'yes' === $settings['sync_products'] );
49
+ $sync_categories = ( 'yes' === $settings['sync_categories'] );
50
+ $sync_inventory = ( 'yes' === $settings['sync_inventory'] );
51
+ $sync_images = ( 'yes' === $settings['sync_images'] );
52
+ $cache_age = apply_filters( 'woocommerce_square_syncing_square_ids_cache', DAY_IN_SECONDS );
53
+ $message = '';
54
+
55
+ if ( ! $sync_products ) {
56
+ wp_send_json( array( 'process' => 'done', 'percentage' => 100, 'type' => 'square-to-wc', 'message' => __( 'Product Sync is disabled. Sync aborted.', 'woocommerce-square' ) ) );
57
+ }
58
+
59
+ // we need to check for cURL
60
+ if ( ! function_exists( 'curl_init' ) ) {
61
+ 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' ) ) );
62
+ }
63
+
64
+ // if a WC to Square process still needs to be completed reset the caches
65
+ // as the two processes ( WC -> Square and Square -> WC ) use the same cache
66
+ if ( 'wc_to_square' === get_transient( 'sq_wc_sync_current_process' ) ) {
67
+
68
+ $this->connect->delete_all_caches();
69
+
70
+ }
71
+
72
+ // set Square->WC as the current active process
73
+ set_transient( 'sq_wc_sync_current_process', 'square_to_wc', $cache_age );
74
+
75
+ // index for the current item in the process
76
+ $process = $this->get_process_index();
77
+
78
+ // only sync categories on the first pass
79
+ if ( ( 0 === $process ) && $sync_categories ) {
80
+
81
+ $this->square_to_wc->sync_categories();
82
+
83
+ }
84
+
85
+ if ( ( 0 === $process ) && $sync_inventory ) {
86
+ // ensure this manual update gets the freshest item counts
87
+ delete_transient( 'wc_square_inventory' );
88
+
89
+ $this->connect->get_square_inventory();
90
+
91
+ }
92
+
93
+ // products
94
+ // get all product ids
95
+ $square_item_ids = $this->get_processing_ids();
96
+
97
+ // run this only on first process
98
+ if ( $process === 0 ) {
99
+ $square_items = $this->connect->get_square_products();
100
+ $square_item_ids = ! empty( $square_items ) ? array_unique( wp_list_pluck( $square_items, 'id' ) ) : array();
101
+
102
+ // cache it
103
+ set_transient( 'wc_square_processing_total_count', count( $square_item_ids ), $cache_age );
104
+ set_transient( 'wc_square_processing_ids', $square_item_ids, $cache_age );
105
+
106
+ }
107
+
108
+ if ( $square_item_ids && $sync_products ) {
109
+
110
+ $square_item_id = array_pop( $square_item_ids );
111
+ $square_item = $this->connect->get_square_product( $square_item_id );
112
+
113
+ if ( $square_item ) {
114
+
115
+ $this->square_to_wc->sync_product( $square_item, $sync_categories, $sync_inventory, $sync_images );
116
+
117
+ } else {
118
+
119
+ WC_Square_Sync_Logger::log( sprintf( '[Square -> WC] Bulk Sync: Error retrieving Square Item with ID %s.', $square_item_id ) );
120
+
121
+ }
122
+
123
+ $process++;
124
+
125
+ $percentage = $this->get_process_percentage( $process );
126
+
127
+ $this->delete_processed_id( $square_item_id );
128
+
129
+ $remaining_ids = $this->get_processing_ids();
130
+
131
+ // run this only on last process
132
+ if ( empty( $remaining_ids ) ) {
133
+ $process = 'done';
134
+
135
+ $percentage = 100;
136
+
137
+ // send sync email
138
+ $this->send_sync_email( $emails, __( 'Sync Completed', 'woocommerce-square' ) );
139
+
140
+ // reset the processed ids
141
+ $this->connect->delete_all_caches();
142
+
143
+ $message = __( 'Sync completed', 'woocommerce-square' );
144
+ }
145
+
146
+ wp_send_json( array( 'process' => $process, 'percentage' => $percentage, 'type' => 'square-to-wc', 'message' => $message ) );
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Process WC to Square ajax
152
+ *
153
+ * @since 1.0.0
154
+ * @version 1.0.0
155
+ * @return bool
156
+ */
157
+ public function wc_to_square_ajax() {
158
+ check_ajax_referer( 'square-sync', 'security' );
159
+
160
+ $settings = get_option( 'woocommerce_squareconnect_settings' );
161
+ $emails = ! empty( $settings['sync_email'] ) ? $settings['sync_email'] : '';
162
+
163
+ $sync_products = ( 'yes' === $settings['sync_products'] );
164
+ $sync_categories = ( 'yes' === $settings['sync_categories'] );
165
+ $sync_inventory = ( 'yes' === $settings['sync_inventory'] );
166
+ $sync_images = ( 'yes' === $settings['sync_images'] );
167
+ $cache_age = apply_filters( 'woocommerce_square_syncing_wc_product_ids_cache', DAY_IN_SECONDS );
168
+ $message = '';
169
+
170
+ if ( ! $sync_products ) {
171
+ wp_send_json( array( 'process' => 'done', 'percentage' => 100, 'type' => 'wc-to-square', 'message' => __( 'Product Sync is disabled. Sync aborted.', 'woocommerce-square' ) ) );
172
+ }
173
+
174
+ // we need to check for cURL
175
+ if ( ! function_exists( 'curl_init' ) ) {
176
+ 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' ) ) );
177
+ }
178
+
179
+ // if a Square to WC process still needs to be completed reset the caches
180
+ // as the two processes ( WC -> Square and Square -> WC ) use the same cache
181
+ if ( 'square_to_wc' === get_transient( 'sq_wc_sync_current_process' ) ) {
182
+
183
+ $this->connect->delete_all_caches();
184
+
185
+ }
186
+
187
+ // set WC->Square as the current active process
188
+ set_transient( 'sq_wc_sync_current_process', 'wc_to_square', $cache_age );
189
+
190
+ $process = $this->get_process_index();
191
+
192
+ // only sync categories on the first pass
193
+ if ( ( 0 === $process ) && $sync_categories ) {
194
+
195
+ $this->wc_to_square->sync_categories();
196
+
197
+ }
198
+
199
+ // products
200
+ // get all product ids
201
+ $wc_product_ids = $this->get_processing_ids();
202
+
203
+ // run the following only on first process and cache it
204
+ if ( ( 0 === $process ) && $sync_products ) {
205
+
206
+ $wc_product_ids = $this->get_all_product_ids();
207
+
208
+ // cache it
209
+ set_transient( 'wc_square_processing_total_count', count( $wc_product_ids ), $cache_age );
210
+ set_transient( 'wc_square_processing_ids', $wc_product_ids, $cache_age );
211
+ }
212
+
213
+ if ( $sync_products && ! empty( $wc_product_ids ) ) {
214
+
215
+ $wc_product_id = array_pop( $wc_product_ids );
216
+
217
+ $wc_product = wc_get_product( $wc_product_id );
218
+
219
+ if ( is_object( $wc_product ) && is_a( $wc_product, 'WC_Product' ) ) {
220
+ $this->wc_to_square->sync_product( $wc_product, $sync_categories, $sync_inventory, $sync_images );
221
+ }
222
+
223
+ $process++;
224
+
225
+ $percentage = $this->get_process_percentage( $process );
226
+
227
+ if ( is_object( $wc_product ) ) {
228
+ $this->delete_processed_id( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->id : $wc_product->get_id() );
229
+ } else {
230
+ $this->delete_processed_id( $wc_product_id );
231
+ }
232
+
233
+ $remaining_ids = $this->get_processing_ids();
234
+
235
+ // run this only on last process
236
+ if ( empty( $remaining_ids ) ) {
237
+ $process = 'done';
238
+
239
+ $percentage = 100;
240
+
241
+ // send sync email
242
+ $this->send_sync_email( $emails, __( 'Sync Completed', 'woocommerce-square' ) );
243
+
244
+ // reset the processed ids
245
+ $this->connect->delete_all_caches();
246
+
247
+ $message = __( 'Sync completed', 'woocommerce-square' );
248
+ }
249
+
250
+ wp_send_json( array( 'process' => $process, 'percentage' => $percentage, 'type' => 'wc-to-square', 'message' => $message ) );
251
+
252
+ }
253
+
254
+ wp_send_json( array( 'process' => 'done', 'percentage' => 100, 'type' => 'wc-to-square', 'message' => __( 'No Products to Sync.', 'woocommerce-square' ) ) );
255
+ }
256
+
257
+ /**
258
+ * Figure out at which product index we are
259
+ * at using the total count and the remaining item.
260
+ * The index stats at 0 .
261
+ *
262
+ * @since 1.0.0
263
+ *
264
+ * @return int $process_index
265
+ */
266
+ public function get_process_index() {
267
+
268
+ $total_items = (int) get_transient( 'wc_square_processing_total_count' );
269
+ $remaining_ids_count = count( $this->get_processing_ids() );
270
+ $process_index = $total_items - $remaining_ids_count;
271
+
272
+ if ( empty( $process_index ) ) {
273
+ $process_index = 0;
274
+ }
275
+
276
+ return $process_index;
277
+ }
278
+
279
+ /**
280
+ * Gets all product ids that are sync-eligible (they have SKUs).
281
+ *
282
+ * This looks for products as well as variations, if a variant has a SKU, the
283
+ * parent product will be included in the result set.
284
+ *
285
+ * @access public
286
+ * @since 1.0.0
287
+ * @version 1.0.14
288
+ * @return array $ids
289
+ */
290
+ public function get_all_product_ids() {
291
+
292
+ $args = apply_filters( 'woocommerce_square_get_all_product_ids_args', array(
293
+ 'posts_per_page' => -1,
294
+ 'post_type' => array( 'product', 'product_variation' ),
295
+ 'post_status' => 'publish',
296
+ 'fields' => 'id=>parent',
297
+ 'meta_query' => array(
298
+ array(
299
+ 'key' => '_sku',
300
+ 'compare' => '!=',
301
+ 'value' => ''
302
+ )
303
+ )
304
+ ) );
305
+
306
+ $products_with_skus = get_posts( $args );
307
+ $product_ids = array();
308
+
309
+ /*
310
+ * Our result set contains products and variations. We're only concerned with
311
+ * returning top-level products, so favor the parent ID if present (denotes a variation)
312
+ */
313
+ foreach ( $products_with_skus as $product_id => $parent_id ) {
314
+ $post_id = 0 == $parent_id ? $product_id : $parent_id;
315
+
316
+ // check if product sync is disable, if so skip
317
+ if ( WC_Square_Utils::skip_product_sync( $post_id ) ) {
318
+ WC_Square_Sync_Logger::log( sprintf( '[WC -> Square] Syncing disabled for this WC Product %d', $post_id ) );
319
+
320
+ continue;
321
+ }
322
+
323
+ // when it is a variation, we need to check its parent for publish
324
+ // post status.
325
+ if ( 0 == $parent_id ) {
326
+ $product_ids[] = $product_id;
327
+ } else {
328
+ $post = get_post( $parent_id );
329
+
330
+ if ( is_object( $post ) && 'publish' === $post->post_status ) {
331
+ $product_ids[] = $parent_id;
332
+ }
333
+ }
334
+ }
335
+
336
+ /*
337
+ * Products can have multiple variants, so we might end up with
338
+ * duplicate parent product IDs in our list.
339
+ */
340
+ $unique_product_ids = array_unique( $product_ids );
341
+
342
+ return $unique_product_ids;
343
+ }
344
+
345
+ /**
346
+ * Deletes the product ID from the list so we can continue if sync is terminated early.
347
+ * This function can take both the WC product id or Square product ID
348
+ *
349
+ * @access public
350
+ * @since 1.0.0
351
+ * @version 1.0.0
352
+ * @param string $product_id
353
+ * @return bool
354
+ */
355
+ public function delete_processed_id( $product_id = null ) {
356
+
357
+ if ( null === $product_id ) {
358
+ return false;
359
+ }
360
+
361
+ $ids = $this->get_processing_ids();
362
+
363
+ if ( ( $key = array_search( $product_id, $ids ) ) !== false ) {
364
+ unset( $ids[ $key ] );
365
+ }
366
+
367
+ set_transient( 'wc_square_processing_ids', $ids, apply_filters( 'woocommerce_square_sync_processing_ids_cache', DAY_IN_SECONDS ) );
368
+
369
+ return true;
370
+ }
371
+
372
+ /**
373
+ * Gets the already processed product IDs ( both Square and WC )
374
+ *
375
+ * @access public
376
+ * @since 1.0.0
377
+ * @version 1.0.0
378
+ * @return array $ids
379
+ */
380
+ public function get_processing_ids() {
381
+ if ( $ids = get_transient( 'wc_square_processing_ids' ) ) {
382
+ return $ids;
383
+ }
384
+
385
+ return array();
386
+ }
387
+
388
+ /**
389
+ * Get process percentage
390
+ *
391
+ * @access public
392
+ * @since 1.0.0
393
+ * @version 1.0.0
394
+ * @param int $process the current process step
395
+ * @return string $percentage
396
+ */
397
+ public function get_process_percentage( $process ) {
398
+
399
+ $total_count = (int) get_transient( 'wc_square_processing_total_count' );
400
+ $percentage = 0;
401
+
402
+ if ( $total_count > 0 ) {
403
+ $percentage = ( $process / $total_count );
404
+ }
405
+
406
+ if ( 0 === $process ) {
407
+ // 10% is added to offset the category process
408
+ $percentage = $percentage + 0.10;
409
+ }
410
+
411
+ return round( $percentage, 2 ) * 100;
412
+ }
413
+
414
+ /**
415
+ * Sends the sync notification email when operation ends
416
+ *
417
+ * @access public
418
+ * @since 1.0.0
419
+ * @version 1.0.14
420
+ * @param string $emails
421
+ * @param string $message
422
+ * @return bool
423
+ */
424
+ public function send_sync_email( $emails, $message = '' ) {
425
+ // default to admin's email
426
+ if ( empty( $emails ) ) {
427
+ $emails = array();
428
+ $emails[] = get_option( 'admin_email' );
429
+ }
430
+
431
+ $subject = sprintf( __( '%s - WooCommerce Square Sync Operation', 'woocommerce-square' ), wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ) );
432
+
433
+ $headers = array();
434
+
435
+ foreach ( $emails as $email ) {
436
+ $headers[] = sprintf( __( '%s', 'woocommerce-square' ) . ' ' . wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ) . ' <' . $email . '>', 'From:' ) . PHP_EOL;
437
+
438
+ wp_mail( $email, $subject, $message, $headers );
439
+ }
440
+
441
+ return true;
442
+ }
443
+
444
+ }
includes/class-wc-square-client.php ADDED
@@ -0,0 +1,379 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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' => 0,
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
+
204
+ while ( true ) {
205
+
206
+ $response = $this->http_request( $debug_label, $request_url, $method, $body );
207
+
208
+ if ( ! $response ) {
209
+
210
+ return $response;
211
+
212
+ }
213
+
214
+ $response_data = json_decode( wp_remote_retrieve_body( $response ) );
215
+
216
+ // A paged list result will be an array, so let's merge if we're already returning an array
217
+ if ( ( 'GET' === $method ) && is_array( $return_data ) && is_array( $response_data ) ) {
218
+
219
+ $return_data = array_merge( $return_data, $response_data );
220
+
221
+ } else {
222
+
223
+ $return_data = $response_data;
224
+
225
+ }
226
+
227
+ $link_header = wp_remote_retrieve_header( $response, 'Link' );
228
+
229
+ // Look for the next page, if specified
230
+ if ( ! preg_match( '/Link:( |)<(.+)>;rel=("|\')next("|\')/i', $link_header ) ) {
231
+ return $return_data;
232
+ }
233
+
234
+ $rel_link_matches = array();
235
+
236
+ // Set up the next page URL for the following loop
237
+ if ( ( 'GET' === $method ) && preg_match( '/Link:( |)<(.+)>;rel=("|\')next("|\')/i', $link_header, $rel_link_matches ) ) {
238
+
239
+ $request_url = $rel_link_matches[2];
240
+ $body = null;
241
+
242
+ } else {
243
+
244
+ return $return_data;
245
+
246
+ }
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Helper method to make HTTP requests to the Square API, with retries.
252
+ *
253
+ * @since 1.0.25 Switch to use WP native remote requests.
254
+ * @param string $debug_label Description of the request, for logging.
255
+ * @param string $request_url URL to request.
256
+ * @param string $method HTTP method to use. Defaults to 'GET'.
257
+ * @param mixed $body Optional. Request payload - will be JSON encoded if non-scalar.
258
+ *
259
+ * @return bool|object|WP_Error
260
+ */
261
+ private function http_request( $debug_label, $request_url, $method = 'GET', $body = null ) {
262
+ $request_args = $this->get_request_args();
263
+
264
+ if ( ! is_null( $body ) ) {
265
+ if ( ! empty( $request_args['headers']['Content-Type'] ) && ( 'application/json' === $request_args['headers']['Content-Type'] ) ) {
266
+ $request_args['body'] = json_encode( $body );
267
+ } else {
268
+ $request_args['body'] = $body;
269
+ }
270
+ }
271
+
272
+ $request_args['method'] = $method;
273
+
274
+ // Make actual request in a retry loop
275
+ $try_count = 1;
276
+ $max_retries = $this->request_retries();
277
+
278
+ while ( true ) {
279
+ $start_time = current_time( 'timestamp' );
280
+ $response = wp_remote_request( untrailingslashit( $request_url ), $request_args );
281
+ $end_time = current_time( 'timestamp' );
282
+
283
+ WC_Square_Sync_Logger::log( sprintf( '%s', $debug_label ), $start_time, $end_time );
284
+
285
+ $decoded_response = json_decode( wp_remote_retrieve_body( $response ) );
286
+
287
+ if ( is_object( $decoded_response ) && ! empty( $decoded_response->type ) ) {
288
+ if ( preg_match( '/bad_request/', $decoded_response->type ) || preg_match( '/not_found/', $decoded_response->type ) ) {
289
+ WC_Square_Sync_Logger::log( sprintf( '%s - %s', $decoded_response->type, $decoded_response->message ), $start_time, $end_time );
290
+
291
+ return false;
292
+ }
293
+ }
294
+
295
+ // handle expired tokens
296
+ if ( is_object( $decoded_response ) &&
297
+ ( ! empty( $decoded_response->type ) && 'oauth.expired' === $decoded_response->type ) ||
298
+ ( ! empty( $decoded_response->errors ) && 'ACCESS_TOKEN_EXPIRED' === $decoded_response->errors[0]->code ) ) {
299
+
300
+ $oauth_connect_url = 'https://connect.woocommerce.com/renew/square';
301
+
302
+ if ( WC_SQUARE_ENABLE_STAGING ) {
303
+ $oauth_connect_url = 'https://connect.woocommerce.com/renew/squaresandbox';
304
+ }
305
+
306
+ $args = array(
307
+ 'body' => array(
308
+ 'token' => $this->access_token,
309
+ ),
310
+ 'timeout' => 45,
311
+ );
312
+
313
+ $start_time = current_time( 'timestamp' );
314
+ $oauth_response = wp_remote_post( $oauth_connect_url, $args );
315
+ $end_time = current_time( 'timestamp' );
316
+
317
+ $decoded_oauth_response = json_decode( wp_remote_retrieve_body( $oauth_response ) );
318
+
319
+ if ( is_wp_error( $oauth_response ) ) {
320
+
321
+ WC_Square_Sync_Logger::log( sprintf( 'Renewing expired token error - %s', $parsed_oauth_response['curl_error'] ), $start_time, $end_time );
322
+
323
+ return false;
324
+
325
+ } elseif ( is_object( $decoded_oauth_response ) && ! empty( $decoded_oauth_response->error ) ) {
326
+
327
+ WC_Square_Sync_Logger::log( sprintf( 'Renewing expired token error - %s', $decoded_oauth_response->type ), $start_time, $end_time );
328
+
329
+ return false;
330
+
331
+ } elseif ( is_object( $decoded_oauth_response ) && ! empty( $decoded_oauth_response->access_token ) ) {
332
+ update_option( 'woocommerce_square_merchant_access_token', sanitize_text_field( urldecode( $decoded_oauth_response->access_token ) ) );
333
+
334
+ // let's set the token instance again so settings option is refreshed
335
+ $this->set_access_token( sanitize_text_field( urldecode( $decoded_oauth_response->access_token ) ) );
336
+ $request_args['headers']['Authorization'] = 'Bearer ' . sanitize_text_field( $this->get_access_token() );
337
+
338
+ WC_Square_Sync_Logger::log( sprintf( 'Retrying with new refreshed token' ), $start_time, $end_time );
339
+
340
+ // start at the beginning again
341
+ continue;
342
+ } else {
343
+ WC_Square_Sync_Logger::log( sprintf( 'Renewing expired token error - Unknown Error' ), $start_time, $end_time );
344
+
345
+ return false;
346
+ }
347
+ }
348
+
349
+ // handle revoked tokens
350
+ if ( is_object( $decoded_response ) && ! empty( $decoded_response->type ) && 'oauth.revoked' === $decoded_response->type ) {
351
+ WC_Square_Sync_Logger::log( sprintf( 'Token is revoked!' ), $start_time, $end_time );
352
+
353
+ return false;
354
+ }
355
+
356
+ if ( is_wp_error( $response ) ) {
357
+
358
+ WC_Square_Sync_Logger::log( sprintf( '(%s) Try #%d - %s', $debug_label, $try_count, $response->get_error_message() ), $start_time, $end_time );
359
+
360
+ } else {
361
+
362
+ return $response;
363
+
364
+ }
365
+
366
+ $try_count++;
367
+
368
+ if ( $try_count > $max_retries ) {
369
+ break;
370
+ }
371
+
372
+ sleep( 1 );
373
+
374
+ }
375
+
376
+ return false;
377
+
378
+ }
379
+ }
includes/class-wc-square-connect.php ADDED
@@ -0,0 +1,678 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ $this->delete_all_caches();
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
+ * Deletes cached data ( both Square and WC )
77
+ *
78
+ * @access public
79
+ * @since 1.0.5
80
+ * @version 1.0.14
81
+ * @return bool
82
+ */
83
+ public function delete_all_caches() {
84
+
85
+ delete_transient( 'wc_square_processing_total_count' );
86
+
87
+ delete_transient( 'wc_square_processing_ids' );
88
+
89
+ delete_transient( 'wc_square_syncing_square_inventory' );
90
+
91
+ delete_transient( 'sq_wc_sync_current_process' );
92
+
93
+ delete_transient( 'wc_square_inventory' );
94
+
95
+ delete_transient( 'wc_square_polling' );
96
+
97
+ return true;
98
+ }
99
+
100
+ /**
101
+ * Checks to see if token is valid.
102
+ *
103
+ * There is no formal way to check this other than to
104
+ * retrieve the merchant account details and if it comes back
105
+ * with a code 200, we assume it is valid.
106
+ *
107
+ * @access public
108
+ * @since 1.0.0
109
+ * @version 1.0.0
110
+ * @return bool
111
+ */
112
+ public function is_valid_token() {
113
+
114
+ $merchant_account_type = $this->get_square_merchant_account_type();
115
+
116
+ return ( false !== $merchant_account_type );
117
+
118
+ }
119
+
120
+ /**
121
+ * Retrieve merchant's account information, such as business name and email address.
122
+ *
123
+ * Endpoint doc: https://docs.connect.squareup.com/api/connect/v1/#navsection-merchant
124
+ * Return value doc: https://docs.connect.squareup.com/api/connect/v1/#datatype-merchant
125
+ *
126
+ * @access public
127
+ * @since 1.0.0
128
+ * @version 1.0.0
129
+ * @return false|null|object False if an HTTP error or non-200 status is encountered. Null on JSON decode error. Object on success.
130
+ */
131
+ public function get_square_merchant() {
132
+
133
+ return $this->_client->request( 'Retrieving Merchant', 'me' );
134
+
135
+ }
136
+
137
+ /**
138
+ * Get the account type for the merchant.
139
+ *
140
+ * See: https://docs.connect.squareup.com/api/connect/v1/#enum-merchantaccounttype
141
+ *
142
+ * @access public
143
+ * @since 1.0.0
144
+ * @version 1.0.0
145
+ * @return bool|string Boolean false on failure, string account type (LOCATION or BUSINESS) on success.
146
+ */
147
+ public function get_square_merchant_account_type() {
148
+
149
+ $account_type = get_transient( self::MERCHANT_ACCOUNT_TYPE_CACHE_KEY );
150
+
151
+ if ( false === $account_type ) {
152
+
153
+ $merchant = $this->get_square_merchant();
154
+
155
+ if ( is_null( $merchant ) || false === $merchant ) {
156
+
157
+ return false;
158
+
159
+ }
160
+
161
+ if ( isset( $merchant->account_type ) ) {
162
+
163
+ $account_type = $merchant->account_type;
164
+
165
+ set_transient( self::MERCHANT_ACCOUNT_TYPE_CACHE_KEY, $account_type, DAY_IN_SECONDS );
166
+
167
+ }
168
+
169
+ }
170
+
171
+ return $account_type;
172
+
173
+ }
174
+
175
+ /**
176
+ * Gets the locations of the business.
177
+ *
178
+ * @access public
179
+ * @since 1.0.0
180
+ * @version 1.0.0
181
+ * @return array $locations
182
+ */
183
+ public function get_square_business_locations() {
184
+
185
+ if ( false !== ( $locations = get_transient( self::LOCATIONS_CACHE_KEY ) ) ) {
186
+
187
+ if ( ! empty( $locations ) ) {
188
+ return $locations;
189
+ }
190
+
191
+ }
192
+
193
+ $locations = array();
194
+ $account_type = $this->get_square_merchant_account_type();
195
+
196
+ /*
197
+ * Only "BUSINESS" accounts have multiple locations that need to be
198
+ * retrieved from a separate endpoint
199
+ * See: https://docs.connect.squareup.com/api/connect/v1/#get-locations
200
+ */
201
+ if ( self::MULTIPLE_LOCATION_ACCOUNT_TYPE === $account_type ) {
202
+
203
+ $items = $this->_client->request( 'Retrieve Business Locations', 'me/locations' );
204
+
205
+ if ( ! empty( $items ) ) {
206
+
207
+ foreach( $items as $item ) {
208
+ if ( is_object( $item ) ) {
209
+ $locations[ $item->id ] = $item->name;
210
+ }
211
+ }
212
+ }
213
+ }
214
+
215
+ /*
216
+ * Single location accounts have all the details under the Merchant object
217
+ */
218
+ if ( self::SINGLE_LOCATION_ACCOUNT_TYPE === $account_type ) {
219
+
220
+ $merchant = $this->get_square_merchant();
221
+
222
+ if ( isset( $merchant->id ) ) {
223
+
224
+ $locations[ $merchant->id ] = $merchant->name;
225
+
226
+ }
227
+
228
+ }
229
+
230
+ set_transient( self::LOCATIONS_CACHE_KEY, $locations, apply_filters( 'woocommerce_square_business_location_cache', DAY_IN_SECONDS ) );
231
+
232
+ return $locations;
233
+
234
+ }
235
+
236
+ /**
237
+ * Create a Square Item for a WC Product.
238
+ *
239
+ * See: https://docs.connect.squareup.com/api/connect/v1/#post-items
240
+ *
241
+ * @param WC_Product $wc_product
242
+ * @param bool $include_category
243
+ * @param bool $include_inventory
244
+ * @return object|bool Created Square Item object on success, boolean False on failure.
245
+ */
246
+ public function create_square_product( $wc_product, $include_category = false, $include_inventory = false ) {
247
+
248
+ // We can only handle simple products or ones with variations
249
+ if ( ! in_array( ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->product_type : $wc_product->get_type() ), array( 'simple', 'variable' ) ) ) {
250
+
251
+ return false;
252
+
253
+ }
254
+
255
+ // TODO: Consider making this method "dumber" - remove this formatting call.
256
+ $product = WC_Square_Utils::format_wc_product_create_for_square_api(
257
+ $wc_product,
258
+ $include_category,
259
+ $include_inventory
260
+ );
261
+
262
+ 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 );
263
+
264
+ }
265
+
266
+ /**
267
+ * Update the corresponding Square Item for a WC Product.
268
+ *
269
+ * See: https://docs.connect.squareup.com/api/connect/v1/#put-itemid
270
+ *
271
+ * @param WC_Product $wc_product
272
+ * @param string $square_item_id
273
+ * @param bool $include_category
274
+ * @param bool $include_inventory
275
+ * @return object|bool Updated Square Item object on success, boolean False on failure.
276
+ */
277
+ public function update_square_product( $wc_product, $square_item_id, $include_category = false, $include_inventory = false ) {
278
+
279
+ // We can only handle simple products or ones with variations
280
+ if ( ! in_array( ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->product_type : $wc_product->get_type() ), array( 'simple', 'variable' ) ) ) {
281
+
282
+ return false;
283
+
284
+ }
285
+
286
+ // TODO: Consider making this method "dumber" - remove this formatting call.
287
+ $product = WC_Square_Utils::format_wc_product_update_for_square_api( $wc_product, $include_category );
288
+
289
+ $endpoint = 'items/' . $square_item_id;
290
+
291
+ return $this->_client->request( 'Updating a Square Base Product for: ' . $square_item_id, $endpoint, 'PUT', $product );
292
+
293
+ }
294
+
295
+ /**
296
+ * Set the HTTP request Content-Type header to multipart/form-data for uploading Item images.
297
+ *
298
+ * @param array $http_args
299
+ * @return array
300
+ */
301
+ public function square_product_image_update_filter_http_args( $http_args ) {
302
+
303
+ if ( empty( $http_args['headers'] ) ) {
304
+
305
+ $http_args['headers'] = array();
306
+
307
+ }
308
+
309
+ $http_args['headers']['Content-Type'] = 'multipart/form-data; boundary=' . self::ITEM_IMAGE_MULTIPART_BOUNDARY;
310
+
311
+ return $http_args;
312
+
313
+ }
314
+
315
+ /**
316
+ * Updates the master image for an Item
317
+ * See: https://docs.connect.squareup.com/api/connect/v1/#post-image
318
+ *
319
+ * @param string $square_item_id Square Item ID to upload image for.
320
+ * @param string $mime_type Mime type of the image.
321
+ * @param string $path_to_image Full path to image, accessible using file_get_contents().
322
+ * @return bool|object Response object on success, boolean false on failure.
323
+ */
324
+ public function update_square_product_image( $square_item_id, $mime_type, $path_to_image ) {
325
+
326
+ // The WP HTTP API doesn't natively support multipart form data, so we must build the body ourselves
327
+ // See: http://lists.automattic.com/pipermail/wp-hackers/2013-January/045105.html
328
+ $request_body = '--' . self::ITEM_IMAGE_MULTIPART_BOUNDARY . "\r\n";
329
+ $request_body .= 'Content-Disposition: form-data; name="image_data"; filename="' . basename( $path_to_image ) . '"' . "\r\n";
330
+ $request_body .= 'Content-Type: ' . $mime_type . "\r\n\r\n"; // requires two CRLFs
331
+ $request_body .= file_get_contents( $path_to_image );
332
+ $request_body .= "\r\n--" . self::ITEM_IMAGE_MULTIPART_BOUNDARY . "--\r\n\r\n"; // requires two CRLFs
333
+
334
+ $api_path = '/items/' . $square_item_id . '/image';
335
+
336
+ add_filter( 'woocommerce_square_request_args', array( $this, 'square_product_image_update_filter_http_args' ) );
337
+
338
+ $result = $this->_client->request( 'Updating Square Item Image for: ' . $square_item_id, $api_path, 'POST', $request_body );
339
+
340
+ remove_filter( 'woocommerce_square_request_args', array( $this, 'square_product_image_update_filter_http_args' ) );
341
+
342
+ return $result;
343
+
344
+ }
345
+
346
+ /**
347
+ * Updates a product for a particular location
348
+ * Square API does not allow updates to a product along with variations (sucks)
349
+ * So a separate requests have to be made to update the variations see - update_square_variation()
350
+ *
351
+ * @access public
352
+ * @since 1.0.0
353
+ * @version 1.0.0
354
+ * @param int $product_id id from Square
355
+ * @param object $wc_product
356
+ * @return mixed
357
+ */
358
+ public function update_square_base_product( $s_product_id, $wc_product ) {
359
+
360
+ $product = array(
361
+ 'name' => $wc_product->get_title(),
362
+ 'description' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->post->post_content : $wc_product->get_description(),
363
+ 'visibility' => 'PUBLIC',
364
+ );
365
+
366
+ $category = wp_get_post_terms( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->id : $wc_product->get_id(), 'product_cat', array( 'parent' => 0 ) );
367
+
368
+ if ( ! empty( $category ) ) {
369
+
370
+ $square_cat_id = get_woocommerce_term_meta( $category[0]->term_id, 'square_cat_id', true );
371
+
372
+ $product['category_id'] = $square_cat_id;
373
+
374
+ }
375
+
376
+ $endpoint = 'items/' . $s_product_id;
377
+
378
+ return $this->_client->request( 'Updating a Square Base Product for: ' . $s_product_id, $endpoint, 'PUT', $product );
379
+
380
+ }
381
+
382
+ /**
383
+ * Updates a single product variation
384
+ * Note that each product has at least one variation in Square
385
+ * Square does not allow multiple variation to be updated at the same time
386
+ *
387
+ * @param string $square_item_id id from Square
388
+ * @param object|array $variation_data Data to create ItemVariation with
389
+ * @return mixed
390
+ */
391
+ public function create_square_variation( $square_item_id, $variation_data ) {
392
+ if ( empty( $square_item_id ) ) {
393
+ return false;
394
+ }
395
+
396
+ $endpoint = '/items/' . $square_item_id . '/variations';
397
+
398
+ return $this->_client->request( 'Creating a Square Product Variation for: ' . $square_item_id, $endpoint, 'POST', $variation_data );
399
+
400
+ }
401
+
402
+ /**
403
+ * Updates a single product variation
404
+ * Note that each product has at least one variation in Square
405
+ * Square does not allow multiple variation to be updated at the same time
406
+ *
407
+ * @param string $square_item_id id from Square
408
+ * @param string $square_variation_id id from Square
409
+ * @param object|array $variation_data Data to update ItemVariation with
410
+ * @return mixed
411
+ */
412
+ public function update_square_variation( $square_item_id, $square_variation_id, $variation_data ) {
413
+ if ( empty( $square_item_id ) ) {
414
+ return false;
415
+ }
416
+
417
+ $endpoint = '/items/' . $square_item_id . '/variations/' . $square_variation_id;
418
+
419
+ return $this->_client->request( 'Updating a Square Product Variation for: ' . $square_variation_id, $endpoint, 'PUT', $variation_data );
420
+
421
+ }
422
+
423
+ /**
424
+ * Deletes a single product variation
425
+ *
426
+ * @access public
427
+ * @since 1.0.0
428
+ * @version 1.0.0
429
+ * @param int $s_product_id id from Square
430
+ * @param int $s_variation_id id from Square
431
+ * @return mixed
432
+ */
433
+ public function delete_square_variation( $s_product_id, $s_variation_id ) {
434
+
435
+ $endpoint = '/items/' . $s_product_id . '/variations/' . $s_variation_id;
436
+
437
+ return $this->_client->request( 'Deleting a Square Product Variation for: ' . $s_variation_id, $endpoint, 'DELETE' );
438
+
439
+ }
440
+
441
+ /**
442
+ * Gets the products for a particular location
443
+ *
444
+ * @access public
445
+ * @since 1.0.0
446
+ * @version 1.0.0
447
+ * @return mixed
448
+ */
449
+ public function get_square_products() {
450
+
451
+ $response = $this->_client->request( 'Retrieving Products', 'items' );
452
+
453
+ return $response ? $response : array();
454
+
455
+ }
456
+
457
+ /**
458
+ * Gets a product for a particular location
459
+ *
460
+ * @access public
461
+ * @since 1.0.0
462
+ * @version 1.0.0
463
+ * @param string $s_product_id
464
+ * @return mixed
465
+ */
466
+ public function get_square_product( $s_product_id ) {
467
+
468
+ if ( empty( $s_product_id ) ) {
469
+
470
+ return array();
471
+
472
+ }
473
+
474
+ $endpoint = 'items/' . $s_product_id;
475
+ $response = $this->_client->request( 'Retrieving a Square Product for: ' . $s_product_id, $endpoint );
476
+
477
+ return $response ? $response : array();
478
+
479
+ }
480
+
481
+ /**
482
+ * Clear the Square Item SKU Map Cache
483
+ */
484
+ public function clear_item_sku_map_cache() {
485
+
486
+ delete_transient( self::ITEM_SKU_MAP_CACHE_KEY );
487
+
488
+ }
489
+
490
+ /**
491
+ * Generate a mapping of Square Item IDs to their associated SKUs.
492
+ *
493
+ * @return array List of Square Item IDs and their variation SKUs.
494
+ */
495
+ public function get_square_product_sku_map() {
496
+
497
+ if ( $cached = get_transient( self::ITEM_SKU_MAP_CACHE_KEY ) ) {
498
+
499
+ return $cached;
500
+
501
+ }
502
+
503
+ $square_products = $this->get_square_products();
504
+ $square_product_sku_map = array();
505
+ $processed_ids = array();
506
+
507
+ foreach ( $square_products as $s_product ) {
508
+ // Square may return dups so make sure we check for that
509
+ if ( in_array( $s_product->id, $processed_ids ) ) {
510
+ continue;
511
+ }
512
+
513
+ $square_product_sku_map[ $s_product->id ] = array();
514
+
515
+ foreach ( $s_product->variations as $s_variation ) {
516
+
517
+ if ( ! empty( $s_variation->sku ) ) {
518
+
519
+ $square_product_sku_map[ $s_product->id ][] = $s_variation->sku;
520
+
521
+ }
522
+ }
523
+
524
+ $processed_ids[] = $s_product;
525
+ }
526
+
527
+ set_transient( self::ITEM_SKU_MAP_CACHE_KEY, $square_product_sku_map, apply_filters( 'woocommerce_square_item_sku_cache', DAY_IN_SECONDS ) );
528
+
529
+ return $square_product_sku_map;
530
+
531
+ }
532
+
533
+ /**
534
+ * Checks if product exists on square
535
+ *
536
+ * @access public
537
+ * @since 1.0.0
538
+ * @version 1.0.0
539
+ * @param string|array $sku_list One or more SKUs to get a product by
540
+ * @return false if not exists or product object
541
+ */
542
+ public function square_product_exists( $sku_list ) {
543
+
544
+ $square_item_sku_map = $this->get_square_product_sku_map();
545
+
546
+ $sku_list = (array) $sku_list;
547
+
548
+ foreach ( $square_item_sku_map as $square_item_id => $square_item_skus ) {
549
+
550
+ if ( array_intersect( $sku_list, $square_item_skus ) ) {
551
+
552
+ return $this->get_square_product( $square_item_id );
553
+
554
+ }
555
+
556
+ }
557
+
558
+ return false;
559
+
560
+ }
561
+
562
+ /**
563
+ * Gets the categories for a particular location
564
+ *
565
+ * @access public
566
+ * @since 1.0.0
567
+ * @version 1.0.0
568
+ * @return mixed
569
+ */
570
+ public function get_square_categories() {
571
+
572
+ $response = $this->_client->request( 'Retrieving Square Categories', 'categories' );
573
+
574
+ return $response ? $response : array();
575
+
576
+ }
577
+
578
+ /**
579
+ * Create a square category
580
+ *
581
+ * @access public
582
+ * @since 1.0.0
583
+ * @version 1.0.0
584
+ * @param string $name
585
+ * @return mixed
586
+ */
587
+ public function create_square_category( $name ) {
588
+
589
+ $category = array(
590
+ 'name' => sanitize_text_field( $name ),
591
+ );
592
+
593
+ return $this->_client->request( 'Creating Square Category for: ' . sanitize_text_field( $name ), 'categories', 'POST', $category );
594
+
595
+ }
596
+
597
+ /**
598
+ * Update a Square Category
599
+ *
600
+ * @param string $square_category_id
601
+ * @param string $name
602
+ *
603
+ * @return bool|object|WP_Error
604
+ */
605
+ public function update_square_category( $square_category_id, $name ) {
606
+
607
+ $endpoint = 'categories/' . $square_category_id;
608
+ $category = array(
609
+ 'name' => sanitize_text_field( $name ),
610
+ );
611
+
612
+ return $this->_client->request( 'Updating Category for: ' . sanitize_text_field( $name ), $endpoint, 'PUT', $category );
613
+
614
+ }
615
+
616
+ /**
617
+ * Get square variation inventory.
618
+ *
619
+ * Note: There is no current way to get a specific product variation's inventory.
620
+ *
621
+ * @return array
622
+ */
623
+ public function get_square_inventory() {
624
+ if ( $cached = get_transient( self::INVENTORY_CACHE_KEY ) ) {
625
+
626
+ return $cached;
627
+
628
+ }
629
+
630
+ $response = $this->_client->request( 'Getting All Square Inventory', 'inventory' ); // default 1000 max limit
631
+
632
+ $square_inventory = array();
633
+
634
+ if ( is_array( $response ) ) {
635
+
636
+ $square_inventory_ids = wp_list_pluck( $response, 'variation_id' );
637
+ $square_inventory_quantities = wp_list_pluck( $response, 'quantity_on_hand' );
638
+ $square_inventory = array_combine( $square_inventory_ids, $square_inventory_quantities );
639
+
640
+ }
641
+
642
+ set_transient( self::INVENTORY_CACHE_KEY, $square_inventory, apply_filters( 'woocommerce_square_inventory_cache', DAY_IN_SECONDS ) );
643
+
644
+ return $square_inventory;
645
+
646
+ }
647
+
648
+ /**
649
+ * Updates square variation inventory
650
+ *
651
+ * @access public
652
+ * @since 1.0.0
653
+ * @version 1.0.0
654
+ * @param string $square_variation_id
655
+ * @param int $quantity_delta
656
+ * @param string $type the type of adjustment MANUAL_ADJUST, RECEIVE_STOCK, SALE
657
+ * @return bool
658
+ */
659
+ public function update_square_inventory( $square_variation_id, $quantity_delta, $type = 'MANUAL_ADJUST' ) {
660
+
661
+ $endpoint = 'inventory/' . $square_variation_id;
662
+ $inventory = array(
663
+ 'quantity_delta' => $quantity_delta,
664
+ 'adjustment_type' => $type,
665
+ );
666
+ $response = $this->_client->request( 'Updating Square Inventory for: ' . $square_variation_id, $endpoint, 'POST', $inventory );
667
+
668
+ return ( false !== $response );
669
+
670
+ }
671
+
672
+ /**
673
+ * Refresh the Square Inventory cache.
674
+ */
675
+ public function refresh_inventory_cache() {
676
+ delete_transient( self::INVENTORY_CACHE_KEY );
677
+ }
678
+ }
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
+ }
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();
includes/class-wc-square-inventory-poll.php ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
45
+ if ( $sync ) {
46
+ $this->to_wc->sync_all_inventory();
47
+ }
48
+ }
49
+ }
includes/class-wc-square-sync-from-square.php ADDED
@@ -0,0 +1,384 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
306
+ $wc_variation_ids = WC_Square_Utils::get_stock_managed_wc_variation_ids( $wc_product );
307
+ $square_inventory = $this->connect->get_square_inventory();
308
+
309
+ foreach ( $wc_variation_ids as $wc_variation_id ) {
310
+
311
+ $square_variation_id = WC_Square_Utils::get_wc_variation_square_id( $wc_variation_id );
312
+
313
+ if ( ! $square_variation_id || ! isset( $square_inventory[ $square_variation_id ] ) ) {
314
+
315
+ continue;
316
+
317
+ }
318
+
319
+ // check each variation stock_tracking setting and set stock if tracking is enabled
320
+ foreach ( $square_item->variations as $variation_item ) {
321
+
322
+ if ( $variation_item->id == $square_variation_id && $variation_item->track_inventory ) {
323
+
324
+ $square_stock = (int) $square_inventory[ $square_variation_id ];
325
+ $wc_variation = wc_get_product( $wc_variation_id );
326
+ $result = version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_variation->set_stock( $square_stock ) : wc_update_product_stock( $wc_variation, $square_stock );
327
+
328
+ }
329
+ }
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Sync all inventory from Square (expensive)
335
+ * @todo if searching for square id fails, check for SKU
336
+ */
337
+ public function sync_all_inventory() {
338
+ try {
339
+ set_time_limit( apply_filters( 'woocommerce_square_inventory_sync_timeout_limit', 200 ) );
340
+
341
+ // refresh cache first to get the latest inventory
342
+ $this->connect->refresh_inventory_cache();
343
+
344
+ $square_inventory = $this->connect->get_square_inventory();
345
+
346
+ // To prevent infinite loop when stock is updated in WC.
347
+ set_transient( 'wc_square_polling', 'yes', 60 * MINUTE_IN_SECONDS );
348
+
349
+ // hopefully there has been a manual sync prior so that square item id
350
+ // has already been saved in the product/variation metas to prevent
351
+ // unnecessary round trip requests to Square to find the SKU
352
+ foreach ( $square_inventory as $variation_id => $stock ) {
353
+ WC_Square_Sync_Logger::log( sprintf( '[Square -> WC] Syncing WC product inventory for Square Item ID %s.', $variation_id ) );
354
+
355
+ $wc_variation_product = WC_Square_Utils::get_wc_product_for_square_item_variation_id( $variation_id );
356
+
357
+ if ( ! is_object( $wc_variation_product ) ) {
358
+ 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 ) );
359
+ continue;
360
+ }
361
+
362
+ $product_id = 0;
363
+
364
+ // check if we need to skip
365
+ if ( 'simple' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_variation_product->product_type : $wc_variation_product->get_type() ) ) {
366
+ $product_id = version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_variation_product->id : $wc_variation_product->get_id();
367
+ } elseif ( 'variation' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_variation_product->product_type : $wc_variation_product->get_type() ) ) {
368
+ $product_id = version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_variation_product->parent->id : $wc_variation_product->get_parent_id();
369
+ }
370
+
371
+ if ( is_object( $wc_variation_product ) && ! WC_Square_Utils::skip_product_sync( $product_id ) ) {
372
+ version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_variation_product->set_stock( (int) $stock ) : wc_update_product_stock( $wc_variation_product, (int) $stock );
373
+ }
374
+ }
375
+
376
+ delete_transient( 'wc_square_polling' );
377
+
378
+ return true;
379
+ } catch ( Exception $e ) {
380
+ delete_transient( 'wc_square_polling' );
381
+ WC_Square_Sync_Logger::log( sprintf( '[Square -> WC] Inventory Poll: ', $e->getMessage() ) );
382
+ }
383
+ }
384
+ }
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
+ }
includes/class-wc-square-sync-to-square-wp-hooks.php ADDED
@@ -0,0 +1,312 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ * WC_Square_Sync_To_Square_WordPress_Hooks constructor.
54
+ *
55
+ * @param WC_Integration $integration
56
+ * @param WC_Square_Sync_To_Square $square
57
+ */
58
+ public function __construct( WC_Integration $integration, WC_Square_Sync_To_Square $square ) {
59
+
60
+ $this->integration = $integration;
61
+ $this->square = $square;
62
+
63
+ $this->sync_products = ( 'yes' === $integration->get_option( 'sync_products' ) );
64
+ $this->sync_categories = ( 'yes' === $integration->get_option( 'sync_categories' ) );
65
+ $this->sync_images = ( 'yes' === $integration->get_option( 'sync_images' ) );
66
+ $this->sync_inventory = ( 'yes' === $integration->get_option( 'sync_inventory' ) );
67
+
68
+ add_action( 'wc_square_loaded', array( $this, 'attach_hooks' ) );
69
+ add_action( 'wc_square_save_post_event', array( $this, 'process_save_post_event' ), 10, 2 );
70
+ add_action( 'wc_square_on_product_set_stock_event', array( $this, 'on_product_set_stock' ) );
71
+ add_action( 'wc_square_on_variation_set_stock_event', array( $this, 'on_variation_set_stock' ) );
72
+ }
73
+
74
+ /**
75
+ * Dynamically enable WP/WC hook callbacks.
76
+ */
77
+ public function enable() {
78
+
79
+ $this->enabled = true;
80
+
81
+ }
82
+
83
+ /**
84
+ * Dynamically disable WP/WC hook callbacks.
85
+ */
86
+ public function disable() {
87
+
88
+ $this->enabled = false;
89
+
90
+ }
91
+
92
+ /**
93
+ * Hook into WordPress and WooCommerce core.
94
+ */
95
+ public function attach_hooks() {
96
+
97
+ if ( $this->sync_products ) {
98
+ if ( version_compare( WC_VERSION, '3.0.0', '<' ) ) {
99
+ add_action( 'save_post', array( $this, 'pre_wc_30_on_save_post' ), 10, 2 );
100
+ } else {
101
+ add_action( 'woocommerce_before_product_object_save', array( $this, 'on_save_post' ), 10, 2 );
102
+ }
103
+ }
104
+
105
+ if ( $this->sync_categories ) {
106
+
107
+ add_action( 'created_product_cat', array( $this, 'on_category_modified' ) );
108
+
109
+ add_action( 'edited_product_cat', array( $this, 'on_category_modified' ) );
110
+
111
+ }
112
+
113
+ if ( $this->sync_inventory ) {
114
+ $param = isset( $_GET['wc-api'] ) ? $_GET['wc-api'] : '';
115
+
116
+ if ( 'WC_Square_Integration' !== $param ) {
117
+
118
+ add_action( 'woocommerce_product_set_stock', array( $this, 'schedule_on_product_set_stock' ) );
119
+
120
+ add_action( 'woocommerce_variation_set_stock', array( $this, 'schedule_on_variation_set_stock' ) );
121
+ }
122
+ }
123
+
124
+ }
125
+
126
+ /**
127
+ * Sync a WC Product to Square when it is saved.
128
+ *
129
+ * @param int $post_id
130
+ * @param bool $sync_categories
131
+ * @param bool $sync_inventory
132
+ * @param bool $sync_images
133
+ */
134
+ public function process_save_post_event( $post_id ) {
135
+ // clear inventory cache
136
+ delete_transient( 'wc_square_inventory' );
137
+
138
+ $wc_product = wc_get_product( $post_id );
139
+
140
+ if ( WC_Square_Utils::skip_product_sync( $post_id ) ) {
141
+ return;
142
+ }
143
+
144
+ if ( is_object( $wc_product ) && ! empty( $wc_product ) ) {
145
+ $this->square->sync_product( $wc_product, $this->sync_categories, $this->sync_inventory, $this->sync_images );
146
+ }
147
+
148
+ $this->delete_all_caches();
149
+
150
+ if ( version_compare( WC_VERSION, '3.0.0', '<' ) ) {
151
+ add_action( 'save_post', array( $this, 'pre_wc_30_on_save_post' ), 10, 2 );
152
+ } else {
153
+ add_action( 'woocommerce_before_product_object_save', array( $this, 'on_save_post' ), 10, 2 );
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Trigger the save post event.
159
+ *
160
+ * @since 1.0.0
161
+ * @version 1.0.20
162
+ * @param object $product
163
+ * @param object $data_store
164
+ */
165
+ public function on_save_post( $product, $data_store ) {
166
+ $post = get_post( $product->get_id() );
167
+
168
+ if ( ! $this->enabled
169
+ || ( defined( 'DOING_AJAX' ) && DOING_AJAX ) // TODO: Look into removing this check.
170
+ || ( defined( 'WP_LOAD_IMPORTERS' ) && WP_LOAD_IMPORTERS )
171
+ || wp_is_post_revision( $post )
172
+ || wp_is_post_autosave( $post )
173
+ || ( 'publish' !== get_post_status( $post ) )
174
+ || 'product' !== $post->post_type
175
+ ) {
176
+ return;
177
+ }
178
+
179
+ $args = array(
180
+ $product->get_id(),
181
+ uniqid(), // this is needed due to WP not scheduling new events with same name and args
182
+ );
183
+
184
+ wp_schedule_single_event( time() + 60, 'wc_square_save_post_event', $args );
185
+
186
+ remove_action( 'woocommerce_before_product_object_save', array( $this, 'on_save_post' ) );
187
+ }
188
+
189
+ /**
190
+ * Trigger the save post event.
191
+ *
192
+ * @see 'save_post'
193
+ * @since 1.0.0
194
+ * @version 1.0.20
195
+ * @param $post_id
196
+ * @param $post
197
+ */
198
+ public function pre_wc_30_on_save_post( $post_id, $post ) {
199
+ if ( ! $this->enabled
200
+ || ( defined( 'DOING_AJAX' ) && DOING_AJAX ) // TODO: Look into removing this check.
201
+ || ( defined( 'WP_LOAD_IMPORTERS' ) && WP_LOAD_IMPORTERS )
202
+ || wp_is_post_revision( $post )
203
+ || wp_is_post_autosave( $post )
204
+ || ( 'publish' !== get_post_status( $post ) )
205
+ || 'product' !== $post->post_type
206
+ ) {
207
+ return;
208
+ }
209
+
210
+ $args = array(
211
+ $post_id,
212
+ uniqid(), // this is needed due to WP not scheduling new events with same name and args
213
+ );
214
+
215
+ wp_schedule_single_event( time() + 60, 'wc_square_save_post_event', $args );
216
+
217
+ remove_action( 'save_post', array( $this, 'pre_wc_30_on_save_post' ) );
218
+ }
219
+
220
+ /**
221
+ * Sync categories to Square when a category is created or altered.
222
+ */
223
+ public function on_category_modified() {
224
+
225
+ if ( $this->enabled ) {
226
+
227
+ $this->square->sync_categories();
228
+
229
+ }
230
+
231
+ }
232
+
233
+ /**
234
+ * Schedule cron job when product stock is changed.
235
+ *
236
+ * @since 1.0.16
237
+ * @version 1.0.16
238
+ */
239
+ public function schedule_on_product_set_stock( WC_Product $wc_product ) {
240
+ $args = array(
241
+ $wc_product,
242
+ uniqid(), // this is needed due to WP not scheduling new events with same name and args
243
+ );
244
+
245
+ $polling = get_transient( 'wc_square_polling' );
246
+
247
+ if ( $this->enabled && ! $polling ) {
248
+ wp_schedule_single_event( time() + 60, 'wc_square_on_product_set_stock_event', $args );
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Schedule cron job when product variation stock is changed.
254
+ *
255
+ * @since 1.0.16
256
+ * @version 1.0.16
257
+ */
258
+ public function schedule_on_variation_set_stock( WC_Product_Variation $wc_variation ) {
259
+ $args = array(
260
+ $wc_variation,
261
+ uniqid(), // this is needed due to WP not scheduling new events with same name and args
262
+ );
263
+
264
+ $polling = get_transient( 'wc_square_polling' );
265
+
266
+ if ( $this->enabled && ! $polling ) {
267
+ wp_schedule_single_event( time() + 60, 'wc_square_on_variation_set_stock_event', $args );
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Sync inventory to Square when a product's stock is altered.
273
+ *
274
+ * @param array $wc_product
275
+ */
276
+ public function on_product_set_stock( $wc_product ) {
277
+ $this->square->sync_inventory( $wc_product );
278
+ }
279
+
280
+ /**
281
+ * Sync inventory to Square when a variation's stock is altered.
282
+ *
283
+ * @param array $wc_variation
284
+ */
285
+ public function on_variation_set_stock( $wc_variation ) {
286
+ $product = version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_variation->parent : wc_get_product( $wc_variation->get_parent_id() );
287
+ $this->square->sync_inventory( $product );
288
+ }
289
+
290
+ /**
291
+ * Deletes cached data ( both Square and WC )
292
+ *
293
+ * @access public
294
+ * @since 1.0.14
295
+ * @version 1.0.14
296
+ * @return bool
297
+ */
298
+ public function delete_all_caches() {
299
+
300
+ delete_transient( 'wc_square_processing_total_count' );
301
+
302
+ delete_transient( 'wc_square_processing_ids' );
303
+
304
+ delete_transient( 'wc_square_syncing_square_inventory' );
305
+
306
+ delete_transient( 'sq_wc_sync_current_process' );
307
+
308
+ delete_transient( 'wc_square_inventory' );
309
+
310
+ return true;
311
+ }
312
+ }
includes/class-wc-square-sync-to-square.php ADDED
@@ -0,0 +1,440 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ // Look for a Square Item with a matching SKU
178
+ $square_item = $this->get_square_item_for_wc_product( $wc_product );
179
+
180
+ // SKU found, update Item
181
+ if ( WC_Square_Utils::is_square_item_found( $square_item ) ) {
182
+
183
+ $result = $this->update_product( $wc_product, $square_item, $include_category, $include_inventory );
184
+
185
+ // No matching SKU found, create new Item
186
+ } else {
187
+ $create = true;
188
+ $result = $this->create_product( $wc_product, $include_category, $include_inventory );
189
+ }
190
+
191
+ // Sync inventory if create/update was successful
192
+ // TODO: consider whether or not this should be part of sync_product()..
193
+ if ( $result ) {
194
+ if ( $include_inventory ) {
195
+ $this->sync_inventory( $wc_product, $create );
196
+
197
+ }
198
+
199
+ if ( $include_image ) {
200
+
201
+ $this->sync_product_image( $wc_product, $result );
202
+
203
+ }
204
+
205
+ } else {
206
+
207
+ 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() ) );
208
+
209
+ }
210
+
211
+ }
212
+
213
+ /**
214
+ * Sync a WC Product's inventory to Square
215
+ *
216
+ * @since 1.0.0
217
+ * @version 1.0.14
218
+ * @param WC_Product $wc_product
219
+ * @param bool $create
220
+ */
221
+ public function sync_inventory( WC_Product $wc_product, $create = false ) {
222
+ // refresh cache first to get the latest inventory
223
+ $this->connect->refresh_inventory_cache();
224
+
225
+ // If not creating new product we need to first retrieve inventory.
226
+ if ( ! $create ) {
227
+ $square_inventory = $this->connect->get_square_inventory();
228
+ }
229
+
230
+ $wc_variation_ids = WC_Square_Utils::get_stock_managed_wc_variation_ids( $wc_product );
231
+
232
+ foreach ( $wc_variation_ids as $wc_variation_id ) {
233
+
234
+ $square_variation_id = WC_Square_Utils::get_wc_variation_square_id( $wc_variation_id );
235
+
236
+ if ( $square_variation_id || ( ! $create && isset( $square_inventory[ $square_variation_id ] ) ) ) {
237
+
238
+ $wc_stock = (int) get_post_meta( $wc_variation_id, '_stock', true );
239
+
240
+ $square_stock = 0;
241
+
242
+ if ( ! $create && isset( $square_inventory[ $square_variation_id ] ) ) {
243
+ $square_stock = (int) $square_inventory[ $square_variation_id ];
244
+ }
245
+
246
+ $delta = $wc_stock - $square_stock;
247
+
248
+ $result = $this->connect->update_square_inventory( $square_variation_id, $delta );
249
+
250
+ if ( ! $result ) {
251
+
252
+ 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() ) );
253
+
254
+ }
255
+
256
+ }
257
+
258
+ }
259
+
260
+ }
261
+
262
+ /**
263
+ * Create a Square Item for a WC Product
264
+ *
265
+ * @param WC_Product $wc_product
266
+ * @param bool $include_category
267
+ * @param bool $include_inventory
268
+ *
269
+ * @return object|bool Created Square Item object on success, boolean False on failure.
270
+ */
271
+ public function create_product( WC_Product $wc_product, $include_category = false, $include_inventory = false ) {
272
+
273
+ $square_item = $this->connect->create_square_product( $wc_product, $include_category, $include_inventory );
274
+
275
+ if ( $square_item ) {
276
+
277
+ WC_Square_Utils::set_square_ids_on_wc_product_by_sku( $wc_product, $square_item );
278
+
279
+ return $square_item;
280
+
281
+ }
282
+
283
+ 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() ) );
284
+
285
+ return false;
286
+
287
+ }
288
+
289
+ /**
290
+ * Update a Square Item for a WC Product
291
+ *
292
+ * @param WC_Product $wc_product
293
+ * @param object $square_item
294
+ * @param bool $include_category
295
+ * @param bool $include_inventory
296
+ *
297
+ * @return object|bool Updated Square Item object on success, boolean False on failure.
298
+ */
299
+ public function update_product( WC_Product $wc_product, $square_item, $include_category = false, $include_inventory = false ) {
300
+
301
+ $square_item = $this->connect->update_square_product( $wc_product, $square_item->id, $include_category, $include_inventory );
302
+
303
+ if ( ! $square_item ) {
304
+
305
+ 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() ) );
306
+ return false;
307
+
308
+ }
309
+
310
+ 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 );
311
+
312
+ if ( 'simple' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->product_type : $wc_product->get_type() ) ) {
313
+
314
+ $wc_variations = array( $wc_product );
315
+
316
+ } elseif ( 'variable' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->product_type : $wc_product->get_type() ) ) {
317
+
318
+ $wc_variations = WC_Square_Utils::get_wc_product_variations( $wc_product );
319
+
320
+ }
321
+
322
+ foreach ( $wc_variations as $wc_variation ) {
323
+ $product_type = version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_variation->product_type : $wc_variation->get_type();
324
+ $variation_data = WC_Square_Utils::format_wc_variation_for_square_api( $wc_variation, $include_inventory );
325
+
326
+ if ( 'variation' === $product_type ) {
327
+ $wc_variation_id = version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_variation->variation_id : $wc_variation->get_id();
328
+ } else {
329
+ $wc_variation_id = version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_variation->id : $wc_variation->get_id();
330
+ }
331
+
332
+ if ( $square_variation_id = WC_Square_Utils::get_wc_variation_square_id( $wc_variation_id ) ) {
333
+
334
+ $result = $this->connect->update_square_variation( $square_item->id, $square_variation_id, $variation_data );
335
+
336
+ } else {
337
+
338
+ $result = $this->connect->create_square_variation( $square_item->id, $variation_data );
339
+
340
+ if ( $result && isset( $result->id ) ) {
341
+
342
+ WC_Square_Utils::update_wc_variation_square_id( $wc_variation_id, $result->id );
343
+
344
+ }
345
+
346
+ }
347
+
348
+ if ( ! $result ) {
349
+
350
+ if ( $square_variation_id ) {
351
+
352
+ WC_Square_Sync_Logger::log( sprintf( '[WC -> Square] Error updating Square ItemVariation %s for WC Variation %d.', $square_variation_id, $wc_variation_id ) );
353
+
354
+ } else {
355
+
356
+ WC_Square_Sync_Logger::log( sprintf( '[WC -> Square] Error creating Square ItemVariation for WC Variation %d.', $wc_variation_id ) );
357
+
358
+ }
359
+
360
+ }
361
+
362
+ }
363
+
364
+ return $square_item;
365
+
366
+ }
367
+
368
+ /**
369
+ * Sync a WC Product's Image to Square
370
+ *
371
+ * @param WC_Product $wc_product WC Product to sync Item Image for.
372
+ * @param object $square_item Optional. Corresponding Square Item object for $wc_product.
373
+ *
374
+ * @return bool Success.
375
+ */
376
+ public function sync_product_image( WC_Product $wc_product, $square_item = null ) {
377
+
378
+ if ( is_null( $square_item ) ) {
379
+
380
+ $square_item = $this->get_square_item_for_wc_product( $wc_product );
381
+
382
+ }
383
+
384
+ if ( ! $square_item ) {
385
+
386
+ 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() ) );
387
+ return false;
388
+
389
+ }
390
+
391
+ if ( ! has_post_thumbnail( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->id : $wc_product->get_id() ) ) {
392
+
393
+ 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() ) );
394
+ return true;
395
+
396
+ }
397
+
398
+ return $this->update_product_image( $wc_product, $square_item->id );
399
+
400
+ }
401
+
402
+ /**
403
+ * Update a Square Item Image for a WC Product
404
+ *
405
+ * @param WC_Product $wc_product
406
+ * @param string $square_item_id
407
+ * @return bool Success.
408
+ */
409
+ public function update_product_image( WC_Product $wc_product, $square_item_id ) {
410
+
411
+ $image_id = get_post_thumbnail_id( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->id : $wc_product->get_id() );
412
+
413
+ if ( empty( $image_id ) ) {
414
+
415
+ 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() ) );
416
+ return true;
417
+
418
+ }
419
+
420
+ $mime_type = get_post_field( 'post_mime_type', $image_id, 'raw' );
421
+ $image_path = get_attached_file( $image_id );
422
+
423
+ $result = $this->connect->update_square_product_image( $square_item_id, $mime_type, $image_path );
424
+
425
+ if ( $result && isset( $result->id ) ) {
426
+
427
+ 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 );
428
+
429
+ return true;
430
+
431
+ } else {
432
+
433
+ 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() ) );
434
+ return false;
435
+
436
+ }
437
+
438
+ }
439
+
440
+ }
includes/class-wc-square-utils.php ADDED
@@ -0,0 +1,918 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
44
+ $formatted['name'] = __( 'Regular', 'woocommerce-square' );
45
+ $formatted['price_money'] = array(
46
+ 'currency_code' => apply_filters( 'woocommerce_square_currency', get_woocommerce_currency() ),
47
+ 'amount' => (int) WC_Square_Utils::format_amount_to_square( version_compare( WC_VERSION, '3.0.0', '<' ) ? $variation->get_display_price() : wc_get_price_excluding_tax( $variation ) ),
48
+ );
49
+ $formatted['sku'] = $variation->get_sku();
50
+
51
+ if ( $include_inventory && $variation->managing_stock() ) {
52
+ $formatted['track_inventory'] = true;
53
+ }
54
+ }
55
+
56
+ if ( $variation instanceof WC_Product_Variation ) {
57
+
58
+ $formatted['name'] = implode( ', ', $variation->get_variation_attributes() );
59
+
60
+ }
61
+
62
+ return array_filter( $formatted );
63
+
64
+ }
65
+
66
+ /**
67
+ * Convert a WC Product into a Square Item for Update
68
+ *
69
+ * Updates (PUT) don't accept the same parameters (namely variations) as Creation
70
+ * See: https://docs.connect.squareup.com/api/connect/v1/index.html#put-itemid
71
+ *
72
+ * @param WC_Product $wc_product
73
+ * @param bool $include_category
74
+ * @return array
75
+ */
76
+ public static function format_wc_product_update_for_square_api( WC_Product $wc_product, $include_category = false ) {
77
+
78
+ $formatted = array(
79
+ 'name' => $wc_product->get_title(),
80
+ 'description' => wp_strip_all_tags( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->post->post_content : $wc_product->get_description() ),
81
+ 'visibility' => 'PUBLIC',
82
+ );
83
+
84
+ if ( $include_category ) {
85
+ $square_cat_id = self::get_square_category_id_for_wc_product( $wc_product );
86
+
87
+ if ( $square_cat_id ) {
88
+ $formatted['category_id'] = $square_cat_id;
89
+ }
90
+ }
91
+
92
+ return array_filter( $formatted );
93
+ }
94
+
95
+ /**
96
+ * Convert a WC Product into a Square Item for Create
97
+ *
98
+ * Creation (POST) allows more parameters than Updating, namely variations
99
+ * See: https://docs.connect.squareup.com/api/connect/v1/index.html#post-items
100
+ *
101
+ * @param WC_Product $wc_product
102
+ * @param bool $include_category
103
+ * @param bool $include_inventory
104
+ * @return array
105
+ */
106
+ public static function format_wc_product_create_for_square_api( WC_Product $wc_product, $include_category = false, $include_inventory = false ) {
107
+
108
+ $formatted = self::format_wc_product_update_for_square_api( $wc_product, $include_category );
109
+
110
+ // check product type
111
+ if ( 'simple' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->product_type : $wc_product->get_type() ) ) {
112
+
113
+ $formatted['variations'] = array(
114
+ WC_Square_Utils::format_wc_variation_for_square_api( $wc_product, $include_inventory ),
115
+ );
116
+
117
+ } elseif ( 'variable' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->product_type : $wc_product->get_type() ) ) {
118
+
119
+ $wc_variations = self::get_wc_product_variations( $wc_product );
120
+
121
+ foreach ( (array) $wc_variations as $wc_variation ) {
122
+
123
+ $formatted['variations'][] = WC_Square_Utils::format_wc_variation_for_square_api( $wc_variation, $include_inventory );
124
+ }
125
+ }
126
+
127
+ return array_filter( $formatted );
128
+ }
129
+
130
+ /**
131
+ * Map existing WC Variation IDs to a formatted product update array.
132
+ *
133
+ * Square ItemVariations are matched to their WC Variation equivalents via SKU.
134
+ *
135
+ * @param WC_Product $wc_product
136
+ * @param array $product_update Formatted product update. @see WC_Square_Utils::format_square_item_for_wc_api
137
+ *
138
+ * @return array WC Product formatted for update, with Variation IDs mapped.
139
+ */
140
+ public static function set_wc_variation_ids_for_update( WC_Product $wc_product, $product_update ) {
141
+
142
+ if ( ( 'variable' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->product_type : $wc_product->get_type() ) ) && isset( $product_update['variations'] ) ) {
143
+
144
+ $wc_variations = self::get_wc_product_variations( $wc_product );
145
+
146
+ $wc_variation_sku_id_map = array();
147
+
148
+ foreach ( $wc_variations as $wc_variation ) {
149
+ $wc_variation_sku = $wc_variation->get_sku();
150
+
151
+ $wc_variation_id = version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_variation->variation_id : $wc_variation->get_id();
152
+
153
+ if ( ! empty( $wc_variation_sku ) && ! empty( $wc_variation_id ) ) {
154
+
155
+ $wc_variation_sku_id_map[ $wc_variation_sku ] = $wc_variation_id;
156
+ }
157
+ }
158
+
159
+ foreach ( (array) $product_update['variations'] as $idx => $variation ) {
160
+ if ( ! empty( $variation['sku'] ) && isset( $wc_variation_sku_id_map[ $variation['sku'] ] ) ) {
161
+ $product_update['variations'][ $idx ]['id'] = $wc_variation_sku_id_map[ $variation['sku'] ];
162
+ }
163
+ }
164
+ }
165
+
166
+ return $product_update;
167
+ }
168
+
169
+ /**
170
+ * Format a Square Item for an UPDATE through the WC Product API
171
+ *
172
+ * See: https://docs.connect.squareup.com/api/connect/v1/#datatype-item
173
+ * See: https://woothemes.github.io/woocommerce-rest-api-docs/#products-properties
174
+ *
175
+ * @param object $square_item
176
+ * @param WC_Product $wc_product
177
+ * @param bool $include_category
178
+ * @param bool $include_inventory
179
+ * @param bool $include_image
180
+ *
181
+ * @return array
182
+ */
183
+ 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 ) {
184
+
185
+ $formatted = self::format_square_item_for_wc_api_create( $square_item, $include_category, $include_inventory, $include_image );
186
+
187
+ return self::set_wc_variation_ids_for_update( $wc_product, $formatted );
188
+ }
189
+
190
+ /**
191
+ * Format a Square Item for a CREATE through the WC Product API
192
+ *
193
+ * See: https://docs.connect.squareup.com/api/connect/v1/#datatype-item
194
+ * See: https://woothemes.github.io/woocommerce-rest-api-docs/#products-properties
195
+ *
196
+ * @param object $square_item
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_create( $square_item, $include_category = false, $include_inventory = false, $include_image = false ) {
203
+
204
+ $formatted = array(
205
+ 'title' => $square_item->name,
206
+ );
207
+
208
+ if ( apply_filters( 'woocommerce_square_sync_from_square_description', false ) ) {
209
+ $description = ! empty( $square_item->description ) ? $square_item->description : '';
210
+ $formatted['description'] = $description;
211
+ }
212
+
213
+ if ( $include_image && isset( $square_item->master_image->url ) ) {
214
+
215
+ $formatted['images'] = array(
216
+ array(
217
+ 'position' => 0,
218
+ 'src' => $square_item->master_image->url,
219
+ ),
220
+ );
221
+ }
222
+
223
+ if ( $include_category && isset( $square_item->category->id ) ) {
224
+ $wc_cat_id = self::get_wc_category_id_for_square_category_id( $square_item->category->id );
225
+
226
+ if ( $wc_cat_id ) {
227
+ $formatted['categories'] = array( $wc_cat_id );
228
+ }
229
+ }
230
+
231
+ if ( count( $square_item->variations ) > 1 ) {
232
+
233
+ $formatted['type'] = 'variable';
234
+ $formatted['variations'] = array();
235
+
236
+ foreach ( $square_item->variations as $square_item_variation ) {
237
+
238
+ $formatted['variations'][] = self::format_square_item_variation_for_wc_api( $square_item_variation, $include_inventory );
239
+
240
+ }
241
+
242
+ $formatted['attributes'] = array(
243
+ array(
244
+ 'name' => 'Attribute',
245
+ 'slug' => 'attribute',
246
+ 'position' => 0,
247
+ 'visible' => true,
248
+ 'variation' => true,
249
+ 'options' => wp_list_pluck( $square_item->variations, 'name' ),
250
+ ),
251
+ );
252
+
253
+ } else {
254
+
255
+ $variation = self::format_square_item_variation_for_wc_api( $square_item->variations[0], $include_inventory );
256
+
257
+ $formatted['type'] = 'simple';
258
+ $formatted['sku'] = isset( $variation['sku'] ) ? $variation['sku'] : null;
259
+ $formatted['regular_price'] = isset( $variation['regular_price'] ) ? $variation['regular_price'] : null;
260
+ $formatted['stock_quantity'] = isset( $variation['stock_quantity'] ) ? $variation['stock_quantity'] : null;
261
+ $formatted['managing_stock'] = isset( $variation['managing_stock'] ) ? $variation['managing_stock'] : null;
262
+
263
+ }
264
+
265
+ return array_filter( $formatted );
266
+ }
267
+
268
+ /**
269
+ * Convert a Square ItemVariation for the WC Product API
270
+ *
271
+ * See: https://docs.connect.squareup.com/api/connect/v1/#datatype-itemvariation
272
+ * See: https://woothemes.github.io/woocommerce-rest-api-docs/#products-properties
273
+ *
274
+ * @param object $square_item_variation
275
+ * @return array
276
+ */
277
+ public static function format_square_item_variation_for_wc_api( $square_item_variation, $include_inventory = false ) {
278
+
279
+ $formatted = array(
280
+ 'sku' => ! empty( $square_item_variation->sku ) ? $square_item_variation->sku : '',
281
+ 'regular_price' => self::format_square_price_for_wc( $square_item_variation->price_money->amount ),
282
+ 'stock_quantity' => null,
283
+ 'attributes' => array(
284
+ array(
285
+ 'name' => 'Attribute',
286
+ 'option' => ! empty( $square_item_variation->name ) ? $square_item_variation->name : '',
287
+ ),
288
+ ),
289
+ );
290
+
291
+ if ( $include_inventory ) {
292
+ $formatted['managing_stock'] = $square_item_variation->track_inventory ? true : null;
293
+ }
294
+
295
+ return array_filter( $formatted );
296
+ }
297
+
298
+ /**
299
+ * Formats the price coming from Square as they use the lowest denominator ex. cents
300
+ *
301
+ * See: https://docs.connect.squareup.com/api/connect/v1/#workingwithmonetaryamounts
302
+ *
303
+ * @param int $price
304
+ * @return int
305
+ */
306
+ public static function format_square_price_for_wc( $price = 0 ) {
307
+ return apply_filters( 'woocommerce_square_format_price', self::format_amount_from_square( $price ) );
308
+ }
309
+
310
+ /**
311
+ * Retrieve the Square ID for a WC Term
312
+ *
313
+ * @param int $wc_term_id
314
+ * @return mixed See get_woocommerce_term_meta()
315
+ */
316
+ public static function get_wc_term_square_id( $wc_term_id ) {
317
+ return get_woocommerce_term_meta( $wc_term_id, self::WC_TERM_SQUARE_ID );
318
+ }
319
+
320
+ /**
321
+ * Update the Square ID for a WC Term
322
+ *
323
+ * @param int $wc_term_id
324
+ * @param string $square_id
325
+ * @return bool See update_woocommerce_term_meta()
326
+ */
327
+ public static function update_wc_term_square_id( $wc_term_id, $square_id ) {
328
+ return update_woocommerce_term_meta( $wc_term_id, self::WC_TERM_SQUARE_ID, $square_id );
329
+ }
330
+
331
+ /**
332
+ * Retrieve the Square ID for a WC Product
333
+ *
334
+ * @param int $wc_product_id
335
+ * @return array|mixed See get_post_meta()
336
+ */
337
+ public static function get_wc_product_square_id( $wc_product_id ) {
338
+ return get_post_meta( $wc_product_id, self::WC_PRODUCT_SQUARE_ID, true );
339
+ }
340
+
341
+ /**
342
+ * Update the Square ID for a WC Product
343
+ *
344
+ * @param int $wc_product_id
345
+ * @param string $square_id
346
+ * @return bool|int See update_post_meta()
347
+ */
348
+ public static function update_wc_product_square_id( $wc_product_id, $square_id ) {
349
+ return update_post_meta( $wc_product_id, self::WC_PRODUCT_SQUARE_ID, $square_id );
350
+ }
351
+
352
+ /**
353
+ * Retrieve the Square ID for a WC Product Variation
354
+ *
355
+ * @param int $wc_variation_id
356
+ * @return array|mixed See get_post_meta()
357
+ */
358
+ public static function get_wc_variation_square_id( $wc_variation_id ) {
359
+ return get_post_meta( $wc_variation_id, self::WC_VARIATION_SQUARE_ID, true );
360
+ }
361
+
362
+ /**
363
+ * Update the Square ID for a WC Product Variation
364
+ *
365
+ * @param int $wc_variation_id
366
+ * @param string $square_id
367
+ * @return bool|int See update_post_meta()
368
+ */
369
+ public static function update_wc_variation_square_id( $wc_variation_id, $square_id ) {
370
+ return update_post_meta( $wc_variation_id, self::WC_VARIATION_SQUARE_ID, $square_id );
371
+ }
372
+
373
+ /**
374
+ * Get all SKUs associated with a WC Product (could be many, if variable).
375
+ *
376
+ * @param WC_Product $wc_product
377
+ * @return array
378
+ */
379
+ public static function get_wc_product_skus( WC_Product $wc_product ) {
380
+ $wc_product_skus = array();
381
+
382
+ if ( 'simple' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->product_type : $wc_product->get_type() ) ) {
383
+
384
+ $wc_product_skus[] = $wc_product->get_sku();
385
+
386
+ } elseif ( 'variable' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->product_type : $wc_product->get_type() ) ) {
387
+ $wc_variations = self::get_wc_product_variations( $wc_product );
388
+
389
+ if ( version_compare( WC_VERSION, '3.0.0', '<' ) ) {
390
+ $wc_product_skus = wp_list_pluck( $wc_variations, 'sku' );
391
+ } else {
392
+ foreach ( $wc_variations as $wc_variation ) {
393
+ $wc_product_skus[] = $wc_variation->get_sku();
394
+ }
395
+ }
396
+ }
397
+
398
+ // SKUs are optional, so let's only return ones that have values
399
+ return array_filter( $wc_product_skus );
400
+ }
401
+
402
+ /**
403
+ * Determine which WC Product Category to send to Square.
404
+ *
405
+ * Returns the first top-level Category that has an associated Square ID.
406
+ *
407
+ * @param WC_Product $wc_product
408
+ * @return bool|mixed
409
+ */
410
+ public static function get_square_category_id_for_wc_product( WC_Product $wc_product ) {
411
+ $wc_categories = wp_get_post_terms( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->id : $wc_product->get_id(), 'product_cat' );
412
+
413
+ if ( is_wp_error( $wc_categories ) && empty( $wc_categories ) ) {
414
+
415
+ return false;
416
+
417
+ }
418
+
419
+ foreach ( $wc_categories as $category ) {
420
+
421
+ if ( $category->parent ) {
422
+
423
+ $ancestors = get_ancestors( $category->term_id, 'product_cat', 'taxonomy' );
424
+ $top_level_id = end( $ancestors );
425
+
426
+ } else {
427
+
428
+ $top_level_id = $category->term_id;
429
+
430
+ }
431
+
432
+ $square_cat_id = self::get_wc_term_square_id( $top_level_id );
433
+
434
+ if ( $square_cat_id ) {
435
+ return $square_cat_id;
436
+ }
437
+ }
438
+
439
+ return false;
440
+ }
441
+
442
+ /**
443
+ * Retrieve the Square Item Image ID for a WC Product
444
+ *
445
+ * @param int $wc_product_id
446
+ * @return array|mixed See get_post_meta()
447
+ */
448
+ public static function get_wc_product_image_square_id( $wc_product_id ) {
449
+ return get_post_meta( $wc_product_id, self::WC_PRODUCT_IMAGE_SQUARE_ID, true );
450
+ }
451
+
452
+ /**
453
+ * Update the Square Item Image ID for a WC Product
454
+ *
455
+ * @param int $wc_product_id
456
+ * @param string $square_id
457
+ * @return bool|int See update_post_meta()
458
+ */
459
+ public static function update_wc_product_image_square_id( $wc_product_id, $square_image_id ) {
460
+ return update_post_meta( $wc_product_id, self::WC_PRODUCT_IMAGE_SQUARE_ID, $square_image_id );
461
+ }
462
+
463
+ /**
464
+ * Retrieve the WC Category ID that corresponds to a given Square Category ID.
465
+ *
466
+ * @param string $square_cat_id
467
+ * @return bool|int WC Category ID on successful match, boolean false otherwise.
468
+ */
469
+ public static function get_wc_category_id_for_square_category_id( $square_cat_id ) {
470
+ $categories = get_terms( 'product_cat', array(
471
+ 'parent' => 0,
472
+ 'hide_empty' => false,
473
+ 'fields' => 'ids',
474
+ ) );
475
+
476
+ if ( is_wp_error( $categories ) ) {
477
+ WC_Square_Sync_Logger::log( sprintf( '%s::%s - Taxonomy "product_cat" not found. Make sure WooCommerce is enabled.', __CLASS__, __FUNCTION__ ) );
478
+ return false;
479
+ }
480
+
481
+ foreach ( $categories as $wc_category ) {
482
+ $wc_square_cat_id = self::get_wc_term_square_id( $wc_category );
483
+
484
+ if ( $wc_square_cat_id && ( $square_cat_id === $wc_square_cat_id ) ) {
485
+ return $wc_category;
486
+ }
487
+ }
488
+
489
+ return false;
490
+ }
491
+
492
+ /**
493
+ * Attempt to find a WC Product that corresponds to a given Square Item.
494
+ *
495
+ * This function first queries for a WC Product already associated to the
496
+ * Square Item's ID. If none found, all WC Products (and variations) are
497
+ * queried using the SKUs present in the Square Item's Variations. If a
498
+ * match is found, the parent (non-variant) WC Product is returned.
499
+ *
500
+ * @param object $square_item
501
+ * @return bool|WC_Product Corresponding WC_Product on successful match, boolean false otherwise.
502
+ */
503
+ public static function get_wc_product_for_square_item( $square_item ) {
504
+ if ( ! is_object( $square_item ) || ! property_exists( $square_item, 'id' ) ) {
505
+ return false;
506
+ }
507
+
508
+ $wc_product_ids = get_posts( array(
509
+ 'post_type' => 'product',
510
+ 'post_status' => 'publish', // this is ignored
511
+ 'meta_query' => array(
512
+ array(
513
+ 'key' => self::WC_PRODUCT_SQUARE_ID,
514
+ 'compare' => '=',
515
+ 'value' => $square_item->id,
516
+ ),
517
+ ),
518
+ 'posts_per_page' => 1,
519
+ 'fields' => 'ids',
520
+ ) );
521
+
522
+ if ( ! empty( $wc_product_ids ) ) {
523
+ $wc_product = wc_get_product( $wc_product_ids[0] );
524
+
525
+ // only return publish products
526
+ if ( 'publish' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->post->post_status : $wc_product->get_status() ) ) {
527
+ return $wc_product;
528
+ }
529
+ }
530
+
531
+ $square_item_skus = self::get_square_item_skus( $square_item );
532
+
533
+ if ( empty( $square_item_skus ) ) {
534
+ return false;
535
+ }
536
+
537
+ $wc_product_ids = get_posts( array(
538
+ 'post_type' => array( 'product', 'product_variation' ),
539
+ 'post_status' => 'publish', // this is ignored
540
+ 'meta_query' => array(
541
+ array(
542
+ 'key' => '_sku',
543
+ 'compare' => 'IN',
544
+ 'value' => $square_item_skus,
545
+ ),
546
+ ),
547
+ 'posts_per_page' => 1,
548
+ 'fields' => 'ids',
549
+ ) );
550
+
551
+ if ( ! empty( $wc_product_ids ) ) {
552
+ $wc_product = wc_get_product( $wc_product_ids[0] );
553
+
554
+ if ( ! is_object( $wc_product ) ) {
555
+ return false;
556
+ }
557
+
558
+ if ( 'publish' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->post->post_status : $wc_product->get_status() ) ) {
559
+ if ( 'simple' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->product_type : $wc_product->get_type() ) ) {
560
+
561
+ return $wc_product;
562
+
563
+ }
564
+
565
+ return wc_get_product( ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->parent : $wc_product->get_parent_id() ) );
566
+ }
567
+ }
568
+
569
+ return false;
570
+ }
571
+
572
+ /**
573
+ * Attempt to find a WC Product that corresponds to a given Square Item.
574
+ *
575
+ * @param string $square_variation_id
576
+ * @return bool|WC_Product Corresponding WC_Product on successful match, boolean false otherwise.
577
+ */
578
+ public static function get_wc_product_for_square_item_variation_id( $square_variation_id ) {
579
+ $wc_product_ids = get_posts( array(
580
+ 'post_type' => array( 'product', 'product_variation' ),
581
+ 'post_status' => 'publish', // this is ignored
582
+ 'meta_query' => array(
583
+ array(
584
+ 'key' => self::WC_VARIATION_SQUARE_ID,
585
+ 'compare' => '=',
586
+ 'value' => $square_variation_id,
587
+ ),
588
+ ),
589
+ 'posts_per_page' => 1,
590
+ 'fields' => 'ids',
591
+ ) );
592
+
593
+ if ( ! empty( $wc_product_ids ) ) {
594
+ $product = wc_get_product( $wc_product_ids[0] );
595
+
596
+ // only return publish products
597
+ if ( 'publish' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->post->post_status : $product->get_status() ) ) {
598
+ return $product;
599
+ }
600
+ }
601
+
602
+ return false;
603
+ }
604
+
605
+ /**
606
+ * Checks if all SKUs have been set for the Square Item.
607
+ * We do not want to sync the item if not all SKU is set.
608
+ *
609
+ * @since 1.0.14
610
+ * @version 1.0.14
611
+ * @param object $square_item
612
+ * @return bool
613
+ */
614
+ public static function is_square_item_skus_set( $square_item ) {
615
+ if ( empty( $square_item ) || empty( $square_item->variations ) ) {
616
+
617
+ return false;
618
+ }
619
+
620
+ foreach ( $square_item->variations as $item_variation ) {
621
+ // If even one sku is missing we don't want to sync.
622
+ if ( empty( $item_variation->sku ) ) {
623
+
624
+ return false;
625
+ }
626
+ }
627
+
628
+ return true;
629
+ }
630
+
631
+ /**
632
+ * Return array of SKUs from all variations of a Square Item
633
+ *
634
+ * @param object $square_item
635
+ * @return array
636
+ */
637
+ public static function get_square_item_skus( $square_item ) {
638
+
639
+ $item_skus = array();
640
+
641
+ if ( empty( $square_item->variations ) ) {
642
+
643
+ return $item_skus;
644
+ }
645
+
646
+ foreach ( $square_item->variations as $item_variation ) {
647
+ if ( ! empty( $item_variation->sku ) ) {
648
+
649
+ $item_skus[] = $item_variation->sku;
650
+ }
651
+ }
652
+
653
+ return $item_skus;
654
+ }
655
+
656
+ /**
657
+ * Store Square Item ID and ItemVariation IDs on a WC Product and it's variations,
658
+ * matching using the SKU value.
659
+ *
660
+ * This is most useful in WC->Square creation, and Square->WC operations.
661
+ *
662
+ * @param WC_Product $wc_product
663
+ * @param object $square_item
664
+ */
665
+ public static function set_square_ids_on_wc_product_by_sku( WC_Product $wc_product, $square_item ) {
666
+ if ( ! is_object( $square_item ) || ! property_exists( $square_item, 'id' ) ) {
667
+ return false;
668
+ }
669
+
670
+ self::update_wc_product_square_id( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->id : $wc_product->get_id(), $square_item->id );
671
+
672
+ if ( 'simple' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->product_type : $wc_product->get_type() ) ) {
673
+
674
+ 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 );
675
+
676
+ } elseif ( 'variable' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->product_type : $wc_product->get_type() ) ) {
677
+
678
+ // Create mapping of Square ItemVariation SKU => ID
679
+ $square_variations = array();
680
+
681
+ foreach ( $square_item->variations as $square_variation ) {
682
+
683
+ if ( ! empty( $square_variation->sku ) ) {
684
+
685
+ $square_variations[ $square_variation->sku ] = $square_variation->id;
686
+
687
+ }
688
+ }
689
+
690
+ // Create mapping of WC Variation SKU => ID
691
+ $wc_item_variations = self::get_wc_product_variations( $wc_product );
692
+ $wc_variations = array();
693
+
694
+ foreach ( $wc_item_variations as $wc_item_variation ) {
695
+ $wc_variation_sku = $wc_item_variation->get_sku();
696
+
697
+ if ( ! empty( $wc_variation_sku ) ) {
698
+ $wc_variations[ $wc_variation_sku ] = version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_item_variation->variation_id : $wc_item_variation->get_id();
699
+ }
700
+ }
701
+
702
+ // Map the WC Variations to their Square ItemVariation counterparts
703
+ foreach ( $wc_variations as $sku => $wc_variation_id ) {
704
+ if ( array_key_exists( $sku, $square_variations ) ) {
705
+
706
+ self::update_wc_variation_square_id( $wc_variation_id, $square_variations[ $sku ] );
707
+ }
708
+ }
709
+ }
710
+ }
711
+
712
+ /**
713
+ * Retrieve WC Variation IDs for a given WC Product, that we're managing stock for.
714
+ *
715
+ * @param WC_Product $wc_product
716
+ * @return array
717
+ */
718
+ public static function get_stock_managed_wc_variation_ids( WC_Product $wc_product ) {
719
+ $wc_product = wc_get_product( $wc_product->get_id() );
720
+
721
+ $wc_variation_ids = array();
722
+
723
+ if ( ! is_object( $wc_product ) ) {
724
+ return $wc_variation_ids;
725
+ }
726
+
727
+ if ( 'simple' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->product_type : $wc_product->get_type() ) ) {
728
+
729
+ if ( $wc_product->managing_stock() ) {
730
+
731
+ $wc_variation_ids = array( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->id : $wc_product->get_id() );
732
+
733
+ }
734
+ } elseif ( 'variable' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_product->product_type : $wc_product->get_type() ) ) {
735
+
736
+ $variations = self::get_wc_product_variations( $wc_product );
737
+
738
+ foreach ( (array) $variations as $variation ) {
739
+
740
+ if ( $variation->managing_stock() ) {
741
+
742
+ $wc_variation_ids[] = version_compare( WC_VERSION, '3.0.0', '<' ) ? $variation->variation_id : $variation->get_id();
743
+
744
+ }
745
+ }
746
+ }
747
+
748
+ return $wc_variation_ids;
749
+ }
750
+
751
+ /**
752
+ * Get all variations of a given WC_Product_Variable.
753
+ *
754
+ * @param WC_Product_Variable $wc_variable_product
755
+ * @return array Array of WC_Product_Variation objects.
756
+ */
757
+ public static function get_wc_product_variations( WC_Product_Variable $wc_variable_product ) {
758
+ $variations = array();
759
+
760
+ foreach ( $wc_variable_product->get_children() as $child_id ) {
761
+
762
+ $variation = version_compare( WC_VERSION, '3.0.0', '<' ) ? $wc_variable_product->get_child( $child_id ) : wc_get_product( $child_id );
763
+
764
+ $variation_id = version_compare( WC_VERSION, '3.0.0', '<' ) ? $variation->variation_id : $variation->get_id();
765
+
766
+ if ( empty( $variation_id ) ) {
767
+ continue;
768
+ }
769
+
770
+ $variations[] = $variation;
771
+ }
772
+
773
+ return $variations;
774
+ }
775
+
776
+ /**
777
+ * Check if the square item is found
778
+ *
779
+ * @param object $square_item
780
+ * @return bool
781
+ */
782
+ public static function is_square_item_found( $square_item ) {
783
+ if ( is_object( $square_item ) && 'not_found' !== $square_item->type ) {
784
+ return true;
785
+ }
786
+
787
+ return false;
788
+ }
789
+
790
+ /**
791
+ * Checks to see if product disable sync is enabled
792
+ *
793
+ * @param int $product_id parent product id
794
+ * @return bool
795
+ */
796
+ public static function skip_product_sync( $product_id = null ) {
797
+ if ( null === $product_id ) {
798
+ return false;
799
+ }
800
+
801
+ $skip_sync = get_post_meta( $product_id, '_wcsquare_disable_sync', true );
802
+
803
+ if ( 'yes' === $skip_sync ) {
804
+ return true;
805
+ }
806
+
807
+ return false;
808
+ }
809
+
810
+ /**
811
+ * Process amount to be passed to Square.
812
+ * @return float
813
+ */
814
+ public static function format_amount_to_square( $total, $currency = '' ) {
815
+ if ( ! $currency ) {
816
+ $currency = get_woocommerce_currency();
817
+ }
818
+
819
+ switch ( strtoupper( $currency ) ) {
820
+ // Zero decimal currencies
821
+ case 'BIF':
822
+ case 'CLP':
823
+ case 'DJF':
824
+ case 'GNF':
825
+ case 'JPY':
826
+ case 'KMF':
827
+ case 'KRW':
828
+ case 'MGA':
829
+ case 'PYG':
830
+ case 'RWF':
831
+ case 'VND':
832
+ case 'VUV':
833
+ case 'XAF':
834
+ case 'XOF':
835
+ case 'XPF':
836
+ $total = absint( $total );
837
+ break;
838
+ default:
839
+ $total = absint( wc_format_decimal( ( (float) $total * 100 ), wc_get_price_decimals() ) ); // In cents.
840
+ break;
841
+ }
842
+
843
+ return $total;
844
+ }
845
+
846
+ /**
847
+ * Process amount to be passed from Square.
848
+ * @return float
849
+ */
850
+ public static function format_amount_from_square( $total, $currency = '' ) {
851
+ if ( ! $currency ) {
852
+ $currency = get_woocommerce_currency();
853
+ }
854
+
855
+ switch ( strtoupper( $currency ) ) {
856
+ // Zero decimal currencies
857
+ case 'BIF':
858
+ case 'CLP':
859
+ case 'DJF':
860
+ case 'GNF':
861
+ case 'JPY':
862
+ case 'KMF':
863
+ case 'KRW':
864
+ case 'MGA':
865
+ case 'PYG':
866
+ case 'RWF':
867
+ case 'VND':
868
+ case 'VUV':
869
+ case 'XAF':
870
+ case 'XOF':
871
+ case 'XPF':
872
+ $total = absint( $total );
873
+ break;
874
+ default:
875
+ $total = wc_format_decimal( absint( $total ) / 100 );
876
+ break;
877
+ }
878
+
879
+ return $total;
880
+ }
881
+
882
+ /**
883
+ * This is for developers to test with their own staging access with Square.
884
+ * This is usually not accessible by regular merchants.
885
+ *
886
+ * @param string $environment
887
+ * @return string
888
+ */
889
+ public static function is_staging( $environment = null ) {
890
+ if ( ! empty( $environment ) && 'staging' === $environment && defined( 'WP_DEBUG' ) && WP_DEBUG ) {
891
+ return true;
892
+ }
893
+
894
+ return false;
895
+ }
896
+
897
+ /**
898
+ * Deletes all transients.
899
+ *
900
+ * @since 1.0.17
901
+ * @version 1.0.17
902
+ */
903
+ public static function delete_transients() {
904
+ delete_transient( 'wc_square_processing_total_count' );
905
+
906
+ delete_transient( 'wc_square_processing_ids' );
907
+
908
+ delete_transient( 'wc_square_syncing_square_inventory' );
909
+
910
+ delete_transient( 'sq_wc_sync_current_process' );
911
+
912
+ delete_transient( 'wc_square_inventory' );
913
+
914
+ delete_transient( 'wc_square_polling' );
915
+
916
+ return true;
917
+ }
918
+ }
includes/class-wc-square-wc-products.php ADDED
@@ -0,0 +1,2458 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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' ), $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' ), $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' ), $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' ), $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' ), $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' ), $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' ), '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' ), 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' ), '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' ), 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' ), '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' ), 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' ), 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' ), 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' ), 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' ), 404 );
493
+ }
494
+
495
+ $term_id = intval( $term->term_id );
496
+
497
+ // Get category display type
498
+ $display_type = get_woocommerce_term_meta( $term_id, 'display_type' );
499
+
500
+ // Get category image
501
+ $image = '';
502
+ if ( $image_id = get_woocommerce_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' ), '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' ), 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' ), 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
+ update_woocommerce_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
+ update_woocommerce_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' ), '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' ), 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' ), 400 );
650
+ }
651
+
652
+ if ( ! empty( $data['display'] ) ) {
653
+ update_woocommerce_term_meta( $id, 'display_type', 'default' === $data['display'] ? '' : sanitize_text_field( $data['display'] ) );
654
+ }
655
+
656
+ if ( isset( $image_id ) ) {
657
+ update_woocommerce_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' ), 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
+ }
1145
+
1146
+ if ( 'grouped' === $product_type ) {
1147
+
1148
+ update_post_meta( $product_id, '_manage_stock', 'no' );
1149
+ update_post_meta( $product_id, '_backorders', 'no' );
1150
+ update_post_meta( $product_id, '_stock', '' );
1151
+
1152
+ wc_update_product_stock_status( $product_id, $stock_status );
1153
+
1154
+ } elseif ( 'external' === $product_type ) {
1155
+
1156
+ update_post_meta( $product_id, '_manage_stock', 'no' );
1157
+ update_post_meta( $product_id, '_backorders', 'no' );
1158
+ update_post_meta( $product_id, '_stock', '' );
1159
+
1160
+ wc_update_product_stock_status( $product_id, 'instock' );
1161
+ } elseif ( 'yes' === $managing_stock ) {
1162
+ update_post_meta( $product_id, '_backorders', $backorders );
1163
+
1164
+ // Stock status is always determined by children so sync later.
1165
+ if ( 'variable' !== $product_type ) {
1166
+ wc_update_product_stock_status( $product_id, $stock_status );
1167
+ }
1168
+
1169
+ // Stock quantity.
1170
+ if ( isset( $data['stock_quantity'] ) ) {
1171
+ wc_update_product_stock( $product_id, wc_stock_amount( $data['stock_quantity'] ) );
1172
+ } elseif ( isset( $data['inventory_delta'] ) ) {
1173
+ $stock_quantity = wc_stock_amount( get_post_meta( $product_id, '_stock', true ) );
1174
+ $stock_quantity += wc_stock_amount( $data['inventory_delta'] );
1175
+
1176
+ wc_update_product_stock( $product_id, wc_stock_amount( $stock_quantity ) );
1177
+ }
1178
+ } else {
1179
+
1180
+ // Don't manage stock.
1181
+ update_post_meta( $product_id, '_manage_stock', 'no' );
1182
+ update_post_meta( $product_id, '_backorders', $backorders );
1183
+ update_post_meta( $product_id, '_stock', '' );
1184
+
1185
+ wc_update_product_stock_status( $product_id, $stock_status );
1186
+ }
1187
+ } elseif ( 'variable' !== $product_type ) {
1188
+ wc_update_product_stock_status( $product_id, $stock_status );
1189
+ }
1190
+
1191
+ // Upsells.
1192
+ if ( isset( $data['upsell_ids'] ) ) {
1193
+ $upsells = array();
1194
+ $ids = $data['upsell_ids'];
1195
+
1196
+ if ( ! empty( $ids ) ) {
1197
+ foreach ( $ids as $id ) {
1198
+ if ( $id && $id > 0 ) {
1199
+ $upsells[] = $id;
1200
+ }
1201
+ }
1202
+
1203
+ update_post_meta( $product_id, '_upsell_ids', $upsells );
1204
+ } else {
1205
+ delete_post_meta( $product_id, '_upsell_ids' );
1206
+ }
1207
+ }
1208
+
1209
+ // Cross sells.
1210
+ if ( isset( $data['cross_sell_ids'] ) ) {
1211
+ $crosssells = array();
1212
+ $ids = $data['cross_sell_ids'];
1213
+
1214
+ if ( ! empty( $ids ) ) {
1215
+ foreach ( $ids as $id ) {
1216
+ if ( $id && $id > 0 ) {
1217
+ $crosssells[] = $id;
1218
+ }
1219
+ }
1220
+
1221
+ update_post_meta( $product_id, '_crosssell_ids', $crosssells );
1222
+ } else {
1223
+ delete_post_meta( $product_id, '_crosssell_ids' );
1224
+ }
1225
+ }
1226
+
1227
+ // Product categories.
1228
+ if ( isset( $data['categories'] ) && is_array( $data['categories'] ) ) {
1229
+ $term_ids = array_unique( array_map( 'intval', $data['categories'] ) );
1230
+ wp_set_object_terms( $product_id, $term_ids, 'product_cat' );
1231
+ }
1232
+
1233
+ // Product tags.
1234
+ if ( isset( $data['tags'] ) && is_array( $data['tags'] ) ) {
1235
+ $term_ids = array_unique( array_map( 'intval', $data['tags'] ) );
1236
+ wp_set_object_terms( $product_id, $term_ids, 'product_tag' );
1237
+ }
1238
+
1239
+ // Downloadable.
1240
+ if ( isset( $data['downloadable'] ) ) {
1241
+ $is_downloadable = ( true === $data['downloadable'] ) ? 'yes' : 'no';
1242
+ update_post_meta( $product_id, '_downloadable', $is_downloadable );
1243
+ } else {
1244
+ $is_downloadable = get_post_meta( $product_id, '_downloadable', true );
1245
+ }
1246
+
1247
+ // Downloadable options.
1248
+ if ( 'yes' == $is_downloadable ) {
1249
+
1250
+ // Downloadable files.
1251
+ if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) {
1252
+ $this->save_downloadable_files( $product_id, $data['downloads'] );
1253
+ }
1254
+
1255
+ // Download limit.
1256
+ if ( isset( $data['download_limit'] ) ) {
1257
+ update_post_meta( $product_id, '_download_limit', ( '' === $data['download_limit'] ) ? '' : absint( $data['download_limit'] ) );
1258
+ }
1259
+
1260
+ // Download expiry.
1261
+ if ( isset( $data['download_expiry'] ) ) {
1262
+ update_post_meta( $product_id, '_download_expiry', ( '' === $data['download_expiry'] ) ? '' : absint( $data['download_expiry'] ) );
1263
+ }
1264
+
1265
+ // Download type.
1266
+ if ( isset( $data['download_type'] ) ) {
1267
+ update_post_meta( $product_id, '_download_type', wc_clean( $data['download_type'] ) );
1268
+ }
1269
+ }
1270
+
1271
+ // Product url.
1272
+ if ( 'external' === $product_type ) {
1273
+ if ( isset( $data['product_url'] ) ) {
1274
+ update_post_meta( $product_id, '_product_url', wc_clean( $data['product_url'] ) );
1275
+ }
1276
+
1277
+ if ( isset( $data['button_text'] ) ) {
1278
+ update_post_meta( $product_id, '_button_text', wc_clean( $data['button_text'] ) );
1279
+ }
1280
+ }
1281
+
1282
+ // Reviews allowed.
1283
+ if ( isset( $data['reviews_allowed'] ) ) {
1284
+ $reviews_allowed = ( true === $data['reviews_allowed'] ) ? 'open' : 'closed';
1285
+
1286
+ $wpdb->update( $wpdb->posts, array( 'comment_status' => $reviews_allowed ), array( 'ID' => $product_id ) );
1287
+ }
1288
+
1289
+ // Do action for product type
1290
+ do_action( 'woocommerce_api_process_product_meta_' . $product_type, $product_id, $data );
1291
+
1292
+ return true;
1293
+ }
1294
+
1295
+ /**
1296
+ * Save variations
1297
+ *
1298
+ * @since 2.2
1299
+ * @param int $id
1300
+ * @param array $data
1301
+ * @return bool
1302
+ * @throws WC_Square_API_Exception
1303
+ */
1304
+ protected function save_variations( $id, $data ) {
1305
+ global $wpdb;
1306
+
1307
+ $variations = $data['variations'];
1308
+ $attributes = (array) maybe_unserialize( get_post_meta( $id, '_product_attributes', true ) );
1309
+
1310
+ foreach ( $variations as $menu_order => $variation ) {
1311
+ $variation_id = isset( $variation['id'] ) ? absint( $variation['id'] ) : 0;
1312
+
1313
+ if ( ! $variation_id && isset( $variation['sku'] ) ) {
1314
+ $variation_sku = wc_clean( $variation['sku'] );
1315
+ $variation_id = wc_get_product_id_by_sku( $variation_sku );
1316
+ }
1317
+
1318
+ // Generate a useful post title
1319
+ $variation_post_title = sprintf( __( 'Variation #%1$s of %2$s', 'woocommerce' ), $variation_id, esc_html( get_the_title( $id ) ) );
1320
+
1321
+ // Update or Add post
1322
+ if ( ! $variation_id ) {
1323
+ $post_status = ( isset( $variation['visible'] ) && false === $variation['visible'] ) ? 'private' : 'publish';
1324
+
1325
+ $new_variation = array(
1326
+ 'post_title' => $variation_post_title,
1327
+ 'post_content' => '',
1328
+ 'post_status' => $post_status,
1329
+ 'post_author' => get_current_user_id(),
1330
+ 'post_parent' => $id,
1331
+ 'post_type' => 'product_variation',
1332
+ 'menu_order' => $menu_order,
1333
+ );
1334
+
1335
+ $variation_id = wp_insert_post( $new_variation );
1336
+
1337
+ do_action( 'woocommerce_create_product_variation', $variation_id );
1338
+ } else {
1339
+ $update_variation = array( 'post_title' => $variation_post_title, 'menu_order' => $menu_order );
1340
+ if ( isset( $variation['visible'] ) ) {
1341
+ $post_status = ( false === $variation['visible'] ) ? 'private' : 'publish';
1342
+ $update_variation['post_status'] = $post_status;
1343
+ }
1344
+
1345
+ $wpdb->update( $wpdb->posts, $update_variation, array( 'ID' => $variation_id ) );
1346
+
1347
+ do_action( 'woocommerce_update_product_variation', $variation_id );
1348
+ }
1349
+
1350
+ // Stop with we don't have a variation ID
1351
+ if ( is_wp_error( $variation_id ) ) {
1352
+ throw new WC_Square_API_Exception( 'woocommerce_api_cannot_save_product_variation', $variation_id->get_error_message(), 400 );
1353
+ }
1354
+
1355
+ // SKU
1356
+ if ( isset( $variation['sku'] ) ) {
1357
+ $sku = get_post_meta( $variation_id, '_sku', true );
1358
+ $new_sku = wc_clean( $variation['sku'] );
1359
+
1360
+ if ( '' == $new_sku ) {
1361
+ update_post_meta( $variation_id, '_sku', '' );
1362
+ } elseif ( $new_sku !== $sku ) {
1363
+ if ( ! empty( $new_sku ) ) {
1364
+ $unique_sku = wc_product_has_unique_sku( $variation_id, $new_sku );
1365
+ if ( ! $unique_sku ) {
1366
+ throw new WC_Square_API_Exception( 'woocommerce_api_product_sku_already_exists', __( 'The SKU already exists on another product', 'woocommerce' ), 400 );
1367
+ } else {
1368
+ update_post_meta( $variation_id, '_sku', $new_sku );
1369
+ }
1370
+ } else {
1371
+ update_post_meta( $variation_id, '_sku', '' );
1372
+ }
1373
+ }
1374
+ }
1375
+
1376
+ // Thumbnail.
1377
+ if ( isset( $variation['image'] ) && is_array( $variation['image'] ) ) {
1378
+ $image = current( $variation['image'] );
1379
+ if ( $image && is_array( $image ) ) {
1380
+ if ( isset( $image['position'] ) && 0 == $image['position'] ) {
1381
+ if ( isset( $image['src'] ) ) {
1382
+ $upload = $this->upload_product_image( wc_clean( $image['src'] ) );
1383
+
1384
+ if ( is_wp_error( $upload ) ) {
1385
+ throw new WC_Square_API_Exception( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), 400 );
1386
+ }
1387
+
1388
+ $attachment_id = $this->set_product_image_as_attachment( $upload, $id );
1389
+ } elseif ( isset( $image['id'] ) ) {
1390
+ $attachment_id = $image['id'];
1391
+ }
1392
+
1393
+ // Set the image alt if present.
1394
+ if ( ! empty( $image['alt'] ) ) {
1395
+ update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) );
1396
+ }
1397
+
1398
+ // Set the image title if present.
1399
+ if ( ! empty( $image['title'] ) ) {
1400
+ wp_update_post( array( 'ID' => $attachment_id, 'post_title' => $image['title'] ) );
1401
+ }
1402
+
1403
+ update_post_meta( $variation_id, '_thumbnail_id', $attachment_id );
1404
+ }
1405
+ } else {
1406
+ delete_post_meta( $variation_id, '_thumbnail_id' );
1407
+ }
1408
+ }
1409
+
1410
+ // Virtual variation
1411
+ if ( isset( $variation['virtual'] ) ) {
1412
+ $is_virtual = ( true === $variation['virtual'] ) ? 'yes' : 'no';
1413
+ update_post_meta( $variation_id, '_virtual', $is_virtual );
1414
+ }
1415
+
1416
+ // Downloadable variation
1417
+ if ( isset( $variation['downloadable'] ) ) {
1418
+ $is_downloadable = ( true === $variation['downloadable'] ) ? 'yes' : 'no';
1419
+ update_post_meta( $variation_id, '_downloadable', $is_downloadable );
1420
+ } else {
1421
+ $is_downloadable = get_post_meta( $variation_id, '_downloadable', true );
1422
+ }
1423
+
1424
+ // Shipping data
1425
+ $this->save_product_shipping_data( $variation_id, $variation );
1426
+
1427
+ // Stock handling
1428
+ if ( isset( $variation['managing_stock'] ) ) {
1429
+ $managing_stock = ( true === $variation['managing_stock'] ) ? 'yes' : 'no';
1430
+ } else {
1431
+ $managing_stock = get_post_meta( $variation_id, '_manage_stock', true );
1432
+ }
1433
+
1434
+ update_post_meta( $variation_id, '_manage_stock', '' === $managing_stock ? 'no' : $managing_stock );
1435
+
1436
+ if ( isset( $variation['in_stock'] ) ) {
1437
+ $stock_status = ( true === $variation['in_stock'] ) ? 'instock' : 'outofstock';
1438
+ } else {
1439
+ $stock_status = get_post_meta( $variation_id, '_stock_status', true );
1440
+ }
1441
+
1442
+ wc_update_product_stock_status( $variation_id, '' === $stock_status ? 'instock' : $stock_status );
1443
+
1444
+ if ( 'yes' === $managing_stock ) {
1445
+ $backorders = get_post_meta( $variation_id, '_backorders', true );
1446
+
1447
+ if ( isset( $variation['backorders'] ) ) {
1448
+ if ( 'notify' === $variation['backorders'] ) {
1449
+ $backorders = 'notify';
1450
+ } else {
1451
+ $backorders = ( true === $variation['backorders'] ) ? 'yes' : 'no';
1452
+ }
1453
+ }
1454
+
1455
+ update_post_meta( $variation_id, '_backorders', '' === $backorders ? 'no' : $backorders );
1456
+
1457
+ if ( isset( $variation['stock_quantity'] ) ) {
1458
+ wc_update_product_stock( $variation_id, wc_stock_amount( $variation['stock_quantity'] ) );
1459
+ } elseif ( isset( $data['inventory_delta'] ) ) {
1460
+ $stock_quantity = wc_stock_amount( get_post_meta( $variation_id, '_stock', true ) );
1461
+ $stock_quantity += wc_stock_amount( $data['inventory_delta'] );
1462
+
1463
+ wc_update_product_stock( $variation_id, wc_stock_amount( $stock_quantity ) );
1464
+ }
1465
+ } else {
1466
+ delete_post_meta( $variation_id, '_backorders' );
1467
+ delete_post_meta( $variation_id, '_stock' );
1468
+ }
1469
+
1470
+ $current_regular_price = get_post_meta( $variation_id, '_regular_price', true );
1471
+ $current_sale_price = get_post_meta( $variation_id, '_sale_price', true );
1472
+
1473
+ // Regular Price passed from Square ( They only have one price ).
1474
+ if ( isset( $variation['regular_price'] ) ) {
1475
+ // Check if current product is on sale.
1476
+ if ( $current_regular_price > $current_sale_price && $variation['regular_price'] < $current_regular_price ) {
1477
+ $regular_price = get_post_meta( $variation_id, '_regular_price', true );
1478
+ $sale_price = $variation['regular_price'];
1479
+ } else {
1480
+ $regular_price = ( '' === $variation['regular_price'] ) ? '' : $variation['regular_price'];
1481
+ $sale_price = '';
1482
+ }
1483
+ } else {
1484
+ $regular_price = get_post_meta( $variation_id, '_regular_price', true );
1485
+ $sale_price = get_post_meta( $variation_id, '_sale_price', true );
1486
+ }
1487
+
1488
+ if ( isset( $variation['sale_price_dates_from'] ) ) {
1489
+ $date_from = $variation['sale_price_dates_from'];
1490
+ } else {
1491
+ $date_from = get_post_meta( $variation_id, '_sale_price_dates_from', true );
1492
+ $date_from = ( '' === $date_from ) ? '' : date( 'Y-m-d', $date_from );
1493
+ }
1494
+
1495
+ if ( isset( $variation['sale_price_dates_to'] ) ) {
1496
+ $date_to = $variation['sale_price_dates_to'];
1497
+ } else {
1498
+ $date_to = get_post_meta( $variation_id, '_sale_price_dates_to', true );
1499
+ $date_to = ( '' === $date_to ) ? '' : date( 'Y-m-d', $date_to );
1500
+ }
1501
+
1502
+ $this->wc_save_product_price( $variation_id, $regular_price, $sale_price, $date_from, $date_to );
1503
+
1504
+ // Tax class
1505
+ if ( isset( $variation['tax_class'] ) ) {
1506
+ if ( 'parent' !== $variation['tax_class'] ) {
1507
+ update_post_meta( $variation_id, '_tax_class', wc_clean( $variation['tax_class'] ) );
1508
+ } else {
1509
+ delete_post_meta( $variation_id, '_tax_class' );
1510
+ }
1511
+ }
1512
+
1513
+ // Downloads
1514
+ if ( 'yes' == $is_downloadable ) {
1515
+ // Downloadable files
1516
+ if ( isset( $variation['downloads'] ) && is_array( $variation['downloads'] ) ) {
1517
+ $this->save_downloadable_files( $id, $variation['downloads'], $variation_id );
1518
+ }
1519
+
1520
+ // Download limit
1521
+ if ( isset( $variation['download_limit'] ) ) {
1522
+ $download_limit = absint( $variation['download_limit'] );
1523
+ update_post_meta( $variation_id, '_download_limit', ( ! $download_limit ) ? '' : $download_limit );
1524
+ }
1525
+
1526
+ // Download expiry
1527
+ if ( isset( $variation['download_expiry'] ) ) {
1528
+ $download_expiry = absint( $variation['download_expiry'] );
1529
+ update_post_meta( $variation_id, '_download_expiry', ( ! $download_expiry ) ? '' : $download_expiry );
1530
+ }
1531
+ } else {
1532
+ update_post_meta( $variation_id, '_download_limit', '' );
1533
+ update_post_meta( $variation_id, '_download_expiry', '' );
1534
+ update_post_meta( $variation_id, '_downloadable_files', '' );
1535
+ }
1536
+
1537
+ // Description.
1538
+ if ( isset( $variation['description'] ) ) {
1539
+ update_post_meta( $variation_id, '_variation_description', wp_kses_post( $variation['description'] ) );
1540
+ }
1541
+
1542
+ // Update taxonomies
1543
+ if ( isset( $variation['attributes'] ) ) {
1544
+ $updated_attribute_keys = array();
1545
+
1546
+ foreach ( $variation['attributes'] as $attribute_key => $attribute ) {
1547
+ if ( ! isset( $attribute['name'] ) ) {
1548
+ continue;
1549
+ }
1550
+
1551
+ $taxonomy = 0;
1552
+ $_attribute = array();
1553
+
1554
+ if ( isset( $attribute['slug'] ) ) {
1555
+ $taxonomy = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] );
1556
+ }
1557
+
1558
+ if ( ! $taxonomy ) {
1559
+ $taxonomy = sanitize_title( $attribute['name'] );
1560
+ }
1561
+
1562
+ if ( isset( $attributes[ $taxonomy ] ) ) {
1563
+ $_attribute = $attributes[ $taxonomy ];
1564
+ }
1565
+
1566
+ if ( isset( $_attribute['is_variation'] ) && $_attribute['is_variation'] ) {
1567
+ $_attribute_key = 'attribute_' . sanitize_title( $_attribute['name'] );
1568
+ $updated_attribute_keys[] = $_attribute_key;
1569
+
1570
+ if ( isset( $_attribute['is_taxonomy'] ) && $_attribute['is_taxonomy'] ) {
1571
+ // Don't use wc_clean as it destroys sanitized characters
1572
+ $_attribute_value = isset( $attribute['option'] ) ? sanitize_title( stripslashes( $attribute['option'] ) ) : '';
1573
+ } else {
1574
+ $_attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : '';
1575
+ }
1576
+
1577
+ update_post_meta( $variation_id, $_attribute_key, $_attribute_value );
1578
+ }
1579
+ }
1580
+
1581
+ // Remove old taxonomies attributes so data is kept up to date - first get attribute key names
1582
+ $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 ) );
1583
+
1584
+ foreach ( $delete_attribute_keys as $key ) {
1585
+ delete_post_meta( $variation_id, $key );
1586
+ }
1587
+ }
1588
+
1589
+ do_action( 'woocommerce_api_save_product_variation', $variation_id, $menu_order, $variation );
1590
+ }
1591
+
1592
+ // Update parent if variable so price sorting works and stays in sync with the cheapest child
1593
+ WC_Product_Variable::sync( $id );
1594
+
1595
+ // Update default attributes options setting
1596
+ if ( isset( $data['default_attribute'] ) ) {
1597
+ $data['default_attributes'] = $data['default_attribute'];
1598
+ }
1599
+
1600
+ if ( isset( $data['default_attributes'] ) && is_array( $data['default_attributes'] ) ) {
1601
+ $default_attributes = array();
1602
+
1603
+ foreach ( $data['default_attributes'] as $default_attr_key => $default_attr ) {
1604
+ if ( ! isset( $default_attr['name'] ) ) {
1605
+ continue;
1606
+ }
1607
+
1608
+ $taxonomy = sanitize_title( $default_attr['name'] );
1609
+
1610
+ if ( isset( $default_attr['slug'] ) ) {
1611
+ $taxonomy = $this->get_attribute_taxonomy_by_slug( $default_attr['slug'] );
1612
+ }
1613
+
1614
+ if ( isset( $attributes[ $taxonomy ] ) ) {
1615
+ $_attribute = $attributes[ $taxonomy ];
1616
+
1617
+ if ( $_attribute['is_variation'] ) {
1618
+ $value = '';
1619
+
1620
+ if ( isset( $default_attr['option'] ) ) {
1621
+ if ( $_attribute['is_taxonomy'] ) {
1622
+ // Don't use wc_clean as it destroys sanitized characters
1623
+ $value = sanitize_title( trim( stripslashes( $default_attr['option'] ) ) );
1624
+ } else {
1625
+ $value = wc_clean( trim( stripslashes( $default_attr['option'] ) ) );
1626
+ }
1627
+ }
1628
+
1629
+ if ( $value ) {
1630
+ $default_attributes[ $taxonomy ] = $value;
1631
+ }
1632
+ }
1633
+ }
1634
+ }
1635
+
1636
+ update_post_meta( $id, '_default_attributes', $default_attributes );
1637
+ }
1638
+
1639
+ return true;
1640
+ }
1641
+
1642
+ /**
1643
+ * Save product shipping data
1644
+ *
1645
+ * @since 2.2
1646
+ * @param int $id
1647
+ * @param array $data
1648
+ */
1649
+ private function save_product_shipping_data( $id, $data ) {
1650
+ if ( isset( $data['weight'] ) ) {
1651
+ update_post_meta( $id, '_weight', ( '' === $data['weight'] ) ? '' : wc_format_decimal( $data['weight'] ) );
1652
+ }
1653
+
1654
+ // Product dimensions
1655
+ if ( isset( $data['dimensions'] ) ) {
1656
+ // Height
1657
+ if ( isset( $data['dimensions']['height'] ) ) {
1658
+ update_post_meta( $id, '_height', ( '' === $data['dimensions']['height'] ) ? '' : wc_format_decimal( $data['dimensions']['height'] ) );
1659
+ }
1660
+
1661
+ // Width
1662
+ if ( isset( $data['dimensions']['width'] ) ) {
1663
+ update_post_meta( $id, '_width', ( '' === $data['dimensions']['width'] ) ? '' : wc_format_decimal( $data['dimensions']['width'] ) );
1664
+ }
1665
+
1666
+ // Length
1667
+ if ( isset( $data['dimensions']['length'] ) ) {
1668
+ update_post_meta( $id, '_length', ( '' === $data['dimensions']['length'] ) ? '' : wc_format_decimal( $data['dimensions']['length'] ) );
1669
+ }
1670
+ }
1671
+
1672
+ // Virtual
1673
+ if ( isset( $data['virtual'] ) ) {
1674
+ $virtual = ( true === $data['virtual'] ) ? 'yes' : 'no';
1675
+
1676
+ if ( 'yes' == $virtual ) {
1677
+ update_post_meta( $id, '_weight', '' );
1678
+ update_post_meta( $id, '_length', '' );
1679
+ update_post_meta( $id, '_width', '' );
1680
+ update_post_meta( $id, '_height', '' );
1681
+ }
1682
+ }
1683
+
1684
+ // Shipping class
1685
+ if ( isset( $data['shipping_class'] ) ) {
1686
+ wp_set_object_terms( $id, wc_clean( $data['shipping_class'] ), 'product_shipping_class' );
1687
+ }
1688
+ }
1689
+
1690
+ /**
1691
+ * Save downloadable files
1692
+ *
1693
+ * @since 2.2
1694
+ * @param int $product_id
1695
+ * @param array $downloads
1696
+ * @param int $variation_id
1697
+ */
1698
+ private function save_downloadable_files( $product_id, $downloads, $variation_id = 0 ) {
1699
+ $files = array();
1700
+
1701
+ // File paths will be stored in an array keyed off md5(file path)
1702
+ foreach ( $downloads as $key => $file ) {
1703
+ if ( isset( $file['url'] ) ) {
1704
+ $file['file'] = $file['url'];
1705
+ }
1706
+
1707
+ if ( ! isset( $file['file'] ) ) {
1708
+ continue;
1709
+ }
1710
+
1711
+ $file_name = isset( $file['name'] ) ? wc_clean( $file['name'] ) : '';
1712
+
1713
+ if ( 0 === strpos( $file['file'], 'http' ) ) {
1714
+ $file_url = esc_url_raw( $file['file'] );
1715
+ } else {
1716
+ $file_url = wc_clean( $file['file'] );
1717
+ }
1718
+
1719
+ $files[ md5( $file_url ) ] = array(
1720
+ 'name' => $file_name,
1721
+ 'file' => $file_url,
1722
+ );
1723
+ }
1724
+
1725
+ // Grant permission to any newly added files on any existing orders for this product prior to saving
1726
+ do_action( 'woocommerce_process_product_file_download_paths', $product_id, $variation_id, $files );
1727
+
1728
+ $id = ( 0 === $variation_id ) ? $product_id : $variation_id;
1729
+ update_post_meta( $id, '_downloadable_files', $files );
1730
+ }
1731
+
1732
+ /**
1733
+ * Get attribute taxonomy by slug.
1734
+ *
1735
+ * @since 2.2
1736
+ * @param string $slug
1737
+ * @return string|null
1738
+ */
1739
+ private function get_attribute_taxonomy_by_slug( $slug ) {
1740
+ $taxonomy = null;
1741
+ $attribute_taxonomies = wc_get_attribute_taxonomies();
1742
+
1743
+ foreach ( $attribute_taxonomies as $key => $tax ) {
1744
+ if ( $slug == $tax->attribute_name ) {
1745
+ $taxonomy = 'pa_' . $tax->attribute_name;
1746
+
1747
+ break;
1748
+ }
1749
+ }
1750
+
1751
+ return $taxonomy;
1752
+ }
1753
+
1754
+ /**
1755
+ * Get the images for a product or product variation
1756
+ *
1757
+ * @since 2.1
1758
+ * @param WC_Product|WC_Product_Variation $product
1759
+ * @return array
1760
+ */
1761
+ private function get_images( $product ) {
1762
+
1763
+ $images = $attachment_ids = array();
1764
+
1765
+ if ( $product->is_type( 'variation' ) ) {
1766
+
1767
+ if ( has_post_thumbnail( version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->get_variation_id() : $product->get_id() ) ) {
1768
+
1769
+ // Add variation image if set
1770
+ $attachment_ids[] = get_post_thumbnail_id( version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->get_variation_id() : $product->get_id() );
1771
+
1772
+ } elseif ( has_post_thumbnail( version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->id : $product->get_id() ) ) {
1773
+
1774
+ // Otherwise use the parent product featured image if set
1775
+ $attachment_ids[] = get_post_thumbnail_id( version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->id : $product->get_id() );
1776
+ }
1777
+ } else {
1778
+
1779
+ // Add featured image
1780
+ if ( has_post_thumbnail( version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->id : $product->get_id() ) ) {
1781
+ $attachment_ids[] = get_post_thumbnail_id( version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->id : $product->get_id() );
1782
+ }
1783
+
1784
+ // Add gallery images
1785
+ $attachment_ids = array_merge( $attachment_ids, ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->get_gallery_attachment_ids() : $product->get_gallery_image_ids() ) );
1786
+ }
1787
+
1788
+ // Build image data
1789
+ foreach ( $attachment_ids as $position => $attachment_id ) {
1790
+
1791
+ $attachment_post = get_post( $attachment_id );
1792
+
1793
+ if ( is_null( $attachment_post ) ) {
1794
+ continue;
1795
+ }
1796
+
1797
+ $attachment = wp_get_attachment_image_src( $attachment_id, 'full' );
1798
+
1799
+ if ( ! is_array( $attachment ) ) {
1800
+ continue;
1801
+ }
1802
+
1803
+ $images[] = array(
1804
+ 'id' => (int) $attachment_id,
1805
+ 'created_at' => $this->format_datetime( $attachment_post->post_date_gmt ),
1806
+ 'updated_at' => $this->format_datetime( $attachment_post->post_modified_gmt ),
1807
+ 'src' => current( $attachment ),
1808
+ 'title' => get_the_title( $attachment_id ),
1809
+ 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ),
1810
+ 'position' => (int) $position,
1811
+ );
1812
+ }
1813
+
1814
+ // Set a placeholder image if the product has no images set
1815
+ if ( empty( $images ) ) {
1816
+
1817
+ $images[] = array(
1818
+ 'id' => 0,
1819
+ 'created_at' => $this->format_datetime( time() ), // Default to now
1820
+ 'updated_at' => $this->format_datetime( time() ),
1821
+ 'src' => wc_placeholder_img_src(),
1822
+ 'title' => __( 'Placeholder', 'woocommerce' ),
1823
+ 'alt' => __( 'Placeholder', 'woocommerce' ),
1824
+ 'position' => 0,
1825
+ );
1826
+ }
1827
+
1828
+ return $images;
1829
+ }
1830
+
1831
+ /**
1832
+ * Save product images.
1833
+ *
1834
+ * @since 2.2
1835
+ * @param array $images
1836
+ * @param int $id
1837
+ * @throws WC_Square_API_Exception
1838
+ */
1839
+ protected function save_product_images( $id, $images ) {
1840
+ if ( is_array( $images ) ) {
1841
+ $gallery = array();
1842
+
1843
+ $product_image = get_the_post_thumbnail_url( $id, 'full' );
1844
+
1845
+ foreach ( $images as $image ) {
1846
+ /**
1847
+ * Because Square saves all images as original.jpeg when passed back,
1848
+ * we can't really check against filename. So we have no choice here but
1849
+ * to not sync the image if one already exists to prevent WP from creating
1850
+ * duplicate images all over the place. See https://github.com/woocommerce/woocommerce-square/issues/131
1851
+ */
1852
+ if ( $product_image ) {
1853
+ continue;
1854
+ }
1855
+
1856
+ if ( isset( $image['position'] ) && 0 == $image['position'] ) {
1857
+ $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0;
1858
+
1859
+ if ( 0 === $attachment_id && isset( $image['src'] ) ) {
1860
+ $upload = $this->upload_product_image( esc_url_raw( $image['src'] ) );
1861
+
1862
+ if ( is_wp_error( $upload ) ) {
1863
+ throw new WC_Square_API_Exception( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), 400 );
1864
+ }
1865
+
1866
+ $attachment_id = $this->set_product_image_as_attachment( $upload, $id );
1867
+ }
1868
+
1869
+ set_post_thumbnail( $id, $attachment_id );
1870
+ } else {
1871
+ $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0;
1872
+
1873
+ if ( 0 === $attachment_id && isset( $image['src'] ) ) {
1874
+ $upload = $this->upload_product_image( esc_url_raw( $image['src'] ) );
1875
+
1876
+ if ( is_wp_error( $upload ) ) {
1877
+ throw new WC_Square_API_Exception( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), 400 );
1878
+ }
1879
+
1880
+ $attachment_id = $this->set_product_image_as_attachment( $upload, $id );
1881
+ }
1882
+
1883
+ $gallery[] = $attachment_id;
1884
+ }
1885
+
1886
+ // Set the image alt if present.
1887
+ if ( ! empty( $image['alt'] ) && $attachment_id ) {
1888
+ update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) );
1889
+ }
1890
+
1891
+ // Set the image title if present.
1892
+ if ( ! empty( $image['title'] ) && $attachment_id ) {
1893
+ wp_update_post( array( 'ID' => $attachment_id, 'post_title' => $image['title'] ) );
1894
+ }
1895
+ }
1896
+
1897
+ if ( ! empty( $gallery ) ) {
1898
+ update_post_meta( $id, '_product_image_gallery', implode( ',', $gallery ) );
1899
+ }
1900
+ } else {
1901
+ delete_post_thumbnail( $id );
1902
+ update_post_meta( $id, '_product_image_gallery', '' );
1903
+ }
1904
+ }
1905
+
1906
+ /**
1907
+ * Upload image from URL
1908
+ *
1909
+ * @since 2.2
1910
+ * @param string $image_url
1911
+ * @return int|WP_Error attachment id
1912
+ */
1913
+ public function upload_product_image( $image_url ) {
1914
+ return $this->upload_image_from_url( $image_url, 'product_image' );
1915
+ }
1916
+
1917
+ /**
1918
+ * Upload product category image from URL.
1919
+ *
1920
+ * @since 2.5.0
1921
+ * @param string $image_url
1922
+ * @return int|WP_Error attachment id
1923
+ */
1924
+ public function upload_product_category_image( $image_url ) {
1925
+ return $this->upload_image_from_url( $image_url, 'product_category_image' );
1926
+ }
1927
+
1928
+ /**
1929
+ * Upload image from URL.
1930
+ *
1931
+ * @throws WC_Square_API_Exception
1932
+ *
1933
+ * @since 2.5.0
1934
+ * @param string $image_url
1935
+ * @param string $upload_for
1936
+ * @return int|WP_Error Attachment id
1937
+ */
1938
+ protected function upload_image_from_url( $image_url, $upload_for = 'product_image' ) {
1939
+ $file_name = basename( current( explode( '?', $image_url ) ) );
1940
+ $parsed_url = @parse_url( $image_url );
1941
+
1942
+ // Check parsed URL.
1943
+ if ( ! $parsed_url || ! is_array( $parsed_url ) ) {
1944
+ throw new WC_Square_API_Exception( 'woocommerce_api_invalid_' . $upload_for, sprintf( __( 'Invalid URL %s', 'woocommerce' ), $image_url ), 400 );
1945
+ }
1946
+
1947
+ // Ensure url is valid.
1948
+ $image_url = str_replace( ' ', '%20', $image_url );
1949
+
1950
+ // Get the file.
1951
+ $response = wp_safe_remote_get( $image_url, array(
1952
+ 'timeout' => 10,
1953
+ ) );
1954
+
1955
+ if ( is_wp_error( $response ) ) {
1956
+ throw new WC_Square_API_Exception( 'woocommerce_api_invalid_remote_' . $upload_for, sprintf( __( 'Error getting remote image %s.', 'woocommerce' ), $image_url ) . ' ' . sprintf( __( 'Error: %s.', 'woocommerce' ), $response->get_error_message() ), 400 );
1957
+ } elseif ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
1958
+ throw new WC_Square_API_Exception( 'woocommerce_api_invalid_remote_' . $upload_for, sprintf( __( 'Error getting remote image %s.', 'woocommerce' ), $image_url ), 400 );
1959
+ }
1960
+
1961
+ // Ensure we have a file name and type.
1962
+ $wp_filetype = wp_check_filetype( $file_name, wc_rest_allowed_image_mime_types() );
1963
+
1964
+ if ( ! $wp_filetype['type'] ) {
1965
+ $headers = wp_remote_retrieve_headers( $response );
1966
+ if ( isset( $headers['content-disposition'] ) && strstr( $headers['content-disposition'], 'filename=' ) ) {
1967
+ $disposition = end( explode( 'filename=', $headers['content-disposition'] ) );
1968
+ $disposition = sanitize_file_name( $disposition );
1969
+ $file_name = $disposition;
1970
+ } elseif ( isset( $headers['content-type'] ) && strstr( $headers['content-type'], 'image/' ) ) {
1971
+ $file_name = 'image.' . str_replace( 'image/', '', $headers['content-type'] );
1972
+ }
1973
+ unset( $headers );
1974
+
1975
+ // Recheck filetype
1976
+ $wp_filetype = wp_check_filetype( $file_name, wc_rest_allowed_image_mime_types() );
1977
+
1978
+ if ( ! $wp_filetype['type'] ) {
1979
+ throw new WC_Square_API_Exception( 'woocommerce_api_invalid_' . $upload_for, __( 'Invalid image type.', 'woocommerce' ), 400 );
1980
+ }
1981
+ }
1982
+
1983
+ // Upload the file.
1984
+ $upload = wp_upload_bits( $file_name, '', wp_remote_retrieve_body( $response ) );
1985
+
1986
+ if ( $upload['error'] ) {
1987
+ throw new WC_Square_API_Exception( 'woocommerce_api_' . $upload_for . '_upload_error', $upload['error'], 400 );
1988
+ }
1989
+
1990
+ // Get filesize.
1991
+ $filesize = filesize( $upload['file'] );
1992
+
1993
+ if ( 0 == $filesize ) {
1994
+ @unlink( $upload['file'] );
1995
+ unset( $upload );
1996
+ throw new WC_Square_API_Exception( 'woocommerce_api_' . $upload_for . '_upload_file_error', __( 'Zero size file downloaded', 'woocommerce' ), 400 );
1997
+ }
1998
+
1999
+ unset( $response );
2000
+
2001
+ do_action( 'woocommerce_api_uploaded_image_from_url', $upload, $image_url, $upload_for );
2002
+
2003
+ return $upload;
2004
+ }
2005
+
2006
+ /**
2007
+ * Sets product image as attachment and returns the attachment ID.
2008
+ *
2009
+ * @since 2.2
2010
+ * @param array $upload
2011
+ * @param int $id
2012
+ * @return int
2013
+ */
2014
+ protected function set_product_image_as_attachment( $upload, $id ) {
2015
+ return $this->set_uploaded_image_as_attachment( $upload, $id );
2016
+ }
2017
+
2018
+ /**
2019
+ * Sets uploaded category image as attachment and returns the attachment ID.
2020
+ *
2021
+ * @since 2.5.0
2022
+ * @param integer $upload Upload information from wp_upload_bits
2023
+ * @return int Attachment ID
2024
+ */
2025
+ protected function set_product_category_image_as_attachment( $upload ) {
2026
+ return $this->set_uploaded_image_as_attachment( $upload );
2027
+ }
2028
+
2029
+ /**
2030
+ * Set uploaded image as attachment.
2031
+ *
2032
+ * @since 2.5.0
2033
+ * @param array $upload Upload information from wp_upload_bits
2034
+ * @param int $id Post ID. Default to 0.
2035
+ * @return int Attachment ID
2036
+ */
2037
+ protected function set_uploaded_image_as_attachment( $upload, $id = 0 ) {
2038
+ $info = wp_check_filetype( $upload['file'] );
2039
+ $title = '';
2040
+ $content = '';
2041
+
2042
+ if ( $image_meta = @wp_read_image_metadata( $upload['file'] ) ) {
2043
+ if ( trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) {
2044
+ $title = wc_clean( $image_meta['title'] );
2045
+ }
2046
+ if ( trim( $image_meta['caption'] ) ) {
2047
+ $content = wc_clean( $image_meta['caption'] );
2048
+ }
2049
+ }
2050
+
2051
+ $attachment = array(
2052
+ 'post_mime_type' => $info['type'],
2053
+ 'guid' => $upload['url'],
2054
+ 'post_parent' => $id,
2055
+ 'post_title' => $title,
2056
+ 'post_content' => $content,
2057
+ );
2058
+
2059
+ $attachment_id = wp_insert_attachment( $attachment, $upload['file'], $id );
2060
+ if ( ! is_wp_error( $attachment_id ) ) {
2061
+ wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $upload['file'] ) );
2062
+ }
2063
+
2064
+ return $attachment_id;
2065
+ }
2066
+
2067
+ /**
2068
+ * Get attribute options.
2069
+ *
2070
+ * @param int $product_id
2071
+ * @param array $attribute
2072
+ * @return array
2073
+ */
2074
+ protected function get_attribute_options( $product_id, $attribute ) {
2075
+ if ( isset( $attribute['is_taxonomy'] ) && $attribute['is_taxonomy'] ) {
2076
+ return wc_get_product_terms( $product_id, $attribute['name'], array( 'fields' => 'names' ) );
2077
+ } elseif ( isset( $attribute['value'] ) ) {
2078
+ return array_map( 'trim', explode( '|', $attribute['value'] ) );
2079
+ }
2080
+
2081
+ return array();
2082
+ }
2083
+
2084
+ /**
2085
+ * Get the attributes for a product or product variation
2086
+ *
2087
+ * @since 2.1
2088
+ * @param WC_Product|WC_Product_Variation $product
2089
+ * @return array
2090
+ */
2091
+ private function get_attributes( $product ) {
2092
+
2093
+ $attributes = array();
2094
+
2095
+ if ( $product->is_type( 'variation' ) ) {
2096
+
2097
+ // variation attributes
2098
+ foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) {
2099
+
2100
+ // taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_`
2101
+ $attributes[] = array(
2102
+ 'name' => wc_attribute_label( str_replace( 'attribute_', '', $attribute_name ), $product ),
2103
+ 'slug' => str_replace( 'attribute_', '', str_replace( 'pa_', '', $attribute_name ) ),
2104
+ 'option' => $attribute,
2105
+ );
2106
+ }
2107
+ } else {
2108
+ foreach ( $product->get_attributes() as $attribute ) {
2109
+ $attributes[] = array(
2110
+ 'name' => wc_attribute_label( $attribute['name'], $product ),
2111
+ 'slug' => str_replace( 'pa_', '', $attribute['name'] ),
2112
+ 'position' => (int) $attribute['position'],
2113
+ 'visible' => (bool) $attribute['is_visible'],
2114
+ 'variation' => (bool) $attribute['is_variation'],
2115
+ 'options' => $this->get_attribute_options( version_compare( WC_VERSION, '3.0.0', '<' ) ? $product->id : $product->get_id(), $attribute ),
2116
+ );
2117
+ }
2118
+ }
2119
+
2120
+ return $attributes;
2121
+ }
2122
+
2123
+ /**
2124
+ * Get the downloads for a product or product variation
2125
+ *
2126
+ * @since 2.1
2127
+ * @param WC_Product|WC_Product_Variation $product
2128
+ * @return array
2129
+ */
2130
+ private function get_downloads( $product ) {
2131
+
2132
+ $downloads = array();
2133
+
2134
+ if ( $product->is_downloadable() ) {
2135
+
2136
+ $files = version_compare( WC_VERSION, '3.0', '<' ) ? $product->get_files() : $product->get_downloads();
2137
+
2138
+ foreach ( $files as $file_id => $file ) {
2139
+
2140
+ $downloads[] = array(
2141
+ 'id' => $file_id, // do not cast as int as this is a hash
2142
+ 'name' => $file['name'],
2143
+ 'file' => $file['file'],
2144
+ );
2145
+ }
2146
+ }
2147
+
2148
+ return $downloads;
2149
+ }
2150
+
2151
+ /**
2152
+ * Get a listing of product attributes
2153
+ *
2154
+ * @since 2.5.0
2155
+ * @param string|null $fields fields to limit response to
2156
+ * @return array
2157
+ */
2158
+ public function get_product_attributes( $fields = null ) {
2159
+ try {
2160
+ // Permissions check.
2161
+ if ( ! current_user_can( 'manage_product_terms' ) ) {
2162
+ throw new WC_Square_API_Exception( 'woocommerce_api_user_cannot_read_product_attributes', __( 'You do not have permission to read product attributes', 'woocommerce' ), 401 );
2163
+ }
2164
+
2165
+ $product_attributes = array();
2166
+ $attribute_taxonomies = wc_get_attribute_taxonomies();
2167
+
2168
+ foreach ( $attribute_taxonomies as $attribute ) {
2169
+ $product_attributes[] = array(
2170
+ 'id' => intval( $attribute->attribute_id ),
2171
+ 'name' => $attribute->attribute_label,
2172
+ 'slug' => wc_attribute_taxonomy_name( $attribute->attribute_name ),
2173
+ 'type' => $attribute->attribute_type,
2174
+ 'order_by' => $attribute->attribute_orderby,
2175
+ 'has_archives' => (bool) $attribute->attribute_public,
2176
+ );
2177
+ }
2178
+
2179
+ return array( 'product_attributes' => apply_filters( 'woocommerce_api_product_attributes_response', $product_attributes, $attribute_taxonomies, $fields, $this ) );
2180
+ } catch ( WC_Square_API_Exception $e ) {
2181
+ return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
2182
+ }
2183
+ }
2184
+
2185
+ /**
2186
+ * Get the product attribute for the given ID
2187
+ *
2188
+ * @since 2.5.0
2189
+ * @param string $id product attribute term ID
2190
+ * @param string|null $fields fields to limit response to
2191
+ * @return array
2192
+ */
2193
+ public function get_product_attribute( $id, $fields = null ) {
2194
+ global $wpdb;
2195
+
2196
+ try {
2197
+ $id = absint( $id );
2198
+
2199
+ // Validate ID
2200
+ if ( empty( $id ) ) {
2201
+ throw new WC_Square_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'Invalid product attribute ID', 'woocommerce' ), 400 );
2202
+ }
2203
+
2204
+ // Permissions check
2205
+ if ( ! current_user_can( 'manage_product_terms' ) ) {
2206
+ throw new WC_Square_API_Exception( 'woocommerce_api_user_cannot_read_product_attributes', __( 'You do not have permission to read product attributes', 'woocommerce' ), 401 );
2207
+ }
2208
+
2209
+ $attribute = $wpdb->get_row( $wpdb->prepare( "
2210
+ SELECT *
2211
+ FROM {$wpdb->prefix}woocommerce_attribute_taxonomies
2212
+ WHERE attribute_id = %d
2213
+ ", $id ) );
2214
+
2215
+ if ( is_wp_error( $attribute ) || is_null( $attribute ) ) {
2216
+ throw new WC_Square_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 );
2217
+ }
2218
+
2219
+ $product_attribute = array(
2220
+ 'id' => intval( $attribute->attribute_id ),
2221
+ 'name' => $attribute->attribute_label,
2222
+ 'slug' => wc_attribute_taxonomy_name( $attribute->attribute_name ),
2223
+ 'type' => $attribute->attribute_type,
2224
+ 'order_by' => $attribute->attribute_orderby,
2225
+ 'has_archives' => (bool) $attribute->attribute_public,
2226
+ );
2227
+
2228
+ return array( 'product_attribute' => apply_filters( 'woocommerce_api_product_attribute_response', $product_attribute, $id, $fields, $attribute, $this ) );
2229
+ } catch ( WC_Square_API_Exception $e ) {
2230
+ return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
2231
+ }
2232
+ }
2233
+
2234
+ /**
2235
+ * Validate attribute data.
2236
+ *
2237
+ * @since 2.5.0
2238
+ * @param string $name
2239
+ * @param string $slug
2240
+ * @param string $type
2241
+ * @param string $order_by
2242
+ * @param bool $new_data
2243
+ * @return bool
2244
+ * @throws WC_Square_API_Exception
2245
+ */
2246
+ protected function validate_attribute_data( $name, $slug, $type, $order_by, $new_data = true ) {
2247
+ if ( empty( $name ) ) {
2248
+ throw new WC_Square_API_Exception( 'woocommerce_api_missing_product_attribute_name', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'name' ), 400 );
2249
+ }
2250
+
2251
+ if ( strlen( $slug ) >= 28 ) {
2252
+ 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' ), $slug ), 400 );
2253
+ } elseif ( wc_check_if_attribute_name_is_reserved( $slug ) ) {
2254
+ 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' ), $slug ), 400 );
2255
+ } elseif ( $new_data && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) {
2256
+ 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' ), $slug ), 400 );
2257
+ }
2258
+
2259
+ // Validate the attribute type
2260
+ if ( ! in_array( wc_clean( $type ), array_keys( wc_get_attribute_types() ) ) ) {
2261
+ 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' ), implode( ', ', array_keys( wc_get_attribute_types() ) ) ), 400 );
2262
+ }
2263
+
2264
+ // Validate the attribute order by
2265
+ if ( ! in_array( wc_clean( $order_by ), array( 'menu_order', 'name', 'name_num', 'id' ) ) ) {
2266
+ 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' ), implode( ', ', array( 'menu_order', 'name', 'name_num', 'id' ) ) ), 400 );
2267
+ }
2268
+
2269
+ return true;
2270
+ }
2271
+
2272
+ /**
2273
+ * Create a new product attribute.
2274
+ *
2275
+ * @since 2.5.0
2276
+ * @param array $data Posted data.
2277
+ * @return array
2278
+ */
2279
+ public function create_product_attribute( $data ) {
2280
+ global $wpdb;
2281
+
2282
+ try {
2283
+ if ( ! isset( $data['product_attribute'] ) ) {
2284
+ throw new WC_Square_API_Exception( 'woocommerce_api_missing_product_attribute_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_attribute' ), 400 );
2285
+ }
2286
+
2287
+ $data = $data['product_attribute'];
2288
+
2289
+ // Check permissions.
2290
+ if ( ! current_user_can( 'manage_product_terms' ) ) {
2291
+ throw new WC_Square_API_Exception( 'woocommerce_api_user_cannot_create_product_attribute', __( 'You do not have permission to create product attributes', 'woocommerce' ), 401 );
2292
+ }
2293
+
2294
+ $data = apply_filters( 'woocommerce_api_create_product_attribute_data', $data, $this );
2295
+
2296
+ if ( ! isset( $data['name'] ) ) {
2297
+ $data['name'] = '';
2298
+ }
2299
+
2300
+ // Set the attribute slug.
2301
+ if ( ! isset( $data['slug'] ) ) {
2302
+ $data['slug'] = wc_sanitize_taxonomy_name( stripslashes( $data['name'] ) );
2303
+ } else {
2304
+ $data['slug'] = preg_replace( '/^pa\_/', '', wc_sanitize_taxonomy_name( stripslashes( $data['slug'] ) ) );
2305
+ }
2306
+
2307
+ // Set attribute type when not sent.
2308
+ if ( ! isset( $data['type'] ) ) {
2309
+ $data['type'] = 'select';
2310
+ }
2311
+
2312
+ // Set order by when not sent.
2313
+ if ( ! isset( $data['order_by'] ) ) {
2314
+ $data['order_by'] = 'menu_order';
2315
+ }
2316
+
2317
+ // Validate the attribute data.
2318
+ $this->validate_attribute_data( $data['name'], $data['slug'], $data['type'], $data['order_by'], true );
2319
+
2320
+ $insert = $wpdb->insert(
2321
+ $wpdb->prefix . 'woocommerce_attribute_taxonomies',
2322
+ array(
2323
+ 'attribute_label' => $data['name'],
2324
+ 'attribute_name' => $data['slug'],
2325
+ 'attribute_type' => $data['type'],
2326
+ 'attribute_orderby' => $data['order_by'],
2327
+ 'attribute_public' => isset( $data['has_archives'] ) && true === $data['has_archives'] ? 1 : 0,
2328
+ ),
2329
+ array( '%s', '%s', '%s', '%s', '%d' )
2330
+ );
2331
+
2332
+ // Checks for an error in the product creation.
2333
+ if ( is_wp_error( $insert ) ) {
2334
+ throw new WC_Square_API_Exception( 'woocommerce_api_cannot_create_product_attribute', $insert->get_error_message(), 400 );
2335
+ }
2336
+
2337
+ $id = $wpdb->insert_id;
2338
+
2339
+ do_action( 'woocommerce_api_create_product_attribute', $id, $data );
2340
+
2341
+ // Clear transients.
2342
+ flush_rewrite_rules();
2343
+ delete_transient( 'wc_attribute_taxonomies' );
2344
+
2345
+ return $this->get_product_attribute( $id );
2346
+ } catch ( WC_Square_API_Exception $e ) {
2347
+ return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
2348
+ }
2349
+ }
2350
+
2351
+ /**
2352
+ * Clear product
2353
+ */
2354
+ protected function clear_product( $product_id ) {
2355
+ if ( ! is_numeric( $product_id ) || 0 >= $product_id ) {
2356
+ return;
2357
+ }
2358
+
2359
+ // Delete product attachments
2360
+ $attachments = get_children( array(
2361
+ 'post_parent' => $product_id,
2362
+ 'post_status' => 'any',
2363
+ 'post_type' => 'attachment',
2364
+ ) );
2365
+
2366
+ foreach ( (array) $attachments as $attachment ) {
2367
+ wp_delete_attachment( $attachment->ID, true );
2368
+ }
2369
+
2370
+ // Delete product
2371
+ wp_delete_post( $product_id, true );
2372
+ }
2373
+
2374
+ /**
2375
+ * Format a unix timestamp or MySQL datetime into an RFC3339 datetime
2376
+ *
2377
+ * @since 2.1
2378
+ * @param int|string $timestamp unix timestamp or MySQL datetime
2379
+ * @param bool $convert_to_utc
2380
+ * @return string RFC3339 datetime
2381
+ */
2382
+ public function format_datetime( $timestamp, $convert_to_utc = false ) {
2383
+
2384
+ if ( $convert_to_utc ) {
2385
+ $timezone = new DateTimeZone( wc_timezone_string() );
2386
+ } else {
2387
+ $timezone = new DateTimeZone( 'UTC' );
2388
+ }
2389
+
2390
+ try {
2391
+
2392
+ if ( is_numeric( $timestamp ) ) {
2393
+ $date = new DateTime( "@{$timestamp}" );
2394
+ } else {
2395
+ $date = new DateTime( $timestamp, $timezone );
2396
+ }
2397
+
2398
+ // convert to UTC by adjusting the time based on the offset of the site's timezone
2399
+ if ( $convert_to_utc ) {
2400
+ $date->modify( -1 * $date->getOffset() . ' seconds' );
2401
+ }
2402
+ } catch ( Exception $e ) {
2403
+
2404
+ $date = new DateTime( '@0' );
2405
+ }
2406
+
2407
+ return $date->format( 'Y-m-d\TH:i:s\Z' );
2408
+ }
2409
+
2410
+ /**
2411
+ * Save product price.
2412
+ *
2413
+ * This is a private function (internal use ONLY) used until a data manipulation api is built.
2414
+ *
2415
+ * @deprecated 2.7.0
2416
+ * @param int $product_id
2417
+ * @param float $regular_price
2418
+ * @param float $sale_price
2419
+ * @param string $date_from
2420
+ * @param string $date_to
2421
+ */
2422
+ public function wc_save_product_price( $product_id, $regular_price, $sale_price = '', $date_from = '', $date_to = '' ) {
2423
+ $product_id = absint( $product_id );
2424
+ $regular_price = wc_format_decimal( $regular_price );
2425
+ $sale_price = '' === $sale_price ? '' : wc_format_decimal( $sale_price );
2426
+ $date_from = wc_clean( $date_from );
2427
+ $date_to = wc_clean( $date_to );
2428
+
2429
+ update_post_meta( $product_id, '_regular_price', $regular_price );
2430
+ update_post_meta( $product_id, '_sale_price', $sale_price );
2431
+
2432
+ // Save Dates
2433
+ update_post_meta( $product_id, '_sale_price_dates_from', $date_from ? strtotime( $date_from ) : '' );
2434
+ update_post_meta( $product_id, '_sale_price_dates_to', $date_to ? strtotime( $date_to ) : '' );
2435
+
2436
+ if ( $date_to && ! $date_from ) {
2437
+ $date_from = strtotime( 'NOW', current_time( 'timestamp' ) );
2438
+ update_post_meta( $product_id, '_sale_price_dates_from', $date_from );
2439
+ }
2440
+
2441
+ // Update price if on sale
2442
+ if ( '' !== $sale_price && '' === $date_to && '' === $date_from ) {
2443
+ update_post_meta( $product_id, '_price', $sale_price );
2444
+ } else {
2445
+ update_post_meta( $product_id, '_price', $regular_price );
2446
+ }
2447
+
2448
+ if ( '' !== $sale_price && $date_from && strtotime( $date_from ) < strtotime( 'NOW', current_time( 'timestamp' ) ) ) {
2449
+ update_post_meta( $product_id, '_price', $sale_price );
2450
+ }
2451
+
2452
+ if ( $date_to && strtotime( $date_to ) < strtotime( 'NOW', current_time( 'timestamp' ) ) ) {
2453
+ update_post_meta( $product_id, '_price', $regular_price );
2454
+ update_post_meta( $product_id, '_sale_price_dates_from', '' );
2455
+ update_post_meta( $product_id, '_sale_price_dates_to', '' );
2456
+ }
2457
+ }
2458
+ }
includes/payment/class-wc-square-gateway.php ADDED
@@ -0,0 +1,542 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ $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" />';
66
+
67
+ return apply_filters( 'woocommerce_gateway_icon', $icon, $this->id );
68
+ }
69
+
70
+ /**
71
+ * Check if required fields are set
72
+ */
73
+ public function admin_notices() {
74
+ if ( 'yes' !== $this->enabled ) {
75
+ return;
76
+ }
77
+
78
+ // Show message if enabled and FORCE SSL is disabled and WordpressHTTPS plugin is not detected
79
+ if ( ! WC_SQUARE_ENABLE_STAGING && get_option( 'woocommerce_force_ssl_checkout' ) === 'no' && ! class_exists( 'WordPressHTTPS' ) ) {
80
+ echo '<div class="error"><p>' . sprintf( __( 'Square is enabled, but the <a href="%s">force SSL option</a> is disabled; your checkout is not secured! Please enable SSL and ensure your server has a valid SSL certificate.', 'woocommerce-square' ), admin_url( 'admin.php?page=wc-settings&tab=checkout' ) ) . '</p></div>';
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Check if this gateway is enabled
86
+ */
87
+ public function is_available() {
88
+ $is_available = true;
89
+
90
+ if ( 'yes' === $this->enabled ) {
91
+ if ( ! WC_SQUARE_ENABLE_STAGING && ! wc_checkout_is_https() ) {
92
+ $is_available = false;
93
+ }
94
+
95
+ if ( ! WC_SQUARE_ENABLE_STAGING && empty( $this->token ) ) {
96
+ $is_available = false;
97
+ }
98
+
99
+ // Square only supports Australia, Canada, Japan, UK, and US for now.
100
+ 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() ) ) {
101
+ $is_available = false;
102
+ }
103
+ } else {
104
+ $is_available = false;
105
+ }
106
+
107
+ return apply_filters( 'woocommerce_square_payment_gateway_is_available', $is_available );
108
+ }
109
+
110
+ /**
111
+ * Initialize Gateway Settings Form Fields
112
+ */
113
+ public function init_form_fields() {
114
+ $this->form_fields = apply_filters( 'woocommerce_square_gateway_settings', array(
115
+ 'enabled' => array(
116
+ 'title' => __( 'Enable/Disable', 'woocommerce-square' ),
117
+ 'label' => __( 'Enable Square', 'woocommerce-square' ),
118
+ 'type' => 'checkbox',
119
+ 'description' => '',
120
+ 'default' => 'no',
121
+ ),
122
+ 'title' => array(
123
+ 'title' => __( 'Title', 'woocommerce-square' ),
124
+ 'type' => 'text',
125
+ 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce-square' ),
126
+ 'default' => __( 'Credit card (Square)', 'woocommerce-square' ),
127
+ ),
128
+ 'description' => array(
129
+ 'title' => __( 'Description', 'woocommerce-square' ),
130
+ 'type' => 'textarea',
131
+ 'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce-square' ),
132
+ 'default' => __( 'Pay with your credit card via Square.', 'woocommerce-square' ),
133
+ ),
134
+ 'capture' => array(
135
+ 'title' => __( 'Delay Capture', 'woocommerce-square' ),
136
+ 'label' => __( 'Enable Delay Capture', 'woocommerce-square' ),
137
+ 'type' => 'checkbox',
138
+ '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' ),
139
+ 'default' => 'no',
140
+ ),
141
+ 'create_customer' => array(
142
+ 'title' => __( 'Create Customer', 'woocommerce-square' ),
143
+ 'label' => __( 'Enable Create Customer', 'woocommerce-square' ),
144
+ 'type' => 'checkbox',
145
+ 'description' => __( 'When enabled, processing a payment will create a customer profile on Square.', 'woocommerce-square' ),
146
+ 'default' => 'no',
147
+ ),
148
+ 'logging' => array(
149
+ 'title' => __( 'Logging', 'woocommerce-square' ),
150
+ 'label' => __( 'Log debug messages', 'woocommerce-square' ),
151
+ 'type' => 'checkbox',
152
+ 'description' => __( 'Save debug messages to the WooCommerce System Status log.', 'woocommerce-square' ),
153
+ 'default' => 'no',
154
+ ),
155
+ ) );
156
+ }
157
+
158
+ /**
159
+ * Payment form on checkout page
160
+ */
161
+ public function payment_fields() {
162
+ ?>
163
+ <fieldset>
164
+ <?php
165
+ $allowed = array(
166
+ 'a' => array(
167
+ 'href' => array(),
168
+ 'title' => array(),
169
+ ),
170
+ 'br' => array(),
171
+ 'em' => array(),
172
+ 'strong' => array(),
173
+ 'span' => array(
174
+ 'class' => array(),
175
+ ),
176
+ );
177
+ if ( $this->description ) {
178
+ echo apply_filters( 'woocommerce_square_description', wpautop( wp_kses( $this->description, $allowed ) ) );
179
+ }
180
+ ?>
181
+ <p class="form-row form-row-wide">
182
+ <label for="sq-card-number"><?php esc_html_e( 'Card Number', 'woocommerce-square' ); ?> <span class="required">*</span></label>
183
+ <input id="sq-card-number" type="text" maxlength="20" autocomplete="off" placeholder="•••• •••• •••• ••••" name="<?php echo esc_attr( $this->id ); ?>-card-number" />
184
+ </p>
185
+
186
+ <p class="form-row form-row-first">
187
+ <label for="sq-expiration-date"><?php esc_html_e( 'Expiry (MM/YY)', 'woocommerce-square' ); ?> <span class="required">*</span></label>
188
+ <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" />
189
+ </p>
190
+
191
+ <p class="form-row form-row-last">
192
+ <label for="sq-cvv"><?php esc_html_e( 'Card Code', 'woocommerce-square' ); ?> <span class="required">*</span></label>
193
+ <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" />
194
+ </p>
195
+
196
+ <p class="form-row form-row-wide">
197
+ <label for="sq-postal-code"><?php esc_html_e( 'Card Postal Code', 'woocommerce-square' ); ?> <span class="required">*</span></label>
198
+ <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" />
199
+ </p>
200
+ </fieldset>
201
+ <?php
202
+ }
203
+
204
+ /**
205
+ * Get payment form input styles.
206
+ * This function is pass to the JS script in order to style the
207
+ * input fields within the iFrame.
208
+ *
209
+ * Possible styles are: mediaMinWidth, mediaMaxWidth, backgroundColor, boxShadow,
210
+ * color, fontFamily, fontSize, fontWeight, lineHeight and padding.
211
+ *
212
+ * @since 1.0.4
213
+ * @version 1.0.4
214
+ * @access public
215
+ * @return json $styles
216
+ */
217
+ public function get_input_styles() {
218
+ $styles = array(
219
+ array(
220
+ 'fontSize' => '1.2em',
221
+ 'padding' => '.618em',
222
+ 'fontWeight' => 400,
223
+ 'backgroundColor' => 'transparent',
224
+ 'lineHeight' => 1.7,
225
+ ),
226
+ array(
227
+ 'mediaMaxWidth' => '1200px',
228
+ 'fontSize' => '1em',
229
+ ),
230
+ );
231
+
232
+ return apply_filters( 'woocommerce_square_payment_input_styles', wp_json_encode( $styles ) );
233
+ }
234
+
235
+ /**
236
+ * payment_scripts function.
237
+ *
238
+ *
239
+ * @access public
240
+ */
241
+ public function payment_scripts() {
242
+ if ( ! is_checkout() ) {
243
+ return;
244
+ }
245
+
246
+ $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
247
+
248
+ wp_register_script( 'square', 'https://js.squareup.com/v2/paymentform', '', '0.0.2', true );
249
+ wp_register_script( 'woocommerce-square', WC_SQUARE_PLUGIN_URL . '/assets/js/wc-square-payments' . $suffix . '.js', array( 'jquery', 'square' ), WC_SQUARE_VERSION, true );
250
+
251
+ wp_localize_script( 'woocommerce-square', 'square_params', array(
252
+ 'application_id' => SQUARE_APPLICATION_ID,
253
+ 'environment' => WC_SQUARE_ENABLE_STAGING ? 'staging' : 'production',
254
+ 'placeholder_card_number' => __( '•••• •••• •••• ••••', 'woocommerce-square' ),
255
+ 'placeholder_card_expiration' => __( 'MM / YY', 'woocommerce-square' ),
256
+ 'placeholder_card_cvv' => __( 'CVV', 'woocommerce-square' ),
257
+ 'placeholder_card_postal_code' => __( 'Card Postal Code', 'woocommerce-square' ),
258
+ 'payment_form_input_styles' => esc_js( $this->get_input_styles() ),
259
+ 'custom_form_trigger_element' => apply_filters( 'woocommerce_square_payment_form_trigger_element', esc_js( '' ) ),
260
+ ) );
261
+
262
+ wp_enqueue_script( 'woocommerce-square' );
263
+
264
+ wp_enqueue_style( 'woocommerce-square-styles', WC_SQUARE_PLUGIN_URL . '/assets/css/wc-square-frontend-styles.css' );
265
+
266
+ return true;
267
+ }
268
+
269
+ /**
270
+ * Process the payment
271
+ */
272
+ public function process_payment( $order_id, $retry = true ) {
273
+ $order = wc_get_order( $order_id );
274
+ $nonce = isset( $_POST['square_nonce'] ) ? wc_clean( $_POST['square_nonce'] ) : '';
275
+ $currency = version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->get_order_currency() : $order->get_currency();
276
+
277
+ $this->log( "Info: Begin processing payment for order {$order_id} for the amount of {$order->get_total()}" );
278
+
279
+ try {
280
+ $data = array(
281
+ 'idempotency_key' => uniqid(),
282
+ 'amount_money' => array(
283
+ 'amount' => (int) WC_Square_Utils::format_amount_to_square( $order->get_total(), $currency ),
284
+ 'currency' => $currency,
285
+ ),
286
+ 'reference_id' => (string) $order->get_order_number(),
287
+ 'delay_capture' => $this->capture ? true : false,
288
+ 'card_nonce' => $nonce,
289
+ 'buyer_email_address' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->billing_email : $order->get_billing_email(),
290
+ 'billing_address' => array(
291
+ 'address_line_1' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->billing_address_1 : $order->get_billing_address_1(),
292
+ 'address_line_2' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->billing_address_2 : $order->get_billing_address_2(),
293
+ 'locality' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->billing_city : $order->get_billing_city(),
294
+ 'administrative_district_level_1' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->billing_state : $order->get_billing_state(),
295
+ 'postal_code' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->billing_postcode : $order->get_billing_postcode(),
296
+ 'country' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->billing_country : $order->get_billing_country(),
297
+ ),
298
+ 'note' => apply_filters( 'woocommerce_square_payment_order_note', 'WooCommerce: Order #' . (string) $order->get_order_number(), $order ),
299
+ );
300
+
301
+ if ( $order->needs_shipping_address() ) {
302
+ $data['shipping_address'] = array(
303
+ 'address_line_1' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->shipping_address_1 : $order->get_shipping_address_1(),
304
+ 'address_line_2' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->shipping_address_2 : $order->get_shipping_address_2(),
305
+ 'locality' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->shipping_city : $order->get_shipping_city(),
306
+ 'administrative_district_level_1' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->shipping_state : $order->get_shipping_state(),
307
+ 'postal_code' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->shipping_postcode : $order->get_shipping_postcode(),
308
+ 'country' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->shipping_country : $order->get_shipping_country(),
309
+ );
310
+ }
311
+
312
+ $result = $this->connect->charge_card_nonce( Woocommerce_Square::instance()->integration->get_option( 'location' ), $data );
313
+
314
+ if ( is_wp_error( $result ) ) {
315
+ wc_add_notice( __( 'Error: Square was unable to complete the transaction. Please try again later or use another means of payment.', 'woocommerce-square' ), 'error' );
316
+
317
+ throw new Exception( $result->get_error_message() );
318
+ }
319
+
320
+ if ( ! empty( $result->errors ) ) {
321
+ if ( 'INVALID_REQUEST_ERROR' === $result->errors[0]->category ) {
322
+ wc_add_notice( __( 'Error: Square was unable to complete the transaction. Please try again later or use another means of payment.', 'woocommerce-square' ), 'error' );
323
+ }
324
+
325
+ if ( 'PAYMENT_METHOD_ERROR' === $result->errors[0]->category || 'VALIDATION_ERROR' === $result->errors[0]->category ) {
326
+ // format errors for display
327
+ $error_html = __( 'Payment Error: ', 'woocommerce-square' );
328
+ $error_html .= '<br />';
329
+ $error_html .= '<ul>';
330
+
331
+ foreach ( $result->errors as $error ) {
332
+ $error_html .= '<li>' . $error->detail . '</li>';
333
+ }
334
+
335
+ $error_html .= '</ul>';
336
+
337
+ wc_add_notice( $error_html, 'error' );
338
+ }
339
+
340
+ $errors = print_r( $result->errors, true );
341
+
342
+ throw new Exception( $errors );
343
+ }
344
+
345
+ if ( empty( $result ) ) {
346
+ wc_add_notice( __( 'Error: Square was unable to complete the transaction. Please try again later or use another means of payment.', 'woocommerce-square' ), 'error' );
347
+
348
+ throw new Exception( 'Unknown Error' );
349
+ }
350
+
351
+ if ( 'CAPTURED' === $result->transaction->tenders[0]->card_details->status ) {
352
+ // Store captured value
353
+ update_post_meta( $order_id, '_square_charge_captured', 'yes' );
354
+
355
+ // Payment complete
356
+ $order->payment_complete( $result->transaction->id );
357
+
358
+ // Add order note
359
+ $complete_message = sprintf( __( 'Square charge complete (Charge ID: %s)', 'woocommerce-square' ), $result->transaction->id );
360
+ $order->add_order_note( $complete_message );
361
+ $this->log( "Success: $complete_message" );
362
+ } else {
363
+ // Store captured value
364
+ update_post_meta( $order_id, '_square_charge_captured', 'no' );
365
+ update_post_meta( $order_id, '_transaction_id', $result->transaction->id );
366
+
367
+ // Mark as on-hold
368
+ $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 );
369
+ $order->update_status( 'on-hold', $authorized_message );
370
+ $this->log( "Success: $authorized_message" );
371
+
372
+ // Reduce stock levels
373
+ version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->reduce_order_stock() : wc_reduce_stock_levels( $order_id );
374
+ }
375
+
376
+ // we got this far which means the transaction went through
377
+ if ( $this->create_customer ) {
378
+ $this->maybe_create_customer( $order );
379
+ }
380
+
381
+ // Remove cart
382
+ WC()->cart->empty_cart();
383
+
384
+ // Return thank you page redirect
385
+ return array(
386
+ 'result' => 'success',
387
+ 'redirect' => $this->get_return_url( $order ),
388
+ );
389
+ } catch ( Exception $e ) {
390
+ $this->log( sprintf( __( 'Error: %s', 'woocommerce-square' ), $e->getMessage() ) );
391
+
392
+ $order->update_status( 'failed', $e->getMessage() );
393
+
394
+ return;
395
+ }
396
+ }
397
+
398
+ /**
399
+ * Tries to create the customer on Square
400
+ *
401
+ * @param object $order
402
+ */
403
+ public function maybe_create_customer( $order ) {
404
+ $user = get_current_user_id();
405
+ $square_customer_id = get_user_meta( $user, '_square_customer_id', true );
406
+ $create_customer = true;
407
+ $phone_number = (string) version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->billing_phone : $order->get_billing_phone();
408
+
409
+ $customer = array(
410
+ 'given_name' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->billing_first_name : $order->get_billing_first_name(),
411
+ 'family_name' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->billing_last_name : $order->get_billing_last_name(),
412
+ 'email_address' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->billing_email : $order->get_billing_email(),
413
+ 'address' => array(
414
+ 'address_line_1' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->billing_address_1 : $order->get_billing_address_1(),
415
+ 'address_line_2' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->billing_address_2 : $order->get_billing_address_2(),
416
+ 'locality' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->billing_city : $order->get_billing_city(),
417
+ 'administrative_district_level_1' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->billing_state : $order->get_billing_state(),
418
+ 'postal_code' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->billing_postcode : $order->get_billing_postcode(),
419
+ 'country' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->billing_country : $order->get_billing_country(),
420
+ ),
421
+ 'phone_number' => ! empty( $phone_number ) ? $phone_number : null,
422
+ 'reference_id' => ! empty( $user ) ? (string) $user : __( 'Guest', 'woocommerce-square' ),
423
+ );
424
+
425
+ // to prevent creating duplicate customer
426
+ // check to make sure this customer does not exist on Square
427
+ if ( ! empty( $square_customer_id ) ) {
428
+ $square_customer = $this->connect->get_customer( $square_customer_id );
429
+
430
+ if ( empty( $square_customer->errors ) ) {
431
+ // customer already exist on Square
432
+ $create_customer = false;
433
+ }
434
+ }
435
+
436
+ if ( $create_customer ) {
437
+ $result = $this->connect->create_customer( $customer );
438
+
439
+ // we don't want to halt any processes here just log it
440
+ if ( is_wp_error( $result ) ) {
441
+ $this->log( sprintf( __( 'Error creating customer: %s', 'woocommerce-square' ), $result->get_error_message() ) );
442
+ $order->add_order_note( sprintf( __( 'Error creating customer: %s', 'woocommerce-square' ), $result->get_error_message() ) );
443
+ }
444
+
445
+ // we don't want to halt any processes here just log it
446
+ if ( ! empty( $result->errors ) ) {
447
+ $this->log( sprintf( __( 'Error creating customer: %s', 'woocommerce-square' ), print_r( $result->errors, true ) ) );
448
+ $order->add_order_note( sprintf( __( 'Error creating customer: %s', 'woocommerce-square' ), print_r( $result->errors, true ) ) );
449
+ }
450
+
451
+ // if no errors save Square customer ID to user meta
452
+ if ( ! is_wp_error( $result ) && empty( $result->errors ) && ! empty( $user ) ) {
453
+ update_user_meta( $user, '_square_customer_id', $result->customer->id );
454
+ $order->add_order_note( sprintf( __( 'Customer created on Square: %s', 'woocommerce-square' ), $result->customer->id ) );
455
+ }
456
+ }
457
+ }
458
+
459
+ /**
460
+ * Refund a charge
461
+ * @param int $order_id
462
+ * @param float $amount
463
+ * @return bool
464
+ */
465
+ public function process_refund( $order_id, $amount = null, $reason = '' ) {
466
+ $order = wc_get_order( $order_id );
467
+
468
+ if ( ! $order || ! $order->get_transaction_id() ) {
469
+ return false;
470
+ }
471
+
472
+ if ( 'square' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->payment_method : $order->get_payment_method() ) ) {
473
+ try {
474
+ $this->log( "Info: Begin refund for order {$order_id} for the amount of {$amount}" );
475
+
476
+ $trans_id = get_post_meta( $order_id, '_transaction_id', true );
477
+ $captured = get_post_meta( $order_id, '_square_charge_captured', true );
478
+ $location = Woocommerce_Square::instance()->integration->get_option( 'location' );
479
+
480
+ $transaction_status = $this->connect->get_transaction_status( $location, $trans_id );
481
+
482
+ if ( 'CAPTURED' === $transaction_status ) {
483
+ $tender_id = $this->connect->get_tender_id( $location, $trans_id );
484
+
485
+ $body = array();
486
+
487
+ $body['idempotency_key'] = uniqid();
488
+ $body['tender_id'] = $tender_id;
489
+
490
+ if ( ! is_null( $amount ) ) {
491
+ $body['amount_money'] = array(
492
+ 'amount' => (int) WC_Square_Utils::format_amount_to_square( $amount ),
493
+ 'currency' => version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->get_order_currency() : $order->get_currency(),
494
+ );
495
+ }
496
+
497
+ if ( $reason ) {
498
+ $body['reason'] = $reason;
499
+ }
500
+
501
+ $result = $this->connect->refund_transaction( $location, $trans_id, $body );
502
+
503
+ if ( is_wp_error( $result ) ) {
504
+ throw new Exception( $result->get_error_message() );
505
+
506
+ } elseif ( ! empty( $result->errors ) ) {
507
+ throw new Exception( 'Error: ' . print_r( $result->errors, true ) );
508
+
509
+ } else {
510
+ if ( 'APPROVED' === $result->refund->status || 'PENDING' === $result->refund->status ) {
511
+ $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 );
512
+
513
+ $order->add_order_note( $refund_message );
514
+
515
+ $this->log( 'Success: ' . html_entity_decode( strip_tags( $refund_message ) ) );
516
+
517
+ return true;
518
+ }
519
+ }
520
+ }
521
+ } catch ( Exception $e ) {
522
+ $this->log( sprintf( __( 'Error: %s', 'woocommerce-square' ), $e->getMessage() ) );
523
+
524
+ return false;
525
+ }
526
+ }
527
+ }
528
+
529
+ /**
530
+ * Logs
531
+ *
532
+ * @since 1.0.0
533
+ * @version 1.0.0
534
+ *
535
+ * @param string $message
536
+ */
537
+ public function log( $message ) {
538
+ if ( $this->logging ) {
539
+ WC_Square_Payment_Logger::log( $message );
540
+ }
541
+ }
542
+ }
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
+ }
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
+ }
includes/payment/class-wc-square-payments.php ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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( $_REQUEST['post'] );
72
+
73
+ // bail if the order wasn't paid for with this gateway
74
+ if ( 'square' !== ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->payment_method : $order->get_payment_method() ) ) {
75
+ return $actions;
76
+ }
77
+
78
+ // bail if charge was already captured
79
+ if ( 'yes' === get_post_meta( version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->id : $order->get_id(), '_square_charge_captured', true ) ) {
80
+ return $actions;
81
+ }
82
+
83
+ $actions['square_capture_charge'] = esc_html__( 'Capture Charge', 'woocommerce-square' );
84
+
85
+ return $actions;
86
+ }
87
+
88
+ public function maybe_capture_charge( $order ) {
89
+ if ( ! is_object( $order ) ) {
90
+ $order = wc_get_order( $order );
91
+ }
92
+
93
+ $this->capture_payment( version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->id : $order->get_id() );
94
+
95
+ return true;
96
+ }
97
+
98
+ /**
99
+ * Capture payment when the order is changed from on-hold to complete or processing
100
+ *
101
+ * @param int $order_id
102
+ */
103
+ public function capture_payment( $order_id ) {
104
+ $order = wc_get_order( $order_id );
105
+
106
+ if ( 'square' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->payment_method : $order->get_payment_method() ) ) {
107
+ try {
108
+ $this->log( "Info: Begin capture for order {$order_id}" );
109
+
110
+ $trans_id = get_post_meta( $order_id, '_transaction_id', true );
111
+ $captured = get_post_meta( $order_id, '_square_charge_captured', true );
112
+ $location = Woocommerce_Square::instance()->integration->get_option( 'location' );
113
+ $token = get_option( 'woocommerce_square_merchant_access_token' );
114
+
115
+ $this->connect->set_access_token( $token );
116
+
117
+ $transaction_status = $this->connect->get_transaction_status( $location, $trans_id );
118
+
119
+ if ( 'AUTHORIZED' === $transaction_status ) {
120
+ $result = $this->connect->capture_transaction( $location, $trans_id ); // returns empty object
121
+
122
+ if ( is_wp_error( $result ) ) {
123
+ $order->add_order_note( __( 'Unable to capture charge!', 'woocommerce-square' ) . ' ' . $result->get_error_message() );
124
+
125
+ throw new Exception( $result->get_error_message() );
126
+ } elseif ( ! empty( $result->errors ) ) {
127
+ $order->add_order_note( __( 'Unable to capture charge!', 'woocommerce-square' ) . ' ' . print_r( $result->errors, true ) );
128
+
129
+ throw new Exception( print_r( $result->errors, true ) );
130
+ } else {
131
+ $order->add_order_note( sprintf( __( 'Square charge complete (Charge ID: %s)', 'woocommerce-square' ), $trans_id ) );
132
+ update_post_meta( $order_id, '_square_charge_captured', 'yes' );
133
+ $this->log( "Info: Capture successful for {$order_id}" );
134
+ }
135
+ }
136
+ } catch ( Exception $e ) {
137
+ $this->log( sprintf( __( 'Error unable to capture charge: %s', 'woocommerce-square' ), $e->getMessage() ) );
138
+ }
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Cancel authorization
144
+ *
145
+ * @param int $order_id
146
+ */
147
+ public function cancel_payment( $order_id ) {
148
+ $order = wc_get_order( $order_id );
149
+
150
+ if ( 'square' === ( version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->payment_method : $order->get_payment_method() ) ) {
151
+ try {
152
+ $this->log( "Info: Cancel payment for order {$order_id}" );
153
+
154
+ $trans_id = get_post_meta( $order_id, '_transaction_id', true );
155
+ $captured = get_post_meta( $order_id, '_square_charge_captured', true );
156
+ $location = Woocommerce_Square::instance()->integration->get_option( 'location' );
157
+ $token = get_option( 'woocommerce_square_merchant_access_token' );
158
+
159
+ $this->connect->set_access_token( $token );
160
+
161
+ $transaction_status = $this->connect->get_transaction_status( $location, $trans_id );
162
+
163
+ if ( 'AUTHORIZED' === $transaction_status ) {
164
+ $result = $this->connect->void_transaction( $location, $trans_id ); // returns empty object
165
+
166
+ if ( is_wp_error( $result ) ) {
167
+ $order->add_order_note( __( 'Unable to void charge!', 'woocommerce-square' ) . ' ' . $result->get_error_message() );
168
+ throw new Exception( $result->get_error_message() );
169
+ } elseif ( ! empty( $result->errors ) ) {
170
+ $order->add_order_note( __( 'Unable to void charge!', 'woocommerce-square' ) . ' ' . print_r( $result->errors, true ) );
171
+
172
+ throw new Exception( print_r( $result->errors, true ) );
173
+ } else {
174
+ $order->add_order_note( sprintf( __( 'Square charge voided! (Charge ID: %s)', 'woocommerce-square' ), $trans_id ) );
175
+ delete_post_meta( $order_id, '_square_charge_captured' );
176
+ delete_post_meta( $order_id, '_transaction_id' );
177
+ }
178
+ }
179
+ } catch ( Exception $e ) {
180
+ $this->log( sprintf( __( 'Unable to void charge!: %s', 'woocommerce-square' ), $e->getMessage() ) );
181
+ }
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Logs
187
+ *
188
+ * @since 1.0.0
189
+ * @version 1.0.0
190
+ *
191
+ * @param string $message
192
+ */
193
+ public function log( $message ) {
194
+ if ( $this->logging ) {
195
+ WC_Square_Payment_Logger::log( $message );
196
+ }
197
+ }
198
+ }
199
+
200
+ new WC_Square_Payments( new WC_Square_Payments_Connect() );
readme.txt ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ === WooCommerce Square ===
2
+ Contributors: automattic, royho, woothemes
3
+ Tags: credit card, square, woocommerce, inventory sync
4
+ Requires at least: 4.4
5
+ Tested up to: 4.9
6
+ Requires PHP: 5.6
7
+ Stable tag: 1.0.25
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.25 - 2018-01-29 =
61
+ * Tweaks - Error handling.
62
+ * Public release on .org
63
+
64
+ = 1.0.24 - 2017-12-13 =
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
+ == Upgrade Notice ==
70
+
71
+ = 1.0.24 =
72
+ * Public Release!
uninstall.php ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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_option( 'woocommerce_square_merchant_access_token' );
23
+ }
woocommerce-square.php ADDED
@@ -0,0 +1,419 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Plugin Name: WooCommerce Square
4
+ * Version: 1.0.25
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: 4.7.2
11
+ * WC requires at least: 2.6
12
+ * WC tested up to: 3.3
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.25' );
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( 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' ) ) {
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/class-wc-square-install.php' );
234
+ require_once( dirname( __FILE__ ) . '/includes/class-wc-square-deactivation.php' );
235
+ require_once( dirname( __FILE__ ) . '/includes/class-wc-square-sync-logger.php' );
236
+ require_once( dirname( __FILE__ ) . '/includes/class-wc-square-wc-products.php' );
237
+ require_once( dirname( __FILE__ ) . '/includes/class-wc-square-connect.php' );
238
+ require_once( dirname( __FILE__ ) . '/includes/class-wc-square-sync-to-square.php' );
239
+ require_once( dirname( __FILE__ ) . '/includes/class-wc-square-sync-from-square.php' );
240
+ require_once( dirname( __FILE__ ) . '/includes/admin/class-wc-square-admin-integration.php' );
241
+ require_once( dirname( __FILE__ ) . '/includes/class-wc-square-sync-to-square-wp-hooks.php' );
242
+ require_once( dirname( __FILE__ ) . '/includes/class-wc-square-client.php' );
243
+ require_once( dirname( __FILE__ ) . '/includes/class-wc-square-utils.php' );
244
+ require_once( dirname( __FILE__ ) . '/includes/class-wc-square-inventory-poll.php' );
245
+ require_once( dirname( __FILE__ ) . '/includes/payment/class-wc-square-payment-logger.php' );
246
+ require_once( dirname( __FILE__ ) . '/includes/payment/class-wc-square-payments.php' );
247
+
248
+ if ( is_admin() ) {
249
+ require_once( dirname( __FILE__ ) . '/includes/admin/class-wc-square-bulk-sync-handler.php' );
250
+ require_once( dirname( __FILE__ ) . '/includes/admin/class-wc-square-admin-product-meta-box.php' );
251
+ }
252
+
253
+ }
254
+
255
+ /**
256
+ * Add integration settings page
257
+ *
258
+ * @access public
259
+ * @since 1.0.0
260
+ * @version 1.0.0
261
+ * @return bool
262
+ */
263
+ public function include_integration( $integrations ) {
264
+ // Square only supports US and Canada for now.
265
+ if ( $this->is_allowed_currencies() && $this->is_allowed_countries() ) {
266
+ $integrations[] = $this->integration;
267
+ }
268
+
269
+ return $integrations;
270
+
271
+ }
272
+
273
+ /**
274
+ * Initializes hooks
275
+ *
276
+ * @access public
277
+ * @since 1.0.0
278
+ * @version 1.0.0
279
+ * @return bool
280
+ */
281
+ public function init_hooks() {
282
+
283
+ register_deactivation_hook( __FILE__, array( 'WC_Square_Deactivation', 'deactivate' ) );
284
+
285
+ if ( is_woocommerce_active() ) {
286
+
287
+ add_filter( 'woocommerce_integrations', array( $this, 'include_integration' ) );
288
+
289
+ add_action( 'plugins_loaded', array( $this, 'load_plugin_textdomain' ) );
290
+
291
+ add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) );
292
+
293
+ add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_styles' ) );
294
+
295
+ add_action( 'woocommerce_square_bulk_syncing_square_to_wc', array( $this->wc_to_square_wp_hooks, 'disable' ) );
296
+
297
+ add_action( 'admin_notices', array( $this, 'is_connected_to_square' ) );
298
+
299
+ } else {
300
+
301
+ add_action( 'admin_notices', array( $this, 'woocommerce_missing_notice' ) );
302
+
303
+ }
304
+
305
+ }
306
+
307
+ /**
308
+ * Loads the admin JS scripts
309
+ *
310
+ * @access public
311
+ * @since 1.0.0
312
+ * @version 1.0.0
313
+ * @return bool
314
+ */
315
+ public function enqueue_admin_scripts() {
316
+ $current_screen = get_current_screen();
317
+
318
+ $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
319
+
320
+ 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 );
321
+
322
+ if ( 'woocommerce_page_wc-settings' === $current_screen->id ) {
323
+
324
+ wp_enqueue_script( 'wc-square-admin-scripts' );
325
+
326
+ $localized_vars = array(
327
+ 'admin_ajax_url' => admin_url( 'admin-ajax.php' ),
328
+ 'ajaxSyncNonce' => wp_create_nonce( 'square-sync' ),
329
+ 'i18n' => array(
330
+ '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' ),
331
+ ),
332
+ );
333
+
334
+ wp_localize_script( 'wc-square-admin-scripts', 'wc_square_local', $localized_vars );
335
+ }
336
+
337
+ return true;
338
+ }
339
+
340
+ /**
341
+ * Loads the admin CSS styles
342
+ *
343
+ * @access public
344
+ * @since 1.0.0
345
+ * @version 1.0.0
346
+ * @return bool
347
+ */
348
+ public function enqueue_admin_styles() {
349
+ $current_screen = get_current_screen();
350
+
351
+ wp_register_style( 'wc-square-admin-styles', WC_SQUARE_PLUGIN_URL . '/assets/css/wc-square-admin-styles.css', null, WC_SQUARE_VERSION );
352
+
353
+ if ( 'woocommerce_page_wc-settings' === $current_screen->id ) {
354
+
355
+ wp_enqueue_style( 'wc-square-admin-styles' );
356
+ }
357
+
358
+ return true;
359
+ }
360
+
361
+ /**
362
+ * Load the plugin text domain for translation.
363
+ *
364
+ * @access public
365
+ * @since 1.0.0
366
+ * @version 1.0.0
367
+ * @return bool
368
+ */
369
+ public function load_plugin_textdomain() {
370
+ $locale = apply_filters( 'woocommerce_square_plugin_locale', get_locale(), 'woocommerce-square' );
371
+
372
+ load_textdomain( 'woocommerce-square', trailingslashit( WP_LANG_DIR ) . 'woocommerce-square/woocommerce-square' . '-' . $locale . '.mo' );
373
+
374
+ load_plugin_textdomain( 'woocommerce-square', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
375
+
376
+ return true;
377
+ }
378
+
379
+ /**
380
+ * WooCommerce fallback notice.
381
+ *
382
+ * @access public
383
+ * @since 1.0.0
384
+ * @version 1.0.0
385
+ * @return string
386
+ */
387
+ public function woocommerce_missing_notice() {
388
+ 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>';
389
+
390
+ return true;
391
+ }
392
+
393
+ /**
394
+ * Shows a notice when the site is not yet connected to square.
395
+ *
396
+ * @access public
397
+ * @since 1.0.0
398
+ * @version 1.0.0
399
+ * @return string
400
+ */
401
+ public function is_connected_to_square() {
402
+ $settings = get_option( 'woocommerce_squareconnect_settings', '' );
403
+ $existing_token = get_option( 'woocommerce_square_merchant_access_token' );
404
+
405
+ if ( empty( $existing_token ) ) {
406
+ 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>';
407
+ }
408
+
409
+ if ( empty( $settings ) || empty( $settings['location'] ) ) {
410
+ 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>';
411
+ }
412
+
413
+ return true;
414
+ }
415
+ }
416
+
417
+ Woocommerce_Square::instance();
418
+
419
+ endif;