diff --git a/.github/workflows/update-feedback-labels.yml b/.github/workflows/update-feedback-labels.yml index 2c311f0195a..bb0af46f64b 100644 --- a/.github/workflows/update-feedback-labels.yml +++ b/.github/workflows/update-feedback-labels.yml @@ -15,10 +15,13 @@ jobs: with: github_token: ${{ secrets.GITHUB_TOKEN }} labels: 'has feedback' - - name: remove needs feedback, stale + - name: remove needs feedback uses: actions-ecosystem/action-remove-labels@v1 with: github_token: ${{ secrets.GITHUB_TOKEN }} - labels: | - 'needs feedback' - 'Stale' + labels: 'needs feedback' + - name: remove stale + uses: actions-ecosystem/action-remove-labels@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + labels: Stale diff --git a/assets/css/admin.scss b/assets/css/admin.scss index 3dca45bdf8c..6ea357b8e2a 100644 --- a/assets/css/admin.scss +++ b/assets/css/admin.scss @@ -161,6 +161,43 @@ } } + .addons-promotion-block { + display: flex; + padding: 20px; + + .addons-img { + height: auto; + width: 200px; + } + } + + .addons-promotion-block-content { + display: flex; + flex-direction: column; + margin-left: 24px; + } + + .addons-promotion-block-description { + margin-bottom: 20px; + } + + .addons-promotion-block-title { + margin: 0 0 16px; + padding: 0; + } + + .addons-promotion-block-buttons { + margin-top: auto; + + .addons-button { + margin-right: 8px; + + &:last-child { + margin-right: 0; + } + } + } + .addons-shipping-methods .addons-wcs-banner-block { margin-left: 0; margin-right: 0; @@ -364,6 +401,12 @@ color: #fff; } + .addons-button-expandable { + display: inline-block; + padding: 0 16px; + width: auto; + } + .addons-button-solid:hover { color: #fff; opacity: 0.8; @@ -2642,6 +2685,30 @@ ul.wc_coupon_list_block { float: right; } } + + .wc_addons_wrap { + + .addons-promotion-block { + flex-direction: column; + padding: 24px; + + .addons-img { + height: auto; + width: 100%; + max-width: 240px; + margin: 0 auto 20px; + } + } + + .addons-promotion-block-content { + display: block; + margin-left: 0; + } + + .addons-promotion-block-title { + margin-top: 4px; + } + } } .column-customer_message .note-on { diff --git a/bin/composer/mozart/composer.lock b/bin/composer/mozart/composer.lock index 6e9a52a1179..81ac1cea0fc 100644 --- a/bin/composer/mozart/composer.lock +++ b/bin/composer/mozart/composer.lock @@ -267,16 +267,16 @@ }, { "name": "symfony/console", - "version": "v5.2.6", + "version": "v5.2.8", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "35f039df40a3b335ebf310f244cb242b3a83ac8d" + "reference": "864568fdc0208b3eba3638b6000b69d2386e6768" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/35f039df40a3b335ebf310f244cb242b3a83ac8d", - "reference": "35f039df40a3b335ebf310f244cb242b3a83ac8d", + "url": "https://api.github.com/repos/symfony/console/zipball/864568fdc0208b3eba3638b6000b69d2386e6768", + "reference": "864568fdc0208b3eba3638b6000b69d2386e6768", "shasum": "" }, "require": { @@ -344,7 +344,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.2.6" + "source": "https://github.com/symfony/console/tree/v5.2.8" }, "funding": [ { @@ -360,20 +360,20 @@ "type": "tidelift" } ], - "time": "2021-03-28T09:42:18+00:00" + "time": "2021-05-11T15:45:21+00:00" }, { "name": "symfony/finder", - "version": "v5.2.4", + "version": "v5.2.8", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "0d639a0943822626290d169965804f79400e6a04" + "reference": "eccb8be70d7a6a2230d05f6ecede40f3fdd9e252" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/0d639a0943822626290d169965804f79400e6a04", - "reference": "0d639a0943822626290d169965804f79400e6a04", + "url": "https://api.github.com/repos/symfony/finder/zipball/eccb8be70d7a6a2230d05f6ecede40f3fdd9e252", + "reference": "eccb8be70d7a6a2230d05f6ecede40f3fdd9e252", "shasum": "" }, "require": { @@ -405,7 +405,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v5.2.4" + "source": "https://github.com/symfony/finder/tree/v5.2.8" }, "funding": [ { @@ -421,7 +421,7 @@ "type": "tidelift" } ], - "time": "2021-02-15T18:55:04+00:00" + "time": "2021-05-10T14:39:23+00:00" }, { "name": "symfony/polyfill-ctype", @@ -911,21 +911,21 @@ }, { "name": "symfony/service-contracts", - "version": "v2.2.0", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1" + "reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d15da7ba4957ffb8f1747218be9e1a121fd298a1", - "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb", + "reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb", "shasum": "" }, "require": { "php": ">=7.2.5", - "psr/container": "^1.0" + "psr/container": "^1.1" }, "suggest": { "symfony/service-implementation": "" @@ -933,7 +933,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2-dev" + "dev-main": "2.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -970,7 +970,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/master" + "source": "https://github.com/symfony/service-contracts/tree/v2.4.0" }, "funding": [ { @@ -986,20 +986,20 @@ "type": "tidelift" } ], - "time": "2020-09-07T11:33:47+00:00" + "time": "2021-04-01T10:43:52+00:00" }, { "name": "symfony/string", - "version": "v5.2.6", + "version": "v5.2.8", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572" + "reference": "01b35eb64cac8467c3f94cd0ce2d0d376bb7d1db" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572", - "reference": "ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572", + "url": "https://api.github.com/repos/symfony/string/zipball/01b35eb64cac8467c3f94cd0ce2d0d376bb7d1db", + "reference": "01b35eb64cac8467c3f94cd0ce2d0d376bb7d1db", "shasum": "" }, "require": { @@ -1053,7 +1053,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.2.6" + "source": "https://github.com/symfony/string/tree/v5.2.8" }, "funding": [ { @@ -1069,7 +1069,7 @@ "type": "tidelift" } ], - "time": "2021-03-17T17:12:15+00:00" + "time": "2021-05-10T14:56:10+00:00" } ], "aliases": [], diff --git a/bin/composer/phpcs/composer.lock b/bin/composer/phpcs/composer.lock index d784a60a0a0..e0653d55467 100644 --- a/bin/composer/phpcs/composer.lock +++ b/bin/composer/phpcs/composer.lock @@ -251,16 +251,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.5.8", + "version": "3.6.0", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "9d583721a7157ee997f235f327de038e7ea6dac4" + "reference": "ffced0d2c8fa8e6cdc4d695a743271fab6c38625" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/9d583721a7157ee997f235f327de038e7ea6dac4", - "reference": "9d583721a7157ee997f235f327de038e7ea6dac4", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ffced0d2c8fa8e6cdc4d695a743271fab6c38625", + "reference": "ffced0d2c8fa8e6cdc4d695a743271fab6c38625", "shasum": "" }, "require": { @@ -303,7 +303,7 @@ "source": "https://github.com/squizlabs/PHP_CodeSniffer", "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" }, - "time": "2020-10-23T02:01:07+00:00" + "time": "2021-04-09T00:54:41+00:00" }, { "name": "woocommerce/woocommerce-sniffs", diff --git a/bin/composer/wp/composer.lock b/bin/composer/wp/composer.lock index 8fb33475e13..3532eb3520e 100644 --- a/bin/composer/wp/composer.lock +++ b/bin/composer/wp/composer.lock @@ -254,23 +254,30 @@ }, { "name": "rmccue/requests", - "version": "v1.7.0", + "version": "v1.8.0", "source": { "type": "git", - "url": "https://github.com/rmccue/Requests.git", - "reference": "87932f52ffad70504d93f04f15690cf16a089546" + "url": "https://github.com/WordPress/Requests.git", + "reference": "afbe4790e4def03581c4a0963a1e8aa01f6030f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rmccue/Requests/zipball/87932f52ffad70504d93f04f15690cf16a089546", - "reference": "87932f52ffad70504d93f04f15690cf16a089546", + "url": "https://api.github.com/repos/WordPress/Requests/zipball/afbe4790e4def03581c4a0963a1e8aa01f6030f1", + "reference": "afbe4790e4def03581c4a0963a1e8aa01f6030f1", "shasum": "" }, "require": { "php": ">=5.2" }, "require-dev": { - "requests/test-server": "dev-master" + "dealerdirect/phpcodesniffer-composer-installer": "^0.7", + "php-parallel-lint/php-console-highlighter": "^0.5.0", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpcompatibility/php-compatibility": "^9.0", + "phpunit/phpunit": "^4.8 || ^5.7 || ^6.5 || ^7.5", + "requests/test-server": "dev-master", + "squizlabs/php_codesniffer": "^3.5", + "wp-coding-standards/wpcs": "^2.0" }, "type": "library", "autoload": { @@ -289,7 +296,7 @@ } ], "description": "A HTTP library written in PHP, for human beings.", - "homepage": "http://github.com/rmccue/Requests", + "homepage": "http://github.com/WordPress/Requests", "keywords": [ "curl", "fsockopen", @@ -300,10 +307,10 @@ "sockets" ], "support": { - "issues": "https://github.com/rmccue/Requests/issues", - "source": "https://github.com/rmccue/Requests/tree/master" + "issues": "https://github.com/WordPress/Requests/issues", + "source": "https://github.com/WordPress/Requests/tree/v1.8.0" }, - "time": "2016-10-13T00:11:37+00:00" + "time": "2021-04-27T11:05:25+00:00" }, { "name": "symfony/finder", @@ -359,26 +366,26 @@ }, { "name": "wp-cli/i18n-command", - "version": "v2.2.6", + "version": "v2.2.8", "source": { "type": "git", "url": "https://github.com/wp-cli/i18n-command.git", - "reference": "a66da3f09f6a728832381012848c3074bf1635c8" + "reference": "8bc234617edc533590ac0f41080164a8d85ec9ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wp-cli/i18n-command/zipball/a66da3f09f6a728832381012848c3074bf1635c8", - "reference": "a66da3f09f6a728832381012848c3074bf1635c8", + "url": "https://api.github.com/repos/wp-cli/i18n-command/zipball/8bc234617edc533590ac0f41080164a8d85ec9ce", + "reference": "8bc234617edc533590ac0f41080164a8d85ec9ce", "shasum": "" }, "require": { "gettext/gettext": "^4.8", "mck89/peast": "^1.8", - "wp-cli/wp-cli": "^2" + "wp-cli/wp-cli": "^2.5" }, "require-dev": { "wp-cli/scaffold-command": "^1.2 || ^2", - "wp-cli/wp-cli-tests": "^2.1.3" + "wp-cli/wp-cli-tests": "^3.0.11" }, "type": "wp-cli-package", "extra": { @@ -414,9 +421,9 @@ "homepage": "https://github.com/wp-cli/i18n-command", "support": { "issues": "https://github.com/wp-cli/i18n-command/issues", - "source": "https://github.com/wp-cli/i18n-command/tree/v2.2.6" + "source": "https://github.com/wp-cli/i18n-command/tree/v2.2.8" }, - "time": "2020-12-07T19:28:27+00:00" + "time": "2021-05-10T10:24:16+00:00" }, { "name": "wp-cli/mustangostang-spyc", @@ -525,23 +532,23 @@ }, { "name": "wp-cli/wp-cli", - "version": "v2.4.1", + "version": "dev-master", "source": { "type": "git", "url": "https://github.com/wp-cli/wp-cli.git", - "reference": "ceb18598e79befa9b2a37a51efbb34910628988b" + "reference": "4c4746d06640af7698f3792cc4c327a8482dc40f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wp-cli/wp-cli/zipball/ceb18598e79befa9b2a37a51efbb34910628988b", - "reference": "ceb18598e79befa9b2a37a51efbb34910628988b", + "url": "https://api.github.com/repos/wp-cli/wp-cli/zipball/4c4746d06640af7698f3792cc4c327a8482dc40f", + "reference": "4c4746d06640af7698f3792cc4c327a8482dc40f", "shasum": "" }, "require": { "ext-curl": "*", "mustache/mustache": "~2.13", - "php": "^5.4 || ^7.0", - "rmccue/requests": "~1.6", + "php": "^5.6 || ^7.0 || ^8.0", + "rmccue/requests": "^1.8", "symfony/finder": ">2.7", "wp-cli/mustangostang-spyc": "^0.6.3", "wp-cli/php-cli-tools": "~0.11.2" @@ -552,12 +559,13 @@ "wp-cli/entity-command": "^1.2 || ^2", "wp-cli/extension-command": "^1.1 || ^2", "wp-cli/package-command": "^1 || ^2", - "wp-cli/wp-cli-tests": "^2.1" + "wp-cli/wp-cli-tests": "^3.0.7" }, "suggest": { "ext-readline": "Include for a better --prompt implementation", "ext-zip": "Needed to support extraction of ZIP archives when doing downloads or updates" }, + "default-branch": true, "bin": [ "bin/wp", "bin/wp.bat" @@ -565,13 +573,17 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.4.x-dev" + "dev-master": "2.5.x-dev" } }, "autoload": { "psr-0": { - "WP_CLI": "php" - } + "WP_CLI\\": "php/" + }, + "classmap": [ + "php/class-wp-cli.php", + "php/class-wp-cli-command.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -588,7 +600,7 @@ "issues": "https://github.com/wp-cli/wp-cli/issues", "source": "https://github.com/wp-cli/wp-cli" }, - "time": "2020-02-18T08:15:37+00:00" + "time": "2021-05-13T09:36:33+00:00" } ], "aliases": [], diff --git a/composer.json b/composer.json index 462458e7920..34a1dc6a246 100644 --- a/composer.json +++ b/composer.json @@ -21,8 +21,8 @@ "pelago/emogrifier": "3.1.0", "psr/container": "1.0.0", "woocommerce/action-scheduler": "3.1.6", - "woocommerce/woocommerce-admin": "2.2.6", - "woocommerce/woocommerce-blocks": "4.9.1" + "woocommerce/woocommerce-admin": "2.3.0", + "woocommerce/woocommerce-blocks": "5.1.0" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.4" diff --git a/composer.lock b/composer.lock index 3f506ced4f6..966a06ec993 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a604678b268820c78736d599e1eb6726", + "content-hash": "ac338dadb8929c73ad9606426621f9ba", "packages": [ { "name": "automattic/jetpack-autoloader", @@ -51,6 +51,9 @@ "GPL-2.0-or-later" ], "description": "Creates a custom autoloader for a plugin or theme.", + "support": { + "source": "https://github.com/Automattic/jetpack-autoloader/tree/2.10.1" + }, "time": "2021-03-30T15:15:59+00:00" }, { @@ -82,6 +85,9 @@ "GPL-2.0-or-later" ], "description": "A wrapper for defining constants in a more testable way.", + "support": { + "source": "https://github.com/Automattic/jetpack-constants/tree/v1.5.1" + }, "time": "2020-10-28T19:00:31+00:00" }, { @@ -214,6 +220,10 @@ "zend", "zikula" ], + "support": { + "issues": "https://github.com/composer/installers/issues", + "source": "https://github.com/composer/installers/tree/v1.11.0" + }, "funding": [ { "url": "https://packagist.com", @@ -288,6 +298,10 @@ "geolocation", "maxmind" ], + "support": { + "issues": "https://github.com/maxmind/MaxMind-DB-Reader-php/issues", + "source": "https://github.com/maxmind/MaxMind-DB-Reader-php/tree/v1.6.0" + }, "time": "2019-12-19T22:59:03+00:00" }, { @@ -362,6 +376,10 @@ "email", "pre-processing" ], + "support": { + "issues": "https://github.com/MyIntervals/emogrifier/issues", + "source": "https://github.com/MyIntervals/emogrifier" + }, "time": "2019-12-26T19:37:31+00:00" }, { @@ -411,6 +429,10 @@ "container-interop", "psr" ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/master" + }, "time": "2017-02-14T16:28:37+00:00" }, { @@ -464,6 +486,9 @@ ], "description": "Symfony CssSelector Component", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/css-selector/tree/master" + }, "time": "2017-05-01T15:01:29+00:00" }, { @@ -499,20 +524,24 @@ ], "description": "Action Scheduler for WordPress and WooCommerce", "homepage": "https://actionscheduler.org/", + "support": { + "issues": "https://github.com/woocommerce/action-scheduler/issues", + "source": "https://github.com/woocommerce/action-scheduler/tree/master" + }, "time": "2020-05-12T16:22:33+00:00" }, { "name": "woocommerce/woocommerce-admin", - "version": "2.2.6", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/woocommerce/woocommerce-admin.git", - "reference": "161e6afa01a3fb69533cfa2b245a71df7512ec3f" + "reference": "c690969d301ddb7145b43e72e4dd99c84cc8ba66" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/woocommerce/woocommerce-admin/zipball/65c5a4d0b0c3a7c7457e250c9ba1f7c15cac6cb8", - "reference": "65c5a4d0b0c3a7c7457e250c9ba1f7c15cac6cb8", + "url": "https://api.github.com/repos/woocommerce/woocommerce-admin/zipball/c690969d301ddb7145b43e72e4dd99c84cc8ba66", + "reference": "c690969d301ddb7145b43e72e4dd99c84cc8ba66", "shasum": "" }, "require": { @@ -521,7 +550,7 @@ "php": ">=7.0" }, "require-dev": { - "phpunit/phpunit": "7.5.20", + "bamarni/composer-bin-plugin": "^1.4", "suin/phpcs-psr4-sniff": "^2.2", "woocommerce/woocommerce-sniffs": "0.1.0" }, @@ -531,6 +560,9 @@ "test": "Run unit tests", "phpcs": "Analyze code against the WordPress coding standards with PHP_CodeSniffer", "phpcbf": "Fix coding standards warnings/errors automatically with PHP Code Beautifier" + }, + "bamarni-bin": { + "target-directory": "bin/composer" } }, "autoload": { @@ -544,20 +576,24 @@ ], "description": "A modern, javascript-driven WooCommerce Admin experience.", "homepage": "https://github.com/woocommerce/woocommerce-admin", - "time": "2021-04-29T14:11:48+00:00" + "support": { + "issues": "https://github.com/woocommerce/woocommerce-admin/issues", + "source": "https://github.com/woocommerce/woocommerce-admin/tree/v2.3.0" + }, + "time": "2021-05-12T15:20:07+00:00" }, { "name": "woocommerce/woocommerce-blocks", - "version": "v4.9.1", + "version": "v5.1.0", "source": { "type": "git", "url": "https://github.com/woocommerce/woocommerce-gutenberg-products-block.git", - "reference": "62f32bfb45dfcb2ba3ca349a6ed0a8cf48ddefce" + "reference": "a4f168596f3832e161b26dec636b69293039ee51" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/woocommerce/woocommerce-gutenberg-products-block/zipball/62f32bfb45dfcb2ba3ca349a6ed0a8cf48ddefce", - "reference": "62f32bfb45dfcb2ba3ca349a6ed0a8cf48ddefce", + "url": "https://api.github.com/repos/woocommerce/woocommerce-gutenberg-products-block/zipball/a4f168596f3832e161b26dec636b69293039ee51", + "reference": "a4f168596f3832e161b26dec636b69293039ee51", "shasum": "" }, "require": { @@ -565,7 +601,7 @@ "composer/installers": "^1.7.0" }, "require-dev": { - "phpunit/phpunit": "6.5.14", + "phpunit/phpunit": "7.5.20", "woocommerce/woocommerce-sniffs": "0.1.0" }, "type": "wordpress-plugin", @@ -591,7 +627,11 @@ "gutenberg", "woocommerce" ], - "time": "2021-04-13T16:11:16+00:00" + "support": { + "issues": "https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues", + "source": "https://github.com/woocommerce/woocommerce-gutenberg-products-block/tree/v5.1.0" + }, + "time": "2021-05-10T15:01:42+00:00" } ], "packages-dev": [ @@ -639,6 +679,10 @@ "isolation", "tool" ], + "support": { + "issues": "https://github.com/bamarni/composer-bin-plugin/issues", + "source": "https://github.com/bamarni/composer-bin-plugin/tree/master" + }, "time": "2020-05-03T08:27:20+00:00" } ], @@ -654,5 +698,5 @@ "platform-overrides": { "php": "7.0" }, - "plugin-api-version": "1.1.0" + "plugin-api-version": "2.0.0" } diff --git a/includes/admin/class-wc-admin-addons.php b/includes/admin/class-wc-admin-addons.php index ed3a0461368..ff33de129e0 100644 --- a/includes/admin/class-wc-admin-addons.php +++ b/includes/admin/class-wc-admin-addons.php @@ -534,6 +534,73 @@ class WC_Admin_Addons { geowhitelist ) ) { + $section_object->geowhitelist = explode( ',', $section_object->geowhitelist ); + } + + if ( ! empty( $section_object->geoblacklist ) ) { + $section_object->geoblacklist = explode( ',', $section_object->geoblacklist ); + } + + if ( ! self::show_extension( $section_object ) ) { + return; + } + + ?> +
+ <?php echo esc_attr( $section['image_alt'] ); ?> +
+

+
+ +
+
+ +
+
+
+ data['name'] || ! isset( $response->data['actions'] ) ) { - return $response; - } - - foreach ( $response->data['actions'] as $action_key => $action ) { - $url_parts = ! empty( $action->query ) ? wp_parse_url( $action->query ) : ''; - - if ( ! isset( $url_parts['query'] ) ) { - continue; - } - - wp_parse_str( $url_parts['query'], $params ); - - if ( array_key_exists( '_nonce_action', $params ) && array_key_exists( '_nonce_name', $params ) ) { - $org_params = $params; - - // Check to make sure we're acting on the whitelisted nonce actions. - if ( 'wc_db_update' !== $params['_nonce_action'] && 'woocommerce_hide_notices_nonce' !== $params['_nonce_action'] ) { - continue; - } - - unset( $org_params['_nonce_action'] ); - unset( $org_params['_nonce_name'] ); - - $url = $url_parts['scheme'] . '://' . $url_parts['host'] . $url_parts['path']; - - $nonce = array( $params['_nonce_name'] => wp_create_nonce( $params['_nonce_action'] ) ); - $merged_params = array_merge( $nonce, $org_params ); - $parsed_query = add_query_arg( $merged_params, $url ); - - $response->data['actions'][ $action_key ]->query = $parsed_query; - $response->data['actions'][ $action_key ]->url = $parsed_query; - } - } + wc_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '5.4.0' ); return $response; } diff --git a/includes/admin/notes/class-wc-notes-run-db-update.php b/includes/admin/notes/class-wc-notes-run-db-update.php index 1fad100ff9e..77017ec2ca2 100644 --- a/includes/admin/notes/class-wc-notes-run-db-update.php +++ b/includes/admin/notes/class-wc-notes-run-db-update.php @@ -109,24 +109,23 @@ class WC_Notes_Run_Db_Update { * @return int Created/Updated note id */ private static function update_needed_notice( $note_id = null ) { - $update_url = html_entity_decode( + $update_url = add_query_arg( array( 'do_update_woocommerce' => 'true', - '_nonce_action' => 'wc_db_update', - '_nonce_name' => 'wc_db_update_nonce', ), wc_get_current_admin_url() ? wc_get_current_admin_url() : admin_url( 'admin.php?page=wc-settings' ) - ) - ); + ); $note_actions = array( array( - 'name' => 'update-db_run', - 'label' => __( 'Update WooCommerce Database', 'woocommerce' ), - 'url' => $update_url, - 'status' => 'unactioned', - 'primary' => true, + 'name' => 'update-db_run', + 'label' => __( 'Update WooCommerce Database', 'woocommerce' ), + 'url' => $update_url, + 'status' => 'unactioned', + 'primary' => true, + 'nonce_action' => 'wc_db_update', + 'nonce_name' => 'wc_db_update_nonce', ), array( 'name' => 'update-db_learn-more', @@ -166,6 +165,10 @@ class WC_Notes_Run_Db_Update { $note->clear_actions(); foreach ( $note_actions as $note_action ) { $note->add_action( ...array_values( $note_action ) ); + + if ( isset( $note_action['nonce_action'] ) ) { + $note->add_nonce_to_action( $note_action['name'], $note_action['nonce_action'], $note_action['nonce_name'] ); + } } return $note->save(); @@ -212,8 +215,6 @@ class WC_Notes_Run_Db_Update { add_query_arg( array( 'wc-hide-notice' => 'update', - '_nonce_action' => 'woocommerce_hide_notices_nonce', - '_nonce_name' => '_wc_notice_nonce', ), wc_get_current_admin_url() ? remove_query_arg( 'do_update_woocommerce', wc_get_current_admin_url() ) : admin_url( 'admin.php?page=wc-settings' ) ) @@ -221,11 +222,13 @@ class WC_Notes_Run_Db_Update { $note_actions = array( array( - 'name' => 'update-db_done', - 'label' => __( 'Thanks!', 'woocommerce' ), - 'url' => $hide_notices_url, - 'status' => 'actioned', - 'primary' => true, + 'name' => 'update-db_done', + 'label' => __( 'Thanks!', 'woocommerce' ), + 'url' => $hide_notices_url, + 'status' => 'actioned', + 'primary' => true, + 'nonce_action' => 'woocommerce_hide_notices_nonce', + 'nonce_name' => '_wc_notice_nonce', ), ); @@ -242,6 +245,10 @@ class WC_Notes_Run_Db_Update { $note->clear_actions(); foreach ( $note_actions as $note_action ) { $note->add_action( ...array_values( $note_action ) ); + + if ( isset( $note_action['nonce_action'] ) ) { + $note->add_nonce_to_action( $note_action['name'], $note_action['nonce_action'], $note_action['nonce_name'] ); + } } $note->save(); diff --git a/includes/class-wc-cache-helper.php b/includes/class-wc-cache-helper.php index f07fca63eb0..4a9c0604eab 100644 --- a/includes/class-wc-cache-helper.php +++ b/includes/class-wc-cache-helper.php @@ -42,14 +42,33 @@ class WC_Cache_Helper { */ public static function additional_nocache_headers( $headers ) { $agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + + $set_cache = false; + /** * Allow plugins to enable nocache headers. Enabled for Google weblight. * - * @see https://support.google.com/webmasters/answer/1061943?hl=en * @param bool $enable_nocache_headers Flag indicating whether to add nocache headers. Default: false. */ - if ( false !== strpos( $agent, 'googleweblight' ) || apply_filters( 'woocommerce_enable_nocache_headers', false ) ) { + if ( apply_filters( 'woocommerce_enable_nocache_headers', false ) ) { + $set_cache = true; + } + + /** + * Enabled for Google weblight. + * + * @see https://support.google.com/webmasters/answer/1061943?hl=en + */ + if ( false !== strpos( $agent, 'googleweblight' ) ) { // no-transform: Opt-out of Google weblight. https://support.google.com/webmasters/answer/6211428?hl=en. + $set_cache = true; + } + + if ( false !== strpos( $agent, 'Chrome' ) && is_cart() ) { + $set_cache = true; + } + + if ( $set_cache ) { $headers['Cache-Control'] = 'no-transform, no-cache, no-store, must-revalidate'; } return $headers; diff --git a/includes/class-wc-checkout.php b/includes/class-wc-checkout.php index 036c4ba612c..3133be81505 100644 --- a/includes/class-wc-checkout.php +++ b/includes/class-wc-checkout.php @@ -679,6 +679,8 @@ class WC_Checkout { // phpcs:enable WordPress.Security.NonceVerification.Missing $skipped = array(); + $form_was_shown = isset( $_POST['woocommerce-process-checkout-nonce'] ); // phpcs:disable WordPress.Security.NonceVerification.Missing + foreach ( $this->get_checkout_fields() as $fieldset_key => $fieldset ) { if ( $this->maybe_skip_fieldset( $fieldset_key, $data ) ) { $skipped[] = $fieldset_key; @@ -688,25 +690,32 @@ class WC_Checkout { foreach ( $fieldset as $key => $field ) { $type = sanitize_title( isset( $field['type'] ) ? $field['type'] : 'text' ); - // phpcs:disable WordPress.Security.NonceVerification.Missing - switch ( $type ) { - case 'checkbox': - $value = isset( $_POST[ $key ] ) ? 1 : ''; - break; - case 'multiselect': - $value = isset( $_POST[ $key ] ) ? implode( ', ', wc_clean( wp_unslash( $_POST[ $key ] ) ) ) : ''; - break; - case 'textarea': - $value = isset( $_POST[ $key ] ) ? wc_sanitize_textarea( wp_unslash( $_POST[ $key ] ) ) : ''; - break; - case 'password': - $value = isset( $_POST[ $key ] ) ? wp_unslash( $_POST[ $key ] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - break; - default: - $value = isset( $_POST[ $key ] ) ? wc_clean( wp_unslash( $_POST[ $key ] ) ) : ''; - break; + if ( isset( $_POST[ $key ] ) && '' !== $_POST[ $key ] ) { // phpcs:disable WordPress.Security.NonceVerification.Missing + $value = wp_unslash( $_POST[ $key ] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + } elseif ( isset( $field['default'] ) && 'checkbox' !== $type && ! $form_was_shown ) { + $value = $field['default']; + } else { + $value = ''; + } + + if ( '' !== $value ) { + switch ( $type ) { + case 'checkbox': + $value = 1; + break; + case 'multiselect': + $value = implode( ', ', wc_clean( $value ) ); + break; + case 'textarea': + $value = wc_sanitize_textarea( $value ); + break; + case 'password': + break; + default: + $value = wc_clean( $value ); + break; + } } - // phpcs:enable WordPress.Security.NonceVerification.Missing $data[ $key ] = apply_filters( 'woocommerce_process_checkout_' . $type . '_field', apply_filters( 'woocommerce_process_checkout_field_' . $key, $value ) ); } @@ -1123,7 +1132,7 @@ class WC_Checkout { */ public function process_checkout() { try { - $nonce_value = wc_get_var( $_REQUEST['woocommerce-process-checkout-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine. + $nonce_value = wc_get_var( $_REQUEST['woocommerce-process-checkout-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // phpcs:ignore if ( empty( $nonce_value ) || ! wp_verify_nonce( $nonce_value, 'woocommerce-process_checkout' ) ) { WC()->session->set( 'refresh_totals', true ); diff --git a/includes/class-wc-countries.php b/includes/class-wc-countries.php index 064f035cabc..e20d1358b27 100644 --- a/includes/class-wc-countries.php +++ b/includes/class-wc-countries.php @@ -365,7 +365,7 @@ class WC_Countries { * @return string[] */ public function get_european_union_countries( $type = '' ) { - $countries = array( 'AT', 'BE', 'BG', 'CY', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FI', 'FR', 'GR', 'HU', 'HR', 'IE', 'IT', 'LT', 'LU', 'LV', 'MT', 'NL', 'PL', 'PT', 'RO', 'SE', 'SI', 'SK' ); + $countries = array( 'AT', 'BE', 'BG', 'CY', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FI', 'FR', 'GR', 'HR', 'HU', 'IE', 'IT', 'LT', 'LU', 'LV', 'MT', 'NL', 'PL', 'PT', 'RO', 'SE', 'SI', 'SK' ); if ( 'eu_vat' === $type ) { $countries[] = 'MC'; @@ -383,7 +383,7 @@ class WC_Countries { */ public function countries_using_vat() { wc_deprecated_function( 'countries_using_vat', '4.0', 'WC_Countries::get_vat_countries' ); - $countries = array( 'AL', 'AR', 'AZ', 'BS', 'BH', 'BY', 'BB', 'BO', 'EG', 'ET', 'CL', 'CO', 'EC', 'SV', 'FJ', 'GM', 'GH', 'GT', 'IN', 'IR', 'IL', 'KZ', 'MU', 'MK', 'MX', 'MD', 'MN', 'ME', 'NA', 'NP', 'NG', 'PS', 'PY', 'RU', 'RW', 'KN', 'SA', 'RS', 'ZA', 'KR', 'LK', 'TH', 'TR', 'UA', 'UY', 'UZ', 'VE', 'VN', 'AE' ); + $countries = array( 'AE', 'AL', 'AR', 'AZ', 'BB', 'BH', 'BO', 'BS', 'BY', 'CL', 'CO', 'EC', 'EG', 'ET', 'FJ', 'GH', 'GM', 'GT', 'IL', 'IN', 'IR', 'KN', 'KR', 'KZ', 'LK', 'MD', 'ME', 'MK', 'MN', 'MU', 'MX', 'NA', 'NG', 'NP', 'PS', 'PY', 'RS', 'RU', 'RW', 'SA', 'SV', 'TH', 'TR', 'UA', 'UY', 'UZ', 'VE', 'VN', 'ZA' ); return apply_filters( 'woocommerce_countries_using_vat', $countries ); } @@ -409,7 +409,7 @@ class WC_Countries { */ public function shipping_to_prefix( $country_code = '' ) { $country_code = $country_code ? $country_code : WC()->customer->get_shipping_country(); - $countries = array( 'GB', 'US', 'AE', 'CZ', 'DO', 'NL', 'PH', 'USAF' ); + $countries = array( 'AE', 'CZ', 'DO', 'GB', 'NL', 'PH', 'US', 'USAF' ); $return = in_array( $country_code, $countries, true ) ? __( 'to the', 'woocommerce' ) : __( 'to', 'woocommerce' ); return apply_filters( 'woocommerce_countries_shipping_to_prefix', $return, $country_code ); @@ -423,7 +423,7 @@ class WC_Countries { */ public function estimated_for_prefix( $country_code = '' ) { $country_code = $country_code ? $country_code : $this->get_base_country(); - $countries = array( 'GB', 'US', 'AE', 'CZ', 'DO', 'NL', 'PH', 'USAF' ); + $countries = array( 'AE', 'CZ', 'DO', 'GB', 'NL', 'PH', 'US', 'USAF' ); $return = in_array( $country_code, $countries, true ) ? __( 'the', 'woocommerce' ) . ' ' : ''; return apply_filters( 'woocommerce_countries_estimated_for_prefix', $return, $country_code ); @@ -510,8 +510,8 @@ class WC_Countries { 'woocommerce_localisation_address_formats', array( 'default' => "{name}\n{company}\n{address_1}\n{address_2}\n{city}\n{state}\n{postcode}\n{country}", - 'AU' => "{name}\n{company}\n{address_1}\n{address_2}\n{city} {state} {postcode}\n{country}", 'AT' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'AU' => "{name}\n{company}\n{address_1}\n{address_2}\n{city} {state} {postcode}\n{country}", 'BE' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'CA' => "{company}\n{name}\n{address_1}\n{address_2}\n{city} {state_code} {postcode}\n{country}", 'CH' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", @@ -519,9 +519,10 @@ class WC_Countries { 'CN' => "{country} {postcode}\n{state}, {city}, {address_2}, {address_1}\n{company}\n{name}", 'CZ' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'DE' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", - 'EE' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", - 'FI' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'DK' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'EE' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'ES' => "{name}\n{company}\n{address_1}\n{address_2}\n{postcode} {city}\n{state}\n{country}", + 'FI' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'FR' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city_upper}\n{country}", 'HK' => "{company}\n{first_name} {last_name_upper}\n{address_1}\n{address_2}\n{city_upper}\n{state_upper}\n{country}", 'HU' => "{last_name} {first_name}\n{company}\n{city}\n{address_1}\n{address_2}\n{postcode}\n{country}", @@ -530,20 +531,19 @@ class WC_Countries { 'IT' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode}\n{city}\n{state_upper}\n{country}", 'JM' => "{name}\n{company}\n{address_1}\n{address_2}\n{city}\n{state}\n{postcode_upper}\n{country}", 'JP' => "{postcode}\n{state} {city} {address_1}\n{address_2}\n{company}\n{last_name} {first_name}\n{country}", - 'TW' => "{company}\n{last_name} {first_name}\n{address_1}\n{address_2}\n{state}, {city} {postcode}\n{country}", 'LI' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'NL' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", - 'NZ' => "{name}\n{company}\n{address_1}\n{address_2}\n{city} {postcode}\n{country}", 'NO' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'NZ' => "{name}\n{company}\n{address_1}\n{address_2}\n{city} {postcode}\n{country}", 'PL' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'PR' => "{company}\n{name}\n{address_1} {address_2}\n{city} \n{country} {postcode}", 'PT' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", - 'SK' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'RS' => "{name}\n{company}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", - 'SI' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", - 'ES' => "{name}\n{company}\n{address_1}\n{address_2}\n{postcode} {city}\n{state}\n{country}", 'SE' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'SI' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'SK' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'TR' => "{name}\n{company}\n{address_1}\n{address_2}\n{postcode} {city} {state}\n{country}", + 'TW' => "{company}\n{last_name} {first_name}\n{address_1}\n{address_2}\n{state}, {city} {postcode}\n{country}", 'UG' => "{name}\n{company}\n{address_1}\n{address_2}\n{city}\n{state}, {country}", 'US' => "{name}\n{company}\n{address_1}\n{address_2}\n{city}, {state_code} {postcode}\n{country}", 'VN' => "{name}\n{company}\n{address_1}\n{city}\n{country}", diff --git a/includes/class-wc-tax.php b/includes/class-wc-tax.php index bf6956e6639..a59f60cc4de 100644 --- a/includes/class-wc-tax.php +++ b/includes/class-wc-tax.php @@ -82,13 +82,7 @@ class WC_Tax { * @return array */ public static function calc_shipping_tax( $price, $rates ) { - // Backwards compatible from WC_Shipping_Rate::get_cost(). - if ( has_filter( 'woocommerce_shipping_rate_cost' ) ) { - $rate = new WC_Shipping_Rate(); - $price = $rate->get_cost(); - } $taxes = self::calc_exclusive_tax( $price, $rates ); - return apply_filters( 'woocommerce_calc_shipping_tax', $taxes, $price, $rates ); } diff --git a/includes/class-woocommerce.php b/includes/class-woocommerce.php index 8dff19c4d0c..e999e73c24e 100644 --- a/includes/class-woocommerce.php +++ b/includes/class-woocommerce.php @@ -25,7 +25,7 @@ final class WooCommerce { * * @var string */ - public $version = '5.3.0'; + public $version = '5.5.0'; /** * WooCommerce Schema version. @@ -205,7 +205,6 @@ final class WooCommerce { add_action( 'switch_blog', array( $this, 'wpdb_table_fix' ), 0 ); add_action( 'activated_plugin', array( $this, 'activated_plugin' ) ); add_action( 'deactivated_plugin', array( $this, 'deactivated_plugin' ) ); - add_filter( 'woocommerce_rest_prepare_note', array( 'WC_Admin_Notices', 'prepare_note_with_nonce' ) ); add_action( 'woocommerce_installed', array( $this, 'add_woocommerce_inbox_variant' ) ); add_action( 'woocommerce_updated', array( $this, 'add_woocommerce_inbox_variant' ) ); diff --git a/includes/gateways/paypal/includes/settings-paypal.php b/includes/gateways/paypal/includes/settings-paypal.php index d9d56ddaadf..53729429e8e 100644 --- a/includes/gateways/paypal/includes/settings-paypal.php +++ b/includes/gateways/paypal/includes/settings-paypal.php @@ -58,7 +58,7 @@ return array( 'description' => sprintf( __( 'Log PayPal events, such as IPN requests, inside %s Note: this may log personal information. We recommend using this for debugging purposes only and deleting the logs when finished.', 'woocommerce' ), '' . WC_Log_Handler_File::get_log_file_path( 'paypal' ) . '' ), ), 'ipn_notification' => array( - 'title' => __( 'IPN Email Notifications', 'woocommerce' ), + 'title' => __( 'IPN email notifications', 'woocommerce' ), 'type' => 'checkbox', 'label' => __( 'Enable IPN email notifications', 'woocommerce' ), 'default' => 'yes', diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-crud-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-crud-controller.php index 2ebf47a1eab..310bd2367e0 100644 --- a/includes/rest-api/Controllers/Version3/class-wc-rest-crud-controller.php +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-crud-controller.php @@ -303,7 +303,7 @@ abstract class WC_REST_CRUD_Controller extends WC_REST_Posts_Controller { // Check flag to use post_date vs post_date_gmt. if ( true === $request['dates_are_gmt'] ) { if ( isset( $request['before'] ) || isset( $request['after'] ) ) { - $args['date_query']['column'] = 'post_date_gmt'; + $args['date_query'][0]['column'] = 'post_date_gmt'; } } diff --git a/includes/widgets/class-wc-widget-products.php b/includes/widgets/class-wc-widget-products.php index be373488920..4d5b61240a7 100644 --- a/includes/widgets/class-wc-widget-products.php +++ b/includes/widgets/class-wc-widget-products.php @@ -186,6 +186,8 @@ class WC_Widget_Products extends WC_Widget { ob_start(); + wc_set_loop_prop( 'name', 'widget' ); + $products = $this->get_products( $args, $instance ); if ( $products && $products->have_posts() ) { $this->widget_start( $args, $instance ); diff --git a/package-lock.json b/package-lock.json index 0c5d1334562..92b593f6ebb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "woocommerce", - "version": "5.3.0", + "version": "5.5.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -9308,7 +9308,7 @@ } }, "prettier": { - "version": "npm:wp-prettier@1.19.1", + "version": "npm:prettier@1.19.1", "resolved": "https://registry.npmjs.org/wp-prettier/-/wp-prettier-1.19.1.tgz", "integrity": "sha512-mqAC2r1NDmRjG+z3KCJ/i61tycKlmADIjxnDhQab+KBxSAGbF/W7/zwB2guy/ypIeKrrftNsIYkNZZQKf3vJcg==", "dev": true diff --git a/package.json b/package.json index 00559ae31f1..3c9b50a16e1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "woocommerce", "title": "WooCommerce", - "version": "5.4.0", + "version": "5.5.0", "homepage": "https://woocommerce.com/", "repository": { "type": "git", diff --git a/templates/emails/customer-invoice.php b/templates/emails/customer-invoice.php index e0564448390..139512a5b22 100644 --- a/templates/emails/customer-invoice.php +++ b/templates/emails/customer-invoice.php @@ -29,7 +29,7 @@ do_action( 'woocommerce_email_header', $email_heading, $email ); ?>

get_billing_first_name() ) ); ?>

