From 8b54b42991a6de74d4dc5bff97316295650f811c Mon Sep 17 00:00:00 2001 From: Isabelle Date: Fri, 11 Sep 2026 16:52:20 +0200 Subject: [PATCH] feat(main): Add configurable VIES-offline fallback policy --- config.xml | 2 +- config_nl.xml | 2 +- src/Adapter/EuValidatorAdapter.php | 49 ++++++++++++- src/Config/Config.php | 18 +++++ src/Model/VatCache.php | 10 +++ src/Module/Admin/SettingsForm.php | 109 +++++++++++++++++++++++++++++ src/Module/Install.php | 10 +++ upgrade/upgrade-1.0.3.php | 12 ++++ ws_vatcheck.php | 4 +- 9 files changed, 212 insertions(+), 4 deletions(-) create mode 100644 src/Module/Admin/SettingsForm.php create mode 100644 upgrade/upgrade-1.0.3.php diff --git a/config.xml b/config.xml index e048a10..dc19e99 100644 --- a/config.xml +++ b/config.xml @@ -2,7 +2,7 @@ ws_vatcheck - + diff --git a/config_nl.xml b/config_nl.xml index 6463315..dc19e99 100644 --- a/config_nl.xml +++ b/config_nl.xml @@ -2,7 +2,7 @@ ws_vatcheck - + diff --git a/src/Adapter/EuValidatorAdapter.php b/src/Adapter/EuValidatorAdapter.php index e8dc1aa..6a2dcf9 100644 --- a/src/Adapter/EuValidatorAdapter.php +++ b/src/Adapter/EuValidatorAdapter.php @@ -8,6 +8,7 @@ use PH7\Eu\Vat\Exception; use Websmid\VatCheck\Config\Config; use Websmid\VatCheck\Model\VatCache; use PrestaShopLogger; +use Configuration; class EuValidatorAdapter { @@ -38,7 +39,7 @@ class EuValidatorAdapter 3 ); - return false; + return $this->resolveOfflineFallback(); } if( $euVatValidator instanceof Validator ){ @@ -52,4 +53,50 @@ class EuValidatorAdapter return true; } + + /** + * Decide what to do when VIES itself can't be reached, per the configured + * Config::OFFLINE_MODE_CONFIG_KEY policy. Returning true here tells + * CacheTaxManager to go ahead and read VatCache::checkVatIsValid() next, so + * the "reuse last known" modes just need to confirm a prior record exists + * (checkVatIsValid() will then return whatever it already holds); the + * "always valid" modes without a prior record need us to actually write one. + */ + private function resolveOfflineFallback(): bool + { + $mode = Configuration::get(Config::OFFLINE_MODE_CONFIG_KEY) ?: Config::OFFLINE_MODE_DEFAULT; + $fullVat = $this->isoCode.$this->vatNr; + + switch ($mode) { + case Config::OFFLINE_MODE_VALID: + $this->storeOfflineResult($fullVat, true); + + return true; + + case Config::OFFLINE_MODE_LAST_KNOWN_OR_INVALID: + return VatCache::hasEverBeenChecked($fullVat); + + case Config::OFFLINE_MODE_LAST_KNOWN_OR_VALID: + if (VatCache::hasEverBeenChecked($fullVat)) { + return true; + } + + $this->storeOfflineResult($fullVat, true); + + return true; + + case Config::OFFLINE_MODE_INVALID: + default: + return false; + } + } + + private function storeOfflineResult(string $fullVat, bool $valid): void + { + $vatCache = VatCache::getOrCreateByVat($fullVat); + $vatCache->vat_nr = $fullVat; + $vatCache->valid = $valid; + + $vatCache->save(); + } } \ No newline at end of file diff --git a/src/Config/Config.php b/src/Config/Config.php index 3ae3daf..4ed708a 100644 --- a/src/Config/Config.php +++ b/src/Config/Config.php @@ -14,6 +14,24 @@ class Config CONST CACHE_VALIDITY = 1; + // Configuration key holding the chosen offline-fallback mode. + CONST OFFLINE_MODE_CONFIG_KEY = 'WS_VATCHECK_OFFLINE_MODE'; + + // What to decide when the VIES service can't be reached. + CONST OFFLINE_MODE_INVALID = 'invalid'; + CONST OFFLINE_MODE_VALID = 'valid'; + CONST OFFLINE_MODE_LAST_KNOWN_OR_INVALID = 'last_known_or_invalid'; + CONST OFFLINE_MODE_LAST_KNOWN_OR_VALID = 'last_known_or_valid'; + + CONST OFFLINE_MODE_DEFAULT = self::OFFLINE_MODE_INVALID; + + CONST OFFLINE_MODE_LABELS = [ + self::OFFLINE_MODE_INVALID => 'Always mark VAT as invalid', + self::OFFLINE_MODE_VALID => 'Always mark VAT as valid', + self::OFFLINE_MODE_LAST_KNOWN_OR_INVALID => 'Reuse the last known result, otherwise mark invalid', + self::OFFLINE_MODE_LAST_KNOWN_OR_VALID => 'Reuse the last known result, otherwise mark valid', + ]; + // Article 39bis W.BTW intra-community exemption notice, per invoice language. CONST INTRACOMMUNITY_EXEMPTION_TEXTS = [ 'nl' => 'Vrijgestelde intracommunautaire levering – artikel 39bis W.BTW', diff --git a/src/Model/VatCache.php b/src/Model/VatCache.php index 252f189..045a2e0 100644 --- a/src/Model/VatCache.php +++ b/src/Model/VatCache.php @@ -45,6 +45,16 @@ class VatCache extends ObjectModel return (bool)Db::getInstance()->getValue('SELECT `valid` FROM `'._DB_PREFIX_.'vat_cache` WHERE `vat_nr` = "'.pSQL($vatNr).'" ORDER BY `id_vat` DESC'); } + /** + * Unlike checkVatIsCached(), this ignores Config::CACHE_VALIDITY entirely — + * used by the offline-fallback "reuse last known result" modes, which want + * whatever was last recorded regardless of how stale it is. + */ + public static function hasEverBeenChecked(string $vatNr) + { + return (bool)Db::getInstance()->getValue('SELECT `id_vat` 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(); diff --git a/src/Module/Admin/SettingsForm.php b/src/Module/Admin/SettingsForm.php new file mode 100644 index 0000000..78d70c9 --- /dev/null +++ b/src/Module/Admin/SettingsForm.php @@ -0,0 +1,109 @@ +postProcessSettings(); + } + + return $this->renderSettingsForm(); + } + + private function postProcessSettings() + { + $mode = Tools::getValue(Config::OFFLINE_MODE_CONFIG_KEY); + + if (!array_key_exists($mode, Config::OFFLINE_MODE_LABELS)) { + return; + } + + Configuration::updateValue(Config::OFFLINE_MODE_CONFIG_KEY, $mode); + } + + private function renderSettingsForm() + { + $helper = new HelperForm(); + + $helper->show_toolbar = false; + $helper->table = $this->table; + $helper->module = $this; + $helper->default_form_language = Context::getContext()->language->id; + $helper->allow_employee_form_lang = (int) Configuration::get('PS_BO_ALLOW_EMPLOYEE_FORM_LANG', 0); + + $helper->identifier = $this->identifier; + $helper->submit_action = 'submitWsVatCheckSettings'; + $helper->currentIndex = Context::getContext()->link->getAdminLink('AdminModules', false) + . '&configure=' . $this->name + . '&tab_module=' . $this->tab + . '&module_name=' . $this->name; + $helper->token = Tools::getAdminTokenLite('AdminModules'); + + $helper->tpl_vars = [ + 'fields_value' => $this->getSettingsFormValues(), + 'languages' => Context::getContext()->controller->getLanguages(), + 'id_language' => Context::getContext()->language->id, + ]; + + return $helper->generateForm([$this->getSettingsFormStructure()]); + } + + private function getSettingsFormValues() + { + return [ + Config::OFFLINE_MODE_CONFIG_KEY => Configuration::get( + Config::OFFLINE_MODE_CONFIG_KEY, + null, + null, + null, + Config::OFFLINE_MODE_DEFAULT + ), + ]; + } + + private function getSettingsFormStructure() + { + $choices = []; + foreach (Config::OFFLINE_MODE_LABELS as $value => $label) { + $choices[] = [ + 'id' => $value, + 'value' => $value, + 'label' => $this->trans($label, [], 'Modules.Ws_VatCheck.Admin'), + ]; + } + + return [ + 'form' => [ + 'legend' => [ + 'title' => $this->trans('VAT Check settings', [], 'Modules.Ws_VatCheck.Admin'), + 'icon' => 'icon-cogs', + ], + 'input' => [ + [ + 'type' => 'radio', + 'label' => $this->trans('When VIES is unreachable', [], 'Modules.Ws_VatCheck.Admin'), + 'name' => Config::OFFLINE_MODE_CONFIG_KEY, + 'desc' => $this->trans( + 'What to decide about a VAT number when the EU VIES service can\'t be reached to check it.', + [], + 'Modules.Ws_VatCheck.Admin' + ), + 'values' => $choices, + ], + ], + 'submit' => [ + 'title' => $this->trans('Save', [], 'Admin.Actions'), + ], + ], + ]; + } +} diff --git a/src/Module/Install.php b/src/Module/Install.php index 46307bb..e6cb9ee 100644 --- a/src/Module/Install.php +++ b/src/Module/Install.php @@ -4,6 +4,7 @@ namespace Websmid\VatCheck\Module; use Websmid\VatCheck\Config\Config; use Db; +use Configuration; trait Install { @@ -21,9 +22,18 @@ trait Install return false; } + if (!$this->registerConfiguration()) { + return false; + } + return true; } + private function registerConfiguration() + { + return Configuration::updateValue(Config::OFFLINE_MODE_CONFIG_KEY, Config::OFFLINE_MODE_DEFAULT); + } + private function registerHooks() { foreach (Config::HOOKS as $hook) { diff --git a/upgrade/upgrade-1.0.3.php b/upgrade/upgrade-1.0.3.php new file mode 100644 index 0000000..3a670f7 --- /dev/null +++ b/upgrade/upgrade-1.0.3.php @@ -0,0 +1,12 @@ +name = 'ws_vatcheck'; - $this->version = '1.0.2'; + $this->version = '1.0.3'; $this->author = 'Johan van der Klis | De Websmid BV'; $this->need_instance = 0; $this->bootstrap = true;