diff --git a/vendor/magento/module-negotiable-quote/Controller/Adminhtml/Quote/Update.php b/vendor/magento/module-negotiable-quote/Controller/Adminhtml/Quote/Update.php index dd31d2ef1a8b..612e213a7eb3 100644 --- a/vendor/magento/module-negotiable-quote/Controller/Adminhtml/Quote/Update.php +++ b/vendor/magento/module-negotiable-quote/Controller/Adminhtml/Quote/Update.php @@ -90,6 +90,7 @@ public function execute() $discountErrorMessage = ''; try { $this->quoteCurrency->updateQuoteCurrency($quoteId); + $this->quoteData['avoid_addresses_cache'] = true; $this->quoteUpdater->updateQuote($quoteId, $this->quoteData, false); } catch (ItemDiscountLockingException $e) { $hasItemLockingDiscountException = true; diff --git a/vendor/magento/module-negotiable-quote/Helper/Quote.php b/vendor/magento/module-negotiable-quote/Helper/Quote.php index 6346f748b8bc..1f34193e33f6 100644 --- a/vendor/magento/module-negotiable-quote/Helper/Quote.php +++ b/vendor/magento/module-negotiable-quote/Helper/Quote.php @@ -26,6 +26,20 @@ */ class Quote extends \Magento\Framework\App\Helper\AbstractHelper { + public const AVAILABLE_OPTIONS_NAMES = [ + 'super_attribute', + 'options', + 'bundle_option', + 'bundle_option_qty', + 'custom_giftcard_amount', + 'giftcard_amount', + 'giftcard_message', + 'giftcard_recipient_email', + 'giftcard_recipient_name', + 'giftcard_sender_email', + 'giftcard_sender_name' + ]; + /** * @var CartInterface */ @@ -268,20 +282,8 @@ public function getFormattedCartPrice(CartItemInterface $item, $quoteCurrency = public function retrieveCustomOptions(CartItemInterface $item, $isString = true) { $options = []; - $optionsNames = [ - 'super_attribute', - 'options', - 'bundle_option', - 'custom_giftcard_amount', - 'giftcard_amount', - 'giftcard_message', - 'giftcard_recipient_email', - 'giftcard_recipient_name', - 'giftcard_sender_email', - 'giftcard_sender_name' - ]; $request = $item->getBuyRequest(); - foreach ($optionsNames as $option) { + foreach (self::AVAILABLE_OPTIONS_NAMES as $option) { if ($request->hasData($option) && $request->getData($option)) { $options[$option] = $request->getData($option); } diff --git a/vendor/magento/module-negotiable-quote/Model/NegotiableItem/GetNegotiatedPrice.php b/vendor/magento/module-negotiable-quote/Model/NegotiableItem/GetNegotiatedPrice.php index f7930f3d3b6c..3035d278125f 100644 --- a/vendor/magento/module-negotiable-quote/Model/NegotiableItem/GetNegotiatedPrice.php +++ b/vendor/magento/module-negotiable-quote/Model/NegotiableItem/GetNegotiatedPrice.php @@ -66,7 +66,7 @@ private function getNegotiableItem(CartItemInterface $quoteItem): NegotiableQuot { $negotiableItem = $quoteItem->getExtensionAttributes()->getNegotiableQuoteItem(); - if ($quoteItem->getParentItemId() && + if ($quoteItem->getParentItem() && $quoteItem->getParentItem()->getProductType() === BundledProductType::TYPE_CODE ) { $negotiableItem = $quoteItem diff --git a/vendor/magento/module-negotiable-quote/Model/NegotiableItem/NegotiatedPriceApplier.php b/vendor/magento/module-negotiable-quote/Model/NegotiableItem/NegotiatedPriceApplier.php new file mode 100644 index 000000000000..f20f14dd45f7 --- /dev/null +++ b/vendor/magento/module-negotiable-quote/Model/NegotiableItem/NegotiatedPriceApplier.php @@ -0,0 +1,145 @@ +appendReconfiguredItemData( + $quote, + $itemsData, + $deferredNegotiableData, + $remainingItems + ); + } + $this->addNegotiatedPriceInfo->execute($quote, $itemsData); + } + + /** + * Persist negotiated price extension attribute data to the negotiable_quote_item table. + * + * Required when addConfiguredItems internally saves the quote, clearing the CartRepository + * identity map. Without this, subsequent reloads in updateQuoteItemsCustomPrices would + * lose the in-memory discount data set by AddNegotiatedPriceInfo. + * + * @param CartInterface $quote + */ + public function persist(CartInterface $quote): void + { + $itemsToSave = []; + foreach ($quote->getAllItems() as $quoteItem) { + $negotiableItem = $quoteItem->getExtensionAttributes()?->getNegotiableQuoteItem(); + if (!$negotiableItem || !$negotiableItem->getItemId()) { + continue; + } + $ext = $negotiableItem->getExtensionAttributes(); + if (!$ext) { + continue; + } + $negotiableItem->setData( + NegotiableQuoteItemInterface::NEGOTIATED_PRICE_TYPE, + $ext->getNegotiatedPriceType() + ); + $negotiableItem->setData( + NegotiableQuoteItemInterface::NEGOTIATED_PRICE_VALUE, + $ext->getNegotiatedPriceValue() + ); + $negotiableItem->setData( + NegotiableQuoteItemInterface::IS_DISCOUNTING_LOCKED, + (int)$ext->getIsDiscountingLocked() + ); + $itemsToSave[] = $negotiableItem; + } + if ($itemsToSave) { + $this->negotiableQuoteItemResource->saveList($itemsToSave); + } + } + + /** + * Append hash-matched negotiable price entries for items that were reconfigured. + * + * @param CartInterface $quote + * @param array $itemsData + * @param array $deferredNegotiableData keyed by productSku + * @param array $remainingItems keyed by item_id + * @return array + */ + private function appendReconfiguredItemData( + CartInterface $quote, + array $itemsData, + array $deferredNegotiableData, + array $remainingItems + ): array { + foreach ($quote->getAllVisibleItems() as $quoteItem) { + if (array_key_exists((int)$quoteItem->getItemId(), $remainingItems)) { + continue; + } + $productSku = $quoteItem->getProduct()->getSku(); + if (!isset($deferredNegotiableData[$productSku])) { + continue; + } + $entry = $deferredNegotiableData[$productSku]; + $entry['item_hash'] = $this->quoteItemHashHandler->getItemHashToIdItem($quoteItem); + $itemsData[] = $entry; + unset($deferredNegotiableData[$productSku]); + } + return $itemsData; + } +} diff --git a/vendor/magento/module-negotiable-quote/Model/NegotiableQuoteItemManagement.php b/vendor/magento/module-negotiable-quote/Model/NegotiableQuoteItemManagement.php index 541b9532d03b..9b83326a6bb9 100644 --- a/vendor/magento/module-negotiable-quote/Model/NegotiableQuoteItemManagement.php +++ b/vendor/magento/module-negotiable-quote/Model/NegotiableQuoteItemManagement.php @@ -355,7 +355,7 @@ public function isItemLockedForDiscounting(CartItemInterface $quoteItem): bool $this->setNegotiableQuoteItem($quoteItem); $negotiableItem = $quoteItem->getExtensionAttributes()->getNegotiableQuoteItem(); - if ($quoteItem->getParentItemId() && + if ($quoteItem->getParentItem() && $quoteItem->getParentItem()->getProductType() === BundledProductType::TYPE_CODE ) { $negotiableItem = $quoteItem @@ -434,7 +434,7 @@ private function validateAllItemDiscountLocking( private function getNegotiatedPriceData(CartItemInterface $quoteItem): array { $this->setNegotiableQuoteItem($quoteItem); - $quoteItem = ($quoteItem->getParentItemId() && + $quoteItem = ($quoteItem->getParentItem() && $quoteItem->getParentItem()->getProductType() === BundledProductType::TYPE_CODE) ? $quoteItem->getParentItem() : $quoteItem; diff --git a/vendor/magento/module-negotiable-quote/Model/QuoteItemsUpdater.php b/vendor/magento/module-negotiable-quote/Model/QuoteItemsUpdater.php index 8a9941c05459..07291c25985d 100644 --- a/vendor/magento/module-negotiable-quote/Model/QuoteItemsUpdater.php +++ b/vendor/magento/module-negotiable-quote/Model/QuoteItemsUpdater.php @@ -20,16 +20,19 @@ use Magento\Checkout\Model\CartFactory; use Magento\Framework\Filter\LocalizedToNormalized; use Magento\Framework\Locale\ResolverInterface; +use Magento\NegotiableQuote\Api\Data\NegotiableQuoteItemInterface; use Magento\NegotiableQuote\Helper\Quote; use Magento\Quote\Api\Data\CartInterface; use Magento\Quote\Api\Data\CartItemInterface; use Magento\NegotiableQuote\Model\Quote\TotalsFactory; -use Magento\NegotiableQuote\Model\NegotiableItem\AddNegotiatedPriceInfo; +use Magento\NegotiableQuote\Model\NegotiableItem\NegotiatedPriceApplier; use Magento\NegotiableQuote\Model\NegotiableItem\AddNegotiatedItemNote; use Magento\Framework\Serialize\Serializer\Json; /** * Class for updating quote items attributes and options for complex products. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class QuoteItemsUpdater { @@ -81,9 +84,9 @@ class QuoteItemsUpdater * @param ResolverInterface $resolver * @param CartFactory $cartFactory * @param TotalsFactory $quoteTotalsFactory - * @param AddNegotiatedPriceInfo $addNegotiatedPriceInfo * @param AddNegotiatedItemNote $addNegotiatedItemNote * @param Json $serializer + * @param NegotiatedPriceApplier $negotiatedPriceApplier */ public function __construct( \Magento\NegotiableQuote\Helper\Quote $negotiableQuoteHelper, @@ -91,9 +94,9 @@ public function __construct( \Magento\Framework\Locale\ResolverInterface $resolver, \Magento\Checkout\Model\CartFactory $cartFactory, TotalsFactory $quoteTotalsFactory, - private readonly AddNegotiatedPriceInfo $addNegotiatedPriceInfo, private readonly AddNegotiatedItemNote $addNegotiatedItemNote, - private readonly Json $serializer + private readonly Json $serializer, + private readonly NegotiatedPriceApplier $negotiatedPriceApplier ) { $this->negotiableQuoteHelper = $negotiableQuoteHelper; $this->cart = $cart; @@ -116,6 +119,7 @@ public function updateItemsForQuote(\Magento\Quote\Api\Data\CartInterface $quote $itemsForAdd = $this->getDataFromArray($itemsData, 'addItems'); $itemsForUpdate = $this->getDataFromArray($itemsData, 'items'); $configuredItemsAdd = $this->getDataFromArray($itemsData, 'configuredItems'); + $deferredNegotiableData = []; $this->quote->setIsSuperMode(true); foreach ($itemsForUpdate as $data) { @@ -134,6 +138,14 @@ public function updateItemsForQuote(\Magento\Quote\Api\Data\CartInterface $quote parse_str(urldecode($data['config']), $config); $item['config'] = $config; $configuredItemsAdd[] = $item; + + if (isset($data['negotiated_price_type'])) { + $deferredNegotiableData[$data['productSku']] = [ + NegotiableQuoteItemInterface::NEGOTIATED_PRICE_TYPE => $data['negotiated_price_type'], + NegotiableQuoteItemInterface::NEGOTIATED_PRICE_VALUE => $data['negotiated_price_value'], + NegotiableQuoteItemInterface::IS_DISCOUNTING_LOCKED => $data['is_discounting_locked'], + ]; + } } else { $itemsForAdd[] = $item; } @@ -141,12 +153,23 @@ public function updateItemsForQuote(\Magento\Quote\Api\Data\CartInterface $quote } $this->removeQuoteItemsNotInArray($items); + $hasConfiguredItems = !empty($configuredItemsAdd); $this->addConfiguredItems($configuredItemsAdd); $this->addItems($itemsForAdd); $this->quote->setIsSuperMode(false); $this->addRemovedSkus($quote, $itemsForUpdate); $this->addNegotiatedItemNote->execute($this->quote, $this->getDataFromArray($itemsData, 'items')); - $this->addNegotiatedPriceInfo->execute($this->quote, $this->getDataFromArray($itemsData, 'items')); + $this->negotiatedPriceApplier->apply( + $this->quote, + $this->getDataFromArray($itemsData, 'items'), + $deferredNegotiableData, + $items + ); + + if ($hasConfiguredItems) { + $this->negotiatedPriceApplier->persist($this->quote); + } + return $this->hasChanges; } @@ -221,10 +244,32 @@ private function isNeedReconfigurationItem(CartItemInterface $item, array $data) $newConfig = []; // phpcs:ignore Magento2.Functions.DiscouragedFunction parse_str(urldecode($data['config']), $newConfig); + $newConfig = $this->sanitizeConfig($newConfig); return is_array($oldConfig) && is_array($newConfig) && $oldConfig != $newConfig; } + /** + * Filter config + * + * @param array $config + * @return array + */ + private function sanitizeConfig(array $config) : array + { + $configKeys = array_keys($config); + $result = $config; + if (in_array('bundle_option', $configKeys)) { + $result = []; + foreach ($config as $key => $value) { + if (in_array($key, $this->negotiableQuoteHelper::AVAILABLE_OPTIONS_NAMES)) { + $result[$key] = $value; + } + } + } + return $result; + } + /** * Remove quote items from quote. * diff --git a/vendor/magento/module-negotiable-quote/Model/QuoteUpdater.php b/vendor/magento/module-negotiable-quote/Model/QuoteUpdater.php index a303c41d73fa..d8de45b1e97d 100644 --- a/vendor/magento/module-negotiable-quote/Model/QuoteUpdater.php +++ b/vendor/magento/module-negotiable-quote/Model/QuoteUpdater.php @@ -131,8 +131,12 @@ public function __construct( * @return bool * @throws \Magento\Framework\Exception\NoSuchEntityException */ - public function updateQuote($quoteId, array $data, $needLogChanges = true, $needSave = false) - { + public function updateQuote( + $quoteId, + array $data, + $needLogChanges = true, + $needSave = false + ) { $this->quote = $this->getQuote($quoteId); $this->hasChanges = false; @@ -156,10 +160,14 @@ public function updateQuote($quoteId, array $data, $needLogChanges = true, $need } if ($this->hasChanges || $this->needRecalculate) { + if (!empty($data['avoid_addresses_cache'])) { + $this->quote->setData('avoid_addresses_cache', true); + } $this->updatePriceQuote($needSave); $this->quote->setTotalsCollectedFlag(false); $this->quote->collectTotals(); $this->ruleChecker->checkIsDiscountRemoved($this->quote, $oldRuleIds, $needLogChanges); + $this->quote->setData('avoid_addresses_cache', false); $priceChange = $this->priceChecker ->setIsProductPriceChanged($this->quote, $oldPriceData, $needLogChanges); $this->priceChecker->setIsCartPriceChanged($this->quote, $oldCartPriceData, $needLogChanges); diff --git a/vendor/magento/module-negotiable-quote/Plugin/Quote/Model/QuoteAddressesCollectionPlugin.php b/vendor/magento/module-negotiable-quote/Plugin/Quote/Model/QuoteAddressesCollectionPlugin.php new file mode 100644 index 000000000000..3355e092bc80 --- /dev/null +++ b/vendor/magento/module-negotiable-quote/Plugin/Quote/Model/QuoteAddressesCollectionPlugin.php @@ -0,0 +1,61 @@ +getExtensionAttributes() && $subject->getExtensionAttributes()->getNegotiableQuote() + && $subject->getData('avoid_addresses_cache') === true) { + $id = $subject->getId(); + if ((int)$id !== 0 && !empty($result)) { + $result = $this->addressFactory->create()->getCollection()->setQuoteFilter($id); + } + } + foreach ($result as $address) { + $address->setQuote($subject); + } + return $result; + } +} diff --git a/vendor/magento/module-negotiable-quote/etc/adminhtml/di.xml b/vendor/magento/module-negotiable-quote/etc/adminhtml/di.xml index 938fd16dee51..263f5e8ee024 100644 --- a/vendor/magento/module-negotiable-quote/etc/adminhtml/di.xml +++ b/vendor/magento/module-negotiable-quote/etc/adminhtml/di.xml @@ -20,6 +20,7 @@ +