<?php
/**
 * Calculate Tax service
 * Makes Calculate Tax API calls, validations, database and WordPress object cache store
 *
 * @package Stripe\StripeTaxForWooCommerce\Stripe
 */

namespace Stripe\StripeTaxForWooCommerce\Stripe;

defined( 'ABSPATH' ) || exit;

use Exception;
use Stripe\StripeTaxForWooCommerce\SDK\lib\Tax\Calculation;
use Stripe\StripeTaxForWooCommerce\WooCommerce\ExtendedProduct;
use Stripe\StripeTaxForWooCommerce\WooCommerce\StripeOrderItemTax;
use Stripe\StripeTaxForWooCommerce\WordPress\Options;
use Stripe\StripeTaxForWooCommerce\WooCommerce\TaxRate;
use Stripe\StripeTaxForWooCommerce\WooCommerce\StripeTaxTaxRateMemRepo;
use Stripe\StripeTaxForWooCommerce\Stripe\Tax_Calculation\Input;
use Stripe\StripeTaxForWooCommerce\SDK\lib\Util\Util;

use WC_Order_Item;

/**
 * Service for Stripe Calculate Tax API.
 *
 * @see https://stripe.com/docs/api/tax/calculations
 */
class CalculateTax {
	use StripeClientTrait;

	const CACHE_GROUP               = 'stripe-tax-for-woocommerce';
	const CACHE_KEY                 = 'calculate-tax';
	const TABLE_NAME                = STRIPE_TAX_FOR_WOOCOMMERCE_DB_PREFIX . 'calculate_tax';
	const CLEANUP_RETENTION_SECONDS = 2 * HOUR_IN_SECONDS;
	const CLEANUP_BATCH_SIZE        = 1500;
	const CLEANUP_COOLDOWN_SECONDS  = 1 * MINUTE_IN_SECONDS;
	const CLEANUP_TRANSIENT_KEY     = 'stripe_tax_calculate_tax_cleanup_lock';

	/**
	 * Stripe API key
	 *
	 * @var string
	 */
	protected $api_key;

	/**
	 * Prepared request to be sent on Stripe Calculate Tax API to make actual tax calculation.
	 *
	 * @see https://stripe.com/docs/api/tax/calculations/create
	 * @var array
	 */
	protected $request = array();

	/**
	 * MD5 of JSON string of Stripe Tax Calculation request array.
	 * Used to check if we are making the same request as already did (to not repeat it if calculation not expired).
	 *
	 * @var string
	 */
	protected $calculate_tax_md5 = '';

	/**
	 * MD5 of JSON string of Stripe Tax Registrations array.
	 * Used to check if Tax Registrations changed (to ensure already made tax calculation is valid if not expired)
	 *
	 * @var string
	 */
	protected $tax_registrations_md5 = '';

	/**
	 * MD5 of JSON string of Stripe Tax Settings array.
	 * Used to check if Tax Settings changed (to ensure already made tax calculation is valid if not expired)
	 *
	 * @var string
	 */
	protected $tax_settings_md5 = '';

	/**
	 * MD5 of Stripe API key string.
	 * Used to check if Stripe API key changed (to ensure already made tax calculation is valid if not expired)
	 *
	 * @var string
	 */
	protected $api_key_md5 = '';

	/**
	 * Concatenated $calculate_tax_md5 + $tax_registrations_md5 + $tax_settings_md5 + $api_key_md5.
	 * Used as key of associative array to cache Tax Calculations inside of WordPress object cache.
	 *
	 * @var string
	 */
	protected $md5_concat = '';

	/**
	 * Tax Registrations object
	 * Used to receive current Stripe Tax Registrations, to check if anything changed before creating Tax Calculation.
	 *
	 * @var TaxRegistrations
	 */
	protected $tax_registration;

	/**
	 * Tax Settings object
	 * Used to receive current Stripe Tax Settings, to check if anything changed before creating Tax Calculation.
	 *
	 * @var TaxSettings
	 */
	protected $tax_settings;

	/**
	 * Create CalculateTax service
	 *
	 * @param string       $api_key API key.
	 * @param array|object $tax_input Tax calculation input.
	 *
	 * @throws Exception If data is not valid.
	 */
	public function __construct( string $api_key, $tax_input ) {
		$this->api_key = $api_key;

		$this->request = $tax_input;

		$this->calculate_tax_md5     = md5( wp_json_encode( $this->request ) );
		$this->tax_registrations_md5 = md5( wp_json_encode( $this->get_tax_registration( $this->api_key )->get_registrations() ) );
		$this->tax_settings_md5      = md5( wp_json_encode( $this->get_tax_settings( $this->api_key )->get_settings() ) );
		$this->api_key_md5           = md5( $this->api_key );
		$this->md5_concat            = $this->calculate_tax_md5 . $this->tax_registrations_md5 . $this->tax_settings_md5 . $this->api_key_md5;
	}

	/**
	 * Factory method to create TaxRegistrations service
	 *
	 * @param string $api_key API key.
	 *
	 * @return TaxRegistrations TaxRegistrations object
	 */
	protected function get_tax_registration( string $api_key ): TaxRegistrations {
		if ( is_null( $this->tax_registration ) ) {
			$this->tax_registration = new TaxRegistrations( $api_key );
		}
		return $this->tax_registration;
	}

	/**
	 * Factory method to create TaxSettings service
	 *
	 * @param string $api_key API key.
	 *
	 * @return TaxSettings TaxSettings object
	 */
	protected function get_tax_settings( string $api_key ): TaxSettings {
		if ( is_null( $this->tax_settings ) ) {
			$this->tax_settings = new TaxSettings( $api_key );
		}
		return $this->tax_settings;
	}

