Completed
Pull Request — master (#11755)
by Mike
09:03
created

WC_REST_Orders_Controller::prepare_links()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 21
Code Lines 13

Duplication

Lines 10
Ratio 47.62 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 3
eloc 13
c 3
b 0
f 0
nc 4
nop 1
dl 10
loc 21
rs 9.3142
1
<?php
2
/**
3
 * REST API Orders controller
4
 *
5
 * Handles requests to the /orders endpoint.
6
 *
7
 * @author   WooThemes
8
 * @category API
9
 * @package  WooCommerce/API
10
 * @since    2.6.0
11
 */
12
13
if ( ! defined( 'ABSPATH' ) ) {
14
	exit;
15
}
16
17
/**
18
 * REST API Orders controller class.
19
 *
20
 * @package WooCommerce/API
21
 * @extends WC_REST_Posts_Controller
22
 */
23
class WC_REST_Orders_Controller extends WC_REST_Posts_Controller {
24
25
	/**
26
	 * Endpoint namespace.
27
	 *
28
	 * @var string
29
	 */
30
	protected $namespace = 'wc/v1';
31
32
	/**
33
	 * Route base.
34
	 *
35
	 * @var string
36
	 */
37
	protected $rest_base = 'orders';
38
39
	/**
40
	 * Post type.
41
	 *
42
	 * @var string
43
	 */
44
	protected $post_type = 'shop_order';
45
46
	/**
47
	 * Stores the request.
48
	 * @var array
49
	 */
50
	protected $request = array();
51
52
	/**
53
	 * Initialize orders actions.
54
	 */
55
	public function __construct() {
56
		add_filter( "woocommerce_rest_{$this->post_type}_query", array( $this, 'query_args' ), 10, 2 );
57
	}
58
59
	/**
60
	 * Register the routes for orders.
61
	 */
62
	public function register_routes() {
63
		register_rest_route( $this->namespace, '/' . $this->rest_base, array(
64
			array(
65
				'methods'             => WP_REST_Server::READABLE,
66
				'callback'            => array( $this, 'get_items' ),
67
				'permission_callback' => array( $this, 'get_items_permissions_check' ),
68
				'args'                => $this->get_collection_params(),
69
			),
70
			array(
71
				'methods'             => WP_REST_Server::CREATABLE,
72
				'callback'            => array( $this, 'create_item' ),
73
				'permission_callback' => array( $this, 'create_item_permissions_check' ),
74
				'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
75
			),
76
			'schema' => array( $this, 'get_public_item_schema' ),
77
		) );
78
79
		register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
80
			array(
81
				'methods'             => WP_REST_Server::READABLE,
82
				'callback'            => array( $this, 'get_item' ),
83
				'permission_callback' => array( $this, 'get_item_permissions_check' ),
84
				'args'                => array(
85
					'context' => $this->get_context_param( array( 'default' => 'view' ) ),
86
				),
87
			),
88
			array(
89
				'methods'             => WP_REST_Server::EDITABLE,
90
				'callback'            => array( $this, 'update_item' ),
91
				'permission_callback' => array( $this, 'update_item_permissions_check' ),
92
				'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
93
			),
94
			array(
95
				'methods'             => WP_REST_Server::DELETABLE,
96
				'callback'            => array( $this, 'delete_item' ),
97
				'permission_callback' => array( $this, 'delete_item_permissions_check' ),
98
				'args'                => array(
99
					'force' => array(
100
						'default'     => false,
101
						'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ),
102
					),
103
					'reassign' => array(),
104
				),
105
			),
106
			'schema' => array( $this, 'get_public_item_schema' ),
107
		) );
108
109
		register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array(
110
			array(
111
				'methods'             => WP_REST_Server::EDITABLE,
112
				'callback'            => array( $this, 'batch_items' ),
113
				'permission_callback' => array( $this, 'batch_items_permissions_check' ),
114
				'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
115
			),
116
			'schema' => array( $this, 'get_public_batch_schema' ),
117
		) );
118
	}
119
120
	/**
121
	 * Expands an order item to get its data.
122
	 * @param WC_Order_item $item
123
	 * @return array
124
	 */
125
	protected function get_order_item_data( $item ) {
126
		$data           = $item->get_data();
127
		$format_decimal = array( 'subtotal', 'subtotal_tax', 'total', 'total_tax', 'tax_total', 'shipping_tax_total' );
128
129
		// Format decimal values.
130 View Code Duplication
		foreach ( $format_decimal as $key ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
131
			if ( isset( $data[ $key ] ) ) {
132
				$data[ $key ] = wc_format_decimal( $data[ $key ], $this->request['dp'] );
133
			}
134
		}
135
136
		// Add meta, SKU and PRICE to products.
137
		if ( is_callable( array( $item, 'get_product' ) ) ) {
138
			$data['sku']   = $item->get_product() ? $item->get_product()->get_sku(): null;
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class WC_Order_Item as the method get_product() does only exist in the following sub-classes of WC_Order_Item: WC_Order_Item_Product. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
139
			$data['price'] = $item->get_total() / max( 1, $item->get_quantity() );
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class WC_Order_Item as the method get_total() does only exist in the following sub-classes of WC_Order_Item: WC_Order_Item_Fee, WC_Order_Item_Product, WC_Order_Item_Shipping. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
140
141
			// Format meta data.
142
			if ( isset( $data['meta_data'] ) ) {
143
				$hideprefix = 'true' === $this->request['all_item_meta'] ? null : '_';
144
				$item_meta  = $item->get_formatted_meta_data( $hideprefix );
145
146
				foreach ( $item_meta as $key => $values ) {
147
					// Label was used in previous version of API - set it here.
148
					$item_meta[ $key ]->label = $values->display_key;
149
					unset( $item_meta[ $key ]->display_key );
150
					unset( $item_meta[ $key ]->display_value );
151
				}
152
153
				$data['meta'] = array_values( $item_meta );
154
			}
155
		}
156
157
		// Format taxes.
158
		if ( ! empty( $data['taxes']['total'] ) ) {
159
			$taxes = array();
160
161
			foreach ( $data['taxes']['total'] as $tax_rate_id => $tax ) {
162
				$taxes[] = array(
163
					'id'       => $tax_rate_id,
164
					'total'    => $tax,
165
					'subtotal' => isset( $data['taxes']['subtotal'][ $tax_rate_id ] ) ? $data['taxes']['subtotal'][ $tax_rate_id ] : '',
166
				);
167
			}
168
			$data['taxes'] = $taxes;
169
		} elseif ( isset( $data['taxes'] ) ) {
170
			$data['taxes'] = array();
171
		}
172
173
		// Remove props we don't want to expose.
174
		unset( $data['order_id'] );
175
		unset( $data['type'] );
176
177
		return $data;
178
	}
179
180
	/**
181
	 * Prepare a single order output for response.
182
	 *
183
	 * @param WP_Post $post Post object.
184
	 * @param WP_REST_Request $request Request object.
185
	 * @return WP_REST_Response $data
186
	 */
