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:
@@ -0,0 +1 @@
|
||||
vendor/
|
||||
@@ -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.
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
@@ -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']
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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!');
|
||||
}
|
||||
}
|
||||
@@ -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!');
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
];
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
@@ -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)');
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Websmid\VatCheck\Module\Admin;
|
||||
|
||||
use Websmid\VatCheck\Config\Config;
|
||||
|
||||
trait AdminHooks
|
||||
{
|
||||
public function isUsingNewTranslationSystem()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -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'];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
if (!defined('_PS_VERSION_')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
function upgrade_module_1_0_2($module)
|
||||
{
|
||||
return $module->registerHook('displayInvoiceLegalFreeText');
|
||||
}
|
||||
@@ -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_];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user