* Register order route

* Get order it

* Add order schema

* Check authorization for getting the order

* Add order data to the response

* Add order schema for the endpoint

* Move validation check to order controller

* Update the error codes

* Add order item schema

* Check if the order is associated with current user

* Update order schema to match cart schema

* Update order item schema to match cart item schema

* Add product item trait

* Update sold individually property

* Allow guests to pay for order

* Update wording for logged out customers

* Allow getting all orders from the endpoint

* Add inline explanation for pay_for_order capability

* Remove unused $user_id and $order variables

* Remove duplicate pay_for_order capability check from validate_order_key

* Update exception wording when missing order id or key, or user mismatch

* Ensure $order_key is not null to avoid fatal error when left blank

* Resolve linting errors in order route class

* Adjust order ID description

* Create an abstract item schema

* Remove unused properties

* Remove unused properties

* Add billing email validation

* Allow to use the order endpoint in dev build only

* Add order status property

* Fix coupon and fee handling

* Update documentation for feature flags

* Update typo in total fees tax

* Update typo in tax lines

* Add missing payment methods to cart response

---------

Co-authored-by: Mike Jolley <mike.jolley@me.com>
This commit is contained in:
Hsing-yu Flowers 2023-07-19 06:49:46 -07:00 committed by GitHub
parent 537c1070ce
commit 26f1a59378
19 changed files with 1231 additions and 421 deletions

View File