187
	public function prepare_item_for_response( $post, $request ) {
188
		$this->request     = $request;
0 ignored issues
show
Documentation Bug introduced by
It seems like $request of type object<WP_REST_Request> is incompatible with the declared type array of property $request.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
189
		$statuses          = wc_get_order_statuses();
0 ignored issues
show
Unused Code introduced by
$statuses is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
190
		$order             = wc_get_order( $post );
191
		$data              = $order->get_data();
192
		$format_decimal    = array( 'discount_total', 'discount_tax', 'shipping_total', 'shipping_tax', 'shipping_total', 'shipping_tax', 'cart_tax', 'total', 'total_tax' );
193
		$format_date       = array( 'date_created', 'date_modified', 'date_completed' );
194
		$format_line_items = array( 'line_items', 'tax_lines', 'shipping_lines', 'fee_lines', 'coupon_lines' );
195
196
		// Format decimal values.
197 View Code Duplication
		foreach ( $format_decimal as $key ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
198
			$data[ $key ] = wc_format_decimal( $data[ $key ], $this->request['dp'] );
199
		}
200
201
		// Format date values.
202 View Code Duplication
		foreach ( $format_date as $key ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
203
			$data[ $key ] = $data[ $key ] ? wc_rest_prepare_date_response( get_gmt_from_date( date( 'Y-m-d H:i:s', $data[ $key ] ) ) ) : false;
204
		}
205
206
		// Format the order status.
207
		$data['status'] = 'wc-' === substr( $data['status'], 0, 3 ) ? substr( $data['status'], 3 ) : $data['status'];
208
209
		// Format line items.
210 View Code Duplication
		foreach ( $format_line_items as $key ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
211
			$data[ $key ] = array_values( array_map( array( $this, 'get_order_item_data' ), $data[ $key ] ) );
212
		}
213
214
		// Refunds.
215
		foreach ( $order->get_refunds() as $refund ) {
216
			$data['refunds'][] = array(
217
				'id'     => $refund->get_id(),
218
				'refund' => $refund->get_reason() ? $refund->get_reason() : '',
219
				'total'  => '-' . wc_format_decimal( $refund->get_amount(), $this->request['dp'] ),
220
			);
221
		}
222
223
		$context  = ! empty( $request['context'] ) ? $request['context'] : 'view';
224
		$data     = $this->add_additional_fields_to_object( $data, $request );
225
		$data     = $this->filter_response_by_context( $data, $context );
226
		$response = rest_ensure_response( $data );
227
		$response->add_links( $this->prepare_links( $order ) );
0 ignored issues
show
Documentation introduced by
$order is of type false|object, but the function expects a object<WC_Order>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
228
229
		/**
230
		 * Filter the data for a response.
231
		 *
232
		 * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
233
		 * prepared for the response.
234
		 *
235
		 * @param WP_REST_Response   $response   The response object.
236
		 * @param WP_Post            $post       Post object.
237
		 * @param WP_REST_Request    $request    Request object.
238
		 */
239
		return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request );
240
	}
241
242
	/**
243
	 * Prepare links for the request.
244
	 *
245
	 * @param WC_Order $order Order object.
246
	 * @return array Links for the given order.
247
	 */
248
	protected function prepare_links( $order ) {
249
		$links = array(
250
			'self' => array(
251
				'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $order->get_id() ) ),
252
			),
253
			'collection' => array(
254
				'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
255
			),
256
		);
257 View Code Duplication
		if ( 0 !== (int) $order->get_user_id() ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
258
			$links['customer'] = array(
259
				'href' => rest_url( sprintf( '/%s/customers/%d', $this->namespace, $order->get_user_id() ) ),
260
			);
261
		}
262 View Code Duplication
		if ( 0 !== (int) $order->get_parent_id() ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
263
			$links['up'] = array(
264
				'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $order->get_parent_id() ) ),
265
			);
266
		}
267
		return $links;
268
	}
269
270
	/**
271
	 * Query args.
272
	 *
273
	 * @param array $args
274
	 * @param WP_REST_Request $request
275
	 * @return array
276
	 */
277
	public function query_args( $args, $request ) {
278
		global $wpdb;
279
280
		// Set post_status.
281
		if ( 'any' !== $request['status'] ) {
282
			$args['post_status'] = 'wc-' . $request['status'];
283
		} else {
284
			$args['post_status'] = 'any';
285
		}
286
287 View Code Duplication
		if ( ! empty( $request['customer'] ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
288
			if ( ! empty( $args['meta_query'] ) ) {
289
				$args['meta_query'] = array();
290
			}
291
292
			$args['meta_query'][] = array(
293
				'key'   => '_customer_user',
294
				'value' => $request['customer'],
295
				'type'  => 'NUMERIC',
296
			);
297
		}
298
299
		// Search by product.
300
		if ( ! empty( $request['product'] ) ) {
301
			$order_ids = $wpdb->get_col( $wpdb->prepare( "
302
				SELECT order_id
303
				FROM {$wpdb->prefix}woocommerce_order_items
304
				WHERE order_item_id IN ( SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_product_id' AND meta_value = %d )
305
				AND order_item_type = 'line_item'
306
			 ", $request['product'] ) );
307
308
			// Force WP_Query return empty if don't found any order.
309
			$order_ids = ! empty( $order_ids ) ? $order_ids : array( 0 );
310
311
			$args['post__in'] = $order_ids;
312
		}
313
314
		// Search.
315
		if ( ! empty( $args['s'] ) ) {
316
			$order_ids = wc_order_search( $args['s'] );
0 ignored issues
show
Bug introduced by
It seems like $args['s'] can also be of type array; however, wc_order_search() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
317
318
			if ( ! empty( $order_ids ) ) {
319
				unset( $args['s'] );
320
				$args['post__in'] =  array_merge( $order_ids, array( 0 ) );
321
			}
322
		}
323
324
		return $args;
325
	}
326
327
	/**
328
	 * Prepare a single order for create.
329
	 *
330
	 * @param  WP_REST_Request $request Request object.
331
	 * @return WP_Error|WC_Order $data Object.
332
	 */
333
	protected function prepare_item_for_database( $request ) {
334
		$id        = isset( $request['id'] ) ? absint( $request['id'] ) : 0;
335
		$order     = new WC_Order( $id );
336
		$schema    = $this->get_item_schema();
337
		$data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) );
338
339
		// Handle all writable props
340
		foreach ( $data_keys as $key ) {
341
			$value = $request[ $key ];
342
343
			if ( ! is_null( $value ) ) {
344
				switch ( $key ) {
345
					case 'billing' :
346
					case 'shipping' :
347
						$this->update_address( $order, $value, $key );
348
						break;
349
					case 'line_items' :
350
					case 'shipping_lines' :
351
					case 'fee_lines' :
352
					case 'coupon_lines' :
353
						if ( is_array( $value ) ) {
354
							foreach ( $value as $item ) {
355
								if ( is_array( $item ) ) {
356
									if ( $this->item_is_null( $item ) || ( isset( $item['quantity'] ) && 0 === $item['quantity'] ) ) {
357
										$order->remove_item( $item['id'] );
358
									} else {
359
										$this->set_item( $order, $key, $item );
360
									}
361
								}
362
							}
363
						}
364
						break;
365
					case 'meta_data' :
366
						if ( is_array( $value ) ) {
367
							foreach ( $value as $meta ) {
368
								$order->update_meta_data( $meta['key'], $meta['value'], $meta['id'] );
369
							}
370
						}
371
						break;
372
					default :
373
						if ( is_callable( array( $order, "set_{$key}" ) ) ) {
374
							$order->{"set_{$key}"}( $value );
375
						}
376
						break;
377
				}
378
			}
379
		}
380
381
		/**
382
		 * Filter the data for the insert.
383
		 *
384
		 * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
385
		 * prepared for the response.
386
		 *
387
		 * @param WC_Order           $order      The prder object.
388
		 * @param WP_REST_Request    $request    Request object.
389
		 */
390
		return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $order, $request );
391
	}
392
393
	/**
394
	 * Create base WC Order object.
395
	 * @deprecated 2.7.0
396
	 * @param array $data
397
	 * @return WC_Order
398
	 */
399
	protected function create_base_order( $data ) {
400
		return wc_create_order( $data );
401
	}
402
403
	/**
404
	 * Only reutrn writeable props from schema.
405
	 * @param  array $schema
406
	 * @return bool
407
	 */
408
	protected function filter_writable_props( $schema ) {
409
		return empty( $schema['readonly'] );
410
	}
411
412
	/**
413
	 * Create order.
414
	 *
415
	 * @param WP_REST_Request $request Full details about the request.
416
	 * @return int|WP_Error
417
	 */
418
	protected function create_order( $request ) {
419
		try {
420
			// Make sure customer exists.
421
			if ( ! is_null( $request['customer_id'] ) && 0 !== $request['customer_id'] && false === get_user_by( 'id', $request['customer_id'] ) ) {
422
				throw new WC_REST_Exception( 'woocommerce_rest_invalid_customer_id',__( 'Customer ID is invalid.', 'woocommerce' ), 400 );
423
			}
424
425
			$order = $this->prepare_item_for_database( $request );
426
			$order->set_created_via( 'rest-api' );
427
			$order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) );
428
			$order->calculate_totals();
429
			$order->save();
430
431
			// Handle set paid
432
			if ( true === $request['set_paid'] ) {
433
				$order->payment_complete( $request['transaction_id'] );
434
			}
435
436
			return $order->get_id();
437
		} catch ( WC_REST_Exception $e ) {
438
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
439
		}
