Skip to content

Commit 83e3f64

Browse files
authored
Merge pull request #73 from Setono/price-source
Adding a price source on catalog promotions
2 parents 2f20442 + 0031042 commit 83e3f64

20 files changed

+198
-131
lines changed

src/Applicator/RuntimePromotionsApplicator.php

+38-48
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
use Psr\EventDispatcher\EventDispatcherInterface;
99
use Setono\Doctrine\ORMTrait;
1010
use Setono\SyliusCatalogPromotionPlugin\Checker\Runtime\RuntimeCheckerInterface;
11-
use Setono\SyliusCatalogPromotionPlugin\Event\CatalogPromotionAppliedEvent;
11+
use Setono\SyliusCatalogPromotionPlugin\Event\CatalogPromotionsAppliedEvent;
1212
use Setono\SyliusCatalogPromotionPlugin\Model\CatalogPromotionInterface;
1313
use Setono\SyliusCatalogPromotionPlugin\Model\ProductInterface;
1414
use Setono\SyliusCatalogPromotionPlugin\Repository\CatalogPromotionRepositoryInterface;
@@ -17,9 +17,6 @@ final class RuntimePromotionsApplicator implements RuntimePromotionsApplicatorIn
1717
{
1818
use ORMTrait;
1919

20-
/** @var array<string, float> */
21-
private array $multiplierCache = [];
22-
2320
/** @var array<string, CatalogPromotionInterface|null> */
2421
private array $catalogPromotionCache = [];
2522

@@ -34,95 +31,88 @@ public function __construct(
3431
$this->managerRegistry = $managerRegistry;
3532
}
3633

37-
public function apply(ProductInterface $product, int $price, bool $manuallyDiscounted): int
34+
public function apply(ProductInterface $product, int $price, int $originalPrice = null): int
3835
{
36+
$originalPrice = $originalPrice ?? $price;
37+
$manuallyDiscounted = $price < $originalPrice;
38+
3939
$catalogPromotions = $product->getPreQualifiedCatalogPromotions();
4040

4141
if ([] === $catalogPromotions) {
4242
return $price;
4343
}
4444

45-
$appliedPrice = (int) floor($this->getMultiplier($catalogPromotions, $manuallyDiscounted) * $price);
46-
if ($appliedPrice !== $price) {
47-
$this->eventDispatcher->dispatch(new CatalogPromotionAppliedEvent($product));
48-
}
45+
$catalogPromotions = $this->provideEligibleCatalogPromotions($catalogPromotions, $manuallyDiscounted);
46+
foreach ($catalogPromotions as $catalogPromotion) {
47+
if (!$catalogPromotion->isManuallyDiscountedProductsExcluded() && $catalogPromotion->isUsingOriginalPriceAsBase()) {
48+
$price = $originalPrice;
4949

50-
return $appliedPrice;
51-
}
50+
break;
51+
}
52+
}
5253

53-
/**
54-
* @param list<string> $catalogPromotions
55-
*/
56-
private function getMultiplier(array $catalogPromotions, bool $manuallyDiscounted): float
57-
{
58-
$cacheKey = sprintf('%s%d', implode($catalogPromotions), (int) $manuallyDiscounted);
54+
$multiplier = 1.0;
5955

60-
if (!isset($this->multiplierCache[$cacheKey])) {
61-
$multiplier = 1.0;
56+
$appliedCatalogPromotions = [];
57+
foreach ($catalogPromotions as $catalogPromotion) {
58+
$multiplier *= 1 - $catalogPromotion->getDiscount();
6259

63-
foreach ($this->getEligibleCatalogPromotions($catalogPromotions, $manuallyDiscounted) as $catalogPromotion) {
64-
$multiplier *= $catalogPromotion->getMultiplier();
65-
}
60+
$appliedCatalogPromotions[] = $catalogPromotion;
61+
}
6662

67-
$this->multiplierCache[$cacheKey] = $multiplier;
63+
if ([] !== $appliedCatalogPromotions) {
64+
$this->eventDispatcher->dispatch(new CatalogPromotionsAppliedEvent($product, $appliedCatalogPromotions));
6865
}
6966

70-
return $this->multiplierCache[$cacheKey];
67+
return (int) floor($price * $multiplier);
7168
}
7269

7370
/**
7471
* @param list<string> $catalogPromotions
7572
*
76-
* @return \Generator<array-key, CatalogPromotionInterface>
73+
* @return list<CatalogPromotionInterface>
7774
*/
78-
private function getEligibleCatalogPromotions(array $catalogPromotions, bool $manuallyDiscounted): \Generator
75+
private function provideEligibleCatalogPromotions(array $catalogPromotions, bool $manuallyDiscounted): array
7976
{
8077
$eligiblePromotions = [];
8178
$eligibleExclusivePromotions = [];
8279

8380
foreach ($catalogPromotions as $catalogPromotion) {
84-
$this->ensureCatalogPromotionCache($catalogPromotion);
85-
86-
if (null === $this->catalogPromotionCache[$catalogPromotion]) {
81+
$catalogPromotion = $this->cacheCatalogPromotion($catalogPromotion);
82+
if (null === $catalogPromotion) {
8783
continue;
8884
}
8985

90-
if ($manuallyDiscounted && $this->catalogPromotionCache[$catalogPromotion]->isManuallyDiscountedProductsExcluded()) {
86+
if ($manuallyDiscounted && $catalogPromotion->isManuallyDiscountedProductsExcluded()) {
9187
continue;
9288
}
9389

94-
if (!$this->runtimeChecker->isEligible($this->catalogPromotionCache[$catalogPromotion])) {
90+
if (!$this->runtimeChecker->isEligible($catalogPromotion)) {
9591
continue;
9692
}
9793

98-
$eligiblePromotions[] = $this->catalogPromotionCache[$catalogPromotion];
94+
$eligiblePromotions[] = $catalogPromotion;
9995

100-
if ($this->catalogPromotionCache[$catalogPromotion]->isExclusive()) {
101-
$eligibleExclusivePromotions[$this->catalogPromotionCache[$catalogPromotion]->getPriority()] = $this->catalogPromotionCache[$catalogPromotion];
96+
if ($catalogPromotion->isExclusive()) {
97+
$eligibleExclusivePromotions[$catalogPromotion->getPriority()] = $catalogPromotion;
10298
}
10399
}
104100

105101
if ([] !== $eligibleExclusivePromotions) {
106102
krsort($eligibleExclusivePromotions, \SORT_NUMERIC);
107-
yield reset($eligibleExclusivePromotions);
108-
} else {
109-
yield from $eligiblePromotions;
103+
104+
return [reset($eligibleExclusivePromotions)];
110105
}
106+
107+
return $eligiblePromotions;
111108
}
112109

113-
private function ensureCatalogPromotionCache(string $catalogPromotion): void
110+
private function cacheCatalogPromotion(string $catalogPromotion): ?CatalogPromotionInterface
114111
{
115-
if (array_key_exists($catalogPromotion, $this->catalogPromotionCache)) {
116-
if (null === $this->catalogPromotionCache[$catalogPromotion]) {
117-
return;
118-
}
119-
120-
// If the entity is still managed, we don't need to do anything
121-
if ($this->getManager($this->catalogPromotionClass)->contains($this->catalogPromotionCache[$catalogPromotion])) {
122-
return;
123-
}
112+
if (!array_key_exists($catalogPromotion, $this->catalogPromotionCache) || (null !== $this->catalogPromotionCache[$catalogPromotion] && !$this->getManager($this->catalogPromotionClass)->contains($this->catalogPromotionCache[$catalogPromotion]))) {
113+
$this->catalogPromotionCache[$catalogPromotion] = $this->getRepository($this->catalogPromotionClass, CatalogPromotionRepositoryInterface::class)->findOneByCode($catalogPromotion);
124114
}
125115

126-
$this->catalogPromotionCache[$catalogPromotion] = $this->getRepository($this->catalogPromotionClass, CatalogPromotionRepositoryInterface::class)->findOneByCode($catalogPromotion);
116+
return $this->catalogPromotionCache[$catalogPromotion];
127117
}
128118
}

src/Applicator/RuntimePromotionsApplicatorInterface.php

+1-3
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,8 @@ interface RuntimePromotionsApplicatorInterface
1010
{
1111
/**
1212
* @param ProductInterface $product The product to apply catalog promotions from
13-
* @param int $price The price to apply catalog promotions to
14-
* @param bool $manuallyDiscounted Whether the price has been manually discounted
1513
*
1614
* @return int The price after the catalog promotions have been applied
1715
*/
18-
public function apply(ProductInterface $product, int $price, bool $manuallyDiscounted): int;
16+
public function apply(ProductInterface $product, int $price, int $originalPrice = null): int;
1917
}

src/Calculator/ProductVariantPricesCalculator.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ private function getPrice(ProductVariantInterface $productVariant, array $contex
5252
return max($prices['minimumPrice'], $this->runtimePromotionsApplicator->apply(
5353
$product,
5454
$prices['price'],
55-
$prices['price'] < $prices['originalPrice'],
55+
$prices['originalPrice'],
5656
));
5757
}
5858

src/Checker/OnSale/OnSaleChecker.php

+4-2
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,10 @@ private function checkVariant(ProductVariantInterface $variant, ChannelInterface
5656
$product = $variant->getProduct();
5757
Assert::isInstanceOf($product, ProductInterface::class);
5858

59-
$appliedPrice = $this->runtimePromotionsApplicator->apply($product, (int) $channelPricing->getPrice(), false);
59+
$price = (int) $channelPricing->getPrice();
6060

61-
return $appliedPrice < $channelPricing->getPrice();
61+
$appliedPrice = $this->runtimePromotionsApplicator->apply($product, $price, $channelPricing->getOriginalPrice());
62+
63+
return $appliedPrice < $price;
6264
}
6365
}

src/DependencyInjection/SetonoSyliusCatalogPromotionExtension.php

+14
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,20 @@ public function prepend(ContainerBuilder $container): void
344344
],
345345
],
346346
],
347+
'setono_sylius_catalog_promotion.admin.catalog_promotion.create.javascripts' => [
348+
'blocks' => [
349+
'javascripts' => [
350+
'template' => '@SetonoSyliusCatalogPromotionPlugin/admin/catalog_promotion/_javascripts.html.twig',
351+
],
352+
],
353+
],
354+
'setono_sylius_catalog_promotion.admin.catalog_promotion.update.javascripts' => [
355+
'blocks' => [
356+
'javascripts' => [
357+
'template' => '@SetonoSyliusCatalogPromotionPlugin/admin/catalog_promotion/_javascripts.html.twig',
358+
],
359+
],
360+
],
347361
],
348362
]);
349363
}

src/Event/CatalogPromotionAppliedEvent.php

-18
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Setono\SyliusCatalogPromotionPlugin\Event;
6+
7+
use Setono\SyliusCatalogPromotionPlugin\Model\CatalogPromotionInterface;
8+
use Setono\SyliusCatalogPromotionPlugin\Model\ProductInterface;
9+
10+
/**
11+
* Event dispatched when catalog promotions was applied to a product.
12+
*/
13+
final class CatalogPromotionsAppliedEvent
14+
{
15+
public function __construct(
16+
public readonly ProductInterface $product,
17+
/** @var non-empty-list<CatalogPromotionInterface> $catalogPromotions */
18+
public readonly array $catalogPromotions,
19+
) {
20+
}
21+
}

src/Form/Type/CatalogPromotionType.php

+4
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
4646
'label' => 'setono_sylius_catalog_promotion.form.catalog_promotion.manually_discounted_products_excluded',
4747
'required' => false,
4848
])
49+
->add('usingOriginalPriceAsBase', CheckboxType::class, [
50+
'label' => 'setono_sylius_catalog_promotion.form.catalog_promotion.using_original_price_as_base',
51+
'required' => false,
52+
])
4953
->add('startsAt', DateTimeType::class, [
5054
'label' => 'setono_sylius_catalog_promotion.form.catalog_promotion.starts_at',
5155
'date_widget' => 'single_text',

src/Model/CatalogPromotion.php

+12-5
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ class CatalogPromotion implements CatalogPromotionInterface
3434

3535
protected bool $manuallyDiscountedProductsExcluded = true;
3636

37+
protected bool $usingOriginalPriceAsBase = false;
38+
3739
protected ?DateTimeInterface $startsAt = null;
3840

3941
protected ?DateTimeInterface $endsAt = null;
@@ -64,11 +66,6 @@ public function __toString(): string
6466
return (string) ($this->getName() ?? $this->getCode() ?? $this->getId());
6567
}
6668

67-
public function getMultiplier(): float
68-
{
69-
return 1 - $this->getDiscount();
70-
}
71-
7269
public function getId(): ?int
7370
{
7471
return $this->id;
@@ -134,6 +131,16 @@ public function setManuallyDiscountedProductsExcluded(bool $manuallyDiscountedPr
134131
$this->manuallyDiscountedProductsExcluded = $manuallyDiscountedProductsExcluded;
135132
}
136133

134+
public function isUsingOriginalPriceAsBase(): bool
135+
{
136+
return $this->usingOriginalPriceAsBase;
137+
}
138+
139+
public function setUsingOriginalPriceAsBase(bool $usingOriginalPriceAsBase): void
140+
{
141+
$this->usingOriginalPriceAsBase = $usingOriginalPriceAsBase;
142+
}
143+
137144
public function getStartsAt(): ?DateTimeInterface
138145
{
139146
return $this->startsAt;

src/Model/CatalogPromotionInterface.php

+4-2
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ interface CatalogPromotionInterface extends ChannelsAwareInterface, CodeAwareInt
1515
{
1616
public function getId(): ?int;
1717

18-
public function getMultiplier(): float;
19-
2018
public function getName(): ?string;
2119

2220
public function setName(?string $name): void;
@@ -40,6 +38,10 @@ public function isManuallyDiscountedProductsExcluded(): bool;
4038

4139
public function setManuallyDiscountedProductsExcluded(bool $manuallyDiscountedProductsExcluded): void;
4240

41+
public function isUsingOriginalPriceAsBase(): bool;
42+
43+
public function setUsingOriginalPriceAsBase(bool $usingOriginalPriceAsBase): void;
44+
4345
public function getStartsAt(): ?DateTimeInterface;
4446

4547
public function setStartsAt(?DateTimeInterface $startsAt): void;

src/Resources/config/doctrine/model/CatalogPromotion.orm.xml

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
</field>
2020
<field name="exclusive" type="boolean"/>
2121
<field name="manuallyDiscountedProductsExcluded" type="boolean"/>
22+
<field name="usingOriginalPriceAsBase" type="boolean"/>
2223
<field name="startsAt" column="starts_at" type="datetime" nullable="true"/>
2324
<field name="endsAt" column="ends_at" type="datetime" nullable="true"/>
2425
<field name="enabled" type="boolean"/>

src/Resources/public/js/app.js

-9
This file was deleted.

src/Resources/translations/messages.en.yaml

+7-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ setono_sylius_catalog_promotion:
88
enabled: Enabled
99
ends_at: Ends at
1010
exclusive: Exclusive
11-
manually_discounted_products_excluded: Manually discounted products excluded
11+
manually_discounted_products_excluded: Exclude manually discounted products
12+
using_original_price_as_base: Use original price as base
1213
name: Name
1314
priority: Priority
1415
rules: Rules
@@ -50,6 +51,11 @@ setono_sylius_catalog_promotion:
5051
no_error: No error
5152
no_start_date: No start date
5253
pending: Pending
54+
using_original_price_as_base_information: |
55+
When calculating discounts, the discount is based on the existing price by default, however, you can choose to use the original price instead. For example, if a product is on sale for $80 (original price: $100) and a 30% discount is applied:<br><br>
56+
- Using the price as base (default): $80 - 30% = $56<br>
57+
- Using the original price as the base: $100 - 30% = $70.<br><br>
58+
<strong>NOTICE</strong> that if you check the "Use original price as base" all other catalog promotions applied to a product will also use the original price.
5359
processing: Processing
5460
products_updated: Products updated
5561
products_updated_less_than_estimated: The number of products updated is less than the estimated number of products to update. This may be due to the fact that some products were filtered/edited/removed after the initial processing started.

src/Resources/views/admin/_javascripts.html.twig

-1
This file was deleted.

src/Resources/views/admin/catalog_promotion/_form.html.twig

+10
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
{# @var resource \Setono\SyliusCatalogPromotionPlugin\Model\CatalogPromotionInterface #}
12
<div class="ui two column stackable grid">
23
<div class="column">
34
{{ form_errors(form) }}
@@ -15,6 +16,15 @@
1516
{{ form_row(form.priority) }}
1617
{{ form_row(form.exclusive) }}
1718
{{ form_row(form.manuallyDiscountedProductsExcluded) }}
19+
<div id="using-original-price-as-base-row" style="{{ resource.manuallyDiscountedProductsExcluded ? 'display: none' : '' }}">
20+
<div class="ui icon info message">
21+
<i class="info icon"></i>
22+
<div class="content">
23+
{{ 'setono_sylius_catalog_promotion.ui.using_original_price_as_base_information'|trans|raw }}
24+
</div>
25+
</div>
26+
{{ form_row(form.usingOriginalPriceAsBase) }}
27+
</div>
1828
</div>
1929
<div class="column">
2030
{{ form_row(form.channels) }}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<script>
2+
(function ($) {
3+
$('#rules a[data-form-collection="add"]').on('click', function () {
4+
setTimeout(function () {
5+
$('select[name^="setono_sylius_catalog_promotion__catalog_promotion[rules]"][name$="[type]"]').last().change();
6+
}, 50);
7+
});
8+
9+
$('#setono_sylius_catalog_promotion__catalog_promotion_manuallyDiscountedProductsExcluded').on('change', function () {
10+
$('#using-original-price-as-base-row').toggle(!$(this).is(':checked'));
11+
});
12+
})(jQuery);
13+
</script>

0 commit comments

Comments
 (0)