merge master

This commit is contained in:
Ron Rennick 2020-04-28 16:13:30 -03:00
commit da494be615
22 changed files with 161 additions and 608 deletions

View File

@ -20,7 +20,6 @@ cache:
# Test main supported versions of PHP against latest WP.
php:
- 5.6
- 7.0
- 7.1
- 7.2
@ -30,7 +29,7 @@ php:
env:
- WP_VERSION=latest WP_MULTISITE=0
# Additional tests against stable PHP (min version is 5.6)
# Additional tests against stable PHP (min version is 7.0)
# and code coverage report.
matrix:
fast_finish: true
@ -55,8 +54,17 @@ matrix:
- name: "WooCommerce unit tests using WordPress nightly"
php: 7.4
env: WP_VERSION=nightly WP_MULTISITE=0
- name: "Minimum requirements"
php: 5.6
- name: "WP latest - 1"
php: 7.2
env: WP_VERSION=5.3 WP_MULTISITE=0
- name: "WP latest - 2"
php: 7.2
env: WP_VERSION=5.2 WP_MULTISITE=0
- name: "WP 5.1"
php: 7.2
env: WP_VERSION=5.1 WP_MULTISITE=0
- name: "WP 5.0"
php: 7.0
env: WP_VERSION=5.0 WP_MULTISITE=0
allow_failures:
- php: 7.4

View File