	/**
	 * Get customer details by WooCommerce order or WooCommerce customer objects
	 *
	 * @param \WC_Order|\WC_Customer $wc_order WooCommerce Order.
	 *
	 * @return array Prepared customer details for Calculate Tax API call
	 */
	public static function get_customer_details_by_order( $wc_order ): array {
		/**
		 * Order
		 *
		 * @var \WC_Order|\WC_Customer $wc_order
		 */
		$shipping         = false;
		$billing          = false;
		$customer_details = array();
		$country          = $wc_order->get_shipping_country( 'edit' );
		$state            = $wc_order->get_shipping_state( 'edit' );
		$postal_code      = $wc_order->get_shipping_postcode( 'edit' );

		if ( 'US' === $country ) {
			if ( $state && $postal_code ) {
				$shipping = true;
			}
		} elseif ( 'CA' === $country ) {
			if ( $postal_code ) {
				$shipping = true;
			}
		} elseif ( $country ) {
			$shipping = true;
		}

		if ( ! $shipping ) {
			$country     = $wc_order->get_billing_country( 'edit' );
			$state       = $wc_order->get_billing_state( 'edit' );
			$postal_code = $wc_order->get_billing_postcode( 'edit' );
			if ( 'US' === $country ) {
				if ( $state && $postal_code ) {
					$billing = true;
				}
			} elseif ( 'CA' === $country ) {
				if ( $postal_code ) {
					$billing = true;
				}
			} elseif ( $country ) {
				$billing = true;
			}
		}

		if ( $shipping ) {
			$customer_details['address'] = array();
			if ( $wc_order->get_shipping_country( 'edit' ) ) {
				$customer_details['address']['country'] = $wc_order->get_shipping_country( 'edit' );
			}
			if ( $wc_order->get_shipping_state( 'edit' ) ) {
				$customer_details['address']['state'] = $wc_order->get_shipping_state( 'edit' );
			}
			if ( $wc_order->get_shipping_city( 'edit' ) ) {
				$customer_details['address']['city'] = $wc_order->get_shipping_city( 'edit' );
			}
			if ( $wc_order->get_shipping_postcode( 'edit' ) ) {
				$customer_details['address']['postal_code'] = $wc_order->get_shipping_postcode( 'edit' );
			}
			if ( $wc_order->get_shipping_address_1( 'edit' ) ) {
				$customer_details['address']['line1'] = $wc_order->get_shipping_address_1( 'edit' );
			}
			if ( $wc_order->get_shipping_address_2( 'edit' ) ) {
				$customer_details['address']['line2'] = $wc_order->get_shipping_address_2( 'edit' );
			}
			if ( $customer_details['address'] ) {
				$customer_details['address_source'] = 'shipping';
			}
		} else {
			$customer_details['address'] = array();
			if ( $wc_order->get_billing_country( 'edit' ) ) {
				$customer_details['address']['country'] = $wc_order->get_billing_country( 'edit' );
			}
			if ( $wc_order->get_billing_state( 'edit' ) ) {
				$customer_details['address']['state'] = $wc_order->get_billing_state( 'edit' );
			}
			if ( $wc_order->get_billing_city( 'edit' ) ) {
				$customer_details['address']['city'] = $wc_order->get_billing_city( 'edit' );
			}
			if ( $wc_order->get_billing_postcode( 'edit' ) ) {
				$customer_details['address']['postal_code'] = $wc_order->get_billing_postcode( 'edit' );
			}
			if ( $wc_order->get_billing_address_1( 'edit' ) ) {
				$customer_details['address']['line1'] = $wc_order->get_billing_address_1( 'edit' );
			}
			if ( $wc_order->get_billing_address_2( 'edit' ) ) {
				$customer_details['address']['line2'] = $wc_order->get_billing_address_2( 'edit' );
			}
			if ( $customer_details['address'] ) {
				$customer_details['address_source'] = 'billing';
			}
		}

		if ( ! $customer_details['address'] ) {
			return array();
		}

		return $customer_details;
	}