-has_status( 'pending' ) ) { ?> +needs_payment() ) { ?>

{ As a convenience utility we've created repositories for core data types that can simplify interacting with the API: +#### Parent/Base Repositories + - `SimpleProduct` - `ExternalProduct` - `GroupedProduct` - `VariableProduct` -- `ProductVariation` - `Coupon` +- `Order` +- `SettingsGroup` + +#### Child Repositories + +- `ProductVariation` +- `Setting` These repositories provide CRUD methods for ease-of-use: @@ -80,7 +88,7 @@ product.id; #### Repository Methods -The following methods are available on all repositories: +The following methods are available on all repositories if the corresponding method is available on the API endpoint: - `create( {...properties} )` - Create a single object of the model type - `delete( objectId )` - Delete a single object of the model type @@ -90,14 +98,14 @@ The following methods are available on all repositories: #### Child Repositories -`ProductVariation` is a child model repository. In child model repositories, each method requires the `parentId` as the first parameter: +In child model repositories, each method requires the `parentId` as the first parameter: ```javascript import { HTTPClientFactory, VariableProduct, ProductVariation } from '@woocommerce/api'; const httpClient = HTTPClientFactory.build( 'https://example.com' ) .withBasicAuth( 'username', 'password' ) - .withIndexPermalinks() + .withIndexPermalinks() .create(); const productRepository = VariableProduct.restRepository( httpClient ); diff --git a/tests/e2e/api/src/models/coupons/coupon.ts b/tests/e2e/api/src/models/coupons/coupon.ts index a0ee904de29..20e3f42ee9b 100644 --- a/tests/e2e/api/src/models/coupons/coupon.ts +++ b/tests/e2e/api/src/models/coupons/coupon.ts @@ -12,6 +12,7 @@ import { import { CouponUpdateParams, } from './shared'; +import { ObjectLinks } from '../shared-types'; /** * The parameters embedded in this generic can be used in the ModelRepository in order to give @@ -222,6 +223,16 @@ export class Coupon extends Model { */ public readonly usedBy: Array = []; + /** + * The coupon's links. + * + * @type {ReadonlyArray.} + */ + public readonly links: ObjectLinks = { + collection: [ { href: '' } ], + self: [ { href: '' } ], + }; + /** * Creates a new coupon instance with the given properties * diff --git a/tests/e2e/api/src/models/index.ts b/tests/e2e/api/src/models/index.ts index 13ce569f196..902fdec58e3 100644 --- a/tests/e2e/api/src/models/index.ts +++ b/tests/e2e/api/src/models/index.ts @@ -3,3 +3,4 @@ export * from './model'; export * from './settings'; export * from './shared-types'; export * from './coupons'; +export * from './orders'; diff --git a/tests/e2e/api/src/models/orders/index.ts b/tests/e2e/api/src/models/orders/index.ts new file mode 100644 index 00000000000..d4df1e6ca90 --- /dev/null +++ b/tests/e2e/api/src/models/orders/index.ts @@ -0,0 +1,2 @@ +export * from './orders'; +export * from './shared'; diff --git a/tests/e2e/api/src/models/orders/orders.ts b/tests/e2e/api/src/models/orders/orders.ts new file mode 100644 index 00000000000..d23c4ca7611 --- /dev/null +++ b/tests/e2e/api/src/models/orders/orders.ts @@ -0,0 +1,369 @@ +import { HTTPClient } from '../../http'; +import { orderRESTRepository } from '../../repositories'; +import { + ModelRepositoryParams, + CreatesModels, + ListsModels, + ReadsModels, + UpdatesModels, + DeletesModels, +} from '../../framework'; +import { + OrderAddressUpdateParams, + OrderCouponUpdateParams, + OrderDataUpdateParams, + OrderFeeUpdateParams, + OrderLineItemUpdateParams, + OrderRefundUpdateParams, + OrderShippingUpdateParams, + OrderTaxUpdateParams, + OrderTotalUpdateParams, + OrderItemMeta, + OrderAddress, + OrderCouponLine, + OrderFeeLine, + OrderLineItem, + OrderRefundLine, + OrderShippingLine, + OrderStatus, + OrderTaxRate, +} from './shared'; +import { ObjectLinks } from '../shared-types'; + +/** + * The parameters that orders can update. + */ +type OrderUpdateParams = OrderAddressUpdateParams + & OrderCouponUpdateParams + & OrderDataUpdateParams + & OrderFeeUpdateParams + & OrderLineItemUpdateParams + & OrderRefundUpdateParams + & OrderShippingUpdateParams + & OrderTaxUpdateParams + & OrderTotalUpdateParams; + +/** + * The parameters embedded in this generic can be used in the ModelRepository in order to give + * type-safety in an incredibly granular way. + */ +export type OrderRepositoryParams = ModelRepositoryParams< Order, never, never, OrderUpdateParams >; + +/** + * An interface for creating orders using the repository. + * + * @typedef CreatesOrders + * @alias CreatesModels. + */ +export type CreatesOrders = CreatesModels< OrderRepositoryParams >; + +/** + * An interface for reading orders using the repository. + * + * @typedef ReadsOrders + * @alias ReadsModels. + */ +export type ReadsOrders = ReadsModels< OrderRepositoryParams >; + +/** + * An interface for updating orders using the repository. + * + * @typedef UpdatesOrders + * @alias UpdatesModels. + */ +export type UpdatesOrders = UpdatesModels< OrderRepositoryParams >; + +/** + * An interface for listing orders using the repository. + * + * @typedef ListsOrders + * @alias ListsModels. + */ +export type ListsOrders = ListsModels< OrderRepositoryParams >; + +/** + * An interface for deleting orders using the repository. + * + * @typedef DeletesOrders + * @alias DeletesModels. + */ +export type DeletesOrders = DeletesModels< OrderRepositoryParams >; + +/** + * An order object. + */ +export class Order extends OrderItemMeta { + /** + * The parent order id. + * + * @type {number} + */ + public readonly parentId: number = 0; + + /** + * The order status. + * + * @type {string} + */ + public readonly status: OrderStatus = ''; + + /** + * The order currency. + * + * @type {string} + */ + public readonly currency: string = ''; + + /** + * The WC version used to create the order. + * + * @type {string} + */ + public readonly version: string = ''; + + /** + * Flags if the prices include tax. + * + * @type {boolean} + */ + public readonly pricesIncludeTax: boolean = false; + + /** + * The total of the discounts on the order. + * + * @type {string} + */ + public readonly discountTotal: string = ''; + + /** + * The total of the tax on discounts on the order. + * + * @type {string} + */ + public readonly discountTax: string = ''; + + /** + * The total of the shipping on the order. + * + * @type {string} + */ + public readonly shippingTotal: string = ''; + + /** + * The total of the tax on shipping on the order. + * + * @type {string} + */ + public readonly shippingTax: string = ''; + + /** + * The total cart tax on the order. + * + * @type {string} + */ + public readonly cartTax: string = ''; + + /** + * The total for the order including adjustments. + * + * @type {string} + */ + public readonly total: string = ''; + + /** + * The total tax for the order including adjustments. + * + * @type {string} + */ + public readonly totalTax: string = ''; + + /** + * The customer id. + * + * @type {number} + */ + public readonly customerId: number = 0; + + /** + * A unique key assigned to the order. + * + * @type {string} + */ + public readonly orderKey: string = ''; + + /** + * The billing address. + * + * @type {OrderAddress} + */ + public readonly billing: OrderAddress | null = null; + + /** + * The shipping address. + * + * @type {OrderAddress} + */ + public readonly shipping: OrderAddress | null = null; + + /** + * Name of the payment method. + * + * @type {string} + */ + public readonly paymentMethod: string = ''; + + /** + * Title of the payment method + * + * @type {string} + */ + public readonly paymentMethodTitle: string = ''; + + /** + * Payment transaction ID. + * + * @type {string} + */ + public readonly transactionId: string = ''; + + /** + * Customer IP address. + * + * @type {string} + */ + public readonly customerIpAddress: string = ''; + + /** + * Customer web browser user agent. + * + * @type {string} + */ + public readonly customerUserAgent: string = ''; + + /** + * Method used to create the order. + * + * @type {string} + */ + public readonly createdVia: string = ''; + + /** + * Customer note. + * + * @type {string} + */ + public readonly customerNote: string = ''; + + /** + * Date the order was completed. + * + * @type {string} + */ + public readonly dateCompleted: Date | null = null; + + /** + * Date the order was paid. + * + * @type {string} + */ + public readonly datePaid: Date | null = null; + + /** + * Hash of the cart's contents. + * + * @type {string} + */ + public readonly cartHash: string = ''; + + /** + * Number assigned to the order. + * + * @type {string} + */ + public readonly orderNumber: string = ''; + + /** + * Currency symbol for the order. + * + * @type {string} + */ + public readonly currencySymbol: string = ''; + + /** + * The order's paid state. + * + * @type {boolean} + */ + public readonly setPaid: boolean = false; + + /** + * The order's line items. + * + * @type {ReadonlyArray.} + */ + public readonly lineItems: OrderLineItem[] = []; + + /** + * The order's tax rates. + * + * @type {ReadonlyArray.} + */ + public readonly taxLines: OrderTaxRate[] = []; + + /** + * The order's shipping charges. + * + * @type {ReadonlyArray.} + */ + public readonly shippingLines: OrderShippingLine[] = []; + + /** + * The order's fees. + * + * @type {ReadonlyArray.} + */ + public readonly feeLines: OrderFeeLine[] = []; + + /** + * The coupons used on the order. + * + * @type {ReadonlyArray.} + */ + public readonly couponLines: OrderCouponLine[] = []; + + /** + * The refunds to the order. + * + * @type {ReadonlyArray.} + */ + public readonly refunds: OrderRefundLine[] = []; + + /** + * The order's links. + * + * @type {ReadonlyArray.} + */ + public readonly links: ObjectLinks = { + collection: [ { href: '' } ], + self: [ { href: '' } ], + }; + + /** + * Creates a new order instance with the given properties + * + * @param {Object} properties The properties to set in the object. + */ + public constructor( properties?: Partial< Order > ) { + super(); + Object.assign( this, properties ); + } + + /** + * Returns the repository for interacting with this type of model. + * + * @param {HTTPClient} httpClient The client for communicating via HTTP. + */ + public static restRepository( httpClient: HTTPClient ): ReturnType< typeof orderRESTRepository > { + return orderRESTRepository( httpClient ); + } +} diff --git a/tests/e2e/api/src/models/orders/shared/classes.ts b/tests/e2e/api/src/models/orders/shared/classes.ts new file mode 100644 index 00000000000..540c543d4ae --- /dev/null +++ b/tests/e2e/api/src/models/orders/shared/classes.ts @@ -0,0 +1,412 @@ +import { MetaData } from '../../shared-types'; +import { Model } from '../../model'; +import { TaxStatus } from './types'; + +/** + * Order item meta. + */ +export class OrderItemMeta extends Model { + /** + * The meta data the order item. + * + * @type {ReadonlyArray.} + */ + public readonly metaData: MetaData[] = []; +} + +/** + * Order line item tax entry. + */ +export class OrderItemTax extends Model { + /** + * The total tax for this tax rate on this item. + * + * @type {string} + */ + public readonly total: string = ''; + + /** + * The subtotal tax for this tax rate on this item. + * + * @type {string} + */ + public readonly subtotal: string = ''; +} + +/** + * An order address. + */ +export class OrderAddress extends Model { + /** + * The first name of the person in the address. + * + * @type {string} + */ + public readonly firstName: string = ''; + + /** + * The last name of the person in the address. + * + * @type {string} + */ + public readonly lastName: string = ''; + + /** + * The company name of the person in the address. + * + * @type {string} + */ + public readonly companyName: string = ''; + + /** + * The first address line in the address. + * + * @type {string} + */ + public readonly address1: string = ''; + + /** + * The second address line in the address. + * + * @type {string} + */ + public readonly address2: string = ''; + + /** + * The city in the address. + * + * @type {string} + */ + public readonly city: string = ''; + + /** + * The state in the address. + * + * @type {string} + */ + public readonly state: string = ''; + + /** + * The postal code in the address. + * + * @type {string} + */ + public readonly postCode: string = ''; + + /** + * The country code for the address. + * + * @type {string} + */ + public readonly countryCode: string = ''; + + /** + * The email address of the person in the address. + * + * @type {string} + */ + public readonly email: string = ''; + + /** + * The phone number of the person in the address. + * + * @type {string} + */ + public readonly phone: string = ''; +} + +/** + * Order Line Item + */ +export class OrderLineItem extends OrderItemMeta { + /** + * The name of the product. + * + * @type {string} + */ + public readonly name: string = ''; + + /** + * The ID of the product. + * + * @type {number} + */ + public readonly productId: number = -1; + + /** + * The ID of the product variation. + * + * @type {number} + */ + public readonly variationId: number = 0; + + /** + * The quantity of the product. + * + * @type {number} + */ + public readonly quantity: number = -1; + + /** + * The tax class for the product. + * + * @type {string} + */ + public readonly taxClass: string = ''; + + /** + * The subtotal for the product. + * + * @type {string} + */ + public readonly subtotal: string = ''; + + /** + * The subtotal tax for the product. + * + * @type {string} + */ + public readonly subtotalTax: string = ''; + + /** + * The total for the product including adjustments. + * + * @type {string} + */ + public readonly total: string = ''; + + /** + * The total tax for the product including adjustments. + * + * @type {string} + */ + public readonly totalTax: string = ''; + + /** + * The taxes applied to the product. + * + * @type {ReadonlyArray.} + */ + public readonly taxes: OrderItemTax[] = []; + + /** + * The product SKU. + * + * @type {string} + */ + public readonly sku: string = ''; + + /** + * The price of the product. + * + * @type {number} + */ + public readonly price: number = -1; + + /** + * The name of the parent product. + * + * @type {string|null} + */ + public readonly parentName: string | null = null; +} + +/** + * Order Tax Rate + */ +export class OrderTaxRate extends Model { + /** + * The tax rate code. + * + * @type {string} + */ + public readonly rateCode: string = ''; + + /** + * The tax rate id. + * + * @type {number} + */ + public readonly rateId: number = 0; + + /** + * The tax label. + * + * @type {string} + */ + public readonly label: string = ''; + + /** + * Flag indicating whether it's a compound tax rate. + * + * @type {boolean} + */ + public readonly compoundRate: boolean = false; + + /** + * The total tax for this rate code. + * + * @type {string} + */ + public readonly taxTotal: string = ''; + + /** + * The total shipping tax for this rate code. + * + * @type {string} + */ + public readonly shippingTaxTotal: string = ''; + + /** + * The tax rate as a percentage. + * + * @type {number} + */ + public readonly ratePercent: number = 0; +} + +/** + * Order shipping line + */ +export class OrderShippingLine extends OrderItemMeta { + /** + * The shipping method title. + * + * @type {string} + */ + public readonly methodTitle: string = ''; + + /** + * The shipping method id. + * + * @type {string} + */ + public readonly methodId: string = ''; + + /** + * The shipping method instance id. + * + * @type {string} + */ + public readonly instanceId: string = ''; + + /** + * The total shipping amount for this method. + * + * @type {string} + */ + public readonly total: string = ''; + + /** + * The total tax amount for this shipping method. + * + * @type {string} + */ + public readonly totalTax: string = ''; + + /** + * The taxes applied to this shipping method. + * + * @type {ReadonlyArray.} + */ + public readonly taxes: OrderItemTax[] = []; +} + +/** + * Order fee line + */ +export class OrderFeeLine extends OrderItemMeta { + /** + * The name of the fee. + * + * @type {string} + */ + public readonly name: string = ''; + + /** + * The tax class of the fee. + * + * @type {string} + */ + public readonly taxClass: string = ''; + + /** + * The tax status of the fee. + * + * @type {TaxStatus} + */ + public readonly taxStatus: TaxStatus = 'taxable'; + + /** + * The total amount for this fee. + * + * @type {string} + */ + public readonly amount: string = ''; + + /** + * The display total amount for this fee. + * + * @type {string} + */ + public readonly total: string = ''; + + /** + * The total tax amount for this fee. + * + * @type {string} + */ + public readonly totalTax: string = ''; + + /** + * The taxes applied to this fee. + * + * @type {ReadonlyArray.} + */ + public readonly taxes: OrderItemTax[] = []; +} + +/** + * Order coupon line + */ +export class OrderCouponLine extends OrderItemMeta { + /** + * The coupon code + * + * @type {string} + */ + public readonly code: string = ''; + + /** + * The discount amount. + * + * @type {string} + */ + public readonly discount: string = ''; + + /** + * The discount tax. + * + * @type {string} + */ + public readonly discountTax: string = ''; +} + +/** + * Order refund line + */ +export class OrderRefundLine extends Model { + /** + * The reason for giving the refund. + * + * @type {string} + */ + public readonly reason: string = ''; + + /** + * The total amount of the refund. + * + * @type {string} + */ + public readonly total: string = ''; +} diff --git a/tests/e2e/api/src/models/orders/shared/index.ts b/tests/e2e/api/src/models/orders/shared/index.ts new file mode 100644 index 00000000000..15eb796c953 --- /dev/null +++ b/tests/e2e/api/src/models/orders/shared/index.ts @@ -0,0 +1,2 @@ +export * from './classes'; +export * from './types'; diff --git a/tests/e2e/api/src/models/orders/shared/types.ts b/tests/e2e/api/src/models/orders/shared/types.ts new file mode 100644 index 00000000000..adeba0128c4 --- /dev/null +++ b/tests/e2e/api/src/models/orders/shared/types.ts @@ -0,0 +1,66 @@ +/** + * An order's status. + * + * @typedef OrderStatus + */ +export type OrderStatus = 'pending' | 'processing' | 'complete' | 'on-hold' | 'refunded' + | 'cancelled' | 'failed' | 'trash' | string; + +/** + * An fee's tax status. + * + * @typedef TaxStatus + */ +export type TaxStatus = 'taxable' | 'none'; + +/** + * Base order properties + */ +export type OrderDataUpdateParams = 'id' | 'parentId' | 'status' | 'currency' | 'version' + | 'pricesIncludeTax' | 'discountTotal' | 'discountTax' | 'shippingTotal' | 'shippingTax' + | 'cartTax' | 'customerId' | 'orderKey' | 'paymentMethod' | 'paymentMethodTitle' + | 'transactionId' | 'customerIpAddress' | 'customerUserAgent' | 'createdVia' | 'datePaid' + | 'customerNote' | 'dateCompleted' | 'cartHash' | 'orderNumber' | 'currencySymbol'; + +/** + * Common total properties + */ +export type OrderTotalUpdateParams = 'total' | 'totalTax'; + +/** + * Order address properties + */ +export type OrderAddressUpdateParams = 'firstName' | 'lastName' | 'companyName' | 'address1' + | 'address2' | 'city' | 'state' | 'postCode' | 'countryCode' | 'email' | 'phone'; + +/** + * Line item properties + */ +export type OrderLineItemUpdateParams = 'name' | 'ProductId' | 'variationId' | 'quantity' + | 'taxClass' | 'subtotal' | 'subtotalTax' | 'sku' | 'price' | 'parentName'; + +/** + * Tax rate properties + */ +export type OrderTaxUpdateParams = 'rateCode' | 'rateId' | 'label' | 'compoundRate' + | 'taxTotal' | 'shippingTaxTotal' | 'ratePercent'; + +/** + * Order shipping properties + */ +export type OrderShippingUpdateParams = 'methodTitle' | 'methodId' | 'instanceId'; + +/** + * Order fee properties + */ +export type OrderFeeUpdateParams = 'name' | 'taxClass' | 'taxStatus' | 'amount'; + +/** + * Order coupon properties + */ +export type OrderCouponUpdateParams = 'code' | 'discount' | 'discountTax'; + +/** + * Order refund properties + */ +export type OrderRefundUpdateParams = 'reason' | 'total'; diff --git a/tests/e2e/api/src/models/products/abstract/common.ts b/tests/e2e/api/src/models/products/abstract/common.ts index d9a4a3525db..bab3e1dc6b7 100644 --- a/tests/e2e/api/src/models/products/abstract/common.ts +++ b/tests/e2e/api/src/models/products/abstract/common.ts @@ -3,9 +3,9 @@ import { ModelID } from '../../model'; import { CatalogVisibility, ProductTerm, - ProductLinks, ProductAttribute, } from '../shared'; +import { ObjectLinks } from '../../shared-types'; /** * The common parameters that all products can use in search. @@ -145,11 +145,11 @@ export abstract class AbstractProduct extends AbstractProductData { public readonly attributes: readonly ProductAttribute[] = []; /** - * The products links. + * The product's links. * - * @type {ReadonlyArray.} + * @type {ReadonlyArray.} */ - public readonly links: ProductLinks = { + public readonly links: ObjectLinks = { collection: [ { href: '' } ], self: [ { href: '' } ], }; diff --git a/tests/e2e/api/src/models/products/abstract/data.ts b/tests/e2e/api/src/models/products/abstract/data.ts index 0d1b83cb37e..e76c1c7d9d8 100644 --- a/tests/e2e/api/src/models/products/abstract/data.ts +++ b/tests/e2e/api/src/models/products/abstract/data.ts @@ -1,6 +1,6 @@ import { Model } from '../../model'; -import { MetaData, PostStatus } from '../../shared-types'; -import { ProductImage, ProductLinks } from '../shared'; +import { MetaData, PostStatus, ObjectLinks } from '../../shared-types'; +import { ProductImage } from '../shared'; /** * Base product data. @@ -93,9 +93,9 @@ export abstract class AbstractProductData extends Model { /** * The product data links. * - * @type {ReadonlyArray.} + * @type {ReadonlyArray.} */ - public readonly links: ProductLinks = { + public readonly links: ObjectLinks = { collection: [ { href: '' } ], self: [ { href: '' } ], }; diff --git a/tests/e2e/api/src/models/products/shared/classes.ts b/tests/e2e/api/src/models/products/shared/classes.ts index 7343c594352..c08e0a20b70 100644 --- a/tests/e2e/api/src/models/products/shared/classes.ts +++ b/tests/e2e/api/src/models/products/shared/classes.ts @@ -208,59 +208,3 @@ export class ProductImage { Object.assign( this, properties ); } } - -/** - * A product link item. - */ -class ProductLinkItem { - /** - * The options which are available for the attribute. - * - * @type {ReadonlyArray.} - */ - public readonly href: string = ''; - - /** - * Creates a new product link item. - * - * @param {Partial.} properties The properties to set. - */ - public constructor( properties?: Partial< ProductLinkItem > ) { - Object.assign( this, properties ); - } -} - -/** - * A product's links. - */ -export class ProductLinks { - /** - * The collection containing the product. - * - * @type {ReadonlyArray.} - */ - public readonly collection: readonly ProductLinkItem[] = []; - - /** - * Self referential link to the product. - * - * @type {ReadonlyArray.} - */ - public readonly self: readonly ProductLinkItem[] = []; - - /** - * The link to the parent. - * - * @type {ReadonlyArray.} - */ - public readonly up?: readonly ProductLinkItem[] = []; - - /** - * Creates a new product link list. - * - * @param {Partial.} properties The properties to set. - */ - public constructor( properties?: Partial< ProductLinks > ) { - Object.assign( this, properties ); - } -} diff --git a/tests/e2e/api/src/models/products/variation.ts b/tests/e2e/api/src/models/products/variation.ts index 78e8580c2fa..beb1bad8071 100644 --- a/tests/e2e/api/src/models/products/variation.ts +++ b/tests/e2e/api/src/models/products/variation.ts @@ -15,13 +15,13 @@ import { ProductPriceUpdateParams, ProductSalesTaxUpdateParams, ProductShippingUpdateParams, - ProductLinks, Taxability, ProductDownload, StockStatus, BackorderStatus, ProductDefaultAttribute, } from './shared'; +import { ObjectLinks } from '../shared-types'; import { CreatesChildModels, DeletesChildModels, @@ -149,11 +149,11 @@ export class ProductVariation extends AbstractProductData implements public readonly shippingClassId: number = 0; /** - * The variation links. + * The variation's links. * - * @type {ReadonlyArray.} + * @type {ReadonlyArray.} */ - public readonly links: ProductLinks = { + public readonly links: ObjectLinks = { collection: [ { href: '' } ], self: [ { href: '' } ], up: [ { href: '' } ], diff --git a/tests/e2e/api/src/models/shared-types.ts b/tests/e2e/api/src/models/shared-types.ts index 5f64fff0b72..d041f3c48c8 100644 --- a/tests/e2e/api/src/models/shared-types.ts +++ b/tests/e2e/api/src/models/shared-types.ts @@ -35,6 +35,20 @@ export class MetaData extends Model { */ public readonly value: any = ''; + /** + * The key of the metadata. + * + * @type {string} + */ + public readonly displayKey?: string = ''; + + /** + * The value of the metadata. + * + * @type {*} + */ + public readonly displayValue?: string = ''; + /** * Creates a new metadata. * @@ -45,3 +59,59 @@ export class MetaData extends Model { Object.assign( this, properties ); } } + +/** + * An object link item. + */ +class LinkItem { + /** + * The href of the link. + * + * @type {ReadonlyArray.} + */ + public readonly href: string = ''; + + /** + * Creates a new product link item. + * + * @param {Partial.} properties The properties to set. + */ + public constructor( properties?: Partial< LinkItem > ) { + Object.assign( this, properties ); + } +} + +/** + * An object's links. + */ +export class ObjectLinks { + /** + * The collection containing the object. + * + * @type {ReadonlyArray.} + */ + public readonly collection: readonly LinkItem[] = []; + + /** + * Self referential link to the object. + * + * @type {ReadonlyArray.} + */ + public readonly self: readonly LinkItem[] = []; + + /** + * The link to the parent object. + * + * @type {ReadonlyArray.} + */ + public readonly up?: readonly LinkItem[] = []; + + /** + * Creates a new product link list. + * + * @param {Partial.} properties The properties to set. + */ + public constructor( properties?: Partial< ObjectLinks > ) { + Object.assign( this, properties ); + } +} diff --git a/tests/e2e/api/src/repositories/rest/coupons/transformer.ts b/tests/e2e/api/src/repositories/rest/coupons/transformer.ts index b3827471242..8e0c7d85eb9 100644 --- a/tests/e2e/api/src/repositories/rest/coupons/transformer.ts +++ b/tests/e2e/api/src/repositories/rest/coupons/transformer.ts @@ -57,6 +57,7 @@ export function createCouponTransformer(): ModelTransformer< Coupon > { maximumAmount: 'maximum_amount', emailRestrictions: 'email_restrictions', usedBy: 'used_by', + links: '_links', }, ), ], diff --git a/tests/e2e/api/src/repositories/rest/index.ts b/tests/e2e/api/src/repositories/rest/index.ts index 15f18f10ee0..7ae5c9a38a2 100644 --- a/tests/e2e/api/src/repositories/rest/index.ts +++ b/tests/e2e/api/src/repositories/rest/index.ts @@ -1,3 +1,4 @@ -export * from './products'; -export * from './settings'; export * from './coupons'; +export * from './products'; +export * from './orders'; +export * from './settings'; diff --git a/tests/e2e/api/src/repositories/rest/orders/index.ts b/tests/e2e/api/src/repositories/rest/orders/index.ts new file mode 100644 index 00000000000..fa4dd50d134 --- /dev/null +++ b/tests/e2e/api/src/repositories/rest/orders/index.ts @@ -0,0 +1,3 @@ +import orderRESTRepository from './order'; + +export { orderRESTRepository }; diff --git a/tests/e2e/api/src/repositories/rest/orders/order.ts b/tests/e2e/api/src/repositories/rest/orders/order.ts new file mode 100644 index 00000000000..76e29525982 --- /dev/null +++ b/tests/e2e/api/src/repositories/rest/orders/order.ts @@ -0,0 +1,47 @@ +import { HTTPClient } from '../../../http'; +import { + ModelRepository, +} from '../../../framework'; +import { + ModelID, + Order, + OrderRepositoryParams, + ListsOrders, + ReadsOrders, + UpdatesOrders, + CreatesOrders, + DeletesOrders, +} from '../../../models'; + +import { + restList, + restCreate, + restRead, + restUpdate, + restDelete, +} from '../shared'; + +import { createOrderTransformer } from './transformer'; +/** + * + * @param {HTTPClient} httpClient The HTTP client for the REST requests to be made using. + */ +export default function orderRESTRepository( httpClient: HTTPClient ): CreatesOrders +& ListsOrders +& ReadsOrders +& UpdatesOrders +& DeletesOrders { + const buildURL = ( id: ModelID ) => '/wc/v3/orders/' + id; + // Using `?force=true` permanently deletes the order + const buildDeleteUrl = ( id: ModelID ) => `/wc/v3/orders/${ id }?force=true`; + + const transformer = createOrderTransformer(); + + return new ModelRepository( + restList< OrderRepositoryParams >( () => '/wc/v3/orders', Order, httpClient, transformer ), + restCreate< OrderRepositoryParams >( () => '/wc/v3/orders', Order, httpClient, transformer ), + restRead< OrderRepositoryParams >( buildURL, Order, httpClient, transformer ), + restUpdate< OrderRepositoryParams >( buildURL, Order, httpClient, transformer ), + restDelete< OrderRepositoryParams >( buildDeleteUrl, httpClient ), + ); +} diff --git a/tests/e2e/api/src/repositories/rest/orders/transformer.ts b/tests/e2e/api/src/repositories/rest/orders/transformer.ts new file mode 100644 index 00000000000..5cd3c0e2b6f --- /dev/null +++ b/tests/e2e/api/src/repositories/rest/orders/transformer.ts @@ -0,0 +1,277 @@ +import { + IgnorePropertyTransformation, + KeyChangeTransformation, + ModelTransformer, + ModelTransformerTransformation, + PropertyType, + PropertyTypeTransformation, +} from '../../../framework'; +import { + Order, + OrderAddress, + OrderCouponLine, + OrderFeeLine, + OrderLineItem, + OrderRefundLine, + OrderShippingLine, + OrderTaxRate, +} from '../../../models'; + +/** + * Creates a transformer for an order object. + * + * @return {ModelTransformer} The created transformer. + */ +export function createOrderTransformer(): ModelTransformer< Order > { + return new ModelTransformer( + [ + new IgnorePropertyTransformation( [ 'date_created', 'date_modified' ] ), + new ModelTransformerTransformation( 'billing', OrderAddress, createOrderAddressTransformer() ), + new ModelTransformerTransformation( 'tax_lines', OrderTaxRate, createOrderTaxRateTransformer() ), + new ModelTransformerTransformation( 'refunds', OrderRefundLine, createOrderRefundLineTransformer() ), + new ModelTransformerTransformation( 'coupon_lines', OrderCouponLine, createOrdeCouponLineTransformer() ), + new ModelTransformerTransformation( 'fee_lines', OrderFeeLine, createOrderFeeLineTransformer() ), + new ModelTransformerTransformation( 'line_items', OrderLineItem, createOrderLineItemTransformer() ), + new ModelTransformerTransformation( 'shipping_lines', OrderShippingLine, createOrderShippingItemTransformer() ), + + new PropertyTypeTransformation( + { + status: PropertyType.String, + currency: PropertyType.String, + discountTotal: PropertyType.String, + discountTax: PropertyType.String, + shippingTotal: PropertyType.String, + shippingTax: PropertyType.String, + cartTax: PropertyType.String, + total: PropertyType.String, + totalTax: PropertyType.String, + pricesIncludeTax: PropertyType.Boolean, + customerId: PropertyType.Integer, + customerNote: PropertyType.String, + paymentMethod: PropertyType.String, + transactionId: PropertyType.String, + setPaid: PropertyType.Boolean, + }, + ), + new KeyChangeTransformation< Order >( + { + discountTotal: 'discount_total', + discountTax: 'discount_tax', + shippingTotal: 'shipping_total', + shippingTax: 'shipping_tax', + cartTax: 'cart_tax', + totalTax: 'total_tax', + pricesIncludeTax: 'prices_include_tax', + customerId: 'customer_id', + customerNote: 'customer_note', + paymentMethod: 'payment_method', + transactionId: 'transaction_id', + setPaid: 'set_paid', + }, + ), + ], + ); +} + +/** + * Creates a transformer for an order address object. + * + * @return {ModelTransformer} The created transformer. + */ +export function createOrderAddressTransformer(): ModelTransformer< OrderAddress > { + return new ModelTransformer( + [ + new PropertyTypeTransformation( + { + firstName: PropertyType.String, + lastName: PropertyType.String, + company: PropertyType.String, + address1: PropertyType.String, + address2: PropertyType.String, + city: PropertyType.String, + state: PropertyType.String, + postCode: PropertyType.String, + country: PropertyType.String, + }, + ), + new KeyChangeTransformation< OrderAddress >( + { + firstName: 'first_name', + lastName: 'last_name', + address1: 'address_1', + address2: 'address_2', + postCode: 'postcode', + }, + ), + ], + ); +} + +/** + * Creates a transformer for an order tax rate object. + * + * @return {ModelTransformer} The created transformer. + */ +function createOrderTaxRateTransformer(): ModelTransformer< OrderTaxRate > { + return new ModelTransformer( + [ + new PropertyTypeTransformation( + { + rateCode: PropertyType.String, + rateId: PropertyType.Integer, + label: PropertyType.String, + compoundRate: PropertyType.Boolean, + taxTotal: PropertyType.String, + shippingTaxTotal: PropertyType.String, + ratePercent: PropertyType.Integer, + }, + ), + new KeyChangeTransformation< OrderTaxRate >( + { + rateCode: 'rate_code', + rateId: 'rate_id', + compoundRate: 'compound', + taxTotal: 'tax_total', + shippingTaxTotal: 'shipping_tax_total', + }, + ), + ], + ); +} + +/** + * Creates a transformer for an order refund line object. + * + * @return {ModelTransformer} The created transformer. + */ +function createOrderRefundLineTransformer(): ModelTransformer< OrderRefundLine > { + return new ModelTransformer( + [ + new PropertyTypeTransformation( + { + reason: PropertyType.String, + total: PropertyType.String, + }, + ), + ], + ); +} + +/** + * Creates a transformer for an order coupon line object. + * + * @return {ModelTransformer} The created transformer. + */ +function createOrdeCouponLineTransformer(): ModelTransformer< OrderCouponLine > { + return new ModelTransformer( + [ + new PropertyTypeTransformation( + { + code: PropertyType.String, + discount: PropertyType.Integer, + discountTax: PropertyType.String, + }, + ), + new KeyChangeTransformation< OrderCouponLine >( + { + discountTax: 'discount_tax', + }, + ), + ], + ); +} + +/** + * Creates a transformer for an order fee line object. + * + * @return {ModelTransformer} The created transformer. + */ +function createOrderFeeLineTransformer(): ModelTransformer< OrderFeeLine > { + return new ModelTransformer( + [ + new ModelTransformerTransformation( 'taxes', OrderTaxRate, createOrderTaxRateTransformer() ), + new PropertyTypeTransformation( + { + name: PropertyType.String, + taxClass: PropertyType.String, + taxStatus: PropertyType.String, + total: PropertyType.String, + totalTax: PropertyType.String, + }, + ), + new KeyChangeTransformation< OrderFeeLine >( + { + taxClass: 'tax_class', + taxStatus: 'tax_status', + totalTax: 'total_tax', + }, + ), + ], + ); +} + +/** + * Creates a transformer for an order line item object. + * + * @return {ModelTransformer} The created transformer. + */ +function createOrderLineItemTransformer(): ModelTransformer< OrderLineItem > { + return new ModelTransformer( + [ + new ModelTransformerTransformation( 'taxes', OrderTaxRate, createOrderTaxRateTransformer() ), + new PropertyTypeTransformation( + { + name: PropertyType.String, + productId: PropertyType.Integer, + variationId: PropertyType.Integer, + quantity: PropertyType.Integer, + taxClass: PropertyType.String, + subtotal: PropertyType.String, + subtotalTax: PropertyType.String, + total: PropertyType.String, + totalTax: PropertyType.String, + sku: PropertyType.String, + price: PropertyType.Integer, + parentName: PropertyType.String, + }, + ), + new KeyChangeTransformation< OrderLineItem >( + { + productId: 'product_id', + variationId: 'variation_id', + taxClass: 'tax_class', + subtotalTax: 'subtotal_tax', + totalTax: 'total_tax', + }, + ), + ], + ); +} + +/** + * Creates a transformer for an order shipping line item object. + * + * @return {ModelTransformer} The created transformer. + */ +function createOrderShippingItemTransformer(): ModelTransformer< OrderShippingLine > { + return new ModelTransformer( + [ + new ModelTransformerTransformation( 'taxes', OrderTaxRate, createOrderTaxRateTransformer() ), + new PropertyTypeTransformation( + { + methodTitle: PropertyType.String, + methodId: PropertyType.String, + total: PropertyType.String, + totalTax: PropertyType.String, + }, + ), + new KeyChangeTransformation< OrderShippingLine >( + { + methodTitle: 'method_title', + methodId: 'method_id', + totalTax: 'total_tax', + }, + ), + ], + ); +} diff --git a/tests/e2e/api/src/repositories/rest/shared.ts b/tests/e2e/api/src/repositories/rest/shared.ts index 1d52d73ec3d..3c4e976d177 100644 --- a/tests/e2e/api/src/repositories/rest/shared.ts +++ b/tests/e2e/api/src/repositories/rest/shared.ts @@ -16,6 +16,7 @@ import { CreateChildFn, ModelTransformer, IgnorePropertyTransformation, + KeyChangeTransformation, // @ts-ignore ModelParentID, } from '../../framework'; @@ -34,6 +35,12 @@ export function createMetaDataTransformer(): ModelTransformer< MetaData > { return new ModelTransformer( [ new IgnorePropertyTransformation( [ 'id' ] ), + new KeyChangeTransformation< MetaData >( + { + displayKey: 'display_key', + displayValue: 'display_value', + }, + ), ], ); } diff --git a/tests/e2e/config/default.json b/tests/e2e/config/default.json index 4d6b397aac2..adb8538915e 100644 --- a/tests/e2e/config/default.json +++ b/tests/e2e/config/default.json @@ -180,6 +180,17 @@ } } }, + "orders": { + "basicPaidOrder": { + "paymentMethod": "cod", + "status": "processing", + "billing": { + "firstName": "John", + "lastName": "Doe", + "email": "john.doe@example.com" + } + } + }, "onboardingwizard": { "industry": "Test industry", "numberofproducts": "1 - 10", diff --git a/tests/e2e/core-tests/specs/api/order.test.js b/tests/e2e/core-tests/specs/api/order.test.js new file mode 100644 index 00000000000..cdfc0b7a5c1 --- /dev/null +++ b/tests/e2e/core-tests/specs/api/order.test.js @@ -0,0 +1,82 @@ +/* eslint-disable jest/no-export, jest/no-disabled-tests */ +/** + * Internal dependencies + */ + const { HTTPClientFactory, Order } = require( '@woocommerce/api' ); + + /** + * External dependencies + */ + const config = require( 'config' ); + const { + it, + describe, + beforeAll, + } = require( '@jest/globals' ); + + /** + * Creates an order and tests interactions with it via the API. + */ +const runOrderApiTest = () => { + describe('REST API > Order', () => { + let client; + let order; + let repository; + + beforeAll(async () => { + order = config.get( 'orders.basicPaidOrder' ); + const admin = config.get( 'users.admin' ); + const url = config.get( 'url' ); + + client = HTTPClientFactory.build( url ) + .withBasicAuth( admin.username, admin.password ) + .withIndexPermalinks() + .create(); + } ); + + it('can create an order', async () => { + repository = Order.restRepository( client ); + + // Check properties of the order in the create order response. + order = await repository.create( order ); + expect( order ).toEqual( expect.objectContaining( order ) ); + }); + + it('can retrieve an order', async () => { + const orderProperties = { + id: order.id, + payment_method: order.paymentMethod, + status: order.status, + }; + + // Read order directly from API to compare. + const response = await client.get( `/wc/v3/orders/${order.id}` ); + expect( response.statusCode ).toBe( 200 ); + expect( response.data ).toEqual( expect.objectContaining( orderProperties ) ); + }); + + it('can update an order', async () => { + const updatedOrderProperties = { + payment_method: 'bacs', + status: 'completed', + }; + + await repository.update( order.id, updatedOrderProperties ); + + // Check the order response for the updated values. + const response = await client.get( `/wc/v3/orders/${order.id}` ); + expect( response.statusCode ).toBe( 200 ); + expect( response.data ).toEqual( expect.objectContaining( updatedOrderProperties ) ); + }); + + it('can delete an order', async () => { + // Delete the order + const status = await repository.delete( order.id ); + + // If the delete is successful, the response comes back truthy + expect( status ).toBeTruthy(); + }); + }); +}; + +module.exports = runOrderApiTest; diff --git a/tests/e2e/core-tests/specs/index.js b/tests/e2e/core-tests/specs/index.js index c7cbd79b2fe..030605d5bd3 100644 --- a/tests/e2e/core-tests/specs/index.js +++ b/tests/e2e/core-tests/specs/index.js @@ -52,6 +52,7 @@ const runExternalProductAPITest = require( './api/external-product.test' ); const runCouponApiTest = require( './api/coupon.test' ); const runGroupedProductAPITest = require( './api/grouped-product.test' ); const runVariableProductAPITest = require( './api/variable-product.test' ); +const runOrderApiTest = require( './api/order.test' ); const runSetupOnboardingTests = () => { runActivationTest(); @@ -106,6 +107,7 @@ const runApiTests = () => { runGroupedProductAPITest(); runVariableProductAPITest(); runCouponApiTest(); + runOrderApiTest(); } module.exports = { @@ -135,6 +137,7 @@ module.exports = { runUpdateGeneralSettingsTest, runProductSettingsTest, runTaxSettingsTest, + runOrderApiTest, runOrderStatusFiltersTest, runOrderRefundTest, runOrderApplyCouponTest, diff --git a/tests/e2e/core-tests/specs/merchant/wp-admin-order-emails.test.js b/tests/e2e/core-tests/specs/merchant/wp-admin-order-emails.test.js index ad18a8de6d1..3b69ba28beb 100644 --- a/tests/e2e/core-tests/specs/merchant/wp-admin-order-emails.test.js +++ b/tests/e2e/core-tests/specs/merchant/wp-admin-order-emails.test.js @@ -11,7 +11,6 @@ const { } = require( '@woocommerce/e2e-utils' ); const config = require( 'config' ); -const simpleProductName = config.get( 'products.simple.name' ); const customerEmail = config.get( 'addresses.customer.billing.email' ); const adminEmail = 'admin@woocommercecoree2etestsuite.com'; const storeName = 'WooCommerce Core E2E Test Suite'; diff --git a/tests/e2e/core-tests/specs/merchant/wp-admin-product-import-csv.test.js b/tests/e2e/core-tests/specs/merchant/wp-admin-product-import-csv.test.js index e3705010a93..1e2b945ad24 100644 --- a/tests/e2e/core-tests/specs/merchant/wp-admin-product-import-csv.test.js +++ b/tests/e2e/core-tests/specs/merchant/wp-admin-product-import-csv.test.js @@ -5,7 +5,7 @@ const { merchant, setCheckbox, - moveAllItemsToTrash + withRestApi, } = require( '@woocommerce/e2e-utils' ); const getCoreTestsRoot = require( '../../core-tests-root' ); @@ -108,10 +108,12 @@ const runImportProductsTest = () => { // Gathering product names await page.waitForSelector('a.row-title'); - const productTitles = await page.$$eval('a.row-title', - elements => elements.map(item => item.innerHTML)); + const productTitles = await page.$$eval( + 'a.row-title', + elements => elements.map(item => item.innerHTML) + ); - // Compare overriden product names + // Compare overridden product names expect(productTitles.sort()).toEqual(productNamesOverride.sort()); // Gathering product prices @@ -119,12 +121,11 @@ const runImportProductsTest = () => { const productPrices = await page.$$eval('.amount', elements => elements.map(item => item.innerText)); - // Compare overriden product prices + // Compare overridden product prices expect(productPrices.sort()).toEqual(productPricesOverride.sort()); - // Move all imported products to trash - await moveAllItemsToTrash(); - + // Delete imported products + await withRestApi.deleteAllProducts(); }); }); }; diff --git a/tests/e2e/specs/rest-api/order.js b/tests/e2e/specs/rest-api/order.js new file mode 100644 index 00000000000..d73a8be5f28 --- /dev/null +++ b/tests/e2e/specs/rest-api/order.js @@ -0,0 +1,6 @@ +/* + * Internal dependencies + */ +const { runOrderApiTest } = require( '@woocommerce/e2e-core-tests' ); + +runOrderApiTest(); diff --git a/tests/e2e/utils/src/flows/with-rest-api.js b/tests/e2e/utils/src/flows/with-rest-api.js index 442d1d20a8c..64b40d773ee 100644 --- a/tests/e2e/utils/src/flows/with-rest-api.js +++ b/tests/e2e/utils/src/flows/with-rest-api.js @@ -16,20 +16,24 @@ const userEndpoint = '/wp/v2/users'; const deleteAllRepositoryObjects = async ( repository, defaultObjectId = null ) => { let objects; const minimum = defaultObjectId == null ? 0 : 1; + const statuses = ['draft','publish','trash']; - objects = await repository.list(); - while ( objects.length > minimum ) { - for (let o = 0; o < objects.length; o++ ) { - // Skip default data store object - if ( objects[ o ].id == defaultObjectId ) { - continue; + for ( let s = 0; s < statuses.length; s++ ) { + const status = statuses[ s ]; + objects = await repository.list( { status } ); + while (objects.length > minimum) { + for (let o = 0; o < objects.length; o++) { + // Skip default data store object + if (objects[o].id == defaultObjectId) { + continue; + } + // We may be getting a cached copy of the dataset and the object has already been deleted. + try { + await repository.delete(objects[o].id); + } catch (e) {} } - // We may be getting a cached copy of the dataset and the object has already been deleted. - try { - await repository.delete(objects[o].id); - } catch (e) {} + objects = await repository.list( { status } ); } - objects = await repository.list(); } }; diff --git a/woocommerce.php b/woocommerce.php index 1fb1516fb43..57edd900f95 100644 --- a/woocommerce.php +++ b/woocommerce.php @@ -3,7 +3,7 @@ * Plugin Name: WooCommerce * Plugin URI: https://woocommerce.com/ * Description: An eCommerce toolkit that helps you sell anything. Beautifully. - * Version: 5.4.0-dev + * Version: 5.5.0-dev * Author: Automattic * Author URI: https://woocommerce.com * Text Domain: woocommerce