# easy-invoice/2.4.3/includes/Services/ViesValidator.php

Easy Invoice – Invoice Generator, PDF Quotes &amp; Payments, version 2.4.3. 227 lines.

- Page: https://pluginprobe.com/plugins/easy-invoice/2.4.3/code/includes/Services/ViesValidator.php
- Raw: https://pluginprobe.com/plugins/easy-invoice/2.4.3/raw/includes/Services/ViesValidator.php
- Modified: 2026-09-15T12:31:20+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/easy-invoice/2.4.3/code/includes/Services/ViesValidator.php#L10-L20`.

```php
<?php
/**
 * Checks a VAT number against the EU's own register.
 *
 * @package Easy_Invoice
 * @subpackage Services
 */

namespace EasyInvoice\Services;

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * Asks VIES whether a VAT identifier is really registered.
 *
 * Why a syntax check is not enough
 * --------------------------------
 * `TaxTreatment::looksLikeValidVat()` only says the number is shaped like a VAT
 * identifier. Whether it is *registered* is a different question, and it is the
 * one that matters: an intra-EU reverse-charge invoice is only valid if the
 * customer really is VAT-registered in another member state. Get it wrong and
 * the supplier owes the VAT they did not charge — the exposure lands on the
 * merchant, not the customer.
 *
 * VIES is the European Commission's own register and the evidence a tax
 * authority expects. It needs no key and no account.
 *
 * "Invalid" and "could not check" are not the same
 * ------------------------------------------------
 * VIES goes down, and individual member states' registers go down
 * independently of it. Reporting an outage as "this VAT number is invalid"
 * would be worse than not checking at all, because a merchant would act on it
 * and charge VAT they should not have. So an unreachable service, a malformed
 * request, or a member state that did not answer all return a WP_Error saying
 * the check could not be made. `valid => false` is returned only when VIES
 * positively said the number is not registered.
 *
 * Advisory, never blocking
 * ------------------------
 * Nothing here decides whether an invoice can be saved or what tax it carries.
 * A merchant with a customer whose registration VIES cannot confirm today still
 * has an invoice to send. This tells them what the register says; the decision
 * stays theirs.
 */
class ViesValidator {

    /** The Commission's REST endpoint. No key, no account. */
    const ENDPOINT = 'https://ec.europa.eu/taxation_customs/vies/rest-api/ms/%s/vat/%s';

    /** Transient prefix for cached answers. */
    const CACHE_PREFIX = 'ei_vies_';

    /** How long a confirmed registration is trusted. */
    const CACHE_VALID = WEEK_IN_SECONDS;

    /** How long a confirmed non-registration is trusted — shorter, because a
     *  business registering for VAT is the change that matters. */
    const CACHE_INVALID = DAY_IN_SECONDS;

    /**
     * Can VIES answer for this country at all?
     *
     * @param string $country ISO 3166-1 alpha-2 code.
     * @return bool
     */
    public static function isSupportedCountry( string $country ): bool {
        return TaxTreatment::isEu( $country );
    }

    /**
     * Ask VIES about a VAT number.
     *
     * @param string $vat     VAT identifier, with or without its country prefix.
     * @param string $country ISO 3166-1 alpha-2 code. Taken from the number when empty.
     * @param bool   $refresh Skip the cache.
     * @return array{valid:bool,name:string,address:string,country:string,number:string,checked_at:string,cached:bool}|\WP_Error
     */
    public static function check( string $vat, string $country = '', bool $refresh = false ) {
        $vat = strtoupper( preg_replace( '/[^A-Za-z0-9]/', '', $vat ) );

        if ( '' === $vat ) {
            return new \WP_Error( 'easy_invoice_vies_empty', __( 'Enter a VAT number to check.', 'easy-invoice' ) );
        }

        // A number usually carries its own country prefix; fall back to the one
        // recorded against the document when it does not.
        if ( preg_match( '/^([A-Z]{2})([A-Z0-9]{2,13})$/', $vat, $m ) ) {
            $country = '' !== $country ? $country : $m[1];
            $number  = $m[2];
            $prefix  = $m[1];
        } else {
            $number = $vat;
            $prefix = '';
        }

        $country = strtoupper( trim( $country ) );

        if ( '' === $country ) {
            return new \WP_Error(
                'easy_invoice_vies_no_country',
                __( 'That VAT number has no country prefix, and no country is recorded for this customer.', 'easy-invoice' )
            );
        }

        if ( ! self::isSupportedCountry( $country ) ) {
            return new \WP_Error(
                'easy_invoice_vies_not_eu',
                sprintf(
                    /* translators: %s: country code. */
                    __( 'VIES only covers the EU VAT area, so it cannot check a %s number.', 'easy-invoice' ),
                    $country
                )
            );
        }

        // Greece files VAT under EL while its ISO country code is GR. VIES wants
        // the tax prefix, so a customer recorded as GR would otherwise never
        // validate.
        $ms = 'GR' === $country ? 'EL' : $country;

        // Strip a prefix that duplicates the member state, but keep one that
        // does not — that is a mismatch worth reporting rather than hiding.
        if ( '' !== $prefix && $prefix !== $ms && ! ( 'EL' === $ms && 'GR' === $prefix ) ) {
            return new \WP_Error(
                'easy_invoice_vies_country_mismatch',
                sprintf(
                    /* translators: 1: prefix on the number, 2: country recorded. */
                    __( 'This VAT number starts with %1$s but the customer is recorded as being in %2$s.', 'easy-invoice' ),
                    $prefix,
                    $country
                )
            );
        }

        $cache_key = self::CACHE_PREFIX . md5( $ms . '|' . $number );

        if ( ! $refresh ) {
            $cached = get_transient( $cache_key );
            if ( is_array( $cached ) ) {
                $cached['cached'] = true;
                return $cached;
            }
        }

        $response = wp_remote_get(
            sprintf( self::ENDPOINT, rawurlencode( $ms ), rawurlencode( $number ) ),
            [
                'timeout' => 15,
                // Never relaxed. This is a check whose answer changes what tax a
                // merchant charges, so the identity of who answered it matters.
                'sslverify' => true,
                'headers'   => [ 'Accept' => 'application/json' ],
            ]
        );

        if ( is_wp_error( $response ) ) {
            return new \WP_Error(
                'easy_invoice_vies_unreachable',
                __( 'The EU VAT register could not be reached, so this number has not been checked. It has not been found invalid — try again shortly.', 'easy-invoice' )
            );
        }

        $code = (int) wp_remote_retrieve_response_code( $response );
        $body = json_decode( (string) wp_remote_retrieve_body( $response ), true );

        if ( 200 !== $code || ! is_array( $body ) ) {
            return new \WP_Error(
                'easy_invoice_vies_bad_response',
                sprintf(
                    /* translators: %d: HTTP status code. */
                    __( 'The EU VAT register answered unexpectedly (HTTP %d), so this number has not been checked.', 'easy-invoice' ),
                    $code
                )
            );
        }

        $user_error = isset( $body['userError'] ) ? (string) $body['userError'] : '';

        // Anything other than a straight VALID/INVALID is the service telling us
        // it could not answer, not telling us the number is bad.
        if ( '' !== $user_error && ! in_array( $user_error, [ 'VALID', 'INVALID' ], true ) ) {
            return new \WP_Error(
                'easy_invoice_vies_indeterminate',
                sprintf(
                    /* translators: %s: status reported by VIES. */
                    __( 'The EU VAT register could not answer for this number (%s), so it has not been checked. It has not been found invalid.', 'easy-invoice' ),
                    $user_error
                ),
                [ 'user_error' => $user_error ]
            );
        }

        $valid = ! empty( $body['isValid'] );

        // Several member states return "---" rather than disclosing the trader's
        // name; that is a policy choice, not missing data, so it is not shown.
        $name    = isset( $body['name'] ) ? trim( (string) $body['name'] ) : '';
        $address = isset( $body['address'] ) ? trim( (string) $body['address'] ) : '';
        $name    = ( '---' === $name ) ? '' : $name;
        $address = ( '---' === $address ) ? '' : $address;

        $result = [
            'valid'      => $valid,
            'name'       => $name,
            'address'    => $address,
            'country'    => $country,
            'number'     => $ms . $number,
            'checked_at' => current_time( 'mysql' ),
            'cached'     => false,
        ];

        set_transient( $cache_key, $result, $valid ? self::CACHE_VALID : self::CACHE_INVALID );

        /**
         * Fires after a VAT number has been checked against VIES.
         *
         * @param array  $result The answer.
         * @param string $vat    The number as supplied.
         */
        do_action( 'easy_invoice_vies_checked', $result, $vat );

        return $result;
    }
}

```