	/**
	 * States whether the tax can be calculated for the provided customer details.
	 *
	 * @param array $customer_details The processed customer details.
	 * @return bool
	 */
	public static function can_calculate_tax( $customer_details ) {
		if ( empty( $customer_details ) || empty( $customer_details['address'] ) ) {
			return false;
		}

		if ( 'US' === $customer_details['address']['country'] && empty( $customer_details['address']['postal_code'] ) ) {
			return false;
		}

		if ( 'CA' === $customer_details['address']['country'] && ( empty( $customer_details['address']['state'] ) || empty( $customer_details['address']['postal_code'] ) ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Get customer details by posted parameters.
	 * This part is triggered on order creation through WooCommerce admin.
	 *
	 * @return array Prepared customer details for Calculate Tax API call
	 */
	public static function get_customer_details_by_post(): array {
		try {
			return self::get_customer_details_address();
		} catch ( \Throwable $e ) {
			try {
				return self::get_customer_details_address( true );
			} catch ( \Throwable $e ) {
				return array();
			}
		}
	}

	/**
	 * Get customer address details from billing or shipping fields.
	 *
	 * @param bool $from_billing If we want to get details from billing fields.
	 * @return array Customer address details.
	 * @throws Exception If data is not correct.
	 */
	private static function get_customer_details_address( bool $from_billing = false ): array {
		if ( ! isset( $_POST['stripe_tax_for_woocommerce_customer_details_shipping_address'] ) && ! isset( $_POST['stripe_tax_for_woocommerce_customer_details_billing_address'] ) ) {
			return array();
		}

		check_admin_referer( 'calc-totals', 'security' );
		if ( ! isset( $_REQUEST['security'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['security'] ) ), 'calc-totals' ) ) {
			exit;
		}

		$customer_details = array();

		if ( $from_billing || 'disabled' === get_option( 'woocommerce_ship_to_countries' ) ) {
			$customer_details_address           = map_deep( wp_unslash( $_POST['stripe_tax_for_woocommerce_customer_details_billing_address'] ?? array() ), 'sanitize_text_field' );
			$customer_details['address_source'] = 'billing';
		} else {
			$customer_details_address           = map_deep( wp_unslash( $_POST['stripe_tax_for_woocommerce_customer_details_shipping_address'] ?? array() ), 'sanitize_text_field' );
			$customer_details['address_source'] = 'shipping';
		}

		Validate::validate_customer_details_address( $customer_details_address );
		$customer_details['address'] = $customer_details_address;
		return $customer_details;
	}

	/**
	 * Get customer id by posted parameters.
	 * This part is triggered on order creation through WooCommerce admin.
	 *
	 * @return string Customer ID or "guest"
	 */
	public static function get_customer_id_by_post(): string {
		if ( empty( $_POST['stripe_tax_for_woocommerce_customer_user'] ) || ( ! is_string( $_POST['stripe_tax_for_woocommerce_customer_user'] ) ) ) {
			return '';
		}

		check_admin_referer( 'calc-totals', 'security' );
		if ( ! isset( $_REQUEST['security'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['security'] ) ), 'calc-totals' ) ) {
			StripeTaxPluginHelper::do_exit();
		}

		return sanitize_text_field( wp_unslash( $_POST['stripe_tax_for_woocommerce_customer_user'] ) );
	}

	/**
	 * Provides tax exemption information, depending on path, how it called.
	 *
	 * @param \WC_Order $wc_order WooCommerce Order object.
	 *
	 * @return string
	 */
	public static function get_order_tax_exempt( \WC_Order $wc_order ): string {
		$tax_exemptions = new TaxExemptions();

		$customer_id = self::get_customer_id_by_post();

		switch ( $customer_id ) {
			case StripeTaxPluginHelper::GUEST_CUSTOMER_ID: // In case of 'guest', assume "none" tax exemption.
				$taxability_override = 'none';
				break;
			case '': // In case of no information about customer id, assume WC_Order object contains it.
				$taxability_override = $tax_exemptions->get_tax_exeption( $wc_order->get_customer_id( 'edit' ) );
				break;
			default: // In other cases (Customer ID received) use it to receive tax exemption information.
				$taxability_override = $tax_exemptions->get_tax_exeption( $customer_id );
		}

		return $taxability_override;
	}

	/**
	 * Get original Stripe Line Item id by Stripe/WooCommerce Line Item reference.
	 * For partial refund Stripe Reversal Transaction API call also needed to send original line item id to be refunded.
	 *
	 * @param mixed $reference Reference.
	 * @param mixed $tax_transaction Tax transaction.
	 *
	 * @return string Line Item ID for partial refund Stripe Reversal Transaction API
	 */
	public static function get_original_line_item_id( $reference, $tax_transaction ) {
		$items = $tax_transaction->line_items->data;
		foreach ( $items as $item ) {
			if ( $item->reference !== $reference ) {
				continue;
			}

			return $item->id;
		}

		return '';
	}

	/**
	 * Get line items by WooCommerce order
	 * Used to prepare line items array to be sent to Stripe Calculate Tax API or to Stripe Reversal Transaction API.
	 *
	 * @param \WC_Order|\WC_Order_Refund $wc_order WooCommerce order object.
	 * @param bool                       $for_refund If line items are preparing for refund, not for Calculate Tax.
	 * @param \stdClass|null             $tax_transaction_data Tax transaction data. Needed only for refund.
	 *
	 * @return array Line items for Calculate Tax API or for Reversal Transaction API calls
	 * @throws Exception If data is not correct.
	 */
	public static function get_line_items_by_order( $wc_order, bool $for_refund = false, ?\stdClass $tax_transaction_data = null ): array {
		$items              = $wc_order->get_items();
		$currency           = strtolower( get_woocommerce_currency() );
		$line_items         = array();
		$line_items_counter = 0;

		$order_prices_include_tax_tag = $wc_order->get_prices_include_tax();

		$order_prices_include_tax = ( true === $order_prices_include_tax_tag || 'yes' === $order_prices_include_tax_tag ) || wc_prices_include_tax();

		// In WooCommerce same item (item with the same title/reference) can be added multiple times.
		// For Stripe API, item title/reference must be unique.
		// This array used to store item's title/reference and $line_items array index of where this item stored.
		// This is needed to not add new existing item to $line_items array, but to increase quantity and cost of items.
		$items_reference_already_added = array();

		foreach ( $items as $item ) {
			/**
			 * Order Item Product
			 *
			 * @var \WC_Order_Item_Product $product
			 * @var \WC_Product $product
			 */
			// @phpstan-ignore-next-line
			$product  = $item->get_product();
			$quantity = ( $for_refund ? - $item->get_quantity() : $item->get_quantity() );
			// "float" casting used, because the WooCommerce returns product properties as values of "string" type.
			$amount                = self::get_order_item_total_amount( $item );
			$normalized_amount     = self::get_normalized_amount( $amount, $currency );
			$normalized_tax_amount = 0;
			if ( $for_refund ) {
				// @phpstan-ignore-next-line
				$tax_amount            = (float) ( $item->get_total_tax( 'edit' ) );
				$normalized_tax_amount = self::get_normalized_amount( $tax_amount, $currency );
			}
			$item_id        = ( $for_refund ? $item->get_meta( '_refunded_item_id' ) : $item->get_id() );
			$reference      = $item->get_name( 'edit' ) . ( (int) $item_id ? ( '#' . $item_id ) : '' );
			$product_id     = $product->get_id();
			$stripe_product = ( new ExtendedProduct( $product_id ) )->get_extended_product();
			if ( ! array_key_exists( $reference, $items_reference_already_added ) ) {
				$line_items[ $line_items_counter ] = array(
					'amount'    => $normalized_amount,
					'reference' => $reference,
					'quantity'  => $quantity,
				);
				if ( ! $for_refund ) {
					if ( $stripe_product['tax_code'] ) {
						$line_items[ $line_items_counter ]['tax_code'] = $stripe_product['tax_code'];
					}

					$tax_settings = new TaxSettings( Options::get_current_mode_key() );

					if ( ! $order_prices_include_tax ) {
						$line_items[ $line_items_counter ]['tax_behavior'] = 'exclusive';
					} elseif ( $item->get_meta( '_stripe_not_subtotal_include_tax' ) === '' ) {
							$line_items[ $line_items_counter ]['tax_behavior'] = 'inclusive';
					} else {
						$line_items[ $line_items_counter ]['tax_behavior'] = 'exclusive';
					}
				} else {
					$line_items[ $line_items_counter ]['original_line_item'] = self::get_original_line_item_id( $reference, $tax_transaction_data );
					$line_items[ $line_items_counter ]['amount_tax']         = $normalized_tax_amount;

				}
				$items_reference_already_added[ $reference ]            = $line_items[ $line_items_counter ];
				$items_reference_already_added[ $reference ]['counter'] = $line_items_counter;
				++$line_items_counter;
			} else {
				$cached_item                            = $items_reference_already_added[ $reference ];
				$new_counter                            = $cached_item['counter'];
				$line_items[ $new_counter ]['amount']   = $line_items[ $new_counter ]['amount'] + $normalized_amount;
				$line_items[ $new_counter ]['quantity'] = $line_items[ $new_counter ]['quantity'] + $quantity;
				if ( $for_refund ) {
					$line_items[ $new_counter ]['amount_tax'] = $line_items[ $new_counter ]['amount_tax'] + $normalized_tax_amount;
				}
				$items_reference_already_added[ $reference ]            = $line_items[ $new_counter ];
				$items_reference_already_added[ $reference ]['counter'] = $new_counter;
			}
		}

		return $line_items;
	}

	/**
	 * Prepare line items for Calculate Tax API from WooCommerce cart
	 *
	 * @param \WC_Cart $wc_cart WooCommerce Cart object.
	 *
	 * @return array Prepared line items for Calculate Tax API call
	 * @throws Exception If data is not correct.
	 */
	public static function get_line_items_by_cart( $wc_cart ): array {
		$currency           = strtolower( get_woocommerce_currency() );
		$line_items         = array();
		$line_items_counter = 0;
		// In WooCommerce same item (item with the same title/reference) can be added multiple times.
		// For Stripe API, item title/reference must be unique.
		// This array used to store item's title/reference and $line_items array index of where this item stored.
		// This is needed to not add new existing item to $line_items array, but to increase quantity and cost of items.
		$items_reference_already_added = array();
		foreach ( $wc_cart->cart_contents as $cart_items ) {
			$quantity = (int) $cart_items['quantity'];

			/**
			 * Product
			 *
			 * @var \WC_Product $product
			 */
			$product = $cart_items['data'];
			$amount  = $cart_items['line_subtotal'];

			if ( array_key_exists( 'line_total', $cart_items ) ) {
				$amount = $cart_items['line_total'];
			}

			if ( $amount < 0 ) {
				continue;
			}

			$normalized_amount = self::get_normalized_amount( $amount, $currency );
			$reference         = $product->get_name();
			$id                = $product->get_id();
			$stripe_product    = ( new ExtendedProduct( $id ) )->get_extended_product();
			if ( ! array_key_exists( $reference, $items_reference_already_added ) ) {
				$line_items[ $line_items_counter ] = array(
					'amount'    => $normalized_amount,
					'reference' => $reference,
					'quantity'  => $quantity,
				);
				if ( $stripe_product['tax_code'] ) {
					$line_items[ $line_items_counter ]['tax_code'] = $stripe_product['tax_code'];
				}

				$tax_settings                                      = new TaxSettings( Options::get_current_mode_key() );
				$line_items[ $line_items_counter ]['tax_behavior'] = $tax_settings->get_tax_behavior();
				$items_reference_already_added[ $reference ]       = $line_items[ $line_items_counter ];
				$items_reference_already_added[ $reference ]['counter'] = $line_items_counter;
				++$line_items_counter;
			} else {
				$cached_item                                 = $items_reference_already_added[ $reference ];
				$new_counter                                 = $cached_item['counter'];
				$line_items[ $new_counter ]['amount']        = $line_items[ $new_counter ]['amount'] + $normalized_amount;
				$line_items[ $new_counter ]['quantity']      = $line_items[ $new_counter ]['quantity'] + $quantity;
				$items_reference_already_added[ $reference ] = $line_items[ $new_counter ];
				$items_reference_already_added[ $reference ]['counter'] = $new_counter;
			}
		}

		return $line_items;
	}

	/**
	 * Get the discount amount for a cart item.
	 *
	 * @param array $cart_item Single item from the cart.
	 * @return float
	 */
	public static function get_cart_item_discount_amount( $cart_item ) {
		if ( ! array_key_exists( 'line_subtotal', $cart_item ) || ! array_key_exists( 'line_total', $cart_item ) ) {
			return 0;
		}

		return (float) wc_format_decimal( $cart_item['line_subtotal'] - $cart_item['line_total'] );
	}

	/**
	 * Get the discount amount for an order item.
	 *
	 * @param WC_Order_Item $order_item Single item from an order.
	 * @return float
	 */
	public static function get_order_item_discount_amount( $order_item ) {
		// @phpstan-ignore-next-line
		return (float) wc_format_decimal( $order_item->get_subtotal() - $order_item->get_total() );
	}

	/**
	 * Get the total amount for an order item.
	 *
	 * @param WC_Order_Item $order_item Single item from an order.
	 * @return float
	 */
	public static function get_order_item_total_amount( $order_item ) {
		// @phpstan-ignore-next-line
		return (float) $order_item->get_total( 'edit' );
	}

	/**
	 * Gets cart or order taxable shipping cost.
	 *
	 * @param \WC_Cart|\WC_Order $wc_cart_or_order Cart or order.
	 *
	 * @return float
	 */
	public static function get_cart_or_order_taxable_shipping_total( $wc_cart_or_order ): float {
		$shipping_methods = StripeTaxPluginHelper::get_cart_or_order_taxable_shipping_methods( $wc_cart_or_order );
		if ( empty( $shipping_methods ) ) {
			return 0.0;
		}

		$taxable_shipping_total = 0.0;

		foreach ( $shipping_methods as $shipping_method ) {
			if ( $wc_cart_or_order instanceof \WC_Cart ) {
				/**
				 * Cart shipping method.
				 *
				 * @var \WC_Shipping_Rate $shipping_method
				 */
				$taxable_shipping_total += (float) $shipping_method->get_cost();
			} elseif ( $wc_cart_or_order instanceof \WC_Order ) {
				/**
				 * Order shipping method.
				 *
				 * @var \WC_Order_Item_Shipping $shipping_method
				 */
				$taxable_shipping_total += (float) $shipping_method->get_total( 'edit' );
			}
		}

		return $taxable_shipping_total;
	}

	/**
	 * Gets cart or order not taxable shipping cost.
	 *
	 * @param \WC_Cart|\WC_Order $wc_cart_or_order Cart or order.
	 *
	 * @return float
	 */
	public static function get_cart_or_order_not_taxable_shipping_total( $wc_cart_or_order ): float {
		$shipping_methods = StripeTaxPluginHelper::get_cart_not_taxable_shipping_methods( $wc_cart_or_order );
		if ( empty( $shipping_methods ) ) {
			return 0.0;
		}

		$not_taxable_shipping_total = 0.0;

		foreach ( $shipping_methods as $shipping_method ) {
			if ( $wc_cart_or_order instanceof \WC_Cart ) {
				/**
				 * Cart shipping method.
				 *
				 * @var \WC_Shipping_Rate $shipping_method
				 */
				$not_taxable_shipping_total += (float) $shipping_method->get_cost();
			} elseif ( $wc_cart_or_order instanceof \WC_Order ) {
				/**
				 * Order shipping method.
				 *
				 * @var \WC_Order_Item_Shipping $shipping_method
				 */
				$not_taxable_shipping_total += (float) $shipping_method->get_total( 'edit' );
			}
		}

		return $not_taxable_shipping_total;
	}

	/**
	 * Builds taxable shipping cost from WooCommerce Cart taxable Shipping Rates for Stripe API.
	 *
	 * @param \WC_Cart|\WC_Order $wc_cart_or_order WooCommerce Cart.
	 * @param string             $currency Currency.
	 *
	 * @return array
	 * @throws Exception In case of any Exception.
	 */
	public static function get_taxable_shipping_cost_from_cart_or_order_for_api( $wc_cart_or_order, string $currency ): array {
		$shipping_total = self::get_cart_or_order_taxable_shipping_total( $wc_cart_or_order );

		$normalized_shipping_total = self::get_normalized_amount( $shipping_total, $currency );

		$tax_settings = new TaxSettings( Options::get_current_mode_key() );

		$shipping_cost = array(
			'amount'       => $normalized_shipping_total,
			'tax_code'     => 'txcd_92010001',
			'tax_behavior' => $tax_settings->get_tax_behavior(),
		);

		return $shipping_cost;
	}

	/**
	 * Apply tax to WooCommerce Cart shipping methods.
	 *
	 * @param array        $shipping_methods Shipping methods.
	 * @param CalculateTax $calculate_tax CalculateTax object.
	 * @param string       $currency Currency.
	 *
	 * @return array
	 * @throws Exception In case of any exception.
	 */
	public static function apply_tax_to_cart_shipping_methods( array $shipping_methods, $calculate_tax, string $currency ): array {
		if ( empty( $shipping_methods ) ) {
			return $shipping_methods;
		}
		$max_cost = 0.0;
		$max_key  = null;
		foreach ( $shipping_methods as $shipping_method_key => $shipping_method ) {
			/**
			 * Shipping method object.
			 *
			 * @var \WC_Shipping_Rate $shipping_method
			 */

			$cost = (float) $shipping_method->get_cost();
			if ( $cost > $max_cost || is_null( $max_key ) ) {
				$max_cost = $cost;
				$max_key  = $shipping_method_key;
			}
		}

		// @phpstan-ignore-next-line
		if ( is_null( $max_key ) || $max_cost <= 0.0 ) {
			return $shipping_methods;
		}

		$response = $calculate_tax->get_response();

		if ( $response->shipping_cost->amount > 0.0 ) {
			$shipping_methods[ $max_key ]->set_taxes( $calculate_tax->get_shipping_cart_item_tax( $currency ) );
		}

		return $shipping_methods;
	}

	/**
	 * Apply tax to WooCommerce Order shipping methods.
	 *
	 * @param mixed  $shipping_methods WooCommerce Order shipping methods.
	 * @param mixed  $calculate_tax CalculateTax object.
	 * @param string $currency Currency.
	 *
	 * @return mixed
	 */
	public static function apply_tax_to_order_shipping_methods( $shipping_methods, $calculate_tax, string $currency ) {
		if ( empty( $shipping_methods ) ) {
			return $shipping_methods;
		}
		$max_cost = 0.0;
		$max_key  = null;
		foreach ( $shipping_methods as $shipping_method_key => $shipping_method ) {
			/**
			 * Shipping method object.
			 *
			 * @var \WC_Order_Item_Shipping $shipping_method
			 */

			$cost = (float) $shipping_method->get_total( 'edit' );
			if ( $cost > $max_cost || is_null( $max_key ) ) {
				$max_cost = $cost;
				$max_key  = $shipping_method_key;
			}
		}

		if ( is_null( $max_key ) || $max_cost <= 0.0 ) {
			return $shipping_methods;
		}

		$response = $calculate_tax->get_response();

		if ( $response->shipping_cost->amount > 0.0 ) {
			$shipping_methods[ $max_key ]->set_taxes( $calculate_tax->get_shipping_order_item_raw_tax_data( $currency ) );
		}

		return $shipping_methods;
	}

	/**
	 * Uses the shipping class to calculate shipping then gets the totals when its finished.
	 *
	 * @param array $shipping_methods WooCommerce Cart shipping methods.
	 * @param mixed $wc_cart WooCommerce Cart.
	 *
	 * @return array
	 */
	public static function calculate_cart_shipping( array $shipping_methods, $wc_cart ): array {
		$shipping_taxes = wp_list_pluck( $shipping_methods, 'taxes' );
		$merged_taxes   = array();
		foreach ( $shipping_taxes as $taxes ) {
			foreach ( $taxes as $tax_id => $tax_amount ) {
				if ( ! isset( $merged_taxes[ $tax_id ] ) ) {
					$merged_taxes[ $tax_id ] = 0;
				}
				$merged_taxes[ $tax_id ] += $tax_amount;
			}
		}

		$wc_cart->set_shipping_tax( array_sum( $merged_taxes ) );
		$wc_cart->set_shipping_taxes( $merged_taxes );
		$wc_cart->set_shipping_total( array_sum( wp_list_pluck( $shipping_methods, 'cost' ) ) );

		if ( wc_prices_include_tax() ) {
			$wc_cart->set_shipping_total( (float) $wc_cart->get_shipping_total( 'edit' ) - (float) $wc_cart->get_shipping_tax( 'edit' ) );
		}

		return $shipping_methods;
	}

	/**
	 * Sets WooCommerce Cart totals.
	 *
	 * @param mixed    $response Stripe Calculate Tax API response object.
	 * @param \WC_Cart $wc_cart WooCommerce Cart.
	 * @param string   $currency Currency.
	 *
	 * @return void
	 * @throws Exception In case of any Exception.
	 */
	public static function calculate_cart_totals( $response, $wc_cart, string $currency ) {
		$wc_cart->set_total( self::get_denormalized_amount( $response->amount_total, $currency ) + $wc_cart->get_fee_total() + self::get_cart_or_order_not_taxable_shipping_total( $wc_cart ) );
		$wc_cart->set_total_tax( self::get_denormalized_amount( $response->tax_amount_exclusive + $response->tax_amount_inclusive, $currency ) );
	}

	/**
	 * Get Stripe Tax rates array from Stripe Tax Calculation API response.
	 *
	 * @param mixed                        $response Response.
	 * @param \WC_Order_Item_Product|false $wc_order_item WC Order items.
	 *
	 * @return array prepared for Woocommerce tax rates from Stripe Tax Calculation API response rates
	 */
	public static function get_wc_rates_array_from_response_for_item( $response, $wc_order_item = false ): array {
		$new_item_tax_rates = array();
		foreach ( $response->line_items->data as $datum ) {
			$product_name = '';
			if ( $wc_order_item ) {
				$product      = $wc_order_item->get_product();
				$item_id      = $wc_order_item->get_id();
				$product_name = $product->get_name() . ( (int) $item_id ? ( '#' . $item_id ) : '' );
			}
			if ( ! $wc_order_item || $datum->reference === $product_name ) {
				foreach ( $datum->tax_breakdown as $tax_breakdown ) {
					if ( ! isset( $tax_breakdown->tax_rate_details ) && ! is_object( $tax_breakdown->tax_rate_details ) ) {
						continue;
					}
					$rate_name       = $tax_breakdown->jurisdiction->display_name . ' ' . $tax_breakdown->tax_rate_details->display_name;
					$rate_percentage = $tax_breakdown->tax_rate_details->percentage_decimal;
					$tax_type        = $tax_breakdown->tax_rate_details->tax_type;
					// $rate_key        = 'stripe_tax_for_woocommerce__' . $tax_type . '__' . $rate_percentage . '__' . $rate_name;

					$rate_key = StripeTaxTaxRateMemRepo::find_or_create(
						$tax_breakdown->jurisdiction->country,
						$tax_breakdown->jurisdiction->state,
						(float) $tax_breakdown->tax_rate_details->percentage_decimal,
						$tax_breakdown->jurisdiction->display_name . ' ' . $tax_breakdown->tax_rate_details->display_name
					);

					if ( ! array_key_exists( $rate_key, $new_item_tax_rates ) ) {
						$new_item_tax_rates[ $rate_key ] = array(
							'rate'      => (float) $rate_percentage,
							'label'     => $rate_name,
							'shipping'  => 'no',
							'compound'  => 'no',
							'inclusive' => 'inclusive' === $datum->tax_behavior,
							'amount'    => $tax_breakdown->amount,
						);
					} else {
						$new_item_tax_rates[ $rate_key ]['amount'] += $tax_breakdown->amount;
					}
				}
			}
		}

		return $new_item_tax_rates;
	}

	/**
	 * Normalize amount according Stripe's currency rules
	 *
	 * @param float  $amount Amount.
	 * @param string $currency Currency.
	 *
	 * @return int
	 * @throws Exception If data is not correct.
	 * @see https://stripe.com/docs/currencies
	 * @see CalculateTax::get_denormalized_amount()
	 */
	public static function get_normalized_amount( float $amount, string $currency ): int {
		Validate::validate_currency( $currency );
		if ( StripeTaxPluginHelper::is_currency_zero_decimal( $currency ) ) {
			return (int) $amount;
		}
		if ( StripeTaxPluginHelper::is_currency_three_decimal( $currency ) ) {
			return (int) ( round( $amount * 100 ) * 10 );
		}
		if ( StripeTaxPluginHelper::is_currency_special_case_round_and_multiply_100( $currency ) ) {
			return (int) ( round( $amount ) * 100 );
		}

		return (int) round( $amount * 100 );
	}

	/**
	 * Makes reversal conversion from normalized amount
	 *
	 * @param float  $amount Amount.
	 * @param string $currency Currency.
	 *
	 * @return float
	 * @throws Exception If data is not correct.
	 * @see https://stripe.com/docs/currencies
	 * @see CalculateTax::get_normalized_amount()
	 */
	public static function get_denormalized_amount( float $amount, string $currency ): float {
		Validate::validate_currency( $currency );
		if ( StripeTaxPluginHelper::is_currency_zero_decimal( $currency ) ) {
			return $amount;
		}
		if ( StripeTaxPluginHelper::is_currency_three_decimal( $currency ) ) {
			return $amount / 1000.0;
		}

		return $amount / 100.0;
	}

	/**
	 * Create Stripe Tax Calculation API call
	 *
	 * @return \Stripe\StripeTaxForWooCommerce\SDK\lib\Tax\Calculation|object
	 * @throws Exception If Response data is not correct.
	 * @see https://stripe.com/docs/api/tax/calculations/create
	 */
	protected function get_from_api_call(): object {
		$stripe_client = $this->get_stripe_client( $this->api_key );

		$request = $this->request;

		$response = $stripe_client->tax->calculations->create( $request );

		if ( ! is_object( $response ) || ! isset( $response->object ) || 'tax.calculation' !== $response->object ) {
			throw new Exception( esc_html__( 'Unknown error', 'stripe-tax-for-woocommerce' ) );
		}

		return $response;
	}

	/**
	 * Check if Stripe Tax Calculation expired.
	 * We use 30 seconds magic number to be sure it will not expire during script execution.
	 * We assume entire site request will be handled not more than in 30 seconds
	 *
	 * @param mixed $response Response.
	 *
	 * @return bool
	 * @see https://stripe.com/docs/api/tax/calculations/object#tax_calculation_object-expires_at
	 */
	protected function is_expired( $response ): bool {
		if ( time() > ( $response['expires_at'] - 30 ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Deletes Stripe Tax Calculation from database.
	 * It is used if Stripe Tax Transaction created from Tax Calculation.
	 *
	 * @return void
	 * @see https://stripe.com/docs/api/tax/transactions/create_from_calculation#tax_transaction_create-calculation
	 */
	public function delete() {
		$items = wp_cache_get( self::CACHE_KEY, self::CACHE_GROUP );
		if ( is_array( $items ) || array_key_exists( $this->md5_concat, $items ) ) {
			unset( $items[ $this->md5_concat ] );
			wp_cache_set( self::CACHE_KEY, $items, self::CACHE_GROUP, 2 * HOUR_IN_SECONDS );
		}
		global $wpdb;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
		$wpdb->query(
			$wpdb->prepare(
				'DELETE FROM %i WHERE %i = %s AND %i = %s AND %i = %s AND %i = %s',
				array(
					self::TABLE_NAME,
					'calculate_tax_md5',
					$this->calculate_tax_md5,
					'tax_registrations_md5',
					$this->tax_registrations_md5,
					'tax_settings_md5',
					$this->tax_settings_md5,
					'api_key_md5',
					$this->api_key_md5,
				)
			)
		);
	}

	/**
	 * Gets Stripe Tax Calculation from WordPress object cache (if exists) to reduce database requests
	 *
	 * @return mixed|null
	 */
	protected function get_from_object_cache() {
		$items = wp_cache_get( self::CACHE_KEY, self::CACHE_GROUP );

		if ( ! is_array( $items ) || ! array_key_exists( $this->md5_concat, $items ) ) {
			return null;
		}

		if ( $this->is_expired( $items[ $this->md5_concat ] ) ) {
			unset( $items[ $this->md5_concat ] );
			wp_cache_set( self::CACHE_KEY, $items, self::CACHE_GROUP, 2 * HOUR_IN_SECONDS );

			return null;
		}

		return $items[ $this->md5_concat ];
	}

	/**
	 * Sets Tax Calculation to WordPress object cache
	 *
	 * @param mixed $response Tax Calculation response.
	 *
	 * @return void
	 */
	protected function set_to_object_cache( $response ) {
		$cache = wp_cache_get( self::CACHE_KEY, self::CACHE_GROUP );

		if ( ! is_array( $cache ) ) {
			$cache = array();
		}

		$cache[ $this->md5_concat ] = $response;

		wp_cache_set( self::CACHE_KEY, $cache, self::CACHE_GROUP, 2 * HOUR_IN_SECONDS );
	}

	/**
	 * Gets Stripe Tax Calculation from database
	 *
	 * @return object|null
	 */
	protected function get_from_db() {
		global $wpdb;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
		$results = $wpdb->get_row(
			$wpdb->prepare(
				'SELECT %i, %i FROM %i WHERE %i = %s AND %i = %s AND %i = %s AND %i = %s',
				array(
					'time',
					'response',
					self::TABLE_NAME,
					'calculate_tax_md5',
					$this->calculate_tax_md5,
					'tax_registrations_md5',
					$this->tax_registrations_md5,
					'tax_settings_md5',
					$this->tax_settings_md5,
					'api_key_md5',
					$this->api_key_md5,
				)
			),
			ARRAY_A
		);

		if ( empty( $results ) ) {
			return null;
		}

		$response = json_decode( $results['response'], true );
		if ( ! $response || $this->is_expired( $response ) ) {
			return null;
		}

		$response = Util::convertToStripeObject( $response, array() );

		return $response;
	}

	/**
	 * Save or update Stripe Tax Calculation into database
	 *
	 * @param object $response Response.
	 *
	 * @return void
	 */
	protected function set_to_db( $response ) {
		global $wpdb;
		$time = time();
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
		$wpdb->query(
			$wpdb->prepare(
				'INSERT INTO %i (%i, %i, %i, %i, %i, %i, %i) VALUES (%d, %s, %s, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE %i = %d, %i = %s',
				array(
					self::TABLE_NAME,
					'time',
					'calculate_tax_md5',
					'tax_registrations_md5',
					'tax_settings_md5',
					'api_key_md5',
					'request',
					'response',
					$time,
					$this->calculate_tax_md5,
					$this->tax_registrations_md5,
					$this->tax_settings_md5,
					$this->api_key_md5,
					wp_json_encode( $this->request ),
					wp_json_encode( $response ),
					'time',
					$time,
					'response',
					wp_json_encode( $response ),
				)
			)
		);

		static::maybe_cleanup_old_rows();
	}

	/**
	 * Deletes old calculate_tax rows in bounded batches.
	 *
	 * @param int $keep_seconds Retention period in seconds. Default: 2 hours.
	 * @param int $batch_size Batch size per delete query. Default: 1000 rows.
	 *
	 * @return int Number of rows deleted.
	 */
	public static function cleanup_old_rows_batch( int $keep_seconds = self::CLEANUP_RETENTION_SECONDS, int $batch_size = self::CLEANUP_BATCH_SIZE ): int {
		global $wpdb;

		$cutoff = time() - $keep_seconds;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
		$deleted = $wpdb->query(
			$wpdb->prepare(
				'DELETE FROM %i WHERE %i < %d LIMIT %d',
				array(
					self::TABLE_NAME,
					'time',
					$cutoff,
					$batch_size,
				)
			)
		);

		return is_int( $deleted ) ? $deleted : 0;
	}

	/**
	 * Runs throttled cleanup of old calculate_tax rows.
	 * Uses a transient to prevent running more than once per minute.
	 *
	 * @return void
	 */
	protected static function maybe_cleanup_old_rows(): void {
		if ( get_transient( self::CLEANUP_TRANSIENT_KEY ) ) {
			return;
		}

		set_transient( self::CLEANUP_TRANSIENT_KEY, 1, self::CLEANUP_COOLDOWN_SECONDS );
		static::cleanup_old_rows_batch();
	}

	/**
	 * Gets response from Create Tax Calculation API
	 * Function will check in object cache and return response from it
	 * If nothing found in WordPress object cache - it will return from database
	 * If nothing found in cache it will call Create Tax calculation api method
	 * Also any cache can be skipped using appropriate method parameter
	 *
	 * @param bool $skip_object_cache Skip object cache.
	 * @param bool $skip_database_cache Skip database cache.
	 *
	 * @return mixed|object|Calculation
	 * @throws Exception If data is not correct.
	 */
	public function get_response( $skip_object_cache = false, $skip_database_cache = false ) {
		if ( ! $skip_object_cache ) {
			$response = $this->get_from_object_cache();
			if ( $response ) {
				return $response;
			}
		}
		if ( ! $skip_database_cache ) {
			$response = $this->get_from_db();
			if ( $response ) {
				$this->set_to_object_cache( $response );

				return $response;
			}
		}
		$response = $this->get_from_api_call();

		$this->set_to_db( $response );
		$this->set_to_object_cache( $response );

		return $response;
	}

	/**
	 * Returns shipping tax in Woocommerce Cart format.
	 *
	 * @param string     $currency Currency for proper Stripe to WooCommerce format.
	 * @param float|null $denormalized_shipping_tax Currency for proper Stripe to WooCommerce format.
	 *
	 * @throws Exception In case of any exception occurs.
	 */
	public function get_shipping_cart_item_tax( string $currency, ?float $denormalized_shipping_tax = null ): array {
		$response              = $this->get_response();
		$shipping_amount       = $response->shipping_cost->amount;
		$shipping_tax          = $response->shipping_cost->amount_tax;
		$tax_included_in_price = ( 'inclusive' === $response->shipping_cost->tax_behavior );
		if ( is_null( $denormalized_shipping_tax ) ) {
			$denormalized_shipping_tax = self::get_denormalized_amount( $shipping_tax, $currency );
		}

		// Rate percentage not used for any calculations, but added for compatibility with other tax rates.
		if ( $tax_included_in_price ) {
			$percentage_decimal = (float) $shipping_tax / (float) ( $shipping_amount - $shipping_tax ) * 100.0;
		} else {
			$percentage_decimal = (float) $shipping_tax / (float) $shipping_amount * 100.0;
		}

		$rate_id = StripeTaxTaxRateMemRepo::find_or_create(
			'',
			'',
			(float) $percentage_decimal,
			'Shipping Tax'
		);

		$shipping_cart_item_tax = array( $rate_id => $denormalized_shipping_tax );

		return $shipping_cart_item_tax;
	}

	/**
	 * Returns shipping tax in Woocommerce Order Item Tax format.
	 *
	 * @param string     $currency Currency for proper Stripe to WooCommerce format.
	 * @param float|null $denormalized_shipping_tax Currency for proper Stripe to WooCommerce format.
	 *
	 * @return StripeOrderItemTax
	 * @throws Exception In case of any exception occurs.
	 */
	public function get_shipping_order_item_tax( string $currency, ?float $denormalized_shipping_tax = null ): StripeOrderItemTax {
		$response              = $this->get_response();
		$shipping_amount       = $response->shipping_cost->amount;
		$shipping_tax          = $response->shipping_cost->amount_tax;
		$tax_included_in_price = ( 'inclusive' === $response->shipping_cost->tax_behavior );
		if ( is_null( $denormalized_shipping_tax ) ) {
			$denormalized_shipping_tax = self::get_denormalized_amount( $shipping_tax, $currency );
		}

		// Rate percentage not used for any calculations, but added for compatibility with other tax rates.
		if ( $tax_included_in_price ) {
			$percentage_decimal = (float) $shipping_tax / (float) ( $shipping_amount - $shipping_tax ) * 100.0;
		} else {
			$percentage_decimal = (float) $shipping_tax / (float) $shipping_amount * 100.0;
		}

		$rate_id = StripeTaxTaxRateMemRepo::find_or_create(
			'',
			'',
			(float) $percentage_decimal,
			'Shipping Tax'
		);

		$shipping_order_item_tax = new StripeOrderItemTax();
		$shipping_order_item_tax->set_rate( $rate_id );
		$shipping_order_item_tax->set_tax_total( 0 );
		$shipping_order_item_tax->set_shipping_tax_total( $denormalized_shipping_tax );

		return $shipping_order_item_tax;
	}

	/**
	 * Returns shipping tax in Woocommerce raw tax data format.
	 *
	 * @param string     $currency Currency for proper Stripe to WooCommerce format.
	 * @param float|null $denormalized_shipping_tax Currency for proper Stripe to WooCommerce format.
	 *
	 * @return array
	 * @throws Exception In case of any exception occurs.
	 */
	public function get_shipping_order_item_raw_tax_data( string $currency, ?float $denormalized_shipping_tax = null ): array {
		$response              = $this->get_response();
		$shipping_amount       = $response->shipping_cost->amount;
		$shipping_tax          = $response->shipping_cost->amount_tax;
		$tax_included_in_price = ( 'inclusive' === $response->shipping_cost->tax_behavior );
		if ( is_null( $denormalized_shipping_tax ) ) {
			$denormalized_shipping_tax = self::get_denormalized_amount( $shipping_tax, $currency );
		}

		// Rate percentage not used for any calculations, but added for compatibility with other tax rates.
		if ( $tax_included_in_price ) {
			$percentage_decimal = (float) $shipping_tax / (float) ( $shipping_amount - $shipping_tax ) * 100.0;
		} else {
			$percentage_decimal = (float) $shipping_tax / (float) $shipping_amount * 100.0;
		}

		$rate_id = StripeTaxTaxRateMemRepo::find_or_create(
			'',
			'',
			(float) $percentage_decimal,
			'Shipping Tax'
		);

		return array( 'total' => array( $rate_id => $denormalized_shipping_tax ) );
	}

	/**
	 * Generate html to render tax id's fields in user profile page
	 *
	 * @param int    $counter Counter, if needed to set array key to be sent from front-end.
	 * @param string $selected Selected <option> of the <select> html element (Tax ID type).
	 * @param string $value Tax ID value.
	 *
	 * @return false|string
	 * @see https://stripe.com/docs/api/tax/calculations/create#calculate_tax-customer_details-tax_ids
	 */
	public static function get_tax_ids_admin_html( int $counter = - 1, string $selected = '', string $value = '' ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
		ob_start();

		return ob_get_clean();
	}

	/**
	 * Retrieves Tax IDs from POST request and reduces them to not more than 5
	 *
	 * @see https://stripe.com/docs/api/tax/calculations/create#calculate_tax-customer_details-tax_ids
	 * @return array
	 */
	public static function get_prepared_posted_tax_ids(): array {
		$user_id = get_current_user_id();

		check_admin_referer( 'update-user_' . $user_id );
		if ( ! isset( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ), 'update-user_' . $user_id ) ) {
			exit;
		}

		if ( ! isset( $_POST['stripe_tax_for_woocommerce_tax_id'] ) || ! is_array( $_POST['stripe_tax_for_woocommerce_tax_id'] ) ) {
			return array();
		}
		$prepared = array();
		foreach ( array_map( 'sanitize_text_field', wp_unslash( $_POST['stripe_tax_for_woocommerce_tax_id'] ) ) as $key => $value ) {
			$new_value_value = trim( sanitize_text_field( wp_unslash( $_POST['stripe_tax_for_woocommerce_tax_id_value'][ $key ] ?? '' ) ) );
			if ( $new_value_value ) {
				$prepared[ $value ] = $new_value_value;
			}
		}

		return array_slice( $prepared, 0, 5, true );
	}

	/**
	 * Prepares Tax IDs for API call. It is needed to remove country code before send to API.
	 *
	 * @param array<string, string> $tax_ids Tax_Ids.
	 *
	 * @return array
	 * @see StripeTaxPluginHelper::TAX_IDS
	 * @see https://stripe.com/docs/api/tax/calculations/create#calculate_tax-customer_details-tax_ids
	 */
	public static function convert_tax_ids_for_api( $tax_ids ): array {
		if ( ! is_array( $tax_ids ) ) {
			return array();
		}
		$for_api = array();
		$counter = 0;
		foreach ( $tax_ids as $key => $value ) {
			$for_api[ $counter ]          = array();
			$for_api[ $counter ]['type']  = substr( $key, 3 );
			$for_api[ $counter ]['value'] = $value;
			++$counter;
		}

		return $for_api;
	}

	/**
	 * Returns true if order prices include taxes.
	 *
	 * @param \WC_Order $wc_order WooCommerce Order object.
	 */
	public static function order_prices_include_tax( $wc_order ) {
		$order_prices_include_tax_tag = $wc_order->get_prices_include_tax();

		$order_prices_include_tax = ( true === $order_prices_include_tax_tag || 'yes' === $order_prices_include_tax_tag ) || wc_prices_include_tax();

		return $order_prices_include_tax;
	}

	/**
	 * Returns true if order prices already include taxes.
	 *
	 * @param string $wc_order WC_Order the order.
	 */
	public static function order_shipping_price_include_tax( $wc_order ) {
		if ( ! static::order_prices_include_tax( $wc_order ) ) {
			return false;
		}
		// @phpstan-ignore-next-line
		$shipping_items = $wc_order->get_items( 'shipping' );

		foreach ( $shipping_items as $shipping_item ) {
			if ( $shipping_item->get_meta( '_stripe_not_subtotal_include_tax' ) === '' ) {
				continue;
			}

			return true;
		}

		return false;
	}
}