|
|
@@ -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);
|
|
|
+ }
|
|
|
+}
|