Completed
Pull Request — master (#10769)
by Mike
07:56
created

WC_Tax::_get_wildcard_postcodes()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 8

Duplication

Lines 11
Ratio 100 %
Metric Value
dl 11
loc 11
rs 9.4285
cc 2
eloc 8
nc 2
nop 1
1
<?php
1 ignored issue
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 16 and the first side effect is on line 4.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
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_ROUNDING_PRECISION;
39
		self::$round_at_subtotal = 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' );
40
	}
41
42
	/**
43
	 * Calculate tax for a line.
44
	 * @param  float  $price              Price to calc tax on
45
	 * @param  array  $rates              Rates to apply
46
	 * @param  boolean $price_includes_tax Whether the passed price has taxes included
47
	 * @param  boolean $suppress_rounding  Whether to suppress any rounding from taking place
48
	 * @return array                      Array of rates + prices after tax
49
	 */
50
	public static function calc_tax( $price, $rates, $price_includes_tax = false, $suppress_rounding = false ) {
51
		// Work in pence to X precision
52
		$price = self::precision( $price );
53
54
		if ( $price_includes_tax ) {
55
			$taxes = self::calc_inclusive_tax( $price, $rates );
56
		} else {
57
			$taxes = self::calc_exclusive_tax( $price, $rates );
58
		}
59
60
		// Round to precision
61
		if ( ! self::$round_at_subtotal && ! $suppress_rounding ) {
62
			$taxes = array_map( 'round', $taxes ); // Round to precision
63
		}
64
65
		// Remove precision
66
		$price     = self::remove_precision( $price );
67
		$taxes     = array_map( array( __CLASS__, 'remove_precision' ), $taxes );
68
69
		return apply_filters( 'woocommerce_calc_tax', $taxes, $price, $rates, $price_includes_tax, $suppress_rounding );
70
	}
71
72
	/**
73
	 * Calculate the shipping tax using a passed array of rates.
74
	 *
75
	 * @param   float		Price
76
	 * @param	array		Taxation Rate
77
	 * @return  array
78
	 */
79
	public static function calc_shipping_tax( $price, $rates ) {
80
		return self::calc_exclusive_tax( $price, $rates );
81
	}
82
83
	/**
84
	 * Multiply cost by pow precision.
85
	 * @param  float $price
86
	 * @return float
87
	 */
88
	private static function precision( $price ) {
89
		return $price * ( pow( 10, self::$precision ) );
90
	}
91
92
	/**
93
	 * Divide cost by pow precision.
94
	 * @param  float $price
95
	 * @return float
96
	 */
97
	private static function remove_precision( $price ) {
98
		return $price / ( pow( 10, self::$precision ) );
99
	}
100
101
	/**
102
	 * Round to precision.
103
	 *
104
	 * Filter example: to return rounding to .5 cents you'd use:
105
	 *
106
	 * public function euro_5cent_rounding( $in ) {
107
	 *      return round( $in / 5, 2 ) * 5;
108
	 * }
109
	 * add_filter( 'woocommerce_tax_round', 'euro_5cent_rounding' );
110
	 * @return double
111
	 */
112
	public static function round( $in ) {
113
		return apply_filters( 'woocommerce_tax_round', round( $in, self::$precision ), $in );
114
	}
115
116
	/**
117
	 * Calc tax from inclusive price.
118
	 *
119
	 * @param  float $price
120
	 * @param  array $rates
121
	 * @return array
122
	 */
