-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Pre-qualification and runtime starting to take shape
- Loading branch information
Showing
43 changed files
with
786 additions
and
483 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Setono\SyliusCatalogPromotionPlugin\Applicator; | ||
|
||
use Setono\SyliusCatalogPromotionPlugin\Checker\Runtime\RuntimeCheckerInterface; | ||
use Setono\SyliusCatalogPromotionPlugin\Model\PromotionInterface; | ||
use Setono\SyliusCatalogPromotionPlugin\Repository\PromotionRepositoryInterface; | ||
|
||
final class RuntimePromotionsApplicator implements RuntimePromotionsApplicatorInterface | ||
{ | ||
/** @var array<string, float> */ | ||
private array $multiplierCache = []; | ||
|
||
/** @var array<string, PromotionInterface|null> */ | ||
private array $catalogPromotionCache = []; | ||
|
||
public function __construct( | ||
private readonly PromotionRepositoryInterface $promotionRepository, | ||
private readonly RuntimeCheckerInterface $runtimeChecker, | ||
) { | ||
} | ||
|
||
public function apply(array $catalogPromotions, int $price, bool $manuallyDiscounted): int | ||
{ | ||
if ([] === $catalogPromotions) { | ||
return $price; | ||
} | ||
|
||
return (int) floor($this->getMultiplier($catalogPromotions, $manuallyDiscounted) * $price); | ||
} | ||
|
||
/** | ||
* @param list<string> $catalogPromotions | ||
*/ | ||
private function getMultiplier(array $catalogPromotions, bool $manuallyDiscounted): float | ||
{ | ||
$cacheKey = sprintf('%s%d', implode($catalogPromotions), (int) $manuallyDiscounted); | ||
|
||
if (!isset($this->multiplierCache[$cacheKey])) { | ||
$multiplier = 1.0; | ||
|
||
foreach ($this->getEligiblePromotions($catalogPromotions, $manuallyDiscounted) as $promotion) { | ||
$multiplier *= $promotion->getMultiplier(); | ||
} | ||
|
||
$this->multiplierCache[$cacheKey] = $multiplier; | ||
} | ||
|
||
return $this->multiplierCache[$cacheKey]; | ||
} | ||
|
||
/** | ||
* @param list<string> $catalogPromotions | ||
* | ||
* @return \Generator<array-key, PromotionInterface> | ||
*/ | ||
private function getEligiblePromotions(array $catalogPromotions, bool $manuallyDiscounted): \Generator | ||
{ | ||
// todo check if any of the promotions are exclusive | ||
foreach ($catalogPromotions as $catalogPromotion) { | ||
if (!array_key_exists($catalogPromotion, $this->catalogPromotionCache)) { | ||
$this->catalogPromotionCache[$catalogPromotion] = $this->promotionRepository->findOneByCode($catalogPromotion); | ||
} | ||
|
||
if (null === $this->catalogPromotionCache[$catalogPromotion]) { | ||
continue; | ||
} | ||
|
||
if ($manuallyDiscounted && $this->catalogPromotionCache[$catalogPromotion]->isManuallyDiscountedProductsExcluded()) { | ||
continue; | ||
} | ||
|
||
if (!$this->runtimeChecker->isEligible($this->catalogPromotionCache[$catalogPromotion])) { | ||
continue; | ||
} | ||
|
||
yield $this->catalogPromotionCache[$catalogPromotion]; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Setono\SyliusCatalogPromotionPlugin\Applicator; | ||
|
||
interface RuntimePromotionsApplicatorInterface | ||
{ | ||
/** | ||
* @param list<string> $catalogPromotions The codes of the catalog promotions to apply | ||
* @param int $price The price before the catalog promotions have been applied | ||
* @param bool $manuallyDiscounted Whether the price has been manually discounted | ||
* | ||
* @return int The price after the catalog promotions have been applied | ||
*/ | ||
public function apply(array $catalogPromotions, int $price, bool $manuallyDiscounted): int; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Setono\SyliusCatalogPromotionPlugin\Calculator; | ||
|
||
use Setono\SyliusCatalogPromotionPlugin\Applicator\RuntimePromotionsApplicatorInterface; | ||
use Setono\SyliusCatalogPromotionPlugin\Model\ProductInterface; | ||
use Sylius\Component\Core\Calculator\ProductVariantPricesCalculatorInterface; | ||
use Sylius\Component\Core\Exception\MissingChannelConfigurationException; | ||
use Sylius\Component\Core\Model\ChannelInterface; | ||
use Sylius\Component\Core\Model\ChannelPricingInterface; | ||
use Sylius\Component\Core\Model\ProductVariantInterface; | ||
use Webmozart\Assert\Assert; | ||
|
||
final class ProductVariantPricesCalculator implements ProductVariantPricesCalculatorInterface | ||
{ | ||
/** @var array<string, array{price: int, originalPrice: int, minimumPrice: int}> */ | ||
private array $persistedPricesCache = []; | ||
|
||
/** @var array<string, int> */ | ||
private array $computedPriceCache = []; | ||
|
||
public function __construct(private readonly RuntimePromotionsApplicatorInterface $runtimePromotionsApplicator) | ||
{ | ||
} | ||
|
||
public function calculate(ProductVariantInterface $productVariant, array $context): int | ||
{ | ||
$hash = spl_object_hash($productVariant); | ||
if (!isset($this->computedPriceCache[$hash])) { | ||
$this->computedPriceCache[$hash] = $this->getPrice($productVariant, $context); | ||
} | ||
|
||
return $this->computedPriceCache[$hash]; | ||
} | ||
|
||
public function calculateOriginal(ProductVariantInterface $productVariant, array $context): int | ||
{ | ||
return $this->getPersistedPrices($productVariant, $context)['originalPrice']; | ||
} | ||
|
||
private function getPrice(ProductVariantInterface $productVariant, array $context): int | ||
{ | ||
$prices = $this->getPersistedPrices($productVariant, $context); | ||
|
||
$product = $productVariant->getProduct(); | ||
if (!$product instanceof ProductInterface || !$product->hasPreQualifiedCatalogPromotions()) { | ||
return $prices['price']; | ||
} | ||
|
||
return max($prices['minimumPrice'], $this->runtimePromotionsApplicator->apply( | ||
$product->getPreQualifiedCatalogPromotions(), | ||
$prices['price'], | ||
$prices['price'] < $prices['originalPrice'], | ||
)); | ||
} | ||
|
||
/** | ||
* @psalm-assert ChannelInterface $context['channel'] | ||
* | ||
* @return array{price: int, originalPrice: int, minimumPrice: int} | ||
*/ | ||
private function getPersistedPrices(ProductVariantInterface $productVariant, array $context): array | ||
{ | ||
$hash = spl_object_hash($productVariant); | ||
|
||
if (!isset($this->persistedPricesCache[$hash])) { | ||
Assert::keyExists($context, 'channel'); | ||
Assert::isInstanceOf($context['channel'], ChannelInterface::class); | ||
|
||
$channelPricing = $productVariant->getChannelPricingForChannel($context['channel']); | ||
|
||
if (null === $channelPricing) { | ||
throw MissingChannelConfigurationException::createForProductVariantChannelPricing($productVariant, $context['channel']); | ||
Check failure on line 75 in src/Calculator/ProductVariantPricesCalculator.php GitHub Actions / Static Code Analysis (PHP8.1 | Deps: lowest | SF~5.4.0)UndefinedMethod
|
||
} | ||
|
||
$price = $channelPricing->getPrice(); | ||
if (null === $price) { | ||
throw MissingChannelConfigurationException::createForProductVariantChannelPricing($productVariant, $context['channel']); | ||
Check failure on line 80 in src/Calculator/ProductVariantPricesCalculator.php GitHub Actions / Static Code Analysis (PHP8.1 | Deps: lowest | SF~5.4.0)UndefinedMethod
|
||
} | ||
|
||
$this->persistedPricesCache[$hash] = [ | ||
'price' => $price, | ||
'originalPrice' => $channelPricing->getOriginalPrice() ?? $price, | ||
'minimumPrice' => self::getMinimumPrice($channelPricing), | ||
]; | ||
} | ||
|
||
return $this->persistedPricesCache[$hash]; | ||
} | ||
|
||
private static function getMinimumPrice(ChannelPricingInterface $channelPricing): int | ||
Check failure on line 93 in src/Calculator/ProductVariantPricesCalculator.php GitHub Actions / Static Code Analysis (PHP8.1 | Deps: lowest | SF~5.4.0)MixedInferredReturnType
|
||
{ | ||
if (method_exists($channelPricing, 'getMinimumPrice')) { | ||
return $channelPricing->getMinimumPrice(); | ||
Check failure on line 96 in src/Calculator/ProductVariantPricesCalculator.php GitHub Actions / Static Code Analysis (PHP8.1 | Deps: lowest | SF~5.4.0)MixedReturnStatement
|
||
} | ||
|
||
return 0; | ||
} | ||
} |
16 changes: 16 additions & 0 deletions
16
src/Checker/PreQualification/PreQualificationCheckerInterface.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Setono\SyliusCatalogPromotionPlugin\Checker\PreQualification; | ||
|
||
use Setono\SyliusCatalogPromotionPlugin\Model\ProductInterface; | ||
use Setono\SyliusCatalogPromotionPlugin\Model\PromotionInterface; | ||
|
||
interface PreQualificationCheckerInterface | ||
{ | ||
/** | ||
* Checks the pre-qualification criteria for the product and returns true if the product is pre-qualified for the promotion | ||
*/ | ||
public function isPreQualified(ProductInterface $product, PromotionInterface $catalogPromotion): bool; | ||
} |
12 changes: 12 additions & 0 deletions
12
src/Checker/PreQualification/Rule/RuleCheckerInterface.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<?php | ||
declare(strict_types=1); | ||
|
||
namespace Setono\SyliusCatalogPromotionPlugin\Checker\PreQualification\Rule; | ||
|
||
|
||
use Sylius\Component\Core\Model\ProductInterface; | ||
|
||
interface RuleCheckerInterface | ||
{ | ||
public function isEligible(ProductInterface $product, array $configuration): bool; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Setono\SyliusCatalogPromotionPlugin\Checker\Runtime; | ||
|
||
use Setono\SyliusCatalogPromotionPlugin\Model\PromotionInterface; | ||
use Sylius\Component\Channel\Context\ChannelContextInterface; | ||
|
||
final class ChannelContextRuntimeChecker implements RuntimeCheckerInterface | ||
{ | ||
public function __construct(private readonly ChannelContextInterface $channelContext) | ||
{ | ||
} | ||
|
||
public function isEligible(PromotionInterface $catalogPromotion): bool | ||
{ | ||
return $catalogPromotion->getChannels()->contains($this->channelContext->getChannel()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Setono\SyliusCatalogPromotionPlugin\Checker\Runtime; | ||
|
||
use Setono\CompositeCompilerPass\CompositeService; | ||
use Setono\SyliusCatalogPromotionPlugin\Model\PromotionInterface; | ||
|
||
/** | ||
* @extends CompositeService<RuntimeCheckerInterface> | ||
*/ | ||
final class CompositeRuntimeChecker extends CompositeService implements RuntimeCheckerInterface | ||
{ | ||
public function isEligible(PromotionInterface $catalogPromotion): bool | ||
{ | ||
foreach ($this->services as $service) { | ||
if (!$service->isEligible($catalogPromotion)) { | ||
return false; | ||
} | ||
} | ||
|
||
return true; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Setono\SyliusCatalogPromotionPlugin\Checker\Runtime; | ||
|
||
use Psr\Clock\ClockInterface; | ||
use Setono\SyliusCatalogPromotionPlugin\Model\PromotionInterface; | ||
|
||
final class DateRuntimeChecker implements RuntimeCheckerInterface | ||
{ | ||
public function __construct(private readonly ClockInterface $clock) | ||
{ | ||
} | ||
|
||
public function isEligible(PromotionInterface $catalogPromotion): bool | ||
{ | ||
$now = $this->clock->now(); | ||
|
||
$startsAt = $catalogPromotion->getStartsAt(); | ||
if (null !== $startsAt && $startsAt > $now) { | ||
return false; | ||
} | ||
|
||
$endsAt = $catalogPromotion->getEndsAt(); | ||
if (null !== $endsAt && $endsAt < $now) { | ||
return false; | ||
} | ||
|
||
return true; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Setono\SyliusCatalogPromotionPlugin\Checker\Runtime; | ||
|
||
use Setono\SyliusCatalogPromotionPlugin\Model\PromotionInterface; | ||
|
||
final class EnabledRuntimeChecker implements RuntimeCheckerInterface | ||
{ | ||
public function isEligible(PromotionInterface $catalogPromotion): bool | ||
{ | ||
return $catalogPromotion->isEnabled(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Setono\SyliusCatalogPromotionPlugin\Checker\Runtime; | ||
|
||
use Setono\SyliusCatalogPromotionPlugin\Model\PromotionInterface; | ||
|
||
interface RuntimeCheckerInterface | ||
{ | ||
/** | ||
* Returns true if runtime checks are eligible for the given catalog promotion. | ||
* This is meant to be run each time a price is calculated on a product. | ||
* This implies that the runtime checks should be fast and not perform any heavy operations. | ||
*/ | ||
public function isEligible(PromotionInterface $catalogPromotion): bool; | ||
} |
Oops, something went wrong.