- -
- countries->get_countries(); - ?> -- | vat_number ? esc_html__( 'Yes', 'woocommerce-eu-vat-number' ) : esc_html__( 'No', 'woocommerce-eu-vat-number' ); ?> | -- |
---|---|---|
- | vat_number ); ?> | -- validated ) { - echo '?'; - } else { - echo $data->valid ? '✔' : '✘'; - } - ?> - | -
- | ip_address ? esc_html( $data->ip_address ) : esc_html__( 'Unknown', 'woocommerce-eu-vat-number' ); ?> | -- |
- | - ip_country ) { - echo esc_html( $countries[ $data->billing_country ] ) . ' '; - - if ( $data->billing_country === $data->ip_country ) { - echo '✔'; - } elseif ( $data->self_declared ) { - esc_html_e( '(self-declared)', 'woocommerce-eu-vat-number' ); - } else { - echo '✘'; - } - } else { - esc_html_e( 'Unknown', 'woocommerce-eu-vat-number' ); - } - ?> - | -- |
- | billing_country ? esc_html( $countries[ $data->billing_country ] ) : esc_html__( 'Unknown', 'woocommerce-eu-vat-number' ); ?> | -- |
'; - - if ( ! self::is_eu_order( $order ) ) { - echo '–'; - } else { - $data = self::get_order_vat_data( $order ); - - if ( $data->vat_number ) { - echo esc_html( $data->vat_number ) . ' '; - - if ( $data->validated && $data->valid ) { - echo '✔'; - } elseif ( ! $data->validated ) { - esc_html_e( '(validation failed)', 'woocommerce-eu-vat-number' ); - } else { - echo '✘'; - } - } else { - $countries = WC()->countries->get_countries(); - - echo esc_html( $countries[ $data->billing_country ] ) . ' '; - - if ( $data->billing_country === $data->ip_country ) { - echo '✔'; - } elseif ( $data->self_declared ) { - esc_html_e( '(self-declared)', 'woocommerce-eu-vat-number' ); - } else { - echo '✘'; - } - } - } - echo '
'; - } - } - - /** - * Handles VAT when order is created/edited within admin manually. - * - * @since 2.3.14 - * @param array $args Additional arguments. - * @param object $order WooCommerce Order Object. - * @throws Exception Error message if VAT validation fails. - */ - public static function admin_order( $args, $order ) { - if ( ! is_object( $order ) ) { - return; - } - - /* - * First try and get the billing country from the - * address form (adding new order). If it is not - * found, get it from the order (editing the order). - */ - $billing_country = isset( $_POST['_billing_country'] ) ? wc_clean( wp_unslash( $_POST['_billing_country'] ) ) : $order->get_billing_country(); // phpcs:ignore WordPress.Security.NonceVerification.Missing - $shipping_country = isset( $_POST['_shipping_country'] ) ? wc_clean( wp_unslash( $_POST['_shipping_country'] ) ) : $order->get_shipping_country(); // phpcs:ignore WordPress.Security.NonceVerification.Missing - $billing_postcode = isset( $_POST['_billing_postcode'] ) ? wc_clean( wp_unslash( $_POST['_billing_postcode'] ) ) : $order->get_billing_postcode(); // phpcs:ignore WordPress.Security.NonceVerification.Missing - - /* - * First try and get the VAT number from the - * address form (adding new order). If it is not - * found, get it from the order (editing the order). - */ - $vat_number = isset( $_POST['_billing_vat_number'] ) ? wc_clean( wp_unslash( $_POST['_billing_vat_number'] ) ) : wc_eu_vat_get_vat_from_order( $order ); // phpcs:ignore WordPress.Security.NonceVerification.Missing - - // Ignore empty VAT Number and countries outside EU. - if ( empty( $vat_number ) || ! in_array( $billing_country, WC_EU_VAT_Number::get_eu_countries(), true ) ) { - return; - } - - $valid = WC_EU_VAT_Number::vat_number_is_valid( $vat_number, $billing_country, $billing_postcode ); - $base_country_match = WC_EU_VAT_Number::is_base_country_match( $billing_country, $shipping_country ); - - if ( 'no' === get_option( 'woocommerce_eu_vat_number_deduct_in_base', 'yes' ) && $base_country_match ) { - add_filter( 'woocommerce_order_is_vat_exempt', '__return_false' ); - return; - } - - $order->update_meta_data( '_vat_number_is_validated', 'true' ); - - try { - if ( true === $valid ) { - $order->update_meta_data( '_vat_number_is_valid', 'true' ); - add_filter( 'woocommerce_order_is_vat_exempt', '__return_true' ); - return; - } - - $fail_handler = get_option( 'woocommerce_eu_vat_number_failure_handling', 'reject' ); - // Exempt VAT even if VAT number is not valid as per "Failed Validation Handling" settings. - if ( 'accept' === $fail_handler ) { - add_filter( 'woocommerce_order_is_vat_exempt', '__return_true' ); - } - if ( is_wp_error( $valid ) ) { - throw new Exception( $valid->get_error_message() ); - } - - if ( ! $valid ) { - // translators: %1$s VAT number field label, %2$s VAT number, %3$s Billing Country. - throw new Exception( sprintf( esc_html__( 'You have entered an invalid %1$s (%2$s) for your billing country (%3$s).', 'woocommerce-eu-vat-number' ), get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ), $vat_number, $billing_country ) ); - } - } catch ( Exception $e ) { - $order->update_meta_data( '_vat_number_is_valid', 'false' ); - echo ''; - } - } - - /** - * Adds custom fields to user profile. - * - * @since 2.3.21 - * @param array $fields WC defined user fields. - * @return array $fields Modified user fields. - */ - public static function add_customer_meta_fields( $fields ) { - $fields['billing']['fields']['vat_number'] = array( - 'label' => esc_html__( 'VAT number', 'woocommerce-eu-vat-number' ), - 'description' => '', - ); - - return $fields; - } - - /** - * Return VAT information to get customer details via AJAX. - * - * @since 2.3.21 - * @param array $data The customer's data in context. - * @param object $customer The customer object in context. - * @param int $user_id The user ID in context. - * @return array $data Modified user data. - */ - public static function get_customer_details( $data, $customer, $user_id ) { - $data['billing']['vat_number'] = get_user_meta( $user_id, 'vat_number', true ); - - return $data; - } - - /** - * Display admin notice for EU VAT Number. - */ - public static function maybe_show_admin_notice() { - - $screen = get_current_screen(); - $screen_id = $screen ? $screen->id : ''; - - if ( 'woocommerce_page_wc-settings' !== $screen_id ) { - return; - } - - // Check whether disclaimer is already dismissed or taxes are not enabled. - if ( ! wc_tax_enabled() || 'yes' === get_option( 'woocommerce_eu_vat_number_dismiss_disclaimer', 'no' ) ) { - return; - } - - // phpcs:ignore WordPress.Security.NonceVerification.Recommended - if ( isset( $_GET[ 'tab' ] ) && 'tax' === $_GET[ 'tab' ] ) { - $screen_id .= '_tax'; - } - - // phpcs:ignore WordPress.Security.NonceVerification.Recommended - if ( ! isset( $_GET[ 'tab' ] ) || ( isset( $_GET[ 'tab' ] ) && 'general' === $_GET[ 'tab' ] ) ) { - $screen_id .= '_general'; - } - - $show_on_screens = array( - 'woocommerce_page_wc-settings_tax', - 'woocommerce_page_wc-settings_general', - ); - - if ( ! in_array( $screen_id, $show_on_screens, true ) ) { - return; - } - - $base_country = WC()->countries->get_base_country(); - $base_postcode = WC()->countries->get_base_postcode(); - - // Create URL to dismiss the disclaimer. - $dismiss_url = add_query_arg( - array( - 'dismiss_eu_vat_disclaimer' => 'yes', - ), - wc_get_current_admin_url() - ); - ?> - - - get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ), + 'show' => false, + 'id' => '_billing_vat_number', + 'value' => $vat_number, + ); + return $fields; + } + + /** + * Add Meta Boxes. + */ + public static function add_meta_boxes() { + if ( class_exists( 'Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController' ) ) { + $screen = wc_get_container()->get( \Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled() + ? wc_get_page_screen_id( 'shop-order' ) + : 'shop_order'; + } else { + $screen = 'shop_order'; + } + + add_meta_box( 'wc_eu_vat', __( 'EU VAT', 'woocommerce-eu-vat-number' ), array( __CLASS__, 'output' ), $screen, 'side' ); + } + + /** + * Enqueue admin styles and scripts + * + * @param string $hook The current admin page. + * @return void + */ + public static function styles_and_scripts( $hook ) { + global $post; + + $is_order_edit_screen = false; + + if ( 'woocommerce_page_wc-orders' === $hook && isset( $_GET['id'] ) ) { + $is_order_edit_screen = true; + } else if ( 'woocommerce_page_wc-orders' === $hook && isset( $_GET['action'] ) && 'new' === wp_unslash( sanitize_text_field( $_GET['action'] ) ) ) { + $is_order_edit_screen = true; + } else if ( in_array( $hook, array( 'post-new.php', 'post.php' ), true ) && $post && 'shop_order' === $post->post_type ) { + $is_order_edit_screen = true; + } + + // Load admin style. + wp_enqueue_style( 'wc_eu_vat_admin_css', plugins_url( 'assets/css/admin.css', WC_EU_VAT_FILE ), array(), WC_EU_VAT_VERSION ); + + // Load script only on add/edit order page. + if ( $is_order_edit_screen ) { + $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min'; + wp_enqueue_script( 'wc-eu-vat-admin', WC_EU_VAT_PLUGIN_URL . '/assets/js/admin' . $suffix . '.js', array( 'jquery' ), WC_EU_VAT_VERSION, true ); + } + } + + /** + * Is this is an EU order? + * + * @param WC_Order $order The order object. + * @return boolean + */ + protected static function is_eu_order( $order ) { + return in_array( $order->get_billing_country(), WC_EU_VAT_Number::get_eu_countries(), true ); + } + + /** + * Get order VAT Number data in one object/array. + * + * @param WC_Order $order The order object. + * @return object + */ + protected static function get_order_vat_data( $order ) { + return (object) array( + 'vat_number' => wc_eu_vat_get_vat_from_order( $order ), + 'valid' => wc_string_to_bool( $order->get_meta( '_vat_number_is_valid', true ) ), + 'validated' => wc_string_to_bool( $order->get_meta( '_vat_number_is_validated', true ) ), + 'billing_country' => $order->get_billing_country(), + 'ip_address' => $order->get_customer_ip_address(), + 'ip_country' => $order->get_meta( '_customer_ip_country', true ), + 'self_declared' => wc_string_to_bool( $order->get_meta( '_customer_self_declared_country', true ) ), + ); + } + + /** + * Output meta box. + */ + public static function output() { + global $post, $theorder; + + if ( ! is_object( $theorder ) ) { + $theorder = wc_get_order( $post->ID ); + } + + // We only need this box for EU orders. + if ( ! self::is_eu_order( $theorder ) ) { + ?> ++ +
+ countries->get_countries(); + ?> ++ | vat_number ? esc_html__( 'Yes', 'woocommerce-eu-vat-number' ) : esc_html__( 'No', 'woocommerce-eu-vat-number' ); ?> | ++ |
---|---|---|
+ | vat_number ); ?> | ++ validated ) { + echo '?'; + } else { + echo $data->valid ? '✔' : '✘'; + } + ?> + | +
+ | ip_address ? esc_html( $data->ip_address ) : esc_html__( 'Unknown', 'woocommerce-eu-vat-number' ); ?> | ++ |
+ | + ip_country ) { + echo esc_html( $countries[ $data->billing_country ] ) . ' '; + + if ( $data->billing_country === $data->ip_country ) { + echo '✔'; + } elseif ( $data->self_declared ) { + esc_html_e( '(self-declared)', 'woocommerce-eu-vat-number' ); + } else { + echo '✘'; + } + } else { + esc_html_e( 'Unknown', 'woocommerce-eu-vat-number' ); + } + ?> + | ++ |
+ | billing_country ? esc_html( $countries[ $data->billing_country ] ) : esc_html__( 'Unknown', 'woocommerce-eu-vat-number' ); ?> | ++ |
'; + + if ( ! self::is_eu_order( $order ) ) { + echo '–'; + } else { + $data = self::get_order_vat_data( $order ); + + if ( $data->vat_number ) { + echo esc_html( $data->vat_number ) . ' '; + + if ( $data->validated && $data->valid ) { + echo '✔'; + } elseif ( ! $data->validated ) { + esc_html_e( '(validation failed)', 'woocommerce-eu-vat-number' ); + } else { + echo '✘'; + } + } else { + $countries = WC()->countries->get_countries(); + + echo esc_html( $countries[ $data->billing_country ] ) . ' '; + + if ( $data->billing_country === $data->ip_country ) { + echo '✔'; + } elseif ( $data->self_declared ) { + esc_html_e( '(self-declared)', 'woocommerce-eu-vat-number' ); + } else { + echo '✘'; + } + } + } + echo '
'; + } + } + + /** + * Handles VAT when order is created/edited within admin manually. + * + * @since 2.3.14 + * @param array $args Additional arguments. + * @param object $order WooCommerce Order Object. + * @throws Exception Error message if VAT validation fails. + */ + public static function admin_order( $args, $order ) { + if ( ! is_object( $order ) ) { + return; + } + + /* + * First try and get the billing country from the + * address form (adding new order). If it is not + * found, get it from the order (editing the order). + */ + $billing_country = isset( $_POST['_billing_country'] ) ? wc_clean( wp_unslash( $_POST['_billing_country'] ) ) : $order->get_billing_country(); // phpcs:ignore WordPress.Security.NonceVerification.Missing + $shipping_country = isset( $_POST['_shipping_country'] ) ? wc_clean( wp_unslash( $_POST['_shipping_country'] ) ) : $order->get_shipping_country(); // phpcs:ignore WordPress.Security.NonceVerification.Missing + $billing_postcode = isset( $_POST['_billing_postcode'] ) ? wc_clean( wp_unslash( $_POST['_billing_postcode'] ) ) : $order->get_billing_postcode(); // phpcs:ignore WordPress.Security.NonceVerification.Missing + + /* + * First try and get the VAT number from the + * address form (adding new order). If it is not + * found, get it from the order (editing the order). + */ + $vat_number = isset( $_POST['_billing_vat_number'] ) ? wc_clean( wp_unslash( $_POST['_billing_vat_number'] ) ) : wc_eu_vat_get_vat_from_order( $order ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + + // Ignore empty VAT Number and countries outside EU. + if ( empty( $vat_number ) || ! in_array( $billing_country, WC_EU_VAT_Number::get_eu_countries(), true ) ) { + return; + } + + $valid = WC_EU_VAT_Number::vat_number_is_valid( $vat_number, $billing_country, $billing_postcode ); + $base_country_match = WC_EU_VAT_Number::is_base_country_match( $billing_country, $shipping_country ); + + if ( 'no' === get_option( 'woocommerce_eu_vat_number_deduct_in_base', 'yes' ) && $base_country_match ) { + add_filter( 'woocommerce_order_is_vat_exempt', '__return_false' ); + return; + } + + $order->update_meta_data( '_vat_number_is_validated', 'true' ); + + try { + if ( true === $valid ) { + $order->update_meta_data( '_vat_number_is_valid', 'true' ); + add_filter( 'woocommerce_order_is_vat_exempt', '__return_true' ); + return; + } + + $fail_handler = get_option( 'woocommerce_eu_vat_number_failure_handling', 'reject' ); + // Exempt VAT even if VAT number is not valid as per "Failed Validation Handling" settings. + if ( 'accept' === $fail_handler ) { + add_filter( 'woocommerce_order_is_vat_exempt', '__return_true' ); + } + if ( is_wp_error( $valid ) ) { + throw new Exception( $valid->get_error_message() ); + } + + if ( ! $valid ) { + // translators: %1$s VAT number field label, %2$s VAT number, %3$s Billing Country. + throw new Exception( sprintf( esc_html__( 'You have entered an invalid %1$s (%2$s) for your billing country (%3$s).', 'woocommerce-eu-vat-number' ), get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ), $vat_number, $billing_country ) ); + } + } catch ( Exception $e ) { + $order->update_meta_data( '_vat_number_is_valid', 'false' ); + echo ''; + } + } + + /** + * Adds custom fields to user profile. + * + * @since 2.3.21 + * @param array $fields WC defined user fields. + * @return array $fields Modified user fields. + */ + public static function add_customer_meta_fields( $fields ) { + $fields['billing']['fields']['vat_number'] = array( + 'label' => esc_html__( 'VAT number', 'woocommerce-eu-vat-number' ), + 'description' => '', + ); + + return $fields; + } + + /** + * Return VAT information to get customer details via AJAX. + * + * @since 2.3.21 + * @param array $data The customer's data in context. + * @param object $customer The customer object in context. + * @param int $user_id The user ID in context. + * @return array $data Modified user data. + */ + public static function get_customer_details( $data, $customer, $user_id ) { + $data['billing']['vat_number'] = get_user_meta( $user_id, 'vat_number', true ); + + return $data; + } + + /** + * Display admin notice for EU VAT Number. + */ + public static function maybe_show_admin_notice() { + + $screen = get_current_screen(); + $screen_id = $screen ? $screen->id : ''; + + if ( 'woocommerce_page_wc-settings' !== $screen_id ) { + return; + } + + // Check whether disclaimer is already dismissed or taxes are not enabled. + if ( ! wc_tax_enabled() || 'yes' === get_option( 'woocommerce_eu_vat_number_dismiss_disclaimer', 'no' ) ) { + return; + } + + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + if ( isset( $_GET[ 'tab' ] ) && 'tax' === $_GET[ 'tab' ] ) { + $screen_id .= '_tax'; + } + + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + if ( ! isset( $_GET[ 'tab' ] ) || ( isset( $_GET[ 'tab' ] ) && 'general' === $_GET[ 'tab' ] ) ) { + $screen_id .= '_general'; + } + + $show_on_screens = array( + 'woocommerce_page_wc-settings_tax', + 'woocommerce_page_wc-settings_general', + ); + + if ( ! in_array( $screen_id, $show_on_screens, true ) ) { + return; + } + + $base_country = WC()->countries->get_base_country(); + $base_postcode = WC()->countries->get_base_postcode(); + + // Create URL to dismiss the disclaimer. + $dismiss_url = add_query_arg( + array( + 'dismiss_eu_vat_disclaimer' => 'yes', + 'dismiss_eu_vat_disclaimer_nonce' => wp_create_nonce( 'dismiss_eu_vat_disclaimer' ), + ), + wc_get_current_admin_url() + ); + ?> + + + extend_rest_api(); - } - - /** - * When an order is completed, woocommerce_store_api_checkout_update_order_from_request is fired. This action allows - * extensions to update the customer's order. In this method, we set the customer's self-delared country and the - * country we think they are from based on their IP. - * - * @param WC_Order $order The current customer's order object. - * @param WP_REST_Request $request The API request currently being processed. - * @return void - */ - public function update_order_from_request( $order, $request ) { - if ( false !== WC_EU_VAT_Number::get_ip_country() ) { - $order->update_meta_data( '_customer_ip_country', WC_EU_VAT_Number::get_ip_country() ); - $order->update_meta_data( '_customer_self_declared_country', ! empty( $request['extensions']['woocommerce-eu-vat-number']['location_confirmation'] ) ? 'true' : 'false' ); - } - } - - /** - * Returns an array of script handles to enqueue in the frontend context. - * - * @return string[] - */ - public function get_script_handles() { - return array( 'wc-blocks-eu-vat-scripts-frontend' ); - } - - /** - * Returns an array of script handles to enqueue in the editor context. - * - * @return string[] - */ - public function get_editor_script_handles() { - return array( 'wc-blocks-eu-vat-scripts-frontend' ); - } - - - /** - * An array of key, value pairs of data made available to the block on the client side. - * - * @return array - */ - public function get_script_data() { - return array( - 'ip_country' => WC_EU_VAT_Number::get_ip_country(), - 'ip_address' => apply_filters( 'wc_eu_vat_self_declared_ip_address', WC_Geolocation::get_ip_address() ), - 'woocommerce_eu_vat_number_validate_ip' => get_option( 'woocommerce_eu_vat_number_validate_ip', 'no' ), - ); - } - - /** - * Stores Rest Extending instance. - * - * @var ExtendRestApi - */ - private static $extend; - - /** - * Adding order meta data that are part of the plugin of orders created using the - * checkout block. - * - * @param WC_Order $order The order being placed. - * @return void - */ - public function update_order_meta( $order ) { - - if ( ! $order instanceof \WC_Order ) { - return; - } - - $vat_number = WC()->session->get( 'vat-number' ); - if ( ! $vat_number ) { - return; - } - - $data = $this::validate(); - $order->update_meta_data( '_vat_number_is_validated', ! is_null( $data['validation']['valid'] ) ? 'true' : 'false' ); - $order->update_meta_data( '_vat_number_is_valid', true === $data['validation']['valid'] ? 'true' : 'false' ); - $order->update_meta_data( '_billing_vat_number', $vat_number ); - - if ( false !== WC_EU_VAT_Number::get_ip_country() ) { - $order->update_meta_data( '_customer_ip_country', WC_EU_VAT_Number::get_ip_country() ); - } - - $this->maybe_apply_exemption(); - } - - /** - * Registers extensions to two rest API endpoints: - * 1 - a cart update endpoint to get the VAT number after it has been typed and check for - * vat excepmption while on the checkout form. - * 2 - a checkout endpoint extension to inform our frontend component about the result of - * the validity of the VAT number and react accordingly. - * - * @return void - */ - public function extend_rest_api() { - - /** - * A cart update endpoint to get the VAT number after it has been typed and check for - * vat exemption while on the checkout form. - */ - $extend = StoreApi::container()->get( ExtendSchema::class ); - - $extend->register_update_callback( - array( - 'namespace' => 'woocommerce-eu-vat-number', - 'callback' => function( $data ) { - if ( isset( $data['vat_number'] ) ) { - if ( empty( $data['vat_number'] ) ) { - WC()->session->set( 'vat-number', null ); - WC()->customer->set_is_vat_exempt( false ); - } else { - WC()->session->set( 'vat-number', strtoupper( $data['vat_number'] ) ); - $this->maybe_apply_exemption( false ); - } - } else { - WC()->session->set( 'vat-number', null ); - WC()->customer->set_is_vat_exempt( false ); - } - }, - ) - ); - - /** - * A checkout endpoint extension to inform our frontend component about the result of - * the validity of the VAT number and react accordingly. - */ - $extend->register_endpoint_data( - array( - 'endpoint' => CartSchema::IDENTIFIER, - 'namespace' => 'woocommerce-eu-vat-number', - 'data_callback' => array( $this, 'vat_number_information' ), - 'schema_callback' => array( $this, 'schema_for_vat_number_information' ), - 'schema_type' => ARRAY_A, - ) - ); - - /** - * A checkout endpoint to accept the location_confirmation key. - */ - $extend->register_endpoint_data( - array( - 'endpoint' => CheckoutSchema::IDENTIFIER, - 'namespace' => 'woocommerce-eu-vat-number', - 'schema_callback' => function() { - return array( - 'location_confirmation' => array( - 'description' => __( 'Location confirmation.', 'woocommerce-eu-vat-number' ), - 'type' => 'boolean', - 'context' => array(), - ), - ); - }, - 'schema_type' => ARRAY_A, - ) - ); - } - - /** - * Information about the status of the given VAT Number. - * - * @return Array Information about the validity of the VAT Number. - */ - public function vat_number_information() { - return array_merge( $this::validate(), array( 'cart_has_digital_goods' => WC_EU_VAT_Number::cart_has_digital_goods() ) ); - } - - /** - * Checks if VAT number is formatted correctly. - * - * @return Array Information about the result of the validation. - */ - public function validate() { - $data = array(); - $vat_number = WC()->session->get( 'vat-number' ); - $country = WC()->customer->get_billing_country(); - $postcode = WC()->customer->get_billing_postcode(); - $fail_handler = get_option( 'woocommerce_eu_vat_number_failure_handling', 'reject' ); - $valid = WC_EU_VAT_Number::vat_number_is_valid( $vat_number, $country, $postcode ); - - if ( is_wp_error( $valid ) ) { - $data['vat_number'] = $vat_number; - $data['validation'] = array( - 'valid' => false, - 'error' => $valid->get_error_message(), - ); - return $data; - } - - $vat_number_formatted = WC_EU_VAT_Number::get_formatted_vat_number( $vat_number ); - $data['vat_number'] = $valid ? WC_EU_VAT_Number::get_vat_number_prefix( $country ) . $vat_number_formatted : $vat_number; - $data['validation'] = array( - 'valid' => $valid, - 'error' => false, - ); - - if ( 'reject' === $fail_handler && ( ! $valid || ! $vat_number ) ) { - $data['validation']['error'] = ! $valid ? __( 'Invalid VAT number.', 'woocommerce-eu-vat-number' ) : false; - } - - return $data; - } - - /** - * Validates VAT Number and tries to apply the exemption given the information. - * - * @param boolean $with_notices Indicates whether to add notices or just run without any feedback. - * This is used while chaning the field on the checkout for and when submitting the order, hence - * the two separate use cases. - * - * @return void - */ - public function maybe_apply_exemption( $with_notices = true ) { - - $vat_number = WC()->session->get( 'vat-number' ); - - if ( ! $vat_number ) { - return; - } - - $validation = $this::validate(); - $fail_handler = get_option( 'woocommerce_eu_vat_number_failure_handling', 'reject' ); - - if ( false === (bool) $validation['validation']['valid'] && $with_notices ) { - switch ( $fail_handler ) { - case 'accept_with_vat': - wc_add_notice( $validation['validation']['error'], 'error' ); - break; - case 'accept': - break; - default: - wc_add_notice( $validation['validation']['error'], 'error' ); - break; - } - } - - $this->set_vat_exemption( $validation ); - } - - /** - * Tries to apply the exemption given the information. - * - * @param mixed $validation Result of the validation of the VAT Number. - * @return void - */ - private function set_vat_exemption( $validation ) { - - $fail_handler = get_option( 'woocommerce_eu_vat_number_failure_handling', 'reject' ); - $b_country = WC()->customer->get_billing_country(); - $s_country = WC()->customer->get_shipping_country(); - - if ( true === (bool) $validation['validation']['valid'] ) { - WC_EU_VAT_Number::maybe_set_vat_exempt( true, $b_country, $s_country ); - } else { - switch ( $fail_handler ) { - case 'accept_with_vat': - WC_EU_VAT_Number::maybe_set_vat_exempt( false, $b_country, $s_country ); - break; - case 'accept': - WC_EU_VAT_Number::maybe_set_vat_exempt( true, $b_country, $s_country ); - break; - default: - WC_EU_VAT_Number::maybe_set_vat_exempt( false, $b_country, $s_country ); - break; - } - } - } - - /** - * Schema for the information about the VAT Number. - * - * @return Array Information about this vat number. - */ - public function schema_for_vat_number_information() { - return array( - 'vat_data' => array( - 'description' => __( 'VAT Data', 'woocommerce-eu-vat-number' ), - 'type' => 'array', - 'readonly' => true, - ), - ); - } -} +extend_rest_api(); + } + + /** + * When an order is completed, woocommerce_store_api_checkout_update_order_from_request is fired. This action allows + * extensions to update the customer's order. In this method, we set the customer's self-delared country and the + * country we think they are from based on their IP. + * + * @param WC_Order $order The current customer's order object. + * @param WP_REST_Request $request The API request currently being processed. + * @return void + */ + public function update_order_from_request( $order, $request ) { + if ( false !== WC_EU_VAT_Number::get_ip_country() ) { + $order->update_meta_data( '_customer_ip_country', WC_EU_VAT_Number::get_ip_country() ); + $order->update_meta_data( '_customer_self_declared_country', ! empty( $request['extensions']['woocommerce-eu-vat-number']['location_confirmation'] ) ? 'true' : 'false' ); + } + } + + /** + * Returns an array of script handles to enqueue in the frontend context. + * + * @return string[] + */ + public function get_script_handles() { + return array( 'wc-blocks-eu-vat-scripts-frontend' ); + } + + /** + * Returns an array of script handles to enqueue in the editor context. + * + * @return string[] + */ + public function get_editor_script_handles() { + return array( 'wc-blocks-eu-vat-scripts-frontend', 'wc-blocks-eu-vat-scripts-index' ); + } + + + /** + * An array of key, value pairs of data made available to the block on the client side. + * + * @return array + */ + public function get_script_data() { + return array( + 'ip_country' => WC_EU_VAT_Number::get_ip_country(), + 'ip_address' => apply_filters( 'wc_eu_vat_self_declared_ip_address', WC_Geolocation::get_ip_address() ), + 'woocommerce_eu_vat_number_validate_ip' => get_option( 'woocommerce_eu_vat_number_validate_ip', 'no' ), + ); + } + + /** + * Stores Rest Extending instance. + * + * @var ExtendRestApi + */ + private static $extend; + + /** + * Adding order meta data that are part of the plugin of orders created using the + * checkout block. + * + * @param WC_Order $order The order being placed. + * @return void + */ + public function update_order_meta( $order ) { + + if ( ! $order instanceof \WC_Order ) { + return; + } + + $vat_number = WC()->session->get( 'vat-number' ); + if ( ! $vat_number ) { + return; + } + + $data = $this::validate(); + $order->update_meta_data( '_vat_number_is_validated', ! is_null( $data['validation']['valid'] ) ? 'true' : 'false' ); + $order->update_meta_data( '_vat_number_is_valid', true === $data['validation']['valid'] ? 'true' : 'false' ); + $order->update_meta_data( '_billing_vat_number', $vat_number ); + $customer_id = $order->get_customer_id(); + + if ( $customer_id ) { + $customer = new \WC_Customer( $customer_id ); + $customer->update_meta_data( 'vat_number', $vat_number ); + $customer->save_meta_data(); + } + + if ( false !== WC_EU_VAT_Number::get_ip_country() ) { + $order->update_meta_data( '_customer_ip_country', WC_EU_VAT_Number::get_ip_country() ); + } + + $this->maybe_apply_exemption(); + } + + /** + * Registers extensions to two rest API endpoints: + * 1 - a cart update endpoint to get the VAT number after it has been typed and check for + * vat excepmption while on the checkout form. + * 2 - a checkout endpoint extension to inform our frontend component about the result of + * the validity of the VAT number and react accordingly. + * + * @return void + */ + public function extend_rest_api() { + + /** + * A cart update endpoint to get the VAT number after it has been typed and check for + * vat exemption while on the checkout form. + */ + $extend = StoreApi::container()->get( ExtendSchema::class ); + + $extend->register_update_callback( + array( + 'namespace' => 'woocommerce-eu-vat-number', + 'callback' => function( $data ) { + if ( isset( $data['vat_number'] ) ) { + if ( empty( $data['vat_number'] ) ) { + WC()->session->set( 'vat-number', null ); + WC()->customer->set_is_vat_exempt( false ); + } else { + WC()->session->set( 'vat-number', strtoupper( $data['vat_number'] ) ); + $this->maybe_apply_exemption( false ); + } + } else { + WC()->session->set( 'vat-number', null ); + WC()->customer->set_is_vat_exempt( false ); + } + }, + ) + ); + + /** + * A checkout endpoint extension to inform our frontend component about the result of + * the validity of the VAT number and react accordingly. + */ + $extend->register_endpoint_data( + array( + 'endpoint' => CartSchema::IDENTIFIER, + 'namespace' => 'woocommerce-eu-vat-number', + 'data_callback' => array( $this, 'vat_number_information' ), + 'schema_callback' => array( $this, 'schema_for_vat_number_information' ), + 'schema_type' => ARRAY_A, + ) + ); + + /** + * A checkout endpoint to accept the location_confirmation key. + */ + $extend->register_endpoint_data( + array( + 'endpoint' => CheckoutSchema::IDENTIFIER, + 'namespace' => 'woocommerce-eu-vat-number', + 'schema_callback' => function() { + return array( + 'location_confirmation' => array( + 'description' => __( 'Location confirmation.', 'woocommerce-eu-vat-number' ), + 'type' => 'boolean', + 'context' => array(), + ), + ); + }, + 'schema_type' => ARRAY_A, + ) + ); + } + + /** + * Information about the status of the given VAT Number. + * + * @return Array Information about the validity of the VAT Number. + */ + public function vat_number_information() { + return array_merge( $this::validate(), array( 'cart_has_digital_goods' => WC_EU_VAT_Number::cart_has_digital_goods() ) ); + } + + /** + * Checks if VAT number is formatted correctly. + * + * @return Array Information about the result of the validation. + */ + public function validate() { + $data = array(); + $vat_number = WC()->session->get( 'vat-number' ); + $country = WC()->customer->get_billing_country(); + $postcode = WC()->customer->get_billing_postcode(); + $fail_handler = get_option( 'woocommerce_eu_vat_number_failure_handling', 'reject' ); + $valid = WC_EU_VAT_Number::vat_number_is_valid( $vat_number, $country, $postcode ); + + if ( is_wp_error( $valid ) ) { + $data['vat_number'] = $vat_number; + $data['validation'] = array( + 'valid' => false, + 'error' => $valid->get_error_message(), + ); + return $data; + } + + $vat_number_formatted = WC_EU_VAT_Number::get_formatted_vat_number( $vat_number ); + $data['vat_number'] = $valid ? WC_EU_VAT_Number::get_vat_number_prefix( $country ) . $vat_number_formatted : $vat_number; + $data['validation'] = array( + 'valid' => $valid, + 'error' => false, + ); + + if ( 'reject' === $fail_handler && ( ! $valid || ! $vat_number ) ) { + $data['validation']['error'] = ! $valid ? __( 'Invalid VAT number.', 'woocommerce-eu-vat-number' ) : false; + } + + return $data; + } + + /** + * Validates VAT Number and tries to apply the exemption given the information. + * + * @param boolean $with_notices Indicates whether to add notices or just run without any feedback. + * This is used while chaning the field on the checkout for and when submitting the order, hence + * the two separate use cases. + * + * @return void + */ + public function maybe_apply_exemption( $with_notices = true ) { + + $vat_number = WC()->session->get( 'vat-number' ); + + if ( ! $vat_number ) { + return; + } + + $validation = $this::validate(); + $fail_handler = get_option( 'woocommerce_eu_vat_number_failure_handling', 'reject' ); + + if ( false === (bool) $validation['validation']['valid'] && $with_notices ) { + switch ( $fail_handler ) { + case 'accept_with_vat': + wc_add_notice( $validation['validation']['error'], 'error' ); + break; + case 'accept': + break; + default: + wc_add_notice( $validation['validation']['error'], 'error' ); + break; + } + } + + $this->set_vat_exemption( $validation ); + } + + /** + * Tries to apply the exemption given the information. + * + * @param mixed $validation Result of the validation of the VAT Number. + * @return void + */ + private function set_vat_exemption( $validation ) { + + $fail_handler = get_option( 'woocommerce_eu_vat_number_failure_handling', 'reject' ); + $b_country = WC()->customer->get_billing_country(); + $s_country = WC()->customer->get_shipping_country(); + + if ( true === (bool) $validation['validation']['valid'] ) { + WC_EU_VAT_Number::maybe_set_vat_exempt( true, $b_country, $s_country ); + } else { + switch ( $fail_handler ) { + case 'accept_with_vat': + WC_EU_VAT_Number::maybe_set_vat_exempt( false, $b_country, $s_country ); + break; + case 'accept': + WC_EU_VAT_Number::maybe_set_vat_exempt( true, $b_country, $s_country ); + break; + default: + WC_EU_VAT_Number::maybe_set_vat_exempt( false, $b_country, $s_country ); + break; + } + } + } + + /** + * Schema for the information about the VAT Number. + * + * @return Array Information about this vat number. + */ + public function schema_for_vat_number_information() { + return array( + 'vat_data' => array( + 'description' => __( 'VAT Data', 'woocommerce-eu-vat-number' ), + 'type' => 'array', + 'readonly' => true, + ), + ); + } +} diff --git a/includes/class-wc-eu-vat-my-account.php b/includes/class-wc-eu-vat-my-account.php index 71209a8..e164c54 100644 --- a/includes/class-wc-eu-vat-my-account.php +++ b/includes/class-wc-eu-vat-my-account.php @@ -1,261 +1,261 @@ -= 2.6. - add_action( 'init', array( $this, 'add_endpoints' ) ); - add_filter( 'woocommerce_get_query_vars', array( $this, 'add_query_vars' ), 0 ); - - // Change My Account page title. - add_filter( 'the_title', array( $this, 'endpoint_title' ) ); - - // Inserting new tab/page into My Account page. - add_filter( 'woocommerce_account_menu_items', array( $this, 'new_menu_items' ) ); - add_action( 'woocommerce_account_' . $this->endpoint . '_endpoint', array( $this, 'endpoint_content' ) ); - - // Save a VAT number from My Account form if one is submitted. - if ( isset( $_POST['action'] ) && 'edit_vat_number' === wc_clean( wp_unslash( $_POST['action'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verifying nonce inside function. - $this->save_vat_number(); - } - - add_action( 'woocommerce_init', array( $this, 'maybe_remove_vat' ) ); - } - - /** - * Checks to see if we need to remove vat from displaying in the cart and from product itself. - * - * @see https://github.com/woocommerce/woocommerce-eu-vat-number/issues/71 - * @see https://github.com/woocommerce/woocommerce-eu-vat-number/issues/74 - * @see https://github.com/woocommerce/woocommerce-eu-vat-number/issues/233 - */ - public function maybe_remove_vat() { - // Ignore checkout page as on checkout page VAT exempt based on VAT number from billing fields. - if ( ( is_admin() && ! defined( 'DOING_AJAX' ) ) || ! wc_tax_enabled() || is_checkout() || ! is_user_logged_in() ) { - return; - } - - $vat_number = get_user_meta( get_current_user_id(), 'vat_number', true ); - if ( empty( $vat_number ) || empty( WC()->customer ) ) { - return; - } - - // Validate if VAT is valid. If valid, check for VAT exempt. - try { - $billing_country = WC()->customer->get_billing_country(); - $shipping_country = WC()->customer->get_shipping_country(); - - if ( $this->validate( $vat_number, $billing_country ) ) { - WC_EU_VAT_Number::maybe_set_vat_exempt( true, $billing_country, $shipping_country ); - } - } catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch - // Ignore Exception. - } - } - - /** - * Register new endpoint to use inside My Account page. - * - * @since 2.1.12 - * - * @see https://developer.wordpress.org/reference/functions/add_rewrite_endpoint/ - */ - public function add_endpoints() { - add_rewrite_endpoint( $this->endpoint, EP_ROOT | EP_PAGES ); - } - - /** - * Add new query var. - * - * @since 2.1.12 - * - * @param array $vars Query vars. - * @return array - */ - public function add_query_vars( $vars ) { - $vars[] = $this->endpoint; - - return $vars; - } - - /** - * Set endpoint title. - * - * @since 2.1.12 - * - * @param string $title Endpoint title. - * @return string - */ - public function endpoint_title( $title ) { - global $wp_query; - - $is_endpoint = isset( $wp_query->query_vars[ $this->endpoint ] ); - - if ( $is_endpoint && ! is_admin() && is_main_query() && in_the_loop() && is_account_page() ) { - $title = get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ); - - remove_filter( 'the_title', array( $this, 'endpoint_title' ) ); - } - - return $title; - } - - /** - * Insert new endpoint into My Account menu. - * - * @since 2.1.12 - * - * @param array $items Menu items. - * @return array Menu items. - */ - public function new_menu_items( $items ) { - // Remove logout menu item. - $logout = $items['customer-logout']; - unset( $items['customer-logout'] ); - - // Insert VAT Number. - $items[ $this->endpoint ] = get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ); - - // Insert back logout item. - $items['customer-logout'] = $logout; - - return $items; - } - - /** - * Endpoint HTML content. - * - * @since 2.1.12 - */ - public function endpoint_content() { - $this->render_my_vat_number_content(); - } - - /** - * Render My VAT Number content. - * - * @since 2.1.12 - */ - public function render_my_vat_number_content() { - $vars = array( - 'vat_number' => get_user_meta( get_current_user_id(), 'vat_number', true ), - 'messages' => $this->messages, - ); - - wc_get_template( - 'my-account/my-vat-number.php', - $vars, - 'woocommerce-eu-vat-number', - untrailingslashit( plugin_dir_path( WC_EU_VAT_FILE ) ) . '/templates/' - ); - } - - /** - * Validate a VAT number. - * - * @version 2.3.0 - * @since 2.3.0 - * @param string $vat_number VAT number passed by the form. - * @param string $billing_country Billing country of the order. - * @param string $billing_postcode Billing postcode of the order. - * @param string $current_vat VAT number saved in database. - * - * @return boolean - * @throws Exception For invalid VAT Number. - */ - public function validate( $vat_number, $billing_country, $billing_postcode = '', $current_vat = '' ) { - if ( empty( $vat_number ) ) { - if ( empty( $current_vat ) ) { - throw new Exception( __( 'VAT number cannot be empty.', 'woocommerce-eu-vat-number' ) ); - } - // Allow empty input to clear VAT field. - return true; - } - - if ( empty( $billing_country ) ) { - /* translators: 1: VAT Number */ - throw new Exception( sprintf( __( '%1$s can not be validated because the billing country is missing. Please update your billing address.', 'woocommerce-eu-vat-number' ), '' . get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ) . '' ) ); - } - - $valid = WC_EU_VAT_Number::vat_number_is_valid( $vat_number, $billing_country, $billing_postcode ); - - if ( is_wp_error( $valid ) ) { - throw new Exception( $valid->get_error_message() ); - } - - if ( ! $valid ) { - // translators: %1$s VAT number field label, %2$s VAT number, %3$s Billing Country. - throw new Exception( sprintf( __( 'You have entered an invalid %1$s (%2$s) for your billing country (%3$s).', 'woocommerce-eu-vat-number' ), get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ), $vat_number, $billing_country ) ); - } - - return true; - } - - /** - * Function to save VAT number from the my account form. - */ - public function save_vat_number() { - if ( isset( $_POST['_wpnonce'] ) && wp_verify_nonce( wp_unslash( $_POST['_wpnonce'] ), 'woocommerce-edit_vat_number' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - try { - $current_user_id = get_current_user_id(); - $vat_number = isset( $_POST['vat_number'] ) ? wc_clean( wp_unslash( $_POST['vat_number'] ) ) : ''; - $posted_vat = strtoupper( str_replace( array( ' ', '.', '-', ',', ', ' ), '', $vat_number ) ); - $user = get_userdata( $current_user_id ); - $current_vat = $user->vat_number; - $billing_country = $user->billing_country; - $billing_postcode = $user->billing_postcode; - - $this->validate( $posted_vat, $billing_country, $billing_postcode, $current_vat ); - - update_user_meta( $current_user_id, 'vat_number', $posted_vat ); - update_user_meta( $current_user_id, 'billing_vat_number', $posted_vat ); - - if ( empty( $posted_vat ) ) { - $message = __( 'VAT number removed successfully!', 'woocommerce-eu-vat-number' ); - } elseif ( empty( $current_vat ) ) { - $message = __( 'VAT number saved successfully!', 'woocommerce-eu-vat-number' ); - } else { - $message = __( 'VAT number updated successfully!', 'woocommerce-eu-vat-number' ); - } - $this->messages = array( 'message' => $message, 'status' => 'info' ); - } catch ( Exception $e ) { - $this->messages = array( - 'message' => $e->getMessage(), - 'status' => 'error', - ); - } - } - } -} - -$wc_eu_vat_my_account = new WC_EU_VAT_My_Account(); += 2.6. + add_action( 'init', array( $this, 'add_endpoints' ) ); + add_filter( 'woocommerce_get_query_vars', array( $this, 'add_query_vars' ), 0 ); + + // Change My Account page title. + add_filter( 'the_title', array( $this, 'endpoint_title' ) ); + + // Inserting new tab/page into My Account page. + add_filter( 'woocommerce_account_menu_items', array( $this, 'new_menu_items' ) ); + add_action( 'woocommerce_account_' . $this->endpoint . '_endpoint', array( $this, 'endpoint_content' ) ); + + // Save a VAT number from My Account form if one is submitted. + if ( isset( $_POST['action'] ) && 'edit_vat_number' === wc_clean( wp_unslash( $_POST['action'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verifying nonce inside function. + $this->save_vat_number(); + } + + add_action( 'woocommerce_init', array( $this, 'maybe_remove_vat' ) ); + } + + /** + * Checks to see if we need to remove vat from displaying in the cart and from product itself. + * + * @see https://github.com/woocommerce/woocommerce-eu-vat-number/issues/71 + * @see https://github.com/woocommerce/woocommerce-eu-vat-number/issues/74 + * @see https://github.com/woocommerce/woocommerce-eu-vat-number/issues/233 + */ + public function maybe_remove_vat() { + // Ignore checkout page as on checkout page VAT exempt based on VAT number from billing fields. + if ( ( is_admin() && ! defined( 'DOING_AJAX' ) ) || ! wc_tax_enabled() || is_checkout() || ! is_user_logged_in() ) { + return; + } + + $vat_number = get_user_meta( get_current_user_id(), 'vat_number', true ); + if ( empty( $vat_number ) || empty( WC()->customer ) ) { + return; + } + + // Validate if VAT is valid. If valid, check for VAT exempt. + try { + $billing_country = WC()->customer->get_billing_country(); + $shipping_country = WC()->customer->get_shipping_country(); + + if ( $this->validate( $vat_number, $billing_country ) ) { + WC_EU_VAT_Number::maybe_set_vat_exempt( true, $billing_country, $shipping_country ); + } + } catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch + // Ignore Exception. + } + } + + /** + * Register new endpoint to use inside My Account page. + * + * @since 2.1.12 + * + * @see https://developer.wordpress.org/reference/functions/add_rewrite_endpoint/ + */ + public function add_endpoints() { + add_rewrite_endpoint( $this->endpoint, EP_ROOT | EP_PAGES ); + } + + /** + * Add new query var. + * + * @since 2.1.12 + * + * @param array $vars Query vars. + * @return array + */ + public function add_query_vars( $vars ) { + $vars[] = $this->endpoint; + + return $vars; + } + + /** + * Set endpoint title. + * + * @since 2.1.12 + * + * @param string $title Endpoint title. + * @return string + */ + public function endpoint_title( $title ) { + global $wp_query; + + $is_endpoint = isset( $wp_query->query_vars[ $this->endpoint ] ); + + if ( $is_endpoint && ! is_admin() && is_main_query() && in_the_loop() && is_account_page() ) { + $title = get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ); + + remove_filter( 'the_title', array( $this, 'endpoint_title' ) ); + } + + return $title; + } + + /** + * Insert new endpoint into My Account menu. + * + * @since 2.1.12 + * + * @param array $items Menu items. + * @return array Menu items. + */ + public function new_menu_items( $items ) { + // Remove logout menu item. + $logout = $items['customer-logout']; + unset( $items['customer-logout'] ); + + // Insert VAT Number. + $items[ $this->endpoint ] = get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ); + + // Insert back logout item. + $items['customer-logout'] = $logout; + + return $items; + } + + /** + * Endpoint HTML content. + * + * @since 2.1.12 + */ + public function endpoint_content() { + $this->render_my_vat_number_content(); + } + + /** + * Render My VAT Number content. + * + * @since 2.1.12 + */ + public function render_my_vat_number_content() { + $vars = array( + 'vat_number' => get_user_meta( get_current_user_id(), 'vat_number', true ), + 'messages' => $this->messages, + ); + + wc_get_template( + 'my-account/my-vat-number.php', + $vars, + 'woocommerce-eu-vat-number', + untrailingslashit( plugin_dir_path( WC_EU_VAT_FILE ) ) . '/templates/' + ); + } + + /** + * Validate a VAT number. + * + * @version 2.3.0 + * @since 2.3.0 + * @param string $vat_number VAT number passed by the form. + * @param string $billing_country Billing country of the order. + * @param string $billing_postcode Billing postcode of the order. + * @param string $current_vat VAT number saved in database. + * + * @return boolean + * @throws Exception For invalid VAT Number. + */ + public function validate( $vat_number, $billing_country, $billing_postcode = '', $current_vat = '' ) { + if ( empty( $vat_number ) ) { + if ( empty( $current_vat ) ) { + throw new Exception( __( 'VAT number cannot be empty.', 'woocommerce-eu-vat-number' ) ); + } + // Allow empty input to clear VAT field. + return true; + } + + if ( empty( $billing_country ) ) { + /* translators: 1: VAT Number */ + throw new Exception( sprintf( __( '%1$s can not be validated because the billing country is missing. Please update your billing address.', 'woocommerce-eu-vat-number' ), '' . get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ) . '' ) ); + } + + $valid = WC_EU_VAT_Number::vat_number_is_valid( $vat_number, $billing_country, $billing_postcode ); + + if ( is_wp_error( $valid ) ) { + throw new Exception( $valid->get_error_message() ); + } + + if ( ! $valid ) { + // translators: %1$s VAT number field label, %2$s VAT number, %3$s Billing Country. + throw new Exception( sprintf( __( 'You have entered an invalid %1$s (%2$s) for your billing country (%3$s).', 'woocommerce-eu-vat-number' ), get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ), $vat_number, $billing_country ) ); + } + + return true; + } + + /** + * Function to save VAT number from the my account form. + */ + public function save_vat_number() { + if ( isset( $_POST['_wpnonce'] ) && wp_verify_nonce( wp_unslash( $_POST['_wpnonce'] ), 'woocommerce-edit_vat_number' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + try { + $current_user_id = get_current_user_id(); + $vat_number = isset( $_POST['vat_number'] ) ? wc_clean( wp_unslash( $_POST['vat_number'] ) ) : ''; + $posted_vat = strtoupper( str_replace( array( ' ', '.', '-', ',', ', ' ), '', $vat_number ) ); + $user = get_userdata( $current_user_id ); + $current_vat = $user->vat_number; + $billing_country = $user->billing_country; + $billing_postcode = $user->billing_postcode; + + $this->validate( $posted_vat, $billing_country, $billing_postcode, $current_vat ); + + update_user_meta( $current_user_id, 'vat_number', $posted_vat ); + update_user_meta( $current_user_id, 'billing_vat_number', $posted_vat ); + + if ( empty( $posted_vat ) ) { + $message = __( 'VAT number removed successfully!', 'woocommerce-eu-vat-number' ); + } elseif ( empty( $current_vat ) ) { + $message = __( 'VAT number saved successfully!', 'woocommerce-eu-vat-number' ); + } else { + $message = __( 'VAT number updated successfully!', 'woocommerce-eu-vat-number' ); + } + $this->messages = array( 'message' => $message, 'status' => 'info' ); + } catch ( Exception $e ) { + $this->messages = array( + 'message' => $e->getMessage(), + 'status' => 'error', + ); + } + } + } +} + +$wc_eu_vat_my_account = new WC_EU_VAT_My_Account(); diff --git a/includes/class-wc-eu-vat-number.php b/includes/class-wc-eu-vat-number.php index 62e4e54..cd78df9 100644 --- a/includes/class-wc-eu-vat-number.php +++ b/includes/class-wc-eu-vat-number.php @@ -1,745 +1,781 @@ - 'U[A-Z\d]{8}', - 'BE' => '0\d{9}', - 'BG' => '\d{9,10}', - 'CY' => '\d{8}[A-Z]', - 'CZ' => '\d{8,10}', - 'DE' => '\d{9}', - 'DK' => '(\d{2} ?){3}\d{2}', - 'EE' => '\d{9}', - 'EL' => '\d{9}', - 'ES' => '[A-Z]\d{7}[A-Z]|\d{8}[A-Z]|[A-Z]\d{8}', - 'FI' => '\d{8}', - 'FR' => '([A-Z]{2}|[A-Z0-9]{2})\d{9}', - 'GB' => '\d{9}|\d{12}|(GD|HA)\d{3}', - 'XI' => '\d{9}|\d{12}|(GD|HA)\d{3}', - 'HR' => '\d{11}', - 'HU' => '\d{8}', - 'IE' => '[A-Z\d]{8,10}', - 'IT' => '\d{11}', - 'LT' => '(\d{9}|\d{12})', - 'LU' => '\d{8}', - 'LV' => '\d{11}', - 'MT' => '\d{8}', - 'NL' => '\d{9}B\d{2}', - 'PL' => '\d{10}', - 'PT' => '\d{9}', - 'RO' => '\d{2,10}', - 'SE' => '\d{12}', - 'SI' => '\d{8}', - 'SK' => '\d{10}', - ); - - /** - * VAT Number data. - * - * @var array - */ - private static $data = array( - 'vat_number' => false, - 'validation' => array( - 'valid' => null, - 'error' => false, - ), - ); - - /** - * Stores the current IP Address' country code after geolocation. - * - * @var boolean - */ - private static $ip_country = false; - - /** - * Init. - */ - public static function init() { - // Add fields to checkout process. - add_action( 'wp_enqueue_scripts', array( __CLASS__, 'load_scripts' ) ); - add_filter( 'woocommerce_billing_fields', array( __CLASS__, 'vat_number_field' ) ); - add_action( 'woocommerce_checkout_process', array( __CLASS__, 'process_checkout' ) ); - add_action( 'woocommerce_checkout_update_order_review', array( __CLASS__, 'ajax_update_checkout_totals' ) ); - add_action( 'woocommerce_review_order_before_submit', array( __CLASS__, 'location_confirmation' ) ); - add_action( 'woocommerce_deposits_after_scheduled_order_props_set', array( __CLASS__, 'set_vat_details_for_scheduled_orders' ), 10, 2 ); - - add_action( 'woocommerce_checkout_create_order', array( __CLASS__, 'set_order_data' ) ); - add_action( 'woocommerce_checkout_update_customer', array( __CLASS__, 'set_customer_data' ) ); - add_action( 'woocommerce_create_refund', array( __CLASS__, 'set_refund_data' ) ); - - // Add VAT to addresses. - add_filter( 'woocommerce_order_formatted_billing_address', array( __CLASS__, 'formatted_billing_address' ), 10, 2 ); - add_filter( 'woocommerce_formatted_address_replacements', array( __CLASS__, 'output_company_vat_number' ), 10, 2 ); - add_filter( 'woocommerce_localisation_address_formats', array( __CLASS__, 'localisation_address_formats' ), 10, 2 ); - - // Digital goods taxable location. - add_filter( 'woocommerce_get_tax_location', array( __CLASS__, 'woocommerce_get_tax_location' ), 10, 2 ); - - // Add VAT Number in order endpoint (REST API). - add_filter( 'woocommerce_api_order_response', array( __CLASS__, 'add_vat_number_to_order_response' ) ); - add_filter( 'woocommerce_rest_prepare_shop_order', array( __CLASS__, 'add_vat_number_to_order_response' ) ); - } - - /** - * Load scripts used on the checkout. - */ - public static function load_scripts() { - if ( is_checkout() ) { - $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min'; - - wp_enqueue_script( 'wc-eu-vat', WC_EU_VAT_PLUGIN_URL . '/assets/js/eu-vat' . $suffix . '.js', array( 'jquery', 'wc-checkout' ), WC_EU_VAT_VERSION, true ); - self::localize_wc_eu_vat_params('wc-eu-vat'); - } - } - - public static function localize_wc_eu_vat_params($script_handle) { - wp_localize_script( - $script_handle, - 'wc_eu_vat_params', - array( - 'eu_countries' => self::get_eu_countries(), - 'b2b_required' => get_option( 'woocommerce_eu_vat_number_b2b', 'false' ), - 'input_label' => get_option( 'woocommerce_eu_vat_number_field_label', 'VAT number' ), - 'input_description' => get_option( 'woocommerce_eu_vat_number_field_description', '' ), - 'failure_handler' => get_option( 'woocommerce_eu_vat_number_failure_handling', 'reject' ), - ) - ); - } - - /** - * Get EU Country codes. - * - * @return array - */ - public static function get_eu_countries() { - if ( empty( self::$eu_countries ) ) { - self::$eu_countries = include 'data/eu-country-codes.php'; - } - return self::$eu_countries; - } - - /** - * Reset number. - */ - public static function reset() { - WC()->customer->set_is_vat_exempt( false ); - self::$data = array( - 'vat_number' => false, - 'validation' => array( - 'valid' => null, - 'error' => false, - ), - ); - } - - /** - * Show the VAT field on the checkout. - * - * @since 1.0.0 - * @version 2.3.1 - * @param array $fields Billing Fields. - * @return array - */ - public static function vat_number_field( $fields ) { - $b2b_vat_enabled = get_option( 'woocommerce_eu_vat_number_b2b', 'no' ); - $user_id = get_current_user_id(); - - // If on edit address page, unset vat number field. - if ( is_wc_endpoint_url( 'edit-address' ) ) { - if ( isset( $fields['billing_vat_number'] ) ) { - unset( $fields['billing_vat_number'] ); - } - return $fields; - } - - $fields['billing_vat_number'] = array( - 'label' => get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ), - 'default' => $user_id > 0 ? get_user_meta( $user_id, 'vat_number', true ) : '', - 'required' => false, - 'class' => array( - 'form-row-wide', - ), - 'description' => get_option( 'woocommerce_eu_vat_number_field_description', '' ), - 'id' => 'woocommerce_eu_vat_number', - 'priority' => 120, - ); - - return $fields; - } - - /** - * Return the vat number prefix. - * - * @param string $country Country Code. - * @return string - */ - public static function get_vat_number_prefix( $country ) { - switch ( $country ) { - case 'GR': - $vat_prefix = 'EL'; - break; - case 'MC': - $vat_prefix = 'FR'; - break; - case 'IM': - $vat_prefix = 'GB'; - break; - default: - $vat_prefix = $country; - break; - } - return $vat_prefix; - } - - /** - * Remove unwanted chars and the prefix from a VAT number. - * - * @param string $vat VAT Number. - * @return string - */ - public static function get_formatted_vat_number( $vat ) { - $vat = strtoupper( str_replace( array( ' ', '.', '-', ',', ', ' ), '', $vat ) ); - - if ( in_array( substr( $vat, 0, 2 ), array_merge( self::get_eu_countries(), array( 'EL', 'XI' ) ), true ) ) { - $vat = substr( $vat, 2 ); - } - - return $vat; - } - - /** - * Get IP address country for user. - * - * @return string - */ - public static function get_ip_country() { - if ( false === self::$ip_country ) { - $geoip = WC_Geolocation::geolocate_ip(); - self::$ip_country = $geoip['country']; - } - return self::$ip_country; - } - - /** - * Validate a number. - * - * @param string $vat_number VAT Number. - * @param string $country CountryCode. - * @param string $postcode Postcode. - * @return bool|WP_Error if valid/not valid, WP_ERROR if validation failed - */ - public static function vat_number_is_valid( $vat_number, $country, $postcode = '' ) { - $vat_prefix = self::get_vat_number_prefix( $country ); - $vat_number_formatted = self::get_formatted_vat_number( $vat_number ); - $transient_name = 'vat_number_' . $vat_prefix . $vat_number_formatted; - $cached_result = get_transient( $transient_name ); - - - // Keep supporting prefix 'XI' for Northern Ireland. - if ( 'GB' === $country && ! empty( $postcode ) && preg_match( '/^(bt).*$/i', $postcode ) && 'XI' === substr( $vat_number, 0, 2 ) ) { - $vat_prefix = 'XI'; - } - - // The StoreAPI will set $vat_number to null if the user does not enter it. We should show an error in this case. - if ( null === $vat_number ) { - return new WP_Error( 'api', __( 'VAT number is required.', 'woocommerce-eu-vat-number' ) ); - } - - // Return error if VAT Country Code doesn't match or exist. - if ( ! isset( self::$country_codes_patterns[ $vat_prefix ] ) || ( $vat_prefix . $vat_number_formatted !== $vat_number ) ) { - // translators: %1$s - VAT number field label, %2$s - VAT Number from user, %3$s - Billing country. - return new WP_Error( 'api', sprintf( __( 'You have entered an invalid country code for %1$s (%2$s) for your billing country (%3$s).', 'woocommerce-eu-vat-number' ), get_option( 'woocommerce_eu_vat_number_field_label', 'VAT number' ), $vat_number, $country ) ); - } - - if ( ! empty( $cached_result ) ) { - return 'yes' === $cached_result; - } - - $is_valid = false; - if ( in_array( $country, array( 'GB', 'IM' ), true ) ) { - // For United Kingdom (UK) (Isle of Man included) check VAT number with UK VAT Number API. - try { - $uk_vat_api = new WC_EU_VAT_UK_Number_API(); - $is_valid = $uk_vat_api->check_vat_number( $vat_number_formatted ); - } catch ( Exception $e ) { - return new WP_Error( 'api', __( 'Error communicating with the VAT validation server - please try again.', 'woocommerce-eu-vat-number' ) ); - } - } else { - // Check rest of EU countries with VIES. - $vies = new VIES_Client(); - $soap_client = $vies->get_soap_client(); - - // Return error if any error occurs in getting the SOAP client. - if ( is_wp_error( $soap_client ) ) { - return $soap_client; - } - - if ( $soap_client ) { - try { - $vies_req = $vies->check_vat( $vat_prefix, $vat_number_formatted ); - $is_valid = $vies_req->is_valid(); - - } catch ( SoapFault $e ) { - return new WP_Error( 'api', __( 'Error communicating with the VAT validation server - please try again.', 'woocommerce-eu-vat-number' ) ); - } - } - } - - /** - * Filter whether the VAT number is valid or not. - * - * @since 2.4.2 - * @hook woocommerce_eu_vat_number_is_valid - * - * @param {boolean} $is_valid Whether the VAT number is valid or not. - * @param {string} $vat_number VAT number. - * @param {string} $country Country. - * - * @return {boolean} - */ - $is_valid = apply_filters( 'woocommerce_eu_vat_number_is_valid', $is_valid, $vat_number, $country ); - - set_transient( $transient_name, $is_valid ? 'yes' : 'no', DAY_IN_SECONDS ); - return $is_valid; - } - - /** - * Validate a number and store the result. - * - * @param string $vat_number VAT Number. - * @param string $billing_country Billing CountryCode. - * @param string $billing_postcode Billing PostCode. - * @return void - */ - public static function validate( $vat_number, $billing_country, $billing_postcode = '' ) { - $valid = self::vat_number_is_valid( $vat_number, $billing_country, $billing_postcode ); - $vat_number_formatted = self::get_formatted_vat_number( $vat_number ); - - if ( is_wp_error( $valid ) ) { - self::$data['vat_number'] = $vat_number; - self::$data['validation'] = array( - 'valid' => null, - 'error' => $valid->get_error_message(), - ); - } else { - self::$data['vat_number'] = $valid ? self::get_vat_number_prefix( $billing_country ) . $vat_number_formatted : $vat_number; - self::$data['validation'] = array( - 'valid' => $valid, - 'error' => false, - ); - } - } - - /** - * Whether the base country match with the billing/shipping country. - * - * @param string $billing_country Billing country of customer. - * @param string $shipping_country Shipping country of customer. - * @return bool - */ - public static function is_base_country_match( $billing_country, $shipping_country ) { - /* - * Special handling needs to be done - * for Isle of Man. Technically Isle of Man - * is separate from UK however in the context - * of VAT, it is considered within UK. - * Ref: https://www.gov.im/categories/tax-vat-and-your-money/customs-and-excise/international-trade-and-the-isle-of-man-requirements-and-standards/ - */ - $base_country = WC()->countries->get_base_country(); - $tax_based_on = get_option( 'woocommerce_tax_based_on', 'billing' ); - $base_country_is_uk = in_array( $base_country, array( 'GB', 'IM' ), true ); - - if ( 'billing' === $tax_based_on ) { - if ( $base_country_is_uk && in_array( $billing_country, array( 'GB', 'IM' ), true ) ) { - return true; - } - return ( $base_country === $billing_country ); - } elseif ( 'shipping' === $tax_based_on ) { - if ( $base_country_is_uk && in_array( $shipping_country, array( 'GB', 'IM' ), true ) ) { - return true; - } - return ( $base_country === $shipping_country ); - } - - return in_array( $base_country, array( $billing_country, $shipping_country ), true ); - } - - /** - * Set tax exception based on countries. - * - * @param bool $exempt Are they exempt?. - * @param string $billing_country Billing country of customer. - * @param string $shipping_country Shipping country of customer. - */ - public static function maybe_set_vat_exempt( $exempt, $billing_country, $shipping_country ) { - $base_country_match = self::is_base_country_match( $billing_country, $shipping_country ); - - if ( ( $base_country_match && 'yes' === get_option( 'woocommerce_eu_vat_number_deduct_in_base', 'yes' ) ) || ! $base_country_match ) { - /** - * Filters the VAT exception. - * - * @since 2.3.6 - * - * @param bool $exempt Are they exempt?. - * @param bool $base_country_match Is Base coutry match?. - * @param string $billing_country Billing country of customer. - * @param string $shipping_country Shipping country of customer. - */ - $exempt = apply_filters( 'woocommerce_eu_vat_number_set_is_vat_exempt', $exempt, $base_country_match, $billing_country, $shipping_country ); - WC()->customer->set_is_vat_exempt( $exempt ); - } - } - - /** - * Validate the VAT number when the checkout form is processed. - * - * For B2C transactions, validate the IP only if this is a digital order. - */ - public static function process_checkout() { - self::reset(); - - self::validate_checkout( $_POST, true ); // phpcs:ignore WordPress.Security.NonceVerification.Missing - } - - /** - * See if we need the user to self-declare location. - * - * This is needed when: - * The IP country cannot be detected - * The IP country is inside the EU OR - * The Billing country is inside the EU AND - * The IP doesn't match the billing country. - * - * @param string $ip_country IP Country of customer. - * @param string $billing_country Billig Country code. - * @return boolean - */ - public static function is_self_declaration_required( $ip_country = null, $billing_country = null ) { - if ( is_null( $ip_country ) ) { - $ip_country = self::get_ip_country(); - } - if ( is_null( $billing_country ) ) { - $billing_country = is_callable( array( WC()->customer, 'get_billing_country' ) ) ? WC()->customer->get_billing_country() : WC()->customer->get_country(); - } - - return ( empty( $ip_country ) || in_array( $ip_country, self::get_eu_countries(), true ) || in_array( $billing_country, self::get_eu_countries(), true ) ) && $ip_country !== $billing_country; - } - - /** - * Show checkbox for customer to confirm their location (location evidence for B2C) - */ - public static function location_confirmation() { - if ( 'yes' === get_option( 'woocommerce_eu_vat_number_validate_ip', 'no' ) && self::cart_has_digital_goods() ) { - if ( false === self::$data['vat_number'] && self::is_self_declaration_required() ) { - wc_get_template( - 'location-confirmation-field.php', - array( - 'location_confirmation_is_checked' => isset( $_POST['location_confirmation'] ), // phpcs:ignore WordPress.Security.NonceVerification.Missing - 'countries' => WC()->countries->get_countries(), - ), - 'woocommerce-eu-vat-number', - untrailingslashit( plugin_dir_path( WC_EU_VAT_FILE ) ) . '/templates/' - ); - } - } - } - - /** - * Support method for WooCommerce Deposits. - * - * Sets the VAT related meta whenever a new scheduled order is created. - * - * @param WC_Order $new_order The scheduled order object. - * @param WC_Order $original_order The original order object. - */ - public static function set_vat_details_for_scheduled_orders( $new_order, $original_order ) { - $vat_number = $original_order->get_meta( '_billing_vat_number' ); - $is_vat_exempt = $original_order->get_meta( 'is_vat_exempt' ); - $is_vat_validated = $original_order->get_meta( '_vat_number_is_validated' ); - $is_vat_valid = $original_order->get_meta( '_vat_number_is_valid' ); - - if ( ! empty( $vat_number ) ) { - $new_order->update_meta_data( '_billing_vat_number', $vat_number ); - } - - if ( ! empty( $is_vat_exempt ) ) { - $new_order->update_meta_data( 'is_vat_exempt', $is_vat_exempt ); - } - - if ( ! empty( $is_vat_validated ) ) { - $new_order->update_meta_data( '_vat_number_is_validated', $is_vat_validated ); - } - - if ( ! empty( $is_vat_valid ) ) { - $new_order->update_meta_data( '_vat_number_is_valid', $is_vat_valid ); - } - } - - /** - * Triggered when the totals are updated on the checkout. - * - * @since 1.0.0 - * @version 2.3.1 - * @param array $form_data Checkout Form data. - */ - public static function ajax_update_checkout_totals( $form_data ) { - parse_str( $form_data, $form_data ); - - self::reset(); - - if ( empty( $form_data['billing_country'] ) && empty( $form_data['shipping_country'] ) || empty( $form_data['billing_vat_number'] ) ) { - return; - } - - self::validate_checkout( $form_data ); - } - - /** - * Sees if a cart contains anything non-shippable. Thanks EU, I hate you. - * - * @return bool - */ - public static function cart_has_digital_goods() { - $has_digital_goods = false; - - if ( WC()->cart->get_cart() ) { - foreach ( WC()->cart->get_cart() as $cart_item_key => $values ) { - $_product = $values['data']; - if ( ! $_product->needs_shipping() ) { - $has_digital_goods = true; - } - } - } - - /** - * Filters if cart has digital goods. - * - * @since 2.1.2 - * - * @param bool $has_digital_goods Is it Digital good? - */ - return apply_filters( 'woocommerce_cart_has_digital_goods', $has_digital_goods ); - } - - /** - * Add VAT ID to the formatted address array - * - * @param array $address Address Array. - * @param WC_Order $order WC Order Object. - * @return array - */ - public static function formatted_billing_address( $address, $order ) { - $vat_id = wc_eu_vat_get_vat_from_order( $order ); - - if ( $vat_id ) { - $address['vat_id'] = $vat_id; - } - return $address; - } - - /** - * Add {vat_id} placeholder - * - * @param array $formats Address formats. - * @param array $args Arguments. - * @return array - */ - public static function output_company_vat_number( $formats, $args ) { - if ( isset( $args['vat_id'] ) ) { - /* translators: %s: VAT Number */ - $formats['{vat_id}'] = sprintf( __( 'VAT Number: %s', 'woocommerce-eu-vat-number' ), $args['vat_id'] ); - } else { - $formats['{vat_id}'] = ''; - } - return $formats; - } - - /** - * Address formats. - * - * @param array $formats Address formats. - * @return array - */ - public static function localisation_address_formats( $formats ) { - foreach ( $formats as $key => $format ) { - if ( 'default' === $key || in_array( $key, self::get_eu_countries(), true ) ) { - $formats[ $key ] .= "\n{vat_id}"; - } - } - return $formats; - } - - /** - * Force Digital Goods tax class to use billing address - * - * @param array $location Location. - * @param string $tax_class Tax Class. - * @return array - */ - public static function woocommerce_get_tax_location( $location, $tax_class = '' ) { - if ( ! empty( WC()->customer ) && in_array( sanitize_title( $tax_class ), get_option( 'woocommerce_eu_vat_number_digital_tax_classes', array() ), true ) ) { - return array( - WC()->customer->get_billing_country(), - WC()->customer->get_billing_state(), - WC()->customer->get_billing_postcode(), - WC()->customer->get_billing_city(), - ); - } - return $location; - } - - /** - * Add VAT Number to order endpoint response. - * - * @since 2.1.12 - * - * @param WP_REST_Response $response The response object. - * - * @return WP_REST_Response The response object with VAT number - */ - public static function add_vat_number_to_order_response( $response ) { - if ( is_a( $response, 'WP_REST_Response' ) ) { - $order = wc_get_order( (int) $response->data['id'] ); - $response->data['vat_number'] = $order->get_meta( '_billing_vat_number', true ); - } elseif ( is_array( $response ) && ! empty( $response['id'] ) ) { - // Legacy endpoint. - $order = wc_get_order( (int) $response['id'] ); - $response['vat_number'] = $order->get_meta( '_billing_vat_number', true ); - } - return $response; - } - - /** - * Save VAT Number to the order during checkout (WC 2.7.x). - * - * @param WC_Order $order WC Order. - */ - public static function set_order_data( $order ) { - $order->update_meta_data( '_billing_vat_number', self::$data['vat_number'] ); - $order->update_meta_data( '_vat_number_is_validated', ! is_null( self::$data['validation']['valid'] ) ? 'true' : 'false' ); - $order->update_meta_data( '_vat_number_is_valid', true === self::$data['validation']['valid'] ? 'true' : 'false' ); - - if ( false !== self::get_ip_country() ) { - $order->update_meta_data( '_customer_ip_country', self::get_ip_country() ); - $order->update_meta_data( '_customer_self_declared_country', ! empty( $_POST['location_confirmation'] ) ? 'true' : 'false' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing - } - } - - /** - * Save VAT Number to the customer during checkout (WC 2.7.x). - * - * @param WC_Customer $customer Customer Object. - */ - public static function set_customer_data( $customer ) { - $customer->update_meta_data( 'vat_number', self::$data['vat_number'] ); - } - - /** - * Save VAT Number to the customer during checkout (WC 2.7.x). - * - * @param WC_Order $refund Refund Order. - */ - public static function set_refund_data( $refund ) { - $order = wc_get_order( $refund->get_parent_id() ); - $refund->update_meta_data( '_billing_vat_number', wc_eu_vat_get_vat_from_order( $order ) ); - } - - /** - * Validate AJAX Order Review / Checkout & add errors if any. - * - * @param array $data Checkout field data. - * @param boolean $doing_checkout True if doing checkout. False if AJAX order review. - */ - public static function validate_checkout( $data, $doing_checkout = false ) { - $b2b_vat_enabled = get_option( 'woocommerce_eu_vat_number_b2b', 'no' ); - $fail_handler = get_option( 'woocommerce_eu_vat_number_failure_handling', 'reject' ); - $billing_country = wc_clean( $data['billing_country'] ); - $shipping_country = wc_clean( ! empty( $data['shipping_country'] ) && ! empty( $data['ship_to_different_address'] ) ? $data['shipping_country'] : $data['billing_country'] ); - $billing_vat_number = wc_clean( $data['billing_vat_number'] ); - $billing_postcode = wc_clean( $data['billing_postcode'] ); - - // Replace unwanted chars on VAT Number. - $billing_vat_number = strtoupper( str_replace( array( ' ', '.', '-', ',', ', ' ), '', $billing_vat_number ) ); - - if ( in_array( $billing_country, self::get_eu_countries(), true ) && ! empty( $billing_vat_number ) ) { - self::validate( $billing_vat_number, $billing_country, $billing_postcode ); - - if ( true === (bool) self::$data['validation']['valid'] ) { - self::maybe_set_vat_exempt( true, $billing_country, $shipping_country ); - } else { - switch ( $fail_handler ) { - case 'accept_with_vat': - self::maybe_set_vat_exempt( false, $billing_country, $shipping_country ); - break; - case 'accept': - self::maybe_set_vat_exempt( true, $billing_country, $shipping_country ); - break; - default: - if ( false === self::$data['validation']['valid'] ) { - /* translators: 1: VAT number field label, 2: VAT Number, 3: Billing country */ - wc_add_notice( sprintf( __( 'You have entered an invalid %1$s (%2$s) for your billing country (%3$s).', 'woocommerce-eu-vat-number' ), get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ), self::$data['vat_number'], $billing_country ), 'error' ); - } else { - wc_add_notice( self::$data['validation']['error'], 'error' ); - } - break; - } - } - } - - // If doing checkout, check for additional conditions. - if ( $doing_checkout ) { - if ( in_array( $billing_country, self::get_eu_countries(), true ) && empty( $billing_vat_number ) ) { - if ( 'yes' === $b2b_vat_enabled ) { - /* translators: 1: VAT number field label, 2: Billing country */ - wc_add_notice( sprintf( __( '%1$s is a required field for your billing country (%2$s).', 'woocommerce-eu-vat-number' ), '' . get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ) . '', $billing_country ), 'error' ); - } - - if ( 'yes' === get_option( 'woocommerce_eu_vat_number_validate_ip', 'no' ) && self::cart_has_digital_goods() ) { - if ( self::is_self_declaration_required( self::get_ip_country(), $billing_country ) && empty( $data['location_confirmation'] ) ) { - - /** - * Filters the self declared IP address. - * - * @since 2.1.10 - */ - $ip_address = apply_filters( 'wc_eu_vat_self_declared_ip_address', WC_Geolocation::get_ip_address() ); - /* translators: 1: Ip Address. */ - wc_add_notice( sprintf( __( 'Your IP Address (%1$s) does not match your billing country (%2$s). European VAT laws require your IP address to match your billing country when purchasing digital goods in the EU. Please confirm you are located within your billing country using the checkbox below.', 'woocommerce-eu-vat-number' ), $ip_address, $billing_country ), 'error' ); - } - } - } - } - } -} - -WC_EU_VAT_Number::init(); + 'U[A-Z\d]{8}', + 'BE' => '0\d{9}', + 'BG' => '\d{9,10}', + 'CY' => '\d{8}[A-Z]', + 'CZ' => '\d{8,10}', + 'DE' => '\d{9}', + 'DK' => '(\d{2} ?){3}\d{2}', + 'EE' => '\d{9}', + 'EL' => '\d{9}', + 'ES' => '[A-Z]\d{7}[A-Z]|\d{8}[A-Z]|[A-Z]\d{8}', + 'FI' => '\d{8}', + 'FR' => '([A-Z]{2}|[A-Z0-9]{2})\d{9}', + 'GB' => '\d{9}|\d{12}|(GD|HA)\d{3}', + 'XI' => '\d{9}|\d{12}|(GD|HA)\d{3}', + 'HR' => '\d{11}', + 'HU' => '\d{8}', + 'IE' => '[A-Z\d]{8,10}', + 'IT' => '\d{11}', + 'LT' => '(\d{9}|\d{12})', + 'LU' => '\d{8}', + 'LV' => '\d{11}', + 'MT' => '\d{8}', + 'NL' => '\d{9}B\d{2}', + 'PL' => '\d{10}', + 'PT' => '\d{9}', + 'RO' => '\d{2,10}', + 'SE' => '\d{12}', + 'SI' => '\d{8}', + 'SK' => '\d{10}', + ); + + /** + * VAT Number data. + * + * @var array + */ + private static $data = array( + 'vat_number' => false, + 'validation' => array( + 'valid' => null, + 'error' => false, + ), + ); + + /** + * Stores the current IP Address' country code after geolocation. + * + * @var boolean + */ + private static $ip_country = false; + + /** + * Init. + */ + public static function init() { + // Add fields to checkout process. + add_action( 'wp_enqueue_scripts', array( __CLASS__, 'load_scripts' ) ); + add_filter( 'woocommerce_billing_fields', array( __CLASS__, 'vat_number_field' ) ); + add_action( 'woocommerce_checkout_process', array( __CLASS__, 'process_checkout' ) ); + add_action( 'woocommerce_checkout_update_order_review', array( __CLASS__, 'ajax_update_checkout_totals' ) ); + add_action( 'woocommerce_review_order_before_submit', array( __CLASS__, 'location_confirmation' ) ); + add_action( 'woocommerce_deposits_after_scheduled_order_props_set', array( __CLASS__, 'set_vat_details_for_scheduled_orders' ), 10, 2 ); + + add_action( 'woocommerce_checkout_create_order', array( __CLASS__, 'set_order_data' ) ); + add_action( 'woocommerce_checkout_update_customer', array( __CLASS__, 'set_customer_data' ) ); + add_action( 'woocommerce_create_refund', array( __CLASS__, 'set_refund_data' ) ); + + // Add VAT to addresses. + add_filter( 'woocommerce_order_formatted_billing_address', array( __CLASS__, 'formatted_billing_address' ), 10, 2 ); + add_filter( 'woocommerce_formatted_address_replacements', array( __CLASS__, 'output_company_vat_number' ), 10, 2 ); + add_filter( 'woocommerce_localisation_address_formats', array( __CLASS__, 'localisation_address_formats' ), 10, 2 ); + + // Digital goods taxable location. + add_filter( 'woocommerce_get_tax_location', array( __CLASS__, 'woocommerce_get_tax_location' ), 10, 2 ); + + // Add VAT Number in order endpoint (REST API). + add_filter( 'woocommerce_api_order_response', array( __CLASS__, 'add_vat_number_to_order_response' ) ); + add_filter( 'woocommerce_rest_prepare_shop_order', array( __CLASS__, 'add_vat_number_to_order_response' ) ); + } + + /** + * Load scripts used on the checkout. + */ + public static function load_scripts() { + if ( is_checkout() ) { + $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min'; + + wp_enqueue_script( 'wc-eu-vat', WC_EU_VAT_PLUGIN_URL . '/assets/js/eu-vat' . $suffix . '.js', array( 'jquery', 'wc-checkout' ), WC_EU_VAT_VERSION, true ); + self::localize_wc_eu_vat_params('wc-eu-vat'); + } + } + + public static function localize_wc_eu_vat_params($script_handle) { + wp_localize_script( + $script_handle, + 'wc_eu_vat_params', + array( + 'eu_countries' => self::get_eu_countries(), + 'b2b_required' => get_option( 'woocommerce_eu_vat_number_b2b', 'false' ), + 'input_label' => get_option( 'woocommerce_eu_vat_number_field_label', 'VAT number' ), + 'input_description' => get_option( 'woocommerce_eu_vat_number_field_description', '' ), + 'failure_handler' => get_option( 'woocommerce_eu_vat_number_failure_handling', 'reject' ), + ) + ); + } + + /** + * Get EU Country codes. + * + * @return array + */ + public static function get_eu_countries() { + if ( empty( self::$eu_countries ) ) { + self::$eu_countries = include 'data/eu-country-codes.php'; + } + return self::$eu_countries; + } + + /** + * Reset number. + */ + public static function reset() { + WC()->customer->set_is_vat_exempt( false ); + self::$data = array( + 'vat_number' => false, + 'validation' => array( + 'valid' => null, + 'error' => false, + ), + ); + } + + /** + * Show the VAT field on the checkout. + * + * @since 1.0.0 + * @version 2.3.1 + * @param array $fields Billing Fields. + * @return array + */ + public static function vat_number_field( $fields ) { + $b2b_vat_enabled = get_option( 'woocommerce_eu_vat_number_b2b', 'no' ); + $user_id = get_current_user_id(); + + // If on edit address page, unset vat number field. + if ( is_wc_endpoint_url( 'edit-address' ) ) { + if ( isset( $fields['billing_vat_number'] ) ) { + unset( $fields['billing_vat_number'] ); + } + return $fields; + } + + $fields['billing_vat_number'] = array( + 'label' => get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ), + 'default' => $user_id > 0 ? get_user_meta( $user_id, 'vat_number', true ) : '', + 'required' => false, + 'class' => array( + 'form-row-wide', + ), + 'description' => get_option( 'woocommerce_eu_vat_number_field_description', '' ), + 'id' => 'woocommerce_eu_vat_number', + 'priority' => 120, + ); + + return $fields; + } + + /** + * Return the vat number prefix. + * + * @param string $country Country Code. + * @return string + */ + public static function get_vat_number_prefix( $country ) { + switch ( $country ) { + case 'GR': + $vat_prefix = 'EL'; + break; + case 'MC': + $vat_prefix = 'FR'; + break; + case 'IM': + $vat_prefix = 'GB'; + break; + default: + $vat_prefix = $country; + break; + } + return $vat_prefix; + } + + /** + * Remove unwanted chars and the prefix from a VAT number. + * + * @param string $vat VAT Number. + * @return string + */ + public static function get_formatted_vat_number( $vat ) { + $vat = strtoupper( str_replace( array( ' ', '.', '-', ',', ', ' ), '', $vat ) ); + + if ( in_array( substr( $vat, 0, 2 ), array_merge( self::get_eu_countries(), array( 'EL', 'XI' ) ), true ) ) { + $vat = substr( $vat, 2 ); + } + + return $vat; + } + + /** + * Get IP address country for user. + * + * @return string + */ + public static function get_ip_country() { + if ( false === self::$ip_country ) { + $geoip = WC_Geolocation::geolocate_ip(); + self::$ip_country = $geoip['country']; + } + return self::$ip_country; + } + + /** + * Validate a number. + * + * @param string $vat_number VAT Number. + * @param string $country CountryCode. + * @param string $postcode Postcode. + * @return bool|WP_Error if valid/not valid, WP_ERROR if validation failed + */ + public static function vat_number_is_valid( $vat_number, $country, $postcode = '' ) { + // Replace unwanted chars on VAT Number. + $vat_number = strtoupper( str_replace( array( ' ', '.', '-', ',', ', ' ), '', $vat_number ) ); + + $vat_prefix = self::get_vat_number_prefix( $country ); + $vat_number_formatted = self::get_formatted_vat_number( $vat_number ); + $transient_name = 'vat_number_' . $vat_prefix . $vat_number_formatted; + $cached_result = get_transient( $transient_name ); + + // Keep supporting prefix 'XI' for Northern Ireland. + if ( 'GB' === $country && ! empty( $postcode ) && preg_match( '/^(bt).*$/i', $postcode ) && 'XI' === substr( $vat_number, 0, 2 ) ) { + $vat_prefix = 'XI'; + } + + // The StoreAPI will set $vat_number to null if the user does not enter it. We should show an error in this case. + if ( null === $vat_number ) { + return new WP_Error( 'api', __( 'VAT number is required.', 'woocommerce-eu-vat-number' ) ); + } + + // Return error if VAT Country Code doesn't match or exist. + if ( ! isset( self::$country_codes_patterns[ $vat_prefix ] ) || ( $vat_prefix . $vat_number_formatted !== $vat_number ) ) { + // translators: %1$s - VAT number field label, %2$s - VAT Number from user, %3$s - Billing country. + return new WP_Error( 'api', sprintf( __( 'You have entered an invalid country code for %1$s (%2$s) for your billing country (%3$s).', 'woocommerce-eu-vat-number' ), get_option( 'woocommerce_eu_vat_number_field_label', 'VAT number' ), $vat_number, $country ) ); + } + + if ( ! empty( $cached_result ) ) { + return 'yes' === $cached_result; + } + + $is_valid = false; + if ( in_array( $country, array( 'GB', 'IM' ), true ) ) { + // For United Kingdom (UK) (Isle of Man included) check VAT number with UK VAT Number API. + try { + $uk_vat_api = new WC_EU_VAT_UK_Number_API(); + $is_valid = $uk_vat_api->check_vat_number( $vat_number_formatted ); + } catch ( Exception $e ) { + return new WP_Error( 'api', __( 'Error communicating with the VAT validation server - please try again.', 'woocommerce-eu-vat-number' ) ); + } + } else { + // Check rest of EU countries with VIES. + $vies = new VIES_Client(); + $soap_client = $vies->get_soap_client(); + + // Return error if any error occurs in getting the SOAP client. + if ( is_wp_error( $soap_client ) ) { + return $soap_client; + } + + if ( $soap_client ) { + try { + $vies_req = $vies->check_vat( $vat_prefix, $vat_number_formatted ); + $is_valid = $vies_req->is_valid(); + + } catch ( SoapFault $e ) { + return new WP_Error( 'api', __( 'Error communicating with the VAT validation server - please try again.', 'woocommerce-eu-vat-number' ) ); + } + } + } + + /** + * Filter whether the VAT number is valid or not. + * + * @since 2.4.2 + * @hook woocommerce_eu_vat_number_is_valid + * + * @param {boolean} $is_valid Whether the VAT number is valid or not. + * @param {string} $vat_number VAT number. + * @param {string} $country Country. + * + * @return {boolean} + */ + $is_valid = apply_filters( 'woocommerce_eu_vat_number_is_valid', $is_valid, $vat_number, $country ); + + set_transient( $transient_name, $is_valid ? 'yes' : 'no', DAY_IN_SECONDS ); + return $is_valid; + } + + /** + * Validate a number and store the result. + * + * @param string $vat_number VAT Number. + * @param string $billing_country Billing CountryCode. + * @param string $billing_postcode Billing PostCode. + * @return void + */ + public static function validate( $vat_number, $billing_country, $billing_postcode = '' ) { + $valid = self::vat_number_is_valid( $vat_number, $billing_country, $billing_postcode ); + $vat_number_formatted = self::get_formatted_vat_number( $vat_number ); + + if ( is_wp_error( $valid ) ) { + self::$data['vat_number'] = $vat_number; + self::$data['validation'] = array( + 'valid' => null, + 'error' => $valid->get_error_message(), + ); + } else { + self::$data['vat_number'] = $valid ? self::get_vat_number_prefix( $billing_country ) . $vat_number_formatted : $vat_number; + self::$data['validation'] = array( + 'valid' => $valid, + 'error' => false, + ); + } + } + + /** + * Whether the base country match with the billing/shipping country. + * + * @param string $billing_country Billing country of customer. + * @param string $shipping_country Shipping country of customer. + * @return bool + */ + public static function is_base_country_match( $billing_country, $shipping_country ) { + /* + * Special handling needs to be done + * for Isle of Man. Technically Isle of Man + * is separate from UK however in the context + * of VAT, it is considered within UK. + * Ref: https://www.gov.im/categories/tax-vat-and-your-money/customs-and-excise/international-trade-and-the-isle-of-man-requirements-and-standards/ + */ + $base_country = WC()->countries->get_base_country(); + $tax_based_on = get_option( 'woocommerce_tax_based_on', 'billing' ); + $base_country_is_uk = in_array( $base_country, array( 'GB', 'IM' ), true ); + + if ( 'billing' === $tax_based_on ) { + if ( $base_country_is_uk && in_array( $billing_country, array( 'GB', 'IM' ), true ) ) { + return true; + } + return ( $base_country === $billing_country ); + } elseif ( 'shipping' === $tax_based_on ) { + if ( $base_country_is_uk && in_array( $shipping_country, array( 'GB', 'IM' ), true ) ) { + return true; + } + return ( $base_country === $shipping_country ); + } + + return in_array( $base_country, array( $billing_country, $shipping_country ), true ); + } + + /** + * Set tax exception based on countries. + * + * @param bool $exempt Are they exempt?. + * @param string $billing_country Billing country of customer. + * @param string $shipping_country Shipping country of customer. + */ + public static function maybe_set_vat_exempt( $exempt, $billing_country, $shipping_country ) { + $base_country_match = self::is_base_country_match( $billing_country, $shipping_country ); + + if ( ( $base_country_match && 'yes' === get_option( 'woocommerce_eu_vat_number_deduct_in_base', 'yes' ) ) || ! $base_country_match ) { + /** + * Filters the VAT exception. + * + * @since 2.3.6 + * + * @param bool $exempt Are they exempt?. + * @param bool $base_country_match Is Base coutry match?. + * @param string $billing_country Billing country of customer. + * @param string $shipping_country Shipping country of customer. + */ + $exempt = apply_filters( 'woocommerce_eu_vat_number_set_is_vat_exempt', $exempt, $base_country_match, $billing_country, $shipping_country ); + WC()->customer->set_is_vat_exempt( $exempt ); + } + } + + /** + * Validate the VAT number when the checkout form is processed. + * + * For B2C transactions, validate the IP only if this is a digital order. + */ + public static function process_checkout() { + self::reset(); + + self::validate_checkout( $_POST, true ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + } + + /** + * See if we need the user to self-declare location. + * + * This is needed when: + * The IP country cannot be detected + * The IP country is inside the EU OR + * The Billing country is inside the EU AND + * The IP doesn't match the billing country. + * + * @param string $ip_country IP Country of customer. + * @param string $billing_country Billig Country code. + * @return boolean + */ + public static function is_self_declaration_required( $ip_country = null, $billing_country = null ) { + if ( is_null( $ip_country ) ) { + $ip_country = self::get_ip_country(); + } + if ( is_null( $billing_country ) ) { + $billing_country = is_callable( array( WC()->customer, 'get_billing_country' ) ) ? WC()->customer->get_billing_country() : WC()->customer->get_country(); + } + + return ( empty( $ip_country ) || in_array( $ip_country, self::get_eu_countries(), true ) || in_array( $billing_country, self::get_eu_countries(), true ) ) && $ip_country !== $billing_country; + } + + /** + * Show checkbox for customer to confirm their location (location evidence for B2C) + */ + public static function location_confirmation() { + if ( 'yes' === get_option( 'woocommerce_eu_vat_number_validate_ip', 'no' ) && self::cart_has_digital_goods() ) { + if ( false === self::$data['vat_number'] && self::is_self_declaration_required() ) { + wc_get_template( + 'location-confirmation-field.php', + array( + 'location_confirmation_is_checked' => isset( $_POST['location_confirmation'] ), // phpcs:ignore WordPress.Security.NonceVerification.Missing + 'countries' => WC()->countries->get_countries(), + ), + 'woocommerce-eu-vat-number', + untrailingslashit( plugin_dir_path( WC_EU_VAT_FILE ) ) . '/templates/' + ); + } + } + } + + /** + * Support method for WooCommerce Deposits. + * + * Sets the VAT related meta whenever a new scheduled order is created. + * + * @param WC_Order $new_order The scheduled order object. + * @param WC_Order $original_order The original order object. + */ + public static function set_vat_details_for_scheduled_orders( $new_order, $original_order ) { + $vat_number = $original_order->get_meta( '_billing_vat_number' ); + $is_vat_exempt = $original_order->get_meta( 'is_vat_exempt' ); + $is_vat_validated = $original_order->get_meta( '_vat_number_is_validated' ); + $is_vat_valid = $original_order->get_meta( '_vat_number_is_valid' ); + + if ( ! empty( $vat_number ) ) { + $new_order->update_meta_data( '_billing_vat_number', $vat_number ); + } + + if ( ! empty( $is_vat_exempt ) ) { + $new_order->update_meta_data( 'is_vat_exempt', $is_vat_exempt ); + } + + if ( ! empty( $is_vat_validated ) ) { + $new_order->update_meta_data( '_vat_number_is_validated', $is_vat_validated ); + } + + if ( ! empty( $is_vat_valid ) ) { + $new_order->update_meta_data( '_vat_number_is_valid', $is_vat_valid ); + } + } + + /** + * Triggered when the totals are updated on the checkout. + * + * @since 1.0.0 + * @version 2.3.1 + * @param array $form_data Checkout Form data. + */ + public static function ajax_update_checkout_totals( $form_data ) { + parse_str( $form_data, $form_data ); + + self::reset(); + + if ( empty( $form_data['billing_country'] ) && empty( $form_data['shipping_country'] ) || empty( $form_data['billing_vat_number'] ) ) { + return; + } + + self::validate_checkout( $form_data ); + } + + /** + * Sees if a cart contains anything non-shippable. Thanks EU, I hate you. + * + * @return bool + */ + public static function cart_has_digital_goods() { + $has_digital_goods = false; + + if ( WC()->cart->get_cart() ) { + foreach ( WC()->cart->get_cart() as $cart_item_key => $values ) { + $_product = $values['data']; + if ( ! $_product->needs_shipping() ) { + $has_digital_goods = true; + } + } + } + + /** + * Filters if cart has digital goods. + * + * @since 2.1.2 + * + * @param bool $has_digital_goods Is it Digital good? + */ + return apply_filters( 'woocommerce_cart_has_digital_goods', $has_digital_goods ); + } + + /** + * Add VAT ID to the formatted address array + * + * @param array $address Address Array. + * @param WC_Order $order WC Order Object. + * @return array + */ + public static function formatted_billing_address( $address, $order ) { + $vat_id = wc_eu_vat_get_vat_from_order( $order ); + + if ( $vat_id ) { + $address['vat_id'] = $vat_id; + } + return $address; + } + + /** + * Add {vat_id} placeholder + * + * @param array $formats Address formats. + * @param array $args Arguments. + * @return array + */ + public static function output_company_vat_number( $formats, $args ) { + if ( isset( $args['vat_id'] ) ) { + /* translators: %s: VAT Number */ + $formats['{vat_id}'] = sprintf( __( 'VAT Number: %s', 'woocommerce-eu-vat-number' ), $args['vat_id'] ); + } else { + $formats['{vat_id}'] = ''; + } + return $formats; + } + + /** + * Address formats. + * + * @param array $formats Address formats. + * @return array + */ + public static function localisation_address_formats( $formats ) { + foreach ( $formats as $key => $format ) { + if ( 'default' === $key || in_array( $key, self::get_eu_countries(), true ) ) { + $formats[ $key ] .= "\n{vat_id}"; + } + } + return $formats; + } + + /** + * Force Digital Goods tax class to use billing address + * + * @param array $location Location. + * @param string $tax_class Tax Class. + * @return array + */ + public static function woocommerce_get_tax_location( $location, $tax_class = '' ) { + if ( ! empty( WC()->customer ) && in_array( sanitize_title( $tax_class ), get_option( 'woocommerce_eu_vat_number_digital_tax_classes', array() ), true ) ) { + return array( + WC()->customer->get_billing_country(), + WC()->customer->get_billing_state(), + WC()->customer->get_billing_postcode(), + WC()->customer->get_billing_city(), + ); + } + return $location; + } + + /** + * Add VAT Number to order endpoint response. + * + * @since 2.1.12 + * + * @param WP_REST_Response $response The response object. + * + * @return WP_REST_Response The response object with VAT number + */ + public static function add_vat_number_to_order_response( $response ) { + if ( is_a( $response, 'WP_REST_Response' ) ) { + $order = wc_get_order( (int) $response->data['id'] ); + $response->data['vat_number'] = $order->get_meta( '_billing_vat_number', true ); + } elseif ( is_array( $response ) && ! empty( $response['id'] ) ) { + // Legacy endpoint. + $order = wc_get_order( (int) $response['id'] ); + $response['vat_number'] = $order->get_meta( '_billing_vat_number', true ); + } + return $response; + } + + /** + * Save VAT Number to the order during checkout (WC 2.7.x). + * + * @param WC_Order $order WC Order. + */ + public static function set_order_data( $order ) { + $order->update_meta_data( '_billing_vat_number', self::$data['vat_number'] ); + $order->update_meta_data( '_vat_number_is_validated', ! is_null( self::$data['validation']['valid'] ) ? 'true' : 'false' ); + $order->update_meta_data( '_vat_number_is_valid', true === self::$data['validation']['valid'] ? 'true' : 'false' ); + + if ( false !== self::get_ip_country() ) { + $order->update_meta_data( '_customer_ip_country', self::get_ip_country() ); + $order->update_meta_data( '_customer_self_declared_country', ! empty( $_POST['location_confirmation'] ) ? 'true' : 'false' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + } + } + + /** + * Save VAT Number to the customer during checkout (WC 2.7.x). + * + * @param WC_Customer $customer Customer Object. + */ + public static function set_customer_data( $customer ) { + $customer->update_meta_data( 'vat_number', self::$data['vat_number'] ); + } + + /** + * Save VAT Number to the customer during checkout (WC 2.7.x). + * + * @param WC_Order $refund Refund Order. + */ + public static function set_refund_data( $refund ) { + $order = wc_get_order( $refund->get_parent_id() ); + $refund->update_meta_data( '_billing_vat_number', wc_eu_vat_get_vat_from_order( $order ) ); + } + + /** + * Validate AJAX Order Review / Checkout & add errors if any. + * + * @param array $data Checkout field data. + * @param boolean $doing_checkout True if doing checkout. False if AJAX order review. + */ + public static function validate_checkout( $data, $doing_checkout = false ) { + $b2b_vat_enabled = get_option( 'woocommerce_eu_vat_number_b2b', 'no' ); + $fail_handler = get_option( 'woocommerce_eu_vat_number_failure_handling', 'reject' ); + $billing_country = wc_clean( $data['billing_country'] ); + $shipping_country = wc_clean( ! empty( $data['shipping_country'] ) && ! empty( $data['ship_to_different_address'] ) ? $data['shipping_country'] : $data['billing_country'] ); + $billing_vat_number = wc_clean( $data['billing_vat_number'] ); + $billing_postcode = wc_clean( $data['billing_postcode'] ); + + if ( in_array( $billing_country, self::get_eu_countries(), true ) && ! empty( $billing_vat_number ) ) { + self::validate( $billing_vat_number, $billing_country, $billing_postcode ); + + if ( true === (bool) self::$data['validation']['valid'] ) { + self::maybe_set_vat_exempt( true, $billing_country, $shipping_country ); + } else { + switch ( $fail_handler ) { + case 'accept_with_vat': + self::maybe_set_vat_exempt( false, $billing_country, $shipping_country ); + break; + case 'accept': + self::maybe_set_vat_exempt( true, $billing_country, $shipping_country ); + break; + default: + if ( false === self::$data['validation']['valid'] ) { + /* translators: 1: VAT number field label, 2: VAT Number, 3: Billing country */ + wc_add_notice( sprintf( __( 'You have entered an invalid %1$s (%2$s) for your billing country (%3$s).', 'woocommerce-eu-vat-number' ), get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ), self::$data['vat_number'], $billing_country ), 'error' ); + } else { + wc_add_notice( self::$data['validation']['error'], 'error' ); + } + break; + } + } + } + + // If doing checkout, check for additional conditions. + if ( $doing_checkout ) { + if ( in_array( $billing_country, self::get_eu_countries(), true ) && empty( $billing_vat_number ) ) { + if ( 'yes' === $b2b_vat_enabled ) { + /* translators: 1: VAT number field label, 2: Billing country */ + wc_add_notice( sprintf( __( '%1$s is a required field for your billing country (%2$s).', 'woocommerce-eu-vat-number' ), '' . get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ) . '', $billing_country ), 'error' ); + } + + if ( 'yes' === get_option( 'woocommerce_eu_vat_number_validate_ip', 'no' ) && self::cart_has_digital_goods() ) { + if ( self::is_self_declaration_required( self::get_ip_country(), $billing_country ) && empty( $data['location_confirmation'] ) ) { + + /** + * Filters the self declared IP address. + * + * @since 2.1.10 + */ + $ip_address = apply_filters( 'wc_eu_vat_self_declared_ip_address', WC_Geolocation::get_ip_address() ); + /* translators: 1: Ip Address. */ + wc_add_notice( sprintf( __( 'Your IP Address (%1$s) does not match your billing country (%2$s). European VAT laws require your IP address to match your billing country when purchasing digital goods in the EU. Please confirm you are located within your billing country using the checkbox below.', 'woocommerce-eu-vat-number' ), $ip_address, $billing_country ), 'error' ); + } + } + } + } + } + + /** + * Load script with all required dependencies. + * + * @param string $handler Script handler. + * @param string $script Script name. + * @param array $dependencies Additional dependencies. + * + * @return void + */ + public static function register_script_with_dependencies( string $handler, string $script, array $dependencies = [] ) { + $script_file = $script . '.js'; + $script_src_url = plugins_url( $script_file, __DIR__ ); + $script_asset_path = WC_EU_ABSPATH . $script . '.asset.php'; + $script_asset = file_exists( $script_asset_path ) ? require $script_asset_path : [ 'dependencies' => [] ]; + $script_asset['dependencies'] = array_merge( $script_asset['dependencies'], $dependencies ); + wp_register_script( + $handler, + $script_src_url, + $script_asset['dependencies'], + self::get_file_version( $script_file ), + true + ); + } + + /** + * Gets the file modified time as a cache buster if we're in dev mode, or the plugin version otherwise. + * + * @param string $file Local path to the file. + * @return string The cache buster value to use for the given file. + */ + public static function get_file_version( $file ): string { + if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG && file_exists( WC_EU_ABSPATH . $file ) ) { + return (string) filemtime( WC_EU_ABSPATH . trim( $file, '/' ) ); + } + return WC_EU_VAT_VERSION; + } +} + +WC_EU_VAT_Number::init(); diff --git a/includes/class-wc-eu-vat-privacy.php b/includes/class-wc-eu-vat-privacy.php index 33da520..8f70395 100644 --- a/includes/class-wc-eu-vat-privacy.php +++ b/includes/class-wc-eu-vat-privacy.php @@ -1,293 +1,293 @@ -add_exporter( 'woocommerce-eu-vat-number-order-data', __( 'WooCommerce EU VAT Order Data', 'woocommerce-eu-vat-number' ), array( $this, 'order_data_exporter' ) ); - $this->add_exporter( 'woocommerce-eu-vat-number-customer-data', __( 'WooCommerce EU VAT Customer Data', 'woocommerce-eu-vat-number' ), array( $this, 'customer_data_exporter' ) ); - - $this->add_eraser( 'woocommerce-eu-vat-number-customer-data', __( 'WooCommerce EU VAT Customer Data', 'woocommerce-eu-vat-number' ), array( $this, 'customer_data_eraser' ) ); - $this->add_eraser( 'woocommerce-eu-vat-number-order-data', __( 'WooCommerce EU VAT Order Data', 'woocommerce-eu-vat-number' ), array( $this, 'order_data_eraser' ) ); - - if ( class_exists( 'WC_Subscriptions' ) || class_exists( 'WC_Subscriptions_Core_Plugin' ) ) { - $this->add_eraser( 'woocommerce-eu-vat-number-subscriptions-data', __( 'WooCommerce EU VAT Subscriptions Data', 'woocommerce-eu-vat-number' ), array( $this, 'subscriptions_data_eraser' ) ); - } - } - - /** - * Returns a list of orders. - * - * @param string $email_address Email address of customer. - * @param int $page Current page. - * @param string $post_type Default: order. Set to 'subscription' if querying for subscription orders. - * - * @return array WP_Post - */ - protected function get_orders( $email_address, $page, $post_type = 'order' ) { - $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data. - - // Default Query Args. - $order_query = array( - 'status' => 'any', - 'limit' => 10, - 'page' => $page, - ); - - // Additional query args to retrieve WooCommerce orders. - if ( $user instanceof WP_User ) { - $order_query['customer_id'] = (int) $user->ID; - } else { - $order_query['billing_email'] = $email_address; - } - - // Using wc_get_orders for WC orders & data store for subscriptions. - if ( 'subscription' === $post_type ) { - $result = WC_Data_Store::load( $post_type )->get_orders( $order_query ); - } else { - $result = wc_get_orders( $order_query ); - } - - return $result; - } - - /** - * Gets the message of the privacy to display. - */ - public function get_privacy_message() { - // translators: Privacy Docs URL. - return wpautop( sprintf( __( 'By using this extension, you may be storing personal data or sharing data with an external service. Learn more about how this works, including what you may want to include in your privacy policy.', 'woocommerce-eu-vat-number' ), 'https://docs.woocommerce.com/document/marketplace-privacy/#woocommerce-eu-vat-number' ) ); - } - - /** - * Handle exporting data for Orders. - * - * @param string $email_address E-mail address to export. - * @param int $page Pagination of data. - * - * @return array - */ - public function order_data_exporter( $email_address, $page = 1 ) { - $done = false; - $data_to_export = array(); - - $orders = $this->get_orders( $email_address, (int) $page ); - - $done = true; - - if ( 0 < count( $orders ) ) { - - /** @var \WC_Order $order */ - foreach ( $orders as $order ) { - $data_to_export[] = array( - 'group_id' => 'woocommerce_orders', - 'group_label' => __( 'Orders', 'woocommerce-eu-vat-number' ), - 'item_id' => 'order-' . $order->get_id(), - 'data' => array( - array( - 'name' => __( 'EU VAT number', 'woocommerce-eu-vat-number' ), - 'value' => $order->get_meta( '_vat_number', true ), - ), - array( - 'name' => __( 'EU Billing VAT number', 'woocommerce-eu-vat-number' ), - 'value' => $order->get_meta( '_billing_vat_number', true ), - ), - array( - 'name' => __( 'EU VAT country', 'woocommerce-eu-vat-number' ), - 'value' => $order->get_meta( '_customer_ip_country', true ), - ), - ), - ); - } - - $done = 10 > count( $orders ); - } - - return array( - 'data' => $data_to_export, - 'done' => $done, - ); - } - - /** - * Finds and exports customer data by email address. - * - * @since 3.4.0 - * @param string $email_address The user email address. - * @param int $page Page. - * - * @return array An array of personal data in name value pairs - */ - public function customer_data_exporter( $email_address, $page ) { - $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data. - $data_to_export = array(); - - if ( $user instanceof WP_User ) { - $data_to_export[] = array( - 'group_id' => 'woocommerce_customer', - 'group_label' => __( 'Customer Data', 'woocommerce-eu-vat-number' ), - 'item_id' => 'user', - 'data' => array( - array( - 'name' => get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ), - 'value' => get_user_meta( $user->ID, 'vat_number', true ), - ), - ), - ); - } - - return array( - 'data' => $data_to_export, - 'done' => true, - ); - } - - /** - * Finds and erases customer data by email address. - * - * @since 3.4.0 - * @param string $email_address The user email address. - * @param int $page Page. - * - * @return array An array of personal data in name value pairs - */ - public function customer_data_eraser( $email_address, $page ) { - $page = (int) $page; - $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data. - - $vat_number = get_user_meta( $user->ID, 'vat_number', true ); - - $items_removed = false; - $messages = array(); - - if ( ! empty( $vat_number ) ) { - $items_removed = true; - delete_user_meta( $user->ID, 'vat_number' ); - $messages[] = __( 'EU VAT User Data Erased.', 'woocommerce-eu-vat-number' ); - } - - return array( - 'items_removed' => $items_removed, - 'items_retained' => false, - 'messages' => $messages, - 'done' => true, - ); - } - - /** - * Finds and erases order data by email address. - * - * @since 3.4.0 - * @param string $email_address The user email address. - * @param int $page Page. - * @return array An array of personal data in name value pairs - */ - public function order_data_eraser( $email_address, $page ) { - $orders = $this->get_orders( $email_address, (int) $page ); - return $this->maybe_erase_order_data( $orders ); - } - - /** - * Finds and erases Subscription order data by email address. - * - * @since 2.3.26 - * - * @param string $email_address The user email address. - * @param int $page Page. - * @return array An array of personal data in name value pairs. - */ - public function subscriptions_data_eraser( $email_address, $page ) { - $subscriptions = $this->get_orders( $email_address, (int) $page, 'subscription' ); - return $this->maybe_erase_order_data( $subscriptions ); - } - - /** - * Erase EU VAT data from WooCommerce/Subscription orders. - * - * @since 2.3.26 - * - * @param array $orders WooCommerce orders. - * - * @return array - */ - protected function maybe_erase_order_data( $orders ) { - $items_removed = false; - $items_retained = false; - $messages = array(); - $done = true; - - foreach ( (array) $orders as $order ) { - list( $removed, $retained, $msgs ) = $this->maybe_handle_order( $order ); - - $items_removed |= $removed; - $items_retained |= $retained; - $messages = array_merge( $messages, $msgs ); - } - - // Tell core if we have more orders to work on still. - $done = count( $orders ) < 10; - - return array( - 'items_removed' => $items_removed, - 'items_retained' => $items_retained, - 'messages' => $messages, - 'done' => $done, - ); - } - - /** - * Handle eraser of data tied to Orders - * - * @param WC_Order $order WooCommerce order/subscription object. - * - * @return array Array of order data removed/retained along with corresponding message. - */ - protected function maybe_handle_order( $order ) { - $order_id = $order->get_id(); - $message = array(); - - $vat_number = $order->get_meta( '_billing_vat_number', true ); - $vat_number = ( ! empty( $vat_number ) ) ? $vat_number : $order->get_meta( '_vat_number', true ); // Compat with version < 2.3.21. - $ip_country = $order->get_meta( '_customer_ip_country', true ); - - if ( empty( $vat_number ) && empty( $ip_country ) ) { - return array( false, false, array() ); - } - - $order->delete_meta_data( '_billing_vat_number' ); - $order->delete_meta_data( '_vat_number' ); // Compat with version < 2.3.21. - $order->delete_meta_data( '_customer_ip_country' ); - - $order->save(); - - // Set default post type to 'order'. - $post_type = 'order'; - - // Check whether the order is a subscription. - if ( function_exists( 'wcs_is_subscription' ) && wcs_is_subscription( $order ) ) { - $post_type = 'subscription'; - } - - // translators: Post type, Order ID. - $message[] = sprintf( __( 'Removed EU VAT data from %1$s %2$s.', 'woocommerce-eu-vat-number' ), $post_type, $order_id ); - - return array( true, false, $message ); - } -} - -new WC_EU_VAT_Privacy(); +add_exporter( 'woocommerce-eu-vat-number-order-data', __( 'WooCommerce EU VAT Order Data', 'woocommerce-eu-vat-number' ), array( $this, 'order_data_exporter' ) ); + $this->add_exporter( 'woocommerce-eu-vat-number-customer-data', __( 'WooCommerce EU VAT Customer Data', 'woocommerce-eu-vat-number' ), array( $this, 'customer_data_exporter' ) ); + + $this->add_eraser( 'woocommerce-eu-vat-number-customer-data', __( 'WooCommerce EU VAT Customer Data', 'woocommerce-eu-vat-number' ), array( $this, 'customer_data_eraser' ) ); + $this->add_eraser( 'woocommerce-eu-vat-number-order-data', __( 'WooCommerce EU VAT Order Data', 'woocommerce-eu-vat-number' ), array( $this, 'order_data_eraser' ) ); + + if ( class_exists( 'WC_Subscriptions' ) || class_exists( 'WC_Subscriptions_Core_Plugin' ) ) { + $this->add_eraser( 'woocommerce-eu-vat-number-subscriptions-data', __( 'WooCommerce EU VAT Subscriptions Data', 'woocommerce-eu-vat-number' ), array( $this, 'subscriptions_data_eraser' ) ); + } + } + + /** + * Returns a list of orders. + * + * @param string $email_address Email address of customer. + * @param int $page Current page. + * @param string $post_type Default: order. Set to 'subscription' if querying for subscription orders. + * + * @return array WP_Post + */ + protected function get_orders( $email_address, $page, $post_type = 'order' ) { + $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data. + + // Default Query Args. + $order_query = array( + 'status' => 'any', + 'limit' => 10, + 'page' => $page, + ); + + // Additional query args to retrieve WooCommerce orders. + if ( $user instanceof WP_User ) { + $order_query['customer_id'] = (int) $user->ID; + } else { + $order_query['billing_email'] = $email_address; + } + + // Using wc_get_orders for WC orders & data store for subscriptions. + if ( 'subscription' === $post_type ) { + $result = WC_Data_Store::load( $post_type )->get_orders( $order_query ); + } else { + $result = wc_get_orders( $order_query ); + } + + return $result; + } + + /** + * Gets the message of the privacy to display. + */ + public function get_privacy_message() { + // translators: Privacy Docs URL. + return wpautop( sprintf( __( 'By using this extension, you may be storing personal data or sharing data with an external service. Learn more about how this works, including what you may want to include in your privacy policy.', 'woocommerce-eu-vat-number' ), 'https://docs.woocommerce.com/document/marketplace-privacy/#woocommerce-eu-vat-number' ) ); + } + + /** + * Handle exporting data for Orders. + * + * @param string $email_address E-mail address to export. + * @param int $page Pagination of data. + * + * @return array + */ + public function order_data_exporter( $email_address, $page = 1 ) { + $done = false; + $data_to_export = array(); + + $orders = $this->get_orders( $email_address, (int) $page ); + + $done = true; + + if ( 0 < count( $orders ) ) { + + /** @var \WC_Order $order */ + foreach ( $orders as $order ) { + $data_to_export[] = array( + 'group_id' => 'woocommerce_orders', + 'group_label' => __( 'Orders', 'woocommerce-eu-vat-number' ), + 'item_id' => 'order-' . $order->get_id(), + 'data' => array( + array( + 'name' => __( 'EU VAT number', 'woocommerce-eu-vat-number' ), + 'value' => $order->get_meta( '_vat_number', true ), + ), + array( + 'name' => __( 'EU Billing VAT number', 'woocommerce-eu-vat-number' ), + 'value' => $order->get_meta( '_billing_vat_number', true ), + ), + array( + 'name' => __( 'EU VAT country', 'woocommerce-eu-vat-number' ), + 'value' => $order->get_meta( '_customer_ip_country', true ), + ), + ), + ); + } + + $done = 10 > count( $orders ); + } + + return array( + 'data' => $data_to_export, + 'done' => $done, + ); + } + + /** + * Finds and exports customer data by email address. + * + * @since 3.4.0 + * @param string $email_address The user email address. + * @param int $page Page. + * + * @return array An array of personal data in name value pairs + */ + public function customer_data_exporter( $email_address, $page ) { + $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data. + $data_to_export = array(); + + if ( $user instanceof WP_User ) { + $data_to_export[] = array( + 'group_id' => 'woocommerce_customer', + 'group_label' => __( 'Customer Data', 'woocommerce-eu-vat-number' ), + 'item_id' => 'user', + 'data' => array( + array( + 'name' => get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ), + 'value' => get_user_meta( $user->ID, 'vat_number', true ), + ), + ), + ); + } + + return array( + 'data' => $data_to_export, + 'done' => true, + ); + } + + /** + * Finds and erases customer data by email address. + * + * @since 3.4.0 + * @param string $email_address The user email address. + * @param int $page Page. + * + * @return array An array of personal data in name value pairs + */ + public function customer_data_eraser( $email_address, $page ) { + $page = (int) $page; + $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data. + + $vat_number = get_user_meta( $user->ID, 'vat_number', true ); + + $items_removed = false; + $messages = array(); + + if ( ! empty( $vat_number ) ) { + $items_removed = true; + delete_user_meta( $user->ID, 'vat_number' ); + $messages[] = __( 'EU VAT User Data Erased.', 'woocommerce-eu-vat-number' ); + } + + return array( + 'items_removed' => $items_removed, + 'items_retained' => false, + 'messages' => $messages, + 'done' => true, + ); + } + + /** + * Finds and erases order data by email address. + * + * @since 3.4.0 + * @param string $email_address The user email address. + * @param int $page Page. + * @return array An array of personal data in name value pairs + */ + public function order_data_eraser( $email_address, $page ) { + $orders = $this->get_orders( $email_address, (int) $page ); + return $this->maybe_erase_order_data( $orders ); + } + + /** + * Finds and erases Subscription order data by email address. + * + * @since 2.3.26 + * + * @param string $email_address The user email address. + * @param int $page Page. + * @return array An array of personal data in name value pairs. + */ + public function subscriptions_data_eraser( $email_address, $page ) { + $subscriptions = $this->get_orders( $email_address, (int) $page, 'subscription' ); + return $this->maybe_erase_order_data( $subscriptions ); + } + + /** + * Erase EU VAT data from WooCommerce/Subscription orders. + * + * @since 2.3.26 + * + * @param array $orders WooCommerce orders. + * + * @return array + */ + protected function maybe_erase_order_data( $orders ) { + $items_removed = false; + $items_retained = false; + $messages = array(); + $done = true; + + foreach ( (array) $orders as $order ) { + list( $removed, $retained, $msgs ) = $this->maybe_handle_order( $order ); + + $items_removed |= $removed; + $items_retained |= $retained; + $messages = array_merge( $messages, $msgs ); + } + + // Tell core if we have more orders to work on still. + $done = count( $orders ) < 10; + + return array( + 'items_removed' => $items_removed, + 'items_retained' => $items_retained, + 'messages' => $messages, + 'done' => $done, + ); + } + + /** + * Handle eraser of data tied to Orders + * + * @param WC_Order $order WooCommerce order/subscription object. + * + * @return array Array of order data removed/retained along with corresponding message. + */ + protected function maybe_handle_order( $order ) { + $order_id = $order->get_id(); + $message = array(); + + $vat_number = $order->get_meta( '_billing_vat_number', true ); + $vat_number = ( ! empty( $vat_number ) ) ? $vat_number : $order->get_meta( '_vat_number', true ); // Compat with version < 2.3.21. + $ip_country = $order->get_meta( '_customer_ip_country', true ); + + if ( empty( $vat_number ) && empty( $ip_country ) ) { + return array( false, false, array() ); + } + + $order->delete_meta_data( '_billing_vat_number' ); + $order->delete_meta_data( '_vat_number' ); // Compat with version < 2.3.21. + $order->delete_meta_data( '_customer_ip_country' ); + + $order->save(); + + // Set default post type to 'order'. + $post_type = 'order'; + + // Check whether the order is a subscription. + if ( function_exists( 'wcs_is_subscription' ) && wcs_is_subscription( $order ) ) { + $post_type = 'subscription'; + } + + // translators: Post type, Order ID. + $message[] = sprintf( __( 'Removed EU VAT data from %1$s %2$s.', 'woocommerce-eu-vat-number' ), $post_type, $order_id ); + + return array( true, false, $message ); + } +} + +new WC_EU_VAT_Privacy(); diff --git a/includes/class-wc-eu-vat-report-ec-sales-list.php b/includes/class-wc-eu-vat-report-ec-sales-list.php index 273cfcd..d757c60 100644 --- a/includes/class-wc-eu-vat-report-ec-sales-list.php +++ b/includes/class-wc-eu-vat-report-ec-sales-list.php @@ -1,244 +1,244 @@ - - - - - __( 'Previous Quarter', 'woocommerce-eu-vat-number' ), - 'last_quarter' => __( 'Last Quarter', 'woocommerce-eu-vat-number' ), - 'quarter' => __( 'This Quarter', 'woocommerce-eu-vat-number' ), - ); - - $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : 'quarter'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended - - if ( ! in_array( $current_range, array( 'custom', 'prev_quarter', 'last_quarter', 'quarter' ), true ) ) { - $current_range = 'quarter'; - } - - $this->calculate_current_range( $current_range ); - - $hide_sidebar = true; - - include WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php'; - } - - /** - * Get the current range and calculate the start and end dates - * - * @param string $current_range Report Range. - */ - public function calculate_current_range( $current_range ) { - $this->chart_groupby = 'month'; - $quarter = absint( ceil( date( 'm', current_time( 'timestamp' ) ) / 3 ) ); - $year = absint( date( 'Y', current_time( 'timestamp' ) ) ); - - switch ( $current_range ) { - case 'prev_quarter': - $quarter = $quarter - 2; - if ( 0 === $quarter ) { - $quarter = 4; - $year --; - } elseif ( -1 === $quarter ) { - $quarter = 3; - $year --; - } - break; - case 'last_quarter': - --$quarter; - if ( 0 === $quarter ) { - $quarter = 4; - $year --; - } - break; - case 'custom': - parent::calculate_current_range( $current_range ); - return; - } - - if ( 1 === $quarter ) { - $this->start_date = strtotime( $year . '-01-01' ); - $this->end_date = strtotime( date( 'Y-m-t', strtotime( $year . '-03-01' ) ) ); - } elseif ( 2 === $quarter ) { - $this->start_date = strtotime( $year . '-04-01' ); - $this->end_date = strtotime( date( 'Y-m-t', strtotime( $year . '-06-01' ) ) ); - } elseif ( 3 === $quarter ) { - $this->start_date = strtotime( $year . '-07-01' ); - $this->end_date = strtotime( date( 'Y-m-t', strtotime( $year . '-09-01' ) ) ); - } elseif ( 4 === $quarter ) { - $this->start_date = strtotime( $year . '-10-01' ); - $this->end_date = strtotime( date( 'Y-m-t', strtotime( $year . '-12-01' ) ) ); - } - } - - /** - * Get the main chart - * - * @return void - */ - public function get_main_chart() { - $ec_sales1 = $this->get_order_report_data( - array( - 'data' => array( - '_order_total' => array( - 'type' => 'meta', - 'function' => 'SUM', - 'name' => 'total_sales', - ), - '_billing_vat_number' => array( - 'type' => 'meta', - 'function' => '', - 'name' => '_billing_vat_number', - ), - '_billing_country' => array( - 'type' => 'meta', - 'function' => '', - 'name' => '_billing_country', - ), - '_order_currency' => array( - 'type' => 'meta', - 'function' => '', - 'name' => '_order_currency', - ), - ), - 'where' => array( - array( - 'key' => 'meta__billing_country.meta_value', - 'value' => WC_EU_VAT_Number::get_eu_countries(), - 'operator' => 'in', - ), - array( - 'key' => 'meta__billing_vat_number.meta_value', - 'value' => '', - 'operator' => '!=', - ), - ), - 'group_by' => '_billing_vat_number', - 'order_by' => '_billing_vat_number ASC', - 'query_type' => 'get_results', - 'order_status' => array( 'completed' ), - 'filter_range' => true, - ) - ); - $ec_sales2 = $this->get_order_report_data( - array( - 'data' => array( - '_order_total' => array( - 'type' => 'meta', - 'function' => 'SUM', - 'name' => 'total_sales', - ), - '_vat_number' => array( - 'type' => 'meta', - 'function' => '', - 'name' => '_vat_number', - ), - '_billing_country' => array( - 'type' => 'meta', - 'function' => '', - 'name' => '_billing_country', - ), - '_order_currency' => array( - 'type' => 'meta', - 'function' => '', - 'name' => '_order_currency', - ), - ), - 'where' => array( - array( - 'key' => 'meta__billing_country.meta_value', - 'value' => WC_EU_VAT_Number::get_eu_countries(), - 'operator' => 'in', - ), - array( - 'key' => 'meta__vat_number.meta_value', - 'value' => '', - 'operator' => '!=', - ), - ), - 'group_by' => '_vat_number', - 'order_by' => '_vat_number ASC', - 'query_type' => 'get_results', - 'order_status' => array( 'completed' ), - 'filter_range' => true, - ) - ); - - $ec_sales = array_merge( $ec_sales1, $ec_sales2 ); - ?> -- | - | - |
---|---|---|
_billing_country ); ?> | -_billing_country, '', $vat_number ) ); ?> | -total_sales, array( 'currency', $ec_sale->_order_currency ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> | -
- |
+ | + | + |
---|---|---|
_billing_country ); ?> | +_billing_country, '', $vat_number ) ); ?> | +total_sales, array( 'currency', $ec_sale->_order_currency ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> | +
+ |
- | - | - | - | - | - | - | - | - |
---|---|---|---|---|---|---|---|---|
countries->countries[ $rate->tax_rate_country ] ); ?> | -- | % | -amount ); ?> | -refunded_amount * -1 ); ?> | -amount + $tax_row->refunded_amount ); ?> | -tax_amount ); ?> | -refunded_tax_amount * -1 ); ?> | -tax_amount + $tax_row->refunded_tax_amount ); ?> | -
- |
+ | + | + | + | + | + | + | + | + |
---|---|---|---|---|---|---|---|---|
countries->countries[ $rate->tax_rate_country ] ); ?> | ++ | % | +amount ); ?> | +refunded_amount * -1 ); ?> | +amount + $tax_row->refunded_amount ); ?> | +tax_amount ); ?> | +refunded_tax_amount * -1 ); ?> | +tax_amount + $tax_row->refunded_tax_amount ); ?> | +
+ |
- | - | - | - | - | - | - | - | - |
---|---|---|---|---|---|---|---|---|
countries->countries[ $country ] ); ?> | -- | - | amount ); ?> | -refunded_amount * -1 ); ?> | -amount + $tax_row->refunded_amount ); ?> | -tax_amount ); ?> | -refunded_tax_amount * -1 ); ?> | -tax_amount + $tax_row->refunded_tax_amount ); ?> | -
- | - | - | - | - | - | - | - | - |
- |
+ | + | + | + | + | + | + | + | + |
---|---|---|---|---|---|---|---|---|
countries->countries[ $country ] ); ?> | ++ | + | amount ); ?> | +refunded_amount * -1 ); ?> | +amount + $tax_row->refunded_amount ); ?> | +tax_amount ); ?> | +refunded_tax_amount * -1 ); ?> | +tax_amount + $tax_row->refunded_tax_amount ); ?> | +
+ | + | + | + | + | + | + | + | + |
+ |
-
+