Skip to content

Commit

Permalink
Add API Rest endpoint to duplicate product (#46141)
Browse files Browse the repository at this point in the history
* Add API Rest endpoint to duplicate product

* Add update functionality

* update duplicate_product

* Fix function comment

* Add unit tests

* Add changelog

* Add missing variable

* Improve tests

* Fix tests and lint

* Add comment explaining process
  • Loading branch information
octaedro committed Apr 8, 2024
1 parent c34399b commit 46b07c0
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: dev

Add API Rest endpoint to duplicate product #46141
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,58 @@ public function register_routes() {
'schema' => array( $this, 'get_public_item_schema' ),
)
);

register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[\d]+)/duplicate',
array(
'args' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'duplicate_product' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}

/**
* Duplicate a product and returns the duplicated product.
* The product status is set to "draft" and the name includes a "(copy)" at the end by default.
*
* @param WP_REST_Request $request Request data.
* @return WP_REST_Response|WP_Error
*/
public function duplicate_product( $request ) {
$product_id = $request->get_param( 'id' );
$product = wc_get_product( $product_id );

if ( ! $product ) {
return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) );
}

if ( 'simple' !== $product->get_type() ) {
$request['type'] = $product->get_type();
}

// Creating product object from request data in preparation for copying.
$updated_product = $this->prepare_object_for_database( $request );
$duplicated_product = ( new WC_Admin_Duplicate_Product() )->product_duplicate( $updated_product );

if ( is_wp_error( $duplicated_product ) ) {
return new WP_Error( 'woocommerce_rest_product_duplicate_error', $duplicated_product->get_error_message(), array( 'status' => 400 ) );
}

$response_data = $duplicated_product->get_data();

return new WP_REST_Response( $response_data, 200 );
}

