class-wc-coupon-data-store-cpt.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. <?php
  2. /**
  3. * Class WC_Coupon_Data_Store_CPT file.
  4. *
  5. * @package WooCommerce\DataStore
  6. */
  7. if ( ! defined( 'ABSPATH' ) ) {
  8. exit;
  9. }
  10. /**
  11. * WC Coupon Data Store: Custom Post Type.
  12. *
  13. * @version 3.0.0
  14. */
  15. class WC_Coupon_Data_Store_CPT extends WC_Data_Store_WP implements WC_Coupon_Data_Store_Interface, WC_Object_Data_Store_Interface {
  16. /**
  17. * Internal meta type used to store coupon data.
  18. *
  19. * @since 3.0.0
  20. * @var string
  21. */
  22. protected $meta_type = 'post';
  23. /**
  24. * Data stored in meta keys, but not considered "meta" for a coupon.
  25. *
  26. * @since 3.0.0
  27. * @var array
  28. */
  29. protected $internal_meta_keys = array(
  30. 'discount_type',
  31. 'coupon_amount',
  32. 'expiry_date',
  33. 'date_expires',
  34. 'usage_count',
  35. 'individual_use',
  36. 'product_ids',
  37. 'exclude_product_ids',
  38. 'usage_limit',
  39. 'usage_limit_per_user',
  40. 'limit_usage_to_x_items',
  41. 'free_shipping',
  42. 'product_categories',
  43. 'exclude_product_categories',
  44. 'exclude_sale_items',
  45. 'minimum_amount',
  46. 'maximum_amount',
  47. 'customer_email',
  48. '_used_by',
  49. '_edit_lock',
  50. '_edit_last',
  51. );
  52. /**
  53. * Method to create a new coupon in the database.
  54. *
  55. * @since 3.0.0
  56. * @param WC_Coupon $coupon Coupon object.
  57. */
  58. public function create( &$coupon ) {
  59. $coupon->set_date_created( current_time( 'timestamp', true ) );
  60. $coupon_id = wp_insert_post(
  61. apply_filters(
  62. 'woocommerce_new_coupon_data',
  63. array(
  64. 'post_type' => 'shop_coupon',
  65. 'post_status' => 'publish',
  66. 'post_author' => get_current_user_id(),
  67. 'post_title' => $coupon->get_code( 'edit' ),
  68. 'post_content' => '',
  69. 'post_excerpt' => $coupon->get_description( 'edit' ),
  70. 'post_date' => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created()->getOffsetTimestamp() ),
  71. 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created()->getTimestamp() ),
  72. )
  73. ), true
  74. );
  75. if ( $coupon_id ) {
  76. $coupon->set_id( $coupon_id );
  77. $this->update_post_meta( $coupon );
  78. $coupon->save_meta_data();
  79. $coupon->apply_changes();
  80. do_action( 'woocommerce_new_coupon', $coupon_id );
  81. }
  82. }
  83. /**
  84. * Method to read a coupon.
  85. *
  86. * @since 3.0.0
  87. *
  88. * @param WC_Coupon $coupon Coupon object.
  89. *
  90. * @throws Exception If invalid coupon.
  91. */
  92. public function read( &$coupon ) {
  93. $coupon->set_defaults();
  94. $post_object = get_post( $coupon->get_id() );
  95. if ( ! $coupon->get_id() || ! $post_object || 'shop_coupon' !== $post_object->post_type ) {
  96. throw new Exception( __( 'Invalid coupon.', 'woocommerce' ) );
  97. }
  98. $coupon_id = $coupon->get_id();
  99. $coupon->set_props(
  100. array(
  101. 'code' => $post_object->post_title,
  102. 'description' => $post_object->post_excerpt,
  103. 'date_created' => 0 < $post_object->post_date_gmt ? wc_string_to_timestamp( $post_object->post_date_gmt ) : null,
  104. 'date_modified' => 0 < $post_object->post_modified_gmt ? wc_string_to_timestamp( $post_object->post_modified_gmt ) : null,
  105. 'date_expires' => metadata_exists( 'post', $coupon_id, 'date_expires' ) ? get_post_meta( $coupon_id, 'date_expires', true ) : get_post_meta( $coupon_id, 'expiry_date', true ),
  106. 'discount_type' => get_post_meta( $coupon_id, 'discount_type', true ),
  107. 'amount' => get_post_meta( $coupon_id, 'coupon_amount', true ),
  108. 'usage_count' => get_post_meta( $coupon_id, 'usage_count', true ),
  109. 'individual_use' => 'yes' === get_post_meta( $coupon_id, 'individual_use', true ),
  110. 'product_ids' => array_filter( (array) explode( ',', get_post_meta( $coupon_id, 'product_ids', true ) ) ),
  111. 'excluded_product_ids' => array_filter( (array) explode( ',', get_post_meta( $coupon_id, 'exclude_product_ids', true ) ) ),
  112. 'usage_limit' => get_post_meta( $coupon_id, 'usage_limit', true ),
  113. 'usage_limit_per_user' => get_post_meta( $coupon_id, 'usage_limit_per_user', true ),
  114. 'limit_usage_to_x_items' => 0 < get_post_meta( $coupon_id, 'limit_usage_to_x_items', true ) ? get_post_meta( $coupon_id, 'limit_usage_to_x_items', true ) : null,
  115. 'free_shipping' => 'yes' === get_post_meta( $coupon_id, 'free_shipping', true ),
  116. 'product_categories' => array_filter( (array) get_post_meta( $coupon_id, 'product_categories', true ) ),
  117. 'excluded_product_categories' => array_filter( (array) get_post_meta( $coupon_id, 'exclude_product_categories', true ) ),
  118. 'exclude_sale_items' => 'yes' === get_post_meta( $coupon_id, 'exclude_sale_items', true ),
  119. 'minimum_amount' => get_post_meta( $coupon_id, 'minimum_amount', true ),
  120. 'maximum_amount' => get_post_meta( $coupon_id, 'maximum_amount', true ),
  121. 'email_restrictions' => array_filter( (array) get_post_meta( $coupon_id, 'customer_email', true ) ),
  122. 'used_by' => array_filter( (array) get_post_meta( $coupon_id, '_used_by' ) ),
  123. )
  124. );
  125. $coupon->read_meta_data();
  126. $coupon->set_object_read( true );
  127. do_action( 'woocommerce_coupon_loaded', $coupon );
  128. }
  129. /**
  130. * Updates a coupon in the database.
  131. *
  132. * @since 3.0.0
  133. * @param WC_Coupon $coupon Coupon object.
  134. */
  135. public function update( &$coupon ) {
  136. $coupon->save_meta_data();
  137. $changes = $coupon->get_changes();
  138. if ( array_intersect( array( 'code', 'description', 'date_created', 'date_modified' ), array_keys( $changes ) ) ) {
  139. $post_data = array(
  140. 'post_title' => $coupon->get_code( 'edit' ),
  141. 'post_excerpt' => $coupon->get_description( 'edit' ),
  142. 'post_date' => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created( 'edit' )->getOffsetTimestamp() ),
  143. 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created( 'edit' )->getTimestamp() ),
  144. 'post_modified' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $coupon->get_date_modified( 'edit' )->getOffsetTimestamp() ) : current_time( 'mysql' ),
  145. 'post_modified_gmt' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $coupon->get_date_modified( 'edit' )->getTimestamp() ) : current_time( 'mysql', 1 ),
  146. );
  147. /**
  148. * When updating this object, to prevent infinite loops, use $wpdb
  149. * to update data, since wp_update_post spawns more calls to the
  150. * save_post action.
  151. *
  152. * This ensures hooks are fired by either WP itself (admin screen save),
  153. * or an update purely from CRUD.
  154. */
  155. if ( doing_action( 'save_post' ) ) {
  156. $GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $coupon->get_id() ) );
  157. clean_post_cache( $coupon->get_id() );
  158. } else {
  159. wp_update_post( array_merge( array( 'ID' => $coupon->get_id() ), $post_data ) );
  160. }
  161. $coupon->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook.
  162. }
  163. $this->update_post_meta( $coupon );
  164. $coupon->apply_changes();
  165. do_action( 'woocommerce_update_coupon', $coupon->get_id() );
  166. }
  167. /**
  168. * Deletes a coupon from the database.
  169. *
  170. * @since 3.0.0
  171. *
  172. * @param WC_Coupon $coupon Coupon object.
  173. * @param array $args Array of args to pass to the delete method.
  174. */
  175. public function delete( &$coupon, $args = array() ) {
  176. $args = wp_parse_args(
  177. $args, array(
  178. 'force_delete' => false,
  179. )
  180. );
  181. $id = $coupon->get_id();
  182. if ( ! $id ) {
  183. return;
  184. }
  185. if ( $args['force_delete'] ) {
  186. wp_delete_post( $id );
  187. wp_cache_delete( WC_Cache_Helper::get_cache_prefix( 'coupons' ) . 'coupon_id_from_code_' . $coupon->get_code(), 'coupons' );
  188. $coupon->set_id( 0 );
  189. do_action( 'woocommerce_delete_coupon', $id );
  190. } else {
  191. wp_trash_post( $id );
  192. do_action( 'woocommerce_trash_coupon', $id );
  193. }
  194. }
  195. /**
  196. * Helper method that updates all the post meta for a coupon based on it's settings in the WC_Coupon class.
  197. *
  198. * @param WC_Coupon $coupon Coupon object.
  199. * @since 3.0.0
  200. */
  201. private function update_post_meta( &$coupon ) {
  202. $updated_props = array();
  203. $meta_key_to_props = array(
  204. 'discount_type' => 'discount_type',
  205. 'coupon_amount' => 'amount',
  206. 'individual_use' => 'individual_use',
  207. 'product_ids' => 'product_ids',
  208. 'exclude_product_ids' => 'excluded_product_ids',
  209. 'usage_limit' => 'usage_limit',
  210. 'usage_limit_per_user' => 'usage_limit_per_user',
  211. 'limit_usage_to_x_items' => 'limit_usage_to_x_items',
  212. 'usage_count' => 'usage_count',
  213. 'date_expires' => 'date_expires',
  214. 'free_shipping' => 'free_shipping',
  215. 'product_categories' => 'product_categories',
  216. 'exclude_product_categories' => 'excluded_product_categories',
  217. 'exclude_sale_items' => 'exclude_sale_items',
  218. 'minimum_amount' => 'minimum_amount',
  219. 'maximum_amount' => 'maximum_amount',
  220. 'customer_email' => 'email_restrictions',
  221. );
  222. $props_to_update = $this->get_props_to_update( $coupon, $meta_key_to_props );
  223. foreach ( $props_to_update as $meta_key => $prop ) {
  224. $value = $coupon->{"get_$prop"}( 'edit' );
  225. switch ( $prop ) {
  226. case 'individual_use':
  227. case 'free_shipping':
  228. case 'exclude_sale_items':
  229. $updated = update_post_meta( $coupon->get_id(), $meta_key, wc_bool_to_string( $value ) );
  230. break;
  231. case 'product_ids':
  232. case 'excluded_product_ids':
  233. $updated = update_post_meta( $coupon->get_id(), $meta_key, implode( ',', array_filter( array_map( 'intval', $value ) ) ) );
  234. break;
  235. case 'product_categories':
  236. case 'excluded_product_categories':
  237. $updated = update_post_meta( $coupon->get_id(), $meta_key, array_filter( array_map( 'intval', $value ) ) );
  238. break;
  239. case 'email_restrictions':
  240. $updated = update_post_meta( $coupon->get_id(), $meta_key, array_filter( array_map( 'sanitize_email', $value ) ) );
  241. break;
  242. case 'date_expires':
  243. $updated = update_post_meta( $coupon->get_id(), $meta_key, ( $value ? $value->getTimestamp() : null ) );
  244. update_post_meta( $coupon->get_id(), 'expiry_date', ( $value ? $value->date( 'Y-m-d' ) : '' ) ); // Update the old meta key for backwards compatibility.
  245. break;
  246. default:
  247. $updated = update_post_meta( $coupon->get_id(), $meta_key, $value );
  248. break;
  249. }
  250. if ( $updated ) {
  251. $updated_props[] = $prop;
  252. }
  253. }
  254. do_action( 'woocommerce_coupon_object_updated_props', $coupon, $updated_props );
  255. }
  256. /**
  257. * Increase usage count for current coupon.
  258. *
  259. * @since 3.0.0
  260. * @param WC_Coupon $coupon Coupon object.
  261. * @param string $used_by Either user ID or billing email.
  262. * @return int New usage count.
  263. */
  264. public function increase_usage_count( &$coupon, $used_by = '' ) {
  265. $new_count = $this->update_usage_count_meta( $coupon, 'increase' );
  266. if ( $used_by ) {
  267. add_post_meta( $coupon->get_id(), '_used_by', strtolower( $used_by ) );
  268. $coupon->set_used_by( (array) get_post_meta( $coupon->get_id(), '_used_by' ) );
  269. }
  270. return $new_count;
  271. }
  272. /**
  273. * Decrease usage count for current coupon.
  274. *
  275. * @since 3.0.0
  276. * @param WC_Coupon $coupon Coupon object.
  277. * @param string $used_by Either user ID or billing email.
  278. * @return int New usage count.
  279. */
  280. public function decrease_usage_count( &$coupon, $used_by = '' ) {
  281. global $wpdb;
  282. $new_count = $this->update_usage_count_meta( $coupon, 'decrease' );
  283. if ( $used_by ) {
  284. /**
  285. * We're doing this the long way because `delete_post_meta( $id, $key, $value )` deletes.
  286. * all instances where the key and value match, and we only want to delete one.
  287. */
  288. $meta_id = $wpdb->get_var( $wpdb->prepare( "SELECT meta_id FROM $wpdb->postmeta WHERE meta_key = '_used_by' AND meta_value = %s AND post_id = %d LIMIT 1;", $used_by, $coupon->get_id() ) );
  289. if ( $meta_id ) {
  290. delete_metadata_by_mid( 'post', $meta_id );
  291. $coupon->set_used_by( (array) get_post_meta( $coupon->get_id(), '_used_by' ) );
  292. }
  293. }
  294. return $new_count;
  295. }
  296. /**
  297. * Increase or decrease the usage count for a coupon by 1.
  298. *
  299. * @since 3.0.0
  300. * @param WC_Coupon $coupon Coupon object.
  301. * @param string $operation 'increase' or 'decrease'.
  302. * @return int New usage count
  303. */
  304. private function update_usage_count_meta( &$coupon, $operation = 'increase' ) {
  305. global $wpdb;
  306. $id = $coupon->get_id();
  307. $operator = ( 'increase' === $operation ) ? '+' : '-';
  308. add_post_meta( $id, 'usage_count', $coupon->get_usage_count( 'edit' ), true );
  309. $wpdb->query(
  310. $wpdb->prepare(
  311. "UPDATE $wpdb->postmeta SET meta_value = meta_value {$operator} 1 WHERE meta_key = 'usage_count' AND post_id = %d;", // phpcs:ignore WordPress.WP.PreparedSQL.NotPrepared
  312. $id
  313. )
  314. );
  315. // Get the latest value direct from the DB, instead of possibly the WP meta cache.
  316. return (int) $wpdb->get_var( $wpdb->prepare( "SELECT meta_value FROM $wpdb->postmeta WHERE meta_key = 'usage_count' AND post_id = %d;", $id ) );
  317. }
  318. /**
  319. * Get the number of uses for a coupon by user ID.
  320. *
  321. * @since 3.0.0
  322. * @param WC_Coupon $coupon Coupon object.
  323. * @param id $user_id User ID.
  324. * @return int
  325. */
  326. public function get_usage_by_user_id( &$coupon, $user_id ) {
  327. global $wpdb;
  328. return $wpdb->get_var( $wpdb->prepare( "SELECT COUNT( meta_id ) FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = '_used_by' AND meta_value = %d;", $coupon->get_id(), $user_id ) );
  329. }
  330. /**
  331. * Return a coupon code for a specific ID.
  332. *
  333. * @since 3.0.0
  334. * @param int $id Coupon ID.
  335. * @return string Coupon Code
  336. */
  337. public function get_code_by_id( $id ) {
  338. global $wpdb;
  339. return $wpdb->get_var(
  340. $wpdb->prepare(
  341. "SELECT post_title
  342. FROM $wpdb->posts
  343. WHERE ID = %d
  344. AND post_type = 'shop_coupon'
  345. AND post_status = 'publish'",
  346. $id
  347. )
  348. );
  349. }
  350. /**
  351. * Return an array of IDs for for a specific coupon code.
  352. * Can return multiple to check for existence.
  353. *
  354. * @since 3.0.0
  355. * @param string $code Coupon code.
  356. * @return array Array of IDs.
  357. */
  358. public function get_ids_by_code( $code ) {
  359. global $wpdb;
  360. return $wpdb->get_col(
  361. $wpdb->prepare(
  362. "SELECT ID FROM $wpdb->posts WHERE post_title = %s AND post_type = 'shop_coupon' AND post_status = 'publish' ORDER BY post_date DESC",
  363. $code
  364. )
  365. );
  366. }
  367. }