440
	}
441
442
	/**
443
	 * Update order.
444
	 *
445
	 * @param WP_REST_Request $request Full details about the request.
446
	 * @return int|WP_Error
447
	 */
448
	protected function update_order( $request ) {
449
		try {
450
			$order = $this->prepare_item_for_database( $request );
451
			$order->save();
452
453
			// Handle set paid
454
			if ( $order->needs_payment() && true === $request['set_paid'] ) {
455
				$order->payment_complete( $request['transaction_id'] );
456
			}
457
458
			// If items have changed, recalculate order totals.
459
			if ( isset( $request[ 'billing' ], $request[ 'shipping' ], $request[ 'line_items' ], $request[ 'shipping' ], $request[ 'fee' ], $request[ 'coupon' ] ) ) {
460
				$order->calculate_totals();
461
			}
462
463
			return $order->get_id();
464
		} catch ( WC_Data_Exception $e ) {
0 ignored issues
show
Bug introduced by
The class WC_Data_Exception does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
465
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
466
		} catch ( WC_REST_Exception $e ) {
467
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
468
		}
469
	}
470
471
	/**
472
	 * Update address.
473
	 *
474
	 * @param WC_Order $order
475
	 * @param array $posted
476
	 * @param string $type
477
	 */
478
	protected function update_address( $order, $posted, $type = 'billing' ) {
479
		foreach ( $posted as $key => $value ) {
480
			if ( is_callable( array( $order, "set_{$type}_{$key}" ) ) ) {
481
				$order->{"set_{$type}_{$key}"}( $value );
482
			}
483
		}
484
	}
485
486
	/**
487
	 * Gets the product ID from the SKU or posted ID.
488
	 * @param array $posted Request data
489
	 * @return int
490
	 */
491
	protected function get_product_id( $posted ) {
492
		if ( ! empty( $posted['sku'] ) ) {
493
			$product_id = (int) wc_get_product_id_by_sku( $posted['sku'] );
494
		} elseif ( ! empty( $posted['product_id'] ) && empty( $posted['variation_id'] ) ) {
495
			$product_id = (int) $posted['product_id'];
496
		} elseif ( ! empty( $posted['variation_id'] ) ) {
497
			$product_id = (int) $posted['variation_id'];
498
		} else {
499
			throw new WC_REST_Exception( 'woocommerce_rest_required_product_reference', __( 'Product ID or SKU is required.', 'woocommerce' ), 400 );
500
		}
501
		return $product_id;
502
	}
503
504
	/**
505
	 * Maybe set an item prop if the value was posted.
506
	 * @param WC_Order_Item $item
507
	 * @param string $prop
508
	 * @param array $posted Request data.
509
	 */
510
	protected function maybe_set_item_prop( $item, $prop, $posted ) {
511
		if ( isset( $posted[ $prop ] ) ) {
512
			$item->{"set_$prop"}( $posted[ $prop ] );
513
		}
514
	}
515
516
	/**
517
	 * Maybe set item props if the values were posted.
518
	 * @param WC_Order_Item $item
519
	 * @param string[] $props
520
	 * @param array $posted Request data.
521
	 */
522
	protected function maybe_set_item_props( $item, $props, $posted ) {
523
		foreach ( $props as $prop ) {
524
			$this->maybe_set_item_prop( $item, $prop, $posted );
525
		}
526
	}
527
528
	/**
529
	 * Create or update a line item.
530
	 *
531
	 * @param array $posted Line item data.
532
	 * @param string $action 'create' to add line item or 'update' to update it.
533
	 * @throws WC_REST_Exception Invalid data, server error.
534
	 */
535
	protected function prepare_line_items( $posted, $action = 'create' ) {
536
		$item    = new WC_Order_Item_Product( ! empty( $posted['id'] ) ? $posted['id'] : '' );
537
		$product = wc_get_product( $this->get_product_id( $posted ) );
538
539
		if ( $product !== $item->get_product() ) {
540
			$item->set_product( $product );
541
542
			if ( 'create' === $action ) {
543
				$total = $product->get_price() * ( isset( $posted['quantity'] ) ? $posted['quantity'] : 1 );
544
				$item->set_total( $total );
545
				$item->set_subtotal( $total );
546
			}
547
		}
548
549
		$this->maybe_set_item_props( $item, array( 'name', 'quantity', 'total', 'subtotal', 'tax_class' ), $posted );
550
551
		return $item;
552
	}
553
554
	/**
555
	 * Create or update an order shipping method.
556
	 *
557
	 * @param $posted $shipping Item data.
0 ignored issues
show
Documentation introduced by
The doc-type $posted could not be parsed: Unknown type name "$posted" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
Bug introduced by
There is no parameter named $shipping. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
558
	 * @param string $action 'create' to add shipping or 'update' to update it.
559
	 * @throws WC_REST_Exception Invalid data, server error.
560
	 */
561 View Code Duplication
	protected function prepare_shipping_lines( $posted, $action ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
562
		$item = new WC_Order_Item_Shipping( ! empty( $posted['id'] ) ? $posted['id'] : '' );
563
564
		if ( 'create' === $action ) {
565
			if ( empty( $posted['method_id'] ) ) {
566
				throw new WC_REST_Exception( 'woocommerce_rest_invalid_shipping_item', __( 'Shipping method ID is required.', 'woocommerce' ), 400 );
567
			}
568
		}
569
570
		$this->maybe_set_item_props( $item, array( 'method_id', 'method_title', 'total' ), $posted );
571
572
		return $item;
573
	}
574
575
	/**
576
	 * Create or update an order fee.
577
	 *
578
	 * @param array $posted Item data.
579
	 * @param string $action 'create' to add fee or 'update' to update it.
580
	 * @throws WC_REST_Exception Invalid data, server error.
581
	 */
582 View Code Duplication
	protected function prepare_fee_lines( $posted, $action ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
583
		$item = new WC_Order_Item_Fee( ! empty( $posted['id'] ) ? $posted['id'] : '' );
584
585
		if ( 'create' === $action ) {
586
			if ( empty( $posted['name'] ) ) {
587
				throw new WC_REST_Exception( 'woocommerce_rest_invalid_fee_item', __( 'Fee name is required.', 'woocommerce' ), 400 );
588
			}
589
		}
590
591
		$this->maybe_set_item_props( $item, array( 'name', 'tax_class', 'tax_status', 'total' ), $posted );
592
593
		return $item;
594
	}
595
596
	/**
597
	 * Create or update an order coupon.
598
	 *
599
	 * @param array $posted Item data.
600
	 * @param string $action 'create' to add coupon or 'update' to update it.
601
	 * @throws WC_REST_Exception Invalid data, server error.
602
	 */
