diff --git a/src/Adapter/Presenter/Cart/CartPresenter.php b/src/Adapter/Presenter/Cart/CartPresenter.php index d76519102140c..b15063e788feb 100644 --- a/src/Adapter/Presenter/Cart/CartPresenter.php +++ b/src/Adapter/Presenter/Cart/CartPresenter.php @@ -26,57 +26,41 @@ namespace PrestaShop\PrestaShop\Adapter\Presenter\Cart; +use Cache; use Cart; -use CartRule; use Configuration; use Context; -use Country; +use Exception; use Hook; +use Link; use PrestaShop\PrestaShop\Adapter\Image\ImageRetriever; use PrestaShop\PrestaShop\Adapter\Presenter\PresenterInterface; -use PrestaShop\PrestaShop\Adapter\Presenter\Product\ProductLazyArray; -use PrestaShop\PrestaShop\Adapter\Presenter\Product\ProductListingLazyArray; -use PrestaShop\PrestaShop\Adapter\Presenter\Product\ProductListingPresenter; -use PrestaShop\PrestaShop\Adapter\Product\PriceFormatter; -use PrestaShop\PrestaShop\Adapter\Product\ProductColorsRetriever; use PrestaShop\PrestaShop\Core\Product\ProductPresentationSettings; use Product; use ProductAssembler; -use Symfony\Contracts\Translation\TranslatorInterface; use TaxConfiguration; -use Tools; class CartPresenter implements PresenterInterface { /** - * @var PriceFormatter - */ - private $priceFormatter; - - /** - * @var \Link + * @var Link */ private $link; - /** - * @var TranslatorInterface - */ - private $translator; - /** * @var ImageRetriever */ private $imageRetriever; /** - * @var TaxConfiguration + * @var ProductPresentationSettings */ - private $taxConfiguration; + protected $settings; /** - * @var ProductPresentationSettings + * @var TaxConfiguration */ - protected $settings; + private $taxConfiguration; /** * @var ProductAssembler @@ -85,106 +69,11 @@ class CartPresenter implements PresenterInterface public function __construct() { - $context = Context::getContext(); - $this->priceFormatter = new PriceFormatter(); - $this->link = $context->link; - $this->translator = $context->getTranslator(); + $this->link = Context::getContext()->link; $this->imageRetriever = new ImageRetriever($this->link); $this->taxConfiguration = new TaxConfiguration(); } - /** - * @return bool - */ - private function includeTaxes() - { - return $this->taxConfiguration->includeTaxes(); - } - - /** - * @param array $rawProduct - * - * @return ProductLazyArray|ProductListingLazyArray - */ - private function presentProduct(array $rawProduct) - { - $assembledProduct = $this->getProductAssembler()->assembleProduct($rawProduct); - $rawProduct = array_merge($assembledProduct, $rawProduct); - - if (isset($rawProduct['attributes']) && is_string($rawProduct['attributes'])) { - $rawProduct['attributes'] = $this->getAttributesArrayFromString($rawProduct['attributes']); - } - $rawProduct['remove_from_cart_url'] = $this->link->getRemoveFromCartURL( - $rawProduct['id_product'], - $rawProduct['id_product_attribute'] - ); - - $rawProduct['up_quantity_url'] = $this->link->getUpQuantityCartURL( - $rawProduct['id_product'], - $rawProduct['id_product_attribute'] - ); - - $rawProduct['down_quantity_url'] = $this->link->getDownQuantityCartURL( - $rawProduct['id_product'], - $rawProduct['id_product_attribute'] - ); - - $rawProduct['update_quantity_url'] = $this->link->getUpdateQuantityCartURL( - $rawProduct['id_product'], - $rawProduct['id_product_attribute'] - ); - - $resetFields = [ - 'ecotax_rate', - 'specific_prices', - 'customizable', - 'online_only', - 'reduction', - 'reduction_without_tax', - 'new', - 'condition', - 'pack', - ]; - foreach ($resetFields as $field) { - if (!array_key_exists($field, $rawProduct)) { - $rawProduct[$field] = ''; - } - } - - $rawProduct['price'] = Tools::ps_round($rawProduct['price'], Context::getContext()->getComputingPrecision()); - $rawProduct['price_wt'] = Tools::ps_round($rawProduct['price_wt'], Context::getContext()->getComputingPrecision()); - - if ($this->includeTaxes()) { - $rawProduct['price_amount'] = $rawProduct['price'] = $rawProduct['price_wt']; - $rawProduct['unit_price'] = $rawProduct['unit_price_tax_included']; - } else { - $rawProduct['price_amount'] = $rawProduct['price_tax_exc'] = $rawProduct['price']; - $rawProduct['unit_price'] = $rawProduct['unit_price_tax_excluded']; - } - - $rawProduct['total'] = $this->priceFormatter->format( - $this->includeTaxes() ? - $rawProduct['total_wt'] : - $rawProduct['total'] - ); - - $rawProduct['quantity_wanted'] = $rawProduct['cart_quantity']; - - $presenter = new ProductListingPresenter( - $this->imageRetriever, - $this->link, - $this->priceFormatter, - new ProductColorsRetriever(), - $this->translator - ); - - return $presenter->present( - $this->getSettings(), - $rawProduct, - Context::getContext()->language - ); - } - /** * @param array $products * @param Cart $cart @@ -308,352 +197,25 @@ public function addCustomizedData(array $products, Cart $cart) /** * @param Cart $cart - * @param bool $shouldSeparateGifts * - * @return array - * - * @throws \Exception + * @throws Exception */ - public function present($cart, $shouldSeparateGifts = false) + public function present($cart, bool $shouldSeparateGifts = false): CartLazyArray { - if (!is_a($cart, 'Cart')) { - throw new \Exception('CartPresenter can only present instance of Cart'); - } - - if ($shouldSeparateGifts) { - $rawProducts = $cart->getProductsWithSeparatedGifts(); - } else { - $rawProducts = $cart->getProducts(true); + $cache_id = 'presentedCart_' . (int) $shouldSeparateGifts . $cart->id; + if (Cache::isStored($cache_id)) { + return Cache::retrieve($cache_id); } - $products = array_map([$this, 'presentProduct'], $rawProducts); - $products = $this->addCustomizedData($products, $cart); - $subtotals = []; - - $productsTotalExcludingTax = $cart->getOrderTotal(false, Cart::ONLY_PRODUCTS); - $total_excluding_tax = $cart->getOrderTotal(false); - $total_including_tax = $cart->getOrderTotal(true); - $total_discount = $cart->getDiscountSubtotalWithoutGifts($this->includeTaxes()); - $totalCartAmount = $cart->getOrderTotal($this->includeTaxes(), Cart::ONLY_PRODUCTS); - - $subtotals['products'] = [ - 'type' => 'products', - 'label' => $this->translator->trans('Subtotal', [], 'Shop.Theme.Checkout'), - 'amount' => $totalCartAmount, - 'value' => $this->priceFormatter->format($totalCartAmount), - ]; - - if ($total_discount) { - $subtotals['discounts'] = [ - 'type' => 'discount', - 'label' => $this->translator->trans('Discount(s)', [], 'Shop.Theme.Checkout'), - 'amount' => $total_discount, - 'value' => $this->priceFormatter->format($total_discount), - ]; - } else { - $subtotals['discounts'] = null; - } - - if ($cart->gift) { - $giftWrappingPrice = ($cart->getGiftWrappingPrice($this->includeTaxes()) != 0) - ? $cart->getGiftWrappingPrice($this->includeTaxes()) - : 0; - - $subtotals['gift_wrapping'] = [ - 'type' => 'gift_wrapping', - 'label' => $this->translator->trans('Gift wrapping', [], 'Shop.Theme.Checkout'), - 'amount' => $giftWrappingPrice, - 'value' => ($giftWrappingPrice > 0) - ? $this->priceFormatter->convertAndFormat($giftWrappingPrice) - : $this->translator->trans('Free', [], 'Shop.Theme.Checkout'), - ]; - } - - if (!$cart->isVirtualCart()) { - $shippingCost = $cart->getTotalShippingCost(null, $this->includeTaxes()); - } else { - $shippingCost = 0; - } - $subtotals['shipping'] = [ - 'type' => 'shipping', - 'label' => $this->translator->trans('Shipping', [], 'Shop.Theme.Checkout'), - 'amount' => $shippingCost, - 'value' => $this->getShippingDisplayValue($cart, $shippingCost), - ]; - - $subtotals['tax'] = null; - if (Configuration::get('PS_TAX_DISPLAY')) { - $taxAmount = $total_including_tax - $total_excluding_tax; - $subtotals['tax'] = [ - 'type' => 'tax', - 'label' => ($this->includeTaxes()) - ? $this->translator->trans('Included taxes', [], 'Shop.Theme.Checkout') - : $this->translator->trans('Taxes', [], 'Shop.Theme.Checkout'), - 'amount' => $taxAmount, - 'value' => $this->priceFormatter->format($taxAmount), - ]; - } - - $totals = [ - 'total' => [ - 'type' => 'total', - 'label' => $this->translator->trans('Total', [], 'Shop.Theme.Checkout'), - 'amount' => $this->includeTaxes() ? $total_including_tax : $total_excluding_tax, - 'value' => $this->priceFormatter->format( - $this->includeTaxes() ? $total_including_tax : $total_excluding_tax - ), - ], - 'total_including_tax' => [ - 'type' => 'total', - 'label' => $this->translator->trans('Total (tax incl.)', [], 'Shop.Theme.Checkout'), - 'amount' => $total_including_tax, - 'value' => $this->priceFormatter->format($total_including_tax), - ], - 'total_excluding_tax' => [ - 'type' => 'total', - 'label' => $this->translator->trans('Total (tax excl.)', [], 'Shop.Theme.Checkout'), - 'amount' => $total_excluding_tax, - 'value' => $this->priceFormatter->format($total_excluding_tax), - ], - ]; - - $products_count = array_reduce($products, function ($count, $product) { - return $count + $product['quantity']; - }, 0); - - $summary_string = $products_count === 1 ? - $this->translator->trans('1 item', [], 'Shop.Theme.Checkout') : - $this->translator->trans('%count% items', ['%count%' => $products_count], 'Shop.Theme.Checkout'); - - $minimalPurchase = $this->priceFormatter->convertAmount((float) Configuration::get('PS_PURCHASE_MINIMUM')); - - Hook::exec('overrideMinimalPurchasePrice', [ - 'minimalPurchase' => &$minimalPurchase, - ]); - - // TODO: move it to a common parent, since it's copied in OrderPresenter and ProductPresenter - $labels = [ - 'tax_short' => ($this->includeTaxes()) - ? $this->translator->trans('(tax incl.)', [], 'Shop.Theme.Global') - : $this->translator->trans('(tax excl.)', [], 'Shop.Theme.Global'), - 'tax_long' => ($this->includeTaxes()) - ? $this->translator->trans('(tax included)', [], 'Shop.Theme.Global') - : $this->translator->trans('(tax excluded)', [], 'Shop.Theme.Global'), - ]; - - $discounts = $cart->getDiscounts(); - $vouchers = $this->getTemplateVarVouchers($cart); - - $cartRulesIds = array_flip(array_map( - function ($voucher) { - return $voucher['id_cart_rule']; - }, - $vouchers['added'] - )); - - $discounts = array_filter($discounts, function ($discount) use ($cartRulesIds, $cart) { - $voucherCustomerId = (int) $discount['id_customer']; - $voucherIsRestrictedToASingleCustomer = ($voucherCustomerId !== 0); - $voucherIsEmptyCode = empty($discount['code']); - if ($voucherIsRestrictedToASingleCustomer && $cart->id_customer !== $voucherCustomerId && $voucherIsEmptyCode) { - return false; - } - - return !array_key_exists($discount['id_cart_rule'], $cartRulesIds); - }); - - $result = [ - 'products' => $products, - 'totals' => $totals, - 'subtotals' => $subtotals, - 'products_count' => $products_count, - 'summary_string' => $summary_string, - 'labels' => $labels, - 'id_address_delivery' => $cart->id_address_delivery, - 'id_address_invoice' => $cart->id_address_invoice, - 'is_virtual' => $cart->isVirtualCart(), - 'vouchers' => $vouchers, - 'discounts' => $discounts, - 'minimalPurchase' => $minimalPurchase, - 'minimalPurchaseRequired' => ($productsTotalExcludingTax < $minimalPurchase) ? - $this->translator->trans( - 'A minimum shopping cart total of %amount% (tax excl.) is required to validate your order. Current cart total is %total% (tax excl.).', - [ - '%amount%' => $this->priceFormatter->format($minimalPurchase), - '%total%' => $this->priceFormatter->format($productsTotalExcludingTax), - ], - 'Shop.Theme.Checkout' - ) : - '', - ]; + $cartLazyArray = new CartLazyArray($cart, $this, $shouldSeparateGifts); Hook::exec('actionPresentCart', - ['presentedCart' => &$result] + ['presentedCart' => &$cartLazyArray] ); - return $result; - } - - /** - * Accepts a cart object with the shipping cost amount and formats the shipping cost display value accordingly. - * If the shipping cost is 0, then we must check if this is because of a free carrier and thus display 'Free' or - * simply because the system was unable to determine shipping cost at this point and thus send an empty string to hide the shipping line. - * - * @param Cart $cart - * @param float $shippingCost - * - * @return string - */ - private function getShippingDisplayValue($cart, $shippingCost) - { - $shippingDisplayValue = ''; - - // if one of the applied cart rules have free shipping, then the shipping display value is 'Free' - foreach ($cart->getCartRules() as $rule) { - if ($rule['free_shipping'] && !$rule['carrier_restriction']) { - return $this->translator->trans('Free', [], 'Shop.Theme.Checkout'); - } - } - - if ($shippingCost != 0) { - $shippingDisplayValue = $this->priceFormatter->format($shippingCost); - } else { - $defaultCountry = null; - - if (isset(Context::getContext()->cookie->id_country)) { - $defaultCountry = new Country((int) Context::getContext()->cookie->id_country); - } - - $deliveryOptionList = $cart->getDeliveryOptionList($defaultCountry); - - if (count($deliveryOptionList) > 0) { - foreach ($deliveryOptionList as $option) { - foreach ($option as $currentCarrier) { - if (isset($currentCarrier['is_free']) && $currentCarrier['is_free'] > 0) { - $shippingDisplayValue = $this->translator->trans('Free', [], 'Shop.Theme.Checkout'); - break 2; - } - } - } - } - } + Cache::store($cache_id, $cartLazyArray); - return $shippingDisplayValue; - } - - private function getTemplateVarVouchers(Cart $cart) - { - $cartVouchers = $cart->getCartRules(); - $vouchers = []; - - $cartHasTax = null === $cart->id ? false : $cart->getAverageProductsTaxRate() * 100; - $freeShippingAlreadySet = false; - /** @var array{id_cart_rule:int, name: string, code: string, reduction_percent: float, reduction_currency: int, free_shipping: bool, reduction_tax: bool, reduction_amount:float, value_real:float|int|string, value_tax_exc:float|int|string} $cartVoucher */ - foreach ($cartVouchers as $cartVoucher) { - $vouchers[$cartVoucher['id_cart_rule']]['id_cart_rule'] = $cartVoucher['id_cart_rule']; - $vouchers[$cartVoucher['id_cart_rule']]['name'] = $cartVoucher['name']; - $vouchers[$cartVoucher['id_cart_rule']]['code'] = $cartVoucher['code']; - $vouchers[$cartVoucher['id_cart_rule']]['reduction_percent'] = $cartVoucher['reduction_percent']; - $vouchers[$cartVoucher['id_cart_rule']]['reduction_currency'] = $cartVoucher['reduction_currency']; - $vouchers[$cartVoucher['id_cart_rule']]['free_shipping'] = (bool) $cartVoucher['free_shipping']; - - // Voucher reduction depending of the cart tax rule - // if $cartHasTax & voucher is tax excluded, set amount voucher to tax included - if ($cartHasTax && $cartVoucher['reduction_tax'] == '0') { - $cartVoucher['reduction_amount'] = $cartVoucher['reduction_amount'] * (1 + $cartHasTax / 100); - } - - $vouchers[$cartVoucher['id_cart_rule']]['reduction_amount'] = $cartVoucher['reduction_amount']; - - if ($this->cartVoucherHasGiftProductReduction($cartVoucher)) { - $cartVoucher['reduction_amount'] = $cartVoucher['value_real']; - } - - $totalCartVoucherReduction = 0; - - if ($this->cartVoucherHasFreeShippingOnly($cartVoucher)) { - $freeShippingOnly = true; - if ($freeShippingAlreadySet) { - unset($vouchers[$cartVoucher['id_cart_rule']]); - continue; - } else { - $freeShippingAlreadySet = true; - } - } else { - $freeShippingOnly = false; - $totalCartVoucherReduction = $this->includeTaxes() ? $cartVoucher['value_real'] : $cartVoucher['value_tax_exc']; - } - - // when a voucher has only a shipping reduction, the value displayed must be "Free Shipping" - if ($freeShippingOnly) { - $cartVoucher['reduction_formatted'] = $this->translator->trans( - 'Free shipping', - [], - 'Admin.Shipping.Feature' - ); - } else { - $cartVoucher['reduction_formatted'] = '-' . $this->priceFormatter->format($totalCartVoucherReduction); - } - $vouchers[$cartVoucher['id_cart_rule']]['reduction_formatted'] = $cartVoucher['reduction_formatted']; - $vouchers[$cartVoucher['id_cart_rule']]['delete_url'] = $this->link->getPageLink( - 'cart', - null, - null, - [ - 'deleteDiscount' => $cartVoucher['id_cart_rule'], - 'token' => Tools::getToken(false), - ] - ); - } - - return [ - 'allowed' => (int) CartRule::isFeatureActive(), - 'added' => $vouchers, - ]; - } - - /** - * @param array $cartVoucher - * - * @return bool - */ - private function cartVoucherHasPercentReduction(array $cartVoucher): bool - { - return isset($cartVoucher['reduction_percent']) - && $cartVoucher['reduction_percent'] > 0 - && $cartVoucher['reduction_amount'] == '0.00'; - } - - /** - * @param array $cartVoucher - * - * @return bool - */ - private function cartVoucherHasAmountReduction(array $cartVoucher): bool - { - return isset($cartVoucher['reduction_amount']) && $cartVoucher['reduction_amount'] > 0; - } - - /** - * @param array $cartVoucher - * - * @return bool - */ - private function cartVoucherHasGiftProductReduction(array $cartVoucher): bool - { - return !empty($cartVoucher['gift_product']); - } - - /** - * @param array $cartVoucher - * - * @return bool - */ - private function cartVoucherHasFreeShippingOnly(array $cartVoucher): bool - { - return !$this->cartVoucherHasPercentReduction($cartVoucher) - && !$this->cartVoucherHasAmountReduction($cartVoucher) - && !$this->cartVoucherHasGiftProductReduction($cartVoucher); + return $cartLazyArray; } /** @@ -663,7 +225,7 @@ private function cartVoucherHasFreeShippingOnly(array $cartVoucher): bool * * @return array Converted attributes in an array */ - protected function getAttributesArrayFromString($attributes) + public function getAttributesArrayFromString($attributes) { $separator = Configuration::get('PS_ATTRIBUTE_ANCHOR_SEPARATOR'); $pattern = '/(?>(?P[^:]+:[^:]+)' . $separator . '+(?!' . $separator . '([^:' . $separator . '])+:))/'; @@ -674,14 +236,14 @@ protected function getAttributesArrayFromString($attributes) } foreach ($matches['attribute'] as $attribute) { - list($key, $value) = explode(':', $attribute); + [$key, $value] = explode(':', $attribute); $attributesArray[trim($key)] = ltrim($value); } return $attributesArray; } - protected function getSettings(): ProductPresentationSettings + public function getSettings(): ProductPresentationSettings { if ($this->settings === null) { $this->settings = new ProductPresentationSettings(); @@ -699,7 +261,7 @@ protected function getSettings(): ProductPresentationSettings return $this->settings; } - protected function getProductAssembler(): ProductAssembler + public function getProductAssembler(): ProductAssembler { if ($this->productAssembler === null) { $this->productAssembler = new ProductAssembler(Context::getContext()); @@ -707,4 +269,9 @@ protected function getProductAssembler(): ProductAssembler return $this->productAssembler; } + + public function includeTaxes(): bool + { + return $this->taxConfiguration->includeTaxes(); + } }