class-wc-coupon.php 32 KB

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