123
	public static function calc_inclusive_tax( $price, $rates ) {
124
		$taxes = array();
125
126
		$regular_tax_rates = $compound_tax_rates = 0;
127
128
		foreach ( $rates as $key => $rate )
129
			if ( $rate['compound'] == 'yes' )
130
				$compound_tax_rates = $compound_tax_rates + $rate['rate'];
131
			else
132
				$regular_tax_rates  = $regular_tax_rates + $rate['rate'];
133
134
		$regular_tax_rate 	= 1 + ( $regular_tax_rates / 100 );
135
		$compound_tax_rate 	= 1 + ( $compound_tax_rates / 100 );
136
		$non_compound_price = $price / $compound_tax_rate;
137
138
		foreach ( $rates as $key => $rate ) {
139
			if ( ! isset( $taxes[ $key ] ) )
140
				$taxes[ $key ] = 0;
141
142
			$the_rate      = $rate['rate'] / 100;
143
144
			if ( $rate['compound'] == 'yes' ) {
145
				$the_price = $price;
146
				$the_rate  = $the_rate / $compound_tax_rate;
147
			} else {
148
				$the_price = $non_compound_price;
149
				$the_rate  = $the_rate / $regular_tax_rate;
150
			}
151
152
			$net_price       = $price - ( $the_rate * $the_price );
153
			$tax_amount      = $price - $net_price;
154
			$taxes[ $key ]   += apply_filters( 'woocommerce_price_inc_tax_amount', $tax_amount, $key, $rate, $price );
155
		}
156
157
		return $taxes;
158
	}
159
160
	/**
161
	 * Calc tax from exclusive price.
162
	 *
163
	 * @param  float $price
164
	 * @param  array $rates
165
	 * @return array
166
	 */
167
	public static function calc_exclusive_tax( $price, $rates ) {
168
		$taxes = array();
169
170
		if ( $rates ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $rates 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...
171
			// Multiple taxes
172
			foreach ( $rates as $key => $rate ) {
173
174
				if ( $rate['compound'] == 'yes' )
175
					continue;
176
177
				$tax_amount = $price * ( $rate['rate'] / 100 );
178
179
				// ADVANCED: Allow third parties to modify this rate
180
				$tax_amount = apply_filters( 'woocommerce_price_ex_tax_amount', $tax_amount, $key, $rate, $price );
181
182
				// Add rate
183 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...
184
					$taxes[ $key ] = $tax_amount;
185
				else
186
					$taxes[ $key ] += $tax_amount;
187
			}
188
189
			$pre_compound_total = array_sum( $taxes );
190
191
			// Compound taxes
192
			foreach ( $rates as $key => $rate ) {
193
194
				if ( $rate['compound'] == 'no' )
195
					continue;
196
197
				$the_price_inc_tax = $price + ( $pre_compound_total );
198
199
				$tax_amount = $the_price_inc_tax * ( $rate['rate'] / 100 );
200
201
				// ADVANCED: Allow third parties to modify this rate
202
				$tax_amount = apply_filters( 'woocommerce_price_ex_tax_amount', $tax_amount, $key, $rate, $price, $the_price_inc_tax, $pre_compound_total );
203
204
				// Add rate
205 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...
206
					$taxes[ $key ] = $tax_amount;
207
				else
208
					$taxes[ $key ] += $tax_amount;
209
			}
210
		}
211
212
		return $taxes;
213
	}
214
215
	/**
216
	 * Searches for all matching country/state/postcode tax rates.
217
	 *
218
	 * @param array $args
219
	 * @return array
220
	 */
221
	public static function find_rates( $args = array() ) {
222
		$args = wp_parse_args( $args, array(
223
			'country'   => '',
224
			'state'     => '',
225
			'city'      => '',
226
			'postcode'  => '',
227
			'tax_class' => ''
228
		) );
229
230
		extract( $args, EXTR_SKIP );
231
232
		if ( ! $country ) {
233
			return array();
234
		}
235
236
		$postcode          = wc_clean( $postcode );
237
		$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 ) );
238
		$matched_tax_rates = wp_cache_get( $cache_key, 'taxes' );
239
240
		if ( false === $matched_tax_rates ) {
241
			$matched_tax_rates = self::get_matched_tax_rates( $country, $state, $postcode, $city, $tax_class );
1 ignored issue
show
Bug introduced by
It seems like $postcode defined by wc_clean($postcode) on line 236 can also be of type array; however, WC_Tax::get_matched_tax_rates() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
242
			wp_cache_set( $cache_key, $matched_tax_rates, 'taxes' );
243
		}
244
245
		return apply_filters( 'woocommerce_find_rates', $matched_tax_rates, $args );
246
	}
247
248
	/**
249
	 * Searches for all matching country/state/postcode tax rates.
250
	 *
251
	 * @param array $args
252
	 * @return array
253
	 */
