Initial commit: ws_vatcheck at v1.0.3

Includes the fix for the GR/EL VAT prefix bug that broke exemption for
every Greek VAT number (in_array-on-values vs array_key_exists in
CacheTaxManager/VatNumber, plus the missing use import in
EuValidatorAdapter). Adds IMPROVEMENTS.txt with a checklist of features
adopted from comparing against the vatchecker module (offline-fallback
policy, failure logging, company-name check, configurable EU country
list, per-product exclusion, GB/XI mapping, AJAX live validation,
address-priority strategy).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-11 15:57:19 +02:00
co-authored by Claude Sonnet 5
parent 290a2967a5
commit 28d91eb69b
25 changed files with 1136 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
vendor/
+66
View File
@@ -0,0 +1,66 @@
ws_vatcheck — planned improvements
adapted from a comparison against the "vatchecker" module (Inform-All & Keraweb), 2026-09-11
cache TTL (1 year) explicitly kept as-is, not on this list.
[ ] 1. Configurable VIES-offline fallback policy
- Add a setting (e.g. VATCHECK_OFFLINE_MODE) with explicit modes instead of an
implicit fail-closed default:
- always mark invalid (current implicit behavior)
- always mark valid
- reuse last cached result, fall back to invalid if none
- reuse last cached result, fall back to valid if none
- Directly related to the GR/EL bug fixed this session: the fallback behavior
should be an explicit, chosen policy, not an accidental side effect of
whether an exception happens to get caught.
[ ] 2. Log every VIES check failure
- Currently EuValidatorAdapter::checkAndStore() has no logging at all on
failure (this silence was a big part of why the GR/EL bug took so long to
diagnose). Add PrestaShopLogger::addLog(...) (or equivalent) whenever the
Europa/Validator call throws, including the VAT number, country code, and
the fault message.
[ ] 3. Company-name cross-check against VIES response
- When VIES returns a company name for a validated VAT number, optionally
compare it (normalized: lowercase, strip punctuation/legal-entity suffixes
like bv/gmbh/ltd/nv/sa) against the address's company field. Make this
togglable, since not all member states return a name.
[ ] 4. Configurable EU country allow-list
- Replace hardcoded VatNumber::$euIso-driven behavior with an admin-configurable
list of which EU countries are allowed to order VAT-free, similar to
vatchecker's VATCHECKER_EU_COUNTRIES setting.
[ ] 5. Per-product VAT exclusion
- Some products (e.g. certain digital services) may need to always charge VAT
regardless of customer VAT-exempt status. Add a product-level flag/table
(mirroring vatchecker_excluded_products) and consult it before applying the
0% tax manager for a given product line.
[ ] 6. GB -> XI (Northern Ireland) mapping
- VatNumber::$euIso has no entry at all for the UK/Northern Ireland. If this
shop (or any future ws_vatcheck deployment) ships to Northern Ireland with an
XI-prefixed VAT number, it will hit the same class of key/value bug already
fixed for Greece. Add the mapping and cover it with the same key-based checks.
[ ] 7. Front-end AJAX live VAT validation
- Add an AJAX front controller + JS so the address form validates the VAT
number as the customer types, instead of only on form submit
(hookActionValidateCustomerAddressForm). Mirrors vatchecker's ajax.php +
front.js/front.css pattern. Improves UX and surfaces bad VAT numbers earlier.
[ ] 8. Configurable address-priority strategy
- Add a setting for which address's VAT number is authoritative when invoice
and delivery addresses differ (shipping only / invoice only / prioritize
invoice / prioritize shipping), instead of relying on implicit PS tax-address
resolution. Mirrors vatchecker's VATCHECKER_ADDRESS_SELECT.
Explicitly deferred / not adopting:
- Shortening the 1-year cache TTL (Config::CACHE_VALIDITY) — decided to leave as-is.
- Class overrides (Carrier/Product) as used by vatchecker — do not port this pattern;
it conflicts with other modules that override the same core classes and is fragile
across PS core upgrades. If shipping/product tax rule assignment ever needs
behavior beyond what the taxManager hook covers, investigate a hook-based solution
first before reaching for an override.
- Legacy $this->l() / translations/{iso}.php system — ws_vatcheck already uses
$this->trans() correctly per house convention for PS >= 1.7.6.0.
+29
View File
@@ -0,0 +1,29 @@
{
"name": "websmid/vatcheck",
"description": "Maakt het vrijstellen van BTW mogelijk via een gecachte vat check",
"authors": [
{
"name": "Johan van der Klis - De Websmid BV",
"email": "johan@dewebsmid.nl"
}
],
"require": {
"ph-7/eu-vat-validator": "3.1"
},
"autoload": {
"psr-4": {
"Websmid\\VatCheck\\": "src/"
},
"classmap": [
"ws_vatcheck.php"
],
"exclude-from-classmap": []
},
"config": {
"preferred-install": "dist",
"prepend-autoloader": false
},
"type": "prestashop-module",
"author": "Johan van der Klis - De Websmid BV",
"license": "MIT"
}
Generated
+91
View File
@@ -0,0 +1,91 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "ccb92e4629eb4c104b85c939daa54a7c",
"packages": [
{
"name": "ph-7/eu-vat-validator",
"version": "3.1",
"source": {
"type": "git",
"url": "https://github.com/pH-7/eu-vat-validator.git",
"reference": "03dc5305984ad71a71c363ab4cb1f691a5d9408b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pH-7/eu-vat-validator/zipball/03dc5305984ad71a71c363ab4cb1f691a5d9408b",
"reference": "03dc5305984ad71a71c363ab4cb1f691a5d9408b",
"shasum": ""
},
"require": {
"ext-soap": "*",
"php": ">=7.1.0"
},
"require-dev": {
"phake/phake": "^3.1",
"phpunit/phpunit": "^7.5"
},
"type": "library",
"autoload": {
"psr-4": {
"PH7\\Eu\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-3.0-or-later"
],
"authors": [
{
"name": "Pierre-Henry Soria",
"email": "hi@ph7.me",
"homepage": "https://github.com/pH-7/eu-vat-validator",
"role": "Software Engineer"
}
],
"description": "A simple and clean PHP class that validates EU VAT numbers against the central ec.europa.eu database (using the official europa API).",
"homepage": "https://pierrehenry.be",
"keywords": [
"TVA",
"eu vat",
"tva number",
"validate eu vat",
"validate vat number",
"validation",
"validator",
"vat",
"vat number",
"vat number validator",
"vat validator",
"vies"
],
"support": {
"issues": "https://github.com/pH-7/eu-vat-validator/issues",
"source": "https://github.com/pH-7/eu-vat-validator/tree/3.1"
},
"funding": [
{
"url": "https://github.com/pH-7",
"type": "github"
},
{
"url": "https://ko-fi.com/phenry",
"type": "ko_fi"
}
],
"time": "2023-01-04T11:50:01+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "2.3.0"
}
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" ?>
<module>
<name>ws_vatcheck</name>
<displayName><![CDATA[Vat Check]]></displayName>
<version><![CDATA[1.0.3]]></version>
<description><![CDATA[Gecachte vat check]]></description>
<author><![CDATA[Johan van der Klis | De Websmid BV]]></author>
<tab><![CDATA[]]></tab>
<is_configurable>0</is_configurable>
<need_instance>0</need_instance>
</module>
+8
View File
@@ -0,0 +1,8 @@
services:
Websmid\VatCheck\Command\CleanCommand:
class: Websmid\VatCheck\Command\CleanCommand
tags: ['console.command']
Websmid\VatCheck\Command\BulkCheckCommand:
class: Websmid\VatCheck\Command\BulkCheckCommand
tags: ['console.command']
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" ?>
<module>
<name>ws_vatcheck</name>
<displayName><![CDATA[Vat Check]]></displayName>
<version><![CDATA[1.0.3]]></version>
<description><![CDATA[Gecachte vat check]]></description>
<author><![CDATA[Johan van der Klis | De Websmid BV]]></author>
<tab><![CDATA[]]></tab>
<is_configurable>0</is_configurable>
<need_instance>0</need_instance>
</module>
+35
View File
@@ -0,0 +1,35 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to https://devdocs.prestashop.com/ for more information.
*
* @author PrestaShop SA and Contributors <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT");
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
header("Location: ../");
exit;
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

+44
View File
@@ -0,0 +1,44 @@
<?php
namespace Websmid\VatCheck\Adapter;
use PH7\Eu\Vat\Validator;
use PH7\Eu\Vat\Provider\Europa;
use PH7\Eu\Vat\Exception;
use Websmid\VatCheck\Config\Config;
use Websmid\VatCheck\Model\VatCache;
class EuValidatorAdapter
{
protected $vatNr = '';
protected $isoCode = '';
private $checkStatus;
public function __construct(string $vatNr, string $isoCode)
{
$this->vatNr = str_replace($isoCode, '', $vatNr);
$this->isoCode = $isoCode;
}
public function checkAndStore()
{
try {
$euVatValidator = new Validator(new Europa, $this->vatNr, $this->isoCode);
} catch(Exception $e){
return false;
}
if( $euVatValidator instanceof Validator ){
$fullVat = $this->isoCode.$this->vatNr;
$vatCache = VatCache::getOrCreateByVat($fullVat);
$vatCache->vat_nr = $fullVat;
$vatCache->valid = $euVatValidator->check();
$vatCache->save();
}
return true;
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
namespace Websmid\VatCheck\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Websmid\VatCheck\Service\LoggingService;
abstract class BaseCommand extends Command
{
public $output;
public $input;
public $logger;
public function __construct(string $name = null)
{
parent::__construct($name);
}
public function log($message, $object = null, $id = null)
{
if (!is_null($this->logger)) {
$this->logger->log($message, $object, $id);
} else {
$now = (new \DateTime())->format('Y-m-d H:i:s');
$line = $now . ' --> ' . $message . PHP_EOL;
$this->logs[] = $line;
$this->output->write($line);
}
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->output = $output;
$this->input = $input;
set_time_limit(0);
ini_set('memory_limit', '-1');
$this->executeCommand();
}
public abstract function executeCommand();
}
+62
View File
@@ -0,0 +1,62 @@
<?php
namespace Websmid\VatCheck\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Websmid\VatCheck\Service\LoggingService;
use Db;
use Country;
use Websmid\VatCheck\Adapter\EuValidatorAdapter;
use Websmid\VatCheck\Model\VatNumber;
class BulkCheckCommand extends BaseCommand
{
protected function configure()
{
$this->setName('vatcheck:bulkcheck');
}
public function executeCommand()
{
$this->log('Start process');
$this->logger = new LoggingService();
$this->logger->setConsole($this->output);
//$this->logger->enablePrestaLog();
$vatInfosToCheck = Db::getInstance()->executeS('
SELECT
`vat_number`, `id_country`
FROM
`'._DB_PREFIX_.'address`
WHERE
`vat_number` NOT IN(
SELECT
`vat_nr`
FROM
`'._DB_PREFIX_.'vat_cache`
) AND `vat_number` != ""
');
foreach($vatInfosToCheck as $vatInfo){
$isoCode = Country::getIsoById((int)$vatInfo['id_country']);
$vatNumber = new VatNumber($vatInfo['vat_number'], $isoCode);
if( $vatNumber->hasErrors() || $vatNumber->isNotEuIso() ){
continue;
}
$euValidator = new EuValidatorAdapter($vatNumber->getFormattedVat(), $isoCode);
$euValidator->checkAndStore();
$this->log('Iteration end, now ging to sleep 3s');
sleep(3);
}
return $this->log('Stored VATS checked!');
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace Websmid\VatCheck\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Websmid\VatCheck\Service\LoggingService;
use Websmid\VatCheck\Model\VatCache;
class CleanCommand extends BaseCommand
{
protected function configure()
{
$this->setName('vatcheck:cleanrecords');
}
public function executeCommand()
{
$this->log('Start process');
$this->logger = new LoggingService();
$this->logger->setConsole($this->output);
//$this->logger->enablePrestaLog();
VatCache::cleanOldRecords();
return $this->log('Records cleaned!');
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace Websmid\VatCheck\Config;
class Config
{
// Hooks
CONST HOOKS = [
'actionTaxManager',
'taxManager',
'actionValidateCustomerAddressForm',
'displayInvoiceLegalFreeText'
];
CONST CACHE_VALIDITY = 1;
// Article 39bis W.BTW intra-community exemption notice, per invoice language.
CONST INTRACOMMUNITY_EXEMPTION_TEXTS = [
'nl' => 'Vrijgestelde intracommunautaire levering – artikel 39bis W.BTW',
'fr' => 'Livraison intracommunautaire exonérée de TVA – article 39bis du Code TVA',
'en' => 'Intra-Community supply exempt from VAT according to Article 39bis of the Belgian VAT Code.',
'de' => 'Von der Mehrwertsteuer befreite innergemeinschaftliche Lieferung – Artikel 39bis des belgischen Mehrwertsteuergesetzbuches',
];
}
+89
View File
@@ -0,0 +1,89 @@
<?php
namespace Websmid\VatCheck\Console;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
class WebInput implements InputInterface
{
public function getFirstArgument()
{
// TODO: Implement getFirstArgument() method.
}
public function hasParameterOption($values, $onlyParams = false)
{
// TODO: Implement hasParameterOption() method.
}
public function getParameterOption($values, $default = false, $onlyParams = false)
{
// TODO: Implement getParameterOption() method.
}
public function bind(InputDefinition $definition)
{
// TODO: Implement bind() method.
}
public function validate()
{
// TODO: Implement validate() method.
}
public function getArguments()
{
// TODO: Implement getArguments() method.
}
public function getArgument($name)
{
// TODO: Implement getArgument() method.
}
public function setArgument($name, $value)
{
// TODO: Implement setArgument() method.
}
public function hasArgument($name)
{
// TODO: Implement hasArgument() method.
}
public function getOptions()
{
// TODO: Implement getOptions() method.
}
public function getOption($name)
{
if (isset($_GET[$name])) {
return $_GET[$name];
}
return true;
}
public function setOption($name, $value)
{
// TODO: Implement setOption() method.
}
public function hasOption($name)
{
// TODO: Implement hasOption() method.
}
public function isInteractive()
{
// TODO: Implement isInteractive() method.
}
public function setInteractive($interactive)
{
// TODO: Implement setInteractive() method.
}
}
+88
View File
@@ -0,0 +1,88 @@
<?php
namespace Websmid\VatCheck\Console;
use Symfony\Component\Console\Formatter\OutputFormatterInterface;
use Symfony\Component\Console\Output\OutputInterface;
class WebOutput implements OutputInterface
{
public $cleaned = false;
public function __construct()
{
ini_set('output_buffering', 'Off');
}
public function write($messages, $newline = false, $options = 0)
{
if (!$this->cleaned) {
//ob_end_flush();
$this->cleaned = true;
}
echo $messages . '<br/>';
$this->flush_buffers();
}
public function flush_buffers()
{
//$result = ob_get_contents();
//echo $result;
flush();
}
public function writeln($messages, $options = 0)
{
// TODO: Implement writeln() method.
}
public function setVerbosity($level)
{
// TODO: Implement setVerbosity() method.
}
public function getVerbosity()
{
// TODO: Implement getVerbosity() method.
}
public function isQuiet()
{
// TODO: Implement isQuiet() method.
}
public function isVerbose()
{
// TODO: Implement isVerbose() method.
}
public function isVeryVerbose()
{
// TODO: Implement isVeryVerbose() method.
}
public function isDebug()
{
// TODO: Implement isDebug() method.
}
public function setDecorated($decorated)
{
// TODO: Implement setDecorated() method.
}
public function isDecorated()
{
// TODO: Implement isDecorated() method.
}
public function setFormatter(OutputFormatterInterface $formatter)
{
// TODO: Implement setFormatter() method.
}
public function getFormatter()
{
// TODO: Implement getFormatter() method.
}
}
+60
View File
@@ -0,0 +1,60 @@
<?php
namespace Websmid\VatCheck\Model;
use Websmid\VatCheck\Config\Config;
use ObjectModel;
use Db;
class VatCache extends ObjectModel
{
/** @var int|null */
public $id;
public $vat_nr = '';
public $valid;
public $date_add;
public $date_upd;
public static $definition = [
'table' => 'vat_cache',
'primary' => 'id_vat',
'fields' => [
'vat_nr' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'required' => true],
'valid' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool', 'required' => true],
'date_add' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
'date_upd' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
],
];
public static function getIdByVat(string $vatNr)
{
return Db::getInstance()->getValue('SELECT `id_vat` FROM `'._DB_PREFIX_.'vat_cache` WHERE `vat_nr` = "'.pSQL($vatNr).'" AND `date_add` > DATE_SUB(NOW(), INTERVAL '.(int)Config::CACHE_VALIDITY.' YEAR)');
}
public static function checkVatIsCached(string $vatNr)
{
return (bool)self::getIdByVat($vatNr);
}
public static function checkVatIsValid(string $vatNr)
{
return (bool)Db::getInstance()->getValue('SELECT `valid` FROM `'._DB_PREFIX_.'vat_cache` WHERE `vat_nr` = "'.pSQL($vatNr).'" ORDER BY `id_vat` DESC');
}
public static function getOrCreateByVat(string $vatNr)
{
$className = get_called_class();
$idVat = $className::getIdByVat($vatNr);
return ( empty($idVat) ) ? new $className() : new $className($idVat);
}
public static function cleanOldRecords()
{
return Db::getInstance()->execute('DELETE FROM `'._DB_PREFIX_.'vat_cache` WHERE `date_add` < DATE_SUB(NOW(), INTERVAL '.(int)Config::CACHE_VALIDITY.' YEAR)');
}
}
+129
View File
@@ -0,0 +1,129 @@
<?php
namespace Websmid\VatCheck\Model;
use Websmid\VatCheck\Config\Config;
use Context;
class VatNumber
{
private $vatNumber;
private $isoCode;
private $errorMessage = '';
public static $euIso = [
'AT' => 'AT',
'BE' => 'BE',
'DK' => 'DK',
'FI' => 'FI',
'FR' => 'FR',
'FX' => 'FR',
'DE' => 'DE',
'GR' => 'EL',
'IE' => 'IE',
'IT' => 'IT',
'LU' => 'LU',
'NL' => 'NL',
'PT' => 'PT',
'ES' => 'ES',
'SE' => 'SE',
'CY' => 'CY',
'EE' => 'EE',
'HU' => 'HU',
'LV' => 'LV',
'LT' => 'LT',
'MT' => 'MT',
'PL' => 'PL',
'SK' => 'SK',
'CZ' => 'CZ',
'SI' => 'SI',
'RO' => 'RO',
'BG' => 'BG',
'HR' => 'HR',
];
private $isoPattern = [
'AT' => 'U[A-Z\d]{8}',
'BE' => '[0|1]{1}\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}',
'GR' => '\d{9}',
'ES' => '[A-Z]\d{7}[A-Z]|\d{8}[A-Z]|[A-Z]\d{8}',
'FI' => '\d{8}',
'FR' => '([A-Z0-9]{2})\d{9}',
'FX' => '([A-Z0-9]{2})\d{9}',
'HR' => '\d{11}',
'HU' => '\d{8}',
'IE' => '[A-Z\d]{8}|[A-Z\d]{9}',
'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}',
];
public function __construct(
string $rawVatNumber,
string $rawIsoCode
) {
$this->vatNumber = $this->commonFormatVatNumber($rawVatNumber);
$this->isoCode = strtoupper($rawIsoCode);
$this->validateVatNr();
}
protected function validateVatNr()
{
$vatPrefix = self::$euIso[$this->isoCode] ?? $this->isoCode;
if( substr($this->vatNumber, 0, 2) != $vatPrefix ){
$this->errorMessage = Context::getContext()->getTranslator()->trans('ISO in VAT number doesn\'t seem to match selected country', [], 'Modules.Ws_VatCheck.Front');
}
$trimmedVat = str_replace($vatPrefix, '', $this->vatNumber);
$isoPattern = $this->isoPattern[$this->isoCode];
if( !preg_match('/^(?:'.$isoPattern.')$/', $trimmedVat) ){
$this->errorMessage = Context::getContext()->getTranslator()->trans('VAT number doesn\'t seem to be valid', [], 'Modules.Ws_VatCheck.Front');
}
}
public function getFormattedVat()
{
return $this->vatNumber;
}
public function hasErrors()
{
return !empty($this->errorMessage);
}
public function isNotEuIso()
{
return !array_key_exists($this->isoCode, self::$euIso);
}
private function commonFormatVatNumber(string $rawVatNumber)
{
return strtoupper(preg_replace('/[ .]/', '', $rawVatNumber));
}
public function getErrorMessage()
{
return $this->errorMessage;
}
}
+13
View File
@@ -0,0 +1,13 @@
<?php
namespace Websmid\VatCheck\Module\Admin;
use Websmid\VatCheck\Config\Config;
trait AdminHooks
{
public function isUsingNewTranslationSystem()
{
return true;
}
}
+71
View File
@@ -0,0 +1,71 @@
<?php
namespace Websmid\VatCheck\Module\Front;
use Websmid\VatCheck\TaxManager\CacheTaxManager;
use Websmid\VatCheck\Model\VatNumber;
use Websmid\VatCheck\Config\Config;
use Address;
use Configuration;
use Country;
use Language;
use Product;
use Tools;
trait FrontHooks
{
public function hookActionValidateCustomerAddressForm(&$params)
{
$form = $params['form'];
$vatNumber = (string)$form->getValue('vat_number');
$vatNumber = str_replace(' ', '', $vatNumber);
if (empty($vatNumber)) {
return true;
}
$countryIso = (string)Country::getIsoById((string)$form->getValue('id_country'));
$vatNumber = new VatNumber($vatNumber, $countryIso);
if( $vatNumber->isNotEuIso() ){
return true;
}
$form->setValue('vat_number', $vatNumber->getFormattedVat());
if( $vatNumber->hasErrors() ){
$form->getField('vat_number')->addError($vatNumber->getErrorMessage());
}
return $vatNumber->hasErrors();
}
public function hookTaxManager($args)
{
if (call_user_func(['Websmid\VatCheck\TaxManager\CacheTaxManager', 'isAvailableForThisAddress'], $args['address'])) {
return new CacheTaxManager();
}
return false;
}
public function hookDisplayInvoiceLegalFreeText(array $params)
{
$order = $params['order'];
$address = new Address((int) $order->{Configuration::get('PS_TAX_ADDRESS_TYPE')});
if (!CacheTaxManager::isAvailableForThisAddress($address)) {
return false;
}
$existingText = Configuration::get('PS_INVOICE_LEGAL_FREE_TEXT', (int) $order->id_lang, null, (int) $order->id_shop);
return trim($existingText . "\n\n" . $this->getIntraCommunityExemptionText((int) $order->id_lang));
}
private function getIntraCommunityExemptionText(int $idLang)
{
$isoCode = strtolower(Language::getIsoById($idLang));
return Config::INTRACOMMUNITY_EXEMPTION_TEXTS[$isoCode] ?? Config::INTRACOMMUNITY_EXEMPTION_TEXTS['en'];
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php
namespace Websmid\VatCheck\Module;
use Websmid\VatCheck\Config\Config;
use Db;
trait Install
{
public function install()
{
if (!parent::install()) {
return false;
}
if (!$this->createTables()) {
return false;
}
if (!$this->registerHooks()) {
return false;
}
return true;
}
private function registerHooks()
{
foreach (Config::HOOKS as $hook) {
if (!$this->registerHook($hook)) {
return false;
}
}
return true;
}
private function createTables()
{
$sqlToExec = [];
$sqlToExec[] = '
CREATE TABLE IF NOT EXISTS '._DB_PREFIX_.'vat_cache (
`id_vat` INTEGER(12) PRIMARY KEY AUTO_INCREMENT,
`vat_nr` VARCHAR(256),
`valid` TINYINT(1),
`date_add` DATE NOT NULL,
`date_upd` DATE NOT NULL
);';
foreach($sqlToExec as $sqlBlock){
Db::getInstance()->execute($sqlBlock);
}
return true;
}
}
+52
View File
@@ -0,0 +1,52 @@
<?php
namespace Websmid\VatCheck\Service;
use Symfony\Component\Console\Output\OutputInterface;
class LoggingService
{
private $output;
private $logs = [];
/**
* @var true
*/
private $echo = false;
private $presta = false;
public function setConsole(OutputInterface $output) : self
{
$this->output = $output;
return $this;
}
public function enableEcho() : self
{
$this->echo = true;
return $this;
}
public function enablePrestaLog() : self
{
$this->presta = true;
return $this;
}
public function log($message, $object = null, $object_id = null)
{
$now = (new \DateTime())->format('Y-m-d H:i:s');
$line = $now . ' --> '.$message . PHP_EOL;
$this->logs[] = $line;
if (!is_null($this->output)) {
$this->output->write($line);
}
if ($this->echo) {
echo $line;
}
if ($this->presta) {
\PrestaShopLogger::addLog($message, 1, null, $object, $object_id, true);
}
}
}
+54
View File
@@ -0,0 +1,54 @@
<?php
namespace Websmid\VatCheck\TaxManager;
use Websmid\VatCheck\Config\Config;
use TaxManagerInterface;
use Address;
use Country;
use Tax;
use TaxCalculator;
use Configuration;
use Context;
use Websmid\VatCheck\Model\VatCache;
use Websmid\VatCheck\Model\VatNumber;
use Websmid\VatCheck\Adapter\EuValidatorAdapter;
class CacheTaxManager implements TaxManagerInterface
{
public static function isAvailableForThisAddress(Address $address)
{
if( empty($address->vat_number) ){
return false;
}
if( (int)$address->id_country == (int)Configuration::get('PS_COUNTRY_DEFAULT') ){
return false;
}
$isoCode = Country::getIsoById((int)$address->id_country);
if( !array_key_exists($isoCode, VatNumber::$euIso) ){
return false;
}
if( VatCache::checkVatIsCached($address->vat_number) ){
return VatCache::checkVatIsValid($address->vat_number);
}
$vatPrefix = VatNumber::$euIso[$isoCode];
$euValidator = new EuValidatorAdapter($address->vat_number, $vatPrefix);
if( !$euValidator->checkAndStore() ){
return false;
}
return VatCache::checkVatIsValid($address->vat_number);
}
public function getTaxCalculator()
{
$tax = new Tax();
$tax->rate = 0;
return new TaxCalculator([$tax]);
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
if (!defined('_PS_VERSION_')) {
exit;
}
function upgrade_module_1_0_2($module)
{
return $module->registerHook('displayInvoiceLegalFreeText');
}
+54
View File
@@ -0,0 +1,54 @@
<?php
/**
* 2007-2023 De Websmid BV
*
* NOTICE OF LICENSE
*
* This file is not open source! Each license that you purchased is only available for 1 wesite only.
* If you want to use this file on more websites (or projects), you need to purchase additional licenses.
* You are not allowed to redistribute, resell, lease, license, sub-license or offer our resources to any third party.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please contact us for extra customization service at an affordable price
*
* @author De Websmid BV <info@dewebsmid.nl>
* @copyright 2007-2024 De Websmid BV
* @license Valid for 1 website (or project) for each purchase of license
* International Registered Trademark & Property of De Websmid BV
*/
use Websmid\VatCheck\Module\Install;
use Websmid\VatCheck\Module\Front\FrontHooks;
use Websmid\VatCheck\Module\Admin\AdminHooks;
if (file_exists(__DIR__ . '/vendor/autoload.php')) {
require_once __DIR__ . '/vendor/autoload.php';
}
if (!defined('_PS_VERSION_')) {
exit;
}
class Ws_VatCheck extends \Module
{
use Install;
use AdminHooks;
use FrontHooks;
public function __construct()
{
$this->name = 'ws_vatcheck';
$this->version = '1.0.3';
$this->author = 'Johan van der Klis | De Websmid BV';
$this->need_instance = 0;
$this->bootstrap = true;
parent::__construct();
$this->displayName = $this->trans('Vat Check', [], 'Modules.Ws_VatCheck.Admin');
$this->description = $this->trans('Gecachte vat check', [], 'Modules.Ws_VatCheck.Description');
$this->ps_versions_compliancy = ['min' => '1.7.0.0', 'max' => _PS_VERSION_];
}
}