603 View Code Duplication
	protected function prepare_coupon_lines( $posted, $action ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
604
		$item = new WC_Order_Item_Coupon( ! empty( $posted['id'] ) ? $posted['id'] : '' );
605
606
		if ( 'create' === $action ) {
607
			if ( empty( $posted['code'] ) ) {
608
				throw new WC_REST_Exception( 'woocommerce_rest_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 );
609
			}
610
		}
611
612
		$this->maybe_set_item_props( $item, array( 'code', 'discount' ), $posted );
613
614
		return $item;
615
	}
616
617
	/**
618
	 * Wrapper method to create/update order items.
619
	 * When updating, the item ID provided is checked to ensure it is associated
620
	 * with the order.
621
	 *
622
	 * @param WC_Order $order order
623
	 * @param string $item_type
624
	 * @param array $posted item provided in the request body
625
	 * @throws WC_REST_Exception If item ID is not associated with order
626
	 */
627
	protected function set_item( $order, $item_type, $posted ) {
628
		global $wpdb;
629
630
		if ( ! empty( $posted['id'] ) ) {
631
			$action = 'update';
632
		} else {
633
			$action = 'create';
634
		}
635
636
		$method = 'prepare_' . $item_type;
637
638
		// Verify provided line item ID is associated with order.
639
		if ( 'update' === $action ) {
640
			$result = $wpdb->get_row(
641
				$wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d AND order_id = %d",
642
				absint( $posted['id'] ),
643
				absint( $order->get_id() )
644
			) );
645
			if ( is_null( $result ) ) {
646
				throw new WC_REST_Exception( 'woocommerce_rest_invalid_item_id', __( 'Order item ID provided is not associated with order.', 'woocommerce' ), 400 );
647
			}
648
		}
649
650
		// Prepare item data
651
		$item = $this->$method( $posted, $action );
652
653
		// Save or add to order
654
		if ( 'create' === $action ) {
655
			$order->add_item( $item );
656
		} else {
657
			$item->save();
658
		}
659
	}
660
661
	/**
662
	 * Helper method to check if the resource ID associated with the provided item is null.
663
	 * Items can be deleted by setting the resource ID to null.
664
	 *
665
	 * @param array $item Item provided in the request body.
666
	 * @return bool True if the item resource ID is null, false otherwise.
667
	 */
668
	protected function item_is_null( $item ) {
669
		$keys = array( 'product_id', 'method_id', 'title', 'code' );
670
671
		foreach ( $keys as $key ) {
672
			if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) {
673
				return true;
674
			}
675
		}
676
677
		return false;
678
	}
679
680
	/**
681
	 * Create a single item.
682
	 *
683
	 * @param WP_REST_Request $request Full details about the request.
684
	 * @return WP_Error|WP_REST_Response
685
	 */
686
	public function create_item( $request ) {
687
		if ( ! empty( $request['id'] ) ) {
688
			return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) );
689
		}
690
691
		$order_id = $this->create_order( $request );
692
		if ( is_wp_error( $order_id ) ) {
693
			return $order_id;
694
		}
695
696
		$post = get_post( $order_id );
697
		$this->update_additional_fields_for_object( $post, $request );
698
699
		/**
700
		 * Fires after a single item is created or updated via the REST API.
701
		 *
702
		 * @param object          $post      Inserted object (not a WP_Post object).
703
		 * @param WP_REST_Request $request   Request object.
704
		 * @param boolean         $creating  True when creating item, false when updating.
705
		 */
706
		do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true );
707
		$request->set_param( 'context', 'edit' );
708
		$response = $this->prepare_item_for_response( $post, $request );
709
		$response = rest_ensure_response( $response );
710
		$response->set_status( 201 );
711
		$response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ) );
712
713
		return $response;
714
	}
715
716
	/**
717
	 * Update a single order.
718
	 *
719
	 * @param WP_REST_Request $request Full details about the request.
720
	 * @return WP_Error|WP_REST_Response
721
	 */
722
	public function update_item( $request ) {
723
		try {
724
			$post_id = (int) $request['id'];
725
726 View Code Duplication
			if ( empty( $post_id ) || $this->post_type !== get_post_type( $post_id ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
727
				return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) );
728
			}
729
730
			$order_id = $this->update_order( $request );
731
			if ( is_wp_error( $order_id ) ) {
732
				return $order_id;
733
			}
734
735
			$post = get_post( $order_id );
736
			$this->update_additional_fields_for_object( $post, $request );
737
738
			/**
739
			 * Fires after a single item is created or updated via the REST API.
740
			 *
741
			 * @param object          $post      Inserted object (not a WP_Post object).
742
			 * @param WP_REST_Request $request   Request object.
743
			 * @param boolean         $creating  True when creating item, false when updating.
744
			 */
745
			do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, false );
746
			$request->set_param( 'context', 'edit' );
747
			$response = $this->prepare_item_for_response( $post, $request );
748
			return rest_ensure_response( $response );
749
750
		} catch ( Exception $e ) {
751
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Exception as the method getErrorCode() does only exist in the following sub-classes of Exception: WC_CLI_Exception, WC_REST_Exception. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
752
		}
753
	}
754
755
	/**
756
	 * Get order statuses without prefixes.
757
	 * @return array
758
	 */
759
	protected function get_order_statuses() {
760
		$order_statuses = array();
761
762
		foreach ( array_keys( wc_get_order_statuses() ) as $status ) {
763
			$order_statuses[] = str_replace( 'wc-', '', $status );
764
		}
765
766
		return $order_statuses;
767
	}
768
769
	/**
770
	 * Get the Order's schema, conforming to JSON Schema.
771
	 *
772
	 * @return array
773
	 */
