Repository: doofinder/doofinder-prestashop
Pull request #291: PLUGINRANGERS-916 | Unified the way the attributes are transformed for update on save and feed generation
Author: login: davidmolinacano
Created: 2025-02-24T08:16:07Z | Merged: 2025-03-03T12:00:40Z

Required by:

- https://github.com/doofinder/dooplugins/issues/916


Diff:
diff --git a/doofinder.php b/doofinder.php
index 2159f133..494975c7 100644
--- a/doofinder.php
+++ b/doofinder.php
@@ -39,7 +39,7 @@ public function __construct()
     {
         $this->name = 'doofinder';
         $this->tab = 'search_filter';
-        $this->version = '4.13.4';
+        $this->version = '5.0.0';
         $this->author = 'Doofinder (http://www.doofinder.com)';
         $this->ps_versions_compliancy = ['min' => '1.5', 'max' => _PS_VERSION_];
         $this->module_key = 'd1504fe6432199c7f56829be4bd16347';
diff --git a/feeds/product.php b/feeds/product.php
index 7c023976..e0170cad 100644
--- a/feeds/product.php
+++ b/feeds/product.php
@@ -13,6 +13,7 @@
  * @license   GPLv3
  */
 
+use PrestaShop\Module\Doofinder\Src\Entity\DfProductBuild;
 use PrestaShop\Module\Doofinder\Src\Entity\DfTools;
 use PrestaShop\Module\Doofinder\Src\Entity\DoofinderConstants;
 
@@ -38,30 +39,6 @@
 
 DfTools::validateSecurityToken(Tools::getValue('dfsec_hash'));
 
-function slugify($text)
-{
-    // replace non letter or digits by -
-    $text = preg_replace('~[^\\pL\d]+~u', '-', $text);
-
-    // trim
-    $text = trim($text, '-');
-
-    // transliterate
-    $text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);
-
-    // lowercase
-    $text = Tools::strtolower($text);
-
-    // remove unwanted characters
-    $text = preg_replace('~[^-\w]+~', '', $text);
-
-    if (empty($text)) {
-        return 'n-a';
-    }
-
-    return $text;
-}
-
 /**
  *  @author camlafit <https://github.com/camlafit>
  *  Merge multidemensionnal array by value on each row
@@ -104,57 +81,34 @@ function arrayMergeByIdProduct($array1 = [], $array2 = [])
     exit('NOT PROPERLY CONFIGURED');
 }
 
-// CONFIG
+// GENERAL PURPOSE VARIABLES + CONTEXT
 $lang = DfTools::getLanguageFromRequest();
 $context->language = $lang;
-$country = Configuration::get('PS_COUNTRY_DEFAULT');
+$country = (int) DfTools::cfg($shop->id, 'PS_COUNTRY_DEFAULT');
 $context->country = new Country($country);
 $currency = DfTools::getCurrencyForLanguageFromRequest($lang);
-$multiprice_enabled = Configuration::get('DF_MULTIPRICE_ENABLED');
 $currencies = Currency::getCurrenciesByIdShop($context->shop->id);
 
-$cfg_short_description = (DfTools::cfg(
-    $shop->id,
-    'DF_GS_DESCRIPTION_TYPE',
-    DoofinderConstants::GS_SHORT_DESCRIPTION
-) == DoofinderConstants::GS_SHORT_DESCRIPTION);
-
-$cfg_display_prices = DfTools::getBooleanFromRequest(
+/* ---------- START CONFIG ---------- */
+$shouldDisplayPrices = DfTools::getBooleanFromRequest(
     'prices',
     (bool) DfTools::cfg($shop->id, 'DF_GS_DISPLAY_PRICES', DoofinderConstants::YES)
 );
