diff --git a/assets/js/base/components/shipping-rates-control/packages.js b/assets/js/base/components/shipping-rates-control/packages.js index cd9ac89d01c..290da7b4a1b 100644 --- a/assets/js/base/components/shipping-rates-control/packages.js +++ b/assets/js/base/components/shipping-rates-control/packages.js @@ -19,7 +19,7 @@ const Packages = ( { } ) => { return shippingRates.map( ( shippingRate, i ) => ( { diff --git a/src/RestApi/StoreApi/Controllers/Cart.php b/src/RestApi/StoreApi/Controllers/Cart.php index 2006d5b1d6e..2c325fe6a98 100644 --- a/src/RestApi/StoreApi/Controllers/Cart.php +++ b/src/RestApi/StoreApi/Controllers/Cart.php @@ -105,17 +105,78 @@ public function register_routes() { ); register_rest_route( $this->namespace, - '/' . $this->rest_base . '/select-shipping', + '/' . $this->rest_base . '/update-shipping', + [ + [ + 'methods' => 'POST', + 'callback' => [ $this, 'update_shipping' ], + 'args' => [ + 'address_1' => array( + 'description' => __( 'First line of the address being shipped to.', 'woo-gutenberg-products-block' ), + 'type' => 'string', + 'default' => '', + 'sanitize_callback' => 'wc_clean', + 'validate_callback' => 'rest_validate_request_arg', + ), + 'address_2' => [ + 'description' => __( 'Second line of the address being shipped to.', 'woo-gutenberg-products-block' ), + 'type' => 'string', + 'default' => '', + 'sanitize_callback' => 'wc_clean', + 'validate_callback' => 'rest_validate_request_arg', + ], + 'city' => [ + 'description' => __( 'City of the address being shipped to.', 'woo-gutenberg-products-block' ), + 'type' => 'string', + 'default' => '', + 'sanitize_callback' => 'wc_clean', + 'validate_callback' => 'rest_validate_request_arg', + ], + 'state' => [ + 'description' => __( 'ISO code, or name, for the state, province, or district of the address being shipped to.', 'woo-gutenberg-products-block' ), + 'type' => 'string', + 'default' => '', + 'sanitize_callback' => 'wc_clean', + 'validate_callback' => 'rest_validate_request_arg', + ], + 'postcode' => [ + 'description' => __( 'Zip or Postcode of the address being shipped to.', 'woo-gutenberg-products-block' ), + 'type' => 'string', + 'default' => '', + 'sanitize_callback' => 'wc_clean', + 'validate_callback' => 'rest_validate_request_arg', + ], + 'country' => [ + 'description' => __( 'ISO code for the country of the address being shipped to.', 'woo-gutenberg-products-block' ), + 'type' => 'string', + 'default' => '', + 'sanitize_callback' => 'wc_clean', + 'validate_callback' => 'rest_validate_request_arg', + ], + ], + ], + 'schema' => [ $this, 'get_public_item_schema' ], + ] + ); + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/select-shipping-rate/(?P[\d]+)', [ 'args' => [ - 'shipping_rates' => [ - 'description' => __( 'Array of selected shipping rates ids.', 'woo-gutenberg-products-block' ), - 'type' => 'array', + 'package_id' => array( + 'description' => __( 'The ID of the package being shipped.', 'woo-gutenberg-products-block' ), + 'type' => 'integer', + 'required' => true, + ), + 'rate_id' => [ + 'description' => __( 'The chosen rate ID for the package.', 'woo-gutenberg-products-block' ), + 'type' => 'string', + 'required' => true, ], ], [ 'methods' => 'POST', - 'callback' => [ $this, 'select_shipping' ], + 'callback' => [ $this, 'select_shipping_rate_for_package' ], ], 'schema' => [ $this, 'get_public_item_schema' ], ] @@ -125,16 +186,20 @@ public function register_routes() { /** * Select a shipping rate for the cart. * - * This selects a shipping rate and adds it to an array of selected shipping rates, passing one or multiple items will add them to the selected shipping rates, passing an empty array will unset everything. + * This selects a shipping rate for a package and adds it to an array of selected shipping rates. * * @param \WP_REST_Request $request Full details about the request. * @return \WP_Error|\WP_REST_Response */ - public function select_shipping( $request ) { + public function select_shipping_rate_for_package( $request ) { if ( ! wc_shipping_enabled() ) { return new RestError( 'woocommerce_rest_shipping_disabled', __( 'Shipping is disabled.', 'woo-gutenberg-products-block' ), array( 'status' => 404 ) ); } + if ( ! isset( $request['package_id'] ) || ! is_numeric( $request['package_id'] ) ) { + return new RestError( 'woocommerce_rest_cart_missing_package_id', __( 'Invalid Package ID.', 'woo-gutenberg-products-block' ), array( 'status' => 403 ) ); + } + $controller = new CartController(); $cart = $controller->get_cart_instance(); @@ -142,9 +207,11 @@ public function select_shipping( $request ) { return new RestError( 'woocommerce_rest_cart_error', __( 'Unable to retrieve cart.', 'woo-gutenberg-products-block' ), array( 'status' => 500 ) ); } - $rates_ids = wc_clean( wp_unslash( $request['shipping_rates'] ) ); + $package_id = absint( $request['package_id'] ); + $rate_id = wc_clean( wp_unslash( $request['rate_id'] ) ); + try { - $controller->select_shipping_rate( $rates_ids ); + $controller->select_shipping_rate( $package_id, $rate_id ); } catch ( RestException $e ) { return new RestError( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } @@ -222,6 +289,52 @@ public function remove_coupon( $request ) { return $response; } + /** + * Given an address, gets updated shipping rates for the cart. + * + * @param \WP_REST_Request $request Full details about the request. + * @return \WP_Error|\WP_REST_Response + */ + public function update_shipping( $request ) { + if ( ! wc_shipping_enabled() ) { + return new RestError( 'woocommerce_rest_shipping_disabled', __( 'Shipping is disabled.', 'woo-gutenberg-products-block' ), array( 'status' => 404 ) ); + } + + $controller = new CartController(); + $cart = $controller->get_cart_instance(); + + if ( ! $cart || ! $cart instanceof \WC_Cart ) { + return new RestError( 'woocommerce_rest_cart_error', __( 'Unable to retrieve cart.', 'woo-gutenberg-products-block' ), array( 'status' => 500 ) ); + } + + $request = $this->validate_shipping_address( $request ); + + if ( is_wp_error( $request ) ) { + return $request; + } + + // Update customer session. + WC()->customer->set_props( + array( + 'shipping_country' => isset( $request['country'] ) ? $request['country'] : null, + 'shipping_state' => isset( $request['state'] ) ? $request['state'] : null, + 'shipping_postcode' => isset( $request['postcode'] ) ? $request['postcode'] : null, + 'shipping_city' => isset( $request['city'] ) ? $request['city'] : null, + 'shipping_address_1' => isset( $request['address_1'] ) ? $request['address_1'] : null, + 'shipping_address_2' => isset( $request['address_2'] ) ? $request['address_2'] : null, + ) + ); + WC()->customer->save(); + + $cart->calculate_shipping(); + $cart->calculate_totals(); + + $data = $this->prepare_item_for_response( $cart, $request ); + $response = rest_ensure_response( $data ); + + return $response; + } + /** * Get the cart. * @@ -259,8 +372,77 @@ public function get_item_schema() { * @return \WP_REST_Response Response object. */ public function prepare_item_for_response( $cart, $request ) { - $data = $this->schema->get_item_response( $cart ); + return rest_ensure_response( $this->schema->get_item_response( $cart ) ); + } + + /** + * Format the request address. + * + * @param \WP_REST_Request $request Full details about the request. + * @return \WP_Error|\WP_REST_Response + */ + protected function validate_shipping_address( $request ) { + $valid_countries = WC()->countries->get_shipping_countries(); + + if ( empty( $request['country'] ) ) { + return new RestError( + 'woocommerce_rest_cart_shipping_rates_missing_country', + sprintf( + /* translators: 1: valid country codes */ + __( 'No destination country code was given. Please provide one of the following: %s', 'woo-gutenberg-products-block' ), + implode( ', ', array_keys( $valid_countries ) ) + ), + [ 'status' => 400 ] + ); + } + + $request['country'] = wc_strtoupper( $request['country'] ); + + if ( + is_array( $valid_countries ) && + count( $valid_countries ) > 0 && + ! array_key_exists( $request['country'], $valid_countries ) + ) { + return new RestError( + 'woocommerce_rest_cart_shipping_rates_invalid_country', + sprintf( + /* translators: 1: valid country codes */ + __( 'Destination country code is not valid. Please enter one of the following: %s', 'woo-gutenberg-products-block' ), + implode( ', ', array_keys( $valid_countries ) ) + ), + [ 'status' => 400 ] + ); + } + + $request['postcode'] = $request['postcode'] ? wc_format_postcode( $request['postcode'], $request['country'] ) : null; + + if ( ! empty( $request['state'] ) ) { + $valid_states = WC()->countries->get_states( $request['country'] ); + + if ( is_array( $valid_states ) && count( $valid_states ) > 0 ) { + $valid_state_values = array_map( 'wc_strtoupper', array_flip( array_map( 'wc_strtoupper', $valid_states ) ) ); + $request['state'] = wc_strtoupper( $request['state'] ); + + if ( isset( $valid_state_values[ $request['state'] ] ) ) { + // With this part we consider state value to be valid as well, + // convert it to the state key for the valid_states check below. + $request['state'] = $valid_state_values[ $request['state'] ]; + } + + if ( ! in_array( $request['state'], $valid_state_values, true ) ) { + return new RestError( + 'woocommerce_rest_cart_shipping_rates_invalid_state', + sprintf( + /* translators: 1: valid states */ + __( 'Destination state is not valid. Please enter one of the following: %s', 'woo-gutenberg-products-block' ), + implode( ', ', $valid_states ) + ), + [ 'status' => 400 ] + ); + } + } + } - return rest_ensure_response( $data ); + return $request; } } diff --git a/src/RestApi/StoreApi/Controllers/CartShippingRates.php b/src/RestApi/StoreApi/Controllers/CartShippingRates.php index d2431e8daca..b8d431579b0 100644 --- a/src/RestApi/StoreApi/Controllers/CartShippingRates.php +++ b/src/RestApi/StoreApi/Controllers/CartShippingRates.php @@ -75,6 +75,10 @@ public function register_routes() { * @return \WP_Error|\WP_REST_Response */ public function get_items( $request ) { + if ( ! wc_shipping_enabled() ) { + return new RestError( 'woocommerce_rest_shipping_disabled', __( 'Shipping is disabled.', 'woo-gutenberg-products-block' ), array( 'status' => 404 ) ); + } + $controller = new CartController(); $cart = $controller->get_cart_instance(); @@ -82,43 +86,11 @@ public function get_items( $request ) { return new RestError( 'woocommerce_rest_cart_error', __( 'Unable to retrieve cart.', 'woo-gutenberg-products-block' ), array( 'status' => 500 ) ); } - if ( ! empty( $request['country'] ) ) { - $valid_countries = WC()->countries->get_shipping_countries(); - - if ( - is_array( $valid_countries ) && - count( $valid_countries ) > 0 && - ! array_key_exists( $request['country'], $valid_countries ) - ) { - return new RestError( - 'woocommerce_rest_cart_shipping_rates_invalid_country', - sprintf( - /* translators: 1: valid country codes */ - __( 'Destination country code is not valid. Please enter one of the following: %s', 'woo-gutenberg-products-block' ), - implode( ', ', array_keys( $valid_countries ) ) - ), - [ 'status' => 400 ] - ); - } - } - - $request = $this->validate_shipping_address( $request ); - - if ( is_wp_error( $request ) ) { - return $request; - } - - $cart_items = $controller->get_cart_items( - function( $item ) { - return ! empty( $item['data'] ) && $item['data']->needs_shipping(); - } - ); - - if ( empty( $cart_items ) ) { + if ( ! $cart->needs_shipping() ) { return rest_ensure_response( [] ); } - $packages = $this->get_shipping_packages( $request ); + $packages = $controller->get_shipping_packages(); $response = []; foreach ( $packages as $key => $package ) { @@ -129,46 +101,6 @@ function( $item ) { return rest_ensure_response( $response ); } - /** - * Format the request address. - * - * @param \WP_REST_Request $request Full details about the request. - * @return \WP_Error|\WP_REST_Response - */ - protected function validate_shipping_address( $request ) { - $request['country'] = wc_strtoupper( $request['country'] ); - $request['postcode'] = $request['postcode'] ? wc_format_postcode( $request['postcode'], $request['country'] ) : null; - - if ( ! empty( $request['state'] ) ) { - $valid_states = WC()->countries->get_states( $request['country'] ); - - if ( is_array( $valid_states ) && count( $valid_states ) > 0 ) { - $valid_state_values = array_map( 'wc_strtoupper', array_flip( array_map( 'wc_strtoupper', $valid_states ) ) ); - $request['state'] = wc_strtoupper( $request['state'] ); - - if ( isset( $valid_state_values[ $request['state'] ] ) ) { - // With this part we consider state value to be valid as well, - // convert it to the state key for the valid_states check below. - $request['state'] = $valid_state_values[ $request['state'] ]; - } - - if ( ! in_array( $request['state'], $valid_state_values, true ) ) { - return new RestError( - 'woocommerce_rest_cart_shipping_rates_invalid_state', - sprintf( - /* translators: 1: valid states */ - __( 'Destination state is not valid. Please enter one of the following: %s', 'woo-gutenberg-products-block' ), - implode( ', ', $valid_states ) - ), - [ 'status' => 400 ] - ); - } - } - } - - return $request; - } - /** * Cart item schema. * @@ -188,95 +120,4 @@ public function get_item_schema() { public function prepare_item_for_response( $package, $request ) { return rest_ensure_response( $this->schema->get_item_response( $package ) ); } - - /** - * Get the query params available for this endpoint. - * - * @return array - */ - public function get_collection_params() { - $params = array(); - $params['context'] = $this->get_context_param(); - $params['context']['default'] = 'view'; - - $params['address_1'] = array( - 'description' => __( 'First line of the address being shipped to.', 'woo-gutenberg-products-block' ), - 'type' => 'string', - 'default' => '', - 'sanitize_callback' => 'wc_clean', - 'validate_callback' => 'rest_validate_request_arg', - ); - - $params['address_2'] = array( - 'description' => __( 'Second line of the address being shipped to.', 'woo-gutenberg-products-block' ), - 'type' => 'string', - 'default' => '', - 'sanitize_callback' => 'wc_clean', - 'validate_callback' => 'rest_validate_request_arg', - ); - - $params['city'] = array( - 'description' => __( 'City of the address being shipped to.', 'woo-gutenberg-products-block' ), - 'type' => 'string', - 'default' => '', - 'sanitize_callback' => 'wc_clean', - 'validate_callback' => 'rest_validate_request_arg', - ); - - $params['state'] = array( - 'description' => __( 'ISO code, or name, for the state, province, or district of the address being shipped to.', 'woo-gutenberg-products-block' ), - 'type' => 'string', - 'default' => '', - 'sanitize_callback' => 'wc_clean', - 'validate_callback' => 'rest_validate_request_arg', - ); - - $params['postcode'] = array( - 'description' => __( 'Zip or Postcode of the address being shipped to.', 'woo-gutenberg-products-block' ), - 'type' => 'string', - 'default' => '', - 'sanitize_callback' => 'wc_clean', - 'validate_callback' => 'rest_validate_request_arg', - ); - - $params['country'] = array( - 'description' => __( 'ISO code for the country of the address being shipped to.', 'woo-gutenberg-products-block' ), - 'type' => 'string', - 'default' => '', - 'sanitize_callback' => 'wc_clean', - 'validate_callback' => 'rest_validate_request_arg', - ); - - return $params; - } - - /** - * Get packages with calculated shipping. - * - * Based on WC_Cart::get_shipping_packages but allows the destination to be - * customised based on passed params. - * - * @param \WP_REST_Request $request Request object. - * @return array of cart items - */ - protected function get_shipping_packages( $request ) { - $packages = WC()->cart->get_shipping_packages(); - - if ( $request['country'] ) { - foreach ( $packages as $key => $package ) { - $packages[ $key ]['destination'] = [ - 'address_1' => $request['address_1'], - 'address_2' => $request['address_2'], - 'city' => $request['city'], - 'state' => $request['state'], - 'postcode' => $request['postcode'], - 'country' => $request['country'], - ]; - } - } - - $packages = WC()->shipping()->calculate_shipping( $packages ); - - return $packages; - } } diff --git a/src/RestApi/StoreApi/Schemas/CartSchema.php b/src/RestApi/StoreApi/Schemas/CartSchema.php index 60c954ef7fa..d23dc87163d 100644 --- a/src/RestApi/StoreApi/Schemas/CartSchema.php +++ b/src/RestApi/StoreApi/Schemas/CartSchema.php @@ -7,6 +7,8 @@ namespace Automattic\WooCommerce\Blocks\RestApi\StoreApi\Schemas; +use Automattic\WooCommerce\Blocks\RestApi\StoreApi\Utilities\CartController; + defined( 'ABSPATH' ) || exit; /** @@ -29,7 +31,7 @@ class CartSchema extends AbstractSchema { */ protected function get_properties() { return [ - 'coupons' => [ + 'coupons' => [ 'description' => __( 'List of applied cart coupons.', 'woo-gutenberg-products-block' ), 'type' => 'array', 'context' => [ 'view', 'edit' ], @@ -39,16 +41,17 @@ protected function get_properties() { 'properties' => $this->force_schema_readonly( ( new CartCouponSchema() )->get_properties() ), ], ], - 'selected_shipping_rates' => [ - 'description' => __( 'List of selected shipping rates.', 'woo-gutenberg-products-block' ), + 'shipping_rates' => [ + 'description' => __( 'List of available shipping rates for the cart.', 'woo-gutenberg-products-block' ), 'type' => 'array', 'context' => [ 'view', 'edit' ], 'readonly' => true, 'items' => [ - 'type' => 'string', + 'type' => 'object', + 'properties' => $this->force_schema_readonly( ( new CartShippingRateSchema() )->get_properties() ), ], ], - 'items' => [ + 'items' => [ 'description' => __( 'List of cart items.', 'woo-gutenberg-products-block' ), 'type' => 'array', 'context' => [ 'view', 'edit' ], @@ -58,25 +61,25 @@ protected function get_properties() { 'properties' => $this->force_schema_readonly( ( new CartItemSchema() )->get_properties() ), ], ], - 'items_count' => [ + 'items_count' => [ 'description' => __( 'Number of items in the cart.', 'woo-gutenberg-products-block' ), 'type' => 'integer', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], - 'items_weight' => [ + 'items_weight' => [ 'description' => __( 'Total weight (in grams) of all products in the cart.', 'woo-gutenberg-products-block' ), 'type' => 'number', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], - 'needs_shipping' => [ + 'needs_shipping' => [ 'description' => __( 'True if the cart needs shipping. False for carts with only digital goods or stores with no shipping methods set-up.', 'woo-gutenberg-products-block' ), 'type' => 'boolean', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], - 'totals' => [ + 'totals' => [ 'description' => __( 'Cart total amounts provided using the smallest unit of the currency.', 'woo-gutenberg-products-block' ), 'type' => 'object', 'context' => [ 'view', 'edit' ], @@ -180,18 +183,20 @@ protected function get_properties() { * @return array */ public function get_item_response( $cart ) { - $cart_coupon_schema = new CartCouponSchema(); - $cart_item_schema = new CartItemSchema(); - $context = 'edit'; + $controller = new CartController(); + $cart_coupon_schema = new CartCouponSchema(); + $cart_item_schema = new CartItemSchema(); + $shipping_rate_schema = new CartShippingRateSchema(); + $context = 'edit'; return [ - 'coupons' => array_values( array_map( [ $cart_coupon_schema, 'get_item_response' ], array_filter( $cart->get_applied_coupons() ) ) ), - 'selected_shipping_rates' => $this->get_selected_shipping_rates(), - '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' ), - 'needs_shipping' => $cart->needs_shipping(), - 'totals' => (object) array_merge( + 'coupons' => array_values( array_map( [ $cart_coupon_schema, 'get_item_response' ], array_filter( $cart->get_applied_coupons() ) ) ), + 'shipping_rates' => array_values( array_map( [ $shipping_rate_schema, 'get_item_response' ], $controller->get_shipping_packages() ) ), + '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' ), + 'needs_shipping' => $cart->needs_shipping(), + 'totals' => (object) array_merge( $this->get_store_currency_response(), [ 'total_items' => $this->prepare_money_response( $cart->get_subtotal(), wc_get_price_decimals() ), @@ -231,13 +236,4 @@ protected function get_tax_lines( $cart ) { return $tax_lines; } - - /** - * Get selected shipping rates from the user session. - * - * @return array - */ - protected function get_selected_shipping_rates() { - return WC()->session->get( 'chosen_shipping_methods', array() ); - } } diff --git a/src/RestApi/StoreApi/Schemas/CartShippingRateSchema.php b/src/RestApi/StoreApi/Schemas/CartShippingRateSchema.php index 7235be01e88..e46da966ba2 100644 --- a/src/RestApi/StoreApi/Schemas/CartShippingRateSchema.php +++ b/src/RestApi/StoreApi/Schemas/CartShippingRateSchema.php @@ -29,6 +29,12 @@ class CartShippingRateSchema extends AbstractSchema { */ protected function get_properties() { return [ + 'package_id' => [ + 'description' => __( 'The ID of the package the shipping rates belong to.', 'woo-gutenberg-products-block' ), + 'type' => 'integer', + 'context' => [ 'view', 'edit' ], + 'readonly' => true, + ], 'destination' => [ 'description' => __( 'Shipping destination address.', 'woo-gutenberg-products-block' ), 'type' => 'object', @@ -74,13 +80,19 @@ protected function get_properties() { ], ], 'items' => [ - 'description' => __( 'List of cart items (keys) the returned shipping rates apply to.', 'woo-gutenberg-products-block' ), + 'description' => __( 'List of cart items the returned shipping rates apply to.', 'woo-gutenberg-products-block' ), 'type' => 'array', 'context' => [ 'view', 'edit' ], 'readonly' => true, 'items' => [ 'type' => 'object', 'properties' => [ + 'key' => [ + 'description' => __( 'Unique identifier for the item within the cart.', 'woo-gutenberg-products-block' ), + 'type' => 'string', + 'context' => [ 'view', 'edit' ], + 'readonly' => true, + ], 'name' => [ 'description' => __( 'Name of the item.', 'woo-gutenberg-products-block' ), 'type' => 'string', @@ -122,8 +134,13 @@ protected function get_properties() { */ protected function get_rate_properties() { return array_merge( - $this->get_store_currency_properties(), [ + 'rate_id' => [ + 'description' => __( 'ID of the shipping rate.', 'woo-gutenberg-products-block' ), + 'type' => 'string', + 'context' => [ 'view', 'edit' ], + 'readonly' => true, + ], 'name' => [ 'description' => __( 'Name of the shipping rate, e.g. Express shipping.', 'woo-gutenberg-products-block' ), 'type' => 'string', @@ -148,12 +165,6 @@ protected function get_rate_properties() { 'context' => [ 'view', 'edit' ], 'readonly' => true, ], - 'rate_id' => [ - 'description' => __( 'ID of the shipping rate.', 'woo-gutenberg-products-block' ), - 'type' => 'string', - 'context' => [ 'view', 'edit' ], - 'readonly' => true, - ], 'method_id' => [ 'description' => __( 'ID of the shipping method that provided the rate.', 'woo-gutenberg-products-block' ), 'type' => 'string', @@ -188,7 +199,14 @@ protected function get_rate_properties() { ], ], ], - ] + 'selected' => [ + 'description' => __( 'True if this is the rate currently selected by the customer for the cart.', 'woo-gutenberg-products-block' ), + 'type' => 'bool', + 'context' => [ 'view', 'edit' ], + 'readonly' => true, + ], + ], + $this->get_store_currency_properties() ); } @@ -202,13 +220,15 @@ public function get_item_response( $package ) { // Add product names and quantities. $items = array(); foreach ( $package['contents'] as $item_id => $values ) { - $items[ $item_id ] = [ + $items[] = [ + 'key' => $item_id, 'name' => $values['data']->get_name(), 'quantity' => $values['quantity'], ]; } return [ + 'package_id' => $package['package_id'], 'destination' => (object) $this->prepare_html_response( [ 'address_1' => $package['destination']['address_1'], @@ -229,29 +249,55 @@ public function get_item_response( $package ) { $package['key'], $package ), - 'shipping_rates' => array_values( array_map( [ $this, 'get_rate_response' ], $package['rates'] ) ), + 'shipping_rates' => $this->prepare_rates_response( $package ), ]; } + /** + * Prepare an array of rates from a package for the response. + * + * @param array $package Shipping package complete with rates from WooCommerce. + * @return array + */ + protected function prepare_rates_response( $package ) { + $rates = $package['rates']; + $selected_rates = WC()->session->get( 'chosen_shipping_methods', array() ); + $selected_rate = isset( $chosen_shipping_methods[ $package['package_id'] ] ) ? $chosen_shipping_methods[ $package['package_id'] ] : ''; + + if ( empty( $selected_rate ) && ! empty( $package['rates'] ) ) { + $selected_rate = wc_get_chosen_shipping_method_for_package( $package['package_id'], $package ); + } + + $response = []; + + foreach ( $package['rates'] as $rate ) { + $response[] = $this->get_rate_response( $rate, $selected_rate ); + } + + return $response; + } + /** * Response for a single rate. * * @param WC_Shipping_Rate $rate Rate object. + * @param string $selected_rate Selected rate. * @return array */ - protected function get_rate_response( $rate ) { + protected function get_rate_response( $rate, $selected_rate = '' ) { return array_merge( - $this->get_store_currency_response(), [ + 'rate_id' => $this->get_rate_prop( $rate, 'id' ), 'name' => $this->prepare_html_response( $this->get_rate_prop( $rate, 'label' ) ), 'description' => $this->prepare_html_response( $this->get_rate_prop( $rate, 'description' ) ), 'delivery_time' => $this->prepare_html_response( $this->get_rate_prop( $rate, 'delivery_time' ) ), 'price' => $this->prepare_money_response( $this->get_rate_prop( $rate, 'cost' ), wc_get_price_decimals() ), - 'rate_id' => $this->get_rate_prop( $rate, 'id' ), 'instance_id' => $this->get_rate_prop( $rate, 'instance_id' ), 'method_id' => $this->get_rate_prop( $rate, 'method_id' ), 'meta_data' => $this->get_rate_meta_data( $rate ), - ] + 'selected' => $selected_rate === $this->get_rate_prop( $rate, 'id' ), + ], + $this->get_store_currency_response() ); } diff --git a/src/RestApi/StoreApi/Utilities/CartController.php b/src/RestApi/StoreApi/Utilities/CartController.php index 67567a479c6..7956b9cc1ce 100644 --- a/src/RestApi/StoreApi/Utilities/CartController.php +++ b/src/RestApi/StoreApi/Utilities/CartController.php @@ -210,21 +210,35 @@ public function get_cart_coupons( $callback = null ) { } /** - * Selects a shipping rate. + * Get shipping packages from the cart and format as required. * - * @param array $rate_ids Shipping rate ids. + * @param bool $calculate_rates Should rates for the packages also be returned. + * @return array */ - public function select_shipping_rate( $rate_ids ) { - $cart = $this->get_cart_instance(); - $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' ) ? WC()->session->get( 'chosen_shipping_methods' ) : array(); - $posted_shipping_methods = $rate_ids ? wc_clean( wp_unslash( $rate_ids ) ) : array(); - - if ( empty( $posted_shipping_methods ) ) { - $chosen_shipping_methods = array(); - } else { - $chosen_shipping_methods = array_merge( $chosen_shipping_methods, $posted_shipping_methods ); + public function get_shipping_packages( $calculate_rates = true ) { + $cart = $this->get_cart_instance(); + $packages = $cart->get_shipping_packages(); + + // Add package ID to array. + foreach ( $packages as $key => $package ) { + $packages[ $key ]['package_id'] = $key; } - WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods ); + + return $calculate_rates ? WC()->shipping()->calculate_shipping( $packages ) : $packages; + } + + /** + * Selects a shipping rate. + * + * @param int $package_id ID of the package to choose a rate for. + * @param string $rate_id ID of the rate being chosen. + */ + 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[ $package_id ] = $rate_id; + + WC()->session->set( 'chosen_shipping_methods', $session_data ); } /**