Skip to content

Commit

Permalink
feature #13349 [CartPromotion][CatalogPromotion] Receiving discount o…
Browse files Browse the repository at this point in the history
…nly on non discounted products (GSadee, SirDomin)

This PR was merged into the 1.11-dev branch.

Discussion
----------

| Q               | A
| --------------- | -----
| Branch?         | master
| Bug fix?        | no
| New feature?    | yes
| BC breaks?      | no
| Deprecations?   | no
| Related tickets | based on #13328
| License         | MIT

<!--
 - Bug fixes must be submitted against the 1.10 branch
 - Features and deprecations must be submitted against the master branch
 - Make sure that the correct base branch is set

 To be sure you are not breaking any Backward Compatibilities, check the documentation:
 https://docs.sylius.com/en/latest/book/organization/backward-compatibility-promise.html
-->


Commits
-------

4413793 [CartPromotion][CatalogPromotion][Behat] Add scenarios for receiving discount only on non discounted products
a65561e [CartPromotion][CatalogPromotion][Behat] Implement scenarios for receiving discount only on non discounted products
3556f47 [CartPromotion][CatalogPromotion][Behat] Fixes after rebase with changing the name of field
1f1e798 [CartPromotion][CatalogPromotion] Receiving discount only on non discounted products for unit cart promotions
372365b [WIP][CartPromotion][CatalogPromotion] Receiving discount only on non discounted products for fixed order cart promotions
e735927 [CartPromotion] handle fixed and percentage order discount
9ce4cb8 [MinimumPrice][ApplyOnDiscounted] merge solutions for apply on discount and minimum prices
  • Loading branch information
AdamKasp committed Dec 9, 2021
2 parents bf3699e + 9ce4cb8 commit 3ff3cf7
Show file tree
Hide file tree
Showing 23 changed files with 591 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,18 @@ Feature: Receiving discounts with product minimum price specified
And the "Keyboard" product should have unit price discounted by "$1.00"
And the "Mouse" product should have unit price discounted by "$0.00"
And the "Headphones" product should have unit price discounted by "$4.00"

@api
Scenario: Distributing discount proportionally between different products when one has minimum price specified and promotion does not apply on discounted products
Given this promotion does not apply on discounted products
And it gives "$27" discount to every order
And there is a catalog promotion "Fixed T-Shirt sale" that reduces price by fixed "$2.50" in the "United States" channel and applies on "PHP Mug" product
And I add 2 products "T-Shirt" to the cart
And I add 2 products "PHP Mug" to the cart
And I add product "Symfony Mug" to the cart
And I specified the billing address as "Ankh Morpork", "Frost Alley", "90210", "United States" for "Jon Snow"
When I proceed with "Free" shipping method and "Offline" payment
Then I should be on the checkout summary step
And the "T-Shirt" product should have unit prices discounted by "$5.00" and "$5.00"
And the "PHP Mug" product should have unit prices discounted by "$2.50" and "$2.50"
And the "Symfony Mug" product should have unit price discounted by "$17.00"
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
@receiving_discount
Feature: Receiving fixed discount from cart promotions only on non discounted products
In order not to combine cart and catalog promotions
As a Store Owner
I want to apply discount only on products that are non discounted

Background:
Given the store operates on a single channel in "United States"
And the store has a product "Mug" priced at "$40.00"
And the store has a product "T-Shirt" priced at "$20.00"
And the store has a product "Cap" priced at "$10.00"
And there is a catalog promotion "Winter sale" that reduces price by "25%" and applies on "T-Shirt" product

@ui @api
Scenario: Receiving product discount from cart promotions also on discounted products
Given there is a promotion "Christmas sale" that applies to discounted products
And this promotion gives "$10.00" off on every product priced between "$10.00" and "$50.00"
When the customer adds "T-Shirt" product to the cart
And the customer adds "Mug" product to the cart
Then the product "T-Shirt" should have discounted unit price "$5.00" in the cart
And the product "Mug" should have discounted unit price "$30.00" in the cart
And my cart total should be "$35.00"

@ui @api
Scenario: Receiving product discount from cart promotions only on non discounted products
Given there is a promotion "Christmas sale" that does not apply to discounted products
And this promotion gives "$10.00" off on every product priced between "$10.00" and "$50.00"
When the customer adds "T-Shirt" product to the cart
And the customer adds "Mug" product to the cart
Then the product "T-Shirt" should have discounted unit price "$15.00" in the cart
And the product "Mug" should have discounted unit price "$30.00" in the cart
And the cart total should be "$45.00"

