<?php
/**
* Validation service
*
* @package Stripe\StripeTaxForWooCommerce\Stripe
*/
namespace Stripe\StripeTaxForWooCommerce\Stripe;
// Exit if script started not from WordPress.
defined( 'ABSPATH' ) || exit;
use Exception;
use Stripe\StripeTaxForWooCommerce\Stripe\Exception\CountryStateException;
use Stripe\StripeTaxForWooCommerce\Stripe\Exception\CountrySupportException;
use Stripe\StripeTaxForWooCommerce\Stripe\Exception\TaxBehaviorException;
use Stripe\StripeTaxForWooCommerce\WordPress\Options;
use WC_Validation;
/**
* Validation service
*/
class Validate {
const MAX_ALLOWED_LINE_ITEMS = 100;
/**
* Validate live key format
*
* @param string $key Key.
*
* @throws Exception If invalid key.
*/
public static function validate_live_key_format( $key ) {
if ( ! is_string( $key ) || 1 !== preg_match( '/^sk_live_[0-9A-Za-z]{99}$/', trim( $key ) ) ) {
throw new Exception( esc_html__( 'The live key must start with "sk_live_" and 99 alphanumeric characters that follow.', 'stripe-tax-for-woocommerce' ) );
}
}
/**
* Validate tax code
*
* @param string $tax_code Tax code.
* @param string $api_key API key.
*
* @return void
* @throws Exception If not valid.
*/
public static function validate_tax_code( string $tax_code, string $api_key ) {
$tax_code_list = ( new TaxCodeList( $api_key ) )->get();
if ( array_key_exists( $tax_code, $tax_code_list ) ) {
return;
}
throw new Exception( esc_html__( 'Unknown tax code', 'stripe-tax-for-woocommerce' ) );
}
/**
* Validate Stripe API key format
*
* @param string $key Key.
* @param int $mode can be 0 or 1.
*
* @return void
* @throws Exception If validation fails.
*/
public static function validate_key_format( $key, $mode = 0 ) {
if ( Options::MODE_TEST === $mode ) {
if ( ! is_string( $key ) || 1 !== preg_match( '/^sk_test_[0-9A-Za-z]{99}$/', trim( $key ) ) ) {
throw new Exception( esc_html__( 'The test key must start with sk_test_ and 99 alphanumeric characters that follow.', 'stripe-tax-for-woocommerce' ) );
}
} elseif ( Options::MODE_LIVE === $mode ) {
if ( ! is_string( $key ) || 1 !== preg_match( '/^sk_live_[0-9A-Za-z]{99}$/', trim( $key ) ) ) {
throw new Exception( esc_html__( 'The live key must start with sk_live_ and 99 alphanumeric characters that follow.', 'stripe-tax-for-woocommerce' ) );
}
}
}
/**
* Validate WooCommerce connect last "wcs_stripe_state" parameter received from WooCommerce.
*
* @param string $state State.
*
* @return void
* @throws Exception In case of invalid security option.
*/
public static function validate_woocommerce_connect_last_state( $state ) {
if ( is_string( $state ) && '' !== $state && Options::get_option( Options::OPTION_WOOCOMMERCE_CONNECT_LAST_STATE ) === $state ) {
return;
}
throw new Exception( esc_html__( 'Invalid security option "wcs_stripe_state" received from WooCommerce. Please try to connect with Stripe again.', 'stripe-tax-for-woocommerce' ) );
}
/**
* Validate country supported as Stripe Origin address
*
* @param string $country Country.
* @param string $message_type Message type, default 'settings'.
*
* @return void
* @throws CountrySupportException In case if not passing validation.
*/
public static function validate_country_support( $country, $message_type = 'settings' ) {
$countries = StripeTaxPluginHelper::get_allowed_origin_address_countries();
if ( is_string( $country ) && '' !== $country && array_key_exists( $country, $countries ) ) {
return;
}
$error_message = esc_html__( 'Stripe Tax isn\'t yet supported for your country. Please contact stripe-tax-support@stripe.com to register your interest or if you want to collect tax in a supported market.', 'stripe-tax-for-woocommerce' );
if ( 'tax_validation' === $message_type ) {
$error_message = esc_html__( 'Country field from billing/shipping address is invalid!', 'stripe-tax-for-woocommerce' );
}
// phpcs:ignore
throw new CountrySupportException( esc_html__( $error_message ) );
}
/**
* Validate state is valid for selected country
*
* @param string $country CISO 3166-1 alpha-2 country code.
* @param string $state State to be validated.
* @param array<string, string> $states States array.
* @param string $message_type Message type, default 'settings'.
*
* @return void
* @throws CountryStateException If not valid.
*/
public static function validate_country_state( $country, $state, $states, $message_type = 'settings' ) {
if ( is_string( $state ) && '' !== $state && array_key_exists( $state, $states ) ) {
return;
}
/* translators: %s: Name of a country */
$error_message = esc_html__( 'Country %s requires more information to calculate taxes accurately. Make sure you provide a province.', 'stripe-tax-for-woocommerce' );
if ( 'tax_validation' === $message_type ) {
$error_message = esc_html__( 'State field from billing/shipping address is invalid!', 'stripe-tax-for-woocommerce' );
}
// phpcs:ignore
throw new CountryStateException( esc_html__( $error_message ) );
}
/**
* Validate tax behavior
*
* @param string $tax_behavior Tax behavior.
*
* @return void
* @throws TaxBehaviorException If not valid.
* @see https://stripe.com/docs/api/tax/calculations/create#calculate_tax-line_items-tax_behavior
*/
public static function validate_tax_behavior( $tax_behavior ) {
$tax_behaviors = array(
'inclusive',
'exclusive',
'inferred_by_currency',
);
if ( is_string( $tax_behavior ) && '' !== $tax_behavior && in_array( $tax_behavior, $tax_behaviors, true ) ) {
return;
}
throw new TaxBehaviorException( esc_html__( 'Invalid product tax behavior. It must be either inclusive, exclusive or inferred by currency.', 'stripe-tax-for-woocommerce' ) );
}
/**
* Validate if currency is supported and throw Exception if not
*
* @param string $currency Currency in ISO 4217 alphabetic code lowercase.
*
* @throws Exception If not supported.
* @see https://stripe.com/docs/currencies#presentment-currencies
*/
public static function validate_currency( string $currency ) {
if ( ! StripeTaxPluginHelper::is_currency_supported( $currency ) ) {
throw new Exception( esc_html__( 'Currency not supported', 'stripe-tax-for-woocommerce' ) );
}
}
/**
* Validate the number of line items for Stripe's Calculate Tax API rules
*
* @param array $line_items Line items.
*
* @throws Exception If number of line items exceeds the maximum allowed line items.
*/
public static function validate_number_of_line_items( array $line_items ) {
$count = count( $line_items );
if ( $count > static::MAX_ALLOWED_LINE_ITEMS ) {
throw new Exception(
sprintf(
/* translators: %d: Number of maximum allowed cart line items */
esc_html__(
'The maximum number of allowed cart items (%d) for tax calculation has been exceeded.',
'stripe-tax-for-woocommerce'
),
esc_html( static::MAX_ALLOWED_LINE_ITEMS )
)
);
}
}
/**
* Validate line items array for Stripe's Calculate Tax API rules
*
* @param array $line_items Line items.
*
* @throws Exception If data is not correct.
* @see https://stripe.com/docs/api/tax/calculations/create#calculate_tax-line_items
* @see self::validate_line_item_single()
*/
public static function validate_line_items( array $line_items ) {
$count = count( $line_items );
for ( $i = 0; $i < $count; $i++ ) {
if ( ! array_key_exists( $i, $line_items ) || ! is_array( $line_items[ $i ] ) ) {
throw new Exception( esc_html__( 'Line items list must be numbered array, each of element of it must be an array', 'stripe-tax-for-woocommerce' ) );
}
self::validate_line_item_single( $line_items[ $i ] );
}
}
/**
* Validate single line item for Stripe's Calculate Tax API
*
* @param array $line_item Line item.
*
* @throws Exception If any required property not exists or not valid type or redundant properties exist.
* @see https://stripe.com/docs/api/tax/calculations/create#calculate_tax-line_items
* @see Validate::validate_line_items()
*/
public static function validate_line_item_single( array $line_item ) {
$fields_to_validate = array_flip( array( 'amount', 'reference', 'quantity', 'tax_behavior', 'tax_code' ) );
$difference_count = count( array_diff_key( $line_item, $fields_to_validate ) );
// Check if $line_item has redundant properties.
if ( $difference_count > 0 ) {
throw new Exception( esc_html__( 'Unsupported property exists inside line item', 'stripe-tax-for-woocommerce' ) );
}
// Check required parameters.
if ( ! array_key_exists( 'amount', $line_item ) || ! is_int( $line_item['amount'] ) ) {
throw new Exception( esc_html__( 'Property "amount" must both exist and be type of "int"', 'stripe-tax-for-woocommerce' ) );
}
if ( ! array_key_exists( 'reference', $line_item ) || ! is_string( $line_item['reference'] ) ) {
throw new Exception( esc_html__( 'Property "reference" must both exist and be type of "string"', 'stripe-tax-for-woocommerce' ) );
}
// Check optional parameters.
if ( array_key_exists( 'quantity', $line_item ) && ! is_int( $line_item['quantity'] ) ) {
throw new Exception( esc_html__( 'Property "quantity" must be type of "int" if set', 'stripe-tax-for-woocommerce' ) );
}
if ( array_key_exists( 'tax_behavior', $line_item ) && ! in_array(
$line_item['tax_behavior'],
array(
'inclusive',
'exclusive',
),
true
) ) {
throw new Exception( esc_html__( 'Property "tax_behavior" must either be "inclusive" or "exclusive" if set', 'stripe-tax-for-woocommerce' ) );
}
if ( array_key_exists( 'tax_code', $line_item ) && ! is_string( $line_item['tax_code'] ) ) {
throw new Exception( esc_html__( 'Property "tax_code" must be type of "string" if set', 'stripe-tax-for-woocommerce' ) );
}
}
/**
* Validate customer details address for Stripe's Calculate Tax API rules
*
* @param array<string, string|array> $customer_details_address Customer details.
*
* @throws Exception If data is not correct.
* @see https://stripe.com/docs/api/tax/calculations/create#calculate_tax-customer_details-address
* @see self::validate_customer_details()
*/
public static function validate_customer_details_address( array $customer_details_address ): void {
$fields_to_validate = array_flip( array( 'country', 'postal_code', 'city', 'line1', 'line2', 'state' ) );
$difference_count = count( array_diff_key( $customer_details_address, $fields_to_validate ) );
// Check if $line_item has redundant properties.
if ( $difference_count > 0 ) {
throw new Exception( esc_html__( 'Unsupported property exists inside customer details', 'stripe-tax-for-woocommerce' ) );
}
// Check required parameters.
if ( ! array_key_exists( 'country', $customer_details_address ) || ! is_string( $customer_details_address['country'] ) ) {
throw new Exception( esc_html__( 'Property "country" must both exist and be type of "string"', 'stripe-tax-for-woocommerce' ) );
}
if ( ( 'US' === $customer_details_address['country'] ) && ! array_key_exists( 'postal_code', $customer_details_address ) ) {
throw new Exception( esc_html__( 'Property "postal_code" must exist if country is set to "US"', 'stripe-tax-for-woocommerce' ) );
}
if ( ( 'CA' === $customer_details_address['country'] ) && ( ! array_key_exists( 'postal_code', $customer_details_address ) || ! array_key_exists( 'state', $customer_details_address ) ) ) {
throw new Exception( esc_html__( 'Properties "postal_code" and "state" must exist if country is set to "CA"', 'stripe-tax-for-woocommerce' ) );
}
// Check optional parameters.
if ( array_key_exists( 'postal_code', $customer_details_address ) && ! is_string( $customer_details_address['postal_code'] ) ) {
throw new Exception( esc_html__( 'Property "postal_code" must be type of "string" if set', 'stripe-tax-for-woocommerce' ) );
}
if ( array_key_exists( 'city', $customer_details_address ) && ! is_string( $customer_details_address['city'] ) ) {
throw new Exception( esc_html__( 'Property "city" must be type of "string" if set', 'stripe-tax-for-woocommerce' ) );
}
if ( array_key_exists( 'line1', $customer_details_address ) && ! is_string( $customer_details_address['line1'] ) ) {
throw new Exception( esc_html__( 'Property "line1" must be type of "string" if set', 'stripe-tax-for-woocommerce' ) );
}
if ( array_key_exists( 'line2', $customer_details_address ) && ! is_string( $customer_details_address['line2'] ) ) {
throw new Exception( esc_html__( 'Property "line2" must be type of "string" if set', 'stripe-tax-for-woocommerce' ) );
}
if ( array_key_exists( 'state', $customer_details_address ) && ! is_string( $customer_details_address['state'] ) ) {
throw new Exception( esc_html__( 'Property "state" must be type of "string" if set', 'stripe-tax-for-woocommerce' ) );
}
}
/**
* Validate customer details required address fields for Stripe's Calculate Tax API rules
*
* @param array<string, string|array> $customer_details_address Customer details.
*
* @throws Exception If data is not correct or empty.
* @see https://stripe.com/docs/api/tax/calculations/create#calculate_tax-customer_details-address
*/
public static function check_customer_details_address_fields( array $customer_details_address ): void {
self::validate_customer_details_address( $customer_details_address );
// Check required parameters.
if ( empty( $customer_details_address['country'] ) ) {
throw new Exception( esc_html__( 'Country field from billing/shipping address can not be empty', 'stripe-tax-for-woocommerce' ) );
}
if ( ( 'US' === $customer_details_address['country'] ) && empty( $customer_details_address['postal_code'] ) ) {
throw new Exception( esc_html__( 'Postcode field from billing/shipping address can not be empty if country is set to "US"', 'stripe-tax-for-woocommerce' ) );
}
if ( ( 'CA' === $customer_details_address['country'] ) && ( empty( $customer_details_address['postal_code'] ) || empty( $customer_details_address['state'] ) ) ) {
throw new Exception( esc_html__( 'Postcode and State fields from billing/shipping address can not be empty if country is set to "CA"', 'stripe-tax-for-woocommerce' ) );
}
self::validate_country_support( $customer_details_address['country'], 'tax_validation' );
$state_handlers = array(
'AE' => 'get_allowed_origin_address_ae_provinces',
'AU' => 'get_allowed_origin_address_au_states',
'CA' => 'get_allowed_origin_address_ca_provinces',
'ES' => 'get_allowed_origin_address_es_provinces',
'HK' => 'get_allowed_origin_address_hk_areas',
'IE' => 'get_allowed_origin_address_ie_counties',
'IT' => 'get_allowed_origin_address_it_provinces',
'JP' => 'get_allowed_origin_address_jp_prefectures',
'US' => 'get_allowed_origin_address_us_states',
);
if ( isset( $state_handlers[ $customer_details_address['country'] ] ) && ! empty( $customer_details_address['state'] ) ) {
$handler_method = $state_handlers[ $customer_details_address['country'] ];
self::validate_country_state( $customer_details_address['country'], $customer_details_address['state'], StripeTaxPluginHelper::$handler_method(), 'tax_validation' );
}
if ( in_array( $customer_details_address['country'], array( 'US', 'CA' ), true ) &&
! empty( $customer_details_address['postal_code'] ) &&
! WC_Validation::is_postcode( $customer_details_address['postal_code'], $customer_details_address['country'] )
) {
throw new Exception( esc_html__( 'Postcode field from billing/shipping address is invalid.', 'stripe-tax-for-woocommerce' ) );
}
}
/**
* Validate customer details single tax id for Stripe's Calculate Tax API rules
*
* @param array{"type":string, "value":string} $customer_details_tax_id_single Customer details tax id.
*
* @return void
* @throws Exception If data is not correct.
* @see https://stripe.com/docs/api/tax/calculations/create#calculate_tax-customer_details-tax_ids
* @see self::validate_customer_details()
* @see self::validate_customer_details_tax_ids()
*/
public static function validate_customer_details_tax_id_single( $customer_details_tax_id_single ) {
$fields_to_validate = array_flip( array( 'type', 'value' ) );
$difference_count = count( array_diff_key( $customer_details_tax_id_single, $fields_to_validate ) );
// Check if $line_item has redundant properties.
if ( $difference_count > 0 ) {
throw new Exception( esc_html__( 'Unsupported key exists inside tax ids array', 'stripe-tax-for-woocommerce' ) );
}
// Check required parameters.
if ( ! array_key_exists( 'type', $customer_details_tax_id_single ) || ! is_string( $customer_details_tax_id_single['type'] ) ) {
throw new Exception( esc_html__( 'Key "type" must both exist and be type of "string"', 'stripe-tax-for-woocommerce' ) );
}
if ( ! array_key_exists( 'value', $customer_details_tax_id_single ) || ! is_string( $customer_details_tax_id_single['value'] ) ) {
throw new Exception( esc_html__( 'Key "value" must both exist and be type of "string"', 'stripe-tax-for-woocommerce' ) );
}
}
/**
* Validate customer details tax ids array for Stripe's Calculate Tax API rules
*
* @param array<int, array{"type":string, "value":string}> $customer_details_tax_ids Customer details tax ids.
*
* @return void
* @throws Exception If not numbered array.
* @see https://stripe.com/docs/api/tax/calculations/create#calculate_tax-customer_details-tax_ids
* @see self::validate_customer_details()
* @see Validate::validate_customer_details_tax_id_single()
*/
public static function validate_customer_details_tax_ids( array $customer_details_tax_ids ) {
$count = count( $customer_details_tax_ids );
for ( $i = 0; $i < $count; $i++ ) {
if ( ! array_key_exists( $i, $customer_details_tax_ids ) || ! is_array( $customer_details_tax_ids[ $i ] ) ) {
throw new Exception( esc_html__( 'Tax ids must be numbered array, each of element of it must be an array', 'stripe-tax-for-woocommerce' ) );
}
self::validate_customer_details_tax_id_single( $customer_details_tax_ids[ $i ] );
}
}
/**
* Validate customer details for Stripe's Calculate Tax API rules
*
* @param array $customer_details Customer details.
*
* @throws Exception If data is not correct.
* @see https://stripe.com/docs/api/tax/calculations/create#calculate_tax-customer_details
* @see Validate::validate_customer_details_address()
* @see Validate::validate_customer_details_tax_ids()
*/
public static function validate_customer_details( array $customer_details ) {
$fields_to_validate = array_flip( array( 'address', 'address_source', 'tax_ids', 'taxability_override' ) );
$difference_count = count( array_diff_key( $customer_details, $fields_to_validate ) );
// Check if $line_item has redundant properties.
if ( $difference_count > 0 ) {
throw new Exception( esc_html__( 'Unsupported property exists inside customer details', 'stripe-tax-for-woocommerce' ) );
}
// Check required parameters.
if ( ! array_key_exists( 'address', $customer_details ) || ! is_array( $customer_details['address'] ) ) {
throw new Exception( esc_html__( 'Property "address" must both exist and be type of "array"', 'stripe-tax-for-woocommerce' ) );
}
self::validate_customer_details_address( $customer_details['address'] );
if ( ! array_key_exists( 'address_source', $customer_details ) || ! in_array(
$customer_details['address_source'],
array(
'billing',
'shipping',
),
true
) ) {
throw new Exception( esc_html__( 'Property "address_source" must both exist and be type of "string"', 'stripe-tax-for-woocommerce' ) );
}
// Check optional parameters.
if ( array_key_exists( 'tax_ids', $customer_details ) && ! is_array( $customer_details['tax_ids'] ) ) {
throw new Exception( esc_html__( 'Property "tax_ids" must be type of "array" if set', 'stripe-tax-for-woocommerce' ) );
}
if ( array_key_exists( 'tax_ids', $customer_details ) && is_array( $customer_details['tax_ids'] ) ) {
self::validate_customer_details_tax_ids( $customer_details['tax_ids'] );
}
if ( array_key_exists( 'taxability_override', $customer_details ) && ! in_array(
$customer_details['taxability_override'],
array(
'none',
'reverse_charge',
'customer_exempt',
),
true
) ) {
throw new Exception( esc_html__( 'Property "taxability_override" must either be "none", "reverse_charge" or "customer_exempt" if set', 'stripe-tax-for-woocommerce' ) );
}
}
}