* Checkout/order WIP schema

* Add _address suffix for billing/shipping

* Rename schema, update endpoints, create tests

* Fix POST in test

* Fix test response checks

* Stock reservation and draft order status

* Add todo for shipping lines

* Readme

* Rename address fields in readme

* 10 min timeout of stock

* Fix broken test

* Update src/RestApi/StoreApi/Controllers/CartOrder.php

Co-Authored-By: Darren Ethier <darren@roughsmootheng.in>

* Add typehinting where possible

* Remove explicit pass by reference

* Further typehinting

* Clarify todo comment

* Validate product instances

* Specific phpcs exclusion rule

* Exclusion rule

* Move ReserveStock code to class

* Correct shipping-rates schema to shipping_rates

* Save shipping rates and lines if included with request

* Insert todo for shipping rate code

* Calculate shipping and selected shipping from order properties, not global cart properties

* Prevent error when shipping is not needed

* Update API readme

* Added tests for stock reserve class

* Fixes conflicts with draft statuses

Co-authored-by: Darren Ethier <darren@roughsmootheng.in>
This commit is contained in:
Mike Jolley 2020-01-10 13:53:16 +00:00 committed by GitHub
parent f339e28310
commit 0150681c4b
15 changed files with 2081 additions and 163 deletions

View File

@ -19,6 +19,63 @@ class Library {
*/ */
public static function init() { public static function init() {
add_action( 'init', array( __CLASS__, 'register_blocks' ) ); add_action( 'init', array( __CLASS__, 'register_blocks' ) );
add_action( 'init', array( __CLASS__, 'define_tables' ) );
add_action( 'init', array( __CLASS__, 'maybe_create_tables' ) );
add_filter( 'wc_order_statuses', array( __CLASS__, 'register_draft_order_status' ) );
add_filter( 'woocommerce_register_shop_order_post_statuses', array( __CLASS__, 'register_draft_order_post_status' ) );
}
/**
* Register custom tables within $wpdb object.
*/
public static function define_tables() {
global $wpdb;
// List of tables without prefixes.
$tables = array(
'wc_reserved_stock' => 'wc_reserved_stock',
);
foreach ( $tables as $name => $table ) {
$wpdb->$name = $wpdb->prefix . $table;
$wpdb->tables[] = $table;
}
}
/**
* Set up the database tables which the plugin needs to function.
*/
public static function maybe_create_tables() {
$db_version = get_option( 'wc_blocks_db_version', 0 );
if ( version_compare( $db_version, \Automattic\WooCommerce\Blocks\Package::get_version(), '>=' ) ) {
return;
}
global $wpdb;
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
$wpdb->hide_errors();
$collate = '';
if ( $wpdb->has_cap( 'collation' ) ) {
$collate = $wpdb->get_charset_collate();
}
dbDelta(
"
CREATE TABLE {$wpdb->prefix}wc_reserved_stock (
`order_id` bigint(20) NOT NULL,
`product_id` bigint(20) NOT NULL,
`stock_quantity` double NOT NULL DEFAULT 0,
`timestamp` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`order_id`, `product_id`)
) $collate;
"
);
update_option( 'wc_blocks_db_version', \Automattic\WooCommerce\Blocks\Package::get_version() );
} }
/** /**
@ -59,4 +116,36 @@ class Library {
$instance->register_block_type(); $instance->register_block_type();
} }
} }
/**
* Register custom order status for orders created via the API during checkout.
*
* Draft order status is used before payment is attempted, during checkout, when a cart is converted to an order.
*
* @param array $statuses Array of statuses.
* @return array
*/
public static function register_draft_order_status( array $statuses ) {
$statuses['wc-checkout-draft'] = _x( 'Draft', 'Order status', 'woo-gutenberg-products-block' );
return $statuses;
}
/**
* Register custom order post status for orders created via the API during checkout.
*
* @param array $statuses Array of statuses.
* @return array
*/
public static function register_draft_order_post_status( array $statuses ) {
$statuses['wc-checkout-draft'] = [
'label' => _x( 'Draft', 'Order status', 'woo-gutenberg-products-block' ),
'public' => false,
'exclude_from_search' => false,
'show_in_admin_all_list' => false,
'show_in_admin_status_list' => true,
/* translators: %s: number of orders */
'label_count' => _n_noop( 'Drafts <span class="count">(%s)</span>', 'Drafts <span class="count">(%s)</span>', 'woo-gutenberg-products-block' ),
];
return $statuses;
}
} }

View File

