Completed
Push — master ( 1e0376...4ea396 )
by Mike
13:48
created

WC_Tax::maybe_remove_tax_class_rates()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 10
c 1
b 0
f 0
nc 3
nop 2
dl 0
loc 16
rs 9.4285
1
<?php
2
3
if ( ! defined( 'ABSPATH' ) ) {
4
	exit; // Exit if accessed directly
5
}
6
7
/**
8
 * Performs tax calculations and loads tax rates
9
 *
10
 * @class 		WC_Tax
11
 * @version		2.2.0
12
 * @package		WooCommerce/Classes
13
 * @category	Class
14
 * @author 		WooThemes
15
 */
16
class WC_Tax {
17
18
	/**
19
	 * Precision.
20
	 *
21
	 * @var int
22
	 */
23
	public static $precision;
24
25
	/**
26
	 * Round at subtotal.
27
	 *
28
	 * @var bool
29
	 */
30
	public static $round_at_subtotal;
31
32
	/**
33
	 * Load options.
34
	 *
35
	 * @access public
36
	 */
37
	public static function init() {
38
		self::$precision         = wc_get_rounding_precision();
39
		self::$round_at_subtotal = 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' );
40
		add_action( 'update_option_woocommerce_tax_classes', array( __CLASS__, 'maybe_remove_tax_class_rates' ), 10, 2 );
41
	}
42
43
	/**
44
	 * When the woocommerce_tax_classes option is changed, remove any orphan rates.
45
	 * @param  string $old_value
46
	 * @param  string $value
47
	 */
48
	public static function maybe_remove_tax_class_rates( $old_value, $value ) {
49
		$old     = array_filter( array_map( 'trim', explode( "\n", $old_value ) ) );
50
		$new     = array_filter( array_map( 'trim', explode( "\n", $value ) ) );
51
		$removed = array_filter( array_map( 'sanitize_title', array_diff( $old, $new ) ) );
52
53
		if ( $removed ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $removed of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
54
			global $wpdb;
55
56
			foreach ( $removed as $removed_tax_class ) {
57
				$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_class = %s;", $removed_tax_class ) );
58
				$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;" );
59
			}
60
61
			WC_Cache_Helper::incr_cache_prefix( 'taxes' );
62
		}
63
	}
64
65
	/**
66
	 * Calculate tax for a line.
67
	 * @param  float  $price              Price to calc tax on
68
	 * @param  array  $rates              Rates to apply
69
	 * @param  boolean $price_includes_tax Whether the passed price has taxes included
70
	 * @param  boolean $suppress_rounding  Whether to suppress any rounding from taking place
71
	 * @return array                      Array of rates + prices after tax
72
	 */
73
	public static function calc_tax( $price, $rates, $price_includes_tax = false, $suppress_rounding = false ) {
74
		// Work in pence to X precision
75
		$price = self::precision( $price );
76
77
		if ( $price_includes_tax ) {
78
			$taxes = self::calc_inclusive_tax( $price, $rates );
79
		} else {
80
			$taxes = self::calc_exclusive_tax( $price, $rates );
81
		}
82
83
		// Round to precision
84
		if ( ! self::$round_at_subtotal && ! $suppress_rounding ) {
85
			$taxes = array_map( 'round', $taxes ); // Round to precision
86
		}
87
88
		// Remove precision
89
		$price     = self::remove_precision( $price );
90
		$taxes     = array_map( array( __CLASS__, 'remove_precision' ), $taxes );
91
92
		return apply_filters( 'woocommerce_calc_tax', $taxes, $price, $rates, $price_includes_tax, $suppress_rounding );
93
	}
94
95
	/**
96
	 * Calculate the shipping tax using a passed array of rates.
97
	 *
98
	 * @param   float		Price
99
	 * @param	array		Taxation Rate
100
	 * @return  array
101
	 */
102
	public static function calc_shipping_tax( $price, $rates ) {
103
		return self::calc_exclusive_tax( $price, $rates );
104
	}
105
106
	/**
107
	 * Multiply cost by pow precision.
108
	 * @param  float $price
109
	 * @return float
110
	 */
111
	private static function precision( $price ) {
112
		return $price * ( pow( 10, self::$precision ) );
113
	}
114
115
	/**
116
	 * Divide cost by pow precision.
117
	 * @param  float $price
118
	 * @return float
119
	 */
120
	private static function remove_precision( $price ) {
121
		return $price / ( pow( 10, self::$precision ) );
122
	}
123
124
	/**
125
	 * Round to precision.
126
	 *
127
	 * Filter example: to return rounding to .5 cents you'd use:
128
	 *
129
	 * function euro_5cent_rounding( $in ) {
130
	 *      return round( $in / 5, 2 ) * 5;
131
	 * }
132
	 * add_filter( 'woocommerce_tax_round', 'euro_5cent_rounding' );
133
	 * @return double
134
	 */
135
	public static function round( $in ) {
136
		return apply_filters( 'woocommerce_tax_round', round( $in, self::$precision ), $in );
137
	}
138
139
	/**
140
	 * Calc tax from inclusive price.
141
	 *
142
	 * @param  float $price
143
	 * @param  array $rates
144
	 * @return array
145
	 */
146
	public static function calc_inclusive_tax( $price, $rates ) {
147
		$taxes = array();
148
149
		$regular_tax_rates = $compound_tax_rates = 0;
150
151
		foreach ( $rates as $key => $rate )
152
			if ( $rate['compound'] == 'yes' )
153
				$compound_tax_rates = $compound_tax_rates + $rate['rate'];
154
			else
155
				$regular_tax_rates  = $regular_tax_rates + $rate['rate'];
156
157
		$regular_tax_rate 	= 1 + ( $regular_tax_rates / 100 );
158
		$compound_tax_rate 	= 1 + ( $compound_tax_rates / 100 );
159
		$non_compound_price = $price / $compound_tax_rate;
160
161
		foreach ( $rates as $key => $rate ) {
162
			if ( ! isset( $taxes[ $key ] ) )
163
				$taxes[ $key ] = 0;
164
165
			$the_rate      = $rate['rate'] / 100;
166
167
			if ( $rate['compound'] == 'yes' ) {
168
				$the_price = $price;
169
				$the_rate  = $the_rate / $compound_tax_rate;
170
			} else {
171
				$the_price = $non_compound_price;
172
				$the_rate  = $the_rate / $regular_tax_rate;
173
			}
174
175
			$net_price       = $price - ( $the_rate * $the_price );
176
			$tax_amount      = $price - $net_price;
177
			$taxes[ $key ]   += apply_filters( 'woocommerce_price_inc_tax_amount', $tax_amount, $key, $rate, $price );
178
		}
179
180
		return $taxes;
181
	}
182
183
	/**
184
	 * Calc tax from exclusive price.
185
	 *
186
	 * @param  float $price
187
	 * @param  array $rates
188
	 * @return array
189
	 */
190
	public static function calc_exclusive_tax( $price, $rates ) {
191
		$taxes = array();
192
193
		if ( ! empty( $rates ) ) {
194
			// Multiple taxes
195
			foreach ( $rates as $key => $rate ) {
196
197
				if ( $rate['compound'] == 'yes' )
198
					continue;
199
200
				$tax_amount = $price * ( $rate['rate'] / 100 );
201
202
				// ADVANCED: Allow third parties to modify this rate
203
				$tax_amount = apply_filters( 'woocommerce_price_ex_tax_amount', $tax_amount, $key, $rate, $price );
204
205
				// Add rate
206 View Code Duplication
				if ( ! isset( $taxes[ $key ] ) )
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
207
					$taxes[ $key ] = $tax_amount;
208
				else
209
					$taxes[ $key ] += $tax_amount;
210
			}
211
212
			$pre_compound_total = array_sum( $taxes );
213
214
			// Compound taxes
215
			foreach ( $rates as $key => $rate ) {
216
217
				if ( $rate['compound'] == 'no' )
218
					continue;
219
220
				$the_price_inc_tax = $price + ( $pre_compound_total );
221
222
				$tax_amount = $the_price_inc_tax * ( $rate['rate'] / 100 );
223
224
				// ADVANCED: Allow third parties to modify this rate
225
				$tax_amount = apply_filters( 'woocommerce_price_ex_tax_amount', $tax_amount, $key, $rate, $price, $the_price_inc_tax, $pre_compound_total );
226
227
				// Add rate
228 View Code Duplication
				if ( ! isset( $taxes[ $key ] ) )
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
229
					$taxes[ $key ] = $tax_amount;
230
				else
231
					$taxes[ $key ] += $tax_amount;
232
			}
233
		}
234
235
		return $taxes;
236
	}
237
238
	/**
239
	 * Searches for all matching country/state/postcode tax rates.
240
	 *
241
	 * @param array $args
242
	 * @return array
243
	 */
244
	public static function find_rates( $args = array() ) {
245
		$args = wp_parse_args( $args, array(
246
			'country'   => '',
247
			'state'     => '',
248
			'city'      => '',
249
			'postcode'  => '',
250
			'tax_class' => ''
251
		) );
252
253
		extract( $args, EXTR_SKIP );
254
255
		if ( ! $country ) {
256
			return array();
257
		}
258
259
		$postcode          = wc_normalize_postcode( wc_clean( $postcode ) );
260
		$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 ) );
