Cart.php 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. declare(strict_types=1);
  7. namespace Magento\Paypal\Model;
  8. /**
  9. * PayPal-specific model for shopping cart items and totals
  10. * The main idea is to accommodate all possible totals into PayPal-compatible 4 totals and line items
  11. */
  12. class Cart extends \Magento\Payment\Model\Cart
  13. {
  14. /**
  15. * @var bool
  16. */
  17. protected $_areAmountsValid = false;
  18. /**
  19. * Get shipping, tax, subtotal and discount amounts all together
  20. *
  21. * @return array
  22. */
  23. public function getAmounts()
  24. {
  25. $this->_collectItemsAndAmounts();
  26. if (!$this->_areAmountsValid) {
  27. $subtotal = $this->getSubtotal() + $this->getTax();
  28. if (empty($this->_transferFlags[self::AMOUNT_SHIPPING])) {
  29. $subtotal += $this->getShipping();
  30. }
  31. if (empty($this->_transferFlags[self::AMOUNT_DISCOUNT])) {
  32. $subtotal -= $this->getDiscount();
  33. }
  34. return [self::AMOUNT_SUBTOTAL => $subtotal];
  35. }
  36. return $this->_amounts;
  37. }
  38. /**
  39. * Calculate subtotal from custom items
  40. *
  41. * @return void
  42. */
  43. protected function _calculateCustomItemsSubtotal()
  44. {
  45. parent::_calculateCustomItemsSubtotal();
  46. $this->_applyDiscountTaxCompensationWorkaround($this->_salesModel);
  47. $this->_validate();
  48. }
  49. /**
  50. * Check the line items and totals according to PayPal business logic limitations
  51. *
  52. * @return void
  53. */
  54. protected function _validate()
  55. {
  56. $areItemsValid = false;
  57. $this->_areAmountsValid = false;
  58. $referenceAmount = $this->_salesModel->getDataUsingMethod('base_grand_total');
  59. $itemsSubtotal = 0;
  60. foreach ($this->getAllItems() as $i) {
  61. $itemsSubtotal = $itemsSubtotal + $i->getQty() * $i->getAmount();
  62. }
  63. $sum = $itemsSubtotal + $this->getTax();
  64. if (empty($this->_transferFlags[self::AMOUNT_SHIPPING])) {
  65. $sum += $this->getShipping();
  66. }
  67. if (empty($this->_transferFlags[self::AMOUNT_DISCOUNT])) {
  68. $sum -= $this->getDiscount();
  69. // PayPal requires to have discount less than items subtotal
  70. $this->_areAmountsValid = round($this->getDiscount(), 4) < round($itemsSubtotal, 4);
  71. } else {
  72. $this->_areAmountsValid = $itemsSubtotal > 0.00001;
  73. }
  74. /**
  75. * numbers are intentionally converted to strings because of possible comparison error
  76. * see http://php.net/float
  77. */
  78. // match sum of all the items and totals to the reference amount
  79. if (sprintf('%.4F', $sum) == sprintf('%.4F', $referenceAmount)) {
  80. $areItemsValid = true;
  81. }
  82. $areItemsValid = $areItemsValid && $this->_areAmountsValid;
  83. if (!$areItemsValid) {
  84. $this->_salesModelItems = [];
  85. $this->_customItems = [];
  86. }
  87. }
  88. /**
  89. * Import items from sales model with workarounds for PayPal
  90. *
  91. * @return void
  92. */
  93. protected function _importItemsFromSalesModel()
  94. {
  95. $this->_salesModelItems = [];
  96. foreach ($this->_salesModel->getAllItems() as $item) {
  97. if ($item->getParentItem()) {
  98. continue;
  99. }
  100. $amount = $item->getPrice();
  101. $qty = $item->getQty();
  102. $subAggregatedLabel = '';
  103. // workaround in case if item subtotal precision is not compatible with PayPal (.2)
  104. if ($amount - round($amount, 2)) {
  105. $amount = $amount * $qty;
  106. $subAggregatedLabel = ' x' . $qty;
  107. $qty = 1;
  108. }
  109. // aggregate item price if item qty * price does not match row total
  110. $itemBaseRowTotal = $item->getOriginalItem()->getBaseRowTotal();
  111. if ($amount * $qty != $itemBaseRowTotal) {
  112. $amount = (double)$itemBaseRowTotal;
  113. $subAggregatedLabel = ' x' . $qty;
  114. $qty = 1;
  115. }
  116. $this->_salesModelItems[] = $this->_createItemFromData(
  117. $item->getName() . $subAggregatedLabel,
  118. $qty,
  119. $amount
  120. );
  121. }
  122. $this->addSubtotal($this->_salesModel->getBaseSubtotal());
  123. $this->addTax($this->_salesModel->getBaseTaxAmount());
  124. $this->addShipping($this->_salesModel->getBaseShippingAmount());
  125. $this->addDiscount(abs($this->_salesModel->getBaseDiscountAmount()));
  126. }
  127. /**
  128. * Add "hidden" discount and shipping tax
  129. *
  130. * Go ahead, try to understand ]:->
  131. *
  132. * Tax settings for getting "discount tax":
  133. * - Catalog Prices = Including Tax
  134. * - Apply Customer Tax = After Discount
  135. * - Apply Discount on Prices = Including Tax
  136. *
  137. * Test case for getting "hidden shipping tax":
  138. * - Make sure shipping is taxable (set shipping tax class)
  139. * - Catalog Prices = Including Tax
  140. * - Shipping Prices = Including Tax
  141. * - Apply Customer Tax = After Discount
  142. * - Create a cart price rule with % discount applied to the Shipping Amount
  143. * - run shopping cart and estimate shipping
  144. * - go to PayPal
  145. *
  146. * @param \Magento\Payment\Model\Cart\SalesModel\SalesModelInterface $salesEntity
  147. * @return void
  148. */
  149. protected function _applyDiscountTaxCompensationWorkaround(
  150. \Magento\Payment\Model\Cart\SalesModel\SalesModelInterface $salesEntity
  151. ) {
  152. $dataContainer = $salesEntity->getTaxContainer();
  153. $this->addTax((double)$dataContainer->getBaseDiscountTaxCompensationAmount());
  154. $this->addTax((double)$dataContainer->getBaseShippingDiscountTaxCompensationAmount());
  155. }
  156. /**
  157. * Check whether any item has negative amount
  158. *
  159. * @return bool
  160. */
  161. public function hasNegativeItemAmount()
  162. {
  163. foreach ($this->_customItems as $item) {
  164. if ($item->getAmount() < 0) {
  165. return true;
  166. }
  167. }
  168. return false;
  169. }
  170. }