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 @@
+
+
+