Vat.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Customer\Model;
  7. use Magento\Framework\App\Config\ScopeConfigInterface;
  8. use Magento\Framework\DataObject;
  9. use Magento\Framework\Exception\LocalizedException;
  10. use Magento\Store\Model\Store;
  11. use Magento\Store\Model\Information as StoreInformation;
  12. use Psr\Log\LoggerInterface as PsrLogger;
  13. use Magento\Store\Model\ScopeInterface;
  14. /**
  15. * Customer VAT model
  16. */
  17. class Vat
  18. {
  19. /**
  20. * Config paths to VAT related customer groups
  21. */
  22. const XML_PATH_CUSTOMER_VIV_INTRA_UNION_GROUP = 'customer/create_account/viv_intra_union_group';
  23. const XML_PATH_CUSTOMER_VIV_DOMESTIC_GROUP = 'customer/create_account/viv_domestic_group';
  24. const XML_PATH_CUSTOMER_VIV_INVALID_GROUP = 'customer/create_account/viv_invalid_group';
  25. const XML_PATH_CUSTOMER_VIV_ERROR_GROUP = 'customer/create_account/viv_error_group';
  26. /**
  27. * VAT class constants
  28. */
  29. const VAT_CLASS_DOMESTIC = 'domestic';
  30. const VAT_CLASS_INTRA_UNION = 'intra_union';
  31. const VAT_CLASS_INVALID = 'invalid';
  32. const VAT_CLASS_ERROR = 'error';
  33. /**
  34. * WSDL of VAT validation service
  35. *
  36. */
  37. const VAT_VALIDATION_WSDL_URL = 'http://ec.europa.eu/taxation_customs/vies/services/checkVatService?wsdl';
  38. /**
  39. * Config path to option that enables/disables automatic group assignment based on VAT
  40. */
  41. const XML_PATH_CUSTOMER_GROUP_AUTO_ASSIGN = 'customer/create_account/auto_group_assign';
  42. /**
  43. * Config path to UE country list
  44. */
  45. const XML_PATH_EU_COUNTRIES_LIST = 'general/country/eu_countries';
  46. /**
  47. * @var ScopeConfigInterface
  48. */
  49. protected $scopeConfig;
  50. /**
  51. * @var PsrLogger
  52. */
  53. protected $logger;
  54. /**
  55. * @param ScopeConfigInterface $scopeConfig
  56. * @param PsrLogger $logger
  57. */
  58. public function __construct(
  59. ScopeConfigInterface $scopeConfig,
  60. PsrLogger $logger
  61. ) {
  62. $this->scopeConfig = $scopeConfig;
  63. $this->logger = $logger;
  64. }
  65. /**
  66. * Retrieve merchant country code
  67. *
  68. * @param Store|string|int|null $store
  69. * @return string
  70. */
  71. public function getMerchantCountryCode($store = null)
  72. {
  73. return (string)$this->scopeConfig->getValue(
  74. StoreInformation::XML_PATH_STORE_INFO_COUNTRY_CODE,
  75. ScopeInterface::SCOPE_STORE,
  76. $store
  77. );
  78. }
  79. /**
  80. * Retrieve merchant VAT number
  81. *
  82. * @param Store|string|int|null $store
  83. * @return string
  84. */
  85. public function getMerchantVatNumber($store = null)
  86. {
  87. return (string)$this->scopeConfig->getValue(
  88. StoreInformation::XML_PATH_STORE_INFO_VAT_NUMBER,
  89. ScopeInterface::SCOPE_STORE,
  90. $store
  91. );
  92. }
  93. /**
  94. * Retrieve customer group ID based on his VAT number
  95. *
  96. * @param string $customerCountryCode
  97. * @param DataObject $vatValidationResult
  98. * @param \Magento\Store\Model\Store|string|int $store
  99. * @return null|int
  100. */
  101. public function getCustomerGroupIdBasedOnVatNumber($customerCountryCode, $vatValidationResult, $store = null)
  102. {
  103. $groupId = null;
  104. $isAutoGroupAssign = $this->scopeConfig->isSetFlag(
  105. self::XML_PATH_CUSTOMER_GROUP_AUTO_ASSIGN,
  106. ScopeInterface::SCOPE_STORE,
  107. $store
  108. );
  109. if (!$isAutoGroupAssign) {
  110. return $groupId;
  111. }
  112. $vatClass = $this->getCustomerVatClass($customerCountryCode, $vatValidationResult, $store);
  113. $vatClassToGroupXmlPathMap = [
  114. self::VAT_CLASS_DOMESTIC => self::XML_PATH_CUSTOMER_VIV_DOMESTIC_GROUP,
  115. self::VAT_CLASS_INTRA_UNION => self::XML_PATH_CUSTOMER_VIV_INTRA_UNION_GROUP,
  116. self::VAT_CLASS_INVALID => self::XML_PATH_CUSTOMER_VIV_INVALID_GROUP,
  117. self::VAT_CLASS_ERROR => self::XML_PATH_CUSTOMER_VIV_ERROR_GROUP,
  118. ];
  119. if (isset($vatClassToGroupXmlPathMap[$vatClass])) {
  120. $groupId = (int)$this->scopeConfig->getValue(
  121. $vatClassToGroupXmlPathMap[$vatClass],
  122. ScopeInterface::SCOPE_STORE,
  123. $store
  124. );
  125. }
  126. return $groupId;
  127. }
  128. /**
  129. * Send request to VAT validation service and return validation result
  130. *
  131. * @param string $countryCode
  132. * @param string $vatNumber
  133. * @param string $requesterCountryCode
  134. * @param string $requesterVatNumber
  135. *
  136. * @return DataObject
  137. */
  138. public function checkVatNumber($countryCode, $vatNumber, $requesterCountryCode = '', $requesterVatNumber = '')
  139. {
  140. // Default response
  141. $gatewayResponse = new DataObject([
  142. 'is_valid' => false,
  143. 'request_date' => '',
  144. 'request_identifier' => '',
  145. 'request_success' => false,
  146. 'request_message' => __('Error during VAT Number verification.'),
  147. ]);
  148. if (!extension_loaded('soap')) {
  149. $this->logger->critical(new LocalizedException(__('PHP SOAP extension is required.')));
  150. return $gatewayResponse;
  151. }
  152. if (!$this->canCheckVatNumber($countryCode, $vatNumber, $requesterCountryCode, $requesterVatNumber)) {
  153. return $gatewayResponse;
  154. }
  155. $countryCodeForVatNumber = $this->getCountryCodeForVatNumber($countryCode);
  156. $requesterCountryCodeForVatNumber = $this->getCountryCodeForVatNumber($requesterCountryCode);
  157. try {
  158. $soapClient = $this->createVatNumberValidationSoapClient();
  159. $requestParams = [];
  160. $requestParams['countryCode'] = $countryCodeForVatNumber;
  161. $vatNumberSanitized = $this->isCountryInEU($countryCode)
  162. ? str_replace([' ', '-', $countryCodeForVatNumber], ['', '', ''], $vatNumber)
  163. : str_replace([' ', '-'], ['', ''], $vatNumber);
  164. $requestParams['vatNumber'] = $vatNumberSanitized;
  165. $requestParams['requesterCountryCode'] = $requesterCountryCodeForVatNumber;
  166. $reqVatNumSanitized = $this->isCountryInEU($requesterCountryCode)
  167. ? str_replace([' ', '-', $requesterCountryCodeForVatNumber], ['', '', ''], $requesterVatNumber)
  168. : str_replace([' ', '-'], ['', ''], $requesterVatNumber);
  169. $requestParams['requesterVatNumber'] = $reqVatNumSanitized;
  170. // Send request to service
  171. $result = $soapClient->checkVatApprox($requestParams);
  172. $gatewayResponse->setIsValid((bool)$result->valid);
  173. $gatewayResponse->setRequestDate((string)$result->requestDate);
  174. $gatewayResponse->setRequestIdentifier((string)$result->requestIdentifier);
  175. $gatewayResponse->setRequestSuccess(true);
  176. if ($gatewayResponse->getIsValid()) {
  177. $gatewayResponse->setRequestMessage(__('VAT Number is valid.'));
  178. } else {
  179. $gatewayResponse->setRequestMessage(__('Please enter a valid VAT number.'));
  180. }
  181. } catch (\Exception $exception) {
  182. $gatewayResponse->setIsValid(false);
  183. $gatewayResponse->setRequestDate('');
  184. $gatewayResponse->setRequestIdentifier('');
  185. }
  186. return $gatewayResponse;
  187. }
  188. /**
  189. * Create SOAP client based on VAT validation service WSDL
  190. *
  191. * @param boolean $trace
  192. * @return \SoapClient
  193. */
  194. protected function createVatNumberValidationSoapClient($trace = false)
  195. {
  196. return new \SoapClient(self::VAT_VALIDATION_WSDL_URL, ['trace' => $trace]);
  197. }
  198. /**
  199. * Check if parameters are valid to send to VAT validation service
  200. *
  201. * @param string $countryCode
  202. * @param string $vatNumber
  203. * @param string $requesterCountryCode
  204. * @param string $requesterVatNumber
  205. *
  206. * @return boolean
  207. *
  208. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  209. */
  210. public function canCheckVatNumber($countryCode, $vatNumber, $requesterCountryCode, $requesterVatNumber)
  211. {
  212. return !(!is_string($countryCode)
  213. || !is_string($vatNumber)
  214. || !is_string($requesterCountryCode)
  215. || !is_string($requesterVatNumber)
  216. || empty($countryCode)
  217. || !$this->isCountryInEU($countryCode)
  218. || empty($vatNumber)
  219. || empty($requesterCountryCode) && !empty($requesterVatNumber)
  220. || !empty($requesterCountryCode) && empty($requesterVatNumber)
  221. || !empty($requesterCountryCode) && !$this->isCountryInEU($requesterCountryCode)
  222. );
  223. }
  224. /**
  225. * Get VAT class
  226. *
  227. * @param string $customerCountryCode
  228. * @param DataObject $vatValidationResult
  229. * @param Store|string|int|null $store
  230. * @return null|string
  231. */
  232. public function getCustomerVatClass($customerCountryCode, $vatValidationResult, $store = null)
  233. {
  234. $vatClass = null;
  235. $isVatNumberValid = $vatValidationResult->getIsValid();
  236. if (is_string($customerCountryCode)
  237. && !empty($customerCountryCode)
  238. && $customerCountryCode === $this->getMerchantCountryCode($store)
  239. && $isVatNumberValid
  240. ) {
  241. $vatClass = self::VAT_CLASS_DOMESTIC;
  242. } elseif ($isVatNumberValid) {
  243. $vatClass = self::VAT_CLASS_INTRA_UNION;
  244. } else {
  245. $vatClass = self::VAT_CLASS_INVALID;
  246. }
  247. if (!$vatValidationResult->getRequestSuccess()) {
  248. $vatClass = self::VAT_CLASS_ERROR;
  249. }
  250. return $vatClass;
  251. }
  252. /**
  253. * Check whether specified country is in EU countries list
  254. *
  255. * @param string $countryCode
  256. * @param null|int $storeId
  257. * @return bool
  258. */
  259. public function isCountryInEU($countryCode, $storeId = null)
  260. {
  261. $euCountries = explode(
  262. ',',
  263. $this->scopeConfig->getValue(self::XML_PATH_EU_COUNTRIES_LIST, ScopeInterface::SCOPE_STORE, $storeId)
  264. );
  265. return in_array($countryCode, $euCountries);
  266. }
  267. /**
  268. * Returns the country code to use in the VAT number which is not always the same as the normal country code
  269. *
  270. * @param string $countryCode
  271. * @return string
  272. */
  273. private function getCountryCodeForVatNumber(string $countryCode): string
  274. {
  275. // Greece uses a different code for VAT numbers then its country code
  276. // See: http://ec.europa.eu/taxation_customs/vies/faq.html#item_11
  277. // And https://en.wikipedia.org/wiki/VAT_identification_number:
  278. // "The full identifier starts with an ISO 3166-1 alpha-2 (2 letters) country code
  279. // (except for Greece, which uses the ISO 639-1 language code EL for the Greek language,
  280. // instead of its ISO 3166-1 alpha-2 country code GR)"
  281. return $countryCode === 'GR' ? 'EL' : $countryCode;
  282. }
  283. }