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
Bug
introduced
by
![]() |
|||
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 |