261
		$matched_tax_rates = wp_cache_get( $cache_key, 'taxes' );
262
263
		if ( false === $matched_tax_rates ) {
264
			$matched_tax_rates = self::get_matched_tax_rates( $country, $state, $postcode, $city, $tax_class );
265
			wp_cache_set( $cache_key, $matched_tax_rates, 'taxes' );
266
		}
267
268
		return apply_filters( 'woocommerce_find_rates', $matched_tax_rates, $args );
269
	}
270
271
	/**
272
	 * Searches for all matching country/state/postcode tax rates.
273
	 *
274
	 * @param array $args
275
	 * @return array
276
	 */
277
	public static function find_shipping_rates( $args = array() ) {
278
		$rates          = self::find_rates( $args );
279
		$shipping_rates = array();
280
281
		if ( is_array( $rates ) ) {
282
			foreach ( $rates as $key => $rate ) {
283
				if ( 'yes' === $rate['shipping'] ) {
284
					$shipping_rates[ $key ] = $rate;
285
				}
286
			}
287
		}
288
289
		return $shipping_rates;
290
	}
291
292
	/**
293
	 * Does the sort comparison.
294
	 */
295
	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...
296
		if ( $rate1->tax_rate_priority !== $rate2->tax_rate_priority ) {
297
			return $rate1->tax_rate_priority < $rate2->tax_rate_priority ? -1 : 1; // ASC
298
		} elseif ( $rate1->tax_rate_country !== $rate2->tax_rate_country ) {
299
			if ( '' === $rate1->tax_rate_country ) {
300
				return 1;
301
			}
302
			if ( '' === $rate2->tax_rate_country ) {
303
				return -1;
304
			}
305
			return strcmp( $rate1->tax_rate_country, $rate2->tax_rate_country ) > 0 ? 1 : -1;
306
		} elseif ( $rate1->tax_rate_state !== $rate2->tax_rate_state ) {
307
			if ( '' === $rate1->tax_rate_state ) {
308
				return 1;
309
			}
310
			if ( '' === $rate2->tax_rate_state ) {
311
				return -1;
312
			}
313
			return strcmp( $rate1->tax_rate_state, $rate2->tax_rate_state ) > 0 ? 1 : -1;
314
		} else {
315
			return $rate1->tax_rate_id < $rate2->tax_rate_id ? -1 : 1; // Identical - use ID
316
		}
