/includes/api/v1/class-wc-rest-coupons-controller.php

  1. <?php 
  2. /** 
  3. * REST API Coupons controller 
  4. * 
  5. * Handles requests to the /coupons endpoint. 
  6. * 
  7. * @author WooThemes 
  8. * @category API 
  9. * @package WooCommerce/API 
  10. * @since 3.0.0 
  11. */ 
  12.  
  13. if ( ! defined( 'ABSPATH' ) ) { 
  14. exit; 
  15.  
  16. /** 
  17. * REST API Coupons controller class. 
  18. * 
  19. * @package WooCommerce/API 
  20. * @extends WC_REST_Posts_Controller 
  21. */ 
  22. class WC_REST_Coupons_V1_Controller extends WC_REST_Posts_Controller { 
  23.  
  24. /** 
  25. * Endpoint namespace. 
  26. * 
  27. * @var string 
  28. */ 
  29. protected $namespace = 'wc/v1'; 
  30.  
  31. /** 
  32. * Route base. 
  33. * 
  34. * @var string 
  35. */ 
  36. protected $rest_base = 'coupons'; 
  37.  
  38. /** 
  39. * Post type. 
  40. * 
  41. * @var string 
  42. */ 
  43. protected $post_type = 'shop_coupon'; 
  44.  
  45. /** 
  46. * Coupons actions. 
  47. */ 
  48. public function __construct() { 
  49. add_filter( "woocommerce_rest_{$this->post_type}_query", array( $this, 'query_args' ), 10, 2 ); 
  50.  
  51. /** 
  52. * Register the routes for coupons. 
  53. */ 
  54. public function register_routes() { 
  55. register_rest_route( $this->namespace, '/' . $this->rest_base, array( 
  56. array( 
  57. 'methods' => WP_REST_Server::READABLE,  
  58. 'callback' => array( $this, 'get_items' ),  
  59. 'permission_callback' => array( $this, 'get_items_permissions_check' ),  
  60. 'args' => $this->get_collection_params(),  
  61. ),  
  62. array( 
  63. 'methods' => WP_REST_Server::CREATABLE,  
  64. 'callback' => array( $this, 'create_item' ),  
  65. 'permission_callback' => array( $this, 'create_item_permissions_check' ),  
  66. 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( 
  67. 'code' => array( 
  68. 'description' => __( 'Coupon code.', 'woocommerce' ),  
  69. 'required' => true,  
  70. 'type' => 'string',  
  71. ),  
  72. ) ),  
  73. ),  
  74. 'schema' => array( $this, 'get_public_item_schema' ),  
  75. ) ); 
  76.  
  77. register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array( 
  78. 'args' => array( 
  79. 'id' => array( 
  80. 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),  
  81. 'type' => 'integer',  
  82. ),  
  83. ),  
  84. array( 
  85. 'methods' => WP_REST_Server::READABLE,  
  86. 'callback' => array( $this, 'get_item' ),  
  87. 'permission_callback' => array( $this, 'get_item_permissions_check' ),  
  88. 'args' => array( 
  89. 'context' => $this->get_context_param( array( 'default' => 'view' ) ),  
  90. ),  
  91. ),  
  92. array( 
  93. 'methods' => WP_REST_Server::EDITABLE,  
  94. 'callback' => array( $this, 'update_item' ),  
  95. 'permission_callback' => array( $this, 'update_item_permissions_check' ),  
  96. 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),  
  97. ),  
  98. array( 
  99. 'methods' => WP_REST_Server::DELETABLE,  
  100. 'callback' => array( $this, 'delete_item' ),  
  101. 'permission_callback' => array( $this, 'delete_item_permissions_check' ),  
  102. 'args' => array( 
  103. 'force' => array( 
  104. 'default' => false,  
  105. 'type' => 'boolean',  
  106. 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ),  
  107. ),  
  108. ),  
  109. ),  
  110. 'schema' => array( $this, 'get_public_item_schema' ),  
  111. ) ); 
  112.  
  113. register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( 
  114. array( 
  115. 'methods' => WP_REST_Server::EDITABLE,  
  116. 'callback' => array( $this, 'batch_items' ),  
  117. 'permission_callback' => array( $this, 'batch_items_permissions_check' ),  
  118. 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),  
  119. ),  
  120. 'schema' => array( $this, 'get_public_batch_schema' ),  
  121. ) ); 
  122.  
  123. /** 
  124. * Query args. 
  125. * 
  126. * @param array $args Query args 
  127. * @param WP_REST_Request $request Request data. 
  128. * @return array 
  129. */ 
  130. public function query_args( $args, $request ) { 
  131. if ( ! empty( $request['code'] ) ) { 
  132. $id = wc_get_coupon_id_by_code( $request['code'] ); 
  133. $args['post__in'] = array( $id ); 
  134.  
  135. return $args; 
  136.  
  137. /** 
  138. * Prepare a single coupon output for response. 
  139. * 
  140. * @param WP_Post $post Post object. 
  141. * @param WP_REST_Request $request Request object. 
  142. * @return WP_REST_Response $data 
  143. */ 
  144. public function prepare_item_for_response( $post, $request ) { 
  145. $coupon = new WC_Coupon( (int) $post->ID ); 
  146. $_data = $coupon->get_data(); 
  147.  
  148. $format_decimal = array( 'amount', 'minimum_amount', 'maximum_amount' ); 
  149. $format_date = array( 'date_created', 'date_modified' ); 
  150. $format_date_utc = array( 'date_expires' ); 
  151. $format_null = array( 'usage_limit', 'usage_limit_per_user' ); 
  152.  
  153. // Format decimal values. 
  154. foreach ( $format_decimal as $key ) { 
  155. $_data[ $key ] = wc_format_decimal( $_data[ $key ], 2 ); 
  156.  
  157. // Format date values. 
  158. foreach ( $format_date as $key ) { 
  159. $_data[ $key ] = $_data[ $key ] ? wc_rest_prepare_date_response( $_data[ $key ], false ) : null; 
  160. foreach ( $format_date_utc as $key ) { 
  161. $_data[ $key ] = $_data[ $key ] ? wc_rest_prepare_date_response( $_data[ $key ] ) : null; 
  162.  
  163. // Format null values. 
  164. foreach ( $format_null as $key ) { 
  165. $_data[ $key ] = $_data[ $key ] ? $_data[ $key ] : null; 
  166.  
  167. $data = array( 
  168. 'id' => $_data['id'],  
  169. 'code' => $_data['code'],  
  170. 'date_created' => $_data['date_created'],  
  171. 'date_modified' => $_data['date_modified'],  
  172. 'discount_type' => $_data['discount_type'],  
  173. 'description' => $_data['description'],  
  174. 'amount' => $_data['amount'],  
  175. 'expiry_date' => $_data['date_expires'],  
  176. 'usage_count' => $_data['usage_count'],  
  177. 'individual_use' => $_data['individual_use'],  
  178. 'product_ids' => $_data['product_ids'],  
  179. 'exclude_product_ids' => $_data['excluded_product_ids'],  
  180. 'usage_limit' => $_data['usage_limit'],  
  181. 'usage_limit_per_user' => $_data['usage_limit_per_user'],  
  182. 'limit_usage_to_x_items' => $_data['limit_usage_to_x_items'],  
  183. 'free_shipping' => $_data['free_shipping'],  
  184. 'product_categories' => $_data['product_categories'],  
  185. 'excluded_product_categories' => $_data['excluded_product_categories'],  
  186. 'exclude_sale_items' => $_data['exclude_sale_items'],  
  187. 'minimum_amount' => $_data['minimum_amount'],  
  188. 'maximum_amount' => $_data['maximum_amount'],  
  189. 'email_restrictions' => $_data['email_restrictions'],  
  190. 'used_by' => $_data['used_by'],  
  191. ); 
  192.  
  193. $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; 
  194. $data = $this->add_additional_fields_to_object( $data, $request ); 
  195. $data = $this->filter_response_by_context( $data, $context ); 
  196. $response = rest_ensure_response( $data ); 
  197. $response->add_links( $this->prepare_links( $post, $request ) ); 
  198.  
  199. /** 
  200. * Filter the data for a response. 
  201. * 
  202. * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being 
  203. * prepared for the response. 
  204. * 
  205. * @param WP_REST_Response $response The response object. 
  206. * @param WP_Post $post Post object. 
  207. * @param WP_REST_Request $request Request object. 
  208. */ 
  209. return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request ); 
  210.  
  211. /** 
  212. * Only reutrn writeable props from schema. 
  213. * @param array $schema 
  214. * @return bool 
  215. */ 
  216. protected function filter_writable_props( $schema ) { 
  217. return empty( $schema['readonly'] ); 
  218.  
  219. /** 
  220. * Prepare a single coupon for create or update. 
  221. * 
  222. * @param WP_REST_Request $request Request object. 
  223. * @return WP_Error|stdClass $data Post object. 
  224. */ 
  225. protected function prepare_item_for_database( $request ) { 
  226. $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; 
  227. $coupon = new WC_Coupon( $id ); 
  228. $schema = $this->get_item_schema(); 
  229. $data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) ); 
  230.  
  231. // Update to schema to make compatible with CRUD schema. 
  232. if ( $request['exclude_product_ids'] ) { 
  233. $request['excluded_product_ids'] = $request['exclude_product_ids']; 
  234. if ( $request['expiry_date'] ) { 
  235. $request['date_expires'] = $request['expiry_date']; 
  236.  
  237. // Validate required POST fields. 
  238. if ( 'POST' === $request->get_method() && 0 === $coupon->get_id() ) { 
  239. if ( empty( $request['code'] ) ) { 
  240. return new WP_Error( 'woocommerce_rest_empty_coupon_code', sprintf( __( 'The coupon code cannot be empty.', 'woocommerce' ), 'code' ), array( 'status' => 400 ) ); 
  241.  
  242. // Handle all writable props. 
  243. foreach ( $data_keys as $key ) { 
  244. $value = $request[ $key ]; 
  245.  
  246. if ( ! is_null( $value ) ) { 
  247. switch ( $key ) { 
  248. case 'code' : 
  249. $coupon_code = wc_format_coupon_code( $value ); 
  250. $id = $coupon->get_id() ? $coupon->get_id() : 0; 
  251. $id_from_code = wc_get_coupon_id_by_code( $coupon_code, $id ); 
  252.  
  253. if ( $id_from_code ) { 
  254. return new WP_Error( 'woocommerce_rest_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), array( 'status' => 400 ) ); 
  255.  
  256. $coupon->set_code( $coupon_code ); 
  257. break; 
  258. case 'description' : 
  259. $coupon->set_description( wp_filter_post_kses( $value ) ); 
  260. break; 
  261. case 'expiry_date' : 
  262. $coupon->set_date_expires( $value ); 
  263. break; 
  264. default : 
  265. if ( is_callable( array( $coupon, "set_{$key}" ) ) ) { 
  266. $coupon->{"set_{$key}"}( $value ); 
  267. break; 
  268.  
  269. /** 
  270. * Filter the query_vars used in `get_items` for the constructed query. 
  271. * 
  272. * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being 
  273. * prepared for insertion. 
  274. * 
  275. * @param WC_Coupon $coupon The coupon object. 
  276. * @param WP_REST_Request $request Request object. 
  277. */ 
  278. return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $coupon, $request ); 
  279.  
  280. /** 
  281. * Create a single item. 
  282. * 
  283. * @param WP_REST_Request $request Full details about the request. 
  284. * @return WP_Error|WP_REST_Response 
  285. */ 
  286. public function create_item( $request ) { 
  287. if ( ! empty( $request['id'] ) ) { 
  288. /** translators: %s: post type */ 
  289. return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); 
  290.  
  291. $coupon_id = $this->save_coupon( $request ); 
  292. if ( is_wp_error( $coupon_id ) ) { 
  293. return $coupon_id; 
  294.  
  295. $post = get_post( $coupon_id ); 
  296. $this->update_additional_fields_for_object( $post, $request ); 
  297.  
  298. $this->add_post_meta_fields( $post, $request ); 
  299.  
  300. /** 
  301. * Fires after a single item is created or updated via the REST API. 
  302. * 
  303. * @param WP_Post $post Post object. 
  304. * @param WP_REST_Request $request Request object. 
  305. * @param boolean $creating True when creating item, false when updating. 
  306. */ 
  307. do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true ); 
  308. $request->set_param( 'context', 'edit' ); 
  309. $response = $this->prepare_item_for_response( $post, $request ); 
  310. $response = rest_ensure_response( $response ); 
  311. $response->set_status( 201 ); 
  312. $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ) ); 
  313.  
  314. return $response; 
  315.  
  316. /** 
  317. * Update a single coupon. 
  318. * 
  319. * @param WP_REST_Request $request Full details about the request. 
  320. * @return WP_Error|WP_REST_Response 
  321. */ 
  322. public function update_item( $request ) { 
  323. try { 
  324. $post_id = (int) $request['id']; 
  325.  
  326. if ( empty( $post_id ) || get_post_type( $post_id ) !== $this->post_type ) { 
  327. return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); 
  328.  
  329. $coupon_id = $this->save_coupon( $request ); 
  330. if ( is_wp_error( $coupon_id ) ) { 
  331. return $coupon_id; 
  332.  
  333. $post = get_post( $coupon_id ); 
  334. $this->update_additional_fields_for_object( $post, $request ); 
  335.  
  336. /** 
  337. * Fires after a single item is created or updated via the REST API. 
  338. * 
  339. * @param WP_Post $post Post object. 
  340. * @param WP_REST_Request $request Request object. 
  341. * @param boolean $creating True when creating item, false when updating. 
  342. */ 
  343. do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, false ); 
  344. $request->set_param( 'context', 'edit' ); 
  345. $response = $this->prepare_item_for_response( $post, $request ); 
  346. return rest_ensure_response( $response ); 
  347.  
  348. } catch ( Exception $e ) { 
  349. return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); 
  350.  
  351. /** 
  352. * Saves a coupon to the database. 
  353. * 
  354. * @since 3.0.0 
  355. * @param WP_REST_Request $request Full details about the request. 
  356. * @return WP_Error|int 
  357. */ 
  358. protected function save_coupon( $request ) { 
  359. try { 
  360. $coupon = $this->prepare_item_for_database( $request ); 
  361.  
  362. if ( is_wp_error( $coupon ) ) { 
  363. return $coupon; 
  364.  
  365. $coupon->save(); 
  366. return $coupon->get_id(); 
  367. } catch ( WC_Data_Exception $e ) { 
  368. return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); 
  369. } catch ( WC_REST_Exception $e ) { 
  370. return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); 
  371.  
  372. /** 
  373. * Get the Coupon's schema, conforming to JSON Schema. 
  374. * 
  375. * @return array 
  376. */ 
  377. public function get_item_schema() { 
  378. $schema = array( 
  379. '$schema' => 'http://json-schema.org/draft-04/schema#',  
  380. 'title' => $this->post_type,  
  381. 'type' => 'object',  
  382. 'properties' => array( 
  383. 'id' => array( 
  384. 'description' => __( 'Unique identifier for the object.', 'woocommerce' ),  
  385. 'type' => 'integer',  
  386. 'context' => array( 'view', 'edit' ),  
  387. 'readonly' => true,  
  388. ),  
  389. 'code' => array( 
  390. 'description' => __( 'Coupon code.', 'woocommerce' ),  
  391. 'type' => 'string',  
  392. 'context' => array( 'view', 'edit' ),  
  393. ),  
  394. 'date_created' => array( 
  395. 'description' => __( "The date the coupon was created, in the site's timezone.", 'woocommerce' ),  
  396. 'type' => 'date-time',  
  397. 'context' => array( 'view', 'edit' ),  
  398. 'readonly' => true,  
  399. ),  
  400. 'date_modified' => array( 
  401. 'description' => __( "The date the coupon was last modified, in the site's timezone.", 'woocommerce' ),  
  402. 'type' => 'date-time',  
  403. 'context' => array( 'view', 'edit' ),  
  404. 'readonly' => true,  
  405. ),  
  406. 'description' => array( 
  407. 'description' => __( 'Coupon description.', 'woocommerce' ),  
  408. 'type' => 'string',  
  409. 'context' => array( 'view', 'edit' ),  
  410. ),  
  411. 'discount_type' => array( 
  412. 'description' => __( 'Determines the type of discount that will be applied.', 'woocommerce' ),  
  413. 'type' => 'string',  
  414. 'default' => 'fixed_cart',  
  415. 'enum' => array_keys( wc_get_coupon_types() ),  
  416. 'context' => array( 'view', 'edit' ),  
  417. ),  
  418. 'amount' => array( 
  419. 'description' => __( 'The amount of discount. Should always be numeric, even if setting a percentage.', 'woocommerce' ),  
  420. 'type' => 'string',  
  421. 'context' => array( 'view', 'edit' ),  
  422. ),  
  423. 'expiry_date' => array( 
  424. 'description' => __( 'UTC DateTime when the coupon expires.', 'woocommerce' ),  
  425. 'type' => 'string',  
  426. 'context' => array( 'view', 'edit' ),  
  427. ),  
  428. 'usage_count' => array( 
  429. 'description' => __( 'Number of times the coupon has been used already.', 'woocommerce' ),  
  430. 'type' => 'integer',  
  431. 'context' => array( 'view', 'edit' ),  
  432. 'readonly' => true,  
  433. ),  
  434. 'individual_use' => array( 
  435. 'description' => __( 'If true, the coupon can only be used individually. Other applied coupons will be removed from the cart.', 'woocommerce' ),  
  436. 'type' => 'boolean',  
  437. 'default' => false,  
  438. 'context' => array( 'view', 'edit' ),  
  439. ),  
  440. 'product_ids' => array( 
  441. 'description' => __( "List of product IDs the coupon can be used on.", 'woocommerce' ),  
  442. 'type' => 'array',  
  443. 'items' => array( 
  444. 'type' => 'integer',  
  445. ),  
  446. 'context' => array( 'view', 'edit' ),  
  447. ),  
  448. 'exclude_product_ids' => array( 
  449. 'description' => __( "List of product IDs the coupon cannot be used on.", 'woocommerce' ),  
  450. 'type' => 'array',  
  451. 'items' => array( 
  452. 'type' => 'integer',  
  453. ),  
  454. 'context' => array( 'view', 'edit' ),  
  455. ),  
  456. 'usage_limit' => array( 
  457. 'description' => __( 'How many times the coupon can be used in total.', 'woocommerce' ),  
  458. 'type' => 'integer',  
  459. 'context' => array( 'view', 'edit' ),  
  460. ),  
  461. 'usage_limit_per_user' => array( 
  462. 'description' => __( 'How many times the coupon can be used per customer.', 'woocommerce' ),  
  463. 'type' => 'integer',  
  464. 'context' => array( 'view', 'edit' ),  
  465. ),  
  466. 'limit_usage_to_x_items' => array( 
  467. 'description' => __( 'Max number of items in the cart the coupon can be applied to.', 'woocommerce' ),  
  468. 'type' => 'integer',  
  469. 'context' => array( 'view', 'edit' ),  
  470. ),  
  471. 'free_shipping' => array( 
  472. 'description' => __( 'If true and if the free shipping method requires a coupon, this coupon will enable free shipping.', 'woocommerce' ),  
  473. 'type' => 'boolean',  
  474. 'default' => false,  
  475. 'context' => array( 'view', 'edit' ),  
  476. ),  
  477. 'product_categories' => array( 
  478. 'description' => __( "List of category IDs the coupon applies to.", 'woocommerce' ),  
  479. 'type' => 'array',  
  480. 'items' => array( 
  481. 'type' => 'integer',  
  482. ),  
  483. 'context' => array( 'view', 'edit' ),  
  484. ),  
  485. 'excluded_product_categories' => array( 
  486. 'description' => __( "List of category IDs the coupon does not apply to.", 'woocommerce' ),  
  487. 'type' => 'array',  
  488. 'items' => array( 
  489. 'type' => 'integer',  
  490. ),  
  491. 'context' => array( 'view', 'edit' ),  
  492. ),  
  493. 'exclude_sale_items' => array( 
  494. 'description' => __( 'If true, this coupon will not be applied to items that have sale prices.', 'woocommerce' ),  
  495. 'type' => 'boolean',  
  496. 'default' => false,  
  497. 'context' => array( 'view', 'edit' ),  
  498. ),  
  499. 'minimum_amount' => array( 
  500. 'description' => __( 'Minimum order amount that needs to be in the cart before coupon applies.', 'woocommerce' ),  
  501. 'type' => 'string',  
  502. 'context' => array( 'view', 'edit' ),  
  503. ),  
  504. 'maximum_amount' => array( 
  505. 'description' => __( 'Maximum order amount allowed when using the coupon.', 'woocommerce' ),  
  506. 'type' => 'string',  
  507. 'context' => array( 'view', 'edit' ),  
  508. ),  
  509. 'email_restrictions' => array( 
  510. 'description' => __( 'List of email addresses that can use this coupon.', 'woocommerce' ),  
  511. 'type' => 'array',  
  512. 'items' => array( 
  513. 'type' => 'string',  
  514. ),  
  515. 'context' => array( 'view', 'edit' ),  
  516. ),  
  517. 'used_by' => array( 
  518. 'description' => __( 'List of user IDs (or guest email addresses) that have used the coupon.', 'woocommerce' ),  
  519. 'type' => 'array',  
  520. 'items' => array( 
  521. 'type' => 'integer',  
  522. ),  
  523. 'context' => array( 'view', 'edit' ),  
  524. 'readonly' => true,  
  525. ),  
  526. ),  
  527. ); 
  528.  
  529. return $this->add_additional_fields_schema( $schema ); 
  530.  
  531. /** 
  532. * Get the query params for collections of attachments. 
  533. * 
  534. * @return array 
  535. */ 
  536. public function get_collection_params() { 
  537. $params = parent::get_collection_params(); 
  538.  
  539. $params['code'] = array( 
  540. 'description' => __( 'Limit result set to resources with a specific code.', 'woocommerce' ),  
  541. 'type' => 'string',  
  542. 'sanitize_callback' => 'sanitize_text_field',  
  543. 'validate_callback' => 'rest_validate_request_arg',  
  544. ); 
  545.  
  546. return $params; 
.