You are here

vat_number.inc in VAT Number 7

File

vat_number.inc
View source
<?php

/**
 * @file
 *  vat_number.inc
 *
 * Validating a VAT by structure
 *
 * VAT structure
 *
 * http://ec.europa.eu/taxation_customs/vies/faq.html  |
 *   official eu page, chosen for inline docs.
 * http://en.wikipedia.org/wiki/VAT_identification_number
 * http://www.advsofteng.com/vatid.html
 * http://pages.ebay.co.uk/help/account/vat-id.html
 * http://www.hmrc.gov.uk/vat/managing/international/esl/country-codes.htm
 *
 * Countries
 *
 * http://europa.eu/about-eu/countries/index_de.htm
 */

/*******************************************************************************
 *                        HELPER FUNCTIONS for EU VAT processing, testing
 ******************************************************************************/

/**
 * Pre check, to output good error messages that can help the user.
 *
 * @param integer $vat_number
 *   VAT number to test e.g. GB123 4567 89
 *
 * @return array
 *   with vat values
 *      county_code, for example DE ES FR
 *      vat_number, the rest after the country code
 *      vat_format, 0 meaning error, > 1 means format valid
 *
 */
function _vat_number_check_vat_format($vat_number) {
  $vat_infos = array();
  $vat_infos = _vat_number_components($vat_number);

  // Check for a country code.
  switch ($vat_infos['country_code']) {

    /* info on the regexes in general. /^(AT){0,1}U[0-9]{8}$/i'; --- we use {0,1} because eventually we want to provide the country later by a second field formatter so tahts no error, but the function           needs to be rewriten for that*/

    // Austria.
    case 'AT':

      // ATU99999999
      // 1 block of 9 characters.
      $format['AT'] = t("AT U + 8 characters");
      $example['AT'] = "AT U12345678";
      $regex = '/^(AT){0,1}U[0-9]{8}$/i';
      break;

    // Belgium.
    case 'BE':

      // BE0999999999
      // 1 block of 10 digits.
      $format['BE'] = t("BE 0 (zero) + 9 digits");
      $example['BE'] = "BE 0123456789";
      $regex = '/^(BE){0,1}[0]{1}[0-9]{9}$/i';
      break;

    // Bulgaria.
    case 'BG':

      // BG999999999 or BG9999999999
      // 1 block of 9 digits or 1 block of 10 digits.
      $format['BG'] = t("BG + 9 or 10 digits");
      $example['BG'] = "BG 123456789, BG 1234567890";
      $regex = '/^(BG){0,1}[0-9]{9,10}$/i';
      break;

    // Cyprus.
    case 'CY':

      // CY99999999L
      // 1 block of 9 characters, ATTENTION L is a letter.
      $format['CY'] = t("CY + 8 digits + 1 letter");
      $example['CY'] = "CY 12345678A";
      $regex = '/^(CY){0,1}[0-9]{8}[A-Z]{1}$/i';
      break;

    // Czech Republic.
    case 'CZ':

      // CZ99999999 or CZ999999999 or CZ9999999999
      // 1 block of either 8, 9 or 10 digits.
      $format['CZ'] = t("CZ + 8, 9 or 10 digits");
      $example['CZ'] = "CZ 12345678, CZ 123456789, CZ 1234567890";
      $regex = '/^(CZ){0,1}[0-9]{8,10}$/i';
      break;

    // Denmark.
    case 'DK':

      // DK99 99 99 99
      // 4 blocks of 2 digits.
      $format['DK'] = t("DK + 4 blocks of 2 digits");
      $example['DK'] = "DK 12 34 56 78";
      $regex = '/^(DK){0,1}([0-9]){8}$/i';
      break;

    // Estonia.
    case 'EE':

      // EE999999999
      // 1 block of 9 digits.
      $format['EE'] = t("EE + 1 block of 9 digits");
      $example['EE'] = "EE 123456789";
      $regex = '/^(EE){0,1}[0-9]{9}$/i';
      break;

    // Germany.
    case 'DE':

      // DE999999999
      // 1 block of 9 digits.
      $format['DE'] = t("DE + 1 block of 9 digits");
      $example['DE'] = "DE 123456789";
      $regex = '/^(DE){0,1}[0-9]{9}$/i';
      break;

    // Greece.
    case 'EL':

      // EL999999999
      // 1 block of 9 digits.
      // ATTENTION: The country code in the VAT ID number (EL) differs from the country's official ISO 3166-1 alpha-2 country code (GR)
      $format['EL'] = t("EL + 1 block of 9 digits");
      $example['EL'] = "EL 123456789";
      $regex = '/^(EL){0,1}[0-9]{9}$/i';
      break;

    // Portugal.
    case 'PT':

      // PT999999999
      // 1 block of 9 digits.
      $format['PT'] = "PT + 1 block of 9 digits";
      $example['PT'] = "PT 123456789";
      $regex = '/^(PT){0,1}[0-9]{9}$/i';
      break;

    // France.
    case 'FR':

      // FRXX 999999999
      // 1 block of 2 characters, 1 block of 9 digits,
      // X is a letter or a digit.
      $format['FR'] = t("FR + 1 block of 2 characters + 1 block of 9 digits");
      $example['FR'] = "FR A0 123456789";
      $regex = '/^(FR){0,1}[0-9A-Z]{2}[0-9]{9}$/i';
      break;

    // Finland.
    case 'FI':

      // FI99999999
      // 1 block of 8 digits.
      $format['FI'] = t("FI + 1 block of 8 digits");
      $example['FI'] = "FI 12345678";
      $regex = '/^(FI){0,1}[0-9]{8}$/i';
      break;

    // Croatia.
    case 'HR':

      // HR12345678901
      // 1 block of 11 digits.
      $format['HR'] = t("HR + 1 block of 11 digits");
      $example['HR'] = "HR 12345678901";
      $regex = '/^(HR){0,1}[0-9]{11}$/i';
      break;

    // Hungary.
    case 'HU':

      // HU99999999
      // 1 block of 8 digits.
      $format['HU'] = t("HU + 1 block of 8 digits");
      $example['HU'] = "HU 12345678";
      $regex = '/^(HU){0,1}[0-9]{8}$/i';
      break;

    // Luxembourg.
    case 'LU':

      // LU99999999
      // 1 block of 8 digits.
      $format['LU'] = t("LU + 1 block of 8 digits");
      $example['LU'] = "LU 12345678";
      $regex = '/^(LU){0,1}[0-9]{8}$/i';
      break;

    // Malta.
    case 'MT':

      // MT99999999
      // 1 block of 8 digits.
      $format['MT'] = t("MT + 1 block of 8 digits");
      $example['MT'] = "MT 12345678";
      $regex = '/^(MT){0,1}[0-9]{8}$/i';
      break;

    // Slovenia.
    case 'SI':

      // SI12345678
      // 1 block of 8 digits.
      $format['SI'] = t("SI + 1 block of 8 digits");
      $example['SI'] = "SI 12345678";
      $regex = '/^(SI){0,1}[0-9]{8}$/i';
      break;

    // Ireland.
    case 'IE':

      // IE9S99999L OR IE9S99999LL
      // 1 block of 8 or 9 characters,
      // S is a letter, a digit, "+" or "*";
      // L is a letter.
      $format['IE'] = t("IE + 1 block of 8 or 9 characters. The second character can be a letter, a digit, \"+\" or \"*\"; the last one or two is a letter.");
      $example['IE'] = "IE 1X23456A";
      $regex = '/^(IE){0,1}[0-9][0-9A-Z\\+\\*][0-9]{5}[A-Z]{1,2}$/i';
      break;

    // Italy.
    case 'IT':

      // IT99999999999
      // 1 block of 11 digits.
      $format['IT'] = t("IT + 1 block of 11 digits");
      $example['IT'] = "IT 12345678901";
      $regex = '/^(IT){0,1}[0-9]{11}$/i';
      break;

    // Latvia.
    case 'LV':

      // LV99999999999
      // 1 block of 11 digits.
      $format['LV'] = t("LV + 1 block of 11 digits");
      $example['LV'] = "LV 12345678901";
      $regex = '/^(LV){0,1}[0-9]{11}$/i';
      break;

    // Lithuania.
    case 'LT':

      // LT999999999 or LT999999999999
      // 1 block of 9 digits, or 1 block of 12 digits.
      $format['LT'] = t("LT + 1 block of 9 or 12 digits");
      $example['LT'] = "LT 123456789, LT 123456789012";
      $regex = '/^(LT){0,1}([0-9]{9}|[0-9]{12})$/i';
      break;

    // Netherlands.
    case 'NL':

      // NL999999999B99
      // 1 block of 12 characters,
      // The 10th position following the prefix is always "B".
      $format['NL'] = t("NL + 1 block of 12 characters, where the 10th character is a \"B\"");
      $example['NL'] = "NL 123456789B01";
      $regex = '/^(NL){0,1}[0-9]{9}B[0-9]{2}$/i';
      break;

    // Poland.
    case 'PL':

      // PL9999999999
      // 1 block of 10 digits.
      $format['PL'] = t("PL + 1 block of 10 digits");
      $example['PL'] = "PL 1234567890";
      $regex = '/^(PL){0,1}[0-9]{10}$/i';
      break;

    // Slovakia.
    case 'SK':

      // SK9999999999
      // 1 block of 10 digits.
      $format['SK'] = t("SK + 1 block of 10 digits");
      $example['SK'] = "SK 1234567890";
      $regex = '/^(SK){0,1}[0-9]{10}$/i';
      break;

    // Romania.
    case 'RO':

      // RO999999999
      // 1 block of minimum 2 digits and maximum 10 digits.
      $format['RO'] = t("RO + 1 block of 2 to 10 digits");
      $example['RO'] = "RO 1234567890";
      $regex = '/^(RO){0,1}[0-9]{2,10}$/i';
      break;

    // Sweden.
    case 'SE':

      // SE999999999999
      // 1 block of 12 digits.
      $format['SE'] = t("SE + 1 block of 12 digits");
      $example['SE'] = "SE 123456789012";
      $regex = '/^(SE){0,1}[0-9]{12}$/i';
      break;

    // Spain.
    case 'ES':

      // ESX9999999X
      // 1 block of 9 characters.
      // The first and last character (X) may be a letter or a digit,
      // but they may not both be digits.
      $format['ES'] = t("ES + 1 block of 9 characters. The first and last character may be a letter.");
      $example['ES'] = "ES A1234567B";
      $regex = '/^(ES){0,1}([0-9A-Z][0-9]{7}[A-Z])|([A-Z][0-9]{7}[0-9A-Z])$/i';
      break;

    // United Kingdom.
    case 'GB':

      // GB999 9999 99      standard:
      // - 9 digits (block of 3, block of 4, block of 2).
      // GB999 9999 99 999  branch traders: 12 digits
      // - (as for 9 digits, followed by a block of 3 digits).
      // GBGD999            government departments:
      // - the letters GD then 3 digits orom 000 to 499 (e.g. GBGD001).
      // GBHA999            health authorities:
      // - the letters HA then 3 digits from 500 to 999 (e.g. GBHA599).
      $format['GB'] = t("GB + 9 or 12 digits; or GB GD + 3 digits; or GB HA + 3 digits");
      $example['GB'] = "GB 999 9999 99, GB 999 9999 99 999, GB GD123, GB HA789";
      $regex = '/^(GB){0,1}(([0-9]{9})|([0-9]{12})|((GD|HA)[0-9]{3}))$/i';
      break;
    default:

      // No valid country code, return all invalid data.
      $vat_infos['vat_format'] = 0;
      $vat_infos['message'] = t("The country of the vat can not be detected. Please do not remove the language Prefix. Example: DE123456789 (where DE is the country prefix)");
      return $vat_infos;
      break;
  }

  // OK now check if the regex matched the supplied vat.
  $vat_number = $vat_infos['country_code'] . $vat_infos['vat_number'];
  $vat_infos['vat_format'] = preg_match($regex, $vat_number);

  // Output a message with an info about wrong frong format if a
  // country code was in front but regex validation does not match
  // in case the user added too many numbers or forgot something else.
  // Help hi mand display a message about formatting information.
  $eu_countries = _vat_number_eu_countries();

  // The country code in the VAT ID number for Greece is "EL" instead of the country's official ISO 3166-1 alpha-2 country code ("GR")
  // => Replace the key "GR" in the $eu_countries array by "EL"
  $eu_countries['EL'] = $eu_countries['GR'];
  unset($eu_countries['GR']);
  ksort($eu_countries);

  // Valid EU country code.
  $valid_eu = isset($eu_countries[$vat_infos['country_code']]);
  if ($valid_eu && !$vat_infos['vat_format']) {
    $vat_infos['message'] = t('You have entered a VAT ID number for the country %country, but it\'s format is incorrect.', array(
      '%country' => $eu_countries[$vat_infos['country_code']],
    )) . '<br />' . t('It must have the following format: %format', array(
      '%format' => $format[$vat_infos['country_code']],
    )) . '<br />' . t('Example:') . ' <strong>' . $example[$vat_infos['country_code']] . '</strong>';
  }
  return $vat_infos;
}