254
	public static function find_shipping_rates( $args = array() ) {
255
		$rates          = self::find_rates( $args );
256
		$shipping_rates = array();
257
258
		if ( is_array( $rates ) ) {
259
			foreach ( $rates as $key => $rate ) {
260
				if ( 'yes' === $rate['shipping'] ) {
261
					$shipping_rates[ $key ] = $rate;
262
				}
263
			}
264
		}
265
266
		return $shipping_rates;
267
	}
268
269
	/**
270
	 * Loop through a set of tax rates and get the matching rates (1 per priority).
271
	 *
272
	 * @param  string $country
273
	 * @param  string $state
274
	 * @param  string $postcode
275
	 * @param  string $city
276
	 * @param  string $tax_class
277
	 * @return array
278
	 */
279
	private static function get_matched_tax_rates( $country, $state, $postcode, $city, $tax_class ) {
280
		global $wpdb;
281
282
		// Query criteria - these will be ANDed
283
		$criteria   = array();
284
		$criteria[] = $wpdb->prepare( "tax_rate_country IN ( %s, '' )", strtoupper( $country ) );
285
		$criteria[] = $wpdb->prepare( "tax_rate_state IN ( %s, '' )", strtoupper( $state ) );
286
		$criteria[] = $wpdb->prepare( "tax_rate_class = %s", sanitize_title( $tax_class ) );
287
288
		// Pre-query postcode ranges for PHP based matching.
289
		$postcode_search = wc_get_wildcard_postcodes( $postcode );
290
		$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 '%-%';" );
291
292
		if ( $postcode_ranges ) {
293
			$matches         = wc_postcode_location_matcher( $postcode, $postcode_ranges, 'tax_rate_id', 'location_code' );
294
			$postcode_search = array_unique( array_merge( $postcode_search, array_values( $matches ) ) );
295
		}
296
297
		/**
298
		 * Location matching criteria - ORed
299
		 * Needs to match:
300
		 * 	- rates with no postcodes and cities
301
		 * 	- rates with a matching postcode and city
302
		 * 	- rates with matching postcode, no city
303
		 * 	- rates with matching city, no postcode
304
		 */
305
		$locations_criteria   = array();
306
		$locations_criteria[] = "locations.location_type IS NULL";
307
		$locations_criteria[] = "
308
			locations.location_type = 'postcode' AND locations.location_code IN ('" . implode( "','", array_map( 'esc_sql', $postcode_search ) ) . "')
309
			AND (
310
				( locations2.location_type = 'city' AND locations2.location_code = '" . esc_sql( strtoupper( $city ) ) . "' )
311
				OR NOT EXISTS (
312
					SELECT sub.tax_rate_id FROM {$wpdb->prefix}woocommerce_tax_rate_locations as sub
313
					WHERE sub.location_type = 'city'
314
					AND sub.tax_rate_id = tax_rates.tax_rate_id
315
				)
316
			)
317
		";
318
		$locations_criteria[] = "
319
			locations.location_type = 'city' AND locations.location_code = '" . esc_sql( strtoupper( $city ) ) . "'
320
			AND NOT EXISTS (
321
				SELECT sub.tax_rate_id FROM {$wpdb->prefix}woocommerce_tax_rate_locations as sub
322
				WHERE sub.location_type = 'postcode'
323
				AND sub.tax_rate_id = tax_rates.tax_rate_id
324
			)
325
		";
326
		$criteria[] = '( ( ' . implode( ' ) OR ( ', $locations_criteria ) . ' ) )';
327
328
		$found_rates = $wpdb->get_results( "
329
			SELECT tax_rates.*
330
			FROM {$wpdb->prefix}woocommerce_tax_rates as tax_rates
331
			LEFT OUTER JOIN {$wpdb->prefix}woocommerce_tax_rate_locations as locations ON tax_rates.tax_rate_id = locations.tax_rate_id
332
			LEFT OUTER JOIN {$wpdb->prefix}woocommerce_tax_rate_locations as locations2 ON tax_rates.tax_rate_id = locations2.tax_rate_id
333
			WHERE 1=1 AND " . implode( ' AND ', $criteria ) . "
334
			GROUP BY tax_rate_id
335
			ORDER BY tax_rate_priority, tax_rate_order
336
		" );
337
338
		$matched_tax_rates = array();
339
		$found_priority    = array();
340
341
		foreach ( $found_rates as $found_rate ) {
342
			if ( in_array( $found_rate->tax_rate_priority, $found_priority ) ) {
343
				continue;
344
			}
345
346
			$matched_tax_rates[ $found_rate->tax_rate_id ] = array(
347
				'rate'     => $found_rate->tax_rate,
348
				'label'    => $found_rate->tax_rate_name,
349
				'shipping' => $found_rate->tax_rate_shipping ? 'yes' : 'no',
350
				'compound' => $found_rate->tax_rate_compound ? 'yes' : 'no'
351
			);
352
353
			$found_priority[] = $found_rate->tax_rate_priority;
354
		}
355
356
		return apply_filters( 'woocommerce_matched_tax_rates', $matched_tax_rates, $country, $state, $postcode, $city, $tax_class );
357
	}
358
359
	/**
360
	 * Get the customer tax location based on their status and the current page.
361
	 *
362
	 * Used by get_rates(), get_shipping_rates().
363
	 *
364
	 * @param  $tax_class string Optional, passed to the filter for advanced tax setups.
365
	 * @return array
366
	 */
367
	public static function get_tax_location( $tax_class = '' ) {
368
		$location = array();
369
370
		if ( ! empty( WC()->customer ) ) {
371
			$location = WC()->customer->get_taxable_address();
372
		} elseif ( wc_prices_include_tax() || 'base' === get_option( 'woocommerce_default_customer_address' ) || 'base' === get_option( 'woocommerce_tax_based_on' ) ) {
373
			$location = array(
374
				WC()->countries->get_base_country(),
375
				WC()->countries->get_base_state(),
376
				WC()->countries->get_base_postcode(),
377
				WC()->countries->get_base_city()
378
			);
379
		}
380
381
		return apply_filters( 'woocommerce_get_tax_location', $location, $tax_class );
382
	}
383
384
	/**
385
	 * Get's an array of matching rates for a tax class.
386
	 * @param string $tax_class
387
	 * @return  array
388
	 */
389
	public static function get_rates( $tax_class = '' ) {
390
		$tax_class         = sanitize_title( $tax_class );
391
		$location          = self::get_tax_location( $tax_class );
392
		$matched_tax_rates = array();
393
394
		if ( sizeof( $location ) === 4 ) {
395
			list( $country, $state, $postcode, $city ) = $location;
396
397
			$matched_tax_rates = self::find_rates( array(
398
				'country' 	=> $country,
399
				'state' 	=> $state,
400
				'postcode' 	=> $postcode,
401
				'city' 		=> $city,
402
				'tax_class' => $tax_class
403
			) );
404
		}
405
406
		return apply_filters( 'woocommerce_matched_rates', $matched_tax_rates, $tax_class );
407
	}
408
409
	/**
410
	 * Get's an array of matching rates for the shop's base country.
411
	 *
412
	 * @param   string	Tax Class
413
	 * @return  array
414
	 */
415
	public static function get_base_tax_rates( $tax_class = '' ) {
416
		return apply_filters( 'woocommerce_base_tax_rates', self::find_rates( array(
417
			'country' 	=> WC()->countries->get_base_country(),
418
			'state' 	=> WC()->countries->get_base_state(),
419
			'postcode' 	=> WC()->countries->get_base_postcode(),
420
			'city' 		=> WC()->countries->get_base_city(),
421
			'tax_class' => $tax_class
422
		) ), $tax_class );
423
	}
424
425
	/**
426
	 * Alias for get_base_tax_rates().
427
	 *
428
	 * @deprecated 2.3
429
	 * @param   string	Tax Class
430
	 * @return  array
431
	 */
432
	public static function get_shop_base_rate( $tax_class = '' ) {
433
		return self::get_base_tax_rates( $tax_class );
434
	}
435
436
	/**
437
	 * Gets an array of matching shipping tax rates for a given class.
438
	 *
439
	 * @param   string	Tax Class
440
	 * @return  mixed
441
	 */
442
	public static function get_shipping_tax_rates( $tax_class = null ) {
443
		// See if we have an explicitly set shipping tax class
444
		if ( $shipping_tax_class = get_option( 'woocommerce_shipping_tax_class' ) ) {
445
			$tax_class = 'standard' === $shipping_tax_class ? '' : $shipping_tax_class;
446
		}
447
448
		$location          = self::get_tax_location( $tax_class );
449
		$matched_tax_rates = array();
450
451
		if ( sizeof( $location ) === 4 ) {
452
			list( $country, $state, $postcode, $city ) = $location;
453
454
			if ( ! is_null( $tax_class ) ) {
455
				// This will be per item shipping
456
				$matched_tax_rates = self::find_shipping_rates( array(
457
					'country' 	=> $country,
458
					'state' 	=> $state,
459
					'postcode' 	=> $postcode,
460
					'city' 		=> $city,
461
					'tax_class' => $tax_class
462
				) );
463
464 View Code Duplication
			} else {
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...
465
466
				// This will be per order shipping - loop through the order and find the highest tax class rate
467
				$cart_tax_classes = WC()->cart->get_cart_item_tax_classes();
468
469
				// If multiple classes are found, use the first one. Don't bother with standard rate, we can get that later.
470
				if ( sizeof( $cart_tax_classes ) > 1 && ! in_array( '', $cart_tax_classes ) ) {
471
					$tax_classes = self::get_tax_classes();
472
473
					foreach ( $tax_classes as $tax_class ) {
474
						$tax_class = sanitize_title( $tax_class );
475
						if ( in_array( $tax_class, $cart_tax_classes ) ) {
476
							$matched_tax_rates = self::find_shipping_rates( array(
477
								'country' 	=> $country,
478
								'state' 	=> $state,
479
								'postcode' 	=> $postcode,
480
								'city' 		=> $city,
481
								'tax_class' => $tax_class
482
							) );
483
							break;
484
						}
485
					}
486
487
				// If a single tax class is found, use it
488
				} elseif ( sizeof( $cart_tax_classes ) == 1 ) {
489
					$matched_tax_rates = self::find_shipping_rates( array(
490
						'country' 	=> $country,
491
						'state' 	=> $state,
492
						'postcode' 	=> $postcode,
493
						'city' 		=> $city,
494
						'tax_class' => $cart_tax_classes[0]
495
					) );
496
				}
497
			}
498
499
			// Get standard rate if no taxes were found
500
			if ( ! sizeof( $matched_tax_rates ) ) {
501
				$matched_tax_rates = self::find_shipping_rates( array(
502
					'country' 	=> $country,
503
					'state' 	=> $state,
504
					'postcode' 	=> $postcode,
505
					'city' 		=> $city
506
				) );
507
			}
508
		}
509
510
		return $matched_tax_rates;
511
	}
512
513
	/**
514
	 * Return true/false depending on if a rate is a compound rate.
515
	 *
516
	 * @param   int		key
517
	 * @return  bool
518
	 */
519
	public static function is_compound( $key ) {
520
		global $wpdb;
521
		return $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate_compound FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) ) ? true : false;
