checkoutSession->getQuote(); $mixGroups = $this->getMixGroups($quote); if ($mixGroups) { return array_merge($mixGroups[0], ['groups' => $mixGroups]); } $quantity = (int)ceil((float)$quote->getItemsQty()); $milestones = $this->getMilestones($quote); $maximum = $milestones ? max(array_column($milestones, 'qty')) : 0; $active = null; $next = null; foreach ($milestones as &$milestone) { $milestone['active'] = $quantity >= $milestone['qty']; $milestone['position'] = $maximum > 0 ? round(min(100, ($milestone['qty'] / $maximum) * 100), 2) : 0; if ($milestone['active']) { $active = $milestone; } elseif ($next === null) { $next = $milestone; } } unset($milestone); return [ 'quantity' => $quantity, 'progress_percent' => $maximum > 0 ? round(min(100, ($quantity / $maximum) * 100), 2) : 0, 'milestones' => $milestones, 'message' => $this->getMessage($quantity, $active, $next), 'groups' => [], ]; } private function getMixGroups(Quote $quote): array { $groupedItems = []; foreach ($quote->getAllVisibleItems() as $item) { if ($item->getProductType() === Configurable::TYPE_CODE) { $groupedItems[(int)$item->getProductId()][] = $item; } } $groups = []; foreach ($groupedItems as $parentProductId => $items) { try { $parentProduct = $this->productRepository->getById( $parentProductId, false, (int)$quote->getStoreId() ); } catch (\Magento\Framework\Exception\NoSuchEntityException) { continue; } $rules = $this->mixStepPriceProvider->getRules( $parentProduct, (int)$quote->getCustomerGroupId() ); if (!$rules) { continue; } $quantity = (int)array_sum(array_map( static fn ($item): float => (float)$item->getQty(), $items )); $milestones = []; foreach ($rules as $rule) { $isFixed = ($rule['price_type'] ?? '') === 'fixed'; $value = $isFixed ? $this->priceCurrency->convertAndFormat( (float)$rule['value'], false, 2, $quote->getStore() ) : $this->formatPercent((float)$rule['value']) . '% Off'; $milestones[] = [ 'qty' => (int)$rule['qty'], 'type' => 'discount', 'value' => $value, 'label' => (string)__('%1 pcs', (int)$rule['qty']), ]; } usort($milestones, static fn (array $left, array $right): int => $left['qty'] <=> $right['qty']); $maximum = $milestones ? max(array_column($milestones, 'qty')) : 0; $active = null; $next = null; foreach ($milestones as &$milestone) { $milestone['active'] = $quantity >= $milestone['qty']; $milestone['position'] = $maximum > 0 ? round(min(100, ($milestone['qty'] / $maximum) * 100), 2) : 0; if ($milestone['active']) { $active = $milestone; } elseif ($next === null) { $next = $milestone; } } unset($milestone); $groups[] = [ 'product_id' => $parentProductId, 'name' => (string)$parentProduct->getName(), 'quantity' => $quantity, 'progress_percent' => $maximum > 0 ? round(min(100, ($quantity / $maximum) * 100), 2) : 0, 'milestones' => $milestones, 'message' => $this->getMessage($quantity, $active, $next), ]; } return $groups; } private function getMilestones(Quote $quote): array { if (!$quote->getStoreId()) { return []; } $collection = $this->ruleCollectionFactory->create(); $collection->setValidationFilter( (int)$quote->getStore()->getWebsiteId(), (int)$quote->getCustomerGroupId(), (string)$quote->getCouponCode(), null, $quote->getShippingAddress() ); $items = []; foreach ($collection as $rule) { $quantity = $this->findQuantityThreshold($rule->getConditions()); if ($quantity === null || $quantity < 1) { continue; } $discountAmount = (float)$rule->getDiscountAmount(); if ($rule->getSimpleAction() === RuleInterface::DISCOUNT_ACTION_BY_PERCENT && $discountAmount > 0) { $key = $quantity . ':discount'; $items[$key] = [ 'qty' => $quantity, 'type' => 'discount', 'value' => $this->formatPercent($discountAmount) . '% Off', 'label' => (string)__('%1 pcs %2% off', $quantity, $this->formatPercent($discountAmount)), 'discount' => $discountAmount, ]; } if ((int)$rule->getSimpleFreeShipping() > 0) { $items[$quantity . ':free_shipping'] = [ 'qty' => $quantity, 'type' => 'free_shipping', 'value' => (string)__('Free Shipping'), 'label' => (string)__('%1 pcs', $quantity), ]; } } $milestones = array_values($items); usort($milestones, static function (array $left, array $right): int { $quantityComparison = $left['qty'] <=> $right['qty']; if ($quantityComparison !== 0) { return $quantityComparison; } return $left['type'] === 'discount' ? -1 : 1; }); return $milestones; } private function findQuantityThreshold(DataObject $condition): ?int { $thresholds = []; $this->collectQuantityThresholds($condition, $thresholds); return $thresholds ? max($thresholds) : null; } private function collectQuantityThresholds(DataObject $condition, array &$thresholds): void { if ((string)$condition->getData('attribute') === 'total_qty') { $value = (float)$condition->getData('value'); $operator = (string)$condition->getData('operator'); if ($operator === '>=') { $thresholds[] = (int)ceil($value); } elseif ($operator === '>') { $thresholds[] = (int)floor($value) + 1; } elseif ($operator === '==') { $thresholds[] = (int)round($value); } } $children = method_exists($condition, 'getConditions') ? (array)$condition->getConditions() : (array)$condition->getData('conditions'); foreach ($children as $child) { if ($child instanceof DataObject) { $this->collectQuantityThresholds($child, $thresholds); } } } private function getMessage(int $quantity, ?array $active, ?array $next): string { if ($next) { $remaining = max(0, (int)$next['qty'] - $quantity); $prefix = $active ? $active['value'] . ' | ' : ''; return $prefix . (string)__('Add %1 More For %2', $remaining, $next['value']); } if ($active) { return (string)__('Unlocked: %1', $active['value']); } return ''; } private function formatPercent(float $value): string { return rtrim(rtrim(number_format($value, 2, '.', ''), '0'), '.'); } }