317
	}
318
319
	/**
320
	 * Logical sort order for tax rates based on the following in order of priority:
321
	 * 		- Priority
322
	 * 		- County code
323
	 * 		- State code
324
	 * @param  array $rates
325
	 * @return array
326
	 * @todo   remove tax_rate_order column
327
	 */
328
	private static function sort_rates( $rates ) {
329
		uasort( $rates, __CLASS__ . '::sort_rates_callback' );
330
		$i = 0;
331
		foreach ( $rates as $key => $rate ) {
332
			$rates[ $key ]->tax_rate_order = $i++;
333
		}
334
		return $rates;
335
	}
336
337
	/**
338
	 * Loop through a set of tax rates and get the matching rates (1 per priority).
339
	 *
340
	 * @param  string $country
341
	 * @param  string $state
342
	 * @param  string $postcode
343
	 * @param  string $city
344
	 * @param  string $tax_class
345
	 * @return array
346
	 */
347
	private static function get_matched_tax_rates( $country, $state, $postcode, $city, $tax_class ) {
348
		global $wpdb;
349
350
		// Query criteria - these will be ANDed
351
		$criteria   = array();
352
		$criteria[] = $wpdb->prepare( "tax_rate_country IN ( %s, '' )", strtoupper( $country ) );
353
		$criteria[] = $wpdb->prepare( "tax_rate_state IN ( %s, '' )", strtoupper( $state ) );
354
		$criteria[] = $wpdb->prepare( "tax_rate_class = %s", sanitize_title( $tax_class ) );
355
356
		// Pre-query postcode ranges for PHP based matching.
357
		$postcode_search = wc_get_wildcard_postcodes( $postcode, $country );
358
		$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 '%...%';" );
359
360
		if ( $postcode_ranges ) {
361
			$matches = wc_postcode_location_matcher( $postcode, $postcode_ranges, 'tax_rate_id', 'location_code', $country );
362
			if ( ! empty( $matches ) ) {
363
				foreach ( $matches as $matched_postcodes ) {
364
					$postcode_search = array_merge( $postcode_search, $matched_postcodes );
365
				}
366
			}
367
		}
368
369
		$postcode_search = array_unique( $postcode_search );
370
371
		/**
372
		 * Location matching criteria - ORed
373
		 * Needs to match:
374
		 * 	- rates with no postcodes and cities
375
		 * 	- rates with a matching postcode and city
376
		 * 	- rates with matching postcode, no city
377
		 * 	- rates with matching city, no postcode
378
		 */
379
		$locations_criteria   = array();
380
		$locations_criteria[] = "locations.location_type IS NULL";
381
		$locations_criteria[] = "
382
			locations.location_type = 'postcode' AND locations.location_code IN ('" . implode( "','", array_map( 'esc_sql', $postcode_search ) ) . "')
383
			AND (
384
				( locations2.location_type = 'city' AND locations2.location_code = '" . esc_sql( strtoupper( $city ) ) . "' )
385
				OR NOT EXISTS (
386
					SELECT sub.tax_rate_id FROM {$wpdb->prefix}woocommerce_tax_rate_locations as sub
387
					WHERE sub.location_type = 'city'
388
					AND sub.tax_rate_id = tax_rates.tax_rate_id
389
				)
390
			)
391
		";
392
		$locations_criteria[] = "
393
			locations.location_type = 'city' AND locations.location_code = '" . esc_sql( strtoupper( $city ) ) . "'
394
			AND NOT EXISTS (
395
				SELECT sub.tax_rate_id FROM {$wpdb->prefix}woocommerce_tax_rate_locations as sub
396
				WHERE sub.location_type = 'postcode'
397
				AND sub.tax_rate_id = tax_rates.tax_rate_id
398
			)
399
		";
400
		$criteria[] = '( ( ' . implode( ' ) OR ( ', $locations_criteria ) . ' ) )';
401
402
		$found_rates = $wpdb->get_results( "
403
			SELECT tax_rates.*
404
			FROM {$wpdb->prefix}woocommerce_tax_rates as tax_rates
405
			LEFT OUTER JOIN {$wpdb->prefix}woocommerce_tax_rate_locations as locations ON tax_rates.tax_rate_id = locations.tax_rate_id
406
			LEFT OUTER JOIN {$wpdb->prefix}woocommerce_tax_rate_locations as locations2 ON tax_rates.tax_rate_id = locations2.tax_rate_id
407
			WHERE 1=1 AND " . implode( ' AND ', $criteria ) . "
408
			GROUP BY tax_rate_id
409
			ORDER BY tax_rate_priority
410
		" );
411
412
		$found_rates       = self::sort_rates( $found_rates );
413
		$matched_tax_rates = array();
414
		$found_priority    = array();
415
416
		foreach ( $found_rates as $found_rate ) {
417
			if ( in_array( $found_rate->tax_rate_priority, $found_priority ) ) {
418
				continue;
419
			}
420
421
			$matched_tax_rates[ $found_rate->tax_rate_id ] = array(
422
				'rate'     => $found_rate->tax_rate,
423
				'label'    => $found_rate->tax_rate_name,
424
				'shipping' => $found_rate->tax_rate_shipping ? 'yes' : 'no',
425
				'compound' => $found_rate->tax_rate_compound ? 'yes' : 'no'
426
			);
427
428
			$found_priority[] = $found_rate->tax_rate_priority;
429
		}
430
431
		return apply_filters( 'woocommerce_matched_tax_rates', $matched_tax_rates, $country, $state, $postcode, $city, $tax_class );
432
	}
433
434
	/**
435
	 * Get the customer tax location based on their status and the current page.
436
	 *
437
	 * Used by get_rates(), get_shipping_rates().
438
	 *
439
	 * @param  $tax_class string Optional, passed to the filter for advanced tax setups.
440
	 * @return array
441
	 */
442
	public static function get_tax_location( $tax_class = '' ) {
443
		$location = array();
444
445
		if ( ! empty( WC()->customer ) ) {
446
			$location = WC()->customer->get_taxable_address();
447
		} elseif ( wc_prices_include_tax() || 'base' === get_option( 'woocommerce_default_customer_address' ) || 'base' === get_option( 'woocommerce_tax_based_on' ) ) {
448
			$location = array(
449
				WC()->countries->get_base_country(),
450
				WC()->countries->get_base_state(),
451
				WC()->countries->get_base_postcode(),
452
				WC()->countries->get_base_city()
453
			);
454
		}
455
456
		return apply_filters( 'woocommerce_get_tax_location', $location, $tax_class );
457
	}
458
459
	/**
460
	 * Get's an array of matching rates for a tax class.
461
	 * @param string $tax_class
462
	 * @return  array
463
	 */
464
	public static function get_rates( $tax_class = '' ) {
465
		$tax_class         = sanitize_title( $tax_class );
466
		$location          = self::get_tax_location( $tax_class );
467
		$matched_tax_rates = array();
468
469
		if ( sizeof( $location ) === 4 ) {
470
			list( $country, $state, $postcode, $city ) = $location;
471
472
			$matched_tax_rates = self::find_rates( array(
473
				'country' 	=> $country,
474
				'state' 	=> $state,
475
				'postcode' 	=> $postcode,
476
				'city' 		=> $city,
477
				'tax_class' => $tax_class
478
			) );
479
		}
480
481
		return apply_filters( 'woocommerce_matched_rates', $matched_tax_rates, $tax_class );
482
	}
483
484
	/**
485
	 * Get's an array of matching rates for the shop's base country.
486
	 *
487
	 * @param   string	Tax Class
488
	 * @return  array
489
	 */
490
	public static function get_base_tax_rates( $tax_class = '' ) {
491
		return apply_filters( 'woocommerce_base_tax_rates', self::find_rates( array(
492
			'country' 	=> WC()->countries->get_base_country(),
493
			'state' 	=> WC()->countries->get_base_state(),
494
			'postcode' 	=> WC()->countries->get_base_postcode(),
495
			'city' 		=> WC()->countries->get_base_city(),
496
			'tax_class' => $tax_class
497
		) ), $tax_class );
498
	}
499
500
	/**
501
	 * Alias for get_base_tax_rates().
502
	 *
503
	 * @deprecated 2.3
504
	 * @param   string	Tax Class
505
	 * @return  array
506
	 */
507
	public static function get_shop_base_rate( $tax_class = '' ) {
508
		return self::get_base_tax_rates( $tax_class );
509
	}
510
511
	/**
512
	 * Gets an array of matching shipping tax rates for a given class.
513
	 *
514
	 * @param   string	Tax Class
515
	 * @return  mixed
516
	 */
517
	public static function get_shipping_tax_rates( $tax_class = null ) {
518
		// See if we have an explicitly set shipping tax class
519
		if ( $shipping_tax_class = get_option( 'woocommerce_shipping_tax_class' ) ) {
520
			$tax_class = 'standard' === $shipping_tax_class ? '' : $shipping_tax_class;
521
		}
522
523
		$location          = self::get_tax_location( $tax_class );
524
		$matched_tax_rates = array();
525
526
		if ( sizeof( $location ) === 4 ) {
527
			list( $country, $state, $postcode, $city ) = $location;
528
529
			if ( ! is_null( $tax_class ) ) {
530
				// This will be per item shipping
531
				$matched_tax_rates = self::find_shipping_rates( array(
532
					'country' 	=> $country,
533
					'state' 	=> $state,
534
					'postcode' 	=> $postcode,
535
					'city' 		=> $city,
536
					'tax_class' => $tax_class
537
				) );
538
539
			} else {
540
541
				// This will be per order shipping - loop through the order and find the highest tax class rate
542
				$cart_tax_classes = WC()->cart->get_cart_item_tax_classes();
543
544
				// If multiple classes are found, use the first one. Don't bother with standard rate, we can get that later.
545
				if ( sizeof( $cart_tax_classes ) > 1 && ! in_array( '', $cart_tax_classes ) ) {
546
					$tax_classes = self::get_tax_classes();
547
548
					foreach ( $tax_classes as $tax_class ) {
549
						$tax_class = sanitize_title( $tax_class );
550
						if ( in_array( $tax_class, $cart_tax_classes ) ) {
551
							$matched_tax_rates = self::find_shipping_rates( array(
552
								'country' 	=> $country,
553
								'state' 	=> $state,
554
								'postcode' 	=> $postcode,
555
								'city' 		=> $city,
556
								'tax_class' => $tax_class
557
							) );
558
							break;
559
						}
560
					}
561
562
				// If a single tax class is found, use it
563
				} elseif ( sizeof( $cart_tax_classes ) == 1 ) {
564
					$matched_tax_rates = self::find_shipping_rates( array(
565
						'country' 	=> $country,
566
						'state' 	=> $state,
567
						'postcode' 	=> $postcode,
568
						'city' 		=> $city,
569
						'tax_class' => $cart_tax_classes[0]
570
					) );
571
				}
572
			}
573
574
			// Get standard rate if no taxes were found
575
			if ( ! sizeof( $matched_tax_rates ) ) {
576
				$matched_tax_rates = self::find_shipping_rates( array(
577
					'country' 	=> $country,
578
					'state' 	=> $state,
579
					'postcode' 	=> $postcode,
580
					'city' 		=> $city
581
				) );
582
			}
583
		}
584
585
		return $matched_tax_rates;
586
	}
587
588
	/**
589
	 * Return true/false depending on if a rate is a compound rate.
590
	 *
591
	 * @param   int		key
592
	 * @return  bool
593
	 */
594
	public static function is_compound( $key ) {
595
		global $wpdb;
596
		return $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate_compound FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) ) ? true : false;
