diff --git a/.plugin-data b/.plugin-data index bdb2af6..86df32b 100644 --- a/.plugin-data +++ b/.plugin-data @@ -1,4 +1,4 @@ { - "version": "6.8.0", + "version": "6.9.0", "slug": "shopping-feed" } diff --git a/readme.md b/readme.md index 73841e9..d12ba45 100644 --- a/readme.md +++ b/readme.md @@ -2,19 +2,23 @@ * Contributors: ShoppingFeed, BeAPI * Tags: shoppingfeed, marketplace, woocommerce, woocommerce shoppingfeed, create woocommerce products shoppingfeed, products feed, generate shoppingfeed, amazon, Jet, Walmart, many marketplace, import orders -* Stable tag: 6.8.0 -* Version: 6.8.0 +* Stable tag: 6.9.0 +* Version: 6.9.0 * Requires PHP: 7.3 * Requires at least: 5.7 -* Tested up to: 6.5 +* Tested up to: 6.7 * WC requires at least: 5.1.0 -* WC tested up to: 8.8 +* WC tested up to: 9.4.3 ## Upgrade Notice > Version 6.0.0 is a major version, there are several changes and improvements which affect the architecture of the plugin. You will have to re-configure the plugin, all the previous settings will be lost ## Changelog +* 6.9.0 + * Feed : Fix attributes not use in variations missing in the feed. + * Feed : Dimension data are correctly included in the feed. + * Orders : Fix invalid timestamp when scheduling async task to acknowledge orders. * 6.8.0 * Feed : Fix the promotion date * 6.7.0 diff --git a/readme.txt b/readme.txt index 1133ff3..c218254 100644 --- a/readme.txt +++ b/readme.txt @@ -1,20 +1,24 @@ ## ShoppingFeed Contributors: ShoppingFeed, BeAPI Tags: shoppingfeed, marketplace, woocommerce, woocommerce shoppingfeed, create woocommerce products shoppingfeed, products feed, generate shoppingfeed, amazon, Jet, Walmart, many marketplace, import orders -Stable tag: 6.8.0 -Version: 6.8.0 +Stable tag: 6.9.0 +Version: 6.9.0 Requires PHP: 7.3 Requires at least: 5.7 -Tested up to: 6.5 +Tested up to: 6.7 WC requires at least: 5.1.0 -WC tested up to: 8.8 +WC tested up to: 9.4.3 == Upgrade Notice == Version 6.0.0 is a major version, there are several changes and improvements which affect the architecture of the plugin. You will have to re-configure the plugin, all the previous settings will be lost == Changelog == +* 6.9.0 + * Feed : Fix attributes not use in variations missing in the feed. + * Feed : Dimension data are correctly included in the feed. + * Orders : Fix invalid timestamp when scheduling async task to acknowledge orders. * 6.8.0 - * Feed : Fix the promotion date + * Feed : Fix the promotion date. * 6.7.0 * Orders : The 'buyer_identification_number' field is imported in an order custom field if it exists. * Orders : Product updates (price and stock) via the SF API are made asynchronously via a scheduled task. diff --git a/shoppingfeed.php b/shoppingfeed.php index 5947c88..d0dc9a7 100644 --- a/shoppingfeed.php +++ b/shoppingfeed.php @@ -7,7 +7,7 @@ * Author URI: https://www.shopping-feed.com/ * Text Domain: shopping-feed * Domain Path: /languages - * Version: 6.8.0 + * Version: 6.9.0 * Requires at least: 5.7 * Requires PHP: 7.3 * WC requires at least: 5.1.0 @@ -26,7 +26,7 @@ require_once plugin_dir_path( __FILE__ ) . '/vendor/autoload.php'; } -define( 'SF_VERSION', '6.8.0' ); +define( 'SF_VERSION', '6.9.0' ); define( 'SF_DB_VERSION_SLUG', 'SF_DB_VERSION' ); define( 'SF_DB_VERSION', '1.0.0' ); define( 'SF_UPGRADE_RUNNING', 'SF_UPGRADE_RUNNING' ); diff --git a/src/Feed/Generator.php b/src/Feed/Generator.php index ae17000..b0a55bd 100644 --- a/src/Feed/Generator.php +++ b/src/Feed/Generator.php @@ -142,12 +142,23 @@ function ( $product->setWeight( (float) $sf_product->get_weight() ); } + if ( ! empty( $sf_product->get_length() ) ) { + $product->setAttribute( 'length', (string) $sf_product->get_length() ); + } + + if ( ! empty( $sf_product->get_width() ) ) { + $product->setAttribute( 'width', (string) $sf_product->get_width() ); + } + + if ( ! empty( $sf_product->get_height() ) ) { + $product->setAttribute( 'height', (string) $sf_product->get_height() ); + } + if ( ! empty( $sf_product->get_category_name() ) ) { $product->setCategory( $sf_product->get_category_name(), $sf_product->get_category_link() ); } - // For variable products, don't include attributes. They will be available in the variations. - if ( ! $sf_product->has_variations() && ! empty( $sf_product->get_attributes() ) ) { + if ( ! empty( $sf_product->get_attributes() ) ) { $product->setAttributes( $sf_product->get_attributes() ); } @@ -213,6 +224,15 @@ function ( if ( ! empty( $variation_images ) ) { $variation->setAdditionalImages( $variation_images ); } + if ( ! empty( $sf_product_variation['width'] ) ) { + $variation->setAttribute( 'width', (string) $sf_product_variation['width'] ); + } + if ( ! empty( $sf_product_variation['length'] ) ) { + $variation->setAttribute( 'length', (string) $sf_product_variation['length'] ); + } + if ( ! empty( $sf_product_variation['height'] ) ) { + $variation->setAttribute( 'height', (string) $sf_product_variation['height'] ); + } } } ); diff --git a/src/Orders/Operations.php b/src/Orders/Operations.php index fe0c7ad..7de1452 100644 --- a/src/Orders/Operations.php +++ b/src/Orders/Operations.php @@ -205,7 +205,7 @@ public static function acknowledge_order( $order_id, $message = '' ) { if ( false === $ok ) { //if we cant acknowledge order => add action after 15 min as_schedule_single_action( - MINUTE_IN_SECONDS * 15, + time() + ( 15 * MINUTE_IN_SECONDS ), 'sf_acknowledge_remain_order', array( $order_id, diff --git a/src/Products/Product.php b/src/Products/Product.php index 50e6c85..f5917b5 100644 --- a/src/Products/Product.php +++ b/src/Products/Product.php @@ -35,6 +35,21 @@ class Product { */ private $weight; + /** + * @var string + */ + private $length; + + /** + * @var string + */ + private $width; + + /** + * @var string + */ + private $height; + /** * @var bool|mixed|\WP_Term */ @@ -52,6 +67,9 @@ public function __construct( $product ) { $this->brand = $this->set_brand(); $this->category = $this->set_category(); $this->weight = $this->product->get_weight(); + $this->length = $this->product->get_length(); + $this->width = $this->product->get_width(); + $this->height = $this->product->get_height(); } /** @@ -225,6 +243,39 @@ public function get_weight() { return $this->weight; } + /** + * @return string + */ + public function get_length() { + if ( empty( $this->length ) ) { + return ''; + } + + return $this->length; + } + + /** + * @return string + */ + public function get_width() { + if ( empty( $this->width ) ) { + return ''; + } + + return $this->width; + } + + /** + * @return string + */ + public function get_height() { + if ( empty( $this->height ) ) { + return ''; + } + + return $this->height; + } + /** * @return string */ @@ -327,7 +378,15 @@ public function get_attributes() { $wc_attributes = $this->product->get_attributes(); - $attributes = array(); + if ( 'variable' === $this->product->get_type() && is_array( $wc_attributes ) && ! empty( $wc_attributes ) ) { + foreach ( $wc_attributes as $key => $attribute ) { + if ( $attribute->get_variation() ) { + unset( $wc_attributes[ $key ] ); + } + } + } + + $attributes = []; if ( ! empty( $wc_attributes ) ) { foreach ( $wc_attributes as $taxonomy => $attribute_obj ) { $attribute = reset( $attribute_obj ); @@ -415,6 +474,9 @@ public function get_variations( $for_feed = false ) { $variation_data['quantity'] = $this->_get_quantity( $variation ); $variation_data['price'] = ! is_null( $variation->get_regular_price() ) ? $variation->get_regular_price() : $variation->get_price(); $variation_data['discount'] = $variation->is_on_sale() ? $variation->get_sale_price() : 0; + $variation_data['width'] = $variation->get_width(); + $variation_data['height'] = $variation->get_height(); + $variation_data['length'] = $variation->get_length(); if ( ! empty( get_the_post_thumbnail_url( $variation->get_id(), 'full' ) ) ) { $variation_data['image_main'] = get_the_post_thumbnail_url( $variation->get_id(), 'full' ); } diff --git a/tests/wpunit/Feed/ProductFeedTest.php b/tests/wpunit/Feed/ProductFeedTest.php index 54876b3..1a67c70 100644 --- a/tests/wpunit/Feed/ProductFeedTest.php +++ b/tests/wpunit/Feed/ProductFeedTest.php @@ -5,6 +5,7 @@ use ShoppingFeed\ShoppingFeedWC\Products\Product; use ShoppingFeed\ShoppingFeedWC\Products\Products; use ShoppingFeed\ShoppingFeedWC\ShoppingFeedHelper; +use ShoppingFeed\ShoppingFeedWC\Tests\wpunit\WC_Helper_Product; class ProductFeedTest extends \Codeception\TestCase\WPTestCase { /** @@ -129,13 +130,13 @@ function ( $value ) { public function test_get_products_for_feed_query_args() { $products_query_args = Products::get_instance()->get_list_args(); $this->assertEqualSets( - array( + [ 'limit' => - 1, 'orderby' => 'date', 'order' => 'DESC', 'status' => 'publish', 'stock_status' => 'instock', - ), + ], $products_query_args ); @@ -150,23 +151,27 @@ function ( $value ) { $products_query_args = Products::get_instance()->get_list_args(); $this->assertEqualSets( - array( + [ 'limit' => - 1, 'orderby' => 'date', 'order' => 'DESC', 'status' => 'publish', 'stock_status' => [ 'instock', 'outofstock' ], - ), + ], $products_query_args ); } + /** + * @covers + */ + /** * @covers \ShoppingFeed\ShoppingFeedWC\Products\Product::get_quantity */ public function test_get_product_quantity_instock() { $wc_product = wc_get_product( 13 ); - $wc_product->set_stock_status('instock'); + $wc_product->set_stock_status( 'instock' ); $wc_product->save(); $p = new Product( wc_get_product( 13 ) ); $this->assertEquals( ShoppingFeedHelper::get_default_product_quantity(), $p->get_quantity() ); @@ -177,7 +182,7 @@ public function test_get_product_quantity_instock() { */ public function test_get_product_quantity_outofstock() { $wc_product = wc_get_product( 13 ); - $wc_product->set_stock_status('outofstock'); + $wc_product->set_stock_status( 'outofstock' ); $wc_product->save(); $p = new Product( wc_get_product( 13 ) ); @@ -209,4 +214,176 @@ public function test_get_product_quantity_outofstock_manage_stock() { $p = new Product( wc_get_product( 13 ) ); $this->assertEquals( 0, $p->get_quantity() ); } + + /** + * @covers \ShoppingFeed\ShoppingFeedWC\Products\Product::get_attributes + * + * @author Stéphane Gillot, Clément Boirie + */ + public function test_attribute_on_variable_product_is_applied_to_variations() { + // Prepare the attribute object + $attribute = new \WC_Product_Attribute(); + $attribute->set_name( 'material' ); + $attribute->set_options( [ 'Coton', 'Linen' ] ); + $attribute->set_variation( 'true' ); + $attribute->set_visible( 'true' ); + + // Prepare the variable product object + $wc_variable_product = WC_Helper_Product::create_variation_product(); + $wc_variable_product->set_attributes( [ $attribute ] ); + $wc_variable_product->save(); + + // Prepare the sf product object + $sf_product = new Product( $wc_variable_product->get_id() ); + + $this->assertEquals( [], $sf_product->get_attributes() ); + } + + /** + * @covers \ShoppingFeed\ShoppingFeedWC\Products\Product::get_attributes + * + * @author Stéphane Gillot, Clément Boirie + */ + public function test_attribute_on_variable_product_is_not_applied_to_variations() { + // Prepare the attribute object + $attribute = new \WC_Product_Attribute(); + $attribute->set_name( 'material' ); + $attribute->set_options( [ 'Coton', 'Linen' ] ); + $attribute->set_variation( 'false' ); + $attribute->set_visible( 'true' ); + + // Prepare the variable product object + $wc_variable_product = WC_Helper_Product::create_variation_product(); + $wc_variable_product->set_attributes( [ $attribute ] ); + $wc_variable_product->save(); + + // Prepare the sf product object + $sf_product = new Product( $wc_variable_product ); + + $this->assertEquals( [ 'material' => 'Coton,Linen' ], $sf_product->get_attributes() ); + } + + /** + * @covers \ShoppingFeed\ShoppingFeedWC\Products\Product::get_attributes + * + * @author Stéphane Gillot, Clément Boirie + */ + public function test_attribute_on_simple_product_exists() { + // Prepare the attribute object + $attribute = new \WC_Product_Attribute(); + $attribute->set_name( 'material' ); + $attribute->set_options( [ 'Coton' ] ); + $attribute->set_visible( 'true' ); + + // Prepare the variable product object + $wc_simple_product = WC_Helper_Product::create_simple_product(); + $wc_simple_product->set_attributes( [ $attribute ] ); + $wc_simple_product->save(); + + // Prepare the sf product object + $sf_product = new Product( $wc_simple_product ); + + $this->assertEquals( [ 'material' => 'Coton' ], $sf_product->get_attributes() ); + } + + /** + * @covers \ShoppingFeed\ShoppingFeedWC\Products\Product::get_attributes + * + * @author Stéphane Gillot, Clément Boirie + */ + public function test_attribute_on_simple_product_does_not_exist() { + // Prepare the variable product object + $wc_simple_product = WC_Helper_Product::create_simple_product(); + + // Prepare the sf product object + $sf_product = new Product( $wc_simple_product ); + + $this->assertEquals( [], $sf_product->get_attributes() ); + } + + /** + * @covers \ShoppingFeed\ShoppingFeedWC\Products\Product::get_length + * @covers \ShoppingFeed\ShoppingFeedWC\Products\Product::get_height + * @covers \ShoppingFeed\ShoppingFeedWC\Products\Product::get_width + * + * @author Stéphane Gillot + */ + public function test_get_simple_product_dimensions_when_defined(){ + $wc_product = WC_Helper_Product::create_simple_product(); + $wc_product->set_length( 5 ); + $wc_product->set_height( 10 ); + $wc_product->set_width( 15 ); + $wc_product->save(); + + $p = new Product( $wc_product->get_id() ); + + $this->assertEquals( 5, $p->get_length(), 'Product length should be 5.' ); + $this->assertEquals( 10, $p->get_height(), 'Product height should be 10.' ); + $this->assertEquals( 15, $p->get_width(), 'Product width should be 15.' ); + } + + /** + * @covers \ShoppingFeed\ShoppingFeedWC\Products\Product::get_length + * @covers \ShoppingFeed\ShoppingFeedWC\Products\Product::get_height + * @covers \ShoppingFeed\ShoppingFeedWC\Products\Product::get_width + * + * @author Stéphane Gillot + */ + public function test_get_simple_product_dimensions_when_not_defined(){ + $wc_product = WC_Helper_Product::create_simple_product(); + $wc_product->set_length('' ); + $wc_product->set_height( '' ); + $wc_product->set_width( '' ); + $wc_product->save(); + + $p = new Product( $wc_product->get_id() ); + + $this->assertEquals( '', $p->get_length(), 'Product length should be an empty string.' ); + $this->assertEquals( '', $p->get_height(), 'Product height should be an empty string.' ); + $this->assertEquals( '', $p->get_width(), 'Product width should be an empty string.' ); + } + + public function test_get_variation_dimensions_when_it_not_defined() { + + // Prepare the variable product object + $wc_variable_product = New \WC_Product_Variable(); + $wc_variable_product->set_length( 5 ); + $wc_variable_product->set_height( 10 ); + $wc_variable_product->set_width( 15 ); + $wc_variable_product->save(); + + $variation = WC_Helper_Product::create_variation_product(); + $variation->set_parent_id( $wc_variable_product->get_id() ); + $variation->save(); + + // Create an SF variation + $sf_product = new Product( $variation ); + $this->assertEquals( '', $sf_product->get_length(), 'Product length should be null.' ); + $this->assertEquals( '', $sf_product->get_height(), 'Product height should be null.' ); + $this->assertEquals( '', $sf_product->get_width(), 'Product width should be null.' ); + } + + public function test_get_variation_dimensions_when_it_overrides_parent_dimensions(){ + + // Prepare the variable product object + $wc_variable_product = New \WC_Product_Variable(); + $wc_variable_product->set_length( 5 ); + $wc_variable_product->set_height( 10 ); + $wc_variable_product->set_width( 15 ); + $wc_variable_product->save(); + + $variation = WC_Helper_Product::create_variation_product(); + $variation->set_parent_id( $wc_variable_product->get_id() ); + $variation->set_length(20); + $variation->set_height(30); + $variation->set_width(40); + $variation->save(); + + + // Create an SF variation + $sf_product = new Product( $variation ); + $this->assertEquals( 20, $sf_product->get_length(), 'Product length should be 20.' ); + $this->assertEquals( 30, $sf_product->get_height(), 'Product height should be 30.' ); + $this->assertEquals( 40, $sf_product->get_width(), 'Product width should be 40.' ); + } } diff --git a/tests/wpunit/WC_Helper_Product.php b/tests/wpunit/WC_Helper_Product.php new file mode 100644 index 0000000..76d9d81 --- /dev/null +++ b/tests/wpunit/WC_Helper_Product.php @@ -0,0 +1,445 @@ +delete( true ); + } + } + + /** + * Create simple product. + * + * @since 2.3 + * @param bool $save Save or return object. + * @param array $props Properties to be set in the new product, as an associative array. + * @return \WC_Product_Simple + */ + public static function create_simple_product( $save = true, $props = array() ) { + $product = new \WC_Product_Simple(); + $default_props = + array( + 'name' => 'Dummy Product', + 'regular_price' => 10, + 'price' => 10, + 'sku' => 'DUMMY SKU' . self::$sku_counter, + 'manage_stock' => false, + 'tax_status' => 'taxable', + 'downloadable' => false, + 'virtual' => false, + 'stock_status' => 'instock', + 'weight' => '1.1', + ); + + ++self::$sku_counter; + + $product->set_props( array_merge( $default_props, $props ) ); + + if ( $save ) { + $product->save(); + return wc_get_product( $product->get_id() ); + } else { + return $product; + } + } + + /** + * Create a downloadable product. + * + * @since 6.4.0 + * + * @param array $downloads An array of arrays (each containing a 'name' and 'file' key) or WC_Product_Download objects. + * @param bool $save Save or return object. + * + * @return \WC_Product_Simple|false + */ + public static function create_downloadable_product( array $downloads = array(), $save = true ) { + $product = new \WC_Product_Simple(); + $product->set_props( + array( + 'name' => 'Downloadable Product', + 'regular_price' => 10, + 'price' => 10, + 'manage_stock' => false, + 'tax_status' => 'taxable', + 'downloadable' => true, + 'virtual' => false, + 'stock_status' => 'instock', + ) + ); + + $product->set_downloads( $downloads ); + + if ( $save ) { + $product->save(); + return \wc_get_product( $product->get_id() ); + } else { + return $product; + } + } + + /** + * Create external product. + * + * @since 3.0.0 + * @return \WC_Product_External + */ + public static function create_external_product() { + $product = new \WC_Product_External(); + $product->set_props( + array( + 'name' => 'Dummy External Product', + 'regular_price' => 10, + 'sku' => 'DUMMY EXTERNAL SKU', + 'product_url' => 'https://woocommerce.com', + 'button_text' => 'Buy external product', + ) + ); + $product->save(); + + return wc_get_product( $product->get_id() ); + } + + /** + * Create grouped product. + * + * @since 3.0.0 + * @return WC_Product_Grouped + */ + public static function create_grouped_product() { + $simple_product_1 = self::create_simple_product(); + $simple_product_2 = self::create_simple_product(); + $product = new \WC_Product_Grouped(); + $product->set_props( + array( + 'name' => 'Dummy Grouped Product', + 'sku' => 'DUMMY GROUPED SKU', + ) + ); + $product->set_children( array( $simple_product_1->get_id(), $simple_product_2->get_id() ) ); + $product->save(); + + return wc_get_product( $product->get_id() ); + } + + /** + * Create a dummy variation product or configure an existing product object with dummy data. + * + * + * @since 2.3 + * @param \WC_Product_Variable|null $product Product object to configure, or null to create a new one. + * @return \WC_Product_Variable|null + */ + /** @psalm-suppress PossiblyNullReference */ + public static function create_variation_product( $product = null ) { + $is_new_product = is_null( $product ); + if ( $is_new_product ) { + $product = new \WC_Product_Variable(); + } + + $product->set_props( + array( + 'name' => 'Dummy Variable Product', + 'sku' => 'DUMMY VARIABLE SKU', + ) + ); + + $attributes = array(); + + $attributes[] = self::create_product_attribute_object( 'size', array( 'small', 'large', 'huge' ) ); + $attributes[] = self::create_product_attribute_object( 'colour', array( 'red', 'blue' ) ); + $attributes[] = self::create_product_attribute_object( 'number', array( '0', '1', '2' ) ); + + $product->set_attributes( $attributes ); + $product->save(); + + $variations = array(); + + $variations[] = self::create_product_variation_object( + $product->get_id(), + 'DUMMY SKU VARIABLE SMALL', + 10, + array( 'pa_size' => 'small' ) + ); + + $variations[] = self::create_product_variation_object( + $product->get_id(), + 'DUMMY SKU VARIABLE LARGE', + 15, + array( 'pa_size' => 'large' ) + ); + + $variations[] = self::create_product_variation_object( + $product->get_id(), + 'DUMMY SKU VARIABLE HUGE RED 0', + 16, + array( + 'pa_size' => 'huge', + 'pa_colour' => 'red', + 'pa_number' => '0', + ) + ); + + $variations[] = self::create_product_variation_object( + $product->get_id(), + 'DUMMY SKU VARIABLE HUGE RED 2', + 17, + array( + 'pa_size' => 'huge', + 'pa_colour' => 'red', + 'pa_number' => '2', + ) + ); + + $variations[] = self::create_product_variation_object( + $product->get_id(), + 'DUMMY SKU VARIABLE HUGE BLUE 2', + 18, + array( + 'pa_size' => 'huge', + 'pa_colour' => 'blue', + 'pa_number' => '2', + ) + ); + + $variations[] = self::create_product_variation_object( + $product->get_id(), + 'DUMMY SKU VARIABLE HUGE BLUE ANY NUMBER', + 19, + array( + 'pa_size' => 'huge', + 'pa_colour' => 'blue', + 'pa_number' => '', + ) + ); + + if ( $is_new_product ) { + return wc_get_product( $product->get_id() ); + } + + $variation_ids = array_map( + function( $variation ) { + return $variation->get_id(); + }, + $variations + ); + + if ( ! empty( $variation_ids ) ) { + $product->set_children( $variation_ids ); + } + return $product; + } + + /** + * Creates an instance of WC_Product_Variation with the supplied parameters, optionally persisting it to the database. + * + * @param string $parent_id Parent product id. + * @param string $sku SKU for the variation. + * @param int $price Price of the variation. + * @param array $attributes Attributes that define the variation, e.g. ['pa_color'=>'red']. + * @param bool $save If true, the object will be saved to the database after being created and configured. + * + * @return \WC_Product_Variation The created object. + */ + public static function create_product_variation_object( $parent_id, $sku, $price, $attributes, $save = true ) { + $variation = new \WC_Product_Variation(); + $variation->set_props( + array( + 'parent_id' => $parent_id, + 'sku' => $sku, + 'regular_price' => $price, + ) + ); + $variation->set_attributes( $attributes ); + if ( $save ) { + $variation->save(); + } + return $variation; + } + + /** + * Creates an instance of WC_Product_Attribute with the supplied parameters. + * + * @param string $raw_name Attribute raw name (without 'pa_' prefix). + * @param array $terms Possible values for the attribute. + * + * @return \WC_Product_Attribute The created attribute object. + */ + public static function create_product_attribute_object( $raw_name = 'size', $terms = array( 'small' ) ) { + $attribute = new \WC_Product_Attribute(); + $attribute_data = self::create_attribute( $raw_name, $terms ); + $attribute->set_id( $attribute_data['attribute_id'] ); + $attribute->set_name( $attribute_data['attribute_taxonomy'] ); + $attribute->set_options( $attribute_data['term_ids'] ); + $attribute->set_position( 1 ); + $attribute->set_visible( true ); + $attribute->set_variation( true ); + return $attribute; + } + + /** + * Create a dummy attribute. + * + * @since 2.3 + * + * @param string $raw_name Name of attribute to create. + * @param array(string) $terms Terms to create for the attribute. + * @return array + */ + public static function create_attribute( $raw_name = 'size', $terms = array( 'small' ) ) { + global $wpdb, $wc_product_attributes; + + // Make sure caches are clean. + delete_transient( 'wc_attribute_taxonomies' ); + \WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' ); + + // These are exported as labels, so convert the label to a name if possible first. + $attribute_labels = wp_list_pluck( wc_get_attribute_taxonomies(), 'attribute_label', 'attribute_name' ); + $attribute_name = array_search( $raw_name, $attribute_labels, true ); + + if ( ! $attribute_name ) { + $attribute_name = wc_sanitize_taxonomy_name( $raw_name ); + } + + $attribute_id = wc_attribute_taxonomy_id_by_name( $attribute_name ); + + if ( ! $attribute_id ) { + $taxonomy_name = wc_attribute_taxonomy_name( $attribute_name ); + + // Degister taxonomy which other tests may have created... + unregister_taxonomy( $taxonomy_name ); + + $attribute_id = wc_create_attribute( + array( + 'name' => $raw_name, + 'slug' => $attribute_name, + 'type' => 'select', + 'order_by' => 'menu_order', + 'has_archives' => 0, + ) + ); + + // Register as taxonomy. + register_taxonomy( + $taxonomy_name, + apply_filters( 'woocommerce_taxonomy_objects_' . $taxonomy_name, array( 'product' ) ), + apply_filters( + 'woocommerce_taxonomy_args_' . $taxonomy_name, + array( + 'labels' => array( + 'name' => $raw_name, + ), + 'hierarchical' => false, + 'show_ui' => false, + 'query_var' => true, + 'rewrite' => false, + ) + ) + ); + + // Set product attributes global. + $wc_product_attributes = array(); + + foreach ( wc_get_attribute_taxonomies() as $taxonomy ) { + $wc_product_attributes[ wc_attribute_taxonomy_name( $taxonomy->attribute_name ) ] = $taxonomy; + } + } + + $attribute = wc_get_attribute( $attribute_id ); + $return = array( + 'attribute_name' => $attribute->name, + 'attribute_taxonomy' => $attribute->slug, + 'attribute_id' => $attribute_id, + 'term_ids' => array(), + ); + + foreach ( $terms as $term ) { + $result = term_exists( $term, $attribute->slug ); + + if ( ! $result ) { + $result = wp_insert_term( $term, $attribute->slug ); + $return['term_ids'][] = $result['term_id']; + } else { + $return['term_ids'][] = $result['term_id']; + } + } + + return $return; + } + + /** + * Delete an attribute. + * + * @param int $attribute_id ID to delete. + * + * @since 2.3 + */ + public static function delete_attribute( $attribute_id ) { + global $wpdb; + + $attribute_id = absint( $attribute_id ); + + $wpdb->query( + $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_attribute_taxonomies WHERE attribute_id = %d", $attribute_id ) + ); + } + + /** + * Creates a new product review on a specific product. + * + * @since 3.0 + * @param int $product_id integer Product ID that the review is for. + * @param string $review_content string Content to use for the product review. + * @return integer Product Review ID. + */ + public static function create_product_review( $product_id, $review_content = 'Review content here' ) { + $data = array( + 'comment_post_ID' => $product_id, + 'comment_author' => 'admin', + 'comment_author_email' => 'woo@woo.local', + 'comment_author_url' => '', + 'comment_date' => '2016-01-01T11:11:11', + 'comment_content' => $review_content, + 'comment_approved' => 1, + 'comment_type' => 'review', + ); + return wp_insert_comment( $data ); + } + + /** + * A helper function for hooking into save_post during the test_product_meta_save_post test. + * @since 3.0.1 + * + * @param int $id ID to update. + */ + public static function save_post_test_update_meta_data_direct( $id ) { + update_post_meta( $id, '_test2', 'world' ); + } +} \ No newline at end of file