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

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