/**
 * A list of valid countrys of the EU
 *
 * @return array
 *   a list of eu countrys, key is country code, value is readable name
 */
function _vat_number_eu_countries() {

  // Necessary for country_get_list().
  require_once DRUPAL_ROOT . '/includes/locale.inc';
  $countries = country_get_list();

  // ISO 3166-1 alpha-2 country codes
  $eu_country_codes = array(
    "AT",
    "BE",
    "BG",
    "CY",
    "CZ",
    "DE",
    "DK",
    "EE",
    "ES",
    "FI",
    "FR",
    "GB",
    "GR",
    "HR",
    "HU",
    "IE",
    "IT",
    "LT",
    "LU",
    "LV",
    "MT",
    "NL",
    "PL",
    "PT",
    "RO",
    "SE",
    "SI",
    "SK",
  );

  // Merge in country names from country_get_list().
  foreach ($eu_country_codes as $key => $value) {
    $eu_countries[$value] = $countries[$value];
  }
  return $eu_countries;
}

/**
 * Check a VAT number. Return error message on error, FALSE on success.
 */
function _vat_number_validate_vat($vat_number, $skip_validation_on_service_failure = FALSE) {
  $vat_infos = _vat_number_components($vat_number);
  $client = _vat_number_connect_vies_database();
  if ($client) {
    $params = array(
      'countryCode' => $vat_infos['country_code'],
      'vatNumber' => $vat_infos['vat_number'],
    );
    try {
      $r = $client
        ->checkVat($params);
      return $r->valid == TRUE ? TRUE : FALSE;
    } catch (SoapFault $e) {
      watchdog('vat_number', $e->faultstring, NULL, WATCHDOG_ERROR);
      if ($skip_validation_on_service_failure) {

        // Something is wrong, but that's fine. Pass the validation..
        return TRUE;
      }
    }
  }
  elseif ($skip_validation_on_service_failure) {

    // We couldn't connect but we are allowed to pass the validation.
    return TRUE;
  }
  else {

    // No $client and can't skip.
    drupal_set_message(t('The connection to the European VAT database server failed and the VAT could not be validated, please contact us about this problem.'), 'error');
    return FALSE;
  }
}