@ -7,20 +7,20 @@
"prefer-stable": true,
"minimum-stability": "dev",
"require": {
"php": ">=5.6|>=7.0",
"php": ">=7.0",
"automattic/jetpack-autoloader": "^1.6.0",
"automattic/jetpack-constants": "^1.1",
"composer/installers": "1.7.0",
"maxmind-db/reader": "1.6.0",
"pelago/emogrifier": "^3.1",
"woocommerce/action-scheduler": "3.1.4",
"woocommerce/woocommerce-admin": "dev-release/1.1.0",
"woocommerce/woocommerce-admin": "1.1.0",
"woocommerce/woocommerce-blocks": "2.5.16",
"woocommerce/woocommerce-rest-api": "1.0.7"
},
"require-dev": {
"phpunit/phpunit": "7.5.20",
"woocommerce/woocommerce-sniffs": "0.0.9",
"woocommerce/woocommerce-sniffs": "0.0.10",
"wp-cli/i18n-command": "^2.2"
},
"config": {

116
composer.lock generated
View File

@ -4,20 +4,20 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "87279b8212b48c975f467572917a51c8",
"content-hash": "edacab36b41ea6ed95fa1807072b8993",
"packages": [
{
"name": "automattic/jetpack-autoloader",
"version": "v1.6.0",
"version": "v1.7.0",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-autoloader.git",
"reference": "3bcbe1ae19febd6beeb181cf11af0bf0b7abe7e7"
"reference": "7c6736eeee0f9fc49fa691fe3e958725efb27ca0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Automattic/jetpack-autoloader/zipball/3bcbe1ae19febd6beeb181cf11af0bf0b7abe7e7",
"reference": "3bcbe1ae19febd6beeb181cf11af0bf0b7abe7e7",
"url": "https://api.github.com/repos/Automattic/jetpack-autoloader/zipball/7c6736eeee0f9fc49fa691fe3e958725efb27ca0",
"reference": "7c6736eeee0f9fc49fa691fe3e958725efb27ca0",
"shasum": ""
},
"require": {
@ -40,23 +40,24 @@
"GPL-2.0-or-later"
],
"description": "Creates a custom autoloader for a plugin or theme.",
"time": "2020-03-26T07:57:53+00:00"
"time": "2020-04-23T02:28:37+00:00"
},
{
"name": "automattic/jetpack-constants",
"version": "v1.1.3",
"version": "v1.2.0",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-constants.git",
"reference": "5fdd94dec1151e7defd684a97e0b64fe6ff1bd3a"
"reference": "881618defb04134ddba120e7835af1a474a11edc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Automattic/jetpack-constants/zipball/5fdd94dec1151e7defd684a97e0b64fe6ff1bd3a",
"reference": "5fdd94dec1151e7defd684a97e0b64fe6ff1bd3a",
"url": "https://api.github.com/repos/Automattic/jetpack-constants/zipball/881618defb04134ddba120e7835af1a474a11edc",
"reference": "881618defb04134ddba120e7835af1a474a11edc",
"shasum": ""
},
"require-dev": {
"php-mock/php-mock": "^2.1",
"phpunit/phpunit": "^5.7 || ^6.5 || ^7.5"
},
"type": "library",
@ -70,7 +71,7 @@
"GPL-2.0-or-later"
],
"description": "A wrapper for defining constants in a more testable way.",
"time": "2019-11-08T21:16:05+00:00"
"time": "2020-04-15T18:58:53+00:00"
},
{
"name": "composer/installers",
@ -330,7 +331,7 @@
},
{
"name": "symfony/css-selector",
"version": "v3.4.39",
"version": "v3.4.40",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
@ -418,16 +419,16 @@
},
{
"name": "woocommerce/woocommerce-admin",
"version": "dev-release/1.1.0",
"version": "v1.1.0",
"source": {
"type": "git",
"url": "https://github.com/woocommerce/woocommerce-admin.git",
"reference": "14c382bb24fb9c460c0eab0800c74287c02cc594"
"reference": "3ef2eedca2b19c54f05333df9d9168a00499901b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/woocommerce/woocommerce-admin/zipball/14c382bb24fb9c460c0eab0800c74287c02cc594",
"reference": "14c382bb24fb9c460c0eab0800c74287c02cc594",
"url": "https://api.github.com/repos/woocommerce/woocommerce-admin/zipball/3ef2eedca2b19c54f05333df9d9168a00499901b",
"reference": "3ef2eedca2b19c54f05333df9d9168a00499901b",
"shasum": ""
},
"require": {
@ -461,7 +462,7 @@
],
"description": "A modern, javascript-driven WooCommerce Admin experience.",
"homepage": "https://github.com/woocommerce/woocommerce-admin",
"time": "2020-04-14T18:49:38+00:00"
"time": "2020-04-21T00:28:43+00:00"
},
{
"name": "woocommerce/woocommerce-blocks",
@ -554,16 +555,16 @@
"packages-dev": [
{
"name": "dealerdirect/phpcodesniffer-composer-installer",
"version": "v0.5.0",
"version": "v0.6.2",
"source": {
"type": "git",
"url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git",
"reference": "e749410375ff6fb7a040a68878c656c2e610b132"
"reference": "8001af8eb107fbfcedc31a8b51e20b07d85b457a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/e749410375ff6fb7a040a68878c656c2e610b132",
"reference": "e749410375ff6fb7a040a68878c656c2e610b132",
"url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/8001af8eb107fbfcedc31a8b51e20b07d85b457a",
"reference": "8001af8eb107fbfcedc31a8b51e20b07d85b457a",
"shasum": ""
},
"require": {
@ -616,7 +617,7 @@
"stylecheck",
"tests"
],
"time": "2018-10-26T13:21:45+00:00"
"time": "2020-01-29T20:22:20+00:00"
},
{
"name": "doctrine/instantiator",
@ -1200,24 +1201,21 @@
},
{
"name": "phpdocumentor/reflection-common",
"version": "2.0.0",
"version": "2.1.0",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionCommon.git",
"reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a"
"reference": "6568f4687e5b41b054365f9ae03fcb1ed5f2069b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/63a995caa1ca9e5590304cd845c15ad6d482a62a",
"reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a",
"url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/6568f4687e5b41b054365f9ae03fcb1ed5f2069b",
"reference": "6568f4687e5b41b054365f9ae03fcb1ed5f2069b",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"require-dev": {
"phpunit/phpunit": "~6"
},
"type": "library",
"extra": {
"branch-alias": {
@ -1248,7 +1246,7 @@
"reflection",
"static analysis"
],
"time": "2018-08-07T13:53:10+00:00"
"time": "2020-04-27T09:25:28+00:00"
},
{
"name": "phpdocumentor/reflection-docblock",
@ -2365,16 +2363,16 @@
},
{
"name": "squizlabs/php_codesniffer",
"version": "3.5.4",
"version": "3.5.5",
"source": {
"type": "git",
"url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
"reference": "dceec07328401de6211037abbb18bda423677e26"
"reference": "73e2e7f57d958e7228fce50dc0c61f58f017f9f6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/dceec07328401de6211037abbb18bda423677e26",
"reference": "dceec07328401de6211037abbb18bda423677e26",
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/73e2e7f57d958e7228fce50dc0c61f58f017f9f6",
"reference": "73e2e7f57d958e7228fce50dc0c61f58f017f9f6",
"shasum": ""
},
"require": {
@ -2412,11 +2410,11 @@
"phpcs",
"standards"
],
"time": "2020-01-30T22:20:29+00:00"
"time": "2020-04-17T01:09:41+00:00"
},
{
"name": "symfony/finder",
"version": "v3.4.39",
"version": "v3.4.40",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
@ -2563,16 +2561,16 @@
},
{
"name": "webmozart/assert",
"version": "1.7.0",
"version": "1.8.0",
"source": {
"type": "git",
"url": "https://github.com/webmozart/assert.git",
"reference": "aed98a490f9a8f78468232db345ab9cf606cf598"
"reference": "ab2cb0b3b559010b75981b1bdce728da3ee90ad6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webmozart/assert/zipball/aed98a490f9a8f78468232db345ab9cf606cf598",
"reference": "aed98a490f9a8f78468232db345ab9cf606cf598",
"url": "https://api.github.com/repos/webmozart/assert/zipball/ab2cb0b3b559010b75981b1bdce728da3ee90ad6",
"reference": "ab2cb0b3b559010b75981b1bdce728da3ee90ad6",
"shasum": ""
},
"require": {
@ -2580,7 +2578,7 @@
"symfony/polyfill-ctype": "^1.8"
},
"conflict": {
"vimeo/psalm": "<3.6.0"
"vimeo/psalm": "<3.9.1"
},
"require-dev": {
"phpunit/phpunit": "^4.8.36 || ^7.5.13"
@ -2607,27 +2605,27 @@
"check",
"validate"
],
"time": "2020-02-14T12:15:55+00:00"
"time": "2020-04-18T12:12:48+00:00"
},
{
"name": "woocommerce/woocommerce-sniffs",
"version": "0.0.9",
"version": "0.0.10",
"source": {
"type": "git",
"url": "https://github.com/woocommerce/woocommerce-sniffs.git",
"reference": "7677a84e9a355fe1e088f704090be891e7a6d427"
"reference": "b0e3d69a53b3ffdbb97a0371bd1b43aa17092d65"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/woocommerce/woocommerce-sniffs/zipball/7677a84e9a355fe1e088f704090be891e7a6d427",
"reference": "7677a84e9a355fe1e088f704090be891e7a6d427",
"url": "https://api.github.com/repos/woocommerce/woocommerce-sniffs/zipball/b0e3d69a53b3ffdbb97a0371bd1b43aa17092d65",
"reference": "b0e3d69a53b3ffdbb97a0371bd1b43aa17092d65",
"shasum": ""
},
"require": {
"dealerdirect/phpcodesniffer-composer-installer": "0.5.0",
"dealerdirect/phpcodesniffer-composer-installer": "0.6.2",
"php": ">=7.0",
"phpcompatibility/phpcompatibility-wp": "2.1.0",
"wp-coding-standards/wpcs": "2.2.0"
"wp-coding-standards/wpcs": "2.2.1"
},
"type": "phpcodesniffer-standard",
"notification-url": "https://packagist.org/downloads/",
@ -2647,7 +2645,7 @@
"woocommerce",
"wordpress"
],
"time": "2019-11-11T15:48:34+00:00"
"time": "2020-04-07T20:25:44+00:00"
},
{
"name": "wp-cli/i18n-command",
@ -2868,16 +2866,16 @@
},
{
"name": "wp-coding-standards/wpcs",
"version": "2.2.0",
"version": "2.2.1",
"source": {
"type": "git",
"url": "https://github.com/WordPress/WordPress-Coding-Standards.git",
"reference": "f90e8692ce97b693633db7ab20bfa78d930f536a"
"reference": "b5a453203114cc2284b1a614c4953456fbe4f546"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/f90e8692ce97b693633db7ab20bfa78d930f536a",
"reference": "f90e8692ce97b693633db7ab20bfa78d930f536a",
"url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/b5a453203114cc2284b1a614c4953456fbe4f546",
"reference": "b5a453203114cc2284b1a614c4953456fbe4f546",
"shasum": ""
},
"require": {
@ -2885,12 +2883,12 @@
"squizlabs/php_codesniffer": "^3.3.1"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "^0.5.0",
"dealerdirect/phpcodesniffer-composer-installer": "^0.5 || ^0.6",
"phpcompatibility/php-compatibility": "^9.0",
"phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0"
},
"suggest": {
"dealerdirect/phpcodesniffer-composer-installer": "^0.5.0 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically."
"dealerdirect/phpcodesniffer-composer-installer": "^0.6 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically."
},
"type": "phpcodesniffer-standard",
"notification-url": "https://packagist.org/downloads/",
@ -2909,18 +2907,16 @@
"standards",
"wordpress"
],
"time": "2019-11-11T12:34:03+00:00"
"time": "2020-02-04T02:52:06+00:00"
}
],
"aliases": [],
"minimum-stability": "dev",
"stability-flags": {
"woocommerce/woocommerce-admin": 20
},
"stability-flags": [],
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
"php": ">=5.6|>=7.0"
"php": ">=7.0"
},
"platform-dev": [],
"platform-overrides": {

View File

@ -765,6 +765,7 @@ class WC_Cart extends WC_Legacy_Cart {
public function check_cart_item_stock() {
$error = new WP_Error();
$product_qty_in_cart = $this->get_cart_item_quantities();
$hold_stock_minutes = (int) get_option( 'woocommerce_hold_stock_minutes', 0 );
$current_session_order_id = isset( WC()->session->order_awaiting_payment ) ? absint( WC()->session->order_awaiting_payment ) : 0;
foreach ( $this->get_cart() as $cart_item_key => $values ) {
@ -783,7 +784,7 @@ class WC_Cart extends WC_Legacy_Cart {
}
// Check stock based on all items in the cart and consider any held stock within pending orders.
$held_stock = wc_get_held_stock_quantity( $product, $current_session_order_id );
$held_stock = ( $hold_stock_minutes > 0 ) ? wc_get_held_stock_quantity( $product, $current_session_order_id ) : 0;
$required_stock = $product_qty_in_cart[ $product->get_stock_managed_by_id() ];
if ( $product->get_stock_quantity() < ( $held_stock + $required_stock ) ) {

View File

@ -388,30 +388,12 @@ class WC_Checkout {
// Save the order.
$order_id = $order->save();
/**
* Action hook fired after an order is created used to add custom meta to the order.
*
* @since 3.0.0
*/
do_action( 'woocommerce_checkout_update_order_meta', $order_id, $data );
/**
* Action hook fired after an order is created.
*
* @since 4.1.0
*/
do_action( 'woocommerce_checkout_order_created', $order );
return $order_id;
} catch ( Exception $e ) {
if ( $order && $order instanceof WC_Order ) {
$order->get_data_store()->release_held_coupons( $order );
/**
* Action hook fired when an order is discarded due to Exception.
*
* @since 4.1.0
*/
do_action( 'woocommerce_checkout_order_exception', $order );
}
return new WP_Error( 'checkout-error', $e->getMessage() );
}

View File

@ -350,18 +350,20 @@ class WC_Countries {
/**
* Gets an array of countries in the EU.
*
* @param string $deprecated Function used to return VAT countries based on this.
* @param string $type Type of countries to retrieve. Blank for EU member countries. eu_vat for EU VAT countries.
* @return string[]
*/
public function get_european_union_countries( $deprecated = '' ) {
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' );
if ( ! empty( $deprecated ) ) {
wc_deprecated_argument( 'type', '4.0.0', 'Use the WC_Countries::get_vat_countries method instead.' );
$countries = $this->get_vat_countries();
if ( 'eu_vat' === $type ) {
$countries[] = 'MC';
$countries[] = 'IM';
// The UK is still part of the EU VAT zone.
$countries[] = 'GB';
}
return apply_filters( 'woocommerce_european_union_countries', $countries, $deprecated );
return apply_filters( 'woocommerce_european_union_countries', $countries, $type );
}
/**

View File

@ -946,14 +946,6 @@ CREATE TABLE {$wpdb->prefix}wc_tax_rate_classes (
slug varchar(200) NOT NULL DEFAULT '',
PRIMARY KEY (tax_rate_class_id),
UNIQUE KEY slug (slug($max_index_length))
) $collate;
CREATE TABLE {$wpdb->prefix}wc_reserved_stock (
`order_id` bigint(20) NOT NULL,
`product_id` bigint(20) NOT NULL,
`stock_quantity` double NOT NULL DEFAULT 0,
`timestamp` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`expires` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`order_id`, `product_id`)
) $collate;
";
@ -988,7 +980,6 @@ CREATE TABLE {$wpdb->prefix}wc_reserved_stock (
"{$wpdb->prefix}woocommerce_shipping_zones",
"{$wpdb->prefix}woocommerce_tax_rate_locations",
"{$wpdb->prefix}woocommerce_tax_rates",
"{$wpdb->prefix}wc_reserved_stock",
);
/**

View File

@ -246,7 +246,6 @@ final class WooCommerce {
'order_itemmeta' => 'woocommerce_order_itemmeta',
'wc_product_meta_lookup' => 'wc_product_meta_lookup',
'wc_tax_rate_classes' => 'wc_tax_rate_classes',
'wc_reserved_stock' => 'wc_reserved_stock',
);
foreach ( $tables as $name => $table ) {

View File

@ -881,8 +881,8 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
$outofstock_where = ' AND exclude_join.object_id IS NULL';
}
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
return $wpdb->get_results(
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
"
SELECT posts.ID as id, posts.post_parent as parent_id
FROM {$wpdb->posts} AS posts
@ -900,8 +900,8 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
)
GROUP BY posts.ID
"
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
);
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
}
/**
@ -1603,7 +1603,7 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
foreach ( $search_terms as $search_term ) {
$like = '%' . $wpdb->esc_like( $search_term ) . '%';
$term_group_query .= $wpdb->prepare( " {$searchand} ( ( posts.post_title LIKE %s) OR ( posts.post_excerpt LIKE %s) OR ( posts.post_content LIKE %s ) OR ( wc_product_meta_lookup.sku LIKE %s ) )", $like, $like, $like, $like ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$term_group_query .= $wpdb->prepare( " {$searchand} ( ( posts.post_title LIKE %s) OR ( posts.post_excerpt LIKE %s) OR ( posts.post_content LIKE %s ) OR ( wc_product_meta_lookup.sku LIKE %s ) )", $like, $like, $like, $like ); // @codingStandardsIgnoreLine.
$searchand = ' AND ';
}
@ -2062,23 +2062,4 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
}
return '';
}
/**
* Returns query statement for getting current `_stock` of a product.
*
* @internal MAX function below is used to make sure result is a scalar.
* @param int $product_id Product ID.
* @return string|void Query statement.
*/
public function get_query_for_stock( $product_id ) {
global $wpdb;
return $wpdb->prepare(
"
SELECT COALESCE ( MAX( meta_value ), 0 ) FROM $wpdb->postmeta as meta_table
WHERE meta_table.meta_key = '_stock'
AND meta_table.post_id = %d
",
$product_id
);
}
}

View File

@ -106,9 +106,10 @@ class WC_Shipping_Free_Shipping extends WC_Shipping_Method {
'desc_tip' => true,
),
'ignore_discounts' => array(
'title' => __( 'Ignore coupons discounts', 'woocommerce' ),
'title' => __( 'Coupons discounts', 'woocommerce' ),
'label' => __( 'Apply minimum order rule before coupon discount', 'woocommerce' ),
'type' => 'checkbox',
'description' => __( 'Discounts will not be applied to the minimum order amount.', 'woocommerce' ),
'description' => __( 'If checked, free shipping would be available based on pre-discount order amount.', 'woocommerce' ),
'default' => 'no',
'desc_tip' => true,
),

View File

@ -84,8 +84,9 @@ class WC_Shortcode_Checkout {
// Pay for existing order.
if ( isset( $_GET['pay_for_order'], $_GET['key'] ) && $order_id ) { // WPCS: input var ok, CSRF ok.
try {
$order_key = isset( $_GET['key'] ) ? wc_clean( wp_unslash( $_GET['key'] ) ) : ''; // WPCS: input var ok, CSRF ok.
$order = wc_get_order( $order_id );
$order_key = isset( $_GET['key'] ) ? wc_clean( wp_unslash( $_GET['key'] ) ) : ''; // WPCS: input var ok, CSRF ok.
$order = wc_get_order( $order_id );
$hold_stock_minutes = (int) get_option( 'woocommerce_hold_stock_minutes', 0 );
// Order or payment link is invalid.
if ( ! $order || $order->get_id() !== $order_id || ! hash_equals( $order->get_order_key(), $order_key ) ) {
@ -157,7 +158,7 @@ class WC_Shortcode_Checkout {
}
// Check stock based on all items in the cart and consider any held stock within pending orders.
$held_stock = wc_get_held_stock_quantity( $product, $order->get_id() );
$held_stock = ( $hold_stock_minutes > 0 ) ? wc_get_held_stock_quantity( $product, $order->get_id() ) : 0;
$required_stock = $quantities[ $product->get_stock_managed_by_id() ];
if ( ! apply_filters( 'woocommerce_pay_order_product_has_enough_stock', ( $product->get_stock_quantity() >= ( $held_stock + $required_stock ) ), $product, $order ) ) {

View File

@ -299,80 +299,33 @@ function wc_increase_stock_levels( $order_id ) {
* @param integer $exclude_order_id Order ID to exclude.
* @return int
*/
function wc_get_held_stock_quantity( WC_Product $product, $exclude_order_id = 0 ) {
/**
* Filter: woocommerce_hold_stock_for_checkout
* Allows enable/disable hold stock functionality on checkout.
*
* @since 4.1.0
* @param bool $enabled Default to true if managing stock globally.
*/
if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', wc_string_to_bool( get_option( 'woocommerce_manage_stock', 'yes' ) ) ) ) {
return 0;
}
function wc_get_held_stock_quantity( $product, $exclude_order_id = 0 ) {
global $wpdb;
return ( new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() )->get_reserved_stock( $product, $exclude_order_id );
return $wpdb->get_var(
$wpdb->prepare(
"
SELECT SUM( order_item_meta.meta_value ) AS held_qty
FROM {$wpdb->posts} AS posts
LEFT JOIN {$wpdb->postmeta} as postmeta ON posts.ID = postmeta.post_id
LEFT JOIN {$wpdb->prefix}woocommerce_order_items as order_items ON posts.ID = order_items.order_id
LEFT JOIN {$wpdb->prefix}woocommerce_order_itemmeta as order_item_meta ON order_items.order_item_id = order_item_meta.order_item_id
LEFT JOIN {$wpdb->prefix}woocommerce_order_itemmeta as order_item_meta2 ON order_items.order_item_id = order_item_meta2.order_item_id
WHERE order_item_meta.meta_key = '_qty'
AND order_item_meta2.meta_key = %s
AND order_item_meta2.meta_value = %d
AND postmeta.meta_key = '_created_via'
AND postmeta.meta_value = 'checkout'
AND posts.post_type IN ( '" . implode( "','", wc_get_order_types() ) . "' )
AND posts.post_status = 'wc-pending'
AND posts.ID != %d;",
'product_variation' === get_post_type( $product->get_stock_managed_by_id() ) ? '_variation_id' : '_product_id',
$product->get_stock_managed_by_id(),
$exclude_order_id
)
); // WPCS: unprepared SQL ok.
}
/**
* Hold stock for an order.
*
* @throws ReserveStockException If reserve stock fails.
*
* @since 4.1.0
* @param \WC_Order|int $order Order ID or instance.
*/
function wc_reserve_stock_for_order( $order ) {
/**
* Filter: woocommerce_hold_stock_for_checkout
* Allows enable/disable hold stock functionality on checkout.
*
* @since @since 4.1.0
* @param bool $enabled Default to true if managing stock globally.
*/
if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', wc_string_to_bool( get_option( 'woocommerce_manage_stock', 'yes' ) ) ) ) {
return;
}
$order = $order instanceof WC_Order ? $order : wc_get_order( $order );
if ( $order ) {
( new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() )->reserve_stock_for_order( $order );
}
}
add_action( 'woocommerce_checkout_order_created', 'wc_reserve_stock_for_order' );
/**
* Release held stock for an order.
*
* @since 4.1.0
* @param \WC_Order|int $order Order ID or instance.
*/
function wc_release_stock_for_order( $order ) {
/**
* Filter: woocommerce_hold_stock_for_checkout
* Allows enable/disable hold stock functionality on checkout.
*
* @since 4.1.0
* @param bool $enabled Default to true if managing stock globally.
*/
if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', wc_string_to_bool( get_option( 'woocommerce_manage_stock', 'yes' ) ) ) ) {
return;
}
$order = $order instanceof WC_Order ? $order : wc_get_order( $order );
if ( $order ) {
( new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() )->release_stock_for_order( $order );
}
}
add_action( 'woocommerce_checkout_order_exception', 'wc_release_stock_for_order' );
add_action( 'woocommerce_payment_complete', 'wc_release_stock_for_order', 11 );
add_action( 'woocommerce_order_status_cancelled', 'wc_release_stock_for_order', 11 );
add_action( 'woocommerce_order_status_completed', 'wc_release_stock_for_order', 11 );
add_action( 'woocommerce_order_status_processing', 'wc_release_stock_for_order', 11 );
add_action( 'woocommerce_order_status_on-hold', 'wc_release_stock_for_order', 11 );
/**
* Return low stock amount to determine if notification needs to be sent
*

View File

@ -26,7 +26,7 @@
<!-- Configs -->
<config name="minimum_supported_wp_version" value="5.0" />
<config name="testVersion" value="5.6-" />
<config name="testVersion" value="7.0-" />
<!-- Rules -->
<rule ref="WooCommerce-Core" />

View File

@ -1,176 +0,0 @@
<?php
/**
* Handle product stock reservation during checkout.
*
* @package Automattic/WooCommerce
*/
namespace Automattic\WooCommerce\Checkout\Helpers;
defined( 'ABSPATH' ) || exit;
/**
* Stock Reservation class.
*/
final class ReserveStock {
/**
* Query for any existing holds on stock for this item.
*
* @param \WC_Product $product Product to get reserved stock for.
* @param integer $exclude_order_id Optional order to exclude from the results.
*
* @return integer Amount of stock already reserved.
*/
public function get_reserved_stock( \WC_Product $product, $exclude_order_id = 0 ) {
global $wpdb;
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
return (int) $wpdb->get_var( $this->get_query_for_reserved_stock( $product->get_stock_managed_by_id(), $exclude_order_id ) );
}
/**
* Put a temporary hold on stock for an order if enough is available.
*
* @throws ReserveStockException If stock cannot be reserved.
*
* @param \WC_Order $order Order object.
* @param int $minutes How long to reserve stock in minutes. Defaults to woocommerce_hold_stock_minutes.
*/
public function reserve_stock_for_order( \WC_Order $order, $minutes = 0 ) {
$minutes = $minutes ? $minutes : (int) get_option( 'woocommerce_hold_stock_minutes', 60 );
if ( ! $minutes ) {
return;
}
try {
$items = array_filter(
$order->get_items(),
function( $item ) {
return $item->is_type( 'line_item' ) && $item->get_product() instanceof \WC_Product && $item->get_quantity() > 0;
}
);
$rows = array();
foreach ( $items as $item ) {
$product = $item->get_product();
if ( ! $product->is_in_stock() ) {
throw new ReserveStockException(
'woocommerce_product_out_of_stock',
sprintf(
/* translators: %s: product name */
__( '&quot;%s&quot; is out of stock and cannot be purchased.', 'woocommerce' ),
$product->get_name()
),
403
);
}
// If stock management is off, no need to reserve any stock here.
if ( ! $product->managing_stock() || $product->backorders_allowed() ) {
continue;
}
$managed_by_id = $product->get_stock_managed_by_id();
$rows[ $managed_by_id ] = isset( $rows[ $managed_by_id ] ) ? $rows[ $managed_by_id ] + $item->get_quantity() : $item->get_quantity();
}
if ( ! empty( $rows ) ) {
foreach ( $rows as $product_id => $quantity ) {
$this->reserve_stock_for_product( $product_id, $quantity, $order, $minutes );
}
}
} catch ( ReserveStockException $e ) {
$this->release_stock_for_order( $order );
throw $e;
}
}
/**
* Release a temporary hold on stock for an order.
*
* @param \WC_Order $order Order object.
*/
public function release_stock_for_order( \WC_Order $order ) {
global $wpdb;
$wpdb->delete(
$wpdb->wc_reserved_stock,
array(
'order_id' => $order->get_id(),
)
);
}
/**
* Reserve stock for a product by inserting rows into the DB.
*
* @throws ReserveStockException If a row cannot be inserted.
*
* @param int $product_id Product ID which is having stock reserved.
* @param int $stock_quantity Stock amount to reserve.
* @param \WC_Order $order Order object which contains the product.
* @param int $minutes How long to reserve stock in minutes.
*/
private function reserve_stock_for_product( $product_id, $stock_quantity, \WC_Order $order, $minutes ) {
global $wpdb;
$product_data_store = \WC_Data_Store::load( 'product' );
$query_for_stock = $product_data_store->get_query_for_stock( $product_id );
$query_for_reserved_stock = $this->get_query_for_reserved_stock( $product_id, $order->get_id() );
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
$result = $wpdb->query(
$wpdb->prepare(
"
REPLACE INTO {$wpdb->wc_reserved_stock} ( order_id, product_id, stock_quantity, expires )
SELECT %d, %d, %d, ( NOW() + INTERVAL %d MINUTE ) from DUAL
WHERE ( $query_for_stock FOR UPDATE ) - ( $query_for_reserved_stock FOR UPDATE ) >= %d
",
$order->get_id(),
$product_id,
$stock_quantity,
$minutes,
$stock_quantity
)
);
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
if ( ! $result ) {
$product = wc_get_product( $product_id );
throw new ReserveStockException(
'woocommerce_product_not_enough_stock',
sprintf(
/* translators: %s: product name */
__( 'Not enough units of %s are available in stock to fulfil this order.', 'woocommerce' ),
$product ? $product->get_name() : '#' . $product_id
),
403
);
}
}
/**
* Returns query statement for getting reserved stock of a product.
*
* @param int $product_id Product ID.
* @param integer $exclude_order_id Optional order to exclude from the results.
* @return string|void Query statement.
*/
private function get_query_for_reserved_stock( $product_id, $exclude_order_id = 0 ) {
global $wpdb;
return $wpdb->prepare(
"
SELECT COALESCE( SUM( stock_table.`stock_quantity` ), 0 ) FROM $wpdb->wc_reserved_stock stock_table
LEFT JOIN $wpdb->posts posts ON stock_table.`order_id` = posts.ID
WHERE posts.post_status IN ( 'wc-checkout-draft', 'wc-pending' )
AND stock_table.`expires` > NOW()
AND stock_table.`product_id` = %d
AND stock_table.`order_id` != %d
",
$product_id,
$exclude_order_id
);
}
}

View File

@ -1,62 +0,0 @@
<?php
/**
* Exceptions for stock reservation.
*
* @package Automattic/WooCommerce
*/
namespace Automattic\WooCommerce\Checkout\Helpers;
defined( 'ABSPATH' ) || exit;
/**
* ReserveStockException class.
*/
class ReserveStockException extends \Exception {
/**
* Sanitized error code.
*
* @var string
*/
protected $error_code;
/**
* Error extra data.
*
* @var array
*/
protected $error_data;
/**
* Setup exception.
*
* @param string $code Machine-readable error code, e.g `woocommerce_invalid_product_id`.
* @param string $message User-friendly translated error message, e.g. 'Product ID is invalid'.
* @param int $http_status_code Proper HTTP status code to respond with, e.g. 400.
* @param array $data Extra error data.
*/
public function __construct( $code, $message, $http_status_code = 400, $data = array() ) {
$this->error_code = $code;
$this->error_data = $data;
parent::__construct( $message, $http_status_code );
}
/**
* Returns the error code.
*
* @return string
*/
public function getErrorCode() {
return $this->error_code;
}
/**
* Returns error data.
*
* @return array
*/
public function getErrorData() {
return $this->error_data;
}
}

View File

@ -2,7 +2,7 @@
This directory is home to new WooCommerce class files under the \Automattic\WooCommerce\ namespace using PSR-4 file naming. This is to take full advantage of autoloading.
Currently, these classes have a PHP 5.6 requirement. No required core classes will be added here until this PHP version is enforced. If running an older version of PHP, these class files will not be used.
Currently, these classes have a PHP 7.0 requirement. No required core classes will be added here until this PHP version is enforced. If running an older version of PHP, these class files will not be used.
## Installing Composer

View File

@ -66,8 +66,18 @@ describe( 'Store owner can go through setup Task List', () => {
page.waitForNavigation( { waitUntil: 'networkidle0' } ),
] );
// Query for store location fields
const storeLocationFields = await page.$$( '.components-text-control__input' );
expect( storeLocationFields ).toHaveLength( 4 );
// Wait for "Continue" button to become active
await page.waitForSelector( 'button.is-primary:not(:disabled)' );
// Click on "Continue" button to move to the shipping cost section
await page.click( 'button.is-primary' );
// Wait for "Proceed" button to become active
await page.waitForSelector( 'button.is-primary:not(:disabled)' );
await page.waitFor( 3000 );
// Click on "Proceed" button to save shipping settings
await page.click( 'button.is-primary' );

View File

@ -9,8 +9,9 @@
* Class WC_Checkout
*/
class WC_Tests_Checkout extends WC_Unit_Test_Case {
/**
* TearDown.
* TearDown for tests.
*/
public function tearDown() {
parent::tearDown();
@ -18,7 +19,7 @@ class WC_Tests_Checkout extends WC_Unit_Test_Case {
}
/**
* Setup.
* Setup for tests.
*/
public function setUp() {
parent::setUp();
@ -31,24 +32,24 @@ class WC_Tests_Checkout extends WC_Unit_Test_Case {
* @throws Exception When unable to create order.
*/
public function test_create_order_with_limited_coupon() {
$coupon_code = 'coupon4one';
$coupon_code = 'coupon4one';
$coupon_data_store = WC_Data_Store::load( 'coupon' );
$coupon = WC_Helper_Coupon::create_coupon(
$coupon = WC_Helper_Coupon::create_coupon(
$coupon_code,
array( 'usage_limit' => 1 )
);
$product = WC_Helper_Product::create_simple_product( true );
$product = WC_Helper_Product::create_simple_product( true );
WC()->cart->add_to_cart( $product->get_id(), 1 );
WC()->cart->add_discount( $coupon->get_code() );
$checkout = WC_Checkout::instance();
$order_id = $checkout->create_order(
array(
'billing_email' => 'a@b.com',
'billing_email' => 'a@b.com',
'payment_method' => 'dummy_payment_gateway',
)
);
$this->assertNotWPError( $order_id );
$order = new WC_Order( $order_id );
$order = new WC_Order( $order_id );
$coupon_held_key = $order->get_data_store()->get_coupon_held_keys( $order );
$this->assertEquals( count( $coupon_held_key ), 1 );
$this->assertEquals( array_keys( $coupon_held_key )[0], $coupon->get_id() );
@ -60,7 +61,7 @@ class WC_Tests_Checkout extends WC_Unit_Test_Case {
WC()->cart->add_discount( $coupon->get_code() );
$order2_id = $checkout->create_order(
array(
'billing_email' => 'a@c.com',
'billing_email' => 'a@c.com',
'payment_method' => 'dummy_payment_gateway',
)
);
@ -74,8 +75,8 @@ class WC_Tests_Checkout extends WC_Unit_Test_Case {
* @throws Exception When unable to create an order.
*/
public function test_create_order_with_multiple_limited_coupons() {
$coupon_code1 = 'coupon1';
$coupon_code2 = 'coupon2';
$coupon_code1 = 'coupon1';
$coupon_code2 = 'coupon2';
$coupon_data_store = WC_Data_Store::load( 'coupon' );
$coupon1 = WC_Helper_Coupon::create_coupon(
@ -90,10 +91,10 @@ class WC_Tests_Checkout extends WC_Unit_Test_Case {
WC()->cart->add_to_cart( $product->get_id(), 1 );
WC()->cart->add_discount( $coupon_code1 );
WC()->cart->add_discount( $coupon_code2 );
$checkout = WC_Checkout::instance();
$checkout = WC_Checkout::instance();
$order_id1 = $checkout->create_order(
array(
'billing_email' => 'a@b.com',
'billing_email' => 'a@b.com',
'payment_method' => 'dummy_payment_gateway',
)
);
@ -109,7 +110,7 @@ class WC_Tests_Checkout extends WC_Unit_Test_Case {
$order2_id = $checkout->create_order(
array(
'billing_email' => 'a@b.com',
'billing_email' => 'a@b.com',
'payment_method' => 'dummy_payment_gateway',
)
);
@ -187,120 +188,4 @@ class WC_Tests_Checkout extends WC_Unit_Test_Case {
return 0.01;
}
/**
* Helper method to create a managed product and a order for that product.
*
* @return array
* @throws Exception When unable to create an order .
*/
protected function create_order_for_managed_inventory_product() {
$product = WC_Helper_Product::create_simple_product();
$product->set_props( array( 'manage_stock' => true ) );
$product->set_stock_quantity( 10 );
$product->save();
WC()->cart->add_to_cart( $product->get_id(), 9 );
$this->assertEquals( true, WC()->cart->check_cart_items() );
$checkout = WC_Checkout::instance();
$order_id = $checkout->create_order(
array(
'payment_method' => 'cod',
'billing_email' => 'a@b.com',
)
);
// Assertions whether the order was created successfully.
$this->assertNotWPError( $order_id );
$order = wc_get_order( $order_id );
return array( $product, $order );
}
/**
* Test when order is out stock because it is held by an order in pending status.
*
* @throws Exception When unable to create order.
*/
public function test_create_order_when_out_of_stock() {
list( $product, $order ) = $this->create_order_for_managed_inventory_product();
$this->assertEquals( 9, $order->get_item_count() );
$this->assertEquals( 'pending', $order->get_status() );
$this->assertEquals( 9, wc_get_held_stock_quantity( $product ) );
WC()->cart->empty_cart();
WC()->cart->add_to_cart( $product->get_stock_managed_by_id(), 2 );
$this->assertEquals( false, WC()->cart->check_cart_items() );
}
/**
* Test if pending stock is cleared when order is cancelled.
*
* @throws Exception When unable to create order.
*/
public function test_pending_is_cleared_when_order_is_cancelled() {
list( $product, $order ) = $this->create_order_for_managed_inventory_product();
$this->assertEquals( 9, wc_get_held_stock_quantity( $product ) );
$order->set_status( 'cancelled' );
$order->save();
$this->assertEquals( 0, wc_get_held_stock_quantity( $product ) );
$this->assertEquals( 10, $product->get_stock_quantity() );
}
/**
* Test if pending stock is cleared when order is processing.
*
* @throws Exception When unable to create order.
*/
public function test_pending_is_cleared_when_order_processed() {
list( $product, $order ) = $this->create_order_for_managed_inventory_product();
$this->assertEquals( 9, wc_get_held_stock_quantity( $product ) );
$order->set_status( 'processing' );
$order->save();
$this->assertEquals( 0, wc_get_held_stock_quantity( $product ) );
}
/**
* Test creating order from managed stock for variable product.
*
* @throws Exception When unable to create an order.
*/
public function test_create_order_for_variation_product() {
$parent_product = WC_Helper_Product::create_variation_product();
$variation = $parent_product->get_available_variations()[0];
$variation = wc_get_product( $variation['variation_id'] );
$variation->set_manage_stock( true );
$variation->set_stock_quantity( 10 );
$variation->save();
WC()->cart->add_to_cart( $variation->get_id(), 9 );
$this->assertEquals( true, WC()->cart->check_cart_items() );
$checkout = WC_Checkout::instance();
$order_id = $checkout->create_order(
array(
'payment_method' => 'cod',
'billing_email' => 'a@b.com',
)
);
// Assertions whether the first order was created successfully.
$this->assertNotWPError( $order_id );
$order = wc_get_order( $order_id );
$this->assertEquals( 9, $order->get_item_count() );
$this->assertEquals( 'pending', $order->get_status() );
$this->assertEquals( 9, wc_get_held_stock_quantity( $variation ) );
WC()->cart->empty_cart();
WC()->cart->add_to_cart( $variation->get_stock_managed_by_id(), 2 );
$this->assertEquals( false, WC()->cart->check_cart_items() );
}
}

View File

@ -8,7 +8,7 @@
/**
* Class WC_Tests_Order.
*/
class WC_Tests_Orders extends WC_Unit_Test_Case {
class WC_Tests_Order extends WC_Unit_Test_Case {
/**
* Test for total when round at subtotal is enabled.

View File

@ -10,17 +10,6 @@
*/
class WC_Tests_Packages extends WC_Unit_Test_Case {
/**
* Setup test class.
*
* @return void
*/
public function setUp() {
if ( version_compare( PHP_VERSION, '5.6.0', '<' ) ) {
$this->markTestSkipped( 'Packages are disabled unless running PHP 5.6+' );
}
}
/**
* Test packages exist - this requires composer install to have ran.
*/

View File

@ -276,19 +276,20 @@ class WC_Tests_Product_Data extends WC_Unit_Test_Case {
public function test_get_image_should_return_product_image() {
$product = new WC_Product();
$image = $this->set_product_image( $product );
$needle = 'width="186" height="144" src="' . $image['url'] . '" class="%s"';
$this->assertEquals(
'<img width="186" height="144" src="' . $image['url'] . '" class="attachment-woocommerce_thumbnail size-woocommerce_thumbnail" alt="" />',
$this->assertContains(
sprintf( $needle, 'attachment-woocommerce_thumbnail size-woocommerce_thumbnail' ),
$product->get_image()
);
$this->assertEquals(
'<img width="186" height="144" src="' . $image['url'] . '" class="attachment-single size-single" alt="" />',
$this->assertContains(
sprintf( $needle, 'attachment-single size-single' ),
$product->get_image( 'single' )
);
$this->assertEquals(
'<img width="186" height="144" src="' . $image['url'] . '" class="custom-class" alt="" />',
$this->assertContains(
sprintf( $needle, 'custom-class' ),
$product->get_image( 'single', array( 'class' => 'custom-class' ) )
);
@ -303,19 +304,20 @@ class WC_Tests_Product_Data extends WC_Unit_Test_Case {
$variations = $variable_product->get_children();
$variation_1 = wc_get_product( $variations[0] );
$image = $this->set_product_image( $variable_product );
$needle = 'width="186" height="144" src="' . $image['url'] . '" class="%s"';
$this->assertEquals(
'<img width="186" height="144" src="' . $image['url'] . '" class="attachment-woocommerce_thumbnail size-woocommerce_thumbnail" alt="" />',
$this->assertContains(
sprintf( $needle, 'attachment-woocommerce_thumbnail size-woocommerce_thumbnail' ),
$variation_1->get_image()
);
$this->assertContains(
'<img width="186" height="144" src="' . $image['url'] . '" class="attachment-single size-single" alt="" />',
sprintf( $needle, 'attachment-single size-single' ),
$variation_1->get_image( 'single' )
);
$this->assertEquals(
'<img width="186" height="144" src="' . $image['url'] . '" class="custom-class" alt="" />',
$this->assertContains(
sprintf( $needle, 'custom-class' ),
$variation_1->get_image( 'single', array( 'class' => 'custom-class' ) )
);

View File

@ -18,24 +18,14 @@ if ( ! defined( 'WC_PLUGIN_FILE' ) ) {
define( 'WC_PLUGIN_FILE', __FILE__ );
}
/**
* Load core packages and the autoloader.
*
* The new packages and autoloader require PHP 5.6+. If this dependency is not met, do not include them. Users will be warned
* that they are using an older version of PHP. WooCommerce will continue to load, but some functionality such as the REST API
* and Blocks will be missing.
*
* This requirement will be enforced in future versions of WooCommerce.
*/
if ( version_compare( PHP_VERSION, '5.6.0', '>=' ) ) {
require __DIR__ . '/src/Autoloader.php';
require __DIR__ . '/src/Packages.php';
// Load core packages and the autoloader.
require __DIR__ . '/src/Autoloader.php';
require __DIR__ . '/src/Packages.php';
if ( ! \Automattic\WooCommerce\Autoloader::init() ) {
return;
}
\Automattic\WooCommerce\Packages::init();
if ( ! \Automattic\WooCommerce\Autoloader::init() ) {
return;
}
\Automattic\WooCommerce\Packages::init();
// Include the main WooCommerce class.
if ( ! class_exists( 'WooCommerce', false ) ) {