chengwl 1 день назад
Родитель
Сommit
faea31f1a8
24 измененных файлов с 614 добавлено и 232 удалено
  1. 2 2
      packages/Longyi/Core/INSTALLATION.md
  2. 2 2
      packages/Longyi/Core/MODULE_SUMMARY.md
  3. 2 2
      packages/Longyi/Core/README.md
  4. 3 3
      packages/Longyi/Core/src/Contracts/ProductVariant.php
  5. 1 1
      packages/Longyi/Core/src/Helpers/FlexibleVariantOption.php
  6. 171 15
      packages/Longyi/Core/src/Http/Controllers/Admin/FlexibleVariantController.php
  7. 8 0
      packages/Longyi/Core/src/Models/ProductOption.php
  8. 8 1
      packages/Longyi/Core/src/Models/ProductOptionValue.php
  9. 62 6
      packages/Longyi/Core/src/Models/ProductVariant.php
  10. 31 9
      packages/Longyi/Core/src/Repositories/ProductVariantRepository.php
  11. 93 82
      packages/Longyi/Core/src/Type/FlexibleVariant.php
  12. 14 13
      packages/Webkul/Admin/src/Http/Controllers/Catalog/ProductController.php
  13. 4 2
      packages/Webkul/DataTransfer/src/Helpers/Importers/Product/Importer.php
  14. 78 39
      packages/Webkul/Installer/src/Database/Seeders/ProductTableSeeder.php
  15. 79 26
      packages/Webkul/Product/src/Helpers/Indexers/Price.php
  16. 2 1
      packages/Webkul/Product/src/Helpers/Indexers/Price/AbstractType.php
  17. 2 1
      packages/Webkul/Product/src/Helpers/Indexers/Price/Bundle.php
  18. 2 1
      packages/Webkul/Product/src/Helpers/Indexers/Price/Configurable.php
  19. 2 1
      packages/Webkul/Product/src/Helpers/Indexers/Price/Grouped.php
  20. 13 5
      packages/Webkul/Product/src/Models/Product.php
  21. 5 4
      packages/Webkul/Product/src/Models/ProductCustomerGroupPrice.php
  22. 10 5
      packages/Webkul/Product/src/Models/ProductPriceIndex.php
  23. 14 9
      packages/Webkul/Product/src/Repositories/ProductCustomerGroupPriceRepository.php
  24. 6 2
      packages/Webkul/Product/src/Repositories/ProductRepository.php

+ 2 - 2
packages/Longyi/Core/INSTALLATION.md

@@ -241,13 +241,13 @@ $variant = $variantRepo->createWithOptions([
 ```php
 $saleableVariants = \Longyi\Core\Models\ProductVariant::where('product_id', 1)
     ->saleable()
-    ->with('optionValues.option')
+    ->with('values.option')
     ->get();
 
 foreach ($saleableVariants as $variant) {
     echo $variant->sku . ': ' . $variant->getEffectivePrice() . PHP_EOL;
     
-    foreach ($variant->optionValues as $optionValue) {
+    foreach ($variant->values as $optionValue) {
         echo '  - ' . $optionValue->option->label . ': ' . $optionValue->label . PHP_EOL;
     }
 }

+ 2 - 2
packages/Longyi/Core/MODULE_SUMMARY.md

@@ -309,14 +309,14 @@ $variant = ProductVariant::create([
     'status' => true,
 ]);
 
-$variant->optionValues()->attach([$redValue->id, $mediumValue->id]);
+$variant->values()->attach([$redValue->id, $mediumValue->id]);
 ```
 
 ### Query Saleable Variants
 ```php
 $variants = ProductVariant::where('product_id', 1)
     ->saleable()
-    ->with('optionValues.option')
+    ->with('values.option')
     ->get();
 ```
 

+ 2 - 2
packages/Longyi/Core/README.md

@@ -127,7 +127,7 @@ $variant = ProductVariant::create([
 ]);
 
 // Link to option values
-$variant->optionValues()->attach([$redValue->id, $mediumValue->id]);
+$variant->values()->attach([$redValue->id, $mediumValue->id]);
 ```
 
 ### Query Saleable Variants
@@ -136,7 +136,7 @@ $variant->optionValues()->attach([$redValue->id, $mediumValue->id]);
 $saleableVariants = ProductVariant::where('product_id', 1)
     ->where('status', true)
     ->where('quantity', '>', 0)
-    ->with('optionValues')
+    ->with('values')
     ->orderBy('sort_order')
     ->get();
 ```

+ 3 - 3
packages/Longyi/Core/src/Contracts/ProductVariant.php

@@ -12,7 +12,7 @@ interface ProductVariant
     /**
      * Get all option values for this variant
      */
-    public function optionValues();
+    public function values();
 
     /**
      * Check if variant is saleable
@@ -20,9 +20,9 @@ interface ProductVariant
     public function isSaleable(): bool;
 
     /**
-     * Get the effective price (considering special price)
+     * Get the effective price from price index (or fallback to basic calculation).
      */
-    public function getEffectivePrice(): float;
+    public function getEffectivePrice(?int $customerGroupId = null, ?int $channelId = null): float;
 
     /**
      * Get images as array

+ 1 - 1
packages/Longyi/Core/src/Helpers/FlexibleVariantOption.php

@@ -161,7 +161,7 @@ class FlexibleVariantOption
                 // Check if this value is used in any saleable variant
                 $isAvailable = $variants->filter(function ($variant) use ($value) {
                     return $variant->isSaleable() && 
-                           $variant->optionValues->contains('id', $value->id);
+                           $variant->values->contains('id', $value->id);
                 })->isNotEmpty();
 
                 $optionData['values'][] = [

+ 171 - 15
packages/Longyi/Core/src/Http/Controllers/Admin/FlexibleVariantController.php

@@ -12,6 +12,11 @@ use Longyi\Core\Repositories\ProductVariantRepository;
 use Longyi\Core\Helpers\FlexibleVariantOption;
 use Webkul\Product\Helpers\Indexers\Price as PriceIndexer;
 use Webkul\Product\Repositories\ProductRepository;
+use Illuminate\Support\Facades\DB;
+use Longyi\Core\Models\ProductVariant;
+use Webkul\Product\Models\ProductVariantProxy;
+use Longyi\Core\Models\ProductOption;
+use Longyi\Core\Models\ProductOptionValue;
 
 class FlexibleVariantController extends Controller
 {
@@ -42,6 +47,12 @@ class FlexibleVariantController extends Controller
      */
     protected $flexibleVariantHelper;
 
+
+    public $_product = null;
+
+
+    public $_selectedOptions = [];
+
     /**
      * Create a new controller instance.
      */
@@ -57,6 +68,16 @@ class FlexibleVariantController extends Controller
         $this->productVariantRepository = $productVariantRepository;
         $this->productRepository = $productRepository;
         $this->flexibleVariantHelper = $flexibleVariantHelper;
+        //根据请求初始化产品信息
+        $this->initProduct($request->product_id ?? null);
+        
+
+    }
+    public function initProduct(int $productId){
+        $this->_product = $this->productRepository->find($productId);
+        if (!$this->_product) {
+            throw new \Exception('Product not found');
+        }
     }
 
     // ==================== OPTION MANAGEMENT ====================
@@ -252,27 +273,162 @@ class FlexibleVariantController extends Controller
             'data' => $variants,
         ]);
     }
