· 5 years ago · Jan 14, 2021, 07:40 PM
1<?php
2/**
3 * REST API Order Refunds controller
4 *
5 * Handles requests to the /orders/<order_id>/refunds endpoint.
6 *
7 * @author WooThemes
8 * @category API
9 * @package WooCommerce\RestApi
10 * @since 2.6.0
11 */
12
13if ( ! defined( 'ABSPATH' ) ) {
14 exit;
15}
16
17/**
18 * REST API Order Refunds controller class.
19 *
20 * @package WooCommerce\RestApi
21 * @extends WC_REST_Orders_V1_Controller
22 */
23class WC_REST_Order_Refunds_V1_Controller extends WC_REST_Orders_V1_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/(?P<order_id>[\d]+)/refunds';
38
39 /**
40 * Post type.
41 *
42 * @var string
43 */
44 protected $post_type = 'shop_order_refund';
45
46 /**
47 * Order refunds actions.
48 */
49 public function __construct() {
50 add_filter( "woocommerce_rest_{$this->post_type}_trashable", '__return_false' );
51 add_filter( "woocommerce_rest_{$this->post_type}_query", array( $this, 'query_args' ), 10, 2 );
52 }
53
54 /**
55 * Register the routes for order refunds.
56 */
57 public function register_routes() {
58 register_rest_route( $this->namespace, '/' . $this->rest_base, array(
59 'args' => array(
60 'order_id' => array(
61 'description' => __( 'The order ID.', 'woocommerce' ),
62 'type' => 'integer',
63 ),
64 ),
65 array(
66 'methods' => WP_REST_Server::READABLE,
67 'callback' => array( $this, 'get_items' ),
68 'permission_callback' => array( $this, 'get_items_permissions_check' ),
69 'args' => $this->get_collection_params(),
70 ),
71 array(
72 'methods' => WP_REST_Server::CREATABLE,
73 'callback' => array( $this, 'create_item' ),
74 'permission_callback' => array( $this, 'create_item_permissions_check' ),
75 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
76 ),
77 'schema' => array( $this, 'get_public_item_schema' ),
78 ) );
79
80 register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
81 'args' => array(
82 'order_id' => array(
83 'description' => __( 'The order ID.', 'woocommerce' ),
84 'type' => 'integer',
85 ),
86 'id' => array(
87 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
88 'type' => 'integer',
89 ),
90 ),
91 array(
92 'methods' => WP_REST_Server::READABLE,
93 'callback' => array( $this, 'get_item' ),
94 'permission_callback' => array( $this, 'get_item_permissions_check' ),
95 'args' => array(
96 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
97 ),
98 ),
99 array(
100 'methods' => WP_REST_Server::DELETABLE,
101 'callback' => array( $this, 'delete_item' ),
102 'permission_callback' => array( $this, 'delete_item_permissions_check' ),
103 'args' => array(
104 'force' => array(
105 'default' => true,
106 'type' => 'boolean',
107 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ),
108 ),
109 ),
110 ),
111 'schema' => array( $this, 'get_public_item_schema' ),
112 ) );
113 }
114
115 /**
116 * Prepare a single order refund output for response.
117 *
118 * @param WP_Post $post Post object.
119 * @param WP_REST_Request $request Request object.
120 *
121 * @return WP_Error|WP_REST_Response
122 */
123 public function prepare_item_for_response( $post, $request ) {
124 $order = wc_get_order( (int) $request['order_id'] );
125
126 if ( ! $order ) {
127 return new WP_Error( 'woocommerce_rest_invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), 404 );
128 }
129
130 $refund = wc_get_order( $post );
131
132 if ( ! $refund || $refund->get_parent_id() !== $order->get_id() ) {
133 return new WP_Error( 'woocommerce_rest_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 404 );
134 }
135
136 $dp = is_null( $request['dp'] ) ? wc_get_price_decimals() : absint( $request['dp'] );
137
138 $data = array(
139 'id' => $refund->get_id(),
140 'date_created' => wc_rest_prepare_date_response( $refund->get_date_created() ),
141 'amount' => wc_format_decimal( $refund->get_amount(), $dp ),
142 'reason' => $refund->get_reason(),
143 'line_items' => array(),
144 );
145
146 // Add line items.
147 foreach ( $refund->get_items() as $item_id => $item ) {
148 $product = $item->get_product();
149 $product_id = 0;
150 $variation_id = 0;
151 $product_sku = null;
152
153 // Check if the product exists.
154 if ( is_object( $product ) ) {
155 $product_id = $item->get_product_id();
156 $variation_id = $item->get_variation_id();
157 $product_sku = $product->get_sku();
158 }
159
160 $item_meta = array();
161
162 $hideprefix = 'true' === $request['all_item_meta'] ? null : '_';
163
164 foreach ( $item->get_formatted_meta_data( $hideprefix, true ) as $meta_key => $formatted_meta ) {
165 $item_meta[] = array(
166 'key' => $formatted_meta->key,
167 'label' => $formatted_meta->display_key,
168 'value' => wc_clean( $formatted_meta->display_value ),
169 );
170 }
171
172 $line_item = array(
173 'id' => $item_id,
174 'name' => $item['name'],
175 'sku' => $product_sku,
176 'product_id' => (int) $product_id,
177 'variation_id' => (int) $variation_id,
178 'quantity' => wc_stock_amount( $item['qty'] ),
179 'tax_class' => ! empty( $item['tax_class'] ) ? $item['tax_class'] : '',
180 'price' => wc_format_decimal( $refund->get_item_total( $item, false, false ), $dp ),
181 'subtotal' => wc_format_decimal( $refund->get_line_subtotal( $item, false, false ), $dp ),
182 'subtotal_tax' => wc_format_decimal( $item['line_subtotal_tax'], $dp ),
183 'total' => wc_format_decimal( $refund->get_line_total( $item, false, false ), $dp ),
184 'total_tax' => wc_format_decimal( $item['line_tax'], $dp ),
185 'taxes' => array(),
186 'meta' => $item_meta,
187 );
188
189 $item_line_taxes = maybe_unserialize( $item['line_tax_data'] );
190 if ( isset( $item_line_taxes['total'] ) ) {
191 $line_tax = array();
192
193 foreach ( $item_line_taxes['total'] as $tax_rate_id => $tax ) {
194 $line_tax[ $tax_rate_id ] = array(
195 'id' => $tax_rate_id,
196 'total' => $tax,
197 'subtotal' => '',
198 );
199 }
200
201 foreach ( $item_line_taxes['subtotal'] as $tax_rate_id => $tax ) {
202 $line_tax[ $tax_rate_id ]['subtotal'] = $tax;
203 }
204
205 $line_item['taxes'] = array_values( $line_tax );
206 }
207
208 $data['line_items'][] = $line_item;
209 }
210
211 $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
212 $data = $this->add_additional_fields_to_object( $data, $request );
213 $data = $this->filter_response_by_context( $data, $context );
214
215 // Wrap the data in a response object.
216 $response = rest_ensure_response( $data );
217
218 $response->add_links( $this->prepare_links( $refund, $request ) );
219
220 /**
221 * Filter the data for a response.
222 *
223 * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
224 * prepared for the response.
225 *
226 * @param WP_REST_Response $response The response object.
227 * @param WP_Post $post Post object.
228 * @param WP_REST_Request $request Request object.
229 */
230 return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request );
231 }
232
233 /**
234 * Prepare links for the request.
235 *
236 * @param WC_Order_Refund $refund Comment object.
237 * @param WP_REST_Request $request Request object.
238 * @return array Links for the given order refund.
239 */
240 protected function prepare_links( $refund, $request ) {
241 $order_id = $refund->get_parent_id();
242 $base = str_replace( '(?P<order_id>[\d]+)', $order_id, $this->rest_base );
243 $links = array(
244 'self' => array(
245 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $refund->get_id() ) ),
246 ),
247 'collection' => array(
248 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ),
249 ),
250 'up' => array(
251 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $order_id ) ),
252 ),
253 );
254
255 return $links;
256 }
257
258 /**
259 * Query args.
260 *
261 * @param array $args Request args.
262 * @param WP_REST_Request $request Request object.
263 * @return array
264 */
265 public function query_args( $args, $request ) {
266 $args['post_status'] = array_keys( wc_get_order_statuses() );
267 $args['post_parent__in'] = array( absint( $request['order_id'] ) );
268
269 return $args;
270 }
271
272 /**
273 * Create a single item.
274 *
275 * @param WP_REST_Request $request Full details about the request.
276 * @return WP_Error|WP_REST_Response
277 */
278 public function create_item( $request ) {
279 if ( ! empty( $request['id'] ) ) {
280 /* translators: %s: post type */
281 return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) );
282 }
283
284 $order_data = get_post( (int) $request['order_id'] );
285
286 if ( empty( $order_data ) ) {
287 return new WP_Error( 'woocommerce_rest_invalid_order', __( 'Order is invalid', 'woocommerce' ), 400 );
288 }
289
290 if ( 0 > $request['amount'] ) {
291 return new WP_Error( 'woocommerce_rest_invalid_order_refund', __( 'Refund amount must be greater than zero.', 'woocommerce' ), 400 );
292 }
293
294 // Create the refund.
295 $refund = wc_create_refund( array(
296 'order_id' => $order_data->ID,
297 'amount' => $request['amount'],
298 'reason' => empty( $request['reason'] ) ? null : $request['reason'],
299 'refund_payment' => is_bool( $request['api_refund'] ) ? $request['api_refund'] : true,
300 'restock_items' => true,
301 ) );
302
303 if ( is_wp_error( $refund ) ) {
304 return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', $refund->get_error_message(), 500 );
305 }
306
307 if ( ! $refund ) {
308 return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 );
309 }
310
311 $post = get_post( $refund->get_id() );
312 $this->update_additional_fields_for_object( $post, $request );
313
314 /**
315 * Fires after a single item is created or updated via the REST API.
316 *
317 * @param WP_Post $post Post object.
318 * @param WP_REST_Request $request Request object.
319 * @param boolean $creating True when creating item, false when updating.
320 */
321 do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true );
322
323 $request->set_param( 'context', 'edit' );
324 $response = $this->prepare_item_for_response( $post, $request );
325 $response = rest_ensure_response( $response );
326 $response->set_status( 201 );
327 $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ) );
328
329 return $response;
330 }
331
332 /**
333 * Get the Order's schema, conforming to JSON Schema.
334 *
335 * @return array
336 */
337 public function get_item_schema() {
338 $schema = array(
339 '$schema' => 'http://json-schema.org/draft-04/schema#',
340 'title' => $this->post_type,
341 'type' => 'object',
342 'properties' => array(
343 'id' => array(
344 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
345 'type' => 'integer',
346 'context' => array( 'view', 'edit' ),
347 'readonly' => true,
348 ),
349 'date_created' => array(
350 'description' => __( "The date the order refund was created, in the site's timezone.", 'woocommerce' ),
351 'type' => 'date-time',
352 'context' => array( 'view', 'edit' ),
353 'readonly' => true,
354 ),
355 'amount' => array(
356 'description' => __( 'Refund amount.', 'woocommerce' ),
357 'type' => 'string',
358 'context' => array( 'view', 'edit' ),
359 ),
360 'reason' => array(
361 'description' => __( 'Reason for refund.', 'woocommerce' ),
362 'type' => 'string',
363 'context' => array( 'view', 'edit' ),
364 ),
365 'line_items' => array(
366 'description' => __( 'Line items data.', 'woocommerce' ),
367 'type' => 'array',
368 'context' => array( 'view', 'edit' ),
369 'readonly' => true,
370 'items' => array(
371 'type' => 'object',
372 'properties' => array(
373 'id' => array(
374 'description' => __( 'Item ID.', 'woocommerce' ),
375 'type' => 'integer',
376 'context' => array( 'view', 'edit' ),
377 'readonly' => true,
378 ),
379 'name' => array(
380 'description' => __( 'Product name.', 'woocommerce' ),
381 'type' => 'mixed',
382 'context' => array( 'view', 'edit' ),
383 'readonly' => true,
384 ),
385 'sku' => array(
386 'description' => __( 'Product SKU.', 'woocommerce' ),
387 'type' => 'string',
388 'context' => array( 'view', 'edit' ),
389 'readonly' => true,
390 ),
391 'product_id' => array(
392 'description' => __( 'Product ID.', 'woocommerce' ),
393 'type' => 'mixed',
394 'context' => array( 'view', 'edit' ),
395 'readonly' => true,
396 ),
397 'variation_id' => array(
398 'description' => __( 'Variation ID, if applicable.', 'woocommerce' ),
399 'type' => 'integer',
400 'context' => array( 'view', 'edit' ),
401 'readonly' => true,
402 ),
403 'quantity' => array(
404 'description' => __( 'Quantity ordered.', 'woocommerce' ),
405 'type' => 'integer',
406 'context' => array( 'view', 'edit' ),
407 'readonly' => true,
408 ),
409 'tax_class' => array(
410 'description' => __( 'Tax class of product.', 'woocommerce' ),
411 'type' => 'string',
412 'context' => array( 'view', 'edit' ),
413 'readonly' => true,
414 ),
415 'price' => array(
416 'description' => __( 'Product price.', 'woocommerce' ),
417 'type' => 'string',
418 'context' => array( 'view', 'edit' ),
419 'readonly' => true,
420 ),
421 'subtotal' => array(
422 'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ),
423 'type' => 'string',
424 'context' => array( 'view', 'edit' ),
425 'readonly' => true,
426 ),
427 'subtotal_tax' => array(
428 'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ),
429 'type' => 'string',
430 'context' => array( 'view', 'edit' ),
431 'readonly' => true,
432 ),
433 'total' => array(
434 'description' => __( 'Line total (after discounts).', 'woocommerce' ),
435 'type' => 'string',
436 'context' => array( 'view', 'edit' ),
437 'readonly' => true,
438 ),
439 'total_tax' => array(
440 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ),
441 'type' => 'string',
442 'context' => array( 'view', 'edit' ),
443 'readonly' => true,
444 ),
445 'taxes' => array(
446 'description' => __( 'Line taxes.', 'woocommerce' ),
447 'type' => 'array',
448 'context' => array( 'view', 'edit' ),
449 'readonly' => true,
450 'items' => array(
451 'type' => 'object',
452 'properties' => array(
453 'id' => array(
454 'description' => __( 'Tax rate ID.', 'woocommerce' ),
455 'type' => 'integer',
456 'context' => array( 'view', 'edit' ),
457 'readonly' => true,
458 ),
459 'total' => array(
460 'description' => __( 'Tax total.', 'woocommerce' ),
461 'type' => 'string',
462 'context' => array( 'view', 'edit' ),
463 'readonly' => true,
464 ),
465 'subtotal' => array(
466 'description' => __( 'Tax subtotal.', 'woocommerce' ),
467 'type' => 'string',
468 'context' => array( 'view', 'edit' ),
469 'readonly' => true,
470 ),
471 ),
472 ),
473 ),
474 'meta' => array(
475 'description' => __( 'Line item meta data.', 'woocommerce' ),
476 'type' => 'array',
477 'context' => array( 'view', 'edit' ),
478 'readonly' => true,
479 'items' => array(
480 'type' => 'object',
481 'properties' => array(
482 'key' => array(
483 'description' => __( 'Meta key.', 'woocommerce' ),
484 'type' => 'string',
485 'context' => array( 'view', 'edit' ),
486 'readonly' => true,
487 ),
488 'label' => array(
489 'description' => __( 'Meta label.', 'woocommerce' ),
490 'type' => 'string',
491 'context' => array( 'view', 'edit' ),
492 'readonly' => true,
493 ),
494 'value' => array(
495 'description' => __( 'Meta value.', 'woocommerce' ),
496 'type' => 'mixed',
497 'context' => array( 'view', 'edit' ),
498 'readonly' => true,
499 ),
500 ),
501 ),
502 ),
503 ),
504 ),
505 ),
506 ),
507 );
508
509 return $this->add_additional_fields_schema( $schema );
510 }
511
512 /**
513 * Get the query params for collections.
514 *
515 * @return array
516 */
517 public function get_collection_params() {
518 $params = parent::get_collection_params();
519
520 $params['dp'] = array(
521 'default' => wc_get_price_decimals(),
522 'description' => __( 'Number of decimal points to use in each resource.', 'woocommerce' ),
523 'type' => 'integer',
524 'sanitize_callback' => 'absint',
525 'validate_callback' => 'rest_validate_request_arg',
526 );
527
528 return $params;
529 }
530}