Completed
Push — master ( 77191a...60b385 )
by Gerhard
07:08
created

includes/class-wc-tax.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * Tax calculation and rate finding class.
4
 *
5
 * @package WooCommerce/Classes
6
 */
7
8
defined( 'ABSPATH' ) || exit;
9
10
/**
11
 * Performs tax calculations and loads tax rates
12
 *
13
 * @class WC_Tax
14
 */
15
class WC_Tax {
16
17
	/**
18
	 * Precision.
19
	 *
20
	 * @var int
21
	 */
22
	public static $precision;
23
24
	/**
25
	 * Round at subtotal.
26
	 *
27
	 * @var bool
28
	 */
29
	public static $round_at_subtotal = false;
30
31
	/**
32
	 * Load options.
33
	 */
34
	public static function init() {
35
		self::$precision         = wc_get_rounding_precision();
36
		self::$round_at_subtotal = 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' );
37
	}
38
39
	/**
40
	 * Calculate tax for a line.
41
	 *
42
	 * @param  float   $price              Price to calc tax on.
43
	 * @param  array   $rates              Rates to apply.
44
	 * @param  boolean $price_includes_tax Whether the passed price has taxes included.
45
	 * @param  boolean $deprecated         Whether to suppress any rounding from taking place. No longer used here.
46
	 * @return array                       Array of rates + prices after tax.
47
	 */
48 43
	public static function calc_tax( $price, $rates, $price_includes_tax = false, $deprecated = false ) {
49 43
		if ( $price_includes_tax ) {
50 14
			$taxes = self::calc_inclusive_tax( $price, $rates );
51
		} else {
52 36
			$taxes = self::calc_exclusive_tax( $price, $rates );
53
		}
54 43
		return apply_filters( 'woocommerce_calc_tax', $taxes, $price, $rates, $price_includes_tax, $deprecated );
55
	}
56
57
	/**
58
	 * Calculate the shipping tax using a passed array of rates.
59
	 *
60
	 * @param float $price Shipping cost.
61
	 * @param array $rates Taxation Rate.
62
	 * @return array
63
	 */
64 65
	public static function calc_shipping_tax( $price, $rates ) {
65 65
		$taxes = self::calc_exclusive_tax( $price, $rates );
66 65
		return apply_filters( 'woocommerce_calc_shipping_tax', $taxes, $price, $rates );
67
	}
68
69
	/**
70
	 * Round to precision.
71
	 *
72
	 * Filter example: to return rounding to .5 cents you'd use:
73
	 *
74
	 * function euro_5cent_rounding( $in ) {
75
	 *      return round( $in / 5, 2 ) * 5;
76
	 * }
77
	 * add_filter( 'woocommerce_tax_round', 'euro_5cent_rounding' );
78
	 *
79
	 * @param float|int $in Value to round.
80
	 * @return float
81
	 */
82 42
	public static function round( $in ) {
83 42
		return apply_filters( 'woocommerce_tax_round', round( $in, wc_get_rounding_precision() ), $in );
84
	}
85
86
	/**
87
	 * Calc tax from inclusive price.
88
	 *
89
	 * @param  float $price Price to calculate tax for.
90
	 * @param  array $rates Array of tax rates.
91
	 * @return array
92
	 */
93 14
	public static function calc_inclusive_tax( $price, $rates ) {
94 14
		$taxes          = array();
95 14
		$compound_rates = array();
96 14
		$regular_rates  = array();
97
98
		// Index array so taxes are output in correct order and see what compound/regular rates we have to calculate.
99 14
		foreach ( $rates as $key => $rate ) {
100 14
			$taxes[ $key ] = 0;
101
102 14
			if ( 'yes' === $rate['compound'] ) {
103 1
				$compound_rates[ $key ] = $rate['rate'];
104
			} else {
105 14
				$regular_rates[ $key ] = $rate['rate'];
106
			}
107
		}
108
109 14
		$compound_rates = array_reverse( $compound_rates, true ); // Working backwards.
110
111 14
		$non_compound_price = $price;
112
113 14
		foreach ( $compound_rates as $key => $compound_rate ) {
114 1
			$tax_amount         = apply_filters( 'woocommerce_price_inc_tax_amount', $non_compound_price - ( $non_compound_price / ( 1 + ( $compound_rate / 100 ) ) ), $key, $rates[ $key ], $price );
115 1
			$taxes[ $key ]     += $tax_amount;
116 1
			$non_compound_price = $non_compound_price - $tax_amount;
117
		}
118
119
		// Regular taxes.
120 14
		$regular_tax_rate = 1 + ( array_sum( $regular_rates ) / 100 );
121
122 14
		foreach ( $regular_rates as $key => $regular_rate ) {
123 14
			$the_rate       = ( $regular_rate / 100 ) / $regular_tax_rate;
124 14
			$net_price      = $price - ( $the_rate * $non_compound_price );
125 14
			$tax_amount     = apply_filters( 'woocommerce_price_inc_tax_amount', $price - $net_price, $key, $rates[ $key ], $price );
126 14
			$taxes[ $key ] += $tax_amount;
127
		}
128
129
		/**
130
		 * Round all taxes to precision (4DP) before passing them back. Note, this is not the same rounding
131
		 * as in the cart calculation class which, depending on settings, will round to 2DP when calculating
132
		 * final totals. Also unlike that class, this rounds .5 up for all cases.
133
		 */
134 14
		$taxes = array_map( array( __CLASS__, 'round' ), $taxes );
135
136 14
		return $taxes;
137
	}
138
139
	/**
140
	 * Calc tax from exclusive price.
141
	 *
142
	 * @param  float $price Price to calculate tax for.
143
	 * @param  array $rates Array of tax rates.
144
	 * @return array
145
	 */
146 92
	public static function calc_exclusive_tax( $price, $rates ) {
147 92
		$taxes = array();
148
149 92
		if ( ! empty( $rates ) ) {
150 36
			foreach ( $rates as $key => $rate ) {
151 36
				if ( 'yes' === $rate['compound'] ) {
152 1
					continue;
153
				}
154
155 36
				$tax_amount = $price * ( $rate['rate'] / 100 );
156 36
				$tax_amount = apply_filters( 'woocommerce_price_ex_tax_amount', $tax_amount, $key, $rate, $price ); // ADVANCED: Allow third parties to modify this rate.
157
158 36 View Code Duplication
				if ( ! isset( $taxes[ $key ] ) ) {
159 36
					$taxes[ $key ] = $tax_amount;
160
				} else {
161
					$taxes[ $key ] += $tax_amount;
162
				}
163
			}
164
165 36
			$pre_compound_total = array_sum( $taxes );
166
167
			// Compound taxes.
168 36
			foreach ( $rates as $key => $rate ) {
169 36
				if ( 'no' === $rate['compound'] ) {
170 36
					continue;
171
				}
172 1
				$the_price_inc_tax = $price + ( $pre_compound_total );
173 1
				$tax_amount        = $the_price_inc_tax * ( $rate['rate'] / 100 );
174 1
				$tax_amount        = apply_filters( 'woocommerce_price_ex_tax_amount', $tax_amount, $key, $rate, $price, $the_price_inc_tax, $pre_compound_total ); // ADVANCED: Allow third parties to modify this rate.
175
176 1 View Code Duplication
				if ( ! isset( $taxes[ $key ] ) ) {
177 1
					$taxes[ $key ] = $tax_amount;
178
				} else {
179
					$taxes[ $key ] += $tax_amount;
180
				}
181
182 1
				$pre_compound_total = array_sum( $taxes );
183
			}
184
		}
185
186
		/**
187
		 * Round all taxes to precision (4DP) before passing them back. Note, this is not the same rounding
188
		 * as in the cart calculation class which, depending on settings, will round to 2DP when calculating
189
		 * final totals. Also unlike that class, this rounds .5 up for all cases.
190
		 */
191 92
		$taxes = array_map( array( __CLASS__, 'round' ), $taxes );
192
193 92
		return $taxes;
194
	}
195
196
	/**
197
	 * Searches for all matching country/state/postcode tax rates.
198
	 *
199
	 * @param array $args Args that determine the rate to find.
200
	 * @return array
201
	 */
202 102
	public static function find_rates( $args = array() ) {
203 102
		$args = wp_parse_args(
204 102
			$args,
205
			array(
206 102
				'country'   => '',
207
				'state'     => '',
208
				'city'      => '',
209
				'postcode'  => '',
210
				'tax_class' => '',
211
			)
212
		);
213
214 102
		$country   = $args['country'];
215 102
		$state     = $args['state'];
216 102
		$city      = $args['city'];
217 102
		$postcode  = wc_normalize_postcode( wc_clean( $args['postcode'] ) );
218 102
		$tax_class = $args['tax_class'];
219
220 102
		if ( ! $country ) {
221
			return array();
222
		}
223
224 102
		$cache_key         = WC_Cache_Helper::get_cache_prefix( 'taxes' ) . 'wc_tax_rates_' . md5( sprintf( '%s+%s+%s+%s+%s', $country, $state, $city, $postcode, $tax_class ) );
225 102
		$matched_tax_rates = wp_cache_get( $cache_key, 'taxes' );
226
227 102
		if ( false === $matched_tax_rates ) {
228 102
			$matched_tax_rates = self::get_matched_tax_rates( $country, $state, $postcode, $city, $tax_class );
229 102
			wp_cache_set( $cache_key, $matched_tax_rates, 'taxes' );
230
		}
231
232 102
		return apply_filters( 'woocommerce_find_rates', $matched_tax_rates, $args );
233
	}
234
235
	/**
236
	 * Searches for all matching country/state/postcode tax rates.
237
	 *
238
	 * @param array $args Args that determine the rate to find.
239
	 * @return array
240
	 */
241 69
	public static function find_shipping_rates( $args = array() ) {
242 69
		$rates          = self::find_rates( $args );
243 69
		$shipping_rates = array();
244
245 69
		if ( is_array( $rates ) ) {
246 69
			foreach ( $rates as $key => $rate ) {
247 13
				if ( 'yes' === $rate['shipping'] ) {
248 12
					$shipping_rates[ $key ] = $rate;
249
				}
250
			}
251
		}
252
253 69
		return $shipping_rates;
254
	}
255
256
	/**
257
	 * Does the sort comparison. Compares (in this order):
258
	 * - Priority
259
	 * - Country
260
	 * - State
261
	 * - Number of postcodes
262
	 * - Number of cities
263
	 * - ID
264
	 *
265
	 * @param object $rate1 First rate to compare.
266
	 * @param object $rate2 Second rate to compare.
267
	 * @return int
268
	 */
269 11
	private static function sort_rates_callback( $rate1, $rate2 ) {
270 11
		if ( $rate1->tax_rate_priority !== $rate2->tax_rate_priority ) {
271 4
			return $rate1->tax_rate_priority < $rate2->tax_rate_priority ? -1 : 1; // ASC.
272
		}
273
274 7 View Code Duplication
		if ( $rate1->tax_rate_country !== $rate2->tax_rate_country ) {
275 3
			if ( '' === $rate1->tax_rate_country ) {
276 3
				return 1;
277
			}
278
			if ( '' === $rate2->tax_rate_country ) {
279
				return -1;
280
			}
281
			return strcmp( $rate1->tax_rate_country, $rate2->tax_rate_country ) > 0 ? 1 : -1;
282
		}
283
284 4 View Code Duplication
		if ( $rate1->tax_rate_state !== $rate2->tax_rate_state ) {
285
			if ( '' === $rate1->tax_rate_state ) {
286
				return 1;
287
			}
288
			if ( '' === $rate2->tax_rate_state ) {
289
				return -1;
290
			}
291
			return strcmp( $rate1->tax_rate_state, $rate2->tax_rate_state ) > 0 ? 1 : -1;
292
		}
293
294 4 View Code Duplication
		if ( isset( $rate1->postcode_count, $rate2->postcode_count ) && $rate1->postcode_count !== $rate2->postcode_count ) {
295
			return $rate1->postcode_count < $rate2->postcode_count ? 1 : -1;
296
		}
297
298 4 View Code Duplication
		if ( isset( $rate1->city_count, $rate2->city_count ) && $rate1->city_count !== $rate2->city_count ) {
299
			return $rate1->city_count < $rate2->city_count ? 1 : -1;
300
		}
301
302 4
		return $rate1->tax_rate_id < $rate2->tax_rate_id ? -1 : 1;
303
	}
304
305
	/**
306
	 * Logical sort order for tax rates based on the following in order of priority.
307
	 *
308
	 * @param  array $rates Rates to be sorted.
309
	 * @return array
310
	 */
311 102
	private static function sort_rates( $rates ) {
312 102
		uasort( $rates, __CLASS__ . '::sort_rates_callback' );
313 102
		$i = 0;
314 102
		foreach ( $rates as $key => $rate ) {
315 46
			$rates[ $key ]->tax_rate_order = $i++;
316
		}
317 102
		return $rates;
318
	}
319
320
	/**
321
	 * Loop through a set of tax rates and get the matching rates (1 per priority).
322
	 *
323
	 * @param  string $country Country code to match against.
324
	 * @param  string $state State code to match against.
325
	 * @param  string $postcode Postcode to match against.
326
	 * @param  string $city City to match against.
327
	 * @param  string $tax_class Tax class to match against.
328
	 * @return array
329
	 */
330 102
	private static function get_matched_tax_rates( $country, $state, $postcode, $city, $tax_class ) {
331
		global $wpdb;
332
333
		// Query criteria - these will be ANDed.
334 102
		$criteria   = array();
335 102
		$criteria[] = $wpdb->prepare( "tax_rate_country IN ( %s, '' )", strtoupper( $country ) );
336 102
		$criteria[] = $wpdb->prepare( "tax_rate_state IN ( %s, '' )", strtoupper( $state ) );
337 102
		$criteria[] = $wpdb->prepare( 'tax_rate_class = %s', sanitize_title( $tax_class ) );
338
339
		// Pre-query postcode ranges for PHP based matching.
340 102
		$postcode_search = wc_get_wildcard_postcodes( $postcode, $country );
341 102
		$postcode_ranges = $wpdb->get_results( "SELECT tax_rate_id, location_code FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE location_type = 'postcode' AND location_code LIKE '%...%';" );
342
343 102
		if ( $postcode_ranges ) {
344
			$matches = wc_postcode_location_matcher( $postcode, $postcode_ranges, 'tax_rate_id', 'location_code', $country );
345
			if ( ! empty( $matches ) ) {
346
				foreach ( $matches as $matched_postcodes ) {
347
					$postcode_search = array_merge( $postcode_search, $matched_postcodes );
348
				}
349
			}
350
		}
351
352 102
		$postcode_search = array_unique( $postcode_search );
353
354
		/**
355
		 * Location matching criteria - ORed
356
		 * Needs to match:
357
		 * - rates with no postcodes and cities
358
		 * - rates with a matching postcode and city
359
		 * - rates with matching postcode, no city
360
		 * - rates with matching city, no postcode
361
		 */
362 102
		$locations_criteria   = array();
363 102
		$locations_criteria[] = 'locations.location_type IS NULL';
364 102
		$locations_criteria[] = "
365 102
			locations.location_type = 'postcode' AND locations.location_code IN ('" . implode( "','", array_map( 'esc_sql', $postcode_search ) ) . "')
366
			AND (
367 102
				( locations2.location_type = 'city' AND locations2.location_code = '" . esc_sql( strtoupper( $city ) ) . "' )
368
				OR NOT EXISTS (
369 102
					SELECT sub.tax_rate_id FROM {$wpdb->prefix}woocommerce_tax_rate_locations as sub
370
					WHERE sub.location_type = 'city'
371
					AND sub.tax_rate_id = tax_rates.tax_rate_id
372
				)
373
			)
374
		";
375 102
		$locations_criteria[] = "
376 102
			locations.location_type = 'city' AND locations.location_code = '" . esc_sql( strtoupper( $city ) ) . "'
377
			AND NOT EXISTS (
378 102
				SELECT sub.tax_rate_id FROM {$wpdb->prefix}woocommerce_tax_rate_locations as sub
379
				WHERE sub.location_type = 'postcode'
380
				AND sub.tax_rate_id = tax_rates.tax_rate_id
381
			)
382
		";
383
384 102
		$criteria[] = '( ( ' . implode( ' ) OR ( ', $locations_criteria ) . ' ) )';
385
386 102
		$criteria_string = implode( ' AND ', $criteria );
387
388
		// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
389 102
		$found_rates = $wpdb->get_results(
390
			"
391
			SELECT tax_rates.*, COUNT( locations.location_id ) as postcode_count, COUNT( locations2.location_id ) as city_count
392 102
			FROM {$wpdb->prefix}woocommerce_tax_rates as tax_rates
393 102
			LEFT OUTER JOIN {$wpdb->prefix}woocommerce_tax_rate_locations as locations ON tax_rates.tax_rate_id = locations.tax_rate_id
394 102
			LEFT OUTER JOIN {$wpdb->prefix}woocommerce_tax_rate_locations as locations2 ON tax_rates.tax_rate_id = locations2.tax_rate_id
395 102
			WHERE 1=1 AND {$criteria_string}
396
			GROUP BY tax_rates.tax_rate_id
397
			ORDER BY tax_rates.tax_rate_priority
398
			"
399
		);
400
		// phpcs:enable
401
402 102
		$found_rates       = self::sort_rates( $found_rates );
403 102
		$matched_tax_rates = array();
404 102
		$found_priority    = array();
405
406 102
		foreach ( $found_rates as $found_rate ) {
407 46
			if ( in_array( $found_rate->tax_rate_priority, $found_priority, true ) ) {
408 7
				continue;
409
			}
410
411 46
			$matched_tax_rates[ $found_rate->tax_rate_id ] = array(
412 46
				'rate'     => (float) $found_rate->tax_rate,
413 46
				'label'    => $found_rate->tax_rate_name,
414 46
				'shipping' => $found_rate->tax_rate_shipping ? 'yes' : 'no',
415 46
				'compound' => $found_rate->tax_rate_compound ? 'yes' : 'no',
416
			);
417
418 46
			$found_priority[] = $found_rate->tax_rate_priority;
419
		}
420
421 102
		return apply_filters( 'woocommerce_matched_tax_rates', $matched_tax_rates, $country, $state, $postcode, $city, $tax_class );
422
	}
423
424
	/**
425
	 * Get the customer tax location based on their status and the current page.
426
	 *
427
	 * Used by get_rates(), get_shipping_rates().
428
	 *
429
	 * @param  string $tax_class string Optional, passed to the filter for advanced tax setups.
430
	 * @param  object $customer Override the customer object to get their location.
431
	 * @return array
432
	 */
433 91
	public static function get_tax_location( $tax_class = '', $customer = null ) {
434 91
		$location = array();
435
436 91
		if ( is_null( $customer ) && WC()->customer ) {
437 76
			$customer = WC()->customer;
438
		}
439
440 91
		if ( ! empty( $customer ) ) {
441 91
			$location = $customer->get_taxable_address();
442
		} elseif ( wc_prices_include_tax() || 'base' === get_option( 'woocommerce_default_customer_address' ) || 'base' === get_option( 'woocommerce_tax_based_on' ) ) {
443
			$location = array(
444
				WC()->countries->get_base_country(),
445
				WC()->countries->get_base_state(),
446
				WC()->countries->get_base_postcode(),
447
				WC()->countries->get_base_city(),
448
			);
449
		}
450
451 91
		return apply_filters( 'woocommerce_get_tax_location', $location, $tax_class, $customer );
452
	}
453
454
	/**
455
	 * Get's an array of matching rates for a tax class.
456
	 *
457
	 * @param string $tax_class Tax class to get rates for.
458
	 * @param object $customer Override the customer object to get their location.
459
	 * @return  array
460
	 */
461 36
	public static function get_rates( $tax_class = '', $customer = null ) {
462 36
		$tax_class         = sanitize_title( $tax_class );
463 36
		$location          = self::get_tax_location( $tax_class, $customer );
464 36
		$matched_tax_rates = array();
465
466 36
		if ( count( $location ) === 4 ) {
467 36
			list( $country, $state, $postcode, $city ) = $location;
468
469 36
			$matched_tax_rates = self::find_rates(
470
				array(
471 36
					'country'   => $country,
472 36
					'state'     => $state,
473 36
					'postcode'  => $postcode,
474 36
					'city'      => $city,
475 36
					'tax_class' => $tax_class,
476
				)
477
			);
478
		}
479
480 36
		return apply_filters( 'woocommerce_matched_rates', $matched_tax_rates, $tax_class );
481
	}
482
483
	/**
484
	 * Get's an array of matching rates for the shop's base country.
485
	 *
486
	 * @param string $tax_class Tax Class.
487
	 * @return array
488
	 */
489 11
	public static function get_base_tax_rates( $tax_class = '' ) {
490 11
		return apply_filters(
491 11
			'woocommerce_base_tax_rates',
492 11
			self::find_rates(
493
				array(
494 11
					'country'   => WC()->countries->get_base_country(),
495 11
					'state'     => WC()->countries->get_base_state(),
496 11
					'postcode'  => WC()->countries->get_base_postcode(),
497 11
					'city'      => WC()->countries->get_base_city(),
498 11
					'tax_class' => $tax_class,
499
				)
500
			),
501 11
			$tax_class
502
		);
503
	}
504
505
	/**
506
	 * Alias for get_base_tax_rates().
507
	 *
508
	 * @deprecated 2.3
509
	 * @param string $tax_class Tax Class.
510
	 * @return array
511
	 */
512
	public static function get_shop_base_rate( $tax_class = '' ) {
513
		return self::get_base_tax_rates( $tax_class );
514
	}
515
516
	/**
517
	 * Gets an array of matching shipping tax rates for a given class.
518
	 *
519
	 * @param string $tax_class Tax class to get rates for.
520
	 * @param object $customer Override the customer object to get their location.
521
	 * @return mixed
522
	 */
523 65
	public static function get_shipping_tax_rates( $tax_class = null, $customer = null ) {
524
		// See if we have an explicitly set shipping tax class.
525 65
		$shipping_tax_class = get_option( 'woocommerce_shipping_tax_class' );
526
527 65
		if ( 'inherit' !== $shipping_tax_class ) {
528
			$tax_class = $shipping_tax_class;
529
		}
530
531 65
		$location          = self::get_tax_location( $tax_class, $customer );
532 65
		$matched_tax_rates = array();
533
534 65
		if ( 4 === count( $location ) ) {
535 65
			list( $country, $state, $postcode, $city ) = $location;
536
537 65
			if ( ! is_null( $tax_class ) ) {
538
				// This will be per item shipping.
539
				$matched_tax_rates = self::find_shipping_rates(
540
					array(
541
						'country'   => $country,
542
						'state'     => $state,
543
						'postcode'  => $postcode,
544
						'city'      => $city,
545
						'tax_class' => $tax_class,
546
					)
547
				);
548
549 65
			} elseif ( WC()->cart->get_cart() ) {
550
551
				// This will be per order shipping - loop through the order and find the highest tax class rate.
552 6
				$cart_tax_classes = WC()->cart->get_cart_item_tax_classes_for_shipping();
553
554
				// No tax classes = no taxable items.
555 6
				if ( empty( $cart_tax_classes ) ) {
556
					return array();
557
				}
558
559
				// If multiple classes are found, use the first one found unless a standard rate item is found. This will be the first listed in the 'additional tax class' section.
560 6
				if ( count( $cart_tax_classes ) > 1 && ! in_array( '', $cart_tax_classes, true ) ) {
561
					$tax_classes = self::get_tax_class_slugs();
562
563
					foreach ( $tax_classes as $tax_class ) {
564
						if ( in_array( $tax_class, $cart_tax_classes, true ) ) {
565
							$matched_tax_rates = self::find_shipping_rates(
566
								array(
567
									'country'   => $country,
568
									'state'     => $state,
569
									'postcode'  => $postcode,
570
									'city'      => $city,
571
									'tax_class' => $tax_class,
572
								)
573
							);
574
							break;
575
						}
576
					}
577 6
				} elseif ( 1 === count( $cart_tax_classes ) ) {
578
					// If a single tax class is found, use it.
579 6
					$matched_tax_rates = self::find_shipping_rates(
580
						array(
581 6
							'country'   => $country,
582 6
							'state'     => $state,
583 6
							'postcode'  => $postcode,
584 6
							'city'      => $city,
585 6
							'tax_class' => $cart_tax_classes[0],
586
						)
587
					);
588
				}
589
			}
590
591
			// Get standard rate if no taxes were found.
592 65
			if ( ! count( $matched_tax_rates ) ) {
593 59
				$matched_tax_rates = self::find_shipping_rates(
594
					array(
595 59
						'country'  => $country,
596 59
						'state'    => $state,
597 59
						'postcode' => $postcode,
598 59
						'city'     => $city,
599
					)
600
				);
601
			}
602
		}
603
604 65
		return $matched_tax_rates;
605
	}
606
607
	/**
608
	 * Return true/false depending on if a rate is a compound rate.
609
	 *
610
	 * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format.
611
	 * @return  bool
612
	 */
613 12 View Code Duplication
	public static function is_compound( $key_or_rate ) {
0 ignored issues
show
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
614
		global $wpdb;
615
616 12
		if ( is_object( $key_or_rate ) ) {
617 10
			$key      = $key_or_rate->tax_rate_id;
618 10
			$compound = $key_or_rate->tax_rate_compound;
619
		} else {
620 3
			$key      = $key_or_rate;
621 3
			$compound = (bool) $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate_compound FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) );
622
		}