522
	}
523
524
	/**
525
	 * Return a given rates label.
526
	 *
527
	 * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format
528
	 * @return  string
529
	 */
530
	public static function get_rate_label( $key_or_rate ) {
531
		global $wpdb;
532
533 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...
534
			$key       = $key_or_rate->tax_rate_id;
535
			$rate_name = $key_or_rate->tax_rate_name;
536
		} else {
537
			$key       = $key_or_rate;
538
			$rate_name = $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate_name FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) );
539
		}
540
541
		if ( ! $rate_name ) {
542
			$rate_name = WC()->countries->tax_or_vat();
543
		}
544
545
		return apply_filters( 'woocommerce_rate_label', $rate_name, $key );
546
	}
547
548
	/**
549
	 * Return a given rates percent.
550
	 *
551
	 * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format
552
	 * @return  string
553
	 */
554
	public static function get_rate_percent( $key_or_rate ) {
555
		global $wpdb;
556
557 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...
558
			$key      = $key_or_rate->tax_rate_id;
559
			$tax_rate = $key_or_rate->tax_rate;
560
		} else {
561
			$key      = $key_or_rate;
562
			$tax_rate = $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) );
563
		}
564
565
		return apply_filters( 'woocommerce_rate_percent', floatval( $tax_rate ) . '%', $key );