597
	}
598
599
	/**
600
	 * Return a given rates label.
601
	 *
602
	 * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format
603
	 * @return  string
604
	 */
605
	public static function get_rate_label( $key_or_rate ) {
606
		global $wpdb;
607
608 View Code Duplication
		if ( is_object( $key_or_rate ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
609
			$key       = $key_or_rate->tax_rate_id;
610
			$rate_name = $key_or_rate->tax_rate_name;
611
		} else {
612
			$key       = $key_or_rate;
613
			$rate_name = $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate_name FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) );
614
		}
615
616
		if ( ! $rate_name ) {
617
			$rate_name = WC()->countries->tax_or_vat();
618
		}
619
620
		return apply_filters( 'woocommerce_rate_label', $rate_name, $key );
621
	}
622
623
	/**
624
	 * Return a given rates percent.
625
	 *
626
	 * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format
627
	 * @return  string
628
	 */
629
	public static function get_rate_percent( $key_or_rate ) {
630
		global $wpdb;
631
632 View Code Duplication
		if ( is_object( $key_or_rate ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
633
			$key      = $key_or_rate->tax_rate_id;
634
			$tax_rate = $key_or_rate->tax_rate;
635
		} else {
636
			$key      = $key_or_rate;
637
			$tax_rate = $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) );
638
		}