623
624 12
		return (bool) apply_filters( 'woocommerce_rate_compound', $compound, $key );
625
	}
626
627
	/**
628
	 * Return a given rates label.
629
	 *
630
	 * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format.
631
	 * @return  string
632
	 */
633 12
	public static function get_rate_label( $key_or_rate ) {
634
		global $wpdb;
635
636 12
		if ( is_object( $key_or_rate ) ) {
637 10
			$key       = $key_or_rate->tax_rate_id;
638 10
			$rate_name = $key_or_rate->tax_rate_name;
639
		} else {
640 3
			$key       = $key_or_rate;
641 3
			$rate_name = $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate_name FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) );
642
		}
643
644 12
		if ( ! $rate_name ) {
645 1
			$rate_name = WC()->countries->tax_or_vat();
646
		}
647
648 12
		return apply_filters( 'woocommerce_rate_label', $rate_name, $key );
649
	}
650
651
	/**
652
	 * Return a given rates percent.
653
	 *
654
	 * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format.
655
	 * @return  string
656
	 */
657 1 View Code Duplication
	public static function get_rate_percent( $key_or_rate ) {
0 ignored issues
show
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
658
		global $wpdb;
659
660 1
		if ( is_object( $key_or_rate ) ) {
661
			$key      = $key_or_rate->tax_rate_id;
662
			$tax_rate = $key_or_rate->tax_rate;
663
		} else {
664 1
			$key      = $key_or_rate;
665 1
			$tax_rate = $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) );
