This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | /** |
||
3 | * Discount calculation |
||
4 | * |
||
5 | * @package WooCommerce/Classes |
||
6 | * @since 3.2.0 |
||
7 | */ |
||
8 | |||
9 | defined( 'ABSPATH' ) || exit; |
||
10 | |||
11 | /** |
||
12 | * Discounts class. |
||
13 | */ |
||
14 | class WC_Discounts { |
||
15 | |||
16 | /** |
||
17 | * Reference to cart or order object. |
||
18 | * |
||
19 | * @since 3.2.0 |
||
20 | * @var WC_Cart|WC_Order |
||
21 | */ |
||
22 | protected $object; |
||
23 | |||
24 | /** |
||
25 | * An array of items to discount. |
||
26 | * |
||
27 | * @var array |
||
28 | */ |
||
29 | protected $items = array(); |
||
30 | |||
31 | /** |
||
32 | * An array of discounts which have been applied to items. |
||
33 | * |
||
34 | * @var array[] Code => Item Key => Value |
||
35 | */ |
||
36 | protected $discounts = array(); |
||
37 | |||
38 | /** |
||
39 | * WC_Discounts Constructor. |
||
40 | * |
||
41 | * @param WC_Cart|WC_Order $object Cart or order object. |
||
42 | */ |
||
43 | 103 | public function __construct( $object = null ) { |
|
44 | 103 | if ( is_a( $object, 'WC_Cart' ) ) { |
|
45 | 90 | $this->set_items_from_cart( $object ); |
|
46 | 45 | } elseif ( is_a( $object, 'WC_Order' ) ) { |
|
47 | 12 | $this->set_items_from_order( $object ); |
|
48 | } |
||
49 | } |
||
50 | |||
51 | /** |
||
52 | * Set items directly. Used by WC_Cart_Totals. |
||
53 | * |
||
54 | * @since 3.2.3 |
||
55 | * @param array $items Items to set. |
||
56 | */ |
||
57 | 82 | public function set_items( $items ) { |
|
58 | 82 | $this->items = $items; |
|
59 | 82 | $this->discounts = array(); |
|
60 | 82 | uasort( $this->items, array( $this, 'sort_by_price' ) ); |
|
61 | } |
||
62 | |||
63 | /** |
||
64 | * Normalise cart items which will be discounted. |
||
65 | * |
||
66 | * @since 3.2.0 |
||
67 | * @param WC_Cart $cart Cart object. |
||
68 | */ |
||
69 | 90 | public function set_items_from_cart( $cart ) { |
|
70 | 90 | $this->items = array(); |
|
71 | 90 | $this->discounts = array(); |
|
72 | |||
73 | 90 | if ( ! is_a( $cart, 'WC_Cart' ) ) { |
|
74 | 1 | return; |
|
75 | } |
||
76 | |||
77 | 90 | $this->object = $cart; |
|
78 | |||
79 | 90 | foreach ( $cart->get_cart() as $key => $cart_item ) { |
|
80 | 82 | $item = new stdClass(); |
|
81 | 82 | $item->key = $key; |
|
82 | 82 | $item->object = $cart_item; |
|
83 | 82 | $item->product = $cart_item['data']; |
|
84 | 82 | $item->quantity = $cart_item['quantity']; |
|
85 | 82 | $item->price = wc_add_number_precision_deep( $item->product->get_price() * $item->quantity ); |
|
86 | 82 | $this->items[ $key ] = $item; |
|
87 | } |
||
88 | |||
89 | 90 | uasort( $this->items, array( $this, 'sort_by_price' ) ); |
|
90 | } |
||
91 | |||
92 | /** |
||
93 | * Normalise order items which will be discounted. |
||
94 | * |
||
95 | * @since 3.2.0 |
||
96 | * @param WC_Order $order Order object. |
||
97 | */ |
||
98 | 12 | public function set_items_from_order( $order ) { |
|
99 | 12 | $this->items = array(); |
|
100 | 12 | $this->discounts = array(); |
|
101 | |||
102 | 12 | if ( ! is_a( $order, 'WC_Order' ) ) { |
|
103 | return; |
||
104 | } |
||
105 | |||
106 | 12 | $this->object = $order; |
|
107 | |||
108 | 12 | foreach ( $order->get_items() as $order_item ) { |
|
109 | 12 | $item = new stdClass(); |
|
110 | 12 | $item->key = $order_item->get_id(); |
|
111 | 12 | $item->object = $order_item; |
|
112 | 12 | $item->product = $order_item->get_product(); |
|
113 | 12 | $item->quantity = $order_item->get_quantity(); |
|
114 | 12 | $item->price = wc_add_number_precision_deep( $order_item->get_subtotal() ); |
|
115 | |||
116 | 12 | if ( $order->get_prices_include_tax() ) { |
|
117 | 4 | $item->price += wc_add_number_precision_deep( $order_item->get_subtotal_tax() ); |
|
118 | } |
||
119 | |||
120 | 12 | $this->items[ $order_item->get_id() ] = $item; |
|
121 | } |
||
122 | |||
123 | 12 | uasort( $this->items, array( $this, 'sort_by_price' ) ); |
|
124 | } |
||
125 | |||
126 | /** |
||
127 | * Get the object concerned. |
||
128 | * |
||
129 | * @since 3.3.2 |
||
130 | * @return object |
||
131 | */ |
||
132 | public function get_object() { |
||
133 | return $this->object; |
||
134 | } |
||
135 | |||
136 | /** |
||
137 | * Get items. |
||
138 | * |
||
139 | * @since 3.2.0 |
||
140 | * @return object[] |
||
141 | */ |
||
142 | 72 | public function get_items() { |
|
143 | 72 | return $this->items; |
|
144 | } |
||
145 | |||
146 | /** |
||
147 | * Get items to validate. |
||
148 | * |
||
149 | * @since 3.3.2 |
||
150 | * @return object[] |
||
151 | */ |
||
152 | 71 | public function get_items_to_validate() { |
|
153 | 71 | return apply_filters( 'woocommerce_coupon_get_items_to_validate', $this->get_items(), $this ); |
|
154 | } |
||
155 | |||
156 | /** |
||
157 | * Get discount by key with or without precision. |
||
158 | * |
||
159 | * @since 3.2.0 |
||
160 | * @param string $key name of discount row to return. |
||
161 | * @param bool $in_cents Should the totals be returned in cents, or without precision. |
||
162 | * @return array |
||
163 | */ |
||
164 | 62 | public function get_discount( $key, $in_cents = false ) { |
|
165 | 62 | $item_discount_totals = $this->get_discounts_by_item( $in_cents ); |
|
166 | 62 | return isset( $item_discount_totals[ $key ] ) ? $item_discount_totals[ $key ] : 0; |
|
167 | } |
||
168 | |||
169 | /** |
||
170 | * Get all discount totals. |
||
171 | * |
||
172 | * @since 3.2.0 |
||
173 | * @param bool $in_cents Should the totals be returned in cents, or without precision. |
||
174 | * @return array |
||
175 | */ |
||
176 | 65 | public function get_discounts( $in_cents = false ) { |
|
177 | 65 | $discounts = $this->discounts; |
|
178 | 65 | return $in_cents ? $discounts : wc_remove_number_precision_deep( $discounts ); |
|
179 | } |
||
180 | |||
181 | /** |
||
182 | * Get all discount totals per item. |
||
183 | * |
||
184 | * @since 3.2.0 |
||
185 | * @param bool $in_cents Should the totals be returned in cents, or without precision. |
||
186 | * @return array |
||
187 | */ |
||
188 | 94 | public function get_discounts_by_item( $in_cents = false ) { |
|
189 | 94 | $discounts = $this->discounts; |
|
190 | 94 | $item_discount_totals = (array) array_shift( $discounts ); |
|
191 | |||
192 | 94 | foreach ( $discounts as $item_discounts ) { |
|
193 | 12 | foreach ( $item_discounts as $item_key => $item_discount ) { |
|
194 | 12 | $item_discount_totals[ $item_key ] += $item_discount; |
|
195 | } |
||
196 | } |
||
197 | |||
198 | 94 | return $in_cents ? $item_discount_totals : wc_remove_number_precision_deep( $item_discount_totals ); |
|
199 | } |
||
200 | |||
201 | /** |
||
202 | * Get all discount totals per coupon. |
||
203 | * |
||
204 | * @since 3.2.0 |
||
205 | * @param bool $in_cents Should the totals be returned in cents, or without precision. |
||
206 | * @return array |
||
207 | */ |
||
208 | 94 | public function get_discounts_by_coupon( $in_cents = false ) { |
|
209 | 94 | $coupon_discount_totals = array_map( 'array_sum', $this->discounts ); |
|
210 | |||
211 | 94 | return $in_cents ? $coupon_discount_totals : wc_remove_number_precision_deep( $coupon_discount_totals ); |
|
212 | } |
||
213 | |||
214 | /** |
||
215 | * Get discounted price of an item without precision. |
||
216 | * |
||
217 | * @since 3.2.0 |
||
218 | * @param object $item Get data for this item. |
||
219 | * @return float |
||
220 | */ |
||
221 | 1 | public function get_discounted_price( $item ) { |
|
222 | 1 | return wc_remove_number_precision_deep( $this->get_discounted_price_in_cents( $item ) ); |
|
223 | } |
||
224 | |||
225 | /** |
||
226 | * Get discounted price of an item to precision (in cents). |
||
227 | * |
||
228 | * @since 3.2.0 |
||
229 | * @param object $item Get data for this item. |
||
230 | * @return int |
||
231 | */ |
||
232 | 62 | public function get_discounted_price_in_cents( $item ) { |
|
233 | 62 | return absint( round( $item->price - $this->get_discount( $item->key, true ) ) ); |
|
234 | } |
||
235 | |||
236 | /** |
||
237 | * Apply a discount to all items using a coupon. |
||
238 | * |
||
239 | * @since 3.2.0 |
||
240 | * @param WC_Coupon $coupon Coupon object being applied to the items. |
||
241 | * @param bool $validate Set to false to skip coupon validation. |
||
242 | * @throws Exception Error message when coupon isn't valid. |
||
243 | * @return bool|WP_Error True if applied or WP_Error instance in failure. |
||
244 | */ |
||
245 | 63 | public function apply_coupon( $coupon, $validate = true ) { |
|
246 | 63 | if ( ! is_a( $coupon, 'WC_Coupon' ) ) { |
|
247 | return new WP_Error( 'invalid_coupon', __( 'Invalid coupon', 'woocommerce' ) ); |
||
248 | } |
||
249 | |||
250 | 63 | $is_coupon_valid = $validate ? $this->is_coupon_valid( $coupon ) : true; |
|
251 | |||
252 | 63 | if ( is_wp_error( $is_coupon_valid ) ) { |
|
253 | return $is_coupon_valid; |
||
254 | } |
||
255 | |||
256 | 63 | if ( ! isset( $this->discounts[ $coupon->get_code() ] ) ) { |
|
257 | 63 | $this->discounts[ $coupon->get_code() ] = array_fill_keys( array_keys( $this->items ), 0 ); |
|
258 | } |
||
259 | |||
260 | 63 | $items_to_apply = $this->get_items_to_apply_coupon( $coupon ); |
|
261 | |||
262 | // Core discounts are handled here as of 3.2. |
||
263 | 63 | switch ( $coupon->get_discount_type() ) { |
|
264 | case 'percent': |
||
265 | 33 | $this->apply_coupon_percent( $coupon, $items_to_apply ); |
|
266 | 33 | break; |
|
267 | case 'fixed_product': |
||
268 | 16 | $this->apply_coupon_fixed_product( $coupon, $items_to_apply ); |
|
269 | 16 | break; |
|
270 | case 'fixed_cart': |
||
271 | 20 | $this->apply_coupon_fixed_cart( $coupon, $items_to_apply ); |
|
272 | 20 | break; |
|
273 | default: |
||
274 | 1 | $this->apply_coupon_custom( $coupon, $items_to_apply ); |
|
275 | 1 | break; |
|
276 | } |
||
277 | |||
278 | 63 | return true; |
|
279 | } |
||
280 | |||
281 | /** |
||
282 | * Sort by price. |
||
283 | * |
||
284 | * @since 3.2.0 |
||
285 | * @param array $a First element. |
||
286 | * @param array $b Second element. |
||
287 | * @return int |
||
288 | */ |
||
289 | 32 | protected function sort_by_price( $a, $b ) { |
|
290 | 32 | $price_1 = $a->price * $a->quantity; |
|
291 | 32 | $price_2 = $b->price * $b->quantity; |
|
292 | 32 | if ( $price_1 === $price_2 ) { |
|
293 | 12 | return 0; |
|
294 | } |
||
295 | 26 | return ( $price_1 < $price_2 ) ? 1 : -1; |
|
296 | } |
||
297 | |||
298 | /** |
||
299 | * Filter out all products which have been fully discounted to 0. |
||
300 | * Used as array_filter callback. |
||
301 | * |
||
302 | * @since 3.2.0 |
||
303 | * @param object $item Get data for this item. |
||
304 | * @return bool |
||
305 | */ |
||
306 | 20 | protected function filter_products_with_price( $item ) { |
|
307 | 20 | return $this->get_discounted_price_in_cents( $item ) > 0; |
|
308 | } |
||
309 | |||
310 | /** |
||
311 | * Get items which the coupon should be applied to. |
||
312 | * |
||
313 | * @since 3.2.0 |
||
314 | * @param object $coupon Coupon object. |
||
315 | * @return array |
||
316 | */ |
||
317 | 63 | protected function get_items_to_apply_coupon( $coupon ) { |
|
318 | 63 | $items_to_apply = array(); |
|
319 | |||
320 | 63 | foreach ( $this->get_items_to_validate() as $item ) { |
|
321 | 62 | $item_to_apply = clone $item; // Clone the item so changes to this item do not affect the originals. |
|
322 | |||
323 | 62 | if ( 0 === $this->get_discounted_price_in_cents( $item_to_apply ) || 0 >= $item_to_apply->quantity ) { |
|
324 | 1 | continue; |
|
325 | } |
||
326 | |||
327 | 62 | if ( ! $coupon->is_valid_for_product( $item_to_apply->product, $item_to_apply->object ) && ! $coupon->is_valid_for_cart() ) { |
|
328 | 2 | continue; |
|
329 | } |
||
330 | |||
331 | 62 | $items_to_apply[] = $item_to_apply; |
|
332 | } |
||
333 | 63 | return $items_to_apply; |
|
334 | } |
||
335 | |||
336 | /** |
||
337 | * Apply percent discount to items and return an array of discounts granted. |
||
338 | * |
||
339 | * @since 3.2.0 |
||
340 | * @param WC_Coupon $coupon Coupon object. Passed through filters. |
||
341 | * @param array $items_to_apply Array of items to apply the coupon to. |
||
342 | * @return int Total discounted. |
||
343 | */ |
||
344 | 33 | protected function apply_coupon_percent( $coupon, $items_to_apply ) { |
|
345 | 33 | $total_discount = 0; |
|
346 | 33 | $cart_total = 0; |
|
347 | 33 | $limit_usage_qty = 0; |
|
348 | 33 | $applied_count = 0; |
|
349 | 33 | $adjust_final_discount = true; |
|
350 | |||
351 | 33 | if ( null !== $coupon->get_limit_usage_to_x_items() ) { |
|
352 | 8 | $limit_usage_qty = $coupon->get_limit_usage_to_x_items(); |
|
353 | } |
||
354 | |||
355 | 33 | $coupon_amount = $coupon->get_amount(); |
|
356 | |||
357 | 33 | foreach ( $items_to_apply as $item ) { |
|
358 | // Find out how much price is available to discount for the item. |
||
359 | 32 | $discounted_price = $this->get_discounted_price_in_cents( $item ); |
|
360 | |||
361 | // Get the price we actually want to discount, based on settings. |
||
362 | 32 | $price_to_discount = ( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ) ? $discounted_price : round( $item->price ); |
|
363 | |||
364 | // See how many and what price to apply to. |
||
365 | 32 | $apply_quantity = $limit_usage_qty && ( $limit_usage_qty - $applied_count ) < $item->quantity ? $limit_usage_qty - $applied_count : $item->quantity; |
|
366 | 32 | $apply_quantity = max( 0, apply_filters( 'woocommerce_coupon_get_apply_quantity', $apply_quantity, $item, $coupon, $this ) ); |
|
367 | 32 | $price_to_discount = ( $price_to_discount / $item->quantity ) * $apply_quantity; |
|
368 | |||
369 | // Run coupon calculations. |
||
370 | 32 | $discount = floor( $price_to_discount * ( $coupon_amount / 100 ) ); |
|
371 | |||
372 | 32 | if ( is_a( $this->object, 'WC_Cart' ) && has_filter( 'woocommerce_coupon_get_discount_amount' ) ) { |
|
373 | // Send through the legacy filter, but not as cents. |
||
374 | 1 | $filtered_discount = wc_add_number_precision( apply_filters( 'woocommerce_coupon_get_discount_amount', wc_remove_number_precision( $discount ), wc_remove_number_precision( $price_to_discount ), $item->object, false, $coupon ) ); |
|
375 | |||
376 | 1 | if ( $filtered_discount !== $discount ) { |
|
377 | 1 | $discount = $filtered_discount; |
|
378 | 1 | $adjust_final_discount = false; |
|
379 | } |
||
380 | } |
||
381 | |||
382 | 32 | $discount = wc_round_discount( min( $discounted_price, $discount ), 0 ); |
|
383 | 32 | $cart_total = $cart_total + $price_to_discount; |
|
384 | 32 | $total_discount = $total_discount + $discount; |
|
385 | 32 | $applied_count = $applied_count + $apply_quantity; |
|
386 | |||
387 | // Store code and discount amount per item. |
||
388 | 32 | $this->discounts[ $coupon->get_code() ][ $item->key ] += $discount; |
|
389 | } |
||
390 | |||
391 | // Work out how much discount would have been given to the cart as a whole and compare to what was discounted on all line items. |
||
392 | 33 | $cart_total_discount = wc_round_discount( $cart_total * ( $coupon_amount / 100 ), 0 ); |
|
393 | |||
394 | 33 | if ( $total_discount < $cart_total_discount && $adjust_final_discount ) { |
|
395 | 2 | $total_discount += $this->apply_coupon_remainder( $coupon, $items_to_apply, $cart_total_discount - $total_discount ); |
|
396 | } |
||
397 | |||
398 | 33 | return $total_discount; |
|
399 | } |
||
400 | |||
401 | /** |
||
402 | * Apply fixed product discount to items. |
||
403 | * |
||
404 | * @since 3.2.0 |
||
405 | * @param WC_Coupon $coupon Coupon object. Passed through filters. |
||
406 | * @param array $items_to_apply Array of items to apply the coupon to. |
||
407 | * @param int $amount Fixed discount amount to apply in cents. Leave blank to pull from coupon. |
||
408 | * @return int Total discounted. |
||
409 | */ |
||
410 | 35 | protected function apply_coupon_fixed_product( $coupon, $items_to_apply, $amount = null ) { |
|
411 | 35 | $total_discount = 0; |
|
412 | 35 | $amount = $amount ? $amount : wc_add_number_precision( $coupon->get_amount() ); |
|
413 | 35 | $limit_usage_qty = 0; |
|
414 | 35 | $applied_count = 0; |
|
415 | |||
416 | 35 | if ( null !== $coupon->get_limit_usage_to_x_items() ) { |
|
417 | 9 | $limit_usage_qty = $coupon->get_limit_usage_to_x_items(); |
|
418 | } |
||
419 | |||
420 | 35 | foreach ( $items_to_apply as $item ) { |
|
421 | // Find out how much price is available to discount for the item. |
||
422 | 35 | $discounted_price = $this->get_discounted_price_in_cents( $item ); |
|
423 | |||
424 | // Get the price we actually want to discount, based on settings. |
||
425 | 35 | $price_to_discount = ( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ) ? $discounted_price : $item->price; |
|
426 | |||
427 | // Run coupon calculations. |
||
428 | 35 | if ( $limit_usage_qty ) { |
|
429 | 9 | $apply_quantity = $limit_usage_qty - $applied_count < $item->quantity ? $limit_usage_qty - $applied_count : $item->quantity; |
|
430 | 9 | $apply_quantity = max( 0, apply_filters( 'woocommerce_coupon_get_apply_quantity', $apply_quantity, $item, $coupon, $this ) ); |
|
431 | 9 | $discount = min( $amount, $item->price / $item->quantity ) * $apply_quantity; |
|
432 | } else { |
||
433 | 26 | $apply_quantity = apply_filters( 'woocommerce_coupon_get_apply_quantity', $item->quantity, $item, $coupon, $this ); |
|
434 | 26 | $discount = $amount * $apply_quantity; |
|
435 | } |
||
436 | |||
437 | 35 | if ( is_a( $this->object, 'WC_Cart' ) && has_filter( 'woocommerce_coupon_get_discount_amount' ) ) { |
|
438 | // Send through the legacy filter, but not as cents. |
||
439 | $discount = wc_add_number_precision( apply_filters( 'woocommerce_coupon_get_discount_amount', wc_remove_number_precision( $discount ), wc_remove_number_precision( $price_to_discount ), $item->object, false, $coupon ) ); |
||
440 | } |
||
441 | |||
442 | 35 | $discount = min( $discounted_price, $discount ); |
|
443 | 35 | $total_discount = $total_discount + $discount; |
|
444 | 35 | $applied_count = $applied_count + $apply_quantity; |
|
445 | |||
446 | // Store code and discount amount per item. |
||
447 | 35 | $this->discounts[ $coupon->get_code() ][ $item->key ] += $discount; |
|
448 | } |
||
449 | 35 | return $total_discount; |
|
450 | } |
||
451 | |||
452 | /** |
||
453 | * Apply fixed cart discount to items. |
||
454 | * |
||
455 | * @since 3.2.0 |
||
456 | * @param WC_Coupon $coupon Coupon object. Passed through filters. |
||
457 | * @param array $items_to_apply Array of items to apply the coupon to. |
||
458 | * @param int $amount Fixed discount amount to apply in cents. Leave blank to pull from coupon. |
||
459 | * @return int Total discounted. |
||
460 | */ |
||
461 | 20 | protected function apply_coupon_fixed_cart( $coupon, $items_to_apply, $amount = null ) { |
|
462 | 20 | $total_discount = 0; |
|
463 | 20 | $amount = $amount ? $amount : wc_add_number_precision( $coupon->get_amount() ); |
|
464 | 20 | $items_to_apply = array_filter( $items_to_apply, array( $this, 'filter_products_with_price' ) ); |
|
465 | 20 | $item_count = array_sum( wp_list_pluck( $items_to_apply, 'quantity' ) ); |
|
466 | |||
467 | 20 | if ( ! $item_count ) { |
|
468 | return $total_discount; |
||
469 | } |
||
470 | |||
471 | 20 | if ( ! $amount ) { |
|
472 | // If there is no amount we still send it through so filters are fired. |
||
473 | $total_discount = $this->apply_coupon_fixed_product( $coupon, $items_to_apply, 0 ); |
||
474 | } else { |
||
475 | 20 | $per_item_discount = absint( $amount / $item_count ); // round it down to the nearest cent. |
|
476 | |||
477 | 20 | if ( $per_item_discount > 0 ) { |
|
478 | 20 | $total_discount = $this->apply_coupon_fixed_product( $coupon, $items_to_apply, $per_item_discount ); |
|
479 | |||
480 | /** |
||
481 | * If there is still discount remaining, repeat the process. |
||
482 | */ |
||
483 | 20 | if ( $total_discount > 0 && $total_discount < $amount ) { |
|
484 | 20 | $total_discount += $this->apply_coupon_fixed_cart( $coupon, $items_to_apply, $amount - $total_discount ); |
|
485 | } |
||
486 | 5 | } elseif ( $amount > 0 ) { |
|
487 | 5 | $total_discount += $this->apply_coupon_remainder( $coupon, $items_to_apply, $amount ); |
|
488 | } |
||
489 | } |
||
490 | 20 | return $total_discount; |
|
491 | } |
||
492 | |||
493 | /** |
||
494 | * Apply custom coupon discount to items. |
||
495 | * |
||
496 | * @since 3.3 |
||
497 | * @param WC_Coupon $coupon Coupon object. Passed through filters. |
||
498 | * @param array $items_to_apply Array of items to apply the coupon to. |
||
499 | * @return int Total discounted. |
||
500 | */ |
||
501 | 1 | protected function apply_coupon_custom( $coupon, $items_to_apply ) { |
|
502 | 1 | $limit_usage_qty = 0; |
|
503 | 1 | $applied_count = 0; |
|
504 | |||
505 | 1 | if ( null !== $coupon->get_limit_usage_to_x_items() ) { |
|
506 | 1 | $limit_usage_qty = $coupon->get_limit_usage_to_x_items(); |
|
507 | } |
||
508 | |||
509 | // Apply the coupon to each item. |
||
510 | 1 | foreach ( $items_to_apply as $item ) { |
|
511 | // Find out how much price is available to discount for the item. |
||
512 | 1 | $discounted_price = $this->get_discounted_price_in_cents( $item ); |
|
513 | |||
514 | // Get the price we actually want to discount, based on settings. |
||
515 | 1 | $price_to_discount = wc_remove_number_precision( ( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ) ? $discounted_price : $item->price ); |
|
516 | |||
517 | // See how many and what price to apply to. |
||
518 | 1 | $apply_quantity = $limit_usage_qty && ( $limit_usage_qty - $applied_count ) < $item->quantity ? $limit_usage_qty - $applied_count : $item->quantity; |
|
519 | 1 | $apply_quantity = max( 0, apply_filters( 'woocommerce_coupon_get_apply_quantity', $apply_quantity, $item, $coupon, $this ) ); |
|
520 | |||
521 | // Run coupon calculations. |
||
522 | 1 | $discount = wc_add_number_precision( $coupon->get_discount_amount( $price_to_discount / $item->quantity, $item->object, true ) ) * $apply_quantity; |
|
523 | 1 | $discount = wc_round_discount( min( $discounted_price, $discount ), 0 ); |
|
524 | 1 | $applied_count = $applied_count + $apply_quantity; |
|
525 | |||
526 | // Store code and discount amount per item. |
||
527 | 1 | $this->discounts[ $coupon->get_code() ][ $item->key ] += $discount; |
|
528 | } |
||
529 | |||
530 | // Allow post-processing for custom coupon types (e.g. calculating discrepancy, etc). |
||
531 | 1 | $this->discounts[ $coupon->get_code() ] = apply_filters( 'woocommerce_coupon_custom_discounts_array', $this->discounts[ $coupon->get_code() ], $coupon ); |
|
532 | |||
533 | 1 | return array_sum( $this->discounts[ $coupon->get_code() ] ); |
|
534 | } |
||
535 | |||
536 | /** |
||
537 | * Deal with remaining fractional discounts by splitting it over items |
||
538 | * until the amount is expired, discounting 1 cent at a time. |
||
539 | * |
||
540 | * @since 3.2.0 |
||
541 | * @param WC_Coupon $coupon Coupon object if appliable. Passed through filters. |
||
542 | * @param array $items_to_apply Array of items to apply the coupon to. |
||
543 | * @param int $amount Fixed discount amount to apply. |
||
544 | * @return int Total discounted. |
||
545 | */ |
||
546 | 7 | protected function apply_coupon_remainder( $coupon, $items_to_apply, $amount ) { |
|
547 | 7 | $total_discount = 0; |
|
548 | |||
549 | 7 | foreach ( $items_to_apply as $item ) { |
|
550 | 7 | for ( $i = 0; $i < $item->quantity; $i ++ ) { |
|
551 | // Find out how much price is available to discount for the item. |
||
552 | 7 | $discounted_price = $this->get_discounted_price_in_cents( $item ); |
|
553 | |||
554 | // Get the price we actually want to discount, based on settings. |
||
555 | 7 | $price_to_discount = ( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ) ? $discounted_price : $item->price; |
|
556 | |||
557 | // Run coupon calculations. |
||
558 | 7 | $discount = min( $price_to_discount, 1 ); |
|
559 | |||
560 | // Store totals. |
||
561 | 7 | $total_discount += $discount; |
|
562 | |||
563 | // Store code and discount amount per item. |
||
564 | 7 | $this->discounts[ $coupon->get_code() ][ $item->key ] += $discount; |
|
565 | |||
566 | 7 | if ( $total_discount >= $amount ) { |
|
567 | 7 | break 2; |
|
568 | } |
||
569 | } |
||
570 | 3 | if ( $total_discount >= $amount ) { |
|
571 | break; |
||
572 | } |
||
573 | } |
||
574 | 7 | return $total_discount; |
|
575 | } |
||
576 | |||
577 | /** |
||
578 | * Ensure coupon exists or throw exception. |
||
579 | * |
||
580 | * @since 3.2.0 |
||
581 | * @throws Exception Error message. |
||
582 | * @param WC_Coupon $coupon Coupon data. |
||
583 | * @return bool |
||
584 | */ |
||
585 | 67 | protected function validate_coupon_exists( $coupon ) { |
|
586 | 67 | if ( ! $coupon->get_id() && ! $coupon->get_virtual() ) { |
|
587 | /* translators: %s: coupon code */ |
||
588 | throw new Exception( sprintf( __( 'Coupon "%s" does not exist!', 'woocommerce' ), $coupon->get_code() ), 105 ); |
||
589 | } |
||
590 | |||
591 | 67 | return true; |
|
592 | } |
||
593 | |||
594 | /** |
||
595 | * Ensure coupon usage limit is valid or throw exception. |
||
596 | * |
||
597 | * @since 3.2.0 |
||
598 | * @throws Exception Error message. |
||
599 | * @param WC_Coupon $coupon Coupon data. |
||
600 | * @return bool |
||
601 | */ |
||
602 | 67 | protected function validate_coupon_usage_limit( $coupon ) { |
|
603 | 67 | if ( $coupon->get_usage_limit() > 0 && $coupon->get_usage_count() >= $coupon->get_usage_limit() ) { |
|
604 | throw new Exception( __( 'Coupon usage limit has been reached.', 'woocommerce' ), 106 ); |
||
605 | } |
||
606 | |||
607 | 67 | return true; |
|
608 | } |
||
609 | |||
610 | /** |
||
611 | * Ensure coupon user usage limit is valid or throw exception. |
||
612 | * |
||
613 | * Per user usage limit - check here if user is logged in (against user IDs). |
||
614 | * Checked again for emails later on in WC_Cart::check_customer_coupons(). |
||
615 | * |
||
616 | * @since 3.2.0 |
||
617 | * @throws Exception Error message. |
||
618 | * @param WC_Coupon $coupon Coupon data. |
||
619 | * @param int $user_id User ID. |
||
620 | * @return bool |
||
621 | */ |
||
622 | 67 | protected function validate_coupon_user_usage_limit( $coupon, $user_id = 0 ) { |
|
623 | 67 | if ( empty( $user_id ) ) { |
|
624 | 67 | if ( $this->object instanceof WC_Order ) { |
|
625 | 8 | $user_id = $this->object->get_customer_id(); |
|
626 | } else { |
||
627 | 59 | $user_id = get_current_user_id(); |
|
628 | } |
||
629 | } |
||
630 | |||
631 | 67 | if ( $coupon && $user_id && apply_filters( 'woocommerce_coupon_validate_user_usage_limit', $coupon->get_usage_limit_per_user() > 0, $user_id, $coupon, $this ) && $coupon->get_id() && $coupon->get_data_store() ) { |
|
632 | $date_store = $coupon->get_data_store(); |
||
633 | $usage_count = $date_store->get_usage_by_user_id( $coupon, $user_id ); |
||
634 | if ( $usage_count >= $coupon->get_usage_limit_per_user() ) { |
||
635 | throw new Exception( __( 'Coupon usage limit has been reached.', 'woocommerce' ), 106 ); |
||
636 | } |
||
637 | } |
||
638 | |||
639 | 67 | return true; |
|
640 | } |
||
641 | |||
642 | /** |
||
643 | * Ensure coupon date is valid or throw exception. |
||
644 | * |
||
645 | * @since 3.2.0 |
||
646 | * @throws Exception Error message. |
||
647 | * @param WC_Coupon $coupon Coupon data. |
||
648 | * @return bool |
||
649 | */ |
||
650 | 67 | protected function validate_coupon_expiry_date( $coupon ) { |
|
651 | 67 | if ( $coupon->get_date_expires() && apply_filters( 'woocommerce_coupon_validate_expiry_date', current_time( 'timestamp', true ) > $coupon->get_date_expires()->getTimestamp(), $coupon, $this ) ) { |
|
652 | 1 | throw new Exception( __( 'This coupon has expired.', 'woocommerce' ), 107 ); |
|
653 | } |
||
654 | |||
655 | 67 | return true; |
|
656 | } |
||
657 | |||
658 | /** |
||
659 | * Ensure coupon amount is valid or throw exception. |
||
660 | * |
||
661 | * @since 3.2.0 |
||
662 | * @throws Exception Error message. |
||
663 | * @param WC_Coupon $coupon Coupon data. |
||
664 | * @return bool |
||
665 | */ |
||
666 | 67 | View Code Duplication | protected function validate_coupon_minimum_amount( $coupon ) { |
667 | 67 | $subtotal = wc_remove_number_precision( $this->get_object_subtotal() ); |
|
668 | |||
669 | 67 | if ( $coupon->get_minimum_amount() > 0 && apply_filters( 'woocommerce_coupon_validate_minimum_amount', $coupon->get_minimum_amount() > $subtotal, $coupon, $subtotal ) ) { |
|
670 | /* translators: %s: coupon minimum amount */ |
||
671 | throw new Exception( sprintf( __( 'The minimum spend for this coupon is %s.', 'woocommerce' ), wc_price( $coupon->get_minimum_amount() ) ), 108 ); |
||
672 | } |
||
673 | |||
674 | 67 | return true; |
|
675 | } |
||
676 | |||
677 | /** |
||
678 | * Ensure coupon amount is valid or throw exception. |
||
679 | * |
||
680 | * @since 3.2.0 |
||
681 | * @throws Exception Error message. |
||
682 | * @param WC_Coupon $coupon Coupon data. |
||
683 | * @return bool |
||
684 | */ |
||
685 | 67 | View Code Duplication | protected function validate_coupon_maximum_amount( $coupon ) { |
686 | 67 | $subtotal = wc_remove_number_precision( $this->get_object_subtotal() ); |
|
687 | |||
688 | 67 | if ( $coupon->get_maximum_amount() > 0 && apply_filters( 'woocommerce_coupon_validate_maximum_amount', $coupon->get_maximum_amount() < $subtotal, $coupon ) ) { |
|
689 | /* translators: %s: coupon maximum amount */ |
||
690 | throw new Exception( sprintf( __( 'The maximum spend for this coupon is %s.', 'woocommerce' ), wc_price( $coupon->get_maximum_amount() ) ), 112 ); |
||
691 | } |
||
692 | |||
693 | 67 | return true; |
|
694 | } |
||
695 | |||
696 | /** |
||
697 | * Ensure coupon is valid for products in the list is valid or throw exception. |
||
698 | * |
||
699 | * @since 3.2.0 |
||
700 | * @throws Exception Error message. |
||
701 | * @param WC_Coupon $coupon Coupon data. |
||
702 | * @return bool |
||
703 | */ |
||
704 | 67 | protected function validate_coupon_product_ids( $coupon ) { |
|
705 | 67 | if ( count( $coupon->get_product_ids() ) > 0 ) { |
|
706 | $valid = false; |
||
707 | |||
708 | View Code Duplication | foreach ( $this->get_items_to_validate() as $item ) { |
|
709 | if ( $item->product && in_array( $item->product->get_id(), $coupon->get_product_ids(), true ) || in_array( $item->product->get_parent_id(), $coupon->get_product_ids(), true ) ) { |
||
710 | $valid = true; |
||
711 | break; |
||
712 | } |
||
713 | } |
||
714 | |||
715 | if ( ! $valid ) { |
||
716 | throw new Exception( __( 'Sorry, this coupon is not applicable to selected products.', 'woocommerce' ), 109 ); |
||
717 | } |
||
718 | } |
||
719 | |||
720 | 67 | return true; |
|
721 | } |
||
722 | |||
723 | /** |
||
724 | * Ensure coupon is valid for product categories in the list is valid or throw exception. |
||
725 | * |
||
726 | * @since 3.2.0 |
||
727 | * @throws Exception Error message. |
||
728 | * @param WC_Coupon $coupon Coupon data. |
||
729 | * @return bool |
||
730 | */ |
||
731 | 67 | protected function validate_coupon_product_categories( $coupon ) { |
|
732 | 67 | if ( count( $coupon->get_product_categories() ) > 0 ) { |
|
733 | $valid = false; |
||
734 | |||
735 | foreach ( $this->get_items_to_validate() as $item ) { |
||
736 | if ( $coupon->get_exclude_sale_items() && $item->product && $item->product->is_on_sale() ) { |
||
737 | continue; |
||
738 | } |
||
739 | |||
740 | $product_cats = wc_get_product_cat_ids( $item->product->get_id() ); |
||
741 | |||
742 | if ( $item->product->get_parent_id() ) { |
||
743 | $product_cats = array_merge( $product_cats, wc_get_product_cat_ids( $item->product->get_parent_id() ) ); |
||
744 | } |
||
745 | |||
746 | // If we find an item with a cat in our allowed cat list, the coupon is valid. |
||
747 | if ( count( array_intersect( $product_cats, $coupon->get_product_categories() ) ) > 0 ) { |
||
748 | $valid = true; |
||
749 | break; |
||
750 | } |
||
751 | } |
||
752 | |||
753 | if ( ! $valid ) { |
||
754 | throw new Exception( __( 'Sorry, this coupon is not applicable to selected products.', 'woocommerce' ), 109 ); |
||
755 | } |
||
756 | } |
||
757 | |||
758 | 67 | return true; |
|
759 | } |
||
760 | |||
761 | /** |
||
762 | * Ensure coupon is valid for sale items in the list is valid or throw exception. |
||
763 | * |
||
764 | * @since 3.2.0 |
||
765 | * @throws Exception Error message. |
||
766 | * @param WC_Coupon $coupon Coupon data. |
||
767 | * @return bool |
||
768 | */ |
||
769 | 21 | protected function validate_coupon_sale_items( $coupon ) { |
|
770 | 21 | if ( $coupon->get_exclude_sale_items() ) { |
|
771 | 1 | $valid = true; |
|
772 | |||
773 | 1 | foreach ( $this->get_items_to_validate() as $item ) { |
|
774 | 1 | if ( $item->product && $item->product->is_on_sale() ) { |
|
775 | 1 | $valid = false; |
|
776 | 1 | break; |
|
777 | } |
||
778 | } |
||
779 | |||
780 | 1 | if ( ! $valid ) { |
|
781 | 1 | throw new Exception( __( 'Sorry, this coupon is not valid for sale items.', 'woocommerce' ), 110 ); |
|
782 | } |
||
783 | } |
||
784 | |||
785 | 21 | return true; |
|
786 | } |
||
787 | |||
788 | /** |
||
789 | * All exclusion rules must pass at the same time for a product coupon to be valid. |
||
790 | * |
||
791 | * @since 3.2.0 |
||
792 | * @throws Exception Error message. |
||
793 | * @param WC_Coupon $coupon Coupon data. |
||
794 | * @return bool |
||
795 | */ |
||
796 | 67 | protected function validate_coupon_excluded_items( $coupon ) { |
|
797 | 67 | $items = $this->get_items_to_validate(); |
|
798 | 67 | if ( ! empty( $items ) && $coupon->is_type( wc_get_product_coupon_types() ) ) { |
|
799 | 46 | $valid = false; |
|
800 | |||
801 | 46 | foreach ( $items as $item ) { |
|
802 | 46 | if ( $item->product && $coupon->is_valid_for_product( $item->product, $item->object ) ) { |
|
803 | 46 | $valid = true; |
|
804 | 46 | break; |
|
805 | } |
||
806 | } |
||
807 | |||
808 | 46 | if ( ! $valid ) { |
|
809 | throw new Exception( __( 'Sorry, this coupon is not applicable to selected products.', 'woocommerce' ), 109 ); |
||
810 | } |
||
811 | } |
||
812 | |||
813 | 67 | return true; |
|
814 | } |
||
815 | |||
816 | /** |
||
817 | * Cart discounts cannot be added if non-eligible product is found. |
||
818 | * |
||
819 | * @since 3.2.0 |
||
820 | * @throws Exception Error message. |
||
821 | * @param WC_Coupon $coupon Coupon data. |
||
822 | * @return bool |
||
823 | */ |
||
824 | 67 | protected function validate_coupon_eligible_items( $coupon ) { |
|
825 | 67 | if ( ! $coupon->is_type( wc_get_product_coupon_types() ) ) { |
|
826 | 21 | $this->validate_coupon_sale_items( $coupon ); |
|
827 | 21 | $this->validate_coupon_excluded_product_ids( $coupon ); |
|
828 | 21 | $this->validate_coupon_excluded_product_categories( $coupon ); |
|
829 | } |
||
830 | |||
831 | 67 | return true; |
|
832 | } |
||
833 | |||
834 | /** |
||
835 | * Exclude products. |
||
836 | * |
||
837 | * @since 3.2.0 |
||
838 | * @throws Exception Error message. |
||
839 | * @param WC_Coupon $coupon Coupon data. |
||
840 | * @return bool |
||
841 | */ |
||
842 | 21 | protected function validate_coupon_excluded_product_ids( $coupon ) { |
|
843 | // Exclude Products. |
||
844 | 21 | if ( count( $coupon->get_excluded_product_ids() ) > 0 ) { |
|
845 | $products = array(); |
||
846 | |||
847 | View Code Duplication | foreach ( $this->get_items_to_validate() as $item ) { |
|
848 | if ( $item->product && in_array( $item->product->get_id(), $coupon->get_excluded_product_ids(), true ) || in_array( $item->product->get_parent_id(), $coupon->get_excluded_product_ids(), true ) ) { |
||
849 | $products[] = $item->product->get_name(); |
||
850 | } |
||
851 | } |
||
852 | |||
853 | View Code Duplication | if ( ! empty( $products ) ) { |
|
854 | /* translators: %s: products list */ |
||
855 | throw new Exception( sprintf( __( 'Sorry, this coupon is not applicable to the products: %s.', 'woocommerce' ), implode( ', ', $products ) ), 113 ); |
||
856 | } |
||
857 | } |
||
858 | |||
859 | 21 | return true; |
|
860 | } |
||
861 | |||
862 | /** |
||
863 | * Exclude categories from product list. |
||
864 | * |
||
865 | * @since 3.2.0 |
||
866 | * @throws Exception Error message. |
||
867 | * @param WC_Coupon $coupon Coupon data. |
||
868 | * @return bool |
||
869 | */ |
||
870 | 21 | protected function validate_coupon_excluded_product_categories( $coupon ) { |
|
871 | 21 | if ( count( $coupon->get_excluded_product_categories() ) > 0 ) { |
|
872 | $categories = array(); |
||
873 | |||
874 | foreach ( $this->get_items_to_validate() as $item ) { |
||
875 | if ( ! $item->product ) { |
||
876 | continue; |
||
877 | } |
||
878 | |||
879 | $product_cats = wc_get_product_cat_ids( $item->product->get_id() ); |
||
880 | |||
881 | if ( $item->product->get_parent_id() ) { |
||
882 | $product_cats = array_merge( $product_cats, wc_get_product_cat_ids( $item->product->get_parent_id() ) ); |
||
883 | } |
||
884 | |||
885 | $cat_id_list = array_intersect( $product_cats, $coupon->get_excluded_product_categories() ); |
||
886 | View Code Duplication | if ( count( $cat_id_list ) > 0 ) { |
|
887 | foreach ( $cat_id_list as $cat_id ) { |
||
888 | $cat = get_term( $cat_id, 'product_cat' ); |
||
889 | $categories[] = $cat->name; |
||
890 | } |
||
891 | } |
||
892 | } |
||
893 | |||
894 | View Code Duplication | if ( ! empty( $categories ) ) { |
|
895 | /* translators: %s: categories list */ |
||
896 | throw new Exception( sprintf( __( 'Sorry, this coupon is not applicable to the categories: %s.', 'woocommerce' ), implode( ', ', array_unique( $categories ) ) ), 114 ); |
||
897 | } |
||
898 | } |
||
899 | |||
900 | 21 | return true; |
|
901 | } |
||
902 | |||
903 | /** |
||
904 | * Get the object subtotal |
||
905 | * |
||
906 | * @return int |
||
907 | */ |
||
908 | 67 | protected function get_object_subtotal() { |
|
909 | 67 | if ( is_a( $this->object, 'WC_Cart' ) ) { |
|
910 | 58 | return wc_add_number_precision( $this->object->get_displayed_subtotal() ); |
|
911 | 9 | } elseif ( is_a( $this->object, 'WC_Order' ) ) { |
|
912 | 8 | $subtotal = wc_add_number_precision( $this->object->get_subtotal() ); |
|
913 | |||
914 | 8 | if ( $this->object->get_prices_include_tax() ) { |
|
0 ignored issues
–
show
|
|||
915 | // Add tax to tax-exclusive subtotal. |
||
916 | 2 | $subtotal = $subtotal + wc_add_number_precision( round( $this->object->get_total_tax(), wc_get_price_decimals() ) ); |
|
917 | } |
||
918 | |||
919 | 8 | return $subtotal; |
|
920 | } else { |
||
921 | 1 | return array_sum( wp_list_pluck( $this->items, 'price' ) ); |
|
922 | } |
||
923 | } |
||
924 | |||
925 | /** |
||
926 | * Check if a coupon is valid. |
||
927 | * |
||
928 | * Error Codes: |
||
929 | * - 100: Invalid filtered. |
||
930 | * - 101: Invalid removed. |
||
931 | * - 102: Not yours removed. |
||
932 | * - 103: Already applied. |
||
933 | * - 104: Individual use only. |
||
934 | * - 105: Not exists. |
||
935 | * - 106: Usage limit reached. |
||
936 | * - 107: Expired. |
||
937 | * - 108: Minimum spend limit not met. |
||
938 | * - 109: Not applicable. |
||
939 | * - 110: Not valid for sale items. |
||
940 | * - 111: Missing coupon code. |
||
941 | * - 112: Maximum spend limit met. |
||
942 | * - 113: Excluded products. |
||
943 | * - 114: Excluded categories. |
||
944 | * |
||
945 | * @since 3.2.0 |
||
946 | * @throws Exception Error message. |
||
947 | * @param WC_Coupon $coupon Coupon data. |
||
948 | * @return bool|WP_Error |
||
949 | */ |
||
950 | 67 | public function is_coupon_valid( $coupon ) { |
|
951 | try { |
||
952 | 67 | $this->validate_coupon_exists( $coupon ); |
|
953 | 67 | $this->validate_coupon_usage_limit( $coupon ); |
|
954 | 67 | $this->validate_coupon_user_usage_limit( $coupon ); |
|
955 | 67 | $this->validate_coupon_expiry_date( $coupon ); |
|
956 | 67 | $this->validate_coupon_minimum_amount( $coupon ); |
|
957 | 67 | $this->validate_coupon_maximum_amount( $coupon ); |
|
958 | 67 | $this->validate_coupon_product_ids( $coupon ); |
|
959 | 67 | $this->validate_coupon_product_categories( $coupon ); |
|
960 | 67 | $this->validate_coupon_excluded_items( $coupon ); |
|
961 | 67 | $this->validate_coupon_eligible_items( $coupon ); |
|
962 | |||
963 | 67 | if ( ! apply_filters( 'woocommerce_coupon_is_valid', true, $coupon, $this ) ) { |
|
964 | 67 | throw new Exception( __( 'Coupon is not valid.', 'woocommerce' ), 100 ); |
|
965 | } |
||
966 | 2 | } catch ( Exception $e ) { |
|
967 | /** |
||
968 | * Filter the coupon error message. |
||
969 | * |
||
970 | * @param string $error_message Error message. |
||
971 | * @param int $error_code Error code. |
||
972 | * @param WC_Coupon $coupon Coupon data. |
||
973 | */ |
||
974 | 2 | $message = apply_filters( 'woocommerce_coupon_error', is_numeric( $e->getMessage() ) ? $coupon->get_coupon_error( $e->getMessage() ) : $e->getMessage(), $e->getCode(), $coupon ); |
|
975 | |||
976 | 2 | return new WP_Error( |
|
977 | 2 | 'invalid_coupon', |
|
978 | $message, |
||
979 | array( |
||
980 | 2 | 'status' => 400, |
|
981 | ) |
||
982 | ); |
||
983 | } |
||
984 | 67 | return true; |
|
985 | } |
||
986 | } |
||
987 |
It seems like the method you are trying to call exists only in some of the possible types.
Let’s take a look at an example:
Available Fixes
Add an additional type-check:
Only allow a single type to be passed if the variable comes from a parameter: