WC_Coupon

WooCommerce coupons.

Defined (1)

The class is defined in the following location(s).

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