class-wc-rest-order-refunds-controller.php 17 KB

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