566
	}
567
568
	/**
569
	 * Get a rates code. Code is made up of COUNTRY-STATE-NAME-Priority. E.g GB-VAT-1, US-AL-TAX-1.
570
	 *
571
	 * @access public
572
	 * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format
573
	 * @return string
574
	 */
575
	public static function get_rate_code( $key_or_rate ) {
576
		global $wpdb;
577
578 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...
579
			$key  = $key_or_rate->tax_rate_id;
580
			$rate = $key_or_rate;
581
		} else {
582
			$key  = $key_or_rate;
583
			$rate = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) );
584
		}
585
586
		$code_string = '';
587
588
		if ( null !== $rate ) {
589
			$code   = array();
590
			$code[] = $rate->tax_rate_country;
591
			$code[] = $rate->tax_rate_state;
592
			$code[] = $rate->tax_rate_name ? $rate->tax_rate_name : 'TAX';
593
			$code[] = absint( $rate->tax_rate_priority );
594
			$code_string = strtoupper( implode( '-', array_filter( $code ) ) );
595
		}
596
597
		return apply_filters( 'woocommerce_rate_code', $code_string, $key );
598
	}
599
600
	/**
601
	 * Round tax lines and return the sum.
602
	 *
603
	 * @param   array
604
	 * @return  float
605
	 */