639
640
		return apply_filters( 'woocommerce_rate_percent', floatval( $tax_rate ) . '%', $key );
641
	}
642
643
	/**
644
	 * Get a rates code. Code is made up of COUNTRY-STATE-NAME-Priority. E.g GB-VAT-1, US-AL-TAX-1.
645
	 *
646
	 * @access public
647
	 * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format
648
	 * @return string
649
	 */
650
	public static function get_rate_code( $key_or_rate ) {
651
		global $wpdb;
652
653 View Code Duplication
		if ( is_object( $key_or_rate ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
654
			$key  = $key_or_rate->tax_rate_id;
655
			$rate = $key_or_rate;
656
		} else {
657
			$key  = $key_or_rate;
658
			$rate = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) );
659
		}
660
661
		$code_string = '';
662
663
		if ( null !== $rate ) {
664
			$code   = array();
665
			$code[] = $rate->tax_rate_country;
666
			$code[] = $rate->tax_rate_state;
667
			$code[] = $rate->tax_rate_name ? $rate->tax_rate_name : 'TAX';
668
			$code[] = absint( $rate->tax_rate_priority );
669
			$code_string = strtoupper( implode( '-', array_filter( $code ) ) );
670
		}
671
672
		return apply_filters( 'woocommerce_rate_code', $code_string, $key );
