/includes/class-wc-coupon.php

  1. <?php 
  2. include_once( 'legacy/class-wc-legacy-coupon.php' ); 
  3.  
  4. if ( ! defined( 'ABSPATH' ) ) { 
  5. exit; 
  6.  
  7. /** 
  8. * WooCommerce coupons. 
  9. * 
  10. * The WooCommerce coupons class gets coupon data from storage and checks coupon validity. 
  11. * 
  12. * @class WC_Coupon 
  13. * @version 3.0.0 
  14. * @package WooCommerce/Classes 
  15. * @category Class 
  16. * @author WooThemes 
  17. */ 
  18. class WC_Coupon extends WC_Legacy_Coupon { 
  19.  
  20. /** 
  21. * Data array, with defaults. 
  22. * @since 3.0.0 
  23. * @var array 
  24. */ 
  25. protected $data = array( 
  26. 'code' => '',  
  27. 'amount' => 0,  
  28. 'date_created' => null,  
  29. 'date_modified' => null,  
  30. 'date_expires' => null,  
  31. 'discount_type' => 'fixed_cart',  
  32. 'description' => '',  
  33. 'usage_count' => 0,  
  34. 'individual_use' => false,  
  35. 'product_ids' => array(),  
  36. 'excluded_product_ids' => array(),  
  37. 'usage_limit' => 0,  
  38. 'usage_limit_per_user' => 0,  
  39. 'limit_usage_to_x_items' => null,  
  40. 'free_shipping' => false,  
  41. 'product_categories' => array(),  
  42. 'excluded_product_categories' => array(),  
  43. 'exclude_sale_items' => false,  
  44. 'minimum_amount' => '',  
  45. 'maximum_amount' => '',  
  46. 'email_restrictions' => array(),  
  47. 'used_by' => array(),  
  48. ); 
  49.  
  50. // Coupon message codes 
  51. const E_WC_COUPON_INVALID_FILTERED = 100; 
  52. const E_WC_COUPON_INVALID_REMOVED = 101; 
  53. const E_WC_COUPON_NOT_YOURS_REMOVED = 102; 
  54. const E_WC_COUPON_ALREADY_APPLIED = 103; 
  55. const E_WC_COUPON_ALREADY_APPLIED_INDIV_USE_ONLY = 104; 
  56. const E_WC_COUPON_NOT_EXIST = 105; 
  57. const E_WC_COUPON_USAGE_LIMIT_REACHED = 106; 
  58. const E_WC_COUPON_EXPIRED = 107; 
  59. const E_WC_COUPON_MIN_SPEND_LIMIT_NOT_MET = 108; 
  60. const E_WC_COUPON_NOT_APPLICABLE = 109; 
  61. const E_WC_COUPON_NOT_VALID_SALE_ITEMS = 110; 
  62. const E_WC_COUPON_PLEASE_ENTER = 111; 
  63. const E_WC_COUPON_MAX_SPEND_LIMIT_MET = 112; 
  64. const E_WC_COUPON_EXCLUDED_PRODUCTS = 113; 
  65. const E_WC_COUPON_EXCLUDED_CATEGORIES = 114; 
  66. const WC_COUPON_SUCCESS = 200; 
  67. const WC_COUPON_REMOVED = 201; 
  68.  
  69. /** 
  70. * Cache group. 
  71. * @var string 
  72. */ 
  73. protected $cache_group = 'coupons'; 
  74.  
  75. /** 
  76. * Coupon constructor. Loads coupon data. 
  77. * @param mixed $data Coupon data, object, ID or code. 
  78. */ 
  79. public function __construct( $data = '' ) { 
  80. parent::__construct( $data ); 
  81.  
  82. if ( $data instanceof WC_Coupon ) { 
  83. $this->set_id( absint( $data->get_id() ) ); 
  84. } elseif ( $coupon = apply_filters( 'woocommerce_get_shop_coupon_data', false, $data ) ) { 
  85. $this->read_manual_coupon( $data, $coupon ); 
  86. return; 
  87. } elseif ( is_numeric( $data ) && 'shop_coupon' === get_post_type( $data ) ) { 
  88. $this->set_id( $data ); 
  89. } elseif ( ! empty( $data ) ) { 
  90. $this->set_id( wc_get_coupon_id_by_code( $data ) ); 
  91. $this->set_code( $data ); 
  92. } else { 
  93. $this->set_object_read( true ); 
  94.  
  95. $this->data_store = WC_Data_Store::load( 'coupon' ); 
  96. if ( $this->get_id() > 0 ) { 
  97. $this->data_store->read( $this ); 
  98.  
  99. /** 
  100. * Checks the coupon type. 
  101. * @param string $type Array or string of types 
  102. * @return bool 
  103. */ 
  104. public function is_type( $type ) { 
  105. return ( $this->get_discount_type() === $type || ( is_array( $type ) && in_array( $this->get_discount_type(), $type ) ) ); 
  106.  
  107. /** 
  108. * Prefix for action and filter hooks on data. 
  109. * 
  110. * @since 3.0.0 
  111. * @return string 
  112. */ 
  113. protected function get_hook_prefix() { 
  114. return 'woocommerce_coupon_get_'; 
  115.  
  116. /** 
  117. |-------------------------------------------------------------------------- 
  118. | Getters 
  119. |-------------------------------------------------------------------------- 
  120. | 
  121. | Methods for getting data from the coupon object. 
  122. | 
  123. */ 
  124.  
  125. /** 
  126. * Get coupon code. 
  127. * @since 3.0.0 
  128. * @param string $context 
  129. * @return string 
  130. */ 
  131. public function get_code( $context = 'view' ) { 
  132. return $this->get_prop( 'code', $context ); 
  133.  
  134. /** 
  135. * Get coupon description. 
  136. * @since 3.0.0 
  137. * @param string $context 
  138. * @return string 
  139. */ 
  140. public function get_description( $context = 'view' ) { 
  141. return $this->get_prop( 'description', $context ); 
  142.  
  143. /** 
  144. * Get discount type. 
  145. * @since 3.0.0 
  146. * @param string $context 
  147. * @return string 
  148. */ 
  149. public function get_discount_type( $context = 'view' ) { 
  150. return $this->get_prop( 'discount_type', $context ); 
  151.  
  152. /** 
  153. * Get coupon amount. 
  154. * @since 3.0.0 
  155. * @param string $context 
  156. * @return float 
  157. */ 
  158. public function get_amount( $context = 'view' ) { 
  159. return $this->get_prop( 'amount', $context ); 
  160.  
  161. /** 
  162. * Get coupon expiration date. 
  163. * @since 3.0.0 
  164. * @param string $context 
  165. * @return WC_DateTime|NULL object if the date is set or null if there is no date. 
  166. */ 
  167. public function get_date_expires( $context = 'view' ) { 
  168. return $this->get_prop( 'date_expires', $context ); 
  169.  
  170. /** 
  171. * Get date_created 
  172. * @since 3.0.0 
  173. * @param string $context 
  174. * @return WC_DateTime|NULL object if the date is set or null if there is no date. 
  175. */ 
  176. public function get_date_created( $context = 'view' ) { 
  177. return $this->get_prop( 'date_created', $context ); 
  178.  
  179. /** 
  180. * Get date_modified 
  181. * @since 3.0.0 
  182. * @param string $context 
  183. * @return WC_DateTime|NULL object if the date is set or null if there is no date. 
  184. */ 
  185. public function get_date_modified( $context = 'view' ) { 
  186. return $this->get_prop( 'date_modified', $context ); 
  187.  
  188. /** 
  189. * Get coupon usage count. 
  190. * @since 3.0.0 
  191. * @param string $context 
  192. * @return integer 
  193. */ 
  194. public function get_usage_count( $context = 'view' ) { 
  195. return $this->get_prop( 'usage_count', $context ); 
  196.  
  197. /** 
  198. * Get the "indvidual use" checkbox status. 
  199. * @since 3.0.0 
  200. * @param string $context 
  201. * @return bool 
  202. */ 
  203. public function get_individual_use( $context = 'view' ) { 
  204. return $this->get_prop( 'individual_use', $context ); 
  205.  
  206. /** 
  207. * Get product IDs this coupon can apply to. 
  208. * @since 3.0.0 
  209. * @param string $context 
  210. * @return array 
  211. */ 
  212. public function get_product_ids( $context = 'view' ) { 
  213. return $this->get_prop( 'product_ids', $context ); 
  214.  
  215. /** 
  216. * Get product IDs that this coupon should not apply to. 
  217. * @since 3.0.0 
  218. * @param string $context 
  219. * @return array 
  220. */ 
  221. public function get_excluded_product_ids( $context = 'view' ) { 
  222. return $this->get_prop( 'excluded_product_ids', $context ); 
  223.  
  224. /** 
  225. * Get coupon usage limit. 
  226. * @since 3.0.0 
  227. * @param string $context 
  228. * @return integer 
  229. */ 
  230. public function get_usage_limit( $context = 'view' ) { 
  231. return $this->get_prop( 'usage_limit', $context ); 
  232.  
  233. /** 
  234. * Get coupon usage limit per customer (for a single customer) 
  235. * @since 3.0.0 
  236. * @param string $context 
  237. * @return integer 
  238. */ 
  239. public function get_usage_limit_per_user( $context = 'view' ) { 
  240. return $this->get_prop( 'usage_limit_per_user', $context ); 
  241.  
  242. /** 
  243. * Usage limited to certain amount of items 
  244. * @since 3.0.0 
  245. * @param string $context 
  246. * @return integer|null 
  247. */ 
  248. public function get_limit_usage_to_x_items( $context = 'view' ) { 
  249. return $this->get_prop( 'limit_usage_to_x_items', $context ); 
  250.  
  251. /** 
  252. * If this coupon grants free shipping or not. 
  253. * @since 3.0.0 
  254. * @param string $context 
  255. * @return bool 
  256. */ 
  257. public function get_free_shipping( $context = 'view' ) { 
  258. return $this->get_prop( 'free_shipping', $context ); 
  259.  
  260. /** 
  261. * Get product categories this coupon can apply to. 
  262. * @since 3.0.0 
  263. * @param string $context 
  264. * @return array 
  265. */ 
  266. public function get_product_categories( $context = 'view' ) { 
  267. return $this->get_prop( 'product_categories', $context ); 
  268.  
  269. /** 
  270. * Get product categories this coupon cannot not apply to. 
  271. * @since 3.0.0 
  272. * @param string $context 
  273. * @return array 
  274. */ 
  275. public function get_excluded_product_categories( $context = 'view' ) { 
  276. return $this->get_prop( 'excluded_product_categories', $context ); 
  277.  
  278. /** 
  279. * If this coupon should exclude items on sale. 
  280. * @since 3.0.0 
  281. * @param string $context 
  282. * @return bool 
  283. */ 
  284. public function get_exclude_sale_items( $context = 'view' ) { 
  285. return $this->get_prop( 'exclude_sale_items', $context ); 
  286.  
  287. /** 
  288. * Get minium spend amount. 
  289. * @since 3.0.0 
  290. * @param string $context 
  291. * @return float 
  292. */ 
  293. public function get_minimum_amount( $context = 'view' ) { 
  294. return $this->get_prop( 'minimum_amount', $context ); 
  295. /** 
  296. * Get maximum spend amount. 
  297. * @since 3.0.0 
  298. * @param string $context 
  299. * @return float 
  300. */ 
  301. public function get_maximum_amount( $context = 'view' ) { 
  302. return $this->get_prop( 'maximum_amount', $context ); 
  303.  
  304. /** 
  305. * Get emails to check customer usage restrictions. 
  306. * @since 3.0.0 
  307. * @param string $context 
  308. * @return array 
  309. */ 
  310. public function get_email_restrictions( $context = 'view' ) { 
  311. return $this->get_prop( 'email_restrictions', $context ); 
  312.  
  313. /** 
  314. * Get records of all users who have used the current coupon. 
  315. * @since 3.0.0 
  316. * @param string $context 
  317. * @return array 
  318. */ 
  319. public function get_used_by( $context = 'view' ) { 
  320. return $this->get_prop( 'used_by', $context ); 
  321.  
  322. /** 
  323. * Get discount amount for a cart item. 
  324. * 
  325. * @param float $discounting_amount Amount the coupon is being applied to 
  326. * @param array|null $cart_item Cart item being discounted if applicable 
  327. * @param boolean $single True if discounting a single qty item, false if its the line 
  328. * @return float Amount this coupon has discounted 
  329. */ 
  330. public function get_discount_amount( $discounting_amount, $cart_item = null, $single = false ) { 
  331. $discount = 0; 
  332. $cart_item_qty = is_null( $cart_item ) ? 1 : $cart_item['quantity']; 
  333.  
  334. if ( $this->is_type( array( 'percent' ) ) ) { 
  335. $discount = (float) $this->get_amount() * ( $discounting_amount / 100 ); 
  336. } elseif ( $this->is_type( 'fixed_cart' ) && ! is_null( $cart_item ) && WC()->cart->subtotal_ex_tax ) { 
  337. /** 
  338. * This is the most complex discount - we need to divide the discount between rows based on their price in. 
  339. * proportion to the subtotal. This is so rows with different tax rates get a fair discount, and so rows. 
  340. * with no price (free) don't get discounted. 
  341. * 
  342. * Get item discount by dividing item cost by subtotal to get a %. 
  343. * 
  344. * Uses price inc tax if prices include tax to work around https://github.com/woocommerce/woocommerce/issues/7669 and https://github.com/woocommerce/woocommerce/issues/8074. 
  345. */ 
  346. if ( wc_prices_include_tax() ) { 
  347. $discount_percent = ( wc_get_price_including_tax( $cart_item['data'] ) * $cart_item_qty ) / WC()->cart->subtotal; 
  348. } else { 
  349. $discount_percent = ( wc_get_price_excluding_tax( $cart_item['data'] ) * $cart_item_qty ) / WC()->cart->subtotal_ex_tax; 
  350. $discount = ( (float) $this->get_amount() * $discount_percent ) / $cart_item_qty; 
  351.  
  352. } elseif ( $this->is_type( 'fixed_product' ) ) { 
  353. $discount = min( $this->get_amount(), $discounting_amount ); 
  354. $discount = $single ? $discount : $discount * $cart_item_qty; 
  355.  
  356. $discount = (float) min( $discount, $discounting_amount ); 
  357.  
  358. // Handle the limit_usage_to_x_items option 
  359. if ( ! $this->is_type( array( 'fixed_cart' ) ) ) { 
  360. if ( $discounting_amount ) { 
  361. if ( null === $this->get_limit_usage_to_x_items() ) { 
  362. $limit_usage_qty = $cart_item_qty; 
  363. } else { 
  364. $limit_usage_qty = min( $this->get_limit_usage_to_x_items(), $cart_item_qty ); 
  365.  
  366. $this->set_limit_usage_to_x_items( max( 0, ( $this->get_limit_usage_to_x_items() - $limit_usage_qty ) ) ); 
  367. if ( $single ) { 
  368. $discount = ( $discount * $limit_usage_qty ) / $cart_item_qty; 
  369. } else { 
  370. $discount = ( $discount / $cart_item_qty ) * $limit_usage_qty; 
  371.  
  372. $discount = round( $discount, wc_get_rounding_precision() ); 
  373.  
  374. return apply_filters( 'woocommerce_coupon_get_discount_amount', $discount, $discounting_amount, $cart_item, $single, $this ); 
  375.  
  376. /** 
  377. |-------------------------------------------------------------------------- 
  378. | Setters 
  379. |-------------------------------------------------------------------------- 
  380. | 
  381. | Functions for setting coupon data. These should not update anything in the 
  382. | database itself and should only change what is stored in the class 
  383. | object. 
  384. | 
  385. */ 
  386.  
  387. /** 
  388. * Set coupon code. 
  389. * @since 3.0.0 
  390. * @param string $code 
  391. * @throws WC_Data_Exception 
  392. */ 
  393. public function set_code( $code ) { 
  394. $this->set_prop( 'code', wc_format_coupon_code( $code ) ); 
  395.  
  396. /** 
  397. * Set coupon description. 
  398. * @since 3.0.0 
  399. * @param string $description 
  400. * @throws WC_Data_Exception 
  401. */ 
  402. public function set_description( $description ) { 
  403. $this->set_prop( 'description', $description ); 
  404.  
  405. /** 
  406. * Set discount type. 
  407. * @since 3.0.0 
  408. * @param string $discount_type 
  409. * @throws WC_Data_Exception 
  410. */ 
  411. public function set_discount_type( $discount_type ) { 
  412. if ( 'percent_product' === $discount_type ) { 
  413. $discount_type = 'percent'; // Backwards compatibility. 
  414. if ( ! in_array( $discount_type, array_keys( wc_get_coupon_types() ) ) ) { 
  415. $this->error( 'coupon_invalid_discount_type', __( 'Invalid discount type', 'woocommerce' ) ); 
  416. $this->set_prop( 'discount_type', $discount_type ); 
  417.  
  418. /** 
  419. * Set amount. 
  420. * @since 3.0.0 
  421. * @param float $amount 
  422. * @throws WC_Data_Exception 
  423. */ 
  424. public function set_amount( $amount ) { 
  425. $this->set_prop( 'amount', wc_format_decimal( $amount ) ); 
  426.  
  427. /** 
  428. * Set expiration date. 
  429. * @since 3.0.0 
  430. * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if there is no date. 
  431. * @throws WC_Data_Exception 
  432. */ 
  433. public function set_date_expires( $date ) { 
  434. $this->set_date_prop( 'date_expires', $date ); 
  435.  
  436. /** 
  437. * Set date_created 
  438. * @since 3.0.0 
  439. * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if there is no date. 
  440. * @throws WC_Data_Exception 
  441. */ 
  442. public function set_date_created( $date ) { 
  443. $this->set_date_prop( 'date_created', $date ); 
  444.  
  445. /** 
  446. * Set date_modified 
  447. * @since 3.0.0 
  448. * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if there is no date. 
  449. * @throws WC_Data_Exception 
  450. */ 
  451. public function set_date_modified( $date ) { 
  452. $this->set_date_prop( 'date_modified', $date ); 
  453.  
  454. /** 
  455. * Set how many times this coupon has been used. 
  456. * @since 3.0.0 
  457. * @param int $usage_count 
  458. * @throws WC_Data_Exception 
  459. */ 
  460. public function set_usage_count( $usage_count ) { 
  461. $this->set_prop( 'usage_count', absint( $usage_count ) ); 
  462.  
  463. /** 
  464. * Set if this coupon can only be used once. 
  465. * @since 3.0.0 
  466. * @param bool $is_individual_use 
  467. * @throws WC_Data_Exception 
  468. */ 
  469. public function set_individual_use( $is_individual_use ) { 
  470. $this->set_prop( 'individual_use', (bool) $is_individual_use ); 
  471.  
  472. /** 
  473. * Set the product IDs this coupon can be used with. 
  474. * @since 3.0.0 
  475. * @param array $product_ids 
  476. * @throws WC_Data_Exception 
  477. */ 
  478. public function set_product_ids( $product_ids ) { 
  479. $this->set_prop( 'product_ids', array_filter( wp_parse_id_list( (array) $product_ids ) ) ); 
  480.  
  481. /** 
  482. * Set the product IDs this coupon cannot be used with. 
  483. * @since 3.0.0 
  484. * @param array $excluded_product_ids 
  485. * @throws WC_Data_Exception 
  486. */ 
  487. public function set_excluded_product_ids( $excluded_product_ids ) { 
  488. $this->set_prop( 'excluded_product_ids', array_filter( wp_parse_id_list( (array) $excluded_product_ids ) ) ); 
  489.  
  490. /** 
  491. * Set the amount of times this coupon can be used. 
  492. * @since 3.0.0 
  493. * @param int $usage_limit 
  494. * @throws WC_Data_Exception 
  495. */ 
  496. public function set_usage_limit( $usage_limit ) { 
  497. $this->set_prop( 'usage_limit', absint( $usage_limit ) ); 
  498.  
  499. /** 
  500. * Set the amount of times this coupon can be used per user. 
  501. * @since 3.0.0 
  502. * @param int $usage_limit 
  503. * @throws WC_Data_Exception 
  504. */ 
  505. public function set_usage_limit_per_user( $usage_limit ) { 
  506. $this->set_prop( 'usage_limit_per_user', absint( $usage_limit ) ); 
  507.  
  508. /** 
  509. * Set usage limit to x number of items. 
  510. * @since 3.0.0 
  511. * @param int|null $limit_usage_to_x_items 
  512. * @throws WC_Data_Exception 
  513. */ 
  514. public function set_limit_usage_to_x_items( $limit_usage_to_x_items ) { 
  515. $this->set_prop( 'limit_usage_to_x_items', is_null( $limit_usage_to_x_items ) ? null : absint( $limit_usage_to_x_items ) ); 
  516.  
  517. /** 
  518. * Set if this coupon enables free shipping or not. 
  519. * @since 3.0.0 
  520. * @param bool $free_shipping 
  521. * @throws WC_Data_Exception 
  522. */ 
  523. public function set_free_shipping( $free_shipping ) { 
  524. $this->set_prop( 'free_shipping', (bool) $free_shipping ); 
  525.  
  526. /** 
  527. * Set the product category IDs this coupon can be used with. 
  528. * @since 3.0.0 
  529. * @param array $product_categories 
  530. * @throws WC_Data_Exception 
  531. */ 
  532. public function set_product_categories( $product_categories ) { 
  533. $this->set_prop( 'product_categories', array_filter( wp_parse_id_list( (array) $product_categories ) ) ); 
  534.  
  535. /** 
  536. * Set the product category IDs this coupon cannot be used with. 
  537. * @since 3.0.0 
  538. * @param array $excluded_product_categories 
  539. * @throws WC_Data_Exception 
  540. */ 
  541. public function set_excluded_product_categories( $excluded_product_categories ) { 
  542. $this->set_prop( 'excluded_product_categories', array_filter( wp_parse_id_list( (array) $excluded_product_categories ) ) ); 
  543.  
  544. /** 
  545. * Set if this coupon should excluded sale items or not. 
  546. * @since 3.0.0 
  547. * @param bool $exclude_sale_items 
  548. * @throws WC_Data_Exception 
  549. */ 
  550. public function set_exclude_sale_items( $exclude_sale_items ) { 
  551. $this->set_prop( 'exclude_sale_items', (bool) $exclude_sale_items ); 
  552.  
  553. /** 
  554. * Set the minimum spend amount. 
  555. * @since 3.0.0 
  556. * @param float $amount 
  557. * @throws WC_Data_Exception 
  558. */ 
  559. public function set_minimum_amount( $amount ) { 
  560. $this->set_prop( 'minimum_amount', wc_format_decimal( $amount ) ); 
  561.  
  562. /** 
  563. * Set the maximum spend amount. 
  564. * @since 3.0.0 
  565. * @param float $amount 
  566. * @throws WC_Data_Exception 
  567. */ 
  568. public function set_maximum_amount( $amount ) { 
  569. $this->set_prop( 'maximum_amount', wc_format_decimal( $amount ) ); 
  570.  
  571. /** 
  572. * Set email restrictions. 
  573. * @since 3.0.0 
  574. * @param array $emails 
  575. * @throws WC_Data_Exception 
  576. */ 
  577. public function set_email_restrictions( $emails = array() ) { 
  578. $emails = array_filter( array_map( 'sanitize_email', (array) $emails ) ); 
  579. foreach ( $emails as $email ) { 
  580. if ( ! is_email( $email ) ) { 
  581. $this->error( 'coupon_invalid_email_address', __( 'Invalid email address restriction', 'woocommerce' ) ); 
  582. $this->set_prop( 'email_restrictions', $emails ); 
  583.  
  584. /** 
  585. * Set which users have used this coupon. 
  586. * @since 3.0.0 
  587. * @param array $used_by 
  588. * @throws WC_Data_Exception 
  589. */ 
  590. public function set_used_by( $used_by ) { 
  591. $this->set_prop( 'used_by', array_filter( $used_by ) ); 
  592.  
  593. /** 
  594. |-------------------------------------------------------------------------- 
  595. | Other Actions 
  596. |-------------------------------------------------------------------------- 
  597. */ 
  598.  
  599. /** 
  600. * Developers can programically return coupons. This function will read those values into our WC_Coupon class. 
  601. * @since 3.0.0 
  602. * @param string $code Coupon code 
  603. * @param array $coupon Array of coupon properties 
  604. */ 
  605. public function read_manual_coupon( $code, $coupon ) { 
  606. foreach ( $coupon as $key => $value ) { 
  607. switch ( $key ) { 
  608. case 'excluded_product_ids' : 
  609. case 'exclude_product_ids' : 
  610. if ( ! is_array( $coupon[ $key ] ) ) { 
  611. wc_doing_it_wrong( $key, $key . ' should be an array instead of a string.', '3.0' ); 
  612. $coupon['excluded_product_ids'] = wc_string_to_array( $value ); 
  613. break; 
  614. case 'exclude_product_categories' : 
  615. case 'excluded_product_categories' : 
  616. if ( ! is_array( $coupon[ $key ] ) ) { 
  617. wc_doing_it_wrong( $key, $key . ' should be an array instead of a string.', '3.0' ); 
  618. $coupon['excluded_product_categories'] = wc_string_to_array( $value ); 
  619. break; 
  620. case 'product_ids' : 
  621. if ( ! is_array( $coupon[ $key ] ) ) { 
  622. wc_doing_it_wrong( $key, $key . ' should be an array instead of a string.', '3.0' ); 
  623. $coupon[ $key ] = wc_string_to_array( $value ); 
  624. break; 
  625. case 'individual_use' : 
  626. case 'free_shipping' : 
  627. case 'exclude_sale_items' : 
  628. if ( ! is_bool( $coupon[ $key ] ) ) { 
  629. wc_doing_it_wrong( $key, $key . ' should be true or false instead of yes or no.', '3.0' ); 
  630. $coupon[ $key ] = wc_string_to_bool( $value ); 
  631. break; 
  632. case 'expiry_date' : 
  633. $coupon['date_expires'] = $value; 
  634. break; 
  635. $this->set_code( $code ); 
  636. $this->set_props( $coupon ); 
  637.  
  638. /** 
  639. * Increase usage count for current coupon. 
  640. * 
  641. * @param string $used_by Either user ID or billing email 
  642. */ 
  643. public function increase_usage_count( $used_by = '' ) { 
  644. if ( $this->get_id() && $this->data_store ) { 
  645. $new_count = $this->data_store->increase_usage_count( $this, $used_by ); 
  646.  
  647. // Bypass set_prop and remove pending changes since the data store saves the count already. 
  648. $this->data['usage_count'] = $new_count; 
  649. if ( isset( $this->changes['usage_count'] ) ) { 
  650. unset( $this->changes['usage_count'] ); 
  651.  
  652. /** 
  653. * Decrease usage count for current coupon. 
  654. * 
  655. * @param string $used_by Either user ID or billing email 
  656. */ 
  657. public function decrease_usage_count( $used_by = '' ) { 
  658. if ( $this->get_id() && $this->get_usage_count() > 0 && $this->data_store ) { 
  659. $new_count = $this->data_store->decrease_usage_count( $this, $used_by ); 
  660.  
  661. // Bypass set_prop and remove pending changes since the data store saves the count already. 
  662. $this->data['usage_count'] = $new_count; 
  663. if ( isset( $this->changes['usage_count'] ) ) { 
  664. unset( $this->changes['usage_count'] ); 
  665.  
  666. /** 
  667. |-------------------------------------------------------------------------- 
  668. | Validation & Error Handling 
  669. |-------------------------------------------------------------------------- 
  670. */ 
  671.  
  672. /** 
  673. * Returns the error_message string. 
  674. * 
  675. * @access public 
  676. * @return string 
  677. */ 
  678. public function get_error_message() { 
  679. return $this->error_message; 
  680.  
  681. /** 
  682. * Ensure coupon exists or throw exception. 
  683. * 
  684. * @throws Exception 
  685. */ 
  686. private function validate_exists() { 
  687. if ( ! $this->get_id() ) { 
  688. throw new Exception( self::E_WC_COUPON_NOT_EXIST ); 
  689.  
  690. /** 
  691. * Ensure coupon usage limit is valid or throw exception. 
  692. * 
  693. * @throws Exception 
  694. */ 
  695. private function validate_usage_limit() { 
  696. if ( $this->get_usage_limit() > 0 && $this->get_usage_count() >= $this->get_usage_limit() ) { 
  697. throw new Exception( self::E_WC_COUPON_USAGE_LIMIT_REACHED ); 
  698.  
  699. /** 
  700. * Ensure coupon user usage limit is valid or throw exception. 
  701. * 
  702. * Per user usage limit - check here if user is logged in (against user IDs). 
  703. * Checked again for emails later on in WC_Cart::check_customer_coupons(). 
  704. * 
  705. * @param int $user_id 
  706. * @throws Exception 
  707. */ 
  708. private function validate_user_usage_limit( $user_id = 0 ) { 
  709. if ( empty( $user_id ) ) { 
  710. $user_id = get_current_user_id(); 
  711. if ( $this->get_usage_limit_per_user() > 0 && is_user_logged_in() && $this->get_id() && $this->data_store ) { 
  712. global $wpdb; 
  713. $usage_count = $this->data_store->get_usage_by_user_id( $this, $user_id ); 
  714. if ( $usage_count >= $this->get_usage_limit_per_user() ) { 
  715. throw new Exception( self::E_WC_COUPON_USAGE_LIMIT_REACHED ); 
  716.  
  717. /** 
  718. * Ensure coupon date is valid or throw exception. 
  719. * 
  720. * @throws Exception 
  721. */ 
  722. private function validate_expiry_date() { 
  723. if ( $this->get_date_expires() && current_time( 'timestamp', true ) > $this->get_date_expires()->getTimestamp() ) { 
  724. throw new Exception( $error_code = self::E_WC_COUPON_EXPIRED ); 
  725.  
  726. /** 
  727. * Ensure coupon amount is valid or throw exception. 
  728. * 
  729. * @throws Exception 
  730. */ 
  731. private function validate_minimum_amount() { 
  732. if ( $this->get_minimum_amount() > 0 && apply_filters( 'woocommerce_coupon_validate_minimum_amount', $this->get_minimum_amount() > WC()->cart->get_displayed_subtotal(), $this ) ) { 
  733. throw new Exception( self::E_WC_COUPON_MIN_SPEND_LIMIT_NOT_MET ); 
  734.  
  735. /** 
  736. * Ensure coupon amount is valid or throw exception. 
  737. * 
  738. * @throws Exception 
  739. */ 
  740. private function validate_maximum_amount() { 
  741. if ( $this->get_maximum_amount() > 0 && apply_filters( 'woocommerce_coupon_validate_maximum_amount', $this->get_maximum_amount() < WC()->cart->get_displayed_subtotal(), $this ) ) { 
  742. throw new Exception( self::E_WC_COUPON_MAX_SPEND_LIMIT_MET ); 
  743.  
  744. /** 
  745. * Ensure coupon is valid for products in the cart is valid or throw exception. 
  746. * 
  747. * @throws Exception 
  748. */ 
  749. private function validate_product_ids() { 
  750. if ( sizeof( $this->get_product_ids() ) > 0 ) { 
  751. $valid_for_cart = false; 
  752. if ( ! WC()->cart->is_empty() ) { 
  753. foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { 
  754. if ( in_array( $cart_item['product_id'], $this->get_product_ids() ) || in_array( $cart_item['variation_id'], $this->get_product_ids() ) || in_array( $cart_item['data']->get_parent_id(), $this->get_product_ids() ) ) { 
  755. $valid_for_cart = true; 
  756. if ( ! $valid_for_cart ) { 
  757. throw new Exception( self::E_WC_COUPON_NOT_APPLICABLE ); 
  758.  
  759. /** 
  760. * Ensure coupon is valid for product categories in the cart is valid or throw exception. 
  761. * 
  762. * @throws Exception 
  763. */ 
  764. private function validate_product_categories() { 
  765. if ( sizeof( $this->get_product_categories() ) > 0 ) { 
  766. $valid_for_cart = false; 
  767. if ( ! WC()->cart->is_empty() ) { 
  768. foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { 
  769. $product_cats = wc_get_product_cat_ids( $cart_item['product_id'] ); 
  770.  
  771. // If we find an item with a cat in our allowed cat list, the coupon is valid 
  772. if ( sizeof( array_intersect( $product_cats, $this->get_product_categories() ) ) > 0 ) { 
  773. $valid_for_cart = true; 
  774. if ( ! $valid_for_cart ) { 
  775. throw new Exception( self::E_WC_COUPON_NOT_APPLICABLE ); 
  776.  
  777. /** 
  778. * Ensure coupon is valid for sale items in the cart is valid or throw exception. 
  779. * 
  780. * @throws Exception 
  781. */ 
  782. private function validate_sale_items() { 
  783. if ( $this->get_exclude_sale_items() ) { 
  784. $valid_for_cart = false; 
  785. $product_ids_on_sale = wc_get_product_ids_on_sale(); 
  786.  
  787. if ( ! WC()->cart->is_empty() ) { 
  788. foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { 
  789. if ( ! empty( $cart_item['variation_id'] ) ) { 
  790. if ( ! in_array( $cart_item['variation_id'], $product_ids_on_sale, true ) ) { 
  791. $valid_for_cart = true; 
  792. } elseif ( ! in_array( $cart_item['product_id'], $product_ids_on_sale, true ) ) { 
  793. $valid_for_cart = true; 
  794. if ( ! $valid_for_cart ) { 
  795. throw new Exception( self::E_WC_COUPON_NOT_VALID_SALE_ITEMS ); 
  796.  
  797. /** 
  798. * All exclusion rules must pass at the same time for a product coupon to be valid. 
  799. */ 
  800. private function validate_excluded_items() { 
  801. if ( ! WC()->cart->is_empty() && $this->is_type( wc_get_product_coupon_types() ) ) { 
  802. $valid = false; 
  803.  
  804. foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { 
  805. if ( $this->is_valid_for_product( $cart_item['data'], $cart_item ) ) { 
  806. $valid = true; 
  807. break; 
  808.  
  809. if ( ! $valid ) { 
  810. throw new Exception( self::E_WC_COUPON_NOT_APPLICABLE ); 
  811.  
  812. /** 
  813. * Cart discounts cannot be added if non-eligble product is found in cart. 
  814. */ 
  815. private function validate_cart_excluded_items() { 
  816. if ( ! $this->is_type( wc_get_product_coupon_types() ) ) { 
  817. $this->validate_cart_excluded_product_ids(); 
  818. $this->validate_cart_excluded_product_categories(); 
  819.  
  820. /** 
  821. * Exclude products from cart. 
  822. * 
  823. * @throws Exception 
  824. */ 
  825. private function validate_cart_excluded_product_ids() { 
  826. // Exclude Products 
  827. if ( sizeof( $this->get_excluded_product_ids() ) > 0 ) { 
  828. $valid_for_cart = true; 
  829. if ( ! WC()->cart->is_empty() ) { 
  830. foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { 
  831. if ( in_array( $cart_item['product_id'], $this->get_excluded_product_ids() ) || in_array( $cart_item['variation_id'], $this->get_excluded_product_ids() ) || in_array( $cart_item['data']->get_parent_id(), $this->get_excluded_product_ids() ) ) { 
  832. $valid_for_cart = false; 
  833. if ( ! $valid_for_cart ) { 
  834. throw new Exception( self::E_WC_COUPON_EXCLUDED_PRODUCTS ); 
  835.  
  836. /** 
  837. * Exclude categories from cart. 
  838. * 
  839. * @throws Exception 
  840. */ 
  841. private function validate_cart_excluded_product_categories() { 
  842. if ( sizeof( $this->get_excluded_product_categories() ) > 0 ) { 
  843. $valid_for_cart = true; 
  844. if ( ! WC()->cart->is_empty() ) { 
  845. foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { 
  846. $product_cats = wc_get_product_cat_ids( $cart_item['product_id'] ); 
  847. if ( sizeof( array_intersect( $product_cats, $this->get_excluded_product_categories() ) ) > 0 ) { 
  848. $valid_for_cart = false; 
  849. if ( ! $valid_for_cart ) { 
  850. throw new Exception( self::E_WC_COUPON_EXCLUDED_CATEGORIES ); 
  851.  
  852. /** 
  853. * Check if a coupon is valid. 
  854. * 
  855. * @return boolean validity 
  856. * @throws Exception 
  857. */ 
  858. public function is_valid() { 
  859. try { 
  860. $this->validate_exists(); 
  861. $this->validate_usage_limit(); 
  862. $this->validate_user_usage_limit(); 
  863. $this->validate_expiry_date(); 
  864. $this->validate_minimum_amount(); 
  865. $this->validate_maximum_amount(); 
  866. $this->validate_product_ids(); 
  867. $this->validate_product_categories(); 
  868. $this->validate_sale_items(); 
  869. $this->validate_excluded_items(); 
  870. $this->validate_cart_excluded_items(); 
  871.  
  872. if ( ! apply_filters( 'woocommerce_coupon_is_valid', true, $this ) ) { 
  873. throw new Exception( self::E_WC_COUPON_INVALID_FILTERED ); 
  874. } catch ( Exception $e ) { 
  875. $this->error_message = $this->get_coupon_error( $e->getMessage() ); 
  876. return false; 
  877.  
  878. return true; 
  879.  
  880. /** 
  881. * Check if a coupon is valid. 
  882. * 
  883. * @return bool 
  884. */ 
  885. public function is_valid_for_cart() { 
  886. return apply_filters( 'woocommerce_coupon_is_valid_for_cart', $this->is_type( wc_get_cart_coupon_types() ), $this ); 
  887.  
  888. /** 
  889. * Check if a coupon is valid for a product. 
  890. * 
  891. * @param WC_Product $product 
  892. * @return boolean 
  893. */ 
  894. public function is_valid_for_product( $product, $values = array() ) { 
  895. if ( ! $this->is_type( wc_get_product_coupon_types() ) ) { 
  896. return apply_filters( 'woocommerce_coupon_is_valid_for_product', false, $product, $this, $values ); 
  897.  
  898. $valid = false; 
  899. $product_cats = wc_get_product_cat_ids( $product->is_type( 'variation' ) ? $product->get_parent_id() : $product->get_id() ); 
  900. $product_ids = array( $product->get_id(), $product->get_parent_id() ); 
  901.  
  902. // Specific products get the discount 
  903. if ( sizeof( $this->get_product_ids() ) && sizeof( array_intersect( $product_ids, $this->get_product_ids() ) ) ) { 
  904. $valid = true; 
  905.  
  906. // Category discounts 
  907. if ( sizeof( $this->get_product_categories() ) && sizeof( array_intersect( $product_cats, $this->get_product_categories() ) ) ) { 
  908. $valid = true; 
  909.  
  910. // No product ids - all items discounted 
  911. if ( ! sizeof( $this->get_product_ids() ) && ! sizeof( $this->get_product_categories() ) ) { 
  912. $valid = true; 
  913.  
  914. // Specific product IDs excluded from the discount 
  915. if ( sizeof( $this->get_excluded_product_ids() ) && sizeof( array_intersect( $product_ids, $this->get_excluded_product_ids() ) ) ) { 
  916. $valid = false; 
  917.  
  918. // Specific categories excluded from the discount 
  919. if ( sizeof( $this->get_excluded_product_categories() ) && sizeof( array_intersect( $product_cats, $this->get_excluded_product_categories() ) ) ) { 
  920. $valid = false; 
  921.  
  922. // Sale Items excluded from discount 
  923. if ( $this->get_exclude_sale_items() ) { 
  924. $product_ids_on_sale = wc_get_product_ids_on_sale(); 
  925.  
  926. if ( in_array( $product->get_id(), $product_ids_on_sale, true ) ) { 
  927. $valid = false; 
  928.  
  929. return apply_filters( 'woocommerce_coupon_is_valid_for_product', $valid, $product, $this, $values ); 
  930.  
  931. /** 
  932. * Converts one of the WC_Coupon message/error codes to a message string and. 
  933. * displays the message/error. 
  934. * 
  935. * @param int $msg_code Message/error code. 
  936. */ 
  937. public function add_coupon_message( $msg_code ) { 
  938. $msg = $msg_code < 200 ? $this->get_coupon_error( $msg_code ) : $this->get_coupon_message( $msg_code ); 
  939.  
  940. if ( ! $msg ) { 
  941. return; 
  942.  
  943. if ( $msg_code < 200 ) { 
  944. wc_add_notice( $msg, 'error' ); 
  945. } else { 
  946. wc_add_notice( $msg ); 
  947.  
  948. /** 
  949. * Map one of the WC_Coupon message codes to a message string. 
  950. * 
  951. * @param integer $msg_code 
  952. * @return string| Message/error string 
  953. */ 
  954. public function get_coupon_message( $msg_code ) { 
  955. switch ( $msg_code ) { 
  956. case self::WC_COUPON_SUCCESS : 
  957. $msg = __( 'Coupon code applied successfully.', 'woocommerce' ); 
  958. break; 
  959. case self::WC_COUPON_REMOVED : 
  960. $msg = __( 'Coupon code removed successfully.', 'woocommerce' ); 
  961. break; 
  962. default: 
  963. $msg = ''; 
  964. break; 
  965. return apply_filters( 'woocommerce_coupon_message', $msg, $msg_code, $this ); 
  966.  
  967. /** 
  968. * Map one of the WC_Coupon error codes to a message string. 
  969. * 
  970. * @param int $err_code Message/error code. 
  971. * @return string| Message/error string 
  972. */ 
  973. public function get_coupon_error( $err_code ) { 
  974. switch ( $err_code ) { 
  975. case self::E_WC_COUPON_INVALID_FILTERED: 
  976. $err = __( 'Coupon is not valid.', 'woocommerce' ); 
  977. break; 
  978. case self::E_WC_COUPON_NOT_EXIST: 
  979. /** translators: %s: coupon code */ 
  980. $err = sprintf( __( 'Coupon "%s" does not exist!', 'woocommerce' ), $this->get_code() ); 
  981. break; 
  982. case self::E_WC_COUPON_INVALID_REMOVED: 
  983. /** translators: %s: coupon code */ 
  984. $err = sprintf( __( 'Sorry, it seems the coupon "%s" is invalid - it has now been removed from your order.', 'woocommerce' ), $this->get_code() ); 
  985. break; 
  986. case self::E_WC_COUPON_NOT_YOURS_REMOVED: 
  987. /** translators: %s: coupon code */ 
  988. $err = sprintf( __( 'Sorry, it seems the coupon "%s" is not yours - it has now been removed from your order.', 'woocommerce' ), $this->get_code() ); 
  989. break; 
  990. case self::E_WC_COUPON_ALREADY_APPLIED: 
  991. $err = __( 'Coupon code already applied!', 'woocommerce' ); 
  992. break; 
  993. case self::E_WC_COUPON_ALREADY_APPLIED_INDIV_USE_ONLY: 
  994. /** translators: %s: coupon code */ 
  995. $err = sprintf( __( 'Sorry, coupon "%s" has already been applied and cannot be used in conjunction with other coupons.', 'woocommerce' ), $this->get_code() ); 
  996. break; 
  997. case self::E_WC_COUPON_USAGE_LIMIT_REACHED: 
  998. $err = __( 'Coupon usage limit has been reached.', 'woocommerce' ); 
  999. break; 
  1000. case self::E_WC_COUPON_EXPIRED: 
  1001. $err = __( 'This coupon has expired.', 'woocommerce' ); 
  1002. break; 
  1003. case self::E_WC_COUPON_MIN_SPEND_LIMIT_NOT_MET: 
  1004. /** translators: %s: coupon minimum amount */ 
  1005. $err = sprintf( __( 'The minimum spend for this coupon is %s.', 'woocommerce' ), wc_price( $this->get_minimum_amount() ) ); 
  1006. break; 
  1007. case self::E_WC_COUPON_MAX_SPEND_LIMIT_MET: 
  1008. /** translators: %s: coupon maximum amount */ 
  1009. $err = sprintf( __( 'The maximum spend for this coupon is %s.', 'woocommerce' ), wc_price( $this->get_maximum_amount() ) ); 
  1010. break; 
  1011. case self::E_WC_COUPON_NOT_APPLICABLE: 
  1012. $err = __( 'Sorry, this coupon is not applicable to your cart contents.', 'woocommerce' ); 
  1013. break; 
  1014. case self::E_WC_COUPON_EXCLUDED_PRODUCTS: 
  1015. // Store excluded products that are in cart in $products 
  1016. $products = array(); 
  1017. if ( ! WC()->cart->is_empty() ) { 
  1018. foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { 
  1019. if ( in_array( $cart_item['product_id'], $this->get_excluded_product_ids() ) || in_array( $cart_item['variation_id'], $this->get_excluded_product_ids() ) || in_array( $cart_item['data']->get_parent_id(), $this->get_excluded_product_ids() ) ) { 
  1020. $products[] = $cart_item['data']->get_name(); 
  1021.  
  1022. /** translators: %s: products list */ 
  1023. $err = sprintf( __( 'Sorry, this coupon is not applicable to the products: %s.', 'woocommerce' ), implode( ', ', $products ) ); 
  1024. break; 
  1025. case self::E_WC_COUPON_EXCLUDED_CATEGORIES: 
  1026. // Store excluded categories that are in cart in $categories 
  1027. $categories = array(); 
  1028. if ( ! WC()->cart->is_empty() ) { 
  1029. foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { 
  1030. $product_cats = wc_get_product_cat_ids( $cart_item['product_id'] ); 
  1031.  
  1032. if ( sizeof( $intersect = array_intersect( $product_cats, $this->get_excluded_product_categories() ) ) > 0 ) { 
  1033.  
  1034. foreach ( $intersect as $cat_id ) { 
  1035. $cat = get_term( $cat_id, 'product_cat' ); 
  1036. $categories[] = $cat->name; 
  1037.  
  1038. /** translators: %s: categories list */ 
  1039. $err = sprintf( __( 'Sorry, this coupon is not applicable to the categories: %s.', 'woocommerce' ), implode( ', ', array_unique( $categories ) ) ); 
  1040. break; 
  1041. case self::E_WC_COUPON_NOT_VALID_SALE_ITEMS: 
  1042. $err = __( 'Sorry, this coupon is not valid for sale items.', 'woocommerce' ); 
  1043. break; 
  1044. default: 
  1045. $err = ''; 
  1046. break; 
  1047. return apply_filters( 'woocommerce_coupon_error', $err, $err_code, $this ); 
  1048.  
  1049. /** 
  1050. * Map one of the WC_Coupon error codes to an error string. 
  1051. * No coupon instance will be available where a coupon does not exist,  
  1052. * so this static method exists. 
  1053. * 
  1054. * @param int $err_code Error code 
  1055. * @return string| Error string 
  1056. */ 
  1057. public static function get_generic_coupon_error( $err_code ) { 
  1058. switch ( $err_code ) { 
  1059. case self::E_WC_COUPON_NOT_EXIST: 
  1060. $err = __( 'Coupon does not exist!', 'woocommerce' ); 
  1061. break; 
  1062. case self::E_WC_COUPON_PLEASE_ENTER: 
  1063. $err = __( 'Please enter a coupon code.', 'woocommerce' ); 
  1064. break; 
  1065. default: 
  1066. $err = ''; 
  1067. break; 
  1068. // When using this static method, there is no $this to pass to filter 
  1069. return apply_filters( 'woocommerce_coupon_error', $err, $err_code, null ); 
.