/**
Expand Down Expand Up @@ -325,7 +377,7 @@ public function add_search_criteria_to_wp_query_where( $where ) {
global $wpdb;
if ( ! empty( $this->search_sku_in_product_lookup_table ) ) {
$like_search = '%' . $wpdb->esc_like( $this->search_sku_in_product_lookup_table ) . '%';
$where .= ' AND ' . $wpdb->prepare( '(wc_product_meta_lookup.sku LIKE %s)', $like_search );
$where .= ' AND ' . $wpdb->prepare( '(wc_product_meta_lookup.sku LIKE %s)', $like_search );
}
return $where;
}
Expand Down Expand Up @@ -1112,7 +1164,7 @@ public function get_item_schema() {
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'low_stock_amount' => array(
'low_stock_amount' => array(
'description' => __( 'Low Stock amount for the product.', 'woocommerce' ),
'type' => array( 'integer', 'null' ),
'context' => array( 'view', 'edit' ),
Expand Down Expand Up @@ -1344,7 +1396,7 @@ public function get_item_schema() {
),
),
),
'has_options' => array(
'has_options' => array(
'description' => __( 'Shows if the product needs to be configured before it can be bought.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ public function get_expected_response_fields() {
public function test_product_api_get_all_fields() {
$expected_response_fields = $this->get_expected_response_fields();

$product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product();
$product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product();
$response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/products/' . $product->get_id() ) );

$this->assertEquals( 200, $response->get_status() );
Expand All @@ -183,7 +183,7 @@ public function test_get_product_data_should_work_without_request_param() {
$call_product_data_wrapper = function () use ( $product ) {
return $this->get_product_data( $product );
};
$response = $call_product_data_wrapper->call( new WC_REST_Products_Controller() );
$response = $call_product_data_wrapper->call( new WC_REST_Products_Controller() );
$this->assertArrayHasKey( 'id', $response );
}

Expand All @@ -192,7 +192,7 @@ public function test_get_product_data_should_work_without_request_param() {
*/
public function test_products_get_each_field_one_by_one() {
$expected_response_fields = $this->get_expected_response_fields();
$product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product();
$product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product();

foreach ( $expected_response_fields as $field ) {
$request = new WP_REST_Request( 'GET', '/wc/v3/products/' . $product->get_id() );
Expand Down Expand Up @@ -325,7 +325,7 @@ public function test_collection_param_include_meta() {
$this->assertArrayHasKey( 'meta_data', $order );
$this->assertEquals( 1, count( $order['meta_data'] ) );
$meta_keys = array_map(
function( $meta_item ) {
function ( $meta_item ) {
return $meta_item->get_data()['key'];
},
$order['meta_data']
Expand All @@ -349,7 +349,7 @@ public function test_collection_param_include_meta_empty() {
foreach ( $response_data as $order ) {
$this->assertArrayHasKey( 'meta_data', $order );
$meta_keys = array_map(
function( $meta_item ) {
function ( $meta_item ) {
return $meta_item->get_data()['key'];
},
$order['meta_data']
Expand All @@ -374,7 +374,7 @@ public function test_collection_param_exclude_meta() {
foreach ( $response_data as $order ) {
$this->assertArrayHasKey( 'meta_data', $order );
$meta_keys = array_map(
function( $meta_item ) {
function ( $meta_item ) {
return $meta_item->get_data()['key'];
},
$order['meta_data']
Expand All @@ -401,7 +401,7 @@ public function test_collection_param_include_meta_override() {
$this->assertArrayHasKey( 'meta_data', $order );
$this->assertEquals( 1, count( $order['meta_data'] ) );
$meta_keys = array_map(
function( $meta_item ) {
function ( $meta_item ) {
return $meta_item->get_data()['key'];
},
$order['meta_data']
Expand All @@ -425,4 +425,86 @@ public function test_collection_param_include_meta_returns_array() {

$this->assertIsArray( $decoded_data_object[0]->meta_data );
}

/**
* Test the duplicate product endpoint with simple products.
*/
public function test_duplicate_simple_product() {
$product = WC_Helper_Product::create_simple_product(
true,
array(
'name' => 'Carrot Cake',
'sku' => 'carrot-cake-1',
)
);
$product_id = $product->get_id();

$request = new WP_REST_Request( 'POST', '/wc/v3/products/' . $product_id . '/duplicate' );
$response = $this->server->dispatch( $request );
$this->assertEquals( 200, $response->get_status() );

$response_data = $response->get_data();
$this->assertArrayHasKey( 'id', $response_data );
$this->assertNotEquals( $product, $response_data['id'] );

$duplicated_product = wc_get_product( $response_data['id'] );
$this->assertEquals( $product->get_name() . ' (Copy)', $duplicated_product->get_name() );
$this->assertEquals( 'draft', $duplicated_product->get_status() );
}

/**
* Test the duplicate product endpoint with variable products.
*/
public function test_duplicate_variable_product() {
$variable_product = WC_Helper_Product::create_variation_product();
$product_id = $variable_product->get_id();

$request = new WP_REST_Request( 'POST', '/wc/v3/products/' . $product_id . '/duplicate' );
$response = $this->server->dispatch( $request );
$this->assertEquals( 200, $response->get_status() );

$response_data = $response->get_data();
$this->assertArrayHasKey( 'id', $response_data );
$this->assertNotEquals( $product_id, $response_data['id'] );

$duplicated_product = wc_get_product( $response_data['id'] );
$this->assertEquals( $variable_product->get_name() . ' (Copy)', $duplicated_product->get_name() );
$this->assertTrue( $duplicated_product->is_type( 'variable' ) );
}

/**
* Test the duplicate product endpoint with extra args to also update the product.
*/
public function test_duplicate_product_with_extra_args() {
$product = WC_Helper_Product::create_simple_product(
true,
array(
'name' => 'Tiramisu Cake',
'sku' => 'tiramisu-cake-1',
)
);
$product_id = $product->get_id();

$request = new WP_REST_Request( 'POST', '/wc/v3/products/' . $product_id . '/duplicate' );
$request->set_param( 'sku', 'new-sku' );
$request->set_param(
'meta_data',
array(
array(
'key' => 'test',
'value' => 'test',
),
)
);
$response = $this->server->dispatch( $request );
$this->assertEquals( 200, $response->get_status() );

$response_data = $response->get_data();
$this->assertArrayHasKey( 'id', $response_data );
$this->assertNotEquals( $product_id, $response_data['id'] );

$duplicated_product = wc_get_product( $response_data['id'] );
$this->assertEquals( 'new-sku', $duplicated_product->get_sku() );
$this->assertEquals( 'test', $duplicated_product->get_meta( 'test', true ) );
}
}

0 comments on commit 46b07c0

Please sign in to comment.