606
	public static function get_tax_total( $taxes ) {
607
		return array_sum( array_map( array( __CLASS__, 'round' ), $taxes ) );
608
	}
609
610
	/**
611
	 * Get store tax classes.
612
	 * @return array
613
	 */
614
	public static function get_tax_classes() {
615
		return array_filter( array_map( 'trim', explode( "\n", get_option( 'woocommerce_tax_classes' ) ) ) );
616
	}
617
618
	/**
619
	 * format the postcodes.
620
	 * @param  string $postcode
621
	 * @return string
622
	 */
623
	private static function format_tax_rate_postcode( $postcode ) {
624
		return strtoupper( trim( $postcode ) );
625
	}
626
627
	/**
628
	 * format the city.
629
	 * @param  string $city
630
	 * @return string
631
	 */
632
	private static function format_tax_rate_city( $city ) {
633
		return strtoupper( trim( $city ) );
634
	}
635
636
	/**
637
	 * format the state.
638
	 * @param  string $state
639
	 * @return string
640
	 */
641
	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...
642
		$state = strtoupper( $state );
643
		return $state === '*' ? '' : $state;
644
	}
645
646
	/**
647
	 * format the country.
648
	 * @param  string $country
649
	 * @return string
650
	 */
651
	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...
652
		$country = strtoupper( $country );
653
		return $country === '*' ? '' : $country;
654
	}
655
656
	/**
657
	 * format the tax rate name.
658
	 * @param  string $name
659
	 * @return string
660
	 */
661
	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...
662
		return $name ? $name : __( 'Tax', 'woocommerce' );
663
	}
664
665
	/**
666
	 * format the rate.
667
	 * @param  double $rate
668
	 * @return string
669
	 */
670
	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...
671
		return number_format( (double) $rate, 4, '.', '' );
672
	}
673
674
	/**
675
	 * format the priority.
676
	 * @param  string $priority
677
	 * @return int
678
	 */
679
	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...
680
		return absint( $priority );
681
	}
682
683
	/**
684
	 * format the class.
685
	 * @param  string $class
686
	 * @return string
687
	 */
688
	public static function format_tax_rate_class( $class ) {
689
		$class = sanitize_title( $class );
690
		$sanitized_classes = array_map( 'sanitize_title', self::get_tax_classes() );
691
		if ( ! in_array( $class, $sanitized_classes ) ) {
692
			$class = '';
693
		}
694
		return $class === 'standard' ? '' : $class;
695
	}
696
697
	/**
698
	 * Prepare and format tax rate for DB insertion.
699
	 * @param  array $tax_rate
700
	 * @return array
701
	 */
702
	private static function prepare_tax_rate( $tax_rate ) {
703
		foreach ( $tax_rate as $key => $value ) {
704
			if ( method_exists( __CLASS__, 'format_' . $key ) ) {
705
				$tax_rate[ $key ] = call_user_func( array( __CLASS__, 'format_' . $key ), $value );
706
			}
707
		}
708
		return $tax_rate;
709
	}
710
711
	/**
712
	 * Insert a new tax rate.
713
	 *
714
	 * Internal use only.
715
	 *
716
	 * @since 2.3.0
717
	 * @access private
718
	 *
719
	 * @param  array $tax_rate
720
	 *
721
	 * @return int tax rate id
722
	 */
723
	public static function _insert_tax_rate( $tax_rate ) {
724
		global $wpdb;
725
726
		$wpdb->insert( $wpdb->prefix . 'woocommerce_tax_rates', self::prepare_tax_rate( $tax_rate ) );
727
728
		WC_Cache_Helper::incr_cache_prefix( 'taxes' );
729
730
		do_action( 'woocommerce_tax_rate_added', $wpdb->insert_id, $tax_rate );
731
732
		return $wpdb->insert_id;
733
	}
734
735
	/**
736
	 * Get tax rate.
737
	 *
738
	 * Internal use only.
739
	 *
740
	 * @since 2.5.0
741
	 * @access private
742
	 *
743
	 * @param int $tax_rate_id
744
	 * @param string $output_type
745
	 *
746
	 * @return array
747
	 */
