CommonTaxCollectorPlugin.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. <?php
  2. /**
  3. * @copyright Vertex. All rights reserved. https://www.vertexinc.com/
  4. * @author Mediotype https://www.mediotype.com/
  5. */
  6. namespace Vertex\Tax\Model\Plugin;
  7. use Magento\Catalog\Api\Data\ProductInterface;
  8. use Magento\Catalog\Api\ProductRepositoryInterface;
  9. use Magento\Customer\Api\Data\AddressInterface;
  10. use Magento\Framework\Api\SearchCriteriaBuilder;
  11. use Magento\Framework\Api\SearchCriteriaBuilderFactory;
  12. use Magento\Quote\Api\Data\ShippingAssignmentInterface;
  13. use Magento\Quote\Model\Quote\Address;
  14. use Magento\Quote\Model\Quote\Item\AbstractItem;
  15. use Magento\Tax\Api\Data\QuoteDetailsItemExtensionFactory;
  16. use Magento\Tax\Api\Data\QuoteDetailsItemExtensionInterface;
  17. use Magento\Tax\Api\Data\QuoteDetailsItemInterface;
  18. use Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory;
  19. use Magento\Tax\Api\Data\TaxClassKeyInterface;
  20. use Magento\Tax\Model\Sales\Total\Quote\CommonTaxCollector;
  21. use Vertex\Tax\Model\Config;
  22. use Vertex\Tax\Model\Repository\TaxClassNameRepository;
  23. /**
  24. * Plugins to the Common Tax Collector
  25. */
  26. class CommonTaxCollectorPlugin
  27. {
  28. /** @var Config */
  29. private $config;
  30. /** @var SearchCriteriaBuilderFactory */
  31. private $criteriaBuilderFactory;
  32. /** @var QuoteDetailsItemExtensionFactory */
  33. private $extensionFactory;
  34. /** @var ProductRepositoryInterface */
  35. private $productRepository;
  36. /** @var TaxClassNameRepository */
  37. private $taxClassNameRepository;
  38. /**
  39. * @param QuoteDetailsItemExtensionFactory $extensionFactory
  40. * @param ProductRepositoryInterface $productRepository
  41. * @param SearchCriteriaBuilderFactory $criteriaBuilderFactory
  42. * @param TaxClassNameRepository $taxClassNameRepository
  43. * @param Config $config
  44. */
  45. public function __construct(
  46. QuoteDetailsItemExtensionFactory $extensionFactory,
  47. ProductRepositoryInterface $productRepository,
  48. SearchCriteriaBuilderFactory $criteriaBuilderFactory,
  49. TaxClassNameRepository $taxClassNameRepository,
  50. Config $config
  51. ) {
  52. $this->extensionFactory = $extensionFactory;
  53. $this->config = $config;
  54. $this->productRepository = $productRepository;
  55. $this->criteriaBuilderFactory = $criteriaBuilderFactory;
  56. $this->taxClassNameRepository = $taxClassNameRepository;
  57. }
  58. /**
  59. * Fetch and store the tax class of the child of any configurable products mapped
  60. *
  61. * Steps we take:
  62. * 1. Reduce the items to process from all items to those that are configurable products
  63. * 2. Retrieve an array of those items SKUs - due to the nature of configurable products, they will be the
  64. * simple's sku
  65. * 3. Fetch all products for items we want to process
  66. * 4. Create a mapping of product sku -> tax class id
  67. * 5. Fetch all tax class names
  68. * 6. Go through the product sku mapping and override the tax class ids on the parent products' items
  69. *
  70. * @param CommonTaxCollector $subject
  71. * @param QuoteDetailsItemInterface[] $items
  72. * @return QuoteDetailsItemInterface[]
  73. */
  74. public function afterMapItems(CommonTaxCollector $subject, array $items)
  75. {
  76. // Manually providing the store ID is not necessary
  77. if (!$this->config->isVertexActive()) {
  78. return $items;
  79. }
  80. /** @var QuoteDetailsItemInterface[] $processItems indexed by product sku */
  81. $processItems = array_reduce(
  82. $items,
  83. function ($carry, QuoteDetailsItemInterface $item) {
  84. if ($item->getExtensionAttributes() && $item->getExtensionAttributes()->getVertexIsConfigurable()) {
  85. $carry[strtoupper($item->getExtensionAttributes()->getVertexProductCode())] = $item;
  86. }
  87. return $carry;
  88. },
  89. []
  90. );
  91. /** @var string[] $productCodes List of SKUs we want to know the tax classes of */
  92. $productCodes = array_keys($processItems);
  93. /** @var SearchCriteriaBuilder $criteriaBuilder */
  94. $criteriaBuilder = $this->criteriaBuilderFactory->create();
  95. $criteriaBuilder->addFilter(ProductInterface::SKU, $productCodes, 'in');
  96. $criteria = $criteriaBuilder->create();
  97. $products = $this->productRepository->getList($criteria)->getItems();
  98. /** @var int[] $productCodeTaxClassMap Mapping of product sku (key) to tax class IDs */
  99. $productCodeTaxClassMap = [];
  100. /** @var ProductInterface[] $products */
  101. foreach ($products as $product) {
  102. $attribute = $product->getCustomAttribute('tax_class_id');
  103. $taxClassId = $attribute ? $attribute->getValue() : null;
  104. $productCodeTaxClassMap[strtoupper($product->getSku())] = $taxClassId;
  105. }
  106. /** @var int[] $taxClassIds */
  107. $taxClassIds = array_values($productCodeTaxClassMap);
  108. $taxClasses = $this->taxClassNameRepository->getListByIds($taxClassIds);
  109. foreach ($productCodeTaxClassMap as $productCode => $taxClassId) {
  110. $processItems[$productCode]->setTaxClassId($taxClasses[$taxClassId]);
  111. $processItems[$productCode]->getTaxClassKey()->setValue($taxClassId);
  112. }
  113. return $items;
  114. }
  115. /**
  116. * Add a created SKU for shipping to the QuoteDetailsItem
  117. *
  118. * @param CommonTaxCollector $subject
  119. * @param callable $super
  120. * @param ShippingAssignmentInterface $shippingAssignment
  121. * @param \Magento\Quote\Model\Quote\Address\Total $total
  122. * @param bool $useBaseCurrency
  123. * @return QuoteDetailsItemInterface
  124. */
  125. public function aroundGetShippingDataObject(
  126. CommonTaxCollector $subject,
  127. callable $super,
  128. ShippingAssignmentInterface $shippingAssignment,
  129. $total,
  130. $useBaseCurrency
  131. ) {
  132. // Allows forward compatibility with argument additions
  133. $arguments = func_get_args();
  134. array_splice($arguments, 0, 2);
  135. /** @var QuoteDetailsItemInterface[] $quoteItems */
  136. $itemDataObject = call_user_func_array($super, $arguments);
  137. $store = $this->getStoreCodeFromShippingAssignment($shippingAssignment);
  138. if ($itemDataObject === null || !$this->config->isVertexActive($store) || !$this->config->isTaxCalculationEnabled($store)) {
  139. return $itemDataObject;
  140. }
  141. $shipping = $shippingAssignment->getShipping();
  142. if ($shipping === null) {
  143. return $itemDataObject;
  144. }
  145. if ($shipping->getMethod() === null && $total->getShippingTaxCalculationAmount() == 0) {
  146. // If there's no method and a $0 price then there's no need for an empty shipping tax item
  147. return null;
  148. }
  149. $extensionAttributes = $this->getExtensionAttributes($itemDataObject);
  150. $extensionAttributes->setVertexProductCode($shippingAssignment->getShipping()->getMethod());
  151. return $itemDataObject;
  152. }
  153. /**
  154. * Add VAT ID to Address used in Tax Calculation
  155. *
  156. * @see CommonTaxCollector::mapAddress()
  157. * @param CommonTaxCollector $subject
  158. * @param callable $super
  159. * @param Address $address
  160. * @return AddressInterface
  161. */
  162. public function aroundMapAddress(
  163. CommonTaxCollector $subject,
  164. callable $super,
  165. Address $address
  166. ) {
  167. $arguments = func_get_args();
  168. array_splice($arguments, 0, 2);
  169. /** @var AddressInterface $customerAddress */
  170. $customerAddress = call_user_func_array($super, $arguments);
  171. $customerAddress->setVatId($address->getVatId());
  172. return $customerAddress;
  173. }
  174. /**
  175. * Add product SKU to a QuoteDetailsItem
  176. *
  177. * @see CommonTaxCollector::mapItem()
  178. * @param CommonTaxCollector $subject
  179. * @param callable $super
  180. * @param QuoteDetailsItemInterfaceFactory $dataObjectFactory
  181. * @param AbstractItem $item
  182. * @param bool $priceIncludesTax
  183. * @param bool $useBaseCurrency
  184. * @param string|null $parentCode
  185. * @return QuoteDetailsItemInterface
  186. */
  187. public function aroundMapItem(
  188. CommonTaxCollector $subject,
  189. callable $super,
  190. $dataObjectFactory,
  191. AbstractItem $item,
  192. $priceIncludesTax,
  193. $useBaseCurrency,
  194. $parentCode = null
  195. ) {
  196. // Allows forward compatibility with argument additions
  197. $arguments = func_get_args();
  198. array_splice($arguments, 0, 2);
  199. /** @var QuoteDetailsItemInterface $taxData */
  200. $taxData = call_user_func_array($super, $arguments);
  201. if ($this->config->isVertexActive($item->getStoreId())) {
  202. $extensionData = $this->getExtensionAttributes($taxData);
  203. $extensionData->setVertexProductCode($item->getProduct()->getSku());
  204. $extensionData->setVertexIsConfigurable($item->getProduct()->getTypeId() === 'configurable');
  205. }
  206. return $taxData;
  207. }
  208. /**
  209. * Add a created SKU and update the tax class of Item-level Giftwrap
  210. *
  211. * @param CommonTaxCollector $subject
  212. * @param callable $super
  213. * @param QuoteDetailsItemInterfaceFactory $dataObjectFactory
  214. * @param AbstractItem $item
  215. * @param $priceIncludesTax
  216. * @param $useBaseCurrency
  217. * @return QuoteDetailsItemInterface[]
  218. */
  219. public function aroundMapItemExtraTaxables(
  220. CommonTaxCollector $subject,
  221. callable $super,
  222. $dataObjectFactory,
  223. AbstractItem $item,
  224. $priceIncludesTax,
  225. $useBaseCurrency
  226. ) {
  227. // Allows forward compatibility with argument additions
  228. $arguments = func_get_args();
  229. array_splice($arguments, 0, 2);
  230. /** @var QuoteDetailsItemInterface[] $quoteItems */
  231. $quoteItems = call_user_func_array($super, $arguments);
  232. $store = $item->getStore();
  233. if (!$this->config->isVertexActive($store->getStoreId())) {
  234. return $quoteItems;
  235. }
  236. foreach ($quoteItems as $quoteItem) {
  237. if ($quoteItem->getType() !== 'item_gw') {
  238. continue;
  239. }
  240. $productSku = $item->getProduct()->getSku();
  241. $taxClassId = $this->config->getGiftWrappingItemClass($store);
  242. $gwPrefix = $this->config->getGiftWrappingItemCodePrefix($store);
  243. // Set the Product Code
  244. $extensionData = $this->getExtensionAttributes($quoteItem);
  245. $extensionData->setVertexProductCode($gwPrefix.$productSku);
  246. // Change the Tax Class ID
  247. $quoteItem->setTaxClassId($taxClassId);
  248. $taxClassKey = $quoteItem->getTaxClassKey();
  249. if ($taxClassKey && $taxClassKey->getType() === TaxClassKeyInterface::TYPE_ID) {
  250. $quoteItem->getTaxClassKey()->setValue($taxClassId);
  251. }
  252. }
  253. return $quoteItems;
  254. }
  255. /**
  256. * Retrieve an extension attribute object for the QuoteDetailsItem
  257. *
  258. * @param QuoteDetailsItemInterface $taxData
  259. * @return QuoteDetailsItemExtensionInterface
  260. */
  261. private function getExtensionAttributes(QuoteDetailsItemInterface $taxData)
  262. {
  263. $extensionAttributes = $taxData->getExtensionAttributes();
  264. if ($extensionAttributes instanceof QuoteDetailsItemExtensionInterface) {
  265. return $extensionAttributes;
  266. }
  267. $extensionAttributes = $this->extensionFactory->create();
  268. $taxData->setExtensionAttributes($extensionAttributes);
  269. return $extensionAttributes;
  270. }
  271. /**
  272. * Retrieve the Store ID from a Shipping Assignment
  273. *
  274. * This is the same way the Magento_Tax module gets the store when its needed - we have a problem, though, where
  275. * getQuote isn't part of the AddressInterface, and I don't particularly trust all the getters to not unexpectedly
  276. * return NULL.
  277. *
  278. * @param ShippingAssignmentInterface|null $shippingAssignment
  279. * @return string|null
  280. */
  281. private function getStoreCodeFromShippingAssignment(ShippingAssignmentInterface $shippingAssignment = null)
  282. {
  283. return $shippingAssignment !== null
  284. && $shippingAssignment->getShipping() !== null
  285. && $shippingAssignment->getShipping()->getAddress() !== null
  286. && method_exists($shippingAssignment->getShipping()->getAddress(), 'getQuote')
  287. && $shippingAssignment->getShipping()->getAddress()->getQuote() !== null
  288. ? $shippingAssignment->getShipping()->getAddress()->getQuote()->getStoreId()
  289. : null;
  290. }
  291. }