Shopware 6: Tax Free with domestic delivery in DE (TaxDecorator & CartRuleLoader)

Viewed 50

Some customers with a domestic shipping address within Germany are still tax exempt (e.g. NATO, or US Army). Since this is considered an export delivery.

How can this be implemented? The only solution I have found is to completely override/replace CartRuleLoader

What I have done so far: I created a custom field in Customers, where a store editor can specify a customer as tax free (even within Germany).

Then I overwrote the class TaxDetector:

class TaxDetector extends \Shopware\Core\Checkout\Cart\Tax\TaxDetector
{

    public function isNetDelivery(SalesChannelContext $context): bool
    {
        $customer = $context->getCustomer();
        if ($customer && ($cf = $customer->getCustomFields()) && !empty($cf['mycustomfield']) && $cf['mycustomfield'] === 'nato') {
            return true;
        }

        return parent::isNetDelivery($context);
    }


    public function getTaxState(SalesChannelContext $context): string
    {
        $customer = $context->getCustomer();
        if ($customer && ($cf = $customer->getCustomFields()) && !empty($cf['mycustomfield']) && $cf['mycustomfield'] === 'nato') {
            return CartPrice::TAX_STATE_FREE;
        }

        return parent::getTaxState($context);
    }

}

The problem is that Shopware 6 checks the validity of the tax status in CartRuleLoader when completing an order. And here the test is done in private functions that I cannot override.

load -> validateTaxFree -> detectTaxType

Any suggestions on how to solve this more eligant and futerprove then override all CartRuleLoader?

1 Answers

This is indeed a little tricky.

When you look at the method CartRuleLoader::detectTaxType you can find these lines:

$currency = $context->getCurrency();
$currencyTaxFreeAmount = $currency->getTaxFreeFrom();
$isReachedCurrencyTaxFreeAmount = $currencyTaxFreeAmount > 0 && $cartNetAmount >= $currencyTaxFreeAmount;

if ($isReachedCurrencyTaxFreeAmount) {
    return CartPrice::TAX_STATE_FREE;
}

It's arguably a little hacky but you could "trick" Shopware to return the tax free state by setting the tax free amount of the currency to a minimal amount.

Therefore you could decorate Shopware\Core\System\SalesChannel\Context\SalesChannelContextFactory.

<service id="MyPlugin\Core\System\SalesChannel\Context\SalesChannelContextFactoryDecorator" decorates="Shopware\Core\System\SalesChannel\Context\SalesChannelContextFactory" decoration-priority="-999">
    <argument type="service" id="MyPlugin\Core\System\SalesChannel\Context\SalesChannelContextFactoryDecorator.inner"/>
</service>

In the decorator you could then have your logic and set the tax free amount:

<?php declare(strict_types=1);

namespace MyPlugin\Core\System\SalesChannel\Context;

use Shopware\Core\Checkout\Cart\Price\Struct\CartPrice;
use Shopware\Core\System\SalesChannel\Context\AbstractSalesChannelContextFactory;
use Shopware\Core\System\SalesChannel\SalesChannelContext;

class SalesChannelContextFactoryDecorator extends AbstractSalesChannelContextFactory
{
    private AbstractSalesChannelContextFactory $decorated;

    public function __construct(
        AbstractSalesChannelContextFactory $decorated,
    ) {
        $this->decorated = $decorated;
    }

    public function getDecorated(): AbstractSalesChannelContextFactory
    {
        return $this->decorated;
    }

    public function create(string $token, string $salesChannelId, array $options = []): SalesChannelContext
    {
        $context = $this->getDecorated()->create($token, $salesChannelId, $options);
        
        // ... your logic
        if (...) {
            $context->getCurrency()->setTaxFreeFrom(0.00001);
            $context->setTaxState(CartPrice::TAX_STATE_FREE);
        }   
        
        return $context;
    }
}

Edit: It looks like this could not be enough as there is a security check which checks if the previous state wasn't tax free and resets it otherwise.

if ($previous !== CartPrice::TAX_STATE_FREE) {
    $context->setTaxState($previous);
}

You might have to additionally set the tax state inside your decorator (updated the above example). The tax free amount would then make it stick.

Related