No Description

class-wc-tax.php 37KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249
  1. <?php
  2. /**
  3. * Tax calculation and rate finding class.
  4. *
  5. * @package WooCommerce\Classes
  6. */
  7. use Automattic\WooCommerce\Utilities\NumberUtil;
  8. defined( 'ABSPATH' ) || exit;
  9. /**
  10. * Performs tax calculations and loads tax rates
  11. *
  12. * @class WC_Tax
  13. */
  14. class WC_Tax {
  15. /**
  16. * Precision.
  17. *
  18. * @var int
  19. */
  20. public static $precision;
  21. /**
  22. * Round at subtotal.
  23. *
  24. * @var bool
  25. */
  26. public static $round_at_subtotal = false;
  27. /**
  28. * Load options.
  29. */
  30. public static function init() {
  31. self::$precision = wc_get_rounding_precision();
  32. self::$round_at_subtotal = 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' );
  33. }
  34. /**
  35. * When the woocommerce_tax_classes option is changed, remove any orphan rates.
  36. *
  37. * @deprecated 3.7.0
  38. * @param string $old_value Old rates value.
  39. * @param string $value New rates value.
  40. */
  41. public static function maybe_remove_tax_class_rates( $old_value, $value ) {
  42. wc_deprecated_function( 'WC_Tax::maybe_remove_tax_class_rates', '3.7', 'WC_Tax::delete_tax_class_by' );
  43. $tax_classes = array_filter( array_map( 'trim', explode( "\n", $value ) ) );
  44. $existing_tax_classes = self::get_tax_classes();
  45. $removed = array_diff( $existing_tax_classes, $tax_classes );
  46. foreach ( $removed as $name ) {
  47. self::delete_tax_class_by( 'name', $name );
  48. }
  49. }
  50. /**
  51. * Calculate tax for a line.
  52. *
  53. * @param float $price Price to calc tax on.
  54. * @param array $rates Rates to apply.
  55. * @param boolean $price_includes_tax Whether the passed price has taxes included.
  56. * @param boolean $deprecated Whether to suppress any rounding from taking place. No longer used here.
  57. * @return array Array of rates + prices after tax.
  58. */
  59. public static function calc_tax( $price, $rates, $price_includes_tax = false, $deprecated = false ) {
  60. if ( $price_includes_tax ) {
  61. $taxes = self::calc_inclusive_tax( $price, $rates );
  62. } else {
  63. $taxes = self::calc_exclusive_tax( $price, $rates );
  64. }
  65. return apply_filters( 'woocommerce_calc_tax', $taxes, $price, $rates, $price_includes_tax, $deprecated );
  66. }
  67. /**
  68. * Calculate the shipping tax using a passed array of rates.
  69. *
  70. * @param float $price Shipping cost.
  71. * @param array $rates Taxation Rate.
  72. * @return array
  73. */
  74. public static function calc_shipping_tax( $price, $rates ) {
  75. $taxes = self::calc_exclusive_tax( $price, $rates );
  76. return apply_filters( 'woocommerce_calc_shipping_tax', $taxes, $price, $rates );
  77. }
  78. /**
  79. * Round to precision.
  80. *
  81. * Filter example: to return rounding to .5 cents you'd use:
  82. *
  83. * function euro_5cent_rounding( $in ) {
  84. * return round( $in / 5, 2 ) * 5;
  85. * }
  86. * add_filter( 'woocommerce_tax_round', 'euro_5cent_rounding' );
  87. *
  88. * @param float|int $in Value to round.
  89. * @return float
  90. */
  91. public static function round( $in ) {
  92. return apply_filters( 'woocommerce_tax_round', NumberUtil::round( $in, wc_get_rounding_precision() ), $in );
  93. }
  94. /**
  95. * Calc tax from inclusive price.
  96. *
  97. * @param float $price Price to calculate tax for.
  98. * @param array $rates Array of tax rates.
  99. * @return array
  100. */
  101. public static function calc_inclusive_tax( $price, $rates ) {
  102. $taxes = array();
  103. $compound_rates = array();
  104. $regular_rates = array();
  105. // Index array so taxes are output in correct order and see what compound/regular rates we have to calculate.
  106. foreach ( $rates as $key => $rate ) {
  107. $taxes[ $key ] = 0;
  108. if ( 'yes' === $rate['compound'] ) {
  109. $compound_rates[ $key ] = $rate['rate'];
  110. } else {
  111. $regular_rates[ $key ] = $rate['rate'];
  112. }
  113. }
  114. $compound_rates = array_reverse( $compound_rates, true ); // Working backwards.
  115. $non_compound_price = $price;
  116. foreach ( $compound_rates as $key => $compound_rate ) {
  117. $tax_amount = apply_filters( 'woocommerce_price_inc_tax_amount', $non_compound_price - ( $non_compound_price / ( 1 + ( $compound_rate / 100 ) ) ), $key, $rates[ $key ], $price );
  118. $taxes[ $key ] += $tax_amount;
  119. $non_compound_price = $non_compound_price - $tax_amount;
  120. }
  121. // Regular taxes.
  122. $regular_tax_rate = 1 + ( array_sum( $regular_rates ) / 100 );
  123. foreach ( $regular_rates as $key => $regular_rate ) {
  124. $the_rate = ( $regular_rate / 100 ) / $regular_tax_rate;
  125. $net_price = $price - ( $the_rate * $non_compound_price );
  126. $tax_amount = apply_filters( 'woocommerce_price_inc_tax_amount', $price - $net_price, $key, $rates[ $key ], $price );
  127. $taxes[ $key ] += $tax_amount;
  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. $taxes = array_map( array( __CLASS__, 'round' ), $taxes );
  135. return $taxes;
  136. }
  137. /**
  138. * Calc tax from exclusive price.
  139. *
  140. * @param float $price Price to calculate tax for.
  141. * @param array $rates Array of tax rates.
  142. * @return array
  143. */
  144. public static function calc_exclusive_tax( $price, $rates ) {
  145. $taxes = array();
  146. if ( ! empty( $rates ) ) {
  147. foreach ( $rates as $key => $rate ) {
  148. if ( 'yes' === $rate['compound'] ) {
  149. continue;
  150. }
  151. $tax_amount = $price * ( $rate['rate'] / 100 );
  152. $tax_amount = apply_filters( 'woocommerce_price_ex_tax_amount', $tax_amount, $key, $rate, $price ); // ADVANCED: Allow third parties to modify this rate.
  153. if ( ! isset( $taxes[ $key ] ) ) {
  154. $taxes[ $key ] = $tax_amount;
  155. } else {
  156. $taxes[ $key ] += $tax_amount;
  157. }
  158. }
  159. $pre_compound_total = array_sum( $taxes );
  160. // Compound taxes.
  161. foreach ( $rates as $key => $rate ) {
  162. if ( 'no' === $rate['compound'] ) {
  163. continue;
  164. }
  165. $the_price_inc_tax = $price + ( $pre_compound_total );
  166. $tax_amount = $the_price_inc_tax * ( $rate['rate'] / 100 );
  167. $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.
  168. if ( ! isset( $taxes[ $key ] ) ) {
  169. $taxes[ $key ] = $tax_amount;
  170. } else {
  171. $taxes[ $key ] += $tax_amount;
  172. }
  173. $pre_compound_total = array_sum( $taxes );
  174. }
  175. }
  176. /**
  177. * Round all taxes to precision (4DP) before passing them back. Note, this is not the same rounding
  178. * as in the cart calculation class which, depending on settings, will round to 2DP when calculating
  179. * final totals. Also unlike that class, this rounds .5 up for all cases.
  180. */
  181. $taxes = array_map( array( __CLASS__, 'round' ), $taxes );
  182. return $taxes;
  183. }
  184. /**
  185. * Searches for all matching country/state/postcode tax rates.
  186. *
  187. * @param array $args Args that determine the rate to find.
  188. * @return array
  189. */
  190. public static function find_rates( $args = array() ) {
  191. $args = wp_parse_args(
  192. $args,
  193. array(
  194. 'country' => '',
  195. 'state' => '',
  196. 'city' => '',
  197. 'postcode' => '',
  198. 'tax_class' => '',
  199. )
  200. );
  201. $country = $args['country'];
  202. $state = $args['state'];
  203. $city = $args['city'];
  204. $postcode = wc_normalize_postcode( wc_clean( $args['postcode'] ) );
  205. $tax_class = $args['tax_class'];
  206. if ( ! $country ) {
  207. return array();
  208. }
  209. $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 ) );
  210. $matched_tax_rates = wp_cache_get( $cache_key, 'taxes' );
  211. if ( false === $matched_tax_rates ) {
  212. $matched_tax_rates = self::get_matched_tax_rates( $country, $state, $postcode, $city, $tax_class );
  213. wp_cache_set( $cache_key, $matched_tax_rates, 'taxes' );
  214. }
  215. return apply_filters( 'woocommerce_find_rates', $matched_tax_rates, $args );
  216. }
  217. /**
  218. * Searches for all matching country/state/postcode tax rates.
  219. *
  220. * @param array $args Args that determine the rate to find.
  221. * @return array
  222. */
  223. public static function find_shipping_rates( $args = array() ) {
  224. $rates = self::find_rates( $args );
  225. $shipping_rates = array();
  226. if ( is_array( $rates ) ) {
  227. foreach ( $rates as $key => $rate ) {
  228. if ( 'yes' === $rate['shipping'] ) {
  229. $shipping_rates[ $key ] = $rate;
  230. }
  231. }
  232. }
  233. return $shipping_rates;
  234. }
  235. /**
  236. * Does the sort comparison. Compares (in this order):
  237. * - Priority
  238. * - Country
  239. * - State
  240. * - Number of postcodes
  241. * - Number of cities
  242. * - ID
  243. *
  244. * @param object $rate1 First rate to compare.
  245. * @param object $rate2 Second rate to compare.
  246. * @return int
  247. */
  248. private static function sort_rates_callback( $rate1, $rate2 ) {
  249. if ( $rate1->tax_rate_priority !== $rate2->tax_rate_priority ) {
  250. return $rate1->tax_rate_priority < $rate2->tax_rate_priority ? -1 : 1; // ASC.
  251. }
  252. if ( $rate1->tax_rate_country !== $rate2->tax_rate_country ) {
  253. if ( '' === $rate1->tax_rate_country ) {
  254. return 1;
  255. }
  256. if ( '' === $rate2->tax_rate_country ) {
  257. return -1;
  258. }
  259. return strcmp( $rate1->tax_rate_country, $rate2->tax_rate_country ) > 0 ? 1 : -1;
  260. }
  261. if ( $rate1->tax_rate_state !== $rate2->tax_rate_state ) {
  262. if ( '' === $rate1->tax_rate_state ) {
  263. return 1;
  264. }
  265. if ( '' === $rate2->tax_rate_state ) {
  266. return -1;
  267. }
  268. return strcmp( $rate1->tax_rate_state, $rate2->tax_rate_state ) > 0 ? 1 : -1;
  269. }
  270. if ( isset( $rate1->postcode_count, $rate2->postcode_count ) && $rate1->postcode_count !== $rate2->postcode_count ) {
  271. return $rate1->postcode_count < $rate2->postcode_count ? 1 : -1;
  272. }
  273. if ( isset( $rate1->city_count, $rate2->city_count ) && $rate1->city_count !== $rate2->city_count ) {
  274. return $rate1->city_count < $rate2->city_count ? 1 : -1;
  275. }
  276. return $rate1->tax_rate_id < $rate2->tax_rate_id ? -1 : 1;
  277. }
  278. /**
  279. * Logical sort order for tax rates based on the following in order of priority.
  280. *
  281. * @param array $rates Rates to be sorted.
  282. * @return array
  283. */
  284. private static function sort_rates( $rates ) {
  285. uasort( $rates, __CLASS__ . '::sort_rates_callback' );
  286. $i = 0;
  287. foreach ( $rates as $key => $rate ) {
  288. $rates[ $key ]->tax_rate_order = $i++;
  289. }
  290. return $rates;
  291. }
  292. /**
  293. * Loop through a set of tax rates and get the matching rates (1 per priority).
  294. *
  295. * @param string $country Country code to match against.
  296. * @param string $state State code to match against.
  297. * @param string $postcode Postcode to match against.
  298. * @param string $city City to match against.
  299. * @param string $tax_class Tax class to match against.
  300. * @return array
  301. */
  302. private static function get_matched_tax_rates( $country, $state, $postcode, $city, $tax_class ) {
  303. global $wpdb;
  304. // Query criteria - these will be ANDed.
  305. $criteria = array();
  306. $criteria[] = $wpdb->prepare( "tax_rate_country IN ( %s, '' )", strtoupper( $country ) );
  307. $criteria[] = $wpdb->prepare( "tax_rate_state IN ( %s, '' )", strtoupper( $state ) );
  308. $criteria[] = $wpdb->prepare( 'tax_rate_class = %s', sanitize_title( $tax_class ) );
  309. // Pre-query postcode ranges for PHP based matching.
  310. $postcode_search = wc_get_wildcard_postcodes( $postcode, $country );
  311. $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 '%...%';" );
  312. if ( $postcode_ranges ) {
  313. $matches = wc_postcode_location_matcher( $postcode, $postcode_ranges, 'tax_rate_id', 'location_code', $country );
  314. if ( ! empty( $matches ) ) {
  315. foreach ( $matches as $matched_postcodes ) {
  316. $postcode_search = array_merge( $postcode_search, $matched_postcodes );
  317. }
  318. }
  319. }
  320. $postcode_search = array_unique( $postcode_search );
  321. /**
  322. * Location matching criteria - ORed
  323. * Needs to match:
  324. * - rates with no postcodes and cities
  325. * - rates with a matching postcode and city
  326. * - rates with matching postcode, no city
  327. * - rates with matching city, no postcode
  328. */
  329. $locations_criteria = array();
  330. $locations_criteria[] = 'locations.location_type IS NULL';
  331. $locations_criteria[] = "
  332. locations.location_type = 'postcode' AND locations.location_code IN ('" . implode( "','", array_map( 'esc_sql', $postcode_search ) ) . "')
  333. AND (
  334. ( locations2.location_type = 'city' AND locations2.location_code = '" . esc_sql( strtoupper( $city ) ) . "' )
  335. OR NOT EXISTS (
  336. SELECT sub.tax_rate_id FROM {$wpdb->prefix}woocommerce_tax_rate_locations as sub
  337. WHERE sub.location_type = 'city'
  338. AND sub.tax_rate_id = tax_rates.tax_rate_id
  339. )
  340. )
  341. ";
  342. $locations_criteria[] = "
  343. locations.location_type = 'city' AND locations.location_code = '" . esc_sql( strtoupper( $city ) ) . "'
  344. AND NOT EXISTS (
  345. SELECT sub.tax_rate_id FROM {$wpdb->prefix}woocommerce_tax_rate_locations as sub
  346. WHERE sub.location_type = 'postcode'
  347. AND sub.tax_rate_id = tax_rates.tax_rate_id
  348. )
  349. ";
  350. $criteria[] = '( ( ' . implode( ' ) OR ( ', $locations_criteria ) . ' ) )';
  351. $criteria_string = implode( ' AND ', $criteria );
  352. // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
  353. $found_rates = $wpdb->get_results(
  354. "
  355. SELECT tax_rates.*, COUNT( locations.location_id ) as postcode_count, COUNT( locations2.location_id ) as city_count
  356. FROM {$wpdb->prefix}woocommerce_tax_rates as tax_rates
  357. LEFT OUTER JOIN {$wpdb->prefix}woocommerce_tax_rate_locations as locations ON tax_rates.tax_rate_id = locations.tax_rate_id
  358. LEFT OUTER JOIN {$wpdb->prefix}woocommerce_tax_rate_locations as locations2 ON tax_rates.tax_rate_id = locations2.tax_rate_id
  359. WHERE 1=1 AND {$criteria_string}
  360. GROUP BY tax_rates.tax_rate_id
  361. ORDER BY tax_rates.tax_rate_priority
  362. "
  363. );
  364. // phpcs:enable
  365. $found_rates = self::sort_rates( $found_rates );
  366. $matched_tax_rates = array();
  367. $found_priority = array();
  368. foreach ( $found_rates as $found_rate ) {
  369. if ( in_array( $found_rate->tax_rate_priority, $found_priority, true ) ) {
  370. continue;
  371. }
  372. $matched_tax_rates[ $found_rate->tax_rate_id ] = array(
  373. 'rate' => (float) $found_rate->tax_rate,
  374. 'label' => $found_rate->tax_rate_name,
  375. 'shipping' => $found_rate->tax_rate_shipping ? 'yes' : 'no',
  376. 'compound' => $found_rate->tax_rate_compound ? 'yes' : 'no',
  377. );
  378. $found_priority[] = $found_rate->tax_rate_priority;
  379. }
  380. return apply_filters( 'woocommerce_matched_tax_rates', $matched_tax_rates, $country, $state, $postcode, $city, $tax_class );
  381. }
  382. /**
  383. * Get the customer tax location based on their status and the current page.
  384. *
  385. * Used by get_rates(), get_shipping_rates().
  386. *
  387. * @param string $tax_class string Optional, passed to the filter for advanced tax setups.
  388. * @param object $customer Override the customer object to get their location.
  389. * @return array
  390. */
  391. public static function get_tax_location( $tax_class = '', $customer = null ) {
  392. $location = array();
  393. if ( is_null( $customer ) && WC()->customer ) {
  394. $customer = WC()->customer;
  395. }
  396. if ( ! empty( $customer ) ) {
  397. $location = $customer->get_taxable_address();
  398. } elseif ( wc_prices_include_tax() || 'base' === get_option( 'woocommerce_default_customer_address' ) || 'base' === get_option( 'woocommerce_tax_based_on' ) ) {
  399. $location = array(
  400. WC()->countries->get_base_country(),
  401. WC()->countries->get_base_state(),
  402. WC()->countries->get_base_postcode(),
  403. WC()->countries->get_base_city(),
  404. );
  405. }
  406. return apply_filters( 'woocommerce_get_tax_location', $location, $tax_class, $customer );
  407. }
  408. /**
  409. * Get's an array of matching rates for a tax class.
  410. *
  411. * @param string $tax_class Tax class to get rates for.
  412. * @param object $customer Override the customer object to get their location.
  413. * @return array
  414. */
  415. public static function get_rates( $tax_class = '', $customer = null ) {
  416. $tax_class = sanitize_title( $tax_class );
  417. $location = self::get_tax_location( $tax_class, $customer );
  418. return self::get_rates_from_location( $tax_class, $location, $customer );
  419. }
  420. /**
  421. * Get's an arrau of matching rates from location and tax class. $customer parameter is used to preserve backward compatibility for filter.
  422. *
  423. * @param string $tax_class Tax class to get rates for.
  424. * @param array $location Location to compute rates for. Should be in form: array( country, state, postcode, city).
  425. * @param object $customer Only used to maintain backward compatibility for filter `woocommerce-matched_rates`.
  426. *
  427. * @return mixed|void Tax rates.
  428. */
  429. public static function get_rates_from_location( $tax_class, $location, $customer = null ) {
  430. $tax_class = sanitize_title( $tax_class );
  431. $matched_tax_rates = array();
  432. if ( count( $location ) === 4 ) {
  433. list( $country, $state, $postcode, $city ) = $location;
  434. $matched_tax_rates = self::find_rates(
  435. array(
  436. 'country' => $country,
  437. 'state' => $state,
  438. 'postcode' => $postcode,
  439. 'city' => $city,
  440. 'tax_class' => $tax_class,
  441. )
  442. );
  443. }
  444. return apply_filters( 'woocommerce_matched_rates', $matched_tax_rates, $tax_class, $customer );
  445. }
  446. /**
  447. * Get's an array of matching rates for the shop's base country.
  448. *
  449. * @param string $tax_class Tax Class.
  450. * @return array
  451. */
  452. public static function get_base_tax_rates( $tax_class = '' ) {
  453. return apply_filters(
  454. 'woocommerce_base_tax_rates',
  455. self::find_rates(
  456. array(
  457. 'country' => WC()->countries->get_base_country(),
  458. 'state' => WC()->countries->get_base_state(),
  459. 'postcode' => WC()->countries->get_base_postcode(),
  460. 'city' => WC()->countries->get_base_city(),
  461. 'tax_class' => $tax_class,
  462. )
  463. ),
  464. $tax_class
  465. );
  466. }
  467. /**
  468. * Alias for get_base_tax_rates().
  469. *
  470. * @deprecated 2.3
  471. * @param string $tax_class Tax Class.
  472. * @return array
  473. */
  474. public static function get_shop_base_rate( $tax_class = '' ) {
  475. return self::get_base_tax_rates( $tax_class );
  476. }
  477. /**
  478. * Gets an array of matching shipping tax rates for a given class.
  479. *
  480. * @param string $tax_class Tax class to get rates for.
  481. * @param object $customer Override the customer object to get their location.
  482. * @return mixed
  483. */
  484. public static function get_shipping_tax_rates( $tax_class = null, $customer = null ) {
  485. // See if we have an explicitly set shipping tax class.
  486. $shipping_tax_class = get_option( 'woocommerce_shipping_tax_class' );
  487. if ( 'inherit' !== $shipping_tax_class ) {
  488. $tax_class = $shipping_tax_class;
  489. }
  490. $location = self::get_tax_location( $tax_class, $customer );
  491. $matched_tax_rates = array();
  492. if ( 4 === count( $location ) ) {
  493. list( $country, $state, $postcode, $city ) = $location;
  494. if ( ! is_null( $tax_class ) ) {
  495. // This will be per item shipping.
  496. $matched_tax_rates = self::find_shipping_rates(
  497. array(
  498. 'country' => $country,
  499. 'state' => $state,
  500. 'postcode' => $postcode,
  501. 'city' => $city,
  502. 'tax_class' => $tax_class,
  503. )
  504. );
  505. } elseif ( WC()->cart->get_cart() ) {
  506. // This will be per order shipping - loop through the order and find the highest tax class rate.
  507. $cart_tax_classes = WC()->cart->get_cart_item_tax_classes_for_shipping();
  508. // No tax classes = no taxable items.
  509. if ( empty( $cart_tax_classes ) ) {
  510. return array();
  511. }
  512. // 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.
  513. if ( count( $cart_tax_classes ) > 1 && ! in_array( '', $cart_tax_classes, true ) ) {
  514. $tax_classes = self::get_tax_class_slugs();
  515. foreach ( $tax_classes as $tax_class ) {
  516. if ( in_array( $tax_class, $cart_tax_classes, true ) ) {
  517. $matched_tax_rates = self::find_shipping_rates(
  518. array(
  519. 'country' => $country,
  520. 'state' => $state,
  521. 'postcode' => $postcode,
  522. 'city' => $city,
  523. 'tax_class' => $tax_class,
  524. )
  525. );
  526. break;
  527. }
  528. }
  529. } elseif ( 1 === count( $cart_tax_classes ) ) {
  530. // If a single tax class is found, use it.
  531. $matched_tax_rates = self::find_shipping_rates(
  532. array(
  533. 'country' => $country,
  534. 'state' => $state,
  535. 'postcode' => $postcode,
  536. 'city' => $city,
  537. 'tax_class' => $cart_tax_classes[0],
  538. )
  539. );
  540. }
  541. }
  542. // Get standard rate if no taxes were found.
  543. if ( ! count( $matched_tax_rates ) ) {
  544. $matched_tax_rates = self::find_shipping_rates(
  545. array(
  546. 'country' => $country,
  547. 'state' => $state,
  548. 'postcode' => $postcode,
  549. 'city' => $city,
  550. )
  551. );
  552. }
  553. }
  554. return $matched_tax_rates;
  555. }
  556. /**
  557. * Return true/false depending on if a rate is a compound rate.
  558. *
  559. * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format.
  560. * @return bool
  561. */
  562. public static function is_compound( $key_or_rate ) {
  563. global $wpdb;
  564. if ( is_object( $key_or_rate ) ) {
  565. $key = $key_or_rate->tax_rate_id;
  566. $compound = $key_or_rate->tax_rate_compound;
  567. } else {
  568. $key = $key_or_rate;
  569. $compound = (bool) $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate_compound FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) );
  570. }
  571. return (bool) apply_filters( 'woocommerce_rate_compound', $compound, $key );
  572. }
  573. /**
  574. * Return a given rates label.
  575. *
  576. * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format.
  577. * @return string
  578. */
  579. public static function get_rate_label( $key_or_rate ) {
  580. global $wpdb;
  581. if ( is_object( $key_or_rate ) ) {
  582. $key = $key_or_rate->tax_rate_id;
  583. $rate_name = $key_or_rate->tax_rate_name;
  584. } else {
  585. $key = $key_or_rate;
  586. $rate_name = $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate_name FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) );
  587. }
  588. if ( ! $rate_name ) {
  589. $rate_name = WC()->countries->tax_or_vat();
  590. }
  591. return apply_filters( 'woocommerce_rate_label', $rate_name, $key );
  592. }
  593. /**
  594. * Return a given rates percent.
  595. *
  596. * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format.
  597. * @return string
  598. */
  599. public static function get_rate_percent( $key_or_rate ) {
  600. $rate_percent_value = self::get_rate_percent_value( $key_or_rate );
  601. $tax_rate_id = is_object( $key_or_rate ) ? $key_or_rate->tax_rate_id : $key_or_rate;
  602. return apply_filters( 'woocommerce_rate_percent', $rate_percent_value . '%', $tax_rate_id );
  603. }
  604. /**
  605. * Return a given rates percent.
  606. *
  607. * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format.
  608. * @return float
  609. */
  610. public static function get_rate_percent_value( $key_or_rate ) {
  611. global $wpdb;
  612. if ( is_object( $key_or_rate ) ) {
  613. $tax_rate = $key_or_rate->tax_rate;
  614. } else {
  615. $key = $key_or_rate;
  616. $tax_rate = $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) );
  617. }
  618. return floatval( $tax_rate );
  619. }
  620. /**
  621. * Get a rates code. Code is made up of COUNTRY-STATE-NAME-Priority. E.g GB-VAT-1, US-AL-TAX-1.
  622. *
  623. * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format.
  624. * @return string
  625. */
  626. public static function get_rate_code( $key_or_rate ) {
  627. global $wpdb;
  628. if ( is_object( $key_or_rate ) ) {
  629. $key = $key_or_rate->tax_rate_id;
  630. $rate = $key_or_rate;
  631. } else {
  632. $key = $key_or_rate;
  633. $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 ) );
  634. }
  635. $code_string = '';
  636. if ( null !== $rate ) {
  637. $code = array();
  638. $code[] = $rate->tax_rate_country;
  639. $code[] = $rate->tax_rate_state;
  640. $code[] = $rate->tax_rate_name ? $rate->tax_rate_name : 'TAX';
  641. $code[] = absint( $rate->tax_rate_priority );
  642. $code_string = strtoupper( implode( '-', array_filter( $code ) ) );
  643. }
  644. return apply_filters( 'woocommerce_rate_code', $code_string, $key );
  645. }
  646. /**
  647. * Sums a set of taxes to form a single total. Values are pre-rounded to precision from 3.6.0.
  648. *
  649. * @param array $taxes Array of taxes.
  650. * @return float
  651. */
  652. public static function get_tax_total( $taxes ) {
  653. return array_sum( $taxes );
  654. }
  655. /**
  656. * Gets all tax rate classes from the database.
  657. *
  658. * @since 3.7.0
  659. * @return array Array of tax class objects consisting of tax_rate_class_id, name, and slug.
  660. */
  661. public static function get_tax_rate_classes() {
  662. global $wpdb;
  663. $cache_key = 'tax-rate-classes';
  664. $tax_rate_classes = wp_cache_get( $cache_key, 'taxes' );
  665. if ( ! is_array( $tax_rate_classes ) ) {
  666. $tax_rate_classes = $wpdb->get_results(
  667. "
  668. SELECT * FROM {$wpdb->wc_tax_rate_classes} ORDER BY name;
  669. "
  670. );
  671. wp_cache_set( $cache_key, $tax_rate_classes, 'taxes' );
  672. }
  673. return $tax_rate_classes;
  674. }
  675. /**
  676. * Get store tax class names.
  677. *
  678. * @return array Array of class names ("Reduced rate", "Zero rate", etc).
  679. */
  680. public static function get_tax_classes() {
  681. return wp_list_pluck( self::get_tax_rate_classes(), 'name' );
  682. }
  683. /**
  684. * Get store tax classes as slugs.
  685. *
  686. * @since 3.0.0
  687. * @return array Array of class slugs ("reduced-rate", "zero-rate", etc).
  688. */
  689. public static function get_tax_class_slugs() {
  690. return wp_list_pluck( self::get_tax_rate_classes(), 'slug' );
  691. }
  692. /**
  693. * Create a new tax class.
  694. *
  695. * @since 3.7.0
  696. * @param string $name Name of the tax class to add.
  697. * @param string $slug (optional) Slug of the tax class to add. Defaults to sanitized name.
  698. * @return WP_Error|array Returns name and slug (array) if the tax class is created, or WP_Error if something went wrong.
  699. */
  700. public static function create_tax_class( $name, $slug = '' ) {
  701. global $wpdb;
  702. if ( empty( $name ) ) {
  703. return new WP_Error( 'tax_class_invalid_name', __( 'Tax class requires a valid name', 'woocommerce' ) );
  704. }
  705. $existing = self::get_tax_classes();
  706. $existing_slugs = self::get_tax_class_slugs();
  707. $name = wc_clean( $name );
  708. if ( in_array( $name, $existing, true ) ) {
  709. return new WP_Error( 'tax_class_exists', __( 'Tax class already exists', 'woocommerce' ) );
  710. }
  711. if ( ! $slug ) {
  712. $slug = sanitize_title( $name );
  713. }
  714. // Stop if there's no slug.
  715. if ( ! $slug ) {
  716. return new WP_Error( 'tax_class_slug_invalid', __( 'Tax class slug is invalid', 'woocommerce' ) );
  717. }
  718. if ( in_array( $slug, $existing_slugs, true ) ) {
  719. return new WP_Error( 'tax_class_slug_exists', __( 'Tax class slug already exists', 'woocommerce' ) );
  720. }
  721. $insert = $wpdb->insert(
  722. $wpdb->wc_tax_rate_classes,
  723. array(
  724. 'name' => $name,
  725. 'slug' => $slug,
  726. )
  727. );
  728. if ( is_wp_error( $insert ) ) {
  729. return new WP_Error( 'tax_class_insert_error', $insert->get_error_message() );
  730. }
  731. wp_cache_delete( 'tax-rate-classes', 'taxes' );
  732. return array(
  733. 'name' => $name,
  734. 'slug' => $slug,
  735. );
  736. }
  737. /**
  738. * Get an existing tax class.
  739. *
  740. * @since 3.7.0
  741. * @param string $field Field to get by. Valid values are id, name, or slug.
  742. * @param string|int $item Item to get.
  743. * @return array|bool Returns the tax class as an array. False if not found.
  744. */
  745. public static function get_tax_class_by( $field, $item ) {
  746. if ( ! in_array( $field, array( 'id', 'name', 'slug' ), true ) ) {
  747. return new WP_Error( 'invalid_field', __( 'Invalid field', 'woocommerce' ) );
  748. }
  749. if ( 'id' === $field ) {
  750. $field = 'tax_rate_class_id';
  751. }
  752. $matches = wp_list_filter(
  753. self::get_tax_rate_classes(),
  754. array(
  755. $field => $item,
  756. )
  757. );
  758. if ( ! $matches ) {
  759. return false;
  760. }
  761. $tax_class = current( $matches );
  762. return array(
  763. 'name' => $tax_class->name,
  764. 'slug' => $tax_class->slug,
  765. );
  766. }
  767. /**
  768. * Delete an existing tax class.
  769. *
  770. * @since 3.7.0
  771. * @param string $field Field to delete by. Valid values are id, name, or slug.
  772. * @param string|int $item Item to delete.
  773. * @return WP_Error|bool Returns true if deleted successfully, false if nothing was deleted, or WP_Error if there is an invalid request.
  774. */
  775. public static function delete_tax_class_by( $field, $item ) {
  776. global $wpdb;
  777. if ( ! in_array( $field, array( 'id', 'name', 'slug' ), true ) ) {
  778. return new WP_Error( 'invalid_field', __( 'Invalid field', 'woocommerce' ) );
  779. }
  780. $tax_class = self::get_tax_class_by( $field, $item );
  781. if ( ! $tax_class ) {
  782. return new WP_Error( 'invalid_tax_class', __( 'Invalid tax class', 'woocommerce' ) );
  783. }
  784. if ( 'id' === $field ) {
  785. $field = 'tax_rate_class_id';
  786. }
  787. $delete = $wpdb->delete(
  788. $wpdb->wc_tax_rate_classes,
  789. array(
  790. $field => $item,
  791. )
  792. );
  793. if ( $delete ) {
  794. // Delete associated tax rates.
  795. $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_class = %s;", $tax_class['slug'] ) );
  796. $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;" );
  797. }
  798. wp_cache_delete( 'tax-rate-classes', 'taxes' );
  799. WC_Cache_Helper::invalidate_cache_group( 'taxes' );
  800. return (bool) $delete;
  801. }
  802. /**
  803. * Format the city.
  804. *
  805. * @param string $city Value to format.
  806. * @return string
  807. */
  808. private static function format_tax_rate_city( $city ) {
  809. return strtoupper( trim( $city ) );
  810. }
  811. /**
  812. * Format the state.
  813. *
  814. * @param string $state Value to format.
  815. * @return string
  816. */
  817. private static function format_tax_rate_state( $state ) {
  818. $state = strtoupper( $state );
  819. return ( '*' === $state ) ? '' : $state;
  820. }
  821. /**
  822. * Format the country.
  823. *
  824. * @param string $country Value to format.
  825. * @return string
  826. */
  827. private static function format_tax_rate_country( $country ) {
  828. $country = strtoupper( $country );
  829. return ( '*' === $country ) ? '' : $country;
  830. }
  831. /**
  832. * Format the tax rate name.
  833. *
  834. * @param string $name Value to format.
  835. * @return string
  836. */
  837. private static function format_tax_rate_name( $name ) {
  838. return $name ? $name : __( 'Tax', 'woocommerce' );
  839. }
  840. /**
  841. * Format the rate.
  842. *
  843. * @param float $rate Value to format.
  844. * @return string
  845. */
  846. private static function format_tax_rate( $rate ) {
  847. return number_format( (float) $rate, 4, '.', '' );
  848. }
  849. /**
  850. * Format the priority.
  851. *
  852. * @param string $priority Value to format.
  853. * @return int
  854. */
  855. private static function format_tax_rate_priority( $priority ) {
  856. return absint( $priority );
  857. }
  858. /**
  859. * Format the class.
  860. *
  861. * @param string $class Value to format.
  862. * @return string
  863. */
  864. public static function format_tax_rate_class( $class ) {
  865. $class = sanitize_title( $class );
  866. $classes = self::get_tax_class_slugs();
  867. if ( ! in_array( $class, $classes, true ) ) {
  868. $class = '';
  869. }
  870. return ( 'standard' === $class ) ? '' : $class;
  871. }
  872. /**
  873. * Prepare and format tax rate for DB insertion.
  874. *
  875. * @param array $tax_rate Tax rate to format.
  876. * @return array
  877. */
  878. private static function prepare_tax_rate( $tax_rate ) {
  879. foreach ( $tax_rate as $key => $value ) {
  880. if ( method_exists( __CLASS__, 'format_' . $key ) ) {
  881. if ( 'tax_rate_state' === $key ) {
  882. $tax_rate[ $key ] = call_user_func( array( __CLASS__, 'format_' . $key ), sanitize_key( $value ) );
  883. } else {
  884. $tax_rate[ $key ] = call_user_func( array( __CLASS__, 'format_' . $key ), $value );
  885. }
  886. }
  887. }
  888. return $tax_rate;
  889. }
  890. /**
  891. * Insert a new tax rate.
  892. *
  893. * Internal use only.
  894. *
  895. * @since 2.3.0
  896. *
  897. * @param array $tax_rate Tax rate to insert.
  898. * @return int tax rate id
  899. */
  900. public static function _insert_tax_rate( $tax_rate ) {
  901. global $wpdb;
  902. $wpdb->insert( $wpdb->prefix . 'woocommerce_tax_rates', self::prepare_tax_rate( $tax_rate ) );
  903. $tax_rate_id = $wpdb->insert_id;
  904. WC_Cache_Helper::invalidate_cache_group( 'taxes' );
  905. do_action( 'woocommerce_tax_rate_added', $tax_rate_id, $tax_rate );
  906. return $tax_rate_id;
  907. }
  908. /**
  909. * Get tax rate.
  910. *
  911. * Internal use only.
  912. *
  913. * @since 2.5.0
  914. *
  915. * @param int $tax_rate_id Tax rate ID.
  916. * @param string $output_type Type of output.
  917. * @return array|object
  918. */
  919. public static function _get_tax_rate( $tax_rate_id, $output_type = ARRAY_A ) {
  920. global $wpdb;
  921. return $wpdb->get_row(
  922. $wpdb->prepare(
  923. "
  924. SELECT *
  925. FROM {$wpdb->prefix}woocommerce_tax_rates
  926. WHERE tax_rate_id = %d
  927. ",
  928. $tax_rate_id
  929. ),
  930. $output_type
  931. );
  932. }
  933. /**
  934. * Update a tax rate.
  935. *
  936. * Internal use only.
  937. *
  938. * @since 2.3.0
  939. *
  940. * @param int $tax_rate_id Tax rate to update.
  941. * @param array $tax_rate Tax rate values.
  942. */
  943. public static function _update_tax_rate( $tax_rate_id, $tax_rate ) {
  944. global $wpdb;
  945. $tax_rate_id = absint( $tax_rate_id );
  946. $wpdb->update(
  947. $wpdb->prefix . 'woocommerce_tax_rates',
  948. self::prepare_tax_rate( $tax_rate ),
  949. array(
  950. 'tax_rate_id' => $tax_rate_id,
  951. )
  952. );
  953. WC_Cache_Helper::invalidate_cache_group( 'taxes' );
  954. do_action( 'woocommerce_tax_rate_updated', $tax_rate_id, $tax_rate );
  955. }
  956. /**
  957. * Delete a tax rate from the database.
  958. *
  959. * Internal use only.
  960. *
  961. * @since 2.3.0
  962. * @param int $tax_rate_id Tax rate to delete.
  963. */
  964. public static function _delete_tax_rate( $tax_rate_id ) {
  965. global $wpdb;
  966. $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE tax_rate_id = %d;", $tax_rate_id ) );
  967. $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %d;", $tax_rate_id ) );
  968. WC_Cache_Helper::invalidate_cache_group( 'taxes' );
  969. do_action( 'woocommerce_tax_rate_deleted', $tax_rate_id );
  970. }
  971. /**
  972. * Update postcodes for a tax rate in the DB.
  973. *
  974. * Internal use only.
  975. *
  976. * @since 2.3.0
  977. *
  978. * @param int $tax_rate_id Tax rate to update.
  979. * @param string $postcodes String of postcodes separated by ; characters.
  980. */
  981. public static function _update_tax_rate_postcodes( $tax_rate_id, $postcodes ) {
  982. if ( ! is_array( $postcodes ) ) {
  983. $postcodes = explode( ';', $postcodes );
  984. }
  985. // No normalization - postcodes are matched against both normal and formatted versions to support wildcards.
  986. foreach ( $postcodes as $key => $postcode ) {
  987. $postcodes[ $key ] = strtoupper( trim( str_replace( chr( 226 ) . chr( 128 ) . chr( 166 ), '...', $postcode ) ) );
  988. }
  989. self::update_tax_rate_locations( $tax_rate_id, array_diff( array_filter( $postcodes ), array( '*' ) ), 'postcode' );
  990. }
  991. /**
  992. * Update cities for a tax rate in the DB.
  993. *
  994. * Internal use only.
  995. *
  996. * @since 2.3.0
  997. *
  998. * @param int $tax_rate_id Tax rate to update.
  999. * @param string $cities Cities to set.
  1000. */
  1001. public static function _update_tax_rate_cities( $tax_rate_id, $cities ) {
  1002. if ( ! is_array( $cities ) ) {
  1003. $cities = explode( ';', $cities );
  1004. }
  1005. $cities = array_filter( array_diff( array_map( array( __CLASS__, 'format_tax_rate_city' ), $cities ), array( '*' ) ) );
  1006. self::update_tax_rate_locations( $tax_rate_id, $cities, 'city' );
  1007. }
  1008. /**
  1009. * Updates locations (postcode and city).
  1010. *
  1011. * Internal use only.
  1012. *
  1013. * @since 2.3.0
  1014. *
  1015. * @param int $tax_rate_id Tax rate ID to update.
  1016. * @param array $values Values to set.
  1017. * @param string $type Location type.
  1018. */
  1019. private static function update_tax_rate_locations( $tax_rate_id, $values, $type ) {
  1020. global $wpdb;
  1021. $tax_rate_id = absint( $tax_rate_id );
  1022. $wpdb->query(
  1023. $wpdb->prepare(
  1024. "DELETE FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE tax_rate_id = %d AND location_type = %s;",
  1025. $tax_rate_id,
  1026. $type
  1027. )
  1028. );
  1029. if ( count( $values ) > 0 ) {
  1030. $sql = "( '" . implode( "', $tax_rate_id, '" . esc_sql( $type ) . "' ),( '", array_map( 'esc_sql', $values ) ) . "', $tax_rate_id, '" . esc_sql( $type ) . "' )";
  1031. $wpdb->query( "INSERT INTO {$wpdb->prefix}woocommerce_tax_rate_locations ( location_code, tax_rate_id, location_type ) VALUES $sql;" ); // @codingStandardsIgnoreLine.
  1032. }
  1033. WC_Cache_Helper::invalidate_cache_group( 'taxes' );
  1034. }
  1035. /**
  1036. * Used by admin settings page.
  1037. *
  1038. * @param string $tax_class Tax class slug.
  1039. *
  1040. * @return array|null|object
  1041. */
  1042. public static function get_rates_for_tax_class( $tax_class ) {
  1043. global $wpdb;
  1044. $tax_class = self::format_tax_rate_class( $tax_class );
  1045. // Get all the rates and locations. Snagging all at once should significantly cut down on the number of queries.
  1046. $rates = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM `{$wpdb->prefix}woocommerce_tax_rates` WHERE `tax_rate_class` = %s;", $tax_class ) );
  1047. $locations = $wpdb->get_results( "SELECT * FROM `{$wpdb->prefix}woocommerce_tax_rate_locations`" );
  1048. if ( ! empty( $rates ) ) {
  1049. // Set the rates keys equal to their ids.
  1050. $rates = array_combine( wp_list_pluck( $rates, 'tax_rate_id' ), $rates );
  1051. }
  1052. // Drop the locations into the rates array.
  1053. foreach ( $locations as $location ) {
  1054. // Don't set them for unexistent rates.
  1055. if ( ! isset( $rates[ $location->tax_rate_id ] ) ) {
  1056. continue;
  1057. }
  1058. // If the rate exists, initialize the array before appending to it.
  1059. if ( ! isset( $rates[ $location->tax_rate_id ]->{$location->location_type} ) ) {
  1060. $rates[ $location->tax_rate_id ]->{$location->location_type} = array();
  1061. }
  1062. $rates[ $location->tax_rate_id ]->{$location->location_type}[] = $location->location_code;
  1063. }
  1064. foreach ( $rates as $rate_id => $rate ) {
  1065. $rates[ $rate_id ]->postcode_count = isset( $rates[ $rate_id ]->postcode ) ? count( $rates[ $rate_id ]->postcode ) : 0;
  1066. $rates[ $rate_id ]->city_count = isset( $rates[ $rate_id ]->city ) ? count( $rates[ $rate_id ]->city ) : 0;
  1067. }
  1068. $rates = self::sort_rates( $rates );
  1069. return $rates;
  1070. }
  1071. }
  1072. WC_Tax::init();