Coupon and Cart Item Validation Fixes (https://github.com/woocommerce/woocommerce-blocks/pull/2245)
* Move validation calls to checkout API class * Validate before payment * Support additional error data * Add coupon validation before payment, and hide hashes from user * Implement validation and recalculation * Abstract notice handler and implement legacy filters * Handle generic cart item errors * strip tags from coupon error messages woocommerce/woocommerce-blocks#2212 * Ensure item errors are surfaced when coupons are removed * Fix wrong value passed to hook * fix broken checkout when no payment method is there * try fixing unit test errors * if preview data has a receiveCart function use it, otherwise default to an anononymous function * fix tests Co-authored-by: Seghir Nadir <nadir.seghir@gmail.com> Co-authored-by: Darren Ethier <darren@roughsmootheng.in>
This commit is contained in:
parent
ae3a2dc7ad
commit
5b142b58ed
|
@ -57,7 +57,7 @@ const CheckoutProcessor = () => {
|
|||
const { hasValidationErrors } = useValidationContext();
|
||||
const { shippingAddress, shippingErrorStatus } = useShippingDataContext();
|
||||
const { billingData } = useBillingDataContext();
|
||||
const { cartNeedsPayment } = useStoreCart();
|
||||
const { cartNeedsPayment, receiveCart } = useStoreCart();
|
||||
const {
|
||||
activePaymentMethod,
|
||||
currentStatus: currentPaymentStatus,
|
||||
|
@ -228,6 +228,10 @@ const CheckoutProcessor = () => {
|
|||
} )
|
||||
.catch( ( error ) => {
|
||||
error.json().then( function( response ) {
|
||||
// If updated cart state was returned, also update that.
|
||||
if ( response.data?.cart ) {
|
||||
receiveCart( response.data.cart );
|
||||
}
|
||||
dispatchActions.setHasError();
|
||||
dispatchActions.setAfterProcessing( response );
|
||||
setIsProcessingOrder( false );
|
||||
|
|
|
@ -24,6 +24,8 @@ jest.mock( '@woocommerce/block-data', () => ( {
|
|||
describe( 'useStoreCart', () => {
|
||||
let registry, renderer;
|
||||
|
||||
const receiveCartMock = () => {};
|
||||
|
||||
const previewCartData = {
|
||||
cartCoupons: previewCart.coupons,
|
||||
cartItems: previewCart.items,
|
||||
|
@ -79,6 +81,7 @@ describe( 'useStoreCart', () => {
|
|||
shippingRates: [],
|
||||
shippingRatesLoading: false,
|
||||
hasShippingAddress: false,
|
||||
receiveCart: undefined,
|
||||
};
|
||||
|
||||
const getWrappedComponents = ( Component ) => (
|
||||
|
@ -88,8 +91,8 @@ describe( 'useStoreCart', () => {
|
|||
);
|
||||
|
||||
const getTestComponent = ( options ) => () => {
|
||||
const results = useStoreCart( options );
|
||||
return <div results={ results } />;
|
||||
const { receiveCart, ...results } = useStoreCart( options );
|
||||
return <div results={ results } receiveCart={ receiveCart } />;
|
||||
};
|
||||
|
||||
const setUpMocks = () => {
|
||||
|
@ -136,9 +139,15 @@ describe( 'useStoreCart', () => {
|
|||
);
|
||||
} );
|
||||
|
||||
const { results } = renderer.root.findByType( 'div' ).props;
|
||||
|
||||
expect( results ).toEqual( defaultCartData );
|
||||
const { results, receiveCart } = renderer.root.findByType(
|
||||
'div'
|
||||
).props;
|
||||
const {
|
||||
receiveCart: defaultReceiveCart,
|
||||
...remaining
|
||||
} = defaultCartData;
|
||||
expect( results ).toEqual( remaining );
|
||||
expect( receiveCart ).toEqual( defaultReceiveCart );
|
||||
} );
|
||||
|
||||
it( 'return store data when shouldSelect is true', () => {
|
||||
|
@ -150,9 +159,12 @@ describe( 'useStoreCart', () => {
|
|||
);
|
||||
} );
|
||||
|
||||
const { results } = renderer.root.findByType( 'div' ).props;
|
||||
const { results, receiveCart } = renderer.root.findByType(
|
||||
'div'
|
||||
).props;
|
||||
|
||||
expect( results ).toEqual( mockStoreCartData );
|
||||
expect( receiveCart ).toBeUndefined();
|
||||
} );
|
||||
} );
|
||||
|
||||
|
@ -160,7 +172,12 @@ describe( 'useStoreCart', () => {
|
|||
beforeEach( () => {
|
||||
mockBaseContext.useEditorContext.mockReturnValue( {
|
||||
isEditor: true,
|
||||
previewData: { previewCart },
|
||||
previewData: {
|
||||
previewCart: {
|
||||
...previewCart,
|
||||
receiveCart: receiveCartMock,
|
||||
},
|
||||
},
|
||||
} );
|
||||
} );
|
||||
|
||||
|
@ -173,9 +190,12 @@ describe( 'useStoreCart', () => {
|
|||
);
|
||||
} );
|
||||
|
||||
const { results } = renderer.root.findByType( 'div' ).props;
|
||||
const { results, receiveCart } = renderer.root.findByType(
|
||||
'div'
|
||||
).props;
|
||||
|
||||
expect( results ).toEqual( previewCartData );
|
||||
expect( receiveCart ).toEqual( receiveCartMock );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
|
|
@ -36,6 +36,7 @@ export const defaultCartData = {
|
|||
shippingRates: [],
|
||||
shippingRatesLoading: false,
|
||||
hasShippingAddress: false,
|
||||
receiveCart: () => {},
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -56,7 +57,7 @@ export const useStoreCart = ( options = { shouldSelect: true } ) => {
|
|||
const { shouldSelect } = options;
|
||||
|
||||
const results = useSelect(
|
||||
( select ) => {
|
||||
( select, { dispatch } ) => {
|
||||
if ( ! shouldSelect ) {
|
||||
return defaultCartData;
|
||||
}
|
||||
|
@ -82,6 +83,10 @@ export const useStoreCart = ( options = { shouldSelect: true } ) => {
|
|||
shippingRates: previewCart.shipping_rates,
|
||||
shippingRatesLoading: false,
|
||||
hasShippingAddress: false,
|
||||
receiveCart:
|
||||
typeof previewCart?.receiveCart === 'function'
|
||||
? previewCart.receiveCart
|
||||
: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -93,6 +98,7 @@ export const useStoreCart = ( options = { shouldSelect: true } ) => {
|
|||
'getCartData'
|
||||
);
|
||||
const shippingRatesLoading = store.areShippingRatesLoading();
|
||||
const { receiveCart } = dispatch( storeKey );
|
||||
|
||||
return {
|
||||
cartCoupons: cartData.coupons,
|
||||
|
@ -109,6 +115,7 @@ export const useStoreCart = ( options = { shouldSelect: true } ) => {
|
|||
shippingRates: cartData.shippingRates,
|
||||
shippingRatesLoading,
|
||||
hasShippingAddress: !! cartData.shippingAddress.country,
|
||||
receiveCart,
|
||||
};
|
||||
},
|
||||
[ shouldSelect ]
|
||||
|
|
|
@ -67,15 +67,14 @@ const Cart = ( { attributes } ) => {
|
|||
appliedCoupons,
|
||||
} = useStoreCartCoupons();
|
||||
|
||||
const { addErrorNotice, removeNotice } = useStoreNotices();
|
||||
const { addErrorNotice } = useStoreNotices();
|
||||
|
||||
// Ensures any cart errors listed in the API response get shown.
|
||||
useEffect( () => {
|
||||
removeNotice( 'cart-item-error' );
|
||||
cartItemErrors.forEach( ( error ) => {
|
||||
addErrorNotice( decodeEntities( error.message ), {
|
||||
isDismissible: false,
|
||||
id: 'cart-item-error',
|
||||
isDismissible: true,
|
||||
id: error.code,
|
||||
} );
|
||||
} );
|
||||
}, [ cartItemErrors ] );
|
||||
|
|
|
@ -5,3 +5,4 @@ export const PRODUCT_NOT_ENOUGH_STOCK =
|
|||
'woocommerce_rest_cart_product_no_stock';
|
||||
export const PRODUCT_SOLD_INDIVIDUALLY =
|
||||
'woocommerce_rest_cart_product_sold_individually';
|
||||
export const GENERIC_CART_ITEM_ERROR = 'woocommerce_rest_cart_item_error';
|
||||
|
|
|
@ -15,8 +15,17 @@ import {
|
|||
PRODUCT_NOT_PURCHASABLE,
|
||||
PRODUCT_NOT_ENOUGH_STOCK,
|
||||
PRODUCT_SOLD_INDIVIDUALLY,
|
||||
GENERIC_CART_ITEM_ERROR,
|
||||
} from './constants';
|
||||
|
||||
const cartItemErrorCodes = [
|
||||
PRODUCT_OUT_OF_STOCK,
|
||||
PRODUCT_NOT_PURCHASABLE,
|
||||
PRODUCT_NOT_ENOUGH_STOCK,
|
||||
PRODUCT_SOLD_INDIVIDUALLY,
|
||||
GENERIC_CART_ITEM_ERROR,
|
||||
];
|
||||
|
||||
/**
|
||||
* When an order was not created for the checkout, for example, when an item
|
||||
* was out of stock, this component will be shown instead of the checkout form.
|
||||
|
@ -59,12 +68,7 @@ const CheckoutError = () => {
|
|||
const ErrorTitle = ( { errorData } ) => {
|
||||
let heading = __( 'Checkout error', 'woo-gutenberg-products-block' );
|
||||
|
||||
if (
|
||||
errorData.code === PRODUCT_NOT_ENOUGH_STOCK ||
|
||||
errorData.code === PRODUCT_NOT_PURCHASABLE ||
|
||||
errorData.code === PRODUCT_OUT_OF_STOCK ||
|
||||
errorData.code === PRODUCT_SOLD_INDIVIDUALLY
|
||||
) {
|
||||
if ( cartItemErrorCodes.includes( errorData.code ) ) {
|
||||
heading = __(
|
||||
'There is a problem with your cart',
|
||||
'woo-gutenberg-products-block'
|
||||
|
@ -84,12 +88,7 @@ const ErrorTitle = ( { errorData } ) => {
|
|||
const ErrorMessage = ( { errorData } ) => {
|
||||
let message = errorData.message;
|
||||
|
||||
if (
|
||||
errorData.code === PRODUCT_NOT_ENOUGH_STOCK ||
|
||||
errorData.code === PRODUCT_NOT_PURCHASABLE ||
|
||||
errorData.code === PRODUCT_OUT_OF_STOCK ||
|
||||
errorData.code === PRODUCT_SOLD_INDIVIDUALLY
|
||||
) {
|
||||
if ( cartItemErrorCodes.includes( errorData.code ) ) {
|
||||
message =
|
||||
message +
|
||||
' ' +
|
||||
|
@ -111,12 +110,7 @@ const ErrorButton = ( { errorData } ) => {
|
|||
let buttonText = __( 'Retry', 'woo-gutenberg-products-block' );
|
||||
let buttonUrl = 'javascript:window.location.reload(true)';
|
||||
|
||||
if (
|
||||
errorData.code === PRODUCT_NOT_ENOUGH_STOCK ||
|
||||
errorData.code === PRODUCT_NOT_PURCHASABLE ||
|
||||
errorData.code === PRODUCT_OUT_OF_STOCK ||
|
||||
errorData.code === PRODUCT_SOLD_INDIVIDUALLY
|
||||
) {
|
||||
if ( cartItemErrorCodes.includes( errorData.code ) ) {
|
||||
buttonText = __( 'Edit your cart', 'woo-gutenberg-products-block' );
|
||||
buttonUrl = CART_URL;
|
||||
}
|
||||
|
|
|
@ -158,8 +158,8 @@ export const useCheckoutSubscriptions = (
|
|||
messageContext: emitResponse.noticeContexts.PAYMENTS,
|
||||
};
|
||||
}
|
||||
// leave for checkout to handle.
|
||||
return null;
|
||||
// so we don't break the observers.
|
||||
return true;
|
||||
};
|
||||
const unsubscribeProcessing = eventRegistration.onPaymentProcessing(
|
||||
onSubmit
|
||||
|
|
|
@ -34,6 +34,8 @@
|
|||
* being loaded.
|
||||
* @property {boolean} hasShippingAddress Whether or not the cart
|
||||
* has a shipping address yet.
|
||||
* @property {function} receiveCart Dispatcher to receive
|
||||
* updated cart.
|
||||
*/
|
||||
|
||||
/**
|
||||
|
|
|
@ -12,6 +12,7 @@ defined( 'ABSPATH' ) || exit;
|
|||
use Automattic\WooCommerce\Blocks\Payments\PaymentResult;
|
||||
use Automattic\WooCommerce\Blocks\Payments\PaymentContext;
|
||||
use Automattic\WooCommerce\Blocks\Payments\PaymentMethodRegistry;
|
||||
use Automattic\WooCommerce\Blocks\RestApi\StoreApi\Utilities\NoticeHandler;
|
||||
|
||||
/**
|
||||
* Library class.
|
||||
|
@ -240,7 +241,7 @@ class Library {
|
|||
$payment_method_object->validate_fields();
|
||||
|
||||
// If errors were thrown, we need to abort.
|
||||
self::convert_notices_to_exceptions();
|
||||
NoticeHandler::convert_notices_to_exceptions( 'woocommerce_rest_payment_error' );
|
||||
|
||||
// Process Payment.
|
||||
$gateway_result = $payment_method_object->process_payment( $context->order->get_id() );
|
||||
|
@ -259,29 +260,4 @@ class Library {
|
|||
$result->set_payment_details( array_merge( $result->payment_details, $gateway_result ) );
|
||||
$result->set_redirect_url( $gateway_result['redirect'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert notices to Exceptions.
|
||||
*
|
||||
* Payment methods may add error notices during validate_fields call to prevent checkout. Since we're not rendering
|
||||
* notices at all, we need to convert them to exceptions.
|
||||
*
|
||||
* This method will find the first error message and thrown an exception instead.
|
||||
*
|
||||
* @throws \Exception If an error notice is detected, Exception is thrown.
|
||||
*/
|
||||
protected static function convert_notices_to_exceptions() {
|
||||
if ( 0 === wc_notice_count( 'error' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$error_notices = wc_get_notices( 'error' );
|
||||
|
||||
// Prevent notices from being output later on.
|
||||
wc_clear_notices();
|
||||
|
||||
foreach ( $error_notices as $error_notice ) {
|
||||
throw new \Exception( $error_notice['notice'] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,9 +30,10 @@ abstract class AbstractCartRoute extends AbstractRoute {
|
|||
* @param string $error_code String based error code.
|
||||
* @param string $error_message User facing error message.
|
||||
* @param int $http_status_code HTTP status. Defaults to 500.
|
||||
* @param array $additional_data Extra data (key value pairs) to expose in the error response.
|
||||
* @return \WP_Error WP Error object.
|
||||
*/
|
||||
protected function get_route_error_response( $error_code, $error_message, $http_status_code = 500 ) {
|
||||
protected function get_route_error_response( $error_code, $error_message, $http_status_code = 500, $additional_data = [] ) {
|
||||
switch ( $http_status_code ) {
|
||||
case 409:
|
||||
// If there was a conflict, return the cart so the client can resolve it.
|
||||
|
@ -42,10 +43,13 @@ abstract class AbstractCartRoute extends AbstractRoute {
|
|||
return new \WP_Error(
|
||||
$error_code,
|
||||
$error_message,
|
||||
[
|
||||
'status' => $http_status_code,
|
||||
'cart' => $this->schema->get_item_response( $cart ),
|
||||
]
|
||||
array_merge(
|
||||
$additional_data,
|
||||
[
|
||||
'status' => $http_status_code,
|
||||
'cart' => $this->schema->get_item_response( $cart ),
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
return new \WP_Error( $error_code, $error_message, [ 'status' => $http_status_code ] );
|
||||
|
|
|
@ -72,7 +72,7 @@ abstract class AbstractRoute implements RouteInterface {
|
|||
$response->header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) );
|
||||
}
|
||||
} catch ( RouteException $error ) {
|
||||
$response = $this->get_route_error_response( $error->getErrorCode(), $error->getMessage(), $error->getCode() );
|
||||
$response = $this->get_route_error_response( $error->getErrorCode(), $error->getMessage(), $error->getCode(), $error->getAdditionalData() );
|
||||
} catch ( \Exception $error ) {
|
||||
$response = $this->get_route_error_response( 'unknown_server_error', $error->getMessage(), 500 );
|
||||
}
|
||||
|
@ -161,10 +161,11 @@ abstract class AbstractRoute implements RouteInterface {
|
|||
* @param string $error_code String based error code.
|
||||
* @param string $error_message User facing error message.
|
||||
* @param int $http_status_code HTTP status. Defaults to 500.
|
||||
* @param array $additional_data Extra data (key value pairs) to expose in the error response.
|
||||
* @return \WP_Error WP Error object.
|
||||
*/
|
||||
protected function get_route_error_response( $error_code, $error_message, $http_status_code = 500 ) {
|
||||
return new \WP_Error( $error_code, $error_message, [ 'status' => $http_status_code ] );
|
||||
protected function get_route_error_response( $error_code, $error_message, $http_status_code = 500, $additional_data = [] ) {
|
||||
return new \WP_Error( $error_code, $error_message, array_merge( $additional_data, [ 'status' => $http_status_code ] ) );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -163,7 +163,16 @@ class Checkout extends AbstractRoute {
|
|||
);
|
||||
}
|
||||
|
||||
// Ensure order still matches cart.
|
||||
$order_controller->update_order_from_cart( $order_object );
|
||||
|
||||
// If any form fields were posted, update the order.
|
||||
$this->update_order_from_request( $order_object, $request );
|
||||
|
||||
// Check order is still valid.
|
||||
$order_controller->validate_order_before_payment( $order_object );
|
||||
|
||||
// Persist customer address data to account.
|
||||
$order_controller->sync_customer_data_with_order( $order_object );
|
||||
|
||||
if ( ! $order_object->needs_payment() ) {
|
||||
|
@ -198,6 +207,37 @@ class Checkout extends AbstractRoute {
|
|||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get route response when something went wrong.
|
||||
*
|
||||
* @param string $error_code String based error code.
|
||||
* @param string $error_message User facing error message.
|
||||
* @param int $http_status_code HTTP status. Defaults to 500.
|
||||
* @param array $additional_data Extra data (key value pairs) to expose in the error response.
|
||||
* @return \WP_Error WP Error object.
|
||||
*/
|
||||
protected function get_route_error_response( $error_code, $error_message, $http_status_code = 500, $additional_data = [] ) {
|
||||
switch ( $http_status_code ) {
|
||||
case 409:
|
||||
// If there was a conflict, return the cart so the client can resolve it.
|
||||
$controller = new CartController();
|
||||
$cart = $controller->get_cart_instance();
|
||||
|
||||
return new \WP_Error(
|
||||
$error_code,
|
||||
$error_message,
|
||||
array_merge(
|
||||
$additional_data,
|
||||
[
|
||||
'status' => $http_status_code,
|
||||
'cart' => wc()->api->get_endpoint_data( '/wc/store/cart' ),
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
return new \WP_Error( $error_code, $error_message, [ 'status' => $http_status_code ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets draft order data from the customer session.
|
||||
*
|
||||
|
@ -246,14 +286,20 @@ class Checkout extends AbstractRoute {
|
|||
* Create or update a draft order based on the cart.
|
||||
*
|
||||
* @throws RouteException On error.
|
||||
*
|
||||
* @return \WC_Order Order object.
|
||||
*/
|
||||
protected function create_or_update_draft_order() {
|
||||
$cart_controller = new CartController();
|
||||
$order_controller = new OrderController();
|
||||
$reserve_stock = \class_exists( '\Automattic\WooCommerce\Checkout\Helpers\ReserveStock' ) ? new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() : new ReserveStock();
|
||||
$order_object = $this->get_draft_order_object( $this->get_draft_order_id() );
|
||||
$created = false;
|
||||
|
||||
// Validate items etc are allowed in the order before it gets created.
|
||||
$cart_controller->validate_cart_items();
|
||||
$cart_controller->validate_cart_coupons();
|
||||
|
||||
if ( ! $order_object ) {
|
||||
$order_object = $order_controller->create_order_from_cart();
|
||||
$created = true;
|
||||
|
|
|
@ -18,16 +18,24 @@ class RouteException extends \Exception {
|
|||
*/
|
||||
public $error_code;
|
||||
|
||||
/**
|
||||
* Additional error data.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $additional_data = [];
|
||||
|
||||
/**
|
||||
* Setup exception.
|
||||
*
|
||||
* @param string $error_code Machine-readable error code, e.g `woocommerce_invalid_product_id`.
|
||||
* @param string $message User-friendly translated error message, e.g. 'Product ID is invalid'.
|
||||
* @param int $http_status_code Proper HTTP status code to respond with, e.g. 400.
|
||||
* @param array $additional_data Extra data (key value pairs) to expose in the error response.
|
||||
*/
|
||||
public function __construct( $error_code, $message, $http_status_code = 400 ) {
|
||||
$this->error_code = $error_code;
|
||||
|
||||
public function __construct( $error_code, $message, $http_status_code = 400, $additional_data = [] ) {
|
||||
$this->error_code = $error_code;
|
||||
$this->additional_data = array_filter( (array) $additional_data );
|
||||
parent::__construct( $message, $http_status_code );
|
||||
}
|
||||
|
||||
|
@ -39,4 +47,13 @@ class RouteException extends \Exception {
|
|||
public function getErrorCode() {
|
||||
return $this->error_code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns additional error data.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAdditionalData() {
|
||||
return $this->additional_data;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -268,7 +268,9 @@ class CartSchema extends AbstractSchema {
|
|||
*/
|
||||
public function get_item_response( $cart ) {
|
||||
$controller = new CartController();
|
||||
$context = 'edit';
|
||||
|
||||
// Get cart errors first so if recalculations are performed, it's reflected in the response.
|
||||
$cart_errors = $this->get_cart_errors( $cart );
|
||||
|
||||
return [
|
||||
'coupons' => array_values( array_map( [ $this->coupon_schema, 'get_item_response' ], array_filter( $cart->get_applied_coupons() ) ) ),
|
||||
|
@ -292,12 +294,12 @@ class CartSchema extends AbstractSchema {
|
|||
'total_shipping_tax' => $this->prepare_money_response( $cart->get_shipping_tax(), wc_get_price_decimals() ),
|
||||
|
||||
// Explicitly request context='edit'; default ('view') will render total as markup.
|
||||
'total_price' => $this->prepare_money_response( $cart->get_total( $context ), wc_get_price_decimals() ),
|
||||
'total_price' => $this->prepare_money_response( $cart->get_total( 'edit' ), wc_get_price_decimals() ),
|
||||
'total_tax' => $this->prepare_money_response( $cart->get_total_tax(), wc_get_price_decimals() ),
|
||||
'tax_lines' => $this->get_tax_lines( $cart ),
|
||||
]
|
||||
),
|
||||
'errors' => $this->get_cart_errors( $cart ),
|
||||
'errors' => $cart_errors,
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -328,9 +330,10 @@ class CartSchema extends AbstractSchema {
|
|||
* @return array
|
||||
*/
|
||||
protected function get_cart_errors( $cart ) {
|
||||
$controller = new CartController();
|
||||
$errors = $controller->get_cart_item_errors();
|
||||
$controller = new CartController();
|
||||
$item_errors = $controller->get_cart_item_errors();
|
||||
$coupon_errors = $controller->get_cart_coupon_errors();
|
||||
|
||||
return array_values( array_map( [ $this->error_schema, 'get_item_response' ], $errors ) );
|
||||
return array_values( array_map( [ $this->error_schema, 'get_item_response' ], array_merge( $item_errors, $coupon_errors ) ) );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ namespace Automattic\WooCommerce\Blocks\RestApi\StoreApi\Utilities;
|
|||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\RestApi\StoreApi\Routes\RouteException;
|
||||
use Automattic\WooCommerce\Blocks\RestApi\StoreApi\Utilities\NoticeHandler;
|
||||
|
||||
/**
|
||||
* Woo Cart Controller class.
|
||||
|
@ -43,65 +44,17 @@ class CartController {
|
|||
|
||||
$request = $this->filter_request_data( $this->parse_variation_data( $request ) );
|
||||
$product = $this->get_product_for_cart( $request );
|
||||
|
||||
if ( $product->is_type( 'variation' ) ) {
|
||||
$product_id = $product->get_parent_id();
|
||||
$variation_id = $product->get_id();
|
||||
} else {
|
||||
$product_id = $product->get_id();
|
||||
$variation_id = 0;
|
||||
}
|
||||
|
||||
$cart_id = wc()->cart->generate_cart_id(
|
||||
$product_id,
|
||||
$variation_id,
|
||||
$cart_id = wc()->cart->generate_cart_id(
|
||||
$this->get_product_id( $product ),
|
||||
$this->get_variation_id( $product ),
|
||||
$request['variation'],
|
||||
$request['cart_item_data']
|
||||
);
|
||||
|
||||
$this->validate_add_to_cart( $product, $request );
|
||||
|
||||
$existing_cart_id = wc()->cart->find_product_in_cart( $cart_id );
|
||||
|
||||
if ( ! $product->is_purchasable() ) {
|
||||
throw new RouteException(
|
||||
'woocommerce_rest_cart_product_is_not_purchasable',
|
||||
sprintf(
|
||||
/* translators: %s: product name */
|
||||
__( '"%s" is not available for purchase.', 'woo-gutenberg-products-block' ),
|
||||
$product->get_name()
|
||||
),
|
||||
403
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! $product->is_in_stock() ) {
|
||||
throw new RouteException(
|
||||
'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() ) {
|
||||
$qty_remaining = $this->get_remaining_stock_for_product( $product );
|
||||
$qty_in_cart = $this->get_product_quantity_in_cart( $product );
|
||||
|
||||
if ( $qty_remaining < $qty_in_cart + $request['quantity'] ) {
|
||||
throw new RouteException(
|
||||
'woocommerce_rest_cart_product_no_stock',
|
||||
sprintf(
|
||||
/* translators: 1: product name 2: quantity in stock */
|
||||
__( 'You cannot add that amount of "%1$s" to the cart because there is not enough stock (%2$s remaining).', 'woo-gutenberg-products-block' ),
|
||||
$product->get_name(),
|
||||
wc_format_stock_quantity_for_display( $product->get_stock_quantity(), $product )
|
||||
),
|
||||
403
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ( $existing_cart_id ) {
|
||||
if ( $product->is_sold_individually() ) {
|
||||
throw new RouteException(
|
||||
|
@ -125,8 +78,8 @@ class CartController {
|
|||
$request['cart_item_data'],
|
||||
array(
|
||||
'key' => $cart_id,
|
||||
'product_id' => $product_id,
|
||||
'variation_id' => $variation_id,
|
||||
'product_id' => $this->get_product_id( $product ),
|
||||
'variation_id' => $this->get_variation_id( $product ),
|
||||
'variation' => $request['variation'],
|
||||
'quantity' => $request['quantity'],
|
||||
'data' => $product,
|
||||
|
@ -141,9 +94,9 @@ class CartController {
|
|||
do_action(
|
||||
'woocommerce_add_to_cart',
|
||||
$cart_id,
|
||||
$product_id,
|
||||
$this->get_product_id( $product ),
|
||||
$request['quantity'],
|
||||
$variation_id,
|
||||
$this->get_variation_id( $product ),
|
||||
$request['variation'],
|
||||
$request['cart_item_data']
|
||||
);
|
||||
|
@ -187,6 +140,83 @@ class CartController {
|
|||
wc()->cart->set_quantity( $item_id, $quantity );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate all items in the cart and check for errors.
|
||||
*
|
||||
* @throws RouteException Exception if invalid data is detected.
|
||||
*
|
||||
* @param \WC_Product $product Product object associated with the cart item.
|
||||
* @param array $request Add to cart request params.
|
||||
*/
|
||||
public function validate_add_to_cart( \WC_Product $product, $request ) {
|
||||
if ( ! $product->is_purchasable() ) {
|
||||
$this->throw_default_product_exception( $product );
|
||||
}
|
||||
|
||||
if ( ! $product->is_in_stock() ) {
|
||||
throw new RouteException(
|
||||
'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() ) {
|
||||
$qty_remaining = $this->get_remaining_stock_for_product( $product );
|
||||
$qty_in_cart = $this->get_product_quantity_in_cart( $product );
|
||||
|
||||
if ( $qty_remaining < $qty_in_cart + $request['quantity'] ) {
|
||||
throw new RouteException(
|
||||
'woocommerce_rest_cart_product_no_stock',
|
||||
sprintf(
|
||||
/* translators: 1: product name 2: quantity in stock */
|
||||
__( 'You cannot add that amount of "%1$s" to the cart because there is not enough stock (%2$s remaining).', 'woo-gutenberg-products-block' ),
|
||||
$product->get_name(),
|
||||
wc_format_stock_quantity_for_display( $product->get_stock_quantity(), $product )
|
||||
),
|
||||
403
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook: woocommerce_add_to_cart_validation (legacy).
|
||||
*
|
||||
* Allow 3rd parties to validate if an item can be added to the cart. This is a legacy hook from Woo core.
|
||||
* This filter will be deprecated because it encourages usage of wc_add_notice. For the API we need to capture
|
||||
* notices and convert to exceptions instead.
|
||||
*/
|
||||
$passed_validation = apply_filters(
|
||||
'woocommerce_add_to_cart_validation',
|
||||
true,
|
||||
$this->get_product_id( $product ),
|
||||
$request['quantity'],
|
||||
$this->get_variation_id( $product ),
|
||||
$request['variation']
|
||||
);
|
||||
|
||||
if ( ! $passed_validation ) {
|
||||
// Validation did not pass - see if an error notice was thrown.
|
||||
NoticeHandler::convert_notices_to_exceptions( 'woocommerce_rest_add_to_cart_error' );
|
||||
|
||||
// If no notice was thrown, throw the default notice instead.
|
||||
$this->throw_default_product_exception( $product );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire action to validate add to cart. Functions hooking into this should throw an \Exception to prevent
|
||||
* add to cart from occuring.
|
||||
*
|
||||
* @param \WC_Product $product Product object being added to the cart.
|
||||
* @param array $request Add to cart request params including id, quantity, and variation attributes.
|
||||
*/
|
||||
do_action( 'wooocommerce_store_api_validate_add_to_cart', $product, $request );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate all items in the cart and check for errors.
|
||||
*
|
||||
|
@ -198,26 +228,16 @@ class CartController {
|
|||
foreach ( $cart_items as $cart_item_key => $cart_item ) {
|
||||
$this->validate_cart_item( $cart_item );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate all items in the cart and get a list of errors.
|
||||
*
|
||||
* @throws RouteException Exception if invalid data is detected.
|
||||
*/
|
||||
public function get_cart_item_errors() {
|
||||
$errors = [];
|
||||
$cart_items = $this->get_cart_items();
|
||||
|
||||
foreach ( $cart_items as $cart_item_key => $cart_item ) {
|
||||
try {
|
||||
$this->validate_cart_item( $cart_item );
|
||||
} catch ( RouteException $error ) {
|
||||
$errors[] = new \WP_Error( $error->getErrorCode(), $error->getMessage() );
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
/**
|
||||
* Hook: woocommerce_check_cart_items
|
||||
*
|
||||
* Allow 3rd parties to validate cart items. This is a legacy hook from Woo core.
|
||||
* This filter will be deprecated because it encourages usage of wc_add_notice. For the API we need to capture
|
||||
* notices and convert to exceptions instead.
|
||||
*/
|
||||
do_action( 'woocommerce_check_cart_items' );
|
||||
NoticeHandler::convert_notices_to_exceptions( 'woocommerce_rest_cart_item_error' );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -225,9 +245,9 @@ class CartController {
|
|||
*
|
||||
* @throws RouteException Exception if invalid data is detected.
|
||||
*
|
||||
* @param array $cart_item Cart item data.
|
||||
* @param array $cart_item Cart item array.
|
||||
*/
|
||||
protected function validate_cart_item( $cart_item ) {
|
||||
public function validate_cart_item( $cart_item ) {
|
||||
$product = $cart_item['data'];
|
||||
|
||||
if ( ! $product instanceof \WC_Product ) {
|
||||
|
@ -235,15 +255,7 @@ class CartController {
|
|||
}
|
||||
|
||||
if ( ! $product->is_purchasable() ) {
|
||||
throw new RouteException(
|
||||
'woocommerce_rest_cart_product_is_not_purchasable',
|
||||
sprintf(
|
||||
/* translators: %s: product name */
|
||||
__( '"%s" is not available for purchase.', 'woo-gutenberg-products-block' ),
|
||||
$product->get_name()
|
||||
),
|
||||
403
|
||||
);
|
||||
$this->throw_default_product_exception( $product );
|
||||
}
|
||||
|
||||
if ( $product->is_sold_individually() && $cart_item['quantity'] > 1 ) {
|
||||
|
@ -292,39 +304,70 @@ class CartController {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire action to validate add to cart. Functions hooking into this should throw an \Exception to prevent
|
||||
* add to cart from occuring.
|
||||
*
|
||||
* @param \WC_Product $product Product object being added to the cart.
|
||||
* @param array $cart_item Cart item array.
|
||||
*/
|
||||
do_action( 'wooocommerce_store_api_validate_cart_item', $product, $cart_item );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the qty of a product across line items.
|
||||
* Validate all coupons in the cart and check for errors.
|
||||
*
|
||||
* @param \WC_Product $product Product object.
|
||||
* @return int
|
||||
* @throws RouteException Exception if invalid data is detected.
|
||||
*/
|
||||
protected function get_product_quantity_in_cart( $product ) {
|
||||
$product_quantities = wc()->cart->get_cart_item_quantities();
|
||||
$product_id = $product->get_stock_managed_by_id();
|
||||
public function validate_cart_coupons() {
|
||||
$cart_coupons = $this->get_cart_coupons();
|
||||
|
||||
return isset( $product_quantities[ $product_id ] ) ? $product_quantities[ $product_id ] : 0;
|
||||
foreach ( $cart_coupons as $code ) {
|
||||
$coupon = new \WC_Coupon( $code );
|
||||
$this->validate_cart_coupon( $coupon );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets remaining stock for a product.
|
||||
* Validate all items in the cart and get a list of errors.
|
||||
*
|
||||
* @param \WC_Product $product Product object.
|
||||
* @return int
|
||||
* @throws RouteException Exception if invalid data is detected.
|
||||
*/
|
||||
protected function get_remaining_stock_for_product( $product ) {
|
||||
// @todo Remove once min support for WC reaches 4.1.0.
|
||||
if ( \class_exists( '\Automattic\WooCommerce\Checkout\Helpers\ReserveStock' ) ) {
|
||||
$reserve_stock_controller = new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock();
|
||||
} else {
|
||||
$reserve_stock_controller = new \Automattic\WooCommerce\Blocks\RestApi\StoreApi\Utilities\ReserveStock();
|
||||
public function get_cart_item_errors() {
|
||||
$errors = [];
|
||||
$cart_items = $this->get_cart_items();
|
||||
|
||||
foreach ( $cart_items as $cart_item_key => $cart_item ) {
|
||||
try {
|
||||
$this->validate_cart_item( $cart_item );
|
||||
} catch ( RouteException $error ) {
|
||||
$errors[] = new \WP_Error( $error->getErrorCode(), $error->getMessage() );
|
||||
}
|
||||
}
|
||||
|
||||
$draft_order = WC()->session->get( 'store_api_draft_order', 0 );
|
||||
$qty_reserved = $reserve_stock_controller->get_reserved_stock( $product, $draft_order );
|
||||
return $errors;
|
||||
}
|
||||
|
||||
return $product->get_stock_quantity() - $qty_reserved;
|
||||
/**
|
||||
* Validate all items in the cart and get a list of errors.
|
||||
*
|
||||
* @throws RouteException Exception if invalid data is detected.
|
||||
*/
|
||||
public function get_cart_coupon_errors() {
|
||||
$errors = [];
|
||||
$cart_coupons = $this->get_cart_coupons();
|
||||
|
||||
foreach ( $cart_coupons as $code ) {
|
||||
try {
|
||||
$coupon = new \WC_Coupon( $code );
|
||||
$this->validate_cart_coupon( $coupon );
|
||||
} catch ( RouteException $error ) {
|
||||
$errors[] = new \WP_Error( $error->getErrorCode(), $error->getMessage() );
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -370,11 +413,11 @@ class CartController {
|
|||
*/
|
||||
public function get_cart_hashes() {
|
||||
return [
|
||||
'line_items' => WC()->cart->get_cart_hash(),
|
||||
'shipping' => md5( wp_json_encode( WC()->cart->shipping_methods ) ),
|
||||
'fees' => md5( wp_json_encode( WC()->cart->get_fees() ) ),
|
||||
'coupons' => md5( wp_json_encode( WC()->cart->get_applied_coupons() ) ),
|
||||
'taxes' => md5( wp_json_encode( WC()->cart->get_taxes() ) ),
|
||||
'line_items' => wc()->cart->get_cart_hash(),
|
||||
'shipping' => md5( wp_json_encode( wc()->cart->shipping_methods ) ),
|
||||
'fees' => md5( wp_json_encode( wc()->cart->get_fees() ) ),
|
||||
'coupons' => md5( wp_json_encode( wc()->cart->get_applied_coupons() ) ),
|
||||
'taxes' => md5( wp_json_encode( wc()->cart->get_taxes() ) ),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -430,7 +473,7 @@ class CartController {
|
|||
}
|
||||
}
|
||||
|
||||
return $calculate_rates ? WC()->shipping()->calculate_shipping( $packages ) : $packages;
|
||||
return $calculate_rates ? wc()->shipping()->calculate_shipping( $packages ) : $packages;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -441,10 +484,10 @@ class CartController {
|
|||
*/
|
||||
public function select_shipping_rate( $package_id, $rate_id ) {
|
||||
$cart = $this->get_cart_instance();
|
||||
$session_data = WC()->session->get( 'chosen_shipping_methods' ) ? WC()->session->get( 'chosen_shipping_methods' ) : [];
|
||||
$session_data = wc()->session->get( 'chosen_shipping_methods' ) ? wc()->session->get( 'chosen_shipping_methods' ) : [];
|
||||
$session_data[ $package_id ] = $rate_id;
|
||||
|
||||
WC()->session->set( 'chosen_shipping_methods', $session_data );
|
||||
wc()->session->set( 'chosen_shipping_methods', $session_data );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -490,7 +533,7 @@ class CartController {
|
|||
if ( ! $coupon->is_valid() ) {
|
||||
throw new RouteException(
|
||||
'woocommerce_rest_cart_coupon_error',
|
||||
$coupon->get_error_message(),
|
||||
wp_strip_all_tags( $coupon->get_error_message() ),
|
||||
403
|
||||
);
|
||||
}
|
||||
|
@ -533,6 +576,63 @@ class CartController {
|
|||
do_action( 'woocommerce_applied_coupon', $coupon_code );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an existing cart coupon and returns any errors.
|
||||
*
|
||||
* @throws RouteException Exception if invalid data is detected.
|
||||
*
|
||||
* @param \WC_Coupon $coupon Coupon object applied to the cart.
|
||||
*/
|
||||
protected function validate_cart_coupon( \WC_Coupon $coupon ) {
|
||||
if ( ! $coupon->is_valid() ) {
|
||||
wc()->cart->remove_coupon( $coupon->get_code() );
|
||||
wc()->cart->calculate_totals();
|
||||
throw new RouteException(
|
||||
'woocommerce_rest_cart_coupon_error',
|
||||
sprintf(
|
||||
// translators: %1$s coupon code, %2$s reason.
|
||||
__( 'The "%1$s" coupon has been removed from your cart: %2$s', 'woo-gutenberg-products-block' ),
|
||||
$coupon->get_code(),
|
||||
wp_strip_all_tags( $coupon->get_error_message() )
|
||||
),
|
||||
409
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the qty of a product across line items.
|
||||
*
|
||||
* @param \WC_Product $product Product object.
|
||||
* @return int
|
||||
*/
|
||||
protected function get_product_quantity_in_cart( $product ) {
|
||||
$product_quantities = wc()->cart->get_cart_item_quantities();
|
||||
$product_id = $product->get_stock_managed_by_id();
|
||||
|
||||
return isset( $product_quantities[ $product_id ] ) ? $product_quantities[ $product_id ] : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets remaining stock for a product.
|
||||
*
|
||||
* @param \WC_Product $product Product object.
|
||||
* @return int
|
||||
*/
|
||||
protected function get_remaining_stock_for_product( $product ) {
|
||||
// @todo Remove once min support for WC reaches 4.1.0.
|
||||
if ( \class_exists( '\Automattic\WooCommerce\Checkout\Helpers\ReserveStock' ) ) {
|
||||
$reserve_stock_controller = new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock();
|
||||
} else {
|
||||
$reserve_stock_controller = new \Automattic\WooCommerce\Blocks\RestApi\StoreApi\Utilities\ReserveStock();
|
||||
}
|
||||
|
||||
$draft_order = wc()->session->get( 'store_api_draft_order', 0 );
|
||||
$qty_reserved = $reserve_stock_controller->get_reserved_stock( $product, $draft_order );
|
||||
|
||||
return $product->get_stock_quantity() - $qty_reserved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a product object to be added to the cart.
|
||||
*
|
||||
|
@ -555,6 +655,45 @@ class CartController {
|
|||
return $product;
|
||||
}
|
||||
|
||||
/**
|
||||
* For a given product, get the product ID.
|
||||
*
|
||||
* @param \WC_Product $product Product object associated with the cart item.
|
||||
* @return int
|
||||
*/
|
||||
protected function get_product_id( \WC_Product $product ) {
|
||||
return $product->is_type( 'variation' ) ? $product->get_parent_id() : $product->get_id();
|
||||
}
|
||||
|
||||
/**
|
||||
* For a given product, get the variation ID.
|
||||
*
|
||||
* @param \WC_Product $product Product object associated with the cart item.
|
||||
* @return int
|
||||
*/
|
||||
protected function get_variation_id( \WC_Product $product ) {
|
||||
return $product->is_type( 'variation' ) ? $product->get_id() : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default exception thrown when an item cannot be added to the cart.
|
||||
*
|
||||
* @throws RouteException Exception with code woocommerce_rest_cart_product_is_not_purchasable.
|
||||
*
|
||||
* @param \WC_Product $product Product object associated with the cart item.
|
||||
*/
|
||||
protected function throw_default_product_exception( \WC_Product $product ) {
|
||||
throw new RouteException(
|
||||
'woocommerce_rest_cart_product_is_not_purchasable',
|
||||
sprintf(
|
||||
/* translators: %s: product name */
|
||||
__( '"%s" is not available for purchase.', 'woo-gutenberg-products-block' ),
|
||||
$product->get_name()
|
||||
),
|
||||
403
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter data for add to cart requests.
|
||||
*
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
/**
|
||||
* Helper class to convert notices to exceptions.
|
||||
*
|
||||
* @package WooCommerce/Blocks
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\RestApi\StoreApi\Utilities;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\RestApi\StoreApi\Routes\RouteException;
|
||||
|
||||
/**
|
||||
* NoticeHandler class.
|
||||
*/
|
||||
class NoticeHandler {
|
||||
|
||||
/**
|
||||
* Convert queued error notices into an exception.
|
||||
*
|
||||
* For example, Payment methods may add error notices during validate_fields call to prevent checkout.
|
||||
* Since we're not rendering notices at all, we need to convert them to exceptions.
|
||||
*
|
||||
* This method will find the first error message and thrown an exception instead. Discards notices once complete.
|
||||
*
|
||||
* @throws RouteException If an error notice is detected, Exception is thrown.
|
||||
*
|
||||
* @param string $error_code Error code for the thrown exceptions.
|
||||
*/
|
||||
public static function convert_notices_to_exceptions( $error_code = 'unknown_server_error' ) {
|
||||
if ( 0 === wc_notice_count( 'error' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$error_notices = wc_get_notices( 'error' );
|
||||
|
||||
// Prevent notices from being output later on.
|
||||
wc_clear_notices();
|
||||
|
||||
foreach ( $error_notices as $error_notice ) {
|
||||
throw new RouteException( $error_code, $error_notice['notice'], 400 );
|
||||
}
|
||||
}
|
||||
}
|
|
@ -97,6 +97,100 @@ class OrderController {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Final validation ran before payment is taken.
|
||||
*
|
||||
* By this point we have an order populated with customer data and items.
|
||||
*
|
||||
* @throws RouteException Exception if invalid data is detected.
|
||||
*
|
||||
* @param \WC_Order $order Order object.
|
||||
*/
|
||||
public function validate_order_before_payment( \WC_Order $order ) {
|
||||
$coupons = $order->get_coupon_codes();
|
||||
|
||||
$coupon_errors = [];
|
||||
|
||||
foreach ( $coupons as $coupon_code ) {
|
||||
$coupon = new \WC_Coupon( $coupon_code );
|
||||
|
||||
try {
|
||||
$this->validate_coupon_email_restriction( $coupon, $order );
|
||||
} catch ( \Exception $error ) {
|
||||
$coupon_errors[ $coupon_code ] = $error->getMessage();
|
||||
}
|
||||
try {
|
||||
$this->validate_coupon_usage_limit( $coupon, $order );
|
||||
} catch ( \Exception $error ) {
|
||||
$coupon_errors[ $coupon_code ] = $error->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
if ( $coupon_errors ) {
|
||||
// Remove all coupons that were not valid.
|
||||
foreach ( $coupon_errors as $coupon_code => $message ) {
|
||||
wc()->cart->remove_coupon( $coupon_code );
|
||||
}
|
||||
|
||||
// Recalculate totals.
|
||||
wc()->cart->calculate_totals();
|
||||
|
||||
// Re-sync order with cart.
|
||||
$this->update_order_from_cart( $order );
|
||||
|
||||
// Return exception so customer can review before payment.
|
||||
throw new RouteException(
|
||||
'woocommerce_rest_cart_coupon_errors',
|
||||
sprintf(
|
||||
// Translators: %s Coupon codes.
|
||||
__( 'Invalid coupons were removed from the cart: "%s"', 'woo-gutenberg-products-block' ),
|
||||
implode( '", "', array_keys( $coupon_errors ) )
|
||||
),
|
||||
409,
|
||||
[
|
||||
'removed_coupons' => $coupon_errors,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check email restrictions of a coupon against the order.
|
||||
*
|
||||
* @throws \Exception Exception if invalid data is detected.
|
||||
*
|
||||
* @param \WC_Coupon $coupon Coupon object applied to the cart.
|
||||
* @param \WC_Order $order Order object.
|
||||
*/
|
||||
protected function validate_coupon_email_restriction( \WC_Coupon $coupon, $order ) {
|
||||
$restrictions = $coupon->get_email_restrictions();
|
||||
|
||||
if ( ! empty( $restrictions ) && $order->get_billing_email() && ! wc()->cart->is_coupon_emails_allowed( [ $order->get_billing_email() ], $restrictions ) ) {
|
||||
throw new \Exception( $coupon->get_coupon_error( \WC_Coupon::E_WC_COUPON_NOT_YOURS_REMOVED ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check usage restrictions of a coupon against the order.
|
||||
*
|
||||
* @throws \Exception Exception if invalid data is detected.
|
||||
*
|
||||
* @param \WC_Coupon $coupon Coupon object applied to the cart.
|
||||
* @param \WC_Order $order Order object.
|
||||
*/
|
||||
protected function validate_coupon_usage_limit( \WC_Coupon $coupon, $order ) {
|
||||
$coupon_usage_limit = $coupon->get_usage_limit_per_user();
|
||||
|
||||
if ( $coupon_usage_limit > 0 ) {
|
||||
$data_store = $coupon->get_data_store();
|
||||
$usage_count = $order->get_customer_id() ? $data_store->get_usage_by_user_id( $coupon, $order->get_customer_id() ) : $data_store->get_usage_by_email( $coupon, $order->get_billing_email() );
|
||||
|
||||
if ( $usage_count >= $coupon_usage_limit ) {
|
||||
throw new \Exception( $coupon->get_coupon_error( \WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes default order status to draft for orders created via this API.
|
||||
*
|
||||
|
@ -113,38 +207,36 @@ class OrderController {
|
|||
*/
|
||||
protected function update_line_items_from_cart( \WC_Order $order ) {
|
||||
$cart_controller = new CartController();
|
||||
$cart_controller->validate_cart_items();
|
||||
|
||||
$cart = $cart_controller->get_cart_instance();
|
||||
$cart_hashes = $cart_controller->get_cart_hashes();
|
||||
$cart = $cart_controller->get_cart_instance();
|
||||
$cart_hashes = $cart_controller->get_cart_hashes();
|
||||
|
||||
if ( $order->get_cart_hash() !== $cart_hashes['line_items'] ) {
|
||||
$order->remove_order_items( 'line_item' );
|
||||
$order->set_cart_hash( $cart_hashes['line_items'] );
|
||||
$order->remove_order_items( 'line_item' );
|
||||
WC()->checkout->create_order_line_items( $order, $cart );
|
||||
}
|
||||
|
||||
if ( $order->get_meta_data( 'shipping_hash' ) !== $cart_hashes['shipping'] ) {
|
||||
if ( $order->get_meta_data( '_shipping_hash' ) !== $cart_hashes['shipping'] ) {
|
||||
$order->update_meta_data( '_shipping_hash', $cart_hashes['shipping'] );
|
||||
$order->remove_order_items( 'shipping' );
|
||||
$order->update_meta_data( 'shipping_hash', $cart_hashes['shipping'] );
|
||||
WC()->checkout->create_order_shipping_lines( $order, WC()->session->get( 'chosen_shipping_methods' ), WC()->shipping()->get_packages() );
|
||||
}
|
||||
|
||||
if ( $order->get_meta_data( 'coupons_hash' ) !== $cart_hashes['coupons'] ) {
|
||||
if ( $order->get_meta_data( '_coupons_hash' ) !== $cart_hashes['coupons'] ) {
|
||||
$order->remove_order_items( 'coupon' );
|
||||
$order->update_meta_data( 'coupons_hash', $cart_hashes['coupons'] );
|
||||
$order->update_meta_data( '_coupons_hash', $cart_hashes['coupons'] );
|
||||
WC()->checkout->create_order_coupon_lines( $order, $cart );
|
||||
}
|
||||
|
||||
if ( $order->get_meta_data( 'fees_hash' ) !== $cart_hashes['fees'] ) {
|
||||
if ( $order->get_meta_data( '_fees_hash' ) !== $cart_hashes['fees'] ) {
|
||||
$order->update_meta_data( '_fees_hash', $cart_hashes['fees'] );
|
||||
$order->remove_order_items( 'fee' );
|
||||
$order->update_meta_data( 'fees_hash', $cart_hashes['fees'] );
|
||||
WC()->checkout->create_order_fee_lines( $order, $cart );
|
||||
}
|
||||
|
||||
if ( $order->get_meta_data( 'taxes_hash' ) !== $cart_hashes['taxes'] ) {
|
||||
if ( $order->get_meta_data( '_taxes_hash' ) !== $cart_hashes['taxes'] ) {
|
||||
$order->update_meta_data( '_taxes_hash', $cart_hashes['taxes'] );
|
||||
$order->remove_order_items( 'tax' );
|
||||
$order->update_meta_data( 'taxes_hash', $cart_hashes['taxes'] );
|
||||
WC()->checkout->create_order_tax_lines( $order, $cart );
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue