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