666
		}
667
668 1
		return apply_filters( 'woocommerce_rate_percent', floatval( $tax_rate ) . '%', $key );
669
	}
670
671
	/**
672
	 * Get a rates code. Code is made up of COUNTRY-STATE-NAME-Priority. E.g GB-VAT-1, US-AL-TAX-1.
673
	 *
674
	 * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format.
675
	 * @return string
676
	 */
677 12
	public static function get_rate_code( $key_or_rate ) {
678
		global $wpdb;
679
680 12
		if ( is_object( $key_or_rate ) ) {
681 10
			$key  = $key_or_rate->tax_rate_id;
682 10
			$rate = $key_or_rate;
683
		} else {
684 3
			$key  = $key_or_rate;
685 3
			$rate = $wpdb->get_row( $wpdb->prepare( "SELECT tax_rate_country, tax_rate_state, tax_rate_name, tax_rate_priority FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) );
686
		}
687
688 12
		$code_string = '';
689
690 12
		if ( null !== $rate ) {
691 12
			$code        = array();
692 12
			$code[]      = $rate->tax_rate_country;
693 12
			$code[]      = $rate->tax_rate_state;
694 12
			$code[]      = $rate->tax_rate_name ? $rate->tax_rate_name : 'TAX';
695 12
			$code[]      = absint( $rate->tax_rate_priority );
696 12
			$code_string = strtoupper( implode( '-', array_filter( $code ) ) );
697
		}
698
699 12
		return apply_filters( 'woocommerce_rate_code', $code_string, $key );
700
	}
701
702
	/**
703
	 * Sums a set of taxes to form a single total. Values are pre-rounded to precision from 3.6.0.
704
	 *
705
	 * @param  array $taxes Array of taxes.
706
	 * @return float
707
	 */
708 1
	public static function get_tax_total( $taxes ) {
709 1
		return array_sum( $taxes );
710
	}
711
712
	/**
713
	 * Gets all tax rate classes from the database.
714
	 *
715
	 * @since 3.7.0
716
	 * @return array Array of tax class objects consisting of tax_rate_class_id, name, and slug.
717
	 */
718 265
	protected static function get_tax_rate_classes() {
719
		global $wpdb;
720
721 265
		$cache_key        = 'tax-rate-classes';
722 265
		$tax_rate_classes = wp_cache_get( $cache_key, 'taxes' );
723
724 265
		if ( ! is_array( $tax_rate_classes ) ) {
725 264
			$tax_rate_classes = $wpdb->get_results(
726
				"
727 264
				SELECT * FROM {$wpdb->wc_tax_rate_classes} ORDER BY name;
728
				"
729
			);
730 264
			wp_cache_set( $cache_key, $tax_rate_classes, 'taxes' );
731
		}
732
733 265
		return $tax_rate_classes;
734
	}
735
736
	/**
737
	 * Get store tax class names.
738
	 *
739
	 * @return array Array of class names ("Reduced rate", "Zero rate", etc).
740
	 */
741 15
	public static function get_tax_classes() {
742 15
		return wp_list_pluck( self::get_tax_rate_classes(), 'name' );
743
	}
744
745
	/**
746
	 * Get store tax classes as slugs.
747
	 *
748
	 * @since  3.0.0
749
	 * @return array Array of class slugs ("reduced-rate", "zero-rate", etc).
750
	 */
751 265
	public static function get_tax_class_slugs() {
752 265
		return wp_list_pluck( self::get_tax_rate_classes(), 'slug' );
753
	}
754
755
	/**
756
	 * Create a new tax class.
757
	 *
758
	 * @since 3.7.0
759
	 * @param string $name Name of the tax class to add.
760
	 * @param string $slug (optional) Slug of the tax class to add. Defaults to sanitized name.
761
	 * @return WP_Error|array Returns name and slug (array) if the tax class is created, or WP_Error if something went wrong.
762
	 */
763 2
	public static function create_tax_class( $name, $slug = '' ) {
764
		global $wpdb;
765
766 2
		if ( empty( $name ) ) {
767
			return new WP_Error( 'tax_class_invalid_name', __( 'Tax class requires a valid name', 'woocommerce' ) );
768
		}
769
770 2
		$existing       = self::get_tax_classes();
771 2
		$existing_slugs = self::get_tax_class_slugs();
772
773 2
		if ( in_array( $name, $existing, true ) ) {
774 2
			return new WP_Error( 'tax_class_exists', __( 'Tax class already exists', 'woocommerce' ) );
775
		}
776
777
		if ( ! $slug ) {
778
			$slug = sanitize_title( $name );
779
		}
780
781
		if ( in_array( $slug, $existing_slugs, true ) ) {
782
			return new WP_Error( 'tax_class_slug_exists', __( 'Tax class slug already exists', 'woocommerce' ) );
783
		}
784
785
		$insert = $wpdb->insert(
786
			$wpdb->wc_tax_rate_classes,
787
			array(
788
				'name' => $name,
789
				'slug' => $slug,
790
			)
791
		);
792
793
		if ( is_wp_error( $insert ) ) {
794
			return new WP_Error( 'tax_class_insert_error', $insert->get_error_message() );
795
		}
796
797
		wp_cache_delete( 'tax-rate-classes', 'taxes' );
798
799
		return array(
800
			'name' => $name,
801
			'slug' => $slug,
802
		);
803
	}
804
805
	/**
806
	 * Get an existing tax class.
807
	 *
808
	 * @since 3.7.0
809
	 * @param string     $field Field to get by. Valid values are id, name, or slug.
810
	 * @param string|int $item Item to get.
811
	 * @return array|bool Returns the tax class as an array. False if not found.
812
	 */
813
	public static function get_tax_class_by( $field, $item ) {
814 View Code Duplication
		if ( ! in_array( $field, array( 'id', 'name', 'slug' ), true ) ) {
815
			return new WP_Error( 'invalid_field', __( 'Invalid field', 'woocommerce' ) );
816
		}
817
818
		if ( 'id' === $field ) {
819
			$field = 'tax_rate_class_id';
820
		}
821
822
		$matches = wp_list_filter(
823
			self::get_tax_rate_classes(),
824
			array(
825
				$field => $item,
826
			)
827
		);
828
829
		if ( ! $matches ) {
830
			return false;
831
		}
832
833
		$tax_class = current( $matches );
834
835
		return array(
836
			'name' => $tax_class->name,
837
			'slug' => $tax_class->slug,
838
		);
839
	}
840
841
	/**
842
	 * Delete an existing tax class.
843
	 *
844
	 * @since 3.7.0
845
	 * @param string     $field Field to delete by. Valid values are id, name, or slug.
846
	 * @param string|int $item Item to delete.
847
	 * @return WP_Error|bool Returns true if deleted successfully, false if nothing was deleted, or WP_Error if there is an invalid request.
848
	 */
849
	public static function delete_tax_class_by( $field, $item ) {
850
		global $wpdb;
851
852 View Code Duplication
		if ( ! in_array( $field, array( 'id', 'name', 'slug' ), true ) ) {
853
			return new WP_Error( 'invalid_field', __( 'Invalid field', 'woocommerce' ) );
854
		}
855
856
		$tax_class = self::get_tax_class_by( $field, $item );
857
858
		if ( ! $tax_class ) {
859
			return new WP_Error( 'invalid_tax_class', __( 'Invalid tax class', 'woocommerce' ) );
860
		}
861
862
		if ( 'id' === $field ) {
863
			$field = 'tax_rate_class_id';
864
		}
865
866
		$delete = $wpdb->delete(
867
			$wpdb->wc_tax_rate_classes,
868
			array(
869
				$field => $item,
870
			)
871
		);
872
873
		if ( $delete ) {
874
			// Delete associated tax rates.
875
			$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_class = %s;", $tax_class['slug'] ) );
876
			$wpdb->query( "DELETE locations FROM {$wpdb->prefix}woocommerce_tax_rate_locations locations LEFT JOIN {$wpdb->prefix}woocommerce_tax_rates rates ON rates.tax_rate_id = locations.tax_rate_id WHERE rates.tax_rate_id IS NULL;" );
877
		}
878
879
		wp_cache_delete( 'tax-rate-classes', 'taxes' );
880
		WC_Cache_Helper::incr_cache_prefix( 'taxes' );
881
882
		return (bool) $delete;
883
	}
884
885
	/**
886
	 * Format the city.
887
	 *
888
	 * @param  string $city Value to format.
889
	 * @return string
890
	 */
891 2
	private static function format_tax_rate_city( $city ) {
892 2
		return strtoupper( trim( $city ) );
893
	}
894
895
	/**
896
	 * Format the state.
897
	 *
898
	 * @param  string $state Value to format.
899
	 * @return string
900
	 */
901 84
	private static function format_tax_rate_state( $state ) {
902 84
		$state = strtoupper( $state );
903 84
		return ( '*' === $state ) ? '' : $state;
904
	}
905
906
	/**
907
	 * Format the country.
908
	 *
909
	 * @param  string $country Value to format.
910
	 * @return string
911
	 */
912 84
	private static function format_tax_rate_country( $country ) {
913 84
		$country = strtoupper( $country );
914 84
		return ( '*' === $country ) ? '' : $country;
915
	}
916
917
	/**
918
	 * Format the tax rate name.
919
	 *
920
	 * @param  string $name Value to format.
921
	 * @return string
922
	 */
923 84
	private static function format_tax_rate_name( $name ) {
924 84
		return $name ? $name : __( 'Tax', 'woocommerce' );
925
	}
926
927
	/**
928
	 * Format the rate.
929
	 *
930
	 * @param  float $rate Value to format.
931
	 * @return string
932
	 */
933 84
	private static function format_tax_rate( $rate ) {
934 84
		return number_format( (float) $rate, 4, '.', '' );
935
	}
936
937
	/**
938
	 * Format the priority.
939
	 *
940
	 * @param  string $priority Value to format.
941
	 * @return int
942
	 */
943 84
	private static function format_tax_rate_priority( $priority ) {
944 84
		return absint( $priority );
945
	}
946
947
	/**
948
	 * Format the class.
949
	 *
950
	 * @param  string $class Value to format.
951
	 * @return string
952
	 */
953 84
	public static function format_tax_rate_class( $class ) {
954 84
		$class   = sanitize_title( $class );
955 84
		$classes = self::get_tax_class_slugs();
956 84
		if ( ! in_array( $class, $classes, true ) ) {
957 84
			$class = '';
958
		}
959 84
		return ( 'standard' === $class ) ? '' : $class;
960
	}
961
962
	/**
963
	 * Prepare and format tax rate for DB insertion.
964
	 *
965
	 * @param  array $tax_rate Tax rate to format.
966
	 * @return array
967
	 */
968 84
	private static function prepare_tax_rate( $tax_rate ) {
969 84
		foreach ( $tax_rate as $key => $value ) {
970 84
			if ( method_exists( __CLASS__, 'format_' . $key ) ) {
971 84
				if ( 'tax_rate_state' === $key ) {
972 84
					$tax_rate[ $key ] = call_user_func( array( __CLASS__, 'format_' . $key ), sanitize_key( $value ) );
973
				} else {
974 84
					$tax_rate[ $key ] = call_user_func( array( __CLASS__, 'format_' . $key ), $value );
975
				}
976
			}
977
		}
978 84
		return $tax_rate;
979
	}
980
981
	/**
982
	 * Insert a new tax rate.
983
	 *
984
	 * Internal use only.
985
	 *
986
	 * @since 2.3.0
987
	 *
988
	 * @param  array $tax_rate Tax rate to insert.
989
	 * @return int tax rate id
990
	 */
991 84 View Code Duplication
	public static function _insert_tax_rate( $tax_rate ) {
992
		global $wpdb;
993
994 84
		$wpdb->insert( $wpdb->prefix . 'woocommerce_tax_rates', self::prepare_tax_rate( $tax_rate ) );
995
996 84
		$tax_rate_id = $wpdb->insert_id;
997
998 84
		WC_Cache_Helper::incr_cache_prefix( 'taxes' );
999
1000 84
		do_action( 'woocommerce_tax_rate_added', $tax_rate_id, $tax_rate );
1001
1002 84
		return $tax_rate_id;
1003
	}
1004
1005
	/**
1006
	 * Get tax rate.
1007
	 *
1008
	 * Internal use only.
1009
	 *
1010
	 * @since 2.5.0
1011
	 *
1012
	 * @param int    $tax_rate_id Tax rate ID.
1013
	 * @param string $output_type Type of output.
1014
	 * @return array|object
1015
	 */
1016 10
	public static function _get_tax_rate( $tax_rate_id, $output_type = ARRAY_A ) {
1017
		global $wpdb;
1018
1019 10
		return $wpdb->get_row(
1020 10
			$wpdb->prepare(
1021 10
				"
1022
					SELECT *
1023 10
					FROM {$wpdb->prefix}woocommerce_tax_rates
1024
					WHERE tax_rate_id = %d
1025
				",
1026
				$tax_rate_id
1027
			),
1028
			$output_type
1029
		);
1030
	}
1031
1032
	/**
1033
	 * Update a tax rate.
1034
	 *
1035
	 * Internal use only.
1036
	 *
1037
	 * @since 2.3.0
1038
	 *
1039
	 * @param int   $tax_rate_id Tax rate to update.
1040
	 * @param array $tax_rate Tax rate values.
1041
	 */
1042 1 View Code Duplication
	public static function _update_tax_rate( $tax_rate_id, $tax_rate ) {
1043
		global $wpdb;
1044
1045 1
		$tax_rate_id = absint( $tax_rate_id );
1046
1047 1
		$wpdb->update(
1048 1
			$wpdb->prefix . 'woocommerce_tax_rates',
1049 1
			self::prepare_tax_rate( $tax_rate ),
1050
			array(
1051 1
				'tax_rate_id' => $tax_rate_id,
1052
			)
1053
		);
1054
1055 1
		WC_Cache_Helper::incr_cache_prefix( 'taxes' );
1056
1057 1
		do_action( 'woocommerce_tax_rate_updated', $tax_rate_id, $tax_rate );
1058
	}
1059
1060
	/**
1061
	 * Delete a tax rate from the database.
1062
	 *
1063
	 * Internal use only.
1064
	 *
1065
	 * @since 2.3.0
1066
	 * @param  int $tax_rate_id Tax rate to delete.
1067
	 */
1068 4
	public static function _delete_tax_rate( $tax_rate_id ) {
1069
		global $wpdb;
1070
1071 4
		$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE tax_rate_id = %d;", $tax_rate_id ) );
1072 4
		$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %d;", $tax_rate_id ) );
1073
1074 4
		WC_Cache_Helper::incr_cache_prefix( 'taxes' );
1075
1076 4
		do_action( 'woocommerce_tax_rate_deleted', $tax_rate_id );
1077
	}
1078
1079
	/**
1080
	 * Update postcodes for a tax rate in the DB.
1081
	 *
1082
	 * Internal use only.
1083
	 *
1084
	 * @since 2.3.0
1085
	 *
1086
	 * @param int    $tax_rate_id Tax rate to update.
1087
	 * @param string $postcodes String of postcodes separated by ; characters.
1088
	 */
1089 2
	public static function _update_tax_rate_postcodes( $tax_rate_id, $postcodes ) {
1090 2
		if ( ! is_array( $postcodes ) ) {
1091 2
			$postcodes = explode( ';', $postcodes );
1092
		}
1093
		// No normalization - postcodes are matched against both normal and formatted versions to support wildcards.
1094 2
		foreach ( $postcodes as $key => $postcode ) {
1095 2
			$postcodes[ $key ] = strtoupper( trim( str_replace( chr( 226 ) . chr( 128 ) . chr( 166 ), '...', $postcode ) ) );
1096
		}
1097 2
		self::update_tax_rate_locations( $tax_rate_id, array_diff( array_filter( $postcodes ), array( '*' ) ), 'postcode' );
1098
	}
1099
1100
	/**
1101
	 * Update cities for a tax rate in the DB.
1102
	 *
1103
	 * Internal use only.
1104
	 *
1105
	 * @since 2.3.0
1106
	 *
1107
	 * @param int    $tax_rate_id Tax rate to update.
1108
	 * @param string $cities Cities to set.
1109
	 */
1110 2
	public static function _update_tax_rate_cities( $tax_rate_id, $cities ) {
1111 2
		if ( ! is_array( $cities ) ) {
1112 2
			$cities = explode( ';', $cities );
1113
		}
1114 2
		$cities = array_filter( array_diff( array_map( array( __CLASS__, 'format_tax_rate_city' ), $cities ), array( '*' ) ) );
1115
1116 2
		self::update_tax_rate_locations( $tax_rate_id, $cities, 'city' );
1117
	}
1118
1119
	/**
1120
	 * Updates locations (postcode and city).
1121
	 *
1122
	 * Internal use only.
1123
	 *
1124
	 * @since 2.3.0
1125
	 *
1126
	 * @param int    $tax_rate_id Tax rate ID to update.
1127
	 * @param array  $values Values to set.
1128
	 * @param string $type Location type.
1129
	 */
1130 3
	private static function update_tax_rate_locations( $tax_rate_id, $values, $type ) {
1131
		global $wpdb;
1132
1133 3
		$tax_rate_id = absint( $tax_rate_id );
1134
1135 3
		$wpdb->query(
1136 3
			$wpdb->prepare(
1137 3
				"DELETE FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE tax_rate_id = %d AND location_type = %s;",
1138
				$tax_rate_id,
1139
				$type
1140
			)
1141
		);
1142
1143 3
		if ( count( $values ) > 0 ) {
1144 3
			$sql = "( '" . implode( "', $tax_rate_id, '" . esc_sql( $type ) . "' ),( '", array_map( 'esc_sql', $values ) ) . "', $tax_rate_id, '" . esc_sql( $type ) . "' )";
1145
1146 3
			$wpdb->query( "INSERT INTO {$wpdb->prefix}woocommerce_tax_rate_locations ( location_code, tax_rate_id, location_type ) VALUES $sql;" ); // @codingStandardsIgnoreLine.
1147
		}
1148
1149 3
		WC_Cache_Helper::incr_cache_prefix( 'taxes' );
1150
	}
1151
1152
	/**
1153
	 * Used by admin settings page.
1154
	 *
1155
	 * @param string $tax_class Tax class slug.
1156
	 *
1157
	 * @return array|null|object
1158
	 */
1159
	public static function get_rates_for_tax_class( $tax_class ) {
1160
		global $wpdb;
1161
1162
		// Get all the rates and locations. Snagging all at once should significantly cut down on the number of queries.
1163
		$rates     = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM `{$wpdb->prefix}woocommerce_tax_rates` WHERE `tax_rate_class` = %s;", sanitize_title( $tax_class ) ) );
1164
		$locations = $wpdb->get_results( "SELECT * FROM `{$wpdb->prefix}woocommerce_tax_rate_locations`" );
1165
1166
		if ( ! empty( $rates ) ) {
1167
			// Set the rates keys equal to their ids.
1168
			$rates = array_combine( wp_list_pluck( $rates, 'tax_rate_id' ), $rates );
1169
		}
1170
1171
		// Drop the locations into the rates array.
1172
		foreach ( $locations as $location ) {
1173
			// Don't set them for unexistent rates.
1174
			if ( ! isset( $rates[ $location->tax_rate_id ] ) ) {
1175
				continue;
1176
			}
1177
			// If the rate exists, initialize the array before appending to it.
1178
			if ( ! isset( $rates[ $location->tax_rate_id ]->{$location->location_type} ) ) {
1179
				$rates[ $location->tax_rate_id ]->{$location->location_type} = array();
1180
			}
1181
			$rates[ $location->tax_rate_id ]->{$location->location_type}[] = $location->location_code;
1182
		}
1183
1184
		foreach ( $rates as $rate_id => $rate ) {
1185
			$rates[ $rate_id ]->postcode_count = isset( $rates[ $rate_id ]->postcode ) ? count( $rates[ $rate_id ]->postcode ) : 0;
1186
			$rates[ $rate_id ]->city_count     = isset( $rates[ $rate_id ]->city ) ? count( $rates[ $rate_id ]->city ) : 0;
1187
		}
1188
1189
		$rates = self::sort_rates( $rates );
1190
1191
		return $rates;
1192
	}
1193
}
1194
WC_Tax::init();
1195