PluginProbe
WooCommerce Square / 3.0.3
WooCommerce Square v3.0.3
5.5.0 5.4.3 5.4.2 5.4.1 5.4.0 trunk 1.0.25 1.0.26 1.0.27 1.0.28 1.0.29 1.0.30 1.0.31 1.0.32 1.0.33 1.0.34 1.0.35 1.0.36 1.0.37 1.0.38 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 All 132 releases
← All changes | includes/API.php +24 -546 trunk3.0.3 View file →
@@ -4,9 +4,9 @@
4 4 *
5 5 * This source file is subject to the GNU General Public License v3.0
6 6 * that is bundled with this package in the file license.txt.
7 7 * It is also available through the world-wide-web at this URL:
8 - * http://www.gnu.org/licenses/gpl-3.0.html GNU General Public License v3.0 or later
8 + * http://www.gnu.org/licenses/gpl-3.0.html
9 9 * If you did not receive a copy of the license and are unable to
10 10 * obtain it through the world-wide-web, please send an email
11 11 * to license@woocommerce.com so we can send you a copy immediately.
12 12 *
@@ -17,19 +17,16 @@
17 17 * needs please refer to https://docs.woocommerce.com/document/woocommerce-square/
18 18 *
19 19 * @author WooCommerce
20 20 * @copyright Copyright: (c) 2019, Automattic, Inc.
21 - * @license http://www.gnu.org/licenses/gpl-3.0.html GNU General Public License v3.0 or later
21 + * @license http://www.gnu.org/licenses/gpl-3.0.html GNU General Public License v3.0
22 22 */
23 23
24 24 namespace WooCommerce\Square;
25 25
26 +use WooCommerce\Square\Framework\Api\Base;
26 27 use WooCommerce\Square\API\Requests;
27 28 use WooCommerce\Square\API\Responses;
28 -use WooCommerce\Square\Framework\Api\Base;
29 -use WooCommerce\Square\Utilities\Coupon_Utility;
30 -use Square\Models\CatalogObject;
31 -use Square\Models\ListCatalogResponse;
32 29 use Square\SquareClient;
33 30 use Square\Environment;
34 31
35 32 defined( 'ABSPATH' ) || exit;
@@ -41,27 +38,8 @@
41 38 */
42 39 class API extends Base {
43 40
44 41
45 - /**
46 - * Holds item option pages while the catalogue is still being walked.
47 - *
48 - * Kept apart from `wc_square_options_data`, which means "every option, fully read". Writing
49 - * partial pages under that name would make the next call return early with an incomplete cache.
50 - *
51 - * One key is shared by every walk, which is only safe because the background job runner takes
52 - * the oldest queued or processing job on every tick and runs a single step of it
53 - * (`WP_Background_Job_Handler::get_job()`, `ORDER BY option_id ASC LIMIT 1`). A job therefore
54 - * holds the runner for the whole of its walk, and no second job can interleave a step that
55 - * starts a competing read. If job selection ever gains priority ordering or parallel runners,
56 - * two walks could overwrite each other's pages here and promote a silently truncated cache,
57 - * which is the very failure this class of bug is about. Make the key per job before that lands.
58 - *
59 - * @since 5.5.0
60 - * @var string
61 - */
62 - const OPTIONS_DATA_PARTIAL_TRANSIENT = 'wc_square_options_data_partial';
63 -
64 42 /** catalog request type */
65 43 const REQUEST_TYPE_CATALOG = 'catalog';
66 44
67 45 /** inventory request type */
@@ -66,15 +44,9 @@
66 44
67 45 /** inventory request type */
68 46 const REQUEST_TYPE_INVENTORY = 'inventory';
69 47
70 - /** tax type inclusive */
71 - const TAX_TYPE_INCLUSIVE = 'INCLUSIVE';
72 48
73 - /** tax type additive */
74 - const TAX_TYPE_ADDITIVE = 'ADDITIVE';
75 -
76 -
77 49 /** @var \Square\SquareClient Square API client instance */
78 50 protected $client;
79 51
80 52
@@ -86,14 +58,12 @@
86 58 * @param string $access_token Square API access token
87 59 * @param bool $is_sandbox If sandbox access is desired
88 60 */
89 61 public function __construct( $access_token, $is_sandbox = null ) {
90 - $this->client = new SquareClient(
91 - array(
92 - 'accessToken' => $access_token,
93 - 'environment' => $is_sandbox ? Environment::SANDBOX : Environment::PRODUCTION,
94 - )
95 - );
62 + $this->client = new SquareClient( [
63 + 'accessToken' => $access_token,
64 + 'environment' => $is_sandbox ? Environment::SANDBOX : Environment::PRODUCTION,
65 + ] );
96 66 }
97 67
98 68
99 69 /** Catalog API Methods *******************************************************************************************/
@@ -214,18 +184,15 @@
214 184 * @since 2.0.0
215 185 *
216 186 * @param string $object_id the Square catalog object ID
217 187 * @param bool $include_related_objects whether or not to include related objects (such as categories)
218 - * @param int|null $object_version The specific version of the object to retrieve. Optional - defaults to latest.
219 - * If the specified version of the object does not exist, the Square API will
220 - * return the latest version. If the version provided is not known to exist consumers
221 - * of this function should validate the version returned by the API.
222 188 * @return Responses\Catalog
223 189 * @throws \Exception
224 190 */
225 - public function retrieve_catalog_object( $object_id, $include_related_objects = false, $object_version = null ) {
191 + public function retrieve_catalog_object( $object_id, $include_related_objects = false ) {
192 +
226 193 $request = $this->get_catalog_request();
227 - $request->set_retrieve_catalog_object_data( $object_id, $include_related_objects, $object_version );
194 + $request->set_retrieve_catalog_object_data( $object_id, $include_related_objects );
228 195
229 196 return $this->perform_request( $request );
230 197 }
231 198
@@ -325,9 +292,9 @@
325 292 if ( ! is_readable( $image_path ) ) {
326 293 throw new \Exception( 'Image file is not readable' );
327 294 }
328 295
329 - $image = file_get_contents( $image_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
296 + $image = file_get_contents( $image_path );
330 297
331 298 $headers = array(
332 299 'accept' => 'application/json',
333 300 'content-type' => 'multipart/form-data; boundary="boundary"',
@@ -353,9 +320,9 @@
353 320 if ( $square_item_id ) {
354 321 $request['object_id'] = $square_item_id;
355 322 }
356 323
357 - $body .= json_encode( $request ); // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode
324 + $body .= json_encode( $request );
358 325
359 326 $body .= "\r\n";
360 327
361 328 $body .= '--boundary' . "\r\n";
@@ -374,9 +341,9 @@
374 341 )
375 342 );
376 343
377 344 if ( is_wp_error( $response ) ) {
378 - throw new \Exception( esc_html( $response->get_error_message() ) );
345 + throw new \Exception( $response->get_error_message() );
379 346 }
380 347
381 348 $body = wp_remote_retrieve_body( $response );
382 349 $body = json_decode( $body, true );
@@ -392,9 +359,9 @@
392 359 } else {
393 360 $message = 'Unknown error';
394 361 }
395 362
396 - throw new \Exception( esc_html( $message ) );
363 + throw new \Exception( $message );
397 364 }
398 365
399 366 return $body['image']['id'];
400 367 }
@@ -621,360 +588,9 @@
621 588
622 589 return $this->perform_request( $request );
623 590 }
624 591
625 - /**
626 - * Fetch the option (attribute) names from Square.
627 - *
628 - * @since 4.9.0
629 - *
630 - * @throws \Exception
631 - */
632 - public function retrieve_options_data( $cursor = '', $refresh = false ) {
633 - $options_data = get_transient( 'wc_square_options_data' );
634 592
635 - // Stop if transient exists and we don't want to refresh.
636 - if ( $options_data && ! $refresh ) {
637 - if ( $this->options_transient_needs_refresh( $options_data ) ) {
638 - $refresh = true;
639 - $options_data = array();
640 - delete_transient( 'wc_square_options_data' );
641 - } else {
642 - return array( '', $options_data, $cursor );
643 - }
644 - }
645 -
646 - // If transient doesn't exist, initialize the array.
647 - if ( ! is_array( $options_data ) ) {
648 - $options_data = array();
649 - }
650 -
651 - // Carry earlier pages forward. The finished cache is only written once the cursor is
652 - // exhausted, because the early return above would otherwise hand back a half built cache and
653 - // re-emit the same cursor forever. So pages accumulate under their own key, and without this
654 - // every page starts from nothing and only the last one survives.
655 - if ( $cursor ) {
656 - $partial = get_transient( self::OPTIONS_DATA_PARTIAL_TRANSIENT );
657 -
658 - if ( is_array( $partial ) ) {
659 - $options_data = $partial + $options_data;
660 - }
661 - }
662 -
663 - $response = $this->list_catalog( $cursor, array( 'ITEM_OPTION' ) );
664 -
665 - if ( ! $response->get_data() instanceof ListCatalogResponse ) {
666 - throw new \Exception( 'API response data is invalid' );
667 - }
668 -
669 - $objects = $response->get_data()->getObjects() ? $response->get_data()->getObjects() : array();
670 -
671 - foreach ( $objects as $object ) {
672 - $option_name = $this->get_item_option_name_from_catalog_object( $object );
673 - $options_data[ $object->getId() ]['name'] = $option_name;
674 -
675 - $option_values_object = $object->getItemOptionData() ? $object->getItemOptionData()->getValues() : array();
676 - $option_values = array();
677 - $option_values_ids = array();
678 -
679 - foreach ( $option_values_object as $option_value ) {
680 - $option_values[] = $option_value->getItemOptionValueData()->getName();
681 - $option_values_ids[ $option_value->getId() ] = $option_value->getItemOptionValueData()->getName();
682 - }
683 - $options_data[ $object->getId() ]['values'] = $option_values;
684 - $options_data[ $object->getId() ]['value_ids'] = $option_values_ids;
685 - }
686 -
687 - $cursor = $response->get_data()->getCursor();
688 - if ( $cursor ) {
689 - // More pages to come, so keep what has been read so far somewhere the early return
690 - // cannot mistake for a finished cache. Given the same lifetime as the finished cache so
691 - // a walk left sitting in the queue cannot come back to a vanished partial and resume
692 - // from nothing, which would truncate it in silence.
693 - set_transient( self::OPTIONS_DATA_PARTIAL_TRANSIENT, $options_data, DAY_IN_SECONDS );
694 - } else {
695 - set_transient( 'wc_square_options_data', $options_data, DAY_IN_SECONDS );
696 - delete_transient( self::OPTIONS_DATA_PARTIAL_TRANSIENT );
697 - }
698 -
699 - return array( $response, $options_data, $cursor );
700 - }
701 -
702 - /**
703 - * Determines whether an option creation failure means the cached options data is stale.
704 - *
705 - * Only a rejection saying the option or its value already exists in Square is worth replaying
706 - * the job for, because that is the one cause a refetch of the options data resolves. The
707 - * message is matched rather than the error code, since Square answers both the stale cache case
708 - * and a permanently invalid payload with the same INVALID_VALUE and BAD_REQUEST codes.
709 - *
710 - * @since 5.5.0
711 - *
712 - * @param string $message the message Square returned
713 - * @return bool
714 - */
715 - protected function is_stale_options_cache_error( $message ) {
716 -
717 - foreach ( array( 'already exists', 'existing item option' ) as $needle ) {
718 - if ( false !== stripos( $message, $needle ) ) {
719 - return true;
720 - }
721 - }
722 -
723 - return false;
724 - }
725 -
726 -
727 - /**
728 - * Folds one item option into the finished options cache.
729 - *
730 - * Callers here hold a single option they just read or created, not a walked catalogue, so the
731 - * cache is only ever extended, never built: writing one from an unlooped read would pass off
732 - * the first page of a paginated catalogue as the whole of it, for a day. With no cache to
733 - * extend there is nothing to do and the next full read is left to build it.
734 - *
735 - * @since 5.5.0
736 - *
737 - * @param string $option_id Square item option ID.
738 - * @param array $option_data Cache entry for the option.
739 - * @return void
740 - */
741 - public function cache_option_data( $option_id, array $option_data ) {
742 -
743 - if ( ! $option_id ) {
744 - return;
745 - }
746 -
747 - $options_data = get_transient( 'wc_square_options_data' );
748 -
749 - if ( ! is_array( $options_data ) ) {
750 - return;
751 - }
752 -
753 - $options_data[ $option_id ] = $option_data;
754 - set_transient( 'wc_square_options_data', $options_data, DAY_IN_SECONDS );
755 - }
756 -
757 - /**
758 - * Create options and values in Square.
759 - *
760 - * @since 4.9.0
761 - *
762 - * @param string $option_id Option ID.
763 - * @param string $attribute_name Attribute name.
764 - * @param array $attribute_option_values Attribute option values.
765 - *
766 - * @return \Square\Models\CatalogObject
767 - */
768 - public function create_options_and_values( $option_id = false, $attribute_name = '', $attribute_option_values = array() ) {
769 - $options_value_data = array();
770 -
771 - if ( $option_id ) {
772 - $option = null;
773 -
774 - try {
775 - $response = $this->retrieve_catalog_object( $option_id );
776 - $option = $response->get_data() ? $response->get_data()->getObject() : null;
777 - } catch ( \Exception $e ) {
778 - // Square answers a deleted or unknown ID with NOT_FOUND, and the response validator
779 - // turns that into an exception before any data comes back, so the null check below
780 - // never gets the chance to see it. Without this the whole method gives up on a
781 - // stale cached ID, no refresh flag is set, and every sync fails the same way until
782 - // the cache expires a day later.
783 - //
784 - // Narrowed to that one code deliberately, and matched in the bracketed form
785 - // do_post_parse_response_validation() emits so it cannot be tripped by another
786 - // error quoting those words in its detail text. Reading a RATE_LIMITED or an auth
787 - // failure as "no such option" would create a duplicate of an option Square still
788 - // holds, and would swallow the rate limit the job runner needs in order to back off.
789 - if ( false === strpos( $e->getMessage(), '[NOT_FOUND]' ) ) {
790 - throw $e;
791 - }
792 - }
793 -
794 - // A cached option ID can name an object Square no longer has, or one that is not an
795 - // item option at all. Treat that as no match and fall through to the create path,
796 - // rather than calling setValues() on a null item option and taking the sync down.
797 - if ( ! $option || 'ITEM_OPTION' !== $option->getType() || ! $option->getItemOptionData() ) {
798 - $option_id = false;
799 - }
800 - }
801 -
802 - if ( $option_id ) {
803 - // Filter out the existing option values from the attribute values.
804 - $square_existing_option_objects = $option->getItemOptionData() ? $option->getItemOptionData()->getValues() : array();
805 - $options_value_data = $square_existing_option_objects;
806 -
807 - $square_existing_option_values = array();
808 - foreach ( $square_existing_option_objects as $option_object ) {
809 - $square_existing_option_values[] = $option_object->getItemOptionValueData()->getName();
810 - }
811 - // Compared without case for the same reason the option name is: Square rejects a value
812 - // whose name differs from an existing one only by case, so a case sensitive diff would
813 - // ask for a value that can never be created.
814 - $attribute_option_values = array_udiff( $attribute_option_values, $square_existing_option_values, 'strcasecmp' );
815 - } else {
816 - // Initialize the option object with a temp ID prefixed with #.
817 - $option = new \Square\Models\CatalogObject( 'ITEM_OPTION', '' );
818 -
819 - if ( $attribute_name ) {
820 - $option->setId( $option_id ? $option_id : '#' . $attribute_name );
821 - }
822 -
823 - $option->setItemOptionData( new \Square\Models\CatalogItemOption() );
824 - $option->getItemOptionData()->setName( $attribute_name );
825 - $option->getItemOptionData()->setDisplayName( $attribute_name );
826 - }
827 -
828 - // Loop through the attribute values to create option values.
829 - foreach ( $attribute_option_values as $attribute_option_value ) {
830 - $option_value = new \Square\Models\CatalogObject( 'ITEM_OPTION_VAL', '#' . $attribute_name . '_' . $attribute_option_value );
831 - $option_value->setItemOptionValueData( new \Square\Models\CatalogItemOptionValue() );
832 - $option_value->getItemOptionValueData()->setName( $attribute_option_value );
833 -
834 - $options_value_data[] = $option_value;
835 - }
836 -
837 - // Set the option values.
838 - $option->getItemOptionData()->setValues( $options_value_data );
839 -
840 - // Push option object to Square to create a new one. Used timestamp as idempotency_key.
841 - try {
842 - $new_option = $this->upsert_catalog_object( md5( serialize( $option ) ) . time() . '_upsert_option', $option ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
843 -
844 - $id_mappings = $new_option->get_data()->getIdMappings();
845 -
846 - // Resolved on the create path only, and never positionally. The upsert reports a mapping
847 - // for every object it created, so when an option that already existed merely gained a
848 - // value, the first row is that new ITEM_OPTION_VAL and the option has no row at all.
849 - // Index 0 therefore hands back a value ID, which travels on into the item's item_options
850 - // and kills the following sync with "An existing Item Option has name X". On the reuse
851 - // path $option_id is already the real ID Square gave us, so the mappings add nothing and
852 - // must not be allowed to overwrite it.
853 - if ( ! $option_id ) {
854 - // The option went up under the temp ID set above, and that is the client ID Square
855 - // echoes back alongside the real one. It is empty only when no attribute name was
856 - // supplied, and there is nothing to correlate on then: guessing a row is the bug.
857 - $client_option_id = $option->getId();
858 -
859 - foreach ( (array) $id_mappings as $id_mapping ) {
860 - if ( $client_option_id && $client_option_id === $id_mapping->getClientObjectId() ) {
861 - $option_id = $id_mapping->getObjectId();
862 - break;
863 - }
864 - }
865 - }
866 -
867 - $response = $this->retrieve_catalog_object( $option_id );
868 - $option = $response->get_data()->getObject();
869 -
870 - $option_values_object = $option->getItemOptionData() ? $option->getItemOptionData()->getValues() : array();
871 - $option_value_ids = array();
872 - $option_values = array();
873 -
874 - foreach ( $option_values_object as $option_value ) {
875 - $option_value_ids[] = $option_value->getId();
876 - $option_values[] = $option_value->getItemOptionValueData()->getName();
877 - }
878 -
879 - $this->cache_option_data(
880 - $option_id,
881 - array(
882 - 'name' => $attribute_name,
883 - 'values' => $option_values,
884 - 'value_ids' => array_combine( $option_value_ids, $option_values ),
885 - )
886 - );
887 -
888 - } catch ( \Exception $e ) {
889 - /**
890 - * Replaying the whole job only helps when Square rejected this because the option or
891 - * value already exists, which means the cached options data is stale: refetching it and
892 - * running the cycle again resolves it.
893 - *
894 - * Any other rejection is permanent. An option payload Square considers invalid, such as
895 - * a value with no name because a variation attribute has no values selected, fails
896 - * identically on every replay, so asking for one burns the retry budget and then fails
897 - * the whole sync over one product. Leaving the flag alone lets the caller's own error
898 - * handling skip that product and carry on.
899 - */
900 - if ( $this->is_stale_options_cache_error( $e->getMessage() ) ) {
901 - update_option( 'woocommerce_square_refresh_sync_cycle', true );
902 -
903 - wc_square()->log( sprintf( 'Resetting the Sync Job. Failed to create option in Square: %s. The system will refetch latest Options from Square.', $e->getMessage() ) );
904 - } else {
905 - wc_square()->log( sprintf( 'Failed to create option in Square: %s. Not replaying the job: this does not indicate stale options data.', $e->getMessage() ) );
906 - }
907 -
908 - // Refetched next time round either way, since the cache may be incomplete.
909 - delete_transient( 'wc_square_options_data' );
910 - // Cleared alongside the finished cache. A walk abandoned by this reset has no reader
911 - // left, and leaving its pages behind would only give the writes below somewhere stale
912 - // to land until the next walk overwrites it.
913 - delete_transient( self::OPTIONS_DATA_PARTIAL_TRANSIENT );
914 -
915 - throw $e;
916 - }
917 -
918 - return $option;
919 - }
920 -
921 - /**
922 - * Determines the best available name for a catalog option.
923 - *
924 - * @since 5.1.1
925 - *
926 - * @param \Square\Models\CatalogObject $catalog_object Catalog option object.
927 - * @return string
928 - */
929 - public function get_item_option_name_from_catalog_object( ?CatalogObject $catalog_object ) {
930 -
931 - if ( ! $catalog_object instanceof CatalogObject || ! $catalog_object->getItemOptionData() ) {
932 - return '';
933 - }
934 -
935 - $option_data = $catalog_object->getItemOptionData();
936 - $name = $option_data->getName();
937 - $name = is_string( $name ) ? trim( $name ) : '';
938 -
939 - if ( '' === $name && method_exists( $option_data, 'getDisplayName' ) ) {
940 - $display_name = $option_data->getDisplayName();
941 - $name = is_string( $display_name ) ? trim( $display_name ) : '';
942 - }
943 -
944 - return $name;
945 - }
946 -
947 - /**
948 - * Checks if the cached options data is missing names and needs refreshing.
949 - *
950 - * @since 5.1.1
951 - *
952 - * @param mixed $options_data Cached options data.
953 - * @return bool
954 - */
955 - private function options_transient_needs_refresh( $options_data ) {
956 -
957 - if ( ! is_array( $options_data ) ) {
958 - return true;
959 - }
960 -
961 - foreach ( $options_data as $option_data ) {
962 - $name = '';
963 -
964 - if ( is_array( $option_data ) && isset( $option_data['name'] ) ) {
965 - $name = is_string( $option_data['name'] ) ? trim( $option_data['name'] ) : '';
966 - }
967 -
968 - if ( '' === $name ) {
969 - return true;
970 - }
971 - }
972 -
973 - return false;
974 - }
975 -
976 -
977 593 /** Locations methods *********************************************************************************************/
978 594
979 595
980 596 /**
@@ -1095,9 +711,9 @@
1095 711 *
1096 712 * @since 2.0.0
1097 713 *
1098 714 * @param API\Request $request request object
1099 - * @return API\Response
715 + * @return API_Response
1100 716 * @throws \Exception
1101 717 */
1102 718 protected function perform_request( $request ) {
1103 719
@@ -1184,28 +800,21 @@
1184 800 }
1185 801
1186 802 $errors = array();
1187 803
1188 - /** @var \Square\Models\Error $error */
1189 804 foreach ( $this->get_response()->get_errors() as $error ) {
1190 - $error_code = $error->getCode();
1191 - if ( empty( $error_code ) ) {
805 + if ( empty( $error->code ) ) {
1192 806 continue;
1193 807 }
1194 808
1195 - $errors[] = trim( "[{$error_code}] {$error->getDetail()}" );
809 + $errors[] = trim( "[{$error->code}] {$error->detail}" );
1196 810
1197 811 // Last attempt to refresh access token.
1198 - if ( in_array( $error_code, array( 'ACCESS_TOKEN_EXPIRED', 'UNAUTHORIZED' ), true ) ) {
1199 - if ( 'ACCESS_TOKEN_EXPIRED' === $error_code ) {
1200 - $this->get_plugin()->log( 'Access Token Expired, attempting a refresh.' );
1201 - } else {
1202 - $this->get_plugin()->log( 'Authorization error occurred, attempting a refresh.' );
1203 - }
1204 -
812 + if ( 'ACCESS_TOKEN_EXPIRED' == $error->code ) {
813 + $this->get_plugin()->log( 'Access Token Expired, attempting a refresh.' );
1205 814 $this->get_plugin()->get_connection_handler()->refresh_connection();
1206 815
1207 - $failure_value = get_option( 'wc_square_refresh_failed', 'yes' );
816 + $failure_value = get_option( 'wc_' . $this->get_plugin()->get_id() . '_refresh_failed', 'yes' );
1208 817
1209 818 if ( empty( $failure_value ) ) {
1210 819 // Successfully refreshed on the last attempt
1211 820 $this->get_plugin()->log( 'Connection successfully refreshed.' );
@@ -1213,9 +822,9 @@
1213 822 }
1214 823 }
1215 824
1216 825 // if the error indicates that access token is bad, disconnect the plugin to prevent further attempts
1217 - if ( in_array( $error_code, array( 'ACCESS_TOKEN_EXPIRED', 'ACCESS_TOKEN_REVOKED', 'UNAUTHORIZED' ), true ) ) {
826 + if ( in_array( $error->code, array( 'ACCESS_TOKEN_EXPIRED', 'ACCESS_TOKEN_REVOKED' ), true ) ) {
1218 827 $this->get_plugin()->get_connection_handler()->disconnect();
1219 828 $this->get_plugin()->log( 'Disconnected due to invalid authorization. Please try connecting again.' );
1220 829 }
1221 830 }
@@ -1220,31 +829,14 @@
1220 829 }
1221 830 }
1222 831
1223 832 // At this point we could not validate the response and assume a failed attempt.
1224 - throw new \Exception( esc_html( implode( ' | ', $errors ) ) );
833 + throw new \Exception( implode( ' | ', $errors ) );
1225 834 }
1226 835
1227 836 /**
1228 - * Get currency from Square order with fallback to store currency.
1229 - *
1230 - * @param \Square\Models\Order $square_order Square order object.
1231 - * @return string Currency code.
1232 - */
1233 - private function get_square_order_currency_or_store_default( $square_order ) {
1234 - $total_money = $square_order->getTotalMoney();
1235 -
1236 - return $total_money ? $total_money->getCurrency() : get_woocommerce_currency();
1237 - }
1238 -
1239 - /**
1240 837 * Performs a remote request with the Square API class.
1241 838 *
1242 - * Note: This method handles both standard SDK methods and special cases like calculateOrder.
1243 - * The Square PHP SDK's CalculateOrderRequest only supports order and proposed_rewards (loyalty).
1244 - * We need to send proposed_discount_codes (discount code IDs), so we handle calculateOrder
1245 - * as a special case using direct HTTP to maintain consistency with the plugin's request/response pattern.
1246 - *
1247 839 * @since 2.0.0
1248 840 *
1249 841 * @param Object $square_api the square API class instance
1250 842 * @param string $method the class method to call
@@ -1251,125 +843,9 @@
1251 843 * @param array $args the args to send with the method call
1252 844 * @throws \Exception
1253 845 */
1254 846 protected function do_square_request( $square_api, $method, $args ) {
1255 - // Handle calculateOrder as a special case: we need to send proposed_discount_codes,
1256 - // which the SDK's CalculateOrderRequest does not support (it only has order and proposed_rewards).
1257 - if ( 'calculateOrder' === $method ) {
1258 - // Get the request object to access stored data (square_order, proposed_discount_codes, etc.).
1259 - $request = $this->get_request();
1260 847
1261 - // Get Square order and proposed discount codes from request object.
1262 - $square_order = $request->square_order;
1263 - $proposed_discount_codes = $request->proposed_discount_codes;
1264 -
1265 - // Convert Square Order object to array for JSON encoding.
1266 - // The Square SDK Order object implements JsonSerializable.
1267 - $order_data = $square_order->jsonSerialize();
1268 -
1269 - // Build request body.
1270 - $request_body = array(
1271 - 'order' => $order_data,
1272 - );
1273 -
1274 - // Add proposed discount codes if provided.
1275 - // These are the discount code IDs that Square will use to calculate the order.
1276 - if ( ! empty( $proposed_discount_codes ) ) {
1277 - $proposed_discount_codes_array = array();
1278 - foreach ( $proposed_discount_codes as $discount_code_id ) {
1279 - if ( ! empty( $discount_code_id ) ) {
1280 - $proposed_discount_codes_array[] = array(
1281 - 'id' => $discount_code_id,
1282 - );
1283 - }
1284 - }
1285 -
1286 - if ( ! empty( $proposed_discount_codes_array ) ) {
1287 - $request_body['proposed_discount_codes'] = $proposed_discount_codes_array;
1288 - }
1289 - }
1290 -
1291 - // Make direct HTTP request via wrapper (SDK's CalculateOrderRequest does not support proposed_discount_codes).
1292 - $result = Coupon_Utility::square_api_post( 'orders/calculate', $request_body );
1293 -
1294 - if ( is_wp_error( $result ) ) {
1295 - $error_message = $result->get_error_message();
1296 - $error_code = $result->get_error_code();
1297 - $this->get_plugin()->log(
1298 - /* translators: %1$s: error code, %2$s: error message */
1299 - sprintf( __( 'Square CalculateOrder API error [%1$s]: %2$s', 'woocommerce-square' ), ( $error_code ? $error_code : 'unknown' ), $error_message ),
1300 - 'square-coupons'
1301 - );
1302 - throw new \Exception( esc_html__( 'We couldn\'t apply the discount. Please try again later.', 'woocommerce-square' ) );
1303 - }
1304 -
1305 - $data = isset( $result['body'] ) ? $result['body'] : array();
1306 -
1307 - if ( empty( $data['order'] ) ) {
1308 - throw new \Exception( 'Square API did not return order data.' );
1309 - }
1310 -
1311 - // Store raw response data in request object for later access.
1312 - // This is needed when return_raw_response is true, as the raw JSON contains
1313 - // per-line-item discount details that aren't easily accessible from the Order object.
1314 - $request->raw_calculate_order_response = $data['order'];
1315 -
1316 - // Update the original Square Order object with calculated values from the response.
1317 - // This preserves all line items, taxes, etc. from the original order.
1318 - // and only updates the calculated totals (total_money, net_amounts, version).
1319 - $calculated_order_data = $data['order'];
1320 -
1321 - // Update total_money from response (this is the key calculated value).
1322 - if ( isset( $calculated_order_data['total_money'] ) ) {
1323 - $total_money = new \Square\Models\Money();
1324 - if ( isset( $calculated_order_data['total_money']['amount'] ) ) {
1325 - $total_money->setAmount( $calculated_order_data['total_money']['amount'] );
1326 - }
1327 - if ( isset( $calculated_order_data['total_money']['currency'] ) ) {
1328 - $total_money->setCurrency( $calculated_order_data['total_money']['currency'] );
1329 - } else {
1330 - $total_money->setCurrency( $this->get_square_order_currency_or_store_default( $square_order ) );
1331 - }
1332 - $square_order->setTotalMoney( $total_money );
1333 - }
1334 -
1335 - // Update version from response if provided.
1336 - if ( isset( $calculated_order_data['version'] ) ) {
1337 - $square_order->setVersion( $calculated_order_data['version'] );
1338 - }
1339 -
1340 - // Update net_amounts if provided in response.
1341 - if ( isset( $calculated_order_data['net_amounts'] ) && isset( $calculated_order_data['net_amounts']['total_money'] ) ) {
1342 - $net_amounts = new \Square\Models\OrderMoneyAmounts();
1343 - $net_total = new \Square\Models\Money();
1344 - if ( isset( $calculated_order_data['net_amounts']['total_money']['amount'] ) ) {
1345 - $net_total->setAmount( $calculated_order_data['net_amounts']['total_money']['amount'] );
1346 - }
1347 - if ( isset( $calculated_order_data['net_amounts']['total_money']['currency'] ) ) {
1348 - $net_total->setCurrency( $calculated_order_data['net_amounts']['total_money']['currency'] );
1349 - } else {
1350 - $net_total->setCurrency( $this->get_square_order_currency_or_store_default( $square_order ) );
1351 - }
1352 - $net_amounts->setTotalMoney( $net_total );
1353 - $square_order->setNetAmounts( $net_amounts );
1354 - }
1355 -
1356 - // Set response data for the response handler.
1357 - // The response handler expects raw_response_body to contain the result.
1358 - $this->raw_response_body = $square_order;
1359 - $this->response_code = 200;
1360 -
1361 - // Return a mock response object to satisfy the response handling flow.
1362 - // The actual response data is already set in raw_response_body above.
1363 - $mock_response = new \stdClass();
1364 - $mock_response->result = $square_order;
1365 - $mock_response->statusCode = 200; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
1366 - $mock_response->isSuccess = true; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
1367 -
1368 - return $mock_response;
1369 - }
1370 -
1371 - // Standard SDK method handling for all other API methods.
1372 848 if ( ! is_callable( array( $square_api, $method ) ) ) {
1373 849 throw new \Exception( 'Invalid API method' );
1374 850 }
1375 851
@@ -1399,5 +875,7 @@
1399 875 public function get_plugin() {
1400 876
1401 877 return wc_square();
1402 878 }
879 +
880 +
1403 881 }