774
	public function get_item_schema() {
775
		$schema = array(
776
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
777
			'title'      => $this->post_type,
778
			'type'       => 'object',
779
			'properties' => array(
780
				'id' => array(
781
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
782
					'type'        => 'integer',
783
					'context'     => array( 'view', 'edit' ),
784
					'readonly'    => true,
785
				),
786
				'parent_id' => array(
787
					'description' => __( 'Parent order ID.', 'woocommerce' ),
788
					'type'        => 'integer',
789
					'context'     => array( 'view', 'edit' ),
790
				),
791
				'status' => array(
792
					'description' => __( 'Order status.', 'woocommerce' ),
793
					'type'        => 'string',
794
					'default'     => 'pending',
795
					'enum'        => $this->get_order_statuses(),
796
					'context'     => array( 'view', 'edit' ),
797
				),
798
				'order_key' => array(
799
					'description' => __( 'Order key.', 'woocommerce' ),
800
					'type'        => 'string',
801
					'context'     => array( 'view', 'edit' ),
802
					'readonly'    => true,
803
				),
804
				'currency' => array(
805
					'description' => __( 'Currency the order was created with, in ISO format.', 'woocommerce' ),
806
					'type'        => 'string',
807
					'default'     => get_woocommerce_currency(),
808
					'enum'        => array_keys( get_woocommerce_currencies() ),
809
					'context'     => array( 'view', 'edit' ),
810
				),
811
				'version' => array(
812
					'description' => __( 'Version of WooCommerce when the order was made.', 'woocommerce' ),
813
					'type'        => 'integer',
814
					'context'     => array( 'view', 'edit' ),
815
					'readonly'    => true,
816
				),
817
				'prices_include_tax' => array(
818
					'description' => __( 'True the prices included tax during checkout.', 'woocommerce' ),
819
					'type'        => 'boolean',
820
					'context'     => array( 'view', 'edit' ),
821
					'readonly'    => true,
822
				),
823
				'date_created' => array(
824
					'description' => __( "The date the order was created, in the site's timezone.", 'woocommerce' ),
825
					'type'        => 'date-time',
826
					'context'     => array( 'view', 'edit' ),
827
				),
828
				'date_modified' => array(
829
					'description' => __( "The date the order was last modified, in the site's timezone.", 'woocommerce' ),
830
					'type'        => 'date-time',
831
					'context'     => array( 'view', 'edit' ),
832
					'readonly'    => true,
833
				),
834
				'customer_id' => array(
835
					'description' => __( 'User ID who owns the order. 0 for guests.', 'woocommerce' ),
836
					'type'        => 'integer',
837
					'default'     => 0,
838
					'context'     => array( 'view', 'edit' ),
839
				),
840
				'discount_total' => array(
841
					'description' => __( 'Total discount amount for the order.', 'woocommerce' ),
842
					'type'        => 'string',
843
					'context'     => array( 'view', 'edit' ),
844
					'readonly'    => true,
845
				),
846
				'discount_tax' => array(
847
					'description' => __( 'Total discount tax amount for the order.', 'woocommerce' ),
848
					'type'        => 'string',
849
					'context'     => array( 'view', 'edit' ),
850
					'readonly'    => true,
851
				),
852
				'shipping_total' => array(
853
					'description' => __( 'Total shipping amount for the order.', 'woocommerce' ),
854
					'type'        => 'string',
855
					'context'     => array( 'view', 'edit' ),
856
					'readonly'    => true,
857
				),
858
				'shipping_tax' => array(
859
					'description' => __( 'Total shipping tax amount for the order.', 'woocommerce' ),
860
					'type'        => 'string',
861
					'context'     => array( 'view', 'edit' ),
862
					'readonly'    => true,
863
				),
864
				'cart_tax' => array(
865
					'description' => __( 'Sum of line item taxes only.', 'woocommerce' ),
866
					'type'        => 'string',
867
					'context'     => array( 'view', 'edit' ),
868
					'readonly'    => true,
869
				),
870
				'total' => array(
871
					'description' => __( 'Grand total.', 'woocommerce' ),
872
					'type'        => 'string',
873
					'context'     => array( 'view', 'edit' ),
874
					'readonly'    => true,
875
				),
876
				'total_tax' => array(
877
					'description' => __( 'Sum of all taxes.', 'woocommerce' ),
878
					'type'        => 'string',
879
					'context'     => array( 'view', 'edit' ),
880
					'readonly'    => true,
881
				),
882
				'billing' => array(
883
					'description' => __( 'Billing address.', 'woocommerce' ),
884
					'type'        => 'array',
885
					'context'     => array( 'view', 'edit' ),
886
					'properties'  => array(
887
						'first_name' => array(
888
							'description' => __( 'First name.', 'woocommerce' ),
889
							'type'        => 'string',
890
							'context'     => array( 'view', 'edit' ),
891
						),
892
						'last_name' => array(
893
							'description' => __( 'Last name.', 'woocommerce' ),
894
							'type'        => 'string',
895
							'context'     => array( 'view', 'edit' ),
896
						),
897
						'company' => array(
898
							'description' => __( 'Company name.', 'woocommerce' ),
899
							'type'        => 'string',
900
							'context'     => array( 'view', 'edit' ),
901
						),
902
						'address_1' => array(
903
							'description' => __( 'Address line 1.', 'woocommerce' ),
904
							'type'        => 'string',
905
							'context'     => array( 'view', 'edit' ),
906
						),
907
						'address_2' => array(
908
							'description' => __( 'Address line 2.', 'woocommerce' ),
909
							'type'        => 'string',
910
							'context'     => array( 'view', 'edit' ),
911
						),
912
						'city' => array(
913
							'description' => __( 'City name.', 'woocommerce' ),
914
							'type'        => 'string',
915
							'context'     => array( 'view', 'edit' ),
916
						),
917
						'state' => array(
918
							'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ),
919
							'type'        => 'string',
920
							'context'     => array( 'view', 'edit' ),
921
						),
922
						'postcode' => array(
923
							'description' => __( 'Postal code.', 'woocommerce' ),
924
							'type'        => 'string',
925
							'context'     => array( 'view', 'edit' ),
926
						),
927
						'country' => array(
928
							'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ),
929
							'type'        => 'string',
930
							'context'     => array( 'view', 'edit' ),
931
						),
932
						'email' => array(
933
							'description' => __( 'Email address.', 'woocommerce' ),
934
							'type'        => 'string',
935
							'format'      => 'email',
936
							'context'     => array( 'view', 'edit' ),
937
						),
938
						'phone' => array(
939
							'description' => __( 'Phone number.', 'woocommerce' ),
940
							'type'        => 'string',
941
							'context'     => array( 'view', 'edit' ),
942
						),
943
					),
944
				),
945
				'shipping' => array(
946
					'description' => __( 'Shipping address.', 'woocommerce' ),
947
					'type'        => 'array',
948
					'context'     => array( 'view', 'edit' ),
949
					'properties'  => array(
950
						'first_name' => array(
951
							'description' => __( 'First name.', 'woocommerce' ),
952
							'type'        => 'string',
953
							'context'     => array( 'view', 'edit' ),
954
						),
955
						'last_name' => array(
956
							'description' => __( 'Last name.', 'woocommerce' ),
957
							'type'        => 'string',
958
							'context'     => array( 'view', 'edit' ),
959
						),
960
						'company' => array(
961
							'description' => __( 'Company name.', 'woocommerce' ),
962
							'type'        => 'string',
963
							'context'     => array( 'view', 'edit' ),
964
						),
965
						'address_1' => array(
966
							'description' => __( 'Address line 1.', 'woocommerce' ),
967
							'type'        => 'string',
968
							'context'     => array( 'view', 'edit' ),
969
						),
970
						'address_2' => array(
971
							'description' => __( 'Address line 2.', 'woocommerce' ),
972
							'type'        => 'string',
973
							'context'     => array( 'view', 'edit' ),
974
						),
975
						'city' => array(
976
							'description' => __( 'City name.', 'woocommerce' ),
977
							'type'        => 'string',
978
							'context'     => array( 'view', 'edit' ),
979
						),
980
						'state' => array(
981
							'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ),
982
							'type'        => 'string',
983
							'context'     => array( 'view', 'edit' ),
984
						),
985
						'postcode' => array(
986
							'description' => __( 'Postal code.', 'woocommerce' ),
987
							'type'        => 'string',
988
							'context'     => array( 'view', 'edit' ),
989
						),
990
						'country' => array(
991
							'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ),
992
							'type'        => 'string',
993
							'context'     => array( 'view', 'edit' ),
994
						),
995
					),
996
				),
997
				'payment_method' => array(
998
					'description' => __( 'Payment method ID.', 'woocommerce' ),
999
					'type'        => 'string',
1000
					'context'     => array( 'view', 'edit' ),
1001
				),
1002
				'payment_method_title' => array(
1003
					'description' => __( 'Payment method title.', 'woocommerce' ),
1004
					'type'        => 'string',
1005
					'context'     => array( 'view', 'edit' ),
1006
				),
1007
				'transaction_id' => array(
1008
					'description' => __( 'Unique transaction ID.', 'woocommerce' ),
1009
					'type'        => 'string',
1010
					'context'     => array( 'view', 'edit' ),
1011
				),
1012
				'customer_ip_address' => array(
1013
					'description' => __( "Customer's IP address.", 'woocommerce' ),
1014
					'type'        => 'string',
1015
					'context'     => array( 'view', 'edit' ),
1016
					'readonly'    => true,
1017
				),
1018
				'customer_user_agent' => array(
1019
					'description' => __( 'User agent of the customer.', 'woocommerce' ),
1020
					'type'        => 'string',
1021
					'context'     => array( 'view', 'edit' ),
1022
					'readonly'    => true,
1023
				),
1024
				'created_via' => array(
1025
					'description' => __( 'Shows where the order was created.', 'woocommerce' ),
1026
					'type'        => 'string',
1027
					'context'     => array( 'view', 'edit' ),
1028
					'readonly'    => true,
1029
				),
1030
				'customer_note' => array(
1031
					'description' => __( 'Note left by customer during checkout.', 'woocommerce' ),
1032
					'type'        => 'string',
1033
					'context'     => array( 'view', 'edit' ),
1034
				),
1035
				'date_completed' => array(
1036
					'description' => __( "The date the order was completed, in the site's timezone.", 'woocommerce' ),
1037
					'type'        => 'date-time',
1038
					'context'     => array( 'view', 'edit' ),
1039
				),
1040
				'date_paid' => array(
1041
					'description' => __( "The date the order has been paid, in the site's timezone.", 'woocommerce' ),
1042
					'type'        => 'date-time',
1043
					'context'     => array( 'view', 'edit' ),
1044
				),
1045
				'cart_hash' => array(
1046
					'description' => __( 'MD5 hash of cart items to ensure orders are not modified.', 'woocommerce' ),
1047
					'type'        => 'string',
1048
					'context'     => array( 'view', 'edit' ),
1049
					'readonly'    => true,
1050
				),
1051
				'number' => array(
1052
					'description' => __( 'Order number.', 'woocommerce' ),
1053
					'type'        => 'string',
1054
					'context'     => array( 'view', 'edit' ),
1055
					'readonly'    => true,
1056
				),
1057
				'meta_data' => array(
1058
					'description' => __( 'Order meta data.', 'woocommerce' ),
1059
					'type'        => 'array',
1060
					'context'     => array( 'view', 'edit' ),
1061
					'properties'  => array(
1062
						'id' => array(
1063
							'description' => __( 'Meta ID.', 'woocommerce' ),
1064
							'type'        => 'int',
1065
							'context'     => array( 'view', 'edit' ),
1066
							'readonly'    => true,
1067
						),
1068
						'key' => array(
1069
							'description' => __( 'Meta key.', 'woocommerce' ),
1070
							'type'        => 'string',
1071
							'context'     => array( 'view', 'edit' ),
1072
						),
1073
						'value' => array(
1074
							'description' => __( 'Meta value.', 'woocommerce' ),
1075
							'type'        => 'string',
1076
							'context'     => array( 'view', 'edit' ),
1077
						),
1078
					),
1079
				),
1080
				'line_items' => array(
1081
					'description' => __( 'Line items data.', 'woocommerce' ),
1082
					'type'        => 'array',
1083
					'context'     => array( 'view', 'edit' ),
1084
					'properties'  => array(
1085
						'id' => array(
1086
							'description' => __( 'Item ID.', 'woocommerce' ),
1087
							'type'        => 'integer',
1088
							'context'     => array( 'view', 'edit' ),
1089
							'readonly'    => true,
1090
						),
1091
						'name' => array(
1092
							'description' => __( 'Product name.', 'woocommerce' ),
1093
							'type'        => 'string',
1094
							'context'     => array( 'view', 'edit' ),
1095
						),
1096
						'product_id' => array(
1097
							'description' => __( 'Product ID.', 'woocommerce' ),
1098
							'type'        => 'integer',
1099
							'context'     => array( 'view', 'edit' ),
1100
						),
1101
						'variation_id' => array(
1102
							'description' => __( 'Variation ID, if applicable.', 'woocommerce' ),
1103
							'type'        => 'integer',
1104
							'context'     => array( 'view', 'edit' ),
1105
						),
1106
						'quantity' => array(
1107
							'description' => __( 'Quantity ordered.', 'woocommerce' ),
1108
							'type'        => 'integer',
1109
							'context'     => array( 'view', 'edit' ),
1110
						),
1111
						'tax_class' => array(
1112
							'description' => __( 'Tax class of product.', 'woocommerce' ),
1113
							'type'        => 'integer',
1114
							'context'     => array( 'view', 'edit' ),
1115
						),
1116
						'subtotal' => array(
1117
							'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ),
1118
							'type'        => 'string',
1119
							'context'     => array( 'view', 'edit' ),
1120
						),
1121
						'subtotal_tax' => array(
1122
							'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ),
1123
							'type'        => 'string',
1124
							'context'     => array( 'view', 'edit' ),
1125
							'readonly'    => true,
1126
						),
1127
						'total' => array(
1128
							'description' => __( 'Line total (after discounts).', 'woocommerce' ),
1129
							'type'        => 'string',
1130
							'context'     => array( 'view', 'edit' ),
1131
						),
1132
						'total_tax' => array(
1133
							'description' => __( 'Line total tax (after discounts).', 'woocommerce' ),
1134
							'type'        => 'string',
1135
							'context'     => array( 'view', 'edit' ),
1136
							'readonly'    => true,
1137
						),
1138
						'taxes' => array(
1139
							'description' => __( 'Line taxes.', 'woocommerce' ),
1140
							'type'        => 'array',
1141
							'context'     => array( 'view', 'edit' ),
1142
							'readonly'    => true,
1143
							'properties'  => array(
1144
								'id' => array(
1145
									'description' => __( 'Tax rate ID.', 'woocommerce' ),
1146
									'type'        => 'integer',
1147
									'context'     => array( 'view', 'edit' ),
1148
								),
1149
								'total' => array(
1150
									'description' => __( 'Tax total.', 'woocommerce' ),
1151
									'type'        => 'string',
1152
									'context'     => array( 'view', 'edit' ),
1153
								),
1154
								'subtotal' => array(
1155
									'description' => __( 'Tax subtotal.', 'woocommerce' ),
1156
									'type'        => 'string',
1157
									'context'     => array( 'view', 'edit' ),
1158
								),
1159
							),
1160
						),
1161
						'meta_data' => array(
1162
							'description' => __( 'Order item meta data.', 'woocommerce' ),
1163
							'type'        => 'array',
1164
							'context'     => array( 'view', 'edit' ),
1165
							'properties'  => array(
1166
								'id' => array(
1167
									'description' => __( 'Meta ID.', 'woocommerce' ),
1168
									'type'        => 'int',
1169
									'context'     => array( 'view', 'edit' ),
1170
									'readonly'    => true,
1171
								),
1172
								'key' => array(
1173
									'description' => __( 'Meta key.', 'woocommerce' ),
1174
									'type'        => 'string',
1175
									'context'     => array( 'view', 'edit' ),
1176
								),
1177
								'value' => array(
1178
									'description' => __( 'Meta value.', 'woocommerce' ),
1179
									'type'        => 'string',
1180
									'context'     => array( 'view', 'edit' ),
1181
								),
1182
							),
1183
						),
1184
						'sku' => array(
1185
							'description' => __( 'Product SKU.', 'woocommerce' ),
1186
							'type'        => 'string',
1187
							'context'     => array( 'view', 'edit' ),
1188
							'readonly'    => true,
1189
						),
1190
						'price' => array(
1191
							'description' => __( 'Product price.', 'woocommerce' ),
1192
							'type'        => 'string',
1193
							'context'     => array( 'view', 'edit' ),
1194
							'readonly'    => true,
1195
						),
1196
						'meta' => array(
1197
							'description' => __( 'Order item meta data (formatted).', 'woocommerce' ),
1198
							'type'        => 'array',
1199
							'context'     => array( 'view', 'edit' ),
1200
							'readonly'    => true,
1201
							'properties'  => array(
1202
								'key' => array(
1203
									'description' => __( 'Meta key.', 'woocommerce' ),
1204
									'type'        => 'string',
1205
									'context'     => array( 'view', 'edit' ),
1206
									'readonly'    => true,
1207
								),
1208
								'label' => array(
1209
									'description' => __( 'Meta label.', 'woocommerce' ),
1210
									'type'        => 'string',
1211
									'context'     => array( 'view', 'edit' ),
1212
									'readonly'    => true,
1213
								),
1214
								'value' => array(
1215
									'description' => __( 'Meta value.', 'woocommerce' ),
1216
									'type'        => 'string',
1217
									'context'     => array( 'view', 'edit' ),
1218
									'readonly'    => true,
1219
								),
1220
							),
1221
						),
1222
					),
1223
				),
1224
				'tax_lines' => array(
1225
					'description' => __( 'Tax lines data.', 'woocommerce' ),
1226
					'type'        => 'array',
1227
					'context'     => array( 'view', 'edit' ),
1228
					'readonly'    => true,
1229
					'properties'  => array(
1230
						'id' => array(
1231
							'description' => __( 'Item ID.', 'woocommerce' ),
1232
							'type'        => 'integer',
1233
							'context'     => array( 'view', 'edit' ),
1234
							'readonly'    => true,
1235
						),
1236
						'rate_code' => array(
1237
							'description' => __( 'Tax rate code.', 'woocommerce' ),
1238
							'type'        => 'string',
1239
							'context'     => array( 'view', 'edit' ),
1240
							'readonly'    => true,
1241
						),
1242
						'rate_id' => array(
1243
							'description' => __( 'Tax rate ID.', 'woocommerce' ),
1244
							'type'        => 'string',
1245
							'context'     => array( 'view', 'edit' ),
1246
							'readonly'    => true,
1247
						),
1248
						'label' => array(
1249
							'description' => __( 'Tax rate label.', 'woocommerce' ),
1250
							'type'        => 'string',
1251
							'context'     => array( 'view', 'edit' ),
1252
							'readonly'    => true,
1253
						),
1254
						'compound' => array(
1255
							'description' => __( 'Show if is a compound tax rate.', 'woocommerce' ),
1256
							'type'        => 'boolean',
1257
							'context'     => array( 'view', 'edit' ),
1258
							'readonly'    => true,
1259
						),
1260
						'tax_total' => array(
1261
							'description' => __( 'Tax total (not including shipping taxes).', 'woocommerce' ),
1262
							'type'        => 'string',
1263
							'context'     => array( 'view', 'edit' ),
1264
							'readonly'    => true,
1265
						),
1266
						'shipping_tax_total' => array(
1267
							'description' => __( 'Shipping tax total.', 'woocommerce' ),
1268
							'type'        => 'string',
1269
							'context'     => array( 'view', 'edit' ),
1270
							'readonly'    => true,
1271
						),
1272
						'meta_data' => array(
1273
							'description' => __( 'Order item meta data.', 'woocommerce' ),
1274
							'type'        => 'array',
1275
							'context'     => array( 'view', 'edit' ),
1276
							'properties'  => array(
1277
								'id' => array(
1278
									'description' => __( 'Meta ID.', 'woocommerce' ),
1279
									'type'        => 'int',
1280
									'context'     => array( 'view', 'edit' ),
1281
									'readonly'    => true,
1282
								),
1283
								'key' => array(
1284
									'description' => __( 'Meta key.', 'woocommerce' ),
1285
									'type'        => 'string',
1286
									'context'     => array( 'view', 'edit' ),
1287
								),
1288
								'value' => array(
1289
									'description' => __( 'Meta value.', 'woocommerce' ),
1290
									'type'        => 'string',
1291
									'context'     => array( 'view', 'edit' ),
1292
								),
1293
							),
1294
						),
1295
					),
1296
				),
1297
				'shipping_lines' => array(
1298
					'description' => __( 'Shipping lines data.', 'woocommerce' ),
1299
					'type'        => 'array',
1300
					'context'     => array( 'view', 'edit' ),
1301
					'properties'  => array(
1302
						'id' => array(
1303
							'description' => __( 'Item ID.', 'woocommerce' ),
1304
							'type'        => 'integer',
1305
							'context'     => array( 'view', 'edit' ),
1306
							'readonly'    => true,
1307
						),
1308
						'method_title' => array(
1309
							'description' => __( 'Shipping method name.', 'woocommerce' ),
1310
							'type'        => 'string',
1311
							'context'     => array( 'view', 'edit' ),
1312
						),
1313
						'method_id' => array(
1314
							'description' => __( 'Shipping method ID.', 'woocommerce' ),
1315
							'type'        => 'string',
1316
							'context'     => array( 'view', 'edit' ),
1317
						),
1318
						'total' => array(
1319
							'description' => __( 'Line total (after discounts).', 'woocommerce' ),
1320
							'type'        => 'string',
1321
							'context'     => array( 'view', 'edit' ),
1322
						),
1323
						'total_tax' => array(
1324
							'description' => __( 'Line total tax (after discounts).', 'woocommerce' ),
1325
							'type'        => 'string',
1326
							'context'     => array( 'view', 'edit' ),
1327
							'readonly'    => true,
1328
						),
1329
						'taxes' => array(
1330
							'description' => __( 'Line taxes.', 'woocommerce' ),
1331
							'type'        => 'array',
1332
							'context'     => array( 'view', 'edit' ),
1333
							'readonly'    => true,
1334
							'properties'  => array(
1335
								'id' => array(
1336
									'description' => __( 'Tax rate ID.', 'woocommerce' ),
1337
									'type'        => 'integer',
1338
									'context'     => array( 'view', 'edit' ),
1339
									'readonly'    => true,
1340
								),
1341
								'total' => array(
1342
									'description' => __( 'Tax total.', 'woocommerce' ),
1343
									'type'        => 'string',
1344
									'context'     => array( 'view', 'edit' ),
1345
									'readonly'    => true,
1346
								),
1347
							),
1348
						),
1349
						'meta_data' => array(
1350
							'description' => __( 'Order item meta data.', 'woocommerce' ),
1351
							'type'        => 'array',
1352
							'context'     => array( 'view', 'edit' ),
1353
							'properties'  => array(
1354
								'id' => array(
1355
									'description' => __( 'Meta ID.', 'woocommerce' ),
1356
									'type'        => 'int',
1357
									'context'     => array( 'view', 'edit' ),
1358
									'readonly'    => true,
1359
								),
1360
								'key' => array(
1361
									'description' => __( 'Meta key.', 'woocommerce' ),
1362
									'type'        => 'string',
1363
									'context'     => array( 'view', 'edit' ),
1364
								),
1365
								'value' => array(
1366
									'description' => __( 'Meta value.', 'woocommerce' ),
1367
									'type'        => 'string',
1368
									'context'     => array( 'view', 'edit' ),
1369
								),
1370
							),
1371
						),
1372
					),
1373
				),
1374
				'fee_lines' => array(
1375
					'description' => __( 'Fee lines data.', 'woocommerce' ),
1376
					'type'        => 'array',
1377
					'context'     => array( 'view', 'edit' ),
1378
					'properties'  => array(
1379
						'id' => array(
1380
							'description' => __( 'Item ID.', 'woocommerce' ),
1381
							'type'        => 'integer',
1382
							'context'     => array( 'view', 'edit' ),
1383
							'readonly'    => true,
1384
						),
1385
						'name' => array(
1386
							'description' => __( 'Fee name.', 'woocommerce' ),
1387
							'type'        => 'string',
1388
							'context'     => array( 'view', 'edit' ),
1389
						),
1390
						'tax_class' => array(
1391
							'description' => __( 'Tax class of fee.', 'woocommerce' ),
1392
							'type'        => 'string',
1393
							'context'     => array( 'view', 'edit' ),
1394
						),
1395
						'tax_status' => array(
1396
							'description' => __( 'Tax status of fee.', 'woocommerce' ),
1397
							'type'        => 'string',
1398
							'context'     => array( 'view', 'edit' ),
1399
							'enum'        => array( 'taxable', 'none' ),
1400
						),
1401
						'total' => array(
1402
							'description' => __( 'Line total (after discounts).', 'woocommerce' ),
1403
							'type'        => 'string',
1404
							'context'     => array( 'view', 'edit' ),
1405
						),
1406
						'total_tax' => array(
1407
							'description' => __( 'Line total tax (after discounts).', 'woocommerce' ),
1408
							'type'        => 'string',
1409
							'context'     => array( 'view', 'edit' ),
1410
							'readonly'    => true,
1411
						),
1412
						'taxes' => array(
1413
							'description' => __( 'Line taxes.', 'woocommerce' ),
1414
							'type'        => 'array',
1415
							'context'     => array( 'view', 'edit' ),
1416
							'readonly'    => true,
1417
							'properties'  => array(
1418
								'id' => array(
1419
									'description' => __( 'Tax rate ID.', 'woocommerce' ),
1420
									'type'        => 'integer',
1421
									'context'     => array( 'view', 'edit' ),
1422
									'readonly'    => true,
1423
								),
1424
								'total' => array(
1425
									'description' => __( 'Tax total.', 'woocommerce' ),
1426
									'type'        => 'string',
1427
									'context'     => array( 'view', 'edit' ),
1428
									'readonly'    => true,
1429
								),
1430
								'subtotal' => array(
1431
									'description' => __( 'Tax subtotal.', 'woocommerce' ),
1432
									'type'        => 'string',
1433
									'context'     => array( 'view', 'edit' ),
1434
									'readonly'    => true,
1435
								),
1436
							),
1437
						),
1438
						'meta_data' => array(
1439
							'description' => __( 'Order item meta data.', 'woocommerce' ),
1440
							'type'        => 'array',
1441
							'context'     => array( 'view', 'edit' ),
1442
							'properties'  => array(
1443
								'id' => array(
1444
									'description' => __( 'Meta ID.', 'woocommerce' ),
1445
									'type'        => 'int',
1446
									'context'     => array( 'view', 'edit' ),
1447
									'readonly'    => true,
1448
								),
1449
								'key' => array(
1450
									'description' => __( 'Meta key.', 'woocommerce' ),
1451
									'type'        => 'string',
1452
									'context'     => array( 'view', 'edit' ),
1453
								),
1454
								'value' => array(
1455
									'description' => __( 'Meta value.', 'woocommerce' ),
1456
									'type'        => 'string',
1457
									'context'     => array( 'view', 'edit' ),
1458
								),
1459
							),
1460
						),
1461
					),
1462
				),
1463
				'coupon_lines' => array(
1464
					'description' => __( 'Coupons line data.', 'woocommerce' ),
1465
					'type'        => 'array',
1466
					'context'     => array( 'view', 'edit' ),
1467
					'properties'  => array(
1468
						'id' => array(
1469
							'description' => __( 'Item ID.', 'woocommerce' ),
1470
							'type'        => 'integer',
1471
							'context'     => array( 'view', 'edit' ),
1472
							'readonly'    => true,
1473
						),
1474
						'code' => array(
1475
							'description' => __( 'Coupon code.', 'woocommerce' ),
1476
							'type'        => 'string',
1477
							'context'     => array( 'view', 'edit' ),
1478
						),
1479
						'discount' => array(
1480
							'description' => __( 'Discount total.', 'woocommerce' ),
1481
							'type'        => 'string',
1482
							'context'     => array( 'view', 'edit' ),
1483
						),
1484
						'discount_tax' => array(
1485
							'description' => __( 'Discount total tax.', 'woocommerce' ),
1486
							'type'        => 'string',
1487
							'context'     => array( 'view', 'edit' ),
1488
							'readonly'    => true,
1489
						),
1490
						'meta_data' => array(
1491
							'description' => __( 'Order item meta data.', 'woocommerce' ),
1492
							'type'        => 'array',
1493
							'context'     => array( 'view', 'edit' ),
1494
							'properties'  => array(
1495
								'id' => array(
1496
									'description' => __( 'Meta ID.', 'woocommerce' ),
1497
									'type'        => 'int',
1498
									'context'     => array( 'view', 'edit' ),
1499
									'readonly'    => true,
1500
								),
1501
								'key' => array(
1502
									'description' => __( 'Meta key.', 'woocommerce' ),
1503
									'type'        => 'string',
1504
									'context'     => array( 'view', 'edit' ),
1505
								),
1506
								'value' => array(
1507
									'description' => __( 'Meta value.', 'woocommerce' ),
1508
									'type'        => 'string',
1509
									'context'     => array( 'view', 'edit' ),
1510
								),
1511
							),
1512
						),
1513
					),
1514
				),
1515
				'refunds' => array(
1516
					'description' => __( 'List of refunds.', 'woocommerce' ),
1517
					'type'        => 'array',
1518
					'context'     => array( 'view', 'edit' ),
1519
					'readonly'    => true,
1520
					'properties'  => array(
1521
						'id' => array(
1522
							'description' => __( 'Refund ID.', 'woocommerce' ),
1523
							'type'        => 'integer',
1524
							'context'     => array( 'view', 'edit' ),
1525
							'readonly'    => true,
1526
						),
1527
						'reason' => array(
1528
							'description' => __( 'Refund reason.', 'woocommerce' ),
1529
							'type'        => 'string',
1530
							'context'     => array( 'view', 'edit' ),
1531
							'readonly'    => true,
1532
						),
1533
						'total' => array(
1534
							'description' => __( 'Refund total.', 'woocommerce' ),
1535
							'type'        => 'string',
1536
							'context'     => array( 'view', 'edit' ),
1537
							'readonly'    => true,
1538
						),
1539
					),
1540
				),
1541
				'set_paid' => array(
1542
					'description' => __( 'Define if the order is paid. It will set the status to processing and reduce stock items.', 'woocommerce' ),
1543
					'type'        => 'boolean',
1544
					'default'     => false,
1545
					'context'     => array( 'edit' ),
1546
				),
1547
			),
1548
		);
1549
1550
		return $this->add_additional_fields_schema( $schema );
1551
	}