@api
Scenario: Receiving order discount from cart promotions distributed only on non discounted products
Given there is a promotion "Christmas sale" that does not apply to discounted products
And this promotion gives "$10.00" discount to every order
When the customer adds "T-Shirt" product to the cart
And the customer adds "Mug" product to the cart
And the customer adds "Cap" product to the cart
Then the product "T-Shirt" should have discounted unit price "$15.00" in the cart
And the product "Mug" should have total price "$32.00" in the cart
And the product "Cap" should have total price "$8.00" in the cart
And the cart total should be "$55.00"
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
@receiving_discount
Feature: Receiving percentage discount from cart promotions only on non discounted products
In order not to combine cart and catalog promotions
As a Store Owner
I want to apply discount only on products that are non discounted

Background:
Given the store operates on a single channel in "United States"
And the store has a product "Mug" priced at "$40.00"
And the store has a product "T-Shirt" priced at "$20.00"
And the store has a product "Cap" priced at "$10.00"
And there is a catalog promotion "Winter sale" that reduces price by "25%" and applies on "T-Shirt" product

@ui @api
Scenario: Receiving product discount from cart promotions also on discounted products
Given there is a promotion "Christmas sale" that applies to discounted products
And this promotion gives "50%" off on every product priced between "$10.00" and "$50.00"
When the customer adds "T-Shirt" product to the cart
And the customer adds "Mug" product to the cart
Then the product "T-Shirt" should have discounted unit price "$7.50" in the cart
And the product "Mug" should have discounted unit price "$20.00" in the cart
And my cart total should be "$27.50"

@ui @api
Scenario: Receiving product discount from cart promotions only on non discounted products
Given there is a promotion "Christmas sale" that does not apply to discounted products
And this promotion gives "50%" off on every product priced between "$10.00" and "$50.00"
When the customer adds "T-Shirt" product to the cart
And the customer adds "Mug" product to the cart
Then the product "T-Shirt" should have discounted unit price "$15.00" in the cart
And the product "Mug" should have discounted unit price "$20.00" in the cart
And the cart total should be "$35.00"

@api
Scenario: Receiving order discount from cart promotions distributed only on non discounted products
Given there is a promotion "Christmas sale" that does not apply to discounted products
And this promotion gives "50%" discount to every order
When the customer adds "T-Shirt" product to the cart
And the customer adds "Mug" product to the cart
And the customer adds "Cap" product to the cart
Then the product "T-Shirt" should have discounted unit price "$15.00" in the cart
And the product "Mug" should have total price "$20.00" in the cart
And the product "Cap" should have total price "$5.00" in the cart
And the cart total should be "$40.00"
20 changes: 20 additions & 0 deletions src/Sylius/Behat/Context/Api/Shop/CartContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ public function myCartShouldBeCleared(): void
/**
* @Then /^my (cart)'s total should be ("[^"]+")$/
* @Then /^my (cart) total should be ("[^"]+")$/
* @Then /^the (cart) total should be ("[^"]+")$/
*/
public function myCartSTotalShouldBe(string $tokenValue, int $total): void
{
Expand Down Expand Up @@ -397,6 +398,7 @@ public function iShouldSeeProductWithUnitPriceInMyCart(string $productName, int

/**
* @Then /^I should see(?:| also) "([^"]+)" with discounted unit price ("[^"]+") in my cart$/
* @Then /^the product "([^"]+)" should have discounted unit price ("[^"]+") in the cart$/
*/
public function iShouldSeeProductWithDiscountedUnitPriceInMyCart(string $productName, int $discountedUnitPrice): void
{
Expand All @@ -413,6 +415,24 @@ public function iShouldSeeProductWithDiscountedUnitPriceInMyCart(string $product
throw new \InvalidArgumentException(sprintf('The product %s does not exist', $productName));
}

/**
* @Then /^the product "([^"]+)" should have total price ("[^"]+") in the cart$/
*/
public function theProductShouldHaveTotalPriceInTheCart(string $productName, int $totalPrice): void
{
$response = $this->cartsClient->getLastResponse();

foreach ($this->responseChecker->getValue($response, 'items') as $item) {
if ($item['productName'] === $productName) {
Assert::same($item['total'], $totalPrice);

return;
}
}

throw new \InvalidArgumentException(sprintf('The product %s does not exist', $productName));
}

/**
* @Then there should be one item in my cart
*/
Expand Down
24 changes: 24 additions & 0 deletions src/Sylius/Behat/Context/Setup/PromotionContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ public function __construct(

/**
* @Given there is (also) a promotion :name
* @Given there is a promotion :name that applies to discounted products
* @Given there is a promotion :name identified by :code code
*/
public function thereIsPromotion(string $name, ?string $code = null): void
Expand Down Expand Up @@ -174,6 +175,19 @@ public function thereIsPromotionWithCoupon(string $promotionName, string $coupon
$this->sharedStorage->set('coupon', $coupon);
}

/**
* @Given there is a promotion :name that does not apply to discounted products
*/
public function thereIsAPromotionThatDoesNotApplyToDiscountedProducts(string $name): void
{
$promotion = $this->createPromotion($name);
$promotion->setAppliesToDiscounted(false);

$this->promotionRepository->add($promotion);

$this->sharedStorage->set('promotion', $promotion);
}

/**
* @Given /^(this promotion) has "([^"]+)", "([^"]+)" and "([^"]+)" coupons/
*/
Expand All @@ -189,6 +203,16 @@ public function thisPromotionHasCoupons(PromotionInterface $promotion, string ..
$this->objectManager->flush();
}

/**
* @Given /^(this promotion) does not apply on discounted products$/
*/
public function thisPromotionDoesNotApplyOnDiscountedProducts(PromotionInterface $promotion): void
{
$promotion->setAppliesToDiscounted(false);

$this->objectManager->flush();
}

/**
* @Given /^(this promotion) has already expired$/
*/
Expand Down
14 changes: 12 additions & 2 deletions src/Sylius/Behat/Context/Ui/Shop/CartContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ public function iChangeQuantityTo($productName, $quantity)
/**
* @Then the grand total value should be :total
* @Then my cart total should be :total
* @Then the cart total should be :total
*/
public function myCartTotalShouldBe($total)
{
Expand Down Expand Up @@ -249,7 +250,7 @@ public function productPriceShouldNotBeDecreased(ProductInterface $product)
* @Given /^I have (product "[^"]+") added to the cart$/
* @Given I added product :product to the cart
* @Given /^I (?:have|had) (product "[^"]+") in the cart$/
* @Given the customer added :product product to the cart
* @Given /^the customer (?:added|adds) ("[^"]+" product) to the cart$/
* @Given /^I (?:add|added) ("[^"]+" product) to the (cart)$/
* @When I add product :product to the cart
*/
Expand Down Expand Up @@ -407,12 +408,21 @@ public function iShouldSeeWithQuantityInMyCart($productName, $quantity)
/**
* @Then /^I should see(?:| also) "([^"]+)" with unit price ("[^"]+") in my cart$/
* @Then /^I should see(?:| also) "([^"]+)" with discounted unit price ("[^"]+") in my cart$/
* @Then /^the product "([^"]+)" should have discounted unit price ("[^"]+") in the cart$/
*/
public function iShouldSeeProductWithUnitPriceInMyCart($productName, $unitPrice): void
public function iShouldSeeProductWithUnitPriceInMyCart(string $productName, int $unitPrice): void
{
Assert::same($this->summaryPage->getItemUnitPrice($productName), $unitPrice);
}

/**
* @Then /^the product "([^"]+)" should have total price ("[^"]+") in the cart$/
*/
public function theProductShouldHaveTotalPrice(string $productName, int $totalPrice): void
{
Assert::same($this->summaryPage->getItemTotal($productName), $totalPrice);
}

/**
* @Then /^I should see "([^"]+)" with original price ("[^"]+") in my cart$/
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ default:
- sylius.behat.context.setup.shipping
- sylius.behat.context.setup.taxonomy
- sylius.behat.context.setup.shop_api_security
- Sylius\Behat\Context\Setup\CatalogPromotionContext

- sylius.behat.context.api.shop.cart
- sylius.behat.context.api.shop.checkout
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ default:
- sylius.behat.context.setup.shop_security
- sylius.behat.context.setup.shipping
- sylius.behat.context.setup.taxonomy
- Sylius\Behat\Context\Setup\CatalogPromotionContext

- sylius.behat.context.ui.channel
- sylius.behat.context.ui.shop.cart
Expand Down
23 changes: 16 additions & 7 deletions src/Sylius/Component/Core/Distributor/MinimumPriceDistributor.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public function __construct(ProportionalIntegerDistributorInterface $proportiona
$this->proportionalIntegerDistributor = $proportionalIntegerDistributor;
}

public function distribute(array $orderItems, int $amount, ChannelInterface $channel): array
public function distribute(array $orderItems, int $amount, ChannelInterface $channel, bool $appliesOnDiscounted): array
{
Assert::allIsInstanceOf($orderItems, OrderItemInterface::class);

Expand All @@ -37,7 +37,6 @@ public function distribute(array $orderItems, int $amount, ChannelInterface $cha
$variant = $orderItem->getVariant();

$minimumPrice = $variant->getChannelPricingForChannel($channel)->getMinimumPrice();

$minimumPrice *= $orderItem->getQuantity();

$orderItemsToProcess['order-item-' . $index] = [
Expand All @@ -48,14 +47,14 @@ public function distribute(array $orderItems, int $amount, ChannelInterface $cha

return array_values(array_map(
function (array $processedOrderItem): int { return $processedOrderItem['promotion']; },
$this->processDistributionWithMinimumPrice($orderItemsToProcess, $amount, $channel)
$this->processDistributionWithMinimumPrice($orderItemsToProcess, $amount, $channel, $appliesOnDiscounted)
));
}

private function processDistributionWithMinimumPrice(array $orderItems, int $amount, $channel): array
private function processDistributionWithMinimumPrice(array $orderItems, int $amount, $channel, bool $appliesOnDiscounted): array
{
$totals = array_values(array_map(function (array $orderItemData): int {
return $orderItemData['orderItem']->getTotal();
$totals = array_values(array_map(function (array $orderItemData) use ($appliesOnDiscounted, $channel): int {
return $this->getTotalPrice($orderItemData['orderItem'], $appliesOnDiscounted, $channel);
}, $orderItems));

$promotionsToDistribute = array_combine(
Expand Down Expand Up @@ -92,7 +91,7 @@ private function processDistributionWithMinimumPrice(array $orderItems, int $amo
return $orderItems;
}

$nestedDistributions = $this->processDistributionWithMinimumPrice($distributableItems, $leftAmount, $channel);
$nestedDistributions = $this->processDistributionWithMinimumPrice($distributableItems, $leftAmount, $channel, $appliesOnDiscounted);

foreach ($nestedDistributions as $index => $distribution) {
$orderItems[$index]['promotion'] += $distribution['promotion'];
Expand All @@ -108,4 +107,14 @@ private function exceedsOrderItemMinimumPrice(
): bool {
return $minimumPriceAdjustedByCurrentDiscount >= ($orderItemTotal + $proposedPromotion);
}

private function getTotalPrice(OrderItemInterface $orderItem, bool $appliesOnDiscounted, ChannelInterface $channel): int
{
$variant = $orderItem->getVariant();
if ($appliesOnDiscounted === false && !empty($variant->getAppliedPromotionsForChannel($channel))) {
return 0;
}

return $orderItem->getTotal();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@

interface MinimumPriceDistributorInterface
{
public function distribute(array $orderItems, int $amount, ChannelInterface $channel): array;
public function distribute(array $orderItems, int $amount, ChannelInterface $channel, bool $appliesOnDiscounted): array;
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,15 @@ public function distribute(array $integers, int $amount): array
$distributedAmounts = [];

foreach ($integers as $element) {
$distributedAmounts[] = (int) round(($element * $amount) / $total, 0, \PHP_ROUND_HALF_DOWN);
if ($element === 0) {
$distributedAmounts[] = 0;
} else {
$distributedAmounts[] = (int) round(($element * $amount) / $total, 0, \PHP_ROUND_HALF_DOWN);
}
}

if(array_sum($distributedAmounts) === 0) {
return $distributedAmounts;
}

$missingAmount = $amount - array_sum($distributedAmounts);
Expand Down
18 changes: 16 additions & 2 deletions src/Sylius/Component/Core/Model/Order.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@
class Order extends BaseOrder implements OrderInterface
{
/**
* @var \Sylius\Component\Core\Model\CustomerInterface|null
* @var CustomerInterface|null
*/
protected $customer;

/**
* @var \Sylius\Component\Core\Model\ChannelInterface|null
* @var ChannelInterface|null
*/
protected $channel;

Expand Down Expand Up @@ -460,4 +460,18 @@ public function setCustomerIp(?string $customerIp): void
{
$this->customerIp = $customerIp;
}

public function getNonDiscountedItemsTotal(): int
{
$total = 0;
/** @var OrderItemInterface $item */
foreach ($this->items as $item) {
$variant = $item->getVariant();
if (empty($variant->getAppliedPromotionsForChannel($this->channel))) {
$total += $item->getTotal();
}
}

return $total;
}
}
2 changes: 2 additions & 0 deletions src/Sylius/Component/Core/Model/OrderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -126,4 +126,6 @@ public function getItems(): Collection;
* @return ChannelInterface|null
*/
public function getChannel(): ?BaseChannelInterface;

public function getNonDiscountedItemsTotal(): int;
}

0 comments on commit 3ff3cf7

Please sign in to comment.