748
	public static function _get_tax_rate( $tax_rate_id, $output_type = ARRAY_A ) {
749
		global $wpdb;
750
751
		return $wpdb->get_row( $wpdb->prepare( "
752
			SELECT *
753
			FROM {$wpdb->prefix}woocommerce_tax_rates
754
			WHERE tax_rate_id = %d
755
		", $tax_rate_id ), $output_type );
756
	}
757
758
	/**
759
	 * Update a tax rate.
760
	 *
761
	 * Internal use only.
762
	 *
763
	 * @since 2.3.0
764
	 * @access private
765
	 *
766
	 * @param int $tax_rate_id
767
	 * @param array $tax_rate
768
	 */
769
	public static function _update_tax_rate( $tax_rate_id, $tax_rate ) {
770
		global $wpdb;
771
772
		$tax_rate_id = absint( $tax_rate_id );
773
774
		$wpdb->update(
775
			$wpdb->prefix . "woocommerce_tax_rates",
776
			self::prepare_tax_rate( $tax_rate ),
777
			array(
778
				'tax_rate_id' => $tax_rate_id
779
			)
780
		);
781
782
		WC_Cache_Helper::incr_cache_prefix( 'taxes' );
783
784
		do_action( 'woocommerce_tax_rate_updated', $tax_rate_id, $tax_rate );
785
	}
786
787
	/**
788
	 * Delete a tax rate from the database.
789
	 *
790
	 * Internal use only.
791
	 *
792
	 * @since 2.3.0
793
	 * @access private
794
	 *
795
	 * @param  int $tax_rate_id
796
	 */
797
	public static function _delete_tax_rate( $tax_rate_id ) {
798
		global $wpdb;
799
800
		$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE tax_rate_id = %d;", $tax_rate_id ) );
801
		$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %d;", $tax_rate_id ) );
802
803
		WC_Cache_Helper::incr_cache_prefix( 'taxes' );
804
805
		do_action( 'woocommerce_tax_rate_deleted', $tax_rate_id );
806
	}
807
808
	/**
809
	 * Update postcodes for a tax rate in the DB.
810
	 *
811
	 * Internal use only.
812
	 *
813
	 * @since 2.3.0
814
	 * @access private
815
	 *
816
	 * @param  int $tax_rate_id
817
	 * @param  string $postcodes String of postcodes separated by ; characters
818
	 * @return string
819
	 */
820
	public static function _update_tax_rate_postcodes( $tax_rate_id, $postcodes ) {
821
		if ( ! is_array( $postcodes ) ) {
822
			$postcodes = explode( ';', $postcodes );
823
		}
824
		$postcodes = array_filter( array_diff( array_map( array( __CLASS__, 'format_tax_rate_postcode' ), $postcodes ), array( '*' ) ) );
825
826
		self::_update_tax_rate_locations( $tax_rate_id, $postcodes, 'postcode' );
827
	}
828
829
	/**
830
	 * Update cities for a tax rate in the DB.
831
	 *
832
	 * Internal use only.
833
	 *
834
	 * @since 2.3.0
835
	 * @access private
836
	 *
837
	 * @param  int $tax_rate_id
838
	 * @param  string $cities
839
	 * @return string
840
	 */
841
	public static function _update_tax_rate_cities( $tax_rate_id, $cities ) {
842
		if ( ! is_array( $cities ) ) {
843
			$cities = explode( ';', $cities );
844
		}
845
		$cities = array_filter( array_diff( array_map( array( __CLASS__, 'format_tax_rate_city' ), $cities ), array( '*' ) ) );
846
847
		self::_update_tax_rate_locations( $tax_rate_id, $cities, 'city' );
848
	}
849
850
	/**
851
	 * Updates locations (postcode and city).
852
	 *
853
	 * Internal use only.
854
	 *
855
	 * @since 2.3.0
856
	 * @access private
857
	 *
858
	 * @param  int $tax_rate_id
859
	 * @param string $type
860
	 * @return string
861
	 */
862
	private static function _update_tax_rate_locations( $tax_rate_id, $values, $type ) {
863
		global $wpdb;
864
865
		$tax_rate_id = absint( $tax_rate_id );
866
867
		$wpdb->query(
868
			$wpdb->prepare( "
869
				DELETE FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE tax_rate_id = %d AND location_type = %s;
870
				", $tax_rate_id, $type
871
			)
872
		);
873
874
		if ( sizeof( $values ) > 0 ) {
875
			$sql = "( '" . implode( "', $tax_rate_id, '" . esc_sql( $type ) . "' ),( '", array_map( 'esc_sql', $values ) ) . "', $tax_rate_id, '" . esc_sql( $type ) . "' )";
876
877
			$wpdb->query( "
878
				INSERT INTO {$wpdb->prefix}woocommerce_tax_rate_locations ( location_code, tax_rate_id, location_type ) VALUES $sql;
879
				" );
880
		}
881
882
		WC_Cache_Helper::incr_cache_prefix( 'taxes' );
883
	}
884
885
	/**
886
	 * Used by admin settings page.
887
	 *
888
	 * @param string $tax_class
889
	 *
890
	 * @return array|null|object
891
	 */
892
	public static function get_rates_for_tax_class( $tax_class ) {
893
		global $wpdb;
894
895
		// Get all the rates and locations. Snagging all at once should significantly cut down on the number of queries.
896
		$rates     = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM `{$wpdb->prefix}woocommerce_tax_rates` WHERE `tax_rate_class` = %s ORDER BY `tax_rate_order`;", sanitize_title( $tax_class ) ) );
897
		$locations = $wpdb->get_results( "SELECT * FROM `{$wpdb->prefix}woocommerce_tax_rate_locations`" );
898
899
		if ( ! empty( $rates ) ) {
900
			// Set the rates keys equal to their ids.
901
			$rates = array_combine( wp_list_pluck( $rates, 'tax_rate_id' ), $rates );
902
		}
903
904
		// Drop the locations into the rates array.
905
		foreach ( $locations as $location ) {
906
			// Don't set them for unexistent rates.
907
			if ( ! isset( $rates[ $location->tax_rate_id ] ) ) {
908
				continue;
909
			}
910
			// If the rate exists, initialize the array before appending to it.
911
			if ( ! isset( $rates[ $location->tax_rate_id ]->{$location->location_type} ) ) {
912
				$rates[ $location->tax_rate_id ]->{$location->location_type} = array();
913
			}
914
			$rates[ $location->tax_rate_id ]->{$location->location_type}[] = $location->location_code;
915
		}
916
917
		return $rates;
918
	}
919
}
920
WC_Tax::init();
921