1552
1553
	/**
1554
	 * Get the query params for collections.
1555
	 *
1556
	 * @return array
1557
	 */
1558
	public function get_collection_params() {
1559
		$params = parent::get_collection_params();
1560
1561
		$params['status'] = array(
1562
			'default'           => 'any',
1563
			'description'       => __( 'Limit result set to orders assigned a specific status.', 'woocommerce' ),
1564
			'type'              => 'string',
1565
			'enum'              => array_merge( array( 'any' ), $this->get_order_statuses() ),
1566
			'sanitize_callback' => 'sanitize_key',
1567
			'validate_callback' => 'rest_validate_request_arg',
1568
		);
1569
		$params['customer'] = array(
1570
			'description'       => __( 'Limit result set to orders assigned a specific customer.', 'woocommerce' ),
1571
			'type'              => 'integer',
1572
			'sanitize_callback' => 'absint',
1573
			'validate_callback' => 'rest_validate_request_arg',
1574
		);
1575
		$params['product'] = array(
1576
			'description'       => __( 'Limit result set to orders assigned a specific product.', 'woocommerce' ),
1577
			'type'              => 'integer',
1578
			'sanitize_callback' => 'absint',
1579
			'validate_callback' => 'rest_validate_request_arg',
1580
		);
1581
		$params['dp'] = array(
1582
			'default'           => 2,
1583
			'description'       => __( 'Number of decimal points to use in each resource.', 'woocommerce' ),
1584
			'type'              => 'integer',
1585
			'sanitize_callback' => 'absint',
1586
			'validate_callback' => 'rest_validate_request_arg',
1587
		);
1588
1589
		return $params;
1590
	}
1591
}
1592