| @@ -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 | } |