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
27declare(strict_types=1);
28
29namespace PrestaShop\PrestaShop\Adapter\Order;
30
31use Address;
32use Cache;
33use Carrier;
34use Cart;
35use CartRule;
36use Currency;
37use Customer;
38use Order;
39use OrderCarrier;
40use OrderCartRule;
41use OrderDetail;
42use PrestaShop\Decimal\Number;
43use PrestaShop\PrestaShop\Adapter\Cart\Comparator\CartProductsComparator;
44use PrestaShop\PrestaShop\Adapter\Cart\Comparator\CartProductUpdate;
45use PrestaShop\PrestaShop\Adapter\ContextStateManager;
46use PrestaShop\PrestaShop\Adapter\Order\Refund\OrderProductRemover;
47use PrestaShop\PrestaShop\Core\Cart\CartRuleData;
48use PrestaShop\PrestaShop\Core\Domain\Configuration\ShopConfigurationInterface;
49use PrestaShop\PrestaShop\Core\Domain\Order\Exception\OrderException;
50use PrestaShop\PrestaShop\Core\Domain\Shop\ValueObject\ShopConstraint;
51use PrestaShop\PrestaShop\Core\Localization\CLDR\ComputingPrecision;
52use PrestaShopDatabaseException;
53use PrestaShopException;
54use Product;
55use Shop;
56use Tools;
57use Validate;
58
59class OrderAmountUpdater
60{
61    /**
62     * @var ShopConfigurationInterface
63     */
64    private $shopConfiguration;
65
66    /**
67     * @var ContextStateManager
68     */
69    private $contextStateManager;
70
71    /**
72     * @var OrderDetailUpdater
73     */
74    private $orderDetailUpdater;
75
76    /**
77     * @var OrderProductRemover
78     */
79    private $orderProductRemover;
80
81    /**
82     * @var array
83     */
84    private $orderConstraints = [];
85
86    /**
87     * @var bool
88     */
89    private $keepOrderPrices = true;
90
91    /**
92     * @param ShopConfigurationInterface $shopConfiguration
93     * @param ContextStateManager $contextStateManager
94     * @param OrderDetailUpdater $orderDetailUpdater
95     * @param OrderProductRemover $orderProductRemover
96     */
97    public function __construct(
98        ShopConfigurationInterface $shopConfiguration,
99        ContextStateManager $contextStateManager,
100        OrderDetailUpdater $orderDetailUpdater,
101        OrderProductRemover $orderProductRemover
102    ) {
103        $this->shopConfiguration = $shopConfiguration;
104        $this->contextStateManager = $contextStateManager;
105        $this->orderDetailUpdater = $orderDetailUpdater;
106        $this->orderProductRemover = $orderProductRemover;
107    }
108
109    /**
110     * @param Order $order
111     * @param Cart $cart
112     * @param int|null $orderInvoiceId
113     *
114     * @throws OrderException
115     * @throws PrestaShopDatabaseException
116     * @throws PrestaShopException
117     */
118    public function update(
119        Order $order,
120        Cart $cart,
121        ?int $orderInvoiceId = null
122    ): void {
123        $this->cleanCaches();
124
125        $this->contextStateManager
126            ->saveCurrentContext()
127            ->setCart($cart)
128            ->setCurrency(new Currency($cart->id_currency))
129            ->setCustomer(new Customer($cart->id_customer))
130            ->setLanguage($cart->getAssociatedLanguage())
131            ->setCountry($cart->getTaxCountry())
132            ->setShop(new Shop($cart->id_shop))
133        ;
134
135        try {
136            // @todo: use https://github.com/PrestaShop/decimal for price computations
137            $computingPrecision = $this->getPrecisionFromCart($cart);
138
139            // Update order details (if quantity or product price have been modified)
140            $this->updateOrderDetails($order, $cart);
141
142            $productsComparator = new CartProductsComparator($cart);
143            // Recalculate cart rules and Fix differences between cart's cartRules and order's cartRules
144            $this->updateOrderCartRules($order, $cart, $computingPrecision, $orderInvoiceId);
145
146            // Synchronize modified products with the order
147            $modifiedProducts = $productsComparator->getModifiedProducts();
148            $this->updateOrderModifiedProducts($modifiedProducts, $cart, $order);
149
150            // Update order totals
151            $this->updateOrderTotals($order, $cart, $computingPrecision);
152
153            // Update carrier weight for shipping cost
154            $this->updateOrderCarrier($order, $cart);
155
156            // Order::update is called after previous functions so that we only call it once
157            if (!$order->update()) {
158                throw new OrderException('Could not update order invoice in database.');
159            }
160
161            $this->updateOrderInvoices($order, $cart, $computingPrecision);
162        } finally {
163            $this->contextStateManager->restorePreviousContext();
164        }
165    }
166
167    /**
168     * Synchronizes modified products from the cart with the order
169     *
170     * @param CartProductUpdate[] $modifiedProducts
171     * @param Cart $cart
172     * @param Order $order
173     *
174     * @throws OrderException
175     * @throws PrestaShopDatabaseException
176     * @throws PrestaShopException
177     */
178    private function updateOrderModifiedProducts(array $modifiedProducts, Cart $cart, Order $order): void
179    {
180        $productsToAddToOrder = [];
181        foreach ($modifiedProducts as $modifiedProduct) {
182            $orderProduct = $this->findProductInOrder($modifiedProduct, $order);
183            $cartProduct = $this->findProductInCart($modifiedProduct, $cart);
184            if (null === $cartProduct) {
185                // The product is not in the cart anymore: delete it from the order
186                $orderDetail = new OrderDetail($orderProduct['id_order_detail']);
187                $this->orderProductRemover->deleteProductFromOrder($order, $orderDetail, false);
188            } elseif (null === $orderProduct) {
189                // The product is not in the order but in the cart: add it to the order
190                $productsToAddToOrder[] = $cartProduct;
191            } else {
192                // the product is both in the cart and the order: update its quantity and price with the cart values
193                $orderDetail = new OrderDetail($orderProduct['id_order_detail']);
194                $orderDetail->product_quantity = $cartProduct['cart_quantity'];
195                $this->orderDetailUpdater->updateOrderDetail(
196                    $orderDetail,
197                    $order,
198                    new Number((string) $cartProduct['price_with_reduction_without_tax']),
199                    new Number((string) $cartProduct['price_with_reduction'])
200                );
201            }
202        }
203        if (count($productsToAddToOrder) > 0) {
204            $orderDetail = new OrderDetail();
205            $orderDetail->createList($order, $cart, $order->getCurrentState(), $productsToAddToOrder);
206        }
207    }
208
209    /**
210     * @param CartProductUpdate $productUpdate
211     * @param Cart $cart
212     *
213     * @return array|null
214     */
215    private function findProductInCart(CartProductUpdate $productUpdate, Cart $cart): ?array
216    {
217        $combinationId = null === $productUpdate->getCombinationId()
218            ? 0
219            : $productUpdate->getCombinationId()->getValue();
220        foreach ($cart->getProducts() as $product) {
221            if ((int) $product['id_product'] === $productUpdate->getProductId()->getValue()
222                && (int) $product['id_product_attribute'] === $combinationId) {
223                return $product;
224            }
225        }
226
227        return null;
228    }
229
230    /**
231     * @param CartProductUpdate $productUpdate
232     * @param Order $order
233     *
234     * @return array|null
235     */
236    private function findProductInOrder(CartProductUpdate $productUpdate, Order $order): ?array
237    {
238        $combinationId = null === $productUpdate->getCombinationId()
239            ? 0
240            : $productUpdate->getCombinationId()->getValue();
241        foreach ($order->getProducts() as $product) {
242            if ((int) $product['product_id'] === $productUpdate->getProductId()->getValue()
243                && (int) $product['product_attribute_id'] === $combinationId) {
244                return $product;
245            }
246        }
247
248        return null;
249    }
250
251    /**
252     * There are many caches among legacy classes that can store previous prices
253     * we need to clean them to make sure the price is completely up to date
254     */
255    private function cleanCaches(): void
256    {
257        // For many intermediate computations
258        Cart::resetStaticCache();
259
260        // For discount computation
261        CartRule::resetStaticCache();
262        Cache::clean('getContextualValue_*');
263
264        // For shipping costs
265        Carrier::resetStaticCache();
266        Cache::clean('getPackageShippingCost_*');
267    }
268
269    /**
270     * @param Order $order
271     * @param Cart $cart
272     * @param int $computingPrecision
273     */
274    private function updateOrderTotals(Order $order, Cart $cart, int $computingPrecision): void
275    {
276        $orderProducts = $order->getCartProducts();
277
278        $carrierId = $order->id_carrier;
279        $order->total_discounts = (float) abs($cart->getOrderTotal(true, Cart::ONLY_DISCOUNTS, $orderProducts, $carrierId, false, $this->keepOrderPrices));
280        $order->total_discounts_tax_excl = (float) abs($cart->getOrderTotal(false, Cart::ONLY_DISCOUNTS, $orderProducts, $carrierId, false, $this->keepOrderPrices));
281        $order->total_discounts_tax_incl = (float) abs($cart->getOrderTotal(true, Cart::ONLY_DISCOUNTS, $orderProducts, $carrierId, false, $this->keepOrderPrices));
282
283        // We should always use Cart::BOTH for the order total since it contains all products, shipping fees and cart rules
284        $order->total_paid = Tools::ps_round(
285            (float) $cart->getOrderTotal(true, Cart::BOTH, $orderProducts, $carrierId, false, $this->keepOrderPrices),
286            $computingPrecision
287        );
288        $order->total_paid_tax_excl = Tools::ps_round(
289            (float) $cart->getOrderTotal(false, Cart::BOTH, $orderProducts, $carrierId, false, $this->keepOrderPrices),
290            $computingPrecision
291        );
292        $order->total_paid_tax_incl = Tools::ps_round(
293            (float) $cart->getOrderTotal(true, Cart::BOTH, $orderProducts, $carrierId, false, $this->keepOrderPrices),
294            $computingPrecision
295        );
296
297        $order->total_products = (float) $cart->getOrderTotal(false, Cart::ONLY_PRODUCTS, $orderProducts, $carrierId, false, $this->keepOrderPrices);
298        $order->total_products_wt = (float) $cart->getOrderTotal(true, Cart::ONLY_PRODUCTS, $orderProducts, $carrierId, false, $this->keepOrderPrices);
299
300        $order->total_wrapping = abs($cart->getOrderTotal(true, Cart::ONLY_WRAPPING, $orderProducts, $carrierId, false, $this->keepOrderPrices));
301        $order->total_wrapping_tax_excl = abs($cart->getOrderTotal(false, Cart::ONLY_WRAPPING, $orderProducts, $carrierId, false, $this->keepOrderPrices));
302        $order->total_wrapping_tax_incl = abs($cart->getOrderTotal(true, Cart::ONLY_WRAPPING, $orderProducts, $carrierId, false, $this->keepOrderPrices));
303
304        $totalShippingTaxIncluded = $order->total_shipping_tax_incl;
305        $totalShippingTaxExcluded = $order->total_shipping_tax_excl;
306
307        $order->total_shipping = $cart->getOrderTotal(true, Cart::ONLY_SHIPPING, $orderProducts, $carrierId, false, $this->keepOrderPrices);
308        $order->total_shipping_tax_excl = $cart->getOrderTotal(false, Cart::ONLY_SHIPPING, $orderProducts, $carrierId, false, $this->keepOrderPrices);
309        $order->total_shipping_tax_incl = $cart->getOrderTotal(true, Cart::ONLY_SHIPPING, $orderProducts, $carrierId, false, $this->keepOrderPrices);
310
311        if (!$this->getOrderConfiguration('PS_ORDER_RECALCULATE_SHIPPING', $order)) {
312            $freeShipping = $this->isFreeShipping($order);
313
314            if ($freeShipping) {
315                $order->total_discounts = $order->total_discounts - $order->total_shipping_tax_incl + $totalShippingTaxIncluded;
316                $order->total_discounts_tax_excl = $order->total_discounts_tax_excl - $order->total_shipping_tax_excl + $totalShippingTaxExcluded;
317                $order->total_discounts_tax_incl = $order->total_discounts_tax_incl - $order->total_shipping_tax_incl + $totalShippingTaxIncluded;
318            } else {
319                $order->total_paid -= ($order->total_shipping_tax_incl - $totalShippingTaxIncluded);
320                $order->total_paid_tax_incl -= ($order->total_shipping_tax_incl - $totalShippingTaxIncluded);
321                $order->total_paid_tax_excl -= ($order->total_shipping_tax_excl - $totalShippingTaxExcluded);
322            }
323
324            $order->total_shipping = $totalShippingTaxIncluded;
325            $order->total_shipping_tax_incl = $totalShippingTaxIncluded;
326            $order->total_shipping_tax_excl = $totalShippingTaxExcluded;
327        }
328    }
329
330    /**
331     * @param Order $order
332     * @param Cart $cart
333     *
334     * @throws PrestaShopDatabaseException
335     * @throws PrestaShopException
336     */
337    private function updateOrderCarrier(Order $order, Cart $cart): void
338    {
339        $orderCarrier = new OrderCarrier((int) $order->getIdOrderCarrier());
340
341        if (Validate::isLoadedObject($orderCarrier)) {
342            $orderCarrier->weight = (float) $order->getTotalWeight();
343            $orderCarrier->shipping_cost_tax_incl = (float) $order->total_shipping_tax_incl;
344            $orderCarrier->shipping_cost_tax_excl = (float) $order->total_shipping_tax_excl;
345
346            if ($orderCarrier->update()) {
347                $order->weight = sprintf('%.3f ' . $this->getOrderConfiguration('PS_WEIGHT_UNIT', $order), $orderCarrier->weight);
348            }
349        }
350
351        if (!$cart->isVirtualCart() && isset($order->id_carrier)) {
352            $carrier = new Carrier((int) $order->id_carrier, (int) $cart->id_lang);
353            if (null !== $carrier && Validate::isLoadedObject($carrier)) {
354                $taxAddressId = (int) $order->{$this->getOrderConfiguration('PS_TAX_ADDRESS_TYPE', $order)};
355                $order->carrier_tax_rate = $carrier->getTaxesRate(new Address($taxAddressId));
356            }
357        }
358    }
359
360    /**
361     * @param Order $order
362     * @param Cart $cart
363     *
364     * @throws OrderException
365     * @throws PrestaShopDatabaseException
366     * @throws PrestaShopException
367     */
368    private function updateOrderDetails(Order $order, Cart $cart): void
369    {
370        // Get cart products with prices kept from order
371        $cartProducts = $cart->getProducts(true, false, null, true, $this->keepOrderPrices);
372        foreach ($order->getCartProducts() as $orderProduct) {
373            $orderDetail = new OrderDetail($orderProduct['id_order_detail'], null, $this->contextStateManager->getContext());
374            $cartProduct = $this->getProductFromCart($cartProducts, (int) $orderDetail->product_id, (int) $orderDetail->product_attribute_id);
375
376            $this->orderDetailUpdater->updateOrderDetail(
377                $orderDetail,
378                $order,
379                new Number((string) $cartProduct['price_with_reduction_without_tax']),
380                new Number((string) $cartProduct['price_with_reduction'])
381            );
382        }
383    }
384
385    /**
386     * @param array $cartProducts
387     * @param int $productId
388     * @param int $productAttributeId
389     *
390     * @return array
391     */
392    private function getProductFromCart(array $cartProducts, int $productId, int $productAttributeId): array
393    {
394        $cartProduct = array_reduce($cartProducts, function ($carry, $item) use ($productId, $productAttributeId) {
395            if (null !== $carry) {
396                return $carry;
397            }
398
399            $productMatch = $item['id_product'] == $productId;
400            $combinationMatch = $item['id_product_attribute'] == $productAttributeId;
401
402            return $productMatch && $combinationMatch ? $item : null;
403        });
404
405        // This shouldn't happen, if it does something was not done before updating the Order (removing an OrderDetail maybe)
406        if (null === $cartProduct) {
407            throw new OrderException('Could not find the product in cart, meaning Order and Cart are out of sync');
408        }
409
410        return $cartProduct;
411    }
412
413    /**
414     * Update cart rules to be synced with current cart:
415     * - cart rules attached to new product may be added/removed
416     * - global shop cart rules may be added/removed
417     * - cart rules amount may vary because other cart rules have been added/removed
418     *
419     * @param Order $order
420     * @param Cart $cart
421     * @param int $computingPrecision
422     * @param int|null $orderInvoiceId
423     *
424     * @throws OrderException
425     * @throws PrestaShopDatabaseException
426     * @throws PrestaShopException
427     */
428    private function updateOrderCartRules(
429        Order $order,
430        Cart $cart,
431        int $computingPrecision,
432        ?int $orderInvoiceId
433    ): void {
434        CartRule::autoAddToCart(null, true);
435        CartRule::autoRemoveFromCart(null, true);
436        $carrierId = $order->id_carrier;
437
438        $newCartRules = $cart->getCartRules(CartRule::FILTER_ACTION_ALL, false);
439        // We need the calculator to compute the discount on the whole products because they can interact with each
440        // other so they can't be computed independently, it needs to keep order prices
441        $calculator = $cart->newCalculator($cart->getProducts(), $newCartRules, $carrierId, $computingPrecision, $this->keepOrderPrices);
442        $calculator->processCalculation();
443
444        foreach ($order->getCartRules() as $orderCartRuleData) {
445            /** @var CartRuleData $cartRuleData */
446            foreach ($calculator->getCartRulesData() as $cartRuleData) {
447                $cartRule = $cartRuleData->getCartRule();
448                if ($cartRule->id == $orderCartRuleData['id_cart_rule']) {
449                    // Cart rule is still in the cart no need to remove it, but we update it as the amount may have changed
450                    $orderCartRule = new OrderCartRule($orderCartRuleData['id_order_cart_rule']);
451                    $orderCartRule->id_order = $order->id;
452                    $orderCartRule->name = $cartRule->name;
453                    $orderCartRule->free_shipping = $cartRule->free_shipping;
454                    $orderCartRule->value = Tools::ps_round($cartRuleData->getDiscountApplied()->getTaxIncluded(), $computingPrecision);
455                    $orderCartRule->value_tax_excl = Tools::ps_round($cartRuleData->getDiscountApplied()->getTaxExcluded(), $computingPrecision);
456
457                    if ($orderCartRule->free_shipping && !$this->getOrderConfiguration('PS_ORDER_RECALCULATE_SHIPPING', $order)) {
458                        $orderCartRule->value = $orderCartRule->value - $calculator->getFees()->getInitialShippingFees()->getTaxIncluded() + $order->total_shipping;
459                        $orderCartRule->value_tax_excl = $orderCartRule->value_tax_excl - $calculator->getFees()->getInitialShippingFees()->getTaxExcluded() + $order->total_shipping_tax_excl;
460                    }
461
462                    $orderCartRule->save();
463                    continue 2;
464                }
465            }
466
467            // This one is no longer in the new cart rules so we delete it
468            $orderCartRule = new OrderCartRule($orderCartRuleData['id_order_cart_rule']);
469            // This one really needs to be deleted because it doesn't match the applied cart rules any more
470            // we don't use soft deleted here (unlike in the handler) but hard delete
471            if (!$orderCartRule->delete()) {
472                throw new OrderException('Could not delete order cart rule from database.');
473            }
474        }
475
476        // Finally add the new cart rules that are not in the Order
477        foreach ($calculator->getCartRulesData() as $cartRuleData) {
478            $cartRule = $cartRuleData->getCartRule();
479            foreach ($order->getCartRules() as $orderCartRuleData) {
480                if ($cartRule->id == $orderCartRuleData['id_cart_rule']) {
481                    // This cart rule is already present no need to add it
482                    continue 2;
483                }
484            }
485
486            // Add missing order cart rule
487            $orderCartRule = new OrderCartRule();
488            $orderCartRule->id_order = $order->id;
489            $orderCartRule->id_cart_rule = $cartRule->id;
490            $orderCartRule->id_order_invoice = $orderInvoiceId ?? 0;
491            $orderCartRule->name = $cartRule->name;
492            $orderCartRule->free_shipping = $cartRule->free_shipping;
493            $orderCartRule->value = Tools::ps_round($cartRuleData->getDiscountApplied()->getTaxIncluded(), $computingPrecision);
494            $orderCartRule->value_tax_excl = Tools::ps_round($cartRuleData->getDiscountApplied()->getTaxExcluded(), $computingPrecision);
495            $orderCartRule->save();
496        }
497    }
498
499    /**
500     * @param Order $order
501     * @param Cart $cart
502     * @param int $computingPrecision
503     *
504     * @throws OrderException
505     * @throws PrestaShopDatabaseException
506     * @throws PrestaShopException
507     */
508    private function updateOrderInvoices(Order $order, Cart $cart, int $computingPrecision): void
509    {
510        $invoiceProducts = [];
511        foreach ($order->getCartProducts() as $orderProduct) {
512            if (!empty($orderProduct['id_order_invoice'])) {
513                $invoiceProducts[$orderProduct['id_order_invoice']][] = $orderProduct;
514            }
515        }
516
517        $invoiceCollection = $order->getInvoicesCollection();
518        $firstInvoice = $invoiceCollection->getFirst();
519
520        foreach ($invoiceCollection as $invoice) {
521            // If all the invoice's products have been removed the offset won't exist
522            $currentInvoiceProducts = isset($invoiceProducts[$invoice->id]) ? $invoiceProducts[$invoice->id] : [];
523
524            // Shipping are computed on first invoice only
525            $carrierId = $order->id_carrier;
526            $totalMethod = ($firstInvoice === false || $firstInvoice->id == $invoice->id) ? Cart::BOTH : Cart::BOTH_WITHOUT_SHIPPING;
527            $invoice->total_paid_tax_excl = Tools::ps_round(
528                (float) $cart->getOrderTotal(false, $totalMethod, $currentInvoiceProducts, $carrierId, false, $this->keepOrderPrices),
529                $computingPrecision
530            );
531            $invoice->total_paid_tax_incl = Tools::ps_round(
532                (float) $cart->getOrderTotal(true, $totalMethod, $currentInvoiceProducts, $carrierId, false, $this->keepOrderPrices),
533                $computingPrecision
534            );
535
536            $invoice->total_products = Tools::ps_round(
537                (float) $cart->getOrderTotal(false, Cart::ONLY_PRODUCTS, $currentInvoiceProducts, $carrierId, false, $this->keepOrderPrices),
538                $computingPrecision
539            );
540            $invoice->total_products_wt = Tools::ps_round(
541                (float) $cart->getOrderTotal(true, Cart::ONLY_PRODUCTS, $currentInvoiceProducts, $carrierId, false, $this->keepOrderPrices),
542                $computingPrecision
543            );
544
545            $invoice->total_discount_tax_excl = Tools::ps_round(
546                (float) $cart->getOrderTotal(false, Cart::ONLY_DISCOUNTS, $currentInvoiceProducts, $carrierId, false, $this->keepOrderPrices),
547                $computingPrecision
548            );
549
550            $invoice->total_discount_tax_incl = Tools::ps_round(
551                (float) $cart->getOrderTotal(true, Cart::ONLY_DISCOUNTS, $currentInvoiceProducts, $carrierId, false, $this->keepOrderPrices),
552                $computingPrecision
553            );
554
555            $totalShippingTaxIncluded = $invoice->total_shipping_tax_incl;
556            $totalShippingTaxExcluded = $invoice->total_shipping_tax_excl;
557
558            $invoice->total_shipping = $cart->getOrderTotal(true, Cart::ONLY_SHIPPING, $currentInvoiceProducts, $carrierId, false, $this->keepOrderPrices);
559            $invoice->total_shipping_tax_excl = $cart->getOrderTotal(false, Cart::ONLY_SHIPPING, $currentInvoiceProducts, $carrierId, false, $this->keepOrderPrices);
560            $invoice->total_shipping_tax_incl = $cart->getOrderTotal(true, Cart::ONLY_SHIPPING, $currentInvoiceProducts, $carrierId, false, $this->keepOrderPrices);
561
562            if (!$this->getOrderConfiguration('PS_ORDER_RECALCULATE_SHIPPING', $order)) {
563                $freeShipping = $this->isFreeShipping($order);
564
565                if ($freeShipping) {
566                    $invoice->total_discount_tax_excl = $invoice->total_discount_tax_excl - $invoice->total_shipping_tax_excl + $totalShippingTaxExcluded;
567                    $invoice->total_discount_tax_incl = $invoice->total_discount_tax_incl - $invoice->total_shipping_tax_incl + $totalShippingTaxIncluded;
568                }
569
570                $invoice->total_shipping_tax_incl = $totalShippingTaxIncluded;
571                $invoice->total_shipping_tax_excl = $totalShippingTaxExcluded;
572
573                if (!$freeShipping) {
574                    $invoice->total_paid_tax_incl -= ($invoice->total_shipping_tax_incl - $totalShippingTaxIncluded);
575                    $invoice->total_paid_tax_excl -= ($invoice->total_shipping_tax_excl - $totalShippingTaxExcluded);
576                }
577            }
578
579            if (!$invoice->update()) {
580                throw new OrderException('Could not update order invoice in database.');
581            }
582        }
583    }
584
585    /**
586     * @param Order $order
587     *
588     * @return bool
589     */
590    protected function isFreeShipping(Order $order): bool
591    {
592        foreach ($order->getCartRules() as $cartRule) {
593            if ($cartRule['free_shipping']) {
594                return true;
595            }
596        }
597
598        return false;
599    }
600
601    /**
602     * @param Cart $cart
603     *
604     * @return int
605     */
606    private function getPrecisionFromCart(Cart $cart): int
607    {
608        $computingPrecision = new ComputingPrecision();
609        $currency = new Currency((int) $cart->id_currency);
610
611        return $computingPrecision->getPrecision((int) $currency->precision);
612    }
613
614    /**
615     * @param string $key
616     * @param Order $order
617     *
618     * @return mixed
619     */
620    private function getOrderConfiguration(string $key, Order $order)
621    {
622        return $this->shopConfiguration->get($key, null, $this->getOrderShopConstraint($order));
623    }
624
625    /**
626     * @param Order $order
627     *
628     * @return ShopConstraint
629     */
630    private function getOrderShopConstraint(Order $order): ShopConstraint
631    {
632        $constraintKey = $order->id_shop . '-' . $order->id_shop_group;
633        if (!isset($this->orderConstraints[$constraintKey])) {
634            $this->orderConstraints[$constraintKey] = ShopConstraint::shop((int) $order->id_shop);
635        }
636
637        return $this->orderConstraints[$constraintKey];
638    }
639}
640