1<?php 2/** 3 * Copyright since 2007 PrestaShop SA and Contributors 4 * PrestaShop is an International Registered Trademark & Property of PrestaShop SA 5 * 6 * NOTICE OF LICENSE 7 * 8 * This source file is subject to the Open Software License (OSL 3.0) 9 * that is bundled with this package in the file LICENSE.md. 10 * It is also available through the world-wide-web at this URL: 11 * https://opensource.org/licenses/OSL-3.0 12 * If you did not receive a copy of the license and are unable to 13 * obtain it through the world-wide-web, please send an email 14 * to license@prestashop.com so we can send you a copy immediately. 15 * 16 * DISCLAIMER 17 * 18 * Do not edit or add to this file if you wish to upgrade PrestaShop to newer 19 * versions in the future. If you wish to customize PrestaShop for your 20 * needs please refer to https://devdocs.prestashop.com/ for more information. 21 * 22 * @author PrestaShop SA and Contributors <contact@prestashop.com> 23 * @copyright Since 2007 PrestaShop SA and Contributors 24 * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) 25 */ 26 27namespace PrestaShop\PrestaShop\Adapter\Cart\QueryHandler; 28 29use Address; 30use AddressFormat; 31use Carrier; 32use Cart; 33use CartRule; 34use Currency; 35use Customer; 36use Link; 37use Message; 38use PrestaShop\Decimal\DecimalNumber; 39use PrestaShop\PrestaShop\Adapter\Cart\AbstractCartHandler; 40use PrestaShop\PrestaShop\Adapter\ContextStateManager; 41use PrestaShop\PrestaShop\Core\Domain\Cart\Exception\CartNotFoundException; 42use PrestaShop\PrestaShop\Core\Domain\Cart\Query\GetCartForOrderCreation; 43use PrestaShop\PrestaShop\Core\Domain\Cart\QueryHandler\GetCartForOrderCreationHandlerInterface; 44use PrestaShop\PrestaShop\Core\Domain\Cart\QueryResult\CartForOrderCreation; 45use PrestaShop\PrestaShop\Core\Domain\Cart\QueryResult\CartForOrderCreation\CartAddress; 46use PrestaShop\PrestaShop\Core\Domain\Cart\QueryResult\CartForOrderCreation\CartDeliveryOption; 47use PrestaShop\PrestaShop\Core\Domain\Cart\QueryResult\CartForOrderCreation\CartProduct; 48use PrestaShop\PrestaShop\Core\Domain\Cart\QueryResult\CartForOrderCreation\CartShipping; 49use PrestaShop\PrestaShop\Core\Domain\Cart\QueryResult\CartForOrderCreation\CartSummary; 50use PrestaShop\PrestaShop\Core\Domain\Cart\QueryResult\CartForOrderCreation\Customization; 51use PrestaShop\PrestaShop\Core\Domain\Cart\QueryResult\CartForOrderCreation\CustomizationFieldData; 52use PrestaShop\PrestaShop\Core\Localization\Exception\LocalizationException; 53use PrestaShop\PrestaShop\Core\Localization\LocaleInterface; 54use PrestaShopException; 55use Product; 56use Shop; 57use Symfony\Component\Translation\TranslatorInterface; 58use Tools; 59 60/** 61 * Handles GetCartForOrderCreation query using legacy object models 62 */ 63final class GetCartForOrderCreationHandler extends AbstractCartHandler implements GetCartForOrderCreationHandlerInterface 64{ 65 /** 66 * @var LocaleInterface 67 */ 68 private $locale; 69 70 /** 71 * @var int 72 */ 73 private $contextLangId; 74 75 /** 76 * @var Link 77 */ 78 private $contextLink; 79 80 /** 81 * @var ContextStateManager 82 */ 83 private $contextStateManager; 84 85 /** 86 * @var TranslatorInterface 87 */ 88 private $translator; 89 90 /** 91 * @param LocaleInterface $locale 92 * @param int $contextLangId 93 * @param Link $contextLink 94 * @param ContextStateManager $contextStateManager 95 * @param TranslatorInterface $translator 96 */ 97 public function __construct( 98 LocaleInterface $locale, 99 int $contextLangId, 100 Link $contextLink, 101 ContextStateManager $contextStateManager, 102 TranslatorInterface $translator 103 ) { 104 $this->locale = $locale; 105 $this->contextLangId = $contextLangId; 106 $this->contextLink = $contextLink; 107 $this->contextStateManager = $contextStateManager; 108 $this->translator = $translator; 109 } 110 111 /** 112 * @param GetCartForOrderCreation $query 113 * 114 * @return CartForOrderCreation 115 * 116 * @throws CartNotFoundException 117 * @throws LocalizationException 118 * @throws PrestaShopException 119 */ 120 public function handle(GetCartForOrderCreation $query): CartForOrderCreation 121 { 122 $cart = $this->getCart($query->getCartId()); 123 $currency = new Currency($cart->id_currency); 124 $language = $cart->getAssociatedLanguage(); 125 126 $this->contextStateManager 127 ->setCart($cart) 128 ->setCurrency($currency) 129 ->setLanguage($language) 130 ->setCustomer(new Customer($cart->id_customer)) 131 ->setShop(new Shop($cart->id_shop)) 132 ; 133 134 try { 135 $addresses = $this->getAddresses($cart); 136 137 if ($query->hideDiscounts()) { 138 $legacySummary = $cart->getSummaryDetails($cart->getAssociatedLanguage()->getId(), true); 139 $products = $this->extractProductsWithGiftSplitFromLegacySummary($cart, $legacySummary, $currency); 140 } else { 141 $legacySummary = $cart->getRawSummaryDetails($cart->getAssociatedLanguage()->getId(), true); 142 $products = $this->extractProductsFromLegacySummary($cart, $legacySummary, $currency); 143 } 144 145 $result = new CartForOrderCreation( 146 $cart->id, 147 $products, 148 (int) $currency->id, 149 (int) $language->id, 150 $this->extractCartRulesFromLegacySummary($cart, $legacySummary, $currency, $query->hideDiscounts()), 151 $addresses, 152 $this->extractSummaryFromLegacySummary($legacySummary, $currency, $cart), 153 $addresses ? $this->extractShippingFromLegacySummary($cart, $legacySummary, $query->hideDiscounts()) : null 154 ); 155 } finally { 156 $this->contextStateManager->restorePreviousContext(); 157 } 158 159 return $result; 160 } 161 162 /** 163 * @param Cart $cart 164 * 165 * @return CartAddress[] 166 */ 167 private function getAddresses(Cart $cart): array 168 { 169 $customer = new Customer($cart->id_customer); 170 $cartAddresses = []; 171 172 foreach ($customer->getAddresses($cart->getAssociatedLanguage()->getId()) as $data) { 173 $addressId = (int) $data['id_address']; 174 $cartAddresses[$addressId] = $this->buildCartAddress($addressId, $cart); 175 } 176 177 // Add addresses already assigned to cart if absent (in case they are deleted) 178 if (0 !== (int) $cart->id_address_delivery && !isset($cartAddresses[$cart->id_address_delivery])) { 179 $cartAddresses[$cart->id_address_delivery] = $this->buildCartAddress( 180 $cart->id_address_delivery, 181 $cart 182 ); 183 } 184 if (0 !== (int) $cart->id_address_invoice && !isset($cartAddresses[$cart->id_address_invoice])) { 185 $cartAddresses[$cart->id_address_invoice] = $this->buildCartAddress( 186 $cart->id_address_invoice, 187 $cart 188 ); 189 } 190 191 return array_values($cartAddresses); 192 } 193 194 /** 195 * @param int $addressId 196 * @param Cart $cart 197 * 198 * @return CartAddress 199 */ 200 private function buildCartAddress(int $addressId, Cart $cart): CartAddress 201 { 202 $address = new Address($addressId); 203 204 return new CartAddress( 205 $address->id, 206 $address->alias, 207 AddressFormat::generateAddress($address, [], '<br />'), 208 (int) $cart->id_address_delivery === $address->id, 209 (int) $cart->id_address_invoice === $address->id 210 ); 211 } 212 213 /** 214 * @param Cart $cart 215 * @param array $legacySummary 216 * @param Currency $currency 217 * @param bool $hideDiscounts 218 * 219 * @return CartForOrderCreation\CartRule[] 220 */ 221 private function extractCartRulesFromLegacySummary(Cart $cart, array $legacySummary, Currency $currency, bool $hideDiscounts = false): array 222 { 223 $cartRules = []; 224 225 foreach ($legacySummary['discounts'] as $discount) { 226 $cartRuleId = (int) $discount['id_cart_rule']; 227 $cartRules[$cartRuleId] = new CartForOrderCreation\CartRule( 228 (int) $discount['id_cart_rule'], 229 $discount['name'], 230 $discount['description'], 231 (new DecimalNumber((string) $discount['value_tax_exc']))->round($currency->precision) 232 ); 233 } 234 235 if ($hideDiscounts) { 236 foreach ($cart->getCartRules(CartRule::FILTER_ACTION_GIFT) as $giftRule) { 237 $giftRuleId = (int) $giftRule['id_cart_rule']; 238 $finalValue = new DecimalNumber((string) $giftRule['value_tax_exc']); 239 240 if (isset($cartRules[$giftRuleId])) { 241 // it is possible that one cart rule can have a gift product, but also have other conditions, 242 //so we need to sum their reduction values 243 /** @var CartForOrderCreation\CartRule $cartRule */ 244 $cartRule = $cartRules[$giftRuleId]; 245 $finalValue = $finalValue->plus(new DecimalNumber($cartRule->getValue())); 246 } 247 248 $cartRules[$giftRuleId] = new CartForOrderCreation\CartRule( 249 (int) $giftRule['id_cart_rule'], 250 $giftRule['name'], 251 $giftRule['description'], 252 $finalValue->round($currency->precision) 253 ); 254 } 255 } 256 257 return $cartRules; 258 } 259 260 /** 261 * @param Cart $cart 262 * @param array $legacySummary 263 * @param Currency $currency 264 * 265 * @return CartProduct[] 266 */ 267 private function extractProductsWithGiftSplitFromLegacySummary(Cart $cart, array $legacySummary, Currency $currency): array 268 { 269 $products = []; 270 $mergedGifts = $this->mergeGiftProducts($legacySummary['gift_products']); 271 272 foreach ($legacySummary['products'] as $product) { 273 $productKey = $this->generateUniqueProductKey($product); 274 275 //decrease product quantity for each identical product which is marked as gift 276 if (isset($mergedGifts[$productKey])) { 277 $identicalGiftedProduct = $mergedGifts[$productKey]; 278 $product['quantity'] -= $identicalGiftedProduct['quantity']; 279 } 280 281 $products[] = $this->buildCartProduct($cart, $currency, $product); 282 } 283 284 foreach ($mergedGifts as $product) { 285 $products[] = $this->buildCartProduct($cart, $currency, $product); 286 } 287 288 return $products; 289 } 290 291 /** 292 * @param Cart $cart 293 * @param array $legacySummary 294 * @param Currency $currency 295 * 296 * @return CartProduct[] 297 */ 298 private function extractProductsFromLegacySummary(Cart $cart, array $legacySummary, Currency $currency): array 299 { 300 $products = []; 301 foreach ($legacySummary['products'] as $product) { 302 $products[] = $this->buildCartProduct($cart, $currency, $product); 303 } 304 305 return $products; 306 } 307 308 /** 309 * @param array $giftProducts 310 * 311 * @return array 312 */ 313 private function mergeGiftProducts(array $giftProducts): array 314 { 315 $mergedGifts = []; 316 317 foreach ($giftProducts as $giftProduct) { 318 $productKey = $this->generateUniqueProductKey($giftProduct); 319 320 if (!isset($mergedGifts[$productKey])) { 321 // set first gift and make sure its quantity is 1. 322 $mergedGifts[$productKey] = $giftProduct; 323 $mergedGifts[$productKey]['quantity'] = 1; 324 } else { 325 //increase existing gift quantity by 1 326 ++$mergedGifts[$productKey]['quantity']; 327 } 328 } 329 330 return $mergedGifts; 331 } 332 333 /** 334 * Forms a unique product key using combination and customization ids. 335 * 336 * @param array $product 337 * 338 * @return string 339 */ 340 private function generateUniqueProductKey(array $product): string 341 { 342 return sprintf( 343 '%s_%s_%s', 344 (int) $product['id_product'], 345 (int) $product['id_product_attribute'], 346 (int) $product['id_customization'] 347 ); 348 } 349 350 /** 351 * @param Cart $cart 352 * @param array $legacySummary 353 * @param bool $hideDiscounts 354 * 355 * @return CartShipping|null 356 */ 357 private function extractShippingFromLegacySummary(Cart $cart, array $legacySummary, bool $hideDiscounts = true): ?CartShipping 358 { 359 $deliveryOptionsByAddress = $cart->getDeliveryOptionList(); 360 $deliveryAddress = (int) $cart->id_address_delivery; 361 362 //Check if there is any delivery options available for cart delivery address 363 if (!array_key_exists($deliveryAddress, $deliveryOptionsByAddress)) { 364 return null; 365 } 366 367 /** @var Carrier $carrier */ 368 $carrier = $legacySummary['carrier']; 369 $isFreeShipping = !empty($cart->getCartRules(CartRule::FILTER_ACTION_SHIPPING)); 370 371 return new CartShipping( 372 $isFreeShipping && $hideDiscounts ? '0' : (string) $legacySummary['total_shipping'], 373 $isFreeShipping, 374 $this->fetchCartDeliveryOptions($deliveryOptionsByAddress, $deliveryAddress), 375 (int) $carrier->id ?: null, 376 (bool) $cart->gift, 377 (bool) $cart->recyclable, 378 $cart->gift_message 379 ); 380 } 381 382 /** 383 * Fetch CartDeliveryOption[] DTO's from legacy array 384 * 385 * @param array $deliveryOptionsByAddress 386 * @param int $deliveryAddressId 387 * 388 * @return array 389 */ 390 private function fetchCartDeliveryOptions(array $deliveryOptionsByAddress, int $deliveryAddressId) 391 { 392 $deliveryOptions = []; 393 // legacy multishipping feature allowed to split cart shipping to multiple addresses. 394 // now when the multishipping feature is removed 395 // the list of carriers should be shared across whole cart for single delivery address 396 foreach ($deliveryOptionsByAddress[$deliveryAddressId] as $deliveryOption) { 397 foreach ($deliveryOption['carrier_list'] as $carrier) { 398 $carrier = $carrier['instance']; 399 // make sure there is no duplicate carrier 400 $deliveryOptions[(int) $carrier->id] = new CartDeliveryOption( 401 (int) $carrier->id, 402 $carrier->name, 403 $carrier->delay[$this->contextLangId] 404 ); 405 } 406 } 407 408 //make sure array is not associative 409 return array_values($deliveryOptions); 410 } 411 412 /** 413 * @param array $legacySummary 414 * @param Currency $currency 415 * @param Cart $cart 416 * 417 * @return CartSummary 418 * 419 * @throws LocalizationException 420 */ 421 private function extractSummaryFromLegacySummary(array $legacySummary, Currency $currency, Cart $cart): CartSummary 422 { 423 $cartId = (int) $cart->id; 424 425 $discount = $this->locale->formatPrice(-1 * $legacySummary['total_discounts_tax_exc'], $currency->iso_code); 426 427 $orderMessage = ''; 428 if ($message = Message::getMessageByCartId($cartId)) { 429 $orderMessage = $message['message']; 430 } 431 432 return new CartSummary( 433 $this->locale->formatPrice($legacySummary['total_products'], $currency->iso_code), 434 $discount, 435 $this->locale->formatPrice($legacySummary['total_shipping'], $currency->iso_code), 436 $this->locale->formatPrice($legacySummary['total_shipping_tax_exc'], $currency->iso_code), 437 $this->locale->formatPrice($legacySummary['total_tax'], $currency->iso_code), 438 $this->locale->formatPrice($legacySummary['total_price'], $currency->iso_code), 439 $this->locale->formatPrice($legacySummary['total_price_without_tax'], $currency->iso_code), 440 $orderMessage, 441 $this->contextLink->getPageLink( 442 'order', 443 false, 444 (int) $cart->getAssociatedLanguage()->getId(), 445 http_build_query([ 446 'step' => 3, 447 'recover_cart' => $cartId, 448 'token_cart' => md5(_COOKIE_KEY_ . 'recover_cart_' . $cartId), 449 ]) 450 ) 451 ); 452 } 453 454 /** 455 * Provides product customizations data 456 * 457 * @param Cart $cart 458 * @param array $product the product array from legacy summary 459 * 460 * @return Customization|null 461 */ 462 private function getProductCustomizedData(Cart $cart, array $product): ?Customization 463 { 464 $customizationId = (int) $product['id_customization']; 465 466 if (!$customizationId) { 467 return null; 468 } 469 470 $customizations = Product::getAllCustomizedDatas( 471 $cart->id, 472 $cart->getAssociatedLanguage()->getId(), 473 true, 474 null, 475 $customizationId 476 ); 477 478 if ($customizations) { 479 $productCustomizedFieldsData = $this->getProductCustomizedFieldsData($customizations, $product); 480 } 481 482 if (empty($productCustomizedFieldsData)) { 483 return null; 484 } 485 486 return new CartForOrderCreation\Customization($customizationId, $productCustomizedFieldsData); 487 } 488 489 /** 490 * Provides customized fields data for product 491 * 492 * @param array $customizations 493 * @param array $product 494 * 495 * @return array 496 */ 497 private function getProductCustomizedFieldsData(array $customizations, array $product) 498 { 499 $customizationFieldsData = []; 500 501 if (isset($customizations[$product['id_product']][$product['id_product_attribute']])) { 502 foreach ($customizations[$product['id_product']][$product['id_product_attribute']] as $customizationByAddress) { 503 foreach ($customizationByAddress as $customization) { 504 if (isset($customization['datas'][Product::CUSTOMIZE_TEXTFIELD])) { 505 foreach ($customization['datas'][Product::CUSTOMIZE_TEXTFIELD] as $text) { 506 $customizationFieldsData[] = new CustomizationFieldData( 507 Product::CUSTOMIZE_TEXTFIELD, 508 $text['name'], 509 $text['value'] 510 ); 511 } 512 } 513 514 if (isset($customization['datas'][Product::CUSTOMIZE_FILE])) { 515 foreach ($customization['datas'][Product::CUSTOMIZE_FILE] as $file) { 516 $customizationFieldsData[] = new CustomizationFieldData( 517 Product::CUSTOMIZE_FILE, 518 $file['name'], 519 _THEME_PROD_PIC_DIR_ . $file['value'] . '_small' 520 ); 521 } 522 } 523 } 524 } 525 } 526 527 return $customizationFieldsData; 528 } 529 530 /** 531 * @param Cart $cart 532 * @param Currency $currency 533 * @param array $product 534 * 535 * @return CartProduct 536 */ 537 private function buildCartProduct( 538 Cart $cart, 539 Currency $currency, 540 array $product 541 ): CartProduct { 542 return new CartProduct( 543 (int) $product['id_product'], 544 isset($product['id_product_attribute']) ? (int) $product['id_product_attribute'] : 0, 545 $product['name'], 546 isset($product['attributes_small']) ? $product['attributes_small'] : '', 547 $product['reference'], 548 Tools::ps_round($product['price'], $currency->precision), 549 $product['quantity'], 550 Tools::ps_round($product['total'], $currency->precision), 551 $this->contextLink->getImageLink($product['link_rewrite'], $product['id_image'], 'small_default'), 552 $this->getProductCustomizedData($cart, $product), 553 Product::getQuantity( 554 (int) $product['id_product'], 555 isset($product['id_product_attribute']) ? (int) $product['id_product_attribute'] : null 556 ), 557 Product::isAvailableWhenOutOfStock((int) $product['out_of_stock']) !== 0, 558 !empty($product['is_gift']) 559 ); 560 } 561} 562