+    public function storeSelectedOptions(){
+        foreach ($this->_selectedOptions as $optionIndex => $option) {
+            $optionModel = empty($option['id']) ? new ProductOption() : ProductOption::findOrFail($option['id']);
+
+
+            $optionModel->label = $option['label'];
+            $optionModel->type = $option['type'];
+            $optionModel->code = $option['code'];
+            $optionModel->position = $option['position'];
+            $optionModel->save();
+
+            $this->_selectedOptions[$optionIndex]['id'] = $optionModel->id;
+            $option['id'] = $optionModel->id;
+           
+            foreach ($option['option_values'] as $optionValueIndex => $value) {
+                $optionValueModel = empty($value['id']) ?
+                    new ProductOptionValue([
+                        'product_option_id' => $option['id'],
+                    ]) :
+                    ProductOptionValue::find($value['id']);
+
+                $optionValueModel->label = $value['label'];
+                $optionValueModel->code = $value['code'];
+                $optionValueModel->position = $value['position'];
+                $optionValueModel->save();
+
+                $this->_selectedOptions[$optionIndex]['option_values'][$optionValueIndex]['id'] =
+                    $optionValueModel->id;
+            }
 
-    /**
-     * Get single variant
-     */
-    public function getVariant(int $id): JsonResponse
-    {
-        $variant = $this->productVariantRepository->with('optionValues.option')->find($id);
+        }
+    }
 
-        if (!$variant) {
+    public function saveVariants($request){
+        $this->validate($request,[
+            'selected_options' => 'array',
+            'selected_options.*.code' => 'required|string',
+            'selected_options.*.option_values.*.code' => 'required|string',
+            'variants' => 'array',
+            'variants.*.sku' => 'required|string',
+            'variants.*.quantity' => 'required|integer',
+            'variants.*.values' => 'array',
+            'variants.*.values.*.code' => 'required|string',
+        ]);
+
+
+
+        DB::beginTransaction();
+        $this->_selectedOptions = $request->selected_options;
+
+        //存储选项
+        $this->storeSelectedOptions();
+
+        $variants=$request->variants;
+        //如果无变体,则删除所有变体和选项
+        if (!count($variants)) {
+            $variant=$this->_product->variants()->first();
+            $variant->values()->detach();
+
+            //删除产品选项
+            $this->_product->productOptions()->delete();
+            $this->_product->variants()
+                ->where('id', '!=', $variant->id)
+                ->get()
+                ->each(
+                    fn ($variant) => $variant->delete()
+                );
+            DB::commit();
             return response()->json([
-                'success' => false,
-                'message' => 'Variant not found',
-            ], 404);
+                'success' => true,
+                'message' => 'Variants deleted successfully',
+            ]);
+
         }
+        //存储变体
+        foreach ($variants as $variantIndex => $variantData) {
+            $variant = new ProductVariant([
+                'product_id' => $this->_product->id,
+            ]);
+            $basePrice = null;
+
+            // if (! empty($variantData['variant_id'])) {
+            //     $variant = ProductVariant::find($variantData['variant_id']);
+            //     $basePrice = $variant->basePrices->first();
+            // }
+
+            if (! empty($variantData['copied_id'])) {
+                $copiedVariant = ProductVariant::find(
+                    $variantData['copied_id']
+                );
+
+                $variant = $copiedVariant->replicate();
+                $variant->save();
+
+                // $basePrice = $copiedVariant->basePrices->first()->replicate();
+                // $basePrice->priceable_id = $variant->id;
+            }
+
+            $variant->sku = $variantData['sku'];
+            $variant->quantity = $variantData['quantity'];
+            $variant->save();
+
+            // $basePrice->price = (int) bcmul($variantData['price'], $basePrice->currency->factor);
+            // $basePrice->save();
+
+            $optionsValues = $this->mapOptionValuesToIds($variantData['values']);
+
+            $variant->values()->sync($optionsValues);
+
+            $variants[$variantIndex]['variant_id'] = $variant->id;
+
+            $productOptions = collect($this->_selectedOptions)
+                ->mapWithKeys(function ($option) use ($variant) {
+                    return [
+                        $option['id'] => [
+                            'position' => $option['position'],
+                            ],
+                        ];
+                });
+            //同步选项
+            $this->_product->productOptions()->sync($productOptions);
+            
+            $variantIds=collect($variants)->pluck('id');
+            //删除无用的变体
+            $this->_product->variants()->whereNotIn('id', $variantIds)->delete();
+            DB::commit();
+            return response()->json([
+                'success' => true,
+                'message' => 'Variants saved successfully',
+            ]);
 
-        return response()->json([
-            'success' => true,
-            'data' => $variant,
-        ]);
-    }
+            
+
+            
+        }
 
+        
+
+    }
+    //将选项值的code映射为id
+
+    public function mapOptionValuesToIds(array $values){
+        $valueIds = [];
+        foreach ($values as $option => $value) {
+            $selectedOption = collect(
+                $this->_selectedOptions
+            )->first(
+                fn ($o) => $o['code'] == $option
+            );
+            $valueId = collect($selectedOption['option_values'])->first(
+                fn ($v) => $v['code'] == $value
+            )['id'];
+            $valueIds[] = $valueId;
+        }
+        return $valueIds;
+    }
     /**
      * Create new variant
      */

+ 8 - 0
packages/Longyi/Core/src/Models/ProductOption.php

@@ -8,6 +8,14 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 use Longyi\Core\Contracts\ProductOption as ProductOptionContract;
 use Webkul\Product\Models\Product;
 