@ -55,7 +55,7 @@ class RestApi {
} }
/** /**
* If we're making a cart request, we may need to load some additonal classes from WC Core so we're ready to deal with requests. * If we're making a cart request, we may need to load some additional classes from WC Core so we're ready to deal with requests.
* *
* Note: We load the session here early so guest nonces are in place. * Note: We load the session here early so guest nonces are in place.
* *
@ -96,6 +96,7 @@ class RestApi {
'store-cart-items' => __NAMESPACE__ . '\RestApi\StoreApi\Controllers\CartItems', 'store-cart-items' => __NAMESPACE__ . '\RestApi\StoreApi\Controllers\CartItems',
'store-cart-coupons' => __NAMESPACE__ . '\RestApi\StoreApi\Controllers\CartCoupons', 'store-cart-coupons' => __NAMESPACE__ . '\RestApi\StoreApi\Controllers\CartCoupons',
'store-cart-shipping-rates' => __NAMESPACE__ . '\RestApi\StoreApi\Controllers\CartShippingRates', 'store-cart-shipping-rates' => __NAMESPACE__ . '\RestApi\StoreApi\Controllers\CartShippingRates',
'store-cart-order' => __NAMESPACE__ . '\RestApi\StoreApi\Controllers\CartOrder',
'store-customer' => __NAMESPACE__ . '\RestApi\StoreApi\Controllers\Customer', 'store-customer' => __NAMESPACE__ . '\RestApi\StoreApi\Controllers\Customer',
'store-products' => __NAMESPACE__ . '\RestApi\StoreApi\Controllers\Products', 'store-products' => __NAMESPACE__ . '\RestApi\StoreApi\Controllers\Products',
'store-product-collection-data' => __NAMESPACE__ . '\RestApi\StoreApi\Controllers\ProductCollectionData', 'store-product-collection-data' => __NAMESPACE__ . '\RestApi\StoreApi\Controllers\ProductCollectionData',

View File

@ -0,0 +1,456 @@
<?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;
use Automattic\WooCommerce\Blocks\RestApi\StoreApi\Utilities\ReserveStock;
/**
* 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, if available.
$this->reserve_stock( $order_object );
$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();
}
/**
* Put a temporary hold on stock for this order.
*
* @throws RestException Exception when stock cannot be reserved.
* @param \WC_Order $order Order object.
*/
protected function reserve_stock( \WC_Order $order ) {
$reserve_stock_helper = new ReserveStock();
$result = $reserve_stock_helper->reserve_stock_for_order( $order );
if ( is_wp_error( $result ) ) {
throw new RestException( $result->get_error_code(), $result->get_error_message(), $result->get_error_data( 'status' ) );
}
}
/**
* 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 );
}
}

View File

@ -114,15 +114,15 @@ class Customer extends RestController {
} }
try { try {
if ( isset( $request['billing'] ) ) { if ( isset( $request['billing_address'] ) ) {
$allowed_billing_values = array_intersect_key( $request['billing'], $schema['properties']['billing']['properties'] ); $allowed_billing_values = array_intersect_key( $request['billing_address'], $schema['properties']['billing_address']['properties'] );
foreach ( $allowed_billing_values as $key => $value ) { foreach ( $allowed_billing_values as $key => $value ) {
$customer->{"set_billing_$key"}( $value ); $customer->{"set_billing_$key"}( $value );
} }
} }
if ( isset( $request['shipping'] ) ) { if ( isset( $request['shipping_address'] ) ) {
$allowed_shipping_values = array_intersect_key( $request['shipping'], $schema['properties']['shipping']['properties'] ); $allowed_shipping_values = array_intersect_key( $request['shipping_address'], $schema['properties']['shipping_address']['properties'] );
foreach ( $allowed_shipping_values as $key => $value ) { foreach ( $allowed_shipping_values as $key => $value ) {
$customer->{"set_shipping_$key"}( $value ); $customer->{"set_shipping_$key"}( $value );
} }

View File

@ -820,32 +820,32 @@ Example response:
```json ```json
[ [
{ {
"code": "20off", "code": "20off",
"totals": { "totals": {
"currency_code": "GBP", "currency_code": "GBP",
"currency_symbol": "£", "currency_symbol": "£",
"currency_minor_unit": 2, "currency_minor_unit": 2,
"currency_decimal_separator": ".", "currency_decimal_separator": ".",
"currency_thousand_separator": ",", "currency_thousand_separator": ",",
"currency_prefix": "£", "currency_prefix": "£",
"currency_suffix": "", "currency_suffix": "",
"total_discount": "1667", "total_discount": "1667",
"total_discount_tax": "333" "total_discount_tax": "333"
}, },
"_links": { "_links": {
"self": [ "self": [
{ {
"href": "http:\/\/local.wordpress.test\/wp-json\/wc\/store\/cart\/coupons\/20off" "href": "http://local.wordpress.test/wp-json/wc/store/cart/coupons/20off"
} }
], ],
"collection": [ "collection": [
{ {
"href": "http:\/\/local.wordpress.test\/wp-json\/wc\/store\/cart\/coupons" "href": "http://local.wordpress.test/wp-json/wc/store/cart/coupons"
} }
] ]
} }
} }
] ]
``` ```
@ -869,18 +869,18 @@ Example response:
```json ```json
{ {
"code": "20off", "code": "20off",
"totals": { "totals": {
"currency_code": "GBP", "currency_code": "GBP",
"currency_symbol": "£", "currency_symbol": "£",
"currency_minor_unit": 2, "currency_minor_unit": 2,
"currency_decimal_separator": ".", "currency_decimal_separator": ".",
"currency_thousand_separator": ",", "currency_thousand_separator": ",",
"currency_prefix": "£", "currency_prefix": "£",
"currency_suffix": "", "currency_suffix": "",
"total_discount": "1667", "total_discount": "1667",
"total_discount_tax": "333" "total_discount_tax": "333"
} }
} }
``` ```
@ -904,18 +904,18 @@ Example response:
```json ```json
{ {
"code": "20off", "code": "20off",
"totals": { "totals": {
"currency_code": "GBP", "currency_code": "GBP",
"currency_symbol": "£", "currency_symbol": "£",
"currency_minor_unit": 2, "currency_minor_unit": 2,
"currency_decimal_separator": ".", "currency_decimal_separator": ".",
"currency_thousand_separator": ",", "currency_thousand_separator": ",",
"currency_prefix": "£", "currency_prefix": "£",
"currency_suffix": "", "currency_suffix": "",
"total_discount": "1667", "total_discount": "1667",
"total_discount_tax": "333" "total_discount_tax": "333"
} }
} }
``` ```
@ -990,7 +990,7 @@ Example response:
"country": "US" "country": "US"
}, },
"items": [ "6512bd43d9caa6e02c990b0a82652dca" ], "items": [ "6512bd43d9caa6e02c990b0a82652dca" ],
"shipping-rates": [ "shipping_rates": [
{ {
"name": "International", "name": "International",
"description": "", "description": "",
@ -1011,6 +1011,137 @@ Example response:
] ]
``` ```
## Cart Order API
Create a new order from the items in the cart.
```http
POST /cart/order/
```
| Attribute | Type | Required | Description |
| :----------------- | :----- | :------: | :-------------------------------------------------------------------------------------- |
| `billing_address` | array | No | Billing address data to store to the new order. |
| `shipping_address` | array | No | Shipping address data to store to the new order. |
| `customer_note` | string | No | Customer note to store to the new order. |
| `shipping_rates` | array | No | Array of objects containing `rate_id` of selected shipping methods to add to the order. |
```http
curl --request POST https://example-store.com/wp-json/wc/store/cart/order
```
Example response:
```json
{
"id": 149,
"number": "149",
"status": "draft",
"order_key": "wc_order_9falc306dOkWb",
"created_via": "store-api",
"prices_include_tax": true,
"events": {
"date_created": "2020-01-07T12:33:23",
"date_created_gmt": "2020-01-07T12:33:23",
"date_modified": "2020-01-07T12:33:23",
"date_modified_gmt": "2020-01-07T12:33:23",
"date_completed": null,
"date_completed_gmt": null,
"date_paid": null,
"date_paid_gmt": null
},
"customer": {
"customer_id": 1,
"customer_ip_address": "192.168.50.1",
"customer_user_agent": "insomnia/7.0.5"
},
"customer_note": "This is a customer note.",
"billing_address": {
"first_name": "Margaret",
"last_name": "Thatchcroft",
"company": "",
"address_1": "123 South Street",
"address_2": "Apt 1",
"city": "Philadelphia",
"state": "PA",
"postcode": "19123",
"country": "US",
"email": "test@test.com",
"phone": ""
},
"shipping_address": {
"first_name": "Margaret",
"last_name": "Thatchcroft",
"company": "",
"address_1": "123 South Street",
"address_2": "Apt 1",
"city": "Philadelphia",
"state": "PA",
"postcode": "19123",
"country": "US"
},
"items": [
{
"id": 12,
"quantity": 1,
"name": "Belt",
"sku": "woo-belt",
"permalink": "http://local.wordpress.test/product/belt/",
"images": [
{
"id": "41",
"src": "http://local.wordpress.test/wp-content/uploads/2019/12/belt-2.jpg",
"thumbnail": "http://local.wordpress.test/wp-content/uploads/2019/12/belt-2-300x300.jpg",
"srcset": "http://local.wordpress.test/wp-content/uploads/2019/12/belt-2.jpg 801w, http://local.wordpress.test/wp-content/uploads/2019/12/belt-2-300x300.jpg 300w, http://local.wordpress.test/wp-content/uploads/2019/12/belt-2-100x100.jpg 100w, http://local.wordpress.test/wp-content/uploads/2019/12/belt-2-450x450.jpg 450w, http://local.wordpress.test/wp-content/uploads/2019/12/belt-2-150x150.jpg 150w, http://local.wordpress.test/wp-content/uploads/2019/12/belt-2-768x768.jpg 768w",
"sizes": "(max-width: 801px) 100vw, 801px",
"name": "belt-2.jpg",
"alt": ""
}
],
"variation": [],
"totals": {
"currency_code": "GBP",
"currency_symbol": "£",
"currency_minor_unit": 2,
"currency_decimal_separator": ".",
"currency_thousand_separator": ",",
"currency_prefix": "£",
"currency_suffix": "",
"line_subtotal": "4583",
"line_subtotal_tax": "917",
"line_total": "4583",
"line_total_tax": "917"
}
}
],
"totals": {
"currency_code": "GBP",
"currency_symbol": "£",
"currency_minor_unit": 2,
"currency_decimal_separator": ".",
"currency_thousand_separator": ",",
"currency_prefix": "£",
"currency_suffix": "",
"total_items": "4583",
"total_items_tax": "917",
"total_fees": "0",
"total_fees_tax": "0",
"total_discount": "0",
"total_discount_tax": "0",
"total_shipping": "499",
"total_shipping_tax": "100",
"total_price": "6099",
"total_tax": "1017",
"tax_lines": [
{
"name": "Tax",
"price": "1017"
}
]
}
}
```
## Customer API ## Customer API
### Get data for the current customer ### Get data for the current customer
@ -1029,31 +1160,31 @@ Example response:
```json ```json
{ {
"id": 0, "id": 0,
"billing": { "billing_address": {
"first_name": "Margaret", "first_name": "Margaret",
"last_name": "Thatchcroft", "last_name": "Thatchcroft",
"company": "", "company": "",
"address_1": "123 South Street", "address_1": "123 South Street",
"address_2": "Apt 1", "address_2": "Apt 1",
"city": "Philadelphia", "city": "Philadelphia",
"state": "PA", "state": "PA",
"postcode": "19123", "postcode": "19123",
"country": "US", "country": "US",
"email": "test@test.com", "email": "test@test.com",
"phone": "" "phone": ""
}, },
"shipping": { "shipping_address": {
"first_name": "Margaret", "first_name": "Margaret",
"last_name": "Thatchcroft", "last_name": "Thatchcroft",
"company": "", "company": "",
"address_1": "123 South Street", "address_1": "123 South Street",
"address_2": "Apt 1", "address_2": "Apt 1",
"city": "Philadelphia", "city": "Philadelphia",
"state": "PA", "state": "PA",
"postcode": "19123", "postcode": "19123",
"country": "US" "country": "US"
} }
} }
``` ```
@ -1065,10 +1196,10 @@ Edit current customer data, such as billing and shipping addresses.
PUT /cart/customer PUT /cart/customer
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| :--------- | :------ | :------: | :--------------------------------- | | :--------- | :----- | :------: | :--------------------------- |
| `billing` | object | No | Billing address properties. | | `billing` | object | No | Billing address properties. |
| `shipping` | object | No | Shipping address properties. | | `shipping` | object | No | Shipping address properties. |
```http ```http
curl --request PUT https://example-store.com/wp-json/wc/store/cart/customer?billing[company]=Test curl --request PUT https://example-store.com/wp-json/wc/store/cart/customer?billing[company]=Test
@ -1078,31 +1209,31 @@ Example response:
```json ```json
{ {
"id": 0, "id": 0,
"billing": { "billing_address": {
"first_name": "Margaret", "first_name": "Margaret",
"last_name": "Thatchcroft", "last_name": "Thatchcroft",
"company": "Test", "company": "Test",
"address_1": "123 South Street", "address_1": "123 South Street",
"address_2": "Apt 1", "address_2": "Apt 1",
"city": "Philadelphia", "city": "Philadelphia",
"state": "PA", "state": "PA",
"postcode": "19123", "postcode": "19123",
"country": "US", "country": "US",
"email": "test@test.com", "email": "test@test.com",
"phone": "" "phone": ""
}, },
"shipping": { "shipping_address": {
"first_name": "Margaret", "first_name": "Margaret",
"last_name": "Thatchcroft", "last_name": "Thatchcroft",
"company": "", "company": "",
"address_1": "123 South Street", "address_1": "123 South Street",
"address_2": "Apt 1", "address_2": "Apt 1",
"city": "Philadelphia", "city": "Philadelphia",
"state": "PA", "state": "PA",
"postcode": "19123", "postcode": "19123",
"country": "US" "country": "US"
} }
} }
``` ```

View File

@ -123,14 +123,14 @@ class CartSchema extends AbstractSchema {
'context' => [ 'view', 'edit' ], 'context' => [ 'view', 'edit' ],
'readonly' => true, 'readonly' => true,
], ],
'total_tax' => [ 'total_price' => [
'description' => __( 'Total tax applied to items and shipping.', 'woo-gutenberg-products-block' ), 'description' => __( 'Total price the customer will pay.', 'woo-gutenberg-products-block' ),
'type' => 'string', 'type' => 'string',
'context' => [ 'view', 'edit' ], 'context' => [ 'view', 'edit' ],
'readonly' => true, 'readonly' => true,
], ],
'total_price' => [ 'total_tax' => [
'description' => __( 'Total price the customer will pay.', 'woo-gutenberg-products-block' ), 'description' => __( 'Total tax applied to items and shipping.', 'woo-gutenberg-products-block' ),
'type' => 'string', 'type' => 'string',
'context' => [ 'view', 'edit' ], 'context' => [ 'view', 'edit' ],
'readonly' => true, 'readonly' => true,
@ -190,8 +190,8 @@ class CartSchema extends AbstractSchema {
'total_discount_tax' => $this->prepare_money_response( $cart->get_discount_tax(), wc_get_price_decimals() ), 'total_discount_tax' => $this->prepare_money_response( $cart->get_discount_tax(), wc_get_price_decimals() ),
'total_shipping' => $this->prepare_money_response( $cart->get_shipping_total(), wc_get_price_decimals() ), 'total_shipping' => $this->prepare_money_response( $cart->get_shipping_total(), wc_get_price_decimals() ),
'total_shipping_tax' => $this->prepare_money_response( $cart->get_shipping_tax(), wc_get_price_decimals() ), 'total_shipping_tax' => $this->prepare_money_response( $cart->get_shipping_tax(), wc_get_price_decimals() ),
'total_tax' => $this->prepare_money_response( $cart->get_total_tax(), wc_get_price_decimals() ),
'total_price' => $this->prepare_money_response( $cart->get_total(), wc_get_price_decimals() ), 'total_price' => $this->prepare_money_response( $cart->get_total(), 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 ), 'tax_lines' => $this->get_tax_lines( $cart ),
] ]
), ),

View File

@ -82,7 +82,7 @@ class CartShippingRateSchema extends AbstractSchema {
'type' => 'string', 'type' => 'string',
], ],
], ],
'shipping-rates' => [ 'shipping_rates' => [
'description' => __( 'List of shipping rates.', 'woo-gutenberg-products-block' ), 'description' => __( 'List of shipping rates.', 'woo-gutenberg-products-block' ),
'type' => 'array', 'type' => 'array',
'context' => [ 'view', 'edit' ], 'context' => [ 'view', 'edit' ],
@ -186,7 +186,7 @@ class CartShippingRateSchema extends AbstractSchema {
'country' => $package['destination']['country'], 'country' => $package['destination']['country'],
], ],
'items' => array_values( wp_list_pluck( $package['contents'], 'key' ) ), 'items' => array_values( wp_list_pluck( $package['contents'], 'key' ) ),
'shipping-rates' => array_values( array_map( [ $this, 'get_rate_response' ], $package['rates'] ) ), 'shipping_rates' => array_values( array_map( [ $this, 'get_rate_response' ], $package['rates'] ) ),
]; ];
} }

View File

@ -29,13 +29,13 @@ class CustomerSchema extends AbstractSchema {
*/ */
protected function get_properties() { protected function get_properties() {
return [ return [
'id' => [ 'id' => [
'description' => __( 'Customer ID. Will return 0 if the customer is logged out.', 'woo-gutenberg-products-block' ), 'description' => __( 'Customer ID. Will return 0 if the customer is logged out.', 'woo-gutenberg-products-block' ),
'type' => 'integer', 'type' => 'integer',
'context' => [ 'view', 'edit' ], 'context' => [ 'view', 'edit' ],
'readonly' => true, 'readonly' => true,
], ],
'billing' => [ 'billing_address' => [
'description' => __( 'List of billing address data.', 'woo-gutenberg-products-block' ), 'description' => __( 'List of billing address data.', 'woo-gutenberg-products-block' ),
'type' => 'object', 'type' => 'object',
'context' => [ 'view', 'edit' ], 'context' => [ 'view', 'edit' ],
@ -102,7 +102,7 @@ class CustomerSchema extends AbstractSchema {
], ],
], ],
], ],
'shipping' => [ 'shipping_address' => [
'description' => __( 'List of shipping address data.', 'woo-gutenberg-products-block' ), 'description' => __( 'List of shipping address data.', 'woo-gutenberg-products-block' ),
'type' => 'object', 'type' => 'object',
'context' => [ 'view', 'edit' ], 'context' => [ 'view', 'edit' ],
@ -165,8 +165,8 @@ class CustomerSchema extends AbstractSchema {
*/ */
public function get_item_response( $object ) { public function get_item_response( $object ) {
return [ return [
'id' => $object->get_id(), 'id' => $object->get_id(),
'billing' => [ 'billing_address' => [
'first_name' => $object->get_billing_first_name(), 'first_name' => $object->get_billing_first_name(),
'last_name' => $object->get_billing_last_name(), 'last_name' => $object->get_billing_last_name(),
'company' => $object->get_billing_company(), 'company' => $object->get_billing_company(),
@ -179,7 +179,7 @@ class CustomerSchema extends AbstractSchema {
'email' => $object->get_billing_email(), 'email' => $object->get_billing_email(),
'phone' => $object->get_billing_phone(), 'phone' => $object->get_billing_phone(),
], ],
'shipping' => [ 'shipping_address' => [
'first_name' => $object->get_shipping_first_name(), 'first_name' => $object->get_shipping_first_name(),
'last_name' => $object->get_shipping_last_name(), 'last_name' => $object->get_shipping_last_name(),
'company' => $object->get_shipping_company(), 'company' => $object->get_shipping_company(),

View File

@ -0,0 +1,247 @@
<?php
/**
* Order Item Schema.
*
* @package WooCommerce/Blocks
*/
namespace Automattic\WooCommerce\Blocks\RestApi\StoreApi\Schemas;
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Blocks\RestApi\Utilities\ProductImages;
/**
* OrderItemSchema class.
*/
class OrderItemSchema extends AbstractSchema {
/**
* The schema item name.
*
* @var string
*/
protected $title = 'order_item';
/**
* Cart schema properties.
*
* @return array
*/
protected function get_properties() {
return [
'id' => [
'description' => __( 'The item product or variation ID.', 'woo-gutenberg-products-block' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'required' => true,
'arg_options' => [
'sanitize_callback' => 'absint',
'validate_callback' => [ $this, 'product_id_exists' ],
],
],
'quantity' => [
'description' => __( 'Quantity of this item in the cart.', 'woo-gutenberg-products-block' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'required' => true,
'arg_options' => [
'sanitize_callback' => 'wc_stock_amount',
],
],
'name' => [
'description' => __( 'Product name.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'sku' => [
'description' => __( 'Stock keeping unit, if applicable.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'permalink' => [
'description' => __( 'Product URL.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'format' => 'uri',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'images' => [
'description' => __( 'List of images.', 'woo-gutenberg-products-block' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => [
'id' => [
'description' => __( 'Image ID.', 'woo-gutenberg-products-block' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'src' => [
'description' => __( 'Image URL.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'format' => 'uri',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'name' => [
'description' => __( 'Image name.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'alt' => [
'description' => __( 'Image alternative text.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
],
'variation' => [
'description' => __( 'Chosen attributes (for variations).', 'woo-gutenberg-products-block' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'items' => [
'type' => 'object',
'properties' => [
'attribute' => [
'description' => __( 'Variation attribute name.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'value' => [
'description' => __( 'Variation attribute value.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
],
],
],
'totals' => [
'description' => __( 'Item total amounts provided using the smallest unit of the currency.', 'woo-gutenberg-products-block' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => array_merge(
$this->get_store_currency_properties(),
[
'line_subtotal' => [
'description' => __( 'Line price subtotal (excluding coupons and discounts).', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'line_subtotal_tax' => [
'description' => __( 'Line price subtotal tax.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'line_total' => [
'description' => __( 'Line price total (including coupons and discounts).', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'line_total_tax' => [
'description' => __( 'Line price total tax.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
]
),
],
];
}
/**
* Check given ID exists,
*
* @param integer $product_id Product ID.
* @return bool
*/
public function product_id_exists( $product_id ) {
$post = get_post( (int) $product_id );
return $post && in_array( $post->post_type, [ 'product', 'product_variation' ], true );
}
/**
* Convert a WooCommerce cart item to an object suitable for the response.
*
* @todo Variation is stored to meta - how can we gather for response?
*
* @param \WC_Order_Item_Product $line_item Order line item array.
* @return array
*/
public function get_item_response( \WC_Order_Item_Product $line_item ) {
$product = $line_item->get_product();
$has_product = $product instanceof \WC_Product;
return [
'id' => $line_item->get_variation_id() ? $line_item->get_variation_id() : $line_item->get_product_id(),
'quantity' => $line_item->get_quantity(),
'name' => $has_product ? $product->get_title() : null,
'sku' => $has_product ? $product->get_sku() : null,
'permalink' => $has_product ? $product->get_permalink() : null,
'images' => $has_product ? ( new ProductImages() )->images_to_array( $product ) : null,
'variation' => $has_product ? $this->format_variation_data( $line_item, $product ) : [],
'totals' => array_merge(
$this->get_store_currency_response(),
[
'line_subtotal' => $this->prepare_money_response( $line_item->get_subtotal(), wc_get_price_decimals() ),
'line_subtotal_tax' => $this->prepare_money_response( $line_item->get_subtotal_tax(), wc_get_price_decimals() ),
'line_total' => $this->prepare_money_response( $line_item->get_total(), wc_get_price_decimals() ),
'line_total_tax' => $this->prepare_money_response( $line_item->get_total_tax(), wc_get_price_decimals() ),
]
),
];
}
/**
* Format variation data. For line items we get meta data and format it.
*
* @param \WC_Order_Item_Product $line_item Line item from the order.
* @param \WC_Product $product Product data.
* @return array
*/
protected function format_variation_data( \WC_Order_Item_Product $line_item, \WC_Product $product ) {
$return = [];
$line_item_meta = $line_item->get_meta_data();
$attribute_keys = array_keys( $product->get_attributes() );
foreach ( $line_item_meta as $meta ) {
$key = $meta->key;
$value = $meta->value;
if ( ! in_array( $key, $attribute_keys, true ) ) {
continue;
}
$taxonomy = wc_attribute_taxonomy_name( str_replace( 'pa_', '', urldecode( $key ) ) );
if ( taxonomy_exists( $taxonomy ) ) {
// If this is a term slug, get the term's nice name.
$term = get_term_by( 'slug', $value, $taxonomy );
if ( ! is_wp_error( $term ) && $term && $term->name ) {
$value = $term->name;
}
$label = wc_attribute_label( $taxonomy );
} else {
// If this is a custom option slug, get the options name.
$value = apply_filters( 'woocommerce_variation_option_name', $value, null, $taxonomy, $product );
$label = wc_attribute_label( $name, $product );
}
$return[ $label ] = $value;
}
return $return;
}
}

View File

@ -0,0 +1,625 @@
<?php
/**
* Order schema for the Store API.
*
* Note, only fields customers can edit via checkout are editable. Everything else is either readonly or hidden.
*
* @package WooCommerce/Blocks
*/
namespace Automattic\WooCommerce\Blocks\RestApi\StoreApi\Schemas;
defined( 'ABSPATH' ) || exit;
/**
* OrderSchema class.
*/
class OrderSchema extends AbstractSchema {
/**
* The schema item name.
*
* @var string
*/
protected $title = 'order';
/**
* Order schema properties.
*
* @return array
*/
protected function get_properties() {
return [
'id' => [
'description' => __( 'Unique identifier for the resource.', 'woo-gutenberg-products-block' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'number' => [
'description' => __( 'Generated order number which may differ from the Order ID.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'status' => [
'description' => __( 'Order status. Payment providers will update this value after payment.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'order_key' => [
'description' => __( 'Order key used to check validity or protect access to certain order data.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'created_via' => [
'description' => __( 'Shows where the order was created.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'prices_include_tax' => [
'description' => __( 'True if the prices included tax when the order was created.', 'woo-gutenberg-products-block' ),
'type' => 'boolean',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'events' => [
'description' => __( 'List of events and dates such as creation date.', 'woo-gutenberg-products-block' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => [
'date_created' => [
'description' => __( "The date the order was created, in the site's timezone.", 'woo-gutenberg-products-block' ),
'type' => 'date-time',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'date_created_gmt' => [
'description' => __( 'The date the order was created, as GMT.', 'woo-gutenberg-products-block' ),
'type' => 'date-time',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'date_modified' => [
'description' => __( "The date the order was last modified, in the site's timezone.", 'woo-gutenberg-products-block' ),
'type' => 'date-time',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'date_modified_gmt' => [
'description' => __( 'The date the order was last modified, as GMT.', 'woo-gutenberg-products-block' ),
'type' => 'date-time',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'date_paid' => [
'description' => __( "The date the order was paid, in the site's timezone.", 'woo-gutenberg-products-block' ),
'type' => 'date-time',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'date_paid_gmt' => [
'description' => __( 'The date the order was paid, as GMT.', 'woo-gutenberg-products-block' ),
'type' => 'date-time',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'date_completed' => [
'description' => __( "The date the order was completed, in the site's timezone.", 'woo-gutenberg-products-block' ),
'type' => 'date-time',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'date_completed_gmt' => [
'description' => __( 'The date the order was completed, as GMT.', 'woo-gutenberg-products-block' ),
'type' => 'date-time',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
'customer' => [
'description' => __( 'Information about the customer that placed the order.', 'woo-gutenberg-products-block' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => [
'customer_id' => [
'description' => __( 'Customer ID if registered. Will return 0 for guest orders.', 'woo-gutenberg-products-block' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'customer_ip_address' => [
'description' => __( 'Customer IP address.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'customer_user_agent' => [
'description' => __( 'Customer web browser identifier.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
'customer_note' => [
'description' => __( 'Note added to the order by the customer during checkout.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'billing_address' => [
'description' => __( 'Billing address.', 'woo-gutenberg-products-block' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'properties' => [
'first_name' => [
'description' => __( 'First name.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'last_name' => [
'description' => __( 'Last name.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'company' => [
'description' => __( 'Company name.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'address_1' => [
'description' => __( 'Address line 1', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'address_2' => [
'description' => __( 'Address line 2', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'city' => [
'description' => __( 'City name.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'state' => [
'description' => __( 'ISO code or name of the state, province or district.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'postcode' => [
'description' => __( 'Postal code.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'country' => [
'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'email' => [
'description' => __( 'Email address.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'format' => 'email',
'context' => [ 'view', 'edit' ],
],
'phone' => [
'description' => __( 'Phone number.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
],
],
'shipping_address' => [
'description' => __( 'Shipping address.', 'woo-gutenberg-products-block' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'properties' => [
'first_name' => [
'description' => __( 'First name.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'last_name' => [
'description' => __( 'Last name.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'company' => [
'description' => __( 'Company name.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'address_1' => [
'description' => __( 'Address line 1', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'address_2' => [
'description' => __( 'Address line 2', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'city' => [
'description' => __( 'City name.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'state' => [
'description' => __( 'ISO code or name of the state, province or district.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'postcode' => [
'description' => __( 'Postal code.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'country' => [
'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
],
],
'coupons' => [
'description' => __( 'List of applied cart coupons.', 'woo-gutenberg-products-block' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => $this->force_schema_readonly( ( new CartCouponSchema() )->get_properties() ),
],
],
'items' => [
'description' => __( 'List of cart items.', 'woo-gutenberg-products-block' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => $this->force_schema_readonly( ( new OrderItemSchema() )->get_properties() ),
],
],
'shipping_lines' => [
'description' => __( 'Shipping lines data.', 'woo-gutenberg-products-block' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => [
'id' => [
'description' => __( 'Item ID.', 'woo-gutenberg-products-block' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'method_title' => [
'description' => __( 'Shipping method name.', 'woo-gutenberg-products-block' ),
'type' => 'mixed',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'method_id' => [
'description' => __( 'Shipping method ID.', 'woo-gutenberg-products-block' ),
'type' => 'mixed',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'instance_id' => [
'description' => __( 'Shipping instance ID.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total' => [
'description' => __( 'Line total (after discounts).', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_tax' => [
'description' => __( 'Line total tax (after discounts).', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'taxes' => [
'description' => __( 'Line taxes.', 'woo-gutenberg-products-block' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => [
'id' => [
'description' => __( 'Tax rate ID.', 'woo-gutenberg-products-block' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total' => [
'description' => __( 'Tax total.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
],
],
],
],
'totals' => [
'description' => __( 'Total amounts provided using the smallest unit of the currency.', 'woo-gutenberg-products-block' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => array_merge(
$this->get_store_currency_properties(),
[
'total_items' => [
'description' => __( 'Total price of items in the order.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_items_tax' => [
'description' => __( 'Total tax on items in the order.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_fees' => [
'description' => __( 'Total price of any applied fees.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_fees_tax' => [
'description' => __( 'Total tax on fees.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_discount' => [
'description' => __( 'Total discount from applied coupons.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_discount_tax' => [
'description' => __( 'Total tax removed due to discount from applied coupons.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_shipping' => [
'description' => __( 'Total price of shipping.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_shipping_tax' => [
'description' => __( 'Total tax on shipping.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_price' => [
'description' => __( 'Total price the customer will pay.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_tax' => [
'description' => __( 'Total tax applied to items and shipping.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'tax_lines' => [
'description' => __( 'Lines of taxes applied to items and shipping.', 'woo-gutenberg-products-block' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => [
'name' => [
'description' => __( 'The name of the tax.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'price' => [
'description' => __( 'The amount of tax charged.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
],
]
),
],
];
}
/**
* Convert a woo order into an object suitable for the response.
*
* @param \WC_Order $order Order class instance.
* @return array
*/
public function get_item_response( \WC_Order $order ) {
$order_item_schema = new OrderItemSchema();
return [
'id' => $order->get_id(),
'number' => $order->get_order_number(),
'status' => $order->get_status(),
'order_key' => $order->get_order_key(),
'created_via' => $order->get_created_via(),
'prices_include_tax' => $order->get_prices_include_tax(),
'events' => $this->get_events( $order ),
'customer' => [
'customer_id' => $order->get_customer_id(),
'customer_ip_address' => $order->get_customer_ip_address(),
'customer_user_agent' => $order->get_customer_user_agent(),
],
'customer_note' => $order->get_customer_note(),
'billing_address' => [
'first_name' => $order->get_billing_first_name(),
'last_name' => $order->get_billing_last_name(),
'company' => $order->get_billing_company(),
'address_1' => $order->get_billing_address_1(),
'address_2' => $order->get_billing_address_2(),
'city' => $order->get_billing_city(),
'state' => $order->get_billing_state(),
'postcode' => $order->get_billing_postcode(),
'country' => $order->get_billing_country(),
'email' => $order->get_billing_email(),
'phone' => $order->get_billing_phone(),
],
'shipping_address' => [
'first_name' => $order->get_shipping_first_name(),
'last_name' => $order->get_shipping_last_name(),
'company' => $order->get_shipping_company(),
'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(),
],
'items' => array_values( array_map( [ $order_item_schema, 'get_item_response' ], $order->get_items( 'line_item' ) ) ),
'totals' => array_merge(
$this->get_store_currency_response(),
[
'total_items' => $this->prepare_money_response( $order->get_subtotal(), wc_get_price_decimals() ),
'total_items_tax' => $this->prepare_money_response( $this->get_subtotal_tax( $order ), wc_get_price_decimals() ),
'total_fees' => $this->prepare_money_response( $this->get_fee_total( $order ), wc_get_price_decimals() ),
'total_fees_tax' => $this->prepare_money_response( $this->get_fee_tax( $order ), wc_get_price_decimals() ),
'total_discount' => $this->prepare_money_response( $order->get_discount_total(), wc_get_price_decimals() ),
'total_discount_tax' => $this->prepare_money_response( $order->get_discount_tax(), wc_get_price_decimals() ),
'total_shipping' => $this->prepare_money_response( $order->get_shipping_total(), wc_get_price_decimals() ),
'total_shipping_tax' => $this->prepare_money_response( $order->get_shipping_tax(), wc_get_price_decimals() ),
'total_price' => $this->prepare_money_response( $order->get_total(), wc_get_price_decimals() ),
'total_tax' => $this->prepare_money_response( $order->get_total_tax(), wc_get_price_decimals() ),
'tax_lines' => $this->get_tax_lines( $order ),
]
),
];
}
/**
* Removes the wc- prefix from order statuses.
*
* @param string $status Status from the order.
* @return string
*/
protected function remove_status_prefix( $status ) {
return 'wc-' === substr( $status, 0, 3 ) ? substr( $status, 3 ) : $status;
}
/**
* Get event dates from an order, formatting both local and GMT values.
*
* @param \WC_Order $order Order class instance.
* @return array
*/
protected function get_events( \WC_Order $order ) {
$events = [];
$props = [ 'date_created', 'date_modified', 'date_completed', 'date_paid' ];
foreach ( $props as $prop ) {
$datetime = $order->{"get_$prop"}();
$events[ $prop ] = wc_rest_prepare_date_response( $datetime, false );
$events[ $prop . '_gmt' ] = wc_rest_prepare_date_response( $datetime );
}
return $events;
}
/**
* Get tax lines from the order and format to match schema.
*
* @param \WC_Order $order Order class instance.
* @return array
*/
protected function get_tax_lines( \WC_Order $order ) {
$tax_totals = $order->get_tax_totals();
$tax_lines = [];
foreach ( $tax_totals as $tax_total ) {
$tax_lines[] = array(
'name' => $tax_total->label,
'price' => $this->prepare_money_response( $tax_total->amount, wc_get_price_decimals() ),
);
}
return $tax_lines;
}
/**
* Get the total amount of tax for line items.
*
* Needed because orders do not hold this total like carts.
*
* @todo In the future this could be added to the core WC_Order class to better match the WC_Cart class.
*
* @param \WC_Order $order Order class instance.
* @return float
*/
protected function get_subtotal_tax( \WC_Order $order ) {
$total = 0;
foreach ( $order->get_items() as $item ) {
$total += $item->get_subtotal_tax();
}
return $total;
}
/**
* Get the total amount of fees.
*
* Needed because orders do not hold this total like carts.
*
* @todo In the future this could be added to the core WC_Order class to better match the WC_Cart class.
*
* @param \WC_Order $order Order class instance.
* @return float
*/
protected function get_fee_total( \WC_Order $order ) {
$total = 0;
foreach ( $order->get_fees() as $item ) {
$total += $item->get_total();
}
return $total;
}
/**
* Get the total tax of fees.
*
* Needed because orders do not hold this total like carts.
*
* @todo In the future this could be added to the core WC_Order class to better match the WC_Cart class.
*
* @param \WC_Order $order Order class instance.
* @return float
*/
protected function get_fee_tax( \WC_Order $order ) {
$total = 0;
foreach ( $order->get_fees() as $item ) {
$total += $item->get_total_tax();
}
return $total;
}
}

View File

@ -0,0 +1,140 @@
<?php
/**
* Helper class to handle product stock reservation.
*
* @package WooCommerce/Blocks
*/
namespace Automattic\WooCommerce\Blocks\RestApi\StoreApi\Utilities;
defined( 'ABSPATH' ) || exit;
use \WP_Error;
/**
* Stock Reservation class.
*/
class ReserveStock {
/**
* Put a temporary hold on stock for an order if enough is available.
*
* @param \WC_Order $order Order object.
* @return bool|WP_Error
*/
public function reserve_stock_for_order( \WC_Order $order ) {
$stock_to_reserve = [];
$items = array_filter(
$order->get_items(),
function( $item ) {
return $item->is_type( 'line_item' ) && $item->get_product() instanceof \WC_Product;
}
);
foreach ( $items as $item ) {
$product = $item->get_product();
if ( ! $product->is_in_stock() ) {
return new WP_Error(
'product_out_of_stock',
sprintf(
/* translators: %s: product name */
__( '%s is out of stock and cannot be purchased.', 'woo-gutenberg-products-block' ),
$product->get_name()
),
[ 'status' => 403 ]
);
}
// If stock management is off, no need to reserve any stock here.
if ( ! $product->managing_stock() || $product->backorders_allowed() ) {
continue;
}
$product_id = $product->get_stock_managed_by_id();
$stock_to_reserve[ $product_id ] = isset( $stock_to_reserve[ $product_id ] ) ? $stock_to_reserve[ $product_id ] : 0;
$reserved_stock = $this->get_reserved_stock( $product, $order->get_id() );
if ( ( $product->get_stock_quantity() - $reserved_stock - $stock_to_reserve[ $product_id ] ) < $item->get_quantity() ) {
return new WP_Error(
'product_not_enough_stock',
sprintf(
/* translators: %s: product name */
__( 'Not enough units of %s are available in stock to fulfil this order.', 'woo-gutenberg-products-block' ),
$product->get_name()
),
[ 'status' => 403 ]
);
}
// Queue for later DB insertion.
$stock_to_reserve[ $product_id ] += $item->get_quantity();
}
$this->reserve_stock( $stock_to_reserve, $order->get_id() );
return true;
}
/**
* Reserve stock by inserting rows into the DB.
*
* @param array $stock_to_reserve Array of Product ID => Qty pairs.
* @param integer $order_id Order ID for which to reserve stock.
*/
protected function reserve_stock( $stock_to_reserve, $order_id ) {
global $wpdb;
$stock_to_reserve = array_filter( $stock_to_reserve );
if ( ! $stock_to_reserve ) {
return;
}
$stock_to_reserve_rows = [];
foreach ( $stock_to_reserve as $product_id => $stock_quantity ) {
$stock_to_reserve_rows[] = '(' . esc_sql( $order_id ) . ',"' . esc_sql( $product_id ) . '","' . esc_sql( $stock_quantity ) . '")';
}
$values = implode( ',', $stock_to_reserve_rows );
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
$wpdb->query( "REPLACE INTO {$wpdb->wc_reserved_stock} ( order_id, product_id, stock_quantity ) VALUES {$values};" );
}
/**
* Query for any existing holds on stock for this item.
*
* - Can ignore reserved stock for a specific order.
* - Ignores stock for orders which are no longer drafts (assuming real stock reduction was performed).
* - Ignores stock reserved over 10 mins ago.
*
* @param \WC_Product $product Product to get reserved stock for.
* @param integer $exclude_order_id Optional order to exclude from the results.
* @return integer Amount of stock already reserved.
*/
public function get_reserved_stock( \WC_Product $product, $exclude_order_id = 0 ) {
global $wpdb;
$reserved_stock = $wpdb->get_var(
$wpdb->prepare(
"
SELECT SUM( stock_table.`stock_quantity` ) FROM $wpdb->wc_reserved_stock stock_table
LEFT JOIN $wpdb->posts posts ON stock_table.`order_id` = posts.ID
WHERE stock_table.`product_id` = %d
AND posts.post_status = 'wc-checkout-draft'
AND stock_table.`order_id` != %d
AND stock_table.`timestamp` > ( NOW() - INTERVAL 10 MINUTE )
",
$product->get_stock_managed_by_id(),
$exclude_order_id
)
);
// Deals with legacy stock reservation which the core Woo checkout performs.
$hold_stock_minutes = (int) get_option( 'woocommerce_hold_stock_minutes', 0 );
$reserved_stock += ( $hold_stock_minutes > 0 ) ? wc_get_held_stock_quantity( $product ) : 0;
return $reserved_stock;
}
}

View File

@ -0,0 +1,157 @@
<?php
/**
* Controller Tests.
*
* @package WooCommerce\Blocks\Tests
*/
namespace Automattic\WooCommerce\Blocks\Tests\RestApi\StoreApi\Controllers;
use \WP_REST_Request;
use \WC_REST_Unit_Test_Case as TestCase;
use \WC_Helper_Product as ProductHelper;
use \WC_Helper_Order as OrderHelper;
/**
* Cart Order Controller Tests.
*/
class CartOrder 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/order', $routes );
$request = new WP_REST_Request( 'GET', '/wc/store/cart/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/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 schema retrieval.
*/
public function test_get_item_schema() {
$controller = new \Automattic\WooCommerce\Blocks\RestApi\StoreApi\Controllers\CartOrder();
$schema = $controller->get_item_schema();
$this->assertArrayHasKey( 'id', $schema['properties'] );
$this->assertArrayHasKey( 'number', $schema['properties'] );
$this->assertArrayHasKey( 'status', $schema['properties'] );
$this->assertArrayHasKey( 'order_key', $schema['properties'] );
$this->assertArrayHasKey( 'created_via', $schema['properties'] );
$this->assertArrayHasKey( 'prices_include_tax', $schema['properties'] );
$this->assertArrayHasKey( 'events', $schema['properties'] );
$this->assertArrayHasKey( 'customer', $schema['properties'] );
$this->assertArrayHasKey( 'billing_address', $schema['properties'] );
$this->assertArrayHasKey( 'shipping_address', $schema['properties'] );
$this->assertArrayHasKey( 'customer_note', $schema['properties'] );
$this->assertArrayHasKey( 'items', $schema['properties'] );
$this->assertArrayHasKey( 'totals', $schema['properties'] );
}
/**
* Test conversion of cart item to rest response.
*/
public function test_prepare_item_for_response() {
$controller = new \Automattic\WooCommerce\Blocks\RestApi\StoreApi\Controllers\CartOrder();
$order = OrderHelper::create_order();
$response = $controller->prepare_item_for_response( $order, [] );
$this->assertArrayHasKey( 'id', $response->get_data() );
$this->assertArrayHasKey( 'number', $response->get_data() );
$this->assertArrayHasKey( 'status', $response->get_data() );
$this->assertArrayHasKey( 'order_key', $response->get_data() );
$this->assertArrayHasKey( 'created_via', $response->get_data() );
$this->assertArrayHasKey( 'prices_include_tax', $response->get_data() );
$this->assertArrayHasKey( 'events', $response->get_data() );
$this->assertArrayHasKey( 'customer', $response->get_data() );
$this->assertArrayHasKey( 'billing_address', $response->get_data() );
$this->assertArrayHasKey( 'shipping_address', $response->get_data() );
$this->assertArrayHasKey( 'customer_note', $response->get_data() );
$this->assertArrayHasKey( 'items', $response->get_data() );
$this->assertArrayHasKey( 'totals', $response->get_data() );
}
}

View File

@ -59,7 +59,7 @@ class CartShippingRates extends TestCase {
$this->assertArrayHasKey( 'destination', $data[0] ); $this->assertArrayHasKey( 'destination', $data[0] );
$this->assertArrayHasKey( 'items', $data[0] ); $this->assertArrayHasKey( 'items', $data[0] );
$this->assertArrayHasKey( 'shipping-rates', $data[0] ); $this->assertArrayHasKey( 'shipping_rates', $data[0] );
$this->assertEquals( null, $data[0]['destination']['address_1'] ); $this->assertEquals( null, $data[0]['destination']['address_1'] );
$this->assertEquals( null, $data[0]['destination']['address_2'] ); $this->assertEquals( null, $data[0]['destination']['address_2'] );
@ -73,7 +73,7 @@ class CartShippingRates extends TestCase {
* Test getting shipping. * Test getting shipping.
*/ */
public function test_get_items_missing_address() { public function test_get_items_missing_address() {
$request = new WP_REST_Request( 'GET', '/wc/store/cart/shipping-rates' ); $request = new WP_REST_Request( 'GET', '/wc/store/cart/shipping-rates' );
$response = $this->server->dispatch( $request ); $response = $this->server->dispatch( $request );
$this->assertEquals( 400, $response->get_status() ); $this->assertEquals( 400, $response->get_status() );
} }
@ -131,15 +131,15 @@ class CartShippingRates extends TestCase {
$this->assertArrayHasKey( 'destination', $schema['properties'] ); $this->assertArrayHasKey( 'destination', $schema['properties'] );
$this->assertArrayHasKey( 'items', $schema['properties'] ); $this->assertArrayHasKey( 'items', $schema['properties'] );
$this->assertArrayHasKey( 'shipping-rates', $schema['properties'] ); $this->assertArrayHasKey( 'shipping_rates', $schema['properties'] );
$this->assertArrayHasKey( 'name', $schema['properties']['shipping-rates']['items']['properties'] ); $this->assertArrayHasKey( 'name', $schema['properties']['shipping_rates']['items']['properties'] );
$this->assertArrayHasKey( 'description', $schema['properties']['shipping-rates']['items']['properties'] ); $this->assertArrayHasKey( 'description', $schema['properties']['shipping_rates']['items']['properties'] );
$this->assertArrayHasKey( 'delivery_time', $schema['properties']['shipping-rates']['items']['properties'] ); $this->assertArrayHasKey( 'delivery_time', $schema['properties']['shipping_rates']['items']['properties'] );
$this->assertArrayHasKey( 'price', $schema['properties']['shipping-rates']['items']['properties'] ); $this->assertArrayHasKey( 'price', $schema['properties']['shipping_rates']['items']['properties'] );
$this->assertArrayHasKey( 'rate_id', $schema['properties']['shipping-rates']['items']['properties'] ); $this->assertArrayHasKey( 'rate_id', $schema['properties']['shipping_rates']['items']['properties'] );
$this->assertArrayHasKey( 'instance_id', $schema['properties']['shipping-rates']['items']['properties'] ); $this->assertArrayHasKey( 'instance_id', $schema['properties']['shipping_rates']['items']['properties'] );
$this->assertArrayHasKey( 'method_id', $schema['properties']['shipping-rates']['items']['properties'] ); $this->assertArrayHasKey( 'method_id', $schema['properties']['shipping_rates']['items']['properties'] );
$this->assertArrayHasKey( 'meta_data', $schema['properties']['shipping-rates']['items']['properties'] ); $this->assertArrayHasKey( 'meta_data', $schema['properties']['shipping_rates']['items']['properties'] );
} }
/** /**
@ -152,7 +152,7 @@ class CartShippingRates extends TestCase {
$this->assertArrayHasKey( 'destination', $response->get_data() ); $this->assertArrayHasKey( 'destination', $response->get_data() );
$this->assertArrayHasKey( 'items', $response->get_data() ); $this->assertArrayHasKey( 'items', $response->get_data() );
$this->assertArrayHasKey( 'shipping-rates', $response->get_data() ); $this->assertArrayHasKey( 'shipping_rates', $response->get_data() );
} }
/** /**

View File

@ -32,8 +32,8 @@ class Customer extends TestCase {
$this->assertEquals( 200, $response->get_status() ); $this->assertEquals( 200, $response->get_status() );
$this->assertArrayHasKey( 'id', $data ); $this->assertArrayHasKey( 'id', $data );
$this->assertArrayHasKey( 'billing', $data ); $this->assertArrayHasKey( 'billing_address', $data );
$this->assertArrayHasKey( 'shipping', $data ); $this->assertArrayHasKey( 'shipping_address', $data );
} }
/** /**
@ -43,7 +43,7 @@ class Customer extends TestCase {
$request = new WP_REST_Request( 'POST', '/wc/store/customer' ); $request = new WP_REST_Request( 'POST', '/wc/store/customer' );
$request->set_body_params( $request->set_body_params(
[ [
'billing' => [ 'billing_address' => [
'address_1' => '123 South Street', 'address_1' => '123 South Street',
'address_2' => 'Apt 1', 'address_2' => 'Apt 1',
'city' => 'Philadelphia', 'city' => 'Philadelphia',
@ -57,18 +57,18 @@ class Customer extends TestCase {
$data = $response->get_data(); $data = $response->get_data();
$this->assertEquals( 200, $response->get_status() ); $this->assertEquals( 200, $response->get_status() );
$this->assertEquals( '123 South Street', $data['billing']['address_1'] ); $this->assertEquals( '123 South Street', $data['billing_address']['address_1'] );
$this->assertEquals( 'Apt 1', $data['billing']['address_2'] ); $this->assertEquals( 'Apt 1', $data['billing_address']['address_2'] );
$this->assertEquals( 'Philadelphia', $data['billing']['city'] ); $this->assertEquals( 'Philadelphia', $data['billing_address']['city'] );
$this->assertEquals( 'PA', $data['billing']['state'] ); $this->assertEquals( 'PA', $data['billing_address']['state'] );
$this->assertEquals( '19123', $data['billing']['postcode'] ); $this->assertEquals( '19123', $data['billing_address']['postcode'] );
$this->assertEquals( 'US', $data['billing']['country'] ); $this->assertEquals( 'US', $data['billing_address']['country'] );
// Invalid email. // Invalid email.
$request = new WP_REST_Request( 'POST', '/wc/store/customer' ); $request = new WP_REST_Request( 'POST', '/wc/store/customer' );
$request->set_body_params( $request->set_body_params(
[ [
'billing' => [ 'billing_address' => [
'email' => 'not-an-email', 'email' => 'not-an-email',
], ],
] ]
@ -87,8 +87,8 @@ class Customer extends TestCase {
$schema = $controller->get_item_schema(); $schema = $controller->get_item_schema();
$this->assertArrayHasKey( 'id', $schema['properties'] ); $this->assertArrayHasKey( 'id', $schema['properties'] );
$this->assertArrayHasKey( 'billing', $schema['properties'] ); $this->assertArrayHasKey( 'billing_address', $schema['properties'] );
$this->assertArrayHasKey( 'shipping', $schema['properties'] ); $this->assertArrayHasKey( 'shipping_address', $schema['properties'] );
} }
/** /**
@ -121,22 +121,22 @@ class Customer extends TestCase {
$response = $controller->prepare_item_for_response( $customer, [] ); $response = $controller->prepare_item_for_response( $customer, [] );
$data = $response->get_data(); $data = $response->get_data();
$this->assertEquals( 'Name', $data['billing']['first_name'] ); $this->assertEquals( 'Name', $data['billing_address']['first_name'] );
$this->assertEquals( 'Surname', $data['billing']['last_name'] ); $this->assertEquals( 'Surname', $data['billing_address']['last_name'] );
$this->assertEquals( '123 South Street', $data['billing']['address_1'] ); $this->assertEquals( '123 South Street', $data['billing_address']['address_1'] );
$this->assertEquals( 'Apt 1', $data['billing']['address_2'] ); $this->assertEquals( 'Apt 1', $data['billing_address']['address_2'] );
$this->assertEquals( 'Philadelphia', $data['billing']['city'] ); $this->assertEquals( 'Philadelphia', $data['billing_address']['city'] );
$this->assertEquals( 'PA', $data['billing']['state'] ); $this->assertEquals( 'PA', $data['billing_address']['state'] );
$this->assertEquals( '19123', $data['billing']['postcode'] ); $this->assertEquals( '19123', $data['billing_address']['postcode'] );
$this->assertEquals( 'US', $data['billing']['country'] ); $this->assertEquals( 'US', $data['billing_address']['country'] );
$this->assertEquals( 'Name', $data['shipping']['first_name'] ); $this->assertEquals( 'Name', $data['shipping_address']['first_name'] );
$this->assertEquals( 'Surname', $data['shipping']['last_name'] ); $this->assertEquals( 'Surname', $data['shipping_address']['last_name'] );
$this->assertEquals( '123 South Street', $data['shipping']['address_1'] ); $this->assertEquals( '123 South Street', $data['shipping_address']['address_1'] );
$this->assertEquals( 'Apt 1', $data['shipping']['address_2'] ); $this->assertEquals( 'Apt 1', $data['shipping_address']['address_2'] );
$this->assertEquals( 'Philadelphia', $data['shipping']['city'] ); $this->assertEquals( 'Philadelphia', $data['shipping_address']['city'] );
$this->assertEquals( 'PA', $data['shipping']['state'] ); $this->assertEquals( 'PA', $data['shipping_address']['state'] );
$this->assertEquals( '19123', $data['shipping']['postcode'] ); $this->assertEquals( '19123', $data['shipping_address']['postcode'] );
$this->assertEquals( 'US', $data['shipping']['country'] ); $this->assertEquals( 'US', $data['shipping_address']['country'] );
} }
} }

View File

@ -0,0 +1,72 @@
<?php
/**
* Utility Tests.
*
* @package WooCommerce\Blocks\Tests
*/
namespace Automattic\WooCommerce\Blocks\Tests\RestApi\StoreApi\Utilities;
use PHPUnit\Framework\TestCase;
use \WC_Helper_Order as OrderHelper;
use \WC_Helper_Product as ProductHelper;
use Automattic\WooCommerce\Blocks\RestApi\StoreApi\Utilities\ReserveStock;
/**
* ReserveStock Utility Tests.
*/
class ReserveStockTests extends TestCase {
/**
* Test that stock is reserved for draft orders.
*/
public function test_reserve_stock_for_order() {
$class = new ReserveStock();
$product = ProductHelper::create_simple_product();
$product->set_manage_stock( true );
$product->set_stock( 10 );
$product->save();
$order = OrderHelper::create_order( 1, $product ); // Note this adds 4 to the order.
$order->set_status( 'checkout-draft' );
$order->save();
$result = $class->reserve_stock_for_order( $order );
$this->assertTrue( $result );
$this->assertEquals( 4, $this->get_reserved_stock_by_product_id( $product->get_stock_managed_by_id() ) );
// Repeat.
$order = OrderHelper::create_order( 1, $product );
$order->set_status( 'checkout-draft' );
$order->save();
$result = $class->reserve_stock_for_order( $order );
$this->assertTrue( $result );
$this->assertEquals( 8, $this->get_reserved_stock_by_product_id( $product->get_stock_managed_by_id() ) );
// Repeat again - should not be enough stock for this.
$order = OrderHelper::create_order( 1, $product );
$order->set_status( 'checkout-draft' );
$order->save();
$result = $class->reserve_stock_for_order( $order );
$this->assertTrue( is_wp_error( $result ) );
}
/**
* Helper to get the count of reserved stock.
*
* @param integer $product_id
* @return integer
*/
protected function get_reserved_stock_by_product_id( $product_id ) {
global $wpdb;
return $wpdb->get_var(
$wpdb->prepare(
"SELECT SUM( stock_table.`stock_quantity` ) FROM $wpdb->wc_reserved_stock stock_table WHERE stock_table.`product_id` = %d",
$product_id
)
);
}
}