673
	}
674
675
	/**
676
	 * Round tax lines and return the sum.
677
	 *
678
	 * @param   array
679
	 * @return  float
680
	 */
681
	public static function get_tax_total( $taxes ) {
682
		return array_sum( array_map( array( __CLASS__, 'round' ), $taxes ) );
683
	}
684
685
	/**
686
	 * Get store tax classes.
687
	 * @return array
688
	 */
689
	public static function get_tax_classes() {
690
		return array_filter( array_map( 'trim', explode( "\n", get_option( 'woocommerce_tax_classes' ) ) ) );
691
	}
692
693
	/**
694
	 * format the city.
695
	 * @param  string $city
696
	 * @return string
697
	 */
698
	private static function format_tax_rate_city( $city ) {
699
		return strtoupper( trim( $city ) );
700
	}
701
702
	/**
703
	 * format the state.
704
	 * @param  string $state
705
	 * @return string
706
	 */
707
	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...
708
		$state = strtoupper( $state );
709
		return $state === '*' ? '' : $state;
710
	}
711
712
	/**
713
	 * format the country.
714
	 * @param  string $country
715
	 * @return string
716
	 */
717
	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...
718
		$country = strtoupper( $country );
719
		return $country === '*' ? '' : $country;
720
	}
721
722
	/**
723
	 * format the tax rate name.
724
	 * @param  string $name
725
	 * @return string
726
	 */
727
	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...
728
		return $name ? $name : __( 'Tax', 'woocommerce' );
729
	}
730
731
	/**
732
	 * format the rate.
733
	 * @param  double $rate
734
	 * @return string
735
	 */
736
	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...
