Store API: Add cart coupon endpoints (https://github.com/woocommerce/woocommerce-blocks/pull/1378)
* Correct docblock * Implement coupons endpoint with add/delete/get functionality * Add totals * Use money formatting * Added tests for new endpoints * Add coupons to main cart endpoint and update docs to match * Fix indenting in readme * is_a to instanceof * Update coupon_exists logic * Documentation/tidy class * Update currency responses for coupon totals w/ tests
This commit is contained in:
parent
ca506d0a48
commit
15dbc627c1
|
@ -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',
|
||||
|
|
|
@ -0,0 +1,252 @@
|
|||
<?php
|
||||
/**
|
||||
* Cart Coupons controller.
|
||||
*
|
||||
* @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions.
|
||||
* @package WooCommerce/Blocks
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\RestApi\StoreApi\Controllers;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use \WP_Error as RestError;
|
||||
use \WP_REST_Server as RestServer;
|
||||
use \WP_REST_Controller as RestController;
|
||||
use \WP_REST_Response as RestResponse;
|
||||
use \WP_REST_Request as RestRequest;
|
||||
use \WC_REST_Exception as RestException;
|
||||
use Automattic\WooCommerce\Blocks\RestApi\StoreApi\Schemas\CartCouponSchema;
|
||||
use Automattic\WooCommerce\Blocks\RestApi\StoreApi\Utilities\CartController;
|
||||
|
||||
/**
|
||||
* Cart Coupons API.
|
||||
*/
|
||||
class CartCoupons extends RestController {
|
||||
/**
|
||||
* Endpoint namespace.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $namespace = 'wc/store';
|
||||
|
||||
/**
|
||||
* Route base.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'cart/coupons';
|
||||
|
||||
/**
|
||||
* Schema class instance.
|
||||
*
|
||||
* @var object
|
||||
*/
|
||||
protected $schema;
|
||||
|
||||
/**
|
||||
* Setup API class.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->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<code>[\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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
<?php
|
||||
/**
|
||||
* Cart Coupon Schema.
|
||||
*
|
||||
* @package WooCommerce/Blocks
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\RestApi\StoreApi\Schemas;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\RestApi\StoreApi\Utilities\CartController;
|
||||
|
||||
/**
|
||||
* CartCouponSchema class.
|
||||
*
|
||||
* @since 2.5.0
|
||||
*/
|
||||
class CartCouponSchema extends AbstractSchema {
|
||||
/**
|
||||
* The schema item name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $title = 'cart_coupon';
|
||||
|
||||
/**
|
||||
* Cart schema properties.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function get_properties() {
|
||||
return [
|
||||
'code' => [
|
||||
'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 ),
|
||||
]
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
|
@ -1,8 +1,6 @@
|
|||
<?php
|
||||
/**
|
||||
* Abstract Schema.
|
||||
*
|
||||
* Rest API schema class.
|
||||
* Cart Item Schema.
|
||||
*
|
||||
* @package WooCommerce/Blocks
|
||||
*/
|
||||
|
@ -14,7 +12,7 @@ defined( 'ABSPATH' ) || exit;
|
|||
use Automattic\WooCommerce\Blocks\RestApi\Utilities\ProductImages;
|
||||
|
||||
/**
|
||||
* AbstractBlock class.
|
||||
* CartItemSchema class.
|
||||
*
|
||||
* @since 2.5.0
|
||||
*/
|
||||
|
|
|
@ -29,6 +29,16 @@ class CartSchema extends AbstractSchema {
|
|||
*/
|
||||
protected function get_properties() {
|
||||
return [
|
||||
'coupons' => [
|
||||
'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' ),
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -0,0 +1,169 @@
|
|||
<?php
|
||||
/**
|
||||
* Controller Tests.
|
||||
*
|
||||
* @package WooCommerce\Blocks\Tests
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\Tests\RestApi\StoreApi\Controllers;
|
||||
|
||||
use \WP_REST_Request;
|
||||
use \WC_REST_Unit_Test_Case as TestCase;
|
||||
use \WC_Helper_Product as ProductHelper;
|
||||
use \WC_Helper_Coupon as CouponHelper;
|
||||
|
||||
/**
|
||||
* Cart Coupons Controller Tests.
|
||||
*/
|
||||
class CartCoupons extends TestCase {
|
||||
/**
|
||||
* Setup test products data. Called before every test.
|
||||
*/
|
||||
public function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
wp_set_current_user( 0 );
|
||||
|
||||
$this->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<code>[\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() );
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue