Skip to content

Commit 1cac3f7

Browse files
committed
AC-13171: Fixed Product Tax (FPT) is not displaying separately with configurable products
Add FPT display for configurable products - Add plugin to inject FPT data into configurable product jsonConfig - Add price-box mixin to render FPT breakdown dynamically - Support product detail pages, category listings, and search results - Match simple product FPT display format (base price + FPT + final price)
1 parent ba9e7d2 commit 1cac3f7

File tree

4 files changed

+364
-0
lines changed

4 files changed

+364
-0
lines changed
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Weee\Plugin\ConfigurableProduct\Block\Product\View\Type;
9+
10+
use Magento\ConfigurableProduct\Block\Product\View\Type\Configurable as ConfigurableBlock;
11+
use Magento\Framework\Json\DecoderInterface;
12+
use Magento\Framework\Json\EncoderInterface;
13+
use Magento\Weee\Helper\Data as WeeeHelper;
14+
15+
/**
16+
* Plugin to add FPT data to configurable product JSON config
17+
*/
18+
class Configurable
19+
{
20+
/**
21+
* @param WeeeHelper $weeeHelper
22+
* @param EncoderInterface $jsonEncoder
23+
* @param DecoderInterface $jsonDecoder
24+
*/
25+
public function __construct(
26+
private readonly WeeeHelper $weeeHelper,
27+
private readonly EncoderInterface $jsonEncoder,
28+
private readonly DecoderInterface $jsonDecoder
29+
) {
30+
}
31+
32+
/**
33+
* Format price using the store's price format
34+
*
35+
* @param float $amount
36+
* @param array $priceFormat
37+
* @return string
38+
*/
39+
private function formatPrice(float $amount, array $priceFormat): string
40+
{
41+
$pattern = $priceFormat['pattern'] ?? '%s';
42+
$precision = $priceFormat['precision'] ?? 2;
43+
$decimalSymbol = $priceFormat['decimalSymbol'] ?? '.';
44+
$groupSymbol = $priceFormat['groupSymbol'] ?? ',';
45+
$groupLength = $priceFormat['groupLength'] ?? 3;
46+
47+
$formatted = number_format($amount, $precision, $decimalSymbol, $groupSymbol);
48+
return str_replace('%s', $formatted, $pattern);
49+
}
50+
51+
/**
52+
* Add FPT data to option prices
53+
*
54+
* @param ConfigurableBlock $subject
55+
* @param string $result
56+
* @return string
57+
*/
58+
public function afterGetJsonConfig(
59+
ConfigurableBlock $subject,
60+
string $result
61+
): string {
62+
$config = $this->jsonDecoder->decode($result);
63+
64+
if (!$config || !isset($config['optionPrices'])) {
65+
return $result;
66+
}
67+
68+
if (!$this->weeeHelper->isEnabled()) {
69+
return $result;
70+
}
71+
72+
foreach ($subject->getAllowProducts() as $product) {
73+
$productId = (string)$product->getId();
74+
75+
if (!isset($config['optionPrices'][$productId])) {
76+
continue;
77+
}
78+
79+
// Get FPT attributes for this product
80+
$weeeAttributes = $this->weeeHelper->getProductWeeeAttributesForDisplay($product);
81+
82+
// Add FPT data to the option price
83+
$config['optionPrices'][$productId]['weeeAttributes'] = [];
84+
85+
if (!empty($weeeAttributes)) {
86+
$weeeTotal = 0;
87+
foreach ($weeeAttributes as $attribute) {
88+
// Use getData('name') which contains the frontend label (label_value or frontend_label)
89+
// This is set by \Magento\Weee\Model\Tax::getProductWeeeAttributes() at line 374-376
90+
$name = $attribute->getData('name');
91+
92+
// Cast to string to handle Magento\Framework\Phrase objects
93+
$name = $name ? (string)$name : 'FPT';
94+
95+
$config['optionPrices'][$productId]['weeeAttributes'][] = [
96+
'name' => $name,
97+
'amount' => (float)$attribute->getAmount(),
98+
'amount_excl_tax' => (float)$attribute->getAmountExclTax(),
99+
'tax_amount' => (float)$attribute->getTaxAmount(),
100+
];
101+
102+
$weeeTotal += (float)$attribute->getAmount();
103+
}
104+
105+
// Calculate base price without WEEE
106+
$basePriceAmount = $config['optionPrices'][$productId]['finalPrice']['amount'] - $weeeTotal;
107+
108+
// Format WEEE amounts for display
109+
$formattedWeeeAttributes = [];
110+
foreach ($config['optionPrices'][$productId]['weeeAttributes'] as $weeeAttr) {
111+
$formattedWeeeAttributes[] = [
112+
'name' => $weeeAttr['name'],
113+
'amount' => $weeeAttr['amount'],
114+
'formatted' => $this->formatPrice($weeeAttr['amount'], $config['priceFormat'])
115+
];
116+
}
117+
118+
// Store WEEE data in finalPrice object (will be used by price-box reloadPrice)
119+
$config['optionPrices'][$productId]['finalPrice']['weeeAmount'] = $weeeTotal;
120+
$config['optionPrices'][$productId]['finalPrice']['weeeAttributes'] = $formattedWeeeAttributes;
121+
$config['optionPrices'][$productId]['finalPrice']['amountWithoutWeee'] = $basePriceAmount;
122+
$config['optionPrices'][$productId]['finalPrice']['formattedWithoutWeee'] =
123+
$this->formatPrice($basePriceAmount, $config['priceFormat']);
124+
$config['optionPrices'][$productId]['finalPrice']['formattedWithWeee'] =
125+
$this->formatPrice(
126+
$config['optionPrices'][$productId]['finalPrice']['amount'],
127+
$config['priceFormat']
128+
);
129+
}
130+
}
131+
132+
return $this->jsonEncoder->encode($config);
133+
}
134+
}