737
		return number_format( (double) $rate, 4, '.', '' );
738
	}
739
740
	/**
741
	 * format the priority.
742
	 * @param  string $priority
743
	 * @return int
744
	 */
745
	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...
746
		return absint( $priority );
747
	}
748
749
	/**
750
	 * format the class.
751
	 * @param  string $class
752
	 * @return string
753
	 */
754
	public static function format_tax_rate_class( $class ) {
755
		$class = sanitize_title( $class );
756
		$sanitized_classes = array_map( 'sanitize_title', self::get_tax_classes() );
757
		if ( ! in_array( $class, $sanitized_classes ) ) {
758
			$class = '';
759
		}
760
		return $class === 'standard' ? '' : $class;
761
	}
762
763
	/**
764
	 * Prepare and format tax rate for DB insertion.
765
	 * @param  array $tax_rate
766
	 * @return array
767
	 */
768
	private static function prepare_tax_rate( $tax_rate ) {
769
		foreach ( $tax_rate as $key => $value ) {
770
			if ( method_exists( __CLASS__, 'format_' . $key ) ) {
771
				$tax_rate[ $key ] = call_user_func( array( __CLASS__, 'format_' . $key ), $value );
772
			}
773
		}
774
		return $tax_rate;
775
	}
776
777
	/**
778
	 * Insert a new tax rate.
779
	 *
780
	 * Internal use only.
781
	 *
782
	 * @since 2.3.0
783
	 * @access private
784
	 *
785
	 * @param  array $tax_rate
786
	 *
787
	 * @return int tax rate id
788
	 */
789
	public static function _insert_tax_rate( $tax_rate ) {
790
		global $wpdb;
791
792
		$wpdb->insert( $wpdb->prefix . 'woocommerce_tax_rates', self::prepare_tax_rate( $tax_rate ) );
793
794
		WC_Cache_Helper::incr_cache_prefix( 'taxes' );
795
796
		do_action( 'woocommerce_tax_rate_added', $wpdb->insert_id, $tax_rate );
797
798
		return $wpdb->insert_id;
799
	}
800
801
	/**
802
	 * Get tax rate.
803
	 *
804
	 * Internal use only.
805
	 *
806
	 * @since 2.5.0
807
	 * @access private
808
	 *
809
	 * @param int $tax_rate_id
810
	 * @param string $output_type
811
	 *
812
	 * @return array
813
	 */
814
	public static function _get_tax_rate( $tax_rate_id, $output_type = ARRAY_A ) {
815
		global $wpdb;
816
817
		return $wpdb->get_row( $wpdb->prepare( "
818
			SELECT *
819
			FROM {$wpdb->prefix}woocommerce_tax_rates
820
			WHERE tax_rate_id = %d
821
		", $tax_rate_id ), $output_type );
822
	}
823
824
	/**
825
	 * Update a tax rate.
826
	 *
827
	 * Internal use only.
828
	 *
829
	 * @since 2.3.0
830
	 * @access private
831
	 *
832
	 * @param int $tax_rate_id
833
	 * @param array $tax_rate
834
	 */
835
	public static function _update_tax_rate( $tax_rate_id, $tax_rate ) {
836
		global $wpdb;
837
838
		$tax_rate_id = absint( $tax_rate_id );
839
840
		$wpdb->update(
841
			$wpdb->prefix . "woocommerce_tax_rates",
842
			self::prepare_tax_rate( $tax_rate ),
843
			array(
844
				'tax_rate_id' => $tax_rate_id
845
			)
846
		);
847
848
		WC_Cache_Helper::incr_cache_prefix( 'taxes' );
849
850
		do_action( 'woocommerce_tax_rate_updated', $tax_rate_id, $tax_rate );
851
	}
852
853
	/**
854
	 * Delete a tax rate from the database.
855
	 *
856
	 * Internal use only.
857
	 *
858
	 * @since 2.3.0
859
	 * @access private
860
	 *
861
	 * @param  int $tax_rate_id
862
	 */
863
	public static function _delete_tax_rate( $tax_rate_id ) {
864
		global $wpdb;
865
866
		$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE tax_rate_id = %d;", $tax_rate_id ) );
867
		$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %d;", $tax_rate_id ) );
868
869
		WC_Cache_Helper::incr_cache_prefix( 'taxes' );
870
871
		do_action( 'woocommerce_tax_rate_deleted', $tax_rate_id );
872
	}
873
874
	/**
875
	 * Update postcodes for a tax rate in the DB.
876
	 *
877
	 * Internal use only.
878
	 *
879
	 * @since 2.3.0
880
	 * @access private
881
	 *
882
	 * @param  int $tax_rate_id
883
	 * @param  string $postcodes String of postcodes separated by ; characters
884
	 * @return string
885
	 */
886
	public static function _update_tax_rate_postcodes( $tax_rate_id, $postcodes ) {
887
		if ( ! is_array( $postcodes ) ) {
888
			$postcodes = explode( ';', $postcodes );
889
		}
890
		// No normalization - postcodes are matched against both normal and formatted versions to support wildcards.
891
		foreach ( $postcodes as $key => $postcode ) {
892
			$postcodes[ $key ] = strtoupper( trim( str_replace( chr( 226 ) . chr( 128 ) . chr( 166 ), '...', $postcode ) ) );
893
		}
894
		self::_update_tax_rate_locations( $tax_rate_id, array_diff( array_filter( $postcodes ), array( '*' ) ), 'postcode' );
895
	}
896
897
	/**
898
	 * Update cities for a tax rate in the DB.
899
	 *
900
	 * Internal use only.
901
	 *
902
	 * @since 2.3.0
903
	 * @access private
904
	 *
905
	 * @param  int $tax_rate_id
906
	 * @param  string $cities
907
	 * @return string
908
	 */
909
	public static function _update_tax_rate_cities( $tax_rate_id, $cities ) {
910
		if ( ! is_array( $cities ) ) {
911
			$cities = explode( ';', $cities );
912
		}
913
		$cities = array_filter( array_diff( array_map( array( __CLASS__, 'format_tax_rate_city' ), $cities ), array( '*' ) ) );
914
915
		self::_update_tax_rate_locations( $tax_rate_id, $cities, 'city' );
916
	}
917
918
	/**
919
	 * Updates locations (postcode and city).
920
	 *
921
	 * Internal use only.
922
	 *
923
	 * @since 2.3.0
924
	 * @access private
925
	 *
926
	 * @param  int $tax_rate_id
927
	 * @param string $type
928
	 * @return string
929
	 */
930
	private static function _update_tax_rate_locations( $tax_rate_id, $values, $type ) {
931
		global $wpdb;
932
933
		$tax_rate_id = absint( $tax_rate_id );
934
935
		$wpdb->query(
936
			$wpdb->prepare( "
937
				DELETE FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE tax_rate_id = %d AND location_type = %s;
938
				", $tax_rate_id, $type
939
			)
940
		);
941
942
		if ( sizeof( $values ) > 0 ) {
943
			$sql = "( '" . implode( "', $tax_rate_id, '" . esc_sql( $type ) . "' ),( '", array_map( 'esc_sql', $values ) ) . "', $tax_rate_id, '" . esc_sql( $type ) . "' )";
944
945
			$wpdb->query( "
946
				INSERT INTO {$wpdb->prefix}woocommerce_tax_rate_locations ( location_code, tax_rate_id, location_type ) VALUES $sql;
947
				" );
948
		}
949
950
		WC_Cache_Helper::incr_cache_prefix( 'taxes' );
951
	}
952
953
	/**
954
	 * Used by admin settings page.
955
	 *
956
	 * @param string $tax_class
957
	 *
958
	 * @return array|null|object
959
	 */
960
	public static function get_rates_for_tax_class( $tax_class ) {
961
		global $wpdb;
962
963
		// Get all the rates and locations. Snagging all at once should significantly cut down on the number of queries.
964
		$rates     = self::sort_rates( $wpdb->get_results( $wpdb->prepare( "SELECT * FROM `{$wpdb->prefix}woocommerce_tax_rates` WHERE `tax_rate_class` = %s;", sanitize_title( $tax_class ) ) ) );
965
		$locations = $wpdb->get_results( "SELECT * FROM `{$wpdb->prefix}woocommerce_tax_rate_locations`" );
966
967
		if ( ! empty( $rates ) ) {
968
			// Set the rates keys equal to their ids.
969
			$rates = array_combine( wp_list_pluck( $rates, 'tax_rate_id' ), $rates );
970
		}
971
972
		// Drop the locations into the rates array.
973
		foreach ( $locations as $location ) {
974
			// Don't set them for unexistent rates.
975
			if ( ! isset( $rates[ $location->tax_rate_id ] ) ) {
976
				continue;
977
			}
978
			// If the rate exists, initialize the array before appending to it.
979
			if ( ! isset( $rates[ $location->tax_rate_id ]->{$location->location_type} ) ) {
980
				$rates[ $location->tax_rate_id ]->{$location->location_type} = array();
981
			}
982
			$rates[ $location->tax_rate_id ]->{$location->location_type}[] = $location->location_code;
983
		}
984
985
		return $rates;
986
	}
987
}
988
WC_Tax::init();
989