Update Cart Order and Checkout Routes (https://github.com/woocommerce/woocommerce-blocks/pull/1958)
* Remove CartShippingRates endpoint This was moved to the /cart endpoint * Unused customer endpoint * Split controllers into routes * Update/fix tests * Add draft order ID property to the Cart schema. * Fix status check * cart/order-create endpoint * Placeholder for checkout endpoint * Resolve merge conflict * Update classes to match master * Use correct schema
This commit is contained in:
parent
757aea3d34
commit
8d76bcb33e
|
@ -1,446 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Cart Order 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;
|
||||
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\OrderSchema;
|
||||
|
||||
/**
|
||||
* Cart Order API.
|
||||
*
|
||||
* Creates orders based on cart contents.
|
||||
*/
|
||||
class CartOrder extends RestController {
|
||||
/**
|
||||
* Endpoint namespace.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $namespace = 'wc/store';
|
||||
|
||||
/**
|
||||
* Route base.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'cart/order';
|
||||
|
||||
/**
|
||||
* Schema class instance.
|
||||
*
|
||||
* @var object
|
||||
*/
|
||||
protected $schema;
|
||||
|
||||
/**
|
||||
* Draft order details, if applicable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $draft_order;
|
||||
|
||||
/**
|
||||
* Setup API class.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->schema = new OrderSchema();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base,
|
||||
[
|
||||
[
|
||||
'methods' => RestServer::CREATABLE,
|
||||
'callback' => array( $this, 'create_item' ),
|
||||
'args' => array_merge(
|
||||
$this->get_endpoint_args_for_item_schema( RestServer::CREATABLE ),
|
||||
array(
|
||||
'shipping_rates' => array(
|
||||
'description' => __( 'Selected shipping rates to apply to the order.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'array',
|
||||
'required' => false,
|
||||
'items' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'rate_id' => [
|
||||
'description' => __( 'ID of the shipping rate.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'string',
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
'schema' => [ $this, 'get_public_item_schema' ],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the global cart to an order object.
|
||||
*
|
||||
* @todo Since this relies on the cart global so much, why doesn't the core cart class do this?
|
||||
*
|
||||
* Based on WC_Checkout::create_order.
|
||||
*
|
||||
* @param RestRequest $request Full details about the request.
|
||||
* @return WP_Error|RestResponse
|
||||
*/
|
||||
public function create_item( $request ) {
|
||||
try {
|
||||
$this->draft_order = WC()->session->get(
|
||||
'store_api_draft_order',
|
||||
[
|
||||
'id' => 0,
|
||||
'hashes' => [
|
||||
'line_items' => false,
|
||||
'fees' => false,
|
||||
'coupons' => false,
|
||||
'taxes' => false,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
// Update session based on posted data.
|
||||
$this->update_session( $request );
|
||||
|
||||
// Create or retrieve the draft order for the current cart.
|
||||
$order_object = $this->create_order_from_cart( $request );
|
||||
|
||||
// Try to reserve stock for 10 mins, if available.
|
||||
// @todo Remove once min support for WC reaches 4.0.0.
|
||||
if ( \class_exists( '\Automattic\WooCommerce\Checkout\Helpers\ReserveStock' ) ) {
|
||||
$reserve_stock = new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock();
|
||||
} else {
|
||||
$reserve_stock = new \Automattic\WooCommerce\Blocks\RestApi\StoreApi\Utilities\ReserveStock();
|
||||
}
|
||||
|
||||
$reserve_stock->reserve_stock_for_order( $order_object, 10 );
|
||||
$response = $this->prepare_item_for_response( $order_object, $request );
|
||||
$response->set_status( 201 );
|
||||
return $response;
|
||||
} catch ( RestException $e ) {
|
||||
return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getCode() );
|
||||
} catch ( Exception $e ) {
|
||||
return new WP_Error( 'create-order-error', $e->getMessage(), [ 'status' => 500 ] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Before creating anything, this method ensures the cart session is up to date and matches the data we're going
|
||||
* to be adding to the order.
|
||||
*
|
||||
* @param RestRequest $request Full details about the request.
|
||||
* @return void
|
||||
*/
|
||||
protected function update_session( RestRequest $request ) {
|
||||
$schema = $this->get_item_schema();
|
||||
|
||||
if ( isset( $request['billing_address'] ) ) {
|
||||
$allowed_billing_values = array_intersect_key( $request['billing_address'], $schema['properties']['billing_address']['properties'] );
|
||||
foreach ( $allowed_billing_values as $key => $value ) {
|
||||
WC()->customer->{"set_billing_$key"}( $value );
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $request['shipping_address'] ) ) {
|
||||
$allowed_shipping_values = array_intersect_key( $request['shipping_address'], $schema['properties']['shipping_address']['properties'] );
|
||||
foreach ( $allowed_shipping_values as $key => $value ) {
|
||||
WC()->customer->{"set_shipping_$key"}( $value );
|
||||
}
|
||||
}
|
||||
|
||||
WC()->customer->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create order and set props based on global settings.
|
||||
*
|
||||
* @param RestRequest $request Full details about the request.
|
||||
* @return \WC_Order A new order object.
|
||||
*/
|
||||
protected function create_order_from_cart( RestRequest $request ) {
|
||||
add_filter( 'woocommerce_default_order_status', array( $this, 'default_order_status' ) );
|
||||
|
||||
$order = $this->get_order_object();
|
||||
$order->set_status( 'checkout-draft' );
|
||||
$order->set_created_via( 'store-api' );
|
||||
$order->set_currency( get_woocommerce_currency() );
|
||||
$order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) );
|
||||
$order->set_customer_id( get_current_user_id() );
|
||||
$order->set_customer_ip_address( \WC_Geolocation::get_ip_address() );
|
||||
$order->set_customer_user_agent( wc_get_user_agent() );
|
||||
$order->set_cart_hash( WC()->cart->get_cart_hash() );
|
||||
$order->update_meta_data( 'is_vat_exempt', WC()->cart->get_customer()->get_is_vat_exempt() ? 'yes' : 'no' );
|
||||
|
||||
$this->set_props_from_request( $order, $request );
|
||||
$this->create_line_items_from_cart( $order, $request );
|
||||
$this->select_shipping_rates( $order, $request );
|
||||
|
||||
// Calc totals, taxes, and save.
|
||||
$order->calculate_totals();
|
||||
|
||||
remove_filter( 'woocommerce_default_order_status', array( $this, 'default_order_status' ) );
|
||||
|
||||
// Store Order details in session so we can look it up later.
|
||||
WC()->session->set(
|
||||
'store_api_draft_order',
|
||||
[
|
||||
'id' => $order->get_id(),
|
||||
'hashes' => $this->get_cart_hashes( $order, $request ),
|
||||
]
|
||||
);
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hashes for items in the current cart. Useful for tracking changes.
|
||||
*
|
||||
* @param \WC_Order $order Object to prepare for the response.
|
||||
* @param RestRequest $request Full details about the request.
|
||||
* @return array
|
||||
*/
|
||||
protected function get_cart_hashes( \WC_Order $order, RestRequest $request ) {
|
||||
return [
|
||||
'line_items' => md5( wp_json_encode( WC()->cart->get_cart() ) ),
|
||||
'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() ) ),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an order object, either using a current draft order, or returning a new one.
|
||||
*
|
||||
* @return \WC_Order A new order object.
|
||||
*/
|
||||
protected function get_order_object() {
|
||||
$draft_order = $this->draft_order['id'] ? wc_get_order( $this->draft_order['id'] ) : false;
|
||||
|
||||
if ( $draft_order && $draft_order->has_status( 'draft' ) && 'store-api' === $draft_order->get_created_via() ) {
|
||||
return $draft_order;
|
||||
}
|
||||
|
||||
return new \WC_Order();
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes default order status to draft for orders created via this API.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function default_order_status() {
|
||||
return 'checkout-draft';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create order line items.
|
||||
*
|
||||
* @todo Knowing if items changed between the order and cart can be complex. Line items are ok because there is a
|
||||
* hash, but no hash exists for other line item types. Having a normalised set of data between cart and order, or
|
||||
* additional hashes, would be useful in the future and to help refactor this code. In the meantime, we're relying
|
||||
* on custom hashes in $this->draft_order to track if things changed.
|
||||
*
|
||||
* @param \WC_Order $order Object to prepare for the response.
|
||||
* @param RestRequest $request Full details about the request.
|
||||
*/
|
||||
protected function create_line_items_from_cart( \WC_Order $order, RestRequest $request ) {
|
||||
$new_hashes = $this->get_cart_hashes( $order, $request );
|
||||
$old_hashes = $this->draft_order['hashes'];
|
||||
|
||||
if ( $new_hashes['line_items'] !== $old_hashes['line_items'] ) {
|
||||
$order->remove_order_items( 'line_item' );
|
||||
WC()->checkout->create_order_line_items( $order, WC()->cart );
|
||||
}
|
||||
|
||||
if ( $new_hashes['coupons'] !== $old_hashes['coupons'] ) {
|
||||
$order->remove_order_items( 'coupon' );
|
||||
WC()->checkout->create_order_coupon_lines( $order, WC()->cart );
|
||||
}
|
||||
|
||||
if ( $new_hashes['fees'] !== $old_hashes['fees'] ) {
|
||||
$order->remove_order_items( 'fee' );
|
||||
WC()->checkout->create_order_fee_lines( $order, WC()->cart );
|
||||
}
|
||||
|
||||
if ( $new_hashes['taxes'] !== $old_hashes['taxes'] ) {
|
||||
$order->remove_order_items( 'tax' );
|
||||
WC()->checkout->create_order_tax_lines( $order, WC()->cart );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select shipping rates and store to order as line items.
|
||||
*
|
||||
* @throws RestException Exception when shipping is invalid.
|
||||
* @param \WC_Order $order Object to prepare for the response.
|
||||
* @param RestRequest $request Full details about the request.
|
||||
*/
|
||||
protected function select_shipping_rates( \WC_Order $order, RestRequest $request ) {
|
||||
$packages = $this->get_shipping_packages( $order, $request );
|
||||
$selected_rates = isset( $request['shipping_rates'] ) ? wp_list_pluck( $request['shipping_rates'], 'rate_id' ) : [];
|
||||
$shipping_hash = md5( wp_json_encode( $packages ) . wp_json_encode( $selected_rates ) );
|
||||
$stored_hash = WC()->session->get( 'store_api_shipping_hash' );
|
||||
|
||||
if ( $shipping_hash === $stored_hash ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$order->remove_order_items( 'shipping' );
|
||||
|
||||
foreach ( $packages as $package_key => $package ) {
|
||||
$rates = $package['rates'];
|
||||
$fallback_rate_id = current( array_keys( $rates ) );
|
||||
$selected_rate_id = isset( $selected_rates[ $package_key ] ) ? $selected_rates[ $package_key ] : $fallback_rate_id;
|
||||
$selected_rate = isset( $rates[ $selected_rate_id ] ) ? $rates[ $selected_rate_id ] : false;
|
||||
|
||||
if ( ! $rates ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( ! $selected_rate ) {
|
||||
throw new RestException(
|
||||
'invalid-shipping-rate-id',
|
||||
sprintf(
|
||||
/* translators: 1: Rate ID, 2: list of valid ids */
|
||||
__( '%1$s is not a valid shipping rate ID. Select one of the following: %2$s', 'woo-gutenberg-products-block' ),
|
||||
$selected_rate_id,
|
||||
implode( ', ', array_keys( $rates ) )
|
||||
),
|
||||
403
|
||||
);
|
||||
}
|
||||
|
||||
$item = new \WC_Order_Item_Shipping();
|
||||
$item->set_props(
|
||||
array(
|
||||
'method_title' => $selected_rate->label,
|
||||
'method_id' => $selected_rate->method_id,
|
||||
'instance_id' => $selected_rate->instance_id,
|
||||
'total' => wc_format_decimal( $selected_rate->cost ),
|
||||
'taxes' => array(
|
||||
'total' => $selected_rate->taxes,
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $selected_rate->get_meta_data() as $key => $value ) {
|
||||
$item->add_meta_data( $key, $value, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Action hook to adjust item before save.
|
||||
*/
|
||||
do_action( 'woocommerce_checkout_create_order_shipping_item', $item, $package_key, $package, $order );
|
||||
|
||||
// Add item to order and save.
|
||||
$order->add_item( $item );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get packages with calculated shipping.
|
||||
*
|
||||
* Based on WC_Cart::get_shipping_packages but allows the destination to be
|
||||
* customised based on the address in the order.
|
||||
*
|
||||
* @param \WC_Order $order Object to prepare for the response.
|
||||
* @param RestRequest $request Full details about the request.
|
||||
* @return array of packages and shipping rates.
|
||||
*/
|
||||
protected function get_shipping_packages( \WC_Order $order, RestRequest $request ) {
|
||||
if ( ! WC()->cart->needs_shipping() ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$packages = WC()->cart->get_shipping_packages();
|
||||
|
||||
foreach ( $packages as $key => $package ) {
|
||||
$packages[ $key ]['destination'] = [
|
||||
'address_1' => $order->get_shipping_address_1(),
|
||||
'address_2' => $order->get_shipping_address_2(),
|
||||
'city' => $order->get_shipping_city(),
|
||||
'state' => $order->get_shipping_state(),
|
||||
'postcode' => $order->get_shipping_postcode(),
|
||||
'country' => $order->get_shipping_country(),
|
||||
];
|
||||
}
|
||||
|
||||
$packages = WC()->shipping()->calculate_shipping( $packages );
|
||||
|
||||
return $packages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set props from API request.
|
||||
*
|
||||
* @param \WC_Order $order Object to prepare for the response.
|
||||
* @param RestRequest $request Full details about the request.
|
||||
*/
|
||||
protected function set_props_from_request( \WC_Order $order, RestRequest $request ) {
|
||||
$schema = $this->get_item_schema();
|
||||
|
||||
if ( isset( $request['billing_address'] ) ) {
|
||||
$allowed_billing_values = array_intersect_key( $request['billing_address'], $schema['properties']['billing_address']['properties'] );
|
||||
foreach ( $allowed_billing_values as $key => $value ) {
|
||||
$order->{"set_billing_$key"}( $value );
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $request['shipping_address'] ) ) {
|
||||
$allowed_shipping_values = array_intersect_key( $request['shipping_address'], $schema['properties']['shipping_address']['properties'] );
|
||||
foreach ( $allowed_shipping_values as $key => $value ) {
|
||||
$order->{"set_shipping_$key"}( $value );
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $request['customer_note'] ) ) {
|
||||
$order->set_customer_note( $request['customer_note'] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cart item schema.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_item_schema() {
|
||||
return $this->schema->get_item_schema();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a single item output for response.
|
||||
*
|
||||
* @param \WC_Order $object Object to prepare for the response.
|
||||
* @param RestRequest $request Request object.
|
||||
* @return RestResponse Response object.
|
||||
*/
|
||||
public function prepare_item_for_response( $object, $request ) {
|
||||
$response = [];
|
||||
|
||||
if ( $object instanceof \WC_Order ) {
|
||||
$response = $this->schema->get_item_response( $object );
|
||||
}
|
||||
return rest_ensure_response( $response );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,271 @@
|
|||
<?php
|
||||
/**
|
||||
* Cart order route.
|
||||
*
|
||||
* @package WooCommerce/Blocks
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\RestApi\StoreApi\Routes;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\RestApi\StoreApi\Utilities\CartController;
|
||||
|
||||
/**
|
||||
* CartCreateOrder class.
|
||||
*/
|
||||
class CartCreateOrder extends AbstractRoute {
|
||||
/**
|
||||
* Get the namespace for this route.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_namespace() {
|
||||
return 'wc/store';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path of this REST route.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_path() {
|
||||
return '/cart/create-order';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get method arguments for this REST route.
|
||||
*
|
||||
* @return array An array of endpoints.
|
||||
*/
|
||||
public function get_args() {
|
||||
return [
|
||||
[
|
||||
'methods' => \WP_REST_Server::CREATABLE,
|
||||
'callback' => [ $this, 'get_response' ],
|
||||
'args' => $this->schema->get_endpoint_args_for_item_schema( \WP_REST_Server::CREATABLE ),
|
||||
],
|
||||
'schema' => [ $this->schema, 'get_public_item_schema' ],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the cart into a new draft order, or update an existing draft order, and return an updated cart response.
|
||||
*
|
||||
* @throws RouteException On error.
|
||||
* @param \WP_REST_Request $request Request object.
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
protected function get_route_post_response( \WP_REST_Request $request ) {
|
||||
$this->draft_order = WC()->session->get(
|
||||
'store_api_draft_order',
|
||||
[
|
||||
'id' => 0,
|
||||
'hashes' => [
|
||||
'line_items' => false,
|
||||
'shipping' => false,
|
||||
'fees' => false,
|
||||
'coupons' => false,
|
||||
'taxes' => false,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
// Update session based on posted data.
|
||||
$this->update_session( $request );
|
||||
|
||||
// Create or retrieve the draft order for the current cart.
|
||||
$order_object = $this->create_order_from_cart( $request );
|
||||
|
||||
// Try to reserve stock for 10 mins, if available.
|
||||
// @todo Remove once min support for WC reaches 4.0.0.
|
||||
if ( \class_exists( '\Automattic\WooCommerce\Checkout\Helpers\ReserveStock' ) ) {
|
||||
$reserve_stock = new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock();
|
||||
} else {
|
||||
$reserve_stock = new \Automattic\WooCommerce\Blocks\RestApi\StoreApi\Utilities\ReserveStock();
|
||||
}
|
||||
|
||||
$reserve_stock->reserve_stock_for_order( $order_object, 10 );
|
||||
$response = $this->prepare_item_for_response( $order_object, $request );
|
||||
$response->set_status( 201 );
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Before creating anything, this method ensures the cart session is up to date and matches the data we're going
|
||||
* to be adding to the order.
|
||||
*
|
||||
* @param \WP_REST_Request $request Full details about the request.
|
||||
* @return void
|
||||
*/
|
||||
protected function update_session( \WP_REST_Request $request ) {
|
||||
$schema = $this->get_item_schema();
|
||||
|
||||
if ( isset( $request['billing_address'] ) ) {
|
||||
$allowed_billing_values = array_intersect_key( $request['billing_address'], $schema['properties']['billing_address']['properties'] );
|
||||
foreach ( $allowed_billing_values as $key => $value ) {
|
||||
WC()->customer->{"set_billing_$key"}( $value );
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $request['shipping_address'] ) ) {
|
||||
$allowed_shipping_values = array_intersect_key( $request['shipping_address'], $schema['properties']['shipping_address']['properties'] );
|
||||
foreach ( $allowed_shipping_values as $key => $value ) {
|
||||
WC()->customer->{"set_shipping_$key"}( $value );
|
||||
}
|
||||
}
|
||||
|
||||
WC()->customer->save();
|
||||
WC()->cart->calculate_shipping();
|
||||
WC()->cart->calculate_totals();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create order and set props based on global settings.
|
||||
*
|
||||
* @param \WP_REST_Request $request Full details about the request.
|
||||
* @return \WC_Order A new order object.
|
||||
*/
|
||||
protected function create_order_from_cart( \WP_REST_Request $request ) {
|
||||
add_filter( 'woocommerce_default_order_status', array( $this, 'default_order_status' ) );
|
||||
|
||||
$order = $this->get_order_object();
|
||||
$order->set_status( 'checkout-draft' );
|
||||
$order->set_created_via( 'store-api' );
|
||||
$order->set_currency( get_woocommerce_currency() );
|
||||
$order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) );
|
||||
$order->set_customer_id( get_current_user_id() );
|
||||
$order->set_customer_ip_address( \WC_Geolocation::get_ip_address() );
|
||||
$order->set_customer_user_agent( wc_get_user_agent() );
|
||||
$order->set_cart_hash( WC()->cart->get_cart_hash() );
|
||||
$order->update_meta_data( 'is_vat_exempt', WC()->cart->get_customer()->get_is_vat_exempt() ? 'yes' : 'no' );
|
||||
|
||||
$this->set_props_from_request( $order, $request );
|
||||
$this->create_line_items_from_cart( $order, $request );
|
||||
|
||||
// Calc totals, taxes, and save.
|
||||
$order->calculate_totals();
|
||||
|
||||
remove_filter( 'woocommerce_default_order_status', array( $this, 'default_order_status' ) );
|
||||
|
||||
// Store Order details in session so we can look it up later.
|
||||
WC()->session->set(
|
||||
'store_api_draft_order',
|
||||
[
|
||||
'id' => $order->get_id(),
|
||||
'hashes' => $this->get_cart_hashes(),
|
||||
]
|
||||
);
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hashes for items in the current cart. Useful for tracking changes.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function get_cart_hashes() {
|
||||
return [
|
||||
'line_items' => md5( wp_json_encode( WC()->cart->get_cart() ) ),
|
||||
'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() ) ),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an order object, either using a current draft order, or returning a new one.
|
||||
*
|
||||
* @return \WC_Order A new order object.
|
||||
*/
|
||||
protected function get_order_object() {
|
||||
$draft_order = $this->draft_order['id'] ? wc_get_order( $this->draft_order['id'] ) : false;
|
||||
|
||||
if ( $draft_order && $draft_order->has_status( 'checkout-draft' ) && 'store-api' === $draft_order->get_created_via() ) {
|
||||
return $draft_order;
|
||||
}
|
||||
|
||||
return new \WC_Order();
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes default order status to draft for orders created via this API.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function default_order_status() {
|
||||
return 'checkout-draft';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create order line items.
|
||||
*
|
||||
* @internal Knowing if items changed between the order and cart can be complex. Line items are ok because there is a
|
||||
* hash, but no hash exists for other line item types. Having a normalized set of data between cart and order, or
|
||||
* additional hashes, would be useful in the future and to help refactor this code. In the meantime, we're relying
|
||||
* on custom hashes in $this->draft_order to track if things changed.
|
||||
*
|
||||
* @param \WC_Order $order Object to prepare for the response.
|
||||
* @param \WP_REST_Request $request Full details about the request.
|
||||
*/
|
||||
protected function create_line_items_from_cart( \WC_Order $order, \WP_REST_Request $request ) {
|
||||
$new_hashes = $this->get_cart_hashes();
|
||||
$old_hashes = $this->draft_order['hashes'];
|
||||
$force = $this->draft_order['id'] !== $order->get_id();
|
||||
|
||||
if ( $force || $new_hashes['line_items'] !== $old_hashes['line_items'] ) {
|
||||
$order->remove_order_items( 'line_item' );
|
||||
WC()->checkout->create_order_line_items( $order, WC()->cart );
|
||||
}
|
||||
|
||||
if ( $force || $new_hashes['shipping'] !== $old_hashes['shipping'] ) {
|
||||
$order->remove_order_items( 'shipping' );
|
||||
WC()->checkout->create_order_shipping_lines( $order, WC()->session->get( 'chosen_shipping_methods' ), WC()->shipping()->get_packages() );
|
||||
}
|
||||
|
||||
if ( $force || $new_hashes['coupons'] !== $old_hashes['coupons'] ) {
|
||||
$order->remove_order_items( 'coupon' );
|
||||
WC()->checkout->create_order_coupon_lines( $order, WC()->cart );
|
||||
}
|
||||
|
||||
if ( $force || $new_hashes['fees'] !== $old_hashes['fees'] ) {
|
||||
$order->remove_order_items( 'fee' );
|
||||
WC()->checkout->create_order_fee_lines( $order, WC()->cart );
|
||||
}
|
||||
|
||||
if ( $force || $new_hashes['taxes'] !== $old_hashes['taxes'] ) {
|
||||
$order->remove_order_items( 'tax' );
|
||||
WC()->checkout->create_order_tax_lines( $order, WC()->cart );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set props from API request.
|
||||
*
|
||||
* @param \WC_Order $order Object to prepare for the response.
|
||||
* @param \WP_REST_Request $request Full details about the request.
|
||||
*/
|
||||
protected function set_props_from_request( \WC_Order $order, \WP_REST_Request $request ) {
|
||||
$schema = $this->get_item_schema();
|
||||
|
||||
if ( isset( $request['billing_address'] ) ) {
|
||||
$allowed_billing_values = array_intersect_key( $request['billing_address'], $schema['properties']['billing_address']['properties'] );
|
||||
foreach ( $allowed_billing_values as $key => $value ) {
|
||||
$order->{"set_billing_$key"}( $value );
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $request['shipping_address'] ) ) {
|
||||
$allowed_shipping_values = array_intersect_key( $request['shipping_address'], $schema['properties']['shipping_address']['properties'] );
|
||||
foreach ( $allowed_shipping_values as $key => $value ) {
|
||||
$order->{"set_shipping_$key"}( $value );
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $request['customer_note'] ) ) {
|
||||
$order->set_customer_note( $request['customer_note'] );
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
/**
|
||||
* Checkout route.
|
||||
*
|
||||
* @package WooCommerce/Blocks
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\RestApi\StoreApi\Routes;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\RestApi\StoreApi\Utilities\CartController;
|
||||
|
||||
/**
|
||||
* Checkout class.
|
||||
*/
|
||||
class Checkout extends AbstractRoute {
|
||||
/**
|
||||
* Get the namespace for this route.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_namespace() {
|
||||
return 'wc/store';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path of this REST route.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_path() {
|
||||
return '/checkout';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get method arguments for this REST route.
|
||||
*
|
||||
* @return array An array of endpoints.
|
||||
*/
|
||||
public function get_args() {
|
||||
return [
|
||||
[
|
||||
'methods' => \WP_REST_Server::CREATABLE,
|
||||
'callback' => [ $this, 'get_response' ],
|
||||
// @todo Determine the args we want to accept here.
|
||||
'args' => [],
|
||||
],
|
||||
'schema' => [ $this->schema, 'get_public_item_schema' ],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the cart into a new draft order, or update an existing draft order, and return an updated cart response.
|
||||
*
|
||||
* @throws RouteException On error.
|
||||
* @param \WP_REST_Request $request Request object.
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
protected function get_route_post_response( \WP_REST_Request $request ) {
|
||||
// @todo Add order and payment gateway processing here.
|
||||
// @todo we need to determine the fields we want to return after processing e.g. redirect URLs.
|
||||
$checkout_result = [
|
||||
'order' => new \WC_Order(),
|
||||
];
|
||||
$response = $this->prepare_item_for_response( $checkout_result, $request );
|
||||
$response->set_status( 200 );
|
||||
return $response;
|
||||
}
|
||||
}
|
|
@ -20,12 +20,14 @@ class RoutesController {
|
|||
public static function register_routes() {
|
||||
$schemas = [
|
||||
'cart' => new Schemas\CartSchema(),
|
||||
'checkout' => new Schemas\CheckoutSchema(),
|
||||
'coupon' => new Schemas\CartCouponSchema(),
|
||||
'cart-item' => new Schemas\CartItemSchema(),
|
||||
'product-attribute' => new Schemas\ProductAttributeSchema(),
|
||||
'term' => new Schemas\TermSchema(),
|
||||
'order' => new Schemas\OrderSchema(),
|
||||
'product' => new Schemas\ProductSchema(),
|
||||
'product-attribute' => new Schemas\ProductAttributeSchema(),
|
||||
'product-collection-data' => new Schemas\ProductCollectionDataSchema(),
|
||||
'term' => new Schemas\TermSchema(),
|
||||
];
|
||||
|
||||
$routes = [
|
||||
|
@ -33,6 +35,7 @@ class RoutesController {
|
|||
new Routes\CartApplyCoupon( $schemas['cart'] ),
|
||||
new Routes\CartCoupons( $schemas['coupon'] ),
|
||||
new Routes\CartCouponsByCode( $schemas['coupon'] ),
|
||||
new Routes\CartCreateOrder( $schemas['order'] ),
|
||||
new Routes\CartItems( $schemas['cart-item'] ),
|
||||
new Routes\CartItemsByKey( $schemas['cart-item'] ),
|
||||
new Routes\CartRemoveCoupon( $schemas['cart'] ),
|
||||
|
@ -40,6 +43,7 @@ class RoutesController {
|
|||
new Routes\CartSelectShippingRate( $schemas['cart'] ),
|
||||
new Routes\CartUpdateItem( $schemas['cart'] ),
|
||||
new Routes\CartUpdateShipping( $schemas['cart'] ),
|
||||
new Routes\Checkout( $schemas['checkout'] ),
|
||||
new Routes\ProductAttributes( $schemas['product-attribute'] ),
|
||||
new Routes\ProductAttributesById( $schemas['product-attribute'] ),
|
||||
new Routes\ProductAttributeTerms( $schemas['term'] ),
|
||||
|
|
|
@ -31,6 +31,12 @@ class CartSchema extends AbstractSchema {
|
|||
*/
|
||||
public function get_properties() {
|
||||
return [
|
||||
'order_id' => [
|
||||
'description' => __( 'The draft order ID associated with this cart if one has been created. 0 if no draft exists.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'integer',
|
||||
'context' => [ 'view', 'edit' ],
|
||||
'readonly' => true,
|
||||
],
|
||||
'coupons' => [
|
||||
'description' => __( 'List of applied cart coupons.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'array',
|
||||
|
@ -201,6 +207,7 @@ class CartSchema extends AbstractSchema {
|
|||
$context = 'edit';
|
||||
|
||||
return [
|
||||
'order_id' => $this->get_order_id(),
|
||||
'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() ) ),
|
||||
'shipping_address' => $shipping_address_schema,
|
||||
|
@ -229,6 +236,23 @@ class CartSchema extends AbstractSchema {
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a draft order ID from the session for current cart.
|
||||
*
|
||||
* @return int Draft order ID, or 0 if there isn't one yet.
|
||||
*/
|
||||
protected function get_order_id() {
|
||||
$draft_order_session = WC()->session->get( 'store_api_draft_order' );
|
||||
$draft_order_id = isset( $draft_order_session['id'] ) ? absint( $draft_order_session['id'] ) : 0;
|
||||
$draft_order = $draft_order_id ? wc_get_order( $draft_order_id ) : false;
|
||||
|
||||
if ( $draft_order && $draft_order->has_status( 'checkout-draft' ) && 'store-api' === $draft_order->get_created_via() ) {
|
||||
return $draft_order_id;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tax lines from the cart and format to match schema.
|
||||
*
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
/**
|
||||
* Checkout schema for the Store API.
|
||||
*
|
||||
* @package WooCommerce/Blocks
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\RestApi\StoreApi\Schemas;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* CheckoutSchema class.
|
||||
*/
|
||||
class CheckoutSchema extends AbstractSchema {
|
||||
/**
|
||||
* The schema item name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $title = 'checkout';
|
||||
|
||||
/**
|
||||
* Checkout schema properties.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_properties() {
|
||||
return [
|
||||
'order' => [
|
||||
'description' => __( 'The order that was processed.', 'woo-gutenberg-products-block' ),
|
||||
'type' => 'object',
|
||||
'context' => [ 'view', 'edit' ],
|
||||
'readonly' => true,
|
||||
'properties' => $this->force_schema_readonly( ( new OrderSchema() )->get_properties() ),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the response for checkout.
|
||||
*
|
||||
* @todo we need to determine the fields we want to return after processing e.g. redirect URLs.
|
||||
*
|
||||
* @param object $checkout_result Result from checkout action.
|
||||
* @return array
|
||||
*/
|
||||
public function get_item_response( $checkout_result ) {
|
||||
$order_schema = new OrderSchema();
|
||||
return [
|
||||
'note' => 'This is a placeholder',
|
||||
'order' => $order_schema->get_item_response( $checkout_result['order'] ),
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,158 @@
|
|||
<?php
|
||||
/**
|
||||
* Create Order 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_Order as OrderHelper;
|
||||
use \WC_Helper_Coupon as CouponHelper;
|
||||
use Automattic\WooCommerce\Blocks\Tests\Helpers\ValidateSchema;
|
||||
|
||||
/**
|
||||
* CartCreateOrder tests.
|
||||
*/
|
||||
class CartCreateOrder extends TestCase {
|
||||
/**
|
||||
* Setup test data. Called before every test.
|
||||
*/
|
||||
public function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
wp_set_current_user( 0 );
|
||||
|
||||
$this->products = [];
|
||||
|
||||
// Create some test products.
|
||||
$this->products[0] = ProductHelper::create_simple_product( false );
|
||||
$this->products[0]->set_weight( 10 );
|
||||
$this->products[0]->set_regular_price( 10 );
|
||||
$this->products[0]->save();
|
||||
|
||||
$this->products[1] = ProductHelper::create_simple_product( false );
|
||||
$this->products[1]->set_weight( 10 );
|
||||
$this->products[1]->set_regular_price( 10 );
|
||||
$this->products[1]->save();
|
||||
|
||||
wc_empty_cart();
|
||||
|
||||
$this->keys = [];
|
||||
$this->keys[] = wc()->cart->add_to_cart( $this->products[0]->get_id(), 2 );
|
||||
$this->keys[] = wc()->cart->add_to_cart( $this->products[1]->get_id(), 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test route registration.
|
||||
*/
|
||||
public function test_register_routes() {
|
||||
$routes = $this->server->get_routes();
|
||||
$this->assertArrayHasKey( '/wc/store/cart/create-order', $routes );
|
||||
|
||||
$request = new WP_REST_Request( 'GET', '/wc/store/cart/create-order' );
|
||||
$response = $this->server->dispatch( $request );
|
||||
$this->assertEquals( 404, $response->get_status() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test order creation from cart data.
|
||||
*/
|
||||
public function test_create_item() {
|
||||
$request = new WP_REST_Request( 'POST', '/wc/store/cart/create-order' );
|
||||
$request->set_param(
|
||||
'billing_address',
|
||||
[
|
||||
'first_name' => 'Margaret',
|
||||
'last_name' => 'Thatchcroft',
|
||||
'address_1' => '123 South Street',
|
||||
'address_2' => 'Apt 1',
|
||||
'city' => 'Philadelphia',
|
||||
'state' => 'PA',
|
||||
'postcode' => '19123',
|
||||
'country' => 'US',
|
||||
'email' => 'test@test.com',
|
||||
'phone' => '',
|
||||
]
|
||||
);
|
||||
$response = $this->server->dispatch( $request );
|
||||
$data = $response->get_data();
|
||||
|
||||
$this->assertEquals( 201, $response->get_status() );
|
||||
|
||||
$this->assertArrayHasKey( 'id', $data );
|
||||
$this->assertArrayHasKey( 'number', $data );
|
||||
$this->assertArrayHasKey( 'status', $data );
|
||||
$this->assertArrayHasKey( 'order_key', $data );
|
||||
$this->assertArrayHasKey( 'created_via', $data );
|
||||
$this->assertArrayHasKey( 'prices_include_tax', $data );
|
||||
$this->assertArrayHasKey( 'events', $data );
|
||||
$this->assertArrayHasKey( 'customer', $data );
|
||||
$this->assertArrayHasKey( 'billing_address', $data );
|
||||
$this->assertArrayHasKey( 'shipping_address', $data );
|
||||
$this->assertArrayHasKey( 'customer_note', $data );
|
||||
$this->assertArrayHasKey( 'items', $data );
|
||||
$this->assertArrayHasKey( 'totals', $data );
|
||||
|
||||
$this->assertEquals( 'Margaret', $data['billing_address']->first_name );
|
||||
$this->assertEquals( 'Thatchcroft', $data['billing_address']->last_name );
|
||||
$this->assertEquals( '123 South Street', $data['billing_address']->address_1 );
|
||||
$this->assertEquals( 'Apt 1', $data['billing_address']->address_2 );
|
||||
$this->assertEquals( 'Philadelphia', $data['billing_address']->city );
|
||||
$this->assertEquals( 'PA', $data['billing_address']->state );
|
||||
$this->assertEquals( '19123', $data['billing_address']->postcode );
|
||||
$this->assertEquals( 'US', $data['billing_address']->country );
|
||||
$this->assertEquals( 'test@test.com', $data['billing_address']->email );
|
||||
$this->assertEquals( '', $data['billing_address']->phone );
|
||||
|
||||
$this->assertEquals( 'checkout-draft', $data['status'] );
|
||||
$this->assertEquals( 2, count( $data['items'] ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test conversion of cart item to rest response.
|
||||
*/
|
||||
public function test_prepare_item_for_response() {
|
||||
$schema = new \Automattic\WooCommerce\Blocks\RestApi\StoreApi\Schemas\OrderSchema();
|
||||
$controller = new \Automattic\WooCommerce\Blocks\RestApi\StoreApi\Routes\CartCreateOrder( $schema );
|
||||
$order = OrderHelper::create_order();
|
||||
$response = $controller->prepare_item_for_response( $order, new \WP_REST_Request() );
|
||||
$data = $response->get_data();
|
||||
|
||||
$this->assertArrayHasKey( 'id', $data );
|
||||
$this->assertArrayHasKey( 'number', $data );
|
||||
$this->assertArrayHasKey( 'status', $data );
|
||||
$this->assertArrayHasKey( 'order_key', $data );
|
||||
$this->assertArrayHasKey( 'created_via', $data );
|
||||
$this->assertArrayHasKey( 'prices_include_tax', $data );
|
||||
$this->assertArrayHasKey( 'events', $data );
|
||||
$this->assertArrayHasKey( 'customer', $data );
|
||||
$this->assertArrayHasKey( 'billing_address', $data );
|
||||
$this->assertArrayHasKey( 'shipping_address', $data );
|
||||
$this->assertArrayHasKey( 'customer_note', $data );
|
||||
$this->assertArrayHasKey( 'items', $data );
|
||||
$this->assertArrayHasKey( 'totals', $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test schema matches responses.
|
||||
*/
|
||||
public function test_schema_matches_response() {
|
||||
$schema = new \Automattic\WooCommerce\Blocks\RestApi\StoreApi\Schemas\OrderSchema();
|
||||
$controller = new \Automattic\WooCommerce\Blocks\RestApi\StoreApi\Routes\CartCreateOrder( $schema );
|
||||
|
||||
$order = OrderHelper::create_order();
|
||||
$coupon = CouponHelper::create_coupon();
|
||||
$order->apply_coupon( $coupon );
|
||||
|
||||
$response = $controller->prepare_item_for_response( $order, new \WP_REST_Request() );
|
||||
$schema = $controller->get_item_schema();
|
||||
$validate = new ValidateSchema( $schema );
|
||||
|
||||
$diff = $validate->get_diff_from_object( $response->get_data() );
|
||||
$this->assertEmpty( $diff, print_r( $diff, true ) );
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue