Files
esun3d/app/code/Esun/Cart/Model/MilestoneProvider.php
2026-09-15 10:35:56 +08:00

260 lines
9.0 KiB
PHP

<?php
declare(strict_types=1);
namespace Esun\Cart\Model;
use Esun\Catalog\Model\MixStepPriceProvider;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Checkout\Model\Session as CheckoutSession;
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
use Magento\Framework\DataObject;
use Magento\Quote\Model\Quote;
use Magento\SalesRule\Api\Data\RuleInterface;
use Magento\SalesRule\Model\ResourceModel\Rule\CollectionFactory;
use Magento\Framework\Pricing\PriceCurrencyInterface;
class MilestoneProvider
{
public function __construct(
private readonly CollectionFactory $ruleCollectionFactory,
private readonly CheckoutSession $checkoutSession,
private readonly ProductRepositoryInterface $productRepository,
private readonly MixStepPriceProvider $mixStepPriceProvider,
private readonly PriceCurrencyInterface $priceCurrency
) {
}
public function getData(?Quote $quote = null): array
{
$quote ??= $this->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'), '.');
}
}