-$cfg_prices_w_taxes = DfTools::getBooleanFromRequest(
+$shouldPricesUseTaxes = DfTools::getBooleanFromRequest(
     'taxes',
     (bool) DfTools::cfg($shop->id, 'DF_GS_PRICES_USE_TAX', DoofinderConstants::YES)
 );
-$cfg_image_size = DfTools::cfg($shop->id, 'DF_GS_IMAGE_SIZE');
-$cfg_mod_rewrite = DfTools::cfg($shop->id, 'PS_REWRITING_SETTINGS', DoofinderConstants::YES);
-$cfg_product_variations = (int) DfTools::cfg($shop->id, 'DF_SHOW_PRODUCT_VARIATIONS');
-$cfg_product_features = DfTools::cfg($shop->id, 'DF_SHOW_PRODUCT_FEATURES');
-$cfg_debug = DfTools::cfg($shop->id, 'DF_DEBUG');
-$cfg_features_shown = explode(',', DfTools::cfg($shop->id, 'DF_FEATURES_SHOWN'));
-
-$cfg_group_attributes_shown = explode(',', DfTools::cfg($shop->id, 'DF_GROUP_ATTRIBUTES_SHOWN'));
-
-$limit_group_attributes = false;
-if (
-    is_array($cfg_group_attributes_shown)
-    && count($cfg_group_attributes_shown) > 0
-    && $cfg_group_attributes_shown[0] !== ''
-) {
-    $group_attributes = AttributeGroup::getAttributesGroups($lang->id);
-    $group_attributes_slug = [];
-    foreach ($group_attributes as $g) {
-        if (in_array($g['id_attribute_group'], $cfg_group_attributes_shown)) {
-            $group_attributes_slug[] = slugify($g['name']);
-        }
-    }
-    $limit_group_attributes = true;
-}
-
+$isMultipriceEnabled = Configuration::get('DF_MULTIPRICE_ENABLED');
+$shouldShowProductVariations = (int) DfTools::cfg($shop->id, 'DF_SHOW_PRODUCT_VARIATIONS');
+$shouldShowProductFeatures = DfTools::cfg($shop->id, 'DF_SHOW_PRODUCT_FEATURES');
+$isDebugEnabled = DfTools::cfg($shop->id, 'DF_DEBUG');
+$featuresShownArray = explode(',', DfTools::cfg($shop->id, 'DF_FEATURES_SHOWN'));
+$attributesShownArray = explode(',', DfTools::cfg($shop->id, 'DF_GROUP_ATTRIBUTES_SHOWN'));
+$shouldLimitGroupAttributes = false;
 $debug = DfTools::getBooleanFromRequest('debug', false);
 $limit = Tools::getValue('limit', false);
 $offset = Tools::getValue('offset', false);
+/* ---------- END CONFIG ---------- */
 
 // To prevent printing errors or warnings that may corrupt the feed.
 if ($debug) {
@@ -165,7 +119,22 @@ function arrayMergeByIdProduct($array1 = [], $array2 = [])
     ini_set('display_errors', 0);
 }
 
-if ($cfg_debug) {
+if (
+    is_array($attributesShownArray)
+    && count($attributesShownArray) > 0
+    && $attributesShownArray[0] !== ''
+) {
+    $groupAttributes = AttributeGroup::getAttributesGroups($lang->id);
+    $groupAttributesSlug = [];
+    foreach ($groupAttributes as $g) {
+        if (in_array($g['id_attribute_group'], $attributesShownArray)) {
+            $groupAttributesSlug[] = DfTools::slugify($g['name']);
+        }
+    }
+    $shouldLimitGroupAttributes = true;
+}
+
+if ($isDebugEnabled) {
     error_log("Starting feed.\n", 3, 'doofinder.log');
 }
 
@@ -178,7 +147,7 @@ function arrayMergeByIdProduct($array1 = [], $array2 = [])
 
 // HEADER
 $header = ['id'];
-if ($cfg_product_variations == 1) {
+if (1 === $shouldShowProductVariations) {
     $header[] = 'item_group_id';
 }
 $header = array_merge($header, [
@@ -193,13 +162,18 @@ function arrayMergeByIdProduct($array1 = [], $array2 = [])
 
 $header[] = 'stock_quantity';
 
-if ($cfg_display_prices) {
+if ($shouldDisplayPrices) {
     $header[] = 'price';
     $header[] = 'sale_price';
-    $header[] = 'df_multiprice';
+
+    if ($isMultipriceEnabled) {
+        $header[] = 'df_multiprice';
+    }
 }
 
-if ($cfg_product_variations == 1) {
+$additionalAttributesHeaders = [];
+
+if (1 === $shouldShowProductVariations) {
     $header[] = 'variation_reference';
     $header[] = 'variation_supplier_reference';
     $header[] = 'variation_mpn';
@@ -207,40 +181,64 @@ function arrayMergeByIdProduct($array1 = [], $array2 = [])
     $header[] = 'variation_upc';
     $header[] = 'df_group_leader';
     $header[] = 'df_variants_information';
-    $attribute_keys = DfTools::getAttributeKeysForShopAndLang($shop->id, $lang->id);
-    $alt_attribute_keys = [];
-    foreach ($attribute_keys as $key) {
-        $header_value = slugify($key);
-        if ($limit_group_attributes && !in_array($header_value, $group_attributes_slug)) {
+    $attributeKeys = DfTools::getAttributeKeysForShopAndLang($shop->id, $lang->id);
+    $altAttributeKeys = [];
+
+    foreach ($attributeKeys as $key) {
+        $headerValue = DfTools::slugify($key);
+        if ($shouldLimitGroupAttributes && !in_array($headerValue, $groupAttributesSlug)) {
             continue;
         }
-        $alt_attribute_keys[] = $key;
-        $header[] = $header_value;
+        $altAttributeKeys[] = $key;
+        $additionalAttributesHeaders[] = $headerValue;
     }
-    $attribute_keys = $alt_attribute_keys;
+    $attributeKeys = $altAttributeKeys;
 }
 
-if ($cfg_product_features) {
-    $all_feature_keys = DfTools::getFeatureKeysForShopAndLang($shop->id, $lang->id);
+if ($shouldShowProductFeatures) {
+    $allFeatureKeys = DfTools::getFeatureKeysForShopAndLang($shop->id, $lang->id);
 
     if (
-        is_array($cfg_features_shown)
-        && count($cfg_features_shown) > 0
-        && $cfg_features_shown[0] !== ''
+        is_array($featuresShownArray)
+        && count($featuresShownArray) > 0
+        && $featuresShownArray[0] !== ''
     ) {
-        $feature_keys = DfTools::getSelectedFeatures($all_feature_keys, $cfg_features_shown);
+        $featurekeys = DfTools::getSelectedFeatures($allFeatureKeys, $featuresShownArray);
     } else {
-        $feature_keys = $all_feature_keys;
+        $featurekeys = $allFeatureKeys;
     }
-    $header[] = 'attributes';
+    $additionalAttributesHeaders[] = 'attributes';
 }
 
+$header = array_merge($header, $additionalAttributesHeaders);
+
 /**
  * @author camlafit <https://github.com/camlafit>
- * Extend doofinder feed
+ * Allows users to extend Doofinder feed by adding extra headers and extra rows.
+ * The hook can be defined in a different custom module or directly in the theme PHP files.
+ * Don't forget to register the hook via registerHook method:
+ *
+ * `$this->registerHook('actionDoofinderExtendFeed');`
  *
- * To add an new header, module can do an array_merge on $extra_header
- * To add an new data to a product, module must create a multidemensionnal array as this :
+ * Example of the function:
+ *
+ * public function hookActionDoofinderExtendFeed($params)
+ * {
+ *     $params['extra_header'] = ['header_custom1', 'header_custom2'];
+ *     $params['extra_rows'] = [
+ *         [
+ *             'id_product' => 1,
+ *             'header_custom1' => 'value1'
+ *         ],
+ *         [
+ *             'id_product' => 2,
+ *             'header_custom2' => 'value2'
+ *         ]
+ *     ];
+ * }
+ *
+ * To add an new header, module can do an array_merge on $extraHeader
+ * To add an new data to a product, module must create a multidimensionnal array as this :
  * array(
  *   index => array(
  *    'id_product' => value,
@@ -248,348 +246,40 @@ function arrayMergeByIdProduct($array1 = [], $array2 = [])
  *   ),
  *   [...]
  * )
- * As each module can extend $extra_header and $extra_rows don't forget to merge them
+ * As each module can extend $extraHeader and $extraRows don't forget to merge them
  */
-$extra_header = [];
-$extra_rows = [];
+$extraHeader = [];
+$extraRows = [];
 Hook::exec('actionDoofinderExtendFeed', [
-    'extra_header' => &$extra_header,
-    'extra_rows' => &$extra_rows,
+    'extra_header' => &$extraHeader,
+    'extra_rows' => &$extraRows,
     'id_lang' => $lang->id,
     'id_shop' => $shop->id,
     'limit' => $limit,
     'offset' => $offset,
 ]);
 
-$header = array_merge($header, $extra_header);
-
-if (!$limit || ($offset !== false && (int) $offset === 0)) {
-    echo implode(DfTools::TXT_SEPARATOR, $header) . PHP_EOL;
-}
+$header = array_merge($header, $extraHeader);
 
 // PRODUCTS
 $rows = DfTools::getAvailableProductsForLanguage($lang->id, $shop->id, $limit, $offset);
 
-$rows = arrayMergeByIdProduct($rows, $extra_rows);
+$rows = arrayMergeByIdProduct($rows, $extraRows);
 
 // In case there is no need to display prices, avoid calculating the mins by variant
-$min_price_variant_by_product_id = ($cfg_product_variations && $cfg_display_prices) ? DfTools::getMinVariantPrices($rows, $cfg_prices_w_taxes, $currencies, $lang->id, $shop->id) : [];
-
-foreach ($rows as $row) {
-    $product_id = $row['id_product'];
-    if (!$cfg_product_variations || DfTools::isParent($row)) {
-        $variant_id = null;
-    } else {
-        $variant_id = $row['id_product_attribute'];
-    }
-    $product_price = DfTools::getPrice($product_id, $cfg_prices_w_taxes, $variant_id);
-    $onsale_price = DfTools::getOnsalePrice($product_id, $cfg_prices_w_taxes, $variant_id);
-    $multiprice = DfTools::getFormattedMultiprice($product_id, $cfg_prices_w_taxes, $currencies, $variant_id);
-
-    if ((int) $row['id_product'] > 0) {
-        // ID, TITLE, LINK
-
-        if (
-            $cfg_product_variations == 1
-            && isset($row['id_product_attribute'])
-            && (int) $row['id_product_attribute'] > 0
-        ) {
-            // ID
-            echo 'VAR-' . $row['id_product_attribute'] . DfTools::TXT_SEPARATOR;
-
-            // ITEM-GROUP-ID
-            echo $row['id_product'] . DfTools::TXT_SEPARATOR;
-            // TITLE
-            $product_title = DfTools::cleanString($row['name']);
-            echo $product_title . DfTools::TXT_SEPARATOR;
-            echo DfTools::cleanURL(
-                $context->link->getProductLink(
-                    (int) $row['id_product'],
-                    $row['link_rewrite'],
-                    $row['cat_link_rew'],
-                    $row['ean13'],
-                    $lang->id,
-                    $shop->id,
-                    (int) $row['id_product_attribute'],
-                    $cfg_mod_rewrite,
-                    false,
-                    true
-                )
-            ) . DfTools::TXT_SEPARATOR;
-        } else {
-            $eanLink = $row['ean13'];
-            // ID
-            echo $row['id_product'] . DfTools::TXT_SEPARATOR;
-
-            if ($cfg_product_variations == 1) {
-                // ITEM-GROUP-ID
-                echo '' . DfTools::TXT_SEPARATOR;
-            }
-
-            // TITLE
-            $product_title = DfTools::cleanString($row['name']);
-            echo $product_title . DfTools::TXT_SEPARATOR;
-
-            $parent_url = DfTools::cleanURL(
-                $context->link->getProductLink(
-                    (int) $row['id_product'],
-                    $row['link_rewrite'],
-                    $row['cat_link_rew'],
-                    $eanLink,
-                    $lang->id,
-                    $shop->id,
-                    0,
-                    $cfg_mod_rewrite
-                )
-            );
-
-            if (key_exists($product_id, $min_price_variant_by_product_id) && !empty($min_price_variant_by_product_id[$product_id])) {
-                $min_variant = $min_price_variant_by_product_id[$product_id];
-                echo $min_variant['onsale_price'] < $onsale_price ? $min_price_variant_by_product_id[$product_id]['link'] : $parent_url;
-            } else {
-                echo $parent_url;
-            }
-            echo DfTools::TXT_SEPARATOR;
-        }
-
-        // DESCRIPTION
-        echo DfTools::cleanString(
-            $row[$cfg_short_description ? 'description_short' : 'description']
-        ) . DfTools::TXT_SEPARATOR;
-
-        // ALTERNATE DESCRIPTION
-        echo DfTools::cleanString(
-            $row[$cfg_short_description ? 'description' : 'description_short']
-        ) . DfTools::TXT_SEPARATOR;
-
-        // META TITLE
-        echo DfTools::cleanString($row['meta_title']) . DfTools::TXT_SEPARATOR;
-
-        // META DESCRIPTION
-        echo DfTools::cleanString($row['meta_description']) . DfTools::TXT_SEPARATOR;
-
-        // IMAGE LINK
-
-        if (
-            $cfg_product_variations == 1 && isset($row['id_product_attribute'])
-            && (int) $row['id_product_attribute'] > 0
-        ) {
-            $cover = Product::getCover($row['id_product_attribute']);
-            $id_image = DfTools::getVariationImg(
-                $row['id_product'],
-                $row['id_product_attribute']
-            );
-
-            if (!empty($id_image)) {
-                $image_link = DfTools::cleanURL(
-                    DfTools::getImageLink(
-                        $row['id_product_attribute'],
-                        $id_image,
-                        $row['link_rewrite'],
-                        $cfg_image_size
-                    )
-                );
-            } else {
-                $image_link = DfTools::cleanURL(
-                    DfTools::getImageLink(
-                        $row['id_product_attribute'],
-                        $row['id_image'],
-                        $row['link_rewrite'],
-                        $cfg_image_size
-                    )
-                );
-            }
-
-            // For variations with no specific pictures
-            if (strpos($image_link, '/-') > -1) {
-                $image_link = DfTools::cleanURL(
-                    DfTools::getImageLink(
-                        $row['id_product'],
-                        $row['id_image'],
-                        $row['link_rewrite'],
-                        $cfg_image_size
-                    )
-                );
-            }
-
-            echo $image_link . DfTools::TXT_SEPARATOR;
-        } else {
-            echo DfTools::cleanURL(
-                DfTools::getImageLink(
-                    $row['id_product'],
-                    $row['id_image'],
-                    $row['link_rewrite'],
-                    $cfg_image_size
-                )
-            ) . DfTools::TXT_SEPARATOR;
-        }
-
-        // MAIN CATEGORY
-        echo DfTools::cleanString($row['main_category']) . DfTools::TXT_SEPARATOR;
-
-        // PRODUCT CATEGORIES
-        echo DfTools::getCategoriesForProductIdAndLanguage(
-            $row['id_product'],
-            $lang->id,
-            $shop->id
-        ) . DfTools::TXT_SEPARATOR;
-
-        // AVAILABILITY
-        $available = (int) $row['available_for_order'] > 0;
-
-        if ((int) DfTools::cfg($shop->id, 'PS_STOCK_MANAGEMENT')) {
-            $stock = StockAvailable::getQuantityAvailableByProduct(
-                $row['id_product'],
-                isset($row['id_product_attribute']) ? $row['id_product_attribute'] : null,
-                $shop->id
-            );
-            $allow_oosp = Product::isAvailableWhenOutOfStock($row['out_of_stock']);
-            echo ($available && ($stock > 0 || $allow_oosp) ? 'in stock' : 'out of stock') . DfTools::TXT_SEPARATOR;
-        } else {
-            echo ($available ? 'in stock' : 'out of stock') . DfTools::TXT_SEPARATOR;
-        }
-
-        // BRAND
-        echo DfTools::cleanString($row['manufacturer']) . DfTools::TXT_SEPARATOR;
-
-        // MPN
-        echo DfTools::cleanString($row['mpn']) . DfTools::TXT_SEPARATOR;
-
-        // EAN13
-        echo DfTools::cleanString($row['ean13']) . DfTools::TXT_SEPARATOR;
-
-        // UPC
-        echo DfTools::cleanString($row['upc']) . DfTools::TXT_SEPARATOR;
-
-        // REFERENCE
-        echo DfTools::cleanString($row['reference']) . DfTools::TXT_SEPARATOR;
-
-        // SUPPLIER_REFERENCE
-        echo DfTools::cleanString($row['supplier_reference']) . DfTools::TXT_SEPARATOR;
-
-        // EXTRA_TITLE_1
-        echo $product_title . DfTools::TXT_SEPARATOR;
-
-        // EXTRA_TITLE_2
-        echo DfTools::splitReferences($product_title) . DfTools::TXT_SEPARATOR;
-
-        // TAGS
-        echo DfTools::escapeSlashes(DfTools::cleanString(DfTools::escapeSlashes($row['tags'])));
-
-        // ISBN
-        if (DfTools::versionGte('1.7.0.0')) {
-            echo DfTools::TXT_SEPARATOR;
-            echo DfTools::cleanString($row['isbn']);
-        }
+$minPriceVariantByProductId = (1 === $shouldShowProductVariations && $shouldDisplayPrices) ? DfTools::getMinVariantPrices($rows, $shouldPricesUseTaxes, $currencies, $lang->id, $shop->id) : [];
+$additionalHeaders = array_merge($additionalAttributesHeaders, $extraHeader);
 
-        // STOCK_QUANTITY
-        echo DfTools::TXT_SEPARATOR;
-        echo DfTools::cleanString($row['stock_quantity']);
+$dfProductBuild = new DfProductBuild($shop->id, $lang->id, $currency->id);
 
-        // PRODUCT PRICE & ON SALE PRICE
-
-        if ($cfg_display_prices && $cfg_product_variations !== 1) {
-            echo DfTools::TXT_SEPARATOR;
-
-            $product_price = DfTools::getPrice($product_id, $cfg_prices_w_taxes);
-            $onsale_price = DfTools::getOnsalePrice($product_id, $cfg_prices_w_taxes);
-            $multiprice = DfTools::getFormattedMultiprice($product_id, $cfg_prices_w_taxes, $currencies);
-
-            if ($row['show_price']) {
-                echo Tools::convertPrice($product_price, $currency);
-                echo DfTools::TXT_SEPARATOR;
-                echo $product_price != $onsale_price ? Tools::convertPrice($onsale_price, $currency) : '';
-                echo DfTools::TXT_SEPARATOR;
-                echo $multiprice_enabled && $multiprice ? $multiprice : '';
-            } else {
-                echo DfTools::TXT_SEPARATOR . DfTools::TXT_SEPARATOR;
-            }
-        } elseif ($cfg_display_prices && $cfg_product_variations == 1) {
-            echo DfTools::TXT_SEPARATOR;
-            // The parent product should have as price the lowest ones of the
-            // variants (combinations) if there are any
-            if (DfTools::isParent($row) && array_key_exists($product_id, $min_price_variant_by_product_id)) {
-                $min_variant = $min_price_variant_by_product_id[$product_id];
-
-                if (
-                    !is_null($min_variant['onsale_price'])
-                    && !is_null($min_variant['price'])
-                    && (empty($onsale_price) || $min_variant['onsale_price'] < $onsale_price)
-                ) {
-                    $product_price = $min_variant['price'];
-                    $onsale_price = $min_variant['onsale_price'];
-                    $multiprice = $min_variant['multiprice'];
-                }
-            }
-
-            if ($row['show_price']) {
-                echo Tools::convertPrice($product_price, $currency);
-                echo DfTools::TXT_SEPARATOR;
-                echo $product_price != $onsale_price ? Tools::convertPrice($onsale_price, $currency) : '';
-                echo DfTools::TXT_SEPARATOR;
-                echo $multiprice_enabled && $multiprice ? $multiprice : '';
-            } else {
-                echo DfTools::TXT_SEPARATOR . DfTools::TXT_SEPARATOR;
-            }
-        }
-
-        if ($cfg_product_variations == 1) {
-            echo DfTools::TXT_SEPARATOR;
-            echo DfTools::cleanString($row['variation_reference']);
-            echo DfTools::TXT_SEPARATOR;
-            echo DfTools::cleanString($row['variation_supplier_reference']);
-            echo DfTools::TXT_SEPARATOR;
-            echo DfTools::cleanString($row['variation_mpn']);
-            echo DfTools::TXT_SEPARATOR;
-            echo DfTools::cleanString($row['variation_ean13']);
-            echo DfTools::TXT_SEPARATOR;
-            echo DfTools::cleanString($row['variation_upc']);
-            echo DfTools::TXT_SEPARATOR;
-            echo DfTools::cleanString($row['df_group_leader']);
-            $variation_attributes = DfTools::getAttributesForProductVariation(
-                $row['id_product_attribute'],
-                $lang->id,
-                $attribute_keys
-            );
-            echo DfTools::TXT_SEPARATOR;
-            if (DfTools::hasAttributes($row['id_product']) && !$row['id_product_attribute']) {
-                $product_attributes = DfTools::hasProductAttributes($row['id_product'], DfTools::cfg($shop->id, 'DF_GROUP_ATTRIBUTES_SHOWN'));
-                if ($product_attributes) {
-                    $attributes = DfTools::getAttributesName($product_attributes, $lang->id);
-
-                    if (is_array($attributes)) {
-                        $variants_keys = array_column($attributes, 'name');
-                        echo implode('%%', array_map('slugify', $variants_keys));
-                    } else {
-                        echo '';
-                    }
-                } else {
-                    echo '';
-                }
-            }
-            foreach ($variation_attributes as $attribute) {
-                echo DfTools::TXT_SEPARATOR . str_replace('/', '//', DfTools::cleanString($attribute));
-            }
-        }
-
-        if ($cfg_product_features) {
-            echo DfTools::TXT_SEPARATOR;
-            foreach (DfTools::getFeaturesForProduct($row['id_product'], $lang->id, $feature_keys) as $key => $values) {
-                foreach ($values as $index => $value) {
-                    echo slugify($key) . '=';
-                    echo str_replace('/', '\/', DfTools::cleanString($value)) . '/';
-                }
-            }
-        }
-
-        /*
-         * @author camlafit <https://github.com/camlafit>
-         */
-        foreach ($extra_header as $extra) {
-            echo DfTools::TXT_SEPARATOR;
-            echo isset($row[$extra]) ? DfTools::cleanString($row[$extra]) : '';
-        }
+$csv = fopen('php://output', 'w');
+if (!$limit || (false !== $offset && 0 === (int) $offset)) {
+    fputcsv($csv, $header, DfTools::TXT_SEPARATOR);
+}
 
-        echo PHP_EOL;
-    }
+foreach ($rows as $row) {
+    $product = $dfProductBuild->buildProduct($row, $minPriceVariantByProductId, $additionalAttributesHeaders, $additionalHeaders);
+    $product = $dfProductBuild->applySpecificTransformationsForCsv($product, $extraHeader, $header);
+    fputcsv($csv, $product, DfTools::TXT_SEPARATOR);
 }
+fclose($csv);
diff --git a/src/Entity/DfProductBuild.php b/src/Entity/DfProductBuild.php
index 8ed93603..760f4eae 100644
--- a/src/Entity/DfProductBuild.php
+++ b/src/Entity/DfProductBuild.php
@@ -44,6 +44,17 @@ public function __construct($idShop, $idLang, $idCurrency)
         $this->idLang = $idLang;
         $this->idCurrency = $idCurrency;
         $this->currencies = \Currency::getCurrenciesByIdShop($idShop);
+        $this->attributesShown = \Configuration::get('DF_GROUP_ATTRIBUTES_SHOWN');
+        $this->displayPrices = \Configuration::get('DF_GS_DISPLAY_PRICES');
+        $this->imageSize = \Configuration::get('DF_GS_IMAGE_SIZE');
+        $this->link = \Context::getContext()->link;
+        $this->linkRewriteConf = \Configuration::get('PS_REWRITING_SETTINGS');
+        $this->productVariations = \Configuration::get('DF_SHOW_PRODUCT_VARIATIONS');
+        $this->showProductFeatures = \Configuration::get('DF_SHOW_PRODUCT_FEATURES');
+        $this->stockManagement = \Configuration::get('PS_STOCK_MANAGEMENT');
+        $this->useTax = \Configuration::get('DF_GS_PRICES_USE_TAX');
+        $this->multipriceEnabled = \Configuration::get('DF_MULTIPRICE_ENABLED');
+        $this->featuresKeys = $this->getFeaturesKeys();
     }
 
     /**
@@ -58,52 +69,27 @@ public function setProducts($arrayProducts)
 
     public function build()
     {
-        $this->assign();
-
         $payload = [];
 
         $products = $this->getProductData();
 
+        $minPriceVariantByProductId = DfTools::getMinVariantPrices($products, $this->useTax, $this->currencies, $this->idLang, $this->idShop);
+
         foreach ($products as $product) {
-            $payload[] = $this->buildProduct($product);
+            $payload[] = $this->buildProduct($product, $minPriceVariantByProductId);
         }
 
         return json_encode($payload);
     }
 
-    private function assign()
-    {
-        $this->attributesShown = \Configuration::get('DF_GROUP_ATTRIBUTES_SHOWN');
-        $this->displayPrices = \Configuration::get('DF_GS_DISPLAY_PRICES');
-        $this->imageSize = \Configuration::get('DF_GS_IMAGE_SIZE');
-        $this->link = \Context::getContext()->link;
-        $this->linkRewriteConf = \Configuration::get('PS_REWRITING_SETTINGS');
-        $this->productVariations = \Configuration::get('DF_SHOW_PRODUCT_VARIATIONS');
-        $this->showProductFeatures = \Configuration::get('DF_SHOW_PRODUCT_FEATURES');
-        $this->stockManagement = \Configuration::get('PS_STOCK_MANAGEMENT');
-        $this->useTax = \Configuration::get('DF_GS_PRICES_USE_TAX') || DoofinderConstants::NO;
-        $this->multipriceEnabled = \Configuration::get('DF_MULTIPRICE_ENABLED');
-        $this->featuresKeys = $this->getFeaturesKeys();
-    }
-
-    private function getProductData()
-    {
-        $products = DfTools::getAvailableProductsForLanguage(
-            $this->idLang,
-            $this->idShop,
-            false,
-            false,
-            $this->products
-        );
-
-        return $products;
-    }
-
-    private function buildProduct($product)
+    public function buildProduct($product, $minPriceVariantByProductId = [], $extraAttributesHeader = [], $extraHeaders = [])
     {
         $p = [];
 
         $p['id'] = $this->getId($product);
+        if ($this->productVariations) {
+            $p['item_group_id'] = $this->getItemGroupId($product);
+        }
         $p['title'] = DfTools::cleanString($product['name']);
         $p['link'] = $this->getLink($product);
         $p['description'] = DfTools::cleanString($product['description_short']);
@@ -128,12 +114,12 @@ private function buildProduct($product)
         $p['extra_title_1'] = $p['title'];
         $p['extra_title_2'] = DfTools::splitReferences($p['title']);
         $p['tags'] = DfTools::cleanString($product['tags']);
-        $p['stock_quantity'] = DfTools::cleanString($product['stock_quantity']);
 
         if (DfTools::versionGte('1.7.0.0')) {
             $p['isbn'] = DfTools::cleanString($product['isbn']);
         }
 
+        $p['stock_quantity'] = DfTools::cleanString($product['stock_quantity']);
         if ($this->displayPrices) {
             $p['price'] = $this->getPrice($product);
             $p['sale_price'] = $this->getPrice($product, true);
@@ -141,33 +127,136 @@ private function buildProduct($product)
             if ($this->multipriceEnabled) {
                 $p['df_multiprice'] = $this->getMultiprice($product);
             }
-        }
 
-        if ($this->showProductFeatures) {
-            $p = array_merge($p, $this->getFeatures($product));
+            if (DfTools::isParent($product) && array_key_exists($p['id'], $minPriceVariantByProductId)) {
+                $minVariant = $minPriceVariantByProductId[$p['id']];
+                if (
+                    !is_null($minVariant['onsale_price'])
+                    && !is_null($minVariant['price'])
+                    && (empty($p['sale_price']) || $minVariant['onsale_price'] < $p['sale_price'])
+                ) {
+                    $p['price'] = $minVariant['price'];
+                    $p['sale_price'] = ($minVariant['onsale_price'] === $minVariant['price']) ? null : $minVariant['onsale_price'];
+                    if ($this->multipriceEnabled) {
+                        $p['df_multiprice'] = $minVariant['multiprice'];
+                    }
+                }
+            }
         }
 
         if ($this->productVariations) {
-            $p['item_group_id'] = $this->getItemGroupId($product);
-            $p['group_id'] = $this->getItemGroupId($product);
-
             $p['variation_reference'] = $product['variation_reference'];
             $p['variation_supplier_reference'] = $product['variation_supplier_reference'];
             $p['variation_mpn'] = $product['variation_mpn'];
             $p['variation_ean13'] = $product['variation_ean13'];
             $p['variation_upc'] = $product['variation_upc'];
-            $p['df_group_leader'] = (!is_null($product['df_group_leader'] && $product['df_group_leader']) ? true : false);
+            $p['df_group_leader'] = (is_numeric($product['df_group_leader']) && 0 !== (int) $product['df_group_leader']);
+            $p['df_variants_information'] = $this->getVariantsInformation($product);
 
             $attributes = $this->getAttributes($product);
 
             $p = array_merge($p, $attributes);
 
-            $p['df_variants_information'] = $this->getVariantsInformation($product);
+            foreach ($extraAttributesHeader as $extraAttributeHeader) {
+                if ('attributes' !== $extraAttributeHeader && !array_key_exists($extraAttributeHeader, $p) && array_key_exists($extraAttributeHeader, $attributes)) {
+                    $p[$extraAttributeHeader] = $attributes[$extraAttributeHeader];
+                }
+            }
+        }
+
+        if ($this->showProductFeatures) {
+            $p['features'] = $this->getFeatures($product);
+        }
+
+        foreach ($extraHeaders as $extraHeader) {
+            if (!empty($p[$extraHeader])) {
+                continue;
+            }
+            $p[$extraHeader] = isset($product[$extraHeader]) ? DfTools::cleanString($product[$extraHeader]) : '';
         }
 
         return $p;
     }
 
+    /**
+     * Applies specific transformations to a product's data for CSV export.
+     *
+     * This method performs several modifications on the product array, like:
+     * - If multi-price is enabled, formats the multiprice field.
+     * - Joins category values using a predefined separator.
+     * - If variants information exists, it slugifies and joins them using "%%".
+     * - Casts the group leader flag to an integer.
+     * - Iterates over extra headers to process attribute values:
+     *   - For each non-empty extra header, it concatenates key-value pairs in the format "key=value",
+     *     with the value cleaned and any "/" characters escaped.
+     * - Processes features (if present and an array) by converting them into an attributes string
+     *   formatted as "key=value" pairs joined with "/". The original features key is removed.
+     * - Ensures the final product fields are ordered according to the given headers.
+     *
+     * @param array $product the associative array representing the product data
+     * @param array $extraHeaders an array of additional headers to process in the product data
+     * @param array $allHeaders an array specifying the order of CSV fields
+     *
+     * @return array the transformed product array ready for CSV export
+     */
+    public function applySpecificTransformationsForCsv($product, $extraHeaders, $allHeaders)
+    {
+        if ($this->multipriceEnabled) {
+            $product['df_multiprice'] = DfTools::getFormattedMultiprice($product['df_multiprice']);
+        }
+        $product['categories'] = implode(DfTools::CATEGORY_SEPARATOR, $product['categories']);
+
+        if (array_key_exists('df_variants_information', $product)) {
+            $product['df_variants_information'] = implode('%%', array_map([__NAMESPACE__ . '\DfTools', 'slugify'], $product['df_variants_information']));
+        }
+
+        $product['df_group_leader'] = (int) $product['df_group_leader'];
+
+        if (array_key_exists('features', $product) && is_array($product['features'])) {
+            $formattedAttributes = [];
+            foreach ($product['features'] as $key => $value) {
+                if (is_array($value)) {
+                    $keyValueToReturn = [];
+                    foreach ($value as $singleValue) {
+                        $keyValueToReturn[] = $key . '=' . $singleValue;
+                    }
+                    $formattedAttributes[] = implode('/', $keyValueToReturn);
+                } else {
+                    $formattedAttributes[] = $key . '=' . $value;
+                }
+            }
+            $product['attributes'] = implode('/', $formattedAttributes);
+            unset($product['features']);
+        }
+
+        $product = self::ensureCsvFieldsOrder($product, $allHeaders);
+
+        return $product;
+    }
+
+    private static function ensureCsvFieldsOrder($product, $allHeaders)
+    {
+        $productWithSortedAttributes = [];
+        foreach ($allHeaders as $header) {
+            $productWithSortedAttributes[$header] = array_key_exists($header, $product) ? $product[$header] : '';
+        }
+
+        return $productWithSortedAttributes;
+    }
+
+    private function getProductData()
+    {
+        $products = DfTools::getAvailableProductsForLanguage(
+            $this->idLang,
+            $this->idShop,
+            false,
+            false,
+            $this->products
+        );
+
+        return $products;
+    }
+
     private function getId($product)
     {
         if ($this->haveVariations($product)) {
@@ -341,10 +430,10 @@ private function getFeatures($product)
         foreach (DfTools::getFeaturesForProduct($product['id_product'], $this->idLang, $keys) as $key => $values) {
             if (count($values) > 1) {
                 foreach ($values as $value) {
-                    $features[$this->slugify($key)][] = DfTools::cleanString($value);
+                    $features[DfTools::slugify($key)][] = DfTools::cleanString($value);
                 }
             } else {
-                $features[$this->slugify($key)] = DfTools::cleanString($values[0]);
+                $features[DfTools::slugify($key)] = DfTools::cleanString($values[0]);
             }
         }
 
@@ -374,7 +463,7 @@ private function getAttributes($product)
         $altAttributes = [];
 
         foreach ($attributes as $attribute) {
-            $altAttributes[$this->slugify($attribute['group_name'])] = $attribute['name'];
+            $altAttributes[DfTools::slugify($attribute['group_name'])] = $attribute['name'];
         }
 
         return $altAttributes;
@@ -404,33 +493,9 @@ private function getVariantsInformation($product)
 
             $names = array_column($attributes, 'name');
 
-            return array_map([$this, 'slugify'], $names);
+            return array_map([__NAMESPACE__ . '\DfTools', 'slugify'], $names);
         }
 
         return [];
     }
-
-    private function slugify($text)
-    {
-        // replace non letter or digits by -
-        $text = preg_replace('~[^\\pL\d]+~u', '-', $text);
-
-        // trim
-        $text = trim($text, '-');
-
-        // transliterate
-        $text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);
-
-        // lowercase
-        $text = \Tools::strtolower($text);
-
-        // remove unwanted characters
-        $text = preg_replace('~[^-\w]+~', '', $text);
-
-        if (empty($text)) {
-            return 'n-a';
-        }
-
-        return $text;
-    }
 }
diff --git a/src/Entity/DfTools.php b/src/Entity/DfTools.php
index 228e059f..24f43c91 100644
--- a/src/Entity/DfTools.php
+++ b/src/Entity/DfTools.php
@@ -1376,9 +1376,9 @@ public static function getMinVariantPrices($products, $includeTaxes, $currencies
             $variantId = $product['id_product_attribute'];
             $variantPrice = self::getPrice($productId, $includeTaxes, $variantId);
             $variantOnsalePrice = self::getOnsalePrice($productId, $includeTaxes, $variantId);
-            $variantMultiprice = self::getFormattedMultiprice($productId, $includeTaxes, $currencies, $variantId);
+            $variantMultiprice = self::getMultiprice($productId, $includeTaxes, $currencies, $variantId);
 
-            if (key_exists($productId, $minPricesByProductId)) {
+            if (array_key_exists($productId, $minPricesByProductId)) {
                 $currentMinPrices = $minPricesByProductId[$productId];
 
                 /*
@@ -1498,28 +1498,6 @@ public static function getMultiprice($productId, $includeTaxes, $currencies, $va
         return $multiprice;
     }
 
-    /**
-     * Given a product and a list of currencies, returns the multiprice field
-     * in the correct format to be used in the feed CSV.
-     *
-     * An example of a value for this field is
-     * "price_EUR=5/sale_price_EUR=3/price_GBP=4.3/sale_price_GBP=2.7"
-     * for a list containing two currencies ["EUR", "GBP"].
-     *
-     * @param int $productId Id of the product to calculate the multiprice for
-     * @param bool $includeTaxes Determines if taxes have to be included in the calculated prices
-     * @param array $currencies List of currencies to consider for the multiprice calculation
-     * @param int $variantId When specified, the multiprice will be calculated for that variant
-     *
-     * @return string
-     */
-    public static function getFormattedMultiprice($productId, $includeTaxes, $currencies, $variantId = null)
-    {
-        $multiprice = self::getMultiprice($productId, $includeTaxes, $currencies, $variantId);
-
-        return self::formatMultiprice($multiprice);
-    }
-
     /**
      * Returns the API Key without the region part.
      *
@@ -1544,7 +1522,7 @@ public static function getFormattedApiKey()
      *
      * @return string
      */
-    private static function formatMultiprice($multiprice)
+    public static function getFormattedMultiprice($multiprice)
     {
         $multiprices = [];
 
@@ -1557,6 +1535,30 @@ private static function formatMultiprice($multiprice)
         return implode('/', $multiprices);
     }
 
+    public static function slugify($text)
+    {
+        // replace non letter or digits by -
+        $text = preg_replace('~[^\\pL\d]+~u', '-', $text);
+
+        // trim
+        $text = trim($text, '-');
+
+        // transliterate
+        $text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);
+
+        // lowercase
+        $text = \Tools::strtolower($text);
+
+        // remove unwanted characters
+        $text = preg_replace('~[^-\w]+~', '', $text);
+
+        if (empty($text)) {
+            return 'n-a';
+        }
+
+        return $text;
+    }
+
     private static function getVariantUrl($product, $context)
     {
         $context = \Context::getContext();
diff --git a/src/Entity/DoofinderConstants.php b/src/Entity/DoofinderConstants.php
index 8b3fee1c..e7786cee 100644
--- a/src/Entity/DoofinderConstants.php
+++ b/src/Entity/DoofinderConstants.php
@@ -27,7 +27,7 @@ class DoofinderConstants
     const DOOPHOENIX_REGION_URL = 'https://%ssearch.doofinder.com';
     const GS_SHORT_DESCRIPTION = 1;
     const GS_LONG_DESCRIPTION = 2;
-    const VERSION = '4.13.4';
+    const VERSION = '5.0.0';
     const NAME = 'doofinder';
     const YES = 1;
     const NO = 0;


Reviews:
user: login: sofia-doofinder
body: David el fichero product.php no se puede mejorar un poco mas?  sobre todo la parte primera que hay como mucho código y no se si realmente es todo necesario... ya que puntuamos esta tarea con un 5 estaría bien darle unas vueltas para intentar dejar esta parte lo mas limpia y clara posible. hay algunas variables también que igual se pueden renombrar para que sean un poco mas claras, cosas como `cfg_display_prices` o `cfg_product_variations`  se hacen un poco raras, por qué ese `cfg` inicial?

 Si crees que ya se ha sacado todo lo posible me dices
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-02-24T17:20:39Z
user: login: davidmolinacano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-25T07:36:25Z
user: login: sofia-doofinder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-25T07:40:23Z
user: login: davidmolinacano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-25T07:41:41Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (10)</summary><blockquote>

<details>
<summary>feeds/product.php (4)</summary><blockquote>

`87-87`: **Consider spacing for readability.**

`$country =(int) ...` is missing a space after `=`. While minor, adding it would increase consistency.

---

`150-150`: **Boolean condition alignment.**

`if ($shouldShowProductVariations == 1)` may be clearer as `if ((int)$shouldShowProductVariations === 1)` or simply `if ($shouldShowProductVariations)`, depending on the variable type.

---

`184-186`: **Flexible attribute keys.**

This section correctly retrieves attribute keys and filters them by group if needed. Using slugified names is good. Continue ensuring attribute collisions are handled to prevent overlapping slugs. 



Also applies to: 187-194

---

`249-250`: **By-reference parameters.**

`&$extraHeader` and `&$extraRows` enable real-time modification by hooks. This is flexible but can lead to side effects if modules don’t manage merges carefully.

</blockquote></details>
<details>
<summary>src/Entity/DfTools.php (1)</summary><blockquote>

`1525-1536`: **Consider potential collisions in formatted multiprice delimiters.**

While this approach is sound for most use cases, using `"/"` as a delimiter for both currency entries and price entries can lead to ambiguities if any unexpected characters appear (e.g., user-generated values containing slashes). You might consider stricter validation or a different delimiter to ensure clarity.

</blockquote></details>
<details>
<summary>src/Entity/DfProductBuild.php (5)</summary><blockquote>

`131-144`: **Update parent product prices with variant minimums.**

This logic is a good improvement, using `array_key_exists($p['id'], $minPriceVariantByProductId)` to override parent product prices. Just confirm that `isParent($product)` and `$p['id']` consistently reflect the correct base product ID for this override.

---

`172-177`: **Extra headers consolidation.**

Falling back to `$product[$extraHeader]` if `$p[$extraHeader]` is empty helps unify data fields. Verify that all extra headers are sanitized properly before stored.

---

`246-254`: **Column ordering ensures consistent CSV structure.**

Using `ensureCsvFieldsOrder` to reorder the final product fields is a good way to keep CSV output stable across multiple consumers. If any mandatory headers are missing from `$allHeaders`, consider throwing or logging a notice to avoid silent data loss.

---

`475-475`: **Slugifying group name for attribute keys.**

Converting `group_name` to a slug for use as the array key helps normalize the data. Consider verifying that `group_name` is correctly localized for all shops and languages.

---

`505-505`: **Provide clarity to variant names.**

Slugifying variant attribute group names yields consistency. If the user needs more descriptive or locale-based text, consider storing both an original name and a slugified version for maximum flexibility.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 9a4b8894c1671a6cb5b4259967a3753ba341f26b and 62670b425ed52189edea4af4f9e155419c4d3a2d.

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `doofinder.php` (1 hunks)
* `feeds/product.php` (5 hunks)
* `src/Entity/DfProductBuild.php` (6 hunks)
* `src/Entity/DfTools.php` (3 hunks)
* `src/Entity/DoofinderConstants.php` (1 hunks)

</details>

<details>
<summary>🔇 Additional comments (34)</summary><blockquote>

<details>
<summary>src/Entity/DoofinderConstants.php (1)</summary>

`30-30`: **Major version bump confirmation.**

Shifting from 4.13.0 to 5.0.0 is a significant semantic jump, indicating potential breaking changes. Ensure that documentation and upgrade instructions reflect this major version increase.

</details>
<details>
<summary>doofinder.php (1)</summary>

`42-42`: **Version alignment acknowledged.**

The constructor’s version now aligns with the constants file. Confirm that any references to 4.13.0 elsewhere have been updated for consistency.

</details>
<details>
<summary>feeds/product.php (19)</summary>

`16-18`: **New imports facilitate modularity.**

Adopting `DfProductBuild`, `DfTools`, and `DoofinderConstants` helps keep feed logic cleaner. Good step toward maintaining a more object-oriented structure.

---

`84-84`: **Trivial comment change.**

No functional changes detected. Nothing to address here.

---

`92-111`: **Config extraction improvements.**

This block elegantly replaces older config variable names with more descriptive ones (`shouldDisplayPrices`, `shouldPricesUseTaxes`, etc.). The approach clarifies the feed’s behavior.

---

`122-135`: **Controlled group attribute display.**

Conditionally limiting group attributes is a concise approach. Consider verifying collisions if two group names slugify to the same string.

---

`165-172`: **Dynamic price headers.**

Including `price`, `sale_price`, and `df_multiprice` only when prices are displayed is logical. Double-check that any downstream consumers handle missing columns gracefully when `prices` is false.

---

`174-175`: **Initial placeholder arrays.**

`$additionalAttributesHeaders` declared upfront helps keep logic straightforward. Looks good.

---

`198-199`: **Feature toggling recognized.**

Wrapping feature logic with `$shouldShowProductFeatures` is clear and maintainable.

---

`202-204`: **User-defined feature subset.**

Selecting only certain features by ID helps keep the feed smaller. Confirm that the final feed data meets business requirements if some features are excluded.

---

`213-213`: **Appending to the header.**

Merging additional attributes last ensures they’re placed conveniently at the end of the CSV. This is consistent.

---

`215-248`: **Extendable feed via hooks.**

Allowing other modules to extend headers and rows is powerful. Check for potential naming collisions if multiple modules add the same header key.

---

`257-257`: **Header final merge.**

Merging previously collected headers ensures all columns appear correctly. Straightforward approach.

---

`261-261`: **CSV header writing.**

Printing CSV headers once is standard practice. Keep consistent ordering for reliable downstream parse behavior.

---

`263-263`: **Stream closure after header.**

Closing and reopening the stream is an interesting choice. Ensure no data flush issues occur with large feeds on certain hosting environments.

---

`268-268`: **Merged extra rows.**

Combining `extraRows` with main rows is a neat solution for hooking custom data. Confirm that merges preserve unique IDs correctly.

---

`270-270`: **Conditionally compute min variant prices.**

Skipping variant prices when the user doesn’t want prices is an efficient early bailout. Nicely avoids unnecessary computations.

---

`273-273`: **Consolidating column definitions.**

Merging `$additionalAttributesHeaders` and `$extraHeader` keeps track of all custom columns in one place.

---

`274-274`: **DfProductBuild instantiation.**

Centralizing product building with a dedicated class is a great way to separate concerns and improve maintainability.

---

`277-281`: **CSV row generation pipeline.**

`buildProduct` → `applySpecificTransformationsForCsv` → `fputcsv` is a clean, stepwise pipeline. This approach is clear, testable, and maintainable.

---

`282-282`: **Final close on output stream.**

Closing the CSV ensures no data remains buffered. Good practice for proper resource management.

</details>
<details>
<summary>src/Entity/DfTools.php (2)</summary>

`1379-1404`: **Ensure variant price checks handle edge cases.**

This block correctly updates minimum variant prices; however, consider verifying that `onsale_price` and `price` are always set and non-null before comparing them. In rare cases, if the array key is missing or its value is unexpectedly `null`, this conditional logic may behave incorrectly.

---

`1538-1560`: **Handle edge cases for slug generation.**

This helper method correctly replaces undesired characters, transliterates, and lowercases the text. However, note that `iconv` may return `false` if transliteration fails or the environment lacks the required support. Consider verifying the return value of `iconv` and handling fallback scenarios gracefully.

</details>
<details>
<summary>src/Entity/DfProductBuild.php (11)</summary>

`47-57`: **Constructor property initialization looks consistent.**

All relevant configuration values are properly loaded and assigned. Just ensure each configuration key is defined and that string vs. boolean checks (like `$this->useTax`) handle unintended values safely.

---

`76-79`: **Min variant price integration seems correct.**

You accurately retrieve minimal prices via `DfTools::getMinVariantPrices` and pass them to `buildProduct`. Ensure that the returned array keys match product IDs exactly (e.g., type consistency) to avoid lookup failures.

---

`85-85`: **Method signature accommodates extra configuration.**

The added parameters (`$minPriceVariantByProductId`, `$extraAttributesHeader`, `$extraHeaders`) support more flexible product building. Be aware of potential data shape issues if these parameters are empty or missing keys.

---

`90-92`: **Conditional usage of item group ID.**

Assigning `item_group_id` only when `$this->productVariations` is enabled helps keep the code streamlined. Ensure that `getItemGroupId()` returns valid identifiers for all variations.

---

`122-122`: **Validate stock quantity casting.**

Storing stock quantity via `DfTools::cleanString($product['stock_quantity'])` is typically safe, but ensure that the source field is numeric to avoid potential non-numeric strings.

---

`153-155`: **Marking group leader and collecting variant information.**

Explicitly checking and casting `(is_numeric($product['df_group_leader']) && 0 !== (int)$product['df_group_leader'])` ensures correct boolean representation in `$p['df_group_leader']`. Ensure that all variant data keys (`'variation_mpn'`, `'variation_ean13'`, etc.) are defined and consistent.

---

`160-166`: **Extensible attribute logic.**

Copying attributes dynamically into `$p` based on `$extraAttributesHeader` adds flexibility. Just confirm that `'attributes'` remains excluded to prevent duplication or inadvertently overwriting existing fields.

---

`168-171`: **Feature injection when enabled.**

The conditional addition of `$p['features']` respects the setting in `$this->showProductFeatures`. Make sure that the `getFeatures` method consistently returns arrays, even if no features are present.

---

`182-244`: **Effective CSV transformations.**

This new method applies transformations and ensures the CSV output is well-structured:
- Multi-price fields get formatted.
- Category arrays join with a known separator.
- Variation data slugifies for consistent representation.
- Feature data merges into an `attributes` string.

These are solid improvements, but remain mindful of any concurrency between `attributes` and `features` so that they do not get overwritten unintentionally.

---

`256-267`: **Separate method for product retrieval.**

Encapsulating the retrieval logic in `getProductData()` clarifies the flow. If performance becomes a concern, caching or partial loads may be worth exploring, but for now, this is clean.

---

`442-443`: **Feature slugification during accumulation.**

In `getFeatures()`, slugs are generated as array keys. This is convenient for search, but be sure that the slugified keys remain unique across different languages or feature sets.



Also applies to: 445-445

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-02-25T10:01:26Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>feeds/product.php (1)</summary><blockquote>

`259-264`: **Potential inefficiency with CSV file handling**

The CSV file is opened and closed twice in the code.

Consider opening the CSV file once and keeping it open until all output is complete:

```diff
$csv = fopen('php://output', 'w');
if (!$limit || ($offset !== false && (int) $offset === 0)) {
    fputcsv($csv, $header, DfTools::TXT_SEPARATOR);
}
-fclose($csv);
-
// PRODUCTS
$rows = DfTools::getAvailableProductsForLanguage($lang->id, $shop->id, $limit, $offset);

$rows = arrayMergeByIdProduct($rows, $extraRows);

// In case there is no need to display prices, avoid calculating the mins by variant
$minPriceVariantByProductId = $shouldDisplayPrices ? DfTools::getMinVariantPrices($rows, $shouldPricesUseTaxes, $currencies, $lang->id, $shop->id) : [];
$additionalHeaders = array_merge($additionalAttributesHeaders, $extraHeader);

$dfProductBuild = new DfProductBuild($shop->id, $lang->id, $currency->id);

-$csv = fopen('php://output', 'w');
foreach ($rows as $row) {
    $product = $dfProductBuild->buildProduct($row, $minPriceVariantByProductId, $additionalAttributesHeaders, $additionalHeaders);
    $product = $dfProductBuild->applySpecificTransformationsForCsv($product, $extraHeader, $header);
    fputcsv($csv, $product, DfTools::TXT_SEPARATOR);
}
fclose($csv);
```


Also applies to: 276-282

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 62670b425ed52189edea4af4f9e155419c4d3a2d and e6e9e2174e0395908f5b7cf2a3e30e1fc921e392.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `feeds/product.php` (5 hunks)

</details>

<details>
<summary>🔇 Additional comments (11)</summary><blockquote>

<details>
<summary>feeds/product.php (11)</summary>

`16-16`: **Added import for DfProductBuild class**

This change supports the refactoring that moves product building logic to a dedicated class, following the Single Responsibility Principle.

---

`84-87`: **Improved code organization with clear variable initialization**

Added clear section comment and improved variable initialization structure for better readability.

---

`92-107`: **Enhanced configuration with better variable naming**

Renamed configuration variables to follow a more descriptive naming convention using the "should" prefix for boolean flags. This makes the code more self-documenting and easier to understand.

---

`122-135`: **Improved group attributes processing**

The conditional logic is now clearer with better variable names, and the code now uses `DfTools::slugify()` instead of an inline function that was likely removed.

---

`137-139`: **Debug logging with descriptive variable name**

Using the more descriptive variable name `$isDebugEnabled` improves code readability.

---

`150-151`: **Improved conditional header building**

Using more descriptive variable names makes the logic clearer and easier to understand. The conditions for including various headers are now more readable.



Also applies to: 165-172

---

`174-196`: **Better attribute headers handling**

The code now uses a more structured approach to build attribute headers with better variable naming. The use of `DfTools::slugify()` ensures consistent slugification across the codebase.

---

`198-211`: **Enhanced product features handling**

The logic for selecting and processing product features has been improved with better variable naming and more structured conditionals.

---

`216-245`: **Improved hook documentation**

The documentation for the hook has been updated with a clearer example of how to use it, which will help developers extend the functionality.

---

`246-255`: **Better hook parameter naming**

There's a potential issue with parameter naming consistency.

The variable is named `$extraHeader` but the parameter passed to the hook is `'extra_header'` (with an underscore). This mismatch could cause confusion. Consider standardizing the naming:

```diff
-    'extra_header' => &$extraHeader,
+    'extra_headers' => &$extraHeader,
```

Or update the variable name to match the parameter name.

---

`274-280`: **Improved product building with separation of concerns**

The code now uses a dedicated class for product building, following the Single Responsibility Principle. This approach makes the code more maintainable and testable.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-02-25T10:12:06Z
user: login: sofia-doofinder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-25T11:42:54Z
user: login: sofia-doofinder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-25T11:43:59Z
user: login: sofia-doofinder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-25T11:46:55Z
user: login: davidmolinacano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-25T13:35:01Z
user: login: davidmolinacano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-25T13:36:06Z
user: login: davidmolinacano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-25T13:48:45Z
user: login: sofia-doofinder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-25T14:09:36Z
user: login: davidmolinacano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-25T14:11:24Z
user: login: sofia-doofinder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-25T14:11:35Z
user: login: davidmolinacano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-25T14:41:21Z
user: login: davidmolinacano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-25T14:59:59Z
user: login: sofia-doofinder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-25T15:04:39Z
user: login: sofia-doofinder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-25T15:05:12Z
user: login: davidmolinacano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-25T15:19:01Z
user: login: davidmolinacano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-25T15:23:02Z
user: login: sofia-doofinder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-26T11:09:11Z
user: login: sofia-doofinder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-26T11:13:35Z
user: login: davidmolinacano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T12:00:11Z
user: login: sofia-doofinder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-26T12:15:38Z
user: login: davidmolinacano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T12:17:35Z
user: login: sofia-doofinder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-26T12:36:43Z
user: login: davidmolinacano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T13:14:50Z
user: login: IgnacioPursalsZ
state: COMMENTED
author_association: NONE
submitted_at: 2025-02-26T17:57:52Z
user: login: davidmolinacano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-27T08:19:37Z
user: login: sofia-doofinder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-27T08:47:20Z
user: login: davidmolinacano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-27T10:34:39Z
user: login: davidmolinacano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-27T10:46:07Z
user: login: sofia-doofinder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-27T11:22:59Z
user: login: sofia-doofinder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-27T11:23:18Z
user: login: davidmolinacano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-27T11:24:55Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>src/Entity/DfProductBuild.php (1)</summary><blockquote>

`216-219`: **Consider adding null check for extraHeader values.**

When processing the `extraHeaders` array, there's no explicit check for null values which could potentially cause issues.

```diff
-        foreach ($extraHeaders as $extraHeader) {
-            if (empty($product[$extraHeader]) || !is_array($product[$extraHeader])) {
+        foreach ($extraHeaders as $extraHeader) {
+            if (empty($extraHeader) || empty($product[$extraHeader]) || !is_array($product[$extraHeader])) {
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 808fc62f7682acd75148e7c09fa1038ba5edeca3 and 1ecf8fc7acf1f7d169522f48164adddb187b7b37.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `feeds/product.php` (5 hunks)
* `src/Entity/DfProductBuild.php` (6 hunks)

</details>

<details>
<summary>🔇 Additional comments (17)</summary><blockquote>

<details>
<summary>feeds/product.php (8)</summary>

`16-16`: **Good addition of DfProductBuild dependency.**

Adding this import supports the new refactored approach to product data processing.

---

`84-88`: **Improved variable naming for better readability.**

The renamed variables provide more clarity about their purpose and content - this is a good improvement over the previous naming.

---

`92-107`: **Well-structured config section with better variable naming.**

The refactored configuration section with descriptive boolean variable names (`shouldDisplayPrices`, `shouldPricesUseTaxes`, etc.) makes the code much more readable and self-documenting. Good use of the `should` prefix for boolean variables.

---

`122-135`: **Improved group attributes handling.**

The code now properly uses `DfTools::slugify` for consistent slug generation and has clearer variable names. The conditional structure is also cleaner and more maintainable.

---

`174-196`: **Well-refactored attribute headers processing.**

The code now more clearly processes attribute keys, and consistently uses `DfTools::slugify` for slug generation. The variable `altAttributeKeys` makes it clear that you're building an alternative set of keys.

---

`215-251`: **Much improved hook documentation.**

The expanded comment block provides clearer guidance on how to use the hook, including specific examples of implementation. This will make it easier for developers to extend the feed functionality.

---

`270-273`: **Optimized price calculation with conditional execution.**

Good optimization to only calculate `minPriceVariantByProductId` when prices should be displayed, avoiding unnecessary processing when prices aren't needed.

---

`274-286`: **Major output improvement using fputcsv.**

The refactored output section is a significant improvement:
1. Properly uses `fputcsv` for consistent CSV generation
2. Leverages the `DfProductBuild` class for cleaner product data handling
3. Applies specific CSV transformations through dedicated methods
4. Properly opens and closes the file handle

This approach is more maintainable and less error-prone than manual string concatenation.

</details>
<details>
<summary>src/Entity/DfProductBuild.php (9)</summary>

`47-57`: **Well-organized constructor initialization.**

The constructor now initializes all properties directly, making the code more maintainable by centralizing all initialization logic in one place. This is a good practice that improves readability and maintainability.

---

`76-76`: **Improved calculation of minimum variant prices.**

Using `DfTools::getMinVariantPrices` centralizes the calculation logic and makes the code more maintainable.

---

`85-85`: **Enhanced method signature with additional parameters.**

The updated signature for `buildProduct` provides more flexibility by allowing external code to pass additional parameters, which supports the new unified approach to attribute transformation.

---

`131-145`: **Well-implemented price adjustment logic for variants.**

This new block correctly handles minimum variant prices, ensuring that parent products display the lowest available variant price when appropriate. The multiple condition checks ensure the price adjustments are only applied when valid data is available.

---

`160-166`: **Improved handling of extra attribute headers.**

The new logic ensures that attribute headers are correctly populated in the product data, with proper checks to avoid overwriting existing values.

---

`172-177`: **Good implementation of extra headers processing.**

This block efficiently handles extra headers by skipping already populated fields and extracting values from the product data when available.

---

`183-251`: **Excellent new method for CSV-specific transformations.**

The new `applySpecificTransformationsForCsv` method is well-documented with a comprehensive docblock that clearly explains its purpose and behavior. The implementation handles various data transformations needed for CSV output, including:

1. Multiprice formatting
2. Categories and variants processing
3. Attributes formatting
4. Features conversion to appropriate format

This centralization of CSV-specific logic improves maintainability and separation of concerns.

---

`253-261`: **Good utility method for maintaining CSV field order.**

The `ensureCsvFieldsOrder` method ensures consistent field ordering in the CSV output, which is important for compatibility with external systems that expect fields in a specific order.

---

`56-56`: <details>
<summary>❓ Verification inconclusive</summary>

**Verify the logic of the OR statement in useTax assignment.**

The logic `$this->useTax = \Configuration::get('DF_GS_PRICES_USE_TAX') || DoofinderConstants::NO;` seems unusual. If `DF_GS_PRICES_USE_TAX` is falsy, this will still evaluate to `DoofinderConstants::NO`, which might not be the intended logic.

---

🏁 Script executed:

```shell
#!/bin/bash
# Look for how DoofinderConstants::NO is defined and other usages of this pattern
rg -A 5 "DoofinderConstants::NO" src/
```

Length of output: 464

---



</details>

**Clarify fallback logic for the useTax assignment**

It appears the current assignment

```php
$this->useTax = \Configuration::get('DF_GS_PRICES_USE_TAX') || DoofinderConstants::NO;
```

will always yield a boolean value. In PHP, the `||` operator evaluates its operands as booleans and returns a boolean result—not the actual configuration value—even if it’s truthy. If the intent is to preserve the actual configuration value when present (and only default when it’s null or undefined), you might consider using a null coalescing operator or a ternary expression. For example:

```php
$this->useTax = \Configuration::get('DF_GS_PRICES_USE_TAX') ?? DoofinderConstants::NO;
```

or

```php
$this->useTax = \Configuration::get('DF_GS_PRICES_USE_TAX') ? \Configuration::get('DF_GS_PRICES_USE_TAX') : DoofinderConstants::NO;
```

Please verify that this fallback behavior is intended and adjust the logic accordingly if a non-boolean configuration value should be preserved.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-02-27T11:34:26Z
user: login: sofia-doofinder
state: APPROVED
author_association: MEMBER
submitted_at: 2025-02-27T13:34:26Z
user: login: ogomezba
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-02T21:40:44Z
user: login: davidmolinacano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T07:31:25Z
user: login: davidmolinacano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T07:37:05Z
user: login: davidmolinacano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T07:39:06Z
user: login: davidmolinacano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T07:41:45Z
user: login: ogomezba
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T07:43:56Z
user: login: davidmolinacano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T07:46:39Z
user: login: davidmolinacano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T07:47:38Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>src/Entity/DfProductBuild.php (3)</summary><blockquote>

`160-164`: **Consider additional validation for extraAttributesHeader processing**

The code doesn't check if `$extraAttributesHeader` is an array before iterating through it. While this likely works in your current implementation since you're controlling the input, adding type checking would make the code more robust against potential errors.

```diff
-        foreach ($extraAttributesHeader as $extraAttributeHeader) {
+        foreach ((array)$extraAttributesHeader as $extraAttributeHeader) {
```

---

`171-176`: **Consider array type checking for extraHeaders**

Similar to the previous suggestion, adding a type check for `$extraHeaders` would improve code robustness.

```diff
-        foreach ($extraHeaders as $extraHeader) {
+        foreach ((array)$extraHeaders as $extraHeader) {
```

---

`215-230`: **Consider simplifying the features formatting logic**

The nested loops for formatting features could be simplified using more concise PHP array functions. This would make the code more readable while maintaining the same functionality.

```diff
-            $formattedAttributes = [];
-            foreach ($product['features'] as $key => $value) {
-                if (is_array($value)) {
-                    $keyValueToReturn = [];
-                    foreach ($value as $singleValue) {
-                        $keyValueToReturn[] = $key . '=' . $singleValue;
-                    }
-                    $formattedAttributes[] = implode('/', $keyValueToReturn);
-                } else {
-                    $formattedAttributes[] = $key . '=' . $value;
-                }
-            }
-            $product['attributes'] = implode('/', $formattedAttributes);
+            $formattedAttributes = array_map(
+                function($key, $value) {
+                    if (is_array($value)) {
+                        $keyValues = array_map(
+                            function($singleValue) use ($key) {
+                                return $key . '=' . $singleValue;
+                            },
+                            $value
+                        );
+                        return implode('/', $keyValues);
+                    }
+                    return $key . '=' . $value;
+                },
+                array_keys($product['features']),
+                $product['features']
+            );
+            $product['attributes'] = implode('/', $formattedAttributes);
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between cb751af8235ef6cf8a4e4b7d15f1e6e2614666f6 and b45697da9e08ccb89066336617e96ae12cee7beb.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `src/Entity/DfProductBuild.php` (6 hunks)

</details>

<details>
<summary>🔇 Additional comments (7)</summary><blockquote>

<details>
<summary>src/Entity/DfProductBuild.php (7)</summary>

`47-57`: **Improved constructor initialization pattern**

You've refactored the constructor to initialize all configuration values directly, which enhances code organization and readability. This is a good practice as it consolidates all property initialization in one place instead of spreading it across multiple methods.

---

`76-79`: **Clean refactoring of the variant price calculation**

Good refactoring to use the centralized `DfTools::getMinVariantPrices` method instead of a local implementation. This promotes code reuse and maintainability by keeping pricing logic in a single place.

---

`85-85`: **Improved method signature for enhanced flexibility**

The updated signature with optional parameters allows this method to be used both for update-on-save operations and feed generation, as mentioned in the previous comments. This unified approach simplifies maintenance and ensures consistent product transformations across different contexts.

---

`131-144`: **Enhanced price handling for variants**

This new logic appropriately adjusts product pricing when dealing with parent products by using the minimum price from their variants. This ensures that parent products display the most competitive price option, which is important for accurate representation in feeds and search results.

---

`202-235`: **Well-structured method with comprehensive documentation**

The `applySpecificTransformationsForCsv` method is well-documented and clearly separates CSV-specific transformations from general product building logic. This separation of concerns improves code organization and makes the codebase more maintainable.

---

`237-245`: **Well-implemented field ordering function**

The `ensureCsvFieldsOrder` method effectively ensures that product fields are ordered according to the specified headers, which is crucial for consistent CSV output. The static method is appropriately used here since it doesn't depend on instance properties.

---

`434-434`: **Consistent use of DfTools::slugify**

The codebase now consistently uses `DfTools::slugify` across all methods, which is a good refactoring that centralizes the slugification logic in one place. This makes the code more maintainable and ensures consistent slugification throughout the application.



Also applies to: 436-436, 466-466, 496-496

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-03T07:48:24Z
user: login: ogomezba
body: LGTM
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T08:09:46Z

Review comments:
diff_hunk: @@ -232,364 +236,60 @@ function arrayMergeByIdProduct($array1 = [], $array2 = [])
     } else {
         $feature_keys = $all_feature_keys;
     }
-    $header[] = 'attributes';
+    $additionalAttributesHeaders[] = 'attributes';
 }
 
+$header = array_merge($header, $additionalAttributesHeaders);
+
 /**
  * @author camlafit <https://github.com/camlafit>
- * Extend doofinder feed
+ * Extend Doofinder feed
path: feeds/product.php
user: login: sofia-doofinder
body: no entiendo este comentario... tampoco me ayuda a entender que es lo que hace el `hook::exec` que viene después... podrías darle una vuelta please?
created_at: 2025-02-24T17:15:13Z
author_association: MEMBER
diff_hunk: @@ -232,364 +236,60 @@ function arrayMergeByIdProduct($array1 = [], $array2 = [])
     } else {
         $feature_keys = $all_feature_keys;
     }
-    $header[] = 'attributes';
+    $additionalAttributesHeaders[] = 'attributes';
 }
 
+$header = array_merge($header, $additionalAttributesHeaders);
+
 /**
  * @author camlafit <https://github.com/camlafit>
- * Extend doofinder feed
+ * Extend Doofinder feed
path: feeds/product.php
user: login: davidmolinacano
body: Ese comentario ya estaba, yo solo he cambiado la `d` de Doofinder de minúscula a mayúscula
created_at: 2025-02-25T07:36:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -232,364 +236,60 @@ function arrayMergeByIdProduct($array1 = [], $array2 = [])
     } else {
         $feature_keys = $all_feature_keys;
     }
-    $header[] = 'attributes';
+    $additionalAttributesHeaders[] = 'attributes';
 }
 
+$header = array_merge($header, $additionalAttributesHeaders);
+
 /**
  * @author camlafit <https://github.com/camlafit>
- * Extend doofinder feed
+ * Extend Doofinder feed
path: feeds/product.php
user: login: sofia-doofinder
body: sí, lo se, pero si dejamos el bosque un poco mas limpio mejor, no? :) 
created_at: 2025-02-25T07:40:23Z
author_association: MEMBER
diff_hunk: @@ -232,364 +236,60 @@ function arrayMergeByIdProduct($array1 = [], $array2 = [])
     } else {
         $feature_keys = $all_feature_keys;
     }
-    $header[] = 'attributes';
+    $additionalAttributesHeaders[] = 'attributes';
 }
 
+$header = array_merge($header, $additionalAttributesHeaders);
+
 /**
  * @author camlafit <https://github.com/camlafit>
- * Extend doofinder feed
+ * Extend Doofinder feed
path: feeds/product.php
user: login: davidmolinacano
body: Estoy añadiendo un comentario con una explicación mucho más detallada :raised_hands: 
created_at: 2025-02-25T07:41:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -104,57 +81,34 @@ function arrayMergeByIdProduct($array1 = [], $array2 = [])
     exit('NOT PROPERLY CONFIGURED');
 }
 
-// CONFIG
+// GENERAL PURPOSE VARIABLES + CONTEXT
 $lang = DfTools::getLanguageFromRequest();
 $context->language = $lang;
-$country = Configuration::get('PS_COUNTRY_DEFAULT');
+$country = (int) DfTools::cfg($shop->id, 'PS_COUNTRY_DEFAULT');
 $context->country = new Country($country);
 $currency = DfTools::getCurrencyForLanguageFromRequest($lang);
-$multiprice_enabled = Configuration::get('DF_MULTIPRICE_ENABLED');
 $currencies = Currency::getCurrenciesByIdShop($context->shop->id);
 
-$cfg_short_description = (DfTools::cfg(
-    $shop->id,
-    'DF_GS_DESCRIPTION_TYPE',
-    DoofinderConstants::GS_SHORT_DESCRIPTION
-) == DoofinderConstants::GS_SHORT_DESCRIPTION);
-
-$cfg_display_prices = DfTools::getBooleanFromRequest(
+/* ---------- START CONFIG ---------- */
+$shouldDisplayPrices = DfTools::getBooleanFromRequest(
path: feeds/product.php
user: login: sofia-doofinder
body: David una duda, esto es porque hay casos en los que se puede indexar sin precio?
created_at: 2025-02-25T11:42:54Z
author_association: MEMBER
diff_hunk: @@ -104,57 +81,34 @@ function arrayMergeByIdProduct($array1 = [], $array2 = [])
     exit('NOT PROPERLY CONFIGURED');
 }
 
-// CONFIG
+// GENERAL PURPOSE VARIABLES + CONTEXT
 $lang = DfTools::getLanguageFromRequest();
 $context->language = $lang;
-$country = Configuration::get('PS_COUNTRY_DEFAULT');
+$country = (int) DfTools::cfg($shop->id, 'PS_COUNTRY_DEFAULT');
 $context->country = new Country($country);
 $currency = DfTools::getCurrencyForLanguageFromRequest($lang);
-$multiprice_enabled = Configuration::get('DF_MULTIPRICE_ENABLED');
 $currencies = Currency::getCurrenciesByIdShop($context->shop->id);
 
-$cfg_short_description = (DfTools::cfg(
-    $shop->id,
-    'DF_GS_DESCRIPTION_TYPE',
-    DoofinderConstants::GS_SHORT_DESCRIPTION
-) == DoofinderConstants::GS_SHORT_DESCRIPTION);
-
-$cfg_display_prices = DfTools::getBooleanFromRequest(
+/* ---------- START CONFIG ---------- */
+$shouldDisplayPrices = DfTools::getBooleanFromRequest(
     'prices',
     (bool) DfTools::cfg($shop->id, 'DF_GS_DISPLAY_PRICES', DoofinderConstants::YES)
 );
-$cfg_prices_w_taxes = DfTools::getBooleanFromRequest(
+$shouldPricesUseTaxes = DfTools::getBooleanFromRequest(
     'taxes',
     (bool) DfTools::cfg($shop->id, 'DF_GS_PRICES_USE_TAX', DoofinderConstants::YES)
 );
-$cfg_image_size = DfTools::cfg($shop->id, 'DF_GS_IMAGE_SIZE');
-$cfg_mod_rewrite = DfTools::cfg($shop->id, 'PS_REWRITING_SETTINGS', DoofinderConstants::YES);
-$cfg_product_variations = (int) DfTools::cfg($shop->id, 'DF_SHOW_PRODUCT_VARIATIONS');
-$cfg_product_features = DfTools::cfg($shop->id, 'DF_SHOW_PRODUCT_FEATURES');
-$cfg_debug = DfTools::cfg($shop->id, 'DF_DEBUG');
-$cfg_features_shown = explode(',', DfTools::cfg($shop->id, 'DF_FEATURES_SHOWN'));
-
-$cfg_group_attributes_shown = explode(',', DfTools::cfg($shop->id, 'DF_GROUP_ATTRIBUTES_SHOWN'));
-
-$limit_group_attributes = false;
-if (
-    is_array($cfg_group_attributes_shown)
-    && count($cfg_group_attributes_shown) > 0
-    && $cfg_group_attributes_shown[0] !== ''
-) {
-    $group_attributes = AttributeGroup::getAttributesGroups($lang->id);
-    $group_attributes_slug = [];
-    foreach ($group_attributes as $g) {
-        if (in_array($g['id_attribute_group'], $cfg_group_attributes_shown)) {
-            $group_attributes_slug[] = slugify($g['name']);
-        }
-    }
-    $limit_group_attributes = true;
-}
-
+$isMultipriceEnabled = \Configuration::get('DF_MULTIPRICE_ENABLED');
+$shouldShowProductVariations = (int) DfTools::cfg($shop->id, 'DF_SHOW_PRODUCT_VARIATIONS');
+$shouldShowProductFeatures = DfTools::cfg($shop->id, 'DF_SHOW_PRODUCT_FEATURES');
path: feeds/product.php
user: login: sofia-doofinder
body: también por desconocimiento, que diferencia hay entre `show_product_features` y `features_shown`? a simple vista parece que son lo mismo no?
created_at: 2025-02-25T11:43:59Z
author_association: MEMBER
diff_hunk: @@ -193,403 +162,119 @@ function arrayMergeByIdProduct($array1 = [], $array2 = [])
 
 $header[] = 'stock_quantity';
 
-if ($cfg_display_prices) {
+if ($shouldDisplayPrices) {
     $header[] = 'price';
     $header[] = 'sale_price';
-    $header[] = 'df_multiprice';
+
+    if ($isMultipriceEnabled) {
+        $header[] = 'df_multiprice';
+    }
 }
 
-if ($cfg_product_variations == 1) {
+$additionalAttributesHeaders = [];
+
+if (1 === $shouldShowProductVariations) {
     $header[] = 'variation_reference';
     $header[] = 'variation_supplier_reference';
     $header[] = 'variation_mpn';
     $header[] = 'variation_ean13';
     $header[] = 'variation_upc';
     $header[] = 'df_group_leader';
     $header[] = 'df_variants_information';
-    $attribute_keys = DfTools::getAttributeKeysForShopAndLang($shop->id, $lang->id);
-    $alt_attribute_keys = [];
-    foreach ($attribute_keys as $key) {
-        $header_value = slugify($key);
-        if ($limit_group_attributes && !in_array($header_value, $group_attributes_slug)) {
+    $attributeKeys = DfTools::getAttributeKeysForShopAndLang($shop->id, $lang->id);
+    $altAttributeKeys = [];
+
+    foreach ($attributeKeys as $key) {
+        $headerValue = DfTools::slugify($key);
+        if ($shouldLimitGroupAttributes && !in_array($headerValue, $groupAttributesSlug)) {
             continue;
         }
-        $alt_attribute_keys[] = $key;
-        $header[] = $header_value;
+        $altAttributeKeys[] = $key;
+        $additionalAttributesHeaders[] = $headerValue;
     }
-    $attribute_keys = $alt_attribute_keys;
+    $attributeKeys = $altAttributeKeys;
 }
 
-if ($cfg_product_features) {
-    $all_feature_keys = DfTools::getFeatureKeysForShopAndLang($shop->id, $lang->id);
+if ($shouldShowProductFeatures) {
+    $allFeatureKeys = DfTools::getFeatureKeysForShopAndLang($shop->id, $lang->id);
 
     if (
-        is_array($cfg_features_shown)
-        && count($cfg_features_shown) > 0
-        && $cfg_features_shown[0] !== ''
+        is_array($shouldFeaturesBeShown)
+        && count($shouldFeaturesBeShown) > 0
+        && $shouldFeaturesBeShown[0] !== ''
     ) {
-        $feature_keys = DfTools::getSelectedFeatures($all_feature_keys, $cfg_features_shown);
+        $featurekeys = DfTools::getSelectedFeatures($allFeatureKeys, $shouldFeaturesBeShown);
     } else {
-        $feature_keys = $all_feature_keys;
+        $featurekeys = $allFeatureKeys;
     }
-    $header[] = 'attributes';
+    $additionalAttributesHeaders[] = 'attributes';
 }
 
+$header = array_merge($header, $additionalAttributesHeaders);
+
 /**
  * @author camlafit <https://github.com/camlafit>
- * Extend doofinder feed
+ * Allows users to extend Doofinder feed by adding extra headers and extra rows. Example:
path: feeds/product.php
user: login: sofia-doofinder
body: muchas gracias por la aclaración David! pero me surge otra duda... como se le puede pedir esos parámetros extra? donde se configura? en que momento se pueden pedir esos campos extra? porque aquí se llama siempre a `hookActionDoofinderExtendFeed` con un array vacío no?
created_at: 2025-02-25T11:46:54Z
author_association: MEMBER
diff_hunk: @@ -193,403 +162,119 @@ function arrayMergeByIdProduct($array1 = [], $array2 = [])
 
 $header[] = 'stock_quantity';
 
-if ($cfg_display_prices) {
+if ($shouldDisplayPrices) {
     $header[] = 'price';
     $header[] = 'sale_price';
-    $header[] = 'df_multiprice';
+
+    if ($isMultipriceEnabled) {
+        $header[] = 'df_multiprice';
+    }
 }
 
-if ($cfg_product_variations == 1) {
+$additionalAttributesHeaders = [];
+
+if (1 === $shouldShowProductVariations) {
     $header[] = 'variation_reference';
     $header[] = 'variation_supplier_reference';
     $header[] = 'variation_mpn';
     $header[] = 'variation_ean13';
     $header[] = 'variation_upc';
     $header[] = 'df_group_leader';
     $header[] = 'df_variants_information';
-    $attribute_keys = DfTools::getAttributeKeysForShopAndLang($shop->id, $lang->id);
-    $alt_attribute_keys = [];
-    foreach ($attribute_keys as $key) {
-        $header_value = slugify($key);
-        if ($limit_group_attributes && !in_array($header_value, $group_attributes_slug)) {
+    $attributeKeys = DfTools::getAttributeKeysForShopAndLang($shop->id, $lang->id);
+    $altAttributeKeys = [];
+
+    foreach ($attributeKeys as $key) {
+        $headerValue = DfTools::slugify($key);
+        if ($shouldLimitGroupAttributes && !in_array($headerValue, $groupAttributesSlug)) {
             continue;
         }
-        $alt_attribute_keys[] = $key;
-        $header[] = $header_value;
+        $altAttributeKeys[] = $key;
+        $additionalAttributesHeaders[] = $headerValue;
     }
-    $attribute_keys = $alt_attribute_keys;
+    $attributeKeys = $altAttributeKeys;
 }
 
-if ($cfg_product_features) {
-    $all_feature_keys = DfTools::getFeatureKeysForShopAndLang($shop->id, $lang->id);
+if ($shouldShowProductFeatures) {
+    $allFeatureKeys = DfTools::getFeatureKeysForShopAndLang($shop->id, $lang->id);
 
     if (
-        is_array($cfg_features_shown)
-        && count($cfg_features_shown) > 0
-        && $cfg_features_shown[0] !== ''
+        is_array($shouldFeaturesBeShown)
+        && count($shouldFeaturesBeShown) > 0
+        && $shouldFeaturesBeShown[0] !== ''
     ) {
-        $feature_keys = DfTools::getSelectedFeatures($all_feature_keys, $cfg_features_shown);
+        $featurekeys = DfTools::getSelectedFeatures($allFeatureKeys, $shouldFeaturesBeShown);
     } else {
-        $feature_keys = $all_feature_keys;
+        $featurekeys = $allFeatureKeys;
     }
-    $header[] = 'attributes';
+    $additionalAttributesHeaders[] = 'attributes';
 }
 
+$header = array_merge($header, $additionalAttributesHeaders);
+
 /**
  * @author camlafit <https://github.com/camlafit>
- * Extend doofinder feed
+ * Allows users to extend Doofinder feed by adding extra headers and extra rows. Example:
path: feeds/product.php
user: login: davidmolinacano
body: En verdad tiene truco ya que el array se modifica por referencia y no por valor (porque tiene el &) que es justo una de las críticas que tiene el CodeRabbit, que al pasarlo por referencia no queda tan claro lo que hace. Quizás lo pueda modificar para que lo pase por valor.
created_at: 2025-02-25T13:35:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -104,57 +81,34 @@ function arrayMergeByIdProduct($array1 = [], $array2 = [])
     exit('NOT PROPERLY CONFIGURED');
 }
 
-// CONFIG
+// GENERAL PURPOSE VARIABLES + CONTEXT
 $lang = DfTools::getLanguageFromRequest();
 $context->language = $lang;
-$country = Configuration::get('PS_COUNTRY_DEFAULT');
+$country = (int) DfTools::cfg($shop->id, 'PS_COUNTRY_DEFAULT');
 $context->country = new Country($country);
 $currency = DfTools::getCurrencyForLanguageFromRequest($lang);
-$multiprice_enabled = Configuration::get('DF_MULTIPRICE_ENABLED');
 $currencies = Currency::getCurrenciesByIdShop($context->shop->id);
 
-$cfg_short_description = (DfTools::cfg(
-    $shop->id,
-    'DF_GS_DESCRIPTION_TYPE',
-    DoofinderConstants::GS_SHORT_DESCRIPTION
-) == DoofinderConstants::GS_SHORT_DESCRIPTION);
-
-$cfg_display_prices = DfTools::getBooleanFromRequest(
+/* ---------- START CONFIG ---------- */
+$shouldDisplayPrices = DfTools::getBooleanFromRequest(
path: feeds/product.php
user: login: davidmolinacano
body: Sí, es que tenemos una config de precios:

![image](https://github.com/user-attachments/assets/9666f04c-87bb-4292-b8c0-60d00c862942)

created_at: 2025-02-25T13:36:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -104,57 +81,34 @@ function arrayMergeByIdProduct($array1 = [], $array2 = [])
     exit('NOT PROPERLY CONFIGURED');
 }
 
-// CONFIG
+// GENERAL PURPOSE VARIABLES + CONTEXT
 $lang = DfTools::getLanguageFromRequest();
 $context->language = $lang;
-$country = Configuration::get('PS_COUNTRY_DEFAULT');
+$country = (int) DfTools::cfg($shop->id, 'PS_COUNTRY_DEFAULT');
 $context->country = new Country($country);
 $currency = DfTools::getCurrencyForLanguageFromRequest($lang);
-$multiprice_enabled = Configuration::get('DF_MULTIPRICE_ENABLED');
 $currencies = Currency::getCurrenciesByIdShop($context->shop->id);
 
-$cfg_short_description = (DfTools::cfg(
-    $shop->id,
-    'DF_GS_DESCRIPTION_TYPE',
-    DoofinderConstants::GS_SHORT_DESCRIPTION
-) == DoofinderConstants::GS_SHORT_DESCRIPTION);
-
-$cfg_display_prices = DfTools::getBooleanFromRequest(
+/* ---------- START CONFIG ---------- */
+$shouldDisplayPrices = DfTools::getBooleanFromRequest(
     'prices',
     (bool) DfTools::cfg($shop->id, 'DF_GS_DISPLAY_PRICES', DoofinderConstants::YES)
 );
-$cfg_prices_w_taxes = DfTools::getBooleanFromRequest(
+$shouldPricesUseTaxes = DfTools::getBooleanFromRequest(
     'taxes',
     (bool) DfTools::cfg($shop->id, 'DF_GS_PRICES_USE_TAX', DoofinderConstants::YES)
 );
-$cfg_image_size = DfTools::cfg($shop->id, 'DF_GS_IMAGE_SIZE');
-$cfg_mod_rewrite = DfTools::cfg($shop->id, 'PS_REWRITING_SETTINGS', DoofinderConstants::YES);
-$cfg_product_variations = (int) DfTools::cfg($shop->id, 'DF_SHOW_PRODUCT_VARIATIONS');
-$cfg_product_features = DfTools::cfg($shop->id, 'DF_SHOW_PRODUCT_FEATURES');
-$cfg_debug = DfTools::cfg($shop->id, 'DF_DEBUG');
-$cfg_features_shown = explode(',', DfTools::cfg($shop->id, 'DF_FEATURES_SHOWN'));
-
-$cfg_group_attributes_shown = explode(',', DfTools::cfg($shop->id, 'DF_GROUP_ATTRIBUTES_SHOWN'));
-
-$limit_group_attributes = false;
-if (
-    is_array($cfg_group_attributes_shown)
-    && count($cfg_group_attributes_shown) > 0
-    && $cfg_group_attributes_shown[0] !== ''
-) {
-    $group_attributes = AttributeGroup::getAttributesGroups($lang->id);
-    $group_attributes_slug = [];
-    foreach ($group_attributes as $g) {
-        if (in_array($g['id_attribute_group'], $cfg_group_attributes_shown)) {
-            $group_attributes_slug[] = slugify($g['name']);
-        }
-    }
-    $limit_group_attributes = true;
-}
-
+$isMultipriceEnabled = \Configuration::get('DF_MULTIPRICE_ENABLED');
+$shouldShowProductVariations = (int) DfTools::cfg($shop->id, 'DF_SHOW_PRODUCT_VARIATIONS');
+$shouldShowProductFeatures = DfTools::cfg($shop->id, 'DF_SHOW_PRODUCT_FEATURES');
path: feeds/product.php
user: login: davidmolinacano
body: Mmmmm voy a investigarlo, que solo he adapatado el código que había en este caso jejejeje
created_at: 2025-02-25T13:48:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -193,403 +162,119 @@ function arrayMergeByIdProduct($array1 = [], $array2 = [])
 
 $header[] = 'stock_quantity';
 
-if ($cfg_display_prices) {
+if ($shouldDisplayPrices) {
     $header[] = 'price';
     $header[] = 'sale_price';
-    $header[] = 'df_multiprice';
+
+    if ($isMultipriceEnabled) {
+        $header[] = 'df_multiprice';
+    }
 }
 
-if ($cfg_product_variations == 1) {
+$additionalAttributesHeaders = [];
+
+if (1 === $shouldShowProductVariations) {
     $header[] = 'variation_reference';
     $header[] = 'variation_supplier_reference';
     $header[] = 'variation_mpn';
     $header[] = 'variation_ean13';
     $header[] = 'variation_upc';
     $header[] = 'df_group_leader';
     $header[] = 'df_variants_information';
-    $attribute_keys = DfTools::getAttributeKeysForShopAndLang($shop->id, $lang->id);
-    $alt_attribute_keys = [];
-    foreach ($attribute_keys as $key) {
-        $header_value = slugify($key);
-        if ($limit_group_attributes && !in_array($header_value, $group_attributes_slug)) {
+    $attributeKeys = DfTools::getAttributeKeysForShopAndLang($shop->id, $lang->id);
+    $altAttributeKeys = [];
+
+    foreach ($attributeKeys as $key) {
+        $headerValue = DfTools::slugify($key);
+        if ($shouldLimitGroupAttributes && !in_array($headerValue, $groupAttributesSlug)) {
             continue;
         }
-        $alt_attribute_keys[] = $key;
-        $header[] = $header_value;
+        $altAttributeKeys[] = $key;
+        $additionalAttributesHeaders[] = $headerValue;
     }
-    $attribute_keys = $alt_attribute_keys;
+    $attributeKeys = $altAttributeKeys;
 }
 
-if ($cfg_product_features) {
-    $all_feature_keys = DfTools::getFeatureKeysForShopAndLang($shop->id, $lang->id);
+if ($shouldShowProductFeatures) {
+    $allFeatureKeys = DfTools::getFeatureKeysForShopAndLang($shop->id, $lang->id);
 
     if (
-        is_array($cfg_features_shown)
-        && count($cfg_features_shown) > 0
-        && $cfg_features_shown[0] !== ''
+        is_array($shouldFeaturesBeShown)
+        && count($shouldFeaturesBeShown) > 0
+        && $shouldFeaturesBeShown[0] !== ''
     ) {
-        $feature_keys = DfTools::getSelectedFeatures($all_feature_keys, $cfg_features_shown);
+        $featurekeys = DfTools::getSelectedFeatures($allFeatureKeys, $shouldFeaturesBeShown);
     } else {
-        $feature_keys = $all_feature_keys;
+        $featurekeys = $allFeatureKeys;
     }
-    $header[] = 'attributes';
+    $additionalAttributesHeaders[] = 'attributes';
 }
 
+$header = array_merge($header, $additionalAttributesHeaders);
+
 /**
  * @author camlafit <https://github.com/camlafit>
- * Extend doofinder feed
+ * Allows users to extend Doofinder feed by adding extra headers and extra rows. Example:
path: feeds/product.php
user: login: sofia-doofinder
body: pero me vengo a referir, el usuario puede añadirlo desde algún sitio? o es un código que tenemos de algo que se podría hacer pero que no hay forma de que el usuario añada cabeceras extras? porque si es código que está ahí por si acaso quizás se puede quitar. Pero si hay forma de que lo utilice desde alguna sección lo dejamos
created_at: 2025-02-25T14:09:36Z
author_association: MEMBER
diff_hunk: @@ -193,403 +162,119 @@ function arrayMergeByIdProduct($array1 = [], $array2 = [])
 
 $header[] = 'stock_quantity';
 
-if ($cfg_display_prices) {
+if ($shouldDisplayPrices) {
     $header[] = 'price';
     $header[] = 'sale_price';
-    $header[] = 'df_multiprice';
+
+    if ($isMultipriceEnabled) {
+        $header[] = 'df_multiprice';
+    }
 }
 
-if ($cfg_product_variations == 1) {
+$additionalAttributesHeaders = [];
+
+if (1 === $shouldShowProductVariations) {
     $header[] = 'variation_reference';
     $header[] = 'variation_supplier_reference';
     $header[] = 'variation_mpn';
     $header[] = 'variation_ean13';
     $header[] = 'variation_upc';
     $header[] = 'df_group_leader';
     $header[] = 'df_variants_information';
-    $attribute_keys = DfTools::getAttributeKeysForShopAndLang($shop->id, $lang->id);
-    $alt_attribute_keys = [];
-    foreach ($attribute_keys as $key) {
-        $header_value = slugify($key);
-        if ($limit_group_attributes && !in_array($header_value, $group_attributes_slug)) {
+    $attributeKeys = DfTools::getAttributeKeysForShopAndLang($shop->id, $lang->id);
+    $altAttributeKeys = [];
+
+    foreach ($attributeKeys as $key) {
+        $headerValue = DfTools::slugify($key);
+        if ($shouldLimitGroupAttributes && !in_array($headerValue, $groupAttributesSlug)) {
             continue;
         }
-        $alt_attribute_keys[] = $key;
-        $header[] = $header_value;
+        $altAttributeKeys[] = $key;
+        $additionalAttributesHeaders[] = $headerValue;
     }
-    $attribute_keys = $alt_attribute_keys;
+    $attributeKeys = $altAttributeKeys;
 }
 
-if ($cfg_product_features) {
-    $all_feature_keys = DfTools::getFeatureKeysForShopAndLang($shop->id, $lang->id);
+if ($shouldShowProductFeatures) {
+    $allFeatureKeys = DfTools::getFeatureKeysForShopAndLang($shop->id, $lang->id);
 
     if (
-        is_array($cfg_features_shown)
-        && count($cfg_features_shown) > 0
-        && $cfg_features_shown[0] !== ''
+        is_array($shouldFeaturesBeShown)
+        && count($shouldFeaturesBeShown) > 0
+        && $shouldFeaturesBeShown[0] !== ''
     ) {
-        $feature_keys = DfTools::getSelectedFeatures($all_feature_keys, $cfg_features_shown);
+        $featurekeys = DfTools::getSelectedFeatures($allFeatureKeys, $shouldFeaturesBeShown);
     } else {
-        $feature_keys = $all_feature_keys;
+        $featurekeys = $allFeatureKeys;
     }
-    $header[] = 'attributes';
+    $additionalAttributesHeaders[] = 'attributes';
 }
 
+$header = array_merge($header, $additionalAttributesHeaders);
+
 /**
  * @author camlafit <https://github.com/camlafit>
- * Extend doofinder feed
+ * Allows users to extend Doofinder feed by adding extra headers and extra rows. Example:
path: feeds/product.php
user: login: davidmolinacano
body: Esto estaría para que el usuario extienda o desde su tema o desde algún módulo custom que hayan creado ellos, pero si el cliente no mete el hook a mano de algún modo esto no hace absolutamente nada. Lo que nos sabemos es si algún cliente está haciendo uso de esto desde hace tiempo :thinking: 
created_at: 2025-02-25T14:11:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -104,57 +81,34 @@ function arrayMergeByIdProduct($array1 = [], $array2 = [])
     exit('NOT PROPERLY CONFIGURED');
 }
 
-// CONFIG
+// GENERAL PURPOSE VARIABLES + CONTEXT
 $lang = DfTools::getLanguageFromRequest();
 $context->language = $lang;
-$country = Configuration::get('PS_COUNTRY_DEFAULT');
+$country = (int) DfTools::cfg($shop->id, 'PS_COUNTRY_DEFAULT');
 $context->country = new Country($country);
 $currency = DfTools::getCurrencyForLanguageFromRequest($lang);
-$multiprice_enabled = Configuration::get('DF_MULTIPRICE_ENABLED');
 $currencies = Currency::getCurrenciesByIdShop($context->shop->id);
 
-$cfg_short_description = (DfTools::cfg(
-    $shop->id,
-    'DF_GS_DESCRIPTION_TYPE',
-    DoofinderConstants::GS_SHORT_DESCRIPTION
-) == DoofinderConstants::GS_SHORT_DESCRIPTION);
-
-$cfg_display_prices = DfTools::getBooleanFromRequest(
+/* ---------- START CONFIG ---------- */
+$shouldDisplayPrices = DfTools::getBooleanFromRequest(
path: feeds/product.php
user: login: sofia-doofinder
body: @IgnacioPursalsZ conocías este botón? yo es la primera vez que lo veo, te menciono para darte visibilidad porque no se si tiene sentido que en el panel de prestashop tengamos esta configuración.. En la configuración de la capa se puede modificar esto?
created_at: 2025-02-25T14:11:35Z
author_association: MEMBER
diff_hunk: @@ -104,57 +81,34 @@ function arrayMergeByIdProduct($array1 = [], $array2 = [])
     exit('NOT PROPERLY CONFIGURED');
 }
 
-// CONFIG
+// GENERAL PURPOSE VARIABLES + CONTEXT
 $lang = DfTools::getLanguageFromRequest();
 $context->language = $lang;
-$country = Configuration::get('PS_COUNTRY_DEFAULT');
+$country = (int) DfTools::cfg($shop->id, 'PS_COUNTRY_DEFAULT');
 $context->country = new Country($country);
 $currency = DfTools::getCurrencyForLanguageFromRequest($lang);
-$multiprice_enabled = Configuration::get('DF_MULTIPRICE_ENABLED');
 $currencies = Currency::getCurrenciesByIdShop($context->shop->id);
 
-$cfg_short_description = (DfTools::cfg(
-    $shop->id,
-    'DF_GS_DESCRIPTION_TYPE',
-    DoofinderConstants::GS_SHORT_DESCRIPTION
-) == DoofinderConstants::GS_SHORT_DESCRIPTION);
-
-$cfg_display_prices = DfTools::getBooleanFromRequest(
+/* ---------- START CONFIG ---------- */
+$shouldDisplayPrices = DfTools::getBooleanFromRequest(
     'prices',
     (bool) DfTools::cfg($shop->id, 'DF_GS_DISPLAY_PRICES', DoofinderConstants::YES)
 );
-$cfg_prices_w_taxes = DfTools::getBooleanFromRequest(
+$shouldPricesUseTaxes = DfTools::getBooleanFromRequest(
     'taxes',
     (bool) DfTools::cfg($shop->id, 'DF_GS_PRICES_USE_TAX', DoofinderConstants::YES)
 );
-$cfg_image_size = DfTools::cfg($shop->id, 'DF_GS_IMAGE_SIZE');
-$cfg_mod_rewrite = DfTools::cfg($shop->id, 'PS_REWRITING_SETTINGS', DoofinderConstants::YES);
-$cfg_product_variations = (int) DfTools::cfg($shop->id, 'DF_SHOW_PRODUCT_VARIATIONS');
-$cfg_product_features = DfTools::cfg($shop->id, 'DF_SHOW_PRODUCT_FEATURES');
-$cfg_debug = DfTools::cfg($shop->id, 'DF_DEBUG');
-$cfg_features_shown = explode(',', DfTools::cfg($shop->id, 'DF_FEATURES_SHOWN'));
-
-$cfg_group_attributes_shown = explode(',', DfTools::cfg($shop->id, 'DF_GROUP_ATTRIBUTES_SHOWN'));
-
-$limit_group_attributes = false;
-if (
-    is_array($cfg_group_attributes_shown)
-    && count($cfg_group_attributes_shown) > 0
-    && $cfg_group_attributes_shown[0] !== ''
-) {
-    $group_attributes = AttributeGroup::getAttributesGroups($lang->id);
-    $group_attributes_slug = [];
-    foreach ($group_attributes as $g) {
-        if (in_array($g['id_attribute_group'], $cfg_group_attributes_shown)) {
-            $group_attributes_slug[] = slugify($g['name']);
-        }
-    }
-    $limit_group_attributes = true;
-}
-
+$isMultipriceEnabled = \Configuration::get('DF_MULTIPRICE_ENABLED');
+$shouldShowProductVariations = (int) DfTools::cfg($shop->id, 'DF_SHOW_PRODUCT_VARIATIONS');
+$shouldShowProductFeatures = DfTools::cfg($shop->id, 'DF_SHOW_PRODUCT_FEATURES');
path: feeds/product.php
user: login: davidmolinacano
body: Bufff, vaya nombrecitos tienen las constantes... :man_facepalming:  Ahora ya no se podrían cambiar, pero al menos sí puedo cambiar las variables para que sean algo más descriptivas 
created_at: 2025-02-25T14:41:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -104,57 +81,34 @@ function arrayMergeByIdProduct($array1 = [], $array2 = [])
     exit('NOT PROPERLY CONFIGURED');
 }
 
-// CONFIG
+// GENERAL PURPOSE VARIABLES + CONTEXT
 $lang = DfTools::getLanguageFromRequest();
 $context->language = $lang;
-$country = Configuration::get('PS_COUNTRY_DEFAULT');
+$country = (int) DfTools::cfg($shop->id, 'PS_COUNTRY_DEFAULT');
 $context->country = new Country($country);
 $currency = DfTools::getCurrencyForLanguageFromRequest($lang);
-$multiprice_enabled = Configuration::get('DF_MULTIPRICE_ENABLED');
 $currencies = Currency::getCurrenciesByIdShop($context->shop->id);
 
-$cfg_short_description = (DfTools::cfg(
-    $shop->id,
-    'DF_GS_DESCRIPTION_TYPE',
-    DoofinderConstants::GS_SHORT_DESCRIPTION
-) == DoofinderConstants::GS_SHORT_DESCRIPTION);
-
-$cfg_display_prices = DfTools::getBooleanFromRequest(
+/* ---------- START CONFIG ---------- */
+$shouldDisplayPrices = DfTools::getBooleanFromRequest(
     'prices',
     (bool) DfTools::cfg($shop->id, 'DF_GS_DISPLAY_PRICES', DoofinderConstants::YES)
 );
-$cfg_prices_w_taxes = DfTools::getBooleanFromRequest(
+$shouldPricesUseTaxes = DfTools::getBooleanFromRequest(
     'taxes',
     (bool) DfTools::cfg($shop->id, 'DF_GS_PRICES_USE_TAX', DoofinderConstants::YES)
 );
-$cfg_image_size = DfTools::cfg($shop->id, 'DF_GS_IMAGE_SIZE');
-$cfg_mod_rewrite = DfTools::cfg($shop->id, 'PS_REWRITING_SETTINGS', DoofinderConstants::YES);
-$cfg_product_variations = (int) DfTools::cfg($shop->id, 'DF_SHOW_PRODUCT_VARIATIONS');
-$cfg_product_features = DfTools::cfg($shop->id, 'DF_SHOW_PRODUCT_FEATURES');
-$cfg_debug = DfTools::cfg($shop->id, 'DF_DEBUG');
-$cfg_features_shown = explode(',', DfTools::cfg($shop->id, 'DF_FEATURES_SHOWN'));
-
-$cfg_group_attributes_shown = explode(',', DfTools::cfg($shop->id, 'DF_GROUP_ATTRIBUTES_SHOWN'));
-
-$limit_group_attributes = false;
-if (
-    is_array($cfg_group_attributes_shown)
-    && count($cfg_group_attributes_shown) > 0
-    && $cfg_group_attributes_shown[0] !== ''
-) {
-    $group_attributes = AttributeGroup::getAttributesGroups($lang->id);
-    $group_attributes_slug = [];
-    foreach ($group_attributes as $g) {
-        if (in_array($g['id_attribute_group'], $cfg_group_attributes_shown)) {
-            $group_attributes_slug[] = slugify($g['name']);
-        }
-    }
-    $limit_group_attributes = true;
-}
-
+$isMultipriceEnabled = \Configuration::get('DF_MULTIPRICE_ENABLED');
+$shouldShowProductVariations = (int) DfTools::cfg($shop->id, 'DF_SHOW_PRODUCT_VARIATIONS');
+$shouldShowProductFeatures = DfTools::cfg($shop->id, 'DF_SHOW_PRODUCT_FEATURES');
path: feeds/product.php
user: login: davidmolinacano
body: Mejorado en: https://github.com/doofinder/doofinder-prestashop/pull/291/commits/048e57c2aa54c763bd79fac3ef55a28e96ce716f (reconozco que hasta yo mismo me había liado con los nombres de antes)
created_at: 2025-02-25T14:59:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -104,57 +81,34 @@ function arrayMergeByIdProduct($array1 = [], $array2 = [])
     exit('NOT PROPERLY CONFIGURED');
 }
 
-// CONFIG
+// GENERAL PURPOSE VARIABLES + CONTEXT
 $lang = DfTools::getLanguageFromRequest();
 $context->language = $lang;
-$country = Configuration::get('PS_COUNTRY_DEFAULT');
+$country = (int) DfTools::cfg($shop->id, 'PS_COUNTRY_DEFAULT');
 $context->country = new Country($country);
 $currency = DfTools::getCurrencyForLanguageFromRequest($lang);
-$multiprice_enabled = Configuration::get('DF_MULTIPRICE_ENABLED');
 $currencies = Currency::getCurrenciesByIdShop($context->shop->id);
 
-$cfg_short_description = (DfTools::cfg(
-    $shop->id,
-    'DF_GS_DESCRIPTION_TYPE',
-    DoofinderConstants::GS_SHORT_DESCRIPTION
-) == DoofinderConstants::GS_SHORT_DESCRIPTION);
-
-$cfg_display_prices = DfTools::getBooleanFromRequest(
+/* ---------- START CONFIG ---------- */
+$shouldDisplayPrices = DfTools::getBooleanFromRequest(
     'prices',
     (bool) DfTools::cfg($shop->id, 'DF_GS_DISPLAY_PRICES', DoofinderConstants::YES)
 );
-$cfg_prices_w_taxes = DfTools::getBooleanFromRequest(
+$shouldPricesUseTaxes = DfTools::getBooleanFromRequest(
     'taxes',
     (bool) DfTools::cfg($shop->id, 'DF_GS_PRICES_USE_TAX', DoofinderConstants::YES)
 );
-$cfg_image_size = DfTools::cfg($shop->id, 'DF_GS_IMAGE_SIZE');
-$cfg_mod_rewrite = DfTools::cfg($shop->id, 'PS_REWRITING_SETTINGS', DoofinderConstants::YES);
-$cfg_product_variations = (int) DfTools::cfg($shop->id, 'DF_SHOW_PRODUCT_VARIATIONS');
-$cfg_product_features = DfTools::cfg($shop->id, 'DF_SHOW_PRODUCT_FEATURES');
-$cfg_debug = DfTools::cfg($shop->id, 'DF_DEBUG');
-$cfg_features_shown = explode(',', DfTools::cfg($shop->id, 'DF_FEATURES_SHOWN'));
-
-$cfg_group_attributes_shown = explode(',', DfTools::cfg($shop->id, 'DF_GROUP_ATTRIBUTES_SHOWN'));
-
-$limit_group_attributes = false;
-if (
-    is_array($cfg_group_attributes_shown)
-    && count($cfg_group_attributes_shown) > 0
-    && $cfg_group_attributes_shown[0] !== ''
-) {
-    $group_attributes = AttributeGroup::getAttributesGroups($lang->id);
-    $group_attributes_slug = [];
-    foreach ($group_attributes as $g) {
-        if (in_array($g['id_attribute_group'], $cfg_group_attributes_shown)) {
-            $group_attributes_slug[] = slugify($g['name']);
-        }
-    }
-    $limit_group_attributes = true;
-}
-
+$isMultipriceEnabled = \Configuration::get('DF_MULTIPRICE_ENABLED');
+$shouldShowProductVariations = (int) DfTools::cfg($shop->id, 'DF_SHOW_PRODUCT_VARIATIONS');
+$shouldShowProductFeatures = DfTools::cfg($shop->id, 'DF_SHOW_PRODUCT_FEATURES');
path: feeds/product.php
user: login: sofia-doofinder
body: muchas gracias!!
created_at: 2025-02-25T15:04:39Z
author_association: MEMBER
diff_hunk: @@ -193,403 +162,119 @@ function arrayMergeByIdProduct($array1 = [], $array2 = [])
 
 $header[] = 'stock_quantity';
 
-if ($cfg_display_prices) {
+if ($shouldDisplayPrices) {
     $header[] = 'price';
     $header[] = 'sale_price';
-    $header[] = 'df_multiprice';
+
+    if ($isMultipriceEnabled) {
+        $header[] = 'df_multiprice';
+    }
 }
 
-if ($cfg_product_variations == 1) {
+$additionalAttributesHeaders = [];
+
+if (1 === $shouldShowProductVariations) {
     $header[] = 'variation_reference';
     $header[] = 'variation_supplier_reference';
     $header[] = 'variation_mpn';
     $header[] = 'variation_ean13';
     $header[] = 'variation_upc';
     $header[] = 'df_group_leader';
     $header[] = 'df_variants_information';
-    $attribute_keys = DfTools::getAttributeKeysForShopAndLang($shop->id, $lang->id);
-    $alt_attribute_keys = [];
-    foreach ($attribute_keys as $key) {
-        $header_value = slugify($key);
-        if ($limit_group_attributes && !in_array($header_value, $group_attributes_slug)) {
+    $attributeKeys = DfTools::getAttributeKeysForShopAndLang($shop->id, $lang->id);
+    $altAttributeKeys = [];
+
+    foreach ($attributeKeys as $key) {
+        $headerValue = DfTools::slugify($key);
+        if ($shouldLimitGroupAttributes && !in_array($headerValue, $groupAttributesSlug)) {
             continue;
         }
-        $alt_attribute_keys[] = $key;
-        $header[] = $header_value;
+        $altAttributeKeys[] = $key;
+        $additionalAttributesHeaders[] = $headerValue;
     }
-    $attribute_keys = $alt_attribute_keys;
+    $attributeKeys = $altAttributeKeys;
 }
 
-if ($cfg_product_features) {
-    $all_feature_keys = DfTools::getFeatureKeysForShopAndLang($shop->id, $lang->id);
+if ($shouldShowProductFeatures) {
+    $allFeatureKeys = DfTools::getFeatureKeysForShopAndLang($shop->id, $lang->id);
 
     if (
-        is_array($cfg_features_shown)
-        && count($cfg_features_shown) > 0
-        && $cfg_features_shown[0] !== ''
+        is_array($shouldFeaturesBeShown)
+        && count($shouldFeaturesBeShown) > 0
+        && $shouldFeaturesBeShown[0] !== ''
     ) {
-        $feature_keys = DfTools::getSelectedFeatures($all_feature_keys, $cfg_features_shown);
+        $featurekeys = DfTools::getSelectedFeatures($allFeatureKeys, $shouldFeaturesBeShown);
     } else {
-        $feature_keys = $all_feature_keys;
+        $featurekeys = $allFeatureKeys;
     }
-    $header[] = 'attributes';
+    $additionalAttributesHeaders[] = 'attributes';
 }
 
+$header = array_merge($header, $additionalAttributesHeaders);
+
 /**
  * @author camlafit <https://github.com/camlafit>
- * Extend doofinder feed
+ * Allows users to extend Doofinder feed by adding extra headers and extra rows. Example:
path: feeds/product.php
user: login: sofia-doofinder
body: gracias por la aclaración david,  lo podrías incluir en el comentario? que la forma de hacerlo es con el hook ese
created_at: 2025-02-25T15:05:12Z
author_association: MEMBER
diff_hunk: @@ -193,403 +162,119 @@ function arrayMergeByIdProduct($array1 = [], $array2 = [])
 
 $header[] = 'stock_quantity';
 
-if ($cfg_display_prices) {
+if ($shouldDisplayPrices) {
     $header[] = 'price';
     $header[] = 'sale_price';
-    $header[] = 'df_multiprice';
+
+    if ($isMultipriceEnabled) {
+        $header[] = 'df_multiprice';
+    }
 }
 
-if ($cfg_product_variations == 1) {
+$additionalAttributesHeaders = [];
+
+if (1 === $shouldShowProductVariations) {
     $header[] = 'variation_reference';
     $header[] = 'variation_supplier_reference';
     $header[] = 'variation_mpn';
     $header[] = 'variation_ean13';
     $header[] = 'variation_upc';
     $header[] = 'df_group_leader';
     $header[] = 'df_variants_information';
-    $attribute_keys = DfTools::getAttributeKeysForShopAndLang($shop->id, $lang->id);
-    $alt_attribute_keys = [];
-    foreach ($attribute_keys as $key) {
-        $header_value = slugify($key);
-        if ($limit_group_attributes && !in_array($header_value, $group_attributes_slug)) {
+    $attributeKeys = DfTools::getAttributeKeysForShopAndLang($shop->id, $lang->id);
+    $altAttributeKeys = [];
+
+    foreach ($attributeKeys as $key) {
+        $headerValue = DfTools::slugify($key);
+        if ($shouldLimitGroupAttributes && !in_array($headerValue, $groupAttributesSlug)) {
             continue;
         }
-        $alt_attribute_keys[] = $key;
-        $header[] = $header_value;
+        $altAttributeKeys[] = $key;
+        $additionalAttributesHeaders[] = $headerValue;
     }
-    $attribute_keys = $alt_attribute_keys;
+    $attributeKeys = $altAttributeKeys;
 }
 
-if ($cfg_product_features) {
-    $all_feature_keys = DfTools::getFeatureKeysForShopAndLang($shop->id, $lang->id);
+if ($shouldShowProductFeatures) {
+    $allFeatureKeys = DfTools::getFeatureKeysForShopAndLang($shop->id, $lang->id);
 
     if (
-        is_array($cfg_features_shown)
-        && count($cfg_features_shown) > 0
-        && $cfg_features_shown[0] !== ''
+        is_array($shouldFeaturesBeShown)
+        && count($shouldFeaturesBeShown) > 0
+        && $shouldFeaturesBeShown[0] !== ''
     ) {
-        $feature_keys = DfTools::getSelectedFeatures($all_feature_keys, $cfg_features_shown);
+        $featurekeys = DfTools::getSelectedFeatures($allFeatureKeys, $shouldFeaturesBeShown);
     } else {
-        $feature_keys = $all_feature_keys;
+        $featurekeys = $allFeatureKeys;
     }
-    $header[] = 'attributes';
+    $additionalAttributesHeaders[] = 'attributes';
 }
 
+$header = array_merge($header, $additionalAttributesHeaders);
+
 /**
  * @author camlafit <https://github.com/camlafit>
- * Extend doofinder feed
+ * Allows users to extend Doofinder feed by adding extra headers and extra rows. Example:
path: feeds/product.php
user: login: davidmolinacano
body: Por supuesto, lo incluyo ahora mismo :smile: 
created_at: 2025-02-25T15:19:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -193,403 +162,119 @@ function arrayMergeByIdProduct($array1 = [], $array2 = [])
 
 $header[] = 'stock_quantity';
 
-if ($cfg_display_prices) {
+if ($shouldDisplayPrices) {
     $header[] = 'price';
     $header[] = 'sale_price';
-    $header[] = 'df_multiprice';
+
+    if ($isMultipriceEnabled) {
+        $header[] = 'df_multiprice';
+    }
 }
 
-if ($cfg_product_variations == 1) {
+$additionalAttributesHeaders = [];
+
+if (1 === $shouldShowProductVariations) {
     $header[] = 'variation_reference';
     $header[] = 'variation_supplier_reference';
     $header[] = 'variation_mpn';
     $header[] = 'variation_ean13';
     $header[] = 'variation_upc';
     $header[] = 'df_group_leader';
     $header[] = 'df_variants_information';
-    $attribute_keys = DfTools::getAttributeKeysForShopAndLang($shop->id, $lang->id);
-    $alt_attribute_keys = [];
-    foreach ($attribute_keys as $key) {
-        $header_value = slugify($key);
-        if ($limit_group_attributes && !in_array($header_value, $group_attributes_slug)) {
+    $attributeKeys = DfTools::getAttributeKeysForShopAndLang($shop->id, $lang->id);
+    $altAttributeKeys = [];
+
+    foreach ($attributeKeys as $key) {
+        $headerValue = DfTools::slugify($key);
+        if ($shouldLimitGroupAttributes && !in_array($headerValue, $groupAttributesSlug)) {
             continue;
         }
-        $alt_attribute_keys[] = $key;
-        $header[] = $header_value;
+        $altAttributeKeys[] = $key;
+        $additionalAttributesHeaders[] = $headerValue;
     }
-    $attribute_keys = $alt_attribute_keys;
+    $attributeKeys = $altAttributeKeys;
 }
 
-if ($cfg_product_features) {
-    $all_feature_keys = DfTools::getFeatureKeysForShopAndLang($shop->id, $lang->id);
+if ($shouldShowProductFeatures) {
+    $allFeatureKeys = DfTools::getFeatureKeysForShopAndLang($shop->id, $lang->id);
 
     if (
-        is_array($cfg_features_shown)
-        && count($cfg_features_shown) > 0
-        && $cfg_features_shown[0] !== ''
+        is_array($shouldFeaturesBeShown)
+        && count($shouldFeaturesBeShown) > 0
+        && $shouldFeaturesBeShown[0] !== ''
     ) {
-        $feature_keys = DfTools::getSelectedFeatures($all_feature_keys, $cfg_features_shown);
+        $featurekeys = DfTools::getSelectedFeatures($allFeatureKeys, $shouldFeaturesBeShown);
     } else {
-        $feature_keys = $all_feature_keys;
+        $featurekeys = $allFeatureKeys;
     }
-    $header[] = 'attributes';
+    $additionalAttributesHeaders[] = 'attributes';
 }
 
+$header = array_merge($header, $additionalAttributesHeaders);
+
 /**
  * @author camlafit <https://github.com/camlafit>
- * Extend doofinder feed
+ * Allows users to extend Doofinder feed by adding extra headers and extra rows. Example:
path: feeds/product.php
user: login: davidmolinacano
body: Incluido en: https://github.com/doofinder/doofinder-prestashop/pull/291/commits/808fc62f7682acd75148e7c09fa1038ba5edeca3
created_at: 2025-02-25T15:23:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -58,52 +69,27 @@ public function setProducts($arrayProducts)
 
     public function build()
path: src/Entity/DfProductBuild.php
user: login: sofia-doofinder
body: David otra duda, cuando se llama a esta función `build`? porque veo que en fichero de product.php al que se llama es a `buildProduct()` no? es para el update on save?
created_at: 2025-02-26T11:09:11Z
author_association: MEMBER
diff_hunk: @@ -128,46 +114,158 @@ private function buildProduct($product)
         $p['extra_title_1'] = $p['title'];
         $p['extra_title_2'] = DfTools::splitReferences($p['title']);
         $p['tags'] = DfTools::cleanString($product['tags']);
-        $p['stock_quantity'] = DfTools::cleanString($product['stock_quantity']);
 
         if (DfTools::versionGte('1.7.0.0')) {
             $p['isbn'] = DfTools::cleanString($product['isbn']);
         }
 
+        $p['stock_quantity'] = DfTools::cleanString($product['stock_quantity']);
         if ($this->displayPrices) {
             $p['price'] = $this->getPrice($product);
             $p['sale_price'] = $this->getPrice($product, true);
 
             if ($this->multipriceEnabled) {
                 $p['df_multiprice'] = $this->getMultiprice($product);
             }
-        }
 
-        if ($this->showProductFeatures) {
-            $p = array_merge($p, $this->getFeatures($product));
+            if (DfTools::isParent($product) && array_key_exists($p['id'], $minPriceVariantByProductId)) {
+                $minVariant = $minPriceVariantByProductId[$p['id']];
+                if (
+                    !is_null($minVariant['onsale_price'])
+                    && !is_null($minVariant['price'])
+                    && (empty($p['sale_price']) || $minVariant['onsale_price'] < $p['sale_price'])
+                ) {
+                    $p['price'] = $minVariant['price'];
+                    $p['sale_price'] = ($minVariant['onsale_price'] === $minVariant['price']) ? null : $minVariant['onsale_price'];
+                    if ($this->multipriceEnabled) {
+                        $p['df_multiprice'] = $minVariant['multiprice'];
+                    }
+                }
+            }
         }
 
         if ($this->productVariations) {
-            $p['item_group_id'] = $this->getItemGroupId($product);
-            $p['group_id'] = $this->getItemGroupId($product);
-
             $p['variation_reference'] = $product['variation_reference'];
             $p['variation_supplier_reference'] = $product['variation_supplier_reference'];
             $p['variation_mpn'] = $product['variation_mpn'];
             $p['variation_ean13'] = $product['variation_ean13'];
             $p['variation_upc'] = $product['variation_upc'];
-            $p['df_group_leader'] = (!is_null($product['df_group_leader'] && $product['df_group_leader']) ? true : false);
-
+            $p['df_group_leader'] = (is_numeric($product['df_group_leader']) && 0 !== (int)$product['df_group_leader']);
+            $p['df_variants_information'] = $this->getVariantsInformation($product);
+            
             $attributes = $this->getAttributes($product);
 
             $p = array_merge($p, $attributes);
 
-            $p['df_variants_information'] = $this->getVariantsInformation($product);
+            foreach ($extraAttributesHeader as $extraAttributeHeader) {
+                if ('attributes' !== $extraAttributeHeader && !array_key_exists($extraAttributeHeader, $p) && array_key_exists($extraAttributeHeader, $attributes)) {
+                    $p[$extraAttributeHeader] = $attributes[$extraAttributeHeader];
+                    continue;
+                }
+            }
+        }
+
+        if ($this->showProductFeatures) {
+            $p['features'] = $this->getFeatures($product);
+        }
+        
+        foreach ($extraHeaders as $extraHeader) {
+            if (!empty($p[$extraHeader])) {
+                continue;
+            }
+            $p[$extraHeader] = isset($product[$extraHeader]) ? DfTools::cleanString($product[$extraHeader]) : '';
         }
 
         return $p;
     }
 
+    /**
+     * Applies specific transformations to a product's data for CSV export.
+     *
+     * This method performs several modifications on the product array, like:
+     * - If multi-price is enabled, formats the multiprice field.
+     * - Joins category values using a predefined separator.
+     * - If variants information exists, it slugifies and joins them using "%%".
+     * - Casts the group leader flag to an integer.
+     * - Iterates over extra headers to process attribute values:
+     *   - For each non-empty extra header, it concatenates key-value pairs in the format "key=value",
+     *     with the value cleaned and any "/" characters escaped.
+     * - Processes features (if present and an array) by converting them into an attributes string
+     *   formatted as "key=value" pairs joined with "/". The original features key is removed.
+     * - Ensures the final product fields are ordered according to the given headers.
+     *
+     * @param array $product      The associative array representing the product data.
+     * @param array $extraHeaders An array of additional headers to process in the product data.
+     * @param array $allHeaders   An array specifying the order of CSV fields.
+     *
+     * @return array The transformed product array ready for CSV export.
+     */
+    public function applySpecificTransformationsForCsv($product, $extraHeaders, $allHeaders)
+    {
+        if ($this->multipriceEnabled) {
+            $product['df_multiprice'] = DfTools::getFormattedMultiprice($product['df_multiprice']);
+        }
+        $product['categories'] = implode(DfTools::CATEGORY_SEPARATOR, $product['categories']);
+
+        if (array_key_exists('df_variants_information', $product)) {
+            $product['df_variants_information'] = implode('%%', array_map([__NAMESPACE__ . '\DfTools', 'slugify'], $product['df_variants_information']));
+        }
+
+        $product['df_group_leader'] = (int)$product['df_group_leader'];
+
+        foreach ($extraHeaders as $extraHeader) {
+            if (empty($product[$extraHeader])) {
+                continue;
+            }
+            $attributeValue = '';
+            foreach ($product[$extraHeader] as $index => $value) {
path: src/Entity/DfProductBuild.php
user: login: sofia-doofinder
body: y otra duda de funcionamiento aquí @davidmolinacano, lo que tiene el producto como `extraHeader` tiene que ser un array? no puede ser un valor simple? digo porque no se si habría que hacer una comprobación antes de que el valor sea un array porque si no pisaríamos el valor ocn una cadena vacía, no?
created_at: 2025-02-26T11:13:35Z
author_association: MEMBER
diff_hunk: @@ -104,57 +81,34 @@ function arrayMergeByIdProduct($array1 = [], $array2 = [])
     exit('NOT PROPERLY CONFIGURED');
 }
 
-// CONFIG
+// GENERAL PURPOSE VARIABLES + CONTEXT
 $lang = DfTools::getLanguageFromRequest();
 $context->language = $lang;
-$country = Configuration::get('PS_COUNTRY_DEFAULT');
+$country = (int) DfTools::cfg($shop->id, 'PS_COUNTRY_DEFAULT');
 $context->country = new Country($country);
 $currency = DfTools::getCurrencyForLanguageFromRequest($lang);
-$multiprice_enabled = Configuration::get('DF_MULTIPRICE_ENABLED');
 $currencies = Currency::getCurrenciesByIdShop($context->shop->id);
 
-$cfg_short_description = (DfTools::cfg(
-    $shop->id,
-    'DF_GS_DESCRIPTION_TYPE',
-    DoofinderConstants::GS_SHORT_DESCRIPTION
-) == DoofinderConstants::GS_SHORT_DESCRIPTION);
-
-$cfg_display_prices = DfTools::getBooleanFromRequest(
+/* ---------- START CONFIG ---------- */
+$shouldDisplayPrices = DfTools::getBooleanFromRequest(
path: feeds/product.php
user: login: davidmolinacano
body: Según lo acordado se va a mover a Advanced (habilitado por defecto) junto con su selector hijo "Show product prices including taxes"
created_at: 2025-02-26T12:00:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -104,57 +81,34 @@ function arrayMergeByIdProduct($array1 = [], $array2 = [])
     exit('NOT PROPERLY CONFIGURED');
 }
 
-// CONFIG
+// GENERAL PURPOSE VARIABLES + CONTEXT
 $lang = DfTools::getLanguageFromRequest();
 $context->language = $lang;
-$country = Configuration::get('PS_COUNTRY_DEFAULT');
+$country = (int) DfTools::cfg($shop->id, 'PS_COUNTRY_DEFAULT');
 $context->country = new Country($country);
 $currency = DfTools::getCurrencyForLanguageFromRequest($lang);
-$multiprice_enabled = Configuration::get('DF_MULTIPRICE_ENABLED');
 $currencies = Currency::getCurrenciesByIdShop($context->shop->id);
 
-$cfg_short_description = (DfTools::cfg(
-    $shop->id,
-    'DF_GS_DESCRIPTION_TYPE',
-    DoofinderConstants::GS_SHORT_DESCRIPTION
-) == DoofinderConstants::GS_SHORT_DESCRIPTION);
-
-$cfg_display_prices = DfTools::getBooleanFromRequest(
+/* ---------- START CONFIG ---------- */
+$shouldDisplayPrices = DfTools::getBooleanFromRequest(
path: feeds/product.php
user: login: sofia-doofinder
body: el cliente puede acceder a la pestaña de advanced? o eso era solo para soporte?
created_at: 2025-02-26T12:15:38Z
author_association: MEMBER
diff_hunk: @@ -104,57 +81,34 @@ function arrayMergeByIdProduct($array1 = [], $array2 = [])
     exit('NOT PROPERLY CONFIGURED');
 }
 
-// CONFIG
+// GENERAL PURPOSE VARIABLES + CONTEXT
 $lang = DfTools::getLanguageFromRequest();
 $context->language = $lang;
-$country = Configuration::get('PS_COUNTRY_DEFAULT');
+$country = (int) DfTools::cfg($shop->id, 'PS_COUNTRY_DEFAULT');
 $context->country = new Country($country);
 $currency = DfTools::getCurrencyForLanguageFromRequest($lang);
-$multiprice_enabled = Configuration::get('DF_MULTIPRICE_ENABLED');
 $currencies = Currency::getCurrenciesByIdShop($context->shop->id);
 
-$cfg_short_description = (DfTools::cfg(
-    $shop->id,
-    'DF_GS_DESCRIPTION_TYPE',
-    DoofinderConstants::GS_SHORT_DESCRIPTION
-) == DoofinderConstants::GS_SHORT_DESCRIPTION);
-
-$cfg_display_prices = DfTools::getBooleanFromRequest(
+/* ---------- START CONFIG ---------- */
+$shouldDisplayPrices = DfTools::getBooleanFromRequest(
path: feeds/product.php
user: login: davidmolinacano
body: Bueno, si ponen el parámetro adecuado podrían acceder, pero la idea es que solo soporte lo use :smile: 
created_at: 2025-02-26T12:17:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -104,57 +81,34 @@ function arrayMergeByIdProduct($array1 = [], $array2 = [])
     exit('NOT PROPERLY CONFIGURED');
 }
 
-// CONFIG
+// GENERAL PURPOSE VARIABLES + CONTEXT
 $lang = DfTools::getLanguageFromRequest();
 $context->language = $lang;
-$country = Configuration::get('PS_COUNTRY_DEFAULT');
+$country = (int) DfTools::cfg($shop->id, 'PS_COUNTRY_DEFAULT');
 $context->country = new Country($country);
 $currency = DfTools::getCurrencyForLanguageFromRequest($lang);
-$multiprice_enabled = Configuration::get('DF_MULTIPRICE_ENABLED');
 $currencies = Currency::getCurrenciesByIdShop($context->shop->id);
 
-$cfg_short_description = (DfTools::cfg(
-    $shop->id,
-    'DF_GS_DESCRIPTION_TYPE',
-    DoofinderConstants::GS_SHORT_DESCRIPTION
-) == DoofinderConstants::GS_SHORT_DESCRIPTION);
-
-$cfg_display_prices = DfTools::getBooleanFromRequest(
+/* ---------- START CONFIG ---------- */
+$shouldDisplayPrices = DfTools::getBooleanFromRequest(
path: feeds/product.php
user: login: sofia-doofinder
body: y lo de indexar con impuesto o sin impuestos no sería mejor dejarselo? porque esto es algo que creo que los usuarios si pueden querer modificarlo...
created_at: 2025-02-26T12:36:43Z
author_association: MEMBER
diff_hunk: @@ -104,57 +81,34 @@ function arrayMergeByIdProduct($array1 = [], $array2 = [])
     exit('NOT PROPERLY CONFIGURED');
 }
 
-// CONFIG
+// GENERAL PURPOSE VARIABLES + CONTEXT
 $lang = DfTools::getLanguageFromRequest();
 $context->language = $lang;
-$country = Configuration::get('PS_COUNTRY_DEFAULT');
+$country = (int) DfTools::cfg($shop->id, 'PS_COUNTRY_DEFAULT');
 $context->country = new Country($country);
 $currency = DfTools::getCurrencyForLanguageFromRequest($lang);
-$multiprice_enabled = Configuration::get('DF_MULTIPRICE_ENABLED');
 $currencies = Currency::getCurrenciesByIdShop($context->shop->id);
 
-$cfg_short_description = (DfTools::cfg(
-    $shop->id,
-    'DF_GS_DESCRIPTION_TYPE',
-    DoofinderConstants::GS_SHORT_DESCRIPTION
-) == DoofinderConstants::GS_SHORT_DESCRIPTION);
-
-$cfg_display_prices = DfTools::getBooleanFromRequest(
+/* ---------- START CONFIG ---------- */
+$shouldDisplayPrices = DfTools::getBooleanFromRequest(
path: feeds/product.php
user: login: davidmolinacano
body: CC @IgnacioPursalsZ 
created_at: 2025-02-26T13:14:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -104,57 +81,34 @@ function arrayMergeByIdProduct($array1 = [], $array2 = [])
     exit('NOT PROPERLY CONFIGURED');
 }
 
-// CONFIG
+// GENERAL PURPOSE VARIABLES + CONTEXT
 $lang = DfTools::getLanguageFromRequest();
 $context->language = $lang;
-$country = Configuration::get('PS_COUNTRY_DEFAULT');
+$country = (int) DfTools::cfg($shop->id, 'PS_COUNTRY_DEFAULT');
 $context->country = new Country($country);
 $currency = DfTools::getCurrencyForLanguageFromRequest($lang);
-$multiprice_enabled = Configuration::get('DF_MULTIPRICE_ENABLED');
 $currencies = Currency::getCurrenciesByIdShop($context->shop->id);
 
-$cfg_short_description = (DfTools::cfg(
-    $shop->id,
-    'DF_GS_DESCRIPTION_TYPE',
-    DoofinderConstants::GS_SHORT_DESCRIPTION
-) == DoofinderConstants::GS_SHORT_DESCRIPTION);
-
-$cfg_display_prices = DfTools::getBooleanFromRequest(
+/* ---------- START CONFIG ---------- */
+$shouldDisplayPrices = DfTools::getBooleanFromRequest(
path: feeds/product.php
user: login: IgnacioPursalsZ
body: Correcto @sofia-doofinder el configurador de si "con impuestos" a "sin impuestos" si se lo podríamos dejar visible
created_at: 2025-02-26T17:57:52Z
author_association: NONE
diff_hunk: @@ -104,57 +81,34 @@ function arrayMergeByIdProduct($array1 = [], $array2 = [])
     exit('NOT PROPERLY CONFIGURED');
 }
 
-// CONFIG
+// GENERAL PURPOSE VARIABLES + CONTEXT
 $lang = DfTools::getLanguageFromRequest();
 $context->language = $lang;
-$country = Configuration::get('PS_COUNTRY_DEFAULT');
+$country = (int) DfTools::cfg($shop->id, 'PS_COUNTRY_DEFAULT');
 $context->country = new Country($country);
 $currency = DfTools::getCurrencyForLanguageFromRequest($lang);
-$multiprice_enabled = Configuration::get('DF_MULTIPRICE_ENABLED');
 $currencies = Currency::getCurrenciesByIdShop($context->shop->id);
 
-$cfg_short_description = (DfTools::cfg(
-    $shop->id,
-    'DF_GS_DESCRIPTION_TYPE',
-    DoofinderConstants::GS_SHORT_DESCRIPTION
-) == DoofinderConstants::GS_SHORT_DESCRIPTION);
-
-$cfg_display_prices = DfTools::getBooleanFromRequest(
+/* ---------- START CONFIG ---------- */
+$shouldDisplayPrices = DfTools::getBooleanFromRequest(
path: feeds/product.php
user: login: davidmolinacano
body: No sé si estos cambios deberían ir en esta PR o en otra, ya que sino podría complicarse en exceso.
created_at: 2025-02-27T08:19:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -104,57 +81,34 @@ function arrayMergeByIdProduct($array1 = [], $array2 = [])
     exit('NOT PROPERLY CONFIGURED');
 }
 
-// CONFIG
+// GENERAL PURPOSE VARIABLES + CONTEXT
 $lang = DfTools::getLanguageFromRequest();
 $context->language = $lang;
-$country = Configuration::get('PS_COUNTRY_DEFAULT');
+$country = (int) DfTools::cfg($shop->id, 'PS_COUNTRY_DEFAULT');
 $context->country = new Country($country);
 $currency = DfTools::getCurrencyForLanguageFromRequest($lang);
-$multiprice_enabled = Configuration::get('DF_MULTIPRICE_ENABLED');
 $currencies = Currency::getCurrenciesByIdShop($context->shop->id);
 
-$cfg_short_description = (DfTools::cfg(
-    $shop->id,
-    'DF_GS_DESCRIPTION_TYPE',
-    DoofinderConstants::GS_SHORT_DESCRIPTION
-) == DoofinderConstants::GS_SHORT_DESCRIPTION);
-
-$cfg_display_prices = DfTools::getBooleanFromRequest(
+/* ---------- START CONFIG ---------- */
+$shouldDisplayPrices = DfTools::getBooleanFromRequest(
path: feeds/product.php
user: login: sofia-doofinder
body: creo que otra PR mejor
created_at: 2025-02-27T08:47:20Z
author_association: MEMBER
diff_hunk: @@ -58,52 +69,27 @@ public function setProducts($arrayProducts)
 
     public function build()
path: src/Entity/DfProductBuild.php
user: login: davidmolinacano
body: Eso es, en los update on save se llama a build y no a buildProduct directamente porque el build hace operaciones extra como convertir a JSON entre otras (cosa que no nos viene bien en el feed de product.php).
created_at: 2025-02-27T10:34:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -128,46 +114,158 @@ private function buildProduct($product)
         $p['extra_title_1'] = $p['title'];
         $p['extra_title_2'] = DfTools::splitReferences($p['title']);
         $p['tags'] = DfTools::cleanString($product['tags']);
-        $p['stock_quantity'] = DfTools::cleanString($product['stock_quantity']);
 
         if (DfTools::versionGte('1.7.0.0')) {
             $p['isbn'] = DfTools::cleanString($product['isbn']);
         }
 
+        $p['stock_quantity'] = DfTools::cleanString($product['stock_quantity']);
         if ($this->displayPrices) {
             $p['price'] = $this->getPrice($product);
             $p['sale_price'] = $this->getPrice($product, true);
 
             if ($this->multipriceEnabled) {
                 $p['df_multiprice'] = $this->getMultiprice($product);
             }
-        }
 
-        if ($this->showProductFeatures) {
-            $p = array_merge($p, $this->getFeatures($product));
+            if (DfTools::isParent($product) && array_key_exists($p['id'], $minPriceVariantByProductId)) {
+                $minVariant = $minPriceVariantByProductId[$p['id']];
+                if (
+                    !is_null($minVariant['onsale_price'])
+                    && !is_null($minVariant['price'])
+                    && (empty($p['sale_price']) || $minVariant['onsale_price'] < $p['sale_price'])
+                ) {
+                    $p['price'] = $minVariant['price'];
+                    $p['sale_price'] = ($minVariant['onsale_price'] === $minVariant['price']) ? null : $minVariant['onsale_price'];
+                    if ($this->multipriceEnabled) {
+                        $p['df_multiprice'] = $minVariant['multiprice'];
+                    }
+                }
+            }
         }
 
         if ($this->productVariations) {
-            $p['item_group_id'] = $this->getItemGroupId($product);
-            $p['group_id'] = $this->getItemGroupId($product);
-
             $p['variation_reference'] = $product['variation_reference'];
             $p['variation_supplier_reference'] = $product['variation_supplier_reference'];
             $p['variation_mpn'] = $product['variation_mpn'];
             $p['variation_ean13'] = $product['variation_ean13'];
             $p['variation_upc'] = $product['variation_upc'];
-            $p['df_group_leader'] = (!is_null($product['df_group_leader'] && $product['df_group_leader']) ? true : false);
-
+            $p['df_group_leader'] = (is_numeric($product['df_group_leader']) && 0 !== (int)$product['df_group_leader']);
+            $p['df_variants_information'] = $this->getVariantsInformation($product);
+            
             $attributes = $this->getAttributes($product);
 
             $p = array_merge($p, $attributes);
 
-            $p['df_variants_information'] = $this->getVariantsInformation($product);
+            foreach ($extraAttributesHeader as $extraAttributeHeader) {
+                if ('attributes' !== $extraAttributeHeader && !array_key_exists($extraAttributeHeader, $p) && array_key_exists($extraAttributeHeader, $attributes)) {
+                    $p[$extraAttributeHeader] = $attributes[$extraAttributeHeader];
+                    continue;
+                }
+            }
+        }
+
+        if ($this->showProductFeatures) {
+            $p['features'] = $this->getFeatures($product);
+        }
+        
+        foreach ($extraHeaders as $extraHeader) {
+            if (!empty($p[$extraHeader])) {
+                continue;
+            }
+            $p[$extraHeader] = isset($product[$extraHeader]) ? DfTools::cleanString($product[$extraHeader]) : '';
         }
 
         return $p;
     }
 
+    /**
+     * Applies specific transformations to a product's data for CSV export.
+     *
+     * This method performs several modifications on the product array, like:
+     * - If multi-price is enabled, formats the multiprice field.
+     * - Joins category values using a predefined separator.
+     * - If variants information exists, it slugifies and joins them using "%%".
+     * - Casts the group leader flag to an integer.
+     * - Iterates over extra headers to process attribute values:
+     *   - For each non-empty extra header, it concatenates key-value pairs in the format "key=value",
+     *     with the value cleaned and any "/" characters escaped.
+     * - Processes features (if present and an array) by converting them into an attributes string
+     *   formatted as "key=value" pairs joined with "/". The original features key is removed.
+     * - Ensures the final product fields are ordered according to the given headers.
+     *
+     * @param array $product      The associative array representing the product data.
+     * @param array $extraHeaders An array of additional headers to process in the product data.
+     * @param array $allHeaders   An array specifying the order of CSV fields.
+     *
+     * @return array The transformed product array ready for CSV export.
+     */
+    public function applySpecificTransformationsForCsv($product, $extraHeaders, $allHeaders)
+    {
+        if ($this->multipriceEnabled) {
+            $product['df_multiprice'] = DfTools::getFormattedMultiprice($product['df_multiprice']);
+        }
+        $product['categories'] = implode(DfTools::CATEGORY_SEPARATOR, $product['categories']);
+
+        if (array_key_exists('df_variants_information', $product)) {
+            $product['df_variants_information'] = implode('%%', array_map([__NAMESPACE__ . '\DfTools', 'slugify'], $product['df_variants_information']));
+        }
+
+        $product['df_group_leader'] = (int)$product['df_group_leader'];
+
+        foreach ($extraHeaders as $extraHeader) {
+            if (empty($product[$extraHeader])) {
+                continue;
+            }
+            $attributeValue = '';
+            foreach ($product[$extraHeader] as $index => $value) {
path: src/Entity/DfProductBuild.php
user: login: davidmolinacano
body: Técnicamente siempre va a ser un array, lo único que podría estar vacío. Pero bueno, nunca está de más comprobar lo que es por si acaso :raised_hands: 
created_at: 2025-02-27T10:46:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -58,52 +69,27 @@ public function setProducts($arrayProducts)
 
     public function build()
path: src/Entity/DfProductBuild.php
user: login: sofia-doofinder
body: genial, gracias por la aclaración!
created_at: 2025-02-27T11:22:59Z
author_association: MEMBER
diff_hunk: @@ -128,46 +114,158 @@ private function buildProduct($product)
         $p['extra_title_1'] = $p['title'];
         $p['extra_title_2'] = DfTools::splitReferences($p['title']);
         $p['tags'] = DfTools::cleanString($product['tags']);
-        $p['stock_quantity'] = DfTools::cleanString($product['stock_quantity']);
 
         if (DfTools::versionGte('1.7.0.0')) {
             $p['isbn'] = DfTools::cleanString($product['isbn']);
         }
 
+        $p['stock_quantity'] = DfTools::cleanString($product['stock_quantity']);
         if ($this->displayPrices) {
             $p['price'] = $this->getPrice($product);
             $p['sale_price'] = $this->getPrice($product, true);
 
             if ($this->multipriceEnabled) {
                 $p['df_multiprice'] = $this->getMultiprice($product);
             }
-        }
 
-        if ($this->showProductFeatures) {
-            $p = array_merge($p, $this->getFeatures($product));
+            if (DfTools::isParent($product) && array_key_exists($p['id'], $minPriceVariantByProductId)) {
+                $minVariant = $minPriceVariantByProductId[$p['id']];
+                if (
+                    !is_null($minVariant['onsale_price'])
+                    && !is_null($minVariant['price'])
+                    && (empty($p['sale_price']) || $minVariant['onsale_price'] < $p['sale_price'])
+                ) {
+                    $p['price'] = $minVariant['price'];
+                    $p['sale_price'] = ($minVariant['onsale_price'] === $minVariant['price']) ? null : $minVariant['onsale_price'];
+                    if ($this->multipriceEnabled) {
+                        $p['df_multiprice'] = $minVariant['multiprice'];
+                    }
+                }
+            }
         }
 
         if ($this->productVariations) {
-            $p['item_group_id'] = $this->getItemGroupId($product);
-            $p['group_id'] = $this->getItemGroupId($product);
-
             $p['variation_reference'] = $product['variation_reference'];
             $p['variation_supplier_reference'] = $product['variation_supplier_reference'];
             $p['variation_mpn'] = $product['variation_mpn'];
             $p['variation_ean13'] = $product['variation_ean13'];
             $p['variation_upc'] = $product['variation_upc'];
-            $p['df_group_leader'] = (!is_null($product['df_group_leader'] && $product['df_group_leader']) ? true : false);
-
+            $p['df_group_leader'] = (is_numeric($product['df_group_leader']) && 0 !== (int)$product['df_group_leader']);
+            $p['df_variants_information'] = $this->getVariantsInformation($product);
+            
             $attributes = $this->getAttributes($product);
 
             $p = array_merge($p, $attributes);
 
-            $p['df_variants_information'] = $this->getVariantsInformation($product);
+            foreach ($extraAttributesHeader as $extraAttributeHeader) {
+                if ('attributes' !== $extraAttributeHeader && !array_key_exists($extraAttributeHeader, $p) && array_key_exists($extraAttributeHeader, $attributes)) {
+                    $p[$extraAttributeHeader] = $attributes[$extraAttributeHeader];
+                    continue;
+                }
+            }
+        }
+
+        if ($this->showProductFeatures) {
+            $p['features'] = $this->getFeatures($product);
+        }
+        
+        foreach ($extraHeaders as $extraHeader) {
+            if (!empty($p[$extraHeader])) {
+                continue;
+            }
+            $p[$extraHeader] = isset($product[$extraHeader]) ? DfTools::cleanString($product[$extraHeader]) : '';
         }
 
         return $p;
     }
 
+    /**
+     * Applies specific transformations to a product's data for CSV export.
+     *
+     * This method performs several modifications on the product array, like:
+     * - If multi-price is enabled, formats the multiprice field.
+     * - Joins category values using a predefined separator.
+     * - If variants information exists, it slugifies and joins them using "%%".
+     * - Casts the group leader flag to an integer.
+     * - Iterates over extra headers to process attribute values:
+     *   - For each non-empty extra header, it concatenates key-value pairs in the format "key=value",
+     *     with the value cleaned and any "/" characters escaped.
+     * - Processes features (if present and an array) by converting them into an attributes string
+     *   formatted as "key=value" pairs joined with "/". The original features key is removed.
+     * - Ensures the final product fields are ordered according to the given headers.
+     *
+     * @param array $product      The associative array representing the product data.
+     * @param array $extraHeaders An array of additional headers to process in the product data.
+     * @param array $allHeaders   An array specifying the order of CSV fields.
+     *
+     * @return array The transformed product array ready for CSV export.
+     */
+    public function applySpecificTransformationsForCsv($product, $extraHeaders, $allHeaders)
+    {
+        if ($this->multipriceEnabled) {
+            $product['df_multiprice'] = DfTools::getFormattedMultiprice($product['df_multiprice']);
+        }
+        $product['categories'] = implode(DfTools::CATEGORY_SEPARATOR, $product['categories']);
+
+        if (array_key_exists('df_variants_information', $product)) {
+            $product['df_variants_information'] = implode('%%', array_map([__NAMESPACE__ . '\DfTools', 'slugify'], $product['df_variants_information']));
+        }
+
+        $product['df_group_leader'] = (int)$product['df_group_leader'];
+
+        foreach ($extraHeaders as $extraHeader) {
+            if (empty($product[$extraHeader])) {
+                continue;
+            }
+            $attributeValue = '';
+            foreach ($product[$extraHeader] as $index => $value) {
path: src/Entity/DfProductBuild.php
user: login: sofia-doofinder
body: me surge la duda sobre todo porque si es algo que define el usuario como aseguramso que siempre sea un array?
created_at: 2025-02-27T11:23:18Z
author_association: MEMBER
diff_hunk: @@ -128,46 +114,158 @@ private function buildProduct($product)
         $p['extra_title_1'] = $p['title'];
         $p['extra_title_2'] = DfTools::splitReferences($p['title']);
         $p['tags'] = DfTools::cleanString($product['tags']);
-        $p['stock_quantity'] = DfTools::cleanString($product['stock_quantity']);
 
         if (DfTools::versionGte('1.7.0.0')) {
             $p['isbn'] = DfTools::cleanString($product['isbn']);
         }
 
+        $p['stock_quantity'] = DfTools::cleanString($product['stock_quantity']);
         if ($this->displayPrices) {
             $p['price'] = $this->getPrice($product);
             $p['sale_price'] = $this->getPrice($product, true);
 
             if ($this->multipriceEnabled) {
                 $p['df_multiprice'] = $this->getMultiprice($product);
             }
-        }
 
-        if ($this->showProductFeatures) {
-            $p = array_merge($p, $this->getFeatures($product));
+            if (DfTools::isParent($product) && array_key_exists($p['id'], $minPriceVariantByProductId)) {
+                $minVariant = $minPriceVariantByProductId[$p['id']];
+                if (
+                    !is_null($minVariant['onsale_price'])
+                    && !is_null($minVariant['price'])
+                    && (empty($p['sale_price']) || $minVariant['onsale_price'] < $p['sale_price'])
+                ) {
+                    $p['price'] = $minVariant['price'];
+                    $p['sale_price'] = ($minVariant['onsale_price'] === $minVariant['price']) ? null : $minVariant['onsale_price'];
+                    if ($this->multipriceEnabled) {
+                        $p['df_multiprice'] = $minVariant['multiprice'];
+                    }
+                }
+            }
         }
 
         if ($this->productVariations) {
-            $p['item_group_id'] = $this->getItemGroupId($product);
-            $p['group_id'] = $this->getItemGroupId($product);
-
             $p['variation_reference'] = $product['variation_reference'];
             $p['variation_supplier_reference'] = $product['variation_supplier_reference'];
             $p['variation_mpn'] = $product['variation_mpn'];
             $p['variation_ean13'] = $product['variation_ean13'];
             $p['variation_upc'] = $product['variation_upc'];
-            $p['df_group_leader'] = (!is_null($product['df_group_leader'] && $product['df_group_leader']) ? true : false);
-
+            $p['df_group_leader'] = (is_numeric($product['df_group_leader']) && 0 !== (int)$product['df_group_leader']);
+            $p['df_variants_information'] = $this->getVariantsInformation($product);
+            
             $attributes = $this->getAttributes($product);
 
             $p = array_merge($p, $attributes);
 
-            $p['df_variants_information'] = $this->getVariantsInformation($product);
+            foreach ($extraAttributesHeader as $extraAttributeHeader) {
+                if ('attributes' !== $extraAttributeHeader && !array_key_exists($extraAttributeHeader, $p) && array_key_exists($extraAttributeHeader, $attributes)) {
+                    $p[$extraAttributeHeader] = $attributes[$extraAttributeHeader];
+                    continue;
+                }
+            }
+        }
+
+        if ($this->showProductFeatures) {
+            $p['features'] = $this->getFeatures($product);
+        }
+        
+        foreach ($extraHeaders as $extraHeader) {
+            if (!empty($p[$extraHeader])) {
+                continue;
+            }
+            $p[$extraHeader] = isset($product[$extraHeader]) ? DfTools::cleanString($product[$extraHeader]) : '';
         }
 
         return $p;
     }
 
+    /**
+     * Applies specific transformations to a product's data for CSV export.
+     *
+     * This method performs several modifications on the product array, like:
+     * - If multi-price is enabled, formats the multiprice field.
+     * - Joins category values using a predefined separator.
+     * - If variants information exists, it slugifies and joins them using "%%".
+     * - Casts the group leader flag to an integer.
+     * - Iterates over extra headers to process attribute values:
+     *   - For each non-empty extra header, it concatenates key-value pairs in the format "key=value",
+     *     with the value cleaned and any "/" characters escaped.
+     * - Processes features (if present and an array) by converting them into an attributes string
+     *   formatted as "key=value" pairs joined with "/". The original features key is removed.
+     * - Ensures the final product fields are ordered according to the given headers.
+     *
+     * @param array $product      The associative array representing the product data.
+     * @param array $extraHeaders An array of additional headers to process in the product data.
+     * @param array $allHeaders   An array specifying the order of CSV fields.
+     *
+     * @return array The transformed product array ready for CSV export.
+     */
+    public function applySpecificTransformationsForCsv($product, $extraHeaders, $allHeaders)
+    {
+        if ($this->multipriceEnabled) {
+            $product['df_multiprice'] = DfTools::getFormattedMultiprice($product['df_multiprice']);
+        }
+        $product['categories'] = implode(DfTools::CATEGORY_SEPARATOR, $product['categories']);
+
+        if (array_key_exists('df_variants_information', $product)) {
+            $product['df_variants_information'] = implode('%%', array_map([__NAMESPACE__ . '\DfTools', 'slugify'], $product['df_variants_information']));
+        }
+
+        $product['df_group_leader'] = (int)$product['df_group_leader'];
+
+        foreach ($extraHeaders as $extraHeader) {
+            if (empty($product[$extraHeader])) {
+                continue;
+            }
+            $attributeValue = '';
+            foreach ($product[$extraHeader] as $index => $value) {
path: src/Entity/DfProductBuild.php
user: login: davidmolinacano
body: Estoy peleándome para reproducir este hook en local, pero no logro que me almacene los valores por referencia :thinking: Cuando lo tenga llegaré a una conclusión muy precisa
created_at: 2025-02-27T11:24:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,6 +44,17 @@ public function __construct($idShop, $idLang, $idCurrency)
         $this->idLang = $idLang;
         $this->idCurrency = $idCurrency;
         $this->currencies = \Currency::getCurrenciesByIdShop($idShop);
+        $this->attributesShown = \Configuration::get('DF_GROUP_ATTRIBUTES_SHOWN');
+        $this->displayPrices = \Configuration::get('DF_GS_DISPLAY_PRICES');
+        $this->imageSize = \Configuration::get('DF_GS_IMAGE_SIZE');
+        $this->link = \Context::getContext()->link;
+        $this->linkRewriteConf = \Configuration::get('PS_REWRITING_SETTINGS');
+        $this->productVariations = \Configuration::get('DF_SHOW_PRODUCT_VARIATIONS');
+        $this->showProductFeatures = \Configuration::get('DF_SHOW_PRODUCT_FEATURES');
+        $this->stockManagement = \Configuration::get('PS_STOCK_MANAGEMENT');
+        $this->useTax = \Configuration::get('DF_GS_PRICES_USE_TAX') || DoofinderConstants::NO;
path: src/Entity/DfProductBuild.php
user: login: ogomezba
body: Por entender también el motivo, por qué en este caso usamos un valor default pero en el resto de casos no lo añadimos? Update: He visto que simplemente se ha movido de la otra parte pero por si crees que realmente este default no hace falta :)
created_at: 2025-03-02T09:05:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -128,46 +114,153 @@ private function buildProduct($product)
         $p['extra_title_1'] = $p['title'];
         $p['extra_title_2'] = DfTools::splitReferences($p['title']);
         $p['tags'] = DfTools::cleanString($product['tags']);
-        $p['stock_quantity'] = DfTools::cleanString($product['stock_quantity']);
 
         if (DfTools::versionGte('1.7.0.0')) {
             $p['isbn'] = DfTools::cleanString($product['isbn']);
         }
 
+        $p['stock_quantity'] = DfTools::cleanString($product['stock_quantity']);
         if ($this->displayPrices) {
             $p['price'] = $this->getPrice($product);
             $p['sale_price'] = $this->getPrice($product, true);
 
             if ($this->multipriceEnabled) {
                 $p['df_multiprice'] = $this->getMultiprice($product);
             }
-        }
 
-        if ($this->showProductFeatures) {
-            $p = array_merge($p, $this->getFeatures($product));
+            if (DfTools::isParent($product) && array_key_exists($p['id'], $minPriceVariantByProductId)) {
+                $minVariant = $minPriceVariantByProductId[$p['id']];
+                if (
+                    !is_null($minVariant['onsale_price'])
+                    && !is_null($minVariant['price'])
+                    && (empty($p['sale_price']) || $minVariant['onsale_price'] < $p['sale_price'])
+                ) {
+                    $p['price'] = $minVariant['price'];
+                    $p['sale_price'] = ($minVariant['onsale_price'] === $minVariant['price']) ? null : $minVariant['onsale_price'];
+                    if ($this->multipriceEnabled) {
+                        $p['df_multiprice'] = $minVariant['multiprice'];
+                    }
+                }
+            }
         }
 
         if ($this->productVariations) {
-            $p['item_group_id'] = $this->getItemGroupId($product);
-            $p['group_id'] = $this->getItemGroupId($product);
-
             $p['variation_reference'] = $product['variation_reference'];
             $p['variation_supplier_reference'] = $product['variation_supplier_reference'];
             $p['variation_mpn'] = $product['variation_mpn'];
             $p['variation_ean13'] = $product['variation_ean13'];
             $p['variation_upc'] = $product['variation_upc'];
-            $p['df_group_leader'] = (!is_null($product['df_group_leader'] && $product['df_group_leader']) ? true : false);
-
+            $p['df_group_leader'] = (is_numeric($product['df_group_leader']) && 0 !== (int)$product['df_group_leader']);
+            $p['df_variants_information'] = $this->getVariantsInformation($product);
+            
             $attributes = $this->getAttributes($product);
 
             $p = array_merge($p, $attributes);
 
-            $p['df_variants_information'] = $this->getVariantsInformation($product);
+            foreach ($extraAttributesHeader as $extraAttributeHeader) {
+                if ('attributes' !== $extraAttributeHeader && !array_key_exists($extraAttributeHeader, $p) && array_key_exists($extraAttributeHeader, $attributes)) {
+                    $p[$extraAttributeHeader] = $attributes[$extraAttributeHeader];
+                }
+            }
+        }
+
+        if ($this->showProductFeatures) {
+            $p['features'] = $this->getFeatures($product);
+        }
+        
+        foreach ($extraHeaders as $extraHeader) {
+            if (!empty($p[$extraHeader])) {
+                continue;
+            }
+            $p[$extraHeader] = isset($product[$extraHeader]) ? DfTools::cleanString($product[$extraHeader]) : '';
         }
 
         return $p;
     }
 
+    /**
+     * Applies specific transformations to a product's data for CSV export.
+     *
+     * This method performs several modifications on the product array, like:
+     * - If multi-price is enabled, formats the multiprice field.
+     * - Joins category values using a predefined separator.
+     * - If variants information exists, it slugifies and joins them using "%%".
+     * - Casts the group leader flag to an integer.
+     * - Iterates over extra headers to process attribute values:
+     *   - For each non-empty extra header, it concatenates key-value pairs in the format "key=value",
+     *     with the value cleaned and any "/" characters escaped.
+     * - Processes features (if present and an array) by converting them into an attributes string
+     *   formatted as "key=value" pairs joined with "/". The original features key is removed.
+     * - Ensures the final product fields are ordered according to the given headers.
+     *
+     * @param array $product      The associative array representing the product data.
+     * @param array $extraHeaders An array of additional headers to process in the product data.
+     * @param array $allHeaders   An array specifying the order of CSV fields.
+     *
+     * @return array The transformed product array ready for CSV export.
+     */
+    public function applySpecificTransformationsForCsv($product, $extraHeaders, $allHeaders)
+    {
+        if ($this->multipriceEnabled) {
+            $product['df_multiprice'] = DfTools::getFormattedMultiprice($product['df_multiprice']);
+        }
+        $product['categories'] = implode(DfTools::CATEGORY_SEPARATOR, $product['categories']);
+
+        if (array_key_exists('df_variants_information', $product)) {
+            $product['df_variants_information'] = implode('%%', array_map([__NAMESPACE__ . '\DfTools', 'slugify'], $product['df_variants_information']));
+        }
+
+        $product['df_group_leader'] = (int)$product['df_group_leader'];
+
+        if (array_key_exists('features', $product) && is_array($product['features'])) {
+
+            $formattedAttributes = array_map(
+                function ($key, $value) {
+                    if (is_array($value)) {
+                        $keyValueToReturn = [];
+                        foreach ($value as $singleValue) {
+                            $keyValueToReturn[] = $key . '=' . $singleValue;
+                        }
+                        return implode('/', $keyValueToReturn);
+                    }
+                    return $key . '=' . $value;
+                },
+                array_keys($product['features']),
+                $product['features']
+            );
+
+            $product['attributes'] = implode('/', $formattedAttributes);
+            unset($product['features']);
+        }
+
+        $product = self::ensureCsvFieldsOrder($product, $allHeaders);
+        
+        return $product;
+    }
+
+    private static function ensureCsvFieldsOrder($product, $allHeaders)
+    {
+        $productWithSortedAttributes = array();
+        foreach ($allHeaders as $header) {
+            $productWithSortedAttributes[$header] = array_key_exists($header, $product) ? $product[$header] : "";
+        }
+
+        return $productWithSortedAttributes;
+    }
path: src/Entity/DfProductBuild.php
user: login: ogomezba
body: Es necesario que hagamos esto? Mis conocimientos de PHP son escasos pero si tenemos un array donde las claves son keys, entiendo que haciendo esto igual se ordena pero creo que quizá tendría más sentido hacerlo al generar el propio CSV? Es decir, la función que genera el CSV acepta tanto los productos como los headers y, dado que los headers son una lista de keys (ordenada), usamos ese hecho para ir pintando los valores (un poco la misma idea que aquí), sin necesidad de hacer un ordenamiento previo de las keys. No sé si tiene sentido lo que he dicho. Me da la sensación de que esto forma parte de la traducción al propio CSV, no de la generación de los productos.
created_at: 2025-03-02T09:53:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -128,46 +114,153 @@ private function buildProduct($product)
         $p['extra_title_1'] = $p['title'];
         $p['extra_title_2'] = DfTools::splitReferences($p['title']);
         $p['tags'] = DfTools::cleanString($product['tags']);
-        $p['stock_quantity'] = DfTools::cleanString($product['stock_quantity']);
 
         if (DfTools::versionGte('1.7.0.0')) {
             $p['isbn'] = DfTools::cleanString($product['isbn']);
         }
 
+        $p['stock_quantity'] = DfTools::cleanString($product['stock_quantity']);
         if ($this->displayPrices) {
             $p['price'] = $this->getPrice($product);
             $p['sale_price'] = $this->getPrice($product, true);
 
             if ($this->multipriceEnabled) {
                 $p['df_multiprice'] = $this->getMultiprice($product);
             }
-        }
 
-        if ($this->showProductFeatures) {
-            $p = array_merge($p, $this->getFeatures($product));
+            if (DfTools::isParent($product) && array_key_exists($p['id'], $minPriceVariantByProductId)) {
+                $minVariant = $minPriceVariantByProductId[$p['id']];
+                if (
+                    !is_null($minVariant['onsale_price'])
+                    && !is_null($minVariant['price'])
+                    && (empty($p['sale_price']) || $minVariant['onsale_price'] < $p['sale_price'])
+                ) {
+                    $p['price'] = $minVariant['price'];
+                    $p['sale_price'] = ($minVariant['onsale_price'] === $minVariant['price']) ? null : $minVariant['onsale_price'];
+                    if ($this->multipriceEnabled) {
+                        $p['df_multiprice'] = $minVariant['multiprice'];
+                    }
+                }
+            }
         }
 
         if ($this->productVariations) {
-            $p['item_group_id'] = $this->getItemGroupId($product);
-            $p['group_id'] = $this->getItemGroupId($product);
-
             $p['variation_reference'] = $product['variation_reference'];
             $p['variation_supplier_reference'] = $product['variation_supplier_reference'];
             $p['variation_mpn'] = $product['variation_mpn'];
             $p['variation_ean13'] = $product['variation_ean13'];
             $p['variation_upc'] = $product['variation_upc'];
-            $p['df_group_leader'] = (!is_null($product['df_group_leader'] && $product['df_group_leader']) ? true : false);
-
+            $p['df_group_leader'] = (is_numeric($product['df_group_leader']) && 0 !== (int)$product['df_group_leader']);
+            $p['df_variants_information'] = $this->getVariantsInformation($product);
+            
             $attributes = $this->getAttributes($product);
 
             $p = array_merge($p, $attributes);
 
-            $p['df_variants_information'] = $this->getVariantsInformation($product);
+            foreach ($extraAttributesHeader as $extraAttributeHeader) {
+                if ('attributes' !== $extraAttributeHeader && !array_key_exists($extraAttributeHeader, $p) && array_key_exists($extraAttributeHeader, $attributes)) {
+                    $p[$extraAttributeHeader] = $attributes[$extraAttributeHeader];
+                }
+            }
+        }
+
+        if ($this->showProductFeatures) {
+            $p['features'] = $this->getFeatures($product);
+        }
+        
+        foreach ($extraHeaders as $extraHeader) {
+            if (!empty($p[$extraHeader])) {
+                continue;
+            }
+            $p[$extraHeader] = isset($product[$extraHeader]) ? DfTools::cleanString($product[$extraHeader]) : '';
         }
 
         return $p;
     }
 
+    /**
+     * Applies specific transformations to a product's data for CSV export.
+     *
+     * This method performs several modifications on the product array, like:
+     * - If multi-price is enabled, formats the multiprice field.
+     * - Joins category values using a predefined separator.
+     * - If variants information exists, it slugifies and joins them using "%%".
+     * - Casts the group leader flag to an integer.
+     * - Iterates over extra headers to process attribute values:
+     *   - For each non-empty extra header, it concatenates key-value pairs in the format "key=value",
+     *     with the value cleaned and any "/" characters escaped.
+     * - Processes features (if present and an array) by converting them into an attributes string
+     *   formatted as "key=value" pairs joined with "/". The original features key is removed.
+     * - Ensures the final product fields are ordered according to the given headers.
+     *
+     * @param array $product      The associative array representing the product data.
+     * @param array $extraHeaders An array of additional headers to process in the product data.
+     * @param array $allHeaders   An array specifying the order of CSV fields.
+     *
+     * @return array The transformed product array ready for CSV export.
+     */
+    public function applySpecificTransformationsForCsv($product, $extraHeaders, $allHeaders)
+    {
+        if ($this->multipriceEnabled) {
+            $product['df_multiprice'] = DfTools::getFormattedMultiprice($product['df_multiprice']);
+        }
+        $product['categories'] = implode(DfTools::CATEGORY_SEPARATOR, $product['categories']);
+
+        if (array_key_exists('df_variants_information', $product)) {
+            $product['df_variants_information'] = implode('%%', array_map([__NAMESPACE__ . '\DfTools', 'slugify'], $product['df_variants_information']));
+        }
+
+        $product['df_group_leader'] = (int)$product['df_group_leader'];
+
+        if (array_key_exists('features', $product) && is_array($product['features'])) {
+
+            $formattedAttributes = array_map(
+                function ($key, $value) {
+                    if (is_array($value)) {
+                        $keyValueToReturn = [];
+                        foreach ($value as $singleValue) {
+                            $keyValueToReturn[] = $key . '=' . $singleValue;
+                        }
+                        return implode('/', $keyValueToReturn);
+                    }
+                    return $key . '=' . $value;
+                },
+                array_keys($product['features']),
+                $product['features']
+            );
path: src/Entity/DfProductBuild.php
user: login: ogomezba
body: Buenas David! Quizá porque no estoy tan familiarizado con las funciones de PHP me había costado entender más esto. Dejo la versión con `foreach` por si crees que, ya que tratamos con key => value's, la hace más seguible de primeras. Si no, veo bien la versión que hay ahora :).

```suggestion
$formattedAttributes = [];

foreach ($product['features'] as $key => $value) {
    if (is_array($value)) {
        $keyValueToReturn = [];
        foreach ($value as $singleValue) {
            $keyValueToReturn[] = $key . '=' . $singleValue;
        }
        $formattedAttributes[] = implode('/', $keyValueToReturn);
    } else {
        $formattedAttributes[] = $key . '=' . $value;
    }
}
```
created_at: 2025-03-02T21:13:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -193,403 +162,125 @@ function arrayMergeByIdProduct($array1 = [], $array2 = [])
 
 $header[] = 'stock_quantity';
 
-if ($cfg_display_prices) {
+if ($shouldDisplayPrices) {
     $header[] = 'price';
     $header[] = 'sale_price';
-    $header[] = 'df_multiprice';
+
+    if ($isMultipriceEnabled) {
+        $header[] = 'df_multiprice';
+    }
 }
 
-if ($cfg_product_variations == 1) {
+$additionalAttributesHeaders = [];
+
+if (1 === $shouldShowProductVariations) {
     $header[] = 'variation_reference';
     $header[] = 'variation_supplier_reference';
     $header[] = 'variation_mpn';
     $header[] = 'variation_ean13';
     $header[] = 'variation_upc';
     $header[] = 'df_group_leader';
     $header[] = 'df_variants_information';
-    $attribute_keys = DfTools::getAttributeKeysForShopAndLang($shop->id, $lang->id);
-    $alt_attribute_keys = [];
-    foreach ($attribute_keys as $key) {
-        $header_value = slugify($key);
-        if ($limit_group_attributes && !in_array($header_value, $group_attributes_slug)) {
+    $attributeKeys = DfTools::getAttributeKeysForShopAndLang($shop->id, $lang->id);
+    $altAttributeKeys = [];
+
+    foreach ($attributeKeys as $key) {
+        $headerValue = DfTools::slugify($key);
+        if ($shouldLimitGroupAttributes && !in_array($headerValue, $groupAttributesSlug)) {
             continue;
         }
-        $alt_attribute_keys[] = $key;
-        $header[] = $header_value;
+        $altAttributeKeys[] = $key;
+        $additionalAttributesHeaders[] = $headerValue;
     }
-    $attribute_keys = $alt_attribute_keys;
+    $attributeKeys = $altAttributeKeys;
 }
 
-if ($cfg_product_features) {
-    $all_feature_keys = DfTools::getFeatureKeysForShopAndLang($shop->id, $lang->id);
+if ($shouldShowProductFeatures) {
+    $allFeatureKeys = DfTools::getFeatureKeysForShopAndLang($shop->id, $lang->id);
 
     if (
-        is_array($cfg_features_shown)
-        && count($cfg_features_shown) > 0
-        && $cfg_features_shown[0] !== ''
+        is_array($featuresShownArray)
+        && count($featuresShownArray) > 0
+        && $featuresShownArray[0] !== ''
     ) {
-        $feature_keys = DfTools::getSelectedFeatures($all_feature_keys, $cfg_features_shown);
+        $featurekeys = DfTools::getSelectedFeatures($allFeatureKeys, $featuresShownArray);
     } else {
-        $feature_keys = $all_feature_keys;
+        $featurekeys = $allFeatureKeys;
     }
-    $header[] = 'attributes';
+    $additionalAttributesHeaders[] = 'attributes';
 }
 
+$header = array_merge($header, $additionalAttributesHeaders);
+
 /**
  * @author camlafit <https://github.com/camlafit>
- * Extend doofinder feed
+ * Allows users to extend Doofinder feed by adding extra headers and extra rows.
+ * The hook can be defined in a different custom module or directly in the theme PHP files.
+ * Don't forget to register the hook via registerHook method:
+ *
+ * `$this->registerHook('actionDoofinderExtendFeed');`
+ *
+ * Example of the function:
  *
- * To add an new header, module can do an array_merge on $extra_header
- * To add an new data to a product, module must create a multidemensionnal array as this :
+ * public function hookActionDoofinderExtendFeed($params)
+ * {
+ *     $params['extra_header'] = ['header_custom1', 'header_custom2'];
+ *     $params['extra_rows'] = [
+ *         [
+ *             'id_product' => 1,
+ *             'header_custom1' => 'value1'
+ *         ],
+ *         [
+ *             'id_product' => 2,
+ *             'header_custom2' => 'value2'
+ *         ]
+ *     ];
+ * }
+ *
+ *
+ * To add an new header, module can do an array_merge on $extraHeader
+ * To add an new data to a product, module must create a multidimensionnal array as this :
  * array(
  *   index => array(
  *    'id_product' => value,
  *    'new_header_column_name' => 'value related to the new column'
  *   ),
  *   [...]
  * )
- * As each module can extend $extra_header and $extra_rows don't forget to merge them
+ * As each module can extend $extraHeader and $extraRows don't forget to merge them
  */
-$extra_header = [];
-$extra_rows = [];
+$extraHeader = [];
+$extraRows = [];
 Hook::exec('actionDoofinderExtendFeed', [
-    'extra_header' => &$extra_header,
-    'extra_rows' => &$extra_rows,
+    'extra_header' => &$extraHeader,
+    'extra_rows' => &$extraRows,
     'id_lang' => $lang->id,
     'id_shop' => $shop->id,
     'limit' => $limit,
     'offset' => $offset,
 ]);
 
-$header = array_merge($header, $extra_header);
-
-if (!$limit || ($offset !== false && (int) $offset === 0)) {
-    echo implode(DfTools::TXT_SEPARATOR, $header) . PHP_EOL;
-}
+$header = array_merge($header, $extraHeader);
 
 // PRODUCTS
 $rows = DfTools::getAvailableProductsForLanguage($lang->id, $shop->id, $limit, $offset);
 
-$rows = arrayMergeByIdProduct($rows, $extra_rows);
+$rows = arrayMergeByIdProduct($rows, $extraRows);
 
 // In case there is no need to display prices, avoid calculating the mins by variant
-$min_price_variant_by_product_id = ($cfg_product_variations && $cfg_display_prices) ? DfTools::getMinVariantPrices($rows, $cfg_prices_w_taxes, $currencies, $lang->id, $shop->id) : [];
+$minPriceVariantByProductId = (1 === $shouldShowProductVariations && $shouldDisplayPrices) ? DfTools::getMinVariantPrices($rows, $shouldPricesUseTaxes, $currencies, $lang->id, $shop->id) : [];
+$additionalHeaders = array_merge($additionalAttributesHeaders, $extraHeader);
 
-foreach ($rows as $row) {
-    $product_id = $row['id_product'];
-    if (!$cfg_product_variations || DfTools::isParent($row)) {
-        $variant_id = null;
-    } else {
-        $variant_id = $row['id_product_attribute'];
-    }
-    $product_price = DfTools::getPrice($product_id, $cfg_prices_w_taxes, $variant_id);
-    $onsale_price = DfTools::getOnsalePrice($product_id, $cfg_prices_w_taxes, $variant_id);
-    $multiprice = DfTools::getFormattedMultiprice($product_id, $cfg_prices_w_taxes, $currencies, $variant_id);
-
-    if ((int) $row['id_product'] > 0) {
-        // ID, TITLE, LINK
-
-        if (
-            $cfg_product_variations == 1
-            && isset($row['id_product_attribute'])
-            && (int) $row['id_product_attribute'] > 0
-        ) {
-            // ID
-            echo 'VAR-' . $row['id_product_attribute'] . DfTools::TXT_SEPARATOR;
-
-            // ITEM-GROUP-ID
-            echo $row['id_product'] . DfTools::TXT_SEPARATOR;
-            // TITLE
-            $product_title = DfTools::cleanString($row['name']);
-            echo $product_title . DfTools::TXT_SEPARATOR;
-            echo DfTools::cleanURL(
-                $context->link->getProductLink(
-                    (int) $row['id_product'],
-                    $row['link_rewrite'],
-                    $row['cat_link_rew'],
-                    $row['ean13'],
-                    $lang->id,
-                    $shop->id,
-                    (int) $row['id_product_attribute'],
-                    $cfg_mod_rewrite,
-                    false,
-                    true
-                )
-            ) . DfTools::TXT_SEPARATOR;
-        } else {
-            $eanLink = $row['ean13'];
-            // ID
-            echo $row['id_product'] . DfTools::TXT_SEPARATOR;
-
-            if ($cfg_product_variations == 1) {
-                // ITEM-GROUP-ID
-                echo '' . DfTools::TXT_SEPARATOR;
-            }
+$dfProductBuild = new DfProductBuild($shop->id, $lang->id, $currency->id);
 
-            // TITLE
-            $product_title = DfTools::cleanString($row['name']);
-            echo $product_title . DfTools::TXT_SEPARATOR;
-
-            $parent_url = DfTools::cleanURL(
-                $context->link->getProductLink(
-                    (int) $row['id_product'],
-                    $row['link_rewrite'],
-                    $row['cat_link_rew'],
-                    $eanLink,
-                    $lang->id,
-                    $shop->id,
-                    0,
-                    $cfg_mod_rewrite
-                )
-            );
-
-            if (key_exists($product_id, $min_price_variant_by_product_id) && !empty($min_price_variant_by_product_id[$product_id])) {
-                $min_variant = $min_price_variant_by_product_id[$product_id];
-                echo $min_variant['onsale_price'] < $onsale_price ? $min_price_variant_by_product_id[$product_id]['link'] : $parent_url;
-            } else {
-                echo $parent_url;
-            }
-            echo DfTools::TXT_SEPARATOR;
-        }
-
-        // DESCRIPTION
-        echo DfTools::cleanString(
-            $row[$cfg_short_description ? 'description_short' : 'description']
-        ) . DfTools::TXT_SEPARATOR;
-
-        // ALTERNATE DESCRIPTION
-        echo DfTools::cleanString(
-            $row[$cfg_short_description ? 'description' : 'description_short']
-        ) . DfTools::TXT_SEPARATOR;
-
-        // META TITLE
-        echo DfTools::cleanString($row['meta_title']) . DfTools::TXT_SEPARATOR;
-
-        // META DESCRIPTION
-        echo DfTools::cleanString($row['meta_description']) . DfTools::TXT_SEPARATOR;
-
-        // IMAGE LINK
-
-        if (
-            $cfg_product_variations == 1 && isset($row['id_product_attribute'])
-            && (int) $row['id_product_attribute'] > 0
-        ) {
-            $cover = Product::getCover($row['id_product_attribute']);
-            $id_image = DfTools::getVariationImg(
-                $row['id_product'],
-                $row['id_product_attribute']
-            );
-
-            if (!empty($id_image)) {
-                $image_link = DfTools::cleanURL(
-                    DfTools::getImageLink(
-                        $row['id_product_attribute'],
-                        $id_image,
-                        $row['link_rewrite'],
-                        $cfg_image_size
-                    )
-                );
-            } else {
-                $image_link = DfTools::cleanURL(
-                    DfTools::getImageLink(
-                        $row['id_product_attribute'],
-                        $row['id_image'],
-                        $row['link_rewrite'],
-                        $cfg_image_size
-                    )
-                );
-            }
-
-            // For variations with no specific pictures
-            if (strpos($image_link, '/-') > -1) {
-                $image_link = DfTools::cleanURL(
-                    DfTools::getImageLink(
-                        $row['id_product'],
-                        $row['id_image'],
-                        $row['link_rewrite'],
-                        $cfg_image_size
-                    )
-                );
-            }
-
-            echo $image_link . DfTools::TXT_SEPARATOR;
-        } else {
-            echo DfTools::cleanURL(
-                DfTools::getImageLink(
-                    $row['id_product'],
-                    $row['id_image'],
-                    $row['link_rewrite'],
-                    $cfg_image_size
-                )
-            ) . DfTools::TXT_SEPARATOR;
-        }
-
-        // MAIN CATEGORY
-        echo DfTools::cleanString($row['main_category']) . DfTools::TXT_SEPARATOR;
-
-        // PRODUCT CATEGORIES
-        echo DfTools::getCategoriesForProductIdAndLanguage(
-            $row['id_product'],
-            $lang->id,
-            $shop->id
-        ) . DfTools::TXT_SEPARATOR;
-
-        // AVAILABILITY
-        $available = (int) $row['available_for_order'] > 0;
-
-        if ((int) DfTools::cfg($shop->id, 'PS_STOCK_MANAGEMENT')) {
-            $stock = StockAvailable::getQuantityAvailableByProduct(
-                $row['id_product'],
-                isset($row['id_product_attribute']) ? $row['id_product_attribute'] : null,
-                $shop->id
-            );
-            $allow_oosp = Product::isAvailableWhenOutOfStock($row['out_of_stock']);
-            echo ($available && ($stock > 0 || $allow_oosp) ? 'in stock' : 'out of stock') . DfTools::TXT_SEPARATOR;
-        } else {
-            echo ($available ? 'in stock' : 'out of stock') . DfTools::TXT_SEPARATOR;
-        }
-
-        // BRAND
-        echo DfTools::cleanString($row['manufacturer']) . DfTools::TXT_SEPARATOR;
-
-        // MPN
-        echo DfTools::cleanString($row['mpn']) . DfTools::TXT_SEPARATOR;
-
-        // EAN13
-        echo DfTools::cleanString($row['ean13']) . DfTools::TXT_SEPARATOR;
-
-        // UPC
-        echo DfTools::cleanString($row['upc']) . DfTools::TXT_SEPARATOR;
-
-        // REFERENCE
-        echo DfTools::cleanString($row['reference']) . DfTools::TXT_SEPARATOR;
-
-        // SUPPLIER_REFERENCE
-        echo DfTools::cleanString($row['supplier_reference']) . DfTools::TXT_SEPARATOR;
-
-        // EXTRA_TITLE_1
-        echo $product_title . DfTools::TXT_SEPARATOR;
-
-        // EXTRA_TITLE_2
-        echo DfTools::splitReferences($product_title) . DfTools::TXT_SEPARATOR;
-
-        // TAGS
-        echo DfTools::escapeSlashes(DfTools::cleanString(DfTools::escapeSlashes($row['tags'])));
-
-        // ISBN
-        if (DfTools::versionGte('1.7.0.0')) {
-            echo DfTools::TXT_SEPARATOR;
-            echo DfTools::cleanString($row['isbn']);
-        }
-
-        // STOCK_QUANTITY
-        echo DfTools::TXT_SEPARATOR;
-        echo DfTools::cleanString($row['stock_quantity']);
-
-        // PRODUCT PRICE & ON SALE PRICE
-
-        if ($cfg_display_prices && $cfg_product_variations !== 1) {
-            echo DfTools::TXT_SEPARATOR;
-
-            $product_price = DfTools::getPrice($product_id, $cfg_prices_w_taxes);
-            $onsale_price = DfTools::getOnsalePrice($product_id, $cfg_prices_w_taxes);
-            $multiprice = DfTools::getFormattedMultiprice($product_id, $cfg_prices_w_taxes, $currencies);
-
-            if ($row['show_price']) {
-                echo Tools::convertPrice($product_price, $currency);
-                echo DfTools::TXT_SEPARATOR;
-                echo $product_price != $onsale_price ? Tools::convertPrice($onsale_price, $currency) : '';
-                echo DfTools::TXT_SEPARATOR;
-                echo $multiprice_enabled && $multiprice ? $multiprice : '';
-            } else {
-                echo DfTools::TXT_SEPARATOR . DfTools::TXT_SEPARATOR;
-            }
-        } elseif ($cfg_display_prices && $cfg_product_variations == 1) {
-            echo DfTools::TXT_SEPARATOR;
-            // The parent product should have as price the lowest ones of the
-            // variants (combinations) if there are any
-            if (DfTools::isParent($row) && array_key_exists($product_id, $min_price_variant_by_product_id)) {
-                $min_variant = $min_price_variant_by_product_id[$product_id];
-
-                if (
-                    !is_null($min_variant['onsale_price'])
-                    && !is_null($min_variant['price'])
-                    && (empty($onsale_price) || $min_variant['onsale_price'] < $onsale_price)
-                ) {
-                    $product_price = $min_variant['price'];
-                    $onsale_price = $min_variant['onsale_price'];
-                    $multiprice = $min_variant['multiprice'];
-                }
-            }
-
-            if ($row['show_price']) {
-                echo Tools::convertPrice($product_price, $currency);
-                echo DfTools::TXT_SEPARATOR;
-                echo $product_price != $onsale_price ? Tools::convertPrice($onsale_price, $currency) : '';
-                echo DfTools::TXT_SEPARATOR;
-                echo $multiprice_enabled && $multiprice ? $multiprice : '';
-            } else {
-                echo DfTools::TXT_SEPARATOR . DfTools::TXT_SEPARATOR;
-            }
-        }
-
-        if ($cfg_product_variations == 1) {
-            echo DfTools::TXT_SEPARATOR;
-            echo DfTools::cleanString($row['variation_reference']);
-            echo DfTools::TXT_SEPARATOR;
-            echo DfTools::cleanString($row['variation_supplier_reference']);
-            echo DfTools::TXT_SEPARATOR;
-            echo DfTools::cleanString($row['variation_mpn']);
-            echo DfTools::TXT_SEPARATOR;
-            echo DfTools::cleanString($row['variation_ean13']);
-            echo DfTools::TXT_SEPARATOR;
-            echo DfTools::cleanString($row['variation_upc']);
-            echo DfTools::TXT_SEPARATOR;
-            echo DfTools::cleanString($row['df_group_leader']);
-            $variation_attributes = DfTools::getAttributesForProductVariation(
-                $row['id_product_attribute'],
-                $lang->id,
-                $attribute_keys
-            );
-            echo DfTools::TXT_SEPARATOR;
-            if (DfTools::hasAttributes($row['id_product']) && !$row['id_product_attribute']) {
-                $product_attributes = DfTools::hasProductAttributes($row['id_product'], DfTools::cfg($shop->id, 'DF_GROUP_ATTRIBUTES_SHOWN'));
-                if ($product_attributes) {
-                    $attributes = DfTools::getAttributesName($product_attributes, $lang->id);
-
-                    if (is_array($attributes)) {
-                        $variants_keys = array_column($attributes, 'name');
-                        echo implode('%%', array_map('slugify', $variants_keys));
-                    } else {
-                        echo '';
-                    }
-                } else {
-                    echo '';
-                }
-            }
-            foreach ($variation_attributes as $attribute) {
-                echo DfTools::TXT_SEPARATOR . str_replace('/', '//', DfTools::cleanString($attribute));
-            }
-        }
-
-        if ($cfg_product_features) {
-            echo DfTools::TXT_SEPARATOR;
-            foreach (DfTools::getFeaturesForProduct($row['id_product'], $lang->id, $feature_keys) as $key => $values) {
-                foreach ($values as $index => $value) {
-                    echo slugify($key) . '=';
-                    echo str_replace('/', '\/', DfTools::cleanString($value)) . '/';
-                }
-            }
-        }
-
-        /*
-         * @author camlafit <https://github.com/camlafit>
-         */
-        foreach ($extra_header as $extra) {
-            echo DfTools::TXT_SEPARATOR;
-            echo isset($row[$extra]) ? DfTools::cleanString($row[$extra]) : '';
-        }
+$csv = fopen('php://output', 'w');
+if (!$limit || (false !== $offset && 0 === (int) $offset)) {
+    fputcsv($csv, $header, DfTools::TXT_SEPARATOR);
+}
 
-        echo PHP_EOL;
-    }
+foreach ($rows as $row) {
+    $product = $dfProductBuild->buildProduct($row, $minPriceVariantByProductId, $additionalAttributesHeaders, $additionalHeaders);
+    $product = $dfProductBuild->applySpecificTransformationsForCsv($product, $extraHeader, $header);
+    fputcsv($csv, $product, DfTools::TXT_SEPARATOR);
path: feeds/product.php
user: login: ogomezba
body: Lo que te comentaba en el otro archivo David. Aquí por ejemplo, justo antes, se podría usar el array de headers para generar la lista de valores a añadir al CSV.
created_at: 2025-03-02T21:40:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -128,46 +114,153 @@ private function buildProduct($product)
         $p['extra_title_1'] = $p['title'];
         $p['extra_title_2'] = DfTools::splitReferences($p['title']);
         $p['tags'] = DfTools::cleanString($product['tags']);
-        $p['stock_quantity'] = DfTools::cleanString($product['stock_quantity']);
 
         if (DfTools::versionGte('1.7.0.0')) {
             $p['isbn'] = DfTools::cleanString($product['isbn']);
         }
 
+        $p['stock_quantity'] = DfTools::cleanString($product['stock_quantity']);
         if ($this->displayPrices) {
             $p['price'] = $this->getPrice($product);
             $p['sale_price'] = $this->getPrice($product, true);
 
             if ($this->multipriceEnabled) {
                 $p['df_multiprice'] = $this->getMultiprice($product);
             }
-        }
 
-        if ($this->showProductFeatures) {
-            $p = array_merge($p, $this->getFeatures($product));
+            if (DfTools::isParent($product) && array_key_exists($p['id'], $minPriceVariantByProductId)) {
+                $minVariant = $minPriceVariantByProductId[$p['id']];
+                if (
+                    !is_null($minVariant['onsale_price'])
+                    && !is_null($minVariant['price'])
+                    && (empty($p['sale_price']) || $minVariant['onsale_price'] < $p['sale_price'])
+                ) {
+                    $p['price'] = $minVariant['price'];
+                    $p['sale_price'] = ($minVariant['onsale_price'] === $minVariant['price']) ? null : $minVariant['onsale_price'];
+                    if ($this->multipriceEnabled) {
+                        $p['df_multiprice'] = $minVariant['multiprice'];
+                    }
+                }
+            }
         }
 
         if ($this->productVariations) {
-            $p['item_group_id'] = $this->getItemGroupId($product);
-            $p['group_id'] = $this->getItemGroupId($product);
-
             $p['variation_reference'] = $product['variation_reference'];
             $p['variation_supplier_reference'] = $product['variation_supplier_reference'];
             $p['variation_mpn'] = $product['variation_mpn'];
             $p['variation_ean13'] = $product['variation_ean13'];
             $p['variation_upc'] = $product['variation_upc'];
-            $p['df_group_leader'] = (!is_null($product['df_group_leader'] && $product['df_group_leader']) ? true : false);
-
+            $p['df_group_leader'] = (is_numeric($product['df_group_leader']) && 0 !== (int)$product['df_group_leader']);
+            $p['df_variants_information'] = $this->getVariantsInformation($product);
+            
             $attributes = $this->getAttributes($product);
 
             $p = array_merge($p, $attributes);
 
-            $p['df_variants_information'] = $this->getVariantsInformation($product);
+            foreach ($extraAttributesHeader as $extraAttributeHeader) {
+                if ('attributes' !== $extraAttributeHeader && !array_key_exists($extraAttributeHeader, $p) && array_key_exists($extraAttributeHeader, $attributes)) {
+                    $p[$extraAttributeHeader] = $attributes[$extraAttributeHeader];
+                }
+            }
+        }
+
+        if ($this->showProductFeatures) {
+            $p['features'] = $this->getFeatures($product);
+        }
+        
+        foreach ($extraHeaders as $extraHeader) {
+            if (!empty($p[$extraHeader])) {
+                continue;
+            }
+            $p[$extraHeader] = isset($product[$extraHeader]) ? DfTools::cleanString($product[$extraHeader]) : '';
         }
 
         return $p;
     }
 
+    /**
+     * Applies specific transformations to a product's data for CSV export.
+     *
+     * This method performs several modifications on the product array, like:
+     * - If multi-price is enabled, formats the multiprice field.
+     * - Joins category values using a predefined separator.
+     * - If variants information exists, it slugifies and joins them using "%%".
+     * - Casts the group leader flag to an integer.
+     * - Iterates over extra headers to process attribute values:
+     *   - For each non-empty extra header, it concatenates key-value pairs in the format "key=value",
+     *     with the value cleaned and any "/" characters escaped.
+     * - Processes features (if present and an array) by converting them into an attributes string
+     *   formatted as "key=value" pairs joined with "/". The original features key is removed.
+     * - Ensures the final product fields are ordered according to the given headers.
+     *
+     * @param array $product      The associative array representing the product data.
+     * @param array $extraHeaders An array of additional headers to process in the product data.
+     * @param array $allHeaders   An array specifying the order of CSV fields.
+     *
+     * @return array The transformed product array ready for CSV export.
+     */
+    public function applySpecificTransformationsForCsv($product, $extraHeaders, $allHeaders)
+    {
+        if ($this->multipriceEnabled) {
+            $product['df_multiprice'] = DfTools::getFormattedMultiprice($product['df_multiprice']);
+        }
+        $product['categories'] = implode(DfTools::CATEGORY_SEPARATOR, $product['categories']);
+
+        if (array_key_exists('df_variants_information', $product)) {
+            $product['df_variants_information'] = implode('%%', array_map([__NAMESPACE__ . '\DfTools', 'slugify'], $product['df_variants_information']));
+        }
+
+        $product['df_group_leader'] = (int)$product['df_group_leader'];
+
+        if (array_key_exists('features', $product) && is_array($product['features'])) {
+
+            $formattedAttributes = array_map(
+                function ($key, $value) {
+                    if (is_array($value)) {
+                        $keyValueToReturn = [];
+                        foreach ($value as $singleValue) {
+                            $keyValueToReturn[] = $key . '=' . $singleValue;
+                        }
+                        return implode('/', $keyValueToReturn);
+                    }
+                    return $key . '=' . $value;
+                },
+                array_keys($product['features']),
+                $product['features']
+            );
+
+            $product['attributes'] = implode('/', $formattedAttributes);
+            unset($product['features']);
+        }
+
+        $product = self::ensureCsvFieldsOrder($product, $allHeaders);
+        
+        return $product;
+    }
+
+    private static function ensureCsvFieldsOrder($product, $allHeaders)
+    {
+        $productWithSortedAttributes = array();
+        foreach ($allHeaders as $header) {
+            $productWithSortedAttributes[$header] = array_key_exists($header, $product) ? $product[$header] : "";
+        }
+
+        return $productWithSortedAttributes;
+    }
path: src/Entity/DfProductBuild.php
user: login: davidmolinacano
body: Es correcto, es de la generación del CSV, de hecho por eso había llamado a la función ensureCsvFieldsOrder.
created_at: 2025-03-03T07:31:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,6 +44,17 @@ public function __construct($idShop, $idLang, $idCurrency)
         $this->idLang = $idLang;
         $this->idCurrency = $idCurrency;
         $this->currencies = \Currency::getCurrenciesByIdShop($idShop);
+        $this->attributesShown = \Configuration::get('DF_GROUP_ATTRIBUTES_SHOWN');
+        $this->displayPrices = \Configuration::get('DF_GS_DISPLAY_PRICES');
+        $this->imageSize = \Configuration::get('DF_GS_IMAGE_SIZE');
+        $this->link = \Context::getContext()->link;
+        $this->linkRewriteConf = \Configuration::get('PS_REWRITING_SETTINGS');
+        $this->productVariations = \Configuration::get('DF_SHOW_PRODUCT_VARIATIONS');
+        $this->showProductFeatures = \Configuration::get('DF_SHOW_PRODUCT_FEATURES');
+        $this->stockManagement = \Configuration::get('PS_STOCK_MANAGEMENT');
+        $this->useTax = \Configuration::get('DF_GS_PRICES_USE_TAX') || DoofinderConstants::NO;
path: src/Entity/DfProductBuild.php
user: login: davidmolinacano
body: Diría que no, lo quito entonces :raised_hands: 
created_at: 2025-03-03T07:37:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -128,46 +114,153 @@ private function buildProduct($product)
         $p['extra_title_1'] = $p['title'];
         $p['extra_title_2'] = DfTools::splitReferences($p['title']);
         $p['tags'] = DfTools::cleanString($product['tags']);
-        $p['stock_quantity'] = DfTools::cleanString($product['stock_quantity']);
 
         if (DfTools::versionGte('1.7.0.0')) {
             $p['isbn'] = DfTools::cleanString($product['isbn']);
         }
 
+        $p['stock_quantity'] = DfTools::cleanString($product['stock_quantity']);
         if ($this->displayPrices) {
             $p['price'] = $this->getPrice($product);
             $p['sale_price'] = $this->getPrice($product, true);
 
             if ($this->multipriceEnabled) {
                 $p['df_multiprice'] = $this->getMultiprice($product);
             }
-        }
 
-        if ($this->showProductFeatures) {
-            $p = array_merge($p, $this->getFeatures($product));
+            if (DfTools::isParent($product) && array_key_exists($p['id'], $minPriceVariantByProductId)) {
+                $minVariant = $minPriceVariantByProductId[$p['id']];
+                if (
+                    !is_null($minVariant['onsale_price'])
+                    && !is_null($minVariant['price'])
+                    && (empty($p['sale_price']) || $minVariant['onsale_price'] < $p['sale_price'])
+                ) {
+                    $p['price'] = $minVariant['price'];
+                    $p['sale_price'] = ($minVariant['onsale_price'] === $minVariant['price']) ? null : $minVariant['onsale_price'];
+                    if ($this->multipriceEnabled) {
+                        $p['df_multiprice'] = $minVariant['multiprice'];
+                    }
+                }
+            }
         }
 
         if ($this->productVariations) {
-            $p['item_group_id'] = $this->getItemGroupId($product);
-            $p['group_id'] = $this->getItemGroupId($product);
-
             $p['variation_reference'] = $product['variation_reference'];
             $p['variation_supplier_reference'] = $product['variation_supplier_reference'];
             $p['variation_mpn'] = $product['variation_mpn'];
             $p['variation_ean13'] = $product['variation_ean13'];
             $p['variation_upc'] = $product['variation_upc'];
-            $p['df_group_leader'] = (!is_null($product['df_group_leader'] && $product['df_group_leader']) ? true : false);
-
+            $p['df_group_leader'] = (is_numeric($product['df_group_leader']) && 0 !== (int)$product['df_group_leader']);
+            $p['df_variants_information'] = $this->getVariantsInformation($product);
+            
             $attributes = $this->getAttributes($product);
 
             $p = array_merge($p, $attributes);
 
-            $p['df_variants_information'] = $this->getVariantsInformation($product);
+            foreach ($extraAttributesHeader as $extraAttributeHeader) {
+                if ('attributes' !== $extraAttributeHeader && !array_key_exists($extraAttributeHeader, $p) && array_key_exists($extraAttributeHeader, $attributes)) {
+                    $p[$extraAttributeHeader] = $attributes[$extraAttributeHeader];
+                }
+            }
+        }
+
+        if ($this->showProductFeatures) {
+            $p['features'] = $this->getFeatures($product);
+        }
+        
+        foreach ($extraHeaders as $extraHeader) {
+            if (!empty($p[$extraHeader])) {
+                continue;
+            }
+            $p[$extraHeader] = isset($product[$extraHeader]) ? DfTools::cleanString($product[$extraHeader]) : '';
         }
 
         return $p;
     }
 
+    /**
+     * Applies specific transformations to a product's data for CSV export.
+     *
+     * This method performs several modifications on the product array, like:
+     * - If multi-price is enabled, formats the multiprice field.
+     * - Joins category values using a predefined separator.
+     * - If variants information exists, it slugifies and joins them using "%%".
+     * - Casts the group leader flag to an integer.
+     * - Iterates over extra headers to process attribute values:
+     *   - For each non-empty extra header, it concatenates key-value pairs in the format "key=value",
+     *     with the value cleaned and any "/" characters escaped.
+     * - Processes features (if present and an array) by converting them into an attributes string
+     *   formatted as "key=value" pairs joined with "/". The original features key is removed.
+     * - Ensures the final product fields are ordered according to the given headers.
+     *
+     * @param array $product      The associative array representing the product data.
+     * @param array $extraHeaders An array of additional headers to process in the product data.
+     * @param array $allHeaders   An array specifying the order of CSV fields.
+     *
+     * @return array The transformed product array ready for CSV export.
+     */
+    public function applySpecificTransformationsForCsv($product, $extraHeaders, $allHeaders)
+    {
+        if ($this->multipriceEnabled) {
+            $product['df_multiprice'] = DfTools::getFormattedMultiprice($product['df_multiprice']);
+        }
+        $product['categories'] = implode(DfTools::CATEGORY_SEPARATOR, $product['categories']);
+
+        if (array_key_exists('df_variants_information', $product)) {
+            $product['df_variants_information'] = implode('%%', array_map([__NAMESPACE__ . '\DfTools', 'slugify'], $product['df_variants_information']));
+        }
+
+        $product['df_group_leader'] = (int)$product['df_group_leader'];
+
+        if (array_key_exists('features', $product) && is_array($product['features'])) {
+
+            $formattedAttributes = array_map(
+                function ($key, $value) {
+                    if (is_array($value)) {
+                        $keyValueToReturn = [];
+                        foreach ($value as $singleValue) {
+                            $keyValueToReturn[] = $key . '=' . $singleValue;
+                        }
+                        return implode('/', $keyValueToReturn);
+                    }
+                    return $key . '=' . $value;
+                },
+                array_keys($product['features']),
+                $product['features']
+            );
path: src/Entity/DfProductBuild.php
user: login: davidmolinacano
body: Bueno, como ambas son válidas en verdad lo cambio :raised_hands: 
created_at: 2025-03-03T07:39:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -193,403 +162,125 @@ function arrayMergeByIdProduct($array1 = [], $array2 = [])
 
 $header[] = 'stock_quantity';
 
-if ($cfg_display_prices) {
+if ($shouldDisplayPrices) {
     $header[] = 'price';
     $header[] = 'sale_price';
-    $header[] = 'df_multiprice';
+
+    if ($isMultipriceEnabled) {
+        $header[] = 'df_multiprice';
+    }
 }
 
-if ($cfg_product_variations == 1) {
+$additionalAttributesHeaders = [];
+
+if (1 === $shouldShowProductVariations) {
     $header[] = 'variation_reference';
     $header[] = 'variation_supplier_reference';
     $header[] = 'variation_mpn';
     $header[] = 'variation_ean13';
     $header[] = 'variation_upc';
     $header[] = 'df_group_leader';
     $header[] = 'df_variants_information';
-    $attribute_keys = DfTools::getAttributeKeysForShopAndLang($shop->id, $lang->id);
-    $alt_attribute_keys = [];
-    foreach ($attribute_keys as $key) {
-        $header_value = slugify($key);
-        if ($limit_group_attributes && !in_array($header_value, $group_attributes_slug)) {
+    $attributeKeys = DfTools::getAttributeKeysForShopAndLang($shop->id, $lang->id);
+    $altAttributeKeys = [];
+
+    foreach ($attributeKeys as $key) {
+        $headerValue = DfTools::slugify($key);
+        if ($shouldLimitGroupAttributes && !in_array($headerValue, $groupAttributesSlug)) {
             continue;
         }
-        $alt_attribute_keys[] = $key;
-        $header[] = $header_value;
+        $altAttributeKeys[] = $key;
+        $additionalAttributesHeaders[] = $headerValue;
     }
-    $attribute_keys = $alt_attribute_keys;
+    $attributeKeys = $altAttributeKeys;
 }
 
-if ($cfg_product_features) {
-    $all_feature_keys = DfTools::getFeatureKeysForShopAndLang($shop->id, $lang->id);
+if ($shouldShowProductFeatures) {
+    $allFeatureKeys = DfTools::getFeatureKeysForShopAndLang($shop->id, $lang->id);
 
     if (
-        is_array($cfg_features_shown)
-        && count($cfg_features_shown) > 0
-        && $cfg_features_shown[0] !== ''
+        is_array($featuresShownArray)
+        && count($featuresShownArray) > 0
+        && $featuresShownArray[0] !== ''
     ) {
-        $feature_keys = DfTools::getSelectedFeatures($all_feature_keys, $cfg_features_shown);
+        $featurekeys = DfTools::getSelectedFeatures($allFeatureKeys, $featuresShownArray);
     } else {
-        $feature_keys = $all_feature_keys;
+        $featurekeys = $allFeatureKeys;
     }
-    $header[] = 'attributes';
+    $additionalAttributesHeaders[] = 'attributes';
 }
 
+$header = array_merge($header, $additionalAttributesHeaders);
+
 /**
  * @author camlafit <https://github.com/camlafit>
- * Extend doofinder feed
+ * Allows users to extend Doofinder feed by adding extra headers and extra rows.
+ * The hook can be defined in a different custom module or directly in the theme PHP files.
+ * Don't forget to register the hook via registerHook method:
+ *
+ * `$this->registerHook('actionDoofinderExtendFeed');`
+ *
+ * Example of the function:
  *
- * To add an new header, module can do an array_merge on $extra_header
- * To add an new data to a product, module must create a multidemensionnal array as this :
+ * public function hookActionDoofinderExtendFeed($params)
+ * {
+ *     $params['extra_header'] = ['header_custom1', 'header_custom2'];
+ *     $params['extra_rows'] = [
+ *         [
+ *             'id_product' => 1,
+ *             'header_custom1' => 'value1'
+ *         ],
+ *         [
+ *             'id_product' => 2,
+ *             'header_custom2' => 'value2'
+ *         ]
+ *     ];
+ * }
+ *
+ *
+ * To add an new header, module can do an array_merge on $extraHeader
+ * To add an new data to a product, module must create a multidimensionnal array as this :
  * array(
  *   index => array(
  *    'id_product' => value,
  *    'new_header_column_name' => 'value related to the new column'
  *   ),
  *   [...]
  * )
- * As each module can extend $extra_header and $extra_rows don't forget to merge them
+ * As each module can extend $extraHeader and $extraRows don't forget to merge them
  */
-$extra_header = [];
-$extra_rows = [];
+$extraHeader = [];
+$extraRows = [];
 Hook::exec('actionDoofinderExtendFeed', [
-    'extra_header' => &$extra_header,
-    'extra_rows' => &$extra_rows,
+    'extra_header' => &$extraHeader,
+    'extra_rows' => &$extraRows,
     'id_lang' => $lang->id,
     'id_shop' => $shop->id,
     'limit' => $limit,
     'offset' => $offset,
 ]);
 
-$header = array_merge($header, $extra_header);
-
-if (!$limit || ($offset !== false && (int) $offset === 0)) {
-    echo implode(DfTools::TXT_SEPARATOR, $header) . PHP_EOL;
-}
+$header = array_merge($header, $extraHeader);
 
 // PRODUCTS
 $rows = DfTools::getAvailableProductsForLanguage($lang->id, $shop->id, $limit, $offset);
 
-$rows = arrayMergeByIdProduct($rows, $extra_rows);
+$rows = arrayMergeByIdProduct($rows, $extraRows);
 
 // In case there is no need to display prices, avoid calculating the mins by variant
-$min_price_variant_by_product_id = ($cfg_product_variations && $cfg_display_prices) ? DfTools::getMinVariantPrices($rows, $cfg_prices_w_taxes, $currencies, $lang->id, $shop->id) : [];
+$minPriceVariantByProductId = (1 === $shouldShowProductVariations && $shouldDisplayPrices) ? DfTools::getMinVariantPrices($rows, $shouldPricesUseTaxes, $currencies, $lang->id, $shop->id) : [];
+$additionalHeaders = array_merge($additionalAttributesHeaders, $extraHeader);
 
-foreach ($rows as $row) {
-    $product_id = $row['id_product'];
-    if (!$cfg_product_variations || DfTools::isParent($row)) {
-        $variant_id = null;
-    } else {
-        $variant_id = $row['id_product_attribute'];
-    }
-    $product_price = DfTools::getPrice($product_id, $cfg_prices_w_taxes, $variant_id);
-    $onsale_price = DfTools::getOnsalePrice($product_id, $cfg_prices_w_taxes, $variant_id);
-    $multiprice = DfTools::getFormattedMultiprice($product_id, $cfg_prices_w_taxes, $currencies, $variant_id);
-
-    if ((int) $row['id_product'] > 0) {
-        // ID, TITLE, LINK
-
-        if (
-            $cfg_product_variations == 1
-            && isset($row['id_product_attribute'])
-            && (int) $row['id_product_attribute'] > 0
-        ) {
-            // ID
-            echo 'VAR-' . $row['id_product_attribute'] . DfTools::TXT_SEPARATOR;
-
-            // ITEM-GROUP-ID
-            echo $row['id_product'] . DfTools::TXT_SEPARATOR;
-            // TITLE
-            $product_title = DfTools::cleanString($row['name']);
-            echo $product_title . DfTools::TXT_SEPARATOR;
-            echo DfTools::cleanURL(
-                $context->link->getProductLink(
-                    (int) $row['id_product'],
-                    $row['link_rewrite'],
-                    $row['cat_link_rew'],
-                    $row['ean13'],
-                    $lang->id,
-                    $shop->id,
-                    (int) $row['id_product_attribute'],
-                    $cfg_mod_rewrite,
-                    false,
-                    true
-                )
-            ) . DfTools::TXT_SEPARATOR;
-        } else {
-            $eanLink = $row['ean13'];
-            // ID
-            echo $row['id_product'] . DfTools::TXT_SEPARATOR;
-
-            if ($cfg_product_variations == 1) {
-                // ITEM-GROUP-ID
-                echo '' . DfTools::TXT_SEPARATOR;
-            }
+$dfProductBuild = new DfProductBuild($shop->id, $lang->id, $currency->id);
 
-            // TITLE
-            $product_title = DfTools::cleanString($row['name']);
-            echo $product_title . DfTools::TXT_SEPARATOR;
-
-            $parent_url = DfTools::cleanURL(
-                $context->link->getProductLink(
-                    (int) $row['id_product'],
-                    $row['link_rewrite'],
-                    $row['cat_link_rew'],
-                    $eanLink,
-                    $lang->id,
-                    $shop->id,
-                    0,
-                    $cfg_mod_rewrite
-                )
-            );
-
-            if (key_exists($product_id, $min_price_variant_by_product_id) && !empty($min_price_variant_by_product_id[$product_id])) {
-                $min_variant = $min_price_variant_by_product_id[$product_id];
-                echo $min_variant['onsale_price'] < $onsale_price ? $min_price_variant_by_product_id[$product_id]['link'] : $parent_url;
-            } else {
-                echo $parent_url;
-            }
-            echo DfTools::TXT_SEPARATOR;
-        }
-
-        // DESCRIPTION
-        echo DfTools::cleanString(
-            $row[$cfg_short_description ? 'description_short' : 'description']
-        ) . DfTools::TXT_SEPARATOR;
-
-        // ALTERNATE DESCRIPTION
-        echo DfTools::cleanString(
-            $row[$cfg_short_description ? 'description' : 'description_short']
-        ) . DfTools::TXT_SEPARATOR;
-
-        // META TITLE
-        echo DfTools::cleanString($row['meta_title']) . DfTools::TXT_SEPARATOR;
-
-        // META DESCRIPTION
-        echo DfTools::cleanString($row['meta_description']) . DfTools::TXT_SEPARATOR;
-
-        // IMAGE LINK
-
-        if (
-            $cfg_product_variations == 1 && isset($row['id_product_attribute'])
-            && (int) $row['id_product_attribute'] > 0
-        ) {
-            $cover = Product::getCover($row['id_product_attribute']);
-            $id_image = DfTools::getVariationImg(
-                $row['id_product'],
-                $row['id_product_attribute']
-            );
-
-            if (!empty($id_image)) {
-                $image_link = DfTools::cleanURL(
-                    DfTools::getImageLink(
-                        $row['id_product_attribute'],
-                        $id_image,
-                        $row['link_rewrite'],
-                        $cfg_image_size
-                    )
-                );
-            } else {
-                $image_link = DfTools::cleanURL(
-                    DfTools::getImageLink(
-                        $row['id_product_attribute'],
-                        $row['id_image'],
-                        $row['link_rewrite'],
-                        $cfg_image_size
-                    )
-                );
-            }
-
-            // For variations with no specific pictures
-            if (strpos($image_link, '/-') > -1) {
-                $image_link = DfTools::cleanURL(
-                    DfTools::getImageLink(
-                        $row['id_product'],
-                        $row['id_image'],
-                        $row['link_rewrite'],
-                        $cfg_image_size
-                    )
-                );
-            }
-
-            echo $image_link . DfTools::TXT_SEPARATOR;
-        } else {
-            echo DfTools::cleanURL(
-                DfTools::getImageLink(
-                    $row['id_product'],
-                    $row['id_image'],
-                    $row['link_rewrite'],
-                    $cfg_image_size
-                )
-            ) . DfTools::TXT_SEPARATOR;
-        }
-
-        // MAIN CATEGORY
-        echo DfTools::cleanString($row['main_category']) . DfTools::TXT_SEPARATOR;
-
-        // PRODUCT CATEGORIES
-        echo DfTools::getCategoriesForProductIdAndLanguage(
-            $row['id_product'],
-            $lang->id,
-            $shop->id
-        ) . DfTools::TXT_SEPARATOR;
-
-        // AVAILABILITY
-        $available = (int) $row['available_for_order'] > 0;
-
-        if ((int) DfTools::cfg($shop->id, 'PS_STOCK_MANAGEMENT')) {
-            $stock = StockAvailable::getQuantityAvailableByProduct(
-                $row['id_product'],
-                isset($row['id_product_attribute']) ? $row['id_product_attribute'] : null,
-                $shop->id
-            );
-            $allow_oosp = Product::isAvailableWhenOutOfStock($row['out_of_stock']);
-            echo ($available && ($stock > 0 || $allow_oosp) ? 'in stock' : 'out of stock') . DfTools::TXT_SEPARATOR;
-        } else {
-            echo ($available ? 'in stock' : 'out of stock') . DfTools::TXT_SEPARATOR;
-        }
-
-        // BRAND
-        echo DfTools::cleanString($row['manufacturer']) . DfTools::TXT_SEPARATOR;
-
-        // MPN
-        echo DfTools::cleanString($row['mpn']) . DfTools::TXT_SEPARATOR;
-
-        // EAN13
-        echo DfTools::cleanString($row['ean13']) . DfTools::TXT_SEPARATOR;
-
-        // UPC
-        echo DfTools::cleanString($row['upc']) . DfTools::TXT_SEPARATOR;
-
-        // REFERENCE
-        echo DfTools::cleanString($row['reference']) . DfTools::TXT_SEPARATOR;
-
-        // SUPPLIER_REFERENCE
-        echo DfTools::cleanString($row['supplier_reference']) . DfTools::TXT_SEPARATOR;
-
-        // EXTRA_TITLE_1
-        echo $product_title . DfTools::TXT_SEPARATOR;
-
-        // EXTRA_TITLE_2
-        echo DfTools::splitReferences($product_title) . DfTools::TXT_SEPARATOR;
-
-        // TAGS
-        echo DfTools::escapeSlashes(DfTools::cleanString(DfTools::escapeSlashes($row['tags'])));
-
-        // ISBN
-        if (DfTools::versionGte('1.7.0.0')) {
-            echo DfTools::TXT_SEPARATOR;
-            echo DfTools::cleanString($row['isbn']);
-        }
-
-        // STOCK_QUANTITY
-        echo DfTools::TXT_SEPARATOR;
-        echo DfTools::cleanString($row['stock_quantity']);
-
-        // PRODUCT PRICE & ON SALE PRICE
-
-        if ($cfg_display_prices && $cfg_product_variations !== 1) {
-            echo DfTools::TXT_SEPARATOR;
-
-            $product_price = DfTools::getPrice($product_id, $cfg_prices_w_taxes);
-            $onsale_price = DfTools::getOnsalePrice($product_id, $cfg_prices_w_taxes);
-            $multiprice = DfTools::getFormattedMultiprice($product_id, $cfg_prices_w_taxes, $currencies);
-
-            if ($row['show_price']) {
-                echo Tools::convertPrice($product_price, $currency);
-                echo DfTools::TXT_SEPARATOR;
-                echo $product_price != $onsale_price ? Tools::convertPrice($onsale_price, $currency) : '';
-                echo DfTools::TXT_SEPARATOR;
-                echo $multiprice_enabled && $multiprice ? $multiprice : '';
-            } else {
-                echo DfTools::TXT_SEPARATOR . DfTools::TXT_SEPARATOR;
-            }
-        } elseif ($cfg_display_prices && $cfg_product_variations == 1) {
-            echo DfTools::TXT_SEPARATOR;
-            // The parent product should have as price the lowest ones of the
-            // variants (combinations) if there are any
-            if (DfTools::isParent($row) && array_key_exists($product_id, $min_price_variant_by_product_id)) {
-                $min_variant = $min_price_variant_by_product_id[$product_id];
-
-                if (
-                    !is_null($min_variant['onsale_price'])
-                    && !is_null($min_variant['price'])
-                    && (empty($onsale_price) || $min_variant['onsale_price'] < $onsale_price)
-                ) {
-                    $product_price = $min_variant['price'];
-                    $onsale_price = $min_variant['onsale_price'];
-                    $multiprice = $min_variant['multiprice'];
-                }
-            }
-
-            if ($row['show_price']) {
-                echo Tools::convertPrice($product_price, $currency);
-                echo DfTools::TXT_SEPARATOR;
-                echo $product_price != $onsale_price ? Tools::convertPrice($onsale_price, $currency) : '';
-                echo DfTools::TXT_SEPARATOR;
-                echo $multiprice_enabled && $multiprice ? $multiprice : '';
-            } else {
-                echo DfTools::TXT_SEPARATOR . DfTools::TXT_SEPARATOR;
-            }
-        }
-
-        if ($cfg_product_variations == 1) {
-            echo DfTools::TXT_SEPARATOR;
-            echo DfTools::cleanString($row['variation_reference']);
-            echo DfTools::TXT_SEPARATOR;
-            echo DfTools::cleanString($row['variation_supplier_reference']);
-            echo DfTools::TXT_SEPARATOR;
-            echo DfTools::cleanString($row['variation_mpn']);
-            echo DfTools::TXT_SEPARATOR;
-            echo DfTools::cleanString($row['variation_ean13']);
-            echo DfTools::TXT_SEPARATOR;
-            echo DfTools::cleanString($row['variation_upc']);
-            echo DfTools::TXT_SEPARATOR;
-            echo DfTools::cleanString($row['df_group_leader']);
-            $variation_attributes = DfTools::getAttributesForProductVariation(
-                $row['id_product_attribute'],
-                $lang->id,
-                $attribute_keys
-            );
-            echo DfTools::TXT_SEPARATOR;
-            if (DfTools::hasAttributes($row['id_product']) && !$row['id_product_attribute']) {
-                $product_attributes = DfTools::hasProductAttributes($row['id_product'], DfTools::cfg($shop->id, 'DF_GROUP_ATTRIBUTES_SHOWN'));
-                if ($product_attributes) {
-                    $attributes = DfTools::getAttributesName($product_attributes, $lang->id);
-
-                    if (is_array($attributes)) {
-                        $variants_keys = array_column($attributes, 'name');
-                        echo implode('%%', array_map('slugify', $variants_keys));
-                    } else {
-                        echo '';
-                    }
-                } else {
-                    echo '';
-                }
-            }
-            foreach ($variation_attributes as $attribute) {
-                echo DfTools::TXT_SEPARATOR . str_replace('/', '//', DfTools::cleanString($attribute));
-            }
-        }
-
-        if ($cfg_product_features) {
-            echo DfTools::TXT_SEPARATOR;
-            foreach (DfTools::getFeaturesForProduct($row['id_product'], $lang->id, $feature_keys) as $key => $values) {
-                foreach ($values as $index => $value) {
-                    echo slugify($key) . '=';
-                    echo str_replace('/', '\/', DfTools::cleanString($value)) . '/';
-                }
-            }
-        }
-
-        /*
-         * @author camlafit <https://github.com/camlafit>
-         */
-        foreach ($extra_header as $extra) {
-            echo DfTools::TXT_SEPARATOR;
-            echo isset($row[$extra]) ? DfTools::cleanString($row[$extra]) : '';
-        }
+$csv = fopen('php://output', 'w');
+if (!$limit || (false !== $offset && 0 === (int) $offset)) {
+    fputcsv($csv, $header, DfTools::TXT_SEPARATOR);
+}
 
-        echo PHP_EOL;
-    }
+foreach ($rows as $row) {
+    $product = $dfProductBuild->buildProduct($row, $minPriceVariantByProductId, $additionalAttributesHeaders, $additionalHeaders);
+    $product = $dfProductBuild->applySpecificTransformationsForCsv($product, $extraHeader, $header);
+    fputcsv($csv, $product, DfTools::TXT_SEPARATOR);
path: feeds/product.php
user: login: davidmolinacano
body: De hecho se usa en la 283 (el último parámetro)
created_at: 2025-03-03T07:41:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -193,403 +162,125 @@ function arrayMergeByIdProduct($array1 = [], $array2 = [])
 
 $header[] = 'stock_quantity';
 
-if ($cfg_display_prices) {
+if ($shouldDisplayPrices) {
     $header[] = 'price';
     $header[] = 'sale_price';
-    $header[] = 'df_multiprice';
+
+    if ($isMultipriceEnabled) {
+        $header[] = 'df_multiprice';
+    }
 }
 
-if ($cfg_product_variations == 1) {
+$additionalAttributesHeaders = [];
+
+if (1 === $shouldShowProductVariations) {
     $header[] = 'variation_reference';
     $header[] = 'variation_supplier_reference';
     $header[] = 'variation_mpn';
     $header[] = 'variation_ean13';
     $header[] = 'variation_upc';
     $header[] = 'df_group_leader';
     $header[] = 'df_variants_information';
-    $attribute_keys = DfTools::getAttributeKeysForShopAndLang($shop->id, $lang->id);
-    $alt_attribute_keys = [];
-    foreach ($attribute_keys as $key) {
-        $header_value = slugify($key);
-        if ($limit_group_attributes && !in_array($header_value, $group_attributes_slug)) {
+    $attributeKeys = DfTools::getAttributeKeysForShopAndLang($shop->id, $lang->id);
+    $altAttributeKeys = [];
+
+    foreach ($attributeKeys as $key) {
+        $headerValue = DfTools::slugify($key);
+        if ($shouldLimitGroupAttributes && !in_array($headerValue, $groupAttributesSlug)) {
             continue;
         }
-        $alt_attribute_keys[] = $key;
-        $header[] = $header_value;
+        $altAttributeKeys[] = $key;
+        $additionalAttributesHeaders[] = $headerValue;
     }
-    $attribute_keys = $alt_attribute_keys;
+    $attributeKeys = $altAttributeKeys;
 }
 
-if ($cfg_product_features) {
-    $all_feature_keys = DfTools::getFeatureKeysForShopAndLang($shop->id, $lang->id);
+if ($shouldShowProductFeatures) {
+    $allFeatureKeys = DfTools::getFeatureKeysForShopAndLang($shop->id, $lang->id);
 
     if (
-        is_array($cfg_features_shown)
-        && count($cfg_features_shown) > 0
-        && $cfg_features_shown[0] !== ''
+        is_array($featuresShownArray)
+        && count($featuresShownArray) > 0
+        && $featuresShownArray[0] !== ''
     ) {
-        $feature_keys = DfTools::getSelectedFeatures($all_feature_keys, $cfg_features_shown);
+        $featurekeys = DfTools::getSelectedFeatures($allFeatureKeys, $featuresShownArray);
     } else {
-        $feature_keys = $all_feature_keys;
+        $featurekeys = $allFeatureKeys;
     }
-    $header[] = 'attributes';
+    $additionalAttributesHeaders[] = 'attributes';
 }
 
+$header = array_merge($header, $additionalAttributesHeaders);
+
 /**
  * @author camlafit <https://github.com/camlafit>
- * Extend doofinder feed
+ * Allows users to extend Doofinder feed by adding extra headers and extra rows.
+ * The hook can be defined in a different custom module or directly in the theme PHP files.
+ * Don't forget to register the hook via registerHook method:
+ *
+ * `$this->registerHook('actionDoofinderExtendFeed');`
+ *
+ * Example of the function:
  *
- * To add an new header, module can do an array_merge on $extra_header
- * To add an new data to a product, module must create a multidemensionnal array as this :
+ * public function hookActionDoofinderExtendFeed($params)
+ * {
+ *     $params['extra_header'] = ['header_custom1', 'header_custom2'];
+ *     $params['extra_rows'] = [
+ *         [
+ *             'id_product' => 1,
+ *             'header_custom1' => 'value1'
+ *         ],
+ *         [
+ *             'id_product' => 2,
+ *             'header_custom2' => 'value2'
+ *         ]
+ *     ];
+ * }
+ *
+ *
+ * To add an new header, module can do an array_merge on $extraHeader
+ * To add an new data to a product, module must create a multidimensionnal array as this :
  * array(
  *   index => array(
  *    'id_product' => value,
  *    'new_header_column_name' => 'value related to the new column'
  *   ),
  *   [...]
  * )
- * As each module can extend $extra_header and $extra_rows don't forget to merge them
+ * As each module can extend $extraHeader and $extraRows don't forget to merge them
  */
-$extra_header = [];
-$extra_rows = [];
+$extraHeader = [];
+$extraRows = [];
 Hook::exec('actionDoofinderExtendFeed', [
-    'extra_header' => &$extra_header,
-    'extra_rows' => &$extra_rows,
+    'extra_header' => &$extraHeader,
+    'extra_rows' => &$extraRows,
     'id_lang' => $lang->id,
     'id_shop' => $shop->id,
     'limit' => $limit,
     'offset' => $offset,
 ]);
 
-$header = array_merge($header, $extra_header);
-
-if (!$limit || ($offset !== false && (int) $offset === 0)) {
-    echo implode(DfTools::TXT_SEPARATOR, $header) . PHP_EOL;
-}
+$header = array_merge($header, $extraHeader);
 
 // PRODUCTS
 $rows = DfTools::getAvailableProductsForLanguage($lang->id, $shop->id, $limit, $offset);
 
-$rows = arrayMergeByIdProduct($rows, $extra_rows);
+$rows = arrayMergeByIdProduct($rows, $extraRows);
 
 // In case there is no need to display prices, avoid calculating the mins by variant
-$min_price_variant_by_product_id = ($cfg_product_variations && $cfg_display_prices) ? DfTools::getMinVariantPrices($rows, $cfg_prices_w_taxes, $currencies, $lang->id, $shop->id) : [];
+$minPriceVariantByProductId = (1 === $shouldShowProductVariations && $shouldDisplayPrices) ? DfTools::getMinVariantPrices($rows, $shouldPricesUseTaxes, $currencies, $lang->id, $shop->id) : [];
+$additionalHeaders = array_merge($additionalAttributesHeaders, $extraHeader);
 
-foreach ($rows as $row) {
-    $product_id = $row['id_product'];
-    if (!$cfg_product_variations || DfTools::isParent($row)) {
-        $variant_id = null;
-    } else {
-        $variant_id = $row['id_product_attribute'];
-    }
-    $product_price = DfTools::getPrice($product_id, $cfg_prices_w_taxes, $variant_id);
-    $onsale_price = DfTools::getOnsalePrice($product_id, $cfg_prices_w_taxes, $variant_id);
-    $multiprice = DfTools::getFormattedMultiprice($product_id, $cfg_prices_w_taxes, $currencies, $variant_id);
-
-    if ((int) $row['id_product'] > 0) {
-        // ID, TITLE, LINK
-
-        if (
-            $cfg_product_variations == 1
-            && isset($row['id_product_attribute'])
-            && (int) $row['id_product_attribute'] > 0
-        ) {
-            // ID
-            echo 'VAR-' . $row['id_product_attribute'] . DfTools::TXT_SEPARATOR;
-
-            // ITEM-GROUP-ID
-            echo $row['id_product'] . DfTools::TXT_SEPARATOR;
-            // TITLE
-            $product_title = DfTools::cleanString($row['name']);
-            echo $product_title . DfTools::TXT_SEPARATOR;
-            echo DfTools::cleanURL(
-                $context->link->getProductLink(
-                    (int) $row['id_product'],
-                    $row['link_rewrite'],
-                    $row['cat_link_rew'],
-                    $row['ean13'],
-                    $lang->id,
-                    $shop->id,
-                    (int) $row['id_product_attribute'],
-                    $cfg_mod_rewrite,
-                    false,
-                    true
-                )
-            ) . DfTools::TXT_SEPARATOR;
-        } else {
-            $eanLink = $row['ean13'];
-            // ID
-            echo $row['id_product'] . DfTools::TXT_SEPARATOR;
-
-            if ($cfg_product_variations == 1) {
-                // ITEM-GROUP-ID
-                echo '' . DfTools::TXT_SEPARATOR;
-            }
+$dfProductBuild = new DfProductBuild($shop->id, $lang->id, $currency->id);
 
-            // TITLE
-            $product_title = DfTools::cleanString($row['name']);
-            echo $product_title . DfTools::TXT_SEPARATOR;
-
-            $parent_url = DfTools::cleanURL(
-                $context->link->getProductLink(
-                    (int) $row['id_product'],
-                    $row['link_rewrite'],
-                    $row['cat_link_rew'],
-                    $eanLink,
-                    $lang->id,
-                    $shop->id,
-                    0,
-                    $cfg_mod_rewrite
-                )
-            );
-
-            if (key_exists($product_id, $min_price_variant_by_product_id) && !empty($min_price_variant_by_product_id[$product_id])) {
-                $min_variant = $min_price_variant_by_product_id[$product_id];
-                echo $min_variant['onsale_price'] < $onsale_price ? $min_price_variant_by_product_id[$product_id]['link'] : $parent_url;
-            } else {
-                echo $parent_url;
-            }
-            echo DfTools::TXT_SEPARATOR;
-        }
-
-        // DESCRIPTION
-        echo DfTools::cleanString(
-            $row[$cfg_short_description ? 'description_short' : 'description']
-        ) . DfTools::TXT_SEPARATOR;
-
-        // ALTERNATE DESCRIPTION
-        echo DfTools::cleanString(
-            $row[$cfg_short_description ? 'description' : 'description_short']
-        ) . DfTools::TXT_SEPARATOR;
-
-        // META TITLE
-        echo DfTools::cleanString($row['meta_title']) . DfTools::TXT_SEPARATOR;
-
-        // META DESCRIPTION
-        echo DfTools::cleanString($row['meta_description']) . DfTools::TXT_SEPARATOR;
-
-        // IMAGE LINK
-
-        if (
-            $cfg_product_variations == 1 && isset($row['id_product_attribute'])
-            && (int) $row['id_product_attribute'] > 0
-        ) {
-            $cover = Product::getCover($row['id_product_attribute']);
-            $id_image = DfTools::getVariationImg(
-                $row['id_product'],
-                $row['id_product_attribute']
-            );
-
-            if (!empty($id_image)) {
-                $image_link = DfTools::cleanURL(
-                    DfTools::getImageLink(
-                        $row['id_product_attribute'],
-                        $id_image,
-                        $row['link_rewrite'],
-                        $cfg_image_size
-                    )
-                );
-            } else {
-                $image_link = DfTools::cleanURL(
-                    DfTools::getImageLink(
-                        $row['id_product_attribute'],
-                        $row['id_image'],
-                        $row['link_rewrite'],
-                        $cfg_image_size
-                    )
-                );
-            }
-
-            // For variations with no specific pictures
-            if (strpos($image_link, '/-') > -1) {
-                $image_link = DfTools::cleanURL(
-                    DfTools::getImageLink(
-                        $row['id_product'],
-                        $row['id_image'],
-                        $row['link_rewrite'],
-                        $cfg_image_size
-                    )
-                );
-            }
-
-            echo $image_link . DfTools::TXT_SEPARATOR;
-        } else {
-            echo DfTools::cleanURL(
-                DfTools::getImageLink(
-                    $row['id_product'],
-                    $row['id_image'],
-                    $row['link_rewrite'],
-                    $cfg_image_size
-                )
-            ) . DfTools::TXT_SEPARATOR;
-        }
-
-        // MAIN CATEGORY
-        echo DfTools::cleanString($row['main_category']) . DfTools::TXT_SEPARATOR;
-
-        // PRODUCT CATEGORIES
-        echo DfTools::getCategoriesForProductIdAndLanguage(
-            $row['id_product'],
-            $lang->id,
-            $shop->id
-        ) . DfTools::TXT_SEPARATOR;
-
-        // AVAILABILITY
-        $available = (int) $row['available_for_order'] > 0;
-
-        if ((int) DfTools::cfg($shop->id, 'PS_STOCK_MANAGEMENT')) {
-            $stock = StockAvailable::getQuantityAvailableByProduct(
-                $row['id_product'],
-                isset($row['id_product_attribute']) ? $row['id_product_attribute'] : null,
-                $shop->id
-            );
-            $allow_oosp = Product::isAvailableWhenOutOfStock($row['out_of_stock']);
-            echo ($available && ($stock > 0 || $allow_oosp) ? 'in stock' : 'out of stock') . DfTools::TXT_SEPARATOR;
-        } else {
-            echo ($available ? 'in stock' : 'out of stock') . DfTools::TXT_SEPARATOR;
-        }
-
-        // BRAND
-        echo DfTools::cleanString($row['manufacturer']) . DfTools::TXT_SEPARATOR;
-
-        // MPN
-        echo DfTools::cleanString($row['mpn']) . DfTools::TXT_SEPARATOR;
-
-        // EAN13
-        echo DfTools::cleanString($row['ean13']) . DfTools::TXT_SEPARATOR;
-
-        // UPC
-        echo DfTools::cleanString($row['upc']) . DfTools::TXT_SEPARATOR;
-
-        // REFERENCE
-        echo DfTools::cleanString($row['reference']) . DfTools::TXT_SEPARATOR;
-
-        // SUPPLIER_REFERENCE
-        echo DfTools::cleanString($row['supplier_reference']) . DfTools::TXT_SEPARATOR;
-
-        // EXTRA_TITLE_1
-        echo $product_title . DfTools::TXT_SEPARATOR;
-
-        // EXTRA_TITLE_2
-        echo DfTools::splitReferences($product_title) . DfTools::TXT_SEPARATOR;
-
-        // TAGS
-        echo DfTools::escapeSlashes(DfTools::cleanString(DfTools::escapeSlashes($row['tags'])));
-
-        // ISBN
-        if (DfTools::versionGte('1.7.0.0')) {
-            echo DfTools::TXT_SEPARATOR;
-            echo DfTools::cleanString($row['isbn']);
-        }
-
-        // STOCK_QUANTITY
-        echo DfTools::TXT_SEPARATOR;
-        echo DfTools::cleanString($row['stock_quantity']);
-
-        // PRODUCT PRICE & ON SALE PRICE
-
-        if ($cfg_display_prices && $cfg_product_variations !== 1) {
-            echo DfTools::TXT_SEPARATOR;
-
-            $product_price = DfTools::getPrice($product_id, $cfg_prices_w_taxes);
-            $onsale_price = DfTools::getOnsalePrice($product_id, $cfg_prices_w_taxes);
-            $multiprice = DfTools::getFormattedMultiprice($product_id, $cfg_prices_w_taxes, $currencies);
-
-            if ($row['show_price']) {
-                echo Tools::convertPrice($product_price, $currency);
-                echo DfTools::TXT_SEPARATOR;
-                echo $product_price != $onsale_price ? Tools::convertPrice($onsale_price, $currency) : '';
-                echo DfTools::TXT_SEPARATOR;
-                echo $multiprice_enabled && $multiprice ? $multiprice : '';
-            } else {
-                echo DfTools::TXT_SEPARATOR . DfTools::TXT_SEPARATOR;
-            }
-        } elseif ($cfg_display_prices && $cfg_product_variations == 1) {
-            echo DfTools::TXT_SEPARATOR;
-            // The parent product should have as price the lowest ones of the
-            // variants (combinations) if there are any
-            if (DfTools::isParent($row) && array_key_exists($product_id, $min_price_variant_by_product_id)) {
-                $min_variant = $min_price_variant_by_product_id[$product_id];
-
-                if (
-                    !is_null($min_variant['onsale_price'])
-                    && !is_null($min_variant['price'])
-                    && (empty($onsale_price) || $min_variant['onsale_price'] < $onsale_price)
-                ) {
-                    $product_price = $min_variant['price'];
-                    $onsale_price = $min_variant['onsale_price'];
-                    $multiprice = $min_variant['multiprice'];
-                }
-            }
-
-            if ($row['show_price']) {
-                echo Tools::convertPrice($product_price, $currency);
-                echo DfTools::TXT_SEPARATOR;
-                echo $product_price != $onsale_price ? Tools::convertPrice($onsale_price, $currency) : '';
-                echo DfTools::TXT_SEPARATOR;
-                echo $multiprice_enabled && $multiprice ? $multiprice : '';
-            } else {
-                echo DfTools::TXT_SEPARATOR . DfTools::TXT_SEPARATOR;
-            }
-        }
-
-        if ($cfg_product_variations == 1) {
-            echo DfTools::TXT_SEPARATOR;
-            echo DfTools::cleanString($row['variation_reference']);
-            echo DfTools::TXT_SEPARATOR;
-            echo DfTools::cleanString($row['variation_supplier_reference']);
-            echo DfTools::TXT_SEPARATOR;
-            echo DfTools::cleanString($row['variation_mpn']);
-            echo DfTools::TXT_SEPARATOR;
-            echo DfTools::cleanString($row['variation_ean13']);
-            echo DfTools::TXT_SEPARATOR;
-            echo DfTools::cleanString($row['variation_upc']);
-            echo DfTools::TXT_SEPARATOR;
-            echo DfTools::cleanString($row['df_group_leader']);
-            $variation_attributes = DfTools::getAttributesForProductVariation(
-                $row['id_product_attribute'],
-                $lang->id,
-                $attribute_keys
-            );
-            echo DfTools::TXT_SEPARATOR;
-            if (DfTools::hasAttributes($row['id_product']) && !$row['id_product_attribute']) {
-                $product_attributes = DfTools::hasProductAttributes($row['id_product'], DfTools::cfg($shop->id, 'DF_GROUP_ATTRIBUTES_SHOWN'));
-                if ($product_attributes) {
-                    $attributes = DfTools::getAttributesName($product_attributes, $lang->id);
-
-                    if (is_array($attributes)) {
-                        $variants_keys = array_column($attributes, 'name');
-                        echo implode('%%', array_map('slugify', $variants_keys));
-                    } else {
-                        echo '';
-                    }
-                } else {
-                    echo '';
-                }
-            }
-            foreach ($variation_attributes as $attribute) {
-                echo DfTools::TXT_SEPARATOR . str_replace('/', '//', DfTools::cleanString($attribute));
-            }
-        }
-
-        if ($cfg_product_features) {
-            echo DfTools::TXT_SEPARATOR;
-            foreach (DfTools::getFeaturesForProduct($row['id_product'], $lang->id, $feature_keys) as $key => $values) {
-                foreach ($values as $index => $value) {
-                    echo slugify($key) . '=';
-                    echo str_replace('/', '\/', DfTools::cleanString($value)) . '/';
-                }
-            }
-        }
-
-        /*
-         * @author camlafit <https://github.com/camlafit>
-         */
-        foreach ($extra_header as $extra) {
-            echo DfTools::TXT_SEPARATOR;
-            echo isset($row[$extra]) ? DfTools::cleanString($row[$extra]) : '';
-        }
+$csv = fopen('php://output', 'w');
+if (!$limit || (false !== $offset && 0 === (int) $offset)) {
+    fputcsv($csv, $header, DfTools::TXT_SEPARATOR);
+}
 
-        echo PHP_EOL;
-    }
+foreach ($rows as $row) {
+    $product = $dfProductBuild->buildProduct($row, $minPriceVariantByProductId, $additionalAttributesHeaders, $additionalHeaders);
+    $product = $dfProductBuild->applySpecificTransformationsForCsv($product, $extraHeader, $header);
+    fputcsv($csv, $product, DfTools::TXT_SEPARATOR);
path: feeds/product.php
user: login: ogomezba
body: Sí sí, era más porque pensaba que tenia más sentido en la propia generación del CSV, pero está bien así también, sí :)
created_at: 2025-03-03T07:43:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,6 +44,17 @@ public function __construct($idShop, $idLang, $idCurrency)
         $this->idLang = $idLang;
         $this->idCurrency = $idCurrency;
         $this->currencies = \Currency::getCurrenciesByIdShop($idShop);
+        $this->attributesShown = \Configuration::get('DF_GROUP_ATTRIBUTES_SHOWN');
+        $this->displayPrices = \Configuration::get('DF_GS_DISPLAY_PRICES');
+        $this->imageSize = \Configuration::get('DF_GS_IMAGE_SIZE');
+        $this->link = \Context::getContext()->link;
+        $this->linkRewriteConf = \Configuration::get('PS_REWRITING_SETTINGS');
+        $this->productVariations = \Configuration::get('DF_SHOW_PRODUCT_VARIATIONS');
+        $this->showProductFeatures = \Configuration::get('DF_SHOW_PRODUCT_FEATURES');
+        $this->stockManagement = \Configuration::get('PS_STOCK_MANAGEMENT');
+        $this->useTax = \Configuration::get('DF_GS_PRICES_USE_TAX') || DoofinderConstants::NO;
path: src/Entity/DfProductBuild.php
user: login: davidmolinacano
body: Quitado en https://github.com/doofinder/doofinder-prestashop/pull/291/commits/b45697da9e08ccb89066336617e96ae12cee7beb :smile: 
created_at: 2025-03-03T07:46:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -128,46 +114,153 @@ private function buildProduct($product)
         $p['extra_title_1'] = $p['title'];
         $p['extra_title_2'] = DfTools::splitReferences($p['title']);
         $p['tags'] = DfTools::cleanString($product['tags']);
-        $p['stock_quantity'] = DfTools::cleanString($product['stock_quantity']);
 
         if (DfTools::versionGte('1.7.0.0')) {
             $p['isbn'] = DfTools::cleanString($product['isbn']);
         }
 
+        $p['stock_quantity'] = DfTools::cleanString($product['stock_quantity']);
         if ($this->displayPrices) {
             $p['price'] = $this->getPrice($product);
             $p['sale_price'] = $this->getPrice($product, true);
 
             if ($this->multipriceEnabled) {
                 $p['df_multiprice'] = $this->getMultiprice($product);
             }
-        }
 
-        if ($this->showProductFeatures) {
-            $p = array_merge($p, $this->getFeatures($product));
+            if (DfTools::isParent($product) && array_key_exists($p['id'], $minPriceVariantByProductId)) {
+                $minVariant = $minPriceVariantByProductId[$p['id']];
+                if (
+                    !is_null($minVariant['onsale_price'])
+                    && !is_null($minVariant['price'])
+                    && (empty($p['sale_price']) || $minVariant['onsale_price'] < $p['sale_price'])
+                ) {
+                    $p['price'] = $minVariant['price'];
+                    $p['sale_price'] = ($minVariant['onsale_price'] === $minVariant['price']) ? null : $minVariant['onsale_price'];
+                    if ($this->multipriceEnabled) {
+                        $p['df_multiprice'] = $minVariant['multiprice'];
+                    }
+                }
+            }
         }
 
         if ($this->productVariations) {
-            $p['item_group_id'] = $this->getItemGroupId($product);
-            $p['group_id'] = $this->getItemGroupId($product);
-
             $p['variation_reference'] = $product['variation_reference'];
             $p['variation_supplier_reference'] = $product['variation_supplier_reference'];
             $p['variation_mpn'] = $product['variation_mpn'];
             $p['variation_ean13'] = $product['variation_ean13'];
             $p['variation_upc'] = $product['variation_upc'];
-            $p['df_group_leader'] = (!is_null($product['df_group_leader'] && $product['df_group_leader']) ? true : false);
-
+            $p['df_group_leader'] = (is_numeric($product['df_group_leader']) && 0 !== (int)$product['df_group_leader']);
+            $p['df_variants_information'] = $this->getVariantsInformation($product);
+            
             $attributes = $this->getAttributes($product);
 
             $p = array_merge($p, $attributes);
 
-            $p['df_variants_information'] = $this->getVariantsInformation($product);
+            foreach ($extraAttributesHeader as $extraAttributeHeader) {
+                if ('attributes' !== $extraAttributeHeader && !array_key_exists($extraAttributeHeader, $p) && array_key_exists($extraAttributeHeader, $attributes)) {
+                    $p[$extraAttributeHeader] = $attributes[$extraAttributeHeader];
+                }
+            }
+        }
+
+        if ($this->showProductFeatures) {
+            $p['features'] = $this->getFeatures($product);
+        }
+        
+        foreach ($extraHeaders as $extraHeader) {
+            if (!empty($p[$extraHeader])) {
+                continue;
+            }
+            $p[$extraHeader] = isset($product[$extraHeader]) ? DfTools::cleanString($product[$extraHeader]) : '';
         }
 
         return $p;
     }
 
+    /**
+     * Applies specific transformations to a product's data for CSV export.
+     *
+     * This method performs several modifications on the product array, like:
+     * - If multi-price is enabled, formats the multiprice field.
+     * - Joins category values using a predefined separator.
+     * - If variants information exists, it slugifies and joins them using "%%".
+     * - Casts the group leader flag to an integer.
+     * - Iterates over extra headers to process attribute values:
+     *   - For each non-empty extra header, it concatenates key-value pairs in the format "key=value",
+     *     with the value cleaned and any "/" characters escaped.
+     * - Processes features (if present and an array) by converting them into an attributes string
+     *   formatted as "key=value" pairs joined with "/". The original features key is removed.
+     * - Ensures the final product fields are ordered according to the given headers.
+     *
+     * @param array $product      The associative array representing the product data.
+     * @param array $extraHeaders An array of additional headers to process in the product data.
+     * @param array $allHeaders   An array specifying the order of CSV fields.
+     *
+     * @return array The transformed product array ready for CSV export.
+     */
+    public function applySpecificTransformationsForCsv($product, $extraHeaders, $allHeaders)
+    {
+        if ($this->multipriceEnabled) {
+            $product['df_multiprice'] = DfTools::getFormattedMultiprice($product['df_multiprice']);
+        }
+        $product['categories'] = implode(DfTools::CATEGORY_SEPARATOR, $product['categories']);
+
+        if (array_key_exists('df_variants_information', $product)) {
+            $product['df_variants_information'] = implode('%%', array_map([__NAMESPACE__ . '\DfTools', 'slugify'], $product['df_variants_information']));
+        }
+
+        $product['df_group_leader'] = (int)$product['df_group_leader'];
+
+        if (array_key_exists('features', $product) && is_array($product['features'])) {
+
+            $formattedAttributes = array_map(
+                function ($key, $value) {
+                    if (is_array($value)) {
+                        $keyValueToReturn = [];
+                        foreach ($value as $singleValue) {
+                            $keyValueToReturn[] = $key . '=' . $singleValue;
+                        }
+                        return implode('/', $keyValueToReturn);
+                    }
+                    return $key . '=' . $value;
+                },
+                array_keys($product['features']),
+                $product['features']
+            );
path: src/Entity/DfProductBuild.php
user: login: davidmolinacano
body: Cambiado en https://github.com/doofinder/doofinder-prestashop/pull/291/commits/b45697da9e08ccb89066336617e96ae12cee7beb :smile: 
created_at: 2025-03-03T07:47:38Z
author_association: CONTRIBUTOR

PR comments:
user: login: ogomezba
created_at: 2025-02-25T07:46:56Z
body: Buenas! Ayer estuvimos viendo Alberto, Bruno y yo esta issue de Prestashop https://github.com/doofinder/support/issues/3293#issuecomment-2679109208 y vimos el problema. Pasa por tocar un poco la consulta. Tiene sentido quizá hacer primero el fix, que el cliente instale la versión a ver si le resuelve el problema, y luego integrar el cambio en esta PR? Qué opináis?
author_association: CONTRIBUTOR
user: login: davidmolinacano
created_at: 2025-02-25T07:49:06Z
body: Me parece correcto, yo lo integro cuando lo tengáis! :raised_hands: 
author_association: CONTRIBUTOR
user: login: coderabbitai[bot]
created_at: 2025-02-25T09:55:10Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

The pull request introduces significant updates to the Doofinder module, marked by a version increment from '4.13.0' to '5.0.0' in both the primary module file and the constants file. The `feeds/product.php` file has had the legacy `slugify` function removed, with product data processing now managed by the `DfProductBuild` class, which has been restructured for enhanced clarity and maintainability. The `DfProductBuild` class has been updated with new properties and methods to improve CSV export functionality. Additionally, the `DfTools` class has been modified to streamline multiprice handling and now includes a new utility method for slugification. These changes reflect a significant shift towards a more modular and object-oriented design within the codebase.

## Changes

| File(s)                                               | Change Summary |
|-------------------------------------------------------|----------------|
| doofinder.php, src/.../DoofinderConstants.php        | Updated version identifiers from '4.13.0' to '5.0.0'. |
| feeds/product.php                                     | Removed legacy `slugify` function; restructured configuration variable names; integrated `DfProductBuild` for CSV product output. |
| src/.../DfProductBuild.php                            | Added new properties, revised the constructor and build methods with extra parameters; removed the `assign` method; added CSV-specific transformation logic. |
| src/.../DfTools.php                                   | Renamed `getFormattedMultiprice` to `getMultiprice`; updated multiprice handling with array_key_exists; introduced a new `slugify` method. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Feed as feeds/product.php
    participant Builder as DfProductBuild
    participant Tools as DfTools
    Feed->>Builder: Initiate product build with product data
    Builder->>Tools: Retrieve minimum variant prices & perform slugification
    Builder->>Builder: Apply CSV-specific transformations and ordering
    Builder->>Feed: Return structured product array
    Feed->>CSV: Output CSV record
```

<!-- walkthrough_end -->


---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between b45697da9e08ccb89066336617e96ae12cee7beb and 9fa658d49c59b99903d9a0f54e3a42e48e557fdb.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `feeds/product.php` (5 hunks)
* `src/Entity/DfProductBuild.php` (6 hunks)
* `src/Entity/DfTools.php` (3 hunks)

</details>

<details>
<summary>🔇 Additional comments (21)</summary><blockquote>

<details>
<summary>src/Entity/DfTools.php (4)</summary>

`1379-1379`: **Code simplification: Now using `getMultiprice` directly.**

The change correctly updates the code to use `getMultiprice` method directly instead of a likely previously existing `getFormattedMultiprice` method. This simplifies the data flow by separating data retrieval from formatting.

---

`1381-1381`: **Best practice: Using standard function name over alias.**

Good change replacing `key_exists` (an alias) with the standard PHP function `array_key_exists`. This improves code maintainability and readability by using the canonical function name.

---

`1525-1536`: **Improved function signature: Simplified parameter handling.**

The `getFormattedMultiprice` method has been refactored to accept the multiprice data directly rather than extracting it internally. This is a good separation of concerns - the method now focuses solely on formatting rather than also handling data retrieval.

---

`1538-1560`: **New utility method: Centralized slug generation.**

Good addition of the `slugify` method to `DfTools` class, allowing consistent slug generation across the codebase. This centralization ensures slugs are created uniformly and eliminates duplicate code.

</details>
<details>
<summary>feeds/product.php (6)</summary>

`16-16`: **Improved modularization: Using DfProductBuild.**

Good architectural improvement importing the `DfProductBuild` class. This indicates a shift towards a more object-oriented and modular approach for product data handling.

---

`92-111`: **Enhanced readability: Descriptive variable naming.**

The variable names have been significantly improved for clarity:
- `$shouldDisplayPrices` instead of `$cfg_display_prices`
- `$shouldPricesUseTaxes` instead of `$cfg_prices_use_tax`
- `$isMultipriceEnabled` instead of `$cfg_multiprice_enabled`
- etc.

These new names are more descriptive and follow a more consistent naming convention, making the code much more readable and self-documenting.

---

`122-135`: **Consistent slug generation: Using DfTools::slugify.**

Good use of the centralized `DfTools::slugify` method for consistent slug generation when handling group attributes. This ensures uniformity across the application.

---

`174-196`: **Improved attribute headers handling.**

This segment improves the extraction and handling of attribute headers with better variable naming and consistent slug generation using `DfTools::slugify`. The conditional logic is also clearer.

---

`270-272`: **Optimize price variant calculation.**

The conditional check for calculating min price variants is now more efficient, only performing the calculation when both product variations are shown AND prices should be displayed. Additionally, combining headers into a single array simplifies subsequent code.

---

`273-285`: **Significant improvement: Using standardized CSV handling.**

This is a major improvement to the CSV output generation:
1. Using the `DfProductBuild` class for structured product data preparation
2. Using PHP's built-in `fputcsv` function for standardized CSV generation
3. Proper handling of headers only on the first page of results
4. Clean separation of transformation logic with `applySpecificTransformationsForCsv`

This approach is more maintainable, less error-prone, and follows PHP best practices for file handling.

</details>
<details>
<summary>src/Entity/DfProductBuild.php (11)</summary>

`41-58`: **Improved constructor: Direct property initialization.**

The constructor now directly initializes more properties from configuration, eliminating the need for a separate initialization method. This is a good refactoring that makes the class more streamlined and reduces points of failure.

---

`76-76`: **Enhanced price handling in build method.**

Now computing `minPriceVariantByProductId` using `DfTools::getMinVariantPrices` for consistent price calculation across the application. This centralizes the calculation logic in one place.

---

`85-85`: **More flexible method signature.**

The `buildProduct` method now accepts additional parameters: `minPriceVariantByProductId`, `extraAttributesHeader`, and `extraHeaders`. This provides greater flexibility in how products are constructed and what data is included.

---

`131-144`: **Improved parent product pricing.**

This new logic properly handles parent product pricing by considering variant prices. If a parent product has variants with lower prices, it now correctly uses those variant prices, providing more accurate pricing information.

---

`160-165`: **Better handling of extra attribute headers.**

New logic ensures all required attributes from `extraAttributesHeader` are included in the product data, improving the completeness of the output.

---

`171-176`: **Comprehensive handling of extra headers.**

This code properly processes all extra headers, ensuring they're included in the product data with appropriate default values when not present in the original product data.

---

`202-235`: **New method: Specialized CSV transformations.**

The new `applySpecificTransformationsForCsv` method enhances code organization by separating CSV-specific transformations from general product building. It handles formatting of multiprices, categories, variant information, and features in a structured way.

This separation of concerns makes the code more maintainable and allows for different transformation strategies for different output formats.

---

`237-245`: **New utility method: Consistent field ordering.**

The `ensureCsvFieldsOrder` method ensures product fields appear in the CSV in the same order as the headers, which is essential for proper CSV formatting. This eliminates potential field misalignment issues.

---

`433-437`: **Consistent slug generation for features.**

Good update to use `DfTools::slugify` for feature keys, ensuring consistent slug generation across the application.

---

`466-466`: **Consistent slug generation for attributes.**

Properly updated to use `DfTools::slugify` for attribute group names, maintaining consistency across the codebase.

---

`496-496`: **More elegant code for slugifying variant information.**

Using `array_map` with the `DfTools::slugify` function is a more elegant approach to transforming the array of variant information names. The fully qualified namespace ensures the correct function is used.

</details>

</blockquote></details>

</details>
<!-- internal state start -->


<!-- DwQgtGAEAqAWCWBnSTIEMB26CuAXA9mAOYCmGJATmriQCaQDG+Ats2bgFyQAOFk+AIwBWJBrngA3EsgEBPRvlqU0AgfFwA6NPEgQAfACgjoCEYDEZyAAUASpADK2VmgqyjcEj2wAbb5AokAI7Y0riQ4rjedJAARFYAMgCqAOIAkgByNgCC6ckAojb2YACcAIwAbJAAPpCJGPAAZvDRuLCeAO5o8q2e1LgU8AJ40ugB4VQYiA34FGz003zY3LTUnvhYiGhS6BjzJNGk5FTi6zGQncgMAav0cpDYiJSQKxLwtMz43vAYaAyY+Dt6KgME4BJRogAmMoaGBtax2bTMZAEdC0WgBRDITAoTEhfwkbw3cIAnrjTBTGbMajwdb8BroXD9QbDZC0bADDBEe7LVZY3aQBr7eiHZQnLC8fAMaSPRAw1JhVBfDAAaxaALQ+OC8ACbAwYUQ3FEjWaQKwSEQeJBzDBfDKlVppIAIvh8E1dk9kuoABLYAT47j4RDqGayGEeeGMbFg9AMKWY6LUcJw6a+fDtb5cxI2eJcADasEZ3EQHAA9CWiOpYL6NExmCXaC63UoKPXG99m2BeKE0IhYPhuCXuD5vCWoaUALoACgLuCLpfLlertdbrvblBXTconYxuB7fYHQ98o7KAEoNOZLABhFi63DIRzOVxGVJYUm12/IR5iGlYV1JzyHn4ATBKEAA09yPHw+BECwJAAF4CBq3zqPARIarQSAMA8Qa0gERAuBhnLoPihI0ECuKeN81g7nu/bgQEgoBBgDAZsRBpGk0DAKKw7AChQLA7PgPR8Ba3ABhQCoUWGbTyLeP4tLAiY9LI4FoN46xcumrSQFk3g2iimD0AAQhQ2AYPg4GKUCSh6saCY8PxAhRMwSaJkwPi3J4GKfFItyyYojSyKxGoga4MIAPIwWwCEahKAaPPQrSJuokDMPARAFpA0a8NgNlhCiTQUIgCrMNwzm8RqTQAB4CjM/4oHldnkRaJCqam6ZEW+Xy8Si3zFWpfikksKxkZAUhFT+xKQI8lH0il3neFIyJtNqDmCM5MIAGKfOpHVcq0SDgcp03YEQpDFdE3w0EQxysW+imciMDT8S5b7skxYSAZqITFeeBgvviBrrI84EvG8HxKr8/zoDdQrnJW/BRfBiEAOSfqd51iuB7bwH84idYpYRwu0+AeSgeokDdqz1dV/DMZRYQXPiaC0KGsKoCQVUMA9pCQAgGVfBld7EUwvgqDM1LbGgYn8b8sBTRKTkkMwYCID5rFaQgr5wjQaDMOByvcIpQZwXdcLwKVMy7vTdKoui0qsRbcyoTQjDdXqOItTIJALJ4bpqfAptEQIsvNowPMjFRpJfcBP2aEYm3ssJ3EfnxAnQbBMWQJzXbxvQLNpbdtILM8Wzg583xQ+ZaPnDMyrgeZ+NckGRD1JxmAlRKUip1SSjk/VTBKNJJDyBaZ2hApSlwgABhKbJiBoRvcNPArwFEChk9GDRJ20fAMd8dDgdwLjiFhhIUN48hR5QSK2ww5/qPIhmQOQcZBrg8h/qSnNIE3ChD5ASKmdEI8HwFdaIpN8qE2mrBMaLhUJKxfnrEY7lvCeVSjMTwShEBXHgNwcQUhwIgWKj+M2ngHhrHpNHfeNUYgMAaEQM4VEJDwJUOvH4bBkBfFVJAae9CiAAH0MIGkJLIARvBcbSBXs/PhDDxH8XnrgARLCBjUmBtPP6QDoogLioGeyAYaC2TUoDfo2AxDslYl/OE6kKxcSjnCQerUTrjxIQTKenhp6OgaFYBRZjcBGWwGvWgK9749k/H2Mm6xL580MuvBohIiDLUTGgBogoxD1RYd4H6/4kQEiWuBdoCB14akeMfY4fszLflpKg+gVIfi83iWgRJJEiQolJMIsqXRbZNAJLQWU7NkAkzJoiKaQZSpfAaN0Bxihej8gtl3NYFB8L1DgmojAR8T64x8C4aJ9iPGCjoIgQcviF5LxXk0KIBSEDc3/PIQU3hGY9kYO9dg0T754BoAEfO/IMKpK2Q8qaPxXj4RoH9R0Zd3gVx+H8cy5wnlkHclQUgiUARMEmG8J4PRHjOMxj+RABSEZDXdEVa2hF9rQMaLc54bwX5CWmvsFyrw2GLKymQH2uNUJ+CWMXHeTxOZ60rljVEQgHi4FTjU1lqUWa9FjDMMll9h7UpwdhSaDEohiCxLVLCyBqkzMgMEAOH9ARSqutoH4agvgfwJdpXWLk2DWkoJcHa4tbpSGiSHM1xVKSO1Kvxbu7BZRGH0MYcAUAyD0D/GgPAhARTlPoO+dgXBeD8GEKIAhIw7iOKoKodQWgdDBpMFAOAqBUDYijQQYgbK40p0Tf4NA7QTqPnkJmmZ2a1CaG0LoMAhgQ2mAMA2VcxLF6wG4BwAwMQJ0GAsDpVIlajhEgtE2u+EdEDuAcRHfupJp4Ds3BQYdy9V7r2+BIHyIxsTDWpm0uE40cJYCtDaW2To2zEowWyKIiqb2TSZtzTAyK04uRRgAFg0KUAAzBoAADCjKaKMACskHIPQc1nshQkxTFiDqlYjxzpB3NhCYSTE0lUAfF+bjNZ5MMJ4zPdNdKbcyMe0/bSb4VxlbsCubjeWH9uBkd8KPDGoRkBUiEHVC9NAdV8B/Y9ZAVFt7MTFIalSWU8C0rtihdYxi0U0CqmEOq7EWKcXDr+kYV7PA2NxvwPgsmqk/EtU/MYXcMWmnqr8hof0Lw6QeaKPFU12miHPmsnV9Ic5WwgXwIcTlzPsBQtIINkAABqjrJqiboFwWevovhcQACSMawAAXkgEBkD4GoMAG4V5UVCZiXhOHd3lawNu59zZ90r0AEmEvDwsZcgNlxLtJ8twYQ6VurbswnVca5QIbDXcOUGa+OydBgIBgCMAcvpxzFB+P3WOidMQp2WCyLO2NC6nBUlcMuozq6DDhhc6+uyy19lCiOXPdbZzD2eDuDqfArwCYeMQFkiskzzmVLFNnWyARL7sZuTjKjmreyNAVFgPsDafunSxGMA7ZEG5phIkFIiDoZ5eOgC6bwxYOBI7+7IFebBWiKHJhdFmiq0VNCIOycjX4gfG1ZWQExplzFfOtfLR441jEqIQVEZATFkHzDqqEgYH8YRxdYUrdGNynnT0y/woRSBOliIkXGFeilthgk5wETpUp6Ca14Zl3spM0GOk16InxkjEDT1UvyMZa8XDchGiMfXr39hYF7n7TDycGfpWZ0D4XzL8XA4eixIijjmYrAtY/RVZmuIlyUJ8tK9QiKFJIMnFEHTREOUd6pMS0TdxVWkC7+gVuG2PfSRHsUyB2eG6wMllF9xxBfDgp4LFnhyANoj4gjh0g/rhgerQJUXI/w3VJtwBkTIhhiZid7TnxVrjMCVHQRVXZXiky4dBczSUwjc1EKqSXfBZ9LAXwMJfZ7+RDDXvqX7zenmt/9ISU38NtIag+GMH2nEzQHs0sEocsEEZC1W3iJy/igSaC+GYS9O6wGE8mfgp+DAyoyA6eduXQrEOu9+9A9eYQje3mPuHObePIo0KIaqaa9UA+cCqiw+yCgaF2cIkCQ4+oaak07OZkzYME5AxEQmdUDEvwBAe6kAL4tOEa9IGEAQYg0SogfYrEmSP0R00yfc5kDaaK6+fimqhBowVA8gDwrElOfYfS/69UniUBa2YgASQS8BhGAywOfwRY2yQOf4ehI0GoE+U+xql49gcWtUswfQPqCyt2JqeoZqKga8j8j6ahJAiqfhARbBymwImO3h0QRhRE08DQ7BDAiAEgzu5w1y/O8yEyzQYREob8ts7QMuuB0Bpcu4PmrBeA7B00/QJAesmiguvgqhngkm50FGZGrsJStGxoMK+oCADQcOzw0gtGxq2hPOTiHwn2+0aYBEmqf+ngJG2yfAz8VIpqlciCjiiEjwf006uknyAWTRWCfmLgVxf4wWEkoWXgEWXEUW4gMW82kAl4BG0mlsTxtAqWXiPi1hMBdh5h08Pi3Y9g+4AAOgALJrZRCwk1ZrgUCwn2AUAMCwl5C2QfwTbLYPbQEzZbZBp9qIBYkli4kRCyAljAnQG2FoIbazbbbnH7ZVqHZLp/j9GfHhiWEgmKKMnBLDZVbcHEp8GeAWgCD9RGJ+AkZjEBYbIST/I7JXzax9HAzoaiHGpbqP5oICl+IU556mFyjqmoYLGiHgQC7KB+BxSUAfEo796Y7ITiABw970AyFprRLPQCQh5M5FxYDKFV7kzvJkq8J9C34sgwlpgYCFHbrYGyAO667gTTwWxNIkD2CBwkBxlKjKg5nfDKg2AkA1HqAkDXgYANBxmEHy6qJN5xm14GliCbQdG4DshSIpleroHwmYDpm3hxkULQBoBVRxnMA+DiB4G4nMrBLV68KCjUBtmIAADSI8TuO+AQe+Dw4ZmItGRpVO9ALefu+Iyxh85pFcI0jswsVmKBMRV0JI0yaG3OohY+M8epwpJh1OB5nO7eU0tY7BHiWeSZJANZqEeoRkiZ0BqQwpmRXIlhBOnwxOpAuA8J3wwFHcgFTu4O8sqAPQ4oYSaoFhr5jZuAu5JpsIkptG85Ywf408hF0Beub+h58pPSHevwUo+CqmKBPAdxlOjqqWAFAwUoqFoF4FoJkFcZnM/QaAWQjIkZYmXoHReGM508ElVA8l0qRUGiZFkAqe3+WsvCtFoJ9Fq+WAOchk+FzGMw4k1MOuwUtAIqxUqcJxEC/uyEo5jKrCHseBUe5uLMyBP4GmbQ6BmBdUehxBwMf0WQL8xZLxnW75tAKZIBl89gho+muM0AEwFIQRTe20FAl4+RhRn5ZoeoJy+FklkwCwLkHh1AlUdUiR2cVU4kuAR8/Eb8wUMlzIYm4EVGMEAwwZz8c5rZGIRRa8ngZAFoHI5Kfslctp9RPSaCjpFmzYCYsqFAYZKIemTUfMCljqiqylkwbZeVEgm0zQ814Uq142qUxpH5TyakasZBds+FDQvw0Ru4rsB04mzYGYf09gJA3RxeLCrscVr+Buh535KIeA0RPekBcFROHAJOv2gUdWkhtssU65NIDw0SSgbo0Q08pOiNl1e5mFK+9172vkwO4goObMxalwG6MpKwq1WZYReN7cQOlR0oMY/EVWXUCBRg4U3R3gvRhmUm0emAUo9UVVjR3hrEzhURlq5RSpp8OxuyWAoO5GKINlREz8dVjxYQV5/ltmZxu2nmAZyId5Nx0u9xQWDVIWEaYW6WkWeJ5RsW8JV1NeFFg15CFBKW7W9taegOk0Bliik4mWhBJ4Q2lWyA/JDJsBwpVEuNlJ1Jj8dJVhgpMdzWkAbWaWrxAo/ttIgdfiwdhB4EmW/FkiQl/iIliikFkA+WuY44xdKlUl7Vd+iAalYctd9dXWjdbdjqNdkAddYd/cEdkBRFQpE2FJDAVJeJtJ9JoJQpM2UALte5D1gJPt2dettIiVsgyVHEaVGVFVAWOVh1hd0BDd2mql21RUxdA0PdRUg9FWvxI90d4JcdE9U9NJydo9adZyLJZJi2Bgb9idH8ydMNsoS8m2k6bJc6oo0Qi6x2n89IPJ52l2jQ9ITF5RFh+OhOTuIp0m9WgD09ID2DzWVpKVCpvGWq0FdM9UUtOOaDY5eCAlWC1ViqcVvCiFOVVIjIdA8JDDeBRlJNysH2J5z86gmBudNmMRhVH+vw0QdwW6iFvDDyjDkiJFigRNGh1KshkQ8gAQTIv1Iwo5yjeBDRGomskCgRXDf8uaWlfwvgU008HDlIfQPDfDTDK8yGZpjjeeyFGA5d6Faj+5DFX5XtHedjfg3jSFbjqjNOustAEVaIamNmYOA8SBSTAVZ+gR9UP8F0NskakAqoapFhpdcYYFRFYl+hXS0jPJ8wL0vChTAiOTd4K8KI08LgBhAiDTTTGFw1EOGxmC6AMs/YtZFScmk0Jc71qGWm0xVgXoVgu1B9UTxj7jBNphxN7+AeMV5mz84ucwDjTjWVZESj457jbUu0F5U00YrF0o0QnpchdydUizf8Q4FA8Uo+RgkVdBcVHZCN/2lkwTRV/Q1hJ5hSHGqG40wsJSTIREt5xEWY8QYAz0QBk+o8v2WUujJAJurE5kGAYAakRsmATglA5mP6VAYgvd5usAsgRsY1R0AwrAEBGIY5M5ZVP2pZt0ML+UAIWQ9gl4qQqQXRNpAt/42KSDbRG+W+9URjJzki+I+jmSa0VRz8ZAMevecIENtmsRfRj9dwvlwU4rxhmAeCrhk0cVhtHmlxTe1xMxFtVrDx1tAJFmWzbxjtnxi9rtNGrclFntXuq9WdnW/Up8OdYztIBzVjrjyzkiJ9ol8VXWzGWSSgg5leUeauLyzE8tXWEeeo1d+WIIvg99WAw9sF2D49CdhDWD8F6dmdHW5mgb5mG9WAYbLjtAxzKjUowd0rbbJABbuD0NJb/c8dk9QDM9DQoDC9kAS9azvl3t/rtbr19bEj00vzsgwd0zPbRbFbROpbQ75bo7xDP9pJ825JZbH9qJxK5ZMpd4zJpJUDaOsDR2LgCDQt50a6fRG6x6p6fInul6ZtvCcWBQ9gqQ4U6QISmpHculKGniY2uVYHeoODEda5v16NyAQZWrf7AHQHIH5p1sjyyAhWYGiGRN0j6D+F/WEGiGRGWIxtLOoxPSGxaAwmfAOWIZOobGgxeM+rBiUWxiMOUx/cDbCmTrA1C56AmIkoLs0Q5uT6U2fA2x76Rg7mFxXmwM1rSgtr3m9rjVzxNbLrEQTtXxF7OH37ZEqWWhYQ/7hQmHfd+HxWKMZWQ9j9UHMnhnHcODr9J7SdZ7zYLncHVbvCZn8WGHwH1n8G5Hg2DnI2Tnu6PnzTA7BDp70HMXYDI608v9R7PahawOUhOAFad78aN4taVAiOD7J2LazYKg7aeaXahgmX746gAibwiAAiaNxZdAAi/UEk1XvaUA5QEI5QAA7BBgIIBhCLBnQLBhCKUAABzFB0AdGAYpKAYNCzelCwawaAalDFAMCAa0CgZoAQj0AFqhrZzlAkCzeTf9eAYkAQagbFCwbFAQZTcNCwYCD9f0IQhoCgYkCgYQYkClANAMDFCTffdA9deZeITFCDcMCndTeAblAQZoDFDlCgZ/AQYMCwZTf9fFCzfXe0AND9e0CbcHf49oAahHcQCQAQaAZTckCwZvcfdoDrcMD9fI8CC0BY9PUMBffPewb7c09I9Sj9cVD0jk9QBTePcA+9f4/lBTcfcMDs+walDU8kBvcQbFBPWlA3dTeIRjdqefdg/HelCiANBPfM+/AND/cE8VB3cQgQgNDU8VALdoi0ACDTf9evcCCgb9cG8U8c+AazdoDlDy9Tcvdc9PdoBC/rcQaPdff0KIQI9ggQbPdlA+9hq2/Y/FCAYTfM8u+28kAzdY+9eAYMBTd4+wYMBK+lCB8QgMBDcpKp+MCveK8pKy+gZjcNBB8m9oBXeAaveE+wYW8kCnd9dK/lBj8d8N/DewblCQ8rCzePexgCAzcQZj+gagZj+lD9dnflBoB/c1/7Du8kB+ii+QDq+B8Y+0D+/o/FACAZ83e0DFBoBJ/rfffd8QgkDK9rf9cNAu9dcGC1c3h6ujXZrkh1a60BGm/IfQEAA= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: ogomezba
created_at: 2025-03-02T22:01:07Z
body: Madre mía @davidmolinacano ! Tremendo gurú de PHP y Pretashop estás hecho. Menuda mejoría. Creo que ha quedado genial.

Lo único que añado y un poco al hilo del comentario de Sofía @sofia-doofinder: 
> David el fichero product.php no se puede mejorar un poco mas? sobre todo la parte primera que hay como mucho código y no se si realmente es todo necesario... ya que puntuamos esta tarea con un 5 estaría bien darle unas vueltas para intentar dejar esta parte lo mas limpia y clara posible. hay algunas variables también que igual se pueden renombrar para que sean un poco mas claras, cosas como cfg_display_prices o cfg_product_variations se hacen un poco raras, por qué ese cfg inicial?

Al ver un poco cómo ha estado quedando, me ha venido esto a la cabeza. Creo que, en cuento a todas las flags y cosas que tenemos y que se usan, distingo dos tipos:
1. Flags que determinan únicamente campos a mostrar en el output de producto (multiprice, mostrar prices)
2. Flags que afectan a los propios valores (mostrar variantes, taxes)

Creeis que aportaría o quedaría mejor hacer que la lógica fuera:
- El DfProductBuild solamente obtiene o se le pasa en el constructor únicamente aquello que **afecta** a los propios valores (el tipo dos que he mencionado, como lo de las taxes). Los valores los calcula siempre.
- El resto de flags que lo único que determinan es qué campos queremos mostrar y su lógica se encapsulan en una función que lo único que hace es determinar qué campos deben llevar los productos.
- El update on save obtiene los productos y obtiene la lista de campos y hace el equivalente en PHP a `Map.take()` de Elixir.
- El `feeds/product.php` obtiene la lista de campos (los headers) y hace lo que comentaba en la review de usarlos para generar cada una de las líneas del CSV.

Como lo veis @davidmolinacano @sofia-doofinder ? Creéis que puede simplificar? Los pros que veo de hacerlo así:
1. La lógica de obtención de flags para mostrado de campos estará en un mismo sitio, que ahora está tanto en el feed como en constructor de DfProductBuild.
2. El feeds/product.php creo que quedaría más organizado porque, aunque no creo que quitemos mucha lógico, creo que igual queda más organizada porque mi problema principal ahora mismo con el `product.php` es que hay muchas líneas sin contexto que yo creo que es lo que hace más complicado abordar ese archivo.
3. Lo que es el cálculo de los valores queda también más limpio, aunque vayamos a calcular valores que puede que no se lleguen a usar. Creo que, como tenemos tantas flags, hay bastante carga al leer la parte de cálculo de los valores.

De todas formas David, ahora que te has pegado mucho con esto, si ves que no es viable o que no aporta demasiado, sin problema porque lo estoy diciendo pensando un poco a alto nivel pero igual al concretar, la cosa no funciona.
author_association: CONTRIBUTOR
user: login: sofia-doofinder
created_at: 2025-03-03T08:56:27Z
body: > Madre mía @davidmolinacano ! Tremendo gurú de PHP y Pretashop estás hecho. Menuda mejoría. Creo que ha quedado genial.
> 
> Lo único que añado y un poco al hilo del comentario de Sofía @sofia-doofinder:
> 
> > David el fichero product.php no se puede mejorar un poco mas? sobre todo la parte primera que hay como mucho código y no se si realmente es todo necesario... ya que puntuamos esta tarea con un 5 estaría bien darle unas vueltas para intentar dejar esta parte lo mas limpia y clara posible. hay algunas variables también que igual se pueden renombrar para que sean un poco mas claras, cosas como cfg_display_prices o cfg_product_variations se hacen un poco raras, por qué ese cfg inicial?
> 
> Al ver un poco cómo ha estado quedando, me ha venido esto a la cabeza. Creo que, en cuento a todas las flags y cosas que tenemos y que se usan, distingo dos tipos:
> 
> 1. Flags que determinan únicamente campos a mostrar en el output de producto (multiprice, mostrar prices)
> 2. Flags que afectan a los propios valores (mostrar variantes, taxes)
> 
> Creeis que aportaría o quedaría mejor hacer que la lógica fuera:
> 
> * El DfProductBuild solamente obtiene o se le pasa en el constructor únicamente aquello que **afecta** a los propios valores (el tipo dos que he mencionado, como lo de las taxes). Los valores los calcula siempre.
> * El resto de flags que lo único que determinan es qué campos queremos mostrar y su lógica se encapsulan en una función que lo único que hace es determinar qué campos deben llevar los productos.
> * El update on save obtiene los productos y obtiene la lista de campos y hace el equivalente en PHP a `Map.take()` de Elixir.
> * El `feeds/product.php` obtiene la lista de campos (los headers) y hace lo que comentaba en la review de usarlos para generar cada una de las líneas del CSV.
> 
> Como lo veis @davidmolinacano @sofia-doofinder ? Creéis que puede simplificar? Los pros que veo de hacerlo así:
> 
> 1. La lógica de obtención de flags para mostrado de campos estará en un mismo sitio, que ahora está tanto en el feed como en constructor de DfProductBuild.
> 2. El feeds/product.php creo que quedaría más organizado porque, aunque no creo que quitemos mucha lógico, creo que igual queda más organizada porque mi problema principal ahora mismo con el `product.php` es que hay muchas líneas sin contexto que yo creo que es lo que hace más complicado abordar ese archivo.
> 3. Lo que es el cálculo de los valores queda también más limpio, aunque vayamos a calcular valores que puede que no se lleguen a usar. Creo que, como tenemos tantas flags, hay bastante carga al leer la parte de cálculo de los valores.
> 
> De todas formas David, ahora que te has pegado mucho con esto, si ves que no es viable o que no aporta demasiado, sin problema porque lo estoy diciendo pensando un poco a alto nivel pero igual al concretar, la cosa no funciona.

Pues a mi me parece buena idea! no había caído en este detalle, pero es verdad que así quedaría esa parte unificado y no repetimos. No se cómo lo ves tu @davidmolinacano?
author_association: MEMBER
user: login: davidmolinacano
created_at: 2025-03-03T08:57:54Z
body: Pues @ogomezba y yo habíamos comentado que podría ser buena idea analizarlo, pero en otra PR mejor. Corrígeme si lo he entendido mal, @ogomezba 
author_association: CONTRIBUTOR