diff --git a/vendor/magento/module-negotiable-quote/Model/NegotiableQuoteConverter.php b/vendor/magento/module-negotiable-quote/Model/NegotiableQuoteConverter.php index a26f42750b..1c1fe8564c 100644 --- a/vendor/magento/module-negotiable-quote/Model/NegotiableQuoteConverter.php +++ b/vendor/magento/module-negotiable-quote/Model/NegotiableQuoteConverter.php @@ -3,10 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\NegotiableQuote\Model; +use Magento\Bundle\Model\Product\Type as BundleType; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable as ConfigurableType; use Magento\Framework\DataObject; +use Magento\Quote\Model\Quote\Item as QuoteItem; use Magento\Quote\Api\Data\CartInterface; use Magento\Quote\Api\Data\CartInterfaceFactory; use Magento\Catalog\Api\Data\ProductInterfaceFactory; @@ -16,6 +20,7 @@ use Magento\Quote\Api\Data\CartItemInterfaceFactory; use Magento\Quote\Api\Data\CartItemInterface; use Magento\NegotiableQuote\Api\Data\NegotiableQuoteItemInterfaceFactory; use Magento\NegotiableQuote\Api\Data\NegotiableQuoteInterfaceFactory; +use Magento\NegotiableQuote\Model\Quote\FinalizedStateChecker; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\Api\FilterBuilder; use Magento\Quote\Model\ResourceModel\Quote\Item\Collection; @@ -24,6 +29,7 @@ use Magento\Quote\Model\ResourceModel\Quote\Item\Collection; * Class for convert form negotiable quote to snapshot and vice versa. * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) */ class NegotiableQuoteConverter { @@ -72,6 +78,11 @@ class NegotiableQuoteConverter */ protected $filterBuilder; + /** + * @var FinalizedStateChecker + */ + private $finalizedStateChecker; + /** * @param CartInterfaceFactory $cartFactory * @param CartItemInterfaceFactory $cartItemFactory @@ -82,6 +93,8 @@ class NegotiableQuoteConverter * @param NegotiableQuoteInterfaceFactory $negotiableQuoteFactory * @param SearchCriteriaBuilder $searchCriteriaBuilder * @param FilterBuilder $filterBuilder + * @param FinalizedStateChecker|null $finalizedStateChecker + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( CartInterfaceFactory $cartFactory, @@ -92,7 +105,8 @@ class NegotiableQuoteConverter NegotiableQuoteItemInterfaceFactory $negotiableQuoteItemFactory, NegotiableQuoteInterfaceFactory $negotiableQuoteFactory, SearchCriteriaBuilder $searchCriteriaBuilder, - FilterBuilder $filterBuilder + FilterBuilder $filterBuilder, + ?FinalizedStateChecker $finalizedStateChecker = null ) { $this->cartFactory = $cartFactory; $this->productFactory = $productFactory; @@ -103,6 +117,8 @@ class NegotiableQuoteConverter $this->negotiableQuoteFactory = $negotiableQuoteFactory; $this->searchCriteriaBuilder = $searchCriteriaBuilder; $this->filterBuilder = $filterBuilder; + $this->finalizedStateChecker = $finalizedStateChecker + ?? \Magento\Framework\App\ObjectManager::getInstance()->get(FinalizedStateChecker::class); } /** @@ -205,56 +221,158 @@ class NegotiableQuoteConverter $quote->setData($serialized['quote']); - $quote->removeAllAddresses(); - if ($serialized['shipping_address']) { - $serializedShippingAddress = $quote->getShippingAddress(); - $serializedShippingAddress->setData($serialized['shipping_address']); - } - if ($serialized['billing_address']) { - $serializedBillingAddress = $quote->getBillingAddress(); - $serializedBillingAddress->setData($serialized['billing_address']); - } + $this->restoreAddresses($quote, $serialized); $quote->removeAllItems(); $itemsCollection = $quote->getItemsCollection(); $itemsCollection->removeAllItems(); - $notExistingProductIds = $this->getNotExistingProductIds($serialized['items']); - $neqProductsCount = count($notExistingProductIds); - foreach ($serialized['items'] as $key => $item) { - if (($neqProductsCount > 0) && in_array($item['product_id'], $notExistingProductIds)) { - unset($serialized['items'][$key]); + $isFinalized = $this->finalizedStateChecker->isFinalized((string) $serialized['negotiable_quote']['status']); + $notExistingProductIds = $isFinalized ? [] : $this->getNotExistingProductIds($serialized['items']); + + $this->populateItemsCollection( + $quote, + $itemsCollection, + $serialized['items'], + $notExistingProductIds, + $isFinalized + ); + $this->resolveItemRelations($itemsCollection); + if ($isFinalized) { + $this->preCacheSnapshotProductData($itemsCollection); + } + $this->attachNegotiableQuoteExtension($quote, $serialized['negotiable_quote']); + + if (count($notExistingProductIds) > 0) { + $quote->setTotalsCollectedFlag(false); + } + + return $quote; + } + + /** + * Restore shipping and billing addresses on the quote from serialized data. + * + * @param CartInterface $quote + * @param array $serialized + * @return void + */ + private function restoreAddresses(CartInterface $quote, array $serialized): void + { + $quote->removeAllAddresses(); + + if (!empty($serialized['shipping_address'])) { + $quote->getShippingAddress()->setData($serialized['shipping_address']); + } + + if (!empty($serialized['billing_address'])) { + $quote->getBillingAddress()->setData($serialized['billing_address']); + } + } + + /** + * Populate the items collection from serialized item data, skipping non-existing products. + * + * @param CartInterface $quote + * @param Collection $itemsCollection + * @param array $serializedItems + * @param array $notExistingProductIds + * @param bool $isFinalized + * @return void + */ + private function populateItemsCollection( + CartInterface $quote, + Collection $itemsCollection, + array $serializedItems, + array $notExistingProductIds, + bool $isFinalized + ): void { + foreach ($serializedItems as $item) { + $productMissing = !empty($notExistingProductIds) && in_array($item['product_id'], $notExistingProductIds); + if ($productMissing && !$isFinalized) { continue; } - $options = $item['options']; - unset($item['options']); - $negotiableQuoteItemData = $item['negotiable_quote_item']; - unset($item['negotiable_quote_item']); - - $itemObject = $this->cartItemFactory->create(); - $itemObject->setData($item); - $itemObject->setQuote($quote); + + $itemObject = $this->buildCartItem($quote, $item, $isFinalized); + $itemsCollection->addItem($itemObject); + } + } + + /** + * Build a cart item object from serialized item data. + * + * @param CartInterface $quote + * @param array $item + * @param bool $attachStub + * @return CartItemInterface + */ + private function buildCartItem(CartInterface $quote, array $item, bool $attachStub): CartItemInterface + { + $options = $item['options']; + $negotiableQuoteItemData = $item['negotiable_quote_item']; + unset($item['options'], $item['negotiable_quote_item']); + + $productSnapshotData = []; + if ($attachStub) { foreach ($options as $option) { - $productObject = $this->productFactory->create(); - $productObject->setData($option['product']); - $option['product'] = $productObject; - $itemObject->addOption(new DataObject($option)); + if (!empty($option['product']) + && ($option['product']['entity_id'] ?? null) == ($item['product_id'] ?? null) + ) { + $productSnapshotData = $option['product']; + break; + } } + } - $this->arrayToQuoteAddNegotiableItem($itemObject, $negotiableQuoteItemData); - $itemsCollection->addItem($itemObject); + /** @var QuoteItem $itemObject */ + $itemObject = $this->cartItemFactory->create(); + $itemObject->setData($item); + $itemObject->setQuote($quote); + + $this->attachItemOptions($itemObject, $options); + + if ($attachStub) { + // For finalized quotes, intentionally use snapshot data to preserve historical accuracy. + $this->attachProductStub($itemObject, $item, $productSnapshotData); } - $this->resolveItemRelations($itemsCollection); - $quoteExtensionAttributes = $this->extensionFactory->create(get_class($quote)); + + $this->arrayToQuoteAddNegotiableItem($itemObject, $negotiableQuoteItemData); + + return $itemObject; + } + + /** + * Attach deserialized options to a cart item. + * + * @param CartItemInterface $itemObject + * @param array $options + * @return void + */ + private function attachItemOptions(CartItemInterface $itemObject, array $options): void + { + foreach ($options as $option) { + $productObject = $this->productFactory->create(); + $productObject->setData($option['product']); + $option['product'] = $productObject; + $itemObject->addOption(new DataObject($option)); + } + } + + /** + * Create a NegotiableQuote instance from data and attach it to the quote's extension attributes. + * + * @param CartInterface $quote + * @param array $negotiableQuoteData + * @return void + */ + private function attachNegotiableQuoteExtension(CartInterface $quote, array $negotiableQuoteData): void + { $negotiableQuote = $this->negotiableQuoteFactory->create(); - $negotiableQuote->setData($serialized['negotiable_quote']); + $negotiableQuote->setData($negotiableQuoteData); + + $quoteExtensionAttributes = $this->extensionFactory->create(get_class($quote)); $quoteExtensionAttributes->setNegotiableQuote($negotiableQuote); $quote->setExtensionAttributes($quoteExtensionAttributes); - if ($neqProductsCount > 0) { - $quote->setTotalsCollectedFlag(false); - } - - return $quote; } /** @@ -283,6 +401,179 @@ class NegotiableQuoteConverter $itemObject->setExtensionAttributes($itemExtensionAttributes); } + /** + * Reconstruct the product object from snapshot data and attach it to a cart item. + * + * @param CartItemInterface $itemObject + * @param array $itemData + * @param array $productSnapshotData + * @return void + */ + private function attachProductStub( + CartItemInterface $itemObject, + array $itemData, + array $productSnapshotData = [] + ): void { + $productStub = $this->productFactory->create(); + + $productSnapshotData = array_replace( + [ + 'entity_id' => $itemData['product_id'] ?? null, + 'name' => $itemData['name'] ?? '', + 'sku' => $itemData['sku'] ?? '', + 'type_id' => $itemData['product_type'] ?? 'simple', + 'status' => \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED, + ], + $productSnapshotData + ); + + // Bundle: force fixed-SKU mode so getSku() returns the stored item SKU, not a DB-recomputed partial one. + if (($productSnapshotData['type_id'] ?? '') === BundleType::TYPE_CODE) { + $productSnapshotData['sku'] = $itemData['sku'] ?? ''; + $productSnapshotData['sku_type'] = 1; + } + + $productStub->setData($productSnapshotData); + $itemObject->setProduct($productStub); + } + + /** + * Pre-seed configurable attribute caches on the product stub to allow label resolution after product deletion. + * + * @param QuoteItem $itemObject + * @return void + */ + private function preCacheConfigurableAttributes(QuoteItem $itemObject): void + { + $product = $itemObject->getProduct(); + $attributesOption = $itemObject->getOptionByCode('attributes'); + + if (!$product || $product->getTypeId() !== ConfigurableType::TYPE_CODE || !$attributesOption) { + return; + } + + $data = json_decode((string) $attributesOption->getValue(), true); + if (!is_array($data)) { + return; + } + + $typeInstance = $product->getTypeInstance(); + $usedAttributes = []; + foreach (array_keys($data) as $attributeId) { + $attribute = $typeInstance->getAttributeById($attributeId, $product); + if ($attribute && $attribute->getId()) { + $usedAttributes[(int) $attributeId] = new DataObject(['product_attribute' => $attribute]); + } + } + if (!empty($usedAttributes)) { + $product->setData('_cache_instance_used_attributes', $usedAttributes); + $product->setData('_cache_instance_used_product_attributes', []); + } + } + + /** + * Dispatch type-specific snapshot pre-caching for each item in the collection. + * + * @param Collection $itemsCollection + * @return void + */ + private function preCacheSnapshotProductData(Collection $itemsCollection): void + { + // Build parent_item_id → children map directly; more robust than relying on getChildren(). + $childrenByParentId = []; + foreach ($itemsCollection->getItems() as $candidate) { + if ($candidate instanceof QuoteItem && $candidate->getParentItemId()) { + $childrenByParentId[(int) $candidate->getParentItemId()][] = $candidate; + } + } + + foreach ($itemsCollection->getItems() as $item) { + if (!($item instanceof QuoteItem)) { + continue; + } + $product = $item->getProduct(); + if (!$product) { + continue; + } + switch ($product->getTypeId()) { + case BundleType::TYPE_CODE: + $this->preCacheBundleSelections($item, $childrenByParentId[(int) $item->getItemId()] ?? []); + break; + case ConfigurableType::TYPE_CODE: + $this->preCacheConfigurableAttributes($item); + break; + } + } + } + + /** + * Pre-seed snapshot bundle option groups on the bundle parent product stub. + * + * @param QuoteItem $item + * @param QuoteItem[] $children + * @return void + */ + private function preCacheBundleSelections(QuoteItem $item, array $children): void + { + // Group children by option_id to reconstruct option label → selection list mapping. + $optionGroups = []; + foreach ($children as $childItem) { + $selection = $this->extractBundleSelectionData($item, $childItem); + if (!$selection) { + continue; + } + $optionId = $selection['option_id']; + if (!isset($optionGroups[$optionId])) { + $optionGroups[$optionId] = ['label' => $selection['label'], 'value' => []]; + } + if ($selection['qty'] > 0 && $selection['name'] !== '') { + $optionGroups[$optionId]['value'][] = $selection['qty'] . ' x ' . $selection['name']; + } + } + + $snapshotOptions = array_values( + array_filter($optionGroups, fn(array $g) => !empty($g['value'])) + ); + if (!empty($snapshotOptions)) { + $item->getProduct()->setData('_snapshot_bundle_options', $snapshotOptions); + } + } + + /** + * Extract option-group data for a single bundle child item, or null if the child should be skipped. + * + * @param QuoteItem $parentItem + * @param QuoteItem $childItem + * @return array|null + */ + private function extractBundleSelectionData(QuoteItem $parentItem, QuoteItem $childItem): ?array + { + $attrsOption = $childItem->getOptionByCode('bundle_selection_attributes'); + if (!$attrsOption) { + return null; + } + $attrs = json_decode((string) $attrsOption->getValue(), true); + if (!is_array($attrs)) { + return null; + } + $optionId = (int) ($attrs['option_id'] ?? 0); + if (!$optionId) { + return null; + } + + // Actual cart qty is stored as selection_qty_{selectionId} on the parent item. + $selectionIdOption = $childItem->getOptionByCode('selection_id'); + $selectionId = $selectionIdOption ? (int) $selectionIdOption->getValue() : 0; + $qtyOption = $parentItem->getOptionByCode('selection_qty_' . $selectionId); + + return [ + 'option_id' => $optionId, + 'label' => (string) ($attrs['option_label'] ?? ''), + 'qty' => $qtyOption ? (int) $qtyOption->getValue() : 1, + 'name' => (string) $childItem->getName(), + ]; + } + /** * Get array of not existing products ID's in quote. * @@ -298,7 +589,7 @@ class NegotiableQuoteConverter $notExistingProducts = []; $filters = []; foreach ($quoteItems as $item) { - $filters[] = $this->filterBuilder + $filters[] = $this->filterBuilder ->setField('entity_id') ->setConditionType('eq') ->setValue($item['product_id']) diff --git a/vendor/magento/module-negotiable-quote/Model/Quote/FinalizedStateChecker.php b/vendor/magento/module-negotiable-quote/Model/Quote/FinalizedStateChecker.php new file mode 100644 index 0000000000..c3439b221d --- /dev/null +++ b/vendor/magento/module-negotiable-quote/Model/Quote/FinalizedStateChecker.php @@ -0,0 +1,42 @@ +negotiableQuoteRepository = $negotiableQuoteRepository; $this->messageApplier = $messageApplier; $this->historyManagement = $historyManagement; $this->serializer = $serializer; $this->logger = $logger; + $this->finalizedStateChecker = $finalizedStateChecker + ?? \Magento\Framework\App\ObjectManager::getInstance()->get(FinalizedStateChecker::class); } /** @@ -90,7 +100,9 @@ class ItemRemove { /** @var NegotiableQuoteInterface $negotiableQuote */ $negotiableQuote = $this->negotiableQuoteRepository->getById($quoteId); - if (!$negotiableQuote->getIsRegularQuote()) { + if (!$negotiableQuote->getIsRegularQuote() || + $this->finalizedStateChecker->isFinalized($negotiableQuote->getStatus()) + ) { return $this; } diff --git a/vendor/magento/module-negotiable-quote/Model/Quote/SnapshotQuoteResolver.php b/vendor/magento/module-negotiable-quote/Model/Quote/SnapshotQuoteResolver.php new file mode 100644 index 0000000000..eacbc96902 --- /dev/null +++ b/vendor/magento/module-negotiable-quote/Model/Quote/SnapshotQuoteResolver.php @@ -0,0 +1,60 @@ +getExtensionAttributes(); + if ($extensionAttributes === null) { + return $quote; + } + $negotiableQuote = $extensionAttributes->getNegotiableQuote(); + if ($negotiableQuote === null) { + return $quote; + } + if (!$this->finalizedStateChecker->isFinalized((string) $negotiableQuote->getStatus())) { + return $quote; + } + return $this->negotiableQuoteManagement->getSnapshotQuote($quote->getId()); + } +} diff --git a/vendor/magento/module-negotiable-quote/Model/QuoteUpdatesInfoProvider/Items.php b/vendor/magento/module-negotiable-quote/Model/QuoteUpdatesInfoProvider/Items.php index aa452d5992..7e0af936d4 100644 --- a/vendor/magento/module-negotiable-quote/Model/QuoteUpdatesInfoProvider/Items.php +++ b/vendor/magento/module-negotiable-quote/Model/QuoteUpdatesInfoProvider/Items.php @@ -7,6 +7,7 @@ declare(strict_types=1); namespace Magento\NegotiableQuote\Model\QuoteUpdatesInfoProvider; +use Magento\NegotiableQuote\Model\Quote\SnapshotQuoteResolver; use Magento\Quote\Api\Data\CartInterface; use Magento\Quote\Api\Data\CartItemInterface; use Magento\Quote\Model\Quote\Item; @@ -14,12 +15,21 @@ use Magento\Quote\Model\Quote\Item; class Items implements ProviderInterface { + /** + * @var SnapshotQuoteResolver + */ + private SnapshotQuoteResolver $snapshotQuoteResolver; + /** * @param array $itemUpdatesInfoProviders + * @param SnapshotQuoteResolver|null $snapshotQuoteResolver */ public function __construct( - private readonly array $itemUpdatesInfoProviders + private readonly array $itemUpdatesInfoProviders, + ?SnapshotQuoteResolver $snapshotQuoteResolver = null ) { + $this->snapshotQuoteResolver = $snapshotQuoteResolver + ?? \Magento\Framework\App\ObjectManager::getInstance()->get(SnapshotQuoteResolver::class); } /** @@ -50,13 +60,18 @@ class Items implements ProviderInterface /** * Get visible quote items. * + * For finalized quotes (ordered/closed), items are loaded from the snapshot + * to preserve historical data regardless of current catalog product state. + * * @param CartInterface $quote * @return array */ private function getVisibleQuoteItems(CartInterface $quote): array { + $sourceQuote = $this->snapshotQuoteResolver->resolve($quote); + $items = []; - $quoteItems = $quote->getItemsCollection(); + $quoteItems = $sourceQuote->getItemsCollection(); foreach ($quoteItems as $item) { if (!$item->isDeleted() && !$item->getParentItem()) { $items[] = $item; @@ -66,6 +81,7 @@ class Items implements ProviderInterface return $items; } + /** * Get items messages. * diff --git a/vendor/magento/module-negotiable-quote/Model/QuoteUpdatesInfoProvider/Totals.php b/vendor/magento/module-negotiable-quote/Model/QuoteUpdatesInfoProvider/Totals.php index 9edaa56ae5..905c7f4b5c 100644 --- a/vendor/magento/module-negotiable-quote/Model/QuoteUpdatesInfoProvider/Totals.php +++ b/vendor/magento/module-negotiable-quote/Model/QuoteUpdatesInfoProvider/Totals.php @@ -10,6 +10,7 @@ namespace Magento\NegotiableQuote\Model\QuoteUpdatesInfoProvider; use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\NegotiableQuote\Model\NegotiableItem\GetNegotiatedPrice; use Magento\NegotiableQuote\Model\NegotiableQuoteItemManagement; +use Magento\NegotiableQuote\Model\Quote\SnapshotQuoteResolver; use Magento\NegotiableQuote\Model\Quote\Totals as NegotiableQuoteTotals; use Magento\NegotiableQuote\Model\Quote\TotalsFactory; use Magento\Quote\Api\Data\CartInterface; @@ -24,18 +25,27 @@ class Totals implements ProviderInterface */ private NegotiableQuoteTotals $totals; + /** + * @var SnapshotQuoteResolver + */ + private SnapshotQuoteResolver $snapshotQuoteResolver; + /** * @param GetNegotiatedPrice $getNegotiatedPrice * @param NegotiableQuoteItemManagement $negotiableQuoteItemManagement * @param PriceCurrencyInterface $priceCurrency * @param TotalsFactory $quoteTotalsFactory + * @param SnapshotQuoteResolver|null $snapshotQuoteResolver */ public function __construct( private readonly GetNegotiatedPrice $getNegotiatedPrice, private readonly NegotiableQuoteItemManagement $negotiableQuoteItemManagement, private readonly PriceCurrencyInterface $priceCurrency, - private readonly TotalsFactory $quoteTotalsFactory + private readonly TotalsFactory $quoteTotalsFactory, + ?SnapshotQuoteResolver $snapshotQuoteResolver = null ) { + $this->snapshotQuoteResolver = $snapshotQuoteResolver + ?? \Magento\Framework\App\ObjectManager::getInstance()->get(SnapshotQuoteResolver::class); } /** @@ -184,15 +194,16 @@ class Totals implements ProviderInterface } /** - * Get subtotal for unlocked items + * Get subtotal for locked items * * @param CartInterface $quote * @return float */ public function getSubtotalOfLockedItems(CartInterface $quote): float { + $sourceQuote = $this->snapshotQuoteResolver->resolve($quote); $lockedItemSubtotal = 0; - foreach ($quote->getAllItems() as $quoteItem) { + foreach ($sourceQuote->getAllItems() as $quoteItem) { if ($this->negotiableQuoteItemManagement->isItemLockedForDiscounting($quoteItem)) { $originalPrice = $this->negotiableQuoteItemManagement->getOriginalPriceByItem($quoteItem); $itemNegotiatedPrice = $this->getNegotiatedPrice->execute( @@ -214,8 +225,9 @@ class Totals implements ProviderInterface */ public function getSubtotalOfUnlockedItems(CartInterface $quote): float { + $sourceQuote = $this->snapshotQuoteResolver->resolve($quote); $unLockedItemSubtotal = 0; - foreach ($quote->getAllItems() as $quoteItem) { + foreach ($sourceQuote->getAllItems() as $quoteItem) { if (!$this->negotiableQuoteItemManagement->isItemLockedForDiscounting($quoteItem)) { $originalPrice = $this->negotiableQuoteItemManagement->getOriginalPriceByItem($quoteItem); $itemNegotiatedPrice = $this->getNegotiatedPrice->execute( diff --git a/vendor/magento/module-negotiable-quote/Plugin/Bundle/Catalog/Product/ConfigurationPlugin.php b/vendor/magento/module-negotiable-quote/Plugin/Bundle/Catalog/Product/ConfigurationPlugin.php new file mode 100644 index 0000000000..c0c6d81399 --- /dev/null +++ b/vendor/magento/module-negotiable-quote/Plugin/Bundle/Catalog/Product/ConfigurationPlugin.php @@ -0,0 +1,60 @@ +getProduct(); + if ($product) { + $snapshotOptions = $product->getData('_snapshot_bundle_options'); + if (is_array($snapshotOptions) && !empty($snapshotOptions)) { + return $snapshotOptions; + } + } + + return $result; + } +} diff --git a/vendor/magento/module-negotiable-quote/Plugin/Quote/Model/QuotePlugin.php b/vendor/magento/module-negotiable-quote/Plugin/Quote/Model/QuotePlugin.php index 0049dee20a..8cf4c36e26 100644 --- a/vendor/magento/module-negotiable-quote/Plugin/Quote/Model/QuotePlugin.php +++ b/vendor/magento/module-negotiable-quote/Plugin/Quote/Model/QuotePlugin.php @@ -6,8 +6,9 @@ namespace Magento\NegotiableQuote\Plugin\Quote\Model; -use Magento\Quote\Model\Quote; +use Magento\NegotiableQuote\Model\Quote\FinalizedStateChecker; use Magento\NegotiableQuote\Model\Quote\ItemRemove; +use Magento\Quote\Model\Quote; /** * Class QuotePlugin @@ -29,10 +30,12 @@ class QuotePlugin * * @param ItemRemove $itemRemove * @param \Magento\Framework\Serialize\SerializerInterface $serializer + * @param FinalizedStateChecker $finalizedStateChecker */ public function __construct( ItemRemove $itemRemove, - \Magento\Framework\Serialize\SerializerInterface $serializer + \Magento\Framework\Serialize\SerializerInterface $serializer, + private FinalizedStateChecker $finalizedStateChecker ) { $this->itemRemove = $itemRemove; $this->serializer = $serializer; @@ -98,7 +101,7 @@ class QuotePlugin */ public function aroundCollectTotals(Quote $subject, \Closure $proceed) { - if (!$subject->getData('trigger_recollect')) { + if (!$subject->getData('trigger_recollect') || $this->isQuoteFinalized($subject)) { $result = $proceed(); } else { $itemsArray = $this->collectItemsData($subject); @@ -117,6 +120,25 @@ class QuotePlugin return $result; } + /** + * Check if the quote is in a finalized negotiable quote state. + * + * @param Quote $quote + * @return bool + */ + private function isQuoteFinalized(Quote $quote): bool + { + $extensionAttributes = $quote->getExtensionAttributes(); + if ($extensionAttributes === null) { + return false; + } + $negotiableQuote = $extensionAttributes->getNegotiableQuote(); + if ($negotiableQuote === null) { + return false; + } + return $this->finalizedStateChecker->isFinalized($negotiableQuote->getStatus()); + } + /** * Collect items data * diff --git a/vendor/magento/module-negotiable-quote/etc/di.xml b/vendor/magento/module-negotiable-quote/etc/di.xml index 51c0f122ab..154038c07c 100644 --- a/vendor/magento/module-negotiable-quote/etc/di.xml +++ b/vendor/magento/module-negotiable-quote/etc/di.xml @@ -518,4 +518,8 @@ + + +