diff --git a/plugins/woocommerce-blocks/src/RestApi.php b/plugins/woocommerce-blocks/src/RestApi.php index 885b918ee18..959bc34f446 100644 --- a/plugins/woocommerce-blocks/src/RestApi.php +++ b/plugins/woocommerce-blocks/src/RestApi.php @@ -94,6 +94,7 @@ class RestApi { 'product-reviews' => __NAMESPACE__ . '\RestApi\Controllers\ProductReviews', 'store-cart' => __NAMESPACE__ . '\RestApi\StoreApi\Controllers\Cart', 'store-cart-items' => __NAMESPACE__ . '\RestApi\StoreApi\Controllers\CartItems', + 'store-cart-coupons' => __NAMESPACE__ . '\RestApi\StoreApi\Controllers\CartCoupons', 'store-products' => __NAMESPACE__ . '\RestApi\StoreApi\Controllers\Products', 'store-product-collection-data' => __NAMESPACE__ . '\RestApi\StoreApi\Controllers\ProductCollectionData', 'store-product-attributes' => __NAMESPACE__ . '\RestApi\StoreApi\Controllers\ProductAttributes', diff --git a/plugins/woocommerce-blocks/src/RestApi/StoreApi/Controllers/CartCoupons.php b/plugins/woocommerce-blocks/src/RestApi/StoreApi/Controllers/CartCoupons.php new file mode 100644 index 00000000000..37542c4e2c4 --- /dev/null +++ b/plugins/woocommerce-blocks/src/RestApi/StoreApi/Controllers/CartCoupons.php @@ -0,0 +1,252 @@ +schema = new CartCouponSchema(); + } + + /** + * Register routes. + */ + public function register_routes() { + register_rest_route( + $this->namespace, + '/' . $this->rest_base, + [ + [ + 'methods' => RestServer::READABLE, + 'callback' => [ $this, 'get_items' ], + 'args' => [ + 'context' => $this->get_context_param( [ 'default' => 'view' ] ), + ], + ], + [ + 'methods' => RestServer::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'args' => $this->get_endpoint_args_for_item_schema( RestServer::CREATABLE ), + ], + [ + 'methods' => RestServer::DELETABLE, + 'callback' => [ $this, 'delete_items' ], + ], + 'schema' => [ $this, 'get_public_item_schema' ], + ] + ); + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\w-]+)', + [ + 'args' => [ + 'code' => [ + 'description' => __( 'Unique identifier for the coupon within the cart.', 'woo-gutenberg-products-block' ), + 'type' => 'string', + ], + ], + [ + 'methods' => RestServer::READABLE, + 'callback' => [ $this, 'get_item' ], + 'args' => [ + 'context' => $this->get_context_param( [ 'default' => 'view' ] ), + ], + ], + [ + 'methods' => RestServer::DELETABLE, + 'callback' => [ $this, 'delete_item' ], + ], + 'schema' => [ $this, 'get_public_item_schema' ], + ] + ); + } + + /** + * Get a collection of cart coupons. + * + * @param RestRequest $request Full details about the request. + * @return RestError|RestResponse + */ + public function get_items( $request ) { + $controller = new CartController(); + $cart_coupons = $controller->get_cart_coupons(); + $items = []; + + foreach ( $cart_coupons as $coupon_code ) { + $response = $this->prepare_item_for_response( $coupon_code, $request ); + $response->add_links( $this->prepare_links( $coupon_code ) ); + + $response = $this->prepare_response_for_collection( $response ); + $items[] = $response; + } + + $response = rest_ensure_response( $items ); + + return $response; + } + + /** + * Get a single cart coupon. + * + * @param RestRequest $request Full details about the request. + * @return RestError|RestResponse + */ + public function get_item( $request ) { + $controller = new CartController(); + + if ( ! $controller->has_coupon( $request['code'] ) ) { + return new RestError( 'woocommerce_rest_cart_coupon_invalid_code', __( 'Coupon does not exist in the cart.', 'woo-gutenberg-products-block' ), array( 'status' => 404 ) ); + } + + $data = $this->prepare_item_for_response( $request['code'], $request ); + $response = rest_ensure_response( $data ); + + return $response; + } + + /** + * Add a coupon to the cart and return the result. + * + * @param RestRequest $request Full data about the request. + * @return RestError|RestResponse Response object on success, or WP_Error object on failure. + */ + public function create_item( $request ) { + if ( ! wc_coupons_enabled() ) { + return new RestError( 'woocommerce_rest_cart_coupon_disabled', __( 'Coupons are disabled.', 'woo-gutenberg-products-block' ), array( 'status' => 404 ) ); + } + + $controller = new CartController(); + + try { + $controller->apply_coupon( $request['code'] ); + } catch ( RestException $e ) { + return new RestError( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + + $response = $this->get_item( $request ); + + if ( $response instanceof RestError ) { + return $response; + } + + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + + return $response; + } + + /** + * Delete a single cart coupon. + * + * @param RestRequest $request Full data about the request. + * @return RestError|RestResponse Response object on success, or WP_Error object on failure. + */ + public function delete_item( $request ) { + $controller = new CartController(); + + if ( ! $controller->has_coupon( $request['code'] ) ) { + return new RestError( 'woocommerce_rest_cart_coupon_invalid_code', __( 'Coupon does not exist in the cart.', 'woo-gutenberg-products-block' ), array( 'status' => 404 ) ); + } + + $cart = $controller->get_cart_instance(); + $cart->remove_coupon( $request['code'] ); + + return new RestResponse( null, 204 ); + } + + /** + * Deletes all coupons in the cart. + * + * @param RestRequest $request Full data about the request. + * @return RestError|RestResponse Response object on success, or WP_Error object on failure. + */ + public function delete_items( $request ) { + $controller = new CartController(); + $cart = $controller->get_cart_instance(); + $cart->remove_coupons(); + + return new RestResponse( [], 200 ); + } + + /** + * Cart item schema. + * + * @return array + */ + public function get_item_schema() { + return $this->schema->get_item_schema(); + } + + /** + * Prepares a single item output for response. + * + * @param string $coupon_code Coupon code. + * @param RestRequest $request Request object. + * @return RestResponse Response object. + */ + public function prepare_item_for_response( $coupon_code, $request ) { + return rest_ensure_response( $this->schema->get_item_response( $coupon_code ) ); + } + + /** + * Prepare links for the request. + * + * @param string $coupon_code Coupon code. + * @return array + */ + protected function prepare_links( $coupon_code ) { + $base = $this->namespace . '/' . $this->rest_base; + $links = array( + 'self' => array( + 'href' => rest_url( trailingslashit( $base ) . $coupon_code ), + ), + 'collection' => array( + 'href' => rest_url( $base ), + ), + ); + return $links; + } +} diff --git a/plugins/woocommerce-blocks/src/RestApi/StoreApi/README.md b/plugins/woocommerce-blocks/src/RestApi/StoreApi/README.md index 60519ce164b..0d472d5ca13 100644 --- a/plugins/woocommerce-blocks/src/RestApi/StoreApi/README.md +++ b/plugins/woocommerce-blocks/src/RestApi/StoreApi/README.md @@ -90,6 +90,7 @@ Available resources in the Store API include: | [`Products`](#products-api) | `/wc/store/products` | | [`Cart`](#cart-api) | `/wc/store/cart` | | [`Cart Items`](#cart-items-api) | `/wc/store/cart/items` | +| [`Cart Coupons`](#cart-coupons-api) | `/wc/store/cart/coupons` | | [`Cart Shipping Rates`](#cart-shipping-rates-api) | `/wc/store/cart/shipping-rates` | | [`Product Attributes`](#product-attributes-api) | `/wc/store/products/attributes` | | [`Product Attribute Terms`](#product-attribute-terms-api) | `/wc/store/products/attributes/1/terms` | @@ -321,6 +322,7 @@ Example response: ```json { + "coupons": [], "items": [ { "key": "6512bd43d9caa6e02c990b0a82652dca", @@ -800,6 +802,159 @@ Example response: [] ``` +## Cart coupons API + +### List cart coupons + +```http +GET /cart/coupons +``` + +There are no parameters required for this endpoint. + +```http +curl "https://example-store.com/wp-json/wc/store/cart/items" +``` + +Example response: + +```json +[ + { + "code": "20off", + "totals": { + "currency_code": "GBP", + "currency_symbol": "£", + "currency_minor_unit": 2, + "currency_decimal_separator": ".", + "currency_thousand_separator": ",", + "currency_prefix": "£", + "currency_suffix": "", + "total_discount": "1667", + "total_discount_tax": "333" + }, + "_links": { + "self": [ + { + "href": "http:\/\/local.wordpress.test\/wp-json\/wc\/store\/cart\/coupons\/20off" + } + ], + "collection": [ + { + "href": "http:\/\/local.wordpress.test\/wp-json\/wc\/store\/cart\/coupons" + } + ] + } + } +] +``` + +### Single cart coupon + +Get a single cart coupon. + +```http +GET /cart/coupons/:code +``` + +| Attribute | Type | Required | Description | +| :-------- | :----- | :------: | :---------------------------------------------- | +| `code` | string | Yes | The coupon code of the cart coupon to retrieve. | + +```http +curl "https://example-store.com/wp-json/wc/store/cart/coupons/20off" +``` + +Example response: + +```json +{ + "code": "20off", + "totals": { + "currency_code": "GBP", + "currency_symbol": "£", + "currency_minor_unit": 2, + "currency_decimal_separator": ".", + "currency_thousand_separator": ",", + "currency_prefix": "£", + "currency_suffix": "", + "total_discount": "1667", + "total_discount_tax": "333" + } +} +``` + +### New cart coupon + +Apply a coupon to the cart. + +```http +POST /cart/coupons/ +``` + +| Attribute | Type | Required | Description | +| :-------- | :----- | :------: | :--------------------------------------------- | +| `code` | string | Yes | The coupon code you wish to apply to the cart. | + +```http +curl --request POST https://example-store.com/wp-json/wc/store/cart/coupons?code=20off +``` + +Example response: + +```json +{ + "code": "20off", + "totals": { + "currency_code": "GBP", + "currency_symbol": "£", + "currency_minor_unit": 2, + "currency_decimal_separator": ".", + "currency_thousand_separator": ",", + "currency_prefix": "£", + "currency_suffix": "", + "total_discount": "1667", + "total_discount_tax": "333" + } +} +``` + +### Delete single cart coupon + +Delete/remove a coupon from the cart. + +```http +DELETE /cart/coupons/:code +``` + +| Attribute | Type | Required | Description | +| :-------- | :----- | :------: | :------------------------------------------------ | +| `code` | string | Yes | The coupon code you wish to remove from the cart. | + +```http +curl --request DELETE https://example-store.com/wp-json/wc/store/cart/coupons/20off +``` + +### Delete all cart coupons + +Delete/remove all coupons from the cart. + +```http +DELETE /cart/coupons/ +``` + +There are no parameters required for this endpoint. + +```http +curl --request DELETE https://example-store.com/wp-json/wc/store/cart/coupons +``` + +Example response: + +```json +[] +``` + ## Cart shipping rates API ### Get shipping rates for current cart diff --git a/plugins/woocommerce-blocks/src/RestApi/StoreApi/Schemas/CartCouponSchema.php b/plugins/woocommerce-blocks/src/RestApi/StoreApi/Schemas/CartCouponSchema.php new file mode 100644 index 00000000000..51e33246c88 --- /dev/null +++ b/plugins/woocommerce-blocks/src/RestApi/StoreApi/Schemas/CartCouponSchema.php @@ -0,0 +1,102 @@ + [ + 'description' => __( 'The coupons unique code.', 'woo-gutenberg-products-block' ), + 'type' => 'string', + 'context' => [ 'view', 'edit' ], + 'arg_options' => [ + 'sanitize_callback' => 'wc_format_coupon_code', + 'validate_callback' => [ $this, 'coupon_exists' ], + ], + ], + 'totals' => [ + 'description' => __( 'Total amounts provided using the smallest unit of the currency.', 'woo-gutenberg-products-block' ), + 'type' => 'object', + 'context' => [ 'view', 'edit' ], + 'readonly' => true, + 'properties' => array_merge( + $this->get_store_currency_properties(), + [ + 'total_discount' => [ + 'description' => __( 'Total discount applied by this coupon.', 'woo-gutenberg-products-block' ), + 'type' => 'string', + 'context' => [ 'view', 'edit' ], + 'readonly' => true, + ], + 'total_discount_tax' => [ + 'description' => __( 'Total tax removed due to discount applied by this coupon.', 'woo-gutenberg-products-block' ), + 'type' => 'string', + 'context' => [ 'view', 'edit' ], + 'readonly' => true, + ], + ] + ), + ], + ]; + } + + /** + * Check given coupon exists. + * + * @param string $coupon_code Coupon code. + * @return bool + */ + public function coupon_exists( $coupon_code ) { + $coupon = new \WC_Coupon( $coupon_code ); + return (bool) $coupon->get_id() || $coupon->get_virtual(); + } + + /** + * Convert a WooCommerce cart item to an object suitable for the response. + * + * @param string $coupon_code Coupon code from the cart. + * @return array + */ + public function get_item_response( $coupon_code ) { + $controller = new CartController(); + $cart = $controller->get_cart_instance(); + $total_discounts = $cart->get_coupon_discount_totals(); + $total_discount_taxes = $cart->get_coupon_discount_tax_totals(); + return [ + 'code' => $coupon_code, + 'totals' => array_merge( + $this->get_store_currency_response(), + [ + 'total_discount' => $this->prepare_money_response( isset( $total_discounts[ $coupon_code ] ) ? $total_discounts[ $coupon_code ] : 0, wc_get_price_decimals() ), + 'total_discount_tax' => $this->prepare_money_response( isset( $total_discount_taxes[ $coupon_code ] ) ? $total_discount_taxes[ $coupon_code ] : 0, wc_get_price_decimals(), PHP_ROUND_HALF_DOWN ), + ] + ), + ]; + } +} diff --git a/plugins/woocommerce-blocks/src/RestApi/StoreApi/Schemas/CartItemSchema.php b/plugins/woocommerce-blocks/src/RestApi/StoreApi/Schemas/CartItemSchema.php index e5764add541..b0269a21ede 100644 --- a/plugins/woocommerce-blocks/src/RestApi/StoreApi/Schemas/CartItemSchema.php +++ b/plugins/woocommerce-blocks/src/RestApi/StoreApi/Schemas/CartItemSchema.php @@ -1,8 +1,6 @@ [ + 'description' => __( 'List of applied cart coupons.', 'woo-gutenberg-products-block' ), + 'type' => 'array', + 'context' => [ 'view', 'edit' ], + 'readonly' => true, + 'items' => [ + 'type' => 'object', + 'properties' => $this->force_schema_readonly( ( new CartCouponSchema() )->get_properties() ), + ], + ], 'items' => [ 'description' => __( 'List of cart items.', 'woo-gutenberg-products-block' ), 'type' => 'array', @@ -161,8 +171,10 @@ class CartSchema extends AbstractSchema { * @return array */ public function get_item_response( $cart ) { - $cart_item_schema = new CartItemSchema(); + $cart_coupon_schema = new CartCouponSchema(); + $cart_item_schema = new CartItemSchema(); return [ + 'coupons' => array_values( array_map( [ $cart_coupon_schema, 'get_item_response' ], array_filter( $cart->get_applied_coupons() ) ) ), 'items' => array_values( array_map( [ $cart_item_schema, 'get_item_response' ], array_filter( $cart->get_cart() ) ) ), 'items_count' => $cart->get_cart_contents_count(), 'items_weight' => wc_get_weight( $cart->get_cart_contents_weight(), 'g' ), diff --git a/plugins/woocommerce-blocks/src/RestApi/StoreApi/Utilities/CartController.php b/plugins/woocommerce-blocks/src/RestApi/StoreApi/Utilities/CartController.php index 54747f1f57c..cce572dc105 100644 --- a/plugins/woocommerce-blocks/src/RestApi/StoreApi/Utilities/CartController.php +++ b/plugins/woocommerce-blocks/src/RestApi/StoreApi/Utilities/CartController.php @@ -2,8 +2,6 @@ /** * Helper class to bridge the gap between the cart API and Woo core. * - * Overrides some of the woo core cart methods to make them work with the API and generally increase flexibility. Some of this logic should move to core. - * * @package WooCommerce/Blocks */ @@ -24,6 +22,10 @@ class CartController { /** * Based on the core cart class but returns errors rather than rendering notices directly. * + * @todo Overriding the core add_to_cart method was necessary because core outputs notices when an item is added to + * the cart. For us this would cause notices to build up and output on the store, out of context. Core would need + * refactoring to split notices out from other cart actions. + * * @throws RestException Exception if invalid data is detected. * * @param array $request Add to cart request params. @@ -52,21 +54,44 @@ class CartController { $variation_id = 0; } - $cart_id = wc()->cart->generate_cart_id( $product_id, $variation_id, $request['variation'], $request['cart_item_data'] ); + $cart_id = wc()->cart->generate_cart_id( + $product_id, + $variation_id, + $request['variation'], + $request['cart_item_data'] + ); $existing_cart_id = wc()->cart->find_product_in_cart( $cart_id ); if ( ! $product->is_purchasable() ) { - throw new RestException( 'woocommerce_rest_cart_product_is_not_purchasable', __( 'This product cannot be purchased.', 'woo-gutenberg-products-block' ), 403 ); + throw new RestException( + 'woocommerce_rest_cart_product_is_not_purchasable', + __( 'This product cannot be purchased.', 'woo-gutenberg-products-block' ), + 403 + ); } if ( $product->is_sold_individually() && $existing_cart_id ) { - /* translators: %s: product name */ - throw new RestException( 'woocommerce_rest_cart_product_sold_individually', sprintf( __( '"%s" is already inside your cart.', 'woo-gutenberg-products-block' ), $product->get_name() ), 403 ); + throw new RestException( + 'woocommerce_rest_cart_product_sold_individually', + sprintf( + /* translators: %s: product name */ + __( '"%s" is already inside your cart.', 'woo-gutenberg-products-block' ), + $product->get_name() + ), + 403 + ); } if ( ! $product->is_in_stock() ) { - /* translators: %s: product name */ - throw new RestException( 'woocommerce_rest_cart_product_no_stock', sprintf( __( 'You cannot add "%s" to the cart because the product is out of stock.', 'woo-gutenberg-products-block' ), $product->get_name() ), 403 ); + throw new RestException( + 'woocommerce_rest_cart_product_no_stock', + sprintf( + /* translators: %s: product name */ + __( 'You cannot add "%s" to the cart because the product is out of stock.', 'woo-gutenberg-products-block' ), + $product->get_name() + ), + 403 + ); } if ( $product->managing_stock() ) { @@ -112,7 +137,15 @@ class CartController { wc()->cart->cart_contents = apply_filters( 'woocommerce_cart_contents_changed', wc()->cart->cart_contents ); - do_action( 'woocommerce_add_to_cart', $cart_id, $product_id, $request['quantity'], $variation_id, $request['variation'], $request['cart_item_data'] ); + do_action( + 'woocommerce_add_to_cart', + $cart_id, + $product_id, + $request['quantity'], + $variation_id, + $request['variation'], + $request['cart_item_data'] + ); return $cart_id; } catch ( RestException $e ) { @@ -156,6 +189,104 @@ class CartController { wc()->cart->empty_cart(); } + /** + * See if cart has applied coupon by code. + * + * @param string $coupon_code Cart coupon code. + * @return bool + */ + public function has_coupon( $coupon_code ) { + return wc()->cart->has_discount( $coupon_code ); + } + + /** + * Returns all applied coupons. + * + * @param callable $callback Optional callback to apply to the array filter. + * @return array + */ + public function get_cart_coupons( $callback = null ) { + return $callback ? array_filter( wc()->cart->get_applied_coupons(), $callback ) : array_filter( wc()->cart->get_applied_coupons() ); + } + + /** + * Based on the core cart class but returns errors rather than rendering notices directly. + * + * @todo Overriding the core apply_coupon method was necessary because core outputs notices when a coupon gets + * applied. For us this would cause notices to build up and output on the store, out of context. Core would need + * refactoring to split notices out from other cart actions. + * + * @throws RestException Exception if invalid data is detected. + * + * @param string $coupon_code Coupon code. + */ + public function apply_coupon( $coupon_code ) { + $cart = $this->get_cart_instance(); + $applied_coupons = $this->get_cart_coupons(); + $coupon = new \WC_Coupon( $coupon_code ); + + if ( $coupon->get_code() !== $coupon_code ) { + throw new RestException( + 'woocommerce_rest_cart_coupon_error', + __( 'Invalid coupon code.', 'woo-gutenberg-products-block' ), + 403 + ); + } + + if ( $this->has_coupon( $coupon_code ) ) { + throw new RestException( + 'woocommerce_rest_cart_coupon_error', + __( 'Coupon has already been applied.', 'woo-gutenberg-products-block' ), + 403 + ); + } + + if ( ! $coupon->is_valid() ) { + throw new RestException( + 'woocommerce_rest_cart_coupon_error', + $coupon->get_error_message(), + 403 + ); + } + + // Prevents new coupons being added if individual use coupons are already in the cart. + $individual_use_coupons = $this->get_cart_coupons( + function( $code ) { + $coupon = new \WC_Coupon( $code ); + return $coupon->get_individual_use(); + } + ); + + foreach ( $individual_use_coupons as $code ) { + $individual_use_coupon = new \WC_Coupon( $code ); + + if ( false === apply_filters( 'woocommerce_apply_with_individual_use_coupon', false, $coupon, $individual_use_coupon, $applied_coupons ) ) { + throw new RestException( + 'woocommerce_rest_cart_coupon_error', + sprintf( + /* translators: %s: coupon code */ + __( '"%s" has already been applied and cannot be used in conjunction with other coupons.', 'woo-gutenberg-products-block' ), + $code + ), + 403 + ); + } + } + + if ( $coupon->get_individual_use() ) { + $coupons_to_remove = array_diff( $applied_coupons, apply_filters( 'woocommerce_apply_individual_use_coupon', array(), $coupon, $applied_coupons ) ); + + foreach ( $coupons_to_remove as $code ) { + $cart->remove_coupon( $code ); + } + } + + $applied_coupons[] = $coupon_code; + $cart->set_applied_coupons( $applied_coupons ); + + do_action( 'woocommerce_applied_coupon', $coupon_code ); + } + /** * Get a product object to be added to the cart. * @@ -168,7 +299,11 @@ class CartController { $product = wc_get_product( $request['id'] ); if ( ! $product || 'trash' === $product->get_status() ) { - throw new RestException( 'woocommerce_rest_cart_invalid_product', __( 'This product cannot be added to the cart.', 'woo-gutenberg-products-block' ), 403 ); + throw new RestException( + 'woocommerce_rest_cart_invalid_product', + __( 'This product cannot be added to the cart.', 'woo-gutenberg-products-block' ), + 403 + ); } return $product; @@ -297,7 +432,11 @@ class CartController { $variation_id = $data_store->find_matching_product_variation( $product, $match_attributes ); if ( empty( $variation_id ) ) { - throw new RestException( 'woocommerce_rest_variation_id_from_variation_data', __( 'No matching variation found.', 'woo-gutenberg-products-block' ), 400 ); + throw new RestException( + 'woocommerce_rest_variation_id_from_variation_data', + __( 'No matching variation found.', 'woo-gutenberg-products-block' ), + 400 + ); } return $variation_id; @@ -321,17 +460,36 @@ class CartController { if ( ! $attribute['is_variation'] ) { continue; } - $attribute_label = wc_attribute_label( $attribute['name'] ); + $attribute_label = wc_attribute_label( $attribute['name'] ); + $variation_attribute_name = wc_variation_attribute_name( $attribute['name'] ); // Attribute labels e.g. Size. if ( isset( $variation_data[ $attribute_label ] ) ) { - $return[ wc_variation_attribute_name( $attribute['name'] ) ] = $attribute['is_taxonomy'] ? sanitize_title( $variation_data[ $attribute_label ] ) : html_entity_decode( wc_clean( $variation_data[ $attribute_label ] ), ENT_QUOTES, get_bloginfo( 'charset' ) ); + $return[ $variation_attribute_name ] = + $attribute['is_taxonomy'] + ? + sanitize_title( $variation_data[ $attribute_label ] ) + : + html_entity_decode( + wc_clean( $variation_data[ $attribute_label ] ), + ENT_QUOTES, + get_bloginfo( 'charset' ) + ); continue; } // Attribute slugs e.g. pa_size. if ( isset( $variation_data[ $attribute['name'] ] ) ) { - $return[ wc_variation_attribute_name( $attribute['name'] ) ] = $attribute['is_taxonomy'] ? sanitize_title( $variation_data[ $attribute['name'] ] ) : html_entity_decode( wc_clean( $variation_data[ $attribute['name'] ] ), ENT_QUOTES, get_bloginfo( 'charset' ) ); + $return[ $variation_attribute_name ] = + $attribute['is_taxonomy'] + ? + sanitize_title( $variation_data[ $attribute['name'] ] ) + : + html_entity_decode( + wc_clean( $variation_data[ $attribute['name'] ] ), + ENT_QUOTES, + get_bloginfo( 'charset' ) + ); } } return $return; @@ -351,7 +509,11 @@ class CartController { } if ( ! $product || 'trash' === $product->get_status() ) { - throw new RestException( 'woocommerce_rest_cart_invalid_parent_product', __( 'This product cannot be added to the cart.', 'woo-gutenberg-products-block' ), 403 ); + throw new RestException( + 'woocommerce_rest_cart_invalid_parent_product', + __( 'This product cannot be added to the cart.', 'woo-gutenberg-products-block' ), + 403 + ); } return $product->get_attributes(); diff --git a/plugins/woocommerce-blocks/tests/php/RestApi/StoreApi/Controllers/CartCoupons.php b/plugins/woocommerce-blocks/tests/php/RestApi/StoreApi/Controllers/CartCoupons.php new file mode 100644 index 00000000000..b531f895964 --- /dev/null +++ b/plugins/woocommerce-blocks/tests/php/RestApi/StoreApi/Controllers/CartCoupons.php @@ -0,0 +1,169 @@ +product = ProductHelper::create_simple_product( false ); + $this->coupon = CouponHelper::create_coupon(); + + wc_empty_cart(); + + wc()->cart->add_to_cart( $this->product->get_id(), 2 ); + wc()->cart->apply_coupon( $this->coupon->get_code() ); + } + + /** + * Test route registration. + */ + public function test_register_routes() { + $routes = $this->server->get_routes(); + $this->assertArrayHasKey( '/wc/store/cart/coupons', $routes ); + $this->assertArrayHasKey( '/wc/store/cart/coupons/(?P[\w-]+)', $routes ); + } + + /** + * Test getting cart. + */ + public function test_get_items() { + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/store/cart/coupons' ) ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 1, count( $data ) ); + } + + /** + * Test getting cart item by key. + */ + public function test_get_item() { + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/store/cart/coupons/' . $this->coupon->get_code() ) ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( $this->coupon->get_code(), $data['code'] ); + $this->assertEquals( '0', $data['totals']['total_discount'] ); + $this->assertEquals( '0', $data['totals']['total_discount_tax'] ); + } + + /** + * Test add to cart. + */ + public function test_create_item() { + wc()->cart->remove_coupons(); + + $request = new WP_REST_Request( 'POST', '/wc/store/cart/coupons' ); + $request->set_body_params( + array( + 'code' => $this->coupon->get_code(), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 201, $response->get_status() ); + $this->assertEquals( $this->coupon->get_code(), $data['code'] ); + } + + /** + * Test add to cart does not allow invalid items. + */ + public function test_invalid_create_item() { + wc()->cart->remove_coupons(); + + $request = new WP_REST_Request( 'POST', '/wc/store/cart/coupons' ); + $request->set_body_params( + array( + 'code' => 'IDONOTEXIST', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 400, $response->get_status() ); + } + + /** + * Test delete item. + */ + public function test_delete_item() { + $request = new WP_REST_Request( 'DELETE', '/wc/store/cart/coupons/' . $this->coupon->get_code() ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 204, $response->get_status() ); + $this->assertEmpty( $data ); + + $request = new WP_REST_Request( 'DELETE', '/wc/store/cart/coupons/' . $this->coupon->get_code() ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 404, $response->get_status() ); + + $request = new WP_REST_Request( 'DELETE', '/wc/store/cart/coupons/i-do-not-exist' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test delete all items. + */ + public function test_delete_items() { + $request = new WP_REST_Request( 'DELETE', '/wc/store/cart/coupons' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( [], $data ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/store/cart/coupons' ) ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 0, count( $data ) ); + } + + /** + * Test schema retrieval. + */ + public function test_get_item_schema() { + $controller = new \Automattic\WooCommerce\Blocks\RestApi\StoreApi\Controllers\CartCoupons(); + $schema = $controller->get_item_schema(); + + $this->assertArrayHasKey( 'code', $schema['properties'] ); + $this->assertArrayHasKey( 'totals', $schema['properties'] ); + } + + /** + * Test conversion of cart item to rest response. + */ + public function test_prepare_item_for_response() { + $controller = new \Automattic\WooCommerce\Blocks\RestApi\StoreApi\Controllers\CartCoupons(); + $response = $controller->prepare_item_for_response( $this->coupon->get_code(), [] ); + + $this->assertArrayHasKey( 'code', $response->get_data() ); + $this->assertArrayHasKey( 'totals', $response->get_data() ); + } +}