chengwl 1 тиждень тому
батько
коміт
d1a57b9466

+ 274 - 0
packages/Longyi/Core/src/Helpers/Indexers/Price/FlexibleVariant.php

@@ -0,0 +1,274 @@
+<?php
+
+namespace Longyi\Core\Helpers\Indexers\Price;
+
+use Carbon\Carbon;
+use Illuminate\Support\Facades\DB;
+use Longyi\Core\Models\ProductVariant;
+use Webkul\Customer\Repositories\CustomerRepository;
+use Webkul\Product\Helpers\Indexers\Price\AbstractType;
+use Webkul\Product\Repositories\ProductCustomerGroupPriceRepository;
+
+class FlexibleVariant extends AbstractType
+{
+    /**
+     * The variant currently being indexed.
+     */
+    protected ?ProductVariant $variant = null;
+
+    /**
+     * Cached catalog rule products for the parent product, keyed by channel-group.
+     */
+    protected ?array $catalogRuleProductsCache = null;
+
+    public function __construct(
+        protected CustomerRepository $customerRepository,
+        protected ProductCustomerGroupPriceRepository $productCustomerGroupPriceRepository,
+    ) {
+        parent::__construct(
+            $customerRepository,
+            $productCustomerGroupPriceRepository,
+            app(\Webkul\CatalogRule\Repositories\CatalogRuleProductPriceRepository::class),
+        );
+    }
+
+    /**
+     * Set the variant being indexed.
+     */
+    public function setVariant(?ProductVariant $variant): static
+    {
+        $this->variant = $variant;
+
+        return $this;
+    }
+
+    /**
+     * Product-level indices (aggregated from all saleable variants).
+     */
+    public function getIndices(): array
+    {
+        $variants = $this->getSaleableVariants();
+
+        $minPrices = [];
+        $maxPrices = [];
+        $regularMinPrices = [];
+        $regularMaxPrices = [];
+
+        foreach ($variants as $variant) {
+            $vi = $this->setVariant($variant)->getVariantIndices();
+
+            $minPrices[] = $vi['min_price'];
+            $maxPrices[] = $vi['max_price'];
+            $regularMinPrices[] = $vi['regular_min_price'];
+            $regularMaxPrices[] = $vi['regular_max_price'];
+        }
+
+        return [
+            'min_price'         => ! empty($minPrices) ? min($minPrices) : 0,
+            'regular_min_price' => ! empty($regularMinPrices) ? min($regularMinPrices) : 0,
+            'max_price'         => ! empty($maxPrices) ? max($maxPrices) : 0,
+            'regular_max_price' => ! empty($regularMaxPrices) ? max($regularMaxPrices) : 0,
+            'priceable_id'      => $this->product->id,
+            'priceable_type'    => get_class($this->product),
+            'channel_id'        => $this->channel->id,
+            'customer_group_id' => $this->customerGroup->id,
+        ];
+    }
+
+    /**
+     * Variant-level indices for the currently set variant.
+     */
+    public function getVariantIndices(): array
+    {
+        $variant = $this->variant;
+
+        $basePrice = (float) $variant->price;
+        $comparePrice = $variant->compare_price ? (float) $variant->compare_price : $basePrice;
+
+        $minPrice = $this->getVariantMinimalPrice($variant, $basePrice);
+
+        return [
+            'min_price'         => $minPrice,
+            'regular_min_price' => $comparePrice,
+            'max_price'         => $minPrice,
+            'regular_max_price' => $comparePrice,
+            'priceable_id'      => $variant->id,
+            'priceable_type'    => get_class($variant),
+            'channel_id'        => $this->channel->id,
+            'customer_group_id' => $this->customerGroup->id,
+        ];
+    }
+
+    /**
+     * Compute the minimal (best) price for a variant, considering:
+     * 1. Special price (time-bounded)
+     * 2. Catalog rule discount (from parent product's rules)
+     * 3. Customer group price
+     */
+    protected function getVariantMinimalPrice(ProductVariant $variant, float $basePrice): float
+    {
+        $prices = [$basePrice];
+
+        // 1. Special price
+        if ($variant->special_price && (float) $variant->special_price > 0) {
+            if (core()->isChannelDateInInterval($variant->special_price_from, $variant->special_price_to)) {
+                $prices[] = (float) $variant->special_price;
+            }
+        }
+
+        // 2. Catalog rule discount (applied to variant.price)
+        $catalogRulePrice = $this->getCatalogRuleDiscount($variant);
+        if ($catalogRulePrice !== null) {
+            $prices[] = $catalogRulePrice;
+        }
+
+        // 3. Customer group price
+        $customerGroupPrice = $this->getVariantCustomerGroupPrice($variant, $basePrice);
+        $prices[] = $customerGroupPrice;
+
+        return min($prices);
+    }
+
+    /**
+     * Compute catalog rule discount for a variant by reading the
+     * parent product's catalog_rule_products entries and applying
+     * the rule formula to the variant's own price.
+     */
+    protected function getCatalogRuleDiscount(ProductVariant $variant): ?float
+    {
+        $rules = $this->getParentCatalogRuleProducts();
+
+        if (empty($rules)) {
+            return null;
+        }
+
+        $today = Carbon::now()->format('Y-m-d');
+        $price = (float) $variant->price;
+        $applied = false;
+
+        foreach ($rules as $rule) {
+            if ($rule->channel_id != $this->channel->id) {
+                continue;
+            }
+
+            if ($rule->customer_group_id != $this->customerGroup->id) {
+                continue;
+            }
+
+            $startsFrom = $rule->starts_from ? Carbon::parse($rule->starts_from)->format('Y-m-d') : null;
+            $endsTill = $rule->ends_till ? Carbon::parse($rule->ends_till)->format('Y-m-d') : null;
+
+            if ($startsFrom && $today < $startsFrom) {
+                continue;
+            }
+
+            if ($endsTill && $today > $endsTill) {
+                continue;
+            }
+
+            $price = $this->calculateRulePrice($rule, $price);
+            $applied = true;
+
+            if ($rule->end_other_rules) {
+                break;
+            }
+        }
+
+        return $applied ? max(0, $price) : null;
+    }
+
+    /**
+     * Apply a single catalog rule's action to a price.
+     * Mirrors CatalogRuleProductPrice::calculate().
+     */
+    protected function calculateRulePrice(object $rule, float $price): float
+    {
+        return match ($rule->action_type) {
+            'to_fixed'   => min($rule->discount_amount, $price),
+            'to_percent' => $price * $rule->discount_amount / 100,
+            'by_fixed'   => max(0, $price - $rule->discount_amount),
+            'by_percent' => $price * (1 - $rule->discount_amount / 100),
+            default      => $price,
+        };
+    }
+
+    /**
+     * Get catalog_rule_products for the parent product (cached per indexer run).
+     */
+    protected function getParentCatalogRuleProducts(): array
+    {
+        if ($this->catalogRuleProductsCache === null) {
+            $this->catalogRuleProductsCache = DB::table('catalog_rule_products')
+                ->where('product_id', $this->product->id)
+                ->orderBy('sort_order')
+                ->orderBy('catalog_rule_id')
+                ->get()
+                ->all();
+        }
+
+        return $this->catalogRuleProductsCache;
+    }
+
+    /**
+     * Customer group price for a variant (mirrors AbstractType::getCustomerGroupPrice).
+     */
+    protected function getVariantCustomerGroupPrice(ProductVariant $variant, float $basePrice): float
+    {
+        $customerGroupPrices = $this->productCustomerGroupPriceRepository
+            ->prices($variant, $this->customerGroup->id);
+
+        if ($customerGroupPrices->isEmpty()) {
+            return $basePrice;
+        }
+
+        $lastQty = 1;
+        $lastPrice = $basePrice;
+
+        foreach ($customerGroupPrices as $cgp) {
+            if ($cgp->qty > 1 || $cgp->qty < $lastQty) {
+                continue;
+            }
+
+            if ($cgp->value_type == 'discount') {
+                if ($cgp->value >= 0 && $cgp->value <= 100) {
+                    $lastPrice = $basePrice - ($basePrice * $cgp->value) / 100;
+                    $lastQty = $cgp->qty;
+                }
+            } else {
+                if ($cgp->value >= 0 && $cgp->value < $lastPrice) {
+                    $lastPrice = $cgp->value;
+                    $lastQty = $cgp->qty;
+                }
+            }
+        }
+
+        return $lastPrice;
+    }
+
+    /**
+     * Get saleable variants from the parent product's loaded relation
+     * or query directly.
+     */
+    protected function getSaleableVariants(): \Illuminate\Support\Collection
+    {
+        if ($this->product->relationLoaded('flexibleVariants')) {
+            return $this->product->flexibleVariants->filter(fn ($v) => $v->isSaleable());
+        }
+
+        return ProductVariant::where('product_id', $this->product->id)
+            ->where('status', true)
+            ->where('quantity', '>', 0)
+            ->get();
+    }
+
+    /**
+     * Reset caches when switching products.
+     */
+    public function setProduct($product): static
+    {
+        $this->catalogRuleProductsCache = null;
+        $this->variant = null;
+
+        return parent::setProduct($product);
+    }
+}