esc_html( $coupon_code )
),
400
);
}
if ( $this->has_coupon( $coupon_code ) ) {
throw new RouteException(
'woocommerce_rest_cart_coupon_error',
sprintf(
/* translators: %s coupon code */
__( 'Coupon code "%s" has already been applied.', 'woocommerce' ),
esc_html( $coupon_code )
),
400
);
}
if ( ! $coupon->is_valid() ) {
throw new RouteException(
'woocommerce_rest_cart_coupon_error',
wp_strip_all_tags( $coupon->get_error_message() ),
400
);
}
// 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 );
/**
* Filters if a coupon can be applied alongside other individual use coupons.
*
* @param boolean $apply_with_individual_use_coupon Defaults to false.
* @param \WC_Coupon $coupon Coupon object applied to the cart.
* @param \WC_Coupon $individual_use_coupon Individual use coupon already applied to the cart.
* @param array $applied_coupons Array of applied coupons already applied to the cart.
* @return boolean
*/
if ( false === apply_filters( 'woocommerce_apply_with_individual_use_coupon', false, $coupon, $individual_use_coupon, $applied_coupons ) ) {
throw new RouteException(
'woocommerce_rest_cart_coupon_error',
sprintf(
/* translators: %s: coupon code */
__( '"%s" has already been applied and cannot be used in conjunction with other coupons.', 'woocommerce' ),
$code
),
400
);
}
}
if ( $coupon->get_individual_use() ) {
/**
* Filter coupons to remove when applying an individual use coupon.
*
* @param array $coupons Array of coupons to remove from the cart.
* @param \WC_Coupon $coupon Coupon object applied to the cart.
* @param array $applied_coupons Array of applied coupons already applied to the cart.
* @return array
*/
$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 = array_diff( $applied_coupons, $coupons_to_remove );
}
$applied_coupons[] = $coupon_code;
$cart->set_applied_coupons( $applied_coupons );
/**
* Fires after a coupon has been applied to the cart.
*
* @param string $coupon_code The coupon code that was applied.
*/
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() ) {
$cart = $this->get_cart_instance();
$cart->remove_coupon( $coupon->get_code() );
$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', 'woocommerce' ),
$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 ) {
$cart = $this->get_cart_instance();
$product_quantities = $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 ) {
$reserve_stock = new ReserveStock();
$draft_order = wc()->session->get( 'store_api_draft_order', 0 );
$qty_reserved = $reserve_stock->get_reserved_stock( $product, $draft_order );
return $product->get_stock_quantity() - $qty_reserved;
}
/**
* Get a product object to be added to the cart.
*
* @throws RouteException Exception if invalid data is detected.
*
* @param array $request Add to cart request params.
* @return \WC_Product|Error Returns a product object if purchasable.
*/
protected function get_product_for_cart( $request ) {
$product = wc_get_product( $request['id'] );
if ( ! $product || 'trash' === $product->get_status() ) {
throw new RouteException(
'woocommerce_rest_cart_invalid_product',
__( 'This product cannot be added to the cart.', 'woocommerce' ),
400
);
}
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.', 'woocommerce' ),
$product->get_name()
),
400
);
}
/**
* Filter data for add to cart requests.
*
* @param array $request Add to cart request params.
* @return array Updated request array.
*/
protected function filter_request_data( $request ) {
$product_id = $request['id'];
$variation_id = 0;
$product = wc_get_product( $product_id );
if ( $product->is_type( 'variation' ) ) {
$product_id = $product->get_parent_id();
$variation_id = $product->get_id();
}
/**
* Filter cart item data for add to cart requests.
*
* @param array $cart_item_data Array of other cart item data.
* @param integer $product_id ID of the product added to the cart.
* @param integer $variation_id Variation ID of the product added to the cart.
* @param integer $quantity Quantity of the item added to the cart.
* @return array
*/
$request['cart_item_data'] = (array) apply_filters(
'woocommerce_add_cart_item_data',
$request['cart_item_data'],
$product_id,
$variation_id,
$request['quantity']
);
if ( $product->is_sold_individually() ) {
/**
* Filter sold individually quantity for add to cart requests.
*
* @param integer $sold_individually_quantity Defaults to 1.
* @param integer $quantity Quantity of the item added to the cart.
* @param integer $product_id ID of the product added to the cart.
* @param integer $variation_id Variation ID of the product added to the cart.
* @param array $cart_item_data Array of other cart item data.
* @return integer
*/
$request['quantity'] = apply_filters( 'woocommerce_add_to_cart_sold_individually_quantity', 1, $request['quantity'], $product_id, $variation_id, $request['cart_item_data'] );
}
return $request;
}
/**
* If variations are set, validate and format the values ready to add to the cart.
*
* @throws RouteException Exception if invalid data is detected.
*
* @param array $request Add to cart request params.
* @return array Updated request array.
*/
protected function parse_variation_data( $request ) {
$product = $this->get_product_for_cart( $request );
// Remove variation request if not needed.
if ( ! $product->is_type( array( 'variation', 'variable' ) ) ) {
$request['variation'] = [];
return $request;
}
// Flatten data and format posted values.
$variable_product_attributes = $this->get_variable_product_attributes( $product );
$request['variation'] = $this->sanitize_variation_data( wp_list_pluck( $request['variation'], 'value', 'attribute' ), $variable_product_attributes );
// If we have a parent product, find the variation ID.
if ( $product->is_type( 'variable' ) ) {
$request['id'] = $this->get_variation_id_from_variation_data( $request, $product );
}
// Now we have a variation ID, get the valid set of attributes for this variation. They will have an attribute_ prefix since they are from meta.
$expected_attributes = wc_get_product_variation_attributes( $request['id'] );
$missing_attributes = [];
foreach ( $variable_product_attributes as $attribute ) {
if ( ! $attribute['is_variation'] ) {
continue;
}
$prefixed_attribute_name = 'attribute_' . sanitize_title( $attribute['name'] );
$expected_value = isset( $expected_attributes[ $prefixed_attribute_name ] ) ? $expected_attributes[ $prefixed_attribute_name ] : '';
$attribute_label = wc_attribute_label( $attribute['name'] );
if ( isset( $request['variation'][ wc_variation_attribute_name( $attribute['name'] ) ] ) ) {
$given_value = $request['variation'][ wc_variation_attribute_name( $attribute['name'] ) ];
if ( $expected_value === $given_value ) {
continue;
}
// If valid values are empty, this is an 'any' variation so get all possible values.
if ( '' === $expected_value && in_array( $given_value, $attribute->get_slugs(), true ) ) {
continue;
}
throw new RouteException(
'woocommerce_rest_invalid_variation_data',
/* translators: %1$s: Attribute name, %2$s: Allowed values. */
sprintf( __( 'Invalid value posted for %1$s. Allowed values: %2$s', 'woocommerce' ), $attribute_label, implode( ', ', $attribute->get_slugs() ) ),
400
);
}
// If no attribute was posted, only error if the variation has an 'any' attribute which requires a value.
if ( '' === $expected_value ) {
$missing_attributes[] = $attribute_label;
}
}
if ( ! empty( $missing_attributes ) ) {
throw new RouteException(
'woocommerce_rest_missing_variation_data',
/* translators: %s: Attribute name. */
__( 'Missing variation data for variable product.', 'woocommerce' ) . ' ' . sprintf( _n( '%s is a required field', '%s are required fields', count( $missing_attributes ), 'woocommerce' ), wc_format_list_of_items( $missing_attributes ) ),
400
);
}
return $request;
}
/**
* Try to match request data to a variation ID and return the ID.
*
* @throws RouteException Exception if variation cannot be found.
*
* @param array $request Add to cart request params.
* @param \WC_Product $product Product being added to the cart.
* @return int Matching variation ID.
*/
protected function get_variation_id_from_variation_data( $request, $product ) {
$data_store = \WC_Data_Store::load( 'product' );
$match_attributes = $request['variation'];
$variation_id = $data_store->find_matching_product_variation( $product, $match_attributes );
if ( empty( $variation_id ) ) {
throw new RouteException(
'woocommerce_rest_variation_id_from_variation_data',
__( 'No matching variation found.', 'woocommerce' ),
400
);
}
return $variation_id;
}
/**
* Format and sanitize variation data posted to the API.
*
* Labels are converted to names (e.g. Size to pa_size), and values are cleaned.
*
* @throws RouteException Exception if variation cannot be found.
*
* @param array $variation_data Key value pairs of attributes and values.
* @param array $variable_product_attributes Product attributes we're expecting.
* @return array
*/
protected function sanitize_variation_data( $variation_data, $variable_product_attributes ) {
$return = [];
foreach ( $variable_product_attributes as $attribute ) {
if ( ! $attribute['is_variation'] ) {
continue;
}
$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[ $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[ $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;
}
/**
* Get product attributes from the variable product (which may be the parent if the product object is a variation).
*
* @throws RouteException Exception if product is invalid.
*
* @param \WC_Product $product Product being added to the cart.
* @return array
*/
protected function get_variable_product_attributes( $product ) {
if ( $product->is_type( 'variation' ) ) {
$product = wc_get_product( $product->get_parent_id() );
}
if ( ! $product || 'trash' === $product->get_status() ) {
throw new RouteException(
'woocommerce_rest_cart_invalid_parent_product',
__( 'This product cannot be added to the cart.', 'woocommerce' ),
400
);
}
return $product->get_attributes();
}
}