/**
 * Try to connect with the VIES database. Catch PHP warnings if connection with
 * the host is not possible.
 */
function _vat_number_connect_vies_database() {
  if (!class_exists('SoapClient')) {
    drupal_set_message(t('The PHP SOAP extension is not installed on this server. The VAT ID could not be validated.'), 'error');
    return FALSE;
  }
  try {

    // Initialize the SoapClient options.
    $options = array(
      'exceptions' => TRUE,
    );

    // The VIES load balance servers require the User-Agent HTTP header, but PHP
    // versions 5.3 before 5.3.11 and version 5.4.0 contain a bug where the
    // SoapClient class ignores the user_agent option. In that case we must use
    // the PHP configuration to get it to send the User-Agent HTTP header.
    if (version_compare(PHP_VERSION, '5.3.11', '<') || version_compare(PHP_VERSION, '5.4.0', '=')) {

      // Set the PHP configuration option if it isn't already set.
      if (!ini_get('user_agent')) {
        ini_set('user_agent', 'PHP');
      }
    }
    else {

      // Set the SoapClient option, as the bug is not present.
      $options += array(
        'user_agent' => 'PHP',
      );
    }
    return @new SoapClient('http://ec.europa.eu/taxation_customs/vies/checkVatService.wsdl', $options);
  } catch (SoapFault $e) {

    // Connection to host not possible, europe.eu down?
    watchdog('vat_number', $e->faultstring, NULL, WATCHDOG_ERROR);
  }
}

/**
 * Splitting up a VAT to country prefix and number and clean it from spaces etc.
 */
function _vat_number_components($vat_number) {

  // Some countries like DK use spaces in the formatting.
  // Maybe someone does that too for readability or uses dots.
  // We remove all dots, spaces and dashes because they are not
  // important for the validation operations and we do NOT regex spaces.
  $vatid = preg_replace('/[ .-]/', '', $vat_number);

  // First two letters are always country code.
  $vat_infos['country_code'] = drupal_strtoupper(drupal_substr($vatid, 0, 2));
  $vat_infos['vat_number'] = drupal_strtoupper(drupal_substr($vatid, 2));
  return $vat_infos;
}

Functions

Namesort descending Description
_vat_number_check_vat_format Pre check, to output good error messages that can help the user.
_vat_number_components Splitting up a VAT to country prefix and number and clean it from spaces etc.
_vat_number_connect_vies_database Try to connect with the VIES database. Catch PHP warnings if connection with the host is not possible.
_vat_number_eu_countries A list of valid countrys of the EU
_vat_number_validate_vat Check a VAT number. Return error message on error, FALSE on success.