From e312f0f6d5bc6727c65fceef74928bb62994bf02 Mon Sep 17 00:00:00 2001 From: jm-extend Date: Fri, 14 Aug 2026 14:46:24 -0500 Subject: [PATCH 1/2] accounting for custom group prices and special cart rules 1. ViewModel/Warranty.php : new getOfferPrice() (line 688) resolves the price through the price info registry, so special price, catalog price rules and customer-group/tier price are all applied; getProductInfo() now uses it (line 654). Fallback is getFinalPrice(1) , with the qty argument, which is what makes _applyTierPrice run at all. Verified against core while writing it: BasePrice::getValue() takes the min over every price implementing BasePriceProviderInterface, and TierPrice is one of them (module-catalog/Pricing/Price/TierPrice.php). canApplyTierPrice() filters rows to the session customer group and to price_qty <= 1, so a group price at qty 1 is picked up while qty-10 volume tiers are correctly ignored. 2. ViewModel/Warranty.php: new getVariationPricesJson() (line 717) emits a sku => price map for configurable children using getSalableUsedProducts() + getOfferPrice(). That's the same pattern core's getOptionPrices() uses for jsonConfig, and the type instance caches the child collection per store+customer group, so it's not an extra load on the PDP. 3. configurable-warranty.js / warranty-offers-base.js: _onOptionsChanged now looks the variant price up in that map and passes it to updateActiveProduct(sku, price), which no longer re-sends the stale parent price. It also updates options.productInfo.price so the interstitial modal prices the selected variant, and the guard is now sku !== ... || price !== ... so a price-only change still propagates. 4. configurable-warranty.phtml / list/warranty-offers.phtml : pass variationPrices (PLP only for configurables, where swatch selection can change the modal price). --- ViewModel/Warranty.php | 66 ++++++++++++++++++- .../product/list/warranty-offers.phtml | 3 + .../product/view/configurable-warranty.phtml | 1 + .../web/js/product/configurable-warranty.js | 31 ++++++++- view/frontend/web/js/warranty-offers-base.js | 10 ++- 5 files changed, 105 insertions(+), 6 deletions(-) diff --git a/ViewModel/Warranty.php b/ViewModel/Warranty.php index 82059c33..0593012c 100644 --- a/ViewModel/Warranty.php +++ b/ViewModel/Warranty.php @@ -21,7 +21,9 @@ use Extend\Warranty\Model\Config\Source\ProductPagePlacement; use Magento\Backend\Model\Auth\Session as AdminSession; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product; use Magento\Catalog\Model\ResourceModel\Category\Collection; +use Magento\Catalog\Pricing\Price\FinalPrice; use Magento\Checkout\Model\Session as CheckoutSession; use Magento\ConfigurableProduct\Api\LinkManagementInterface; use Magento\ConfigurableProduct\Model\Product\Type\Configurable; @@ -649,7 +651,7 @@ public function showLeadOffer($orderItem) public function getProductInfo($product) { - $price = $product->getFinalPrice(); + $price = $this->getOfferPrice($product); /** @var Collection $categoryCollection */ $categoryCollection = $product->getCategoryCollection(); @@ -666,6 +668,68 @@ public function getProductInfo($product) ]; } + /** + * Retrieve the price the storefront renders for the product + * + * The price info registry resolves special prices, catalog price rules and + * customer group / tier prices for the current session, which is what the PDP + * price block displays. Product::getFinalPrice() without a qty argument skips + * tier prices entirely - and customer group prices are stored as tier price + * rows - so it reports the base or special price to Extend instead. + * + * getValue() is used rather than getAmount()->getValue() to keep the price + * excluding tax, consistent with the rest of the offer payload. + * + * @param ProductInterface $product + * @return float + */ + public function getOfferPrice($product): float + { + try { + if (method_exists($product, 'getPriceInfo')) { + $finalPrice = $product->getPriceInfo()->getPrice(FinalPrice::PRICE_CODE); + if ($finalPrice) { + return (float)$finalPrice->getValue(); + } + } + } catch (Exception $e) { + // price info is unavailable for this product, fall back to the price model + } + + // the qty argument is required, otherwise tier and group prices are ignored + return (float)$product->getFinalPrice(1); + } + + /** + * Offer prices of each variation of a configurable product, keyed by child SKU + * + * Passed to the PDP widget so the offer can be re-priced when the shopper + * switches variation - children carry their own special and customer group + * prices, and the parent price is only the lowest of them. + * + * + * @param ProductInterface $product + * @return string + */ + public function getVariationPricesJson($product): string + { + $prices = []; + + if ($product instanceof Product && $product->getTypeId() === Configurable::TYPE_CODE) { + $typeInstance = $product->getTypeInstance(); + + if ($typeInstance instanceof Configurable) { + foreach ($typeInstance->getSalableUsedProducts($product) as $childProduct) { + $prices[$childProduct->getSku()] = $this->helper->formatPrice( + $this->getOfferPrice($childProduct) + ); + } + } + } + + return $this->jsonSerializer->serialize($prices); + } + public function getOrderIncrementId(string|int|null $orderId = null){ if ($orderId){ $order = $this->orderRepository->get($orderId); diff --git a/view/frontend/templates/product/list/warranty-offers.phtml b/view/frontend/templates/product/list/warranty-offers.phtml index 8ced8a8b..c3489735 100644 --- a/view/frontend/templates/product/list/warranty-offers.phtml +++ b/view/frontend/templates/product/list/warranty-offers.phtml @@ -33,6 +33,9 @@ $isConfigurable = $productType === \Magento\ConfigurableProduct\Model\Product\Ty "productSku": "escapeJs($_product->getData('sku')) : '' ?>", "buttonEnabled": false, "productInfo": getProductInfo($_product)); ?>, + + "variationPrices": getVariationPricesJson($_product); ?>, + "modalEnabled": true, "insertionPoint": "div.product-item-actions", "insertionLogic": "append", diff --git a/view/frontend/templates/product/view/configurable-warranty.phtml b/view/frontend/templates/product/view/configurable-warranty.phtml index d0902062..3147c433 100644 --- a/view/frontend/templates/product/view/configurable-warranty.phtml +++ b/view/frontend/templates/product/view/configurable-warranty.phtml @@ -28,6 +28,7 @@ $product = $block->getProduct(); "productId": "escapeJs($product->getId()) ?>", "productSku": "warranties_are_only_for_simples", "productInfo": getProductInfo($product)); ?>, + "variationPrices": getVariationPricesJson($product); ?>, "buttonEnabled": escapeJs($viewModel->isProductDetailPageOffersEnabled() ? 1 : 0) ?>, "modalEnabled": escapeJs($viewModel->isInterstitialCartOffersEnabled() ? 1 : 0) ?>, "insertionPoint": "escapeJs($placement['insertionPoint']) ?>", diff --git a/view/frontend/web/js/product/configurable-warranty.js b/view/frontend/web/js/product/configurable-warranty.js index eba91812..3685f997 100644 --- a/view/frontend/web/js/product/configurable-warranty.js +++ b/view/frontend/web/js/product/configurable-warranty.js @@ -22,6 +22,7 @@ define([ productSku: null, buttonEnabled: true, modalEnabled: false, + variationPrices: {}, blockClass: 'product-warranty-offers', insertionPoint: 'div.actions', insertionLogic: 'before', @@ -52,11 +53,37 @@ define([ * @param {Event} event - The event arguments */ _onOptionsChanged: function (event) { - if (!this.options.buttonEnabled) + if (!this.options.buttonEnabled && !this.options.modalEnabled) return; var productSku = this._getWarrantyProductSku(); - this.warrantyBlock.extendWarrantyOffers('updateActiveProduct', productSku); + var price = this._getVariationPrice(productSku); + + // keeps the interstitial modal on the price of the selected variation + if (price !== null) { + this.options.productInfo.price = price; + } + + if (this.options.buttonEnabled) { + this.warrantyBlock.extendWarrantyOffers('updateActiveProduct', productSku, price); + } + }, + + /** + * Returns the offer price of the given variation, `null` when it is unknown + * + * Variation prices are rendered server side, so special, catalog rule and + * customer group prices of the child product are all taken into account. + * + * + * @protected + * @param {String} productSku + * @return {Number|null} + */ + _getVariationPrice: function (productSku) { + var prices = this.options.variationPrices || {}; + + return productSku && prices.hasOwnProperty(productSku) ? prices[productSku] : null; }, /** diff --git a/view/frontend/web/js/warranty-offers-base.js b/view/frontend/web/js/warranty-offers-base.js index 1e0aa601..6949d8b4 100644 --- a/view/frontend/web/js/warranty-offers-base.js +++ b/view/frontend/web/js/warranty-offers-base.js @@ -98,17 +98,21 @@ define([ * Updates warranty offers product * * @param {String} productSku - new product SKU + * @param {Number|null} price - price of the new product, the price of the + * currently active product is kept when omitted */ - updateActiveProduct: function (productSku) { + updateActiveProduct: function (productSku, price) { var component = this.getButtonInstance(); if (!component) return; var product = component.getActiveProduct() || { id: '' }; - if (product.id !== productSku) { + var newPrice = (price === undefined || price === null) ? product.price : price; + + if (product.id !== productSku || product.price !== newPrice) { let activeProduct = { referenceId:productSku, - price: product.price, + price: newPrice, category: this.sanitizeValue(product.category) }; component.setActiveProduct(activeProduct); From 6b20f1829e1102fb36edbb57ef619579667aab17 Mon Sep 17 00:00:00 2001 From: jm-extend Date: Fri, 14 Aug 2026 15:05:39 -0500 Subject: [PATCH 2/2] Update configurable-warranty.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit based on Code rabbit feedback 1. _onOptionsChanged() (lines 59–67) — after _getWarrantyProductSku() and before _getVariationPrice(), an empty SKU on a PLP (isInProductView === false) now falls back to resolving the selected configurable option. Downstream price handling and the updateActiveProduct call are untouched, and they now receive the resolved variation SKU instead of "". 2. New _getSelectedConfigurableSku() (line 135+) — extracted from the existing product-page branch: reads input[name=selected_configurable_option] inside mainWrap and maps the ID through the mageConfigurable widget's spConfig.skus. 3. _getWarrantyProductSku() (line 119–121) — the PDP branch now delegates to that helper; behavior is unchanged (both old and new code return a falsy value when nothing resolves, which falls through to this.options.productSku). --- .../web/js/product/configurable-warranty.js | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/view/frontend/web/js/product/configurable-warranty.js b/view/frontend/web/js/product/configurable-warranty.js index 3685f997..2f018b67 100644 --- a/view/frontend/web/js/product/configurable-warranty.js +++ b/view/frontend/web/js/product/configurable-warranty.js @@ -57,6 +57,13 @@ define([ return; var productSku = this._getWarrantyProductSku(); + + // the parent SKU is not rendered for configurable products on the PLP, so a + // non-swatch selection leaves us without a SKU to price the offer with + if (!productSku && !this.options.isInProductView) { + productSku = this._getSelectedConfigurableSku(); + } + var price = this._getVariationPrice(productSku); // keeps the interstitial modal on the price of the selected variation @@ -109,14 +116,30 @@ define([ } } } else if (this.options.isInProductView) { - var selectedId = $('input[name=selected_configurable_option]', this.mainWrap).val(); - if (selectedId && selectedId !== '') { - var spConfig = this.addToCartForm.data('mageConfigurable').options.spConfig; - selectedSku = spConfig && spConfig.skus ? spConfig.skus[selectedId] : null; - } + selectedSku = this._getSelectedConfigurableSku(); } return selectedSku ? selectedSku : this.options.productSku; + }, + + /** + * Returns the SKU of the currently selected configurable option, `null` when + * nothing is selected or the configurable widget is unavailable + * + * @protected + * @return {String|null} + */ + _getSelectedConfigurableSku: function () { + var selectedId = $('input[name=selected_configurable_option]', this.mainWrap).val(); + + if (!selectedId || selectedId === '') { + return null; + } + + var configurable = this.addToCartForm.data('mageConfigurable'); + var spConfig = configurable ? configurable.options.spConfig : null; + + return spConfig && spConfig.skus && spConfig.skus[selectedId] ? spConfig.skus[selectedId] : null; } });