Issues (2)

src/OrderProcessing/VatNumberOrderProcessor.php (1 issue)

Labels
Severity
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Gewebe\SyliusVATPlugin\OrderProcessing;
6
7
use Doctrine\ORM\EntityManagerInterface;
8
use Gewebe\SyliusVATPlugin\Entity\VatNumberAddressInterface;
9
use Sylius\Component\Addressing\Model\ZoneInterface;
10
use Sylius\Component\Addressing\Repository\ZoneRepositoryInterface;
11
use Sylius\Component\Core\Model\AdjustmentInterface;
12
use Sylius\Component\Core\Model\Scope;
13
use Sylius\Component\Core\Resolver\TaxationAddressResolverInterface;
14
use Sylius\Component\Order\Model\OrderInterface;
15
use Sylius\Component\Order\Processor\OrderProcessorInterface;
16
17
/**
18
 * Recalculates the order without VAT tax
19
 */
20
final class VatNumberOrderProcessor implements OrderProcessorInterface
21
{
22
    private ?ZoneInterface $euZone;
23
24
    public function __construct(
25
        private EntityManagerInterface $entityManager,
26
        private ZoneRepositoryInterface $zoneRepository,
27
        private TaxationAddressResolverInterface $taxationAddressResolver,
28
        private bool $isActive = true,
29
    ) {
30
    }
31
32
    /**
33
     * @param \Sylius\Component\Core\Model\OrderInterface $order
34
     *
35
     * @phpstan-ignore-next-line
36
     */
37
    public function process(OrderInterface $order): void
38
    {
39
        $this->euZone = $this->getEuZone();
40
41
        if (!$this->isActive || $this->euZone === null) {
42
            return;
43
        }
44
45
        if ($this->isValidForZeroTax($order)) {
46
            $this->removeIncludedTaxes($order);
47
48
            $order->removeAdjustmentsRecursively(AdjustmentInterface::TAX_ADJUSTMENT);
49
        }
50
    }
51
52
    private function removeIncludedTaxes(OrderInterface $order): void
53
    {
54
        foreach ($order->getAdjustments(AdjustmentInterface::TAX_ADJUSTMENT) as $taxAdjustment) {
55
            if ($taxAdjustment->isNeutral()) {
56
                foreach ($order->getAdjustments(AdjustmentInterface::SHIPPING_ADJUSTMENT) as $shipmentAdjustment) {
57
                    if ($shipmentAdjustment->getDetails()['shippingMethodCode'] == $taxAdjustment->getDetails()['shippingMethodCode']) {
58
                        $shipmentAdjustment->setAmount($shipmentAdjustment->getAmount() - $taxAdjustment->getAmount());
59
                    }
60
                }
61
            }
62
        }
63
64
        foreach ($order->getItems() as $item) {
65
            $includedTaxes = 0;
66
            foreach ($item->getAdjustmentsRecursively(AdjustmentInterface::TAX_ADJUSTMENT) as $taxAdjustment) {
67
                if ($taxAdjustment->isNeutral()) {
68
                    $includedTaxes += $taxAdjustment->getAmount();
69
                }
70
            }
71
72
            if ($includedTaxes > 0) {
73
                $unitTax = (int) floor($includedTaxes / $item->getQuantity());
74
75
                $item->setUnitPrice($item->getUnitPrice() - $unitTax);
76
                $item->recalculateUnitsTotal();
77
            }
78
        }
79
    }
80
81
    /**
82
     * @param \Sylius\Component\Core\Model\OrderInterface $order
83
     */
84
    private function isValidForZeroTax(OrderInterface $order): bool
85
    {
86
        $channel = $order->getChannel();
0 ignored issues
show
The method getChannel() does not exist on Sylius\Component\Order\Model\OrderInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Sylius\Component\Order\Model\Order. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

86
        /** @scrutinizer ignore-call */ 
87
        $channel = $order->getChannel();
Loading history...
87
        if ($channel === null) {
88
            return false;
89
        }
90
91
        $shopBillingData = $channel->getShopBillingData();
92
        if ($shopBillingData === null ||
93
            !$this->isEuZone($shopBillingData->getCountryCode())) {
94
            return false;
95
        }
96
97
        $taxationAddress = $this->taxationAddressResolver->getTaxationAddressFromOrder($order);
98
99
        if ($taxationAddress instanceof VatNumberAddressInterface &&
100
            $taxationAddress->hasValidVatNumber() &&
101
            $this->isEuZone($taxationAddress->getCountryCode()) &&
102
            $taxationAddress->getCountryCode() !== $shopBillingData->getCountryCode()) {
103
            return true;
104
        }
105
106
        return false;
107
    }
108
109
    private function getEuZone(): ?ZoneInterface
110
    {
111
        /** @var ZoneInterface|null $euZone */
112
        $euZone = $this->zoneRepository->findOneBy(['code' => 'EU', 'scope' => Scope::ALL]);
113
114
        // @fixme ZoneRepository finds Zone properly with all members, after OrderTaxesProcessor have been executed
115
        if ($euZone instanceof ZoneInterface) {
116
            $this->entityManager->refresh($euZone);
117
        }
118
119
        return $euZone;
120
    }
121
122
    private function isEuZone(?string $countyCode): bool
123
    {
124
        if ($countyCode === null || $this->euZone === null) {
125
            return false;
126
        }
127
128
        foreach ($this->euZone->getMembers() as $member) {
129
            $zoneMemberCode = $member->getCode();
130
            if (null !== $zoneMemberCode) {
131
                $posDivider = strpos($zoneMemberCode, '-');
132
133
                if ($posDivider !== false) {
134
                    $zoneMemberCode = substr($zoneMemberCode, 0, $posDivider);
135
                }
136
            }
137
138
            if ($zoneMemberCode === $countyCode) {
139
                return true;
140
            }
141
        }
142
143
        return false;
144
    }
145
}
146