@ -42,6 +42,7 @@ The majority of our feature flagging is blocks, this is a list of them:
- Product Rating Stars ([PHP flag](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/src/BlockTypesController.php#L230) | [webpack flag](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/bin/webpack-entries.js#L68-L70)) - Product Rating Stars ([PHP flag](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/src/BlockTypesController.php#L230) | [webpack flag](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/bin/webpack-entries.js#L68-L70))
- Product Rating Counter ([PHP flag](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/src/BlockTypesController.php#L229) | [webpack flag](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/bin/webpack-entries.js#L71-L73)) - Product Rating Counter ([PHP flag](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/src/BlockTypesController.php#L229) | [webpack flag](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/bin/webpack-entries.js#L71-L73))
- ⚛️ Add to cart ([JS flag](https://github.com/woocommerce/woocommerce-blocks/blob/dfd2902bd8a247b5d048577db6753c5e901fc60f/assets/js/atomic/blocks/product-elements/add-to-cart/index.ts#L26-L29)). - ⚛️ Add to cart ([JS flag](https://github.com/woocommerce/woocommerce-blocks/blob/dfd2902bd8a247b5d048577db6753c5e901fc60f/assets/js/atomic/blocks/product-elements/add-to-cart/index.ts#L26-L29)).
- Order Route ([PHP flag](https://github.com/woocommerce/woocommerce-blocks/blob/b4a9dc9334f82c09f533b0f88c947b5c34e4e546/src/StoreApi/RoutesController.php#L65-L67))
## Features behind flags ## Features behind flags

View File

@ -0,0 +1,120 @@
<?php
namespace Automattic\WooCommerce\StoreApi\Routes\V1;
use Automattic\WooCommerce\StoreApi\SchemaController;
use Automattic\WooCommerce\StoreApi\Schemas\v1\AbstractSchema;
use Automattic\WooCommerce\StoreApi\Utilities\OrderController;
use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;
/**
* Order class.
*/
class Order extends AbstractRoute {
/**
* The route identifier.
*
* @var string
*/
const IDENTIFIER = 'order';
/**
* The schema item identifier.
*
* @var string
*/
const SCHEMA_TYPE = 'order';
/**
* Order controller class instance.
*
* @var OrderController
*/
protected $order_controller;
/**
* Constructor.
*
* @param SchemaController $schema_controller Schema Controller instance.
* @param AbstractSchema $schema Schema class for this route.
*/
public function __construct( SchemaController $schema_controller, AbstractSchema $schema ) {
parent::__construct( $schema_controller, $schema );
$this->order_controller = new OrderController();
}
/**
* Get the path of this REST route.
*
* @return string
*/
public function get_path() {
return '/order/(?P<id>[\d]+)';
}
/**
* Get method arguments for this REST route.
*
* @return array An array of endpoints.
*/
public function get_args() {
return [
[
'methods' => \WP_REST_Server::READABLE,
'callback' => [ $this, 'get_response' ],
'permission_callback' => [ $this, 'is_authorized' ],
'args' => [
'context' => $this->get_context_param( [ 'default' => 'view' ] ),
],
],
'schema' => [ $this->schema, 'get_public_item_schema' ],
];
}
/**
* Check if authorized to get the order.
*
* @throws RouteException If the order is not found or the order key is invalid.
*
* @param \WP_REST_Request $request Request object.
* @return boolean|WP_Error
*/
public function is_authorized( \WP_REST_Request $request ) {
$order_id = absint( $request['id'] );
$order_key = wc_clean( wp_unslash( $request->get_param( 'key' ) ) );
$billing_email = wc_clean( wp_unslash( $request->get_param( 'billing_email' ) ) );
try {
// In this context, pay_for_order capability checks that the current user ID matches the customer ID stored
// within the order, or if the order was placed by a guest.
// See https://github.com/woocommerce/woocommerce/blob/abcedbefe02f9e89122771100c42ff588da3e8e0/plugins/woocommerce/includes/wc-user-functions.php#L458.
if ( ! current_user_can( 'pay_for_order', $order_id ) ) {
throw new RouteException( 'woocommerce_rest_invalid_user', __( 'This order belongs to a different customer. Please log in to the correct account.', 'woo-gutenberg-products-block' ), 403 );
}
if ( get_current_user_id() === 0 ) {
$this->order_controller->validate_order_key( $order_id, $order_key );
$this->order_controller->validate_billing_email( $order_id, $billing_email );
}
} catch ( RouteException $error ) {
return new \WP_Error(
$error->getErrorCode(),
$error->getMessage(),
array( 'status' => $error->getCode() )
);
}
return true;
}
/**
* Handle the request and return a valid response for this endpoint.
*
* @param \WP_REST_Request $request Request object.
* @return \WP_REST_Response
*/
protected function get_route_response( \WP_REST_Request $request ) {
$order_id = absint( $request['id'] );
return rest_ensure_response( $this->schema->get_item_response( $this->order_controller->get_order( $order_id ) ) );
}
}

View File

@ -1,6 +1,7 @@
<?php <?php
namespace Automattic\WooCommerce\StoreApi; namespace Automattic\WooCommerce\StoreApi;
use Automattic\WooCommerce\Blocks\Package;
use Automattic\WooCommerce\StoreApi\SchemaController; use Automattic\WooCommerce\StoreApi\SchemaController;
use Exception; use Exception;
use Routes\AbstractRoute; use Routes\AbstractRoute;
@ -60,6 +61,10 @@ class RoutesController {
Routes\V1\ProductsBySlug::IDENTIFIER => Routes\V1\ProductsBySlug::class, Routes\V1\ProductsBySlug::IDENTIFIER => Routes\V1\ProductsBySlug::class,
], ],
]; ];
if ( Package::is_experimental_build() ) {
$this->routes['v1'][ Routes\V1\Order::IDENTIFIER ] = Routes\V1\Order::class;
}
} }
/** /**

View File

@ -44,6 +44,10 @@ class SchemaController {
Schemas\V1\CartSchema::IDENTIFIER => Schemas\V1\CartSchema::class, Schemas\V1\CartSchema::IDENTIFIER => Schemas\V1\CartSchema::class,
Schemas\V1\CartExtensionsSchema::IDENTIFIER => Schemas\V1\CartExtensionsSchema::class, Schemas\V1\CartExtensionsSchema::IDENTIFIER => Schemas\V1\CartExtensionsSchema::class,
Schemas\V1\CheckoutSchema::IDENTIFIER => Schemas\V1\CheckoutSchema::class, Schemas\V1\CheckoutSchema::IDENTIFIER => Schemas\V1\CheckoutSchema::class,
Schemas\V1\OrderItemSchema::IDENTIFIER => Schemas\V1\OrderItemSchema::class,
Schemas\V1\OrderCouponSchema::IDENTIFIER => Schemas\V1\OrderCouponSchema::class,
Schemas\V1\OrderFeeSchema::IDENTIFIER => Schemas\V1\OrderFeeSchema::class,
Schemas\V1\OrderSchema::IDENTIFIER => Schemas\V1\OrderSchema::class,
Schemas\V1\ProductSchema::IDENTIFIER => Schemas\V1\ProductSchema::class, Schemas\V1\ProductSchema::IDENTIFIER => Schemas\V1\ProductSchema::class,
Schemas\V1\ProductAttributeSchema::IDENTIFIER => Schemas\V1\ProductAttributeSchema::class, Schemas\V1\ProductAttributeSchema::IDENTIFIER => Schemas\V1\ProductAttributeSchema::class,
Schemas\V1\ProductCategorySchema::IDENTIFIER => Schemas\V1\ProductCategorySchema::class, Schemas\V1\ProductCategorySchema::IDENTIFIER => Schemas\V1\ProductCategorySchema::class,

View File

@ -63,6 +63,16 @@ abstract class AbstractSchema {
); );
} }
/**
* Returns the full item response.
*
* @param mixed $item Item to get response for.
* @return array|stdClass
*/
public function get_item_response( $item ) {
return [];
}
/** /**
* Return schema properties. * Return schema properties.
* *

View File

@ -87,7 +87,7 @@ class BillingAddressSchema extends AbstractAddressSchema {
* @param \WC_Order|\WC_Customer $address An object with billing address. * @param \WC_Order|\WC_Customer $address An object with billing address.
* *
* @throws RouteException When the invalid object types are provided. * @throws RouteException When the invalid object types are provided.
* @return stdClass * @return array
*/ */
public function get_item_response( $address ) { public function get_item_response( $address ) {
$validation_util = new ValidationUtils(); $validation_util = new ValidationUtils();
@ -99,7 +99,7 @@ class BillingAddressSchema extends AbstractAddressSchema {
$billing_state = ''; $billing_state = '';
} }
return (object) $this->prepare_html_response( return $this->prepare_html_response(
[ [
'first_name' => $address->get_billing_first_name(), 'first_name' => $address->get_billing_first_name(),
'last_name' => $address->get_billing_last_name(), 'last_name' => $address->get_billing_last_name(),

View File

@ -1,14 +1,14 @@
<?php <?php
namespace Automattic\WooCommerce\StoreApi\Schemas\V1; namespace Automattic\WooCommerce\StoreApi\Schemas\V1;
use Automattic\WooCommerce\StoreApi\Utilities\DraftOrderTrait; use Automattic\WooCommerce\StoreApi\Utilities\ProductItemTrait;
use Automattic\WooCommerce\StoreApi\Utilities\QuantityLimits; use Automattic\WooCommerce\StoreApi\Utilities\QuantityLimits;
/** /**
* CartItemSchema class. * CartItemSchema class.
*/ */
class CartItemSchema extends ProductSchema { class CartItemSchema extends ItemSchema {
use DraftOrderTrait; use ProductItemTrait;
/** /**
* The schema item name. * The schema item name.
@ -24,308 +24,6 @@ class CartItemSchema extends ProductSchema {
*/ */
const IDENTIFIER = 'cart-item'; const IDENTIFIER = 'cart-item';
/**
* Cart schema properties.
*
* @return array
*/
public function get_properties() {
return [
'key' => [
'description' => __( 'Unique identifier for the item within the cart.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'id' => [
'description' => __( 'The cart item product or variation ID.', 'woo-gutenberg-products-block' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'quantity' => [
'description' => __( 'Quantity of this item in the cart.', 'woo-gutenberg-products-block' ),
'type' => 'number',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'quantity_limits' => [
'description' => __( 'How the quantity of this item should be controlled, for example, any limits in place.', 'woo-gutenberg-products-block' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => [
'minimum' => [
'description' => __( 'The minimum quantity allowed in the cart for this line item.', 'woo-gutenberg-products-block' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'maximum' => [
'description' => __( 'The maximum quantity allowed in the cart for this line item.', 'woo-gutenberg-products-block' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'multiple_of' => [
'description' => __( 'The amount that quantities increment by. Quantity must be an multiple of this value.', 'woo-gutenberg-products-block' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'default' => 1,
],
'editable' => [
'description' => __( 'If the quantity in the cart is editable or fixed.', 'woo-gutenberg-products-block' ),
'type' => 'boolean',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'default' => true,
],
],
],
'name' => [
'description' => __( 'Product name.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'short_description' => [
'description' => __( 'Product short description in HTML format.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'description' => [
'description' => __( 'Product full description in HTML format.', '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,
],
'low_stock_remaining' => [
'description' => __( 'Quantity left in stock if stock is low, or null if not applicable.', 'woo-gutenberg-products-block' ),
'type' => [ 'integer', 'null' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'backorders_allowed' => [
'description' => __( 'True if backorders are allowed past stock availability.', 'woo-gutenberg-products-block' ),
'type' => [ 'boolean' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'show_backorder_badge' => [
'description' => __( 'True if the product is on backorder.', 'woo-gutenberg-products-block' ),
'type' => [ 'boolean' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'sold_individually' => [
'description' => __( 'If true, only one item of this product is allowed for purchase in a single order.', 'woo-gutenberg-products-block' ),
'type' => 'boolean',
'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' => $this->image_attachment_schema->get_properties(),
],
],
'variation' => [
'description' => __( 'Chosen attributes (for variations).', 'woo-gutenberg-products-block' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => [
'attribute' => [
'description' => __( 'Variation attribute name.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'value' => [
'description' => __( 'Variation attribute value.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
],
'item_data' => [
'description' => __( 'Metadata related to the cart item', 'woo-gutenberg-products-block' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => [
'name' => [
'description' => __( 'Name of the metadata.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'value' => [
'description' => __( 'Value of the metadata.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'display' => [
'description' => __( 'Optionally, how the metadata value should be displayed to the user.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
],
'prices' => [
'description' => __( 'Price data for the product in the current line item, including or excluding taxes based on the "display prices during cart and checkout" setting. 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(),
[
'price' => [
'description' => __( 'Current product price.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'regular_price' => [
'description' => __( 'Regular product price.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'sale_price' => [
'description' => __( 'Sale product price, if applicable.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'price_range' => [
'description' => __( 'Price range, if applicable.', 'woo-gutenberg-products-block' ),
'type' => [ 'object', 'null' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => [
'min_amount' => [
'description' => __( 'Price amount.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'max_amount' => [
'description' => __( 'Price amount.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
'raw_prices' => [
'description' => __( 'Raw unrounded product prices used in calculations. Provided using a higher unit of precision than the currency.', 'woo-gutenberg-products-block' ),
'type' => [ 'object', 'null' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => [
'precision' => [
'description' => __( 'Decimal precision of the returned prices.', 'woo-gutenberg-products-block' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'price' => [
'description' => __( 'Current product price.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'regular_price' => [
'description' => __( 'Regular product price.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'sale_price' => [
'description' => __( 'Sale product price, if applicable.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
]
),
],
'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 subtotal (the price of the product before coupon discounts have been applied).', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'line_subtotal_tax' => [
'description' => __( 'Line subtotal tax.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'line_total' => [
'description' => __( 'Line total (the price of the product after coupon discounts have been applied).', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'line_total_tax' => [
'description' => __( 'Line total tax.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
]
),
],
'catalog_visibility' => [
'description' => __( 'Whether the product is visible in the catalog', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
self::EXTENDING_KEY => $this->get_extended_schema( self::IDENTIFIER ),
];
}
/** /**
* Convert a WooCommerce cart item to an object suitable for the response. * Convert a WooCommerce cart item to an object suitable for the response.
* *
@ -380,82 +78,6 @@ class CartItemSchema extends ProductSchema {
]; ];
} }
/**
* Get an array of pricing data.
*
* @param \WC_Product $product Product instance.
* @param string $tax_display_mode If returned prices are incl or excl of tax.
* @return array
*/
protected function prepare_product_price_response( \WC_Product $product, $tax_display_mode = '' ) {
$tax_display_mode = $this->get_tax_display_mode( $tax_display_mode );
$price_function = $this->get_price_function_from_tax_display_mode( $tax_display_mode );
$prices = parent::prepare_product_price_response( $product, $tax_display_mode );
// Add raw prices (prices with greater precision).
$prices['raw_prices'] = [
'precision' => wc_get_rounding_precision(),
'price' => $this->prepare_money_response( $price_function( $product ), wc_get_rounding_precision() ),
'regular_price' => $this->prepare_money_response( $price_function( $product, [ 'price' => $product->get_regular_price() ] ), wc_get_rounding_precision() ),
'sale_price' => $this->prepare_money_response( $price_function( $product, [ 'price' => $product->get_sale_price() ] ), wc_get_rounding_precision() ),
];
return $prices;
}
/**
* Format variation data, for example convert slugs such as attribute_pa_size to Size.
*
* @param array $variation_data Array of data from the cart.
* @param \WC_Product $product Product data.
* @return array
*/
protected function format_variation_data( $variation_data, $product ) {
$return = [];
if ( ! is_iterable( $variation_data ) ) {
return $return;
}
foreach ( $variation_data as $key => $value ) {
$taxonomy = wc_attribute_taxonomy_name( str_replace( 'attribute_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 {
/**
* Filters the variation option name.
*
* Filters the variation option name for custom option slugs.
*
* @since 2.5.0
*
* @internal Matches filter name in WooCommerce core.
*
* @param string $value The name to display.
* @param null $unused Unused because this is not a variation taxonomy.
* @param string $taxonomy Taxonomy or product attribute name.
* @param \WC_Product $product Product data.
* @return string
*/
$value = apply_filters( 'woocommerce_variation_option_name', $value, null, $taxonomy, $product );
$label = wc_attribute_label( str_replace( 'attribute_', '', $key ), $product );
}
$return[] = [
'attribute' => $this->prepare_html_response( $label ),
'value' => $this->prepare_html_response( $value ),
];
}
return $return;
}
/** /**
* Format cart item data removing any HTML tag. * Format cart item data removing any HTML tag.
* *

View File

@ -349,42 +349,51 @@ class CartSchema extends AbstractSchema {
$cross_sells = array_filter( array_map( 'wc_get_product', $cart->get_cross_sells() ), 'wc_products_array_filter_visible' ); $cross_sells = array_filter( array_map( 'wc_get_product', $cart->get_cross_sells() ), 'wc_products_array_filter_visible' );
return [ return [
'coupons' => $this->get_item_responses_from_schema( $this->coupon_schema, $cart->get_applied_coupons() ),
'shipping_rates' => $this->get_item_responses_from_schema( $this->shipping_rate_schema, $shipping_packages ),
'shipping_address' => $this->shipping_address_schema->get_item_response( wc()->customer ),
'billing_address' => $this->billing_address_schema->get_item_response( wc()->customer ),
'items' => $this->get_item_responses_from_schema( $this->item_schema, $cart->get_cart() ), 'items' => $this->get_item_responses_from_schema( $this->item_schema, $cart->get_cart() ),
'coupons' => $this->get_item_responses_from_schema( $this->coupon_schema, $cart->get_applied_coupons() ),
'fees' => $this->get_item_responses_from_schema( $this->fee_schema, $cart->get_fees() ),
'totals' => (object) $this->prepare_currency_response( $this->get_totals( $cart ) ),
'shipping_address' => (object) $this->shipping_address_schema->get_item_response( wc()->customer ),
'billing_address' => (object) $this->billing_address_schema->get_item_response( wc()->customer ),
'needs_payment' => $cart->needs_payment(),
'needs_shipping' => $cart->needs_shipping(),
'payment_requirements' => $this->extend->get_payment_requirements(),
'has_calculated_shipping' => $has_calculated_shipping,
'shipping_rates' => $this->get_item_responses_from_schema( $this->shipping_rate_schema, $shipping_packages ),
'items_count' => $cart->get_cart_contents_count(), 'items_count' => $cart->get_cart_contents_count(),
'items_weight' => wc_get_weight( $cart->get_cart_contents_weight(), 'g' ), 'items_weight' => wc_get_weight( $cart->get_cart_contents_weight(), 'g' ),
'cross_sells' => $this->get_item_responses_from_schema( $this->cross_sells_item_schema, $cross_sells ), 'cross_sells' => $this->get_item_responses_from_schema( $this->cross_sells_item_schema, $cross_sells ),
'needs_payment' => $cart->needs_payment(),
'needs_shipping' => $cart->needs_shipping(),
'has_calculated_shipping' => $has_calculated_shipping,
'fees' => $this->get_item_responses_from_schema( $this->fee_schema, $cart->get_fees() ),
'totals' => (object) $this->prepare_currency_response(
[
'total_items' => $this->prepare_money_response( $cart->get_subtotal(), wc_get_price_decimals() ),
'total_items_tax' => $this->prepare_money_response( $cart->get_subtotal_tax(), wc_get_price_decimals() ),
'total_fees' => $this->prepare_money_response( $cart->get_fee_total(), wc_get_price_decimals() ),
'total_fees_tax' => $this->prepare_money_response( $cart->get_fee_tax(), wc_get_price_decimals() ),
'total_discount' => $this->prepare_money_response( $cart->get_discount_total(), wc_get_price_decimals() ),
'total_discount_tax' => $this->prepare_money_response( $cart->get_discount_tax(), wc_get_price_decimals() ),
'total_shipping' => $has_calculated_shipping ? $this->prepare_money_response( $cart->get_shipping_total(), wc_get_price_decimals() ) : null,
'total_shipping_tax' => $has_calculated_shipping ? $this->prepare_money_response( $cart->get_shipping_tax(), wc_get_price_decimals() ) : null,
// Explicitly request context='edit'; default ('view') will render total as markup.
'total_price' => $this->prepare_money_response( $cart->get_total( 'edit' ), wc_get_price_decimals() ),
'total_tax' => $this->prepare_money_response( $cart->get_total_tax(), wc_get_price_decimals() ),
'tax_lines' => $this->get_tax_lines( $cart ),
]
),
'errors' => $cart_errors, 'errors' => $cart_errors,
'payment_methods' => array_values( wp_list_pluck( WC()->payment_gateways->get_available_payment_gateways(), 'id' ) ), 'payment_methods' => array_values( wp_list_pluck( WC()->payment_gateways->get_available_payment_gateways(), 'id' ) ),
'payment_requirements' => $this->extend->get_payment_requirements(),
self::EXTENDING_KEY => $this->get_extended_data( self::IDENTIFIER ), self::EXTENDING_KEY => $this->get_extended_data( self::IDENTIFIER ),
]; ];
} }
/**
* Get total data.
*
* @param \WC_Cart $cart Cart class instance.
* @return array
*/
protected function get_totals( $cart ) {
$has_calculated_shipping = $cart->show_shipping();
return [
'total_items' => $this->prepare_money_response( $cart->get_subtotal(), wc_get_price_decimals() ),
'total_items_tax' => $this->prepare_money_response( $cart->get_subtotal_tax(), wc_get_price_decimals() ),
'total_fees' => $this->prepare_money_response( $cart->get_fee_total(), wc_get_price_decimals() ),
'total_fees_tax' => $this->prepare_money_response( $cart->get_fee_tax(), wc_get_price_decimals() ),
'total_discount' => $this->prepare_money_response( $cart->get_discount_total(), wc_get_price_decimals() ),
'total_discount_tax' => $this->prepare_money_response( $cart->get_discount_tax(), wc_get_price_decimals() ),
'total_shipping' => $has_calculated_shipping ? $this->prepare_money_response( $cart->get_shipping_total(), wc_get_price_decimals() ) : null,
'total_shipping_tax' => $has_calculated_shipping ? $this->prepare_money_response( $cart->get_shipping_tax(), wc_get_price_decimals() ) : null,
// Explicitly request context='edit'; default ('view') will render total as markup.
'total_price' => $this->prepare_money_response( $cart->get_total( 'edit' ), wc_get_price_decimals() ),
'total_tax' => $this->prepare_money_response( $cart->get_total_tax(), wc_get_price_decimals() ),
'tax_lines' => $this->get_tax_lines( $cart ),
];
}
/** /**
* Get tax lines from the cart and format to match schema. * Get tax lines from the cart and format to match schema.
* *

View File

@ -197,8 +197,8 @@ class CheckoutSchema extends AbstractSchema {
'order_number' => $order->get_order_number(), 'order_number' => $order->get_order_number(),
'customer_note' => $order->get_customer_note(), 'customer_note' => $order->get_customer_note(),
'customer_id' => $order->get_customer_id(), 'customer_id' => $order->get_customer_id(),
'billing_address' => $this->billing_address_schema->get_item_response( $order ), 'billing_address' => (object) $this->billing_address_schema->get_item_response( $order ),
'shipping_address' => $this->shipping_address_schema->get_item_response( $order ), 'shipping_address' => (object) $this->shipping_address_schema->get_item_response( $order ),
'payment_method' => $order->get_payment_method(), 'payment_method' => $order->get_payment_method(),
'payment_result' => [ 'payment_result' => [
'payment_status' => $payment_result->status, 'payment_status' => $payment_result->status,

View File

@ -47,7 +47,7 @@ class ErrorSchema extends AbstractSchema {
* @param \WP_Error $error Error object. * @param \WP_Error $error Error object.
* @return array * @return array
*/ */
public function get_item_response( \WP_Error $error ) { public function get_item_response( $error ) {
return [ return [
'code' => $this->prepare_html_response( $error->get_error_code() ), 'code' => $this->prepare_html_response( $error->get_error_code() ),
'message' => $this->prepare_html_response( $error->get_error_message() ), 'message' => $this->prepare_html_response( $error->get_error_message() ),

View File

@ -0,0 +1,310 @@
<?php
namespace Automattic\WooCommerce\StoreApi\Schemas\V1;
/**
* ItemSchema class.
*/
abstract class ItemSchema extends ProductSchema {
/**
* Item schema properties.
*
* @return array
*/
public function get_properties() {
return [
'key' => [
'description' => __( 'Unique identifier for the item.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'id' => [
'description' => __( 'The item product or variation ID.', 'woo-gutenberg-products-block' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'quantity' => [
'description' => __( 'Quantity of this item.', 'woo-gutenberg-products-block' ),
'type' => 'number',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'quantity_limits' => [
'description' => __( 'How the quantity of this item should be controlled, for example, any limits in place.', 'woo-gutenberg-products-block' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => [
'minimum' => [
'description' => __( 'The minimum quantity allowed for this line item.', 'woo-gutenberg-products-block' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'maximum' => [
'description' => __( 'The maximum quantity allowed for this line item.', 'woo-gutenberg-products-block' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'multiple_of' => [
'description' => __( 'The amount that quantities increment by. Quantity must be an multiple of this value.', 'woo-gutenberg-products-block' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'default' => 1,
],
'editable' => [
'description' => __( 'If the quantity is editable or fixed.', 'woo-gutenberg-products-block' ),
'type' => 'boolean',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'default' => true,
],
],
],
'name' => [
'description' => __( 'Product name.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'short_description' => [
'description' => __( 'Product short description in HTML format.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'description' => [
'description' => __( 'Product full description in HTML format.', '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,
],
'low_stock_remaining' => [
'description' => __( 'Quantity left in stock if stock is low, or null if not applicable.', 'woo-gutenberg-products-block' ),
'type' => [ 'integer', 'null' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'backorders_allowed' => [
'description' => __( 'True if backorders are allowed past stock availability.', 'woo-gutenberg-products-block' ),
'type' => [ 'boolean' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'show_backorder_badge' => [
'description' => __( 'True if the product is on backorder.', 'woo-gutenberg-products-block' ),
'type' => [ 'boolean' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'sold_individually' => [
'description' => __( 'If true, only one item of this product is allowed for purchase in a single order.', 'woo-gutenberg-products-block' ),
'type' => 'boolean',
'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' => $this->image_attachment_schema->get_properties(),
],
],
'variation' => [
'description' => __( 'Chosen attributes (for variations).', 'woo-gutenberg-products-block' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => [
'attribute' => [
'description' => __( 'Variation attribute name.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'value' => [
'description' => __( 'Variation attribute value.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
],
'item_data' => [
'description' => __( 'Metadata related to the item', 'woo-gutenberg-products-block' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => [
'name' => [
'description' => __( 'Name of the metadata.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'value' => [
'description' => __( 'Value of the metadata.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'display' => [
'description' => __( 'Optionally, how the metadata value should be displayed to the user.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
],
'prices' => [
'description' => __( 'Price data for the product in the current line item, including or excluding taxes based on the "display prices during cart and checkout" setting. 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(),
[
'price' => [
'description' => __( 'Current product price.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'regular_price' => [
'description' => __( 'Regular product price.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'sale_price' => [
'description' => __( 'Sale product price, if applicable.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'price_range' => [
'description' => __( 'Price range, if applicable.', 'woo-gutenberg-products-block' ),
'type' => [ 'object', 'null' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => [
'min_amount' => [
'description' => __( 'Price amount.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'max_amount' => [
'description' => __( 'Price amount.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
'raw_prices' => [
'description' => __( 'Raw unrounded product prices used in calculations. Provided using a higher unit of precision than the currency.', 'woo-gutenberg-products-block' ),
'type' => [ 'object', 'null' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => [
'precision' => [
'description' => __( 'Decimal precision of the returned prices.', 'woo-gutenberg-products-block' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'price' => [
'description' => __( 'Current product price.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'regular_price' => [
'description' => __( 'Regular product price.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'sale_price' => [
'description' => __( 'Sale product price, if applicable.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
]
),
],
'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 subtotal (the price of the product before coupon discounts have been applied).', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'line_subtotal_tax' => [
'description' => __( 'Line subtotal tax.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'line_total' => [
'description' => __( 'Line total (the price of the product after coupon discounts have been applied).', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'line_total_tax' => [
'description' => __( 'Line total tax.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
]
),
],
'catalog_visibility' => [
'description' => __( 'Whether the product is visible in the catalog', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
self::EXTENDING_KEY => $this->get_extended_schema( self::IDENTIFIER ),
];
}
}

View File

@ -26,13 +26,19 @@ class OrderCouponSchema extends AbstractSchema {
*/ */
public function get_properties() { public function get_properties() {
return [ return [
'code' => [ 'code' => [
'description' => __( 'The coupons unique code.', 'woo-gutenberg-products-block' ), 'description' => __( 'The coupons unique code.', 'woo-gutenberg-products-block' ),
'type' => 'string', 'type' => 'string',
'context' => [ 'view', 'edit' ], 'context' => [ 'view', 'edit' ],
'readonly' => true, 'readonly' => true,
], ],
'totals' => [ 'discount_type' => [
'description' => __( 'The discount type for the coupon (e.g. percentage or fixed amount)', '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' ), 'description' => __( 'Total amounts provided using the smallest unit of the currency.', 'woo-gutenberg-products-block' ),
'type' => 'object', 'type' => 'object',
'context' => [ 'view', 'edit' ], 'context' => [ 'view', 'edit' ],
@ -61,13 +67,15 @@ class OrderCouponSchema extends AbstractSchema {
/** /**
* Convert an order coupon to an object suitable for the response. * Convert an order coupon to an object suitable for the response.
* *
* @param \WC_Order_Item_Coupon $coupon Order coupon array. * @param \WC_Order_Item_Coupon $coupon Order coupon object.
* @return array * @return array
*/ */
public function get_item_response( \WC_Order_Item_Coupon $coupon ) { public function get_item_response( $coupon ) {
$coupon_object = new \WC_Coupon( $coupon->get_code() );
return [ return [
'code' => $coupon->get_code(), 'code' => $coupon->get_code(),
'totals' => (object) $this->prepare_currency_response( 'discount_type' => $coupon_object ? $coupon_object->get_discount_type() : '',
'totals' => (object) $this->prepare_currency_response(
[ [
'total_discount' => $this->prepare_money_response( $coupon->get_discount(), wc_get_price_decimals() ), 'total_discount' => $this->prepare_money_response( $coupon->get_discount(), wc_get_price_decimals() ),
'total_discount_tax' => $this->prepare_money_response( $coupon->get_discount_tax(), wc_get_price_decimals(), PHP_ROUND_HALF_DOWN ), 'total_discount_tax' => $this->prepare_money_response( $coupon->get_discount_tax(), wc_get_price_decimals(), PHP_ROUND_HALF_DOWN ),

View File

@ -0,0 +1,88 @@
<?php
namespace Automattic\WooCommerce\StoreApi\Schemas\V1;
/**
* OrderFeeSchema class.
*/
class OrderFeeSchema extends AbstractSchema {
/**
* The schema item name.
*
* @var string
*/
protected $title = 'order_fee';
/**
* The schema item identifier.
*
* @var string
*/
const IDENTIFIER = 'order-fee';
/**
* Cart schema properties.
*
* @return array
*/
public function get_properties() {
return [
'id' => [
'description' => __( 'Unique identifier for the fee within the cart', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'name' => [
'description' => __( 'Fee name', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'totals' => [
'description' => __( 'Fee 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' => [
'description' => __( 'Total amount for this fee.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_tax' => [
'description' => __( 'Total tax amount for this fee.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
]
),
],
];
}
/**
* Convert a WooCommerce cart fee to an object suitable for the response.
*
* @param \WC_Order_Item_Fee $fee Order fee object.
* @return array
*/
public function get_item_response( $fee ) {
if ( ! $fee ) {
return [];
}
return [
'key' => $fee->get_id(),
'name' => $this->prepare_html_response( $fee->get_name() ),
'totals' => (object) $this->prepare_currency_response(
[
'total' => $this->prepare_money_response( $fee->get_total(), wc_get_price_decimals() ),
'total_tax' => $this->prepare_money_response( $fee->get_total_tax(), wc_get_price_decimals(), PHP_ROUND_HALF_DOWN ),
]
),
];
}
}

View File

@ -0,0 +1,78 @@
<?php
namespace Automattic\WooCommerce\StoreApi\Schemas\V1;
use Automattic\WooCommerce\StoreApi\Utilities\ProductItemTrait;
/**
* OrderItemSchema class.
*/
class OrderItemSchema extends ItemSchema {
use ProductItemTrait;
/**
* The schema item name.
*
* @var string
*/
protected $title = 'order_item';
/**
* The schema item identifier.
*
* @var string
*/
const IDENTIFIER = 'order-item';
/**
* Get order items data.
*
* @param \WC_Order_Item_Product $order_item Order item instance.
* @return array
*/
public function get_item_response( $order_item ) {
$order = $order_item->get_order();
$product = $order_item->get_product();
return [
'key' => $order->get_order_key(),
'id' => $order_item->get_id(),
'quantity' => $order_item->get_quantity(),
'quantity_limits' => array(
'minimum' => $order_item->get_quantity(),
'maximum' => $order_item->get_quantity(),
'multiple_of' => 1,
'editable' => false,
),
'name' => $order_item->get_name(),
'short_description' => $this->prepare_html_response( wc_format_content( wp_kses_post( $product->get_short_description() ) ) ),
'description' => $this->prepare_html_response( wc_format_content( wp_kses_post( $product->get_description() ) ) ),
'sku' => $this->prepare_html_response( $product->get_sku() ),
'low_stock_remaining' => null,
'backorders_allowed' => false,
'show_backorder_badge' => false,
'sold_individually' => $product->is_sold_individually(),
'permalink' => $product->get_permalink(),
'images' => $this->get_images( $product ),
'variation' => $this->format_variation_data( $product->get_attributes(), $product ),
'item_data' => $order_item->get_all_formatted_meta_data(),
'prices' => (object) $this->prepare_product_price_response( $product, get_option( 'woocommerce_tax_display_cart' ) ),
'totals' => (object) $this->prepare_currency_response( $this->get_totals( $order_item ) ),
'catalog_visibility' => $product->get_catalog_visibility(),
];
}
/**
* Get totals data.
*
* @param \WC_Order_Item_Product $order_item Order item instance.
* @return array
*/
public function get_totals( $order_item ) {
return [
'line_subtotal' => $this->prepare_money_response( $order_item->get_subtotal(), wc_get_price_decimals() ),
'line_subtotal_tax' => $this->prepare_money_response( $order_item->get_subtotal_tax(), wc_get_price_decimals() ),
'line_total' => $this->prepare_money_response( $order_item->get_total(), wc_get_price_decimals() ),
'line_total_tax' => $this->prepare_money_response( $order_item->get_total_tax(), wc_get_price_decimals() ),
];
}
}

View File

@ -0,0 +1,342 @@
<?php
namespace Automattic\WooCommerce\StoreApi\Schemas\V1;
use Automattic\WooCommerce\StoreApi\SchemaController;
use Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema;
use Automattic\WooCommerce\StoreApi\Utilities\OrderController;
/**
* OrderSchema class.
*/
class OrderSchema extends AbstractSchema {
/**
* The schema item name.
*
* @var string
*/
protected $title = 'order';
/**
* The schema item identifier.
*
* @var string
*/
const IDENTIFIER = 'order';
/**
* Item schema instance.
*
* @var OrderItemSchema
*/
public $item_schema;
/**
* Order controller class instance.
*
* @var OrderController
*/
protected $order_controller;
/**
* Constructor.
*
* @param ExtendSchema $extend Rest Extending instance.
* @param SchemaController $controller Schema Controller instance.
*/
public function __construct( ExtendSchema $extend, SchemaController $controller ) {
parent::__construct( $extend, $controller );
$this->item_schema = $this->controller->get( OrderItemSchema::IDENTIFIER );
$this->coupon_schema = $this->controller->get( OrderCouponSchema::IDENTIFIER );
$this->fee_schema = $this->controller->get( OrderFeeSchema::IDENTIFIER );
$this->shipping_rate_schema = $this->controller->get( CartShippingRateSchema::IDENTIFIER );
$this->shipping_address_schema = $this->controller->get( ShippingAddressSchema::IDENTIFIER );
$this->billing_address_schema = $this->controller->get( BillingAddressSchema::IDENTIFIER );
$this->error_schema = $this->controller->get( ErrorSchema::IDENTIFIER );
$this->order_controller = new OrderController();
}
/**
* Order schema properties.
*
* @return array
*/
public function get_properties() {
return [
'id' => [
'description' => __( 'The order ID.', 'woo-gutenberg-products-block' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'items' => [
'description' => __( 'Line items data.', 'woo-gutenberg-products-block' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'items' => [
'type' => 'object',
'properties' => $this->force_schema_readonly( $this->item_schema->get_properties() ),
],
],
'totals' => [
'description' => __( 'Order totals.', 'woo-gutenberg-products-block' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => array_merge(
$this->get_store_currency_properties(),
[
'subtotal' => [
'description' => __( 'Subtotal of the order.', '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_shipping' => [
'description' => __( 'Total price of shipping.', 'woo-gutenberg-products-block' ),
'type' => [ 'string', 'null' ],
'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_tax' => [
'description' => __( 'Total tax applied to the order.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_refund' => [
'description' => __( 'Total refund applied to the order.', '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_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_tax' => [
'description' => __( 'Total tax on fees.', '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_tax' => [
'description' => __( 'Total tax on shipping. If shipping has not been calculated, a null response will be sent.', 'woo-gutenberg-products-block' ),
'type' => [ 'string', 'null' ],
'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,
],
'rate' => [
'description' => __( 'The rate at which tax is applied.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
],
]
),
],
'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( $this->coupon_schema->get_properties() ),
],
],
'shipping_address' => [
'description' => __( 'Current set shipping address for the customer.', 'woo-gutenberg-products-block' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => $this->force_schema_readonly( $this->shipping_address_schema->get_properties() ),
],
'billing_address' => [
'description' => __( 'Current set billing address for the customer.', 'woo-gutenberg-products-block' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => $this->force_schema_readonly( $this->billing_address_schema->get_properties() ),
],
'needs_payment' => [
'description' => __( 'True if the cart needs payment. False for carts with only free products and no shipping costs.', 'woo-gutenberg-products-block' ),
'type' => 'boolean',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'needs_shipping' => [
'description' => __( 'True if the cart needs shipping. False for carts with only digital goods or stores with no shipping methods set-up.', 'woo-gutenberg-products-block' ),
'type' => 'boolean',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'errors' => [
'description' => __( 'List of cart item errors, for example, items in the cart which are out of stock.', 'woo-gutenberg-products-block' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => $this->force_schema_readonly( $this->error_schema->get_properties() ),
],
],
'payment_requirements' => [
'description' => __( 'List of required payment gateway features to process the order.', 'woo-gutenberg-products-block' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'status' => [
'description' => __( 'Status of the order.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
];
}
/**
* Get an order for response.
*
* @param \WC_Order $order Order instance.
* @return array
*/
public function get_item_response( $order ) {
$order_id = $order->get_id();
$errors = [];
$failed_order_stock_error = $this->order_controller->get_failed_order_stock_error( $order_id );
if ( $failed_order_stock_error ) {
$errors[] = $failed_order_stock_error;
}
return [
'id' => $order_id,
'status' => $order->get_status(),
'items' => $this->get_item_responses_from_schema( $this->item_schema, $order->get_items() ),
'coupons' => $this->get_item_responses_from_schema( $this->coupon_schema, $order->get_items( 'coupon' ) ),
'fees' => $this->get_item_responses_from_schema( $this->fee_schema, $order->get_items( 'fee' ) ),
'totals' => (object) $this->prepare_currency_response( $this->get_totals( $order ) ),
'shipping_address' => (object) $this->shipping_address_schema->get_item_response( new \WC_Customer( $order->get_customer_id() ) ),
'billing_address' => (object) $this->billing_address_schema->get_item_response( new \WC_Customer( $order->get_customer_id() ) ),
'needs_payment' => $order->needs_payment(),
'needs_shipping' => $order->needs_shipping_address(),
'payment_requirements' => $this->extend->get_payment_requirements(),
'errors' => $errors,
];
}
/**
* Get total data.
*
* @param \WC_Order $order Order instance.
* @return array
*/
protected function get_totals( $order ) {
return [
'subtotal' => $this->prepare_money_response( $order->get_subtotal() ),
'total_discount' => $this->prepare_money_response( $order->get_total_discount() ),
'total_shipping' => $this->prepare_money_response( $order->get_total_shipping() ),
'total_fees' => $this->prepare_money_response( $order->get_total_fees() ),
'total_tax' => $this->prepare_money_response( $order->get_total_tax() ),
'total_refund' => $this->prepare_money_response( $order->get_total_refunded() ),
'total_price' => $this->prepare_money_response( $order->get_total() ),
'total_items' => $this->prepare_money_response(
array_sum(
array_map(
function( $item ) {
return $item->get_total();
},
array_values( $order->get_items( 'line_item' ) )
)
)
),
'total_items_tax' => $this->prepare_money_response(
array_sum(
array_map(
function( $item ) {
return $item->get_tax_total();
},
array_values( $order->get_items( 'tax' ) )
)
)
),
'total_fees_tax' => $this->prepare_money_response(
array_sum(
array_map(
function( $item ) {
return $item->get_total_tax();
},
array_values( $order->get_items( 'fee' ) )
)
)
),
'total_discount_tax' => $this->prepare_money_response( $order->get_discount_tax() ),
'total_shipping_tax' => $this->prepare_money_response( $order->get_shipping_tax() ),
'tax_lines' => array_map(
function( $item ) {
return [
'name' => $item->get_name(),
'price' => $item->get_tax_total(),
'rate' => strval( $item->get_rate_percent() ),
];
},
array_values( $order->get_items( 'tax' ) )
),
];
}
}

View File

@ -158,7 +158,7 @@ class ProductReviewSchema extends AbstractSchema {
* @param \WP_Comment $review Product review object. * @param \WP_Comment $review Product review object.
* @return array * @return array
*/ */
public function get_item_response( \WP_Comment $review ) { public function get_item_response( $review ) {
$context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$rating = get_comment_meta( $review->comment_ID, 'rating', true ) === '' ? null : (int) get_comment_meta( $review->comment_ID, 'rating', true ); $rating = get_comment_meta( $review->comment_ID, 'rating', true ) === '' ? null : (int) get_comment_meta( $review->comment_ID, 'rating', true );
$data = [ $data = [

View File

@ -30,7 +30,7 @@ class ShippingAddressSchema extends AbstractAddressSchema {
* @param \WC_Order|\WC_Customer $address An object with shipping address. * @param \WC_Order|\WC_Customer $address An object with shipping address.
* *
* @throws RouteException When the invalid object types are provided. * @throws RouteException When the invalid object types are provided.
* @return stdClass * @return array
*/ */
public function get_item_response( $address ) { public function get_item_response( $address ) {
$validation_util = new ValidationUtils(); $validation_util = new ValidationUtils();
@ -42,7 +42,7 @@ class ShippingAddressSchema extends AbstractAddressSchema {
$shipping_state = ''; $shipping_state = '';
} }
return (object) $this->prepare_html_response( return $this->prepare_html_response(
[ [
'first_name' => $address->get_shipping_first_name(), 'first_name' => $address->get_shipping_first_name(),
'last_name' => $address->get_shipping_last_name(), 'last_name' => $address->get_shipping_last_name(),

View File

@ -38,6 +38,18 @@ class OrderController {
return $order; return $order;
} }
/**
* Get order.
*
* @throws RouteException Exception if invalid data is detected.
*
* @param integer $order_id Order ID.
* @return \WC_Order A new order object.
*/
public function get_order( $order_id ) {
return wc_get_order( $order_id );
}
/** /**
* Update an order using data from the current cart. * Update an order using data from the current cart.
* *
@ -461,6 +473,122 @@ class OrderController {
} }
} }
/**
* Validate a given order key against an existing order.
*
* @throws RouteException Exception if invalid data is detected.
* @param integer $order_id Order ID.
* @param string $order_key Order key.
*/
public function validate_order_key( $order_id, $order_key ) {
$order = $this->get_order( $order_id );
if ( ! $order || ! $order_key || $order->get_id() !== $order_id || ! hash_equals( $order->get_order_key(), $order_key ) ) {
throw new RouteException( 'woocommerce_rest_invalid_order', __( 'Invalid order ID or key provided.', 'woo-gutenberg-products-block' ), 401 );
}
}
/**
* Validate a given billing email against an existing order.
*
* @throws RouteException Exception if invalid data is detected.
* @param integer $order_id Order ID.
* @param string $billing_email Billing email.
*/
public function validate_billing_email( $order_id, $billing_email ) {
$order = $this->get_order( $order_id );
if ( ! $order || ! $billing_email || $order->get_billing_email() !== $billing_email ) {
throw new RouteException( 'woocommerce_rest_invalid_billing_email', __( 'Invalid billing email provided.', 'woo-gutenberg-products-block' ), 401 );
}
}
/**
* Get errors for order stock on failed orders.
*
* @throws RouteException Exception if invalid data is detected.
* @param integer $order_id Order ID.
*/
public function get_failed_order_stock_error( $order_id ) {
$order = $this->get_order( $order_id );
// Ensure order items are still stocked if paying for a failed order. Pending orders do not need this check because stock is held.
if ( ! $order->has_status( wc_get_is_pending_statuses() ) ) {
$quantities = array();
foreach ( $order->get_items() as $item_key => $item ) {
if ( $item && is_callable( array( $item, 'get_product' ) ) ) {
$product = $item->get_product();
if ( ! $product ) {
continue;
}
$quantities[ $product->get_stock_managed_by_id() ] = isset( $quantities[ $product->get_stock_managed_by_id() ] ) ? $quantities[ $product->get_stock_managed_by_id() ] + $item->get_quantity() : $item->get_quantity();
}
}
// Stock levels may already have been adjusted for this order (in which case we don't need to worry about checking for low stock).
if ( ! $order->get_data_store()->get_stock_reduced( $order->get_id() ) ) {
foreach ( $order->get_items() as $item_key => $item ) {
if ( $item && is_callable( array( $item, 'get_product' ) ) ) {
$product = $item->get_product();
if ( ! $product ) {
continue;
}
/**
* Filters whether or not the product is in stock for this pay for order.
*
* @param boolean True if in stock.
* @param \WC_Product $product Product.
* @param \WC_Order $order Order.
*
* @since 9.8.0-dev
*/
if ( ! apply_filters( 'woocommerce_pay_order_product_in_stock', $product->is_in_stock(), $product, $order ) ) {
return array(
'code' => 'woocommerce_rest_out_of_stock',
/* translators: %s: product name */
'message' => sprintf( __( 'Sorry, "%s" is no longer in stock so this order cannot be paid for. We apologize for any inconvenience caused.', 'woo-gutenberg-products-block' ), $product->get_name() ),
);
}
// We only need to check products managing stock, with a limited stock qty.
if ( ! $product->managing_stock() || $product->backorders_allowed() ) {
continue;
}
// Check stock based on all items in the cart and consider any held stock within pending orders.
$held_stock = wc_get_held_stock_quantity( $product, $order->get_id() );
$required_stock = $quantities[ $product->get_stock_managed_by_id() ];
/**
* Filters whether or not the product has enough stock.
*
* @param boolean True if has enough stock.
* @param \WC_Product $product Product.
* @param \WC_Order $order Order.
*
* @since 9.8.0-dev
*/
if ( ! apply_filters( 'woocommerce_pay_order_product_has_enough_stock', ( $product->get_stock_quantity() >= ( $held_stock + $required_stock ) ), $product, $order ) ) {
/* translators: 1: product name 2: quantity in stock */
return array(
'code' => 'woocommerce_rest_out_of_stock',
/* translators: %s: product name */
'message' => sprintf( __( 'Sorry, we do not have enough "%1$s" in stock to fulfill your order (%2$s available). We apologize for any inconvenience caused.', 'woo-gutenberg-products-block' ), $product->get_name(), wc_format_stock_quantity_for_display( $product->get_stock_quantity() - $held_stock, $product ) ),
);
}
}
}
}
}
return null;
}
/** /**
* Changes default order status to draft for orders created via this API. * Changes default order status to draft for orders created via this API.
* *

View File

@ -0,0 +1,85 @@
<?php
namespace Automattic\WooCommerce\StoreApi\Utilities;
/**
* ProductItemTrait
*
* Shared functionality for formating product item data.
*/
trait ProductItemTrait {
/**
* Get an array of pricing data.
*
* @param \WC_Product $product Product instance.
* @param string $tax_display_mode If returned prices are incl or excl of tax.
* @return array
*/
protected function prepare_product_price_response( \WC_Product $product, $tax_display_mode = '' ) {
$tax_display_mode = $this->get_tax_display_mode( $tax_display_mode );
$price_function = $this->get_price_function_from_tax_display_mode( $tax_display_mode );
$prices = parent::prepare_product_price_response( $product, $tax_display_mode );
// Add raw prices (prices with greater precision).
$prices['raw_prices'] = [
'precision' => wc_get_rounding_precision(),
'price' => $this->prepare_money_response( $price_function( $product ), wc_get_rounding_precision() ),
'regular_price' => $this->prepare_money_response( $price_function( $product, [ 'price' => $product->get_regular_price() ] ), wc_get_rounding_precision() ),
'sale_price' => $this->prepare_money_response( $price_function( $product, [ 'price' => $product->get_sale_price() ] ), wc_get_rounding_precision() ),
];
return $prices;
}
/**
* Format variation data, for example convert slugs such as attribute_pa_size to Size.
*
* @param array $variation_data Array of data from the cart.
* @param \WC_Product $product Product data.
* @return array
*/
protected function format_variation_data( $variation_data, $product ) {
$return = [];
if ( ! is_iterable( $variation_data ) ) {
return $return;
}
foreach ( $variation_data as $key => $value ) {
$taxonomy = wc_attribute_taxonomy_name( str_replace( 'attribute_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 {
/**
* Filters the variation option name.
*
* Filters the variation option name for custom option slugs.
*
* @since 2.5.0
*
* @internal Matches filter name in WooCommerce core.
*
* @param string $value The name to display.
* @param null $unused Unused because this is not a variation taxonomy.
* @param string $taxonomy Taxonomy or product attribute name.
* @param \WC_Product $product Product data.
* @return string
*/
$value = apply_filters( 'woocommerce_variation_option_name', $value, null, $taxonomy, $product );
$label = wc_attribute_label( str_replace( 'attribute_', '', $key ), $product );
}
$return[] = [
'attribute' => $this->prepare_html_response( $label ),
'value' => $this->prepare_html_response( $value ),
];
}
return $return;
}
}