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

WC_Tax::get_rate_percent_value()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12

Duplication

Lines 6
Ratio 50 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 1
dl 6
loc 12
ccs 6
cts 6
cp 1
crap 2
rs 9.8666
c 0
b 0
f 0
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'] ) );
0 ignored issues
show
Bug introduced by
It seems like wc_clean($args['postcode']) targeting wc_clean() can also be of type array; however, wc_normalize_postcode() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

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