+/**
+ * @property int    $id
+ * @property string $label
+ * @property string $type
+ * @property string $code
+ * @property int    $position
+ * @property array  $meta
+ */
 class ProductOption extends Model implements ProductOptionContract
 {
     /**

+ 8 - 1
packages/Longyi/Core/src/Models/ProductOptionValue.php

@@ -6,7 +6,14 @@ use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
 use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 use Longyi\Core\Contracts\ProductOptionValue as ProductOptionValueContract;
-
+/**
+ * @property int    $id
+ * @property int    $product_option_id
+ * @property string $label
+ * @property string $code
+ * @property int    $position
+ * @property array  $meta
+ */
 class ProductOptionValue extends Model implements ProductOptionValueContract
 {
     /**

+ 62 - 6
packages/Longyi/Core/src/Models/ProductVariant.php

@@ -5,10 +5,26 @@ namespace Longyi\Core\Models;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
 use Illuminate\Database\Eloquent\Relations\BelongsToMany;
+use Illuminate\Database\Eloquent\Relations\MorphMany;
 use Illuminate\Database\Eloquent\SoftDeletes;
 use Longyi\Core\Contracts\ProductVariant as ProductVariantContract;
 use Webkul\Product\Models\Product;
-
+use Webkul\Product\Models\ProductCustomerGroupPriceProxy;
+use Webkul\Product\Models\ProductPriceIndexProxy;
+
+/**
+ * @property int    $id
+ * @property int    $product_id
+ * @property string $sku
+ * @property string $name
+ * @property float  $price
+ * @property float  $cost
+ * @property float  $weight
+ * @property int    $quantity
+ * @property boolean $status
+ * @property array  $images
+ * @property int    $sort_order
+ */
 class ProductVariant extends Model implements ProductVariantContract
 {
     use SoftDeletes;
@@ -72,7 +88,7 @@ class ProductVariant extends Model implements ProductVariantContract
 
         // When deleting a variant, detach all option values
         static::deleting(function ($variant) {
-            $variant->optionValues()->detach();
+            $variant->values()->detach();
         });
     }
 
@@ -84,10 +100,26 @@ class ProductVariant extends Model implements ProductVariantContract
         return $this->belongsTo(Product::class, 'product_id');
     }
 
+    /**
+     * Get the price indices for this variant.
+     */
+    public function price_indices(): MorphMany
+    {
+        return $this->morphMany(ProductPriceIndexProxy::modelClass(), 'priceable');
+    }
+
+    /**
+     * Get the customer group prices for this variant.
+     */
+    public function customer_group_prices(): MorphMany
+    {
+        return $this->morphMany(ProductCustomerGroupPriceProxy::modelClass(), 'priceable');
+    }
+
     /**
      * Get all option values for this variant (many-to-many)
      */
-    public function optionValues(): BelongsToMany
+    public function values(): BelongsToMany
     {
         return $this->belongsToMany(
             ProductOptionValue::class,
@@ -106,11 +138,35 @@ class ProductVariant extends Model implements ProductVariantContract
     }
 
     /**
-     * Get the effective price (considering special price)
+     * Get the effective price for the current customer group / channel
+     * from the pre-computed price index. Falls back to variant.price
+     * when no index exists yet (e.g. before the first reindex).
+     */
+    public function getEffectivePrice(?int $customerGroupId = null, ?int $channelId = null): float
+    {
+        $customerGroupId ??= app(\Webkul\Customer\Repositories\CustomerRepository::class)
+            ->getCurrentGroup()->id;
+
+        $channelId ??= core()->getCurrentChannel()->id;
+
+        $index = $this->price_indices
+            ->where('customer_group_id', $customerGroupId)
+            ->where('channel_id', $channelId)
+            ->first();
+
+        if ($index) {
+            return (float) $index->min_price;
+        }
+
+        return $this->getBasicEffectivePrice();
+    }
+
+    /**
+     * Basic effective price calculation without price index
+     * (considers only special_price on the variant itself).
      */
-    public function getEffectivePrice(): float
+    public function getBasicEffectivePrice(): float
     {
-        // Check if special price is active
         if ($this->special_price) {
             $now = now();
             $isActive = true;

+ 31 - 9
packages/Longyi/Core/src/Repositories/ProductVariantRepository.php

@@ -3,8 +3,9 @@
 namespace Longyi\Core\Repositories;
 
 use Webkul\Core\Eloquent\Repository;
+use Webkul\Product\Helpers\Indexers\Price as PriceIndexer;
+use Webkul\Product\Repositories\ProductRepository;
 use Longyi\Core\Models\ProductVariant;
-use Illuminate\Support\Collection;
 
 class ProductVariantRepository extends Repository
 {
@@ -29,7 +30,7 @@ class ProductVariantRepository extends Repository
     {
         $query = $this->model
             ->where('product_id', $productId)
-            ->with('optionValues.option')
+            ->with('values.option')
             ->orderBy('sort_order');
 
         if ($onlyActive) {
@@ -51,7 +52,7 @@ class ProductVariantRepository extends Repository
             ->where('product_id', $productId)
             ->where('status', true)
             ->where('quantity', '>', 0)
-            ->with('optionValues.option')
+            ->with('values.option')
             ->orderBy('sort_order')
             ->get();
     }
@@ -81,10 +82,12 @@ class ProductVariantRepository extends Repository
         $variant = $this->create($data);
 
         if (!empty($optionValueIds)) {
-            $variant->optionValues()->attach($optionValueIds);
+            $variant->values()->attach($optionValueIds);
         }
 
-        return $variant->fresh('optionValues.option');
+        $this->reindexProductPrices($variant->product_id);
+
+        return $variant->fresh('values.option');
     }
 
     /**
@@ -104,10 +107,12 @@ class ProductVariantRepository extends Repository
         $variant->update($data);
 
         if ($optionValueIds !== null) {
-            $variant->optionValues()->sync($optionValueIds);
+            $variant->values()->sync($optionValueIds);
         }
 
-        return $variant->fresh('optionValues.option');
+        $this->reindexProductPrices($variant->product_id);
+
+        return $variant->fresh('values.option');
     }
 
     /**
@@ -126,12 +131,12 @@ class ProductVariantRepository extends Repository
         // Get all variants for the product
         $variants = $this->model
             ->where('product_id', $productId)
-            ->with('optionValues')
+            ->with('values')
             ->get();
 
         // Find variant with exact option value match
         foreach ($variants as $variant) {
-            $variantOptionValueIds = $variant->optionValues->pluck('id')->sort()->values()->toArray();
+            $variantOptionValueIds = $variant->values->pluck('id')->sort()->values()->toArray();
 
             if ($variantOptionValueIds === $optionValueIds && count($variantOptionValueIds) === $count) {
                 return $variant;
@@ -173,4 +178,21 @@ class ProductVariantRepository extends Repository
         $variant->increment('quantity', $quantity);
         return true;
     }
+
+    /**
+     * Trigger price reindex for the parent product (and its variants).
+     */
+    protected function reindexProductPrices(int $productId): void
+    {
+        $product = app(ProductRepository::class)->with([
+            'attribute_family',
+            'attribute_values',
+            'price_indices',
+            'customer_group_prices',
+        ])->find($productId);
+
+        if ($product) {
+            app(PriceIndexer::class)->reindexBatch([$product]);
+        }
+    }
 }

+ 93 - 82
packages/Longyi/Core/src/Type/FlexibleVariant.php

@@ -2,28 +2,23 @@
 
 namespace Longyi\Core\Type;
 
+use Longyi\Core\Helpers\Indexers\Price\FlexibleVariant as FlexibleVariantIndexer;
 use Webkul\Product\Type\AbstractType;
 
 class FlexibleVariant extends AbstractType
 {
     /**
      * Is a composite product type.
-     *
-     * @var bool
      */
     protected $isComposite = true;
 
     /**
      * Is a stockable product type.
-     *
-     * @var bool
      */
     protected $isStockable = false;
 
     /**
      * Show quantity box.
-     *
-     * @var bool
      */
     protected $showQuantityBox = false;
 
@@ -31,67 +26,49 @@ class FlexibleVariant extends AbstractType
      * Has child products i.e. variants.
      * Set to false because flexible_variant uses a separate product_variants table
      * instead of Bagisto's standard child products system.
-     *
-     * @var bool
      */
     protected $hasVariants = false;
 
     /**
      * Product children price can be calculated.
-     *
-     * @var bool
      */
     protected $isChildrenCalculated = true;
 
     /**
-     * Create product with flexible variants
-     *
-     * @param  array  $data
-     * @return \Webkul\Product\Contracts\Product
+     * Create product with flexible variants.
      */
     public function create(array $data)
     {
         $product = $this->productRepository->getModel()->create($data);
 
-        // Attach options to the product (many-to-many)
         if (isset($data['options']) && is_array($data['options'])) {
             foreach ($data['options'] as $optionData) {
                 $product->options()->attach($optionData['id'], [
-                    'position' => $optionData['position'] ?? 0,
+                    'position'    => $optionData['position'] ?? 0,
                     'is_required' => $optionData['is_required'] ?? false,
-                    'meta' => isset($optionData['meta']) ? json_encode($optionData['meta']) : null,
+                    'meta'        => isset($optionData['meta']) ? json_encode($optionData['meta']) : null,
                 ]);
             }
         }
 
-        // NOTE: Variants are NOT auto-generated here
-        // They will be manually created via the admin panel or API
-        // This is the key difference from Configurable product type
-
         return $product;
     }
 
     /**
-     * Update product with flexible variants
-     *
-     * @param  array  $data
-     * @param  int  $id
-     * @param  string  $attribute
-     * @return \Webkul\Product\Contracts\Product
+     * Update product with flexible variants.
      */
     public function update(array $data, $id, $attribute = 'id')
     {
         $product = parent::update($data, $id, $attribute);
 
-        // Update options (many-to-many sync)
         if (isset($data['options']) && is_array($data['options'])) {
             $syncData = [];
-            
+
             foreach ($data['options'] as $optionData) {
                 $syncData[$optionData['id']] = [
-                    'position' => $optionData['position'] ?? 0,
+                    'position'    => $optionData['position'] ?? 0,
                     'is_required' => $optionData['is_required'] ?? false,
-                    'meta' => isset($optionData['meta']) ? json_encode($optionData['meta']) : null,
+                    'meta'        => isset($optionData['meta']) ? json_encode($optionData['meta']) : null,
                 ];
             }
 
@@ -102,91 +79,125 @@ class FlexibleVariant extends AbstractType
     }
 
     /**
-     * Get product's minimal price
-     *
-     * @return float
+     * Returns the price indexer for this product type.
      */
-    public function getMinimalPrice()
+    public function getPriceIndexer()
     {
-        // Get the minimum price from all active variants
-        $variantRepository = app(\Longyi\Core\Repositories\ProductVariantRepository::class);
-        
-        $variants = $variantRepository->getSaleableByProduct($this->product->id);
+        return app(FlexibleVariantIndexer::class);
+    }
 
-        if ($variants->isEmpty()) {
-            return 0;
+    /**
+     * Return the parent product's own ID so that catalog rules
+     * store the rule entry against the parent (not individual variants,
+     * which live in a separate table and can't be FK'd to products).
+     */
+    public function getChildrenIds(): array
+    {
+        return [$this->product->id];
+    }
+
+    /**
+     * Get product minimal price from price index.
+     */
+    public function getMinimalPrice()
+    {
+        if ($priceIndex = $this->getPriceIndex()) {
+            return $priceIndex->min_price;
         }
 
-        return $variants->min(function ($variant) {
-            return $variant->getEffectivePrice();
-        });
+        return $this->getMinimalPriceFromVariants();
     }
 
     /**
-     * Get product's maximum price
-     *
-     * @return float
+     * Get product maximum price from price index.
      */
     public function getMaximumPrice()
     {
-        // Get the maximum price from all active variants
-        $variantRepository = app(\Longyi\Core\Repositories\ProductVariantRepository::class);
-        
-        $variants = $variantRepository->getSaleableByProduct($this->product->id);
+        if ($priceIndex = $this->getPriceIndex()) {
+            return $priceIndex->max_price;
+        }
+
+        return $this->getMaximumPriceFromVariants();
+    }
+
+    /**
+     * Get product prices for display.
+     */
+    public function getProductPrices()
+    {
+        $minPrice = $this->getMinimalPrice();
+        $regularMinPrice = $this->getRegularMinimalPrice();
+
+        return [
+            'regular' => [
+                'price'           => core()->convertPrice($regularMinPrice),
+                'formatted_price' => core()->currency($regularMinPrice),
+            ],
+            'final' => [
+                'price'           => core()->convertPrice($minPrice),
+                'formatted_price' => core()->currency($minPrice),
+            ],
+        ];
+    }
+
+    /**
+     * Fallback: compute minimal price directly from variants.
+     */
+    protected function getMinimalPriceFromVariants(): float
+    {
+        $variants = $this->getSaleableVariants();
 
         if ($variants->isEmpty()) {
             return 0;
         }
 
-        return $variants->max(function ($variant) {
-            return $variant->getEffectivePrice();
-        });
+        return $variants->min(fn ($v) => $v->getBasicEffectivePrice());
     }
 
     /**
-     * Check if product has sufficient quantity
-     *
-     * @param  int  $qty
-     * @return bool
+     * Fallback: compute maximum price directly from variants.
      */
-    public function haveSufficientQuantity(int $qty): bool
+    protected function getMaximumPriceFromVariants(): float
     {
-        // Check total quantity across all saleable variants
-        $variantRepository = app(\Longyi\Core\Repositories\ProductVariantRepository::class);
-        
-        $variants = $variantRepository->getSaleableByProduct($this->product->id);
+        $variants = $this->getSaleableVariants();
 
-        $totalQuantity = $variants->sum('quantity');
+        if ($variants->isEmpty()) {
+            return 0;
+        }
 
-        return $totalQuantity >= $qty;
+        return $variants->max(fn ($v) => $v->getBasicEffectivePrice());
     }
 
     /**
-     * Get product total quantity
-     *
-     * @return int
+     * Get saleable variants (cached via relation).
      */
+    protected function getSaleableVariants()
+    {
+        if ($this->product->relationLoaded('flexibleVariants')) {
+            return $this->product->flexibleVariants->filter(fn ($v) => $v->isSaleable());
+        }
+
+        return app(\Longyi\Core\Repositories\ProductVariantRepository::class)
+            ->getSaleableByProduct($this->product->id);
+    }
+
+    public function haveSufficientQuantity(int $qty): bool
+    {
+        return $this->getSaleableVariants()->sum('quantity') >= $qty;
+    }
+
     public function totalQuantity(): int
     {
-        $variantRepository = app(\Longyi\Core\Repositories\ProductVariantRepository::class);
-        
-        $variants = $variantRepository->getByProduct($this->product->id, true);
+        $variants = $this->product->relationLoaded('flexibleVariants')
+            ? $this->product->flexibleVariants->where('status', true)
+            : app(\Longyi\Core\Repositories\ProductVariantRepository::class)
+                ->getByProduct($this->product->id, true);
 
         return $variants->sum('quantity');
     }
 
-    /**
-     * Is product saleable?
-     *
-     * @return bool
-     */
     public function isSaleable(): bool
     {
-        // Product is saleable if at least one variant is saleable
-        $variantRepository = app(\Longyi\Core\Repositories\ProductVariantRepository::class);
-        
-        $saleableVariants = $variantRepository->getSaleableByProduct($this->product->id);
-
-        return $saleableVariants->isNotEmpty();
+        return $this->getSaleableVariants()->isNotEmpty();
     }
 }

+ 14 - 13
packages/Webkul/Admin/src/Http/Controllers/Catalog/ProductController.php

@@ -44,7 +44,8 @@ class ProductController extends Controller
         protected ProductInventoryRepository $productInventoryRepository,
         protected ProductRepository $productRepository,
         protected CustomerRepository $customerRepository,
-    ) {}
+    ) {
+    }
 
     /**
      * Display a listing of the resource.
@@ -89,19 +90,19 @@ class ProductController extends Controller
     {
         $productType = request()->input('type');
         $validationRules = [
-            'type'                => 'required',
+            'type' => 'required',
             'attribute_family_id' => 'required',
-            'sku'                 => ['required', 'unique:products,sku', new Slug],
+            'sku' => ['required', 'unique:products,sku', new Slug],
         ];
         if ($productType !== 'flexible_variant') {
-            $validationRules['super_attributes']    = 'array|min:1';
-            $validationRules['super_attributes.*']  = 'array|min:1';
+            $validationRules['super_attributes'] = 'array|min:1';
+            $validationRules['super_attributes.*'] = 'array|min:1';
         }
         $this->validate(request(), $validationRules);
         if (
             $productType !== 'flexible_variant'
             && ProductType::hasVariants(request()->input('type'))
-            && ! request()->has('super_attributes')
+            && !request()->has('super_attributes')
         ) {
             $configurableFamily = $this->attributeFamilyRepository
                 ->find(request()->input('attribute_family_id'));
@@ -180,7 +181,7 @@ class ProductController extends Controller
         Event::dispatch('catalog.product.update.after', $product);
 
         return response()->json([
-            'message'      => __('admin::app.catalog.products.saved-inventory-message'),
+            'message' => __('admin::app.catalog.products.saved-inventory-message'),
             'updatedTotal' => $this->productInventoryRepository->where('product_id', $product->id)->sum('qty'),
         ]);
     }
@@ -298,7 +299,7 @@ class ProductController extends Controller
             Event::dispatch('catalog.product.update.before', $productId);
 
             $product = $this->productRepository->update([
-                'status'  => $massUpdateRequest->input('value'),
+                'status' => $massUpdateRequest->input('value'),
             ], $productId, ['status']);
 
             Event::dispatch('catalog.product.update.after', $product);
@@ -352,10 +353,10 @@ class ProductController extends Controller
         $channelId = $this->customerRepository->find(request('customer_id'))->channel_id ?? null;
 
         $params = [
-            'index'      => $indexNames ?? null,
-            'name'       => request('query'),
-            'sort'       => 'created_at',
-            'order'      => 'desc',
+            'index' => $indexNames ?? null,
+            'name' => request('query'),
+            'sort' => 'created_at',
+            'order' => 'desc',
             'channel_id' => $channelId,
         ];
 
@@ -384,7 +385,7 @@ class ProductController extends Controller
     public function download($productId, $attributeId)
     {
         $productAttribute = $this->productAttributeValueRepository->findOneWhere([
-            'product_id'   => $productId,
+            'product_id' => $productId,
             'attribute_id' => $attributeId,
         ]);
 

+ 4 - 2
packages/Webkul/DataTransfer/src/Helpers/Importers/Product/Importer.php

@@ -1095,11 +1095,13 @@ class Importer extends AbstractImporter
             foreach ($skuCustomerGroupPrices as $customerGroupPrices) {
                 $product = $this->skuStorage->get($sku);
 
-                $customerGroupPrices['product_id'] = (int) $product['id'];
+                $customerGroupPrices['priceable_id'] = (int) $product['id'];
+                $customerGroupPrices['priceable_type'] = 'Webkul\\Product\\Models\\Product';
 
                 $customerGroupPrices['unique_id'] = implode('|', array_filter([
                     $customerGroupPrices['qty'],
-                    $customerGroupPrices['product_id'],
+                    $customerGroupPrices['priceable_id'],
+                    $customerGroupPrices['priceable_type'],
                     $customerGroupPrices['customer_group_id'],
                 ]));
 

+ 78 - 39
packages/Webkul/Installer/src/Database/Seeders/ProductTableSeeder.php

@@ -255,7 +255,8 @@ class ProductTableSeeder extends Seeder
         DB::table('product_price_indices')->insert([
             [
                 'id'                   => 1,
-                'product_id'           => 1,
+                'priceable_id'         => 1,
+                'priceable_type'       => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id'    => 1,
                 'channel_id'           => 1,
                 'min_price'            => 14,
@@ -266,7 +267,8 @@ class ProductTableSeeder extends Seeder
                 'updated_at'           => $now,
             ], [
                 'id'                   => 2,
-                'product_id'           => 1,
+                'priceable_id'         => 1,
+                'priceable_type'       => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id'    => 2,
                 'channel_id'           => 1,
                 'min_price'            => 14,
@@ -277,7 +279,8 @@ class ProductTableSeeder extends Seeder
                 'updated_at'           => $now,
             ], [
                 'id'                   => 3,
-                'product_id'           => 1,
+                'priceable_id'         => 1,
+                'priceable_type'       => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id'    => 3,
                 'channel_id'           => 1,
                 'min_price'            => 14,
@@ -288,7 +291,8 @@ class ProductTableSeeder extends Seeder
                 'updated_at'           => $now,
             ], [
                 'id'                   => 4,
-                'product_id'           => 2,
+                'priceable_id'         => 2,
+                'priceable_type'       => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id'    => 1,
                 'channel_id'           => 1,
                 'min_price'            => 17,
@@ -299,7 +303,8 @@ class ProductTableSeeder extends Seeder
                 'updated_at'           => $now,
             ], [
                 'id'                   => 5,
-                'product_id'           => 2,
+                'priceable_id'         => 2,
+                'priceable_type'       => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id'    => 2,
                 'channel_id'           => 1,
                 'min_price'            => 17,
@@ -310,7 +315,8 @@ class ProductTableSeeder extends Seeder
                 'updated_at'           => $now,
             ], [
                 'id'                   => 6,
-                'product_id'           => 2,
+                'priceable_id'         => 2,
+                'priceable_type'       => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id'    => 3,
                 'channel_id'           => 1,
                 'min_price'            => 17,
@@ -321,7 +327,8 @@ class ProductTableSeeder extends Seeder
                 'updated_at'           => $now,
             ], [
                 'id'                   => 7,
-                'product_id'           => 3,
+                'priceable_id'         => 3,
+                'priceable_type'       => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id'    => 1,
                 'channel_id'           => 1,
                 'min_price'            => 21,
@@ -332,7 +339,8 @@ class ProductTableSeeder extends Seeder
                 'updated_at'           => $now,
             ], [
                 'id'                   => 8,
-                'product_id'           => 3,
+                'priceable_id'         => 3,
+                'priceable_type'       => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id'    => 2,
                 'channel_id'           => 1,
                 'min_price'            => 21,
@@ -343,7 +351,8 @@ class ProductTableSeeder extends Seeder
                 'updated_at'           => $now,
             ], [
                 'id'                   => 9,
-                'product_id'           => 3,
+                'priceable_id'         => 3,
+                'priceable_type'       => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id'    => 3,
                 'channel_id'           => 1,
                 'min_price'            => 21,
@@ -354,7 +363,8 @@ class ProductTableSeeder extends Seeder
                 'updated_at'           => $now,
             ], [
                 'id'                   => 10,
-                'product_id'           => 4,
+                'priceable_id'         => 4,
+                'priceable_type'       => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id'    => 1,
                 'channel_id'           => 1,
                 'min_price'            => 21,
@@ -365,7 +375,8 @@ class ProductTableSeeder extends Seeder
                 'updated_at'           => $now,
             ], [
                 'id'                   => 11,
-                'product_id'           => 4,
+                'priceable_id'         => 4,
+                'priceable_type'       => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id'    => 2,
                 'channel_id'           => 1,
                 'min_price'            => 21,
@@ -376,7 +387,8 @@ class ProductTableSeeder extends Seeder
                 'updated_at'           => $now,
             ], [
                 'id'                   => 12,
-                'product_id'           => 4,
+                'priceable_id'         => 4,
+                'priceable_type'       => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id'    => 3,
                 'channel_id'           => 1,
                 'min_price'            => 21,
@@ -387,7 +399,8 @@ class ProductTableSeeder extends Seeder
                 'updated_at'           => $now,
             ], [
                 'id'                   => 13,
-                'product_id'           => 5,
+                'priceable_id'         => 5,
+                'priceable_type'       => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id'    => 1,
                 'channel_id'           => 1,
                 'min_price'            => 14,
@@ -398,7 +411,8 @@ class ProductTableSeeder extends Seeder
                 'updated_at'           => $now,
             ], [
                 'id'                   => 14,
-                'product_id'           => 5,
+                'priceable_id'         => 5,
+                'priceable_type'       => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id'    => 2,
                 'channel_id'           => 1,
                 'min_price'            => 14,
@@ -409,7 +423,8 @@ class ProductTableSeeder extends Seeder
                 'updated_at'           => $now,
             ], [
                 'id'                   => 15,
-                'product_id'           => 5,
+                'priceable_id'         => 5,
+                'priceable_type'       => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id'    => 3,
                 'channel_id'           => 1,
                 'min_price'            => 14,
@@ -420,7 +435,8 @@ class ProductTableSeeder extends Seeder
                 'updated_at'           => $now,
             ], [
                 'id'                   => 16,
-                'product_id'           => 6,
+                'priceable_id'         => 6,
+                'priceable_type'       => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id'    => 1,
                 'channel_id'           => 1,
                 'min_price'            => 14,
@@ -431,7 +447,8 @@ class ProductTableSeeder extends Seeder
                 'updated_at'           => $now,
             ], [
                 'id'                   => 17,
-                'product_id'           => 6,
+                'priceable_id'         => 6,
+                'priceable_type'       => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id'    => 2,
                 'channel_id'           => 1,
                 'min_price'            => 14,
@@ -442,7 +459,8 @@ class ProductTableSeeder extends Seeder
                 'updated_at'           => $now,
             ], [
                 'id'                   => 18,
-                'product_id'           => 6,
+                'priceable_id'         => 6,
+                'priceable_type'       => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id'    => 3,
                 'channel_id'           => 1,
                 'min_price'            => 14,
@@ -453,7 +471,8 @@ class ProductTableSeeder extends Seeder
                 'updated_at'           => $now,
             ], [
                 'id'                   => 19,
-                'product_id'           => 8,
+                'priceable_id'         => 8,
+                'priceable_type'       => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id'    => 1,
                 'channel_id'           => 1,
                 'min_price'            => 14,
@@ -464,7 +483,8 @@ class ProductTableSeeder extends Seeder
                 'updated_at'           => $now,
             ], [
                 'id'                   => 20,
-                'product_id'           => 8,
+                'priceable_id'         => 8,
+                'priceable_type'       => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id'    => 2,
                 'channel_id'           => 1,
                 'min_price'            => 14,
@@ -475,7 +495,8 @@ class ProductTableSeeder extends Seeder
                 'updated_at'           => $now,
             ], [
                 'id'                   => 21,
-                'product_id'           => 8,
+                'priceable_id'         => 8,
+                'priceable_type'       => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id'    => 3,
                 'channel_id'           => 1,
                 'min_price'            => 14,
@@ -486,7 +507,8 @@ class ProductTableSeeder extends Seeder
                 'updated_at'           => $now,
             ], [
                 'id'                   => 22,
-                'product_id'           => 9,
+                'priceable_id'         => 9,
+                'priceable_type'       => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id'    => 1,
                 'channel_id'           => 1,
                 'min_price'            => 17,
@@ -497,7 +519,8 @@ class ProductTableSeeder extends Seeder
                 'updated_at'           => $now,
             ], [
                 'id'                   => 23,
-                'product_id'           => 9,
+                'priceable_id'         => 9,
+                'priceable_type'       => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id'    => 2,
                 'channel_id'           => 1,
                 'min_price'            => 17,
@@ -508,7 +531,8 @@ class ProductTableSeeder extends Seeder
                 'updated_at'           => $now,
             ], [
                 'id'                   => 24,
-                'product_id'           => 9,
+                'priceable_id'         => 9,
+                'priceable_type'       => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id'    => 3,
                 'channel_id'           => 1,
                 'min_price'            => 17,
@@ -519,7 +543,8 @@ class ProductTableSeeder extends Seeder
                 'updated_at'           => $now,
             ], [
                 'id'                   => 25,
-                'product_id'           => 10,
+                'priceable_id'         => 10,
+                'priceable_type'       => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id'    => 1,
                 'channel_id'           => 1,
                 'min_price'            => 21,
@@ -530,7 +555,8 @@ class ProductTableSeeder extends Seeder
                 'updated_at'           => $now,
             ], [
                 'id'                   => 26,
-                'product_id'           => 10,
+                'priceable_id'         => 10,
+                'priceable_type'       => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id'    => 2,
                 'channel_id'           => 1,
                 'min_price'            => 21,
@@ -541,7 +567,8 @@ class ProductTableSeeder extends Seeder
                 'updated_at'           => $now,
             ], [
                 'id'                   => 27,
-                'product_id'           => 10,
+                'priceable_id'         => 10,
+                'priceable_type'       => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id'    => 3,
                 'channel_id'           => 1,
                 'min_price'            => 21,
@@ -552,7 +579,8 @@ class ProductTableSeeder extends Seeder
                 'updated_at'           => $now,
             ], [
                 'id'                   => 28,
-                'product_id'           => 11,
+                'priceable_id'         => 11,
+                'priceable_type'       => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id'    => 1,
                 'channel_id'           => 1,
                 'min_price'            => 21,
@@ -563,7 +591,8 @@ class ProductTableSeeder extends Seeder
                 'updated_at'           => $now,
             ], [
                 'id'                   => 29,
-                'product_id'           => 11,
+                'priceable_id'         => 11,
+                'priceable_type'       => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id'    => 2,
                 'channel_id'           => 1,
                 'min_price'            => 21,
@@ -574,7 +603,8 @@ class ProductTableSeeder extends Seeder
                 'updated_at'           => $now,
             ], [
                 'id'                   => 30,
-                'product_id'           => 11,
+                'priceable_id'         => 11,
+                'priceable_type'       => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id'    => 3,
                 'channel_id'           => 1,
                 'min_price'            => 21,
@@ -585,7 +615,8 @@ class ProductTableSeeder extends Seeder
                 'updated_at'           => $now,
             ], [
                 'id'                   => 31,
-                'product_id'           => 7,
+                'priceable_id'         => 7,
+                'priceable_type'       => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id'    => 1,
                 'channel_id'           => 1,
                 'min_price'            => 17,
@@ -596,7 +627,8 @@ class ProductTableSeeder extends Seeder
                 'updated_at'           => $now,
             ], [
                 'id'                   => 32,
-                'product_id'           => 7,
+                'priceable_id'         => 7,
+                'priceable_type'       => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id'    => 2,
                 'channel_id'           => 1,
                 'min_price'            => 17,
@@ -607,7 +639,8 @@ class ProductTableSeeder extends Seeder
                 'updated_at'           => $now,
             ], [
                 'id'                   => 33,
-                'product_id'           => 7,
+                'priceable_id'         => 7,
+                'priceable_type'       => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id'    => 3,
                 'channel_id'           => 1,
                 'min_price'            => 17,
@@ -625,7 +658,8 @@ class ProductTableSeeder extends Seeder
                 'qty'               => 2,
                 'value_type'        => 'fixed',
                 'value'             => 12,
-                'product_id'        => 1,
+                'priceable_id'        => 1,
+                'priceable_type'      => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id' => 1,
                 'created_at'        => $now,
                 'updated_at'        => $now,
@@ -635,7 +669,8 @@ class ProductTableSeeder extends Seeder
                 'qty'               => 2,
                 'value_type'        => 'fixed',
                 'value'             => 12,
-                'product_id'        => 1,
+                'priceable_id'        => 1,
+                'priceable_type'      => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id' => 2,
                 'created_at'        => $now,
                 'updated_at'        => $now,
@@ -645,7 +680,8 @@ class ProductTableSeeder extends Seeder
                 'qty'               => 2,
                 'value_type'        => 'fixed',
                 'value'             => 12,
-                'product_id'        => 1,
+                'priceable_id'        => 1,
+                'priceable_type'      => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id' => 3,
                 'created_at'        => $now,
                 'updated_at'        => $now,
@@ -655,7 +691,8 @@ class ProductTableSeeder extends Seeder
                 'qty'               => 3,
                 'value_type'        => 'fixed',
                 'value'             => 50,
-                'product_id'        => 1,
+                'priceable_id'        => 1,
+                'priceable_type'      => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id' => 1,
                 'created_at'        => $now,
                 'updated_at'        => $now,
@@ -665,7 +702,8 @@ class ProductTableSeeder extends Seeder
                 'qty'               => 3,
                 'value_type'        => 'fixed',
                 'value'             => 50,
-                'product_id'        => 1,
+                'priceable_id'        => 1,
+                'priceable_type'      => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id' => 2,
                 'created_at'        => $now,
                 'updated_at'        => $now,
@@ -675,7 +713,8 @@ class ProductTableSeeder extends Seeder
                 'qty'               => 3,
                 'value_type'        => 'fixed',
                 'value'             => 50,
-                'product_id'        => 1,
+                'priceable_id'        => 1,
+                'priceable_type'      => 'Webkul\\Product\\Models\\Product',
                 'customer_group_id' => 3,
                 'created_at'        => $now,
                 'updated_at'        => $now,

+ 79 - 26
packages/Webkul/Product/src/Helpers/Indexers/Price.php

@@ -64,6 +64,9 @@ class Price extends AbstractIndexer
                     'variants.customer_group_prices',
                     'catalog_rule_prices',
                     'variants.catalog_rule_prices',
+                    'flexibleVariants',
+                    'flexibleVariants.price_indices',
+                    'flexibleVariants.customer_group_prices',
                 ])
                 ->cursorPaginate($this->batchSize);
 
@@ -102,6 +105,9 @@ class Price extends AbstractIndexer
                     'variants.customer_group_prices',
                     'catalog_rule_prices',
                     'variants.catalog_rule_prices',
+                    'flexibleVariants',
+                    'flexibleVariants.price_indices',
+                    'flexibleVariants.customer_group_prices',
                 ])
                 ->join('product_attribute_values as special_price_from_pav', function ($join) {
                     $join->on('products.id', '=', 'special_price_from_pav.product_id')
@@ -144,40 +150,87 @@ class Price extends AbstractIndexer
             $indexer = $this->getTypeIndexer($product)
                 ->setProduct($product);
 
+            $isFlexibleVariant = $product->type === 'flexible_variant';
+
             foreach ($this->getChannels() as $channel) {
                 foreach ($this->getCustomerGroups() as $customerGroup) {
-                    $customerGroupIndex = $product->price_indices
-                        ->where('channel_id', $channel->id)
-                        ->where('customer_group_id', $customerGroup->id)
-                        ->where('product_id', $product->id)
-                        ->first();
-
-                    $newIndex = $indexer
-                        ->setChannel($channel)
-                        ->setCustomerGroup($customerGroup)
-                        ->getIndices();
-
-                    if ($customerGroupIndex) {
-                        $oldIndex = collect($customerGroupIndex->toArray())
-                            ->except('id', 'created_at', 'updated_at')
-                            ->toArray();
-
-                        $isIndexChanged = $this->isIndexChanged(
-                            $oldIndex,
-                            $newIndex
-                        );
-
-                        if ($isIndexChanged) {
-                            $this->productPriceIndexRepository->update($newIndex, $customerGroupIndex->id);
+                    $indexer->setChannel($channel)->setCustomerGroup($customerGroup);
+
+                    // Index each flexible variant individually
+                    if ($isFlexibleVariant) {
+                        $variants = $product->relationLoaded('flexibleVariants')
+                            ? $product->flexibleVariants
+                            : $product->flexibleVariants()->get();
+
+                        foreach ($variants as $variant) {
+                            if (! $variant->isSaleable()) {
+                                continue;
+                            }
+
+                            $variantIndex = $indexer->setVariant($variant)->getVariantIndices();
+
+                            $this->upsertIndex(
+                                $variant->price_indices ?? collect(),
+                                $variantIndex,
+                                $newIndices,
+                                $channel->id,
+                                $customerGroup->id,
+                                $variant->id,
+                                get_class($variant)
+                            );
                         }
-                    } else {
-                        $newIndices[] = $newIndex;
                     }
+
+                    // Product-level index (aggregated for flexible_variant, normal for others)
+                    $newIndex = $indexer->getIndices();
+
+                    $this->upsertIndex(
+                        $product->price_indices,
+                        $newIndex,
+                        $newIndices,
+                        $channel->id,
+                        $customerGroup->id,
+                        $product->id,
+                        get_class($product)
+                    );
                 }
             }
         }
 
-        $this->productPriceIndexRepository->insert($newIndices);
+        if (! empty($newIndices)) {
+            $this->productPriceIndexRepository->insert($newIndices);
+        }
+    }
+
+    /**
+     * Upsert a single price index entry: update if exists, or queue for bulk insert.
+     */
+    protected function upsertIndex(
+        $existingIndices,
+        array $newIndex,
+        array &$newIndices,
+        int $channelId,
+        int $customerGroupId,
+        int $priceableId,
+        string $priceableType
+    ): void {
+        $existing = $existingIndices
+            ->where('channel_id', $channelId)
+            ->where('customer_group_id', $customerGroupId)
+            ->where('priceable_id', $priceableId)
+            ->first();
+
+        if ($existing) {
+            $oldIndex = collect($existing->toArray())
+                ->except('id', 'created_at', 'updated_at')
+                ->toArray();
+
+            if ($this->isIndexChanged($oldIndex, $newIndex)) {
+                $this->productPriceIndexRepository->update($newIndex, $existing->id);
+            }
+        } else {
+            $newIndices[] = $newIndex;
+        }
     }
 
     /**

+ 2 - 1
packages/Webkul/Product/src/Helpers/Indexers/Price/AbstractType.php

@@ -92,7 +92,8 @@ abstract class AbstractType
             'regular_min_price' => $this->product->price ?? 0,
             'max_price'         => $minPrice ?? 0,
             'regular_max_price' => $this->product->price ?? 0,
-            'product_id'        => $this->product->id,
+            'priceable_id'      => $this->product->id,
+            'priceable_type'    => get_class($this->product),
             'channel_id'        => $this->channel->id,
             'customer_group_id' => $this->customerGroup->id,
         ];

+ 2 - 1
packages/Webkul/Product/src/Helpers/Indexers/Price/Bundle.php

@@ -16,7 +16,8 @@ class Bundle extends AbstractType
             'regular_min_price' => $this->getRegularMinimalPrice() ?? 0,
             'max_price'         => $this->getMaximumPrice() ?? 0,
             'regular_max_price' => $this->getRegularMaximumPrice() ?? 0,
-            'product_id'        => $this->product->id,
+            'priceable_id'      => $this->product->id,
+            'priceable_type'    => get_class($this->product),
             'channel_id'        => $this->channel->id,
             'customer_group_id' => $this->customerGroup->id,
         ];

+ 2 - 1
packages/Webkul/Product/src/Helpers/Indexers/Price/Configurable.php

@@ -16,7 +16,8 @@ class Configurable extends AbstractType
             'regular_min_price' => $this->getRegularMinimalPrice() ?? 0,
             'max_price'         => $this->getMaximumPrice() ?? 0,
             'regular_max_price' => $this->getRegularMaximumPrice() ?? 0,
-            'product_id'        => $this->product->id,
+            'priceable_id'      => $this->product->id,
+            'priceable_type'    => get_class($this->product),
             'channel_id'        => $this->channel->id,
             'customer_group_id' => $this->customerGroup->id,
         ];

+ 2 - 1
packages/Webkul/Product/src/Helpers/Indexers/Price/Grouped.php

@@ -16,7 +16,8 @@ class Grouped extends AbstractType
             'regular_min_price' => $this->getRegularMinimalPrice() ?? 0,
             'max_price'         => $this->getMaximumPrice() ?? 0,
             'regular_max_price' => $this->getRegularMaximumPrice() ?? 0,
-            'product_id'        => $this->product->id,
+            'priceable_id'      => $this->product->id,
+            'priceable_type'    => get_class($this->product),
             'channel_id'        => $this->channel->id,
             'customer_group_id' => $this->customerGroup->id,
         ];

+ 13 - 5
packages/Webkul/Product/src/Models/Product.php

@@ -96,9 +96,9 @@ class Product extends Model implements ProductContract
     /**
      * Get the product customer group prices that owns the product.
      */
-    public function customer_group_prices(): HasMany
+    public function customer_group_prices(): \Illuminate\Database\Eloquent\Relations\MorphMany
     {
-        return $this->hasMany(ProductCustomerGroupPriceProxy::modelClass());
+        return $this->morphMany(ProductCustomerGroupPriceProxy::modelClass(), 'priceable');
     }
 
     /**
@@ -112,9 +112,9 @@ class Product extends Model implements ProductContract
     /**
      * Get the price indices that owns the product.
      */
-    public function price_indices(): HasMany
+    public function price_indices(): \Illuminate\Database\Eloquent\Relations\MorphMany
     {
-        return $this->hasMany(ProductPriceIndexProxy::modelClass());
+        return $this->morphMany(ProductPriceIndexProxy::modelClass(), 'priceable');
     }
 
     /**
@@ -214,13 +214,21 @@ class Product extends Model implements ProductContract
     }
 
     /**
-     * Get the product variants that owns the product.
+     * Get the product variants that owns the product (Bagisto configurable variants).
      */
     public function variants(): HasMany
     {
         return $this->hasMany(static::class, 'parent_id');
     }
 
+    /**
+     * Get the flexible variants (Longyi product_variants table).
+     */
+    public function flexibleVariants(): HasMany
+    {
+        return $this->hasMany(\Longyi\Core\Models\ProductVariant::class, 'product_id');
+    }
+
     /**
      * Get the grouped products that owns the product.
      */

+ 5 - 4
packages/Webkul/Product/src/Models/ProductCustomerGroupPrice.php

@@ -22,17 +22,18 @@ class ProductCustomerGroupPrice extends Model implements ProductCustomerGroupPri
         'qty',
         'value_type',
         'value',
-        'product_id',
+        'priceable_id',
+        'priceable_type',
         'customer_group_id',
         'unique_id',
     ];
 
     /**
-     * Get the product that owns the customer group price.
+     * Get the owning priceable model (Product or ProductVariant).
      */
-    public function product()
+    public function priceable()
     {
-        return $this->belongsTo(ProductProxy::modelClass());
+        return $this->morphTo();
     }
 
     /**

+ 10 - 5
packages/Webkul/Product/src/Models/ProductPriceIndex.php

@@ -7,6 +7,10 @@ use Webkul\Core\Models\ChannelProxy;
 use Webkul\Customer\Models\CustomerGroupProxy;
 use Webkul\Product\Contracts\ProductPriceIndex as ProductPriceIndexContract;
 
+/**
+ * @property int    $priceable_id
+ * @property string $priceable_type
+ */
 class ProductPriceIndex extends Model implements ProductPriceIndexContract
 {
     /**
@@ -19,19 +23,20 @@ class ProductPriceIndex extends Model implements ProductPriceIndexContract
         'regular_min_price',
         'max_price',
         'regular_max_price',
-        'product_id',
+        'priceable_id',
+        'priceable_type',
         'channel_id',
         'customer_group_id',
     ];
 
     /**
-     * Get the product that owns the price index.
+     * Get the owning priceable model (Product or ProductVariant).
      *
-     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+     * @return \Illuminate\Database\Eloquent\Relations\MorphTo
      */
-    public function product()
+    public function priceable()
     {
-        return $this->belongsTo(ProductProxy::modelClass());
+        return $this->morphTo();
     }
 
     /**

+ 14 - 9
packages/Webkul/Product/src/Repositories/ProductCustomerGroupPriceRepository.php

@@ -16,12 +16,14 @@ class ProductCustomerGroupPriceRepository extends Repository
     }
 
     /**
-     * @param  \Webkul\Product\Contracts\Product  $product
+     * Save customer group prices for a priceable entity (Product or ProductVariant).
+     *
+     * @param  \Illuminate\Database\Eloquent\Model  $priceable  Product or ProductVariant
      * @return void
      */
-    public function saveCustomerGroupPrices(array $data, $product)
+    public function saveCustomerGroupPrices(array $data, $priceable)
     {
-        $previousCustomerGroupPriceIds = $product->customer_group_prices()->pluck('id');
+        $previousCustomerGroupPriceIds = $priceable->customer_group_prices()->pluck('id');
 
         if (isset($data['customer_group_prices'])) {
             foreach ($data['customer_group_prices'] as $customerGroupPriceId => $row) {
@@ -29,13 +31,15 @@ class ProductCustomerGroupPriceRepository extends Repository
 
                 $row['unique_id'] = implode('|', array_filter([
                     $row['qty'],
-                    $product->id,
+                    $priceable->id,
+                    get_class($priceable),
                     $row['customer_group_id'],
                 ]));
 
                 if (Str::contains($customerGroupPriceId, 'price_')) {
                     $this->create(array_merge([
-                        'product_id' => $product->id,
+                        'priceable_id'   => $priceable->id,
+                        'priceable_type' => get_class($priceable),
                     ], $row));
                 } else {
                     if (is_numeric($index = $previousCustomerGroupPriceIds->search($customerGroupPriceId))) {
@@ -53,13 +57,14 @@ class ProductCustomerGroupPriceRepository extends Repository
     }
 
     /**
-     * Check if product customer group prices already loaded then load from it.
+     * Get applicable customer group prices for a priceable entity.
      *
-     * @return object
+     * @param  \Illuminate\Database\Eloquent\Model  $priceable  Product or ProductVariant
+     * @return \Illuminate\Support\Collection
      */
-    public function prices($product, $customerGroupId)
+    public function prices($priceable, $customerGroupId)
     {
-        $prices = $product->customer_group_prices->filter(function ($customerGroupPrice) use ($customerGroupId) {
+        $prices = $priceable->customer_group_prices->filter(function ($customerGroupPrice) use ($customerGroupId) {
             return $customerGroupPrice->customer_group_id == $customerGroupId
                 || is_null($customerGroupPrice->customer_group_id);
         });

+ 6 - 2
packages/Webkul/Product/src/Repositories/ProductRepository.php

@@ -264,7 +264,8 @@ class ProductRepository extends Repository
                 ->leftJoin('product_price_indices', function ($join) {
                     $customerGroup = $this->customerRepository->getCurrentGroup();
 
-                    $join->on('products.id', '=', 'product_price_indices.product_id')
+                    $join->on('products.id', '=', 'product_price_indices.priceable_id')
+                        ->where('product_price_indices.priceable_type', 'Webkul\\Product\\Models\\Product')
                         ->where('product_price_indices.customer_group_id', $customerGroup->id);
                 });
 
@@ -570,7 +571,10 @@ class ProductRepository extends Repository
         $customerGroup = $this->customerRepository->getCurrentGroup();
 
         $query = $this->model
-            ->leftJoin('product_price_indices', 'products.id', 'product_price_indices.product_id')
+            ->leftJoin('product_price_indices', function ($join) {
+                $join->on('products.id', '=', 'product_price_indices.priceable_id')
+                    ->where('product_price_indices.priceable_type', 'Webkul\\Product\\Models\\Product');
+            })
             ->leftJoin('product_categories', 'products.id', 'product_categories.product_id')
             ->where('product_price_indices.customer_group_id', $customerGroup->id);