app/code/Magento/Weee/etc/di.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@
9393
<type name="Magento\ConfigurableProduct\Pricing\Price\FinalPriceResolver">
9494
<plugin name="configurableProductPriceAdjustment" type="Magento\Weee\Plugin\ConfigurableProduct\Pricing\FinalPriceResolver"/>
9595
</type>
96+
<type name="Magento\ConfigurableProduct\Block\Product\View\Type\Configurable">
97+
<plugin name="addWeeeToConfigurableJsonConfig" type="Magento\Weee\Plugin\ConfigurableProduct\Block\Product\View\Type\Configurable"/>
98+
</type>
99+
<type name="Magento\Swatches\Block\Product\Renderer\Listing\Configurable">
100+
<plugin name="addWeeeToSwatchListingJsonConfig" type="Magento\Weee\Plugin\ConfigurableProduct\Block\Product\View\Type\Configurable"/>
101+
</type>
96102
<type name="Magento\Catalog\Model\ResourceModel\Product\Indexer\LinkedProductSelectBuilderByIndexPrice">
97103
<plugin name="weeeAttributeProductSort" type="Magento\Weee\Plugin\Catalog\ResourceModel\Product\WeeeAttributeProductSort"/>
98104
</type>

app/code/Magento/Weee/view/frontend/requirejs-config.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,12 @@ var config = {
99
'taxToggle': 'Magento_Weee/js/tax-toggle',
1010
'Magento_Weee/tax-toggle': 'Magento_Weee/js/tax-toggle'
1111
}
12+
},
13+
config: {
14+
mixins: {
15+
'Magento_Catalog/js/price-box': {
16+
'Magento_Weee/js/price-box-mixin': true
17+
}
18+
}
1219
}
1320
};
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
/**
2+
* Copyright 2025 Adobe
3+
* All Rights Reserved.
4+
*/
5+
define([
6+
'jquery',
7+
'mage/template',
8+
'Magento_Catalog/js/price-utils'
9+
], function ($, mageTemplate, priceUtils) {
10+
'use strict';
11+
12+
return function (priceBox) {
13+
$.widget('mage.priceBox', priceBox, {
14+
15+
options: {
16+
weeeTemplate: '<span class="weee" data-price-type="weee" data-label="<%- data.label %>">' +
17+
'<span class="price"><%- data.formatted %></span>' +
18+
'</span>',
19+
weeeFinalPriceTemplate: '<span class="price-final price-final_price" data-price-type="weeePrice">' +
20+
'<span class="price"><%- data.formatted %></span>' +
21+
'</span>'
22+
},
23+
24+
/**
25+
* Override reloadPrice to add WEEE breakdown
26+
*/
27+
reloadPrice: function reDrawPrices() {
28+
var priceFormat = (this.options.priceConfig && this.options.priceConfig.priceFormat) || {},
29+
priceTemplate = mageTemplate(this.options.priceTemplate);
30+
31+
// First, render prices normally
32+
_.each(this.cache.displayPrices, function (price, priceCode) {
33+
price.final = _.reduce(price.adjustments, function (memo, amount) {
34+
return memo + amount;
35+
}, price.amount);
36+
37+
price.formatted = priceUtils.formatPrice(price.final, priceFormat);
38+
39+
$('[data-price-type="' + priceCode + '"]', this.element).html(priceTemplate({
40+
data: price
41+
}));
42+
}, this);
43+
44+
// Then, add WEEE breakdown if available
45+
this._addWeeeBreakdown();
46+
},
47+
48+
/**
49+
* Add WEEE breakdown to price display
50+
*/
51+
_addWeeeBreakdown: function() {
52+
var productId = this._getSelectedProductId(),
53+
weeeData,
54+
priceContainer,
55+
weeeTemplate,
56+
weeeFinalTemplate,
57+
weeeHtml = '';
58+
59+
if (!productId) {
60+
return;
61+
}
62+
63+
weeeData = this._getWeeeData(productId);
64+
65+
if (!weeeData || !weeeData.weeeAttributes || weeeData.weeeAttributes.length === 0) {
66+
return;
67+
}
68+
69+
// Get templates
70+
weeeTemplate = mageTemplate(this.options.weeeTemplate);
71+
weeeFinalTemplate = mageTemplate(this.options.weeeFinalPriceTemplate);
72+
73+
// Find the price container
74+
priceContainer = this.element.find('[data-price-type="finalPrice"]').parent();
75+
76+
// Remove old WEEE elements
77+
priceContainer.find('.weee, .price-final').remove();
78+
79+
// Update the main price to show base price without WEEE
80+
this.element.find('[data-price-type="finalPrice"]').html(
81+
'<span class="price">' + weeeData.formattedWithoutWeee + '</span>'
82+
);
83+
84+
// Build WEEE HTML using template
85+
_.each(weeeData.weeeAttributes, function(weee) {
86+
weeeHtml += weeeTemplate({
87+
data: {
88+
label: weee.name,
89+
formatted: weee.formatted
90+
}
91+
});
92+
});
93+
94+
// Add final price (with WEEE) using template
95+
weeeHtml += weeeFinalTemplate({
96+
data: {
97+
formatted: weeeData.formattedWithWeee
98+
}
99+
});
100+
101+
// Append to container
102+
priceContainer.append(weeeHtml);
103+
},
104+
105+
/**
106+
* Get selected product ID from configurable/swatch widget
107+
*/
108+
_getSelectedProductId: function() {
109+
var swatchWidget = this._getSwatchWidget(),
110+
configurableWidget;
111+
112+
// Try to get from swatch widget (product detail page)
113+
if (swatchWidget && swatchWidget.getProduct) {
114+
return swatchWidget.getProduct();
115+
}
116+
117+
// Try to get from configurable widget (product detail page)
118+
configurableWidget = this._getConfigurableWidget();
119+
if (configurableWidget && configurableWidget.simpleProduct) {
120+
return configurableWidget.simpleProduct;
121+
}
122+
123+
return null;
124+
},
125+
126+
/**
127+
* Get WEEE data from jsonConfig
128+
*/
129+
_getWeeeData: function(productId) {
130+
var swatchWidget = this._getSwatchWidget(),
131+
configurableWidget,
132+
optionPrices;
133+
134+
if (swatchWidget && swatchWidget.options.jsonConfig) {
135+
optionPrices = swatchWidget.options.jsonConfig.optionPrices;
136+
if (optionPrices && optionPrices[productId] && optionPrices[productId].finalPrice) {
137+
return optionPrices[productId].finalPrice;
138+
}
139+
}
140+
141+
configurableWidget = this._getConfigurableWidget();
142+
if (configurableWidget && configurableWidget.options.spConfig) {
143+
optionPrices = configurableWidget.options.spConfig.optionPrices;
144+
if (optionPrices && optionPrices[productId] && optionPrices[productId].finalPrice) {
145+
return optionPrices[productId].finalPrice;
146+
}
147+
}
148+
149+
return null;
150+
},
151+
152+
/**
153+
* Find the swatch widget relative to this price-box
154+
*/
155+
_getSwatchWidget: function() {
156+
var $productItem = this.element.closest('.product-item, .product-item-info'),
157+
$swatchOptions,
158+
widget;
159+
160+
// On listing pages, find swatch widget in the same product item
161+
if ($productItem.length) {
162+
// On listing pages, swatch renderer uses data-role="swatch-option-{productId}"
163+
$swatchOptions = $productItem.find('[data-role^="swatch-option-"]');
164+
165+
if ($swatchOptions.length) {
166+
widget = $swatchOptions.data('mage-SwatchRenderer') ||
167+
$swatchOptions.data('mageSwatchRenderer');
168+
if (widget) {
169+
return widget;
170+
}
171+
}
172+
173+
// Try product detail page selector
174+
$swatchOptions = $productItem.find('[data-role="swatch-options"]');
175+
if ($swatchOptions.length) {
176+
widget = $swatchOptions.data('mage-SwatchRenderer');
177+
return widget;
178+
}
179+
}
180+
181+
// On product detail page, use global selector
182+
$swatchOptions = $('[data-role="swatch-options"]');
183+
if ($swatchOptions.length) {
184+
return $swatchOptions.data('mage-SwatchRenderer');
185+
}
186+
187+
return null;
188+
},
189+
190+
/**
191+
* Find the configurable widget relative to this price-box
192+
*/
193+
_getConfigurableWidget: function() {
194+
var $productItem = this.element.closest('.product-item, .product-item-info'),
195+
$form;
196+
197+
// On listing pages, find form in the same product item
198+
if ($productItem.length) {
199+
$form = $productItem.find('form');
200+
if ($form.length) {
201+
return $form.data('mageConfigurable') || $form.data('configurable');
202+
}
203+
}
204+
205+
// On product detail page, use global form selector
206+
$form = $('#product_addtocart_form');
207+
if ($form.length) {
208+
return $form.data('mageConfigurable') || $form.data('configurable');
209+
}
210+
211+
return null;
212+
}
213+
});
214+
215+
return $.mage.priceBox;
216+
};
217+
});

0 commit comments

Comments
 (0)