260 lines
9.0 KiB
PHP
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'), '.');
|
|
}
|
|
}
|