<?php declare(strict_types=1);
namespace Acris\Tax;
use Acris\Tax\Components\Service\VatIdValidationService;
use Doctrine\DBAL\Connection;
use Shopware\Core\Content\Rule\Aggregate\RuleCondition\RuleConditionEntity;
use Shopware\Core\Content\Rule\RuleEntity;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearchResult;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\IdSearchResult;
use Shopware\Core\Framework\Plugin;
use Shopware\Core\Framework\Plugin\Context\InstallContext;
use Shopware\Core\Framework\Plugin\Context\UninstallContext;
use Shopware\Core\Framework\Plugin\Context\UpdateContext;
use Shopware\Core\Framework\Plugin\Context\ActivateContext;
use Shopware\Core\System\Country\CountryEntity;
use Shopware\Core\System\CustomField\CustomFieldTypes;
use Shopware\Core\System\Snippet\SnippetEntity;
class AcrisTaxCS extends Plugin
{
const CUSTOM_FIELD_SET_NAME = 'acris_tax_specific_country';
const CUSTOM_FIELD_SET_NAME_PRODUCT_EXCLUDE_TAX_FREE = 'acris_tax_exclude_product_tax_free';
const DEFAULT_CUSTOM_CUSTOMER_HAS_VAT_ID_RULE_NAME = 'ACRIS customer personal data has VAT Reg.No. (Shopware standard)';
const DEFAULT_CUSTOM_BILLING_ADDRESS_HAS_VAT_ID_RULE_NAME = 'ACRIS billing address has VAT Reg.No.';
const DEFAULT_CUSTOM_SHIPPING_ADDRESS_HAS_VAT_ID_RULE_NAME = 'ACRIS shipping address has VAT Reg.No.';
const DEFAULT_CUSTOM_CUSTOMER_HAS_VALID_VAT_ID_RULE_NAME = 'ACRIS customer personal data has valid VAT Reg.No.';
const DEFAULT_CUSTOM_BILLING_ADDRESS_HAS_VALID_VAT_ID_RULE_NAME = 'ACRIS billing address has valid VAT Reg.No.';
const DEFAULT_CUSTOM_SHIPPING_ADDRESS_HAS_VALID_VAT_ID_RULE_NAME = 'ACRIS shipping address has valid VAT Reg.No.';
const DEFAULT_CUSTOM_EXCLUDE_PRODUCTS_FROM_TAX_FREE_RULE_NAME = 'ACRIS exclude products from tax-free';
const DEFAULT_CUSTOM_SAME_COUNTRY_IN_BILLING_AND_SHIPPING_ADDRESS_NAME = 'ACRIS same country in billing and shipping address';
public function update(UpdateContext $updateContext): void
{
if(version_compare($updateContext->getCurrentPluginVersion(), '2.3.0', '<')
&& version_compare($updateContext->getUpdatePluginVersion(), '2.3.0', '>=')) {
$this->replaceExistingRuleNames($updateContext->getContext());
}
if(version_compare($updateContext->getCurrentPluginVersion(), '6.1.0', '<')
&& version_compare($updateContext->getUpdatePluginVersion(), '6.1.0', '>=')) {
$this->assignDefaultVatIdCheckForCountries($updateContext->getContext());
}
}
public function postUpdate(UpdateContext $context): void
{
if(version_compare($context->getCurrentPluginVersion(), '6.2.0', '<')
&& version_compare($context->getUpdatePluginVersion(), '6.2.0', '>=')) {
$this->insertDefaultValues($context->getContext());
$this->setDefaultAdvancedTaxFreeRulesSettings($context->getContext());
}
if(version_compare($context->getCurrentPluginVersion(), '6.4.0', '<')
&& version_compare($context->getUpdatePluginVersion(), '6.4.0', '>=')) {
$this->insertDefaultValues($context->getContext());
}
if(version_compare($context->getCurrentPluginVersion(), '6.9.0', '<')
&& version_compare($context->getUpdatePluginVersion(), '6.9.0', '>=')) {
$this->insertDefaultValues($context->getContext());
}
if(version_compare($context->getCurrentPluginVersion(), '6.14.0', '<')
&& version_compare($context->getUpdatePluginVersion(), '6.14.0', '>=')) {
$this->addCustomFields($context->getContext());
}
if(version_compare($context->getCurrentPluginVersion(), '6.14.1', '<')
&& version_compare($context->getUpdatePluginVersion(), '6.14.1', '>=')) {
$this->removeCustomFields($context->getContext(), [self::CUSTOM_FIELD_SET_NAME_PRODUCT_EXCLUDE_TAX_FREE]);
$this->addCustomFields($context->getContext());
}
$this->insertDefaultValues($context->getContext());
}
public function activate(ActivateContext $activateContext): void
{
$this->insertDefaultValues($activateContext->getContext());
$this->assignDefaultVatIdCheckForCountries($activateContext->getContext());
$this->setDefaultAdvancedTaxFreeRulesSettings($activateContext->getContext());
}
public function install(InstallContext $context): void
{
$this->addCustomFields($context->getContext());
}
public function uninstall(UninstallContext $context): void
{
if ($context->keepUserData()) {
return;
}
$this->removeCustomFields($context->getContext(), [self::CUSTOM_FIELD_SET_NAME_PRODUCT_EXCLUDE_TAX_FREE]);
$this->removeRules($context->getContext());
$this->cleanupDatabase();
}
private function cleanupDatabase(): void
{
$connection = $this->container->get(Connection::class);
$connection->executeStatement('DROP TABLE IF EXISTS acris_tax_vat_id_validation_log');
$connection->executeStatement('DROP TABLE IF EXISTS acris_tax_customer_group_rule');
$connection->executeStatement('DROP TABLE IF EXISTS acris_tax_country_rule');
try {
$connection->executeStatement('ALTER TABLE `customer` DROP `acrisTaxVatIdValidationLogs`');
$connection->executeStatement('ALTER TABLE `customer_group` DROP `acrisRules`');
$connection->executeStatement('ALTER TABLE `country` DROP `acrisRules`');
} catch (\Exception $e) { }
}
private function replaceExistingRuleNames(Context $context)
{
$ruleConditionRepository = $this->container->get('rule_condition.repository');
/** @var EntitySearchResult $existingRuleConditionSearchResult */
$existingRuleConditionSearchResult = $ruleConditionRepository->search((new Criteria())->addFilter(
new MultiFilter(MultiFilter::CONNECTION_OR, [
new EqualsFilter('type', 'customerHasVatIdBillingRule'),
new EqualsFilter('type', 'customerHasVatIdShippingRule')
])
), $context);
if ($existingRuleConditionSearchResult->count() <= 0) {
return;
}
$changedRuleConditions = [];
/** @var RuleConditionEntity $ruleConditionEntity */
foreach ($existingRuleConditionSearchResult->getElements() as $ruleConditionEntity) {
$value = $ruleConditionEntity->getValue();
if(!is_array($value)) {
continue;
}
if(array_key_exists('customerHasVatIdBilling', $value)) {
$value = ['customerHasVatId' => $value['customerHasVatIdBilling']];
} elseif(array_key_exists('customerHasVatIdShipping', $value)) {
$value = ['customerHasVatId' => $value['customerHasVatIdShipping']];
} else {
continue;
}
$changedRuleConditions[] = [
'id' => $ruleConditionEntity->getId(),
'type' => 'customerHasVatIdRule',
'value' => $value
];
}
if(!empty($changedRuleConditions)) {
$ruleConditionRepository->upsert($changedRuleConditions, $context);
}
}
private function assignDefaultVatIdCheckForCountries(Context $context): void
{
/** @var EntityRepositoryInterface $countryRepository */
$countryRepository = $this->container->get('country.repository');
$criteria = new Criteria();
$criteria->addFilter(new MultiFilter(MultiFilter::CONNECTION_AND, [
new EqualsAnyFilter('iso', VatIdValidationService::DEFAULT_SPECIFIC_COUNTRIES)
]));
/** @var EntitySearchResult $countryIds */
$countries = $countryRepository->search($criteria, $context);
if ($countries->count() === 0 || !$countries->first()) return;
$updateData = [];
/** @var CountryEntity $country */
foreach ($countries->getEntities()->getElements() as $country) {
$customFields = $country->getTranslation('customFields') ?? [];
if (array_key_exists('acris_tax_specific_country_validate_vat_id', $customFields)) continue;
$customFields['acris_tax_specific_country_validate_vat_id'] = true;
$updateData[] = [
'id' => $country->getId(),
'customFields' => $customFields
];
}
if (!empty($updateData)) {
$countryRepository->update($updateData, $context);
}
}
private function insertDefaultValues(Context $context): void
{
/** @var EntityRepositoryInterface $ruleRepository */
$ruleRepository = $this->container->get('rule.repository');
$rules = [
[
'name' => self::DEFAULT_CUSTOM_CUSTOMER_HAS_VAT_ID_RULE_NAME,
'priority' => 100,
'conditions' => [
[
'type' => 'customerHasVatIdRule',
'value' => [
'customerHasVatId' => true
]
]
]
], [
'name' => self::DEFAULT_CUSTOM_BILLING_ADDRESS_HAS_VAT_ID_RULE_NAME,
'priority' => 100,
'conditions' => [
[
'type' => 'billingAddressHasVatIdRule',
'value' => [
'billingAddressHasVatId' => true
]
]
]
], [
'name' => self::DEFAULT_CUSTOM_SHIPPING_ADDRESS_HAS_VAT_ID_RULE_NAME,
'priority' => 100,
'conditions' => [
[
'type' => 'shippingAddressHasVatIdRule',
'value' => [
'shippingAddressHasVatId' => true
]
]
]
], [
'name' => self::DEFAULT_CUSTOM_CUSTOMER_HAS_VALID_VAT_ID_RULE_NAME,
'priority' => 100,
'conditions' => [
[
'type' => 'customerHasVatIdValidRule',
'value' => [
'customerHasVatId' => true
]
]
]
], [
'name' => self::DEFAULT_CUSTOM_BILLING_ADDRESS_HAS_VALID_VAT_ID_RULE_NAME,
'priority' => 100,
'conditions' => [
[
'type' => 'billingAddressHasVatIdValidRule',
'value' => [
'billingAddressHasVatId' => true
]
]
]
], [
'name' => self::DEFAULT_CUSTOM_SHIPPING_ADDRESS_HAS_VALID_VAT_ID_RULE_NAME,
'priority' => 100,
'conditions' => [
[
'type' => 'shippingAddressHasVatIdValidRule',
'value' => [
'shippingAddressHasVatId' => true
]
]
]
], [
'name' => self::DEFAULT_CUSTOM_SAME_COUNTRY_IN_BILLING_AND_SHIPPING_ADDRESS_NAME,
'priority' => 100,
'conditions' => [
[
'type' => 'sameCountryInBillingAddressAndShippingAddressRule',
'value' => [
'isSameCountryInBillingAddressAndShippingAddress' => true
]
]
]
]];
list($countryId, $countryIds) = $this->getCountryIds($context);
if (!empty($countryId) && is_string($countryId) && !empty($countryIds) && is_array($countryIds)) {
$rules[] = [
'name' => self::DEFAULT_CUSTOM_EXCLUDE_PRODUCTS_FROM_TAX_FREE_RULE_NAME,
'priority' => 100,
'conditions' => [
[
'type' => 'orContainer',
'value' => [],
'children' => [
[
'type' => 'andContainer',
'value' => [],
'children' => [
[
'type' => 'productWarehouse',
'value' => [
'operator' => '=',
'warehouse' => 'EU'
]
],
[
'type' => 'customerShippingCountry',
'value' => [
'operator' => '=',
'countryIds' => $countryIds
]
]
]
],
[
'type' => 'andContainer',
'value' => [],
'children' => [
[
'type' => 'customerShippingCountry',
'value' => [
'operator' => '=',
'countryIds' => [$countryId]
],
]
]
]
]
]
]
];
}
foreach ($rules as $rule) {
try {
$this->createIfNotExists($ruleRepository, [['name' => 'name', 'value' => $rule['name']]], $rule, $context);
} catch (\Throwable $e) {
// Do nothing - prevent a Shopware bug for deleting cache for rules
// TODO: Fix missing rule id now!
}
}
}
private function createIfNotExists(EntityRepositoryInterface $repository, array $equalFields, array $data, Context $context): void
{
$filters = [];
foreach ($equalFields as $equalField) {
$filters[] = new EqualsFilter($equalField['name'], $equalField['value']);
}
if (sizeof($filters) > 1) {
$filter = new MultiFilter(MultiFilter::CONNECTION_OR, $filters);
} else {
$filter = array_shift($filters);
}
$searchResult = $repository->search((new Criteria())->addFilter($filter), $context);
if (empty($searchResult->first())) {
$repository->create([$data], $context);
}
}
private function removeRules(Context $context): void
{
/** @var EntityRepositoryInterface $rulesRepository */
$rulesRepository = $this->container->get('rule.repository');
/** @var EntitySearchResult $rulesResult */
$rulesResult = $rulesRepository->search((new Criteria())->addFilter(new MultiFilter(MultiFilter::CONNECTION_OR, [
new EqualsFilter('name', self::DEFAULT_CUSTOM_CUSTOMER_HAS_VAT_ID_RULE_NAME),
new EqualsFilter('name', self::DEFAULT_CUSTOM_BILLING_ADDRESS_HAS_VAT_ID_RULE_NAME),
new EqualsFilter('name', self::DEFAULT_CUSTOM_SHIPPING_ADDRESS_HAS_VAT_ID_RULE_NAME),
new EqualsFilter('name', self::DEFAULT_CUSTOM_CUSTOMER_HAS_VALID_VAT_ID_RULE_NAME),
new EqualsFilter('name', self::DEFAULT_CUSTOM_BILLING_ADDRESS_HAS_VALID_VAT_ID_RULE_NAME),
new EqualsFilter('name', self::DEFAULT_CUSTOM_SHIPPING_ADDRESS_HAS_VALID_VAT_ID_RULE_NAME),
new EqualsFilter('name', self::DEFAULT_CUSTOM_SAME_COUNTRY_IN_BILLING_AND_SHIPPING_ADDRESS_NAME),
new EqualsFilter('name', self::DEFAULT_CUSTOM_EXCLUDE_PRODUCTS_FROM_TAX_FREE_RULE_NAME),
])), $context);
if ($rulesResult->getTotal() > 0 && $rulesResult->first()) {
$ruleIds = [];
/** @var RuleEntity $rule */
foreach ($rulesResult->getEntities()->getElements() as $rule) {
$ruleIds[] = ['id' => $rule->getId()];
}
$rulesRepository->delete($ruleIds, $context);
}
}
private function removeCustomFields(Context $context, array $setNames): void
{
/* Check for snippets if they exist for custom fields */
$this->checkForExistingCustomFieldSnippets($context);
$customFieldSet = $this->container->get('custom_field_set.repository');
foreach ($setNames as $setName) {
$id = $customFieldSet->searchIds((new Criteria())->addFilter(new EqualsFilter('name', $setName)), $context)->firstId();
if($id) $customFieldSet->delete([['id' => $id]], $context);
}
}
private function setDefaultAdvancedTaxFreeRulesSettings(Context $context)
{
/** @var EntityRepositoryInterface $ruleRepository */
$ruleRepository = $this->container->get('rule.repository');
$configService = $this->container->get('Shopware\Core\System\SystemConfig\SystemConfigService');
if($configService->get('AcrisTaxCS.config.acrisAdvancedTaxRules') === null) {
/** @var IdSearchResult $idSearchResult */
$idSearchResult = $ruleRepository->searchIds((new Criteria())->addFilter(new MultiFilter(MultiFilter::CONNECTION_OR, [
new EqualsFilter('name', self::DEFAULT_CUSTOM_CUSTOMER_HAS_VAT_ID_RULE_NAME),
new EqualsFilter('name', self::DEFAULT_CUSTOM_BILLING_ADDRESS_HAS_VAT_ID_RULE_NAME),
new EqualsFilter('name', self::DEFAULT_CUSTOM_SHIPPING_ADDRESS_HAS_VAT_ID_RULE_NAME),
])), $context);
if ($idSearchResult->getTotal() > 0 && !empty($idSearchResult->firstId())) {
$configService->set('AcrisTaxCS.config.acrisAdvancedTaxRules', $idSearchResult->getIds());
}
}
}
private function getCountryIds(Context $context): array
{
$countryId = null;
$countryIds = [];
$countryISOs = [
'DE',
'AT',
];
$countryISO = 'NL';
$allISOs = array_merge($countryISOs, [$countryISO]);
/** @var EntityRepositoryInterface $countryRepository */
$countryRepository = $this->container->get('country.repository');
$criteria = (new Criteria())->addFilter(new EqualsAnyFilter('iso', $allISOs));
$countryCollection = $countryRepository->search($criteria, $context)->getEntities();
if ($countryCollection->count() === 0) {
return [null, null];
}
$valid = true;
/** @var CountryEntity $country */
foreach ($countryCollection->getElements() as $country) {
if ($country->getIso() === $countryISO) {
$countryId = $country->getId();
continue;
}
if (in_array($country->getIso(), $countryISOs)) {
$countryIds[] = $country->getId();
continue;
}
$valid = false;
}
if ($valid !== true) {
return [null, null];
}
return [$countryId, $countryIds];
}
private function addCustomFields(Context $context): void
{
/* Check for snippets if they exist for custom fields */
$this->checkForExistingCustomFieldSnippets($context);
$customFieldSet = $this->container->get('custom_field_set.repository');
if($customFieldSet->search((new Criteria())->addFilter(new EqualsFilter('name', self::CUSTOM_FIELD_SET_NAME_PRODUCT_EXCLUDE_TAX_FREE)), $context)->count() == 0) {
$customFieldSet->create([[
'name' => self::CUSTOM_FIELD_SET_NAME_PRODUCT_EXCLUDE_TAX_FREE,
'config' => [
'label' => [
'en-GB' => 'ACRIS Tax',
'de-DE' => 'ACRIS Steuer'
]
],
'customFields' => [
['name' => 'acris_tax_exclude_product_tax_free_warehouse', 'type' => CustomFieldTypes::TEXT,
'config' => [
'componentName' => 'sw-field',
'type' => 'text',
'customFieldType' => 'text',
'customFieldPosition' => 1,
'label' => [
'en-GB' => 'Tax tag',
'de-DE' => 'Steuer-Tag'
],
'helpText' => [
'en-GB' => 'This field will be used and checked in the "Tax tag" rule condition.',
'de-DE' => 'Dieses Feld wird in der Regelbedingung "Steuer-Tag" verwendet und geprüft.'
],
'placeholder' => [
'en-GB' => 'Enter tax tag...',
'de-DE' => 'Steuer-Tag eingeben...'
]
]]
],
'relations' => [
[
'entityName' => 'product'
]
]
]], $context);
}
}
private function checkForExistingCustomFieldSnippets(Context $context)
{
/** @var EntityRepositoryInterface $snippetRepository */
$snippetRepository = $this->container->get('snippet.repository');
$criteria = new Criteria();
$criteria->addFilter(new MultiFilter(MultiFilter::CONNECTION_OR, [
new EqualsFilter('translationKey', 'customFields.' . 'acris_tax_exclude_product_tax_free_warehouse')
]));
/** @var EntitySearchResult $searchResult */
$searchResult = $snippetRepository->search($criteria, $context);
if ($searchResult->count() > 0) {
$snippetIds = [];
/** @var SnippetEntity $snippet */
foreach ($searchResult->getEntities()->getElements() as $snippet) {
$snippetIds[] = [
'id' => $snippet->getId()
];
}
if (!empty($snippetIds)) {
$snippetRepository->delete($snippetIds, $context);
}
}
}
}